chromium/ash/wm/pip/pip_window_resizer_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.

#include "ash/wm/pip/pip_window_resizer.h"
#include "base/memory/raw_ptr.h"

#include <memory>
#include <string>
#include <tuple>
#include <utility>

#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/keyboard/ui/test/keyboard_test_util.h"
#include "ash/metrics/pip_uma.h"
#include "ash/public/cpp/keyboard/keyboard_switches.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/pip/pip_controller.h"
#include "ash/wm/pip/pip_positioner.h"
#include "ash/wm/pip/pip_test_utils.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/test/fake_window_state.h"
#include "ash/wm/test/test_non_client_frame_view_ash.h"
#include "ash/wm/toplevel_window_event_handler.h"
#include "ash/wm/window_state.h"
#include "ash/wm/wm_event.h"
#include "ash/wm/work_area_insets.h"
#include "base/functional/callback_helpers.h"
#include "base/numerics/angle_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#include "ui/base/hit_test.h"
#include "ui/compositor/layer.h"
#include "ui/display/scoped_display_for_new_windows.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace ash {

using ::chromeos::WindowStateType;
using Sample = base::HistogramBase::Sample;

class PipWindowResizerTest : public AshTestBase,
                             public ::testing::WithParamInterface<
                                 std::tuple<std::string, std::size_t>> {
 public:
  PipWindowResizerTest() = default;

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

  ~PipWindowResizerTest() override = default;

  void SetUp() override {
    AshTestBase::SetUp();
    SetVirtualKeyboardEnabled(true);

    const std::string& display_string = std::get<0>(GetParam());
    const std::size_t root_window_index = std::get<1>(GetParam());
    UpdateWorkArea(display_string);
    ASSERT_LT(root_window_index, Shell::GetAllRootWindows().size());
    scoped_display_ = std::make_unique<display::ScopedDisplayForNewWindows>(
        Shell::GetAllRootWindows()[root_window_index]);
    ForceHideShelvesForTest();
  }

  void TearDown() override {
    widget_.reset();
    scoped_display_.reset();
    SetVirtualKeyboardEnabled(false);
    AshTestBase::TearDown();
  }

 protected:
  views::Widget* widget() { return widget_.get(); }
  aura::Window* window() { return window_; }
  FakeWindowState* test_state() { return test_state_; }
  base::HistogramTester& histograms() { return histograms_; }

  std::unique_ptr<views::Widget> CreateWidgetForTest(const gfx::Rect& bounds) {
    auto* root_window = Shell::GetRootWindowForNewWindows();
    gfx::Rect screen_bounds = bounds;
    ::wm::ConvertRectToScreen(root_window, &screen_bounds);

    auto* pip_container =
        Shell::GetContainer(root_window, kShellWindowId_PipContainer);

    std::unique_ptr<views::Widget> widget(new views::Widget);
    views::Widget::InitParams params(
        views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
    params.bounds = screen_bounds;
    params.z_order = ui::ZOrderLevel::kFloatingWindow;
    params.context = root_window;
    params.parent = pip_container;

    // Add a delegate to make it possible to set the maximum and minimum
    // size for the window with `NonClientFrameViewAsh`.
    params.delegate = new TestWidgetDelegateAsh();

    widget->Init(std::move(params));
    widget->Show();
    return widget;
  }

  PipWindowResizer* CreateResizerForTest(int window_component) {
    return CreateResizerForTest(window_component, window(),
                                window()->bounds().CenterPoint());
  }

  PipWindowResizer* CreateResizerForTest(int window_component,
                                         const gfx::Point& point_in_parent) {
    return CreateResizerForTest(window_component, window(), point_in_parent);
  }

  PipWindowResizer* CreateResizerForTest(int window_component,
                                         aura::Window* window,
                                         const gfx::Point& point_in_parent) {
    WindowState* window_state = WindowState::Get(window);
    window_state->CreateDragDetails(gfx::PointF(point_in_parent),
                                    window_component,
                                    ::wm::WINDOW_MOVE_SOURCE_MOUSE);
    return new PipWindowResizer(window_state);
  }

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

  void Fling(std::unique_ptr<WindowResizer> resizer,
             float velocity_x,
             float velocity_y) {
    aura::Window* target_window = resizer->GetTarget();
    base::TimeTicks timestamp = base::TimeTicks::Now();
    ui::GestureEventDetails details = ui::GestureEventDetails(
        ui::EventType::kScrollFlingStart, velocity_x, velocity_y);
    ui::GestureEvent event = ui::GestureEvent(
        target_window->bounds().origin().x(),
        target_window->bounds().origin().y(), ui::EF_NONE, timestamp, details);
    ui::Event::DispatcherApi(&event).set_target(target_window);
    resizer->FlingOrSwipe(&event);
  }

  void PreparePipWindow(const gfx::Rect& bounds) {
    widget_ = CreateWidgetForTest(bounds);
    window_ = widget_->GetNativeWindow();

    auto test_state = std::make_unique<FakeWindowState>(WindowStateType::kPip);
    test_state_ = test_state.get();
    WindowState::Get(window_)->SetStateObject(std::move(test_state));
    Shell::Get()->pip_controller()->SetPipWindow(window_);

    auto* custom_frame = static_cast<TestNonClientFrameViewAsh*>(
        NonClientFrameViewAsh::Get(window()));
    custom_frame->SetMaximumSize(gfx::Size(300, 200));
    custom_frame->SetMinimumSize(gfx::Size(30, 20));

    long root_window_index = static_cast<long>(std::get<1>(GetParam()));
    window_->SetProperty(aura::client::kFullscreenTargetDisplayIdKey,
                         root_window_index);
  }

 private:
  std::unique_ptr<views::Widget> widget_;
  raw_ptr<aura::Window, DanglingUntriaged> window_;
  raw_ptr<FakeWindowState, DanglingUntriaged> test_state_;
  base::HistogramTester histograms_;
  std::unique_ptr<display::ScopedDisplayForNewWindows> scoped_display_;

  void UpdateWorkArea(const std::string& bounds) {
    UpdateDisplay(bounds);
    for (aura::Window* root : Shell::GetAllRootWindows()) {
      WorkAreaInsets::ForWindow(root)->UpdateWorkAreaInsetsForTest(
          root, gfx::Rect(), gfx::Insets(), gfx::Insets());
    }
  }
};

TEST_P(PipWindowResizerTest, PipWindowCanDrag) {
  PreparePipWindow(gfx::Rect(200, 200, 100, 100));

  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
  ASSERT_TRUE(resizer.get());

  resizer->Drag(CalculateDragPoint(*resizer, 0, 10), 0);
  EXPECT_EQ(gfx::Rect(200, 210, 100, 100),
            test_state()->last_requested_bounds());
}

TEST_P(PipWindowResizerTest, PipWindowCanResize) {
  PreparePipWindow(gfx::Rect(200, 200, 100, 100));
  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTBOTTOM));
  ASSERT_TRUE(resizer.get());

  resizer->Drag(CalculateDragPoint(*resizer, 0, 10), 0);
  EXPECT_EQ(gfx::Rect(200, 200, 100, 110),
            test_state()->last_requested_bounds());
}

