| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "cc/animation/worklet_animation.h" |
| |
| #include "base/memory/ptr_util.h" |
| #include "cc/animation/scroll_timeline.h" |
| #include "cc/test/animation_test_common.h" |
| #include "cc/test/animation_timelines_test_common.h" |
| #include "cc/trees/property_tree.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| |
| using ::testing::Mock; |
| using ::testing::NiceMock; |
| using ::testing::Return; |
| using ::testing::_; |
| |
| namespace cc { |
| |
| namespace { |
| |
| class MockKeyframeEffect : public KeyframeEffect { |
| public: |
| MockKeyframeEffect() : KeyframeEffect(0) {} |
| MOCK_METHOD1(Tick, void(base::TimeTicks monotonic_time)); |
| }; |
| |
| class WorkletAnimationTest : public AnimationTimelinesTest { |
| public: |
| WorkletAnimationTest() = default; |
| ~WorkletAnimationTest() override = default; |
| |
| void AttachWorkletAnimation() { |
| client_.RegisterElement(element_id_, ElementListType::ACTIVE); |
| |
| worklet_animation_ = WrapRefCounted( |
| new WorkletAnimation(1, worklet_animation_id_, "test_name", nullptr, |
| nullptr, true /* controlling instance*/)); |
| worklet_animation_->AttachElement(element_id_); |
| host_->AddAnimationTimeline(timeline_); |
| timeline_->AttachAnimation(worklet_animation_); |
| } |
| |
| scoped_refptr<WorkletAnimation> worklet_animation_; |
| WorkletAnimationId worklet_animation_id_{11, 12}; |
| }; |
| |
| class MockScrollTimeline : public ScrollTimeline { |
| public: |
| MockScrollTimeline() |
| : ScrollTimeline(ElementId(), |
| ScrollTimeline::Vertical, |
| base::nullopt, |
| base::nullopt, |
| 0) {} |
| MOCK_CONST_METHOD2(CurrentTime, double(const ScrollTree&, bool)); |
| }; |
| |
| TEST_F(WorkletAnimationTest, NonImplInstanceDoesNotTickKeyframe) { |
| std::unique_ptr<MockKeyframeEffect> effect = |
| std::make_unique<MockKeyframeEffect>(); |
| MockKeyframeEffect* mock_effect = effect.get(); |
| |
| scoped_refptr<WorkletAnimation> worklet_animation = |
| WrapRefCounted(new WorkletAnimation( |
| 1, worklet_animation_id_, "test_name", nullptr, nullptr, |
| false /* not impl instance*/, std::move(effect))); |
| |
| EXPECT_CALL(*mock_effect, Tick(_)).Times(0); |
| |
| MutatorOutputState::AnimationState state(worklet_animation_id_); |
| state.local_times.push_back(base::TimeDelta::FromSecondsD(1)); |
| worklet_animation->SetOutputState(state); |
| worklet_animation->Tick(base::TimeTicks()); |
| } |
| |
| TEST_F(WorkletAnimationTest, LocalTimeIsUsedWhenTicking) { |
| AttachWorkletAnimation(); |
| |
| const float start_opacity = .7f; |
| const float end_opacity = .3f; |
| const double duration = 1.; |
| |
| const float expected_opacity = |
| start_opacity + (end_opacity - start_opacity) / 2; |
| AddOpacityTransitionToAnimation(worklet_animation_.get(), duration, |
| start_opacity, end_opacity, true); |
| |
| KeyframeModel* keyframe_model = |
| worklet_animation_->GetKeyframeModel(TargetProperty::OPACITY); |
| // Impl side animation don't need synchronized start time. |
| keyframe_model->set_needs_synchronized_start_time(false); |
| |
| base::TimeDelta local_time = base::TimeDelta::FromSecondsD(duration / 2); |
| MutatorOutputState::AnimationState state(worklet_animation_id_); |
| state.local_times.push_back(local_time); |
| worklet_animation_->SetOutputState(state); |
| |
| worklet_animation_->Tick(base::TimeTicks()); |
| |
| TestLayer* layer = |
| client_.FindTestLayer(element_id_, ElementListType::ACTIVE); |
| EXPECT_TRUE(layer->is_property_mutated(TargetProperty::OPACITY)); |
| client_.ExpectOpacityPropertyMutated(element_id_, ElementListType::ACTIVE, |
| expected_opacity); |
| } |
| |
| TEST_F(WorkletAnimationTest, CurrentTimeCorrectlyUsesScrollTimeline) { |
| auto scroll_timeline = std::make_unique<MockScrollTimeline>(); |
| EXPECT_CALL(*scroll_timeline, CurrentTime(_, _)).WillRepeatedly(Return(1234)); |
| scoped_refptr<WorkletAnimation> worklet_animation = WorkletAnimation::Create( |
| worklet_animation_id_, "test_name", std::move(scroll_timeline), nullptr); |
| |
| ScrollTree scroll_tree; |
| std::unique_ptr<MutatorInputState> state = |
| std::make_unique<MutatorInputState>(); |
| worklet_animation->UpdateInputState(state.get(), base::TimeTicks::Now(), |
| scroll_tree, true); |
| std::unique_ptr<AnimationWorkletInput> input = |
| state->TakeWorkletState(worklet_animation_id_.scope_id); |
| EXPECT_EQ(1234, input->added_and_updated_animations[0].current_time); |
| } |
| |
| TEST_F(WorkletAnimationTest, |
| CurrentTimeFromDocumentTimelineIsOffsetByStartTime) { |
| scoped_refptr<WorkletAnimation> worklet_animation = WorkletAnimation::Create( |
| worklet_animation_id_, "test_name", nullptr, nullptr); |
| |
| base::TimeTicks first_ticks = |
| base::TimeTicks() + base::TimeDelta::FromMillisecondsD(111); |
| base::TimeTicks second_ticks = |
| base::TimeTicks() + base::TimeDelta::FromMillisecondsD(111 + 123.4); |
| base::TimeTicks third_ticks = |
| base::TimeTicks() + base::TimeDelta::FromMillisecondsD(111 + 246.8); |
| |
| ScrollTree scroll_tree; |
| std::unique_ptr<MutatorInputState> state = |
| std::make_unique<MutatorInputState>(); |
| worklet_animation->UpdateInputState(state.get(), first_ticks, scroll_tree, |
| true); |
| // First state request sets the start time and thus current time should be 0. |
| std::unique_ptr<AnimationWorkletInput> input = |
| state->TakeWorkletState(worklet_animation_id_.scope_id); |
| EXPECT_EQ(0, input->added_and_updated_animations[0].current_time); |
| state.reset(new MutatorInputState); |
| worklet_animation->UpdateInputState(state.get(), second_ticks, scroll_tree, |
| true); |
| input = state->TakeWorkletState(worklet_animation_id_.scope_id); |
| EXPECT_EQ(123.4, input->updated_animations[0].current_time); |
| // Should always offset from start time. |
| state.reset(new MutatorInputState()); |
| worklet_animation->UpdateInputState(state.get(), third_ticks, scroll_tree, |
| true); |
| input = state->TakeWorkletState(worklet_animation_id_.scope_id); |
| EXPECT_EQ(246.8, input->updated_animations[0].current_time); |
| } |
| |
| // This test verifies that worklet animation state is properly updated. |
| TEST_F(WorkletAnimationTest, UpdateInputStateProducesCorrectState) { |
| AttachWorkletAnimation(); |
| |
| const float start_opacity = .7f; |
| const float end_opacity = .3f; |
| const double duration = 1.; |
| |
| int keyframe_model_id = AddOpacityTransitionToAnimation( |
| worklet_animation_.get(), duration, start_opacity, end_opacity, true); |
| |
| ScrollTree scroll_tree; |
| std::unique_ptr<MutatorInputState> state = |
| std::make_unique<MutatorInputState>(); |
| |
| KeyframeModel* keyframe_model = |
| worklet_animation_->GetKeyframeModel(TargetProperty::OPACITY); |
| ASSERT_TRUE(keyframe_model); |
| |
| base::TimeTicks time; |
| worklet_animation_->UpdateInputState(state.get(), time, scroll_tree, true); |
| std::unique_ptr<AnimationWorkletInput> input = |
| state->TakeWorkletState(worklet_animation_id_.scope_id); |
| EXPECT_EQ(input->added_and_updated_animations.size(), 1u); |
| EXPECT_EQ("test_name", input->added_and_updated_animations[0].name); |
| EXPECT_EQ(input->updated_animations.size(), 0u); |
| EXPECT_EQ(input->removed_animations.size(), 0u); |
| |
| // The state of WorkletAnimation is updated to RUNNING after calling |
| // UpdateInputState above. |
| state.reset(new MutatorInputState()); |
| time += base::TimeDelta::FromSecondsD(0.1); |
| worklet_animation_->UpdateInputState(state.get(), time, scroll_tree, true); |
| input = state->TakeWorkletState(worklet_animation_id_.scope_id); |
| EXPECT_EQ(input->added_and_updated_animations.size(), 0u); |
| EXPECT_EQ(input->updated_animations.size(), 1u); |
| EXPECT_EQ(input->removed_animations.size(), 0u); |
| |
| // Operating on individual KeyframeModel doesn't affect the state of |
| // WorkletAnimation. |
| keyframe_model->SetRunState(KeyframeModel::FINISHED, time); |
| state.reset(new MutatorInputState()); |
| time += base::TimeDelta::FromSecondsD(0.1); |
| worklet_animation_->UpdateInputState(state.get(), time, scroll_tree, true); |
| input = state->TakeWorkletState(worklet_animation_id_.scope_id); |
| EXPECT_EQ(input->added_and_updated_animations.size(), 0u); |
| EXPECT_EQ(input->updated_animations.size(), 1u); |
| EXPECT_EQ(input->removed_animations.size(), 0u); |
| |
| // WorkletAnimation sets state to REMOVED when JavaScript fires cancel() which |
| // leads to RemoveKeyframeModel. |
| worklet_animation_->RemoveKeyframeModel(keyframe_model_id); |
| worklet_animation_->UpdateState(true, nullptr); |
| state.reset(new MutatorInputState()); |
| worklet_animation_->UpdateInputState(state.get(), time, scroll_tree, true); |
| input = state->TakeWorkletState(worklet_animation_id_.scope_id); |
| EXPECT_EQ(input->added_and_updated_animations.size(), 0u); |
| EXPECT_EQ(input->updated_animations.size(), 0u); |
| EXPECT_EQ(input->removed_animations.size(), 1u); |
| EXPECT_EQ(input->removed_animations[0], worklet_animation_id_); |
| } |
| |
| // This test verifies that worklet animation gets skipped properly. |
| TEST_F(WorkletAnimationTest, SkipUnchangedAnimations) { |
| AttachWorkletAnimation(); |
| |
| const float start_opacity = .7f; |
| const float end_opacity = .3f; |
| const double duration = 1.; |
| |
| int keyframe_model_id = AddOpacityTransitionToAnimation( |
| worklet_animation_.get(), duration, start_opacity, end_opacity, true); |
| |
| ScrollTree scroll_tree; |
| std::unique_ptr<MutatorInputState> state = |
| std::make_unique<MutatorInputState>(); |
| |
| base::TimeTicks time; |
| worklet_animation_->UpdateInputState(state.get(), time, scroll_tree, true); |
| std::unique_ptr<AnimationWorkletInput> input = |
| state->TakeWorkletState(worklet_animation_id_.scope_id); |
| EXPECT_EQ(input->added_and_updated_animations.size(), 1u); |
| EXPECT_EQ(input->updated_animations.size(), 0u); |
| |
| state.reset(new MutatorInputState()); |
| // No update on the input state if input time stays the same. |
| worklet_animation_->UpdateInputState(state.get(), time, scroll_tree, true); |
| input = state->TakeWorkletState(worklet_animation_id_.scope_id); |
| EXPECT_FALSE(input); |
| |
| state.reset(new MutatorInputState()); |
| // Different input time causes the input state to be updated. |
| time += base::TimeDelta::FromSecondsD(0.1); |
| worklet_animation_->UpdateInputState(state.get(), time, scroll_tree, true); |
| input = state->TakeWorkletState(worklet_animation_id_.scope_id); |
| EXPECT_EQ(input->updated_animations.size(), 1u); |
| |
| state.reset(new MutatorInputState()); |
| // Input state gets updated when the worklet animation is to be removed even |
| // the input time doesn't change. |
| worklet_animation_->RemoveKeyframeModel(keyframe_model_id); |
| worklet_animation_->UpdateInputState(state.get(), time, scroll_tree, true); |
| input = state->TakeWorkletState(worklet_animation_id_.scope_id); |
| EXPECT_EQ(input->updated_animations.size(), 0u); |
| EXPECT_EQ(input->removed_animations.size(), 1u); |
| } |
| |
| } // namespace |
| |
| } // namespace cc |