blob: 3811b46324717131065be304fe21f55e7c9b3f5b [file] [log] [blame]
// 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 "testing/gtest/include/gtest/gtest.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/script_module.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/v8_cache_options.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/inspector/worker_devtools_params.h"
#include "third_party/blink/renderer/core/origin_trials/origin_trial_context.h"
#include "third_party/blink/renderer/core/script/script.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/core/workers/global_scope_creation_params.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/animation_and_paint_worklet_thread.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/cross_thread_functional.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_loader_options.h"
#include "third_party/blink/renderer/platform/waitable_event.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_set_global_scope_(false) {}
void SetGlobalScope(WorkletGlobalScope*) override {
did_set_global_scope_ = true;
}
bool did_set_global_scope() { return did_set_global_scope_; }
private:
bool did_set_global_scope_;
};
} // namespace
class AnimationWorkletGlobalScopeTest : public PageTestBase {
public:
AnimationWorkletGlobalScopeTest() = default;
void SetUp() override {
PageTestBase::SetUp(IntSize());
Document* document = &GetDocument();
document->SetURL(KURL("https://example.com/"));
document->UpdateSecurityOrigin(SecurityOrigin::Create(document->Url()));
reporting_proxy_ = std::make_unique<WorkerReportingProxy>();
}
std::unique_ptr<AnimationAndPaintWorkletThread>
CreateAnimationAndPaintWorkletThread(
AnimationWorkletProxyClient* proxy_client) {
std::unique_ptr<AnimationAndPaintWorkletThread> thread =
AnimationAndPaintWorkletThread::CreateForAnimationWorklet(
*reporting_proxy_);
WorkerClients* clients = WorkerClients::Create();
if (proxy_client)
ProvideAnimationWorkletProxyClientTo(clients, proxy_client);
Document* document = &GetDocument();
thread->Start(
std::make_unique<GlobalScopeCreationParams>(
document->Url(), mojom::ScriptType::kModule, document->UserAgent(),
nullptr /* web_worker_fetch_context */, Vector<CSPHeaderAndType>(),
document->GetReferrerPolicy(), document->GetSecurityOrigin(),
document->IsSecureContext(), document->GetHttpsState(), clients,
document->AddressSpace(),
OriginTrialContext::GetTokens(document).get(),
base::UnguessableToken::Create(), nullptr /* worker_settings */,
kV8CacheOptionsDefault, new WorkletModuleResponsesMap),
base::nullopt, std::make_unique<WorkerDevToolsParams>(),
ParentExecutionContextTaskRunners::Create());
return thread;
}
using TestCalback = void (AnimationWorkletGlobalScopeTest::*)(WorkerThread*,
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 =
CreateAnimationAndPaintWorkletThread(nullptr);
WaitableEvent waitable_event;
PostCrossThreadTask(
*worklet->GetTaskRunner(TaskType::kInternalTest), FROM_HERE,
CrossThreadBind(callback, CrossThreadUnretained(this),
CrossThreadUnretained(worklet.get()),
CrossThreadUnretained(&waitable_event)));
waitable_event.Wait();
worklet->Terminate();
worklet->WaitForShutdownForTesting();
}
void RunScriptOnWorklet(String source_code,
WorkerThread* thread,
WaitableEvent* waitable_event) {
ASSERT_TRUE(thread->IsCurrentThread());
auto* global_scope = To<AnimationWorkletGlobalScope>(thread->GlobalScope());
ScriptState* script_state =
global_scope->ScriptController()->GetScriptState();
ASSERT_TRUE(script_state);
v8::Isolate* isolate = script_state->GetIsolate();
ASSERT_TRUE(isolate);
ScriptState::Scope scope(script_state);
ASSERT_TRUE(EvaluateScriptModule(global_scope, source_code));
waitable_event->Signal();
}
void RunBasicParsingTestOnWorklet(WorkerThread* thread,
WaitableEvent* waitable_event) {
ASSERT_TRUE(thread->IsCurrentThread());
auto* global_scope = To<AnimationWorkletGlobalScope>(thread->GlobalScope());
ScriptState* script_state =
global_scope->ScriptController()->GetScriptState();
ASSERT_TRUE(script_state);
v8::Isolate* isolate = script_state->GetIsolate();
ASSERT_TRUE(isolate);
ScriptState::Scope scope(script_state);
{
// registerAnimator() with a valid class definition should define an
// animator.
String source_code =
R"JS(
registerAnimator('test', class {
constructor () {}
animate () {}
});
)JS";
ASSERT_TRUE(EvaluateScriptModule(global_scope, source_code));
AnimatorDefinition* definition =
global_scope->FindDefinitionForTest("test");
ASSERT_TRUE(definition);
EXPECT_TRUE(definition->ConstructorLocal(isolate)->IsFunction());
EXPECT_TRUE(definition->AnimateLocal(isolate)->IsFunction());
}
{
// registerAnimator() with a null class definition should fail to define
// an animator.
String source_code = "registerAnimator('null', null);";
ASSERT_FALSE(EvaluateScriptModule(global_scope, source_code));
EXPECT_FALSE(global_scope->FindDefinitionForTest("null"));
}
EXPECT_FALSE(global_scope->FindDefinitionForTest("non-existent"));
waitable_event->Signal();
}
void RunConstructAndAnimateTestOnWorklet(WorkerThread* thread,
WaitableEvent* waitable_event) {
ASSERT_TRUE(thread->IsCurrentThread());
auto* global_scope = To<AnimationWorkletGlobalScope>(thread->GlobalScope());
ScriptState* script_state =
global_scope->ScriptController()->GetScriptState();
ASSERT_TRUE(script_state);
v8::Isolate* isolate = script_state->GetIsolate();
ASSERT_TRUE(isolate);
ScriptState::Scope scope(script_state);
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(EvaluateScriptModule(global_scope, source_code));
ScriptValue constructed_before =
global_scope->ScriptController()->EvaluateAndReturnValueForTest(
ScriptSourceCode("Function('return this')().constructed"));
EXPECT_FALSE(
ToBoolean(isolate, constructed_before.V8Value(), ASSERT_NO_EXCEPTION))
<< "constructor is not invoked";
ScriptValue animated_before =
global_scope->ScriptController()->EvaluateAndReturnValueForTest(
ScriptSourceCode("Function('return this')().animated"));
EXPECT_FALSE(
ToBoolean(isolate, animated_before.V8Value(), ASSERT_NO_EXCEPTION))
<< "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;
state.added_and_updated_animations.emplace_back(animation_id, "test", 5000,
nullptr, 1);
std::unique_ptr<AnimationWorkletOutput> output =
global_scope->Mutate(state);
EXPECT_TRUE(output);
ScriptValue constructed_after =
global_scope->ScriptController()->EvaluateAndReturnValueForTest(
ScriptSourceCode("Function('return this')().constructed"));
EXPECT_TRUE(
ToBoolean(isolate, constructed_after.V8Value(), ASSERT_NO_EXCEPTION))
<< "constructor is not invoked";
ScriptValue animated_after =
global_scope->ScriptController()->EvaluateAndReturnValueForTest(
ScriptSourceCode("Function('return this')().animated"));
EXPECT_TRUE(
ToBoolean(isolate, animated_after.V8Value(), ASSERT_NO_EXCEPTION))
<< "animate function is not invoked";
waitable_event->Signal();
}
void RunAnimateOutputTestOnWorklet(WorkerThread* thread,
WaitableEvent* waitable_event) {
AnimationWorkletGlobalScope* global_scope =
static_cast<AnimationWorkletGlobalScope*>(thread->GlobalScope());
ASSERT_TRUE(global_scope);
ASSERT_TRUE(global_scope->IsAnimationWorkletGlobalScope());
ScriptState* script_state =
global_scope->ScriptController()->GetScriptState();
ASSERT_TRUE(script_state);
v8::Isolate* isolate = script_state->GetIsolate();
ASSERT_TRUE(isolate);
ScriptState::Scope scope(script_state);
global_scope->ScriptController()->Evaluate(ScriptSourceCode(
R"JS(
registerAnimator('test', class {
animate (currentTime, effect) {
effect.localTime = 123;
}
});
)JS"),
kSharableCrossOrigin);
// 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;
state.added_and_updated_animations.emplace_back(animation_id, "test", 5000,
nullptr, 1);
std::unique_ptr<AnimationWorkletOutput> output =
global_scope->Mutate(state);
EXPECT_TRUE(output);
EXPECT_EQ(output->animations.size(), 1ul);
EXPECT_EQ(output->animations[0].local_times[0],
WTF::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,
WaitableEvent* waitable_event) {
AnimationWorkletGlobalScope* global_scope =
static_cast<AnimationWorkletGlobalScope*>(thread->GlobalScope());
ASSERT_TRUE(global_scope);
ASSERT_TRUE(global_scope->IsAnimationWorkletGlobalScope());
ScriptState* script_state =
global_scope->ScriptController()->GetScriptState();
ASSERT_TRUE(script_state);
v8::Isolate* isolate = script_state->GetIsolate();
ASSERT_TRUE(isolate);
EXPECT_EQ(global_scope->GetAnimatorsSizeForTest(), 0u);
ScriptState::Scope scope(script_state);
global_scope->ScriptController()->Evaluate(ScriptSourceCode(
R"JS(
registerAnimator('test', class {
animate (currentTime, effect) {
effect.localTime = 123;
}
});
)JS"),
kSharableCrossOrigin);
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);
global_scope->Mutate(state);
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);
global_scope->Mutate(state);
EXPECT_EQ(global_scope->GetAnimatorsSizeForTest(), 0u);
state.added_and_updated_animations.push_back(
{animation_id, "test", 5000, nullptr, 1});
EXPECT_EQ(state.added_and_updated_animations.size(), 1u);
global_scope->Mutate(state);
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,
WaitableEvent* waitable_event) {
AnimationWorkletGlobalScope* global_scope =
static_cast<AnimationWorkletGlobalScope*>(thread->GlobalScope());
ASSERT_TRUE(global_scope);
ASSERT_TRUE(global_scope->IsAnimationWorkletGlobalScope());
ScriptState* script_state =
global_scope->ScriptController()->GetScriptState();
ASSERT_TRUE(script_state);
v8::Isolate* isolate = script_state->GetIsolate();
ASSERT_TRUE(isolate);
EXPECT_EQ(global_scope->GetAnimatorsSizeForTest(), 0u);
ScriptState::Scope scope(script_state);
global_scope->ScriptController()->Evaluate(ScriptSourceCode(
R"JS(
registerAnimator('test', class {
animate (currentTime, effect) {
effect.localTime = 123;
}
});
)JS"),
kSharableCrossOrigin);
cc::WorkletAnimationId animation_id = {1, 1};
AnimationWorkletInput state;
state.added_and_updated_animations.push_back(
{animation_id, "test", 5000, nullptr, 1});
EXPECT_EQ(state.added_and_updated_animations.size(), 1u);
global_scope->Mutate(state);
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);
global_scope->Mutate(state);
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);
global_scope->Mutate(state);
EXPECT_EQ(global_scope->GetAnimatorsSizeForTest(), 0u);
waitable_event->Signal();
}
private:
// Returns false when a script evaluation error happens.
bool EvaluateScriptModule(AnimationWorkletGlobalScope* global_scope,
const String& source_code) {
ScriptState* script_state =
global_scope->ScriptController()->GetScriptState();
EXPECT_TRUE(script_state);
const KURL js_url("https://example.com/worklet.js");
ScriptModule module = ScriptModule::Compile(
script_state->GetIsolate(), source_code, js_url, js_url,
ScriptFetchOptions(), TextPosition::MinimumPosition(),
ASSERT_NO_EXCEPTION);
EXPECT_FALSE(module.IsNull());
ScriptValue exception = module.Instantiate(script_state);
EXPECT_TRUE(exception.IsEmpty());
ScriptValue value = module.Evaluate(script_state);
return value.IsEmpty();
}
std::unique_ptr<WorkerReportingProxy> reporting_proxy_;
};
TEST_F(AnimationWorkletGlobalScopeTest, BasicParsing) {
RunTestOnWorkletThread(
&AnimationWorkletGlobalScopeTest::RunBasicParsingTestOnWorklet);
}
TEST_F(AnimationWorkletGlobalScopeTest, ConstructAndAnimate) {
RunTestOnWorkletThread(
&AnimationWorkletGlobalScopeTest::RunConstructAndAnimateTestOnWorklet);
}
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 =
new MockAnimationWorkletProxyClient();
std::unique_ptr<WorkerThread> worklet =
CreateAnimationAndPaintWorkletThread(proxy_client);
// Animation worklet global scope (AWGS) should not register itself upon
// creation.
EXPECT_FALSE(proxy_client->did_set_global_scope());
WaitableEvent waitable_event;
String source_code =
R"JS(
registerAnimator('test', class {
constructor () {}
animate () {}
});
)JS";
PostCrossThreadTask(
*worklet->GetTaskRunner(TaskType::kInternalTest), FROM_HERE,
CrossThreadBind(&AnimationWorkletGlobalScopeTest::RunScriptOnWorklet,
CrossThreadUnretained(this),
Passed(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_set_global_scope());
worklet->Terminate();
worklet->WaitForShutdownForTesting();
}
} // namespace blink