<!DOCTYPE html>
<meta charset=utf-8>
<title>Animatable.animate</title>
<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animatable-animate">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../testcommon.js"></script>
<script src="../../resources/easing-tests.js"></script>
<script src="../../resources/keyframe-utils.js"></script>
<script src="../../resources/keyframe-tests.js"></script>
<script src="../../resources/timing-utils.js"></script>
<script src="../../resources/timing-tests.js"></script>
<style>
.pseudo::before {content: '';}
.pseudo::after {content: '';}
.pseudo::marker {content: '';}
</style>
<body>
<div id="log"></div>
<iframe width="10" height="10" id="iframe"></iframe>
<script>
'use strict';

// Tests on Element

test(t => {
  const anim = createDiv(t).animate(null);
  assert_class_string(anim, 'Animation', 'Returned object is an Animation');
}, 'Element.animate() creates an Animation object');

test(t => {
  const iframe = window.frames[0];
  const div = createDiv(t, iframe.document);
  const anim = Element.prototype.animate.call(div, null);
  assert_equals(Object.getPrototypeOf(anim), iframe.Animation.prototype,
                'The prototype of the created Animation is that defined on'
                + ' the relevant global for the target element');
  assert_not_equals(Object.getPrototypeOf(anim), Animation.prototype,
                    'The prototype of the created Animation is NOT that of'
                    + ' the current global');
}, 'Element.animate() creates an Animation object in the relevant realm of'
   + ' the target element');

test(t => {
  const div = createDiv(t);
  const anim = Element.prototype.animate.call(div, null);
  assert_class_string(anim.effect, 'KeyframeEffect',
                      'Returned Animation has a KeyframeEffect');
}, 'Element.animate() creates an Animation object with a KeyframeEffect');

test(t => {
  const iframe = window.frames[0];
  const div = createDiv(t, iframe.document);
  const anim = Element.prototype.animate.call(div, null);
  assert_equals(Object.getPrototypeOf(anim.effect),
                iframe.KeyframeEffect.prototype,
                'The prototype of the created KeyframeEffect is that defined on'
                + ' the relevant global for the target element');
  assert_not_equals(Object.getPrototypeOf(anim.effect),
                    KeyframeEffect.prototype,
                    'The prototype of the created KeyframeEffect is NOT that of'
                    + ' the current global');
}, 'Element.animate() creates an Animation object with a KeyframeEffect'
   + ' that is created in the relevant realm of the target element');

for (const subtest of gEmptyKeyframeListTests) {
  test(t => {
    const anim = createDiv(t).animate(subtest, 2000);
    assert_not_equals(anim, null);
  }, 'Element.animate() accepts empty keyframe lists ' +
     `(input: ${JSON.stringify(subtest)})`);
}

for (const subtest of gKeyframesTests) {
  test(t => {
    const anim = createDiv(t).animate(subtest.input, 2000);
    assert_frame_lists_equal(anim.effect.getKeyframes(), subtest.output);
  }, `Element.animate() accepts ${subtest.desc}`);
}

for (const subtest of gInvalidKeyframesTests) {
  test(t => {
    const div = createDiv(t);
    assert_throws_js(TypeError, () => {
      div.animate(subtest.input, 2000);
    });
  }, `Element.animate() does not accept ${subtest.desc}`);
}

test(t => {
  const anim = createDiv(t).animate(null, 2000);
  assert_equals(anim.effect.getTiming().duration, 2000);
  assert_default_timing_except(anim.effect, ['duration']);
}, 'Element.animate() accepts a double as an options argument');

test(t => {
  const anim = createDiv(t).animate(null,
                                    { duration: Infinity, fill: 'forwards' });
  assert_equals(anim.effect.getTiming().duration, Infinity);
  assert_equals(anim.effect.getTiming().fill, 'forwards');
  assert_default_timing_except(anim.effect, ['duration', 'fill']);
}, 'Element.animate() accepts a KeyframeAnimationOptions argument');

test(t => {
  const anim = createDiv(t).animate(null);
  assert_default_timing_except(anim.effect, []);
}, 'Element.animate() accepts an absent options argument');

for (const invalid of gBadDelayValues) {
  test(t => {
    assert_throws_js(TypeError, () => {
      createDiv(t).animate(null, { delay: invalid });
    });
  }, `Element.animate() does not accept invalid delay value: ${invalid}`);
}

