chromium/ash/wm/tile_group/window_splitter_unittest.cc

// Copyright 2023 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/wm/tile_group/window_splitter.h"

#include <memory>

#include "ash/public/cpp/window_finder.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/wm_event.h"
#include "ash/wm/wm_metrics.h"
#include "ash/wm/workspace/phantom_window_controller.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "chromeos/ui/base/window_state_type.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/client/focus_client.h"
#include "ui/aura/test/test_window_delegate.h"
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/wm/core/window_util.h"

namespace ash {

namespace {

using DragType = WindowSplitter::DragType;
using SplitRegion = WindowSplitter::SplitRegion;

constexpr gfx::Rect kTopmostWindowBounds(10, 20, 500, 300);
constexpr gfx::Rect kDraggedWindowBounds(50, 60, 350, 250);

class WindowSplitterTest : public AshTestBase {
 public:
  WindowSplitterTest()
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  WindowSplitterTest(const WindowSplitterTest&) = delete;
  WindowSplitterTest& operator=(const WindowSplitterTest&) = delete;
  ~WindowSplitterTest() override = default;

 protected:
  aura::test::TestWindowDelegate* GetTestDelegate(aura::Window* window) {
    return static_cast<aura::test::TestWindowDelegate*>(window->delegate());
  }

  gfx::Rect GetPhantomWindowTargetBounds(const WindowSplitter& splitter) {
    if (auto* controller = splitter.GetPhantomWindowControllerForTesting()) {
      return controller->GetTargetBoundsInScreenForTesting();
    }
    return gfx::Rect();
  }

  gfx::Rect TopHalf(gfx::Rect bounds) {
    bounds.set_height(bounds.height() / 2);
    return bounds;
  }
  gfx::Rect BottomHalf(gfx::Rect bounds) {
    bounds.Subtract(TopHalf(bounds));
    return bounds;
  }
  gfx::Rect LeftHalf(gfx::Rect bounds) {
    bounds.set_width(bounds.width() / 2);
    return bounds;
  }
  gfx::Rect RightHalf(gfx::Rect bounds) {
    bounds.Subtract(LeftHalf(bounds));
    return bounds;
  }

  void ExpectHistogramWithSplit(const base::HistogramTester& histogram_tester,
                                SplitRegion split_region,
                                int preview_count) {
    histogram_tester.ExpectUniqueSample(kWindowSplittingDragTypeHistogramName,
                                        DragType::kSplit, 1);
    histogram_tester.ExpectUniqueSample(
        kWindowSplittingSplitRegionHistogramName, split_region, 1);
    histogram_tester.ExpectTotalCount(
        kWindowSplittingDragDurationPerSplitHistogramName, 1);
    histogram_tester.ExpectTotalCount(
        kWindowSplittingDragDurationPerNoSplitHistogramName, 0);
    histogram_tester.ExpectUniqueSample(
        kWindowSplittingPreviewsShownCountPerSplitDragHistogramName,
        preview_count, 1);
    histogram_tester.ExpectTotalCount(
        kWindowSplittingPreviewsShownCountPerNoSplitDragHistogramName, 0);
  }

  void ExpectHistogramWithNoSplit(const base::HistogramTester& histogram_tester,
                                  int preview_count) {
    histogram_tester.ExpectUniqueSample(kWindowSplittingDragTypeHistogramName,
                                        DragType::kNoSplit, 1);
    histogram_tester.ExpectTotalCount(kWindowSplittingSplitRegionHistogramName,
                                      0);
    histogram_tester.ExpectTotalCount(
        kWindowSplittingDragDurationPerSplitHistogramName, 0);
    histogram_tester.ExpectTotalCount(
        kWindowSplittingDragDurationPerNoSplitHistogramName, 1);
    histogram_tester.ExpectTotalCount(
        kWindowSplittingPreviewsShownCountPerSplitDragHistogramName, 0);
    histogram_tester.ExpectUniqueSample(
        kWindowSplittingPreviewsShownCountPerNoSplitDragHistogramName,
        preview_count, 1);
  }

