Bug 1354501 - Dispatch web animation events at the same time when CSS animations/transitions events are dispatched. r?birtles
MozReview-Commit-ID: u7lWtAF8Ml
--- a/dom/animation/Animation.cpp
+++ b/dom/animation/Animation.cpp
@@ -4,19 +4,19 @@
* 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/dom/DocumentTimeline.h"
+#include "mozilla/AnimationEventDispatcher.h"
#include "mozilla/AnimationTarget.h"
#include "mozilla/AutoRestore.h"
-#include "mozilla/AsyncEventDispatcher.h" // For AsyncEventDispatcher
#include "mozilla/Maybe.h" // For Maybe
#include "mozilla/TypeTraits.h" // For std::forward<>
#include "nsAnimationManager.h" // For CSSAnimation
#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch
#include "nsIDocument.h" // For nsIDocument
#include "nsIPresShell.h" // For nsIPresShell
#include "nsThreadUtils.h" // For nsRunnableMethod and nsRevocableEventPtr
#include "nsTransitionManager.h" // For CSSTransition
@@ -869,17 +869,18 @@ Animation::CancelNoUpdate()
if (PlayState() != AnimationPlayState::Idle) {
ResetPendingTasks();
if (mFinished) {
mFinished->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
}
ResetFinishedPromise();
- DispatchPlaybackEvent(NS_LITERAL_STRING("cancel"));
+ // FIXME: Bug 1472900 - Use the timestamp associated with the timeline.
+ QueuePlaybackEvent(NS_LITERAL_STRING("cancel"), TimeStamp());
}
StickyTimeDuration activeTime = mEffect
? mEffect->GetComputedTiming().mActiveTime
: StickyTimeDuration();
mHoldTime.SetNull();
mStartTime.SetNull();
@@ -1550,16 +1551,22 @@ Animation::GetRenderedDocument() const
{
if (!mEffect || !mEffect->AsKeyframeEffect()) {
return nullptr;
}
return mEffect->AsKeyframeEffect()->GetRenderedDocument();
}
+nsIDocument*
+Animation::GetTimelineDocument() const
+{
+ return mTimeline ? mTimeline->GetDocument() : nullptr;
+}
+
class AsyncFinishNotification : public MicroTaskRunnable
{
public:
explicit AsyncFinishNotification(Animation* aAnimation)
: MicroTaskRunnable()
, mAnimation(aAnimation)
{}
@@ -1619,38 +1626,54 @@ Animation::DoFinishNotificationImmediate
mFinishNotificationTask = nullptr;
if (PlayState() != AnimationPlayState::Finished) {
return;
}
MaybeResolveFinishedPromise();
- DispatchPlaybackEvent(NS_LITERAL_STRING("finish"));
+ QueuePlaybackEvent(NS_LITERAL_STRING("finish"),
+ AnimationTimeToTimeStamp(EffectEnd()));
}
void
-Animation::DispatchPlaybackEvent(const nsAString& aName)
+Animation::QueuePlaybackEvent(const nsAString& aName,
+ TimeStamp&& aScheduledEventTime)
{
+ // Use document for timing.
+ // https://drafts.csswg.org/web-animations-1/#document-for-timing
+ nsIDocument* doc = GetTimelineDocument();
+ if (!doc) {
+ return;
+ }
+
+ nsPresContext* presContext = doc->GetPresContext();
+ if (!presContext) {
+ return;
+ }
+
AnimationPlaybackEventInit init;
if (aName.EqualsLiteral("finish")) {
init.mCurrentTime = GetCurrentTimeAsDouble();
}
if (mTimeline) {
init.mTimelineTime = mTimeline->GetCurrentTimeAsDouble();
}
RefPtr<AnimationPlaybackEvent> event =
AnimationPlaybackEvent::Constructor(this, aName, init);
event->SetTrusted(true);
- RefPtr<AsyncEventDispatcher> asyncDispatcher =
- new AsyncEventDispatcher(this, event);
- asyncDispatcher->PostDOMEvent();
+ presContext->AnimationEventDispatcher()->
+ QueueEvent(AnimationEventInfo(aName,
+ std::move(event),
+ std::move(aScheduledEventTime),
+ this));
}
bool
Animation::IsRunningOnCompositor() const
{
return mEffect &&
mEffect->AsKeyframeEffect() &&
mEffect->AsKeyframeEffect()->IsRunningOnCompositor();
--- a/dom/animation/Animation.h
+++ b/dom/animation/Animation.h
@@ -439,17 +439,18 @@ protected:
*/
void FlushUnanimatedStyle() const;
void PostUpdate();
void ResetFinishedPromise();
void MaybeResolveFinishedPromise();
void DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag);
friend class AsyncFinishNotification;
void DoFinishNotificationImmediately(MicroTaskRunnable* aAsync = nullptr);
- void DispatchPlaybackEvent(const nsAString& aName);
+ void QueuePlaybackEvent(const nsAString& aName,
+ TimeStamp&& aScheduledEventTime);
/**
* 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();
@@ -517,16 +518,17 @@ protected:
static constexpr StickyTimeDuration zeroDuration = StickyTimeDuration();
return std::max(
std::min((EffectEnd() - mEffect->SpecifiedTiming().Delay()),
aActiveDuration),
zeroDuration);
}
nsIDocument* GetRenderedDocument() const;
+ nsIDocument* GetTimelineDocument() const;
RefPtr<AnimationTimeline> mTimeline;
RefPtr<AnimationEffect> mEffect;
// The beginning of the delay period.
Nullable<TimeDuration> mStartTime; // Timeline timescale
Nullable<TimeDuration> mHoldTime; // Animation timescale
Nullable<TimeDuration> mPendingReadyTime; // Timeline timescale
Nullable<TimeDuration> mPreviousCurrentTime; // Animation timescale
--- a/dom/animation/AnimationEventDispatcher.h
+++ b/dom/animation/AnimationEventDispatcher.h
@@ -8,31 +8,34 @@
#define mozilla_AnimationEventDispatcher_h
#include <algorithm> // For <std::stable_sort>
#include "mozilla/AnimationComparator.h"
#include "mozilla/Assertions.h"
#include "mozilla/ContentEvents.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/Variant.h"
+#include "mozilla/dom/AnimationPlaybackEvent.h"
#include "nsCSSProps.h"
#include "nsCycleCollectionParticipant.h"
class nsPresContext;
class nsRefreshDriver;
namespace mozilla {
struct AnimationEventInfo
{
RefPtr<dom::EventTarget> mTarget;
RefPtr<dom::Animation> mAnimation;
TimeStamp mTimeStamp;
- typedef Variant<InternalTransitionEvent, InternalAnimationEvent> EventVariant;
+ typedef Variant<InternalTransitionEvent,
+ InternalAnimationEvent,
+ RefPtr<dom::AnimationPlaybackEvent>> EventVariant;
EventVariant mEvent;
// For CSS animation events
AnimationEventInfo(nsAtom* aAnimationName,
const NonOwningAnimationTarget& aTarget,
EventMessage aMessage,
double aElapsedTime,
const TimeStamp& aTimeStamp,
@@ -68,33 +71,86 @@ struct AnimationEventInfo
event.mPropertyName =
NS_ConvertUTF8toUTF16(nsCSSProps::GetStringValue(aProperty));
// XXX Looks like nobody initialize WidgetEvent::time
event.mElapsedTime = aElapsedTime;
event.mPseudoElement =
nsCSSPseudoElements::PseudoTypeAsString(aTarget.mPseudoType);
}
+ // For web animation events
+ AnimationEventInfo(const nsAString& aName,
+ RefPtr<dom::AnimationPlaybackEvent>&& aEvent,
+ TimeStamp&& aTimeStamp,
+ dom::Animation* aAnimation)
+ : mTarget(aAnimation)
+ , mAnimation(aAnimation)
+ , mTimeStamp(std::move(aTimeStamp))
+ , mEvent(std::move(aEvent))
+ {
+ }
+
AnimationEventInfo(const AnimationEventInfo& aOther) = delete;
AnimationEventInfo& operator=(const AnimationEventInfo& aOther) = delete;
AnimationEventInfo(AnimationEventInfo&& aOther) = default;
AnimationEventInfo& operator=(AnimationEventInfo&& aOther) = default;
+ bool IsWebAnimationEvent() const
+ {
+ return mEvent.is<RefPtr<dom::AnimationPlaybackEvent>>();
+ }
+
+#ifdef DEBUG
+ bool IsStale() const
+ {
+ const WidgetEvent* widgetEvent = AsWidgetEvent();
+ return widgetEvent->mFlags.mIsBeingDispatched ||
+ widgetEvent->mFlags.mDispatchedAtLeastOnce;
+ }
+
+ const WidgetEvent* AsWidgetEvent() const
+ {
+ return const_cast<AnimationEventInfo*>(this)->AsWidgetEvent();
+ }
+#endif
+
WidgetEvent* AsWidgetEvent()
{
if (mEvent.is<InternalTransitionEvent>()) {
return &mEvent.as<InternalTransitionEvent>();
}
if (mEvent.is<InternalAnimationEvent>()) {
return &mEvent.as<InternalAnimationEvent>();
}
+ if (mEvent.is<RefPtr<dom::AnimationPlaybackEvent>>()) {
+ return mEvent.as<RefPtr<dom::AnimationPlaybackEvent>>()
+ ->WidgetEventPtr();
+ }
MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Unexpected event type");
return nullptr;
}
+
+ void Dispatch(nsPresContext* aPresContext)
+ {
+ if (mEvent.is<RefPtr<dom::AnimationPlaybackEvent>>()) {
+ EventDispatcher::DispatchDOMEvent(
+ mTarget,
+ nullptr /* WidgetEvent */,
+ mEvent.as<RefPtr<dom::AnimationPlaybackEvent>>(),
+ aPresContext,
+ nullptr /* nsEventStatus */);
+ return;
+ }
+
+ MOZ_ASSERT(mEvent.is<InternalTransitionEvent>() ||
+ mEvent.is<InternalAnimationEvent>());
+
+ EventDispatcher::Dispatch(mTarget, aPresContext, AsWidgetEvent());
+ }
};
class AnimationEventDispatcher final
{
public:
explicit AnimationEventDispatcher(nsPresContext* aPresContext)
: mPresContext(aPresContext)
, mIsSorted(true)
@@ -121,22 +177,18 @@ public:
SortEvents();
EventArray events;
mPendingEvents.SwapElements(events);
// mIsSorted will be set to true by SortEvents above, and we leave it
// that way since mPendingEvents is now empty
for (AnimationEventInfo& info : events) {
- MOZ_ASSERT(!info.AsWidgetEvent()->mFlags.mIsBeingDispatched &&
- !info.AsWidgetEvent()->mFlags.mDispatchedAtLeastOnce,
- "The WidgetEvent should be fresh");
- EventDispatcher::Dispatch(info.mTarget,
- mPresContext,
- info.AsWidgetEvent());
+ MOZ_ASSERT(!info.IsStale(), "The event shouldn't be stale");
+ info.Dispatch(mPresContext);
// Bail out if our mPresContext was nullified due to destroying the pres
// context.
if (!mPresContext) {
break;
}
}
}
@@ -169,16 +221,21 @@ private:
// Null timestamps sort first
if (a.mTimeStamp.IsNull() || b.mTimeStamp.IsNull()) {
return a.mTimeStamp.IsNull();
} else {
return a.mTimeStamp < b.mTimeStamp;
}
}
+ // Events in the Web Animations spec are prior to CSS events.
+ if (a.IsWebAnimationEvent() != b.IsWebAnimationEvent()) {
+ return a.IsWebAnimationEvent();
+ }
+
AnimationPtrComparator<RefPtr<dom::Animation>> comparator;
return comparator.LessThan(a.mAnimation, b.mAnimation);
}
};
// Sort all pending CSS animation/transition events by scheduled event time
// and composite order.
// https://drafts.csswg.org/web-animations/#update-animations-and-send-events
--- a/dom/animation/AnimationTimeline.h
+++ b/dom/animation/AnimationTimeline.h
@@ -18,16 +18,18 @@
#include "nsTHashtable.h"
// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to
// GetTickCount().
#ifdef GetCurrentTime
#undef GetCurrentTime
#endif
+class nsIDocument;
+
namespace mozilla {
namespace dom {
class Animation;
class AnimationTimeline
: public nsISupports
, public nsWrapperCache
@@ -99,16 +101,18 @@ public:
* time.
*/
bool HasAnimations() const {
return !mAnimations.IsEmpty();
}
virtual void RemoveAnimation(Animation* aAnimation);
+ virtual nsIDocument* GetDocument() const = 0;
+
protected:
nsCOMPtr<nsIGlobalObject> mWindow;
// Animations observing this timeline
//
// We store them in (a) a hashset for quick lookup, and (b) an array
// to maintain a fixed sampling order.
//
--- a/dom/animation/DocumentTimeline.h
+++ b/dom/animation/DocumentTimeline.h
@@ -84,16 +84,18 @@ public:
void RemoveAnimation(Animation* aAnimation) override;
// nsARefreshObserver methods
void WillRefresh(TimeStamp aTime) override;
void NotifyRefreshDriverCreated(nsRefreshDriver* aDriver);
void NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver);
+ nsIDocument* GetDocument() const override { return mDocument; }
+
protected:
TimeStamp GetCurrentTimeStamp() const;
nsRefreshDriver* GetRefreshDriver() const;
void UnregisterFromRefreshDriver();
nsCOMPtr<nsIDocument> mDocument;
// The most recently used refresh driver time. This is used in cases where
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -377893,16 +377893,22 @@
]
],
"web-animations/timing-model/timelines/timelines.html": [
[
"/web-animations/timing-model/timelines/timelines.html",
{}
]
],
+ "web-animations/timing-model/timelines/update-and-send-events.html": [
+ [
+ "/web-animations/timing-model/timelines/update-and-send-events.html",
+ {}
+ ]
+ ],
"web-nfc/idlharness.https.html": [
[
"/web-nfc/idlharness.https.html",
{}
]
],
"web-nfc/nfc_insecure_context.html": [
[
@@ -618764,16 +618770,20 @@
"web-animations/timing-model/timelines/document-timelines.html": [
"d0fcb390c19c9ede7288278dc11ea5b3d33671cb",
"testharness"
],
"web-animations/timing-model/timelines/timelines.html": [
"29d7fe91c355fc22f563ca17315d2ab493dc0566",
"testharness"
],
+ "web-animations/timing-model/timelines/update-and-send-events.html": [
+ "1f60aba2afa57960f17f19aa5ea1ff0370ec0b74",
+ "testharness"
+ ],
"web-nfc/OWNERS": [
"d42f3f15d00686bf5a5c7c69169ef5cf2554bd7b",
"support"
],
"web-nfc/idlharness.https.html": [
"4e939e8328c0fa1ffde6a0e5a259fc790db84551",
"testharness"
],
--- a/testing/web-platform/mozilla/meta/MANIFEST.json
+++ b/testing/web-platform/mozilla/meta/MANIFEST.json
@@ -1039,17 +1039,17 @@
}
},
"paths": {
"./placeholder": [
"74e16eb87ecdfeb2dfc28f36e0c73a584abdf9c2",
"support"
],
"baselinecoverage/wpt_baselinecoverage.html": [
- "7bb4029a6718dbdb06b33fb2d86aef3e7344e20e",
+ "86c9040e69d1b7eab0106fa49bd4582549be99a6",
"testharness"
],
"binast/insecure.html": [
"04b9d7807d0adf6a471d0621f18ddf20010fdac1",
"testharness"
],
"binast/large.binjs": [
"b83da2863c47d8cedec22bf136fbea077ba8a86b",
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html
@@ -0,0 +1,191 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Update animations and send events</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#update-animations-and-send-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ // The ready promise should be resolved as part of micro-task checkpoint
+ // after updating the current time of all timeslines in the procedure to
+ // "update animations and send events".
+ await animation.ready;
+
+ let rAFReceived = false;
+ requestAnimationFrame(() => {
+ rAFReceived = true;
+ });
+
+ const eventWatcher = new EventWatcher(t, animation, 'cancel');
+ animation.cancel();
+
+ await eventWatcher.wait_for('cancel');
+
+ assert_false(rAFReceived,
+ 'cancel event should be fired before requestAnimationFrame');
+}, 'Fires cancel event before requestAnimationFrame');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ // Like the above test, the ready promise should be resolved micro-task
+ // checkpoint after updating the current time of all timeslines in the
+ // procedure to "update animations and send events".
+ await animation.ready;
+
+ let rAFReceived = false;
+ requestAnimationFrame(() => {
+ rAFReceived = true;
+ });
+
+ const eventWatcher = new EventWatcher(t, animation, 'finish');
+ animation.finish();
+
+ await eventWatcher.wait_for('finish');
+
+ assert_false(rAFReceived,
+ 'finish event should be fired before requestAnimationFrame');
+}, 'Fires finish event before requestAnimationFrame');
+
+function animationType(anim) {
+ if (anim instanceof CSSAnimation) {
+ return 'CSSAnimation';
+ } else if (anim instanceof CSSTransition) {
+ return 'CSSTransition';
+ } else {
+ return 'ScriptAnimation';
+ }
+}
+
+promise_test(async t => {
+ createStyle(t, { '@keyframes anim': '' });
+ const div = createDiv(t);
+
+ getComputedStyle(div).marginLeft;
+ div.style = 'animation: anim 100s; ' +
+ 'transition: margin-left 100s; ' +
+ 'margin-left: 100px;';
+ div.animate(null, 100 * MS_PER_SEC);
+ const animations = div.getAnimations();
+
+ let receivedEvents = [];
+ animations.forEach(anim => {
+ anim.onfinish = event => {
+ receivedEvents.push({
+ type: animationType(anim) + ':' + event.type,
+ timeStamp: event.timeStamp
+ });
+ };
+ });
+
+ await Promise.all(animations.map(anim => anim.ready));
+
+ // Setting current time to the time just before the effect end.
+ animations.forEach(anim => anim.currentTime = 100 * MS_PER_SEC - 1);
+
+ await waitForNextFrame();
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ [ 'CSSTransition:finish', 'CSSAnimation:finish',
+ 'ScriptAnimation:finish' ],
+ 'finish events for various animation type should be sorted by composite ' +
+ 'order');
+}, 'Sorts finish events by composite order');
+
+promise_test(async t => {
+ createStyle(t, { '@keyframes anim': '' });
+ const div = createDiv(t);
+
+ let receivedEvents = [];
+ function receiveEvent(type, timeStamp) {
+ receivedEvents.push({ type, timeStamp });
+ }
+
+ div.onanimationcancel = event => {
+ receiveEvent(event.type, event.timeStamp);
+ };
+ div.ontransitioncancel = event => {
+ receiveEvent(event.type, event.timeStamp);
+ };
+
+ getComputedStyle(div).marginLeft;
+ div.style = 'animation: anim 100s; ' +
+ 'transition: margin-left 100s; ' +
+ 'margin-left: 100px;';
+ div.animate(null, 100 * MS_PER_SEC);
+ const animations = div.getAnimations();
+
+ animations.forEach(anim => {
+ anim.oncancel = event => {
+ receiveEvent(animationType(anim) + ':' + event.type, event.timeStamp);
+ };
+ });
+
+ await Promise.all(animations.map(anim => anim.ready));
+
+ const timeInAnimationReady = document.timeline.currentTime;
+
+ // Call cancel() in reverse composite order. I.e. canceling for script
+ // animation happen first, then for CSS animation and CSS transition.
+ // 'cancel' events for these animations should be sorted by composite
+ // order.
+ animations.reverse().forEach(anim => anim.cancel());
+
+ // requestAnimationFrame callback which is actually the _same_ frame since we
+ // are currently operating in the `ready` callbac of the animations which
+ // happens as part of the "Update animations and send events" procedure
+ // _before_ we run animation frame callbacks.
+ await waitForAnimationFrames(1);
+
+ assert_times_equal(timeInAnimationReady, document.timeline.currentTime,
+ 'A rAF callback should happen in the same frame');
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ // This ordering needs more clarification in the spec, but the intention is
+ // that the cancel playback event fires before the equivalent CSS cancel
+ // event in each case.
+ [ 'CSSTransition:cancel', 'CSSAnimation:cancel', 'ScriptAnimation:cancel',
+ 'transitioncancel', 'animationcancel' ],
+ 'cancel events should be sorted by composite order');
+}, 'Sorts cancel events by composite order');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ getComputedStyle(div).marginLeft;
+ div.style = 'transition: margin-left 100s; margin-left: 100px;';
+ const anim = div.getAnimations()[0];
+
+ let receivedEvents = [];
+ anim.oncancel = event => {
+ receivedEvents.push(event);
+ };
+
+ const eventWatcher = new EventWatcher(t, div, 'transitionstart');
+ await eventWatcher.wait_for('transitionstart');
+
+ const timeInEventCallback = document.timeline.currentTime;
+
+ // Calling cancel() queues a cancel event
+ anim.cancel();
+
+ await waitForAnimationFrames(1);
+ assert_times_equal(timeInEventCallback, document.timeline.currentTime,
+ 'A rAF callback should happen in the same frame');
+
+ assert_array_equals(receivedEvents, [],
+ 'The queued cancel event shouldn\'t be dispatched in the same frame');
+
+ await waitForAnimationFrames(1);
+ assert_array_equals(receivedEvents.map(event => event.type), ['cancel'],
+ 'The cancel event should be dispatched in a later frame');
+}, 'Queues a cancel event in transitionstart event callback');
+
+</script>