Removed exception for setting playback rate on scroll linked animations

Initial hold time for scroll-linked animations now accounts for playback
rate.

Added test for setting playback rate on scroll linked animations

Bug: 916117
Change-Id: Ibd7e80ce60346d2f13155b13b8cc14252afc0c3a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1970911
Commit-Queue: Jordan Taylor <jortaylo@microsoft.com>
Reviewed-by: Majid Valipour <majidvp@chromium.org>
Cr-Commit-Position: refs/heads/master@{#740882}
diff --git a/scroll-animations/scroll-animation.html b/scroll-animations/scroll-animation.html
index bee9e36..7a6f875 100644
--- a/scroll-animations/scroll-animation.html
+++ b/scroll-animations/scroll-animation.html
@@ -4,6 +4,7 @@
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="/web-animations/testcommon.js"></script>
+<script src="testcommon.js"></script>
 <style>
   .scroller {
     overflow: auto;
@@ -18,30 +19,6 @@
 <div id="log"></div>
 <script>
   'use strict';
-
-  function createScroller(test) {
-    var scroller = createDiv(test);
-    scroller.innerHTML = "<div class='contents'></div>";
-    scroller.classList.add('scroller');
-    return scroller;
-  }
-
-  function createScrollTimeline(test) {
-    return new ScrollTimeline({
-      scrollSource: createScroller(test),
-      timeRange: 1000
-    });
-  }
-
-  function createScrollLinkedAnimation(test, timeline) {
-    if(timeline === undefined)
-      timeline = createScrollTimeline(test);
-    const DURATION = 1000; // ms
-    const KEYFRAMES = { opacity: [1, 0] };
-    return new Animation(
-      new KeyframeEffect(createDiv(test), KEYFRAMES, DURATION), timeline);
-  }
-
   promise_test(async t => {
     const animation = createScrollLinkedAnimation(t);
     const scroller = animation.timeline.scrollSource;
diff --git a/scroll-animations/setting-current-time.html b/scroll-animations/setting-current-time.html
index dd2abb4..069a7cc 100644
--- a/scroll-animations/setting-current-time.html
+++ b/scroll-animations/setting-current-time.html
@@ -5,6 +5,7 @@
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="/web-animations/testcommon.js"></script>
+<script src="testcommon.js"></script>
 <style>
 .scroller {
   overflow: auto;
@@ -20,30 +21,6 @@
 <div id="log"></div>
 <script>
 'use strict';
-
-  function createScroller(test) {
-    var scroller = createDiv(test);
-    scroller.innerHTML = "<div class='contents'></div>";
-    scroller.classList.add('scroller');
-    return scroller;
-  }
-
-  function createScrollTimeline(test) {
-    return new ScrollTimeline({
-      scrollSource: createScroller(test),
-      timeRange: 1000
-    });
-  }
-
-  function createScrollLinkedAnimation(test, timeline) {
-    if(timeline === undefined)
-      timeline = createScrollTimeline(test);
-    const DURATION = 1000; // ms
-    const KEYFRAMES = { opacity: [1, 0] };
-    return new Animation(
-      new KeyframeEffect(createDiv(test), KEYFRAMES, DURATION), timeline);
-  }
-
   promise_test(async t => {
     const animation = createScrollLinkedAnimation(t);
     const scroller = animation.timeline.scrollSource;
diff --git a/scroll-animations/setting-playback-rate.html b/scroll-animations/setting-playback-rate.html
new file mode 100644
index 0000000..a5fdf09
--- /dev/null
+++ b/scroll-animations/setting-playback-rate.html
@@ -0,0 +1,253 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Setting the playback rate of an animation that is using a ScrollTimeline</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#setting-the-playback-rate-of-an-animation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="testcommon.js"></script>
+<style>
+.scroller {
+  overflow: auto;
+  height: 100px;
+  width: 100px;
+}
+.contents {
+  height: 1000px;
+  width: 100%;
+}
+</style>
+<body>
+<script>
+'use strict';
+  promise_test(async t => {
+    const animation = createScrollLinkedAnimation(t);
+    const scroller = animation.timeline.scrollSource;
+    // this forces a layout which results in an active timeline
+    scroller.scrollTop = 0;
+
+    animation.playbackRate = 0.5;
+    animation.play();
+
+    assert_equals(animation.currentTime, 0,
+      'Zero current time is not affected by playbackRate change.');
+  }, 'Zero current time is not affected by playbackRate set while the ' +
+       'animation is in idle state.');
+
+  promise_test(async t => {
+    const animation = createScrollLinkedAnimation(t);
+    const scroller = animation.timeline.scrollSource;
+    // this forces a layout which results in an active timeline
+    scroller.scrollTop = 0;
+
+    animation.play();
+    animation.playbackRate = 0.5;
+
+    assert_equals(animation.currentTime, 0,
+      'Zero current time is not affected by playbackRate change.');
+  }, 'Zero current time is not affected by playbackRate set while the ' +
+      'animation is in play-pending state.');
+
+  promise_test(async t => {
+    const animation = createScrollLinkedAnimation(t);
+    const scroller = animation.timeline.scrollSource;
+    const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+    const timeRange = animation.timeline.timeRange;
+    scroller.scrollTop = 0.2 * maxScroll;
+
+    animation.playbackRate = 0.5;
+    animation.play();
+    await animation.ready;
+    assert_equals(animation.currentTime, 0.2 * timeRange * 0.5,
+      'Initial current time is scaled by playbackRate change.');
+  }, 'Initial current time is scaled by playbackRate set while ' +
+      'scroll-linked animation is in running state.');
+
+  promise_test(async t => {
+    const animation = createScrollLinkedAnimation(t);
+    const scroller = animation.timeline.scrollSource;
+    const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+    const timeRange = animation.timeline.timeRange;
+    const playbackRate = 2;
+
+    scroller.scrollTop = 0.2 * maxScroll;
+
+    animation.play();
+    await animation.ready;
+    // Set playback rate while the animation is playing.
+    animation.playbackRate = playbackRate;
+    assert_times_equal(animation.currentTime, 0.2 * timeRange,
+      'The current time should stay unaffected by setting playback rate.');
+  }, 'The current time is not affected by playbackRate set while the ' +
+      'scroll-linked animation is in play state.');
+
+  promise_test(async t => {
+    const animation = createScrollLinkedAnimation(t);
+    const scroller = animation.timeline.scrollSource;
+    const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+    const timeRange = animation.timeline.timeRange;
+
+    // Set playback rate while the animation is in 'idle' state.
+    animation.playbackRate = 2;
+    animation.play();
+    await animation.ready;
+    scroller.scrollTop = 0.2 * maxScroll;
+
+    assert_times_equal(animation.currentTime, 0.2 * timeRange * 2,
+      'The current time should increase two times faster than timeline time.');
+  }, 'The playback rate set before scroll-linked animation started playing ' +
+      'affects the rate of progress of the current time');
+
+  promise_test(async t => {
+    const animation = createScrollLinkedAnimation(t);
+    const scroller = animation.timeline.scrollSource;
+    const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+    animation.play();
+
+    await animation.ready;
+
+    animation.playbackRate = 2;
+    scroller.scrollTop = 0.25 * maxScroll;
+
+    assert_times_equal(
+      animation.currentTime,
+      animation.timeline.currentTime * animation.playbackRate,
+      'The current time should increase two times faster than timeline time'
+    );
+  }, 'The playback rate affects the rate of progress of the current time' +
+    ' when scrolling');
+
+  test(t => {
+    const animation = createScrollLinkedAnimation(t);
+    const scroller = animation.timeline.scrollSource;
+    const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+    scroller.scrollTop = 0.25 * maxScroll;
+    animation.play();
+
+    animation.playbackRate = 2;
+
+    assert_equals(animation.playState, "running");
+    assert_true(animation.pending);
+    assert_times_equal(animation.currentTime, animation.timeline.currentTime);
+  }, 'Setting the playback rate while play-pending preserves the current time' +
+    ' from scrollTimeline.');
+
+  test(t => {
+    const animation = createScrollLinkedAnimation(t);
+    animation.play();
+    animation.currentTime = 250;
+    animation.playbackRate = 2;
+
+    assert_equals(animation.playState, "running");
+    assert_true(animation.pending);
+    assert_times_equal(animation.currentTime, 250);
+  }, 'Setting the playback rate while play-pending preserves the set current' +
+    ' time.');
+
+  promise_test(async t => {
+    const animation = createScrollLinkedAnimation(t);
+    const scroller = animation.timeline.scrollSource;
+    const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+    scroller.scrollTop = 0.25 * maxScroll;
+    animation.play();
+
+    await animation.ready;
+    animation.playbackRate = 2;
+
+    assert_times_equal(animation.currentTime, animation.timeline.currentTime);
+  }, 'Setting the playback rate while playing preserves the current time' +
+    ' from scrollTimeline.');
+
+  promise_test(async t => {
+    const animation = createScrollLinkedAnimation(t);
+
+    /* Wait for animation frame is here for now to avoid a renderer crash
+    caused by crbug.com/1042924. Once that is fixed, these can be removed */
+    await waitForAnimationFrames(2);
+
+    animation.play();
+
+    animation.currentTime = 250;
+    await animation.ready;
+    animation.playbackRate = 2;
+
+    assert_times_equal(animation.currentTime, 250);
+  }, 'Setting the playback rate while playing preserves the set current time.');
+
+  promise_test(async t => {
+    const animation = createScrollLinkedAnimation(t);
+    const scroller = animation.timeline.scrollSource;
+    const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+    const range = animation.timeline.timeRange;
+    animation.playbackRate = -1;
+    scroller.scrollTop = 0.3 * maxScroll;
+    animation.play();
+
+    await animation.ready;
+    const expectedCurrentTime = range - animation.timeline.currentTime;
+    assert_times_equal(animation.currentTime, expectedCurrentTime);
+  }, 'Negative initial playback rate should correctly modify initial current' +
+    ' time.');
+
+  promise_test(async t => {
+    const animation = createScrollLinkedAnimation(t);
+    const scroller = animation.timeline.scrollSource;
+    const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+    scroller.scrollTop = 0.5 * maxScroll;
+    animation.play();
+
+    await animation.ready;
+    const startingTimelineTime = animation.timeline.currentTime;
+    const startingCurrentTime = animation.currentTime;
+    assert_times_equal(startingCurrentTime, startingTimelineTime);
+
+    animation.playbackRate = -1;
+
+    scroller.scrollTop = 0.8 * maxScroll;
+    // -300 = 500 - 800
+    let timelineDiff = startingTimelineTime - animation.timeline.currentTime;
+    // 200 = 500 + (-300)
+    let expected = startingCurrentTime + timelineDiff;
+    assert_times_equal(animation.currentTime, expected);
+
+    scroller.scrollTop = 0.2 * maxScroll;
+    // 300 = 500 - 200
+    timelineDiff = startingTimelineTime - animation.timeline.currentTime;
+    // 800 = 500 + 300
+    expected = startingCurrentTime + timelineDiff;
+    assert_times_equal(animation.currentTime, expected);
+  }, 'Reversing the playback rate while playing correctly impacts current' +
+    ' time during future scrolls');
+
+  promise_test(async t => {
+    const animation = createScrollLinkedAnimation(t);
+    const scroller = animation.timeline.scrollSource;
+    const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+    const range = animation.timeline.timeRange;
+    animation.playbackRate = 0;
+    scroller.scrollTop = 0.3 * maxScroll;
+    animation.play();
+
+    await animation.ready;
+    assert_times_equal(animation.currentTime, 0);
+  }, 'Zero initial playback rate should correctly modify initial current' +
+    ' time.');
+
+  promise_test(async t => {
+    const animation = createScrollLinkedAnimation(t);
+    const scroller = animation.timeline.scrollSource;
+    const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+    scroller.scrollTop = 0.2 * maxScroll;
+    animation.play();
+
+    await animation.ready;
+    assert_times_equal(animation.currentTime, 200);
+    animation.playbackRate = 0;
+    scroller.scrollTop = 0.5 * maxScroll;
+
+    // Ensure that current time does not change.
+    assert_time_equals_literal(animation.timeline.currentTime, 500);
+    assert_time_equals_literal(animation.currentTime, 200);
+  }, 'Setting a zero playback rate while running preserves the current time');
+</script>
+</body>
diff --git a/scroll-animations/testcommon.js b/scroll-animations/testcommon.js
new file mode 100644
index 0000000..ca25968
--- /dev/null
+++ b/scroll-animations/testcommon.js
@@ -0,0 +1,22 @@
+function createScroller(test) {
+  var scroller = createDiv(test);
+  scroller.innerHTML = "<div class='contents'></div>";
+  scroller.classList.add('scroller');
+  return scroller;
+}
+
+function createScrollTimeline(test) {
+  return new ScrollTimeline({
+    scrollSource: createScroller(test),
+    timeRange: 1000
+  });
+}
+
+function createScrollLinkedAnimation(test, timeline) {
+  if(timeline === undefined)
+    timeline = createScrollTimeline(test);
+  const DURATION = 1000; // ms
+  const KEYFRAMES = { opacity: [1, 0] };
+  return new Animation(
+    new KeyframeEffect(createDiv(test), KEYFRAMES, DURATION), timeline);
+}