// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "ash/app_list/views/app_list_bubble_apps_page.h"
#include "ash/app_list/views/app_list_item_view.h"
#include "ash/app_list/views/apps_grid_view.h"
#include "ash/ash_element_identifiers.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "ash/public/cpp/shelf_item.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_app_button.h"
#include "ash/shelf/shelf_view.h"
#include "ash/shell.h"
#include "ash/system/status_area_widget.h"
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "base/base64.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/types/expected.h"
#include "chrome/browser/ash/app_list/app_list_client_impl.h"
#include "chrome/browser/ash/app_restore/full_restore_app_launch_handler.h"
#include "chrome/browser/ash/file_manager/app_id.h"
#include "chrome/browser/ash/login/test/guest_session_mixin.h"
#include "chrome/browser/ash/login/test/logged_in_user_mixin.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/chromeos/echo/echo_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_list_observer.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/web_applications/preinstalled_web_apps/container.h"
#include "chrome/browser/web_applications/preinstalled_web_apps/preinstalled_web_apps.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/mixin_based_in_process_browser_test.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/constants/chromeos_switches.h"
#include "components/app_constants/constants.h"
#include "components/session_manager/session_manager_types.h"
#include "components/sync/base/command_line_switches.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/test/browser_test.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/menu/menu_controller.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
namespace {
// Aliases.
using ::testing::AllOf;
using ::testing::Bool;
using ::testing::Conditional;
using ::testing::Contains;
using ::testing::Eq;
using ::testing::IsEmpty;
using ::testing::Not;
using ::testing::Pointer;
using ::testing::PrintToStringParamName;
using ::testing::Property;
using ::testing::ValuesIn;
using ::testing::WithParamInterface;
// Elements --------------------------------------------------------------------
// Identifiers.
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kBrowserWebContentsElementId);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kContainerAppWebContentsElementId);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kSettingsAppWebContentsElementId);
// Names.
inline char kAppsGridViewElementName[] = "AppsGridView";
inline char kAppListBubbleAppsPageElementName[] = "AppListBubbleAppsPage";
inline char kChromeAppElementName[] = "ChromeApp";
inline char kContainerAppElementName[] = "ContainerApp";
inline char kFilesAppElementName[] = "FilesApp";
inline char kGmailAppElementName[] = "GmailApp";
inline char kShowAppInfoMenuItemElementName[] = "ShowAppInfoMenuItem";
// Returns all `descendants` of the specified `parent` matching the given class.
template <typename ViewClass>
void FindDescendantsOfClass(views::View* parent,
std::vector<raw_ptr<ViewClass>>& descendants) {
for (views::View* child : parent->children()) {
if (views::IsViewClass<ViewClass>(child)) {
descendants.emplace_back(views::AsViewClass<ViewClass>(child));
}
FindDescendantsOfClass(child, descendants);
}
}
// Returns the index of `value` in the specified `range`.
template <typename Range, typename Value>
std::optional<size_t> FindIndex(const Range& range, const Value* value) {
auto it = base::ranges::find(range, value);
return it != range.end()
? std::make_optional<size_t>(std::distance(range.begin(), it))
: std::make_optional<size_t>();
}
// Returns the `views::MenuItemView`s for the currently showing menu.
std::vector<raw_ptr<views::MenuItemView>> FindMenuItemViews() {
if (auto* menu_controller = views::MenuController::GetActiveInstance()) {
if (auto* menu_item_view = menu_controller->GetSelectedMenuItem()) {
std::vector<raw_ptr<views::MenuItemView>> items;
FindDescendantsOfClass(menu_item_view->parent(), items);
return items;
}
}
return {};
}
// Returns the `views::MenuItemView` for the currently showing menu associated
// with the specified command `id`.
views::MenuItemView* FindMenuItemViewForCommand(int id) {
std::vector<raw_ptr<views::MenuItemView>> views = FindMenuItemViews();
auto it = base::ranges::find(views, id, &views::MenuItemView::GetCommand);
return it != views.end() ? *it : nullptr;
}
// Returns the `ash::ShelfItem` for the given web app `id`.
const ash::ShelfItem* FindShelfItemForWebApp(std::string_view id) {
const ash::ShelfItems& items = ash::ShelfModel::Get()->items();
auto it = base::ranges::find_if(
items, [id](const ash::ShelfItem& item) { return item.id.app_id == id; });
return it != items.end() ? &*it : nullptr;
}
// Returns if `view` is the `ash::AppListItemView` for the given web app `id`.
bool IsAppListItemViewForWebApp(std::string_view id, const views::View* view) {
return views::IsViewClass<ash::AppListItemView>(view) &&
views::AsViewClass<ash::AppListItemView>(view)->item()->id() == id;
}
// Returns if `browser` is the `Browser` for the given web app `id`.
bool IsBrowserForWebApp(const webapps::AppId& id, const Browser* browser) {
return web_app::AppBrowserController::IsForWebApp(browser, id);
}
// Returns if the menu is currently showing.
bool IsMenuShowing() {
auto* menu_controller = views::MenuController::GetActiveInstance();
return menu_controller && menu_controller->GetSelectedMenuItem();
}
// Returns if `view` is the `ash::ShelfAppButton` for the given web app `id`.
bool IsShelfAppButtonForWebApp(
std::reference_wrapper<const raw_ptr<ash::ShelfView>> shelf,
std::string_view id,
const views::View* view) {
const ash::ShelfItem* const item = FindShelfItemForWebApp(id);
return views::IsViewClass<ash::ShelfAppButton>(view) && item &&
shelf.get()->GetShelfAppButton(item->id) == view;
}
// Waiters ---------------------------------------------------------------------
// Class which waits for `BrowserListObserver::OnBrowserSetLastActive()` events.
class OnBrowserSetLastActiveWaiter : public BrowserListObserver {
public:
void Wait() {
CHECK(!run_loop_);
base::ScopedObservation<BrowserList, BrowserListObserver> observer(this);
observer.Observe(BrowserList::GetInstance());
run_loop_ = std::make_unique<base::RunLoop>(
base::RunLoop::Type::kNestableTasksAllowed);
run_loop_->Run();
run_loop_.reset();
}
private:
// BrowserListObserver:
void OnBrowserSetLastActive(Browser* browser) override {
CHECK(run_loop_);
run_loop_->Quit();
}
// Used to wait for `OnBrowserSetLastActive()` events.
std::unique_ptr<base::RunLoop> run_loop_;
};
} // namespace
// ContainerAppInteractiveUiTestBase -------------------------------------------
// Base class for interactive UI tests of the container app.
class ContainerAppInteractiveUiTestBase
: public InteractiveBrowserTestT<MixinBasedInProcessBrowserTest> {
public:
ContainerAppInteractiveUiTestBase(
std::optional<ash::LoggedInUserMixin::LogInType> login_type,
bool should_ignore_feature_debug_key)
: user_session_mixin_(CreateUserSessionMixin(login_type)) {
// Conditionally ignore the container app preinstallation debug key.
if (should_ignore_feature_debug_key) {
ignore_container_app_preinstall_debug_key_ =
std::make_unique<base::AutoReset<bool>>(
chromeos::switches::
SetIgnoreContainerAppPreinstallDebugKeyForTesting());
}
// Enable container app preinstallation.
scoped_feature_list_.InitWithFeatures(
{chromeos::features::kContainerAppPreinstall,
chromeos::features::kFeatureManagementContainerAppPreinstall},
{});
// Use a consistent context for element tracking. Otherwise each widget has
// its own context, greatly increasing the complexity of tracking
// cross-widget CUJs as is the case in this test suite.
views::ElementTrackerViews::SetContextOverrideCallback(
base::BindRepeating([](views::Widget* widget) {
return ui::ElementContext(ash::Shell::GetPrimaryRootWindow());
}));
}
// Returns a builder for a step which assigns the last active browser to the
// specified `ptr_ref`.
[[nodiscard]] auto AssignLastActiveBrowser(
std::reference_wrapper<Browser*> ptr_ref) {
return Do([ptr_ref]() {
ptr_ref.get() = BrowserList::GetInstance()->GetLastActive();
});
}
// Returns a builder for a step which assigns the view associated with the
// given `element_specifier` to the given `ptr`.
template <typename ViewClass>
[[nodiscard]] static auto AssignView(
ElementSpecifier element_specifier,
std::reference_wrapper<raw_ptr<ViewClass>> ptr) {
return WithView(element_specifier,
[ptr](ViewClass* view) { ptr.get() = view; });
}
// Returns the expected launch URL for the container app.
GURL GetContainerAppLaunchUrl() const {
GURL::Replacements components;
components.SetQueryStr(*container_app_install_info_->launch_query_params);
return container_app_install_info_->start_url().ReplaceComponents(
components);
}
// Returns the expected title for the container app.
const std::u16string& GetContainerAppTitle() const {
return container_app_install_info_->title;
}
// Returns a builder for a step which presses and releases the given `key`.
[[nodiscard]] static auto PressAndReleaseKey(ui::KeyboardCode key) {
return Do([key]() {
ui::test::EventGenerator(ash::Shell::GetPrimaryRootWindow())
.PressAndReleaseKeyAndModifierKeys(key, ui::EF_NONE);
});
}
// Returns a builder for a step which resets the specified `ptr`.
template <typename T>
[[nodiscard]] static auto Reset(std::reference_wrapper<raw_ptr<T>> ptr) {
return Do([ptr]() { ptr.get() = nullptr; });
}
// Returns a builder for a step which waits for a
// `BrowserList::OnBrowserSetLastActive()` event.
[[nodiscard]] auto WaitForOnBrowserSetLastActive() {
return Do([]() { OnBrowserSetLastActiveWaiter().Wait(); });
}
protected:
// InteractiveBrowserTestT<MixinBasedInProcessBrowserTest>:
void SetUpDefaultCommandLine(base::CommandLine* command_line) override {
InteractiveBrowserTestT<
MixinBasedInProcessBrowserTest>::SetUpDefaultCommandLine(command_line);
// Remove the `switches::kDisableDefaultApps` switch to ensure that default
// apps are installed. The container app is a default app.
command_line->RemoveSwitch(switches::kDisableDefaultApps);
// Disable sync as it would otherwise block updating of shelf pins.
command_line->AppendSwitch(syncer::kDisableSync);
}
void SetUpOnMainThread() override {
// There's nothing to do if not logging in the user.
if (!ShouldLogInUser()) {
InteractiveBrowserTestT<
MixinBasedInProcessBrowserTest>::SetUpOnMainThread();
return;
}
// For logged-in user sessions, perform login prior to
// `InteractiveBrowserTestT<>::SetUpOnMainThread()` so that the interactive
// browser test base class will successfully set the context widget for the
// test sequence. The context widget will be associated with the browser.
if (absl::holds_alternative<ash::LoggedInUserMixin>(user_session_mixin_)) {
absl::get<ash::LoggedInUserMixin>(user_session_mixin_).LogInUser();
}
InteractiveBrowserTestT<
MixinBasedInProcessBrowserTest>::SetUpOnMainThread();
// Wait for installation of both system and external web apps. The container
// app is an external app and this test suite will verify its adjacency to
// system web apps.
Profile* const profile = browser()->profile();
ash::SystemWebAppManager::GetForTest(profile)
->InstallSystemAppsForTesting();
web_app::test::WaitUntilWebAppProviderAndSubsystemsReady(
web_app::WebAppProvider::GetForTest(profile));
AppListClientImpl::GetInstance()->UpdateProfile();
// Fetch `device_info` from echo.
base::test::TestFuture<std::optional<base::Time>> oobe_timestamp;
chromeos::echo_util::GetOobeTimestamp(oobe_timestamp.GetCallback());
ASSERT_TRUE(oobe_timestamp.Wait());
ASSERT_TRUE(oobe_timestamp.Get().has_value());
web_app::DeviceInfo device_info;
device_info.oobe_timestamp = oobe_timestamp.Get().value();
// Cache install info for the container app.
container_app_install_info_ =
web_app::GetConfigForContainer(device_info).app_info_factory.Run();
}
private:
// Creates the appropriate guest or logged-in user session mixin based on
// the presence of `login_type`.
absl::variant<ash::GuestSessionMixin, ash::LoggedInUserMixin>
CreateUserSessionMixin(
std::optional<ash::LoggedInUserMixin::LogInType> login_type) {
if (!login_type) {
return absl::variant<ash::GuestSessionMixin, ash::LoggedInUserMixin>(
absl::in_place_type_t<ash::GuestSessionMixin>(), &mixin_host_);
}
return absl::variant<ash::GuestSessionMixin, ash::LoggedInUserMixin>(
absl::in_place_type_t<ash::LoggedInUserMixin>(), &mixin_host_,
/*test_base=*/this, embedded_test_server(), login_type.value());
}
// Returns whether the user should be logged in as part of test setup.
virtual bool ShouldLogInUser() const { return true; }
// Used to manage either a guest or logged-in user session based on test
// parameterization.
absl::variant<ash::GuestSessionMixin, ash::LoggedInUserMixin>
user_session_mixin_;
// Used to enable the container app preinstallation.
base::test::ScopedFeatureList scoped_feature_list_;
// Used to retrieve expected title/URL for the container app.
std::unique_ptr<web_app::WebAppInstallInfo> container_app_install_info_;
// Used to conditionally ignore the container app preinstallation debug key.
std::unique_ptr<base::AutoReset<bool>>
ignore_container_app_preinstall_debug_key_;
};
// ContainerAppInteractiveUiTest -----------------------------------------------
// Base class for interactive UI tests of the container app, parameterized by
// whether the logged-in user is new or existing. Tests include a PRE_ session,
// where user state is initialized, followed by a subsequent session containing
// test logic. Chrome is restarted between sessions.
class ContainerAppInteractiveUiTest
: public ContainerAppInteractiveUiTestBase,
public WithParamInterface</*existing_user=*/bool> {
public:
ContainerAppInteractiveUiTest()
: ContainerAppInteractiveUiTestBase(
ash::LoggedInUserMixin::LogInType::kConsumer,
/*should_ignore_feature_debug_key=*/false) {
// Disable the container app during the PRE_ session so that the subsequent
// session containing test logic is when the app preinstallation occurs.
if (IsPreSession()) {
scoped_feature_list_.InitAndDisableFeature(
chromeos::features::kContainerAppPreinstall);
}
}
protected:
// ContainerAppInteractiveUiTestBase:
void SetUpOnMainThread() override {
ContainerAppInteractiveUiTestBase::SetUpOnMainThread();
// Check that session state is as expected.
const auto* session_controller = ash::Shell::Get()->session_controller();
EXPECT_THAT(session_controller->GetSessionState(),
Conditional(ShouldLogInUser(),
Eq(session_manager::SessionState::ACTIVE),
Eq(session_manager::SessionState::LOGIN_PRIMARY)));
// Check that login state is as expected.
EXPECT_THAT(
session_controller->IsUserFirstLogin(),
Conditional(IsPreSession(), IsExistingUser(), Not(IsExistingUser())));
}
bool ShouldLogInUser() const override {
// Existing users should be logged in for both the PRE_ session and the
// subsequent session containing test logic. New users should only be logged
// in for the subsequent session.
return IsExistingUser() || !IsPreSession();
}
// Returns whether the logged-in user is existing given test parameterization.
bool IsExistingUser() const { return GetParam(); }
// Returns whether the current session is the PRE_ session. The PRE_ session
// is the session before the subsequent session containing test logic.
bool IsPreSession() const {
return base::StartsWith(
testing::UnitTest::GetInstance()->current_test_info()->name(), "PRE_");
}
private:
// Used to disable container app preinstallation for the PRE_ session.
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(
All,
ContainerAppInteractiveUiTest,
/*existing_user=*/Bool(),
[](const testing::TestParamInfo</*existing_user=*/bool>& info) {
return info.param ? "ExistingUser" : "NewUser";
});
// Tests -----------------------------------------------------------------------
// Initializes user state and restarts Chrome before `LaunchFromAppList`.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiTest, PRE_LaunchFromAppList) {}
// Verifies that the container app can be launched from the app list.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiTest, LaunchFromAppList) {
// Views.
raw_ptr<ash::AppsGridView> apps_grid_view = nullptr;
raw_ptr<ash::AppListItemView> container_app = nullptr;
raw_ptr<ash::AppListItemView> files_app = nullptr;
raw_ptr<ash::AppListItemView> gmail_app = nullptr;
// Test.
RunTestSequence(
// Launch app list.
DoDefaultAction(ash::kHomeButtonElementId),
WaitForShow(ash::kAppListBubbleViewElementId),
// Find apps page.
NameDescendantViewByType<ash::AppListBubbleAppsPage>(
ash::kAppListBubbleViewElementId, kAppListBubbleAppsPageElementName),
// Find apps grid.
NameDescendantViewByType<ash::AppsGridView>(
kAppListBubbleAppsPageElementName, kAppsGridViewElementName),
// Cache apps grid.
AssignView(kAppsGridViewElementName, std::ref(apps_grid_view)),
// Find container app.
NameDescendantView(kAppsGridViewElementName, kContainerAppElementName,
base::BindRepeating(&IsAppListItemViewForWebApp,
web_app::kContainerAppId)),
// Cache container app.
AssignView(kContainerAppElementName, std::ref(container_app)),
// Find Files app.
NameDescendantView(
kAppsGridViewElementName, kFilesAppElementName,
base::BindRepeating(&IsAppListItemViewForWebApp,
file_manager::kFileManagerSwaAppId)),
// Cache Files app.
AssignView(kFilesAppElementName, std::ref(files_app)),
// Find Gmail app.
NameDescendantView(kAppsGridViewElementName, kGmailAppElementName,
base::BindRepeating(&IsAppListItemViewForWebApp,
web_app::kGmailAppId)),
// Cache Gmail app.
AssignView(kGmailAppElementName, std::ref(gmail_app)),
// Check container app title.
CheckView(kContainerAppElementName,
base::BindOnce(&ash::AppListItemView::title),
Property(&views::Label::GetText, Eq(GetContainerAppTitle()))),
// Check container app position.
Check([&]() {
std::vector<raw_ptr<ash::AppListItemView>> apps;
FindDescendantsOfClass(apps_grid_view, apps);
const auto container_app_index = FindIndex(apps, container_app.get());
if (IsExistingUser()) {
return container_app_index == 0u;
}
const auto files_app_index = FindIndex(apps, files_app.get());
const auto gmail_app_index = FindIndex(apps, gmail_app.get());
return (files_app_index == container_app_index.value() - 1u) &&
(gmail_app_index == container_app_index.value() + 1u);
}),
// Reset cached pointers which might dangle when the app list closes.
Reset(std::ref(apps_grid_view)), Reset(std::ref(container_app)),
Reset(std::ref(files_app)), Reset(std::ref(gmail_app)),
// Launch container app.
InstrumentNextTab(kContainerAppWebContentsElementId, AnyBrowser()),
DoDefaultAction(kContainerAppElementName),
// Check container app browser.
CheckElement(kContainerAppWebContentsElementId,
base::BindOnce(&AsInstrumentedWebContents)
.Then(base::BindOnce(
&WebContentsInteractionTestUtil::web_contents))
.Then(base::BindOnce(&chrome::FindBrowserWithTab))
.Then(base::BindOnce(&IsBrowserForWebApp,
web_app::kContainerAppId))),
// Check container app launch URL.
WaitForWebContentsReady(kContainerAppWebContentsElementId,
GetContainerAppLaunchUrl()));
}
// Initializes user state and restarts Chrome before `LaunchFromShelf`.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiTest, PRE_LaunchFromShelf) {}
// Verifies that the container app can be launched from the shelf.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiTest, LaunchFromShelf) {
// Views.
raw_ptr<ash::ShelfAppButton> chrome_app = nullptr;
raw_ptr<ash::ShelfAppButton> container_app = nullptr;
raw_ptr<ash::ShelfAppButton> gmail_app = nullptr;
raw_ptr<ash::ShelfView> shelf = nullptr;
// Test.
RunTestSequence(
// Cache shelf.
AssignView(ash::kShelfViewElementId, std::ref(shelf)),
// Find container app.
NameDescendantView(
ash::kShelfViewElementId, kContainerAppElementName,
base::BindRepeating(&IsShelfAppButtonForWebApp, std::cref(shelf),
web_app::kContainerAppId)),
// Cache container app.
AssignView(kContainerAppElementName, std::ref(container_app)),
// Find Chrome app.
NameDescendantView(
ash::kShelfViewElementId, kChromeAppElementName,
base::BindRepeating(&IsShelfAppButtonForWebApp, std::cref(shelf),
app_constants::kChromeAppId)),
// Cache Chrome app.
AssignView(kChromeAppElementName, std::ref(chrome_app)),
// Find Gmail app.
NameDescendantView(
ash::kShelfViewElementId, kGmailAppElementName,
base::BindRepeating(&IsShelfAppButtonForWebApp, std::cref(shelf),
web_app::kGmailAppId)),
// Cache Gmail app.
AssignView(kGmailAppElementName, std::ref(gmail_app)),
// Check container app position.
Check([&]() {
std::vector<raw_ptr<ash::ShelfAppButton>> apps;
FindDescendantsOfClass(shelf, apps);
const auto container_app_index = FindIndex(apps, container_app.get());
if (IsExistingUser()) {
return container_app_index == 0u;
}
const auto chrome_app_index = FindIndex(apps, chrome_app.get());
const auto gmail_app_index = FindIndex(apps, gmail_app.get());
return (chrome_app_index == container_app_index.value() - 1u) &&
(gmail_app_index == container_app_index.value() + 1u);
}),
// Launch container app.
InstrumentNextTab(kContainerAppWebContentsElementId, AnyBrowser()),
DoDefaultAction(kContainerAppElementName),
// Check container app browser.
CheckElement(kContainerAppWebContentsElementId,
base::BindOnce(&AsInstrumentedWebContents)
.Then(base::BindOnce(
&WebContentsInteractionTestUtil::web_contents))
.Then(base::BindOnce(&chrome::FindBrowserWithTab))
.Then(base::BindOnce(&IsBrowserForWebApp,
web_app::kContainerAppId))),
// Check container app launch URL.
WaitForWebContentsReady(kContainerAppWebContentsElementId,
GetContainerAppLaunchUrl()));
}
// Initializes user state and restarts Chrome before
// `PreferredAppForSupportedLinks`.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiTest,
PRE_PreferredAppForSupportedLinks) {}
// Verifies that the container app is the preferred app for supported links.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiTest,
PreferredAppForSupportedLinks) {
// Browser.
Browser* container_app_browser = nullptr;
// Test.
RunTestSequence(
// Navigate browser to page with supported link.
AddInstrumentedTab(
kBrowserWebContentsElementId,
GURL(base::StrCat({"data:text/html;base64,",
base::Base64Encode(base::ReplaceStringPlaceholders(
R"(<DOCTYPE html>
<html>
<head>
<style>
html, body, a {
display: block;
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<a href="$1" target="_blank"></a>
</body>
</html>)",
/*subst=*/{GetContainerAppLaunchUrl().spec()},
/*offsets=*/nullptr))}))),
// Launch container app via supported link.
MoveMouseTo(kBrowserViewElementId), ClickMouse(),
// Instrument container app browser.
WaitForOnBrowserSetLastActive(),
AssignLastActiveBrowser(std::ref(container_app_browser)),
InstrumentTab(kContainerAppWebContentsElementId,
/*tab_index=*/std::nullopt,
std::ref(container_app_browser)),
// Check container app browser.
CheckElement(kContainerAppWebContentsElementId,
base::BindOnce(&AsInstrumentedWebContents)
.Then(base::BindOnce(
&WebContentsInteractionTestUtil::web_contents))
.Then(base::BindOnce(&chrome::FindBrowserWithTab))
.Then(base::BindOnce(&IsBrowserForWebApp,
web_app::kContainerAppId))),
// Check container app launch URL.
WaitForWebContentsReady(kContainerAppWebContentsElementId,
GetContainerAppLaunchUrl()));
}
// Initializes user state and restarts Chrome before `UninstallFromAppList`.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiTest,
PRE_UninstallFromAppList) {}
// Verifies that the container app cannot be uninstalled from the app list.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiTest, UninstallFromAppList) {
RunTestSequence(
// Launch app list.
DoDefaultAction(ash::kHomeButtonElementId),
WaitForShow(ash::kAppListBubbleViewElementId),
// Find apps page.
NameDescendantViewByType<ash::AppListBubbleAppsPage>(
ash::kAppListBubbleViewElementId, kAppListBubbleAppsPageElementName),
// Find apps grid.
NameDescendantViewByType<ash::AppsGridView>(
kAppListBubbleAppsPageElementName, kAppsGridViewElementName),
// Find container app.
NameDescendantView(kAppsGridViewElementName, kContainerAppElementName,
base::BindRepeating(&IsAppListItemViewForWebApp,
web_app::kContainerAppId)),
// Open menu.
MoveMouseTo(kContainerAppElementName), ClickMouse(ui_controls::RIGHT),
Check(&IsMenuShowing),
// Activate menu.
PressAndReleaseKey(ui::VKEY_DOWN),
// Check container app cannot be uninstalled.
CheckResult(
&FindMenuItemViews,
AllOf(Not(IsEmpty()),
Not(Contains(Pointer(Property(&views::MenuItemView::GetCommand,
Eq(ash::UNINSTALL))))))));
}
// Initializes user state and restarts Chrome before `UninstallFromSettings`.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiTest,
PRE_UninstallFromSettings) {}
// Verifies that the container app cannot be uninstalled from the Settings app.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiTest, UninstallFromSettings) {
// Views.
raw_ptr<ash::ShelfView> shelf = nullptr;
// Queries.
auto get_settings_app_subpage_query = [](std::string_view query,
bool shadow_dom = false) {
constexpr char kOsSettingsAppsPage[] = "os-settings-apps-page";
constexpr char kOsSettingsMain[] = "os-settings-main";
constexpr char kOsSettingsMainPageContainer[] = "#mainPageContainer";
constexpr char kOsSettingsSubpage[] = "os-settings-subpage";
constexpr char kOsSettingsUi[] = "os-settings-ui";
const DeepQuery deep_query({kOsSettingsUi, kOsSettingsMain,
kOsSettingsMainPageContainer,
kOsSettingsAppsPage});
return shadow_dom
? (deep_query + kOsSettingsSubpage) + std::string(query)
: (deep_query + base::StrCat({kOsSettingsSubpage, " ", query}));
};
// Test.
RunTestSequence(
// Cache shelf.
AssignView(ash::kShelfViewElementId, std::ref(shelf)),
// Find container app.
NameDescendantView(
ash::kShelfViewElementId, kContainerAppElementName,
base::BindRepeating(&IsShelfAppButtonForWebApp, std::cref(shelf),
web_app::kContainerAppId)),
// Open menu.
MoveMouseTo(kContainerAppElementName), ClickMouse(ui_controls::RIGHT),
Check(&IsMenuShowing),
// Activate menu.
PressAndReleaseKey(ui::VKEY_DOWN),
// Find menu item.
NameView(kShowAppInfoMenuItemElementName,
base::BindOnce(FindMenuItemViewForCommand, ash::SHOW_APP_INFO)
.Then(base::BindOnce<views::View*(views::View*)>(
views::AsViewClass<views::View>))),
// Launch Settings app.
InstrumentNextTab(kSettingsAppWebContentsElementId, AnyBrowser()),
DoDefaultAction(kShowAppInfoMenuItemElementName),
// Check Settings app browser.
CheckElement(kSettingsAppWebContentsElementId,
base::BindOnce(&AsInstrumentedWebContents)
.Then(base::BindOnce(
&WebContentsInteractionTestUtil::web_contents))
.Then(base::BindOnce(&chrome::FindBrowserWithTab))
.Then(base::BindOnce(&IsBrowserForWebApp,
web_app::kOsSettingsAppId))),
// Check Settings app launch URL.
WaitForWebContentsReady(
kSettingsAppWebContentsElementId,
chrome::GetOSSettingsUrl(
base::StrCat({chromeos::settings::mojom::kAppDetailsSubpagePath,
"?id=", web_app::kContainerAppId}))),
// Check container app title.
CheckJsResultAt(
kSettingsAppWebContentsElementId,
get_settings_app_subpage_query("#subpageTitle", /*shadow_dom=*/true),
base::StrCat({"subpageTitle => subpageTitle.innerText === '",
base::UTF16ToUTF8(GetContainerAppTitle()), "'"})),
// Check container app cannot be uninstalled.
CheckJsResultAt(
kSettingsAppWebContentsElementId,
get_settings_app_subpage_query("app-management-uninstall-button"),
"appManagementUninstallButton => "
"!appManagementUninstallButton.shadowRoot.querySelector('*[role="
"button]')"));
}
// Initializes user state and restarts Chrome before `UninstallFromShelf`.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiTest, PRE_UninstallFromShelf) {}
// Verifies that the container app cannot be uninstalled from the shelf.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiTest, UninstallFromShelf) {
// Views.
raw_ptr<ash::ShelfView> shelf = nullptr;
// Test.
RunTestSequence(
// Cache shelf.
AssignView(ash::kShelfViewElementId, std::ref(shelf)),
// Find container app.
NameDescendantView(
ash::kShelfViewElementId, kContainerAppElementName,
base::BindRepeating(&IsShelfAppButtonForWebApp, std::cref(shelf),
web_app::kContainerAppId)),
// Open menu.
MoveMouseTo(kContainerAppElementName), ClickMouse(ui_controls::RIGHT),
Check(&IsMenuShowing),
// Activate menu.
PressAndReleaseKey(ui::VKEY_DOWN),
// Check container app cannot be uninstalled.
CheckResult(
&FindMenuItemViews,
AllOf(Not(IsEmpty()),
Not(Contains(Pointer(Property(&views::MenuItemView::GetCommand,
Eq(ash::UNINSTALL))))))));
}
// ContainerAppInteractiveUiIneligibilityTest ----------------------------------
// Reasons why the user may be ineligible for container app preinstallation.
enum class IneligibilityReason {
kMinValue = 0,
kFeatureDebugAndManagementFlagsDisabled = kMinValue,
kFeatureDebugKeyAbsent,
kFeatureDebugKeyEmpty,
kFeatureDebugKeyIncorrect,
kFeatureFlagDisabled,
kUserManaged,
kUserTypeChild,
kUserTypeGuest,
kMaxValue = kUserTypeGuest,
};
#define INELIGIBILITY_REASON_CASE(reason) \
case IneligibilityReason::reason: \
return os << std::string(#reason)
inline std::ostream& operator<<(std::ostream& os, IneligibilityReason reason) {
switch (reason) {
INELIGIBILITY_REASON_CASE(kFeatureDebugAndManagementFlagsDisabled);
INELIGIBILITY_REASON_CASE(kFeatureDebugKeyAbsent);
INELIGIBILITY_REASON_CASE(kFeatureDebugKeyEmpty);
INELIGIBILITY_REASON_CASE(kFeatureDebugKeyIncorrect);
INELIGIBILITY_REASON_CASE(kFeatureFlagDisabled);
INELIGIBILITY_REASON_CASE(kUserManaged);
INELIGIBILITY_REASON_CASE(kUserTypeChild);
INELIGIBILITY_REASON_CASE(kUserTypeGuest);
}
}
// Base class for interactive UI tests of container app ineligibility.
class ContainerAppInteractiveUiIneligibilityTest
: public ContainerAppInteractiveUiTestBase,
public WithParamInterface<IneligibilityReason> {
public:
ContainerAppInteractiveUiIneligibilityTest()
: ContainerAppInteractiveUiTestBase(GetLoginType(),
ShouldIgnoreFeatureDebugKey()) {
scoped_feature_list_.InitWithFeatureStates(
{{chromeos::features::kContainerAppPreinstall, IsFeatureFlagEnabled()},
{chromeos::features::kContainerAppPreinstallDebug,
IsFeatureDebugFlagEnabled()},
{chromeos::features::kFeatureManagementContainerAppPreinstall,
IsFeatureManagementFlagEnabled()}});
}
private:
// ContainerAppInteractiveUiTestBase:
void SetUpDefaultCommandLine(base::CommandLine* command_line) override {
ContainerAppInteractiveUiTestBase::SetUpDefaultCommandLine(command_line);
// Feature debug key.
if (IsFeatureDebugKeyEmpty()) {
command_line->AppendSwitchASCII(
chromeos::switches::kContainerAppPreinstallDebugKey,
base::EmptyString());
} else if (IsFeatureDebugKeyIncorrect()) {
command_line->AppendSwitchASCII(
chromeos::switches::kContainerAppPreinstallDebugKey,
"<INCORRECT_KEY>");
}
}
void SetUpOnMainThread() override {
// Web app preinstallation times out for child user types due to failure to
// install some default web apps. Since this test suite only cares about the
// preinstallation of the container app, circumvent timeouts by disabling
// other default web apps.
std::unique_ptr<web_app::ScopedTestingPreinstalledAppData> app_data;
if (GetLoginType() == ash::LoggedInUserMixin::LogInType::kChild) {
app_data = std::make_unique<web_app::ScopedTestingPreinstalledAppData>();
app_data->apps.emplace_back(
web_app::GetConfigForContainer(/*device_info=*/std::nullopt));
}
ContainerAppInteractiveUiTestBase::SetUpOnMainThread();
}
// Returns the login type for the user given test parameterization.
std::optional<ash::LoggedInUserMixin::LogInType> GetLoginType() const {
switch (GetParam()) {
case IneligibilityReason::kUserTypeChild:
return ash::LoggedInUserMixin::LogInType::kChild;
case IneligibilityReason::kUserTypeGuest:
return std::nullopt;
case IneligibilityReason::kUserManaged:
return ash::LoggedInUserMixin::LogInType::kManaged;
default:
return ash::LoggedInUserMixin::LogInType::kConsumer;
}
}
// Returns whether the feature debug flag is enabled given test
// parameterization.
bool IsFeatureDebugFlagEnabled() const {
return GetParam() !=
IneligibilityReason::kFeatureDebugAndManagementFlagsDisabled;
}
// Returns whether the feature debug key is empty given test
// parameterization.
bool IsFeatureDebugKeyEmpty() const {
return GetParam() == IneligibilityReason::kFeatureDebugKeyEmpty;
}
// Returns whether the feature debug key is incorrect given test
// parameterization.
bool IsFeatureDebugKeyIncorrect() const {
return GetParam() == IneligibilityReason::kFeatureDebugKeyIncorrect;
}
// Returns whether the feature flag is enabled given test parameterization.
bool IsFeatureFlagEnabled() const {
return GetParam() != IneligibilityReason::kFeatureFlagDisabled;
}
// Returns whether the feature management flag is enabled given test
// parameterization.
bool IsFeatureManagementFlagEnabled() const {
// Disable the feature management flag when attempting to enable the feature
// via the debug flag. Otherwise the debug flag/key will not be considered.
if (!ShouldIgnoreFeatureDebugKey()) {
return false;
}
return GetParam() !=
IneligibilityReason::kFeatureDebugAndManagementFlagsDisabled;
}
// Returns whether the feature debug key should be ignored given test
// parameterization.
bool ShouldIgnoreFeatureDebugKey() const {
return !std::set<IneligibilityReason>(
{IneligibilityReason::kFeatureDebugKeyAbsent,
IneligibilityReason::kFeatureDebugKeyEmpty,
IneligibilityReason::kFeatureDebugKeyIncorrect})
.contains(GetParam());
}
// Used to enable/disable the container app preinstallation based on test
// parameterization.
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(
All,
ContainerAppInteractiveUiIneligibilityTest,
ValuesIn(([]() {
std::vector<IneligibilityReason> reasons(
static_cast<int>(IneligibilityReason::kMaxValue) -
static_cast<int>(IneligibilityReason::kMinValue) + 1);
base::ranges::generate(
reasons.begin(), reasons.end(),
[i = static_cast<int>(IneligibilityReason::kMinValue)]() mutable {
return static_cast<IneligibilityReason>(i++);
});
return reasons;
})()),
PrintToStringParamName());
// Tests -----------------------------------------------------------------------
// Verifies the container app is absent from the app list for ineligible users.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiIneligibilityTest,
AbsentFromAppList) {
RunTestSequence(
// Launch app list.
DoDefaultAction(ash::kHomeButtonElementId),
WaitForShow(ash::kAppListBubbleViewElementId),
// Find apps page.
NameDescendantViewByType<ash::AppListBubbleAppsPage>(
ash::kAppListBubbleViewElementId, kAppListBubbleAppsPageElementName),
// Find apps grid.
NameDescendantViewByType<ash::AppsGridView>(
kAppListBubbleAppsPageElementName, kAppsGridViewElementName),
// Check container app absent.
CheckView(
kAppsGridViewElementName, [](ash::AppsGridView* apps_grid_view) {
std::vector<raw_ptr<ash::AppListItemView>> apps;
FindDescendantsOfClass(apps_grid_view, apps);
return apps.size() &&
base::ranges::none_of(apps, [&](ash::AppListItemView* app) {
return IsAppListItemViewForWebApp(web_app::kContainerAppId,
app);
});
}));
}
// Verifies the container app is absent from the shelf for ineligible users.
IN_PROC_BROWSER_TEST_P(ContainerAppInteractiveUiIneligibilityTest,
AbsentFromShelf) {
RunTestSequence(
// Check container app absent.
CheckView(ash::kShelfViewElementId, [](ash::ShelfView* shelf) {
std::vector<raw_ptr<ash::ShelfAppButton>> apps;
FindDescendantsOfClass(shelf, apps);
return apps.size() &&
base::ranges::none_of(
apps, [&, shelf = raw_ptr(shelf)](ash::ShelfAppButton* app) {
return IsShelfAppButtonForWebApp(
std::cref(shelf), web_app::kContainerAppId, app);
});
}));
}