Fire animation events at the correct times. (Bug 435442, patch 14) r=bzbarsky
authorL. David Baron <dbaron@dbaron.org>
Mon, 11 Apr 2011 23:18:44 -0700
changeset 67988 37cc67bd29b0c37f826f7acb59e672baa70ec712
parent 67987 6645b30313c5a33d07e5028080962233800b164e
child 67989 5f6f0204b6827bd2263ebc5d6c6c4c19f29ea3fc
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbzbarsky
bugs435442
milestone2.2a1pre
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
Fire animation events at the correct times. (Bug 435442, patch 14) r=bzbarsky
layout/base/nsPresShell.cpp
layout/style/nsAnimationManager.cpp
layout/style/nsAnimationManager.h
layout/style/test/test_animations.html
--- a/layout/base/nsPresShell.cpp
+++ b/layout/base/nsPresShell.cpp
@@ -4785,29 +4785,36 @@ PresShell::FlushPendingNotifications(moz
       }
 #endif // MOZ_SMIL
 
       nsAutoScriptBlocker scriptBlocker;
       mFrameConstructor->CreateNeededFrames();
       mFrameConstructor->ProcessPendingRestyles();
     }
 
+    // Dispatch any 'animationstart' events those (or earlier) restyles
+    // queued up.
+    if (!mIsDestroying) {
+      mPresContext->AnimationManager()->DispatchEvents();
+    }
+
     // Process whatever XBL constructors those restyles queued up.  This
     // ensures that onload doesn't fire too early and that we won't do extra
     // reflows after those constructors run.
     if (!mIsDestroying) {
       mDocument->BindingManager()->ProcessAttachedQueue();
     }
 
-    // Now those constructors might have posted restyle events.  At the same
-    // time, we still need up-to-date style data.  In particular, reflow
-    // depends on style being completely up to date.  If it's not, then style
-    // context reparenting, which can happen during reflow, might suddenly pick
-    // up the new rules and we'll end up with frames whose style doesn't match
-    // the frame type.
+    // Now those constructors or events might have posted restyle
+    // events.  At the same time, we still need up-to-date style data.
+    // In particular, reflow depends on style being completely up to
+    // date.  If it's not, then style context reparenting, which can
+    // happen during reflow, might suddenly pick up the new rules and
+    // we'll end up with frames whose style doesn't match the frame
+    // type.
     if (!mIsDestroying) {
       nsAutoScriptBlocker scriptBlocker;
       mFrameConstructor->CreateNeededFrames();
       mFrameConstructor->ProcessPendingRestyles();
     }
 
 
     // There might be more pending constructors now, but we're not going to
--- a/layout/style/nsAnimationManager.cpp
+++ b/layout/style/nsAnimationManager.cpp
@@ -36,19 +36,19 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsAnimationManager.h"
 #include "nsPresContext.h"
 #include "nsRuleProcessorData.h"
 #include "nsStyleSet.h"
 #include "nsCSSRules.h"
-#include "mozilla/TimeStamp.h"
 #include "nsStyleAnimation.h"
 #include "nsSMILKeySpline.h"
+#include "nsEventDispatcher.h"
 
 using namespace mozilla;
 
 struct AnimationSegmentProperty
 {
   nsCSSProperty mProperty;
   nsStyleAnimation::Value mFromValue, mToValue;
 };