TEST_P(PipWindowResizerTest, PipWindowCanPinchResize) {
  gfx::RectF initial_bounds(200, 200, 120, 80);
  gfx::PointF initial_location = initial_bounds.CenterPoint();
  gfx::Vector2dF location_change(0.f, 0.f);
  gfx::PointF new_location = initial_location + location_change;
  float scale = 1.5f;

  PreparePipWindow(gfx::ToRoundedRect(initial_bounds));

  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
  ASSERT_TRUE(resizer.get());

  window()->SetProperty(aura::client::kAspectRatio, gfx::SizeF(3.f, 2.f));

  // Pinch zoom in.
  resizer->Pinch(
      CalculateDragPoint(*resizer, location_change.x(), location_change.y()),
      scale);

  // Calculate the expected new bounds.
  float left_ratio =
      (initial_location.x() - initial_bounds.x()) / initial_bounds.width();
  float top_ratio =
      (initial_location.y() - initial_bounds.y()) / initial_bounds.height();
  gfx::SizeF new_size(gfx::ScaleSize(initial_bounds.size(), scale));
  gfx::Rect expected_bounds(new_location.x() - new_size.width() * left_ratio,
                            new_location.y() - new_size.height() * top_ratio,
                            new_size.width(), new_size.height());

  // Verify that the window has expected new bounds.
  EXPECT_EQ(expected_bounds, test_state()->last_requested_bounds());

  // Pinch zoom out.
  resizer->Pinch(CalculateDragPoint(*resizer, 0, 0), /*scale=*/0.5f);

  // Calculate the expected new bounds.
  scale *= 0.5f;
  left_ratio =
      (initial_location.x() - initial_bounds.x()) / initial_bounds.width();
  top_ratio =
      (initial_location.y() - initial_bounds.y()) / initial_bounds.height();
  new_size = gfx::ScaleSize(initial_bounds.size(), scale);
  expected_bounds = gfx::Rect(new_location.x() - new_size.width() * left_ratio,
                              new_location.y() - new_size.height() * top_ratio,
                              new_size.width(), new_size.height());

  EXPECT_EQ(expected_bounds, test_state()->last_requested_bounds());
}

