text: // 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
  