blob: d2002e2270494ab287352892959f8a1f8d484cc9 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/exo/ui_lock_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/shell.h"
#include "ash/wm/window_state.h"
#include "base/feature_list.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/exo/buffer.h"
#include "components/exo/display.h"
#include "components/exo/shell_surface.h"
#include "components/exo/surface.h"
#include "components/exo/test/exo_test_base.h"
#include "components/exo/test/exo_test_helper.h"
#include "components/exo/wm_helper.h"
#include "components/fullscreen_control/fullscreen_control_popup.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/class_property.h"
#include "ui/gfx/animation/animation_test_api.h"
#include "ui/wm/core/window_util.h"
namespace exo {
namespace {
constexpr char kNoEscHoldAppId[] = "no-esc-hold";
constexpr char kEscToMinimizeAppId[] = "esc-to-minimize";
struct SurfaceTriplet {
std::unique_ptr<Surface> surface;
std::unique_ptr<ShellSurface> shell_surface;
std::unique_ptr<Buffer> buffer;
aura::Window* GetAlwaysOnTopContainer() {
aura::Window* native_window = GetTopLevelWidget()->GetNativeWindow();
return ash::Shell::GetContainer(native_window->GetRootWindow(),
ash::kShellWindowId_AlwaysOnTopContainer);
}
views::Widget* GetTopLevelWidget() {
views::Widget* top_level_widget =
views::Widget::GetTopLevelWidgetForNativeView(surface->window());
assert(top_level_widget);
return top_level_widget;
}
aura::Window* GetTopLevelWindow() {
auto* top_level_widget = views::Widget::GetTopLevelWidgetForNativeView(
shell_surface->host_window());
assert(top_level_widget);
return top_level_widget->GetNativeWindow();
}
ash::WindowState* GetTopLevelWindowState() {
return ash::WindowState::Get(GetTopLevelWindow());
}
};
class UILockControllerTest : public test::ExoTestBase {
public:
UILockControllerTest()
: test::ExoTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
~UILockControllerTest() override = default;
UILockControllerTest(const UILockControllerTest&) = delete;
UILockControllerTest& operator=(const UILockControllerTest&) = delete;
protected:
class TestPropertyResolver : public exo::WMHelper::AppPropertyResolver {
public:
TestPropertyResolver() = default;
~TestPropertyResolver() override = default;
void PopulateProperties(
const Params& params,
ui::PropertyHandler& out_properties_container) override {
out_properties_container.SetProperty(chromeos::kEscHoldToExitFullscreen,
params.app_id != kNoEscHoldAppId);
out_properties_container.SetProperty(
chromeos::kEscHoldExitFullscreenToMinimized,
params.app_id == kEscToMinimizeAppId);
}
};
// test::ExoTestBase:
void SetUp() override {
test::ExoTestBase::SetUp();
seat_ = std::make_unique<Seat>();
scoped_feature_list_.InitAndEnableFeature(
chromeos::features::kExoLockNotification);
WMHelper::GetInstance()->RegisterAppPropertyResolver(
std::make_unique<TestPropertyResolver>());
}
void TearDown() override {
seat_.reset();
test::ExoTestBase::TearDown();
}
SurfaceTriplet BuildSurface(gfx::Point origin, int w, int h) {
auto surface = std::make_unique<Surface>();
auto shell_surface = std::make_unique<ShellSurface>(
surface.get(), origin,
/*can_minimize=*/true, ash::desks_util::GetActiveDeskContainerId());
auto buffer = std::make_unique<Buffer>(
exo_test_helper()->CreateGpuMemoryBuffer({w, h}));
surface->Attach(buffer.get());
return {std::move(surface), std::move(shell_surface), std::move(buffer)};
}
SurfaceTriplet BuildSurface(int w, int h) {
return BuildSurface(gfx::Point(0, 0), w, h);
}
views::Widget* GetEscNotification(SurfaceTriplet* surface) {
return seat_->GetUILockControllerForTesting()->GetEscNotificationForTesting(
surface->GetTopLevelWindow());
}
bool IsExitPopupVisible(aura::Window* window) {
FullscreenControlPopup* popup =
seat_->GetUILockControllerForTesting()->GetExitPopupForTesting(window);
if (popup && popup->IsAnimating()) {
gfx::AnimationTestApi animation_api(popup->GetAnimationForTesting());
base::TimeTicks now = base::TimeTicks::Now();
animation_api.SetStartTime(now);
animation_api.Step(now + base::Milliseconds(500));
}
return popup && popup->IsVisible();
}
std::unique_ptr<Seat> seat_;
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(UILockControllerTest, HoldingEscapeExitsFullscreen) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
auto* window_state = test_surface.GetTopLevelWindowState();
EXPECT_TRUE(window_state->IsFullscreen());
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::Seconds(1));
EXPECT_TRUE(window_state->IsFullscreen()); // no change yet
task_environment()->FastForwardBy(base::Seconds(1));
EXPECT_FALSE(window_state->IsFullscreen());
EXPECT_TRUE(window_state->IsNormalStateType());
}
TEST_F(UILockControllerTest, HoldingCtrlEscapeDoesNotExitFullscreen) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
auto* window_state = test_surface.GetTopLevelWindowState();
EXPECT_TRUE(window_state->IsFullscreen());
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_CONTROL_DOWN);
task_environment()->FastForwardBy(base::Seconds(2));
EXPECT_TRUE(window_state->IsFullscreen());
}
TEST_F(UILockControllerTest,
HoldingEscapeOnlyExitsFullscreenIfWindowPropertySet) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
// Do not set chromeos::kEscHoldToExitFullscreen on TopLevelWindow.
test_surface.shell_surface->SetApplicationId(kNoEscHoldAppId);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
auto* window_state = test_surface.GetTopLevelWindowState();
EXPECT_TRUE(window_state->IsFullscreen());
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::Seconds(2));
EXPECT_TRUE(window_state->IsFullscreen());
}
TEST_F(UILockControllerTest, HoldingEscapeOnlyExitsFocusedFullscreen) {
SurfaceTriplet test_surface1 = BuildSurface(1024, 768);
test_surface1.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface1.shell_surface->SetFullscreen(true);
test_surface1.surface->Commit();
SurfaceTriplet test_surface2 = BuildSurface(1024, 768);
test_surface2.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface2.shell_surface->SetFullscreen(true);
test_surface2.surface->Commit();
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::Seconds(2));
EXPECT_TRUE(test_surface1.GetTopLevelWindowState()->IsFullscreen());
EXPECT_FALSE(test_surface2.GetTopLevelWindowState()->IsFullscreen());
}
TEST_F(UILockControllerTest, DestroyingWindowCancels) {
std::unique_ptr<SurfaceTriplet> test_surface =
std::make_unique<SurfaceTriplet>(BuildSurface(1024, 768));
test_surface->shell_surface->SetUseImmersiveForFullscreen(false);
test_surface->shell_surface->SetFullscreen(true);
test_surface->surface->Commit();
auto* window_state = test_surface->GetTopLevelWindowState();
EXPECT_TRUE(window_state->IsFullscreen());
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::Seconds(1));
test_surface.reset(); // Destroying the Surface destroys the Window
task_environment()->FastForwardBy(base::Seconds(3));
// The implicit assertion is that the code doesn't crash.
}
TEST_F(UILockControllerTest, FocusChangeCancels) {
// Arrange: two windows, one is fullscreen and focused
SurfaceTriplet other_surface = BuildSurface(1024, 768);
other_surface.surface->Commit();
SurfaceTriplet fullscreen_surface = BuildSurface(1024, 768);
fullscreen_surface.shell_surface->SetUseImmersiveForFullscreen(false);
fullscreen_surface.shell_surface->SetFullscreen(true);
fullscreen_surface.surface->Commit();
EXPECT_EQ(fullscreen_surface.surface.get(), seat_->GetFocusedSurface());
EXPECT_FALSE(fullscreen_surface.GetTopLevelWindowState()->IsMinimized());
// Act: Press escape, then toggle focus back and forth
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::Seconds(1));
wm::ActivateWindow(other_surface.surface->window());
wm::ActivateWindow(fullscreen_surface.surface->window());
task_environment()->FastForwardBy(base::Seconds(2));
// Assert: Fullscreen window was not minimized, despite regaining focus.
EXPECT_FALSE(fullscreen_surface.GetTopLevelWindowState()->IsMinimized());
EXPECT_EQ(fullscreen_surface.surface.get(), seat_->GetFocusedSurface());
}
TEST_F(UILockControllerTest, ShortHoldEscapeDoesNotExitFullscreen) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
auto* window_state = test_surface.GetTopLevelWindowState();
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::Seconds(1));
GetEventGenerator()->ReleaseKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::Seconds(2));
EXPECT_TRUE(window_state->IsFullscreen());
}
TEST_F(UILockControllerTest, HoldingEscapeMinimizesIfPropertySet) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
// Set chromeos::kEscHoldExitFullscreenToMinimized on TopLevelWindow.
test_surface.shell_surface->SetApplicationId(kEscToMinimizeAppId);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
auto* window_state = test_surface.GetTopLevelWindowState();
EXPECT_TRUE(window_state->IsFullscreen());
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::Seconds(1));
EXPECT_TRUE(window_state->IsFullscreen()); // no change yet
task_environment()->FastForwardBy(base::Seconds(1));
EXPECT_FALSE(window_state->IsFullscreen());
EXPECT_TRUE(window_state->IsMinimized());
}
TEST_F(UILockControllerTest, HoldingEscapeDoesNotMinimizeIfWindowed) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
// Set chromeos::kEscHoldExitFullscreenToMinimized on TopLevelWindow.
test_surface.shell_surface->SetApplicationId(kEscToMinimizeAppId);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.surface->Commit();
auto* window_state = test_surface.GetTopLevelWindowState();
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::Seconds(2));
EXPECT_FALSE(window_state->IsMinimized());
}
TEST_F(UILockControllerTest, FullScreenShowsEscNotification) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
EXPECT_TRUE(test_surface.GetTopLevelWindowState()->IsFullscreen());
EXPECT_TRUE(GetEscNotification(&test_surface));
}
TEST_F(UILockControllerTest, EscNotificationClosesAfterDuration) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
EXPECT_TRUE(GetEscNotification(&test_surface));
task_environment()->FastForwardBy(base::Seconds(5));
EXPECT_FALSE(GetEscNotification(&test_surface));
}
TEST_F(UILockControllerTest, HoldingEscapeHidesNotification) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
EXPECT_TRUE(test_surface.GetTopLevelWindowState()->IsFullscreen());
EXPECT_TRUE(GetEscNotification(&test_surface));
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::Seconds(3));
EXPECT_FALSE(test_surface.GetTopLevelWindowState()->IsFullscreen());
EXPECT_FALSE(GetEscNotification(&test_surface));
}
TEST_F(UILockControllerTest, LosingFullscreenHidesNotification) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
EXPECT_TRUE(test_surface.GetTopLevelWindowState()->IsFullscreen());
EXPECT_TRUE(GetEscNotification(&test_surface));
// Have surface loose fullscreen, notification should now be hidden.
test_surface.shell_surface->Minimize();
test_surface.shell_surface->SetFullscreen(false);
test_surface.surface->Commit();
EXPECT_FALSE(test_surface.GetTopLevelWindowState()->IsFullscreen());
EXPECT_FALSE(
seat_->GetUILockControllerForTesting()->GetEscNotificationForTesting(
test_surface.GetTopLevelWindow()));
}
TEST_F(UILockControllerTest, EscNotificationIsReshown) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
EXPECT_TRUE(GetEscNotification(&test_surface));
// Stop fullscreen.
test_surface.shell_surface->SetFullscreen(false);
EXPECT_FALSE(
seat_->GetUILockControllerForTesting()->GetEscNotificationForTesting(
test_surface.GetTopLevelWindow()));
// Fullscreen should show notification since it did not stay visible for
// duration.
test_surface.shell_surface->SetFullscreen(true);
EXPECT_TRUE(GetEscNotification(&test_surface));
// After duration, notification should be removed.
task_environment()->FastForwardBy(base::Seconds(5));
EXPECT_FALSE(GetEscNotification(&test_surface));
// Notification is shown after fullscreen toggle.
test_surface.shell_surface->SetFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
EXPECT_TRUE(GetEscNotification(&test_surface));
}
TEST_F(UILockControllerTest, EscNotificationShowsOnSecondaryDisplay) {
// Create surface on secondary display.
UpdateDisplay("900x800,70x600");
SurfaceTriplet test_surface = BuildSurface(gfx::Point(900, 100), 200, 200);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
// Esc notification should be in secondary display.
views::Widget* esc_notification = GetEscNotification(&test_surface);
EXPECT_TRUE(GetSecondaryDisplay().bounds().Contains(
esc_notification->GetWindowBoundsInScreen()));
}
TEST_F(UILockControllerTest, ExitPopup) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
auto* window_state = test_surface.GetTopLevelWindowState();
EXPECT_TRUE(window_state->IsFullscreen());
aura::Window* window = test_surface.GetTopLevelWindow();
EXPECT_FALSE(IsExitPopupVisible(window));
EXPECT_TRUE(GetEscNotification(&test_surface));
// Move mouse above y=3 should not show exit popup while notification is
// visible.
GetEventGenerator()->MoveMouseTo(0, 2);
EXPECT_FALSE(IsExitPopupVisible(window));
// Wait for notification to close, now exit popup should show.
task_environment()->FastForwardBy(base::Seconds(5));
EXPECT_FALSE(GetEscNotification(&test_surface));
GetEventGenerator()->MoveMouseTo(1, 2);
EXPECT_TRUE(IsExitPopupVisible(window));
// Move mouse below y=150 should hide exit popup.
GetEventGenerator()->MoveMouseTo(0, 160);
EXPECT_FALSE(IsExitPopupVisible(window));
// Move mouse back above y=3 should show exit popup.
GetEventGenerator()->MoveMouseTo(0, 2);
EXPECT_TRUE(IsExitPopupVisible(window));
// Popup should hide after 3s.
task_environment()->FastForwardBy(base::Seconds(5));
EXPECT_FALSE(IsExitPopupVisible(window));
// Moving mouse to y=100, then above y=3 should still have popup hidden.
GetEventGenerator()->MoveMouseTo(0, 100);
GetEventGenerator()->MoveMouseTo(0, 2);
EXPECT_FALSE(IsExitPopupVisible(window));
// Moving mouse below y=150, then above y=3 should show exit popup.
GetEventGenerator()->MoveMouseTo(0, 160);
GetEventGenerator()->MoveMouseTo(0, 2);
EXPECT_TRUE(IsExitPopupVisible(window));
// Clicking exit popup should exit fullscreen.
FullscreenControlPopup* popup =
seat_->GetUILockControllerForTesting()->GetExitPopupForTesting(window);
GetEventGenerator()->MoveMouseTo(
popup->GetPopupWidget()->GetWindowBoundsInScreen().CenterPoint());
GetEventGenerator()->ClickLeftButton();
EXPECT_FALSE(window_state->IsFullscreen());
EXPECT_FALSE(IsExitPopupVisible(window));
}
TEST_F(UILockControllerTest, ExitPopupNotShownIfPropertySet) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
// Set chromeos::kEscHoldExitFullscreenToMinimized on TopLevelWindow.
test_surface.shell_surface->SetApplicationId(kEscToMinimizeAppId);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
EXPECT_FALSE(IsExitPopupVisible(test_surface.GetTopLevelWindow()));
// Move mouse above y=3 should not show exit popup.
GetEventGenerator()->MoveMouseTo(0, 2);
EXPECT_FALSE(IsExitPopupVisible(test_surface.GetTopLevelWindow()));
}
TEST_F(UILockControllerTest, OnlyShowWhenActive) {
SurfaceTriplet test_surface1 = BuildSurface(1024, 768);
test_surface1.surface->Commit();
SurfaceTriplet test_surface2 = BuildSurface(gfx::Point(100, 100), 200, 200);
test_surface2.surface->Commit();
// Surface2 is active when we make Surface1 fullscreen.
// Esc notification, and exit popup should not be shown.
test_surface1.shell_surface->SetFullscreen(true);
EXPECT_FALSE(GetEscNotification(&test_surface1));
GetEventGenerator()->MoveMouseTo(0, 2);
EXPECT_FALSE(IsExitPopupVisible(test_surface1.GetTopLevelWindow()));
}
} // namespace
} // namespace exo