TEST_P(PipWindowResizerTest, PipWindowDragIsRestrictedToWorkArea) {
  PreparePipWindow(gfx::Rect(200, 200, 100, 100));
  // Specify point in parent as center so the drag point does not leave the
  // display. If the drag point is not in any display bounds, it causes the
  // window to be moved to the default display.
  auto landscape =
      display::Screen::GetScreen()->GetPrimaryDisplay().is_landscape();
  int right_x = landscape ? 392 : 292;
  int bottom_y = landscape ? 292 : 392;

  std::unique_ptr<PipWindowResizer> resizer(
      CreateResizerForTest(HTCAPTION, gfx::Point(250, 250)));
  ASSERT_TRUE(resizer.get());

  // Drag to the right.
  resizer->Drag(CalculateDragPoint(*resizer, 250, 0), 0);
  EXPECT_EQ(gfx::Rect(right_x, 200, 100, 100),
            test_state()->last_requested_bounds());

  // Drag down.
  resizer->Drag(CalculateDragPoint(*resizer, 0, 250), 0);
  EXPECT_EQ(gfx::Rect(200, bottom_y, 100, 100),
            test_state()->last_requested_bounds());

  // Drag to the left.
  resizer->Drag(CalculateDragPoint(*resizer, -250, 0), 0);
  EXPECT_EQ(gfx::Rect(8, 200, 100, 100), test_state()->last_requested_bounds());

  // Drag up.
  resizer->Drag(CalculateDragPoint(*resizer, 0, -250), 0);
  EXPECT_EQ(gfx::Rect(200, 8, 100, 100), test_state()->last_requested_bounds());
}

TEST_P(PipWindowResizerTest, PipWindowCanBeDraggedInTabletMode) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  PreparePipWindow(gfx::Rect(200, 200, 100, 100));
  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
  ASSERT_TRUE(resizer.get());

  resizer->Drag(CalculateDragPoint(*resizer, 0, 10), 0);
  EXPECT_EQ(gfx::Rect(200, 210, 100, 100),
            test_state()->last_requested_bounds());
}

TEST_P(PipWindowResizerTest, PipWindowCanBeResizedInTabletMode) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  PreparePipWindow(gfx::Rect(200, 200, 100, 100));
  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTBOTTOM));
  ASSERT_TRUE(resizer.get());

  resizer->Drag(CalculateDragPoint(*resizer, 0, 10), 0);
  EXPECT_EQ(gfx::Rect(200, 200, 100, 110),
            test_state()->last_requested_bounds());
}

