| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/web_applications/app_browser_controller.h" |
| #include "chrome/browser/ui/web_applications/web_app_browser_controller.h" |
| #include "chrome/browser/ui/web_applications/web_app_browsertest_base.h" |
| #include "chrome/browser/ui/web_applications/web_app_dialog_utils.h" |
| #include "chrome/browser/ui/web_applications/web_app_dialogs.h" |
| #include "chrome/browser/web_applications/web_app_helpers.h" |
| #include "chrome/browser/web_applications/web_app_provider.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/permissions/permission_request_manager.h" |
| #include "components/webapps/browser/install_result_code.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/features_generated.h" |
| |
| namespace { |
| constexpr char kInstallElementId[] = "install-app"; |
| } // namespace |
| |
| namespace web_app { |
| |
| class InstallElementBrowserTest : public WebAppBrowserTestBase { |
| public: |
| InstallElementBrowserTest() { |
| scoped_feature_list_.InitWithFeatures( |
| {blink::features::kInstallElement, |
| blink::features::kBypassPepcSecurityForTesting}, |
| {}); |
| } |
| |
| void SetUpOnMainThread() override { |
| WebAppBrowserTestBase::SetUpOnMainThread(); |
| console_observer_ = |
| std::make_unique<content::WebContentsConsoleObserver>(web_contents()); |
| } |
| |
| content::WebContents* web_contents() { |
| return browser()->tab_strip_model()->GetActiveWebContents(); |
| } |
| |
| bool NavigateToInstallElementPage( |
| const std::string& path = "/web_apps/install_element/index.html") { |
| VLOG(0) << https_server()->GetURL(path).spec(); |
| // We must await installability checks to avoid race conditions with our |
| // WebInstallFromUrlCommand which also fetches the manifest. |
| // TODO(crbug.com/468047211): Change to ui_test_utils::NavigateToURL after |
| // WebInstallServiceImpl stops using InstallableManager. |
| return NavigateAndAwaitInstallabilityCheck(browser(), |
| https_server()->GetURL(path)); |
| } |
| |
| void BlockWebInstallPermission(const GURL& url) { |
| HostContentSettingsMap* settings_map = |
| HostContentSettingsMapFactory::GetForProfile(profile()); |
| settings_map->SetContentSettingDefaultScope( |
| url, GURL(), ContentSettingsType::WEB_APP_INSTALLATION, |
| CONTENT_SETTING_BLOCK); |
| } |
| |
| bool SetButtonInstallUrl(const GURL& install_url) { |
| const std::string script = |
| "document.getElementById('" + std::string(kInstallElementId) + |
| "').setAttribute('installurl', '" + install_url.spec() + "');"; |
| return content::ExecJs(web_contents(), script); |
| } |
| |
| bool SetButtonManifestId(const GURL& manifest_id) { |
| const std::string script = |
| "document.getElementById('" + std::string(kInstallElementId) + |
| "').setAttribute('manifestid', '" + manifest_id.spec() + "');"; |
| return content::ExecJs(web_contents(), script); |
| } |
| |
| // Simulates a click on an element with the given |id|. |
| bool ClickElementWithId(const std::string& id, |
| content::WebContents* contents = nullptr) { |
| const std::string script = "document.getElementById('" + id + "').click();"; |
| return content::ExecJs(contents ? contents : web_contents(), script); |
| } |
| |
| void WaitForPromptActionEvent(const std::string& id) { |
| ExpectConsoleMessage(id + "-promptaction"); |
| } |
| |
| void WaitForDismissEvent(const std::string& id) { |
| ExpectConsoleMessage(id + "-promptdismiss"); |
| } |
| |
| // The web app test pages log quite a few additional console messages during |
| // setup/load. Make sure that the set of received messages contains at least 1 |
| // instance of `expected_message`. |
| void ExpectConsoleMessage(const std::string& expected_message) { |
| EXPECT_TRUE(console_observer_->Wait()); |
| EXPECT_GE(console_observer_->messages().size(), 1u); |
| |
| bool found = false; |
| for (const auto& message : console_observer_->messages()) { |
| if (base::UTF16ToUTF8(message.message) == expected_message) { |
| found = true; |
| break; |
| } |
| } |
| EXPECT_TRUE(found) << "Expected console message not found: " |
| << expected_message; |
| |
| // Reset console observer to wait for next message. |
| console_observer_ = |
| std::make_unique<content::WebContentsConsoleObserver>(web_contents()); |
| } |
| |
| protected: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| std::unique_ptr<content::WebContentsConsoleObserver> console_observer_; |
| }; |
| |
| // Test installing current document (no attributes). |
| // <install></install> |
| IN_PROC_BROWSER_TEST_F(InstallElementBrowserTest, Install) { |
| // TODO(crbug.com/469831343): Fix race condition with <install></install>. |
| // Replace this with a basic `NavigateToURL`. |
| // Navigate to a page with <install> elements and wait for installability |
| // checks to complete. |
| EXPECT_TRUE(NavigateAndAwaitInstallabilityCheck( |
| browser(), https_server()->GetURL("/web_apps/install_element/" |
| "index.html"))); |
| |
| // Setup test listeners and dialog auto-accepts. |
| auto auto_accept_pwa_install_confirmation = |
| SetAutoAcceptPWAInstallConfirmationForTesting(); |
| |
| // Click the install element and wait for the app to open. |
| ui_test_utils::BrowserCreatedObserver browser_created_observer; |
| ClickElementWithId(kInstallElementId); |
| Browser* web_app_browser = browser_created_observer.Wait(); |
| |
| // Verify promptaction event was fired. |
| WaitForPromptActionEvent(kInstallElementId); |
| |
| // Verify the app launched. |
| ASSERT_TRUE(AppBrowserController::IsWebApp(web_app_browser)); |
| const WebAppBrowserController* app_controller = |
| WebAppBrowserController::From(web_app_browser); |
| EXPECT_EQ(app_controller->GetTitle(), |
| u"Web app install element test app with id"); |
| |
| // The registrar should now have one app installed. |
| EXPECT_EQ(provider().registrar_unsafe().GetAppIds().size(), 1u); |
| } |
| |
| // Test installing from a background document (installurl only). |
| // <install installurl="..."></install> |
| IN_PROC_BROWSER_TEST_F(InstallElementBrowserTest, InstallWithUrl) { |
| // Navigate to a page with <install> elements. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), |
| https_server()->GetURL("/web_apps/install_element/index.html"))); |
| |
| // Setup test listeners and dialog auto-accepts. |
| auto auto_accept_pwa_install_confirmation = |
| SetAutoAcceptPWAInstallConfirmationForTesting(); |
| |
| // Dynamically set the installurl attribute. |
| // Since we're installing by URL only, the manifest must contain an id. |
| const GURL install_url = |
| https_server()->GetURL("/web_apps/custom_id/install_url.html"); |
| ASSERT_TRUE(SetButtonInstallUrl(install_url)); |
| |
| // Click the install element and wait for the app to open. |
| ui_test_utils::BrowserCreatedObserver browser_created_observer; |
| ClickElementWithId(kInstallElementId); |
| Browser* web_app_browser = browser_created_observer.Wait(); |
| |
| // Verify promptaction event was fired. |
| WaitForPromptActionEvent(kInstallElementId); |
| |
| // Verify the app launched. |
| ASSERT_TRUE(AppBrowserController::IsWebApp(web_app_browser)); |
| const WebAppBrowserController* app_controller = |
| WebAppBrowserController::From(web_app_browser); |
| EXPECT_EQ(app_controller->GetTitle(), u"Simple web app with a custom id"); |
| |
| // The registrar should now have one app installed. |
| EXPECT_EQ(provider().registrar_unsafe().GetAppIds().size(), 1u); |
| } |
| |
| // Test installing from a background document (both installurl and manifestid). |
| // <install installurl="..." manifestid="..."></install> |
| IN_PROC_BROWSER_TEST_F(InstallElementBrowserTest, InstallWithUrlAndId) { |
| // Navigate to a page with <install> elements. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), |
| https_server()->GetURL("/web_apps/install_element/index.html"))); |
| |
| // Setup test listeners and dialog auto-accepts. |
| auto auto_accept_pwa_install_confirmation = |
| SetAutoAcceptPWAInstallConfirmationForTesting(); |
| |
| // Dynamically set the installurl and manifestid attributes. |
| const GURL install_url = |
| https_server()->GetURL("/web_apps/install_url/install_url.html"); |
| ASSERT_TRUE(SetButtonInstallUrl(install_url)); |
| const GURL manifest_id = |
| https_server()->GetURL("/web_apps/install_url/index.html"); |
| ASSERT_TRUE(SetButtonManifestId(manifest_id)); |
| |
| // Click the install element and wait for the app to open. |
| ui_test_utils::BrowserCreatedObserver browser_created_observer; |
| ClickElementWithId(kInstallElementId); |
| Browser* web_app_browser = browser_created_observer.Wait(); |
| |
| // Verify promptaction event was fired. |
| WaitForPromptActionEvent(kInstallElementId); |
| |
| // Verify the app launched. |
| ASSERT_TRUE(AppBrowserController::IsWebApp(web_app_browser)); |
| const WebAppBrowserController* app_controller = |
| WebAppBrowserController::From(web_app_browser); |
| EXPECT_EQ(app_controller->GetTitle(), u"Simple web app"); |
| |
| // The registrar should now have one app installed. |
| EXPECT_EQ(provider().registrar_unsafe().GetAppIds().size(), 1u); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(InstallElementBrowserTest, InstallWithUrl_UserDenies) { |
| // Navigate to a page with <install> elements. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), |
| https_server()->GetURL("/web_apps/install_element/index.html"))); |
| |
| // Simulate the user declining the install prompt. |
| auto auto_decline_pwa_install_confirmation = |
| SetAutoDeclinePWAInstallConfirmationForTesting(); |
| |
| // Dynamically set the installurl attribute. |
| // Since we're installing by URL only, the manifest must contain an id. |
| const GURL install_url = |
| https_server()->GetURL("/web_apps/custom_id/install_url.html"); |
| ASSERT_TRUE(SetButtonInstallUrl(install_url)); |
| |
| // Click the install element. |
| ClickElementWithId(kInstallElementId); |
| |
| // Verify promptdismiss event was fired. |
| WaitForDismissEvent(kInstallElementId); |
| |
| // The registrar should still have zero apps installed. |
| EXPECT_EQ(provider().registrar_unsafe().GetAppIds().size(), 0u); |
| } |
| |
| // Test that current document install succeeds even when permission is denied, |
| // since current document installs bypass permission. |
| IN_PROC_BROWSER_TEST_F(InstallElementBrowserTest, Install_DenyPermission) { |
| // TODO(crbug.com/469831343): Fix race condition with <install></install>. |
| // Replace this with a basic `NavigateToURL`. |
| // Navigate to a page with <install> elements and wait for installability |
| // checks to complete. |
| const GURL current_document_url = https_server()->GetURL( |
| "/web_apps/install_element/" |
| "index.html"); |
| EXPECT_TRUE( |
| NavigateAndAwaitInstallabilityCheck(browser(), current_document_url)); |
| auto auto_accept_pwa_install_confirmation = |
| SetAutoAcceptPWAInstallConfirmationForTesting(); |
| |
| // Block the web install permission for the current document origin. |
| BlockWebInstallPermission(current_document_url); |
| |
| // Click the install element and wait for the app to open. |
| ui_test_utils::BrowserCreatedObserver browser_created_observer; |
| ClickElementWithId(kInstallElementId); |
| Browser* web_app_browser = browser_created_observer.Wait(); |
| |
| // Verify promptaction event was fired. |
| WaitForPromptActionEvent(kInstallElementId); |
| |
| // Verify the app launched. |
| ASSERT_TRUE(AppBrowserController::IsWebApp(web_app_browser)); |
| const WebAppBrowserController* app_controller = |
| WebAppBrowserController::From(web_app_browser); |
| EXPECT_EQ(app_controller->GetTitle(), |
| u"Web app install element test app with id"); |
| |
| // The registrar should now have one app installed. |
| EXPECT_EQ(provider().registrar_unsafe().GetAppIds().size(), 1u); |
| } |
| |
| // Test that when permission is denied for background document install, no |
| // install occurs. |
| IN_PROC_BROWSER_TEST_F(InstallElementBrowserTest, |
| InstallWithUrl_DenyPermission) { |
| // Navigate to a page with <install> elements. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), |
| https_server()->GetURL("/web_apps/install_element/index.html"))); |
| |
| // Dynamically set the installurl attribute to a background document URL. |
| const GURL install_url = |
| https_server()->GetURL("/web_apps/custom_id/install_url.html"); |
| ASSERT_TRUE(SetButtonInstallUrl(install_url)); |
| |
| // Block the web install permission for this origin. |
| BlockWebInstallPermission(install_url); |
| |
| // Click the install element. |
| ClickElementWithId(kInstallElementId); |
| |
| // When permission is already denied, promptdismiss should fire without |
| // showing an actual prompt. |
| WaitForDismissEvent(kInstallElementId); |
| |
| // Verify that no app was installed by checking the app count hasn't changed. |
| EXPECT_EQ(provider().registrar_unsafe().GetAppIds().size(), 0u); |
| } |
| |
| } // namespace web_app |