| <!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> |