| <!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'); |
| |
| promise_test(async t => { |
| // Setup a hierarchy as follows: |
| // |
| // parent |
| // | |
| // (::marker, ::before, ::after) |
| // | |
| // 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 (CSS.supports('selector(::marker)')) { |
| 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 (!CSS.supports('selector(::marker)')) { |
| expectedEvents.splice(1, 1); |
| } |
| |
| checkEvents(events, ...expectedEvents); |
| }, 'Same events on pseudo-elements follow the prescribed order'); |
| |
| 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> |