  void ExpectHistogramWithIncompleteDragType(
      const base::HistogramTester& histogram_tester) {
    histogram_tester.ExpectUniqueSample(kWindowSplittingDragTypeHistogramName,
                                        DragType::kIncomplete, 1);
    histogram_tester.ExpectTotalCount(kWindowSplittingSplitRegionHistogramName,
                                      0);
    histogram_tester.ExpectTotalCount(
        kWindowSplittingDragDurationPerSplitHistogramName, 0);
    histogram_tester.ExpectTotalCount(
        kWindowSplittingDragDurationPerNoSplitHistogramName, 0);
    histogram_tester.ExpectTotalCount(
        kWindowSplittingPreviewsShownCountPerSplitDragHistogramName, 0);
    histogram_tester.ExpectTotalCount(
        kWindowSplittingPreviewsShownCountPerNoSplitDragHistogramName, 0);
  }

  void FastForwardPastDwellDuration() {
    task_environment()->FastForwardBy(WindowSplitter::kDwellActivationDuration +
                                      base::Milliseconds(100));
  }

  void FastForwardPastCancellationDuration() {
    task_environment()->FastForwardBy(
        WindowSplitter::kDwellCancellationDuration + base::Milliseconds(100));
  }

  // Moves the cursor back and forth across the top margin of the given window,
  // at the specified speed in pixels per second.
  // Returns the last location of the cursor, in screen coordinates.
  gfx::PointF MoveCursorAcrossWindowTopMargin(WindowSplitter* splitter,
                                              aura::Window* topmost_window,
                                              float movement_speed) {
    EXPECT_GT(movement_speed, 0);

    // Drag across top margin.
    const gfx::Rect window_bounds_in_screen =
        topmost_window->GetBoundsInScreen();
    gfx::Point left_location = window_bounds_in_screen.origin();
    left_location.Offset(WindowSplitter::kBaseTriggerMargins.left() + 10,
                         WindowSplitter::kBaseTriggerMargins.top() - 5);
    gfx::Point right_location = window_bounds_in_screen.top_right();
    right_location.Offset(WindowSplitter::kBaseTriggerMargins.right() - 10,
                          WindowSplitter::kBaseTriggerMargins.top() - 5);

    // Use duration much longer than dwell activation to check whether phantom
    // window is shown.
    constexpr base::TimeDelta kTotalDuration =
        WindowSplitter::kDwellActivationDuration * 3;
    // Use frequent enough updates for more accurate velocity calculation.
    constexpr int kNumUpdates = kTotalDuration / base::Milliseconds(30);
    constexpr base::TimeDelta kDeltaDuration = kTotalDuration / kNumUpdates;
    float dx = movement_speed * kDeltaDuration.InSecondsF();
    EXPECT_LT(dx, (right_location.x() - left_location.x()) / 2.0);

    gfx::PointF current_location(left_location);
    for (int i = 0; i < kNumUpdates; ++i) {
      // Keep cursor within the top margin of the window.
      // Flip the movement direction if it would go out of bounds.
      const float new_x = current_location.x() + dx;
      if (new_x > right_location.x() || new_x < left_location.x()) {
        dx = -dx;
      }
      current_location.Offset(dx, 0);

      splitter->UpdateDrag(current_location, /*can_split=*/true);
      task_environment()->FastForwardBy(kDeltaDuration);
    }
    return current_location;
  }
};

}  // namespace

TEST_F(WindowSplitterTest, CanSplitWindowFromTop) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  gfx::PointF screen_location(kTopmostWindowBounds.top_center());
  screen_location.Offset(0, 5);

  auto split_bounds = WindowSplitter::MaybeSplitWindow(
      topmost_window.get(), dragged_window.get(), screen_location);

  ASSERT_TRUE(split_bounds);
  EXPECT_EQ(split_bounds->topmost_window_bounds,
            BottomHalf(kTopmostWindowBounds));
  EXPECT_EQ(split_bounds->dragged_window_bounds, TopHalf(kTopmostWindowBounds));
}

