Bug 1178664 - Part 3 - Implement Animation.onfinish event. r=bbirtles, r=smaug
authorHiroyuki Ikezoe <hiikezoe@mozilla-japan.org>
Thu, 30 Jul 2015 23:25:00 +0200
changeset 287313 9338f9c7fb959f6af499b638d4a6902f9bbb3e90
parent 287312 98c90fc9a0bbdb27d894da3c3005e30ccdeef0bc
child 287314 db31cfe558094370503866157f7207b6f0f7c99b
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbbirtles, smaug
bugs1178664
milestone42.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 1178664 - Part 3 - Implement Animation.onfinish event. r=bbirtles, r=smaug
dom/animation/Animation.cpp
dom/animation/Animation.h
dom/animation/test/css-animations/file_animation-onfinish.html
dom/animation/test/css-animations/test_animation-onfinish.html
dom/animation/test/mochitest.ini
dom/base/nsGkAtomList.h
dom/webidl/Animation.webidl
--- a/dom/animation/Animation.cpp
+++ b/dom/animation/Animation.cpp
@@ -2,17 +2,19 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "Animation.h"
 #include "AnimationUtils.h"
 #include "mozilla/dom/AnimationBinding.h"
+#include "mozilla/dom/AnimationPlaybackEvent.h"
 #include "mozilla/AutoRestore.h"
+#include "mozilla/AsyncEventDispatcher.h" //For AsyncEventDispatcher
 #include "AnimationCommon.h" // For AnimationCollection,
                              // CommonAnimationManager
 #include "nsIDocument.h" // For nsIDocument
 #include "nsIPresShell.h" // For nsIPresShell
 #include "nsLayoutUtils.h" // For PostRestyleEvent (remove after bug 1073336)
 #include "nsThreadUtils.h" // For nsRunnableMethod and nsRevocableEventPtr
 #include "PendingAnimationTracker.h" // For PendingAnimationTracker
 
