blob: a4bbd27297f241d550a7760530e9cabdbe3a99c7 [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/core/script/dynamic_module_resolver.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/bindings/core/v8/referrer_script_info.h"
#include "third_party/blink/renderer/bindings/core/v8/script_function.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/script_value.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/loader/modulescript/module_script_fetch_request.h"
#include "third_party/blink/renderer/core/script/module_script.h"
#include "third_party/blink/renderer/core/testing/dummy_modulator.h"
#include "v8/include/v8.h"
namespace blink {
namespace {
constexpr const char* kTestReferrerURL = "https://example.com/referrer.js";
constexpr const char* kTestDependencyURL = "https://example.com/dependency.js";
const KURL TestReferrerURL() {
return KURL(kTestReferrerURL);
}
const KURL TestDependencyURL() {
return KURL(kTestDependencyURL);
}
class DynamicModuleResolverTestModulator final : public DummyModulator {
public:
explicit DynamicModuleResolverTestModulator(ScriptState* script_state)
: script_state_(script_state) {}
~DynamicModuleResolverTestModulator() override = default;
void ResolveTreeFetch(ModuleScript* module_script) {
ASSERT_TRUE(pending_client_);
pending_client_->NotifyModuleTreeLoadFinished(module_script);
pending_client_ = nullptr;
}
void SetExpectedFetchTreeURL(const KURL& url) {
expected_fetch_tree_url_ = url;
}
bool fetch_tree_was_called() const { return fetch_tree_was_called_; }
void Trace(blink::Visitor*) override;
private:
// Implements Modulator:
ScriptState* GetScriptState() final { return script_state_; }
ModuleScript* GetFetchedModuleScript(const KURL& url) final {
EXPECT_EQ(TestReferrerURL(), url);
ModuleScript* module_script =
ModuleScript::CreateForTest(this, ScriptModule(), url);
return module_script;
}
KURL ResolveModuleSpecifier(const String& module_request,
const KURL& base_url,
String* failure_reason) final {
if (module_request == "invalid-specifier")
return KURL();
return KURL(base_url, module_request);
}
void FetchTree(const KURL& url,
ResourceFetcher*,
mojom::RequestContextType,
const ScriptFetchOptions&,
ModuleScriptCustomFetchType custom_fetch_type,
ModuleTreeClient* client) final {
EXPECT_EQ(expected_fetch_tree_url_, url);
// Currently there are no usage of custom fetch hooks for dynamic import in
// web specifications.
EXPECT_EQ(ModuleScriptCustomFetchType::kNone, custom_fetch_type);
pending_client_ = client;
fetch_tree_was_called_ = true;
}
ScriptValue ExecuteModule(const ModuleScript* module_script,
CaptureEvalErrorFlag capture_error) final {
EXPECT_EQ(CaptureEvalErrorFlag::kCapture, capture_error);
ScriptState::Scope scope(script_state_);
return module_script->Record().Evaluate(script_state_);
}
Member<ScriptState> script_state_;
Member<ModuleTreeClient> pending_client_;
KURL expected_fetch_tree_url_;
bool fetch_tree_was_called_ = false;
};
void DynamicModuleResolverTestModulator::Trace(blink::Visitor* visitor) {
visitor->Trace(script_state_);
visitor->Trace(pending_client_);
DummyModulator::Trace(visitor);
}
// CaptureExportedStringFunction implements a javascript function
// with a single argument of type module namespace.
// CaptureExportedStringFunction captures the exported string value
// from the module namespace as a blink::String, exposed via CapturedValue().
class CaptureExportedStringFunction final : public ScriptFunction {
public:
CaptureExportedStringFunction(ScriptState* script_state,
const String& export_name)
: ScriptFunction(script_state), export_name_(export_name) {}
v8::Local<v8::Function> Bind() { return BindToV8Function(); }
bool WasCalled() const { return was_called_; }
const String& CapturedValue() const { return captured_value_; }
private:
ScriptValue Call(ScriptValue value) override {
was_called_ = true;
v8::Isolate* isolate = GetScriptState()->GetIsolate();
v8::Local<v8::Context> context = GetScriptState()->GetContext();
v8::Local<v8::Object> module_namespace =
value.V8Value()->ToObject(context).ToLocalChecked();
v8::Local<v8::Value> exported_value =
module_namespace->Get(context, V8String(isolate, export_name_))
.ToLocalChecked();
captured_value_ = ToCoreString(exported_value->ToString(isolate));
return ScriptValue();
}
const String export_name_;
bool was_called_ = false;
String captured_value_;
};
// CaptureErrorFunction implements a javascript function which captures
// name and error of the exception passed as its argument.
class CaptureErrorFunction final : public ScriptFunction {
public:
explicit CaptureErrorFunction(ScriptState* script_state)
: ScriptFunction(script_state) {}
v8::Local<v8::Function> Bind() { return BindToV8Function(); }
bool WasCalled() const { return was_called_; }
const String& Name() const { return name_; }
const String& Message() const { return message_; }
private:
ScriptValue Call(ScriptValue value) override {
was_called_ = true;
v8::Isolate* isolate = GetScriptState()->GetIsolate();
v8::Local<v8::Context> context = GetScriptState()->GetContext();
v8::Local<v8::Object> error_object =
value.V8Value()->ToObject(context).ToLocalChecked();
v8::Local<v8::Value> name =
error_object->Get(context, V8String(isolate, "name")).ToLocalChecked();
name_ = ToCoreString(name->ToString(isolate));
v8::Local<v8::Value> message =
error_object->Get(context, V8String(isolate, "message"))
.ToLocalChecked();
message_ = ToCoreString(message->ToString(isolate));
return ScriptValue();
}
bool was_called_ = false;
String name_;
String message_;
};
class DynamicModuleResolverTestNotReached final : public ScriptFunction {
public:
static v8::Local<v8::Function> CreateFunction(ScriptState* script_state) {
auto* not_reached =
MakeGarbageCollected<DynamicModuleResolverTestNotReached>(script_state);
return not_reached->BindToV8Function();
}
explicit DynamicModuleResolverTestNotReached(ScriptState* script_state)
: ScriptFunction(script_state) {}
private:
ScriptValue Call(ScriptValue) override {
ADD_FAILURE();
return ScriptValue();
}
};
} // namespace
TEST(DynamicModuleResolverTest, ResolveSuccess) {
V8TestingScope scope;
auto* modulator = MakeGarbageCollected<DynamicModuleResolverTestModulator>(
scope.GetScriptState());
modulator->SetExpectedFetchTreeURL(TestDependencyURL());
auto* promise_resolver =
ScriptPromiseResolver::Create(scope.GetScriptState());
ScriptPromise promise = promise_resolver->Promise();
auto* capture = MakeGarbageCollected<CaptureExportedStringFunction>(
scope.GetScriptState(), "foo");
promise.Then(capture->Bind(),
DynamicModuleResolverTestNotReached::CreateFunction(
scope.GetScriptState()));
auto* resolver = DynamicModuleResolver::Create(modulator);
resolver->ResolveDynamically("./dependency.js", TestReferrerURL(),
ReferrerScriptInfo(), promise_resolver);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_FALSE(capture->WasCalled());
ScriptModule record = ScriptModule::Compile(
scope.GetIsolate(), "export const foo = 'hello';", TestReferrerURL(),
TestReferrerURL(), ScriptFetchOptions(), TextPosition::MinimumPosition(),
ASSERT_NO_EXCEPTION);
ModuleScript* module_script =
ModuleScript::CreateForTest(modulator, record, TestDependencyURL());
EXPECT_TRUE(record.Instantiate(scope.GetScriptState()).IsEmpty());
modulator->ResolveTreeFetch(module_script);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(capture->WasCalled());
EXPECT_EQ("hello", capture->CapturedValue());
}
TEST(DynamicModuleResolverTest, ResolveSpecifierFailure) {
V8TestingScope scope;
auto* modulator = MakeGarbageCollected<DynamicModuleResolverTestModulator>(
scope.GetScriptState());
modulator->SetExpectedFetchTreeURL(TestDependencyURL());
auto* promise_resolver =
ScriptPromiseResolver::Create(scope.GetScriptState());
ScriptPromise promise = promise_resolver->Promise();
auto* capture =
MakeGarbageCollected<CaptureErrorFunction>(scope.GetScriptState());
promise.Then(DynamicModuleResolverTestNotReached::CreateFunction(
scope.GetScriptState()),
capture->Bind());
auto* resolver = DynamicModuleResolver::Create(modulator);
resolver->ResolveDynamically("invalid-specifier", TestReferrerURL(),
ReferrerScriptInfo(), promise_resolver);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(capture->WasCalled());
EXPECT_EQ("TypeError", capture->Name());
EXPECT_TRUE(capture->Message().StartsWith("Failed to resolve"));
}
TEST(DynamicModuleResolverTest, FetchFailure) {
V8TestingScope scope;
auto* modulator = MakeGarbageCollected<DynamicModuleResolverTestModulator>(
scope.GetScriptState());
modulator->SetExpectedFetchTreeURL(TestDependencyURL());
auto* promise_resolver =
ScriptPromiseResolver::Create(scope.GetScriptState());
ScriptPromise promise = promise_resolver->Promise();
auto* capture =
MakeGarbageCollected<CaptureErrorFunction>(scope.GetScriptState());
promise.Then(DynamicModuleResolverTestNotReached::CreateFunction(
scope.GetScriptState()),
capture->Bind());
auto* resolver = DynamicModuleResolver::Create(modulator);
resolver->ResolveDynamically("./dependency.js", TestReferrerURL(),
ReferrerScriptInfo(), promise_resolver);
EXPECT_FALSE(capture->WasCalled());
modulator->ResolveTreeFetch(nullptr);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(capture->WasCalled());
EXPECT_EQ("TypeError", capture->Name());
EXPECT_TRUE(capture->Message().StartsWith("Failed to fetch"));
}
TEST(DynamicModuleResolverTest, ExceptionThrown) {
V8TestingScope scope;
auto* modulator = MakeGarbageCollected<DynamicModuleResolverTestModulator>(
scope.GetScriptState());
modulator->SetExpectedFetchTreeURL(TestDependencyURL());
auto* promise_resolver =
ScriptPromiseResolver::Create(scope.GetScriptState());
ScriptPromise promise = promise_resolver->Promise();
auto* capture =
MakeGarbageCollected<CaptureErrorFunction>(scope.GetScriptState());
promise.Then(DynamicModuleResolverTestNotReached::CreateFunction(
scope.GetScriptState()),
capture->Bind());
auto* resolver = DynamicModuleResolver::Create(modulator);
resolver->ResolveDynamically("./dependency.js", TestReferrerURL(),
ReferrerScriptInfo(), promise_resolver);
EXPECT_FALSE(capture->WasCalled());
ScriptModule record = ScriptModule::Compile(
scope.GetIsolate(), "throw Error('bar')", TestReferrerURL(),
TestReferrerURL(), ScriptFetchOptions(), TextPosition::MinimumPosition(),
ASSERT_NO_EXCEPTION);
ModuleScript* module_script =
ModuleScript::CreateForTest(modulator, record, TestDependencyURL());
EXPECT_TRUE(record.Instantiate(scope.GetScriptState()).IsEmpty());
modulator->ResolveTreeFetch(module_script);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(capture->WasCalled());
EXPECT_EQ("Error", capture->Name());
EXPECT_EQ("bar", capture->Message());
}
TEST(DynamicModuleResolverTest, ResolveWithNullReferrerScriptSuccess) {
V8TestingScope scope;
scope.GetDocument().SetURL(KURL("https://example.com"));
auto* modulator = MakeGarbageCollected<DynamicModuleResolverTestModulator>(
scope.GetScriptState());
modulator->SetExpectedFetchTreeURL(TestDependencyURL());
auto* promise_resolver =
ScriptPromiseResolver::Create(scope.GetScriptState());
ScriptPromise promise = promise_resolver->Promise();
auto* capture = MakeGarbageCollected<CaptureExportedStringFunction>(
scope.GetScriptState(), "foo");
promise.Then(capture->Bind(),
DynamicModuleResolverTestNotReached::CreateFunction(
scope.GetScriptState()));
auto* resolver = DynamicModuleResolver::Create(modulator);
resolver->ResolveDynamically("./dependency.js", /* null referrer */ KURL(),
ReferrerScriptInfo(), promise_resolver);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_FALSE(capture->WasCalled());
ScriptModule record = ScriptModule::Compile(
scope.GetIsolate(), "export const foo = 'hello';", TestDependencyURL(),
TestDependencyURL(), ScriptFetchOptions(),
TextPosition::MinimumPosition(), ASSERT_NO_EXCEPTION);
ModuleScript* module_script =
ModuleScript::CreateForTest(modulator, record, TestDependencyURL());
EXPECT_TRUE(record.Instantiate(scope.GetScriptState()).IsEmpty());
modulator->ResolveTreeFetch(module_script);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(capture->WasCalled());
EXPECT_EQ("hello", capture->CapturedValue());
}
TEST(DynamicModuleResolverTest, ResolveWithReferrerScriptInfoBaseURL) {
V8TestingScope scope;
scope.GetDocument().SetURL(KURL("https://example.com"));
auto* modulator = MakeGarbageCollected<DynamicModuleResolverTestModulator>(
scope.GetScriptState());
modulator->SetExpectedFetchTreeURL(
KURL("https://example.com/correct/dependency.js"));
auto* promise_resolver =
ScriptPromiseResolver::Create(scope.GetScriptState());
auto* resolver = DynamicModuleResolver::Create(modulator);
KURL wrong_base_url("https://example.com/wrong/bar.js");
KURL correct_base_url("https://example.com/correct/baz.js");
resolver->ResolveDynamically(
"./dependency.js", wrong_base_url,
ReferrerScriptInfo(correct_base_url, ScriptFetchOptions()),
promise_resolver);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(modulator->fetch_tree_was_called());
}
} // namespace blink