TEST_F(WindowSplitterTest, CanSplitWindowFromLeft) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  gfx::PointF screen_location(kTopmostWindowBounds.left_center());
  screen_location.Offset(5, 0);

  auto split_bounds = WindowSplitter::MaybeSplitWindow(
      topmost_window.get(), dragged_window.get(), screen_location);

  ASSERT_TRUE(split_bounds);
  EXPECT_EQ(split_bounds->topmost_window_bounds,
            RightHalf(kTopmostWindowBounds));
  EXPECT_EQ(split_bounds->dragged_window_bounds,
            LeftHalf(kTopmostWindowBounds));
}

TEST_F(WindowSplitterTest, CanSplitWindowFromBottom) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  gfx::PointF screen_location(kTopmostWindowBounds.bottom_center());
  screen_location.Offset(0, -5);

  auto split_bounds = WindowSplitter::MaybeSplitWindow(
      topmost_window.get(), dragged_window.get(), screen_location);

  ASSERT_TRUE(split_bounds);
  EXPECT_EQ(split_bounds->topmost_window_bounds, TopHalf(kTopmostWindowBounds));
  EXPECT_EQ(split_bounds->dragged_window_bounds,
            BottomHalf(kTopmostWindowBounds));
}

TEST_F(WindowSplitterTest, CanSplitWindowFromRight) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  gfx::PointF screen_location(kTopmostWindowBounds.right_center());
  screen_location.Offset(-5, 0);

  auto split_bounds = WindowSplitter::MaybeSplitWindow(
      topmost_window.get(), dragged_window.get(), screen_location);

  ASSERT_TRUE(split_bounds);
  EXPECT_EQ(split_bounds->topmost_window_bounds,
            LeftHalf(kTopmostWindowBounds));
  EXPECT_EQ(split_bounds->dragged_window_bounds,
            RightHalf(kTopmostWindowBounds));
}

TEST_F(WindowSplitterTest, NoSplitWindowNearCenter) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  auto split_bounds = WindowSplitter::MaybeSplitWindow(
      topmost_window.get(), dragged_window.get(),
      gfx::PointF(kTopmostWindowBounds.CenterPoint()));
  EXPECT_FALSE(split_bounds);
}

TEST_F(WindowSplitterTest, NoSplitTopmostWindowUnderMinimumSize) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  GetTestDelegate(topmost_window.get())->set_minimum_size(gfx::Size(300, 100));
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  gfx::PointF screen_location(kTopmostWindowBounds.left_center());
  screen_location.Offset(5, 0);

  auto split_bounds = WindowSplitter::MaybeSplitWindow(
      topmost_window.get(), dragged_window.get(), screen_location);
  EXPECT_FALSE(split_bounds);
}

TEST_F(WindowSplitterTest, NoSplitDraggedWindowUnderMinimumSize) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);
  GetTestDelegate(dragged_window.get())->set_minimum_size(gfx::Size(300, 100));

  gfx::PointF screen_location(kTopmostWindowBounds.left_center());
  screen_location.Offset(5, 0);

  auto split_bounds = WindowSplitter::MaybeSplitWindow(
      topmost_window.get(), dragged_window.get(), screen_location);
  EXPECT_FALSE(split_bounds);
}