TEST_P(PipWindowResizerTest, ResizingPipWindowDoesNotTriggerFling) {
  PreparePipWindow(gfx::Rect(8, 8, 100, 100));
  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTBOTTOM));
  ASSERT_TRUE(resizer.get());

  Fling(std::move(resizer), 0.f, 4000.f);

  // Ensure that the PIP window isn't flung to the bottom edge during resize.
  EXPECT_EQ(gfx::Point(8, 8), test_state()->last_requested_bounds().origin());
}

TEST_P(PipWindowResizerTest, PipWindowCanBeSwipeDismissed) {
  PreparePipWindow(gfx::Rect(8, 8, 100, 100));
  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
  ASSERT_TRUE(resizer.get());

  // Drag to the top.
  resizer->Drag(CalculateDragPoint(*resizer, 0, -100), 0);

  // Should be dismissed when the drag completes.
  resizer->CompleteDrag();
  EXPECT_TRUE(widget()->IsClosed());
}

TEST_P(PipWindowResizerTest, PipWindowPartiallySwipedDoesNotDismiss) {
  PreparePipWindow(gfx::Rect(8, 8, 100, 100));
  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
  ASSERT_TRUE(resizer.get());

  // Drag to the top, but only a little bit.
  resizer->Drag(CalculateDragPoint(*resizer, 0, -30), 0);

  // Should not be dismissed when the drag completes.
  resizer->CompleteDrag();
  EXPECT_FALSE(widget()->IsClosed());
  EXPECT_EQ(gfx::Rect(8, 8, 100, 100), test_state()->last_requested_bounds());
}

TEST_P(PipWindowResizerTest, PipWindowInSwipeToDismissGestureLocksToAxis) {
  PreparePipWindow(gfx::Rect(8, 8, 100, 100));
  std::unique_ptr<PipWindowResizer> resizer(
      CreateResizerForTest(HTCAPTION, gfx::Point(50, 50)));
  ASSERT_TRUE(resizer.get());

  // Drag to the top, but only a little bit, to start a swipe-to-dismiss.
  resizer->Drag(CalculateDragPoint(*resizer, 0, -30), 0);
  EXPECT_EQ(gfx::Rect(8, -22, 100, 100), test_state()->last_requested_bounds());

  // Now try to drag to the right, it should be locked to the horizontal axis.
  resizer->Drag(CalculateDragPoint(*resizer, 30, -30), 0);
  EXPECT_EQ(gfx::Rect(8, -22, 100, 100), test_state()->last_requested_bounds());
}

TEST_P(PipWindowResizerTest,
       PipWindowMovedAwayFromScreenEdgeNoLongerCanSwipeToDismiss) {
  PreparePipWindow(gfx::Rect(8, 16, 100, 100));
  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
  ASSERT_TRUE(resizer.get());

  // Drag to the bottom and left a bit.
  resizer->Drag(CalculateDragPoint(*resizer, -8, 30), 0);
  EXPECT_EQ(gfx::Rect(8, 46, 100, 100), test_state()->last_requested_bounds());

  // Now try to drag to the top start a swipe-to-dismiss. It should stop
  // at the edge of the work area.
  resizer->Drag(CalculateDragPoint(*resizer, -8, -30), 0);
  EXPECT_EQ(gfx::Rect(8, 8, 100, 100), test_state()->last_requested_bounds());
}

TEST_P(PipWindowResizerTest, PipWindowAtCornerLocksToOneAxisOnSwipeToDismiss) {
  PreparePipWindow(gfx::Rect(8, 8, 100, 100));
  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
  ASSERT_TRUE(resizer.get());

  // Try dragging up and to the left. It should lock onto the axis with the
  // largest displacement.
  resizer->Drag(CalculateDragPoint(*resizer, -40, -30), 0);
  EXPECT_EQ(gfx::Rect(-32, 8, 100, 100), test_state()->last_requested_bounds());
}

