diff options
Diffstat (limited to 'testing/web-platform/tests/event-timing/resources')
3 files changed, 530 insertions, 0 deletions
diff --git a/testing/web-platform/tests/event-timing/resources/crossiframe-childframe.html b/testing/web-platform/tests/event-timing/resources/crossiframe-childframe.html new file mode 100644 index 0000000000..6a8bc6b642 --- /dev/null +++ b/testing/web-platform/tests/event-timing/resources/crossiframe-childframe.html @@ -0,0 +1,39 @@ +<!DOCType html> +<html> +<body> +<script src=event-timing-test-utils.js></script> +<div style="width: 300px; height: 300px" id='iframe_div' onmousedown="mainThreadBusy(120)"> +<script> +(async () => { + const observerPromise = new Promise(resolve => { + new PerformanceObserver(entryList => { + const mouseDowns = entryList.getEntriesByName('mousedown'); + if (mouseDowns.length === 0) + return; + resolve(mouseDowns); + }).observe({ type:'event' }); + }); + const entries = await observerPromise; + const clickDone = performance.now(); + if (entries.length !== 1) { + top.postMessage("failed", "*"); + return; + } + const entry = entries[0]; + top.postMessage({ + // Entry values (|entry| itself is not clonable) + "name": entry.name, + "cancelable": entry.cancelable, + "entryType": entry.entryType, + "startTime": entry.startTime, + "processingStart": entry.processingStart, + "processingEnd": entry.processingEnd, + "duration": entry.duration, + // Other values + "clickDone" : clickDone, + "target": entry.target.id, + }, '*'); +}) (); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/event-timing/resources/event-timing-test-utils.js b/testing/web-platform/tests/event-timing/resources/event-timing-test-utils.js new file mode 100644 index 0000000000..9670d8d26f --- /dev/null +++ b/testing/web-platform/tests/event-timing/resources/event-timing-test-utils.js @@ -0,0 +1,484 @@ +function mainThreadBusy(ms) { + const target = performance.now() + ms; + while (performance.now() < target); +} + +async function wait() { + return new Promise(resolve => step_timeout(resolve, 0)); +} + +async function raf() { + return new Promise(resolve => requestAnimationFrame(resolve)); +} + +async function afterNextPaint() { + await raf(); + await wait(); +} + +async function blockNextEventListener(target, eventType, duration = 120) { + return new Promise(resolve => { + target.addEventListener(eventType, () => { + mainThreadBusy(duration); + resolve(); + }, { once: true }); + }); +} + +async function clickAndBlockMain(id, options = {}) { + options = { + eventType: "pointerdown", + duration: 120, + ...options + }; + const element = document.getElementById(id); + + await Promise.all([ + blockNextEventListener(element, options.eventType, options.duration), + click(element), + ]); +} + + +// 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, event='pointerdown') { + verifyEvent(entry, event, targetId, isFirst, minDuration); +} + + + // 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 has duration greater than |slowDur|. + // * Asserting that exactly |numEntries| entries are received. + // Parameters: + // |t| - the test harness. + // |dur| - the durationThreshold for the PerformanceObserver. + // |id| - the ID of the element to be clicked. + // |numEntries| - the number of entries. + // |slowDur| - the min duration of a slow entry. +async function testDuration(t, id, numEntries, dur, 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 pointerDowns = list.getEntriesByName('pointerdown'); + pointerDowns.forEach(e => { + t.step(() => { + verifyClickEvent(e, id, false /* isFirst */, minDuration); + }); + }); + numEntriesReceived += pointerDowns.length; + // All the entries should be received since the slowDur is higher + // than the duration threshold. + if (numEntriesReceived === numEntries) + resolve(); + }).observe({type: "event", durationThreshold: dur}); + }); + const clicksPromise = new Promise(async resolve => { + for (let index = 0; index < numEntries; index++) { + // Add some click events that has at least slowDur for duration. + await clickAndBlockMain(id, { duration: slowDur }); + } + resolve(); + }); + return Promise.all([observerPromise, clicksPromise]); +} + + // Add a PerformanceObserver and observe with a durationThreshold of |durThreshold|. 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 |durThreshold|. + // * Issuing |numEntries| entries that have at least |processingDelay| as duration. + // * Asserting that the entries we receive has duration greater than or equals to the + // duration threshold we setup + // Parameters: + // |t| - the test harness. + // |id| - the ID of the element to be clicked. + // |durThreshold| - the durationThreshold for the PerformanceObserver. + // |numEntries| - the number of slow and number of fast entries. + // |processingDelay| - the event duration we add on each event. + async function testDurationWithDurationThreshold(t, id, numEntries, durThreshold, processingDelay) { + assert_implements(window.PerformanceEventTiming, 'Event Timing is not supported.'); + const observerPromise = new Promise(async resolve => { + let minDuration = Math.ceil(durThreshold / 8) * 8; + // Exposed events must always have a minimum duration of 16. + minDuration = Math.max(minDuration, 16); + new PerformanceObserver(t.step_func(list => { + const pointerDowns = list.getEntriesByName('pointerdown'); + pointerDowns.forEach(p => { + assert_greater_than_equal(p.duration, minDuration, + "The entry's duration should be greater than or equal to " + minDuration + " ms."); + }); + resolve(); + })).observe({type: "event", durationThreshold: durThreshold}); + }); + for (let index = 0; index < numEntries; index++) { + // These clicks are expected to be ignored, unless the test has some extra delays. + // In that case, the test will verify the event duration to ensure the event duration is + // greater than the duration threshold + await clickAndBlockMain(id, { duration: processingDelay }); + } + // Send click with event duration equals to or greater than |durThreshold|, so the + // observer promise can be resolved + await clickAndBlockMain(id, { duration: durThreshold }); + return observerPromise; + } + +// 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 if (eventType === 'keyup' || eventType === 'keydown') { + // Any key here as an input should work. + // TODO: Switch this to use test_driver.Actions.key{up,down} + // when test driver supports it. + // Please check crbug.com/893480. + const key = 'k'; + return test_driver.send_keys(target, key); + } 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', + 'keyup', + 'keydown' + ].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 afterNextPaint(); + 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 afterNextPaint(); + + await observerPromise; +} + +function addListeners(target, events) { + const eventListener = (e) => { + mainThreadBusy(200); + }; + events.forEach(e => { target.addEventListener(e, eventListener); }); +} + +// The testdriver.js, testdriver-vendor.js and testdriver-actions.js need to be +// included to use this function. +async function tap(target) { + return new test_driver.Actions() + .addPointer("touchPointer", "touch") + .pointerMove(0, 0, { origin: target }) + .pointerDown() + .pointerUp() + .send(); +} + +async function click(target) { + return test_driver.click(target); +} + +async function auxClick(target) { + const actions = new test_driver.Actions(); + return actions.addPointer("mousePointer", "mouse") + .pointerMove(0, 0, { origin: target }) + .pointerDown({ button: actions.ButtonType.RIGHT }) + .pointerUp({ button: actions.ButtonType.RIGHT }) + .send(); +} + +async function pointerdown(target) { + const actions = new test_driver.Actions(); + return actions.addPointer("mousePointer", "mouse") + .pointerMove(0, 0, { origin: target }) + .pointerDown() + .send(); +} + +async function auxPointerdown(target) { + const actions = new test_driver.Actions(); + return actions.addPointer("mousePointer", "mouse") + .pointerMove(0, 0, { origin: target }) + .pointerDown({ button: actions.ButtonType.RIGHT }) + .send(); +} + +// The testdriver.js, testdriver-vendor.js need to be included to use this +// function. +async function pressKey(target, key) { + await test_driver.send_keys(target, key); +} + +// The testdriver.js, testdriver-vendor.js need to be included to use this +// function. +async function addListenersAndPress(target, key, events) { + addListeners(target, events); + return pressKey(target, key); +} + +// The testdriver.js, testdriver-vendor.js need to be included to use this +// function. +async function addListenersAndClick(target) { + addListeners(target, + ['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'click']); + return click(target); +} + +function filterAndAddToMap(events, map) { + return function (entry) { + if (events.includes(entry.name)) { + map.set(entry.name, entry.interactionId); + return true; + } + return false; + } +} + +async function createPerformanceObserverPromise(observeTypes, callback, readyToResolve +) { + return new Promise(resolve => { + new PerformanceObserver(entryList => { + callback(entryList); + + if (readyToResolve()) { + resolve(); + } + }).observe({ entryTypes: observeTypes }); + }); +} + +// The testdriver.js, testdriver-vendor.js need to be included to use this +// function. +async function interactAndObserve(interactionType, target, observerPromise) { + let interactionPromise; + switch (interactionType) { + case 'tap': { + addListeners(target, ['pointerdown', 'pointerup']); + interactionPromise = tap(target); + break; + } + case 'click': { + addListeners(target, + ['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'click']); + interactionPromise = click(target); + break; + } + case 'auxclick': { + addListeners(target, + ['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'contextmenu', 'auxclick']); + interactionPromise = auxClick(target); + break; + } + case 'aux-pointerdown': { + addListeners(target, + ['mousedown', 'pointerdown', 'contextmenu']); + interactionPromise = auxPointerdown(target); + break; + } + case 'aux-pointerdown-and-pointerdown': { + addListeners(target, + ['mousedown', 'pointerdown', 'contextmenu']); + interactionPromise = Promise.all([auxPointerdown(target), pointerdown(target)]); + break; + } + } + return Promise.all([interactionPromise, observerPromise]); +} + +async function interact(interactionType, element, key = '') { + switch (interactionType) { + case 'click': { + return click(element); + } + case 'tap': { + return tap(element); + } + case 'key': { + return test_driver.send_keys(element, key); + } + } +} + +async function verifyInteractionCount(t, expectedCount) { + await t.step_wait(() => { + return performance.interactionCount >= expectedCount; + }, 'interactionCount did not increase enough', 10000, 5); + assert_equals(performance.interactionCount, expectedCount, + 'interactionCount increased more than expected'); +} + +function interactionCount_test(interactionType, elements, key = '') { + return promise_test(async t => { + assert_implements(window.PerformanceEventTiming, + 'Event Timing is not supported'); + assert_equals(performance.interactionCount, 0, 'Initial count is not 0'); + + let expectedCount = 1; + for (let element of elements) { + await interact(interactionType, element, key); + await verifyInteractionCount(t, expectedCount++); + } + }, `EventTiming: verify interactionCount for ${interactionType} interaction`); +} diff --git a/testing/web-platform/tests/event-timing/resources/slow-image.py b/testing/web-platform/tests/event-timing/resources/slow-image.py new file mode 100644 index 0000000000..c2f91655cf --- /dev/null +++ b/testing/web-platform/tests/event-timing/resources/slow-image.py @@ -0,0 +1,7 @@ +import time + +def main(request, response): + # Sleep for 500ms to delay onload. + time.sleep(0.5) + response.headers.set(b"Cache-Control", b"no-cache, must-revalidate"); + response.headers.set(b"Location", b"data:image/gif;base64,R0lGODlhAQABAJAAAMjIyAAAACwAAAAAAQABAAACAgQBADs%3D"); |