test(t => {
  const anim = createDiv(t).animate(null, { duration: 'auto' });
  assert_equals(anim.effect.getTiming().duration, 'auto', 'set duration \'auto\'');
  assert_equals(anim.effect.getComputedTiming().duration, 0,
                'getComputedTiming() after set duration \'auto\'');
}, 'Element.animate() accepts a duration of \'auto\' using a dictionary'
   + ' object');

for (const invalid of gBadDurationValues) {
  if (typeof invalid === 'string' && !isNaN(parseFloat(invalid))) {
    continue;
  }
  test(t => {
    assert_throws_js(TypeError, () => {
      createDiv(t).animate(null, invalid);
    });
  }, 'Element.animate() does not accept invalid duration value: '
     + (typeof invalid === 'string' ? `"${invalid}"` : invalid));
}

for (const invalid of gBadDurationValues) {
  test(t => {
    assert_throws_js(TypeError, () => {
      createDiv(t).animate(null, { duration: invalid });
    });
  }, 'Element.animate() does not accept invalid duration value: '
     + (typeof invalid === 'string' ? `"${invalid}"` : invalid)
     + ' using a dictionary object');
}

for (const invalidEasing of gInvalidEasings) {
  test(t => {
    assert_throws_js(TypeError, () => {
      createDiv(t).animate({ easing: invalidEasing }, 2000);
    });
  }, `Element.animate() does not accept invalid easing: '${invalidEasing}'`);
}

for (const invalid of gBadIterationStartValues) {
  test(t => {
    assert_throws_js(TypeError, () => {
      createDiv(t).animate(null, { iterationStart: invalid });
    });
  }, 'Element.animate() does not accept invalid iterationStart value: ' +
     invalid);
}

for (const invalid of gBadIterationsValues) {
  test(t => {
    assert_throws_js(TypeError, () => {
      createDiv(t).animate(null, { iterations: invalid });
    });
  }, 'Element.animate() does not accept invalid iterations value: ' +
     invalid);
}

test(t => {
  const anim = createDiv(t).animate(null, 2000);
  assert_equals(anim.id, '');
}, 'Element.animate() correctly sets the id attribute when no id is specified');

test(t => {
  const anim = createDiv(t).animate(null, { id: 'test' });
  assert_equals(anim.id, 'test');
}, 'Element.animate() correctly sets the id attribute');

test(t => {
  const anim = createDiv(t).animate(null, 2000);
  assert_equals(anim.timeline, document.timeline);
}, 'Element.animate() correctly sets the Animation\'s timeline');

async_test(t => {
  const iframe = document.createElement('iframe');
  iframe.width = 10;
  iframe.height = 10;

  iframe.addEventListener('load', t.step_func(() => {
    const div = createDiv(t, iframe.contentDocument);
    const anim = div.animate(null, 2000);
    assert_equals(anim.timeline, iframe.contentDocument.timeline);
    iframe.remove();
    t.done();
  }));

  document.body.appendChild(iframe);
}, 'Element.animate() correctly sets the Animation\'s timeline when ' +
   'triggered on an element in a different document');

for (const subtest of gAnimationTimelineTests) {
  test(t => {
    const anim = createDiv(t).animate(null, { timeline: subtest.timeline });
    assert_not_equals(anim, null,
                      'An animation sohuld be created');
    assert_equals(anim.timeline, subtest.expectedTimeline,
                  'Animation timeline should be '+
                  subtest.expectedTimelineDescription);
  }, 'Element.animate() correctly sets the Animation\'s timeline '
     + subtest.description + ' in KeyframeAnimationOptions.');
}

test(t => {
  const anim = createDiv(t).animate(null, 2000);
  assert_equals(anim.playState, 'running');
}, 'Element.animate() calls play on the Animation');

promise_test(async t => {
  const div = createDiv(t);

  let gotTransition = false;
  div.addEventListener('transitionrun', () => {
    gotTransition = true;
  });

  // Setup transition start point.
  div.style.transition = 'opacity 100s';
  getComputedStyle(div).opacity;

  // Update specified style but don't flush style.
  div.style.opacity = '0.5';

  // Trigger a new animation at the same time.
  const anim = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);

  // If Element.animate() produces a style change event it will have triggered
  // a transition.
  //
  // If it does NOT produce a style change event, the animation will override
  // the before-change style and after-change style such that a transition is
  // never triggered.

  // Wait for the animation to start and then for one more animation
  // frame to give the transitionrun event a chance to be dispatched.
  await anim.ready;
  await waitForAnimationFrames(1);

  assert_false(gotTransition, 'A transition should NOT have been triggered');
}, 'Element.animate() does NOT trigger a style change event');

