blob: 70fd66abfbcb22a62c699dba4466f76a539729ed [file] [log] [blame]
// Clicks on the element with the given ID. It adds an event handler to the element which
// ensures that the events have a duration of at least |delay|. Calls |callback| during
// event handler if |callback| is provided.
async function clickOnElementAndDelay(id, delay, callback) {
const element = document.getElementById(id);
const clickHandler = () => {
mainThreadBusy(delay);
if (callback)
callback();
element.removeEventListener("mousedown", clickHandler);
};
element.addEventListener("mousedown", clickHandler);
await test_driver.click(element);
}
function mainThreadBusy(duration) {
const now = performance.now();
while (performance.now() < now + duration);
}
// This method should receive an entry of type 'event'. |isFirst| is true only when we want
// to check that the event also happens to correspond to the first event. In this case, the
// timings of the 'first-input' entry should be equal to those of this entry. |minDuration|
// is used to compared against entry.duration.
function verifyEvent(entry, eventType, targetId, isFirst=false, minDuration=104, notCancelable=false) {
assert_equals(entry.cancelable, !notCancelable, 'cancelable property');
assert_equals(entry.name, eventType);
assert_equals(entry.entryType, 'event');
assert_greater_than_equal(entry.duration, minDuration,
"The entry's duration should be greater than or equal to " + minDuration + " ms.");
assert_greater_than_equal(entry.processingStart, entry.startTime,
"The entry's processingStart should be greater than or equal to startTime.");
assert_greater_than_equal(entry.processingEnd, entry.processingStart,
"The entry's processingEnd must be at least as large as processingStart.");
// |duration| is a number rounded to the nearest 8 ms, so add 4 to get a lower bound
// on the actual duration.
assert_greater_than_equal(entry.duration + 4, entry.processingEnd - entry.startTime,
"The entry's duration must be at least as large as processingEnd - startTime.");
if (isFirst) {
let firstInputs = performance.getEntriesByType('first-input');
assert_equals(firstInputs.length, 1, 'There should be a single first-input entry');
let firstInput = firstInputs[0];
assert_equals(firstInput.name, entry.name);
assert_equals(firstInput.entryType, 'first-input');
assert_equals(firstInput.startTime, entry.startTime);
assert_equals(firstInput.duration, entry.duration);
assert_equals(firstInput.processingStart, entry.processingStart);
assert_equals(firstInput.processingEnd, entry.processingEnd);
assert_equals(firstInput.cancelable, entry.cancelable);
}
if (targetId)
assert_equals(entry.target, document.getElementById(targetId));
}
function verifyClickEvent(entry, targetId, isFirst=false, minDuration=104) {
verifyEvent(entry, 'mousedown', targetId, isFirst, minDuration);
}
function wait() {
return new Promise((resolve, reject) => {
step_timeout(() => {
resolve();
}, 0);
});
}
function clickAndBlockMain(id) {
return new Promise((resolve, reject) => {
clickOnElementAndDelay(id, 120, resolve);
});
}
function waitForTick() {
return new Promise(resolve => {
window.requestAnimationFrame(() => {
window.requestAnimationFrame(resolve);
});
});
}
// Add a PerformanceObserver and observe with a durationThreshold of |dur|. This test will
// attempt to check that the duration is appropriately checked by:
// * Asserting that entries received have a duration which is the smallest multiple of 8
// that is greater than or equal to |dur|.
// * Issuing |numEntries| entries that are fast, of duration |slowDur|.
// * Issuing |numEntries| entries that are slow, of duration |fastDur|.
// * Asserting that at least |numEntries| entries are received (at least the slow ones).
// Parameters:
// |t| - the test harness.
// |dur| - the durationThreshold for the PerformanceObserver.
// |id| - the ID of the element to be clicked.
// |numEntries| - the number of slow and number of fast entries.
// |slowDur| - the min duration of a slow entry.
// |fastDur| - the min duration of a fast entry.
async function testDuration(t, id, numEntries, dur, fastDur, slowDur) {
assert_implements(window.PerformanceEventTiming, 'Event Timing is not supported.');
const observerPromise = new Promise(async resolve => {
let minDuration = Math.ceil(dur / 8) * 8;
// Exposed events must always have a minimum duration of 16.
minDuration = Math.max(minDuration, 16);
let numEntriesReceived = 0;
new PerformanceObserver(list => {
const mouseDowns = list.getEntriesByName('mousedown');
mouseDowns.forEach(e => {
t.step(() => {
verifyClickEvent(e, id, false /* isFirst */, minDuration);
});
});
numEntriesReceived += mouseDowns.length;
// Note that we may receive more entries if the 'fast' click events turn out slower
// than expected.
if (numEntriesReceived >= numEntries)
resolve();
}).observe({type: "event", durationThreshold: dur});
});
const clicksPromise = new Promise(async resolve => {
for (let index = 0; index < numEntries; index++) {
// Add some fast click events.
await clickOnElementAndDelay(id, slowDur);
// Add some slow click events.
if (fastDur > 0) {
await clickOnElementAndDelay(id, fastDur);
} else {
// We can just directly call test_driver when |fastDur| is 0.
await test_driver.click(document.getElementById(id));
}
}
resolve();
});
return Promise.all([observerPromise, clicksPromise]);
}
// Apply events that trigger an event of the given |eventType| to be dispatched to the
// |target|. Some of these assume that the target is not on the top left corner of the
// screen, which means that (0, 0) of the viewport is outside of the |target|.
function applyAction(eventType, target) {
const actions = new test_driver.Actions();
if (eventType === 'auxclick') {
actions.pointerMove(0, 0, {origin: target})
.pointerDown({button: actions.ButtonType.MIDDLE})
.pointerUp({button: actions.ButtonType.MIDDLE});
} else if (eventType === 'click' || eventType === 'mousedown' || eventType === 'mouseup'
|| eventType === 'pointerdown' || eventType === 'pointerup'
|| eventType === 'touchstart' || eventType === 'touchend') {
actions.pointerMove(0, 0, {origin: target})
.pointerDown()
.pointerUp();
} else if (eventType === 'contextmenu') {
actions.pointerMove(0, 0, {origin: target})
.pointerDown({button: actions.ButtonType.RIGHT})
.pointerUp({button: actions.ButtonType.RIGHT});
} else if (eventType === 'dblclick') {
actions.pointerMove(0, 0, {origin: target})
.pointerDown()
.pointerUp()
.pointerDown()
.pointerUp()
// Reset by clicking outside of the target.
.pointerMove(0, 0)
.pointerDown()
} else if (eventType === 'mouseenter' || eventType === 'mouseover'
|| eventType === 'pointerenter' || eventType === 'pointerover') {
// Move outside of the target and then back inside.
// Moving it to 0, 1 because 0, 0 doesn't cause the pointer to
// move in Firefox. See https://github.com/w3c/webdriver/issues/1545
actions.pointerMove(0, 1)
.pointerMove(0, 0, {origin: target});
} else if (eventType === 'mouseleave' || eventType === 'mouseout'
|| eventType === 'pointerleave' || eventType === 'pointerout') {
actions.pointerMove(0, 0, {origin: target})
.pointerMove(0, 0);
} else {
assert_unreached('The event type ' + eventType + ' is not supported.');
}
return actions.send();
}
function requiresListener(eventType) {
return ['mouseenter',
'mouseleave',
'pointerdown',
'pointerenter',
'pointerleave',
'pointerout',
'pointerover',
'pointerup'
].includes(eventType);
}
function notCancelable(eventType) {
return ['mouseenter', 'mouseleave', 'pointerenter', 'pointerleave'].includes(eventType);
}
// Tests the given |eventType|'s performance.eventCounts value. Since this is populated only when
// the event is processed, we check every 10 ms until we've found the |expectedCount|.
function testCounts(t, resolve, looseCount, eventType, expectedCount) {
const counts = performance.eventCounts.get(eventType);
if (counts < expectedCount) {
t.step_timeout(() => {
testCounts(t, resolve, looseCount, eventType, expectedCount);
}, 10);
return;
}
if (looseCount) {
assert_greater_than_equal(performance.eventCounts.get(eventType), expectedCount,
`Should have at least ${expectedCount} ${eventType} events`)
} else {
assert_equals(performance.eventCounts.get(eventType), expectedCount,
`Should have ${expectedCount} ${eventType} events`);
}
resolve();
}
// Tests the given |eventType| by creating events whose target are the element with id
// 'target'. The test assumes that such element already exists. |looseCount| is set for
// eventTypes for which events would occur for other interactions other than the ones being
// specified for the target, so the counts could be larger.
async function testEventType(t, eventType, looseCount=false) {
assert_implements(window.EventCounts, "Event Counts isn't supported");
const target = document.getElementById('target');
if (requiresListener(eventType)) {
target.addEventListener(eventType, () =>{});
}
const initialCount = performance.eventCounts.get(eventType);
if (!looseCount) {
assert_equals(initialCount, 0, 'No events yet.');
}
// Trigger two 'fast' events of the type.
await applyAction(eventType, target);
await applyAction(eventType, target);
await waitForTick();
await new Promise(t.step_func(resolve => {
testCounts(t, resolve, looseCount, eventType, initialCount + 2);
}));
// The durationThreshold used by the observer. A slow events needs to be slower than that.
const durationThreshold = 16;
// Now add an event handler to cause a slow event.
target.addEventListener(eventType, () => {
mainThreadBusy(durationThreshold + 4);
});
const observerPromise = new Promise(async resolve => {
new PerformanceObserver(t.step_func(entryList => {
let eventTypeEntries = entryList.getEntriesByName(eventType);
if (eventTypeEntries.length === 0)
return;
let entry = null;
if (!looseCount) {
entry = eventTypeEntries[0];
assert_equals(eventTypeEntries.length, 1);
} else {
// The other events could also be considered slow. Find the one with the correct
// target.
eventTypeEntries.forEach(e => {
if (e.target === document.getElementById('target'))
entry = e;
});
if (!entry)
return;
}
verifyEvent(entry,
eventType,
'target',
false /* isFirst */,
durationThreshold,
notCancelable(eventType));
// Shouldn't need async testing here since we already got the observer entry, but might as
// well reuse the method.
testCounts(t, resolve, looseCount, eventType, initialCount + 3);
})).observe({type: 'event', durationThreshold: durationThreshold});
});
// Cause a slow event.
await applyAction(eventType, target);
await waitForTick();
await observerPromise;
}