From c4730100294c28df0a170f47565ded7eebfaba49 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Mar 2018 08:26:26 +0100 Subject: Bug 1202333: AnimationEvent elapsedTime should reflect playbackRate (without tests) Issue #55 --- layout/style/nsAnimationManager.cpp | 152 +++++++++++++++++++++--------------- layout/style/nsAnimationManager.h | 14 ++-- 2 files changed, 93 insertions(+), 73 deletions(-) diff --git a/layout/style/nsAnimationManager.cpp b/layout/style/nsAnimationManager.cpp index ed2b5afc7..8d4e8fcee 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 EventPair; +struct AnimationEventParams { + EventMessage mMessage; + StickyTimeDuration mElapsedTime; + TimeStamp mTimeStamp; +}; } // anonymous namespace @@ -196,75 +200,93 @@ CSSAnimation::QueueEvents() ComputedTiming computedTiming = mEffect->GetComputedTiming(); - if (computedTiming.mPhase == ComputedTiming::AnimationPhase::Null) { - return; // do nothing + ComputedTiming::AnimationPhase currentPhase = computedTiming.mPhase; + uint64_t currentIteration = computedTiming.mCurrentIteration; + if (currentPhase == mPreviousPhase && + currentIteration == mPreviousIteration) { + return; } - // 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; + 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); + + uint64_t iterationBoundary = mPreviousIteration > currentIteration + ? currentIteration + 1 + : currentIteration; + StickyTimeDuration iterationStartTime = + computedTiming.mDuration.MultDouble( + (iterationBoundary - computedTiming.mIterationStart)); + + TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); + TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); + TimeStamp iterationTimeStamp = ElapsedTimeToTimeStamp(iterationStartTime); + + AutoTArray events; + switch (mPreviousPhase) { + case AnimationPhase::Null: + 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; } - AutoTArray 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 - } + mPreviousPhase = currentPhase; + mPreviousIteration = currentIteration; - for (const EventPair& pair : events){ + 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..868d4bb42 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::Null) + , mPreviousIteration(0) { // We might need to drop this assertion once we add a script-accessible // constructor but for animations generated from CSS markup the @@ -257,13 +258,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 */ -- cgit v1.2.3 From cf4482199c5b92c5029b79c086cf9f831a87f895 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Mar 2018 08:52:52 +0100 Subject: Bug 1264125: Fire transitioncancel event when a transition is canceled - part 1 (in the description) Issue #55 part 1 - Add transitioncancel event handler part 2 - Add ontransitioncancel EventHandler to WebIDL part 3 - Add member of active time to ComputedTiming --- dom/animation/AnimationEffectReadOnly.cpp | 18 ++++++++---------- dom/animation/ComputedTiming.h | 2 ++ dom/base/nsGkAtomList.h | 1 + dom/events/EventNameList.h | 4 ++++ dom/webidl/EventHandler.webidl | 1 + widget/EventMessageList.h | 1 + 6 files changed, 17 insertions(+), 10 deletions(-) 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..addf15865 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. diff --git a/dom/base/nsGkAtomList.h b/dom/base/nsGkAtomList.h index e4ae7ede8..09d73e617 100644 --- a/dom/base/nsGkAtomList.h +++ b/dom/base/nsGkAtomList.h @@ -941,6 +941,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 ba2427623..61942b251 100644 --- a/dom/events/EventNameList.h +++ b/dom/events/EventNameList.h @@ -1003,6 +1003,10 @@ EVENT(transitionend, eTransitionEnd, EventNameType_All, eTransitionEventClass) +EVENT(transitioncancel, + eTransitionCancel, + EventNameType_All, + eTransitionEventClass) EVENT(animationstart, eAnimationStart, EventNameType_All, diff --git a/dom/webidl/EventHandler.webidl b/dom/webidl/EventHandler.webidl index e65a75787..c76dfe211 100644 --- a/dom/webidl/EventHandler.webidl +++ b/dom/webidl/EventHandler.webidl @@ -131,6 +131,7 @@ interface GlobalEventHandlers { 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) diff --git a/widget/EventMessageList.h b/widget/EventMessageList.h index 7fe642637..474e25862 100644 --- a/widget/EventMessageList.h +++ b/widget/EventMessageList.h @@ -340,6 +340,7 @@ 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) -- cgit v1.2.3 From 3a9cafda4dcbf860862347241515e730707f3581 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Mar 2018 09:04:22 +0100 Subject: Revert - Bug 1324985: Disable firing the transitionrun and transitionstart on Aurora 52 Issue #55 --- dom/animation/test/mochitest.ini | 2 + dom/webidl/EventHandler.webidl | 6 +- layout/style/nsTransitionManager.cpp | 39 ++++++++++- .../test_animations_event_handler_attribute.html | 12 +++- layout/style/test/test_animations_event_order.html | 77 +++++++++++++++++----- 5 files changed, 111 insertions(+), 25 deletions(-) diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini index feb424518..c694e4d25 100644 --- a/dom/animation/test/mochitest.ini +++ b/dom/animation/test/mochitest.ini @@ -28,6 +28,7 @@ support-files = css-transitions/file_animation-pausing.html css-transitions/file_animation-ready.html css-transitions/file_animation-starttime.html + css-transitions/file_csstransition-events.html css-transitions/file_csstransition-transitionproperty.html css-transitions/file_document-get-animations.html css-transitions/file_effect-target.html @@ -81,6 +82,7 @@ support-files = [css-transitions/test_animation-pausing.html] [css-transitions/test_animation-ready.html] [css-transitions/test_animation-starttime.html] +[css-transitions/test_csstransition-events.html] [css-transitions/test_csstransition-transitionproperty.html] [css-transitions/test_document-get-animations.html] [css-transitions/test_effect-target.html] diff --git a/dom/webidl/EventHandler.webidl b/dom/webidl/EventHandler.webidl index c76dfe211..6691ca224 100644 --- a/dom/webidl/EventHandler.webidl +++ b/dom/webidl/EventHandler.webidl @@ -133,10 +133,8 @@ interface GlobalEventHandlers { 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/style/nsTransitionManager.cpp b/layout/style/nsTransitionManager.cpp index 4a1a5b7ad..d40e26a82 100644 --- a/layout/style/nsTransitionManager.cpp +++ b/layout/style/nsTransitionManager.cpp @@ -211,6 +211,7 @@ CSSTransition::QueueEvents() // perhaps even with different timelines. // The zero timestamp is for transitionrun events where we ignore the delay // for the purpose of ordering events. + TimeStamp zeroTimeStamp = AnimationTimeToTimeStamp(zeroDuration); TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); @@ -227,7 +228,25 @@ CSSTransition::QueueEvents() AutoTArray 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 }); @@ -236,7 +255,14 @@ CSSTransition::QueueEvents() 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 }); @@ -256,7 +282,14 @@ CSSTransition::QueueEvents() 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 }); diff --git a/layout/style/test/test_animations_event_handler_attribute.html b/layout/style/test/test_animations_event_handler_attribute.html index e5def2b34..23a749daa 100644 --- a/layout/style/test/test_animations_event_handler_attribute.html +++ b/layout/style/test/test_animations_event_handler_attribute.html @@ -90,14 +90,22 @@ targets.forEach(div => { div.remove(); }); // 2. Test CSS Transition event handlers. -var targets = createAndRegisterTargets([ 'ontransitionend' ]); +var targets = createAndRegisterTargets([ 'ontransitionrun', + 'ontransitionstart', + 'ontransitionend' ]); 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); diff --git a/layout/style/test/test_animations_event_order.html b/layout/style/test/test_animations_event_order.html index 5af7639cc..f948bf0a5 100644 --- a/layout/style/test/test_animations_event_order.html +++ b/layout/style/test/test_animations_event_order.html @@ -46,6 +46,8 @@ var gDisplay = document.getElementById('display'); [ 'animationstart', 'animationiteration', 'animationend', + 'transitionrun', + 'transitionstart', 'transitionend' ] .forEach(event => gDisplay.addEventListener(event, @@ -322,9 +324,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 +366,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 +420,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 +461,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 +492,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 +520,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 +552,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 +580,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 +612,13 @@ 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 = []; -- cgit v1.2.3 From 4644a03770ef95fd3acbaa205fae339b3a99f599 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Mar 2018 09:10:08 +0100 Subject: Bug 1264125: Queue transitioncancel when animation status is idle Issue #55 --- layout/style/nsTransitionManager.cpp | 76 +++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/layout/style/nsTransitionManager.cpp b/layout/style/nsTransitionManager.cpp index d40e26a82..4a5ecdef6 100644 --- a/layout/style/nsTransitionManager.cpp +++ b/layout/style/nsTransitionManager.cpp @@ -211,9 +211,9 @@ CSSTransition::QueueEvents() // perhaps even with different timelines. // The zero timestamp is for transitionrun events where we ignore the delay // for the purpose of ordering events. - TimeStamp zeroTimeStamp = AnimationTimeToTimeStamp(zeroDuration); - 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 && @@ -226,30 +226,44 @@ CSSTransition::QueueEvents() } AutoTArray events; + + // Handle cancel events firts + if (mPreviousTransitionPhase != TransitionPhase::Idle && + currentPhase == TransitionPhase::Idle) { + // FIXME: bug 1264125: We will need to get active time when cancelling + // the transition. + StickyTimeDuration activeTime(0); + TimeStamp activeTimeStamp = ElapsedTimeToTimeStamp(activeTime); + events.AppendElement(TransitionEventParams{ eTransitionCancel, + activeTime, + activeTimeStamp }); + } + + // All other events switch (mPreviousTransitionPhase) { case TransitionPhase::Idle: if (currentPhase == TransitionPhase::Pending || currentPhase == TransitionPhase::Before) { events.AppendElement(TransitionEventParams{ eTransitionRun, - intervalStartTime, - zeroTimeStamp }); + intervalStartTime, + zeroTimeStamp }); } else if (currentPhase == TransitionPhase::Active) { events.AppendElement(TransitionEventParams{ eTransitionRun, - intervalStartTime, - zeroTimeStamp }); + intervalStartTime, + zeroTimeStamp }); events.AppendElement(TransitionEventParams{ eTransitionStart, - intervalStartTime, - startTimeStamp }); + intervalStartTime, + startTimeStamp }); } else if (currentPhase == TransitionPhase::After) { events.AppendElement(TransitionEventParams{ eTransitionRun, - intervalStartTime, - zeroTimeStamp }); + intervalStartTime, + zeroTimeStamp }); events.AppendElement(TransitionEventParams{ eTransitionStart, - intervalStartTime, - startTimeStamp }); + intervalStartTime, + startTimeStamp }); events.AppendElement(TransitionEventParams{ eTransitionEnd, - intervalEndTime, - endTimeStamp }); + intervalEndTime, + endTimeStamp }); } break; @@ -257,42 +271,42 @@ CSSTransition::QueueEvents() case TransitionPhase::Before: if (currentPhase == TransitionPhase::Active) { events.AppendElement(TransitionEventParams{ eTransitionStart, - intervalStartTime, - startTimeStamp }); + intervalStartTime, + startTimeStamp }); } else if (currentPhase == TransitionPhase::After) { events.AppendElement(TransitionEventParams{ eTransitionStart, - intervalStartTime, - startTimeStamp }); + 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::Active) { events.AppendElement(TransitionEventParams{ eTransitionStart, - intervalEndTime, - startTimeStamp }); + intervalEndTime, + startTimeStamp }); } else if (currentPhase == TransitionPhase::Before) { events.AppendElement(TransitionEventParams{ eTransitionStart, - intervalEndTime, - startTimeStamp }); + intervalEndTime, + startTimeStamp }); events.AppendElement(TransitionEventParams{ eTransitionEnd, - intervalStartTime, - endTimeStamp }); + intervalStartTime, + endTimeStamp }); } break; } -- cgit v1.2.3 From 34ef9d4683b3e81b8df1be1a9c38eae331e8c398 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Mar 2018 09:16:03 +0100 Subject: Bug 1264125: Call the queueing events when canceling transition via Style or Script Issue #55 --- dom/animation/Animation.cpp | 23 +++++++++++++++++++++++ dom/animation/Animation.h | 10 ++++++++++ layout/style/nsTransitionManager.cpp | 9 +++------ layout/style/nsTransitionManager.h | 18 +++++++++++++++++- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp index 6dd583ed1..cefdbb76d 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 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); } @@ -771,6 +778,10 @@ Animation::CancelNoUpdate() DispatchPlaybackEvent(NS_LITERAL_STRING("cancel")); + StickyTimeDuration activeTime = mEffect + ? mEffect->GetComputedTiming().mActiveTime + : StickyTimeDuration(); + mHoldTime.SetNull(); mStartTime.SetNull(); @@ -779,6 +790,7 @@ Animation::CancelNoUpdate() if (mTimeline) { mTimeline->RemoveAnimation(this); } + MaybeQueueCancelEvent(activeTime); } void @@ -819,6 +831,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/layout/style/nsTransitionManager.cpp b/layout/style/nsTransitionManager.cpp index 4a5ecdef6..da12a0ecd 100644 --- a/layout/style/nsTransitionManager.cpp +++ b/layout/style/nsTransitionManager.cpp @@ -180,7 +180,7 @@ CSSTransition::UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) } void -CSSTransition::QueueEvents() +CSSTransition::QueueEvents(StickyTimeDuration aActiveTime) { if (!mEffect || !mOwningElement.IsSet()) { @@ -230,12 +230,9 @@ CSSTransition::QueueEvents() // Handle cancel events firts if (mPreviousTransitionPhase != TransitionPhase::Idle && currentPhase == TransitionPhase::Idle) { - // FIXME: bug 1264125: We will need to get active time when cancelling - // the transition. - StickyTimeDuration activeTime(0); - TimeStamp activeTimeStamp = ElapsedTimeToTimeStamp(activeTime); + TimeStamp activeTimeStamp = ElapsedTimeToTimeStamp(aActiveTime); events.AppendElement(TransitionEventParams{ eTransitionCancel, - activeTime, + aActiveTime, activeTimeStamp }); } diff --git a/layout/style/nsTransitionManager.h b/layout/style/nsTransitionManager.h index 56ec61572..80042adcd 100644 --- a/layout/style/nsTransitionManager.h +++ b/layout/style/nsTransitionManager.h @@ -162,6 +162,18 @@ public: Animation::CancelFromStyle(); + // The above call to Animation::CancelFromStyle may cause a transitioncancel + // event to be queued. However, it will also remove the transition from its + // timeline. If this transition was the last animation attached to + // the timeline, the timeline will stop observing the refresh driver and + // there may be no subsequent tick fro dispatching animation events. + // + // To ensure the cancel event is dispatched we tell the timeline it needs to + // observe the refresh driver for at least one more tick. + if (mTimeline) { + mTimeline->NotifyAnimationUpdated(*this); + } + // It is important we do this *after* calling CancelFromStyle(). // This is because CancelFromStyle() will end up posting a restyle and // that restyle should target the *transitions* level of the cascade. @@ -214,6 +226,10 @@ public: const TimeDuration& aStartTime, double aPlaybackRate); + void MaybeQueueCancelEvent(StickyTimeDuration aActiveTime) override { + QueueEvents(aActiveTime); + } + protected: virtual ~CSSTransition() { @@ -225,7 +241,7 @@ protected: void UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) override; - void QueueEvents(); + void QueueEvents(StickyTimeDuration activeTime = StickyTimeDuration()); // The (pseudo-)element whose computed transition-property refers to this // transition (if any). -- cgit v1.2.3 From dcc00ffadf64270b0ae82431f5b71c527590e91b Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Mar 2018 09:18:49 +0100 Subject: Bug 1264125: Queue CSS related event when setting null target effect Issue #55 --- dom/animation/Animation.cpp | 6 +++-- layout/style/nsTransitionManager.cpp | 52 +++++++++++++++++++++++++++--------- layout/style/nsTransitionManager.h | 6 +++++ 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp index cefdbb76d..242a0c6d6 100644 --- a/dom/animation/Animation.cpp +++ b/dom/animation/Animation.cpp @@ -729,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); } diff --git a/layout/style/nsTransitionManager.cpp b/layout/style/nsTransitionManager.cpp index da12a0ecd..abb9e2311 100644 --- a/layout/style/nsTransitionManager.cpp +++ b/layout/style/nsTransitionManager.cpp @@ -182,8 +182,7 @@ CSSTransition::UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) void CSSTransition::QueueEvents(StickyTimeDuration aActiveTime) { - if (!mEffect || - !mOwningElement.IsSet()) { + if (!mOwningElement.IsSet()) { return; } @@ -197,14 +196,27 @@ CSSTransition::QueueEvents(StickyTimeDuration aActiveTime) 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 = GetTransitionPhaseWithoutEffect(); + intervalStartTime = zeroDuration; + intervalEndTime = zeroDuration; + } else { + ComputedTiming computedTiming = mEffect->GetComputedTiming(); + + currentPhase = static_cast(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, @@ -215,14 +227,11 @@ CSSTransition::QueueEvents(StickyTimeDuration aActiveTime) 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(computedTiming.mPhase); } AutoTArray events; @@ -320,6 +329,23 @@ CSSTransition::QueueEvents(StickyTimeDuration aActiveTime) } } +CSSTransition::TransitionPhase +CSSTransition::GetTransitionPhaseWithoutEffect() const +{ + MOZ_ASSERT(!mEffect, "Should only be called when we do not have an effect"); + + Nullable currentTime = GetCurrentTime(); + if (currentTime.IsNull()) { + return TransitionPhase::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() + ? TransitionPhase::Before + : TransitionPhase::After; +} + void CSSTransition::Tick() { diff --git a/layout/style/nsTransitionManager.h b/layout/style/nsTransitionManager.h index 80042adcd..e2f198a40 100644 --- a/layout/style/nsTransitionManager.h +++ b/layout/style/nsTransitionManager.h @@ -243,6 +243,12 @@ protected: void QueueEvents(StickyTimeDuration activeTime = StickyTimeDuration()); + + enum class TransitionPhase; + // Return the TransitionPhase to use when the transition doesn't have a target + // effect. + TransitionPhase GetTransitionPhaseWithoutEffect() const; + // The (pseudo-)element whose computed transition-property refers to this // transition (if any). // -- cgit v1.2.3 From aade91b13a50ee4f246016fa8d8d1561f58f80ee Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Mar 2018 11:45:38 +0100 Subject: moebius#89: DOM - implement animationcancel event Issue #55 --- dom/animation/Animation.cpp | 13 ++++++- dom/animation/ComputedTiming.h | 4 +- dom/base/nsGkAtomList.h | 1 + dom/events/EventNameList.h | 4 ++ dom/webidl/EventHandler.webidl | 1 + layout/style/AnimationCommon.h | 20 ++++++++++ layout/style/nsAnimationManager.cpp | 75 ++++++++++++++++++++++-------------- layout/style/nsAnimationManager.h | 15 ++++++-- layout/style/nsTransitionManager.cpp | 21 +--------- layout/style/nsTransitionManager.h | 17 +------- widget/EventMessageList.h | 1 + 11 files changed, 99 insertions(+), 73 deletions(-) diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp index 242a0c6d6..bd318f79e 100644 --- a/dom/animation/Animation.cpp +++ b/dom/animation/Animation.cpp @@ -787,12 +787,21 @@ Animation::CancelNoUpdate() 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 diff --git a/dom/animation/ComputedTiming.h b/dom/animation/ComputedTiming.h index addf15865..525d59d23 100644 --- a/dom/animation/ComputedTiming.h +++ b/dom/animation/ComputedTiming.h @@ -64,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/base/nsGkAtomList.h b/dom/base/nsGkAtomList.h index 09d73e617..948487083 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") diff --git a/dom/events/EventNameList.h b/dom/events/EventNameList.h index 61942b251..00478c87b 100644 --- a/dom/events/EventNameList.h +++ b/dom/events/EventNameList.h @@ -1019,6 +1019,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/webidl/EventHandler.webidl b/dom/webidl/EventHandler.webidl index 6691ca224..306372ff1 100644 --- a/dom/webidl/EventHandler.webidl +++ b/dom/webidl/EventHandler.webidl @@ -128,6 +128,7 @@ interface GlobalEventHandlers { attribute EventHandler onmozpointerlockerror; // CSS-Animation and CSS-Transition handlers. + attribute EventHandler onanimationcancel; attribute EventHandler onanimationend; attribute EventHandler onanimationiteration; attribute EventHandler onanimationstart; 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 +PhaseType GetAnimationPhaseWithoutEffect(const dom::Animation& aAnimation) +{ + MOZ_ASSERT(!aAnimation.GetEffect(), + "Should only be called when we do not have an effect"); + + Nullable 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 8d4e8fcee..aa1b6fe78 100644 --- a/layout/style/nsAnimationManager.cpp +++ b/layout/style/nsAnimationManager.cpp @@ -158,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) { @@ -198,39 +194,60 @@ CSSAnimation::QueueEvents() } nsAnimationManager* manager = presContext->AnimationManager(); - ComputedTiming computedTiming = mEffect->GetComputedTiming(); - - ComputedTiming::AnimationPhase currentPhase = computedTiming.mPhase; - uint64_t currentIteration = computedTiming.mCurrentIteration; - if (currentPhase == mPreviousPhase && - currentIteration == mPreviousIteration) { - return; - } 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); - - uint64_t iterationBoundary = mPreviousIteration > currentIteration - ? currentIteration + 1 - : currentIteration; - StickyTimeDuration iterationStartTime = - computedTiming.mDuration.MultDouble( - (iterationBoundary - computedTiming.mIterationStart)); + uint64_t currentIteration = 0; + ComputedTiming::AnimationPhase currentPhase; + StickyTimeDuration intervalStartTime; + StickyTimeDuration intervalEndTime; + StickyTimeDuration iterationStartTime; + + if (!mEffect) { + currentPhase = GetAnimationPhaseWithoutEffect + (*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)); + } TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); TimeStamp iterationTimeStamp = ElapsedTimeToTimeStamp(iterationStartTime); AutoTArray 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 }); + } + switch (mPreviousPhase) { - case AnimationPhase::Null: + case AnimationPhase::Idle: case AnimationPhase::Before: if (currentPhase == AnimationPhase::Active) { events.AppendElement(AnimationEventParams{ eAnimationStart, diff --git a/layout/style/nsAnimationManager.h b/layout/style/nsAnimationManager.h index 868d4bb42..d838d090a 100644 --- a/layout/style/nsAnimationManager.h +++ b/layout/style/nsAnimationManager.h @@ -76,7 +76,7 @@ public: , mIsStylePaused(false) , mPauseShouldStick(false) , mNeedsNewAnimationIndexWhenRun(false) - , mPreviousPhase(ComputedTiming::AnimationPhase::Null) + , mPreviousPhase(ComputedTiming::AnimationPhase::Idle) , mPreviousIteration(0) { // We might need to drop this assertion once we add a script-accessible @@ -110,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. @@ -126,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; } @@ -158,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() { diff --git a/layout/style/nsTransitionManager.cpp b/layout/style/nsTransitionManager.cpp index abb9e2311..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; @@ -203,7 +201,7 @@ CSSTransition::QueueEvents(StickyTimeDuration aActiveTime) StickyTimeDuration intervalEndTime; if (!mEffect) { - currentPhase = GetTransitionPhaseWithoutEffect(); + currentPhase = GetAnimationPhaseWithoutEffect(*this); intervalStartTime = zeroDuration; intervalEndTime = zeroDuration; } else { @@ -329,23 +327,6 @@ CSSTransition::QueueEvents(StickyTimeDuration aActiveTime) } } -CSSTransition::TransitionPhase -CSSTransition::GetTransitionPhaseWithoutEffect() const -{ - MOZ_ASSERT(!mEffect, "Should only be called when we do not have an effect"); - - Nullable currentTime = GetCurrentTime(); - if (currentTime.IsNull()) { - return TransitionPhase::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() - ? TransitionPhase::Before - : TransitionPhase::After; -} - void CSSTransition::Tick() { diff --git a/layout/style/nsTransitionManager.h b/layout/style/nsTransitionManager.h index e2f198a40..1c48cc8cd 100644 --- a/layout/style/nsTransitionManager.h +++ b/layout/style/nsTransitionManager.h @@ -162,18 +162,6 @@ public: Animation::CancelFromStyle(); - // The above call to Animation::CancelFromStyle may cause a transitioncancel - // event to be queued. However, it will also remove the transition from its - // timeline. If this transition was the last animation attached to - // the timeline, the timeline will stop observing the refresh driver and - // there may be no subsequent tick fro dispatching animation events. - // - // To ensure the cancel event is dispatched we tell the timeline it needs to - // observe the refresh driver for at least one more tick. - if (mTimeline) { - mTimeline->NotifyAnimationUpdated(*this); - } - // It is important we do this *after* calling CancelFromStyle(). // This is because CancelFromStyle() will end up posting a restyle and // that restyle should target the *transitions* level of the cascade. @@ -245,9 +233,6 @@ protected: enum class TransitionPhase; - // Return the TransitionPhase to use when the transition doesn't have a target - // effect. - TransitionPhase GetTransitionPhaseWithoutEffect() const; // The (pseudo-)element whose computed transition-property refers to this // transition (if any). @@ -272,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(ComputedTiming::AnimationPhase::Null), + Idle = static_cast(ComputedTiming::AnimationPhase::Idle), Before = static_cast(ComputedTiming::AnimationPhase::Before), Active = static_cast(ComputedTiming::AnimationPhase::Active), After = static_cast(ComputedTiming::AnimationPhase::After), diff --git a/widget/EventMessageList.h b/widget/EventMessageList.h index 474e25862..7903f6a26 100644 --- a/widget/EventMessageList.h +++ b/widget/EventMessageList.h @@ -344,6 +344,7 @@ 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) -- cgit v1.2.3 From c58cec26c73da9a5c48f7d75555c6a2409965693 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Mar 2018 11:55:23 +0100 Subject: Bug 1202333: AnimationEvent elapsedTime should reflect playbackRate (added tests) Issue #55 --- .../test/css-animations/file_event-dispatch.html | 252 +++++++++++++++++++++ .../test/css-animations/file_event-order.html | 160 +++++++++++++ .../test/css-animations/test_event-dispatch.html | 15 ++ .../test/css-animations/test_event-order.html | 15 ++ dom/animation/test/mochitest.ini | 4 + dom/events/test/test_legacy_event.html | 21 +- .../transform-3d/animate-backface-hidden.html | 10 +- .../transform-3d/animate-preserve3d-parent.html | 10 +- layout/style/test/test_animations.html | 3 - layout/style/test/test_animations_omta.html | 3 - 10 files changed, 469 insertions(+), 24 deletions(-) create mode 100644 dom/animation/test/css-animations/file_event-dispatch.html create mode 100644 dom/animation/test/css-animations/file_event-order.html create mode 100644 dom/animation/test/css-animations/test_event-dispatch.html create mode 100644 dom/animation/test/css-animations/test_event-order.html 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 @@ + + +Tests for CSS animation event dispatch + + + + + + + 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 @@ + + +Tests for CSS animation event order + + + + + + + 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 @@ + + + + +
+ + diff --git a/dom/animation/test/css-animations/test_event-order.html b/dom/animation/test/css-animations/test_event-order.html new file mode 100644 index 000000000..57f1f3876 --- /dev/null +++ b/dom/animation/test/css-animations/test_event-order.html @@ -0,0 +1,15 @@ + + + + +
+ + diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini index c694e4d25..07e04d4d6 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 @@ -73,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] 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/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; } @@ -27,7 +27,13 @@ body { padding: 50px } 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 @@ - - -Tests for CSS-Transition events - - - - - - 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 @@ + + +Tests for CSS-Transition events + + + + + + 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_csstransition-events.html b/dom/animation/test/css-transitions/test_csstransition-events.html deleted file mode 100644 index 92559ad67..000000000 --- a/dom/animation/test/css-transitions/test_csstransition-events.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - -
- 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 @@ + + + + +
+ diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini index 07e04d4d6..db6dffada 100644 --- a/dom/animation/test/mochitest.ini +++ b/dom/animation/test/mochitest.ini @@ -30,11 +30,11 @@ support-files = css-transitions/file_animation-pausing.html css-transitions/file_animation-ready.html css-transitions/file_animation-starttime.html - css-transitions/file_csstransition-events.html css-transitions/file_csstransition-transitionproperty.html css-transitions/file_document-get-animations.html css-transitions/file_effect-target.html css-transitions/file_element-get-animations.html + css-transitions/file_event-dispatch.html css-transitions/file_keyframeeffect-getkeyframes.html css-transitions/file_pseudoElement-get-animations.html css-transitions/file_setting-effect.html @@ -86,11 +86,11 @@ support-files = [css-transitions/test_animation-pausing.html] [css-transitions/test_animation-ready.html] [css-transitions/test_animation-starttime.html] -[css-transitions/test_csstransition-events.html] [css-transitions/test_csstransition-transitionproperty.html] [css-transitions/test_document-get-animations.html] [css-transitions/test_effect-target.html] [css-transitions/test_element-get-animations.html] +[css-transitions/test_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/layout/style/test/test_animations_event_handler_attribute.html b/layout/style/test/test_animations_event_handler_attribute.html index 23a749daa..036a77779 100644 --- a/layout/style/test/test_animations_event_handler_attribute.html +++ b/layout/style/test/test_animations_event_handler_attribute.html @@ -88,11 +88,12 @@ 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([ 'ontransitionrun', 'ontransitionstart', - 'ontransitionend' ]); + 'ontransitionend', + 'ontransitioncancel' ]); targets.forEach(div => { div.style.transition = 'margin-left 100ms 200ms'; getComputedStyle(div).marginLeft; // flush @@ -111,6 +112,31 @@ 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 f948bf0a5..7204934d2 100644 --- a/layout/style/test/test_animations_event_order.html +++ b/layout/style/test/test_animations_event_order.html @@ -48,7 +48,8 @@ var gDisplay = document.getElementById('display'); 'animationend', 'transitionrun', 'transitionstart', - 'transitionend' ] + 'transitionend', + 'transitioncancel' ] .forEach(event => gDisplay.addEventListener(event, event => gEventsReceived.push(event), @@ -623,6 +624,43 @@ checkEventOrder([ divs[0], 'transitionrun' ], 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 = []; + SpecialPowers.DOMWindowUtils.restoreNormalRefresh(); -- cgit v1.2.3