| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/wm/snap_group/snap_group.h" |
| |
| #include "ash/display/display_move_window_util.h" |
| #include "ash/screen_util.h" |
| #include "ash/shell.h" |
| #include "ash/style/icon_button.h" |
| #include "ash/wm/desks/desk_action_context_menu.h" |
| #include "ash/wm/desks/desks_test_api.h" |
| #include "ash/wm/desks/desks_test_util.h" |
| #include "ash/wm/desks/overview_desk_bar_view.h" |
| #include "ash/wm/overview/overview_controller.h" |
| #include "ash/wm/overview/overview_grid.h" |
| #include "ash/wm/overview/overview_session.h" |
| #include "ash/wm/overview/overview_test_util.h" |
| #include "ash/wm/overview/overview_utils.h" |
| #include "ash/wm/snap_group/snap_group_controller.h" |
| #include "ash/wm/snap_group/snap_group_test_util.h" |
| #include "ash/wm/splitview/split_view_constants.h" |
| #include "ash/wm/splitview/split_view_setup_view.h" |
| #include "ash/wm/window_state.h" |
| #include "ash/wm/wm_event.h" |
| #include "base/memory/raw_ptr.h" |
| #include "chrome/browser/ash/system_web_apps/system_web_app_manager.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/ui/ash/new_window/chrome_new_window_client.h" |
| #include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_tabstrip.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/browser_window/public/browser_window_interface.h" |
| #include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "chrome/browser/ui/views/frame/tab_strip_view_interface.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip_observer.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "chrome/test/base/ash/util/ash_test_util.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "ui/display/manager/display_manager.h" |
| #include "ui/display/test/display_manager_test_api.h" |
| #include "ui/events/test/event_generator.h" |
| #include "ui/views/controls/button/button.h" |
| #include "ui/views/controls/menu/menu_item_view.h" |
| #include "ui/views/view_utils.h" |
| #include "ui/wm/core/coordinate_conversion.h" |
| #include "ui/wm/core/window_util.h" |
| |
| namespace { |
| |
| void ClickButton(const views::Button* button) { |
| CHECK(button); |
| CHECK(button->GetVisible()); |
| aura::Window* root_window = |
| button->GetWidget()->GetNativeWindow()->GetRootWindow(); |
| ui::test::EventGenerator event_generator(root_window); |
| event_generator.MoveMouseToInHost(button->GetBoundsInScreen().CenterPoint()); |
| event_generator.ClickLeftButton(); |
| } |
| |
| const GURL& GetActiveUrl(Browser* browser) { |
| return browser->tab_strip_model() |
| ->GetActiveWebContents() |
| ->GetLastCommittedURL(); |
| } |
| |
| // This class observes the `TabStripModelObserver` and reacts with predetermined |
| // actions to manage tab behavior. In particular, it finalizes tab detachment |
| // by releasing the left mouse button when a tab strip is removed from a window. |
| class TabRemoveObserver : public TabStripModelObserver { |
| public: |
| TabRemoveObserver(Browser* browser, ui::test::EventGenerator* event_generator) |
| : browser_(browser), event_generator_(event_generator) { |
| browser_->tab_strip_model()->AddObserver(this); |
| } |
| TabRemoveObserver(const TabRemoveObserver&) = delete; |
| TabRemoveObserver& operator=(const TabRemoveObserver&) = delete; |
| ~TabRemoveObserver() override { |
| browser_->tab_strip_model()->RemoveObserver(this); |
| } |
| |
| // TabStripModelObserver: |
| void OnTabWillBeRemoved(content::WebContents* contents, int index) override { |
| // Tab detachment is asynchronous. Release the mouse button after the tab |
| // move is done. |
| event_generator_->ReleaseLeftButton(); |
| } |
| |
| private: |
| raw_ptr<Browser> browser_; |
| raw_ptr<ui::test::EventGenerator> event_generator_; |
| }; |
| |
| } // namespace |
| |
| // ----------------------------------------------------------------------------- |
| // FasterSplitScreenBrowserTest: |
| |
| using FasterSplitScreenBrowserTest = InProcessBrowserTest; |
| |
| // Tests that if partial overview is active, and a window gets session |
| // restore'd, partial overview auto-snaps the window. See b/314816288. |
| IN_PROC_BROWSER_TEST_F(FasterSplitScreenBrowserTest, |
| AutoSnapWhileInSessionRestore) { |
| // Create two browser windows and snap `window1` to start partial overview. |
| aura::Window* window1 = browser()->window()->GetNativeWindow(); |
| ash::WindowState* window_state = ash::WindowState::Get(window1); |
| CreateBrowser(browser()->profile()); |
| |
| const ash::WindowSnapWMEvent primary_snap_event( |
| ash::WM_EVENT_SNAP_PRIMARY, |
| ash::WindowSnapActionSource::kSnapByWindowLayoutMenu); |
| window_state->OnWMEvent(&primary_snap_event); |
| ash::WaitForOverviewEntered(); |
| ASSERT_TRUE(ash::OverviewController::Get()->InOverviewSession()); |
| |
| // Open a new browser window. Test it gets auto-snapped. |
| Browser* browser3 = CreateBrowser(browser()->profile()); |
| aura::Window* window3 = browser3->window()->GetNativeWindow(); |
| EXPECT_TRUE(ash::WindowState::Get(window3)->IsSnapped()); |
| EXPECT_FALSE(ash::OverviewController::Get()->InOverviewSession()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FasterSplitScreenBrowserTest, |
| SnapWindowWithNewSettings) { |
| // Install the Settings App. |
| ash::SystemWebAppManager::GetForTest(browser()->profile()) |
| ->InstallSystemAppsForTesting(); |
| |
| // Create two browser windows and snap `window` to start partial overview. |
| aura::Window* window = browser()->window()->GetNativeWindow(); |
| CreateBrowser(browser()->profile()); |
| ash::WindowState* window_state = ash::WindowState::Get(window); |
| const ash::WindowSnapWMEvent primary_snap_event( |
| ash::WM_EVENT_SNAP_PRIMARY, |
| ash::WindowSnapActionSource::kSnapByWindowLayoutMenu); |
| window_state->OnWMEvent(&primary_snap_event); |
| ash::WaitForOverviewEntered(); |
| ASSERT_TRUE(ash::OverviewController::Get()->InOverviewSession()); |
| |
| // Partial overview contains the settings button. |
| auto* overview_grid = |
| ash::OverviewController::Get()->overview_session()->GetGridWithRootWindow( |
| window->GetRootWindow()); |
| ASSERT_TRUE(overview_grid); |
| |
| auto* split_view_setup_view = overview_grid->GetSplitViewSetupView(); |
| ASSERT_TRUE(split_view_setup_view); |
| views::Button* settings_button = const_cast<views::Button*>( |
| views::AsViewClass<views::Button>(split_view_setup_view->GetViewByID( |
| ash::SplitViewSetupView::kSettingsButtonIDForTest))); |
| ASSERT_TRUE(settings_button); |
| |
| // Setup navigation observer to wait for the OS Settings page. |
| constexpr char kOsSettingsUrl[] = |
| "chrome://os-settings/systemPreferences?settingId=1900"; |
| GURL os_settings(kOsSettingsUrl); |
| content::TestNavigationObserver navigation_observer(os_settings); |
| navigation_observer.StartWatchingNewWebContents(); |
| |
| // Click the overview settings button. |
| ClickButton(settings_button); |
| |
| // Wait for OS Settings to open. |
| navigation_observer.Wait(); |
| |
| // Verify correct OS Settings page is opened. |
| Browser* settings_browser = ash::FindSystemWebAppBrowser( |
| browser()->profile(), ash::SystemWebAppType::SETTINGS); |
| ASSERT_TRUE(settings_browser); |
| ASSERT_EQ(os_settings, GetActiveUrl(settings_browser)); |
| } |
| |
| // ----------------------------------------------------------------------------- |
| // SnapGroupBrowserTest: |
| |
| using SnapGroupBrowserTest = InProcessBrowserTest; |
| |
| // Tests that creating a snap group in a rotated display works correctly. |
| // Regression test for http://335323173. |
| // Test layout, where Display 2 is rotated such that Files is physically on top |
| // and Settings on bottom: |
| // +----------------+----------+ |
| // | | | |
| // | Display 1 | Files | |
| // | | | |
| // +----------------|----------| |
| // | | |
| // | Settings | |
| // | | |
| // +----------+ |
| IN_PROC_BROWSER_TEST_F(SnapGroupBrowserTest, RotatedSnapGroup) { |
| auto* display_manager = ash::Shell::Get()->display_manager(); |
| display::test::DisplayManagerTestApi(display_manager) |
| .UpdateDisplay("0+0-800x600,800+0-1200x900/r"); |
| display::test::DisplayManagerTestApi display_manager_test(display_manager); |
| |
| const auto& displays = display_manager->active_display_list(); |
| const display::Display display2 = displays[1]; |
| ASSERT_EQ(display2.id(), display_manager_test.GetSecondaryDisplay().id()); |
| ASSERT_EQ(chromeos::OrientationType::kPortraitSecondary, |
| chromeos::GetDisplayCurrentOrientation(display2)); |
| |
| // Open Files and Settings app windows. |
| Profile* profile = ProfileManager::GetActiveUserProfile(); |
| ash::test::InstallSystemAppsForTesting(profile); |
| |
| ash::test::CreateSystemWebApp(profile, ash::SystemWebAppType::FILE_MANAGER); |
| aura::Window* w1 = GetLastActiveBrowserWindowInterfaceWithAnyProfile() |
| ->GetWindow() |
| ->GetNativeWindow(); |
| ash::display_move_window_util::HandleMoveActiveWindowBetweenDisplays(); |
| auto* root2 = ash::Shell::GetAllRootWindows()[1].get(); |
| ASSERT_EQ(root2, w1->GetRootWindow()); |
| |
| ash::test::CreateSystemWebApp(profile, ash::SystemWebAppType::SETTINGS); |
| aura::Window* w2 = GetLastActiveBrowserWindowInterfaceWithAnyProfile() |
| ->GetWindow() |
| ->GetNativeWindow(); |
| ash::display_move_window_util::HandleMoveActiveWindowBetweenDisplays(); |
| ASSERT_EQ(root2, w1->GetRootWindow()); |
| |
| // Snap Files to secondary which is physically on top. |
| ash::WindowState* window_state1 = ash::WindowState::Get(w1); |
| const ash::WindowSnapWMEvent primary_snap_event( |
| ash::WM_EVENT_SNAP_SECONDARY, |
| ash::WindowSnapActionSource::kDragWindowToEdgeToSnap); |
| window_state1->OnWMEvent(&primary_snap_event); |
| ash::WaitForOverviewEntered(); |
| ASSERT_TRUE(ash::OverviewController::Get()->InOverviewSession()); |
| |
| // Test the bounds are updated. Rects are also hardcoded for readability. |
| // Note `display::Display::work_area()` is still relative to the screen, i.e. |
| // not rotated. |
| const gfx::Rect work_area2 = |
| ash::screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer( |
| w1); |
| ASSERT_EQ(gfx::Rect(800, 0, 900, 1152), work_area2); |
| gfx::Rect top_half, bottom_half; |
| work_area2.SplitHorizontally(top_half, bottom_half); |
| ASSERT_EQ(chromeos::WindowStateType::kSecondarySnapped, |
| ash::WindowState::Get(w1)->GetStateType()); |
| EXPECT_EQ(top_half, w1->GetBoundsInScreen()); |
| |
| // Activate `w2` to simulate clicking it from overview. |
| wm::ActivateWindow(w2); |
| ash::WaitForOverviewExitAnimation(); |
| ASSERT_EQ(chromeos::WindowStateType::kPrimarySnapped, |
| ash::WindowState::Get(w2)->GetStateType()); |
| EXPECT_TRUE(ash::SnapGroupController::Get()->AreWindowsInSnapGroup(w1, w2)); |
| |
| // Test the bounds are updated. Rects are also hardcoded for readability. |
| const gfx::Rect divider_bounds( |
| work_area2.x(), |
| work_area2.CenterPoint().y() - ash::kSplitviewDividerShortSideLength / 2, |
| work_area2.width(), ash::kSplitviewDividerShortSideLength); |
| ASSERT_EQ(gfx::Rect(800, 573, 900, 6), divider_bounds); |
| EXPECT_EQ(divider_bounds, ash::SnapGroupController::Get() |
| ->GetSnapGroupForGivenWindow(w1) |
| ->snap_group_divider() |
| ->GetDividerBoundsInScreen( |
| /*is_dragging=*/false)); |
| top_half.Subtract(divider_bounds); |
| bottom_half.Subtract(divider_bounds); |
| ASSERT_EQ(gfx::Rect(800, 0, 900, 573), top_half); |
| EXPECT_EQ(top_half, w1->GetBoundsInScreen()); |
| ASSERT_EQ(gfx::Rect(800, 579, 900, 573), bottom_half); |
| EXPECT_EQ(bottom_half, w2->GetBoundsInScreen()); |
| } |
| |
| // Verify that dragging a tab within a Snap Group window does not break the |
| // group. |
| IN_PROC_BROWSER_TEST_F(SnapGroupBrowserTest, DoNotBreakGroupOnTabDragging) { |
| aura::Window* window1 = browser()->window()->GetNativeWindow(); |
| chrome::AddTabAt(browser(), GURL(chrome::kChromeUITabSearchURL), -1, true); |
| ASSERT_EQ(2, browser()->tab_strip_model()->GetTabCount()); |
| |
| aura::Window* window2 = |
| CreateBrowser(browser()->profile())->window()->GetNativeWindow(); |
| |
| aura::Window* root_window = ash::Shell::GetPrimaryRootWindow(); |
| ui::test::EventGenerator event_generator(root_window); |
| ash::SnapTwoTestWindows(window1, window2, /*horizontal=*/true, |
| &event_generator); |
| ASSERT_TRUE( |
| ash::SnapGroupController::Get()->AreWindowsInSnapGroup(window1, window2)); |
| |
| TabStripViewInterface* tab_strip_view = |
| BrowserView::GetBrowserViewForBrowser(browser())->tab_strip_view(); |
| const auto start_point = |
| tab_strip_view->GetTabAnchorViewAt(1)->GetBoundsInScreen().CenterPoint(); |
| const auto end_point = |
| tab_strip_view->GetTabAnchorViewAt(0)->GetBoundsInScreen().left_center(); |
| event_generator.MoveMouseTo(start_point); |
| event_generator.PressLeftButton(); |
| event_generator.MoveMouseTo(end_point); |
| event_generator.ReleaseLeftButton(); |
| |
| EXPECT_EQ(2u, chrome::GetTotalBrowserCount()); |
| EXPECT_TRUE( |
| ash::SnapGroupController::Get()->AreWindowsInSnapGroup(window1, window2)); |
| } |
| |
| // Verify that detaching a tab from a window within a Snap Group doesn't break |
| // the group. |
| IN_PROC_BROWSER_TEST_F(SnapGroupBrowserTest, DoNotBreakGroupOnTabDetaching) { |
| aura::Window* window1 = browser()->window()->GetNativeWindow(); |
| chrome::AddTabAt(browser(), GURL(chrome::kChromeUITabSearchURL), -1, true); |
| ASSERT_EQ(2, browser()->tab_strip_model()->GetTabCount()); |
| |
| aura::Window* window2 = |
| CreateBrowser(browser()->profile())->window()->GetNativeWindow(); |
| |
| ui::test::EventGenerator event_generator(ash::Shell::GetPrimaryRootWindow()); |
| ash::SnapTwoTestWindows(window1, window2, /*horizontal=*/true, |
| &event_generator); |
| ASSERT_TRUE( |
| ash::SnapGroupController::Get()->AreWindowsInSnapGroup(window1, window2)); |
| |
| ASSERT_EQ(2u, chrome::GetTotalBrowserCount()); |
| |
| TabStripViewInterface* tab_strip_view = |
| BrowserView::GetBrowserViewForBrowser(browser())->tab_strip_view(); |
| const gfx::Point start_point = |
| tab_strip_view->GetTabAnchorViewAt(1)->GetBoundsInScreen().CenterPoint(); |
| const gfx::Point end_point = window2->GetBoundsInScreen().CenterPoint(); |
| event_generator.MoveMouseTo(start_point); |
| event_generator.PressLeftButton(); |
| TabRemoveObserver observer(browser(), &event_generator); |
| event_generator.MoveMouseTo(end_point); |
| |
| // Verify that detaching a tab results in a new window being created and that |
| // `window1` and `window2` still belong to the Snap Group. |
| EXPECT_EQ(3u, chrome::GetTotalBrowserCount()); |
| EXPECT_TRUE( |
| ash::SnapGroupController::Get()->AreWindowsInSnapGroup(window1, window2)); |
| } |
| |
| // Test that "Save Desk for Later" is not supported when both windows in a Snap |
| // Group are in incognito mode. |
| IN_PROC_BROWSER_TEST_F(SnapGroupBrowserTest, |
| SaveDeskForLaterWithTwoIncognitoWindows) { |
| auto* desks_controller = ash::DesksController::Get(); |
| desks_controller->NewDesk(ash::DesksCreationRemovalSource::kButton); |
| const auto& desks = desks_controller->desks(); |
| ASSERT_EQ(2u, desks.size()); |
| aura::Window* root_window = ash::Shell::GetPrimaryRootWindow(); |
| |
| // Explicitly move the default non-incognito browser window to another desk. |
| aura::Window* window = browser()->window()->GetNativeWindow(); |
| desks_controller->MoveWindowFromActiveDeskTo( |
| window, desks[1].get(), root_window, |
| ash::DesksMoveWindowFromActiveDeskSource::kShortcut); |
| |
| // Create a Snap Group with two incognito browser windows. |
| Browser* incognito_browser1 = CreateIncognitoBrowser(); |
| aura::Window* window1 = incognito_browser1->window()->GetNativeWindow(); |
| Browser* incognito_browser2 = CreateIncognitoBrowser(); |
| aura::Window* window2 = incognito_browser2->window()->GetNativeWindow(); |
| |
| ui::test::EventGenerator event_generator(root_window); |
| ash::SnapTwoTestWindows(window1, window2, /*horizontal=*/true, |
| &event_generator); |
| ash::OverviewController::Get()->StartOverview( |
| ash::OverviewStartAction::kOverviewButton); |
| ash::WaitForOverviewEntered(); |
| ASSERT_TRUE(ash::IsInOverviewSession()); |
| |
| ASSERT_EQ(2u, ash::GetPrimaryRootDesksBarView()->mini_views().size()); |
| ash::OverviewGrid* overview_grid = ash::GetOverviewGridForRoot(root_window); |
| EXPECT_EQ(1u, overview_grid->item_list().size()); |
| auto* desks_bar_view0 = overview_grid->desks_bar_view(); |
| ASSERT_TRUE(desks_bar_view0); |
| ash::DeskMiniView* desk_mini_view0 = desks_bar_view0->mini_views()[0]; |
| ASSERT_TRUE(desk_mini_view0); |
| |
| event_generator.MoveMouseTo( |
| desk_mini_view0->GetBoundsInScreen().CenterPoint()); |
| event_generator.ClickRightButton(); |
| |
| // Activate the desk mini view to enable context menu. |
| ash::DeskActionContextMenu* mini_view_menu0 = desk_mini_view0->context_menu(); |
| ASSERT_TRUE(mini_view_menu0); |
| |
| const views::MenuItemView* save_for_later_item = |
| ash::DesksTestApi::GetDeskActionContextMenuItem( |
| mini_view_menu0, |
| ash::DeskActionContextMenu::CommandId::kSaveForLater); |
| ASSERT_TRUE(save_for_later_item); |
| |
| // Click on the "Save Desk for Later" button in the context menu. |
| event_generator.MoveMouseTo( |
| save_for_later_item->GetBoundsInScreen().CenterPoint()); |
| event_generator.ClickLeftButton(); |
| |
| // Verify that the "Save to Desk" feature is not supported as both windows |
| // within a Snap Group are in incognito mode. |
| EXPECT_TRUE(ash::IsInOverviewSession()); |
| EXPECT_EQ(1u, overview_grid->item_list().size()); |
| EXPECT_EQ(3u, chrome::GetTotalBrowserCount()); |
| } |