TEST_P(
    PipWindowResizerTest,
    PipWindowMustBeDraggedMostlyInDirectionOfDismissToInitiateSwipeToDismiss) {
  PreparePipWindow(gfx::Rect(8, 8, 100, 100));
  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
  ASSERT_TRUE(resizer.get());

  // Try a lot to the right and a bit to the top. Swiping should not be
  // initiated.
  resizer->Drag(CalculateDragPoint(*resizer, 50, -30), 0);
  EXPECT_EQ(gfx::Rect(58, 8, 100, 100), test_state()->last_requested_bounds());
}

TEST_P(PipWindowResizerTest,
       PipWindowDoesNotMoveUntilStatusOfSwipeToDismissGestureIsKnown) {
  PreparePipWindow(gfx::Rect(8, 8, 100, 100));
  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
  ASSERT_TRUE(resizer.get());

  // Move a small amount - this should not trigger any bounds change, since
  // we don't know whether a swipe will start or not.
  resizer->Drag(CalculateDragPoint(*resizer, 0, -4), 0);
  EXPECT_TRUE(test_state()->last_requested_bounds().IsEmpty());
}

TEST_P(PipWindowResizerTest, PipWindowIsFlungToEdge) {
  auto landscape =
      display::Screen::GetScreen()->GetPrimaryDisplay().is_landscape();

  {
    PreparePipWindow(gfx::Rect(200, 200, 100, 100));
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, 0, 10), 0);
    Fling(std::move(resizer), 0.f, 4000.f);

    auto origin = landscape ? gfx::Point(200, 292) : gfx::Point(200, 392);

    // Flung downwards.
    EXPECT_EQ(origin, test_state()->last_requested_bounds().origin());
  }

  {
    PreparePipWindow(gfx::Rect(200, 200, 100, 100));
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, 0, -10), 0);
    Fling(std::move(resizer), 0.f, -4000.f);

    // Flung upwards.
    EXPECT_EQ(gfx::Rect(200, 8, 100, 100),
              test_state()->last_requested_bounds());
  }

  {
    PreparePipWindow(gfx::Rect(200, 200, 100, 100));
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, 10, 0), 0);
    Fling(std::move(resizer), 4000.f, 0.f);

    auto origin = landscape ? gfx::Point(392, 200) : gfx::Point(292, 200);
    // Flung to the right.
    EXPECT_EQ(origin, test_state()->last_requested_bounds().origin());
  }

  {
    PreparePipWindow(gfx::Rect(200, 200, 100, 100));
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, -10, 0), 0);
    Fling(std::move(resizer), -4000.f, 0.f);

    // Flung to the left.
    EXPECT_EQ(gfx::Rect(8, 200, 100, 100),
              test_state()->last_requested_bounds());
  }
}

