| // 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 "third_party/blink/renderer/modules/animationworklet/animation_worklet_global_scope.h" |
| |
| #include "base/synchronization/waitable_event.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/mojom/v8_cache_options.mojom-blink.h" |
| #include "third_party/blink/public/platform/task_type.h" |
| #include "third_party/blink/public/platform/web_url_request.h" |
| #include "third_party/blink/renderer/bindings/core/v8/module_record.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_source_code.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h" |
| #include "third_party/blink/renderer/bindings/core/v8/worker_or_worklet_script_controller.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/script/classic_script.h" |
| #include "third_party/blink/renderer/core/testing/page_test_base.h" |
| #include "third_party/blink/renderer/core/workers/worker_reporting_proxy.h" |
| #include "third_party/blink/renderer/core/workers/worklet_module_responses_map.h" |
| #include "third_party/blink/renderer/modules/animationworklet/animation_worklet.h" |
| #include "third_party/blink/renderer/modules/animationworklet/animation_worklet_proxy_client.h" |
| #include "third_party/blink/renderer/modules/animationworklet/animator.h" |
| #include "third_party/blink/renderer/modules/animationworklet/animator_definition.h" |
| #include "third_party/blink/renderer/modules/worklet/worklet_thread_test_common.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_state.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_loader_options.h" |
| #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" |
| #include "third_party/blink/renderer/platform/wtf/text/text_position.h" |
| |
| #include <memory> |
| |
| namespace blink { |
| namespace { |
| |
| class MockAnimationWorkletProxyClient : public AnimationWorkletProxyClient { |
| public: |
| MockAnimationWorkletProxyClient() |
| : AnimationWorkletProxyClient(0, nullptr, nullptr, nullptr, nullptr), |
| did_add_global_scope_(false) {} |
| void AddGlobalScope(WorkletGlobalScope*) override { |
| did_add_global_scope_ = true; |
| } |
| void SynchronizeAnimatorName(const String&) override {} |
| bool did_add_global_scope() { return did_add_global_scope_; } |
| |
| private: |
| bool did_add_global_scope_; |
| }; |
| |
| std::unique_ptr<AnimationWorkletOutput> ProxyClientMutate( |
| AnimationWorkletInput& state, |
| AnimationWorkletGlobalScope* global_scope) { |
| std::unique_ptr<AnimationWorkletOutput> output = |
| std::make_unique<AnimationWorkletOutput>(); |
| global_scope->UpdateAnimatorsList(state); |
| global_scope->UpdateAnimators(state, output.get(), |
| [](Animator*) { return true; }); |
| return output; |
| } |
| |
| std::unique_ptr<WorkletAnimationEffectTimings> CreateEffectTimings() { |
| auto timings = base::MakeRefCounted<base::RefCountedData<Vector<Timing>>>(); |
| timings->data.push_back(Timing()); |
| return std::make_unique<WorkletAnimationEffectTimings>(std::move(timings)); |
| } |
| |
| } // namespace |
| |
| class AnimationWorkletGlobalScopeTest : public PageTestBase { |
| public: |
| AnimationWorkletGlobalScopeTest() = default; |
| |
| void SetUp() override { |
| PageTestBase::SetUp(IntSize()); |
| NavigateTo(KURL("https://example.com/")); |
| reporting_proxy_ = std::make_unique<WorkerReportingProxy>(); |
| } |
| |
| using TestCalback = void ( |
| AnimationWorkletGlobalScopeTest::*)(WorkerThread*, base::WaitableEvent*); |
| // Create a new animation worklet and run the callback task on it. Terminate |
| // the worklet once the task completion is signaled. |
| void RunTestOnWorkletThread(TestCalback callback) { |
| std::unique_ptr<WorkerThread> worklet = |
| CreateThreadAndProvideAnimationWorkletProxyClient( |
| &GetDocument(), reporting_proxy_.get()); |
| base::WaitableEvent waitable_event; |
| PostCrossThreadTask( |
| *worklet->GetTaskRunner(TaskType::kInternalTest), FROM_HERE, |
| CrossThreadBindOnce(callback, CrossThreadUnretained(this), |
| CrossThreadUnretained(worklet.get()), |
| CrossThreadUnretained(&waitable_event))); |
| waitable_event.Wait(); |
| |
| worklet->Terminate(); |
| worklet->WaitForShutdownForTesting(); |
| } |
| |
| void RunScriptOnWorklet(String source_code, |
| WorkerThread* thread, |
| base::WaitableEvent* waitable_event) { |
| ASSERT_TRUE(thread->IsCurrentThread()); |
| auto* global_scope = To<AnimationWorkletGlobalScope>(thread->GlobalScope()); |
| ASSERT_TRUE( |
| ClassicScript::CreateUnspecifiedScript(ScriptSourceCode(source_code)) |
| ->RunScriptOnWorkerOrWorklet(*global_scope)); |
| |
| waitable_event->Signal(); |
| } |
| |
| void RunBasicParsingTestOnWorklet(WorkerThread* thread, |
| base::WaitableEvent* waitable_event) { |
| ASSERT_TRUE(thread->IsCurrentThread()); |
| auto* global_scope = To<AnimationWorkletGlobalScope>(thread->GlobalScope()); |
| |
| { |
| // registerAnimator() with a valid class definition should define an |
| // animator. |
| String source_code = |
| R"JS( |
| registerAnimator('test', class { |
| constructor () {} |
| animate () {} |
| }); |
| )JS"; |
| ASSERT_TRUE( |
| ClassicScript::CreateUnspecifiedScript(ScriptSourceCode(source_code)) |
| ->RunScriptOnWorkerOrWorklet(*global_scope)); |
| |
| AnimatorDefinition* definition = |
| global_scope->FindDefinitionForTest("test"); |
| ASSERT_TRUE(definition); |
| } |
| |
| { |
| // registerAnimator() with a null class definition should fail to define |
| // an animator. |
| String source_code = "registerAnimator('null', null);"; |
| ASSERT_FALSE( |
| ClassicScript::CreateUnspecifiedScript(ScriptSourceCode(source_code)) |
| ->RunScriptOnWorkerOrWorklet(*global_scope)); |
| EXPECT_FALSE(global_scope->FindDefinitionForTest("null")); |
| } |
| |
| EXPECT_FALSE(global_scope->FindDefinitionForTest("non-existent")); |
| |
| waitable_event->Signal(); |
| } |
| |
| static bool RunScriptAndGetBoolean(AnimationWorkletGlobalScope* global_scope, |
| const String& script) { |
| ScriptState* script_state = |
| global_scope->ScriptController()->GetScriptState(); |
| DCHECK(script_state); |
| v8::Isolate* isolate = script_state->GetIsolate(); |
| DCHECK(isolate); |
| v8::HandleScope scope(isolate); |
| |
| ClassicScript* classic_script = |
| ClassicScript::CreateUnspecifiedScript(ScriptSourceCode(script)); |
| |
| ScriptEvaluationResult result = |
| classic_script->RunScriptOnScriptStateAndReturnValue(script_state); |
| DCHECK_EQ(result.GetResultType(), |
| ScriptEvaluationResult::ResultType::kSuccess); |
| return ToBoolean(isolate, result.GetSuccessValue(), ASSERT_NO_EXCEPTION); |
| } |
| |
| void RunConstructAndAnimateTestOnWorklet( |
| WorkerThread* thread, |
| base::WaitableEvent* waitable_event) { |
| ASSERT_TRUE(thread->IsCurrentThread()); |
| auto* global_scope = To<AnimationWorkletGlobalScope>(thread->GlobalScope()); |
| |
| String source_code = |
| R"JS( |
| // Worklet doesn't have a reference to the global object. Instead, |
| // retrieve it in a tricky way. |
| var global_object = Function('return this')(); |
| global_object.constructed = false; |
| global_object.animated = false; |
| |
| registerAnimator('test', class { |
| constructor () { |
| constructed = true; |
| } |
| animate () { |
| animated = true; |
| } |
| }); |
| )JS"; |
| ASSERT_TRUE( |
| ClassicScript::CreateUnspecifiedScript(ScriptSourceCode(source_code)) |
| ->RunScriptOnWorkerOrWorklet(*global_scope)); |
| |
| EXPECT_FALSE(RunScriptAndGetBoolean( |
| global_scope, "Function('return this')().constructed")) |
| << "constructor is not invoked"; |
| |
| EXPECT_FALSE(RunScriptAndGetBoolean(global_scope, |
| "Function('return this')().animated")) |
| << "animate function is invoked early"; |
| |
| // Passing a new input state with a new animation id should cause the |
| // worklet to create and animate an animator. |
| cc::WorkletAnimationId animation_id = {1, 1}; |
| AnimationWorkletInput state; |
| std::unique_ptr<WorkletAnimationEffectTimings> effect_timings = |
| CreateEffectTimings(); |
| state.added_and_updated_animations.emplace_back( |
| animation_id, "test", 5000, nullptr, std::move(effect_timings)); |
| |
| std::unique_ptr<AnimationWorkletOutput> output = |
| ProxyClientMutate(state, global_scope); |
| EXPECT_EQ(output->animations.size(), 1ul); |
| |
| EXPECT_TRUE(RunScriptAndGetBoolean(global_scope, |
| "Function('return this')().constructed")) |
| << "constructor is not invoked"; |
| |
| EXPECT_TRUE(RunScriptAndGetBoolean(global_scope, |
| "Function('return this')().animated")) |
| << "animate function is not invoked"; |
| |
| waitable_event->Signal(); |
| } |
| |
| void RunStateExistenceTestOnWorklet(WorkerThread* thread, |
| base::WaitableEvent* waitable_event) { |
| ASSERT_TRUE(thread->IsCurrentThread()); |
| auto* global_scope = To<AnimationWorkletGlobalScope>(thread->GlobalScope()); |
| String source_code = |
| R"JS( |
| class Stateful { |
| animate () {} |
| state () {} |
| } |
| |
| class Stateless { |
| animate () {} |
| } |
| |
| class Foo { |
| animate () {} |
| } |
| Foo.prototype.state = function() {}; |
| |
| registerAnimator('stateful_animator', Stateful); |
| registerAnimator('stateless_animator', Stateless); |
| registerAnimator('foo', Foo); |
| )JS"; |
| ASSERT_TRUE( |
| ClassicScript::CreateUnspecifiedScript(ScriptSourceCode(source_code)) |
| ->RunScriptOnWorkerOrWorklet(*global_scope)); |
| |
| AnimatorDefinition* first_definition = |
| global_scope->FindDefinitionForTest("stateful_animator"); |
| EXPECT_TRUE(first_definition->IsStateful()); |
| AnimatorDefinition* second_definition = |
| global_scope->FindDefinitionForTest("stateless_animator"); |
| EXPECT_FALSE(second_definition->IsStateful()); |
| AnimatorDefinition* third_definition = |
| global_scope->FindDefinitionForTest("foo"); |
| EXPECT_TRUE(third_definition->IsStateful()); |
| |
| waitable_event->Signal(); |
| } |
| |
| void RunAnimateOutputTestOnWorklet(WorkerThread* thread, |
| base::WaitableEvent* waitable_event) { |
| AnimationWorkletGlobalScope* global_scope = |
| static_cast<AnimationWorkletGlobalScope*>(thread->GlobalScope()); |
| ASSERT_TRUE(global_scope); |
| ASSERT_TRUE(global_scope->IsAnimationWorkletGlobalScope()); |
| ClassicScript::CreateUnspecifiedScript(ScriptSourceCode( |
| R"JS( |
| registerAnimator('test', class { |
| animate (currentTime, effect) { |
| effect.localTime = 123; |
| } |
| }); |
| )JS")) |
| ->RunScriptOnWorkerOrWorklet(*global_scope); |
| |
| // Passing a new input state with a new animation id should cause the |
| // worklet to create and animate an animator. |
| cc::WorkletAnimationId animation_id = {1, 1}; |
| AnimationWorkletInput state; |
| std::unique_ptr<WorkletAnimationEffectTimings> effect_timings = |
| CreateEffectTimings(); |
| state.added_and_updated_animations.emplace_back( |
| animation_id, "test", 5000, nullptr, std::move(effect_timings)); |
| |
| std::unique_ptr<AnimationWorkletOutput> output = |
| ProxyClientMutate(state, global_scope); |
| |
| EXPECT_EQ(output->animations.size(), 1ul); |
| EXPECT_EQ(output->animations[0].local_times[0], |
| base::TimeDelta::FromMillisecondsD(123)); |
| |
| waitable_event->Signal(); |
| } |
| |
| // This test verifies that an animator instance is not created if |
| // MutatorInputState does not have an animation in |
| // added_and_updated_animations. |
| void RunAnimatorInstanceCreationTestOnWorklet( |
| WorkerThread* thread, |
| base::WaitableEvent* waitable_event) { |
| AnimationWorkletGlobalScope* global_scope = |
| static_cast<AnimationWorkletGlobalScope*>(thread->GlobalScope()); |
| ASSERT_TRUE(global_scope); |
| ASSERT_TRUE(global_scope->IsAnimationWorkletGlobalScope()); |
| EXPECT_EQ(global_scope->GetAnimatorsSizeForTest(), 0u); |
| ClassicScript::CreateUnspecifiedScript(ScriptSourceCode( |
| R"JS( |
| registerAnimator('test', class { |
| animate (currentTime, effect) { |
| effect.localTime = 123; |
| } |
| }); |
| )JS")) |
| ->RunScriptOnWorkerOrWorklet(*global_scope); |
| |
| cc::WorkletAnimationId animation_id = {1, 1}; |
| AnimationWorkletInput state; |
| state.updated_animations.push_back({animation_id, 5000}); |
| EXPECT_EQ(state.added_and_updated_animations.size(), 0u); |
| EXPECT_EQ(state.updated_animations.size(), 1u); |
| |
| std::unique_ptr<AnimationWorkletOutput> output = |
| ProxyClientMutate(state, global_scope); |
| EXPECT_EQ(global_scope->GetAnimatorsSizeForTest(), 0u); |
| |
| state.removed_animations.push_back(animation_id); |
| EXPECT_EQ(state.added_and_updated_animations.size(), 0u); |
| EXPECT_EQ(state.removed_animations.size(), 1u); |
| |
| output = ProxyClientMutate(state, global_scope); |
| EXPECT_EQ(global_scope->GetAnimatorsSizeForTest(), 0u); |
| |
| std::unique_ptr<WorkletAnimationEffectTimings> effect_timings = |
| CreateEffectTimings(); |
| state.added_and_updated_animations.push_back( |
| {animation_id, "test", 5000, nullptr, std::move(effect_timings)}); |
| EXPECT_EQ(state.added_and_updated_animations.size(), 1u); |
| |
| output = ProxyClientMutate(state, global_scope); |
| EXPECT_EQ(global_scope->GetAnimatorsSizeForTest(), 1u); |
| waitable_event->Signal(); |
| } |
| |
| // This test verifies that an animator instance is created and removed |
| // properly. |
| void RunAnimatorInstanceUpdateTestOnWorklet( |
| WorkerThread* thread, |
| base::WaitableEvent* waitable_event) { |
| AnimationWorkletGlobalScope* global_scope = |
| static_cast<AnimationWorkletGlobalScope*>(thread->GlobalScope()); |
| ASSERT_TRUE(global_scope); |
| ASSERT_TRUE(global_scope->IsAnimationWorkletGlobalScope()); |
| EXPECT_EQ(global_scope->GetAnimatorsSizeForTest(), 0u); |
| ClassicScript::CreateUnspecifiedScript(ScriptSourceCode( |
| R"JS( |
| registerAnimator('test', class { |
| animate (currentTime, effect) { |
| effect.localTime = 123; |
| } |
| }); |
| )JS")) |
| ->RunScriptOnWorkerOrWorklet(*global_scope); |
| |
| cc::WorkletAnimationId animation_id = {1, 1}; |
| AnimationWorkletInput state; |
| std::unique_ptr<WorkletAnimationEffectTimings> effect_timings = |
| CreateEffectTimings(); |
| state.added_and_updated_animations.push_back( |
| {animation_id, "test", 5000, nullptr, std::move(effect_timings)}); |
| EXPECT_EQ(state.added_and_updated_animations.size(), 1u); |
| |
| std::unique_ptr<AnimationWorkletOutput> output = |
| ProxyClientMutate(state, global_scope); |
| EXPECT_EQ(global_scope->GetAnimatorsSizeForTest(), 1u); |
| |
| state.added_and_updated_animations.clear(); |
| state.updated_animations.push_back({animation_id, 6000}); |
| EXPECT_EQ(state.added_and_updated_animations.size(), 0u); |
| EXPECT_EQ(state.updated_animations.size(), 1u); |
| |
| output = ProxyClientMutate(state, global_scope); |
| EXPECT_EQ(global_scope->GetAnimatorsSizeForTest(), 1u); |
| |
| state.updated_animations.clear(); |
| state.removed_animations.push_back(animation_id); |
| EXPECT_EQ(state.updated_animations.size(), 0u); |
| EXPECT_EQ(state.removed_animations.size(), 1u); |
| |
| output = ProxyClientMutate(state, global_scope); |
| EXPECT_EQ(global_scope->GetAnimatorsSizeForTest(), 0u); |
| |
| waitable_event->Signal(); |
| } |
| |
| std::unique_ptr<WorkerReportingProxy> reporting_proxy_; |
| }; |
| |
| TEST_F(AnimationWorkletGlobalScopeTest, BasicParsing) { |
| RunTestOnWorkletThread( |
| &AnimationWorkletGlobalScopeTest::RunBasicParsingTestOnWorklet); |
| } |
| |
| TEST_F(AnimationWorkletGlobalScopeTest, ConstructAndAnimate) { |
| RunTestOnWorkletThread( |
| &AnimationWorkletGlobalScopeTest::RunConstructAndAnimateTestOnWorklet); |
| } |
| |
| TEST_F(AnimationWorkletGlobalScopeTest, StateExistence) { |
| RunTestOnWorkletThread( |
| &AnimationWorkletGlobalScopeTest::RunStateExistenceTestOnWorklet); |
| } |
| |
| TEST_F(AnimationWorkletGlobalScopeTest, AnimationOutput) { |
| RunTestOnWorkletThread( |
| &AnimationWorkletGlobalScopeTest::RunAnimateOutputTestOnWorklet); |
| } |
| |
| TEST_F(AnimationWorkletGlobalScopeTest, AnimatorInstanceCreation) { |
| RunTestOnWorkletThread(&AnimationWorkletGlobalScopeTest:: |
| RunAnimatorInstanceCreationTestOnWorklet); |
| } |
| |
| TEST_F(AnimationWorkletGlobalScopeTest, AnimatorInstanceUpdate) { |
| RunTestOnWorkletThread( |
| &AnimationWorkletGlobalScopeTest::RunAnimatorInstanceUpdateTestOnWorklet); |
| } |
| |
| TEST_F(AnimationWorkletGlobalScopeTest, |
| ShouldRegisterItselfAfterFirstAnimatorRegistration) { |
| MockAnimationWorkletProxyClient* proxy_client = |
| MakeGarbageCollected<MockAnimationWorkletProxyClient>(); |
| std::unique_ptr<WorkerThread> worklet = |
| CreateThreadAndProvideAnimationWorkletProxyClient( |
| &GetDocument(), reporting_proxy_.get(), proxy_client); |
| // Animation worklet global scope (AWGS) should not register itself upon |
| // creation. |
| EXPECT_FALSE(proxy_client->did_add_global_scope()); |
| |
| base::WaitableEvent waitable_event; |
| String source_code = |
| R"JS( |
| registerAnimator('test', class { |
| constructor () {} |
| animate () {} |
| }); |
| )JS"; |
| PostCrossThreadTask( |
| *worklet->GetTaskRunner(TaskType::kInternalTest), FROM_HERE, |
| CrossThreadBindOnce(&AnimationWorkletGlobalScopeTest::RunScriptOnWorklet, |
| CrossThreadUnretained(this), std::move(source_code), |
| CrossThreadUnretained(worklet.get()), |
| CrossThreadUnretained(&waitable_event))); |
| waitable_event.Wait(); |
| |
| // AWGS should register itself first time an animator is registered with it. |
| EXPECT_TRUE(proxy_client->did_add_global_scope()); |
| |
| worklet->Terminate(); |
| worklet->WaitForShutdownForTesting(); |
| } |
| |
| } // namespace blink |