<!doctype html> <meta charset=utf-8> <title>Tests for CSS animation event order</title> <link rel="help" href="https://drafts.csswg.org/css-animations-2/#event-dispatch"/> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="support/testcommon.js"></script> <style> @keyframes anim { from { margin-left: 0px; } to { margin-left: 100px; } } @keyframes color-anim { from { color: red; } to { color: green; } } </style> <div id="log"></div> <script type='text/javascript'> 'use strict'; /** * Asserts that the set of actual and received events match. * @param actualEvents An array of the received AnimationEvent objects. * @param expectedEvents A series of array objects representing the expected * events, each having the form: * [ event type, target element, [pseudo type], elapsed time ] */ const checkEvents = (actualEvents, ...expectedEvents) => { const actualTypeSummary = actualEvents.map(event => event.type).join(', '); const expectedTypeSummary = expectedEvents.map(event => event[0]).join(', '); assert_equals( actualEvents.length, expectedEvents.length, `Number of events received (${actualEvents.length}) \ should match expected number (${expectedEvents.length}) \ (expected: ${expectedTypeSummary}, actual: ${actualTypeSummary})` ); for (const [index, actualEvent] of actualEvents.entries()) { const expectedEvent = expectedEvents[index]; const [type, target] = expectedEvent; const pseudoElement = expectedEvent.length === 4 ? expectedEvent[2] : ''; const elapsedTime = expectedEvent[expectedEvent.length - 1]; assert_equals( actualEvent.type, type, `Event #${index + 1} types should match \ (expected: ${expectedTypeSummary}, actual: ${actualTypeSummary})` ); assert_equals( actualEvent.target, target, `Event #${index + 1} targets should match` ); assert_equals( actualEvent.pseudoElement, pseudoElement, `Event #${index + 1} pseudoElements should match` ); assert_equals( actualEvent.elapsedTime, elapsedTime, `Event #${index + 1} elapsedTimes should match` ); } }; const setupAnimation = (t, animationStyle, receiveEvents) => { const div = addDiv(t, { style: 'animation: ' + animationStyle }); for (const name of ['start', 'iteration', 'end']) { div['onanimation' + name] = evt => { receiveEvents.push({ type: evt.type, target: evt.target, pseudoElement: evt.pseudoElement, elapsedTime: evt.elapsedTime, }); }; } const watcher = new EventWatcher(t, div, [ 'animationstart', 'animationiteration', 'animationend', ]); const animation = div.getAnimations()[0]; return [animation, watcher, div]; }; promise_test(async t => { let events = []; const [animation1, watcher1, div1] = setupAnimation(t, 'anim 100s 2 paused', events); const [animation2, watcher2, div2] = setupAnimation(t, 'anim 100s 2 paused', events); await Promise.all([ watcher1.wait_for('animationstart'), watcher2.wait_for('animationstart') ]); checkEvents(events, ['animationstart', div1, 0], ['animationstart', div2, 0]); events.length = 0; // Clear received event array animation1.currentTime = 100 * MS_PER_SEC; animation2.currentTime = 100 * MS_PER_SEC; await Promise.all([ watcher1.wait_for('animationiteration'), watcher2.wait_for('animationiteration') ]); checkEvents(events, ['animationiteration', div1, 100], ['animationiteration', div2, 100]); events.length = 0; // Clear received event array animation1.finish(); animation2.finish(); await Promise.all([ watcher1.wait_for('animationend'), watcher2.wait_for('animationend') ]); checkEvents(events, ['animationend', div1, 200], ['animationend', div2, 200]); }, 'Same events are ordered by elements'); function pseudoTest(description, testMarkerPseudos) { promise_test(async t => { // Setup a hierarchy as follows: // // parent // | // (::marker, ::before, ::after) // ::marker optional // | // child const parentDiv = addDiv(t, { style: 'animation: anim 100s' }); parentDiv.id = 'parent-div'; addStyle(t, { '#parent-div::after': "content: ''; animation: anim 100s", '#parent-div::before': "content: ''; animation: anim 100s", }); if (testMarkerPseudos) { parentDiv.style.display = 'list-item'; addStyle(t, { '#parent-div::marker': "content: ''; animation: color-anim 100s", }); } const childDiv = addDiv(t, { style: 'animation: anim 100s' }); parentDiv.append(childDiv); // Setup event handlers let events = []; for (const name of ['start', 'iteration', 'end', 'cancel']) { parentDiv['onanimation' + name] = evt => { events.push({ type: evt.type, target: evt.target, pseudoElement: evt.pseudoElement, elapsedTime: evt.elapsedTime, }); }; } // Wait a couple of frames for the events to be dispatched await waitForFrame(); await waitForFrame(); const expectedEvents = [ ['animationstart', parentDiv, 0], ['animationstart', parentDiv, '::marker', 0], ['animationstart', parentDiv, '::before', 0], ['animationstart', parentDiv, '::after', 0], ['animationstart', childDiv, 0], ]; if (!testMarkerPseudos) { expectedEvents.splice(1, 1); } checkEvents(events, ...expectedEvents); }, description); } pseudoTest('Same events on pseudo-elements follow the prescribed order', false); pseudoTest('Same events on pseudo-elements follow the prescribed order ' + '(::marker)', true); promise_test(async t => { let events = []; const [animation1, watcher1, div1] = setupAnimation(t, 'anim 200s 400s', events); const [animation2, watcher2, div2] = setupAnimation(t, 'anim 300s 2', events); await watcher2.wait_for('animationstart'); animation1.currentTime = 400 * MS_PER_SEC; animation2.currentTime = 400 * MS_PER_SEC; events.length = 0; // Clear received event array await Promise.all([ watcher1.wait_for('animationstart'), watcher2.wait_for('animationiteration') ]); checkEvents(events, ['animationiteration', div2, 300], ['animationstart', div1, 0]); }, 'Start and iteration events are ordered by time'); promise_test(async t => { let events = []; const [animation1, watcher1, div1] = setupAnimation(t, 'anim 150s', events); const [animation2, watcher2, div2] = setupAnimation(t, 'anim 100s 2', events); await Promise.all([ watcher1.wait_for('animationstart'), watcher2.wait_for('animationstart') ]); animation1.currentTime = 150 * MS_PER_SEC; animation2.currentTime = 150 * MS_PER_SEC; events.length = 0; // Clear received event array await Promise.all([ watcher1.wait_for('animationend'), watcher2.wait_for('animationiteration') ]); checkEvents(events, ['animationiteration', div2, 100], ['animationend', div1, 150]); }, 'Iteration and end events are ordered by time'); promise_test(async t => { let events = []; const [animation1, watcher1, div1] = setupAnimation(t, 'anim 100s 100s', events); const [animation2, watcher2, div2] = setupAnimation(t, 'anim 100s 2', events); animation1.finish(); animation2.finish(); await Promise.all([ watcher1.wait_for([ 'animationstart', 'animationend' ]), watcher2.wait_for([ 'animationstart', 'animationend' ]) ]); checkEvents(events, ['animationstart', div2, 0], ['animationstart', div1, 0], ['animationend', div1, 100], ['animationend', div2, 200]); }, 'Start and end events are sorted correctly when fired simultaneously'); </script>