diff options
30 files changed, 1529 insertions, 459 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. diff --git a/layout/reftests/transform-3d/animate-backface-hidden.html b/layout/reftests/transform-3d/animate-backface-hidden.html index ce957bf73..27b587006 100644 --- a/layout/reftests/transform-3d/animate-backface-hidden.html +++ b/layout/reftests/transform-3d/animate-backface-hidden.html @@ -17,7 +17,7 @@ body { padding: 50px } height: 200px; width: 200px; backface-visibility: hidden; /* use a -99.9s delay to start at 99.9% and then move to 0% */ - animation: flip 100s -99.9s linear 2; + animation: flip 100s -99.9s linear 2 paused; } </style> @@ -27,7 +27,13 @@ body { padding: 50px } <script> -document.getElementById("test").addEventListener("animationiteration", IterationListener, false); +document.getElementById("test").addEventListener("animationstart", StartListener, false); + +function StartListener(event) { + var test = document.getElementById("test"); + test.style.animationPlayState = 'running'; + test.addEventListener("animationiteration", IterationListener, false); +} function IterationListener(event) { setTimeout(RemoveReftestWait, 0); diff --git a/layout/reftests/transform-3d/animate-preserve3d-parent.html b/layout/reftests/transform-3d/animate-preserve3d-parent.html index ae3fec196..d05beac6f 100644 --- a/layout/reftests/transform-3d/animate-preserve3d-parent.html +++ b/layout/reftests/transform-3d/animate-preserve3d-parent.html @@ -18,7 +18,7 @@ body { padding: 50px } border: 1px solid black; transform-style: preserve-3d; /* use a -99.9s delay to start at 99.9% and then move to 0% */ - animation: spin 100s -99.9s linear 2; + animation: spin 100s -99.9s linear 2 paused; } #child { @@ -39,7 +39,13 @@ body { padding: 50px } <script> -document.getElementById("parent").addEventListener("animationiteration", IterationListener, false); +document.getElementById("parent").addEventListener("animationstart", StartListener, false); + +function StartListener(event) { + var test = document.getElementById("parent"); + test.style.animationPlayState = 'running'; + test.addEventListener("animationiteration", IterationListener, false); +} function IterationListener(event) { setTimeout(RemoveReftestWait, 0); diff --git a/layout/style/AnimationCommon.h b/layout/style/AnimationCommon.h index 37030411c..025c034a4 100644 --- a/layout/style/AnimationCommon.h +++ b/layout/style/AnimationCommon.h @@ -251,6 +251,26 @@ ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, aField.Traverse(&aCallback, aName); } +// Return the TransitionPhase or AnimationPhase to use when the animation +// doesn't have a target effect. +template <typename PhaseType> +PhaseType GetAnimationPhaseWithoutEffect(const dom::Animation& aAnimation) +{ + MOZ_ASSERT(!aAnimation.GetEffect(), + "Should only be called when we do not have an effect"); + + Nullable<TimeDuration> currentTime = aAnimation.GetCurrentTime(); + if (currentTime.IsNull()) { + return PhaseType::Idle; + } + + // If we don't have a target effect, the duration will be zero so the phase is + // 'before' if the current time is less than zero. + return currentTime.Value() < TimeDuration() + ? PhaseType::Before + : PhaseType::After; +}; + } // namespace mozilla #endif /* !defined(mozilla_css_AnimationCommon_h) */ diff --git a/layout/style/nsAnimationManager.cpp b/layout/style/nsAnimationManager.cpp index ed2b5afc7..aa1b6fe78 100644 --- a/layout/style/nsAnimationManager.cpp +++ b/layout/style/nsAnimationManager.cpp @@ -33,11 +33,15 @@ using mozilla::dom::AnimationPlayState; using mozilla::dom::KeyframeEffectReadOnly; using mozilla::dom::CSSAnimation; +typedef mozilla::ComputedTiming::AnimationPhase AnimationPhase; + namespace { -// Pair of an event message and elapsed time used when determining the set of -// events to queue. -typedef Pair<EventMessage, StickyTimeDuration> EventPair; +struct AnimationEventParams { + EventMessage mMessage; + StickyTimeDuration mElapsedTime; + TimeStamp mTimeStamp; +}; } // anonymous namespace @@ -154,12 +158,8 @@ CSSAnimation::HasLowerCompositeOrderThan(const CSSAnimation& aOther) const } void -CSSAnimation::QueueEvents() +CSSAnimation::QueueEvents(StickyTimeDuration aActiveTime) { - if (!mEffect) { - return; - } - // If the animation is pending, we ignore animation events until we finish // pending. if (mPendingState != PendingState::NotPending) { @@ -194,77 +194,116 @@ CSSAnimation::QueueEvents() } nsAnimationManager* manager = presContext->AnimationManager(); - ComputedTiming computedTiming = mEffect->GetComputedTiming(); - if (computedTiming.mPhase == ComputedTiming::AnimationPhase::Null) { - return; // do nothing + const StickyTimeDuration zeroDuration; + uint64_t currentIteration = 0; + ComputedTiming::AnimationPhase currentPhase; + StickyTimeDuration intervalStartTime; + StickyTimeDuration intervalEndTime; + StickyTimeDuration iterationStartTime; + + if (!mEffect) { + currentPhase = GetAnimationPhaseWithoutEffect + <ComputedTiming::AnimationPhase>(*this); + } else { + ComputedTiming computedTiming = mEffect->GetComputedTiming(); + currentPhase = computedTiming.mPhase; + currentIteration = computedTiming.mCurrentIteration; + if (currentPhase == mPreviousPhase && + currentIteration == mPreviousIteration) { + return; + } + intervalStartTime = + std::max(std::min(StickyTimeDuration(-mEffect->SpecifiedTiming().mDelay), + computedTiming.mActiveDuration), + zeroDuration); + intervalEndTime = + std::max(std::min((EffectEnd() - mEffect->SpecifiedTiming().mDelay), + computedTiming.mActiveDuration), + zeroDuration); + + uint64_t iterationBoundary = mPreviousIteration > currentIteration + ? currentIteration + 1 + : currentIteration; + iterationStartTime = + computedTiming.mDuration.MultDouble( + (iterationBoundary - computedTiming.mIterationStart)); } - // Note that script can change the start time, so we have to handle moving - // backwards through the animation as well as forwards. An 'animationstart' - // is dispatched if we enter the active phase (regardless if that is from - // before or after the animation's active phase). An 'animationend' is - // dispatched if we leave the active phase (regardless if that is to before - // or after the animation's active phase). - - bool wasActive = mPreviousPhaseOrIteration != PREVIOUS_PHASE_BEFORE && - mPreviousPhaseOrIteration != PREVIOUS_PHASE_AFTER; - bool isActive = - computedTiming.mPhase == ComputedTiming::AnimationPhase::Active; - bool isSameIteration = - computedTiming.mCurrentIteration == mPreviousPhaseOrIteration; - bool skippedActivePhase = - (mPreviousPhaseOrIteration == PREVIOUS_PHASE_BEFORE && - computedTiming.mPhase == ComputedTiming::AnimationPhase::After) || - (mPreviousPhaseOrIteration == PREVIOUS_PHASE_AFTER && - computedTiming.mPhase == ComputedTiming::AnimationPhase::Before); - bool skippedFirstIteration = - isActive && - mPreviousPhaseOrIteration == PREVIOUS_PHASE_BEFORE && - computedTiming.mCurrentIteration > 0; - - MOZ_ASSERT(!skippedActivePhase || (!isActive && !wasActive), - "skippedActivePhase only makes sense if we were & are inactive"); - - if (computedTiming.mPhase == ComputedTiming::AnimationPhase::Before) { - mPreviousPhaseOrIteration = PREVIOUS_PHASE_BEFORE; - } else if (computedTiming.mPhase == ComputedTiming::AnimationPhase::Active) { - mPreviousPhaseOrIteration = computedTiming.mCurrentIteration; - } else if (computedTiming.mPhase == ComputedTiming::AnimationPhase::After) { - mPreviousPhaseOrIteration = PREVIOUS_PHASE_AFTER; + TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); + TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); + TimeStamp iterationTimeStamp = ElapsedTimeToTimeStamp(iterationStartTime); + + AutoTArray<AnimationEventParams, 2> events; + + // Handle cancel event first + if ((mPreviousPhase != AnimationPhase::Idle && + mPreviousPhase != AnimationPhase::After) && + currentPhase == AnimationPhase::Idle) { + TimeStamp activeTimeStamp = ElapsedTimeToTimeStamp(aActiveTime); + events.AppendElement(AnimationEventParams{ eAnimationCancel, + aActiveTime, + activeTimeStamp }); } - AutoTArray<EventPair, 2> events; - StickyTimeDuration initialAdvance = StickyTimeDuration(InitialAdvance()); - StickyTimeDuration iterationStart = computedTiming.mDuration * - computedTiming.mCurrentIteration; - const StickyTimeDuration& activeDuration = computedTiming.mActiveDuration; - - if (skippedFirstIteration) { - // Notify animationstart and animationiteration in same tick. - events.AppendElement(EventPair(eAnimationStart, initialAdvance)); - events.AppendElement(EventPair(eAnimationIteration, - std::max(iterationStart, initialAdvance))); - } else if (!wasActive && isActive) { - events.AppendElement(EventPair(eAnimationStart, initialAdvance)); - } else if (wasActive && !isActive) { - events.AppendElement(EventPair(eAnimationEnd, activeDuration)); - } else if (wasActive && isActive && !isSameIteration) { - events.AppendElement(EventPair(eAnimationIteration, iterationStart)); - } else if (skippedActivePhase) { - events.AppendElement(EventPair(eAnimationStart, - std::min(initialAdvance, activeDuration))); - events.AppendElement(EventPair(eAnimationEnd, activeDuration)); - } else { - return; // No events need to be sent + switch (mPreviousPhase) { + case AnimationPhase::Idle: + case AnimationPhase::Before: + if (currentPhase == AnimationPhase::Active) { + events.AppendElement(AnimationEventParams{ eAnimationStart, + intervalStartTime, + startTimeStamp }); + } else if (currentPhase == AnimationPhase::After) { + events.AppendElement(AnimationEventParams{ eAnimationStart, + intervalStartTime, + startTimeStamp }); + events.AppendElement(AnimationEventParams{ eAnimationEnd, + intervalEndTime, + endTimeStamp }); + } + break; + case AnimationPhase::Active: + if (currentPhase == AnimationPhase::Before) { + events.AppendElement(AnimationEventParams{ eAnimationEnd, + intervalStartTime, + startTimeStamp }); + } else if (currentPhase == AnimationPhase::Active) { + // The currentIteration must have changed or element we would have + // returned early above. + MOZ_ASSERT(currentIteration != mPreviousIteration); + events.AppendElement(AnimationEventParams{ eAnimationIteration, + iterationStartTime, + iterationTimeStamp }); + } else if (currentPhase == AnimationPhase::After) { + events.AppendElement(AnimationEventParams{ eAnimationEnd, + intervalEndTime, + endTimeStamp }); + } + break; + case AnimationPhase::After: + if (currentPhase == AnimationPhase::Before) { + events.AppendElement(AnimationEventParams{ eAnimationStart, + intervalEndTime, + startTimeStamp}); + events.AppendElement(AnimationEventParams{ eAnimationEnd, + intervalStartTime, + endTimeStamp }); + } else if (currentPhase == AnimationPhase::Active) { + events.AppendElement(AnimationEventParams{ eAnimationStart, + intervalEndTime, + endTimeStamp }); + } + break; } - for (const EventPair& pair : events){ + mPreviousPhase = currentPhase; + mPreviousIteration = currentIteration; + + for (const AnimationEventParams& event : events){ manager->QueueEvent( AnimationEventInfo(owningElement, owningPseudoType, - pair.first(), mAnimationName, - pair.second(), - ElapsedTimeToTimeStamp(pair.second()), + event.mMessage, mAnimationName, + event.mElapsedTime, event.mTimeStamp, this)); } } diff --git a/layout/style/nsAnimationManager.h b/layout/style/nsAnimationManager.h index abe3aeeb8..d838d090a 100644 --- a/layout/style/nsAnimationManager.h +++ b/layout/style/nsAnimationManager.h @@ -76,7 +76,8 @@ public: , mIsStylePaused(false) , mPauseShouldStick(false) , mNeedsNewAnimationIndexWhenRun(false) - , mPreviousPhaseOrIteration(PREVIOUS_PHASE_BEFORE) + , mPreviousPhase(ComputedTiming::AnimationPhase::Idle) + , mPreviousIteration(0) { // We might need to drop this assertion once we add a script-accessible // constructor but for animations generated from CSS markup the @@ -109,8 +110,6 @@ public: void PauseFromStyle(); void CancelFromStyle() override { - mOwningElement = OwningElementRef(); - // When an animation is disassociated with style it enters an odd state // where its composite order is undefined until it first transitions // out of the idle state. @@ -125,10 +124,15 @@ public: mNeedsNewAnimationIndexWhenRun = true; Animation::CancelFromStyle(); + + // We need to do this *after* calling CancelFromStyle() since + // CancelFromStyle might synchronously trigger a cancel event for which + // we need an owning element to target the event at. + mOwningElement = OwningElementRef(); } void Tick() override; - void QueueEvents(); + void QueueEvents(StickyTimeDuration aActiveTime = StickyTimeDuration()); bool IsStylePaused() const { return mIsStylePaused; } @@ -157,6 +161,10 @@ public: // reflect changes to that markup. bool IsTiedToMarkup() const { return mOwningElement.IsSet(); } + void MaybeQueueCancelEvent(StickyTimeDuration aActiveTime) override { + QueueEvents(aActiveTime); + } + protected: virtual ~CSSAnimation() { @@ -257,13 +265,10 @@ protected: // its animation index should be updated. bool mNeedsNewAnimationIndexWhenRun; - enum { - PREVIOUS_PHASE_BEFORE = uint64_t(-1), - PREVIOUS_PHASE_AFTER = uint64_t(-2) - }; - // One of the PREVIOUS_PHASE_* constants, or an integer for the iteration - // whose start we last notified on. - uint64_t mPreviousPhaseOrIteration; + // Phase and current iteration from the previous time we queued events. + // This is used to determine what new events to dispatch. + ComputedTiming::AnimationPhase mPreviousPhase; + uint64_t mPreviousIteration; }; } /* namespace dom */ diff --git a/layout/style/nsTransitionManager.cpp b/layout/style/nsTransitionManager.cpp index 4a1a5b7ad..118702e8f 100644 --- a/layout/style/nsTransitionManager.cpp +++ b/layout/style/nsTransitionManager.cpp @@ -46,8 +46,6 @@ using mozilla::dom::KeyframeEffectReadOnly; using namespace mozilla; using namespace mozilla::css; -typedef mozilla::ComputedTiming::AnimationPhase AnimationPhase; - namespace { struct TransitionEventParams { EventMessage mMessage; @@ -180,10 +178,9 @@ CSSTransition::UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) } void -CSSTransition::QueueEvents() +CSSTransition::QueueEvents(StickyTimeDuration aActiveTime) { - if (!mEffect || - !mOwningElement.IsSet()) { + if (!mOwningElement.IsSet()) { return; } @@ -197,69 +194,123 @@ CSSTransition::QueueEvents() return; } - ComputedTiming computedTiming = mEffect->GetComputedTiming(); - const StickyTimeDuration zeroDuration; - StickyTimeDuration intervalStartTime = - std::max(std::min(StickyTimeDuration(-mEffect->SpecifiedTiming().mDelay), - computedTiming.mActiveDuration), zeroDuration); - StickyTimeDuration intervalEndTime = - std::max(std::min((EffectEnd() - mEffect->SpecifiedTiming().mDelay), - computedTiming.mActiveDuration), zeroDuration); + const StickyTimeDuration zeroDuration = StickyTimeDuration(); + + TransitionPhase currentPhase; + StickyTimeDuration intervalStartTime; + StickyTimeDuration intervalEndTime; + + if (!mEffect) { + currentPhase = GetAnimationPhaseWithoutEffect<TransitionPhase>(*this); + intervalStartTime = zeroDuration; + intervalEndTime = zeroDuration; + } else { + ComputedTiming computedTiming = mEffect->GetComputedTiming(); + + currentPhase = static_cast<TransitionPhase>(computedTiming.mPhase); + intervalStartTime = + std::max(std::min(StickyTimeDuration(-mEffect->SpecifiedTiming().mDelay), + computedTiming.mActiveDuration), zeroDuration); + intervalEndTime = + std::max(std::min((EffectEnd() - mEffect->SpecifiedTiming().mDelay), + computedTiming.mActiveDuration), zeroDuration); + } // TimeStamps to use for ordering the events when they are dispatched. We // use a TimeStamp so we can compare events produced by different elements, // perhaps even with different timelines. // The zero timestamp is for transitionrun events where we ignore the delay // for the purpose of ordering events. - TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); - TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); + TimeStamp zeroTimeStamp = AnimationTimeToTimeStamp(zeroDuration); + TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); + TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); - TransitionPhase currentPhase; if (mPendingState != PendingState::NotPending && (mPreviousTransitionPhase == TransitionPhase::Idle || mPreviousTransitionPhase == TransitionPhase::Pending)) { currentPhase = TransitionPhase::Pending; - } else { - currentPhase = static_cast<TransitionPhase>(computedTiming.mPhase); } AutoTArray<TransitionEventParams, 3> events; + + // Handle cancel events firts + if (mPreviousTransitionPhase != TransitionPhase::Idle && + currentPhase == TransitionPhase::Idle) { + TimeStamp activeTimeStamp = ElapsedTimeToTimeStamp(aActiveTime); + events.AppendElement(TransitionEventParams{ eTransitionCancel, + aActiveTime, + activeTimeStamp }); + } + + // All other events switch (mPreviousTransitionPhase) { case TransitionPhase::Idle: - if (currentPhase == TransitionPhase::After) { + if (currentPhase == TransitionPhase::Pending || + currentPhase == TransitionPhase::Before) { + events.AppendElement(TransitionEventParams{ eTransitionRun, + intervalStartTime, + zeroTimeStamp }); + } else if (currentPhase == TransitionPhase::Active) { + events.AppendElement(TransitionEventParams{ eTransitionRun, + intervalStartTime, + zeroTimeStamp }); + events.AppendElement(TransitionEventParams{ eTransitionStart, + intervalStartTime, + startTimeStamp }); + } else if (currentPhase == TransitionPhase::After) { + events.AppendElement(TransitionEventParams{ eTransitionRun, + intervalStartTime, + zeroTimeStamp }); + events.AppendElement(TransitionEventParams{ eTransitionStart, + intervalStartTime, + startTimeStamp }); events.AppendElement(TransitionEventParams{ eTransitionEnd, - intervalEndTime, - endTimeStamp }); + intervalEndTime, + endTimeStamp }); } break; case TransitionPhase::Pending: case TransitionPhase::Before: - if (currentPhase == TransitionPhase::After) { + if (currentPhase == TransitionPhase::Active) { + events.AppendElement(TransitionEventParams{ eTransitionStart, + intervalStartTime, + startTimeStamp }); + } else if (currentPhase == TransitionPhase::After) { + events.AppendElement(TransitionEventParams{ eTransitionStart, + intervalStartTime, + startTimeStamp }); events.AppendElement(TransitionEventParams{ eTransitionEnd, - intervalEndTime, - endTimeStamp }); + intervalEndTime, + endTimeStamp }); } break; case TransitionPhase::Active: if (currentPhase == TransitionPhase::After) { events.AppendElement(TransitionEventParams{ eTransitionEnd, - intervalEndTime, - endTimeStamp }); + intervalEndTime, + endTimeStamp }); } else if (currentPhase == TransitionPhase::Before) { events.AppendElement(TransitionEventParams{ eTransitionEnd, - intervalStartTime, - startTimeStamp }); + intervalStartTime, + startTimeStamp }); } break; case TransitionPhase::After: - if (currentPhase == TransitionPhase::Before) { + if (currentPhase == TransitionPhase::Active) { + events.AppendElement(TransitionEventParams{ eTransitionStart, + intervalEndTime, + startTimeStamp }); + } else if (currentPhase == TransitionPhase::Before) { + events.AppendElement(TransitionEventParams{ eTransitionStart, + intervalEndTime, + startTimeStamp }); events.AppendElement(TransitionEventParams{ eTransitionEnd, - intervalStartTime, - endTimeStamp }); + intervalStartTime, + endTimeStamp }); } break; } diff --git a/layout/style/nsTransitionManager.h b/layout/style/nsTransitionManager.h index 56ec61572..1c48cc8cd 100644 --- a/layout/style/nsTransitionManager.h +++ b/layout/style/nsTransitionManager.h @@ -214,6 +214,10 @@ public: const TimeDuration& aStartTime, double aPlaybackRate); + void MaybeQueueCancelEvent(StickyTimeDuration aActiveTime) override { + QueueEvents(aActiveTime); + } + protected: virtual ~CSSTransition() { @@ -225,7 +229,10 @@ protected: void UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) override; - void QueueEvents(); + void QueueEvents(StickyTimeDuration activeTime = StickyTimeDuration()); + + + enum class TransitionPhase; // The (pseudo-)element whose computed transition-property refers to this // transition (if any). @@ -250,7 +257,7 @@ protected: // to be queued on this tick. // See: https://drafts.csswg.org/css-transitions-2/#transition-phase enum class TransitionPhase { - Idle = static_cast<int>(ComputedTiming::AnimationPhase::Null), + Idle = static_cast<int>(ComputedTiming::AnimationPhase::Idle), Before = static_cast<int>(ComputedTiming::AnimationPhase::Before), Active = static_cast<int>(ComputedTiming::AnimationPhase::Active), After = static_cast<int>(ComputedTiming::AnimationPhase::After), diff --git a/layout/style/test/test_animations.html b/layout/style/test/test_animations.html index eaccba122..4019af77f 100644 --- a/layout/style/test/test_animations.html +++ b/layout/style/test/test_animations.html @@ -1195,9 +1195,6 @@ is_approx(px_to_num(cs.marginRight), 100 * gTF.ease_in(0.4), 0.01, "large negative delay test at 0ms"); check_events([{ type: 'animationstart', target: div, animationName: 'anim2', elapsedTime: 3.6, - pseudoElement: "" }, - { type: 'animationiteration', target: div, - animationName: 'anim2', elapsedTime: 3.6, pseudoElement: "" }], "right after start in large negative delay test"); advance_clock(380); diff --git a/layout/style/test/test_animations_event_handler_attribute.html b/layout/style/test/test_animations_event_handler_attribute.html index e5def2b34..036a77779 100644 --- a/layout/style/test/test_animations_event_handler_attribute.html +++ b/layout/style/test/test_animations_event_handler_attribute.html @@ -88,21 +88,55 @@ checkReceivedEvents("animationend", targets); targets.forEach(div => { div.remove(); }); -// 2. Test CSS Transition event handlers. +// 2a. Test CSS Transition event handlers (without transitioncancel) -var targets = createAndRegisterTargets([ 'ontransitionend' ]); +var targets = createAndRegisterTargets([ 'ontransitionrun', + 'ontransitionstart', + 'ontransitionend', + 'ontransitioncancel' ]); targets.forEach(div => { - div.style.transition = 'margin-left 100ms'; + div.style.transition = 'margin-left 100ms 200ms'; getComputedStyle(div).marginLeft; // flush div.style.marginLeft = "200px"; getComputedStyle(div).marginLeft; // flush }); +advance_clock(0); +checkReceivedEvents("transitionrun", targets); + +advance_clock(200); +checkReceivedEvents("transitionstart", targets); + advance_clock(100); checkReceivedEvents("transitionend", targets); targets.forEach(div => { div.remove(); }); +// 2b. Test CSS Transition cancel event handler. + +var targets = createAndRegisterTargets([ 'ontransitioncancel' ]); +targets.forEach(div => { + div.style.transition = 'margin-left 100ms 200ms'; + getComputedStyle(div).marginLeft; // flush + div.style.marginLeft = "200px"; + getComputedStyle(div).marginLeft; // flush +}); + +advance_clock(200); + +targets.forEach(div => { + div.style.display = "none" +}); +getComputedStyle(targets[0]).display; // flush + +advance_clock(0); +checkReceivedEvents("transitioncancel", targets); + +advance_clock(100); +targets.forEach( div => { is(div.receivedEventType, undefined); }); + +targets.forEach(div => { div.remove(); }); + // 3. Test prefixed CSS Animation event handlers. var targets = createAndRegisterTargets([ 'onwebkitanimationstart', diff --git a/layout/style/test/test_animations_event_order.html b/layout/style/test/test_animations_event_order.html index 5af7639cc..7204934d2 100644 --- a/layout/style/test/test_animations_event_order.html +++ b/layout/style/test/test_animations_event_order.html @@ -46,7 +46,10 @@ var gDisplay = document.getElementById('display'); [ 'animationstart', 'animationiteration', 'animationend', - 'transitionend' ] + 'transitionrun', + 'transitionstart', + 'transitionend', + 'transitioncancel' ] .forEach(event => gDisplay.addEventListener(event, event => gEventsReceived.push(event), @@ -322,9 +325,13 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ divs[0], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[0], 'transitionstart' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionend' ], [ divs[1], 'transitionend' ], - 'Simultaneous transitionend on siblings'); + 'Simultaneous transitionrun/start/end on siblings'); divs.forEach(div => div.remove()); divs = []; @@ -360,10 +367,16 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ divs[0], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[0], 'transitionstart' ], + [ divs[2], 'transitionrun' ], + [ divs[2], 'transitionstart' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionend' ], [ divs[2], 'transitionend' ], [ divs[1], 'transitionend' ], - 'Simultaneous transitionend on children'); + 'Simultaneous transitionrun/start/end on children'); divs.forEach(div => div.remove()); divs = []; @@ -408,11 +421,19 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ divs[0], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[0], 'transitionstart' ], + [ divs[0], '::before', 'transitionrun' ], + [ divs[0], '::before', 'transitionstart' ], + [ divs[0], '::after', 'transitionrun' ], + [ divs[0], '::after', 'transitionstart' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionend' ], [ divs[0], '::before', 'transitionend' ], [ divs[0], '::after', 'transitionend' ], [ divs[1], 'transitionend' ], - 'Simultaneous transitionend on pseudo-elements'); + 'Simultaneous transitionrun/start/end on pseudo-elements'); divs.forEach(div => div.remove()); divs = []; @@ -441,9 +462,13 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ divs[1], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[0], 'transitionstart' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[1], 'transitionend' ], [ divs[0], 'transitionend' ], - 'Sorting of transitionend events by time'); + 'Sorting of transitionrun/start/end events by time'); divs.forEach(div => div.remove()); divs = []; @@ -468,9 +493,13 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(10 * 1000); -checkEventOrder([ divs[1], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionstart' ], + [ divs[1], 'transitionend' ], [ divs[0], 'transitionend' ], - 'Sorting of transitionend events by time' + + 'Sorting of transitionrun/start/end events by time' + '(including delay)'); divs.forEach(div => div.remove()); @@ -492,9 +521,14 @@ getComputedStyle(div).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ 'margin-left', 'transitionend' ], +checkEventOrder([ 'margin-left', 'transitionrun' ], + [ 'margin-left', 'transitionstart' ], + [ 'opacity', 'transitionrun' ], + [ 'opacity', 'transitionstart' ], + [ 'margin-left', 'transitionend' ], [ 'opacity', 'transitionend' ], - 'Sorting of transitionend events by transition-property') + 'Sorting of transitionrun/start/end events by ' + + 'transition-property') div.remove(); div = undefined; @@ -519,7 +553,11 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ divs[0], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[0], 'transitionstart' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionend' ], [ divs[1], 'transitionend' ], 'Transition events are sorted by document position first, ' + 'before transition-property'); @@ -543,7 +581,11 @@ getComputedStyle(div).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ 'opacity', 'transitionend' ], +checkEventOrder([ 'margin-left', 'transitionrun' ], + [ 'margin-left', 'transitionstart' ], + [ 'opacity', 'transitionrun' ], + [ 'opacity', 'transitionstart' ], + [ 'opacity', 'transitionend' ], [ 'margin-left', 'transitionend' ], 'Transition events are sorted by time first, before ' + 'transition-property'); @@ -571,9 +613,50 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(15 * 1000); -checkEventOrder([ divs[1], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionstart' ], + [ divs[1], 'transitionend' ], [ divs[0], 'transitionend' ], - 'Simultaneous transitionend on siblings'); + 'Simultaneous transitionrun/start/end on siblings'); + +divs.forEach(div => div.remove()); +divs = []; + +// 4j. Test sorting transitions with cancel +// The order of transitioncancel is based on StyleManager. +// So this test looks like wrong result at a glance. However +// the gecko will cancel div1's transition before div2 in this case. + +divs = [ document.createElement('div'), + document.createElement('div') ]; +divs.forEach((div, i) => { + gDisplay.appendChild(div); + div.style.marginLeft = '0px'; + div.setAttribute('id', 'div' + i); +}); + +divs[0].style.transition = 'margin-left 10s 5s'; +divs[1].style.transition = 'margin-left 10s'; + +getComputedStyle(divs[0]).marginLeft; +divs.forEach(div => div.style.marginLeft = '100px'); +getComputedStyle(divs[0]).marginLeft; + +advance_clock(0); +advance_clock(5 * 1000); +divs.forEach(div => div.style.display = 'none' ); +getComputedStyle(divs[0]).display; +advance_clock(10 * 1000); + +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionstart' ], + [ divs[1], 'transitioncancel' ], + [ divs[0], 'transitioncancel' ], + 'Simultaneous transitionrun/start/cancel on siblings'); divs.forEach(div => div.remove()); divs = []; diff --git a/layout/style/test/test_animations_omta.html b/layout/style/test/test_animations_omta.html index 4b276c896..0b2a61ecc 100644 --- a/layout/style/test/test_animations_omta.html +++ b/layout/style/test/test_animations_omta.html @@ -1408,9 +1408,6 @@ addAsyncAnimTest(function *() { "large negative delay test at 0ms"); check_events([{ type: 'animationstart', target: gDiv, animationName: 'anim2', elapsedTime: 3.6, - pseudoElement: "" }, - { type: 'animationiteration', target: gDiv, - animationName: 'anim2', elapsedTime: 3.6, pseudoElement: "" }], "right after start in large negative delay test"); advance_clock(380); diff --git a/widget/EventMessageList.h b/widget/EventMessageList.h index f6cf76209..2eed7e618 100644 --- a/widget/EventMessageList.h +++ b/widget/EventMessageList.h @@ -341,9 +341,11 @@ NS_EVENT_MESSAGE(eScrolledAreaChanged) NS_EVENT_MESSAGE(eTransitionStart) NS_EVENT_MESSAGE(eTransitionRun) NS_EVENT_MESSAGE(eTransitionEnd) +NS_EVENT_MESSAGE(eTransitionCancel) NS_EVENT_MESSAGE(eAnimationStart) NS_EVENT_MESSAGE(eAnimationEnd) NS_EVENT_MESSAGE(eAnimationIteration) +NS_EVENT_MESSAGE(eAnimationCancel) // Webkit-prefixed versions of Transition & Animation events, for web compat: NS_EVENT_MESSAGE(eWebkitTransitionEnd) |