// Copyright 2017 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/splitview/split_view_controller.h"
#include <memory>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "ash/accessibility/magnifier/docked_magnifier_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/display/screen_orientation_controller_test_api.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/keyboard/ui/test/keyboard_test_util.h"
#include "ash/public/cpp/window_backdrop.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/session/session_controller_impl.h"
#include "ash/session/test_session_controller_client.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/overview/overview_button_tray.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/status_area_widget_test_helper.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/test_window_builder.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/float/float_controller.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_grid.h"
#include "ash/wm/overview/overview_item.h"
#include "ash/wm/overview/overview_observer.h"
#include "ash/wm/overview/overview_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_divider_view.h"
#include "ash/wm/splitview/split_view_drag_indicators.h"
#include "ash/wm/splitview/split_view_metrics_controller.h"
#include "ash/wm/splitview/split_view_overview_session.h"
#include "ash/wm/splitview/split_view_types.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/test/fake_window_state.h"
#include "ash/wm/window_properties.h"
#include "ash/wm/window_resizer.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_state_delegate.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h"
#include "ash/wm/wm_metrics.h"
#include "base/containers/contains.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/frame/caption_buttons/snap_controller.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/test/test_window_delegate.h"
#include "ui/aura/test/test_windows.h"
#include "ui/base/ime/dummy_text_input_client.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/presentation_time_recorder.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/test/test_utils.h"
#include "ui/compositor_extra/shadow.h"
#include "ui/display/screen.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/shadow_controller.h"
#include "ui/wm/core/window_util.h"
namespace ash {
namespace {
constexpr int kCaretHeightForTest = 8;
// The observer to observe the overview states in |root_window_|.
class OverviewStatesObserver : public OverviewObserver {
public:
explicit OverviewStatesObserver(aura::Window* root_window)
: root_window_(root_window) {
OverviewController::Get()->AddObserver(this);
}
OverviewStatesObserver(const OverviewStatesObserver&) = delete;
OverviewStatesObserver& operator=(const OverviewStatesObserver&) = delete;
~OverviewStatesObserver() override {
OverviewController::Get()->RemoveObserver(this);
}
// OverviewObserver:
void OnOverviewModeStarting() override {
// Reset the value to true.
overview_animate_when_exiting_ = true;
}
void OnOverviewModeEnding(OverviewSession* overview_session) override {
OverviewGrid* grid = overview_session->GetGridWithRootWindow(root_window_);
if (!grid) {
return;
}
overview_animate_when_exiting_ = grid->should_animate_when_exiting();
}
bool overview_animate_when_exiting() const {
return overview_animate_when_exiting_;
}
private:
bool overview_animate_when_exiting_ = true;
raw_ptr<aura::Window> root_window_;
};
// The test BubbleDialogDelegateView for bubbles.
class TestBubbleDialogDelegateView : public views::BubbleDialogDelegateView {
public:
explicit TestBubbleDialogDelegateView(views::View* anchor_view)
: BubbleDialogDelegateView(anchor_view, views::BubbleBorder::NONE) {}
TestBubbleDialogDelegateView(const TestBubbleDialogDelegateView&) = delete;
TestBubbleDialogDelegateView& operator=(const TestBubbleDialogDelegateView&) =
delete;
~TestBubbleDialogDelegateView() override {}
};
// Helper class to simulate the text input field in a window. When the text
// input field is focused, the attached window will also be focused and show the
// virtual keyboard. If the text input field is unfocused, it will hide the
// virtual keyboard.
class TestTextInputClient : public ui::DummyTextInputClient {
public:
explicit TestTextInputClient(aura::Window* window)
: ui::DummyTextInputClient(ui::TEXT_INPUT_TYPE_TEXT), window_(window) {
DCHECK(window_);
}
TestTextInputClient(const TestTextInputClient&) = delete;
TestTextInputClient& operator=(const TestTextInputClient&) = delete;
~TestTextInputClient() override {
auto* ime = keyboard::KeyboardUIController::Get()->GetInputMethodForTest();
ime->DetachTextInputClient(this);
}
// ui::DummyTextInputClient:
gfx::Rect GetCaretBounds() const override { return caret_bounds_; }
void set_caret_bounds(gfx::Rect caret_bounds) {
caret_bounds_ = caret_bounds;
}
// When the text client is focused, the attached window will also be focused
// and the virtual keyboard is enabled.
void Focus() {
auto* ime = keyboard::KeyboardUIController::Get()->GetInputMethodForTest();
ime->SetFocusedTextInputClient(this);
if (window_) {
window_->Focus();
}
ime->SetVirtualKeyboardVisibilityIfEnabled(true);
ASSERT_TRUE(keyboard::test::WaitUntilShown());
}
// When the text client is unfocused, hide the virtual keyboard.
void UnFocus() {
auto* ime = keyboard::KeyboardUIController::Get()->GetInputMethodForTest();
ime->DetachTextInputClient(this);
keyboard::KeyboardUIController::Get()->HideKeyboardExplicitlyBySystem();
}
private:
// The window to which the text client attaches to.
raw_ptr<aura::Window> window_;
// The bounds of the caret.
gfx::Rect caret_bounds_;
};
} // namespace
class SplitViewControllerTest : public AshTestBase {
public:
SplitViewControllerTest() {
scoped_feature_list_.InitWithFeatures(
/*enabled_features=*/{features::kSnapGroup,
features::kOsSettingsRevampWayfinding},
/*disabled_features=*/{});
}
SplitViewControllerTest(const SplitViewControllerTest&) = delete;
SplitViewControllerTest& operator=(const SplitViewControllerTest&) = delete;
~SplitViewControllerTest() override = default;
// test::AshTestBase:
void SetUp() override {
AshTestBase::SetUp();
// Avoid TabletModeController::OnGetSwitchStates() from disabling tablet
// mode.
base::RunLoop().RunUntilIdle();
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
ui::PresentationTimeRecorder::SetReportPresentationTimeImmediatelyForTest(
true);
}
void TearDown() override {
ui::PresentationTimeRecorder::SetReportPresentationTimeImmediatelyForTest(
false);
trace_names_.clear();
AshTestBase::TearDown();
}
std::unique_ptr<aura::Window> CreateWindow(
const gfx::Rect& bounds,
aura::client::WindowType type = aura::client::WINDOW_TYPE_NORMAL) {
std::unique_ptr<aura::Window> window = TestWindowBuilder()
.SetBounds(bounds)
.SetTestWindowDelegate()
.SetWindowType(type)
.Build();
// Create non maximizable window so that it's centered when created,
// then allow maximize/fullscreen state.
window->SetProperty(aura::client::kResizeBehaviorKey,
aura::client::kResizeBehaviorCanFullscreen |
aura::client::kResizeBehaviorCanMaximize |
aura::client::kResizeBehaviorCanMinimize |
aura::client::kResizeBehaviorCanResize);
return window;
}
std::unique_ptr<aura::Window> CreateNonSnappableWindow(
const gfx::Rect& bounds) {
return TestWindowBuilder()
.SetWindowProperty(aura::client::kResizeBehaviorKey,
aura::client::kResizeBehaviorNone)
.SetBounds(bounds)
.SetTestWindowDelegate()
.Build();
}
bool IsDividerAnimating() {
return split_view_controller()->IsDividerAnimating();
}
void SkipDividerSnapAnimation() {
if (!IsDividerAnimating()) {
return;
}
split_view_controller()->StopAndShoveAnimatedDivider();
split_view_controller()->EndResizeWithDividerImpl();
split_view_controller()->EndSplitViewAfterResizingAtEdgeIfAppropriate();
}
void EndSplitView() { split_view_controller()->EndSplitView(); }
void LongPressOnOverviewButtonTray() {
ui::GestureEvent event(
0, 0, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureLongPress));
StatusAreaWidgetTestHelper::GetStatusAreaWidget()
->overview_button_tray()
->OnGestureEvent(&event);
}
SplitViewController* split_view_controller() {
return SplitViewController::Get(Shell::GetPrimaryRootWindow());
}
SplitViewDivider* split_view_divider() {
return split_view_controller()->split_view_divider();
}
int GetDividerPosition() {
return split_view_controller()->GetDividerPosition();
}
float divider_closest_ratio() {
return split_view_controller()->divider_closest_ratio_;
}
protected:
void CheckForDuplicateTraceName(const char* trace) {
DCHECK(!base::Contains(trace_names_, trace)) << trace;
trace_names_.push_back(trace);
}
void CheckOverviewEnterExitHistogram(const char* trace,
std::vector<int>&& enter_counts,
std::vector<int>&& exit_counts) {
CheckForDuplicateTraceName(trace);
// Force a frame then wait, ensuring there is one more frame presented after
// animation finishes to allow animation throughput data to be passed from
// cc to ui.
ui::Compositor* compositor =
Shell::GetPrimaryRootWindow()->layer()->GetCompositor();
compositor->ScheduleFullRedraw();
std::ignore =
ui::WaitForNextFrameToBePresented(compositor, base::Milliseconds(500));
{
SCOPED_TRACE(trace + std::string(".Enter"));
CheckOverviewHistogram("Ash.Overview.AnimationSmoothness.Enter",
std::move(enter_counts));
}
{
SCOPED_TRACE(trace + std::string(".Exit"));
CheckOverviewHistogram("Ash.Overview.AnimationSmoothness.Exit",
std::move(exit_counts));
}
}
const base::HistogramTester& histograms() const { return histograms_; }
private:
void CheckOverviewHistogram(const char* histogram, std::vector<int> counts) {
// These two events should never happen in this test.
histograms_.ExpectTotalCount(histogram + std::string(".ClamshellMode"), 0);
histograms_.ExpectTotalCount(
histogram + std::string(".SingleClamshellMode"), 0);
histograms_.ExpectTotalCount(histogram + std::string(".TabletMode"),
counts[0]);
histograms_.ExpectTotalCount(histogram + std::string(".SplitView"),
counts[1]);
}
std::vector<std::string> trace_names_;
base::HistogramTester histograms_;
base::test::ScopedFeatureList scoped_feature_list_;
};
// Tests the basic functionalities.
TEST_F(SplitViewControllerTest, Basic) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kNoSnap);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kPrimarySnapped);
EXPECT_EQ(split_view_controller()->primary_window(), window1.get());
EXPECT_NE(split_view_controller()->primary_window(), window2.get());
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
EXPECT_EQ(
window1->GetBoundsInScreen(),
split_view_controller()->GetSnappedWindowBoundsInScreen(
SnapPosition::kPrimary, window1.get(), chromeos::kDefaultSnapRatio,
/*account_for_divider_width=*/true));
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
EXPECT_EQ(split_view_controller()->secondary_window(), window2.get());
EXPECT_NE(split_view_controller()->secondary_window(), window1.get());
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
EXPECT_EQ(
window2->GetBoundsInScreen(),
split_view_controller()->GetSnappedWindowBoundsInScreen(
SnapPosition::kSecondary, window2.get(), chromeos::kDefaultSnapRatio,
/*account_for_divider_width=*/true));
EndSplitView();
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kNoSnap);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
}
// Tests that the default snapped window is the first window that gets snapped.
TEST_F(SplitViewControllerTest, DefaultSnappedWindow) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
EXPECT_EQ(window1.get(), split_view_controller()->GetDefaultSnappedWindow());
EndSplitView();
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kSecondary);
EXPECT_EQ(window2.get(), split_view_controller()->GetDefaultSnappedWindow());
}
// Tests that if there are two snapped windows, closing one of them will open
// overview window grid on the closed window side of the screen. If there is
// only one snapped windows, closing the snapped window will end split view mode
// and adjust the overview window grid bounds if the overview mode is active at
// that moment.
TEST_F(SplitViewControllerTest, WindowCloseTest) {
// 1 - First test one snapped window scenario.
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window0(CreateWindow(bounds));
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
split_view_controller()->SnapWindow(window0.get(), SnapPosition::kPrimary);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
// Closing this snapped window should exit split view mode.
window0.reset();
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
// 2 - Then test two snapped windows scenario.
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
EXPECT_EQ(split_view_controller()->default_snap_position(),
SnapPosition::kPrimary);
// Closing one of the two snapped windows will not end split view mode.
window1.reset();
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kSecondarySnapped);
// Since left window was closed, its default snap position changed to RIGHT.
EXPECT_EQ(split_view_controller()->default_snap_position(),
SnapPosition::kSecondary);
// Window grid is showing no recent items, and has no windows, but it is still
// available.
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
// Now close the other snapped window.
window2.reset();
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kNoSnap);
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
// 3 - Then test the scenario with more than two windows.
std::unique_ptr<aura::Window> window3(CreateWindow(bounds));
std::unique_ptr<aura::Window> window4(CreateWindow(bounds));
std::unique_ptr<aura::Window> window5(CreateWindow(bounds));
split_view_controller()->SnapWindow(window3.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window4.get(), SnapPosition::kSecondary);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
EXPECT_EQ(split_view_controller()->default_snap_position(),
SnapPosition::kPrimary);
// Close one of the snapped windows.
window4.reset();
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kPrimarySnapped);
EXPECT_EQ(split_view_controller()->default_snap_position(),
SnapPosition::kPrimary);
// Now overview window grid can be opened.
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
// Close the other snapped window.
window3.reset();
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kNoSnap);
// Test the overview winow grid should still open.
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
}
// Tests that split view overview session is started and ended correctly.
TEST_F(SplitViewControllerTest, StartEndSplitViewOverviewSession) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kNoSnap);
EXPECT_FALSE(RootWindowController::ForWindow(Shell::GetPrimaryRootWindow())
->split_view_overview_session());
// Snap `window1`. Test we are in kPrimarySnapped state and split view
// overview.
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kPrimarySnapped);
EXPECT_EQ(split_view_controller()->primary_window(), window1.get());
EXPECT_FALSE(split_view_controller()->secondary_window());
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
// Snap `window2`. Test we are in kBothSnapped state and not overview or split
// view overview.
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
EXPECT_EQ(split_view_controller()->primary_window(), window1.get());
EXPECT_EQ(split_view_controller()->secondary_window(), window2.get());
EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
EXPECT_FALSE(RootWindowController::ForWindow(Shell::GetPrimaryRootWindow())
->split_view_overview_session());
// Close `window1`. Test we are in kSecondarySnapped state and split view
// overview.
window1.reset();
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kSecondarySnapped);
EXPECT_FALSE(split_view_controller()->primary_window());
EXPECT_EQ(split_view_controller()->secondary_window(), window2.get());
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
// Close `window2`. Test we are in kNoSnap state and in overview but not
// split view overview.
window2.reset();
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kNoSnap);
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
}
// Tests that when the divider bouncing animation is triggered in
// `SplitViewController`, overview will end properly with no crash. See
// http://b/313517079 for more details about the crash reported.
TEST_F(SplitViewControllerTest,
NoCrashWhenBoucingAnimatingIfTotalSizeExceedsLimit) {
UpdateDisplay("900x600");
SplitViewController* controller = split_view_controller();
aura::test::TestWindowDelegate delegate1;
std::unique_ptr<aura::Window> window1(CreateTestWindowInShellWithDelegate(
&delegate1, /*id=*/-1, gfx::Rect(200, 300)));
EXPECT_FALSE(controller->IsWindowInSplitView(window1.get()));
// Create `window2` and set the minimum size to be between 1/3 and 1/2 so that
// it can only be snapped with 0.5 snap ratio.
aura::test::TestWindowDelegate delegate2;
std::unique_ptr<aura::Window> window2(CreateTestWindowInShellWithDelegate(
&delegate2, /*id=*/-1, gfx::Rect(450, 600)));
delegate2.set_minimum_size(gfx::Size(420, 300));
EXPECT_FALSE(controller->IsWindowInSplitView(window2.get()));
EXPECT_FALSE(
controller->CanSnapWindow(window2.get(), chromeos::kOneThirdSnapRatio));
EXPECT_TRUE(
controller->CanSnapWindow(window2.get(), chromeos::kDefaultSnapRatio));
wm::ActivateWindow(window2.get());
wm::ActivateWindow(window1.get());
const WindowSnapWMEvent snap_event1(WM_EVENT_SNAP_PRIMARY,
chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kTest);
WindowState::Get(window1.get())->OnWMEvent(&snap_event1);
EXPECT_EQ(controller->state(), SplitViewController::State::kPrimarySnapped);
OverviewController* overview_controller = OverviewController::Get();
EXPECT_TRUE(overview_controller->InOverviewSession());
auto* item2 = GetOverviewItemForWindow(window2.get());
CHECK(item2);
GetEventGenerator()->GestureTapAt(
gfx::ToRoundedPoint(item2->target_bounds().CenterPoint()));
EXPECT_FALSE(overview_controller->InOverviewSession());
EXPECT_EQ(controller->state(), SplitViewController::State::kBothSnapped);
EXPECT_FALSE(IsDividerAnimating());
// Re-snap `window` to trigger the bounce animation.
const WindowSnapWMEvent snap_event2(WM_EVENT_SNAP_PRIMARY,
chromeos::kTwoThirdSnapRatio,
WindowSnapActionSource::kTest);
WindowState::Get(window1.get())->OnWMEvent(&snap_event2);
EXPECT_TRUE(IsDividerAnimating());
SkipDividerSnapAnimation();
EXPECT_EQ(controller->state(), SplitViewController::State::kBothSnapped);
const auto work_area =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
const int divider_delta = kSplitviewDividerShortSideLength / 2;
EXPECT_EQ(
static_cast<float>(window1->GetBoundsInScreen().width() + divider_delta) /
work_area.width(),
0.5f);
EXPECT_EQ(
static_cast<float>(window2->GetBoundsInScreen().width() + divider_delta) /
work_area.width(),
0.5f);
}
// Verify that dragging the divider to the edge of the display to trigger
// `SplitViewDividerView::EndResizing()` and tapping it does not cause a crash.
// See regression at http://b/338665640.
TEST_F(SplitViewControllerTest,
NoCrashWhenDraggingDividerOutOfScreenAndTapDivider) {
UpdateDisplay("900x600");
SplitViewController* controller = split_view_controller();
std::unique_ptr<aura::Window> window1(
CreateWindow(gfx::Rect(0, 0, 520, 500)));
std::unique_ptr<aura::Window> window2(
CreateWindow(gfx::Rect(200, 100, 520, 200)));
EXPECT_FALSE(controller->IsWindowInSplitView(window1.get()));
controller->SnapWindow(window1.get(), SnapPosition::kPrimary,
WindowSnapActionSource::kSnapByWindowLayoutMenu,
/*activate_window=*/false,
chromeos::kDefaultSnapRatio);
EXPECT_EQ(SplitViewController::State::kPrimarySnapped, controller->state());
OverviewController* overview_controller = OverviewController::Get();
ASSERT_TRUE(overview_controller->InOverviewSession());
OverviewSession* overview_session = overview_controller->overview_session();
ASSERT_TRUE(overview_session);
auto* event_generator = GetEventGenerator();
event_generator->PressTouch(gfx::ToRoundedPoint(
overview_session->GetOverviewItemForWindow(window2.get())
->target_bounds()
.CenterPoint()));
event_generator->ReleaseTouch();
EXPECT_EQ(SplitViewController::State::kBothSnapped, controller->state());
SplitViewDivider* divider = split_view_divider();
auto* divider_Widget = divider->divider_widget();
EXPECT_TRUE(divider_Widget);
// Use the initial tap to move the divider to the edge of the screen.
event_generator->PressTouchId(
/*touch_id=*/0,
divider->GetDividerBoundsInScreen(/*is_dragging=*/false).CenterPoint());
event_generator->MoveTouchId(gfx::Point(-10, 0), 0);
ASSERT_TRUE(divider);
auto* divider_view = divider->divider_view_for_testing();
ASSERT_TRUE(divider_view);
ui::GestureEvent gesture_end(
0, 0, 0, ui::EventTimeForNow(),
ui::GestureEventDetails(ui::EventType::kGestureEnd));
divider->divider_view_for_testing()->OnGestureEvent(&gesture_end);
// Trigger a second tap, using a different `touch_id` for this tap, to
// simulate the crash scenario.
event_generator->PressTouchId(
/*touch_id=*/1,
divider->GetDividerBoundsInScreen(/*is_dragging=*/true).CenterPoint());
event_generator->ReleaseTouchId(1);
EXPECT_FALSE(controller->InSplitViewMode());
}
// Tests that when creating a new window while dragging the divider there will
// be no crash. See http://b/315549001 for more details about the crash
// reported.
TEST_F(SplitViewControllerTest, NoCrashWhenCreatingNewWindowWhileDragging) {
ui::ScopedAnimationDurationScaleMode animation_scale(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
UpdateDisplay("900x600");
SplitViewController* controller = split_view_controller();
std::unique_ptr<aura::Window> window1(
CreateWindow(gfx::Rect(0, 0, 520, 500)));
wm::ActivateWindow(window1.get());
EXPECT_FALSE(controller->IsWindowInSplitView(window1.get()));
controller->SnapWindow(
window1.get(), SnapPosition::kPrimary, WindowSnapActionSource::kTest,
/*activate_window=*/false, chromeos::kTwoThirdSnapRatio);
EXPECT_EQ(controller->state(), SplitViewController::State::kPrimarySnapped);
OverviewController* overview_controller = OverviewController::Get();
EXPECT_TRUE(overview_controller->InOverviewSession());
SplitViewDivider* divider = split_view_divider();
EXPECT_TRUE(divider->divider_widget());
const auto center_point =
divider->GetDividerBoundsInScreen(/*is_dragging=*/false).CenterPoint();
divider->StartResizeWithDivider(center_point);
divider->ResizeWithDivider(center_point + gfx::Vector2d(-20, 0));
// Verify that `window2` will be auto-snapped and overview will end with no
// crash.
std::unique_ptr<aura::Window> window2(
CreateWindow(gfx::Rect(0, 0, 520, 500)));
wm::ActivateWindow(window2.get());
EXPECT_FALSE(overview_controller->InOverviewSession());
EXPECT_EQ(controller->state(), SplitViewController::State::kBothSnapped);
}
// Tests that if there are two snapped windows, minimizing one of them will open
// overview window grid on the minimized window side of the screen. If there is
// only one snapped windows, minimizing the sanpped window will end split view
// mode and adjust the overview window grid bounds if the overview mode is
// active at that moment.
TEST_F(SplitViewControllerTest, MinimizeWindowTest) {
const gfx::Rect bounds(0, 0, 400, 400);
// 1 - First test one snapped window scenario.
std::unique_ptr<aura::Window> window0(CreateWindow(bounds));
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
split_view_controller()->SnapWindow(window0.get(), SnapPosition::kPrimary);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
WMEvent minimize_event(WM_EVENT_MINIMIZE);
WindowState::Get(window0.get())->OnWMEvent(&minimize_event);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
// 2 - Then test the scenario that has 2 or more windows.
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
EXPECT_EQ(split_view_controller()->default_snap_position(),
SnapPosition::kPrimary);
// Minimizing one of the two snapped windows will not end split view mode.
WindowState::Get(window1.get())->OnWMEvent(&minimize_event);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kSecondarySnapped);
// Since left window was minimized, its default snap position changed to
// RIGHT.
EXPECT_EQ(split_view_controller()->default_snap_position(),
SnapPosition::kSecondary);
// The overview window grid will open.
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
// Now minimize the other snapped window.
WindowState::Get(window2.get())->OnWMEvent(&minimize_event);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kNoSnap);
// The overview window grid is still open.
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
}
// Tests that if one of the snapped window gets maximized / full-screened, the
// split view mode ends.
TEST_F(SplitViewControllerTest, WindowStateChangeTest) {
const gfx::Rect bounds(0, 0, 400, 400);
// 1 - First test one snapped window scenario.
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
WMEvent maximize_event(WM_EVENT_MAXIMIZE);
WindowState::Get(window1.get())->OnWMEvent(&maximize_event);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
WMEvent fullscreen_event(WM_EVENT_FULLSCREEN);
WindowState::Get(window1.get())->OnWMEvent(&fullscreen_event);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
// 2 - Then test two snapped window scenario.
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
// Reactivate |window1| because it is the one that we will be maximizing and
// fullscreening. When |window1| goes out of scope at the end of the test, it
// will be a full screen window, and if it is not the active window, then the
// destructor will cause a |DCHECK| failure in |ash::WindowState::Get|.
wm::ActivateWindow(window1.get());
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
// Maximize one of the snapped window will end the split view mode.
WindowState::Get(window1.get())->OnWMEvent(&maximize_event);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
// Full-screen one of the snapped window will also end the split view mode.
WindowState::Get(window1.get())->OnWMEvent(&fullscreen_event);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
// 3 - Test the scenario that part of the screen is a snapped window and part
// of the screen is the overview window grid.
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
ToggleOverview();
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
// Maximize the snapped window will end the split view mode and overview mode.
WindowState::Get(window1.get())->OnWMEvent(&maximize_event);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
ToggleOverview();
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
// Fullscreen the snapped window will end the split view mode and overview
// mode.
WindowState::Get(window1.get())->OnWMEvent(&fullscreen_event);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
}
// Tests that if split view mode is active, activate another window will snap
// the window to the non-default side of the screen.
TEST_F(SplitViewControllerTest, WindowActivationTest) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
std::unique_ptr<aura::Window> window3(CreateWindow(bounds));
EXPECT_EQ(split_view_controller()->InSplitViewMode(), false);
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_EQ(split_view_controller()->InSplitViewMode(), true);
EXPECT_EQ(split_view_controller()->primary_window(), window1.get());
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kPrimarySnapped);
wm::ActivateWindow(window2.get());
EXPECT_EQ(split_view_controller()->secondary_window(), window2.get());
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
wm::ActivateWindow(window3.get());
EXPECT_EQ(split_view_controller()->secondary_window(), window3.get());
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
}
// Test that if the overview mode is active in clamshell mode, the window with
// |kUnresizableSnappedSizeKey| property can be snapped with size constraints.
TEST_F(SplitViewControllerTest, SnapWindowWithUnresizableSnapProperty) {
UpdateDisplay("800x600");
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window(CreateWindow(bounds));
window->SetProperty(aura::client::kResizeBehaviorKey,
aura::client::kResizeBehaviorNone);
window->SetProperty(kUnresizableSnappedSizeKey, new gfx::Size(300, 0));
// Switch to clamshell mode and enter overview mode.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
ToggleOverview();
ASSERT_TRUE(OverviewController::Get()->InOverviewSession());
split_view_controller()->SnapWindow(window.get(), SnapPosition::kPrimary);
EXPECT_EQ(window->GetBoundsInScreen().width(), 300);
}
// Tests that if split view mode is active when entering overview, the overview
// windows grid should show in the non-default side of the screen, and the
// default snapped window should not be shown in the overview window grid.
TEST_F(SplitViewControllerTest, EnterOverviewMode) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
std::unique_ptr<aura::Window> window3(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
EXPECT_EQ(split_view_controller()->GetDefaultSnappedWindow(), window1.get());
ToggleOverview();
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kPrimarySnapped);
EXPECT_FALSE(
base::Contains(GetWindowsListInOverviewGrids(),
split_view_controller()->GetDefaultSnappedWindow()));
}
// Tests that if split view mode and overview mode are active at the same time,
// i.e., half of the screen is occupied by a snapped window and half of the
// screen is occupied by the overview windows grid, the next activatable window
// will be picked to snap when exiting the overview mode.
TEST_F(SplitViewControllerTest, ExitOverviewMode) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
std::unique_ptr<aura::Window> window3(CreateWindow(bounds));
ASSERT_FALSE(split_view_controller()->InSplitViewMode());
ToggleOverview();
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kPrimarySnapped);
EXPECT_EQ(split_view_controller()->primary_window(), window1.get());
// Activate `window1` in preparation to verify that it stays active when
// overview mode is ended.
wm::ActivateWindow(window1.get());
ToggleOverview();
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
EXPECT_EQ(split_view_controller()->secondary_window(), window3.get());
EXPECT_TRUE(wm::IsActiveWindow(window1.get()));
}
#if defined(NDEBUG) && !defined(ADDRESS_SANITIZER) && \
!defined(LEAK_SANITIZER) && !defined(THREAD_SANITIZER)
// Tests that the overview mode enter exit smoothness histograms are recorded
// properly when one window is snapped.
TEST_F(SplitViewControllerTest, EnterExitOverviewModeHistograms) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
std::unique_ptr<aura::Window> window3(CreateWindow(bounds));
// Snap `window1` to the left. This will auto trigger entering overview.
wm::ActivateWindow(window1.get());
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
ASSERT_EQ(SplitViewController::State::kBothSnapped,
split_view_controller()->state());
ui::ScopedAnimationDurationScaleMode animation_scale(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
ToggleOverview();
WaitForOverviewEnterAnimation();
CheckOverviewEnterExitHistogram("EnterInSplitView", {0, 1}, {0, 0});
ToggleOverview();
WaitForOverviewExitAnimation();
CheckOverviewEnterExitHistogram("ExitInSplitView", {0, 1}, {0, 1});
}
#endif
// Tests that the split divider was created when the split view mode is active
// and destroyed when the split view mode is ended. The split divider should be
// always above the two snapped windows.
TEST_F(SplitViewControllerTest, SplitDividerBasicTest) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
EXPECT_TRUE(!split_view_divider()->divider_widget());
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_TRUE(split_view_divider()->divider_widget());
EXPECT_EQ(ui::ZOrderLevel::kNormal,
split_view_divider()->divider_widget()->GetZOrderLevel());
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
EXPECT_TRUE(split_view_divider()->divider_widget());
EXPECT_EQ(ui::ZOrderLevel::kNormal,
split_view_divider()->divider_widget()->GetZOrderLevel());
EXPECT_TRUE(window_util::IsStackedBelow(
window1.get(), split_view_divider()->GetDividerWindow()));
EXPECT_TRUE(window_util::IsStackedBelow(
window2.get(), split_view_divider()->GetDividerWindow()));
// Test that activating an non-snappable window ends the split view mode.
std::unique_ptr<aura::Window> window3(CreateNonSnappableWindow(bounds));
wm::ActivateWindow(window3.get());
EXPECT_FALSE(split_view_divider()->divider_widget());
}
// Tests that the split divider has the correct state when the dragged overview
// item is destroyed.
TEST_F(SplitViewControllerTest, DividerStateWhenDraggedOverviewItemDestroyed) {
std::unique_ptr<aura::Window> window1 = CreateTestWindow();
std::unique_ptr<aura::Window> window2 = CreateTestWindow();
std::unique_ptr<aura::Window> window3 = CreateTestWindow();
ToggleOverview();
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_EQ(ui::ZOrderLevel::kNormal,
split_view_divider()->divider_widget()->GetZOrderLevel());
OverviewSession* overview_session =
OverviewController::Get()->overview_session();
auto* overview_item =
overview_session->GetOverviewItemForWindow(window2.get());
gfx::PointF drag_point = overview_item->target_bounds().CenterPoint();
overview_session->InitiateDrag(overview_item, drag_point,
/*is_touch_dragging=*/false, overview_item);
drag_point.Offset(5.f, 0.f);
overview_session->Drag(overview_item, drag_point);
EXPECT_EQ(ui::ZOrderLevel::kNormal,
split_view_divider()->divider_widget()->GetZOrderLevel());
window2.reset();
EXPECT_EQ(ui::ZOrderLevel::kNormal,
split_view_divider()->divider_widget()->GetZOrderLevel());
// The split view divider should always be on top of the two snapped windows.
split_view_controller()->SnapWindow(window3.get(), SnapPosition::kSecondary);
EXPECT_EQ(ui::ZOrderLevel::kNormal,
split_view_divider()->divider_widget()->GetZOrderLevel());
EXPECT_TRUE(window_util::IsStackedBelow(window1.get(), window3.get()));
EXPECT_TRUE(window_util::IsStackedBelow(
window3.get(), split_view_divider()->GetDividerWindow()));
}
// Tests that the split divider has the correct state when the drag of the
// overview item is cancelled.
TEST_F(SplitViewControllerTest, DividerStateWhenOverviewItemDragCancelled) {
std::unique_ptr<aura::Window> window1 = CreateTestWindow();
std::unique_ptr<aura::Window> window2 = CreateTestWindow();
std::unique_ptr<aura::Window> window3 = CreateTestWindow();
ToggleOverview();
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_EQ(ui::ZOrderLevel::kNormal,
split_view_divider()->divider_widget()->GetZOrderLevel());
OverviewSession* overview_session =
OverviewController::Get()->overview_session();
auto* overview_item =
overview_session->GetOverviewItemForWindow(window2.get());
gfx::PointF drag_point = overview_item->target_bounds().CenterPoint();
overview_session->InitiateDrag(overview_item, drag_point,
/*is_touch_dragging=*/false, overview_item);
drag_point.Offset(5.f, 0.f);
overview_session->Drag(overview_item, drag_point);
EXPECT_EQ(ui::ZOrderLevel::kNormal,
split_view_divider()->divider_widget()->GetZOrderLevel());
// If the drag is canceled, the divider should be placed on top of the snapped
// window.
overview_session->ResetDraggedWindowGesture();
EXPECT_EQ(ui::ZOrderLevel::kNormal,
split_view_divider()->divider_widget()->GetZOrderLevel());
split_view_controller()->SnapWindow(window3.get(), SnapPosition::kSecondary);
EXPECT_EQ(ui::ZOrderLevel::kNormal,
split_view_divider()->divider_widget()->GetZOrderLevel());
wm::ActivateWindow(window3.get());
EXPECT_EQ(ui::ZOrderLevel::kNormal,
split_view_divider()->divider_widget()->GetZOrderLevel());
EXPECT_TRUE(window_util::IsStackedBelow(
window1.get(), split_view_divider()->GetDividerWindow()));
EXPECT_TRUE(window_util::IsStackedBelow(
window3.get(), split_view_divider()->GetDividerWindow()));
}
// Verifys that the bounds of the two windows in splitview are as expected.
TEST_F(SplitViewControllerTest, SplitDividerWindowBounds) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
ASSERT_TRUE(split_view_divider()->divider_widget());
// Verify with two freshly snapped windows are roughly the same width (off by
// one pixel at most due to the display maybe being even and the divider being
// a fixed odd pixel width).
int window1_width = window1->GetBoundsInScreen().width();
int window2_width = window2->GetBoundsInScreen().width();
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
const int screen_width =
screen_util::GetDisplayWorkAreaBoundsInParent(window1.get()).width();
EXPECT_NEAR(window1_width, window2_width, 1);
EXPECT_EQ(screen_width,
window1_width + divider_bounds.width() + window2_width);
// Drag the divider to a position two thirds of the screen size. Verify window
// 1 is wider than window 2.
GetEventGenerator()->set_current_screen_location(
divider_bounds.CenterPoint());
GetEventGenerator()->DragMouseTo(screen_width * 0.67f, 0);
SkipDividerSnapAnimation();
window1_width = window1->GetBoundsInScreen().width();
window2_width = window2->GetBoundsInScreen().width();
const int old_window1_width = window1_width;
const int old_window2_width = window2_width;
EXPECT_GT(window1_width, 2 * window2_width);
EXPECT_EQ(screen_width,
window1_width + divider_bounds.width() + window2_width);
// Drag the divider to a position close to two thirds of the screen size.
// Verify the divider snaps to two thirds of the screen size, and the windows
// remain the same size as previously.
divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
GetEventGenerator()->set_current_screen_location(
divider_bounds.CenterPoint());
GetEventGenerator()->DragMouseTo(screen_width * 0.7f, 0);
SkipDividerSnapAnimation();
window1_width = window1->GetBoundsInScreen().width();
window2_width = window2->GetBoundsInScreen().width();
EXPECT_EQ(window1_width, old_window1_width);
EXPECT_EQ(window2_width, old_window2_width);
// Drag the divider to a position one third of the screen size. Verify window
// 1 is wider than window 2.
divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
GetEventGenerator()->set_current_screen_location(
divider_bounds.CenterPoint());
GetEventGenerator()->DragMouseTo(screen_width * 0.33f, 0);
SkipDividerSnapAnimation();
window1_width = window1->GetBoundsInScreen().width();
window2_width = window2->GetBoundsInScreen().width();
EXPECT_GT(window2_width, 2 * window1_width);
EXPECT_EQ(screen_width,
window1_width + divider_bounds.width() + window2_width);
// Verify that the left window from dragging the divider to two thirds of the
// screen size is roughly the same size as the right window after dragging the
// divider to one third of the screen size, and vice versa.
EXPECT_NEAR(window1_width, old_window2_width, 1);
EXPECT_NEAR(window2_width, old_window1_width, 1);
}
// Tests that tablet mode multidisplay will end split view to reset window
// observations.
TEST_F(SplitViewControllerTest, TabletModeMultiDisplay) {
UpdateDisplay("800x600,800x600");
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
// Turn off the display mirror mode.
Shell::Get()->display_manager()->SetMirrorMode(display::MirrorMode::kOff,
std::nullopt);
// 1. Snap 1 window on display 1.
auto* split_view_controller1 =
SplitViewController::Get(Shell::GetAllRootWindows()[0]);
auto* split_view_controller2 =
SplitViewController::Get(Shell::GetAllRootWindows()[1]);
std::unique_ptr<aura::Window> w1(
CreateTestWindowInShellWithBounds(gfx::Rect(0, 0, 100, 100)));
split_view_controller1->SnapWindow(w1.get(), SnapPosition::kPrimary);
EXPECT_TRUE(split_view_controller1->InSplitViewMode());
EXPECT_TRUE(split_view_controller1->split_view_divider()->divider_widget());
// Move the window to display 2. Test we end split view on display 1.
PressAndReleaseKey(ui::VKEY_M, ui::EF_COMMAND_DOWN | ui::EF_ALT_DOWN);
EXPECT_TRUE(WindowState::Get(w1.get())->IsMaximized());
EXPECT_FALSE(split_view_controller1->InSplitViewMode());
EXPECT_FALSE(split_view_controller1->split_view_divider()->divider_widget());
// 2. Snap 2 windows on display 1.
std::unique_ptr<aura::Window> w2(
CreateTestWindowInShellWithBounds(gfx::Rect(0, 0, 100, 100)));
split_view_controller1->SnapWindow(w1.get(), SnapPosition::kPrimary);
split_view_controller1->SnapWindow(w2.get(), SnapPosition::kSecondary);
EXPECT_FALSE(split_view_controller2->InSplitViewMode());
EXPECT_TRUE(split_view_controller1->InSplitViewMode());
EXPECT_TRUE(split_view_controller1->split_view_divider()->divider_widget());
// Move the window to display 2. Test we end split view on display 1.
PressAndReleaseKey(ui::VKEY_M, ui::EF_COMMAND_DOWN | ui::EF_ALT_DOWN);
EXPECT_TRUE(WindowState::Get(w1.get())->IsMaximized());
EXPECT_TRUE(WindowState::Get(w2.get())->IsMaximized());
EXPECT_FALSE(split_view_controller1->InSplitViewMode());
EXPECT_FALSE(split_view_controller1->split_view_divider()->divider_widget());
}
// Verify that disconnecting a display which has a snapped window in it in
// tablet mode won't lead to a crash. Regression test for
// https://crbug.com/1316230.
TEST_F(SplitViewControllerTest,
DisplayDisconnectionWithSnappedWindowInTabletMode) {
ui::ScopedAnimationDurationScaleMode animation_scale(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
UpdateDisplay("800x600,800x600");
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
EXPECT_TRUE(EnterOverview());
// Turn off the display mirror mode.
Shell::Get()->display_manager()->SetMirrorMode(display::MirrorMode::kOff,
std::nullopt);
std::unique_ptr<aura::Window> w1(
CreateTestWindowInShellWithBounds(gfx::Rect(0, 0, 100, 100)));
std::unique_ptr<aura::Window> w2(
CreateTestWindowInShellWithBounds(gfx::Rect(900, 0, 100, 100)));
ASSERT_NE(w1->GetRootWindow(), w2->GetRootWindow());
// Snap the window on the second display.
auto* split_view_controller_on_display2 =
SplitViewController::Get(w2->GetRootWindow());
split_view_controller_on_display2->SnapWindow(w2.get(),
SnapPosition::kPrimary);
ASSERT_TRUE(split_view_controller_on_display2->split_view_divider()
->divider_widget());
// Now disconnect the second display, verify there's no crash.
UpdateDisplay("800x600");
base::RunLoop().RunUntilIdle();
}
// Verify that disconnecting a display while dragging the split view divider in
// it in tablet mode won't lead to a crash. Regression test for
// https://crbug.com/1316892.
TEST_F(SplitViewControllerTest,
DisplayDisconnectionWhileDraggingSplitDividerInTabletMode) {
ui::ScopedAnimationDurationScaleMode animation_scale(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
UpdateDisplay("800x600,800x600");
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
EXPECT_TRUE(EnterOverview());
// Turn off the display mirror mode.
Shell::Get()->display_manager()->SetMirrorMode(display::MirrorMode::kOff,
std::nullopt);
// Create a window on the secondary display.
std::unique_ptr<aura::Window> w(
CreateTestWindowInShellWithBounds(gfx::Rect(900, 0, 100, 100)));
// Snap the window on the second display.
auto* split_view_controller = SplitViewController::Get(w->GetRootWindow());
split_view_controller->SnapWindow(w.get(), SnapPosition::kPrimary);
auto* split_view_divider = split_view_controller->split_view_divider();
ASSERT_TRUE(split_view_divider->divider_widget());
auto* event_generator = GetEventGenerator();
const gfx::Point divider_center_pointer =
split_view_divider->GetDividerBoundsInScreen(/*is_dragging=*/false)
.CenterPoint();
event_generator->PressTouch(divider_center_pointer);
// Drag the split view divider without releasing the drag.
const gfx::Vector2d delta(100, 0);
event_generator->MoveTouch(divider_center_pointer + delta);
// Now disconnect the second display, verify there's no crash.
UpdateDisplay("800x600");
base::RunLoop().RunUntilIdle();
}
// Tests that the bounds of the snapped windows and divider are adjusted when
// the screen display configuration changes.
TEST_F(SplitViewControllerTest, DisplayConfigurationChangeTest) {
UpdateDisplay("407x400");
const gfx::Rect bounds(0, 0, 200, 200);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
const gfx::Rect bounds_window1 = window1->GetBoundsInScreen();
const gfx::Rect bounds_window2 = window2->GetBoundsInScreen();
const gfx::Rect bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Test that |window1| and |window2| has the same width and height after snap.
EXPECT_NEAR(bounds_window1.width(), bounds_window2.width(), 1);
EXPECT_EQ(bounds_window1.height(), bounds_window2.height());
EXPECT_EQ(bounds_divider.height(), bounds_window1.height());
// Test that |window1|, divider, |window2| are aligned properly.
EXPECT_EQ(bounds_divider.x(), bounds_window1.x() + bounds_window1.width());
EXPECT_EQ(bounds_window2.x(), bounds_divider.x() + bounds_divider.width());
// Now change the display configuration.
UpdateDisplay("507x500");
const gfx::Rect new_bounds_window1 = window1->GetBoundsInScreen();
const gfx::Rect new_bounds_window2 = window2->GetBoundsInScreen();
const gfx::Rect new_bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Test that the new bounds are different with the old ones.
EXPECT_NE(bounds_window1, new_bounds_window1);
EXPECT_NE(bounds_window2, new_bounds_window2);
EXPECT_NE(bounds_divider, new_bounds_divider);
// Test that |window1|, divider, |window2| are still aligned properly.
EXPECT_EQ(new_bounds_divider.x(),
new_bounds_window1.x() + new_bounds_window1.width());
EXPECT_EQ(new_bounds_window2.x(),
new_bounds_divider.x() + new_bounds_divider.width());
}
// Tests that the bounds of the snapped windows and divider are adjusted when
// the internal screen display configuration changes.
TEST_F(SplitViewControllerTest, InternalDisplayConfigurationChangeTest) {
UpdateDisplay("407x400");
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
const gfx::Rect bounds(0, 0, 200, 200);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
const gfx::Rect bounds_window1 = window1->GetBoundsInScreen();
const gfx::Rect bounds_window2 = window2->GetBoundsInScreen();
const gfx::Rect bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Test that |window1| and |window2| has the same width and height after snap.
EXPECT_NEAR(bounds_window1.width(), bounds_window2.width(), 1);
EXPECT_EQ(bounds_window1.height(), bounds_window2.height());
EXPECT_EQ(bounds_divider.height(), bounds_window1.height());
// Test that |window1|, divider, |window2| are aligned properly.
EXPECT_EQ(bounds_divider.x(), bounds_window1.x() + bounds_window1.width());
EXPECT_EQ(bounds_window2.x(), bounds_divider.x() + bounds_divider.width());
// Now change the display configuration.
UpdateDisplay("507x500");
const gfx::Rect new_bounds_window1 = window1->GetBoundsInScreen();
const gfx::Rect new_bounds_window2 = window2->GetBoundsInScreen();
const gfx::Rect new_bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Test that the new bounds are different with the old ones.
EXPECT_NE(bounds_window1, new_bounds_window1);
EXPECT_NE(bounds_window2, new_bounds_window2);
EXPECT_NE(bounds_divider, new_bounds_divider);
// Test that |window1|, divider, |window2| are still aligned properly.
EXPECT_EQ(new_bounds_divider.x(),
new_bounds_window1.x() + new_bounds_window1.width());
EXPECT_EQ(new_bounds_window2.x(),
new_bounds_divider.x() + new_bounds_divider.width());
}
// Test that if the internal screen display configuration changes during the
// divider snap animation, then this animation stops, and the bounds of the
// snapped windows and divider are adjusted as normal.
TEST_F(SplitViewControllerTest,
InternalDisplayConfigurationChangeDuringDividerSnap) {
UpdateDisplay("407x400");
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
const gfx::Rect bounds(0, 0, 200, 200);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
const gfx::Rect bounds_window1 = window1->GetBoundsInScreen();
const gfx::Rect bounds_window2 = window2->GetBoundsInScreen();
const gfx::Rect bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Test that |window1| and |window2| has the same width and height after snap.
EXPECT_NEAR(bounds_window1.width(), bounds_window2.width(), 1);
EXPECT_EQ(bounds_window1.height(), bounds_window2.height());
EXPECT_EQ(bounds_divider.height(), bounds_window1.height());
// Test that |window1|, divider, |window2| are aligned properly.
EXPECT_EQ(bounds_divider.x(), bounds_window1.x() + bounds_window1.width());
EXPECT_EQ(bounds_window2.x(), bounds_divider.x() + bounds_divider.width());
// Drag the divider to trigger the snap animation.
const gfx::Point divider_center =
split_view_divider()
->GetDividerBoundsInScreen(false /* is_dragging */)
.CenterPoint();
GetEventGenerator()->set_current_screen_location(divider_center);
GetEventGenerator()->DragMouseBy(20, 0);
ASSERT_TRUE(IsDividerAnimating());
const gfx::Rect animation_start_bounds_window1 = window1->GetBoundsInScreen();
const gfx::Rect animation_start_bounds_window2 = window2->GetBoundsInScreen();
const gfx::Rect animation_start_bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Change the display configuration and check that the snap animation stops.
UpdateDisplay("507x500");
EXPECT_FALSE(IsDividerAnimating());
const gfx::Rect new_bounds_window1 = window1->GetBoundsInScreen();
const gfx::Rect new_bounds_window2 = window2->GetBoundsInScreen();
const gfx::Rect new_bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Test that the new bounds are different with the old ones.
EXPECT_NE(bounds_window1, new_bounds_window1);
EXPECT_NE(bounds_window2, new_bounds_window2);
EXPECT_NE(bounds_divider, new_bounds_divider);
// Test that the new bounds are also different with the ones from the start of
// the divider snap animation.
EXPECT_NE(bounds_window1, animation_start_bounds_window1);
EXPECT_NE(bounds_window2, animation_start_bounds_window2);
EXPECT_NE(bounds_divider, animation_start_bounds_divider);
// Test that |window1|, divider, |window2| are still aligned properly.
EXPECT_EQ(new_bounds_divider.x(),
new_bounds_window1.x() + new_bounds_window1.width());
EXPECT_EQ(new_bounds_window2.x(),
new_bounds_divider.x() + new_bounds_divider.width());
}
// Test that if the internal screen display configuration changes during the
// divider snap animation, and if the adjusted divider bounds place it at the
// left edge of the screen, then split view ends.
TEST_F(SplitViewControllerTest,
InternalDisplayConfigurationChangeDuringDividerSnapToLeft) {
UpdateDisplay("407x400");
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
const gfx::Rect bounds(0, 0, 200, 200);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
const gfx::Rect bounds_window1 = window1->GetBoundsInScreen();
const gfx::Rect bounds_window2 = window2->GetBoundsInScreen();
const gfx::Rect bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Test that |window1| and |window2| has the same width and height after snap.
EXPECT_NEAR(bounds_window1.width(), bounds_window2.width(), 1);
EXPECT_EQ(bounds_window1.height(), bounds_window2.height());
EXPECT_EQ(bounds_divider.height(), bounds_window1.height());
// Test that |window1|, divider, |window2| are aligned properly.
EXPECT_EQ(bounds_divider.x(), bounds_window1.x() + bounds_window1.width());
EXPECT_EQ(bounds_window2.x(), bounds_divider.x() + bounds_divider.width());
// Drag the divider to end split view pending the snap animation.
const gfx::Point divider_center =
split_view_divider()
->GetDividerBoundsInScreen(false /* is_dragging */)
.CenterPoint();
GetEventGenerator()->set_current_screen_location(divider_center);
GetEventGenerator()->DragMouseBy(20 - bounds_window1.width(), 0);
ASSERT_TRUE(split_view_controller()->InSplitViewMode());
ASSERT_TRUE(IsDividerAnimating());
// Change the display configuration and check that split view ends.
UpdateDisplay("507x500");
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
}
// Test that if the internal screen display configuration changes during the
// divider snap animation, and if the adjusted divider bounds place it at the
// right edge of the screen, then split view ends.
TEST_F(SplitViewControllerTest,
InternalDisplayConfigurationChangeDuringDividerSnapToRight) {
UpdateDisplay("407x400");
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
const gfx::Rect bounds(0, 0, 200, 200);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
const gfx::Rect bounds_window1 = window1->GetBoundsInScreen();
const gfx::Rect bounds_window2 = window2->GetBoundsInScreen();
const gfx::Rect bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Test that |window1| and |window2| has the same width and height after snap.
EXPECT_NEAR(bounds_window1.width(), bounds_window2.width(), 1);
EXPECT_EQ(bounds_window1.height(), bounds_window2.height());
EXPECT_EQ(bounds_divider.height(), bounds_window1.height());
// Test that |window1|, divider, |window2| are aligned properly.
EXPECT_EQ(bounds_divider.x(), bounds_window1.x() + bounds_window1.width());
EXPECT_EQ(bounds_window2.x(), bounds_divider.x() + bounds_divider.width());
// Drag the divider to end split view pending the snap animation.
const gfx::Point divider_center =
split_view_divider()
->GetDividerBoundsInScreen(false /* is_dragging */)
.CenterPoint();
GetEventGenerator()->set_current_screen_location(divider_center);
GetEventGenerator()->DragMouseBy(bounds_window2.width() - 20, 0);
ASSERT_TRUE(split_view_controller()->InSplitViewMode());
ASSERT_TRUE(IsDividerAnimating());
// Change the display configuration and check that split view ends.
UpdateDisplay("507x500");
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
}
// Verify the left and right windows get swapped when SwapWindows is called or
// the divider is double clicked.
TEST_F(SplitViewControllerTest, SwapWindows) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
ASSERT_EQ(split_view_controller()->primary_window(), window1.get());
ASSERT_EQ(split_view_controller()->secondary_window(), window2.get());
gfx::Rect left_bounds = window1->GetBoundsInScreen();
gfx::Rect right_bounds = window2->GetBoundsInScreen();
// Verify that after swapping windows, the windows and their bounds have been
// swapped.
split_view_controller()->SwapWindows();
EXPECT_EQ(split_view_controller()->primary_window(), window2.get());
EXPECT_EQ(split_view_controller()->secondary_window(), window1.get());
EXPECT_EQ(left_bounds, window2->GetBoundsInScreen());
EXPECT_EQ(right_bounds, window1->GetBoundsInScreen());
// End split view mode and snap the window to RIGHT first, verify the function
// SwapWindows() still works properly.
EndSplitView();
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kSecondary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kPrimary);
ASSERT_EQ(split_view_controller()->secondary_window(), window1.get());
ASSERT_EQ(split_view_controller()->primary_window(), window2.get());
left_bounds = window2->GetBoundsInScreen();
right_bounds = window1->GetBoundsInScreen();
split_view_controller()->SwapWindows();
EXPECT_EQ(split_view_controller()->primary_window(), window1.get());
EXPECT_EQ(split_view_controller()->secondary_window(), window2.get());
EXPECT_EQ(left_bounds, window1->GetBoundsInScreen());
EXPECT_EQ(right_bounds, window2->GetBoundsInScreen());
// Perform a double click on the divider center.
const gfx::Point divider_center =
split_view_divider()
->GetDividerBoundsInScreen(false /* is_dragging */)
.CenterPoint();
GetEventGenerator()->set_current_screen_location(divider_center);
GetEventGenerator()->DoubleClickLeftButton();
EXPECT_EQ(split_view_controller()->primary_window(), window2.get());
EXPECT_EQ(split_view_controller()->secondary_window(), window1.get());
EXPECT_EQ(left_bounds, window2->GetBoundsInScreen());
EXPECT_EQ(right_bounds, window1->GetBoundsInScreen());
}
// Verify the left and right windows get swapped when the divider is double
// tapped and/or double clicked. Also tests we don't start a drag to resize.
TEST_F(SplitViewControllerTest, DoubleTapAndClickDivider) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
ASSERT_EQ(split_view_controller()->primary_window(), window1.get());
ASSERT_EQ(split_view_controller()->secondary_window(), window2.get());
gfx::Rect left_bounds = window1->GetBoundsInScreen();
gfx::Rect right_bounds = window2->GetBoundsInScreen();
// Perform a double tap on the divider center.
const gfx::Point divider_center =
split_view_divider()
->GetDividerBoundsInScreen(false /* is_dragging */)
.CenterPoint();
auto* event_generator = GetEventGenerator();
event_generator->GestureTapAt(divider_center);
EXPECT_FALSE(WindowState::Get(window1.get())->is_dragged());
EXPECT_FALSE(WindowState::Get(window2.get())->is_dragged());
event_generator->GestureTapAt(divider_center);
EXPECT_FALSE(WindowState::Get(window1.get())->is_dragged());
EXPECT_FALSE(WindowState::Get(window2.get())->is_dragged());
EXPECT_EQ(split_view_controller()->primary_window(), window2.get());
EXPECT_EQ(split_view_controller()->secondary_window(), window1.get());
EXPECT_EQ(left_bounds, window2->GetBoundsInScreen());
EXPECT_EQ(right_bounds, window1->GetBoundsInScreen());
// Press without releasing the mouse. Test we don't start a drag.
event_generator->MoveMouseTo(divider_center);
event_generator->PressLeftButton();
EXPECT_FALSE(WindowState::Get(window1.get())->is_dragged());
EXPECT_FALSE(WindowState::Get(window2.get())->is_dragged());
event_generator->ReleaseLeftButton();
// Now double click. Note we need to set `EF_IS_DOUBLE_CLICK` in the event
// flags to simulate a double click.
event_generator->DoubleClickLeftButton();
EXPECT_FALSE(WindowState::Get(window1.get())->is_dragged());
EXPECT_FALSE(WindowState::Get(window2.get())->is_dragged());
EXPECT_EQ(split_view_controller()->primary_window(), window1.get());
EXPECT_EQ(split_view_controller()->secondary_window(), window2.get());
EXPECT_EQ(left_bounds, window1->GetBoundsInScreen());
EXPECT_EQ(right_bounds, window2->GetBoundsInScreen());
}
// Verify the left and right windows do not get swapped when the divider is
// dragged and double clicked.
TEST_F(SplitViewControllerTest, DragAndDoubleClickDivider) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
ASSERT_EQ(split_view_controller()->primary_window(), window1.get());
ASSERT_EQ(split_view_controller()->secondary_window(), window2.get());
// Drag the divider and double click it before the snap animation moves it.
const gfx::Point divider_center =
split_view_divider()
->GetDividerBoundsInScreen(false /* is_dragging */)
.CenterPoint();
GetEventGenerator()->set_current_screen_location(divider_center);
GetEventGenerator()->DragMouseBy(20, 0);
GetEventGenerator()->DoubleClickLeftButton();
EXPECT_EQ(split_view_controller()->primary_window(), window1.get());
EXPECT_EQ(split_view_controller()->secondary_window(), window2.get());
}
// Verify the left and right windows do not get swapped when the divider is
// dragged and double tapped.
TEST_F(SplitViewControllerTest, DragAndDoubleTapDivider) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
ASSERT_EQ(split_view_controller()->primary_window(), window1.get());
ASSERT_EQ(split_view_controller()->secondary_window(), window2.get());
// Drag the divider and double tap it before the snap animation moves it.
const gfx::Point divider_center =
split_view_divider()
->GetDividerBoundsInScreen(false /* is_dragging */)
.CenterPoint();
GetEventGenerator()->set_current_screen_location(divider_center);
const gfx::Point drag_destination = divider_center + gfx::Vector2d(20, 0);
GetEventGenerator()->DragMouseTo(drag_destination);
GetEventGenerator()->GestureTapAt(drag_destination);
GetEventGenerator()->GestureTapAt(drag_destination);
EXPECT_EQ(split_view_controller()->primary_window(), window1.get());
EXPECT_EQ(split_view_controller()->secondary_window(), window2.get());
}
// Verify overview does not steal focus from a split view window when trading
// places with it.
TEST_F(SplitViewControllerTest, OverviewNotStealFocusOnSwapWindows) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
ToggleOverview();
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kPrimary);
wm::ActivateWindow(window2.get());
split_view_controller()->SwapWindows();
EXPECT_TRUE(wm::IsActiveWindow(window2.get()));
}
using SplitViewControllerFloatTest = SplitViewControllerTest;
// Tests that the floated window is not auto-snapped if it's on top of two
// snapped windows. It should only get snapped if it's activated from overview.
TEST_F(SplitViewControllerFloatTest, DontAutosnapFloatedWindow) {
// Create 2 normal windows and 1 floated window.
std::unique_ptr<aura::Window> window1(CreateAppWindow());
std::unique_ptr<aura::Window> window2(CreateAppWindow());
std::unique_ptr<aura::Window> floated_window(CreateAppWindow());
Shell::Get()->float_controller()->ToggleFloat(floated_window.get());
ASSERT_TRUE(WindowState::Get(floated_window.get())->IsFloated());
// Snap `window1` so that Overview is open.
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
auto* overview_controller = OverviewController::Get();
ASSERT_TRUE(OverviewController::Get()->InOverviewSession());
auto* overview_session = overview_controller->overview_session();
ASSERT_TRUE(overview_session->IsWindowInOverview(window2.get()));
ASSERT_TRUE(overview_session->IsWindowInOverview(floated_window.get()));
// Activate `window2` from Overview. Test that it gets snapped in splitview,
// and `floated_window` remains floated.
wm::ActivateWindow(window2.get());
EXPECT_TRUE(split_view_controller()->IsWindowInSplitView(window2.get()));
wm::ActivateWindow(floated_window.get());
EXPECT_FALSE(
split_view_controller()->IsWindowInSplitView(floated_window.get()));
EXPECT_TRUE(WindowState::Get(floated_window.get())->IsFloated());
// Snap `window1` again, then activate `floated_window` from Overview. Test
// that it gets snapped in splitview.
EndSplitView();
EXPECT_TRUE(WindowState::Get(floated_window.get())->IsFloated());
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
overview_session = overview_controller->overview_session();
EXPECT_TRUE(overview_session->IsWindowInOverview(floated_window.get()));
wm::ActivateWindow(floated_window.get());
EXPECT_TRUE(
split_view_controller()->IsWindowInSplitView(floated_window.get()));
EXPECT_FALSE(WindowState::Get(floated_window.get())->IsFloated());
}
// Verify that you cannot start dragging the divider during its snap animation.
TEST_F(SplitViewControllerTest, StartDraggingDividerDuringSnapAnimation) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
ASSERT_EQ(split_view_controller()->primary_window(), window1.get());
ASSERT_EQ(split_view_controller()->secondary_window(), window2.get());
// Drag the divider and then try to start dragging it again without waiting
// for the snap animation.
const gfx::Point divider_center =
split_view_divider()
->GetDividerBoundsInScreen(false /* is_dragging */)
.CenterPoint();
GetEventGenerator()->set_current_screen_location(divider_center);
GetEventGenerator()->DragMouseBy(20, 0);
GetEventGenerator()->PressLeftButton();
EXPECT_FALSE(split_view_controller()->IsResizingWithDivider());
GetEventGenerator()->ReleaseLeftButton();
}
TEST_F(SplitViewControllerTest, LongPressEntersSplitView) {
// Tests that with no active windows, split view does not get activated.
LongPressOnOverviewButtonTray();
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
wm::ActivateWindow(window1.get());
// Tests that with split view gets activated with an active window.
LongPressOnOverviewButtonTray();
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
}
// Verify that when in split view mode with either one snapped or two snapped
// windows, split view mode gets exited when the overview button gets a long
// press event.
TEST_F(SplitViewControllerTest, LongPressExitsSplitView) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
wm::ActivateWindow(window2.get());
wm::ActivateWindow(window1.get());
// Snap |window1| to the left.
ToggleOverview();
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
ASSERT_TRUE(split_view_controller()->InSplitViewMode());
// Verify that by long pressing on the overview button tray with left snapped
// window, split view mode gets exited and the left window (|window1|) is the
// current active window.
LongPressOnOverviewButtonTray();
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
EXPECT_EQ(window1.get(), window_util::GetActiveWindow());
// Snap |window1| to the right.
ToggleOverview();
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kSecondary);
ASSERT_TRUE(split_view_controller()->InSplitViewMode());
// Verify that by long pressing on the overview button tray with right snapped
// window, split view mode gets exited and the right window (|window1|) is the
// current active window.
LongPressOnOverviewButtonTray();
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
EXPECT_EQ(window1.get(), window_util::GetActiveWindow());
// Snap two windows and activate the left window, |window1|.
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
wm::ActivateWindow(window1.get());
ASSERT_TRUE(split_view_controller()->InSplitViewMode());
// Verify that by long pressing on the overview button tray with two snapped
// windows, split view mode gets exited.
LongPressOnOverviewButtonTray();
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(window1.get(), window_util::GetActiveWindow());
// Snap two windows and activate the right window, |window2|.
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
wm::ActivateWindow(window2.get());
ASSERT_TRUE(split_view_controller()->InSplitViewMode());
// Verify that by long pressing on the overview button tray with two snapped
// windows, split view mode gets exited, and the activated window in splitview
// is the current active window.
LongPressOnOverviewButtonTray();
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(window2.get(), window_util::GetActiveWindow());
}
// Verify that if a window with a transient child which is not snappable is
// activated, and the the overview tray is long pressed, we will enter splitview
// with the transient parent snapped.
TEST_F(SplitViewControllerTest, LongPressEntersSplitViewWithTransientChild) {
// Add two windows with one being a transient child of the first.
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> parent(CreateWindow(bounds));
std::unique_ptr<aura::Window> child(
CreateWindow(bounds, aura::client::WINDOW_TYPE_POPUP));
wm::AddTransientChild(parent.get(), child.get());
wm::ActivateWindow(parent.get());
wm::ActivateWindow(child.get());
// Verify that long press will snap the focused transient child's parent.
LongPressOnOverviewButtonTray();
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(split_view_controller()->GetDefaultSnappedWindow(), parent.get());
}
TEST_F(SplitViewControllerTest, LongPressExitsSplitViewWithTransientChild) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> left_window(CreateWindow(bounds));
std::unique_ptr<aura::Window> right_window(CreateWindow(bounds));
wm::ActivateWindow(left_window.get());
wm::ActivateWindow(right_window.get());
ToggleOverview();
split_view_controller()->SnapWindow(left_window.get(),
SnapPosition::kPrimary);
split_view_controller()->SnapWindow(right_window.get(),
SnapPosition::kSecondary);
ASSERT_TRUE(split_view_controller()->InSplitViewMode());
// Add a transient child to |right_window|, and activate it.
aura::Window* transient_child =
aura::test::CreateTestWindowWithId(0, right_window.get());
::wm::AddTransientChild(right_window.get(), transient_child);
wm::ActivateWindow(transient_child);
// Verify that by long pressing on the overview button tray, split view mode
// gets exited and the window which contained |transient_child| is the
// current active window.
LongPressOnOverviewButtonTray();
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
EXPECT_EQ(right_window.get(), window_util::GetActiveWindow());
}
// Verify that split view mode get activated when long pressing on the overview
// button while in overview mode if we have at least one window.
TEST_F(SplitViewControllerTest, LongPressInOverviewMode) {
ToggleOverview();
ASSERT_TRUE(OverviewController::Get()->InOverviewSession());
ASSERT_FALSE(split_view_controller()->InSplitViewMode());
// Nothing happens if there are no windows.
LongPressOnOverviewButtonTray();
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
std::unique_ptr<aura::Window> window = CreateAppWindow();
ASSERT_FALSE(OverviewController::Get()->InOverviewSession());
ToggleOverview();
ASSERT_TRUE(OverviewController::Get()->InOverviewSession());
ASSERT_FALSE(split_view_controller()->InSplitViewMode());
// Verify that with a window, a long press on the overview button tray will
// enter splitview.
LongPressOnOverviewButtonTray();
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(window.get(), split_view_controller()->primary_window());
}
// Tests the overview animation smoothness histograms when using long pressing
// the overview button.
#if defined(NDEBUG) && !defined(ADDRESS_SANITIZER) && \
!defined(LEAK_SANITIZER) && !defined(THREAD_SANITIZER)
TEST_F(SplitViewControllerTest, LongPressInOverviewModeHistograms) {
ui::ScopedAnimationDurationScaleMode animation_scale(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
ToggleOverview();
WaitForOverviewEnterAnimation();
ASSERT_TRUE(OverviewController::Get()->InOverviewSession());
CheckOverviewEnterExitHistogram("EnterInTablet", {0, 0}, {0, 0});
// Nothing happens if there are no windows.
LongPressOnOverviewButtonTray();
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
// Activating a window will exit overview.
std::unique_ptr<aura::Window> window = CreateAppWindow();
CheckOverviewEnterExitHistogram("ExitByActivation", {0, 0}, {0, 0});
ToggleOverview();
WaitForOverviewEnterAnimation();
CheckOverviewEnterExitHistogram("EnterInTablet2", {1, 0}, {0, 0});
ASSERT_TRUE(OverviewController::Get()->InOverviewSession());
ASSERT_FALSE(split_view_controller()->InSplitViewMode());
// Verify that with a window, a long press on the overview button tray will
// enter splitview, but with no animation.
LongPressOnOverviewButtonTray();
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(window.get(), split_view_controller()->primary_window());
CheckOverviewEnterExitHistogram("NoTransition", {1, 0}, {0, 0});
}
#endif
TEST_F(SplitViewControllerTest, LongPressWithUnsnappableWindow) {
// Add an unsnappable window and a regular window.
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> unsnappable_window(
CreateNonSnappableWindow(bounds));
ASSERT_FALSE(split_view_controller()->InSplitViewMode());
std::unique_ptr<aura::Window> regular_window(CreateWindow(bounds));
wm::ActivateWindow(regular_window.get());
wm::ActivateWindow(unsnappable_window.get());
ASSERT_EQ(unsnappable_window.get(), window_util::GetActiveWindow());
// Verify split view is not activated when long press occurs when active
// window is unsnappable.
LongPressOnOverviewButtonTray();
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
// Verify split view is not activated when long press occurs in overview mode
// and the most recent window is unsnappable.
ToggleOverview();
ASSERT_TRUE(Shell::Get()
->mru_window_tracker()
->BuildWindowForCycleList(kActiveDesk)
.size() > 0);
ASSERT_EQ(unsnappable_window.get(),
Shell::Get()->mru_window_tracker()->BuildWindowForCycleList(
kActiveDesk)[0]);
LongPressOnOverviewButtonTray();
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
}
// Tests that long press works even if the window is minimized.
TEST_F(SplitViewControllerTest, LongPressWithMinimizedWindow) {
std::unique_ptr<aura::Window> window(CreateWindow(gfx::Rect(400, 400)));
WindowState::Get(window.get())->Minimize();
LongPressOnOverviewButtonTray();
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
}
// Test the rotation functionalities in split view mode.
TEST_F(SplitViewControllerTest, RotationTest) {
UpdateDisplay("807x407");
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
ScreenOrientationControllerTestApi test_api(
Shell::Get()->screen_orientation_controller());
// Set the screen orientation to LANDSCAPE_PRIMARY.
test_api.SetDisplayRotation(display::Display::ROTATE_0,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(test_api.GetCurrentOrientation(),
chromeos::OrientationType::kLandscapePrimary);
const gfx::Rect bounds(0, 0, 200, 200);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
gfx::Rect bounds_window1 = window1->GetBoundsInScreen();
gfx::Rect bounds_window2 = window2->GetBoundsInScreen();
gfx::Rect bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Test |window1|, divider and |window2| are aligned horizontally.
// |window1| is on the left, then the divider, and then |window2|.
EXPECT_EQ(bounds_divider.x(), bounds_window1.x() + bounds_window1.width());
EXPECT_EQ(bounds_window2.x(), bounds_divider.x() + bounds_divider.width());
EXPECT_EQ(bounds_window1.height(), bounds_divider.height());
EXPECT_EQ(bounds_window1.height(), bounds_window2.height());
// Rotate the screen by 270 degree.
test_api.SetDisplayRotation(display::Display::ROTATE_270,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(test_api.GetCurrentOrientation(),
chromeos::OrientationType::kPortraitPrimary);
bounds_window1 = window1->GetBoundsInScreen();
bounds_window2 = window2->GetBoundsInScreen();
bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Test that |window1|, divider, |window2| are now aligned vertically.
// |window1| is on the top, then the divider, and then |window2|.
EXPECT_EQ(bounds_divider.y(), bounds_window1.y() + bounds_window1.height());
EXPECT_EQ(bounds_window2.y(), bounds_divider.y() + bounds_divider.height());
EXPECT_EQ(bounds_window1.width(), bounds_divider.width());
EXPECT_EQ(bounds_window1.width(), bounds_window2.width());
// Rotate the screen by 180 degree.
test_api.SetDisplayRotation(display::Display::ROTATE_180,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(test_api.GetCurrentOrientation(),
chromeos::OrientationType::kLandscapeSecondary);
bounds_window1 = window1->GetBoundsInScreen();
bounds_window2 = window2->GetBoundsInScreen();
bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Test that |window1|, divider, |window2| are now aligned horizontally.
// |window2| is on the left, then the divider, and then |window1|.
EXPECT_EQ(bounds_divider.x(), bounds_window2.x() + bounds_window2.width());
EXPECT_EQ(bounds_window1.x(), bounds_divider.x() + bounds_divider.width());
EXPECT_EQ(bounds_window1.height(), bounds_divider.height());
EXPECT_EQ(bounds_window1.height(), bounds_window2.height());
// Rotate the screen by 90 degree.
test_api.SetDisplayRotation(display::Display::ROTATE_90,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(test_api.GetCurrentOrientation(),
chromeos::OrientationType::kPortraitSecondary);
bounds_window1 = window1->GetBoundsInScreen();
bounds_window2 = window2->GetBoundsInScreen();
bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Test that |window1|, divider, |window2| are now aligned vertically.
// |window2| is on the top, then the divider, and then |window1|.
EXPECT_EQ(bounds_divider.y(), bounds_window2.y() + bounds_window2.height());
EXPECT_EQ(bounds_window1.y(), bounds_divider.y() + bounds_divider.height());
EXPECT_EQ(bounds_window1.width(), bounds_divider.width());
EXPECT_EQ(bounds_window1.width(), bounds_window2.width());
// Rotate the screen back to 0 degree.
test_api.SetDisplayRotation(display::Display::ROTATE_0,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(test_api.GetCurrentOrientation(),
chromeos::OrientationType::kLandscapePrimary);
bounds_window1 = window1->GetBoundsInScreen();
bounds_window2 = window2->GetBoundsInScreen();
bounds_divider =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
// Test |window1|, divider and |window2| are aligned horizontally.
// |window1| is on the left, then the divider, and then |window2|.
EXPECT_EQ(bounds_divider.x(), bounds_window1.x() + bounds_window1.width());
EXPECT_EQ(bounds_window2.x(), bounds_divider.x() + bounds_divider.width());
EXPECT_EQ(bounds_window1.height(), bounds_divider.height());
EXPECT_EQ(bounds_window1.height(), bounds_window2.height());
}
// Test that if the split view mode is active when exiting tablet mode, we
// should also end split view mode.
TEST_F(SplitViewControllerTest, ExitTabletModeEndSplitView) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
}
// Test that |SplitViewController::CanSnapWindow| checks that the minimum size
// of the window fits into the left or top, with the default divider position.
// (If the work area length is odd, then the right or bottom will be one pixel
// larger.)
TEST_F(SplitViewControllerTest, SnapWindowWithMinimumSizeTest) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
EXPECT_TRUE(split_view_controller()->CanSnapWindow(
window1.get(), chromeos::kDefaultSnapRatio));
UpdateDisplay("800x600");
aura::test::TestWindowDelegate* delegate =
static_cast<aura::test::TestWindowDelegate*>(window1->delegate());
const int divider_delta = kSplitviewDividerShortSideLength / 2;
delegate->set_minimum_size(
gfx::Size(800 * chromeos::kDefaultSnapRatio - divider_delta, 0));
EXPECT_TRUE(split_view_controller()->CanSnapWindow(
window1.get(), chromeos::kDefaultSnapRatio));
delegate->set_minimum_size(
gfx::Size(800 * chromeos::kDefaultSnapRatio - divider_delta + 1, 0));
EXPECT_FALSE(split_view_controller()->CanSnapWindow(
window1.get(), chromeos::kDefaultSnapRatio));
UpdateDisplay("799x600");
delegate->set_minimum_size(
gfx::Size(799 * chromeos::kDefaultSnapRatio - divider_delta, 0));
EXPECT_TRUE(split_view_controller()->CanSnapWindow(
window1.get(), chromeos::kDefaultSnapRatio));
delegate->set_minimum_size(
gfx::Size(799 * chromeos::kDefaultSnapRatio - divider_delta + 1, 0));
EXPECT_FALSE(split_view_controller()->CanSnapWindow(
window1.get(), chromeos::kDefaultSnapRatio));
}
// Test that |SplitViewController::CanSnapWindow| property checks that the
// unresizable snapping condition.
TEST_F(SplitViewControllerTest, CanSnapWindowWithUnresizableSnapProperty) {
UpdateDisplay("800x600");
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window(CreateWindow(bounds));
window->SetProperty(aura::client::kResizeBehaviorKey,
aura::client::kResizeBehaviorNone);
EXPECT_FALSE(split_view_controller()->CanSnapWindow(
window.get(), chromeos::kDefaultSnapRatio));
// Clamshell mode supports unresizable snapping.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
window->SetProperty(kUnresizableSnappedSizeKey, new gfx::Size(300, 0));
EXPECT_TRUE(split_view_controller()->CanSnapWindow(
window.get(), chromeos::kDefaultSnapRatio));
// Tablet mode doesn't support unresizable snapping.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
EXPECT_FALSE(split_view_controller()->CanSnapWindow(
window.get(), chromeos::kDefaultSnapRatio));
// If the display is too small for the unresizable snapping, it can't be
// snapped.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
UpdateDisplay("200x100");
EXPECT_FALSE(split_view_controller()->CanSnapWindow(
window.get(), chromeos::kDefaultSnapRatio));
}
// Tests that the snapped window can not be moved outside of work area when its
// minimum size is larger than its current desired resizing bounds.
TEST_F(SplitViewControllerTest, ResizingSnappedWindowWithMinimumSizeTest) {
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
ScreenOrientationControllerTestApi test_api(
Shell::Get()->screen_orientation_controller());
const gfx::Rect bounds(0, 0, 300, 200);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
aura::test::TestWindowDelegate* delegate1 =
static_cast<aura::test::TestWindowDelegate*>(window1->delegate());
// Set the screen orientation to LANDSCAPE_PRIMARY
test_api.SetDisplayRotation(display::Display::ROTATE_0,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(test_api.GetCurrentOrientation(),
chromeos::OrientationType::kLandscapePrimary);
gfx::Rect display_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window1.get());
ToggleOverview();
EXPECT_TRUE(split_view_controller()->CanSnapWindow(
window1.get(), chromeos::kDefaultSnapRatio));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
delegate1->set_minimum_size(
gfx::Size(display_bounds.width() * 0.4f, display_bounds.height()));
EXPECT_TRUE(window1->layer()->GetTargetTransform().IsIdentity());
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false);
split_view_divider()->StartResizeWithDivider(divider_bounds.CenterPoint());
gfx::Point resize_point(display_bounds.width() * 0.33f, 0);
split_view_divider()->ResizeWithDivider(resize_point);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.TabletMode.SingleWindow", 1);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.SingleWindow",
0);
gfx::Rect snapped_window_bounds =
split_view_controller()->GetSnappedWindowBoundsInScreen(
SnapPosition::kPrimary, window1.get(), chromeos::kDefaultSnapRatio,
/*account_for_divider_width=*/true);
// The snapped window bounds can't be pushed outside of the display area.
EXPECT_EQ(snapped_window_bounds.x(), display_bounds.x());
EXPECT_EQ(snapped_window_bounds.width(),
window1->delegate()->GetMinimumSize().width());
EXPECT_FALSE(window1->layer()->GetTargetTransform().IsIdentity());
split_view_divider()->EndResizeWithDivider(resize_point);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.TabletMode.SingleWindow", 1);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.SingleWindow",
1);
SkipDividerSnapAnimation();
EndSplitView();
// Rotate the screen by 270 degree.
test_api.SetDisplayRotation(display::Display::ROTATE_270,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(test_api.GetCurrentOrientation(),
chromeos::OrientationType::kPortraitPrimary);
display_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window1.get());
delegate1->set_minimum_size(
gfx::Size(display_bounds.width(), display_bounds.height() * 0.4f));
EXPECT_TRUE(split_view_controller()->CanSnapWindow(
window1.get(), chromeos::kDefaultSnapRatio));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_TRUE(window1->layer()->GetTargetTransform().IsIdentity());
divider_bounds = split_view_divider()->GetDividerBoundsInScreen(false);
split_view_divider()->StartResizeWithDivider(divider_bounds.CenterPoint());
resize_point.SetPoint(0, display_bounds.height() * 0.33f);
split_view_divider()->ResizeWithDivider(resize_point);
snapped_window_bounds =
split_view_controller()->GetSnappedWindowBoundsInScreen(
SnapPosition::kPrimary, window1.get(), chromeos::kDefaultSnapRatio,
/*account_for_divider_width=*/true);
EXPECT_EQ(snapped_window_bounds.y(), display_bounds.y());
EXPECT_EQ(snapped_window_bounds.height(),
window1->delegate()->GetMinimumSize().height());
EXPECT_FALSE(window1->layer()->GetTargetTransform().IsIdentity());
split_view_divider()->EndResizeWithDivider(resize_point);
SkipDividerSnapAnimation();
EndSplitView();
// Rotate the screen by 180 degree.
test_api.SetDisplayRotation(display::Display::ROTATE_180,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(test_api.GetCurrentOrientation(),
chromeos::OrientationType::kLandscapeSecondary);
display_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window1.get());
delegate1->set_minimum_size(
gfx::Size(display_bounds.width() * 0.4f, display_bounds.height()));
EXPECT_TRUE(split_view_controller()->CanSnapWindow(
window1.get(), chromeos::kDefaultSnapRatio));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kSecondary);
EXPECT_TRUE(window1->layer()->GetTargetTransform().IsIdentity());
divider_bounds = split_view_divider()->GetDividerBoundsInScreen(false);
split_view_divider()->StartResizeWithDivider(divider_bounds.CenterPoint());
resize_point.SetPoint(display_bounds.width() * 0.33f, 0);
split_view_divider()->ResizeWithDivider(resize_point);
snapped_window_bounds =
split_view_controller()->GetSnappedWindowBoundsInScreen(
SnapPosition::kSecondary, window1.get(), chromeos::kDefaultSnapRatio,
/*account_for_divider_width=*/true);
EXPECT_EQ(snapped_window_bounds.x(), display_bounds.x());
EXPECT_EQ(snapped_window_bounds.width(),
window1->delegate()->GetMinimumSize().width());
EXPECT_FALSE(window1->layer()->GetTargetTransform().IsIdentity());
split_view_divider()->EndResizeWithDivider(resize_point);
SkipDividerSnapAnimation();
EndSplitView();
// Rotate the screen by 90 degree.
test_api.SetDisplayRotation(display::Display::ROTATE_90,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(test_api.GetCurrentOrientation(),
chromeos::OrientationType::kPortraitSecondary);
display_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window1.get());
delegate1->set_minimum_size(
gfx::Size(display_bounds.width(), display_bounds.height() * 0.4f));
EXPECT_TRUE(split_view_controller()->CanSnapWindow(
window1.get(), chromeos::kDefaultSnapRatio));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kSecondary);
EXPECT_TRUE(window1->layer()->GetTargetTransform().IsIdentity());
divider_bounds = split_view_divider()->GetDividerBoundsInScreen(false);
split_view_divider()->StartResizeWithDivider(divider_bounds.CenterPoint());
resize_point.SetPoint(0, display_bounds.height() * 0.33f);
split_view_divider()->ResizeWithDivider(resize_point);
snapped_window_bounds =
split_view_controller()->GetSnappedWindowBoundsInScreen(
SnapPosition::kSecondary, window1.get(), chromeos::kDefaultSnapRatio,
/*account_for_divider_width=*/true);
EXPECT_EQ(snapped_window_bounds.y(), display_bounds.y());
EXPECT_EQ(snapped_window_bounds.height(),
window1->delegate()->GetMinimumSize().height());
EXPECT_FALSE(window1->layer()->GetTargetTransform().IsIdentity());
split_view_divider()->EndResizeWithDivider(resize_point);
SkipDividerSnapAnimation();
EndSplitView();
}
// Tests that the divider should not be moved to a position that is smaller than
// the snapped window's minimum size after resizing.
TEST_F(SplitViewControllerTest,
DividerPositionOnResizingSnappedWindowWithMinimumSizeTest) {
const gfx::Rect bounds(0, 0, 200, 200);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
aura::test::TestWindowDelegate* delegate1 =
static_cast<aura::test::TestWindowDelegate*>(window1->delegate());
ui::test::EventGenerator* generator = GetEventGenerator();
EXPECT_EQ(chromeos::OrientationType::kLandscapePrimary,
GetCurrentScreenOrientation());
gfx::Rect workarea_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window1.get());
// Snap the divider to one third position when there is only left window with
// minimum size larger than one third of the display's width. The divider
// should be snapped to the middle position after dragging.
ToggleOverview();
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
delegate1->set_minimum_size(
gfx::Size(workarea_bounds.width() * 0.4f, workarea_bounds.height()));
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false);
generator->set_current_screen_location(divider_bounds.CenterPoint());
generator->DragMouseTo(gfx::Point(workarea_bounds.width() * 0.33f, 0));
SkipDividerSnapAnimation();
EXPECT_GT(GetDividerPosition(), 0.33f * workarea_bounds.width());
EXPECT_LE(GetDividerPosition(), 0.5f * workarea_bounds.width());
// Snap the divider to two third position, it should be kept at there after
// dragging.
generator->set_current_screen_location(divider_bounds.CenterPoint());
generator->DragMouseTo(gfx::Point(workarea_bounds.width() * 0.67f, 0));
SkipDividerSnapAnimation();
EXPECT_GT(GetDividerPosition(), 0.5f * workarea_bounds.width());
EXPECT_LE(GetDividerPosition(), 0.67f * workarea_bounds.width());
EndSplitView();
// Snap the divider to two third position when there is only right window with
// minium size larger than one third of the display's width. The divider
// should be snapped to the middle position after dragging.
delegate1->set_minimum_size(
gfx::Size(workarea_bounds.width() * 0.4f, workarea_bounds.height()));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kSecondary);
divider_bounds = split_view_divider()->GetDividerBoundsInScreen(false);
generator->set_current_screen_location(divider_bounds.CenterPoint());
generator->DragMouseTo(gfx::Point(workarea_bounds.width() * 0.67f, 0));
SkipDividerSnapAnimation();
EXPECT_GT(GetDividerPosition(), 0.33f * workarea_bounds.width());
EXPECT_LE(GetDividerPosition(), 0.5f * workarea_bounds.width());
// Snap the divider to one third position, it should be kept at there after
// dragging.
generator->set_current_screen_location(divider_bounds.CenterPoint());
generator->DragMouseTo(gfx::Point(workarea_bounds.width() * 0.33f, 0));
SkipDividerSnapAnimation();
EXPECT_GT(GetDividerPosition(), 0);
EXPECT_LE(GetDividerPosition(), 0.33f * workarea_bounds.width());
EndSplitView();
// Snap the divider to one third position when there are both left and right
// snapped windows with the same minimum size larger than one third of the
// display's width. The divider should be snapped to the middle position after
// dragging.
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
aura::test::TestWindowDelegate* delegate2 =
static_cast<aura::test::TestWindowDelegate*>(window2->delegate());
delegate2->set_minimum_size(
gfx::Size(workarea_bounds.width() * 0.4f, workarea_bounds.height()));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
divider_bounds = split_view_divider()->GetDividerBoundsInScreen(false);
generator->set_current_screen_location(divider_bounds.CenterPoint());
generator->DragMouseTo(gfx::Point(workarea_bounds.width() * 0.33f, 0));
SkipDividerSnapAnimation();
EXPECT_GT(GetDividerPosition(), 0.33f * workarea_bounds.width());
EXPECT_LE(GetDividerPosition(), 0.5f * workarea_bounds.width());
// Snap the divider to two third position, it should be snapped to the middle
// position after dragging.
generator->set_current_screen_location(divider_bounds.CenterPoint());
generator->DragMouseTo(gfx::Point(workarea_bounds.width() * 0.67f, 0));
SkipDividerSnapAnimation();
EXPECT_GT(GetDividerPosition(), 0.33f * workarea_bounds.width());
EXPECT_LE(GetDividerPosition(), 0.5f * workarea_bounds.width());
EndSplitView();
}
// Tests that the divider and snapped windows bounds should be updated if
// snapping a new window with minimum size, which is larger than the bounds
// of its snap position.
TEST_F(SplitViewControllerTest,
DividerPositionWithWindowMinimumSizeOnSnapTest) {
const gfx::Rect bounds(0, 0, 200, 300);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
const gfx::Rect workarea_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window1.get());
// Divider should be moved to the middle at the beginning.
ToggleOverview();
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
ASSERT_TRUE(split_view_divider()->divider_widget());
EXPECT_GT(GetDividerPosition(), 0.33f * workarea_bounds.width());
EXPECT_LE(GetDividerPosition(), 0.5f * workarea_bounds.width());
// Drag the divider to two-third position.
ui::test::EventGenerator* generator = GetEventGenerator();
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false);
generator->set_current_screen_location(divider_bounds.CenterPoint());
generator->DragMouseTo(gfx::Point(workarea_bounds.width() * 0.67f, 0));
SkipDividerSnapAnimation();
EXPECT_GT(GetDividerPosition(), 0.5f * workarea_bounds.width());
EXPECT_LE(GetDividerPosition(), 0.67f * workarea_bounds.width());
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
aura::test::TestWindowDelegate* delegate2 =
static_cast<aura::test::TestWindowDelegate*>(window2->delegate());
delegate2->set_minimum_size(
gfx::Size(workarea_bounds.width() * 0.4f, workarea_bounds.height()));
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
EXPECT_GT(GetDividerPosition(), 0.33f * workarea_bounds.width());
EXPECT_LE(GetDividerPosition(), 0.5f * workarea_bounds.width());
}
// Test that if display configuration changes in lock screen, the split view
// mode doesn't end.
TEST_F(SplitViewControllerTest, DoNotEndSplitViewInLockScreen) {
display::test::DisplayManagerTestApi(display_manager())
.SetFirstDisplayAsInternalDisplay();
UpdateDisplay("800x400");
const gfx::Rect bounds(0, 0, 200, 300);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
// Now lock the screen.
GetSessionControllerClient()->LockScreen();
// Change display configuration. Split view mode is still active.
UpdateDisplay("400x800");
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
// Now unlock the screen.
GetSessionControllerClient()->UnlockScreen();
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
}
// Test that when split view and overview are both active when a new window is
// added to the window hierarchy, overview is not ended.
TEST_F(SplitViewControllerTest, NewWindowTest) {
const gfx::Rect bounds(0, 0, 200, 300);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
ToggleOverview();
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kPrimarySnapped);
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
// Now new a window. Test it won't end the overview mode
std::unique_ptr<aura::Window> window3(CreateWindow(bounds));
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
}
// Tests that when split view ends because of a transition from tablet mode to
// laptop mode during a resize operation, drags are properly completed.
TEST_F(SplitViewControllerTest, ExitTabletModeDuringResizeCompletesDrags) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
auto* w1_state = WindowState::Get(window1.get());
auto* w2_state = WindowState::Get(window2.get());
// Setup delegates
auto* window_state_delegate1 = new FakeWindowStateDelegate();
auto* window_state_delegate2 = new FakeWindowStateDelegate();
w1_state->SetDelegate(base::WrapUnique(window_state_delegate1));
w2_state->SetDelegate(base::WrapUnique(window_state_delegate2));
// Set up windows.
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
// Start a drag but don't release the mouse button.
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
const int screen_width =
screen_util::GetDisplayWorkAreaBoundsInParent(window1.get()).width();
GetEventGenerator()->set_current_screen_location(
divider_bounds.CenterPoint());
GetEventGenerator()->PressLeftButton();
GetEventGenerator()->MoveMouseTo(screen_width * 0.67f, 0);
// Drag is started for both windows.
EXPECT_TRUE(window_state_delegate1->drag_in_progress());
EXPECT_TRUE(window_state_delegate2->drag_in_progress());
EXPECT_NE(nullptr, w1_state->drag_details());
EXPECT_NE(nullptr, w2_state->drag_details());
// End tablet mode.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
// Drag is ended for both windows.
EXPECT_EQ(nullptr, w1_state->drag_details());
EXPECT_EQ(nullptr, w2_state->drag_details());
EXPECT_FALSE(window_state_delegate1->drag_in_progress());
EXPECT_FALSE(window_state_delegate2->drag_in_progress());
}
// Tests that when a single window is present in split view mode is minimized
// during a resize operation, then drags are properly completed.
TEST_F(SplitViewControllerTest,
MinimizeSingleWindowDuringResizeCompletesDrags) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
auto* w1_state = WindowState::Get(window1.get());
// Setup delegate
auto* window_state_delegate1 = new FakeWindowStateDelegate();
w1_state->SetDelegate(base::WrapUnique(window_state_delegate1));
// Set up window.
ToggleOverview();
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
// Start a drag but don't release the mouse button.
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
const int screen_width =
screen_util::GetDisplayWorkAreaBoundsInParentForActiveDeskContainer(
window1.get())
.width();
GetEventGenerator()->set_current_screen_location(
divider_bounds.CenterPoint());
GetEventGenerator()->PressLeftButton();
GetEventGenerator()->MoveMouseTo(screen_width * 0.67f, 0);
// Drag is started.
EXPECT_TRUE(window_state_delegate1->drag_in_progress());
EXPECT_NE(nullptr, w1_state->drag_details());
// Minimize the window.
WMEvent minimize_event(WM_EVENT_MINIMIZE);
WindowState::Get(window1.get())->OnWMEvent(&minimize_event);
// Drag is ended.
EXPECT_FALSE(window_state_delegate1->drag_in_progress());
EXPECT_EQ(nullptr, w1_state->drag_details());
}
// Tests that when two windows are present in split view mode and one of them
// is minimized during a resize, then drags are properly completed.
TEST_F(SplitViewControllerTest,
MinimizeOneOfTwoWindowsDuringResizeCompletesDrags) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
auto* w1_state = WindowState::Get(window1.get());
auto* w2_state = WindowState::Get(window2.get());
// Setup delegates
auto* window_state_delegate1 = new FakeWindowStateDelegate();
auto* window_state_delegate2 = new FakeWindowStateDelegate();
w1_state->SetDelegate(base::WrapUnique(window_state_delegate1));
w2_state->SetDelegate(base::WrapUnique(window_state_delegate2));
// Set up windows.
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
// Start a drag but don't release the mouse button.
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
const int screen_width =
screen_util::GetDisplayWorkAreaBoundsInParent(window1.get()).width();
GetEventGenerator()->set_current_screen_location(
divider_bounds.CenterPoint());
GetEventGenerator()->PressLeftButton();
GetEventGenerator()->MoveMouseTo(screen_width * 0.67f, 0);
// Drag is started for both windows.
EXPECT_TRUE(window_state_delegate1->drag_in_progress());
EXPECT_TRUE(window_state_delegate2->drag_in_progress());
EXPECT_NE(nullptr, w1_state->drag_details());
EXPECT_NE(nullptr, w2_state->drag_details());
// Minimize the left window.
WMEvent minimize_event(WM_EVENT_MINIMIZE);
WindowState::Get(window1.get())->OnWMEvent(&minimize_event);
// Drag is ended as the window is detached from splitview.
EXPECT_FALSE(window_state_delegate1->drag_in_progress());
EXPECT_FALSE(window_state_delegate2->drag_in_progress());
EXPECT_EQ(nullptr, w1_state->drag_details());
EXPECT_EQ(nullptr, w2_state->drag_details());
}
// Test that when a snapped window's resizablity property change from resizable
// to unresizable, the split view mode is ended.
TEST_F(SplitViewControllerTest, ResizabilityChangeTest) {
const gfx::Rect bounds(0, 0, 200, 300);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
window1->SetProperty(aura::client::kResizeBehaviorKey,
aura::client::kResizeBehaviorNone);
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
}
// Tests that shadows on windows disappear when the window is snapped, and
// reappear when unsnapped.
TEST_F(SplitViewControllerTest, ShadowDisappearsWhenSnapped) {
const gfx::Rect bounds(200, 200);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
std::unique_ptr<aura::Window> window3(CreateWindow(bounds));
::wm::ShadowController* shadow_controller = Shell::Get()->shadow_controller();
EXPECT_TRUE(shadow_controller->IsShadowVisibleForWindow(window1.get()));
EXPECT_TRUE(shadow_controller->IsShadowVisibleForWindow(window2.get()));
EXPECT_TRUE(shadow_controller->IsShadowVisibleForWindow(window3.get()));
// Snap |window1| to the left. Its shadow should disappear.
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_FALSE(shadow_controller->IsShadowVisibleForWindow(window1.get()));
auto* overview_controller = OverviewController::Get();
EXPECT_TRUE(overview_controller->InOverviewSession());
auto* overview_session = overview_controller->overview_session();
EXPECT_TRUE(overview_session->IsWindowInOverview(window2.get()));
EXPECT_TRUE(overview_session->IsWindowInOverview(window3.get()));
EXPECT_FALSE(shadow_controller->IsShadowVisibleForWindow(window2.get()));
EXPECT_FALSE(shadow_controller->IsShadowVisibleForWindow(window3.get()));
// Snap |window2| to the right. Its shadow should also disappear.
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
EXPECT_FALSE(shadow_controller->IsShadowVisibleForWindow(window1.get()));
EXPECT_FALSE(shadow_controller->IsShadowVisibleForWindow(window2.get()));
EXPECT_TRUE(shadow_controller->IsShadowVisibleForWindow(window3.get()));
// Snap |window3| to the right. Its shadow should disappear and |window2|'s
// shadow should reappear.
split_view_controller()->SnapWindow(window3.get(), SnapPosition::kSecondary);
EXPECT_FALSE(shadow_controller->IsShadowVisibleForWindow(window1.get()));
EXPECT_TRUE(shadow_controller->IsShadowVisibleForWindow(window2.get()));
EXPECT_FALSE(shadow_controller->IsShadowVisibleForWindow(window3.get()));
}
// Tests that if snapping a window causes overview to end (e.g., select two
// windows in overview mode to snap to both side of the screen), or toggle
// overview to end overview causes a window to snap, we should not have the
// exiting animation.
// TODO(b/315345858): Fix flakiness and re-enable.
TEST_F(SplitViewControllerTest, DISABLED_OverviewExitAnimationTest) {
ui::ScopedAnimationDurationScaleMode anmatin_scale(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
std::unique_ptr<aura::Window> window3(CreateWindow(bounds));
// 1) For normal toggle overview case, we should have animation when
// exiting overview.
std::unique_ptr<OverviewStatesObserver> overview_observer =
std::make_unique<OverviewStatesObserver>(window1->GetRootWindow());
ToggleOverview();
WaitForOverviewEnterAnimation();
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
ToggleOverview();
WaitForOverviewExitAnimation();
EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
EXPECT_TRUE(overview_observer->overview_animate_when_exiting());
CheckOverviewEnterExitHistogram("NormalEnterExit", {1, 0}, {1, 0});
// 2) If overview is ended because of activating a window:
ToggleOverview();
WaitForOverviewEnterAnimation();
// It will end overview.
wm::ActivateWindow(window1.get());
WaitForOverviewExitAnimation();
EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
EXPECT_TRUE(overview_observer->overview_animate_when_exiting());
CheckOverviewEnterExitHistogram("EnterExitByActivation", {2, 0}, {2, 0});
// 3) If overview is ended because of snapping a window:
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
WaitForOverviewEnterAnimation();
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
// Reset the observer as we'll need the OverviewStatesObserver to be added to
// to ShellObserver list after SplitViewController.
overview_observer =
std::make_unique<OverviewStatesObserver>(window1->GetRootWindow());
// Test |overview_animate_when_exiting_| has been properly reset.
EXPECT_TRUE(overview_observer->overview_animate_when_exiting());
CheckOverviewEnterExitHistogram("EnterInSplitView", {2, 1}, {2, 0});
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
WaitForOverviewExitAnimation();
EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
EXPECT_FALSE(overview_observer->overview_animate_when_exiting());
CheckOverviewEnterExitHistogram("ExitBySnap", {2, 1}, {2, 1});
// 4) If ending overview causes a window to snap:
ToggleOverview();
WaitForOverviewEnterAnimation();
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
// Test |overview_animate_when_exiting_| has been properly reset.
EXPECT_TRUE(overview_observer->overview_animate_when_exiting());
CheckOverviewEnterExitHistogram("EnterInSplitView2", {2, 2}, {2, 1});
ToggleOverview();
WaitForOverviewExitAnimation();
EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
EXPECT_FALSE(overview_observer->overview_animate_when_exiting());
CheckOverviewEnterExitHistogram("ExitInSplitView", {2, 2}, {2, 2});
}
// Test the window state is normally maximized on splitview end, except when we
// end it from home launcher.
TEST_F(SplitViewControllerTest, WindowStateOnExit) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
using svc = SnapPosition;
// Tests that normally, window will maximize on splitview ended.
split_view_controller()->SnapWindow(window1.get(), svc::kPrimary);
split_view_controller()->SnapWindow(window2.get(), svc::kSecondary);
split_view_controller()->EndSplitView();
EXPECT_TRUE(WindowState::Get(window1.get())->IsMaximized());
EXPECT_TRUE(WindowState::Get(window2.get())->IsMaximized());
// Tests that if we end splitview from home launcher, the windows do not get
// maximized.
split_view_controller()->SnapWindow(window1.get(), svc::kPrimary);
split_view_controller()->SnapWindow(window2.get(), svc::kSecondary);
split_view_controller()->EndSplitView(
SplitViewController::EndReason::kHomeLauncherPressed);
EXPECT_FALSE(WindowState::Get(window1.get())->IsMaximized());
EXPECT_FALSE(WindowState::Get(window2.get())->IsMaximized());
}
// Test that if overview and splitview are both active at the same time,
// activiate an unsnappable window should end both overview and splitview mode.
TEST_F(SplitViewControllerTest, ActivateNonSnappableWindow) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
std::unique_ptr<aura::Window> window3(CreateNonSnappableWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
wm::ActivateWindow(window3.get());
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
}
// Tests that if a snapped window has a bubble transient child, the bubble's
// bounds should always align with the snapped window's bounds.
TEST_F(SplitViewControllerTest, AdjustTransientChildBounds) {
std::unique_ptr<views::Widget> widget(
CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET));
aura::Window* window = widget->GetNativeWindow();
window->SetProperty(aura::client::kResizeBehaviorKey,
aura::client::kResizeBehaviorCanResize |
aura::client::kResizeBehaviorCanMaximize);
split_view_controller()->SnapWindow(window, SnapPosition::kPrimary);
const gfx::Rect window_bounds = window->GetBoundsInScreen();
// Create a bubble widget that's anchored to |widget|.
views::Widget* bubble_widget = views::BubbleDialogDelegateView::CreateBubble(
new TestBubbleDialogDelegateView(widget->GetContentsView()));
aura::Window* bubble_window = bubble_widget->GetNativeWindow();
EXPECT_TRUE(::wm::HasTransientAncestor(bubble_window, window));
// Test that the bubble is created inside its anchor widget.
EXPECT_TRUE(window_bounds.Contains(bubble_window->GetBoundsInScreen()));
// Now try to manually move the bubble out of the snapped window.
bubble_window->SetBoundsInScreen(
split_view_controller()->GetSnappedWindowBoundsInScreen(
SnapPosition::kSecondary, window, chromeos::kDefaultSnapRatio,
/*account_for_divider_width=*/true),
display::Screen::GetScreen()->GetDisplayNearestWindow(window));
// Test that the bubble can't be moved outside of its anchor widget.
EXPECT_TRUE(window_bounds.Contains(bubble_window->GetBoundsInScreen()));
EndSplitView();
}
// Tests the divider closest position ratio if work area is not starts from the
// top of the display.
TEST_F(SplitViewControllerTest, DividerClosestRatioOnWorkArea) {
UpdateDisplay("1200x800");
// Docked magnifier will put a view port window on the top of the display.
Shell::Get()->docked_magnifier_controller()->SetEnabled(true);
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
ScreenOrientationControllerTestApi test_api(
Shell::Get()->screen_orientation_controller());
ui::test::EventGenerator* generator = GetEventGenerator();
ASSERT_EQ(chromeos::OrientationType::kLandscapePrimary,
test_api.GetCurrentOrientation());
const gfx::Rect bounds(0, 0, 200, 200);
std::unique_ptr<aura::Window> window(CreateWindow(bounds));
ToggleOverview();
split_view_controller()->SnapWindow(window.get(), SnapPosition::kPrimary);
test_api.SetDisplayRotation(display::Display::ROTATE_90,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(chromeos::OrientationType::kPortraitSecondary,
test_api.GetCurrentOrientation());
EXPECT_EQ(divider_closest_ratio(), chromeos::kDefaultSnapRatio);
test_api.SetDisplayRotation(display::Display::ROTATE_0,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(chromeos::OrientationType::kLandscapePrimary,
test_api.GetCurrentOrientation());
EXPECT_EQ(divider_closest_ratio(), chromeos::kDefaultSnapRatio);
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false);
gfx::Rect workarea_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window.get());
generator->set_current_screen_location(divider_bounds.CenterPoint());
// Drag the divider to one third position of the work area's width.
generator->DragMouseTo(
gfx::Point(workarea_bounds.width() * chromeos::kOneThirdSnapRatio,
workarea_bounds.y()));
SkipDividerSnapAnimation();
EXPECT_EQ(divider_closest_ratio(), chromeos::kOneThirdSnapRatio);
// Divider closest position ratio changed from one third to two thirds if
// left/top window changes.
test_api.SetDisplayRotation(display::Display::ROTATE_90,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(chromeos::OrientationType::kPortraitSecondary,
test_api.GetCurrentOrientation());
EXPECT_EQ(divider_closest_ratio(), chromeos::kTwoThirdSnapRatio);
// Divider closest position ratio is kept as one third if left/top window
// doesn't changes.
test_api.SetDisplayRotation(display::Display::ROTATE_270,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(chromeos::OrientationType::kPortraitPrimary,
test_api.GetCurrentOrientation());
EXPECT_EQ(divider_closest_ratio(), chromeos::kOneThirdSnapRatio);
}
// Tests that the divider closest position ratio is properly updated for display
// rotation after a clamshell/tablet transition that does not trigger a call to
// |SplitViewController::OnDisplayMetricsChanged|. The point here is that if
// |SplitViewController::is_previous_layout_right_side_up_| is only ever updated
// in |SplitViewController::OnDisplayMetricsChanged|, then a clamshell/tablet
// transition can leave it with a stale value which can cause broken behavior.
TEST_F(SplitViewControllerTest,
DividerClosestRatioUpdatedForClamshellTabletTransition) {
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
// Set the display orientation to landscape secondary (upside down).
ScreenOrientationControllerTestApi test_api(
Shell::Get()->screen_orientation_controller());
test_api.SetDisplayRotation(display::Display::ROTATE_180,
display::Display::RotationSource::ACTIVE);
// Switch to clamshell mode.
TabletModeController* tablet_mode_controller =
Shell::Get()->tablet_mode_controller();
tablet_mode_controller->SetEnabledForTest(false);
// Set the display orientation to landscape secondary (upside down).
Shell::Get()->display_manager()->SetDisplayRotation(
display_id, display::Display::ROTATE_180,
display::Display::RotationSource::ACTIVE);
// Switch to tablet mode.
tablet_mode_controller->SetEnabledForTest(true);
// Enter split view.
const gfx::Rect bounds(0, 0, 200, 200);
std::unique_ptr<aura::Window> window(CreateWindow(bounds));
ToggleOverview();
split_view_controller()->SnapWindow(window.get(), SnapPosition::kPrimary);
// Drag the divider so that the snapped window spans only one third of the way
// across the work area.
ui::test::EventGenerator* generator = GetEventGenerator();
const gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false);
generator->set_current_screen_location(divider_bounds.CenterPoint());
const gfx::Rect workarea_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window.get());
generator->DragMouseTo(
gfx::Point(workarea_bounds.width() * chromeos::kOneThirdSnapRatio,
workarea_bounds.y()));
SkipDividerSnapAnimation();
// Expect that the divider closest position ratio is two thirds with the
// display upside down.
EXPECT_EQ(divider_closest_ratio(), chromeos::kTwoThirdSnapRatio);
// Set the display orientation to landscape primary (right side up).
test_api.SetDisplayRotation(display::Display::ROTATE_0,
display::Display::RotationSource::ACTIVE);
// Expect that the divider closest position ratio is updated to one third.
EXPECT_EQ(divider_closest_ratio(), chromeos::kOneThirdSnapRatio);
}
// Test that pinning a window ends split view mode.
TEST_F(SplitViewControllerTest, PinningWindowEndsSplitView) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
window_util::PinWindow(window1.get(), true);
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
}
// Test that split view mode is disallowed while we're in pinned mode (there is
// a pinned window).
TEST_F(SplitViewControllerTest, PinnedWindowDisallowsSplitView) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
EXPECT_TRUE(ShouldAllowSplitView());
window_util::PinWindow(window1.get(), true);
EXPECT_FALSE(ShouldAllowSplitView());
}
// Test that if split view ends while the divider is dragged to where a snapped
// window is sliding off the screen because it has reached minimum size, then
// the offset is cleared.
TEST_F(SplitViewControllerTest, EndSplitViewWhileResizingBeyondMinimum) {
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
ScreenOrientationControllerTestApi test_api(
Shell::Get()->screen_orientation_controller());
const gfx::Rect bounds(0, 0, 300, 200);
std::unique_ptr<aura::Window> window(CreateWindow(bounds));
aura::test::TestWindowDelegate* delegate =
static_cast<aura::test::TestWindowDelegate*>(window->delegate());
// Set the screen orientation to LANDSCAPE_PRIMARY
test_api.SetDisplayRotation(display::Display::ROTATE_0,
display::Display::RotationSource::ACTIVE);
gfx::Rect display_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window.get());
ToggleOverview();
split_view_controller()->SnapWindow(window.get(), SnapPosition::kPrimary);
delegate->set_minimum_size(
gfx::Size(display_bounds.width() * 0.4f, display_bounds.height()));
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false);
split_view_divider()->StartResizeWithDivider(divider_bounds.CenterPoint());
gfx::Point resize_point(display_bounds.width() * 0.33f, 0);
split_view_divider()->ResizeWithDivider(resize_point);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.TabletMode.SingleWindow", 1);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.SingleWindow",
0);
ASSERT_FALSE(window->layer()->GetTargetTransform().IsIdentity());
EndSplitView();
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.TabletMode.SingleWindow", 1);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.SingleWindow",
1);
EXPECT_TRUE(window->layer()->GetTargetTransform().IsIdentity());
}
// Test if presentation time is recorded for multi window resizing and resizing
// with overview.
TEST_F(SplitViewControllerTest, ResizeTwoWindows) {
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
ScreenOrientationControllerTestApi test_api(
Shell::Get()->screen_orientation_controller());
const gfx::Rect bounds(0, 0, 300, 200);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
gfx::Rect display_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window1.get());
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false);
split_view_divider()->StartResizeWithDivider(divider_bounds.CenterPoint());
gfx::Point resize_point(display_bounds.width() * chromeos::kOneThirdSnapRatio,
0);
split_view_divider()->ResizeWithDivider(resize_point);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.TabletMode.MultiWindow", 1);
split_view_divider()->ResizeWithDivider(
gfx::Point(resize_point.x(), resize_point.y() + 1));
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.TabletMode.MultiWindow", 2);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.MultiWindow",
0);
split_view_divider()->EndResizeWithDivider(resize_point);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.TabletMode.MultiWindow", 2);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.MultiWindow",
1);
ToggleOverview();
split_view_divider()->StartResizeWithDivider(divider_bounds.CenterPoint());
split_view_divider()->ResizeWithDivider(resize_point);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.TabletMode.WithOverview", 1);
split_view_divider()->ResizeWithDivider(
gfx::Point(resize_point.x(), resize_point.y() + 1));
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.TabletMode.WithOverview", 2);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.WithOverview",
0);
split_view_divider()->EndResizeWithDivider(resize_point);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.TabletMode.WithOverview", 2);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.WithOverview",
1);
}
// Test that if split view ends during the divider snap animation while a
// snapped window is sliding off the screen because it has reached minimum size,
// then the animation is ended and the window offset is cleared.
TEST_F(SplitViewControllerTest, EndSplitViewDuringDividerSnapAnimation) {
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
ScreenOrientationControllerTestApi test_api(
Shell::Get()->screen_orientation_controller());
const gfx::Rect bounds(0, 0, 300, 200);
std::unique_ptr<aura::Window> window(CreateWindow(bounds));
aura::test::TestWindowDelegate* delegate =
static_cast<aura::test::TestWindowDelegate*>(window->delegate());
// Set the screen orientation to LANDSCAPE_PRIMARY
test_api.SetDisplayRotation(display::Display::ROTATE_0,
display::Display::RotationSource::ACTIVE);
gfx::Rect display_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window.get());
ToggleOverview();
split_view_controller()->SnapWindow(window.get(), SnapPosition::kPrimary);
delegate->set_minimum_size(
gfx::Size(display_bounds.width() * 0.4f, display_bounds.height()));
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false);
split_view_divider()->StartResizeWithDivider(divider_bounds.CenterPoint());
gfx::Point resize_point((int)(display_bounds.width() * 0.33f) + 20, 0);
split_view_divider()->ResizeWithDivider(resize_point);
split_view_divider()->EndResizeWithDivider(resize_point);
ASSERT_TRUE(IsDividerAnimating());
ASSERT_FALSE(window->layer()->GetTargetTransform().IsIdentity());
EndSplitView();
EXPECT_FALSE(IsDividerAnimating());
EXPECT_TRUE(window->layer()->GetTargetTransform().IsIdentity());
}
// Test `OverviewObserver` which tracks how many overview items there are when
// overview mode is about to end.
class TestOverviewItemsOnOverviewModeEndObserver : public OverviewObserver {
public:
TestOverviewItemsOnOverviewModeEndObserver() {
OverviewController::Get()->AddObserver(this);
}
TestOverviewItemsOnOverviewModeEndObserver(
const TestOverviewItemsOnOverviewModeEndObserver&) = delete;
TestOverviewItemsOnOverviewModeEndObserver& operator=(
const TestOverviewItemsOnOverviewModeEndObserver&) = delete;
~TestOverviewItemsOnOverviewModeEndObserver() override {
OverviewController::Get()->RemoveObserver(this);
}
size_t items_on_last_overview_end() const {
return items_on_last_overview_end_;
}
void OnOverviewModeEnding(OverviewSession* overview_session) override {
items_on_last_overview_end_ = overview_session->GetNumWindows();
}
private:
size_t items_on_last_overview_end_ = 0;
};
TEST_F(SplitViewControllerTest, ItemsRemovedFromOverviewOnSnap) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
ToggleOverview();
ASSERT_EQ(2u, OverviewController::Get()->overview_session()->GetNumWindows());
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
ASSERT_TRUE(OverviewController::Get()->InOverviewSession());
EXPECT_EQ(1u, OverviewController::Get()->overview_session()->GetNumWindows());
// Create |observer| after splitview is entered so that it gets notified after
// splitview does, and so will notice the changes splitview made to overview
// on overview end.
TestOverviewItemsOnOverviewModeEndObserver observer;
ToggleOverview();
EXPECT_EQ(0u, observer.items_on_last_overview_end());
}
// Test that resizing ends properly if split view ends during divider dragging.
TEST_F(SplitViewControllerTest, EndSplitViewWhileDragging) {
// Enter split view mode.
std::unique_ptr<aura::Window> window = CreateTestWindow();
ToggleOverview();
split_view_controller()->SnapWindow(window.get(), SnapPosition::kPrimary);
// Start resizing.
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false);
split_view_divider()->StartResizeWithDivider(divider_bounds.CenterPoint());
// Verify the setup.
ASSERT_TRUE(split_view_controller()->InSplitViewMode());
ASSERT_TRUE(split_view_controller()->IsResizingWithDivider());
gfx::Point resize_point(divider_bounds.CenterPoint());
resize_point.Offset(100, 0);
split_view_divider()->ResizeWithDivider(resize_point);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.TabletMode.SingleWindow", 1);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.SingleWindow",
0);
// End split view and check that resizing has ended properly.
split_view_controller()->EndSplitView();
EXPECT_FALSE(split_view_controller()->IsResizingWithDivider());
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.TabletMode.SingleWindow", 1);
histograms().ExpectTotalCount(
"Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.SingleWindow",
1);
}
// Tests that auto snapping is properly triggered if a window is going to
// unminimized (visible but minimized) in tablet split view mode.
TEST_F(SplitViewControllerTest, AutoSnapFromMinimizedState) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateNonSnappableWindow(bounds));
std::unique_ptr<aura::Window> window3(CreateWindow(bounds));
// Nothing should happen in clamshell mode.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
WindowState::Get(window1.get())->Minimize();
window1->Show();
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
EXPECT_FALSE(split_view_controller()->IsWindowInSplitView(window1.get()));
// Nothing should happen not in tablet split view mode.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
WindowState::Get(window1.get())->Minimize();
window1->Show();
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
EXPECT_FALSE(split_view_controller()->IsWindowInSplitView(window1.get()));
// Nothing should happen for a non-snappable window.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
WindowState::Get(window2.get())->Minimize();
window2->Show();
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
EXPECT_FALSE(split_view_controller()->IsWindowInSplitView(window2.get()));
// Nothing should happen for transient visibility changing due to dragging.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
split_view_controller()->SnapWindow(window3.get(), SnapPosition::kPrimary);
EXPECT_TRUE(split_view_controller()->InTabletSplitViewMode());
EXPECT_TRUE(split_view_controller()->IsWindowInSplitView(window3.get()));
EXPECT_EQ(split_view_controller()->GetPositionOfSnappedWindow(window3.get()),
SnapPosition::kPrimary);
WindowState::Get(window1.get())->Minimize();
window1->SetProperty(kHideDuringWindowDragging, true);
window1->Show();
EXPECT_FALSE(split_view_controller()->IsWindowInSplitView(window1.get()));
window1->ClearProperty(kHideDuringWindowDragging);
// Should performs auto snapping when showing a snappable window in table
// split view mode.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
split_view_controller()->SnapWindow(window3.get(), SnapPosition::kPrimary);
EXPECT_TRUE(split_view_controller()->InTabletSplitViewMode());
EXPECT_TRUE(split_view_controller()->IsWindowInSplitView(window3.get()));
EXPECT_EQ(split_view_controller()->GetPositionOfSnappedWindow(window3.get()),
SnapPosition::kPrimary);
WindowState::Get(window1.get())->Minimize();
window1->Show();
EXPECT_TRUE(split_view_controller()->InTabletSplitViewMode());
EXPECT_TRUE(split_view_controller()->IsWindowInSplitView(window1.get()));
EXPECT_EQ(split_view_controller()->GetPositionOfSnappedWindow(window1.get()),
SnapPosition::kSecondary);
EndSplitView();
}
// Test that if the transient parent window is no longer snapped in split view,
// split view divider should no longer observe the transient child window.
TEST_F(SplitViewControllerTest, DoNotObserveTransientIfNotInSplitview) {
// Create two normal window.
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
// Add another two windows with one being a bubble transient child of the
// other.
std::unique_ptr<views::Widget> widget(
CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET));
aura::Window* parent = widget->GetNativeWindow();
parent->SetProperty(aura::client::kResizeBehaviorKey,
aura::client::kResizeBehaviorCanResize |
aura::client::kResizeBehaviorCanMaximize);
views::Widget* bubble_widget = views::BubbleDialogDelegateView::CreateBubble(
new TestBubbleDialogDelegateView(widget->GetContentsView()));
aura::Window* bubble_transient = bubble_widget->GetNativeWindow();
EXPECT_TRUE(::wm::HasTransientAncestor(bubble_transient, parent));
ToggleOverview();
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(parent, SnapPosition::kSecondary);
EXPECT_TRUE(bubble_transient->HasObserver(split_view_divider()));
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
EXPECT_FALSE(bubble_transient->HasObserver(split_view_divider()));
}
// Test that if a snapped window is destroyed during resizing, we should end
// resizing.
TEST_F(SplitViewControllerTest, WindowDestroyedDuringResize) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false);
split_view_divider()->StartResizeWithDivider(divider_bounds.CenterPoint());
split_view_divider()->ResizeWithDivider(gfx::Point(100, 100));
window1.reset();
EXPECT_FALSE(split_view_controller()->IsResizingWithDivider());
}
TEST_F(SplitViewControllerTest, WMSnapEvent) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
// Test the functionalities in tablet mode.
// Sending `WM_EVENT_SNAP_PRIMARY` to snap `window1` on the primary snapped
// position.
WindowSnapWMEvent wm_left_snap_event(WM_EVENT_SNAP_PRIMARY);
WindowState::Get(window1.get())->OnWMEvent(&wm_left_snap_event);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(split_view_controller()->primary_window(), window1.get());
EXPECT_FALSE(split_view_controller()->IsWindowInSplitView(window2.get()));
OverviewController* overview_controller = OverviewController::Get();
EXPECT_TRUE(overview_controller->InOverviewSession());
OverviewSession* overview_session = overview_controller->overview_session();
EXPECT_TRUE(overview_session->IsWindowInOverview(window2.get()));
// Sending `WM_EVENT_SNAP_SECONDARY` to snap `window1` on the secondary
// snapped position.
WindowSnapWMEvent wm_right_snap_event(WM_EVENT_SNAP_SECONDARY);
WindowState::Get(window1.get())->OnWMEvent(&wm_right_snap_event);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(split_view_controller()->secondary_window(), window1.get());
EXPECT_FALSE(split_view_controller()->IsWindowInSplitView(window2.get()));
EXPECT_TRUE(overview_controller->InOverviewSession());
EXPECT_TRUE(overview_session->IsWindowInOverview(window2.get()));
// Sending WM_EVENT_SNAP_SECONDARY to |window2| will replace |window1|.
WindowState::Get(window2.get())->OnWMEvent(&wm_right_snap_event);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(split_view_controller()->secondary_window(), window2.get());
EXPECT_FALSE(split_view_controller()->IsWindowInSplitView(window1.get()));
EXPECT_TRUE(overview_controller->InOverviewSession());
EXPECT_TRUE(overview_session->IsWindowInOverview(window1.get()));
// Sending WM_EVENT_SNAP_PRIMARY to |window1| to snap |window1|.
WindowState::Get(window1.get())->OnWMEvent(&wm_left_snap_event);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(split_view_controller()->primary_window(), window1.get());
EXPECT_EQ(split_view_controller()->secondary_window(), window2.get());
EXPECT_FALSE(overview_controller->InOverviewSession());
// Sending WM_EVENT_SNAP_SECONDARY to |window1| will replace |window2| and put
// |window2| in overview.
WindowState::Get(window1.get())->OnWMEvent(&wm_right_snap_event);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(split_view_controller()->secondary_window(), window1.get());
EXPECT_FALSE(split_view_controller()->IsWindowInSplitView(window2.get()));
EXPECT_TRUE(overview_controller->InOverviewSession());
overview_session = overview_controller->overview_session();
EXPECT_TRUE(overview_session->IsWindowInOverview(window2.get()));
ToggleOverview();
EndSplitView();
// Test the functionalities in clamshell mode.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
// Sending `WM_EVENT_SNAP_PRIMARY` to `window1` will snap to left.
WindowState::Get(window1.get())->OnWMEvent(&wm_left_snap_event);
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
EXPECT_FALSE(overview_controller->InOverviewSession());
ToggleOverview();
// Sending WM_EVENT_SNAP_PRIMARY to |window1| to snap to left while overview
// is active will put |window1| in splitview and |window2| in overview.
WindowState::Get(window1.get())->OnWMEvent(&wm_left_snap_event);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_TRUE(overview_controller->InOverviewSession());
overview_session = overview_controller->overview_session();
EXPECT_TRUE(overview_session->IsWindowInOverview(window2.get()));
// Sending WM_EVENT_SNAP_SECONDARY to |window1| to snap to right while
// overview is active will put |window1| to snap to the right in splitview and
// |window2| remains in overview.
WindowState::Get(window1.get())->OnWMEvent(&wm_right_snap_event);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
EXPECT_EQ(split_view_controller()->secondary_window(), window1.get());
EXPECT_FALSE(split_view_controller()->IsWindowInSplitView(window2.get()));
EXPECT_TRUE(overview_controller->InOverviewSession());
EXPECT_TRUE(overview_session->IsWindowInOverview(window2.get()));
}
// Tests that the split view divider observers the snapped windows when the
// tablet mode split view starts.
TEST_F(SplitViewControllerTest, SplitViewDividerObserveSnappedWindow) {
auto* tablet_mode_controller = Shell::Get()->tablet_mode_controller();
// Exit tablet mode.
tablet_mode_controller->SetEnabledForTest(false);
EXPECT_FALSE(display::Screen::GetScreen()->InTabletMode());
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> left_window(CreateWindow(bounds));
std::unique_ptr<aura::Window> right_window(CreateWindow(bounds));
// Snap the left and right window.
split_view_controller()->SnapWindow(left_window.get(),
SnapPosition::kPrimary);
split_view_controller()->SnapWindow(right_window.get(),
SnapPosition::kSecondary);
// Entering tablet mode will start tablet mode split view and the split view
// divider will be created.
tablet_mode_controller->SetEnabledForTest(true);
EXPECT_TRUE(display::Screen::GetScreen()->InTabletMode());
EXPECT_TRUE(split_view_controller()->InTabletSplitViewMode());
EXPECT_TRUE(split_view_divider()->divider_widget());
// The left and right windows are observed by split view divider->
aura::Window::Windows observed_windows =
split_view_divider()->observed_windows();
EXPECT_TRUE(base::Contains(observed_windows, left_window.get()));
EXPECT_TRUE(base::Contains(observed_windows, right_window.get()));
}
// Tests that the bounds of the window and divider get updated correctly when
// snapping with different ratios.
TEST_F(SplitViewControllerTest, SnapBetweenDifferentRatios) {
std::unique_ptr<aura::Window> window1 = CreateTestWindow();
std::unique_ptr<aura::Window> window2 = CreateTestWindow();
// Snap `window1` to primary position and `window2` to secondary position,
// both with default snap ratios.
WindowSnapWMEvent snap_primary_default(WM_EVENT_SNAP_PRIMARY);
WindowState::Get(window1.get())->OnWMEvent(&snap_primary_default);
WindowSnapWMEvent snap_secondary_default(WM_EVENT_SNAP_SECONDARY);
WindowState::Get(window2.get())->OnWMEvent(&snap_secondary_default);
// Test that both window bounds are at half the work area width and that the
// divider is positioned at half of the work area width minus the
// `divider_delta`.
const gfx::Rect work_area_bounds =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
int divider_origin_x = split_view_divider()
->GetDividerBoundsInScreen(
/*is_dragging=*/false)
.x();
const int divider_delta = kSplitviewDividerShortSideLength / 2;
EXPECT_EQ(
divider_origin_x,
work_area_bounds.width() * chromeos::kDefaultSnapRatio - divider_delta);
EXPECT_EQ(work_area_bounds.width() * chromeos::kDefaultSnapRatio,
window1->bounds().width() + divider_delta);
EXPECT_EQ(work_area_bounds.width() * chromeos::kDefaultSnapRatio,
window2->bounds().width() + divider_delta);
// Snap `window1`, still in primary position, but with two thirds snap ratio.
WindowSnapWMEvent snap_primary_two_third(WM_EVENT_SNAP_PRIMARY,
chromeos::kTwoThirdSnapRatio);
WindowState::Get(window1.get())->OnWMEvent(&snap_primary_two_third);
// Wait until the divider animation completes.
base::RunLoop().RunUntilIdle();
// Test that the window bounds have updated to two thirds and one third of the
// work area width respectively. The the divider is positioned at two thirds
// of the work area width minus the `divider_delta`.
divider_origin_x = split_view_divider()
->GetDividerBoundsInScreen(
/*is_dragging=*/false)
.x();
EXPECT_EQ(divider_origin_x, std::round(work_area_bounds.width() *
chromeos::kTwoThirdSnapRatio) -
divider_delta);
EXPECT_EQ(std::round(work_area_bounds.width() * chromeos::kTwoThirdSnapRatio),
window1->bounds().width() + divider_delta);
EXPECT_EQ(std::round(work_area_bounds.width() * chromeos::kOneThirdSnapRatio),
window2->bounds().width() + divider_delta);
}
// Tests that swap partial windows keeps the window sizes.
TEST_F(SplitViewControllerTest, SwapPartialWindows) {
std::unique_ptr<aura::Window> window1 = CreateTestWindow();
std::unique_ptr<aura::Window> window2 = CreateTestWindow();
// Snap `window1` to primary with 2/3 width and `window2` to secondary with
// 1/3 width. Verify the divider is at 2/3 of the work area.
WindowSnapWMEvent snap_primary_two_third(WM_EVENT_SNAP_PRIMARY,
chromeos::kTwoThirdSnapRatio);
WindowState::Get(window1.get())->OnWMEvent(&snap_primary_two_third);
WindowSnapWMEvent snap_secondary_one_third(WM_EVENT_SNAP_SECONDARY,
chromeos::kOneThirdSnapRatio);
WindowState::Get(window2.get())->OnWMEvent(&snap_secondary_one_third);
const gfx::Rect work_area_bounds =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
int divider_origin_x = split_view_divider()
->GetDividerBoundsInScreen(
/*is_dragging=*/false)
.x();
const int divider_delta = kSplitviewDividerShortSideLength / 2;
EXPECT_EQ(divider_origin_x, std::round(work_area_bounds.width() *
chromeos::kTwoThirdSnapRatio) -
divider_delta);
EXPECT_EQ(std::round(work_area_bounds.width() * chromeos::kTwoThirdSnapRatio),
window1->bounds().width() + divider_delta);
EXPECT_EQ(std::round(work_area_bounds.width() * chromeos::kOneThirdSnapRatio),
window2->bounds().width() + divider_delta);
// Verify that after swapping windows, the window widths remain the same, and
// the divider is now at 1/3 of the work area.
split_view_controller()->SwapWindows();
EXPECT_EQ(WindowState::Get(window1.get())->GetStateType(),
chromeos::WindowStateType::kSecondarySnapped);
EXPECT_EQ(WindowState::Get(window2.get())->GetStateType(),
chromeos::WindowStateType::kPrimarySnapped);
divider_origin_x = split_view_divider()
->GetDividerBoundsInScreen(
/*is_dragging=*/false)
.x();
// These are off by 1 pixel because the original snap ratio was 0.66repeating.
// When swapping windows, we go through another code path that updates the
// snap ratio based on the window dimensions, resulting in a similar but
// slightly different snap ratio of 0.6625.
EXPECT_NEAR(
divider_origin_x,
std::round(work_area_bounds.width() * chromeos::kOneThirdSnapRatio) -
divider_delta,
1);
EXPECT_NEAR(
std::round(work_area_bounds.width() * chromeos::kTwoThirdSnapRatio),
window1->bounds().width() + divider_delta, 1);
EXPECT_NEAR(
std::round(work_area_bounds.width() * chromeos::kOneThirdSnapRatio),
window2->bounds().width() + divider_delta, 1);
}
// Tests that we can snap two thirds even when one half is not available.
TEST_F(SplitViewControllerTest, SnapTwoThirdPartialWindow) {
UpdateDisplay("800x600");
// Create a window that has a minimum width such that it cannot be snapped one
// half, but can be snapped two thirds.
aura::test::TestWindowDelegate window_delegate;
std::unique_ptr<aura::Window> window(CreateTestWindowInShellWithDelegate(
&window_delegate, /*id=*/-1, gfx::Rect(500, 500)));
window_delegate.set_minimum_size(gfx::Size(500, 500));
window->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::BROWSER);
WindowSnapWMEvent snap_primary(WM_EVENT_SNAP_PRIMARY,
chromeos::kTwoThirdSnapRatio);
WindowState::Get(window.get())->OnWMEvent(&snap_primary);
EXPECT_TRUE(WindowState::Get(window.get())->IsSnapped());
}
// Tests that selecting a window that cannot be one third snapped from overview
// will maximize it and exit splitview. Regression test for b/278921341.
TEST_F(SplitViewControllerTest, SelectWindowCannotOneThirdSnap) {
UpdateDisplay("900x600");
// The first window can be snapped 2/3, but not 1/2 or 1/3.
aura::test::TestWindowDelegate window_delegate1;
std::unique_ptr<aura::Window> window1(CreateTestWindowInShellWithDelegate(
&window_delegate1, /*id=*/-1, gfx::Rect(500, 500)));
window_delegate1.set_minimum_size(gfx::Size(500, 500));
window1->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::BROWSER);
// The second window can be snapped 1/2 but not 1/3.
aura::test::TestWindowDelegate window_delegate2;
std::unique_ptr<aura::Window> window2(CreateTestWindowInShellWithDelegate(
&window_delegate2, /*id=*/-1, gfx::Rect(500, 500)));
window_delegate2.set_minimum_size(gfx::Size(400, 400));
window2->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::BROWSER);
// Snap `window1` 2/3 to the left.
wm::ActivateWindow(window1.get());
WindowSnapWMEvent snap_primary(WM_EVENT_SNAP_PRIMARY,
chromeos::kTwoThirdSnapRatio);
WindowState::Get(window1.get())->OnWMEvent(&snap_primary);
ASSERT_FALSE(IsDividerAnimating());
ASSERT_EQ(chromeos::kTwoThirdSnapRatio,
WindowState::Get(window1.get())->snap_ratio());
ASSERT_TRUE(WindowState::Get(window1.get())->IsSnapped());
ASSERT_TRUE(OverviewController::Get()->InOverviewSession());
// Select `window2`. Test that both windows are maximized and we have exited
// splitview.
wm::ActivateWindow(window2.get());
ASSERT_FALSE(IsDividerAnimating());
EXPECT_TRUE(WindowState::Get(window1.get())->IsMaximized());
EXPECT_TRUE(WindowState::Get(window2.get())->IsMaximized());
EXPECT_EQ(SplitViewController::State::kNoSnap,
split_view_controller()->state());
}
// Tests that, if two windows are snapped and one window has min size, trying to
// partial split the other window starts a bounce animation.
TEST_F(SplitViewControllerTest,
SnapWindowWithMinSizeStartsDividerSnapAnimation) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
gfx::Rect work_area_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window1.get());
aura::test::TestWindowDelegate* delegate2 =
static_cast<aura::test::TestWindowDelegate*>(window2->delegate());
// Set `window2` min length to be 0.4 of the work area so it can't fit in 1/3
// split.
delegate2->set_minimum_size(
gfx::Size(work_area_bounds.width() * 0.4f, work_area_bounds.height()));
// 1 - First test scenario where the secondary window can't fit in 1/3.
// Snap `window1` to primary and `window2` to secondary.
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kSecondary);
// Try to snap `window1` to 2/3 primary. Since `window2` can't fit in 1/3
// secondary, test that the divider and both windows bounce back to 1/2.
WindowSnapWMEvent snap_primary_two_third(WM_EVENT_SNAP_PRIMARY,
chromeos::kTwoThirdSnapRatio);
WindowState::Get(window1.get())->OnWMEvent(&snap_primary_two_third);
ASSERT_TRUE(IsDividerAnimating());
SkipDividerSnapAnimation();
gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(/*is_dragging=*/false);
EXPECT_EQ(work_area_bounds.width() * 0.5f,
divider_bounds.x() + kSplitviewDividerShortSideLength / 2);
EXPECT_EQ(work_area_bounds.width() * 0.5f,
window1->bounds().width() + kSplitviewDividerShortSideLength / 2);
EXPECT_EQ(work_area_bounds.width() * 0.5f,
window2->bounds().width() + kSplitviewDividerShortSideLength / 2);
// 2 - Second test scenario where the primary window can't fit in 1/3.
// Snap `window2` to primary and `window1` to secondary.
EndSplitView();
split_view_controller()->SnapWindow(window2.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kSecondary);
// Try to snap `window1` to 2/3 secondary. Since `window2` can't fit in 1/3
// primary, the divider and windows both bounce to 1/2.
WindowSnapWMEvent snap_secondary_two_thirds(WM_EVENT_SNAP_SECONDARY,
chromeos::kTwoThirdSnapRatio);
WindowState::Get(window1.get())->OnWMEvent(&snap_secondary_two_thirds);
ASSERT_TRUE(IsDividerAnimating());
SkipDividerSnapAnimation();
divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(/*is_dragging=*/false);
EXPECT_EQ(work_area_bounds.width() * 0.5f,
divider_bounds.x() + kSplitviewDividerShortSideLength / 2);
EXPECT_EQ(work_area_bounds.width() * 0.5f,
window1->bounds().width() + kSplitviewDividerShortSideLength / 2);
EXPECT_EQ(work_area_bounds.width() * 0.5f,
window2->bounds().width() + kSplitviewDividerShortSideLength / 2);
}
// Tests no crash on tablet <-> clamshell transition after a divider snap
// animation is started.
TEST_F(SplitViewControllerTest, NoCrashAfterDividerSnapAnimation) {
ui::ScopedAnimationDurationScaleMode animation_scale(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
// Snap 2 windows in split view. Set `window2` min length to be 0.4 of
// the work area so it can't fit in 1/3 split.
gfx::Rect work_area_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window1.get());
aura::test::TestWindowDelegate* delegate2 =
static_cast<aura::test::TestWindowDelegate*>(window2->delegate());
delegate2->set_minimum_size(
gfx::Size(work_area_bounds.width() * 0.4f, work_area_bounds.height()));
WindowSnapWMEvent snap_primary(WM_EVENT_SNAP_PRIMARY);
WindowState::Get(window1.get())->OnWMEvent(&snap_primary);
WindowSnapWMEvent snap_secondary(WM_EVENT_SNAP_SECONDARY);
WindowState::Get(window2.get())->OnWMEvent(&snap_secondary);
// Since `window2` can't fit in 1/3, we start a divider snap animation.
WindowSnapWMEvent snap_primary_two_third(WM_EVENT_SNAP_PRIMARY,
chromeos::kTwoThirdSnapRatio);
WindowState::Get(window1.get())->OnWMEvent(&snap_primary_two_third);
ASSERT_TRUE(IsDividerAnimating());
// Transition to clamshell mode. Test no crash.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
}
// Tests that resnapping a snapped window to its opposite snap position will
// start the partial overview and divider will be at the correct position. See
// crash at b/311216394.
TEST_F(SplitViewControllerTest, ResnapASnappedWindowToOppositePosition) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
gfx::Rect work_area_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window1.get());
aura::test::TestWindowDelegate* delegate2 =
static_cast<aura::test::TestWindowDelegate*>(window2->delegate());
// Set the window minimum size to be between 1/3 and 1/2.
delegate2->set_minimum_size(
gfx::Size(work_area_bounds.width() * 0.4f, work_area_bounds.height()));
// Snap `window1` to primary 2/3.
WindowSnapWMEvent snap_primary_two_thirds(WM_EVENT_SNAP_PRIMARY,
chromeos::kTwoThirdSnapRatio);
WindowState::Get(window1.get())->OnWMEvent(&snap_primary_two_thirds);
SkipDividerSnapAnimation();
OverviewController* overview_controller = OverviewController::Get();
EXPECT_TRUE(overview_controller->InOverviewSession());
// Select `window2` from overview. Since its minimum size is greater than 1/3,
// it gets snapped at 1/2.
auto* item2 = GetOverviewItemForWindow(window2.get());
GetEventGenerator()->GestureTapAt(
gfx::ToRoundedPoint(item2->target_bounds().CenterPoint()));
ASSERT_FALSE(IsDividerAnimating());
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
EXPECT_EQ(
work_area_bounds.width() * 0.5f - kSplitviewDividerShortSideLength / 2,
split_view_controller()->GetDividerPosition());
EXPECT_EQ(work_area_bounds.width() * 0.5f,
window1->bounds().width() + kSplitviewDividerShortSideLength / 2);
EXPECT_EQ(work_area_bounds.width() * 0.5f,
window2->bounds().width() + kSplitviewDividerShortSideLength / 2);
// Re-snap `window2` to primary 2/3.
WindowSnapWMEvent snap_secondary_two_thirds(WM_EVENT_SNAP_PRIMARY,
chromeos::kTwoThirdSnapRatio);
WindowState::Get(window2.get())->OnWMEvent(&snap_secondary_two_thirds);
ASSERT_FALSE(IsDividerAnimating());
EXPECT_NEAR(work_area_bounds.width() * 0.67f,
split_view_controller()->GetDividerPosition(),
kSplitviewDividerShortSideLength);
EXPECT_NEAR(work_area_bounds.width() * 0.67f, window2->bounds().width(),
kSplitviewDividerShortSideLength);
EXPECT_TRUE(overview_controller->InOverviewSession());
EXPECT_TRUE(split_view_divider()->divider_widget());
}
// Tests that auto-snap for partial windows works correctly.
TEST_F(SplitViewControllerTest, AutoSnapPartialWindows) {
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
// 1. Test without min size. Snap `window1` to 2/3.
WindowSnapWMEvent snap_primary_two_third(WM_EVENT_SNAP_PRIMARY,
chromeos::kTwoThirdSnapRatio);
WindowState::Get(window1.get())->OnWMEvent(&snap_primary_two_third);
// Activate `window2`. Test that `window2` gets auto-snapped to 1/3.
wm::ActivateWindow(window2.get());
gfx::Rect work_area_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window1.get());
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
const int divider_delta = kSplitviewDividerShortSideLength / 2;
EXPECT_EQ(std::round(work_area_bounds.width() * chromeos::kTwoThirdSnapRatio),
window1->bounds().width() + divider_delta);
EXPECT_EQ(std::round(work_area_bounds.width() * chromeos::kOneThirdSnapRatio),
window2->bounds().width() + divider_delta);
EndSplitView();
// 2. Test with min size. Set `window2` min length so that it can't fit in 1/3
// split. Snap `window1` to primary 2/3.
aura::test::TestWindowDelegate* delegate2 =
static_cast<aura::test::TestWindowDelegate*>(window2->delegate());
delegate2->set_minimum_size(
gfx::Size(work_area_bounds.width() * 0.4f, work_area_bounds.height()));
WindowState::Get(window1.get())->OnWMEvent(&snap_primary_two_third);
// Activate `window2`. Test that `window2` gets auto-snapped but pushed to 1/2
// and `window1` also gets updated to 1/2.
wm::ActivateWindow(window2.get());
EXPECT_EQ(split_view_controller()->state(),
SplitViewController::State::kBothSnapped);
EXPECT_EQ(work_area_bounds.width() * chromeos::kDefaultSnapRatio,
window1->bounds().width() + divider_delta);
EXPECT_EQ(work_area_bounds.width() * chromeos::kDefaultSnapRatio,
window2->bounds().width() + divider_delta);
}
// Tests that the split view divider will be stacked above the two observed
// windows in split view. On window drag started, the divider will be placed
// below the dragged window. On window drag ended, the divider will be placed
// back on top of the two observed windows.
TEST_F(SplitViewControllerTest, StackingOrderWithDivider) {
std::unique_ptr<aura::Window> w1(CreateTestWindow());
std::unique_ptr<aura::Window> w2(CreateTestWindow());
SplitViewController* controller = split_view_controller();
controller->SnapWindow(w1.get(), SnapPosition::kPrimary);
EXPECT_EQ(split_view_controller()->primary_window(), w1.get());
split_view_controller()->SnapWindow(w2.get(), SnapPosition::kSecondary);
EXPECT_EQ(controller->state(), SplitViewController::State::kBothSnapped);
SplitViewDivider* divider = split_view_divider();
ASSERT_TRUE(divider->divider_widget());
aura::Window* divider_widget_native_window = divider->GetDividerWindow();
EXPECT_TRUE(
window_util::IsStackedBelow(w1.get(), divider_widget_native_window));
EXPECT_TRUE(
window_util::IsStackedBelow(w2.get(), divider_widget_native_window));
controller->OnWindowDragStarted(w1.get());
EXPECT_TRUE(
window_util::IsStackedBelow(divider_widget_native_window, w1.get()));
controller->OnWindowDragCanceled();
EXPECT_TRUE(
window_util::IsStackedBelow(w1.get(), divider_widget_native_window));
EXPECT_TRUE(
window_util::IsStackedBelow(w2.get(), divider_widget_native_window));
}
// Tests that the divider remains visible when minimizing and restoring the
// window in tablet split view.
TEST_F(SplitViewControllerTest, DividerStaysVisibleDuringMinimizeAndRestore) {
std::unique_ptr<aura::Window> w1(CreateTestWindow());
std::unique_ptr<aura::Window> w2(CreateTestWindow());
SplitViewController* controller = split_view_controller();
controller->SnapWindow(w1.get(), SnapPosition::kPrimary);
EXPECT_EQ(split_view_controller()->primary_window(), w1.get());
split_view_controller()->SnapWindow(w2.get(), SnapPosition::kSecondary);
EXPECT_EQ(controller->state(), SplitViewController::State::kBothSnapped);
SplitViewDivider* divider = split_view_divider();
ASSERT_TRUE(divider->divider_widget());
EXPECT_TRUE(divider->GetDividerWindow()->IsVisible());
// Tests that the divider stays visible on `w1` minimized and restore.
// To simulate the actual CUJ when user minimizes a window i.e. the minimized
// window will be activated by either clicking on the minimize button or
// shortcut.
wm::ActivateWindow(w1.get());
WMEvent w1_minimize(WM_EVENT_MINIMIZE);
WindowState::Get(w1.get())->OnWMEvent(&w1_minimize);
EXPECT_FALSE(w1->IsVisible());
EXPECT_TRUE(divider->GetDividerWindow()->IsVisible());
// Restoring the window will refresh the widget but keep it visible.
WMEvent w1_restore(WM_EVENT_RESTORE);
WindowState::Get(w1.get())->OnWMEvent(&w1_restore);
EXPECT_TRUE(divider->GetDividerWindow()->IsVisible());
}
// Tests the windows stay onscreen during fast resize. Regression test for
// b/304367964.
TEST_F(SplitViewControllerTest, PerformantResize) {
ui::ScopedAnimationDurationScaleMode animation_scale(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
UpdateDisplay("900x600");
const gfx::Rect work_area =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
std::unique_ptr<aura::Window> w1(CreateTestWindow());
std::unique_ptr<aura::Window> w2(CreateTestWindow());
SplitViewController* controller = split_view_controller();
controller->SnapWindow(w1.get(), SnapPosition::kPrimary);
controller->SnapWindow(w2.get(), SnapPosition::kSecondary);
SplitViewController::SetUseFastResizeForTesting(true);
// Move the divider very far left.
auto* generator = GetEventGenerator();
const gfx::Rect divider_bounds(
split_view_divider()->GetDividerBoundsInScreen(/*is_dragging=*/false));
const gfx::Point divider_point(divider_bounds.CenterPoint());
const gfx::Point resize_point1(work_area.x() + 1, divider_point.y());
generator->GestureScrollSequence(divider_point, resize_point1,
base::Milliseconds(500),
/*steps=*/3);
// Test the windows are onscreen.
EXPECT_TRUE(work_area.Contains(w1->GetTargetBounds()));
EXPECT_TRUE(work_area.Contains(w2->GetTargetBounds()));
// Move the divider very far right.
const gfx::Point resize_point2(work_area.right() - 1, divider_point.y());
generator->GestureScrollSequence(resize_point1, resize_point2,
base::Milliseconds(500),
/*steps=*/3);
// Test the windows are onscreen.
EXPECT_TRUE(work_area.Contains(w1->GetTargetBounds()));
EXPECT_TRUE(work_area.Contains(w2->GetTargetBounds()));
}
// Tests that windows with different containers can be snapped properly with no
// crash. The stacking order and parent of the split view divider will be
// updated correctly with window activation and dragging operations.
TEST_F(SplitViewControllerTest, SnapWindowsWithDifferentParentContainers) {
std::unique_ptr<aura::Window> always_on_top_window(CreateTestWindow());
always_on_top_window->SetProperty(aura::client::kZOrderingKey,
ui::ZOrderLevel::kFloatingWindow);
std::unique_ptr<aura::Window> normal_window(CreateTestWindow());
SplitViewController* controller = split_view_controller();
controller->SnapWindow(always_on_top_window.get(), SnapPosition::kPrimary);
controller->SnapWindow(normal_window.get(), SnapPosition::kSecondary);
EXPECT_EQ(controller->state(), SplitViewController::State::kBothSnapped);
SplitViewDivider* divider = split_view_divider();
ASSERT_TRUE(divider->divider_widget());
aura::Window* divider_widget_native_window = divider->GetDividerWindow();
EXPECT_EQ(divider_widget_native_window->parent(),
always_on_top_window->parent());
EXPECT_EQ(ui::ZOrderLevel::kFloatingWindow,
always_on_top_window->GetProperty(aura::client::kZOrderingKey));
EXPECT_EQ(ui::ZOrderLevel::kNormal,
normal_window->GetProperty(aura::client::kZOrderingKey));
wm::ActivateWindow(always_on_top_window.get());
EXPECT_EQ(controller->state(), SplitViewController::State::kBothSnapped);
EXPECT_EQ(ui::ZOrderLevel::kFloatingWindow,
always_on_top_window->GetProperty(aura::client::kZOrderingKey));
EXPECT_TRUE(window_util::IsStackedBelow(always_on_top_window.get(),
divider_widget_native_window));
wm::ActivateWindow(normal_window.get());
EXPECT_EQ(controller->state(), SplitViewController::State::kBothSnapped);
EXPECT_EQ(ui::ZOrderLevel::kFloatingWindow,
always_on_top_window->GetProperty(aura::client::kZOrderingKey));
EXPECT_TRUE(window_util::IsStackedBelow(always_on_top_window.get(),
divider_widget_native_window));
// The split view divider will be stacked below the dragged window i.e.
// `normal_window` temporarily during dragging. The divider will also be
// reparented to be sibling of `normal_window` while dragging.
controller->OnWindowDragStarted(normal_window.get());
EXPECT_EQ(divider_widget_native_window->parent(), normal_window->parent());
EXPECT_TRUE(window_util::IsStackedBelow(divider_widget_native_window,
normal_window.get()));
// On drag ended, the split view divider will be stacked back on top of the
// above window i.e. the `always_on_top_window`. The divider will also be
// reparented to be sibling of `always_on_top_window`.
controller->OnWindowDragCanceled();
EXPECT_EQ(divider_widget_native_window->parent(),
always_on_top_window->parent());
EXPECT_TRUE(window_util::IsStackedBelow(always_on_top_window.get(),
divider_widget_native_window));
}
TEST_F(SplitViewControllerTest, WMSnapEventDeviceOrientationMetricsInTablet) {
UpdateDisplay("800x600");
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
ScreenOrientationControllerTestApi test_api(
Shell::Get()->screen_orientation_controller());
ASSERT_EQ(test_api.GetCurrentOrientation(),
chromeos::OrientationType::kLandscapePrimary);
constexpr char kDeviceOrientationTablet[] =
"Ash.SplitView.DeviceOrientation.TabletMode";
constexpr char kDeviceOrientationEntryPoint[] =
"Ash.SplitView.EntryPoint.DeviceOrientation";
constexpr char kDeviceOrientationInSplitView[] =
"Ash.SplitView.OrientationInSplitView";
base::HistogramTester histogram_tester;
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
// 1. Test landscape orientation.
// Snap |window1| to the left to enter split view overview in tablet mode.
WindowSnapWMEvent wm_left_snap_event(WM_EVENT_SNAP_PRIMARY);
WindowState::Get(window1.get())->OnWMEvent(&wm_left_snap_event);
EXPECT_TRUE(split_view_controller()->InSplitViewMode());
OverviewController* overview_controller = OverviewController::Get();
EXPECT_TRUE(overview_controller->InOverviewSession());
histogram_tester.ExpectBucketCount(
kDeviceOrientationTablet,
SplitViewMetricsController::DeviceOrientation::kLandscape, 1);
histogram_tester.ExpectBucketCount(
kDeviceOrientationEntryPoint,
SplitViewMetricsController::DeviceOrientation::kLandscape, 1);
// 2. Test portrait orientation.
// Rotate the screen by 270 degrees to portrait primary orientation.
test_api.SetDisplayRotation(display::Display::ROTATE_270,
display::Display::RotationSource::ACTIVE);
ASSERT_EQ(test_api.GetCurrentOrientation(),
chromeos::OrientationType::kPortraitPrimary);
histogram_tester.ExpectBucketCount(
kDeviceOrientationTablet,
SplitViewMetricsController::DeviceOrientation::kPortrait, 1);
histogram_tester.ExpectBucketCount(
kDeviceOrientationInSplitView,
SplitViewMetricsController::DeviceOrientation::kPortrait, 1);
}
TEST_F(SplitViewControllerTest,
WMSnapEventDeviceOrientationMetricsInClamshell) {
UpdateDisplay("800x600/l");
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
base::HistogramTester histogram_tester;
constexpr char kDeviceOrientationClamshell[] =
"Ash.SplitView.DeviceOrientation.ClamshellMode";
constexpr char kDeviceOrientationEntryPoint[] =
"Ash.SplitView.EntryPoint.DeviceOrientation";
constexpr char kDeviceOrientationInSplitView[] =
"Ash.SplitView.OrientationInSplitView";
const gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> window1(CreateAppWindow(bounds));
std::unique_ptr<aura::Window> window2(CreateAppWindow(bounds));
wm::ActivateWindow(window1.get());
EXPECT_FALSE(split_view_controller()->InSplitViewMode());
const WindowSnapWMEvent wm_primary_snap_event(WM_EVENT_SNAP_PRIMARY,
chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kTest);
const WindowSnapWMEvent wm_secondary_snap_event(
WM_EVENT_SNAP_SECONDARY, chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kTest);
const WMEvent fullscreen_event(WM_EVENT_TOGGLE_FULLSCREEN);
// Check the initial value of the histograms.
histogram_tester.ExpectBucketCount(
kDeviceOrientationClamshell,
SplitViewMetricsController::DeviceOrientation::kPortrait, 0);
histogram_tester.ExpectBucketCount(
kDeviceOrientationEntryPoint,
SplitViewMetricsController::DeviceOrientation::kPortrait, 0);
// 1. Test portrait orientation.
// Snap `window1` to the left.
WindowState::Get(window1.get())->OnWMEvent(&wm_primary_snap_event);
// With faster split screen enabled, `SplitViewController` will now manage
// snapping a window without being in overview case.
histogram_tester.ExpectBucketCount(
kDeviceOrientationClamshell,
SplitViewMetricsController::DeviceOrientation::kPortrait, 1);
histogram_tester.ExpectBucketCount(
kDeviceOrientationEntryPoint,
SplitViewMetricsController::DeviceOrientation::kPortrait, 1);
// Activate `window2` to snap to the right. With windows snapped to both side,
// split view metric controller should start recording metrics.
wm::ActivateWindow(window2.get());
WindowState::Get(window2.get())->OnWMEvent(&wm_secondary_snap_event);
histogram_tester.ExpectBucketCount(
kDeviceOrientationClamshell,
SplitViewMetricsController::DeviceOrientation::kPortrait, 1);
histogram_tester.ExpectBucketCount(
kDeviceOrientationEntryPoint,
SplitViewMetricsController::DeviceOrientation::kPortrait, 1);
// 2. Test landscape orientation.
histogram_tester.ExpectBucketCount(
kDeviceOrientationClamshell,
SplitViewMetricsController::DeviceOrientation::kLandscape, 0);
histogram_tester.ExpectBucketCount(
kDeviceOrientationInSplitView,
SplitViewMetricsController::DeviceOrientation::kLandscape, 0);
// Update display to landscape and check that the counts for orientation
// metrics increase except the count for orientation entry point.
UpdateDisplay("800x600");
histogram_tester.ExpectBucketCount(
kDeviceOrientationClamshell,
SplitViewMetricsController::DeviceOrientation::kLandscape, 1);
histogram_tester.ExpectBucketCount(
kDeviceOrientationInSplitView,
SplitViewMetricsController::DeviceOrientation::kLandscape, 1);
histogram_tester.ExpectBucketCount(
kDeviceOrientationEntryPoint,
SplitViewMetricsController::DeviceOrientation::kLandscape, 0);
// Maximize both `window1` and `window2` to unsnap and re-snap `window1` to
// the left to trigger the split view metrics recording.
WindowState::Get(window1.get())->OnWMEvent(&fullscreen_event);
WindowState::Get(window2.get())->OnWMEvent(&fullscreen_event);
WindowState::Get(window1.get())->OnWMEvent(&wm_primary_snap_event);
histogram_tester.ExpectBucketCount(
kDeviceOrientationClamshell,
SplitViewMetricsController::DeviceOrientation::kLandscape, 2);
histogram_tester.ExpectBucketCount(
kDeviceOrientationInSplitView,
SplitViewMetricsController::DeviceOrientation::kLandscape, 1);
histogram_tester.ExpectBucketCount(
kDeviceOrientationEntryPoint,
SplitViewMetricsController::DeviceOrientation::kLandscape, 1);
}
// Test that there will be no crash when disabling a tablet mode when a window
// with a transient bubble widget is snapped. Regression test for b/327135981.
TEST_F(SplitViewControllerTest,
ClamshellConversionWithSnappedWindowWithTransient) {
// Create a widget with a transient bubble widget.
std::unique_ptr<views::Widget> widget(
CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET));
aura::Window* window = widget->GetNativeWindow();
window->SetProperty(aura::client::kResizeBehaviorKey,
aura::client::kResizeBehaviorCanResize |
aura::client::kResizeBehaviorCanMaximize);
views::Widget* bubble_widget = views::BubbleDialogDelegateView::CreateBubble(
new TestBubbleDialogDelegateView(widget->GetContentsView()));
aura::Window* bubble_transient = bubble_widget->GetNativeWindow();
EXPECT_TRUE(wm::HasTransientAncestor(bubble_transient, window));
// Snap the window.
ToggleOverview();
split_view_controller()->SnapWindow(window, SnapPosition::kSecondary);
// Convert the device to clamshell mode. There should be no crash.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
}
TEST_F(SplitViewControllerTest, SplitViewDividerViewAccessibleProperties) {
UpdateDisplay("900x600");
std::unique_ptr<aura::Window> window(CreateWindow(gfx::Rect(0, 0, 520, 500)));
split_view_controller()->SnapWindow(
window.get(), SnapPosition::kPrimary,
WindowSnapActionSource::kSnapByWindowLayoutMenu,
/*activate_window=*/false, chromeos::kDefaultSnapRatio);
auto* divider_view = split_view_divider()->divider_view_for_testing();
ui::AXNodeData data;
ASSERT_TRUE(divider_view);
divider_view->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(data.role, ax::mojom::Role::kToolbar);
EXPECT_EQ(data.GetString16Attribute(ax::mojom::StringAttribute::kName),
l10n_util::GetStringUTF16(IDS_ASH_SNAP_GROUP_DIVIDER_A11Y_NAME));
}
// The test class that enables the feature flag of portrait mode split view
// virtual keyboard improvement and the virtual keyboard.
class SplitViewKeyboardTest : public SplitViewControllerTest {
public:
SplitViewKeyboardTest() = default;
SplitViewKeyboardTest(const SplitViewKeyboardTest&) = delete;
SplitViewKeyboardTest& operator=(const SplitViewKeyboardTest&) = delete;
~SplitViewKeyboardTest() override = default;
// SplitViewControllerTest:
void SetUp() override {
SplitViewControllerTest::SetUp();
SetVirtualKeyboardEnabled(true);
}
keyboard::KeyboardUIController* keyboard_controller() {
return keyboard::KeyboardUIController::Get();
}
};
// Tests that when the input field in the bottom window is blocked by the
// virtual keyboard (the bottom of the caret is less than
// `kMinCaretKeyboardDist` above the virtual keyboard), the bottom window will
// be pushed above the virtual keyboard.
TEST_F(SplitViewKeyboardTest, PushUpBottomWindow) {
UpdateDisplay("1200x800");
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
ScreenOrientationControllerTestApi test_api(
Shell::Get()->screen_orientation_controller());
ASSERT_EQ(chromeos::OrientationType::kLandscapePrimary,
test_api.GetCurrentOrientation());
gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> bottom_window(CreateWindow(bounds));
auto bottom_client =
std::make_unique<TestTextInputClient>(bottom_window.get());
split_view_controller()->SnapWindow(bottom_window.get(),
SnapPosition::kSecondary);
test_api.SetDisplayRotation(display::Display::ROTATE_270,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(chromeos::OrientationType::kPortraitPrimary,
test_api.GetCurrentOrientation());
EXPECT_FALSE(
IsPhysicallyLeftOrTop(SnapPosition::kSecondary, bottom_window.get()));
const gfx::Rect keyboard_bounds =
keyboard_controller()->GetKeyboardWindow()->GetBoundsInScreen();
const gfx::Rect orig_bottom_bounds = bottom_window->GetBoundsInScreen();
const gfx::Rect orig_divider_bounds = split_view_controller()
->split_view_divider()
->divider_widget()
->GetWindowBoundsInScreen();
// Set the caret position in bottom window above the upper bounds of the
// virtual keyboard. When the virtual keyboard is enabled, the bottom window
// will not shift.
bottom_client->set_caret_bounds(gfx::Rect(
keyboard_bounds.top_center() +
gfx::Vector2d(0, -kMinCaretKeyboardDist - kCaretHeightForTest - 10),
gfx::Size(0, kCaretHeightForTest)));
bottom_client->Focus();
EXPECT_TRUE(keyboard_controller()->IsKeyboardVisible());
EXPECT_EQ(orig_bottom_bounds, bottom_window->GetBoundsInScreen());
// The split view divider is adjustable and not moved.
EXPECT_EQ(orig_divider_bounds, split_view_controller()
->split_view_divider()
->divider_widget()
->GetWindowBoundsInScreen());
EXPECT_TRUE(split_view_controller()->split_view_divider()->IsAdjustable());
// Disable the keyboard.
bottom_client->UnFocus();
EXPECT_FALSE(keyboard_controller()->IsKeyboardVisible());
const gfx::Rect shift_bottom_bounds(
keyboard_bounds.origin() + gfx::Vector2d(0, -orig_bottom_bounds.height()),
orig_bottom_bounds.size());
const gfx::Rect shift_divider_bounds(
shift_bottom_bounds.origin() +
gfx::Vector2d(0, -orig_divider_bounds.height()),
orig_divider_bounds.size());
// Set the caret position in bottom window below the upper bounds of the
// virtual keyboard. When the virtual keyboard is enabled, the bottom window
// will shift above the virtual keyboard.
bottom_client->set_caret_bounds(
gfx::Rect(keyboard_bounds.top_center() + gfx::Vector2d(0, 10),
gfx::Size(0, kCaretHeightForTest)));
bottom_client->Focus();
EXPECT_TRUE(keyboard_controller()->IsKeyboardVisible());
EXPECT_EQ(shift_bottom_bounds, bottom_window->GetBoundsInScreen());
// The split view divider will also be shifted and become unadjustable.
EXPECT_EQ(shift_divider_bounds, split_view_controller()
->split_view_divider()
->divider_widget()
->GetWindowBoundsInScreen());
EXPECT_FALSE(split_view_controller()->split_view_divider()->IsAdjustable());
// Disable the keyboard. The bottom window will restore to original bounds.
// The split view divider will also be adjustable and restore to original
// bounds.
bottom_client->UnFocus();
EXPECT_FALSE(keyboard_controller()->IsKeyboardVisible());
EXPECT_EQ(orig_bottom_bounds, bottom_window->GetBoundsInScreen());
EXPECT_EQ(orig_divider_bounds, split_view_controller()
->split_view_divider()
->divider_widget()
->GetWindowBoundsInScreen());
EXPECT_TRUE(split_view_controller()->split_view_divider()->IsAdjustable());
}
// When the bottom window is pushed up due to the virtual keyboard and the
// shifted window position cannot exceed `1 - kMinDividerPositionRatio` of the
// screen height.
TEST_F(SplitViewKeyboardTest, PushUpBottomWindowLimitHeight) {
UpdateDisplay("1200x800");
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
ScreenOrientationControllerTestApi test_api(
Shell::Get()->screen_orientation_controller());
ASSERT_EQ(chromeos::OrientationType::kLandscapePrimary,
test_api.GetCurrentOrientation());
gfx::Rect bounds(0, 0, 200, 200);
std::unique_ptr<aura::Window> bottom_window(CreateWindow(bounds));
auto bottom_client =
std::make_unique<TestTextInputClient>(bottom_window.get());
SplitViewController* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
split_view_controller->SnapWindow(bottom_window.get(),
SnapPosition::kSecondary);
test_api.SetDisplayRotation(display::Display::ROTATE_270,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(chromeos::OrientationType::kPortraitPrimary,
test_api.GetCurrentOrientation());
EXPECT_FALSE(
IsPhysicallyLeftOrTop(SnapPosition::kSecondary, bottom_window.get()));
const gfx::Rect keyboard_bounds =
keyboard_controller()->GetKeyboardWindow()->GetBoundsInScreen();
const gfx::Rect divider_bounds =
split_view_divider()->GetDividerBoundsInScreen(false /* is_dragging */);
const gfx::Rect screen_bounds =
screen_util::GetDisplayWorkAreaBoundsInParent(bottom_window.get());
const int screen_height = screen_bounds.height();
const int limit_y = screen_height * kMinDividerPositionRatio;
gfx::Rect ori_divider_bounds = split_view_controller->split_view_divider()
->divider_widget()
->GetWindowBoundsInScreen();
// Resize divider to a position that when the bottom window is pushed up, its
// position will exceeds `1-kMinDividerPositionRatio` of screen height.
const auto drag_start_point = divider_bounds.CenterPoint();
split_view_divider()->StartResizeWithDivider(drag_start_point);
const int drag_end_point_y = screen_height * 0.15f;
split_view_divider()->ResizeWithDivider(gfx::Point(0, screen_height * 0.15f));
// Adjust the `ori_divider_bounds` with the dragging distance to be used for
// check later.
ori_divider_bounds.Offset(0, drag_end_point_y - drag_start_point.y());
const gfx::Rect ori_bottom_bounds = bottom_window->GetBoundsInScreen();
EXPECT_LT(keyboard_bounds.y() - ori_bottom_bounds.height(), limit_y);
// Set the caret position in bottom window below the upper bounds of the
// virtual keyboard. When the virtual keyboard is enabled, the bottom window
// will shift above the virtual keyboard but the upper bounds will be limited
// to `kMinDividerPositionRatio` of the screen height.
const gfx::Rect shift_bottom_bounds(0, limit_y, keyboard_bounds.width(),
keyboard_bounds.y() - limit_y);
const gfx::Rect shift_divider_bounds(
shift_bottom_bounds.origin() +
gfx::Vector2d(0, -ori_divider_bounds.height()),
ori_divider_bounds.size());
bottom_client->set_caret_bounds(gfx::Rect(keyboard_bounds.top_center(),
gfx::Size(0, kCaretHeightForTest)));
bottom_client->Focus();
EXPECT_TRUE(keyboard_controller()->IsKeyboardVisible());
EXPECT_EQ(shift_bottom_bounds, bottom_window->GetBoundsInScreen());
// The split view divider will also be shifted and become unadjustable.
EXPECT_EQ(shift_divider_bounds, split_view_controller->split_view_divider()
->divider_widget()
->GetWindowBoundsInScreen());
EXPECT_FALSE(split_view_controller->split_view_divider()->IsAdjustable());
// Disable the keyboard. The bottom window will restore to original bounds.
// The split view divider will also be adjustable and restore to original
// bounds.
bottom_client->UnFocus();
EXPECT_FALSE(keyboard_controller()->IsKeyboardVisible());
EXPECT_EQ(ori_bottom_bounds, bottom_window->GetBoundsInScreen());
EXPECT_EQ(ori_divider_bounds, split_view_controller->split_view_divider()
->divider_widget()
->GetWindowBoundsInScreen());
EXPECT_TRUE(split_view_controller->split_view_divider()->IsAdjustable());
}
// Tests that when the bottom window is pushed up due to the virtual keyboard
// and the top window is activated, then the bottom window should restore to the
// original layout.
TEST_F(SplitViewKeyboardTest, RestoreByActivatingTopWindow) {
UpdateDisplay("1200x800");
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
ScreenOrientationControllerTestApi test_api(
Shell::Get()->screen_orientation_controller());
ASSERT_EQ(chromeos::OrientationType::kLandscapePrimary,
test_api.GetCurrentOrientation());
gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> top_window(CreateWindow(bounds));
std::unique_ptr<aura::Window> bottom_window(CreateWindow(bounds));
auto top_client = std::make_unique<TestTextInputClient>(top_window.get());
auto bottom_client =
std::make_unique<TestTextInputClient>(bottom_window.get());
split_view_controller()->SnapWindow(top_window.get(), SnapPosition::kPrimary);
split_view_controller()->SnapWindow(bottom_window.get(),
SnapPosition::kSecondary);
test_api.SetDisplayRotation(display::Display::ROTATE_270,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(chromeos::OrientationType::kPortraitPrimary,
test_api.GetCurrentOrientation());
EXPECT_TRUE(IsPhysicallyLeftOrTop(SnapPosition::kPrimary, top_window.get()));
const gfx::Rect keyboard_bounds =
keyboard_controller()->GetKeyboardWindow()->GetBoundsInScreen();
const gfx::Rect orig_bottom_bounds = bottom_window->GetBoundsInScreen();
const gfx::Rect shift_bottom_bounds(
keyboard_bounds.origin() + gfx::Vector2d(0, -orig_bottom_bounds.height()),
orig_bottom_bounds.size());
const gfx::Rect orig_divider_bounds = split_view_controller()
->split_view_divider()
->divider_widget()
->GetWindowBoundsInScreen();
const gfx::Rect shift_divider_bounds(
shift_bottom_bounds.origin() +
gfx::Vector2d(0, -orig_divider_bounds.height()),
orig_divider_bounds.size());
// Set the caret position in bottom window below the upper bounds of the
// virtual keyboard. When the virtual keyboard is enabled, the bottom window
// will shift.
bottom_client->set_caret_bounds(
gfx::Rect(keyboard_bounds.top_center() + gfx::Vector2d(0, 10),
gfx::Size(0, kCaretHeightForTest)));
bottom_client->Focus();
EXPECT_TRUE(keyboard_controller()->IsKeyboardVisible());
EXPECT_EQ(shift_bottom_bounds, bottom_window->GetBoundsInScreen());
// The split view divider will also be shifted and become unadjustable.
EXPECT_EQ(shift_divider_bounds, split_view_controller()
->split_view_divider()
->divider_widget()
->GetWindowBoundsInScreen());
EXPECT_FALSE(split_view_controller()->split_view_divider()->IsAdjustable());
// Activate the top window. The bottom window will restore to original bounds.
top_client->Focus();
EXPECT_TRUE(keyboard_controller()->IsKeyboardVisible());
EXPECT_EQ(orig_bottom_bounds, bottom_window->GetBoundsInScreen());
EXPECT_EQ(orig_divider_bounds, split_view_controller()
->split_view_divider()
->divider_widget()
->GetWindowBoundsInScreen());
EXPECT_TRUE(split_view_controller()->split_view_divider()->IsAdjustable());
}
// Tests that when there is no activated input field in the bottom window,
// showing keyboard (on-screen keyboard) will not change the split view layout.
TEST_F(SplitViewKeyboardTest, NoInputField) {
UpdateDisplay("1200x800");
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
ScreenOrientationControllerTestApi test_api(
Shell::Get()->screen_orientation_controller());
ASSERT_EQ(chromeos::OrientationType::kLandscapePrimary,
test_api.GetCurrentOrientation());
gfx::Rect bounds(0, 0, 400, 400);
std::unique_ptr<aura::Window> bottom_window(CreateWindow(bounds));
split_view_controller()->SnapWindow(bottom_window.get(),
SnapPosition::kSecondary);
test_api.SetDisplayRotation(display::Display::ROTATE_270,
display::Display::RotationSource::ACTIVE);
EXPECT_EQ(chromeos::OrientationType::kPortraitPrimary,
test_api.GetCurrentOrientation());
EXPECT_FALSE(
IsPhysicallyLeftOrTop(SnapPosition::kSecondary, bottom_window.get()));
const gfx::Rect orig_bottom_bounds = bottom_window->GetBoundsInScreen();
const gfx::Rect orig_divider_bounds = split_view_controller()
->split_view_divider()
->divider_widget()
->GetWindowBoundsInScreen();
// Enable keyboard. The bottom window and divider will not move since there is
// no input field.
keyboard_controller()->ShowKeyboard(/*lock=*/false);
EXPECT_TRUE(keyboard_controller()->IsKeyboardVisible());
EXPECT_EQ(orig_bottom_bounds, bottom_window->GetBoundsInScreen());
EXPECT_EQ(orig_divider_bounds, split_view_controller()
->split_view_divider()
->divider_widget()
->GetWindowBoundsInScreen());
EXPECT_TRUE(split_view_controller()->split_view_divider()->IsAdjustable());
}
// Tests that in the split view with Overview enabled, the snapped window bounds
// will be updated when the on-screen keyboard is enabled and disabled.
TEST_F(SplitViewKeyboardTest, ShowHideOnScreenKeyboardWithOverviewEnabled) {
UpdateDisplay("1200x800");
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager = Shell::Get()->display_manager();
display::test::ScopedSetInternalDisplayId set_internal(display_manager,
display_id);
ScreenOrientationControllerTestApi test_api(
Shell::Get()->screen_orientation_controller());
std::unique_ptr<aura::Window> right_window(
CreateWindow(gfx::Rect(0, 0, 400, 400)));
for (auto rotation :
{display::Display::ROTATE_0, display::Display::ROTATE_270}) {
EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
test_api.SetDisplayRotation(rotation,
display::Display::RotationSource::ACTIVE);
// Cache the original work area.
const gfx::Rect origin_work_area =
screen_util::GetDisplayWorkAreaBoundsInParent(right_window.get());
// Enable an on-screen virtual keyboard. The display work area should shrink
// the size of intersection between on-screen keyboard and original work
// area.
keyboard_controller()->ShowKeyboard(/*lock=*/true);
const gfx::Rect shrink_work_area =
screen_util::GetDisplayWorkAreaBoundsInParent(right_window.get());
const gfx::Rect keyboard_bounds =
keyboard_controller()->GetKeyboardWindow()->GetBoundsInScreen();
EXPECT_EQ(origin_work_area.height() - shrink_work_area.height(),
gfx::IntersectRects(keyboard_bounds, origin_work_area).height());
// Snapping the window will enable Overview, the window's bottom is equal to
// the shrunk work area bottom.
split_view_controller()->SnapWindow(right_window.get(),
SnapPosition::kSecondary);
EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
EXPECT_EQ(right_window->bounds().bottom(), shrink_work_area.bottom());
// Dismiss on-screen keyboard, the window's bottom is equal to the original
// work area bottom.
keyboard_controller()->HideKeyboardByUser();
EXPECT_EQ(right_window->bounds().bottom(), origin_work_area.bottom());
EndSplitView();
ExitOverview();
}
}
// Tests no crash in clamshell split view on keyboard bounds change. Regression
// test for b/331194782.
TEST_F(SplitViewKeyboardTest, NoCrashOnClamshellBoundsChange) {
// Enter vertical splitview in clamshell mode.
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
UpdateDisplay("800x1200");
gfx::Rect bounds(0, 0, 200, 200);
std::unique_ptr<aura::Window> window1(CreateWindow(bounds));
auto input_client = std::make_unique<TestTextInputClient>(window1.get());
std::unique_ptr<aura::Window> window2(CreateWindow(bounds));
ToggleOverview();
const gfx::Rect keyboard_bounds =
keyboard_controller()->GetKeyboardWindow()->GetBoundsInScreen();
split_view_controller()->SnapWindow(window1.get(), SnapPosition::kSecondary);
EXPECT_TRUE(split_view_controller()->InClamshellSplitViewMode());
// Focus the bottom client to show the virtual keyboard. Test no crash.
auto* keyboard_controller = keyboard::KeyboardUIController::Get();
input_client->set_caret_bounds(gfx::Rect(keyboard_bounds.top_center(),
gfx::Size(0, kCaretHeightForTest)));
input_client->Focus();
EXPECT_TRUE(keyboard_controller->IsKeyboardVisible());
}
} // namespace ash