Bug 1545707 - Add tests for animation of ::marker pseudo-elements; r=mats
authorBrian Birtles <birtles@gmail.com>
Mon, 22 Apr 2019 00:54:09 +0000
changeset 470326 bc8f94bb6bb6dad3f71b1ac955f1d89643548013
parent 470325 6ccf33c9e56e44937858a86890a65c4296861d98
child 470327 593761a810cea50aebb2b9a423af791c773c10ac
push id112863
push usershindli@mozilla.com
push dateMon, 22 Apr 2019 09:53:25 +0000
treeherdermozilla-inbound@ab1da7fa2ad0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmats
bugs1545707
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1545707 - Add tests for animation of ::marker pseudo-elements; r=mats In particular, this tests the composite order of ::marker pseudo-elements and dispatching cancel events for these elements. It adds one test that is known to fail in Gecko. This will be fixed in the next patch in this series. Differential Revision: https://phabricator.services.mozilla.com/D28175
testing/web-platform/meta/css/css-transitions/non-rendered-element-002.html.ini
testing/web-platform/tests/css/css-animations/Document-getAnimations.tentative.html
testing/web-platform/tests/css/css-animations/event-order.tentative.html
testing/web-platform/tests/css/css-transitions/Document-getAnimations.tentative.html
testing/web-platform/tests/css/css-transitions/non-rendered-element-002.html
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/css/css-transitions/non-rendered-element-002.html.ini
@@ -0,0 +1,4 @@
+[non-rendered-element-002.html]
+  expected: TIMEOUT
+  [Transitions on ::marker pseudo-elements are canceled when the parent display type is no longer list-item]
+    expected: TIMEOUT
--- a/testing/web-platform/tests/css/css-animations/Document-getAnimations.tentative.html
+++ b/testing/web-platform/tests/css/css-animations/Document-getAnimations.tentative.html
@@ -13,22 +13,16 @@
   to { top: 100px }
 }
 @keyframes animBottom {
   to { bottom: 100px }
 }
 @keyframes animRight {
   to { right: 100px }
 }
-::before {
-  content: ''
-}
-::after {
-  content: ''
-}
 </style>
 <div id="log"></div>
 <script>
 'use strict';
 
 test(t => {
   assert_equals(document.getAnimations().length, 0,
     'getAnimations returns an empty sequence for a document'
@@ -246,39 +240,82 @@ test(t => {
   anim.cancel();
   anim.play();
   assert_equals(document.getAnimations().length, 1,
                 'CSS animations canceled and restarted by the API are ' +
                 'returned');
 }, 'CSS Animations canceled and restarted via the API are returned');
 
 test(t => {
-  addStyle(t, { '#parent::after': 'animation: animLeft 10s;',
-                '#parent::before': 'animation: animRight 10s;' });
-  // create two divs with these arrangement:
+  // Create two divs with the following arrangement:
+  //
   //       parent
+  //    (::marker,)
   //     ::before,
   //     ::after
   //        |
   //       child
-  const parent = addDiv(t, { 'id': 'parent' });
+
+  addStyle(t, {
+    '#parent::after': "content: ''; animation: animLeft 100s;",
+    '#parent::before': "content: ''; animation: animRight 100s;",
+  });
+
+  const supportsMarkerPseudos = CSS.supports('selector(::marker)');
+  if (supportsMarkerPseudos) {
+    addStyle(t, {
+      '#parent': 'display: list-item;',
+      '#parent::marker': "content: ''; animation: animLeft 100s;",
+    });
+  }
+
+  const parent = addDiv(t, { id: 'parent' });
   const child = addDiv(t);
   parent.appendChild(child);
   for (const div of [parent, child]) {
-    div.setAttribute('style', 'animation: animBottom 10s');
+    div.setAttribute('style', 'animation: animBottom 100s');
+  }
+
+  const expectedAnimations = [
+    [parent, undefined],
+    [parent, '::marker'],
+    [parent, '::before'],
+    [parent, '::after'],
+    [child, undefined],
+  ];
+  if (!supportsMarkerPseudos) {
+    expectedAnimations.splice(1, 1);
   }
 
-  const anims = document.getAnimations();
-  assert_equals(anims.length, 4,
-                'CSS animations on both pseudo-elements and elements ' +
-                'are returned');
-  assert_equals(anims[0].effect.target, parent,
-                'The animation targeting the parent element comes first');
-  assert_equals(anims[1].effect.target.type, '::before',
-                'The animation targeting the ::before element comes second');
-  assert_equals(anims[2].effect.target.type, '::after',
-                'The animation targeting the ::after element comes third');
-  assert_equals(anims[3].effect.target, child,
-                'The animation targeting the child element comes last');
-}, 'CSS Animations targetting (pseudo-)elements should have correct order ' +
-   'after sorting');
+  const animations = document.getAnimations();
+  assert_equals(
+    animations.length,
+    expectedAnimations.length,
+    'CSS animations on both pseudo-elements and elements are returned'
+  );
+
+  for (const [index, expected] of expectedAnimations.entries()) {
+    const [element, pseudo] = expected;
+    const actual = animations[index];
+
+    if (pseudo) {
+      assert_equals(
+        actual.effect.target.element,
+        element,
+        `Animation #${index + 1} has expected target`
+      );
+      assert_equals(
+        actual.effect.target.type,
+        pseudo,
+        `Animation #${index + 1} has expected pseudo type`
+      );
+    } else {
+      assert_equals(
+        actual.effect.target,
+        element,
+        `Animation #${index + 1} has expected target`
+      );
+    }
+  }
+}, 'CSS Animations targetting (pseudo-)elements should have correct order '
+   + 'after sorting');
 
 </script>
--- a/testing/web-platform/tests/css/css-animations/event-order.tentative.html
+++ b/testing/web-platform/tests/css/css-animations/event-order.tentative.html
@@ -1,62 +1,97 @@
 <!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 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, elapsed time ]
