blob: 7f3e8c61d07173a71a013a9cad6679f770356061 [file] [log] [blame]
// Copyright 2021 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 "base/memory/raw_ptr.h"
#include "base/strings/strcat.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/shared_storage/shared_storage_worklet_driver.h"
#include "content/browser/shared_storage/shared_storage_worklet_host.h"
#include "content/browser/shared_storage/shared_storage_worklet_host_manager.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
namespace content {
namespace {
const char kSimplePagePath[] = "/simple_page.html";
const char kPageWithBlankIframePath[] = "/page_with_blank_iframe.html";
} // namespace
class TestSharedStorageWorkletHost : public SharedStorageWorkletHost {
public:
using SharedStorageWorkletHost::SharedStorageWorkletHost;
~TestSharedStorageWorkletHost() override = default;
void WaitForWorkletResponsesCount(size_t count) {
if (worklet_responses_count_ >= count)
return;
expected_worklet_responses_count_ = count;
worklet_responses_count_waiter_.Run();
}
private:
void OnAddModuleOnWorkletFinished(
blink::mojom::SharedStorageDocumentService::AddModuleOnWorkletCallback
callback,
bool success,
const std::string& error_message) override {
SharedStorageWorkletHost::OnAddModuleOnWorkletFinished(
std::move(callback), success, error_message);
OnWorkletResponseReceived();
}
void OnRunOperationOnWorkletFinished(
bool success,
const std::string& error_message) override {
SharedStorageWorkletHost::OnRunOperationOnWorkletFinished(success,
error_message);
OnWorkletResponseReceived();
}
void OnWorkletResponseReceived() {
++worklet_responses_count_;
if (worklet_responses_count_waiter_.running() &&
worklet_responses_count_ >= expected_worklet_responses_count_) {
worklet_responses_count_waiter_.Quit();
}
}
// How many worklet operations have finished. This only include addModule and
// runOperation.
size_t worklet_responses_count_ = 0;
size_t expected_worklet_responses_count_ = 0;
base::RunLoop worklet_responses_count_waiter_;
};
class TestSharedStorageWorkletHostManager
: public SharedStorageWorkletHostManager {
public:
using SharedStorageWorkletHostManager::SharedStorageWorkletHostManager;
~TestSharedStorageWorkletHostManager() override = default;
std::unique_ptr<SharedStorageWorkletHost> CreateSharedStorageWorkletHost(
std::unique_ptr<SharedStorageWorkletDriver> driver,
RenderFrameHost& render_frame_host) override {
// Use the default handling for unrelated worklet hosts
if (render_frame_host.GetLastCommittedURL().path() != kSimplePagePath &&
render_frame_host.GetLastCommittedURL().path() !=
kPageWithBlankIframePath) {
return SharedStorageWorkletHostManager::CreateSharedStorageWorkletHost(
std::move(driver), render_frame_host);
}
return std::make_unique<TestSharedStorageWorkletHost>(std::move(driver),
render_frame_host);
}
// Precondition: there's only one eligible worklet host.
TestSharedStorageWorkletHost* GetWorkletHost() {
DCHECK_EQ(1u, GetWorkletHostsCount());
return static_cast<TestSharedStorageWorkletHost*>(
GetWorkletHostsForTesting().begin()->second.get());
}
size_t GetWorkletHostsCount() { return GetWorkletHostsForTesting().size(); }
};
class SharedStorageBrowserTest : public ContentBrowserTest {
public:
SharedStorageBrowserTest() {
scoped_feature_list_.InitAndEnableFeature(
blink::features::kSharedStorageAPI);
}
void SetUpOnMainThread() override {
auto test_worklet_host_manager =
std::make_unique<TestSharedStorageWorkletHostManager>();
test_worklet_host_manager_ = test_worklet_host_manager.get();
static_cast<StoragePartitionImpl*>(shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition())
->OverrideSharedStorageWorkletHostManagerForTesting(
std::move(test_worklet_host_manager));
host_resolver()->AddRule("*", "127.0.0.1");
SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
TestSharedStorageWorkletHostManager& test_worklet_host_manager() {
DCHECK(test_worklet_host_manager_);
return *test_worklet_host_manager_;
}
~SharedStorageBrowserTest() override = default;
private:
base::test::ScopedFeatureList scoped_feature_list_;
raw_ptr<TestSharedStorageWorkletHostManager> test_worklet_host_manager_ =
nullptr;
};
IN_PROC_BROWSER_TEST_F(SharedStorageBrowserTest, AddModule_Success) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", kSimplePagePath)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_EQ(nullptr, EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageBrowserTest, AddModule_ScriptNotFound) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", kSimplePagePath)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
std::string expected_error = base::StrCat(
{"a JavaScript error:\nError: Failed to load ",
embedded_test_server()
->GetURL("a.com", "/shared_storage/nonexistent_module.js")
.spec(),
" HTTP status = 404 Not Found.\n"});
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/nonexistent_module.js');
)");
EXPECT_EQ(expected_error, result.error);
EXPECT_EQ(1u, test_worklet_host_manager().GetWorkletHostsCount());
EXPECT_EQ(0u, console_observer.messages().size());
}
IN_PROC_BROWSER_TEST_F(SharedStorageBrowserTest, AddModule_RedirectNotAllowed) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", kSimplePagePath)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
std::string expected_error = base::StrCat(
{"a JavaScript error:\nError: Unexpected redirect on ",
embedded_test_server()
->GetURL("a.com", "/server-redirect?shared_storage/simple_module.js")
.spec(),
".\n"});
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.worklet.addModule(
'/server-redirect?shared_storage/simple_module.js');
)");
EXPECT_EQ(expected_error, result.error);
EXPECT_EQ(1u, test_worklet_host_manager().GetWorkletHostsCount());
EXPECT_EQ(0u, console_observer.messages().size());
}
IN_PROC_BROWSER_TEST_F(SharedStorageBrowserTest,
AddModule_ScriptExecutionFailure) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", kSimplePagePath)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
std::string expected_error = base::StrCat(
{"a JavaScript error:\nError: ",
embedded_test_server()
->GetURL("a.com", "/shared_storage/erroneous_module.js")
.spec(),
":6 Uncaught ReferenceError: undefinedVariable is not defined.\n"});
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/erroneous_module.js');
)");
EXPECT_EQ(expected_error, result.error);
EXPECT_EQ(1u, test_worklet_host_manager().GetWorkletHostsCount());
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("Start executing erroneous_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageBrowserTest,
AddModule_MultipleAddModuleFailure) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", kSimplePagePath)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_EQ(nullptr, EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
std::string expected_error =
"a JavaScript error:\nError: sharedStorage.worklet.addModule() can only "
"be invoked once per browsing context.\n";
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)");
EXPECT_EQ(expected_error, result.error);
EXPECT_EQ(1u, test_worklet_host_manager().GetWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageBrowserTest, RunOperation_Success) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", kSimplePagePath)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_EQ(nullptr, EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ(nullptr, EvalJs(shell(), R"(
sharedStorage.runOperation(
'test-operation', {data: {'customKey': 'customValue'}});
)"));
// There are 2 "worklet operations": addModule and runOperation.
test_worklet_host_manager().GetWorkletHost()->WaitForWorkletResponsesCount(2);
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("{\"customKey\":\"customValue\"}",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("Finish executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[4].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageBrowserTest,
RunOperation_Failure_RunOperationBeforeAddModule) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", kSimplePagePath)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_EQ(nullptr, EvalJs(shell(), R"(
sharedStorage.runOperation(
'test-operation', {data: {'customKey': 'customValue'}});
)"));
EXPECT_EQ(nullptr, EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetWorkletHostsCount());
// There are 2 "worklet operations": runOperation and addModule.
test_worklet_host_manager().GetWorkletHost()->WaitForWorkletResponsesCount(2);
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ(
"sharedStorage.worklet.addModule() has to be called before "
"sharedStorage.runOperation().",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kError,
console_observer.messages()[0].log_level);
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[2].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageBrowserTest,
RunOperation_Failure_InvalidOptionsArgument) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", kSimplePagePath)));
EXPECT_EQ(nullptr, EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EvalJsResult result = EvalJs(shell(), R"(
function testFunction() {}
sharedStorage.runOperation(
'test-operation', {data: {'customKey': testFunction}});
)");
EXPECT_EQ(
std::string(
"a JavaScript error:\nError: function testFunction() {} could not be "
"cloned.\n at eval (__const_std::string&_script__:4:21):\n "
" .then((result) => true ? result : Promise.reject(),\n "
" ^^^^^\n at eval (<anonymous>)\n at "
"EvalJs-runner.js:2:34\n"),
result.error);
}
IN_PROC_BROWSER_TEST_F(SharedStorageBrowserTest,
RunOperation_Failure_ErrorInRunOperation) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", kSimplePagePath)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_EQ(nullptr, EvalJs(shell(), R"(
sharedStorage.worklet.addModule(
'shared_storage/erroneous_function_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing erroneous_function_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[0].log_level);
EXPECT_EQ("Finish executing erroneous_function_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[0].log_level);
EXPECT_EQ(nullptr, EvalJs(shell(), R"(
sharedStorage.runOperation(
'test-operation', {data: {'customKey': 'customValue'}});
)"));
// There are 2 "worklet operations": addModule and runOperation.
test_worklet_host_manager().GetWorkletHost()->WaitForWorkletResponsesCount(2);
EXPECT_EQ(4u, console_observer.messages().size());
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[2].log_level);
EXPECT_EQ("ReferenceError: undefinedVariable is not defined",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kError,
console_observer.messages()[3].log_level);
}
IN_PROC_BROWSER_TEST_F(SharedStorageBrowserTest,
RunOperation_Failure_UnimplementedSharedStorageMethod) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", kSimplePagePath)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_EQ(nullptr, EvalJs(shell(), R"(
sharedStorage.worklet.addModule(
'shared_storage/shared_storage_keys_function_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing shared_storage_keys_function_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing shared_storage_keys_function_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ(nullptr, EvalJs(shell(), R"(
sharedStorage.runOperation('test-operation');
)"));
// There are 2 "worklet operations": addModule and runOperation.
test_worklet_host_manager().GetWorkletHost()->WaitForWorkletResponsesCount(2);
EXPECT_EQ(4u, console_observer.messages().size());
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[2].log_level);
EXPECT_EQ("sharedStorage.keys() is not implemented",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kError,
console_observer.messages()[3].log_level);
}
IN_PROC_BROWSER_TEST_F(SharedStorageBrowserTest, WorkletDestroyed) {
// The test assumes pages get deleted after navigation. To ensure this,
// disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_ASSUMES_NO_CACHING);
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", kSimplePagePath)));
EXPECT_EQ(nullptr, EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetWorkletHostsCount());
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
EXPECT_EQ(0u, test_worklet_host_manager().GetWorkletHostsCount());
}
IN_PROC_BROWSER_TEST_F(SharedStorageBrowserTest, TwoWorklets) {
// The test assumes pages get deleted after navigation. To ensure this,
// disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_ASSUMES_NO_CACHING);
EXPECT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
"a.com", kPageWithBlankIframePath)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
RenderFrameHost* iframe =
static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root()
->child_at(0)
->current_frame_host();
EXPECT_EQ(nullptr, EvalJs(iframe, R"(
sharedStorage.worklet.addModule('shared_storage/simple_module2.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetWorkletHostsCount());
EXPECT_EQ(nullptr, EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(2u, test_worklet_host_manager().GetWorkletHostsCount());
NavigateIframeToURL(shell()->web_contents(), "test_iframe",
GURL(url::kAboutBlankURL));
EXPECT_EQ(1u, test_worklet_host_manager().GetWorkletHostsCount());
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("Executing simple_module2.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[2].message));
}
} // namespace content