@@ -61,16 +61,21 @@ struct AnimationSegment
 };
 
 /**
  * Data about one animation (i.e., one of the values of
  * 'animation-name') running on an element.
  */
 struct ElementAnimation
 {
+  ElementAnimation()
+    : mLastNotification(LAST_NOTIFICATION_NONE)
+  {
+  }
+
   nsString mName; // empty string for 'none'
   float mIterationCount; // NS_IEEEPositiveInfinity() means infinite
   PRUint8 mDirection;
   PRUint8 mFillMode;
   PRUint8 mPlayState;
 
   bool FillsForwards() const {
     return mFillMode == NS_STYLE_ANIMATION_FILL_MODE_BOTH ||
@@ -84,38 +89,52 @@ struct ElementAnimation
   bool IsPaused() const {
     return mPlayState == NS_STYLE_ANIMATION_PLAY_STATE_PAUSED;
   }
 
   TimeStamp mStartTime; // with delay taken into account
   TimeStamp mPauseStart;
   TimeDuration mIterationDuration;
 
+  enum {
+    LAST_NOTIFICATION_NONE = PRUint32(-1),
+    LAST_NOTIFICATION_END = PRUint32(-2)
+  };
+  // One of the above constants, or an integer for the iteration
+  // whose start we last notified on.
+  PRUint32 mLastNotification;
+
   InfallibleTArray<AnimationSegment> mSegments;
 };
 
+typedef nsAnimationManager::EventArray EventArray;
+typedef nsAnimationManager::AnimationEventInfo AnimationEventInfo;
+
 /**
  * Data about all of the animations running on an element.
  */
 struct ElementAnimations : public mozilla::css::CommonElementAnimationData
 {
   ElementAnimations(dom::Element *aElement, nsIAtom *aElementProperty,
                      nsAnimationManager *aAnimationManager)
     : CommonElementAnimationData(aElement, aElementProperty,
                                  aAnimationManager),
       mNeedsRefreshes(true)
   {
   }
 
-  void EnsureStyleRuleFor(TimeStamp aRefreshTime);
+  void EnsureStyleRuleFor(TimeStamp aRefreshTime,
+                          EventArray &aEventsToDispatch);
+
+  bool IsForElement() const { // rather than for a pseudo-element
+    return mElementProperty == nsGkAtoms::animationsProperty;
+  }
 
   void PostRestyleForAnimation(nsPresContext *aPresContext) {
-    nsRestyleHint hint =
-      (mElementProperty == nsGkAtoms::animationsProperty)
-        ? eRestyle_Self : eRestyle_Subtree;
+    nsRestyleHint hint = IsForElement() ? eRestyle_Self : eRestyle_Subtree;
     aPresContext->PresShell()->RestyleForAnimation(mElement, hint);
   }
 
   // This style rule contains the style data for currently animating
   // values.  It only matches when styling with animation.  When we
   // style without animation, we need to not use it so that we can
   // detect any new changes; if necessary we restyle immediately
   // afterwards with animation.
@@ -139,17 +158,18 @@ ElementAnimationsPropertyDtor(void      
                               void           *aPropertyValue,
                               void           *aData)
 {
   ElementAnimations *ea = static_cast<ElementAnimations*>(aPropertyValue);
   delete ea;
 }
 
 void
-ElementAnimations::EnsureStyleRuleFor(TimeStamp aRefreshTime)
+ElementAnimations::EnsureStyleRuleFor(TimeStamp aRefreshTime,
+                                      EventArray& aEventsToDispatch)
 {
   if (!mNeedsRefreshes) {
     // All of our animations are paused or completed.
     mStyleRuleRefreshTime = aRefreshTime;
     return;
   }
 
   mNeedsRefreshes = false;
@@ -162,17 +182,17 @@ ElementAnimations::EnsureStyleRuleFor(Ti
 
     // FIXME(spec): assume that properties in higher animations override
     // those in lower ones (and that our |HasProperty| check in
     // |BuildSegment| matches the definition of when they should do so.
     // Therefore, we iterate from last animation to first.
     nsCSSPropertySet properties;
 
     for (PRUint32 i = mAnimations.Length(); i-- != 0; ) {
-      const ElementAnimation &anim = mAnimations[i];
+      ElementAnimation &anim = mAnimations[i];
 
       if (anim.mSegments.Length() == 0 ||
           anim.mIterationDuration.ToMilliseconds() <= 0.0) {
         // No animation data.
         continue;
       }
 
       TimeDuration currentTimeDuration;
@@ -182,32 +202,45 @@ ElementAnimations::EnsureStyleRuleFor(Ti
       } else {
         currentTimeDuration = aRefreshTime - anim.mStartTime;
       }
 
       // Set |currentIterationCount| to the (fractional) number of
       // iterations we've completed up to the current position.
       double currentIterationCount =
         currentTimeDuration / anim.mIterationDuration;
+      bool dispatchStartOrIteration = false;
       if (currentIterationCount >= double(anim.mIterationCount)) {
+        // Dispatch 'animationend' when needed.
+        if (IsForElement() && 
+            anim.mLastNotification !=
+              ElementAnimation::LAST_NOTIFICATION_END) {
+          anim.mLastNotification = ElementAnimation::LAST_NOTIFICATION_END;
+          AnimationEventInfo ei(mElement, anim.mName, NS_ANIMATION_END,
+                                currentTimeDuration);
+          aEventsToDispatch.AppendElement(ei);
+        }
+
         if (!anim.FillsForwards()) {
           // No animation data.
           continue;
         }
         currentIterationCount = double(anim.mIterationCount);
       } else {
         if (!anim.IsPaused()) {
           mNeedsRefreshes = true;
         }
         if (currentIterationCount < 0.0) {
           if (!anim.FillsBackwards()) {
             // No animation data.
             continue;
           }
           currentIterationCount = 0.0;
+        } else {
+          dispatchStartOrIteration = !anim.IsPaused();
         }
       }
 
       // Set |positionInIteration| to the position from 0% to 100% along
       // the keyframes.
       NS_ABORT_IF_FALSE(currentIterationCount >= 0.0, "must be positive");
       PRUint32 whichIteration = int(currentIterationCount);
       if (whichIteration == anim.mIterationCount) {
@@ -218,16 +251,34 @@ ElementAnimations::EnsureStyleRuleFor(Ti
       }
       double positionInIteration =
         currentIterationCount - double(whichIteration);
       if (anim.mDirection == NS_STYLE_ANIMATION_DIRECTION_ALTERNATE &&
           (whichIteration & 1) == 1) {
         positionInIteration = 1.0 - positionInIteration;
       }
 
+      // Dispatch 'animationstart' or 'animationiteration' when needed.
+      if (IsForElement() && dispatchStartOrIteration &&
+          whichIteration != anim.mLastNotification) {
+        // Notify 'animationstart' even if a negative delay puts us
+        // past the first iteration.
+        // Note that when somebody changes the animation-duration
+        // dynamically, this will fire an extra iteration event
+        // immediately in many cases.  It's not clear to me if that's the
+        // right thing to do.
+        PRUint32 message =
+          anim.mLastNotification == ElementAnimation::LAST_NOTIFICATION_NONE
+            ? NS_ANIMATION_START : NS_ANIMATION_ITERATION;
+        anim.mLastNotification = whichIteration;
+        AnimationEventInfo ei(mElement, anim.mName, message,
+                              currentTimeDuration);
+        aEventsToDispatch.AppendElement(ei);
+      }
+
       NS_ABORT_IF_FALSE(0.0 <= positionInIteration &&
                           positionInIteration <= 1.0,
                         "position should be in [0-1]");
 
       NS_ABORT_IF_FALSE(anim.mSegments[0].mFromKey == 0.0,
                         "incorrect first from key");
       NS_ABORT_IF_FALSE(anim.mSegments[anim.mSegments.Length() - 1].mToKey
                           == 1.0,
@@ -445,16 +496,17 @@ nsAnimationManager::CheckAnimationRule(n
               break;
             }
           }
           if (!oldAnim) {
             continue;
           }
 
           newAnim->mStartTime = oldAnim->mStartTime;
+          newAnim->mLastNotification = oldAnim->mLastNotification;
 
           if (oldAnim->IsPaused()) {
             if (newAnim->IsPaused()) {
               // Copy pause start just like start time.
               newAnim->mPauseStart = oldAnim->mPauseStart;
             } else {
               // Handle change in pause state by adjusting start
               // time to unpause.
@@ -465,17 +517,21 @@ nsAnimationManager::CheckAnimationRule(n
       }
     } else {
       ea = GetElementAnimations(aElement, aStyleContext->GetPseudoType(),
                                 PR_TRUE);
     }
     ea->mAnimations.SwapElements(newAnimations);
     ea->mNeedsRefreshes = true;
 
-    ea->EnsureStyleRuleFor(refreshTime);
+    ea->EnsureStyleRuleFor(refreshTime, mPendingEvents);
+    // We don't actually dispatch the mPendingEvents now.  We'll either
+    // dispatch them the next time we get a refresh driver notification
+    // or the next time somebody calls
+    // nsPresShell::FlushPendingNotifications.
   }
 
   return GetAnimationRule(aElement, aStyleContext->GetPseudoType());
 }
 
 class PercentageHashKey : public PLDHashEntryHdr
 {
 public:
@@ -776,21 +832,39 @@ nsAnimationManager::WillRefresh(mozilla:
 
   // FIXME: check that there's at least one style rule that's not
   // in its "done" state, and if there isn't, remove ourselves from
   // the refresh driver (but leave the animations!).
   for (PRCList *l = PR_LIST_HEAD(&mElementData); l != &mElementData;
        l = PR_NEXT_LINK(l)) {
     ElementAnimations *ea = static_cast<ElementAnimations*>(l);
     nsRefPtr<css::AnimValuesStyleRule> oldStyleRule = ea->mStyleRule;
-    ea->EnsureStyleRuleFor(mPresContext->RefreshDriver()->MostRecentRefresh());
+    ea->EnsureStyleRuleFor(mPresContext->RefreshDriver()->MostRecentRefresh(),
+                           mPendingEvents);
     if (oldStyleRule != ea->mStyleRule) {
       ea->PostRestyleForAnimation(mPresContext);
     }
   }
+
+  DispatchEvents(); // may destroy us
+}
+
+void
+nsAnimationManager::DispatchEvents()
+{
+  EventArray events;
+  mPendingEvents.SwapElements(events);
+  for (PRUint32 i = 0, i_end = events.Length(); i < i_end; ++i) {
+    AnimationEventInfo &info = events[i];
+    nsEventDispatcher::Dispatch(info.mElement, mPresContext, &info.mEvent);
+
+    if (!mPresContext) {
+      break;
+    }
+  }
 }
 
 nsCSSKeyframesRule*
 nsAnimationManager::KeyframesRuleFor(const nsSubstring& aName)
 {
   if (mKeyframesListIsDirty) {
     mKeyframesListIsDirty = false;
 
--- a/layout/style/nsAnimationManager.h
+++ b/layout/style/nsAnimationManager.h
@@ -37,16 +37,19 @@
  * ***** END LICENSE BLOCK ***** */
 #ifndef nsAnimationManager_h_
 #define nsAnimationManager_h_
 
 #include "AnimationCommon.h"
 #include "nsCSSPseudoElements.h"
 #include "nsStyleContext.h"
 #include "nsTHashtable.h"
+#include "nsGUIEvent.h"
+#include "mozilla/TimeStamp.h"
+#include "nsThreadUtils.h"
 
 class nsCSSKeyframesRule;
 struct AnimationSegment;
 struct ElementAnimation;
 struct ElementAnimations;
 
 namespace mozilla {
 namespace css {
@@ -59,16 +62,38 @@ class nsAnimationManager : public mozill
 public:
   nsAnimationManager(nsPresContext *aPresContext)
     : mozilla::css::CommonAnimationManager(aPresContext),
       mKeyframesListIsDirty(true)
   {
     mKeyframesRules.Init(16); // FIXME: make infallible!
   }
 
+  struct AnimationEventInfo {
+    nsRefPtr<mozilla::dom::Element> mElement;
+    nsAnimationEvent mEvent;
+
+    AnimationEventInfo(mozilla::dom::Element *aElement,
+                       const nsString& aAnimationName,
+                       PRUint32 aMessage, mozilla::TimeDuration aElapsedTime)
+      : mElement(aElement),
+        mEvent(PR_TRUE, aMessage, aAnimationName, aElapsedTime.ToSeconds())
+    {
+    }
+
+    // nsAnimationEvent doesn't support copy-construction, so we need
+    // to ourselves in order to work with nsTArray
+    AnimationEventInfo(const AnimationEventInfo &aOther)
+      : mElement(aOther.mElement),
+        mEvent(PR_TRUE, aOther.mEvent.message,
+               aOther.mEvent.animationName, aOther.mEvent.elapsedTime)
+    {
+    }
+  };
+
   // nsIStyleRuleProcessor (parts)
   virtual void RulesMatching(ElementRuleProcessorData* aData);
   virtual void RulesMatching(PseudoElementRuleProcessorData* aData);
   virtual void RulesMatching(AnonBoxRuleProcessorData* aData);
 #ifdef MOZ_XUL
   virtual void RulesMatching(XULTreeRuleProcessorData* aData);
 #endif
 
@@ -88,16 +113,27 @@ public:
    */
   nsIStyleRule* CheckAnimationRule(nsStyleContext* aStyleContext,
                                    mozilla::dom::Element* aElement);
 
   void KeyframesListIsDirty() {
     mKeyframesListIsDirty = PR_TRUE;
   }
 
+  typedef InfallibleTArray<AnimationEventInfo> EventArray;
+
+  /**
+   * Dispatch any pending events.  We accumulate animationend and
+   * animationiteration events only during refresh driver notifications
+   * (and dispatch them at the end of such notifications), but we
+   * accumulate animationstart events at other points when style
+   * contexts are created.
+   */
+  void DispatchEvents();
+
 private:
   ElementAnimations* GetElementAnimations(mozilla::dom::Element *aElement,
                                           nsCSSPseudoElements::Type aPseudoType,
                                           PRBool aCreateIfNeeded);
   void BuildAnimations(nsStyleContext* aStyleContext,
                        InfallibleTArray<ElementAnimation>& aAnimations);
   void BuildSegment(InfallibleTArray<AnimationSegment>& aSegments,
                     const nsAnimation& aAnimation,
@@ -107,11 +143,13 @@ private:
                     mozilla::css::Declaration* aToDeclaration);
   nsIStyleRule* GetAnimationRule(mozilla::dom::Element* aElement,
                                  nsCSSPseudoElements::Type aPseudoType);
 
   nsCSSKeyframesRule* KeyframesRuleFor(const nsSubstring& aName);
 
   bool mKeyframesListIsDirty;
   nsDataHashtable<nsStringHashKey, nsCSSKeyframesRule*> mKeyframesRules;
+
+  EventArray mPendingEvents;
 };
 
 #endif /* !defined(nsAnimationManager_h_) */
--- a/layout/style/test/test_animations.html
+++ b/layout/style/test/test_animations.html
@@ -55,16 +55,21 @@ https://bugzilla.mozilla.org/show_bug.cg
   @-moz-keyframes kf_cascade2 { from, to { margin-left: 300px } }
   @-moz-keyframes kf_tf1 {
     0%   { padding-bottom: 20px; -moz-animation-timing-function: ease }
     25%  { padding-bottom: 60px; }
     50%  { padding-bottom: 160px; -moz-animation-timing-function: steps(5) }
     75%  { padding-bottom: 120px; -moz-animation-timing-function: linear }
     100% { padding-bottom: 20px; -moz-animation-timing-function: ease-out }
   }
+
+  #withbefore::before, #withafter::after {
+    content: "";
+    -moz-animation: anim2 1s linear alternate infinite;
+  }
   </style>
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=435442">Mozilla Bug 435442</a>
 <div id="display"></div>
 <pre id="test">
 <script type="application/javascript">
 "use strict";
@@ -73,32 +78,72 @@ https://bugzilla.mozilla.org/show_bug.cg
 
 function advance_clock(milliseconds) {
   SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(milliseconds);
 }
 
 var display = document.getElementById("display");
 var div = null;
 var cs = null;
+var events_received = [];
 function new_div(style) {
   if (div != null || cs != null) {
     ok(false, "test author forgot to call done_div");
   }
   if (typeof(style) != "string") {
     ok(false, "test author forgot to pass argument");
   }
   div = document.createElement("div");
   div.setAttribute("style", style);
   display.appendChild(div);
   cs = getComputedStyle(div, "");
 }
+function listen() {
+  events_received = [];
+  function listener(event) {
+    events_received.push(event);
+  }
+  div.addEventListener("animationstart", listener, false);
+  div.addEventListener("animationiteration", listener, false);
+  div.addEventListener("animationend", listener, false);
+}
+function check_events(events_expected, desc) {
+  // This function checks that the list of events_expected matches
+  // the received events -- but it only checks the properties that
+  // are present on events_expected.
+  is(events_received.length, events_expected.length,
+     "number of events received for " + desc);
+  for (var i = 0,
+       i_end = Math.min(events_expected.length, events_received.length);
+       i != i_end; ++i) {
+    var exp = events_expected[i];
+    var rec = events_received[i];
+    for (var prop in exp) {
+      if (prop == "elapsedTime") {
+        // Allow floating point error.
+        ok(Math.abs(rec.elapsedTime - exp.elapsedTime) < 0.000002,
+           "events[" + i + "]." + prop + " for " + desc +
+           " received=" + rec.elapsedTime + " expected=" + exp.elapsedTime);
+      } else {
+        is(rec[prop], exp[prop], "events[" + i + "]." + prop + " for " + desc);
+      }
+    }
+  }
+  for (var i = events_expected.length; i < events_received.length; ++i) {
+    ok(false, "unexpected " + events_received[i].type + " event for " + desc);
+  }
+  events_received = [];
+}
 function done_div() {
   display.removeChild(div);
   div = null;
   cs = null;
+  if (events_received.length) {
+    ok(false, "caller should have called check_events");
+  }
 }
 
 // take over the refresh driver right from the start.
 advance_clock(0);
 
 /*
  * css3-animations:  2. Animations
  * http://dev.w3.org/csswg/css3-animations/#animations
@@ -114,39 +159,50 @@ function test_fill_mode(fill_mode, fills
   var desc;
   if (fill_mode.length > 0) {
     style += " " + fill_mode;
     desc = "fill mode " + fill_mode + ": ";
   } else {
     desc = "default fill mode: ";
   }
   new_div(style);
+  listen();
   if (fills_backwards)
     is(cs.marginLeft, "0px", desc + "does affect value during delay (0s)");
   else
     is(cs.marginLeft, "30px", desc + "doesn't affect value during delay (0s)");
   advance_clock(2000);
   if (fills_backwards)
     is(cs.marginLeft, "0px", desc + "does affect value during delay (2s)");
   else
     is(cs.marginLeft, "30px", desc + "doesn't affect value during delay (2s)");
+  check_events([], "before start in test_fill_mode");
   advance_clock(1000);
+  check_events([{ type: 'animationstart', target: div,
+                  bubbles: true, cancelable: true,
+                  animationName: 'anim1', elapsedTime: 0.0 }],
+               "right after start in test_fill_mode");
   if (fills_backwards)
     is(cs.marginLeft, "0px", desc + "affects value at start of animation");
   advance_clock(125);
   is(cs.marginLeft, "2px", desc + "affects value during animation");
   advance_clock(2375);
   is(cs.marginLeft, "40px", desc + "affects value during animation");
   advance_clock(2500);
   is(cs.marginLeft, "80px", desc + "affects value during animation");
   advance_clock(2500);
   is(cs.marginLeft, "90px", desc + "affects value during animation");
   advance_clock(2375);
   is(cs.marginLeft, "99.5px", desc + "affects value during animation");
+  check_events([], "before end in test_fill_mode");
   advance_clock(125);
+  check_events([{ type: 'animationend', target: div,
+                  bubbles: true, cancelable: true,
+                  animationName: 'anim1', elapsedTime: 10.0 }],
+               "right after end in test_fill_mode");
   if (fills_forwards)
     is(cs.marginLeft, "100px", desc + "affects value at end of animation");
   advance_clock(10);
   if (fills_forwards)
     is(cs.marginLeft, "100px", desc + "does affect value after animation");
   else
     is(cs.marginLeft, "30px", desc + "does not affect value after animation");
   done_div();
@@ -971,29 +1027,95 @@ done_div();
 new_div("margin-top: 1000px");
 advance_clock(300);
 div.style.marginTop = "100px";
 div.style.MozAnimation = "kf1 1s -0.1s ease-in";
 is_approx(px_to_num(cs.marginTop), 100 - 50 * gTF.ease_in(0.2), 0.01,
           "delay and implicit starting values test");
 done_div();
 
+// test large negative delay that causes the animation to start
+// in the fourth iteration
+new_div("-moz-animation: anim2 1s -3.6s ease-in 5 alternate forwards");
+listen(); // rely on no flush having happened yet
+is_approx(px_to_num(cs.marginRight), 100 * gTF.ease_in(0.4), 0.01,
+          "large negative delay test at 0ms");
+check_events([{ type: 'animationstart', target: div,
+                animationName: 'anim2', elapsedTime: 3.6 }],
+             "right after start in large negative delay test");
+advance_clock(380);
+is_approx(px_to_num(cs.marginRight), 100 * gTF.ease_in(0.02), 0.01,
+          "large negative delay test at 380ms");
+check_events([]);
+advance_clock(20);
+is(cs.marginRight, "0px", "large negative delay test at 400ms");
+check_events([{ type: 'animationiteration', target: div,
+                animationName: 'anim2', elapsedTime: 4.0 }],
+             "right after start in large negative delay test");
+advance_clock(800);
+is_approx(px_to_num(cs.marginRight), 100 * gTF.ease_in(0.8), 0.01,
+          "large negative delay test at 1200ms");
+check_events([]);
+advance_clock(200);
+is(cs.marginRight, "100px", "large negative delay test at 1400ms");
+check_events([{ type: 'animationend', target: div,
+                animationName: 'anim2', elapsedTime: 5.0 }],
+             "right after start in large negative delay test");
+done_div();
+
 /*
  * css3-animations:  3.9. The 'animation-fill-mode' Property
  * http://dev.w3.org/csswg/css3-animations/#the-animation-fill-mode-property-
  */
 
 // animation-fill-mode is tested in the tests for section (2).
 
 /*
  * css3-animations:  3.10. The 'animation' Shorthand Property
  * http://dev.w3.org/csswg/css3-animations/#the-animation-shorthand-property-
  */
 
 // shorthand vs. longhand is adequately tested by the
 // property_database.js-based tests.
 
+/**
+ * Basic tests of animations on pseudo-elements
+ */
+new_div("");
+listen();
+div.id = "withbefore";
+var cs_before = getComputedStyle(div, ":before");
+is(cs_before.marginRight, "0px", ":before test at 0ms");
+advance_clock(400);
+is(cs_before.marginRight, "40px", ":before test at 400ms");
+advance_clock(800);
+is(cs_before.marginRight, "80px", ":before test at 1200ms");
+is(cs.marginRight, "0px", ":before animation should not affect element");
+advance_clock(800);
+is(cs_before.marginRight, "0px", ":before test at 2000ms");
+advance_clock(300);
+is(cs_before.marginRight, "30px", ":before test at 2300ms");
+check_events([], "no events should be fired for animations on :before");
+done_div();
+
+new_div("");
+listen();
+div.id = "withafter";
+var cs_after = getComputedStyle(div, ":after");
+is(cs_after.marginRight, "0px", ":after test at 0ms");
+advance_clock(400);
+is(cs_after.marginRight, "40px", ":after test at 400ms");
+advance_clock(800);
+is(cs_after.marginRight, "80px", ":after test at 1200ms");
+is(cs.marginRight, "0px", ":after animation should not affect element");
+advance_clock(800);
+is(cs_after.marginRight, "0px", ":after test at 2000ms");
+advance_clock(300);
+is(cs_after.marginRight, "30px", ":after test at 2300ms");
+check_events([], "no events should be fired for animations on :after");
+done_div();
+
 SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
 
 </script>
 </pre>
 </body>
 </html>