chromium/ash/wm/overview/overview_controller_unittest.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "ash/wm/overview/overview_controller.h"

#include <memory>

#include "ash/app_list/test/app_list_test_helper.h"
#include "ash/frame_throttler/frame_throttling_controller.h"
#include "ash/frame_throttler/mock_frame_throttling_observer.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/keyboard/ui/keyboard_util.h"
#include "ash/keyboard/ui/test/keyboard_test_util.h"
#include "ash/public/cpp/keyboard/keyboard_switches.h"
#include "ash/public/cpp/overview_test_api.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/wallpaper/views/wallpaper_view.h"
#include "ash/wallpaper/views/wallpaper_widget_controller.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_test_util.h"
#include "ash/wm/overview/overview_metrics.h"
#include "ash/wm/overview/overview_observer.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_test_util.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "ash/wm/window_resizer.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h"
#include "base/command_line.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/window_types.h"
#include "ui/base/hit_test.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/display/screen.h"
#include "ui/events/test/event_generator.h"

namespace ash {
namespace {

gfx::PointF CalculateDragPoint(const WindowResizer& resizer,
                               int delta_x,
                               int delta_y) {
  gfx::PointF location = resizer.GetInitialLocation();
  location.set_x(location.x() + delta_x);
  location.set_y(location.y() + delta_y);
  return location;
}

class TestOverviewObserver : public OverviewObserver {
 public:
  enum AnimationState {
    UNKNOWN,
    COMPLETED,
    CANCELED,
  };

  explicit TestOverviewObserver(bool should_monitor_animation_state)
      : should_monitor_animation_state_(should_monitor_animation_state) {
    Shell::Get()->overview_controller()->AddObserver(this);
  }

  TestOverviewObserver(const TestOverviewObserver&) = delete;
  TestOverviewObserver& operator=(const TestOverviewObserver&) = delete;

  ~TestOverviewObserver() override {
    Shell::Get()->overview_controller()->RemoveObserver(this);
  }

  // OverviewObserver:
  void OnOverviewModeWillStart() override { ++observer_counts_.will_start; }
  void OnOverviewModeStarting() override {
    ++observer_counts_.starting;
    UpdateLastAnimationStates(
        Shell::Get()->overview_controller()->overview_session());
  }
  void OnOverviewModeStartingAnimationComplete(bool canceled) override {
    ++observer_counts_.starting_animation_complete;
    if (!should_monitor_animation_state_)
      return;

    EXPECT_EQ(UNKNOWN, starting_animation_state_);
    starting_animation_state_ = canceled ? CANCELED : COMPLETED;
    if (run_loop_)
      run_loop_->Quit();
  }
  void OnOverviewModeEnding(OverviewSession* overview_session) override {
    ++observer_counts_.ending;
    UpdateLastAnimationStates(overview_session);
  }
  void OnOverviewModeEnded() override { ++observer_counts_.ended; }
  void OnOverviewModeEndingAnimationComplete(bool canceled) override {
    ++observer_counts_.ending_animation_complete;
    if (!should_monitor_animation_state_)
      return;

    EXPECT_EQ(UNKNOWN, ending_animation_state_);
    ending_animation_state_ = canceled ? CANCELED : COMPLETED;
    if (run_loop_)
      run_loop_->Quit();
  }

  void Reset() {
    starting_animation_state_ = UNKNOWN;
    ending_animation_state_ = UNKNOWN;
  }

  void WaitForStartingAnimationComplete() {
    while (starting_animation_state_ != COMPLETED) {
      run_loop_ = std::make_unique<base::RunLoop>();
      run_loop_->RunUntilIdle();
    }
  }

  void WaitForEndingAnimationComplete() {
    while (ending_animation_state_ != COMPLETED) {
      run_loop_ = std::make_unique<base::RunLoop>();
      run_loop_->RunUntilIdle();
    }
  }

  // Checks if all the observed methods have fired the same amount of times.
  bool ObserverCountsEqual() {
    const int expected_count = observer_counts_.will_start;
    DCHECK_GT(expected_count, 0);
    if (observer_counts_.starting != expected_count)
      return false;
    if (observer_counts_.starting_animation_complete != expected_count)
      return false;
    if (observer_counts_.ending != expected_count)
      return false;
    if (observer_counts_.ended != expected_count)
      return false;
    if (observer_counts_.ending_animation_complete != expected_count)
      return false;
    return true;
  }

