blob: 4454b1c2bc38b98d95bfe3addb6bfc06335fd06c [file] [log] [blame]
// 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 "chrome/browser/ui/ash/graduation/graduation_manager_impl.h"
#include <algorithm>
#include <ranges>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/web_app_id_constants.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/public/cpp/shelf_model_observer.h"
#include "base/functional/callback_forward.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/time/time.h"
#include "base/time/time_override.h"
#include "base/values.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/ash/base/locale_util.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/ash/system_web_apps/test_support/system_web_app_browsertest_base.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/web_applications/web_app_command_manager.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/mixin_based_in_process_browser_test.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace ash {
namespace {
constexpr char kSessionStartTime[] = "30 Sep 2024 00:00:00 PST";
apps::AppServiceProxy* GetAppServiceProxy(Profile* profile) {
return apps::AppServiceProxyFactory::GetForProfile(profile);
}
void WaitForAppRegistryCommands(Profile* profile) {
auto* web_app_provider = web_app::WebAppProvider::GetForTest(profile);
base::RunLoop run_loop;
web_app_provider->on_registry_ready().Post(FROM_HERE, run_loop.QuitClosure());
run_loop.Run();
web_app_provider->command_manager().AwaitAllCommandsCompleteForTesting();
}
// Called on completion of locale_util::SwitchLanguage.
void OnLocaleSwitched(base::RunLoop* run_loop,
const locale_util::LanguageSwitchResult& result) {
run_loop->Quit();
}
} // namespace
class GraduationManagerTest : public SystemWebAppBrowserTestBase,
public ash::ShelfModelObserver {
public:
GraduationManagerTest() {
scoped_feature_list_.InitAndEnableFeature(features::kGraduation);
}
~GraduationManagerTest() override = default;
GraduationManagerTest(const GraduationManagerTest&) = delete;
GraduationManagerTest& operator=(const GraduationManagerTest&) = delete;
void SetUpOnMainThread() override {
SystemWebAppBrowserTestBase::SetUpOnMainThread();
logged_in_user_mixin_.LogInUser();
SetMockClocksAndTaskRunner();
WaitForTestSystemAppInstall();
WaitForAppRegistryCommands(browser()->profile());
}
static void SetTimeNow(base::Time new_time_now) { time_now_ = new_time_now; }
static base::Time GetTimeNow() { return time_now_; }
void SetMockClocksAndTaskRunner() {
// Set the system time in the task runner.
base::Time start_time;
EXPECT_TRUE(base::Time::FromUTCString(kSessionStartTime, &start_time));
task_runner_ = base::MakeRefCounted<base::TestMockTimeTaskRunner>(
start_time, base::TimeTicks::UnixEpoch());
ash::graduation::GraduationManagerImpl::Get()->SetClocksForTesting(
task_runner_->GetMockClock(), task_runner_->GetMockTickClock());
// Override base::Time::Now() so the util functions work properly.
SetTimeNow(start_time);
time_override_ = std::make_unique<base::subtle::ScopedTimeClockOverrides>(
/*time_override=*/&GraduationManagerTest::GetTimeNow,
/*time_ticks_override=*/nullptr,
/*thread_ticks_override=*/nullptr);
}
void AdvanceTimeBy(base::TimeDelta advance_length) {
task_runner_->FastForwardBy(advance_length);
SetTimeNow(base::Time::Now() + advance_length);
ResumeTimer();
}
// task_runner_->FastForwardBy(TimeDelta) doesn't trigger the midnight timer
// so it needs to be manually resumed.
void ResumeTimer() {
ash::graduation::GraduationManagerImpl::Get()->ResumeTimerForTesting();
}
bool IsItemPinned(const std::string& item_id) {
const auto& shelf_items = ShelfModel::Get()->items();
auto pinned_item =
std::ranges::find_if(shelf_items, [&item_id](const auto& shelf_item) {
return shelf_item.id.app_id == item_id;
});
return pinned_item != std::ranges::end(shelf_items);
}
apps::Readiness GetAppReadiness(const webapps::AppId& app_id) {
apps::Readiness readiness;
bool app_found =
GetAppServiceProxy(browser()->profile())
->AppRegistryCache()
.ForOneApp(app_id, [&readiness](const apps::AppUpdate& update) {
readiness = update.Readiness();
});
EXPECT_TRUE(app_found);
return readiness;
}
void SetGraduationEnablement(bool is_enabled) {
base::Value::Dict status;
status.Set("is_enabled", is_enabled);
browser()->profile()->GetPrefs()->SetDict(
prefs::kGraduationEnablementStatus, status.Clone());
}
void SetGraduationEnablementWithStartDate(bool is_enabled,
int day,
int month,
int year) {
base::Value::Dict status;
status.Set("is_enabled", is_enabled);
base::Value::Dict start_date;
start_date.Set("day", day);
start_date.Set("month", month);
start_date.Set("year", year);
status.Set("start_date", start_date.Clone());
browser()->profile()->GetPrefs()->SetDict(
prefs::kGraduationEnablementStatus, status.Clone());
}
void SetGraduationEnablementWithEndDate(bool is_enabled,
int day,
int month,
int year) {
base::Value::Dict status;
status.Set("is_enabled", is_enabled);
base::Value::Dict end_date;
end_date.Set("day", day);
end_date.Set("month", month);
end_date.Set("year", year);
status.Set("end_date", end_date.Clone());
browser()->profile()->GetPrefs()->SetDict(
prefs::kGraduationEnablementStatus, status.Clone());
}
std::string GetLanguageCode() {
return ash::graduation::GraduationManagerImpl::Get()->GetLanguageCode();
}
void WaitForShelfItemAdd() {
base::RunLoop run_loop;
auto* shelf_model = ShelfModel::Get();
shelf_model->AddObserver(this);
item_added_callback_ = run_loop.QuitClosure();
run_loop.Run();
shelf_model->RemoveObserver(this);
}
void WaitForShefItemRemoved() {
base::RunLoop run_loop;
auto* shelf_model = ShelfModel::Get();
shelf_model->AddObserver(this);
item_removed_callback_ = run_loop.QuitClosure();
run_loop.Run();
shelf_model->RemoveObserver(this);
}
// ShelfModelObserver:
void ShelfItemAdded(int index) override {
if (item_added_callback_) {
item_added_callback_.Run();
}
}
void ShelfItemRemoved(int index, const ShelfItem& old_item) override {
if (item_removed_callback_) {
item_removed_callback_.Run();
}
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
base::RepeatingClosure item_added_callback_;
base::RepeatingClosure item_removed_callback_;
scoped_refptr<base::TestMockTimeTaskRunner> task_runner_;
std::unique_ptr<base::subtle::ScopedTimeClockOverrides> time_override_;
static base::Time time_now_;
LoggedInUserMixin logged_in_user_mixin_{
&mixin_host_, /*test_base=*/this, embedded_test_server(),
LoggedInUserMixin::LogInType::kManaged};
};
// static
base::Time GraduationManagerTest::time_now_;
IN_PROC_BROWSER_TEST_F(GraduationManagerTest, PRE_AppPinnedWhenPolicyEnabled) {
// Set pref value in PRE_ to ensure that the pref value is set at the time of
// user session start in the test.
SetGraduationEnablement(true);
}
IN_PROC_BROWSER_TEST_F(GraduationManagerTest, AppPinnedWhenPolicyEnabled) {
EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
SetGraduationEnablement(false);
WaitForAppRegistryCommands(browser()->profile());
EXPECT_FALSE(IsItemPinned(ash::kGraduationAppId));
EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
GetAppReadiness(ash::kGraduationAppId));
}
IN_PROC_BROWSER_TEST_F(GraduationManagerTest,
PRE_AppPinnedWhenStartDateIsReached) {
// Set pref value in PRE_ to ensure that the pref value is set at the time of
// user session start in the test.
SetGraduationEnablementWithStartDate(true, 1, 10, 2024);
}
IN_PROC_BROWSER_TEST_F(GraduationManagerTest, AppPinnedWhenStartDateIsReached) {
EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
GetAppReadiness(ash::kGraduationAppId));
EXPECT_FALSE(IsItemPinned(ash::kGraduationAppId));
// Fast forward to the policy enablement start date set in the pre-test.
AdvanceTimeBy(base::Days(1));
WaitForAppRegistryCommands(browser()->profile());
WaitForShelfItemAdd();
EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
}
IN_PROC_BROWSER_TEST_F(GraduationManagerTest,
PRE_AppPinnedWhenStartDateIsReachedInMoreThanOneDay) {
// Set pref value in PRE_ to ensure that the pref value is set at the time of
// user session start in the test.
SetGraduationEnablementWithStartDate(true, 2, 10, 2024);
}
IN_PROC_BROWSER_TEST_F(GraduationManagerTest,
AppPinnedWhenStartDateIsReachedInMoreThanOneDay) {
EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
GetAppReadiness(ash::kGraduationAppId));
EXPECT_FALSE(IsItemPinned(ash::kGraduationAppId));
// Fast forward to the policy enablement start date set in the pre-test.
AdvanceTimeBy(base::Days(2));
WaitForAppRegistryCommands(browser()->profile());
// Wait for the new shelf iteme to finish.
WaitForShelfItemAdd();
EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
}
IN_PROC_BROWSER_TEST_F(GraduationManagerTest, PRE_AppPinnedOnEndDate) {
// Set pref value in PRE_ to ensure that the pref value is set at the time of
// user session start in the test.
SetGraduationEnablementWithEndDate(true, 1, 10, 2024);
}
IN_PROC_BROWSER_TEST_F(GraduationManagerTest, AppPinnedOnEndDate) {
EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
// Fast forward to the policy enablement end date set in the pre-test.
AdvanceTimeBy(base::Days(1));
WaitForAppRegistryCommands(browser()->profile());
// Since this is the last day the app is available, the app should be pinned.
EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
}
IN_PROC_BROWSER_TEST_F(GraduationManagerTest, AppUnpinnedWhenPolicyUnset) {
EXPECT_FALSE(IsItemPinned(ash::kGraduationAppId));
EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
GetAppReadiness(ash::kGraduationAppId));
SetGraduationEnablement(true);
WaitForAppRegistryCommands(browser()->profile());
EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
}
IN_PROC_BROWSER_TEST_F(GraduationManagerTest,
PRE_AppUnpinnedWhenPolicyDisabled) {
// Set pref value in PRE_ to ensure that the pref value is set at the time of
// user session start in the test.
SetGraduationEnablement(false);
}
IN_PROC_BROWSER_TEST_F(GraduationManagerTest, AppUnpinnedWhenPolicyDisabled) {
EXPECT_FALSE(IsItemPinned(ash::kGraduationAppId));
EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
GetAppReadiness(ash::kGraduationAppId));
SetGraduationEnablement(true);
WaitForAppRegistryCommands(browser()->profile());
EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
}
IN_PROC_BROWSER_TEST_F(GraduationManagerTest,
PRE_AppUnpinnedWhenEndDateHasPassed) {
// Set pref value in PRE_ to ensure that the pref value is set at the time of
// user session start in the test.
SetGraduationEnablementWithEndDate(true, 1, 10, 2024);
}
IN_PROC_BROWSER_TEST_F(GraduationManagerTest, AppUnpinnedWhenEndDateHasPassed) {
EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
// Fast forward to one day past the policy enablement end date set in the
// pre-test.
AdvanceTimeBy(base::Days(2));
WaitForAppRegistryCommands(browser()->profile());
// Wait for the shelf item to finish being removed.
WaitForShefItemRemoved();
EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
GetAppReadiness(ash::kGraduationAppId));
EXPECT_FALSE(IsItemPinned(ash::kGraduationAppId));
}
IN_PROC_BROWSER_TEST_F(GraduationManagerTest, GetLanguageCode) {
// Browser tests should default to English.
EXPECT_EQ("en-US", GetLanguageCode());
// Switch the application locale to Spanish.
base::RunLoop run_loop;
locale_util::SwitchLanguage("es", true, false,
base::BindRepeating(&OnLocaleSwitched, &run_loop),
ProfileManager::GetActiveUserProfile());
run_loop.Run();
EXPECT_EQ("es", GetLanguageCode());
}
class GraduationManagerWithConsumerUserTest
: public SystemWebAppBrowserTestBase,
public ::testing::WithParamInterface<bool> {
public:
GraduationManagerWithConsumerUserTest() {
scoped_feature_list_.InitAndEnableFeature(features::kGraduation);
}
~GraduationManagerWithConsumerUserTest() override = default;
GraduationManagerWithConsumerUserTest(
const GraduationManagerWithConsumerUserTest&) = delete;
GraduationManagerWithConsumerUserTest& operator=(
const GraduationManagerWithConsumerUserTest&) = delete;
void SetUpOnMainThread() override {
SystemWebAppBrowserTestBase::SetUpOnMainThread();
logged_in_user_mixin_.LogInUser();
WaitForTestSystemAppInstall();
WaitForAppRegistryCommands(browser()->profile());
}
private:
bool IsChildUser() const { return GetParam(); }
base::test::ScopedFeatureList scoped_feature_list_;
LoggedInUserMixin logged_in_user_mixin_{
&mixin_host_, /*test_base=*/this, embedded_test_server(),
IsChildUser() ? LoggedInUserMixin::LogInType::kChild
: LoggedInUserMixin::LogInType::kConsumer};
};
IN_PROC_BROWSER_TEST_P(GraduationManagerWithConsumerUserTest, AppNotInstalled) {
EXPECT_FALSE(GetManager().IsSystemWebApp(ash::kGraduationAppId));
}
INSTANTIATE_TEST_SUITE_P(All,
GraduationManagerWithConsumerUserTest,
testing::Bool());
} // namespace ash