TEST_F(WindowSplitterTest, NoSplitWindowOutsideWorkArea) {
  gfx::Rect topmost_window_bounds = kTopmostWindowBounds;
  topmost_window_bounds.set_x(-10);
  auto topmost_window = CreateToplevelTestWindow(topmost_window_bounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  gfx::PointF screen_location(topmost_window_bounds.right_center());
  screen_location.Offset(-5, 0);

  auto split_bounds = WindowSplitter::MaybeSplitWindow(
      topmost_window.get(), dragged_window.get(), screen_location);
  EXPECT_FALSE(split_bounds);
}

TEST_F(WindowSplitterTest, DragSplitWindow) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(kTopmostWindowBounds.right_center());
    screen_location.Offset(-5, 0);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    FastForwardPastDwellDuration();
    EXPECT_TRUE(RightHalf(kTopmostWindowBounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    splitter.CompleteDrag(screen_location);
    EXPECT_EQ(topmost_window->GetBoundsInScreen(),
              LeftHalf(kTopmostWindowBounds));
    EXPECT_EQ(dragged_window->GetBoundsInScreen(),
              RightHalf(kTopmostWindowBounds));
  }

  ExpectHistogramWithSplit(histogram_tester, SplitRegion::kRight,
                           /*preview_count=*/1);
}

TEST_F(WindowSplitterTest, DragSplitWindowShowPreviewMultipleTimes) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(kTopmostWindowBounds.right_center());
    screen_location.Offset(-5, 0);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());
    FastForwardPastDwellDuration();
    EXPECT_TRUE(RightHalf(kTopmostWindowBounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    screen_location = gfx::PointF(kTopmostWindowBounds.CenterPoint());
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());
    FastForwardPastDwellDuration();
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    screen_location = gfx::PointF(kTopmostWindowBounds.left_center());
    screen_location.Offset(5, 0);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());
    FastForwardPastDwellDuration();
    EXPECT_TRUE(LeftHalf(kTopmostWindowBounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    screen_location = gfx::PointF(kTopmostWindowBounds.bottom_center());
    screen_location.Offset(0, -5);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());
    FastForwardPastDwellDuration();
    EXPECT_TRUE(BottomHalf(kTopmostWindowBounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    // Update drag again at the same place to ensure no metric double counting.
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(BottomHalf(kTopmostWindowBounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    splitter.CompleteDrag(screen_location);
    EXPECT_EQ(topmost_window->GetBoundsInScreen(),
              TopHalf(kTopmostWindowBounds));
    EXPECT_EQ(dragged_window->GetBoundsInScreen(),
              BottomHalf(kTopmostWindowBounds));
  }

  ExpectHistogramWithSplit(histogram_tester, SplitRegion::kBottom,
                           /*preview_count=*/3);
}

TEST_F(WindowSplitterTest, DragDwellCancelPhantomWindow) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(kTopmostWindowBounds.right_center());
    screen_location.Offset(-5, 0);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    FastForwardPastDwellDuration();
    EXPECT_TRUE(RightHalf(kTopmostWindowBounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    FastForwardPastCancellationDuration();
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    splitter.CompleteDrag(screen_location);
    EXPECT_EQ(topmost_window->GetBoundsInScreen(), kTopmostWindowBounds);
    EXPECT_EQ(dragged_window->GetBoundsInScreen(), kDraggedWindowBounds);
  }

  ExpectHistogramWithNoSplit(histogram_tester,
                             /*preview_count=*/1);
}

TEST_F(WindowSplitterTest, DragEnterExitMarginNoSplitBeforePhantom) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  ASSERT_TRUE(topmost_window->IsVisible());

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(kTopmostWindowBounds.right_center());
    screen_location.Offset(-5, 0);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    // Use location barely outside window bounds to ensure extended hit region
    // does not activate window splitting.
    screen_location.set_x(kTopmostWindowBounds.right() + 1);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    // Should still not have phantom window after dwelling.
    FastForwardPastDwellDuration();
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    splitter.CompleteDrag(screen_location);
    EXPECT_EQ(topmost_window->GetBoundsInScreen(), kTopmostWindowBounds);
    EXPECT_EQ(dragged_window->GetBoundsInScreen(), kDraggedWindowBounds);
  }

  ExpectHistogramWithNoSplit(histogram_tester,
                             /*preview_count=*/0);
}

TEST_F(WindowSplitterTest, DragEnterExitMarginNoSplitAfterPhantom) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  ASSERT_TRUE(topmost_window->IsVisible());

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(kTopmostWindowBounds.right_center());
    screen_location.Offset(-5, 0);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());
    FastForwardPastDwellDuration();
    EXPECT_TRUE(RightHalf(kTopmostWindowBounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    // Use location barely outside window bounds to ensure extended hit region
    // does not activate window splitting.
    screen_location.set_x(kTopmostWindowBounds.right() + 1);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    // Should still not have phantom window after dwelling.
    FastForwardPastDwellDuration();
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    splitter.CompleteDrag(screen_location);
    EXPECT_EQ(topmost_window->GetBoundsInScreen(), kTopmostWindowBounds);
    EXPECT_EQ(dragged_window->GetBoundsInScreen(), kDraggedWindowBounds);
  }

  ExpectHistogramWithNoSplit(histogram_tester,
                             /*preview_count=*/1);
}

