| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <optional> |
| #include <string_view> |
| |
| #include "base/barrier_closure.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_util.h" |
| #include "base/test/gmock_expected_support.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "chrome/browser/content_settings/cookie_settings_factory.h" |
| #include "chrome/browser/extensions/chrome_test_extension_loader.h" |
| #include "chrome/browser/gcm/gcm_profile_service_factory.h" |
| #include "chrome/browser/notifications/notification_display_service_tester.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/push_messaging/push_messaging_app_identifier.h" |
| #include "chrome/browser/push_messaging/push_messaging_constants.h" |
| #include "chrome/browser/push_messaging/push_messaging_features.h" |
| #include "chrome/browser/push_messaging/push_messaging_service_factory.h" |
| #include "chrome/browser/push_messaging/push_messaging_service_impl.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_command_controller.h" |
| #include "chrome/browser/ui/toolbar/app_menu_model.h" |
| #include "chrome/browser/ui/web_applications/app_browser_controller.h" |
| #include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h" |
| #include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h" |
| #include "chrome/browser/ui/web_applications/web_app_browsertest_base.h" |
| #include "chrome/browser/ui/web_applications/web_app_menu_model.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h" |
| #include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h" |
| #include "chrome/browser/web_applications/test/web_app_icon_test_utils.h" |
| #include "chrome/browser/web_applications/test/web_app_test_utils.h" |
| #include "chrome/browser/web_applications/web_app_helpers.h" |
| #include "chrome/browser/web_applications/web_app_provider.h" |
| #include "chrome/browser/web_applications/web_app_sync_bridge.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/content_settings/core/browser/cookie_settings.h" |
| #include "components/content_settings/core/common/content_settings.h" |
| #include "components/gcm_driver/common/gcm_message.h" |
| #include "components/gcm_driver/fake_gcm_profile_service.h" |
| #include "components/permissions/permission_request_manager.h" |
| #include "components/permissions/permission_uma_util.h" |
| #include "components/site_engagement/content/site_engagement_service.h" |
| #include "components/web_package/test_support/signed_web_bundles/web_bundle_signer.h" |
| #include "components/web_package/web_bundle_builder.h" |
| #include "components/webapps/browser/test/service_worker_registration_waiter.h" |
| #include "content/public/browser/push_messaging_service.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/service_worker_context.h" |
| #include "content/public/browser/service_worker_running_info.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_exposed_isolation_level.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "extensions/test/result_catcher.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/mojom/service_worker/service_worker_database.mojom-forward.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "third_party/skia/include/core/SkStream.h" |
| #include "third_party/skia/include/encode/SkPngEncoder.h" |
| |
| namespace web_app { |
| |
| namespace { |
| |
| using ::testing::Eq; |
| using ::testing::Ne; |
| using ::testing::StartsWith; |
| |
| const char kNonAppHost[] = "nonapp.com"; |
| |
| const int32_t kApplicationServerKeyLength = 65; |
| // NIST P-256 public key made available to tests. Must be an uncompressed |
| // point in accordance with SEC1 2.3.3. |
| const uint8_t kApplicationServerKey[kApplicationServerKeyLength] = { |
| 0x04, 0x55, 0x52, 0x6A, 0xA5, 0x6E, 0x8E, 0xAA, 0x47, 0x97, 0x36, |
| 0x10, 0xC1, 0x66, 0x3C, 0x1E, 0x65, 0xBF, 0xA1, 0x7B, 0xEE, 0x48, |
| 0xC9, 0xC6, 0xBB, 0xBF, 0x02, 0x18, 0x53, 0x72, 0x1D, 0x0C, 0x7B, |
| 0xA9, 0xE3, 0x11, 0xB7, 0x03, 0x52, 0x21, 0xD3, 0x71, 0x90, 0x13, |
| 0xA8, 0xC1, 0xCF, 0xED, 0x20, 0xF7, 0x1F, 0xD1, 0x7F, 0xF2, 0x76, |
| 0xB6, 0x01, 0x20, 0xD8, 0x35, 0xA5, 0xD9, 0x3C, 0x43, 0xFD}; |
| |
| std::string GetTestApplicationServerKey() { |
| std::string application_server_key( |
| kApplicationServerKey, |
| kApplicationServerKey + std::size(kApplicationServerKey)); |
| |
| return application_server_key; |
| } |
| |
| class BaseServiceWorkerVersionWaiter |
| : public content::ServiceWorkerContextObserver { |
| public: |
| explicit BaseServiceWorkerVersionWaiter( |
| content::StoragePartition* storage_partition) { |
| DCHECK(storage_partition); |
| |
| service_worker_context_ = storage_partition->GetServiceWorkerContext(); |
| service_worker_context_->AddObserver(this); |
| } |
| |
| BaseServiceWorkerVersionWaiter(const BaseServiceWorkerVersionWaiter&) = |
| delete; |
| BaseServiceWorkerVersionWaiter& operator=( |
| const BaseServiceWorkerVersionWaiter&) = delete; |
| |
| ~BaseServiceWorkerVersionWaiter() override { |
| if (service_worker_context_) { |
| service_worker_context_->RemoveObserver(this); |
| } |
| } |
| |
| protected: |
| raw_ptr<content::ServiceWorkerContext> service_worker_context_ = nullptr; |
| |
| private: |
| void OnDestruct(content::ServiceWorkerContext* context) override { |
| service_worker_context_->RemoveObserver(this); |
| service_worker_context_ = nullptr; |
| } |
| }; |
| |
| class ServiceWorkerVersionActivatedWaiter |
| : public BaseServiceWorkerVersionWaiter { |
| public: |
| ServiceWorkerVersionActivatedWaiter( |
| content::StoragePartition* storage_partition, |
| const GURL& url) |
| : BaseServiceWorkerVersionWaiter(storage_partition), url_(url) {} |
| |
| int64_t AwaitVersionActivated() { return future.Get(); } |
| |
| private: |
| // content::ServiceWorkerContextObserver: |
| void OnVersionActivated(int64_t version_id, const GURL& scope) override { |
| if (content::ServiceWorkerContext::ScopeMatches(scope, url_)) { |
| future.SetValue(version_id); |
| } |
| } |
| |
| GURL url_; |
| base::test::TestFuture<int64_t> future; |
| }; |
| |
| class ServiceWorkerVersionStartedRunningWaiter |
| : public BaseServiceWorkerVersionWaiter { |
| public: |
| ServiceWorkerVersionStartedRunningWaiter( |
| content::StoragePartition* storage_partition, |
| int64_t version_id) |
| : BaseServiceWorkerVersionWaiter(storage_partition), |
| version_id_(version_id) {} |
| |
| void AwaitVersionStartedRunning() { run_loop_.Run(); } |
| |
| private: |
| // content::ServiceWorkerContextObserver: |
| void OnVersionStartedRunning( |
| int64_t version_id, |
| const content::ServiceWorkerRunningInfo& running_info) override { |
| if (version_id == version_id_) { |
| run_loop_.Quit(); |
| } |
| } |
| |
| const int64_t version_id_ = blink::mojom::kInvalidServiceWorkerVersionId; |
| base::RunLoop run_loop_; |
| }; |
| |
| class ServiceWorkerVersionStoppedRunningWaiter |
| : public BaseServiceWorkerVersionWaiter { |
| public: |
| ServiceWorkerVersionStoppedRunningWaiter( |
| content::StoragePartition* storage_partition, |
| int64_t version_id) |
| : BaseServiceWorkerVersionWaiter(storage_partition), |
| version_id_(version_id) {} |
| |
| void AwaitVersionStoppedRunning() { run_loop_.Run(); } |
| |
| private: |
| // content::ServiceWorkerContextObserver: |
| void OnVersionStoppedRunning(int64_t version_id) override { |
| if (version_id == version_id_) { |
| run_loop_.Quit(); |
| } |
| } |
| |
| const int64_t version_id_ = blink::mojom::kInvalidServiceWorkerVersionId; |
| base::RunLoop run_loop_; |
| }; |
| |
| std::string CreateSerializedIcon() { |
| SkBitmap icon = CreateSquareIcon(256, SK_ColorBLUE); |
| SkDynamicMemoryWStream stream; |
| CHECK(SkPngEncoder::Encode(&stream, icon.pixmap(), {})); |
| sk_sp<SkData> icon_skdata = stream.detachAsData(); |
| return std::string(static_cast<const char*>(icon_skdata->data()), |
| icon_skdata->size()); |
| } |
| |
| } // namespace |
| |
| class IsolatedWebAppBrowserTest : public IsolatedWebAppBrowserTestHarness { |
| public: |
| IsolatedWebAppBrowserTest() { |
| isolated_web_app_dev_server_ = |
| CreateAndStartServer(FILE_PATH_LITERAL("web_apps/simple_isolated_app")); |
| } |
| |
| IsolatedWebAppBrowserTest(const IsolatedWebAppBrowserTest&) = delete; |
| IsolatedWebAppBrowserTest& operator=(const IsolatedWebAppBrowserTest&) = |
| delete; |
| |
| protected: |
| content::StoragePartition* default_storage_partition() { |
| return browser()->profile()->GetDefaultStoragePartition(); |
| } |
| |
| content::RenderFrameHost* GetPrimaryMainFrame(Browser* browser) { |
| return browser->tab_strip_model() |
| ->GetActiveWebContents() |
| ->GetPrimaryMainFrame(); |
| } |
| |
| const net::EmbeddedTestServer& isolated_web_app_dev_server() { |
| return *isolated_web_app_dev_server_.get(); |
| } |
| |
| private: |
| std::unique_ptr<net::EmbeddedTestServer> isolated_web_app_dev_server_; |
| }; |
| |
| // TODO(crbug.com/325132780): Remove when manifest fallback logic is gone. |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppBrowserTest, NewManifestPathPreferred) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| std::unique_ptr<ScopedBundledIsolatedWebApp> app = |
| IsolatedWebAppBuilder(ManifestBuilder().SetName("new path used")) |
| .AddResource("/manifest.webmanifest", |
| ManifestBuilder().SetName("old path used").ToJson(), |
| "application/manifest+json") |
| .BuildBundle(); |
| |
| app->TrustSigningKey(); |
| IsolatedWebAppUrlInfo url_info = app->Install(profile()).value(); |
| |
| EXPECT_EQ(provider().registrar_unsafe().GetAppShortName(url_info.app_id()), |
| "new path used"); |
| } |
| |
| // TODO(crbug.com/325132780): Remove when manifest fallback logic is gone. |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppBrowserTest, FallsBackToOldManifestPath) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| |
| auto key_pair = web_package::WebBundleSigner::Ed25519KeyPair::CreateRandom(); |
| auto web_bundle_id = |
| web_package::SignedWebBundleId::CreateForEd25519PublicKey( |
| key_pair.public_key); |
| |
| // We don't use IsolatedWebAppBuilder here becuause it can't create a bundle |
| // without a manifest. |
| web_package::WebBundleBuilder builder; |
| builder.AddExchange( |
| "/manifest.webmanifest", |
| {{":status", "200"}, {"content-type", "application/manifest+json"}}, |
| ManifestBuilder() |
| .AddIcon("/icon.png", gfx::Size(256, 256), "image/png") |
| .SetName("fallback manifest") |
| .ToJson()); |
| builder.AddExchange("/", {{":status", "200"}, {"content-type", "text/html"}}, |
| "Test html"); |
| builder.AddExchange("/icon.png", |
| {{":status", "200"}, {"content-type", "image/png"}}, |
| CreateSerializedIcon()); |
| |
| auto app = ScopedBundledIsolatedWebApp::Create( |
| web_bundle_id, web_package::WebBundleSigner::SignBundle( |
| builder.CreateBundle(), {key_pair})); |
| app->TrustSigningKey(); |
| ASSERT_OK_AND_ASSIGN(auto url_info, app->Install(profile())); |
| |
| EXPECT_EQ(provider().registrar_unsafe().GetAppShortName(url_info.app_id()), |
| "fallback manifest"); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppBrowserTest, AppsPartitioned) { |
| web_app::IsolatedWebAppUrlInfo url_info1 = InstallDevModeProxyIsolatedWebApp( |
| isolated_web_app_dev_server().GetOrigin()); |
| web_app::IsolatedWebAppUrlInfo url_info2 = InstallDevModeProxyIsolatedWebApp( |
| isolated_web_app_dev_server().GetOrigin()); |
| |
| auto* non_app_frame = ui_test_utils::NavigateToURL( |
| browser(), https_server()->GetURL("/simple.html")); |
| EXPECT_TRUE(non_app_frame); |
| EXPECT_EQ(default_storage_partition(), non_app_frame->GetStoragePartition()); |
| |
| auto* app_frame = OpenApp(url_info1.app_id()); |
| EXPECT_NE(default_storage_partition(), app_frame->GetStoragePartition()); |
| |
| auto* app2_frame = OpenApp(url_info2.app_id()); |
| EXPECT_NE(default_storage_partition(), app2_frame->GetStoragePartition()); |
| |
| EXPECT_NE(app_frame->GetStoragePartition(), |
| app2_frame->GetStoragePartition()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppBrowserTest, |
| OmniboxNavigationOpensNewPwaWindow) { |
| web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp( |
| isolated_web_app_dev_server().GetOrigin()); |
| |
| GURL app_url = url_info.origin().GetURL().Resolve("/index.html"); |
| auto* app_frame = |
| NavigateToURLInNewTab(browser(), app_url, WindowOpenDisposition::UNKNOWN); |
| |
| // The browser shouldn't have opened the app's page. |
| EXPECT_EQ(GetPrimaryMainFrame(browser())->GetLastCommittedURL(), GURL()); |
| |
| // The app's frame should belong to an isolated PWA browser window. |
| Browser* app_browser = GetBrowserFromFrame(app_frame); |
| EXPECT_NE(app_browser, browser()); |
| EXPECT_TRUE( |
| AppBrowserController::IsForWebApp(app_browser, url_info.app_id())); |
| EXPECT_EQ(content::WebExposedIsolationLevel::kIsolatedApplication, |
| app_frame->GetWebExposedIsolationLevel()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| IsolatedWebAppBrowserTest, |
| OmniboxNavigationOpensNewPwaWindowEvenIfUserDisplayModeIsBrowser) { |
| web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp( |
| isolated_web_app_dev_server().GetOrigin()); |
| |
| WebAppProvider::GetForTest(browser()->profile()) |
| ->sync_bridge_unsafe() |
| .SetAppUserDisplayModeForTesting(url_info.app_id(), |
| mojom::UserDisplayMode::kBrowser); |
| |
| GURL app_url = url_info.origin().GetURL().Resolve("/index.html"); |
| auto* app_frame = |
| NavigateToURLInNewTab(browser(), app_url, WindowOpenDisposition::UNKNOWN); |
| |
| // The browser shouldn't have opened the app's page. |
| EXPECT_EQ(GetPrimaryMainFrame(browser())->GetLastCommittedURL(), GURL()); |
| |
| // The app's frame should belong to an isolated PWA browser window. |
| Browser* app_browser = GetBrowserFromFrame(app_frame); |
| EXPECT_NE(app_browser, browser()); |
| EXPECT_TRUE( |
| AppBrowserController::IsForWebApp(app_browser, url_info.app_id())); |
| EXPECT_EQ(content::WebExposedIsolationLevel::kIsolatedApplication, |
| app_frame->GetWebExposedIsolationLevel()); |
| } |
| |
| // Tests that the app menu doesn't have an 'Open in Chrome' option. |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppBrowserTest, NoOpenInChrome) { |
| web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp( |
| isolated_web_app_dev_server().GetOrigin()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| Browser* app_browser = GetBrowserFromFrame(app_frame); |
| |
| EXPECT_FALSE( |
| app_browser->command_controller()->IsCommandEnabled(IDC_OPEN_IN_CHROME)); |
| |
| auto app_menu_model = std::make_unique<WebAppMenuModel>( |
| /*provider=*/nullptr, app_browser); |
| app_menu_model->Init(); |
| ui::MenuModel* model = app_menu_model.get(); |
| size_t index = 0; |
| const bool found = app_menu_model->GetModelAndIndexForCommandId( |
| IDC_OPEN_IN_CHROME, &model, &index); |
| EXPECT_TRUE(found); |
| EXPECT_FALSE(model->IsVisibleAt(index)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppBrowserTest, WasmLoadableFromFile) { |
| web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp( |
| isolated_web_app_dev_server().GetOrigin()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| content::EvalJsResult result = EvalJs(app_frame, R"( |
| (async function() { |
| const response = await fetch('empty.wasm'); |
| await WebAssembly.instantiateStreaming(response); |
| return 'loaded'; |
| })(); |
| )"); |
| |
| EXPECT_EQ("loaded", result); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppBrowserTest, WasmLoadableFromBytes) { |
| web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp( |
| isolated_web_app_dev_server().GetOrigin()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| content::EvalJsResult result = EvalJs(app_frame, R"( |
| (async function() { |
| // The smallest possible Wasm module. Just the header (0, "A", "S", "M"), |
| // and the version (0x1). |
| const bytes = new Uint8Array([0, 0x61, 0x73, 0x6d, 0x1, 0, 0, 0]); |
| await WebAssembly.instantiate(bytes); |
| return 'loaded'; |
| })(); |
| )"); |
| |
| EXPECT_EQ("loaded", result); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppBrowserTest, CanNavigateToBlobUrl) { |
| web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp( |
| isolated_web_app_dev_server().GetOrigin()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| content::TestNavigationObserver navigation_observer( |
| content::WebContents::FromRenderFrameHost(app_frame)); |
| EXPECT_TRUE(ExecJs(app_frame, |
| "const blob = new Blob(['test'], {type : 'text/plain'});" |
| "location.href = window.URL.createObjectURL(blob)")); |
| navigation_observer.Wait(); |
| |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| EXPECT_THAT(navigation_observer.last_net_error_code(), Eq(net::OK)); |
| EXPECT_THAT(navigation_observer.last_navigation_url().spec(), |
| StartsWith("blob:" + url_info.origin().GetURL().spec())); |
| } |
| |
| class IsolatedWebAppApiAccessBrowserTest : public IsolatedWebAppBrowserTest { |
| protected: |
| std::unique_ptr<ScopedBundledIsolatedWebApp> CreateAppWithSocketPermission() { |
| return IsolatedWebAppBuilder( |
| ManifestBuilder().AddPermissionsPolicy( |
| blink::mojom::PermissionsPolicyFeature::kDirectSockets, |
| /*self=*/true, {})) |
| .BuildBundle(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_{blink::features::kDirectSockets}; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiAccessBrowserTest, |
| NoApiAccessInDataIframe) { |
| std::unique_ptr<ScopedBundledIsolatedWebApp> app = |
| CreateAppWithSocketPermission(); |
| app->TrustSigningKey(); |
| ASSERT_OK_AND_ASSIGN(auto url_info, app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_THAT(EvalJs(app_frame, "'TCPSocket' in window"), Eq(true)); |
| |
| ASSERT_TRUE(ExecJs(app_frame, R"( |
| const src = '<!DOCTYPE html><p>data: URL</p>'; |
| const url = `data:text/html;base64,${btoa(src)}`; |
| new Promise(resolve => { |
| const f = document.createElement('iframe'); |
| f.src = url; |
| f.addEventListener('load', resolve); |
| document.body.appendChild(f); |
| }); |
| )")); |
| content::RenderFrameHost* iframe = ChildFrameAt(app_frame, 0); |
| ASSERT_THAT(iframe, Ne(nullptr)); |
| |
| EXPECT_THAT( |
| EvalJs(iframe, "location.href"), |
| Eq("data:text/html;base64,PCFET0NUWVBFIGh0bWw+PHA+ZGF0YTogVVJMPC9wPg==")); |
| EXPECT_THAT(EvalJs(iframe, "window.origin"), Eq("null")); |
| EXPECT_THAT(EvalJs(iframe, "window.isSecureContext"), Eq(false)); |
| EXPECT_THAT(EvalJs(iframe, "window.crossOriginIsolated"), Eq(false)); |
| EXPECT_THAT(EvalJs(iframe, "'TCPSocket' in window"), Eq(false)); |
| EXPECT_THAT( |
| iframe->GetLastCommittedURL(), |
| Eq("data:text/html;base64,PCFET0NUWVBFIGh0bWw+PHA+ZGF0YTogVVJMPC9wPg==")); |
| EXPECT_THAT(iframe->GetLastCommittedOrigin().opaque(), Eq(true)); |
| EXPECT_THAT(iframe->GetWebExposedIsolationLevel(), |
| Eq(content::WebExposedIsolationLevel::kNotIsolated)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiAccessBrowserTest, |
| NoApiAccessInSandboxedIframe) { |
| std::unique_ptr<ScopedBundledIsolatedWebApp> app = |
| CreateAppWithSocketPermission(); |
| app->TrustSigningKey(); |
| ASSERT_OK_AND_ASSIGN(auto url_info, app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_THAT(EvalJs(app_frame, "'TCPSocket' in window"), Eq(true)); |
| |
| std::string start_url = url_info.origin().GetURL().spec(); |
| ASSERT_TRUE(ExecJs(app_frame, content::JsReplace(R"( |
| new Promise(resolve => { |
| const f = document.createElement('iframe'); |
| f.src = $1; |
| f.sandbox = 'allow-scripts'; // for EvalJs |
| f.addEventListener('load', resolve); |
| document.body.appendChild(f); |
| }); |
| )", |
| start_url))); |
| content::RenderFrameHost* iframe = ChildFrameAt(app_frame, 0); |
| ASSERT_THAT(iframe, Ne(nullptr)); |
| |
| EXPECT_THAT(EvalJs(iframe, "location.href"), Eq(start_url)); |
| EXPECT_THAT(EvalJs(iframe, "window.origin"), Eq("null")); |
| EXPECT_THAT(EvalJs(iframe, "window.isSecureContext"), Eq(true)); |
| EXPECT_THAT(EvalJs(iframe, "window.crossOriginIsolated"), Eq(false)); |
| EXPECT_THAT(EvalJs(iframe, "'TCPSocket' in window"), Eq(false)); |
| EXPECT_THAT(iframe->GetProcess()->GetWebExposedIsolationLevel(), |
| Eq(content::WebExposedIsolationLevel::kIsolated)); |
| EXPECT_THAT(iframe->GetLastCommittedURL(), Eq(start_url)); |
| EXPECT_THAT(iframe->GetLastCommittedOrigin().opaque(), Eq(true)); |
| EXPECT_THAT(iframe->GetWebExposedIsolationLevel(), |
| Eq(content::WebExposedIsolationLevel::kNotIsolated)); |
| } |
| |
| class IsolatedWebAppBrowserCookieTest : public IsolatedWebAppBrowserTest { |
| public: |
| using CookieHeaders = std::vector<std::string>; |
| |
| void SetUpOnMainThread() override { |
| https_server()->RegisterRequestMonitor( |
| base::BindRepeating(&IsolatedWebAppBrowserCookieTest::MonitorRequest, |
| base::Unretained(this))); |
| |
| base::FilePath isolated_web_app_dev_server_root = |
| GetChromeTestDataDir().Append( |
| FILE_PATH_LITERAL("web_apps/simple_isolated_app")); |
| isolated_web_app_dev_server_ = std::make_unique<net::EmbeddedTestServer>(); |
| isolated_web_app_dev_server_->AddDefaultHandlers( |
| isolated_web_app_dev_server_root); |
| isolated_web_app_dev_server_->RegisterRequestMonitor( |
| base::BindRepeating(&IsolatedWebAppBrowserCookieTest::MonitorRequest, |
| base::Unretained(this))); |
| CHECK(isolated_web_app_dev_server_->Start()); |
| |
| IsolatedWebAppBrowserTest::SetUpOnMainThread(); |
| } |
| |
| void MonitorRequest(const net::test_server::HttpRequest& request) { |
| // Replace the host in |request.GetURL()| with the value from the Host |
| // header, as GetURL()'s host will be 127.0.0.1. |
| std::string host = GURL("https://" + GetHeader(request, "Host")).host(); |
| GURL::Replacements replace_host; |
| replace_host.SetHostStr(host); |
| GURL url = request.GetURL().ReplaceComponents(replace_host); |
| cookie_map_[url.spec()].push_back(GetHeader(request, "cookie")); |
| } |
| |
| protected: |
| // Returns the "Cookie" headers that were received for the given URL. |
| const CookieHeaders& GetCookieHeadersForUrl(const GURL& url) { |
| return cookie_map_[url.spec()]; |
| } |
| |
| const net::EmbeddedTestServer& isolated_web_app_dev_server() { |
| return *isolated_web_app_dev_server_.get(); |
| } |
| |
| private: |
| std::string GetHeader(const net::test_server::HttpRequest& request, |
| const std::string& header_name) { |
| auto header = request.headers.find(header_name); |
| return header != request.headers.end() ? header->second : ""; |
| } |
| |
| // Maps GURLs to a vector of cookie strings. The nth item in the vector will |
| // contain the contents of the "Cookies" header for the nth request to the |
| // given GURL. |
| std::unordered_map<std::string, CookieHeaders> cookie_map_; |
| std::unique_ptr<net::EmbeddedTestServer> isolated_web_app_dev_server_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppBrowserCookieTest, Cookies) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| InstallDevModeProxyIsolatedWebApp(url::Origin::Create( |
| isolated_web_app_dev_server().GetURL("localhost", "/"))); |
| |
| GURL app_url = url_info.origin().GetURL().Resolve("/cookie.html"); |
| GURL app_proxy_url = |
| isolated_web_app_dev_server().GetURL("localhost", "/cookie.html"); |
| GURL non_app_url = https_server()->GetURL( |
| kNonAppHost, "/web_apps/simple_isolated_app/cookie.html"); |
| CookieSettingsFactory::GetForProfile(browser()->profile()) |
| ->SetCookieSetting(non_app_url, CONTENT_SETTING_ALLOW); |
| |
| // Load a page that sets a cookie, then create a cross-origin iframe that |
| // loads the same page. |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| Browser* app_browser = GetBrowserFromFrame(app_frame); |
| app_frame = ui_test_utils::NavigateToURL(app_browser, app_url); |
| web_app::CreateIframe(app_frame, "child", non_app_url, ""); |
| |
| const auto& app_cookies = GetCookieHeadersForUrl(app_proxy_url); |
| EXPECT_EQ(1u, app_cookies.size()); |
| EXPECT_TRUE(app_cookies[0].empty()); |
| const auto& non_app_cookies = GetCookieHeadersForUrl(non_app_url); |
| EXPECT_EQ(1u, non_app_cookies.size()); |
| EXPECT_TRUE(non_app_cookies[0].empty()); |
| |
| // Load the pages again. The non-app page should send the cookie, but the |
| // app won't because the proxy disables cookies (CredentialsMode::kOmit). |
| content::RenderFrameHost* app_frame2 = OpenApp(url_info.app_id()); |
| Browser* app_browser2 = GetBrowserFromFrame(app_frame2); |
| app_frame2 = ui_test_utils::NavigateToURL(app_browser2, app_url); |
| web_app::CreateIframe(app_frame2, "child", non_app_url, ""); |
| |
| EXPECT_EQ(2u, app_cookies.size()); |
| EXPECT_TRUE(app_cookies[1].empty()); |
| EXPECT_EQ(2u, non_app_cookies.size()); |
| EXPECT_EQ("foo=bar", non_app_cookies[1]); |
| |
| // Load the cross-origin's iframe as a top-level page. Because this page was |
| // previously loaded in an isolated app, it shouldn't have cookies set when |
| // loaded in a main frame here. |
| ASSERT_TRUE(NavigateToURLInNewTab(browser(), non_app_url)); |
| |
| EXPECT_EQ(3u, non_app_cookies.size()); |
| EXPECT_TRUE(non_app_cookies[2].empty()); |
| } |
| |
| class IsolatedWebAppBrowserServiceWorkerTest |
| : public IsolatedWebAppBrowserTest { |
| protected: |
| int64_t InstallIsolatedWebAppAndWaitForServiceWorker() { |
| web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp( |
| isolated_web_app_dev_server().GetOrigin()); |
| app_url_ = url_info.origin().GetURL(); |
| |
| content::RenderFrameHost* original_frame = OpenApp(url_info.app_id()); |
| CHECK_NE(default_storage_partition(), |
| original_frame->GetStoragePartition()); |
| |
| app_web_contents_ = |
| content::WebContents::FromRenderFrameHost(original_frame); |
| app_window_ = GetBrowserFromFrame(original_frame); |
| |
| GURL register_service_worker_page = |
| app_url_.Resolve("register_service_worker.html"); |
| |
| app_frame_ = |
| ui_test_utils::NavigateToURL(app_window_, register_service_worker_page); |
| storage_partition_ = app_frame_->GetStoragePartition(); |
| CHECK_NE(default_storage_partition(), storage_partition_); |
| |
| ServiceWorkerVersionActivatedWaiter version_activated_waiter( |
| storage_partition_, app_url_); |
| |
| return version_activated_waiter.AwaitVersionActivated(); |
| } |
| |
| const GURL& app_url() const { return app_url_; } |
| |
| raw_ptr<Browser, AcrossTasksDanglingUntriaged> app_window_ = nullptr; |
| raw_ptr<content::WebContents, AcrossTasksDanglingUntriaged> |
| app_web_contents_ = nullptr; |
| raw_ptr<content::RenderFrameHost, AcrossTasksDanglingUntriaged> app_frame_ = |
| nullptr; |
| raw_ptr<content::StoragePartition, AcrossTasksDanglingUntriaged> |
| storage_partition_ = nullptr; |
| GURL app_url_; |
| |
| std::unique_ptr<net::EmbeddedTestServer> isolated_web_app_dev_server_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppBrowserServiceWorkerTest, |
| ServiceWorkerPartitioned) { |
| InstallIsolatedWebAppAndWaitForServiceWorker(); |
| test::CheckServiceWorkerStatus( |
| app_url(), storage_partition_, |
| content::ServiceWorkerCapability::SERVICE_WORKER_WITH_FETCH_HANDLER); |
| } |
| |
| class IsolatedWebAppBrowserServiceWorkerPushTest |
| : public IsolatedWebAppBrowserServiceWorkerTest { |
| public: |
| IsolatedWebAppBrowserServiceWorkerPushTest() |
| : scoped_testing_factory_installer_( |
| base::BindRepeating(&gcm::FakeGCMProfileService::Build)) {} |
| |
| protected: |
| void SetUpOnMainThread() override { |
| IsolatedWebAppBrowserServiceWorkerTest::SetUpOnMainThread(); |
| |
| notification_tester_ = std::make_unique<NotificationDisplayServiceTester>( |
| browser()->profile()); |
| } |
| |
| void SendMessageAndWaitUntilHandled( |
| content::BrowserContext* context, |
| const PushMessagingAppIdentifier& app_identifier, |
| const gcm::IncomingMessage& message) { |
| PushMessagingServiceImpl* push_service = |
| PushMessagingServiceFactory::GetForProfile(context); |
| |
| CHECK_EQ(push_service->GetPermissionStatus(app_url(), |
| /*user_visible=*/true), |
| blink::mojom::PermissionStatus::GRANTED); |
| |
| // If there is not enough budget, a generic notification will be displayed |
| // saying: "This site has been updated in the background.". In order to |
| // avoid flakiness, we give the URL the maximum value of EngagementPoints so |
| // it will not display the generic notification. |
| site_engagement::SiteEngagementService* service = |
| site_engagement::SiteEngagementService::Get(profile()); |
| service->ResetBaseScoreForURL(app_url(), service->GetMaxPoints()); |
| CHECK(service->GetMaxPoints() == service->GetScore(app_url())); |
| |
| base::RunLoop run_loop; |
| base::RepeatingClosure quit_barrier = |
| base::BarrierClosure(/*num_closures=*/2, run_loop.QuitClosure()); |
| push_service->SetMessageCallbackForTesting(quit_barrier); |
| notification_tester_->SetNotificationAddedClosure(quit_barrier); |
| push_service->OnMessage(app_identifier.app_id(), message); |
| run_loop.Run(); |
| } |
| |
| PushMessagingAppIdentifier GetAppIdentifierForServiceWorkerRegistration( |
| int64_t service_worker_registration_id) { |
| PushMessagingAppIdentifier app_identifier = |
| PushMessagingAppIdentifier::FindByServiceWorker( |
| browser()->profile(), app_url(), service_worker_registration_id); |
| return app_identifier; |
| } |
| |
| std::unique_ptr<NotificationDisplayServiceTester> notification_tester_; |
| |
| private: |
| gcm::GCMProfileServiceFactory::ScopedTestingFactoryInstaller |
| scoped_testing_factory_installer_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F( |
| IsolatedWebAppBrowserServiceWorkerPushTest, |
| ServiceWorkerPartitionedWhenWakingUpDueToPushNotification) { |
| int64_t service_worker_version_id = |
| InstallIsolatedWebAppAndWaitForServiceWorker(); |
| |
| // Request and confirm permission to show notifications. |
| auto* permission_request_manager = |
| permissions::PermissionRequestManager::FromWebContents(app_web_contents_); |
| permission_request_manager->set_auto_response_for_test( |
| permissions::PermissionRequestManager::ACCEPT_ALL); |
| |
| ASSERT_EQ("permission status - granted", content::EvalJs(app_frame_, R"js( |
| (async () => { |
| return 'permission status - ' + await Notification.requestPermission(); |
| })(); |
| )js")); |
| |
| // Subscribe to push notifications and retrieve the app identifier. |
| std::string push_messaging_endpoint = content::EvalJs(app_frame_, R"js( |
| // NIST P-256 public key made available to tests. Must be an uncompressed |
| // point in accordance with SEC1 2.3.3. |
| var kApplicationServerKey = new Uint8Array([ |
| 0x04, 0x55, 0x52, 0x6A, 0xA5, 0x6E, 0x8E, 0xAA, 0x47, 0x97, 0x36, 0x10, 0xC1, |
| 0x66, 0x3C, 0x1E, 0x65, 0xBF, 0xA1, 0x7B, 0xEE, 0x48, 0xC9, 0xC6, 0xBB, 0xBF, |
| 0x02, 0x18, 0x53, 0x72, 0x1D, 0x0C, 0x7B, 0xA9, 0xE3, 0x11, 0xB7, 0x03, 0x52, |
| 0x21, 0xD3, 0x71, 0x90, 0x13, 0xA8, 0xC1, 0xCF, 0xED, 0x20, 0xF7, 0x1F, 0xD1, |
| 0x7F, 0xF2, 0x76, 0xB6, 0x01, 0x20, 0xD8, 0x35, 0xA5, 0xD9, 0x3C, 0x43, 0xFD |
| ]); |
| |
| (async () => { |
| const registration = await navigator.serviceWorker.ready; |
| const subscription = await registration.pushManager.subscribe({ |
| userVisibleOnly: true, |
| applicationServerKey: kApplicationServerKey.buffer, |
| }); |
| return subscription.endpoint; |
| })(); |
| )js") |
| .ExtractString(); |
| |
| size_t last_slash = push_messaging_endpoint.rfind('/'); |
| ASSERT_EQ(kPushMessagingGcmEndpoint, |
| push_messaging_endpoint.substr(0, last_slash + 1)); |
| PushMessagingAppIdentifier app_identifier = |
| GetAppIdentifierForServiceWorkerRegistration(0LL); |
| EXPECT_FALSE(app_identifier.is_null()); |
| |
| // Close the browser and stop the ServiceWorker |
| ServiceWorkerVersionStoppedRunningWaiter version_stopped_waiter( |
| storage_partition_, service_worker_version_id); |
| CloseBrowserSynchronously(app_window_); |
| base::RunLoop run_loop; |
| storage_partition_->GetServiceWorkerContext()->StopAllServiceWorkers( |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| version_stopped_waiter.AwaitVersionStoppedRunning(); |
| |
| // Push a message to the ServiceWorker and make sure the service worker is |
| // started again. |
| ServiceWorkerVersionStartedRunningWaiter version_started_waiter( |
| storage_partition_, service_worker_version_id); |
| |
| gcm::IncomingMessage message; |
| message.sender_id = GetTestApplicationServerKey(); |
| message.raw_data = "test"; |
| message.decrypted = true; |
| SendMessageAndWaitUntilHandled(browser()->profile(), app_identifier, message); |
| |
| version_started_waiter.AwaitVersionStartedRunning(); |
| |
| // Verify that the ServiceWorker has received the push message and created |
| // a push notification, then click on it. |
| auto notifications = notification_tester_->GetDisplayedNotificationsForType( |
| NotificationHandler::Type::WEB_PERSISTENT); |
| EXPECT_EQ(notifications.size(), 1UL); |
| |
| BrowserWaiter browser_waiter(nullptr); |
| notification_tester_->SimulateClick(NotificationHandler::Type::WEB_PERSISTENT, |
| notifications[0].id(), std::nullopt, |
| std::nullopt); |
| |
| // Check that the click resulted in a new isolated web app window that runs in |
| // the same isolated non-default storage partition. |
| auto* new_app_window = browser_waiter.AwaitAdded(); |
| auto* new_app_frame = new_app_window->tab_strip_model() |
| ->GetActiveWebContents() |
| ->GetPrimaryMainFrame(); |
| auto* new_storage_partition = new_app_frame->GetStoragePartition(); |
| EXPECT_EQ(new_storage_partition, storage_partition_); |
| EXPECT_EQ(new_app_frame->GetWebExposedIsolationLevel(), |
| content::WebExposedIsolationLevel::kIsolatedApplication); |
| EXPECT_TRUE(AppBrowserController::IsWebApp(new_app_window)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppBrowserTest, SharedWorker) { |
| std::string register_worker_js = R"( |
| const policy = trustedTypes.createPolicy('default', { |
| createScriptURL: (url) => url, |
| }); |
| const worker = new SharedWorker( |
| policy.createScriptURL('/shared_worker.js')); |
| |
| let listener = null; |
| worker.port.addEventListener('message', (e) => { |
| listener(e.data); |
| listener = null; |
| }); |
| worker.port.start(); |
| |
| function sendMessage(body) { |
| if (listener !== null) { |
| return Promise.reject('Already have pending request'); |
| } |
| return new Promise((resolve) => { |
| listener = resolve; |
| worker.port.postMessage(body); |
| }); |
| } |
| )"; |
| |
| web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp( |
| isolated_web_app_dev_server().GetOrigin()); |
| content::RenderFrameHost* app_frame1 = OpenApp(url_info.app_id()); |
| ASSERT_TRUE(ExecJs(app_frame1, register_worker_js)); |
| |
| EXPECT_EQ("none", EvalJs(app_frame1, "sendMessage('hello')")); |
| EXPECT_EQ("hello", EvalJs(app_frame1, "sendMessage('world')")); |
| |
| // Open a second window and make sure it uses the same worker instance. |
| content::RenderFrameHost* app_frame2 = OpenApp(url_info.app_id()); |
| ASSERT_TRUE(ExecJs(app_frame2, register_worker_js)); |
| |
| EXPECT_EQ("world", EvalJs(app_frame2, "sendMessage('frame2!')")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppBrowserTest, DedicatedWorker) { |
| std::string register_worker_js = R"( |
| const policy = trustedTypes.createPolicy('default', { |
| createScriptURL: (url) => url, |
| }); |
| const worker = new Worker(policy.createScriptURL('/dedicated_worker.js')); |
| |
| let listener = null; |
| worker.addEventListener('message', (e) => { |
| listener(e.data); |
| listener = null; |
| }); |
| |
| function sendMessage(body) { |
| if (listener !== null) { |
| return Promise.reject('Already have pending request'); |
| } |
| return new Promise((resolve) => { |
| listener = resolve; |
| worker.postMessage(body); |
| }); |
| } |
| )"; |
| |
| web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp( |
| isolated_web_app_dev_server().GetOrigin()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| ASSERT_TRUE(ExecJs(app_frame, register_worker_js)); |
| |
| EXPECT_EQ("none", EvalJs(app_frame, "sendMessage('hello')")); |
| EXPECT_EQ("hello", EvalJs(app_frame, "sendMessage('world')")); |
| } |
| |
| struct ExtensionTestParam { |
| std::string test_name; |
| bool should_succeed; |
| // The value to set in the extension's manifest as |
| // `externally_connectable.matches[0]`. `${IWA_ORIGIN}` will be replaced by |
| // the IWA's origin without a trailing slash. |
| std::string externally_connectable_match; |
| }; |
| |
| class IsolatedWebAppExtensionBrowserTest |
| : public IsolatedWebAppBrowserTest, |
| public ::testing::WithParamInterface<ExtensionTestParam> { |
| protected: |
| void SetUp() override { |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| IsolatedWebAppBrowserTest::SetUp(); |
| } |
| |
| bool IsChromeRuntimeDefined(content::RenderFrameHost* app_frame) { |
| return EvalJs(app_frame, "chrome.runtime !== undefined").ExtractBool(); |
| } |
| |
| std::string GetMatch(const web_app::IsolatedWebAppUrlInfo& url_info) { |
| std::string origin = url_info.origin().GetURL().spec(); |
| std::string match = GetParam().externally_connectable_match; |
| base::ReplaceSubstringsAfterOffset( |
| &match, /*start_offset=*/0, "${IWA_ORIGIN}", |
| base::TrimString(origin, "/", base::TRIM_TRAILING)); |
| return match; |
| } |
| |
| base::ScopedTempDir temp_dir_; |
| |
| static constexpr std::string_view kExtensionManifest = R"({ |
| "name": "foo", |
| "description": "foo", |
| "version": "0.1", |
| "manifest_version": 3, |
| "externally_connectable": { |
| "matches": [ $1 ] |
| }, |
| "background": {"service_worker": "service_worker_background.js"} |
| })"; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(IsolatedWebAppExtensionBrowserTest, |
| SendMessageToExtension) { |
| web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp( |
| isolated_web_app_dev_server().GetOrigin()); |
| |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::WriteFile(temp_dir_.GetPath().AppendASCII("manifest.json"), |
| content::JsReplace(kExtensionManifest, GetMatch(url_info))); |
| // Extension: Listen for pings from the IWA. |
| base::WriteFile( |
| temp_dir_.GetPath().AppendASCII("service_worker_background.js"), |
| R"( |
| chrome.runtime.onMessageExternal.addListener( |
| (request, sender, sendResponse) => { |
| chrome.test.assertEq('iwa->extension: ping', request); |
| sendResponse('extension->iwa: pong'); |
| chrome.test.notifyPass(); |
| }); |
| )"); |
| } |
| |
| extensions::ResultCatcher result_catcher; |
| extensions::ChromeTestExtensionLoader loader(profile()); |
| scoped_refptr<const extensions::Extension> extension = |
| loader.LoadExtension(temp_dir_.GetPath()); |
| ASSERT_TRUE(extension); |
| |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| if (!GetParam().should_succeed) { |
| ASSERT_FALSE(IsChromeRuntimeDefined(app_frame)); |
| return; |
| } |
| ASSERT_TRUE(IsChromeRuntimeDefined(app_frame)); |
| |
| // IWA: Send a ping to the extension and wait for the pong. |
| constexpr std::string_view kSendPing = R"( |
| chrome.runtime.sendMessage($1, "iwa->extension: ping"); |
| )"; |
| EXPECT_EQ(EvalJs(app_frame, content::JsReplace(kSendPing, extension->id())), |
| "extension->iwa: pong"); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IsolatedWebAppExtensionBrowserTest, ConnectToExtension) { |
| web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp( |
| isolated_web_app_dev_server().GetOrigin()); |
| |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::WriteFile(temp_dir_.GetPath().AppendASCII("manifest.json"), |
| content::JsReplace(kExtensionManifest, GetMatch(url_info))); |
| // Extension: Listen for pings from the IWA. |
| base::WriteFile( |
| temp_dir_.GetPath().AppendASCII("service_worker_background.js"), |
| R"( |
| chrome.runtime.onConnectExternal.addListener( |
| (port) => |
| port.onMessage.addListener((message) => { |
| chrome.test.assertEq('iwa->extension: ping', message); |
| port.postMessage('extension->iwa: pong'); |
| chrome.test.notifyPass(); |
| })); |
| )"); |
| } |
| |
| extensions::ResultCatcher result_catcher; |
| extensions::ChromeTestExtensionLoader loader(profile()); |
| scoped_refptr<const extensions::Extension> extension = |
| loader.LoadExtension(temp_dir_.GetPath()); |
| |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| if (!GetParam().should_succeed) { |
| ASSERT_FALSE(IsChromeRuntimeDefined(app_frame)); |
| return; |
| } |
| ASSERT_TRUE(IsChromeRuntimeDefined(app_frame)); |
| |
| // IWA: Send a ping to the extension and wait for the pong. |
| constexpr std::string_view kSendPing = R"( |
| new Promise((resolve, reject) => { |
| const port = chrome.runtime.connect($1); |
| port.onMessage.addListener((response) => resolve(response)); |
| port.onDisconnect.addListener(() => reject()); |
| port.postMessage("iwa->extension: ping"); |
| }); |
| )"; |
| EXPECT_EQ(EvalJs(app_frame, content::JsReplace(kSendPing, extension->id())), |
| "extension->iwa: pong"); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /* no prefix*/, |
| IsolatedWebAppExtensionBrowserTest, |
| ::testing::Values( |
| ExtensionTestParam{ |
| .test_name = "origin_with_start_url", |
| .should_succeed = true, |
| // /index.html is the IWA's start_url which is opened in the test. |
| .externally_connectable_match = {"${IWA_ORIGIN}/index.html"}}, |
| ExtensionTestParam{ |
| .test_name = "origin_with_other_path", |
| .should_succeed = false, |
| .externally_connectable_match = {"${IWA_ORIGIN}/foo"}}, |
| ExtensionTestParam{.test_name = "origin_with_star", |
| .should_succeed = true, |
| .externally_connectable_match = {"${IWA_ORIGIN}/*"}}, |
| ExtensionTestParam{.test_name = "all_urls", |
| .should_succeed = true, |
| .externally_connectable_match = {"<all_urls>"}}, |
| ExtensionTestParam{ |
| .test_name = "wildcard_all_iwas", |
| .should_succeed = true, |
| .externally_connectable_match = {"isolated-app://*/*"}}, |
| ExtensionTestParam{ |
| .test_name = "non_matching_url", |
| .should_succeed = false, |
| .externally_connectable_match = {"https://example.com/"}}), |
| [](const ::testing::TestParamInfo<ExtensionTestParam>& info) { |
| return info.param.test_name; |
| }); |
| |
| } // namespace web_app |