Bug 1248338 - Implement iterationStart; r=birtles
authorDaisuke Akatsuka <daisuke@mozilla-japan.org>
Wed, 02 Mar 2016 16:23:34 +0900
changeset 286400 12dfe2b87a791ecb0d6cb193d135d41260c5ca92
parent 286399 33087bab1346dbbfaff79478b7c90e5a978e6ff2
child 286401 c6ebde846f2d47225592194a716160be1831c5f9
push id30045
push usercbook@mozilla.com
push dateWed, 02 Mar 2016 14:54:36 +0000
treeherdermozilla-central@bdde3fedb45b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbirtles
bugs1248338
milestone47.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 1248338 - Implement iterationStart; r=birtles
dom/animation/AnimationEffectTimingReadOnly.cpp
dom/animation/AnimationEffectTimingReadOnly.h
dom/animation/KeyframeEffect.cpp
dom/animation/KeyframeEffect.h
gfx/layers/composite/AsyncCompositionManager.cpp
gfx/layers/ipc/LayersMessages.ipdlh
layout/base/nsDisplayList.cpp
layout/style/test/file_animations_iterationstart.html
layout/style/test/mochitest.ini
layout/style/test/test_animations_iterationstart.html
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/web-animations/keyframe-effect/getComputedTiming-currentIteration.html
testing/web-platform/tests/web-animations/keyframe-effect/getComputedTiming-progress.html
--- a/dom/animation/AnimationEffectTimingReadOnly.cpp
+++ b/dom/animation/AnimationEffectTimingReadOnly.cpp
@@ -14,16 +14,17 @@
 
 namespace mozilla {
 
 TimingParams::TimingParams(const dom::AnimationEffectTimingProperties& aRhs,
                            const dom::Element* aTarget)
   : mDuration(aRhs.mDuration)
   , mDelay(TimeDuration::FromMilliseconds(aRhs.mDelay))
   , mIterations(aRhs.mIterations)
+  , mIterationStart(aRhs.mIterationStart)
   , mDirection(aRhs.mDirection)
   , mFill(aRhs.mFill)
 {
   mFunction = AnimationUtils::ParseEasing(aTarget, aRhs.mEasing);
 }
 
 TimingParams::TimingParams(double aDuration)
 {
@@ -108,16 +109,17 @@ TimingParams::operator==(const TimingPar
     // We consider all string values and uninitialized values as meaning "auto".
     // Since mDuration is either a string or uninitialized, we consider it equal
     // if aOther.mDuration is also either a string or uninitialized.
     durationEqual = !aOther.mDuration.IsUnrestrictedDouble();
   }
   return durationEqual &&
          mDelay == aOther.mDelay &&
          mIterations == aOther.mIterations &&
+         mIterationStart == aOther.mIterationStart &&
          mDirection == aOther.mDirection &&
          mFill == aOther.mFill &&
          mFunction == aOther.mFunction;
 }
 
 namespace dom {
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AnimationEffectTimingReadOnly, mParent)
--- a/dom/animation/AnimationEffectTimingReadOnly.h
+++ b/dom/animation/AnimationEffectTimingReadOnly.h
@@ -46,16 +46,17 @@ struct TimingParams
     const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
     const Nullable<dom::ElementOrCSSPseudoElement>& aTarget);
 
   // The unitialized state of mDuration represents "auto".
   // Bug 1237173: We will replace this with Maybe<TimeDuration>.
   dom::OwningUnrestrictedDoubleOrString mDuration;
   TimeDuration mDelay;      // Initializes to zero
   double mIterations = 1.0; // Can be NaN, negative, +/-Infinity