TEST_P(PipWindowResizerTest, PipWindowIsFlungDiagonally) {
  auto landscape =
      display::Screen::GetScreen()->GetPrimaryDisplay().is_landscape();

  {
    PreparePipWindow(gfx::Rect(200, 200, 100, 100));
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, 10, 10), 0);
    Fling(std::move(resizer), 3000.f, 3000.f);

    // Flung downward and to the right, into the corner.
    EXPECT_EQ(gfx::Rect(292, 292, 100, 100),
              test_state()->last_requested_bounds());
  }

  {
    PreparePipWindow(gfx::Rect(200, 200, 100, 100));
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, 3, 4), 0);
    Fling(std::move(resizer), 3000.f, 4000.f);
    gfx::Point origin = landscape ? gfx::Point(269, 292) : gfx::Point(292, 322);

    // Flung downward and to the right, but reaching the bottom edge first.
    EXPECT_EQ(origin, test_state()->last_requested_bounds().origin());
  }
  {
    PreparePipWindow(gfx::Rect(200, 200, 100, 100));
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, 4, 3), 0);
    Fling(std::move(resizer), 4000.f, 3000.f);
    gfx::Point origin = landscape ? gfx::Point(322, 292) : gfx::Point(292, 269);
    // Flung downward and to the right, but reaching the right edge first.
    EXPECT_EQ(origin, test_state()->last_requested_bounds().origin());
  }

  {
    PreparePipWindow(gfx::Rect(200, 200, 100, 100));
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, -3, -4), 0);
    Fling(std::move(resizer), -3000.f, -4000.f);

    // Flung upward and to the left, but reaching the top edge first.
    EXPECT_EQ(gfx::Point(56, 8),
              test_state()->last_requested_bounds().origin());
  }

  {
    PreparePipWindow(gfx::Rect(200, 200, 100, 100));
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, -4, -3), 0);
    Fling(std::move(resizer), -4000.f, -3000.f);

    // Flung upward and to the left, but reaching the left edge first.
    EXPECT_EQ(gfx::Rect(8, 56, 100, 100),
              test_state()->last_requested_bounds());
  }

  {
    PreparePipWindow(gfx::Rect(200, 200, 100, 100));
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, 3, -9), 0);
    Fling(std::move(resizer), 3000.f, -9000.f);

    // Flung upward and to the right, but reaching the top edge first.
    EXPECT_EQ(gfx::Rect(264, 8, 100, 100),
              test_state()->last_requested_bounds());
  }

  {
    PreparePipWindow(gfx::Rect(200, 200, 100, 100));
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, 3, -3), 0);
    Fling(std::move(resizer), 3000.f, -3000.f);

    gfx::Point origin = landscape ? gfx::Point(392, 8) : gfx::Point(292, 108);
    // Flung upward and to the right, but reaching the right edge first.
    EXPECT_EQ(origin, test_state()->last_requested_bounds().origin());
  }

  {
    PreparePipWindow(gfx::Rect(200, 200, 100, 100));
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, -3, 3), 0);
    Fling(std::move(resizer), -3000.f, 3000.f);

    gfx::Point origin = landscape ? gfx::Point(108, 292) : gfx::Point(8, 392);
    // Flung downward and to the left, but reaching the bottom edge first.
    EXPECT_EQ(origin, test_state()->last_requested_bounds().origin());
  }

  {
    PreparePipWindow(gfx::Rect(200, 200, 100, 100));
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, -9, 3), 0);
    Fling(std::move(resizer), -9000.f, 3000.f);

    // Flung downward and to the left, but reaching the left edge first.
    EXPECT_EQ(gfx::Rect(8, 264, 100, 100),
              test_state()->last_requested_bounds());
  }
}

TEST_P(PipWindowResizerTest, PipWindowFlungAvoidsFloatingKeyboard) {
  PreparePipWindow(gfx::Rect(200, 200, 75, 75));

  auto* keyboard_controller = keyboard::KeyboardUIController::Get();
  keyboard_controller->SetContainerType(keyboard::ContainerType::kFloating,
                                        gfx::Rect(0, 0, 1, 1),
                                        base::DoNothing());
  const display::Display display = WindowState::Get(window())->GetDisplay();
  keyboard_controller->ShowKeyboardInDisplay(display);
  ASSERT_TRUE(keyboard::test::WaitUntilShown());

  aura::Window* keyboard_window = keyboard_controller->GetKeyboardWindow();
  keyboard_window->SetBounds(gfx::Rect(8, 150, 100, 100));

  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
  ASSERT_TRUE(resizer.get());

  // Fling to the left - but don't intersect with the floating keyboard.
  resizer->Drag(CalculateDragPoint(*resizer, -10, 0), 0);
  Fling(std::move(resizer), -4000.f, 0.f);

  // Appear below the keyboard.
  EXPECT_EQ(gfx::Rect(8, 258, 75, 75), test_state()->last_requested_bounds());
}

TEST_P(PipWindowResizerTest, PipWindowDoesNotChangeDisplayOnDrag) {
  PreparePipWindow(gfx::Rect(200, 200, 100, 100));
  const display::Display display = WindowState::Get(window())->GetDisplay();
  gfx::Rect rect_in_screen = window()->bounds();
  ::wm::ConvertRectToScreen(window()->parent(), &rect_in_screen);
  EXPECT_TRUE(display.bounds().Contains(rect_in_screen));

  // Drag inside the display.
  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
  ASSERT_TRUE(resizer.get());
  resizer->Drag(CalculateDragPoint(*resizer, 10, 10), 0);

  // Ensure the position is still in the display.
  EXPECT_EQ(gfx::Rect(210, 210, 100, 100),
            test_state()->last_requested_bounds());
  EXPECT_EQ(display.id(), WindowState::Get(window())->GetDisplay().id());
  rect_in_screen = window()->bounds();
  ::wm::ConvertRectToScreen(window()->parent(), &rect_in_screen);
  EXPECT_TRUE(display.bounds().Contains(rect_in_screen));
}