TEST_F(WindowSplitterTest, DragLowVelocityShowPhantom) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF last_location = MoveCursorAcrossWindowTopMargin(
        &splitter, topmost_window.get(),
        WindowSplitter::kDwellMaxVelocityPixelsPerSec - 10.0);

    // Should already be showing phantom window by now.
    EXPECT_TRUE(TopHalf(kTopmostWindowBounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));
    splitter.CompleteDrag(last_location);
    EXPECT_EQ(topmost_window->GetBoundsInScreen(),
              BottomHalf(kTopmostWindowBounds));
    EXPECT_EQ(dragged_window->GetBoundsInScreen(),
              TopHalf(kTopmostWindowBounds));
  }

  ExpectHistogramWithSplit(histogram_tester, SplitRegion::kTop,
                           /*preview_count=*/1);
}

TEST_F(WindowSplitterTest, DragHighVelocityNoShowPhantom) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF last_location = MoveCursorAcrossWindowTopMargin(
        &splitter, topmost_window.get(),
        WindowSplitter::kDwellMaxVelocityPixelsPerSec + 30.0);

    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());
    splitter.CompleteDrag(last_location);
    EXPECT_EQ(topmost_window->GetBoundsInScreen(), kTopmostWindowBounds);
    EXPECT_EQ(dragged_window->GetBoundsInScreen(), kDraggedWindowBounds);
  }

  ExpectHistogramWithNoSplit(histogram_tester, /*preview_count=*/0);
}

TEST_F(WindowSplitterTest, DragHighVelocityThenStopShowsPhantom) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF last_location = MoveCursorAcrossWindowTopMargin(
        &splitter, topmost_window.get(),
        WindowSplitter::kDwellMaxVelocityPixelsPerSec + 30.0);

    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    FastForwardPastDwellDuration();
    EXPECT_TRUE(TopHalf(kTopmostWindowBounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    splitter.CompleteDrag(last_location);
    EXPECT_EQ(topmost_window->GetBoundsInScreen(),
              BottomHalf(kTopmostWindowBounds));
    EXPECT_EQ(dragged_window->GetBoundsInScreen(),
              TopHalf(kTopmostWindowBounds));
  }

  ExpectHistogramWithSplit(histogram_tester, SplitRegion::kTop,
                           /*preview_count=*/1);
}

TEST_F(WindowSplitterTest, DragWithCantSplit) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  ASSERT_TRUE(topmost_window->IsVisible());

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(kTopmostWindowBounds.top_center());
    screen_location.Offset(0, 5);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());
    FastForwardPastDwellDuration();
    EXPECT_TRUE(TopHalf(kTopmostWindowBounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    // Use same location but with splitting disabled.
    splitter.UpdateDrag(screen_location, /*can_split=*/false);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    // Should still not have phantom window after dwelling.
    FastForwardPastDwellDuration();
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    splitter.CompleteDrag(screen_location);
    EXPECT_EQ(topmost_window->GetBoundsInScreen(), kTopmostWindowBounds);
    EXPECT_EQ(dragged_window->GetBoundsInScreen(), kDraggedWindowBounds);
  }

  ExpectHistogramWithNoSplit(histogram_tester, /*preview_count=*/1);
}

TEST_F(WindowSplitterTest, DragDraggedWindowDestroyedBeforePhantom) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  ASSERT_TRUE(topmost_window->IsVisible());

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(kTopmostWindowBounds.top_center());
    screen_location.Offset(0, 5);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    // Dragged window got destroyed during drag!
    dragged_window.reset();

    // Callback should handle dragged window disappearing.
    FastForwardPastDwellDuration();
  }

  ExpectHistogramWithIncompleteDragType(histogram_tester);
}

TEST_F(WindowSplitterTest, DragDraggedWindowDestroyedAfterPhantom) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  ASSERT_TRUE(topmost_window->IsVisible());

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(kTopmostWindowBounds.top_center());
    screen_location.Offset(0, 5);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    FastForwardPastDwellDuration();
    EXPECT_TRUE(TopHalf(kTopmostWindowBounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    // Dragged window got destroyed during drag!
    dragged_window.reset();
  }

  ExpectHistogramWithIncompleteDragType(histogram_tester);
}