@@ -1067,41 +1069,68 @@ Animation::GetCollection() const
 
   return manager->GetAnimations(targetElement, targetPseudoType, false);
 }
 
 void
 Animation::DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag)
 {
   if (aSyncNotifyFlag == SyncNotifyFlag::Sync) {
-    MaybeResolveFinishedPromise();
+    DoFinishNotificationImmediately();
   } else if (!mFinishNotificationTask.IsPending()) {
     nsRefPtr<nsRunnableMethod<Animation>> runnable =
-      NS_NewRunnableMethod(this, &Animation::MaybeResolveFinishedPromise);
+      NS_NewRunnableMethod(this, &Animation::DoFinishNotificationImmediately);
     Promise::DispatchToMicroTask(runnable);
     mFinishNotificationTask = runnable;
   }
 }
 
 void
 Animation::ResetFinishedPromise()
 {
   mFinishedIsResolved = false;
   mFinished = nullptr;
 }
 
 void
 Animation::MaybeResolveFinishedPromise()
 {
+  if (mFinished) {
+    mFinished->MaybeResolve(this);
+  }
+  mFinishedIsResolved = true;
+}
+
+void
+Animation::DoFinishNotificationImmediately()
+{
   mFinishNotificationTask.Revoke();
 
   if (PlayState() != AnimationPlayState::Finished) {
     return;
   }
 
-  if (mFinished) {
-    mFinished->MaybeResolve(this);
+  MaybeResolveFinishedPromise();
+
+  DispatchPlaybackEvent(NS_LITERAL_STRING("finish"));
+}
+
+void
+Animation::DispatchPlaybackEvent(const nsAString& aName)
+{
+  AnimationPlaybackEventInit init;
+
+  init.mCurrentTime = GetCurrentTimeAsDouble();
+  if (mTimeline) {
+    init.mTimelineTime = mTimeline->GetCurrentTimeAsDouble();
   }
-  mFinishedIsResolved = true;
+
+  nsRefPtr<AnimationPlaybackEvent> event =
+    AnimationPlaybackEvent::Constructor(this, aName, init);
+  event->SetTrusted(true);
+
+  nsRefPtr<AsyncEventDispatcher> asyncDispatcher =
+    new AsyncEventDispatcher(this, event);
+  asyncDispatcher->PostDOMEvent();
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/animation/Animation.h
+++ b/dom/animation/Animation.h
@@ -106,16 +106,17 @@ public:
   virtual Promise* GetReady(ErrorResult& aRv);
   virtual Promise* GetFinished(ErrorResult& aRv);
   void Cancel();
   virtual void Finish(ErrorResult& aRv);
   virtual void Play(ErrorResult& aRv, LimitBehavior aLimitBehavior);
   virtual void Pause(ErrorResult& aRv);
   virtual void Reverse(ErrorResult& aRv);
   bool IsRunningOnCompositor() const { return mIsRunningOnCompositor; }
+  IMPL_EVENT_HANDLER(finish);
 
   // Wrapper functions for Animation DOM methods when called
   // from script.
   //
   // We often use the same methods internally and from script but when called
   // from script we (or one of our subclasses) perform extra steps such as
   // flushing style or converting the return type.
   Nullable<double> GetStartTimeAsDouble() const;
@@ -334,16 +335,18 @@ protected:
   void UpdateFinishedState(SeekFlag aSeekFlag,
                            SyncNotifyFlag aSyncNotifyFlag);
   void UpdateEffect();
   void FlushStyle() const;
   void PostUpdate();
   void ResetFinishedPromise();
   void MaybeResolveFinishedPromise();
   void DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag);
+  void DoFinishNotificationImmediately();
+  void DispatchPlaybackEvent(const nsAString& aName);
 
   /**
    * Remove this animation from the pending animation tracker and reset
    * mPendingState as necessary. The caller is responsible for resolving or
    * aborting the mReady promise as necessary.
    */
   void CancelPendingTasks();
 
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-onfinish.html
@@ -0,0 +1,142 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes abc {
+  to { transform: translate(10px) }
+}
+</style>
+<body>
+<script>
+'use strict';
+
+const ANIM_PROP_VAL = 'abc 100s';
+const ANIM_DURATION = 100000; // ms
+
+async_test(function(t) {
+  var div = addDiv(t);
+  div.style.animation = ANIM_PROP_VAL;
+  var animation = div.getAnimations()[0];
+
+  var finishedTimelineTime;
+  animation.finished.then(function() {
+    finishedTimelineTime = animation.timeline.currentTime;
+  });
+
+  animation.onfinish = t.step_func_done(function(event) {
+    assert_equals(event.currentTime, 0,
+      'event.currentTime should be zero');
+    assert_equals(event.timelineTime, finishedTimelineTime,
+      'event.timelineTime should equal to the animation timeline ' +
+      'when finished promise is resolved');
+  });
+
+  animation.playbackRate = -1;
+}, 'onfinish event is fired when the currentTime < 0 and ' +
+   'the playbackRate < 0');
+
+async_test(function(t) {
+  var div = addDiv(t);
+  div.style.animation = ANIM_PROP_VAL;
+  var animation = div.getAnimations()[0];
+
+  var finishedTimelineTime;
+  animation.finished.then(function() {
+    finishedTimelineTime = animation.timeline.currentTime;
+  });
+
+  animation.onfinish = t.step_func_done(function(event) {
+    assert_equals(event.currentTime, ANIM_DURATION,
+      'event.currentTime should be the effect end');
+    assert_equals(event.timelineTime, finishedTimelineTime,
+      'event.timelineTime should equal to the animation timeline ' +
+      'when finished promise is resolved');
+  });
+
+  animation.currentTime = ANIM_DURATION;
+}, 'onfinish event is fired when the currentTime > 0 and ' +
+   'the playbackRate > 0');
+
+async_test(function(t) {
+  var div = addDiv(t, {'class': 'animated-div'});
+  div.style.animation = ANIM_PROP_VAL;
+  var animation = div.getAnimations()[0];
+
+  var finishedTimelineTime;
+  animation.finished.then(function() {
+    finishedTimelineTime = animation.timeline.currentTime;
+  });
+
+  animation.onfinish = t.step_func_done(function(event) {
+    assert_equals(event.currentTime, ANIM_DURATION,
+      'event.currentTime should be the effect end');
+    assert_equals(event.timelineTime, finishedTimelineTime,
+      'event.timelineTime should equal to the animation timeline ' +
+      'when finished promise is resolved');
+  });
+
+  animation.finish();
+}, 'onfinish event is fired when animation.finish() is called');
+
+async_test(function(t) {
+  var div = addDiv(t);
+  div.style.animation = ANIM_PROP_VAL;
+  var animation = div.getAnimations()[0];
+
+  animation.onfinish = t.step_func(function(event) {
+    assert_unreached('onfinish event should not be fired');
+  });
+
+  animation.currentTime = ANIM_DURATION / 2;
+  animation.pause();
+
+  animation.ready.then(t.step_func(function() {
+    animation.currentTime = ANIM_DURATION;
+    return waitForAnimationFrames(2);
+  })).then(t.step_func(function() {
+    t.done();
+  }));
+}, 'onfinish event is not fired when paused');
+
+async_test(function(t) {
+  var div = addDiv(t);
+  div.style.animation = ANIM_PROP_VAL;
+  var animation = div.getAnimations()[0];
+
+  animation.onfinish = t.step_func(function(event) {
+    assert_unreached('onfinish event should not be fired');
+  });
+
+  animation.ready.then(function() {
+    animation.playbackRate = 0;
+    animation.currentTime = ANIM_DURATION;
+    return waitForAnimationFrames(2);
+  }).then(t.step_func(function() {
+    t.done();
+  }));
+
+}, 'onfinish event is not fired when the playbackRate is zero');
+
+async_test(function(t) {
+  var div = addDiv(t);
+  div.style.animation = ANIM_PROP_VAL;
+  var animation = div.getAnimations()[0];
+
+  animation.onfinish = t.step_func(function(event) {
+    assert_unreached('onfinish event should not be fired');
+  });
+
+  animation.ready.then(function() {
+    animation.currentTime = ANIM_DURATION;
+    animation.currentTime = ANIM_DURATION / 2;
+    return waitForAnimationFrames(2);
+  }).then(t.step_func(function() {
+    t.done();
+  }));
+
+}, 'onfinish event is not fired when the animation falls out ' +
+   'finished state immediately');
+
+done();
+</script>
+</body>
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-onfinish.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+  { "set": [["dom.animations-api.core.enabled", true]]},
+  function() {
+    window.open("file_animation-onfinish.html");
+  });
+</script>
+</html>
--- a/dom/animation/test/mochitest.ini
+++ b/dom/animation/test/mochitest.ini
@@ -7,16 +7,18 @@ support-files = css-animations/file_anim
 [css-animations/test_animation-cancel.html]
 support-files = css-animations/file_animation-cancel.html
 [css-animations/test_animation-currenttime.html]
 support-files = css-animations/file_animation-currenttime.html
 [css-animations/test_animation-finish.html]
 support-files = css-animations/file_animation-finish.html
 [css-animations/test_animation-finished.html]
 support-files = css-animations/file_animation-finished.html