+ *          [ event type, target element, [pseudo type], elapsed time ]
  */
 const checkEvents = (actualEvents, ...expectedEvents) => {
-  assert_equals(actualEvents.length, expectedEvents.length,
-                `Number of actual events (${actualEvents.length}: \
-${actualEvents.map(event => event.type).join(', ')}) should match expected \
-events (${expectedEvents.map(event => event.type).join(', ')})`);
+  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];
 
-  actualEvents.forEach((actualEvent, i) => {
-    assert_equals(expectedEvents[i][0], actualEvent.type,
-                  'Event type should match');
-    assert_equals(expectedEvents[i][1], actualEvent.target,
-                  'Event target should match');
-    assert_equals(expectedEvents[i][2], actualEvent.elapsedTime,
-                  'Event\'s elapsed time should match');
-  });
+    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 });
+  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,
-                         elapsedTime: evt.elapsedTime });
+      receiveEvents.push({
+        type: evt.type,
+        target: evt.target,
+        pseudoElement: evt.pseudoElement,
+        elapsedTime: evt.elapsedTime,
+      });
     };
   }
 
-  const watcher = new EventWatcher(t, div, [ 'animationstart',
-                                             'animationiteration',
-                                             'animationend' ]);
+  const watcher = new EventWatcher(t, div, [
+    'animationstart',
+    'animationiteration',
+    'animationend',
+  ]);
 
   const animation = div.getAnimations()[0];
 
   return [animation, watcher, div];
 };
 
 promise_test(async t => {
   let events = [];
@@ -87,17 +122,74 @@ promise_test(async t => {
   animation1.finish();
   animation2.finish();
 
   await Promise.all([ watcher1.wait_for('animationend'),
                       watcher2.wait_for('animationend') ]);
 
   checkEvents(events, ['animationend', div1, 200],
                       ['animationend', div2, 200]);
-}, 'Test same events are ordered by elements.');
+}, '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);
 
@@ -108,17 +200,17 @@ promise_test(async t => {
 
   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]);
-}, 'Test start and iteration events are ordered by time.');
+}, '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);
 
@@ -130,17 +222,17 @@ promise_test(async t => {
 
   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]);
-}, 'Test iteration and end events are ordered by time.');
+}, '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);
 
@@ -151,11 +243,11 @@ promise_test(async t => {
                                           'animationend' ]),
                       watcher2.wait_for([ 'animationstart',
                                           'animationend' ]) ]);
 
   checkEvents(events, ['animationstart', div2, 0],
                       ['animationstart', div1, 0],
                       ['animationend',   div1, 100],
                       ['animationend',   div2, 200]);
-}, 'Test start and end events are sorted correctly when fired simultaneously');
+}, 'Start and end events are sorted correctly when fired simultaneously');
 
 </script>
