blob: 802fe840bd2032b636c3b60695a5f65693a7111e [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/public/common/features.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.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_creation_params.h"
#include "third_party/blink/renderer/core/loader/modulescript/module_script_fetch_request.h"
#include "third_party/blink/renderer/core/script/js_module_script.h"
#include "third_party/blink/renderer/core/testing/dummy_modulator.h"
#include "third_party/blink/renderer/core/testing/module_test_base.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";
constexpr const char* kTestDependencyURLJSON =
"https://example.com/dependency.json";
const KURL TestReferrerURL() {
return KURL(kTestReferrerURL);
}
const KURL TestDependencyURL() {
return KURL(kTestDependencyURL);
}
const KURL TestDependencyURLJSON() {
return KURL(kTestDependencyURLJSON);
}
class DynamicModuleResolverTestModulator final : public DummyModulator {
public:
explicit DynamicModuleResolverTestModulator(ScriptState* script_state)
: script_state_(script_state) {
Modulator::SetModulator(script_state, this);
}
~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;
}
void SetExpectedFetchTreeModuleType(const ModuleType& module_type) {
expected_fetch_tree_module_type_ = module_type;
}
bool fetch_tree_was_called() const { return fetch_tree_was_called_; }
void Trace(Visitor*) const override;
private:
// Implements Modulator:
ScriptState* GetScriptState() final { return script_state_; }
ModuleScript* GetFetchedModuleScript(const KURL& url,
ModuleType module_type) final {
EXPECT_EQ(TestReferrerURL(), url);
ModuleScript* module_script =
JSModuleScript::CreateForTest(this, v8::Local<v8::Module>(), 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 SetAcquiringImportMapsState(AcquiringImportMapsState) final {}
void FetchTree(const KURL& url,
ModuleType module_type,
ResourceFetcher*,
mojom::blink::RequestContextType,
network::mojom::RequestDestination,
const ScriptFetchOptions&,
ModuleScriptCustomFetchType custom_fetch_type,
ModuleTreeClient* client) final {
EXPECT_EQ(expected_fetch_tree_url_, url);
EXPECT_EQ(expected_fetch_tree_module_type_, module_type);
// 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;
}
Member<ScriptState> script_state_;
Member<ModuleTreeClient> pending_client_;
KURL expected_fetch_tree_url_;
ModuleType expected_fetch_tree_module_type_ = ModuleType::kJavaScript;
bool fetch_tree_was_called_ = false;
};
void DynamicModuleResolverTestModulator::Trace(Visitor* visitor) const {
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 WTF::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(context).ToLocalChecked());
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(context).ToLocalChecked());
v8::Local<v8::Value> message =
error_object->Get(context, V8String(isolate, "message"))
.ToLocalChecked();
message_ = ToCoreString(message->ToString(context).ToLocalChecked());
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();
}
};
class DynamicModuleResolverTest : public testing::Test,
public ParametrizedModuleTest {
public:
void SetUp() override { ParametrizedModuleTest::SetUp(); }
void TearDown() override { ParametrizedModuleTest::TearDown(); }
};
} // namespace
TEST_P(DynamicModuleResolverTest, ResolveSuccess) {
V8TestingScope scope;
auto* modulator = MakeGarbageCollected<DynamicModuleResolverTestModulator>(
scope.GetScriptState());
modulator->SetExpectedFetchTreeURL(TestDependencyURL());
auto* promise_resolver =
MakeGarbageCollected<ScriptPromiseResolver>(scope.GetScriptState());
ScriptPromise promise = promise_resolver->Promise();
auto* capture = MakeGarbageCollected<CaptureExportedStringFunction>(
scope.GetScriptState(), "foo");
promise.Then(capture->Bind(),
DynamicModuleResolverTestNotReached::CreateFunction(
scope.GetScriptState()));
auto* resolver = MakeGarbageCollected<DynamicModuleResolver>(modulator);
ModuleRequest module_request("./dependency.js", TextPosition(),
Vector<ImportAssertion>());
resolver->ResolveDynamically(module_request, TestReferrerURL(),
ReferrerScriptInfo(), promise_resolver);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_FALSE(capture->WasCalled());
v8::Local<v8::Module> record = ModuleTestBase::CompileModule(
scope.GetIsolate(), "export const foo = 'hello';", TestReferrerURL());
ModuleScript* module_script =
JSModuleScript::CreateForTest(modulator, record, TestDependencyURL());
EXPECT_TRUE(ModuleRecord::Instantiate(scope.GetScriptState(), record,
TestReferrerURL())
.IsEmpty());
modulator->ResolveTreeFetch(module_script);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(capture->WasCalled());
EXPECT_EQ("hello", capture->CapturedValue());
}
TEST_P(DynamicModuleResolverTest, ResolveJSONModuleSuccess) {
V8TestingScope scope;
auto* modulator = MakeGarbageCollected<DynamicModuleResolverTestModulator>(
scope.GetScriptState());
modulator->SetExpectedFetchTreeURL(TestDependencyURLJSON());
modulator->SetExpectedFetchTreeModuleType(ModuleType::kJSON);
auto* promise_resolver =
MakeGarbageCollected<ScriptPromiseResolver>(scope.GetScriptState());
ScriptPromise promise = promise_resolver->Promise();
auto* resolver = MakeGarbageCollected<DynamicModuleResolver>(modulator);
Vector<ImportAssertion> import_assertions{
ImportAssertion("type", "json", TextPosition())};
ModuleRequest module_request("./dependency.json", TextPosition(),
import_assertions);
resolver->ResolveDynamically(module_request, TestReferrerURL(),
ReferrerScriptInfo(), promise_resolver);
// Instantiating and evaluating a JSON module requires a lot of
// machinery not currently available in this unit test suite. For
// the purposes of a DynamicModuleResolver unit test, it should be sufficient
// to validate that the correct arguments are passed from
// DynamicModuleResolver::ResolveDynamically to Modulator::FetchTree, which is
// validated during DynamicModuleResolverTestModulator::FetchTree.
}
TEST_P(DynamicModuleResolverTest, ResolveSpecifierFailure) {
V8TestingScope scope;
auto* modulator = MakeGarbageCollected<DynamicModuleResolverTestModulator>(
scope.GetScriptState());
modulator->SetExpectedFetchTreeURL(TestDependencyURL());
auto* promise_resolver =
MakeGarbageCollected<ScriptPromiseResolver>(scope.GetScriptState());
ScriptPromise promise = promise_resolver->Promise();
auto* capture =
MakeGarbageCollected<CaptureErrorFunction>(scope.GetScriptState());
promise.Then(DynamicModuleResolverTestNotReached::CreateFunction(
scope.GetScriptState()),
capture->Bind());
auto* resolver = MakeGarbageCollected<DynamicModuleResolver>(modulator);
ModuleRequest module_request("invalid-specifier", TextPosition(),
Vector<ImportAssertion>());
resolver->ResolveDynamically(module_request, 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_P(DynamicModuleResolverTest, ResolveModuleTypeFailure) {
V8TestingScope scope;
auto* modulator = MakeGarbageCollected<DynamicModuleResolverTestModulator>(
scope.GetScriptState());
modulator->SetExpectedFetchTreeURL(TestDependencyURL());
auto* promise_resolver =
MakeGarbageCollected<ScriptPromiseResolver>(scope.GetScriptState());
ScriptPromise promise = promise_resolver->Promise();
auto* capture =
MakeGarbageCollected<CaptureErrorFunction>(scope.GetScriptState());
promise.Then(DynamicModuleResolverTestNotReached::CreateFunction(
scope.GetScriptState()),
capture->Bind());
auto* resolver = MakeGarbageCollected<DynamicModuleResolver>(modulator);
Vector<ImportAssertion> import_assertions{
ImportAssertion("type", "notARealType", TextPosition())};
ModuleRequest module_request("./dependency.js", TextPosition(),
import_assertions);
resolver->ResolveDynamically(module_request, TestReferrerURL(),
ReferrerScriptInfo(), promise_resolver);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(capture->WasCalled());
EXPECT_EQ("TypeError", capture->Name());
EXPECT_EQ("\"notARealType\" is not a valid module type.", capture->Message());
}
TEST_P(DynamicModuleResolverTest, FetchFailure) {
V8TestingScope scope;
auto* modulator = MakeGarbageCollected<DynamicModuleResolverTestModulator>(
scope.GetScriptState());
modulator->SetExpectedFetchTreeURL(TestDependencyURL());
auto* promise_resolver =
MakeGarbageCollected<ScriptPromiseResolver>(scope.GetScriptState());
ScriptPromise promise = promise_resolver->Promise();
auto* capture =
MakeGarbageCollected<CaptureErrorFunction>(scope.GetScriptState());
promise.Then(DynamicModuleResolverTestNotReached::CreateFunction(
scope.GetScriptState()),
capture->Bind());
auto* resolver = MakeGarbageCollected<DynamicModuleResolver>(modulator);
ModuleRequest module_request("./dependency.js", TextPosition(),
Vector<ImportAssertion>());
resolver->ResolveDynamically(module_request, 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_P(DynamicModuleResolverTest, ExceptionThrown) {
V8TestingScope scope;
auto* modulator = MakeGarbageCollected<DynamicModuleResolverTestModulator>(
scope.GetScriptState());
modulator->SetExpectedFetchTreeURL(TestDependencyURL());
auto* promise_resolver =
MakeGarbageCollected<ScriptPromiseResolver>(scope.GetScriptState());
ScriptPromise promise = promise_resolver->Promise();
auto* capture =
MakeGarbageCollected<CaptureErrorFunction>(scope.GetScriptState());
promise.Then(DynamicModuleResolverTestNotReached::CreateFunction(
scope.GetScriptState()),
capture->Bind());
auto* resolver = MakeGarbageCollected<DynamicModuleResolver>(modulator);
ModuleRequest module_request("./dependency.js", TextPosition(),
Vector<ImportAssertion>());
resolver->ResolveDynamically(module_request, TestReferrerURL(),
ReferrerScriptInfo(), promise_resolver);
EXPECT_FALSE(capture->WasCalled());
v8::Local<v8::Module> record = ModuleTestBase::CompileModule(
scope.GetIsolate(), "throw Error('bar')", TestReferrerURL());
ModuleScript* module_script =
JSModuleScript::CreateForTest(modulator, record, TestDependencyURL());
EXPECT_TRUE(ModuleRecord::Instantiate(scope.GetScriptState(), record,
TestReferrerURL())
.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_P(DynamicModuleResolverTest, ResolveWithNullReferrerScriptSuccess) {
V8TestingScope scope;
scope.GetDocument().SetURL(KURL("https://example.com"));
auto* modulator = MakeGarbageCollected<DynamicModuleResolverTestModulator>(
scope.GetScriptState());
modulator->SetExpectedFetchTreeURL(TestDependencyURL());
auto* promise_resolver =
MakeGarbageCollected<ScriptPromiseResolver>(scope.GetScriptState());
ScriptPromise promise = promise_resolver->Promise();
auto* capture = MakeGarbageCollected<CaptureExportedStringFunction>(
scope.GetScriptState(), "foo");
promise.Then(capture->Bind(),
DynamicModuleResolverTestNotReached::CreateFunction(
scope.GetScriptState()));
auto* resolver = MakeGarbageCollected<DynamicModuleResolver>(modulator);
ModuleRequest module_request("./dependency.js", TextPosition(),
Vector<ImportAssertion>());
resolver->ResolveDynamically(module_request, /* null referrer */ KURL(),
ReferrerScriptInfo(), promise_resolver);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_FALSE(capture->WasCalled());
v8::Local<v8::Module> record = ModuleTestBase::CompileModule(
scope.GetIsolate(), "export const foo = 'hello';", TestDependencyURL());
ModuleScript* module_script =
JSModuleScript::CreateForTest(modulator, record, TestDependencyURL());
EXPECT_TRUE(ModuleRecord::Instantiate(scope.GetScriptState(), record,
TestDependencyURL())
.IsEmpty());
modulator->ResolveTreeFetch(module_script);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(capture->WasCalled());
EXPECT_EQ("hello", capture->CapturedValue());
}
TEST_P(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 =
MakeGarbageCollected<ScriptPromiseResolver>(scope.GetScriptState());
auto* resolver = MakeGarbageCollected<DynamicModuleResolver>(modulator);
KURL wrong_base_url("https://example.com/wrong/bar.js");
KURL correct_base_url("https://example.com/correct/baz.js");
ModuleRequest module_request("./dependency.js", TextPosition(),
Vector<ImportAssertion>());
resolver->ResolveDynamically(
module_request, wrong_base_url,
ReferrerScriptInfo(correct_base_url, ScriptFetchOptions(),
ReferrerScriptInfo::BaseUrlSource::kOther),
promise_resolver);
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(modulator->fetch_tree_was_called());
}
// Instantiate tests once with TLA and once without:
INSTANTIATE_TEST_SUITE_P(DynamicModuleResolverTestGroup,
DynamicModuleResolverTest,
testing::Bool(),
ParametrizedModuleTestParamName());
} // namespace blink