| // Copyright 2015 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/bind_helpers.h" |
| #include "base/strings/stringprintf.h" |
| #include "chrome/browser/extensions/extension_apitest.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/version_info/version_info.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/page_type.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "extensions/browser/extension_host.h" |
| #include "extensions/browser/process_manager.h" |
| #include "extensions/test/background_page_watcher.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // Pass into ServiceWorkerTest::StartTestFromBackgroundPage to indicate that |
| // registration is expected to succeed. |
| std::string* const kExpectSuccess = nullptr; |
| |
| void DoNothingWithBool(bool b) {} |
| |
| } // namespace |
| |
| class ServiceWorkerTest : public ExtensionApiTest { |
| public: |
| ServiceWorkerTest() : current_channel_(version_info::Channel::UNKNOWN) {} |
| |
| ~ServiceWorkerTest() override {} |
| |
| protected: |
| // Returns the ProcessManager for the test's profile. |
| ProcessManager* process_manager() { return ProcessManager::Get(profile()); } |
| |
| // Starts running a test from the background page test extension. |
| // |
| // This registers a service worker with |script_name|, and fetches the |
| // registration result. |
| // |
| // If |error_or_null| is null (kExpectSuccess), success is expected and this |
| // will fail if there is an error. |
| // If |error_or_null| is not null, nothing is assumed, and the error (which |
| // may be empty) is written to it. |
| const Extension* StartTestFromBackgroundPage(const char* script_name, |
| std::string* error_or_null) { |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("service_worker/background")); |
| CHECK(extension); |
| ExtensionHost* background_host = |
| process_manager()->GetBackgroundHostForExtension(extension->id()); |
| CHECK(background_host); |
| std::string error; |
| CHECK(content::ExecuteScriptAndExtractString( |
| background_host->host_contents(), |
| base::StringPrintf("test.registerServiceWorker('%s')", script_name), |
| &error)); |
| if (error_or_null) |
| *error_or_null = error; |
| else if (!error.empty()) |
| ADD_FAILURE() << "Got unexpected error " << error; |
| return extension; |
| } |
| |
| // Navigates the browser to a new tab at |url|, waits for it to load, then |
| // returns it. |
| content::WebContents* Navigate(const GURL& url) { |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), url, NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION); |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| content::WaitForLoadStop(web_contents); |
| return web_contents; |
| } |
| |
| // Navigates the browser to |url| and returns the new tab's page type. |
| content::PageType NavigateAndGetPageType(const GURL& url) { |
| return Navigate(url)->GetController().GetActiveEntry()->GetPageType(); |
| } |
| |
| // Extracts the innerText from |contents|. |
| std::string ExtractInnerText(content::WebContents* contents) { |
| std::string inner_text; |
| if (!content::ExecuteScriptAndExtractString( |
| contents, |
| "window.domAutomationController.send(document.body.innerText)", |
| &inner_text)) { |
| ADD_FAILURE() << "Failed to get inner text for " |
| << contents->GetVisibleURL(); |
| } |
| return inner_text; |
| } |
| |
| // Navigates the browser to |url|, then returns the innerText of the new |
| // tab's WebContents' main frame. |
| std::string NavigateAndExtractInnerText(const GURL& url) { |
| return ExtractInnerText(Navigate(url)); |
| } |
| |
| private: |
| // Sets the channel to "trunk" since service workers are restricted to trunk. |
| ScopedCurrentChannel current_channel_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ServiceWorkerTest); |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ServiceWorkerTest, RegisterSucceedsOnTrunk) { |
| StartTestFromBackgroundPage("register.js", kExpectSuccess); |
| } |
| |
| // This feature is restricted to trunk, so on dev it should have existing |
| // behavior - which is for it to fail. |
| IN_PROC_BROWSER_TEST_F(ServiceWorkerTest, RegisterFailsOnDev) { |
| ScopedCurrentChannel current_channel_override( |
| version_info::Channel::DEV); |
| std::string error; |
| const Extension* extension = |
| StartTestFromBackgroundPage("register.js", &error); |
| EXPECT_EQ( |
| "Failed to register a ServiceWorker: The URL protocol of the current " |
| "origin ('chrome-extension://" + |
| extension->id() + "') is not supported.", |
| error); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ServiceWorkerTest, FetchArbitraryPaths) { |
| const Extension* extension = |
| StartTestFromBackgroundPage("fetch.js", kExpectSuccess); |
| |
| // Open some arbirary paths. Their contents should be what the service worker |
| // responds with, which in this case is the path of the fetch. |
| EXPECT_EQ( |
| "Caught a fetch for /index.html", |
| NavigateAndExtractInnerText(extension->GetResourceURL("index.html"))); |
| EXPECT_EQ("Caught a fetch for /path/to/other.html", |
| NavigateAndExtractInnerText( |
| extension->GetResourceURL("path/to/other.html"))); |
| EXPECT_EQ("Caught a fetch for /some/text/file.txt", |
| NavigateAndExtractInnerText( |
| extension->GetResourceURL("some/text/file.txt"))); |
| EXPECT_EQ("Caught a fetch for /no/file/extension", |
| NavigateAndExtractInnerText( |
| extension->GetResourceURL("no/file/extension"))); |
| EXPECT_EQ("Caught a fetch for /", |
| NavigateAndExtractInnerText(extension->GetResourceURL(""))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ServiceWorkerTest, |
| LoadingBackgroundPageBypassesServiceWorker) { |
| const Extension* extension = |
| StartTestFromBackgroundPage("fetch.js", kExpectSuccess); |
| |
| std::string kExpectedInnerText = "background.html contents for testing."; |
| |
| // Sanity check that the background page has the expected content. |
| ExtensionHost* background_page = |
| process_manager()->GetBackgroundHostForExtension(extension->id()); |
| ASSERT_TRUE(background_page); |
| EXPECT_EQ(kExpectedInnerText, |
| ExtractInnerText(background_page->host_contents())); |
| |
| // Close the background page. |
| background_page->Close(); |
| BackgroundPageWatcher(process_manager(), extension).WaitForClose(); |
| background_page = nullptr; |
| |
| // Start it again. |
| process_manager()->WakeEventPage(extension->id(), |
| base::Bind(&DoNothingWithBool)); |
| BackgroundPageWatcher(process_manager(), extension).WaitForOpen(); |
| |
| // Content should not have been affected by the fetch, which would otherwise |
| // be "Caught fetch for...". |
| background_page = |
| process_manager()->GetBackgroundHostForExtension(extension->id()); |
| ASSERT_TRUE(background_page); |
| content::WaitForLoadStop(background_page->host_contents()); |
| |
| // TODO(kalman): Everything you've read has been a LIE! It should be: |
| // |
| // EXPECT_EQ(kExpectedInnerText, |
| // ExtractInnerText(background_page->host_contents())); |
| // |
| // but there is a bug, and we're actually *not* bypassing the service worker |
| // for background page loads! For now, let it pass (assert wrong behavior) |
| // because it's not a regression, but this must be fixed eventually. |
| // |
| // Tracked in crbug.com/532720. |
| EXPECT_EQ("Caught a fetch for /background.html", |
| ExtractInnerText(background_page->host_contents())); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ServiceWorkerTest, |
| ServiceWorkerPostsMessageToBackgroundClient) { |
| const Extension* extension = StartTestFromBackgroundPage( |
| "post_message_to_background_client.js", kExpectSuccess); |
| |
| // The service worker in this test simply posts a message to the background |
| // client it receives from getBackgroundClient(). |
| const char* kScript = |
| "var messagePromise = null;\n" |
| "if (test.lastMessageFromServiceWorker) {\n" |
| " messagePromise = Promise.resolve(test.lastMessageFromServiceWorker);\n" |
| "} else {\n" |
| " messagePromise = test.waitForMessage(navigator.serviceWorker);\n" |
| "}\n" |
| "messagePromise.then(function(message) {\n" |
| " window.domAutomationController.send(String(message == 'success'));\n" |
| "})\n"; |
| EXPECT_EQ("true", ExecuteScriptInBackgroundPage(extension->id(), kScript)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ServiceWorkerTest, |
| BackgroundPagePostsMessageToServiceWorker) { |
| const Extension* extension = |
| StartTestFromBackgroundPage("post_message_to_sw.js", kExpectSuccess); |
| |
| // The service worker in this test waits for a message, then echoes it back |
| // by posting a message to the background page via getBackgroundClient(). |
| const char* kScript = |
| "var mc = new MessageChannel();\n" |
| "test.waitForMessage(mc.port1).then(function(message) {\n" |
| " window.domAutomationController.send(String(message == 'hello'));\n" |
| "});\n" |
| "test.registeredServiceWorker.postMessage(\n" |
| " {message: 'hello', port: mc.port2}, [mc.port2])\n"; |
| EXPECT_EQ("true", ExecuteScriptInBackgroundPage(extension->id(), kScript)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ServiceWorkerTest, |
| ServiceWorkerSuspensionOnExtensionUnload) { |
| // For this test, only hold onto the extension's ID and URL + a function to |
| // get a resource URL, because we're going to be disabling and uninstalling |
| // it, which will invalidate the pointer. |
| std::string extension_id; |
| GURL extension_url; |
| { |
| const Extension* extension = |
| StartTestFromBackgroundPage("fetch.js", kExpectSuccess); |
| extension_id = extension->id(); |
| extension_url = extension->url(); |
| } |
| auto get_resource_url = [&extension_url](const std::string& path) { |
| return Extension::GetResourceURL(extension_url, path); |
| }; |
| |
| // Fetch should route to the service worker. |
| EXPECT_EQ("Caught a fetch for /index.html", |
| NavigateAndExtractInnerText(get_resource_url("index.html"))); |
| |
| // Disable the extension. Opening the page should fail. |
| extension_service()->DisableExtension(extension_id, |
| Extension::DISABLE_USER_ACTION); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_EQ(content::PAGE_TYPE_ERROR, |
| NavigateAndGetPageType(get_resource_url("index.html"))); |
| EXPECT_EQ(content::PAGE_TYPE_ERROR, |
| NavigateAndGetPageType(get_resource_url("other.html"))); |
| |
| // Re-enable the extension. Opening pages should immediately start to succeed |
| // again. |
| extension_service()->EnableExtension(extension_id); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_EQ("Caught a fetch for /index.html", |
| NavigateAndExtractInnerText(get_resource_url("index.html"))); |
| EXPECT_EQ("Caught a fetch for /other.html", |
| NavigateAndExtractInnerText(get_resource_url("other.html"))); |
| EXPECT_EQ("Caught a fetch for /another.html", |
| NavigateAndExtractInnerText(get_resource_url("another.html"))); |
| |
| // Uninstall the extension. Opening pages should fail again. |
| base::string16 error; |
| extension_service()->UninstallExtension( |
| extension_id, UninstallReason::UNINSTALL_REASON_FOR_TESTING, |
| base::Bind(&base::DoNothing), &error); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_EQ(content::PAGE_TYPE_ERROR, |
| NavigateAndGetPageType(get_resource_url("index.html"))); |
| EXPECT_EQ(content::PAGE_TYPE_ERROR, |
| NavigateAndGetPageType(get_resource_url("other.html"))); |
| EXPECT_EQ(content::PAGE_TYPE_ERROR, |
| NavigateAndGetPageType(get_resource_url("anotherother.html"))); |
| EXPECT_EQ(content::PAGE_TYPE_ERROR, |
| NavigateAndGetPageType(get_resource_url("final.html"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ServiceWorkerTest, BackgroundPageIsWokenIfAsleep) { |
| const Extension* extension = |
| StartTestFromBackgroundPage("wake_on_fetch.js", kExpectSuccess); |
| |
| // Navigate to special URLs that this test's service worker recognises, each |
| // making a check then populating the response with either "true" or "false". |
| EXPECT_EQ("true", NavigateAndExtractInnerText(extension->GetResourceURL( |
| "background-client-is-awake"))); |
| EXPECT_EQ("true", NavigateAndExtractInnerText( |
| extension->GetResourceURL("ping-background-client"))); |
| // Ping more than once for good measure. |
| EXPECT_EQ("true", NavigateAndExtractInnerText( |
| extension->GetResourceURL("ping-background-client"))); |
| |
| // Shut down the event page. The SW should detect that it's closed, but still |
| // be able to ping it. |
| ExtensionHost* background_page = |
| process_manager()->GetBackgroundHostForExtension(extension->id()); |
| ASSERT_TRUE(background_page); |
| background_page->Close(); |
| BackgroundPageWatcher(process_manager(), extension).WaitForClose(); |
| |
| EXPECT_EQ("false", NavigateAndExtractInnerText(extension->GetResourceURL( |
| "background-client-is-awake"))); |
| EXPECT_EQ("true", NavigateAndExtractInnerText( |
| extension->GetResourceURL("ping-background-client"))); |
| EXPECT_EQ("true", NavigateAndExtractInnerText( |
| extension->GetResourceURL("ping-background-client"))); |
| EXPECT_EQ("true", NavigateAndExtractInnerText(extension->GetResourceURL( |
| "background-client-is-awake"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ServiceWorkerTest, |
| GetBackgroundClientFailsWithNoBackgroundPage) { |
| // This extension doesn't have a background page, only a tab at page.html. |
| // The service worker it registers tries to call getBackgroundClient() and |
| // should fail. |
| // Note that this also tests that service workers can be registered from tabs. |
| EXPECT_TRUE(RunExtensionSubtest("service_worker/no_background", "page.html")); |
| } |
| |
| } // namespace extensions |