| // Copyright 2026 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "chrome/browser/devtools/protocol/browser_handler_android.h" | |
| #include <cstdint> | |
| #include <memory> | |
| #include <optional> | |
| #include <utility> | |
| #include "base/android/jni_android.h" | |
| #include "base/auto_reset.h" | |
| #include "base/callback_list.h" | |
| #include "base/functional/bind.h" | |
| #include "base/functional/callback.h" | |
| #include "chrome/browser/android/tab_android.h" | |
| #include "chrome/browser/ui/android/tab_model/tab_model_test_helper.h" | |
| #include "chrome/browser/ui/browser_window/public/browser_window_interface.h" | |
| #include "chrome/browser/ui/browser_window/test/native_unit_test_support_jni/BrowserWindowInterfaceIteratorAndroidNativeUnitTestSupport_jni.h" | |
| #include "chrome/test/base/chrome_render_view_host_test_harness.h" | |
| #include "components/sessions/content/session_tab_helper.h" | |
| #include "components/sessions/core/session_id.h" | |
| #include "content/public/browser/web_contents.h" | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| #include "ui/base/base_window.h" | |
| #include "ui/base/mojom/window_show_state.mojom.h" | |
| #include "ui/gfx/geometry/rect.h" | |
| namespace { | |
| // Minimal ui::BaseWindow stub for bounds and state reads and mutations. | |
| class FakeBaseWindow : public ui::BaseWindow { | |
| public: | |
| FakeBaseWindow(gfx::Rect bounds, | |
| gfx::Rect restored_bounds, | |
| bool fullscreen, | |
| bool maximized, | |
| bool minimized, | |
| bool can_resize = true) | |
| : bounds_(bounds), | |
| restored_bounds_(restored_bounds), | |
| fullscreen_(fullscreen), | |
| maximized_(maximized), | |
| minimized_(minimized), | |
| resize_precheck_result_( | |
| can_resize | |
| ? ui::WindowResizePrecheckResult::kOk | |
| : ui::WindowResizePrecheckResult::kAndroidNotAFreeformWindow) {} | |
| bool IsActive() const override { return false; } | |
| bool IsMaximized() const override { return maximized_; } | |
| bool IsMinimized() const override { return minimized_; } | |
| bool IsFullscreen() const override { return fullscreen_; } | |
| gfx::NativeWindow GetNativeWindow() const override { return {}; } | |
| gfx::Rect GetRestoredBounds() const override { return restored_bounds_; } | |
| ui::mojom::WindowShowState GetRestoredState() const override { | |
| return ui::mojom::WindowShowState::kNormal; | |
| } | |
| gfx::Rect GetBounds() const override { return bounds_; } | |
| void Show() override {} | |
| void Hide() override {} | |
| bool IsVisible() const override { return true; } | |
| void ShowInactive() override {} | |
| void Close() override {} | |
| void Activate() override {} | |
| void Deactivate() override {} | |
| bool CanResize(ui::WindowResizePrecheckResult& result) const override { | |
| result = resize_precheck_result_; | |
| return result == ui::WindowResizePrecheckResult::kOk; | |
| } | |
| void Maximize() override { | |
| maximized_ = true; | |
| minimized_ = false; | |
| } | |
| void Minimize() override { | |
| maximized_ = false; | |
| minimized_ = true; | |
| } | |
| void Restore() override { | |
| maximized_ = false; | |
| minimized_ = false; | |
| } | |
| void SetBounds(const gfx::Rect& bounds) override { | |
| bounds_ = bounds; | |
| maximized_ = false; | |
| minimized_ = false; | |
| } | |
| void FlashFrame(bool flash) override {} | |
| ui::ZOrderLevel GetZOrderLevel() const override { | |
| return ui::ZOrderLevel::kNormal; | |
| } | |
| void SetZOrderLevel(ui::ZOrderLevel order) override {} | |
| void SetResizePrecheckResult(ui::WindowResizePrecheckResult result) { | |
| resize_precheck_result_ = result; | |
| } | |
| private: | |
| gfx::Rect bounds_; | |
| gfx::Rect restored_bounds_; | |
| bool fullscreen_; | |
| bool maximized_; | |
| bool minimized_; | |
| ui::WindowResizePrecheckResult resize_precheck_result_; | |
| }; | |
| class FakeBrowserWindowInterface : public BrowserWindowInterface { | |
| public: | |
| FakeBrowserWindowInterface(Profile* profile, | |
| SessionID session_id, | |
| ui::BaseWindow* window) | |
| : profile_(profile), session_id_(session_id), window_(window) {} | |
| ~FakeBrowserWindowInterface() override { | |
| browser_did_close_callback_list_.Notify(this); | |
| } | |
| Profile* GetProfile() override { return profile_; } | |
| const Profile* GetProfile() const override { return profile_; } | |
| const SessionID& GetSessionID() const override { return session_id_; } | |
| bool IsDeleteScheduled() const override { return false; } | |
| base::CallbackListSubscription RegisterBrowserDidClose( | |
| BrowserDidCloseCallback callback) override { | |
| return browser_did_close_callback_list_.Add(std::move(callback)); | |
| } | |
| ui::UnownedUserDataHost& GetUnownedUserDataHost() override { | |
| return user_data_host_; | |
| } | |
| const ui::UnownedUserDataHost& GetUnownedUserDataHost() const override { | |
| return user_data_host_; | |
| } | |
| Type GetType() const override { return Type::TYPE_NORMAL; } | |
| ui::BaseWindow* GetWindow() override { return window_; } | |
| const ui::BaseWindow* GetWindow() const override { return window_; } | |
| content::WebContents* OpenURL( | |
| const content::OpenURLParams&, | |
| base::OnceCallback<void(content::NavigationHandle&)>) override { | |
| return nullptr; | |
| } | |
| base::WeakPtr<BrowserWindowInterface> GetWeakPtr() override { | |
| return weak_ptr_factory_.GetWeakPtr(); | |
| } | |
| private: | |
| raw_ptr<Profile> profile_; | |
| const SessionID session_id_; | |
| raw_ptr<ui::BaseWindow> window_; | |
| ui::UnownedUserDataHost user_data_host_; | |
| base::RepeatingCallbackList<void(BrowserWindowInterface*)> | |
| browser_did_close_callback_list_; | |
| base::WeakPtrFactory<FakeBrowserWindowInterface> weak_ptr_factory_{this}; | |
| }; | |
| class TestFrontendChannel : public protocol::FrontendChannel { | |
| public: | |
| void SendProtocolResponse( | |
| int call_id, | |
| std::unique_ptr<protocol::Serializable> message) override {} | |
| void SendProtocolNotification( | |
| std::unique_ptr<protocol::Serializable> message) override {} | |
| void FlushProtocolNotifications() override {} | |
| }; | |
| } // namespace | |
| class BrowserHandlerAndroidTest : public ChromeRenderViewHostTestHarness {}; | |
| TEST_F(BrowserHandlerAndroidTest, PrefersTabWindowIdOverTabModelSessionId) { | |
| OwningTestTabModel tab_model(profile()); | |
| TabAndroid* tab = | |
| tab_model.AddTabFromWebContents(CreateTestWebContents(), /*index=*/0); | |
| sessions::SessionTabHelper::CreateForWebContents(tab->web_contents(), | |
| base::NullCallback()); | |
| SessionID tab_window_id = SessionID::NewUnique(); | |
| tab->SetWindowSessionID(tab_window_id); | |
| ASSERT_NE(tab_model.GetSessionId(), tab_window_id); | |
| std::optional<int> resolved = | |
| BrowserHandlerAndroid::ResolveWindowIdForWebContents(tab->web_contents()); | |
| ASSERT_TRUE(resolved.has_value()); | |
| EXPECT_EQ(tab_window_id.id(), resolved.value()); | |
| } | |
| TEST_F(BrowserHandlerAndroidTest, | |
| FallsBackToTabModelSessionIdWhenTabHasNoWindowId) { | |
| OwningTestTabModel tab_model(profile()); | |
| TabAndroid* tab = | |
| tab_model.AddTabFromWebContents(CreateTestWebContents(), /*index=*/0); | |
| // Tab carries no explicit window id; resolver should fall back to the | |
| // owning TabModel's session id. | |
| ASSERT_FALSE(tab->GetWindowId().is_valid()); | |
| std::optional<int> resolved = | |
| BrowserHandlerAndroid::ResolveWindowIdForWebContents(tab->web_contents()); | |
| ASSERT_TRUE(resolved.has_value()); | |
| EXPECT_EQ(tab_model.GetSessionId().id(), resolved.value()); | |
| } | |
| TEST_F(BrowserHandlerAndroidTest, ReturnsNulloptWhenWebContentsUnknown) { | |
| std::unique_ptr<content::WebContents> standalone = CreateTestWebContents(); | |
| std::optional<int> resolved = | |
| BrowserHandlerAndroid::ResolveWindowIdForWebContents(standalone.get()); | |
| EXPECT_FALSE(resolved.has_value()); | |
| } | |
| TEST(BrowserHandlerAndroidStateTest, FullscreenWinsOverEverything) { | |
| EXPECT_EQ("fullscreen", BrowserHandlerAndroid::ComputeWindowStateString( | |
| /*is_fullscreen=*/true, | |
| /*is_maximized=*/false, | |
| /*is_minimized=*/false)); | |
| EXPECT_EQ("fullscreen", BrowserHandlerAndroid::ComputeWindowStateString( | |
| /*is_fullscreen=*/true, | |
| /*is_maximized=*/true, | |
| /*is_minimized=*/false)); | |
| EXPECT_EQ("fullscreen", BrowserHandlerAndroid::ComputeWindowStateString( | |
| /*is_fullscreen=*/true, | |
| /*is_maximized=*/false, | |
| /*is_minimized=*/true)); | |
| EXPECT_EQ("fullscreen", BrowserHandlerAndroid::ComputeWindowStateString( | |
| /*is_fullscreen=*/true, | |
| /*is_maximized=*/true, | |
| /*is_minimized=*/true)); | |
| } | |
| TEST(BrowserHandlerAndroidStateTest, MaximizedBeatsMinimizedWhenNotFullscreen) { | |
| EXPECT_EQ("maximized", BrowserHandlerAndroid::ComputeWindowStateString( | |
| /*is_fullscreen=*/false, | |
| /*is_maximized=*/true, | |
| /*is_minimized=*/true)); | |
| } | |
| TEST(BrowserHandlerAndroidStateTest, MinimizedWhenOnlyMinimized) { | |
| EXPECT_EQ("minimized", BrowserHandlerAndroid::ComputeWindowStateString( | |
| /*is_fullscreen=*/false, | |
| /*is_maximized=*/false, | |
| /*is_minimized=*/true)); | |
| } | |
| TEST(BrowserHandlerAndroidStateTest, NormalWhenAllFalse) { | |
| EXPECT_EQ("normal", BrowserHandlerAndroid::ComputeWindowStateString( | |
| /*is_fullscreen=*/false, | |
| /*is_maximized=*/false, | |
| /*is_minimized=*/false)); | |
| } | |
| TEST_F(BrowserHandlerAndroidTest, | |
| FindBrowserWindowByIdReturnsWindowForRegisteredBwi) { | |
| // Positive case: when a live AndroidBrowserWindow owns a session id, | |
| // FindBrowserWindowById returns its ui::BaseWindow. | |
| BrowserWindowInterface* live_window = reinterpret_cast< | |
| BrowserWindowInterface*>( | |
| Java_BrowserWindowInterfaceIteratorAndroidNativeUnitTestSupport_createBrowserWindow( | |
| base::android::AttachCurrentThread(), /*taskId=*/1, | |
| profile()->GetJavaObject())); | |
| ASSERT_NE(nullptr, live_window); | |
| SessionID live_id = live_window->GetSessionID(); | |
| ui::BaseWindow* resolved = | |
| BrowserHandlerAndroid::FindBrowserWindowById(live_id.id()); | |
| EXPECT_EQ(live_window->GetWindow(), resolved); | |
| Java_BrowserWindowInterfaceIteratorAndroidNativeUnitTestSupport_destroyBrowserWindow( | |
| base::android::AttachCurrentThread(), /*taskId=*/1); | |
| } | |
| TEST_F(BrowserHandlerAndroidTest, | |
| FindBrowserWindowByIdReturnsNullForUnregisteredWindowId) { | |
| // An id without a matching BWI returns nullptr even when other BWIs are | |
| // registered. | |
| BrowserWindowInterface* live_window = reinterpret_cast< | |
| BrowserWindowInterface*>( | |
| Java_BrowserWindowInterfaceIteratorAndroidNativeUnitTestSupport_createBrowserWindow( | |
| base::android::AttachCurrentThread(), /*taskId=*/1, | |
| profile()->GetJavaObject())); | |
| ASSERT_NE(nullptr, live_window); | |
| SessionID unregistered_window_id = SessionID::NewUnique(); | |
| ASSERT_NE(live_window->GetSessionID(), unregistered_window_id); | |
| EXPECT_EQ(nullptr, BrowserHandlerAndroid::FindBrowserWindowById( | |
| unregistered_window_id.id())); | |
| Java_BrowserWindowInterfaceIteratorAndroidNativeUnitTestSupport_destroyBrowserWindow( | |
| base::android::AttachCurrentThread(), /*taskId=*/1); | |
| } | |
| TEST_F(BrowserHandlerAndroidTest, | |
| GetWindowBoundsReturnsErrorForUnknownWindowId) { | |
| TestFrontendChannel channel; | |
| protocol::UberDispatcher dispatcher(&channel); | |
| BrowserHandlerAndroid handler(&dispatcher, /*target_id=*/""); | |
| std::unique_ptr<protocol::Browser::Bounds> bounds; | |
| protocol::Response response = | |
| handler.GetWindowBounds(SessionID::NewUnique().id(), &bounds); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ("Browser window not found", response.Message()); | |
| EXPECT_EQ(nullptr, bounds); | |
| } | |
| TEST_F(BrowserHandlerAndroidTest, | |
| GetWindowBoundsUsesWindowTrackedByDevToolsSession) { | |
| TestFrontendChannel channel; | |
| protocol::UberDispatcher dispatcher(&channel); | |
| BrowserHandlerAndroid handler(&dispatcher, /*target_id=*/""); | |
| FakeBaseWindow base_window(/*bounds=*/gfx::Rect(10, 20, 800, 600), | |
| /*restored_bounds=*/gfx::Rect(), | |
| /*fullscreen=*/false, | |
| /*maximized=*/true, | |
| /*minimized=*/false); | |
| const SessionID session_id = SessionID::NewUnique(); | |
| FakeBrowserWindowInterface browser_window(profile(), session_id, | |
| &base_window); | |
| ASSERT_EQ(nullptr, | |
| BrowserHandlerAndroid::FindBrowserWindowById(session_id.id())); | |
| handler.TrackBrowserWindow(&browser_window); | |
| std::unique_ptr<protocol::Browser::Bounds> bounds; | |
| protocol::Response response = | |
| handler.GetWindowBounds(session_id.id(), &bounds); | |
| EXPECT_TRUE(response.IsSuccess()); | |
| ASSERT_NE(nullptr, bounds); | |
| EXPECT_EQ(10, bounds->GetLeft()); | |
| EXPECT_EQ(20, bounds->GetTop()); | |
| EXPECT_EQ(800, bounds->GetWidth()); | |
| EXPECT_EQ(600, bounds->GetHeight()); | |
| EXPECT_EQ("maximized", bounds->GetWindowState()); | |
| } | |
| TEST_F(BrowserHandlerAndroidTest, | |
| GetWindowBoundsRejectsDestroyedTrackedWindow) { | |
| TestFrontendChannel channel; | |
| protocol::UberDispatcher dispatcher(&channel); | |
| BrowserHandlerAndroid handler(&dispatcher, /*target_id=*/""); | |
| FakeBaseWindow base_window(/*bounds=*/gfx::Rect(10, 20, 800, 600), | |
| /*restored_bounds=*/gfx::Rect(), | |
| /*fullscreen=*/false, | |
| /*maximized=*/true, | |
| /*minimized=*/false); | |
| const SessionID session_id = SessionID::NewUnique(); | |
| auto browser_window = std::make_unique<FakeBrowserWindowInterface>( | |
| profile(), session_id, &base_window); | |
| handler.TrackBrowserWindow(browser_window.get()); | |
| browser_window.reset(); | |
| std::unique_ptr<protocol::Browser::Bounds> bounds; | |
| protocol::Response response = | |
| handler.GetWindowBounds(session_id.id(), &bounds); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ("Browser window not found", response.Message()); | |
| EXPECT_EQ(nullptr, bounds); | |
| } | |
| TEST_F(BrowserHandlerAndroidTest, | |
| SetWindowBoundsPrefersRegisteredWindowOverTrackedFallback) { | |
| BrowserWindowInterface* live_window = reinterpret_cast< | |
| BrowserWindowInterface*>( | |
| Java_BrowserWindowInterfaceIteratorAndroidNativeUnitTestSupport_createBrowserWindow( | |
| base::android::AttachCurrentThread(), /*taskId=*/1, | |
| profile()->GetJavaObject())); | |
| ASSERT_NE(nullptr, live_window); | |
| TestFrontendChannel channel; | |
| protocol::UberDispatcher dispatcher(&channel); | |
| BrowserHandlerAndroid handler(&dispatcher, /*target_id=*/""); | |
| FakeBaseWindow fallback_window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, | |
| false, false); | |
| fallback_window.SetResizePrecheckResult( | |
| ui::WindowResizePrecheckResult::kAndroidNoActivity); | |
| FakeBrowserWindowInterface tracked_window( | |
| profile(), live_window->GetSessionID(), &fallback_window); | |
| handler.TrackBrowserWindow(&tracked_window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("minimized").Build(); | |
| protocol::Response response = handler.SetWindowBounds( | |
| live_window->GetSessionID().id(), std::move(bounds)); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ( | |
| "Window state or bounds cannot be changed in the current Android " | |
| "configuration", | |
| response.Message()); | |
| EXPECT_FALSE(fallback_window.IsMinimized()); | |
| Java_BrowserWindowInterfaceIteratorAndroidNativeUnitTestSupport_destroyBrowserWindow( | |
| base::android::AttachCurrentThread(), /*taskId=*/1); | |
| } | |
| TEST(BrowserHandlerAndroidBoundsTest, BuildsBoundsAndStateFromWindow) { | |
| FakeBaseWindow window(/*bounds=*/gfx::Rect(10, 20, 800, 600), | |
| /*restored_bounds=*/gfx::Rect(0, 0, 400, 300), | |
| /*fullscreen=*/false, | |
| /*maximized=*/false, | |
| /*minimized=*/false); | |
| auto bounds = BrowserHandlerAndroid::BuildBrowserWindowBounds(&window); | |
| ASSERT_NE(nullptr, bounds); | |
| EXPECT_EQ(10, bounds->GetLeft()); | |
| EXPECT_EQ(20, bounds->GetTop()); | |
| EXPECT_EQ(800, bounds->GetWidth()); | |
| EXPECT_EQ(600, bounds->GetHeight()); | |
| EXPECT_EQ("normal", bounds->GetWindowState("")); | |
| } | |
| TEST(BrowserHandlerAndroidBoundsTest, BuildsRestoredBoundsWhenMinimized) { | |
| FakeBaseWindow window(/*bounds=*/gfx::Rect(10, 20, 800, 600), | |
| /*restored_bounds=*/gfx::Rect(0, 0, 400, 300), | |
| /*fullscreen=*/false, | |
| /*maximized=*/false, | |
| /*minimized=*/true); | |
| auto bounds = BrowserHandlerAndroid::BuildBrowserWindowBounds(&window); | |
| ASSERT_NE(nullptr, bounds); | |
| // Minimized windows report their restored bounds, not current bounds. | |
| EXPECT_EQ(0, bounds->GetLeft()); | |
| EXPECT_EQ(0, bounds->GetTop()); | |
| EXPECT_EQ(400, bounds->GetWidth()); | |
| EXPECT_EQ(300, bounds->GetHeight()); | |
| EXPECT_EQ("minimized", bounds->GetWindowState("")); | |
| } | |
| TEST(BrowserHandlerAndroidBoundsTest, BuildsFullscreenStateAndBounds) { | |
| FakeBaseWindow window(/*bounds=*/gfx::Rect(0, 0, 1920, 1080), | |
| /*restored_bounds=*/gfx::Rect(100, 100, 800, 600), | |
| /*fullscreen=*/true, | |
| /*maximized=*/false, | |
| /*minimized=*/false); | |
| auto bounds = BrowserHandlerAndroid::BuildBrowserWindowBounds(&window); | |
| ASSERT_NE(nullptr, bounds); | |
| EXPECT_EQ(1920, bounds->GetWidth()); | |
| EXPECT_EQ(1080, bounds->GetHeight()); | |
| EXPECT_EQ("fullscreen", bounds->GetWindowState("")); | |
| } | |
| TEST(BrowserHandlerAndroidBoundsTest, | |
| BuildsRestoredBoundsWhenMinimizedAndMaximized) { | |
| FakeBaseWindow window(/*bounds=*/gfx::Rect(10, 20, 800, 600), | |
| /*restored_bounds=*/gfx::Rect(0, 0, 400, 300), | |
| /*fullscreen=*/false, | |
| /*maximized=*/true, | |
| /*minimized=*/true); | |
| auto bounds = BrowserHandlerAndroid::BuildBrowserWindowBounds(&window); | |
| ASSERT_NE(nullptr, bounds); | |
| EXPECT_EQ(0, bounds->GetLeft()); | |
| EXPECT_EQ(0, bounds->GetTop()); | |
| EXPECT_EQ(400, bounds->GetWidth()); | |
| EXPECT_EQ(300, bounds->GetHeight()); | |
| EXPECT_EQ("maximized", bounds->GetWindowState("")); | |
| } | |
| TEST(BrowserHandlerAndroidBoundsTest, | |
| BuildsMaximizedBoundsForNonResizableMinimizedTask) { | |
| FakeBaseWindow window(/*bounds=*/gfx::Rect(10, 20, 800, 600), | |
| /*restored_bounds=*/gfx::Rect(0, 0, 400, 300), | |
| /*fullscreen=*/false, | |
| /*maximized=*/false, | |
| /*minimized=*/true, | |
| /*can_resize=*/false); | |
| auto bounds = BrowserHandlerAndroid::BuildBrowserWindowBounds(&window); | |
| ASSERT_NE(nullptr, bounds); | |
| EXPECT_EQ(10, bounds->GetLeft()); | |
| EXPECT_EQ(20, bounds->GetTop()); | |
| EXPECT_EQ(800, bounds->GetWidth()); | |
| EXPECT_EQ(600, bounds->GetHeight()); | |
| EXPECT_EQ("maximized", bounds->GetWindowState("")); | |
| } | |
| class BrowserHandlerAndroidMutationTest | |
| : public ChromeRenderViewHostTestHarness { | |
| protected: | |
| // Registers `window` as a live (Activity-attached) browser window. | |
| void RegisterLiveWindow(FakeBaseWindow* window) { | |
| session_id_ = SessionID::NewUnique(); | |
| lookup_override_.emplace( | |
| BrowserHandlerAndroid::SetBrowserWindowLookupForTesting( | |
| base::BindRepeating( | |
| [](int live_id, ui::BaseWindow* live_window, | |
| int window_id) -> ui::BaseWindow* { | |
| return window_id == live_id ? live_window : nullptr; | |
| }, | |
| session_id_.id(), base::Unretained(window)))); | |
| } | |
| // Tracks `window` as a DevTools-created window whose Activity is pending. | |
| void TrackPendingWindow(FakeBaseWindow* window) { | |
| session_id_ = SessionID::NewUnique(); | |
| browser_window_ = std::make_unique<FakeBrowserWindowInterface>( | |
| profile(), session_id_, window); | |
| handler_.TrackBrowserWindow(browser_window_.get()); | |
| } | |
| protocol::Response SetWindowBounds( | |
| std::unique_ptr<protocol::Browser::Bounds> bounds) { | |
| return handler_.SetWindowBounds(session_id_.id(), std::move(bounds)); | |
| } | |
| TestFrontendChannel channel_; | |
| protocol::UberDispatcher dispatcher_{&channel_}; | |
| BrowserHandlerAndroid handler_{&dispatcher_, /*target_id=*/""}; | |
| SessionID session_id_ = SessionID::NewUnique(); | |
| std::unique_ptr<FakeBrowserWindowInterface> browser_window_; | |
| std::optional<base::AutoReset<BrowserHandlerAndroid::BrowserWindowLookup>> | |
| lookup_override_; | |
| }; | |
| TEST_F(BrowserHandlerAndroidMutationTest, RejectsUnknownWindow) { | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("maximized").Build(); | |
| protocol::Response response = | |
| handler_.SetWindowBounds(SessionID::NewUnique().id(), std::move(bounds)); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ("Browser window not found", response.Message()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, RejectsBoundsWithNonNormalState) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, false, | |
| false); | |
| RegisterLiveWindow(&window); | |
| auto bounds = protocol::Browser::Bounds::Create() | |
| .SetWidth(400) | |
| .SetWindowState("maximized") | |
| .Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ( | |
| "The 'minimized', 'maximized' and 'fullscreen' states cannot be " | |
| "combined with 'left', 'top', 'width' or 'height'", | |
| response.Message()); | |
| EXPECT_FALSE(window.IsMaximized()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, RejectsFullscreen) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, false, | |
| false); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("fullscreen").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ("Fullscreen not supported on Android", response.Message()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, RejectsInvalidWindowState) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, false, | |
| false); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("invalid").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ("Invalid windowState: invalid", response.Message()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, RejectsMaximizeFromMinimized) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, false, | |
| true); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("maximized").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ( | |
| "To maximize a minimized or fullscreen window, restore it to normal " | |
| "state first.", | |
| response.Message()); | |
| EXPECT_FALSE(window.IsMaximized()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, RejectsMaximizeFromFullscreen) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), true, false, | |
| false); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("maximized").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ( | |
| "To maximize a minimized or fullscreen window, restore it to normal " | |
| "state first.", | |
| response.Message()); | |
| EXPECT_FALSE(window.IsMaximized()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, MaximizesWindow) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, false, | |
| false); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("maximized").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_TRUE(response.IsSuccess()); | |
| EXPECT_TRUE(window.IsMaximized()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, | |
| MaximizeIsIdempotentForFixedSizeWindow) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, true, | |
| false, /*can_resize=*/false); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("maximized").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_TRUE(response.IsSuccess()); | |
| EXPECT_TRUE(window.IsMaximized()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, MinimizesWindow) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, false, | |
| false); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("minimized").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_TRUE(response.IsSuccess()); | |
| EXPECT_TRUE(window.IsMinimized()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, MinimizeIsIdempotent) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, false, | |
| true); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("minimized").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_TRUE(response.IsSuccess()); | |
| EXPECT_TRUE(window.IsMinimized()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, MinimizesMaximizedWindow) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, true, | |
| false); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("minimized").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_TRUE(response.IsSuccess()); | |
| EXPECT_FALSE(window.IsMaximized()); | |
| EXPECT_TRUE(window.IsMinimized()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, RejectsMinimizeFromFullscreen) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), true, false, | |
| false); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("minimized").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ( | |
| "To minimize a fullscreen window, restore it to normal state first.", | |
| response.Message()); | |
| EXPECT_FALSE(window.IsMinimized()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, | |
| RejectsMutationForFixedSizeAndroidWindow) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, true, | |
| false, /*can_resize=*/false); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("minimized").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ( | |
| "Window state or bounds cannot be changed in the current Android " | |
| "configuration", | |
| response.Message()); | |
| EXPECT_FALSE(window.IsMinimized()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, | |
| RejectsMutationWhileAndroidWindowIsPending) { | |
| // Resizable, so the rejection comes from the pending check, not CanResize(). | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, false, | |
| false); | |
| TrackPendingWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("minimized").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ( | |
| "Window state or bounds cannot be changed before the Android window " | |
| "is created", | |
| response.Message()); | |
| EXPECT_FALSE(window.IsMinimized()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, | |
| RejectsNormalWithoutBoundsWhileAndroidWindowIsPending) { | |
| // A pending window reports default (normal) state, so "normal" with no | |
| // bounds would otherwise short-circuit to Success without any mutation. | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, false, | |
| false); | |
| TrackPendingWindow(&window); | |
| auto bounds = protocol::Browser::Bounds::Create() // nocheck | |
| .SetWindowState("normal") | |
| .Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ( | |
| "Window state or bounds cannot be changed before the Android window " | |
| "is created", | |
| response.Message()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, RestoresWindow) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), false, true, | |
| false); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("normal").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_TRUE(response.IsSuccess()); | |
| EXPECT_FALSE(window.IsMaximized()); | |
| EXPECT_FALSE(window.IsMinimized()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, | |
| PartialBoundsPreserveUnspecifiedValuesAndRestoreWindow) { | |
| FakeBaseWindow window(gfx::Rect(10, 20, 800, 600), gfx::Rect(), false, true, | |
| false); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetLeft(30).SetWidth(400).Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_TRUE(response.IsSuccess()); | |
| EXPECT_EQ(gfx::Rect(30, 20, 400, 600), window.GetBounds()); | |
| EXPECT_FALSE(window.IsMaximized()); | |
| } | |
| TEST_F(BrowserHandlerAndroidMutationTest, RejectsNormalFromFullscreen) { | |
| FakeBaseWindow window(gfx::Rect(0, 0, 800, 600), gfx::Rect(), true, false, | |
| false); | |
| RegisterLiveWindow(&window); | |
| auto bounds = | |
| protocol::Browser::Bounds::Create().SetWindowState("normal").Build(); | |
| protocol::Response response = SetWindowBounds(std::move(bounds)); | |
| EXPECT_FALSE(response.IsSuccess()); | |
| EXPECT_EQ("Cannot exit fullscreen on Android", response.Message()); | |
| } |