summaryrefslogtreecommitdiff
path: root/dom/animation
diff options
context:
space:
mode:
Diffstat (limited to 'dom/animation')
-rw-r--r--dom/animation/AnimValuesStyleRule.cpp110
-rw-r--r--dom/animation/AnimValuesStyleRule.h58
-rw-r--r--dom/animation/Animation.cpp1426
-rw-r--r--dom/animation/Animation.h453
-rw-r--r--dom/animation/AnimationComparator.h32
-rw-r--r--dom/animation/AnimationEffectReadOnly.cpp343
-rw-r--r--dom/animation/AnimationEffectReadOnly.h102
-rw-r--r--dom/animation/AnimationEffectTiming.cpp152
-rw-r--r--dom/animation/AnimationEffectTiming.h49
-rw-r--r--dom/animation/AnimationEffectTimingReadOnly.cpp51
-rw-r--r--dom/animation/AnimationEffectTimingReadOnly.h63
-rw-r--r--dom/animation/AnimationPerformanceWarning.cpp79
-rw-r--r--dom/animation/AnimationPerformanceWarning.h79
-rw-r--r--dom/animation/AnimationTarget.h78
-rw-r--r--dom/animation/AnimationTimeline.cpp63
-rw-r--r--dom/animation/AnimationTimeline.h125
-rw-r--r--dom/animation/AnimationUtils.cpp81
-rw-r--r--dom/animation/AnimationUtils.h74
-rw-r--r--dom/animation/CSSPseudoElement.cpp123
-rw-r--r--dom/animation/CSSPseudoElement.h91
-rw-r--r--dom/animation/ComputedTiming.h78
-rw-r--r--dom/animation/ComputedTimingFunction.cpp194
-rw-r--r--dom/animation/ComputedTimingFunction.h65
-rw-r--r--dom/animation/DocumentTimeline.cpp283
-rw-r--r--dom/animation/DocumentTimeline.h111
-rw-r--r--dom/animation/EffectCompositor.cpp920
-rw-r--r--dom/animation/EffectCompositor.h307
-rw-r--r--dom/animation/EffectSet.cpp177
-rw-r--r--dom/animation/EffectSet.h261
-rw-r--r--dom/animation/KeyframeEffect.cpp211
-rw-r--r--dom/animation/KeyframeEffect.h82
-rw-r--r--dom/animation/KeyframeEffectParams.cpp169
-rw-r--r--dom/animation/KeyframeEffectParams.h68
-rw-r--r--dom/animation/KeyframeEffectReadOnly.cpp1430
-rw-r--r--dom/animation/KeyframeEffectReadOnly.h439
-rw-r--r--dom/animation/KeyframeUtils.cpp1667
-rw-r--r--dom/animation/KeyframeUtils.h151
-rw-r--r--dom/animation/PendingAnimationTracker.cpp124
-rw-r--r--dom/animation/PendingAnimationTracker.h84
-rw-r--r--dom/animation/PseudoElementHashEntry.h58
-rw-r--r--dom/animation/TimingParams.cpp182
-rw-r--r--dom/animation/TimingParams.h130
-rw-r--r--dom/animation/moz.build67
-rw-r--r--dom/animation/test/chrome.ini17
-rw-r--r--dom/animation/test/chrome/file_animate_xrays.html19
-rw-r--r--dom/animation/test/chrome/test_animate_xrays.html31
-rw-r--r--dom/animation/test/chrome/test_animation_observers.html1177
-rw-r--r--dom/animation/test/chrome/test_animation_performance_warning.html957
-rw-r--r--dom/animation/test/chrome/test_animation_properties.html993
-rw-r--r--dom/animation/test/chrome/test_generated_content_getAnimations.html83
-rw-r--r--dom/animation/test/chrome/test_observers_for_sync_api.html854
-rw-r--r--dom/animation/test/chrome/test_restyles.html815
-rw-r--r--dom/animation/test/chrome/test_running_on_compositor.html966
-rw-r--r--dom/animation/test/crashtests/1216842-1.html35
-rw-r--r--dom/animation/test/crashtests/1216842-2.html35
-rw-r--r--dom/animation/test/crashtests/1216842-3.html27
-rw-r--r--dom/animation/test/crashtests/1216842-4.html27
-rw-r--r--dom/animation/test/crashtests/1216842-5.html38
-rw-r--r--dom/animation/test/crashtests/1216842-6.html38
-rw-r--r--dom/animation/test/crashtests/1239889-1.html12
-rw-r--r--dom/animation/test/crashtests/1244595-1.html3
-rw-r--r--dom/animation/test/crashtests/1272475-1.html20
-rw-r--r--dom/animation/test/crashtests/1272475-2.html20
-rw-r--r--dom/animation/test/crashtests/1277272-1-inner.html19
-rw-r--r--dom/animation/test/crashtests/1277272-1.html26
-rw-r--r--dom/animation/test/crashtests/1278485-1.html26
-rw-r--r--dom/animation/test/crashtests/1290535-1.html20
-rw-r--r--dom/animation/test/crashtests/crashtests.list13
-rw-r--r--dom/animation/test/css-animations/file_animation-cancel.html154
-rw-r--r--dom/animation/test/css-animations/file_animation-computed-timing.html566
-rw-r--r--dom/animation/test/css-animations/file_animation-currenttime.html345
-rw-r--r--dom/animation/test/css-animations/file_animation-finish.html97
-rw-r--r--dom/animation/test/css-animations/file_animation-finished.html93
-rw-r--r--dom/animation/test/css-animations/file_animation-id.html24
-rw-r--r--dom/animation/test/css-animations/file_animation-pausing.html165
-rw-r--r--dom/animation/test/css-animations/file_animation-playstate.html71
-rw-r--r--dom/animation/test/css-animations/file_animation-ready.html149
-rw-r--r--dom/animation/test/css-animations/file_animation-reverse.html29
-rw-r--r--dom/animation/test/css-animations/file_animation-starttime.html383
-rw-r--r--dom/animation/test/css-animations/file_animations-dynamic-changes.html154
-rw-r--r--dom/animation/test/css-animations/file_cssanimation-animationname.html37
-rw-r--r--dom/animation/test/css-animations/file_document-get-animations.html276
-rw-r--r--dom/animation/test/css-animations/file_effect-target.html54
-rw-r--r--dom/animation/test/css-animations/file_element-get-animations.html445
-rw-r--r--dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html672
-rw-r--r--dom/animation/test/css-animations/file_pseudoElement-get-animations.html70
-rw-r--r--dom/animation/test/css-animations/test_animation-cancel.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-computed-timing.html16
-rw-r--r--dom/animation/test/css-animations/test_animation-currenttime.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-finish.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-finished.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-id.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-pausing.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-playstate.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-ready.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-reverse.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-starttime.html15
-rw-r--r--dom/animation/test/css-animations/test_animations-dynamic-changes.html15
-rw-r--r--dom/animation/test/css-animations/test_cssanimation-animationname.html15
-rw-r--r--dom/animation/test/css-animations/test_document-get-animations.html15
-rw-r--r--dom/animation/test/css-animations/test_effect-target.html15
-rw-r--r--dom/animation/test/css-animations/test_element-get-animations.html15
-rw-r--r--dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html15
-rw-r--r--dom/animation/test/css-animations/test_pseudoElement-get-animations.html14
-rw-r--r--dom/animation/test/css-transitions/file_animation-cancel.html165
-rw-r--r--dom/animation/test/css-transitions/file_animation-computed-timing.html315
-rw-r--r--dom/animation/test/css-transitions/file_animation-currenttime.html307
-rw-r--r--dom/animation/test/css-transitions/file_animation-finished.html61
-rw-r--r--dom/animation/test/css-transitions/file_animation-pausing.html50
-rw-r--r--dom/animation/test/css-transitions/file_animation-ready.html96
-rw-r--r--dom/animation/test/css-transitions/file_animation-starttime.html284
-rw-r--r--dom/animation/test/css-transitions/file_csstransition-events.html223
-rw-r--r--dom/animation/test/css-transitions/file_csstransition-transitionproperty.html24
-rw-r--r--dom/animation/test/css-transitions/file_document-get-animations.html93
-rw-r--r--dom/animation/test/css-transitions/file_effect-target.html66
-rw-r--r--dom/animation/test/css-transitions/file_element-get-animations.html147
-rw-r--r--dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html95
-rw-r--r--dom/animation/test/css-transitions/file_pseudoElement-get-animations.html45
-rw-r--r--dom/animation/test/css-transitions/file_setting-effect.html91
-rw-r--r--dom/animation/test/css-transitions/test_animation-cancel.html14
-rw-r--r--dom/animation/test/css-transitions/test_animation-computed-timing.html16
-rw-r--r--dom/animation/test/css-transitions/test_animation-currenttime.html14
-rw-r--r--dom/animation/test/css-transitions/test_animation-finished.html14
-rw-r--r--dom/animation/test/css-transitions/test_animation-pausing.html14
-rw-r--r--dom/animation/test/css-transitions/test_animation-ready.html14
-rw-r--r--dom/animation/test/css-transitions/test_animation-starttime.html14
-rw-r--r--dom/animation/test/css-transitions/test_csstransition-events.html14
-rw-r--r--dom/animation/test/css-transitions/test_csstransition-transitionproperty.html14
-rw-r--r--dom/animation/test/css-transitions/test_document-get-animations.html15
-rw-r--r--dom/animation/test/css-transitions/test_effect-target.html14
-rw-r--r--dom/animation/test/css-transitions/test_element-get-animations.html14
-rw-r--r--dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html14
-rw-r--r--dom/animation/test/css-transitions/test_pseudoElement-get-animations.html14
-rw-r--r--dom/animation/test/css-transitions/test_setting-effect.html14
-rw-r--r--dom/animation/test/document-timeline/file_document-timeline.html135
-rw-r--r--dom/animation/test/document-timeline/test_document-timeline.html14
-rw-r--r--dom/animation/test/document-timeline/test_request_animation_frame.html27
-rw-r--r--dom/animation/test/mochitest.ini111
-rw-r--r--dom/animation/test/mozilla/file_cubic_bezier_limits.html167
-rw-r--r--dom/animation/test/mozilla/file_deferred_start.html121
-rw-r--r--dom/animation/test/mozilla/file_disable_animations_api_core.html30
-rw-r--r--dom/animation/test/mozilla/file_disabled_properties.html73
-rw-r--r--dom/animation/test/mozilla/file_discrete-animations.html170
-rw-r--r--dom/animation/test/mozilla/file_document-timeline-origin-time-range.html30
-rw-r--r--dom/animation/test/mozilla/file_hide_and_show.html162
-rw-r--r--dom/animation/test/mozilla/file_partial_keyframes.html41
-rw-r--r--dom/animation/test/mozilla/file_set-easing.html34
-rw-r--r--dom/animation/test/mozilla/file_spacing_property_order.html33
-rw-r--r--dom/animation/test/mozilla/file_spacing_transform.html240
-rw-r--r--dom/animation/test/mozilla/file_transform_limits.html55
-rw-r--r--dom/animation/test/mozilla/file_transition_finish_on_compositor.html67
-rw-r--r--dom/animation/test/mozilla/file_underlying-discrete-value.html192
-rw-r--r--dom/animation/test/mozilla/test_cubic_bezier_limits.html14
-rw-r--r--dom/animation/test/mozilla/test_deferred_start.html14
-rw-r--r--dom/animation/test/mozilla/test_disable_animations_api_core.html14
-rw-r--r--dom/animation/test/mozilla/test_disabled_properties.html14
-rw-r--r--dom/animation/test/mozilla/test_discrete-animations.html18
-rw-r--r--dom/animation/test/mozilla/test_document-timeline-origin-time-range.html14
-rw-r--r--dom/animation/test/mozilla/test_hide_and_show.html14
-rw-r--r--dom/animation/test/mozilla/test_partial_keyframes.html14
-rw-r--r--dom/animation/test/mozilla/test_set-easing.html14
-rw-r--r--dom/animation/test/mozilla/test_spacing_property_order.html14
-rw-r--r--dom/animation/test/mozilla/test_spacing_transform.html14
-rw-r--r--dom/animation/test/mozilla/test_transform_limits.html14
-rw-r--r--dom/animation/test/mozilla/test_transition_finish_on_compositor.html14
-rw-r--r--dom/animation/test/mozilla/test_underlying-discrete-value.html15
-rw-r--r--dom/animation/test/style/file_animation-seeking-with-current-time.html121
-rw-r--r--dom/animation/test/style/file_animation-seeking-with-start-time.html121
-rw-r--r--dom/animation/test/style/file_animation-setting-effect.html125
-rw-r--r--dom/animation/test/style/file_animation-setting-spacing.html111
-rw-r--r--dom/animation/test/style/test_animation-seeking-with-current-time.html15
-rw-r--r--dom/animation/test/style/test_animation-seeking-with-start-time.html15
-rw-r--r--dom/animation/test/style/test_animation-setting-effect.html15
-rw-r--r--dom/animation/test/style/test_animation-setting-spacing.html14
-rw-r--r--dom/animation/test/testcommon.js216
175 files changed, 26146 insertions, 0 deletions
diff --git a/dom/animation/AnimValuesStyleRule.cpp b/dom/animation/AnimValuesStyleRule.cpp
new file mode 100644
index 0000000000..dbdffd5b5d
--- /dev/null
+++ b/dom/animation/AnimValuesStyleRule.cpp
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "AnimValuesStyleRule.h"
+#include "nsRuleData.h"
+#include "nsStyleContext.h"
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(AnimValuesStyleRule, nsIStyleRule)
+
+void
+AnimValuesStyleRule::MapRuleInfoInto(nsRuleData* aRuleData)
+{
+ nsStyleContext *contextParent = aRuleData->mStyleContext->GetParent();
+ if (contextParent && contextParent->HasPseudoElementData()) {
+ // Don't apply transitions or animations to things inside of
+ // pseudo-elements.
+ // FIXME (Bug 522599): Add tests for this.
+
+ // Prevent structs from being cached on the rule node since we're inside
+ // a pseudo-element, as we could determine cacheability differently
+ // when walking the rule tree for a style context that is not inside
+ // a pseudo-element. Note that nsRuleNode::GetStyle##name_ and GetStyleData
+ // will never look at cached structs when we're animating things inside
+ // a pseduo-element, so that we don't incorrectly return a struct that
+ // is only appropriate for non-pseudo-elements.
+ aRuleData->mConditions.SetUncacheable();
+ return;
+ }
+
+ for (auto iter = mAnimationValues.ConstIter(); !iter.Done(); iter.Next()) {
+ nsCSSPropertyID property = static_cast<nsCSSPropertyID>(iter.Key());
+ if (aRuleData->mSIDs & nsCachedStyleData::GetBitForSID(
+ nsCSSProps::kSIDTable[property])) {
+ nsCSSValue *prop = aRuleData->ValueFor(property);
+ if (prop->GetUnit() == eCSSUnit_Null) {
+ DebugOnly<bool> ok =
+ StyleAnimationValue::UncomputeValue(property, iter.Data(),
+ *prop);
+ MOZ_ASSERT(ok, "could not store computed value");
+ }
+ }
+ }
+}
+
+bool
+AnimValuesStyleRule::MightMapInheritedStyleData()
+{
+ return mStyleBits & NS_STYLE_INHERITED_STRUCT_MASK;
+}
+
+bool
+AnimValuesStyleRule::GetDiscretelyAnimatedCSSValue(nsCSSPropertyID aProperty,
+ nsCSSValue* aValue)
+{
+ MOZ_ASSERT(false, "GetDiscretelyAnimatedCSSValue is not implemented yet");
+ return false;
+}
+
+void
+AnimValuesStyleRule::AddValue(nsCSSPropertyID aProperty,
+ const StyleAnimationValue &aValue)
+{
+ MOZ_ASSERT(aProperty != eCSSProperty_UNKNOWN,
+ "Unexpected css property");
+ mAnimationValues.Put(aProperty, aValue);
+ mStyleBits |=
+ nsCachedStyleData::GetBitForSID(nsCSSProps::kSIDTable[aProperty]);
+}
+
+void
+AnimValuesStyleRule::AddValue(nsCSSPropertyID aProperty,
+ StyleAnimationValue&& aValue)
+{
+ MOZ_ASSERT(aProperty != eCSSProperty_UNKNOWN,
+ "Unexpected css property");
+ mAnimationValues.Put(aProperty, Move(aValue));
+ mStyleBits |=
+ nsCachedStyleData::GetBitForSID(nsCSSProps::kSIDTable[aProperty]);
+}
+
+#ifdef DEBUG
+void
+AnimValuesStyleRule::List(FILE* out, int32_t aIndent) const
+{
+ nsAutoCString str;
+ for (int32_t index = aIndent; --index >= 0; ) {
+ str.AppendLiteral(" ");
+ }
+ str.AppendLiteral("[anim values] { ");
+ for (auto iter = mAnimationValues.ConstIter(); !iter.Done(); iter.Next()) {
+ nsCSSPropertyID property = static_cast<nsCSSPropertyID>(iter.Key());
+ str.Append(nsCSSProps::GetStringValue(property));
+ str.AppendLiteral(": ");
+ nsAutoString value;
+ Unused <<
+ StyleAnimationValue::UncomputeValue(property, iter.Data(), value);
+ AppendUTF16toUTF8(value, str);
+ str.AppendLiteral("; ");
+ }
+ str.AppendLiteral("}\n");
+ fprintf_stderr(out, "%s", str.get());
+}
+#endif
+
+} // namespace mozilla
diff --git a/dom/animation/AnimValuesStyleRule.h b/dom/animation/AnimValuesStyleRule.h
new file mode 100644
index 0000000000..3562014b96
--- /dev/null
+++ b/dom/animation/AnimValuesStyleRule.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_AnimValuesStyleRule_h
+#define mozilla_AnimValuesStyleRule_h
+
+#include "mozilla/StyleAnimationValue.h"
+#include "nsCSSPropertyID.h"
+#include "nsCSSPropertyIDSet.h"
+#include "nsDataHashtable.h"
+#include "nsHashKeys.h" // For nsUint32HashKey
+#include "nsIStyleRule.h"
+#include "nsISupportsImpl.h" // For NS_DECL_ISUPPORTS
+#include "nsRuleNode.h" // For nsCachedStyleData
+#include "nsTArray.h" // For nsTArray
+
+namespace mozilla {
+
+/**
+ * A style rule that maps property-StyleAnimationValue pairs.
+ */
+class AnimValuesStyleRule final : public nsIStyleRule
+{
+public:
+ AnimValuesStyleRule()
+ : mStyleBits(0) {}
+
+ // nsISupports implementation
+ NS_DECL_ISUPPORTS
+
+ // nsIStyleRule implementation
+ void MapRuleInfoInto(nsRuleData* aRuleData) override;
+ bool MightMapInheritedStyleData() override;
+ bool GetDiscretelyAnimatedCSSValue(nsCSSPropertyID aProperty,
+ nsCSSValue* aValue) override;
+#ifdef DEBUG
+ void List(FILE* out = stdout, int32_t aIndent = 0) const override;
+#endif
+
+ // For the following functions, it there is already a value for |aProperty| it
+ // will be replaced with |aValue|.
+ void AddValue(nsCSSPropertyID aProperty, const StyleAnimationValue &aValue);
+ void AddValue(nsCSSPropertyID aProperty, StyleAnimationValue&& aValue);
+
+private:
+ ~AnimValuesStyleRule() {}
+
+ nsDataHashtable<nsUint32HashKey, StyleAnimationValue> mAnimationValues;
+
+ uint32_t mStyleBits;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_AnimValuesStyleRule_h
diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp
new file mode 100644
index 0000000000..6dd583ed1e
--- /dev/null
+++ b/dom/animation/Animation.cpp
@@ -0,0 +1,1426 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/dom/DocumentTimeline.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/AsyncEventDispatcher.h" // For AsyncEventDispatcher
+#include "mozilla/Maybe.h" // For Maybe
+#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
+#include "PendingAnimationTracker.h" // For PendingAnimationTracker
+
+namespace mozilla {
+namespace dom {
+
+// Static members
+uint64_t Animation::sNextAnimationIndex = 0;
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(Animation, DOMEventTargetHelper,
+ mTimeline,
+ mEffect,
+ mReady,
+ mFinished)
+
+NS_IMPL_ADDREF_INHERITED(Animation, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(Animation, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(Animation)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+JSObject*
+Animation::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return dom::AnimationBinding::Wrap(aCx, this, aGivenProto);
+}
+
+// ---------------------------------------------------------------------------
+//
+// Utility methods
+//
+// ---------------------------------------------------------------------------
+
+namespace {
+ // A wrapper around nsAutoAnimationMutationBatch that looks up the
+ // appropriate document from the supplied animation.
+ class MOZ_RAII AutoMutationBatchForAnimation {
+ public:
+ explicit AutoMutationBatchForAnimation(const Animation& aAnimation
+ MOZ_GUARD_OBJECT_NOTIFIER_PARAM) {
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+ Maybe<NonOwningAnimationTarget> target =
+ nsNodeUtils::GetTargetForAnimation(&aAnimation);
+ if (!target) {
+ return;
+ }
+
+ // For mutation observers, we use the OwnerDoc.
+ nsIDocument* doc = target->mElement->OwnerDoc();
+ if (!doc) {
+ return;
+ }
+
+ mAutoBatch.emplace(doc);
+ }
+
+ private:
+ MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+ Maybe<nsAutoAnimationMutationBatch> mAutoBatch;
+ };
+}
+
+// ---------------------------------------------------------------------------
+//
+// Animation interface:
+//
+// ---------------------------------------------------------------------------
+/* static */ already_AddRefed<Animation>
+Animation::Constructor(const GlobalObject& aGlobal,
+ AnimationEffectReadOnly* aEffect,
+ const Optional<AnimationTimeline*>& aTimeline,
+ ErrorResult& aRv)
+{
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ RefPtr<Animation> animation = new Animation(global);
+
+ AnimationTimeline* timeline;
+ if (aTimeline.WasPassed()) {
+ timeline = aTimeline.Value();
+ } else {
+ nsIDocument* document =
+ AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
+ if (!document) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ timeline = document->Timeline();
+ }
+
+ animation->SetTimelineNoUpdate(timeline);
+ animation->SetEffectNoUpdate(aEffect);
+
+ return animation.forget();
+}
+
+void
+Animation::SetId(const nsAString& aId)
+{
+ if (mId == aId) {
+ return;
+ }
+ mId = aId;
+ nsNodeUtils::AnimationChanged(this);
+}
+
+void
+Animation::SetEffect(AnimationEffectReadOnly* aEffect)
+{
+ SetEffectNoUpdate(aEffect);
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#setting-the-target-effect
+void
+Animation::SetEffectNoUpdate(AnimationEffectReadOnly* aEffect)
+{
+ RefPtr<Animation> kungFuDeathGrip(this);
+
+ if (mEffect == aEffect) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+ bool wasRelevant = mIsRelevant;
+
+ if (mEffect) {
+ if (!aEffect) {
+ // If the new effect is null, call ResetPendingTasks before clearing
+ // mEffect since ResetPendingTasks needs it to get the appropriate
+ // PendingAnimationTracker.
+ ResetPendingTasks();
+ }
+
+ // We need to notify observers now because once we set mEffect to null
+ // we won't be able to find the target element to notify.
+ if (mIsRelevant) {
+ nsNodeUtils::AnimationRemoved(this);
+ }
+
+ // Break links with the old effect and then drop it.
+ RefPtr<AnimationEffectReadOnly> oldEffect = mEffect;
+ mEffect = nullptr;
+ oldEffect->SetAnimation(nullptr);
+
+ // The following will not do any notification because mEffect is null.
+ UpdateRelevance();
+ }
+
+ if (aEffect) {
+ // Break links from the new effect to its previous animation, if any.
+ RefPtr<AnimationEffectReadOnly> newEffect = aEffect;
+ Animation* prevAnim = aEffect->GetAnimation();
+ if (prevAnim) {
+ prevAnim->SetEffect(nullptr);
+ }
+
+ // Create links with the new effect. SetAnimation(this) will also update
+ // mIsRelevant of this animation, and then notify mutation observer if
+ // needed by calling Animation::UpdateRelevance(), so we don't need to
+ // call it again.
+ mEffect = newEffect;
+ mEffect->SetAnimation(this);
+
+ // Notify possible add or change.
+ // If the target is different, the change notification will be ignored by
+ // AutoMutationBatchForAnimation.
+ if (wasRelevant && mIsRelevant) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+
+ // Reschedule pending pause or pending play tasks.
+ // If we have a pending animation, it will either be registered
+ // in the pending animation tracker and have a null pending ready time,
+ // or, after it has been painted, it will be removed from the tracker
+ // and assigned a pending ready time.
+ // After updating the effect we'll typically need to repaint so if we've
+ // already been assigned a pending ready time, we should clear it and put
+ // the animation back in the tracker.
+ if (!mPendingReadyTime.IsNull()) {
+ mPendingReadyTime.SetNull();
+
+ nsIDocument* doc = GetRenderedDocument();
+ if (doc) {
+ PendingAnimationTracker* tracker =
+ doc->GetOrCreatePendingAnimationTracker();
+ if (mPendingState == PendingState::PlayPending) {
+ tracker->AddPlayPending(*this);
+ } else {
+ tracker->AddPausePending(*this);
+ }
+ }
+ }
+ }
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+}
+
+void
+Animation::SetTimeline(AnimationTimeline* aTimeline)
+{
+ SetTimelineNoUpdate(aTimeline);
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#setting-the-timeline
+void
+Animation::SetTimelineNoUpdate(AnimationTimeline* aTimeline)
+{
+ if (mTimeline == aTimeline) {
+ return;
+ }
+
+ RefPtr<AnimationTimeline> oldTimeline = mTimeline;
+ if (oldTimeline) {
+ oldTimeline->RemoveAnimation(this);
+ }
+
+ mTimeline = aTimeline;
+ if (!mStartTime.IsNull()) {
+ mHoldTime.SetNull();
+ }
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+}
+
+// https://w3c.github.io/web-animations/#set-the-animation-start-time
+void
+Animation::SetStartTime(const Nullable<TimeDuration>& aNewStartTime)
+{
+ if (aNewStartTime == mStartTime) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ Nullable<TimeDuration> timelineTime;
+ if (mTimeline) {
+ // The spec says to check if the timeline is active (has a resolved time)
+ // before using it here, but we don't need to since it's harmless to set
+ // the already null time to null.
+ timelineTime = mTimeline->GetCurrentTime();
+ }
+ if (timelineTime.IsNull() && !aNewStartTime.IsNull()) {
+ mHoldTime.SetNull();
+ }
+
+ Nullable<TimeDuration> previousCurrentTime = GetCurrentTime();
+ mStartTime = aNewStartTime;
+ if (!aNewStartTime.IsNull()) {
+ if (mPlaybackRate != 0.0) {
+ mHoldTime.SetNull();
+ }
+ } else {
+ mHoldTime = previousCurrentTime;
+ }
+
+ CancelPendingTasks();
+ if (mReady) {
+ // We may have already resolved mReady, but in that case calling
+ // MaybeResolve is a no-op, so that's okay.
+ mReady->MaybeResolve(this);
+ }
+
+ UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async);
+ if (IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#current-time
+Nullable<TimeDuration>
+Animation::GetCurrentTime() const
+{
+ Nullable<TimeDuration> result;
+ if (!mHoldTime.IsNull()) {
+ result = mHoldTime;
+ return result;
+ }
+
+ if (mTimeline && !mStartTime.IsNull()) {
+ Nullable<TimeDuration> timelineTime = mTimeline->GetCurrentTime();
+ if (!timelineTime.IsNull()) {
+ result.SetValue((timelineTime.Value() - mStartTime.Value())
+ .MultDouble(mPlaybackRate));
+ }
+ }
+ return result;
+}
+
+// https://w3c.github.io/web-animations/#set-the-current-time
+void
+Animation::SetCurrentTime(const TimeDuration& aSeekTime)
+{
+ // Return early if the current time has not changed. However, if we
+ // are pause-pending, then setting the current time to any value
+ // including the current value has the effect of aborting the
+ // pause so we should not return early in that case.
+ if (mPendingState != PendingState::PausePending &&
+ Nullable<TimeDuration>(aSeekTime) == GetCurrentTime()) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ SilentlySetCurrentTime(aSeekTime);
+
+ if (mPendingState == PendingState::PausePending) {
+ // Finish the pause operation
+ mHoldTime.SetValue(aSeekTime);
+ mStartTime.SetNull();
+
+ if (mReady) {
+ mReady->MaybeResolve(this);
+ }
+ CancelPendingTasks();
+ }
+
+ UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async);
+ if (IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#set-the-animation-playback-rate
+void
+Animation::SetPlaybackRate(double aPlaybackRate)
+{
+ if (aPlaybackRate == mPlaybackRate) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ Nullable<TimeDuration> previousTime = GetCurrentTime();
+ mPlaybackRate = aPlaybackRate;
+ if (!previousTime.IsNull()) {
+ SetCurrentTime(previousTime.Value());
+ }
+
+ // In the case where GetCurrentTime() returns the same result before and
+ // after updating mPlaybackRate, SetCurrentTime will return early since,
+ // as far as it can tell, nothing has changed.
+ // As a result, we need to perform the following updates here:
+ // - update timing (since, if the sign of the playback rate has changed, our
+ // finished state may have changed),
+ // - dispatch a change notification for the changed playback rate, and
+ // - update the playback rate on animations on layers.
+ UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async);
+ if (IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#play-state
+AnimationPlayState
+Animation::PlayState() const
+{
+ if (mPendingState != PendingState::NotPending) {
+ return AnimationPlayState::Pending;
+ }
+
+ Nullable<TimeDuration> currentTime = GetCurrentTime();
+ if (currentTime.IsNull()) {
+ return AnimationPlayState::Idle;
+ }
+
+ if (mStartTime.IsNull()) {
+ return AnimationPlayState::Paused;
+ }
+
+ if ((mPlaybackRate > 0.0 && currentTime.Value() >= EffectEnd()) ||
+ (mPlaybackRate < 0.0 && currentTime.Value() <= TimeDuration())) {
+ return AnimationPlayState::Finished;
+ }
+
+ return AnimationPlayState::Running;
+}
+
+Promise*
+Animation::GetReady(ErrorResult& aRv)
+{
+ nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal();
+ if (!mReady && global) {
+ mReady = Promise::Create(global, aRv); // Lazily create on demand
+ }
+ if (!mReady) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ } else if (PlayState() != AnimationPlayState::Pending) {
+ mReady->MaybeResolve(this);
+ }
+ return mReady;
+}
+
+Promise*
+Animation::GetFinished(ErrorResult& aRv)
+{
+ nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal();
+ if (!mFinished && global) {
+ mFinished = Promise::Create(global, aRv); // Lazily create on demand
+ }
+ if (!mFinished) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ } else if (mFinishedIsResolved) {
+ MaybeResolveFinishedPromise();
+ }
+ return mFinished;
+}
+
+void
+Animation::Cancel()
+{
+ CancelNoUpdate();
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#finish-an-animation
+void
+Animation::Finish(ErrorResult& aRv)
+{
+ if (mPlaybackRate == 0 ||
+ (mPlaybackRate > 0 && EffectEnd() == TimeDuration::Forever())) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ // Seek to the end
+ TimeDuration limit =
+ mPlaybackRate > 0 ? TimeDuration(EffectEnd()) : TimeDuration(0);
+ bool didChange = GetCurrentTime() != Nullable<TimeDuration>(limit);
+ SilentlySetCurrentTime(limit);
+
+ // If we are paused or play-pending we need to fill in the start time in
+ // order to transition to the finished state.
+ //
+ // We only do this, however, if we have an active timeline. If we have an
+ // inactive timeline we can't transition into the finished state just like
+ // we can't transition to the running state (this finished state is really
+ // a substate of the running state).
+ if (mStartTime.IsNull() &&
+ mTimeline &&
+ !mTimeline->GetCurrentTime().IsNull()) {
+ mStartTime.SetValue(mTimeline->GetCurrentTime().Value() -
+ limit.MultDouble(1.0 / mPlaybackRate));
+ didChange = true;
+ }
+
+ // If we just resolved the start time for a pause or play-pending
+ // animation, we need to clear the task. We don't do this as a branch of
+ // the above however since we can have a play-pending animation with a
+ // resolved start time if we aborted a pause operation.
+ if (!mStartTime.IsNull() &&
+ (mPendingState == PendingState::PlayPending ||
+ mPendingState == PendingState::PausePending)) {
+ if (mPendingState == PendingState::PausePending) {
+ mHoldTime.SetNull();
+ }
+ CancelPendingTasks();
+ didChange = true;
+ if (mReady) {
+ mReady->MaybeResolve(this);
+ }
+ }
+ UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Sync);
+ if (didChange && IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+ PostUpdate();
+}
+
+void
+Animation::Play(ErrorResult& aRv, LimitBehavior aLimitBehavior)
+{
+ PlayNoUpdate(aRv, aLimitBehavior);
+ PostUpdate();
+}
+
+void
+Animation::Pause(ErrorResult& aRv)
+{
+ PauseNoUpdate(aRv);
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#reverse-an-animation
+void
+Animation::Reverse(ErrorResult& aRv)
+{
+ if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ if (mPlaybackRate == 0.0) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ SilentlySetPlaybackRate(-mPlaybackRate);
+ Play(aRv, LimitBehavior::AutoRewind);
+
+ if (IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+}
+
+// ---------------------------------------------------------------------------
+//
+// JS wrappers for Animation interface:
+//
+// ---------------------------------------------------------------------------
+
+Nullable<double>
+Animation::GetStartTimeAsDouble() const
+{
+ return AnimationUtils::TimeDurationToDouble(mStartTime);
+}
+
+void
+Animation::SetStartTimeAsDouble(const Nullable<double>& aStartTime)
+{
+ return SetStartTime(AnimationUtils::DoubleToTimeDuration(aStartTime));
+}
+
+Nullable<double>
+Animation::GetCurrentTimeAsDouble() const
+{
+ return AnimationUtils::TimeDurationToDouble(GetCurrentTime());
+}
+
+void
+Animation::SetCurrentTimeAsDouble(const Nullable<double>& aCurrentTime,
+ ErrorResult& aRv)
+{
+ if (aCurrentTime.IsNull()) {
+ if (!GetCurrentTime().IsNull()) {
+ aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
+ }
+ return;
+ }
+
+ return SetCurrentTime(TimeDuration::FromMilliseconds(aCurrentTime.Value()));
+}
+
+// ---------------------------------------------------------------------------
+
+void
+Animation::Tick()
+{
+ // Finish pending if we have a pending ready time, but only if we also
+ // have an active timeline.
+ if (mPendingState != PendingState::NotPending &&
+ !mPendingReadyTime.IsNull() &&
+ mTimeline &&
+ !mTimeline->GetCurrentTime().IsNull()) {
+ // Even though mPendingReadyTime is initialized using TimeStamp::Now()
+ // during the *previous* tick of the refresh driver, it can still be
+ // ahead of the *current* timeline time when we are using the
+ // vsync timer so we need to clamp it to the timeline time.
+ mPendingReadyTime.SetValue(std::min(mTimeline->GetCurrentTime().Value(),
+ mPendingReadyTime.Value()));
+ FinishPendingAt(mPendingReadyTime.Value());
+ mPendingReadyTime.SetNull();
+ }
+
+ if (IsPossiblyOrphanedPendingAnimation()) {
+ MOZ_ASSERT(mTimeline && !mTimeline->GetCurrentTime().IsNull(),
+ "Orphaned pending animations should have an active timeline");
+ FinishPendingAt(mTimeline->GetCurrentTime().Value());
+ }
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+
+ if (!mEffect) {
+ return;
+ }
+
+ // Update layers if we are newly finished.
+ KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect();
+ if (keyframeEffect &&
+ !keyframeEffect->Properties().IsEmpty() &&
+ !mFinishedAtLastComposeStyle &&
+ PlayState() == AnimationPlayState::Finished) {
+ PostUpdate();
+ }
+}
+
+void
+Animation::TriggerOnNextTick(const Nullable<TimeDuration>& aReadyTime)
+{
+ // Normally we expect the play state to be pending but it's possible that,
+ // due to the handling of possibly orphaned animations in Tick(), this
+ // animation got started whilst still being in another document's pending
+ // animation map.
+ if (PlayState() != AnimationPlayState::Pending) {
+ return;
+ }
+
+ // If aReadyTime.IsNull() we'll detect this in Tick() where we check for
+ // orphaned animations and trigger this animation anyway
+ mPendingReadyTime = aReadyTime;
+}
+
+void
+Animation::TriggerNow()
+{
+ // Normally we expect the play state to be pending but when an animation
+ // is cancelled and its rendered document can't be reached, we can end up
+ // with the animation still in a pending player tracker even after it is
+ // no longer pending.
+ if (PlayState() != AnimationPlayState::Pending) {
+ return;
+ }
+
+ // If we don't have an active timeline we can't trigger the animation.
+ // However, this is a test-only method that we don't expect to be used in
+ // conjunction with animations without an active timeline so generate
+ // a warning if we do find ourselves in that situation.
+ if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) {
+ NS_WARNING("Failed to trigger an animation with an active timeline");
+ return;
+ }
+
+ FinishPendingAt(mTimeline->GetCurrentTime().Value());
+}
+
+Nullable<TimeDuration>
+Animation::GetCurrentOrPendingStartTime() const
+{
+ Nullable<TimeDuration> result;
+
+ if (!mStartTime.IsNull()) {
+ result = mStartTime;
+ return result;
+ }
+
+ if (mPendingReadyTime.IsNull() || mHoldTime.IsNull()) {
+ return result;
+ }
+
+ // Calculate the equivalent start time from the pending ready time.
+ result = StartTimeFromReadyTime(mPendingReadyTime.Value());
+
+ return result;
+}
+
+TimeDuration
+Animation::StartTimeFromReadyTime(const TimeDuration& aReadyTime) const
+{
+ MOZ_ASSERT(!mHoldTime.IsNull(), "Hold time should be set in order to"
+ " convert a ready time to a start time");
+ if (mPlaybackRate == 0) {
+ return aReadyTime;
+ }
+ return aReadyTime - mHoldTime.Value().MultDouble(1 / mPlaybackRate);
+}
+
+TimeStamp
+Animation::AnimationTimeToTimeStamp(const StickyTimeDuration& aTime) const
+{
+ // Initializes to null. Return the same object every time to benefit from
+ // return-value-optimization.
+ TimeStamp result;
+
+ // We *don't* check for mTimeline->TracksWallclockTime() here because that
+ // method only tells us if the timeline times can be converted to
+ // TimeStamps that can be compared to TimeStamp::Now() or not, *not*
+ // whether the timelines can be converted to TimeStamp values at all.
+ //
+ // Furthermore, we want to be able to use this method when the refresh driver
+ // is under test control (in which case TracksWallclockTime() will return
+ // false).
+ //
+ // Once we introduce timelines that are not time-based we will need to
+ // differentiate between them here and determine how to sort their events.
+ if (!mTimeline) {
+ return result;
+ }
+
+ // Check the time is convertible to a timestamp
+ if (aTime == TimeDuration::Forever() ||
+ mPlaybackRate == 0.0 ||
+ mStartTime.IsNull()) {
+ return result;
+ }
+
+ // Invert the standard relation:
+ // animation time = (timeline time - start time) * playback rate
+ TimeDuration timelineTime =
+ TimeDuration(aTime).MultDouble(1.0 / mPlaybackRate) + mStartTime.Value();
+
+ result = mTimeline->ToTimeStamp(timelineTime);
+ return result;
+}
+
+TimeStamp
+Animation::ElapsedTimeToTimeStamp(
+ const StickyTimeDuration& aElapsedTime) const
+{
+ return AnimationTimeToTimeStamp(aElapsedTime +
+ mEffect->SpecifiedTiming().mDelay);
+}
+
+
+// https://w3c.github.io/web-animations/#silently-set-the-current-time
+void
+Animation::SilentlySetCurrentTime(const TimeDuration& aSeekTime)
+{
+ if (!mHoldTime.IsNull() ||
+ mStartTime.IsNull() ||
+ !mTimeline ||
+ mTimeline->GetCurrentTime().IsNull() ||
+ mPlaybackRate == 0.0) {
+ mHoldTime.SetValue(aSeekTime);
+ if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) {
+ mStartTime.SetNull();
+ }
+ } else {
+ mStartTime.SetValue(mTimeline->GetCurrentTime().Value() -
+ (aSeekTime.MultDouble(1 / mPlaybackRate)));
+ }
+
+ mPreviousCurrentTime.SetNull();
+}
+
+void
+Animation::SilentlySetPlaybackRate(double aPlaybackRate)
+{
+ Nullable<TimeDuration> previousTime = GetCurrentTime();
+ mPlaybackRate = aPlaybackRate;
+ if (!previousTime.IsNull()) {
+ SilentlySetCurrentTime(previousTime.Value());
+ }
+}
+
+// https://w3c.github.io/web-animations/#cancel-an-animation
+void
+Animation::CancelNoUpdate()
+{
+ ResetPendingTasks();
+
+ if (mFinished) {
+ mFinished->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ }
+ ResetFinishedPromise();
+
+ DispatchPlaybackEvent(NS_LITERAL_STRING("cancel"));
+
+ mHoldTime.SetNull();
+ mStartTime.SetNull();
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+
+ if (mTimeline) {
+ mTimeline->RemoveAnimation(this);
+ }
+}
+
+void
+Animation::UpdateRelevance()
+{
+ bool wasRelevant = mIsRelevant;
+ mIsRelevant = HasCurrentEffect() || IsInEffect();
+
+ // Notify animation observers.
+ if (wasRelevant && !mIsRelevant) {
+ nsNodeUtils::AnimationRemoved(this);
+ } else if (!wasRelevant && mIsRelevant) {
+ nsNodeUtils::AnimationAdded(this);
+ }
+}
+
+bool
+Animation::HasLowerCompositeOrderThan(const Animation& aOther) const
+{
+ // 0. Object-equality case
+ if (&aOther == this) {
+ return false;
+ }
+
+ // 1. CSS Transitions sort lowest
+ {
+ auto asCSSTransitionForSorting =
+ [] (const Animation& anim) -> const CSSTransition*
+ {
+ const CSSTransition* transition = anim.AsCSSTransition();
+ return transition && transition->IsTiedToMarkup() ?
+ transition :
+ nullptr;
+ };
+ auto thisTransition = asCSSTransitionForSorting(*this);
+ auto otherTransition = asCSSTransitionForSorting(aOther);
+ if (thisTransition && otherTransition) {
+ return thisTransition->HasLowerCompositeOrderThan(*otherTransition);
+ }
+ if (thisTransition || otherTransition) {
+ return thisTransition;
+ }
+ }
+
+ // 2. CSS Animations sort next
+ {
+ auto asCSSAnimationForSorting =
+ [] (const Animation& anim) -> const CSSAnimation*
+ {
+ const CSSAnimation* animation = anim.AsCSSAnimation();
+ return animation && animation->IsTiedToMarkup() ? animation : nullptr;
+ };
+ auto thisAnimation = asCSSAnimationForSorting(*this);
+ auto otherAnimation = asCSSAnimationForSorting(aOther);
+ if (thisAnimation && otherAnimation) {
+ return thisAnimation->HasLowerCompositeOrderThan(*otherAnimation);
+ }
+ if (thisAnimation || otherAnimation) {
+ return thisAnimation;
+ }
+ }
+
+ // Subclasses of Animation repurpose mAnimationIndex to implement their
+ // own brand of composite ordering. However, by this point we should have
+ // handled any such custom composite ordering so we should now have unique
+ // animation indices.
+ MOZ_ASSERT(mAnimationIndex != aOther.mAnimationIndex,
+ "Animation indices should be unique");
+
+ // 3. Finally, generic animations sort by their position in the global
+ // animation array.
+ return mAnimationIndex < aOther.mAnimationIndex;
+}
+
+void
+Animation::ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
+ const nsCSSPropertyIDSet& aPropertiesToSkip)
+{
+ if (!mEffect) {
+ return;
+ }
+
+ if (!IsInEffect()) {
+ return;
+ }
+
+ // In order to prevent flicker, there are a few cases where we want to use
+ // a different time for rendering that would otherwise be returned by
+ // GetCurrentTime. These are:
+ //
+ // (a) For animations that are pausing but which are still running on the
+ // compositor. In this case we send a layer transaction that removes the
+ // animation but which also contains the animation values calculated on
+ // the main thread. To prevent flicker when this occurs we want to ensure
+ // the timeline time used to calculate the main thread animation values
+ // does not lag far behind the time used on the compositor. Ideally we
+ // would like to use the "animation ready time" calculated at the end of
+ // the layer transaction as the timeline time but it will be too late to
+ // update the style rule at that point so instead we just use the current
+ // wallclock time.
+ //
+ // (b) For animations that are pausing that we have already taken off the
+ // compositor. In this case we record a pending ready time but we don't
+ // apply it until the next tick. However, while waiting for the next tick,
+ // we should still use the pending ready time as the timeline time. If we
+ // use the regular timeline time the animation may appear jump backwards
+ // if the main thread's timeline time lags behind the compositor.
+ //
+ // (c) For animations that are play-pending due to an aborted pause operation
+ // (i.e. a pause operation that was interrupted before we entered the
+ // paused state). When we cancel a pending pause we might momentarily take
+ // the animation off the compositor, only to re-add it moments later. In
+ // that case the compositor might have been ahead of the main thread so we
+ // should use the current wallclock time to ensure the animation doesn't
+ // temporarily jump backwards.
+ //
+ // To address each of these cases we temporarily tweak the hold time
+ // immediately before updating the style rule and then restore it immediately
+ // afterwards. This is purely to prevent visual flicker. Other behavior
+ // such as dispatching events continues to rely on the regular timeline time.
+ AnimationPlayState playState = PlayState();
+ {
+ AutoRestore<Nullable<TimeDuration>> restoreHoldTime(mHoldTime);
+
+ if (playState == AnimationPlayState::Pending &&
+ mHoldTime.IsNull() &&
+ !mStartTime.IsNull()) {
+ Nullable<TimeDuration> timeToUse = mPendingReadyTime;
+ if (timeToUse.IsNull() &&
+ mTimeline &&
+ mTimeline->TracksWallclockTime()) {
+ timeToUse = mTimeline->ToTimelineTime(TimeStamp::Now());
+ }
+ if (!timeToUse.IsNull()) {
+ mHoldTime.SetValue((timeToUse.Value() - mStartTime.Value())
+ .MultDouble(mPlaybackRate));
+ }
+ }
+
+ KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect();
+ if (keyframeEffect) {
+ keyframeEffect->ComposeStyle(aStyleRule, aPropertiesToSkip);
+ }
+ }
+
+ MOZ_ASSERT(playState == PlayState(),
+ "Play state should not change during the course of compositing");
+ mFinishedAtLastComposeStyle = (playState == AnimationPlayState::Finished);
+}
+
+void
+Animation::NotifyEffectTimingUpdated()
+{
+ MOZ_ASSERT(mEffect,
+ "We should only update timing effect when we have a target "
+ "effect");
+ UpdateTiming(Animation::SeekFlag::NoSeek,
+ Animation::SyncNotifyFlag::Async);
+}
+
+// https://w3c.github.io/web-animations/#play-an-animation
+void
+Animation::PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior)
+{
+ AutoMutationBatchForAnimation mb(*this);
+
+ bool abortedPause = mPendingState == PendingState::PausePending;
+
+ Nullable<TimeDuration> currentTime = GetCurrentTime();
+ if (mPlaybackRate > 0.0 &&
+ (currentTime.IsNull() ||
+ (aLimitBehavior == LimitBehavior::AutoRewind &&
+ (currentTime.Value() < TimeDuration() ||
+ currentTime.Value() >= EffectEnd())))) {
+ mHoldTime.SetValue(TimeDuration(0));
+ } else if (mPlaybackRate < 0.0 &&
+ (currentTime.IsNull() ||
+ (aLimitBehavior == LimitBehavior::AutoRewind &&
+ (currentTime.Value() <= TimeDuration() ||
+ currentTime.Value() > EffectEnd())))) {
+ if (EffectEnd() == TimeDuration::Forever()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ mHoldTime.SetValue(TimeDuration(EffectEnd()));
+ } else if (mPlaybackRate == 0.0 && currentTime.IsNull()) {
+ mHoldTime.SetValue(TimeDuration(0));
+ }
+
+ bool reuseReadyPromise = false;
+ if (mPendingState != PendingState::NotPending) {
+ CancelPendingTasks();
+ reuseReadyPromise = true;
+ }
+
+ // If the hold time is null then we're either already playing normally (and
+ // we can ignore this call) or we aborted a pending pause operation (in which
+ // case, for consistency, we need to go through the motions of doing an
+ // asynchronous start even though we already have a resolved start time).
+ if (mHoldTime.IsNull() && !abortedPause) {
+ return;
+ }
+
+ // Clear the start time until we resolve a new one. We do this except
+ // for the case where we are aborting a pause and don't have a hold time.
+ //
+ // If we're aborting a pause and *do* have a hold time (e.g. because
+ // the animation is finished or we just applied the auto-rewind behavior
+ // above) we should respect it by clearing the start time. If we *don't*
+ // have a hold time we should keep the current start time so that the
+ // the animation continues moving uninterrupted by the aborted pause.
+ //
+ // (If we're not aborting a pause, mHoldTime must be resolved by now
+ // or else we would have returned above.)
+ if (!mHoldTime.IsNull()) {
+ mStartTime.SetNull();
+ }
+
+ if (!reuseReadyPromise) {
+ // Clear ready promise. We'll create a new one lazily.
+ mReady = nullptr;
+ }
+
+ mPendingState = PendingState::PlayPending;
+
+ nsIDocument* doc = GetRenderedDocument();
+ if (doc) {
+ PendingAnimationTracker* tracker =
+ doc->GetOrCreatePendingAnimationTracker();
+ tracker->AddPlayPending(*this);
+ } else {
+ TriggerOnNextTick(Nullable<TimeDuration>());
+ }
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+ if (IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+}
+
+// https://w3c.github.io/web-animations/#pause-an-animation
+void
+Animation::PauseNoUpdate(ErrorResult& aRv)
+{
+ if (IsPausedOrPausing()) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ // If we are transitioning from idle, fill in the current time
+ if (GetCurrentTime().IsNull()) {
+ if (mPlaybackRate >= 0.0) {
+ mHoldTime.SetValue(TimeDuration(0));
+ } else {
+ if (EffectEnd() == TimeDuration::Forever()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ mHoldTime.SetValue(TimeDuration(EffectEnd()));
+ }
+ }
+
+ bool reuseReadyPromise = false;
+ if (mPendingState == PendingState::PlayPending) {
+ CancelPendingTasks();
+ reuseReadyPromise = true;
+ }
+
+ if (!reuseReadyPromise) {
+ // Clear ready promise. We'll create a new one lazily.
+ mReady = nullptr;
+ }
+
+ mPendingState = PendingState::PausePending;
+
+ nsIDocument* doc = GetRenderedDocument();
+ if (doc) {
+ PendingAnimationTracker* tracker =
+ doc->GetOrCreatePendingAnimationTracker();
+ tracker->AddPausePending(*this);
+ } else {
+ TriggerOnNextTick(Nullable<TimeDuration>());
+ }
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+ if (IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+}
+
+void
+Animation::ResumeAt(const TimeDuration& aReadyTime)
+{
+ // This method is only expected to be called for an animation that is
+ // waiting to play. We can easily adapt it to handle other states
+ // but it's currently not necessary.
+ MOZ_ASSERT(mPendingState == PendingState::PlayPending,
+ "Expected to resume a play-pending animation");
+ MOZ_ASSERT(mHoldTime.IsNull() != mStartTime.IsNull(),
+ "An animation in the play-pending state should have either a"
+ " resolved hold time or resolved start time (but not both)");
+
+ // If we aborted a pending pause operation we will already have a start time
+ // we should use. In all other cases, we resolve it from the ready time.
+ if (mStartTime.IsNull()) {
+ mStartTime = StartTimeFromReadyTime(aReadyTime);
+ if (mPlaybackRate != 0) {
+ mHoldTime.SetNull();
+ }
+ }
+ mPendingState = PendingState::NotPending;
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+
+ if (mReady) {
+ mReady->MaybeResolve(this);
+ }
+}
+
+void
+Animation::PauseAt(const TimeDuration& aReadyTime)
+{
+ MOZ_ASSERT(mPendingState == PendingState::PausePending,
+ "Expected to pause a pause-pending animation");
+
+ if (!mStartTime.IsNull() && mHoldTime.IsNull()) {
+ mHoldTime.SetValue((aReadyTime - mStartTime.Value())
+ .MultDouble(mPlaybackRate));
+ }
+ mStartTime.SetNull();
+ mPendingState = PendingState::NotPending;
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+
+ if (mReady) {
+ mReady->MaybeResolve(this);
+ }
+}
+
+void
+Animation::UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag)
+{
+ // We call UpdateFinishedState before UpdateEffect because the former
+ // can change the current time, which is used by the latter.
+ UpdateFinishedState(aSeekFlag, aSyncNotifyFlag);
+ UpdateEffect();
+
+ if (mTimeline) {
+ mTimeline->NotifyAnimationUpdated(*this);
+ }
+}
+
+// https://w3c.github.io/web-animations/#update-an-animations-finished-state
+void
+Animation::UpdateFinishedState(SeekFlag aSeekFlag,
+ SyncNotifyFlag aSyncNotifyFlag)
+{
+ Nullable<TimeDuration> currentTime = GetCurrentTime();
+ TimeDuration effectEnd = TimeDuration(EffectEnd());
+
+ if (!mStartTime.IsNull() &&
+ mPendingState == PendingState::NotPending) {
+ if (mPlaybackRate > 0.0 &&
+ !currentTime.IsNull() &&
+ currentTime.Value() >= effectEnd) {
+ if (aSeekFlag == SeekFlag::DidSeek) {
+ mHoldTime = currentTime;
+ } else if (!mPreviousCurrentTime.IsNull()) {
+ mHoldTime.SetValue(std::max(mPreviousCurrentTime.Value(), effectEnd));
+ } else {
+ mHoldTime.SetValue(effectEnd);
+ }
+ } else if (mPlaybackRate < 0.0 &&
+ !currentTime.IsNull() &&
+ currentTime.Value() <= TimeDuration()) {
+ if (aSeekFlag == SeekFlag::DidSeek) {
+ mHoldTime = currentTime;
+ } else if (!mPreviousCurrentTime.IsNull()) {
+ mHoldTime.SetValue(std::min(mPreviousCurrentTime.Value(),
+ TimeDuration(0)));
+ } else {
+ mHoldTime.SetValue(0);
+ }
+ } else if (mPlaybackRate != 0.0 &&
+ !currentTime.IsNull() &&
+ mTimeline &&
+ !mTimeline->GetCurrentTime().IsNull()) {
+ if (aSeekFlag == SeekFlag::DidSeek && !mHoldTime.IsNull()) {
+ mStartTime.SetValue(mTimeline->GetCurrentTime().Value() -
+ (mHoldTime.Value().MultDouble(1 / mPlaybackRate)));
+ }
+ mHoldTime.SetNull();
+ }
+ }
+
+ bool currentFinishedState = PlayState() == AnimationPlayState::Finished;
+ if (currentFinishedState && !mFinishedIsResolved) {
+ DoFinishNotification(aSyncNotifyFlag);
+ } else if (!currentFinishedState && mFinishedIsResolved) {
+ ResetFinishedPromise();
+ }
+ // We must recalculate the current time to take account of any mHoldTime
+ // changes the code above made.
+ mPreviousCurrentTime = GetCurrentTime();
+}
+
+void
+Animation::UpdateEffect()
+{
+ if (mEffect) {
+ UpdateRelevance();
+
+ KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect();
+ if (keyframeEffect) {
+ keyframeEffect->NotifyAnimationTimingUpdated();
+ }
+ }
+}
+
+void
+Animation::FlushStyle() const
+{
+ nsIDocument* doc = GetRenderedDocument();
+ if (doc) {
+ doc->FlushPendingNotifications(Flush_Style);
+ }
+}
+
+void
+Animation::PostUpdate()
+{
+ if (!mEffect) {
+ return;
+ }
+
+ KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect();
+ if (!keyframeEffect) {
+ return;
+ }
+
+ Maybe<NonOwningAnimationTarget> target = keyframeEffect->GetTarget();
+ if (!target) {
+ return;
+ }
+
+ nsPresContext* presContext = keyframeEffect->GetPresContext();
+ if (!presContext) {
+ return;
+ }
+
+ presContext->EffectCompositor()
+ ->RequestRestyle(target->mElement,
+ target->mPseudoType,
+ EffectCompositor::RestyleType::Layer,
+ CascadeLevel());
+}
+
+void
+Animation::CancelPendingTasks()
+{
+ if (mPendingState == PendingState::NotPending) {
+ return;
+ }
+
+ nsIDocument* doc = GetRenderedDocument();
+ if (doc) {
+ PendingAnimationTracker* tracker = doc->GetPendingAnimationTracker();
+ if (tracker) {
+ if (mPendingState == PendingState::PlayPending) {
+ tracker->RemovePlayPending(*this);
+ } else {
+ tracker->RemovePausePending(*this);
+ }
+ }
+ }
+
+ mPendingState = PendingState::NotPending;
+ mPendingReadyTime.SetNull();
+}
+
+// https://w3c.github.io/web-animations/#reset-an-animations-pending-tasks
+void
+Animation::ResetPendingTasks()
+{
+ if (mPendingState == PendingState::NotPending) {
+ return;
+ }
+
+ CancelPendingTasks();
+ if (mReady) {
+ mReady->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ }
+}
+
+bool
+Animation::IsPossiblyOrphanedPendingAnimation() const
+{
+ // Check if we are pending but might never start because we are not being
+ // tracked.
+ //
+ // This covers the following cases:
+ //
+ // * We started playing but our effect's target element was orphaned
+ // or bound to a different document.
+ // (note that for the case of our effect changing we should handle
+ // that in SetEffect)
+ // * We started playing but our timeline became inactive.
+ // In this case the pending animation tracker will drop us from its hashmap
+ // when we have been painted.
+ // * When we started playing we couldn't find a PendingAnimationTracker to
+ // register with (perhaps the effect had no document) so we simply
+ // set mPendingState in PlayNoUpdate and relied on this method to catch us
+ // on the next tick.
+
+ // If we're not pending we're ok.
+ if (mPendingState == PendingState::NotPending) {
+ return false;
+ }
+
+ // If we have a pending ready time then we will be started on the next
+ // tick.
+ if (!mPendingReadyTime.IsNull()) {
+ return false;
+ }
+
+ // If we don't have an active timeline then we shouldn't start until
+ // we do.
+ if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) {
+ return false;
+ }
+
+ // If we have no rendered document, or we're not in our rendered document's
+ // PendingAnimationTracker then there's a good chance no one is tracking us.
+ //
+ // If we're wrong and another document is tracking us then, at worst, we'll
+ // simply start/pause the animation one tick too soon. That's better than
+ // never starting/pausing the animation and is unlikely.
+ nsIDocument* doc = GetRenderedDocument();
+ if (!doc) {
+ return true;
+ }
+
+ PendingAnimationTracker* tracker = doc->GetPendingAnimationTracker();
+ return !tracker ||
+ (!tracker->IsWaitingToPlay(*this) &&
+ !tracker->IsWaitingToPause(*this));
+}
+
+StickyTimeDuration
+Animation::EffectEnd() const
+{
+ if (!mEffect) {
+ return StickyTimeDuration(0);
+ }
+
+ return mEffect->SpecifiedTiming().EndTime();
+}
+
+nsIDocument*
+Animation::GetRenderedDocument() const
+{
+ if (!mEffect || !mEffect->AsKeyframeEffect()) {
+ return nullptr;
+ }
+
+ return mEffect->AsKeyframeEffect()->GetRenderedDocument();
+}
+
+void
+Animation::DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag)
+{
+ CycleCollectedJSContext* context = CycleCollectedJSContext::Get();
+
+ if (aSyncNotifyFlag == SyncNotifyFlag::Sync) {
+ DoFinishNotificationImmediately();
+ } else if (!mFinishNotificationTask.IsPending()) {
+ RefPtr<nsRunnableMethod<Animation>> runnable =
+ NewRunnableMethod(this, &Animation::DoFinishNotificationImmediately);
+ context->DispatchToMicroTask(do_AddRef(runnable));
+ mFinishNotificationTask = runnable.forget();
+ }
+}
+
+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;
+ }
+
+ MaybeResolveFinishedPromise();
+
+ DispatchPlaybackEvent(NS_LITERAL_STRING("finish"));
+}
+
+void
+Animation::DispatchPlaybackEvent(const nsAString& aName)
+{
+ 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();
+}
+
+bool
+Animation::IsRunningOnCompositor() const
+{
+ return mEffect &&
+ mEffect->AsKeyframeEffect() &&
+ mEffect->AsKeyframeEffect()->IsRunningOnCompositor();
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/animation/Animation.h b/dom/animation/Animation.h
new file mode 100644
index 0000000000..c59d7d6ce0
--- /dev/null
+++ b/dom/animation/Animation.h
@@ -0,0 +1,453 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_dom_Animation_h
+#define mozilla_dom_Animation_h
+
+#include "nsWrapperCache.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/EffectCompositor.h" // For EffectCompositor::CascadeLevel
+#include "mozilla/LinkedList.h"
+#include "mozilla/TimeStamp.h" // for TimeStamp, TimeDuration
+#include "mozilla/dom/AnimationBinding.h" // for AnimationPlayState
+#include "mozilla/dom/AnimationEffectReadOnly.h"
+#include "mozilla/dom/AnimationTimeline.h"
+#include "mozilla/dom/Promise.h"
+#include "nsCSSPropertyID.h"
+#include "nsIGlobalObject.h"
+
+// X11 has a #define for CurrentTime.
+#ifdef CurrentTime
+#undef CurrentTime
+#endif
+
+// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to
+// GetTickCount().
+#ifdef GetCurrentTime
+#undef GetCurrentTime
+#endif
+
+struct JSContext;
+class nsCSSPropertyIDSet;
+class nsIDocument;
+class nsPresContext;
+
+namespace mozilla {
+
+class AnimValuesStyleRule;
+
+namespace dom {
+
+class CSSAnimation;
+class CSSTransition;
+
+class Animation
+ : public DOMEventTargetHelper
+ , public LinkedListElement<Animation>
+{
+protected:
+ virtual ~Animation() {}
+
+public:
+ explicit Animation(nsIGlobalObject* aGlobal)
+ : DOMEventTargetHelper(aGlobal)
+ , mPlaybackRate(1.0)
+ , mPendingState(PendingState::NotPending)
+ , mAnimationIndex(sNextAnimationIndex++)
+ , mFinishedAtLastComposeStyle(false)
+ , mIsRelevant(false)
+ , mFinishedIsResolved(false)
+ {
+ }
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Animation,
+ DOMEventTargetHelper)
+
+ nsIGlobalObject* GetParentObject() const { return GetOwnerGlobal(); }
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual CSSAnimation* AsCSSAnimation() { return nullptr; }
+ virtual const CSSAnimation* AsCSSAnimation() const { return nullptr; }
+ virtual CSSTransition* AsCSSTransition() { return nullptr; }
+ virtual const CSSTransition* AsCSSTransition() const { return nullptr; }
+
+ /**
+ * Flag to pass to Play to indicate whether or not it should automatically
+ * rewind the current time to the start point if the animation is finished.
+ * For regular calls to play() from script we should do this, but when a CSS
+ * animation's animation-play-state changes we shouldn't rewind the animation.
+ */
+ enum class LimitBehavior {
+ AutoRewind,
+ Continue
+ };
+
+ // Animation interface methods
+ static already_AddRefed<Animation>
+ Constructor(const GlobalObject& aGlobal,
+ AnimationEffectReadOnly* aEffect,
+ const Optional<AnimationTimeline*>& aTimeline,
+ ErrorResult& aRv);
+ void GetId(nsAString& aResult) const { aResult = mId; }
+ void SetId(const nsAString& aId);
+ AnimationEffectReadOnly* GetEffect() const { return mEffect; }
+ void SetEffect(AnimationEffectReadOnly* aEffect);
+ AnimationTimeline* GetTimeline() const { return mTimeline; }
+ void SetTimeline(AnimationTimeline* aTimeline);
+ Nullable<TimeDuration> GetStartTime() const { return mStartTime; }
+ void SetStartTime(const Nullable<TimeDuration>& aNewStartTime);
+ Nullable<TimeDuration> GetCurrentTime() const;
+ void SetCurrentTime(const TimeDuration& aNewCurrentTime);
+ double PlaybackRate() const { return mPlaybackRate; }
+ void SetPlaybackRate(double aPlaybackRate);
+ AnimationPlayState PlayState() const;
+ 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;
+ IMPL_EVENT_HANDLER(finish);
+ IMPL_EVENT_HANDLER(cancel);
+
+ // 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;
+ void SetStartTimeAsDouble(const Nullable<double>& aStartTime);
+ Nullable<double> GetCurrentTimeAsDouble() const;
+ void SetCurrentTimeAsDouble(const Nullable<double>& aCurrentTime,
+ ErrorResult& aRv);
+ virtual AnimationPlayState PlayStateFromJS() const { return PlayState(); }
+ virtual void PlayFromJS(ErrorResult& aRv)
+ {
+ Play(aRv, LimitBehavior::AutoRewind);
+ }
+ /**
+ * PauseFromJS is currently only here for symmetry with PlayFromJS but
+ * in future we will likely have to flush style in
+ * CSSAnimation::PauseFromJS so we leave it for now.
+ */
+ void PauseFromJS(ErrorResult& aRv) { Pause(aRv); }
+
+ // Wrapper functions for Animation DOM methods when called from style.
+
+ virtual void CancelFromStyle() { CancelNoUpdate(); }
+ void SetTimelineNoUpdate(AnimationTimeline* aTimeline);
+ void SetEffectNoUpdate(AnimationEffectReadOnly* aEffect);
+
+ virtual void Tick();
+ bool NeedsTicks() const
+ {
+ AnimationPlayState playState = PlayState();
+ return playState == AnimationPlayState::Running ||
+ playState == AnimationPlayState::Pending;
+ }
+
+ /**
+ * Set the time to use for starting or pausing a pending animation.
+ *
+ * Typically, when an animation is played, it does not start immediately but
+ * is added to a table of pending animations on the document of its effect.
+ * In the meantime it sets its hold time to the time from which playback
+ * should begin.
+ *
+ * When the document finishes painting, any pending animations in its table
+ * are marked as being ready to start by calling StartOnNextTick.
+ * The moment when the paint completed is also recorded, converted to a
+ * timeline time, and passed to StartOnTick. This is so that when these
+ * animations do start, they can be timed from the point when painting
+ * completed.
+ *
+ * After calling TriggerOnNextTick, animations remain in the pending state
+ * until the next refresh driver tick. At that time they transition out of
+ * the pending state using the time passed to TriggerOnNextTick as the
+ * effective time at which they resumed.
+ *
+ * This approach means that any setup time required for performing the
+ * initial paint of an animation such as layerization is not deducted from
+ * the running time of the animation. Without this we can easily drop the
+ * first few frames of an animation, or, on slower devices, the whole
+ * animation.
+ *
+ * Furthermore:
+ *
+ * - Starting the animation immediately when painting finishes is problematic
+ * because the start time of the animation will be ahead of its timeline
+ * (since the timeline time is based on the refresh driver time).
+ * That's a problem because the animation is playing but its timing
+ * suggests it starts in the future. We could update the timeline to match
+ * the start time of the animation but then we'd also have to update the
+ * timing and style of all animations connected to that timeline or else be
+ * stuck in an inconsistent state until the next refresh driver tick.
+ *
+ * - If we simply use the refresh driver time on its next tick, the lag
+ * between triggering an animation and its effective start is unacceptably
+ * long.
+ *
+ * For pausing, we apply the same asynchronous approach. This is so that we
+ * synchronize with animations that are running on the compositor. Otherwise
+ * if the main thread lags behind the compositor there will be a noticeable
+ * jump backwards when the main thread takes over. Even though main thread
+ * animations could be paused immediately, we do it asynchronously for
+ * consistency and so that animations paused together end up in step.
+ *
+ * Note that the caller of this method is responsible for removing the
+ * animation from any PendingAnimationTracker it may have been added to.
+ */
+ void TriggerOnNextTick(const Nullable<TimeDuration>& aReadyTime);
+ /**
+ * Testing only: Start or pause a pending animation using the current
+ * timeline time. This is used to support existing tests that expect
+ * animations to begin immediately. Ideally we would rewrite the those tests
+ * and get rid of this method, but there are a lot of them.
+ *
+ * As with TriggerOnNextTick, the caller of this method is responsible for
+ * removing the animation from any PendingAnimationTracker it may have been
+ * added to.
+ */
+ void TriggerNow();
+ /**
+ * When StartOnNextTick is called, we store the ready time but we don't apply
+ * it until the next tick. In the meantime, GetStartTime() will return null.
+ *
+ * However, if we build layer animations again before the next tick, we
+ * should initialize them with the start time that GetStartTime() will return
+ * on the next tick.
+ *
+ * If we were to simply set the start time of layer animations to null, their
+ * start time would be updated to the current wallclock time when rendering
+ * finishes, thus making them out of sync with the start time stored here.
+ * This, in turn, will make the animation jump backwards when we build
+ * animations on the next tick and apply the start time stored here.
+ *
+ * This method returns the start time, if resolved. Otherwise, if we have
+ * a pending ready time, it returns the corresponding start time. If neither
+ * of those are available, it returns null.
+ */
+ Nullable<TimeDuration> GetCurrentOrPendingStartTime() const;
+
+ /**
+ * Calculates the corresponding start time to use for an animation that is
+ * currently pending with current time |mHoldTime| but should behave
+ * as if it began or resumed playback at timeline time |aReadyTime|.
+ */
+ TimeDuration StartTimeFromReadyTime(const TimeDuration& aReadyTime) const;
+
+ /**
+ * Converts a time in the timescale of this Animation's currentTime, to a
+ * TimeStamp. Returns a null TimeStamp if the conversion cannot be performed
+ * because of the current state of this Animation (e.g. it has no timeline, a
+ * zero playbackRate, an unresolved start time etc.) or the value of the time
+ * passed-in (e.g. an infinite time).
+ */
+ TimeStamp AnimationTimeToTimeStamp(const StickyTimeDuration& aTime) const;
+
+ // Converts an AnimationEvent's elapsedTime value to an equivalent TimeStamp
+ // that can be used to sort events by when they occurred.
+ TimeStamp ElapsedTimeToTimeStamp(const StickyTimeDuration& aElapsedTime) const;
+
+ bool IsPausedOrPausing() const
+ {
+ return PlayState() == AnimationPlayState::Paused ||
+ mPendingState == PendingState::PausePending;
+ }
+
+ bool HasCurrentEffect() const
+ {
+ return GetEffect() && GetEffect()->IsCurrent();
+ }
+ bool IsInEffect() const
+ {
+ return GetEffect() && GetEffect()->IsInEffect();
+ }
+
+ /**
+ * Returns true if this animation's playback state makes it a candidate for
+ * running on the compositor.
+ * We send animations to the compositor when their target effect is 'current'
+ * (a definition that is roughly equivalent to when they are in their before
+ * or active phase). However, we don't send animations to the compositor when
+ * they are paused/pausing (including being effectively paused due to
+ * having a zero playback rate), have a zero-duration active interval, or have
+ * no target effect at all.
+ */
+ bool IsPlayableOnCompositor() const
+ {
+ return HasCurrentEffect() &&
+ mPlaybackRate != 0.0 &&
+ (PlayState() == AnimationPlayState::Running ||
+ mPendingState == PendingState::PlayPending) &&
+ !GetEffect()->IsActiveDurationZero();
+ }
+ bool IsRelevant() const { return mIsRelevant; }
+ void UpdateRelevance();
+
+ /**
+ * Returns true if this Animation has a lower composite order than aOther.
+ */
+ bool HasLowerCompositeOrderThan(const Animation& aOther) const;
+
+ /**
+ * Returns the level at which the effect(s) associated with this Animation
+ * are applied to the CSS cascade.
+ */
+ virtual EffectCompositor::CascadeLevel CascadeLevel() const
+ {
+ return EffectCompositor::CascadeLevel::Animations;
+ }
+
+ /**
+ * Returns true if this animation does not currently need to update
+ * style on the main thread (e.g. because it is empty, or is
+ * running on the compositor).
+ */
+ bool CanThrottle() const;
+ /**
+ * Updates |aStyleRule| with the animation values of this animation's effect,
+ * if any.
+ * Any properties contained in |aPropertiesToSkip| will not be added or
+ * updated in |aStyleRule|.
+ */
+ void ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
+ const nsCSSPropertyIDSet& aPropertiesToSkip);
+
+ void NotifyEffectTimingUpdated();
+
+protected:
+ void SilentlySetCurrentTime(const TimeDuration& aNewCurrentTime);
+ void SilentlySetPlaybackRate(double aPlaybackRate);
+ void CancelNoUpdate();
+ void PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior);
+ void PauseNoUpdate(ErrorResult& aRv);
+ void ResumeAt(const TimeDuration& aReadyTime);
+ void PauseAt(const TimeDuration& aReadyTime);
+ void FinishPendingAt(const TimeDuration& aReadyTime)
+ {
+ if (mPendingState == PendingState::PlayPending) {
+ ResumeAt(aReadyTime);
+ } else if (mPendingState == PendingState::PausePending) {
+ PauseAt(aReadyTime);
+ } else {
+ NS_NOTREACHED("Can't finish pending if we're not in a pending state");
+ }
+ }
+
+ /**
+ * Finishing behavior depends on if changes to timing occurred due
+ * to a seek or regular playback.
+ */
+ enum class SeekFlag {
+ NoSeek,
+ DidSeek
+ };
+
+ enum class SyncNotifyFlag {
+ Sync,
+ Async
+ };
+
+ virtual void UpdateTiming(SeekFlag aSeekFlag,
+ SyncNotifyFlag aSyncNotifyFlag);
+ 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();
+
+ /**
+ * Performs the same steps as CancelPendingTasks and also rejects and
+ * recreates the ready promise if the animation was pending.
+ */
+ void ResetPendingTasks();
+
+ bool IsPossiblyOrphanedPendingAnimation() const;
+ StickyTimeDuration EffectEnd() const;
+
+ nsIDocument* GetRenderedDocument() const;
+
+ RefPtr<AnimationTimeline> mTimeline;
+ RefPtr<AnimationEffectReadOnly> 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
+ double mPlaybackRate;
+
+ // A Promise that is replaced on each call to Play()
+ // and fulfilled when Play() is successfully completed.
+ // This object is lazily created by GetReady.
+ // See http://w3c.github.io/web-animations/#current-ready-promise
+ RefPtr<Promise> mReady;
+
+ // A Promise that is resolved when we reach the end of the effect, or
+ // 0 when playing backwards. The Promise is replaced if the animation is
+ // finished but then a state change makes it not finished.
+ // This object is lazily created by GetFinished.
+ // See http://w3c.github.io/web-animations/#current-finished-promise
+ RefPtr<Promise> mFinished;
+
+ // Indicates if the animation is in the pending state (and what state it is
+ // waiting to enter when it finished pending). We use this rather than
+ // checking if this animation is tracked by a PendingAnimationTracker because
+ // the animation will continue to be pending even after it has been removed
+ // from the PendingAnimationTracker while it is waiting for the next tick
+ // (see TriggerOnNextTick for details).
+ enum class PendingState { NotPending, PlayPending, PausePending };
+ PendingState mPendingState;
+
+ static uint64_t sNextAnimationIndex;
+
+ // The relative position of this animation within the global animation list.
+ // This is kNoIndex while the animation is in the idle state and is updated
+ // each time the animation transitions out of the idle state.
+ //
+ // Note that subclasses such as CSSTransition and CSSAnimation may repurpose
+ // this member to implement their own brand of sorting. As a result, it is
+ // possible for two different objects to have the same index.
+ uint64_t mAnimationIndex;
+
+ bool mFinishedAtLastComposeStyle;
+ // Indicates that the animation should be exposed in an element's
+ // getAnimations() list.
+ bool mIsRelevant;
+
+ nsRevocableEventPtr<nsRunnableMethod<Animation>> mFinishNotificationTask;
+ // True if mFinished is resolved or would be resolved if mFinished has
+ // yet to be created. This is not set when mFinished is rejected since
+ // in that case mFinished is immediately reset to represent a new current
+ // finished promise.
+ bool mFinishedIsResolved;
+
+ nsString mId;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_Animation_h
diff --git a/dom/animation/AnimationComparator.h b/dom/animation/AnimationComparator.h
new file mode 100644
index 0000000000..ff665e82aa
--- /dev/null
+++ b/dom/animation/AnimationComparator.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_AnimationComparator_h
+#define mozilla_AnimationComparator_h
+
+namespace mozilla {
+
+// Although this file is called AnimationComparator, we don't actually
+// implement AnimationComparator (to compare const Animation& parameters)
+// since it's not actually needed (yet).
+
+template<typename AnimationPtrType>
+class AnimationPtrComparator {
+public:
+ bool Equals(const AnimationPtrType& a, const AnimationPtrType& b) const
+ {
+ return a == b;
+ }
+
+ bool LessThan(const AnimationPtrType& a, const AnimationPtrType& b) const
+ {
+ return a->HasLowerCompositeOrderThan(*b);
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_AnimationComparator_h
diff --git a/dom/animation/AnimationEffectReadOnly.cpp b/dom/animation/AnimationEffectReadOnly.cpp
new file mode 100644
index 0000000000..aff28a37bf
--- /dev/null
+++ b/dom/animation/AnimationEffectReadOnly.cpp
@@ -0,0 +1,343 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "mozilla/dom/AnimationEffectReadOnly.h"
+#include "mozilla/dom/AnimationEffectReadOnlyBinding.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/FloatingPoint.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(AnimationEffectReadOnly)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationEffectReadOnly)
+ if (tmp->mTiming) {
+ tmp->mTiming->Unlink();
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument, mTiming, mAnimation)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationEffectReadOnly)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument, mTiming, mAnimation)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(AnimationEffectReadOnly)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationEffectReadOnly)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationEffectReadOnly)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationEffectReadOnly)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+AnimationEffectReadOnly::AnimationEffectReadOnly(
+ nsIDocument* aDocument, AnimationEffectTimingReadOnly* aTiming)
+ : mDocument(aDocument)
+ , mTiming(aTiming)
+{
+ MOZ_ASSERT(aTiming);
+}
+
+// https://w3c.github.io/web-animations/#current
+bool
+AnimationEffectReadOnly::IsCurrent() const
+{
+ if (!mAnimation || mAnimation->PlayState() == AnimationPlayState::Finished) {
+ return false;
+ }
+
+ ComputedTiming computedTiming = GetComputedTiming();
+ return computedTiming.mPhase == ComputedTiming::AnimationPhase::Before ||
+ computedTiming.mPhase == ComputedTiming::AnimationPhase::Active;
+}
+
+// https://w3c.github.io/web-animations/#in-effect
+bool
+AnimationEffectReadOnly::IsInEffect() const
+{
+ ComputedTiming computedTiming = GetComputedTiming();
+ return !computedTiming.mProgress.IsNull();
+}
+
+already_AddRefed<AnimationEffectTimingReadOnly>
+AnimationEffectReadOnly::Timing()
+{
+ RefPtr<AnimationEffectTimingReadOnly> temp(mTiming);
+ return temp.forget();
+}
+
+void
+AnimationEffectReadOnly::SetSpecifiedTiming(const TimingParams& aTiming)
+{
+ if (mTiming->AsTimingParams() == aTiming) {
+ return;
+ }
+ mTiming->SetTimingParams(aTiming);
+ if (mAnimation) {
+ mAnimation->NotifyEffectTimingUpdated();
+ }
+ // For keyframe effects, NotifyEffectTimingUpdated above will eventually cause
+ // KeyframeEffectReadOnly::NotifyAnimationTimingUpdated to be called so it can
+ // update its registration with the target element as necessary.
+}
+
+ComputedTiming
+AnimationEffectReadOnly::GetComputedTimingAt(
+ const Nullable<TimeDuration>& aLocalTime,
+ const TimingParams& aTiming,
+ double aPlaybackRate)
+{
+ const StickyTimeDuration zeroDuration;
+
+ // Always return the same object to benefit from return-value optimization.
+ ComputedTiming result;
+
+ if (aTiming.mDuration) {
+ MOZ_ASSERT(aTiming.mDuration.ref() >= zeroDuration,
+ "Iteration duration should be positive");
+ result.mDuration = aTiming.mDuration.ref();
+ }
+
+ MOZ_ASSERT(aTiming.mIterations >= 0.0 && !IsNaN(aTiming.mIterations),
+ "mIterations should be nonnegative & finite, as ensured by "
+ "ValidateIterations or CSSParser");
+ result.mIterations = aTiming.mIterations;
+
+ MOZ_ASSERT(aTiming.mIterationStart >= 0.0,
+ "mIterationStart should be nonnegative, as ensured by "
+ "ValidateIterationStart");
+ result.mIterationStart = aTiming.mIterationStart;
+
+ result.mActiveDuration = aTiming.ActiveDuration();
+ result.mEndTime = aTiming.EndTime();
+ result.mFill = aTiming.mFill == dom::FillMode::Auto ?
+ dom::FillMode::None :
+ aTiming.mFill;
+
+ // The default constructor for ComputedTiming sets all other members to
+ // values consistent with an animation that has not been sampled.
+ if (aLocalTime.IsNull()) {
+ return result;
+ }
+ const TimeDuration& localTime = aLocalTime.Value();
+
+ // Calculate the time within the active interval.
+ // https://w3c.github.io/web-animations/#active-time
+ StickyTimeDuration activeTime;
+
+ StickyTimeDuration beforeActiveBoundary =
+ std::max(std::min(StickyTimeDuration(aTiming.mDelay), result.mEndTime),
+ zeroDuration);
+
+ StickyTimeDuration activeAfterBoundary =
+ std::max(std::min(StickyTimeDuration(aTiming.mDelay +
+ result.mActiveDuration),
+ result.mEndTime),
+ zeroDuration);
+
+ if (localTime > activeAfterBoundary ||
+ (aPlaybackRate >= 0 && localTime == activeAfterBoundary)) {
+ result.mPhase = ComputedTiming::AnimationPhase::After;
+ if (!result.FillsForwards()) {
+ // The animation isn't active or filling at this time.
+ return result;
+ }
+ activeTime =
+ std::max(std::min(StickyTimeDuration(localTime - aTiming.mDelay),
+ result.mActiveDuration),
+ zeroDuration);
+ } else if (localTime < beforeActiveBoundary ||
+ (aPlaybackRate < 0 && localTime == beforeActiveBoundary)) {
+ result.mPhase = ComputedTiming::AnimationPhase::Before;
+ if (!result.FillsBackwards()) {
+ // The animation isn't active or filling at this time.
+ return result;
+ }
+ activeTime = std::max(StickyTimeDuration(localTime - aTiming.mDelay),
+ zeroDuration);
+ } 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;
+ }
+
+ // Convert active time to a multiple of iterations.
+ // https://w3c.github.io/web-animations/#overall-progress
+ double overallProgress;
+ if (result.mDuration == zeroDuration) {
+ overallProgress = result.mPhase == ComputedTiming::AnimationPhase::Before
+ ? 0.0
+ : result.mIterations;
+ } else {
+ overallProgress = activeTime / result.mDuration;
+ }
+
+ // Factor in iteration start offset.
+ if (IsFinite(overallProgress)) {
+ overallProgress += result.mIterationStart;
+ }
+
+ // Determine the 0-based index of the current iteration.
+ // https://w3c.github.io/web-animations/#current-iteration
+ result.mCurrentIteration =
+ IsInfinite(result.mIterations) &&
+ result.mPhase == ComputedTiming::AnimationPhase::After
+ ? UINT64_MAX // In GetComputedTimingDictionary(),
+ // we will convert this into Infinity
+ : static_cast<uint64_t>(overallProgress);
+
+ // Convert the overall progress to a fraction of a single iteration--the
+ // simply iteration progress.
+ // https://w3c.github.io/web-animations/#simple-iteration-progress
+ double progress = IsFinite(overallProgress)
+ ? fmod(overallProgress, 1.0)
+ : fmod(result.mIterationStart, 1.0);
+
+ // When we finish exactly at the end of an iteration we need to report
+ // the end of the final iteration and not the start of the next iteration.
+ // We *don't* want to do this when we have a zero-iteration animation or
+ // when the animation has been effectively made into a zero-duration animation
+ // using a negative end-delay, however.
+ if (result.mPhase == ComputedTiming::AnimationPhase::After &&
+ progress == 0.0 &&
+ result.mIterations != 0.0 &&
+ (activeTime != zeroDuration || result.mDuration == zeroDuration)) {
+ // The only way we can be in the after phase with a progress of zero and
+ // a current iteration of zero, is if we have a zero iteration count or
+ // were clipped using a negative end delay--both of which we should have
+ // detected above.
+ MOZ_ASSERT(result.mCurrentIteration != 0,
+ "Should not have zero current iteration");
+ progress = 1.0;
+ if (result.mCurrentIteration != UINT64_MAX) {
+ result.mCurrentIteration--;
+ }
+ }
+
+ // Factor in the direction.
+ bool thisIterationReverse = false;
+ switch (aTiming.mDirection) {
+ case PlaybackDirection::Normal:
+ thisIterationReverse = false;
+ break;
+ case PlaybackDirection::Reverse:
+ thisIterationReverse = true;
+ break;
+ case PlaybackDirection::Alternate:
+ thisIterationReverse = (result.mCurrentIteration & 1) == 1;
+ break;
+ case PlaybackDirection::Alternate_reverse:
+ thisIterationReverse = (result.mCurrentIteration & 1) == 0;
+ break;
+ default:
+ MOZ_ASSERT(true, "Unknown PlaybackDirection type");
+ }
+ if (thisIterationReverse) {
+ progress = 1.0 - progress;
+ }
+
+ // Calculate the 'before flag' which we use when applying step timing
+ // functions.
+ if ((result.mPhase == ComputedTiming::AnimationPhase::After &&
+ thisIterationReverse) ||
+ (result.mPhase == ComputedTiming::AnimationPhase::Before &&
+ !thisIterationReverse)) {
+ result.mBeforeFlag = ComputedTimingFunction::BeforeFlag::Set;
+ }
+
+ // Apply the easing.
+ if (aTiming.mFunction) {
+ progress = aTiming.mFunction->GetValue(progress, result.mBeforeFlag);
+ }
+
+ MOZ_ASSERT(IsFinite(progress), "Progress value should be finite");
+ result.mProgress.SetValue(progress);
+ return result;
+}
+
+ComputedTiming
+AnimationEffectReadOnly::GetComputedTiming(const TimingParams* aTiming) const
+{
+ double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
+ return GetComputedTimingAt(GetLocalTime(),
+ aTiming ? *aTiming : SpecifiedTiming(),
+ playbackRate);
+}
+
+// Helper functions for generating a ComputedTimingProperties dictionary
+static void
+GetComputedTimingDictionary(const ComputedTiming& aComputedTiming,
+ const Nullable<TimeDuration>& aLocalTime,
+ const TimingParams& aTiming,
+ ComputedTimingProperties& aRetVal)
+{
+ // AnimationEffectTimingProperties
+ aRetVal.mDelay = aTiming.mDelay.ToMilliseconds();
+ aRetVal.mEndDelay = aTiming.mEndDelay.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);
+ }
+}
+
+void
+AnimationEffectReadOnly::GetComputedTimingAsDict(
+ ComputedTimingProperties& aRetVal) const
+{
+ double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
+ const Nullable<TimeDuration> currentTime = GetLocalTime();
+ GetComputedTimingDictionary(GetComputedTimingAt(currentTime,
+ SpecifiedTiming(),
+ playbackRate),
+ currentTime,
+ SpecifiedTiming(),
+ aRetVal);
+}
+
+AnimationEffectReadOnly::~AnimationEffectReadOnly()
+{
+ // mTiming is cycle collected, so we have to do null check first even though
+ // mTiming shouldn't be null during the lifetime of KeyframeEffect.
+ if (mTiming) {
+ mTiming->Unlink();
+ }
+}
+
+Nullable<TimeDuration>
+AnimationEffectReadOnly::GetLocalTime() const
+{
+ // Since the *animation* start time is currently always zero, the local
+ // time is equal to the parent time.
+ Nullable<TimeDuration> result;
+ if (mAnimation) {
+ result = mAnimation->GetCurrentTime();
+ }
+ return result;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/animation/AnimationEffectReadOnly.h b/dom/animation/AnimationEffectReadOnly.h
new file mode 100644
index 0000000000..fdea493142
--- /dev/null
+++ b/dom/animation/AnimationEffectReadOnly.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_dom_AnimationEffectReadOnly_h
+#define mozilla_dom_AnimationEffectReadOnly_h
+
+#include "mozilla/ComputedTiming.h"
+#include "mozilla/dom/AnimationEffectTimingReadOnly.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StickyTimeDuration.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/TimingParams.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+
+struct ElementPropertyTransition;
+
+namespace dom {
+
+class Animation;
+class AnimationEffectTimingReadOnly;
+class KeyframeEffectReadOnly;
+struct ComputedTimingProperties;
+
+class AnimationEffectReadOnly : public nsISupports,
+ public nsWrapperCache
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AnimationEffectReadOnly)
+
+ AnimationEffectReadOnly(nsIDocument* aDocument,
+ AnimationEffectTimingReadOnly* aTiming);
+
+ virtual KeyframeEffectReadOnly* AsKeyframeEffect() { return nullptr; }
+
+ virtual ElementPropertyTransition* AsTransition() { return nullptr; }
+ virtual const ElementPropertyTransition* AsTransition() const
+ {
+ return nullptr;
+ }
+
+ nsISupports* GetParentObject() const { return mDocument; }
+
+ bool IsCurrent() const;
+ bool IsInEffect() const;
+ bool IsActiveDurationZero() const
+ {
+ return SpecifiedTiming().ActiveDuration() == StickyTimeDuration();
+ }
+
+ already_AddRefed<AnimationEffectTimingReadOnly> Timing();
+ const TimingParams& SpecifiedTiming() const
+ {
+ return mTiming->AsTimingParams();
+ }
+ void SetSpecifiedTiming(const TimingParams& aTiming);
+
+ // This function takes as input the timing parameters of an animation and
+ // returns the computed timing at the specified local time.
+ //
+ // The local time may be null in which case only static parameters such as the
+ // active duration are calculated. All other members of the returned object
+ // are given a null/initial value.
+ //
+ // This function returns a null mProgress member of the return value
+ // if the animation should not be run
+ // (because it is not currently active and is not filling at this time).
+ static ComputedTiming
+ GetComputedTimingAt(const Nullable<TimeDuration>& aLocalTime,
+ const TimingParams& aTiming,
+ double aPlaybackRate);
+ // Shortcut that gets the computed timing using the current local time as
+ // calculated from the timeline time.
+ ComputedTiming GetComputedTiming(const TimingParams* aTiming = nullptr) const;
+ void GetComputedTimingAsDict(ComputedTimingProperties& aRetVal) const;
+
+ virtual void SetAnimation(Animation* aAnimation) = 0;
+ Animation* GetAnimation() const { return mAnimation; };
+
+protected:
+ virtual ~AnimationEffectReadOnly();
+
+ Nullable<TimeDuration> GetLocalTime() const;
+
+protected:
+ RefPtr<nsIDocument> mDocument;
+ RefPtr<AnimationEffectTimingReadOnly> mTiming;
+ RefPtr<Animation> mAnimation;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_AnimationEffectReadOnly_h
diff --git a/dom/animation/AnimationEffectTiming.cpp b/dom/animation/AnimationEffectTiming.cpp
new file mode 100644
index 0000000000..8eb4c6edfc
--- /dev/null
+++ b/dom/animation/AnimationEffectTiming.cpp
@@ -0,0 +1,152 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "mozilla/dom/AnimationEffectTiming.h"
+
+#include "mozilla/dom/AnimatableBinding.h"
+#include "mozilla/dom/AnimationEffectTimingBinding.h"
+#include "mozilla/dom/KeyframeEffect.h"
+#include "mozilla/TimingParams.h"
+#include "nsAString.h"
+
+namespace mozilla {
+namespace dom {
+
+JSObject*
+AnimationEffectTiming::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return AnimationEffectTimingBinding::Wrap(aCx, this, aGivenProto);
+}
+
+static inline void
+PostSpecifiedTimingUpdated(KeyframeEffect* aEffect)
+{
+ if (aEffect) {
+ aEffect->NotifySpecifiedTimingUpdated();
+ }
+}
+
+void
+AnimationEffectTiming::SetDelay(double aDelay)
+{
+ TimeDuration delay = TimeDuration::FromMilliseconds(aDelay);
+ if (mTiming.mDelay == delay) {
+ return;
+ }
+ mTiming.mDelay = delay;
+
+ PostSpecifiedTimingUpdated(mEffect);
+}
+
+void
+AnimationEffectTiming::SetEndDelay(double aEndDelay)
+{
+ TimeDuration endDelay = TimeDuration::FromMilliseconds(aEndDelay);
+ if (mTiming.mEndDelay == endDelay) {
+ return;
+ }
+ mTiming.mEndDelay = endDelay;
+
+ PostSpecifiedTimingUpdated(mEffect);
+}
+
+void
+AnimationEffectTiming::SetFill(const FillMode& aFill)
+{
+ if (mTiming.mFill == aFill) {
+ return;
+ }
+ mTiming.mFill = aFill;
+
+ PostSpecifiedTimingUpdated(mEffect);
+}
+
+void
+AnimationEffectTiming::SetIterationStart(double aIterationStart,
+ ErrorResult& aRv)
+{
+ if (mTiming.mIterationStart == aIterationStart) {
+ return;
+ }
+
+ TimingParams::ValidateIterationStart(aIterationStart, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ mTiming.mIterationStart = aIterationStart;
+
+ PostSpecifiedTimingUpdated(mEffect);
+}
+
+void
+AnimationEffectTiming::SetIterations(double aIterations, ErrorResult& aRv)
+{
+ if (mTiming.mIterations == aIterations) {
+ return;
+ }
+
+ TimingParams::ValidateIterations(aIterations, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ mTiming.mIterations = aIterations;
+
+ PostSpecifiedTimingUpdated(mEffect);
+}
+
+void
+AnimationEffectTiming::SetDuration(const UnrestrictedDoubleOrString& aDuration,
+ ErrorResult& aRv)
+{
+ Maybe<StickyTimeDuration> newDuration =
+ TimingParams::ParseDuration(aDuration, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ if (mTiming.mDuration == newDuration) {
+ return;
+ }
+
+ mTiming.mDuration = newDuration;
+
+ PostSpecifiedTimingUpdated(mEffect);
+}
+
+void
+AnimationEffectTiming::SetDirection(const PlaybackDirection& aDirection)
+{
+ if (mTiming.mDirection == aDirection) {
+ return;
+ }
+
+ mTiming.mDirection = aDirection;
+
+ PostSpecifiedTimingUpdated(mEffect);
+}
+
+void
+AnimationEffectTiming::SetEasing(const nsAString& aEasing, ErrorResult& aRv)
+{
+ Maybe<ComputedTimingFunction> newFunction =
+ TimingParams::ParseEasing(aEasing, mDocument, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ if (mTiming.mFunction == newFunction) {
+ return;
+ }
+
+ mTiming.mFunction = newFunction;
+
+ PostSpecifiedTimingUpdated(mEffect);
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/animation/AnimationEffectTiming.h b/dom/animation/AnimationEffectTiming.h
new file mode 100644
index 0000000000..06844b3202
--- /dev/null
+++ b/dom/animation/AnimationEffectTiming.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef mozilla_dom_AnimationEffectTiming_h
+#define mozilla_dom_AnimationEffectTiming_h
+
+#include "mozilla/dom/AnimationEffectTimingReadOnly.h"
+#include "mozilla/Attributes.h" // For MOZ_NON_OWNING_REF
+#include "nsStringFwd.h"
+
+namespace mozilla {
+namespace dom {
+
+class KeyframeEffect;
+
+class AnimationEffectTiming : public AnimationEffectTimingReadOnly
+{
+public:
+ AnimationEffectTiming(nsIDocument* aDocument,
+ const TimingParams& aTiming,
+ KeyframeEffect* aEffect)
+ : AnimationEffectTimingReadOnly(aDocument, aTiming)
+ , mEffect(aEffect) { }
+
+ JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+ void Unlink() override { mEffect = nullptr; }
+
+ void SetDelay(double aDelay);
+ void SetEndDelay(double aEndDelay);
+ void SetFill(const FillMode& aFill);
+ void SetIterationStart(double aIterationStart, ErrorResult& aRv);
+ void SetIterations(double aIterations, ErrorResult& aRv);
+ void SetDuration(const UnrestrictedDoubleOrString& aDuration,
+ ErrorResult& aRv);
+ void SetDirection(const PlaybackDirection& aDirection);
+ void SetEasing(const nsAString& aEasing, ErrorResult& aRv);
+
+private:
+ KeyframeEffect* MOZ_NON_OWNING_REF mEffect;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_AnimationEffectTiming_h
diff --git a/dom/animation/AnimationEffectTimingReadOnly.cpp b/dom/animation/AnimationEffectTimingReadOnly.cpp
new file mode 100644
index 0000000000..76cd53049e
--- /dev/null
+++ b/dom/animation/AnimationEffectTimingReadOnly.cpp
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "mozilla/dom/AnimationEffectTimingReadOnly.h"
+
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/dom/AnimatableBinding.h"
+#include "mozilla/dom/AnimationEffectTimingReadOnlyBinding.h"
+#include "mozilla/dom/CSSPseudoElement.h"
+#include "mozilla/dom/KeyframeEffectBinding.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AnimationEffectTimingReadOnly, mDocument)
+
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(AnimationEffectTimingReadOnly, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(AnimationEffectTimingReadOnly, Release)
+
+JSObject*
+AnimationEffectTimingReadOnly::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return AnimationEffectTimingReadOnlyBinding::Wrap(aCx, this, aGivenProto);
+}
+
+void
+AnimationEffectTimingReadOnly::GetDuration(
+ OwningUnrestrictedDoubleOrString& aRetVal) const
+{
+ if (mTiming.mDuration) {
+ aRetVal.SetAsUnrestrictedDouble() = mTiming.mDuration->ToMilliseconds();
+ } else {
+ aRetVal.SetAsString().AssignLiteral("auto");
+ }
+}
+
+void
+AnimationEffectTimingReadOnly::GetEasing(nsString& aRetVal) const
+{
+ if (mTiming.mFunction) {
+ mTiming.mFunction->AppendToString(aRetVal);
+ } else {
+ aRetVal.AssignLiteral("linear");
+ }
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/animation/AnimationEffectTimingReadOnly.h b/dom/animation/AnimationEffectTimingReadOnly.h
new file mode 100644
index 0000000000..1f1d50619a
--- /dev/null
+++ b/dom/animation/AnimationEffectTimingReadOnly.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef mozilla_dom_AnimationEffectTimingReadOnly_h
+#define mozilla_dom_AnimationEffectTimingReadOnly_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/TimingParams.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+namespace dom {
+
+class AnimationEffectTimingReadOnly : public nsWrapperCache
+{
+public:
+ AnimationEffectTimingReadOnly() = default;
+ AnimationEffectTimingReadOnly(nsIDocument* aDocument,
+ const TimingParams& aTiming)
+ : mDocument(aDocument)
+ , mTiming(aTiming) { }
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AnimationEffectTimingReadOnly)
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(AnimationEffectTimingReadOnly)
+
+protected:
+ virtual ~AnimationEffectTimingReadOnly() = default;
+
+public:
+ nsISupports* GetParentObject() const { return mDocument; }
+ JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+ double Delay() const { return mTiming.mDelay.ToMilliseconds(); }
+ double EndDelay() const { return mTiming.mEndDelay.ToMilliseconds(); }
+ FillMode Fill() const { return mTiming.mFill; }
+ double IterationStart() const { return mTiming.mIterationStart; }
+ double Iterations() const { return mTiming.mIterations; }
+ void GetDuration(OwningUnrestrictedDoubleOrString& aRetVal) const;
+ PlaybackDirection Direction() const { return mTiming.mDirection; }
+ void GetEasing(nsString& aRetVal) const;
+
+ const TimingParams& AsTimingParams() const { return mTiming; }
+ void SetTimingParams(const TimingParams& aTiming) { mTiming = aTiming; }
+
+ virtual void Unlink() { }
+
+protected:
+ RefPtr<nsIDocument> mDocument;
+ TimingParams mTiming;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_AnimationEffectTimingReadOnly_h
diff --git a/dom/animation/AnimationPerformanceWarning.cpp b/dom/animation/AnimationPerformanceWarning.cpp
new file mode 100644
index 0000000000..80ece31986
--- /dev/null
+++ b/dom/animation/AnimationPerformanceWarning.cpp
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "AnimationPerformanceWarning.h"
+
+#include "nsContentUtils.h"
+
+namespace mozilla {
+
+template<uint32_t N> nsresult
+AnimationPerformanceWarning::ToLocalizedStringWithIntParams(
+ const char* aKey, nsXPIDLString& aLocalizedString) const
+{
+ nsAutoString strings[N];
+ const char16_t* charParams[N];
+
+ for (size_t i = 0, n = mParams->Length(); i < n; i++) {
+ strings[i].AppendInt((*mParams)[i]);
+ charParams[i] = strings[i].get();
+ }
+
+ return nsContentUtils::FormatLocalizedString(
+ nsContentUtils::eLAYOUT_PROPERTIES, aKey, charParams, aLocalizedString);
+}
+
+bool
+AnimationPerformanceWarning::ToLocalizedString(
+ nsXPIDLString& aLocalizedString) const
+{
+ const char* key = nullptr;
+
+ switch (mType) {
+ case Type::ContentTooSmall:
+ MOZ_ASSERT(mParams && mParams->Length() == 2,
+ "Parameter's length should be 2 for ContentTooSmall");
+
+ return NS_SUCCEEDED(
+ ToLocalizedStringWithIntParams<2>(
+ "CompositorAnimationWarningContentTooSmall", aLocalizedString));
+ case Type::ContentTooLarge:
+ MOZ_ASSERT(mParams && mParams->Length() == 7,
+ "Parameter's length should be 7 for ContentTooLarge");
+
+ return NS_SUCCEEDED(
+ ToLocalizedStringWithIntParams<7>(
+ "CompositorAnimationWarningContentTooLarge", aLocalizedString));
+ case Type::TransformBackfaceVisibilityHidden:
+ key = "CompositorAnimationWarningTransformBackfaceVisibilityHidden";
+ break;
+ case Type::TransformPreserve3D:
+ key = "CompositorAnimationWarningTransformPreserve3D";
+ break;
+ case Type::TransformSVG:
+ key = "CompositorAnimationWarningTransformSVG";
+ break;
+ case Type::TransformWithGeometricProperties:
+ key = "CompositorAnimationWarningTransformWithGeometricProperties";
+ break;
+ case Type::TransformFrameInactive:
+ key = "CompositorAnimationWarningTransformFrameInactive";
+ break;
+ case Type::OpacityFrameInactive:
+ key = "CompositorAnimationWarningOpacityFrameInactive";
+ break;
+ case Type::HasRenderingObserver:
+ key = "CompositorAnimationWarningHasRenderingObserver";
+ break;
+ }
+
+ nsresult rv =
+ nsContentUtils::GetLocalizedString(nsContentUtils::eLAYOUT_PROPERTIES,
+ key, aLocalizedString);
+ return NS_SUCCEEDED(rv);
+}
+
+} // namespace mozilla
diff --git a/dom/animation/AnimationPerformanceWarning.h b/dom/animation/AnimationPerformanceWarning.h
new file mode 100644
index 0000000000..025857e0c0
--- /dev/null
+++ b/dom/animation/AnimationPerformanceWarning.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_dom_AnimationPerformanceWarning_h
+#define mozilla_dom_AnimationPerformanceWarning_h
+
+#include <initializer_list>
+
+class nsXPIDLString;
+
+namespace mozilla {
+
+// Represents the reason why we can't run the CSS property on the compositor.
+struct AnimationPerformanceWarning
+{
+ enum class Type : uint8_t {
+ ContentTooSmall,
+ ContentTooLarge,
+ TransformBackfaceVisibilityHidden,
+ TransformPreserve3D,
+ TransformSVG,
+ TransformWithGeometricProperties,
+ TransformFrameInactive,
+ OpacityFrameInactive,
+ HasRenderingObserver,
+ };
+
+ explicit AnimationPerformanceWarning(Type aType)
+ : mType(aType) { }
+
+ AnimationPerformanceWarning(Type aType,
+ std::initializer_list<int32_t> aParams)
+ : mType(aType)
+ {
+ // FIXME: Once std::initializer_list::size() become a constexpr function,
+ // we should use static_assert here.
+ MOZ_ASSERT(aParams.size() <= kMaxParamsForLocalization,
+ "The length of parameters should be less than "
+ "kMaxParamsForLocalization");
+ mParams.emplace(aParams);
+ }
+
+ // Maximum number of parameters passed to
+ // nsContentUtils::FormatLocalizedString to localize warning messages.
+ //
+ // NOTE: This constexpr can't be forward declared, so if you want to use
+ // this variable, please include this header file directly.
+ // This value is the same as the limit of nsStringBundle::FormatString.
+ // See the implementation of nsStringBundle::FormatString.
+ static constexpr uint8_t kMaxParamsForLocalization = 10;
+
+ // Indicates why this property could not be animated on the compositor.
+ Type mType;
+
+ // Optional parameters that may be used for localization.
+ Maybe<nsTArray<int32_t>> mParams;
+
+ bool ToLocalizedString(nsXPIDLString& aLocalizedString) const;
+ template<uint32_t N>
+ nsresult ToLocalizedStringWithIntParams(
+ const char* aKey, nsXPIDLString& aLocalizedString) const;
+
+ bool operator==(const AnimationPerformanceWarning& aOther) const
+ {
+ return mType == aOther.mType &&
+ mParams == aOther.mParams;
+ }
+ bool operator!=(const AnimationPerformanceWarning& aOther) const
+ {
+ return !(*this == aOther);
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_dom_AnimationPerformanceWarning_h
diff --git a/dom/animation/AnimationTarget.h b/dom/animation/AnimationTarget.h
new file mode 100644
index 0000000000..dbfef2e10c
--- /dev/null
+++ b/dom/animation/AnimationTarget.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_AnimationTarget_h
+#define mozilla_AnimationTarget_h
+
+#include "mozilla/Attributes.h" // For MOZ_NON_OWNING_REF
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "nsCSSPseudoElements.h"
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+} // namespace dom
+
+struct OwningAnimationTarget
+{
+ OwningAnimationTarget(dom::Element* aElement, CSSPseudoElementType aType)
+ : mElement(aElement), mPseudoType(aType) { }
+
+ explicit OwningAnimationTarget(dom::Element* aElement)
+ : mElement(aElement) { }
+
+ bool operator==(const OwningAnimationTarget& aOther) const
+ {
+ return mElement == aOther.mElement &&
+ mPseudoType == aOther.mPseudoType;
+ }
+
+ // mElement represents the parent element of a pseudo-element, not the
+ // generated content element.
+ RefPtr<dom::Element> mElement;
+ CSSPseudoElementType mPseudoType = CSSPseudoElementType::NotPseudo;
+};
+
+struct NonOwningAnimationTarget
+{
+ NonOwningAnimationTarget(dom::Element* aElement, CSSPseudoElementType aType)
+ : mElement(aElement), mPseudoType(aType) { }
+
+ explicit NonOwningAnimationTarget(const OwningAnimationTarget& aOther)
+ : mElement(aOther.mElement), mPseudoType(aOther.mPseudoType) { }
+
+ // mElement represents the parent element of a pseudo-element, not the
+ // generated content element.
+ dom::Element* MOZ_NON_OWNING_REF mElement = nullptr;
+ CSSPseudoElementType mPseudoType = CSSPseudoElementType::NotPseudo;
+};
+
+
+// Helper functions for cycle-collecting Maybe<OwningAnimationTarget>
+inline void
+ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback,
+ Maybe<OwningAnimationTarget>& aTarget,
+ const char* aName,
+ uint32_t aFlags = 0)
+{
+ if (aTarget) {
+ ImplCycleCollectionTraverse(aCallback, aTarget->mElement, aName, aFlags);
+ }
+}
+
+inline void
+ImplCycleCollectionUnlink(Maybe<OwningAnimationTarget>& aTarget)
+{
+ if (aTarget) {
+ ImplCycleCollectionUnlink(aTarget->mElement);
+ }
+}
+
+} // namespace mozilla
+
+#endif // mozilla_AnimationTarget_h
diff --git a/dom/animation/AnimationTimeline.cpp b/dom/animation/AnimationTimeline.cpp
new file mode 100644
index 0000000000..643106807c
--- /dev/null
+++ b/dom/animation/AnimationTimeline.cpp
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "AnimationTimeline.h"
+#include "mozilla/AnimationComparator.h"
+#include "mozilla/dom/Animation.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(AnimationTimeline)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationTimeline)
+ tmp->mAnimationOrder.clear();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow, mAnimations)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationTimeline)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow, mAnimations)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(AnimationTimeline)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationTimeline)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationTimeline)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationTimeline)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+void
+AnimationTimeline::NotifyAnimationUpdated(Animation& aAnimation)
+{
+ if (mAnimations.Contains(&aAnimation)) {
+ return;
+ }
+
+ if (aAnimation.GetTimeline() && aAnimation.GetTimeline() != this) {
+ aAnimation.GetTimeline()->RemoveAnimation(&aAnimation);
+ }
+
+ mAnimations.PutEntry(&aAnimation);
+ mAnimationOrder.insertBack(&aAnimation);
+}
+
+void
+AnimationTimeline::RemoveAnimation(Animation* aAnimation)
+{
+ MOZ_ASSERT(!aAnimation->GetTimeline() || aAnimation->GetTimeline() == this);
+ if (aAnimation->isInList()) {
+ aAnimation->remove();
+ }
+ mAnimations.RemoveEntry(aAnimation);
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/animation/AnimationTimeline.h b/dom/animation/AnimationTimeline.h
new file mode 100644
index 0000000000..d36cc40271
--- /dev/null
+++ b/dom/animation/AnimationTimeline.h
@@ -0,0 +1,125 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_dom_AnimationTimeline_h
+#define mozilla_dom_AnimationTimeline_h
+
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+#include "nsCycleCollectionParticipant.h"
+#include "js/TypeDecls.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/Attributes.h"
+#include "nsHashKeys.h"
+#include "nsIGlobalObject.h"
+#include "nsTHashtable.h"
+
+// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to
+// GetTickCount().
+#ifdef GetCurrentTime
+#undef GetCurrentTime
+#endif
+
+namespace mozilla {
+namespace dom {
+
+class Animation;
+
+class AnimationTimeline
+ : public nsISupports
+ , public nsWrapperCache
+{
+public:
+ explicit AnimationTimeline(nsIGlobalObject* aWindow)
+ : mWindow(aWindow)
+ {
+ MOZ_ASSERT(mWindow);
+ }
+
+protected:
+ virtual ~AnimationTimeline()
+ {
+ mAnimationOrder.clear();
+ }
+
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AnimationTimeline)
+
+ nsIGlobalObject* GetParentObject() const { return mWindow; }
+
+ // AnimationTimeline methods
+ virtual Nullable<TimeDuration> GetCurrentTime() const = 0;
+
+ // Wrapper functions for AnimationTimeline DOM methods when called from
+ // script.
+ Nullable<double> GetCurrentTimeAsDouble() const {
+ return AnimationUtils::TimeDurationToDouble(GetCurrentTime());
+ }
+
+ /**
+ * Returns true if the times returned by GetCurrentTime() are convertible
+ * to and from wallclock-based TimeStamp (e.g. from TimeStamp::Now()) values
+ * using ToTimelineTime() and ToTimeStamp().
+ *
+ * Typically this is true, but it will be false in the case when this
+ * timeline has no refresh driver or is tied to a refresh driver under test
+ * control.
+ */
+ virtual bool TracksWallclockTime() const = 0;
+
+ /**
+ * Converts a TimeStamp to the equivalent value in timeline time.
+ * Note that when TracksWallclockTime() is false, there is no correspondence
+ * between timeline time and wallclock time. In such a case, passing a
+ * timestamp from TimeStamp::Now() to this method will not return a
+ * meaningful result.
+ */
+ virtual Nullable<TimeDuration> ToTimelineTime(const TimeStamp&
+ aTimeStamp) const = 0;
+
+ virtual TimeStamp ToTimeStamp(const TimeDuration& aTimelineTime) const = 0;
+
+ /**
+ * Inform this timeline that |aAnimation| which is or was observing the
+ * timeline, has been updated. This serves as both the means to associate
+ * AND disassociate animations with a timeline. The timeline itself will
+ * determine if it needs to begin, continue or stop tracking this animation.
+ */
+ virtual void NotifyAnimationUpdated(Animation& aAnimation);
+
+ /**
+ * Returns true if any CSS animations, CSS transitions or Web animations are
+ * currently associated with this timeline. As soon as an animation is
+ * applied to an element it is associated with the timeline even if it has a
+ * delayed start, so this includes animations that may not be active for some
+ * time.
+ */
+ bool HasAnimations() const {
+ return !mAnimations.IsEmpty();
+ }
+
+ virtual void RemoveAnimation(Animation* aAnimation);
+
+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.
+ //
+ // The hashset keeps a strong reference to each animation since
+ // dealing with addref/release with LinkedList is difficult.
+ typedef nsTHashtable<nsRefPtrHashKey<dom::Animation>> AnimationSet;
+ AnimationSet mAnimations;
+ LinkedList<dom::Animation> mAnimationOrder;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_AnimationTimeline_h
diff --git a/dom/animation/AnimationUtils.cpp b/dom/animation/AnimationUtils.cpp
new file mode 100644
index 0000000000..476652f778
--- /dev/null
+++ b/dom/animation/AnimationUtils.cpp
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "AnimationUtils.h"
+
+#include "nsContentUtils.h" // For nsContentUtils::IsCallerChrome
+#include "nsDebug.h"
+#include "nsIAtom.h"
+#include "nsIContent.h"
+#include "nsIDocument.h"
+#include "nsGlobalWindow.h"
+#include "nsString.h"
+#include "xpcpublic.h" // For xpc::NativeGlobal
+#include "mozilla/Preferences.h"
+
+namespace mozilla {
+
+/* static */ void
+AnimationUtils::LogAsyncAnimationFailure(nsCString& aMessage,
+ const nsIContent* aContent)
+{
+ if (aContent) {
+ aMessage.AppendLiteral(" [");
+ aMessage.Append(nsAtomCString(aContent->NodeInfo()->NameAtom()));
+
+ nsIAtom* id = aContent->GetID();
+ if (id) {
+ aMessage.AppendLiteral(" with id '");
+ aMessage.Append(nsAtomCString(aContent->GetID()));
+ aMessage.Append('\'');
+ }
+ aMessage.Append(']');
+ }
+ aMessage.Append('\n');
+ printf_stderr("%s", aMessage.get());
+}
+
+/* static */ nsIDocument*
+AnimationUtils::GetCurrentRealmDocument(JSContext* aCx)
+{
+ nsGlobalWindow* win = xpc::CurrentWindowOrNull(aCx);
+ if (!win) {
+ return nullptr;
+ }
+ return win->GetDoc();
+}
+
+/* static */ bool
+AnimationUtils::IsOffscreenThrottlingEnabled()
+{
+ static bool sOffscreenThrottlingEnabled;
+ static bool sPrefCached = false;
+
+ if (!sPrefCached) {
+ sPrefCached = true;
+ Preferences::AddBoolVarCache(&sOffscreenThrottlingEnabled,
+ "dom.animations.offscreen-throttling");
+ }
+
+ return sOffscreenThrottlingEnabled;
+}
+
+/* static */ bool
+AnimationUtils::IsCoreAPIEnabledForCaller()
+{
+ static bool sCoreAPIEnabled;
+ static bool sPrefCached = false;
+
+ if (!sPrefCached) {
+ sPrefCached = true;
+ Preferences::AddBoolVarCache(&sCoreAPIEnabled,
+ "dom.animations-api.core.enabled");
+ }
+
+ return sCoreAPIEnabled || nsContentUtils::IsCallerChrome();
+}
+
+} // namespace mozilla
diff --git a/dom/animation/AnimationUtils.h b/dom/animation/AnimationUtils.h
new file mode 100644
index 0000000000..82ae69bc85
--- /dev/null
+++ b/dom/animation/AnimationUtils.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_dom_AnimationUtils_h
+#define mozilla_dom_AnimationUtils_h
+
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/Nullable.h"
+#include "nsStringFwd.h"
+
+class nsIContent;
+class nsIDocument;
+struct JSContext;
+
+namespace mozilla {
+
+class ComputedTimingFunction;
+
+class AnimationUtils
+{
+public:
+ static dom::Nullable<double>
+ TimeDurationToDouble(const dom::Nullable<TimeDuration>& aTime)
+ {
+ dom::Nullable<double> result;
+
+ if (!aTime.IsNull()) {
+ result.SetValue(aTime.Value().ToMilliseconds());
+ }
+
+ return result;
+ }
+
+ static dom::Nullable<TimeDuration>
+ DoubleToTimeDuration(const dom::Nullable<double>& aTime)
+ {
+ dom::Nullable<TimeDuration> result;
+
+ if (!aTime.IsNull()) {
+ result.SetValue(TimeDuration::FromMilliseconds(aTime.Value()));
+ }
+
+ return result;
+ }
+
+ static void LogAsyncAnimationFailure(nsCString& aMessage,
+ const nsIContent* aContent = nullptr);
+
+ /**
+ * Get the document from the JS context to use when parsing CSS properties.
+ */
+ static nsIDocument*
+ GetCurrentRealmDocument(JSContext* aCx);
+
+ /**
+ * Checks if offscreen animation throttling is enabled.
+ */
+ static bool
+ IsOffscreenThrottlingEnabled();
+
+ /**
+ * Returns true if the preference to enable the core Web Animations API is
+ * true or the caller is chrome.
+ */
+ static bool
+ IsCoreAPIEnabledForCaller();
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/animation/CSSPseudoElement.cpp b/dom/animation/CSSPseudoElement.cpp
new file mode 100644
index 0000000000..a4dede0b3b
--- /dev/null
+++ b/dom/animation/CSSPseudoElement.cpp
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "mozilla/dom/CSSPseudoElement.h"
+#include "mozilla/dom/CSSPseudoElementBinding.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/AnimationComparator.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CSSPseudoElement, mParentElement)
+
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CSSPseudoElement, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CSSPseudoElement, Release)
+
+CSSPseudoElement::CSSPseudoElement(Element* aElement,
+ CSSPseudoElementType aType)
+ : mParentElement(aElement)
+ , mPseudoType(aType)
+{
+ MOZ_ASSERT(aElement);
+ MOZ_ASSERT(aType == CSSPseudoElementType::after ||
+ aType == CSSPseudoElementType::before,
+ "Unexpected Pseudo Type");
+}
+
+CSSPseudoElement::~CSSPseudoElement()
+{
+ // Element might have been unlinked already, so we have to do null check.
+ if (mParentElement) {
+ mParentElement->DeleteProperty(
+ GetCSSPseudoElementPropertyAtom(mPseudoType));
+ }
+}
+
+ParentObject
+CSSPseudoElement::GetParentObject() const
+{
+ return mParentElement->GetParentObject();
+}
+
+JSObject*
+CSSPseudoElement::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return CSSPseudoElementBinding::Wrap(aCx, this, aGivenProto);
+}
+
+void
+CSSPseudoElement::GetAnimations(const AnimationFilter& filter,
+ nsTArray<RefPtr<Animation>>& aRetVal)
+{
+ nsIDocument* doc = mParentElement->GetComposedDoc();
+ if (doc) {
+ doc->FlushPendingNotifications(Flush_Style);
+ }
+
+ Element::GetAnimationsUnsorted(mParentElement, mPseudoType, aRetVal);
+ aRetVal.Sort(AnimationPtrComparator<RefPtr<Animation>>());
+}
+
+already_AddRefed<Animation>
+CSSPseudoElement::Animate(
+ JSContext* aContext,
+ JS::Handle<JSObject*> aKeyframes,
+ const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
+ ErrorResult& aError)
+{
+ Nullable<ElementOrCSSPseudoElement> target;
+ target.SetValue().SetAsCSSPseudoElement() = this;
+ return Element::Animate(target, aContext, aKeyframes, aOptions, aError);
+}
+
+/* static */ already_AddRefed<CSSPseudoElement>
+CSSPseudoElement::GetCSSPseudoElement(Element* aElement,
+ CSSPseudoElementType aType)
+{
+ if (!aElement) {
+ return nullptr;
+ }
+
+ nsIAtom* propName = CSSPseudoElement::GetCSSPseudoElementPropertyAtom(aType);
+ RefPtr<CSSPseudoElement> pseudo =
+ static_cast<CSSPseudoElement*>(aElement->GetProperty(propName));
+ if (pseudo) {
+ return pseudo.forget();
+ }
+
+ // CSSPseudoElement is a purely external interface created on-demand, and
+ // when all references from script to the pseudo are dropped, we can drop the
+ // CSSPseudoElement object, so use a non-owning reference from Element to
+ // CSSPseudoElement.
+ pseudo = new CSSPseudoElement(aElement, aType);
+ nsresult rv = aElement->SetProperty(propName, pseudo, nullptr, true);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SetProperty failed");
+ return nullptr;
+ }
+ return pseudo.forget();
+}
+
+/* static */ nsIAtom*
+CSSPseudoElement::GetCSSPseudoElementPropertyAtom(CSSPseudoElementType aType)
+{
+ switch (aType) {
+ case CSSPseudoElementType::before:
+ return nsGkAtoms::cssPseudoElementBeforeProperty;
+
+ case CSSPseudoElementType::after:
+ return nsGkAtoms::cssPseudoElementAfterProperty;
+
+ default:
+ NS_NOTREACHED("Should not try to get CSSPseudoElement "
+ "other than ::before or ::after");
+ return nullptr;
+ }
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/animation/CSSPseudoElement.h b/dom/animation/CSSPseudoElement.h
new file mode 100644
index 0000000000..00445cc60a
--- /dev/null
+++ b/dom/animation/CSSPseudoElement.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef mozilla_dom_CSSPseudoElement_h
+#define mozilla_dom_CSSPseudoElement_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/RefPtr.h"
+#include "nsCSSPseudoElements.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+namespace dom {
+
+class Animation;
+class Element;
+class UnrestrictedDoubleOrKeyframeAnimationOptions;
+
+class CSSPseudoElement final : public nsWrapperCache
+{
+public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(CSSPseudoElement)
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(CSSPseudoElement)
+
+protected:
+ virtual ~CSSPseudoElement();
+
+public:
+ ParentObject GetParentObject() const;
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ CSSPseudoElementType GetType() const { return mPseudoType; }
+ void GetType(nsString& aRetVal) const
+ {
+ MOZ_ASSERT(nsCSSPseudoElements::GetPseudoAtom(mPseudoType),
+ "All pseudo-types allowed by this class should have a"
+ " corresponding atom");
+ // Our atoms use one colon and we would like to return two colons syntax
+ // for the returned pseudo type string, so serialize this to the
+ // non-deprecated two colon syntax.
+ aRetVal.Assign(char16_t(':'));
+ aRetVal.Append(
+ nsDependentAtomString(nsCSSPseudoElements::GetPseudoAtom(mPseudoType)));
+ }
+ already_AddRefed<Element> ParentElement() const
+ {
+ RefPtr<Element> retVal(mParentElement);
+ return retVal.forget();
+ }
+
+ void GetAnimations(const AnimationFilter& filter,
+ nsTArray<RefPtr<Animation>>& aRetVal);
+ already_AddRefed<Animation>
+ Animate(JSContext* aContext,
+ JS::Handle<JSObject*> aKeyframes,
+ const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
+ ErrorResult& aError);
+
+ // Given an element:pseudoType pair, returns the CSSPseudoElement stored as a
+ // property on |aElement|. If there is no CSSPseudoElement for the specified
+ // pseudo-type on element, a new CSSPseudoElement will be created and stored
+ // on the element.
+ static already_AddRefed<CSSPseudoElement>
+ GetCSSPseudoElement(Element* aElement, CSSPseudoElementType aType);
+
+private:
+ // Only ::before and ::after are supported.
+ CSSPseudoElement(Element* aElement, CSSPseudoElementType aType);
+
+ static nsIAtom* GetCSSPseudoElementPropertyAtom(CSSPseudoElementType aType);
+
+ // mParentElement needs to be an owning reference since if script is holding
+ // on to the pseudo-element, it needs to continue to be able to refer to
+ // the parent element.
+ RefPtr<Element> mParentElement;
+ CSSPseudoElementType mPseudoType;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_CSSPseudoElement_h
diff --git a/dom/animation/ComputedTiming.h b/dom/animation/ComputedTiming.h
new file mode 100644
index 0000000000..4a98e39337
--- /dev/null
+++ b/dom/animation/ComputedTiming.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_ComputedTiming_h
+#define mozilla_ComputedTiming_h
+
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/StickyTimeDuration.h"
+#include "mozilla/ComputedTimingFunction.h"
+
+// X11 has a #define for None
+#ifdef None
+#undef None
+#endif
+#include "mozilla/dom/AnimationEffectReadOnlyBinding.h" // FillMode
+
+namespace mozilla {
+
+/**
+ * Stores the results of calculating the timing properties of an animation
+ * at a given sample time.
+ */
+struct ComputedTiming
+{
+ // The total duration of the animation including all iterations.
+ // Will equal StickyTimeDuration::Forever() if the animation repeats
+ // indefinitely.
+ StickyTimeDuration mActiveDuration;
+ // The effect end time in local time (i.e. an offset from the effect's
+ // start time). Will equal StickyTimeDuration::Forever() if the animation
+ // plays indefinitely.
+ StickyTimeDuration mEndTime;
+ // Progress towards the end of the current iteration. If the effect is
+ // being sampled backwards, this will go from 1.0 to 0.0.
+ // 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 ||
+ mFill == dom::FillMode::Forwards;
+ }
+ bool FillsBackwards() const {
+ MOZ_ASSERT(mFill != dom::FillMode::Auto,
+ "mFill should not be Auto in ComputedTiming.");
+ return mFill == dom::FillMode::Both ||
+ mFill == dom::FillMode::Backwards;
+ }
+
+ enum class AnimationPhase {
+ Null, // Not sampled (null sample time)
+ Before, // Sampled prior to the start of the active interval
+ Active, // Sampled within the active interval
+ After // Sampled after (or at) the end of the active interval
+ };
+ AnimationPhase mPhase = AnimationPhase::Null;
+
+ ComputedTimingFunction::BeforeFlag mBeforeFlag =
+ ComputedTimingFunction::BeforeFlag::Unset;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ComputedTiming_h
diff --git a/dom/animation/ComputedTimingFunction.cpp b/dom/animation/ComputedTimingFunction.cpp
new file mode 100644
index 0000000000..95b7fa7855
--- /dev/null
+++ b/dom/animation/ComputedTimingFunction.cpp
@@ -0,0 +1,194 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "ComputedTimingFunction.h"
+#include "nsAlgorithm.h" // For clamped()
+#include "nsStyleUtil.h"
+
+namespace mozilla {
+
+void
+ComputedTimingFunction::Init(const nsTimingFunction &aFunction)
+{
+ mType = aFunction.mType;
+ if (nsTimingFunction::IsSplineType(mType)) {
+ mTimingFunction.Init(aFunction.mFunc.mX1, aFunction.mFunc.mY1,
+ aFunction.mFunc.mX2, aFunction.mFunc.mY2);
+ } else {
+ mSteps = aFunction.mSteps;
+ }
+}
+
+static inline double
+StepTiming(uint32_t aSteps,
+ double aPortion,
+ ComputedTimingFunction::BeforeFlag aBeforeFlag,
+ nsTimingFunction::Type aType)
+{
+ MOZ_ASSERT(0.0 <= aPortion && aPortion <= 1.0, "out of range");
+ MOZ_ASSERT(aType == nsTimingFunction::Type::StepStart ||
+ aType == nsTimingFunction::Type::StepEnd, "invalid type");
+
+ if (aPortion == 1.0) {
+ return 1.0;
+ }
+
+ // Calculate current step using step-end behavior
+ uint32_t step = uint32_t(aPortion * aSteps); // floor
+
+ // step-start is one step ahead
+ if (aType == nsTimingFunction::Type::StepStart) {
+ step++;
+ }
+
+ // If the "before flag" is set and we are at a transition point,
+ // drop back a step (but only if we are not already at the zero point--
+ // we do this clamping here since |step| is an unsigned integer)
+ if (step != 0 &&
+ aBeforeFlag == ComputedTimingFunction::BeforeFlag::Set &&
+ fmod(aPortion * aSteps, 1) == 0) {
+ step--;
+ }
+
+ // Convert to a progress value
+ return double(step) / double(aSteps);
+}
+
+double
+ComputedTimingFunction::GetValue(
+ double aPortion,
+ ComputedTimingFunction::BeforeFlag aBeforeFlag) const
+{
+ if (HasSpline()) {
+ // Check for a linear curve.
+ // (GetSplineValue(), below, also checks this but doesn't work when
+ // aPortion is outside the range [0.0, 1.0]).
+ if (mTimingFunction.X1() == mTimingFunction.Y1() &&
+ mTimingFunction.X2() == mTimingFunction.Y2()) {
+ return aPortion;
+ }
+
+ // Ensure that we return 0 or 1 on both edges.
+ if (aPortion == 0.0) {
+ return 0.0;
+ }
+ if (aPortion == 1.0) {
+ return 1.0;
+ }
+
+ // For negative values, try to extrapolate with tangent (p1 - p0) or,
+ // if p1 is coincident with p0, with (p2 - p0).
+ if (aPortion < 0.0) {
+ if (mTimingFunction.X1() > 0.0) {
+ return aPortion * mTimingFunction.Y1() / mTimingFunction.X1();
+ } else if (mTimingFunction.Y1() == 0 && mTimingFunction.X2() > 0.0) {
+ return aPortion * mTimingFunction.Y2() / mTimingFunction.X2();
+ }
+ // If we can't calculate a sensible tangent, don't extrapolate at all.
+ return 0.0;
+ }
+
+ // For values greater than 1, try to extrapolate with tangent (p2 - p3) or,
+ // if p2 is coincident with p3, with (p1 - p3).
+ if (aPortion > 1.0) {
+ if (mTimingFunction.X2() < 1.0) {
+ return 1.0 + (aPortion - 1.0) *
+ (mTimingFunction.Y2() - 1) / (mTimingFunction.X2() - 1);
+ } else if (mTimingFunction.Y2() == 1 && mTimingFunction.X1() < 1.0) {
+ return 1.0 + (aPortion - 1.0) *
+ (mTimingFunction.Y1() - 1) / (mTimingFunction.X1() - 1);
+ }
+ // If we can't calculate a sensible tangent, don't extrapolate at all.
+ return 1.0;
+ }
+
+ return mTimingFunction.GetSplineValue(aPortion);
+ }
+
+ // Since we use endpoint-exclusive timing, the output of a steps(start) timing
+ // function when aPortion = 0.0 is the top of the first step. When aPortion is
+ // negative, however, we should use the bottom of the first step. We handle
+ // negative values of aPortion specially here since once we clamp aPortion
+ // to [0,1] below we will no longer be able to distinguish to the two cases.
+ if (aPortion < 0.0) {
+ return 0.0;
+ }
+
+ // Clamp in case of steps(end) and steps(start) for values greater than 1.
+ aPortion = clamped(aPortion, 0.0, 1.0);
+
+ return StepTiming(mSteps, aPortion, aBeforeFlag, mType);
+}
+
+int32_t
+ComputedTimingFunction::Compare(const ComputedTimingFunction& aRhs) const
+{
+ if (mType != aRhs.mType) {
+ return int32_t(mType) - int32_t(aRhs.mType);
+ }
+
+ if (mType == nsTimingFunction::Type::CubicBezier) {
+ int32_t order = mTimingFunction.Compare(aRhs.mTimingFunction);
+ if (order != 0) {
+ return order;
+ }
+ } else if (mType == nsTimingFunction::Type::StepStart ||
+ mType == nsTimingFunction::Type::StepEnd) {
+ if (mSteps != aRhs.mSteps) {
+ return int32_t(mSteps) - int32_t(aRhs.mSteps);
+ }
+ }
+
+ return 0;
+}
+
+void
+ComputedTimingFunction::AppendToString(nsAString& aResult) const
+{
+ switch (mType) {
+ case nsTimingFunction::Type::CubicBezier:
+ nsStyleUtil::AppendCubicBezierTimingFunction(mTimingFunction.X1(),
+ mTimingFunction.Y1(),
+ mTimingFunction.X2(),
+ mTimingFunction.Y2(),
+ aResult);
+ break;
+ case nsTimingFunction::Type::StepStart:
+ case nsTimingFunction::Type::StepEnd:
+ nsStyleUtil::AppendStepsTimingFunction(mType, mSteps, aResult);
+ break;
+ default:
+ nsStyleUtil::AppendCubicBezierKeywordTimingFunction(mType, aResult);
+ break;
+ }
+}
+
+/* static */ int32_t
+ComputedTimingFunction::Compare(const Maybe<ComputedTimingFunction>& aLhs,
+ const Maybe<ComputedTimingFunction>& aRhs)
+{
+ // We can't use |operator<| for const Maybe<>& here because
+ // 'ease' is prior to 'linear' which is represented by Nothing().
+ // So we have to convert Nothing() as 'linear' and check it first.
+ nsTimingFunction::Type lhsType = aLhs.isNothing() ?
+ nsTimingFunction::Type::Linear : aLhs->GetType();
+ nsTimingFunction::Type rhsType = aRhs.isNothing() ?
+ nsTimingFunction::Type::Linear : aRhs->GetType();
+
+ if (lhsType != rhsType) {
+ return int32_t(lhsType) - int32_t(rhsType);
+ }
+
+ // Both of them are Nothing().
+ if (lhsType == nsTimingFunction::Type::Linear) {
+ return 0;
+ }
+
+ // Other types.
+ return aLhs->Compare(aRhs.value());
+}
+
+} // namespace mozilla
diff --git a/dom/animation/ComputedTimingFunction.h b/dom/animation/ComputedTimingFunction.h
new file mode 100644
index 0000000000..9980972650
--- /dev/null
+++ b/dom/animation/ComputedTimingFunction.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_ComputedTimingFunction_h
+#define mozilla_ComputedTimingFunction_h
+
+#include "nsSMILKeySpline.h" // nsSMILKeySpline
+#include "nsStyleStruct.h" // nsTimingFunction
+
+namespace mozilla {
+
+class ComputedTimingFunction
+{
+public:
+ // BeforeFlag is used in step timing function.
+ // https://w3c.github.io/web-animations/#before-flag
+ enum class BeforeFlag {
+ Unset,
+ Set
+ };
+ void Init(const nsTimingFunction &aFunction);
+ double GetValue(double aPortion, BeforeFlag aBeforeFlag) const;
+ const nsSMILKeySpline* GetFunction() const
+ {
+ NS_ASSERTION(HasSpline(), "Type mismatch");
+ return &mTimingFunction;
+ }
+ nsTimingFunction::Type GetType() const { return mType; }
+ bool HasSpline() const { return nsTimingFunction::IsSplineType(mType); }
+ uint32_t GetSteps() const { return mSteps; }
+ bool operator==(const ComputedTimingFunction& aOther) const
+ {
+ return mType == aOther.mType &&
+ (HasSpline() ?
+ mTimingFunction == aOther.mTimingFunction :
+ mSteps == aOther.mSteps);
+ }
+ bool operator!=(const ComputedTimingFunction& aOther) const
+ {
+ return !(*this == aOther);
+ }
+ int32_t Compare(const ComputedTimingFunction& aRhs) const;
+ void AppendToString(nsAString& aResult) const;
+
+ static double GetPortion(const Maybe<ComputedTimingFunction>& aFunction,
+ double aPortion,
+ BeforeFlag aBeforeFlag)
+ {
+ return aFunction ? aFunction->GetValue(aPortion, aBeforeFlag) : aPortion;
+ }
+ static int32_t Compare(const Maybe<ComputedTimingFunction>& aLhs,
+ const Maybe<ComputedTimingFunction>& aRhs);
+
+private:
+ nsTimingFunction::Type mType;
+ nsSMILKeySpline mTimingFunction;
+ uint32_t mSteps;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ComputedTimingFunction_h
diff --git a/dom/animation/DocumentTimeline.cpp b/dom/animation/DocumentTimeline.cpp
new file mode 100644
index 0000000000..78a4877d25
--- /dev/null
+++ b/dom/animation/DocumentTimeline.cpp
@@ -0,0 +1,283 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "DocumentTimeline.h"
+#include "mozilla/dom/DocumentTimelineBinding.h"
+#include "AnimationUtils.h"
+#include "nsContentUtils.h"
+#include "nsDOMMutationObserver.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsIPresShell.h"
+#include "nsPresContext.h"
+#include "nsRefreshDriver.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentTimeline)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentTimeline,
+ AnimationTimeline)
+ tmp->UnregisterFromRefreshDriver();
+ if (tmp->isInList()) {
+ tmp->remove();
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentTimeline,
+ AnimationTimeline)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(DocumentTimeline,
+ AnimationTimeline)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(DocumentTimeline)
+NS_INTERFACE_MAP_END_INHERITING(AnimationTimeline)
+
+NS_IMPL_ADDREF_INHERITED(DocumentTimeline, AnimationTimeline)
+NS_IMPL_RELEASE_INHERITED(DocumentTimeline, AnimationTimeline)
+
+JSObject*
+DocumentTimeline::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return DocumentTimelineBinding::Wrap(aCx, this, aGivenProto);
+}
+
+/* static */ already_AddRefed<DocumentTimeline>
+DocumentTimeline::Constructor(const GlobalObject& aGlobal,
+ const DocumentTimelineOptions& aOptions,
+ ErrorResult& aRv)
+{
+ nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
+ if (!doc) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ TimeDuration originTime =
+ TimeDuration::FromMilliseconds(aOptions.mOriginTime);
+
+ if (originTime == TimeDuration::Forever() ||
+ originTime == -TimeDuration::Forever()) {
+ aRv.ThrowTypeError<dom::MSG_TIME_VALUE_OUT_OF_RANGE>(
+ NS_LITERAL_STRING("Origin time"));
+ return nullptr;
+ }
+ RefPtr<DocumentTimeline> timeline = new DocumentTimeline(doc, originTime);
+
+ return timeline.forget();
+}
+
+Nullable<TimeDuration>
+DocumentTimeline::GetCurrentTime() const
+{
+ return ToTimelineTime(GetCurrentTimeStamp());
+}
+
+TimeStamp
+DocumentTimeline::GetCurrentTimeStamp() const
+{
+ nsRefreshDriver* refreshDriver = GetRefreshDriver();
+ TimeStamp refreshTime = refreshDriver
+ ? refreshDriver->MostRecentRefresh()
+ : TimeStamp();
+
+ // Always return the same object to benefit from return-value optimization.
+ TimeStamp result = !refreshTime.IsNull()
+ ? refreshTime
+ : mLastRefreshDriverTime;
+
+ // If we don't have a refresh driver and we've never had one use the
+ // timeline's zero time.
+ if (result.IsNull()) {
+ RefPtr<nsDOMNavigationTiming> timing = mDocument->GetNavigationTiming();
+ if (timing) {
+ result = timing->GetNavigationStartTimeStamp();
+ // Also, let this time represent the current refresh time. This way
+ // we'll save it as the last refresh time and skip looking up
+ // navigation timing each time.
+ refreshTime = result;
+ }
+ }
+
+ if (!refreshTime.IsNull()) {
+ mLastRefreshDriverTime = refreshTime;
+ }
+
+ return result;
+}
+
+Nullable<TimeDuration>
+DocumentTimeline::ToTimelineTime(const TimeStamp& aTimeStamp) const
+{
+ Nullable<TimeDuration> result; // Initializes to null
+ if (aTimeStamp.IsNull()) {
+ return result;
+ }
+
+ RefPtr<nsDOMNavigationTiming> timing = mDocument->GetNavigationTiming();
+ if (MOZ_UNLIKELY(!timing)) {
+ return result;
+ }
+
+ result.SetValue(aTimeStamp
+ - timing->GetNavigationStartTimeStamp()
+ - mOriginTime);
+ return result;
+}
+
+void
+DocumentTimeline::NotifyAnimationUpdated(Animation& aAnimation)
+{
+ AnimationTimeline::NotifyAnimationUpdated(aAnimation);
+
+ if (!mIsObservingRefreshDriver) {
+ nsRefreshDriver* refreshDriver = GetRefreshDriver();
+ if (refreshDriver) {
+ MOZ_ASSERT(isInList(),
+ "We should not register with the refresh driver if we are not"
+ " in the document's list of timelines");
+ refreshDriver->AddRefreshObserver(this, Flush_Style);
+ mIsObservingRefreshDriver = true;
+ }
+ }
+}
+
+void
+DocumentTimeline::WillRefresh(mozilla::TimeStamp aTime)
+{
+ MOZ_ASSERT(mIsObservingRefreshDriver);
+ MOZ_ASSERT(GetRefreshDriver(),
+ "Should be able to reach refresh driver from within WillRefresh");
+
+ bool needsTicks = false;
+ nsTArray<Animation*> animationsToRemove(mAnimations.Count());
+
+ nsAutoAnimationMutationBatch mb(mDocument);
+
+ for (Animation* animation = mAnimationOrder.getFirst(); animation;
+ animation = animation->getNext()) {
+ // Skip any animations that are longer need associated with this timeline.
+ if (animation->GetTimeline() != this) {
+ // If animation has some other timeline, it better not be also in the
+ // animation list of this timeline object!
+ MOZ_ASSERT(!animation->GetTimeline());
+ animationsToRemove.AppendElement(animation);
+ continue;
+ }
+
+ needsTicks |= animation->NeedsTicks();
+ // Even if |animation| doesn't need future ticks, we should still
+ // Tick it this time around since it might just need a one-off tick in
+ // order to dispatch events.
+ animation->Tick();
+
+ if (!animation->IsRelevant() && !animation->NeedsTicks()) {
+ animationsToRemove.AppendElement(animation);
+ }
+ }
+
+ for (Animation* animation : animationsToRemove) {
+ RemoveAnimation(animation);
+ }
+
+ if (!needsTicks) {
+ // We already assert that GetRefreshDriver() is non-null at the beginning
+ // of this function but we check it again here to be sure that ticking
+ // animations does not have any side effects that cause us to lose the
+ // connection with the refresh driver, such as triggering the destruction
+ // of mDocument's PresShell.
+ MOZ_ASSERT(GetRefreshDriver(),
+ "Refresh driver should still be valid at end of WillRefresh");
+ UnregisterFromRefreshDriver();
+ }
+}
+
+void
+DocumentTimeline::NotifyRefreshDriverCreated(nsRefreshDriver* aDriver)
+{
+ MOZ_ASSERT(!mIsObservingRefreshDriver,
+ "Timeline should not be observing the refresh driver before"
+ " it is created");
+
+ if (!mAnimationOrder.isEmpty()) {
+ MOZ_ASSERT(isInList(),
+ "We should not register with the refresh driver if we are not"
+ " in the document's list of timelines");
+ aDriver->AddRefreshObserver(this, Flush_Style);
+ mIsObservingRefreshDriver = true;
+ }
+}
+
+void
+DocumentTimeline::NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver)
+{
+ if (!mIsObservingRefreshDriver) {
+ return;
+ }
+
+ aDriver->RemoveRefreshObserver(this, Flush_Style);
+ mIsObservingRefreshDriver = false;
+}
+
+void
+DocumentTimeline::RemoveAnimation(Animation* aAnimation)
+{
+ AnimationTimeline::RemoveAnimation(aAnimation);
+
+ if (mIsObservingRefreshDriver && mAnimations.IsEmpty()) {
+ UnregisterFromRefreshDriver();
+ }
+}
+
+TimeStamp
+DocumentTimeline::ToTimeStamp(const TimeDuration& aTimeDuration) const
+{
+ TimeStamp result;
+ RefPtr<nsDOMNavigationTiming> timing = mDocument->GetNavigationTiming();
+ if (MOZ_UNLIKELY(!timing)) {
+ return result;
+ }
+
+ result =
+ timing->GetNavigationStartTimeStamp() + (aTimeDuration + mOriginTime);
+ return result;
+}
+
+nsRefreshDriver*
+DocumentTimeline::GetRefreshDriver() const
+{
+ nsIPresShell* presShell = mDocument->GetShell();
+ if (MOZ_UNLIKELY(!presShell)) {
+ return nullptr;
+ }
+
+ nsPresContext* presContext = presShell->GetPresContext();
+ if (MOZ_UNLIKELY(!presContext)) {
+ return nullptr;
+ }
+
+ return presContext->RefreshDriver();
+}
+
+void
+DocumentTimeline::UnregisterFromRefreshDriver()
+{
+ if (!mIsObservingRefreshDriver) {
+ return;
+ }
+
+ nsRefreshDriver* refreshDriver = GetRefreshDriver();
+ if (!refreshDriver) {
+ return;
+ }
+
+ refreshDriver->RemoveRefreshObserver(this, Flush_Style);
+ mIsObservingRefreshDriver = false;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/animation/DocumentTimeline.h b/dom/animation/DocumentTimeline.h
new file mode 100644
index 0000000000..888a1d33d4
--- /dev/null
+++ b/dom/animation/DocumentTimeline.h
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_dom_DocumentTimeline_h
+#define mozilla_dom_DocumentTimeline_h
+
+#include "mozilla/dom/DocumentTimelineBinding.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/TimeStamp.h"
+#include "AnimationTimeline.h"
+#include "nsIDocument.h"
+#include "nsDOMNavigationTiming.h" // for DOMHighResTimeStamp
+#include "nsRefreshDriver.h"
+
+struct JSContext;
+
+// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to
+// GetTickCount().
+#ifdef GetCurrentTime
+#undef GetCurrentTime
+#endif
+
+namespace mozilla {
+namespace dom {
+
+class DocumentTimeline final
+ : public AnimationTimeline
+ , public nsARefreshObserver
+ , public LinkedListElement<DocumentTimeline>
+{
+public:
+ DocumentTimeline(nsIDocument* aDocument, const TimeDuration& aOriginTime)
+ : AnimationTimeline(aDocument->GetParentObject())
+ , mDocument(aDocument)
+ , mIsObservingRefreshDriver(false)
+ , mOriginTime(aOriginTime)
+ {
+ if (mDocument) {
+ mDocument->Timelines().insertBack(this);
+ }
+ }
+
+protected:
+ virtual ~DocumentTimeline()
+ {
+ MOZ_ASSERT(!mIsObservingRefreshDriver, "Timeline should have disassociated"
+ " from the refresh driver before being destroyed");
+ if (isInList()) {
+ remove();
+ }
+ }
+
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(DocumentTimeline,
+ AnimationTimeline)
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<DocumentTimeline>
+ Constructor(const GlobalObject& aGlobal,
+ const DocumentTimelineOptions& aOptions,
+ ErrorResult& aRv);
+
+ // AnimationTimeline methods
+ virtual Nullable<TimeDuration> GetCurrentTime() const override;
+
+ bool TracksWallclockTime() const override
+ {
+ nsRefreshDriver* refreshDriver = GetRefreshDriver();
+ return !refreshDriver ||
+ !refreshDriver->IsTestControllingRefreshesEnabled();
+ }
+ Nullable<TimeDuration> ToTimelineTime(const TimeStamp& aTimeStamp) const
+ override;
+ TimeStamp ToTimeStamp(const TimeDuration& aTimelineTime) const override;
+
+ void NotifyAnimationUpdated(Animation& aAnimation) override;
+
+ void RemoveAnimation(Animation* aAnimation) override;
+
+ // nsARefreshObserver methods
+ void WillRefresh(TimeStamp aTime) override;
+
+ void NotifyRefreshDriverCreated(nsRefreshDriver* aDriver);
+ void NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver);
+
+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
+ // we don't have a refresh driver (e.g. because we are in a display:none
+ // iframe).
+ mutable TimeStamp mLastRefreshDriverTime;
+ bool mIsObservingRefreshDriver;
+
+ TimeDuration mOriginTime;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_DocumentTimeline_h
diff --git a/dom/animation/EffectCompositor.cpp b/dom/animation/EffectCompositor.cpp
new file mode 100644
index 0000000000..c88cabe90b
--- /dev/null
+++ b/dom/animation/EffectCompositor.cpp
@@ -0,0 +1,920 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "EffectCompositor.h"
+
+#include "mozilla/dom/Animation.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/KeyframeEffectReadOnly.h"
+#include "mozilla/AnimationComparator.h"
+#include "mozilla/AnimationPerformanceWarning.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/EffectSet.h"
+#include "mozilla/LayerAnimationInfo.h"
+#include "mozilla/RestyleManagerHandle.h"
+#include "mozilla/RestyleManagerHandleInlines.h"
+#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetPresShellForContent
+#include "nsCSSPropertyIDSet.h"
+#include "nsCSSProps.h"
+#include "nsIPresShell.h"
+#include "nsLayoutUtils.h"
+#include "nsRuleNode.h" // For nsRuleNode::ComputePropertiesOverridingAnimation
+#include "nsRuleProcessorData.h" // For ElementRuleProcessorData etc.
+#include "nsTArray.h"
+#include <bitset>
+#include <initializer_list>
+
+using mozilla::dom::Animation;
+using mozilla::dom::Element;
+using mozilla::dom::KeyframeEffectReadOnly;
+
+namespace mozilla {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(EffectCompositor)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EffectCompositor)
+ for (auto& elementSet : tmp->mElementsToRestyle) {
+ elementSet.Clear();
+ }
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EffectCompositor)
+ for (auto& elementSet : tmp->mElementsToRestyle) {
+ for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) {
+ CycleCollectionNoteChild(cb, iter.Key().mElement,
+ "EffectCompositor::mElementsToRestyle[]",
+ cb.Flags());
+ }
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(EffectCompositor, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(EffectCompositor, Release)
+
+// Helper function to factor out the common logic from
+// GetAnimationsForCompositor and HasAnimationsForCompositor.
+//
+// Takes an optional array to fill with eligible animations.
+//
+// Returns true if there are eligible animations, false otherwise.
+bool
+FindAnimationsForCompositor(const nsIFrame* aFrame,
+ nsCSSPropertyID aProperty,
+ nsTArray<RefPtr<dom::Animation>>* aMatches /*out*/)
+{
+ MOZ_ASSERT(!aMatches || aMatches->IsEmpty(),
+ "Matches array, if provided, should be empty");
+
+ EffectSet* effects = EffectSet::GetEffectSet(aFrame);
+ if (!effects || effects->IsEmpty()) {
+ return false;
+ }
+
+ // If the property will be added to the animations level of the cascade but
+ // there is an !important rule for that property in the cascade then the
+ // animation will not be applied since the !important rule overrides it.
+ if (effects->PropertiesWithImportantRules().HasProperty(aProperty) &&
+ effects->PropertiesForAnimationsLevel().HasProperty(aProperty)) {
+ return false;
+ }
+
+ if (aFrame->RefusedAsyncAnimation()) {
+ return false;
+ }
+
+ // The animation cascade will almost always be up-to-date by this point
+ // but there are some cases such as when we are restoring the refresh driver
+ // from test control after seeking where it might not be the case.
+ //
+ // Those cases are probably not important but just to be safe, let's make
+ // sure the cascade is up to date since if it *is* up to date, this is
+ // basically a no-op.
+ Maybe<NonOwningAnimationTarget> pseudoElement =
+ EffectCompositor::GetAnimationElementAndPseudoForFrame(aFrame);
+ if (pseudoElement) {
+ EffectCompositor::MaybeUpdateCascadeResults(pseudoElement->mElement,
+ pseudoElement->mPseudoType,
+ aFrame->StyleContext());
+ }
+
+ if (!nsLayoutUtils::AreAsyncAnimationsEnabled()) {
+ if (nsLayoutUtils::IsAnimationLoggingEnabled()) {
+ nsCString message;
+ message.AppendLiteral("Performance warning: Async animations are "
+ "disabled");
+ AnimationUtils::LogAsyncAnimationFailure(message);
+ }
+ return false;
+ }
+
+ // Disable async animations if we have a rendering observer that
+ // depends on our content (svg masking, -moz-element etc) so that
+ // it gets updated correctly.
+ nsIContent* content = aFrame->GetContent();
+ while (content) {
+ if (content->HasRenderingObservers()) {
+ EffectCompositor::SetPerformanceWarning(
+ aFrame, aProperty,
+ AnimationPerformanceWarning(
+ AnimationPerformanceWarning::Type::HasRenderingObserver));
+ return false;
+ }
+ content = content->GetParent();
+ }
+
+ bool foundSome = false;
+ for (KeyframeEffectReadOnly* effect : *effects) {
+ MOZ_ASSERT(effect && effect->GetAnimation());
+ Animation* animation = effect->GetAnimation();
+
+ if (!animation->IsPlayableOnCompositor()) {
+ continue;
+ }
+
+ AnimationPerformanceWarning::Type warningType;
+ if (aProperty == eCSSProperty_transform &&
+ effect->ShouldBlockAsyncTransformAnimations(aFrame,
+ warningType)) {
+ if (aMatches) {
+ aMatches->Clear();
+ }
+ EffectCompositor::SetPerformanceWarning(
+ aFrame, aProperty,
+ AnimationPerformanceWarning(warningType));
+ return false;
+ }
+
+ if (!effect->HasEffectiveAnimationOfProperty(aProperty)) {
+ continue;
+ }
+
+ if (aMatches) {
+ aMatches->AppendElement(animation);
+ }
+ foundSome = true;
+ }
+
+ MOZ_ASSERT(!foundSome || !aMatches || !aMatches->IsEmpty(),
+ "If return value is true, matches array should be non-empty");
+
+ if (aMatches && foundSome) {
+ aMatches->Sort(AnimationPtrComparator<RefPtr<dom::Animation>>());
+ }
+ return foundSome;
+}
+
+void
+EffectCompositor::RequestRestyle(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ RestyleType aRestyleType,
+ CascadeLevel aCascadeLevel)
+{
+ if (!mPresContext) {
+ // Pres context will be null after the effect compositor is disconnected.
+ return;
+ }
+
+ // Ignore animations on orphaned elements.
+ if (!aElement->IsInComposedDoc()) {
+ return;
+ }
+
+ auto& elementsToRestyle = mElementsToRestyle[aCascadeLevel];
+ PseudoElementHashEntry::KeyType key = { aElement, aPseudoType };
+
+ if (aRestyleType == RestyleType::Throttled) {
+ if (!elementsToRestyle.Contains(key)) {
+ elementsToRestyle.Put(key, false);
+ }
+ mPresContext->Document()->SetNeedStyleFlush();
+ } else {
+ // Get() returns 0 if the element is not found. It will also return
+ // false if the element is found but does not have a pending restyle.
+ bool hasPendingRestyle = elementsToRestyle.Get(key);
+ if (!hasPendingRestyle) {
+ PostRestyleForAnimation(aElement, aPseudoType, aCascadeLevel);
+ }
+ elementsToRestyle.Put(key, true);
+ }
+
+ if (aRestyleType == RestyleType::Layer) {
+ // Prompt layers to re-sync their animations.
+ MOZ_ASSERT(mPresContext->RestyleManager()->IsGecko(),
+ "stylo: Servo-backed style system should not be using "
+ "EffectCompositor");
+ mPresContext->RestyleManager()->AsGecko()->IncrementAnimationGeneration();
+ EffectSet* effectSet =
+ EffectSet::GetEffectSet(aElement, aPseudoType);
+ if (effectSet) {
+ effectSet->UpdateAnimationGeneration(mPresContext);
+ }
+ }
+}
+
+void
+EffectCompositor::PostRestyleForAnimation(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ CascadeLevel aCascadeLevel)
+{
+ if (!mPresContext) {
+ return;
+ }
+
+ dom::Element* element = GetElementToRestyle(aElement, aPseudoType);
+ if (!element) {
+ return;
+ }
+
+ nsRestyleHint hint = aCascadeLevel == CascadeLevel::Transitions ?
+ eRestyle_CSSTransitions :
+ eRestyle_CSSAnimations;
+ mPresContext->PresShell()->RestyleForAnimation(element, hint);
+}
+
+void
+EffectCompositor::PostRestyleForThrottledAnimations()
+{
+ for (size_t i = 0; i < kCascadeLevelCount; i++) {
+ CascadeLevel cascadeLevel = CascadeLevel(i);
+ auto& elementSet = mElementsToRestyle[cascadeLevel];
+
+ for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) {
+ bool& postedRestyle = iter.Data();
+ if (postedRestyle) {
+ continue;
+ }
+
+ PostRestyleForAnimation(iter.Key().mElement,
+ iter.Key().mPseudoType,
+ cascadeLevel);
+ postedRestyle = true;
+ }
+ }
+}
+
+void
+EffectCompositor::UpdateEffectProperties(nsStyleContext* aStyleContext,
+ dom::Element* aElement,
+ CSSPseudoElementType aPseudoType)
+{
+ EffectSet* effectSet = EffectSet::GetEffectSet(aElement, aPseudoType);
+ if (!effectSet) {
+ return;
+ }
+
+ // Style context change might cause CSS cascade level,
+ // e.g removing !important, so we should update the cascading result.
+ effectSet->MarkCascadeNeedsUpdate();
+
+ for (KeyframeEffectReadOnly* effect : *effectSet) {
+ effect->UpdateProperties(aStyleContext);
+ }
+}
+
+void
+EffectCompositor::MaybeUpdateAnimationRule(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ CascadeLevel aCascadeLevel,
+ nsStyleContext* aStyleContext)
+{
+ // First update cascade results since that may cause some elements to
+ // be marked as needing a restyle.
+ MaybeUpdateCascadeResults(aElement, aPseudoType, aStyleContext);
+
+ auto& elementsToRestyle = mElementsToRestyle[aCascadeLevel];
+ PseudoElementHashEntry::KeyType key = { aElement, aPseudoType };
+
+ if (!mPresContext || !elementsToRestyle.Contains(key)) {
+ return;
+ }
+
+ ComposeAnimationRule(aElement, aPseudoType, aCascadeLevel,
+ mPresContext->RefreshDriver()->MostRecentRefresh());
+
+ elementsToRestyle.Remove(key);
+}
+
+nsIStyleRule*
+EffectCompositor::GetAnimationRule(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ CascadeLevel aCascadeLevel,
+ nsStyleContext* aStyleContext)
+{
+ // NOTE: We need to be careful about early returns in this method where
+ // we *don't* update mElementsToRestyle. When we get a call to
+ // RequestRestyle that results in a call to PostRestyleForAnimation, we
+ // will set a bool flag in mElementsToRestyle indicating that we've
+ // called PostRestyleForAnimation so we don't need to call it again
+ // until that restyle happens. During that restyle, if we arrive here
+ // and *don't* update mElementsToRestyle we'll continue to skip calling
+ // PostRestyleForAnimation from RequestRestyle.
+
+ if (!mPresContext || !mPresContext->IsDynamic()) {
+ // For print or print preview, ignore animations.
+ return nullptr;
+ }
+
+ MOZ_ASSERT(mPresContext->RestyleManager()->IsGecko(),
+ "stylo: Servo-backed style system should not be using "
+ "EffectCompositor");
+ if (mPresContext->RestyleManager()->AsGecko()->SkipAnimationRules()) {
+ // We don't need to worry about updating mElementsToRestyle in this case
+ // since this is not the animation restyle we requested when we called
+ // PostRestyleForAnimation (see comment at start of this method).
+ return nullptr;
+ }
+
+ MaybeUpdateAnimationRule(aElement, aPseudoType, aCascadeLevel, aStyleContext);
+
+#ifdef DEBUG
+ {
+ auto& elementsToRestyle = mElementsToRestyle[aCascadeLevel];
+ PseudoElementHashEntry::KeyType key = { aElement, aPseudoType };
+ MOZ_ASSERT(!elementsToRestyle.Contains(key),
+ "Element should no longer require a restyle after its "
+ "animation rule has been updated");
+ }
+#endif
+
+ EffectSet* effectSet = EffectSet::GetEffectSet(aElement, aPseudoType);
+ if (!effectSet) {
+ return nullptr;
+ }
+
+ return effectSet->AnimationRule(aCascadeLevel);
+}
+
+/* static */ dom::Element*
+EffectCompositor::GetElementToRestyle(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType)
+{
+ if (aPseudoType == CSSPseudoElementType::NotPseudo) {
+ return aElement;
+ }
+
+ nsIFrame* primaryFrame = aElement->GetPrimaryFrame();
+ if (!primaryFrame) {
+ return nullptr;
+ }
+ nsIFrame* pseudoFrame;
+ if (aPseudoType == CSSPseudoElementType::before) {
+ pseudoFrame = nsLayoutUtils::GetBeforeFrame(primaryFrame);
+ } else if (aPseudoType == CSSPseudoElementType::after) {
+ pseudoFrame = nsLayoutUtils::GetAfterFrame(primaryFrame);
+ } else {
+ NS_NOTREACHED("Should not try to get the element to restyle for a pseudo "
+ "other that :before or :after");
+ return nullptr;
+ }
+ if (!pseudoFrame) {
+ return nullptr;
+ }
+ return pseudoFrame->GetContent()->AsElement();
+}
+
+bool
+EffectCompositor::HasPendingStyleUpdates() const
+{
+ for (auto& elementSet : mElementsToRestyle) {
+ if (elementSet.Count()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool
+EffectCompositor::HasThrottledStyleUpdates() const
+{
+ for (auto& elementSet : mElementsToRestyle) {
+ for (auto iter = elementSet.ConstIter(); !iter.Done(); iter.Next()) {
+ if (!iter.Data()) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+void
+EffectCompositor::AddStyleUpdatesTo(RestyleTracker& aTracker)
+{
+ if (!mPresContext) {
+ return;
+ }
+
+ for (size_t i = 0; i < kCascadeLevelCount; i++) {
+ CascadeLevel cascadeLevel = CascadeLevel(i);
+ auto& elementSet = mElementsToRestyle[cascadeLevel];
+
+ // Copy the list of elements to restyle to a separate array that we can
+ // iterate over. This is because we need to call MaybeUpdateCascadeResults
+ // on each element, but doing that can mutate elementSet. In this case
+ // it will only mutate the bool value associated with each element in the
+ // set but even doing that will cause assertions in PLDHashTable to fail
+ // if we are iterating over the hashtable at the same time.
+ nsTArray<PseudoElementHashEntry::KeyType> elementsToRestyle(
+ elementSet.Count());
+ for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) {
+ // Skip animations on elements that have been orphaned since they
+ // requested a restyle.
+ if (iter.Key().mElement->IsInComposedDoc()) {
+ elementsToRestyle.AppendElement(iter.Key());
+ }
+ }
+
+ for (auto& pseudoElem : elementsToRestyle) {
+ MaybeUpdateCascadeResults(pseudoElem.mElement,
+ pseudoElem.mPseudoType,
+ nullptr);
+
+ ComposeAnimationRule(pseudoElem.mElement,
+ pseudoElem.mPseudoType,
+ cascadeLevel,
+ mPresContext->RefreshDriver()->MostRecentRefresh());
+
+ dom::Element* elementToRestyle =
+ GetElementToRestyle(pseudoElem.mElement, pseudoElem.mPseudoType);
+ if (elementToRestyle) {
+ nsRestyleHint rshint = cascadeLevel == CascadeLevel::Transitions ?
+ eRestyle_CSSTransitions :
+ eRestyle_CSSAnimations;
+ aTracker.AddPendingRestyle(elementToRestyle, rshint, nsChangeHint(0));
+ }
+ }
+
+ elementSet.Clear();
+ // Note: mElement pointers in elementsToRestyle might now dangle
+ }
+}
+
+/* static */ bool
+EffectCompositor::HasAnimationsForCompositor(const nsIFrame* aFrame,
+ nsCSSPropertyID aProperty)
+{
+ return FindAnimationsForCompositor(aFrame, aProperty, nullptr);
+}
+
+/* static */ nsTArray<RefPtr<dom::Animation>>
+EffectCompositor::GetAnimationsForCompositor(const nsIFrame* aFrame,
+ nsCSSPropertyID aProperty)
+{
+ nsTArray<RefPtr<dom::Animation>> result;
+
+#ifdef DEBUG
+ bool foundSome =
+#endif
+ FindAnimationsForCompositor(aFrame, aProperty, &result);
+ MOZ_ASSERT(!foundSome || !result.IsEmpty(),
+ "If return value is true, matches array should be non-empty");
+
+ return result;
+}
+
+/* static */ void
+EffectCompositor::ClearIsRunningOnCompositor(const nsIFrame *aFrame,
+ nsCSSPropertyID aProperty)
+{
+ EffectSet* effects = EffectSet::GetEffectSet(aFrame);
+ if (!effects) {
+ return;
+ }
+
+ for (KeyframeEffectReadOnly* effect : *effects) {
+ effect->SetIsRunningOnCompositor(aProperty, false);
+ }
+}
+
+/* static */ void
+EffectCompositor::MaybeUpdateCascadeResults(Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ nsStyleContext* aStyleContext)
+{
+ EffectSet* effects = EffectSet::GetEffectSet(aElement, aPseudoType);
+ if (!effects || !effects->CascadeNeedsUpdate()) {
+ return;
+ }
+
+ nsStyleContext* styleContext = aStyleContext;
+ if (!styleContext) {
+ dom::Element* elementToRestyle = GetElementToRestyle(aElement, aPseudoType);
+ if (elementToRestyle) {
+ nsIFrame* frame = elementToRestyle->GetPrimaryFrame();
+ if (frame) {
+ styleContext = frame->StyleContext();
+ }
+ }
+ }
+ UpdateCascadeResults(*effects, aElement, aPseudoType, styleContext);
+
+ MOZ_ASSERT(!effects->CascadeNeedsUpdate(), "Failed to update cascade state");
+}
+
+namespace {
+ class EffectCompositeOrderComparator {
+ public:
+ bool Equals(const KeyframeEffectReadOnly* a,
+ const KeyframeEffectReadOnly* b) const
+ {
+ return a == b;
+ }
+
+ bool LessThan(const KeyframeEffectReadOnly* a,
+ const KeyframeEffectReadOnly* b) const
+ {
+ MOZ_ASSERT(a->GetAnimation() && b->GetAnimation());
+ MOZ_ASSERT(
+ Equals(a, b) ||
+ a->GetAnimation()->HasLowerCompositeOrderThan(*b->GetAnimation()) !=
+ b->GetAnimation()->HasLowerCompositeOrderThan(*a->GetAnimation()));
+ return a->GetAnimation()->HasLowerCompositeOrderThan(*b->GetAnimation());
+ }
+ };
+}
+
+/* static */ void
+EffectCompositor::UpdateCascadeResults(Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ nsStyleContext* aStyleContext)
+{
+ EffectSet* effects = EffectSet::GetEffectSet(aElement, aPseudoType);
+ if (!effects) {
+ return;
+ }
+
+ UpdateCascadeResults(*effects, aElement, aPseudoType, aStyleContext);
+}
+
+/* static */ Maybe<NonOwningAnimationTarget>
+EffectCompositor::GetAnimationElementAndPseudoForFrame(const nsIFrame* aFrame)
+{
+ // Always return the same object to benefit from return-value optimization.
+ Maybe<NonOwningAnimationTarget> result;
+
+ CSSPseudoElementType pseudoType =
+ aFrame->StyleContext()->GetPseudoType();
+
+ if (pseudoType != CSSPseudoElementType::NotPseudo &&
+ pseudoType != CSSPseudoElementType::before &&
+ pseudoType != CSSPseudoElementType::after) {
+ return result;
+ }
+
+ nsIContent* content = aFrame->GetContent();
+ if (!content) {
+ return result;
+ }
+
+ if (pseudoType == CSSPseudoElementType::before ||
+ pseudoType == CSSPseudoElementType::after) {
+ content = content->GetParent();
+ if (!content) {
+ return result;
+ }
+ }
+
+ if (!content->IsElement()) {
+ return result;
+ }
+
+ result.emplace(content->AsElement(), pseudoType);
+
+ return result;
+}
+
+/* static */ void
+EffectCompositor::ComposeAnimationRule(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ CascadeLevel aCascadeLevel,
+ TimeStamp aRefreshTime)
+{
+ EffectSet* effects = EffectSet::GetEffectSet(aElement, aPseudoType);
+ if (!effects) {
+ return;
+ }
+
+ // The caller is responsible for calling MaybeUpdateCascadeResults first.
+ MOZ_ASSERT(!effects->CascadeNeedsUpdate(),
+ "Animation cascade out of date when composing animation rule");
+
+ // Get a list of effects sorted by composite order.
+ nsTArray<KeyframeEffectReadOnly*> sortedEffectList(effects->Count());
+ for (KeyframeEffectReadOnly* effect : *effects) {
+ sortedEffectList.AppendElement(effect);
+ }
+ sortedEffectList.Sort(EffectCompositeOrderComparator());
+
+ RefPtr<AnimValuesStyleRule>& animationRule =
+ effects->AnimationRule(aCascadeLevel);
+ animationRule = nullptr;
+
+ // If multiple animations affect the same property, animations with higher
+ // composite order (priority) override or add or animations with lower
+ // priority except properties in propertiesToSkip.
+ const nsCSSPropertyIDSet& propertiesToSkip =
+ aCascadeLevel == CascadeLevel::Animations
+ ? effects->PropertiesForAnimationsLevel().Invert()
+ : effects->PropertiesForAnimationsLevel();
+ for (KeyframeEffectReadOnly* effect : sortedEffectList) {
+ effect->GetAnimation()->ComposeStyle(animationRule, propertiesToSkip);
+ }
+
+ MOZ_ASSERT(effects == EffectSet::GetEffectSet(aElement, aPseudoType),
+ "EffectSet should not change while composing style");
+
+ effects->UpdateAnimationRuleRefreshTime(aCascadeLevel, aRefreshTime);
+}
+
+/* static */ void
+EffectCompositor::GetOverriddenProperties(nsStyleContext* aStyleContext,
+ EffectSet& aEffectSet,
+ nsCSSPropertyIDSet&
+ aPropertiesOverridden)
+{
+ AutoTArray<nsCSSPropertyID, LayerAnimationInfo::kRecords> propertiesToTrack;
+ {
+ nsCSSPropertyIDSet propertiesToTrackAsSet;
+ for (KeyframeEffectReadOnly* effect : aEffectSet) {
+ for (const AnimationProperty& property : effect->Properties()) {
+ if (nsCSSProps::PropHasFlags(property.mProperty,
+ CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR) &&
+ !propertiesToTrackAsSet.HasProperty(property.mProperty)) {
+ propertiesToTrackAsSet.AddProperty(property.mProperty);
+ propertiesToTrack.AppendElement(property.mProperty);
+ }
+ }
+ // Skip iterating over the rest of the effects if we've already
+ // found all the compositor-animatable properties.
+ if (propertiesToTrack.Length() == LayerAnimationInfo::kRecords) {
+ break;
+ }
+ }
+ }
+
+ if (propertiesToTrack.IsEmpty()) {
+ return;
+ }
+
+ nsRuleNode::ComputePropertiesOverridingAnimation(propertiesToTrack,
+ aStyleContext,
+ aPropertiesOverridden);
+}
+
+/* static */ void
+EffectCompositor::UpdateCascadeResults(EffectSet& aEffectSet,
+ Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ nsStyleContext* aStyleContext)
+{
+ MOZ_ASSERT(EffectSet::GetEffectSet(aElement, aPseudoType) == &aEffectSet,
+ "Effect set should correspond to the specified (pseudo-)element");
+ if (aEffectSet.IsEmpty()) {
+ aEffectSet.MarkCascadeUpdated();
+ return;
+ }
+
+ // Get a list of effects sorted by composite order.
+ nsTArray<KeyframeEffectReadOnly*> sortedEffectList(aEffectSet.Count());
+ for (KeyframeEffectReadOnly* effect : aEffectSet) {
+ sortedEffectList.AppendElement(effect);
+ }
+ sortedEffectList.Sort(EffectCompositeOrderComparator());
+
+ // Get properties that override the *animations* level of the cascade.
+ //
+ // We only do this for properties that we can animate on the compositor
+ // since we will apply other properties on the main thread where the usual
+ // cascade applies.
+ nsCSSPropertyIDSet overriddenProperties;
+ if (aStyleContext) {
+ GetOverriddenProperties(aStyleContext, aEffectSet, overriddenProperties);
+ }
+
+ // Returns a bitset the represents which properties from
+ // LayerAnimationInfo::sRecords are present in |aPropertySet|.
+ auto compositorPropertiesInSet =
+ [](nsCSSPropertyIDSet& aPropertySet) ->
+ std::bitset<LayerAnimationInfo::kRecords> {
+ std::bitset<LayerAnimationInfo::kRecords> result;
+ for (size_t i = 0; i < LayerAnimationInfo::kRecords; i++) {
+ if (aPropertySet.HasProperty(
+ LayerAnimationInfo::sRecords[i].mProperty)) {
+ result.set(i);
+ }
+ }
+ return result;
+ };
+
+ nsCSSPropertyIDSet& propertiesWithImportantRules =
+ aEffectSet.PropertiesWithImportantRules();
+ nsCSSPropertyIDSet& propertiesForAnimationsLevel =
+ aEffectSet.PropertiesForAnimationsLevel();
+
+ // Record which compositor-animatable properties were originally set so we can
+ // compare for changes later.
+ std::bitset<LayerAnimationInfo::kRecords>
+ prevCompositorPropertiesWithImportantRules =
+ compositorPropertiesInSet(propertiesWithImportantRules);
+ std::bitset<LayerAnimationInfo::kRecords>
+ prevCompositorPropertiesForAnimationsLevel =
+ compositorPropertiesInSet(propertiesForAnimationsLevel);
+
+ propertiesWithImportantRules.Empty();
+ propertiesForAnimationsLevel.Empty();
+
+ bool hasCompositorPropertiesForTransition = false;
+
+ for (const KeyframeEffectReadOnly* effect : sortedEffectList) {
+ MOZ_ASSERT(effect->GetAnimation(),
+ "Effects on a target element should have an Animation");
+ CascadeLevel cascadeLevel = effect->GetAnimation()->CascadeLevel();
+
+ for (const AnimationProperty& prop : effect->Properties()) {
+ if (overriddenProperties.HasProperty(prop.mProperty)) {
+ propertiesWithImportantRules.AddProperty(prop.mProperty);
+ }
+ if (cascadeLevel == EffectCompositor::CascadeLevel::Animations) {
+ propertiesForAnimationsLevel.AddProperty(prop.mProperty);
+ }
+
+ if (nsCSSProps::PropHasFlags(prop.mProperty,
+ CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR) &&
+ cascadeLevel == EffectCompositor::CascadeLevel::Transitions) {
+ hasCompositorPropertiesForTransition = true;
+ }
+ }
+ }
+
+ aEffectSet.MarkCascadeUpdated();
+
+ nsPresContext* presContext = GetPresContext(aElement);
+ if (!presContext) {
+ return;
+ }
+
+ // If properties for compositor are newly overridden by !important rules, or
+ // released from being overridden by !important rules, we need to update
+ // layers for animations level because it's a trigger to send animations to
+ // the compositor or pull animations back from the compositor.
+ if (prevCompositorPropertiesWithImportantRules !=
+ compositorPropertiesInSet(propertiesWithImportantRules)) {
+ presContext->EffectCompositor()->
+ RequestRestyle(aElement, aPseudoType,
+ EffectCompositor::RestyleType::Layer,
+ EffectCompositor::CascadeLevel::Animations);
+ }
+ // If we have transition properties for compositor and if the same propery
+ // for animations level is newly added or removed, we need to update layers
+ // for transitions level because composite order has been changed now.
+ if (hasCompositorPropertiesForTransition &&
+ prevCompositorPropertiesForAnimationsLevel !=
+ compositorPropertiesInSet(propertiesForAnimationsLevel)) {
+ presContext->EffectCompositor()->
+ RequestRestyle(aElement, aPseudoType,
+ EffectCompositor::RestyleType::Layer,
+ EffectCompositor::CascadeLevel::Transitions);
+ }
+}
+
+/* static */ nsPresContext*
+EffectCompositor::GetPresContext(Element* aElement)
+{
+ MOZ_ASSERT(aElement);
+ nsIPresShell* shell = nsComputedDOMStyle::GetPresShellForContent(aElement);
+ if (!shell) {
+ return nullptr;
+ }
+ return shell->GetPresContext();
+}
+
+/* static */ void
+EffectCompositor::SetPerformanceWarning(
+ const nsIFrame *aFrame,
+ nsCSSPropertyID aProperty,
+ const AnimationPerformanceWarning& aWarning)
+{
+ EffectSet* effects = EffectSet::GetEffectSet(aFrame);
+ if (!effects) {
+ return;
+ }
+
+ for (KeyframeEffectReadOnly* effect : *effects) {
+ effect->SetPerformanceWarning(aProperty, aWarning);
+ }
+}
+
+// ---------------------------------------------------------
+//
+// Nested class: AnimationStyleRuleProcessor
+//
+// ---------------------------------------------------------
+
+NS_IMPL_ISUPPORTS(EffectCompositor::AnimationStyleRuleProcessor,
+ nsIStyleRuleProcessor)
+
+nsRestyleHint
+EffectCompositor::AnimationStyleRuleProcessor::HasStateDependentStyle(
+ StateRuleProcessorData* aData)
+{
+ return nsRestyleHint(0);
+}
+
+nsRestyleHint
+EffectCompositor::AnimationStyleRuleProcessor::HasStateDependentStyle(
+ PseudoElementStateRuleProcessorData* aData)
+{
+ return nsRestyleHint(0);
+}
+
+bool
+EffectCompositor::AnimationStyleRuleProcessor::HasDocumentStateDependentStyle(
+ StateRuleProcessorData* aData)
+{
+ return false;
+}
+
+nsRestyleHint
+EffectCompositor::AnimationStyleRuleProcessor::HasAttributeDependentStyle(
+ AttributeRuleProcessorData* aData,
+ RestyleHintData& aRestyleHintDataResult)
+{
+ return nsRestyleHint(0);
+}
+
+bool
+EffectCompositor::AnimationStyleRuleProcessor::MediumFeaturesChanged(
+ nsPresContext* aPresContext)
+{
+ return false;
+}
+
+void
+EffectCompositor::AnimationStyleRuleProcessor::RulesMatching(
+ ElementRuleProcessorData* aData)
+{
+ nsIStyleRule *rule =
+ mCompositor->GetAnimationRule(aData->mElement,
+ CSSPseudoElementType::NotPseudo,
+ mCascadeLevel,
+ nullptr);
+ if (rule) {
+ aData->mRuleWalker->Forward(rule);
+ aData->mRuleWalker->CurrentNode()->SetIsAnimationRule();
+ }
+}
+
+void
+EffectCompositor::AnimationStyleRuleProcessor::RulesMatching(
+ PseudoElementRuleProcessorData* aData)
+{
+ if (aData->mPseudoType != CSSPseudoElementType::before &&
+ aData->mPseudoType != CSSPseudoElementType::after) {
+ return;
+ }
+
+ nsIStyleRule *rule =
+ mCompositor->GetAnimationRule(aData->mElement,
+ aData->mPseudoType,
+ mCascadeLevel,
+ nullptr);
+ if (rule) {
+ aData->mRuleWalker->Forward(rule);
+ aData->mRuleWalker->CurrentNode()->SetIsAnimationRule();
+ }
+}
+
+void
+EffectCompositor::AnimationStyleRuleProcessor::RulesMatching(
+ AnonBoxRuleProcessorData* aData)
+{
+}
+
+#ifdef MOZ_XUL
+void
+EffectCompositor::AnimationStyleRuleProcessor::RulesMatching(
+ XULTreeRuleProcessorData* aData)
+{
+}
+#endif
+
+size_t
+EffectCompositor::AnimationStyleRuleProcessor::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const
+{
+ return 0;
+}
+
+size_t
+EffectCompositor::AnimationStyleRuleProcessor::SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const
+{
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+} // namespace mozilla
diff --git a/dom/animation/EffectCompositor.h b/dom/animation/EffectCompositor.h
new file mode 100644
index 0000000000..732fbb3336
--- /dev/null
+++ b/dom/animation/EffectCompositor.h
@@ -0,0 +1,307 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_EffectCompositor_h
+#define mozilla_EffectCompositor_h
+
+#include "mozilla/EnumeratedArray.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/PseudoElementHashEntry.h"
+#include "mozilla/RefPtr.h"
+#include "nsCSSPropertyID.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsDataHashtable.h"
+#include "nsIStyleRuleProcessor.h"
+#include "nsTArray.h"
+
+class nsCSSPropertyIDSet;
+class nsIFrame;
+class nsIStyleRule;
+class nsPresContext;
+class nsStyleContext;
+
+namespace mozilla {
+
+class EffectSet;
+class RestyleTracker;
+struct AnimationPerformanceWarning;
+struct NonOwningAnimationTarget;
+
+namespace dom {
+class Animation;
+class Element;
+}
+
+class EffectCompositor
+{
+public:
+ explicit EffectCompositor(nsPresContext* aPresContext)
+ : mPresContext(aPresContext)
+ {
+ for (size_t i = 0; i < kCascadeLevelCount; i++) {
+ CascadeLevel cascadeLevel = CascadeLevel(i);
+ mRuleProcessors[cascadeLevel] =
+ new AnimationStyleRuleProcessor(this, cascadeLevel);
+ }
+ }
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(EffectCompositor)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(EffectCompositor)
+
+ void Disconnect() {
+ mPresContext = nullptr;
+ }
+
+ // Animations can be applied at two different levels in the CSS cascade:
+ enum class CascadeLevel {
+ // The animations sheet (CSS animations, script-generated animations,
+ // and CSS transitions that are no longer tied to CSS markup)
+ Animations,
+ // The transitions sheet (CSS transitions that are tied to CSS markup)
+ Transitions
+ };
+ // We don't define this as part of CascadeLevel as then we'd have to add
+ // explicit checks for the Count enum value everywhere CascadeLevel is used.
+ static const size_t kCascadeLevelCount =
+ static_cast<size_t>(CascadeLevel::Transitions) + 1;
+
+ // NOTE: This can return null after Disconnect().
+ nsPresContext* PresContext() const { return mPresContext; }
+
+ enum class RestyleType {
+ // Animation style has changed but the compositor is applying the same
+ // change so we might be able to defer updating the main thread until it
+ // becomes necessary.
+ Throttled,
+ // Animation style has changed and needs to be updated on the main thread.
+ Standard,
+ // Animation style has changed and needs to be updated on the main thread
+ // as well as forcing animations on layers to be updated.
+ // This is needed in cases such as when an animation becomes paused or has
+ // its playback rate changed. In such cases, although the computed style
+ // and refresh driver time might not change, we still need to ensure the
+ // corresponding animations on layers are updated to reflect the new
+ // configuration of the animation.
+ Layer
+ };
+
+ // Notifies the compositor that the animation rule for the specified
+ // (pseudo-)element at the specified cascade level needs to be updated.
+ // The specified steps taken to update the animation rule depend on
+ // |aRestyleType| whose values are described above.
+ void RequestRestyle(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ RestyleType aRestyleType,
+ CascadeLevel aCascadeLevel);
+
+ // Schedule an animation restyle. This is called automatically by
+ // RequestRestyle when necessary. However, it is exposed here since we also
+ // need to perform this step when triggering transitions *without* also
+ // invalidating the animation style rule (which RequestRestyle would do).
+ void PostRestyleForAnimation(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ CascadeLevel aCascadeLevel);
+
+ // Posts an animation restyle for any elements whose animation style rule
+ // is out of date but for which an animation restyle has not yet been
+ // posted because updates on the main thread are throttled.
+ void PostRestyleForThrottledAnimations();
+
+ // Called when the style context on the specified (pseudo-) element might
+ // have changed so that any context-sensitive values stored within
+ // animation effects (e.g. em-based endpoints used in keyframe effects)
+ // can be re-resolved to computed values.
+ void UpdateEffectProperties(nsStyleContext* aStyleContext,
+ dom::Element* aElement,
+ CSSPseudoElementType aPseudoType);
+
+ // Updates the animation rule stored on the EffectSet for the
+ // specified (pseudo-)element for cascade level |aLevel|.
+ // If the animation rule is not marked as needing an update,
+ // no work is done.
+ // |aStyleContext| is used for UpdateCascadingResults.
+ // |aStyleContext| can be nullptr if style context, which is associated with
+ // the primary frame of the specified (pseudo-)element, is the current style
+ // context.
+ // If we are resolving a new style context, we shoud pass the newly created
+ // style context, otherwise we may use an old style context, it will result
+ // unexpected cascading results.
+ void MaybeUpdateAnimationRule(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ CascadeLevel aCascadeLevel,
+ nsStyleContext *aStyleContext);
+
+ // We need to pass the newly resolved style context as |aStyleContext| when
+ // we call this function during resolving style context because this function
+ // calls UpdateCascadingResults with a style context if necessary, at the
+ // time, we end up using the previous style context if we don't pass the new
+ // style context.
+ // When we are not resolving style context, |aStyleContext| can be nullptr, we
+ // will use a style context associated with the primary frame of the specified
+ // (pseudo-)element.
+ nsIStyleRule* GetAnimationRule(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ CascadeLevel aCascadeLevel,
+ nsStyleContext* aStyleContext);
+
+ bool HasPendingStyleUpdates() const;
+ bool HasThrottledStyleUpdates() const;
+
+ // Tell the restyle tracker about all the animated styles that have
+ // pending updates so that it can update the animation rule for these
+ // elements.
+ void AddStyleUpdatesTo(RestyleTracker& aTracker);
+
+ nsIStyleRuleProcessor* RuleProcessor(CascadeLevel aCascadeLevel) const
+ {
+ return mRuleProcessors[aCascadeLevel];
+ }
+
+ static bool HasAnimationsForCompositor(const nsIFrame* aFrame,
+ nsCSSPropertyID aProperty);
+
+ static nsTArray<RefPtr<dom::Animation>>
+ GetAnimationsForCompositor(const nsIFrame* aFrame,
+ nsCSSPropertyID aProperty);
+
+ static void ClearIsRunningOnCompositor(const nsIFrame* aFrame,
+ nsCSSPropertyID aProperty);
+
+ // Update animation cascade results for the specified (pseudo-)element
+ // but only if we have marked the cascade as needing an update due a
+ // the change in the set of effects or a change in one of the effects'
+ // "in effect" state.
+ // |aStyleContext| may be nullptr in which case we will use the
+ // nsStyleContext of the primary frame of the specified (pseudo-)element.
+ //
+ // This method does NOT detect if other styles that apply above the
+ // animation level of the cascade have changed.
+ static void
+ MaybeUpdateCascadeResults(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ nsStyleContext* aStyleContext);
+
+ // Update the mPropertiesWithImportantRules and
+ // mPropertiesForAnimationsLevel members of the corresponding EffectSet.
+ //
+ // This can be expensive so we should only call it if styles that apply
+ // above the animation level of the cascade might have changed. For all
+ // other cases we should call MaybeUpdateCascadeResults.
+ static void
+ UpdateCascadeResults(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ nsStyleContext* aStyleContext);
+
+ // Helper to fetch the corresponding element and pseudo-type from a frame.
+ //
+ // For frames corresponding to pseudo-elements, the returned element is the
+ // element on which we store the animations (i.e. the EffectSet and/or
+ // AnimationCollection), *not* the generated content.
+ //
+ // Returns an empty result when a suitable element cannot be found including
+ // when the frame represents a pseudo-element on which we do not support
+ // animations.
+ static Maybe<NonOwningAnimationTarget>
+ GetAnimationElementAndPseudoForFrame(const nsIFrame* aFrame);
+
+ // Associates a performance warning with effects on |aFrame| that animates
+ // |aProperty|.
+ static void SetPerformanceWarning(
+ const nsIFrame* aFrame,
+ nsCSSPropertyID aProperty,
+ const AnimationPerformanceWarning& aWarning);
+
+private:
+ ~EffectCompositor() = default;
+
+ // Rebuilds the animation rule corresponding to |aCascadeLevel| on the
+ // EffectSet associated with the specified (pseudo-)element.
+ static void ComposeAnimationRule(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ CascadeLevel aCascadeLevel,
+ TimeStamp aRefreshTime);
+
+ static dom::Element* GetElementToRestyle(dom::Element* aElement,
+ CSSPseudoElementType
+ aPseudoType);
+
+ // Get the properties in |aEffectSet| that we are able to animate on the
+ // compositor but which are also specified at a higher level in the cascade
+ // than the animations level in |aStyleContext|.
+ static void
+ GetOverriddenProperties(nsStyleContext* aStyleContext,
+ EffectSet& aEffectSet,
+ nsCSSPropertyIDSet& aPropertiesOverridden);
+
+ static void
+ UpdateCascadeResults(EffectSet& aEffectSet,
+ dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ nsStyleContext* aStyleContext);
+
+ static nsPresContext* GetPresContext(dom::Element* aElement);
+
+ nsPresContext* mPresContext;
+
+ // Elements with a pending animation restyle. The associated bool value is
+ // true if a pending animation restyle has also been dispatched. For
+ // animations that can be throttled, we will add an entry to the hashtable to
+ // indicate that the style rule on the element is out of date but without
+ // posting a restyle to update it.
+ EnumeratedArray<CascadeLevel, CascadeLevel(kCascadeLevelCount),
+ nsDataHashtable<PseudoElementHashEntry, bool>>
+ mElementsToRestyle;
+
+ class AnimationStyleRuleProcessor final : public nsIStyleRuleProcessor
+ {
+ public:
+ AnimationStyleRuleProcessor(EffectCompositor* aCompositor,
+ CascadeLevel aCascadeLevel)
+ : mCompositor(aCompositor)
+ , mCascadeLevel(aCascadeLevel)
+ {
+ MOZ_ASSERT(aCompositor);
+ }
+
+ NS_DECL_ISUPPORTS
+
+ // nsIStyleRuleProcessor (parts)
+ nsRestyleHint HasStateDependentStyle(
+ StateRuleProcessorData* aData) override;
+ nsRestyleHint HasStateDependentStyle(
+ PseudoElementStateRuleProcessorData* aData) override;
+ bool HasDocumentStateDependentStyle(StateRuleProcessorData* aData) override;
+ nsRestyleHint HasAttributeDependentStyle(
+ AttributeRuleProcessorData* aData,
+ RestyleHintData& aRestyleHintDataResult) override;
+ bool MediumFeaturesChanged(nsPresContext* aPresContext) override;
+ void RulesMatching(ElementRuleProcessorData* aData) override;
+ void RulesMatching(PseudoElementRuleProcessorData* aData) override;
+ void RulesMatching(AnonBoxRuleProcessorData* aData) override;
+#ifdef MOZ_XUL
+ void RulesMatching(XULTreeRuleProcessorData* aData) override;
+#endif
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf)
+ const MOZ_MUST_OVERRIDE override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf)
+ const MOZ_MUST_OVERRIDE override;
+
+ private:
+ ~AnimationStyleRuleProcessor() = default;
+
+ EffectCompositor* mCompositor;
+ CascadeLevel mCascadeLevel;
+ };
+
+ EnumeratedArray<CascadeLevel, CascadeLevel(kCascadeLevelCount),
+ OwningNonNull<AnimationStyleRuleProcessor>>
+ mRuleProcessors;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_EffectCompositor_h
diff --git a/dom/animation/EffectSet.cpp b/dom/animation/EffectSet.cpp
new file mode 100644
index 0000000000..ffd3bb523e
--- /dev/null
+++ b/dom/animation/EffectSet.cpp
@@ -0,0 +1,177 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "EffectSet.h"
+#include "mozilla/dom/Element.h" // For Element
+#include "mozilla/RestyleManagerHandle.h"
+#include "mozilla/RestyleManagerHandleInlines.h"
+#include "nsCSSPseudoElements.h" // For CSSPseudoElementType
+#include "nsCycleCollectionNoteChild.h" // For CycleCollectionNoteChild
+#include "nsPresContext.h"
+#include "nsLayoutUtils.h"
+
+namespace mozilla {
+
+/* static */ void
+EffectSet::PropertyDtor(void* aObject, nsIAtom* aPropertyName,
+ void* aPropertyValue, void* aData)
+{
+ EffectSet* effectSet = static_cast<EffectSet*>(aPropertyValue);
+
+#ifdef DEBUG
+ MOZ_ASSERT(!effectSet->mCalledPropertyDtor, "Should not call dtor twice");
+ effectSet->mCalledPropertyDtor = true;
+#endif
+
+ delete effectSet;
+}
+
+void
+EffectSet::Traverse(nsCycleCollectionTraversalCallback& aCallback)
+{
+ for (auto iter = mEffects.Iter(); !iter.Done(); iter.Next()) {
+ CycleCollectionNoteChild(aCallback, iter.Get()->GetKey(),
+ "EffectSet::mEffects[]", aCallback.Flags());
+ }
+}
+
+/* static */ EffectSet*
+EffectSet::GetEffectSet(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType)
+{
+ nsIAtom* propName = GetEffectSetPropertyAtom(aPseudoType);
+ return static_cast<EffectSet*>(aElement->GetProperty(propName));
+}
+
+/* static */ EffectSet*
+EffectSet::GetEffectSet(const nsIFrame* aFrame)
+{
+ Maybe<NonOwningAnimationTarget> target =
+ EffectCompositor::GetAnimationElementAndPseudoForFrame(aFrame);
+
+ if (!target) {
+ return nullptr;
+ }
+
+ if (!target->mElement->MayHaveAnimations()) {
+ return nullptr;
+ }
+
+ return GetEffectSet(target->mElement, target->mPseudoType);
+}
+
+/* static */ EffectSet*
+EffectSet::GetOrCreateEffectSet(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType)
+{
+ EffectSet* effectSet = GetEffectSet(aElement, aPseudoType);
+ if (effectSet) {
+ return effectSet;
+ }
+
+ nsIAtom* propName = GetEffectSetPropertyAtom(aPseudoType);
+ effectSet = new EffectSet();
+
+ nsresult rv = aElement->SetProperty(propName, effectSet,
+ &EffectSet::PropertyDtor, true);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SetProperty failed");
+ // The set must be destroyed via PropertyDtor, otherwise
+ // mCalledPropertyDtor assertion is triggered in destructor.
+ EffectSet::PropertyDtor(aElement, propName, effectSet, nullptr);
+ return nullptr;
+ }
+
+ aElement->SetMayHaveAnimations();
+
+ return effectSet;
+}
+
+/* static */ void
+EffectSet::DestroyEffectSet(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType)
+{
+ nsIAtom* propName = GetEffectSetPropertyAtom(aPseudoType);
+ EffectSet* effectSet =
+ static_cast<EffectSet*>(aElement->GetProperty(propName));
+ if (!effectSet) {
+ return;
+ }
+
+ MOZ_ASSERT(!effectSet->IsBeingEnumerated(),
+ "Should not destroy an effect set while it is being enumerated");
+ effectSet = nullptr;
+
+ aElement->DeleteProperty(propName);
+}
+
+void
+EffectSet::UpdateAnimationGeneration(nsPresContext* aPresContext)
+{
+ MOZ_ASSERT(aPresContext->RestyleManager()->IsGecko(),
+ "stylo: Servo-backed style system should not be using "
+ "EffectSet");
+ mAnimationGeneration =
+ aPresContext->RestyleManager()->AsGecko()->GetAnimationGeneration();
+}
+
+/* static */ nsIAtom**
+EffectSet::GetEffectSetPropertyAtoms()
+{
+ static nsIAtom* effectSetPropertyAtoms[] =
+ {
+ nsGkAtoms::animationEffectsProperty,
+ nsGkAtoms::animationEffectsForBeforeProperty,
+ nsGkAtoms::animationEffectsForAfterProperty,
+ nullptr
+ };
+
+ return effectSetPropertyAtoms;
+}
+
+/* static */ nsIAtom*
+EffectSet::GetEffectSetPropertyAtom(CSSPseudoElementType aPseudoType)
+{
+ switch (aPseudoType) {
+ case CSSPseudoElementType::NotPseudo:
+ return nsGkAtoms::animationEffectsProperty;
+
+ case CSSPseudoElementType::before:
+ return nsGkAtoms::animationEffectsForBeforeProperty;
+
+ case CSSPseudoElementType::after:
+ return nsGkAtoms::animationEffectsForAfterProperty;
+
+ default:
+ NS_NOTREACHED("Should not try to get animation effects for a pseudo "
+ "other that :before or :after");
+ return nullptr;
+ }
+}
+
+void
+EffectSet::AddEffect(dom::KeyframeEffectReadOnly& aEffect)
+{
+ if (mEffects.Contains(&aEffect)) {
+ return;
+ }
+
+ mEffects.PutEntry(&aEffect);
+ MarkCascadeNeedsUpdate();
+}
+
+void
+EffectSet::RemoveEffect(dom::KeyframeEffectReadOnly& aEffect)
+{
+ if (!mEffects.Contains(&aEffect)) {
+ return;
+ }
+
+ mEffects.RemoveEntry(&aEffect);
+ MarkCascadeNeedsUpdate();
+}
+
+} // namespace mozilla
diff --git a/dom/animation/EffectSet.h b/dom/animation/EffectSet.h
new file mode 100644
index 0000000000..9ba31ef91d
--- /dev/null
+++ b/dom/animation/EffectSet.h
@@ -0,0 +1,261 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_EffectSet_h
+#define mozilla_EffectSet_h
+
+#include "mozilla/AnimValuesStyleRule.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/EffectCompositor.h"
+#include "mozilla/EnumeratedArray.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/KeyframeEffectReadOnly.h"
+#include "nsHashKeys.h" // For nsPtrHashKey
+#include "nsTHashtable.h" // For nsTHashtable
+
+class nsPresContext;
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+} // namespace dom
+
+enum class CSSPseudoElementType : uint8_t;
+
+// A wrapper around a hashset of AnimationEffect objects to handle
+// storing the set as a property of an element.
+class EffectSet
+{
+public:
+ EffectSet()
+ : mCascadeNeedsUpdate(false)
+ , mAnimationGeneration(0)
+#ifdef DEBUG
+ , mActiveIterators(0)
+ , mCalledPropertyDtor(false)
+#endif
+ {
+ MOZ_COUNT_CTOR(EffectSet);
+ }
+
+ ~EffectSet()
+ {
+ MOZ_ASSERT(mCalledPropertyDtor,
+ "must call destructor through element property dtor");
+ MOZ_ASSERT(mActiveIterators == 0,
+ "Effect set should not be destroyed while it is being "
+ "enumerated");
+ MOZ_COUNT_DTOR(EffectSet);
+ }
+ static void PropertyDtor(void* aObject, nsIAtom* aPropertyName,
+ void* aPropertyValue, void* aData);
+
+ // Methods for supporting cycle-collection
+ void Traverse(nsCycleCollectionTraversalCallback& aCallback);
+
+ static EffectSet* GetEffectSet(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType);
+ static EffectSet* GetEffectSet(const nsIFrame* aFrame);
+ static EffectSet* GetOrCreateEffectSet(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType);
+ static void DestroyEffectSet(dom::Element* aElement,
+ CSSPseudoElementType aPseudoType);
+
+ void AddEffect(dom::KeyframeEffectReadOnly& aEffect);
+ void RemoveEffect(dom::KeyframeEffectReadOnly& aEffect);
+
+private:
+ typedef nsTHashtable<nsRefPtrHashKey<dom::KeyframeEffectReadOnly>>
+ OwningEffectSet;
+
+public:
+ // A simple iterator to support iterating over the effects in this object in
+ // range-based for loops.
+ //
+ // This allows us to avoid exposing mEffects directly and saves the
+ // caller from having to dereference hashtable iterators using
+ // the rather complicated: iter.Get()->GetKey().
+ class Iterator
+ {
+ public:
+ explicit Iterator(EffectSet& aEffectSet)
+ : mEffectSet(aEffectSet)
+ , mHashIterator(mozilla::Move(aEffectSet.mEffects.Iter()))
+ , mIsEndIterator(false)
+ {
+#ifdef DEBUG
+ mEffectSet.mActiveIterators++;
+#endif
+ }
+
+ Iterator(Iterator&& aOther)
+ : mEffectSet(aOther.mEffectSet)
+ , mHashIterator(mozilla::Move(aOther.mHashIterator))
+ , mIsEndIterator(aOther.mIsEndIterator)
+ {
+#ifdef DEBUG
+ mEffectSet.mActiveIterators++;
+#endif
+ }
+
+ static Iterator EndIterator(EffectSet& aEffectSet)
+ {
+ Iterator result(aEffectSet);
+ result.mIsEndIterator = true;
+ return result;
+ }
+
+ ~Iterator()
+ {
+#ifdef DEBUG
+ MOZ_ASSERT(mEffectSet.mActiveIterators > 0);
+ mEffectSet.mActiveIterators--;
+#endif
+ }
+
+ bool operator!=(const Iterator& aOther) const {
+ if (Done() || aOther.Done()) {
+ return Done() != aOther.Done();
+ }
+ return mHashIterator.Get() != aOther.mHashIterator.Get();
+ }
+
+ Iterator& operator++() {
+ MOZ_ASSERT(!Done());
+ mHashIterator.Next();
+ return *this;
+ }
+
+ dom::KeyframeEffectReadOnly* operator* ()
+ {
+ MOZ_ASSERT(!Done());
+ return mHashIterator.Get()->GetKey();
+ }
+
+ private:
+ Iterator() = delete;
+ Iterator(const Iterator&) = delete;
+ Iterator& operator=(const Iterator&) = delete;
+ Iterator& operator=(const Iterator&&) = delete;
+
+ bool Done() const {
+ return mIsEndIterator || mHashIterator.Done();
+ }
+
+ EffectSet& mEffectSet;
+ OwningEffectSet::Iterator mHashIterator;
+ bool mIsEndIterator;
+ };
+
+ friend class Iterator;
+
+ Iterator begin() { return Iterator(*this); }
+ Iterator end() { return Iterator::EndIterator(*this); }
+#ifdef DEBUG
+ bool IsBeingEnumerated() const { return mActiveIterators != 0; }
+#endif
+
+ bool IsEmpty() const { return mEffects.IsEmpty(); }
+
+ size_t Count() const { return mEffects.Count(); }
+
+ RefPtr<AnimValuesStyleRule>& AnimationRule(EffectCompositor::CascadeLevel
+ aCascadeLevel)
+ {
+ return mAnimationRule[aCascadeLevel];
+ }
+
+ const TimeStamp& AnimationRuleRefreshTime(EffectCompositor::CascadeLevel
+ aCascadeLevel) const
+ {
+ return mAnimationRuleRefreshTime[aCascadeLevel];
+ }
+ void UpdateAnimationRuleRefreshTime(EffectCompositor::CascadeLevel
+ aCascadeLevel,
+ const TimeStamp& aRefreshTime)
+ {
+ mAnimationRuleRefreshTime[aCascadeLevel] = aRefreshTime;
+ }
+
+ bool CascadeNeedsUpdate() const { return mCascadeNeedsUpdate; }
+ void MarkCascadeNeedsUpdate() { mCascadeNeedsUpdate = true; }
+ void MarkCascadeUpdated() { mCascadeNeedsUpdate = false; }
+
+ void UpdateAnimationGeneration(nsPresContext* aPresContext);
+ uint64_t GetAnimationGeneration() const { return mAnimationGeneration; }
+
+ static nsIAtom** GetEffectSetPropertyAtoms();
+
+ nsCSSPropertyIDSet& PropertiesWithImportantRules()
+ {
+ return mPropertiesWithImportantRules;
+ }
+ nsCSSPropertyIDSet& PropertiesForAnimationsLevel()
+ {
+ return mPropertiesForAnimationsLevel;
+ }
+
+private:
+ static nsIAtom* GetEffectSetPropertyAtom(CSSPseudoElementType aPseudoType);
+
+ OwningEffectSet mEffects;
+
+ // These style rules contain the style data for currently animating
+ // values. They only match when styling with animation. When we
+ // style without animation, we need to not use them so that we can
+ // detect any new changes; if necessary we restyle immediately
+ // afterwards with animation.
+ EnumeratedArray<EffectCompositor::CascadeLevel,
+ EffectCompositor::CascadeLevel(
+ EffectCompositor::kCascadeLevelCount),
+ RefPtr<AnimValuesStyleRule>> mAnimationRule;
+
+ // A parallel array to mAnimationRule that records the refresh driver
+ // timestamp when the rule was last updated. This is used for certain
+ // animations which are updated only periodically (e.g. transform animations
+ // running on the compositor that affect the scrollable overflow region).
+ EnumeratedArray<EffectCompositor::CascadeLevel,
+ EffectCompositor::CascadeLevel(
+ EffectCompositor::kCascadeLevelCount),
+ TimeStamp> mAnimationRuleRefreshTime;
+
+ // Dirty flag to represent when the mPropertiesWithImportantRules and
+ // mPropertiesForAnimationsLevel on effects in this set might need to be
+ // updated.
+ //
+ // Set to true any time the set of effects is changed or when
+ // one the effects goes in or out of the "in effect" state.
+ bool mCascadeNeedsUpdate;
+
+ // RestyleManager keeps track of the number of animation restyles.
+ // 'mini-flushes' (see nsTransitionManager::UpdateAllThrottledStyles()).
+ // mAnimationGeneration is the sequence number of the last flush where a
+ // transition/animation changed. We keep a similar count on the
+ // corresponding layer so we can check that the layer is up to date with
+ // the animation manager.
+ uint64_t mAnimationGeneration;
+
+ // Specifies the compositor-animatable properties that are overridden by
+ // !important rules.
+ nsCSSPropertyIDSet mPropertiesWithImportantRules;
+ // Specifies the properties for which the result will be added to the
+ // animations level of the cascade and hence should be skipped when we are
+ // composing the animation style for the transitions level of the cascede.
+ nsCSSPropertyIDSet mPropertiesForAnimationsLevel;
+
+#ifdef DEBUG
+ // Track how many iterators are referencing this effect set when we are
+ // destroyed, we can assert that nothing is still pointing to us.
+ uint64_t mActiveIterators;
+
+ bool mCalledPropertyDtor;
+#endif
+};
+
+} // namespace mozilla
+
+#endif // mozilla_EffectSet_h
diff --git a/dom/animation/KeyframeEffect.cpp b/dom/animation/KeyframeEffect.cpp
new file mode 100644
index 0000000000..decbf63050
--- /dev/null
+++ b/dom/animation/KeyframeEffect.cpp
@@ -0,0 +1,211 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "mozilla/dom/KeyframeEffect.h"
+
+#include "mozilla/dom/KeyframeAnimationOptionsBinding.h"
+ // For UnrestrictedDoubleOrKeyframeAnimationOptions
+#include "mozilla/dom/AnimationEffectTiming.h"
+#include "mozilla/dom/KeyframeEffectBinding.h"
+#include "mozilla/KeyframeUtils.h"
+#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch
+#include "nsIScriptError.h"
+
+namespace mozilla {
+namespace dom {
+
+KeyframeEffect::KeyframeEffect(nsIDocument* aDocument,
+ const Maybe<OwningAnimationTarget>& aTarget,
+ const TimingParams& aTiming,
+ const KeyframeEffectParams& aOptions)
+ : KeyframeEffectReadOnly(aDocument, aTarget,
+ new AnimationEffectTiming(aDocument, aTiming, this),
+ aOptions)
+{
+}
+
+JSObject*
+KeyframeEffect::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto)
+{
+ return KeyframeEffectBinding::Wrap(aCx, this, aGivenProto);
+}
+
+/* static */ already_AddRefed<KeyframeEffect>
+KeyframeEffect::Constructor(
+ const GlobalObject& aGlobal,
+ const Nullable<ElementOrCSSPseudoElement>& aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
+ ErrorResult& aRv)
+{
+ return ConstructKeyframeEffect<KeyframeEffect>(aGlobal, aTarget, aKeyframes,
+ aOptions, aRv);
+}
+
+/* static */ already_AddRefed<KeyframeEffect>
+KeyframeEffect::Constructor(const GlobalObject& aGlobal,
+ KeyframeEffectReadOnly& aSource,
+ ErrorResult& aRv)
+{
+ return ConstructKeyframeEffect<KeyframeEffect>(aGlobal, aSource, aRv);
+}
+
+/* static */ already_AddRefed<KeyframeEffect>
+KeyframeEffect::Constructor(
+ const GlobalObject& aGlobal,
+ const Nullable<ElementOrCSSPseudoElement>& aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
+ ErrorResult& aRv)
+{
+ return ConstructKeyframeEffect<KeyframeEffect>(aGlobal, aTarget, aKeyframes,
+ aOptions, aRv);
+}
+
+void
+KeyframeEffect::NotifySpecifiedTimingUpdated()
+{
+ // Use the same document for a pseudo element and its parent element.
+ // Use nullptr if we don't have mTarget, so disable the mutation batch.
+ nsAutoAnimationMutationBatch mb(mTarget ? mTarget->mElement->OwnerDoc()
+ : nullptr);
+
+ if (mAnimation) {
+ mAnimation->NotifyEffectTimingUpdated();
+
+ if (mAnimation->IsRelevant()) {
+ nsNodeUtils::AnimationChanged(mAnimation);
+ }
+
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+ }
+}
+
+void
+KeyframeEffect::SetTarget(const Nullable<ElementOrCSSPseudoElement>& aTarget)
+{
+ Maybe<OwningAnimationTarget> newTarget = ConvertTarget(aTarget);
+ if (mTarget == newTarget) {
+ // Assign the same target, skip it.
+ return;
+ }
+
+ if (mTarget) {
+ UnregisterTarget();
+ ResetIsRunningOnCompositor();
+
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+
+ nsAutoAnimationMutationBatch mb(mTarget->mElement->OwnerDoc());
+ if (mAnimation) {
+ nsNodeUtils::AnimationRemoved(mAnimation);
+ }
+ }
+
+ mTarget = newTarget;
+
+ if (mTarget) {
+ UpdateTargetRegistration();
+ RefPtr<nsStyleContext> styleContext = GetTargetStyleContext();
+ if (styleContext) {
+ UpdateProperties(styleContext);
+ } else if (mEffectOptions.mSpacingMode == SpacingMode::paced) {
+ KeyframeUtils::ApplyDistributeSpacing(mKeyframes);
+ }
+
+ MaybeUpdateFrameForCompositor();
+
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+
+ nsAutoAnimationMutationBatch mb(mTarget->mElement->OwnerDoc());
+ if (mAnimation) {
+ nsNodeUtils::AnimationAdded(mAnimation);
+ }
+ } else if (mEffectOptions.mSpacingMode == SpacingMode::paced) {
+ // New target is null, so fall back to distribute spacing.
+ KeyframeUtils::ApplyDistributeSpacing(mKeyframes);
+ }
+}
+
+void
+KeyframeEffect::SetIterationComposite(
+ const IterationCompositeOperation& aIterationComposite)
+{
+ // Ignore iterationComposite if the Web Animations API is not enabled,
+ // then the default value 'Replace' will be used.
+ if (!AnimationUtils::IsCoreAPIEnabledForCaller()) {
+ return;
+ }
+
+ if (mEffectOptions.mIterationComposite == aIterationComposite) {
+ return;
+ }
+
+ if (mAnimation && mAnimation->IsRelevant()) {
+ nsNodeUtils::AnimationChanged(mAnimation);
+ }
+
+ mEffectOptions.mIterationComposite = aIterationComposite;
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+}
+
+void
+KeyframeEffect::SetSpacing(JSContext* aCx,
+ const nsAString& aSpacing,
+ ErrorResult& aRv)
+{
+ SpacingMode spacingMode = SpacingMode::distribute;
+ nsCSSPropertyID pacedProperty = eCSSProperty_UNKNOWN;
+ nsAutoString invalidPacedProperty;
+ KeyframeEffectParams::ParseSpacing(aSpacing,
+ spacingMode,
+ pacedProperty,
+ invalidPacedProperty,
+ aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ if (!invalidPacedProperty.IsEmpty()) {
+ const char16_t* params[] = { invalidPacedProperty.get() };
+ nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aCx);
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_CSTRING("Animation"),
+ doc,
+ nsContentUtils::eDOM_PROPERTIES,
+ "UnanimatablePacedProperty",
+ params, ArrayLength(params));
+ }
+
+ if (mEffectOptions.mSpacingMode == spacingMode &&
+ mEffectOptions.mPacedProperty == pacedProperty) {
+ return;
+ }
+
+ mEffectOptions.mSpacingMode = spacingMode;
+ mEffectOptions.mPacedProperty = pacedProperty;
+
+ // Apply spacing. We apply distribute here. If the new spacing is paced,
+ // UpdateProperties() will apply it.
+ if (mEffectOptions.mSpacingMode == SpacingMode::distribute) {
+ KeyframeUtils::ApplyDistributeSpacing(mKeyframes);
+ }
+
+ if (mAnimation && mAnimation->IsRelevant()) {
+ nsNodeUtils::AnimationChanged(mAnimation);
+ }
+
+ if (mTarget) {
+ RefPtr<nsStyleContext> styleContext = GetTargetStyleContext();
+ if (styleContext) {
+ UpdateProperties(styleContext);
+ }
+ }
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/animation/KeyframeEffect.h b/dom/animation/KeyframeEffect.h
new file mode 100644
index 0000000000..3c704a8202
--- /dev/null
+++ b/dom/animation/KeyframeEffect.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_dom_KeyframeEffect_h
+#define mozilla_dom_KeyframeEffect_h
+
+#include "nsWrapperCache.h"
+#include "mozilla/dom/KeyframeEffectReadOnly.h"
+#include "mozilla/AnimationTarget.h" // For (Non)OwningAnimationTarget
+#include "mozilla/Maybe.h"
+
+struct JSContext;
+class JSObject;
+class nsIDocument;
+
+namespace mozilla {
+
+class ErrorResult;
+struct KeyframeEffectParams;
+struct TimingParams;
+
+namespace dom {
+
+class ElementOrCSSPseudoElement;
+class GlobalObject;
+class UnrestrictedDoubleOrKeyframeAnimationOptions;
+class UnrestrictedDoubleOrKeyframeEffectOptions;
+
+class KeyframeEffect : public KeyframeEffectReadOnly
+{
+public:
+ KeyframeEffect(nsIDocument* aDocument,
+ const Maybe<OwningAnimationTarget>& aTarget,
+ const TimingParams& aTiming,
+ const KeyframeEffectParams& aOptions);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<KeyframeEffect>
+ Constructor(const GlobalObject& aGlobal,
+ const Nullable<ElementOrCSSPseudoElement>& aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
+ ErrorResult& aRv);
+
+ static already_AddRefed<KeyframeEffect>
+ Constructor(const GlobalObject& aGlobal,
+ KeyframeEffectReadOnly& aSource,
+ ErrorResult& aRv);
+
+ // Variant of Constructor that accepts a KeyframeAnimationOptions object
+ // for use with for Animatable.animate.
+ // Not exposed to content.
+ static already_AddRefed<KeyframeEffect>
+ Constructor(const GlobalObject& aGlobal,
+ const Nullable<ElementOrCSSPseudoElement>& aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
+ ErrorResult& aRv);
+
+ void NotifySpecifiedTimingUpdated();
+
+ // This method calls GetTargetStyleContext which is not safe to use when
+ // we are in the middle of updating style. If we need to use this when
+ // updating style, we should pass the nsStyleContext into this method and use
+ // that to update the properties rather than calling
+ // GetStyleContextForElement.
+ void SetTarget(const Nullable<ElementOrCSSPseudoElement>& aTarget);
+
+ void SetSpacing(JSContext* aCx, const nsAString& aSpacing, ErrorResult& aRv);
+ void SetIterationComposite(
+ const IterationCompositeOperation& aIterationComposite);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_KeyframeEffect_h
diff --git a/dom/animation/KeyframeEffectParams.cpp b/dom/animation/KeyframeEffectParams.cpp
new file mode 100644
index 0000000000..2576406919
--- /dev/null
+++ b/dom/animation/KeyframeEffectParams.cpp
@@ -0,0 +1,169 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "mozilla/KeyframeEffectParams.h"
+
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/KeyframeUtils.h"
+#include "mozilla/RangedPtr.h"
+#include "nsReadableUtils.h"
+
+namespace mozilla {
+
+static inline bool
+IsLetter(char16_t aCh)
+{
+ return (0x41 <= aCh && aCh <= 0x5A) || (0x61 <= aCh && aCh <= 0x7A);
+}
+
+static inline bool
+IsDigit(char16_t aCh)
+{
+ return 0x30 <= aCh && aCh <= 0x39;
+}
+
+static inline bool
+IsNameStartCode(char16_t aCh)
+{
+ return IsLetter(aCh) || aCh >= 0x80 || aCh == '_';
+}
+
+static inline bool
+IsNameCode(char16_t aCh)
+{
+ return IsNameStartCode(aCh) || IsDigit(aCh) || aCh == '-';
+}
+
+static inline bool
+IsNewLine(char16_t aCh)
+{
+ // 0x0A (LF), 0x0C (FF), 0x0D (CR), or pairs of CR followed by LF are
+ // replaced by LF.
+ return aCh == 0x0A || aCh == 0x0C || aCh == 0x0D;
+}
+
+static inline bool
+IsValidEscape(char16_t aFirst, char16_t aSecond)
+{
+ return aFirst == '\\' && !IsNewLine(aSecond);
+}
+
+static bool
+IsIdentStart(RangedPtr<const char16_t> aIter,
+ const char16_t* const aEnd)
+{
+ if (aIter == aEnd) {
+ return false;
+ }
+
+ if (*aIter == '-') {
+ if (aIter + 1 == aEnd) {
+ return false;
+ }
+ char16_t second = *(aIter + 1);
+ return IsNameStartCode(second) ||
+ second == '-' ||
+ (aIter + 2 != aEnd && IsValidEscape(second, *(aIter + 2)));
+ }
+ return IsNameStartCode(*aIter) ||
+ (aIter + 1 != aEnd && IsValidEscape(*aIter, *(aIter + 1)));
+}
+
+static void
+ConsumeIdentToken(RangedPtr<const char16_t>& aIter,
+ const char16_t* const aEnd,
+ nsAString& aResult)
+{
+ aResult.Truncate();
+
+ // Check if it starts with an identifier.
+ if (!IsIdentStart(aIter, aEnd)) {
+ return;
+ }
+
+ // Start to consume.
+ while (aIter != aEnd) {
+ if (IsNameCode(*aIter)) {
+ aResult.Append(*aIter);
+ } else if (*aIter == '\\') {
+ const RangedPtr<const char16_t> secondChar = aIter + 1;
+ if (secondChar == aEnd || !IsValidEscape(*aIter, *secondChar)) {
+ break;
+ }
+ // Consume '\\' and append the character following this '\\'.
+ ++aIter;
+ aResult.Append(*aIter);
+ } else {
+ break;
+ }
+ ++aIter;
+ }
+}
+
+/* static */ void
+KeyframeEffectParams::ParseSpacing(const nsAString& aSpacing,
+ SpacingMode& aSpacingMode,
+ nsCSSPropertyID& aPacedProperty,
+ nsAString& aInvalidPacedProperty,
+ ErrorResult& aRv)
+{
+ aInvalidPacedProperty.Truncate();
+
+ // Ignore spacing if the core API is not enabled since it is not yet ready to
+ // ship.
+ if (!AnimationUtils::IsCoreAPIEnabledForCaller()) {
+ aSpacingMode = SpacingMode::distribute;
+ return;
+ }
+
+ // Parse spacing.
+ // distribute | paced({ident})
+ // https://w3c.github.io/web-animations/#dom-keyframeeffectreadonly-spacing
+ // 1. distribute spacing.
+ if (aSpacing.EqualsLiteral("distribute")) {
+ aSpacingMode = SpacingMode::distribute;
+ return;
+ }
+
+ // 2. paced spacing.
+ static const nsLiteralString kPacedPrefix = NS_LITERAL_STRING("paced(");
+ if (!StringBeginsWith(aSpacing, kPacedPrefix)) {
+ aRv.ThrowTypeError<dom::MSG_INVALID_SPACING_MODE_ERROR>(aSpacing);
+ return;
+ }
+
+ RangedPtr<const char16_t> iter(aSpacing.Data() + kPacedPrefix.Length(),
+ aSpacing.Data(), aSpacing.Length());
+ const char16_t* const end = aSpacing.EndReading();
+
+ nsAutoString identToken;
+ ConsumeIdentToken(iter, end, identToken);
+ if (identToken.IsEmpty()) {
+ aRv.ThrowTypeError<dom::MSG_INVALID_SPACING_MODE_ERROR>(aSpacing);
+ return;
+ }
+
+ aPacedProperty =
+ nsCSSProps::LookupProperty(identToken, CSSEnabledState::eForAllContent);
+ if (aPacedProperty == eCSSProperty_UNKNOWN ||
+ aPacedProperty == eCSSPropertyExtra_variable ||
+ !KeyframeUtils::IsAnimatableProperty(aPacedProperty)) {
+ aPacedProperty = eCSSProperty_UNKNOWN;
+ aInvalidPacedProperty = identToken;
+ }
+
+ if (end - iter.get() != 1 || *iter != ')') {
+ aRv.ThrowTypeError<dom::MSG_INVALID_SPACING_MODE_ERROR>(aSpacing);
+ return;
+ }
+
+ aSpacingMode = aPacedProperty == eCSSProperty_UNKNOWN
+ ? SpacingMode::distribute
+ : SpacingMode::paced;
+}
+
+} // namespace mozilla
diff --git a/dom/animation/KeyframeEffectParams.h b/dom/animation/KeyframeEffectParams.h
new file mode 100644
index 0000000000..92a284eae1
--- /dev/null
+++ b/dom/animation/KeyframeEffectParams.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_KeyframeEffectParams_h
+#define mozilla_KeyframeEffectParams_h
+
+#include "nsCSSProps.h"
+#include "nsString.h"
+// X11 has a #define for None
+#ifdef None
+#undef None
+#endif
+#include "mozilla/dom/KeyframeEffectBinding.h" // IterationCompositeOperation
+
+namespace mozilla {
+
+class ErrorResult;
+
+enum class SpacingMode
+{
+ distribute,
+ paced
+};
+
+struct KeyframeEffectParams
+{
+ void GetSpacingAsString(nsAString& aSpacing) const
+ {
+ if (mSpacingMode == SpacingMode::distribute) {
+ aSpacing.AssignLiteral("distribute");
+ } else {
+ aSpacing.AssignLiteral("paced(");
+ aSpacing.AppendASCII(nsCSSProps::GetStringValue(mPacedProperty).get());
+ aSpacing.AppendLiteral(")");
+ }
+ }
+
+ /**
+ * Parse spacing string.
+ *
+ * @param aSpacing The input spacing string.
+ * @param [out] aSpacingMode The parsed spacing mode.
+ * @param [out] aPacedProperty The parsed CSS property if using paced spacing.
+ * @param [out] aInvalidPacedProperty A string that, if we parsed a string of
+ * the form 'paced(<ident>)' where <ident>
+ * is not a recognized animatable property,
+ * will be set to <ident>.
+ * @param [out] aRv The error result.
+ */
+ static void ParseSpacing(const nsAString& aSpacing,
+ SpacingMode& aSpacingMode,
+ nsCSSPropertyID& aPacedProperty,
+ nsAString& aInvalidPacedProperty,
+ ErrorResult& aRv);
+
+ dom::IterationCompositeOperation mIterationComposite =
+ dom::IterationCompositeOperation::Replace;
+ // FIXME: Bug 1216844: Add CompositeOperation
+ SpacingMode mSpacingMode = SpacingMode::distribute;
+ nsCSSPropertyID mPacedProperty = eCSSProperty_UNKNOWN;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_KeyframeEffectParams_h
diff --git a/dom/animation/KeyframeEffectReadOnly.cpp b/dom/animation/KeyframeEffectReadOnly.cpp
new file mode 100644
index 0000000000..639e0b2b0d
--- /dev/null
+++ b/dom/animation/KeyframeEffectReadOnly.cpp
@@ -0,0 +1,1430 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "mozilla/dom/KeyframeEffectReadOnly.h"
+
+#include "mozilla/dom/KeyframeAnimationOptionsBinding.h"
+ // For UnrestrictedDoubleOrKeyframeAnimationOptions;
+#include "mozilla/dom/CSSPseudoElement.h"
+#include "mozilla/dom/KeyframeEffectBinding.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/EffectSet.h"
+#include "mozilla/FloatingPoint.h" // For IsFinite
+#include "mozilla/LookAndFeel.h" // For LookAndFeel::GetInt
+#include "mozilla/KeyframeUtils.h"
+#include "mozilla/ServoBindings.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "Layers.h" // For Layer
+#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetStyleContextForElement
+#include "nsContentUtils.h" // nsContentUtils::ReportToConsole
+#include "nsCSSPropertyIDSet.h"
+#include "nsCSSProps.h" // For nsCSSProps::PropHasFlags
+#include "nsCSSPseudoElements.h" // For CSSPseudoElementType
+#include "nsIPresShell.h"
+#include "nsIScriptError.h"
+
+namespace mozilla {
+
+bool
+PropertyValuePair::operator==(const PropertyValuePair& aOther) const
+{
+ if (mProperty != aOther.mProperty || mValue != aOther.mValue) {
+ return false;
+ }
+ if (mServoDeclarationBlock == aOther.mServoDeclarationBlock) {
+ return true;
+ }
+ if (!mServoDeclarationBlock || !aOther.mServoDeclarationBlock) {
+ return false;
+ }
+ return Servo_DeclarationBlock_Equals(mServoDeclarationBlock,
+ aOther.mServoDeclarationBlock);
+}
+
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(KeyframeEffectReadOnly,
+ AnimationEffectReadOnly,
+ mTarget)
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(KeyframeEffectReadOnly,
+ AnimationEffectReadOnly)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(KeyframeEffectReadOnly)
+NS_INTERFACE_MAP_END_INHERITING(AnimationEffectReadOnly)
+
+NS_IMPL_ADDREF_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly)
+NS_IMPL_RELEASE_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly)
+
+KeyframeEffectReadOnly::KeyframeEffectReadOnly(
+ nsIDocument* aDocument,
+ const Maybe<OwningAnimationTarget>& aTarget,
+ const TimingParams& aTiming,
+ const KeyframeEffectParams& aOptions)
+ : KeyframeEffectReadOnly(aDocument, aTarget,
+ new AnimationEffectTimingReadOnly(aDocument,
+ aTiming),
+ aOptions)
+{
+}
+
+KeyframeEffectReadOnly::KeyframeEffectReadOnly(
+ nsIDocument* aDocument,
+ const Maybe<OwningAnimationTarget>& aTarget,
+ AnimationEffectTimingReadOnly* aTiming,
+ const KeyframeEffectParams& aOptions)
+ : AnimationEffectReadOnly(aDocument, aTiming)
+ , mTarget(aTarget)
+ , mEffectOptions(aOptions)
+ , mInEffectOnLastAnimationTimingUpdate(false)
+ , mCumulativeChangeHint(nsChangeHint(0))
+{
+}
+
+JSObject*
+KeyframeEffectReadOnly::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto)
+{
+ return KeyframeEffectReadOnlyBinding::Wrap(aCx, this, aGivenProto);
+}
+
+IterationCompositeOperation
+KeyframeEffectReadOnly::IterationComposite() const
+{
+ return mEffectOptions.mIterationComposite;
+}
+
+CompositeOperation
+KeyframeEffectReadOnly::Composite() const
+{
+ return CompositeOperation::Replace;
+}
+
+void
+KeyframeEffectReadOnly::NotifyAnimationTimingUpdated()
+{
+ UpdateTargetRegistration();
+
+ // If the effect is not relevant it will be removed from the target
+ // element's effect set. However, effects not in the effect set
+ // will not be included in the set of candidate effects for running on
+ // the compositor and hence they won't have their compositor status
+ // updated. As a result, we need to make sure we clear their compositor
+ // status here.
+ bool isRelevant = mAnimation && mAnimation->IsRelevant();
+ if (!isRelevant) {
+ ResetIsRunningOnCompositor();
+ }
+
+ // Detect changes to "in effect" status since we need to recalculate the
+ // animation cascade for this element whenever that changes.
+ bool inEffect = IsInEffect();
+ if (inEffect != mInEffectOnLastAnimationTimingUpdate) {
+ MarkCascadeNeedsUpdate();
+ mInEffectOnLastAnimationTimingUpdate = inEffect;
+ }
+
+ // Request restyle if necessary.
+ if (mAnimation && !mProperties.IsEmpty() && HasComputedTimingChanged()) {
+ EffectCompositor::RestyleType restyleType =
+ CanThrottle() ?
+ EffectCompositor::RestyleType::Throttled :
+ EffectCompositor::RestyleType::Standard;
+ RequestRestyle(restyleType);
+ }
+
+ // If we're no longer "in effect", our ComposeStyle method will never be
+ // called and we will never have a chance to update mProgressOnLastCompose
+ // and mCurrentIterationOnLastCompose.
+ // We clear them here to ensure that if we later become "in effect" we will
+ // request a restyle (above).
+ if (!inEffect) {
+ mProgressOnLastCompose.SetNull();
+ mCurrentIterationOnLastCompose = 0;
+ }
+}
+
+static bool
+KeyframesEqualIgnoringComputedOffsets(const nsTArray<Keyframe>& aLhs,
+ const nsTArray<Keyframe>& aRhs)
+{
+ if (aLhs.Length() != aRhs.Length()) {
+ return false;
+ }
+
+ for (size_t i = 0, len = aLhs.Length(); i < len; ++i) {
+ const Keyframe& a = aLhs[i];
+ const Keyframe& b = aRhs[i];
+ if (a.mOffset != b.mOffset ||
+ a.mTimingFunction != b.mTimingFunction ||
+ a.mPropertyValues != b.mPropertyValues) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// https://w3c.github.io/web-animations/#dom-keyframeeffect-setkeyframes
+void
+KeyframeEffectReadOnly::SetKeyframes(JSContext* aContext,
+ JS::Handle<JSObject*> aKeyframes,
+ ErrorResult& aRv)
+{
+ nsTArray<Keyframe> keyframes =
+ KeyframeUtils::GetKeyframesFromObject(aContext, mDocument, aKeyframes, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ RefPtr<nsStyleContext> styleContext = GetTargetStyleContext();
+ SetKeyframes(Move(keyframes), styleContext);
+}
+
+void
+KeyframeEffectReadOnly::SetKeyframes(nsTArray<Keyframe>&& aKeyframes,
+ nsStyleContext* aStyleContext)
+{
+ if (KeyframesEqualIgnoringComputedOffsets(aKeyframes, mKeyframes)) {
+ return;
+ }
+
+ mKeyframes = Move(aKeyframes);
+ // Apply distribute spacing irrespective of the spacing mode. We will apply
+ // the specified spacing mode when we generate computed animation property
+ // values from the keyframes since both operations require a style context
+ // and need to be performed whenever the style context changes.
+ KeyframeUtils::ApplyDistributeSpacing(mKeyframes);
+
+ if (mAnimation && mAnimation->IsRelevant()) {
+ nsNodeUtils::AnimationChanged(mAnimation);
+ }
+
+ if (aStyleContext) {
+ UpdateProperties(aStyleContext);
+ MaybeUpdateFrameForCompositor();
+ }
+}
+
+const AnimationProperty*
+KeyframeEffectReadOnly::GetEffectiveAnimationOfProperty(
+ nsCSSPropertyID aProperty) const
+{
+ EffectSet* effectSet =
+ EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
+ for (size_t propIdx = 0, propEnd = mProperties.Length();
+ propIdx != propEnd; ++propIdx) {
+ if (aProperty == mProperties[propIdx].mProperty) {
+ const AnimationProperty* result = &mProperties[propIdx];
+ // Skip if there is a property of animation level that is overridden
+ // by !important rules.
+ if (effectSet &&
+ effectSet->PropertiesWithImportantRules()
+ .HasProperty(result->mProperty) &&
+ effectSet->PropertiesForAnimationsLevel()
+ .HasProperty(result->mProperty)) {
+ result = nullptr;
+ }
+ return result;
+ }
+ }
+ return nullptr;
+}
+
+bool
+KeyframeEffectReadOnly::HasAnimationOfProperty(nsCSSPropertyID aProperty) const
+{
+ for (const AnimationProperty& property : mProperties) {
+ if (property.mProperty == aProperty) {
+ return true;
+ }
+ }
+ return false;
+}
+
+#ifdef DEBUG
+bool
+SpecifiedKeyframeArraysAreEqual(const nsTArray<Keyframe>& aA,
+ const nsTArray<Keyframe>& aB)
+{
+ if (aA.Length() != aB.Length()) {
+ return false;
+ }
+
+ for (size_t i = 0; i < aA.Length(); i++) {
+ const Keyframe& a = aA[i];
+ const Keyframe& b = aB[i];
+ if (a.mOffset != b.mOffset ||
+ a.mTimingFunction != b.mTimingFunction ||
+ a.mPropertyValues != b.mPropertyValues) {
+ return false;
+ }
+ }
+
+ return true;
+}
+#endif
+
+void
+KeyframeEffectReadOnly::UpdateProperties(nsStyleContext* aStyleContext)
+{
+ MOZ_ASSERT(aStyleContext);
+
+ nsTArray<AnimationProperty> properties = BuildProperties(aStyleContext);
+
+ if (mProperties == properties) {
+ return;
+ }
+
+ // Preserve the state of the mIsRunningOnCompositor flag.
+ nsCSSPropertyIDSet runningOnCompositorProperties;
+
+ for (const AnimationProperty& property : mProperties) {
+ if (property.mIsRunningOnCompositor) {
+ runningOnCompositorProperties.AddProperty(property.mProperty);
+ }
+ }
+
+ mProperties = Move(properties);
+
+ for (AnimationProperty& property : mProperties) {
+ property.mIsRunningOnCompositor =
+ runningOnCompositorProperties.HasProperty(property.mProperty);
+ }
+
+ // FIXME (bug 1303235): Do this for Servo too
+ if (aStyleContext->PresContext()->StyleSet()->IsGecko()) {
+ CalculateCumulativeChangeHint(aStyleContext);
+ }
+
+ MarkCascadeNeedsUpdate();
+
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+}
+
+void
+KeyframeEffectReadOnly::ComposeStyle(
+ RefPtr<AnimValuesStyleRule>& aStyleRule,
+ const nsCSSPropertyIDSet& aPropertiesToSkip)
+{
+ ComputedTiming computedTiming = GetComputedTiming();
+ mProgressOnLastCompose = computedTiming.mProgress;
+ mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
+
+ // If the progress is null, we don't have fill data for the current
+ // time so we shouldn't animate.
+ if (computedTiming.mProgress.IsNull()) {
+ return;
+ }
+
+ for (size_t propIdx = 0, propEnd = mProperties.Length();
+ propIdx != propEnd; ++propIdx)
+ {
+ const AnimationProperty& prop = mProperties[propIdx];
+
+ MOZ_ASSERT(prop.mSegments[0].mFromKey == 0.0, "incorrect first from key");
+ MOZ_ASSERT(prop.mSegments[prop.mSegments.Length() - 1].mToKey == 1.0,
+ "incorrect last to key");
+
+ if (aPropertiesToSkip.HasProperty(prop.mProperty)) {
+ continue;
+ }
+
+ MOZ_ASSERT(prop.mSegments.Length() > 0,
+ "property should not be in animations if it has no segments");
+
+ // FIXME: Maybe cache the current segment?
+ const AnimationPropertySegment *segment = prop.mSegments.Elements(),
+ *segmentEnd = segment + prop.mSegments.Length();
+ while (segment->mToKey <= computedTiming.mProgress.Value()) {
+ MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys");
+ if ((segment+1) == segmentEnd) {
+ break;
+ }
+ ++segment;
+ MOZ_ASSERT(segment->mFromKey == (segment-1)->mToKey, "incorrect keys");
+ }
+ MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys");
+ MOZ_ASSERT(segment >= prop.mSegments.Elements() &&
+ size_t(segment - prop.mSegments.Elements()) <
+ prop.mSegments.Length(),
+ "out of array bounds");
+
+ if (!aStyleRule) {
+ // Allocate the style rule now that we know we have animation data.
+ aStyleRule = new AnimValuesStyleRule();
+ }
+
+ StyleAnimationValue fromValue = segment->mFromValue;
+ StyleAnimationValue toValue = segment->mToValue;
+ // Iteration composition for accumulate
+ if (mEffectOptions.mIterationComposite ==
+ IterationCompositeOperation::Accumulate &&
+ computedTiming.mCurrentIteration > 0) {
+ const AnimationPropertySegment& lastSegment =
+ prop.mSegments.LastElement();
+ // FIXME: Bug 1293492: Add a utility function to calculate both of
+ // below StyleAnimationValues.
+ DebugOnly<bool> accumulateResult =
+ StyleAnimationValue::Accumulate(prop.mProperty,
+ fromValue,
+ lastSegment.mToValue,
+ computedTiming.mCurrentIteration);
+ // We can't check the accumulation result in case of filter property.
+ // That's because some filter property can't accumulate,
+ // e.g. 'contrast(2) brightness(2)' onto 'brightness(1) contrast(1)'
+ // because of mismatch of the order.
+ MOZ_ASSERT(accumulateResult || prop.mProperty == eCSSProperty_filter,
+ "could not accumulate value");
+ accumulateResult =
+ StyleAnimationValue::Accumulate(prop.mProperty,
+ toValue,
+ lastSegment.mToValue,
+ computedTiming.mCurrentIteration);
+ MOZ_ASSERT(accumulateResult || prop.mProperty == eCSSProperty_filter,
+ "could not accumulate value");
+ }
+
+ // Special handling for zero-length segments
+ if (segment->mToKey == segment->mFromKey) {
+ if (computedTiming.mProgress.Value() < 0) {
+ aStyleRule->AddValue(prop.mProperty, Move(fromValue));
+ } else {
+ aStyleRule->AddValue(prop.mProperty, Move(toValue));
+ }
+ continue;
+ }
+
+ double positionInSegment =
+ (computedTiming.mProgress.Value() - segment->mFromKey) /
+ (segment->mToKey - segment->mFromKey);
+ double valuePosition =
+ ComputedTimingFunction::GetPortion(segment->mTimingFunction,
+ positionInSegment,
+ computedTiming.mBeforeFlag);
+
+ MOZ_ASSERT(IsFinite(valuePosition), "Position value should be finite");
+ StyleAnimationValue val;
+ if (StyleAnimationValue::Interpolate(prop.mProperty,
+ fromValue,
+ toValue,
+ valuePosition, val)) {
+ aStyleRule->AddValue(prop.mProperty, Move(val));
+ } else if (valuePosition < 0.5) {
+ aStyleRule->AddValue(prop.mProperty, Move(fromValue));
+ } else {
+ aStyleRule->AddValue(prop.mProperty, Move(toValue));
+ }
+ }
+}
+
+bool
+KeyframeEffectReadOnly::IsRunningOnCompositor() const
+{
+ // We consider animation is running on compositor if there is at least
+ // one property running on compositor.
+ // Animation.IsRunningOnCompotitor will return more fine grained
+ // information in bug 1196114.
+ for (const AnimationProperty& property : mProperties) {
+ if (property.mIsRunningOnCompositor) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void
+KeyframeEffectReadOnly::SetIsRunningOnCompositor(nsCSSPropertyID aProperty,
+ bool aIsRunning)
+{
+ MOZ_ASSERT(nsCSSProps::PropHasFlags(aProperty,
+ CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR),
+ "Property being animated on compositor is a recognized "
+ "compositor-animatable property");
+ for (AnimationProperty& property : mProperties) {
+ if (property.mProperty == aProperty) {
+ property.mIsRunningOnCompositor = aIsRunning;
+ // We currently only set a performance warning message when animations
+ // cannot be run on the compositor, so if this animation is running
+ // on the compositor we don't need a message.
+ if (aIsRunning) {
+ property.mPerformanceWarning.reset();
+ }
+ return;
+ }
+ }
+}
+
+void
+KeyframeEffectReadOnly::ResetIsRunningOnCompositor()
+{
+ for (AnimationProperty& property : mProperties) {
+ property.mIsRunningOnCompositor = false;
+ }
+}
+
+static const KeyframeEffectOptions&
+KeyframeEffectOptionsFromUnion(
+ const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions)
+{
+ MOZ_ASSERT(aOptions.IsKeyframeEffectOptions());
+ return aOptions.GetAsKeyframeEffectOptions();
+}
+
+static const KeyframeEffectOptions&
+KeyframeEffectOptionsFromUnion(
+ const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions)
+{
+ MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions());
+ return aOptions.GetAsKeyframeAnimationOptions();
+}
+
+template <class OptionsType>
+static KeyframeEffectParams
+KeyframeEffectParamsFromUnion(const OptionsType& aOptions,
+ nsAString& aInvalidPacedProperty,
+ ErrorResult& aRv)
+{
+ KeyframeEffectParams result;
+ if (!aOptions.IsUnrestrictedDouble()) {
+ const KeyframeEffectOptions& options =
+ KeyframeEffectOptionsFromUnion(aOptions);
+ KeyframeEffectParams::ParseSpacing(options.mSpacing,
+ result.mSpacingMode,
+ result.mPacedProperty,
+ aInvalidPacedProperty,
+ aRv);
+ // Ignore iterationComposite if the Web Animations API is not enabled,
+ // then the default value 'Replace' will be used.
+ if (AnimationUtils::IsCoreAPIEnabledForCaller()) {
+ result.mIterationComposite = options.mIterationComposite;
+ }
+ }
+ return result;
+}
+
+/* static */ Maybe<OwningAnimationTarget>
+KeyframeEffectReadOnly::ConvertTarget(
+ const Nullable<ElementOrCSSPseudoElement>& aTarget)
+{
+ // Return value optimization.
+ Maybe<OwningAnimationTarget> result;
+
+ if (aTarget.IsNull()) {
+ return result;
+ }
+
+ const ElementOrCSSPseudoElement& target = aTarget.Value();
+ MOZ_ASSERT(target.IsElement() || target.IsCSSPseudoElement(),
+ "Uninitialized target");
+
+ if (target.IsElement()) {
+ result.emplace(&target.GetAsElement());
+ } else {
+ RefPtr<Element> elem = target.GetAsCSSPseudoElement().ParentElement();
+ result.emplace(elem, target.GetAsCSSPseudoElement().GetType());
+ }
+ return result;
+}
+
+template <class KeyframeEffectType, class OptionsType>
+/* static */ already_AddRefed<KeyframeEffectType>
+KeyframeEffectReadOnly::ConstructKeyframeEffect(
+ const GlobalObject& aGlobal,
+ const Nullable<ElementOrCSSPseudoElement>& aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const OptionsType& aOptions,
+ ErrorResult& aRv)
+{
+ nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
+ if (!doc) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ TimingParams timingParams =
+ TimingParams::FromOptionsUnion(aOptions, doc, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ nsAutoString invalidPacedProperty;
+ KeyframeEffectParams effectOptions =
+ KeyframeEffectParamsFromUnion(aOptions, invalidPacedProperty, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (!invalidPacedProperty.IsEmpty()) {
+ const char16_t* params[] = { invalidPacedProperty.get() };
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_CSTRING("Animation"),
+ doc,
+ nsContentUtils::eDOM_PROPERTIES,
+ "UnanimatablePacedProperty",
+ params, ArrayLength(params));
+ }
+
+ Maybe<OwningAnimationTarget> target = ConvertTarget(aTarget);
+ RefPtr<KeyframeEffectType> effect =
+ new KeyframeEffectType(doc, target, timingParams, effectOptions);
+
+ effect->SetKeyframes(aGlobal.Context(), aKeyframes, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ return effect.forget();
+}
+
+template<class KeyframeEffectType>
+/* static */ already_AddRefed<KeyframeEffectType>
+KeyframeEffectReadOnly::ConstructKeyframeEffect(const GlobalObject& aGlobal,
+ KeyframeEffectReadOnly& aSource,
+ ErrorResult& aRv)
+{
+ nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
+ if (!doc) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // Create a new KeyframeEffectReadOnly object with aSource's target,
+ // iteration composite operation, composite operation, and spacing mode.
+ // The constructor creates a new AnimationEffect(ReadOnly) object by
+ // aSource's TimingParams.
+ // Note: we don't need to re-throw exceptions since the value specified on
+ // aSource's timing object can be assumed valid.
+ RefPtr<KeyframeEffectType> effect =
+ new KeyframeEffectType(doc,
+ aSource.mTarget,
+ aSource.SpecifiedTiming(),
+ aSource.mEffectOptions);
+ // Copy cumulative change hint. mCumulativeChangeHint should be the same as
+ // the source one because both of targets are the same.
+ effect->mCumulativeChangeHint = aSource.mCumulativeChangeHint;
+
+ // Copy aSource's keyframes and animation properties.
+ // Note: We don't call SetKeyframes directly, which might revise the
+ // computed offsets and rebuild the animation properties.
+ // FIXME: Bug 1314537: We have to make sure SharedKeyframeList is handled
+ // properly.
+ effect->mKeyframes = aSource.mKeyframes;
+ effect->mProperties = aSource.mProperties;
+ return effect.forget();
+}
+
+nsTArray<AnimationProperty>
+KeyframeEffectReadOnly::BuildProperties(nsStyleContext* aStyleContext)
+{
+ MOZ_ASSERT(aStyleContext);
+
+ nsTArray<AnimationProperty> result;
+ // If mTarget is null, return an empty property array.
+ if (!mTarget) {
+ return result;
+ }
+
+ // When GetComputedKeyframeValues or GetAnimationPropertiesFromKeyframes
+ // calculate computed values from |mKeyframes|, they could possibly
+ // trigger a subsequent restyle in which we rebuild animations. If that
+ // happens we could find that |mKeyframes| is overwritten while it is
+ // being iterated over. Normally that shouldn't happen but just in case we
+ // make a copy of |mKeyframes| first and iterate over that instead.
+ auto keyframesCopy(mKeyframes);
+
+ nsTArray<ComputedKeyframeValues> computedValues =
+ KeyframeUtils::GetComputedKeyframeValues(keyframesCopy,
+ mTarget->mElement,
+ aStyleContext);
+
+ if (mEffectOptions.mSpacingMode == SpacingMode::paced) {
+ KeyframeUtils::ApplySpacing(keyframesCopy, SpacingMode::paced,
+ mEffectOptions.mPacedProperty,
+ computedValues, aStyleContext);
+ }
+
+ result = KeyframeUtils::GetAnimationPropertiesFromKeyframes(keyframesCopy,
+ computedValues,
+ aStyleContext);
+
+#ifdef DEBUG
+ MOZ_ASSERT(SpecifiedKeyframeArraysAreEqual(mKeyframes, keyframesCopy),
+ "Apart from the computed offset members, the keyframes array"
+ " should not be modified");
+#endif
+
+ mKeyframes.SwapElements(keyframesCopy);
+ return result;
+}
+
+void
+KeyframeEffectReadOnly::UpdateTargetRegistration()
+{
+ if (!mTarget) {
+ return;
+ }
+
+ bool isRelevant = mAnimation && mAnimation->IsRelevant();
+
+ // Animation::IsRelevant() returns a cached value. It only updates when
+ // something calls Animation::UpdateRelevance. Whenever our timing changes,
+ // we should be notifying our Animation before calling this, so
+ // Animation::IsRelevant() should be up-to-date by the time we get here.
+ MOZ_ASSERT(isRelevant == IsCurrent() || IsInEffect(),
+ "Out of date Animation::IsRelevant value");
+
+ if (isRelevant) {
+ EffectSet* effectSet =
+ EffectSet::GetOrCreateEffectSet(mTarget->mElement, mTarget->mPseudoType);
+ effectSet->AddEffect(*this);
+ } else {
+ UnregisterTarget();
+ }
+}
+
+void
+KeyframeEffectReadOnly::UnregisterTarget()
+{
+ EffectSet* effectSet =
+ EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
+ if (effectSet) {
+ effectSet->RemoveEffect(*this);
+ if (effectSet->IsEmpty()) {
+ EffectSet::DestroyEffectSet(mTarget->mElement, mTarget->mPseudoType);
+ }
+ }
+}
+
+void
+KeyframeEffectReadOnly::RequestRestyle(
+ EffectCompositor::RestyleType aRestyleType)
+{
+ nsPresContext* presContext = GetPresContext();
+ if (presContext && mTarget && mAnimation) {
+ presContext->EffectCompositor()->
+ RequestRestyle(mTarget->mElement, mTarget->mPseudoType,
+ aRestyleType, mAnimation->CascadeLevel());
+ }
+}
+
+already_AddRefed<nsStyleContext>
+KeyframeEffectReadOnly::GetTargetStyleContext()
+{
+ nsIPresShell* shell = GetPresShell();
+ if (!shell) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(mTarget,
+ "Should only have a presshell when we have a target element");
+
+ nsIAtom* pseudo = mTarget->mPseudoType < CSSPseudoElementType::Count
+ ? nsCSSPseudoElements::GetPseudoAtom(mTarget->mPseudoType)
+ : nullptr;
+ return nsComputedDOMStyle::GetStyleContextForElement(mTarget->mElement,
+ pseudo, shell);
+}
+
+#ifdef DEBUG
+void
+DumpAnimationProperties(nsTArray<AnimationProperty>& aAnimationProperties)
+{
+ for (auto& p : aAnimationProperties) {
+ printf("%s\n", nsCSSProps::GetStringValue(p.mProperty).get());
+ for (auto& s : p.mSegments) {
+ nsString fromValue, toValue;
+ Unused << StyleAnimationValue::UncomputeValue(p.mProperty,
+ s.mFromValue,
+ fromValue);
+ Unused << StyleAnimationValue::UncomputeValue(p.mProperty,
+ s.mToValue,
+ toValue);
+ printf(" %f..%f: %s..%s\n", s.mFromKey, s.mToKey,
+ NS_ConvertUTF16toUTF8(fromValue).get(),
+ NS_ConvertUTF16toUTF8(toValue).get());
+ }
+ }
+}
+#endif
+
+/* static */ already_AddRefed<KeyframeEffectReadOnly>
+KeyframeEffectReadOnly::Constructor(
+ const GlobalObject& aGlobal,
+ const Nullable<ElementOrCSSPseudoElement>& aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
+ ErrorResult& aRv)
+{
+ return ConstructKeyframeEffect<KeyframeEffectReadOnly>(aGlobal, aTarget,
+ aKeyframes, aOptions,
+ aRv);
+}
+
+/* static */ already_AddRefed<KeyframeEffectReadOnly>
+KeyframeEffectReadOnly::Constructor(const GlobalObject& aGlobal,
+ KeyframeEffectReadOnly& aSource,
+ ErrorResult& aRv)
+{
+ return ConstructKeyframeEffect<KeyframeEffectReadOnly>(aGlobal, aSource, aRv);
+}
+
+void
+KeyframeEffectReadOnly::GetTarget(
+ Nullable<OwningElementOrCSSPseudoElement>& aRv) const
+{
+ if (!mTarget) {
+ aRv.SetNull();
+ return;
+ }
+
+ switch (mTarget->mPseudoType) {
+ case CSSPseudoElementType::before:
+ case CSSPseudoElementType::after:
+ aRv.SetValue().SetAsCSSPseudoElement() =
+ CSSPseudoElement::GetCSSPseudoElement(mTarget->mElement,
+ mTarget->mPseudoType);
+ break;
+
+ case CSSPseudoElementType::NotPseudo:
+ aRv.SetValue().SetAsElement() = mTarget->mElement;
+ break;
+
+ default:
+ NS_NOTREACHED("Animation of unsupported pseudo-type");
+ aRv.SetNull();
+ }
+}
+
+static void
+CreatePropertyValue(nsCSSPropertyID aProperty,
+ float aOffset,
+ const Maybe<ComputedTimingFunction>& aTimingFunction,
+ const StyleAnimationValue& aValue,
+ AnimationPropertyValueDetails& aResult)
+{
+ aResult.mOffset = aOffset;
+
+ nsString stringValue;
+ DebugOnly<bool> uncomputeResult =
+ StyleAnimationValue::UncomputeValue(aProperty, aValue, stringValue);
+ MOZ_ASSERT(uncomputeResult, "failed to uncompute value");
+ aResult.mValue = stringValue;
+
+ if (aTimingFunction) {
+ aResult.mEasing.Construct();
+ aTimingFunction->AppendToString(aResult.mEasing.Value());
+ } else {
+ aResult.mEasing.Construct(NS_LITERAL_STRING("linear"));
+ }
+
+ aResult.mComposite = CompositeOperation::Replace;
+}
+
+void
+KeyframeEffectReadOnly::GetProperties(
+ nsTArray<AnimationPropertyDetails>& aProperties,
+ ErrorResult& aRv) const
+{
+ for (const AnimationProperty& property : mProperties) {
+ AnimationPropertyDetails propertyDetails;
+ propertyDetails.mProperty =
+ NS_ConvertASCIItoUTF16(nsCSSProps::GetStringValue(property.mProperty));
+ propertyDetails.mRunningOnCompositor = property.mIsRunningOnCompositor;
+
+ nsXPIDLString localizedString;
+ if (property.mPerformanceWarning &&
+ property.mPerformanceWarning->ToLocalizedString(localizedString)) {
+ propertyDetails.mWarning.Construct(localizedString);
+ }
+
+ if (!propertyDetails.mValues.SetCapacity(property.mSegments.Length(),
+ mozilla::fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ for (size_t segmentIdx = 0, segmentLen = property.mSegments.Length();
+ segmentIdx < segmentLen;
+ segmentIdx++)
+ {
+ const AnimationPropertySegment& segment = property.mSegments[segmentIdx];
+
+ binding_detail::FastAnimationPropertyValueDetails fromValue;
+ CreatePropertyValue(property.mProperty, segment.mFromKey,
+ segment.mTimingFunction, segment.mFromValue,
+ fromValue);
+ // We don't apply timing functions for zero-length segments, so
+ // don't return one here.
+ if (segment.mFromKey == segment.mToKey) {
+ fromValue.mEasing.Reset();
+ }
+ // The following won't fail since we have already allocated the capacity
+ // above.
+ propertyDetails.mValues.AppendElement(fromValue, mozilla::fallible);
+
+ // Normally we can ignore the to-value for this segment since it is
+ // identical to the from-value from the next segment. However, we need
+ // to add it if either:
+ // a) this is the last segment, or
+ // b) the next segment's from-value differs.
+ if (segmentIdx == segmentLen - 1 ||
+ property.mSegments[segmentIdx + 1].mFromValue != segment.mToValue) {
+ binding_detail::FastAnimationPropertyValueDetails toValue;
+ CreatePropertyValue(property.mProperty, segment.mToKey,
+ Nothing(), segment.mToValue, toValue);
+ // It doesn't really make sense to have a timing function on the
+ // last property value or before a sudden jump so we just drop the
+ // easing property altogether.
+ toValue.mEasing.Reset();
+ propertyDetails.mValues.AppendElement(toValue, mozilla::fallible);
+ }
+ }
+
+ aProperties.AppendElement(propertyDetails);
+ }
+}
+
+void
+KeyframeEffectReadOnly::GetKeyframes(JSContext*& aCx,
+ nsTArray<JSObject*>& aResult,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(aResult.IsEmpty());
+ MOZ_ASSERT(!aRv.Failed());
+
+ if (!aResult.SetCapacity(mKeyframes.Length(), mozilla::fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ for (const Keyframe& keyframe : mKeyframes) {
+ // Set up a dictionary object for the explicit members
+ BaseComputedKeyframe keyframeDict;
+ if (keyframe.mOffset) {
+ keyframeDict.mOffset.SetValue(keyframe.mOffset.value());
+ }
+ MOZ_ASSERT(keyframe.mComputedOffset != Keyframe::kComputedOffsetNotSet,
+ "Invalid computed offset");
+ keyframeDict.mComputedOffset.Construct(keyframe.mComputedOffset);
+ if (keyframe.mTimingFunction) {
+ keyframeDict.mEasing.Truncate();
+ keyframe.mTimingFunction.ref().AppendToString(keyframeDict.mEasing);
+ } // else if null, leave easing as its default "linear".
+
+ JS::Rooted<JS::Value> keyframeJSValue(aCx);
+ if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ JS::Rooted<JSObject*> keyframeObject(aCx, &keyframeJSValue.toObject());
+ for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) {
+
+ const char* name = nsCSSProps::PropertyIDLName(propertyValue.mProperty);
+
+ // nsCSSValue::AppendToString does not accept shorthands properties but
+ // works with token stream values if we pass eCSSProperty_UNKNOWN as
+ // the property.
+ nsCSSPropertyID propertyForSerializing =
+ nsCSSProps::IsShorthand(propertyValue.mProperty)
+ ? eCSSProperty_UNKNOWN
+ : propertyValue.mProperty;
+
+ nsAutoString stringValue;
+ if (propertyValue.mServoDeclarationBlock) {
+ Servo_DeclarationBlock_SerializeOneValue(
+ propertyValue.mServoDeclarationBlock, &stringValue);
+ } else {
+ propertyValue.mValue.AppendToString(
+ propertyForSerializing, stringValue, nsCSSValue::eNormalized);
+ }
+
+ JS::Rooted<JS::Value> value(aCx);
+ if (!ToJSValue(aCx, stringValue, &value) ||
+ !JS_DefineProperty(aCx, keyframeObject, name, value,
+ JSPROP_ENUMERATE)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ }
+
+ aResult.AppendElement(keyframeObject);
+ }
+}
+
+/* static */ const TimeDuration
+KeyframeEffectReadOnly::OverflowRegionRefreshInterval()
+{
+ // The amount of time we can wait between updating throttled animations
+ // on the main thread that influence the overflow region.
+ static const TimeDuration kOverflowRegionRefreshInterval =
+ TimeDuration::FromMilliseconds(200);
+
+ return kOverflowRegionRefreshInterval;
+}
+
+bool
+KeyframeEffectReadOnly::CanThrottle() const
+{
+ // Unthrottle if we are not in effect or current. This will be the case when
+ // our owning animation has finished, is idle, or when we are in the delay
+ // phase (but without a backwards fill). In each case the computed progress
+ // value produced on each tick will be the same so we will skip requesting
+ // unnecessary restyles in NotifyAnimationTimingUpdated. Any calls we *do* get
+ // here will be because of a change in state (e.g. we are newly finished or
+ // newly no longer in effect) in which case we shouldn't throttle the sample.
+ if (!IsInEffect() || !IsCurrent()) {
+ return false;
+ }
+
+ nsIFrame* frame = GetAnimationFrame();
+ if (!frame) {
+ // There are two possible cases here.
+ // a) No target element
+ // b) The target element has no frame, e.g. because it is in a display:none
+ // subtree.
+ // In either case we can throttle the animation because there is no
+ // need to update on the main thread.
+ return true;
+ }
+
+ // We can throttle the animation if the animation is paint only and
+ // the target frame is out of view or the document is in background tabs.
+ if (CanIgnoreIfNotVisible()) {
+ nsIPresShell* presShell = GetPresShell();
+ if ((presShell && !presShell->IsActive()) ||
+ frame->IsScrolledOutOfView()) {
+ return true;
+ }
+ }
+
+ // First we need to check layer generation and transform overflow
+ // prior to the property.mIsRunningOnCompositor check because we should
+ // occasionally unthrottle these animations even if the animations are
+ // already running on compositor.
+ for (const LayerAnimationInfo::Record& record :
+ LayerAnimationInfo::sRecords) {
+ // Skip properties that are overridden by !important rules.
+ // (GetEffectiveAnimationOfProperty, as called by
+ // HasEffectiveAnimationOfProperty, only returns a property which is
+ // neither overridden by !important rules nor overridden by other
+ // animation.)
+ if (!HasEffectiveAnimationOfProperty(record.mProperty)) {
+ continue;
+ }
+
+ EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
+ mTarget->mPseudoType);
+ MOZ_ASSERT(effectSet, "CanThrottle should be called on an effect "
+ "associated with a target element");
+ layers::Layer* layer =
+ FrameLayerBuilder::GetDedicatedLayer(frame, record.mLayerType);
+ // Unthrottle if the layer needs to be brought up to date
+ if (!layer ||
+ effectSet->GetAnimationGeneration() !=
+ layer->GetAnimationGeneration()) {
+ return false;
+ }
+
+ // If this is a transform animation that affects the overflow region,
+ // we should unthrottle the animation periodically.
+ if (record.mProperty == eCSSProperty_transform &&
+ !CanThrottleTransformChanges(*frame)) {
+ return false;
+ }
+ }
+
+ for (const AnimationProperty& property : mProperties) {
+ if (!property.mIsRunningOnCompositor) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool
+KeyframeEffectReadOnly::CanThrottleTransformChanges(nsIFrame& aFrame) const
+{
+ // If we know that the animation cannot cause overflow,
+ // we can just disable flushes for this animation.
+
+ // If we don't show scrollbars, we don't care about overflow.
+ if (LookAndFeel::GetInt(LookAndFeel::eIntID_ShowHideScrollbars) == 0) {
+ return true;
+ }
+
+ nsPresContext* presContext = GetPresContext();
+ // CanThrottleTransformChanges is only called as part of a refresh driver tick
+ // in which case we expect to has a pres context.
+ MOZ_ASSERT(presContext);
+
+ TimeStamp now =
+ presContext->RefreshDriver()->MostRecentRefresh();
+
+ EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
+ mTarget->mPseudoType);
+ MOZ_ASSERT(effectSet, "CanThrottleTransformChanges is expected to be called"
+ " on an effect in an effect set");
+ MOZ_ASSERT(mAnimation, "CanThrottleTransformChanges is expected to be called"
+ " on an effect with a parent animation");
+ TimeStamp animationRuleRefreshTime =
+ effectSet->AnimationRuleRefreshTime(mAnimation->CascadeLevel());
+ // If this animation can cause overflow, we can throttle some of the ticks.
+ if (!animationRuleRefreshTime.IsNull() &&
+ (now - animationRuleRefreshTime) < OverflowRegionRefreshInterval()) {
+ return true;
+ }
+
+ // If the nearest scrollable ancestor has overflow:hidden,
+ // we don't care about overflow.
+ nsIScrollableFrame* scrollable =
+ nsLayoutUtils::GetNearestScrollableFrame(&aFrame);
+ if (!scrollable) {
+ return true;
+ }
+
+ ScrollbarStyles ss = scrollable->GetScrollbarStyles();
+ if (ss.mVertical == NS_STYLE_OVERFLOW_HIDDEN &&
+ ss.mHorizontal == NS_STYLE_OVERFLOW_HIDDEN &&
+ scrollable->GetLogicalScrollPosition() == nsPoint(0, 0)) {
+ return true;
+ }
+
+ return false;
+}
+
+nsIFrame*
+KeyframeEffectReadOnly::GetAnimationFrame() const
+{
+ if (!mTarget) {
+ return nullptr;
+ }
+
+ nsIFrame* frame = mTarget->mElement->GetPrimaryFrame();
+ if (!frame) {
+ return nullptr;
+ }
+
+ if (mTarget->mPseudoType == CSSPseudoElementType::before) {
+ frame = nsLayoutUtils::GetBeforeFrame(frame);
+ } else if (mTarget->mPseudoType == CSSPseudoElementType::after) {
+ frame = nsLayoutUtils::GetAfterFrame(frame);
+ } else {
+ MOZ_ASSERT(mTarget->mPseudoType == CSSPseudoElementType::NotPseudo,
+ "unknown mTarget->mPseudoType");
+ }
+ if (!frame) {
+ return nullptr;
+ }
+
+ return nsLayoutUtils::GetStyleFrame(frame);
+}
+
+nsIDocument*
+KeyframeEffectReadOnly::GetRenderedDocument() const
+{
+ if (!mTarget) {
+ return nullptr;
+ }
+ return mTarget->mElement->GetComposedDoc();
+}
+
+nsIPresShell*
+KeyframeEffectReadOnly::GetPresShell() const
+{
+ nsIDocument* doc = GetRenderedDocument();
+ if (!doc) {
+ return nullptr;
+ }
+ return doc->GetShell();
+}
+
+nsPresContext*
+KeyframeEffectReadOnly::GetPresContext() const
+{
+ nsIPresShell* shell = GetPresShell();
+ if (!shell) {
+ return nullptr;
+ }
+ return shell->GetPresContext();
+}
+
+/* static */ bool
+KeyframeEffectReadOnly::IsGeometricProperty(
+ const nsCSSPropertyID aProperty)
+{
+ switch (aProperty) {
+ case eCSSProperty_bottom:
+ case eCSSProperty_height:
+ case eCSSProperty_left:
+ case eCSSProperty_right:
+ case eCSSProperty_top:
+ case eCSSProperty_width:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/* static */ bool
+KeyframeEffectReadOnly::CanAnimateTransformOnCompositor(
+ const nsIFrame* aFrame,
+ AnimationPerformanceWarning::Type& aPerformanceWarning)
+{
+ // Disallow OMTA for preserve-3d transform. Note that we check the style property
+ // rather than Extend3DContext() since that can recurse back into this function
+ // via HasOpacity(). See bug 779598.
+ if (aFrame->Combines3DTransformWithAncestors() ||
+ aFrame->StyleDisplay()->mTransformStyle == NS_STYLE_TRANSFORM_STYLE_PRESERVE_3D) {
+ aPerformanceWarning = AnimationPerformanceWarning::Type::TransformPreserve3D;
+ return false;
+ }
+ // Note that testing BackfaceIsHidden() is not a sufficient test for
+ // what we need for animating backface-visibility correctly if we
+ // remove the above test for Extend3DContext(); that would require
+ // looking at backface-visibility on descendants as well. See bug 1186204.
+ if (aFrame->BackfaceIsHidden()) {
+ aPerformanceWarning =
+ AnimationPerformanceWarning::Type::TransformBackfaceVisibilityHidden;
+ return false;
+ }
+ // Async 'transform' animations of aFrames with SVG transforms is not
+ // supported. See bug 779599.
+ if (aFrame->IsSVGTransformed()) {
+ aPerformanceWarning = AnimationPerformanceWarning::Type::TransformSVG;
+ return false;
+ }
+
+ return true;
+}
+
+bool
+KeyframeEffectReadOnly::ShouldBlockAsyncTransformAnimations(
+ const nsIFrame* aFrame,
+ AnimationPerformanceWarning::Type& aPerformanceWarning) const
+{
+ // We currently only expect this method to be called for effects whose
+ // animations are eligible for the compositor since, Animations that are
+ // paused, zero-duration, finished etc. should not block other animations from
+ // running on the compositor.
+ MOZ_ASSERT(mAnimation && mAnimation->IsPlayableOnCompositor());
+
+ EffectSet* effectSet =
+ EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
+ for (const AnimationProperty& property : mProperties) {
+ // If there is a property for animations level that is overridden by
+ // !important rules, it should not block other animations from running
+ // on the compositor.
+ // NOTE: We don't currently check for !important rules for properties that
+ // don't run on the compositor. As result such properties (e.g. margin-left)
+ // can still block async animations even if they are overridden by
+ // !important rules.
+ if (effectSet &&
+ effectSet->PropertiesWithImportantRules()
+ .HasProperty(property.mProperty) &&
+ effectSet->PropertiesForAnimationsLevel()
+ .HasProperty(property.mProperty)) {
+ continue;
+ }
+ // Check for geometric properties
+ if (IsGeometricProperty(property.mProperty)) {
+ aPerformanceWarning =
+ AnimationPerformanceWarning::Type::TransformWithGeometricProperties;
+ return true;
+ }
+
+ // Check for unsupported transform animations
+ if (property.mProperty == eCSSProperty_transform) {
+ if (!CanAnimateTransformOnCompositor(aFrame,
+ aPerformanceWarning)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+void
+KeyframeEffectReadOnly::SetPerformanceWarning(
+ nsCSSPropertyID aProperty,
+ const AnimationPerformanceWarning& aWarning)
+{
+ for (AnimationProperty& property : mProperties) {
+ if (property.mProperty == aProperty &&
+ (!property.mPerformanceWarning ||
+ *property.mPerformanceWarning != aWarning)) {
+ property.mPerformanceWarning = Some(aWarning);
+
+ nsXPIDLString localizedString;
+ if (nsLayoutUtils::IsAnimationLoggingEnabled() &&
+ property.mPerformanceWarning->ToLocalizedString(localizedString)) {
+ nsAutoCString logMessage = NS_ConvertUTF16toUTF8(localizedString);
+ AnimationUtils::LogAsyncAnimationFailure(logMessage, mTarget->mElement);
+ }
+ return;
+ }
+ }
+}
+
+static already_AddRefed<nsStyleContext>
+CreateStyleContextForAnimationValue(nsCSSPropertyID aProperty,
+ const StyleAnimationValue& aValue,
+ nsStyleContext* aBaseStyleContext)
+{
+ MOZ_ASSERT(aBaseStyleContext,
+ "CreateStyleContextForAnimationValue needs to be called "
+ "with a valid nsStyleContext");
+
+ RefPtr<AnimValuesStyleRule> styleRule = new AnimValuesStyleRule();
+ styleRule->AddValue(aProperty, aValue);
+
+ nsCOMArray<nsIStyleRule> rules;
+ rules.AppendObject(styleRule);
+
+ MOZ_ASSERT(aBaseStyleContext->PresContext()->StyleSet()->IsGecko(),
+ "ServoStyleSet should not use StyleAnimationValue for animations");
+ nsStyleSet* styleSet =
+ aBaseStyleContext->PresContext()->StyleSet()->AsGecko();
+
+ RefPtr<nsStyleContext> styleContext =
+ styleSet->ResolveStyleByAddingRules(aBaseStyleContext, rules);
+
+ // We need to call StyleData to generate cached data for the style context.
+ // Otherwise CalcStyleDifference returns no meaningful result.
+ styleContext->StyleData(nsCSSProps::kSIDTable[aProperty]);
+
+ return styleContext.forget();
+}
+
+void
+KeyframeEffectReadOnly::CalculateCumulativeChangeHint(
+ nsStyleContext *aStyleContext)
+{
+ mCumulativeChangeHint = nsChangeHint(0);
+
+ for (const AnimationProperty& property : mProperties) {
+ for (const AnimationPropertySegment& segment : property.mSegments) {
+ RefPtr<nsStyleContext> fromContext =
+ CreateStyleContextForAnimationValue(property.mProperty,
+ segment.mFromValue, aStyleContext);
+
+ RefPtr<nsStyleContext> toContext =
+ CreateStyleContextForAnimationValue(property.mProperty,
+ segment.mToValue, aStyleContext);
+
+ uint32_t equalStructs = 0;
+ uint32_t samePointerStructs = 0;
+ nsChangeHint changeHint =
+ fromContext->CalcStyleDifference(toContext,
+ nsChangeHint(0),
+ &equalStructs,
+ &samePointerStructs);
+
+ mCumulativeChangeHint |= changeHint;
+ }
+ }
+}
+
+void
+KeyframeEffectReadOnly::SetAnimation(Animation* aAnimation)
+{
+ if (mAnimation == aAnimation) {
+ return;
+ }
+
+ // Restyle for the old animation.
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+
+ mAnimation = aAnimation;
+
+ // The order of these function calls is important:
+ // NotifyAnimationTimingUpdated() need the updated mIsRelevant flag to check
+ // if it should create the effectSet or not, and MarkCascadeNeedsUpdate()
+ // needs a valid effectSet, so we should call them in this order.
+ if (mAnimation) {
+ mAnimation->UpdateRelevance();
+ }
+ NotifyAnimationTimingUpdated();
+ if (mAnimation) {
+ MarkCascadeNeedsUpdate();
+ }
+}
+
+bool
+KeyframeEffectReadOnly::CanIgnoreIfNotVisible() const
+{
+ if (!AnimationUtils::IsOffscreenThrottlingEnabled()) {
+ return false;
+ }
+
+ // FIXME (bug 1303235): We don't calculate mCumulativeChangeHint for
+ // the Servo backend yet
+ nsPresContext* presContext = GetPresContext();
+ if (!presContext || presContext->StyleSet()->IsServo()) {
+ return false;
+ }
+
+ // FIXME: For further sophisticated optimization we need to check
+ // change hint on the segment corresponding to computedTiming.progress.
+ return NS_IsHintSubset(
+ mCumulativeChangeHint, nsChangeHint_Hints_CanIgnoreIfNotVisible);
+}
+
+void
+KeyframeEffectReadOnly::MaybeUpdateFrameForCompositor()
+{
+ nsIFrame* frame = GetAnimationFrame();
+ if (!frame) {
+ return;
+ }
+
+ // FIXME: Bug 1272495: If this effect does not win in the cascade, the
+ // NS_FRAME_MAY_BE_TRANSFORMED flag should be removed when the animation
+ // will be removed from effect set or the transform keyframes are removed
+ // by setKeyframes. The latter case will be hard to solve though.
+ for (const AnimationProperty& property : mProperties) {
+ if (property.mProperty == eCSSProperty_transform) {
+ frame->AddStateBits(NS_FRAME_MAY_BE_TRANSFORMED);
+ return;
+ }
+ }
+}
+
+void
+KeyframeEffectReadOnly::MarkCascadeNeedsUpdate()
+{
+ if (!mTarget) {
+ return;
+ }
+
+ EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
+ mTarget->mPseudoType);
+ if (!effectSet) {
+ return;
+ }
+ effectSet->MarkCascadeNeedsUpdate();
+}
+
+bool
+KeyframeEffectReadOnly::HasComputedTimingChanged() const
+{
+ // Typically we don't need to request a restyle if the progress hasn't
+ // changed since the last call to ComposeStyle. The one exception is if the
+ // iteration composite mode is 'accumulate' and the current iteration has
+ // changed, since that will often produce a different result.
+ ComputedTiming computedTiming = GetComputedTiming();
+ return computedTiming.mProgress != mProgressOnLastCompose ||
+ (mEffectOptions.mIterationComposite ==
+ IterationCompositeOperation::Accumulate &&
+ computedTiming.mCurrentIteration !=
+ mCurrentIterationOnLastCompose);
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/animation/KeyframeEffectReadOnly.h b/dom/animation/KeyframeEffectReadOnly.h
new file mode 100644
index 0000000000..889159b387
--- /dev/null
+++ b/dom/animation/KeyframeEffectReadOnly.h
@@ -0,0 +1,439 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_dom_KeyframeEffectReadOnly_h
+#define mozilla_dom_KeyframeEffectReadOnly_h
+
+#include "nsChangeHint.h"
+#include "nsCSSPropertyID.h"
+#include "nsCSSValue.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTArray.h"
+#include "nsWrapperCache.h"
+#include "mozilla/AnimationPerformanceWarning.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ComputedTimingFunction.h"
+#include "mozilla/EffectCompositor.h"
+#include "mozilla/KeyframeEffectParams.h"
+#include "mozilla/LayerAnimationInfo.h" // LayerAnimations::kRecords
+#include "mozilla/ServoBindingTypes.h" // RawServoDeclarationBlock and
+ // associated RefPtrTraits
+#include "mozilla/StyleAnimationValue.h"
+#include "mozilla/dom/AnimationEffectReadOnly.h"
+#include "mozilla/dom/Element.h"
+
+struct JSContext;
+class JSObject;
+class nsCSSPropertyIDSet;
+class nsIContent;
+class nsIDocument;
+class nsIFrame;
+class nsIPresShell;
+class nsPresContext;
+
+namespace mozilla {
+
+class AnimValuesStyleRule;
+enum class CSSPseudoElementType : uint8_t;
+class ErrorResult;
+struct TimingParams;
+
+namespace dom {
+class ElementOrCSSPseudoElement;
+class GlobalObject;
+class OwningElementOrCSSPseudoElement;
+class UnrestrictedDoubleOrKeyframeAnimationOptions;
+class UnrestrictedDoubleOrKeyframeEffectOptions;
+enum class IterationCompositeOperation : uint32_t;
+enum class CompositeOperation : uint32_t;
+struct AnimationPropertyDetails;
+}
+
+/**
+ * A property-value pair specified on a keyframe.
+ */
+struct PropertyValuePair
+{
+ nsCSSPropertyID mProperty;
+ // The specified value for the property. For shorthand properties or invalid
+ // property values, we store the specified property value as a token stream
+ // (string).
+ nsCSSValue mValue;
+
+ // The specified value when using the Servo backend. However, even when
+ // using the Servo backend, we still fill in |mValue| in the case where we
+ // fail to parse the value since we use it to store the original string.
+ RefPtr<RawServoDeclarationBlock> mServoDeclarationBlock;
+
+ bool operator==(const PropertyValuePair&) const;
+};
+
+/**
+ * A single keyframe.
+ *
+ * This is the canonical form in which keyframe effects are stored and
+ * corresponds closely to the type of objects returned via the getKeyframes()
+ * API.
+ *
+ * Before computing an output animation value, however, we flatten these frames
+ * down to a series of per-property value arrays where we also resolve any
+ * overlapping shorthands/longhands, convert specified CSS values to computed
+ * values, etc.
+ *
+ * When the target element or style context changes, however, we rebuild these
+ * per-property arrays from the original list of keyframes objects. As a result,
+ * these objects represent the master definition of the effect's values.
+ */
+struct Keyframe
+{
+ Keyframe() = default;
+ Keyframe(const Keyframe& aOther) = default;
+ Keyframe(Keyframe&& aOther)
+ {
+ *this = Move(aOther);
+ }
+
+ Keyframe& operator=(const Keyframe& aOther) = default;
+ Keyframe& operator=(Keyframe&& aOther)
+ {
+ mOffset = aOther.mOffset;
+ mComputedOffset = aOther.mComputedOffset;
+ mTimingFunction = Move(aOther.mTimingFunction);
+ mPropertyValues = Move(aOther.mPropertyValues);
+ return *this;
+ }
+
+ Maybe<double> mOffset;
+ static constexpr double kComputedOffsetNotSet = -1.0;
+ double mComputedOffset = kComputedOffsetNotSet;
+ Maybe<ComputedTimingFunction> mTimingFunction; // Nothing() here means
+ // "linear"
+ nsTArray<PropertyValuePair> mPropertyValues;
+};
+
+struct AnimationPropertySegment
+{
+ float mFromKey, mToKey;
+ StyleAnimationValue mFromValue, mToValue;
+ Maybe<ComputedTimingFunction> mTimingFunction;
+
+ bool operator==(const AnimationPropertySegment& aOther) const
+ {
+ return mFromKey == aOther.mFromKey &&
+ mToKey == aOther.mToKey &&
+ mFromValue == aOther.mFromValue &&
+ mToValue == aOther.mToValue &&
+ mTimingFunction == aOther.mTimingFunction;
+ }
+ bool operator!=(const AnimationPropertySegment& aOther) const
+ {
+ return !(*this == aOther);
+ }
+};
+
+struct AnimationProperty
+{
+ nsCSSPropertyID mProperty = eCSSProperty_UNKNOWN;
+
+ // If true, the propery is currently being animated on the compositor.
+ //
+ // Note that when the owning Animation requests a non-throttled restyle, in
+ // between calling RequestRestyle on its EffectCompositor and when the
+ // restyle is performed, this member may temporarily become false even if
+ // the animation remains on the layer after the restyle.
+ //
+ // **NOTE**: This member is not included when comparing AnimationProperty
+ // objects for equality.
+ bool mIsRunningOnCompositor = false;
+
+ Maybe<AnimationPerformanceWarning> mPerformanceWarning;
+
+ InfallibleTArray<AnimationPropertySegment> mSegments;
+
+ // The copy constructor/assignment doesn't copy mIsRunningOnCompositor and
+ // mPerformanceWarning.
+ AnimationProperty() = default;
+ AnimationProperty(const AnimationProperty& aOther)
+ : mProperty(aOther.mProperty), mSegments(aOther.mSegments) { }
+ AnimationProperty& operator=(const AnimationProperty& aOther)
+ {
+ mProperty = aOther.mProperty;
+ mSegments = aOther.mSegments;
+ return *this;
+ }
+
+ // NOTE: This operator does *not* compare the mIsRunningOnCompositor member.
+ // This is because AnimationProperty objects are compared when recreating
+ // CSS animations to determine if mutation observer change records need to
+ // be created or not. However, at the point when these objects are compared
+ // the mIsRunningOnCompositor will not have been set on the new objects so
+ // we ignore this member to avoid generating spurious change records.
+ bool operator==(const AnimationProperty& aOther) const
+ {
+ return mProperty == aOther.mProperty &&
+ mSegments == aOther.mSegments;
+ }
+ bool operator!=(const AnimationProperty& aOther) const
+ {
+ return !(*this == aOther);
+ }
+};
+
+struct ElementPropertyTransition;
+
+namespace dom {
+
+class Animation;
+
+class KeyframeEffectReadOnly : public AnimationEffectReadOnly
+{
+public:
+ KeyframeEffectReadOnly(nsIDocument* aDocument,
+ const Maybe<OwningAnimationTarget>& aTarget,
+ const TimingParams& aTiming,
+ const KeyframeEffectParams& aOptions);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(KeyframeEffectReadOnly,
+ AnimationEffectReadOnly)
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ KeyframeEffectReadOnly* AsKeyframeEffect() override { return this; }
+
+ // KeyframeEffectReadOnly interface
+ static already_AddRefed<KeyframeEffectReadOnly>
+ Constructor(const GlobalObject& aGlobal,
+ const Nullable<ElementOrCSSPseudoElement>& aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
+ ErrorResult& aRv);
+
+ static already_AddRefed<KeyframeEffectReadOnly>
+ Constructor(const GlobalObject& aGlobal,
+ KeyframeEffectReadOnly& aSource,
+ ErrorResult& aRv);
+
+ void GetTarget(Nullable<OwningElementOrCSSPseudoElement>& aRv) const;
+ Maybe<NonOwningAnimationTarget> GetTarget() const
+ {
+ Maybe<NonOwningAnimationTarget> result;
+ if (mTarget) {
+ result.emplace(*mTarget);
+ }
+ return result;
+ }
+ void GetKeyframes(JSContext*& aCx,
+ nsTArray<JSObject*>& aResult,
+ ErrorResult& aRv);
+ void GetProperties(nsTArray<AnimationPropertyDetails>& aProperties,
+ ErrorResult& aRv) const;
+
+ IterationCompositeOperation IterationComposite() const;
+ CompositeOperation Composite() const;
+ void GetSpacing(nsString& aRetVal) const
+ {
+ mEffectOptions.GetSpacingAsString(aRetVal);
+ }
+
+ void NotifyAnimationTimingUpdated();
+
+ void SetAnimation(Animation* aAnimation) override;
+
+ void SetKeyframes(JSContext* aContext, JS::Handle<JSObject*> aKeyframes,
+ ErrorResult& aRv);
+ void SetKeyframes(nsTArray<Keyframe>&& aKeyframes,
+ nsStyleContext* aStyleContext);
+
+ // Returns true if the effect includes |aProperty| regardless of whether the
+ // property is overridden by !important rule.
+ bool HasAnimationOfProperty(nsCSSPropertyID aProperty) const;
+
+ // GetEffectiveAnimationOfProperty returns AnimationProperty corresponding
+ // to a given CSS property if the effect includes the property and the
+ // property is not overridden by !important rules.
+ // Also EffectiveAnimationOfProperty returns true under the same condition.
+ //
+ // NOTE: We don't currently check for !important rules for properties that
+ // can't run on the compositor.
+ bool HasEffectiveAnimationOfProperty(nsCSSPropertyID aProperty) const
+ {
+ return GetEffectiveAnimationOfProperty(aProperty) != nullptr;
+ }
+ const AnimationProperty* GetEffectiveAnimationOfProperty(
+ nsCSSPropertyID aProperty) const;
+
+ const InfallibleTArray<AnimationProperty>& Properties() const
+ {
+ return mProperties;
+ }
+
+ // Update |mProperties| by recalculating from |mKeyframes| using
+ // |aStyleContext| to resolve specified values.
+ void UpdateProperties(nsStyleContext* aStyleContext);
+
+ // Updates |aStyleRule| with the animation values produced by this
+ // AnimationEffect for the current time except any properties contained
+ // in |aPropertiesToSkip|.
+ void ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
+ const nsCSSPropertyIDSet& aPropertiesToSkip);
+ // Returns true if at least one property is being animated on compositor.
+ bool IsRunningOnCompositor() const;
+ void SetIsRunningOnCompositor(nsCSSPropertyID aProperty, bool aIsRunning);
+ void ResetIsRunningOnCompositor();
+
+ // Returns true if this effect, applied to |aFrame|, contains properties
+ // that mean we shouldn't run transform compositor animations on this element.
+ //
+ // For example, if we have an animation of geometric properties like 'left'
+ // and 'top' on an element, we force all 'transform' animations running at
+ // the same time on the same element to run on the main thread.
+ //
+ // When returning true, |aPerformanceWarning| stores the reason why
+ // we shouldn't run the transform animations.
+ bool ShouldBlockAsyncTransformAnimations(
+ const nsIFrame* aFrame,
+ AnimationPerformanceWarning::Type& aPerformanceWarning) const;
+
+ nsIDocument* GetRenderedDocument() const;
+ nsPresContext* GetPresContext() const;
+ nsIPresShell* GetPresShell() const;
+
+ // Associates a warning with the animated property on the specified frame
+ // indicating why, for example, the property could not be animated on the
+ // compositor. |aParams| and |aParamsLength| are optional parameters which
+ // will be used to generate a localized message for devtools.
+ void SetPerformanceWarning(
+ nsCSSPropertyID aProperty,
+ const AnimationPerformanceWarning& aWarning);
+
+ // Cumulative change hint on each segment for each property.
+ // This is used for deciding the animation is paint-only.
+ void CalculateCumulativeChangeHint(nsStyleContext* aStyleContext);
+
+ // Returns true if all of animation properties' change hints
+ // can ignore painting if the animation is not visible.
+ // See nsChangeHint_Hints_CanIgnoreIfNotVisible in nsChangeHint.h
+ // in detail which change hint can be ignored.
+ bool CanIgnoreIfNotVisible() const;
+
+protected:
+ KeyframeEffectReadOnly(nsIDocument* aDocument,
+ const Maybe<OwningAnimationTarget>& aTarget,
+ AnimationEffectTimingReadOnly* aTiming,
+ const KeyframeEffectParams& aOptions);
+
+ ~KeyframeEffectReadOnly() override = default;
+
+ static Maybe<OwningAnimationTarget>
+ ConvertTarget(const Nullable<ElementOrCSSPseudoElement>& aTarget);
+
+ template<class KeyframeEffectType, class OptionsType>
+ static already_AddRefed<KeyframeEffectType>
+ ConstructKeyframeEffect(const GlobalObject& aGlobal,
+ const Nullable<ElementOrCSSPseudoElement>& aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const OptionsType& aOptions,
+ ErrorResult& aRv);
+
+ template<class KeyframeEffectType>
+ static already_AddRefed<KeyframeEffectType>
+ ConstructKeyframeEffect(const GlobalObject& aGlobal,
+ KeyframeEffectReadOnly& aSource,
+ ErrorResult& aRv);
+
+ // Build properties by recalculating from |mKeyframes| using |aStyleContext|
+ // to resolve specified values. This function also applies paced spacing if
+ // needed.
+ nsTArray<AnimationProperty> BuildProperties(nsStyleContext* aStyleContext);
+
+ // This effect is registered with its target element so long as:
+ //
+ // (a) It has a target element, and
+ // (b) It is "relevant" (i.e. yet to finish but not idle, or finished but
+ // filling forwards)
+ //
+ // As a result, we need to make sure this gets called whenever anything
+ // changes with regards to this effects's timing including changes to the
+ // owning Animation's timing.
+ void UpdateTargetRegistration();
+
+ // Remove the current effect target from its EffectSet.
+ void UnregisterTarget();
+
+ void RequestRestyle(EffectCompositor::RestyleType aRestyleType);
+
+ // Update the associated frame state bits so that, if necessary, a stacking
+ // context will be created and the effect sent to the compositor. We
+ // typically need to do this when the properties referenced by the keyframe
+ // have changed, or when the target frame might have changed.
+ void MaybeUpdateFrameForCompositor();
+
+ // Looks up the style context associated with the target element, if any.
+ // We need to be careful to *not* call this when we are updating the style
+ // context. That's because calling GetStyleContextForElement when we are in
+ // the process of building a style context may trigger various forms of
+ // infinite recursion.
+ already_AddRefed<nsStyleContext>
+ GetTargetStyleContext();
+
+ // A wrapper for marking cascade update according to the current
+ // target and its effectSet.
+ void MarkCascadeNeedsUpdate();
+
+ Maybe<OwningAnimationTarget> mTarget;
+
+ KeyframeEffectParams mEffectOptions;
+
+ // The specified keyframes.
+ nsTArray<Keyframe> mKeyframes;
+
+ // A set of per-property value arrays, derived from |mKeyframes|.
+ nsTArray<AnimationProperty> mProperties;
+
+ // The computed progress last time we composed the style rule. This is
+ // used to detect when the progress is not changing (e.g. due to a step
+ // timing function) so we can avoid unnecessary style updates.
+ Nullable<double> mProgressOnLastCompose;
+
+ // The purpose of this value is the same as mProgressOnLastCompose but
+ // this is used to detect when the current iteration is not changing
+ // in the case when iterationComposite is accumulate.
+ uint64_t mCurrentIterationOnLastCompose = 0;
+
+ // We need to track when we go to or from being "in effect" since
+ // we need to re-evaluate the cascade of animations when that changes.
+ bool mInEffectOnLastAnimationTimingUpdate;
+
+private:
+ nsChangeHint mCumulativeChangeHint;
+
+ nsIFrame* GetAnimationFrame() const;
+
+ bool CanThrottle() const;
+ bool CanThrottleTransformChanges(nsIFrame& aFrame) const;
+
+ // Returns true if the computedTiming has changed since the last
+ // composition.
+ bool HasComputedTimingChanged() const;
+
+ // Returns true unless Gecko limitations prevent performing transform
+ // animations for |aFrame|. When returning true, the reason for the
+ // limitation is stored in |aOutPerformanceWarning|.
+ static bool CanAnimateTransformOnCompositor(
+ const nsIFrame* aFrame,
+ AnimationPerformanceWarning::Type& aPerformanceWarning);
+ static bool IsGeometricProperty(const nsCSSPropertyID aProperty);
+
+ static const TimeDuration OverflowRegionRefreshInterval();
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_KeyframeEffectReadOnly_h
diff --git a/dom/animation/KeyframeUtils.cpp b/dom/animation/KeyframeUtils.cpp
new file mode 100644
index 0000000000..8e396f84c3
--- /dev/null
+++ b/dom/animation/KeyframeUtils.cpp
@@ -0,0 +1,1667 @@
+/* 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 "mozilla/KeyframeUtils.h"
+
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Move.h"
+#include "mozilla/RangedArray.h"
+#include "mozilla/ServoBindings.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "mozilla/TimingParams.h"
+#include "mozilla/dom/BaseKeyframeTypesBinding.h" // For FastBaseKeyframe etc.
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/KeyframeEffectBinding.h"
+#include "mozilla/dom/KeyframeEffectReadOnly.h" // For PropertyValuesPair etc.
+#include "jsapi.h" // For ForOfIterator etc.
+#include "nsClassHashtable.h"
+#include "nsCSSParser.h"
+#include "nsCSSPropertyIDSet.h"
+#include "nsCSSProps.h"
+#include "nsCSSPseudoElements.h" // For CSSPseudoElementType
+#include "nsTArray.h"
+#include <algorithm> // For std::stable_sort
+
+namespace mozilla {
+
+// ------------------------------------------------------------------
+//
+// Internal data types
+//
+// ------------------------------------------------------------------
+
+// This is used while calculating paced spacing. If the keyframe is not pacable,
+// we set its cumulative distance to kNotPaceable, so we can use this to check.
+const double kNotPaceable = -1.0;
+
+// For the aAllowList parameter of AppendStringOrStringSequence and
+// GetPropertyValuesPairs.
+enum class ListAllowance { eDisallow, eAllow };
+
+/**
+ * A comparator to sort nsCSSPropertyID values such that longhands are sorted
+ * before shorthands, and shorthands with fewer components are sorted before
+ * shorthands with more components.
+ *
+ * Using this allows us to prioritize values specified by longhands (or smaller
+ * shorthand subsets) when longhands and shorthands are both specified
+ * on the one keyframe.
+ *
+ * Example orderings that result from this:
+ *
+ * margin-left, margin
+ *
+ * and:
+ *
+ * border-top-color, border-color, border-top, border
+ */
+class PropertyPriorityComparator
+{
+public:
+ PropertyPriorityComparator()
+ : mSubpropertyCountInitialized(false) {}
+
+ bool Equals(nsCSSPropertyID aLhs, nsCSSPropertyID aRhs) const
+ {
+ return aLhs == aRhs;
+ }
+
+ bool LessThan(nsCSSPropertyID aLhs,
+ nsCSSPropertyID aRhs) const
+ {
+ bool isShorthandLhs = nsCSSProps::IsShorthand(aLhs);
+ bool isShorthandRhs = nsCSSProps::IsShorthand(aRhs);
+
+ if (isShorthandLhs) {
+ if (isShorthandRhs) {
+ // First, sort shorthands by the number of longhands they have.
+ uint32_t subpropCountLhs = SubpropertyCount(aLhs);
+ uint32_t subpropCountRhs = SubpropertyCount(aRhs);
+ if (subpropCountLhs != subpropCountRhs) {
+ return subpropCountLhs < subpropCountRhs;
+ }
+ // Otherwise, sort by IDL name below.
+ } else {
+ // Put longhands before shorthands.
+ return false;
+ }
+ } else {
+ if (isShorthandRhs) {
+ // Put longhands before shorthands.
+ return true;
+ }
+ }
+ // For two longhand properties, or two shorthand with the same number
+ // of longhand components, sort by IDL name.
+ return nsCSSProps::PropertyIDLNameSortPosition(aLhs) <
+ nsCSSProps::PropertyIDLNameSortPosition(aRhs);
+ }
+
+ uint32_t SubpropertyCount(nsCSSPropertyID aProperty) const
+ {
+ if (!mSubpropertyCountInitialized) {
+ PodZero(&mSubpropertyCount);
+ mSubpropertyCountInitialized = true;
+ }
+ if (mSubpropertyCount[aProperty] == 0) {
+ uint32_t count = 0;
+ CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(
+ p, aProperty, CSSEnabledState::eForAllContent) {
+ ++count;
+ }
+ mSubpropertyCount[aProperty] = count;
+ }
+ return mSubpropertyCount[aProperty];
+ }
+
+private:
+ // Cache of shorthand subproperty counts.
+ mutable RangedArray<
+ uint32_t,
+ eCSSProperty_COUNT_no_shorthands,
+ eCSSProperty_COUNT - eCSSProperty_COUNT_no_shorthands> mSubpropertyCount;
+ mutable bool mSubpropertyCountInitialized;
+};
+
+/**
+ * Adaptor for PropertyPriorityComparator to sort objects which have
+ * a mProperty member.
+ */
+template <typename T>
+class TPropertyPriorityComparator : PropertyPriorityComparator
+{
+public:
+ bool Equals(const T& aLhs, const T& aRhs) const
+ {
+ return PropertyPriorityComparator::Equals(aLhs.mProperty, aRhs.mProperty);
+ }
+ bool LessThan(const T& aLhs, const T& aRhs) const
+ {
+ return PropertyPriorityComparator::LessThan(aLhs.mProperty, aRhs.mProperty);
+ }
+};
+
+/**
+ * Iterator to walk through a PropertyValuePair array using the ordering
+ * provided by PropertyPriorityComparator.
+ */
+class PropertyPriorityIterator
+{
+public:
+ explicit PropertyPriorityIterator(
+ const nsTArray<PropertyValuePair>& aProperties)
+ : mProperties(aProperties)
+ {
+ mSortedPropertyIndices.SetCapacity(mProperties.Length());
+ for (size_t i = 0, len = mProperties.Length(); i < len; ++i) {
+ PropertyAndIndex propertyIndex = { mProperties[i].mProperty, i };
+ mSortedPropertyIndices.AppendElement(propertyIndex);
+ }
+ mSortedPropertyIndices.Sort(PropertyAndIndex::Comparator());
+ }
+
+ class Iter
+ {
+ public:
+ explicit Iter(const PropertyPriorityIterator& aParent)
+ : mParent(aParent)
+ , mIndex(0) { }
+
+ static Iter EndIter(const PropertyPriorityIterator &aParent)
+ {
+ Iter iter(aParent);
+ iter.mIndex = aParent.mSortedPropertyIndices.Length();
+ return iter;
+ }
+
+ bool operator!=(const Iter& aOther) const
+ {
+ return mIndex != aOther.mIndex;
+ }
+
+ Iter& operator++()
+ {
+ MOZ_ASSERT(mIndex + 1 <= mParent.mSortedPropertyIndices.Length(),
+ "Should not seek past end iterator");
+ mIndex++;
+ return *this;
+ }
+
+ const PropertyValuePair& operator*()
+ {
+ MOZ_ASSERT(mIndex < mParent.mSortedPropertyIndices.Length(),
+ "Should not try to dereference an end iterator");
+ return mParent.mProperties[mParent.mSortedPropertyIndices[mIndex].mIndex];
+ }
+
+ private:
+ const PropertyPriorityIterator& mParent;
+ size_t mIndex;
+ };
+
+ Iter begin() { return Iter(*this); }
+ Iter end() { return Iter::EndIter(*this); }
+
+private:
+ struct PropertyAndIndex
+ {
+ nsCSSPropertyID mProperty;
+ size_t mIndex; // Index of mProperty within mProperties
+
+ typedef TPropertyPriorityComparator<PropertyAndIndex> Comparator;
+ };
+
+ const nsTArray<PropertyValuePair>& mProperties;
+ nsTArray<PropertyAndIndex> mSortedPropertyIndices;
+};
+
+/**
+ * A property-values pair obtained from the open-ended properties
+ * discovered on a regular keyframe or property-indexed keyframe object.
+ *
+ * Single values (as required by a regular keyframe, and as also supported
+ * on property-indexed keyframes) are stored as the only element in
+ * mValues.
+ */
+struct PropertyValuesPair
+{
+ nsCSSPropertyID mProperty;
+ nsTArray<nsString> mValues;
+
+ typedef TPropertyPriorityComparator<PropertyValuesPair> Comparator;
+};
+
+/**
+ * An additional property (for a property-values pair) found on a
+ * BaseKeyframe or BasePropertyIndexedKeyframe object.
+ */
+struct AdditionalProperty
+{
+ nsCSSPropertyID mProperty;
+ size_t mJsidIndex; // Index into |ids| in GetPropertyValuesPairs.
+
+ struct PropertyComparator
+ {
+ bool Equals(const AdditionalProperty& aLhs,
+ const AdditionalProperty& aRhs) const
+ {
+ return aLhs.mProperty == aRhs.mProperty;
+ }
+ bool LessThan(const AdditionalProperty& aLhs,
+ const AdditionalProperty& aRhs) const
+ {
+ return nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) <
+ nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
+ }
+ };
+};
+
+/**
+ * Data for a segment in a keyframe animation of a given property
+ * whose value is a StyleAnimationValue.
+ *
+ * KeyframeValueEntry is used in GetAnimationPropertiesFromKeyframes
+ * to gather data for each individual segment.
+ */
+struct KeyframeValueEntry
+{
+ nsCSSPropertyID mProperty;
+ StyleAnimationValue mValue;
+ float mOffset;
+ Maybe<ComputedTimingFunction> mTimingFunction;
+
+ struct PropertyOffsetComparator
+ {
+ static bool Equals(const KeyframeValueEntry& aLhs,
+ const KeyframeValueEntry& aRhs)
+ {
+ return aLhs.mProperty == aRhs.mProperty &&
+ aLhs.mOffset == aRhs.mOffset;
+ }
+ static bool LessThan(const KeyframeValueEntry& aLhs,
+ const KeyframeValueEntry& aRhs)
+ {
+ // First, sort by property IDL name.
+ int32_t order = nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) -
+ nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
+ if (order != 0) {
+ return order < 0;
+ }
+
+ // Then, by offset.
+ return aLhs.mOffset < aRhs.mOffset;
+ }
+ };
+};
+
+class ComputedOffsetComparator
+{
+public:
+ static bool Equals(const Keyframe& aLhs, const Keyframe& aRhs)
+ {
+ return aLhs.mComputedOffset == aRhs.mComputedOffset;
+ }
+
+ static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs)
+ {
+ return aLhs.mComputedOffset < aRhs.mComputedOffset;
+ }
+};
+
+// ------------------------------------------------------------------
+//
+// Inlined helper methods
+//
+// ------------------------------------------------------------------
+
+inline bool
+IsInvalidValuePair(const PropertyValuePair& aPair, StyleBackendType aBackend)
+{
+ if (aBackend == StyleBackendType::Servo) {
+ return !aPair.mServoDeclarationBlock;
+ }
+
+ // There are three types of values we store as token streams:
+ //
+ // * Shorthand values (where we manually extract the token stream's string
+ // value) and pass that along to various parsing methods
+ // * Longhand values with variable references
+ // * Invalid values
+ //
+ // We can distinguish between the last two cases because for invalid values
+ // we leave the token stream's mPropertyID as eCSSProperty_UNKNOWN.
+ return !nsCSSProps::IsShorthand(aPair.mProperty) &&
+ aPair.mValue.GetUnit() == eCSSUnit_TokenStream &&
+ aPair.mValue.GetTokenStreamValue()->mPropertyID
+ == eCSSProperty_UNKNOWN;
+}
+
+
+// ------------------------------------------------------------------
+//
+// Internal helper method declarations
+//
+// ------------------------------------------------------------------
+
+static void
+GetKeyframeListFromKeyframeSequence(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::ForOfIterator& aIterator,
+ nsTArray<Keyframe>& aResult,
+ ErrorResult& aRv);
+
+static bool
+ConvertKeyframeSequence(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::ForOfIterator& aIterator,
+ nsTArray<Keyframe>& aResult);
+
+static bool
+GetPropertyValuesPairs(JSContext* aCx,
+ JS::Handle<JSObject*> aObject,
+ ListAllowance aAllowLists,
+ nsTArray<PropertyValuesPair>& aResult);
+
+static bool
+AppendStringOrStringSequenceToArray(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ListAllowance aAllowLists,
+ nsTArray<nsString>& aValues);
+
+static bool
+AppendValueAsString(JSContext* aCx,
+ nsTArray<nsString>& aValues,
+ JS::Handle<JS::Value> aValue);
+
+static PropertyValuePair
+MakePropertyValuePair(nsCSSPropertyID aProperty, const nsAString& aStringValue,
+ nsCSSParser& aParser, nsIDocument* aDocument);
+
+static bool
+HasValidOffsets(const nsTArray<Keyframe>& aKeyframes);
+
+static void
+MarkAsComputeValuesFailureKey(PropertyValuePair& aPair);
+
+static bool
+IsComputeValuesFailureKey(const PropertyValuePair& aPair);
+
+static void
+BuildSegmentsFromValueEntries(nsTArray<KeyframeValueEntry>& aEntries,
+ nsTArray<AnimationProperty>& aResult);
+
+static void
+GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::Handle<JS::Value> aValue,
+ nsTArray<Keyframe>& aResult,
+ ErrorResult& aRv);
+
+static bool
+RequiresAdditiveAnimation(const nsTArray<Keyframe>& aKeyframes,
+ nsIDocument* aDocument);
+
+static void
+DistributeRange(const Range<Keyframe>& aSpacingRange,
+ const Range<Keyframe>& aRangeToAdjust);
+
+static void
+DistributeRange(const Range<Keyframe>& aSpacingRange);
+
+static void
+PaceRange(const Range<Keyframe>& aKeyframes,
+ const Range<double>& aCumulativeDistances);
+
+static nsTArray<double>
+GetCumulativeDistances(const nsTArray<ComputedKeyframeValues>& aValues,
+ nsCSSPropertyID aProperty,
+ nsStyleContext* aStyleContext);
+
+// ------------------------------------------------------------------
+//
+// Public API
+//
+// ------------------------------------------------------------------
+
+/* static */ nsTArray<Keyframe>
+KeyframeUtils::GetKeyframesFromObject(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::Handle<JSObject*> aFrames,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(!aRv.Failed());
+
+ nsTArray<Keyframe> keyframes;
+
+ if (!aFrames) {
+ // The argument was explicitly null meaning no keyframes.
+ return keyframes;
+ }
+
+ // At this point we know we have an object. We try to convert it to a
+ // sequence of keyframes first, and if that fails due to not being iterable,
+ // we try to convert it to a property-indexed keyframe.
+ JS::Rooted<JS::Value> objectValue(aCx, JS::ObjectValue(*aFrames));
+ JS::ForOfIterator iter(aCx);
+ if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return keyframes;
+ }
+
+ if (iter.valueIsIterable()) {
+ GetKeyframeListFromKeyframeSequence(aCx, aDocument, iter, keyframes, aRv);
+ } else {
+ GetKeyframeListFromPropertyIndexedKeyframe(aCx, aDocument, objectValue,
+ keyframes, aRv);
+ }
+
+ if (aRv.Failed()) {
+ MOZ_ASSERT(keyframes.IsEmpty(),
+ "Should not set any keyframes when there is an error");
+ return keyframes;
+ }
+
+ // We currently don't support additive animation. However, Web Animations
+ // says that if you don't have a keyframe at offset 0 or 1, then you should
+ // synthesize one using an additive zero value when you go to compose style.
+ // Until we implement additive animations we just throw if we encounter any
+ // set of keyframes that would put us in that situation.
+
+ if (RequiresAdditiveAnimation(keyframes, aDocument)) {
+ aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR);
+ keyframes.Clear();
+ }
+
+ return keyframes;
+}
+
+/* static */ void
+KeyframeUtils::ApplySpacing(nsTArray<Keyframe>& aKeyframes,
+ SpacingMode aSpacingMode,
+ nsCSSPropertyID aProperty,
+ nsTArray<ComputedKeyframeValues>& aComputedValues,
+ nsStyleContext* aStyleContext)
+{
+ if (aKeyframes.IsEmpty()) {
+ return;
+ }
+
+ nsTArray<double> cumulativeDistances;
+ if (aSpacingMode == SpacingMode::paced) {
+ MOZ_ASSERT(IsAnimatableProperty(aProperty),
+ "Paced property should be animatable");
+
+ cumulativeDistances = GetCumulativeDistances(aComputedValues, aProperty,
+ aStyleContext);
+ // Reset the computed offsets if using paced spacing.
+ for (Keyframe& keyframe : aKeyframes) {
+ keyframe.mComputedOffset = Keyframe::kComputedOffsetNotSet;
+ }
+ }
+
+ // If the first keyframe has an unspecified offset, fill it in with 0%.
+ // If there is only a single keyframe, then it gets 100%.
+ if (aKeyframes.Length() > 1) {
+ Keyframe& firstElement = aKeyframes[0];
+ firstElement.mComputedOffset = firstElement.mOffset.valueOr(0.0);
+ // We will fill in the last keyframe's offset below
+ } else {
+ Keyframe& lastElement = aKeyframes.LastElement();
+ lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0);
+ }
+
+ // Fill in remaining missing offsets.
+ const Keyframe* const last = aKeyframes.cend() - 1;
+ const RangedPtr<Keyframe> begin(aKeyframes.begin(), aKeyframes.Length());
+ RangedPtr<Keyframe> keyframeA = begin;
+ while (keyframeA != last) {
+ // Find keyframe A and keyframe B *between* which we will apply spacing.
+ RangedPtr<Keyframe> keyframeB = keyframeA + 1;
+ while (keyframeB->mOffset.isNothing() && keyframeB != last) {
+ ++keyframeB;
+ }
+ keyframeB->mComputedOffset = keyframeB->mOffset.valueOr(1.0);
+
+ // Fill computed offsets in (keyframe A, keyframe B).
+ if (aSpacingMode == SpacingMode::distribute) {
+ DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1));
+ } else {
+ // a) Find Paced A (first paceable keyframe) and
+ // Paced B (last paceable keyframe) in [keyframe A, keyframe B].
+ RangedPtr<Keyframe> pacedA = keyframeA;
+ while (pacedA < keyframeB &&
+ cumulativeDistances[pacedA - begin] == kNotPaceable) {
+ ++pacedA;
+ }
+ RangedPtr<Keyframe> pacedB = keyframeB;
+ while (pacedB > keyframeA &&
+ cumulativeDistances[pacedB - begin] == kNotPaceable) {
+ --pacedB;
+ }
+ // As spec says, if there is no paceable keyframe
+ // in [keyframe A, keyframe B], we let Paced A and Paced B refer to
+ // keyframe B.
+ if (pacedA > pacedB) {
+ pacedA = pacedB = keyframeB;
+ }
+ // b) Apply distributing offsets in (keyframe A, Paced A] and
+ // [Paced B, keyframe B).
+ DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1),
+ Range<Keyframe>(keyframeA + 1, pacedA + 1));
+ DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1),
+ Range<Keyframe>(pacedB, keyframeB));
+ // c) Apply paced offsets to each paceable keyframe in (Paced A, Paced B).
+ // We pass the range [Paced A, Paced B] since PaceRange needs the end
+ // points of the range in order to calculate the correct offset.
+ PaceRange(Range<Keyframe>(pacedA, pacedB + 1),
+ Range<double>(&cumulativeDistances[pacedA - begin],
+ pacedB - pacedA + 1));
+ // d) Fill in any computed offsets in (Paced A, Paced B) that are still
+ // not set (e.g. because the keyframe was not paceable, or because the
+ // cumulative distance between paceable properties was zero).
+ for (RangedPtr<Keyframe> frame = pacedA + 1; frame < pacedB; ++frame) {
+ if (frame->mComputedOffset != Keyframe::kComputedOffsetNotSet) {
+ continue;
+ }
+
+ RangedPtr<Keyframe> start = frame - 1;
+ RangedPtr<Keyframe> end = frame + 1;
+ while (end < pacedB &&
+ end->mComputedOffset == Keyframe::kComputedOffsetNotSet) {
+ ++end;
+ }
+ DistributeRange(Range<Keyframe>(start, end + 1));
+ frame = end;
+ }
+ }
+ keyframeA = keyframeB;
+ }
+}
+
+/* static */ void
+KeyframeUtils::ApplyDistributeSpacing(nsTArray<Keyframe>& aKeyframes)
+{
+ nsTArray<ComputedKeyframeValues> emptyArray;
+ ApplySpacing(aKeyframes, SpacingMode::distribute, eCSSProperty_UNKNOWN,
+ emptyArray, nullptr);
+}
+
+/* static */ nsTArray<ComputedKeyframeValues>
+KeyframeUtils::GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
+ dom::Element* aElement,
+ nsStyleContext* aStyleContext)
+{
+ MOZ_ASSERT(aStyleContext);
+ MOZ_ASSERT(aElement);
+
+ StyleBackendType styleBackend = aElement->OwnerDoc()->GetStyleBackendType();
+
+ const size_t len = aKeyframes.Length();
+ nsTArray<ComputedKeyframeValues> result(len);
+
+ for (const Keyframe& frame : aKeyframes) {
+ nsCSSPropertyIDSet propertiesOnThisKeyframe;
+ ComputedKeyframeValues* computedValues = result.AppendElement();
+ for (const PropertyValuePair& pair :
+ PropertyPriorityIterator(frame.mPropertyValues)) {
+ MOZ_ASSERT(!pair.mServoDeclarationBlock ||
+ styleBackend == StyleBackendType::Servo,
+ "Animation values were parsed using Servo backend but target"
+ " element is not using Servo backend?");
+
+ if (IsInvalidValuePair(pair, styleBackend)) {
+ continue;
+ }
+
+ // Expand each value into the set of longhands and produce
+ // a KeyframeValueEntry for each value.
+ nsTArray<PropertyStyleAnimationValuePair> values;
+
+ if (styleBackend == StyleBackendType::Servo) {
+ if (!StyleAnimationValue::ComputeValues(pair.mProperty,
+ CSSEnabledState::eForAllContent, aStyleContext,
+ *pair.mServoDeclarationBlock, values)) {
+ continue;
+ }
+ } else {
+ // For shorthands, we store the string as a token stream so we need to
+ // extract that first.
+ if (nsCSSProps::IsShorthand(pair.mProperty)) {
+ nsCSSValueTokenStream* tokenStream = pair.mValue.GetTokenStreamValue();
+ if (!StyleAnimationValue::ComputeValues(pair.mProperty,
+ CSSEnabledState::eForAllContent, aElement, aStyleContext,
+ tokenStream->mTokenStream, /* aUseSVGMode */ false, values) ||
+ IsComputeValuesFailureKey(pair)) {
+ continue;
+ }
+ } else {
+ if (!StyleAnimationValue::ComputeValues(pair.mProperty,
+ CSSEnabledState::eForAllContent, aElement, aStyleContext,
+ pair.mValue, /* aUseSVGMode */ false, values)) {
+ continue;
+ }
+ MOZ_ASSERT(values.Length() == 1,
+ "Longhand properties should produce a single"
+ " StyleAnimationValue");
+ }
+ }
+
+ for (auto& value : values) {
+ // If we already got a value for this property on the keyframe,
+ // skip this one.
+ if (propertiesOnThisKeyframe.HasProperty(value.mProperty)) {
+ continue;
+ }
+ computedValues->AppendElement(value);
+ propertiesOnThisKeyframe.AddProperty(value.mProperty);
+ }
+ }
+ }
+
+ MOZ_ASSERT(result.Length() == aKeyframes.Length(), "Array length mismatch");
+ return result;
+}
+
+/* static */ nsTArray<AnimationProperty>
+KeyframeUtils::GetAnimationPropertiesFromKeyframes(
+ const nsTArray<Keyframe>& aKeyframes,
+ const nsTArray<ComputedKeyframeValues>& aComputedValues,
+ nsStyleContext* aStyleContext)
+{
+ MOZ_ASSERT(aKeyframes.Length() == aComputedValues.Length(),
+ "Array length mismatch");
+
+ nsTArray<KeyframeValueEntry> entries(aKeyframes.Length());
+
+ const size_t len = aKeyframes.Length();
+ for (size_t i = 0; i < len; ++i) {
+ const Keyframe& frame = aKeyframes[i];
+ for (auto& value : aComputedValues[i]) {
+ MOZ_ASSERT(frame.mComputedOffset != Keyframe::kComputedOffsetNotSet,
+ "Invalid computed offset");
+ KeyframeValueEntry* entry = entries.AppendElement();
+ entry->mOffset = frame.mComputedOffset;
+ entry->mProperty = value.mProperty;
+ entry->mValue = value.mValue;
+ entry->mTimingFunction = frame.mTimingFunction;
+ }
+ }
+
+ nsTArray<AnimationProperty> result;
+ BuildSegmentsFromValueEntries(entries, result);
+ return result;
+}
+
+/* static */ bool
+KeyframeUtils::IsAnimatableProperty(nsCSSPropertyID aProperty)
+{
+ if (aProperty == eCSSProperty_UNKNOWN) {
+ return false;
+ }
+
+ if (!nsCSSProps::IsShorthand(aProperty)) {
+ return nsCSSProps::kAnimTypeTable[aProperty] != eStyleAnimType_None;
+ }
+
+ CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, aProperty,
+ CSSEnabledState::eForAllContent) {
+ if (nsCSSProps::kAnimTypeTable[*subprop] != eStyleAnimType_None) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// ------------------------------------------------------------------
+//
+// Internal helpers
+//
+// ------------------------------------------------------------------
+
+/**
+ * Converts a JS object to an IDL sequence<Keyframe>.
+ *
+ * @param aCx The JSContext corresponding to |aIterator|.
+ * @param aDocument The document to use when parsing CSS properties.
+ * @param aIterator An already-initialized ForOfIterator for the JS
+ * object to iterate over as a sequence.
+ * @param aResult The array into which the resulting Keyframe objects will be
+ * appended.
+ * @param aRv Out param to store any errors thrown by this function.
+ */
+static void
+GetKeyframeListFromKeyframeSequence(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::ForOfIterator& aIterator,
+ nsTArray<Keyframe>& aResult,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(!aRv.Failed());
+ MOZ_ASSERT(aResult.IsEmpty());
+
+ // Convert the object in aIterator to a sequence of keyframes producing
+ // an array of Keyframe objects.
+ if (!ConvertKeyframeSequence(aCx, aDocument, aIterator, aResult)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ aResult.Clear();
+ return;
+ }
+
+ // If the sequence<> had zero elements, we won't generate any
+ // keyframes.
+ if (aResult.IsEmpty()) {
+ return;
+ }
+
+ // Check that the keyframes are loosely sorted and with values all
+ // between 0% and 100%.
+ if (!HasValidOffsets(aResult)) {
+ aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
+ aResult.Clear();
+ return;
+ }
+}
+
+/**
+ * Converts a JS object wrapped by the given JS::ForIfIterator to an
+ * IDL sequence<Keyframe> and stores the resulting Keyframe objects in
+ * aResult.
+ */
+static bool
+ConvertKeyframeSequence(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::ForOfIterator& aIterator,
+ nsTArray<Keyframe>& aResult)
+{
+ JS::Rooted<JS::Value> value(aCx);
+ nsCSSParser parser(aDocument->CSSLoader());
+
+ for (;;) {
+ bool done;
+ if (!aIterator.next(&value, &done)) {
+ return false;
+ }
+ if (done) {
+ break;
+ }
+ // Each value found when iterating the object must be an object
+ // or null/undefined (which gets treated as a default {} dictionary
+ // value).
+ if (!value.isObject() && !value.isNullOrUndefined()) {
+ dom::ThrowErrorMessage(aCx, dom::MSG_NOT_OBJECT,
+ "Element of sequence<Keyframe> argument");
+ return false;
+ }
+
+ // Convert the JS value into a BaseKeyframe dictionary value.
+ dom::binding_detail::FastBaseKeyframe keyframeDict;
+ if (!keyframeDict.Init(aCx, value,
+ "Element of sequence<Keyframe> argument")) {
+ return false;
+ }
+
+ Keyframe* keyframe = aResult.AppendElement(fallible);
+ if (!keyframe) {
+ return false;
+ }
+ if (!keyframeDict.mOffset.IsNull()) {
+ keyframe->mOffset.emplace(keyframeDict.mOffset.Value());
+ }
+
+ ErrorResult rv;
+ keyframe->mTimingFunction =
+ TimingParams::ParseEasing(keyframeDict.mEasing, aDocument, rv);
+ if (rv.MaybeSetPendingException(aCx)) {
+ return false;
+ }
+
+ // Look for additional property-values pairs on the object.
+ nsTArray<PropertyValuesPair> propertyValuePairs;
+ if (value.isObject()) {
+ JS::Rooted<JSObject*> object(aCx, &value.toObject());
+ if (!GetPropertyValuesPairs(aCx, object,
+ ListAllowance::eDisallow,
+ propertyValuePairs)) {
+ return false;
+ }
+ }
+
+ for (PropertyValuesPair& pair : propertyValuePairs) {
+ MOZ_ASSERT(pair.mValues.Length() == 1);
+ keyframe->mPropertyValues.AppendElement(
+ MakePropertyValuePair(pair.mProperty, pair.mValues[0], parser,
+ aDocument));
+
+ // When we go to convert keyframes into arrays of property values we
+ // call StyleAnimation::ComputeValues. This should normally return true
+ // but in order to test the case where it does not, BaseKeyframeDict
+ // includes a chrome-only member that can be set to indicate that
+ // ComputeValues should fail for shorthand property values on that
+ // keyframe.
+ if (nsCSSProps::IsShorthand(pair.mProperty) &&
+ keyframeDict.mSimulateComputeValuesFailure) {
+ MarkAsComputeValuesFailureKey(keyframe->mPropertyValues.LastElement());
+ }
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Reads the property-values pairs from the specified JS object.
+ *
+ * @param aObject The JS object to look at.
+ * @param aAllowLists If eAllow, values will be converted to
+ * (DOMString or sequence<DOMString); if eDisallow, values
+ * will be converted to DOMString.
+ * @param aResult The array into which the enumerated property-values
+ * pairs will be stored.
+ * @return false on failure or JS exception thrown while interacting
+ * with aObject; true otherwise.
+ */
+static bool
+GetPropertyValuesPairs(JSContext* aCx,
+ JS::Handle<JSObject*> aObject,
+ ListAllowance aAllowLists,
+ nsTArray<PropertyValuesPair>& aResult)
+{
+ nsTArray<AdditionalProperty> properties;
+
+ // Iterate over all the properties on aObject and append an
+ // entry to properties for them.
+ //
+ // We don't compare the jsids that we encounter with those for
+ // the explicit dictionary members, since we know that none
+ // of the CSS property IDL names clash with them.
+ JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, aObject, &ids)) {
+ return false;
+ }
+ for (size_t i = 0, n = ids.length(); i < n; i++) {
+ nsAutoJSString propName;
+ if (!propName.init(aCx, ids[i])) {
+ return false;
+ }
+ nsCSSPropertyID property =
+ nsCSSProps::LookupPropertyByIDLName(propName,
+ CSSEnabledState::eForAllContent);
+ if (KeyframeUtils::IsAnimatableProperty(property)) {
+ AdditionalProperty* p = properties.AppendElement();
+ p->mProperty = property;
+ p->mJsidIndex = i;
+ }
+ }
+
+ // Sort the entries by IDL name and then get each value and
+ // convert it either to a DOMString or to a
+ // (DOMString or sequence<DOMString>), depending on aAllowLists,
+ // and build up aResult.
+ properties.Sort(AdditionalProperty::PropertyComparator());
+
+ for (AdditionalProperty& p : properties) {
+ JS::Rooted<JS::Value> value(aCx);
+ if (!JS_GetPropertyById(aCx, aObject, ids[p.mJsidIndex], &value)) {
+ return false;
+ }
+ PropertyValuesPair* pair = aResult.AppendElement();
+ pair->mProperty = p.mProperty;
+ if (!AppendStringOrStringSequenceToArray(aCx, value, aAllowLists,
+ pair->mValues)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Converts aValue to DOMString, if aAllowLists is eDisallow, or
+ * to (DOMString or sequence<DOMString>) if aAllowLists is aAllow.
+ * The resulting strings are appended to aValues.
+ */
+static bool
+AppendStringOrStringSequenceToArray(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ListAllowance aAllowLists,
+ nsTArray<nsString>& aValues)
+{
+ if (aAllowLists == ListAllowance::eAllow && aValue.isObject()) {
+ // The value is an object, and we want to allow lists; convert
+ // aValue to (DOMString or sequence<DOMString>).
+ JS::ForOfIterator iter(aCx);
+ if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) {
+ return false;
+ }
+ if (iter.valueIsIterable()) {
+ // If the object is iterable, convert it to sequence<DOMString>.
+ JS::Rooted<JS::Value> element(aCx);
+ for (;;) {
+ bool done;
+ if (!iter.next(&element, &done)) {
+ return false;
+ }
+ if (done) {
+ break;
+ }
+ if (!AppendValueAsString(aCx, aValues, element)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ // Either the object is not iterable, or aAllowLists doesn't want
+ // a list; convert it to DOMString.
+ if (!AppendValueAsString(aCx, aValues, aValue)) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Converts aValue to DOMString and appends it to aValues.
+ */
+static bool
+AppendValueAsString(JSContext* aCx,
+ nsTArray<nsString>& aValues,
+ JS::Handle<JS::Value> aValue)
+{
+ return ConvertJSValueToString(aCx, aValue, dom::eStringify, dom::eStringify,
+ *aValues.AppendElement());
+}
+
+/**
+ * Construct a PropertyValuePair parsing the given string into a suitable
+ * nsCSSValue object.
+ *
+ * @param aProperty The CSS property.
+ * @param aStringValue The property value to parse.
+ * @param aParser The CSS parser object to use.
+ * @param aDocument The document to use when parsing.
+ * @return The constructed PropertyValuePair object.
+ */
+static PropertyValuePair
+MakePropertyValuePair(nsCSSPropertyID aProperty, const nsAString& aStringValue,
+ nsCSSParser& aParser, nsIDocument* aDocument)
+{
+ MOZ_ASSERT(aDocument);
+ PropertyValuePair result;
+
+ result.mProperty = aProperty;
+
+ if (aDocument->GetStyleBackendType() == StyleBackendType::Servo) {
+ nsCString name = nsCSSProps::GetStringValue(aProperty);
+
+ NS_ConvertUTF16toUTF8 value(aStringValue);
+ RefPtr<ThreadSafeURIHolder> base =
+ new ThreadSafeURIHolder(aDocument->GetDocumentURI());
+ RefPtr<ThreadSafeURIHolder> referrer =
+ new ThreadSafeURIHolder(aDocument->GetDocumentURI());
+ RefPtr<ThreadSafePrincipalHolder> principal =
+ new ThreadSafePrincipalHolder(aDocument->NodePrincipal());
+
+ nsCString baseString;
+ aDocument->GetDocumentURI()->GetSpec(baseString);
+
+ RefPtr<RawServoDeclarationBlock> servoDeclarationBlock =
+ Servo_ParseProperty(&name, &value, &baseString,
+ base, referrer, principal).Consume();
+
+ if (servoDeclarationBlock) {
+ result.mServoDeclarationBlock = servoDeclarationBlock.forget();
+ return result;
+ }
+ }
+
+ nsCSSValue value;
+ if (!nsCSSProps::IsShorthand(aProperty)) {
+ aParser.ParseLonghandProperty(aProperty,
+ aStringValue,
+ aDocument->GetDocumentURI(),
+ aDocument->GetDocumentURI(),
+ aDocument->NodePrincipal(),
+ value);
+ }
+
+ if (value.GetUnit() == eCSSUnit_Null) {
+ // Either we have a shorthand, or we failed to parse a longhand.
+ // In either case, store the string value as a token stream.
+ nsCSSValueTokenStream* tokenStream = new nsCSSValueTokenStream;
+ tokenStream->mTokenStream = aStringValue;
+
+ // We are about to convert a null value to a token stream value but
+ // by leaving the mPropertyID as unknown, we will be able to
+ // distinguish between invalid values and valid token stream values
+ // (e.g. values with variable references).
+ MOZ_ASSERT(tokenStream->mPropertyID == eCSSProperty_UNKNOWN,
+ "The property of a token stream should be initialized"
+ " to unknown");
+
+ // By leaving mShorthandPropertyID as unknown, we ensure that when
+ // we call nsCSSValue::AppendToString we get back the string stored
+ // in mTokenStream.
+ MOZ_ASSERT(tokenStream->mShorthandPropertyID == eCSSProperty_UNKNOWN,
+ "The shorthand property of a token stream should be initialized"
+ " to unknown");
+ value.SetTokenStreamValue(tokenStream);
+ } else {
+ // If we succeeded in parsing with Gecko, but not Servo the animation is
+ // not going to work since, for the purposes of animation, we're going to
+ // ignore |mValue| when the backend is Servo.
+ NS_WARNING_ASSERTION(aDocument->GetStyleBackendType() !=
+ StyleBackendType::Servo,
+ "Gecko succeeded in parsing where Servo failed");
+ }
+
+ result.mValue = value;
+
+ return result;
+}
+
+/**
+ * Checks that the given keyframes are loosely ordered (each keyframe's
+ * offset that is not null is greater than or equal to the previous
+ * non-null offset) and that all values are within the range [0.0, 1.0].
+ *
+ * @return true if the keyframes' offsets are correctly ordered and
+ * within range; false otherwise.
+ */
+static bool
+HasValidOffsets(const nsTArray<Keyframe>& aKeyframes)
+{
+ double offset = 0.0;
+ for (const Keyframe& keyframe : aKeyframes) {
+ if (keyframe.mOffset) {
+ double thisOffset = keyframe.mOffset.value();
+ if (thisOffset < offset || thisOffset > 1.0f) {
+ return false;
+ }
+ offset = thisOffset;
+ }
+ }
+ return true;
+}
+
+/**
+ * Takes a property-value pair for a shorthand property and modifies the
+ * value to indicate that when we call StyleAnimationValue::ComputeValues on
+ * that value we should behave as if that function had failed.
+ *
+ * @param aPair The PropertyValuePair to modify. |aPair.mProperty| must be
+ * a shorthand property.
+ */
+static void
+MarkAsComputeValuesFailureKey(PropertyValuePair& aPair)
+{
+ MOZ_ASSERT(nsCSSProps::IsShorthand(aPair.mProperty),
+ "Only shorthand property values can be marked as failure values");
+
+ // We store shorthand values as nsCSSValueTokenStream objects whose mProperty
+ // and mShorthandPropertyID are eCSSProperty_UNKNOWN and whose mTokenStream
+ // member contains the shorthand property's value as a string.
+ //
+ // We need to leave mShorthandPropertyID as eCSSProperty_UNKNOWN so that
+ // nsCSSValue::AppendToString returns the mTokenStream value, but we can
+ // update mPropertyID to a special value to indicate that this is
+ // a special failure sentinel.
+ nsCSSValueTokenStream* tokenStream = aPair.mValue.GetTokenStreamValue();
+ MOZ_ASSERT(tokenStream->mPropertyID == eCSSProperty_UNKNOWN,
+ "Shorthand value should initially have an unknown property ID");
+ tokenStream->mPropertyID = eCSSPropertyExtra_no_properties;
+}
+
+/**
+ * Returns true if |aPair| is a property-value pair on which we have
+ * previously called MarkAsComputeValuesFailureKey (and hence we should
+ * simulate failure when calling StyleAnimationValue::ComputeValues using its
+ * value).
+ *
+ * @param aPair The property-value pair to test.
+ * @return True if |aPair| represents a failure value.
+ */
+static bool
+IsComputeValuesFailureKey(const PropertyValuePair& aPair)
+{
+ return nsCSSProps::IsShorthand(aPair.mProperty) &&
+ aPair.mValue.GetTokenStreamValue()->mPropertyID ==
+ eCSSPropertyExtra_no_properties;
+}
+
+/**
+ * Builds an array of AnimationProperty objects to represent the keyframe
+ * animation segments in aEntries.
+ */
+static void
+BuildSegmentsFromValueEntries(nsTArray<KeyframeValueEntry>& aEntries,
+ nsTArray<AnimationProperty>& aResult)
+{
+ if (aEntries.IsEmpty()) {
+ return;
+ }
+
+ // Sort the KeyframeValueEntry objects so that all entries for a given
+ // property are together, and the entries are sorted by offset otherwise.
+ std::stable_sort(aEntries.begin(), aEntries.end(),
+ &KeyframeValueEntry::PropertyOffsetComparator::LessThan);
+
+ // For a given index i, we want to generate a segment from aEntries[i]
+ // to aEntries[j], if:
+ //
+ // * j > i,
+ // * aEntries[i + 1]'s offset/property is different from aEntries[i]'s, and
+ // * aEntries[j - 1]'s offset/property is different from aEntries[j]'s.
+ //
+ // That will eliminate runs of same offset/property values where there's no
+ // point generating zero length segments in the middle of the animation.
+ //
+ // Additionally we need to generate a zero length segment at offset 0 and at
+ // offset 1, if we have multiple values for a given property at that offset,
+ // since we need to retain the very first and very last value so they can
+ // be used for reverse and forward filling.
+ //
+ // Typically, for each property in |aEntries|, we expect there to be at least
+ // one KeyframeValueEntry with offset 0.0, and at least one with offset 1.0.
+ // However, since it is possible that when building |aEntries|, the call to
+ // StyleAnimationValue::ComputeValues might fail, this can't be guaranteed.
+ // Furthermore, since we don't yet implement additive animation and hence
+ // don't have sensible fallback behavior when these values are missing, the
+ // following loop takes care to identify properties that lack a value at
+ // offset 0.0/1.0 and drops those properties from |aResult|.
+
+ nsCSSPropertyID lastProperty = eCSSProperty_UNKNOWN;
+ AnimationProperty* animationProperty = nullptr;
+
+ size_t i = 0, n = aEntries.Length();
+
+ while (i < n) {
+ // Check that the last property ends with an entry at offset 1.
+ if (i + 1 == n) {
+ if (aEntries[i].mOffset != 1.0f && animationProperty) {
+ aResult.RemoveElementAt(aResult.Length() - 1);
+ animationProperty = nullptr;
+ }
+ break;
+ }
+
+ MOZ_ASSERT(aEntries[i].mProperty != eCSSProperty_UNKNOWN &&
+ aEntries[i + 1].mProperty != eCSSProperty_UNKNOWN,
+ "Each entry should specify a valid property");
+
+ // Skip properties that don't have an entry with offset 0.
+ if (aEntries[i].mProperty != lastProperty &&
+ aEntries[i].mOffset != 0.0f) {
+ // Since the entries are sorted by offset for a given property, and
+ // since we don't update |lastProperty|, we will keep hitting this
+ // condition until we change property.
+ ++i;
+ continue;
+ }
+
+ // Drop properties that don't end with an entry with offset 1.
+ if (aEntries[i].mProperty != aEntries[i + 1].mProperty &&
+ aEntries[i].mOffset != 1.0f) {
+ if (animationProperty) {
+ aResult.RemoveElementAt(aResult.Length() - 1);
+ animationProperty = nullptr;
+ }
+ ++i;
+ continue;
+ }
+
+ // Starting from i, determine the next [i, j] interval from which to
+ // generate a segment.
+ size_t j;
+ if (aEntries[i].mOffset == 0.0f && aEntries[i + 1].mOffset == 0.0f) {
+ // We need to generate an initial zero-length segment.
+ MOZ_ASSERT(aEntries[i].mProperty == aEntries[i + 1].mProperty);
+ j = i + 1;
+ while (aEntries[j + 1].mOffset == 0.0f &&
+ aEntries[j + 1].mProperty == aEntries[j].mProperty) {
+ ++j;
+ }
+ } else if (aEntries[i].mOffset == 1.0f) {
+ if (aEntries[i + 1].mOffset == 1.0f &&
+ aEntries[i + 1].mProperty == aEntries[i].mProperty) {
+ // We need to generate a final zero-length segment.
+ j = i + 1;
+ while (j + 1 < n &&
+ aEntries[j + 1].mOffset == 1.0f &&
+ aEntries[j + 1].mProperty == aEntries[j].mProperty) {
+ ++j;
+ }
+ } else {
+ // New property.
+ MOZ_ASSERT(aEntries[i].mProperty != aEntries[i + 1].mProperty);
+ animationProperty = nullptr;
+ ++i;
+ continue;
+ }
+ } else {
+ while (aEntries[i].mOffset == aEntries[i + 1].mOffset &&
+ aEntries[i].mProperty == aEntries[i + 1].mProperty) {
+ ++i;
+ }
+ j = i + 1;
+ }
+
+ // If we've moved on to a new property, create a new AnimationProperty
+ // to insert segments into.
+ if (aEntries[i].mProperty != lastProperty) {
+ MOZ_ASSERT(aEntries[i].mOffset == 0.0f);
+ MOZ_ASSERT(!animationProperty);
+ animationProperty = aResult.AppendElement();
+ animationProperty->mProperty = aEntries[i].mProperty;
+ lastProperty = aEntries[i].mProperty;
+ }
+
+ MOZ_ASSERT(animationProperty, "animationProperty should be valid pointer.");
+
+ // Now generate the segment.
+ AnimationPropertySegment* segment =
+ animationProperty->mSegments.AppendElement();
+ segment->mFromKey = aEntries[i].mOffset;
+ segment->mToKey = aEntries[j].mOffset;
+ segment->mFromValue = aEntries[i].mValue;
+ segment->mToValue = aEntries[j].mValue;
+ segment->mTimingFunction = aEntries[i].mTimingFunction;
+
+ i = j;
+ }
+}
+
+/**
+ * Converts a JS object representing a property-indexed keyframe into
+ * an array of Keyframe objects.
+ *
+ * @param aCx The JSContext for |aValue|.
+ * @param aDocument The document to use when parsing CSS properties.
+ * @param aValue The JS object.
+ * @param aResult The array into which the resulting AnimationProperty
+ * objects will be appended.
+ * @param aRv Out param to store any errors thrown by this function.
+ */
+static void
+GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::Handle<JS::Value> aValue,
+ nsTArray<Keyframe>& aResult,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(aValue.isObject());
+ MOZ_ASSERT(aResult.IsEmpty());
+ MOZ_ASSERT(!aRv.Failed());
+
+ // Convert the object to a property-indexed keyframe dictionary to
+ // get its explicit dictionary members.
+ dom::binding_detail::FastBasePropertyIndexedKeyframe keyframeDict;
+ if (!keyframeDict.Init(aCx, aValue, "BasePropertyIndexedKeyframe argument",
+ false)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ Maybe<ComputedTimingFunction> easing =
+ TimingParams::ParseEasing(keyframeDict.mEasing, aDocument, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ // Get all the property--value-list pairs off the object.
+ JS::Rooted<JSObject*> object(aCx, &aValue.toObject());
+ nsTArray<PropertyValuesPair> propertyValuesPairs;
+ if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eAllow,
+ propertyValuesPairs)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // Create a set of keyframes for each property.
+ nsCSSParser parser(aDocument->CSSLoader());
+ nsClassHashtable<nsFloatHashKey, Keyframe> processedKeyframes;
+ for (const PropertyValuesPair& pair : propertyValuesPairs) {
+ size_t count = pair.mValues.Length();
+ if (count == 0) {
+ // No animation values for this property.
+ continue;
+ }
+ if (count == 1) {
+ // We don't support additive values and so can't support an
+ // animation that goes from the underlying value to this
+ // specified value. Throw an exception until we do support this.
+ aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR);
+ return;
+ }
+
+ size_t n = pair.mValues.Length() - 1;
+ size_t i = 0;
+
+ for (const nsString& stringValue : pair.mValues) {
+ double offset = i++ / double(n);
+ Keyframe* keyframe = processedKeyframes.LookupOrAdd(offset);
+ if (keyframe->mPropertyValues.IsEmpty()) {
+ keyframe->mTimingFunction = easing;
+ keyframe->mComputedOffset = offset;
+ }
+ keyframe->mPropertyValues.AppendElement(
+ MakePropertyValuePair(pair.mProperty, stringValue, parser, aDocument));
+ }
+ }
+
+ aResult.SetCapacity(processedKeyframes.Count());
+ for (auto iter = processedKeyframes.Iter(); !iter.Done(); iter.Next()) {
+ aResult.AppendElement(Move(*iter.UserData()));
+ }
+
+ aResult.Sort(ComputedOffsetComparator());
+}
+
+/**
+ * Returns true if the supplied set of keyframes has keyframe values for
+ * any property for which it does not also supply a value for the 0% and 100%
+ * offsets. In this case we are supposed to synthesize an additive zero value
+ * but since we don't support additive animation yet we can't support this
+ * case. We try to detect that here so we can throw an exception. The check is
+ * not entirely accurate but should detect most common cases.
+ *
+ * @param aKeyframes The set of keyframes to analyze.
+ * @param aDocument The document to use when parsing keyframes so we can
+ * try to detect where we have an invalid value at 0%/100%.
+ */
+static bool
+RequiresAdditiveAnimation(const nsTArray<Keyframe>& aKeyframes,
+ nsIDocument* aDocument)
+{
+ // We are looking to see if that every property referenced in |aKeyframes|
+ // has a valid property at offset 0.0 and 1.0. The check as to whether a
+ // property is valid or not, however, is not precise. We only check if the
+ // property can be parsed, NOT whether it can also be converted to a
+ // StyleAnimationValue since doing that requires a target element bound to
+ // a document which we might not always have at the point where we want to
+ // perform this check.
+ //
+ // This is only a temporary measure until we implement additive animation.
+ // So as long as this check catches most cases, and we don't do anything
+ // horrible in one of the cases we can't detect, it should be sufficient.
+
+ nsCSSPropertyIDSet properties; // All properties encountered.
+ nsCSSPropertyIDSet propertiesWithFromValue; // Those with a defined 0% value.
+ nsCSSPropertyIDSet propertiesWithToValue; // Those with a defined 100% value.
+
+ auto addToPropertySets = [&](nsCSSPropertyID aProperty, double aOffset) {
+ properties.AddProperty(aProperty);
+ if (aOffset == 0.0) {
+ propertiesWithFromValue.AddProperty(aProperty);
+ } else if (aOffset == 1.0) {
+ propertiesWithToValue.AddProperty(aProperty);
+ }
+ };
+
+ StyleBackendType styleBackend = aDocument->GetStyleBackendType();
+
+ for (size_t i = 0, len = aKeyframes.Length(); i < len; i++) {
+ const Keyframe& frame = aKeyframes[i];
+
+ // We won't have called ApplySpacing when this is called so
+ // we can't use frame.mComputedOffset. Instead we do a rough version
+ // of that algorithm that substitutes null offsets with 0.0 for the first
+ // frame, 1.0 for the last frame, and 0.5 for everything else.
+ double computedOffset = i == len - 1
+ ? 1.0
+ : i == 0 ? 0.0 : 0.5;
+ double offsetToUse = frame.mOffset
+ ? frame.mOffset.value()
+ : computedOffset;
+
+ for (const PropertyValuePair& pair : frame.mPropertyValues) {
+ if (IsInvalidValuePair(pair, styleBackend)) {
+ continue;
+ }
+
+ if (nsCSSProps::IsShorthand(pair.mProperty)) {
+ if (styleBackend == StyleBackendType::Gecko) {
+ nsCSSValueTokenStream* tokenStream =
+ pair.mValue.GetTokenStreamValue();
+ nsCSSParser parser(aDocument->CSSLoader());
+ if (!parser.IsValueValidForProperty(pair.mProperty,
+ tokenStream->mTokenStream)) {
+ continue;
+ }
+ }
+ // For the Servo backend, invalid shorthand values are represented by
+ // a null mServoDeclarationBlock member which we skip above in
+ // IsInvalidValuePair.
+ MOZ_ASSERT(styleBackend != StyleBackendType::Servo ||
+ pair.mServoDeclarationBlock);
+ CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(
+ prop, pair.mProperty, CSSEnabledState::eForAllContent) {
+ addToPropertySets(*prop, offsetToUse);
+ }
+ } else {
+ addToPropertySets(pair.mProperty, offsetToUse);
+ }
+ }
+ }
+
+ return !propertiesWithFromValue.Equals(properties) ||
+ !propertiesWithToValue.Equals(properties);
+}
+
+/**
+ * Evenly distribute the computed offsets in (A, B).
+ * We pass the range keyframes in [A, B] and use A, B to calculate distributing
+ * computed offsets in (A, B). The second range, aRangeToAdjust, is passed, so
+ * we can know which keyframe we want to apply to. aRangeToAdjust should be in
+ * the range of aSpacingRange.
+ *
+ * @param aSpacingRange The sequence of keyframes between whose endpoints we
+ * should apply distribute spacing.
+ * @param aRangeToAdjust The range of keyframes we want to apply to.
+ */
+static void
+DistributeRange(const Range<Keyframe>& aSpacingRange,
+ const Range<Keyframe>& aRangeToAdjust)
+{
+ MOZ_ASSERT(aRangeToAdjust.begin() >= aSpacingRange.begin() &&
+ aRangeToAdjust.end() <= aSpacingRange.end(),
+ "Out of range");
+ const size_t n = aSpacingRange.length() - 1;
+ const double startOffset = aSpacingRange[0].mComputedOffset;
+ const double diffOffset = aSpacingRange[n].mComputedOffset - startOffset;
+ for (auto iter = aRangeToAdjust.begin();
+ iter != aRangeToAdjust.end();
+ ++iter) {
+ size_t index = iter - aSpacingRange.begin();
+ iter->mComputedOffset = startOffset + double(index) / n * diffOffset;
+ }
+}
+
+/**
+ * Overload of DistributeRange to apply distribute spacing to all keyframes in
+ * between the endpoints of the given range.
+ *
+ * @param aSpacingRange The sequence of keyframes between whose endpoints we
+ * should apply distribute spacing.
+ */
+static void
+DistributeRange(const Range<Keyframe>& aSpacingRange)
+{
+ // We don't need to apply distribute spacing to keyframe A and keyframe B.
+ DistributeRange(aSpacingRange,
+ Range<Keyframe>(aSpacingRange.begin() + 1,
+ aSpacingRange.end() - 1));
+}
+
+/**
+ * Apply paced spacing to all paceable keyframes in between the endpoints of the
+ * given range.
+ *
+ * @param aKeyframes The range of keyframes between whose endpoints we should
+ * apply paced spacing. Both endpoints should be paceable, i.e. the
+ * corresponding elements in |aCumulativeDist| should not be kNotPaceable.
+ * Within this function, we refer to the start and end points of this range
+ * as Paced A and Paced B respectively in keeping with the notation used in
+ * the spec.
+ * @param aCumulativeDistances The sequence of cumulative distances of the paced
+ * property as returned by GetCumulativeDistances(). This acts as a
+ * parallel range to |aKeyframes|.
+ */
+static void
+PaceRange(const Range<Keyframe>& aKeyframes,
+ const Range<double>& aCumulativeDistances)
+{
+ MOZ_ASSERT(aKeyframes.length() == aCumulativeDistances.length(),
+ "Range length mismatch");
+
+ const size_t len = aKeyframes.length();
+ // If there is nothing between the end points, there is nothing to space.
+ if (len < 3) {
+ return;
+ }
+
+ const double distA = *(aCumulativeDistances.begin());
+ const double distB = *(aCumulativeDistances.end() - 1);
+ MOZ_ASSERT(distA != kNotPaceable && distB != kNotPaceable,
+ "Both Paced A and Paced B should be paceable");
+
+ // If the total distance is zero, we should fall back to distribute spacing.
+ // The caller will fill-in any keyframes without a computed offset using
+ // distribute spacing so we can just return here.
+ if (distA == distB) {
+ return;
+ }
+
+ const RangedPtr<Keyframe> pacedA = aKeyframes.begin();
+ const RangedPtr<Keyframe> pacedB = aKeyframes.end() - 1;
+ MOZ_ASSERT(pacedA->mComputedOffset != Keyframe::kComputedOffsetNotSet &&
+ pacedB->mComputedOffset != Keyframe::kComputedOffsetNotSet,
+ "Both Paced A and Paced B should have valid computed offsets");
+
+ // Apply computed offset.
+ const double offsetA = pacedA->mComputedOffset;
+ const double diffOffset = pacedB->mComputedOffset - offsetA;
+ const double initialDist = distA;
+ const double totalDist = distB - initialDist;
+ for (auto iter = pacedA + 1; iter != pacedB; ++iter) {
+ size_t k = iter - aKeyframes.begin();
+ if (aCumulativeDistances[k] == kNotPaceable) {
+ continue;
+ }
+
+ double dist = aCumulativeDistances[k] - initialDist;
+ iter->mComputedOffset = offsetA + diffOffset * dist / totalDist;
+ }
+}
+
+/**
+ * Get cumulative distances for the paced property.
+ *
+ * @param aValues The computed values returned by GetComputedKeyframeValues.
+ * @param aPacedProperty The paced property.
+ * @param aStyleContext The style context for computing distance on transform.
+ * @return The cumulative distances for the paced property. The length will be
+ * the same as aValues.
+ */
+static nsTArray<double>
+GetCumulativeDistances(const nsTArray<ComputedKeyframeValues>& aValues,
+ nsCSSPropertyID aPacedProperty,
+ nsStyleContext* aStyleContext)
+{
+ // a) If aPacedProperty is a shorthand property, get its components.
+ // Otherwise, just add the longhand property into the set.
+ size_t pacedPropertyCount = 0;
+ nsCSSPropertyIDSet pacedPropertySet;
+ bool isShorthand = nsCSSProps::IsShorthand(aPacedProperty);
+ if (isShorthand) {
+ CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(p, aPacedProperty,
+ CSSEnabledState::eForAllContent) {
+ pacedPropertySet.AddProperty(*p);
+ ++pacedPropertyCount;
+ }
+ } else {
+ pacedPropertySet.AddProperty(aPacedProperty);
+ pacedPropertyCount = 1;
+ }
+
+ // b) Search each component (shorthand) or the longhand property, and
+ // calculate the cumulative distances of paceable keyframe pairs.
+ const size_t len = aValues.Length();
+ nsTArray<double> cumulativeDistances(len);
+ // cumulativeDistances is a parallel array to |aValues|, so set its length to
+ // the length of |aValues|.
+ cumulativeDistances.SetLength(len);
+ ComputedKeyframeValues prevPacedValues;
+ size_t preIdx = 0;
+ for (size_t i = 0; i < len; ++i) {
+ // Find computed values of the paced property.
+ ComputedKeyframeValues pacedValues;
+ for (const PropertyStyleAnimationValuePair& pair : aValues[i]) {
+ if (pacedPropertySet.HasProperty(pair.mProperty)) {
+ pacedValues.AppendElement(pair);
+ }
+ }
+
+ // Check we have values for all the paceable longhand components.
+ if (pacedValues.Length() != pacedPropertyCount) {
+ // This keyframe is not paceable, assign kNotPaceable and skip it.
+ cumulativeDistances[i] = kNotPaceable;
+ continue;
+ }
+
+ // Sort the pacedValues first, so the order of subproperties of
+ // pacedValues is always the same as that of prevPacedValues.
+ if (isShorthand) {
+ pacedValues.Sort(
+ TPropertyPriorityComparator<PropertyStyleAnimationValuePair>());
+ }
+
+ if (prevPacedValues.IsEmpty()) {
+ // This is the first paceable keyframe so its cumulative distance is 0.0.
+ cumulativeDistances[i] = 0.0;
+ } else {
+ double dist = 0.0;
+ if (isShorthand) {
+ // Apply the distance by the square root of the sum of squares of
+ // longhand component distances.
+ for (size_t propIdx = 0; propIdx < pacedPropertyCount; ++propIdx) {
+ nsCSSPropertyID prop = prevPacedValues[propIdx].mProperty;
+ MOZ_ASSERT(pacedValues[propIdx].mProperty == prop,
+ "Property mismatch");
+
+ double componentDistance = 0.0;
+ if (StyleAnimationValue::ComputeDistance(
+ prop,
+ prevPacedValues[propIdx].mValue,
+ pacedValues[propIdx].mValue,
+ aStyleContext,
+ componentDistance)) {
+ dist += componentDistance * componentDistance;
+ }
+ }
+ dist = sqrt(dist);
+ } else {
+ // If the property is longhand, we just use the 1st value.
+ // If ComputeDistance() fails, |dist| will remain zero so there will be
+ // no distance between the previous paced value and this value.
+ Unused <<
+ StyleAnimationValue::ComputeDistance(aPacedProperty,
+ prevPacedValues[0].mValue,
+ pacedValues[0].mValue,
+ aStyleContext,
+ dist);
+ }
+ cumulativeDistances[i] = cumulativeDistances[preIdx] + dist;
+ }
+ prevPacedValues.SwapElements(pacedValues);
+ preIdx = i;
+ }
+ return cumulativeDistances;
+}
+
+} // namespace mozilla
diff --git a/dom/animation/KeyframeUtils.h b/dom/animation/KeyframeUtils.h
new file mode 100644
index 0000000000..dbaed97f59
--- /dev/null
+++ b/dom/animation/KeyframeUtils.h
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_KeyframeUtils_h
+#define mozilla_KeyframeUtils_h
+
+#include "nsTArrayForwardDeclare.h" // For nsTArray
+#include "js/RootingAPI.h" // For JS::Handle
+#include "mozilla/KeyframeEffectParams.h" // SpacingMode
+
+struct JSContext;
+class JSObject;
+class nsIDocument;
+class nsStyleContext;
+
+namespace mozilla {
+struct AnimationProperty;
+enum class CSSPseudoElementType : uint8_t;
+class ErrorResult;
+struct Keyframe;
+struct PropertyStyleAnimationValuePair;
+
+namespace dom {
+class Element;
+} // namespace dom
+} // namespace mozilla
+
+
+namespace mozilla {
+
+// Represents the set of property-value pairs on a Keyframe converted to
+// computed values.
+using ComputedKeyframeValues = nsTArray<PropertyStyleAnimationValuePair>;
+
+/**
+ * Utility methods for processing keyframes.
+ */
+class KeyframeUtils
+{
+public:
+ /**
+ * Converts a JS value representing a property-indexed keyframe or a sequence
+ * of keyframes to an array of Keyframe objects.
+ *
+ * @param aCx The JSContext that corresponds to |aFrames|.
+ * @param aDocument The document to use when parsing CSS properties.
+ * @param aFrames The JS value, provided as an optional IDL |object?| value,
+ * that is the keyframe list specification.
+ * @param aRv (out) Out-param to hold any error returned by this function.
+ * Must be initially empty.
+ * @return The set of processed keyframes. If an error occurs, aRv will be
+ * filled-in with the appropriate error code and an empty array will be
+ * returned.
+ */
+ static nsTArray<Keyframe>
+ GetKeyframesFromObject(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::Handle<JSObject*> aFrames,
+ ErrorResult& aRv);
+
+ /**
+ * Calculate the StyleAnimationValues of properties of each keyframe.
+ * This involves expanding shorthand properties into longhand properties,
+ * removing the duplicated properties for each keyframe, and creating an
+ * array of |property:computed value| pairs for each keyframe.
+ *
+ * These computed values are used *both* when computing the final set of
+ * per-property animation values (see GetAnimationPropertiesFromKeyframes) as
+ * well when applying paced spacing. By returning these values here, we allow
+ * the result to be re-used in both operations.
+ *
+ * @param aKeyframes The input keyframes.
+ * @param aElement The context element.
+ * @param aStyleContext The style context to use when computing values.
+ * @return The set of ComputedKeyframeValues. The length will be the same as
+ * aFrames.
+ */
+ static nsTArray<ComputedKeyframeValues>
+ GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
+ dom::Element* aElement,
+ nsStyleContext* aStyleContext);
+
+ /**
+ * Fills in the mComputedOffset member of each keyframe in the given array
+ * using the specified spacing mode.
+ *
+ * https://w3c.github.io/web-animations/#spacing-keyframes
+ *
+ * @param aKeyframes The set of keyframes to adjust.
+ * @param aSpacingMode The spacing mode to apply.
+ * @param aProperty The paced property. Only used when |aSpacingMode| is
+ * SpacingMode::paced. In all other cases it is ignored and hence may be
+ * any value, e.g. eCSSProperty_UNKNOWN.
+ * @param aComputedValues The set of computed keyframe values as returned by
+ * GetComputedKeyframeValues. Only used when |aSpacingMode| is
+ * SpacingMode::paced. In all other cases this parameter is unused and may
+ * be any value including an empty array.
+ * @param aStyleContext The style context used for calculating paced spacing
+ * on transform.
+ */
+ static void ApplySpacing(nsTArray<Keyframe>& aKeyframes,
+ SpacingMode aSpacingMode,
+ nsCSSPropertyID aProperty,
+ nsTArray<ComputedKeyframeValues>& aComputedValues,
+ nsStyleContext* aStyleContext);
+
+ /**
+ * Wrapper for ApplySpacing to simplify using distribute spacing.
+ *
+ * @param aKeyframes The set of keyframes to adjust.
+ */
+ static void ApplyDistributeSpacing(nsTArray<Keyframe>& aKeyframes);
+
+ /**
+ * Converts an array of Keyframe objects into an array of AnimationProperty
+ * objects. This involves creating an array of computed values for each
+ * longhand property and determining the offset and timing function to use
+ * for each value.
+ *
+ * @param aKeyframes The input keyframes.
+ * @param aComputedValues The computed keyframe values (as returned by
+ * GetComputedKeyframeValues) used to fill in the individual
+ * AnimationPropertySegment objects. Although these values could be
+ * calculated from |aKeyframes|, passing them in as a separate parameter
+ * allows the result of GetComputedKeyframeValues to be re-used both
+ * here and in ApplySpacing.
+ * @param aStyleContext The style context to calculate the style difference.
+ * @return The set of animation properties. If an error occurs, the returned
+ * array will be empty.
+ */
+ static nsTArray<AnimationProperty> GetAnimationPropertiesFromKeyframes(
+ const nsTArray<Keyframe>& aKeyframes,
+ const nsTArray<ComputedKeyframeValues>& aComputedValues,
+ nsStyleContext* aStyleContext);
+
+ /**
+ * Check if the property or, for shorthands, one or more of
+ * its subproperties, is animatable.
+ *
+ * @param aProperty The property to check.
+ * @return true if |aProperty| is animatable.
+ */
+ static bool IsAnimatableProperty(nsCSSPropertyID aProperty);
+};
+
+} // namespace mozilla
+
+#endif // mozilla_KeyframeUtils_h
diff --git a/dom/animation/PendingAnimationTracker.cpp b/dom/animation/PendingAnimationTracker.cpp
new file mode 100644
index 0000000000..a97814a7c0
--- /dev/null
+++ b/dom/animation/PendingAnimationTracker.cpp
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "PendingAnimationTracker.h"
+
+#include "mozilla/dom/AnimationTimeline.h"
+#include "nsIFrame.h"
+#include "nsIPresShell.h"
+
+using namespace mozilla;
+
+namespace mozilla {
+
+NS_IMPL_CYCLE_COLLECTION(PendingAnimationTracker,
+ mPlayPendingSet,
+ mPausePendingSet,
+ mDocument)
+
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(PendingAnimationTracker, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(PendingAnimationTracker, Release)
+
+void
+PendingAnimationTracker::AddPending(dom::Animation& aAnimation,
+ AnimationSet& aSet)
+{
+ aSet.PutEntry(&aAnimation);
+
+ // Schedule a paint. Otherwise animations that don't trigger a paint by
+ // themselves (e.g. CSS animations with an empty keyframes rule) won't
+ // start until something else paints.
+ EnsurePaintIsScheduled();
+}
+
+void
+PendingAnimationTracker::RemovePending(dom::Animation& aAnimation,
+ AnimationSet& aSet)
+{
+ aSet.RemoveEntry(&aAnimation);
+}
+
+bool
+PendingAnimationTracker::IsWaiting(const dom::Animation& aAnimation,
+ const AnimationSet& aSet) const
+{
+ return aSet.Contains(const_cast<dom::Animation*>(&aAnimation));
+}
+
+void
+PendingAnimationTracker::TriggerPendingAnimationsOnNextTick(const TimeStamp&
+ aReadyTime)
+{
+ auto triggerAnimationsAtReadyTime = [aReadyTime](AnimationSet& aAnimationSet)
+ {
+ for (auto iter = aAnimationSet.Iter(); !iter.Done(); iter.Next()) {
+ dom::Animation* animation = iter.Get()->GetKey();
+ dom::AnimationTimeline* timeline = animation->GetTimeline();
+
+ // If the animation does not have a timeline, just drop it from the map.
+ // The animation will detect that it is not being tracked and will trigger
+ // itself on the next tick where it has a timeline.
+ if (!timeline) {
+ iter.Remove();
+ continue;
+ }
+
+ // When the timeline's refresh driver is under test control, its values
+ // have no correspondance to wallclock times so we shouldn't try to
+ // convert aReadyTime (which is a wallclock time) to a timeline value.
+ // Instead, the animation will be started/paused when the refresh driver
+ // is next advanced since this will trigger a call to
+ // TriggerPendingAnimationsNow.
+ if (!timeline->TracksWallclockTime()) {
+ continue;
+ }
+
+ Nullable<TimeDuration> readyTime = timeline->ToTimelineTime(aReadyTime);
+ animation->TriggerOnNextTick(readyTime);
+
+ iter.Remove();
+ }
+ };
+
+ triggerAnimationsAtReadyTime(mPlayPendingSet);
+ triggerAnimationsAtReadyTime(mPausePendingSet);
+}
+
+void
+PendingAnimationTracker::TriggerPendingAnimationsNow()
+{
+ auto triggerAndClearAnimations = [](AnimationSet& aAnimationSet) {
+ for (auto iter = aAnimationSet.Iter(); !iter.Done(); iter.Next()) {
+ iter.Get()->GetKey()->TriggerNow();
+ }
+ aAnimationSet.Clear();
+ };
+
+ triggerAndClearAnimations(mPlayPendingSet);
+ triggerAndClearAnimations(mPausePendingSet);
+}
+
+void
+PendingAnimationTracker::EnsurePaintIsScheduled()
+{
+ if (!mDocument) {
+ return;
+ }
+
+ nsIPresShell* presShell = mDocument->GetShell();
+ if (!presShell) {
+ return;
+ }
+
+ nsIFrame* rootFrame = presShell->GetRootFrame();
+ if (!rootFrame) {
+ return;
+ }
+
+ rootFrame->SchedulePaint();
+}
+
+} // namespace mozilla
diff --git a/dom/animation/PendingAnimationTracker.h b/dom/animation/PendingAnimationTracker.h
new file mode 100644
index 0000000000..8d638c73fb
--- /dev/null
+++ b/dom/animation/PendingAnimationTracker.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_dom_PendingAnimationTracker_h
+#define mozilla_dom_PendingAnimationTracker_h
+
+#include "mozilla/dom/Animation.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIDocument.h"
+#include "nsTHashtable.h"
+
+class nsIFrame;
+
+namespace mozilla {
+
+class PendingAnimationTracker final
+{
+public:
+ explicit PendingAnimationTracker(nsIDocument* aDocument)
+ : mDocument(aDocument)
+ { }
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PendingAnimationTracker)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(PendingAnimationTracker)
+
+ void AddPlayPending(dom::Animation& aAnimation)
+ {
+ MOZ_ASSERT(!IsWaitingToPause(aAnimation),
+ "Animation is already waiting to pause");
+ AddPending(aAnimation, mPlayPendingSet);
+ }
+ void RemovePlayPending(dom::Animation& aAnimation)
+ {
+ RemovePending(aAnimation, mPlayPendingSet);
+ }
+ bool IsWaitingToPlay(const dom::Animation& aAnimation) const
+ {
+ return IsWaiting(aAnimation, mPlayPendingSet);
+ }
+
+ void AddPausePending(dom::Animation& aAnimation)
+ {
+ MOZ_ASSERT(!IsWaitingToPlay(aAnimation),
+ "Animation is already waiting to play");
+ AddPending(aAnimation, mPausePendingSet);
+ }
+ void RemovePausePending(dom::Animation& aAnimation)
+ {
+ RemovePending(aAnimation, mPausePendingSet);
+ }
+ bool IsWaitingToPause(const dom::Animation& aAnimation) const
+ {
+ return IsWaiting(aAnimation, mPausePendingSet);
+ }
+
+ void TriggerPendingAnimationsOnNextTick(const TimeStamp& aReadyTime);
+ void TriggerPendingAnimationsNow();
+ bool HasPendingAnimations() const {
+ return mPlayPendingSet.Count() > 0 || mPausePendingSet.Count() > 0;
+ }
+
+private:
+ ~PendingAnimationTracker() { }
+
+ void EnsurePaintIsScheduled();
+
+ typedef nsTHashtable<nsRefPtrHashKey<dom::Animation>> AnimationSet;
+
+ void AddPending(dom::Animation& aAnimation, AnimationSet& aSet);
+ void RemovePending(dom::Animation& aAnimation, AnimationSet& aSet);
+ bool IsWaiting(const dom::Animation& aAnimation,
+ const AnimationSet& aSet) const;
+
+ AnimationSet mPlayPendingSet;
+ AnimationSet mPausePendingSet;
+ nsCOMPtr<nsIDocument> mDocument;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_dom_PendingAnimationTracker_h
diff --git a/dom/animation/PseudoElementHashEntry.h b/dom/animation/PseudoElementHashEntry.h
new file mode 100644
index 0000000000..63f9c60c80
--- /dev/null
+++ b/dom/animation/PseudoElementHashEntry.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_PseudoElementHashEntry_h
+#define mozilla_PseudoElementHashEntry_h
+
+#include "mozilla/dom/Element.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/HashFunctions.h"
+#include "PLDHashTable.h"
+
+namespace mozilla {
+
+// A hash entry that uses a RefPtr<dom::Element>, CSSPseudoElementType pair
+class PseudoElementHashEntry : public PLDHashEntryHdr
+{
+public:
+ typedef NonOwningAnimationTarget KeyType;
+ typedef const NonOwningAnimationTarget* KeyTypePointer;
+
+ explicit PseudoElementHashEntry(KeyTypePointer aKey)
+ : mElement(aKey->mElement)
+ , mPseudoType(aKey->mPseudoType) { }
+ explicit PseudoElementHashEntry(const PseudoElementHashEntry& aCopy)=default;
+
+ ~PseudoElementHashEntry() = default;
+
+ KeyType GetKey() const { return { mElement, mPseudoType }; }
+ bool KeyEquals(KeyTypePointer aKey) const
+ {
+ return mElement == aKey->mElement &&
+ mPseudoType == aKey->mPseudoType;
+ }
+
+ static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; }
+ static PLDHashNumber HashKey(KeyTypePointer aKey)
+ {
+ if (!aKey)
+ return 0;
+
+ // Convert the scoped enum into an integer while adding it to hash.
+ // Note: CSSPseudoElementTypeBase is uint8_t, so we convert it into
+ // uint8_t directly to avoid including the header.
+ return mozilla::HashGeneric(aKey->mElement,
+ static_cast<uint8_t>(aKey->mPseudoType));
+ }
+ enum { ALLOW_MEMMOVE = true };
+
+ RefPtr<dom::Element> mElement;
+ CSSPseudoElementType mPseudoType;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_PseudoElementHashEntry_h
diff --git a/dom/animation/TimingParams.cpp b/dom/animation/TimingParams.cpp
new file mode 100644
index 0000000000..db61c84478
--- /dev/null
+++ b/dom/animation/TimingParams.cpp
@@ -0,0 +1,182 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "mozilla/TimingParams.h"
+
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/dom/AnimatableBinding.h"
+#include "mozilla/dom/KeyframeAnimationOptionsBinding.h"
+#include "mozilla/dom/KeyframeEffectBinding.h"
+#include "nsCSSParser.h" // For nsCSSParser
+#include "nsIDocument.h"
+#include "nsRuleNode.h"
+
+namespace mozilla {
+
+template <class OptionsType>
+static const dom::AnimationEffectTimingProperties&
+GetTimingProperties(const OptionsType& aOptions);
+
+template <>
+/* static */ const dom::AnimationEffectTimingProperties&
+GetTimingProperties(
+ const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions)
+{
+ MOZ_ASSERT(aOptions.IsKeyframeEffectOptions());
+ return aOptions.GetAsKeyframeEffectOptions();
+}
+
+template <>
+/* static */ const dom::AnimationEffectTimingProperties&
+GetTimingProperties(
+ const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions)
+{
+ MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions());
+ return aOptions.GetAsKeyframeAnimationOptions();
+}
+
+template <class OptionsType>
+static TimingParams
+TimingParamsFromOptionsUnion(const OptionsType& aOptions,
+ nsIDocument* aDocument,
+ ErrorResult& aRv)
+{
+ TimingParams result;
+ if (aOptions.IsUnrestrictedDouble()) {
+ double durationInMs = aOptions.GetAsUnrestrictedDouble();
+ if (durationInMs >= 0) {
+ result.mDuration.emplace(
+ StickyTimeDuration::FromMilliseconds(durationInMs));
+ } else {
+ aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
+ }
+ } else {
+ const dom::AnimationEffectTimingProperties& timing =
+ GetTimingProperties(aOptions);
+
+ Maybe<StickyTimeDuration> duration =
+ TimingParams::ParseDuration(timing.mDuration, aRv);
+ if (aRv.Failed()) {
+ return result;
+ }
+ TimingParams::ValidateIterationStart(timing.mIterationStart, aRv);
+ if (aRv.Failed()) {
+ return result;
+ }
+ TimingParams::ValidateIterations(timing.mIterations, aRv);
+ if (aRv.Failed()) {
+ return result;
+ }
+ Maybe<ComputedTimingFunction> easing =
+ TimingParams::ParseEasing(timing.mEasing, aDocument, aRv);
+ if (aRv.Failed()) {
+ return result;
+ }
+
+ result.mDuration = duration;
+ result.mDelay = TimeDuration::FromMilliseconds(timing.mDelay);
+ result.mEndDelay = TimeDuration::FromMilliseconds(timing.mEndDelay);
+ result.mIterations = timing.mIterations;
+ result.mIterationStart = timing.mIterationStart;
+ result.mDirection = timing.mDirection;
+ result.mFill = timing.mFill;
+ result.mFunction = easing;
+ }
+ return result;
+}
+
+/* static */ TimingParams
+TimingParams::FromOptionsUnion(
+ const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
+ nsIDocument* aDocument,
+ ErrorResult& aRv)
+{
+ return TimingParamsFromOptionsUnion(aOptions, aDocument, aRv);
+}
+
+/* static */ TimingParams
+TimingParams::FromOptionsUnion(
+ const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
+ nsIDocument* aDocument,
+ ErrorResult& aRv)
+{
+ return TimingParamsFromOptionsUnion(aOptions, aDocument, aRv);
+}
+
+/* static */ Maybe<ComputedTimingFunction>
+TimingParams::ParseEasing(const nsAString& aEasing,
+ nsIDocument* aDocument,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(aDocument);
+
+ nsCSSValue value;
+ nsCSSParser parser;
+ parser.ParseLonghandProperty(eCSSProperty_animation_timing_function,
+ aEasing,
+ aDocument->GetDocumentURI(),
+ aDocument->GetDocumentURI(),
+ aDocument->NodePrincipal(),
+ value);
+
+ switch (value.GetUnit()) {
+ case eCSSUnit_List: {
+ const nsCSSValueList* list = value.GetListValue();
+ if (list->mNext) {
+ // don't support a list of timing functions
+ break;
+ }
+ switch (list->mValue.GetUnit()) {
+ case eCSSUnit_Enumerated:
+ // Return Nothing() if "linear" is passed in.
+ if (list->mValue.GetIntValue() ==
+ NS_STYLE_TRANSITION_TIMING_FUNCTION_LINEAR) {
+ return Nothing();
+ }
+ MOZ_FALLTHROUGH;
+ case eCSSUnit_Cubic_Bezier:
+ case eCSSUnit_Steps: {
+ nsTimingFunction timingFunction;
+ nsRuleNode::ComputeTimingFunction(list->mValue, timingFunction);
+ ComputedTimingFunction computedTimingFunction;
+ computedTimingFunction.Init(timingFunction);
+ return Some(computedTimingFunction);
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("unexpected animation-timing-function list "
+ "item unit");
+ break;
+ }
+ break;
+ }
+ case eCSSUnit_Inherit:
+ case eCSSUnit_Initial:
+ case eCSSUnit_Unset:
+ case eCSSUnit_TokenStream:
+ case eCSSUnit_Null:
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("unexpected animation-timing-function unit");
+ break;
+ }
+
+ aRv.ThrowTypeError<dom::MSG_INVALID_EASING_ERROR>(aEasing);
+ return Nothing();
+}
+
+bool
+TimingParams::operator==(const TimingParams& aOther) const
+{
+ return mDuration == aOther.mDuration &&
+ mDelay == aOther.mDelay &&
+ mIterations == aOther.mIterations &&
+ mIterationStart == aOther.mIterationStart &&
+ mDirection == aOther.mDirection &&
+ mFill == aOther.mFill &&
+ mFunction == aOther.mFunction;
+}
+
+} // namespace mozilla
diff --git a/dom/animation/TimingParams.h b/dom/animation/TimingParams.h
new file mode 100644
index 0000000000..bfecee90cc
--- /dev/null
+++ b/dom/animation/TimingParams.h
@@ -0,0 +1,130 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_TimingParams_h
+#define mozilla_TimingParams_h
+
+#include "nsStringFwd.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/dom/UnionTypes.h" // For OwningUnrestrictedDoubleOrString
+#include "mozilla/ComputedTimingFunction.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StickyTimeDuration.h"
+#include "mozilla/TimeStamp.h" // for TimeDuration
+
+// X11 has a #define for None
+#ifdef None
+#undef None
+#endif
+#include "mozilla/dom/AnimationEffectReadOnlyBinding.h" // for FillMode
+ // and PlaybackDirection
+
+class nsIDocument;
+
+namespace mozilla {
+
+namespace dom {
+class UnrestrictedDoubleOrKeyframeEffectOptions;
+class UnrestrictedDoubleOrKeyframeAnimationOptions;
+}
+
+struct TimingParams
+{
+ TimingParams() = default;
+
+ static TimingParams FromOptionsUnion(
+ const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
+ nsIDocument* aDocument, ErrorResult& aRv);
+ static TimingParams FromOptionsUnion(
+ const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
+ nsIDocument* aDocument, ErrorResult& aRv);
+
+ // Range-checks and validates an UnrestrictedDoubleOrString or
+ // OwningUnrestrictedDoubleOrString object and converts to a
+ // StickyTimeDuration value or Nothing() if aDuration is "auto".
+ // Caller must check aRv.Failed().
+ template <class DoubleOrString>
+ static Maybe<StickyTimeDuration> ParseDuration(DoubleOrString& aDuration,
+ ErrorResult& aRv)
+ {
+ Maybe<StickyTimeDuration> result;
+ if (aDuration.IsUnrestrictedDouble()) {
+ double durationInMs = aDuration.GetAsUnrestrictedDouble();
+ if (durationInMs >= 0) {
+ result.emplace(StickyTimeDuration::FromMilliseconds(durationInMs));
+ } else {
+ aRv.ThrowTypeError<dom::MSG_ENFORCE_RANGE_OUT_OF_RANGE>(
+ NS_LITERAL_STRING("duration"));
+ }
+ } else if (!aDuration.GetAsString().EqualsLiteral("auto")) {
+ aRv.ThrowTypeError<dom::MSG_INVALID_DURATION_ERROR>(
+ aDuration.GetAsString());
+ }
+ return result;
+ }
+
+ static void ValidateIterationStart(double aIterationStart,
+ ErrorResult& aRv)
+ {
+ if (aIterationStart < 0) {
+ aRv.ThrowTypeError<dom::MSG_ENFORCE_RANGE_OUT_OF_RANGE>(
+ NS_LITERAL_STRING("iterationStart"));
+ }
+ }
+
+ static void ValidateIterations(double aIterations, ErrorResult& aRv)
+ {
+ if (IsNaN(aIterations) || aIterations < 0) {
+ aRv.ThrowTypeError<dom::MSG_ENFORCE_RANGE_OUT_OF_RANGE>(
+ NS_LITERAL_STRING("iterations"));
+ }
+ }
+
+ static Maybe<ComputedTimingFunction> ParseEasing(const nsAString& aEasing,
+ nsIDocument* aDocument,
+ ErrorResult& aRv);
+
+ // mDuration.isNothing() represents the "auto" value
+ Maybe<StickyTimeDuration> mDuration;
+ TimeDuration mDelay; // Initializes to zero
+ TimeDuration mEndDelay;
+ 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;
+
+ // Return the duration of the active interval calculated by duration and
+ // iteration count.
+ StickyTimeDuration ActiveDuration() const
+ {
+ // If either the iteration duration or iteration count is zero,
+ // Web Animations says that the active duration is zero. This is to
+ // ensure that the result is defined when the other argument is Infinity.
+ static const StickyTimeDuration zeroDuration;
+ if (!mDuration || *mDuration == zeroDuration || mIterations == 0.0) {
+ return zeroDuration;
+ }
+
+ return mDuration->MultDouble(mIterations);
+ }
+
+ StickyTimeDuration EndTime() const
+ {
+ return std::max(mDelay + ActiveDuration() + mEndDelay,
+ StickyTimeDuration());
+ }
+
+ bool operator==(const TimingParams& aOther) const;
+ bool operator!=(const TimingParams& aOther) const
+ {
+ return !(*this == aOther);
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_TimingParams_h
diff --git a/dom/animation/moz.build b/dom/animation/moz.build
new file mode 100644
index 0000000000..bd8c937072
--- /dev/null
+++ b/dom/animation/moz.build
@@ -0,0 +1,67 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+MOCHITEST_MANIFESTS += ['test/mochitest.ini']
+MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
+
+EXPORTS.mozilla.dom += [
+ 'Animation.h',
+ 'AnimationEffectReadOnly.h',
+ 'AnimationEffectTiming.h',
+ 'AnimationEffectTimingReadOnly.h',
+ 'AnimationTimeline.h',
+ 'CSSPseudoElement.h',
+ 'DocumentTimeline.h',
+ 'KeyframeEffect.h',
+ 'KeyframeEffectReadOnly.h',
+]
+
+EXPORTS.mozilla += [
+ 'AnimationComparator.h',
+ 'AnimationPerformanceWarning.h',
+ 'AnimationTarget.h',
+ 'AnimationUtils.h',
+ 'AnimValuesStyleRule.h',
+ 'ComputedTiming.h',
+ 'ComputedTimingFunction.h',
+ 'EffectCompositor.h',
+ 'EffectSet.h',
+ 'KeyframeEffectParams.h',
+ 'KeyframeUtils.h',
+ 'PendingAnimationTracker.h',
+ 'PseudoElementHashEntry.h',
+ 'TimingParams.h',
+]
+
+UNIFIED_SOURCES += [
+ 'Animation.cpp',
+ 'AnimationEffectReadOnly.cpp',
+ 'AnimationEffectTiming.cpp',
+ 'AnimationEffectTimingReadOnly.cpp',
+ 'AnimationPerformanceWarning.cpp',
+ 'AnimationTimeline.cpp',
+ 'AnimationUtils.cpp',
+ 'AnimValuesStyleRule.cpp',
+ 'ComputedTimingFunction.cpp',
+ 'CSSPseudoElement.cpp',
+ 'DocumentTimeline.cpp',
+ 'EffectCompositor.cpp',
+ 'EffectSet.cpp',
+ 'KeyframeEffect.cpp',
+ 'KeyframeEffectParams.cpp',
+ 'KeyframeEffectReadOnly.cpp',
+ 'KeyframeUtils.cpp',
+ 'PendingAnimationTracker.cpp',
+ 'TimingParams.cpp',
+]
+
+LOCAL_INCLUDES += [
+ '/dom/base',
+ '/layout/base',
+ '/layout/style',
+]
+
+FINAL_LIBRARY = 'xul'
diff --git a/dom/animation/test/chrome.ini b/dom/animation/test/chrome.ini
new file mode 100644
index 0000000000..9026bcbd2e
--- /dev/null
+++ b/dom/animation/test/chrome.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+support-files =
+ testcommon.js
+ ../../imptests/testharness.js
+ ../../imptests/testharnessreport.js
+ !/dom/animation/test/chrome/file_animate_xrays.html
+
+[chrome/test_animate_xrays.html]
+# file_animate_xrays.html needs to go in mochitest.ini since it is served
+# over HTTP
+[chrome/test_animation_observers.html]
+[chrome/test_animation_performance_warning.html]
+[chrome/test_animation_properties.html]
+[chrome/test_generated_content_getAnimations.html]
+[chrome/test_observers_for_sync_api.html]
+[chrome/test_restyles.html]
+[chrome/test_running_on_compositor.html]
diff --git a/dom/animation/test/chrome/file_animate_xrays.html b/dom/animation/test/chrome/file_animate_xrays.html
new file mode 100644
index 0000000000..8a68fc548f
--- /dev/null
+++ b/dom/animation/test/chrome/file_animate_xrays.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+<head>
+<meta charset=utf-8>
+<script>
+Element.prototype.animate = function() {
+ throw 'Called animate() as defined in content document';
+}
+// Bug 1211783: Use KeyframeEffect (not KeyframeEffectReadOnly) here
+for (var obj of [KeyframeEffectReadOnly, Animation]) {
+ obj = function() {
+ throw 'Called overridden ' + String(obj) + ' constructor';
+ };
+}
+</script>
+<body>
+<div id="target"></div>
+</body>
+</html>
diff --git a/dom/animation/test/chrome/test_animate_xrays.html b/dom/animation/test/chrome/test_animate_xrays.html
new file mode 100644
index 0000000000..56b981bf15
--- /dev/null
+++ b/dom/animation/test/chrome/test_animate_xrays.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994"
+ target="_blank">Mozilla Bug 1045994</a>
+<div id="log"></div>
+<iframe id="iframe"
+ src="http://example.org/tests/dom/animation/test/chrome/file_animate_xrays.html"></iframe>
+<script>
+'use strict';
+
+var win = document.getElementById('iframe').contentWindow;
+
+async_test(function(t) {
+ window.addEventListener('load', t.step_func(function() {
+ var target = win.document.getElementById('target');
+ var anim = target.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+ // In the x-ray case, the frames object will be given an opaque wrapper
+ // so it won't be possible to fetch any frames from it.
+ assert_equals(anim.effect.getKeyframes().length, 0);
+ t.done();
+ }));
+}, 'Calling animate() across x-rays');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_animation_observers.html b/dom/animation/test/chrome/test_animation_observers.html
new file mode 100644
index 0000000000..237128e041
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_observers.html
@@ -0,0 +1,1177 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test chrome-only MutationObserver animation notifications</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+<script src="../testcommon.js"></script>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+<style>
+@keyframes anim {
+ to { transform: translate(100px); }
+}
+@keyframes anotherAnim {
+ to { transform: translate(0px); }
+}
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: yellow;
+ line-height: 16px;
+}
+</style>
+<div id=container><div id=target></div></div>
+<script>
+var div = document.getElementById("target");
+var gRecords = [];
+var gObserver = new MutationObserver(function(newRecords) {
+ gRecords.push(...newRecords);
+});
+
+// Asynchronous testing framework based on layout/style/test/animation_utils.js.
+
+var gTests = [];
+var gCurrentTestName;
+
+function addAsyncAnimTest(aName, aOptions, aTestGenerator) {
+ aTestGenerator.testName = aName;
+ aTestGenerator.options = aOptions || {};
+ gTests.push(aTestGenerator);
+}
+
+function runAsyncTest(aTestGenerator) {
+ return waitForFrame().then(function() {
+ var generator;
+
+ function step(arg) {
+ var next;
+ try {
+ next = generator.next(arg);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ if (next.done) {
+ return Promise.resolve(next.value);
+ } else {
+ return Promise.resolve(next.value).then(step);
+ }
+ }
+
+ var subtree = aTestGenerator.options.subtree;
+
+ gCurrentTestName = aTestGenerator.testName;
+ if (subtree) {
+ gCurrentTestName += ":subtree";
+ }
+
+ gRecords = [];
+ gObserver.disconnect();
+ gObserver.observe(aTestGenerator.options.observe,
+ { animations: true, subtree: subtree});
+
+ generator = aTestGenerator();
+ return step();
+ });
+};
+
+function runAllAsyncTests() {
+ return gTests.reduce(function(sequence, test) {
+ return sequence.then(() => runAsyncTest(test));
+ }, Promise.resolve());
+}
+
+// Wrap is and ok with versions that prepend the current sub-test name
+// to the assertion description.
+var old_is = is, old_ok = ok;
+is = function(a, b, message) {
+ if (gCurrentTestName && message) {
+ message = `[${gCurrentTestName}] ${message}`;
+ }
+ old_is(a, b, message);
+}
+ok = function(a, message) {
+ if (gCurrentTestName && message) {
+ message = `[${gCurrentTestName}] ${message}`;
+ }
+ old_ok(a, message);
+}
+
+// Adds an event listener and returns a Promise that is resolved when the
+// event listener is called.
+function await_event(aElement, aEventName) {
+ return new Promise(function(aResolve) {
+ function listener(aEvent) {
+ aElement.removeEventListener(aEventName, listener);
+ aResolve();
+ }
+ aElement.addEventListener(aEventName, listener, false);
+ });
+}
+
+function assert_record_list(actual, expected, desc, index, listName) {
+ is(actual.length, expected.length, `${desc} - record[${index}].${listName} length`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ ok(actual.indexOf(expected[i]) != -1,
+ `${desc} - record[${index}].${listName} contains expected Animation`);
+ }
+}
+
+function assert_records(expected, desc) {
+ var records = gRecords;
+ gRecords = [];
+ is(records.length, expected.length, `${desc} - number of records`);
+ if (records.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < records.length; i++) {
+ assert_record_list(records[i].addedAnimations, expected[i].added, desc, i, "addedAnimations");
+ assert_record_list(records[i].changedAnimations, expected[i].changed, desc, i, "changedAnimations");
+ assert_record_list(records[i].removedAnimations, expected[i].removed, desc, i, "removedAnimations");
+ }
+}
+
+// -- Tests ------------------------------------------------------------------
+
+// We run all tests first targeting the div and observing the div, then again
+// targeting the div and observing its parent while using the subtree:true
+// MutationObserver option.
+
+[
+ { observe: div, target: div, subtree: false },
+ { observe: div.parentNode, target: div, subtree: true },
+].forEach(function(aOptions) {
+
+ var e = aOptions.target;
+
+ // Test that starting a single transition that completes normally
+ // dispatches an added notification and then a removed notification.
+ addAsyncAnimTest("single_transition", aOptions, function*() {
+ // Start a transition.
+ e.style = "transition: background-color 100s; background-color: lime;";
+
+ // Register for the end of the transition.
+ var transitionEnd = await_event(e, "transitionend");
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Advance until near the end of the transition, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ yield transitionEnd;
+
+ // After the transition has finished, the Animation should disappear.
+ is(e.getAnimations().length, 0,
+ "getAnimations().length after transition end");
+
+ // Wait for the change MutationRecord for seeking the Animation to be
+ // delivered, followed by the the removal MutationRecord.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition that is cancelled by resetting
+ // the transition-property property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_transition_cancelled_property", aOptions, function*() {
+ // Start a long transition.
+ e.style = "transition: background-color 100s; background-color: lime;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting transition-property.
+ e.style.transitionProperty = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition that is cancelled by setting
+ // style to the currently animated value dispatches an added
+ // notification and then a removed notification.
+ addAsyncAnimTest("single_transition_cancelled_value", aOptions, function*() {
+ // Start a long transition with a predictable value.
+ e.style = "transition: background-color 100s steps(2, end) -51s; background-color: lime;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting the current animation value.
+ var value = "rgb(128, 255, 0)";
+ is(getComputedStyle(e).backgroundColor, value, "half-way transition value");
+ e.style.backgroundColor = value;
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition that is cancelled by setting
+ // style to a non-interpolable value dispatches an added notification
+ // and then a removed notification.
+ addAsyncAnimTest("single_transition_cancelled_noninterpolable", aOptions, function*() {
+ // Start a long transition.
+ e.style = "transition: line-height 100s; line-height: 100px;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting line-height to a non-interpolable value.
+ e.style.lineHeight = "normal";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition and then reversing it
+ // dispatches an added notification, then a simultaneous removed and
+ // added notification, then a removed notification once finished.
+ addAsyncAnimTest("single_transition_reversed", aOptions, function*() {
+ // Start a long transition.
+ e.style = "transition: background-color 100s step-start; background-color: lime;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ var firstAnimation = animations[0];
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [firstAnimation], changed: [], removed: [] }],
+ "records after transition start");
+
+ // Wait for the Animation to be playing, then seek well into
+ // the transition.
+ yield firstAnimation.ready;
+ firstAnimation.currentTime = 50 * MS_PER_SEC;
+
+ // Reverse the transition by setting the background-color back to its
+ // original value.
+ e.style.backgroundColor = "yellow";
+
+ // The reversal should cause the creation of a new Animation.
+ animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition reversal");
+
+ var secondAnimation = animations[0];
+
+ ok(firstAnimation != secondAnimation,
+ "second Animation should be different from the first");
+
+ // Wait for the change Mutation record from seeking the first animation
+ // to be delivered, followed by a subsequent MutationRecord for the removal
+ // of the original Animation and the addition of the new Animation.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [firstAnimation], removed: [] },
+ { added: [secondAnimation], changed: [],
+ removed: [firstAnimation] }],
+ "records after transition reversal");
+
+ // Cancel the transition.
+ e.style.transitionProperty = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: [secondAnimation] }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that multiple transitions starting and ending on an element
+ // at the same time get batched up into a single MutationRecord.
+ addAsyncAnimTest("multiple_transitions", aOptions, function*() {
+ // Start three long transitions.
+ e.style = "transition-duration: 100s; " +
+ "transition-property: color, background-color, line-height; " +
+ "color: blue; background-color: lime; line-height: 24px;";
+
+ // The transitions should cause the creation of three Animations.
+ var animations = e.getAnimations();
+ is(animations.length, 3, "getAnimations().length after transition starts");
+
+ // Wait for the single MutationRecord for the Animation additions to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition starts");
+
+ // Wait for the Animations to get going.
+ yield animations[0].ready;
+ is(animations.filter(p => p.playState == "running").length, 3,
+ "number of running Animations");
+
+ // Seek well into each animation.
+ animations.forEach(p => p.currentTime = 50 * MS_PER_SEC);
+
+ // Prepare the set of expected change MutationRecords, one for each
+ // animation that was seeked.
+ var seekRecords = animations.map(
+ p => ({ added: [], changed: [p], removed: [] })
+ );
+
+ // Cancel one of the transitions by setting transition-property.
+ e.style.transitionProperty = "background-color, line-height";
+
+ var colorAnimation = animations.filter(p => p.playState != "running");
+ var otherAnimations = animations.filter(p => p.playState == "running");
+
+ is(colorAnimation.length, 1,
+ "number of non-running Animations after cancelling one");
+ is(otherAnimations.length, 2,
+ "number of running Animations after cancelling one");
+
+ // Wait for the MutationRecords to be delivered: one for each animation
+ // that was seeked, followed by one for the removal of the color animation.
+ yield waitForFrame();
+ assert_records(seekRecords.concat(
+ { added: [], changed: [], removed: colorAnimation }),
+ "records after color transition end");
+
+ // Cancel the remaining transitions.
+ e.style.transitionProperty = "none";
+
+ // Wait for the MutationRecord for the other two Animation
+ // removals to be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: otherAnimations }],
+ "records after other transition ends");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that completes normally
+ // dispatches an added notification and then a removed notification.
+ addAsyncAnimTest("single_animation", aOptions, function*() {
+ // Start an animation.
+ e.style = "animation: anim 100s;";
+
+ // Register for the end of the animation.
+ var animationEnd = await_event(e, "animationend");
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance until near the end of the animation, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ yield animationEnd;
+
+ // After the animation has finished, the Animation should disappear.
+ is(e.getAnimations().length, 0,
+ "getAnimations().length after animation end");
+
+ // Wait for the change MutationRecord from seeking the Animation to
+ // be delivered, followed by a further MutationRecord for the Animation
+ // removal.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by resetting
+ // the animation-name property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_name", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Cancel the animation by setting animation-name.
+ e.style.animationName = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-duration property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_duration", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance the animation by a second.
+ animations[0].currentTime += 1 * MS_PER_SEC;
+
+ // Cancel the animation by setting animation-duration to a value less
+ // than a second.
+ e.style.animationDuration = "0.1s";
+
+ // Wait for the change MutationRecord from seeking the Animation to
+ // be delivered, followed by a further MutationRecord for the Animation
+ // removal.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-delay property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_delay", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Cancel the animation by setting animation-delay.
+ e.style.animationDelay = "-200s";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-fill-mode property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_fill", aOptions, function*() {
+ // Start a short, filled animation.
+ e.style = "animation: anim 100s forwards;";
+
+ // Register for the end of the animation.
+ var animationEnd = await_event(e, "animationend");
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance until near the end of the animation, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ yield animationEnd;
+
+ // The only MutationRecord at this point should be the change from
+ // seeking the Animation.
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after animation starts filling");
+
+ // Cancel the animation by setting animation-fill-mode.
+ e.style.animationFillMode = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-iteration-count property dispatches an added notification
+ // and then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_iteration_count",
+ aOptions, function*() {
+ // Start a short, repeated animation.
+ e.style = "animation: anim 0.5s infinite;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance the animation until we are past the first iteration.
+ animations[0].currentTime += 1 * MS_PER_SEC;
+
+ // The only MutationRecord at this point should be the change from
+ // seeking the Animation.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after seeking animations");
+
+ // Cancel the animation by setting animation-iteration-count.
+ e.style.animationIterationCount = "1";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that updating an animation property dispatches a changed notification.
+ [
+ { name: "duration", prop: "animationDuration", val: "200s" },
+ { name: "timing", prop: "animationTimingFunction", val: "linear" },
+ { name: "iteration", prop: "animationIterationCount", val: "2" },
+ { name: "direction", prop: "animationDirection", val: "reverse" },
+ { name: "state", prop: "animationPlayState", val: "paused" },
+ { name: "delay", prop: "animationDelay", val: "-1s" },
+ { name: "fill", prop: "animationFillMode", val: "both" },
+ ].forEach(function(aChangeTest) {
+ addAsyncAnimTest(`single_animation_change_${aChangeTest.name}`, aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Change a property of the animation such that it keeps running.
+ e.style[aChangeTest.prop] = aChangeTest.val;
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after animation change");
+
+ // Cancel the animation.
+ e.style.animationName = "none";
+
+ // Wait for the addition, change and removal MutationRecords to be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+ });
+
+ // Test that calling finish() on a paused (but otherwise finished) animation
+ // dispatches a changed notification.
+ addAsyncAnimTest("finish_from_pause", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is playing.
+ yield animations[0].ready;
+
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+
+ // Wait for the pause to complete.
+ yield animations[0].ready;
+ is(animations[0].playState, "paused",
+ "playState after finishing and pausing");
+
+ // We should have two MutationRecords for the Animation changes:
+ // one for the finish, one for the pause.
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after finish() and pause()");
+
+ // Call finish() again.
+ animations[0].finish();
+ is(animations[0].playState, "finished",
+ "playState after finishing from paused state");
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered. Even though the currentTime does not change, the
+ // playState will change.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after finish() and pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling finish() on a pause-pending (but otherwise finished)
+ // animation dispatches a changed notification.
+ addAsyncAnimTest("finish_from_pause_pending", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is playing.
+ yield animations[0].ready;
+
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+ is(animations[0].playState, "pending",
+ "playState after finishing and calling pause()");
+
+ // Call finish() again to abort the pause
+ animations[0].finish();
+ is(animations[0].playState, "finished",
+ "playState after finishing and calling pause()");
+
+ // Wait for three MutationRecords for the Animation changes to
+ // be delivered: one for each finish(), pause(), finish() operation.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after finish(), pause(), finish()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on a paused Animation dispatches a changed
+ // notification.
+ addAsyncAnimTest("play", aOptions, function*() {
+ // Start a long, paused animation
+ e.style = "animation: anim 100s paused";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Play
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Redundant play
+ animations[0].play();
+
+ // Wait to ensure no change is dispatched
+ yield waitForFrame();
+ assert_records([], "records after redundant play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on a finished Animation that fills forwards
+ // dispatches a changed notification.
+ addAsyncAnimTest("play_filling_forwards", aOptions, function*() {
+ // Start a long animation with a forwards fill
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Seek to the end
+ animations[0].finish();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after finish()");
+
+ // Since we are filling forwards, calling play() should produce a
+ // change record since the animation remains relevant.
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on a finished Animation that does *not* fill
+ // forwards dispatches an addition notification.
+ addAsyncAnimTest("play_after_finish", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Seek to the end
+ animations[0].finish();
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after finish()");
+
+ // Since we are *not* filling forwards, calling play() is equivalent
+ // to creating a new animation since it becomes relevant again.
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling pause() on an Animation dispatches a changed
+ // notification.
+ addAsyncAnimTest("pause", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Pause
+ animations[0].pause();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Redundant pause
+ animations[0].pause();
+
+ // Wait to ensure no change is dispatched
+ yield animations[0].ready;
+ assert_records([], "records after redundant pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling pause() on an Animation that is pause-pending
+ // does not dispatch an additional changed notification.
+ addAsyncAnimTest("pause_while_pause_pending", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending, but pause again
+ animations[0].pause();
+
+ // We should only get a single change record
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on an Animation that is pause-pending
+ // dispatches a changed notification.
+ addAsyncAnimTest("aborted_pause", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending. If we play() now, we will abort the pause
+ animations[0].play();
+
+ // We should get two change records
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after aborting a pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that a non-cancelling change to an animation followed immediately by a
+ // cancelling change will only send an animation removal notification.
+ addAsyncAnimTest("coalesce_change_cancel", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Update the animation's delay such that it is still running.
+ e.style.animationDelay = "-1s";
+
+ // Then cancel the animation by updating its duration.
+ e.style.animationDuration = "0.5s";
+
+ // We should get a single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+});
+
+addAsyncAnimTest("tree_ordering", { observe: div, subtree: true }, function*() {
+ // Add style for pseudo elements
+ var extraStyle = document.createElement('style');
+ document.head.appendChild(extraStyle);
+ var sheet = extraStyle.sheet;
+ var rules = { ".before::before": "animation: anim 100s;",
+ ".after::after" : "animation: anim 100s, anim 100s;" };
+ for (var selector in rules) {
+ sheet.insertRule(selector + '{' + rules[selector] + '}',
+ sheet.cssRules.length);
+ }
+
+ // Create a tree with two children:
+ //
+ // div
+ // (::before)
+ // (::after)
+ // / \
+ // childA childB(::before)
+ var childA = document.createElement("div");
+ var childB = document.createElement("div");
+
+ div.appendChild(childA);
+ div.appendChild(childB);
+
+ // Start an animation on each (using order: childB, div, childA)
+ //
+ // We include multiple animations on some nodes so that we can test batching
+ // works as expected later in this test.
+ childB.style = "animation: anim 100s";
+ div.style = "animation: anim 100s, anim 100s, anim 100s";
+ childA.style = "animation: anim 100s, anim 100s";
+
+ // Start animations targeting to pseudo element of div and childB.
+ childB.classList.add("before");
+ div.classList.add("after");
+ div.classList.add("before");
+
+ // Check all animations we have in this document
+ var docAnims = document.getAnimations();
+ is(docAnims.length, 10, "total animations");
+
+ var divAnimations = div.getAnimations();
+ var childAAnimations = childA.getAnimations();
+ var childBAnimations = childB.getAnimations();
+ var divBeforeAnimations =
+ [ for (x of docAnims) if (x.effect.target.parentElement == div &&
+ x.effect.target.type == "::before") x ];
+ var divAfterAnimations =
+ [ for (x of docAnims) if (x.effect.target.parentElement == div &&
+ x.effect.target.type == "::after") x ];
+ var childBPseudoAnimations =
+ [ for (x of docAnims) if (x.effect.target.parentElement == childB) x ];
+
+ // The order in which we get the corresponding records is currently
+ // based on the order we visit these nodes when updating styles.
+ //
+ // That is because we don't do any document-level batching of animation
+ // mutation records when we flush styles. We may introduce that in the
+ // future but for now all we are interested in testing here is that the order
+ // these records are dispatched is consistent between runs.
+ //
+ // We currently expect to get records in order div::after, childA, childB,
+ // childB::before, div, div::before
+ yield waitForFrame();
+ assert_records([{ added: divAfterAnimations, changed: [], removed: [] },
+ { added: childAAnimations, changed: [], removed: [] },
+ { added: childBAnimations, changed: [], removed: [] },
+ { added: childBPseudoAnimations, changed: [], removed: [] },
+ { added: divAnimations, changed: [], removed: [] },
+ { added: divBeforeAnimations, changed: [], removed: [] }],
+ "records after simultaneous animation start");
+
+ // The one case where we *do* currently perform document-level (or actually
+ // timeline-level) batching is when animations are updated from a refresh
+ // driver tick. In particular, this means that when animations finish
+ // naturally the removed records should be dispatched according to the
+ // position of the elements in the tree.
+
+ // First, flatten the set of animations. we put the animations targeting to
+ // pseudo elements last. (Actually, we don't care the order in the list.)
+ var animations = [ ...divAnimations,
+ ...childAAnimations,
+ ...childBAnimations,
+ ...divBeforeAnimations,
+ ...divAfterAnimations,
+ ...childBPseudoAnimations ];
+
+ // Fast-forward to *just* before the end of the animation.
+ animations.forEach(animation => animation.currentTime = 99999);
+
+ // Prepare the set of expected change MutationRecords, one for each
+ // animation that was seeked.
+ var seekRecords = animations.map(
+ p => ({ added: [], changed: [p], removed: [] })
+ );
+
+ yield await_event(div, "animationend");
+
+ // After the changed notifications, which will be dispatched in the order that
+ // the animations were seeked, we should get removal MutationRecords in order
+ // (div, div::before, div::after), childA, (childB, childB::before).
+ // Note: The animations targeting to the pseudo element are appended after
+ // the animations of its parent element.
+ divAnimations = [ ...divAnimations,
+ ...divBeforeAnimations,
+ ...divAfterAnimations ];
+ childBAnimations = [ ...childBAnimations, ...childBPseudoAnimations ];
+ assert_records(seekRecords.concat(
+ { added: [], changed: [], removed: divAnimations },
+ { added: [], changed: [], removed: childAAnimations },
+ { added: [], changed: [], removed: childBAnimations }),
+ "records after finishing");
+
+ // Clean up
+ div.classList.remove("before");
+ div.classList.remove("after");
+ div.style = "";
+ childA.remove();
+ childB.remove();
+ extraStyle.remove();
+});
+
+// Run the tests.
+SimpleTest.requestLongerTimeout(2);
+SimpleTest.waitForExplicitFinish();
+
+runAllAsyncTests().then(function() {
+ SimpleTest.finish();
+}, function(aError) {
+ ok(false, "Something failed: " + aError);
+});
+</script>
diff --git a/dom/animation/test/chrome/test_animation_performance_warning.html b/dom/animation/test/chrome/test_animation_performance_warning.html
new file mode 100644
index 0000000000..a3bd63efcd
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_performance_warning.html
@@ -0,0 +1,957 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1196114 - Test metadata related to which animation properties
+ are running on the compositor</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<style>
+.compositable {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+@keyframes fade {
+ from { opacity: 1 }
+ to { opacity: 0 }
+}
+</style>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196114"
+ target="_blank">Mozilla Bug 1196114</a>
+<div id="log"></div>
+<script>
+'use strict';
+
+// This is used for obtaining localized strings.
+var gStringBundle;
+
+W3CTest.runner.requestLongerTimeout(2);
+
+SpecialPowers.pushPrefEnv({ "set": [
+ ["general.useragent.locale", "en-US"],
+ // Need to set devPixelsPerPx explicitly to gain
+ // consistent pixel values in warning messages
+ // regardless of platform DPIs.
+ ["layout.css.devPixelsPerPx", 1],
+ ] },
+ start);
+
+function compare_property_state(a, b) {
+ if (a.property > b.property) {
+ return -1;
+ } else if (a.property < b.property) {
+ return 1;
+ }
+ if (a.runningOnCompositor != b.runningOnCompositor) {
+ return a.runningOnCompositor ? 1 : -1;
+ }
+ return a.warning > b.warning ? -1 : 1;
+}
+
+function assert_animation_property_state_equals(actual, expected) {
+ assert_equals(actual.length, expected.length, 'Number of properties');
+
+ var sortedActual = actual.sort(compare_property_state);
+ var sortedExpected = expected.sort(compare_property_state);
+
+ for (var i = 0; i < sortedActual.length; i++) {
+ assert_equals(sortedActual[i].property,
+ sortedExpected[i].property,
+ 'CSS property name should match');
+ assert_equals(sortedActual[i].runningOnCompositor,
+ sortedExpected[i].runningOnCompositor,
+ 'runningOnCompositor property should match');
+ if (sortedExpected[i].warning instanceof RegExp) {
+ assert_regexp_match(sortedActual[i].warning,
+ sortedExpected[i].warning,
+ 'warning message should match');
+ } else if (sortedExpected[i].warning) {
+ assert_equals(sortedActual[i].warning,
+ gStringBundle.GetStringFromName(sortedExpected[i].warning),
+ 'warning message should match');
+ }
+ }
+}
+
+// Check that the animation is running on compositor and
+// warning property is not set for the CSS property regardless
+// expected values.
+function assert_property_state_on_compositor(actual, expected) {
+ assert_equals(actual.length, expected.length);
+
+ var sortedActual = actual.sort(compare_property_state);
+ var sortedExpected = expected.sort(compare_property_state);
+
+ for (var i = 0; i < sortedActual.length; i++) {
+ assert_equals(sortedActual[i].property,
+ sortedExpected[i].property,
+ 'CSS property name should match');
+ assert_true(sortedActual[i].runningOnCompositor,
+ 'runningOnCompositor property should be true on ' +
+ sortedActual[i].property);
+ assert_not_exists(sortedActual[i], 'warning',
+ 'warning property should not be set');
+ }
+}
+
+var gAnimationsTests = [
+ {
+ desc: 'animations on compositor',
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'animations on main thread',
+ frames: {
+ backgroundColor: ['white', 'red']
+ },
+ expected: [
+ {
+ property: 'background-color',
+ runningOnCompositor: false
+ }
+ ]
+ },
+ {
+ desc: 'animations on both threads',
+ frames: {
+ backgroundColor: ['white', 'red'],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'background-color',
+ runningOnCompositor: false
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'two animation properties on compositor thread',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'opacity on compositor with animation of geometric properties',
+ frames: {
+ width: ['100px', '200px'],
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+];
+
+// Test cases that check results of adding/removing a 'width' property on the
+// same animation object.
+var gAnimationWithGeometricKeyframeTests = [
+ {
+ desc: 'transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ },
+ {
+ desc: 'opacity and transform',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ },
+];
+
+// Performance warning tests that set and clear a style property.
+var gPerformanceWarningTestsStyle = [
+ {
+ desc: 'preserve-3d transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'transform-style: preserve-3d',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformPreserve3D'
+ }
+ ]
+ },
+ {
+ desc: 'transform with backface-visibility:hidden',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ {
+ desc: 'opacity and transform with preserve-3d',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'transform-style: preserve-3d',
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformPreserve3D'
+ }
+ ]
+ },
+ {
+ desc: 'opacity and transform with backface-visibility:hidden',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+];
+
+// Performance warning tests that set and clear the id property
+var gPerformanceWarningTestsId= [
+ {
+ desc: 'moz-element referencing a transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ id: 'transformed',
+ createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ }
+ ]
+ },
+];
+
+var gMultipleAsyncAnimationsTests = [
+ {
+ desc: 'opacity and transform with preserve-3d',
+ style: 'transform-style: preserve-3d',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformPreserve3D'
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+ {
+ desc: 'opacity and transform with backface-visibility:hidden',
+ style: 'backface-visibility: hidden;',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+];
+
+// Test cases that check results of adding/removing a 'width' keyframe on the
+// same animation object, where multiple animation objects belong to the same
+// element.
+// The 'width' property is added to animations[1].
+var gMultipleAsyncAnimationsWithGeometricKeyframeTests = [
+ {
+ desc: 'transform and opacity with geometric keyframes',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false,
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ }
+ ],
+ },
+ {
+ desc: 'opacity and transform with geometric keyframes',
+ animations: [
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false,
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ }
+ ]
+ },
+];
+
+// Test cases that check results of adding/removing 'width' animation on the
+// same element which has async animations.
+var gMultipleAsyncAnimationsWithGeometricAnimationTests = [
+ {
+ desc: 'transform',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'opacity',
+ animations: [
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'opacity and transform',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+];
+
+var gAnimationsOnTooSmallElementTests = [
+ {
+ desc: 'opacity on too small element',
+ frames: {
+ opacity: [0, 1]
+ },
+ style: { style: 'width: 8px; height: 8px; background-color: red;' +
+ // We need to set transform here to try creating an
+ // individual frame for this opacity element.
+ // Without this, this small element is created on the same
+ // nsIFrame of mochitest iframe, i.e. the document which are
+ // running this test, as a result the layer corresponding
+ // to the frame is sent to compositor.
+ 'transform: translateX(100px);' },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: false,
+ warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/
+ }
+ ]
+ },
+ {
+ desc: 'transform on too small element',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: { style: 'width: 8px; height: 8px; background-color: red;' },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/
+ }
+ ]
+ },
+];
+
+function start() {
+ var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
+ .getService(SpecialPowers.Ci.nsIStringBundleService);
+ gStringBundle = bundleService
+ .createBundle("chrome://global/locale/layout_errors.properties");
+
+ gAnimationsTests.forEach(function(subtest) {
+ promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ });
+ }, subtest.desc);
+ });
+
+ gAnimationWithGeometricKeyframeTests.forEach(function(subtest) {
+ promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ // First, a transform animation is running on compositor.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withoutGeometric);
+ }).then(function() {
+ // Add a 'width' property.
+ var keyframes = animation.effect.getKeyframes();
+
+ keyframes[0].width = '100px';
+ keyframes[1].width = '200px';
+
+ animation.effect.setKeyframes(keyframes);
+ return waitForFrame();
+ }).then(function() {
+ // Now the transform animation is not running on compositor because of
+ // the 'width' property.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withGeometric);
+ }).then(function() {
+ // Remove the 'width' property.
+ var keyframes = animation.effect.getKeyframes();
+
+ delete keyframes[0].width;
+ delete keyframes[1].width;
+
+ animation.effect.setKeyframes(keyframes);
+ return waitForFrame();
+ }).then(function() {
+ // Finally, the transform animation is running on compositor.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withoutGeometric);
+ });
+ }, 'An animation has: ' + subtest.desc);
+ });
+
+ gPerformanceWarningTestsStyle.forEach(function(subtest) {
+ promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_property_state_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.style = subtest.style;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.style = '';
+ return waitForFrame();
+ }).then(function() {
+ assert_property_state_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ });
+ }, subtest.desc);
+ });
+
+ gPerformanceWarningTestsId.forEach(function(subtest) {
+ promise_test(function(t) {
+ if (subtest.createelement) {
+ addDiv(t, { style: subtest.createelement });
+ }
+
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_property_state_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.id = subtest.id;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.id = '';
+ return waitForFrame();
+ }).then(function() {
+ assert_property_state_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ });
+ }, subtest.desc);
+ });
+
+ gMultipleAsyncAnimationsTests.forEach(function(subtest) {
+ promise_test(function(t) {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(function(anim) {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+ return waitForAllAnimations(animations).then(function() {
+ animations.forEach(function(anim) {
+ assert_property_state_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ div.style = subtest.style;
+ return waitForFrame();
+ }).then(function() {
+ animations.forEach(function(anim) {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ div.style = '';
+ return waitForFrame();
+ }).then(function() {
+ animations.forEach(function(anim) {
+ assert_property_state_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ });
+ }, 'Multiple animations: ' + subtest.desc);
+ });
+
+ gMultipleAsyncAnimationsWithGeometricKeyframeTests.forEach(function(subtest) {
+ promise_test(function(t) {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(function(anim) {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+ return waitForAllAnimations(animations).then(function() {
+ // First, all animations are running on compositor.
+ animations.forEach(function(anim) {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withoutGeometric);
+ });
+ }).then(function() {
+ // Add a 'width' property to animations[1].
+ var keyframes = animations[1].effect.getKeyframes();
+
+ keyframes[0].width = '100px';
+ keyframes[1].width = '200px';
+
+ animations[1].effect.setKeyframes(keyframes);
+ return waitForFrame();
+ }).then(function() {
+ // Now the transform animation is not running on compositor because of
+ // the 'width' property.
+ animations.forEach(function(anim) {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withGeometric);
+ });
+ }).then(function() {
+ // Remove the 'width' property from animations[1].
+ var keyframes = animations[1].effect.getKeyframes();
+
+ delete keyframes[0].width;
+ delete keyframes[1].width;
+
+ animations[1].effect.setKeyframes(keyframes);
+ return waitForFrame();
+ }).then(function() {
+ // Finally, all animations are running on compositor.
+ animations.forEach(function(anim) {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withoutGeometric);
+ });
+ });
+ }, 'Multiple animations with geometric property: ' + subtest.desc);
+ });
+
+ gMultipleAsyncAnimationsWithGeometricAnimationTests.forEach(function(subtest) {
+ promise_test(function(t) {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(function(anim) {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+
+ var widthAnimation;
+
+ return waitForAllAnimations(animations).then(function() {
+ animations.forEach(function(anim) {
+ assert_property_state_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ }).then(function() {
+ // Append 'width' animation on the same element.
+ widthAnimation = div.animate({ width: ['100px', '200px'] },
+ 100 * MS_PER_SEC);
+ return waitForFrame();
+ }).then(function() {
+ // Now transform animations are not running on compositor because of
+ // the 'width' animation.
+ animations.forEach(function(anim) {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ // Remove the 'width' animation.
+ widthAnimation.cancel();
+ return waitForFrame();
+ }).then(function() {
+ // Now all animations are running on compositor.
+ animations.forEach(function(anim) {
+ assert_property_state_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ });
+ }, 'Multiple async animations and geometric animation: ' + subtest.desc);
+ });
+
+ gAnimationsOnTooSmallElementTests.forEach(function(subtest) {
+ promise_test(function(t) {
+ var div = addDiv(t, subtest.style);
+ var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ });
+ }, subtest.desc);
+ });
+
+ promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ { transform: [ 'translate(0px)',
+ 'translate(100px)'] },
+ 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: 'transform', runningOnCompositor: true } ]);
+ animation.effect.target.style = 'width: 10000px; height: 10000px';
+ return waitForFrame();
+ }).then(function() {
+ // viewport depends on test environment.
+ var expectedWarning = new RegExp(
+ "Animation cannot be run on the compositor because the frame size " +
+ "\\(10000, 10000\\) is bigger than the viewport \\(\\d+, \\d+\\) " +
+ "or the visual rectangle \\(10000, 10000\\) is larger than the " +
+ "maximum allowed value \\(\\d+\\)");
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: expectedWarning
+ } ]);
+ animation.effect.target.style = 'width: 100px; height: 100px';
+ return waitForFrame();
+ }).then(function() {
+ // FIXME: Bug 1253164: the animation should get back on compositor.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: 'transform', runningOnCompositor: false } ]);
+ });
+ }, 'transform on too big element');
+
+ promise_test(function(t) {
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ svg.setAttribute('width', '100');
+ svg.setAttribute('height', '100');
+ var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ rect.setAttribute('width', '100');
+ rect.setAttribute('height', '100');
+ rect.setAttribute('fill', 'red');
+ svg.appendChild(rect);
+ document.body.appendChild(svg);
+ t.add_cleanup(function() {
+ svg.remove();
+ });
+
+ var animation = svg.animate(
+ { transform: ['translate(0px)', 'translate(100px)'] }, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: 'transform', runningOnCompositor: true } ]);
+ svg.setAttribute('transform', 'translate(10, 20)');
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformSVG'
+ } ]);
+ svg.removeAttribute('transform');
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: 'transform', runningOnCompositor: true } ]);
+ });
+ }, 'transform of nsIFrame with SVG transform');
+
+ promise_test(function(t) {
+ var div = addDiv(t, { class: 'compositable',
+ style: 'animation: fade 100s' });
+ var cssAnimation = div.getAnimations()[0];
+ var scriptAnimation = div.animate({ opacity: [ 1, 0 ] }, 100 * MS_PER_SEC);
+ return scriptAnimation.ready.then(function() {
+ assert_animation_property_state_equals(
+ cssAnimation.effect.getProperties(),
+ [ { property: 'opacity', runningOnCompositor: true } ]);
+ assert_animation_property_state_equals(
+ scriptAnimation.effect.getProperties(),
+ [ { property: 'opacity', runningOnCompositor: true } ]);
+ });
+ }, 'overridden animation');
+}
+
+</script>
+
+</body>
diff --git a/dom/animation/test/chrome/test_animation_properties.html b/dom/animation/test/chrome/test_animation_properties.html
new file mode 100644
index 0000000000..5349013062
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_properties.html
@@ -0,0 +1,993 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1254419 - Test the values returned by
+ KeyframeEffectReadOnly.getProperties()</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1254419"
+ target="_blank">Mozilla Bug 1254419</a>
+<div id="log"></div>
+<style>
+
+:root {
+ --var-100px: 100px;
+ --var-100px-200px: 100px 200px;
+}
+div {
+ font-size: 10px; /* For calculating em-based units */
+}
+</style>
+<script>
+'use strict';
+
+function assert_properties_equal(actual, expected) {
+ assert_equals(actual.length, expected.length);
+
+ var compareProperties = (a, b) =>
+ a.property == b.property ? 0 : (a.property < b.property ? -1 : 1);
+
+ var sortedActual = actual.sort(compareProperties);
+ var sortedExpected = expected.sort(compareProperties);
+
+ // We want to serialize the values in the following form:
+ //
+ // { offset: 0, easing: linear, composite: replace, value: 5px }, ...
+ //
+ // So that we can just compare strings and, in the failure case,
+ // easily see where the differences lie.
+ var serializeMember = value => {
+ return typeof value === 'undefined' ? '<not set>' : value;
+ }
+ var serializeValues = values =>
+ values.map(value =>
+ '{ ' +
+ [ 'offset', 'value', 'easing', 'composite' ].map(
+ member => `${member}: ${serializeMember(value[member])}`
+ ).join(', ') +
+ ' }')
+ .join(', ');
+
+ for (var i = 0; i < sortedActual.length; i++) {
+ assert_equals(sortedActual[i].property,
+ sortedExpected[i].property,
+ 'CSS property name should match');
+ assert_equals(serializeValues(sortedActual[i].values),
+ serializeValues(sortedExpected[i].values),
+ `Values arrays do not match for `
+ + `${sortedActual[i].property} property`);
+ }
+}
+
+// Shorthand for constructing a value object
+function value(offset, value, composite, easing) {
+ return { offset: offset, value: value, easing: easing, composite: composite };
+}
+
+var gTests = [
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for property-indexed specifications
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a one-property two-value property-indexed specification',
+ frames: { left: ['10px', '20px'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ]
+ },
+ { desc: 'a one-shorthand-property two-value property-indexed'
+ + ' specification',
+ frames: { margin: ['10px', '10px 20px 30px 40px'] },
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '10px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a two-property (one shorthand and one of its longhand'
+ + ' components) two-value property-indexed specification',
+ frames: { marginTop: ['50px', '60px'],
+ margin: ['10px', '10px 20px 30px 40px'] },
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '50px', 'replace', 'linear'),
+ value(1, '60px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a two-property property-indexed specification with different'
+ + ' numbers of values',
+ frames: { left: ['10px', '20px', '30px'],
+ top: ['40px', '50px'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.5, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a property-indexed specification with an invalid value',
+ frames: { left: ['10px', '20px', '30px', '40px', '50px'],
+ top: ['15px', '25px', 'invalid', '45px', '55px'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.25, '20px', 'replace', 'linear'),
+ value(0.5, '30px', 'replace', 'linear'),
+ value(0.75, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '15px', 'replace', 'linear'),
+ value(0.25, '25px', 'replace', 'linear'),
+ value(0.75, '45px', 'replace', 'linear'),
+ value(1, '55px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-value property-indexed specification that'
+ + ' needs to stringify its values',
+ frames: { opacity: [0, 1] },
+ expected: [ { property: 'opacity',
+ values: [ value(0, '0', 'replace', 'linear'),
+ value(1, '1', 'replace') ] } ]
+ },
+ { desc: 'a property-indexed keyframe where a lesser shorthand precedes'
+ + ' a greater shorthand',
+ frames: { borderLeft: [ '1px solid rgb(1, 2, 3)',
+ '2px solid rgb(4, 5, 6)' ],
+ border: [ '3px dotted rgb(7, 8, 9)',
+ '4px dashed rgb(10, 11, 12)' ] },
+ expected: [ { property: 'border-bottom-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ value(1, 'rgb(4, 5, 6)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ value(0, '1px', 'replace', 'linear'),
+ value(1, '2px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ value(0, 'solid', 'replace', 'linear'),
+ value(1, 'solid', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+ value(1, '0 0 0 0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+ value(1, 'stretch stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ value(0, '100% 100% 100% 100%',
+ 'replace', 'linear'),
+ value(1, '100% 100% 100% 100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+ value(1, '1 1 1 1', 'replace') ] },
+ { property: '-moz-border-bottom-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-left-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-right-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-top-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] } ]
+ },
+ { desc: 'a property-indexed keyframe where a greater shorthand precedes'
+ + ' a lesser shorthand',
+ frames: { border: [ '3px dotted rgb(7, 8, 9)',
+ '4px dashed rgb(10, 11, 12)' ],
+ borderLeft: [ '1px solid rgb(1, 2, 3)',
+ '2px solid rgb(4, 5, 6)' ] },
+ expected: [ { property: 'border-bottom-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ value(1, 'rgb(4, 5, 6)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ value(0, '1px', 'replace', 'linear'),
+ value(1, '2px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ value(0, 'solid', 'replace', 'linear'),
+ value(1, 'solid', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+ value(1, '0 0 0 0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+ value(1, 'stretch stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ value(0, '100% 100% 100% 100%',
+ 'replace', 'linear'),
+ value(1, '100% 100% 100% 100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+ value(1, '1 1 1 1', 'replace') ] },
+ { property: '-moz-border-bottom-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-left-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-right-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-top-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for keyframe sequences
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a keyframe sequence specification with repeated values at'
+ + ' offset 0/1 with different easings',
+ frames: [ { offset: 0.0, left: '100px', easing: 'ease' },
+ { offset: 0.0, left: '200px', easing: 'ease' },
+ { offset: 0.5, left: '300px', easing: 'linear' },
+ { offset: 1.0, left: '400px', easing: 'ease-out' },
+ { offset: 1.0, left: '500px', easing: 'step-end' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '100px', 'replace'),
+ value(0, '200px', 'replace', 'ease'),
+ value(0.5, '300px', 'replace', 'linear'),
+ value(1, '400px', 'replace'),
+ value(1, '500px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-keyframe sequence',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 1, left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ]
+ },
+ { desc: 'a two-property two-keyframe sequence',
+ frames: [ { offset: 0, left: '10px', top: '30px' },
+ { offset: 1, left: '20px', top: '40px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '30px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a one shorthand property two-keyframe sequence',
+ frames: [ { offset: 0, margin: '10px' },
+ { offset: 1, margin: '20px 30px 40px 50px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a two-property (a shorthand and one of its component longhands)'
+ + ' two-keyframe sequence',
+ frames: [ { offset: 0, margin: '10px', marginTop: '20px' },
+ { offset: 1, marginTop: '70px',
+ margin: '30px 40px 50px 60px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(1, '70px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with duplicate values for a given interior'
+ + ' offset',
+ frames: [ { offset: 0.0, left: '10px' },
+ { offset: 0.5, left: '20px' },
+ { offset: 0.5, left: '30px' },
+ { offset: 0.5, left: '40px' },
+ { offset: 1.0, left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.5, '20px', 'replace'),
+ value(0.5, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with duplicate values for offsets 0 and 1',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 0, left: '20px' },
+ { offset: 0, left: '30px' },
+ { offset: 1, left: '40px' },
+ { offset: 1, left: '50px' },
+ { offset: 1, left: '60px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace'),
+ value(0, '30px', 'replace', 'linear'),
+ value(1, '40px', 'replace'),
+ value(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a two-property four-keyframe sequence',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 0, top: '20px' },
+ { offset: 1, top: '30px' },
+ { offset: 1, left: '40px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a one-property keyframe sequence with some omitted offsets',
+ frames: [ { offset: 0.00, left: '10px' },
+ { offset: 0.25, left: '20px' },
+ { left: '30px' },
+ { left: '40px' },
+ { offset: 1.00, left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.25, '20px', 'replace', 'linear'),
+ value(0.5, '30px', 'replace', 'linear'),
+ value(0.75, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a two-property keyframe sequence with some omitted offsets',
+ frames: [ { offset: 0.00, left: '10px', top: '20px' },
+ { offset: 0.25, left: '30px' },
+ { left: '40px' },
+ { left: '50px', top: '60px' },
+ { offset: 1.00, left: '70px', top: '80px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.25, '30px', 'replace', 'linear'),
+ value(0.5, '40px', 'replace', 'linear'),
+ value(0.75, '50px', 'replace', 'linear'),
+ value(1, '70px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(0.75, '60px', 'replace', 'linear'),
+ value(1, '80px', 'replace') ] } ]
+ },
+ { desc: 'a one-property keyframe sequence with all omitted offsets',
+ frames: [ { left: '10px' },
+ { left: '20px' },
+ { left: '30px' },
+ { left: '40px' },
+ { left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.25, '20px', 'replace', 'linear'),
+ value(0.5, '30px', 'replace', 'linear'),
+ value(0.75, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with different easing values, but the'
+ + ' same easing value for a given offset',
+ frames: [ { offset: 0.0, easing: 'ease', left: '10px'},
+ { offset: 0.0, easing: 'ease', top: '20px'},
+ { offset: 0.5, easing: 'linear', left: '30px' },
+ { offset: 0.5, easing: 'linear', top: '40px' },
+ { offset: 1.0, easing: 'step-end', left: '50px' },
+ { offset: 1.0, easing: 'step-end', top: '60px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'ease'),
+ value(0.5, '30px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '20px', 'replace', 'ease'),
+ value(0.5, '40px', 'replace', 'linear'),
+ value(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-keyframe sequence that needs to'
+ + ' stringify its values',
+ frames: [ { offset: 0, opacity: 0 },
+ { offset: 1, opacity: 1 } ],
+ expected: [ { property: 'opacity',
+ values: [ value(0, '0', 'replace', 'linear'),
+ value(1, '1', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where shorthand precedes longhand',
+ frames: [ { offset: 0, margin: '10px', marginRight: '20px' },
+ { offset: 1, margin: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where longhand precedes shorthand',
+ frames: [ { offset: 0, marginRight: '20px', margin: '10px' },
+ { offset: 1, margin: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where lesser shorthand precedes greater'
+ + ' shorthand',
+ frames: [ { offset: 0, borderLeft: '1px solid rgb(1, 2, 3)',
+ border: '2px dotted rgb(4, 5, 6)' },
+ { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
+ expected: [ { property: 'border-bottom-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ value(0, '1px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ value(0, 'solid', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+ value(1, '0 0 0 0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+ value(1, 'stretch stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ value(0, '100% 100% 100% 100%',
+ 'replace', 'linear'),
+ value(1, '100% 100% 100% 100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+ value(1, '1 1 1 1', 'replace') ] },
+ { property: '-moz-border-bottom-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-left-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-right-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-top-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where greater shorthand precedes'
+ + ' lesser shorthand',
+ frames: [ { offset: 0, border: '2px dotted rgb(4, 5, 6)',
+ borderLeft: '1px solid rgb(1, 2, 3)' },
+ { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
+ expected: [ { property: 'border-bottom-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ value(0, '1px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ value(0, 'solid', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+ value(1, '0 0 0 0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+ value(1, 'stretch stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ value(0, '100% 100% 100% 100%',
+ 'replace', 'linear'),
+ value(1, '100% 100% 100% 100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+ value(1, '1 1 1 1', 'replace') ] },
+ { property: '-moz-border-bottom-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-left-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-right-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-top-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for unit conversion
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'em units are resolved to px values',
+ frames: { left: ['10em', '20em'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '100px', 'replace', 'linear'),
+ value(1, '200px', 'replace') ] } ]
+ },
+ { desc: 'calc() expressions are resolved to the equivalent units',
+ frames: { left: ['calc(10em + 10px)', 'calc(10em + 10%)'] },
+ expected: [ { property: 'left',
+ values: [ value(0, 'calc(110px)', 'replace', 'linear'),
+ value(1, 'calc(100px + 10%)', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for CSS variable handling conversion
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'CSS variables are resolved to their corresponding values',
+ frames: { left: ['10px', 'var(--var-100px)'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '100px', 'replace') ] } ]
+ },
+ { desc: 'CSS variables in calc() expressions are resolved',
+ frames: { left: ['10px', 'calc(var(--var-100px) / 2 - 10%)'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, 'calc(50px + -10%)', 'replace') ] } ]
+ },
+ { desc: 'CSS variables in shorthands are resolved to their corresponding'
+ + ' values',
+ frames: { margin: ['10px', 'var(--var-100px-200px)'] },
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '100px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '200px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '100px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '200px', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for properties that parse correctly but which we fail to
+ // convert to computed values.
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe',
+ frames: [ { margin: '5px', simulateComputeValuesFailure: true },
+ { margin: '5px' } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe where we have enough values to create'
+ + ' a final segment',
+ frames: [ { margin: '5px', simulateComputeValuesFailure: true },
+ { margin: '5px' },
+ { margin: '5px' } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial overlapping keyframes (first in series of two)',
+ frames: [ { margin: '5px', offset: 0,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 0 },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial overlapping keyframes (second in series of two)',
+ frames: [ { margin: '5px', offset: 0 },
+ { margin: '5px', offset: 0,
+ simulateComputeValuesFailure: true },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial overlapping keyframes (second in series of three)',
+ frames: [ { margin: '5px', offset: 0 },
+ { margin: '5px', offset: 0,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 0 },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe',
+ frames: [ { margin: '5px' },
+ { margin: '5px', simulateComputeValuesFailure: true } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe where it forms the last segment in the series',
+ frames: [ { margin: '5px' },
+ { margin: '5px',
+ marginLeft: '5px',
+ marginRight: '5px',
+ marginBottom: '5px',
+ // margin-top sorts last and only it will be missing since
+ // the other longhand components are specified
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe where we have enough values to create'
+ + ' an initial segment',
+ frames: [ { margin: '5px' },
+ { margin: '5px' },
+ { margin: '5px', simulateComputeValuesFailure: true } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final overlapping keyframes (first in series of two)',
+ frames: [ { margin: '5px' },
+ { margin: '5px', offset: 1,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 1 } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final overlapping keyframes (second in series of two)',
+ frames: [ { margin: '5px' },
+ { margin: '5px', offset: 1 },
+ { margin: '5px', offset: 1,
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final overlapping keyframes (second in series of three)',
+ frames: [ { margin: '5px' },
+ { margin: '5px', offset: 1 },
+ { margin: '5px', offset: 1,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 1 } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' intermediate keyframe',
+ frames: [ { margin: '5px' },
+ { margin: '5px', simulateComputeValuesFailure: true },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe along with other values',
+ // simulateComputeValuesFailure only applies to shorthands so we can set
+ // it on the same keyframe and it will only apply to |margin| and not
+ // |left|.
+ frames: [ { margin: '77%', left: '10px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe along with other values where those'
+ + ' values sort after the property with missing values',
+ frames: [ { margin: '77%', right: '10px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', right: '20px' } ],
+ expected: [ { property: 'right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe along with other values',
+ frames: [ { margin: '5px', left: '10px' },
+ { margin: '5px', left: '20px',
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe along with other values where those'
+ + ' values sort after the property with missing values',
+ frames: [ { margin: '5px', right: '10px' },
+ { margin: '5px', right: '20px',
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' an intermediate keyframe along with other values',
+ frames: [ { margin: '5px', left: '10px' },
+ { margin: '5px', left: '20px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', left: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.5, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' an intermediate keyframe by itself',
+ frames: [ { margin: '5px', left: '10px' },
+ { margin: '5px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', left: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+];
+
+gTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
+ assert_properties_equal(animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_generated_content_getAnimations.html b/dom/animation/test/chrome/test_generated_content_getAnimations.html
new file mode 100644
index 0000000000..04e0f9bd09
--- /dev/null
+++ b/dom/animation/test/chrome/test_generated_content_getAnimations.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<head>
+<meta charset=utf-8>
+<title>Test getAnimations() for generated-content elements</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<style>
+@keyframes anim { }
+@keyframes anim2 { }
+.before::before {
+ content: '';
+ animation: anim 100s;
+}
+.after::after {
+ content: '';
+ animation: anim 100s, anim2 100s;
+}
+</style>
+</head>
+<body>
+<div id='root' class='before after'>
+ <div class='before'></div>
+ <div></div>
+</div>
+<script>
+'use strict';
+
+const {Cc, Ci, Cu} = SpecialPowers;
+
+function getWalker(node) {
+ var walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].
+ createInstance(Ci.inIDeepTreeWalker);
+ walker.showAnonymousContent = true;
+ walker.init(node.ownerDocument, Ci.nsIDOMNodeFilter.SHOW_ALL);
+ walker.currentNode = node;
+ return walker;
+}
+
+test(function(t) {
+ var root = document.getElementById('root');
+ // Flush first to make sure the generated-content elements are ready
+ // in the tree.
+ flushComputedStyle(root);
+ var before = getWalker(root).firstChild();
+ var after = getWalker(root).lastChild();
+
+ // Sanity Checks
+ assert_equals(document.getAnimations().length, 4,
+ 'All animations in this document');
+ assert_equals(before.tagName, '_moz_generated_content_before',
+ 'First child is ::before element');
+ assert_equals(after.tagName, '_moz_generated_content_after',
+ 'Last child is ::after element');
+
+ // Test Element.getAnimations() for generated-content elements
+ assert_equals(before.getAnimations().length, 1,
+ 'Animations of ::before generated-content element');
+ assert_equals(after.getAnimations().length, 2,
+ 'Animations of ::after generated-content element');
+}, 'Element.getAnimations() used on generated-content elements');
+
+test(function(t) {
+ var root = document.getElementById('root');
+ flushComputedStyle(root);
+ var walker = getWalker(root);
+
+ var animations = [];
+ var element = walker.currentNode;
+ while (element) {
+ if (element.getAnimations) {
+ animations = [...animations, ...element.getAnimations()];
+ }
+ element = walker.nextNode();
+ }
+
+ assert_equals(animations.length, document.getAnimations().length,
+ 'The number of animations got by DeepTreeWalker and ' +
+ 'document.getAnimations() should be the same');
+}, 'Element.getAnimations() used by traversing DeepTreeWalker');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_observers_for_sync_api.html b/dom/animation/test/chrome/test_observers_for_sync_api.html
new file mode 100644
index 0000000000..20c3f3670d
--- /dev/null
+++ b/dom/animation/test/chrome/test_observers_for_sync_api.html
@@ -0,0 +1,854 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>
+Test chrome-only MutationObserver animation notifications for sync APIs
+</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<div id="log"></div>
+<style>
+@keyframes anim {
+ to { transform: translate(100px); }
+}
+@keyframes anotherAnim {
+ to { transform: translate(0px); }
+}
+</style>
+<script>
+
+function assert_record_list(actual, expected, desc, index, listName) {
+ assert_equals(actual.length, expected.length,
+ `${desc} - record[${index}].${listName} length`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ assert_not_equals(actual.indexOf(expected[i]), -1,
+ `${desc} - record[${index}].${listName} contains expected Animation`);
+ }
+}
+
+function assert_equals_records(actual, expected, desc) {
+ assert_equals(actual.length, expected.length, `${desc} - number of records`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ assert_record_list(actual[i].addedAnimations,
+ expected[i].added, desc, i, "addedAnimations");
+ assert_record_list(actual[i].changedAnimations,
+ expected[i].changed, desc, i, "changedAnimations");
+ assert_record_list(actual[i].removedAnimations,
+ expected[i].removed, desc, i, "removedAnimations");
+ }
+}
+
+// Create a pseudo element
+function createPseudo(test, element, type) {
+ addStyle(test, { '@keyframes anim': '',
+ ['.pseudo::' + type]: 'animation: anim 10s;' });
+ element.classList.add('pseudo');
+ var anims = document.getAnimations();
+ assert_true(anims.length >= 1);
+ var anim = anims[anims.length - 1];
+ assert_equals(anim.effect.target.parentElement, element);
+ assert_equals(anim.effect.target.type, '::' + type);
+ anim.cancel();
+ return anim.effect.target;
+}
+
+[ { subtree: false },
+ { subtree: true }
+].forEach(aOptions => {
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.duration = 100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after duration is changed");
+
+ anim.effect.timing.duration = 100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = anim.effect.timing.duration * 2;
+ anim.finish();
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.timing.duration = anim.effect.timing.duration * 3;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+
+ anim.effect.timing.duration = "auto";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after duration set \"auto\"");
+
+ anim.effect.timing.duration = "auto";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value \"auto\"");
+ }, "change_duration_and_currenttime");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.endDelay = 10 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after endDelay is changed");
+
+ anim.effect.timing.endDelay = 10 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = 109 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after currentTime during endDelay");
+
+ anim.effect.timing.endDelay = -110 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning negative value");
+ }, "change_enddelay_and_currenttime");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ endDelay: -100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after animation is added");
+ }, "zero_end_time");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.iterations = 2;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after iterations is changed");
+
+ anim.effect.timing.iterations = 2;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.effect.timing.iterations = 0;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.timing.iterations = Infinity;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+ }, "change_iterations");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.delay = 100;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after delay is changed");
+
+ anim.effect.timing.delay = 100;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.effect.timing.delay = -100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.timing.delay = 0;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+ }, "change_delay");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ easing: "steps(2, start)" });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.easing = "steps(2, end)";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after easing is changed");
+
+ anim.effect.timing.easing = "steps(2, end)";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+ }, "change_easing");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100, delay: -100 });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning negative value");
+ }, "negative_delay_in_constructor");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var effect = new KeyframeEffectReadOnly(null,
+ { opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ var anim = new Animation(effect, document.timeline);
+ anim.play();
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after animation is added");
+ }, "create_animation_without_target");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.target = div;
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after setting the same target");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after setting null");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after setting redundant null");
+ }, "set_redundant_animation_target");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation is removed");
+ }, "set_null_animation_effect");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = new Animation();
+ anim.play();
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+ }, "set_effect_on_null_effect_animation");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after replace effects");
+ }, "replace_effect_targeting_on_the_same_element");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.currentTime = 60 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after animation is changed");
+
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 50 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after replacing effects");
+ }, "replace_effect_targeting_on_the_same_element_not_in_effect");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate([ { marginLeft: "0px" },
+ { marginLeft: "-20px" },
+ { marginLeft: "100px" },
+ { marginLeft: "50px" } ],
+ { duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.spacing = "paced(margin-left)";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after animation is changed");
+ }, "set_spacing");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate([ { marginLeft: "0px" },
+ { marginLeft: "-20px" },
+ { marginLeft: "100px" },
+ { marginLeft: "50px" } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: "paced(margin-left)" });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.spacing = "paced(animation-duration)";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after setting a non-animatable paced property");
+ }, "set_spacing_on_a_non-animatable_property");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate([ { marginLeft: "0px" },
+ { marginLeft: "-20px" },
+ { marginLeft: "100px" },
+ { marginLeft: "50px" } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: "paced(margin-left)" });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.spacing = "paced(margin-left)";
+ assert_equals_records(observer.takeRecords(),
+ [], "no record after setting the same spacing");
+ }, "set_the_same_spacing");
+
+ // Test that starting a single animation that is cancelled by calling
+ // cancel() dispatches an added notification and then a removed
+ // notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].cancel();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ // Re-trigger the animation.
+ animations[0].play();
+
+ // Single MutationRecord for the Animation (re-)addition.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+ }, "single_animation_cancelled_api");
+
+ // Test that updating a property on the Animation object dispatches a changed
+ // notification.
+ [
+ { prop: "playbackRate", val: 0.5 },
+ { prop: "startTime", val: 50 * MS_PER_SEC },
+ { prop: "currentTime", val: 50 * MS_PER_SEC },
+ ].forEach(function(aChangeTest) {
+ test(t => {
+ // We use a forwards fill mode so that even if the change we make causes
+ // the animation to become finished, it will still be "relevant" so we
+ // won't mark it as removed.
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Update the property.
+ animations[0][aChangeTest.prop] = aChangeTest.val;
+
+ // Make a redundant change.
+ animations[0][aChangeTest.prop] = animations[0][aChangeTest.prop];
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after animation property change");
+ }, `single_animation_api_change_${aChangeTest.prop}`);
+ });
+
+ // Test that making a redundant change to currentTime while an Animation
+ // is pause-pending still generates a change MutationRecord since setting
+ // the currentTime to any value in this state aborts the pending pause.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].pause();
+
+ // We are now pause-pending. Even if we make a redundant change to the
+ // currentTime, we should still get a change record because setting the
+ // currentTime while pause-pending has the effect of cancelling a pause.
+ animations[0].currentTime = animations[0].currentTime;
+
+ // Two MutationRecords for the Animation changes: one for pausing, one
+ // for aborting the pause.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after pausing then seeking");
+ }, "change_currentTime_while_pause_pending");
+
+ // Test that calling finish() on a forwards-filling Animation dispatches
+ // a changed notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after finish()");
+
+ // Redundant finish.
+ animations[0].finish();
+
+ // Ensure no change records.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after redundant finish()");
+ }, "finish_with_forwards_fill");
+
+ // Test that calling finish() on an Animation that does not fill forwards,
+ // dispatches a removal notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].finish();
+
+ // Single MutationRecord for the Animation removal.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after finishing");
+ }, "finish_without_fill");
+
+ // Test that calling finish() on a forwards-filling Animation dispatches
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animation = div.getAnimations()[0];
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [animation], changed: [], removed: []}],
+ "records after creation");
+ animation.id = "new id";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [animation], removed: []}],
+ "records after id is changed");
+
+ animation.id = "new id";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value with id");
+ }, "change_id");
+
+ // Test that calling reverse() dispatches a changed notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s both" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].reverse();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after calling reverse()");
+ }, "reverse");
+
+ // Test that calling reverse() does *not* dispatch a changed notification
+ // when playbackRate == 0.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s both" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Seek to the middle and set playbackRate to zero.
+ animations[0].currentTime = 50 * MS_PER_SEC;
+ animations[0].playbackRate = 0;
+
+ // Two MutationRecords, one for each change.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after seeking and setting playbackRate");
+
+ animations[0].reverse();
+
+ // We should get no notifications.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after calling reverse()");
+ }, "reverse_with_zero_playbackRate");
+
+ // Test that attempting to start an animation that should already be finished
+ // does not send any notifications.
+ test(t => {
+ // Start an animation that should already be finished.
+ var div = addDiv(t, { style: "animation: anim 1s -2s;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause no Animations to be created.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 0,
+ "getAnimations().length after animation start");
+
+ // And we should get no notifications.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after attempted animation start");
+ }, "already_finished");
+
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s, anotherAnim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: []}],
+ "records after creation");
+
+ div.style.animation = "anotherAnim 100s, anim 100s";
+ animations = div.getAnimations();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: []}],
+ "records after the order is changed");
+
+ div.style.animation = "anotherAnim 100s, anim 100s";
+
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after applying the same order");
+ }, "animtion_order_change");
+
+});
+
+test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, div, true);
+
+ var child = document.createElement("div");
+ div.appendChild(child);
+
+ var anim1 = div.animate({ marginLeft: [ "0px", "50px" ] },
+ 100 * MS_PER_SEC);
+ var anim2 = child.animate({ marginLeft: [ "0px", "100px" ] },
+ 50 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim1], changed: [], removed: [] },
+ { added: [anim2], changed: [], removed: [] }],
+ "records after animation is added");
+
+ // After setting a new effect, we remove the current animation, anim1,
+ // because it is no longer attached to |div|, and then remove the previous
+ // animation, anim2. Finally, add back the anim1 which is in effect on
+ // |child| now. In addition, we sort them by tree order and they are
+ // batched.
+ anim1.effect = anim2.effect;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim1] }, // div
+ { added: [anim1], changed: [], removed: [anim2] }], // child
+ "records after animation effects are changed");
+}, "set_effect_with_previous_animation");
+
+test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, document, true);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+
+ var newTarget = document.createElement("div");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after setting null");
+
+ anim.effect.target = div;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after setting a target");
+
+ anim.effect.target = addDiv(t);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] },
+ { added: [anim], changed: [], removed: [] }],
+ "records after setting a different target");
+}, "set_animation_target");
+
+test(t => {
+ var div = addDiv(t);
+ var pseudoTarget = createPseudo(t, div, 'before');
+ var observer = setupSynchronousObserver(t, div, true);
+
+ var anim = pseudoTarget.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.duration = 100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after duration is changed");
+
+ anim.effect.timing.duration = 100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = anim.effect.timing.duration * 2;
+ anim.finish();
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.timing.duration = anim.effect.timing.duration * 3;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+
+ anim.effect.timing.duration = "auto";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after duration set \"auto\"");
+
+ anim.effect.timing.duration = "auto";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value \"auto\"");
+}, "change_duration_and_currenttime_on_pseudo_elements");
+
+test(t => {
+ var div = addDiv(t);
+ var pseudoTarget = createPseudo(t, div, 'before');
+ var observer = setupSynchronousObserver(t, div, false);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ var pAnim = pseudoTarget.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.finish();
+ pAnim.finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation is finished");
+}, "exclude_animations_targeting_pseudo_elements");
+
+</script>
diff --git a/dom/animation/test/chrome/test_restyles.html b/dom/animation/test/chrome/test_restyles.html
new file mode 100644
index 0000000000..e59967c19d
--- /dev/null
+++ b/dom/animation/test/chrome/test_restyles.html
@@ -0,0 +1,815 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Tests restyles caused by animations</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js"></script>
+<script src="../testcommon.js"></script>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+<style>
+@keyframes opacity {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+@keyframes background-color {
+ from { background-color: red; }
+ to { background-color: blue; }
+}
+@keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+</head>
+<body>
+<script>
+'use strict';
+
+function observeStyling(frameCount, onFrame) {
+ var docShell = window.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .QueryInterface(SpecialPowers.Ci.nsIDocShell);
+
+ docShell.recordProfileTimelineMarkers = true;
+ docShell.popProfileTimelineMarkers();
+
+ return new Promise(function(resolve) {
+ return waitForAnimationFrames(frameCount, onFrame).then(function() {
+ var markers = docShell.popProfileTimelineMarkers();
+ docShell.recordProfileTimelineMarkers = false;
+ var stylingMarkers = markers.filter(function(marker, index) {
+ return marker.name == 'Styles' &&
+ (marker.restyleHint == 'eRestyle_CSSAnimations' ||
+ marker.restyleHint == 'eRestyle_CSSTransitions');
+ });
+ resolve(stylingMarkers);
+ });
+ });
+}
+
+function ensureElementRemoval(aElement) {
+ return new Promise(function(resolve) {
+ aElement.remove();
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+var omtaEnabled = isOMTAEnabled();
+
+var isAndroid = !!navigator.userAgent.includes("Android");
+
+function add_task_if_omta_enabled(test) {
+ if (!omtaEnabled) {
+ info(test.name + " is skipped because OMTA is disabled");
+ return;
+ }
+ add_task(test);
+}
+
+// We need to wait for all paints before running tests to avoid contaminations
+// from styling of this document itself.
+waitForAllPaints(function() {
+ add_task(function* restyling_for_main_thread_animations() {
+ var div = addDiv(null, { style: 'animation: background-color 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(!animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 5,
+ 'CSS animations running on the main-thread should update style ' +
+ 'on the main thread');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_for_compositor_animations() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animations running on the compositor should not update style ' +
+ 'on the main thread');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_for_compositor_transitions() {
+ var div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'CSS transitions running on the compositor should not update style ' +
+ 'on the main thread');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_when_animation_duration_is_changed() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ div.animationDuration = '200s';
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Animations running on the compositor should not update style ' +
+ 'on the main thread');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* only_one_restyling_after_finish_is_called() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ animation.finish();
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 1,
+ 'Animations running on the compositor should only update style ' +
+ 'once after finish() is called');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* no_restyling_mouse_movement_on_finished_transition() {
+ var div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ var animation = div.getAnimations()[0];
+ var initialRect = div.getBoundingClientRect();
+
+ yield animation.finished;
+
+ var mouseX = initialRect.left + initialRect.width / 2;
+ var mouseY = initialRect.top + initialRect.height / 2;
+ var markers = yield observeStyling(5, function() {
+ // We can't use synthesizeMouse here since synthesizeMouse causes
+ // layout flush.
+ synthesizeMouseAtPoint(mouseX++, mouseY++,
+ { type: 'mousemove' }, window);
+ });
+
+ is(markers.length, 0,
+ 'Bug 1219236: Finished transitions should never cause restyles ' +
+ 'when mouse is moved on the animations');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* no_restyling_mouse_movement_on_finished_animation() {
+ var div = addDiv(null, { style: 'animation: opacity 1ms' });
+ var animation = div.getAnimations()[0];
+
+ var initialRect = div.getBoundingClientRect();
+
+ yield animation.finished;
+
+ var mouseX = initialRect.left + initialRect.width / 2;
+ var mouseY = initialRect.top + initialRect.height / 2;
+ var markers = yield observeStyling(5, function() {
+ // We can't use synthesizeMouse here since synthesizeMouse causes
+ // layout flush.
+ synthesizeMouseAtPoint(mouseX++, mouseY++,
+ { type: 'mousemove' }, window);
+ });
+
+ is(markers.length, 0,
+ 'Bug 1219236: Finished animations should never cause restyles ' +
+ 'when mouse is moved on the animations');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_compositor_animations_out_of_view_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ var div = addDiv(null,
+ { style: 'animation: opacity 100s; transform: translateY(-400px);' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(!animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in an out-of-view element ' +
+ 'should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_out_of_view_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; transform: translateY(-400px);' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread in an out-of-view element ' +
+ 'should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_scrolled_out_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android the opacity animation runs on the compositor even if it is
+ scrolled out of view. We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var div = addDiv(null,
+ { style: 'animation: opacity 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor for elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_in_scrolled_out_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android throttled animations are left behind on the main thread in some
+ frames, We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread for elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_in_nested_scrolled_out_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android throttled animations are left behind on the main thread in some
+ frames, We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+
+ var grandParent = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 100px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ grandParent.appendChild(parentElement);
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread which are in nested elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ yield ensureElementRemoval(grandParent);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_visiblily_hidden_element() {
+ var div = addDiv(null,
+ { style: 'animation: opacity 100s; visibility: hidden' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(!animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+
+ todo_is(markers.length, 0,
+ 'Bug 1237454: Animations running on the compositor in ' +
+ 'visibility hidden element should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android throttled animations are left behind on the main thread in some
+ frames, We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ var parentRect = parentElement.getBoundingClientRect();
+ var centerX = parentRect.left + parentRect.width / 2;
+ var centerY = parentRect.top + parentRect.height / 2;
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(1, function() {
+ // We can't use synthesizeWheel here since synthesizeWheel causes
+ // layout flush.
+ synthesizeWheelAtPoint(centerX, centerY,
+ { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+ deltaY: 100 });
+ });
+
+ is(markers.length, 1,
+ 'Animations running on the main-thread which were in scrolled out ' +
+ 'elements should update restyling soon after the element moved in ' +
+ 'view by scrolling');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ var grandParent = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 200px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ grandParent.appendChild(parentElement);
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ var parentRect = grandParent.getBoundingClientRect();
+ var centerX = parentRect.left + parentRect.width / 2;
+ var centerY = parentRect.top + parentRect.height / 2;
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(1, function() {
+ // We can't use synthesizeWheel here since synthesizeWheel causes
+ // layout flush.
+ synthesizeWheelAtPoint(centerX, centerY,
+ { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+ deltaY: 100 });
+ });
+
+ // FIXME: We should reduce a redundant restyle here.
+ ok(markers.length >= 1,
+ 'Animations running on the main-thread which were in nested scrolled ' +
+ 'out elements should update restyle soon after the element moved ' +
+ 'in view by scrolling');
+
+ yield ensureElementRemoval(grandParent);
+ });
+
+ add_task(function* restyling_main_thread_animations_move_out_of_view_by_scrolling() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android throttled animations are left behind on the main thread in some
+ frames, We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 200px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s;' });
+ var pad = addDiv(null,
+ { style: 'height: 400px;' });
+ parentElement.appendChild(div);
+ parentElement.appendChild(pad);
+ var animation = div.getAnimations()[0];
+
+ var parentRect = parentElement.getBoundingClientRect();
+ var centerX = parentRect.left + parentRect.width / 2;
+ var centerY = parentRect.top + parentRect.height / 2;
+
+ yield animation.ready;
+
+ // We can't use synthesizeWheel here since synthesizeWheel causes
+ // layout flush.
+ synthesizeWheelAtPoint(centerX, centerY,
+ { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+ deltaY: 200 });
+
+ var markers = yield observeStyling(5);
+
+ // FIXME: We should reduce a redundant restyle here.
+ ok(markers.length >= 0,
+ 'Animations running on the main-thread which are in scrolled out ' +
+ 'elements should throttle restyling');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* restyling_main_thread_animations_moved_in_view_by_resizing() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(1, function() {
+ parentElement.style.height = '100px';
+ });
+
+ is(markers.length, 1,
+ 'Animations running on the main-thread which was in scrolled out ' +
+ 'elements should update restyling soon after the element moved in ' +
+ 'view by resizing');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_in_visiblily_hidden_element() {
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; visibility: hidden' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ todo_is(markers.length, 0,
+ 'Bug 1237454: Animations running on the main-thread in ' +
+ 'visibility hidden element should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_compositor_animations_after_pause_is_called() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ animation.pause();
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Bug 1232563: Paused animations running on the compositor should ' +
+ 'never cause restyles once after pause() is called');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_after_pause_is_called() {
+ var div = addDiv(null, { style: 'animation: background-color 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+
+ animation.pause();
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Bug 1232563: Paused animations running on the main-thread should ' +
+ 'never cause restyles after pause() is called');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* only_one_restyling_when_current_time_is_set_to_middle_of_duration() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+
+ animation.currentTime = 50 * MS_PER_SEC;
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 1,
+ 'Bug 1235478: Animations running on the compositor should only once ' +
+ 'update style when currentTime is set to middle of duration time');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* change_duration_and_currenttime() {
+ var div = addDiv(null);
+ var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ // Set currentTime to a time longer than duration.
+ animation.currentTime = 500 * MS_PER_SEC;
+
+ // Now the animation immediately get back from compositor.
+ ok(!animation.isRunningOnCompositor);
+
+ // Extend the duration.
+ animation.effect.timing.duration = 800 * MS_PER_SEC;
+ var markers = yield observeStyling(5);
+ is(markers.length, 1,
+ 'Animations running on the compositor should update style ' +
+ 'when timing.duration is made longer than the current time');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* script_animation_on_display_none_element() {
+ var div = addDiv(null);
+ var animation = div.animate({ backgroundColor: [ 'red', 'blue' ] },
+ 100 * MS_PER_SEC);
+
+ yield animation.ready;
+
+ div.style.display = 'none';
+
+ // We need to wait a frame to apply display:none style.
+ yield waitForFrame();
+
+ is(animation.playState, 'running',
+ 'Script animations keep running even when the target element has ' +
+ '"display: none" style');
+
+ ok(!animation.isRunningOnCompositor,
+ 'Script animations on "display:none" element should not run on the ' +
+ 'compositor');
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Script animations on "display: none" element should not update styles');
+
+ div.style.display = '';
+
+ // We need to wait a frame to unapply display:none style.
+ yield waitForFrame();
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 5,
+ 'Script animations restored from "display: none" state should update ' +
+ 'styles');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* compositable_script_animation_on_display_none_element() {
+ var div = addDiv(null);
+ var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ yield animation.ready;
+
+ div.style.display = 'none';
+
+ // We need to wait a frame to apply display:none style.
+ yield waitForFrame();
+
+ is(animation.playState, 'running',
+ 'Opacity script animations keep running even when the target element ' +
+ 'has "display: none" style');
+
+ ok(!animation.isRunningOnCompositor,
+ 'Opacity script animations on "display:none" element should not ' +
+ 'run on the compositor');
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Opacity script animations on "display: none" element should not ' +
+ 'update styles');
+
+ div.style.display = '';
+
+ // We need to wait a frame to unapply display:none style.
+ yield waitForFrame();
+
+ ok(animation.isRunningOnCompositor,
+ 'Opacity script animations restored from "display: none" should be ' +
+ 'run on the compositor');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* restyling_for_empty_keyframes() {
+ var div = addDiv(null);
+ var animation = div.animate({ }, 100 * MS_PER_SEC);
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations with no keyframes should not cause restyles');
+
+ animation.effect.setKeyframes({ backgroundColor: ['red', 'blue'] });
+ markers = yield observeStyling(5);
+
+ is(markers.length, 5,
+ 'Setting valid keyframes should cause regular animation restyles to ' +
+ 'occur');
+
+ animation.effect.setKeyframes({ });
+ markers = yield observeStyling(5);
+
+ is(markers.length, 1,
+ 'Setting an empty set of keyframes should trigger a single restyle ' +
+ 'to remove the previous animated style');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_when_animation_style_when_re_setting_same_animation_property() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+ // Apply the same animation style
+ div.style.animation = 'opacity 100s';
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Applying same animation style ' +
+ 'should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* necessary_update_should_be_invoked() {
+ var div = addDiv(null, { style: 'animation: background-color 100s' });
+ var animation = div.getAnimations()[0];
+ yield animation.ready;
+ yield waitForAnimationFrames(5);
+ // Apply another animation style
+ div.style.animation = 'background-color 110s';
+ var animation = div.getAnimations()[0];
+ var markers = yield observeStyling(5);
+ is(markers.length, 5,
+ 'Applying animation style with different duration ' +
+ 'should cause restyles on every frame.');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(
+ function* changing_cascading_result_for_main_thread_animation() {
+ var div = addDiv(null, { style: 'background-color: blue' });
+ var animation = div.animate({ opacity: [0, 1],
+ backgroundColor: ['green', 'red'] },
+ 100 * MS_PER_SEC);
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor,
+ 'The opacity animation is running on the compositor');
+ // Make the background-color style as !important to cause an update
+ // to the cascade.
+ // Bug 1300982: The background-color animation should be no longer
+ // running on the main thread.
+ div.style.setProperty('background-color', '1', 'important');
+ var markers = yield observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Changing cascading result for the property running on the main ' +
+ 'thread does not cause synchronization layer of opacity animation ' +
+ 'running on the compositor');
+ yield ensureElementRemoval(div);
+ }
+ );
+
+ add_task(function* restyling_for_animation_on_orphaned_element() {
+ var div = addDiv(null);
+ var animation = div.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ yield animation.ready;
+
+ div.remove();
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Animation on orphaned element should not cause restyles');
+
+ document.body.appendChild(div);
+
+ markers = yield observeStyling(1);
+ // We are observing restyles in rAF callback which is processed before
+ // restyling process in each frame, so in the first frame there should be
+ // no observed restyle since we don't process restyle while the element
+ // is not attached to the document.
+ is(markers.length, 0,
+ 'We observe no restyle in the first frame right after re-atatching ' +
+ 'to the document');
+ markers = yield observeStyling(5);
+ is(markers.length, 5,
+ 'Animation on re-attached to the document begins to update style');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(
+ // Tests that if we remove an element from the document whose animation
+ // cascade needs recalculating, that it is correctly updated when it is
+ // re-attached to the document.
+ function* restyling_for_opacity_animation_on_re_attached_element() {
+ var div = addDiv(null, { style: 'opacity: 1 ! important' });
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ yield animation.ready;
+ ok(!animation.isRunningOnCompositor,
+ 'The opacity animation overridden by an !important rule is NOT ' +
+ 'running on the compositor');
+
+ // Drop the !important rule to update the cascade.
+ div.style.setProperty('opacity', '1', '');
+
+ div.remove();
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Opacity animation on orphaned element should not cause restyles');
+
+ document.body.appendChild(div);
+
+ // Need a frame to give the animation a chance to be sent to the
+ // compositor.
+ yield waitForFrame();
+
+ ok(animation.isRunningOnCompositor,
+ 'The opacity animation which is no longer overridden by the ' +
+ '!important rule begins running on the compositor even if the ' +
+ '!important rule had been dropped before the target element was ' +
+ 'removed');
+
+ yield ensureElementRemoval(div);
+ }
+ );
+
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_running_on_compositor.html b/dom/animation/test/chrome/test_running_on_compositor.html
new file mode 100644
index 0000000000..cd6c679b8e
--- /dev/null
+++ b/dom/animation/test/chrome/test_running_on_compositor.html
@@ -0,0 +1,966 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1045994 - Add a chrome-only property to inspect if an animation is
+ running on the compositor or not</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<style>
+@keyframes anim {
+ to { transform: translate(100px) }
+}
+@keyframes transform-starts-with-none {
+ 0% { transform: none }
+ 99% { transform: none }
+ 100% { transform: translate(100px) }
+}
+@keyframes opacity {
+ to { opacity: 0 }
+}
+@keyframes background_and_translate {
+ to { background-color: red; transform: translate(100px); }
+}
+@keyframes background {
+ to { background-color: red; }
+}
+@keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+@keyframes rotate-and-opacity {
+ from { transform: rotate(0deg); opacity: 1;}
+ to { transform: rotate(360deg); opacity: 0;}
+}
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994"
+ target="_blank">Mozilla Bug 1045994</a>
+<div id="log"></div>
+<script>
+'use strict';
+
+/** Test for bug 1045994 - Add a chrome-only property to inspect if an
+ animation is running on the compositor or not **/
+
+var omtaEnabled = isOMTAEnabled();
+
+function assert_animation_is_running_on_compositor(animation, desc) {
+ assert_equals(animation.isRunningOnCompositor, omtaEnabled,
+ desc + ' at ' + animation.currentTime + 'ms');
+}
+
+function assert_animation_is_not_running_on_compositor(animation, desc) {
+ assert_equals(animation.isRunningOnCompositor, false,
+ desc + ' at ' + animation.currentTime + 'ms');
+}
+
+promise_test(function(t) {
+ // FIXME: When we implement Element.animate, use that here instead of CSS
+ // so that we remove any dependency on the CSS mapping.
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' during playback');
+
+ div.style.animationPlayState = 'paused';
+
+ return animation.ready;
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when paused');
+ });
+}, '');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: background 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' for animation of "background"');
+ });
+}, 'isRunningOnCompositor is false for animation of "background"');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: background_and_translate 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when the animation has two properties, where one can run'
+ + ' on the compositor, the other cannot');
+ });
+}, 'isRunningOnCompositor is true if the animation has at least one ' +
+ 'property can run on compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.pause();
+ return animation.ready;
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when animation.pause() is called');
+ });
+}, 'isRunningOnCompositor is false when the animation.pause() is called');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.finish();
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after animation.finish() is called');
+ // Check that we don't set the flag back again on the next tick.
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after animation.finish() is called');
+ });
+}, 'isRunningOnCompositor is false when the animation.finish() is called');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.currentTime = 100 * MS_PER_SEC;
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after manually seeking the animation to the end');
+ // Check that we don't set the flag back again on the next tick.
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after manually seeking the animation to the end');
+ });
+}, 'isRunningOnCompositor is false when manually seeking the animation to ' +
+ 'the end');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.cancel();
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after animation.cancel() is called');
+ // Check that we don't set the flag back again on the next tick.
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after animation.cancel() is called');
+ });
+}, 'isRunningOnCompositor is false when animation.cancel() is called');
+
+// This is to test that we don't simply clobber the flag when ticking
+// animations and then set it again during painting.
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ return new Promise(function(resolve) {
+ window.requestAnimationFrame(function() {
+ t.step(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in requestAnimationFrame callback');
+ });
+
+ resolve();
+ });
+ });
+ });
+}, 'isRunningOnCompositor is true in requestAnimationFrame callback');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ return new Promise(function(resolve) {
+ var observer = new MutationObserver(function(records) {
+ var changedAnimation;
+
+ records.forEach(function(record) {
+ changedAnimation =
+ record.changedAnimations.find(function(changedAnim) {
+ return changedAnim == animation;
+ });
+ });
+
+ t.step(function() {
+ assert_true(!!changedAnimation, 'The animation should be recorded '
+ + 'as one of the changedAnimations');
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in MutationObserver callback');
+ });
+
+ resolve();
+ });
+ observer.observe(div, { animations: true, subtree: false });
+ t.add_cleanup(function() {
+ observer.disconnect();
+ });
+ div.style.animationDuration = "200s";
+ });
+ });
+}, 'isRunningOnCompositor is true in MutationObserver callback');
+
+// This is to test that we don't temporarily clear the flag when forcing
+// an unthrottled sample.
+promise_test(function(t) {
+ return new Promise(function(resolve) {
+ // Needs scrollbars to cause overflow.
+ SpecialPowers.pushPrefEnv({ set: [["ui.showHideScrollbars", 1]] },
+ resolve);
+ }).then(function() {
+ var div = addDiv(t, { style: 'animation: rotate 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ return new Promise(function(resolve) {
+ var timeAtStart = window.performance.now();
+ function handleFrame() {
+ t.step(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in requestAnimationFrame callback');
+ });
+
+ // we have to wait at least 200ms because this animation is
+ // unthrottled on every 200ms.
+ // See http://hg.mozilla.org/mozilla-central/file/cafb1c90f794/layout/style/AnimationCommon.cpp#l863
+ if (window.performance.now() - timeAtStart > 200) {
+ resolve();
+ return;
+ }
+ window.requestAnimationFrame(handleFrame);
+ }
+ window.requestAnimationFrame(handleFrame);
+ });
+ });
+ });
+}, 'isRunningOnCompositor remains true in requestAnimationFrameCallback for ' +
+ 'overflow animation');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
+
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Transition reports that it is running on the compositor'
+ + ' during playback for opacity transition');
+ });
+}, 'isRunningOnCompositor for transitions');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: rotate-and-opacity 100s; ' +
+ 'backface-visibility: hidden; ' +
+ 'transform: none !important;' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'If an animation has a property that can run on the compositor and a '
+ + 'property that cannot (due to Gecko limitations) but where the latter'
+ + 'property is overridden in the CSS cascade, the animation should '
+ + 'still report that it is running on the compositor');
+ });
+}, 'isRunningOnCompositor is true when a property that would otherwise block ' +
+ 'running on the compositor is overridden in the CSS cascade');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.currentTime = 150 * MS_PER_SEC;
+ animation.effect.timing.duration = 100 * MS_PER_SEC;
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when the animation is set a shorter duration than current time');
+ });
+}, 'animation is immediately removed from compositor' +
+ 'when timing.duration is made shorter than the current time');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.currentTime = 500 * MS_PER_SEC;
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when finished');
+
+ animation.effect.timing.duration = 1000 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when restarted');
+ });
+}, 'animation is added to compositor' +
+ ' when timing.duration is made longer than the current time');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.timing.endDelay = 100 * MS_PER_SEC;
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when endDelay is changed');
+
+ animation.currentTime = 110 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when currentTime is during endDelay');
+ });
+}, 'animation is removed from compositor' +
+ ' when current time is made longer than the duration even during endDelay');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.timing.endDelay = -200 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when endTime is negative value');
+ });
+}, 'animation is removed from compositor' +
+ ' when endTime is negative value');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.timing.endDelay = -100 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when endTime is positive and endDelay is negative');
+ animation.currentTime = 110 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when currentTime is after endTime');
+ });
+}, 'animation is NOT running on compositor' +
+ ' when endTime is positive and endDelay is negative');
+
+promise_test(function(t) {
+ var effect = new KeyframeEffect(null,
+ { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+
+ var div = addDiv(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation with null target reports that it is not running ' +
+ 'on the compositor');
+
+ animation.effect.target = div;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor ' +
+ 'after setting a valid target');
+ });
+}, 'animation is added to the compositor when setting a valid target');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.target = null;
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the ' +
+ 'compositor after setting null target');
+ });
+}, 'animation is removed from the compositor when setting null target');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ delay: 100 * MS_PER_SEC,
+ fill: 'backwards' });
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation with fill:backwards in delay phase reports ' +
+ 'that it is running on the compositor');
+
+ animation.currentTime = 100 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation with fill:backwards in delay phase reports ' +
+ 'that it is running on the compositor after delay phase');
+ });
+}, 'animation with fill:backwards in delay phase is running on the ' +
+ ' main-thread while it is in delay phase');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate([{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }], 100 * MS_PER_SEC);
+
+ var another = addDiv(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on a 100% opacity keyframe reports ' +
+ 'that it is running on the compositor from the begining');
+
+ animation.effect.target = another;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on a 100% opacity keyframe keeps ' +
+ 'running on the compositor after changing the target ' +
+ 'element');
+ });
+}, '100% opacity animations with keeps running on the ' +
+ 'compositor after changing the target element');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Color animation reports that it is not running on the ' +
+ 'compositor');
+
+ animation.effect.setKeyframes([{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }]);
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ '100% opacity animation set by using setKeyframes reports ' +
+ 'that it is running on the compositor');
+ });
+}, '100% opacity animation set up by converting an existing animation with ' +
+ 'cannot be run on the compositor, is running on the compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
+ var effect = new KeyframeEffect(div,
+ [{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }],
+ 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Color animation reports that it is not running on the ' +
+ 'compositor');
+
+ animation.effect = effect;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ '100% opacity animation set up by changing effects reports ' +
+ 'that it is running on the compositor');
+ });
+}, '100% opacity animation set up by changing the effects on an existing ' +
+ 'animation which cannot be run on the compositor, is running on the ' +
+ 'compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: "opacity: 1 ! important" });
+
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has 100% opacity style with ' +
+ '!important flag reports that it is not running on the compositor');
+ // Clear important flag from the opacity style on the target element.
+ div.style.setProperty("opacity", "1", "");
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation reports that it is running on the compositor after '
+ + 'clearing the !important flag');
+ });
+}, 'Clearing *important* opacity style on the target element sends the ' +
+ 'animation to the compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority opacity animation on an element ' +
+ 'reports that it is running on the compositor');
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'A lower-priority opacity animation on the same ' +
+ 'element also reports that it is running on the compositor');
+ });
+}, 'Opacity animations on the same element run on the compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
+
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ getComputedStyle(div).opacity;
+
+ var transition = div.getAnimations()[0];
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return Promise.all([transition.ready, animation.ready]).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'An opacity animation on an element reports that' +
+ 'that it is running on the compositor');
+ assert_animation_is_running_on_compositor(transition,
+ 'An opacity transition on the same element reports that ' +
+ 'it is running on the compositor');
+ });
+}, 'Both of transition and script animation on the same element run on the ' +
+ 'compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var importantOpacityElement = addDiv(t, { style: "opacity: 1 ! important" });
+
+ var animation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on an element reports ' +
+ 'that it is running on the compositor');
+
+ animation.effect.target = null;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation is no longer running on the compositor after ' +
+ 'removing from the element');
+ animation.effect.target = importantOpacityElement;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation is NOT running on the compositor even after ' +
+ 'being applied to a different element which has an ' +
+ '!important opacity declaration');
+ });
+}, 'Animation continues not running on the compositor after being ' +
+ 'applied to an element which has an important declaration and ' +
+ 'having previously been temporarily associated with no target element');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var another = addDiv(t);
+
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'An opacity animation on an element reports that ' +
+ 'it is running on the compositor');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'Opacity animation on a different element reports ' +
+ 'that it is running on the compositor');
+
+ lowerAnimation.effect.target = null;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(lowerAnimation,
+ 'Animation is no longer running on the compositor after ' +
+ 'being removed from the element');
+ lowerAnimation.effect.target = another;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'A lower-priority animation begins running ' +
+ 'on the compositor after being applied to an element ' +
+ 'which has a higher-priority animation');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority animation continues to run on the ' +
+ 'compositor even after a lower-priority animation is ' +
+ 'applied to the same element');
+ });
+}, 'Animation begins running on the compositor after being applied ' +
+ 'to an element which has a higher-priority animation and after ' +
+ 'being temporarily associated with no target element');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var another = addDiv(t);
+
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'An opacity animation on an element reports that ' +
+ 'it is running on the compositor');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'Opacity animation on a different element reports ' +
+ 'that it is running on the compositor');
+
+ higherAnimation.effect.target = null;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(higherAnimation,
+ 'Animation is no longer running on the compositor after ' +
+ 'being removed from the element');
+ higherAnimation.effect.target = div;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'Animation continues running on the compositor after ' +
+ 'a higher-priority animation applied to the same element');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority animation begins to running on the ' +
+ 'compositor after being applied to an element which has ' +
+ 'a lower-priority-animation');
+ });
+}, 'Animation begins running on the compositor after being applied ' +
+ 'to an element which has a lower-priority animation once after ' +
+ 'disassociating with an element');
+
+var delayPhaseTests = [
+ {
+ desc: 'script animation of opacity',
+ setupAnimation: function(t) {
+ return addDiv(t).animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'script animation of transform',
+ setupAnimation: function(t) {
+ return addDiv(t).animate(
+ { transform: ['translateX(0px)', 'translateX(100px)'] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'CSS animation of opacity',
+ setupAnimation: function(t) {
+ return addDiv(t, { style: 'animation: opacity 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS animation of transform',
+ setupAnimation: function(t) {
+ return addDiv(t, { style: 'animation: anim 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of opacity',
+ setupAnimation: function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s 100s' });
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ return div.getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of transform',
+ setupAnimation: function(t) {
+ var div = addDiv(t, { style: 'transition: transform 100s 100s' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+ return div.getAnimations()[0];
+ },
+ },
+];
+
+delayPhaseTests.forEach(function(test) {
+ promise_test(function(t) {
+ var animation = test.setupAnimation(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in the delay phase');
+ });
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
+ 'it is in the delay phase');
+});
+
+// The purpose of thie test cases is to check that
+// NS_FRAME_MAY_BE_TRANSFORMED flag on the associated nsIFrame persists
+// after transform style on the frame is removed.
+var delayPhaseWithTransformStyleTests = [
+ {
+ desc: 'script animation of transform with transform style',
+ setupAnimation: function(t) {
+ return addDiv(t, { style: 'transform: translateX(10px)' }).animate(
+ { transform: ['translateX(0px)', 'translateX(100px)'] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'CSS animation of transform with transform style',
+ setupAnimation: function(t) {
+ return addDiv(t, { style: 'animation: anim 100s 100s;' +
+ 'transform: translateX(10px)' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of transform with transform style',
+ setupAnimation: function(t) {
+ var div = addDiv(t, { style: 'transition: transform 100s 100s;' +
+ 'transform: translateX(10px)'});
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+ return div.getAnimations()[0];
+ },
+ },
+];
+
+delayPhaseWithTransformStyleTests.forEach(function(test) {
+ promise_test(function(t) {
+ var animation = test.setupAnimation(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in the delay phase');
+ }).then(function() {
+ // Remove the initial transform style during delay phase.
+ animation.effect.target.style.transform = 'none';
+ return animation.ready;
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it keeps running on the '
+ + 'compositor after removing the initial transform style');
+ });
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true after removing ' +
+ 'the initial transform style during the delay phase');
+});
+
+var startsWithNoneTests = [
+ {
+ desc: 'script animation of transform starts with transform:none segment',
+ setupAnimation: function(t) {
+ return addDiv(t).animate(
+ { transform: ['none', 'none', 'translateX(100px)'] }, 100 * MS_PER_SEC);
+ },
+ },
+ {
+ desc: 'CSS animation of transform starts with transform:none segment',
+ setupAnimation: function(t) {
+ return addDiv(t,
+ { style: 'animation: transform-starts-with-none 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+];
+
+startsWithNoneTests.forEach(function(test) {
+ promise_test(function(t) {
+ var animation = test.setupAnimation(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in transform:none segment');
+ });
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
+ 'it is in transform:none segment');
+});
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'opacity: 1 ! important' });
+
+ var animation = div.animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has opacity:1 important style'
+ + 'reports that it is not running on the compositor');
+ // Clear the opacity style on the target element.
+ div.style.setProperty("opacity", "1", "");
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animations reports that it is running on the compositor after '
+ + 'clearing the opacity style on the element');
+ });
+}, 'Clearing *important* opacity style on the target element sends the ' +
+ 'animation to the compositor even if the animation is in the delay phase');
+
+promise_test(function(t) {
+ var opaqueDiv = addDiv(t, { style: 'opacity: 1 ! important' });
+ var anotherDiv = addDiv(t);
+
+ var animation = opaqueDiv.animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has opacity:1 important style'
+ + 'reports that it is not running on the compositor');
+ // Changing target element to another element which has no opacity style.
+ animation.effect.target = anotherDiv;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animations reports that it is running on the compositor after '
+ + 'changing the target element to another elemenent having no '
+ + 'opacity style');
+ });
+}, 'Changing target element of opacity animation sends the animation to the ' +
+ 'the compositor even if the animation is in the delay phase');
+
+promise_test(function(t) {
+ var animation =
+ addDivAndAnimate(t,
+ {},
+ { width: ['100px', '200px'] },
+ { duration: 100 * MS_PER_SEC, delay: 100 * MS_PER_SEC });
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Width animation reports that it is not running on the compositor '
+ + 'in the delay phase');
+ // Changing to property runnable on the compositor.
+ animation.effect.setKeyframes({ opacity: [0, 1] });
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation reports that it is running on the compositor '
+ + 'after changing the property from width property in the delay phase');
+ });
+}, 'Dynamic change to a property runnable on the compositor ' +
+ 'in the delay phase');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s; ' +
+ 'opacity: 0 !important' });
+ getComputedStyle(div).opacity;
+
+ div.style.setProperty('opacity', '1', 'important');
+ getComputedStyle(div).opacity;
+
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Transition reports that it is running on the compositor even if the ' +
+ 'property is overridden by an !important rule');
+ });
+}, 'Transitions override important rules');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s; ' +
+ 'opacity: 0 !important' });
+ getComputedStyle(div).opacity;
+
+ div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ div.style.setProperty('opacity', '1', 'important');
+ getComputedStyle(div).opacity;
+
+ var [transition, animation] = div.getAnimations();
+
+ return Promise.all([transition.ready, animation.ready]).then(function() {
+ assert_animation_is_not_running_on_compositor(transition,
+ 'Transition suppressed by an animation which is overridden by an ' +
+ '!important rule reports that it is NOT running on the compositor');
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation overridden by an !important rule reports that it is ' +
+ 'NOT running on the compositor');
+ });
+}, 'Neither transition nor animation does run on the compositor if the ' +
+ 'property is overridden by an !important rule');
+
+</script>
+</body>
diff --git a/dom/animation/test/crashtests/1216842-1.html b/dom/animation/test/crashtests/1216842-1.html
new file mode 100644
index 0000000000..8df6808aed
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-1.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1216842: effect-level easing function produces negative values (compositor thread)</title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffectReadOnly(
+ target,
+ { opacity: [0, 1] },
+ {
+ fill: "forwards",
+ /* The function produces negative values in (0, 0.766312060) */
+ easing: "cubic-bezier(0,-0.5,1,-0.5)",
+ duration: 100,
+ iterations: 0.75 /* To finish in the extraporation range */
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-2.html b/dom/animation/test/crashtests/1216842-2.html
new file mode 100644
index 0000000000..ec2a2e167e
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-2.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1216842: effect-level easing function produces values greater than 1 (compositor thread)</title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffectReadOnly(
+ target,
+ { opacity: [0, 1] },
+ {
+ fill: "forwards",
+ /* The function produces values greater than 1 in (0.23368794, 1) */
+ easing: "cubic-bezier(0,1.5,1,1.5)",
+ duration: 100,
+ iterations: 0.25 /* To finish in the extraporation range */
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-3.html b/dom/animation/test/crashtests/1216842-3.html
new file mode 100644
index 0000000000..2e5a762aa0
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-3.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1216842: effect-level easing function produces values greater than 1 (main-thread)</title>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffectReadOnly(
+ target,
+ { color: ["red", "blue"] },
+ {
+ fill: "forwards",
+ /* The function produces values greater than 1 in (0.23368794, 1) */
+ easing: "cubic-bezier(0,1.5,1,1.5)",
+ duration: 100
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.pause();
+ animation.currentTime = 250;
+ document.documentElement.className = "";
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-4.html b/dom/animation/test/crashtests/1216842-4.html
new file mode 100644
index 0000000000..2951adc959
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-4.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1216842: effect-level easing function produces negative values (main-thread)</title>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffectReadOnly(
+ target,
+ { color: ["red", "blue"] },
+ {
+ fill: "forwards",
+ /* The function produces negative values in (0, 0.766312060) */
+ easing: "cubic-bezier(0,-0.5,1,-0.5)",
+ duration: 100
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.pause();
+ animation.currentTime = 250;
+ document.documentElement.className = "";
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-5.html b/dom/animation/test/crashtests/1216842-5.html
new file mode 100644
index 0000000000..65c64fa481
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-5.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>
+ Bug 1216842: effect-level easing function produces negative values passed
+ to step-end function (compositor thread)
+ </title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffectReadOnly(
+ target,
+ { opacity: [0, 1], easing: "step-end" },
+ {
+ fill: "forwards",
+ /* The function produces negative values in (0, 0.766312060) */
+ easing: "cubic-bezier(0,-0.5,1,-0.5)",
+ duration: 100,
+ iterations: 0.75 /* To finish in the extraporation range */
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-6.html b/dom/animation/test/crashtests/1216842-6.html
new file mode 100644
index 0000000000..a588c68f1e
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-6.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>
+ Bug 1216842: effect-level easing function produces values greater than 1
+ which are passed to step-end function (compositor thread)
+ </title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffectReadOnly(
+ target,
+ { opacity: [0, 1], easing: "step-end" },
+ {
+ fill: "forwards",
+ /* The function produces values greater than 1 in (0.23368794, 1) */
+ easing: "cubic-bezier(0,1.5,1,1.5)",
+ duration: 100,
+ iterations: 0.25 /* To finish in the extraporation range */
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1239889-1.html b/dom/animation/test/crashtests/1239889-1.html
new file mode 100644
index 0000000000..476f3322b4
--- /dev/null
+++ b/dom/animation/test/crashtests/1239889-1.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Bug 1239889</title>
+ </head>
+ <body>
+ </body>
+ <script>
+ var div = document.createElement('div');
+ var effect = new KeyframeEffectReadOnly(div, { opacity: [0, 1] });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1244595-1.html b/dom/animation/test/crashtests/1244595-1.html
new file mode 100644
index 0000000000..13b2e2d7e7
--- /dev/null
+++ b/dom/animation/test/crashtests/1244595-1.html
@@ -0,0 +1,3 @@
+<div id=target><script>
+ var player = target.animate([{background: 'green'}, {background: 'green'}]);
+</script>
diff --git a/dom/animation/test/crashtests/1272475-1.html b/dom/animation/test/crashtests/1272475-1.html
new file mode 100644
index 0000000000..e0b0495388
--- /dev/null
+++ b/dom/animation/test/crashtests/1272475-1.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Bug 1272475 - scale function with an extreme large value</title>
+ <script>
+ function test() {
+ var div = document.createElement("div");
+ div.setAttribute("style", "width: 1px; height: 1px; " +
+ "background: red;");
+ document.body.appendChild(div);
+ div.animate([ { "transform": "scale(8)" },
+ { "transform": "scale(9.5e+307)" },
+ { "transform": "scale(32)" } ],
+ { "duration": 1000, "fill": "both" });
+ }
+ </script>
+ </head>
+ <body onload="test()">
+ </body>
+</html>
diff --git a/dom/animation/test/crashtests/1272475-2.html b/dom/animation/test/crashtests/1272475-2.html
new file mode 100644
index 0000000000..da0e8605bd
--- /dev/null
+++ b/dom/animation/test/crashtests/1272475-2.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Bug 1272475 - rotate function with an extreme large value</title>
+ <script>
+ function test() {
+ var div = document.createElement("div");
+ div.setAttribute("style", "width: 100px; height: 100px; " +
+ "background: red;");
+ document.body.appendChild(div);
+ div.animate([ { "transform": "rotate(8rad)" },
+ { "transform": "rotate(9.5e+307rad)" },
+ { "transform": "rotate(32rad)" } ],
+ { "duration": 1000, "fill": "both" });
+ }
+ </script>
+ </head>
+ <body onload="test()">
+ </body>
+</html>
diff --git a/dom/animation/test/crashtests/1277272-1-inner.html b/dom/animation/test/crashtests/1277272-1-inner.html
new file mode 100644
index 0000000000..2ba52174dd
--- /dev/null
+++ b/dom/animation/test/crashtests/1277272-1-inner.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<head>
+<script>
+function start() {
+ var animation = document.body.animate([{marks: 'crop'},{marks: 'crop'}], 12);
+ document.write('<html><body></body></html>');
+
+ setTimeout(function() { animation.play(); }, 4);
+ setTimeout(function() {
+ animation.timeline = undefined;
+ SpecialPowers.Cu.forceGC();
+ window.top.continueTest();
+ }, 5);
+}
+</script>
+</head>
+<body onload="start()"></body>
+</html>
+
diff --git a/dom/animation/test/crashtests/1277272-1.html b/dom/animation/test/crashtests/1277272-1.html
new file mode 100644
index 0000000000..f398bcf6d9
--- /dev/null
+++ b/dom/animation/test/crashtests/1277272-1.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html class="reftest-wait">
+<head>
+<script>
+var count = 0;
+
+function start() {
+ if (++count > 10) {
+ document.documentElement.className = "";
+ return;
+ }
+
+ var frame = document.getElementById("frame");
+ frame.src = "./1277272-1-inner.html";
+}
+
+function continueTest() {
+ setTimeout(start.bind(window), 1);
+}
+
+</script>
+</head>
+<body onload="start()"></body>
+<iframe id="frame">
+</html>
+
diff --git a/dom/animation/test/crashtests/1278485-1.html b/dom/animation/test/crashtests/1278485-1.html
new file mode 100644
index 0000000000..e7347f5d84
--- /dev/null
+++ b/dom/animation/test/crashtests/1278485-1.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+function boom()
+{
+ document.body.animate([],
+ { duration: 6,
+ easing: "cubic-bezier(0, -1e+39, 0, 0)" });
+ document.body.animate([],
+ { duration: 6,
+ easing: "cubic-bezier(0, 1e+39, 0, 0)" });
+ document.body.animate([],
+ { duration: 6,
+ easing: "cubic-bezier(0, 0, 0, -1e+39)" });
+ document.body.animate([],
+ { duration: 6,
+ easing: "cubic-bezier(0, 0, 0, 1e+39)" });
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/animation/test/crashtests/1290535-1.html b/dom/animation/test/crashtests/1290535-1.html
new file mode 100644
index 0000000000..20b44d8bfc
--- /dev/null
+++ b/dom/animation/test/crashtests/1290535-1.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Bug 1290535 - Sort paced subproperties of a shorthand property</title>
+<meta charset="UTF-8">
+<script>
+
+function test()
+{
+ var div = document.createElement('div');
+ document.documentElement.appendChild(div);
+ div.animate([ { borderRadius: "0", borderTopRightRadius: "0" },
+ { borderRadius: "50%" } ],
+ { spacing:"paced(border-radius)" });
+}
+
+</script>
+</head>
+<body onload="test();"></body>
+</html>
diff --git a/dom/animation/test/crashtests/crashtests.list b/dom/animation/test/crashtests/crashtests.list
new file mode 100644
index 0000000000..f61d7f876e
--- /dev/null
+++ b/dom/animation/test/crashtests/crashtests.list
@@ -0,0 +1,13 @@
+pref(dom.animations-api.core.enabled,true) load 1239889-1.html
+pref(dom.animations-api.core.enabled,true) load 1244595-1.html
+pref(dom.animations-api.core.enabled,true) load 1216842-1.html
+pref(dom.animations-api.core.enabled,true) load 1216842-2.html
+pref(dom.animations-api.core.enabled,true) load 1216842-3.html
+pref(dom.animations-api.core.enabled,true) load 1216842-4.html
+pref(dom.animations-api.core.enabled,true) load 1216842-5.html
+pref(dom.animations-api.core.enabled,true) load 1216842-6.html
+pref(dom.animations-api.core.enabled,true) load 1272475-1.html
+pref(dom.animations-api.core.enabled,true) load 1272475-2.html
+pref(dom.animations-api.core.enabled,true) load 1278485-1.html
+pref(dom.animations-api.core.enabled,true) load 1277272-1.html
+pref(dom.animations-api.core.enabled,true) load 1290535-1.html
diff --git a/dom/animation/test/css-animations/file_animation-cancel.html b/dom/animation/test/css-animations/file_animation-cancel.html
new file mode 100644
index 0000000000..85499addfd
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-cancel.html
@@ -0,0 +1,154 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes translateAnim {
+ to { transform: translate(100px) }
+}
+@keyframes marginLeftAnim {
+ to { margin-left: 100px }
+}
+@keyframes marginLeftAnim100To200 {
+ from { margin-left: 100px }
+ to { margin-left: 200px }
+}
+</style>
+<body>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: translateAnim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_not_equals(getComputedStyle(div).transform, 'none',
+ 'transform style is animated before cancelling');
+ animation.cancel();
+ assert_equals(getComputedStyle(div).transform, 'none',
+ 'transform style is no longer animated after cancelling');
+ });
+}, 'Animated style is cleared after cancelling a running CSS animation');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: translateAnim 100s forwards' });
+ var animation = div.getAnimations()[0];
+ animation.finish();
+
+ return animation.ready.then(function() {
+ assert_not_equals(getComputedStyle(div).transform, 'none',
+ 'transform style is filling before cancelling');
+ animation.cancel();
+ assert_equals(getComputedStyle(div).transform, 'none',
+ 'fill style is cleared after cancelling');
+ });
+}, 'Animated style is cleared after cancelling a filling CSS animation');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: translateAnim 100s' });
+ var animation = div.getAnimations()[0];
+ div.addEventListener('animationend', t.step_func(function() {
+ assert_unreached('Got unexpected end event on cancelled animation');
+ }));
+
+ return animation.ready.then(function() {
+ // Seek to just before the end then cancel
+ animation.currentTime = 99.9 * 1000;
+ animation.cancel();
+
+ // Then wait a couple of frames and check that no event was dispatched
+ return waitForAnimationFrames(2);
+ });
+}, 'Cancelled CSS animations do not dispatch events');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: marginLeftAnim 100s linear' });
+ var animation = div.getAnimations()[0];
+ animation.cancel();
+
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is not animated after cancelling');
+
+ animation.currentTime = 50 * 1000;
+ assert_equals(getComputedStyle(div).marginLeft, '50px',
+ 'margin-left style is updated when cancelled animation is'
+ + ' seeked');
+}, 'After cancelling an animation, it can still be seeked');
+
+promise_test(function(t) {
+ var div =
+ addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is not animated after cancelling');
+ animation.play();
+ assert_equals(getComputedStyle(div).marginLeft, '100px',
+ 'margin-left style is animated after re-starting animation');
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.playState, 'running',
+ 'Animation succeeds in running after being re-started');
+ });
+}, 'After cancelling an animation, it can still be re-used');
+
+test(function(t) {
+ var div =
+ addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' });
+ var animation = div.getAnimations()[0];
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is not animated after cancelling');
+
+ // Trigger a change to some animation properties and check that this
+ // doesn't cause the animation to become live again
+ div.style.animationDuration = '200s';
+ flushComputedStyle(div);
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is still not animated after updating'
+ + ' animation-duration');
+ assert_equals(animation.playState, 'idle',
+ 'Animation is still idle after updating animation-duration');
+}, 'After cancelling an animation, updating animation properties doesn\'t make'
+ + ' it live again');
+
+test(function(t) {
+ var div =
+ addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' });
+ var animation = div.getAnimations()[0];
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is not animated after cancelling');
+
+ // Make some changes to animation-play-state and check that the
+ // animation doesn't become live again. This is because it should be
+ // possible to cancel an animation from script such that all future
+ // changes to style are ignored.
+
+ // Redundant change
+ div.style.animationPlayState = 'running';
+ assert_equals(animation.playState, 'idle',
+ 'Animation is still idle after a redundant change to'
+ + ' animation-play-state');
+
+ // Pause
+ div.style.animationPlayState = 'paused';
+ assert_equals(animation.playState, 'idle',
+ 'Animation is still idle after setting'
+ + ' animation-play-state: paused');
+
+ // Play
+ div.style.animationPlayState = 'running';
+ assert_equals(animation.playState, 'idle',
+ 'Animation is still idle after re-setting'
+ + ' animation-play-state: running');
+
+}, 'After cancelling an animation, updating animation-play-state doesn\'t'
+ + ' make it live again');
+
+done();
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/css-animations/file_animation-computed-timing.html b/dom/animation/test/css-animations/file_animation-computed-timing.html
new file mode 100644
index 0000000000..53597a473a
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-computed-timing.html
@@ -0,0 +1,566 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes moveAnimation {
+ from { margin-left: 100px }
+ to { margin-left: 200px }
+}
+</style>
+<body>
+<script>
+
+'use strict';
+
+// --------------------
+// delay
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().delay, 0,
+ 'Initial value of delay');
+}, 'delay of a new animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s -10s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().delay, -10 * MS_PER_SEC,
+ 'Initial value of delay');
+}, 'Negative delay of a new animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 10s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().delay, 10 * MS_PER_SEC,
+ 'Initial value of delay');
+}, 'Positive delay of a new animation');
+
+
+// --------------------
+// endDelay
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().endDelay, 0,
+ 'Initial value of endDelay');
+}, 'endDelay of a new animation');
+
+
+// --------------------
+// fill
+// --------------------
+test(function(t) {
+ var getEffectWithFill = function(fill) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s ' + fill});
+ return div.getAnimations()[0].effect;
+ };
+
+ var effect = getEffectWithFill('');
+ assert_equals(effect.getComputedTiming().fill, 'none',
+ 'Initial value of fill');
+ effect = getEffectWithFill('forwards');
+ assert_equals(effect.getComputedTiming().fill, 'forwards',
+ 'Fill forwards');
+ effect = getEffectWithFill('backwards');
+ assert_equals(effect.getComputedTiming().fill, 'backwards',
+ 'Fill backwards');
+ effect = getEffectWithFill('both');
+ assert_equals(effect.getComputedTiming().fill, 'both',
+ 'Fill forwards and backwards');
+}, 'fill of a new animation');
+
+
+// --------------------
+// iterationStart
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().iterationStart, 0,
+ 'Initial value of iterationStart');
+}, 'iterationStart of a new animation');
+
+
+// --------------------
+// iterations
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().iterations, 1,
+ 'Initial value of iterations');
+}, 'iterations of a new animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 2016.5'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().iterations, 2016.5,
+ 'Initial value of iterations');
+}, 'iterations of a finitely repeating animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s infinite'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().iterations, Infinity,
+ 'Initial value of iterations');
+}, 'iterations of an infinitely repeating animation');
+
+
+// --------------------
+// duration
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s -10s infinite'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().duration, 100 * MS_PER_SEC,
+ 'Initial value of duration');
+}, 'duration of a new animation');
+
+
+// --------------------
+// direction
+// --------------------
+test(function(t) {
+ var getEffectWithDir = function(dir) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s ' + dir});
+ return div.getAnimations()[0].effect;
+ };
+
+ var effect = getEffectWithDir('');
+ assert_equals(effect.getComputedTiming().direction, 'normal',
+ 'Initial value of normal direction');
+ effect = getEffectWithDir('reverse');
+ assert_equals(effect.getComputedTiming().direction, 'reverse',
+ 'Initial value of reverse direction');
+ effect = getEffectWithDir('alternate');
+ assert_equals(effect.getComputedTiming().direction, 'alternate',
+ 'Initial value of alternate direction');
+ effect = getEffectWithDir('alternate-reverse');
+ assert_equals(effect.getComputedTiming().direction, 'alternate-reverse',
+ 'Initial value of alternate-reverse direction');
+}, 'direction of a new animation');
+
+
+// --------------------
+// easing
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().easing, 'linear',
+ 'Initial value of easing');
+}, 'easing of a new animation');
+
+
+// ------------------------------
+// endTime
+// = max(start delay + active duration + end delay, 0)
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().endTime, 100 * MS_PER_SEC,
+ 'Initial value of endTime');
+}, 'endTime of an new animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s -5s'});
+ var effect = div.getAnimations()[0].effect;
+ var answer = (100 - 5) * MS_PER_SEC;
+ assert_equals(effect.getComputedTiming().endTime, answer,
+ 'Initial value of endTime');
+}, 'endTime of an animation with a negative delay');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 10s -100s infinite'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().endTime, Infinity,
+ 'Initial value of endTime');
+}, 'endTime of an infinitely repeating animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 100s infinite'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().endTime, 100 * MS_PER_SEC,
+ 'Initial value of endTime');
+}, 'endTime of an infinitely repeating zero-duration animation');
+
+test(function(t) {
+ // Fill forwards so div.getAnimations()[0] won't return an
+ // undefined value.
+ var div = addDiv(t, {style: 'animation: moveAnimation 10s -100s forwards'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().endTime, 0,
+ 'Initial value of endTime');
+}, 'endTime of an animation that finishes before its startTime');
+
+
+// --------------------
+// activeDuration
+// = iteration duration * iteration count
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 5'});
+ var effect = div.getAnimations()[0].effect;
+ var answer = 100 * MS_PER_SEC * 5;
+ assert_equals(effect.getComputedTiming().activeDuration, answer,
+ 'Initial value of activeDuration');
+}, 'activeDuration of a new animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s infinite'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().activeDuration, Infinity,
+ 'Initial value of activeDuration');
+}, 'activeDuration of an infinitely repeating animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 1s infinite'});
+ var effect = div.getAnimations()[0].effect;
+ // If either the iteration duration or iteration count are zero,
+ // the active duration is zero.
+ assert_equals(effect.getComputedTiming().activeDuration, 0,
+ 'Initial value of activeDuration');
+}, 'activeDuration of an infinitely repeating zero-duration animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 1s 0'});
+ var effect = div.getAnimations()[0].effect;
+ // If either the iteration duration or iteration count are zero,
+ // the active duration is zero.
+ assert_equals(effect.getComputedTiming().activeDuration, 0,
+ 'Initial value of activeDuration');
+}, 'activeDuration of an animation with zero iterations');
+
+
+// --------------------
+// localTime
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().localTime, 0,
+ 'Initial value of localTime');
+}, 'localTime of a new animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var anim = div.getAnimations()[0];
+ anim.currentTime = 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
+ 'current localTime after setting currentTime');
+}, 'localTime of an animation is always equal to currentTime');
+
+promise_test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+
+ var anim = div.getAnimations()[0];
+ anim.playbackRate = 2; // 2 times faster
+
+ return anim.ready.then(function() {
+ assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
+ 'localTime is equal to currentTime');
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
+ 'localTime is equal to currentTime');
+ });
+}, 'localTime reflects playbackRate immediately');
+
+test(function(t) {
+ var div = addDiv(t);
+ var effect = new KeyframeEffectReadOnly(div, {left: ["0px", "100px"]});
+
+ assert_equals(effect.getComputedTiming().localTime, null,
+ 'localTime for orphaned effect');
+}, 'localTime of an AnimationEffect without an Animation');
+
+
+// --------------------
+// progress
+// Note: Default timing function is linear.
+// --------------------
+test(function(t) {
+ [{fill: '', progress: [ null, null ]},
+ {fill: 'none', progress: [ null, null ]},
+ {fill: 'forwards', progress: [ null, 1.0 ]},
+ {fill: 'backwards', progress: [ 0.0, null ]},
+ {fill: 'both', progress: [ 0.0, 1.0 ]}]
+ .forEach(function(test) {
+ var div =
+ addDiv(t, {style: 'animation: moveAnimation 100s 10s ' + test.fill});
+ var anim = div.getAnimations()[0];
+ assert_true(anim.effect.getComputedTiming().progress === test.progress[0],
+ 'initial progress with "' + test.fill + '" fill');
+ anim.finish();
+ assert_true(anim.effect.getComputedTiming().progress === test.progress[1],
+ 'finished progress with "' + test.fill + '" fill');
+ });
+}, 'progress of an animation with different fill modes');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 10s 10 both'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'Initial value of progress');
+ anim.currentTime += 2.5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+ anim.currentTime += 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+ anim.currentTime += 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+ anim.finish()
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'Value of progress');
+}, 'progress of an integral repeating animation with normal direction');
+
+test(function(t) {
+ var div = addDiv(t);
+ // Note: FillMode here is "both" because
+ // 1. Since this a zero-duration animation, it will already have finished
+ // so it won't be returned by getAnimations() unless it fills forwards.
+ // 2. Fill backwards, so the progress before phase wouldn't be
+ // unresolved (null value).
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s infinite both'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'Initial value of progress in after phase');
+
+ // Seek backwards
+ anim.currentTime -= 1 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'Value of progress before phase');
+}, 'progress of an infinitely repeating zero-duration animation');
+
+test(function(t) {
+ // Default iterations = 1
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s both'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'Initial value of progress in after phase');
+
+ // Seek backwards
+ anim.currentTime -= 1 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'Value of progress before phase');
+}, 'progress of a finitely repeating zero-duration animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 5s 10.25 both'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'Initial value of progress (before phase)');
+
+ // Using iteration duration of 1 now.
+ // currentIteration now is floor(10.25) = 10, so progress should be 25%.
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress in after phase');
+}, 'progress of a non-integral repeating zero-duration animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 5s 10.25 both reverse'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'Initial value of progress (before phase)');
+
+ // Seek forwards
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress in after phase');
+}, 'Progress of a non-integral repeating zero-duration animation ' +
+ 'with reversing direction');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 10s 10.25 both alternate'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'Initial value of progress');
+ anim.currentTime += 2.5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+ anim.currentTime += 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+ anim.currentTime += 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+ anim.finish()
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+}, 'progress of a non-integral repeating animation ' +
+ 'with alternate direction');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 10s 10.25 both alternate-reverse'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'Initial value of progress');
+ anim.currentTime += 2.5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+ anim.currentTime += 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+ anim.currentTime += 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+ anim.finish()
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+}, 'progress of a non-integral repeating animation ' +
+ 'with alternate-reversing direction');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 10.25 both alternate'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Initial value of progress');
+ anim.currentTime += 2.5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+ anim.currentTime -= 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'Value of progress');
+ anim.finish()
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+}, 'progress of a non-integral repeating zero-duration animation ' +
+ 'with alternate direction');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 10.25 both alternate-reverse'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Initial value of progress');
+ anim.currentTime += 2.5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+ anim.currentTime -= 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'Value of progress');
+ anim.finish()
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+}, 'progress of a non-integral repeating zero-duration animation ' +
+ 'with alternate-reverse direction');
+
+
+// --------------------
+// currentIteration
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 2s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().currentIteration, null,
+ 'Initial value of currentIteration before phase');
+}, 'currentIteration of a new animation with no backwards fill is unresolved ' +
+ 'in before phase');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var anim = div.getAnimations()[0];
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Initial value of currentIteration');
+}, 'currentIteration of a new animation is zero');
+
+test(function(t) {
+ // Note: FillMode here is "both" because
+ // 1. Since this a zero-duration animation, it will already have finished
+ // so it won't be returned by getAnimations() unless it fills forwards.
+ // 2. Fill backwards, so the currentIteration (before phase) wouldn't be
+ // unresolved (null value).
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s infinite both'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().currentIteration, Infinity,
+ 'Initial value of currentIteration in after phase');
+
+ // Seek backwards
+ anim.currentTime -= 2 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Value of currentIteration count during before phase');
+}, 'currentIteration of an infinitely repeating zero-duration animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 10.5 both'});
+ var anim = div.getAnimations()[0];
+
+ // Note: currentIteration = ceil(iteration start + iteration count) - 1
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 10,
+ 'Initial value of currentIteration');
+
+ // Seek backwards
+ anim.currentTime -= 2 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Value of currentIteration count during before phase');
+}, 'currentIteration of a finitely repeating zero-duration animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 5.5 forwards'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Initial value of currentIteration');
+ // The 3rd iteration
+ // Note: currentIteration = floor(scaled active time / iteration duration)
+ anim.currentTime = 250 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 2,
+ 'Value of currentIteration during the 3rd iteration');
+ // Finish
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 5,
+ 'Value of currentIteration in after phase');
+}, 'currentIteration of an animation with a non-integral iteration count');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 2 forwards'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Initial value of currentIteration');
+ // Finish
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 1,
+ 'Value of currentIteration in after phase');
+}, 'currentIteration of an animation with an integral iteration count');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s forwards'});
+ var anim = div.getAnimations()[0];
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Initial value of currentIteration');
+ // Finish
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Value of currentIteration in after phase');
+}, 'currentIteration of an animation with a default iteration count');
+
+test(function(t) {
+ var div = addDiv(t);
+ var effect = new KeyframeEffectReadOnly(div, {left: ["0px", "100px"]});
+
+ assert_equals(effect.getComputedTiming().currentIteration, null,
+ 'currentIteration for orphaned effect');
+}, 'currentIteration of an AnimationEffect without an Animation');
+
+// TODO: If iteration duration is Infinity, currentIteration is 0.
+// However, we cannot set iteration duration to Infinity in CSS Animation now.
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-currenttime.html b/dom/animation/test/css-animations/file_animation-currenttime.html
new file mode 100644
index 0000000000..ec6fb3f1a8
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-currenttime.html
@@ -0,0 +1,345 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for the effect of setting a CSS animation's
+ Animation.currentTime</title>
+ <style>
+
+.animated-div {
+ margin-left: 10px;
+ /* Make it easier to calculate expected values: */
+ animation-timing-function: linear ! important;
+}
+
+@keyframes anim {
+ from { margin-left: 100px; }
+ to { margin-left: 200px; }
+}
+
+ </style>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+
+'use strict';
+
+// TODO: We should separate this test(Testing for CSS Animation events /
+// Testing for currentTime of Web Animation).
+// e.g:
+// CSS Animation events test :
+// - check the firing an event using Animation.currentTime
+// The current Time of Web Animation test :
+// - check an current time value on several situation(init / processing..)
+// - Based on W3C Spec, check the behavior of setting current time.
+
+// TODO: Once the computedTiming property is implemented, add checks to the
+// checker helpers to ensure that computedTiming's properties are updated as
+// expected.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
+
+const CSS_ANIM_EVENTS =
+ ['animationstart', 'animationiteration', 'animationend'];
+
+test(function(t)
+{
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+ var animation = div.getAnimations()[0];
+
+ // Animations shouldn't start until the next paint tick, so:
+ assert_equals(animation.currentTime, 0,
+ 'Animation.currentTime should be zero when an animation ' +
+ 'is initially created');
+
+ // Make sure the animation is running before we set the current time.
+ animation.startTime = animation.timeline.currentTime;
+
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_times_equal(animation.currentTime, 50 * MS_PER_SEC,
+ 'Check setting of currentTime actually works');
+}, 'Sanity test to check round-tripping assigning to new animation\'s ' +
+ 'currentTime');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ // the 0.0001 here is for rounding error
+ assert_less_than_equal(animation.currentTime,
+ animation.timeline.currentTime - animation.startTime + 0.0001,
+ 'Animation.currentTime should be less than the local time ' +
+ 'equivalent of the timeline\'s currentTime on the first paint tick ' +
+ 'after animation creation');
+
+ animation.currentTime = 100 * MS_PER_SEC;
+ return eventWatcher.wait_for('animationstart');
+ }).then(function() {
+ animation.currentTime = 200 * MS_PER_SEC;
+ return eventWatcher.wait_for('animationend');
+ });
+}, 'Skipping forward through animation');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+ animation.currentTime = 200 * MS_PER_SEC;
+ var previousTimelineTime = animation.timeline.currentTime;
+
+ return eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ assert_true(document.timeline.currentTime - previousTimelineTime <
+ 100 * MS_PER_SEC,
+ 'Sanity check that seeking worked rather than the events ' +
+ 'firing after normal playback through the very long ' +
+ 'animation duration');
+
+ animation.currentTime = 150 * MS_PER_SEC;
+ return eventWatcher.wait_for('animationstart');
+ }).then(function() {
+ animation.currentTime = 0;
+ return eventWatcher.wait_for('animationend');
+ });
+}, 'Skipping backwards through animation');
+
+// Next we have multiple tests to check that redundant currentTime changes do
+// NOT dispatch events. It's impossible to distinguish between events not being
+// dispatched and events just taking an incredibly long time to dispatch
+// without waiting an infinitely long time. Obviously we don't want to do that
+// (block this test from finishing forever), so instead we just listen for
+// events until two animation frames (i.e. requestAnimationFrame callbacks)
+// have happened, then assume that no events will ever be dispatched for the
+// redundant changes if no events were detected in that time.
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ animation.currentTime = 150 * MS_PER_SEC;
+ animation.currentTime = 50 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+}, 'Redundant change, before -> active, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ animation.currentTime = 250 * MS_PER_SEC;
+ animation.currentTime = 50 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+}, 'Redundant change, before -> after, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for('animationstart').then(function() {
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.currentTime = 150 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.currentTime = 150 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, active -> before, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for('animationstart').then(function() {
+ animation.currentTime = 250 * MS_PER_SEC;
+ animation.currentTime = 150 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.currentTime = 150 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, active -> after, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.currentTime = 250 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.currentTime = 250 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, after -> before, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ animation.currentTime = 150 * MS_PER_SEC;
+ animation.currentTime = 250 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.currentTime = 250 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, after -> active, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s"
+ var animation = div.getAnimations()[0];
+
+ animation.pause();
+ animation.currentTime = 150 * MS_PER_SEC;
+
+ return eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ animation.currentTime = 50 * MS_PER_SEC;
+ return eventWatcher.wait_for('animationstart');
+ });
+}, 'Seeking finished -> paused dispatches animationstart');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ var exception;
+ try {
+ animation.currentTime = null;
+ } catch (e) {
+ exception = e;
+ }
+ assert_equals(exception.name, 'TypeError',
+ 'Expect TypeError exception on trying to set ' +
+ 'Animation.currentTime to null');
+ });
+}, 'Setting currentTime to null');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s';
+
+ var animation = div.getAnimations()[0];
+ var pauseTime;
+
+ return animation.ready.then(function() {
+ assert_not_equals(animation.currentTime, null,
+ 'Animation.currentTime not null on ready Promise resolve');
+ animation.pause();
+ return animation.ready;
+ }).then(function() {
+ pauseTime = animation.currentTime;
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(animation.currentTime, pauseTime,
+ 'Animation.currentTime is unchanged after pausing');
+ });
+}, 'Animation.currentTime after pausing');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ // just before animation ends:
+ animation.currentTime = 100 * MS_PER_SEC - 1;
+ return waitForAnimationFrames(2);
+ }).then(function() {
+ assert_equals(animation.currentTime, 100 * MS_PER_SEC,
+ 'Animation.currentTime should not continue to increase after the ' +
+ 'animation has finished');
+ });
+}, 'Animation.currentTime clamping');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ // play backwards:
+ animation.playbackRate = -1;
+
+ // just before animation ends (at the "start"):
+ animation.currentTime = 1;
+
+ return waitForAnimationFrames(2);
+ }).then(function() {
+ assert_equals(animation.currentTime, 0,
+ 'Animation.currentTime should not continue to decrease after an ' +
+ 'animation running in reverse has finished and currentTime is zero');
+ });
+}, 'Animation.currentTime clamping for reversed animation');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s';
+ var animation = div.getAnimations()[0];
+ animation.cancel();
+
+ assert_equals(animation.currentTime, null,
+ 'The currentTime of a cancelled animation should be null');
+}, 'Animation.currentTime after cancelling');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s';
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.finish();
+
+ // Initiate a pause then abort it
+ animation.pause();
+ animation.play();
+
+ // Wait to return to running state
+ return animation.ready;
+ }).then(function() {
+ assert_true(animation.currentTime < 100 * 1000,
+ 'After aborting a pause when finished, the currentTime should'
+ + ' jump back towards the start of the animation');
+ });
+}, 'After aborting a pause when finished, the call to play() should rewind'
+ + ' the current time');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/css-animations/file_animation-finish.html b/dom/animation/test/css-animations/file_animation-finish.html
new file mode 100644
index 0000000000..996cb2ce7f
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-finish.html
@@ -0,0 +1,97 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim {
+ from { margin-left: 100px; }
+ to { margin-left: 200px; }
+}
+</style>
+<body>
+<script>
+
+'use strict';
+
+const ANIM_PROP_VAL = 'anim 100s';
+const ANIM_DURATION = 100000; // ms
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = ANIM_PROP_VAL;
+ div.style.animationIterationCount = 'infinite';
+ var animation = div.getAnimations()[0];
+
+ var threw = false;
+ try {
+ animation.finish();
+ } catch (e) {
+ threw = true;
+ assert_equals(e.name, 'InvalidStateError',
+ 'Exception should be an InvalidStateError exception when ' +
+ 'trying to finish an infinite animation');
+ }
+ assert_true(threw,
+ 'Expect InvalidStateError exception trying to finish an ' +
+ 'infinite animation');
+}, 'Test exceptions when finishing infinite animation');
+
+async_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = ANIM_PROP_VAL + ' paused';
+ var animation = div.getAnimations()[0];
+
+ animation.ready.then(t.step_func(function() {
+ animation.finish();
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a paused animation should become ' +
+ '"finished" after finish() is called');
+ assert_approx_equals(animation.startTime,
+ animation.timeline.currentTime - ANIM_DURATION,
+ 0.0001,
+ 'The start time of a paused animation should be set ' +
+ 'after calling finish()');
+ t.done();
+ }));
+}, 'Test finish() while paused');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = ANIM_PROP_VAL + ' paused';
+ var animation = div.getAnimations()[0];
+
+ // Update playbackRate so we can test that the calculated startTime
+ // respects it
+ animation.playbackRate = 2;
+
+ // While animation is still pause-pending call finish()
+ animation.finish();
+
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a pause-pending animation should become ' +
+ '"finished" after finish() is called');
+ assert_approx_equals(animation.startTime,
+ animation.timeline.currentTime - ANIM_DURATION / 2,
+ 0.0001,
+ 'The start time of a pause-pending animation should ' +
+ 'be set after calling finish()');
+}, 'Test finish() while pause-pending with positive playbackRate');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = ANIM_PROP_VAL + ' paused';
+ var animation = div.getAnimations()[0];
+
+ animation.playbackRate = -2;
+ animation.finish();
+
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a pause-pending animation should become ' +
+ '"finished" after finish() is called');
+ assert_equals(animation.startTime, animation.timeline.currentTime,
+ 'The start time of a pause-pending animation should be ' +
+ 'set after calling finish()');
+}, 'Test finish() while pause-pending with negative playbackRate');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-finished.html b/dom/animation/test/css-animations/file_animation-finished.html
new file mode 100644
index 0000000000..c296abb117
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-finished.html
@@ -0,0 +1,93 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes abc {
+ to { transform: translate(10px) }
+}
+@keyframes def {}
+</style>
+<body>
+<script>
+'use strict';
+
+const ANIM_PROP_VAL = 'abc 100s';
+const ANIM_DURATION = 100 * MS_PER_SEC;
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ // Set up pending animation
+ div.style.animation = ANIM_PROP_VAL;
+ var animation = div.getAnimations()[0];
+ var previousFinishedPromise = animation.finished;
+ // Set up listeners on finished promise
+ var retPromise = animation.finished.then(function() {
+ assert_unreached('finished promise is fulfilled');
+ }).catch(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'finished promise is rejected with AbortError');
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise should change after the original is ' +
+ 'rejected');
+ });
+
+ // Now cancel the animation and flush styles
+ div.style.animation = '';
+ window.getComputedStyle(div).animation;
+
+ return retPromise;
+}, 'finished promise is rejected when an animation is cancelled by resetting ' +
+ 'the animation property');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ // As before, but this time instead of removing all animations, simply update
+ // the list of animations. At least for Firefox, updating is a different
+ // code path.
+
+ // Set up pending animation
+ div.style.animation = ANIM_PROP_VAL;
+ var animation = div.getAnimations()[0];
+ var previousFinishedPromise = animation.finished;
+
+ // Set up listeners on finished promise
+ var retPromise = animation.finished.then(function() {
+ assert_unreached('finished promise was fulfilled');
+ }).catch(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'finished promise is rejected with AbortError');
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise should change after the original is ' +
+ 'rejected');
+ });
+
+ // Now update the animation and flush styles
+ div.style.animation = 'def 100s';
+ window.getComputedStyle(div).animation;
+
+ return retPromise;
+}, 'finished promise is rejected when an animation is cancelled by changing ' +
+ 'the animation property');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = ANIM_PROP_VAL;
+ var animation = div.getAnimations()[0];
+ var previousFinishedPromise = animation.finished;
+ animation.currentTime = ANIM_DURATION;
+ return animation.finished.then(function() {
+ div.style.animationPlayState = 'running';
+ return waitForAnimationFrames(2);
+ }).then(function() {
+ assert_equals(animation.finished, previousFinishedPromise,
+ 'Should not replay when animation-play-state changes to ' +
+ '"running" on finished animation');
+ assert_equals(animation.currentTime, ANIM_DURATION,
+ 'currentTime should not change when animation-play-state ' +
+ 'changes to "running" on finished animation');
+ });
+}, 'Test finished promise changes when animationPlayState set to running');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-id.html b/dom/animation/test/css-animations/file_animation-id.html
new file mode 100644
index 0000000000..dbd5ee0eef
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-id.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes abc { }
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'abc 100s';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.id, '', 'id for CSS Animation is initially empty');
+ animation.id = 'anim'
+
+ assert_equals(animation.id, 'anim', 'animation.id reflects the value set');
+}, 'Animation.id for CSS Animations');
+
+done();
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/css-animations/file_animation-pausing.html b/dom/animation/test/css-animations/file_animation-pausing.html
new file mode 100644
index 0000000000..7176a0c1d6
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-pausing.html
@@ -0,0 +1,165 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim {
+ 0% { margin-left: 0px }
+ 100% { margin-left: 10000px }
+}
+</style>
+<body>
+<script>
+'use strict';
+
+function getMarginLeft(cs) {
+ return parseFloat(cs.marginLeft);
+}
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s paused';
+ var animation = div.getAnimations()[0];
+ assert_equals(getMarginLeft(cs), 0,
+ 'Initial value of margin-left is zero');
+ animation.play();
+
+ return animation.ready.then(waitForFrame).then(function() {
+ assert_true(getMarginLeft(cs) > 0,
+ 'Playing value of margin-left is greater than zero');
+ });
+}, 'play() overrides animation-play-state');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s paused';
+ var animation = div.getAnimations()[0];
+ assert_equals(getMarginLeft(cs), 0,
+ 'Initial value of margin-left is zero');
+
+ animation.pause();
+ div.style.animationPlayState = 'running';
+
+ return animation.ready.then(waitForFrame).then(function() {
+ assert_equals(cs.animationPlayState, 'running',
+ 'animation-play-state is running');
+ assert_equals(getMarginLeft(cs), 0,
+ 'Paused value of margin-left is zero');
+ });
+}, 'pause() overrides animation-play-state');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s paused';
+ var animation = div.getAnimations()[0];
+ assert_equals(getMarginLeft(cs), 0,
+ 'Initial value of margin-left is zero');
+ animation.play();
+ var previousAnimVal;
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'running';
+ cs.animationPlayState; // Trigger style resolution
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(cs.animationPlayState, 'running',
+ 'animation-play-state is running');
+ div.style.animationPlayState = 'paused';
+ return animation.ready;
+ }).then(function() {
+ assert_equals(cs.animationPlayState, 'paused',
+ 'animation-play-state is paused');
+ previousAnimVal = getMarginLeft(cs);
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(getMarginLeft(cs), previousAnimVal,
+ 'Animated value of margin-left does not change when'
+ + ' paused by style');
+ });
+}, 'play() is overridden by later setting "animation-play-state: paused"');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s';
+ var animation = div.getAnimations()[0];
+ assert_equals(getMarginLeft(cs), 0,
+ 'Initial value of margin-left is zero');
+
+ // Set the specified style first. If implementations fail to
+ // apply the style changes first, they will ignore the redundant
+ // call to play() and fail to correctly override the pause style.
+ div.style.animationPlayState = 'paused';
+ animation.play();
+ var previousAnimVal = getMarginLeft(cs);
+
+ return animation.ready.then(waitForFrame).then(function() {
+ assert_equals(cs.animationPlayState, 'paused',
+ 'animation-play-state is paused');
+ assert_true(getMarginLeft(cs) > previousAnimVal,
+ 'Playing value of margin-left is increasing');
+ });
+}, 'play() flushes pending changes to animation-play-state first');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s paused';
+ var animation = div.getAnimations()[0];
+ assert_equals(getMarginLeft(cs), 0,
+ 'Initial value of margin-left is zero');
+
+ // Unlike the previous test for play(), since calling pause() is sticky,
+ // we'll apply it even if the underlying style also says we're paused.
+ //
+ // We would like to test that implementations flush styles before running
+ // pause() but actually there's no style we can currently set that will
+ // change the behavior of pause(). That may change in the future
+ // (e.g. if we introduce animation-timeline or animation-playback-rate etc.).
+ //
+ // For now this just serves as a sanity check that we do the same thing
+ // even if we set style before calling the API.
+ div.style.animationPlayState = 'running';
+ animation.pause();
+ var previousAnimVal = getMarginLeft(cs);
+
+ return animation.ready.then(waitForFrame).then(function() {
+ assert_equals(cs.animationPlayState, 'running',
+ 'animation-play-state is running');
+ assert_equals(getMarginLeft(cs), previousAnimVal,
+ 'Paused value of margin-left does not change');
+ });
+}, 'pause() applies pending changes to animation-play-state first');
+// (Note that we can't actually test for this; see comment above, in test-body.)
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 1000s' });
+ var animation = div.getAnimations()[0];
+ var readyPromiseRun = false;
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'paused';
+ assert_equals(animation.playState, 'pending', 'Animation is pause pending');
+
+ // Set current time
+ animation.currentTime = 5 * MS_PER_SEC;
+ assert_equals(animation.playState, 'paused',
+ 'Animation is paused immediately after setting currentTime');
+ assert_equals(animation.startTime, null,
+ 'Animation startTime is unresolved immediately after ' +
+ 'setting currentTime');
+ assert_equals(animation.currentTime, 5 * MS_PER_SEC,
+ 'Animation currentTime does not change when forcing a ' +
+ 'pause operation to complete');
+
+ // The ready promise should now be resolved. If it's not then test will
+ // probably time out before anything else happens that causes it to resolve.
+ return animation.ready;
+ });
+}, 'Setting the current time completes a pending pause');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-playstate.html b/dom/animation/test/css-animations/file_animation-playstate.html
new file mode 100644
index 0000000000..ce9839f382
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-playstate.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim { }
+</style>
+<body>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.playState, 'pending');
+
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'running');
+ });
+}, 'Animation returns correct playState when running');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s paused';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.playState, 'pending');
+
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'paused');
+ });
+}, 'Animation returns correct playState when paused');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s';
+ var animation = div.getAnimations()[0];
+ animation.pause();
+ assert_equals(animation.playState, 'pending');
+
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'paused');
+ });
+}, 'Animation.playState updates when paused by script');
+
+test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s paused';
+ var animation = div.getAnimations()[0];
+ div.style.animationPlayState = 'running';
+
+ // This test also checks that calling playState flushes style
+ assert_equals(animation.playState, 'pending',
+ 'Animation.playState reports pending after updating'
+ + ' animation-play-state (got: ' + animation.playState + ')');
+}, 'Animation.playState updates when resumed by setting style');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim 1000s';
+ var animation = div.getAnimations()[0];
+ animation.cancel();
+ assert_equals(animation.playState, 'idle');
+}, 'Animation returns correct playState when cancelled');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-ready.html b/dom/animation/test/css-animations/file_animation-ready.html
new file mode 100644
index 0000000000..9318a1a182
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-ready.html
@@ -0,0 +1,149 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes abc {
+ to { transform: translate(10px) }
+}
+</style>
+<body>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'abc 100s paused';
+ var animation = div.getAnimations()[0];
+ var originalReadyPromise = animation.ready;
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'running';
+ assert_not_equals(animation.ready, originalReadyPromise,
+ 'After updating animation-play-state a new ready promise'
+ + ' object is created');
+ });
+}, 'A new ready promise is created when setting animation-play-state: running');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+
+ // Set up pending animation
+ div.style.animation = 'abc 100s';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.playState, 'pending',
+ 'Animation is initially pending');
+
+ // Set up listeners on ready promise
+ var retPromise = animation.ready.then(function() {
+ assert_unreached('ready promise is fulfilled');
+ }).catch(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'ready promise is rejected with AbortError');
+ });
+
+ // Now cancel the animation and flush styles
+ div.style.animation = '';
+ window.getComputedStyle(div).animation;
+
+ return retPromise;
+}, 'ready promise is rejected when an animation is cancelled by resetting'
+ + ' the animation property');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+
+ // As before, but this time instead of removing all animations, simply update
+ // the list of animations. At least for Firefox, updating is a different
+ // code path.
+
+ // Set up pending animation
+ div.style.animation = 'abc 100s';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.playState, 'pending',
+ 'Animation is initially pending');
+
+ // Set up listeners on ready promise
+ var retPromise = animation.ready.then(function() {
+ assert_unreached('ready promise was fulfilled');
+ }).catch(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'ready promise is rejected with AbortError');
+ });
+
+ // Now update the animation and flush styles
+ div.style.animation = 'def 100s';
+ window.getComputedStyle(div).animation;
+
+ return retPromise;
+}, 'ready promise is rejected when an animation is cancelled by updating'
+ + ' the animation property');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: abc 100s' });
+ var animation = div.getAnimations()[0];
+ var originalReadyPromise = animation.ready;
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'paused';
+ assert_not_equals(animation.ready, originalReadyPromise,
+ 'A new Promise object is generated when setting'
+ + ' animation-play-state: paused');
+ });
+}, 'A new ready promise is created when setting animation-play-state: paused');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: abc 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'paused';
+ var firstReadyPromise = animation.ready;
+ animation.pause();
+ assert_equals(animation.ready, firstReadyPromise,
+ 'Ready promise objects are identical after redundant pause');
+ });
+}, 'Pausing twice re-uses the same Promise');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: abc 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'paused';
+
+ // Flush style and verify we're pending at the same time
+ assert_equals(animation.playState, 'pending', 'Animation is pending');
+ var pauseReadyPromise = animation.ready;
+
+ // Now play again immediately
+ div.style.animationPlayState = 'running';
+ assert_equals(animation.playState, 'pending', 'Animation is still pending');
+ assert_equals(animation.ready, pauseReadyPromise,
+ 'The pause Promise is re-used when playing while waiting'
+ + ' to pause');
+
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.playState, 'running',
+ 'Animation is running after aborting a pause');
+ });
+}, 'If a pause operation is interrupted, the ready promise is reused');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: abc 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'paused';
+ return animation.ready;
+ }).then(function(resolvedAnimation) {
+ assert_equals(resolvedAnimation, animation,
+ 'Promise received when ready Promise for a pause operation'
+ + ' is completed is the animation on which the pause was'
+ + ' performed');
+ });
+}, 'When a pause is complete the Promise callback gets the correct animation');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-reverse.html b/dom/animation/test/css-animations/file_animation-reverse.html
new file mode 100644
index 0000000000..5060fa55f1
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-reverse.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim {
+ to { transform: translate(100px) }
+}
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+ div.style.animation = "";
+ flushComputedStyle(div);
+
+ assert_equals(animation.currentTime, null);
+ animation.reverse();
+
+ assert_equals(animation.currentTime, 100 * MS_PER_SEC,
+ 'animation.currentTime should be its effect end');
+}, 'reverse() from idle state starts playing the animation');
+
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-starttime.html b/dom/animation/test/css-animations/file_animation-starttime.html
new file mode 100644
index 0000000000..46144464c0
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-starttime.html
@@ -0,0 +1,383 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for the effect of setting a CSS animation's
+ Animation.startTime</title>
+ <style>
+
+.animated-div {
+ margin-left: 10px;
+ /* Make it easier to calculate expected values: */
+ animation-timing-function: linear ! important;
+}
+
+@keyframes anim {
+ from { margin-left: 100px; }
+ to { margin-left: 200px; }
+}
+
+ </style>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+
+'use strict';
+
+// TODO: We should separate this test(Testing for CSS Animation events /
+// Testing for start time of Web Animation).
+// e.g:
+// CSS Animation events test:
+// - check the firing an event after setting an Animation.startTime
+// The start time of Web Animation test:
+// - check an start time value on several situation(init / processing..)
+// - Based on W3C Spec, check the behavior of setting current time.
+
+// TODO: Once the computedTiming property is implemented, add checks to the
+// checker helpers to ensure that computedTiming's properties are updated as
+// expected.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
+
+const CSS_ANIM_EVENTS =
+ ['animationstart', 'animationiteration', 'animationend'];
+
+test(function(t)
+{
+ var div = addDiv(t, { 'style': 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+}, 'startTime of a newly created (play-pending) animation is unresolved');
+
+test(function(t)
+{
+ var div = addDiv(t, { 'style': 'animation: anim 100s paused' });
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+}, 'startTime of a newly created (pause-pending) animation is unresolved');
+
+promise_test(function(t)
+{
+ var div = addDiv(t, { 'style': 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_true(animation.startTime > 0,
+ 'startTime is resolved when running');
+ });
+}, 'startTime is resolved when running');
+
+promise_test(function(t)
+{
+ var div = addDiv(t, { 'style': 'animation: anim 100s paused' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_equals(animation.startTime, null,
+ 'startTime is unresolved when paused');
+ });
+}, 'startTime is unresolved when paused');
+
+promise_test(function(t)
+{
+ var div = addDiv(t, { 'style': 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'paused';
+ getComputedStyle(div).animationPlayState;
+
+ assert_not_equals(animation.startTime, null,
+ 'startTime is resolved when pause-pending');
+
+ div.style.animationPlayState = 'running';
+ getComputedStyle(div).animationPlayState;
+
+ assert_not_equals(animation.startTime, null,
+ 'startTime is preserved when a pause is aborted');
+ });
+}, 'startTime while pause-pending and play-pending');
+
+promise_test(function(t) {
+ var div = addDiv(t, { 'style': 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+ // Seek to end to put us in the finished state
+ animation.currentTime = 100 * MS_PER_SEC;
+
+ return animation.ready.then(function() {
+ // Call play() which puts us back in the running state
+ animation.play();
+
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+ });
+}, 'startTime while play-pending from finished state');
+
+test(function(t) {
+ var div = addDiv(t, { 'style': 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ // Call play() which puts us back in the running state
+ animation.play();
+
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+}, 'startTime while play-pending from finished state using finish()');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ assert_equals(animation.startTime, null, 'The initial startTime is null');
+ var initialTimelineTime = document.timeline.currentTime;
+
+ return animation.ready.then(function() {
+ assert_true(animation.startTime > initialTimelineTime,
+ 'After the animation has started, startTime is greater than ' +
+ 'the time when it was started');
+ var startTimeBeforePausing = animation.startTime;
+
+ div.style.animationPlayState = 'paused';
+ // Flush styles just in case querying animation.startTime doesn't flush
+ // styles (which would be a bug in of itself and could mask a further bug
+ // by causing startTime to appear to not change).
+ getComputedStyle(div).animationPlayState;
+
+ assert_equals(animation.startTime, startTimeBeforePausing,
+ 'The startTime does not change when pausing-pending');
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.startTime, null,
+ 'After actually pausing, the startTime of an animation ' +
+ 'is null');
+ });
+}, 'Pausing should make the startTime become null');
+
+test(function(t)
+{
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s 100s';
+ var animation = div.getAnimations()[0];
+ var currentTime = animation.timeline.currentTime;
+ animation.startTime = currentTime;
+
+ assert_times_equal(animation.startTime, currentTime,
+ 'Check setting of startTime actually works');
+}, 'Sanity test to check round-tripping assigning to a new animation\'s ' +
+ 'startTime');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = 'anim 100s 100s';
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_less_than_equal(animation.startTime, animation.timeline.currentTime,
+ 'Animation.startTime should be less than the timeline\'s ' +
+ 'currentTime on the first paint tick after animation creation');
+
+ animation.startTime = animation.timeline.currentTime - 100 * MS_PER_SEC;
+ return eventWatcher.wait_for('animationstart');
+ }).then(function() {
+ animation.startTime = animation.timeline.currentTime - 200 * MS_PER_SEC;
+ return eventWatcher.wait_for('animationend');
+ });
+}, 'Skipping forward through animation');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = 'anim 100s 100s';
+ var animation = div.getAnimations()[0];
+ animation.startTime = animation.timeline.currentTime - 200 * MS_PER_SEC;
+ var previousTimelineTime = animation.timeline.currentTime;
+
+ return eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ assert_true(document.timeline.currentTime - previousTimelineTime <
+ 100 * MS_PER_SEC,
+ 'Sanity check that seeking worked rather than the events ' +
+ 'firing after normal playback through the very long ' +
+ 'animation duration');
+
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+
+ // Despite going backwards from after the end of the animation (to being
+ // in the active interval), we now expect an 'animationstart' event
+ // because the animation should go from being inactive to active.
+ return eventWatcher.wait_for('animationstart');
+ }).then(function() {
+ animation.startTime = animation.timeline.currentTime;
+
+ // Despite going backwards from just after the active interval starts to
+ // the animation start time, we now expect an animationend event
+ // because we went from inside to outside the active interval.
+ return eventWatcher.wait_for('animationend');
+ }).then(function() {
+ assert_less_than_equal(animation.startTime, animation.timeline.currentTime,
+ 'Animation.startTime should be less than the timeline\'s ' +
+ 'currentTime on the first paint tick after animation creation');
+ });
+}, 'Skipping backwards through animation');
+
+// Next we have multiple tests to check that redundant startTime changes do NOT
+// dispatch events. It's impossible to distinguish between events not being
+// dispatched and events just taking an incredibly long time to dispatch
+// without waiting an infinitely long time. Obviously we don't want to do that
+// (block this test from finishing forever), so instead we just listen for
+// events until two animation frames (i.e. requestAnimationFrame callbacks)
+// have happened, then assume that no events will ever be dispatched for the
+// redundant changes if no events were detected in that time.
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+}, 'Redundant change, before -> active, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+ animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+}, 'Redundant change, before -> after, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for('animationstart').then(function() {
+ animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, active -> before, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for('animationstart').then(function() {
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, active -> after, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, after -> before, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+
+ });
+ // get us into the initial state:
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, after -> active, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s 100s';
+ var animation = div.getAnimations()[0];
+ var storedCurrentTime;
+
+ return animation.ready.then(function() {
+ storedCurrentTime = animation.currentTime;
+ animation.startTime = null;
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.currentTime, storedCurrentTime,
+ 'Test that hold time is correct');
+ });
+}, 'Setting startTime to null');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s';
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ var savedStartTime = animation.startTime;
+
+ assert_not_equals(animation.startTime, null,
+ 'Animation.startTime not null on ready Promise resolve');
+
+ animation.pause();
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.startTime, null,
+ 'Animation.startTime is null after paused');
+ assert_equals(animation.playState, 'paused',
+ 'Animation.playState is "paused" after pause() call');
+ });
+}, 'Animation.startTime after pausing');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s';
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.cancel();
+ assert_equals(animation.startTime, null,
+ 'The startTime of a cancelled animation should be null');
+ });
+}, 'Animation.startTime after cancelling');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/css-animations/file_animations-dynamic-changes.html b/dom/animation/test/css-animations/file_animations-dynamic-changes.html
new file mode 100644
index 0000000000..8f16536ae9
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animations-dynamic-changes.html
@@ -0,0 +1,154 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim1 {
+ to { left: 100px }
+}
+@keyframes anim2 { }
+</style>
+<body>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s';
+ var originalAnimation = div.getAnimations()[0];
+ var originalStartTime;
+ var originalCurrentTime;
+
+ // Wait a moment so we can confirm the startTime doesn't change (and doesn't
+ // simply reflect the current time).
+ return originalAnimation.ready.then(function() {
+ originalStartTime = originalAnimation.startTime;
+ originalCurrentTime = originalAnimation.currentTime;
+
+ // Wait a moment so we can confirm the startTime doesn't change (and
+ // doesn't simply reflect the current time).
+ return waitForFrame();
+ }).then(function() {
+ div.style.animationDuration = '200s';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation, originalAnimation,
+ 'The same Animation is returned after updating'
+ + ' animation duration');
+ assert_equals(animation.startTime, originalStartTime,
+ 'Animations returned by getAnimations preserve'
+ + ' their startTime even when they are updated');
+ // Sanity check
+ assert_not_equals(animation.currentTime, originalCurrentTime,
+ 'Animation.currentTime has updated in next'
+ + ' requestAnimationFrame callback');
+ });
+}, 'Animations preserve their startTime when changed');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s, anim1 100s';
+
+ // Store original state
+ var animations = div.getAnimations();
+ var animation1 = animations[0];
+ var animation2 = animations[1];
+
+ // Update first in list
+ div.style.animationDuration = '200s, 100s';
+ animations = div.getAnimations();
+ assert_equals(animations[0], animation1,
+ 'First Animation is in same position after update');
+ assert_equals(animations[1], animation2,
+ 'Second Animation is in same position after update');
+}, 'Updated Animations maintain their order in the list');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 200s, anim1 100s';
+
+ // Store original state
+ var animations = div.getAnimations();
+ var animation1 = animations[0];
+ var animation2 = animations[1];
+
+ // Wait before continuing so we can compare start times (otherwise the
+ // new Animation objects and existing Animation objects will all have the same
+ // start time).
+ return waitForAllAnimations(animations).then(waitForFrame).then(function() {
+ // Swap duration of first and second in list and prepend animation at the
+ // same time
+ div.style.animation = 'anim1 100s, anim1 100s, anim1 200s';
+ animations = div.getAnimations();
+ assert_true(animations[0] !== animation1 && animations[0] !== animation2,
+ 'New Animation is prepended to start of list');
+ assert_equals(animations[1], animation1,
+ 'First Animation is in second position after update');
+ assert_equals(animations[2], animation2,
+ 'Second Animation is in third position after update');
+ assert_equals(animations[1].startTime, animations[2].startTime,
+ 'Old Animations have the same start time');
+ // TODO: Check that animations[0].startTime === null
+ return animations[0].ready;
+ }).then(function() {
+ assert_true(animations[0].startTime > animations[1].startTime,
+ 'New Animation has later start time');
+ });
+}, 'Only the startTimes of existing animations are preserved');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s, anim1 100s';
+ var secondAnimation = div.getAnimations()[1];
+
+ // Wait before continuing so we can compare start times
+ return secondAnimation.ready.then(waitForFrame).then(function() {
+ // Trim list of animations
+ div.style.animationName = 'anim1';
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1, 'List of Animations was trimmed');
+ assert_equals(animations[0], secondAnimation,
+ 'Remaining Animation is the second one in the list');
+ assert_equals(typeof(animations[0].startTime), 'number',
+ 'Remaining Animation has resolved startTime');
+ assert_true(animations[0].startTime < animations[0].timeline.currentTime,
+ 'Remaining Animation preserves startTime');
+ });
+}, 'Animations are removed from the start of the list while preserving'
+ + ' the state of existing Animations');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s';
+ var firstAddedAnimation = div.getAnimations()[0],
+ secondAddedAnimation,
+ animations;
+
+ // Wait and add second Animation
+ return firstAddedAnimation.ready.then(waitForFrame).then(function() {
+ div.style.animation = 'anim1 100s, anim1 100s';
+ secondAddedAnimation = div.getAnimations()[0];
+
+ // Wait again and add another Animation
+ return secondAddedAnimation.ready.then(waitForFrame);
+ }).then(function() {
+ div.style.animation = 'anim1 100s, anim2 100s, anim1 100s';
+ animations = div.getAnimations();
+ assert_not_equals(firstAddedAnimation, secondAddedAnimation,
+ 'New Animations are added to start of the list');
+ assert_equals(animations[0], secondAddedAnimation,
+ 'Second Animation remains in same position after'
+ + ' interleaving');
+ assert_equals(animations[2], firstAddedAnimation,
+ 'First Animation remains in same position after'
+ + ' interleaving');
+ return animations[1].ready;
+ }).then(function() {
+ assert_true(animations[1].startTime > animations[0].startTime,
+ 'Interleaved animation starts later than existing animations');
+ assert_true(animations[0].startTime > animations[2].startTime,
+ 'Original animations retain their start time');
+ });
+}, 'Animation state is preserved when interleaving animations in list');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_cssanimation-animationname.html b/dom/animation/test/css-animations/file_cssanimation-animationname.html
new file mode 100644
index 0000000000..fd69d85777
--- /dev/null
+++ b/dom/animation/test/css-animations/file_cssanimation-animationname.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes xyz {
+ to { left: 100px }
+}
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'xyz 100s';
+ assert_equals(div.getAnimations()[0].animationName, 'xyz',
+ 'Animation name matches keyframes rule name');
+}, 'Animation name makes keyframe rule');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'x\\yz 100s';
+ assert_equals(div.getAnimations()[0].animationName, 'xyz',
+ 'Escaped animation name matches keyframes rule name');
+}, 'Escaped animation name');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'x\\79 z 100s';
+ assert_equals(div.getAnimations()[0].animationName, 'xyz',
+ 'Hex-escaped animation name matches keyframes rule'
+ + ' name');
+}, 'Animation name with hex-escape');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_document-get-animations.html b/dom/animation/test/css-animations/file_document-get-animations.html
new file mode 100644
index 0000000000..abe02d7fc3
--- /dev/null
+++ b/dom/animation/test/css-animations/file_document-get-animations.html
@@ -0,0 +1,276 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes animLeft {
+ to { left: 100px }
+}
+@keyframes animTop {
+ to { top: 100px }
+}
+@keyframes animBottom {
+ to { bottom: 100px }
+}
+@keyframes animRight {
+ to { right: 100px }
+}
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ assert_equals(document.getAnimations().length, 0,
+ 'getAnimations returns an empty sequence for a document'
+ + ' with no animations');
+}, 'getAnimations for non-animated content');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ // Add an animation
+ div.style.animation = 'animLeft 100s';
+ assert_equals(document.getAnimations().length, 1,
+ 'getAnimations returns a running CSS Animation');
+
+ // Add another animation
+ div.style.animation = 'animLeft 100s, animTop 100s';
+ assert_equals(document.getAnimations().length, 2,
+ 'getAnimations returns two running CSS Animations');
+
+ // Remove both
+ div.style.animation = '';
+ assert_equals(document.getAnimations().length, 0,
+ 'getAnimations returns no running CSS Animations');
+}, 'getAnimations for CSS Animations');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'animLeft 100s, animTop 100s, animRight 100s, ' +
+ 'animBottom 100s';
+
+ var animations = document.getAnimations();
+ assert_equals(animations.length, 4,
+ 'getAnimations returns all running CSS Animations');
+ assert_equals(animations[0].animationName, 'animLeft',
+ 'Order of first animation returned');
+ assert_equals(animations[1].animationName, 'animTop',
+ 'Order of second animation returned');
+ assert_equals(animations[2].animationName, 'animRight',
+ 'Order of third animation returned');
+ assert_equals(animations[3].animationName, 'animBottom',
+ 'Order of fourth animation returned');
+}, 'Order of CSS Animations - within an element');
+
+test(function(t) {
+ var div1 = addDiv(t, { style: 'animation: animLeft 100s' });
+ var div2 = addDiv(t, { style: 'animation: animLeft 100s' });
+ var div3 = addDiv(t, { style: 'animation: animLeft 100s' });
+ var div4 = addDiv(t, { style: 'animation: animLeft 100s' });
+
+ var animations = document.getAnimations();
+ assert_equals(animations.length, 4,
+ 'getAnimations returns all running CSS Animations');
+ assert_equals(animations[0].effect.target, div1,
+ 'Order of first animation returned');
+ assert_equals(animations[1].effect.target, div2,
+ 'Order of second animation returned');
+ assert_equals(animations[2].effect.target, div3,
+ 'Order of third animation returned');
+ assert_equals(animations[3].effect.target, div4,
+ 'Order of fourth animation returned');
+
+ // Order should be depth-first pre-order so add some depth as follows:
+ //
+ // <parent>
+ // / |
+ // 2 3
+ // / \
+ // 1 4
+ //
+ // Which should give: 2, 1, 4, 3
+ div2.appendChild(div1);
+ div2.appendChild(div4);
+ animations = document.getAnimations();
+ assert_equals(animations[0].effect.target, div2,
+ 'Order of first animation returned after tree surgery');
+ assert_equals(animations[1].effect.target, div1,
+ 'Order of second animation returned after tree surgery');
+ assert_equals(animations[2].effect.target, div4,
+ 'Order of third animation returned after tree surgery');
+ assert_equals(animations[3].effect.target, div3,
+ 'Order of fourth animation returned after tree surgery');
+
+}, 'Order of CSS Animations - across elements');
+
+test(function(t) {
+ var div1 = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' });
+ var div2 = addDiv(t, { style: 'animation: animBottom 100s' });
+
+ var expectedResults = [ [ div1, 'animLeft' ],
+ [ div1, 'animTop' ],
+ [ div2, 'animBottom' ] ];
+ var animations = document.getAnimations();
+ assert_equals(animations.length, expectedResults.length,
+ 'getAnimations returns all running CSS Animations');
+ animations.forEach(function(anim, i) {
+ assert_equals(anim.effect.target, expectedResults[i][0],
+ 'Target of animation in position ' + i);
+ assert_equals(anim.animationName, expectedResults[i][1],
+ 'Name of animation in position ' + i);
+ });
+
+ // Modify tree structure and animation list
+ div2.appendChild(div1);
+ div1.style.animation = 'animLeft 100s, animRight 100s, animTop 100s';
+
+ expectedResults = [ [ div2, 'animBottom' ],
+ [ div1, 'animLeft' ],
+ [ div1, 'animRight' ],
+ [ div1, 'animTop' ] ];
+ animations = document.getAnimations();
+ assert_equals(animations.length, expectedResults.length,
+ 'getAnimations returns all running CSS Animations after ' +
+ 'making changes');
+ animations.forEach(function(anim, i) {
+ assert_equals(anim.effect.target, expectedResults[i][0],
+ 'Target of animation in position ' + i + ' after changes');
+ assert_equals(anim.animationName, expectedResults[i][1],
+ 'Name of animation in position ' + i + ' after changes');
+ });
+}, 'Order of CSS Animations - across and within elements');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' });
+ var animLeft = document.getAnimations()[0];
+ assert_equals(animLeft.animationName, 'animLeft',
+ 'Originally, animLeft animation comes first');
+
+ // Disassociate animLeft from markup and restart
+ div.style.animation = 'animTop 100s';
+ animLeft.play();
+
+ var animations = document.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations returns markup-bound and free animations');
+ assert_equals(animations[0].animationName, 'animTop',
+ 'Markup-bound animations come first');
+ assert_equals(animations[1], animLeft, 'Free animations come last');
+}, 'Order of CSS Animations - markup-bound vs free animations');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' });
+ var animLeft = document.getAnimations()[0];
+ var animTop = document.getAnimations()[1];
+
+ // Disassociate both animations from markup and restart in opposite order
+ div.style.animation = '';
+ animTop.play();
+ animLeft.play();
+
+ var animations = document.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations returns free animations');
+ assert_equals(animations[0], animTop,
+ 'Free animations are returned in the order they are started');
+ assert_equals(animations[1], animLeft,
+ 'Animations started later are returned later');
+
+ // Restarting an animation should have no effect
+ animTop.cancel();
+ animTop.play();
+ assert_equals(document.getAnimations()[0], animTop,
+ 'After restarting, the ordering of free animations' +
+ ' does not change');
+}, 'Order of CSS Animations - free animations');
+
+test(function(t) {
+ // Add an animation first
+ var div = addDiv(t, { style: 'animation: animLeft 100s' });
+ div.style.top = '0px';
+ div.style.transition = 'all 100s';
+ flushComputedStyle(div);
+
+ // *Then* add a transition
+ div.style.top = '100px';
+ flushComputedStyle(div);
+
+ // Although the transition was added later, it should come first in the list
+ var animations = document.getAnimations();
+ assert_equals(animations.length, 2,
+ 'Both CSS animations and transitions are returned');
+ assert_class_string(animations[0], 'CSSTransition', 'Transition comes first');
+ assert_class_string(animations[1], 'CSSAnimation', 'Animation comes second');
+}, 'Order of CSS Animations and CSS Transitions');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s forwards' });
+ div.getAnimations()[0].finish();
+ assert_equals(document.getAnimations().length, 1,
+ 'Forwards-filling CSS animations are returned');
+}, 'Finished but filling CSS Animations are returned');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s' });
+ div.getAnimations()[0].finish();
+ assert_equals(document.getAnimations().length, 0,
+ 'Non-filling finished CSS animations are not returned');
+}, 'Finished but not filling CSS Animations are not returned');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s 100s' });
+ assert_equals(document.getAnimations().length, 1,
+ 'Yet-to-start CSS animations are returned');
+}, 'Yet-to-start CSS Animations are returned');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s' });
+ div.getAnimations()[0].cancel();
+ assert_equals(document.getAnimations().length, 0,
+ 'CSS animations cancelled by the API are not returned');
+}, 'CSS Animations cancelled via the API are not returned');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s' });
+ var anim = div.getAnimations()[0];
+ anim.cancel();
+ anim.play();
+ assert_equals(document.getAnimations().length, 1,
+ 'CSS animations cancelled and restarted by the API are ' +
+ 'returned');
+}, 'CSS Animations cancelled and restarted via the API are returned');
+
+test(function(t) {
+ addStyle(t, { '#parent::after': 'animation: animLeft 10s;',
+ '#parent::before': 'animation: animRight 10s;' });
+ // create two divs with these arrangement:
+ // parent
+ // ::before,
+ // ::after
+ // |
+ // child
+ var parent = addDiv(t, { 'id': 'parent' });
+ var child = addDiv(t);
+ parent.appendChild(child);
+ [parent, child].forEach((div) => {
+ div.setAttribute('style', 'animation: animBottom 10s');
+ });
+
+ var anims = document.getAnimations();
+ assert_equals(anims.length, 4,
+ 'CSS animations on both pseudo-elements and elements ' +
+ 'are returned');
+ assert_equals(anims[0].effect.target, parent,
+ 'The animation targeting the parent element comes first');
+ assert_equals(anims[1].effect.target.type, '::before',
+ 'The animation targeting the ::before element comes second');
+ assert_equals(anims[2].effect.target.type, '::after',
+ 'The animation targeting the ::after element comes third');
+ assert_equals(anims[3].effect.target, child,
+ 'The animation targeting the child element comes last');
+}, 'CSS Animations targetting (pseudo-)elements should have correct order ' +
+ 'after sorting');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_effect-target.html b/dom/animation/test/css-animations/file_effect-target.html
new file mode 100644
index 0000000000..006028e345
--- /dev/null
+++ b/dom/animation/test/css-animations/file_effect-target.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim { }
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim 100s';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.effect.target, div,
+ 'Animation.target is the animatable div');
+}, 'Returned CSS animations have the correct effect target');
+
+test(function(t) {
+ addStyle(t, { '.after::after': 'animation: anim 10s, anim 100s;' });
+ var div = addDiv(t, { class: 'after' });
+ var anims = document.getAnimations();
+ assert_equals(anims.length, 2,
+ 'Got animations running on ::after pseudo element');
+ assert_equals(anims[0].effect.target, anims[1].effect.target,
+ 'Both animations return the same target object');
+}, 'effect.target should return the same CSSPseudoElement object each time');
+
+test(function(t) {
+ addStyle(t, { '.after::after': 'animation: anim 10s;' });
+ var div = addDiv(t, { class: 'after' });
+ var pseudoTarget = document.getAnimations()[0].effect.target;
+ var effect = new KeyframeEffectReadOnly(pseudoTarget,
+ { background: ["blue", "red"] },
+ 3 * MS_PER_SEC);
+ var newAnim = new Animation(effect, document.timeline);
+ newAnim.play();
+ var anims = document.getAnimations();
+ assert_equals(anims.length, 2,
+ 'Got animations running on ::after pseudo element');
+ assert_not_equals(anims[0], newAnim,
+ 'The scriped-generated animation appears last');
+ assert_equals(newAnim.effect.target, pseudoTarget,
+ 'The effect.target of the scripted-generated animation is ' +
+ 'the same as the one from the argument of ' +
+ 'KeyframeEffectReadOnly constructor');
+ assert_equals(anims[0].effect.target, newAnim.effect.target,
+ 'Both animations return the same target object');
+}, 'effect.target from the script-generated animation should return the same ' +
+ 'CSSPseudoElement object as that from the CSS generated animation');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_element-get-animations.html b/dom/animation/test/css-animations/file_element-get-animations.html
new file mode 100644
index 0000000000..68386c98b0
--- /dev/null
+++ b/dom/animation/test/css-animations/file_element-get-animations.html
@@ -0,0 +1,445 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim1 {
+ to { left: 100px }
+}
+@keyframes anim2 {
+ to { top: 100px }
+}
+@keyframes multiPropAnim {
+ to { background: green, opacity: 0.5, left: 100px, top: 100px }
+}
+@keyframes empty { }
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+ assert_equals(div.getAnimations().length, 0,
+ 'getAnimations returns an empty sequence for an element'
+ + ' with no animations');
+}, 'getAnimations for non-animated content');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+
+ // FIXME: This test does too many things. It should be split up.
+
+ // Add an animation
+ div.style.animation = 'anim1 100s';
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ 'getAnimations returns an Animation running CSS Animations');
+ return animations[0].ready.then(function() {
+ var startTime = animations[0].startTime;
+ assert_true(startTime > 0 && startTime <= document.timeline.currentTime,
+ 'CSS animation has a sensible start time');
+
+ // Wait a moment then add a second animation.
+ //
+ // We wait for the next frame so that we can test that the start times of
+ // the animations differ.
+ return waitForFrame();
+ }).then(function() {
+ div.style.animation = 'anim1 100s, anim2 100s';
+ animations = div.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations returns one Animation for each value of'
+ + ' animation-name');
+ // Wait until both Animations are ready
+ // (We don't make any assumptions about the order of the Animations since
+ // that is the purpose of the following test.)
+ return waitForAllAnimations(animations);
+ }).then(function() {
+ assert_true(animations[0].startTime < animations[1].startTime,
+ 'Additional Animations for CSS animations start after the original'
+ + ' animation and appear later in the list');
+ });
+}, 'getAnimations for CSS Animations');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim1 100s' });
+ assert_class_string(div.getAnimations()[0], 'CSSAnimation',
+ 'Interface of returned animation is CSSAnimation');
+}, 'getAnimations returns CSSAnimation objects for CSS Animations');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ // Add an animation that targets multiple properties
+ div.style.animation = 'multiPropAnim 100s';
+ assert_equals(div.getAnimations().length, 1,
+ 'getAnimations returns only one Animation for a CSS Animation'
+ + ' that targets multiple properties');
+}, 'getAnimations for multi-property animations');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+
+ // Add an animation
+ div.style.backgroundColor = 'red';
+ div.style.animation = 'anim1 100s';
+ window.getComputedStyle(div).backgroundColor;
+
+ // Wait until a frame after the animation starts, then add a transition
+ var animations = div.getAnimations();
+ return animations[0].ready.then(waitForFrame).then(function() {
+ div.style.transition = 'all 100s';
+ div.style.backgroundColor = 'green';
+
+ animations = div.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations returns Animations for both animations and'
+ + ' transitions that run simultaneously');
+ assert_class_string(animations[0], 'CSSTransition',
+ 'First-returned animation is the CSS Transition');
+ assert_class_string(animations[1], 'CSSAnimation',
+ 'Second-returned animation is the CSS Animation');
+ });
+}, 'getAnimations for both CSS Animations and CSS Transitions at once');
+
+async_test(function(t) {
+ var div = addDiv(t);
+
+ // Set up event listener
+ div.addEventListener('animationend', t.step_func(function() {
+ assert_equals(div.getAnimations().length, 0,
+ 'getAnimations does not return Animations for finished '
+ + ' (and non-forwards-filling) CSS Animations');
+ t.done();
+ }));
+
+ // Add a very short animation
+ div.style.animation = 'anim1 0.01s';
+}, 'getAnimations for CSS Animations that have finished');
+
+async_test(function(t) {
+ var div = addDiv(t);
+
+ // Set up event listener
+ div.addEventListener('animationend', t.step_func(function() {
+ assert_equals(div.getAnimations().length, 1,
+ 'getAnimations returns Animations for CSS Animations that have'
+ + ' finished but are filling forwards');
+ t.done();
+ }));
+
+ // Add a very short animation
+ div.style.animation = 'anim1 0.01s forwards';
+}, 'getAnimations for CSS Animations that have finished but are'
+ + ' forwards filling');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'none 100s';
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 0,
+ 'getAnimations returns an empty sequence for an element'
+ + ' with animation-name: none');
+
+ div.style.animation = 'none 100s, anim1 100s';
+ animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ 'getAnimations returns Animations only for those CSS Animations whose'
+ + ' animation-name is not none');
+}, 'getAnimations for CSS Animations with animation-name: none');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'missing 100s';
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 0,
+ 'getAnimations returns an empty sequence for an element'
+ + ' with animation-name: missing');
+
+ div.style.animation = 'anim1 100s, missing 100s';
+ animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ 'getAnimations returns Animations only for those CSS Animations whose'
+ + ' animation-name is found');
+}, 'getAnimations for CSS Animations with animation-name: missing');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s, notyet 100s';
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ 'getAnimations initally only returns Animations for CSS Animations whose'
+ + ' animation-name is found');
+
+ return animations[0].ready.then(waitForFrame).then(function() {
+ var keyframes = '@keyframes notyet { to { left: 100px; } }';
+ document.styleSheets[0].insertRule(keyframes, 0);
+ animations = div.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations includes Animation when @keyframes rule is added'
+ + ' later');
+ return waitForAllAnimations(animations);
+ }).then(function() {
+ assert_true(animations[0].startTime < animations[1].startTime,
+ 'Newly added animation has a later start time');
+ document.styleSheets[0].deleteRule(0);
+ });
+}, 'getAnimations for CSS Animations where the @keyframes rule is added'
+ + ' later');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s, anim1 100s';
+ assert_equals(div.getAnimations().length, 2,
+ 'getAnimations returns one Animation for each CSS animation-name'
+ + ' even if the names are duplicated');
+}, 'getAnimations for CSS Animations with duplicated animation-name');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'empty 100s';
+ assert_equals(div.getAnimations().length, 1,
+ 'getAnimations returns Animations for CSS animations with an'
+ + ' empty keyframes rule');
+}, 'getAnimations for CSS Animations with empty keyframes rule');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s 100s';
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ 'getAnimations returns animations for CSS animations whose'
+ + ' delay makes them start later');
+ return animations[0].ready.then(waitForFrame).then(function() {
+ assert_true(animations[0].startTime <= document.timeline.currentTime,
+ 'For CSS Animations in delay phase, the start time of the Animation is'
+ + ' not in the future');
+ });
+}, 'getAnimations for CSS animations in delay phase');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 0s 100s';
+ assert_equals(div.getAnimations().length, 1,
+ 'getAnimations returns animations for CSS animations whose'
+ + ' duration is zero');
+ div.remove();
+}, 'getAnimations for zero-duration CSS Animations');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s';
+ var originalAnimation = div.getAnimations()[0];
+
+ // Update pause state (an Animation change)
+ div.style.animationPlayState = 'paused';
+ var pendingAnimation = div.getAnimations()[0];
+ assert_equals(pendingAnimation.playState, 'pending',
+ 'animation\'s play state is updated');
+ assert_equals(originalAnimation, pendingAnimation,
+ 'getAnimations returns the same objects even when their'
+ + ' play state changes');
+
+ // Update duration (an Animation change)
+ div.style.animationDuration = '200s';
+ var extendedAnimation = div.getAnimations()[0];
+ // FIXME: Check extendedAnimation.effect.timing.duration has changed once the
+ // API is available
+ assert_equals(originalAnimation, extendedAnimation,
+ 'getAnimations returns the same objects even when their'
+ + ' duration changes');
+}, 'getAnimations returns objects with the same identity');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s';
+
+ assert_equals(div.getAnimations().length, 1,
+ 'getAnimations returns an animation before cancelling');
+
+ var animation = div.getAnimations()[0];
+
+ animation.cancel();
+ assert_equals(div.getAnimations().length, 0,
+ 'getAnimations does not return cancelled animations');
+
+ animation.play();
+ assert_equals(div.getAnimations().length, 1,
+ 'getAnimations returns cancelled animations that have been re-started');
+
+}, 'getAnimations for CSS Animations that are cancelled');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim2 100s';
+
+ return div.getAnimations()[0].ready.then(function() {
+ // Prepend to the list and test that even though anim1 was triggered
+ // *after* anim2, it should come first because it appears first
+ // in the animation-name property.
+ div.style.animation = 'anim1 100s, anim2 100s';
+ var anims = div.getAnimations();
+ assert_equals(anims[0].animationName, 'anim1',
+ 'animation order after prepending to list');
+ assert_equals(anims[1].animationName, 'anim2',
+ 'animation order after prepending to list');
+
+ // Normally calling cancel and play would this push anim1 to the top of
+ // the stack but it shouldn't for CSS animations that map an the
+ // animation-name property.
+ var anim1 = anims[0];
+ anim1.cancel();
+ anim1.play();
+ anims = div.getAnimations();
+ assert_equals(anims[0].animationName, 'anim1',
+ 'animation order after cancelling and restarting');
+ assert_equals(anims[1].animationName, 'anim2',
+ 'animation order after cancelling and restarting');
+ });
+}, 'getAnimations for CSS Animations follows animation-name order');
+
+test(function(t) {
+ addStyle(t, { '#target::after': 'animation: anim1 10s;',
+ '#target::before': 'animation: anim1 10s;' });
+ var target = addDiv(t, { 'id': 'target' });
+ target.style.animation = 'anim1 100s';
+
+ var animations = target.getAnimations({ subtree: false });
+ assert_equals(animations.length, 1,
+ 'Should find only the element');
+ assert_equals(animations[0].effect.target, target,
+ 'Effect target should be the element');
+}, 'Test AnimationFilter{ subtree: false } with single element');
+
+test(function(t) {
+ addStyle(t, { '#target::after': 'animation: anim1 10s;',
+ '#target::before': 'animation: anim1 10s;' });
+ var target = addDiv(t, { 'id': 'target' });
+ target.style.animation = 'anim1 100s';
+
+ var animations = target.getAnimations({ subtree: true });
+ assert_equals(animations.length, 3,
+ 'getAnimations({ subtree: true }) ' +
+ 'should return animations on pseudo-elements');
+ assert_equals(animations[0].effect.target, target,
+ 'The animation targeting the parent element ' +
+ 'should be returned first');
+ assert_equals(animations[1].effect.target.type, '::before',
+ 'The animation targeting the ::before pseudo-element ' +
+ 'should be returned second');
+ assert_equals(animations[2].effect.target.type, '::after',
+ 'The animation targeting the ::after pesudo-element ' +
+ 'should be returned last');
+}, 'Test AnimationFilter{ subtree: true } with single element');
+
+test(function(t) {
+ addStyle(t, { '#parent::after': 'animation: anim1 10s;',
+ '#parent::before': 'animation: anim1 10s;',
+ '#child::after': 'animation: anim1 10s;',
+ '#child::before': 'animation: anim1 10s;' });
+ var parent = addDiv(t, { 'id': 'parent' });
+ parent.style.animation = 'anim1 100s';
+ var child = addDiv(t, { 'id': 'child' });
+ child.style.animation = 'anim1 100s';
+ parent.appendChild(child);
+
+ var animations = parent.getAnimations({ subtree: false });
+ assert_equals(animations.length, 1,
+ 'Should find only the element even if it has a child');
+ assert_equals(animations[0].effect.target, parent,
+ 'Effect target shuld be the element');
+}, 'Test AnimationFilter{ subtree: false } with element that has a child');
+
+test(function(t) {
+ addStyle(t, { '#parent::after': 'animation: anim1 10s;',
+ '#parent::before': 'animation: anim1 10s;',
+ '#child::after': 'animation: anim1 10s;',
+ '#child::before': 'animation: anim1 10s;' });
+ var parent = addDiv(t, { 'id': 'parent' });
+ var child = addDiv(t, { 'id': 'child' });
+ parent.style.animation = 'anim1 100s';
+ child.style.animation = 'anim1 100s';
+ parent.appendChild(child);
+
+ var animations = parent.getAnimations({ subtree: true });
+ assert_equals(animations.length, 6,
+ 'Should find all elements, pesudo-elements that parent has');
+
+ assert_equals(animations[0].effect.target, parent,
+ 'The animation targeting the parent element ' +
+ 'should be returned first');
+ assert_equals(animations[1].effect.target.type, '::before',
+ 'The animation targeting the ::before pseudo-element ' +
+ 'should be returned second');
+ assert_equals(animations[1].effect.target.parentElement, parent,
+ 'This ::before element should be child of parent element');
+ assert_equals(animations[2].effect.target.type, '::after',
+ 'The animation targeting the ::after pesudo-element ' +
+ 'should be returned third');
+ assert_equals(animations[2].effect.target.parentElement, parent,
+ 'This ::after element should be child of parent element');
+
+ assert_equals(animations[3].effect.target, child,
+ 'The animation targeting the child element ' +
+ 'should be returned fourth');
+ assert_equals(animations[4].effect.target.type, '::before',
+ 'The animation targeting the ::before pseudo-element ' +
+ 'should be returned fifth');
+ assert_equals(animations[4].effect.target.parentElement, child,
+ 'This ::before element should be child of child element');
+ assert_equals(animations[5].effect.target.type, '::after',
+ 'The animation targeting the ::after pesudo-element ' +
+ 'should be returned last');
+ assert_equals(animations[5].effect.target.parentElement, child,
+ 'This ::after element should be child of child element');
+}, 'Test AnimationFilter{ subtree: true } with element that has a child');
+
+test(function(t) {
+ var parent = addDiv(t, { 'id': 'parent' });
+ var child1 = addDiv(t, { 'id': 'child1' });
+ var grandchild1 = addDiv(t, { 'id': 'grandchild1' });
+ var grandchild2 = addDiv(t, { 'id': 'grandchild2' });
+ var child2 = addDiv(t, { 'id': 'child2' });
+
+ parent.style.animation = 'anim1 100s';
+ child1.style.animation = 'anim1 100s';
+ grandchild1.style.animation = 'anim1 100s';
+ grandchild2.style.animation = 'anim1 100s';
+ child2.style.animation = 'anim1 100s';
+
+ parent.appendChild(child1);
+ child1.appendChild(grandchild1);
+ child1.appendChild(grandchild2);
+ parent.appendChild(child2);
+
+ var animations = parent.getAnimations({ subtree: true });
+ assert_equals(
+ parent.getAnimations({ subtree: true }).length, 5,
+ 'Should find all descendants of the element');
+
+ assert_equals(animations[0].effect.target, parent,
+ 'The animation targeting the parent element ' +
+ 'should be returned first');
+
+ assert_equals(animations[1].effect.target, child1,
+ 'The animation targeting the child1 element ' +
+ 'should be returned second');
+
+ assert_equals(animations[2].effect.target, grandchild1,
+ 'The animation targeting the grandchild1 element ' +
+ 'should be returned third');
+
+ assert_equals(animations[3].effect.target, grandchild2,
+ 'The animation targeting the grandchild2 element ' +
+ 'should be returned fourth');
+
+ assert_equals(animations[4].effect.target, child2,
+ 'The animation targeting the child2 element ' +
+ 'should be returned last');
+
+}, 'Test AnimationFilter{ subtree: true } with element that has many descendant');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html b/dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html
new file mode 100644
index 0000000000..15e2d23f18
--- /dev/null
+++ b/dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html
@@ -0,0 +1,672 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim-empty { }
+
+@keyframes anim-empty-frames {
+ from { }
+ to { }
+}
+
+@keyframes anim-only-timing {
+ from { animation-timing-function: linear; }
+ to { }
+}
+
+@keyframes anim-only-non-animatable {
+ from { animation-duration: 3s; }
+ to { animation-duration: 5s; }
+}
+
+@keyframes anim-simple {
+ from { color: black; }
+ to { color: white; }
+}
+
+@keyframes anim-simple-three {
+ from { color: black; }
+ 50% { color: blue; }
+ to { color: white; }
+}
+
+@keyframes anim-simple-timing {
+ from { color: black; animation-timing-function: linear; }
+ 50% { color: blue; animation-timing-function: ease-in-out; }
+ to { color: white; animation-timing-function: step-end; }
+}
+
+@keyframes anim-simple-timing-some {
+ from { color: black; animation-timing-function: linear; }
+ 50% { color: blue; }
+ to { color: white; }
+}
+
+@keyframes anim-simple-shorthand {
+ from { margin: 8px; }
+ to { margin: 16px; }
+}
+
+@keyframes anim-omit-to {
+ from { color: blue; }
+}
+
+@keyframes anim-omit-from {
+ to { color: blue; }
+}
+
+@keyframes anim-omit-from-to {
+ 50% { color: blue; }
+}
+
+@keyframes anim-partially-omit-to {
+ from { margin-top: 50px;
+ margin-bottom: 100px; }
+ to { margin-top: 150px !important; /* ignored */
+ margin-bottom: 200px; }
+}
+
+@keyframes anim-different-props {
+ from { color: black; margin-top: 8px; }
+ 25% { color: blue; }
+ 75% { margin-top: 12px; }
+ to { color: white; margin-top: 16px }
+}
+
+@keyframes anim-different-props-and-easing {
+ from { color: black; margin-top: 8px; animation-timing-function: linear; }
+ 25% { color: blue; animation-timing-function: step-end; }
+ 75% { margin-top: 12px; animation-timing-function: ease-in; }
+ to { color: white; margin-top: 16px }
+}
+
+@keyframes anim-merge-offset {
+ from { color: black; }
+ to { color: white; }
+ from { margin-top: 8px; }
+ to { margin-top: 16px; }
+}
+
+@keyframes anim-merge-offset-and-easing {
+ from { color: black; animation-timing-function: step-end; }
+ to { color: white; }
+ from { margin-top: 8px; animation-timing-function: linear; }
+ to { margin-top: 16px; }
+ from { font-size: 16px; animation-timing-function: step-end; }
+ to { font-size: 32px; }
+ from { padding-left: 2px; animation-timing-function: linear; }
+ to { padding-left: 4px; }
+}
+
+@keyframes anim-no-merge-equiv-easing {
+ from { margin-top: 0px; animation-timing-function: steps(1, end); }
+ from { margin-right: 0px; animation-timing-function: step-end; }
+ from { margin-bottom: 0px; animation-timing-function: steps(1); }
+ 50% { margin-top: 10px; animation-timing-function: step-end; }
+ 50% { margin-right: 10px; animation-timing-function: step-end; }
+ 50% { margin-bottom: 10px; animation-timing-function: step-end; }
+ to { margin-top: 20px; margin-right: 20px; margin-bottom: 20px; }
+}
+
+@keyframes anim-overriding {
+ from { padding-top: 50px }
+ 50%, from { padding-top: 30px } /* wins: 0% */
+ 75%, 85%, 50% { padding-top: 20px } /* wins: 75%, 50% */
+ 100%, 85% { padding-top: 70px } /* wins: 100% */
+ 85.1% { padding-top: 60px } /* wins: 85.1% */
+ 85% { padding-top: 30px } /* wins: 85% */
+}
+
+@keyframes anim-filter {
+ to { filter: blur(5px) sepia(60%) saturate(30%); }
+}
+
+@keyframes anim-text-shadow {
+ to { text-shadow: none; }
+}
+
+@keyframes anim-background-size {
+ to { background-size: 50%, 6px, contain }
+}
+
+:root {
+ --var-100px: 100px;
+}
+@keyframes anim-variables {
+ to { transform: translate(var(--var-100px), 0) }
+}
+@keyframes anim-variables-shorthand {
+ to { margin: var(--var-100px) }
+}
+</style>
+<body>
+<script>
+"use strict";
+
+function getKeyframes(e) {
+ return e.getAnimations()[0].effect.getKeyframes();
+}
+
+function assert_frames_equal(a, b, name) {
+ assert_equals(Object.keys(a).sort().toString(),
+ Object.keys(b).sort().toString(),
+ "properties on " + name);
+ for (var p in a) {
+ if (p === 'offset' || p === 'computedOffset') {
+ assert_approx_equals(a[p], b[p], 0.00001,
+ "value for '" + p + "' on " + name);
+ } else {
+ assert_equals(a[p], b[p], "value for '" + p + "' on " + name);
+ }
+ }
+}
+
+// animation-timing-function values to test with, where the value
+// is exactly the same as its serialization, sorted by the order
+// getKeyframes() will group frames with the same easing function
+// together (by nsTimingFunction::Compare).
+const kTimingFunctionValues = [
+ "ease",
+ "linear",
+ "ease-in",
+ "ease-out",
+ "ease-in-out",
+ "steps(1, start)",
+ "steps(2, start)",
+ "steps(1)",
+ "steps(2)",
+ "cubic-bezier(0, 0, 1, 1)",
+ "cubic-bezier(0, 0.25, 0.75, 1)"
+];
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-empty 100s';
+ assert_equals(getKeyframes(div).length, 0,
+ "number of frames with empty @keyframes");
+
+ div.style.animation = 'anim-empty-frames 100s';
+ assert_equals(getKeyframes(div).length, 0,
+ "number of frames when @keyframes has empty keyframes");
+
+ div.style.animation = 'anim-only-timing 100s';
+ assert_equals(getKeyframes(div).length, 0,
+ "number of frames when @keyframes only has keyframes with " +
+ "animation-timing-function");
+
+ div.style.animation = 'anim-only-non-animatable 100s';
+ assert_equals(getKeyframes(div).length, 0,
+ "number of frames when @keyframes only has frames with " +
+ "non-animatable properties");
+}, 'KeyframeEffectReadOnly.getKeyframes() returns no frames for various kinds'
+ + ' of empty enimations');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-simple 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ color: "rgb(0, 0, 0)" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+ + 'animation');
+
+test(function(t) {
+ kTimingFunctionValues.forEach(function(easing) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-simple-three 100s ' + easing;
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 3, "number of frames");
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_equals(frames[i].easing, easing,
+ "value for 'easing' on ComputedKeyframe #" + i);
+ }
+ });
+}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing'
+ + ' values, when the easing comes from animation-timing-function on the'
+ + ' element');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-simple-timing 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 3, "number of frames");
+ assert_equals(frames[0].easing, "linear",
+ "value of 'easing' on ComputedKeyframe #0");
+ assert_equals(frames[1].easing, "ease-in-out",
+ "value of 'easing' on ComputedKeyframe #1");
+ assert_equals(frames[2].easing, "steps(1)",
+ "value of 'easing' on ComputedKeyframe #2");
+}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing'
+ + ' values, when the easing is specified on each keyframe');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-simple-timing-some 100s step-start';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 3, "number of frames");
+ assert_equals(frames[0].easing, "linear",
+ "value of 'easing' on ComputedKeyframe #0");
+ assert_equals(frames[1].easing, "steps(1, start)",
+ "value of 'easing' on ComputedKeyframe #1");
+ assert_equals(frames[2].easing, "steps(1, start)",
+ "value of 'easing' on ComputedKeyframe #2");
+}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing'
+ + ' values, when the easing is specified on some keyframes');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-simple-shorthand 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ marginBottom: "8px", marginLeft: "8px",
+ marginRight: "8px", marginTop: "8px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ marginBottom: "16px", marginLeft: "16px",
+ marginRight: "16px", marginTop: "16px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+ + ' animation that specifies a single shorthand property');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-omit-to 100s';
+ div.style.color = 'white';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ color: "rgb(0, 0, 255)" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with a 0% keyframe and no 100% keyframe');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-omit-from 100s';
+ div.style.color = 'white';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ color: "rgb(255, 255, 255)" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(0, 0, 255)" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with a 100% keyframe and no 0% keyframe');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-omit-from-to 100s';
+ div.style.color = 'white';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 3, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ color: "rgb(255, 255, 255)" },
+ { offset: 0.5, computedOffset: 0.5, easing: "ease",
+ color: "rgb(0, 0, 255)" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with no 0% or 100% keyframe but with a 50% keyframe');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-partially-omit-to 100s';
+ div.style.marginTop = '250px';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ marginTop: '50px', marginBottom: '100px' },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ marginTop: '250px', marginBottom: '200px' },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with a partially complete 100% keyframe (because the ' +
+ '!important rule is ignored)');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-different-props 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 4, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ color: "rgb(0, 0, 0)", marginTop: "8px" },
+ { offset: 0.25, computedOffset: 0.25, easing: "ease",
+ color: "rgb(0, 0, 255)" },
+ { offset: 0.75, computedOffset: 0.75, easing: "ease",
+ marginTop: "12px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)", marginTop: "16px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with different properties on different keyframes, all ' +
+ 'with the same easing function');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-different-props-and-easing 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 4, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "linear",
+ color: "rgb(0, 0, 0)", marginTop: "8px" },
+ { offset: 0.25, computedOffset: 0.25, easing: "steps(1)",
+ color: "rgb(0, 0, 255)" },
+ { offset: 0.75, computedOffset: 0.75, easing: "ease-in",
+ marginTop: "12px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)", marginTop: "16px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with different properties on different keyframes, with ' +
+ 'a different easing function on each');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-merge-offset 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ color: "rgb(0, 0, 0)", marginTop: "8px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)", marginTop: "16px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with multiple keyframes for the same time, and all with ' +
+ 'the same easing function');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-merge-offset-and-easing 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 3, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "steps(1)",
+ color: "rgb(0, 0, 0)", fontSize: "16px" },
+ { offset: 0, computedOffset: 0, easing: "linear",
+ marginTop: "8px", paddingLeft: "2px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)", fontSize: "32px", marginTop: "16px",
+ paddingLeft: "4px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with multiple keyframes for the same time and with ' +
+ 'different easing functions');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-no-merge-equiv-easing 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 3, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "steps(1)",
+ marginTop: "0px", marginRight: "0px", marginBottom: "0px" },
+ { offset: 0.5, computedOffset: 0.5, easing: "steps(1)",
+ marginTop: "10px", marginRight: "10px", marginBottom: "10px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ marginTop: "20px", marginRight: "20px", marginBottom: "20px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with multiple keyframes for the same time and with ' +
+ 'different but equivalent easing functions');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-overriding 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 6, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ paddingTop: "30px" },
+ { offset: 0.5, computedOffset: 0.5, easing: "ease",
+ paddingTop: "20px" },
+ { offset: 0.75, computedOffset: 0.75, easing: "ease",
+ paddingTop: "20px" },
+ { offset: 0.85, computedOffset: 0.85, easing: "ease",
+ paddingTop: "30px" },
+ { offset: 0.851, computedOffset: 0.851, easing: "ease",
+ paddingTop: "60px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ paddingTop: "70px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for ' +
+ 'overlapping keyframes');
+
+// Gecko-specific test case: We are specifically concerned here that the
+// computed value for filter, "none", is correctly represented.
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-filter 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ filter: "none" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ filter: "blur(5px) sepia(60%) saturate(30%)" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+ 'animations with filter properties and missing keyframes');
+
+// Gecko-specific test case: We are specifically concerned here that the
+// computed value for text-shadow and a "none" specified on a keyframe
+// are correctly represented.
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.textShadow = '1px 1px 2px black, 0 0 16px blue, 0 0 3.2px blue';
+ div.style.animation = 'anim-text-shadow 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ textShadow: "1px 1px 2px 0px rgb(0, 0, 0),"
+ + " 0px 0px 16px 0px rgb(0, 0, 255),"
+ + " 0px 0px 3.2px 0px rgb(0, 0, 255)" },
+ { offset: 1, computedOffset: 1, easing: "ease", textShadow: "none" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+ 'animations with text-shadow properties and missing keyframes');
+
+// Gecko-specific test case: We are specifically concerned here that the
+// initial value for background-size and the specified list are correctly
+// represented.
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-background-size 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ backgroundSize: "auto auto" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ backgroundSize: "50% auto, 6px auto, contain" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+
+ // Test inheriting a background-size value
+
+ expected[0].backgroundSize = div.style.backgroundSize =
+ "30px auto, 40% auto, auto auto";
+ frames = getKeyframes(div);
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i
+ + " after updating current style");
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+ 'animations with background-size properties and missing keyframes');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim-variables 100s';
+
+ var frames = getKeyframes(div);
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ transform: "none" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ transform: "translate(100px, 0px)" },
+ ];
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+ 'animations with CSS variables as keyframe values');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim-variables-shorthand 100s';
+
+ var frames = getKeyframes(div);
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ marginBottom: "0px",
+ marginLeft: "0px",
+ marginRight: "0px",
+ marginTop: "0px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ marginBottom: "100px",
+ marginLeft: "100px",
+ marginRight: "100px",
+ marginTop: "100px" },
+ ];
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+ 'animations with CSS variables as keyframe values in a shorthand property');
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_pseudoElement-get-animations.html b/dom/animation/test/css-animations/file_pseudoElement-get-animations.html
new file mode 100644
index 0000000000..bebe145334
--- /dev/null
+++ b/dom/animation/test/css-animations/file_pseudoElement-get-animations.html
@@ -0,0 +1,70 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim1 { }
+@keyframes anim2 { }
+.before::before {
+ animation: anim1 10s;
+}
+.after-with-mix-anims-trans::after {
+ content: '';
+ animation: anim1 10s, anim2 10s;
+ width: 0px;
+ height: 0px;
+ transition: all 100s;
+}
+.after-change::after {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t, { class: 'before' });
+ var pseudoTarget = document.getAnimations()[0].effect.target;
+ assert_equals(pseudoTarget.getAnimations().length, 1,
+ 'Expected number of animations are returned');
+ assert_equals(pseudoTarget.getAnimations()[0].animationName, 'anim1',
+ 'CSS animation name matches');
+}, 'getAnimations returns CSSAnimation objects');
+
+test(function(t) {
+ var div = addDiv(t, { class: 'after-with-mix-anims-trans' });
+ // Trigger transitions
+ flushComputedStyle(div);
+ div.classList.add('after-change');
+
+ // Create additional animation on the pseudo-element from script
+ var pseudoTarget = document.getAnimations()[0].effect.target;
+ var effect = new KeyframeEffectReadOnly(pseudoTarget,
+ { background: ["blue", "red"] },
+ 3 * MS_PER_SEC);
+ var newAnimation = new Animation(effect, document.timeline);
+ newAnimation.id = 'scripted-anim';
+ newAnimation.play();
+
+ // Check order - the script-generated animation should appear later
+ var anims = pseudoTarget.getAnimations();
+ assert_equals(anims.length, 5,
+ 'Got expected number of animations/trnasitions running on ' +
+ '::after pseudo element');
+ assert_equals(anims[0].transitionProperty, 'height',
+ '1st animation is the 1st transition sorted by name');
+ assert_equals(anims[1].transitionProperty, 'width',
+ '2nd animation is the 2nd transition sorted by name ');
+ assert_equals(anims[2].animationName, 'anim1',
+ '3rd animation is the 1st animation in animation-name list');
+ assert_equals(anims[3].animationName, 'anim2',
+ '4rd animation is the 2nd animation in animation-name list');
+ assert_equals(anims[4].id, 'scripted-anim',
+ 'Animation added by script appears last');
+}, 'getAnimations returns css transitions/animations, and script-generated ' +
+ 'animations in the expected order');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/test_animation-cancel.html b/dom/animation/test/css-animations/test_animation-cancel.html
new file mode 100644
index 0000000000..15c9b482fe
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-cancel.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-cancel.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-computed-timing.html b/dom/animation/test/css-animations/test_animation-computed-timing.html
new file mode 100644
index 0000000000..c1b40aaf36
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-computed-timing.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<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-computed-timing.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-currenttime.html b/dom/animation/test/css-animations/test_animation-currenttime.html
new file mode 100644
index 0000000000..7e3a8d74d6
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-currenttime.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-currenttime.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-finish.html b/dom/animation/test/css-animations/test_animation-finish.html
new file mode 100644
index 0000000000..abbd267d82
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-finish.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-finish.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-finished.html b/dom/animation/test/css-animations/test_animation-finished.html
new file mode 100644
index 0000000000..295ffe0af6
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-finished.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-finished.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-id.html b/dom/animation/test/css-animations/test_animation-id.html
new file mode 100644
index 0000000000..c23501b8d6
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-id.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-id.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-pausing.html b/dom/animation/test/css-animations/test_animation-pausing.html
new file mode 100644
index 0000000000..10be1ddf02
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-pausing.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-pausing.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-playstate.html b/dom/animation/test/css-animations/test_animation-playstate.html
new file mode 100644
index 0000000000..54c8e1f104
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-playstate.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-playstate.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-ready.html b/dom/animation/test/css-animations/test_animation-ready.html
new file mode 100644
index 0000000000..445f751b46
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-ready.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-ready.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-reverse.html b/dom/animation/test/css-animations/test_animation-reverse.html
new file mode 100644
index 0000000000..673b1e0d3a
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-reverse.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-reverse.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-starttime.html b/dom/animation/test/css-animations/test_animation-starttime.html
new file mode 100644
index 0000000000..dfae89ffa0
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-starttime.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-starttime.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animations-dynamic-changes.html b/dom/animation/test/css-animations/test_animations-dynamic-changes.html
new file mode 100644
index 0000000000..ce4eb378dc
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animations-dynamic-changes.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_animations-dynamic-changes.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_cssanimation-animationname.html b/dom/animation/test/css-animations/test_cssanimation-animationname.html
new file mode 100644
index 0000000000..ccddecc338
--- /dev/null
+++ b/dom/animation/test/css-animations/test_cssanimation-animationname.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_cssanimation-animationname.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_document-get-animations.html b/dom/animation/test/css-animations/test_document-get-animations.html
new file mode 100644
index 0000000000..dc964e62c8
--- /dev/null
+++ b/dom/animation/test/css-animations/test_document-get-animations.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_document-get-animations.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_effect-target.html b/dom/animation/test/css-animations/test_effect-target.html
new file mode 100644
index 0000000000..6c230c729f
--- /dev/null
+++ b/dom/animation/test/css-animations/test_effect-target.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_effect-target.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_element-get-animations.html b/dom/animation/test/css-animations/test_element-get-animations.html
new file mode 100644
index 0000000000..7b39e65ccb
--- /dev/null
+++ b/dom/animation/test/css-animations/test_element-get-animations.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_element-get-animations.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html b/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html
new file mode 100644
index 0000000000..3cf2270084
--- /dev/null
+++ b/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.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_keyframeeffect-getkeyframes.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_pseudoElement-get-animations.html b/dom/animation/test/css-animations/test_pseudoElement-get-animations.html
new file mode 100644
index 0000000000..1e0dc5c825
--- /dev/null
+++ b/dom/animation/test/css-animations/test_pseudoElement-get-animations.html
@@ -0,0 +1,14 @@
+<!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_pseudoElement-get-animations.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/file_animation-cancel.html b/dom/animation/test/css-transitions/file_animation-cancel.html
new file mode 100644
index 0000000000..6094b383f3
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-cancel.html
@@ -0,0 +1,165 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+ flushComputedStyle(div);
+
+ var animation = div.getAnimations()[0];
+ return animation.ready.then(waitForFrame).then(function() {
+ assert_not_equals(getComputedStyle(div).marginLeft, '1000px',
+ 'transform style is animated before cancelling');
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, div.style.marginLeft,
+ 'transform style is no longer animated after cancelling');
+ });
+}, 'Animated style is cleared after cancelling a running CSS transition');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+ flushComputedStyle(div);
+
+ div.addEventListener('transitionend', function() {
+ assert_unreached('Got unexpected end event on cancelled transition');
+ });
+
+ var animation = div.getAnimations()[0];
+ return animation.ready.then(function() {
+ // Seek to just before the end then cancel
+ animation.currentTime = 99.9 * 1000;
+ animation.cancel();
+
+ // Then wait a couple of frames and check that no event was dispatched
+ return waitForAnimationFrames(2);
+ });
+}, 'Cancelled CSS transitions do not dispatch events');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+ flushComputedStyle(div);
+
+ var animation = div.getAnimations()[0];
+ return animation.ready.then(function() {
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '1000px',
+ 'margin-left style is not animated after cancelling');
+ animation.play();
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is animated after re-starting transition');
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.playState, 'running',
+ 'Transition succeeds in running after being re-started');
+ });
+}, 'After cancelling a transition, it can still be re-used');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+ flushComputedStyle(div);
+
+ var animation = div.getAnimations()[0];
+ return animation.ready.then(function() {
+ animation.finish();
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '1000px',
+ 'margin-left style is not animated after cancelling');
+ animation.play();
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is animated after re-starting transition');
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.playState, 'running',
+ 'Transition succeeds in running after being re-started');
+ });
+}, 'After cancelling a finished transition, it can still be re-used');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+ flushComputedStyle(div);
+
+ var animation = div.getAnimations()[0];
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '1000px',
+ 'margin-left style is not animated after cancelling');
+
+ // Trigger a change to a transition property and check that this
+ // doesn't cause the animation to become live again
+ div.style.transitionDuration = '200s';
+ flushComputedStyle(div);
+ assert_equals(getComputedStyle(div).marginLeft, '1000px',
+ 'margin-left style is still not animated after updating'
+ + ' transition-duration');
+ assert_equals(animation.playState, 'idle',
+ 'Transition is still idle after updating transition-duration');
+}, 'After cancelling a transition, updating transition properties doesn\'t make'
+ + ' it live again');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+ flushComputedStyle(div);
+
+ var animation = div.getAnimations()[0];
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'running');
+ div.style.display = 'none';
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(animation.playState, 'idle');
+ assert_equals(getComputedStyle(div).marginLeft, '1000px');
+ });
+}, 'Setting display:none on an element cancels its transitions');
+
+promise_test(function(t) {
+ var parentDiv = addDiv(t);
+ var childDiv = document.createElement('div');
+ parentDiv.appendChild(childDiv);
+ childDiv.setAttribute('style', 'margin-left: 0px');
+
+ flushComputedStyle(childDiv);
+
+ childDiv.style.transition = 'margin-left 100s';
+ childDiv.style.marginLeft = '1000px';
+ flushComputedStyle(childDiv);
+
+ var animation = childDiv.getAnimations()[0];
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'running');
+ parentDiv.style.display = 'none';
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(animation.playState, 'idle');
+ assert_equals(getComputedStyle(childDiv).marginLeft, '1000px');
+ });
+}, 'Setting display:none cancels transitions on a child element');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_animation-computed-timing.html b/dom/animation/test/css-transitions/file_animation-computed-timing.html
new file mode 100644
index 0000000000..2dac82d753
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-computed-timing.html
@@ -0,0 +1,315 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+
+.animated-div {
+ margin-left: 100px;
+}
+
+</style>
+<body>
+<script>
+
+'use strict';
+
+// --------------------
+// delay
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().delay, 0,
+ 'Initial value of delay');
+}, 'delay of a new tranisition');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().delay, 10000,
+ 'Initial value of delay');
+}, 'Positive delay of a new transition');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s -5s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().delay, -5000,
+ 'Initial value of delay');
+}, 'Negative delay of a new transition');
+
+
+// --------------------
+// endDelay
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().endDelay, 0,
+ 'Initial value of endDelay');
+}, 'endDelay of a new transition');
+
+
+// --------------------
+// fill
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().fill, 'backwards',
+ 'Fill backwards');
+}, 'fill of a new transition');
+
+
+// --------------------
+// iterationStart
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().iterationStart, 0,
+ 'Initial value of iterationStart');
+}, 'iterationStart of a new transition');
+
+
+// --------------------
+// iterations
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().iterations, 1,
+ 'Initial value of iterations');
+}, 'iterations of a new transition');
+
+
+// --------------------
+// duration
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().duration, 10000,
+ 'Initial value of duration');
+}, 'duration of a new transition');
+
+
+// --------------------
+// direction
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().direction, 'normal',
+ 'Initial value of direction');
+}, 'direction of a new transition');
+
+
+// --------------------
+// easing
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().easing, 'linear',
+ 'Initial value of easing');
+}, 'easing of a new transition');
+
+
+// ------------------------------
+// endTime
+// = max(start delay + active duration + end delay, 0)
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 100s -5s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ var answer = 100000 - 5000; // ms
+ assert_equals(effect.getComputedTiming().endTime, answer,
+ 'Initial value of endTime');
+}, 'endTime of a new transition');
+
+
+// --------------------
+// activeDuration
+// = iteration duration * iteration count(==1)
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 100s -5s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().activeDuration, 100000,
+ 'Initial value of activeDuration');
+}, 'activeDuration of a new transition');
+
+
+// --------------------
+// localTime
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 100s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().localTime, 0,
+ 'Initial value of localTime');
+}, 'localTime of a new transition');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 100s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var anim = div.getAnimations()[0];
+ anim.currentTime = 5000;
+ assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
+ 'current localTime after setting currentTime');
+}, 'localTime is always equal to currentTime');
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 100s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var anim = div.getAnimations()[0];
+ anim.playbackRate = 2; // 2 times faster
+
+ anim.ready.then(t.step_func(function() {
+ assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
+ 'localTime is equal to currentTime');
+ return waitForFrame();
+ })).then(t.step_func_done(function() {
+ assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
+ 'localTime is equal to currentTime');
+ }));
+}, 'localTime reflects playbackRate immediately');
+
+
+// --------------------
+// progress
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10.5s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().progress, 0.0,
+ 'Initial value of progress');
+}, 'progress of a new transition');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10.5s 2s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().progress, 0.0,
+ 'Initial value of progress');
+}, 'progress of a new transition with positive delay in before phase');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10.5s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var anim = div.getAnimations()[0];
+ anim.finish()
+ assert_equals(anim.effect.getComputedTiming().progress, null,
+ 'finished progress');
+}, 'progress of a finished transition');
+
+
+// --------------------
+// currentIteration
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().currentIteration, 0,
+ 'Initial value of currentIteration');
+}, 'currentIteration of a new transition');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s 2s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().currentIteration, 0,
+ 'Initial value of currentIteration');
+}, 'currentIteration of a new transition with positive delay in before phase');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var anim = div.getAnimations()[0];
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, null,
+ 'finished currentIteration');
+}, 'currentIteration of a finished transition');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_animation-currenttime.html b/dom/animation/test/css-transitions/file_animation-currenttime.html
new file mode 100644
index 0000000000..2a0f105d20
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-currenttime.html
@@ -0,0 +1,307 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for the effect of setting a CSS transition's
+ Animation.currentTime</title>
+ <style>
+
+.animated-div {
+ margin-left: 100px;
+ transition: margin-left 1000s linear 1000s;
+}
+
+ </style>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+
+'use strict';
+
+// TODO: Once the computedTiming property is implemented, add checks to the
+// checker helpers to ensure that computedTiming's properties are updated as
+// expected.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
+
+
+const ANIM_DELAY_MS = 1000000; // 1000s
+const ANIM_DUR_MS = 1000000; // 1000s
+
+/**
+ * These helpers get the value that the currentTime needs to be set to, to put
+ * an animation that uses the above ANIM_DELAY_MS and ANIM_DUR_MS values into
+ * the middle of various phases or points through the active duration.
+ */
+function currentTimeForBeforePhase() {
+ return ANIM_DELAY_MS / 2;
+}
+function currentTimeForActivePhase() {
+ return ANIM_DELAY_MS + ANIM_DUR_MS / 2;
+}
+function currentTimeForAfterPhase() {
+ return ANIM_DELAY_MS + ANIM_DUR_MS + ANIM_DELAY_MS / 2;
+}
+function currentTimeForStartOfActiveInterval() {
+ return ANIM_DELAY_MS;
+}
+function currentTimeForFiftyPercentThroughActiveInterval() {
+ return ANIM_DELAY_MS + ANIM_DUR_MS * 0.5;
+}
+function currentTimeForEndOfActiveInterval() {
+ return ANIM_DELAY_MS + ANIM_DUR_MS;
+}
+
+
+// Expected computed 'margin-left' values at points during the active interval:
+// When we assert_between_inclusive using these values we could in theory cause
+// intermittent failure due to very long delays between paints, but since the
+// active duration is 1000s long, a delay would need to be around 100s to cause
+// that. If that's happening then there are likely other issues that should be
+// fixed, so a failure to make us look into that seems like a good thing.
+const INITIAL_POSITION = 100;
+const TEN_PCT_POSITION = 110;
+const FIFTY_PCT_POSITION = 150;
+const END_POSITION = 200;
+
+
+// The terms used for the naming of the following helper functions refer to
+// terms used in the Web Animations specification for specific phases of an
+// animation. The terms can be found here:
+//
+// http://w3c.github.io/web-animations/#animation-effect-phases-and-states
+
+// Called when currentTime is set to zero (the beginning of the start delay).
+function checkStateOnSettingCurrentTimeToZero(animation)
+{
+ // We don't test animation.currentTime since our caller just set it.
+
+ assert_equals(animation.playState, 'running',
+ 'Animation.playState should be "running" at the start of ' +
+ 'the start delay');
+
+ assert_equals(animation.effect.target.style.animationPlayState, 'running',
+ 'Animation.effect.target.style.animationPlayState should be ' +
+ '"running" at the start of the start delay');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, UNANIMATED_POSITION,
+ 'the computed value of margin-left should be unaffected ' +
+ 'at the beginning of the start delay');
+}
+
+// Called when the ready Promise's callbacks should happen
+function checkStateOnReadyPromiseResolved(animation)
+{
+ // the 0.0001 here is for rounding error
+ assert_less_than_equal(animation.currentTime,
+ animation.timeline.currentTime - animation.startTime + 0.0001,
+ 'Animation.currentTime should be less than the local time ' +
+ 'equivalent of the timeline\'s currentTime on the first paint tick ' +
+ 'after animation creation');
+
+ assert_equals(animation.playState, 'running',
+ 'Animation.playState should be "running" on the first paint ' +
+ 'tick after animation creation');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, INITIAL_POSITION,
+ 'the computed value of margin-left should be unaffected ' +
+ 'by an animation with a delay on ready Promise resolve');
+}
+
+// Called when currentTime is set to the time the active interval starts.
+function checkStateAtActiveIntervalStartTime(animation)
+{
+ // We don't test animation.currentTime since our caller just set it.
+
+ assert_equals(animation.playState, 'running',
+ 'Animation.playState should be "running" at the start of ' +
+ 'the active interval');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_between_inclusive(marginLeft, INITIAL_POSITION, TEN_PCT_POSITION,
+ 'the computed value of margin-left should be close to the value at the ' +
+ 'beginning of the animation');
+}
+
+function checkStateAtFiftyPctOfActiveInterval(animation)
+{
+ // We don't test animation.currentTime since our caller just set it.
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, FIFTY_PCT_POSITION,
+ 'the computed value of margin-left should be half way through the ' +
+ 'animation at the midpoint of the active interval');
+}
+
+// Called when currentTime is set to the time the active interval ends.
+function checkStateAtActiveIntervalEndTime(animation)
+{
+ // We don't test animation.currentTime since our caller just set it.
+
+ assert_equals(animation.playState, 'finished',
+ 'Animation.playState should be "finished" at the end of ' +
+ 'the active interval');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, END_POSITION,
+ 'the computed value of margin-left should be the final transitioned-to ' +
+ 'value at the end of the active duration');
+}
+
+test(function(t)
+{
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.currentTime, 0, 'currentTime should be zero');
+}, 'currentTime of a newly created transition is zero');
+
+
+test(function(t)
+{
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ // So that animation is running instead of paused when we set currentTime:
+ animation.startTime = animation.timeline.currentTime;
+
+ animation.currentTime = 10;
+ assert_equals(animation.currentTime, 10,
+ 'Check setting of currentTime actually works');
+}, 'Sanity test to check round-tripping assigning to new animation\'s ' +
+ 'currentTime');
+
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, 'transitionend');
+
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ animation.ready.then(t.step_func(function() {
+ checkStateOnReadyPromiseResolved(animation);
+
+ animation.currentTime = currentTimeForStartOfActiveInterval();
+ checkStateAtActiveIntervalStartTime(animation);
+
+ animation.currentTime = currentTimeForFiftyPercentThroughActiveInterval();
+ checkStateAtFiftyPctOfActiveInterval(animation);
+
+ animation.currentTime = currentTimeForEndOfActiveInterval();
+ return eventWatcher.wait_for('transitionend');
+ })).then(t.step_func(function() {
+ checkStateAtActiveIntervalEndTime(animation);
+ })).catch(t.step_func(function(reason) {
+ assert_unreached(reason);
+ })).then(function() {
+ t.done();
+ });
+}, 'Skipping forward through transition');
+
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, 'transitionend');
+
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ // Unlike in the case of CSS animations, we cannot skip to the end and skip
+ // backwards since when we reach the end the transition effect is removed and
+ // changes to the Animation object no longer affect the element. For
+ // this reason we only skip forwards as far as the 50% through point.
+
+ animation.ready.then(t.step_func(function() {
+ animation.currentTime = currentTimeForFiftyPercentThroughActiveInterval();
+ checkStateAtFiftyPctOfActiveInterval(animation);
+
+ animation.currentTime = currentTimeForStartOfActiveInterval();
+
+ // Despite going backwards from being in the active interval to being
+ // before it, we now expect a 'transitionend' event because the transition
+ // should go from being active to inactive.
+ //
+ // Calling checkStateAtActiveIntervalStartTime will check computed style,
+ // causing computed style to be updated and the 'transitionend' event to
+ // be dispatched synchronously. We need to call wait_for first
+ // otherwise eventWatcher will assert that the event was unexpected.
+ eventWatcher.wait_for('transitionend').then(function() {
+ t.done();
+ });
+ checkStateAtActiveIntervalStartTime(animation);
+ }));
+}, 'Skipping backwards through transition');
+
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ animation.ready.then(t.step_func(function() {
+ var exception;
+ try {
+ animation.currentTime = null;
+ } catch (e) {
+ exception = e;
+ }
+ assert_equals(exception.name, 'TypeError',
+ 'Expect TypeError exception on trying to set ' +
+ 'Animation.currentTime to null');
+ })).catch(t.step_func(function(reason) {
+ assert_unreached(reason);
+ })).then(function() {
+ t.done();
+ });
+}, 'Setting currentTime to null');
+
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+ var pauseTime;
+
+ animation.ready.then(t.step_func(function() {
+ assert_not_equals(animation.currentTime, null,
+ 'Animation.currentTime not null on ready Promise resolve');
+ animation.pause();
+ return animation.ready;
+ })).then(t.step_func(function() {
+ pauseTime = animation.currentTime;
+ return waitForFrame();
+ })).then(t.step_func(function() {
+ assert_equals(animation.currentTime, pauseTime,
+ 'Animation.currentTime is unchanged after pausing');
+ })).catch(t.step_func(function(reason) {
+ assert_unreached(reason);
+ })).then(function() {
+ t.done();
+ });
+}, 'Animation.currentTime after pausing');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/css-transitions/file_animation-finished.html b/dom/animation/test/css-transitions/file_animation-finished.html
new file mode 100644
index 0000000000..2f6bcf47d8
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-finished.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+
+.animated-div {
+ margin-left: 100px;
+ transition: margin-left 1000s linear 1000s;
+}
+
+</style>
+<body>
+<script>
+
+'use strict';
+
+const ANIM_DELAY_MS = 1000000; // 1000s
+const ANIM_DUR_MS = 1000000; // 1000s
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ animation.finish();
+
+ animation.finished.then(t.step_func(function() {
+ animation.play();
+ assert_equals(animation.currentTime, 0,
+ 'Replaying a finished transition should reset its ' +
+ 'currentTime');
+ t.done();
+ }));
+}, 'Test restarting a finished transition');
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ animation.ready.then(function() {
+ animation.playbackRate = -1;
+ return animation.finished;
+ }).then(t.step_func(function() {
+ animation.play();
+ // FIXME: once animation.effect.computedTiming.endTime is available (bug
+ // 1108055) we should use that here.
+ assert_equals(animation.currentTime, ANIM_DELAY_MS + ANIM_DUR_MS,
+ 'Replaying a finished reversed transition should reset ' +
+ 'its currentTime to the end of the effect');
+ t.done();
+ }));
+}, 'Test restarting a reversed finished transition');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_animation-pausing.html b/dom/animation/test/css-transitions/file_animation-pausing.html
new file mode 100644
index 0000000000..b2f2d46188
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-pausing.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+function getMarginLeft(cs) {
+ return parseFloat(cs.marginLeft);
+}
+
+async_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+
+ div.style.marginLeft = '0px';
+ cs.marginLeft; // Flush style to set up transition start point
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '10000px';
+ cs.marginLeft;
+
+ var animation = div.getAnimations()[0];
+ assert_equals(getMarginLeft(cs), 0,
+ 'Initial value of margin-left is zero');
+ var previousAnimVal = getMarginLeft(cs);
+
+ animation.ready.then(waitForFrame).then(t.step_func(function() {
+ assert_true(getMarginLeft(cs) > previousAnimVal,
+ 'margin-left is initially increasing');
+ animation.pause();
+ return animation.ready;
+ })).then(t.step_func(function() {
+ previousAnimVal = getMarginLeft(cs);
+ return waitForFrame();
+ })).then(t.step_func(function() {
+ assert_equals(getMarginLeft(cs), previousAnimVal,
+ 'margin-left does not increase after calling pause()');
+ previousAnimVal = getMarginLeft(cs);
+ animation.play();
+ return animation.ready.then(waitForFrame);
+ })).then(t.step_func(function() {
+ assert_true(getMarginLeft(cs) > previousAnimVal,
+ 'margin-left increases after calling play()');
+ t.done();
+ }));
+}, 'pause() and play() a transition');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_animation-ready.html b/dom/animation/test/css-transitions/file_animation-ready.html
new file mode 100644
index 0000000000..f141da7960
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-ready.html
@@ -0,0 +1,96 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+async_test(function(t) {
+ var div = addDiv(t);
+ div.style.transform = 'translate(0px)';
+ window.getComputedStyle(div).transform;
+ div.style.transition = 'transform 100s';
+ div.style.transform = 'translate(10px)';
+ window.getComputedStyle(div).transform;
+
+ var animation = div.getAnimations()[0];
+ var originalReadyPromise = animation.ready;
+
+ animation.ready.then(t.step_func(function() {
+ assert_equals(animation.ready, originalReadyPromise,
+ 'Ready promise is the same object when playing completes');
+ animation.pause();
+ assert_not_equals(animation.ready, originalReadyPromise,
+ 'Ready promise object identity differs when pausing');
+ t.done();
+ }));
+}, 'A new ready promise is created each time play() is called'
+ + ' the animation property');
+
+async_test(function(t) {
+ var div = addDiv(t);
+
+ // Set up pending transition
+ div.style.transform = 'translate(0px)';
+ window.getComputedStyle(div).transform;
+ div.style.transition = 'transform 100s';
+ div.style.transform = 'translate(10px)';
+ window.getComputedStyle(div).transform;
+
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.playState, 'pending', 'Animation is initially pending');
+
+ // Set up listeners on ready promise
+ animation.ready.then(t.step_func(function() {
+ assert_unreached('ready promise was fulfilled');
+ })).catch(t.step_func(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'ready promise is rejected with AbortError');
+ assert_equals(animation.playState, 'idle',
+ 'Animation is idle after transition was cancelled');
+ })).then(t.step_func(function() {
+ t.done();
+ }));
+
+ // Now remove transform from transition-property and flush styles
+ div.style.transitionProperty = 'none';
+ window.getComputedStyle(div).transitionProperty;
+
+}, 'ready promise is rejected when a transition is cancelled by updating'
+ + ' transition-property');
+
+async_test(function(t) {
+ var div = addDiv(t);
+
+ // Set up pending transition
+ div.style.marginLeft = '0px';
+ window.getComputedStyle(div).marginLeft;
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '100px';
+ window.getComputedStyle(div).marginLeft;
+
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.playState, 'pending', 'Animation is initially pending');
+
+ // Set up listeners on ready promise
+ animation.ready.then(t.step_func(function() {
+ assert_unreached('ready promise was fulfilled');
+ })).catch(t.step_func(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'ready promise is rejected with AbortError');
+ assert_equals(animation.playState, 'idle',
+ 'Animation is idle after transition was cancelled');
+ })).then(t.step_func(function() {
+ t.done();
+ }));
+
+ // Now update the transition to animate to something not-interpolable
+ div.style.marginLeft = 'auto';
+ window.getComputedStyle(div).marginLeft;
+
+}, 'ready promise is rejected when a transition is cancelled by changing'
+ + ' the transition property to something not interpolable');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_animation-starttime.html b/dom/animation/test/css-transitions/file_animation-starttime.html
new file mode 100644
index 0000000000..a156ba0a08
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-starttime.html
@@ -0,0 +1,284 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for the effect of setting a CSS transition's
+ Animation.startTime</title>
+ <style>
+
+.animated-div {
+ margin-left: 100px;
+ transition: margin-left 1000s linear 1000s;
+}
+
+ </style>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+
+'use strict';
+
+// TODO: Once the computedTiming property is implemented, add checks to the
+// checker helpers to ensure that computedTiming's properties are updated as
+// expected.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
+
+
+const ANIM_DELAY_MS = 1000000; // 1000s
+const ANIM_DUR_MS = 1000000; // 1000s
+
+/**
+ * These helpers get the value that the startTime needs to be set to, to put an
+ * animation that uses the above ANIM_DELAY_MS and ANIM_DUR_MS values into the
+ * middle of various phases or points through the active duration.
+ */
+function startTimeForBeforePhase(timeline) {
+ return timeline.currentTime - ANIM_DELAY_MS / 2;
+}
+function startTimeForActivePhase(timeline) {
+ return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS / 2;
+}
+function startTimeForAfterPhase(timeline) {
+ return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS - ANIM_DELAY_MS / 2;
+}
+function startTimeForStartOfActiveInterval(timeline) {
+ return timeline.currentTime - ANIM_DELAY_MS;
+}
+function startTimeForFiftyPercentThroughActiveInterval(timeline) {
+ return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS * 0.5;
+}
+function startTimeForEndOfActiveInterval(timeline) {
+ return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS;
+}
+
+
+// Expected computed 'margin-left' values at points during the active interval:
+// When we assert_between_inclusive using these values we could in theory cause
+// intermittent failure due to very long delays between paints, but since the
+// active duration is 1000s long, a delay would need to be around 100s to cause
+// that. If that's happening then there are likely other issues that should be
+// fixed, so a failure to make us look into that seems like a good thing.
+const INITIAL_POSITION = 100;
+const TEN_PCT_POSITION = 110;
+const FIFTY_PCT_POSITION = 150;
+const END_POSITION = 200;
+
+// The terms used for the naming of the following helper functions refer to
+// terms used in the Web Animations specification for specific phases of an
+// animation. The terms can be found here:
+//
+// https://w3c.github.io/web-animations/#animation-effect-phases-and-states
+//
+// Note the distinction between the "animation start time" which occurs before
+// the start delay and the start of the active interval which occurs after it.
+
+// Called when the ready Promise's callbacks should happen
+function checkStateOnReadyPromiseResolved(animation)
+{
+ assert_less_than_equal(animation.startTime, animation.timeline.currentTime,
+ 'Animation.startTime should be less than the timeline\'s ' +
+ 'currentTime on the first paint tick after animation creation');
+
+ assert_equals(animation.playState, 'running',
+ 'Animation.playState should be "running" on the first paint ' +
+ 'tick after animation creation');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, INITIAL_POSITION,
+ 'the computed value of margin-left should be unaffected ' +
+ 'by an animation with a delay on ready Promise resolve');
+}
+
+// Called when startTime is set to the time the active interval starts.
+function checkStateAtActiveIntervalStartTime(animation)
+{
+ // We don't test animation.startTime since our caller just set it.
+
+ assert_equals(animation.playState, 'running',
+ 'Animation.playState should be "running" at the start of ' +
+ 'the active interval');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_between_inclusive(marginLeft, INITIAL_POSITION, TEN_PCT_POSITION,
+ 'the computed value of margin-left should be close to the value at the ' +
+ 'beginning of the animation');
+}
+
+function checkStateAtFiftyPctOfActiveInterval(animation)
+{
+ // We don't test animation.startTime since our caller just set it.
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, FIFTY_PCT_POSITION,
+ 'the computed value of margin-left should be half way through the ' +
+ 'animation at the midpoint of the active interval');
+}
+
+// Called when startTime is set to the time the active interval ends.
+function checkStateAtActiveIntervalEndTime(animation)
+{
+ // We don't test animation.startTime since our caller just set it.
+
+ assert_equals(animation.playState, 'finished',
+ 'Animation.playState should be "finished" at the end of ' +
+ 'the active interval');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, END_POSITION,
+ 'the computed value of margin-left should be the final transitioned-to ' +
+ 'value at the end of the active duration');
+}
+
+test(function(t)
+{
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+}, 'startTime of a newly created transition is unresolved');
+
+
+test(function(t)
+{
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+ var currentTime = animation.timeline.currentTime;
+ animation.startTime = currentTime;
+ assert_approx_equals(animation.startTime, currentTime, 0.0001, // rounding error
+ 'Check setting of startTime actually works');
+}, 'Sanity test to check round-tripping assigning to new animation\'s ' +
+ 'startTime');
+
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, 'transitionend');
+
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ animation.ready.then(t.step_func(function() {
+ checkStateOnReadyPromiseResolved(animation);
+
+ animation.startTime = startTimeForStartOfActiveInterval(animation.timeline);
+ checkStateAtActiveIntervalStartTime(animation);
+
+ animation.startTime =
+ startTimeForFiftyPercentThroughActiveInterval(animation.timeline);
+ checkStateAtFiftyPctOfActiveInterval(animation);
+
+ animation.startTime = startTimeForEndOfActiveInterval(animation.timeline);
+ return eventWatcher.wait_for('transitionend');
+ })).then(t.step_func(function() {
+ checkStateAtActiveIntervalEndTime(animation);
+ })).catch(t.step_func(function(reason) {
+ assert_unreached(reason);
+ })).then(function() {
+ t.done();
+ });
+}, 'Skipping forward through animation');
+
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, 'transitionend');
+
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ // Unlike in the case of CSS animations, we cannot skip to the end and skip
+ // backwards since when we reach the end the transition effect is removed and
+ // changes to the Animation object no longer affect the element. For
+ // this reason we only skip forwards as far as the 90% through point.
+
+ animation.startTime =
+ startTimeForFiftyPercentThroughActiveInterval(animation.timeline);
+ checkStateAtFiftyPctOfActiveInterval(animation);
+
+ animation.startTime = startTimeForStartOfActiveInterval(animation.timeline);
+
+ // Despite going backwards from being in the active interval to being before
+ // it, we now expect an 'animationend' event because the animation should go
+ // from being active to inactive.
+ //
+ // Calling checkStateAtActiveIntervalStartTime will check computed style,
+ // causing computed style to be updated and the 'transitionend' event to
+ // be dispatched synchronously. We need to call waitForEvent first
+ // otherwise eventWatcher will assert that the event was unexpected.
+ eventWatcher.wait_for('transitionend').then(function() {
+ t.done();
+ });
+ checkStateAtActiveIntervalStartTime(animation);
+}, 'Skipping backwards through transition');
+
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ var storedCurrentTime;
+
+ animation.ready.then(t.step_func(function() {
+ storedCurrentTime = animation.currentTime;
+ animation.startTime = null;
+ return animation.ready;
+ })).catch(t.step_func(function(reason) {
+ assert_unreached(reason);
+ })).then(t.step_func(function() {
+ assert_equals(animation.currentTime, storedCurrentTime,
+ 'Test that hold time is correct');
+ t.done();
+ }));
+}, 'Setting startTime to null');
+
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ animation.ready.then(t.step_func(function() {
+ var savedStartTime = animation.startTime;
+
+ assert_not_equals(animation.startTime, null,
+ 'Animation.startTime not null on ready Promise resolve');
+
+ animation.pause();
+ return animation.ready;
+ })).then(t.step_func(function() {
+ assert_equals(animation.startTime, null,
+ 'Animation.startTime is null after paused');
+ assert_equals(animation.playState, 'paused',
+ 'Animation.playState is "paused" after pause() call');
+ })).catch(t.step_func(function(reason) {
+ assert_unreached(reason);
+ })).then(function() {
+ t.done();
+ });
+}, 'Animation.startTime after paused');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/css-transitions/file_csstransition-events.html b/dom/animation/test/css-transitions/file_csstransition-events.html
new file mode 100644
index 0000000000..5011bc1309
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_csstransition-events.html
@@ -0,0 +1,223 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Tests for CSS-Transition events</title>
+<link rel="help" href="https://drafts.csswg.org/css-transitions-2/#transition-events">
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+/**
+ * Helper class to record the elapsedTime member of each event.
+ * The EventWatcher class in testharness.js allows us to wait on
+ * multiple events in a certain order but only records the event
+ * parameters of the most recent event.
+ */
+function TransitionEventHandler(target) {
+ this.target = target;
+ this.target.ontransitionrun = function(evt) {
+ this.transitionrun = evt.elapsedTime;
+ }.bind(this);
+ this.target.ontransitionstart = function(evt) {
+ this.transitionstart = evt.elapsedTime;
+ }.bind(this);
+ this.target.ontransitionend = function(evt) {
+ this.transitionend = evt.elapsedTime;
+ }.bind(this);
+}
+
+TransitionEventHandler.prototype.clear = function() {
+ this.transitionrun = undefined;
+ this.transitionstart = undefined;
+ this.transitionend = undefined;
+};
+
+function setupTransition(t, transitionStyle) {
+ var div, watcher, handler, transition;
+ transitionStyle = transitionStyle || 'transition: margin-left 100s 100s';
+ div = addDiv(t, { style: transitionStyle });
+ watcher = new EventWatcher(t, div, [ 'transitionrun',
+ 'transitionstart',
+ 'transitionend' ]);
+ handler = new TransitionEventHandler(div);
+ flushComputedStyle(div);
+
+ div.style.marginLeft = '100px';
+ flushComputedStyle(div);
+
+ transition = div.getAnimations()[0];
+
+ return [transition, watcher, handler];
+}
+
+// On the next frame (i.e. when events are queued), whether or not the
+// transition is still pending depends on the implementation.
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ return watcher.wait_for('transitionrun').then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Idle -> Pending or Before');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Force the transition to leave the idle phase
+ transition.startTime = document.timeline.currentTime;
+ return watcher.wait_for('transitionrun').then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Idle -> Before');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Seek to Active phase.
+ transition.currentTime = 100 * MS_PER_SEC;
+ transition.pause();
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ assert_equals(handler.transitionrun, 0.0);
+ assert_equals(handler.transitionstart, 0.0);
+ });
+}, 'Idle or Pending -> Active');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Seek to After phase.
+ transition.finish();
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart',
+ 'transitionend' ]).then(function(evt) {
+ assert_equals(handler.transitionrun, 0.0);
+ assert_equals(handler.transitionstart, 0.0);
+ assert_equals(handler.transitionend, 100.0);
+ });
+}, 'Idle or Pending -> After');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+
+ return Promise.all([ watcher.wait_for('transitionrun'),
+ transition.ready ]).then(function() {
+ transition.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for('transitionstart');
+ }).then(function() {
+ assert_equals(handler.transitionstart, 0.0);
+ });
+}, 'Before -> Active');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ return Promise.all([ watcher.wait_for('transitionrun'),
+ transition.ready ]).then(function() {
+ // Seek to After phase.
+ transition.currentTime = 200 * MS_PER_SEC;
+ return watcher.wait_for([ 'transitionstart', 'transitionend' ]);
+ }).then(function(evt) {
+ assert_equals(handler.transitionstart, 0.0);
+ assert_equals(handler.transitionend, 100.0);
+ });
+}, 'Before -> After');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Seek to Active phase.
+ transition.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ // Seek to Before phase.
+ transition.currentTime = 0;
+ return watcher.wait_for('transitionend');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Active -> Before');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Seek to Active phase.
+ transition.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ // Seek to After phase.
+ transition.currentTime = 200 * MS_PER_SEC;
+ return watcher.wait_for('transitionend');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 100.0);
+ });
+}, 'Active -> After');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Seek to After phase.
+ transition.finish();
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart',
+ 'transitionend' ]).then(function(evt) {
+ // Seek to Before phase.
+ transition.currentTime = 0;
+ return watcher.wait_for([ 'transitionstart', 'transitionend' ]);
+ }).then(function(evt) {
+ assert_equals(handler.transitionstart, 100.0);
+ assert_equals(handler.transitionend, 0.0);
+ });
+}, 'After -> Before');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Seek to After phase.
+ transition.finish();
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart',
+ 'transitionend' ]).then(function(evt) {
+ // Seek to Active phase.
+ transition.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for('transitionstart');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 100.0);
+ });
+}, 'After -> Active');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] =
+ setupTransition(t, 'transition: margin-left 100s -50s');
+
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function() {
+ assert_equals(handler.transitionrun, 50.0);
+ assert_equals(handler.transitionstart, 50.0);
+ transition.finish();
+ return watcher.wait_for('transitionend');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 100.0);
+ });
+}, 'Calculating the interval start and end time with negative start delay.');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+
+ return watcher.wait_for('transitionrun').then(function(evt) {
+ // We can't set the end delay via generated effect timing.
+ // Because CSS-Transition use the AnimationEffectTimingReadOnly.
+ transition.effect = new KeyframeEffect(handler.target,
+ { marginleft: [ '0px', '100px' ]},
+ { duration: 100 * MS_PER_SEC,
+ endDelay: -50 * MS_PER_SEC });
+ // Seek to Before and play.
+ transition.cancel();
+ transition.play();
+ return watcher.wait_for('transitionstart');
+ }).then(function() {
+ assert_equals(handler.transitionstart, 0.0);
+
+ // Seek to After phase.
+ transition.finish();
+ return watcher.wait_for('transitionend');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 50.0);
+ });
+}, 'Calculating the interval start and end time with negative end delay.');
+
+done();
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/css-transitions/file_csstransition-transitionproperty.html b/dom/animation/test/css-transitions/file_csstransition-transitionproperty.html
new file mode 100644
index 0000000000..176cc5a4df
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_csstransition-transitionproperty.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+
+ // Add a transition
+ div.style.left = '0px';
+ window.getComputedStyle(div).transitionProperty;
+ div.style.transition = 'all 100s';
+ div.style.left = '100px';
+
+ assert_equals(div.getAnimations()[0].transitionProperty, 'left',
+ 'The transitionProperty for the corresponds to the specific ' +
+ 'property being transitioned');
+}, 'CSSTransition.transitionProperty');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_document-get-animations.html b/dom/animation/test/css-transitions/file_document-get-animations.html
new file mode 100644
index 0000000000..a5d55b76cd
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_document-get-animations.html
@@ -0,0 +1,93 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ assert_equals(document.getAnimations().length, 0,
+ 'getAnimations returns an empty sequence for a document'
+ + ' with no animations');
+}, 'getAnimations for non-animated content');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ // Add a couple of transitions
+ div.style.left = '0px';
+ div.style.top = '0px';
+ getComputedStyle(div).transitionProperty;
+
+ div.style.transition = 'all 100s';
+ div.style.left = '100px';
+ div.style.top = '100px';
+ assert_equals(document.getAnimations().length, 2,
+ 'getAnimations returns two running CSS Transitions');
+
+ // Remove both
+ div.style.transitionProperty = 'none';
+ assert_equals(document.getAnimations().length, 0,
+ 'getAnimations returns no running CSS Transitions');
+}, 'getAnimations for CSS Transitions');
+
+test(function(t) {
+ addStyle(t, { '.init::after': 'content: ""; width: 0px; ' +
+ 'transition: all 100s;',
+ '.init::before': 'content: ""; width: 0px; ' +
+ 'transition: all 10s;',
+ '.change::after': 'width: 100px;',
+ '.change::before': 'width: 100px;' });
+ // create two divs with these arrangement:
+ // parent
+ // ::before,
+ // ::after
+ // |
+ // child
+ var parent = addDiv(t);
+ var child = addDiv(t);
+ parent.appendChild(child);
+
+ parent.style.left = '0px';
+ parent.style.transition = 'left 10s';
+ parent.classList.add('init');
+ child.style.left = '0px';
+ child.style.transition = 'left 10s';
+ flushComputedStyle(parent);
+
+ parent.style.left = '100px';
+ parent.classList.add('change');
+ child.style.left = '100px';
+
+ var anims = document.getAnimations();
+ assert_equals(anims.length, 4,
+ 'CSS transition on both pseudo-elements and elements ' +
+ 'are returned');
+ assert_equals(anims[0].effect.target, parent,
+ 'The animation targeting the parent element comes first');
+ assert_equals(anims[1].effect.target.type, '::before',
+ 'The animation targeting the ::before element comes second');
+ assert_equals(anims[2].effect.target.type, '::after',
+ 'The animation targeting the ::after element comes third');
+ assert_equals(anims[3].effect.target, child,
+ 'The animation targeting the child element comes last');
+}, 'CSS Transitions targetting (pseudo-)elements should have correct order ' +
+ 'after sorting');
+
+async_test(function(t) {
+ var div = addDiv(t, { style: 'left: 0px; transition: all 50ms' });
+ flushComputedStyle(div);
+
+ div.style.left = '100px';
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1, 'Got transition');
+ animations[0].finished.then(t.step_func(function() {
+ assert_equals(document.getAnimations().length, 0,
+ 'No animations returned');
+ t.done();
+ }));
+}, 'Transitions are not returned after they have finished');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_effect-target.html b/dom/animation/test/css-transitions/file_effect-target.html
new file mode 100644
index 0000000000..0f67b0b9aa
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_effect-target.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.left = '0px';
+ window.getComputedStyle(div).transitionProperty;
+ div.style.transition = 'left 100s';
+ div.style.left = '100px';
+
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.effect.target, div,
+ 'Animation.target is the animatable div');
+}, 'Returned CSS transitions have the correct Animation.target');
+
+test(function(t) {
+ addStyle(t, { '.init::after': 'content: ""; width: 0px; height: 0px; ' +
+ 'transition: all 10s;',
+ '.change::after': 'width: 100px; height: 100px;' });
+ var div = addDiv(t, { class: 'init' });
+ flushComputedStyle(div);
+ div.classList.add('change');
+
+ var anims = document.getAnimations();
+ assert_equals(anims.length, 2,
+ 'Got transitions running on ::after pseudo element');
+ assert_equals(anims[0].effect.target, anims[1].effect.target,
+ 'Both transitions return the same target object');
+}, 'effect.target should return the same CSSPseudoElement object each time');
+
+test(function(t) {
+ addStyle(t, { '.init::after': 'content: ""; width: 0px; transition: all 10s;',
+ '.change::after': 'width: 100px;' });
+ var div = addDiv(t, { class: 'init' });
+ flushComputedStyle(div);
+ div.classList.add('change');
+ var pseudoTarget = document.getAnimations()[0].effect.target;
+ var effect = new KeyframeEffectReadOnly(pseudoTarget,
+ { background: ["blue", "red"] },
+ 3000);
+ var newAnim = new Animation(effect, document.timeline);
+ newAnim.play();
+
+ var anims = document.getAnimations();
+ assert_equals(anims.length, 2,
+ 'Got animations running on ::after pseudo element');
+ assert_not_equals(anims[0], newAnim,
+ 'The scriped-generated animation appears last');
+ assert_equals(newAnim.effect.target, pseudoTarget,
+ 'The effect.target of the scripted-generated animation is ' +
+ 'the same as the one from the argument of ' +
+ 'KeyframeEffectReadOnly constructor');
+ assert_equals(anims[0].effect.target, newAnim.effect.target,
+ 'Both the transition and the scripted-generated animation ' +
+ 'return the same target object');
+}, 'effect.target from the script-generated animation should return the same ' +
+ 'CSSPseudoElement object as that from the CSS generated transition');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_element-get-animations.html b/dom/animation/test/css-transitions/file_element-get-animations.html
new file mode 100644
index 0000000000..0ce145da04
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_element-get-animations.html
@@ -0,0 +1,147 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+async_test(function(t) {
+ var div = addDiv(t);
+
+ // FIXME: This test does too many things. It should be split up.
+
+ // Add a couple of transitions
+ div.style.left = '0px';
+ div.style.top = '0px';
+ window.getComputedStyle(div).transitionProperty;
+
+ div.style.transition = 'all 100s';
+ div.style.left = '100px';
+ div.style.top = '100px';
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations() returns one Animation per transitioning property');
+ waitForAllAnimations(animations).then(t.step_func(function() {
+ var startTime = animations[0].startTime;
+ assert_true(startTime > 0 && startTime <= document.timeline.currentTime,
+ 'CSS transitions have sensible start times');
+ assert_equals(animations[0].startTime, animations[1].startTime,
+ 'CSS transitions started together have the same start time');
+ // Wait a moment then add a third transition
+ return waitForFrame();
+ })).then(t.step_func(function() {
+ div.style.backgroundColor = 'green';
+ animations = div.getAnimations();
+ assert_equals(animations.length, 3,
+ 'getAnimations returns Animations for all running CSS Transitions');
+ return waitForAllAnimations(animations);
+ })).then(t.step_func(function() {
+ assert_less_than(animations[1].startTime, animations[2].startTime,
+ 'Animation for additional CSS transition starts after the original'
+ + ' transitions and appears later in the list');
+ t.done();
+ }));
+}, 'getAnimations for CSS Transitions');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'left: 0px; transition: all 100s' });
+
+ flushComputedStyle(div);
+ div.style.left = '100px';
+
+ assert_class_string(div.getAnimations()[0], 'CSSTransition',
+ 'Interface of returned animation is CSSTransition');
+}, 'getAnimations returns CSSTransition objects for CSS Transitions');
+
+async_test(function(t) {
+ var div = addDiv(t);
+
+ // Set up event listener
+ div.addEventListener('transitionend', t.step_func(function() {
+ assert_equals(div.getAnimations().length, 0,
+ 'getAnimations does not return finished CSS Transitions');
+ t.done();
+ }));
+
+ // Add a very short transition
+ div.style.left = '0px';
+ window.getComputedStyle(div).left;
+
+ div.style.transition = 'all 0.01s';
+ div.style.left = '100px';
+ window.getComputedStyle(div).left;
+}, 'getAnimations for CSS Transitions that have finished');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ // Try to transition non-animatable property animation-duration
+ div.style.animationDuration = '10s';
+ window.getComputedStyle(div).animationDuration;
+
+ div.style.transition = 'all 100s';
+ div.style.animationDuration = '100s';
+
+ assert_equals(div.getAnimations().length, 0,
+ 'getAnimations returns an empty sequence for a transition'
+ + ' of a non-animatable property');
+}, 'getAnimations for transition on non-animatable property');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.setProperty('-vendor-unsupported', '0px', '');
+ window.getComputedStyle(div).transitionProperty;
+ div.style.transition = 'all 100s';
+ div.style.setProperty('-vendor-unsupported', '100px', '');
+
+ assert_equals(div.getAnimations().length, 0,
+ 'getAnimations returns an empty sequence for a transition'
+ + ' of an unsupported property');
+}, 'getAnimations for transition on unsupported property');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'transform: translate(0px); ' +
+ 'opacity: 0; ' +
+ 'border-width: 0px; ' + // Shorthand
+ 'border-style: solid' });
+ getComputedStyle(div).transform;
+
+ div.style.transition = 'all 100s';
+ div.style.transform = 'translate(100px)';
+ div.style.opacity = '1';
+ div.style.borderWidth = '1px';
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 6,
+ 'Generated expected number of transitions');
+ assert_equals(animations[0].transitionProperty, 'border-bottom-width');
+ assert_equals(animations[1].transitionProperty, 'border-left-width');
+ assert_equals(animations[2].transitionProperty, 'border-right-width');
+ assert_equals(animations[3].transitionProperty, 'border-top-width');
+ assert_equals(animations[4].transitionProperty, 'opacity');
+ assert_equals(animations[5].transitionProperty, 'transform');
+}, 'getAnimations sorts simultaneous transitions by name');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'transform: translate(0px); ' +
+ 'opacity: 0' });
+ getComputedStyle(div).transform;
+
+ div.style.transition = 'all 100s';
+ div.style.transform = 'translate(100px)';
+ assert_equals(div.getAnimations().length, 1,
+ 'Initially there is only one (transform) transition');
+ div.style.opacity = '1';
+ assert_equals(div.getAnimations().length, 2,
+ 'Then a second (opacity) transition is added');
+
+ var animations = div.getAnimations();
+ assert_equals(animations[0].transitionProperty, 'transform');
+ assert_equals(animations[1].transitionProperty, 'opacity');
+}, 'getAnimations sorts transitions by when they were generated');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html b/dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html
new file mode 100644
index 0000000000..7bbf76fa7a
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html
@@ -0,0 +1,95 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+:root {
+ --var-100px: 100px;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+function getKeyframes(e) {
+ return e.getAnimations()[0].effect.getKeyframes();
+}
+
+function assert_frames_equal(a, b, name) {
+ assert_equals(Object.keys(a).sort().toString(),
+ Object.keys(b).sort().toString(),
+ "properties on " + name);
+ for (var p in a) {
+ assert_equals(a[p], b[p], "value for '" + p + "' on " + name);
+ }
+}
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.left = '0px';
+ window.getComputedStyle(div).transitionProperty;
+ div.style.transition = 'left 100s';
+ div.style.left = '100px';
+
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease", left: "0px" },
+ { offset: 1, computedOffset: 1, easing: "linear", left: "100px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+ + ' transition');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.left = '0px';
+ window.getComputedStyle(div).transitionProperty;
+ div.style.transition = 'left 100s steps(2,end)';
+ div.style.left = '100px';
+
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "steps(2)", left: "0px" },
+ { offset: 1, computedOffset: 1, easing: "linear", left: "100px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+ + ' transition with a non-default easing function');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.left = '0px';
+ window.getComputedStyle(div).transitionProperty;
+ div.style.transition = 'left 100s';
+ div.style.left = 'var(--var-100px)';
+
+ var frames = getKeyframes(div);
+
+ // CSS transition endpoints are based on the computed value so we
+ // shouldn't see the variable reference
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: 'ease', left: '0px' },
+ { offset: 1, computedOffset: 1, easing: 'linear', left: '100px' },
+ ];
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a'
+ + ' transition with a CSS variable endpoint');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_pseudoElement-get-animations.html b/dom/animation/test/css-transitions/file_pseudoElement-get-animations.html
new file mode 100644
index 0000000000..5683a14a14
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_pseudoElement-get-animations.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+.init::before {
+ content: '';
+ height: 0px;
+ width: 0px;
+ opacity: 0;
+ transition: all 100s;
+}
+.change::before {
+ height: 100px;
+ width: 100px;
+ opacity: 1;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t, { class: 'init' });
+ flushComputedStyle(div);
+ div.classList.add('change');
+
+ // Sanity checks
+ assert_equals(document.getAnimations().length, 3,
+ 'Got expected number of animations on document');
+ var pseudoTarget = document.getAnimations()[0].effect.target;
+ assert_class_string(pseudoTarget, 'CSSPseudoElement',
+ 'Got pseudo-element target');
+
+ // Check animations returned from the pseudo element are in correct order
+ var anims = pseudoTarget.getAnimations();
+ assert_equals(anims.length, 3,
+ 'Got expected number of animations on pseudo-element');
+ assert_equals(anims[0].transitionProperty, 'height');
+ assert_equals(anims[1].transitionProperty, 'opacity');
+ assert_equals(anims[2].transitionProperty, 'width');
+}, 'getAnimations sorts simultaneous transitions by name');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_setting-effect.html b/dom/animation/test/css-transitions/file_setting-effect.html
new file mode 100644
index 0000000000..c61877194e
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_setting-effect.html
@@ -0,0 +1,91 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='../testcommon.js'></script>
+<body>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.left = '0px';
+
+ div.style.transition = 'left 100s';
+ flushComputedStyle(div);
+ div.style.left = '100px';
+
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ transition.currentTime = 50 * MS_PER_SEC;
+ transition.effect = null;
+ assert_equals(transition.transitionProperty, 'left');
+ assert_equals(transition.playState, 'finished');
+ assert_equals(window.getComputedStyle(div).left, '100px');
+ });
+}, 'Test for removing a transition effect');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.left = '0px';
+
+ div.style.transition = 'left 100s';
+ flushComputedStyle(div);
+ div.style.left = '100px';
+
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ transition.currentTime = 50 * MS_PER_SEC;
+ transition.effect = new KeyframeEffect(div,
+ { marginLeft: [ '0px' , '100px'] },
+ 100 * MS_PER_SEC);
+ assert_equals(transition.transitionProperty, 'left');
+ assert_equals(transition.playState, 'running');
+ assert_equals(window.getComputedStyle(div).left, '100px');
+ assert_equals(window.getComputedStyle(div).marginLeft, '50px');
+ });
+}, 'Test for replacing the transition effect by a new keyframe effect');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.left = '0px';
+ div.style.width = '0px';
+
+ div.style.transition = 'left 100s';
+ flushComputedStyle(div);
+ div.style.left = '100px';
+
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ transition.currentTime = 50 * MS_PER_SEC;
+ transition.effect = new KeyframeEffect(div,
+ { marginLeft: [ '0px' , '100px'] },
+ 20 * MS_PER_SEC);
+ assert_equals(transition.playState, 'finished');
+ });
+}, 'Test for setting a new keyframe effect with a shorter duration');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.left = '0px';
+ div.style.width = '0px';
+
+ div.style.transition = 'left 100s';
+ flushComputedStyle(div);
+ div.style.left = '100px';
+
+ var transition = div.getAnimations()[0];
+ assert_equals(transition.playState, 'pending');
+
+ transition.effect = new KeyframeEffect(div,
+ { marginLeft: [ '0px' , '100px'] },
+ 100 * MS_PER_SEC);
+ assert_equals(transition.transitionProperty, 'left');
+ assert_equals(transition.playState, 'pending');
+
+ return transition.ready.then(function() {
+ assert_equals(transition.playState, 'running');
+ });
+}, 'Test for setting a new keyframe effect to a pending transition');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/test_animation-cancel.html b/dom/animation/test/css-transitions/test_animation-cancel.html
new file mode 100644
index 0000000000..949e0843e4
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-cancel.html
@@ -0,0 +1,14 @@
+<!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-cancel.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_animation-computed-timing.html b/dom/animation/test/css-transitions/test_animation-computed-timing.html
new file mode 100644
index 0000000000..c1b40aaf36
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-computed-timing.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<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-computed-timing.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-transitions/test_animation-currenttime.html b/dom/animation/test/css-transitions/test_animation-currenttime.html
new file mode 100644
index 0000000000..30b0ed030c
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-currenttime.html
@@ -0,0 +1,14 @@
+<!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-currenttime.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_animation-finished.html b/dom/animation/test/css-transitions/test_animation-finished.html
new file mode 100644
index 0000000000..f2ed7f80b4
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-finished.html
@@ -0,0 +1,14 @@
+<!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-finished.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_animation-pausing.html b/dom/animation/test/css-transitions/test_animation-pausing.html
new file mode 100644
index 0000000000..67484a2a53
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-pausing.html
@@ -0,0 +1,14 @@
+<!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-pausing.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_animation-ready.html b/dom/animation/test/css-transitions/test_animation-ready.html
new file mode 100644
index 0000000000..a928ded641
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-ready.html
@@ -0,0 +1,14 @@
+<!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-ready.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_animation-starttime.html b/dom/animation/test/css-transitions/test_animation-starttime.html
new file mode 100644
index 0000000000..8a8c85f2dd
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-starttime.html
@@ -0,0 +1,14 @@
+<!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-starttime.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_csstransition-events.html b/dom/animation/test/css-transitions/test_csstransition-events.html
new file mode 100644
index 0000000000..92559ad679
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_csstransition-events.html
@@ -0,0 +1,14 @@
+<!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_csstransition-events.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_csstransition-transitionproperty.html b/dom/animation/test/css-transitions/test_csstransition-transitionproperty.html
new file mode 100644
index 0000000000..0aa1912d96
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_csstransition-transitionproperty.html
@@ -0,0 +1,14 @@
+<!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_csstransition-transitionproperty.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_document-get-animations.html b/dom/animation/test/css-transitions/test_document-get-animations.html
new file mode 100644
index 0000000000..dc964e62c8
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_document-get-animations.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_document-get-animations.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-transitions/test_effect-target.html b/dom/animation/test/css-transitions/test_effect-target.html
new file mode 100644
index 0000000000..f3ae72229c
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_effect-target.html
@@ -0,0 +1,14 @@
+<!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_effect-target.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_element-get-animations.html b/dom/animation/test/css-transitions/test_element-get-animations.html
new file mode 100644
index 0000000000..87abdfa730
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_element-get-animations.html
@@ -0,0 +1,14 @@
+<!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_element-get-animations.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html b/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html
new file mode 100644
index 0000000000..dcc54255d5
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html
@@ -0,0 +1,14 @@
+<!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_keyframeeffect-getkeyframes.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_pseudoElement-get-animations.html b/dom/animation/test/css-transitions/test_pseudoElement-get-animations.html
new file mode 100644
index 0000000000..1e0dc5c825
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_pseudoElement-get-animations.html
@@ -0,0 +1,14 @@
+<!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_pseudoElement-get-animations.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_setting-effect.html b/dom/animation/test/css-transitions/test_setting-effect.html
new file mode 100644
index 0000000000..a9654ec556
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_setting-effect.html
@@ -0,0 +1,14 @@
+<!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_setting-effect.html');
+ });
+</script>
diff --git a/dom/animation/test/document-timeline/file_document-timeline.html b/dom/animation/test/document-timeline/file_document-timeline.html
new file mode 100644
index 0000000000..7ec48e2915
--- /dev/null
+++ b/dom/animation/test/document-timeline/file_document-timeline.html
@@ -0,0 +1,135 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Web Animations API: DocumentTimeline tests</title>
+<script src="../testcommon.js"></script>
+<iframe src="data:text/html;charset=utf-8," width="10" height="10" id="iframe"></iframe>
+<iframe src="data:text/html;charset=utf-8,%3Chtml%20style%3D%22display%3Anone%22%3E%3C%2Fhtml%3E" width="10" height="10" id="hidden-iframe"></iframe>
+<script>
+'use strict';
+
+test(function() {
+ assert_equals(document.timeline, document.timeline,
+ 'document.timeline returns the same object every time');
+ var iframe = document.getElementById('iframe');
+ assert_not_equals(document.timeline, iframe.contentDocument.timeline,
+ 'document.timeline returns a different object for each document');
+ assert_not_equals(iframe.contentDocument.timeline, null,
+ 'document.timeline on an iframe is not null');
+},
+'document.timeline identity tests',
+{
+ help: 'http://dev.w3.org/fxtf/web-animations/#the-document-timeline',
+ assert: [ 'Each document has a timeline called the document timeline' ],
+ author: 'Brian Birtles'
+});
+
+async_test(function(t) {
+ assert_true(document.timeline.currentTime > 0,
+ 'document.timeline.currentTime is positive');
+ // document.timeline.currentTime should be set even before document
+ // load fires. We expect this code to be run before document load and hence
+ // the above assertion is sufficient.
+ // If the following assertion fails, this test needs to be redesigned.
+ assert_true(document.readyState !== 'complete',
+ 'Test is running prior to document load');
+
+ // Test that the document timeline's current time is measured from
+ // navigationStart.
+ //
+ // We can't just compare document.timeline.currentTime to
+ // window.performance.now() because currentTime is only updated on a sample
+ // so we use requestAnimationFrame instead.
+ window.requestAnimationFrame(t.step_func(function(rafTime) {
+ assert_equals(document.timeline.currentTime, rafTime,
+ 'document.timeline.currentTime matches' +
+ ' requestAnimationFrame time');
+ t.done();
+ }));
+},
+'document.timeline.currentTime value tests',
+{
+ help: [
+ 'http://dev.w3.org/fxtf/web-animations/#the-global-clock',
+ 'http://dev.w3.org/fxtf/web-animations/#the-document-timeline'
+ ],
+ assert: [
+ 'The global clock is a source of monotonically increasing time values',
+ 'The time values of the document timeline are calculated as a fixed' +
+ ' offset from the global clock',
+ 'the zero time corresponds to the navigationStart moment',
+ 'the time value of each document timeline must be equal to the time ' +
+ 'passed to animation frame request callbacks for that browsing context'
+ ],
+ author: 'Brian Birtles'
+});
+
+async_test(function(t) {
+ var valueAtStart = document.timeline.currentTime;
+ var timeAtStart = window.performance.now();
+ while (window.performance.now() - timeAtStart < 100) {
+ // Wait 100ms
+ }
+ assert_equals(document.timeline.currentTime, valueAtStart,
+ 'document.timeline.currentTime does not change within a script block');
+ window.requestAnimationFrame(t.step_func(function() {
+ assert_true(document.timeline.currentTime > valueAtStart,
+ 'document.timeline.currentTime increases between script blocks');
+ t.done();
+ }));
+},
+'document.timeline.currentTime liveness tests',
+{
+ help: 'http://dev.w3.org/fxtf/web-animations/#script-execution-and-live-updates-to-the-model',
+ assert: [ 'The value returned by the currentTime attribute of a' +
+ ' document timeline will not change within a script block' ],
+ author: 'Brian Birtles'
+});
+
+test(function() {
+ var hiddenIFrame = document.getElementById('hidden-iframe');
+ assert_equals(typeof hiddenIFrame.contentDocument.timeline.currentTime,
+ 'number',
+ 'currentTime of an initially hidden subframe\'s timeline is a number');
+ assert_true(hiddenIFrame.contentDocument.timeline.currentTime >= 0,
+ 'currentTime of an initially hidden subframe\'s timeline is >= 0');
+}, 'document.timeline.currentTime hidden subframe test');
+
+async_test(function(t) {
+ var hiddenIFrame = document.getElementById('hidden-iframe');
+
+ // Don't run the test until after the iframe has completed loading or else the
+ // contentDocument may change.
+ var testToRunOnLoad = t.step_func(function() {
+ // Remove display:none
+ hiddenIFrame.style.display = 'block';
+ window.getComputedStyle(hiddenIFrame).display;
+
+ window.requestAnimationFrame(t.step_func(function() {
+ assert_true(hiddenIFrame.contentDocument.timeline.currentTime > 0,
+ 'document.timeline.currentTime is positive after removing'
+ + ' display:none');
+ var previousValue = hiddenIFrame.contentDocument.timeline.currentTime;
+
+ // Re-introduce display:none
+ hiddenIFrame.style.display = 'none';
+ window.getComputedStyle(hiddenIFrame).display;
+
+ window.requestAnimationFrame(t.step_func(function() {
+ assert_true(
+ hiddenIFrame.contentDocument.timeline.currentTime >= previousValue,
+ 'document.timeline.currentTime does not go backwards after'
+ + ' re-setting display:none');
+ t.done();
+ }));
+ }));
+ });
+
+ if (hiddenIFrame.contentDocument.readyState === 'complete') {
+ testToRunOnLoad();
+ } else {
+ hiddenIFrame.addEventListener("load", testToRunOnLoad);
+ }
+}, 'document.timeline.currentTime hidden subframe dynamic test');
+
+done();
+</script>
diff --git a/dom/animation/test/document-timeline/test_document-timeline.html b/dom/animation/test/document-timeline/test_document-timeline.html
new file mode 100644
index 0000000000..812d88ef2a
--- /dev/null
+++ b/dom/animation/test/document-timeline/test_document-timeline.html
@@ -0,0 +1,14 @@
+<!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_document-timeline.html");
+ });
+</script>
diff --git a/dom/animation/test/document-timeline/test_request_animation_frame.html b/dom/animation/test/document-timeline/test_request_animation_frame.html
new file mode 100644
index 0000000000..302a385b76
--- /dev/null
+++ b/dom/animation/test/document-timeline/test_request_animation_frame.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test RequestAnimationFrame Timestamps are monotonically increasing</title>
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+ var lastRequestAnimationFrameTimestamp = 0;
+ var requestAnimationFrameCount = 20;
+ var currentCount = 0;
+
+ // Test that all timestamps are always increasing
+ // and do not ever go backwards
+ function rafCallback(aTimestamp) {
+ SimpleTest.ok(aTimestamp > lastRequestAnimationFrameTimestamp,
+ "New RequestAnimationFrame timestamp should be later than the previous RequestAnimationFrame timestamp");
+ lastRequestAnimationFrameTimestamp = aTimestamp;
+ if (currentCount == requestAnimationFrameCount) {
+ SimpleTest.finish();
+ } else {
+ currentCount++;
+ window.requestAnimationFrame(rafCallback);
+ }
+ }
+
+ window.requestAnimationFrame(rafCallback);
+ SimpleTest.waitForExplicitFinish();
+</script>
diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini
new file mode 100644
index 0000000000..feb424518d
--- /dev/null
+++ b/dom/animation/test/mochitest.ini
@@ -0,0 +1,111 @@
+[DEFAULT]
+# Support files for chrome tests that we want to load over HTTP need
+# to go in here, not chrome.ini.
+support-files =
+ chrome/file_animate_xrays.html
+ css-animations/file_animation-cancel.html
+ css-animations/file_animation-computed-timing.html
+ css-animations/file_animation-currenttime.html
+ css-animations/file_animation-finish.html
+ css-animations/file_animation-finished.html
+ css-animations/file_animation-id.html
+ css-animations/file_animation-pausing.html
+ css-animations/file_animation-playstate.html
+ css-animations/file_animation-ready.html
+ css-animations/file_animation-reverse.html
+ css-animations/file_animation-starttime.html
+ css-animations/file_animations-dynamic-changes.html
+ css-animations/file_cssanimation-animationname.html
+ css-animations/file_document-get-animations.html
+ css-animations/file_effect-target.html
+ css-animations/file_element-get-animations.html
+ css-animations/file_keyframeeffect-getkeyframes.html
+ css-animations/file_pseudoElement-get-animations.html
+ css-transitions/file_animation-cancel.html
+ css-transitions/file_animation-computed-timing.html
+ css-transitions/file_animation-currenttime.html
+ css-transitions/file_animation-finished.html
+ css-transitions/file_animation-pausing.html
+ css-transitions/file_animation-ready.html
+ css-transitions/file_animation-starttime.html
+ css-transitions/file_csstransition-transitionproperty.html
+ css-transitions/file_document-get-animations.html
+ css-transitions/file_effect-target.html
+ css-transitions/file_element-get-animations.html
+ css-transitions/file_keyframeeffect-getkeyframes.html
+ css-transitions/file_pseudoElement-get-animations.html
+ css-transitions/file_setting-effect.html
+ document-timeline/file_document-timeline.html
+ mozilla/file_cubic_bezier_limits.html
+ mozilla/file_deferred_start.html
+ mozilla/file_disabled_properties.html
+ mozilla/file_disable_animations_api_core.html
+ mozilla/file_discrete-animations.html
+ mozilla/file_document-timeline-origin-time-range.html
+ mozilla/file_hide_and_show.html
+ mozilla/file_partial_keyframes.html
+ mozilla/file_spacing_property_order.html
+ mozilla/file_spacing_transform.html
+ mozilla/file_transform_limits.html
+ mozilla/file_transition_finish_on_compositor.html
+ mozilla/file_underlying-discrete-value.html
+ mozilla/file_set-easing.html
+ style/file_animation-seeking-with-current-time.html
+ style/file_animation-seeking-with-start-time.html
+ style/file_animation-setting-effect.html
+ style/file_animation-setting-spacing.html
+ testcommon.js
+
+[css-animations/test_animations-dynamic-changes.html]
+[css-animations/test_animation-cancel.html]
+[css-animations/test_animation-computed-timing.html]
+[css-animations/test_animation-currenttime.html]
+[css-animations/test_animation-finish.html]
+[css-animations/test_animation-finished.html]
+[css-animations/test_animation-id.html]
+[css-animations/test_animation-pausing.html]
+[css-animations/test_animation-playstate.html]
+[css-animations/test_animation-ready.html]
+[css-animations/test_animation-reverse.html]
+[css-animations/test_animation-starttime.html]
+[css-animations/test_cssanimation-animationname.html]
+[css-animations/test_document-get-animations.html]
+[css-animations/test_effect-target.html]
+[css-animations/test_element-get-animations.html]
+[css-animations/test_keyframeeffect-getkeyframes.html]
+[css-animations/test_pseudoElement-get-animations.html]
+[css-transitions/test_animation-cancel.html]
+[css-transitions/test_animation-computed-timing.html]
+[css-transitions/test_animation-currenttime.html]
+[css-transitions/test_animation-finished.html]
+[css-transitions/test_animation-pausing.html]
+[css-transitions/test_animation-ready.html]
+[css-transitions/test_animation-starttime.html]
+[css-transitions/test_csstransition-transitionproperty.html]
+[css-transitions/test_document-get-animations.html]
+[css-transitions/test_effect-target.html]
+[css-transitions/test_element-get-animations.html]
+[css-transitions/test_keyframeeffect-getkeyframes.html]
+[css-transitions/test_pseudoElement-get-animations.html]
+[css-transitions/test_setting-effect.html]
+[document-timeline/test_document-timeline.html]
+[document-timeline/test_request_animation_frame.html]
+[mozilla/test_cubic_bezier_limits.html]
+[mozilla/test_deferred_start.html]
+[mozilla/test_disable_animations_api_core.html]
+[mozilla/test_disabled_properties.html]
+[mozilla/test_discrete-animations.html]
+[mozilla/test_document-timeline-origin-time-range.html]
+[mozilla/test_hide_and_show.html]
+[mozilla/test_partial_keyframes.html]
+[mozilla/test_set-easing.html]
+[mozilla/test_spacing_property_order.html]
+[mozilla/test_spacing_transform.html]
+[mozilla/test_transform_limits.html]
+[mozilla/test_transition_finish_on_compositor.html]
+skip-if = toolkit == 'android'
+[mozilla/test_underlying-discrete-value.html]
+[style/test_animation-seeking-with-current-time.html]
+[style/test_animation-seeking-with-start-time.html]
+[style/test_animation-setting-effect.html]
+[style/test_animation-setting-spacing.html]
diff --git a/dom/animation/test/mozilla/file_cubic_bezier_limits.html b/dom/animation/test/mozilla/file_cubic_bezier_limits.html
new file mode 100644
index 0000000000..a0378f3953
--- /dev/null
+++ b/dom/animation/test/mozilla/file_cubic_bezier_limits.html
@@ -0,0 +1,167 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<style>
+@keyframes anim {
+ to { margin-left: 100px; }
+}
+
+.transition-div {
+ margin-left: 100px;
+}
+</style>
+<script>
+'use strict';
+
+// We clamp +infinity or -inifinity value in floating point to
+// maximum floating point value or -maxinum floating point value.
+const max_float = 3.40282e+38;
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim = div.animate({ }, 100 * MS_PER_SEC);
+
+ anim.effect.timing.easing = 'cubic-bezier(0, 1e+39, 0, 0)';
+ assert_equals(anim.effect.timing.easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for effect easing is out of upper boundary');
+
+ anim.effect.timing.easing = 'cubic-bezier(0, 0, 0, 1e+39)';
+ assert_equals(anim.effect.timing.easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for effect easing is out of upper boundary');
+
+ anim.effect.timing.easing = 'cubic-bezier(0, -1e+39, 0, 0)';
+ assert_equals(anim.effect.timing.easing,
+ 'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+ 'y1 control point for effect easing is out of lower boundary');
+
+ anim.effect.timing.easing = 'cubic-bezier(0, 0, 0, -1e+39)';
+ assert_equals(anim.effect.timing.easing,
+ 'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+ 'y2 control point for effect easing is out of lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for effect easing' );
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim = div.animate({ }, 100 * MS_PER_SEC);
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 1e+39, 0, 0)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for keyframe easing is out of upper boundary');
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, 1e+39)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for keyframe easing is out of upper boundary');
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, -1e+39, 0, 0)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+ 'y1 control point for keyframe easing is out of lower boundary');
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, -1e+39)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+ 'y2 control point for keyframe easing is out of lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for keyframe easing' );
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim 100s cubic-bezier(0, 1e+39, 0, 0)';
+
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for CSS animation is out of upper boundary');
+
+ div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, 1e+39)';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for CSS animation is out of upper boundary');
+
+ div.style.animation = 'anim 100s cubic-bezier(0, -1e+39, 0, 0)';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+ 'y1 control point for CSS animation is out of lower boundary');
+
+ div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, -1e+39)';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+ 'y2 control point for CSS animation is out of lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for CSS animation' );
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'transition-div'});
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, 1e+39, 0, 0)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for CSS transition on upper boundary');
+ div.style.transition = '';
+ div.style.marginLeft = '';
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, 1e+39)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for CSS transition on upper boundary');
+ div.style.transition = '';
+ div.style.marginLeft = '';
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, -1e+39, 0, 0)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+ 'y1 control point for CSS transition on lower boundary');
+ div.style.transition = '';
+ div.style.marginLeft = '';
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, -1e+39)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+ 'y2 control point for CSS transition on lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for CSS transition' );
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim = div.animate({ }, { duration: 100 * MS_PER_SEC, fill: 'forwards' });
+
+ anim.pause();
+ // The positive steepest function on both edges.
+ anim.effect.timing.easing = 'cubic-bezier(0, 1e+39, 0, 1e+39)';
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'progress on lower edge for the highest value of y1 and y2 control points');
+
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'progress on upper edge for the highest value of y1 and y2 control points');
+
+ // The negative steepest function on both edges.
+ anim.effect.timing.easing = 'cubic-bezier(0, -1e+39, 0, -1e+39)';
+ anim.currentTime = 0;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'progress on lower edge for the lowest value of y1 and y2 control points');
+
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'progress on lower edge for the lowest value of y1 and y2 control points');
+
+}, 'Calculated values on both edges');
+
+done();
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_deferred_start.html b/dom/animation/test/mozilla/file_deferred_start.html
new file mode 100644
index 0000000000..3be3f56aae
--- /dev/null
+++ b/dom/animation/test/mozilla/file_deferred_start.html
@@ -0,0 +1,121 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+@keyframes empty { }
+@keyframes animTransform {
+ from { transform: translate(0px); }
+ to { transform: translate(100px); }
+}
+.target {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+function waitForDocLoad() {
+ return new Promise(function(resolve, reject) {
+ if (document.readyState === 'complete') {
+ resolve();
+ } else {
+ window.addEventListener('load', resolve);
+ }
+ });
+}
+
+function waitForPaints() {
+ return new Promise(function(resolve, reject) {
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+
+ // Test that empty animations actually start.
+ //
+ // Normally we tie the start of animations to when their first frame of
+ // the animation is rendered. However, for animations that don't actually
+ // trigger a paint (e.g. because they are empty, or are animating something
+ // that doesn't render or is offscreen) we want to make sure they still
+ // start.
+ //
+ // Before we start, wait for the document to finish loading. This is because
+ // during loading we will have other paint events taking place which might,
+ // by luck, happen to trigger animations that otherwise would not have been
+ // triggered, leading to false positives.
+ //
+ // As a result, it's better to wait until we have a more stable state before
+ // continuing.
+ var promiseCallbackDone = false;
+ return waitForDocLoad().then(function() {
+ div.style.animation = 'empty 1000s';
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ promiseCallbackDone = true;
+ }).catch(function() {
+ assert_unreached('ready promise was rejected');
+ });
+ }).then(function() {
+ // We need to wait for up to three frames. This is because in some
+ // cases it can take up to two frames for the initial layout
+ // to take place. Even after that happens we don't actually resolve the
+ // ready promise until the following tick.
+ return waitForAnimationFrames(3);
+ }).then(function() {
+ assert_true(promiseCallbackDone,
+ 'ready promise for an empty animation was resolved'
+ + ' within three animation frames');
+ });
+}, 'Animation.ready is resolved for an empty animation');
+
+// Test that compositor animations with delays get synced correctly
+//
+// NOTE: It is important that we DON'T use
+// SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh here since that takes
+// us through a different code path.
+promise_test(function(t) {
+ // This test only applies to compositor animations
+ if (!isOMTAEnabled()) {
+ return;
+ }
+
+ // Setup animation
+ var div = addDiv(t);
+ div.classList.add('target');
+ div.style.animation = 'animTransform 100s -50s forwards';
+ var animation = div.getAnimations()[0];
+
+ return waitForPaints(function() {
+ var transformStr =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+
+ var matrixComponents =
+ transformStr.startsWith('matrix(')
+ ? transformStr.substring('matrix('.length, transformStr.length-1)
+ .split(',')
+ .map(component => Number(component))
+ : [];
+ assert_equals(matrixComponents.length, 6,
+ 'Got a valid transform matrix on the compositor'
+ + ' (got: "' + transformStr + '")');
+
+ // If the delay has been applied correctly we should be at least
+ // half-way through the animation
+ assert_true(matrixComponents[4] >= 50,
+ 'Animation is at least half-way through on the compositor'
+ + ' (got translation of ' + matrixComponents[4] + ')');
+ });
+}, 'Starting an animation with a delay starts from the correct point');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_disable_animations_api_core.html b/dom/animation/test/mozilla/file_disable_animations_api_core.html
new file mode 100644
index 0000000000..ef77988d9a
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disable_animations_api_core.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim =
+ div.animate({ marginLeft: ['0px', '10px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ // NOTE: We can't check iterationComposite value itself though API since
+ // Animation.effect is also behind the the Web Animations API. So we just
+ // check that style value is not affected by iterationComposite.
+ anim.currentTime = 200 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'Animated style should not be accumulated when the Web Animations API is ' +
+ 'not enabled even if accumulate is specified in the constructor');
+}, 'iterationComposite should not affect at all if the Web Animations API ' +
+ 'is not enabled');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_disabled_properties.html b/dom/animation/test/mozilla/file_disabled_properties.html
new file mode 100644
index 0000000000..f1b72973f4
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disabled_properties.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+function waitForSetPref(pref, value) {
+ return new Promise(function(resolve, reject) {
+ SpecialPowers.pushPrefEnv({ 'set': [[pref, value]] }, resolve);
+ });
+}
+
+/*
+ * These tests rely on the fact that the -webkit-text-fill-color property
+ * is disabled by the layout.css.prefixes.webkit pref. If we ever remove that
+ * pref we will need to substitute some other pref:property combination.
+ */
+
+promise_test(function(t) {
+ return waitForSetPref('layout.css.prefixes.webkit', true).then(() => {
+ var anim = addDiv(t).animate({ webkitTextFillColor: [ 'green', 'blue' ]});
+ assert_equals(anim.effect.getKeyframes().length, 2,
+ 'A property-indexed keyframe specifying only enabled'
+ + ' properties produces keyframes');
+ return waitForSetPref('layout.css.prefixes.webkit', false);
+ }).then(() => {
+ var anim = addDiv(t).animate({ webkitTextFillColor: [ 'green', 'blue' ]});
+ assert_equals(anim.effect.getKeyframes().length, 0,
+ 'A property-indexed keyframe specifying only disabled'
+ + ' properties produces no keyframes');
+ });
+}, 'Specifying a disabled property using a property-indexed keyframe');
+
+promise_test(function(t) {
+ var createAnim = () => {
+ var anim = addDiv(t).animate([ { webkitTextFillColor: 'green' },
+ { webkitTextFillColor: 'blue' } ]);
+ assert_equals(anim.effect.getKeyframes().length, 2,
+ 'Animation specified using a keyframe sequence should'
+ + ' return the same number of keyframes regardless of'
+ + ' whether or not the specified properties are disabled');
+ return anim;
+ };
+
+ var assert_has_property = (anim, index, descr, property) => {
+ assert_true(
+ anim.effect.getKeyframes()[index].hasOwnProperty(property),
+ `${descr} should have the '${property}' property`);
+ };
+ var assert_does_not_have_property = (anim, index, descr, property) => {
+ assert_false(
+ anim.effect.getKeyframes()[index].hasOwnProperty(property),
+ `${descr} should NOT have the '${property}' property`);
+ };
+
+ return waitForSetPref('layout.css.prefixes.webkit', true).then(() => {
+ var anim = createAnim();
+ assert_has_property(anim, 0, 'Initial keyframe', 'webkitTextFillColor');
+ assert_has_property(anim, 1, 'Final keyframe', 'webkitTextFillColor');
+ return waitForSetPref('layout.css.prefixes.webkit', false);
+ }).then(() => {
+ var anim = createAnim();
+ assert_does_not_have_property(anim, 0, 'Initial keyframe',
+ 'webkitTextFillColor');
+ assert_does_not_have_property(anim, 1, 'Final keyframe',
+ 'webkitTextFillColor');
+ });
+}, 'Specifying a disabled property using a keyframe sequence');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_discrete-animations.html b/dom/animation/test/mozilla/file_discrete-animations.html
new file mode 100644
index 0000000000..35e818a909
--- /dev/null
+++ b/dom/animation/test/mozilla/file_discrete-animations.html
@@ -0,0 +1,170 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test Mozilla-specific discrete animatable properties</title>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+const gMozillaSpecificProperties = {
+ "-moz-appearance": {
+ // https://drafts.csswg.org/css-align/#propdef-align-content
+ from: "button",
+ to: "none"
+ },
+ "-moz-border-bottom-colors": {
+ from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+ to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+ },
+ "-moz-border-left-colors": {
+ from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+ to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+ },
+ "-moz-border-right-colors": {
+ from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+ to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+ },
+ "-moz-border-top-colors": {
+ from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+ to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+ },
+ "-moz-box-align": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/box-align
+ from: "center",
+ to: "stretch"
+ },
+ "-moz-box-direction": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/box-direction
+ from: "reverse",
+ to: "normal"
+ },
+ "-moz-box-ordinal-group": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/box-ordinal-group
+ from: "1",
+ to: "5"
+ },
+ "-moz-box-orient": {
+ // https://www.w3.org/TR/css-flexbox-1/
+ from: "horizontal",
+ to: "vertical"
+ },
+ "-moz-box-pack": {
+ // https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#propdef-box-pack
+ from: "center",
+ to: "end"
+ },
+ "-moz-float-edge": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/-moz-float-edge
+ from: "margin-box",
+ to: "content-box"
+ },
+ "-moz-force-broken-image-icon": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/-moz-force-broken-image-icon
+ from: "1",
+ to: "5"
+ },
+ "image-rendering": {
+ // https://drafts.csswg.org/css-images-3/#propdef-image-rendering
+ from: "-moz-crisp-edges",
+ to: "auto"
+ },
+ "-moz-stack-sizing": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/-moz-stack-sizing
+ from: "ignore",
+ to: "stretch-to-fit"
+ },
+ "-moz-tab-size": {
+ // https://drafts.csswg.org/css-text-3/#propdef-tab-size
+ from: "1",
+ to: "5"
+ },
+ "-moz-text-size-adjust": {
+ // https://drafts.csswg.org/css-size-adjust/#propdef-text-size-adjust
+ from: "none",
+ to: "auto"
+ },
+ "-webkit-text-stroke-width": {
+ // https://compat.spec.whatwg.org/#propdef--webkit-text-stroke-width
+ from: "10px",
+ to: "50px"
+ }
+}
+
+for (let property in gMozillaSpecificProperties) {
+ const testData = gMozillaSpecificProperties[property];
+ const from = testData.from;
+ const to = testData.to;
+ const idlName = propertyToIDL(property);
+ const keyframes = {};
+ keyframes[idlName] = [from, to];
+
+ test(t => {
+ const div = addDiv(t);
+ const animation = div.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 499, expected: from.toLowerCase() },
+ { time: 500, expected: to.toLowerCase() },
+ { time: 1000, expected: to.toLowerCase() }]);
+ }, property + " should animate between '"
+ + from + "' and '" + to + "' with linear easing");
+
+ test(function(t) {
+ // Easing: http://cubic-bezier.com/#.68,0,1,.01
+ // With this curve, we don't reach the 50% point until about 95% of
+ // the time has expired.
+ const div = addDiv(t);
+ const animation = div.animate(keyframes,
+ { duration: 1000, fill: "both",
+ easing: "cubic-bezier(0.68,0,1,0.01)" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 940, expected: from.toLowerCase() },
+ { time: 960, expected: to.toLowerCase() }]);
+ }, property + " should animate between '"
+ + from + "' and '" + to + "' with effect easing");
+
+ test(function(t) {
+ // Easing: http://cubic-bezier.com/#.68,0,1,.01
+ // With this curve, we don't reach the 50% point until about 95% of
+ // the time has expired.
+ keyframes.easing = "cubic-bezier(0.68,0,1,0.01)";
+ const div = addDiv(t);
+ const animation = div.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 940, expected: from.toLowerCase() },
+ { time: 960, expected: to.toLowerCase() }]);
+ }, property + " should animate between '"
+ + from + "' and '" + to + "' with keyframe easing");
+}
+
+function propertyToIDL(property) {
+ var prefixMatch = property.match(/^-(\w+)-/);
+ if (prefixMatch) {
+ var prefix = prefixMatch[1] === "moz" ? "Moz" : prefixMatch[1];
+ property = prefix + property.substring(prefixMatch[0].length - 1);
+ }
+ // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
+ return property.replace(/-([a-z])/gi, function(str, group) {
+ return group.toUpperCase();
+ });
+}
+
+function testAnimationSamples(animation, idlName, testSamples) {
+ const target = animation.effect.target;
+ testSamples.forEach(testSample => {
+ animation.currentTime = testSample.time;
+ assert_equals(getComputedStyle(target)[idlName], testSample.expected,
+ "The value should be " + testSample.expected +
+ " at " + testSample.time + "ms");
+ });
+}
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html b/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html
new file mode 100644
index 0000000000..083bf0903b
--- /dev/null
+++ b/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+// If the originTime parameter passed to the DocumentTimeline exceeds
+// the range of the internal storage type (a signed 64-bit integer number
+// of ticks--a platform-dependent unit) then we should throw.
+// Infinity isn't allowed as an origin time value and clamping to just
+// inside the allowed range will just mean we overflow elsewhere.
+
+test(function(t) {
+ assert_throws({ name: 'TypeError'},
+ function() {
+ new DocumentTimeline({ originTime: Number.MAX_SAFE_INTEGER });
+ });
+}, 'Calculated current time is positive infinity');
+
+test(function(t) {
+ assert_throws({ name: 'TypeError'},
+ function() {
+ new DocumentTimeline({ originTime: -1 * Number.MAX_SAFE_INTEGER });
+ });
+}, 'Calculated current time is negative infinity');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_hide_and_show.html b/dom/animation/test/mozilla/file_hide_and_show.html
new file mode 100644
index 0000000000..0771fcce1f
--- /dev/null
+++ b/dom/animation/test/mozilla/file_hide_and_show.html
@@ -0,0 +1,162 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes move {
+ 100% {
+ transform: translateX(100px);
+ }
+}
+
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ div.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'display:none element has no animations');
+}, 'Animation stops playing when the element style display is set to "none"');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ parentElement.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+}, 'Animation stops playing when its parent element style display is set ' +
+ 'to "none"');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ div.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'display:none element has no animations');
+
+ div.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer display:none has animations ' +
+ 'again');
+}, 'Animation starts playing when the element gets shown from ' +
+ '"display:none" state');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ parentElement.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+
+ parentElement.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer in display:none subtree has ' +
+ 'animations again');
+}, 'Animation starts playing when its parent element is shown from ' +
+ '"display:none" state');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: move 100s forwards' });
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ assert_equals(div.getAnimations().length, 1,
+ 'Element has finished animation if the animation ' +
+ 'fill-mode is forwards');
+
+ div.style.display = 'none';
+ assert_equals(animation.playState, 'idle',
+ 'The animation.playState should be idle');
+
+ assert_equals(div.getAnimations().length, 0,
+ 'display:none element has no animations');
+
+ div.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer display:none has animations ' +
+ 'again');
+ assert_not_equals(div.getAnimations()[0], animation,
+ 'Restarted animation is a newly-generated animation');
+
+}, 'Animation which has already finished starts playing when the element ' +
+ 'gets shown from "display:none" state');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s forwards' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ assert_equals(div.getAnimations().length, 1,
+ 'Element has finished animation if the animation ' +
+ 'fill-mode is forwards');
+
+ parentElement.style.display = 'none';
+ assert_equals(animation.playState, 'idle',
+ 'The animation.playState should be idle');
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+
+ parentElement.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer in display:none subtree has ' +
+ 'animations again');
+
+ assert_not_equals(div.getAnimations()[0], animation,
+ 'Restarted animation is a newly-generated animation');
+
+}, 'Animation with fill:forwards which has already finished starts playing ' +
+ 'when its parent element is shown from "display:none" state');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ assert_equals(div.getAnimations().length, 0,
+ 'Element does not have finished animations');
+
+ parentElement.style.display = 'none';
+ assert_equals(animation.playState, 'idle',
+ 'The animation.playState should be idle');
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+
+ parentElement.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer in display:none subtree has ' +
+ 'animations again');
+
+ assert_not_equals(div.getAnimations()[0], animation,
+ 'Restarted animation is a newly-generated animation');
+
+}, 'CSS Animation which has already finished starts playing when its parent ' +
+ 'element is shown from "display:none" state');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_partial_keyframes.html b/dom/animation/test/mozilla/file_partial_keyframes.html
new file mode 100644
index 0000000000..68832be7a0
--- /dev/null
+++ b/dom/animation/test/mozilla/file_partial_keyframes.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+// Tests for cases we currently don't handle and should throw an exception for.
+var gTests = [
+ { desc: "single Keyframe with no offset",
+ keyframes: [{ left: "100px" }] },
+ { desc: "multiple Keyframes with missing 0% Keyframe",
+ keyframes: [{ left: "100px", offset: 0.25 },
+ { left: "200px", offset: 0.50 },
+ { left: "300px", offset: 1.00 }] },
+ { desc: "multiple Keyframes with missing 100% Keyframe",
+ keyframes: [{ left: "100px", offset: 0.00 },
+ { left: "200px", offset: 0.50 },
+ { left: "300px", offset: 0.75 }] },
+ { desc: "multiple Keyframes with missing properties on first Keyframe",
+ keyframes: [{ left: "100px", offset: 0.0 },
+ { left: "200px", top: "200px", offset: 0.5 },
+ { left: "300px", top: "300px", offset: 1.0 }] },
+ { desc: "multiple Keyframes with missing properties on last Keyframe",
+ keyframes: [{ left: "100px", top: "200px", offset: 0.0 },
+ { left: "200px", top: "200px", offset: 0.5 },
+ { left: "300px", offset: 1.0 }] },
+];
+
+gTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = addDiv(t);
+ assert_throws("NotSupportedError", function() {
+ new KeyframeEffectReadOnly(div, subtest.keyframes);
+ });
+ }, "KeyframeEffectReadOnly constructor throws with " + subtest.desc);
+});
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_set-easing.html b/dom/animation/test/mozilla/file_set-easing.html
new file mode 100644
index 0000000000..072b125cb0
--- /dev/null
+++ b/dom/animation/test/mozilla/file_set-easing.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test setting easing in sandbox</title>
+<script src="../testcommon.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+test(function(t) {
+ const div = document.createElement("div");
+ document.body.appendChild(div);
+ div.animate({ opacity: [0, 1] }, 100000 );
+
+ const contentScript = function() {
+ try {
+ document.getAnimations()[0].effect.timing.easing = "linear";
+ assert_true(true, 'Setting easing should not throw in sandbox');
+ } catch (e) {
+ assert_unreached('Setting easing threw ' + e);
+ }
+ };
+
+ const sandbox = new SpecialPowers.Cu.Sandbox(window);
+ sandbox.importFunction(document, "document");
+ sandbox.importFunction(assert_true, "assert_true");
+ sandbox.importFunction(assert_unreached, "assert_unreached");
+ SpecialPowers.Cu.evalInSandbox(`(${contentScript.toSource()})()`, sandbox);
+}, 'Setting easing should not throw any exceptions in sandbox');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_spacing_property_order.html b/dom/animation/test/mozilla/file_spacing_property_order.html
new file mode 100644
index 0000000000..1338d60811
--- /dev/null
+++ b/dom/animation/test/mozilla/file_spacing_property_order.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = document.createElement('div');
+ document.documentElement.appendChild(div);
+ var anim = div.animate([ { borderRadius: "0", borderTopRightRadius: "10%" },
+ { borderTopLeftRadius: "20%",
+ borderTopRightRadius: "30%",
+ borderBottomRightRadius: "40%",
+ borderBottomLeftRadius: "50%" },
+ { borderRadius: "50%" } ],
+ { spacing:"paced(border-radius)" });
+
+ var frames = anim.effect.getKeyframes();
+ var dist = [ 0,
+ Math.sqrt(20 * 20 + (30 - 10) * (30 - 10) + 40 * 40 + 50 * 50),
+ Math.sqrt((50 - 20) * (50 - 20) + (50 - 30) * (50 - 30) +
+ (50 - 40) * (50 - 40) + (50 - 50) * (50 - 50)) ];
+ var cumDist = [];
+ dist.reduce(function(prev, curr, i) { return cumDist[i] = prev + curr; }, 0);
+ assert_approx_equals(frames[1].computedOffset, cumDist[1] / cumDist[2],
+ 0.0001, 'frame offset');
+}, 'Test for the longhand components of the shorthand property surely sorted' );
+
+done();
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_spacing_transform.html b/dom/animation/test/mozilla/file_spacing_transform.html
new file mode 100644
index 0000000000..0de7737864
--- /dev/null
+++ b/dom/animation/test/mozilla/file_spacing_transform.html
@@ -0,0 +1,240 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+const pi = Math.PI;
+const cos = Math.cos;
+const sin = Math.sin;
+const tan = Math.tan;
+const sqrt = Math.sqrt;
+
+// Help function for testing the computed offsets by the distance array.
+function assert_animation_offsets(anim, dist) {
+ const epsilon = 0.00000001;
+ const frames = anim.effect.getKeyframes();
+ const cumDist = dist.reduce( (prev, curr) => {
+ prev.push(prev.length == 0 ? curr : curr + prev[prev.length - 1]);
+ return prev;
+ }, []);
+
+ const total = cumDist[cumDist.length - 1];
+ for (var i = 0; i < frames.length; ++i) {
+ assert_approx_equals(frames[i].computedOffset, cumDist[i] / total,
+ epsilon, 'computedOffset of frame ' + i);
+ }
+}
+
+function getAngleDist(rotate1, rotate2) {
+ function quaternion(axis, angle) {
+ var x = axis[0] * sin(angle/2.0);
+ var y = axis[1] * sin(angle/2.0);
+ var z = axis[2] * sin(angle/2.0);
+ var w = cos(angle/2.0);
+ return { 'x': x, 'y': y, 'z': z, 'w': w };
+ }
+ var q1 = quaternion(rotate1.axis, rotate1.angle);
+ var q2 = quaternion(rotate2.axis, rotate2.angle);
+ var dotProduct = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w;
+ return 2.0 * Math.acos(dotProduct);
+}
+
+function createMatrix(elements, Is3D) {
+ return (Is3D ? "matrix3d" : "matrix") + "(" + elements.join() + ")";
+}
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "none" },
+ { transform: "translate(-20px)" },
+ { transform: "translate(100px)" },
+ { transform: "translate(50px)"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim, [ 0, 20, 120, 50 ]);
+}, 'Test spacing on translate' );
+
+test(function(t) {
+ var anim =
+ addDiv(t).animate([ { transform: "none" },
+ { transform: "translate3d(-20px, 10px, 100px)" },
+ { transform: "translate3d(100px, 200px, 50px)" },
+ { transform: "translate(50px, -10px)"} ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ Math.sqrt(20 * 20 + 10 * 10 + 100 * 100),
+ Math.sqrt(120 * 120 + 190 * 190 + 50 * 50),
+ Math.sqrt(50 * 50 + 210 * 210 + 50 * 50) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on translate3d' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "scale(0.5)" },
+ { transform: "scale(4.5)" },
+ { transform: "scale(2.5)" },
+ { transform: "none"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim, [ 0, 4.0, 2.0, 1.5 ]);
+}, 'Test spacing on scale' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "scale(0.5, 0.5)" },
+ { transform: "scale3d(4.5, 5.0, 2.5)" },
+ { transform: "scale3d(2.5, 1.0, 2.0)" },
+ { transform: "scale3d(1, 0.5, 1.0)"} ],
+ { spacing:"paced(transform)" });
+ var dist = [ 0,
+ Math.sqrt(4.0 * 4.0 + 4.5 * 4.5 + 1.5 * 1.5),
+ Math.sqrt(2.0 * 2.0 + 4.0 * 4.0 + 0.5 * 0.5),
+ Math.sqrt(1.5 * 1.5 + 0.5 * 0.5 + 1.0 * 1.0) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on scale3d' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "rotate(60deg)" },
+ { transform: "none" },
+ { transform: "rotate(720deg)" },
+ { transform: "rotate(-360deg)"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim, [ 0, 60, 720, 1080 ]);
+}, 'Test spacing on rotate' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "rotate3d(1,0,0,60deg)" },
+ { transform: "rotate3d(1,0,0,70deg)" },
+ { transform: "rotate3d(0,0,1,-110deg)" },
+ { transform: "rotate3d(1,0,0,219deg)"} ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ getAngleDist({ axis: [1,0,0], angle: 60 * pi / 180 },
+ { axis: [1,0,0], angle: 70 * pi / 180 }),
+ getAngleDist({ axis: [0,1,0], angle: 70 * pi / 180 },
+ { axis: [0,0,1], angle: -110 * pi / 180 }),
+ getAngleDist({ axis: [0,0,1], angle: -110 * pi / 180 },
+ { axis: [1,0,0], angle: 219 * pi / 180 }) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on rotate3d' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "skew(60deg)" },
+ { transform: "none" },
+ { transform: "skew(-90deg)" },
+ { transform: "skew(90deg)"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim, [ 0, 60, 90, 180 ]);
+}, 'Test spacing on skew' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "skew(60deg, 30deg)" },
+ { transform: "none" },
+ { transform: "skew(-90deg, 60deg)" },
+ { transform: "skew(90deg, 60deg)"} ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ sqrt(60 * 60 + 30 * 30),
+ sqrt(90 * 90 + 60 * 60),
+ sqrt(180 * 180 + 0) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on skew along both X and Y' );
+
+test(function(t) {
+ // We calculate the distance of two perspective functions by converting them
+ // into two matrix3ds, and then do matrix decomposition to get two
+ // perspective vectors, so the equivalent perspective vectors are:
+ // perspective 1: (0, 0, -1/128, 1);
+ // perspective 2: (0, 0, -1/infinity = 0, 1);
+ // perspective 3: (0, 0, -1/1024, 1);
+ // perspective 4: (0, 0, -1/32, 1);
+ var anim = addDiv(t).animate([ { transform: "perspective(128px)" },
+ { transform: "none" },
+ { transform: "perspective(1024px)" },
+ { transform: "perspective(32px)"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim,
+ [ 0, 1/128, 1/1024, 1/32 - 1/1024 ]);
+}, 'Test spacing on perspective' );
+
+test(function(t) {
+ var anim =
+ addDiv(t).animate([ { transform: "none" },
+ { transform: "rotate(180deg) translate(0px)" },
+ { transform: "rotate(180deg) translate(1000px)" },
+ { transform: "rotate(360deg) translate(1000px)"} ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ sqrt(pi * pi + 0),
+ sqrt(1000 * 1000),
+ sqrt(pi * pi + 0) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on matched transform lists' );
+
+test(function(t) {
+ // matrix1 => translate(100px, 50px), skewX(60deg).
+ // matrix2 => translate(1000px), rotate(180deg).
+ // matrix3 => translate(1000px), scale(1.5, 0.7).
+ const matrix1 = createMatrix([ 1, 0, tan(pi/4.0), 1, 100, 50 ]);
+ const matrix2 = createMatrix([ cos(pi), sin(pi),
+ -sin(pi), cos(pi),
+ 1000, 0 ]);
+ const matrix3 = createMatrix([ 1.5, 0, 0, 0.7, 1000, 0 ]);
+ var anim = addDiv(t).animate([ { transform: "none" },
+ { transform: matrix1 },
+ { transform: matrix2 },
+ { transform: matrix3 } ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ sqrt(100 * 100 + 50 * 50 + pi/4 * pi/4),
+ sqrt(900 * 900 + 50 * 50 + pi * pi + pi/4 * pi/4),
+ sqrt(pi * pi + 0.5 * 0.5 + 0.3 * 0.3) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on matrix' );
+
+test(function(t) {
+ // matrix1 => translate3d(100px, 50px, -10px), skew(60deg).
+ // matrix2 => translate3d(1000px, 0, 0), rotate3d(1, 0, 0, 180deg).
+ // matrix3 => translate3d(1000px, 0, 0), scale3d(1.5, 0.7, 2.2).
+ const matrix1 = createMatrix([ 1, 0, 0, 0,
+ tan(pi/4.0), 1, 0, 0,
+ 0, 0, 1, 0,
+ 100, 50, -10, 1 ], true);
+ const matrix2 = createMatrix([ 1, 0, 0, 0,
+ 0, cos(pi), sin(pi), 0,
+ 0, -sin(pi), cos(pi), 0,
+ 1000, 0, 0, 1 ], true);
+ const matrix3 = createMatrix([ 1.5, 0, 0, 0,
+ 0, 0.7, 0, 0,
+ 0, 0, 2.2, 0,
+ 1000, 0, 0, 1 ], true);
+ var anim = addDiv(t).animate([ { transform: "none" },
+ { transform: matrix1 },
+ { transform: matrix2 },
+ { transform: matrix3 } ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ sqrt(100 * 100 + 50 * 50 + 10 * 10 + pi/4 * pi/4),
+ sqrt(900 * 900 + 50 * 50 + 10 * 10 + pi/4 * pi/4 + pi * pi),
+ sqrt(0.5 * 0.5 + 0.3 * 0.3 + 1.2 * 1.2 + pi * pi) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on matrix3d' );
+
+test(function(t) {
+ var anim =
+ addDiv(t).animate([ { transform: "none" },
+ { transform: "translate(100px, 50px) skew(45deg)" },
+ { transform: "translate(1000px) " +
+ "rotate3d(1, 0, 0, 180deg)" },
+ { transform: "translate(1000px) " +
+ "scale3d(2.5, 0.5, 0.7)" } ],
+ { spacing: "paced(transform)" });
+
+ var dist = [ 0,
+ sqrt(100 * 100 + 50 * 50 + pi/4 * pi/4),
+ sqrt(900 * 900 + 50 * 50 + pi/4 * pi/4 + pi * pi),
+ sqrt(1.5 * 1.5 + 0.5 * 0.5 + 0.3 * 0.3 + pi * pi) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on mismatched transform list' );
+
+done();
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_transform_limits.html b/dom/animation/test/mozilla/file_transform_limits.html
new file mode 100644
index 0000000000..d4c813c67d
--- /dev/null
+++ b/dom/animation/test/mozilla/file_transform_limits.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+// We clamp +infinity or -inifinity value in floating point to
+// maximum floating point value or -maximum floating point value.
+const max_float = 3.40282e+38;
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style = "width: 1px; height: 1px;";
+ var anim = div.animate([ { transform: 'scale(1)' },
+ { transform: 'scale(3.5e+38)'},
+ { transform: 'scale(3)' } ], 100 * MS_PER_SEC);
+
+ anim.pause();
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(' + max_float + ', 0, 0, ' + max_float + ', 0, 0)');
+}, 'Test that the parameter of transform scale is clamped' );
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style = "width: 1px; height: 1px;";
+ var anim = div.animate([ { transform: 'translate(1px)' },
+ { transform: 'translate(3.5e+38px)'},
+ { transform: 'translate(3px)' } ], 100 * MS_PER_SEC);
+
+ anim.pause();
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(1, 0, 0, 1, ' + max_float + ', 0)');
+}, 'Test that the parameter of transform translate is clamped' );
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style = "width: 1px; height: 1px;";
+ var anim = div.animate([ { transform: 'matrix(0.5, 0, 0, 0.5, 0, 0)' },
+ { transform: 'matrix(2, 0, 0, 2, 3.5e+38, 0)'},
+ { transform: 'matrix(0, 2, 0, -2, 0, 0)' } ],
+ 100 * MS_PER_SEC);
+
+ anim.pause();
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(2, 0, 0, 2, ' + max_float + ', 0)');
+}, 'Test that the parameter of transform matrix is clamped' );
+
+done();
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_transition_finish_on_compositor.html b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html
new file mode 100644
index 0000000000..4912d05dd1
--- /dev/null
+++ b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html
@@ -0,0 +1,67 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+function waitForPaints() {
+ return new Promise(function(resolve, reject) {
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+promise_test(t => {
+ // This test only applies to compositor animations
+ if (!isOMTAEnabled()) {
+ return;
+ }
+
+ var div = addDiv(t, { style: 'transition: transform 50ms; ' +
+ 'transform: translateX(0px)' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+
+ var timeBeforeStart = window.performance.now();
+ return waitForPaints().then(() => {
+ // If it took over 50ms to paint the transition, we have no luck
+ // to test it. This situation will happen if GC runs while waiting for the
+ // paint.
+ if (window.performance.now() - timeBeforeStart >= 50) {
+ return;
+ }
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_not_equals(transform, '',
+ 'The transition style is applied on the compositor');
+
+ // Generate artificial busyness on the main thread for 100ms.
+ var timeAtStart = window.performance.now();
+ while (window.performance.now() - timeAtStart < 100) {}
+
+ // Now the transition on the compositor should finish but stay at the final
+ // position because there was no chance to pull the transition back from
+ // the compositor.
+ transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)',
+ 'The final transition style is still applied on the ' +
+ 'compositor');
+ });
+}, 'Transition on the compositor keeps the final style while the main thread ' +
+ 'is busy even if the transition finished on the compositor');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_underlying-discrete-value.html b/dom/animation/test/mozilla/file_underlying-discrete-value.html
new file mode 100644
index 0000000000..3be01b9041
--- /dev/null
+++ b/dom/animation/test/mozilla/file_underlying-discrete-value.html
@@ -0,0 +1,192 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+"use strict";
+
+// Tests that we correctly extract the underlying value when the animation
+// type is 'discrete'.
+const discreteTests = [
+ {
+ stylesheet: {
+ "@keyframes keyframes":
+ "from { align-content: flex-start; } to { align-content: flex-end; } "
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "flex-start" },
+ { computedOffset: 1, alignContent: "flex-end" }
+ ],
+ explanation: "Test for fully-specified keyframes"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "from { align-content: flex-start; }"
+ },
+ // The value of 100% should be 'stretch',
+ // but we are not supporting underlying value.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1295401
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "flex-start" },
+ { computedOffset: 1, alignContent: "unset" }
+ ],
+ explanation: "Test for 0% keyframe only"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "to { align-content: flex-end; }"
+ },
+ // The value of 0% should be 'stretch',
+ // but we are not supporting underlying value.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1295401
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "unset" },
+ { computedOffset: 1, alignContent: "flex-end" }
+ ],
+ explanation: "Test for 100% keyframe only"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "#target": "align-content: space-between;"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }"
+ },
+ attributes: {
+ style: "align-content: space-between"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element using style attribute"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "#target": "align-content: inherit;"
+ },
+ // The value of 0%/100% should be 'stretch',
+ // but we are not supporting underlying value.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1295401
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "inherit" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "inherit" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and 'inherit' specified on target element"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ ".target": "align-content: space-between;"
+ },
+ attributes: {
+ class: "target"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element using class selector"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "div": "align-content: space-between;"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element using type selector"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "div": "align-content: space-between;",
+ ".target": "align-content: flex-start;",
+ "#target": "align-content: flex-end;"
+ },
+ attributes: {
+ class: "target"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "flex-end" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "flex-end" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element " +
+ "using ID selector that overrides class selector"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "div": "align-content: space-between !important;",
+ ".target": "align-content: flex-start;",
+ "#target": "align-content: flex-end;"
+ },
+ attributes: {
+ class: "target"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element " +
+ "using important type selector that overrides other rules"
+ },
+];
+
+discreteTests.forEach(testcase => {
+ test(t => {
+ addStyle(t, testcase.stylesheet);
+
+ const div = addDiv(t, { "id": "target" });
+ if (testcase.attributes) {
+ for (let attributeName in testcase.attributes) {
+ div.setAttribute(attributeName, testcase.attributes[attributeName]);
+ }
+ }
+ div.style.animation = "keyframes 100s";
+
+ const keyframes = div.getAnimations()[0].effect.getKeyframes();
+ const expectedKeyframes = testcase.expectedKeyframes;
+ assert_equals(keyframes.length, expectedKeyframes.length,
+ `keyframes.length should be ${ expectedKeyframes.length }`);
+
+ keyframes.forEach((keyframe, index) => {
+ const expectedKeyframe = expectedKeyframes[index];
+ assert_equals(keyframe.computedOffset, expectedKeyframe.computedOffset,
+ `computedOffset of keyframes[${ index }] should be ` +
+ `${ expectedKeyframe.computedOffset }`);
+ assert_equals(keyframe.alignContent, expectedKeyframe.alignContent,
+ `alignContent of keyframes[${ index }] should be ` +
+ `${ expectedKeyframe.alignContent }`);
+ });
+ }, testcase.explanation);
+});
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_cubic_bezier_limits.html b/dom/animation/test/mozilla/test_cubic_bezier_limits.html
new file mode 100644
index 0000000000..e67e5dbbb0
--- /dev/null
+++ b/dom/animation/test/mozilla/test_cubic_bezier_limits.html
@@ -0,0 +1,14 @@
+<!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_cubic_bezier_limits.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_deferred_start.html b/dom/animation/test/mozilla/test_deferred_start.html
new file mode 100644
index 0000000000..4db4bf6766
--- /dev/null
+++ b/dom/animation/test/mozilla/test_deferred_start.html
@@ -0,0 +1,14 @@
+<!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_deferred_start.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_disable_animations_api_core.html b/dom/animation/test/mozilla/test_disable_animations_api_core.html
new file mode 100644
index 0000000000..cfb64e5371
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disable_animations_api_core.html
@@ -0,0 +1,14 @@
+<!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", false]]},
+ function() {
+ window.open("file_disable_animations_api_core.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_disabled_properties.html b/dom/animation/test/mozilla/test_disabled_properties.html
new file mode 100644
index 0000000000..86d02e6b69
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disabled_properties.html
@@ -0,0 +1,14 @@
+<!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_disabled_properties.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_discrete-animations.html b/dom/animation/test/mozilla/test_discrete-animations.html
new file mode 100644
index 0000000000..2a36bd50e0
--- /dev/null
+++ b/dom/animation/test/mozilla/test_discrete-animations.html
@@ -0,0 +1,18 @@
+<!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],
+ ["layout.css.osx-font-smoothing.enabled", true],
+ ["layout.css.prefixes.webkit", true]
+ ] },
+ function() {
+ window.open("file_discrete-animations.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html b/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html
new file mode 100644
index 0000000000..f73c233d39
--- /dev/null
+++ b/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html
@@ -0,0 +1,14 @@
+<!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_document-timeline-origin-time-range.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_hide_and_show.html b/dom/animation/test/mozilla/test_hide_and_show.html
new file mode 100644
index 0000000000..929a31bd47
--- /dev/null
+++ b/dom/animation/test/mozilla/test_hide_and_show.html
@@ -0,0 +1,14 @@
+<!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_hide_and_show.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_partial_keyframes.html b/dom/animation/test/mozilla/test_partial_keyframes.html
new file mode 100644
index 0000000000..28eb4c5881
--- /dev/null
+++ b/dom/animation/test/mozilla/test_partial_keyframes.html
@@ -0,0 +1,14 @@
+<!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_partial_keyframes.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_set-easing.html b/dom/animation/test/mozilla/test_set-easing.html
new file mode 100644
index 0000000000..e0069ff1c8
--- /dev/null
+++ b/dom/animation/test/mozilla/test_set-easing.html
@@ -0,0 +1,14 @@
+<!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_set-easing.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_spacing_property_order.html b/dom/animation/test/mozilla/test_spacing_property_order.html
new file mode 100644
index 0000000000..afcc12bedf
--- /dev/null
+++ b/dom/animation/test/mozilla/test_spacing_property_order.html
@@ -0,0 +1,14 @@
+<!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_spacing_property_order.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_spacing_transform.html b/dom/animation/test/mozilla/test_spacing_transform.html
new file mode 100644
index 0000000000..38dce7e99c
--- /dev/null
+++ b/dom/animation/test/mozilla/test_spacing_transform.html
@@ -0,0 +1,14 @@
+<!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_spacing_transform.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_transform_limits.html b/dom/animation/test/mozilla/test_transform_limits.html
new file mode 100644
index 0000000000..6c9b5e4fa9
--- /dev/null
+++ b/dom/animation/test/mozilla/test_transform_limits.html
@@ -0,0 +1,14 @@
+<!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_transform_limits.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_transition_finish_on_compositor.html b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html
new file mode 100644
index 0000000000..357e5297e9
--- /dev/null
+++ b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html
@@ -0,0 +1,14 @@
+<!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_transition_finish_on_compositor.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_underlying-discrete-value.html b/dom/animation/test/mozilla/test_underlying-discrete-value.html
new file mode 100644
index 0000000000..7feee53a15
--- /dev/null
+++ b/dom/animation/test/mozilla/test_underlying-discrete-value.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_underlying-discrete-value.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/style/file_animation-seeking-with-current-time.html b/dom/animation/test/style/file_animation-seeking-with-current-time.html
new file mode 100644
index 0000000000..c3a5903948
--- /dev/null
+++ b/dom/animation/test/style/file_animation-seeking-with-current-time.html
@@ -0,0 +1,121 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for seeking using Animation.currentTime</title>
+ <style>
+.animated-div {
+ margin-left: -10px;
+ animation-timing-function: linear ! important;
+}
+
+@keyframes anim {
+ from { margin-left: 0px; }
+ to { margin-left: 100px; }
+}
+ </style>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+'use strict';
+
+function assert_marginLeft_equals(target, expect, description) {
+ var marginLeft = parseFloat(getComputedStyle(target).marginLeft);
+ assert_equals(marginLeft, expect, description);
+}
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.currentTime = 90 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 90,
+ 'Computed style is updated when seeking forwards in active interval');
+
+ animation.currentTime = 10 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 10,
+ 'Computed style is updated when seeking backwards in active interval');
+ });
+}, 'Seeking forwards and backward in active interval');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ assert_marginLeft_equals(div, -10,
+ 'Computed style is unaffected in before phase with no backwards fill');
+
+ // before -> active (non-active -> active)
+ animation.currentTime = 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed style is updated when seeking forwards from ' +
+ 'not \'in effect\' to \'in effect\' state');
+ });
+}, 'Seeking to non-\'in effect\' from \'in effect\' (before -> active)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ // move to after phase
+ animation.currentTime = 250 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed style is unaffected in after phase with no forwards fill');
+
+ // after -> active (non-active -> active)
+ animation.currentTime = 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed style is updated when seeking backwards from ' +
+ 'not \'in effect\' to \'in effect\' state');
+ });
+}, 'Seeking to non-\'in effect\' from \'in effect\' (after -> active)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ // move to active phase
+ animation.currentTime = 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed value is set during active phase');
+
+ // active -> before
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed value is not effected after seeking backwards from ' +
+ '\'in effect\' to not \'in effect\' state');
+ });
+}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> before)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ // move to active phase
+ animation.currentTime = 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed value is set during active phase');
+
+ // active -> after
+ animation.currentTime = 250 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed value is not affected after seeking forwards from ' +
+ '\'in effect\' to not \'in effect\' state');
+ });
+}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> after)');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/style/file_animation-seeking-with-start-time.html b/dom/animation/test/style/file_animation-seeking-with-start-time.html
new file mode 100644
index 0000000000..ba09827c63
--- /dev/null
+++ b/dom/animation/test/style/file_animation-seeking-with-start-time.html
@@ -0,0 +1,121 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for seeking using Animation.startTime</title>
+ <style>
+.animated-div {
+ margin-left: -10px;
+ animation-timing-function: linear ! important;
+}
+
+@keyframes anim {
+ from { margin-left: 0px; }
+ to { margin-left: 100px; }
+}
+ </style>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+'use strict';
+
+function assert_marginLeft_equals(target, expect, description) {
+ var marginLeft = parseFloat(getComputedStyle(target).marginLeft);
+ assert_equals(marginLeft, expect, description);
+}
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.startTime = animation.timeline.currentTime - 90 * MS_PER_SEC
+ assert_marginLeft_equals(div, 90,
+ 'Computed style is updated when seeking forwards in active interval');
+
+ animation.startTime = animation.timeline.currentTime - 10 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 10,
+ 'Computed style is updated when seeking backwards in active interval');
+ });
+}, 'Seeking forwards and backward in active interval');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ assert_marginLeft_equals(div, -10,
+ 'Computed style is unaffected in before phase with no backwards fill');
+
+ // before -> active (non-active -> active)
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed style is updated when seeking forwards from ' +
+ 'not \'in effect\' to \'in effect\' state');
+ });
+}, 'Seeking to non-\'in effect\' from \'in effect\' (before -> active)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ // move to after phase
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed style is unaffected in after phase with no forwards fill');
+
+ // after -> active (non-active -> active)
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed style is updated when seeking backwards from ' +
+ 'not \'in effect\' to \'in effect\' state');
+ });
+}, 'Seeking to non-\'in effect\' from \'in effect\' (after -> active)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ // move to active phase
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed value is set during active phase');
+
+ // active -> before
+ animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed value is not affected after seeking backwards from ' +
+ '\'in effect\' to not \'in effect\' state');
+ });
+}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> before)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ // move to active phase
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed value is set during active phase');
+
+ // active -> after
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed value is not affected after seeking forwards from ' +
+ '\'in effect\' to not \'in effect\' state');
+ });
+}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> after)');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/style/file_animation-setting-effect.html b/dom/animation/test/style/file_animation-setting-effect.html
new file mode 100644
index 0000000000..cf50cf2cee
--- /dev/null
+++ b/dom/animation/test/style/file_animation-setting-effect.html
@@ -0,0 +1,125 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for setting effects by using Animation.effect</title>
+ <script src='../testcommon.js'></script>
+ </head>
+ <body>
+ <script type='text/javascript'>
+
+'use strict';
+
+test(function(t) {
+ var target = addDiv(t);
+ var anim = new Animation();
+ anim.effect = new KeyframeEffectReadOnly(target,
+ { marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '50px');
+}, 'After setting target effect on an animation with null effect, the ' +
+ 'animation still works');
+
+test(function(t) {
+ var target = addDiv(t);
+ target.style.marginLeft = '10px';
+ var anim = target.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '50px');
+
+ anim.effect = null;
+ assert_equals(getComputedStyle(target).marginLeft, '10px');
+}, 'After setting null target effect, the computed style of the target ' +
+ 'element becomes the initial value');
+
+test(function(t) {
+ var target = addDiv(t);
+ var animA = target.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ var animB = new Animation();
+ animA.currentTime = 50 * MS_PER_SEC;
+ animB.currentTime = 20 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '50px',
+ 'original computed style of the target element');
+
+ animB.effect = animA.effect;
+ assert_equals(getComputedStyle(target).marginLeft, '20px',
+ 'new computed style of the target element');
+}, 'After setting the target effect from an existing animation, the computed ' +
+ 'style of the target effect should reflect the time of the updated ' +
+ 'animation.');
+
+test(function(t) {
+ var target = addDiv(t);
+ target.style.marginTop = '-10px';
+ var animA = target.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ var animB = target.animate({ marginTop: [ '0px', '100px' ] },
+ 50 * MS_PER_SEC);
+ animA.currentTime = 50 * MS_PER_SEC;
+ animB.currentTime = 10 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '50px',
+ 'original margin-left of the target element');
+ assert_equals(getComputedStyle(target).marginTop, '20px',
+ 'original margin-top of the target element');
+
+ animB.effect = animA.effect;
+ assert_equals(getComputedStyle(target).marginLeft, '10px',
+ 'new margin-left of the target element');
+ assert_equals(getComputedStyle(target).marginTop, '-10px',
+ 'new margin-top of the target element');
+}, 'After setting target effect with an animation to another animation which ' +
+ 'also has an target effect and both animation effects target to the same ' +
+ 'element, the computed style of this element should reflect the time and ' +
+ 'effect of the animation that was set');
+
+test(function(t) {
+ var targetA = addDiv(t);
+ var targetB = addDiv(t);
+ targetB.style.marginLeft = '-10px';
+ var animA = targetA.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ var animB = targetB.animate({ marginLeft: [ '0px', '100px' ] },
+ 50 * MS_PER_SEC);
+ animA.currentTime = 50 * MS_PER_SEC;
+ animB.currentTime = 10 * MS_PER_SEC;
+ assert_equals(getComputedStyle(targetA).marginLeft, '50px',
+ 'original margin-left of the first element');
+ assert_equals(getComputedStyle(targetB).marginLeft, '20px',
+ 'original margin-left of the second element');
+
+ animB.effect = animA.effect;
+ assert_equals(getComputedStyle(targetA).marginLeft, '10px',
+ 'new margin-left of the first element');
+ assert_equals(getComputedStyle(targetB).marginLeft, '-10px',
+ 'new margin-left of the second element');
+}, 'After setting target effect with an animation to another animation which ' +
+ 'also has an target effect and these animation effects target to ' +
+ 'different elements, the computed styles of the two elements should ' +
+ 'reflect the time and effect of the animation that was set');
+
+test(function(t) {
+ var target = addDiv(t);
+ var animA = target.animate({ marginLeft: [ '0px', '100px' ] },
+ 50 * MS_PER_SEC);
+ var animB = target.animate({ marginTop: [ '0px', '50px' ] },
+ 100 * MS_PER_SEC);
+ animA.currentTime = 20 * MS_PER_SEC;
+ animB.currentTime = 30 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '40px');
+ assert_equals(getComputedStyle(target).marginTop, '15px');
+
+ var effectA = animA.effect;
+ animA.effect = animB.effect;
+ animB.effect = effectA;
+ assert_equals(getComputedStyle(target).marginLeft, '60px');
+ assert_equals(getComputedStyle(target).marginTop, '10px');
+}, 'After swapping effects of two playing animations, both animations are ' +
+ 'still running with the same current time');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/style/file_animation-setting-spacing.html b/dom/animation/test/style/file_animation-setting-spacing.html
new file mode 100644
index 0000000000..6098b74336
--- /dev/null
+++ b/dom/animation/test/style/file_animation-setting-spacing.html
@@ -0,0 +1,111 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Tests for setting spacing by using KeyframeEffect.spacing</title>
+<script src='../testcommon.js'></script>
+</head>
+<body>
+<script>
+'use strict';
+
+function calculateInterpolation(pacedDistances, values, progress) {
+ if (progress == 0.0) {
+ return values[0];
+ } else if (progress == 1.0) {
+ return values[valus.length - 1];
+ }
+
+ const cumDist = pacedDistances.reduce( (prev, curr) => {
+ prev.push(prev.length == 0 ? curr : curr + prev[prev.length - 1]);
+ return prev;
+ }, []);
+
+ const last = cumDist[cumDist.length - 1];
+ const offsets = cumDist.map( (curr) => { return curr / last; } );
+
+ let idx = 0;
+ for (let i = 0; i < offsets.length - 1; ++i) {
+ if (progress >= offsets[i] && progress < offsets[i + 1]) {
+ idx = i;
+ break;
+ }
+ }
+
+ const ratio = (progress - offsets[idx]) / (offsets[idx + 1] - offsets[idx]);
+ return values[idx] + ratio * (values[idx + 1] - values[idx]) + 'px';
+}
+
+promise_test(function(t) {
+ var target = addDiv(t);
+ var anim = target.animate([ { marginLeft: '0px' },
+ { marginLeft: '-20px' },
+ { marginLeft: '100px' },
+ { marginLeft: '50px' } ],
+ 100 * MS_PER_SEC);
+
+ return anim.ready.then(function() {
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '40px',
+ 'computed value before setting a new spacing');
+
+ var dist = [0, 20, 120, 50];
+ var marginLeftValues = [0, -20, 100, 50];
+ anim.effect.spacing = 'paced(margin-left)';
+ assert_equals(getComputedStyle(target).marginLeft,
+ calculateInterpolation(dist, marginLeftValues, 0.5),
+ 'computed value after setting a new spacing');
+ });
+}, 'Test for setting spacing from distribute to paced');
+
+promise_test(function(t) {
+ var target = addDiv(t);
+ var anim = target.animate([ { marginLeft: '0px' },
+ { marginLeft: '-20px' },
+ { marginLeft: '100px' },
+ { marginLeft: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ return anim.ready.then(function() {
+ var dist = [0, 20, 120, 50];
+ var marginLeftValues = [0, -20, 100, 50];
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft,
+ calculateInterpolation(dist, marginLeftValues, 0.5),
+ 'computed value before setting a new spacing');
+
+ anim.effect.spacing = 'distribute';
+ assert_equals(getComputedStyle(target).marginLeft, '40px',
+ 'computed value after setting a new spacing');
+ });
+}, 'Test for setting spacing from paced to distribute');
+
+promise_test(function(t) {
+ var target = addDiv(t);
+ var anim =
+ target.animate([ { marginLeft: '0px', borderRadius: '0%' },
+ { marginLeft: '-20px', borderRadius: '50%' },
+ { marginLeft: '100px', borderRadius: '25%' },
+ { marginLeft: '50px', borderRadius: '100%' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ return anim.ready.then(function() {
+ var dist = [0, 20, 120, 50];
+ var marginLeftValues = [0, -20, 100, 50];
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft,
+ calculateInterpolation(dist, marginLeftValues, 0.5),
+ 'computed value before setting a new spacing');
+
+ dist = [0, 50, 25, 75];
+ anim.effect.spacing = 'paced(border-radius)';
+ assert_equals(getComputedStyle(target).marginLeft,
+ calculateInterpolation(dist, marginLeftValues, 0.5),
+ 'computed value after setting a new spacing');
+ });
+}, 'Test for setting spacing from paced to a different paced');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/style/test_animation-seeking-with-current-time.html b/dom/animation/test/style/test_animation-seeking-with-current-time.html
new file mode 100644
index 0000000000..386e577886
--- /dev/null
+++ b/dom/animation/test/style/test_animation-seeking-with-current-time.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-seeking-with-current-time.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/style/test_animation-seeking-with-start-time.html b/dom/animation/test/style/test_animation-seeking-with-start-time.html
new file mode 100644
index 0000000000..0a1691a082
--- /dev/null
+++ b/dom/animation/test/style/test_animation-seeking-with-start-time.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-seeking-with-start-time.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/style/test_animation-setting-effect.html b/dom/animation/test/style/test_animation-setting-effect.html
new file mode 100644
index 0000000000..1199b3e752
--- /dev/null
+++ b/dom/animation/test/style/test_animation-setting-effect.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-setting-effect.html');
+ });
+</script>
+</html>
diff --git a/dom/animation/test/style/test_animation-setting-spacing.html b/dom/animation/test/style/test_animation-setting-spacing.html
new file mode 100644
index 0000000000..1c703e2a3c
--- /dev/null
+++ b/dom/animation/test/style/test_animation-setting-spacing.html
@@ -0,0 +1,14 @@
+<!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-setting-spacing.html');
+ });
+</script>
diff --git a/dom/animation/test/testcommon.js b/dom/animation/test/testcommon.js
new file mode 100644
index 0000000000..b9001e4f42
--- /dev/null
+++ b/dom/animation/test/testcommon.js
@@ -0,0 +1,216 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Use this variable if you specify duration or some other properties
+ * for script animation.
+ * E.g., div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+ *
+ * NOTE: Creating animations with short duration may cause intermittent
+ * failures in asynchronous test. For example, the short duration animation
+ * might be finished when animation.ready has been fulfilled because of slow
+ * platforms or busyness of the main thread.
+ * Setting short duration to cancel its animation does not matter but
+ * if you don't want to cancel the animation, consider using longer duration.
+ */
+const MS_PER_SEC = 1000;
+
+/* The recommended minimum precision to use for time values[1].
+ *
+ * [1] https://w3c.github.io/web-animations/#precision-of-time-values
+ */
+var TIME_PRECISION = 0.0005; // ms
+
+/*
+ * Allow implementations to substitute an alternative method for comparing
+ * times based on their precision requirements.
+ */
+function assert_times_equal(actual, expected, description) {
+ assert_approx_equals(actual, expected, TIME_PRECISION, description);
+}
+
+/**
+ * Appends a div to the document body and creates an animation on the div.
+ * NOTE: This function asserts when trying to create animations with durations
+ * shorter than 100s because the shorter duration may cause intermittent
+ * failures. If you are not sure how long it is suitable, use 100s; it's
+ * long enough but shorter than our test framework timeout (330s).
+ * If you really need to use shorter durations, use animate() function directly.
+ *
+ * @param t The testharness.js Test object. If provided, this will be used
+ * to register a cleanup callback to remove the div when the test
+ * finishes.
+ * @param attrs A dictionary object with attribute names and values to set on
+ * the div.
+ * @param frames The keyframes passed to Element.animate().
+ * @param options The options passed to Element.animate().
+ */
+function addDivAndAnimate(t, attrs, frames, options) {
+ let animDur = (typeof options === 'object') ?
+ options.duration : options;
+ assert_greater_than_equal(animDur, 100 * MS_PER_SEC,
+ 'Clients of this addDivAndAnimate API must request a duration ' +
+ 'of at least 100s, to avoid intermittent failures from e.g.' +
+ 'the main thread being busy for an extended period');
+
+ return addDiv(t, attrs).animate(frames, options);
+}
+
+/**
+ * Appends a div to the document body.
+ *
+ * @param t The testharness.js Test object. If provided, this will be used
+ * to register a cleanup callback to remove the div when the test
+ * finishes.
+ *
+ * @param attrs A dictionary object with attribute names and values to set on
+ * the div.
+ */
+function addDiv(t, attrs) {
+ var div = document.createElement('div');
+ if (attrs) {
+ for (var attrName in attrs) {
+ div.setAttribute(attrName, attrs[attrName]);
+ }
+ }
+ document.body.appendChild(div);
+ if (t && typeof t.add_cleanup === 'function') {
+ t.add_cleanup(function() {
+ if (div.parentNode) {
+ div.parentNode.removeChild(div);
+ }
+ });
+ }
+ return div;
+}
+
+/**
+ * Appends a style div to the document head.
+ *
+ * @param t The testharness.js Test object. If provided, this will be used
+ * to register a cleanup callback to remove the style element
+ * when the test finishes.
+ *
+ * @param rules A dictionary object with selector names and rules to set on
+ * the style sheet.
+ */
+function addStyle(t, rules) {
+ var extraStyle = document.createElement('style');
+ document.head.appendChild(extraStyle);
+ if (rules) {
+ var sheet = extraStyle.sheet;
+ for (var selector in rules) {
+ sheet.insertRule(selector + '{' + rules[selector] + '}',
+ sheet.cssRules.length);
+ }
+ }
+
+ if (t && typeof t.add_cleanup === 'function') {
+ t.add_cleanup(function() {
+ extraStyle.remove();
+ });
+ }
+}
+
+/**
+ * Promise wrapper for requestAnimationFrame.
+ */
+function waitForFrame() {
+ return new Promise(function(resolve, reject) {
+ window.requestAnimationFrame(resolve);
+ });
+}
+
+/**
+ * Returns a Promise that is resolved after the given number of consecutive
+ * animation frames have occured (using requestAnimationFrame callbacks).
+ *
+ * @param frameCount The number of animation frames.
+ * @param onFrame An optional function to be processed in each animation frame.
+ */
+function waitForAnimationFrames(frameCount, onFrame) {
+ return new Promise(function(resolve, reject) {
+ function handleFrame() {
+ if (onFrame && typeof onFrame === 'function') {
+ onFrame();
+ }
+ if (--frameCount <= 0) {
+ resolve();
+ } else {
+ window.requestAnimationFrame(handleFrame); // wait another frame
+ }
+ }
+ window.requestAnimationFrame(handleFrame);
+ });
+}
+
+/**
+ * Wrapper that takes a sequence of N animations and returns:
+ *
+ * Promise.all([animations[0].ready, animations[1].ready, ... animations[N-1].ready]);
+ */
+function waitForAllAnimations(animations) {
+ return Promise.all(animations.map(function(animation) {
+ return animation.ready;
+ }));
+}
+
+/**
+ * Flush the computed style for the given element. This is useful, for example,
+ * when we are testing a transition and need the initial value of a property
+ * to be computed so that when we synchronouslyet set it to a different value
+ * we actually get a transition instead of that being the initial value.
+ */
+function flushComputedStyle(elem) {
+ var cs = window.getComputedStyle(elem);
+ cs.marginLeft;
+}
+
+if (opener) {
+ for (var funcName of ["async_test", "assert_not_equals", "assert_equals",
+ "assert_approx_equals", "assert_less_than",
+ "assert_less_than_equal", "assert_greater_than",
+ "assert_between_inclusive",
+ "assert_true", "assert_false",
+ "assert_class_string", "assert_throws",
+ "assert_unreached", "promise_test", "test"]) {
+ window[funcName] = opener[funcName].bind(opener);
+ }
+
+ window.EventWatcher = opener.EventWatcher;
+
+ function done() {
+ opener.add_completion_callback(function() {
+ self.close();
+ });
+ opener.done();
+ }
+}
+
+/**
+ * Return a new MutaionObserver which started observing |target| element
+ * with { animations: true, subtree: |subtree| } option.
+ * NOTE: This observer should be used only with takeRecords(). If any of
+ * MutationRecords are observed in the callback of the MutationObserver,
+ * it will raise an assertion.
+ */
+function setupSynchronousObserver(t, target, subtree) {
+ var observer = new MutationObserver(records => {
+ assert_unreached("Any MutationRecords should not be observed in this " +
+ "callback");
+ });
+ t.add_cleanup(() => {
+ observer.disconnect();
+ });
+ observer.observe(target, { animations: true, subtree: subtree });
+ return observer;
+}
+
+/**
+ * Returns true if off-main-thread animations.
+ */
+function isOMTAEnabled() {
+ const OMTAPrefKey = 'layers.offmainthreadcomposition.async-animations';
+ return SpecialPowers.DOMWindowUtils.layerManagerRemote &&
+ SpecialPowers.getBoolPref(OMTAPrefKey);
+}