blob: 338d4e2a2d3997e3730887f8b04a887ec0d2ee43 [file]
// 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 <climits>
#include <initializer_list>
#include <memory>
#include <string>
#include <string_view>
#include <tuple>
#include <vector>
#include "base/check.h"
#include "base/functional/function_ref.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/devtools/protocol/devtools_protocol_test_support.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "chrome/browser/web_applications/proto/web_app_os_integration_state.pb.h"
#include "chrome/browser/web_applications/test/os_integration_test_override_impl.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
#include "components/webapps/common/web_app_id.h"
#include "components/webapps/isolated_web_apps/scheme.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/base/filename_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "url/gurl.h"
namespace {
using web_app::WebAppInstallInfo;
using web_app::WebAppProvider;
using webapps::ManifestId;
class IWAProtocolTestBase : public DevToolsProtocolTestBase {
public:
IWAProtocolTestBase() {
iwa_scoped_feature_list_.InitWithFeatures(
{
#if !BUILDFLAG(IS_CHROMEOS)
features::kIsolatedWebApps,
#endif // !BUILDFLAG(IS_CHROMEOS)
features::kIsolatedWebAppDevMode},
{});
}
void SetUp() override {
auto [url, bundle_id] = SetUpIwa();
bundle_url_ = url;
bundle_id_ = bundle_id;
auto url_info_ =
web_app::IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(bundle_id);
app_id_ = url_info_.app_id();
// This is strange, but the tests are running in the SetUp().
// So all additional setup must be done before.
DevToolsProtocolTestBase::SetUp();
}
virtual std::pair<GURL, web_package::SignedWebBundleId> SetUpIwa() = 0;
void SetUpOnMainThread() override {
DevToolsProtocolTestBase::SetUpOnMainThread();
AttachToBrowserTarget();
override_registration_ =
web_app::OsIntegrationTestOverrideImpl::OverrideForTesting();
}
void TearDownOnMainThread() override {
web_app::test::UninstallAllWebApps(browser()->GetProfile());
override_registration_.reset();
DevToolsProtocolTestBase::TearDownOnMainThread();
}
protected:
GURL InstallUrl() const { return bundle_url_; }
std::string InstallManifestId() const {
return base::StrCat({webapps::kIsolatedAppScheme, "://", bundle_id_.id()});
}
webapps::AppId AppId() const { return app_id_; }
const web_package::SignedWebBundleId& BundleId() const { return bundle_id_; }
bool AppExists() {
auto* provider = WebAppProvider::GetForTest(browser()->GetProfile());
CHECK(provider);
return provider->registrar_unsafe().GetInstallState(AppId()).has_value();
}
void InstallCommand(const GURL& url) {
EXPECT_TRUE(SendCommandSync("PWA.install",
base::DictValue{}
.Set("manifestId", InstallManifestId())
.Set("installUrlOrBundleUrl", url.spec())));
EXPECT_TRUE(AppExists());
if (error()) {
const std::string& message = *error()->FindString("message");
LOG(ERROR) << message;
}
}
void Install() { InstallCommand(bundle_url_); }
void AssertErrorMessageContains(
std::initializer_list<std::string> pieces) const {
ASSERT_TRUE(error());
const std::string& message = *error()->FindString("message");
for (const auto& piece : pieces) {
EXPECT_THAT(message, testing::HasSubstr(piece));
}
}
private:
std::unique_ptr<web_app::OsIntegrationTestOverrideImpl::BlockingRegistration>
override_registration_;
base::test::ScopedFeatureList iwa_scoped_feature_list_;
GURL bundle_url_;
webapps::AppId app_id_;
// Will be overwritten.
web_package::SignedWebBundleId bundle_id_ =
web_package::SignedWebBundleId::CreateRandomForProxyMode();
};
class IWAProtocolTestLocalFile : public IWAProtocolTestBase {
std::pair<GURL, web_package::SignedWebBundleId> SetUpIwa() override {
bundle_ = web_app::IsolatedWebAppBuilder(web_app::ManifestBuilder())
.BuildBundle();
return std::pair(net::FilePathToFileURL(bundle_->path()),
bundle_->web_bundle_id());
}
private:
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> bundle_;
};
class IWAProtocolTestRemoteFile : public IWAProtocolTestBase {
std::pair<GURL, web_package::SignedWebBundleId> SetUpIwa() override {
bundle_ = web_app::IsolatedWebAppBuilder(web_app::ManifestBuilder())
.BuildBundle();
const base::FilePath& bundle_path = bundle_->path();
embedded_test_server()->AddDefaultHandlers();
embedded_test_server()->ServeFilesFromDirectory(bundle_path.DirName());
test_server_closer_ = embedded_test_server()->StartAndReturnHandle();
GURL url = embedded_test_server()->GetURL(
"/" + bundle_path.BaseName().MaybeAsASCII());
return std::pair(url, bundle_->web_bundle_id());
}
private:
net::test_server::EmbeddedTestServerHandle test_server_closer_;
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> bundle_;
};
class IWAProtocolTestRemoteProxy : public IWAProtocolTestBase {
std::pair<GURL, web_package::SignedWebBundleId> SetUpIwa() override {
proxy_app_ = web_app::IsolatedWebAppBuilder(web_app::ManifestBuilder())
.BuildAndStartProxyServer();
auto bundle_id = web_package::SignedWebBundleId::CreateRandomForProxyMode();
return std::pair(proxy_app_->proxy_server().GetOrigin().GetURL(),
bundle_id);
}
private:
std::unique_ptr<web_app::ScopedProxyIsolatedWebApp> proxy_app_;
};
} // namespace
IN_PROC_BROWSER_TEST_F(IWAProtocolTestLocalFile, Install) {
Install();
}
// TODO(crbug.com/482445180): Flaky on windows.
#if BUILDFLAG(IS_WIN)
#define MAYBE_RemoteFileInstall DISABLED_RemoteFileInstall
#else
#define MAYBE_RemoteFileInstall RemoteFileInstall
#endif // BUILDFLAG(IS_WIN)
IN_PROC_BROWSER_TEST_F(IWAProtocolTestRemoteFile, MAYBE_RemoteFileInstall) {
Install();
}
IN_PROC_BROWSER_TEST_F(IWAProtocolTestRemoteProxy, Install) {
Install();
}
// In comparison with PWA, we cannot install IWA twice.
IN_PROC_BROWSER_TEST_F(IWAProtocolTestLocalFile, Install_Twice) {
Install();
ASSERT_FALSE(SendCommandSync(
"PWA.install", base::DictValue{}
.Set("manifestId", InstallManifestId())
.Set("installUrlOrBundleUrl", InstallUrl().spec())));
AssertErrorMessageContains({InstallManifestId(), "already installed"});
ASSERT_TRUE(AppExists());
}
IN_PROC_BROWSER_TEST_F(IWAProtocolTestLocalFile, Install_UrlUnreachable) {
ASSERT_FALSE(SendCommandSync(
"PWA.install",
base::DictValue{}
.Set("manifestId", InstallManifestId())
.Set("installUrlOrBundleUrl", "http://hello/this/is/not/existing")));
AssertErrorMessageContains(
{InstallManifestId(), "http://hello/this/is/not/existing"});
ASSERT_FALSE(AppExists());
}
IN_PROC_BROWSER_TEST_F(IWAProtocolTestLocalFile, Install_InvalidBundleId) {
std::string garbage_id = "isolated-app://garbage_id";
ASSERT_FALSE(SendCommandSync(
"PWA.install", base::DictValue{}
.Set("manifestId", garbage_id)
.Set("installUrlOrBundleUrl", InstallUrl().spec())));
AssertErrorMessageContains(
{"must be a valid signed web bundle id", garbage_id});
ASSERT_FALSE(AppExists());
}
IN_PROC_BROWSER_TEST_F(IWAProtocolTestLocalFile, Install_UnmatchManifestId) {
std::string unmatched_id =
"isolated-app://"
"aiv4bxauvcu3zvbu6r5yynoh5atkzqqaoeof5mwz54b4zfywcrjuoaacai";
ASSERT_FALSE(SendCommandSync(
"PWA.install", base::DictValue{}
.Set("manifestId", unmatched_id)
.Set("installUrlOrBundleUrl", InstallUrl().spec())));
AssertErrorMessageContains(
{InstallUrl().spec(), unmatched_id, "Web bundle id mismatch"});
ASSERT_FALSE(AppExists());
}
IN_PROC_BROWSER_TEST_F(IWAProtocolTestRemoteFile, Install_UnmatchManifestId) {
std::string unmatched_id =
"isolated-app://"
"aiv4bxauvcu3zvbu6r5yynoh5atkzqqaoeof5mwz54b4zfywcrjuoaacai";
ASSERT_FALSE(SendCommandSync(
"PWA.install", base::DictValue{}
.Set("manifestId", unmatched_id)
.Set("installUrlOrBundleUrl", InstallUrl().spec())));
AssertErrorMessageContains(
{InstallUrl().spec(), unmatched_id, "Web bundle id mismatch"});
ASSERT_FALSE(AppExists());
}
IN_PROC_BROWSER_TEST_F(IWAProtocolTestLocalFile, Install_Uninstall) {
ASSERT_FALSE(AppExists());
Install();
ASSERT_TRUE(AppExists());
ASSERT_TRUE(SendCommandSync(
"PWA.uninstall",
base::DictValue{}.Set("manifestId", InstallManifestId())));
ASSERT_FALSE(AppExists());
}
IN_PROC_BROWSER_TEST_F(IWAProtocolTestLocalFile, GetAppId_BundleIdForIwa) {
Install();
content::RenderFrameHost* iwa_frame =
web_app::OpenIsolatedWebApp(browser()->GetProfile(), AppId());
content::WebContents* iwa_contents =
content::WebContents::FromRenderFrameHost(iwa_frame);
DetachProtocolClient();
AttachToWebContents(iwa_contents);
const base::DictValue* result =
SendCommandSync("Page.getAppId", base::DictValue{});
ASSERT_TRUE(result);
EXPECT_THAT(result->FindString("bundleId"),
testing::Pointee(testing::Eq(BundleId().id())));
EXPECT_FALSE(result->FindString("parentAppName"));
DetachProtocolClient();
}
IN_PROC_BROWSER_TEST_F(IWAProtocolTestLocalFile,
GetAppId_ParentAppNameForSubApp) {
Install();
embedded_test_server()->AddDefaultHandlers(GetChromeTestDataDir());
ASSERT_TRUE(embedded_test_server()->Start());
GURL sub_app_url = embedded_test_server()->GetURL("/web_apps/basic.html");
std::unique_ptr<WebAppInstallInfo> sub_app_info =
WebAppInstallInfo::CreateWithStartUrlForTesting(sub_app_url);
sub_app_info->title = u"Sub App 1";
sub_app_info->scope = sub_app_url;
sub_app_info->parent_app_id = AppId();
webapps::AppId sub_app_id = web_app::test::InstallWebApp(
browser()->GetProfile(), std::move(sub_app_info));
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), sub_app_url));
DetachProtocolClient();
AttachToWebContents(browser()->tab_strip_model()->GetActiveWebContents());
const base::DictValue* sub_app_result =
SendCommandSync("Page.getAppId", base::DictValue{});
ASSERT_TRUE(sub_app_result);
EXPECT_THAT(sub_app_result->FindString("parentAppName"),
testing::Pointee(testing::Eq("Test App")));
DetachProtocolClient();
}
IN_PROC_BROWSER_TEST_F(IWAProtocolTestLocalFile, SubApps_ListAndSiblingList) {
Install();
embedded_test_server()->AddDefaultHandlers(GetChromeTestDataDir());
ASSERT_TRUE(embedded_test_server()->Start());
GURL sub_app_1_url = embedded_test_server()->GetURL("/web_apps/basic.html");
std::unique_ptr<WebAppInstallInfo> sub_app_1_info =
WebAppInstallInfo::CreateWithStartUrlForTesting(sub_app_1_url);
sub_app_1_info->title = u"Sub App 1";
sub_app_1_info->scope = sub_app_1_url;
sub_app_1_info->parent_app_id = AppId();
webapps::AppId sub_app_1_id = web_app::test::InstallWebApp(
browser()->GetProfile(), std::move(sub_app_1_info));
GURL sub_app_2_url =
embedded_test_server()->GetURL("/web_apps/has_manifest_id.html");
std::unique_ptr<WebAppInstallInfo> sub_app_2_info =
WebAppInstallInfo::CreateWithStartUrlForTesting(sub_app_2_url);
sub_app_2_info->title = u"Sub App 2";
sub_app_2_info->scope = sub_app_2_url;
sub_app_2_info->parent_app_id = AppId();
webapps::AppId sub_app_2_id = web_app::test::InstallWebApp(
browser()->GetProfile(), std::move(sub_app_2_info));
// Inspect Sub-App 1:
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), sub_app_1_url));
DetachProtocolClient();
AttachToWebContents(browser()->tab_strip_model()->GetActiveWebContents());
// getSiblingSubApps on sub-app 1 returns only sub-app 2 (excluding itself):
const base::DictValue* siblings_result =
SendCommandSync("Page.getSiblingSubApps", base::DictValue{});
ASSERT_TRUE(siblings_result);
const base::ListValue* sibling_apps = siblings_result->FindList("subApps");
ASSERT_TRUE(sibling_apps);
EXPECT_EQ(sibling_apps->size(), 1u);
EXPECT_THAT((*sibling_apps)[0].GetDict().FindString("name"),
testing::Pointee(testing::Eq("Sub App 2")));
EXPECT_THAT((*sibling_apps)[0].GetDict().FindString("startUrl"),
testing::Pointee(testing::Eq(sub_app_2_url.spec())));
// Inspect Parent App:
content::RenderFrameHost* iwa_frame =
web_app::OpenIsolatedWebApp(browser()->GetProfile(), AppId());
content::WebContents* iwa_contents =
content::WebContents::FromRenderFrameHost(iwa_frame);
DetachProtocolClient();
AttachToWebContents(iwa_contents);
// getSubApps on parent app returns both sub-apps:
const base::DictValue* sub_apps_result =
SendCommandSync("Page.getSubApps", base::DictValue{});
ASSERT_TRUE(sub_apps_result);
const base::ListValue* sub_apps = sub_apps_result->FindList("subApps");
ASSERT_TRUE(sub_apps);
EXPECT_EQ(sub_apps->size(), 2u);
std::vector<std::string> sub_app_names;
for (const auto& item : *sub_apps) {
const std::string* name = item.GetDict().FindString("name");
ASSERT_TRUE(name);
sub_app_names.push_back(*name);
}
EXPECT_THAT(sub_app_names,
testing::UnorderedElementsAre("Sub App 1", "Sub App 2"));
DetachProtocolClient();
}
IN_PROC_BROWSER_TEST_F(IWAProtocolTestLocalFile, GetSubApps_NoSubApps) {
Install();
content::RenderFrameHost* iwa_frame =
web_app::OpenIsolatedWebApp(browser()->GetProfile(), AppId());
content::WebContents* iwa_contents =
content::WebContents::FromRenderFrameHost(iwa_frame);
DetachProtocolClient();
AttachToWebContents(iwa_contents);
const base::DictValue* sub_apps_result =
SendCommandSync("Page.getSubApps", base::DictValue{});
ASSERT_TRUE(sub_apps_result);
const base::ListValue* sub_apps = sub_apps_result->FindList("subApps");
ASSERT_TRUE(sub_apps);
EXPECT_TRUE(sub_apps->empty());
const base::DictValue* siblings_result =
SendCommandSync("Page.getSiblingSubApps", base::DictValue{});
ASSERT_TRUE(siblings_result);
const base::ListValue* sibling_apps = siblings_result->FindList("subApps");
ASSERT_TRUE(sibling_apps);
EXPECT_TRUE(sibling_apps->empty());
DetachProtocolClient();
}
IN_PROC_BROWSER_TEST_F(IWAProtocolTestLocalFile, GetAppId_NoManifestUrl) {
Install();
embedded_test_server()->AddDefaultHandlers(GetChromeTestDataDir());
ASSERT_TRUE(embedded_test_server()->Start());
GURL sub_app_url = embedded_test_server()->GetURL("/title1.html");
std::unique_ptr<WebAppInstallInfo> sub_app_info =
WebAppInstallInfo::CreateWithStartUrlForTesting(sub_app_url);
sub_app_info->title = u"Sub App Without Manifest";
sub_app_info->scope = sub_app_url;
sub_app_info->parent_app_id = AppId();
webapps::AppId sub_app_id = web_app::test::InstallWebApp(
browser()->GetProfile(), std::move(sub_app_info));
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), sub_app_url));
DetachProtocolClient();
AttachToWebContents(browser()->tab_strip_model()->GetActiveWebContents());
const base::DictValue* result =
SendCommandSync("Page.getAppId", base::DictValue{});
ASSERT_TRUE(result);
EXPECT_FALSE(result->FindString("appId"));
EXPECT_FALSE(result->FindString("recommendedId"));
EXPECT_THAT(result->FindString("parentAppName"),
testing::Pointee(testing::Eq("Test App")));
DetachProtocolClient();
}