blob: f6c64280850d4398ccaa39a290117300b96495f1 [file] [log] [blame]
// Copyright 2020 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 "chrome/browser/web_applications/external_web_app_manager.h"
#include "base/files/file_path.h"
#include "base/json/json_reader.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "build/branding_buildflags.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/components/external_app_install_features.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/browser/web_applications/components/web_app_id_constants.h"
#include "chrome/browser/web_applications/preinstalled_web_apps/preinstalled_web_apps.h"
#include "chrome/browser/web_applications/test/test_file_utils.h"
#include "chrome/browser/web_applications/test/test_os_integration_manager.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_launcher.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
namespace web_app {
class ExternalWebAppManagerBrowserTest
: public extensions::ExtensionBrowserTest {
public:
ExternalWebAppManagerBrowserTest() {
ExternalWebAppManager::SkipStartupForTesting();
WebAppProvider::SetOsIntegrationManagerFactoryForTesting(
[](Profile* profile) -> std::unique_ptr<OsIntegrationManager> {
return std::make_unique<TestOsIntegrationManager>(profile, nullptr,
nullptr, nullptr);
});
}
void SetUpOnMainThread() override {
ExtensionBrowserTest::SetUpOnMainThread();
}
GURL GetAppUrl() const {
return embedded_test_server()->GetURL("/web_apps/basic.html");
}
const AppRegistrar& registrar() {
return WebAppProvider::Get(browser()->profile())->registrar();
}
void SyncEmptyConfigs() {
std::vector<base::Value> app_configs;
ExternalWebAppManager::SetConfigsForTesting(&app_configs);
base::RunLoop run_loop;
WebAppProvider::Get(browser()->profile())
->external_web_app_manager()
.LoadAndSynchronizeForTesting(base::BindLambdaForTesting(
[&](std::map<GURL, PendingAppManager::InstallResult>
install_results,
std::map<GURL, bool> uninstall_results) {
EXPECT_EQ(install_results.size(), 0u);
EXPECT_EQ(uninstall_results.size(), 0u);
run_loop.Quit();
}));
run_loop.Run();
ExternalWebAppManager::SetConfigsForTesting(nullptr);
}
// Mocks "icon.png" as available in the config's directory.
base::Optional<InstallResultCode> SyncDefaultAppConfig(
const GURL& install_url,
base::StringPiece app_config_string) {
base::FilePath test_config_dir(FILE_PATH_LITERAL("test_dir"));
ExternalWebAppManager::SetConfigDirForTesting(&test_config_dir);
base::FilePath source_root_dir;
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &source_root_dir));
base::FilePath test_icon_path =
source_root_dir.Append(GetChromeTestDataDir())
.AppendASCII("web_apps/blue-192.png");
TestFileUtils file_utils(
{{base::FilePath(FILE_PATH_LITERAL("test_dir/icon.png")),
test_icon_path}});
ExternalWebAppManager::SetFileUtilsForTesting(&file_utils);
std::vector<base::Value> app_configs;
app_configs.push_back(*base::JSONReader::Read(app_config_string));
ExternalWebAppManager::SetConfigsForTesting(&app_configs);
base::Optional<InstallResultCode> code;
base::RunLoop sync_run_loop;
WebAppProvider::Get(browser()->profile())
->external_web_app_manager()
.LoadAndSynchronizeForTesting(base::BindLambdaForTesting(
[&](std::map<GURL, PendingAppManager::InstallResult>
install_results,
std::map<GURL, bool> uninstall_results) {
auto it = install_results.find(install_url);
if (it != install_results.end())
code = it->second.code;
sync_run_loop.Quit();
}));
sync_run_loop.Run();
ExternalWebAppManager::SetConfigDirForTesting(nullptr);
ExternalWebAppManager::SetFileUtilsForTesting(nullptr);
ExternalWebAppManager::SetConfigsForTesting(nullptr);
return code;
}
~ExternalWebAppManagerBrowserTest() override = default;
private:
ScopedOsHooksSuppress os_hooks_suppress_;
};
IN_PROC_BROWSER_TEST_F(ExternalWebAppManagerBrowserTest,
LaunchQueryParamsBasic) {
ExternalWebAppManager::BypassOfflineManifestRequirementForTesting();
ASSERT_TRUE(embedded_test_server()->Start());
GURL start_url = embedded_test_server()->GetURL("/web_apps/basic.html");
AppId app_id = GenerateAppIdFromURL(start_url);
EXPECT_FALSE(registrar().IsInstalled(app_id));
EXPECT_EQ(SyncDefaultAppConfig(start_url, base::ReplaceStringPlaceholders(
R"({
"app_url": "$1",
"launch_container": "window",
"user_type": ["unmanaged"],
"launch_query_params": "test_launch_params"
})",
{start_url.spec()}, nullptr)),
InstallResultCode::kSuccessNewInstall);
EXPECT_TRUE(registrar().IsInstalled(app_id));
EXPECT_EQ(registrar().GetAppStartUrl(app_id).spec(), start_url);
GURL launch_url =
embedded_test_server()->GetURL("/web_apps/basic.html?test_launch_params");
EXPECT_EQ(registrar().GetAppLaunchUrl(app_id), launch_url);
Browser* app_browser = LaunchWebAppBrowserAndWait(profile(), app_id);
EXPECT_EQ(
app_browser->tab_strip_model()->GetActiveWebContents()->GetVisibleURL(),
launch_url);
}
IN_PROC_BROWSER_TEST_F(ExternalWebAppManagerBrowserTest,
LaunchQueryParamsDuplicate) {
ExternalWebAppManager::BypassOfflineManifestRequirementForTesting();
ASSERT_TRUE(embedded_test_server()->Start());
GURL install_url = embedded_test_server()->GetURL(
"/web_apps/query_params_in_start_url.html");
GURL start_url = embedded_test_server()->GetURL(
"/web_apps/query_params_in_start_url.html?query_params=in&start=url");
AppId app_id = GenerateAppIdFromURL(start_url);
EXPECT_FALSE(registrar().IsInstalled(app_id));
EXPECT_EQ(
SyncDefaultAppConfig(install_url, base::ReplaceStringPlaceholders(
R"({
"app_url": "$1",
"launch_container": "window",
"user_type": ["unmanaged"],
"launch_query_params": "query_params=in"
})",
{install_url.spec()}, nullptr)),
InstallResultCode::kSuccessNewInstall);
EXPECT_TRUE(registrar().IsInstalled(app_id));
EXPECT_EQ(registrar().GetAppStartUrl(app_id).spec(), start_url);
// We should not duplicate the query param if start_url already has it.
EXPECT_EQ(registrar().GetAppLaunchUrl(app_id), start_url);
Browser* app_browser = LaunchWebAppBrowserAndWait(profile(), app_id);
EXPECT_EQ(
app_browser->tab_strip_model()->GetActiveWebContents()->GetVisibleURL(),
start_url);
}
IN_PROC_BROWSER_TEST_F(ExternalWebAppManagerBrowserTest,
LaunchQueryParamsComplex) {
ExternalWebAppManager::BypassOfflineManifestRequirementForTesting();
ASSERT_TRUE(embedded_test_server()->Start());
GURL install_url = embedded_test_server()->GetURL(
"/web_apps/query_params_in_start_url.html");
GURL start_url = embedded_test_server()->GetURL(
"/web_apps/query_params_in_start_url.html?query_params=in&start=url");
AppId app_id = GenerateAppIdFromURL(start_url);
EXPECT_FALSE(registrar().IsInstalled(app_id));
EXPECT_EQ(
SyncDefaultAppConfig(install_url, base::ReplaceStringPlaceholders(
R"({
"app_url": "$1",
"launch_container": "window",
"user_type": ["unmanaged"],
"launch_query_params": "!@#$$%^*&)("
})",
{install_url.spec()}, nullptr)),
InstallResultCode::kSuccessNewInstall);
EXPECT_TRUE(registrar().IsInstalled(app_id));
EXPECT_EQ(registrar().GetAppStartUrl(app_id).spec(), start_url);
GURL launch_url = embedded_test_server()->GetURL(
"/web_apps/"
"query_params_in_start_url.html?query_params=in&start=url&!@%23$%^*&)(");
EXPECT_EQ(registrar().GetAppLaunchUrl(app_id), launch_url);
Browser* app_browser = LaunchWebAppBrowserAndWait(profile(), app_id);
EXPECT_EQ(
app_browser->tab_strip_model()->GetActiveWebContents()->GetVisibleURL(),
launch_url);
}
IN_PROC_BROWSER_TEST_F(ExternalWebAppManagerBrowserTest, UninstallAndReplace) {
ExternalWebAppManager::BypassOfflineManifestRequirementForTesting();
ASSERT_TRUE(embedded_test_server()->Start());
Profile* profile = browser()->profile();
// Install Chrome app to be replaced.
const char kChromeAppDirectory[] = "app";
const char kChromeAppName[] = "App Test";
const extensions::Extension* app = InstallExtensionWithSourceAndFlags(
test_data_dir_.AppendASCII(kChromeAppDirectory), 1,
extensions::Manifest::INTERNAL, extensions::Extension::NO_FLAGS);
EXPECT_EQ(app->name(), kChromeAppName);
// Start listening for Chrome app uninstall.
extensions::TestExtensionRegistryObserver uninstall_observer(
extensions::ExtensionRegistry::Get(profile));
EXPECT_EQ(SyncDefaultAppConfig(GetAppUrl(),
base::ReplaceStringPlaceholders(
R"({
"app_url": "$1",
"launch_container": "window",
"user_type": ["unmanaged"],
"uninstall_and_replace": ["$2"]
})",
{GetAppUrl().spec(), app->id()}, nullptr)),
InstallResultCode::kSuccessNewInstall);
// Chrome app should get uninstalled.
scoped_refptr<const extensions::Extension> uninstalled_app =
uninstall_observer.WaitForExtensionUninstalled();
EXPECT_EQ(app, uninstalled_app.get());
}
// The offline manifest JSON config functionality is only available on Chrome
// OS.
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Check that offline fallback installs work offline.
IN_PROC_BROWSER_TEST_F(ExternalWebAppManagerBrowserTest,
OfflineFallbackManifestSiteOffline) {
constexpr char kAppInstallUrl[] = "https://offline-site.com/install.html";
constexpr char kAppName[] = "Offline app name";
constexpr char kAppStartUrl[] = "https://offline-site.com/start.html";
constexpr char kAppScope[] = "https://offline-site.com/";
AppId app_id = GenerateAppIdFromURL(GURL(kAppStartUrl));
EXPECT_FALSE(registrar().IsInstalled(app_id));
EXPECT_EQ(
SyncDefaultAppConfig(
GURL(kAppInstallUrl),
base::ReplaceStringPlaceholders(
R"({
"app_url": "$1",
"launch_container": "window",
"user_type": ["unmanaged"],
"offline_manifest": {
"name": "$2",
"start_url": "$3",
"scope": "$4",
"display": "minimal-ui",
"theme_color_argb_hex": "AABBCCDD",
"icon_any_pngs": ["icon.png"]
}
})",
{kAppInstallUrl, kAppName, kAppStartUrl, kAppScope}, nullptr)),
InstallResultCode::kSuccessOfflineFallbackInstall);
EXPECT_TRUE(registrar().IsInstalled(app_id));
EXPECT_EQ(registrar().GetAppShortName(app_id), kAppName);
EXPECT_EQ(registrar().GetAppStartUrl(app_id).spec(), kAppStartUrl);
EXPECT_EQ(registrar().GetAppScope(app_id).spec(), kAppScope);
// theme_color must be installed opaque.
EXPECT_EQ(registrar().GetAppThemeColor(app_id),
SkColorSetARGB(0xFF, 0xBB, 0xCC, 0xDD));
EXPECT_EQ(ReadAppIconPixel(browser()->profile(), app_id, /*size=*/192,
/*x=*/0, /*y=*/0),
SK_ColorBLUE);
}
// Check that offline fallback installs attempt fetching the install_url.
IN_PROC_BROWSER_TEST_F(ExternalWebAppManagerBrowserTest,
OfflineFallbackManifestSiteOnline) {
ASSERT_TRUE(embedded_test_server()->Start());
// This install_url serves a manifest with different values to what we specify
// in the offline_manifest. Check that it gets used instead of the
// offline_manifest.
GURL install_url = embedded_test_server()->GetURL("/web_apps/basic.html");
GURL offline_start_url = embedded_test_server()->GetURL(
"/web_apps/offline-only-start-url-that-does-not-exist.html");
GURL scope = embedded_test_server()->GetURL("/web_apps/");
AppId offline_app_id = GenerateAppIdFromURL(offline_start_url);
EXPECT_FALSE(registrar().IsInstalled(offline_app_id));
EXPECT_EQ(SyncDefaultAppConfig(
install_url, base::ReplaceStringPlaceholders(
R"({
"app_url": "$1",
"launch_container": "window",
"user_type": ["unmanaged"],
"offline_manifest": {
"name": "Offline only app name",
"start_url": "$2",
"scope": "$3",
"display": "minimal-ui",
"theme_color_argb_hex": "AABBCCDD",
"icon_any_pngs": ["icon.png"]
}
})",
{install_url.spec(), offline_start_url.spec(),
scope.spec()},
nullptr)),
InstallResultCode::kSuccessNewInstall);
EXPECT_FALSE(registrar().IsInstalled(offline_app_id));
// basic.html's manifest start_url is basic.html.
AppId app_id = GenerateAppIdFromURL(install_url);
EXPECT_TRUE(registrar().IsInstalled(app_id));
EXPECT_EQ(registrar().GetAppShortName(app_id), "Basic web app");
EXPECT_EQ(registrar().GetAppStartUrl(app_id).spec(), install_url);
EXPECT_EQ(registrar().GetAppScope(app_id).spec(), scope);
}
// Check that offline only installs work offline.
IN_PROC_BROWSER_TEST_F(ExternalWebAppManagerBrowserTest,
OfflineOnlyManifestSiteOffline) {
constexpr char kAppInstallUrl[] = "https://offline-site.com/install.html";
constexpr char kAppName[] = "Offline app name";
constexpr char kAppStartUrl[] = "https://offline-site.com/start.html";
constexpr char kAppScope[] = "https://offline-site.com/";
AppId app_id = GenerateAppIdFromURL(GURL(kAppStartUrl));
EXPECT_FALSE(registrar().IsInstalled(app_id));
EXPECT_EQ(
SyncDefaultAppConfig(
GURL(kAppInstallUrl),
base::ReplaceStringPlaceholders(
R"({
"app_url": "$1",
"launch_container": "window",
"user_type": ["unmanaged"],
"only_use_offline_manifest": true,
"offline_manifest": {
"name": "$2",
"start_url": "$3",
"scope": "$4",
"display": "minimal-ui",
"theme_color_argb_hex": "AABBCCDD",
"icon_any_pngs": ["icon.png"]
}
})",
{kAppInstallUrl, kAppName, kAppStartUrl, kAppScope}, nullptr)),
InstallResultCode::kSuccessOfflineOnlyInstall);
EXPECT_TRUE(registrar().IsInstalled(app_id));
EXPECT_EQ(registrar().GetAppShortName(app_id), kAppName);
EXPECT_EQ(registrar().GetAppStartUrl(app_id).spec(), kAppStartUrl);
EXPECT_EQ(registrar().GetAppScope(app_id).spec(), kAppScope);
// theme_color must be installed opaque.
EXPECT_EQ(registrar().GetAppThemeColor(app_id),
SkColorSetARGB(0xFF, 0xBB, 0xCC, 0xDD));
EXPECT_EQ(ReadAppIconPixel(browser()->profile(), app_id, /*size=*/192,
/*x=*/0, /*y=*/0),
SK_ColorBLUE);
}
// Check that offline only installs don't fetch from the install_url.
IN_PROC_BROWSER_TEST_F(ExternalWebAppManagerBrowserTest,
OfflineOnlyManifestSiteOnline) {
ASSERT_TRUE(embedded_test_server()->Start());
// This install_url serves a manifest with different values to what we specify
// in the offline_manifest. Check that it doesn't get used.
GURL install_url = GetAppUrl();
const char kAppName[] = "Offline only app name";
GURL start_url = embedded_test_server()->GetURL(
"/web_apps/offline-only-start-url-that-does-not-exist.html");
GURL scope = embedded_test_server()->GetURL("/web_apps/");
AppId app_id = GenerateAppIdFromURL(start_url);
EXPECT_FALSE(registrar().IsInstalled(app_id));
EXPECT_EQ(
SyncDefaultAppConfig(install_url, base::ReplaceStringPlaceholders(
R"({
"app_url": "$1",
"launch_container": "window",
"user_type": ["unmanaged"],
"only_use_offline_manifest": true,
"offline_manifest": {
"name": "$2",
"start_url": "$3",
"scope": "$4",
"display": "minimal-ui",
"theme_color_argb_hex": "AABBCCDD",
"icon_any_pngs": ["icon.png"]
}
})",
{install_url.spec(), kAppName,
start_url.spec(), scope.spec()},
nullptr)),
InstallResultCode::kSuccessOfflineOnlyInstall);
EXPECT_TRUE(registrar().IsInstalled(app_id));
EXPECT_EQ(registrar().GetAppShortName(app_id), kAppName);
EXPECT_EQ(registrar().GetAppStartUrl(app_id).spec(), start_url);
EXPECT_EQ(registrar().GetAppScope(app_id).spec(), scope);
// theme_color must be installed opaque.
EXPECT_EQ(registrar().GetAppThemeColor(app_id),
SkColorSetARGB(0xFF, 0xBB, 0xCC, 0xDD));
EXPECT_EQ(ReadAppIconPixel(browser()->profile(), app_id, /*size=*/192,
/*x=*/0, /*y=*/0),
SK_ColorBLUE);
}
const char kOnlyForNewUsersInstallUrl[] = "https://example.org/";
const char kOnlyForNewUsersConfig[] = R"({
"app_url": "https://example.org/",
"launch_container": "window",
"user_type": ["unmanaged"],
"only_for_new_users": true,
"only_use_offline_manifest": true,
"offline_manifest": {
"name": "Test",
"start_url": "https://example.org/",
"scope": "https://example.org/",
"display": "standalone",
"icon_any_pngs": ["icon.png"]
}
})";
IN_PROC_BROWSER_TEST_F(ExternalWebAppManagerBrowserTest,
PRE_OnlyForNewUsersWithNewUser) {
// New user should have the app installed.
EXPECT_EQ(SyncDefaultAppConfig(GURL(kOnlyForNewUsersInstallUrl),
kOnlyForNewUsersConfig),
InstallResultCode::kSuccessOfflineOnlyInstall);
}
IN_PROC_BROWSER_TEST_F(ExternalWebAppManagerBrowserTest,
OnlyForNewUsersWithNewUser) {
// App should persist after user stops being a new user.
EXPECT_EQ(SyncDefaultAppConfig(GURL(kOnlyForNewUsersInstallUrl),
kOnlyForNewUsersConfig),
InstallResultCode::kSuccessAlreadyInstalled);
}
IN_PROC_BROWSER_TEST_F(ExternalWebAppManagerBrowserTest,
PRE_OnlyForNewUsersWithOldUser) {
// Simulate running Chrome without the configs present.
SyncEmptyConfigs();
}
IN_PROC_BROWSER_TEST_F(ExternalWebAppManagerBrowserTest,
OnlyForNewUsersWithOldUser) {
// This instance of Chrome should be considered not a new user after the
// previous PRE_ launch and sync.
EXPECT_EQ(SyncDefaultAppConfig(GURL(kOnlyForNewUsersInstallUrl),
kOnlyForNewUsersConfig),
base::nullopt);
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
// Icon resourcs are only available on Chrome branded builds.
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
IN_PROC_BROWSER_TEST_F(ExternalWebAppManagerBrowserTest, PreinstalledWebApps) {
base::AutoReset<bool> scope =
SetExternalAppInstallFeatureAlwaysEnabledForTesting();
auto& provider = *WebAppProvider::Get(browser()->profile());
struct Expectation {
const char* app_id;
const char* install_url;
const char* launch_url;
} kExpectations[] = {
#if BUILDFLAG(IS_CHROMEOS_ASH)
{
kGoogleCalendarAppId,
"https://calendar.google.com/calendar/installwebapp?usp=chrome_default",
"https://calendar.google.com/calendar/r?usp=installed_webapp",
},
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
{
kGoogleDocsAppId,
"https://docs.google.com/document/installwebapp?usp=chrome_default",
"https://docs.google.com/document/?usp=installed_webapp",
},
{
kGoogleSlidesAppId,
"https://docs.google.com/presentation/installwebapp?usp=chrome_default",
"https://docs.google.com/presentation/?usp=installed_webapp",
},
{
kGoogleSheetsAppId,
"https://docs.google.com/spreadsheets/installwebapp?usp=chrome_default",
"https://docs.google.com/spreadsheets/?usp=installed_webapp",
},
{
kGoogleDriveAppId,
"https://drive.google.com/drive/installwebapp?usp=chrome_default",
"https://drive.google.com/?lfhs=2&usp=installed_webapp",
},
{
kGmailAppId,
"https://mail.google.com/mail/installwebapp?usp=chrome_default",
"https://mail.google.com/?usp=installed_webapp",
},
{
kYoutubeAppId,
"https://www.youtube.com/s/notifications/manifest/cr_install.html",
"https://www.youtube.com/?feature=ytca",
},
};
base::RunLoop run_loop;
provider.external_web_app_manager().LoadAndSynchronizeForTesting(
base::BindLambdaForTesting(
[&](std::map<GURL, PendingAppManager::InstallResult> install_results,
std::map<GURL, bool> uninstall_results) {
EXPECT_EQ(install_results.size(),
base::span<Expectation>(kExpectations).size());
for (const Expectation& expectation : kExpectations) {
EXPECT_EQ(install_results[GURL(expectation.install_url)].code,
InstallResultCode::kSuccessOfflineOnlyInstall);
}
EXPECT_EQ(uninstall_results.size(), 0u);
run_loop.Quit();
}));
run_loop.Run();
for (const Expectation& expectation : kExpectations) {
EXPECT_EQ(provider.registrar().GetAppLaunchUrl(expectation.app_id),
GURL(expectation.launch_url));
}
// Note that default web apps *DO* show app icons on Chrome OS however it
// is done via the |WebAppsChromeOs| publishing live our current app state to
// the app service rather than writing shortcut files as the case on all other
// desktop platforms.
auto* test_os_integration_manager =
provider.os_integration_manager().AsTestOsIntegrationManager();
EXPECT_EQ(test_os_integration_manager->num_create_shortcuts_calls(), 0u);
EXPECT_EQ(test_os_integration_manager->num_create_file_handlers_calls(), 0u);
EXPECT_EQ(test_os_integration_manager->num_register_run_on_os_login_calls(),
0u);
EXPECT_EQ(
test_os_integration_manager->num_add_app_to_quick_launch_bar_calls(), 0u);
EXPECT_FALSE(test_os_integration_manager->did_add_to_desktop());
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
} // namespace web_app