  bool is_ended() const { return ending_animation_state_ != UNKNOWN; }
  bool is_started() const { return starting_animation_state_ != UNKNOWN; }
  AnimationState starting_animation_state() const {
    return starting_animation_state_;
  }
  AnimationState ending_animation_state() const {
    return ending_animation_state_;
  }
  bool last_animation_was_fade() const { return last_animation_was_fade_; }

 private:
  void UpdateLastAnimationStates(OverviewSession* selector) {
    DCHECK(selector);
    const OverviewEnterExitType enter_exit_type =
        selector->enter_exit_overview_type();

    last_animation_was_fade_ =
        enter_exit_type == OverviewEnterExitType::kFadeInEnter ||
        enter_exit_type == OverviewEnterExitType::kFadeOutExit;
  }

  // Struct which keeps track of the counts a OverviewObserver method has fired.
  // These are used to verify that certain methods have a one to one ratio.
  struct ObserverCounts {
    int will_start;
    int starting;
    int starting_animation_complete;
    int ending;
    int ended;
    int ending_animation_complete;
  } observer_counts_ = {0};

  AnimationState starting_animation_state_ = UNKNOWN;
  AnimationState ending_animation_state_ = UNKNOWN;
  bool last_animation_was_fade_ = false;
  // If false, skips the checks in OnOverviewMode Starting/Ending
  // AnimationComplete.
  bool should_monitor_animation_state_;

