blob: 04860d423942c42666bc1d969b88bbb362b68592 [file] [log] [blame]
// Copyright 2018 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 "third_party/blink/renderer/modules/animationworklet/worklet_animation.h"
#include <memory>
#include <utility>
#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/serialization/serialized_script_value.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_scroll_timeline_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/animation_effect_or_animation_effect_sequence.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/scroll_timeline.h"
#include "third_party/blink/renderer/core/animation/worklet_animation_controller.h"
#include "third_party/blink/renderer/core/dom/document.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/core/testing/dummy_page_holder.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
namespace blink {
namespace {
// Only expect precision up to 1 microsecond with an additional epsilon to
// account for float conversion error (mainly due to timeline time getting
// converted between float and base::TimeDelta).
static constexpr double time_error_ms = 0.001 + 1e-13;
#define EXPECT_TIME_NEAR(expected, value) \
EXPECT_NEAR(expected, value, time_error_ms)
KeyframeEffectModelBase* CreateEffectModel() {
StringKeyframeVector frames_mixed_properties;
Persistent<StringKeyframe> keyframe = MakeGarbageCollected<StringKeyframe>();
keyframe->SetOffset(0);
keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "0",
SecureContextMode::kInsecureContext, nullptr);
frames_mixed_properties.push_back(keyframe);
keyframe = MakeGarbageCollected<StringKeyframe>();
keyframe->SetOffset(1);
keyframe->SetCSSPropertyValue(CSSPropertyID::kOpacity, "1",
SecureContextMode::kInsecureContext, nullptr);
frames_mixed_properties.push_back(keyframe);
return MakeGarbageCollected<StringKeyframeEffectModel>(
frames_mixed_properties);
}
KeyframeEffect* CreateKeyframeEffect(Element* element) {
Timing timing;
timing.iteration_duration = AnimationTimeDelta::FromSecondsD(30);
return MakeGarbageCollected<KeyframeEffect>(element, CreateEffectModel(),
timing);
}
WorkletAnimation* CreateWorkletAnimation(
ScriptState* script_state,
Element* element,
const String& animator_name,
ScrollTimeline* scroll_timeline = nullptr) {
AnimationEffectOrAnimationEffectSequence effects;
AnimationEffect* effect = CreateKeyframeEffect(element);
effects.SetAnimationEffect(effect);
DocumentTimelineOrScrollTimeline timeline;
if (scroll_timeline)
timeline.SetScrollTimeline(scroll_timeline);
ScriptValue options;
ScriptState::Scope scope(script_state);
return WorkletAnimation::Create(script_state, animator_name, effects,
timeline, options, ASSERT_NO_EXCEPTION);
}
base::TimeDelta ToTimeDelta(double milliseconds) {
return base::TimeDelta::FromMillisecondsD(milliseconds);
}
} // namespace
class WorkletAnimationTest : public RenderingTest {
public:
WorkletAnimationTest()
: RenderingTest(MakeGarbageCollected<SingleChildLocalFrameClient>()) {}
void SetUp() override {
RenderingTest::SetUp();
element_ = GetDocument().CreateElementForBinding("test");
GetDocument().body()->appendChild(element_);
// Animator has to be registered before constructing WorkletAnimation. For
// unit test this is faked by adding the animator name to
// WorkletAnimationController.
animator_name_ = "WorkletAnimationTest";
GetDocument().GetWorkletAnimationController().SynchronizeAnimatorName(
animator_name_);
worklet_animation_ =
CreateWorkletAnimation(GetScriptState(), element_, animator_name_);
GetDocument().Timeline().ResetForTesting();
GetDocument().GetAnimationClock().ResetTimeForTesting();
}
void SimulateFrame(double milliseconds) {
base::TimeTicks tick =
base::TimeTicks() +
GetDocument().Timeline().CalculateZeroTime().since_origin() +
ToTimeDelta(milliseconds);
GetDocument().GetAnimationClock().UpdateTime(tick);
GetDocument().GetWorkletAnimationController().UpdateAnimationStates();
GetDocument().GetWorkletAnimationController().UpdateAnimationTimings(
kTimingUpdateForAnimationFrame);
}
ScriptState* GetScriptState() {
return ToScriptStateForMainWorld(&GetFrame());
}
Persistent<Element> element_;
Persistent<WorkletAnimation> worklet_animation_;
String animator_name_;
};
TEST_F(WorkletAnimationTest, WorkletAnimationInElementAnimations) {
worklet_animation_->play(ASSERT_NO_EXCEPTION);
EXPECT_EQ(1u,
element_->EnsureElementAnimations().GetWorkletAnimations().size());
worklet_animation_->cancel();
EXPECT_EQ(0u,
element_->EnsureElementAnimations().GetWorkletAnimations().size());
}
// Regression test for crbug.com/1136120, pass if there is no crash.
TEST_F(WorkletAnimationTest, SetCurrentTimeInfNotCrash) {
base::Optional<base::TimeDelta> seek_time =
base::TimeDelta::FromString("inf");
worklet_animation_->SetPlayState(Animation::kRunning);
GetDocument().GetAnimationClock().UpdateTime(base::TimeTicks::Max());
worklet_animation_->SetCurrentTime(seek_time);
}
TEST_F(WorkletAnimationTest, StyleHasCurrentAnimation) {
scoped_refptr<ComputedStyle> style =
GetDocument()
.GetStyleResolver()
.StyleForElement(element_, StyleRecalcContext())
.get();
EXPECT_EQ(false, style->HasCurrentOpacityAnimation());
worklet_animation_->play(ASSERT_NO_EXCEPTION);
element_->EnsureElementAnimations().UpdateAnimationFlags(*style);
EXPECT_EQ(true, style->HasCurrentOpacityAnimation());
}
TEST_F(WorkletAnimationTest,
CurrentTimeFromDocumentTimelineIsOffsetByStartTime) {
WorkletAnimationId id = worklet_animation_->GetWorkletAnimationId();
SimulateFrame(111);
worklet_animation_->play(ASSERT_NO_EXCEPTION);
worklet_animation_->UpdateCompositingState();
std::unique_ptr<AnimationWorkletDispatcherInput> state =
std::make_unique<AnimationWorkletDispatcherInput>();
worklet_animation_->UpdateInputState(state.get());
// First state request sets the start time and thus current time should be 0.
std::unique_ptr<AnimationWorkletInput> input =
state->TakeWorkletState(id.worklet_id);
EXPECT_TIME_NEAR(0, input->added_and_updated_animations[0].current_time);
SimulateFrame(111 + 123.4);
state.reset(new AnimationWorkletDispatcherInput);
worklet_animation_->UpdateInputState(state.get());
input = state->TakeWorkletState(id.worklet_id);
EXPECT_TIME_NEAR(123.4, input->updated_animations[0].current_time);
}
TEST_F(WorkletAnimationTest,
CurrentTimeFromScrollTimelineNotOffsetByStartTime) {
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"));
ASSERT_TRUE(scroller);
ASSERT_TRUE(scroller->IsScrollContainer());
PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea();
ASSERT_TRUE(scrollable_area);
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);
WorkletAnimation* worklet_animation = CreateWorkletAnimation(
GetScriptState(), element_, animator_name_, scroll_timeline);
worklet_animation->play(ASSERT_NO_EXCEPTION);
worklet_animation->UpdateCompositingState();
scrollable_area->SetScrollOffset(ScrollOffset(0, 40),
mojom::blink::ScrollType::kProgrammatic);
// Simulate a new animation frame which allows the timeline to compute new
// current time.
GetPage().Animator().ServiceScriptedAnimations(base::TimeTicks::Now());
ASSERT_TRUE(worklet_animation->currentTime().has_value());
EXPECT_TIME_NEAR(40, worklet_animation->currentTime().value());
scrollable_area->SetScrollOffset(ScrollOffset(0, 70),
mojom::blink::ScrollType::kProgrammatic);
GetPage().Animator().ServiceScriptedAnimations(base::TimeTicks::Now());
ASSERT_TRUE(worklet_animation->currentTime().has_value());
EXPECT_TIME_NEAR(70, worklet_animation->currentTime().value());
}
// Verifies correctness of current time when playback rate is set while the
// animation is in idle state.
TEST_F(WorkletAnimationTest, DocumentTimelineSetPlaybackRate) {
double playback_rate = 2.0;
SimulateFrame(111.0);
worklet_animation_->setPlaybackRate(GetScriptState(), playback_rate);
worklet_animation_->play(ASSERT_NO_EXCEPTION);
worklet_animation_->UpdateCompositingState();
// Zero current time is not impacted by playback rate.
ASSERT_TRUE(worklet_animation_->currentTime().has_value());
EXPECT_TIME_NEAR(0, worklet_animation_->currentTime().value());
// Play the animation until second_ticks.
SimulateFrame(111.0 + 123.4);
// Verify that the current time is updated playback_rate faster than the
// timeline time.
ASSERT_TRUE(worklet_animation_->currentTime().has_value());
EXPECT_TIME_NEAR(123.4 * playback_rate,
worklet_animation_->currentTime().value());
}
// Verifies correctness of current time when playback rate is set while the
// animation is playing.
TEST_F(WorkletAnimationTest, DocumentTimelineSetPlaybackRateWhilePlaying) {
SimulateFrame(0);
double playback_rate = 0.5;
// Start animation.
SimulateFrame(111.0);
worklet_animation_->play(ASSERT_NO_EXCEPTION);
worklet_animation_->UpdateCompositingState();
// Update playback rate after second tick.
SimulateFrame(111.0 + 123.4);
worklet_animation_->setPlaybackRate(GetScriptState(), playback_rate);
// Verify current time after third tick.
SimulateFrame(111.0 + 123.4 + 200.0);
ASSERT_TRUE(worklet_animation_->currentTime().has_value());
EXPECT_TIME_NEAR(123.4 + 200.0 * playback_rate,
worklet_animation_->currentTime().value());
}
TEST_F(WorkletAnimationTest, PausePlay) {
SimulateFrame(0);
worklet_animation_->play(ASSERT_NO_EXCEPTION);
EXPECT_EQ(Animation::kPending, worklet_animation_->PlayState());
SimulateFrame(0);
EXPECT_EQ(Animation::kRunning, worklet_animation_->PlayState());
EXPECT_TRUE(worklet_animation_->Playing());
EXPECT_TIME_NEAR(0, worklet_animation_->currentTime().value());
SimulateFrame(10);
worklet_animation_->pause(ASSERT_NO_EXCEPTION);
EXPECT_EQ(Animation::kPaused, worklet_animation_->PlayState());
EXPECT_FALSE(worklet_animation_->Playing());
EXPECT_TIME_NEAR(10, worklet_animation_->currentTime().value());
SimulateFrame(20);
EXPECT_EQ(Animation::kPaused, worklet_animation_->PlayState());
EXPECT_TIME_NEAR(10, worklet_animation_->currentTime().value());
worklet_animation_->play(ASSERT_NO_EXCEPTION);
EXPECT_EQ(Animation::kPending, worklet_animation_->PlayState());
SimulateFrame(20);
EXPECT_EQ(Animation::kRunning, worklet_animation_->PlayState());
EXPECT_TRUE(worklet_animation_->Playing());
EXPECT_TIME_NEAR(10, worklet_animation_->currentTime().value());
SimulateFrame(30);
EXPECT_EQ(Animation::kRunning, worklet_animation_->PlayState());
EXPECT_TIME_NEAR(20, worklet_animation_->currentTime().value());
}
// Verifies correctness of current time when playback rate is set while
// scroll-linked animation is in idle state.
TEST_F(WorkletAnimationTest, ScrollTimelineSetPlaybackRate) {
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"));
ASSERT_TRUE(scroller);
ASSERT_TRUE(scroller->IsScrollContainer());
PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea();
ASSERT_TRUE(scrollable_area);
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);
WorkletAnimation* worklet_animation = CreateWorkletAnimation(
GetScriptState(), element_, animator_name_, scroll_timeline);
DummyExceptionStateForTesting exception_state;
double playback_rate = 2.0;
// Set playback rate while the animation is in 'idle' state.
worklet_animation->setPlaybackRate(GetScriptState(), playback_rate);
worklet_animation->play(exception_state);
worklet_animation->UpdateCompositingState();
// Initial current time increased by playback rate.
ASSERT_TRUE(worklet_animation->currentTime().has_value());
EXPECT_TIME_NEAR(40, worklet_animation->currentTime().value());
// Update scroll offset.
scrollable_area->SetScrollOffset(ScrollOffset(0, 40),
mojom::blink::ScrollType::kProgrammatic);
// Simulate a new animation frame which allows the timeline to compute new
// current time.
GetPage().Animator().ServiceScriptedAnimations(base::TimeTicks::Now());
// Verify that the current time is updated playback_rate faster than the
// timeline time.
ASSERT_TRUE(worklet_animation->currentTime().has_value());
EXPECT_TIME_NEAR(40 + 20 * playback_rate,
worklet_animation->currentTime().value());
}
// Verifies correctness of current time when playback rate is set while the
// scroll-linked animation is playing.
TEST_F(WorkletAnimationTest, ScrollTimelineSetPlaybackRateWhilePlaying) {
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"));
ASSERT_TRUE(scroller);
ASSERT_TRUE(scroller->IsScrollContainer());
PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea();
ASSERT_TRUE(scrollable_area);
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);
WorkletAnimation* worklet_animation = CreateWorkletAnimation(
GetScriptState(), element_, animator_name_, scroll_timeline);
double playback_rate = 0.5;
// Start the animation.
DummyExceptionStateForTesting exception_state;
worklet_animation->play(exception_state);
worklet_animation->UpdateCompositingState();
// Update scroll offset and playback rate.
scrollable_area->SetScrollOffset(ScrollOffset(0, 40),
mojom::blink::ScrollType::kProgrammatic);
// Simulate a new animation frame which allows the timeline to compute new
// current time.
GetPage().Animator().ServiceScriptedAnimations(base::TimeTicks::Now());
worklet_animation->setPlaybackRate(GetScriptState(), playback_rate);
// Verify the current time after another scroll offset update.
scrollable_area->SetScrollOffset(ScrollOffset(0, 80),
mojom::blink::ScrollType::kProgrammatic);
GetPage().Animator().ServiceScriptedAnimations(base::TimeTicks::Now());
ASSERT_TRUE(worklet_animation->currentTime().has_value());
EXPECT_TIME_NEAR(40 + 40 * playback_rate,
worklet_animation->currentTime().value());
}
// Verifies correcteness of worklet animation start and current time when
// inactive timeline becomes active.
TEST_F(WorkletAnimationTest, ScrollTimelineNewlyActive) {
SetBodyInnerHTML(R"HTML(
<style>
#scroller { overflow: visible; width: 100px; height: 100px; }
#spacer { width: 200px; height: 200px; }
</style>
<div id='scroller'>
<div id='spacer'></div>
</div>
)HTML");
Element* scroller_element = GetElementById("scroller");
ScrollTimelineOptions* options = ScrollTimelineOptions::Create();
DoubleOrScrollTimelineAutoKeyword time_range =
DoubleOrScrollTimelineAutoKeyword::FromDouble(100);
options->setTimeRange(time_range);
options->setScrollSource(scroller_element);
ScrollTimeline* scroll_timeline =
ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION);
ASSERT_FALSE(scroll_timeline->IsActive());
WorkletAnimation* worklet_animation = CreateWorkletAnimation(
GetScriptState(), element_, animator_name_, scroll_timeline);
// Start the animation.
DummyExceptionStateForTesting exception_state;
worklet_animation->play(exception_state);
worklet_animation->UpdateCompositingState();
// Scroll timeline is inactive, thus the current and start times are
// unresolved.
ASSERT_FALSE(worklet_animation->currentTime().has_value());
ASSERT_FALSE(worklet_animation->startTime().has_value());
// Make the timeline active.
scroller_element->setAttribute(html_names::kStyleAttr,
"overflow:scroll;width:100px;height:100px;");
UpdateAllLifecyclePhasesForTest();
// Simulate a new animation frame which allows the timeline to compute new
// current time.
GetPage().Animator().ServiceScriptedAnimations(base::TimeTicks::Now());
ASSERT_TRUE(scroll_timeline->IsActive());
// As the timeline becomes newly active, start and current time must be
// initialized to zero.
auto current_time = worklet_animation->currentTime();
ASSERT_TRUE(current_time.has_value());
EXPECT_TIME_NEAR(0, current_time.value());
auto start_time = worklet_animation->startTime();
ASSERT_TRUE(start_time.has_value());
EXPECT_TIME_NEAR(0, start_time.value());
}
// Verifies correcteness of worklet animation start and current time when
// active timeline becomes inactive and then active again.
TEST_F(WorkletAnimationTest, ScrollTimelineNewlyInactive) {
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");
Element* scroller_element = GetElementById("scroller");
ScrollTimelineOptions* options = ScrollTimelineOptions::Create();
DoubleOrScrollTimelineAutoKeyword time_range =
DoubleOrScrollTimelineAutoKeyword::FromDouble(100);
options->setTimeRange(time_range);
options->setScrollSource(scroller_element);
ScrollTimeline* scroll_timeline =
ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION);
ASSERT_TRUE(scroll_timeline->IsActive());
auto* scroller =
To<LayoutBoxModelObject>(GetLayoutObjectByElementId("scroller"));
ASSERT_TRUE(scroller);
ASSERT_TRUE(scroller->IsScrollContainer());
PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea();
ASSERT_TRUE(scrollable_area);
scrollable_area->SetScrollOffset(ScrollOffset(0, 40),
mojom::blink::ScrollType::kProgrammatic);
// Simulate a new animation frame which allows the timeline to compute new
// current time.
GetPage().Animator().ServiceScriptedAnimations(base::TimeTicks::Now());
WorkletAnimation* worklet_animation = CreateWorkletAnimation(
GetScriptState(), element_, animator_name_, scroll_timeline);
// Start the animation.
DummyExceptionStateForTesting exception_state;
worklet_animation->play(exception_state);
worklet_animation->UpdateCompositingState();
// Scroll timeline is active, thus the current and start times are resolved.
auto current_time = worklet_animation->currentTime();
EXPECT_TRUE(current_time.has_value());
EXPECT_TIME_NEAR(40, current_time.value());
auto start_time = worklet_animation->startTime();
EXPECT_TRUE(start_time.has_value());
EXPECT_TIME_NEAR(0, start_time.value());
// Make the timeline inactive.
scroller_element->setAttribute(html_names::kStyleAttr,
"overflow:visible;width:100px;height:100px;");
UpdateAllLifecyclePhasesForTest();
GetPage().Animator().ServiceScriptedAnimations(base::TimeTicks::Now());
ASSERT_FALSE(scroll_timeline->IsActive());
// As the timeline becomes newly inactive, start time must be unresolved and
// current time the same as previous current time.
start_time = worklet_animation->startTime();
EXPECT_FALSE(start_time.has_value());
current_time = worklet_animation->currentTime();
EXPECT_TRUE(current_time.has_value());
EXPECT_TIME_NEAR(40, current_time.value());
// Make the timeline active again.
scroller_element->setAttribute(html_names::kStyleAttr,
"overflow:scroll;width:100px;height:100px;");
UpdateAllLifecyclePhasesForTest();
GetPage().Animator().ServiceScriptedAnimations(base::TimeTicks::Now());
ASSERT_TRUE(scroll_timeline->IsActive());
// As the timeline becomes newly active, start time must be recalculated and
// current time same as the previous current time.
start_time = worklet_animation->startTime();
EXPECT_TRUE(start_time.has_value());
EXPECT_TIME_NEAR(0, start_time.value());
current_time = worklet_animation->currentTime();
EXPECT_TRUE(current_time.has_value());
EXPECT_TIME_NEAR(40, current_time.value());
}
} // namespace blink