TEST_P(PipWindowResizerTest, PipRestoreBoundsSetOnFling) {
  PreparePipWindow(gfx::Rect(200, 200, 100, 100));

  {
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, 10, 10), 0);
    Fling(std::move(resizer), 3000.f, 3000.f);
  }

  WindowState* window_state = WindowState::Get(window());
  EXPECT_TRUE(PipPositioner::HasSnapFraction(window_state));
}

TEST_P(PipWindowResizerTest, PipStartAndFinishFreeResizeUmaMetrics) {
  PreparePipWindow(gfx::Rect(200, 200, 100, 100));
  std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTBOTTOM));
  ASSERT_TRUE(resizer.get());

  EXPECT_EQ(1, histograms().GetBucketCount(kAshPipEventsHistogramName,
                                           Sample(AshPipEvents::FREE_RESIZE)));
  histograms().ExpectTotalCount(kAshPipEventsHistogramName, 1);

  resizer->Drag(CalculateDragPoint(*resizer, 100, 0), 0);
  resizer->CompleteDrag();

  histograms().ExpectTotalCount(kAshPipEventsHistogramName, 1);
}

TEST_P(PipWindowResizerTest, PipPinchResizeTriggersResizeUmaMetrics) {
  PreparePipWindow(gfx::Rect(200, 200, 100, 100));

  // Send pinch event. This also creates a `WindowResizer`.
  base::TimeTicks timestamp = base::TimeTicks::Now();
  ui::GestureEventDetails details(ui::EventType::kGesturePinchBegin);
  ui::GestureEvent event(window()->bounds().origin().x(),
                         window()->bounds().origin().y(), ui::EF_NONE,
                         timestamp, details);
  ui::Event::DispatcherApi(&event).set_target(window());
  ui::Event::DispatcherApi(&event).set_phase(ui::EP_PRETARGET);
  Shell::Get()->toplevel_window_event_handler()->OnGestureEvent(&event);

  EXPECT_EQ(1, histograms().GetBucketCount(kAshPipEventsHistogramName,
                                           Sample(AshPipEvents::FREE_RESIZE)));
  histograms().ExpectTotalCount(kAshPipEventsHistogramName, 1);
}

TEST_P(PipWindowResizerTest, DragDetailsAreDestroyed) {
  PreparePipWindow(gfx::Rect(200, 200, 100, 100));
  WindowState* window_state = WindowState::Get(window());

  {
    std::unique_ptr<PipWindowResizer> resizer(CreateResizerForTest(HTCAPTION));
    ASSERT_TRUE(resizer.get());

    resizer->Drag(CalculateDragPoint(*resizer, 0, 10), 0);
    EXPECT_NE(nullptr, window_state->drag_details());

    resizer->CompleteDrag();
    EXPECT_NE(nullptr, window_state->drag_details());
  }
  EXPECT_EQ(nullptr, window_state->drag_details());
}

// TODO: UpdateDisplay() doesn't support different layouts of multiple displays.
// We should add some way to try multiple layouts.
INSTANTIATE_TEST_SUITE_P(All,
                         PipWindowResizerTest,
                         testing::Values(std::make_tuple("500x400", 0u),
                                         std::make_tuple("500x400/r", 0u),
                                         std::make_tuple("500x400/u", 0u),
                                         std::make_tuple("500x400/l", 0u),
                                         std::make_tuple("1000x800*2", 0u),
                                         std::make_tuple("500x400,500x400", 0u),
                                         std::make_tuple("500x400,500x400",
                                                         1u)));

}  // namespace ash