  std::unique_ptr<base::RunLoop> run_loop_;
};

void WaitForShowAnimation(aura::Window* window) {
  while (window->layer()->opacity() != 1.f)
    base::RunLoop().RunUntilIdle();
}

}  // namespace

using OverviewControllerTest = AshTestBase;

// Tests that press the overview key in keyboard when a window is being dragged
// in clamshell mode should not toggle overview.
TEST_F(OverviewControllerTest,
       PressOverviewKeyDuringWindowDragInClamshellMode) {
  ASSERT_FALSE(display::Screen::GetScreen()->InTabletMode());
  std::unique_ptr<aura::Window> dragged_window = CreateTestWindow();
  std::unique_ptr<WindowResizer> resizer =
      CreateWindowResizer(dragged_window.get(), gfx::PointF(), HTCAPTION,
                          ::wm::WINDOW_MOVE_SOURCE_MOUSE);
  resizer->Drag(CalculateDragPoint(*resizer, 10, 0), 0);
  EXPECT_TRUE(WindowState::Get(dragged_window.get())->is_dragged());
  GetEventGenerator()->PressKey(ui::VKEY_MEDIA_LAUNCH_APP1, ui::EF_NONE);
  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
  resizer->CompleteDrag();
}

TEST_F(OverviewControllerTest, OcclusionTestWithSnapshot) {
  using OcclusionState = aura::Window::OcclusionState;

  Shell::Get()
      ->overview_controller()
      ->set_occlusion_pause_duration_for_end_for_test(base::Milliseconds(500));
  Shell::Get()->overview_controller()->set_windows_have_snapshot_for_test(true);
  TestOverviewObserver observer(/*should_monitor_animation_state = */ true);
  ui::ScopedAnimationDurationScaleMode non_zero(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  constexpr gfx::Rect kBounds(0, 0, 100, 100);
  std::unique_ptr<aura::Window> window1(CreateAppWindow(kBounds));
  std::unique_ptr<aura::Window> window2(CreateAppWindow(kBounds));
  // Wait for show/hide animation because occlusion tracker because
  // the test depends on opacity.
  WaitForShowAnimation(window1.get());
  WaitForShowAnimation(window2.get());

  window1->TrackOcclusionState();
  window2->TrackOcclusionState();
  EXPECT_EQ(OcclusionState::OCCLUDED, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());

  // Enter with windows.
  EnterOverview();
  EXPECT_EQ(OcclusionState::OCCLUDED, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());

  observer.WaitForStartingAnimationComplete();
  // Occlusion tracking is paused.
  EXPECT_EQ(OcclusionState::OCCLUDED, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());
  WaitForOcclusionStateChange(window1.get(), OcclusionState::VISIBLE);

  // Exit with windows.
  ExitOverview();
  EXPECT_EQ(OcclusionState::VISIBLE, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());
  observer.WaitForEndingAnimationComplete();
  EXPECT_EQ(OcclusionState::VISIBLE, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());
  WaitForOcclusionStateChange(window1.get(), OcclusionState::OCCLUDED);

  observer.Reset();

  // Enter again.
  EnterOverview();
  EXPECT_EQ(OcclusionState::OCCLUDED, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());
  auto* active = window_util::GetActiveWindow();
  EXPECT_EQ(window2.get(), active);

  observer.WaitForStartingAnimationComplete();

  // Window 1 is still occluded because tracker is paused.
  EXPECT_EQ(OcclusionState::OCCLUDED, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());

  WaitForOcclusionStateChange(window1.get(), OcclusionState::VISIBLE);

  wm::ActivateWindow(window1.get());
  observer.WaitForEndingAnimationComplete();

  // Windows are visible because tracker is paused.
  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window1->GetOcclusionState());
  WaitForOcclusionStateChange(window2.get(), OcclusionState::OCCLUDED);
  EXPECT_EQ(OcclusionState::VISIBLE, window1->GetOcclusionState());
}

TEST_F(OverviewControllerTest, OcclusionTestWithoutSnapshot) {
  using OcclusionState = aura::Window::OcclusionState;

  Shell::Get()
      ->overview_controller()
      ->set_occlusion_pause_duration_for_end_for_test(base::Milliseconds(500));
  Shell::Get()->overview_controller()->set_windows_have_snapshot_for_test(
      false);
  TestOverviewObserver observer(/*should_monitor_animation_state = */ true);
  ui::ScopedAnimationDurationScaleMode non_zero(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  constexpr gfx::Rect kBounds(0, 0, 100, 100);
  std::unique_ptr<aura::Window> window1(CreateAppWindow(kBounds));
  std::unique_ptr<aura::Window> window2(CreateAppWindow(kBounds));
  // Wait for show/hide animation because occlusion tracker because
  // the test depends on opacity.
  WaitForShowAnimation(window1.get());
  WaitForShowAnimation(window2.get());

  window1->TrackOcclusionState();
  window2->TrackOcclusionState();
  EXPECT_EQ(OcclusionState::OCCLUDED, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());

  // Enter with windows.
  EnterOverview();
  // Tracker is not paused for enter, and items are forced visible.
  EXPECT_EQ(OcclusionState::VISIBLE, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());

  observer.WaitForStartingAnimationComplete();
  EXPECT_EQ(OcclusionState::VISIBLE, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());

  // Exit with windows.
  ExitOverview();
  EXPECT_EQ(OcclusionState::VISIBLE, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());
  observer.WaitForEndingAnimationComplete();
  EXPECT_EQ(OcclusionState::VISIBLE, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());
  WaitForOcclusionStateChange(window1.get(), OcclusionState::OCCLUDED);

  observer.Reset();

  // Enter again.
  EnterOverview();
  EXPECT_EQ(OcclusionState::VISIBLE, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());
  auto* active = window_util::GetActiveWindow();
  EXPECT_EQ(window2.get(), active);

  observer.WaitForStartingAnimationComplete();

  EXPECT_EQ(OcclusionState::VISIBLE, window1->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());

  wm::ActivateWindow(window1.get());
  observer.WaitForEndingAnimationComplete();

  // Windows are visible because tracker is paused (tracker is paused for exit).
  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
  EXPECT_EQ(OcclusionState::VISIBLE, window2->GetOcclusionState());
  EXPECT_EQ(OcclusionState::VISIBLE, window1->GetOcclusionState());
  WaitForOcclusionStateChange(window2.get(), OcclusionState::OCCLUDED);
  EXPECT_EQ(OcclusionState::VISIBLE, window1->GetOcclusionState());
}

// Tests that PIP windows are not shown in overview.
TEST_F(OverviewControllerTest, PipMustNotInOverviewGridTest) {
  gfx::Rect bounds{100, 100};
  std::unique_ptr<aura::Window> window(
      CreateTestWindowInShellWithBounds(bounds));
  WaitForShowAnimation(window.get());
  auto* controller = Shell::Get()->overview_controller();
  EnterOverview();
  // Ensure |window| is in overview with window state non-PIP.
  EXPECT_TRUE(controller->overview_session()->IsWindowInOverview(window.get()));
  WMEvent pip_event(WM_EVENT_PIP);
  WindowState::Get(window.get())->OnWMEvent(&pip_event);
  // Ensure |window| is not in overview with window state PIP.
  EXPECT_FALSE(
      controller->overview_session()->IsWindowInOverview(window.get()));
}

// Tests that beginning window selection hides the app list.
TEST_F(OverviewControllerTest, SelectingHidesAppList) {
  std::unique_ptr<aura::Window> window(CreateTestWindow());

  GetAppListTestHelper()->ShowAndRunLoop(GetPrimaryDisplay().id());
  GetAppListTestHelper()->CheckVisibility(true);

  EnterOverview();
  GetAppListTestHelper()->WaitUntilIdle();
  GetAppListTestHelper()->CheckVisibility(false);
}

// Tests that windows that are excluded from overview, are actually not shown in
// overview.
TEST_F(OverviewControllerTest, ExcludedWindowsHidden) {
  // Create three windows, one normal, one which is not user positionable (and
  // so should be hidden) and one specifically set to be hidden in overview.
  std::unique_ptr<aura::Window> window1 = CreateTestWindow();
  std::unique_ptr<aura::Window> window2 =
      CreateTestWindow(gfx::Rect(), aura::client::WINDOW_TYPE_POPUP);
  std::unique_ptr<aura::Window> window3 = CreateTestWindow();
  window3->SetProperty(kHideInOverviewKey, true);

  // After creation, all windows are visible.
  ASSERT_TRUE(window1->IsVisible());
  ASSERT_TRUE(window2->IsVisible());
  ASSERT_TRUE(window3->IsVisible());

  // Enter overview. Only one of the three windows is in overview, and visible.
  EnterOverview();
  auto* session = Shell::Get()->overview_controller()->overview_session();
  ASSERT_TRUE(session);
  EXPECT_TRUE(session->IsWindowInOverview(window1.get()));
  EXPECT_FALSE(session->IsWindowInOverview(window2.get()));
  EXPECT_FALSE(session->IsWindowInOverview(window3.get()));
  EXPECT_TRUE(window1->IsVisible());
  EXPECT_FALSE(window2->IsVisible());
  EXPECT_FALSE(window3->IsVisible());

  // On exiting overview, the windows should all be visible. Use a run loop
  // since |session| is destroyed in a post task, and the restoring windows'
  // previous visibility happens in the destructor.
  ExitOverview();
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(window1->IsVisible());
  EXPECT_TRUE(window2->IsVisible());
  EXPECT_TRUE(window3->IsVisible());
}

// Some ash codes are reliant on some OverviewObserver calls matching (i.e. the
// amount of starts should match the amount of ends). This test verifies that
// behavior. Tests for both tablet and clamshell mode.
TEST_F(OverviewControllerTest, ObserverCallsMatch) {
  ui::ScopedAnimationDurationScaleMode non_zero(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  TestOverviewObserver observer(/*should_monitor_animation_state=*/false);

  // Helper which waits for an overview animation to finish.
  auto wait_for_animation = [](bool enter) {
    ShellTestApi().WaitForOverviewAnimationState(
        enter ? OverviewAnimationState::kEnterAnimationComplete
              : OverviewAnimationState::kExitAnimationComplete);
  };

  auto set_tablet_mode_enabled = [](bool enabled) {
    TabletMode::Waiter waiter(enabled);
    if (enabled)
      TabletModeControllerTestApi().EnterTabletMode();
    else
      TabletModeControllerTestApi().LeaveTabletMode();
    waiter.Wait();
  };

  // Tests the case where we enter without windows and do regular enter/exit
  // (wait for enter animation to finish before exiting).
  for (bool is_tablet_mode : {false, true}) {
    SCOPED_TRACE(is_tablet_mode ? "Tablet Mode" : "Clamshell Mode");
    set_tablet_mode_enabled(is_tablet_mode);

    EnterOverview();
    wait_for_animation(/*enter=*/true);
    ExitOverview();
    wait_for_animation(/*enter=*/false);
    EXPECT_TRUE(observer.ObserverCountsEqual());
  }

  // Create one window for the next set of tests.
  std::unique_ptr<aura::Window> window(CreateTestWindow());

  for (bool is_tablet_mode : {false, true}) {
    SCOPED_TRACE(is_tablet_mode ? "Tablet Mode" : "Clamshell Mode");
    set_tablet_mode_enabled(is_tablet_mode);

    // Tests the case where we enter with windows and do regular enter/exit
    // (wait for enter animation to finish before exiting).
    EnterOverview();
    wait_for_animation(/*enter=*/true);
    ExitOverview();
    wait_for_animation(/*enter=*/false);
    EXPECT_TRUE(observer.ObserverCountsEqual());

    // Tests the case where we exit overview before the start animation has
    // completed.
    EnterOverview();
    ExitOverview();
    wait_for_animation(/*enter=*/false);
    EXPECT_TRUE(observer.ObserverCountsEqual());

    // Tests the case where we enter overview before the exit animation has
    // completed.
    EnterOverview();
    wait_for_animation(/*enter=*/true);
    ExitOverview();
    EnterOverview();
    ExitOverview();
    wait_for_animation(/*enter=*/false);
    EXPECT_TRUE(observer.ObserverCountsEqual());
  }
}

// Tests which animation for overview is used in tablet if all windows
// are minimized, and that if overview is exited from the home launcher all
// windows are minimized.
TEST_F(OverviewControllerTest, OverviewEnterExitAnimationTablet) {
  TestOverviewObserver observer(/*should_monitor_animation_state = */ false);

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  // Ensure calls to SetEnabledForTest complete.
  base::RunLoop().RunUntilIdle();

  const gfx::Rect bounds(200, 200);
  std::unique_ptr<aura::Window> window(
      CreateTestWindowInShellWithBounds(bounds));

  EnterOverview();
  EXPECT_FALSE(observer.last_animation_was_fade());

  // Exit to home launcher using fade out animation. This should minimize all
  // windows.
  ExitOverview(OverviewEnterExitType::kFadeOutExit);

  EXPECT_TRUE(observer.last_animation_was_fade());

  ASSERT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
  EXPECT_TRUE(WindowState::Get(window.get())->IsMinimized());

  // All windows are minimized, so we should use the fade in animation to enter
  // overview.
  EnterOverview();
  EXPECT_TRUE(observer.last_animation_was_fade());
}

// Tests that fade animations are not used to enter or exit overview in
// clamshell.
TEST_F(OverviewControllerTest, OverviewEnterExitAnimationClamshell) {
  TestOverviewObserver observer(/*should_monitor_animation_state = */ false);

  const gfx::Rect bounds(200, 200);
  std::unique_ptr<aura::Window> window(
      CreateTestWindowInShellWithBounds(bounds));

  EnterOverview();
  EXPECT_FALSE(observer.last_animation_was_fade());

  ExitOverview();
  EXPECT_FALSE(observer.last_animation_was_fade());

  // Even with all window minimized, overview should not use fade animation to
  // enter.
  ASSERT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
  WindowState::Get(window.get())->Minimize();
  EnterOverview();
  EXPECT_FALSE(observer.last_animation_was_fade());
}

// Tests that overview session exits cleanly if exit is requested before
// previous enter animations finish.
TEST_F(OverviewControllerTest, OverviewExitWhileStillEntering) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  // Ensure calls to SetEnabledForTest complete.
  base::RunLoop().RunUntilIdle();

  const gfx::Rect bounds(200, 200);
  std::unique_ptr<aura::Window> window(
      CreateTestWindowInShellWithBounds(bounds));
  wm::ActivateWindow(window.get());

  // Start overview session - set non zero animation duration so overview is
  // started asynchronously.
  ui::ScopedAnimationDurationScaleMode non_zero(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  EnterOverview();

  // Exit to home launcher using fade out animation. This should minimize all
  // windows.
  TestOverviewObserver observer(/*should_monitor_animation_state = */ true);
  ExitOverview(OverviewEnterExitType::kFadeOutExit);

  EXPECT_TRUE(observer.last_animation_was_fade());

  // Verify that the overview exits cleanly.
  observer.WaitForEndingAnimationComplete();

  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
  EXPECT_TRUE(WindowState::Get(window.get())->IsMinimized());
}

// Tests that overview animations continue even if a window gets destroyed
// during the animation.
TEST_F(OverviewControllerTest, CloseWindowDuringAnimation) {
  // Create two windows. They should both be visible so that they both get
  // animated.
  std::unique_ptr<aura::Window> window1 = CreateAppWindow(gfx::Rect(250, 100));
  std::unique_ptr<aura::Window> window2 =
      CreateAppWindow(gfx::Rect(250, 250, 250, 100));

  ui::ScopedAnimationDurationScaleMode non_zero(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  EnterOverview();

  // Destroy a window during the enter animation.
  window1.reset();
  ShellTestApi().WaitForOverviewAnimationState(
      OverviewAnimationState::kEnterAnimationComplete);
  ASSERT_TRUE(Shell::Get()->overview_controller()->InOverviewSession());

  ExitOverview();

  // Destroy a window during the exit animation.
  window2.reset();
  ShellTestApi().WaitForOverviewAnimationState(
      OverviewAnimationState::kExitAnimationComplete);
  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
}

// Quick test on all `OverviewStartAction`s to verify that they are recorded
// correctly in uma metric.
TEST_F(OverviewControllerTest, OverviewStartActionHistogramTest) {
  base::HistogramTester histogram_tester;
  constexpr char kOverviewStartActionHistogram[] = "Ash.Overview.StartAction";
  OverviewController* overview_controller = OverviewController::Get();

  for (OverviewStartAction start_action : {
           OverviewStartAction::kSplitView,
           OverviewStartAction::kAccelerator,
           OverviewStartAction::kDragWindowFromShelf,
           OverviewStartAction::kExitHomeLauncher,
           OverviewStartAction::kOverviewButton,
           OverviewStartAction::kOverviewButtonLongPress,
           OverviewStartAction::kBentoBar_DEPRECATED,
           OverviewStartAction::k3FingerVerticalScroll,
           OverviewStartAction::kDevTools,
           OverviewStartAction::kTests,
           OverviewStartAction::kOverviewDeskSwitch,
           OverviewStartAction::kDeskButton,
           OverviewStartAction::kFasterSplitScreenSetup,
       }) {
    // Verify the initial count for the histogram.
    histogram_tester.ExpectBucketCount(kOverviewStartActionHistogram,
                                       start_action,
                                       /*expected_count=*/0);
    overview_controller->StartOverview(start_action);
    histogram_tester.ExpectBucketCount(kOverviewStartActionHistogram,
                                       start_action,
                                       /*expected_count=*/1);
    overview_controller->EndOverview(OverviewEndAction::kTests);
  }
}

// Quick test on all `OverviewEndAction`s to verify that they are recorded
// correctly in uma metric.
TEST_F(OverviewControllerTest, OverviewEndActionHistogramTest) {
  base::HistogramTester histogram_tester;
  constexpr char kOverviewEndActionHistogram[] = "Ash.Overview.EndAction";
  OverviewController* overview_controller = OverviewController::Get();

  for (OverviewEndAction end_action : {
           OverviewEndAction::kSplitView,
           OverviewEndAction::kDragWindowFromShelf,
           OverviewEndAction::kEnterHomeLauncher,
           OverviewEndAction::kClickingOutsideWindowsInOverview,
           OverviewEndAction::kWindowActivating,
           OverviewEndAction::kLastWindowRemoved,
           OverviewEndAction::kDisplayAdded,
           OverviewEndAction::kKeyEscapeOrBack,
           OverviewEndAction::kDeskActivation,
           OverviewEndAction::kOverviewButton,
           OverviewEndAction::kOverviewButtonLongPress,
           OverviewEndAction::k3FingerVerticalScroll,
           OverviewEndAction::kEnabledDockedMagnifier,
           OverviewEndAction::kUserSwitch,
           OverviewEndAction::kStartedWindowCycle,
           OverviewEndAction::kShuttingDown,
           OverviewEndAction::kAppListActivatedInClamshell,
           OverviewEndAction::kShelfAlignmentChanged,
           OverviewEndAction::kDevTools,
           OverviewEndAction::kTests,
           OverviewEndAction::kShowGlanceables_DEPRECATED,
       }) {
    // Verify the initial count for the histogram.
    histogram_tester.ExpectBucketCount(kOverviewEndActionHistogram, end_action,
                                       /*expected_count=*/0);
    overview_controller->StartOverview(OverviewStartAction::kTests);
    overview_controller->EndOverview(end_action);
    histogram_tester.ExpectBucketCount(kOverviewEndActionHistogram, end_action,
                                       /*expected_count=*/1);
  }
}

// A subclass of DeskSwitchAnimationWaiter that additionally attempts to start
// overview after the desk animation screenshots have been taken. Using the
// regular DeskSwitchAnimatorWaiter and attempting to start overview before
// calling Wait() would be similar to performing a desk switch when overview is
// already open. This waiter mocks the behavior of trying to enter overview
// while the desk switch is already in motion.
class DeskSwitchStartOverviewAnimationWaiter
    : public DeskSwitchAnimationWaiter {
 public:
  DeskSwitchStartOverviewAnimationWaiter() = default;
  DeskSwitchStartOverviewAnimationWaiter(
      const DeskSwitchStartOverviewAnimationWaiter&) = delete;
  DeskSwitchStartOverviewAnimationWaiter& operator=(
      const DeskSwitchStartOverviewAnimationWaiter&) = delete;
  ~DeskSwitchStartOverviewAnimationWaiter() override = default;

  // DeskSwitchAnimationWaiter:
  void OnDeskActivationChanged(const Desk* activated,
                               const Desk* deactivated) override {
    Shell::Get()->overview_controller()->StartOverview(
        OverviewStartAction::kTests);
  }
};

// Tests that entering overview while performing a desk animation is disallowed,
// but exiting is still done.
TEST_F(OverviewControllerTest, OverviewEnterExitWhileDeskAnimation) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kKeyboard);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk1 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk2 = desks_controller->GetDeskAtIndex(1);

  ui::ScopedAnimationDurationScaleMode non_zero(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Animate to desk 2. Try to enter overview while animating. On desk animation
  // finished, we shouldn't be in overview.
  DeskSwitchStartOverviewAnimationWaiter waiter;
  desks_controller->ActivateDesk(desk2, DesksSwitchSource::kDeskSwitchShortcut);
  waiter.Wait();
  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());

  EnterOverview();
  ASSERT_TRUE(Shell::Get()->overview_controller()->InOverviewSession());

  // Tests that exiting overview works as it is part of the desk switch
  // animation.
  ActivateDesk(desk1);
  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
}

// Tests that clipping the window to remove the top view inset (header) works as
// expected.
TEST_F(OverviewControllerTest, WindowClipping) {
  std::unique_ptr<aura::Window> window = CreateTestWindow();
  window->SetBounds(gfx::Rect(300, 300));
  window->SetProperty(aura::client::kTopViewInset, 20);
  ASSERT_EQ(gfx::Rect(), window->layer()->GetTargetClipRect());

  ui::ScopedAnimationDurationScaleMode non_zero(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Tests that the clipping bounds in overview will clip away the top inset.
  // There is a extra pixel added to account for what seems to be a rounding
  // error.
  EnterOverview();
  WaitForOverviewEnterAnimation();
  EXPECT_EQ(gfx::Rect(0, 21, 300, 279), window->layer()->GetTargetClipRect());

  // Tests that we animate to the window size from the overview clip on exit.
  ExitOverview();
  EXPECT_EQ(gfx::Rect(300, 300), window->layer()->GetTargetClipRect());

  // Tests that the clipping is removed after the animation ends.
  WaitForOverviewExitAnimation();
  EXPECT_EQ(gfx::Rect(), window->layer()->GetTargetClipRect());
}

class OverviewVirtualKeyboardTest : public OverviewControllerTest {
 protected:
  void SetUp() override {
    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        keyboard::switches::kEnableVirtualKeyboard);
    OverviewControllerTest::SetUp();

    TabletModeControllerTestApi().EnterTabletMode();
    base::RunLoop().RunUntilIdle();
    ASSERT_TRUE(keyboard::IsKeyboardEnabled());
    keyboard::test::WaitUntilLoaded();

    keyboard_ui_controller()->GetKeyboardWindow()->SetBounds(
        keyboard::test::KeyboardBoundsFromRootBounds(
            Shell::GetPrimaryRootWindow()->bounds(), 100));
    // Wait for keyboard window to load.
    base::RunLoop().RunUntilIdle();
  }

  keyboard::KeyboardUIController* keyboard_ui_controller() {
    return keyboard::KeyboardUIController::Get();
  }
};

TEST_F(OverviewVirtualKeyboardTest, ToggleOverviewModeHidesVirtualKeyboard) {
  keyboard_ui_controller()->ShowKeyboard(false /* locked */);
  ASSERT_TRUE(keyboard::test::WaitUntilShown());

  EnterOverview();

  // Timeout failure here if the keyboard does not hide.
  keyboard::test::WaitUntilHidden();
}

TEST_F(OverviewVirtualKeyboardTest,
       ToggleOverviewModeDoesNotHideLockedVirtualKeyboard) {
  keyboard_ui_controller()->ShowKeyboard(true /* locked */);
  ASSERT_TRUE(keyboard::test::WaitUntilShown());

  EnterOverview();
  EXPECT_FALSE(keyboard::test::IsKeyboardHiding());
}

// Tests that frame throttling starts and ends accordingly when overview starts
// and ends.
TEST_F(OverviewControllerTest, FrameThrottling) {
  MockFrameThrottlingObserver observer;
  FrameThrottlingController* frame_throttling_controller =
      Shell::Get()->frame_throttling_controller();
  frame_throttling_controller->AddArcObserver(&observer);
  const int browser_window_count = 3;
  const int arc_window_count = 2;

  const std::vector<viz::FrameSinkId> ids{{1u, 1u}, {2u, 2u}, {3u, 3u}};
  std::unique_ptr<aura::Window>
      created_windows[browser_window_count + arc_window_count];
  for (int i = 0; i < browser_window_count; ++i) {
    created_windows[i] =
        CreateAppWindow(gfx::Rect(), chromeos::AppType::BROWSER);
    created_windows[i]->SetEmbedFrameSinkId(ids[i]);
  }

  std::vector<aura::Window*> arc_windows(arc_window_count, nullptr);
  for (int i = 0; i < arc_window_count; ++i) {
    created_windows[i + browser_window_count] =
        CreateAppWindow(gfx::Rect(), chromeos::AppType::ARC_APP);
    arc_windows[i] = created_windows[i + browser_window_count].get();
  }

  EXPECT_CALL(observer,
              OnThrottlingStarted(
                  testing::UnorderedElementsAreArray(arc_windows),
                  frame_throttling_controller->GetCurrentThrottledFrameRate()));
  EnterOverview();
  EXPECT_THAT(frame_throttling_controller->GetFrameSinkIdsToThrottle(),
              ::testing::UnorderedElementsAreArray(ids));

  EXPECT_CALL(observer, OnThrottlingEnded());
  ExitOverview();
  EXPECT_TRUE(frame_throttling_controller->GetFrameSinkIdsToThrottle().empty());

  frame_throttling_controller->RemoveArcObserver(&observer);
}

// Tests that Ash.Overview.DeskCount metric is recorded.
TEST_F(OverviewControllerTest, RecordsDeskCountMetric) {
  base::HistogramTester histogram_tester;
  EnterOverview();
  ExitOverview();
  histogram_tester.ExpectUniqueSample("Ash.Overview.DeskCount", 1, 1);

  DesksController::Get()->NewDesk(DesksCreationRemovalSource::kKeyboard);
  ASSERT_EQ(2u, DesksController::Get()->desks().size());
  EnterOverview();
  ExitOverview();
  histogram_tester.ExpectBucketCount("Ash.Overview.DeskCount", 1, 1);
  histogram_tester.ExpectBucketCount("Ash.Overview.DeskCount", 2, 1);
}

class OverviewEnterFromWallpaperTest : public OverviewControllerTest {
 public:
  OverviewEnterFromWallpaperTest() {
    scoped_feature_list_.InitWithFeatures(
        {features::kEnterOverviewFromWallpaper}, {});
  }
  ~OverviewEnterFromWallpaperTest() override = default;

  WallpaperView* wallpaper_view() {
    return Shell::Get()
        ->GetPrimaryRootWindowController()
        ->wallpaper_widget_controller()
        ->wallpaper_view();
  }

 protected:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that the user can enter/exit overview by clicking on the wallpaper.
TEST_F(OverviewEnterFromWallpaperTest,
       OverviewEnterExitClamshellFromWallpaper) {
  std::unique_ptr<aura::Window> window1(
      CreateTestWindowInShellWithBounds(gfx::Rect(400, 400)));

  ASSERT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());

  GetEventGenerator()->set_current_screen_location(
      wallpaper_view()->GetBoundsInScreen().right_center());
  GetEventGenerator()->ClickLeftButton();
  ASSERT_TRUE(Shell::Get()->overview_controller()->InOverviewSession());

  GetEventGenerator()->ClickLeftButton();
  ASSERT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
}

}  // namespace ash