+  double mIterationStart = 0.0;
   dom::PlaybackDirection mDirection = dom::PlaybackDirection::Normal;
   dom::FillMode mFill = dom::FillMode::Auto;
   Maybe<ComputedTimingFunction> mFunction;
 
   bool operator==(const TimingParams& aOther) const;
   bool operator!=(const TimingParams& aOther) const
   {
     return !(*this == aOther);
@@ -80,17 +81,17 @@ protected:
 
 public:
   nsISupports* GetParentObject() const { return mParent; }
   JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   double Delay() const { return mTiming.mDelay.ToMilliseconds(); }
   double EndDelay() const { return 0.0; }
   FillMode Fill() const { return mTiming.mFill; }
-  double IterationStart() const { return 0.0; }
+  double IterationStart() const { return mTiming.mIterationStart; }
   double Iterations() const { return mTiming.mIterations; }
   void GetDuration(OwningUnrestrictedDoubleOrString& aRetVal) const
   {
     aRetVal = mTiming.mDuration;
   }
   PlaybackDirection Direction() const { return mTiming.mDirection; }
   void GetEasing(nsString& aRetVal) const;
 
--- a/dom/animation/KeyframeEffect.cpp
+++ b/dom/animation/KeyframeEffect.cpp
@@ -31,25 +31,27 @@ GetComputedTimingDictionary(const Comput
                             const Nullable<TimeDuration>& aLocalTime,
                             const TimingParams& aTiming,
                             dom::ComputedTimingProperties& aRetVal)
 {
   // AnimationEffectTimingProperties
   aRetVal.mDelay = aTiming.mDelay.ToMilliseconds();
   aRetVal.mFill = aComputedTiming.mFill;
   aRetVal.mIterations = aComputedTiming.mIterations;
+  aRetVal.mIterationStart = aComputedTiming.mIterationStart;
   aRetVal.mDuration.SetAsUnrestrictedDouble() =
     aComputedTiming.mDuration.ToMilliseconds();
   aRetVal.mDirection = aTiming.mDirection;
 
   // ComputedTimingProperties
   aRetVal.mActiveDuration = aComputedTiming.mActiveDuration.ToMilliseconds();
   aRetVal.mEndTime = aComputedTiming.mEndTime.ToMilliseconds();
   aRetVal.mLocalTime = AnimationUtils::TimeDurationToDouble(aLocalTime);
   aRetVal.mProgress = aComputedTiming.mProgress;
+
   if (!aRetVal.mProgress.IsNull()) {
     // Convert the returned currentIteration into Infinity if we set
     // (uint64_t) aComputedTiming.mCurrentIteration to UINT64_MAX
     double iteration = aComputedTiming.mCurrentIteration == UINT64_MAX
                      ? PositiveInfinity<double>()
                      : static_cast<double>(aComputedTiming.mCurrentIteration);
     aRetVal.mCurrentIteration.SetValue(iteration);
   }
@@ -239,16 +241,18 @@ KeyframeEffectReadOnly::GetComputedTimin
     double durationMs = aTiming.mDuration.GetAsUnrestrictedDouble();
     if (!IsNaN(durationMs) && durationMs >= 0.0f) {
       result.mDuration = StickyTimeDuration::FromMilliseconds(durationMs);
     }
   }
   result.mIterations = IsNaN(aTiming.mIterations) || aTiming.mIterations < 0.0f ?
                        1.0f :
                        aTiming.mIterations;
+  result.mIterationStart = std::max(aTiming.mIterationStart, 0.0);
+
   result.mActiveDuration = ActiveDuration(result.mDuration, result.mIterations);
   // Bug 1244635: Add endDelay to the end time calculation
   result.mEndTime = aTiming.mDelay + result.mActiveDuration;
   result.mFill = aTiming.mFill == dom::FillMode::Auto ?
                  dom::FillMode::None :
                  aTiming.mFill;
 
   // The default constructor for ComputedTiming sets all other members to
@@ -268,78 +272,96 @@ KeyframeEffectReadOnly::GetComputedTimin
   if (localTime >= aTiming.mDelay + result.mActiveDuration) {
     result.mPhase = ComputedTiming::AnimationPhase::After;
     if (!result.FillsForwards()) {
       // The animation isn't active or filling at this time.
       result.mProgress.SetNull();
       return result;
     }
     activeTime = result.mActiveDuration;
-    // Note that infinity == floor(infinity) so this will also be true when we
-    // have finished an infinitely repeating animation of zero duration.
+    double finiteProgress =
+      (IsInfinite(result.mIterations) ? 0.0 : result.mIterations)
+      + result.mIterationStart;
     isEndOfFinalIteration = result.mIterations != 0.0 &&
-                            result.mIterations == floor(result.mIterations);
+                            fmod(finiteProgress, 1.0) == 0;
   } else if (localTime < aTiming.mDelay) {
     result.mPhase = ComputedTiming::AnimationPhase::Before;
     if (!result.FillsBackwards()) {
       // The animation isn't active or filling at this time.
       result.mProgress.SetNull();
       return result;
     }
     // activeTime is zero
   } else {
     MOZ_ASSERT(result.mActiveDuration != zeroDuration,
                "How can we be in the middle of a zero-duration interval?");
     result.mPhase = ComputedTiming::AnimationPhase::Active;
     activeTime = localTime - aTiming.mDelay;
   }
 
+  // Calculate the scaled active time
+  // (We handle the case where the iterationStart is zero separately in case
+  // the duration is infinity, since 0 * Infinity is undefined.)
+  StickyTimeDuration startOffset =
+    result.mIterationStart == 0.0
+    ? StickyTimeDuration(0)
+    : result.mDuration.MultDouble(result.mIterationStart);
+  StickyTimeDuration scaledActiveTime = activeTime + startOffset;
+
   // Get the position within the current iteration.
   StickyTimeDuration iterationTime;
-  if (result.mDuration != zeroDuration) {
+  if (result.mDuration != zeroDuration &&
+      scaledActiveTime != StickyTimeDuration::Forever()) {
     iterationTime = isEndOfFinalIteration
                     ? result.mDuration
-                    : activeTime % result.mDuration;
-  } /* else, iterationTime is zero */
+      : scaledActiveTime % result.mDuration;
+  } /* else, either the duration is zero and iterationTime is zero,
+       or the scaledActiveTime is infinity in which case the iterationTime
+       should become infinity but we will not use the iterationTime in that
+       case so we just leave it as zero */
 
   // Determine the 0-based index of the current iteration.
-  if (isEndOfFinalIteration) {
+  if (result.mPhase == ComputedTiming::AnimationPhase::Before ||
+      result.mIterations == 0) {
+    result.mCurrentIteration = static_cast<uint64_t>(result.mIterationStart);
+  } else if (result.mPhase == ComputedTiming::AnimationPhase::After) {
     result.mCurrentIteration =
-      IsInfinite(result.mIterations) // Positive Infinity?
+      IsInfinite(result.mIterations)
       ? UINT64_MAX // In GetComputedTimingDictionary(), we will convert this
                    // into Infinity.
-      : static_cast<uint64_t>(result.mIterations) - 1;
-  } else if (activeTime == zeroDuration) {
-    // If the active time is zero we're either in the first iteration
-    // (including filling backwards) or we have finished an animation with an
-    // iteration duration of zero that is filling forwards (but we're not at
-    // the exact end of an iteration since we deal with that above).
-    result.mCurrentIteration =
-      result.mPhase == ComputedTiming::AnimationPhase::After
-      ? static_cast<uint64_t>(result.mIterations) // floor
-      : 0;
+      : static_cast<uint64_t>(ceil(result.mIterations +
+                              result.mIterationStart)) - 1;
+  } else if (result.mDuration == StickyTimeDuration::Forever()) {
+    result.mCurrentIteration = static_cast<uint64_t>(result.mIterationStart);
   } else {
     result.mCurrentIteration =
-      static_cast<uint64_t>(activeTime / result.mDuration); // floor
+      static_cast<uint64_t>(scaledActiveTime / result.mDuration); // floor
   }
 
   // Normalize the iteration time into a fraction of the iteration duration.
-  if (result.mPhase == ComputedTiming::AnimationPhase::Before) {
-    result.mProgress.SetValue(0.0);
+  if (result.mPhase == ComputedTiming::AnimationPhase::Before ||
+      result.mIterations == 0) {
+    double progress = fmod(result.mIterationStart, 1.0);
+    result.mProgress.SetValue(progress);
   } else if (result.mPhase == ComputedTiming::AnimationPhase::After) {
-    double progress = isEndOfFinalIteration
-                      ? 1.0
-                      : fmod(result.mIterations, 1.0);
+    double progress;
+    if (isEndOfFinalIteration) {
+      progress = 1.0;
+    } else if (IsInfinite(result.mIterations)) {
+      progress = fmod(result.mIterationStart, 1.0);
+    } else {
+      progress = fmod(result.mIterations + result.mIterationStart, 1.0);
+    }
     result.mProgress.SetValue(progress);
   } else {
     // We are in the active phase so the iteration duration can't be zero.
     MOZ_ASSERT(result.mDuration != zeroDuration,
                "In the active phase of a zero-duration animation?");
     double progress = result.mDuration == StickyTimeDuration::Forever()
-                      ? 0.0
+                      ? fmod(result.mIterationStart, 1.0)
                       : iterationTime / result.mDuration;
     result.mProgress.SetValue(progress);
   }
 
   bool thisIterationReverse = false;
   switch (aTiming.mDirection) {
     case PlaybackDirection::Normal:
       thisIterationReverse = false;
--- a/dom/animation/KeyframeEffect.h
+++ b/dom/animation/KeyframeEffect.h
@@ -64,16 +64,17 @@ struct ComputedTiming
   // Will be null if the animation is neither animating nor
   // filling at the sampled time.
   Nullable<double>    mProgress;
   // Zero-based iteration index (meaningless if mProgress is null).
   uint64_t            mCurrentIteration = 0;
   // Unlike TimingParams::mIterations, this value is
   // guaranteed to be in the range [0, Infinity].
   double              mIterations = 1.0;
+  double              mIterationStart = 0.0;
   StickyTimeDuration  mDuration;
 
   // This is the computed fill mode so it is never auto
   dom::FillMode       mFill = dom::FillMode::None;
   bool FillsForwards() const {
     MOZ_ASSERT(mFill != dom::FillMode::Auto,
                "mFill should not be Auto in ComputedTiming.");
     return mFill == dom::FillMode::Both ||
--- a/gfx/layers/composite/AsyncCompositionManager.cpp
+++ b/gfx/layers/composite/AsyncCompositionManager.cpp
@@ -584,16 +584,17 @@ SampleAnimations(Layer* aLayer, TimeStam
 
     TimingParams timing;
     timing.mDuration.SetAsUnrestrictedDouble() =
       animation.duration().ToMilliseconds();
     // Currently animations run on the compositor have their delay factored
     // into their start time, hence the delay is effectively zero.
     timing.mDelay = TimeDuration(0);
     timing.mIterations = animation.iterations();
+    timing.mIterationStart = animation.iterationStart();
     timing.mDirection =
       static_cast<dom::PlaybackDirection>(animation.direction());
     // Animations typically only run on the compositor during their active
     // interval but if we end up sampling them outside that range (for
     // example, while they are waiting to be removed) we currently just
     // assume that we should fill.
     timing.mFill = dom::FillMode::Both;
     timing.mFunction =
--- a/gfx/layers/ipc/LayersMessages.ipdlh
+++ b/gfx/layers/ipc/LayersMessages.ipdlh
@@ -190,16 +190,17 @@ struct Animation {
   // For each frame, the interpolation point is computed based on the
   // startTime, the direction, the duration, and the current time.
   // The segments must uniquely cover the portion from 0.0 to 1.0
   AnimationSegment[] segments;
   // Number of times to repeat the animation, including positive infinity.
   // Values <= 0 mean the animation will not play (although events are still
   // dispatched on the main thread).
   float iterations;
+  float iterationStart;
   // This uses the NS_STYLE_ANIMATION_DIRECTION_* constants.
   int32_t direction;
   nsCSSProperty property;
   AnimationData data;
   float playbackRate;
   // This is used in the transformed progress calculation.
   TimingFunction easingFunction;
 };
--- a/layout/base/nsDisplayList.cpp
+++ b/layout/base/nsDisplayList.cpp
@@ -394,16 +394,17 @@ AddAnimationForProperty(nsIFrame* aFrame
   animation->startTime() = startTime.IsNull()
                            ? TimeStamp()
                            : aAnimation->AnimationTimeToTimeStamp(
                               StickyTimeDuration(timing.mDelay));
   animation->initialCurrentTime() = aAnimation->GetCurrentTime().Value()
                                     - timing.mDelay;
   animation->duration() = computedTiming.mDuration;
   animation->iterations() = computedTiming.mIterations;
+  animation->iterationStart() = computedTiming.mIterationStart;
   animation->direction() = static_cast<uint32_t>(timing.mDirection);
   animation->property() = aProperty.mProperty;
   animation->playbackRate() = aAnimation->PlaybackRate();
   animation->data() = aData;
   animation->easingFunction() = ToTimingFunction(timing.mFunction);
 
   for (uint32_t segIdx = 0; segIdx < aProperty.mSegments.Length(); segIdx++) {
     const AnimationPropertySegment& segment = aProperty.mSegments[segIdx];
new file mode 100644
--- /dev/null
+++ b/layout/style/test/file_animations_iterationstart.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <script type="application/javascript"
+    src="/tests/SimpleTest/paint_listener.js"></script>
+  <script type="application/javascript" src="animation_utils.js"></script>
+  <style type="text/css">
+    .target {
+      /* The animation target needs geometry in order to qualify for OMTA */
+      width: 100px;
+      height: 100px;
+      background-color: white;
+    }
+  </style>
+  <script>
+    var ok = opener.ok.bind(opener);
+    var is = opener.is.bind(opener);
+    var todo = opener.todo.bind(opener);
+    function finish() {
+      var o = opener;
+      self.close();
+      o.SimpleTest.finish();
+    }
+  </script>
+</head>
+<body>
+<div id="display"></div>
+<script type="application/javascript">
+"use strict";
+
+runOMTATest(function() {
+  runAllAsyncAnimTests().then(function() {
+    finish();
+  });
+}, finish, opener.SpecialPowers);
+
+
+addAsyncAnimTest(function *() {
+  var [ div ] = new_div("test");
+  var animation = div.animate(
+    { transform: ["translate(0px)", "translate(100px)"] },
+    { iterationStart: 0.5, duration: 10000, fill: "both"}
+  );
+  yield waitForPaints();
+  omta_is(div, "transform", { tx: 50 }, RunningOn.Compositor, "Start of Animation");
+
+  advance_clock(4000);
+  yield waitForPaints();
+  omta_is(div, "transform", { tx: 90 }, RunningOn.Compositor, "40% of Animation");
+
+  advance_clock(6000);
+  yield waitForPaints();
+  omta_is(div, "transform", { tx: 50 }, RunningOn.MainThread, "End of Animation");
+
+  done_div();
+});
+
+</script>
+</body>
+</html>
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -40,16 +40,18 @@ generated-files = css_properties.js
 [test_animations.html]
 skip-if = toolkit == 'android'
 [test_animations_async_tests.html]
 support-files = ../../reftests/fonts/Ahem.ttf file_animations_async_tests.html
 [test_animations_dynamic_changes.html]
 [test_animations_effect_timing_duration.html]
 support-files = file_animations_effect_timing_duration.html
 [test_animations_event_order.html]
+[test_animations_iterationstart.html]
+support-files = file_animations_iterationstart.html
 [test_animations_omta.html]
 [test_animations_omta_start.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # bug 1041017
 [test_animations_pausing.html]
 support-files = file_animations_pausing.html
 [test_animations_playbackrate.html]
 support-files = file_animations_playbackrate.html
 [test_animations_styles_on_event.html]
new file mode 100644
--- /dev/null
+++ b/layout/style/test/test_animations_iterationstart.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1248338
+-->
+<head>
+  <title>Test for iterationStart on compositor animations (Bug 1248338)</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248338">Mozilla Bug 1248338</a>
+<div id="display"></div>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+  { "set": [[ "dom.animations-api.core.enabled", true]] },
+  function() {
+    window.open("file_animations_iterationstart.html");
+  });
+</script>
+</pre>
+</body>
+</html>
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -28151,16 +28151,24 @@
         "path": "web-animations/keyframe-effect/effect-easing.html",
         "url": "/web-animations/keyframe-effect/effect-easing.html"
       },
       {
         "path": "web-animations/keyframe-effect/getComputedTiming.html",
         "url": "/web-animations/keyframe-effect/getComputedTiming.html"
       },
       {
+        "path": "web-animations/keyframe-effect/getComputedTiming-currentIteration.html",
+        "url": "/web-animations/keyframe-effect/getComputedTiming-currentIteration.html"
+      },
+      {
+        "path": "web-animations/keyframe-effect/getComputedTiming-progress.html",
+        "url": "/web-animations/keyframe-effect/getComputedTiming-progress.html"
+      },
+      {
         "path": "webaudio/the-audio-api/the-audiobuffer-interface/idl-test.html",
         "url": "/webaudio/the-audio-api/the-audiobuffer-interface/idl-test.html"
       },
       {
         "path": "webaudio/the-audio-api/the-audiodestinationnode-interface/idl-test.html",
         "url": "/webaudio/the-audio-api/the-audiodestinationnode-interface/idl-test.html"
       },
       {
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/keyframe-effect/getComputedTiming-currentIteration.html
@@ -0,0 +1,446 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>currentIteration of KeyframeEffectReadOnly getComputedTiming() tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animationeffectreadonly-getcomputedtiming">
+<link rel="author" title="Daisuke Akatsuka" href="mailto:daisuke@mozilla-japan.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<link rel="stylesheet" href="/resources/testharness.css">
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function executeTests(tests, description) {
+  tests.forEach(function(currentTest) {
+    var testParams = '';
+    for (var attr in currentTest.input) {
+      testParams += ' ' + attr + ':' + currentTest.input[attr];
+    }
+    test(function(t) {
+      var div = createDiv(t);
+      var anim = div.animate({ opacity: [ 0, 1 ] }, currentTest.input);
+      assert_equals(anim.effect.getComputedTiming().currentIteration,
+                    currentTest.before);
+      anim.currentTime = currentTest.input.delay || 0;
+      assert_equals(anim.effect.getComputedTiming().currentIteration,
+                    currentTest.active);
+      if (typeof currentTest.after !== 'undefined') {
+        anim.finish();
+        assert_equals(anim.effect.getComputedTiming().currentIteration,
+                      currentTest.after);
+      }
+    }, description + testParams);
+  });
+}
+
+async_test(function(t) {
+  var div = createDiv(t);
+  var anim = div.animate({ opacity: [ 0, 1 ] }, { delay: 1 });
+  assert_equals(anim.effect.getComputedTiming().currentIteration, null);
+  anim.finished.then(t.step_func(function() {
+    assert_equals(anim.effect.getComputedTiming().currentIteration, null);
+    t.done();
+  }));
+}, 'Test currentIteration during before and after phase when fill is none');
+
+var gTests_zero_iterations = [
+  {
+    input:    { iterations: 0,
+                iterationStart: 0,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 0
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 0,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 0
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 0,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 0
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 2.5,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 2,
+    active: 2,
+    after: 2
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 2.5,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 2,
+    active: 2,
+    after: 2
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 2.5,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 2,
+    active: 2,
+    after: 2
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 3,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 3,
+    active: 3,
+    after: 3
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 3,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 3,
+    active: 3,
+    after: 3
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 3,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 3,
+    active: 3,
+    after: 3
+  }
+];
+
+var gTests_integer_iterations = [
+  {
+    input:    { iterations: 3,
+                iterationStart: 0,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 2,
+    after: 2
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 0,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 2
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 0,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 2.5,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 2,
+    active: 5,
+    after: 5
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 2.5,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 2,
+    active: 2,
+    after: 5
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 2.5,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 2,
+    active: 2
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 3,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 3,
+    active: 5,
+    after: 5
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 3,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 3,
+    active: 3,
+    after: 5
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 3,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 3,
+    active: 3
+  }
+];
+
+var gTests_fractional_iterations = [
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 0,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 3,
+    after: 3
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 0,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 3
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 0,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 2.5,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 2,
+    active: 5,
+    after: 5
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 2.5,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 2,
+    active: 2,
+    after: 5
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 2.5,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 2,
+    active: 2
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 3,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 3,
+    active: 6,
+    after: 6
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 3,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 3,
+    active: 3,
+    after: 6
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 3,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 3,
+    active: 3
+  }
+];
+
+var gTests_infinity_iterations = [
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 0,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: Infinity,
+    after: Infinity
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 0,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 0,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 2.5,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 2,
+    active: Infinity,
+    after: Infinity
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 2.5,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 2,
+    active: 2
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 2.5,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 2,
+    active: 2
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 3,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 3,
+    active: Infinity,
+    after: Infinity
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 3,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 3,
+    active: 3
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 3,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 3,
+    active: 3
+  }
+];
+
+executeTests(gTests_zero_iterations, "Test zero iterations:");
+executeTests(gTests_integer_iterations, "Test integer iterations:");
+executeTests(gTests_fractional_iterations, "Test fractional iterations:");
+executeTests(gTests_infinity_iterations, "Test infinity iterations:");
+
+</script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/keyframe-effect/getComputedTiming-progress.html
@@ -0,0 +1,446 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>progress of KeyframeEffectReadOnly getComputedTiming() tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animationeffectreadonly-getcomputedtiming">
+<link rel="author" title="Daisuke Akatsuka" href="mailto:daisuke@mozilla-japan.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<link rel="stylesheet" href="/resources/testharness.css">
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function executeTests(tests, description) {
+  tests.forEach(function(currentTest) {
+    var testParams = '';
+    for (var attr in currentTest.input) {
+      testParams += ' ' + attr + ':' + currentTest.input[attr];
+    }
+    test(function(t) {
+      var div = createDiv(t);
+      var anim = div.animate({ opacity: [ 0, 1 ] }, currentTest.input);
+      assert_equals(anim.effect.getComputedTiming().progress,
+                    currentTest.before);
+      anim.currentTime = currentTest.input.delay || 0;
+      assert_equals(anim.effect.getComputedTiming().progress,
+                    currentTest.active);
+      if (typeof currentTest.after !== 'undefined') {
+        anim.finish();
+        assert_equals(anim.effect.getComputedTiming().progress,
+                      currentTest.after);
+      }
+    }, description + testParams);
+  });
+}
+
+async_test(function(t) {
+  var div = createDiv(t);
+  var anim = div.animate({ opacity: [ 0, 1 ] }, { delay: 1 });
+  assert_equals(anim.effect.getComputedTiming().progress, null);
+  anim.finished.then(t.step_func(function() {
+    assert_equals(anim.effect.getComputedTiming().progress, null);
+    t.done();
+  }));
+}, 'Test progress during before and after phase when fill is none');
+
+var gTests_zero_iterations = [
+  {
+    input:    { iterations: 0,
+                iterationStart: 0,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 0
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 0,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 0
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 0,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 0
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 2.5,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0.5,
+    active: 0.5,
+    after: 0.5
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 2.5,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0.5,
+    active: 0.5,
+    after: 0.5
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 2.5,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0.5,
+    active: 0.5,
+    after: 0.5
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 3,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 0
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 3,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 0
+  },
+
+  {
+    input:    { iterations: 0,
+                iterationStart: 3,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 0
+  }
+];
+
+var gTests_integer_iterations = [
+  {
+    input:    { iterations: 3,
+                iterationStart: 0,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 1,
+    after: 1
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 0,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 1
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 0,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 2.5,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0.5,
+    active: 0.5,
+    after: 0.5
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 2.5,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0.5,
+    active: 0.5,
+    after: 0.5
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 2.5,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0.5,
+    active: 0.5
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 3,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 1,
+    after: 1
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 3,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 1
+  },
+
+  {
+    input:    { iterations: 3,
+                iterationStart: 3,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0
+  }
+];
+
+var gTests_fractional_iterations = [
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 0,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0.5,
+    after: 0.5
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 0,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 0.5
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 0,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 2.5,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0.5,
+    active: 1,
+    after: 1
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 2.5,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0.5,
+    active: 0.5,
+    after: 1
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 2.5,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0.5,
+    active: 0.5
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 3,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0.5,
+    after: 0.5
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 3,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0,
+    after: 0.5
+  },
+
+  {
+    input:    { iterations: 3.5,
+                iterationStart: 3,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0
+  }
+];
+
+var gTests_infinity_iterations = [
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 0,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 1,
+    after: 1
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 0,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 0,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 2.5,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0.5,
+    active: 0.5,
+    after: 0.5
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 2.5,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0.5,
+    active: 0.5
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 2.5,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0.5,
+    active: 0.5
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 3,
+                duration: 0,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 1,
+    after: 1
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 3,
+                duration: 100,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0
+  },
+
+  {
+    input:    { iterations: Infinity,
+                iterationStart: 3,
+                duration: Infinity,
+                delay: 1,
+                fill: 'both' },
+    before: 0,
+    active: 0
+  }
+];
+
+executeTests(gTests_zero_iterations, "Test zero iterations:");
+executeTests(gTests_integer_iterations, "Test integer iterations:");
+executeTests(gTests_fractional_iterations, "Test fractional iterations:");
+executeTests(gTests_infinity_iterations, "Test infinity iterations:");
+
+</script>
+</body>