chromium/ash/wm/snap_group/snap_group_pixeltest.cc

// 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 <memory>
#include <optional>

#include "ash/constants/ash_features.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_util.h"
#include "ash/test/pixel/ash_pixel_differ.h"
#include "ash/wm/overview/overview_item_base.h"
#include "ash/wm/overview/overview_test_util.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/overview/scoped_overview_transform_window.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/snap_group/snap_group_test_util.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_divider.h"
#include "ash/wm/splitview/split_view_test_util.h"
#include "ash/wm/window_cycle/window_cycle_controller.h"
#include "ash/wm/window_cycle/window_cycle_list.h"
#include "ash/wm/window_cycle/window_cycle_view.h"
#include "base/test/scoped_feature_list.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/aura/window_tree_host.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/point.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/window_util.h"

namespace ash {

// Visual regression tests for Snap Groups feature, comparing visuals against
// established benchmarks.
class SnapGroupPixelTest : public AshTestBase {
 public:
  SnapGroupPixelTest() {
    scoped_feature_list_.InitWithFeatures(
        {features::kSnapGroup, features::kForestFeature,
         features::kSavedDeskUiRevamp},
        {});
  }
  SnapGroupPixelTest(const SnapGroupPixelTest&) = delete;
  SnapGroupPixelTest& operator=(const SnapGroupPixelTest&) = delete;
  ~SnapGroupPixelTest() override = default;

 private:
  // AshTestBase:
  std::optional<pixel_test::InitParams> CreatePixelTestInitParams()
      const override {
    return pixel_test::InitParams();
  }

  base::test::ScopedFeatureList scoped_feature_list_;
};

// -----------------------------------------------------------------------------
// Landscape:

// Visual regression test for divider component (default and hover states).
TEST_F(SnapGroupPixelTest, SnapGroupDividerBasic) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  DecorateWindow(w1.get(), /*title=*/u"w1", SK_ColorGREEN);
  auto* w1_widget = views::Widget::GetWidgetForNativeView(w1.get());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  DecorateWindow(w2.get(), /*title=*/u"w2", SK_ColorBLUE);
  auto* w2_widget = views::Widget::GetWidgetForNativeView(w2.get());

  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);

  auto* divider_widget = GetTopmostSnapGroupDivider()->divider_widget();
  ASSERT_TRUE(divider_widget);

  // Verify the snap group divider UI components on default state.
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "snap_group_divider_default_state",
      /*revision_number=*/0, divider_widget, w1_widget, w2_widget));

  // Move the mouse to the position that is a off the center(divider handler
  // view).
  event_generator->MoveMouseTo(
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint() +
      gfx::Vector2d(0, 10));

  // Verify the snap group divider UI components on mouse hover.
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "snap_group_divider_hover_state",
      /*revision_number=*/0, divider_widget, w1_widget, w2_widget));
}

// Visual regression test partial split screen layout.
TEST_F(SnapGroupPixelTest, PartialSplit) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  DecorateWindow(w1.get(), /*title=*/u"w1", SK_ColorGREEN);
  auto* w1_widget = views::Widget::GetWidgetForNativeView(w1.get());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  DecorateWindow(w2.get(), /*title=*/u"w2", SK_ColorBLUE);
  auto* w2_widget = views::Widget::GetWidgetForNativeView(w2.get());

  SnapOneTestWindow(w1.get(),
                    /*state_type=*/chromeos::WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio);
  VerifySplitViewOverviewSession(w1.get());
  ClickOverviewItem(GetEventGenerator(), w2.get());
  auto* snap_group_controller = SnapGroupController::Get();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  auto* divider_widget = GetTopmostSnapGroupDivider()->divider_widget();
  ASSERT_TRUE(divider_widget);

  // Verify the snap group divider UI components on in 2/3 and 1/3 split screen
  // layout.
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "snap_group_partial_split",
      /*revision_number=*/0, divider_widget, w1_widget, w2_widget));
}