TEST_F(WindowSplitterTest, DragTopmostWindowDestroyedBeforePhantom) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  ASSERT_TRUE(topmost_window->IsVisible());

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(kTopmostWindowBounds.top_center());
    screen_location.Offset(0, 5);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    // Top most window got destroyed during drag!
    topmost_window.reset();

    // Callback should handle top most window disappearing.
    FastForwardPastDwellDuration();
  }

  ExpectHistogramWithIncompleteDragType(histogram_tester);
}

TEST_F(WindowSplitterTest, DragTopmostWindowDestroyedAfterPhantom) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  ASSERT_TRUE(topmost_window->IsVisible());

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(kTopmostWindowBounds.top_center());
    screen_location.Offset(0, 5);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    FastForwardPastDwellDuration();
    EXPECT_TRUE(TopHalf(kTopmostWindowBounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    // Top most window got destroyed during drag!
    topmost_window.reset();
  }

  ExpectHistogramWithIncompleteDragType(histogram_tester);
}

TEST_F(WindowSplitterTest, SplitMaximizedWindow) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  auto* topmost_window_state = WindowState::Get(topmost_window.get());
  topmost_window_state->Maximize();
  ASSERT_EQ(topmost_window_state->GetStateType(),
            chromeos::WindowStateType::kMaximized);
  const auto topmost_window_bounds = topmost_window->GetBoundsInScreen();
  EXPECT_EQ(topmost_window_bounds, GetPrimaryDisplay().work_area());

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(topmost_window_bounds.top_center());
    screen_location.Offset(0, 5);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    FastForwardPastDwellDuration();
    EXPECT_TRUE(TopHalf(topmost_window_bounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    splitter.CompleteDrag(screen_location);
    EXPECT_EQ(topmost_window->GetBoundsInScreen(),
              BottomHalf(topmost_window_bounds));
    EXPECT_EQ(dragged_window->GetBoundsInScreen(),
              TopHalf(topmost_window_bounds));
    EXPECT_TRUE(chromeos::IsNormalWindowStateType(
        topmost_window_state->GetStateType()));
  }

  ExpectHistogramWithSplit(histogram_tester, SplitRegion::kTop,
                           /*preview_count=*/1);
}

TEST_F(WindowSplitterTest, SplitSnappedWindow) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  const WindowSnapWMEvent event(
      WM_EVENT_SNAP_SECONDARY, WindowSnapActionSource::kDragWindowToEdgeToSnap);
  auto* topmost_window_state = WindowState::Get(topmost_window.get());
  topmost_window_state->OnWMEvent(&event);
  ASSERT_TRUE(
      chromeos::IsSnappedWindowStateType(topmost_window_state->GetStateType()));
  const auto topmost_window_bounds = topmost_window->GetBoundsInScreen();
  EXPECT_EQ(topmost_window_bounds, RightHalf(GetPrimaryDisplay().work_area()));

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(topmost_window_bounds.bottom_center());
    screen_location.Offset(0, -5);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    FastForwardPastDwellDuration();
    EXPECT_TRUE(BottomHalf(topmost_window_bounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    splitter.CompleteDrag(screen_location);
    EXPECT_EQ(topmost_window->GetBoundsInScreen(),
              TopHalf(topmost_window_bounds));
    EXPECT_EQ(dragged_window->GetBoundsInScreen(),
              BottomHalf(topmost_window_bounds));
    EXPECT_TRUE(chromeos::IsNormalWindowStateType(
        topmost_window_state->GetStateType()));
  }

  ExpectHistogramWithSplit(histogram_tester, SplitRegion::kBottom,
                           /*preview_count=*/1);
}