+[css-animations/test_animation-onfinish.html]
+support-files = css-animations/file_animation-onfinish.html
 [css-animations/test_animation-pausing.html]
 support-files = css-animations/file_animation-pausing.html
 [css-animations/test_animation-play.html]
 support-files = css-animations/file_animation-play.html
 [css-animations/test_animation-playbackrate.html]
 support-files = css-animations/file_animation-playbackrate.html
 [css-animations/test_animation-playstate.html]
 support-files = css-animations/file_animation-playstate.html
--- a/dom/base/nsGkAtomList.h
+++ b/dom/base/nsGkAtomList.h
@@ -765,16 +765,17 @@ GK_ATOM(oneitbroadcasted, "oneitbroadcas
 GK_ATOM(onenabled, "onenabled")
 GK_ATOM(onenterpincodereq, "onenterpincodereq")
 GK_ATOM(onemergencycbmodechange, "onemergencycbmodechange")
 GK_ATOM(onerror, "onerror")
 GK_ATOM(onevicted, "onevicted")
 GK_ATOM(onfacesdetected, "onfacesdetected")
 GK_ATOM(onfailed, "onfailed")
 GK_ATOM(onfetch, "onfetch")
+GK_ATOM(onfinish, "onfinish")
 GK_ATOM(onfocus, "onfocus")
 GK_ATOM(onfrequencychange, "onfrequencychange")
 GK_ATOM(onspeakerforcedchange, "onspeakerforcedchange")
 GK_ATOM(onget, "onget")
 GK_ATOM(ongroupchange, "ongroupchange")
 GK_ATOM(onhashchange, "onhashchange")
 GK_ATOM(onheadphoneschange, "onheadphoneschange")
 GK_ATOM(onheld, "onheld")
--- a/dom/webidl/Animation.webidl
+++ b/dom/webidl/Animation.webidl
@@ -25,16 +25,17 @@ interface Animation : EventTarget {
 
            attribute double             playbackRate;
   [BinaryName="playStateFromJS"]
   readonly attribute AnimationPlayState playState;
   [Throws]
   readonly attribute Promise<Animation> ready;
   [Throws]
   readonly attribute Promise<Animation> finished;
+           attribute EventHandler       onfinish;
   void cancel ();
   [Throws]
   void finish ();
   [Throws, BinaryName="playFromJS"]
   void play ();
   [Throws, BinaryName="pauseFromJS"]
   void pause ();
   [Throws]