// Visual regression test for `OverviewGroupItem`.
TEST_F(SnapGroupPixelTest, OverviewGroupItem) {
  base::test::ScopedFeatureList scoped_feature_list{features::kForestFeature};

  ScopedOverviewTransformWindow::SetImmediateCloseForTests(/*immediate=*/true);

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  DecorateWindow(w1.get(), /*title=*/u"w1", SK_ColorGREEN);
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  DecorateWindow(w2.get(), /*title=*/u"w2", SK_ColorBLUE);

  SnapTwoTestWindows(w1.get(), /*window2=*/w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  OverviewItemBase* overview_group_item = GetOverviewItemForWindow(w1.get());
  ASSERT_TRUE(overview_group_item);
  auto* group_item_widget = overview_group_item->item_widget();
  ASSERT_TRUE(group_item_widget);

  // Verify the `OverviewGroupItem` visuals.
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "overviewgroupitem",
      /*revision_number=*/1, group_item_widget));

  // Verify the visuals after one of the windows in the group got destroyed.
  w2.reset();
  OverviewItemBase* item_after_destruction = GetOverviewItemForWindow(w1.get());
  ASSERT_TRUE(item_after_destruction);
  auto* remaining_item_widget = item_after_destruction->item_widget();
  ASSERT_TRUE(item_after_destruction);
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "remaining_item_widget",
      /*revision_number=*/1, remaining_item_widget));
}

// Visual regression test for Snap Group in window cycle view.
TEST_F(SnapGroupPixelTest, WindowCycleView) {
  WindowCycleList::SetDisableInitialDelayForTesting(true);

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  DecorateWindow(w1.get(), /*title=*/u"w1", SK_ColorGREEN);
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  DecorateWindow(w2.get(), /*title=*/u"w2", SK_ColorBLUE);

  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  // Explicitly activate the primary-snapped window so that it comes before
  // secondary-snapped window in MRU order, anticipating a future Alt+Tab
  // revamp.
  wm::ActivateWindow(w1.get());

  auto* event_generator = GetEventGenerator();
  event_generator->PressAndReleaseKey(ui::VKEY_TAB, ui::EF_ALT_DOWN);

  WindowCycleController* window_cycle_controller =
      Shell::Get()->window_cycle_controller();
  EXPECT_TRUE(window_cycle_controller->IsCycling());

  const WindowCycleView* window_cycle_view =
      window_cycle_controller->window_cycle_list()->cycle_view();
  ASSERT_TRUE(window_cycle_view);

  views::Widget* window_cycle_widget =
      const_cast<views::Widget*>(window_cycle_view->GetWidget());
  ASSERT_TRUE(window_cycle_widget);

  // Verify the visuals with secondary-snapped window gets focused.
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "window_cycle_with_snap_group_secondary_focused",
      /*revision_number=*/0, window_cycle_widget));

  // Verify the visuals with primary-snapped window gets focused.
  event_generator->PressAndReleaseKey(ui::VKEY_TAB, ui::EF_ALT_DOWN);
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "window_cycle_with_snap_group_primary_focused",
      /*revision_number=*/0, window_cycle_widget));

  // Verify the visuals after one of the windows in the group got destroyed
  // while stepping.
  w2.reset();
  EXPECT_TRUE(window_cycle_controller->IsCycling());
  const WindowCycleView* updated_window_cycle_view =
      window_cycle_controller->window_cycle_list()->cycle_view();
  ASSERT_TRUE(updated_window_cycle_view);

  views::Widget* updated_window_cycle_widget =
      const_cast<views::Widget*>(window_cycle_view->GetWidget());
  ASSERT_TRUE(updated_window_cycle_widget);

  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "window_cycle_with_snap_group_window_destruction",
      /*revision_number=*/0, updated_window_cycle_widget));
}

// -----------------------------------------------------------------------------
// Portrait:

// Visual regression test for divider component in portrait mode (default and
// hover states).
TEST_F(SnapGroupPixelTest, SnapGroupDividerBasicInPortrait) {
  UpdateDisplay("900x1200");

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  DecorateWindow(w1.get(), /*title=*/u"w1", SK_ColorGREEN);
  auto* w1_widget = views::Widget::GetWidgetForNativeView(w1.get());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  DecorateWindow(w2.get(), /*title=*/u"w2", SK_ColorBLUE);
  auto* w2_widget = views::Widget::GetWidgetForNativeView(w2.get());

  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/false, event_generator);

  auto* divider_widget = GetTopmostSnapGroupDivider()->divider_widget();
  ASSERT_TRUE(divider_widget);

  // Verify the snap group divider UI components on default state in portrait
  // mode.
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "snap_group_divider_default_state_in_portrait",
      /*revision_number=*/0, divider_widget, w1_widget, w2_widget));

  // Move the mouse to the position that is a off the center(divider handler
  // view).
  event_generator->MoveMouseTo(
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint() +
      gfx::Vector2d(10, 0));

  // Verify the snap group divider UI components on mouse hover in portrait
  // mode.
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "snap_group_divider_hover_state_in_portrait",
      /*revision_number=*/0, divider_widget, w1_widget, w2_widget));
}

// Visual regression test for `OverviewGroupItem` in portrait mode.
TEST_F(SnapGroupPixelTest, OverviewGroupItemInPortrait) {
  base::test::ScopedFeatureList scoped_feature_list{features::kForestFeature};

  UpdateDisplay("900x1200");

  ScopedOverviewTransformWindow::SetImmediateCloseForTests(/*immediate=*/true);

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  DecorateWindow(w1.get(), /*title=*/u"w1", SK_ColorGREEN);
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  DecorateWindow(w2.get(), /*title=*/u"w2", SK_ColorBLUE);

  SnapTwoTestWindows(w1.get(), /*window2=*/w2.get(), /*horizontal=*/false,
                     GetEventGenerator());

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  OverviewItemBase* overview_group_item = GetOverviewItemForWindow(w1.get());
  ASSERT_TRUE(overview_group_item);
  auto* group_item_widget = overview_group_item->item_widget();
  ASSERT_TRUE(group_item_widget);

  // Verify the `OverviewGroupItem` visuals in portrait.
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "overviewgroupitem_in_portrait",
      /*revision_number=*/1, group_item_widget));
}

// Portrait mode visual regression test for Snap Group visuals in window cycle
// view.
TEST_F(SnapGroupPixelTest, WindowCycleViewInPortrait) {
  UpdateDisplay("900x1200");

  WindowCycleList::SetDisableInitialDelayForTesting(true);

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  DecorateWindow(w1.get(), /*title=*/u"w1", SK_ColorGREEN);
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  DecorateWindow(w2.get(), /*title=*/u"w2", SK_ColorBLUE);

  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/false,
                     GetEventGenerator());

  // Explicitly activate the primary-snapped window so that it comes before
  // secondary-snapped window in MRU order, anticipating a future Alt+Tab
  // revamp.
  wm::ActivateWindow(w1.get());

  auto* event_generator = GetEventGenerator();
  event_generator->PressAndReleaseKey(ui::VKEY_TAB, ui::EF_ALT_DOWN);

  WindowCycleController* window_cycle_controller =
      Shell::Get()->window_cycle_controller();
  EXPECT_TRUE(window_cycle_controller->IsCycling());

  const WindowCycleView* window_cycle_view =
      window_cycle_controller->window_cycle_list()->cycle_view();
  ASSERT_TRUE(window_cycle_view);

  views::Widget* window_cycle_widget =
      const_cast<views::Widget*>(window_cycle_view->GetWidget());
  ASSERT_TRUE(window_cycle_widget);

  // Verify the visuals with secondary-snapped window gets focused.
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "window_cycle_with_snap_group_secondary_focused_in_portrait",
      /*revision_number=*/0, window_cycle_widget));

  // Verify the visuals with primary-snapped window gets focused.
  event_generator->PressAndReleaseKey(ui::VKEY_TAB, ui::EF_ALT_DOWN);
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "window_cycle_with_snap_group_primary_focused_in_portrait",
      /*revision_number=*/0, window_cycle_widget));
}

}  // namespace ash