--- a/testing/web-platform/tests/css/css-transitions/Document-getAnimations.tentative.html
+++ b/testing/web-platform/tests/css/css-transitions/Document-getAnimations.tentative.html
@@ -31,57 +31,98 @@ test(t => {
 
   // Remove both
   div.style.transitionProperty = 'none';
   assert_equals(document.getAnimations().length, 0,
                 'getAnimations returns no running CSS Transitions');
 }, 'getAnimations for CSS Transitions');
 
 test(t => {
-  addStyle(t, { '.init::after': 'content: ""; width: 0px; ' +
-                                'transition: all 100s;',
-                '.init::before': 'content: ""; width: 0px; ' +
-                                 'transition: all 10s;',
-                '.change::after': 'width: 100px;',
-                '.change::before': 'width: 100px;' });
-  // create two divs with these arrangement:
+  // Create two divs with the following arrangement:
+  //
   //       parent
+  //    (::marker,)
   //     ::before,
   //     ::after
   //        |
   //       child
-  const parent = addDiv(t);
+
+  addStyle(t, {
+    '.init::after': 'content: ""; width: 0px; transition: all 100s;',
+    '.init::before': 'content: ""; width: 0px; transition: all 100s;',
+    '.change::after': 'width: 100px;',
+    '.change::before': 'width: 100px;',
+  });
+
+  const supportsMarkerPseudos = CSS.supports('selector(::marker)');
+  if (supportsMarkerPseudos) {
+    addStyle(t, {
+      '.init::marker': 'content: ""; color: red; transition: all 100s;',
+      '.change::marker': 'color: green;',
+    });
+  }
+
+  const parent = addDiv(t, { 'style': 'display: list-item' });
   const child = addDiv(t);
   parent.appendChild(child);
 
   parent.style.left = '0px';
-  parent.style.transition = 'left 10s';
+  parent.style.transition = 'left 100s';
   parent.classList.add('init');
   child.style.left = '0px';
-  child.style.transition = 'left 10s';
+  child.style.transition = 'left 100s';
   getComputedStyle(parent).left;
 
   parent.style.left = '100px';
   parent.classList.add('change');
   child.style.left = '100px';
 
-  const anims = document.getAnimations();
-  assert_equals(anims.length, 4,
-                'CSS transition on both pseudo-elements and elements ' +
-                'are returned');
-  assert_equals(anims[0].effect.target, parent,
-                'The animation targeting the parent element comes first');
-  assert_equals(anims[1].effect.target.type, '::before',
-                'The animation targeting the ::before element comes second');
-  assert_equals(anims[2].effect.target.type, '::after',
-                'The animation targeting the ::after element comes third');
-  assert_equals(anims[3].effect.target, child,
-                'The animation targeting the child element comes last');
-}, 'CSS Transitions targetting (pseudo-)elements should have correct order ' +
-   'after sorting');
+  const expectedTransitions = [
+    [parent, undefined],
+    [parent, '::marker'],
+    [parent, '::before'],
+    [parent, '::after'],
+    [child, undefined],
+  ];
+  if (!supportsMarkerPseudos) {
+    expectedTransitions.splice(1, 1);
+  }
+
+  const transitions = document.getAnimations();
+  assert_equals(
+    transitions.length,
+    expectedTransitions.length,
+    'CSS transition on both pseudo-elements and elements are returned'
+  );
+
+  for (const [index, expected] of expectedTransitions.entries()) {
+    const [element, pseudo] = expected;
+    const actual = transitions[index];
+
+    if (pseudo) {
+      assert_equals(
+        actual.effect.target.element,
+        element,
+        `Transition #${index + 1} has expected target`
+      );
+      assert_equals(
+        actual.effect.target.type,
+        pseudo,
+        `Transition #${index + 1} has expected pseudo type`
+      );
+    } else {
+      assert_equals(
+        actual.effect.target,
+        element,
+        `Transition #${index + 1} has expected target`
+      );
+    }
+  }
+}, 'CSS Transitions targetting (pseudo-)elements should have correct order '
+   + 'after sorting');
 
 promise_test(async t => {
   const div = addDiv(t, { style: 'left: 0px; transition: all 50ms' });
   getComputedStyle(div).left;
 
   div.style.left = '100px';
   const animations = div.getAnimations();
   assert_equals(animations.length, 1, 'Got transition');
--- a/testing/web-platform/tests/css/css-transitions/non-rendered-element-002.html
+++ b/testing/web-platform/tests/css/css-transitions/non-rendered-element-002.html
@@ -45,12 +45,46 @@ promise_test(async t => {
 
     await eventWatcher.wait_for('transitioncancel');
 
     div.remove();
     style.remove();
   }
 }, 'Transitions on ::before/::after pseudo-elements are canceled when the'
    + ' content property is cleared');
+
+promise_test(async t => {
+  if (!CSS.supports('selector(::marker)')) {
+    return;
+  }
+
+  addStyle(t, {
+    '.init::marker': 'content: ""; color: red; transition: color 100s;',
+    '.change::marker': 'color: green',
+  });
+
+  // Create element (and pseudo-element) and attach event listeners
+  const div = addDiv(t, { 'style': 'display: list-item' });
+  div.classList.add('init');
+
+  const eventWatcher = new EventWatcher(t, div, [
+    'transitionrun',
+    'transitioncancel',
+  ]);
+
+  // Trigger transition
+  getComputedStyle(div).color;
+  div.classList.add('change');
+  getComputedStyle(div).color;
+
+  await eventWatcher.wait_for('transitionrun');
+
+  // Make the parent element no longer display: list-item so that the pseudo
+  // element no longer renders
+  div.style.display = 'block';
+
+  await eventWatcher.wait_for('transitioncancel');
+}, 'Transitions on ::marker pseudo-elements are canceled when the'
+   + ' parent display type is no longer list-item');
 </script>
 
 </body>
 </html>