summaryrefslogtreecommitdiffstats
path: root/dom
diff options
context:
space:
mode:
Diffstat (limited to 'dom')
-rw-r--r--dom/animation/Animation.cpp42
-rw-r--r--dom/animation/Animation.h10
-rw-r--r--dom/animation/AnimationEffectReadOnly.cpp18
-rw-r--r--dom/animation/ComputedTiming.h6
-rw-r--r--dom/animation/test/css-animations/file_event-dispatch.html252
-rw-r--r--dom/animation/test/css-animations/file_event-order.html160
-rw-r--r--dom/animation/test/css-animations/test_event-dispatch.html15
-rw-r--r--dom/animation/test/css-animations/test_event-order.html (renamed from dom/animation/test/css-transitions/test_csstransition-events.html)3
-rw-r--r--dom/animation/test/css-transitions/file_animation-cancel.html182
-rw-r--r--dom/animation/test/css-transitions/file_csstransition-events.html223
-rw-r--r--dom/animation/test/css-transitions/file_event-dispatch.html474
-rw-r--r--dom/animation/test/css-transitions/file_setting-effect.html9
-rw-r--r--dom/animation/test/css-transitions/test_event-dispatch.html14
-rw-r--r--dom/animation/test/mochitest.ini6
-rw-r--r--dom/base/nsGkAtomList.h2
-rw-r--r--dom/events/EventNameList.h8
-rw-r--r--dom/events/test/test_legacy_event.html21
-rw-r--r--dom/webidl/EventHandler.webidl8
18 files changed, 1138 insertions, 315 deletions
diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp
index 6dd583ed1..bd318f79e 100644
--- a/dom/animation/Animation.cpp
+++ b/dom/animation/Animation.cpp
@@ -230,6 +230,10 @@ Animation::SetTimelineNoUpdate(AnimationTimeline* aTimeline)
return;
}
+ StickyTimeDuration activeTime = mEffect
+ ? mEffect->GetComputedTiming().mActiveTime
+ : StickyTimeDuration();
+
RefPtr<AnimationTimeline> oldTimeline = mTimeline;
if (oldTimeline) {
oldTimeline->RemoveAnimation(this);
@@ -240,6 +244,9 @@ Animation::SetTimelineNoUpdate(AnimationTimeline* aTimeline)
mHoldTime.SetNull();
}
+ if (!aTimeline) {
+ MaybeQueueCancelEvent(activeTime);
+ }
UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
}
@@ -722,8 +729,10 @@ TimeStamp
Animation::ElapsedTimeToTimeStamp(
const StickyTimeDuration& aElapsedTime) const
{
- return AnimationTimeToTimeStamp(aElapsedTime +
- mEffect->SpecifiedTiming().mDelay);
+ TimeDuration delay = mEffect
+ ? mEffect->SpecifiedTiming().mDelay
+ : TimeDuration();
+ return AnimationTimeToTimeStamp(aElapsedTime + delay);
}
@@ -771,14 +780,28 @@ Animation::CancelNoUpdate()
DispatchPlaybackEvent(NS_LITERAL_STRING("cancel"));
+ StickyTimeDuration activeTime = mEffect
+ ? mEffect->GetComputedTiming().mActiveTime
+ : StickyTimeDuration();
+
mHoldTime.SetNull();
mStartTime.SetNull();
- UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
-
if (mTimeline) {
mTimeline->RemoveAnimation(this);
}
+ MaybeQueueCancelEvent(activeTime);
+
+ // When an animation is cancelled it no longer needs further ticks from the
+ // timeline. However, if we queued a cancel event and this was the last
+ // animation attached to the timeline, the timeline will stop observing the
+ // refresh driver and there may be no subsequent refresh driver tick for
+ // dispatching the queued event.
+ //
+ // By calling UpdateTiming *after* removing ourselves from our timeline, we
+ // ensure the timeline will register with the refresh driver for at least one
+ // more tick.
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
}
void
@@ -819,6 +842,17 @@ Animation::HasLowerCompositeOrderThan(const Animation& aOther) const
return thisTransition->HasLowerCompositeOrderThan(*otherTransition);
}
if (thisTransition || otherTransition) {
+ // Cancelled transitions no longer have an owning element. To be strictly
+ // correct we should store a strong reference to the owning element
+ // so that if we arrive here while sorting cancel events, we can sort
+ // them in the correct order.
+ //
+ // However, given that cancel events are almost always queued
+ // synchronously in some deterministic manner, we can be fairly sure
+ // that cancel events will be dispatched in a deterministic order
+ // (which is our only hard requirement until specs say otherwise).
+ // Furthermore, we only reach here when we have events with equal
+ // timestamps so this is an edge case we can probably ignore for now.
return thisTransition;
}
}
diff --git a/dom/animation/Animation.h b/dom/animation/Animation.h
index c59d7d6ce..3263b30c4 100644
--- a/dom/animation/Animation.h
+++ b/dom/animation/Animation.h
@@ -326,6 +326,16 @@ public:
void NotifyEffectTimingUpdated();
+ /**
+ * Used by subclasses to synchronously queue a cancel event in situations
+ * where the Animation may have been cancelled.
+ *
+ * We need to do this synchronously because after a CSS animation/transition
+ * is canceled, it will be released by its owning element and may not still
+ * exist when we would normally go to queue events on the next tick.
+ */
+ virtual void MaybeQueueCancelEvent(StickyTimeDuration aActiveTime) {};
+
protected:
void SilentlySetCurrentTime(const TimeDuration& aNewCurrentTime);
void SilentlySetPlaybackRate(double aPlaybackRate);
diff --git a/dom/animation/AnimationEffectReadOnly.cpp b/dom/animation/AnimationEffectReadOnly.cpp
index aff28a37b..bf2e2197d 100644
--- a/dom/animation/AnimationEffectReadOnly.cpp
+++ b/dom/animation/AnimationEffectReadOnly.cpp
@@ -127,10 +127,6 @@ AnimationEffectReadOnly::GetComputedTimingAt(
}
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);
@@ -148,7 +144,7 @@ AnimationEffectReadOnly::GetComputedTimingAt(
// The animation isn't active or filling at this time.
return result;
}
- activeTime =
+ result.mActiveTime =
std::max(std::min(StickyTimeDuration(localTime - aTiming.mDelay),
result.mActiveDuration),
zeroDuration);
@@ -159,13 +155,14 @@ AnimationEffectReadOnly::GetComputedTimingAt(
// The animation isn't active or filling at this time.
return result;
}
- activeTime = std::max(StickyTimeDuration(localTime - aTiming.mDelay),
- zeroDuration);
+ result.mActiveTime
+ = 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;
+ result.mActiveTime = localTime - aTiming.mDelay;
}
// Convert active time to a multiple of iterations.
@@ -176,7 +173,7 @@ AnimationEffectReadOnly::GetComputedTimingAt(
? 0.0
: result.mIterations;
} else {
- overallProgress = activeTime / result.mDuration;
+ overallProgress = result.mActiveTime / result.mDuration;
}
// Factor in iteration start offset.
@@ -208,7 +205,8 @@ AnimationEffectReadOnly::GetComputedTimingAt(
if (result.mPhase == ComputedTiming::AnimationPhase::After &&
progress == 0.0 &&
result.mIterations != 0.0 &&
- (activeTime != zeroDuration || result.mDuration == zeroDuration)) {
+ (result.mActiveTime != 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
diff --git a/dom/animation/ComputedTiming.h b/dom/animation/ComputedTiming.h
index 4a98e3933..b1c6674a5 100644
--- a/dom/animation/ComputedTiming.h
+++ b/dom/animation/ComputedTiming.h
@@ -29,6 +29,8 @@ struct ComputedTiming
// Will equal StickyTimeDuration::Forever() if the animation repeats
// indefinitely.
StickyTimeDuration mActiveDuration;
+ // The time within the active interval.
+ StickyTimeDuration mActiveTime;
// 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.
@@ -62,12 +64,12 @@ struct ComputedTiming
}
enum class AnimationPhase {
- Null, // Not sampled (null sample time)
+ Idle, // 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;
+ AnimationPhase mPhase = AnimationPhase::Idle;
ComputedTimingFunction::BeforeFlag mBeforeFlag =
ComputedTimingFunction::BeforeFlag::Unset;
diff --git a/dom/animation/test/css-animations/file_event-dispatch.html b/dom/animation/test/css-animations/file_event-dispatch.html
new file mode 100644
index 000000000..266205bc3
--- /dev/null
+++ b/dom/animation/test/css-animations/file_event-dispatch.html
@@ -0,0 +1,252 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Tests for CSS animation event dispatch</title>
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#event-dispatch"/>
+<script src="../testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { margin-left: 0px; }
+ to { margin-left: 100px; }
+ }
+</style>
+<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 AnimationEventHandler(target) {
+ this.target = target;
+ this.target.onanimationstart = function(evt) {
+ this.animationstart = evt.elapsedTime;
+ }.bind(this);
+ this.target.onanimationiteration = function(evt) {
+ this.animationiteration = evt.elapsedTime;
+ }.bind(this);
+ this.target.onanimationend = function(evt) {
+ this.animationend = evt.elapsedTime;
+ }.bind(this);
+}
+AnimationEventHandler.prototype.clear = function() {
+ this.animationstart = undefined;
+ this.animationiteration = undefined;
+ this.animationend = undefined;
+}
+
+function setupAnimation(t, animationStyle) {
+ var div = addDiv(t, { style: "animation: " + animationStyle });
+ var watcher = new EventWatcher(t, div, [ 'animationstart',
+ 'animationiteration',
+ 'animationend' ]);
+ var handler = new AnimationEventHandler(div);
+ var animation = div.getAnimations()[0];
+
+ return [animation, watcher, handler, div];
+}
+
+promise_test(function(t) {
+ // Add 1ms delay to ensure that the delay is not included in the elapsedTime.
+ const [animation, watcher] = setupAnimation(t, 'anim 100s 1ms');
+
+ return watcher.wait_for('animationstart').then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Idle -> Active');
+
+promise_test(function(t) {
+ const [animation, watcher, handler] = setupAnimation(t, 'anim 100s');
+
+ // Seek to After phase.
+ animation.finish();
+ return watcher.wait_for([ 'animationstart',
+ 'animationend' ]).then(function() {
+ assert_equals(handler.animationstart, 0.0);
+ assert_equals(handler.animationend, 100);
+ });
+}, 'Idle -> After');
+
+promise_test(function(t) {
+ const [animation, watcher, handler] =
+ setupAnimation(t, 'anim 100s 100s paused');
+
+ return animation.ready.then(function() {
+ // Seek to Active phase.
+ animation.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for('animationstart');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Before -> Active');
+
+promise_test(function(t) {
+ const [animation, watcher, handler] =
+ setupAnimation(t, 'anim 100s 100s paused');
+
+ return animation.ready.then(function() {
+ // Seek to After phase.
+ animation.finish();
+ return watcher.wait_for([ 'animationstart', 'animationend' ]);
+ }).then(function(evt) {
+ assert_equals(handler.animationstart, 0.0);
+ assert_equals(handler.animationend, 100.0);
+ });
+}, 'Before -> After');
+
+promise_test(function(t) {
+ const [animation, watcher, handler] =
+ setupAnimation(t, 'anim 100s 100s paused');
+
+ // Seek to Active phase.
+ animation.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for('animationstart').then(function() {
+ // Seek to Before phase.
+ animation.currentTime = 0;
+ return watcher.wait_for('animationend');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Active -> Before');
+
+promise_test(function(t) {
+ const [animation, watcher, handler] = setupAnimation(t, 'anim 100s paused');
+
+ return watcher.wait_for('animationstart').then(function(evt) {
+ // Seek to After phase.
+ animation.finish();
+ return watcher.wait_for('animationend');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 100.0);
+ });
+}, 'Active -> After');
+
+promise_test(function(t) {
+ const [animation, watcher, handler] =
+ setupAnimation(t, 'anim 100s 100s paused');
+
+ // Seek to After phase.
+ animation.finish();
+ return watcher.wait_for([ 'animationstart',
+ 'animationend' ]).then(function() {
+ // Seek to Before phase.
+ animation.currentTime = 0;
+ handler.clear();
+ return watcher.wait_for([ 'animationstart', 'animationend' ]);
+ }).then(function() {
+ assert_equals(handler.animationstart, 100.0);
+ assert_equals(handler.animationend, 0.0);
+ });
+}, 'After -> Before');
+
+promise_test(function(t) {
+ const [animation, watcher, handler] =
+ setupAnimation(t, 'anim 100s 100s paused');
+
+ // Seek to After phase.
+ animation.finish();
+ return watcher.wait_for([ 'animationstart',
+ 'animationend' ]).then(function() {
+ // Seek to Active phase.
+ animation.currentTime = 100 * MS_PER_SEC;
+ handler.clear();
+ return watcher.wait_for('animationstart');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 100.0);
+ });
+}, 'After -> Active');
+
+promise_test(function(t) {
+ const [animation, watcher, handler]
+ = setupAnimation(t, 'anim 100s 100s 3 paused');
+
+ return animation.ready.then(function() {
+ // Seek to iteration 0 (no animationiteration event should be dispatched)
+ animation.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for('animationstart');
+ }).then(function(evt) {
+ // Seek to iteration 2
+ animation.currentTime = 300 * MS_PER_SEC;
+ handler.clear();
+ return watcher.wait_for('animationiteration');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 200);
+ // Seek to After phase (no animationiteration event should be dispatched)
+ animation.currentTime = 400 * MS_PER_SEC;
+ return watcher.wait_for('animationend');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 300);
+ });
+}, 'Active -> Active (forwards)');
+
+promise_test(function(t) {
+ const [animation, watcher, handler] = setupAnimation(t, 'anim 100s 100s 3');
+
+ // Seek to After phase.
+ animation.finish();
+ return watcher.wait_for([ 'animationstart',
+ 'animationend' ]).then(function() {
+ // Seek to iteration 2 (no animationiteration event should be dispatched)
+ animation.pause();
+ animation.currentTime = 300 * MS_PER_SEC;
+ return watcher.wait_for('animationstart');
+ }).then(function() {
+ // Seek to mid of iteration 0 phase.
+ animation.currentTime = 200 * MS_PER_SEC;
+ return watcher.wait_for('animationiteration');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 200.0);
+ // Seek to before phase (no animationiteration event should be dispatched)
+ animation.currentTime = 0;
+ return watcher.wait_for('animationend');
+ });
+}, 'Active -> Active (backwards)');
+
+promise_test(function(t) {
+ const [animation, watcher, handler, div] =
+ setupAnimation(t, 'anim 100s paused');
+ return watcher.wait_for('animationstart').then(function(evt) {
+ // Seek to Idle phase.
+ div.style.display = 'none';
+ flushComputedStyle(div);
+
+ // FIXME: bug 1302648: Add test for animationcancel event here.
+
+ // Restart this animation.
+ div.style.display = '';
+ return watcher.wait_for('animationstart');
+ });
+}, 'Active -> Idle -> Active: animationstart is fired by restarting animation');
+
+promise_test(function(t) {
+ const [animation, watcher, handler, div] =
+ setupAnimation(t, 'anim 100s 100s 2 paused');
+
+ // Make After.
+ animation.finish();
+ return watcher.wait_for([ 'animationstart',
+ 'animationend' ]).then(function(evt) {
+ animation.playbackRate = -1;
+ return watcher.wait_for('animationstart');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 200);
+ // Seek to 1st iteration
+ animation.currentTime = 200 * MS_PER_SEC - 1;
+ return watcher.wait_for('animationiteration');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 100);
+ // Seek to before
+ animation.currentTime = 100 * MS_PER_SEC - 1;
+ return watcher.wait_for('animationend');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 0);
+ assert_equals(animation.playState, 'running'); // delay
+ });
+}, 'Negative playbackRate sanity test(Before -> Active -> Before)');
+
+done();
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/css-animations/file_event-order.html b/dom/animation/test/css-animations/file_event-order.html
new file mode 100644
index 000000000..da78b6541
--- /dev/null
+++ b/dom/animation/test/css-animations/file_event-order.html
@@ -0,0 +1,160 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Tests for CSS animation event order</title>
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#event-dispatch"/>
+<script src="../testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { margin-left: 0px; }
+ to { margin-left: 100px; }
+ }
+</style>
+<body>
+<script type='text/javascript'>
+'use strict';
+
+/**
+ * Asserts that the set of actual and received events match.
+ * @param actualEvents An array of the received AnimationEvent objects.
+ * @param expectedEvents A series of array objects representing the expected
+ * events, each having the form:
+ * [ event type, target element, elapsed time ]
+ */
+function checkEvents(actualEvents, ...expectedEvents) {
+ assert_equals(actualEvents.length, expectedEvents.length,
+ `Number of actual events (${actualEvents.length}: \
+${actualEvents.map(event => event.type).join(', ')}) should match expected \
+events (${expectedEvents.map(event => event.type).join(', ')})`);
+
+ actualEvents.forEach((actualEvent, i) => {
+ assert_equals(expectedEvents[i][0], actualEvent.type,
+ 'Event type should match');
+ assert_equals(expectedEvents[i][1], actualEvent.target,
+ 'Event target should match');
+ assert_equals(expectedEvents[i][2], actualEvent.elapsedTime,
+ 'Event\'s elapsed time should match');
+ });
+}
+
+function setupAnimation(t, animationStyle, receiveEvents) {
+ const div = addDiv(t, { style: "animation: " + animationStyle });
+ const watcher = new EventWatcher(t, div, [ 'animationstart',
+ 'animationiteration',
+ 'animationend' ]);
+
+ ['start', 'iteration', 'end'].forEach(name => {
+ div['onanimation' + name] = function(evt) {
+ receiveEvents.push({ type: evt.type,
+ target: evt.target,
+ elapsedTime: evt.elapsedTime });
+ }.bind(this);
+ });
+
+ const animation = div.getAnimations()[0];
+
+ return [animation, watcher, div];
+}
+
+promise_test(function(t) {
+ let events = [];
+ const [animation1, watcher1, div1] =
+ setupAnimation(t, 'anim 100s 2 paused', events);
+ const [animation2, watcher2, div2] =
+ setupAnimation(t, 'anim 100s 2 paused', events);
+
+ return Promise.all([ watcher1.wait_for('animationstart'),
+ watcher2.wait_for('animationstart') ]).then(function() {
+ checkEvents(events, ['animationstart', div1, 0],
+ ['animationstart', div2, 0]);
+
+ events.length = 0; // Clear received event array
+
+ animation1.currentTime = 100 * MS_PER_SEC;
+ animation2.currentTime = 100 * MS_PER_SEC;
+ return Promise.all([ watcher1.wait_for('animationiteration'),
+ watcher2.wait_for('animationiteration') ]);
+ }).then(function() {
+ checkEvents(events, ['animationiteration', div1, 100],
+ ['animationiteration', div2, 100]);
+
+ events.length = 0; // Clear received event array
+
+ animation1.finish();
+ animation2.finish();
+
+ return Promise.all([ watcher1.wait_for('animationend'),
+ watcher2.wait_for('animationend') ]);
+ }).then(function() {
+ checkEvents(events, ['animationend', div1, 200],
+ ['animationend', div2, 200]);
+ });
+}, 'Test same events are ordered by elements.');
+
+promise_test(function(t) {
+ let events = [];
+ const [animation1, watcher1, div1] =
+ setupAnimation(t, 'anim 200s 400s', events);
+ const [animation2, watcher2, div2] =
+ setupAnimation(t, 'anim 300s 2', events);
+
+ return watcher2.wait_for('animationstart').then(function(evt) {
+ animation1.currentTime = 400 * MS_PER_SEC;
+ animation2.currentTime = 400 * MS_PER_SEC;
+
+ events.length = 0; // Clear received event array
+
+ return Promise.all([ watcher1.wait_for('animationstart'),
+ watcher2.wait_for('animationiteration') ]);
+ }).then(function() {
+ checkEvents(events, ['animationiteration', div2, 300],
+ ['animationstart', div1, 0]);
+ });
+}, 'Test start and iteration events are ordered by time.');
+
+promise_test(function(t) {
+ let events = [];
+ const [animation1, watcher1, div1] =
+ setupAnimation(t, 'anim 150s', events);
+ const [animation2, watcher2, div2] =
+ setupAnimation(t, 'anim 100s 2', events);
+
+ return Promise.all([ watcher1.wait_for('animationstart'),
+ watcher2.wait_for('animationstart') ]).then(function() {
+ animation1.currentTime = 150 * MS_PER_SEC;
+ animation2.currentTime = 150 * MS_PER_SEC;
+
+ events.length = 0; // Clear received event array
+
+ return Promise.all([ watcher1.wait_for('animationend'),
+ watcher2.wait_for('animationiteration') ]);
+ }).then(function() {
+ checkEvents(events, ['animationiteration', div2, 100],
+ ['animationend', div1, 150]);
+ });
+}, 'Test iteration and end events are ordered by time.');
+
+promise_test(function(t) {
+ let events = [];
+ const [animation1, watcher1, div1] =
+ setupAnimation(t, 'anim 100s 100s', events);
+ const [animation2, watcher2, div2] =
+ setupAnimation(t, 'anim 100s 2', events);
+
+ animation1.finish();
+ animation2.finish();
+
+ return Promise.all([ watcher1.wait_for([ 'animationstart',
+ 'animationend' ]),
+ watcher2.wait_for([ 'animationstart',
+ 'animationend' ]) ]).then(function() {
+ checkEvents(events, ['animationstart', div2, 0],
+ ['animationstart', div1, 0],
+ ['animationend', div1, 100],
+ ['animationend', div2, 200]);
+ });
+}, 'Test start and end events are sorted correctly when fired simultaneously');
+
+done();
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/css-animations/test_event-dispatch.html b/dom/animation/test/css-animations/test_event-dispatch.html
new file mode 100644
index 000000000..de3be0301
--- /dev/null
+++ b/dom/animation/test/css-animations/test_event-dispatch.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_event-dispatch.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-transitions/test_csstransition-events.html b/dom/animation/test/css-animations/test_event-order.html
index 92559ad67..57f1f3876 100644
--- a/dom/animation/test/css-transitions/test_csstransition-events.html
+++ b/dom/animation/test/css-animations/test_event-order.html
@@ -9,6 +9,7 @@ setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
- window.open("file_csstransition-events.html");
+ window.open("file_event-order.html");
});
</script>
+</html>
diff --git a/dom/animation/test/css-transitions/file_animation-cancel.html b/dom/animation/test/css-transitions/file_animation-cancel.html
index 6094b383f..71f02fb11 100644
--- a/dom/animation/test/css-transitions/file_animation-cancel.html
+++ b/dom/animation/test/css-transitions/file_animation-cancel.html
@@ -11,13 +11,12 @@ promise_test(function(t) {
div.style.transition = 'margin-left 100s';
div.style.marginLeft = '1000px';
- flushComputedStyle(div);
- var animation = div.getAnimations()[0];
- return animation.ready.then(waitForFrame).then(function() {
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(waitForFrame).then(function() {
assert_not_equals(getComputedStyle(div).marginLeft, '1000px',
'transform style is animated before cancelling');
- animation.cancel();
+ transition.cancel();
assert_equals(getComputedStyle(div).marginLeft, div.style.marginLeft,
'transform style is no longer animated after cancelling');
});
@@ -29,45 +28,21 @@ promise_test(function(t) {
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();
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ transition.cancel();
assert_equals(getComputedStyle(div).marginLeft, '1000px',
'margin-left style is not animated after cancelling');
- animation.play();
+ transition.play();
assert_equals(getComputedStyle(div).marginLeft, '0px',
'margin-left style is animated after re-starting transition');
- return animation.ready;
+ return transition.ready;
}).then(function() {
- assert_equals(animation.playState, 'running',
+ assert_equals(transition.playState, 'running',
'Transition succeeds in running after being re-started');
});
-}, 'After cancelling a transition, it can still be re-used');
+}, 'After canceling a transition, it can still be re-used');
promise_test(function(t) {
var div = addDiv(t, { style: 'margin-left: 0px' });
@@ -75,20 +50,19 @@ promise_test(function(t) {
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();
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ transition.finish();
+ transition.cancel();
assert_equals(getComputedStyle(div).marginLeft, '1000px',
'margin-left style is not animated after cancelling');
- animation.play();
+ transition.play();
assert_equals(getComputedStyle(div).marginLeft, '0px',
'margin-left style is animated after re-starting transition');
- return animation.ready;
+ return transition.ready;
}).then(function() {
- assert_equals(animation.playState, 'running',
+ assert_equals(transition.playState, 'running',
'Transition succeeds in running after being re-started');
});
}, 'After cancelling a finished transition, it can still be re-used');
@@ -99,10 +73,9 @@ test(function(t) {
div.style.transition = 'margin-left 100s';
div.style.marginLeft = '1000px';
- flushComputedStyle(div);
- var animation = div.getAnimations()[0];
- animation.cancel();
+ var transition = div.getAnimations()[0];
+ transition.cancel();
assert_equals(getComputedStyle(div).marginLeft, '1000px',
'margin-left style is not animated after cancelling');
@@ -113,7 +86,7 @@ test(function(t) {
assert_equals(getComputedStyle(div).marginLeft, '1000px',
'margin-left style is still not animated after updating'
+ ' transition-duration');
- assert_equals(animation.playState, 'idle',
+ assert_equals(transition.playState, 'idle',
'Transition is still idle after updating transition-duration');
}, 'After cancelling a transition, updating transition properties doesn\'t make'
+ ' it live again');
@@ -124,15 +97,14 @@ promise_test(function(t) {
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');
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ assert_equals(transition.playState, 'running');
div.style.display = 'none';
return waitForFrame();
}).then(function() {
- assert_equals(animation.playState, 'idle');
+ assert_equals(transition.playState, 'idle');
assert_equals(getComputedStyle(div).marginLeft, '1000px');
});
}, 'Setting display:none on an element cancels its transitions');
@@ -147,19 +119,115 @@ promise_test(function(t) {
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');
+ var transition = childDiv.getAnimations()[0];
+ return transition.ready.then(function() {
+ assert_equals(transition.playState, 'running');
parentDiv.style.display = 'none';
return waitForFrame();
}).then(function() {
- assert_equals(animation.playState, 'idle');
+ assert_equals(transition.playState, 'idle');
assert_equals(getComputedStyle(childDiv).marginLeft, '1000px');
});
}, 'Setting display:none cancels transitions on a child element');
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ assert_equals(transition.playState, 'running');
+ // Set an unrecognized property value
+ div.style.transitionProperty = 'none';
+ flushComputedStyle(div);
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(transition.playState, 'idle');
+ assert_equals(getComputedStyle(div).marginLeft, '1000px');
+ });
+}, 'Removing a property from transition-property cancels transitions on that '+
+ 'property');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ assert_equals(transition.playState, 'running');
+ div.style.transition = 'margin-top 10s -10s'; // combined duration is zero
+ flushComputedStyle(div);
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(transition.playState, 'idle');
+ assert_equals(getComputedStyle(div).marginLeft, '1000px');
+ });
+}, 'Setting zero combined duration');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ assert_equals(transition.playState, 'running');
+ div.style.marginLeft = '2000px';
+ flushComputedStyle(div);
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(transition.playState, 'idle');
+ });
+}, 'Changing style to another interpolable value cancels the original ' +
+ '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';
+
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ assert_equals(transition.playState, 'running');
+ div.style.marginLeft = 'auto';
+ flushComputedStyle(div);
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(div.getAnimations().length, 0,
+ 'There should be no transitions');
+ assert_equals(transition.playState, 'idle');
+ });
+}, 'An after-change style value can\'t be interpolated');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ assert_equals(transition.playState, 'running');
+ div.style.marginLeft = '0px';
+ flushComputedStyle(div);
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(transition.playState, 'idle');
+ });
+}, 'Reversing a running transition cancels the original transition');
+
done();
</script>
</body>
diff --git a/dom/animation/test/css-transitions/file_csstransition-events.html b/dom/animation/test/css-transitions/file_csstransition-events.html
deleted file mode 100644
index 5011bc130..000000000
--- a/dom/animation/test/css-transitions/file_csstransition-events.html
+++ /dev/null
@@ -1,223 +0,0 @@
-<!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_event-dispatch.html b/dom/animation/test/css-transitions/file_event-dispatch.html
new file mode 100644
index 000000000..7140cda36
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_event-dispatch.html
@@ -0,0 +1,474 @@
+<!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);
+ this.target.ontransitioncancel = function(evt) {
+ this.transitioncancel = evt.elapsedTime;
+ }.bind(this);
+}
+
+TransitionEventHandler.prototype.clear = function() {
+ this.transitionrun = undefined;
+ this.transitionstart = undefined;
+ this.transitionend = undefined;
+ this.transitioncancel = undefined;
+};
+
+function setupTransition(t, transitionStyle) {
+ var div = addDiv(t, { style: 'transition: ' + transitionStyle });
+ var watcher = new EventWatcher(t, div, [ 'transitionrun',
+ 'transitionstart',
+ 'transitionend',
+ 'transitioncancel' ]);
+ flushComputedStyle(div);
+
+ div.style.marginLeft = '100px';
+ var transition = div.getAnimations()[0];
+
+ return [transition, watcher, div];
+}
+
+// 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] =
+ setupTransition(t, 'margin-left 100s 100s');
+ 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] =
+ setupTransition(t, 'margin-left 100s 100s');
+ // 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, div] =
+ setupTransition(t, 'margin-left 100s 100s');
+ var handler = new TransitionEventHandler(div);
+
+ // 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, div] =
+ setupTransition(t, 'margin-left 100s 100s');
+ var handler = new TransitionEventHandler(div);
+
+ // 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, div] =
+ setupTransition(t, 'margin-left 100s 100s');
+
+ return Promise.all([ watcher.wait_for('transitionrun'),
+ transition.ready ]).then(function() {
+ // Make idle
+ div.style.display = 'none';
+ flushComputedStyle(div);
+ return watcher.wait_for('transitioncancel');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Before -> Idle (display: none)');
+
+promise_test(function(t) {
+ var [transition, watcher] =
+ setupTransition(t, 'margin-left 100s 100s');
+
+ return Promise.all([ watcher.wait_for('transitionrun'),
+ transition.ready ]).then(function() {
+ // Make idle
+ transition.timeline = null;
+ return watcher.wait_for('transitioncancel');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Before -> Idle (Animation.timeline = null)');
+
+promise_test(function(t) {
+ var [transition, watcher] =
+ setupTransition(t, 'margin-left 100s 100s');
+
+ return Promise.all([ watcher.wait_for('transitionrun'),
+ transition.ready ]).then(function() {
+ transition.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for('transitionstart');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Before -> Active');
+
+promise_test(function(t) {
+ var [transition, watcher, div] =
+ setupTransition(t, 'margin-left 100s 100s');
+ var handler = new TransitionEventHandler(div);
+
+ 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, div] =
+ setupTransition(t, 'margin-left 100s');
+
+ // Seek to Active start position.
+ transition.pause();
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ // Make idle
+ div.style.display = 'none';
+ flushComputedStyle(div);
+ return watcher.wait_for('transitioncancel');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Active -> Idle, no delay (display: none)');
+
+promise_test(function(t) {
+ var [transition, watcher] =
+ setupTransition(t, 'margin-left 100s');
+
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ // Make idle
+ transition.currentTime = 0;
+ transition.timeline = null;
+ return watcher.wait_for('transitioncancel');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Active -> Idle, no delay (Animation.timeline = null)');
+
+promise_test(function(t) {
+ var [transition, watcher, div] =
+ setupTransition(t, 'margin-left 100s 100s');
+ // Pause so the currentTime is fixed and we can accurately compare the event
+ // time in transition cancel events.
+ transition.pause();
+
+ // Seek to Active phase.
+ transition.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ // Make idle
+ div.style.display = 'none';
+ flushComputedStyle(div);
+ return watcher.wait_for('transitioncancel');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Active -> Idle, with positive delay (display: none)');
+
+promise_test(function(t) {
+ var [transition, watcher] =
+ setupTransition(t, 'margin-left 100s 100s');
+
+ // Seek to Active phase.
+ transition.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ // Make idle
+ transition.currentTime = 100 * MS_PER_SEC;
+ transition.timeline = null;
+ return watcher.wait_for('transitioncancel');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Active -> Idle, with positive delay (Animation.timeline = null)');
+
+promise_test(function(t) {
+ var [transition, watcher, div] =
+ setupTransition(t, 'margin-left 100s -50s');
+
+ // Pause so the currentTime is fixed and we can accurately compare the event
+ // time in transition cancel events.
+ transition.pause();
+
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ // Make idle
+ div.style.display = 'none';
+ flushComputedStyle(div);
+ return watcher.wait_for('transitioncancel');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 50.0);
+ });
+}, 'Active -> Idle, with negative delay (display: none)');
+
+promise_test(function(t) {
+ var [transition, watcher] =
+ setupTransition(t, 'margin-left 100s -50s');
+
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ // Make idle
+ transition.currentTime = 50 * MS_PER_SEC;
+ transition.timeline = null;
+ return watcher.wait_for('transitioncancel');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Active -> Idle, with negative delay (Animation.timeline = null)');
+
+promise_test(function(t) {
+ var [transition, watcher] =
+ setupTransition(t, 'margin-left 100s 100s');
+ // 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] =
+ setupTransition(t, 'margin-left 100s 100s');
+ // 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, div] =
+ setupTransition(t, 'margin-left 100s 100s');
+ var handler = new TransitionEventHandler(div);
+
+ // 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] =
+ setupTransition(t, 'margin-left 100s 100s');
+ // 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, div] =
+ setupTransition(t, 'margin-left 100s -50s');
+ var handler = new TransitionEventHandler(div);
+
+ 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, div] =
+ setupTransition(t, 'margin-left 100s 100s');
+ var handler = new TransitionEventHandler(div);
+
+ 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(div,
+ { 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([ 'transitioncancel',
+ 'transitionrun',
+ '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.');
+
+promise_test(function(t) {
+ var [transition, watcher, div] =
+ setupTransition(t, 'margin-left 100s 100s');
+
+ return watcher.wait_for('transitionrun').then(function() {
+ // Make idle
+ div.style.display = 'none';
+ flushComputedStyle(div);
+ return watcher.wait_for('transitioncancel');
+ }).then(function() {
+ transition.cancel();
+ // Then wait a couple of frames and check that no event was dispatched
+ return waitForAnimationFrames(2);
+ });
+}, 'Call Animation.cancel after cancelling transition.');
+
+promise_test(function(t) {
+ var [transition, watcher, div] =
+ setupTransition(t, 'margin-left 100s 100s');
+
+ return watcher.wait_for('transitionrun').then(function(evt) {
+ // Make idle
+ div.style.display = 'none';
+ flushComputedStyle(div);
+ transition.play();
+ watcher.wait_for([ 'transitioncancel',
+ 'transitionrun',
+ 'transitionstart' ]);
+ });
+}, 'Restart transition after cancelling transition immediately');
+
+promise_test(function(t) {
+ var [transition, watcher, div] =
+ setupTransition(t, 'margin-left 100s 100s');
+
+ return watcher.wait_for('transitionrun').then(function(evt) {
+ // Make idle
+ div.style.display = 'none';
+ flushComputedStyle(div);
+ transition.play();
+ transition.cancel();
+ return watcher.wait_for('transitioncancel');
+ }).then(function(evt) {
+ // Then wait a couple of frames and check that no event was dispatched
+ return waitForAnimationFrames(2);
+ });
+}, 'Call Animation.cancel after restarting transition immediately');
+
+promise_test(function(t) {
+ var [transition, watcher] =
+ setupTransition(t, 'margin-left 100s');
+
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ // Make idle
+ transition.timeline = null;
+ return watcher.wait_for('transitioncancel');
+ }).then(function(evt) {
+ transition.timeline = document.timeline;
+ transition.play();
+
+ return watcher.wait_for(['transitionrun', 'transitionstart']);
+ });
+}, 'Set timeline and play transition after clear the timeline');
+
+promise_test(function(t) {
+ var [transition, watcher, div] =
+ setupTransition(t, 'margin-left 100s');
+
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function() {
+ transition.cancel();
+ return watcher.wait_for('transitioncancel');
+ }).then(function() {
+ // Make After phase
+ transition.effect = null;
+
+ // Then wait a couple of frames and check that no event was dispatched
+ return waitForAnimationFrames(2);
+ });
+}, 'Set null target effect after cancel the transition');
+
+promise_test(function(t) {
+ var [transition, watcher, div] =
+ setupTransition(t, 'margin-left 100s');
+
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ transition.effect = null;
+ return watcher.wait_for('transitionend');
+ }).then(function(evt) {
+ transition.cancel();
+ return watcher.wait_for('transitioncancel');
+ });
+}, 'Cancel the transition after clearing the target effect');
+
+done();
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/css-transitions/file_setting-effect.html b/dom/animation/test/css-transitions/file_setting-effect.html
index c61877194..81279ea55 100644
--- a/dom/animation/test/css-transitions/file_setting-effect.html
+++ b/dom/animation/test/css-transitions/file_setting-effect.html
@@ -7,6 +7,8 @@
promise_test(function(t) {
var div = addDiv(t);
+ var watcher = new EventWatcher(t, div, [ 'transitionend',
+ 'transitioncancel' ]);
div.style.left = '0px';
div.style.transition = 'left 100s';
@@ -20,11 +22,14 @@ promise_test(function(t) {
assert_equals(transition.transitionProperty, 'left');
assert_equals(transition.playState, 'finished');
assert_equals(window.getComputedStyle(div).left, '100px');
+ return watcher.wait_for('transitionend');
});
}, 'Test for removing a transition effect');
promise_test(function(t) {
var div = addDiv(t);
+ var watcher = new EventWatcher(t, div, [ 'transitionend',
+ 'transitioncancel' ]);
div.style.left = '0px';
div.style.transition = 'left 100s';
@@ -46,6 +51,8 @@ promise_test(function(t) {
promise_test(function(t) {
var div = addDiv(t);
+ var watcher = new EventWatcher(t, div, [ 'transitionend',
+ 'transitioncancel' ]);
div.style.left = '0px';
div.style.width = '0px';
@@ -65,6 +72,8 @@ promise_test(function(t) {
promise_test(function(t) {
var div = addDiv(t);
+ var watcher = new EventWatcher(t, div, [ 'transitionend',
+ 'transitioncancel' ]);
div.style.left = '0px';
div.style.width = '0px';
diff --git a/dom/animation/test/css-transitions/test_event-dispatch.html b/dom/animation/test/css-transitions/test_event-dispatch.html
new file mode 100644
index 000000000..c90431cd1
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_event-dispatch.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_event-dispatch.html");
+ });
+</script>
diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini
index feb424518..db6dffada 100644
--- a/dom/animation/test/mochitest.ini
+++ b/dom/animation/test/mochitest.ini
@@ -19,6 +19,8 @@ support-files =
css-animations/file_document-get-animations.html
css-animations/file_effect-target.html
css-animations/file_element-get-animations.html
+ css-animations/file_event-dispatch.html
+ css-animations/file_event-order.html
css-animations/file_keyframeeffect-getkeyframes.html
css-animations/file_pseudoElement-get-animations.html
css-transitions/file_animation-cancel.html
@@ -32,6 +34,7 @@ support-files =
css-transitions/file_document-get-animations.html
css-transitions/file_effect-target.html
css-transitions/file_element-get-animations.html
+ css-transitions/file_event-dispatch.html
css-transitions/file_keyframeeffect-getkeyframes.html
css-transitions/file_pseudoElement-get-animations.html
css-transitions/file_setting-effect.html
@@ -72,6 +75,8 @@ support-files =
[css-animations/test_document-get-animations.html]
[css-animations/test_effect-target.html]
[css-animations/test_element-get-animations.html]
+[css-animations/test_event-dispatch.html]
+[css-animations/test_event-order.html]
[css-animations/test_keyframeeffect-getkeyframes.html]
[css-animations/test_pseudoElement-get-animations.html]
[css-transitions/test_animation-cancel.html]
@@ -85,6 +90,7 @@ support-files =
[css-transitions/test_document-get-animations.html]
[css-transitions/test_effect-target.html]
[css-transitions/test_element-get-animations.html]
+[css-transitions/test_event-dispatch.html]
[css-transitions/test_keyframeeffect-getkeyframes.html]
[css-transitions/test_pseudoElement-get-animations.html]
[css-transitions/test_setting-effect.html]
diff --git a/dom/base/nsGkAtomList.h b/dom/base/nsGkAtomList.h
index aa4ef2ca3..50b4449ec 100644
--- a/dom/base/nsGkAtomList.h
+++ b/dom/base/nsGkAtomList.h
@@ -694,6 +694,7 @@ GK_ATOM(onadapterremoved, "onadapterremoved")
GK_ATOM(onafterprint, "onafterprint")
GK_ATOM(onafterscriptexecute, "onafterscriptexecute")
GK_ATOM(onalerting, "onalerting")
+GK_ATOM(onanimationcancel, "onanimationcancel")
GK_ATOM(onanimationend, "onanimationend")
GK_ATOM(onanimationiteration, "onanimationiteration")
GK_ATOM(onanimationstart, "onanimationstart")
@@ -942,6 +943,7 @@ GK_ATOM(ontouchstart, "ontouchstart")
GK_ATOM(ontouchend, "ontouchend")
GK_ATOM(ontouchmove, "ontouchmove")
GK_ATOM(ontouchcancel, "ontouchcancel")
+GK_ATOM(ontransitioncancel, "ontransitioncancel")
GK_ATOM(ontransitionend, "ontransitionend")
GK_ATOM(ontransitionrun, "ontransitionrun")
GK_ATOM(ontransitionstart, "ontransitionstart")
diff --git a/dom/events/EventNameList.h b/dom/events/EventNameList.h
index 891035c43..509863e6c 100644
--- a/dom/events/EventNameList.h
+++ b/dom/events/EventNameList.h
@@ -1007,6 +1007,10 @@ EVENT(transitionend,
eTransitionEnd,
EventNameType_All,
eTransitionEventClass)
+EVENT(transitioncancel,
+ eTransitionCancel,
+ EventNameType_All,
+ eTransitionEventClass)
EVENT(animationstart,
eAnimationStart,
EventNameType_All,
@@ -1019,6 +1023,10 @@ EVENT(animationiteration,
eAnimationIteration,
EventNameType_All,
eAnimationEventClass)
+EVENT(animationcancel,
+ eAnimationCancel,
+ EventNameType_All,
+ eAnimationEventClass)
// Webkit-prefixed versions of Transition & Animation events, for web compat:
EVENT(webkitAnimationEnd,
diff --git a/dom/events/test/test_legacy_event.html b/dom/events/test/test_legacy_event.html
index d772be106..b2105a6df 100644
--- a/dom/events/test/test_legacy_event.html
+++ b/dom/events/test/test_legacy_event.html
@@ -73,22 +73,15 @@ function triggerShortAnimation(node) {
node.style.animation = "anim1 1ms linear";
}
-// This function triggers a long animation with two iterations, which is
-// *nearly* at the end of its first iteration. It will hit the end of that
-// iteration (firing an event) almost immediately, 1ms in the future.
+// This function triggers a very short (10ms long) animation with many
+// iterations, which will cause a start event followed by an iteration event
+// on each subsequent tick, to fire.
//
-// NOTE: It's important that this animation have a *long* duration. If it were
-// short (e.g. 1ms duration), then we might jump past all its iterations in
-// a single refresh-driver tick. And if that were to happens, we'd *never* fire
-// any animationiteration events -- the CSS Animations spec says this event
-// must not be fired "...when an animationend event would fire at the same time"
-// (which would be the case in this example with a 1ms duration). So, to make
-// sure our event does fire, we use a long duration and a nearly-as-long
-// negative delay. This ensures we hit the end of the first iteration right
-// away, and that we don't risk hitting the end of the second iteration at the
-// same time.
+// NOTE: We need the many iterations since if an animation frame coincides
+// with the animation starting or ending we dispatch only the start or end
+// event and not the iteration event.
function triggerAnimationIteration(node) {
- node.style.animation = "anim1 300s -299.999s linear 2";
+ node.style.animation = "anim1 10ms linear 20000";
}
// GENERAL UTILITY FUNCTIONS
diff --git a/dom/webidl/EventHandler.webidl b/dom/webidl/EventHandler.webidl
index fce6d9b52..1edc45ac9 100644
--- a/dom/webidl/EventHandler.webidl
+++ b/dom/webidl/EventHandler.webidl
@@ -129,14 +129,14 @@ interface GlobalEventHandlers {
attribute EventHandler onmozpointerlockerror;
// CSS-Animation and CSS-Transition handlers.
+ attribute EventHandler onanimationcancel;
attribute EventHandler onanimationend;
attribute EventHandler onanimationiteration;
attribute EventHandler onanimationstart;
+ attribute EventHandler ontransitioncancel;
attribute EventHandler ontransitionend;
- // We will ship transitionrun and transitionstart events
- // on Firefox 53. (For detail, see bug 1324985)
-// attribute EventHandler ontransitionrun;
-// attribute EventHandler ontransitionstart;
+ attribute EventHandler ontransitionrun;
+ attribute EventHandler ontransitionstart;
// CSS-Animation and CSS-Transition legacy handlers.
// This handler isn't standard.