// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/app_list/app_list_client_impl.h"
#include <stddef.h>
#include <memory>
#include <optional>
#include <vector>
#include "ash/app_list/apps_collections_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/app_list/app_list_metrics.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/public/cpp/test/app_list_test_api.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/test/active_window_waiter.h"
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/memory/raw_ptr.h"
#include "base/one_shot_event.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/run_until.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "chrome/browser/apps/app_service/app_launch_params.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app_registry_cache.h"
#include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
#include "chrome/browser/ash/app_list/app_list_controller_delegate.h"
#include "chrome/browser/ash/app_list/app_list_model_updater.h"
#include "chrome/browser/ash/app_list/app_list_model_updater_observer.h"
#include "chrome/browser/ash/app_list/app_list_survey_handler.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service_factory.h"
#include "chrome/browser/ash/app_list/app_list_test_util.h"
#include "chrome/browser/ash/app_list/chrome_app_list_item.h"
#include "chrome/browser/ash/app_list/chrome_app_list_model_updater.h"
#include "chrome/browser/ash/app_list/search/search_controller.h"
#include "chrome/browser/ash/app_list/search/test/app_list_search_test_helper.h"
#include "chrome/browser/ash/app_list/search/test/search_results_changed_waiter.h"
#include "chrome/browser/ash/app_list/test/chrome_app_list_test_support.h"
#include "chrome/browser/ash/file_manager/app_id.h"
#include "chrome/browser/ash/hats/hats_config.h"
#include "chrome/browser/ash/hats/hats_notification_controller.h"
#include "chrome/browser/ash/login/demo_mode/demo_mode_test_utils.h"
#include "chrome/browser/ash/login/demo_mode/demo_session.h"
#include "chrome/browser/ash/login/login_manager_test.h"
#include "chrome/browser/ash/login/test/login_manager_mixin.h"
#include "chrome/browser/ash/login/ui/user_adding_screen.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/notifications/notification_display_service_tester.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
#include "chrome/browser/ui/ash/shelf/shelf_controller_helper.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "chromeos/ash/components/standalone_browser/feature_refs.h"
#include "components/account_id/account_id.h"
#include "components/app_constants/constants.h"
#include "components/browser_sync/browser_sync_switches.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/package_id.h"
#include "components/user_manager/user_manager.h"
#include "components/user_manager/user_names.h"
#include "components/user_manager/user_type.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/common/constants.h"
#include "ui/aura/window.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/display/display.h"
#include "ui/display/scoped_display_for_new_windows.h"
#include "ui/display/screen.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/wm/core/window_util.h"
// Browser Test for AppListClientImpl.
using AppListClientImplBrowserTest = extensions::PlatformAppBrowserTest;
using ::testing::Invoke;
using ::testing::NiceMock;
namespace {
const apps::PackageId kTestPackageId =
apps::PackageId(apps::PackageType::kArc, "com.test.package");
class TestObserver : public app_list::AppListSyncableService::Observer {
public:
explicit TestObserver(app_list::AppListSyncableService* syncable_service) {
observer_.Observe(syncable_service);
}
TestObserver(const TestObserver&) = delete;
TestObserver& operator=(const TestObserver&) = delete;
~TestObserver() override = default;
size_t add_or_update_count() const { return add_or_update_count_; }
// app_list::AppListSyncableService::Observer:
void OnSyncModelUpdated() override {}
void OnAddOrUpdateFromSyncItemForTest() override { ++add_or_update_count_; }
private:
base::ScopedObservation<app_list::AppListSyncableService,
app_list::AppListSyncableService::Observer>
observer_{this};
size_t add_or_update_count_ = 0;
};
// A fake for AppListSyncableService that allows easy modifications.
class AppListSyncableServiceFake : public app_list::AppListSyncableService {
public:
AppListSyncableServiceFake(Profile* profile,
bool was_first_sync_ever,
base::OneShotEvent* on_first_sync)
: app_list::AppListSyncableService(profile),
on_first_sync_(on_first_sync),
was_first_sync_ever_(was_first_sync_ever) {}
~AppListSyncableServiceFake() override = default;
AppListSyncableServiceFake(const AppListSyncableServiceFake&) = delete;
AppListSyncableServiceFake& operator=(const AppListSyncableServiceFake&) =
delete;
void OnFirstSync(
base::OnceCallback<void(bool was_first_sync_ever)> callback) override {
on_first_sync_->Post(
FROM_HERE, base::BindOnce(std::move(callback), was_first_sync_ever_));
}
// The event to signal when the first app list sync in the session has been
// completed.
const raw_ptr<base::OneShotEvent> on_first_sync_;
bool was_first_sync_ever_;
};
} // namespace
// Test AppListClient::IsAppOpen for extension apps.
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest, IsExtensionAppOpen) {
AppListControllerDelegate* delegate = AppListClientImpl::GetInstance();
EXPECT_FALSE(delegate->IsAppOpen("fake_extension_app_id"));
base::FilePath extension_path = test_data_dir_.AppendASCII("app");
const extensions::Extension* extension_app = LoadExtension(extension_path);
ASSERT_NE(nullptr, extension_app);
EXPECT_FALSE(delegate->IsAppOpen(extension_app->id()));
{
content::CreateAndLoadWebContentsObserver app_loaded_observer;
apps::AppServiceProxyFactory::GetForProfile(profile())->Launch(
extension_app->id(),
apps::GetEventFlags(WindowOpenDisposition::NEW_WINDOW,
false /* preferred_containner */),
apps::LaunchSource::kFromTest,
std::make_unique<apps::WindowInfo>(
display::Screen::GetScreen()->GetPrimaryDisplay().id()));
app_loaded_observer.Wait();
}
EXPECT_TRUE(delegate->IsAppOpen(extension_app->id()));
}
// Test AppListClient::IsAppOpen for platform apps.
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest, IsPlatformAppOpen) {
AppListControllerDelegate* delegate = AppListClientImpl::GetInstance();
EXPECT_FALSE(delegate->IsAppOpen("fake_platform_app_id"));
const extensions::Extension* app = InstallPlatformApp("minimal");
EXPECT_FALSE(delegate->IsAppOpen(app->id()));
{
content::CreateAndLoadWebContentsObserver app_loaded_observer;
LaunchPlatformApp(app);
app_loaded_observer.Wait();
}
EXPECT_TRUE(delegate->IsAppOpen(app->id()));
}
// Test UninstallApp for platform apps.
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest, UninstallApp) {
AppListClientImpl* client = AppListClientImpl::GetInstance();
const extensions::Extension* app = InstallPlatformApp("minimal");
auto* app_service_proxy =
apps::AppServiceProxyFactory::GetForProfile(browser()->profile());
ASSERT_TRUE(app_service_proxy);
// Bring up the app list.
EXPECT_FALSE(client->GetAppListWindow());
client->ShowAppList(ash::AppListShowSource::kSearchKey);
ash::AppListTestApi().WaitForBubbleWindow(
/*wait_for_opening_animation=*/false);
EXPECT_TRUE(client->GetAppListWindow());
EXPECT_TRUE(wm::GetTransientChildren(client->GetAppListWindow()).empty());
// Open the uninstall dialog.
base::RunLoop run_loop;
app_service_proxy->UninstallForTesting(
app->id(), client->GetAppListWindow(),
base::BindLambdaForTesting([&](bool) { run_loop.Quit(); }));
run_loop.Run();
EXPECT_FALSE(wm::GetTransientChildren(client->GetAppListWindow()).empty());
// The app list should not be dismissed when the dialog is shown.
EXPECT_TRUE(client->app_list_visible());
EXPECT_TRUE(client->GetAppListWindow());
}
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest, ShowAppInfo) {
ash::SystemWebAppManager::GetForTest(profile())
->InstallSystemAppsForTesting();
AppListClientImpl* client = AppListClientImpl::GetInstance();
const extensions::Extension* app = InstallPlatformApp("minimal");
// Bring up the app list.
EXPECT_FALSE(client->GetAppListWindow());
client->ShowAppList(ash::AppListShowSource::kSearchKey);
EXPECT_TRUE(client->GetAppListWindow());
EXPECT_TRUE(wm::GetTransientChildren(client->GetAppListWindow()).empty());
// Open the app info dialog.
ui_test_utils::BrowserChangeObserver browser_opened(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
client->DoShowAppInfoFlow(profile(), app->id());
browser_opened.Wait();
Browser* settings_app =
chrome::SettingsWindowManager::GetInstance()->FindBrowserForProfile(
profile());
EXPECT_TRUE(content::WaitForLoadStop(
settings_app->tab_strip_model()->GetActiveWebContents()));
EXPECT_EQ(
chrome::GetOSSettingsUrl(
base::StrCat({chromeos::settings::mojom::kAppDetailsSubpagePath,
"?id=", app->id()})),
settings_app->tab_strip_model()->GetActiveWebContents()->GetVisibleURL());
// The app list should be dismissed when the dialog is shown.
EXPECT_FALSE(client->app_list_visible());
EXPECT_FALSE(client->GetAppListWindow());
}
// Test the CreateNewWindow function of the controller delegate.
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest, CreateNewWindow) {
AppListClientImpl* client = AppListClientImpl::GetInstance();
AppListControllerDelegate* controller = client;
ASSERT_TRUE(controller);
EXPECT_EQ(1U, chrome::GetBrowserCount(browser()->profile()));
EXPECT_EQ(0U,
chrome::GetBrowserCount(browser()->profile()->GetPrimaryOTRProfile(
/*create_if_needed=*/true)));
controller->CreateNewWindow(/*incognito=*/false,
/*should_trigger_session_restore=*/true);
EXPECT_EQ(2U, chrome::GetBrowserCount(browser()->profile()));
controller->CreateNewWindow(/*incognito=*/true,
/*should_trigger_session_restore=*/true);
EXPECT_EQ(1U,
chrome::GetBrowserCount(browser()->profile()->GetPrimaryOTRProfile(
/*create_if_needed=*/true)));
}
// When getting activated, SelfDestroyAppItem has itself removed from the
// model updater.
class SelfDestroyAppItem : public ChromeAppListItem {
public:
SelfDestroyAppItem(Profile* profile,
const std::string& app_id,
AppListModelUpdater* model_updater)
: ChromeAppListItem(profile, app_id, model_updater),
updater_(model_updater) {}
~SelfDestroyAppItem() override = default;
// ChromeAppListItem:
void Activate(int event_flags) override {
updater_->RemoveItem(id(), /*is_uninstall=*/true);
}
private:
raw_ptr<AppListModelUpdater> updater_;
};
// Verifies that activating an app item which destroys itself during activation
// will not cause crash (see https://crbug.com/990282).
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest, ActivateSelfDestroyApp) {
AppListClientImpl* client = AppListClientImpl::GetInstance();
client->UpdateProfile();
ASSERT_TRUE(client);
AppListModelUpdater* model_updater = test::GetModelUpdater(client);
// Add an app item which destroys itself during activation.
const std::string app_id("fake_id");
model_updater->AddItem(std::make_unique<SelfDestroyAppItem>(
browser()->profile(), app_id, model_updater));
ChromeAppListItem* item = model_updater->FindItem(app_id);
ASSERT_TRUE(item);
// Activates |item|.
client->ActivateItem(/*profile_id=*/0, item->id(), /*event_flags=*/0,
ash::AppListLaunchedFrom::kLaunchedFromGrid,
/*is_above_the_fold=*/true);
}
// Verifies that the first app activation by a new user is recorded.
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest,
AppActivationShouldBeRecorded) {
AppListClientImpl* client = AppListClientImpl::GetInstance();
client->UpdateProfile();
// Emulate that the current user is new.
client->InitializeAsIfNewUserLoginForTest();
TestObserver syncable_service_observer(
app_list::AppListSyncableServiceFactory::GetInstance()->GetForProfile(
profile()));
// Add an app item.
AppListModelUpdater* model_updater = test::GetModelUpdater(client);
const std::string app_id("fake_id");
auto new_item = std::make_unique<ChromeAppListItem>(browser()->profile(),
app_id, model_updater);
new_item->SetChromeName("Fake app");
model_updater->AddItem(std::move(new_item));
// Verify that the app addition from the app list client side should not
// trigger the update recursively, i.e. the client side observers the update
// in the app list model then reacts to it.
EXPECT_EQ(0u, syncable_service_observer.add_or_update_count());
base::HistogramTester histogram_tester;
// Verify that app activation is recorded.
client->ShowAppList(ash::AppListShowSource::kSearchKey);
ChromeAppListItem* item = model_updater->FindItem(app_id);
ASSERT_TRUE(item);
client->ActivateItem(/*profile_id=*/0, item->id(), /*event_flags=*/0,
ash::AppListLaunchedFrom::kLaunchedFromGrid,
/*is_above_the_fold=*/true);
histogram_tester.ExpectBucketCount(
"Apps.NewUserFirstLauncherAction.ClamshellMode",
static_cast<int>(ash::AppListLaunchedFrom::kLaunchedFromGrid),
/*expected_bucket_count=*/1);
histogram_tester.ExpectTotalCount(
"Apps.TimeBetweenNewUserSessionActivationAndFirstLauncherAction."
"ClamshellMode",
/*expected_bucket_count=*/1);
// Verify that only the first app activation is recorded.
client->ActivateItem(/*profile_id=*/0, item->id(), /*event_flags=*/0,
ash::AppListLaunchedFrom::kLaunchedFromGrid,
/*is_above_the_fold=*/true);
histogram_tester.ExpectBucketCount(
"Apps.NewUserFirstLauncherAction.ClamshellMode",
static_cast<int>(ash::AppListLaunchedFrom::kLaunchedFromGrid),
/*expected_bucket_count=*/1);
histogram_tester.ExpectTotalCount(
"Apps.TimeBetweenNewUserSessionActivationAndFirstLauncherAction."
"ClamshellMode",
/*expected_bucket_count=*/1);
}
// Test that all the items in the context menu for a hosted app have valid
// labels.
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest, ShowContextMenu) {
AppListClientImpl* client = AppListClientImpl::GetInstance();
EXPECT_TRUE(client);
// Show the app list to ensure it has loaded a profile.
client->ShowAppList(ash::AppListShowSource::kSearchKey);
AppListModelUpdater* model_updater = test::GetModelUpdater(client);
EXPECT_TRUE(model_updater);
// Get the webstore hosted app, which is always present.
ChromeAppListItem* item = model_updater->FindItem(extensions::kWebStoreAppId);
EXPECT_TRUE(item);
base::RunLoop run_loop;
std::unique_ptr<ui::SimpleMenuModel> menu_model;
item->GetContextMenuModel(
ash::AppListItemContext::kNone,
base::BindLambdaForTesting(
[&](std::unique_ptr<ui::SimpleMenuModel> created_menu) {
menu_model = std::move(created_menu);
run_loop.Quit();
}));
run_loop.Run();
EXPECT_TRUE(menu_model);
size_t num_items = menu_model->GetItemCount();
EXPECT_GT(num_items, 0u);
for (size_t i = 0; i < num_items; i++) {
if (menu_model->GetTypeAt(i) == ui::MenuModel::TYPE_SEPARATOR)
continue;
std::u16string label = menu_model->GetLabelAt(i);
EXPECT_FALSE(label.empty());
}
}
class AppListClientImplBrowserPromiseAppTest
: public AppListClientImplBrowserTest,
public AppListModelUpdaterObserver {
public:
AppListClientImplBrowserPromiseAppTest() {
feature_list_.InitWithFeatures({ash::features::kPromiseIcons}, {});
}
// extensions::PlatformAppBrowserTest:
void SetUpOnMainThread() override {
extensions::PlatformAppBrowserTest::SetUpOnMainThread();
AppListClientImpl* client = AppListClientImpl::GetInstance();
ASSERT_TRUE(client);
client->UpdateProfile();
test::GetModelUpdater(client)->AddObserver(this);
}
void TearDownOnMainThread() override {
AppListClientImpl* client = AppListClientImpl::GetInstance();
ASSERT_TRUE(client);
test::GetModelUpdater(client)->RemoveObserver(this);
extensions::PlatformAppBrowserTest::TearDownOnMainThread();
}
apps::AppServiceProxy* app_service_proxy() {
return apps::AppServiceProxyFactory::GetForProfile(profile());
}
apps::PromiseAppRegistryCache* cache() {
return app_service_proxy()->PromiseAppRegistryCache();
}
// AppListModelUpdaterObserver:
void OnAppListItemUpdated(ChromeAppListItem* item) override {
last_updated_metadata_ = item->CloneMetadata();
updates_++;
}
ash::AppListItemMetadata* GetMetadataFromLastUpdate() {
return last_updated_metadata_.get();
}
int GetAndResetUpdateCount() {
int cached_updates = updates_;
updates_ = 0;
return cached_updates;
}
private:
int updates_ = 0;
std::unique_ptr<ash::AppListItemMetadata> last_updated_metadata_;
base::test::ScopedFeatureList feature_list_;
};
// Tests that progress updates from promise apps registry are reflected into the
// launcher.
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserPromiseAppTest,
PromiseAppsInLauncher) {
std::string app_name = "Long App Name";
AppListClientImpl* client = AppListClientImpl::GetInstance();
EXPECT_TRUE(client);
// Register a promise app in the promise app registry cache.
apps::PromiseAppPtr promise_app =
std::make_unique<apps::PromiseApp>(kTestPackageId);
promise_app->status = apps::PromiseStatus::kPending;
promise_app->name = app_name;
promise_app->should_show = true;
cache()->OnPromiseApp(std::move(promise_app));
// Show the app list to ensure it has loaded a profile.
client->ShowAppList(ash::AppListShowSource::kSearchKey);
AppListModelUpdater* model_updater = test::GetModelUpdater(client);
EXPECT_TRUE(model_updater);
ChromeAppListItem* item = model_updater->FindItem(kTestPackageId.ToString());
ASSERT_TRUE(item);
EXPECT_EQ(item->progress(), 0);
EXPECT_EQ(item->app_status(), ash::AppStatus::kPending);
ASSERT_EQ(item->name(),
base::UTF16ToUTF8(ShelfControllerHelper::GetLabelForPromiseStatus(
apps::PromiseStatus::kPending)));
ASSERT_EQ(item->accessible_name(),
base::UTF16ToUTF8(
ShelfControllerHelper::GetAccessibleLabelForPromiseStatus(
app_name, apps::PromiseStatus::kPending)));
GetAndResetUpdateCount();
// Update the promise app in the promise app registry cache.
apps::PromiseAppPtr update =
std::make_unique<apps::PromiseApp>(kTestPackageId);
update->progress = 0.3;
update->status = apps::PromiseStatus::kInstalling;
cache()->OnPromiseApp(std::move(update));
// Verify that OnAppListItemUpdated was called four times:
// For accessible name, for name, for progress and for app_status.
EXPECT_EQ(4, GetAndResetUpdateCount());
// Promise app item should have updated fields.
EXPECT_EQ(item->progress(), 0.3f);
EXPECT_EQ(item->app_status(), ash::AppStatus::kInstalling);
EXPECT_EQ(item->name(),
base::UTF16ToUTF8(ShelfControllerHelper::GetLabelForPromiseStatus(
apps::PromiseStatus::kInstalling)));
ASSERT_EQ(item->accessible_name(),
base::UTF16ToUTF8(
ShelfControllerHelper::GetAccessibleLabelForPromiseStatus(
app_name, apps::PromiseStatus::kInstalling)));
// Register (i.e. "install") an app with a matching package ID. This should
// trigger removal of the promise app.
std::string app_id = "asdfghjkl";
apps::AppPtr app = std::make_unique<apps::App>(apps::AppType::kArc, app_id);
app->publisher_id = kTestPackageId.identifier();
app->readiness = apps::Readiness::kReady;
std::vector<apps::AppPtr> apps;
apps.push_back(std::move(app));
app_service_proxy()->OnApps(std::move(apps), apps::AppType::kArc,
/*should_notify_initialized=*/false);
// Verify that the promise app was updated correctly into a successful status
// before it was removed.
ash::AppListItemMetadata* metadata_before_removal =
GetMetadataFromLastUpdate();
EXPECT_EQ(1, GetAndResetUpdateCount());
EXPECT_EQ(ash::AppStatus::kInstallSuccess,
metadata_before_removal->app_status);
EXPECT_FALSE(model_updater->FindItem(kTestPackageId.ToString()));
}
// Test that OpenSearchResult that dismisses app list runs fine without
// use-after-free.
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest, OpenSearchResult) {
AppListClientImpl* client = AppListClientImpl::GetInstance();
ASSERT_TRUE(client);
// Emulate that the current user is new.
client->InitializeAsIfNewUserLoginForTest();
// Associate |client| with the current profile.
client->UpdateProfile();
// Show the launcher.
client->ShowAppList(ash::AppListShowSource::kSearchKey);
ash::AppListTestApi().WaitForBubbleWindow(
/*wait_for_opening_animation=*/false);
AppListModelUpdater* model_updater = test::GetModelUpdater(client);
ASSERT_TRUE(model_updater);
app_list::SearchController* search_controller = client->search_controller();
ASSERT_TRUE(search_controller);
// Any app that opens a window to dismiss app list is good enough for this
// test.
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
const std::string app_title = "chrome";
#else
const std::string app_title = "chromium";
#endif // !BUILDFLAG(GOOGLE_CHROME_BRANDING)
const std::string app_result_id =
"chrome-extension://mgndgikekgjfcpckkfioiadnlibdjbkf/";
// Search by title and the app must present in the results.
ash::AppListTestApi().SimulateSearch(base::UTF8ToUTF16(app_title));
ASSERT_TRUE(search_controller->FindSearchResult(app_result_id));
// Expect that the browser window is not minimized.
ASSERT_FALSE(browser()->window()->IsMinimized());
// Open the app result.
base::HistogramTester histogram_tester;
client->OpenSearchResult(model_updater->model_id(), app_result_id,
ui::EF_NONE,
ash::AppListLaunchedFrom::kLaunchedFromSearchBox,
ash::AppListLaunchType::kAppSearchResult, 0,
false /* launch_as_default */);
// Expect that opening the result from the search box is recorded.
histogram_tester.ExpectBucketCount(
"Apps.OpenedAppListSearchResultFromSearchBoxV2."
"ExistNonAppBrowserWindowOpenAndNotMinimized",
static_cast<int>(ash::EXTENSION_APP),
/*expected_bucket_count=*/1);
// Verify that opening the app result is recorded.
histogram_tester.ExpectBucketCount(
"Apps.NewUserFirstLauncherAction.ClamshellMode",
static_cast<int>(ash::AppListLaunchedFrom::kLaunchedFromSearchBox),
/*expected_bucket_count=*/1);
histogram_tester.ExpectTotalCount(
"Apps.TimeBetweenNewUserSessionActivationAndFirstLauncherAction."
"ClamshellMode",
/*expected_bucket_count=*/1);
// App list should be dismissed.
EXPECT_FALSE(client->app_list_target_visibility());
// Minimize the browser. Then show the app list and open the app result.
browser()->window()->Minimize();
client->ShowAppList(ash::AppListShowSource::kSearchKey);
client->OpenSearchResult(model_updater->model_id(), app_result_id,
ui::EF_NONE,
ash::AppListLaunchedFrom::kLaunchedFromSearchBox,
ash::AppListLaunchType::kAppSearchResult, 0,
false /* launch_as_default */);
// Expect that opening the result from the search box is recorded.
histogram_tester.ExpectBucketCount(
"Apps.OpenedAppListSearchResultFromSearchBoxV2."
"NonAppBrowserWindowsEitherClosedOrMinimized",
static_cast<int>(ash::EXTENSION_APP),
/*expected_bucket_count=*/1);
// Needed to let AppLaunchEventLogger finish its work on worker thread.
// Otherwise, its |weak_factory_| is released on UI thread and causing
// the bound WeakPtr to fail sequence check on a worker thread.
// TODO(crbug.com/41459944): Remove after fixing AppLaunchEventLogger.
content::RunAllTasksUntilIdle();
}
// TODO(crbug.com/335362001): Re-enable this test.
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_OpenSearchResultOnPrimaryDisplay \
DISABLED_OpenSearchResultOnPrimaryDisplay
#else
#define MAYBE_OpenSearchResultOnPrimaryDisplay OpenSearchResultOnPrimaryDisplay
#endif
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest,
MAYBE_OpenSearchResultOnPrimaryDisplay) {
display::test::DisplayManagerTestApi display_manager(
ash::ShellTestApi().display_manager());
display_manager.UpdateDisplay("400x300,500x400");
const display::Display& primary_display =
display::Screen::GetScreen()->GetPrimaryDisplay();
AppListClientImpl* const client = AppListClientImpl::GetInstance();
ASSERT_TRUE(client);
// Associate |client| with the current profile.
client->UpdateProfile();
EXPECT_EQ(display::kInvalidDisplayId, client->GetAppListDisplayId());
aura::Window* const primary_root_window =
ash::Shell::GetRootWindowForDisplayId(primary_display.id());
client->ShowAppList(ash::AppListShowSource::kSearchKey);
ash::AppListTestApi().WaitForBubbleWindowInRootWindow(
primary_root_window,
/*wait_for_opening_animation=*/true);
EXPECT_EQ(primary_display.id(), client->GetAppListDisplayId());
// Simluate search, and verify an activated search result opens on the
// primary display.
const std::u16string app_query = u"Chrom";
const std::string app_id = app_constants::kChromeAppId;
const std::string app_result_id =
base::StringPrintf("chrome-extension://%s/", app_id.c_str());
app_list::SearchResultsChangedWaiter results_changed_waiter(
AppListClientImpl::GetInstance()->search_controller(),
{app_list::ResultType::kInstalledApp});
app_list::ResultsWaiter results_waiter(app_query);
// Search by title and the app must present in the results.
ash::AppListTestApi().SimulateSearch(app_query);
results_changed_waiter.Wait();
results_waiter.Wait();
app_list::SearchController* const search_controller =
client->search_controller();
ASSERT_TRUE(search_controller);
ASSERT_TRUE(search_controller->FindSearchResult(app_result_id));
AppListModelUpdater* model_updater = test::GetModelUpdater(client);
ASSERT_TRUE(model_updater);
ash::ActiveWindowWaiter window_waiter(primary_root_window);
client->OpenSearchResult(model_updater->model_id(), app_result_id,
ui::EF_NONE,
ash::AppListLaunchedFrom::kLaunchedFromSearchBox,
ash::AppListLaunchType::kAppSearchResult, 0,
false /* launch_as_default */);
aura::Window* const app_window = window_waiter.Wait();
ASSERT_TRUE(app_window);
EXPECT_EQ(primary_root_window, app_window->GetRootWindow());
EXPECT_EQ(app_id,
ash::ShelfID::Deserialize(app_window->GetProperty(ash::kShelfIDKey))
.app_id);
}
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest,
OpenSearchResultOnSecondaryDisplay) {
display::test::DisplayManagerTestApi display_manager(
ash::ShellTestApi().display_manager());
display_manager.UpdateDisplay("400x300,500x400");
const display::Display& secondary_display =
display_manager.GetSecondaryDisplay();
AppListClientImpl* const client = AppListClientImpl::GetInstance();
ASSERT_TRUE(client);
// Associate |client| with the current profile.
client->UpdateProfile();
EXPECT_EQ(display::kInvalidDisplayId, client->GetAppListDisplayId());
aura::Window* const secondary_root_window =
ash::Shell::GetRootWindowForDisplayId(secondary_display.id());
// Open app list on a secondary display.
{
display::ScopedDisplayForNewWindows scoped_display(secondary_display.id());
client->ShowAppList(ash::AppListShowSource::kSearchKey);
ash::AppListTestApi().WaitForBubbleWindowInRootWindow(
secondary_root_window,
/*wait_for_opening_animation=*/true);
}
EXPECT_EQ(secondary_display.id(), client->GetAppListDisplayId());
// Simluate search, and verify an activated search result opens on the
// secondary display.
const std::u16string app_query = u"Chrom";
const std::string app_id = app_constants::kChromeAppId;
const std::string app_result_id =
base::StringPrintf("chrome-extension://%s/", app_id.c_str());
app_list::SearchResultsChangedWaiter results_changed_waiter(
AppListClientImpl::GetInstance()->search_controller(),
{app_list::ResultType::kInstalledApp});
app_list::ResultsWaiter results_waiter(app_query);
// Search by title and the app must present in the results.
ash::AppListTestApi().SimulateSearch(app_query);
results_changed_waiter.Wait();
results_waiter.Wait();
app_list::SearchController* const search_controller =
client->search_controller();
ASSERT_TRUE(search_controller);
ASSERT_TRUE(search_controller->FindSearchResult(app_result_id));
AppListModelUpdater* model_updater = test::GetModelUpdater(client);
ASSERT_TRUE(model_updater);
ash::ActiveWindowWaiter window_waiter(secondary_root_window);
client->OpenSearchResult(model_updater->model_id(), app_result_id,
ui::EF_NONE,
ash::AppListLaunchedFrom::kLaunchedFromSearchBox,
ash::AppListLaunchType::kAppSearchResult, 0,
false /* launch_as_default */);
aura::Window* const app_window = window_waiter.Wait();
ASSERT_TRUE(app_window);
EXPECT_EQ(secondary_root_window, app_window->GetRootWindow());
EXPECT_EQ(app_id,
ash::ShelfID::Deserialize(app_window->GetProperty(ash::kShelfIDKey))
.app_id);
// Open app list on the primary display, and verify `GetAppListDisplayId()`
// returns the display where the launcher is shown.
{
display::ScopedDisplayForNewWindows scoped_display(
display::Screen::GetScreen()->GetPrimaryDisplay().id());
client->ShowAppList(ash::AppListShowSource::kSearchKey);
ash::AppListTestApi().WaitForBubbleWindow(
/*wait_for_opening_animation=*/true);
}
EXPECT_EQ(display::Screen::GetScreen()->GetPrimaryDisplay().id(),
client->GetAppListDisplayId());
}
class AppListClientImplLacrosOnlyBrowserTest
: public AppListClientImplBrowserTest {
public:
AppListClientImplLacrosOnlyBrowserTest() {
feature_list_.InitWithFeatures(ash::standalone_browser::GetFeatureRefs(),
{});
scoped_command_line_.GetProcessCommandLine()->AppendSwitch(
ash::switches::kEnableLacrosForTesting);
}
private:
base::test::ScopedFeatureList feature_list_;
base::test::ScopedCommandLine scoped_command_line_;
};
IN_PROC_BROWSER_TEST_F(AppListClientImplLacrosOnlyBrowserTest, ChromeApp) {
AppListControllerDelegate* delegate = AppListClientImpl::GetInstance();
ASSERT_TRUE(delegate);
ASSERT_TRUE(profile());
EXPECT_EQ(
extensions::LAUNCH_TYPE_INVALID,
delegate->GetExtensionLaunchType(profile(), app_constants::kChromeAppId));
}
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest, ChromeApp) {
AppListControllerDelegate* delegate = AppListClientImpl::GetInstance();
ASSERT_TRUE(delegate);
ASSERT_TRUE(profile());
EXPECT_EQ(
extensions::LAUNCH_TYPE_REGULAR,
delegate->GetExtensionLaunchType(profile(), app_constants::kChromeAppId));
}
// Test that browser launch time is recorded is recorded in preferences.
// This is important for suggested apps sorting.
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest,
BrowserLaunchTimeRecorded) {
AppListClientImpl* client = AppListClientImpl::GetInstance();
AppListControllerDelegate* controller = client;
ASSERT_TRUE(controller);
Profile* profile = browser()->profile();
Profile* profile_otr =
profile->GetPrimaryOTRProfile(/*create_if_needed=*/true);
extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile);
// Starting with just one regular browser.
EXPECT_EQ(1U, chrome::GetBrowserCount(profile));
EXPECT_EQ(0U, chrome::GetBrowserCount(profile_otr));
// First browser launch time should be recorded.
const base::Time time_recorded1 =
prefs->GetLastLaunchTime(app_constants::kChromeAppId);
EXPECT_NE(base::Time(), time_recorded1);
// Create an incognito browser so that we can close the regular one without
// exiting the test.
controller->CreateNewWindow(/*incognito=*/true,
/*should_trigger_session_restore=*/true);
EXPECT_EQ(1U, chrome::GetBrowserCount(profile_otr));
// Creating incognito browser should not update the launch time.
EXPECT_EQ(time_recorded1,
prefs->GetLastLaunchTime(app_constants::kChromeAppId));
// Close the regular browser.
CloseBrowserSynchronously(chrome::FindBrowserWithProfile(profile));
EXPECT_EQ(0U, chrome::GetBrowserCount(profile));
// Recorded the launch time should not update.
EXPECT_EQ(time_recorded1,
prefs->GetLastLaunchTime(app_constants::kChromeAppId));
// Launch another regular browser.
const base::Time time_before_launch = base::Time::Now();
controller->CreateNewWindow(/*incognito=*/false,
/*should_trigger_session_restore=*/true);
const base::Time time_after_launch = base::Time::Now();
EXPECT_EQ(1U, chrome::GetBrowserCount(profile));
const base::Time time_recorded2 =
prefs->GetLastLaunchTime(app_constants::kChromeAppId);
EXPECT_LE(time_before_launch, time_recorded2);
EXPECT_GE(time_after_launch, time_recorded2);
// Creating a second regular browser should not update the launch time.
controller->CreateNewWindow(/*incognito=*/false,
/*should_trigger_session_restore=*/true);
EXPECT_EQ(2U, chrome::GetBrowserCount(profile));
EXPECT_EQ(time_recorded2,
prefs->GetLastLaunchTime(app_constants::kChromeAppId));
}
// Verifies that apps visibility is correctly calculated.
IN_PROC_BROWSER_TEST_F(AppListClientImplBrowserTest, AppsVisibility) {
AppListClientImpl* client = AppListClientImpl::GetInstance();
EXPECT_TRUE(client);
client->UpdateProfile();
// Show the app list to ensure it has loaded a profile.
client->ShowAppList(ash::AppListShowSource::kSearchKey);
AppListModelUpdater* model_updater = test::GetModelUpdater(client);
EXPECT_TRUE(model_updater);
// Get the webstore hosted app.
ChromeAppListItem* item = model_updater->FindItem(extensions::kWebStoreAppId);
EXPECT_TRUE(item);
// Fetch the correct histogram name.
base::HistogramTester histogram_tester;
const std::string apps_collections_state =
ash::AppsCollectionsController::Get()
->GetUserExperimentalArmAsHistogramSuffix();
const std::string histogram_prefix =
"Apps.AppListBubble.AppsPage.AppLaunchesByVisibility";
histogram_tester.ExpectTotalCount(
base::StrCat({histogram_prefix, ".AboveTheFold", apps_collections_state}),
0);
histogram_tester.ExpectTotalCount(
base::StrCat({histogram_prefix, ".BelowTheFold", apps_collections_state}),
0);
// Activates web store as if it was activated below the fold.
client->ActivateItem(/*profile_id=*/0, item->id(), /*event_flags=*/0,
ash::AppListLaunchedFrom::kLaunchedFromGrid,
/*is_above_the_fold=*/false);
histogram_tester.ExpectTotalCount(
base::StrCat({histogram_prefix, ".AboveTheFold", apps_collections_state}),
0);
histogram_tester.ExpectTotalCount(
base::StrCat({histogram_prefix, ".BelowTheFold", apps_collections_state}),
1);
// Activates web store as if it was activated above the fold.
client->ActivateItem(/*profile_id=*/0, item->id(), /*event_flags=*/0,
ash::AppListLaunchedFrom::kLaunchedFromGrid,
/*is_above_the_fold=*/true);
histogram_tester.ExpectTotalCount(
base::StrCat({histogram_prefix, ".AboveTheFold", apps_collections_state}),
1);
histogram_tester.ExpectTotalCount(
base::StrCat({histogram_prefix, ".BelowTheFold", apps_collections_state}),
1);
}
// Browser Test for AppListClient that observes search result changes.
using AppListClientSearchResultsBrowserTest = extensions::ExtensionBrowserTest;
// Test showing search results, and uninstalling one of them while displayed.
IN_PROC_BROWSER_TEST_F(AppListClientSearchResultsBrowserTest,
UninstallSearchResult) {
base::FilePath test_extension_path;
ASSERT_TRUE(
base::PathService::Get(chrome::DIR_TEST_DATA, &test_extension_path));
test_extension_path = test_extension_path.AppendASCII("extensions")
.AppendASCII("platform_apps")
.AppendASCII("minimal");
AppListClientImpl* client = AppListClientImpl::GetInstance();
ASSERT_TRUE(client);
// Associate |client| with the current profile.
client->UpdateProfile();
AppListModelUpdater* model_updater = test::GetModelUpdater(client);
ASSERT_TRUE(model_updater);
app_list::SearchController* search_controller = client->search_controller();
ASSERT_TRUE(search_controller);
// Install the extension.
const extensions::Extension* extension = InstallExtension(
test_extension_path, 1 /* expected_change: new install */);
ASSERT_TRUE(extension);
const std::string title = extension->name();
// Show the app list first, otherwise we won't have a search box to update.
client->ShowAppList(ash::AppListShowSource::kSearchKey);
ash::AppListTestApi().WaitForBubbleWindow(
/*wait_for_opening_animation=*/false);
// Currently the search box is empty, so we have no result.
EXPECT_FALSE(search_controller->GetResultByTitleForTest(title));
// Now a search finds the extension.
ash::AppListTestApi().SimulateSearch(base::UTF8ToUTF16(title));
EXPECT_TRUE(search_controller->GetResultByTitleForTest(title));
// Uninstall the extension.
UninstallExtension(extension->id());
// Allow async callbacks to run.
base::RunLoop().RunUntilIdle();
// We cannot find the extension any more.
EXPECT_FALSE(search_controller->GetResultByTitleForTest(title));
client->DismissView();
}
class AppListClientGuestModeBrowserTest : public InProcessBrowserTest {
public:
AppListClientGuestModeBrowserTest() = default;
AppListClientGuestModeBrowserTest(const AppListClientGuestModeBrowserTest&) =
delete;
AppListClientGuestModeBrowserTest& operator=(
const AppListClientGuestModeBrowserTest&) = delete;
protected:
void SetUpCommandLine(base::CommandLine* command_line) override;
};
void AppListClientGuestModeBrowserTest::SetUpCommandLine(
base::CommandLine* command_line) {
command_line->AppendSwitch(ash::switches::kGuestSession);
command_line->AppendSwitchASCII(ash::switches::kLoginUser,
user_manager::kGuestUserName);
command_line->AppendSwitchASCII(ash::switches::kLoginProfile,
TestingProfile::kTestUserProfileDir);
command_line->AppendSwitch(switches::kIncognito);
}
// Test creating the initial app list in guest mode.
IN_PROC_BROWSER_TEST_F(AppListClientGuestModeBrowserTest, Incognito) {
AppListClientImpl* client = AppListClientImpl::GetInstance();
EXPECT_TRUE(client->GetCurrentAppListProfile());
client->ShowAppList(ash::AppListShowSource::kSearchKey);
EXPECT_EQ(browser()->profile(), client->GetCurrentAppListProfile());
}
class AppListAppLaunchTest : public extensions::ExtensionBrowserTest {
protected:
AppListAppLaunchTest() : extensions::ExtensionBrowserTest() {
histogram_tester_ = std::make_unique<base::HistogramTester>();
}
AppListAppLaunchTest(const AppListAppLaunchTest&) = delete;
AppListAppLaunchTest& operator=(const AppListAppLaunchTest&) = delete;
~AppListAppLaunchTest() override = default;
// InProcessBrowserTest:
void SetUpOnMainThread() override {
extensions::ExtensionBrowserTest::SetUpOnMainThread();
AppListClientImpl* app_list = AppListClientImpl::GetInstance();
EXPECT_TRUE(app_list);
// Need to set the profile to get the model updater.
app_list->UpdateProfile();
model_updater_ = app_list->GetModelUpdaterForTest();
EXPECT_TRUE(model_updater_);
}
void LaunchChromeAppListItem(const std::string& id) {
model_updater_->FindItem(id)->PerformActivate(ui::EF_NONE);
}
// Captures histograms.
std::unique_ptr<base::HistogramTester> histogram_tester_;
private:
raw_ptr<AppListModelUpdater, DanglingUntriaged> model_updater_;
};
IN_PROC_BROWSER_TEST_F(AppListAppLaunchTest,
NoDemoModeAppLaunchSourceReported) {
EXPECT_FALSE(ash::DemoSession::IsDeviceInDemoMode());
LaunchChromeAppListItem(app_constants::kChromeAppId);
// Should see 0 apps launched from the Launcher in the histogram when not in
// Demo mode.
histogram_tester_->ExpectTotalCount("DemoMode.AppLaunchSource", 0);
}
IN_PROC_BROWSER_TEST_F(AppListAppLaunchTest, DemoModeAppLaunchSourceReported) {
ash::test::LockDemoDeviceInstallAttributes();
EXPECT_TRUE(ash::DemoSession::IsDeviceInDemoMode());
// Should see 0 apps launched from the Launcher in the histogram at first.
histogram_tester_->ExpectTotalCount("DemoMode.AppLaunchSource", 0);
// Launch chrome browser from the Launcher. The same mechanism
// (ChromeAppListItem) is used for all types of apps
// (ARC, extension, etc), so launching just the browser suffices
// to test all these cases.
LaunchChromeAppListItem(app_constants::kChromeAppId);
// Should see 1 app launched from the Launcher in the histogram.
histogram_tester_->ExpectUniqueSample(
"DemoMode.AppLaunchSource", ash::DemoSession::AppLaunchSource::kAppList,
1);
}
// Verifies that the duration between login and the first launcher showing by
// a new account is recorded correctly.
class DurationBetweenSeesionActivationAndFirstLauncherShowingBrowserTest
: public ash::LoginManagerTest {
public:
DurationBetweenSeesionActivationAndFirstLauncherShowingBrowserTest() {
login_mixin_.AppendRegularUsers(2);
new_user_id_ = login_mixin_.users()[0].account_id;
registered_user_id_ = login_mixin_.users()[1].account_id;
}
~DurationBetweenSeesionActivationAndFirstLauncherShowingBrowserTest()
override = default;
protected:
void ShowAppListAndVerify() {
auto* client = AppListClientImpl::GetInstance();
client->ShowAppList(ash::AppListShowSource::kSearchKey);
ash::AppListTestApi().WaitForBubbleWindow(
/*wait_for_opening_animation=*/false);
ASSERT_TRUE(client->app_list_visible());
}
// ash::LoginManagerTest:
void SetUpOnMainThread() override {
ash::LoginManagerTest::SetUpOnMainThread();
// Emulate to sign in to a new account. It is time-consuming for an end to
// end test, i.e. the test covering the whole process from OOBE flow to
// showing the launcher. Therefore we set the current user to be new
// explicitly.
LoginUser(new_user_id_);
user_manager::UserManager::Get()->SetIsCurrentUserNew(true);
AppListClientImpl::GetInstance()->InitializeAsIfNewUserLoginForTest();
}
AccountId new_user_id_;
AccountId registered_user_id_;
ash::LoginManagerMixin login_mixin_{&mixin_host_};
};
IN_PROC_BROWSER_TEST_F(
DurationBetweenSeesionActivationAndFirstLauncherShowingBrowserTest,
MetricRecordedOnNewAccount) {
base::HistogramTester tester;
ShowAppListAndVerify();
tester.ExpectTotalCount(
"Apps.TimeDurationBetweenNewUserSessionActivationAndFirstLauncherOpening."
"ClamshellMode",
1);
}
// The duration between OOBE and the first launcher showing should not be
// recorded if the current user is pre-registered.
IN_PROC_BROWSER_TEST_F(
DurationBetweenSeesionActivationAndFirstLauncherShowingBrowserTest,
MetricNotRecordedOnRegisteredAccount) {
ash::UserAddingScreen::Get()->Start();
// Verify that the launcher usage state is recorded when switching accounts.
base::HistogramTester tester;
AddUser(registered_user_id_);
// Verify that the metric is not recorded.
ShowAppListAndVerify();
tester.ExpectTotalCount(
"Apps.TimeDurationBetweenNewUserSessionActivationAndFirstLauncherOpening."
"ClamshellMode",
0);
}
// The duration between OOBE and the first launcher showing should not be
// recorded if a user signs in to a new account, switches to another account
// then switches back to the new account.
IN_PROC_BROWSER_TEST_F(
DurationBetweenSeesionActivationAndFirstLauncherShowingBrowserTest,
MetricNotRecordedAfterUserSwitch) {
// Switch to a registered user account then switch back.
ash::UserAddingScreen::Get()->Start();
AddUser(registered_user_id_);
user_manager::UserManager::Get()->SwitchActiveUser(new_user_id_);
// Verify that the metric is not recorded.
base::HistogramTester tester;
ShowAppListAndVerify();
tester.ExpectTotalCount(
"Apps.TimeDurationBetweenNewUserSessionActivationAndFirstLauncherOpening."
"ClamshellMode",
0);
}
// Verifies that the duration between login and the first time apps collections
// is shown by a new account is recorded correctly.
class DurationBetweenSeesionActivationAndAppsCollectionsShowingBrowserTest
: public DurationBetweenSeesionActivationAndFirstLauncherShowingBrowserTest {
public:
DurationBetweenSeesionActivationAndAppsCollectionsShowingBrowserTest()
: DurationBetweenSeesionActivationAndFirstLauncherShowingBrowserTest() {
feature_list_.InitWithFeatures({app_list_features::kAppsCollections}, {});
}
~DurationBetweenSeesionActivationAndAppsCollectionsShowingBrowserTest()
override = default;
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(
DurationBetweenSeesionActivationAndAppsCollectionsShowingBrowserTest,
MetricRecordedOnNewAccount) {
base::HistogramTester tester;
ShowAppListAndVerify();
tester.ExpectTotalCount(
"Apps.TimeDurationBetweenNewUserSessionActivationAndAppsCollectionShown",
1);
}
// The duration between OOBE and the first launcher with apps collections
// showing should not be recorded if the current user is pre-registered.
IN_PROC_BROWSER_TEST_F(
DurationBetweenSeesionActivationAndAppsCollectionsShowingBrowserTest,
MetricNotRecordedOnRegisteredAccount) {
ash::UserAddingScreen::Get()->Start();
// Verify that the launcher usage state is recorded when switching accounts.
base::HistogramTester tester;
AddUser(registered_user_id_);
// Verify that the metric is not recorded.
ShowAppListAndVerify();
tester.ExpectTotalCount(
"Apps.TimeDurationBetweenNewUserSessionActivationAndAppsCollectionShown",
0);
}
// The duration between OOBE and the first launcher with apps collections
// showing should not be recorded if a user signs in to a new account, switches
// to another account then switches back to the new account.
IN_PROC_BROWSER_TEST_F(
DurationBetweenSeesionActivationAndAppsCollectionsShowingBrowserTest,
MetricNotRecordedAfterUserSwitch) {
// Switch to a registered user account then switch back.
ash::UserAddingScreen::Get()->Start();
AddUser(registered_user_id_);
user_manager::UserManager::Get()->SwitchActiveUser(new_user_id_);
// Verify that the metric is not recorded.
base::HistogramTester tester;
ShowAppListAndVerify();
tester.ExpectTotalCount(
"Apps.TimeDurationBetweenNewUserSessionActivationAndAppsCollectionShown",
0);
}
class AppListClientNewUserTest : public InProcessBrowserTest,
public testing::WithParamInterface<bool> {
public:
AppListClientNewUserTest() = default;
~AppListClientNewUserTest() override = default;
public:
// Returns the event to signal when the first app list sync in the session has
// been completed.
base::OneShotEvent& on_first_sync() { return on_first_sync_; }
// Returns whether the first app list sync in the session was the first sync
// ever across all ChromeOS devices and sessions for the given user, based on
// test parameterization.
bool was_first_sync_ever() const { return GetParam(); }
// Returns the `AccountId` for the primary `profile()`.
const AccountId& account_id() const { return account_id_; }
private:
// InProcessBrowserTest:
void SetUpOnMainThread() override {
SetUpEnvironment();
// Inject the testing profile into the client, since once a user session was
// created, with one browser, the client stops observing the profile
// manager.
AppListClientImpl::GetInstance()->OnProfileAdded(profile_);
InProcessBrowserTest::SetUpOnMainThread();
}
// Sets up profile and user manager. Should be called only once on test setup.
void SetUpEnvironment() {
account_id_ = AccountId::FromUserEmailGaiaId("test@test-user", "gaia-id");
TestingProfile::Builder profile_builder;
profile_builder.AddTestingFactory(
app_list::AppListSyncableServiceFactory::GetInstance(),
base::BindLambdaForTesting([&](content::BrowserContext* browser_context)
-> std::unique_ptr<KeyedService> {
return std::make_unique<AppListSyncableServiceFake>(
Profile::FromBrowserContext(browser_context),
was_first_sync_ever(), &on_first_sync_);
}));
profile_builder.SetProfileName("test@test-user");
profile_builder.SetPath(
ash::BrowserContextHelper::Get()->GetBrowserContextPathByUserIdHash(
user_manager::FakeUserManager::GetFakeUsernameHash(account_id_)));
std::unique_ptr<TestingProfile> testing_profile = profile_builder.Build();
profile_ = testing_profile.get();
g_browser_process->profile_manager()->RegisterTestingProfile(
std::move(testing_profile), true);
auto user_manager = std::make_unique<ash::FakeChromeUserManager>();
user_manager->AddUserWithAffiliationAndTypeAndProfile(
account_id_, true, user_manager::UserType::kRegular, profile_);
user_manager->LoginUser(account_id_);
user_manager_enabler_ = std::make_unique<user_manager::ScopedUserManager>(
std::move(user_manager));
}
void TearDownOnMainThread() override {
profile_ = nullptr;
base::RunLoop().RunUntilIdle();
user_manager_enabler_.reset();
InProcessBrowserTest::TearDownOnMainThread();
}
std::unique_ptr<user_manager::ScopedUserManager> user_manager_enabler_;
// The event to signal when the first app list sync in the session has been
// completed.
base::OneShotEvent on_first_sync_;
raw_ptr<TestingProfile> profile_;
AccountId account_id_;
};
INSTANTIATE_TEST_SUITE_P(All, AppListClientNewUserTest, testing::Bool());
IN_PROC_BROWSER_TEST_P(AppListClientNewUserTest, IsNewUser) {
// Until the first app list sync in the session has been completed, it is
// not known whether a given user can be considered new.
EXPECT_EQ(AppListClientImpl::GetInstance()->IsNewUser(account_id()),
std::nullopt);
// Signal that the first app list sync in the session has been completed.
on_first_sync().Signal();
// Once the first app list sync in the session has been completed, a task
// will be posted to the `AppListClient` which will cache whether the given
// user can be considered new.
EXPECT_TRUE(base::test::RunUntil([&]() {
return AppListClientImpl::GetInstance()->IsNewUser(account_id()) ==
was_first_sync_ever();
}));
}
// An enum identifying the possible combinations for the Launcher HATS survey in
// tests.
enum class AppListSurveyConfiguration {
// No HATS configurations is selected for this test.
kNone,
// ash::kHatsLauncherAppsFindingSurvey
kAppsFinding,
// ash::kHatsLauncherAppsNeedingSurvey
kAppsNeeding,
};
class AppListSurveyTriggerTest
: public AppListClientImplBrowserTest,
public testing::WithParamInterface<
std::tuple<ash::AppsCollectionsController::ExperimentalArm,
AppListSurveyConfiguration>> {
public:
AppListSurveyTriggerTest() {
std::vector<base::test::FeatureRefAndParams> enabled_features;
std::vector<base::test::FeatureRef> disabled_features;
ash::AppsCollectionsController::ExperimentalArm arm = GetExperimentalArm();
switch (arm) {
case ash::AppsCollectionsController::ExperimentalArm::kDefaultValue:
case ash::AppsCollectionsController::ExperimentalArm::kControl:
disabled_features.push_back(app_list_features::kAppsCollections);
break;
case ash::AppsCollectionsController::ExperimentalArm::kEnabled:
enabled_features.push_back(base::test::FeatureRefAndParams(
app_list_features::kAppsCollections,
{{"is-counterfactual", "false"}, {"is-modified-order", "false"}}));
break;
case ash::AppsCollectionsController::ExperimentalArm::kCounterfactual:
enabled_features.push_back(base::test::FeatureRefAndParams(
app_list_features::kAppsCollections,
{{"is-counterfactual", "true"}, {"is-modified-order", "false"}}));
break;
case ash::AppsCollectionsController::ExperimentalArm::kModifiedOrder:
enabled_features.push_back(base::test::FeatureRefAndParams(
app_list_features::kAppsCollections,
{{"is-counterfactual", "false"}, {"is-modified-order", "true"}}));
break;
}
switch (GetHatsConfig()) {
case AppListSurveyConfiguration::kNone:
disabled_features.push_back(
ash::kHatsLauncherAppsNeedingSurvey.feature);
disabled_features.push_back(
ash::kHatsLauncherAppsFindingSurvey.feature);
break;
case AppListSurveyConfiguration::kAppsFinding:
enabled_features.push_back(base::test::FeatureRefAndParams(
ash::kHatsLauncherAppsFindingSurvey.feature, {}));
disabled_features.push_back(
ash::kHatsLauncherAppsNeedingSurvey.feature);
break;
case AppListSurveyConfiguration::kAppsNeeding:
enabled_features.push_back(base::test::FeatureRefAndParams(
ash::kHatsLauncherAppsNeedingSurvey.feature, {}));
disabled_features.push_back(
ash::kHatsLauncherAppsFindingSurvey.feature);
break;
}
scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features,
disabled_features);
}
~AppListSurveyTriggerTest() override = default;
// AppListClientImplBrowserTest:
void SetUpOnMainThread() override {
AppListClientImplBrowserTest::SetUpOnMainThread();
display_service_ = std::make_unique<NotificationDisplayServiceTester>(
browser()->profile());
user_manager::UserManager::Get()->SetIsCurrentUserNew(true);
AppListClientImpl::GetInstance()->InitializeAsIfNewUserLoginForTest();
}
void SetUpDefaultCommandLine(base::CommandLine* command_line) override {
AppListClientImplBrowserTest::SetUpDefaultCommandLine(command_line);
switch (GetHatsConfig()) {
case AppListSurveyConfiguration::kNone:
break;
case AppListSurveyConfiguration::kAppsFinding:
command_line->AppendSwitchASCII(
ash::switches::kForceHappinessTrackingSystem,
ash::kHatsLauncherAppsFindingSurvey.feature.name);
break;
case AppListSurveyConfiguration::kAppsNeeding:
command_line->AppendSwitchASCII(
ash::switches::kForceHappinessTrackingSystem,
ash::kHatsLauncherAppsNeedingSurvey.feature.name);
break;
}
}
bool IsHatsNotificationActive() const {
return display_service_
->GetNotification(ash::HatsNotificationController::kNotificationId)
.has_value();
}
void MaybeWaitForHatsNotification() {
if (!ShouldShowHatsSurvey()) {
return;
}
base::RunLoop loop;
display_service_->SetNotificationAddedClosure(loop.QuitClosure());
loop.Run();
}
const ash::HatsNotificationController* GetHatsNotificationController() const {
return AppListClientImpl::GetInstance()
->survey_handler_->GetHatsNotificationControllerForTesting();
}
// Returns the HATS Survey that is expected to trigger.
AppListSurveyConfiguration GetHatsConfig() const {
return std::get<1>(GetParam());
}
// Returns the experimental arm that this test was set up for AppsCollections.
ash::AppsCollectionsController::ExperimentalArm GetExperimentalArm() const {
return std::get<0>(GetParam());
}
// Returns whether the a HATS survey should trigger for this parameter
// configuration.
bool ShouldShowHatsSurvey() {
return GetExperimentalArm() !=
ash::AppsCollectionsController::ExperimentalArm::kControl &&
GetHatsConfig() != AppListSurveyConfiguration::kNone;
}
base::test::ScopedFeatureList scoped_feature_list_;
std::unique_ptr<NotificationDisplayServiceTester> display_service_;
};
INSTANTIATE_TEST_SUITE_P(
All,
AppListSurveyTriggerTest,
::testing::Combine(
testing::Values(
ash::AppsCollectionsController::ExperimentalArm::kControl,
ash::AppsCollectionsController::ExperimentalArm::kCounterfactual,
ash::AppsCollectionsController::ExperimentalArm::kEnabled,
ash::AppsCollectionsController::ExperimentalArm::kModifiedOrder),
testing::Values(AppListSurveyConfiguration::kAppsFinding,
AppListSurveyConfiguration::kAppsNeeding,
AppListSurveyConfiguration::kNone)));
IN_PROC_BROWSER_TEST_P(AppListSurveyTriggerTest, ShowSurveySuccess) {
EXPECT_FALSE(IsHatsNotificationActive());
AppListClientImpl* client = AppListClientImpl::GetInstance();
// Bring up the app list.
EXPECT_FALSE(client->GetAppListWindow());
client->ShowAppList(ash::AppListShowSource::kSearchKey);
ash::AppListTestApi().WaitForBubbleWindow(
/*wait_for_opening_animation=*/false);
EXPECT_TRUE(client->GetAppListWindow());
MaybeWaitForHatsNotification();
EXPECT_EQ(GetHatsNotificationController() != nullptr, ShouldShowHatsSurvey());
EXPECT_EQ(IsHatsNotificationActive(), ShouldShowHatsSurvey());
}
IN_PROC_BROWSER_TEST_P(AppListSurveyTriggerTest, ShowSurveyOnlyOnce) {
if (!ShouldShowHatsSurvey()) {
return;
}
EXPECT_FALSE(IsHatsNotificationActive());
AppListClientImpl* client = AppListClientImpl::GetInstance();
// Bring up the app list.
EXPECT_FALSE(client->GetAppListWindow());
client->ShowAppList(ash::AppListShowSource::kSearchKey);
ash::AppListTestApi().WaitForBubbleWindow(
/*wait_for_opening_animation=*/false);
EXPECT_TRUE(client->GetAppListWindow());
MaybeWaitForHatsNotification();
const ash::HatsNotificationController* hats_notification_controller =
GetHatsNotificationController();
EXPECT_NE(hats_notification_controller, nullptr);
EXPECT_TRUE(IsHatsNotificationActive());
// Bring up the app list again but the controller shouldn't be a new instance.
client->DismissView();
EXPECT_FALSE(client->GetAppListWindow());
client->ShowAppList(ash::AppListShowSource::kSearchKey);
ash::AppListTestApi().WaitForBubbleWindow(
/*wait_for_opening_animation=*/false);
EXPECT_TRUE(client->GetAppListWindow());
EXPECT_EQ(hats_notification_controller, GetHatsNotificationController());
}
// A suite for verifying the experimental arm for apps collections experiment
// that modifies the order of apps.
class AppListModifiedDefaultAppOrderTest
: public AppListClientImplBrowserTest,
public testing::WithParamInterface<bool> {
public:
AppListModifiedDefaultAppOrderTest() {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
app_list_features::kAppsCollections,
{{"is-counterfactual", "false"},
{"is-modified-order",
IsModifiedOrderExperimentalArm() ? "true" : "false"}});
}
~AppListModifiedDefaultAppOrderTest() override = default;
// AppListClientImplBrowserTest:
void SetUpOnMainThread() override {
AppListClientImplBrowserTest::SetUpOnMainThread();
user_manager::UserManager::Get()->SetIsCurrentUserNew(true);
AppListClientImpl::GetInstance()->InitializeAsIfNewUserLoginForTest();
}
bool IsModifiedOrderExperimentalArm() { return GetParam(); }
void AddSyncedItem(std::string app_id, AppListModelUpdater* model_updater) {
app_list::AppListSyncableService* syncable_service =
app_list_syncable_service();
ASSERT_TRUE(syncable_service);
syncable_service->set_app_default_positioned_for_new_users_only_for_test(
app_id);
auto new_item = std::make_unique<ChromeAppListItem>(browser()->profile(),
app_id, model_updater);
new_item->SetChromeName(app_id);
syncable_service->AddItem(std::move(new_item));
}
ChromeAppListModelUpdater* GetChromeAppListModelUpdater() {
return static_cast<ChromeAppListModelUpdater*>(
app_list_syncable_service()->GetModelUpdater());
}
app_list::AppListSyncableService* app_list_syncable_service() {
return app_list::AppListSyncableServiceFactory::GetForProfile(profile());
}
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(All,
AppListModifiedDefaultAppOrderTest,
::testing::Bool());
// Verify that the default order of apps is changed once the recalculation
// happens for the first time in the modified order experimental arm of apps
// collections.
IN_PROC_BROWSER_TEST_P(AppListModifiedDefaultAppOrderTest,
DefaultOrdinalsChangeAfterRecalculation) {
AppListClientImpl* client = AppListClientImpl::GetInstance();
ASSERT_TRUE(client);
client->UpdateProfile();
ChromeAppListModelUpdater* model_updater = GetChromeAppListModelUpdater();
ASSERT_TRUE(model_updater);
// Install some default apps by syncing.
// In the default app order, youtube appears before the camera app. For the
// apps collections experimental arm, camera appears first.
AddSyncedItem(web_app::kCameraAppId, model_updater);
AddSyncedItem(extension_misc::kYoutubeAppId, model_updater);
ChromeAppListItem* camera_item =
model_updater->FindItem(web_app::kCameraAppId);
const syncer::StringOrdinal camera_ordinal = camera_item->position();
ChromeAppListItem* youtube_item =
model_updater->FindItem(extension_misc::kYoutubeAppId);
const syncer::StringOrdinal youtube_ordinal = youtube_item->position();
// Before calculating the experimental arm, the default apps should be ordered
// as default, with youtube having a lesser ordinal than camera.
EXPECT_TRUE(youtube_ordinal.LessThan(camera_ordinal));
// Trigger a recalculation of the experimental arm and apps position for
// testing simplicity. This is usually done on first sync.
client->MaybeRecalculateAppsGridDefaultOrder();
const syncer::StringOrdinal new_camera_ordinal = camera_item->position();
const syncer::StringOrdinal new_youtube_ordinal = youtube_item->position();
// After determining if the user belongs in the
// experimental arm or not, the default apps may change their ordinals if the
// user belongs in the experimental modified order. The order of youtube and
// camera is also changed so that now camera has a lesser ordinal than
// youtube.
EXPECT_EQ(camera_ordinal != new_camera_ordinal,
IsModifiedOrderExperimentalArm());
EXPECT_EQ(youtube_ordinal != new_youtube_ordinal,
IsModifiedOrderExperimentalArm());
EXPECT_EQ(new_camera_ordinal.LessThan(new_youtube_ordinal),
IsModifiedOrderExperimentalArm());
}
// Verify that the default order of apps is changed once the app list opens for
// the first time in the modified order experimental arm of apps collections.
IN_PROC_BROWSER_TEST_P(AppListModifiedDefaultAppOrderTest,
DefaultOrdinalsNotChangeAfterReorder) {
AppListClientImpl* client = AppListClientImpl::GetInstance();
ASSERT_TRUE(client);
client->UpdateProfile();
ChromeAppListModelUpdater* model_updater = GetChromeAppListModelUpdater();
ASSERT_TRUE(model_updater);
// Install some default apps by syncing.
AddSyncedItem(web_app::kCameraAppId, model_updater);
AddSyncedItem(extension_misc::kYoutubeAppId, model_updater);
AddSyncedItem(web_app::kCalculatorAppId, model_updater);
ChromeAppListItem* camera_item =
model_updater->FindItem(web_app::kCameraAppId);
const syncer::StringOrdinal camera_ordinal = camera_item->position();
ChromeAppListItem* youtube_item =
model_updater->FindItem(extension_misc::kYoutubeAppId);
const syncer::StringOrdinal youtube_ordinal = youtube_item->position();
ChromeAppListItem* calculator_item =
model_updater->FindItem(web_app::kCalculatorAppId);
syncer::StringOrdinal calculator_ordinal = calculator_item->position();
// Before calculating the experimental arm, the default apps should be ordered
// as default, with youtube having a lesser ordinal than camera, which have a
// lesser ordinal than calculator.
EXPECT_TRUE(youtube_ordinal.LessThan(camera_ordinal));
EXPECT_TRUE(camera_ordinal.LessThan(calculator_ordinal));
// Move the calculator before the camera
model_updater->RequestPositionUpdate(
web_app::kCalculatorAppId, camera_ordinal.CreateBefore(),
ash::RequestPositionUpdateReason::kMoveItem);
calculator_ordinal = calculator_item->position();
EXPECT_TRUE(calculator_ordinal.LessThan(camera_ordinal));
// Trigger a recalculation of the experimental arm and apps position for
// testing simplicity. This is usually done on first sync.
client->MaybeRecalculateAppsGridDefaultOrder();
const syncer::StringOrdinal new_camera_ordinal = camera_item->position();
const syncer::StringOrdinal new_youtube_ordinal = youtube_item->position();
const syncer::StringOrdinal new_calculator_ordinal =
calculator_item->position();
// Because there was an app reorder, ordinals should not change.
EXPECT_EQ(camera_ordinal, new_camera_ordinal);
EXPECT_EQ(youtube_ordinal, new_youtube_ordinal);
EXPECT_EQ(calculator_ordinal, new_calculator_ordinal);
}