blob: 02aba0fa2e1537275c96591f739c4b7dba8d5926 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/api/offscreen/offscreen_document_manager.h"
#include "base/test/bind.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_destroyer.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/api/offscreen/lifetime_enforcer_factories.h"
#include "extensions/browser/api/offscreen/offscreen_document_lifetime_enforcer.h"
#include "extensions/browser/disable_reason.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_host_test_helper.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/offscreen_document_host.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/api/offscreen.h"
#include "extensions/common/mojom/view_type.mojom.h"
#include "extensions/common/switches.h"
#include "extensions/test/test_extension_dir.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "chrome/browser/ui/browser.h"
#endif
static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
namespace extensions {
namespace {
// A programmable lifetime enforcer.
class TestLifetimeEnforcer : public OffscreenDocumentLifetimeEnforcer {
public:
TestLifetimeEnforcer(OffscreenDocumentHost* offscreen_document,
TerminationCallback termination_callback,
NotifyInactiveCallback notify_inactive_callback)
: OffscreenDocumentLifetimeEnforcer(offscreen_document,
std::move(termination_callback),
std::move(notify_inactive_callback)) {
}
~TestLifetimeEnforcer() override = default;
void CallTerminate() { TerminateDocument(); }
void CallNotifyInactive() {
DCHECK(!is_active_);
NotifyInactive();
}
void SetActive(bool is_active) { is_active_ = is_active; }
private:
bool IsActive() override { return is_active_; }
bool is_active_ = true;
};
// A test-only factory method to create and populate a test-only lifetime
// enforcer.
std::unique_ptr<OffscreenDocumentLifetimeEnforcer> CreateTestLifetimeEnforcer(
TestLifetimeEnforcer** lifetime_enforcer_out,
OffscreenDocumentHost* offscreen_document,
OffscreenDocumentLifetimeEnforcer::TerminationCallback termination_callback,
OffscreenDocumentLifetimeEnforcer::NotifyInactiveCallback
notify_inactive_callback) {
auto enforcer = std::make_unique<TestLifetimeEnforcer>(
offscreen_document, std::move(termination_callback),
std::move(notify_inactive_callback));
*lifetime_enforcer_out = enforcer.get();
return enforcer;
}
} // namespace
class OffscreenDocumentManagerBrowserTest : public ExtensionApiTest {
public:
OffscreenDocumentManagerBrowserTest() = default;
~OffscreenDocumentManagerBrowserTest() override = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
ExtensionApiTest::SetUpCommandLine(command_line);
// Add the kOffscreenDocumentTesting switch to allow the use of the
// `TESTING` reason in offscreen document creation.
command_line->AppendSwitch(switches::kOffscreenDocumentTesting);
}
// Creates a new offscreen document with the given `extension`, `url`,
// `reasons`, and `profile`, and waits for it to load.
OffscreenDocumentHost* CreateDocumentAndWaitForLoad(
const Extension& extension,
const GURL& url,
std::set<api::offscreen::Reason> reasons,
Profile& profile) {
ExtensionHostTestHelper host_waiter(&profile);
host_waiter.RestrictToType(mojom::ViewType::kOffscreenDocument);
OffscreenDocumentHost* offscreen_document =
OffscreenDocumentManager::Get(&profile)->CreateOffscreenDocument(
extension, url, reasons);
host_waiter.WaitForHostCompletedFirstLoad();
return offscreen_document;
}
// Same as above, defaulting to a single reason of Reason::kTesting.
OffscreenDocumentHost* CreateDocumentAndWaitForLoad(
const Extension& extension,
const GURL& url,
Profile& profile) {
return CreateDocumentAndWaitForLoad(
extension, url, {api::offscreen::Reason::kTesting}, profile);
}
// Same as the above, defaulting to the on-the-record profile.
OffscreenDocumentHost* CreateDocumentAndWaitForLoad(
const Extension& extension,
const GURL& url) {
return CreateDocumentAndWaitForLoad(extension, url, *profile());
}
OffscreenDocumentManager* offscreen_document_manager() {
return OffscreenDocumentManager::Get(profile());
}
};
// Tests the flow of the OffscreenDocumentManager creating a new offscreen
// document for an extension.
IN_PROC_BROWSER_TEST_F(OffscreenDocumentManagerBrowserTest,
CreateOffscreenDocument) {
static constexpr char kManifest[] =
R"({
"name": "Offscreen Document Test",
"manifest_version": 3,
"version": "0.1"
})";
static constexpr char kOffscreenDocumentHtml[] =
R"(<html>
<body>
<div id="signal">Hello, World</div>
</body>
</html>)";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
kOffscreenDocumentHtml);
// Note: We wrap `extension` in a refptr because we'll unload it later in the
// test and need to make sure the object isn't deleted.
scoped_refptr<const Extension> extension =
LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
// To start, the manager should not have any offscreen documents registered.
EXPECT_EQ(nullptr,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
OffscreenDocumentHost* offscreen_document = CreateDocumentAndWaitForLoad(
*extension, extension->GetResourceURL("offscreen.html"));
{
// Check the document loaded properly. Note: general capabilities of
// offscreen documents are exercised more in the OffscreenDocumentHost
// tests, but this helps sanity check that the manager created it properly.
static constexpr char kScript[] =
R"({
let div = document.getElementById('signal');
div ? div.innerText : '<no div>';
})";
EXPECT_EQ("Hello, World",
content::EvalJs(offscreen_document->host_contents(), kScript));
}
// The manager should now have a record of a document for the extension.
EXPECT_EQ(offscreen_document,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
{
// Disable the extension. This causes it to unload, and the offscreen
// document should be closed.
ExtensionHostTestHelper host_waiter(profile());
host_waiter.RestrictToHost(offscreen_document);
DisableExtension(extension->id(), {disable_reason::DISABLE_USER_ACTION});
host_waiter.WaitForHostDestroyed();
// Note: `offscreen_document` is destroyed at this point.
}
// There should no longer be a document for the extension.
EXPECT_EQ(nullptr,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
}
// Tests the flow of closing an existing offscreen document through the
// manager.
IN_PROC_BROWSER_TEST_F(OffscreenDocumentManagerBrowserTest,
ClosingDocumentThroughTheManager) {
static constexpr char kManifest[] =
R"({
"name": "Offscreen Document Test",
"manifest_version": 3,
"version": "0.1"
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
"<html>offscreen</html>");
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
const GURL offscreen_url = extension->GetResourceURL("offscreen.html");
OffscreenDocumentHost* offscreen_document =
CreateDocumentAndWaitForLoad(*extension, offscreen_url);
ASSERT_TRUE(offscreen_document);
{
ExtensionHostTestHelper host_waiter(profile());
host_waiter.RestrictToHost(offscreen_document);
offscreen_document_manager()->CloseOffscreenDocumentForExtension(
*extension);
}
EXPECT_EQ(nullptr,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
}
// Tests calling window.close() in an offscreen document closes it (through the
// manager).
IN_PROC_BROWSER_TEST_F(OffscreenDocumentManagerBrowserTest,
CallingWindowCloseInAnOffscreenDocumentClosesIt) {
static constexpr char kManifest[] =
R"({
"name": "Offscreen Document Test",
"manifest_version": 3,
"version": "0.1"
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
"<html>offscreen</html>");
scoped_refptr<const Extension> extension =
LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
OffscreenDocumentHost* offscreen_document = CreateDocumentAndWaitForLoad(
*extension, extension->GetResourceURL("offscreen.html"));
ASSERT_TRUE(offscreen_document);
EXPECT_EQ(offscreen_document,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
{
// Call window.close() from the offscreen document. This should cause the
// manager to close the document, destroying the host.
ExtensionHostTestHelper host_waiter(profile());
host_waiter.RestrictToHost(offscreen_document);
ASSERT_TRUE(content::ExecJs(offscreen_document->host_contents(),
"window.close();"));
host_waiter.WaitForHostDestroyed();
}
EXPECT_EQ(nullptr,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
}
// Tests that lifetime enforcers can terminate an offscreen document (such as if
// a hard limit is reached).
IN_PROC_BROWSER_TEST_F(OffscreenDocumentManagerBrowserTest,
LifetimeEnforcement_Terminate) {
// Override the factory method for the testing reason to use our own
// TestLifetimeEnforcer.
TestLifetimeEnforcer* lifetime_enforcer = nullptr;
LifetimeEnforcerFactories::TestingOverride factory_override;
factory_override.map().emplace(
api::offscreen::Reason::kTesting,
base::BindRepeating(&CreateTestLifetimeEnforcer, &lifetime_enforcer));
// Load an extension and create an offscreen document.
static constexpr char kManifest[] =
R"({
"name": "Offscreen Document Test",
"manifest_version": 3,
"version": "0.1"
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
"<html>offscreen</html>");
scoped_refptr<const Extension> extension =
LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
OffscreenDocumentHost* offscreen_document = CreateDocumentAndWaitForLoad(
*extension, extension->GetResourceURL("offscreen.html"));
ASSERT_TRUE(offscreen_document);
EXPECT_EQ(offscreen_document,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
// The lifetime enforcer should have been created. Call the termination
// callback; the offscreen document should be closed.
ASSERT_TRUE(lifetime_enforcer);
lifetime_enforcer->CallTerminate();
// Note: `offscreen_document` is now unsafe to use.
EXPECT_EQ(nullptr,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
}
// Tests that the offscreen document is terminated when all the lifetime
// enforcers (currently only ever one) notify that the document is inactive.
IN_PROC_BROWSER_TEST_F(OffscreenDocumentManagerBrowserTest,
LifetimeEnforcement_NotifyInactive) {
// Override the factory method for the testing reason to use our own
// TestLifetimeEnforcer.
TestLifetimeEnforcer* lifetime_enforcer = nullptr;
LifetimeEnforcerFactories::TestingOverride factory_override;
factory_override.map().emplace(
api::offscreen::Reason::kTesting,
base::BindRepeating(&CreateTestLifetimeEnforcer, &lifetime_enforcer));
// Load an extension and create an offscreen document.
static constexpr char kManifest[] =
R"({
"name": "Offscreen Document Test",
"manifest_version": 3,
"version": "0.1"
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
"<html>offscreen</html>");
scoped_refptr<const Extension> extension =
LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
OffscreenDocumentHost* offscreen_document = CreateDocumentAndWaitForLoad(
*extension, extension->GetResourceURL("offscreen.html"));
ASSERT_TRUE(offscreen_document);
EXPECT_EQ(offscreen_document,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
// The lifetime enforcer should have been created.
ASSERT_TRUE(lifetime_enforcer);
// Set the document to be inactive and notify. The document should be closed.
lifetime_enforcer->SetActive(false);
lifetime_enforcer->CallNotifyInactive();
// Note: `offscreen_document` and `lifetime_enforcer` are now unsafe to use.
EXPECT_EQ(nullptr,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
}
// Tests that when multiple reasons are provided, a lifetime enforcer is
// created for each, and the offscreen document is only terminated once all
// lifetime enforcers indicate the document is inactive.
IN_PROC_BROWSER_TEST_F(
OffscreenDocumentManagerBrowserTest,
LifetimeEnforcement_DocumentIsNotTerminatedUntilAllInactive) {
// Override the factory method for both the dom parsing and blobs reasons to
// use our own TestLifetimeEnforcer.
TestLifetimeEnforcer* dom_parser_enforcer = nullptr;
LifetimeEnforcerFactories::TestingOverride factory_override;
factory_override.map().emplace(
api::offscreen::Reason::kDomParser,
base::BindRepeating(&CreateTestLifetimeEnforcer, &dom_parser_enforcer));
TestLifetimeEnforcer* blobs_enforcer = nullptr;
factory_override.map().emplace(
api::offscreen::Reason::kBlobs,
base::BindRepeating(&CreateTestLifetimeEnforcer, &blobs_enforcer));
// Load an extension an create an offscreen document.
static constexpr char kManifest[] =
R"({
"name": "Offscreen Document Test",
"manifest_version": 3,
"version": "0.1"
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
"<html>offscreen</html>");
scoped_refptr<const Extension> extension =
LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
// Create a new document for both the blob and dom parser reasons.
OffscreenDocumentHost* offscreen_document = CreateDocumentAndWaitForLoad(
*extension, extension->GetResourceURL("offscreen.html"),
{api::offscreen::Reason::kBlobs, api::offscreen::Reason::kDomParser},
*profile());
ASSERT_TRUE(offscreen_document);
EXPECT_EQ(offscreen_document,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
// Each lifetime enforcer should have been created.
ASSERT_TRUE(dom_parser_enforcer);
ASSERT_TRUE(blobs_enforcer);
// Set the dom parser enforcer to be inactive. Note that the blob enforcer is
// still active.
dom_parser_enforcer->SetActive(false);
dom_parser_enforcer->CallNotifyInactive();
// The document should still be around, since the blob enforcer is still
// active.
EXPECT_EQ(offscreen_document,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
// Re-activate and deactivate the dom parser enforcer (to verify it's okay for
// it to cycle between states multiple times).
// Note: Technically, the SetActive() calls here aren't necessary, but it
// better indicates the real scenario.
dom_parser_enforcer->SetActive(true);
dom_parser_enforcer->SetActive(false);
dom_parser_enforcer->CallNotifyInactive();
// The document should still be active.
EXPECT_EQ(offscreen_document,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
// Switch the active enforcers, first making the dom parser enforcer active,
// then making the blob enforcer inactive.
dom_parser_enforcer->SetActive(true);
blobs_enforcer->SetActive(false);
blobs_enforcer->CallNotifyInactive();
// As above, the document should still be around, since a lifetime enforcer
// is still active (this time, the dom parser enforcer).
EXPECT_EQ(offscreen_document,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
// Finally, re-mark the dom parser as inactive.
dom_parser_enforcer->SetActive(false);
dom_parser_enforcer->CallNotifyInactive();
// Note: `offscreen_document`, `dom_parser_enforcer`, and `blobs_enforcer`
// are all now unsafe to use!
// Now, the document should be closed.
EXPECT_EQ(nullptr,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
}
// Tests creating offscreen documents for an incognito split-mode extension.
IN_PROC_BROWSER_TEST_F(OffscreenDocumentManagerBrowserTest,
IncognitoOffscreenDocuments) {
// `split` incognito mode is required in order to allow the extension to
// have a separate process in incognito.
static constexpr char kManifest[] =
R"({
"name": "Offscreen Document Test",
"manifest_version": 3,
"version": "0.1",
"incognito": "split"
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
"<html>offscreen</html>");
scoped_refptr<const Extension> extension =
LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
{
// Enable the extension in incognito. This results in an extension reload;
// wait for that to finish and update the `extension` pointer.
TestExtensionRegistryObserver registry_observer(
ExtensionRegistry::Get(profile()), extension->id());
util::SetIsIncognitoEnabled(extension->id(), profile(),
/*enabled=*/true);
extension = registry_observer.WaitForExtensionLoaded();
}
ASSERT_TRUE(extension);
ASSERT_TRUE(util::IsIncognitoEnabled(extension->id(), profile()));
const GURL offscreen_url = extension->GetResourceURL("offscreen.html");
// Create an on-the-record offscreen document.
OffscreenDocumentHost* on_the_record_host =
CreateDocumentAndWaitForLoad(*extension, offscreen_url);
ASSERT_TRUE(on_the_record_host);
// Ensure the on-the-record context is used.
// Note: Throughout this test, we use
// `OffscreenDocumentHost::host_contents()` to access the BrowserContext
// instead of `OffscreenDocumentHost::browser_context()`; this is to ensure
// that the WebContents is hosted properly.
EXPECT_FALSE(on_the_record_host->host_contents()
->GetBrowserContext()
->IsOffTheRecord());
#if BUILDFLAG(ENABLE_EXTENSIONS)
// Create an incognito browser and an incognito offscreen document, and
// validate that the proper context is used.
Browser* incognito_browser = CreateIncognitoBrowser();
ASSERT_TRUE(incognito_browser);
Profile* incognito_profile = incognito_browser->profile();
#else
Profile* incognito_profile =
profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);
#endif
OffscreenDocumentHost* incognito_host = CreateDocumentAndWaitForLoad(
*extension, offscreen_url, *incognito_profile);
ASSERT_TRUE(incognito_host);
EXPECT_TRUE(
incognito_host->host_contents()->GetBrowserContext()->IsOffTheRecord());
// These should be separate offscreen documents and have separate profiles,
// but the same original profile.
EXPECT_NE(incognito_host, on_the_record_host);
EXPECT_EQ(Profile::FromBrowserContext(
on_the_record_host->host_contents()->GetBrowserContext()),
Profile::FromBrowserContext(
incognito_host->host_contents()->GetBrowserContext())
->GetOriginalProfile());
// Ensure the offscreen documents are registered with the appropriate
// context.
EXPECT_EQ(on_the_record_host,
OffscreenDocumentManager::Get(profile())
->GetOffscreenDocumentForExtension(*extension));
EXPECT_EQ(incognito_host, OffscreenDocumentManager::Get(incognito_profile)
->GetOffscreenDocumentForExtension(*extension));
{
ExtensionHostTestHelper host_waiter(incognito_profile);
host_waiter.RestrictToHost(incognito_host);
#if BUILDFLAG(IS_ANDROID)
// Destroy OTR profile. Consequently, the `incognito_host` should be
// destroyed.
ProfileDestroyer::DestroyOTRProfileWhenAppropriate(incognito_profile);
#else
// Shut down the incognito browser, OTR profile will be destroyed.
// Consequently, the `incognito_host` should be destroyed.
CloseBrowserSynchronously(incognito_browser);
#endif
host_waiter.WaitForHostDestroyed();
// Note: `incognito_host` is destroyed at this point.
}
// The on-the-record document should remain.
EXPECT_EQ(on_the_record_host,
offscreen_document_manager()->GetOffscreenDocumentForExtension(
*extension));
}
} // namespace extensions