// Copyright 2022 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/run_loop.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/apps/app_service/app_registry_cache_waiter.h"
#include "chrome/browser/lacros/browser_test_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window.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/test/web_app_browsertest_util.h"
#include "chrome/browser/ui/web_applications/test/web_app_navigation_browsertest.h"
#include "chrome/browser/ui/web_applications/web_app_dialogs.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/test/web_app_test_observers.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/crosapi/mojom/test_controller.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "chromeos/startup/browser_params_proxy.h"
#include "components/app_constants/constants.h"
#include "components/webapps/browser/test/service_worker_registration_waiter.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/blink/public/common/features.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
using crosapi::mojom::ShelfItemState;
namespace {
constexpr char kFirstAppUrlHost[] = "first-pwa.test";
constexpr char kSecondAppUrlHost[] = "second-pwa.test";
} // namespace
namespace web_app {
class LacrosWebAppShelfBrowserTest : public WebAppBrowserTestBase {
public:
LacrosWebAppShelfBrowserTest() = default;
~LacrosWebAppShelfBrowserTest() override = default;
protected:
// If ash is does not contain the relevant test controller functionality, then
// there's nothing to do for this test.
bool IsServiceAvailable() {
DCHECK(IsWebAppsCrosapiEnabled());
uint32_t version =
chromeos::LacrosService::Get()
->GetInterfaceVersion<crosapi::mojom::TestController>();
using MethodMinVersions = crosapi::mojom::TestController::MethodMinVersions;
if (version < MethodMinVersions::kGetShelfItemStateMinVersion ||
version < MethodMinVersions::kSelectContextMenuForShelfItemMinVersion) {
LOG(WARNING) << "Unsupported ash version.";
return false;
}
return true;
}
base::test::ScopedFeatureList tab_strip_feature_{
blink::features::kDesktopPWAsTabStrip};
base::test::ScopedFeatureList tab_strip_customizations_feature_{
blink::features::kDesktopPWAsTabStripCustomizations};
};
IN_PROC_BROWSER_TEST_F(LacrosWebAppShelfBrowserTest, Activation) {
if (!IsServiceAvailable())
GTEST_SKIP();
const GURL app1_url =
https_server()->GetURL(kFirstAppUrlHost, "/web_apps/basic.html");
const webapps::AppId app1_id =
InstallWebAppFromPageAndCloseAppBrowser(browser(), app1_url);
const GURL app2_url = https_server()->GetURL(
kSecondAppUrlHost, "/web_apps/standalone/basic.html");
const webapps::AppId app2_id =
InstallWebAppFromPageAndCloseAppBrowser(browser(), app2_url);
apps::AppReadinessWaiter(profile(), app1_id).Await();
Browser* app_browser1 = LaunchWebAppBrowser(app1_id);
EXPECT_TRUE(AppBrowserController::IsForWebApp(app_browser1, app1_id));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kActive)));
ASSERT_TRUE(AddTabAtIndex(/*index=*/1, app1_url, ui::PAGE_TRANSITION_TYPED));
apps::AppReadinessWaiter(profile(), app2_id).Await();
LaunchWebAppBrowser(app2_id);
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app2_id, static_cast<uint32_t>(ShelfItemState::kActive)));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kRunning)));
CloseAndWait(app_browser1);
// A tab open at app1_url is not sufficient for the app to be considered
// running.
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kNormal)));
test::UninstallWebApp(profile(), app2_id);
apps::AppReadinessWaiter(profile(), app2_id,
apps::Readiness::kUninstalledByUser)
.Await();
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app2_id, static_cast<uint32_t>(ShelfItemState::kNormal)));
test::UninstallWebApp(profile(), app1_id);
}
// Navigating out of scope in an app window does not affect which app is
// considered running.
IN_PROC_BROWSER_TEST_F(LacrosWebAppShelfBrowserTest, Navigation) {
if (!IsServiceAvailable())
GTEST_SKIP();
const GURL app1_url =
https_server()->GetURL(kFirstAppUrlHost, "/web_apps/basic.html");
const webapps::AppId app1_id =
InstallWebAppFromPageAndCloseAppBrowser(browser(), app1_url);
const GURL app2_url = https_server()->GetURL(
kSecondAppUrlHost, "/web_app_shortcuts/shortcuts.html");
const webapps::AppId app2_id =
InstallWebAppFromPageAndCloseAppBrowser(browser(), app2_url);
GURL out_of_scope_url = https_server()->GetURL("/empty.html");
Browser* app_browser1 = LaunchWebAppBrowser(app1_id);
{
NavigateParams params(app_browser1, out_of_scope_url,
ui::PAGE_TRANSITION_LINK);
params.tabstrip_index = app_browser1->tab_strip_model()->active_index();
params.disposition = WindowOpenDisposition::CURRENT_TAB;
Navigate(¶ms);
ASSERT_TRUE(
content::WaitForLoadStop(params.navigated_or_inserted_contents));
EXPECT_EQ(app_browser1->tab_strip_model()->count(), 1);
EXPECT_EQ(app_browser1->tab_strip_model()
->GetActiveWebContents()
->GetLastCommittedURL(),
out_of_scope_url);
}
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kActive)));
LaunchWebAppBrowser(app2_id);
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app2_id, static_cast<uint32_t>(ShelfItemState::kActive)));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kRunning)));
app_browser1->window()->Activate();
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kActive)));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app2_id, static_cast<uint32_t>(ShelfItemState::kRunning)));
test::UninstallWebApp(profile(), app1_id);
test::UninstallWebApp(profile(), app2_id);
}
IN_PROC_BROWSER_TEST_F(LacrosWebAppShelfBrowserTest, BadgeShown) {
if (!IsServiceAvailable())
GTEST_SKIP();
const GURL app_url = https_server()->GetURL(
kFirstAppUrlHost, "/web_apps/minimal_ui/basic.html");
const webapps::AppId app_id =
InstallWebAppFromPageAndCloseAppBrowser(browser(), app_url);
apps::AppReadinessWaiter(profile(), app_id).Await();
Browser* app_browser = LaunchWebAppBrowser(app_id);
content::WebContents* const web_contents =
app_browser->tab_strip_model()->GetActiveWebContents();
EXPECT_TRUE(AppBrowserController::IsForWebApp(app_browser, app_id));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app_id, static_cast<uint32_t>(ShelfItemState::kActive)));
ASSERT_TRUE(content::ExecJs(web_contents, "navigator.setAppBadge();",
content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app_id, static_cast<uint32_t>(ShelfItemState::kActive) |
static_cast<uint32_t>(ShelfItemState::kNotification)));
ASSERT_TRUE(content::ExecJs(web_contents, "navigator.clearAppBadge();",
content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app_id, static_cast<uint32_t>(ShelfItemState::kActive)));
test::UninstallWebApp(profile(), app_id);
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app_id, static_cast<uint32_t>(ShelfItemState::kNormal)));
}
IN_PROC_BROWSER_TEST_F(LacrosWebAppShelfBrowserTest, RunningInTab) {
if (!IsServiceAvailable())
GTEST_SKIP();
auto& test_controller = chromeos::LacrosService::Get()
->GetRemote<crosapi::mojom::TestController>();
const GURL app1_url = https_server()->GetURL(
kFirstAppUrlHost, "/web_apps/standalone/basic.html");
const webapps::AppId app1_id =
InstallWebAppFromPageAndCloseAppBrowser(browser(), app1_url);
const GURL app2_url =
https_server()->GetURL(kSecondAppUrlHost, "/web_apps/basic.html");
const webapps::AppId app2_id =
InstallWebAppFromPageAndCloseAppBrowser(browser(), app2_url);
{
auto& sync_bridge =
WebAppProvider::GetForTest(profile())->sync_bridge_unsafe();
Browser* app_browser1 = LaunchWebAppBrowser(app1_id);
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kActive)));
{
base::test::TestFuture<bool> success_future;
test_controller->PinOrUnpinItemInShelf(app1_id, /*pin=*/true,
success_future.GetCallback());
EXPECT_TRUE(success_future.Get());
}
CloseAndWait(app_browser1);
sync_bridge.SetAppUserDisplayModeForTesting(
app1_id, mojom::UserDisplayMode::kBrowser);
apps::AppWindowModeWaiter(profile(), app1_id, apps::WindowMode::kBrowser)
.Await();
Browser* app_browser2 = LaunchWebAppBrowser(app2_id);
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app2_id, static_cast<uint32_t>(ShelfItemState::kActive)));
{
base::test::TestFuture<bool> success_future;
test_controller->PinOrUnpinItemInShelf(app2_id, /*pin=*/true,
success_future.GetCallback());
EXPECT_TRUE(success_future.Get());
}
CloseAndWait(app_browser2);
sync_bridge.SetAppUserDisplayModeForTesting(
app2_id, mojom::UserDisplayMode::kBrowser);
apps::AppWindowModeWaiter(profile(), app2_id, apps::WindowMode::kBrowser)
.Await();
}
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app_constants::kLacrosAppId,
static_cast<uint32_t>(ShelfItemState::kActive)));
test_controller->LaunchAppFromAppList(app1_id);
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kActive)));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app_constants::kLacrosAppId,
static_cast<uint32_t>(ShelfItemState::kRunning)));
test_controller->LaunchAppFromAppList(app2_id);
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app2_id, static_cast<uint32_t>(ShelfItemState::kActive)));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kRunning)));
EXPECT_EQ(BrowserList::GetInstance()->size(), 1U);
TabStripModel* tab_strip_model = browser()->tab_strip_model();
tab_strip_model->CloseWebContentsAt(tab_strip_model->active_index(),
TabCloseTypes::CLOSE_NONE);
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app2_id, static_cast<uint32_t>(ShelfItemState::kNormal)));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kActive)));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app_constants::kLacrosAppId,
static_cast<uint32_t>(ShelfItemState::kRunning)));
tab_strip_model->CloseWebContentsAt(tab_strip_model->active_index(),
TabCloseTypes::CLOSE_NONE);
ASSERT_TRUE(AddTabAtIndex(/*index=*/1, app2_url, ui::PAGE_TRANSITION_TYPED));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kNormal)));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app2_id, static_cast<uint32_t>(ShelfItemState::kActive)));
// Navigation is sufficient to change which app is considered running.
{
NavigateParams params(browser(), app1_url, ui::PAGE_TRANSITION_TYPED);
params.tabstrip_index = tab_strip_model->active_index();
params.disposition = WindowOpenDisposition::CURRENT_TAB;
Navigate(¶ms);
ASSERT_TRUE(
content::WaitForLoadStop(params.navigated_or_inserted_contents));
}
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app2_id, static_cast<uint32_t>(ShelfItemState::kNormal)));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kActive)));
test::UninstallWebApp(profile(), app1_id);
test::UninstallWebApp(profile(), app2_id);
}
// Tests that a web page without a manifest may be used to create a shortcut.
// Tests that a web page with a manifest etc. may be used to install a PWA.
// Tests that opening a shortcut in a tab does not make it appear in the Shelf.
// Tests that web apps opened in windows do appear in the Shelf.
IN_PROC_BROWSER_TEST_F(LacrosWebAppShelfBrowserTest, CreateShortcut) {
if (!IsServiceAvailable())
GTEST_SKIP();
ASSERT_TRUE(embedded_test_server()->Start());
crosapi::mojom::TestController* const test_controller =
chromeos::LacrosService::Get()
->GetRemote<crosapi::mojom::TestController>()
.get();
auto& sync_bridge =
WebAppProvider::GetForTest(profile())->sync_bridge_unsafe();
GURL app1_url(
embedded_test_server()->GetURL("/banners/scope_a/no_manifest.html"));
GURL app2_url(
embedded_test_server()->GetURL("/banners/scope_b/scope_b.html"));
webapps::AppId app1_id;
Browser* app1_browser;
{
web_app::ServiceWorkerRegistrationWaiter registration_waiter(profile(),
app1_url);
ASSERT_TRUE(
AddTabAtIndex(/*index=*/1, app1_url, ui::PAGE_TRANSITION_TYPED));
registration_waiter.AwaitRegistration();
ASSERT_TRUE(
AddTabAtIndex(/*index=*/2, app2_url, ui::PAGE_TRANSITION_TYPED));
ASSERT_TRUE(base::test::RunUntil([&] {
return web_app::GetAppMenuCommandState(IDC_INSTALL_PWA, browser()) ==
kEnabled;
}));
// Install app1 shortcut.
browser()->tab_strip_model()->ActivateTabAt(/*index=*/1);
AppMenuCommandState install_pwa_state =
base::FeatureList::IsEnabled(features::kWebAppUniversalInstall)
? kEnabled
: kNotPresent;
EXPECT_EQ(GetAppMenuCommandState(IDC_CREATE_SHORTCUT, browser()), kEnabled);
EXPECT_EQ(GetAppMenuCommandState(IDC_INSTALL_PWA, browser()),
install_pwa_state);
SetAutoAcceptWebAppDialogForTesting(
/*auto_accept=*/true,
/*auto_open_in_window=*/true);
ui_test_utils::BrowserChangeObserver browser_change_observer(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
WebAppTestInstallObserver install_observer(profile());
install_observer.BeginListening();
CHECK(chrome::ExecuteCommand(browser(), IDC_CREATE_SHORTCUT));
app1_id = install_observer.Wait();
app1_browser = browser_change_observer.Wait();
EXPECT_TRUE(AppBrowserController::IsForWebApp(app1_browser, app1_id));
SetAutoAcceptWebAppDialogForTesting(
/*auto_accept=*/false,
/*auto_open_in_window=*/false);
}
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kActive)));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app_constants::kLacrosAppId,
static_cast<uint32_t>(ShelfItemState::kRunning)));
// Launch app1 in a browser tab (only).
{
sync_bridge.SetAppUserDisplayModeForTesting(
app1_id, mojom::UserDisplayMode::kBrowser);
apps::AppWindowModeWaiter(profile(), app1_id, apps::WindowMode::kBrowser)
.Await();
app1_browser->window()->Close();
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kNormal)));
ui_test_utils::TabAddedWaiter waiter(browser());
test_controller->LaunchAppFromAppList(app1_id);
waiter.Wait();
}
// Install app2 PWA.
webapps::AppId app2_id;
Browser* app2_browser;
{
browser()->tab_strip_model()->ActivateTabAt(/*index=*/1);
EXPECT_EQ(GetAppMenuCommandState(IDC_CREATE_SHORTCUT, browser()), kEnabled);
EXPECT_EQ(GetAppMenuCommandState(IDC_INSTALL_PWA, browser()), kEnabled);
SetAutoAcceptPWAInstallConfirmationForTesting(/*auto_accept=*/true);
ui_test_utils::BrowserChangeObserver browser_change_observer(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
WebAppTestInstallObserver observer(profile());
observer.BeginListening();
CHECK(chrome::ExecuteCommand(browser(), IDC_INSTALL_PWA));
app2_id = observer.Wait();
app2_browser = browser_change_observer.Wait();
EXPECT_TRUE(AppBrowserController::IsForWebApp(app2_browser, app2_id));
SetAutoAcceptPWAInstallConfirmationForTesting(
/*auto_accept=*/false);
}
// App1 is open in a tab, but does not appear in the shelf.
EXPECT_EQ(
browser()->tab_strip_model()->GetActiveWebContents()->GetVisibleURL(),
app1_url);
EXPECT_EQ(
app2_browser->tab_strip_model()->GetActiveWebContents()->GetVisibleURL(),
app2_url);
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app2_id, static_cast<uint32_t>(ShelfItemState::kActive)));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app1_id, static_cast<uint32_t>(ShelfItemState::kNormal)));
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app_constants::kLacrosAppId,
static_cast<uint32_t>(ShelfItemState::kRunning)));
ASSERT_TRUE(browser_test_util::WaitForShelfItem(app1_id, /*exists=*/false));
}
// Tests that opening a new window for a tabbed web app opens a new window.
// TODO(crbug.com/40284715): Make this run on Ash as well.
IN_PROC_BROWSER_TEST_F(LacrosWebAppShelfBrowserTest, NewTabbedWindow) {
const std::optional<std::vector<std::string>>& capabilities =
chromeos::BrowserParamsProxy::Get()->AshCapabilities();
if (!capabilities || !base::Contains(*capabilities, "crbug/1490336")) {
GTEST_SKIP() << "Unsupported Ash version.";
}
if (!IsServiceAvailable()) {
GTEST_SKIP() << "Unsupported Ash version.";
}
auto& test_controller = chromeos::LacrosService::Get()
->GetRemote<crosapi::mojom::TestController>();
webapps::AppId app_id = InstallWebAppFromPage(
browser(),
https_server()->GetURL("/web_apps/tab_strip_customizations.html"));
const WebApp& app =
*WebAppProvider::GetForTest(profile())->registrar_unsafe().GetAppById(
app_id);
ASSERT_EQ(app.display_mode_override().front(), DisplayMode::kTabbed);
ASSERT_TRUE(absl::holds_alternative<blink::Manifest::HomeTabParams>(
app.tab_strip()->home_tab));
ui_test_utils::BrowserChangeObserver browser_observer(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
ASSERT_TRUE(browser_test_util::WaitForShelfItemState(
app_id, static_cast<uint32_t>(ShelfItemState::kActive)));
{
base::test::TestFuture<bool> future;
// Should select the "New window" menu item.
test_controller->SelectContextMenuForShelfItem(app_id, /*index=*/0,
future.GetCallback());
ASSERT_TRUE(future.Get());
}
EXPECT_TRUE(
AppBrowserController::IsForWebApp(browser_observer.Wait(), app_id));
}
} // namespace web_app