// 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 "ash/display/resolution_notification_controller.h"
#include "ash/display/display_change_dialog.h"
#include "ash/display/display_util.h"
#include "ash/screen_util.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/screen_layout_observer.h"
#include "ash/test/ash_test_base.h"
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"
#include "ui/display/display_features.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/gfx/geometry/size.h"
#include "ui/views/controls/label.h"
namespace ash {
class ResolutionNotificationControllerTest
: public AshTestBase,
public ::testing::WithParamInterface<bool> {
public:
ResolutionNotificationControllerTest() : accept_count_(0) {}
ResolutionNotificationControllerTest(
const ResolutionNotificationControllerTest&) = delete;
ResolutionNotificationControllerTest& operator=(
const ResolutionNotificationControllerTest&) = delete;
~ResolutionNotificationControllerTest() override = default;
std::u16string ExpectedNotificationMessage(int64_t display_id,
const gfx::Size& new_resolution,
float new_refresh_rate,
uint16_t timeout_count) {
const std::u16string display_name =
base::UTF8ToUTF16(display_manager()->GetDisplayNameForId(display_id));
const std::u16string countdown = ui::TimeFormat::Simple(
ui::TimeFormat::FORMAT_DURATION, ui::TimeFormat::LENGTH_LONG,
base::Seconds(timeout_count));
if (::display::features::IsListAllDisplayModesEnabled()) {
return l10n_util::GetStringFUTF16(
IDS_ASH_RESOLUTION_REFRESH_CHANGE_DIALOG_CHANGED_NEW, display_name,
base::UTF8ToUTF16(new_resolution.ToString()),
ConvertRefreshRateToString16(new_refresh_rate), countdown);
}
return l10n_util::GetStringFUTF16(
IDS_ASH_RESOLUTION_CHANGE_DIALOG_CHANGED, display_name,
base::UTF8ToUTF16(new_resolution.ToString()), countdown);
}
std::u16string ExpectedFallbackNotificationMessage(
int64_t display_id,
const gfx::Size& specified_resolution,
float specified_refresh_rate,
const gfx::Size& fallback_resolution,
float fallback_refresh_rate,
uint16_t timeout_count) {
const std::u16string display_name =
base::UTF8ToUTF16(display_manager()->GetDisplayNameForId(display_id));
const std::u16string countdown = ui::TimeFormat::Simple(
ui::TimeFormat::FORMAT_DURATION, ui::TimeFormat::LENGTH_LONG,
base::Seconds(timeout_count));
if (::display::features::IsListAllDisplayModesEnabled()) {
return l10n_util::GetStringFUTF16(
IDS_ASH_RESOLUTION_REFRESH_CHANGE_DIALOG_FALLBACK_NEW,
{display_name, base::UTF8ToUTF16(fallback_resolution.ToString()),
ConvertRefreshRateToString16(fallback_refresh_rate),
base::UTF8ToUTF16(specified_resolution.ToString()),
ConvertRefreshRateToString16(specified_refresh_rate), countdown},
/*offsets=*/nullptr);
}
return l10n_util::GetStringFUTF16(
IDS_ASH_RESOLUTION_CHANGE_DIALOG_FALLBACK, display_name,
base::UTF8ToUTF16(specified_resolution.ToString()),
base::UTF8ToUTF16(fallback_resolution.ToString()), countdown);
}
protected:
void SetUp() override {
if (GetParam()) {
scoped_feature_list_.InitAndEnableFeature(
display::features::kListAllDisplayModes);
} else {
scoped_feature_list_.InitAndDisableFeature(
display::features::kListAllDisplayModes);
}
AshTestBase::SetUp();
}
void SetDisplayResolutionAndNotifyWithResolution(
const display::Display& display,
const gfx::Size& new_resolution,
const gfx::Size& actual_new_resolution,
float new_refresh_rate,
bool old_is_native,
bool new_is_native,
crosapi::mojom::DisplayConfigSource source =
crosapi::mojom::DisplayConfigSource::kUser) {
{
const display::ManagedDisplayInfo& info =
display_manager()->GetDisplayInfo(display.id());
display::ManagedDisplayMode old_mode(
info.size_in_pixel(), info.refresh_rate(), false /* interlaced */,
old_is_native);
display::ManagedDisplayMode new_mode(
new_resolution, new_refresh_rate, old_mode.is_interlaced(),
new_is_native, old_mode.device_scale_factor());
EXPECT_TRUE(controller()->PrepareNotificationAndSetDisplayMode(
display.id(), old_mode, new_mode, source,
base::BindOnce(&ResolutionNotificationControllerTest::OnAccepted,
base::Unretained(this))));
}
// OnConfigurationChanged event won't be emitted in the test environment,
// so invoke UpdateDisplay() to emit that event explicitly.
std::vector<display::ManagedDisplayInfo> info_list;
for (size_t i = 0; i < display_manager()->GetNumDisplays(); ++i) {
int64_t id = display_manager()->GetDisplayAt(i).id();
display::ManagedDisplayInfo info = display_manager()->GetDisplayInfo(id);
if (display.id() == id) {
gfx::Rect bounds = info.bounds_in_native();
bounds.set_size(actual_new_resolution);
info.SetBounds(bounds);
info.set_refresh_rate(new_refresh_rate);
info.set_native(new_is_native);
}
info_list.push_back(info);
}
display_manager()->OnNativeDisplaysChanged(info_list);
base::RunLoop().RunUntilIdle();
}
void SetDisplayResolutionAndNotify(
const display::Display& display,
const gfx::Size& new_resolution,
float refresh_rate,
bool old_is_native,
bool new_is_native,
crosapi::mojom::DisplayConfigSource source =
crosapi::mojom::DisplayConfigSource::kUser) {
SetDisplayResolutionAndNotifyWithResolution(
display, new_resolution, new_resolution, refresh_rate, old_is_native,
new_is_native, source);
}
static std::u16string GetNotificationMessage() {
return controller()->dialog_for_testing()->label_->GetText();
}
static uint16_t GetTimeoutCount() {
return controller()->dialog_for_testing()->timeout_count_;
}
static void ClickOnNotification() {
controller()->dialog_for_testing()->AcceptDialog();
}
static void CloseNotification() {
controller()->dialog_for_testing()->AcceptDialog();
}
static bool IsNotificationVisible() {
return controller()->dialog_for_testing() != nullptr;
}
static void TickTimer() { controller()->dialog_for_testing()->OnTimerTick(); }
static ResolutionNotificationController* controller() {
return Shell::Get()->resolution_notification_controller();
}
static void CancelNotification() {
controller()->dialog_for_testing()->CancelDialog();
}
int accept_count() const { return accept_count_; }
private:
void OnAccepted() {
accept_count_++;
}
int accept_count_;
base::test::ScopedFeatureList scoped_feature_list_;
};
// Basic behaviors and verifies it doesn't cause crashes.
TEST_P(ResolutionNotificationControllerTest, Basic) {
UpdateDisplay("400x300#400x300%57|300x200%58,300x250#300x250%60|300x200%59");
display::test::DisplayManagerTestApi display_manager_test(display_manager());
int64_t id2 = display_manager_test.GetSecondaryDisplay().id();
ASSERT_EQ(0, accept_count());
EXPECT_FALSE(IsNotificationVisible());
// Changes the resolution and apply the result.
SetDisplayResolutionAndNotify(display_manager_test.GetSecondaryDisplay(),
gfx::Size(300, 200), 59, /*old_is_native=*/true,
/*new_is_native=*/false);
EXPECT_TRUE(IsNotificationVisible());
EXPECT_EQ(ExpectedNotificationMessage(id2, gfx::Size(300, 200), 59,
GetTimeoutCount()),
GetNotificationMessage());
display::ManagedDisplayMode mode;
EXPECT_TRUE(display_manager()->GetSelectedModeForDisplayId(id2, &mode));
EXPECT_EQ(gfx::Size(300, 200), mode.size());
EXPECT_EQ(59.0, mode.refresh_rate());
// Click the revert button, which reverts to the best resolution.
CancelNotification();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsNotificationVisible());
EXPECT_EQ(0, accept_count());
EXPECT_TRUE(display_manager()->GetSelectedModeForDisplayId(id2, &mode));
EXPECT_EQ(gfx::Size(300, 250), mode.size());
EXPECT_EQ(60.0, mode.refresh_rate());
}
// Check that notification is not shown when changes are forced by policy.
TEST_P(ResolutionNotificationControllerTest, ForcedByPolicy) {
UpdateDisplay("400x300#400x300%57|300x200%58,300x250#300x250%59|300x200%60");
display::test::DisplayManagerTestApi display_manager_test(display_manager());
int64_t id2 = display_manager_test.GetSecondaryDisplay().id();
ASSERT_EQ(0, accept_count());
EXPECT_FALSE(IsNotificationVisible());
// Changes the resolution and apply the result.
SetDisplayResolutionAndNotify(display_manager_test.GetSecondaryDisplay(),
gfx::Size(300, 200), 60, /*old_is_native=*/true,
/*new_is_native=*/false,
crosapi::mojom::DisplayConfigSource::kPolicy);
EXPECT_FALSE(IsNotificationVisible());
display::ManagedDisplayMode mode;
EXPECT_TRUE(display_manager()->GetSelectedModeForDisplayId(id2, &mode));
EXPECT_EQ(gfx::Size(300, 200), mode.size());
EXPECT_EQ(60.0, mode.refresh_rate());
}
TEST_P(ResolutionNotificationControllerTest, ClickMeansAccept) {
UpdateDisplay("400x300#400x300%57|300x200%58,300x250#300x250%59|300x200%60");
display::test::DisplayManagerTestApi display_manager_test(display_manager());
int64_t id2 = display_manager_test.GetSecondaryDisplay().id();
ASSERT_EQ(0, accept_count());
EXPECT_FALSE(IsNotificationVisible());
// Changes the resolution and apply the result.
SetDisplayResolutionAndNotify(display_manager_test.GetSecondaryDisplay(),
gfx::Size(300, 200), 60, /*old_is_native=*/true,
/*new_is_native=*/false);
EXPECT_TRUE(IsNotificationVisible());
display::ManagedDisplayMode mode;
EXPECT_TRUE(display_manager()->GetSelectedModeForDisplayId(id2, &mode));
EXPECT_EQ(gfx::Size(300, 200), mode.size());
EXPECT_EQ(60.0, mode.refresh_rate());
ClickOnNotification();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsNotificationVisible());
EXPECT_EQ(1, accept_count());
EXPECT_TRUE(display_manager()->GetSelectedModeForDisplayId(id2, &mode));
EXPECT_EQ(gfx::Size(300, 200), mode.size());
EXPECT_EQ(60.0, mode.refresh_rate());
}
TEST_P(ResolutionNotificationControllerTest, AcceptButton) {
UpdateDisplay("400x300#400x300%59|300x200%60");
const display::Display& display =
display::Screen::GetScreen()->GetPrimaryDisplay();
SetDisplayResolutionAndNotify(display, gfx::Size(300, 200), 60,
/*old_is_native=*/true,
/*new_is_native=*/false);
EXPECT_TRUE(IsNotificationVisible());
controller()->dialog_for_testing()->AcceptDialog();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsNotificationVisible());
EXPECT_EQ(1, accept_count());
display::ManagedDisplayMode mode;
EXPECT_TRUE(
display_manager()->GetSelectedModeForDisplayId(display.id(), &mode));
EXPECT_EQ(gfx::Size(300, 200), mode.size());
EXPECT_EQ(60.0f, mode.refresh_rate());
// In that case the second button is revert.
UpdateDisplay("400x300#400x300%60|300x200%59");
SetDisplayResolutionAndNotify(display, gfx::Size(300, 200), 59,
/*old_is_native=*/true,
/*new_is_native=*/false);
EXPECT_TRUE(IsNotificationVisible());
controller()->dialog_for_testing()->CancelDialog();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsNotificationVisible());
EXPECT_EQ(1, accept_count());
EXPECT_TRUE(
display_manager()->GetSelectedModeForDisplayId(display.id(), &mode));
EXPECT_EQ(gfx::Size(400, 300), mode.size());
EXPECT_EQ(60.0f, mode.refresh_rate());
}
TEST_P(ResolutionNotificationControllerTest, Close) {
UpdateDisplay("200x100,250x150#250x150%59|300x200%60");
display::test::DisplayManagerTestApi display_manager_test(display_manager());
int64_t id2 = display_manager_test.GetSecondaryDisplay().id();
ASSERT_EQ(0, accept_count());
EXPECT_FALSE(IsNotificationVisible());
// Changes the resolution and apply the result.
SetDisplayResolutionAndNotify(
display_manager_test.GetSecondaryDisplay(), gfx::Size(300, 200), 60,
/*old_is_native=*/false, /*new_is_native=*/true);
EXPECT_TRUE(IsNotificationVisible());
display::ManagedDisplayMode mode;
EXPECT_TRUE(display_manager()->GetSelectedModeForDisplayId(id2, &mode));
EXPECT_EQ(gfx::Size(300, 200), mode.size());
EXPECT_EQ(60.0f, mode.refresh_rate());
// Close the notification (imitates clicking [x] button). Also verifies if
// this does not cause a crash. See crbug.com/271784
CloseNotification();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsNotificationVisible());
EXPECT_EQ(1, accept_count());
}
TEST_P(ResolutionNotificationControllerTest, Timeout) {
UpdateDisplay("400x300#400x300%60|300x200%60");
const display::Display& display =
display::Screen::GetScreen()->GetPrimaryDisplay();
SetDisplayResolutionAndNotify(display, gfx::Size(300, 200), 60,
/*old_is_native=*/true,
/*new_is_native=*/false);
for (int i = 0; i < DisplayChangeDialog::kDefaultTimeoutInSeconds; ++i) {
EXPECT_TRUE(IsNotificationVisible())
<< "notification is closed after " << i << "-th timer tick";
TickTimer();
base::RunLoop().RunUntilIdle();
}
EXPECT_FALSE(IsNotificationVisible());
EXPECT_EQ(0, accept_count());
display::ManagedDisplayMode mode;
EXPECT_TRUE(
display_manager()->GetSelectedModeForDisplayId(display.id(), &mode));
EXPECT_EQ(gfx::Size(400, 300), mode.size());
EXPECT_EQ(60.0f, mode.refresh_rate());
}
TEST_P(ResolutionNotificationControllerTest, DisplayDisconnected) {
UpdateDisplay(
"400x300#400x300%56|300x200%57,"
"300x200#300x250%58|300x200%60|200x100%60");
display::test::DisplayManagerTestApi display_manager_test(display_manager());
int64_t id2 = display_manager_test.GetSecondaryDisplay().id();
SetDisplayResolutionAndNotify(
display_manager_test.GetSecondaryDisplay(), gfx::Size(200, 100), 60,
/*old_is_native=*/false, /*new_is_native=*/false);
ASSERT_TRUE(IsNotificationVisible());
// Disconnects the secondary display and verifies it doesn't cause crashes.
UpdateDisplay("400x300#400x300%60|300x200%60");
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsNotificationVisible());
EXPECT_EQ(0, accept_count());
display::ManagedDisplayMode mode;
EXPECT_TRUE(display_manager()->GetSelectedModeForDisplayId(id2, &mode));
EXPECT_EQ(gfx::Size(300, 200), mode.size());
EXPECT_EQ(60.0f, mode.refresh_rate());
}
// See http://crbug.com/869401 for details.
TEST_P(ResolutionNotificationControllerTest, MultipleResolutionChange) {
UpdateDisplay(
"400x300#400x300%56|300x200%57,"
"350x250#350x250%58|300x200%59");
display::test::DisplayManagerTestApi display_manager_test(display_manager());
int64_t id2 = display_manager_test.GetSecondaryDisplay().id();
SetDisplayResolutionAndNotify(display_manager_test.GetSecondaryDisplay(),
gfx::Size(300, 200), 59, /*old_is_native=*/true,
/*new_is_native=*/false);
EXPECT_TRUE(IsNotificationVisible());
display::ManagedDisplayMode mode;
EXPECT_TRUE(display_manager()->GetSelectedModeForDisplayId(id2, &mode));
EXPECT_EQ(gfx::Size(300, 200), mode.size());
EXPECT_EQ(59.0f, mode.refresh_rate());
// Invokes SetDisplayResolutionAndNotify during the previous notification is
// visible.
SetDisplayResolutionAndNotify(
display_manager_test.GetSecondaryDisplay(), gfx::Size(350, 250), 58,
/*old_is_native=*/false, /*new_is_native=*/true);
EXPECT_TRUE(display_manager()->GetSelectedModeForDisplayId(id2, &mode));
EXPECT_EQ(gfx::Size(350, 250), mode.size());
EXPECT_EQ(58.0f, mode.refresh_rate());
// Then, click the revert button. Although |old_resolution| for the second
// SetDisplayResolutionAndNotify is 300x200, it should revert to the original
// size 350x250.
CancelNotification();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsNotificationVisible());
EXPECT_EQ(0, accept_count());
EXPECT_TRUE(display_manager()->GetSelectedModeForDisplayId(id2, &mode));
EXPECT_EQ(gfx::Size(350, 250), mode.size());
EXPECT_EQ(58.0f, mode.refresh_rate());
}
TEST_P(ResolutionNotificationControllerTest, Fallback) {
UpdateDisplay(
"400x300#400x300%56|300x200%57,"
"350x250#350x250%60|220x210%60|300x200%60");
display::test::DisplayManagerTestApi display_manager_test(display_manager());
int64_t id2 = display_manager_test.GetSecondaryDisplay().id();
ASSERT_EQ(0, accept_count());
EXPECT_FALSE(IsNotificationVisible());
// Changes the resolution and apply the result.
SetDisplayResolutionAndNotifyWithResolution(
display_manager_test.GetSecondaryDisplay(), gfx::Size(220, 210),
gfx::Size(300, 200), 60, /*old_is_native=*/true, /*new_is_native=*/false);
EXPECT_TRUE(IsNotificationVisible());
EXPECT_EQ(ExpectedFallbackNotificationMessage(id2, gfx::Size(220, 210), 60,
gfx::Size(300, 200), 60,
GetTimeoutCount()),
GetNotificationMessage());
display::ManagedDisplayMode mode;
EXPECT_TRUE(display_manager()->GetSelectedModeForDisplayId(id2, &mode));
EXPECT_EQ(gfx::Size(300, 200), mode.size());
EXPECT_EQ(60.0f, mode.refresh_rate());
// Click the revert button, which reverts to the best resolution.
CancelNotification();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsNotificationVisible());
EXPECT_EQ(0, accept_count());
EXPECT_TRUE(display_manager()->GetSelectedModeForDisplayId(id2, &mode));
EXPECT_EQ(gfx::Size(350, 250), mode.size());
EXPECT_EQ(60.0f, mode.refresh_rate());
}
TEST_P(ResolutionNotificationControllerTest, NoTimeoutInKioskMode) {
// Login in as kiosk app.
UserSession session;
session.session_id = 1u;
session.user_info.type = user_manager::UserType::kKioskApp;
session.user_info.account_id = AccountId::FromUserEmail("[email protected]");
session.user_info.display_name = "User 1";
session.user_info.display_email = "[email protected]";
Shell::Get()->session_controller()->UpdateUserSession(std::move(session));
EXPECT_EQ(LoginStatus::KIOSK_APP,
Shell::Get()->session_controller()->login_status());
UpdateDisplay("400x300#400x300%59|300x200%60");
const display::Display& display =
display::Screen::GetScreen()->GetPrimaryDisplay();
SetDisplayResolutionAndNotify(display, gfx::Size(300, 200), 60,
/*old_is_native=*/true,
/*new_is_native=*/false);
}
TEST_P(ResolutionNotificationControllerTest, NoDialogInKioskMode) {
// Login in as kiosk app.
UserSession session;
session.session_id = 1u;
session.user_info.type = user_manager::UserType::kKioskApp;
session.user_info.account_id = AccountId::FromUserEmail("[email protected]");
session.user_info.display_name = "User 1";
session.user_info.display_email = "[email protected]";
Shell::Get()->session_controller()->UpdateUserSession(std::move(session));
EXPECT_EQ(LoginStatus::KIOSK_APP,
Shell::Get()->session_controller()->login_status());
UpdateDisplay("200x100,250x150#250x150%59|300x200%60");
display::test::DisplayManagerTestApi display_manager_test(display_manager());
int64_t id2 = display_manager_test.GetSecondaryDisplay().id();
ASSERT_EQ(0, accept_count());
EXPECT_FALSE(IsNotificationVisible());
// Changes the resolution and apply the result.
SetDisplayResolutionAndNotify(
display_manager_test.GetSecondaryDisplay(), gfx::Size(300, 200), 60,
/*old_is_native=*/false, /*new_is_native=*/true);
EXPECT_FALSE(IsNotificationVisible());
display::ManagedDisplayMode mode;
EXPECT_TRUE(display_manager()->GetSelectedModeForDisplayId(id2, &mode));
EXPECT_EQ(gfx::Size(300, 200), mode.size());
EXPECT_EQ(60.0f, mode.refresh_rate());
}
// Parametrizes all tests to run with display::features::kListAllDisplayModes
// enabled and disabled.
INSTANTIATE_TEST_SUITE_P(All,
ResolutionNotificationControllerTest,
::testing::Bool());
} // namespace ash