| /* |
| * Copyright (c) 2013, Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "third_party/blink/renderer/core/animation/animation.h" |
| |
| #include <memory> |
| |
| #include "base/bits.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/renderer/bindings/core/v8/double_or_scroll_timeline_auto_keyword.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_optional_effect_timing.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_scroll_timeline_options.h" |
| #include "third_party/blink/renderer/core/animation/animation_clock.h" |
| #include "third_party/blink/renderer/core/animation/css/compositor_keyframe_double.h" |
| #include "third_party/blink/renderer/core/animation/css_number_interpolation_type.h" |
| #include "third_party/blink/renderer/core/animation/document_timeline.h" |
| #include "third_party/blink/renderer/core/animation/element_animations.h" |
| #include "third_party/blink/renderer/core/animation/keyframe_effect.h" |
| #include "third_party/blink/renderer/core/animation/keyframe_effect_model.h" |
| #include "third_party/blink/renderer/core/animation/pending_animations.h" |
| #include "third_party/blink/renderer/core/animation/scroll_timeline.h" |
| #include "third_party/blink/renderer/core/animation/timing.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/dom_node_ids.h" |
| #include "third_party/blink/renderer/core/dom/events/event.h" |
| #include "third_party/blink/renderer/core/dom/events/native_event_listener.h" |
| #include "third_party/blink/renderer/core/dom/qualified_name.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h" |
| #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h" |
| #include "third_party/blink/renderer/platform/animation/compositor_animation.h" |
| #include "third_party/blink/renderer/platform/animation/compositor_keyframe_model.h" |
| #include "third_party/blink/renderer/platform/animation/compositor_target_property.h" |
| #include "third_party/blink/renderer/platform/bindings/microtask.h" |
| #include "third_party/blink/renderer/platform/bindings/v8_per_isolate_data.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/testing/histogram_tester.h" |
| #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" |
| #include "third_party/blink/renderer/platform/weborigin/kurl.h" |
| |
| namespace blink { |
| |
| void ExpectRelativeErrorWithinEpsilon(double expected, double observed) { |
| EXPECT_NEAR(1.0, observed / expected, std::numeric_limits<double>::epsilon()); |
| } |
| |
| class AnimationAnimationTestNoCompositing : public RenderingTest { |
| public: |
| AnimationAnimationTestNoCompositing() |
| : RenderingTest(MakeGarbageCollected<SingleChildLocalFrameClient>()) {} |
| |
| void SetUp() override { |
| last_frame_time = 0; |
| RenderingTest::SetUp(); |
| SetUpWithoutStartingTimeline(); |
| StartTimeline(); |
| } |
| |
| void SetUpWithoutStartingTimeline() { |
| GetDocument().GetAnimationClock().ResetTimeForTesting(); |
| timeline = GetDocument().Timeline(); |
| timeline->ResetForTesting(); |
| animation = timeline->Play(nullptr); |
| animation->setStartTime(CSSNumberish::FromDouble(0)); |
| animation->setEffect(MakeAnimation()); |
| } |
| |
| void StartTimeline() { SimulateFrame(0); } |
| |
| KeyframeEffectModelBase* MakeSimpleEffectModel() { |
| PropertyHandle PropertyHandleOpacity(GetCSSPropertyOpacity()); |
| TransitionKeyframe* start_keyframe = |
| MakeGarbageCollected<TransitionKeyframe>(PropertyHandleOpacity); |
| start_keyframe->SetValue(std::make_unique<TypedInterpolationValue>( |
| CSSNumberInterpolationType(PropertyHandleOpacity), |
| std::make_unique<InterpolableNumber>(1.0))); |
| start_keyframe->SetOffset(0.0); |
| // Egregious hack: Sideload the compositor value. |
| // This is usually set in a part of the rendering process SimulateFrame |
| // doesn't call. |
| start_keyframe->SetCompositorValue( |
| MakeGarbageCollected<CompositorKeyframeDouble>(1.0)); |
| TransitionKeyframe* end_keyframe = |
| MakeGarbageCollected<TransitionKeyframe>(PropertyHandleOpacity); |
| end_keyframe->SetValue(std::make_unique<TypedInterpolationValue>( |
| CSSNumberInterpolationType(PropertyHandleOpacity), |
| std::make_unique<InterpolableNumber>(0.0))); |
| end_keyframe->SetOffset(1.0); |
| // Egregious hack: Sideload the compositor value. |
| end_keyframe->SetCompositorValue( |
| MakeGarbageCollected<CompositorKeyframeDouble>(0.0)); |
| |
| TransitionKeyframeVector keyframes; |
| keyframes.push_back(start_keyframe); |
| keyframes.push_back(end_keyframe); |
| |
| return MakeGarbageCollected<TransitionKeyframeEffectModel>(keyframes); |
| } |
| |
| void ResetWithCompositedAnimation() { |
| // Get rid of the default animation. |
| animation->cancel(); |
| |
| RunDocumentLifecycle(); |
| |
| SetBodyInnerHTML("<div id='target'></div>"); |
| |
| MakeCompositedAnimation(); |
| } |
| |
| void MakeCompositedAnimation() { |
| // Create a compositable animation; in this case opacity from 1 to 0. |
| Timing timing; |
| timing.iteration_duration = AnimationTimeDelta::FromSecondsD(30); |
| |
| Persistent<StringKeyframe> start_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| start_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "1.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| Persistent<StringKeyframe> end_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| end_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "0.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| |
| StringKeyframeVector keyframes; |
| keyframes.push_back(start_keyframe); |
| keyframes.push_back(end_keyframe); |
| |
| Element* element = GetElementById("target"); |
| auto* model = MakeGarbageCollected<StringKeyframeEffectModel>(keyframes); |
| animation = timeline->Play( |
| MakeGarbageCollected<KeyframeEffect>(element, model, timing)); |
| |
| // After creating the animation we need to clean the lifecycle so that the |
| // animation can be pushed to the compositor. |
| UpdateAllLifecyclePhasesForTest(); |
| |
| GetDocument().GetAnimationClock().UpdateTime(base::TimeTicks()); |
| GetDocument().GetPendingAnimations().Update(nullptr, true); |
| } |
| |
| KeyframeEffectModelBase* MakeEmptyEffectModel() { |
| return MakeGarbageCollected<StringKeyframeEffectModel>( |
| StringKeyframeVector()); |
| } |
| |
| KeyframeEffect* MakeAnimation( |
| double duration = 30, |
| Timing::FillMode fill_mode = Timing::FillMode::AUTO) { |
| Timing timing; |
| timing.iteration_duration = AnimationTimeDelta::FromSecondsD(duration); |
| timing.fill_mode = fill_mode; |
| return MakeGarbageCollected<KeyframeEffect>(nullptr, MakeEmptyEffectModel(), |
| timing); |
| } |
| |
| bool SimulateFrame(double time_ms) { |
| if (animation->pending()) { |
| animation->NotifyReady( |
| AnimationTimeDelta::FromMillisecondsD(last_frame_time)); |
| } |
| SimulateMicrotask(); |
| |
| last_frame_time = time_ms; |
| const auto* paint_artifact_compositor = |
| GetDocument().GetFrame()->View()->GetPaintArtifactCompositor(); |
| GetDocument().GetAnimationClock().UpdateTime( |
| base::TimeTicks() + base::TimeDelta::FromMillisecondsD(time_ms)); |
| GetDocument().GetPendingAnimations().Update(paint_artifact_compositor, |
| false); |
| // The timeline does not know about our animation, so we have to explicitly |
| // call update(). |
| return animation->Update(kTimingUpdateForAnimationFrame); |
| } |
| |
| void SimulateAwaitReady() { SimulateFrame(last_frame_time); } |
| |
| void SimulateMicrotask() { |
| Microtask::PerformCheckpoint(V8PerIsolateData::MainThreadIsolate()); |
| } |
| |
| void SimulateFrameForScrollAnimations() { |
| // Advance time by 100 ms. |
| auto new_time = GetAnimationClock().CurrentTime() + |
| base::TimeDelta::FromMilliseconds(100); |
| GetPage().Animator().ServiceScriptedAnimations(new_time); |
| } |
| |
| bool StartTimeIsSet(Persistent<blink::Animation> animation) { |
| CSSNumberish start_time; |
| animation->startTime(start_time); |
| return !start_time.IsNull(); |
| } |
| |
| bool CurrentTimeIsSet(Persistent<blink::Animation> animation) { |
| CSSNumberish current_time; |
| animation->currentTime(current_time); |
| return !current_time.IsNull(); |
| } |
| |
| double GetStartTimeMs(Persistent<blink::Animation> animation) { |
| CSSNumberish start_time; |
| animation->startTime(start_time); |
| return start_time.GetAsDouble(); |
| } |
| |
| double GetCurrentTimeMs(Persistent<blink::Animation> animation) { |
| CSSNumberish current_time; |
| animation->currentTime(current_time); |
| return current_time.GetAsDouble(); |
| } |
| |
| #define EXPECT_TIME(expected, observed) \ |
| EXPECT_NEAR(expected, observed, Animation::kTimeToleranceMs) |
| |
| Persistent<DocumentTimeline> timeline; |
| Persistent<Animation> animation; |
| |
| private: |
| double last_frame_time; |
| }; |
| |
| class AnimationAnimationTestCompositing |
| : public AnimationAnimationTestNoCompositing { |
| public: |
| Animation* CreateAnimation(CSSPropertyID property_id, |
| String from, |
| String to) { |
| Timing timing; |
| timing.iteration_duration = AnimationTimeDelta::FromSecondsD(30); |
| |
| Persistent<StringKeyframe> start_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| start_keyframe->SetCSSPropertyValue( |
| property_id, from, SecureContextMode::kInsecureContext, nullptr); |
| Persistent<StringKeyframe> end_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| end_keyframe->SetCSSPropertyValue( |
| property_id, to, SecureContextMode::kInsecureContext, nullptr); |
| |
| StringKeyframeVector keyframes; |
| keyframes.push_back(start_keyframe); |
| keyframes.push_back(end_keyframe); |
| |
| Element* element = GetElementById("target"); |
| auto* model = MakeGarbageCollected<StringKeyframeEffectModel>(keyframes); |
| |
| NonThrowableExceptionState exception_state; |
| DocumentTimeline* timeline = |
| MakeGarbageCollected<DocumentTimeline>(&GetDocument()); |
| return Animation::Create( |
| MakeGarbageCollected<KeyframeEffect>(element, model, timing), timeline, |
| exception_state); |
| } |
| |
| private: |
| void SetUp() override { |
| EnableCompositing(); |
| AnimationAnimationTestNoCompositing::SetUp(); |
| } |
| }; |
| |
| class AnimationAnimationTestCompositeAfterPaint |
| : public AnimationAnimationTestNoCompositing { |
| void SetUp() override { |
| EnableCompositing(); |
| AnimationAnimationTestNoCompositing::SetUp(); |
| } |
| |
| ScopedCompositeAfterPaintForTest enable_cap{true}; |
| }; |
| |
| TEST_F(AnimationAnimationTestNoCompositing, InitialState) { |
| SetUpWithoutStartingTimeline(); |
| animation = timeline->Play(nullptr); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| EXPECT_TRUE(animation->pending()); |
| EXPECT_FALSE(animation->Paused()); |
| EXPECT_EQ(1, animation->playbackRate()); |
| EXPECT_FALSE(StartTimeIsSet(animation)); |
| |
| StartTimeline(); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_TIME(0, timeline->CurrentTimeMilliseconds().value()); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| EXPECT_FALSE(animation->Paused()); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_EQ(1, animation->playbackRate()); |
| EXPECT_TIME(0, GetStartTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, CurrentTimeDoesNotSetOutdated) { |
| EXPECT_FALSE(animation->Outdated()); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| EXPECT_FALSE(animation->Outdated()); |
| // FIXME: We should split simulateFrame into a version that doesn't update |
| // the animation and one that does, as most of the tests don't require |
| // update() to be called. |
| GetDocument().GetAnimationClock().UpdateTime( |
| base::TimeTicks() + base::TimeDelta::FromMilliseconds(10000)); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| EXPECT_FALSE(animation->Outdated()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetCurrentTime) { |
| EXPECT_EQ("running", animation->playState()); |
| animation->setCurrentTime(CSSNumberish::FromDouble(10000)); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| |
| SimulateFrame(10000); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetCurrentTimeNegative) { |
| animation->setCurrentTime(CSSNumberish::FromDouble(-10000)); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TIME(-10000, GetCurrentTimeMs(animation)); |
| |
| SimulateFrame(20000); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| animation->setPlaybackRate(-2); |
| animation->setCurrentTime(CSSNumberish::FromDouble(-10000)); |
| EXPECT_EQ("finished", animation->playState()); |
| // A seek can set current time outside the range [0, EffectEnd()]. |
| EXPECT_TIME(-10000, GetCurrentTimeMs(animation)); |
| |
| SimulateFrame(40000); |
| // Hold current time even though outside normal range for the animation. |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_TIME(-10000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, |
| SetCurrentTimeNegativeWithoutSimultaneousPlaybackRateChange) { |
| SimulateFrame(20000); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| EXPECT_EQ("running", animation->playState()); |
| |
| // Reversing the direction preserves current time. |
| animation->setPlaybackRate(-1); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| SimulateAwaitReady(); |
| |
| SimulateFrame(30000); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| EXPECT_EQ("running", animation->playState()); |
| |
| animation->setCurrentTime(CSSNumberish::FromDouble(-10000)); |
| EXPECT_EQ("finished", animation->playState()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetCurrentTimePastContentEnd) { |
| animation->setCurrentTime(CSSNumberish::FromDouble(50000)); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_TIME(50000, GetCurrentTimeMs(animation)); |
| |
| SimulateFrame(20000); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_TIME(50000, GetCurrentTimeMs(animation)); |
| // Reversing the play direction changes the play state from finished to |
| // running. |
| animation->setPlaybackRate(-2); |
| animation->setCurrentTime(CSSNumberish::FromDouble(50000)); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TIME(50000, GetCurrentTimeMs(animation)); |
| SimulateAwaitReady(); |
| |
| SimulateFrame(40000); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetCurrentTimeMax) { |
| double limit = std::numeric_limits<double>::max(); |
| animation->setCurrentTime(CSSNumberish::FromDouble(limit)); |
| CSSNumberish current_time; |
| animation->currentTime(current_time); |
| ExpectRelativeErrorWithinEpsilon(limit, current_time.GetAsDouble()); |
| |
| SimulateFrame(100000); |
| animation->currentTime(current_time); |
| ExpectRelativeErrorWithinEpsilon(limit, current_time.GetAsDouble()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetCurrentTimeSetsStartTime) { |
| EXPECT_TIME(0, GetStartTimeMs(animation)); |
| animation->setCurrentTime(CSSNumberish::FromDouble(1000)); |
| EXPECT_TIME(-1000, GetStartTimeMs(animation)); |
| |
| SimulateFrame(1000); |
| EXPECT_TIME(-1000, GetStartTimeMs(animation)); |
| EXPECT_TIME(2000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetStartTime) { |
| SimulateFrame(20000); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TIME(0, GetStartTimeMs(animation)); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| animation->setStartTime(CSSNumberish::FromDouble(10000)); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TIME(10000, GetStartTimeMs(animation)); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| |
| SimulateFrame(30000); |
| EXPECT_TIME(10000, GetStartTimeMs(animation)); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| animation->setStartTime(CSSNumberish::FromDouble(-20000)); |
| EXPECT_EQ("finished", animation->playState()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetStartTimeLimitsAnimation) { |
| // Setting the start time is a seek operation, which is not constrained by the |
| // normal limits on the animation. |
| animation->setStartTime(CSSNumberish::FromDouble(-50000)); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_TRUE(animation->Limited()); |
| EXPECT_TIME(50000, GetCurrentTimeMs(animation)); |
| animation->setPlaybackRate(-1); |
| EXPECT_EQ("running", animation->playState()); |
| animation->setStartTime(CSSNumberish::FromDouble(-100000)); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_TIME(-100000, GetCurrentTimeMs(animation)); |
| EXPECT_TRUE(animation->Limited()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetStartTimeOnLimitedAnimation) { |
| // The setStartTime method is a seek and thus not constrained by the normal |
| // limits on the animation. |
| SimulateFrame(30000); |
| animation->setStartTime(CSSNumberish::FromDouble(-10000)); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_TIME(40000, GetCurrentTimeMs(animation)); |
| EXPECT_TRUE(animation->Limited()); |
| |
| animation->setCurrentTime(CSSNumberish::FromDouble(50000)); |
| EXPECT_TIME(50000, GetCurrentTimeMs(animation)); |
| animation->setStartTime(CSSNumberish::FromDouble(-40000)); |
| EXPECT_TIME(70000, GetCurrentTimeMs(animation)); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_TRUE(animation->Limited()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, StartTimePauseFinish) { |
| NonThrowableExceptionState exception_state; |
| animation->pause(); |
| EXPECT_EQ("paused", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| SimulateAwaitReady(); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_FALSE(StartTimeIsSet(animation)); |
| animation->finish(exception_state); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_TIME(-30000, GetStartTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, FinishWhenPaused) { |
| NonThrowableExceptionState exception_state; |
| animation->pause(); |
| EXPECT_EQ("paused", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| |
| SimulateFrame(10000); |
| EXPECT_EQ("paused", animation->playState()); |
| EXPECT_FALSE(animation->pending()); |
| animation->finish(exception_state); |
| EXPECT_EQ("finished", animation->playState()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, StartTimeFinishPause) { |
| NonThrowableExceptionState exception_state; |
| animation->finish(exception_state); |
| EXPECT_TIME(-30000, GetStartTimeMs(animation)); |
| animation->pause(); |
| EXPECT_EQ("paused", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| SimulateAwaitReady(); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_FALSE(StartTimeIsSet(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, StartTimeWithZeroPlaybackRate) { |
| animation->setPlaybackRate(0); |
| EXPECT_EQ("running", animation->playState()); |
| SimulateAwaitReady(); |
| EXPECT_TRUE(StartTimeIsSet(animation)); |
| |
| SimulateFrame(10000); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, PausePlay) { |
| // Pause the animation at the 10s mark. |
| SimulateFrame(10000); |
| animation->pause(); |
| EXPECT_EQ("paused", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| |
| // Resume playing the animation at the 20s mark. |
| SimulateFrame(20000); |
| EXPECT_EQ("paused", animation->playState()); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| animation->play(); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| SimulateAwaitReady(); |
| EXPECT_FALSE(animation->pending()); |
| |
| // Advance another 10s. |
| SimulateFrame(30000); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, PlayRewindsToStart) { |
| // Auto-replay when starting from limit. |
| animation->setCurrentTime(CSSNumberish::FromDouble(30000)); |
| animation->play(); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| |
| // Auto-replay when starting past the upper bound. |
| animation->setCurrentTime(CSSNumberish::FromDouble(40000)); |
| animation->play(); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| |
| // Snap to start of the animation if playing in forward direction starting |
| // from a negative value of current time. |
| SimulateFrame(10000); |
| EXPECT_FALSE(animation->pending()); |
| animation->setCurrentTime(CSSNumberish::FromDouble(-10000)); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_FALSE(animation->pending()); |
| animation->play(); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| SimulateAwaitReady(); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_FALSE(animation->pending()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, PlayRewindsToEnd) { |
| // Snap to end when playing a reversed animation from the start. |
| animation->setPlaybackRate(-1); |
| animation->play(); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| |
| // Snap to end if playing a reversed animation starting past the upper limit. |
| animation->setCurrentTime(CSSNumberish::FromDouble(40000)); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| animation->play(); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| EXPECT_TRUE(animation->pending()); |
| |
| SimulateFrame(10000); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_FALSE(animation->pending()); |
| |
| // Snap to the end if playing a reversed animation starting with a negative |
| // value for current time. |
| animation->setCurrentTime(CSSNumberish::FromDouble(-10000)); |
| animation->play(); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| |
| SimulateFrame(20000); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_FALSE(animation->pending()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, |
| PlayWithPlaybackRateZeroDoesNotSeek) { |
| // When playback rate is zero, any value set for the current time effectively |
| // becomes the hold time. |
| animation->setPlaybackRate(0); |
| animation->play(); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| |
| animation->setCurrentTime(CSSNumberish::FromDouble(40000)); |
| animation->play(); |
| EXPECT_TIME(40000, GetCurrentTimeMs(animation)); |
| |
| animation->setCurrentTime(CSSNumberish::FromDouble(-10000)); |
| animation->play(); |
| EXPECT_TIME(-10000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, |
| PlayAfterPauseWithPlaybackRateZeroUpdatesPlayState) { |
| animation->pause(); |
| animation->setPlaybackRate(0); |
| |
| SimulateFrame(1000); |
| EXPECT_EQ("paused", animation->playState()); |
| animation->play(); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, Reverse) { |
| animation->setCurrentTime(CSSNumberish::FromDouble(10000)); |
| animation->pause(); |
| animation->reverse(); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| // Effective playback rate does not kick in until the animation is ready. |
| EXPECT_EQ(1, animation->playbackRate()); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| SimulateAwaitReady(); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_EQ(-1, animation->playbackRate()); |
| // Updating the playback rate does not change current time. |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, |
| ReverseHoldsCurrentTimeWithPlaybackRateZero) { |
| animation->setCurrentTime(CSSNumberish::FromDouble(10000)); |
| animation->setPlaybackRate(0); |
| animation->pause(); |
| animation->reverse(); |
| SimulateAwaitReady(); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_EQ(0, animation->playbackRate()); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| |
| SimulateFrame(20000); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, ReverseSeeksToStart) { |
| animation->setCurrentTime(CSSNumberish::FromDouble(-10000)); |
| animation->setPlaybackRate(-1); |
| animation->reverse(); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, ReverseSeeksToEnd) { |
| animation->setCurrentTime(CSSNumberish::FromDouble(40000)); |
| animation->reverse(); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, ReverseBeyondLimit) { |
| animation->setCurrentTime(CSSNumberish::FromDouble(40000)); |
| animation->setPlaybackRate(-1); |
| animation->reverse(); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| |
| animation->setCurrentTime(CSSNumberish::FromDouble(-10000)); |
| animation->reverse(); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, Finish) { |
| NonThrowableExceptionState exception_state; |
| animation->finish(exception_state); |
| // Finished snaps to the end of the animation. |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| EXPECT_EQ("finished", animation->playState()); |
| // Finished is a synchronous operation. |
| EXPECT_FALSE(animation->pending()); |
| |
| animation->setPlaybackRate(-1); |
| animation->finish(exception_state); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_FALSE(animation->pending()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, FinishAfterEffectEnd) { |
| NonThrowableExceptionState exception_state; |
| // OK to set current time out of bounds. |
| animation->setCurrentTime(CSSNumberish::FromDouble(40000)); |
| animation->finish(exception_state); |
| // The finish method triggers a snap to the upper boundary. |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, FinishBeforeStart) { |
| NonThrowableExceptionState exception_state; |
| animation->setCurrentTime(CSSNumberish::FromDouble(-10000)); |
| animation->setPlaybackRate(-1); |
| animation->finish(exception_state); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, |
| FinishDoesNothingWithPlaybackRateZero) { |
| // Cannot finish an animation that has a playback rate of zero. |
| DummyExceptionStateForTesting exception_state; |
| animation->setCurrentTime(CSSNumberish::FromDouble(10000)); |
| animation->setPlaybackRate(0); |
| animation->finish(exception_state); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| EXPECT_TRUE(exception_state.HadException()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, FinishRaisesException) { |
| // Cannot finish an animation that has an infinite iteration-count and a |
| // non-zero iteration-duration. |
| Timing timing; |
| timing.iteration_duration = AnimationTimeDelta::FromSecondsD(1); |
| timing.iteration_count = std::numeric_limits<double>::infinity(); |
| animation->setEffect(MakeGarbageCollected<KeyframeEffect>( |
| nullptr, MakeEmptyEffectModel(), timing)); |
| animation->setCurrentTime(CSSNumberish::FromDouble(10000)); |
| |
| DummyExceptionStateForTesting exception_state; |
| animation->finish(exception_state); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| EXPECT_TRUE(exception_state.HadException()); |
| EXPECT_EQ(DOMExceptionCode::kInvalidStateError, |
| exception_state.CodeAs<DOMExceptionCode>()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, LimitingAtEffectEnd) { |
| SimulateFrame(30000); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| EXPECT_TRUE(animation->Limited()); |
| |
| // Cannot run past the end of the animation without a seek. |
| SimulateFrame(40000); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| EXPECT_FALSE(animation->Paused()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, LimitingAtStart) { |
| SimulateFrame(30000); |
| animation->setPlaybackRate(-2); |
| SimulateAwaitReady(); |
| |
| SimulateFrame(45000); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| EXPECT_TRUE(animation->Limited()); |
| |
| SimulateFrame(60000); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| EXPECT_FALSE(animation->Paused()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, LimitingWithNoEffect) { |
| animation->setEffect(nullptr); |
| EXPECT_TRUE(animation->Limited()); |
| SimulateFrame(30000); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetPlaybackRate) { |
| animation->setPlaybackRate(2); |
| SimulateAwaitReady(); |
| EXPECT_EQ(2, animation->playbackRate()); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| |
| SimulateFrame(10000); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetPlaybackRateWhilePaused) { |
| SimulateFrame(10000); |
| animation->pause(); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| animation->setPlaybackRate(2); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| SimulateAwaitReady(); |
| |
| SimulateFrame(20000); |
| animation->play(); |
| // Change to playback rate does not alter current time. |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| SimulateAwaitReady(); |
| |
| SimulateFrame(25000); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetPlaybackRateWhileLimited) { |
| // Animation plays until it hits the upper bound. |
| SimulateFrame(40000); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| EXPECT_TRUE(animation->Limited()); |
| animation->setPlaybackRate(2); |
| SimulateAwaitReady(); |
| |
| // Already at the end of the animation. |
| SimulateFrame(50000); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| animation->setPlaybackRate(-2); |
| SimulateAwaitReady(); |
| |
| SimulateFrame(60000); |
| EXPECT_FALSE(animation->Limited()); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetPlaybackRateZero) { |
| SimulateFrame(10000); |
| animation->setPlaybackRate(0); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| |
| SimulateFrame(20000); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| animation->setCurrentTime(CSSNumberish::FromDouble(20000)); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetPlaybackRateMax) { |
| animation->setPlaybackRate(std::numeric_limits<double>::max()); |
| EXPECT_EQ(std::numeric_limits<double>::max(), animation->playbackRate()); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| SimulateAwaitReady(); |
| |
| SimulateFrame(1); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, UpdatePlaybackRate) { |
| animation->updatePlaybackRate(2); |
| EXPECT_EQ(1, animation->playbackRate()); |
| SimulateAwaitReady(); |
| EXPECT_EQ(2, animation->playbackRate()); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| |
| SimulateFrame(10000); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, UpdatePlaybackRateWhilePaused) { |
| animation->pause(); |
| |
| // Pending playback rate on pending-paused animation is picked up after async |
| // tick. |
| EXPECT_EQ("paused", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| animation->updatePlaybackRate(2); |
| EXPECT_EQ(1, animation->playbackRate()); |
| SimulateAwaitReady(); |
| EXPECT_EQ(2, animation->playbackRate()); |
| EXPECT_FALSE(animation->pending()); |
| |
| // Pending playback rate on a paused animation is resolved immediately. |
| animation->updatePlaybackRate(3); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_EQ(3, animation->playbackRate()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, UpdatePlaybackRateWhileLimited) { |
| NonThrowableExceptionState exception_state; |
| animation->finish(exception_state); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| |
| // Updating playback rate does not affect current time. |
| animation->updatePlaybackRate(2); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| |
| // Updating payback rate is resolved immediately for an animation in the |
| // finished state. |
| EXPECT_EQ(2, animation->playbackRate()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, UpdatePlaybackRateWhileRunning) { |
| animation->play(); |
| SimulateFrame(1000); |
| animation->updatePlaybackRate(2); |
| |
| // Updating playback rate triggers pending state for the play state. |
| // Pending playback rate is not resolved until next async tick. |
| EXPECT_TRUE(animation->pending()); |
| EXPECT_EQ(1, animation->playbackRate()); |
| SimulateAwaitReady(); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_EQ(2, animation->playbackRate()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetEffect) { |
| animation = timeline->Play(nullptr); |
| animation->setStartTime(CSSNumberish::FromDouble(0)); |
| AnimationEffect* effect1 = MakeAnimation(); |
| AnimationEffect* effect2 = MakeAnimation(); |
| animation->setEffect(effect1); |
| EXPECT_EQ(effect1, animation->effect()); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| animation->setCurrentTime(CSSNumberish::FromDouble(15000)); |
| animation->setEffect(effect2); |
| EXPECT_TIME(15000, GetCurrentTimeMs(animation)); |
| EXPECT_EQ(nullptr, effect1->GetAnimationForTesting()); |
| EXPECT_EQ(animation, effect2->GetAnimationForTesting()); |
| EXPECT_EQ(effect2, animation->effect()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetEffectLimitsAnimation) { |
| animation->setCurrentTime(CSSNumberish::FromDouble(20000)); |
| animation->setEffect(MakeAnimation(10)); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| EXPECT_TRUE(animation->Limited()); |
| SimulateFrame(10000); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, SetEffectUnlimitsAnimation) { |
| animation->setCurrentTime(CSSNumberish::FromDouble(40000)); |
| animation->setEffect(MakeAnimation(60)); |
| EXPECT_FALSE(animation->Limited()); |
| EXPECT_TIME(40000, GetCurrentTimeMs(animation)); |
| SimulateFrame(10000); |
| EXPECT_TIME(50000, GetCurrentTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, EmptyAnimationsDontUpdateEffects) { |
| animation = timeline->Play(nullptr); |
| animation->Update(kTimingUpdateOnDemand); |
| EXPECT_EQ(base::nullopt, animation->TimeToEffectChange()); |
| |
| SimulateFrame(1234); |
| EXPECT_EQ(base::nullopt, animation->TimeToEffectChange()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, AnimationsDisassociateFromEffect) { |
| AnimationEffect* animation_node = animation->effect(); |
| Animation* animation2 = timeline->Play(animation_node); |
| EXPECT_EQ(nullptr, animation->effect()); |
| animation->setEffect(animation_node); |
| EXPECT_EQ(nullptr, animation2->effect()); |
| } |
| |
| #define EXPECT_TIMEDELTA(expected, observed) \ |
| EXPECT_NEAR(expected.InMillisecondsF(), observed.InMillisecondsF(), \ |
| Animation::kTimeToleranceMs) |
| |
| TEST_F(AnimationAnimationTestNoCompositing, AnimationsReturnTimeToNextEffect) { |
| Timing timing; |
| timing.start_delay = 1; |
| timing.iteration_duration = AnimationTimeDelta::FromSecondsD(1); |
| timing.end_delay = 1; |
| auto* keyframe_effect = MakeGarbageCollected<KeyframeEffect>( |
| nullptr, MakeEmptyEffectModel(), timing); |
| animation = timeline->Play(keyframe_effect); |
| animation->setStartTime(CSSNumberish::FromDouble(0)); |
| |
| // Next effect change at end of start delay. |
| SimulateFrame(0); |
| EXPECT_TIMEDELTA(AnimationTimeDelta::FromSecondsD(1), |
| animation->TimeToEffectChange().value()); |
| |
| // Next effect change at end of start delay. |
| SimulateFrame(500); |
| EXPECT_TIMEDELTA(AnimationTimeDelta::FromSecondsD(0.5), |
| animation->TimeToEffectChange().value()); |
| |
| // Start of active phase. |
| SimulateFrame(1000); |
| EXPECT_TIMEDELTA(AnimationTimeDelta(), |
| animation->TimeToEffectChange().value()); |
| |
| // Still in active phase. |
| SimulateFrame(1500); |
| EXPECT_TIMEDELTA(AnimationTimeDelta(), |
| animation->TimeToEffectChange().value()); |
| |
| // Start of the after phase. Next effect change at end of after phase. |
| SimulateFrame(2000); |
| EXPECT_TIMEDELTA(AnimationTimeDelta::FromSecondsD(1), |
| animation->TimeToEffectChange().value()); |
| |
| // Still in effect if fillmode = forward|both. |
| SimulateFrame(3000); |
| EXPECT_EQ(base::nullopt, animation->TimeToEffectChange()); |
| |
| // Reset to start of animation. Next effect at the end of the start delay. |
| animation->setCurrentTime(CSSNumberish::FromDouble(0)); |
| SimulateFrame(3000); |
| EXPECT_TIMEDELTA(AnimationTimeDelta::FromSecondsD(1), |
| animation->TimeToEffectChange().value()); |
| |
| // Start delay is scaled by playback rate. |
| animation->setPlaybackRate(2); |
| SimulateFrame(3000); |
| EXPECT_TIMEDELTA(AnimationTimeDelta::FromSecondsD(0.5), |
| animation->TimeToEffectChange().value()); |
| |
| // Effectively a paused animation. |
| animation->setPlaybackRate(0); |
| animation->Update(kTimingUpdateOnDemand); |
| EXPECT_EQ(base::nullopt, animation->TimeToEffectChange()); |
| |
| // Reversed animation from end time. Next effect after end delay. |
| animation->setCurrentTime(CSSNumberish::FromDouble(3000)); |
| animation->setPlaybackRate(-1); |
| animation->Update(kTimingUpdateOnDemand); |
| SimulateFrame(3000); |
| EXPECT_TIMEDELTA(AnimationTimeDelta::FromSecondsD(1), |
| animation->TimeToEffectChange().value()); |
| |
| // End delay is scaled by playback rate. |
| animation->setPlaybackRate(-2); |
| animation->Update(kTimingUpdateOnDemand); |
| SimulateFrame(3000); |
| EXPECT_TIMEDELTA(AnimationTimeDelta::FromSecondsD(0.5), |
| animation->TimeToEffectChange().value()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, TimeToNextEffectWhenPaused) { |
| EXPECT_TIMEDELTA(AnimationTimeDelta(), |
| animation->TimeToEffectChange().value()); |
| animation->pause(); |
| EXPECT_TRUE(animation->pending()); |
| EXPECT_EQ("paused", animation->playState()); |
| SimulateAwaitReady(); |
| EXPECT_FALSE(animation->pending()); |
| animation->Update(kTimingUpdateOnDemand); |
| EXPECT_EQ(base::nullopt, animation->TimeToEffectChange()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, |
| TimeToNextEffectWhenCancelledBeforeStart) { |
| EXPECT_TIMEDELTA(AnimationTimeDelta(), |
| animation->TimeToEffectChange().value()); |
| animation->setCurrentTime(CSSNumberish::FromDouble(-8000)); |
| animation->setPlaybackRate(2); |
| EXPECT_EQ("running", animation->playState()); |
| animation->cancel(); |
| EXPECT_EQ("idle", animation->playState()); |
| EXPECT_FALSE(animation->pending()); |
| animation->Update(kTimingUpdateOnDemand); |
| // This frame will fire the finish event event though no start time has been |
| // received from the compositor yet, as cancel() nukes start times. |
| EXPECT_EQ(base::nullopt, animation->TimeToEffectChange()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, |
| TimeToNextEffectWhenCancelledBeforeStartReverse) { |
| EXPECT_TIMEDELTA(AnimationTimeDelta(), |
| animation->TimeToEffectChange().value()); |
| animation->setCurrentTime(CSSNumberish::FromDouble(9000)); |
| animation->setPlaybackRate(-3); |
| EXPECT_EQ("running", animation->playState()); |
| animation->cancel(); |
| EXPECT_EQ("idle", animation->playState()); |
| EXPECT_FALSE(animation->pending()); |
| animation->Update(kTimingUpdateOnDemand); |
| EXPECT_EQ(base::nullopt, animation->TimeToEffectChange()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, |
| TimeToNextEffectSimpleCancelledBeforeStart) { |
| EXPECT_TIMEDELTA(AnimationTimeDelta(), |
| animation->TimeToEffectChange().value()); |
| EXPECT_EQ("running", animation->playState()); |
| animation->cancel(); |
| EXPECT_EQ("idle", animation->playState()); |
| EXPECT_FALSE(animation->pending()); |
| animation->Update(kTimingUpdateOnDemand); |
| EXPECT_EQ(base::nullopt, animation->TimeToEffectChange()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, AttachedAnimations) { |
| Persistent<Element> element = GetDocument().CreateElementForBinding("foo"); |
| |
| Timing timing; |
| auto* keyframe_effect = MakeGarbageCollected<KeyframeEffect>( |
| element.Get(), MakeEmptyEffectModel(), timing); |
| Animation* animation = timeline->Play(keyframe_effect); |
| SimulateFrame(0); |
| timeline->ServiceAnimations(kTimingUpdateForAnimationFrame); |
| EXPECT_EQ( |
| 1U, element->GetElementAnimations()->Animations().find(animation)->value); |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| EXPECT_TRUE(element->GetElementAnimations()->Animations().IsEmpty()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, HasLowerCompositeOrdering) { |
| Animation* animation1 = timeline->Play(nullptr); |
| Animation* animation2 = timeline->Play(nullptr); |
| EXPECT_TRUE(Animation::HasLowerCompositeOrdering( |
| animation1, animation2, |
| Animation::CompareAnimationsOrdering::kPointerOrder)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, PlayAfterCancel) { |
| animation->cancel(); |
| EXPECT_EQ("idle", animation->playState()); |
| EXPECT_FALSE(CurrentTimeIsSet(animation)); |
| EXPECT_FALSE(StartTimeIsSet(animation)); |
| animation->play(); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| EXPECT_FALSE(StartTimeIsSet(animation)); |
| SimulateAwaitReady(); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| EXPECT_TIME(0, GetStartTimeMs(animation)); |
| |
| SimulateFrame(10000); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TIME(10000, GetCurrentTimeMs(animation)); |
| EXPECT_TIME(0, GetStartTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, PlayBackwardsAfterCancel) { |
| animation->setPlaybackRate(-1); |
| animation->setCurrentTime(CSSNumberish::FromDouble(15000)); |
| animation->cancel(); |
| EXPECT_EQ("idle", animation->playState()); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_FALSE(CurrentTimeIsSet(animation)); |
| EXPECT_FALSE(StartTimeIsSet(animation)); |
| |
| // Snap to the end of the animation. |
| animation->play(); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| EXPECT_FALSE(StartTimeIsSet(animation)); |
| SimulateAwaitReady(); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_TIME(30000, GetStartTimeMs(animation)); |
| |
| SimulateFrame(10000); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| EXPECT_TIME(30000, GetStartTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, ReverseAfterCancel) { |
| animation->cancel(); |
| EXPECT_EQ("idle", animation->playState()); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_FALSE(CurrentTimeIsSet(animation)); |
| EXPECT_FALSE(StartTimeIsSet(animation)); |
| |
| // Reverse snaps to the end of the animation. |
| animation->reverse(); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| EXPECT_FALSE(StartTimeIsSet(animation)); |
| SimulateAwaitReady(); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_TIME(30000, GetStartTimeMs(animation)); |
| |
| SimulateFrame(10000); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TIME(20000, GetCurrentTimeMs(animation)); |
| EXPECT_TIME(30000, GetStartTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, FinishAfterCancel) { |
| NonThrowableExceptionState exception_state; |
| animation->cancel(); |
| EXPECT_EQ("idle", animation->playState()); |
| EXPECT_FALSE(CurrentTimeIsSet(animation)); |
| EXPECT_FALSE(StartTimeIsSet(animation)); |
| |
| animation->finish(exception_state); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_TIME(30000, GetCurrentTimeMs(animation)); |
| EXPECT_TIME(-30000, GetStartTimeMs(animation)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, PauseAfterCancel) { |
| animation->cancel(); |
| EXPECT_EQ("idle", animation->playState()); |
| EXPECT_FALSE(CurrentTimeIsSet(animation)); |
| EXPECT_FALSE(StartTimeIsSet(animation)); |
| animation->pause(); |
| EXPECT_EQ("paused", animation->playState()); |
| EXPECT_TRUE(animation->pending()); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| EXPECT_FALSE(StartTimeIsSet(animation)); |
| SimulateAwaitReady(); |
| EXPECT_FALSE(animation->pending()); |
| EXPECT_TIME(0, GetCurrentTimeMs(animation)); |
| EXPECT_FALSE(StartTimeIsSet(animation)); |
| } |
| |
| // crbug.com/1052217 |
| TEST_F(AnimationAnimationTestNoCompositing, SetPlaybackRateAfterFinish) { |
| animation->setEffect(MakeAnimation(30, Timing::FillMode::FORWARDS)); |
| animation->finish(); |
| animation->Update(kTimingUpdateOnDemand); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_EQ(base::nullopt, animation->TimeToEffectChange()); |
| |
| // Reversing a finished animation marks the animation as outdated. Required |
| // to recompute the time to next interval. |
| animation->setPlaybackRate(-1); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_EQ(animation->playbackRate(), -1); |
| EXPECT_TRUE(animation->Outdated()); |
| animation->Update(kTimingUpdateOnDemand); |
| EXPECT_TIMEDELTA(AnimationTimeDelta(), |
| animation->TimeToEffectChange().value()); |
| EXPECT_FALSE(animation->Outdated()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, UpdatePlaybackRateAfterFinish) { |
| animation->setEffect(MakeAnimation(30, Timing::FillMode::FORWARDS)); |
| animation->finish(); |
| animation->Update(kTimingUpdateOnDemand); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_EQ(base::nullopt, animation->TimeToEffectChange()); |
| |
| // Reversing a finished animation marks the animation as outdated. Required |
| // to recompute the time to next interval. The pending playback rate is |
| // immediately applied when updatePlaybackRate is called on a non-running |
| // animation. |
| animation->updatePlaybackRate(-1); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_EQ(animation->playbackRate(), -1); |
| EXPECT_TRUE(animation->Outdated()); |
| animation->Update(kTimingUpdateOnDemand); |
| EXPECT_TIMEDELTA(AnimationTimeDelta(), |
| animation->TimeToEffectChange().value()); |
| EXPECT_FALSE(animation->Outdated()); |
| } |
| |
| TEST_F(AnimationAnimationTestCompositeAfterPaint, |
| NoCompositeWithoutCompositedElementId) { |
| SetBodyInnerHTML( |
| "<div id='foo' style='position: relative; will-change: " |
| "opacity;'>composited</div>" |
| "<div id='bar' style='position: relative'>not composited</div>"); |
| |
| LayoutObject* object_composited = GetLayoutObjectByElementId("foo"); |
| LayoutObject* object_not_composited = GetLayoutObjectByElementId("bar"); |
| |
| Timing timing; |
| timing.iteration_duration = AnimationTimeDelta::FromSecondsD(30); |
| auto* keyframe_effect_composited = MakeGarbageCollected<KeyframeEffect>( |
| To<Element>(object_composited->GetNode()), MakeSimpleEffectModel(), |
| timing); |
| Animation* animation_composited = timeline->Play(keyframe_effect_composited); |
| auto* keyframe_effect_not_composited = MakeGarbageCollected<KeyframeEffect>( |
| To<Element>(object_not_composited->GetNode()), MakeSimpleEffectModel(), |
| timing); |
| Animation* animation_not_composited = |
| timeline->Play(keyframe_effect_not_composited); |
| |
| SimulateFrame(0); |
| EXPECT_EQ(animation_composited->CheckCanStartAnimationOnCompositorInternal(), |
| CompositorAnimations::kNoFailure); |
| const PaintArtifactCompositor* paint_artifact_compositor = |
| GetDocument().View()->GetPaintArtifactCompositor(); |
| ASSERT_TRUE(paint_artifact_compositor); |
| EXPECT_EQ(animation_composited->CheckCanStartAnimationOnCompositor( |
| paint_artifact_compositor), |
| CompositorAnimations::kNoFailure); |
| EXPECT_NE(animation_not_composited->CheckCanStartAnimationOnCompositor( |
| paint_artifact_compositor), |
| CompositorAnimations::kNoFailure); |
| } |
| |
| // Regression test for http://crbug.com/819591 . If a compositable animation is |
| // played and then paused before any start time is set (either blink or |
| // compositor side), the pausing must still set compositor pending or the pause |
| // won't be synced. |
| TEST_F(AnimationAnimationTestCompositing, |
| SetCompositorPendingWithUnresolvedStartTimes) { |
| ResetWithCompositedAnimation(); |
| |
| // At this point, the animation exists on both the compositor and blink side, |
| // but no start time has arrived on either side. The compositor is currently |
| // synced, no update is pending. |
| EXPECT_FALSE(animation->CompositorPendingForTesting()); |
| |
| // However, if we pause the animation then the compositor should still be |
| // marked pending. This is required because otherwise the compositor will go |
| // ahead and start playing the animation once it receives a start time (e.g. |
| // on the next compositor frame). |
| animation->pause(); |
| |
| EXPECT_TRUE(animation->CompositorPendingForTesting()); |
| } |
| |
| TEST_F(AnimationAnimationTestCompositing, PreCommitWithUnresolvedStartTimes) { |
| ResetWithCompositedAnimation(); |
| |
| // At this point, the animation exists on both the compositor and blink side, |
| // but no start time has arrived on either side. The compositor is currently |
| // synced, no update is pending. |
| EXPECT_FALSE(animation->CompositorPendingForTesting()); |
| |
| // At this point, a call to PreCommit should bail out and tell us to wait for |
| // next commit because there are no resolved start times. |
| EXPECT_FALSE(animation->PreCommit(0, nullptr, true)); |
| } |
| |
| namespace { |
| int GenerateHistogramValue(CompositorAnimations::FailureReason reason) { |
| // The enum values in CompositorAnimations::FailureReasons are stored as 2^i |
| // as they are a bitmask, but are recorded into the histogram as (i+1) to give |
| // sequential histogram values. The exception is kNoFailure, which is stored |
| // as 0 and recorded as 0. |
| if (reason == CompositorAnimations::kNoFailure) |
| return CompositorAnimations::kNoFailure; |
| return base::bits::CountTrailingZeroBits(static_cast<uint32_t>(reason)) + 1; |
| } |
| } // namespace |
| |
| TEST_F(AnimationAnimationTestCompositing, PreCommitRecordsHistograms) { |
| const std::string histogram_name = |
| "Blink.Animation.CompositedAnimationFailureReason"; |
| |
| // Initially the animation in this test has no target, so it is invalid. |
| { |
| HistogramTester histogram; |
| ASSERT_TRUE(animation->PreCommit(0, nullptr, true)); |
| histogram.ExpectBucketCount( |
| histogram_name, |
| GenerateHistogramValue(CompositorAnimations::kInvalidAnimationOrEffect), |
| 1); |
| } |
| |
| // Restart the animation with a target and compositing state. |
| { |
| HistogramTester histogram; |
| ResetWithCompositedAnimation(); |
| histogram.ExpectBucketCount( |
| histogram_name, |
| GenerateHistogramValue(CompositorAnimations::kNoFailure), 1); |
| } |
| |
| // Now make the playback rate 0. This trips both the invalid animation and |
| // unsupported timing parameter reasons. |
| animation->setPlaybackRate(0); |
| animation->NotifyReady(AnimationTimeDelta::FromSecondsD(100)); |
| { |
| HistogramTester histogram; |
| ASSERT_TRUE(animation->PreCommit(0, nullptr, true)); |
| histogram.ExpectBucketCount( |
| histogram_name, |
| GenerateHistogramValue(CompositorAnimations::kInvalidAnimationOrEffect), |
| 1); |
| histogram.ExpectBucketCount( |
| histogram_name, |
| GenerateHistogramValue( |
| CompositorAnimations::kEffectHasUnsupportedTimingParameters), |
| 1); |
| } |
| animation->setPlaybackRate(1); |
| |
| // Finally, change the keyframes to something unsupported by the compositor. |
| Persistent<StringKeyframe> start_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| start_keyframe->SetCSSPropertyValue( |
| CSSPropertyID::kLeft, "0", SecureContextMode::kInsecureContext, nullptr); |
| Persistent<StringKeyframe> end_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| end_keyframe->SetCSSPropertyValue(CSSPropertyID::kLeft, "100px", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| |
| To<KeyframeEffect>(animation->effect()) |
| ->SetKeyframes({start_keyframe, end_keyframe}); |
| UpdateAllLifecyclePhasesForTest(); |
| { |
| HistogramTester histogram; |
| ASSERT_TRUE(animation->PreCommit(0, nullptr, true)); |
| histogram.ExpectBucketCount( |
| histogram_name, |
| GenerateHistogramValue(CompositorAnimations::kUnsupportedCSSProperty), |
| 1); |
| } |
| } |
| |
| // crbug.com/990000. |
| TEST_F(AnimationAnimationTestCompositing, ReplaceCompositedAnimation) { |
| const std::string histogram_name = |
| "Blink.Animation.CompositedAnimationFailureReason"; |
| |
| // Start with a composited animation. |
| ResetWithCompositedAnimation(); |
| ASSERT_TRUE(animation->HasActiveAnimationsOnCompositor()); |
| |
| // Replace the animation. The new animation should not be incompatible and |
| // therefore able to run on the compositor. |
| animation->cancel(); |
| MakeCompositedAnimation(); |
| ASSERT_TRUE(animation->HasActiveAnimationsOnCompositor()); |
| } |
| |
| TEST_F(AnimationAnimationTestCompositing, SetKeyframesCausesCompositorPending) { |
| ResetWithCompositedAnimation(); |
| |
| // At this point, the animation exists on both the compositor and blink side, |
| // but no start time has arrived on either side. The compositor is currently |
| // synced, no update is pending. |
| EXPECT_FALSE(animation->CompositorPendingForTesting()); |
| |
| // Now change the keyframes; this should mark the animation as compositor |
| // pending as we need to sync the compositor side. |
| Persistent<StringKeyframe> start_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| start_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "0.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| Persistent<StringKeyframe> end_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| end_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "1.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| |
| StringKeyframeVector keyframes; |
| keyframes.push_back(start_keyframe); |
| keyframes.push_back(end_keyframe); |
| |
| To<KeyframeEffect>(animation->effect())->SetKeyframes(keyframes); |
| |
| EXPECT_TRUE(animation->CompositorPendingForTesting()); |
| } |
| |
| // crbug.com/1057076 |
| // Infinite duration animations should not run on the compositor. |
| TEST_F(AnimationAnimationTestCompositing, InfiniteDurationAnimation) { |
| ResetWithCompositedAnimation(); |
| EXPECT_EQ(CompositorAnimations::kNoFailure, |
| animation->CheckCanStartAnimationOnCompositor(nullptr)); |
| |
| OptionalEffectTiming* effect_timing = OptionalEffectTiming::Create(); |
| effect_timing->setDuration(UnrestrictedDoubleOrString::FromUnrestrictedDouble( |
| std::numeric_limits<double>::infinity())); |
| animation->effect()->updateTiming(effect_timing); |
| EXPECT_EQ(CompositorAnimations::kEffectHasUnsupportedTimingParameters, |
| animation->CheckCanStartAnimationOnCompositor(nullptr)); |
| } |
| |
| // This test ensures that a background-color animation can start on compositor. |
| TEST_F(AnimationAnimationTestCompositing, BackgroundColorComposited) { |
| ScopedCompositeBGColorAnimationForTest composite_bgcolor_animation(true); |
| SetBodyInnerHTML(R"HTML( |
| <div id ="target" style="width: 100px; height: 100px"> |
| </div> |
| )HTML"); |
| |
| Animation* animation = |
| CreateAnimation(CSSPropertyID::kBackgroundColor, "red", "green"); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| animation->play(); |
| // A basic condition for an animation to be compositable is that it is set so |
| // by BackgroundColorPaintWorklet::GetBGColorPaintWorkletParams. |
| animation->SetCanCompositeBGColorAnim(); |
| EXPECT_EQ(animation->CheckCanStartAnimationOnCompositor(nullptr), |
| CompositorAnimations::kNoFailure); |
| } |
| |
| // crbug.com/1149012 |
| // Regression test to ensure proper restart logic for composited animations on |
| // relative transforms after a size change. In this test, the transform depends |
| // on the width and height of the box and a change to either triggers a restart |
| // of the animation if running. |
| TEST_F(AnimationAnimationTestCompositing, |
| RestartCompositedAnimationOnSizeChange) { |
| // TODO(crbug.com/389359): Remove forced feature enabling once on by |
| // default. |
| ScopedCompositeRelativeKeyframesForTest composite_relative_keyframes(true); |
| SetBodyInnerHTML(R"HTML( |
| <div id ="target" |
| style="width: 100px; height: 200px; will-change: transform"> |
| </div> |
| )HTML"); |
| |
| Animation* animation = CreateAnimation( |
| CSSPropertyID::kTransform, "translate(100%, 100%)", "translate(0%, 0%)"); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| animation->play(); |
| KeyframeEffect* keyframe_effect = |
| DynamicTo<KeyframeEffect>(animation->effect()); |
| ASSERT_TRUE(keyframe_effect); |
| |
| EXPECT_EQ(animation->CheckCanStartAnimationOnCompositor(nullptr), |
| CompositorAnimations::kNoFailure); |
| |
| GetDocument().GetPendingAnimations().Update(nullptr, true); |
| EXPECT_TRUE(animation->HasActiveAnimationsOnCompositor()); |
| |
| // Kick the animation out of the play-pending state. |
| animation->setStartTime(CSSNumberish::FromDouble(0)); |
| |
| // No size change and animation does not require a restart. |
| keyframe_effect->UpdateBoxSizeAndCheckTransformAxisAlignment( |
| FloatSize(100, 200)); |
| EXPECT_TRUE(animation->HasActiveAnimationsOnCompositor()); |
| |
| // Restart animation on a width change. |
| keyframe_effect->UpdateBoxSizeAndCheckTransformAxisAlignment( |
| FloatSize(200, 200)); |
| EXPECT_FALSE(animation->HasActiveAnimationsOnCompositor()); |
| |
| GetDocument().GetPendingAnimations().Update(nullptr, true); |
| EXPECT_TRUE(animation->HasActiveAnimationsOnCompositor()); |
| |
| // Restart animation on a height change. |
| keyframe_effect->UpdateBoxSizeAndCheckTransformAxisAlignment( |
| FloatSize(200, 300)); |
| EXPECT_FALSE(animation->HasActiveAnimationsOnCompositor()); |
| } |
| |
| // crbug.com/1149012 |
| // Regression test to ensure proper restart logic for composited animations on |
| // relative transforms after a size change. In this test, the transform only |
| // depends on width and a change to the height does not trigger a restart. |
| TEST_F(AnimationAnimationTestCompositing, |
| RestartCompositedAnimationOnWidthChange) { |
| // TODO(crbug.com/389359): Remove forced feature enabling once on by |
| // default. |
| ScopedCompositeRelativeKeyframesForTest composite_relative_keyframes(true); |
| SetBodyInnerHTML(R"HTML( |
| <div id ="target" |
| style="width: 100px; height: 200px; will-change: transform"> |
| </div> |
| )HTML"); |
| |
| animation = CreateAnimation(CSSPropertyID::kTransform, "translateX(100%)", |
| "translateX(0%)"); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| animation->play(); |
| KeyframeEffect* keyframe_effect = |
| DynamicTo<KeyframeEffect>(animation->effect()); |
| ASSERT_TRUE(keyframe_effect); |
| |
| GetDocument().GetPendingAnimations().Update(nullptr, true); |
| EXPECT_TRUE(animation->HasActiveAnimationsOnCompositor()); |
| keyframe_effect->UpdateBoxSizeAndCheckTransformAxisAlignment( |
| FloatSize(100, 200)); |
| animation->setStartTime(CSSNumberish::FromDouble(0)); |
| |
| // Transform is not height dependent and a change to the height does not force |
| // an animation restart. |
| keyframe_effect->UpdateBoxSizeAndCheckTransformAxisAlignment( |
| FloatSize(100, 300)); |
| EXPECT_TRUE(animation->HasActiveAnimationsOnCompositor()); |
| |
| // Width change forces a restart. |
| keyframe_effect->UpdateBoxSizeAndCheckTransformAxisAlignment( |
| FloatSize(200, 300)); |
| EXPECT_FALSE(animation->HasActiveAnimationsOnCompositor()); |
| } |
| |
| // crbug.com/1149012 |
| // Regression test to ensure proper restart logic for composited animations on |
| // relative transforms after a size change. In this test, the transition only |
| // affects height and a change to the width does not trigger a restart. |
| TEST_F(AnimationAnimationTestCompositing, |
| RestartCompositedAnimationOnHeightChange) { |
| // TODO(crbug.com/389359): Remove forced feature enabling once on by |
| // default. |
| ScopedCompositeRelativeKeyframesForTest composite_relative_keyframes(true); |
| SetBodyInnerHTML(R"HTML( |
| <div id ="target" |
| style="width: 100px; height: 200px; will-change: transform"> |
| </div> |
| )HTML"); |
| |
| animation = CreateAnimation(CSSPropertyID::kTransform, "translateY(100%)", |
| "translateY(0%)"); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| animation->play(); |
| KeyframeEffect* keyframe_effect = |
| DynamicTo<KeyframeEffect>(animation->effect()); |
| ASSERT_TRUE(keyframe_effect); |
| |
| GetDocument().GetPendingAnimations().Update(nullptr, true); |
| EXPECT_TRUE(animation->HasActiveAnimationsOnCompositor()); |
| keyframe_effect->UpdateBoxSizeAndCheckTransformAxisAlignment( |
| FloatSize(100, 200)); |
| animation->setStartTime(CSSNumberish::FromDouble(0)); |
| |
| // Transform is not width dependent and a change to the width does not force |
| // an animation restart. |
| keyframe_effect->UpdateBoxSizeAndCheckTransformAxisAlignment( |
| FloatSize(300, 200)); |
| EXPECT_TRUE(animation->HasActiveAnimationsOnCompositor()); |
| |
| // Height change forces a restart. |
| keyframe_effect->UpdateBoxSizeAndCheckTransformAxisAlignment( |
| FloatSize(300, 400)); |
| EXPECT_FALSE(animation->HasActiveAnimationsOnCompositor()); |
| } |
| |
| TEST_F(AnimationAnimationTestCompositing, |
| ScrollLinkedAnimationCanBeComposited) { |
| ResetWithCompositedAnimation(); |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { will-change: transform; overflow: scroll; width: 100px; height: 100px; } |
| #target { width: 100px; height: 200px; will-change: opacity;} |
| #spacer { width: 200px; height: 2000px; } |
| </style> |
| <div id ='scroller'> |
| <div id ='target'></div> |
| <div id ='spacer'></div> |
| </div> |
| )HTML"); |
| |
| // Create ScrollTimeline |
| auto* scroller = |
| To<LayoutBoxModelObject>(GetLayoutObjectByElementId("scroller")); |
| PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea(); |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 20), |
| mojom::blink::ScrollType::kProgrammatic); |
| ScrollTimelineOptions* options = ScrollTimelineOptions::Create(); |
| DoubleOrScrollTimelineAutoKeyword time_range = |
| DoubleOrScrollTimelineAutoKeyword::FromDouble(100); |
| options->setTimeRange(time_range); |
| options->setScrollSource(GetElementById("scroller")); |
| ScrollTimeline* scroll_timeline = |
| ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION); |
| |
| // Create KeyframeEffect |
| Timing timing; |
| timing.iteration_duration = AnimationTimeDelta::FromSecondsD(30); |
| |
| Persistent<StringKeyframe> start_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| start_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "1.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| Persistent<StringKeyframe> end_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| end_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "0.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| |
| StringKeyframeVector keyframes; |
| keyframes.push_back(start_keyframe); |
| keyframes.push_back(end_keyframe); |
| |
| Element* element = GetElementById("target"); |
| auto* model = MakeGarbageCollected<StringKeyframeEffectModel>(keyframes); |
| |
| // Create scroll-linked animation |
| NonThrowableExceptionState exception_state; |
| Animation* scroll_animation = Animation::Create( |
| MakeGarbageCollected<KeyframeEffect>(element, model, timing), |
| scroll_timeline, exception_state); |
| |
| model->SnapshotAllCompositorKeyframesIfNecessary( |
| *element, *ComputedStyle::Create(), nullptr); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| scroll_animation->play(); |
| EXPECT_EQ(scroll_animation->CheckCanStartAnimationOnCompositor(nullptr), |
| CompositorAnimations::kNoFailure); |
| } |
| |
| TEST_F(AnimationAnimationTestCompositing, |
| StartScrollLinkedAnimationWithStartTimeIfApplicable) { |
| ResetWithCompositedAnimation(); |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { will-change: transform; overflow: scroll; width: 100px; height: 100px; } |
| #target { width: 100px; height: 200px; will-change: opacity;} |
| #spacer { width: 200px; height: 700px; } |
| </style> |
| <div id ='scroller'> |
| <div id ='target'></div> |
| <div id ='spacer'></div> |
| </div> |
| )HTML"); |
| |
| // Create ScrollTimeline |
| auto* scroller = |
| To<LayoutBoxModelObject>(GetLayoutObjectByElementId("scroller")); |
| PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea(); |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 100), |
| mojom::blink::ScrollType::kProgrammatic); |
| ScrollTimelineOptions* options = ScrollTimelineOptions::Create(); |
| DoubleOrScrollTimelineAutoKeyword time_range = |
| DoubleOrScrollTimelineAutoKeyword::FromDouble(100); |
| options->setTimeRange(time_range); |
| options->setScrollSource(GetElementById("scroller")); |
| ScrollTimeline* scroll_timeline = |
| ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION); |
| |
| // Create KeyframeEffect |
| Timing timing; |
| timing.iteration_duration = AnimationTimeDelta::FromSecondsD(30); |
| |
| Persistent<StringKeyframe> start_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| start_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "1.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| Persistent<StringKeyframe> end_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| end_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "0.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| |
| StringKeyframeVector keyframes; |
| keyframes.push_back(start_keyframe); |
| keyframes.push_back(end_keyframe); |
| |
| Element* element = GetElementById("target"); |
| auto* model = MakeGarbageCollected<StringKeyframeEffectModel>(keyframes); |
| |
| KeyframeEffect* keyframe_effect = |
| MakeGarbageCollected<KeyframeEffect>(element, model, timing); |
| |
| // Create scroll-linked animation |
| NonThrowableExceptionState exception_state; |
| Animation* scroll_animation = |
| Animation::Create(keyframe_effect, scroll_timeline, exception_state); |
| |
| model->SnapshotAllCompositorKeyframesIfNecessary( |
| *element, *ComputedStyle::Create(), nullptr); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| const double TEST_START_TIME = 10; |
| scroll_animation->setStartTime(CSSNumberish::FromDouble(TEST_START_TIME)); |
| scroll_animation->play(); |
| EXPECT_EQ(scroll_animation->CheckCanStartAnimationOnCompositor(nullptr), |
| CompositorAnimations::kNoFailure); |
| // Start the animation on compositor. The time offset of the compositor |
| // keyframe should be unset if we start the animation with its start time. |
| scroll_animation->PreCommit(1, nullptr, true); |
| cc::KeyframeModel* keyframe_model = |
| keyframe_effect->GetAnimationForTesting() |
| ->GetCompositorAnimation() |
| ->CcAnimation() |
| ->GetKeyframeModel(compositor_target_property::OPACITY); |
| EXPECT_EQ(keyframe_model->start_time() - base::TimeTicks(), |
| base::TimeDelta::FromMilliseconds(TEST_START_TIME)); |
| EXPECT_EQ(keyframe_model->time_offset(), base::TimeDelta()); |
| } |
| |
| // Verifies correctness of scroll linked animation current and start times in |
| // various animation states. |
| TEST_F(AnimationAnimationTestNoCompositing, ScrollLinkedAnimationCreation) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { overflow: scroll; width: 100px; height: 100px; } |
| #spacer { width: 200px; height: 200px; } |
| </style> |
| <div id='scroller'> |
| <div id ='spacer'></div> |
| </div> |
| )HTML"); |
| |
| auto* scroller = |
| To<LayoutBoxModelObject>(GetLayoutObjectByElementId("scroller")); |
| PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea(); |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 20), |
| mojom::blink::ScrollType::kProgrammatic); |
| ScrollTimelineOptions* options = ScrollTimelineOptions::Create(); |
| DoubleOrScrollTimelineAutoKeyword time_range = |
| DoubleOrScrollTimelineAutoKeyword::FromDouble(100); |
| options->setTimeRange(time_range); |
| options->setScrollSource(GetElementById("scroller")); |
| ScrollTimeline* scroll_timeline = |
| ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION); |
| |
| NonThrowableExceptionState exception_state; |
| Animation* scroll_animation = |
| Animation::Create(MakeAnimation(), scroll_timeline, exception_state); |
| |
| // Verify start and current times in Idle state. |
| EXPECT_FALSE(StartTimeIsSet(scroll_animation)); |
| EXPECT_FALSE(CurrentTimeIsSet(scroll_animation)); |
| |
| scroll_animation->play(); |
| |
| // Verify start and current times in Pending state. |
| EXPECT_TIME(0, GetStartTimeMs(scroll_animation)); |
| EXPECT_TIME(20, GetCurrentTimeMs(scroll_animation)); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| // Verify start and current times in Playing state. |
| EXPECT_TIME(0, GetStartTimeMs(scroll_animation)); |
| EXPECT_TIME(20, GetCurrentTimeMs(scroll_animation)); |
| |
| // Verify current time after scroll. |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 40), |
| mojom::blink::ScrollType::kProgrammatic); |
| SimulateFrameForScrollAnimations(); |
| EXPECT_TIME(40, GetCurrentTimeMs(scroll_animation)); |
| } |
| |
| // Verifies that finished composited scroll-linked animations restart on |
| // compositor upon reverse scrolling. |
| TEST_F(AnimationAnimationTestCompositing, |
| FinishedScrollLinkedAnimationRestartsOnReverseScrolling) { |
| ResetWithCompositedAnimation(); |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { will-change: transform; overflow: scroll; width: 100px; height: 100px; } |
| #target { width: 100px; height: 200px; will-change: opacity;} |
| #spacer { width: 200px; height: 700px; } |
| </style> |
| <div id ='scroller'> |
| <div id ='target'></div> |
| <div id ='spacer'></div> |
| </div> |
| )HTML"); |
| |
| auto* scroller = |
| To<LayoutBoxModelObject>(GetLayoutObjectByElementId("scroller")); |
| ASSERT_TRUE(scroller->UsesCompositedScrolling()); |
| |
| // Create ScrollTimeline |
| ScrollTimelineOptions* options = ScrollTimelineOptions::Create(); |
| DoubleOrScrollTimelineAutoKeyword time_range = |
| DoubleOrScrollTimelineAutoKeyword::FromDouble(100); |
| options->setTimeRange(time_range); |
| options->setScrollSource(GetElementById("scroller")); |
| ScrollTimeline* scroll_timeline = |
| ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION); |
| |
| // Create KeyframeEffect |
| Timing timing; |
| timing.iteration_duration = AnimationTimeDelta::FromSecondsD(30); |
| Persistent<StringKeyframe> start_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| start_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "1.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| Persistent<StringKeyframe> end_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| end_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "0.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| |
| StringKeyframeVector keyframes; |
| keyframes.push_back(start_keyframe); |
| keyframes.push_back(end_keyframe); |
| |
| Element* element = GetElementById("target"); |
| auto* model = MakeGarbageCollected<StringKeyframeEffectModel>(keyframes); |
| |
| KeyframeEffect* keyframe_effect = |
| MakeGarbageCollected<KeyframeEffect>(element, model, timing); |
| |
| // Create scroll-linked animation |
| NonThrowableExceptionState exception_state; |
| Animation* scroll_animation = |
| Animation::Create(keyframe_effect, scroll_timeline, exception_state); |
| model->SnapshotAllCompositorKeyframesIfNecessary( |
| *element, *ComputedStyle::Create(), nullptr); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| scroll_animation->play(); |
| EXPECT_EQ(scroll_animation->playState(), "running"); |
| GetDocument().GetPendingAnimations().Update(nullptr, true); |
| EXPECT_TRUE(scroll_animation->HasActiveAnimationsOnCompositor()); |
| |
| // Advances the animation to "finished" state. The composited animation will |
| // be destroyed accordingly. |
| scroll_animation->setCurrentTime(CSSNumberish::FromDouble(50000)); |
| EXPECT_EQ(scroll_animation->playState(), "finished"); |
| scroll_animation->Update(kTimingUpdateForAnimationFrame); |
| GetDocument().GetPendingAnimations().Update(nullptr, true); |
| EXPECT_FALSE(scroll_animation->HasActiveAnimationsOnCompositor()); |
| |
| // Restarting the animation should create a new compositor animation. |
| scroll_animation->setCurrentTime(CSSNumberish::FromDouble(100)); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_EQ(scroll_animation->playState(), "running"); |
| scroll_animation->Update(kTimingUpdateForAnimationFrame); |
| GetDocument().GetPendingAnimations().Update(nullptr, true); |
| EXPECT_TRUE(scroll_animation->HasActiveAnimationsOnCompositor()); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, |
| RemoveCanceledAnimationFromActiveSet) { |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->Update(kTimingUpdateForAnimationFrame)); |
| SimulateFrame(1000); |
| EXPECT_TRUE(animation->Update(kTimingUpdateForAnimationFrame)); |
| animation->cancel(); |
| EXPECT_EQ("idle", animation->playState()); |
| EXPECT_FALSE(animation->Update(kTimingUpdateForAnimationFrame)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, |
| RemoveFinishedAnimationFromActiveSet) { |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->Update(kTimingUpdateForAnimationFrame)); |
| SimulateFrame(1000); |
| EXPECT_TRUE(animation->Update(kTimingUpdateForAnimationFrame)); |
| |
| // Synchronous completion. |
| animation->finish(); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_FALSE(animation->Update(kTimingUpdateForAnimationFrame)); |
| |
| // Play creates a new pending finished promise. |
| animation->play(); |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_TRUE(animation->Update(kTimingUpdateForAnimationFrame)); |
| |
| // Asynchronous completion. |
| animation->setCurrentTime(CSSNumberish::FromDouble(50000)); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_FALSE(animation->Update(kTimingUpdateForAnimationFrame)); |
| } |
| |
| TEST_F(AnimationAnimationTestNoCompositing, |
| PendingActivityWithFinishedPromise) { |
| // No pending activity even when running if there is no finished promise |
| // or event listener. |
| EXPECT_EQ("running", animation->playState()); |
| SimulateFrame(1000); |
| EXPECT_FALSE(animation->HasPendingActivity()); |
| |
| // An unresolved finished promise indicates pending activity. |
| ScriptState* script_state = |
| ToScriptStateForMainWorld(GetDocument().GetFrame()); |
| animation->finished(script_state); |
| EXPECT_TRUE(animation->HasPendingActivity()); |
| |
| // Resolving the finished promise clears the pending activity. |
| animation->setCurrentTime(CSSNumberish::FromDouble(50000)); |
| EXPECT_EQ("finished", animation->playState()); |
| SimulateMicrotask(); |
| EXPECT_FALSE(animation->Update(kTimingUpdateForAnimationFrame)); |
| EXPECT_FALSE(animation->HasPendingActivity()); |
| |
| // Playing an already finished animation creates a new pending finished |
| // promise. |
| animation->play(); |
| EXPECT_EQ("running", animation->playState()); |
| SimulateFrame(2000); |
| EXPECT_TRUE(animation->HasPendingActivity()); |
| // Cancel rejects the finished promise and creates a new pending finished |
| // promise. |
| // TODO(crbug.com/960944): Investigate if this should return false to prevent |
| // holding onto the animation indefinitely. |
| animation->cancel(); |
| EXPECT_TRUE(animation->HasPendingActivity()); |
| } |
| |
| class MockEventListener final : public NativeEventListener { |
| public: |
| MOCK_METHOD2(Invoke, void(ExecutionContext*, Event*)); |
| }; |
| |
| TEST_F(AnimationAnimationTestNoCompositing, |
| PendingActivityWithFinishedEventListener) { |
| EXPECT_EQ("running", animation->playState()); |
| EXPECT_FALSE(animation->HasPendingActivity()); |
| |
| // Attaching a listener for the finished event indicates pending activity. |
| Persistent<MockEventListener> event_listener = |
| MakeGarbageCollected<MockEventListener>(); |
| animation->addEventListener(event_type_names::kFinish, event_listener); |
| EXPECT_TRUE(animation->HasPendingActivity()); |
| |
| // Synchronous finish clears pending activity. |
| animation->finish(); |
| EXPECT_EQ("finished", animation->playState()); |
| EXPECT_FALSE(animation->Update(kTimingUpdateForAnimationFrame)); |
| EXPECT_TRUE(animation->HasPendingActivity()); |
| animation->pending_finished_event_ = nullptr; |
| EXPECT_FALSE(animation->HasPendingActivity()); |
| |
| // Playing an already finished animation resets the finished state. |
| animation->play(); |
| EXPECT_EQ("running", animation->playState()); |
| SimulateFrame(2000); |
| EXPECT_TRUE(animation->HasPendingActivity()); |
| |
| // Finishing the animation asynchronously clears the pending activity. |
| animation->setCurrentTime(CSSNumberish::FromDouble(50000)); |
| EXPECT_EQ("finished", animation->playState()); |
| SimulateMicrotask(); |
| EXPECT_FALSE(animation->Update(kTimingUpdateForAnimationFrame)); |
| EXPECT_TRUE(animation->HasPendingActivity()); |
| animation->pending_finished_event_ = nullptr; |
| EXPECT_FALSE(animation->HasPendingActivity()); |
| |
| // Canceling an animation clears the pending activity. |
| animation->play(); |
| EXPECT_EQ("running", animation->playState()); |
| SimulateFrame(2000); |
| animation->cancel(); |
| EXPECT_EQ("idle", animation->playState()); |
| EXPECT_FALSE(animation->Update(kTimingUpdateForAnimationFrame)); |
| EXPECT_FALSE(animation->HasPendingActivity()); |
| } |
| |
| class AnimationPendingAnimationsTest : public RenderingTest { |
| public: |
| AnimationPendingAnimationsTest() |
| : RenderingTest(MakeGarbageCollected<SingleChildLocalFrameClient>()) {} |
| |
| enum CompositingMode { kComposited, kNonComposited }; |
| |
| void SetUp() override { |
| EnableCompositing(); |
| RenderingTest::SetUp(); |
| GetDocument().GetAnimationClock().ResetTimeForTesting(); |
| timeline = GetDocument().Timeline(); |
| timeline->ResetForTesting(); |
| } |
| |
| Animation* MakeAnimation(const char* target, CompositingMode mode) { |
| Timing timing; |
| timing.iteration_duration = AnimationTimeDelta::FromSecondsD(30); |
| Persistent<StringKeyframe> start_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| start_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "1.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| Persistent<StringKeyframe> end_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| end_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "0.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| |
| StringKeyframeVector keyframes; |
| keyframes.push_back(start_keyframe); |
| keyframes.push_back(end_keyframe); |
| |
| Element* element = GetElementById(target); |
| auto* model = MakeGarbageCollected<StringKeyframeEffectModel>(keyframes); |
| |
| Animation* animation = timeline->Play( |
| MakeGarbageCollected<KeyframeEffect>(element, model, timing)); |
| |
| if (mode == kNonComposited) { |
| // Having a playback rate of zero is one of several ways to force an |
| // animation to be non-composited. |
| animation->updatePlaybackRate(0); |
| } |
| |
| return animation; |
| } |
| |
| bool Update() { |
| UpdateAllLifecyclePhasesForTest(); |
| GetDocument().GetAnimationClock().UpdateTime(base::TimeTicks()); |
| return GetDocument().GetPendingAnimations().Update(nullptr, true); |
| } |
| |
| void NotifyAnimationStarted(Animation* animation) { |
| animation->GetDocument() |
| ->GetPendingAnimations() |
| .NotifyCompositorAnimationStarted(0, animation->CompositorGroup()); |
| } |
| |
| void restartAnimation(Animation* animation) { |
| animation->cancel(); |
| animation->play(); |
| } |
| |
| Persistent<DocumentTimeline> timeline; |
| }; |
| |
| TEST_F(AnimationPendingAnimationsTest, PendingAnimationStartSynchronization) { |
| RunDocumentLifecycle(); |
| SetBodyInnerHTML("<div id='foo'></div><div id='bar'></div>"); |
| |
| Persistent<Animation> animA = MakeAnimation("foo", kComposited); |
| Persistent<Animation> animB = MakeAnimation("bar", kNonComposited); |
| |
| // B's start time synchronized with A's start time. |
| EXPECT_TRUE(Update()); |
| EXPECT_TRUE(animA->pending()); |
| EXPECT_TRUE(animB->pending()); |
| EXPECT_TRUE(animA->HasActiveAnimationsOnCompositor()); |
| EXPECT_FALSE(animB->HasActiveAnimationsOnCompositor()); |
| NotifyAnimationStarted(animA); |
| EXPECT_FALSE(animA->pending()); |
| EXPECT_FALSE(animB->pending()); |
| } |
| |
| TEST_F(AnimationPendingAnimationsTest, |
| PendingAnimationCancelUnblocksSynchronizedStart) { |
| RunDocumentLifecycle(); |
| SetBodyInnerHTML("<div id='foo'></div><div id='bar'></div>"); |
| |
| Persistent<Animation> animA = MakeAnimation("foo", kComposited); |
| Persistent<Animation> animB = MakeAnimation("bar", kNonComposited); |
| |
| EXPECT_TRUE(Update()); |
| EXPECT_TRUE(animA->pending()); |
| EXPECT_TRUE(animB->pending()); |
| animA->cancel(); |
| |
| // Animation A no longer blocks B from starting. |
| EXPECT_FALSE(Update()); |
| EXPECT_FALSE(animB->pending()); |
| } |
| |
| TEST_F(AnimationPendingAnimationsTest, |
| PendingAnimationOnlySynchronizeStartsOfNewlyPendingAnimations) { |
| RunDocumentLifecycle(); |
| SetBodyInnerHTML( |
| "<div id='foo'></div><div id='bar'></div><div id='baz'></div>"); |
| |
| Persistent<Animation> animA = MakeAnimation("foo", kComposited); |
| Persistent<Animation> animB = MakeAnimation("bar", kNonComposited); |
| |
| // This test simulates the conditions in crbug.com/666710. The start of a |
| // non-composited animation is deferred in order to synchronize with a |
| // composited animation, which is canceled before it starts. Subsequent frames |
| // produce new composited animations which prevented the non-composited |
| // animation from ever starting. Non-composited animations should not be |
| // synchronize with new composited animations if queued up in a prior call to |
| // PendingAnimations::Update. |
| EXPECT_TRUE(Update()); |
| EXPECT_TRUE(animA->pending()); |
| EXPECT_TRUE(animB->pending()); |
| animA->cancel(); |
| |
| Persistent<Animation> animC = MakeAnimation("baz", kComposited); |
| Persistent<Animation> animD = MakeAnimation("bar", kNonComposited); |
| |
| EXPECT_TRUE(Update()); |
| // B's is unblocked despite newly created composited animation. |
| EXPECT_FALSE(animB->pending()); |
| EXPECT_TRUE(animC->pending()); |
| // D's start time is synchronized with C's start. |
| EXPECT_TRUE(animD->pending()); |
| NotifyAnimationStarted(animC); |
| EXPECT_FALSE(animC->pending()); |
| EXPECT_FALSE(animD->pending()); |
| } |
| |
| TEST_F(AnimationAnimationTestCompositing, |
| ScrollLinkedAnimationNotCompositedIfScrollSourceIsNotComposited) { |
| GetDocument().GetSettings()->SetPreferCompositingToLCDTextEnabled(false); |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { overflow: scroll; width: 100px; height: 100px; } |
| /* to prevent the mock overlay scrollbar from affecting compositing. */ |
| #scroller::-webkit-scrollbar { display: none; } |
| #target { width: 100px; height: 200px; will-change: transform; } |
| #spacer { width: 200px; height: 2000px; } |
| </style> |
| <div id ='scroller'> |
| <div id ='target'></div> |
| <div id ='spacer'></div> |
| </div> |
| )HTML"); |
| |
| // Create ScrollTimeline |
| auto* scroller = |
| To<LayoutBoxModelObject>(GetLayoutObjectByElementId("scroller")); |
| PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea(); |
| ASSERT_FALSE(scroller->UsesCompositedScrolling()); |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 20), |
| mojom::blink::ScrollType::kProgrammatic); |
| ScrollTimelineOptions* options = ScrollTimelineOptions::Create(); |
| DoubleOrScrollTimelineAutoKeyword time_range = |
| DoubleOrScrollTimelineAutoKeyword::FromDouble(100); |
| options->setTimeRange(time_range); |
| options->setScrollSource(GetElementById("scroller")); |
| ScrollTimeline* scroll_timeline = |
| ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION); |
| |
| // Create KeyframeEffect |
| Timing timing; |
| timing.iteration_duration = AnimationTimeDelta::FromSecondsD(30); |
| |
| Persistent<StringKeyframe> start_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| start_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "1.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| Persistent<StringKeyframe> end_keyframe = |
| MakeGarbageCollected<StringKeyframe>(); |
| end_keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "0.0", |
| SecureContextMode::kInsecureContext, |
| nullptr); |
| |
| StringKeyframeVector keyframes; |
| keyframes.push_back(start_keyframe); |
| keyframes.push_back(end_keyframe); |
| |
| Element* element = GetElementById("target"); |
| auto* model = MakeGarbageCollected<StringKeyframeEffectModel>(keyframes); |
| |
| // Create scroll-linked animation |
| NonThrowableExceptionState exception_state; |
| Animation* scroll_animation = Animation::Create( |
| MakeGarbageCollected<KeyframeEffect>(element, model, timing), |
| scroll_timeline, exception_state); |
| |
| model->SnapshotAllCompositorKeyframesIfNecessary( |
| *element, *ComputedStyle::Create(), nullptr); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| scroll_animation->play(); |
| EXPECT_EQ(scroll_animation->CheckCanStartAnimationOnCompositor(nullptr), |
| CompositorAnimations::kTimelineSourceHasInvalidCompositingState); |
| } |
| |
| TEST_F(AnimationAnimationTestCompositing, ContentVisibleDisplayLockTest) { |
| animation->cancel(); |
| RunDocumentLifecycle(); |
| |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| .container { |
| content-visibility: auto; |
| } |
| @keyframes anim { |
| from { opacity: 0; } |
| to { opacity: 1; } |
| } |
| #target { |
| background-color: blue; |
| width: 50px; |
| height: 50px; |
| animation: anim 1s linear alternate infinite; |
| } |
| </style> |
| <div id="outer" class="container"> |
| <div id="inner" class="container"> |
| <div id ="target"> |
| </div> |
| </div> |
| </div> |
| )HTML"); |
| |
| RunDocumentLifecycle(); |
| |
| Element* outer = GetElementById("outer"); |
| Element* inner = GetElementById("inner"); |
| Element* target = GetElementById("target"); |
| |
| ElementAnimations* element_animations = target->GetElementAnimations(); |
| EXPECT_EQ(1u, element_animations->Animations().size()); |
| |
| Animation* animation = element_animations->Animations().begin()->key; |
| ASSERT_TRUE(!!animation); |
| EXPECT_FALSE(animation->IsInDisplayLockedSubtree()); |
| |
| inner->setAttribute(html_names::kStyleAttr, "content-visibility: hidden"); |
| RunDocumentLifecycle(); |
| EXPECT_TRUE(animation->IsInDisplayLockedSubtree()); |
| |
| inner->setAttribute(html_names::kStyleAttr, "content-visibility: visible"); |
| RunDocumentLifecycle(); |
| EXPECT_FALSE(animation->IsInDisplayLockedSubtree()); |
| |
| outer->setAttribute(html_names::kStyleAttr, "content-visibility: hidden"); |
| RunDocumentLifecycle(); |
| EXPECT_TRUE(animation->IsInDisplayLockedSubtree()); |
| |
| // Ensure that the animation has not been canceled even though display locked. |
| EXPECT_EQ(1u, target->GetElementAnimations()->Animations().size()); |
| EXPECT_EQ(animation->playState(), "running"); |
| } |
| |
| } // namespace blink |