'use strict' // Runs a set of tests for a given prefixed/unprefixed animation event (e.g. // animationstart/webkitAnimationStart). // // The eventDetails object must have the following form: // { // isTransition: false, <-- can be omitted, default false // unprefixedType: 'animationstart', // prefixedType: 'webkitAnimationStart', // animationCssStyle: '1ms', <-- must NOT include animation name or // transition property // } function runAnimationEventTests(eventDetails) { const { isTransition, unprefixedType, prefixedType, animationCssStyle } = eventDetails; // Derive the DOM event handler names, e.g. onanimationstart. const unprefixedHandler = `on${unprefixedType}`; const prefixedHandler = `on${prefixedType.toLowerCase()}`; const style = document.createElement('style'); document.head.appendChild(style); if (isTransition) { style.sheet.insertRule( `.baseStyle { width: 100px; transition: width ${animationCssStyle}; }`); style.sheet.insertRule('.transition { width: 200px !important; }'); } else { style.sheet.insertRule('@keyframes anim {}'); } function triggerAnimation(div) { if (isTransition) { div.classList.add('transition'); } else { div.style.animation = `anim ${animationCssStyle}`; } } test(t => { const div = createDiv(t); assert_equals(div[unprefixedHandler], null, `${unprefixedHandler} should initially be null`); assert_equals(div[prefixedHandler], null, `${prefixedHandler} should initially be null`); // Setting one should not affect the other. div[unprefixedHandler] = () => { }; assert_not_equals(div[unprefixedHandler], null, `setting ${unprefixedHandler} should make it non-null`); assert_equals(div[prefixedHandler], null, `setting ${unprefixedHandler} should not affect ${prefixedHandler}`); div[prefixedHandler] = () => { }; assert_not_equals(div[prefixedHandler], null, `setting ${prefixedHandler} should make it non-null`); assert_not_equals(div[unprefixedHandler], div[prefixedHandler], 'the setters should be different'); }, `${unprefixedHandler} and ${prefixedHandler} are not aliases`); // The below tests primarily test the interactions of prefixed animation // events in the algorithm for invoking events: // https://dom.spec.whatwg.org/#concept-event-listener-invoke promise_test(async t => { const div = createDiv(t); let receivedEventCount = 0; addTestScopedEventHandler(t, div, prefixedHandler, () => { receivedEventCount++; }); addTestScopedEventListener(t, div, prefixedType, () => { receivedEventCount++; }); // The HTML spec[0] specifies that the prefixed event handlers have an // 'Event handler event type' of the appropriate prefixed event type. E.g. // onwebkitanimationend creates a listener for the event type // 'webkitAnimationEnd'. // // [0]: https://html.spec.whatwg.org/multipage/webappapis.html#event-handlers-on-elements,-document-objects,-and-window-objects div.dispatchEvent(new AnimationEvent(prefixedType)); assert_equals(receivedEventCount, 2, 'prefixed listener and handler received event'); }, `dispatchEvent of a ${prefixedType} event does trigger a ` + `prefixed event handler or listener`); promise_test(async t => { const div = createDiv(t); let receivedEvent = false; addTestScopedEventHandler(t, div, unprefixedHandler, () => { receivedEvent = true; }); addTestScopedEventListener(t, div, unprefixedType, () => { receivedEvent = true; }); div.dispatchEvent(new AnimationEvent(prefixedType)); assert_false(receivedEvent, 'prefixed listener or handler received event'); }, `dispatchEvent of a ${prefixedType} event does not trigger an ` + `unprefixed event handler or listener`); promise_test(async t => { const div = createDiv(t); let receivedEvent = false; addTestScopedEventHandler(t, div, prefixedHandler, () => { receivedEvent = true; }); addTestScopedEventListener(t, div, prefixedType, () => { receivedEvent = true; }); // The rewrite rules from // https://dom.spec.whatwg.org/#concept-event-listener-invoke step 8 do not // apply because isTrusted will be false. div.dispatchEvent(new AnimationEvent(unprefixedType)); assert_false(receivedEvent, 'prefixed listener or handler received event'); }, `dispatchEvent of an ${unprefixedType} event does not trigger a ` + `prefixed event handler or listener`); promise_test(async t => { const div = createDiv(t); let receivedEvent = false; addTestScopedEventHandler(t, div, prefixedHandler, () => { receivedEvent = true; }); triggerAnimation(div); await waitForEventThenAnimationFrame(t, unprefixedType); assert_true(receivedEvent, `received ${prefixedHandler} event`); }, `${prefixedHandler} event handler should trigger for an animation`); promise_test(async t => { const div = createDiv(t); let receivedPrefixedEvent = false; addTestScopedEventHandler(t, div, prefixedHandler, () => { receivedPrefixedEvent = true; }); let receivedUnprefixedEvent = false; addTestScopedEventHandler(t, div, unprefixedHandler, () => { receivedUnprefixedEvent = true; }); triggerAnimation(div); await waitForEventThenAnimationFrame(t, unprefixedType); assert_true(receivedUnprefixedEvent, `received ${unprefixedHandler} event`); assert_false(receivedPrefixedEvent, `received ${prefixedHandler} event`); }, `${prefixedHandler} event handler should not trigger if an unprefixed ` + `event handler also exists`); promise_test(async t => { const div = createDiv(t); let receivedPrefixedEvent = false; addTestScopedEventHandler(t, div, prefixedHandler, () => { receivedPrefixedEvent = true; }); let receivedUnprefixedEvent = false; addTestScopedEventListener(t, div, unprefixedType, () => { receivedUnprefixedEvent = true; }); triggerAnimation(div); await waitForEventThenAnimationFrame(t, unprefixedHandler); assert_true(receivedUnprefixedEvent, `received ${unprefixedHandler} event`); assert_false(receivedPrefixedEvent, `received ${prefixedHandler} event`); }, `${prefixedHandler} event handler should not trigger if an unprefixed ` + `listener also exists`); promise_test(async t => { // We use a parent/child relationship to be able to register both prefixed // and unprefixed event handlers without the deduplication logic kicking in. const parent = createDiv(t); const child = createDiv(t); parent.appendChild(child); // After moving the child, we have to clean style again. getComputedStyle(child).transition; getComputedStyle(child).width; let observedUnprefixedType; addTestScopedEventHandler(t, parent, unprefixedHandler, e => { observedUnprefixedType = e.type; }); let observedPrefixedType; addTestScopedEventHandler(t, child, prefixedHandler, e => { observedPrefixedType = e.type; }); triggerAnimation(child); await waitForEventThenAnimationFrame(t, unprefixedType); assert_equals(observedUnprefixedType, unprefixedType); assert_equals(observedPrefixedType, prefixedType); }, `event types for prefixed and unprefixed ${unprefixedType} event ` + `handlers should be named appropriately`); promise_test(async t => { const div = createDiv(t); let receivedEvent = false; addTestScopedEventListener(t, div, prefixedType, () => { receivedEvent = true; }); triggerAnimation(div); await waitForEventThenAnimationFrame(t, unprefixedHandler); assert_true(receivedEvent, `received ${prefixedType} event`); }, `${prefixedType} event listener should trigger for an animation`); promise_test(async t => { const div = createDiv(t); let receivedPrefixedEvent = false; addTestScopedEventListener(t, div, prefixedType, () => { receivedPrefixedEvent = true; }); let receivedUnprefixedEvent = false; addTestScopedEventListener(t, div, unprefixedType, () => { receivedUnprefixedEvent = true; }); triggerAnimation(div); await waitForEventThenAnimationFrame(t, unprefixedHandler); assert_true(receivedUnprefixedEvent, `received ${unprefixedType} event`); assert_false(receivedPrefixedEvent, `received ${prefixedType} event`); }, `${prefixedType} event listener should not trigger if an unprefixed ` + `listener also exists`); promise_test(async t => { const div = createDiv(t); let receivedPrefixedEvent = false; addTestScopedEventListener(t, div, prefixedType, () => { receivedPrefixedEvent = true; }); let receivedUnprefixedEvent = false; addTestScopedEventHandler(t, div, unprefixedHandler, () => { receivedUnprefixedEvent = true; }); triggerAnimation(div); await waitForEventThenAnimationFrame(t, unprefixedHandler); assert_true(receivedUnprefixedEvent, `received ${unprefixedType} event`); assert_false(receivedPrefixedEvent, `received ${prefixedType} event`); }, `${prefixedType} event listener should not trigger if an unprefixed ` + `event handler also exists`); promise_test(async t => { // We use a parent/child relationship to be able to register both prefixed // and unprefixed event listeners without the deduplication logic kicking in. const parent = createDiv(t); const child = createDiv(t); parent.appendChild(child); // After moving the child, we have to clean style again. getComputedStyle(child).transition; getComputedStyle(child).width; let observedUnprefixedType; addTestScopedEventListener(t, parent, unprefixedType, e => { observedUnprefixedType = e.type; }); let observedPrefixedType; addTestScopedEventListener(t, child, prefixedType, e => { observedPrefixedType = e.type; }); triggerAnimation(child); await waitForEventThenAnimationFrame(t, unprefixedHandler); assert_equals(observedUnprefixedType, unprefixedType); assert_equals(observedPrefixedType, prefixedType); }, `event types for prefixed and unprefixed ${unprefixedType} event ` + `listeners should be named appropriately`); promise_test(async t => { const div = createDiv(t); let receivedEvent = false; addTestScopedEventListener(t, div, prefixedType.toLowerCase(), () => { receivedEvent = true; }); addTestScopedEventListener(t, div, prefixedType.toUpperCase(), () => { receivedEvent = true; }); triggerAnimation(div); await waitForEventThenAnimationFrame(t, unprefixedHandler); assert_false(receivedEvent, `received ${prefixedType} event`); }, `${prefixedType} event listener is case sensitive`); } // Below are utility functions. // Creates a div element, appends it to the document body and removes the // created element during test cleanup. function createDiv(test) { const element = document.createElement('div'); element.classList.add('baseStyle'); document.body.appendChild(element); test.add_cleanup(() => { element.remove(); }); // Flush style before returning. Some browsers only do partial style re-calc, // so ask for all important properties to make sure they are applied. getComputedStyle(element).transition; getComputedStyle(element).width; return element; } // Adds an event handler for |handlerName| (calling |callback|) to the given // |target|, that will automatically be cleaned up at the end of the test. function addTestScopedEventHandler(test, target, handlerName, callback) { assert_regexp_match( handlerName, /^on/, 'Event handler names must start with "on"'); assert_equals(target[handlerName], null, `${handlerName} must be supported and not previously set`); target[handlerName] = callback; // We need this cleaned up even if the event handler doesn't run. test.add_cleanup(() => { if (target[handlerName]) target[handlerName] = null; }); } // Adds an event listener for |type| (calling |callback|) to the given // |target|, that will automatically be cleaned up at the end of the test. function addTestScopedEventListener(test, target, type, callback) { target.addEventListener(type, callback); // We need this cleaned up even if the event handler doesn't run. test.add_cleanup(() => { target.removeEventListener(type, callback); }); } // Returns a promise that will resolve once the passed event (|eventName|) has // triggered and one more animation frame has happened. Automatically chooses // between an event handler or event listener based on whether |eventName| // begins with 'on'. // // We always listen on window as we don't want to interfere with the test via // triggering the prefixed event deduplication logic. function waitForEventThenAnimationFrame(test, eventName) { return new Promise((resolve, _) => { const eventFunc = eventName.startsWith('on') ? addTestScopedEventHandler : addTestScopedEventListener; eventFunc(test, window, eventName, () => { // rAF once to give the event under test time to come through. requestAnimationFrame(resolve); }); }); }