TEST_F(WindowSplitterTest, DragWithinExtendedDisplay) {
  UpdateDisplay("0+0-1200x800,[email protected]");
  const gfx::Rect topmost_window_bounds(1300, 20, 500, 400);
  const gfx::Rect dragged_window_bounds(1400, 120, 300, 200);
  auto topmost_window = CreateToplevelTestWindow(topmost_window_bounds);
  auto dragged_window = CreateToplevelTestWindow(dragged_window_bounds);

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(topmost_window_bounds.right_center());
    screen_location.Offset(-5, 0);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    FastForwardPastDwellDuration();
    EXPECT_TRUE(RightHalf(topmost_window_bounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    splitter.CompleteDrag(screen_location);
    EXPECT_EQ(topmost_window->GetBoundsInScreen(),
              LeftHalf(topmost_window_bounds));
    EXPECT_EQ(dragged_window->GetBoundsInScreen(),
              RightHalf(topmost_window_bounds));
  }

  ExpectHistogramWithSplit(histogram_tester, SplitRegion::kRight,
                           /*preview_count=*/1);
}

TEST_F(WindowSplitterTest, DragAcrossExtendedDisplay) {
  UpdateDisplay("0+0-1200x800,[email protected]");
  const gfx::Rect topmost_window_bounds(1300, 20, 500, 400);
  auto topmost_window = CreateToplevelTestWindow(topmost_window_bounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);

  base::HistogramTester histogram_tester;

  {
    // Nested scope used to exercise metrics update on splitter destruction.
    WindowSplitter splitter(dragged_window.get());
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    gfx::PointF screen_location(topmost_window_bounds.right_center());
    screen_location.Offset(-5, 0);
    splitter.UpdateDrag(screen_location, /*can_split=*/true);
    EXPECT_TRUE(GetPhantomWindowTargetBounds(splitter).IsEmpty());

    FastForwardPastDwellDuration();
    EXPECT_TRUE(RightHalf(topmost_window_bounds)
                    .Contains(GetPhantomWindowTargetBounds(splitter)));

    splitter.CompleteDrag(screen_location);
    EXPECT_EQ(topmost_window->GetBoundsInScreen(),
              LeftHalf(topmost_window_bounds));
    EXPECT_EQ(dragged_window->GetBoundsInScreen(),
              RightHalf(topmost_window_bounds));
  }

  ExpectHistogramWithSplit(histogram_tester, SplitRegion::kRight,
                           /*preview_count=*/1);
}

TEST_F(WindowSplitterTest, DragSplitWindowBringToTop) {
  auto topmost_window = CreateToplevelTestWindow(kTopmostWindowBounds);
  auto dragged_window = CreateToplevelTestWindow(kDraggedWindowBounds);
  auto occluding_window = CreateToplevelTestWindow(gfx::Rect(80, 10, 500, 350));

  constexpr gfx::Point left_point(100, 40);
  constexpr gfx::Point right_point(400, 200);

  EXPECT_EQ(GetTopmostWindowAtPoint(left_point, {}), occluding_window.get());
  EXPECT_EQ(GetTopmostWindowAtPoint(right_point, {}), occluding_window.get());

  WindowSplitter splitter(dragged_window.get());
  gfx::PointF screen_location(kTopmostWindowBounds.left_center());
  screen_location.Offset(5, 0);
  splitter.UpdateDrag(screen_location, /*can_split=*/true);
  FastForwardPastDwellDuration();
  EXPECT_TRUE(LeftHalf(kTopmostWindowBounds)
                  .Contains(GetPhantomWindowTargetBounds(splitter)));

  splitter.CompleteDrag(screen_location);
  EXPECT_EQ(topmost_window->GetBoundsInScreen(),
            RightHalf(kTopmostWindowBounds));
  EXPECT_EQ(dragged_window->GetBoundsInScreen(),
            LeftHalf(kTopmostWindowBounds));
  EXPECT_EQ(GetTopmostWindowAtPoint(left_point, {}), dragged_window.get());
  EXPECT_EQ(GetTopmostWindowAtPoint(right_point, {}), topmost_window.get());
  EXPECT_THAT(
      Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk),
      testing::ElementsAre(dragged_window.get(), topmost_window.get(),
                           occluding_window.get()));
  EXPECT_TRUE(wm::IsActiveWindow(dragged_window.get()));
  EXPECT_EQ(
      aura::client::GetFocusClient(dragged_window.get())->GetFocusedWindow(),
      dragged_window.get());
}

}  // namespace ash