// Tests on pseudo-elements
// Some tests occur twice (on pseudo-elements with and without content)
// in order to test both code paths for tree-abiding pseudo-elements in blink.

test(t => {
  const div = createDiv(t);
  div.classList.add('pseudo');
  const anim = div.animate(null, {pseudoElement: '::before'});
  assert_class_string(anim, 'Animation', 'The returned object is an Animation');
}, 'animate() with pseudoElement parameter creates an Animation object');

test(t => {
  const div = createDiv(t);
  const anim = div.animate(null, {pseudoElement: '::before'});
  assert_class_string(anim, 'Animation', 'The returned object is an Animation');
}, 'animate() with pseudoElement parameter without content creates an Animation object');

test(t => {
  const div = createDiv(t);
  div.classList.add('pseudo');
  div.style.display = 'list-item';
  const anim = div.animate(null, {pseudoElement: '::marker'});
  assert_class_string(anim, 'Animation', 'The returned object is an Animation for ::marker');
}, 'animate() with pseudoElement parameter creates an Animation object for ::marker');

test(t => {
  const div = createDiv(t);
  div.classList.add('pseudo');
  div.textContent = 'foo';
  const anim = div.animate(null, {pseudoElement: '::first-line'});
  assert_class_string(anim, 'Animation', 'The returned object is an Animation for ::first-line');
}, 'animate() with pseudoElement parameter creates an Animation object for ::first-line');

test(t => {
  const div = createDiv(t);
  div.classList.add('pseudo');
  const anim = div.animate(null, {pseudoElement: '::before'});
  assert_equals(anim.effect.target, div, 'The returned element has the correct target element');
  assert_equals(anim.effect.pseudoElement, '::before',
                'The returned Animation targets the correct selector');
}, 'animate() with pseudoElement an Animation object targeting ' +
   'the correct pseudo-element');

test(t => {
  const div = createDiv(t);
  const anim = div.animate(null, {pseudoElement: '::before'});
  assert_equals(anim.effect.target, div, 'The returned element has the correct target element');
  assert_equals(anim.effect.pseudoElement, '::before',
                'The returned Animation targets the correct selector');
}, 'animate() with pseudoElement without content creates an Animation object targeting ' +
   'the correct pseudo-element');

test(t => {
  const div = createDiv(t);
  div.classList.add('pseudo');
  div.style.display = 'list-item';
  const anim = div.animate(null, {pseudoElement: '::marker'});
  assert_equals(anim.effect.target, div, 'The returned element has the correct target element');
  assert_equals(anim.effect.pseudoElement, '::marker',
                'The returned Animation targets the correct selector');
}, 'animate() with pseudoElement an Animation object targeting ' +
   'the correct pseudo-element for ::marker');

test(t => {
  const div = createDiv(t);
  div.classList.add('pseudo');
  div.textContent = 'foo';
  const anim = div.animate(null, {pseudoElement: '::first-line'});
  assert_equals(anim.effect.target, div, 'The returned element has the correct target element');
  assert_equals(anim.effect.pseudoElement, '::first-line',
                'The returned Animation targets the correct selector');
}, 'animate() with pseudoElement an Animation object targeting ' +
   'the correct pseudo-element for ::first-line');

for (const pseudo of [
  '',
  'before',
  ':abc',
  '::abc',
  '::placeholder',
]) {
  test(t => {
    const div = createDiv(t);
    assert_throws_dom("SyntaxError", () => {
      div.animate(null, {pseudoElement: pseudo});
    });
  }, `animate() with a non-null invalid pseudoElement '${pseudo}' throws a ` +
     `SyntaxError`);
}

promise_test(async t => {
  const div = createDiv(t);
  div.classList.add('pseudo');
  let animBefore = div.animate({opacity: [1, 0]}, {duration: 1, pseudoElement: '::before', fill: 'both'});
  let animAfter = div.animate({opacity: [1, 0]}, {duration: 1, pseudoElement: '::after', fill: 'both'});
  await animBefore.finished;
  await animAfter.finished;
  // The animation on ::before should not be replaced as it targets a different
  // pseudo-element.
  assert_equals(animBefore.replaceState, 'active');
  assert_equals(animAfter.replaceState, 'active');
}, 'Finished fill animation doesn\'t replace animation on a different pseudoElement');

</script>
</body>
