| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <limits.h> |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <array> |
| #include <memory> |
| #include <string> |
| |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/format_macros.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/run_loop.h" |
| #include "base/strings/pattern.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/to_string.h" |
| #include "base/test/gmock_expected_support.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "base/test/test_future.h" |
| #include "base/test/test_timeouts.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/devtools/devtools_window_testing.h" |
| #include "chrome/browser/extensions/api/tabs/tabs_api.h" |
| #include "chrome/browser/extensions/api/tabs/tabs_constants.h" |
| #include "chrome/browser/extensions/api/tabs/tabs_windows_api.h" |
| #include "chrome/browser/extensions/browser_extension_window_controller.h" |
| #include "chrome/browser/extensions/extension_apitest.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/extensions/profile_util.h" |
| #include "chrome/browser/extensions/window_controller.h" |
| #include "chrome/browser/prefs/incognito_mode_prefs.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/resource_coordinator/tab_lifecycle_unit_external.h" |
| #include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h" |
| #include "chrome/browser/tab_list/tab_list_interface.h" |
| #include "chrome/browser/ui/browser_window/public/browser_window_features.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/tabs/saved_tab_groups/saved_tab_group_utils.h" |
| #include "chrome/browser/ui/ui_features.h" |
| #include "chrome/browser/ui/zoom/chrome_zoom_level_prefs.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "components/policy/core/common/policy_pref_names.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/saved_tab_groups/public/tab_group_sync_service.h" |
| #include "components/split_tabs/split_tab_id.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "extensions/browser/api_test_utils.h" |
| #include "extensions/browser/event_router.h" |
| #include "extensions/browser/extension_function_dispatcher.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "extensions/browser/test_event_router_observer.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/error_utils.h" |
| #include "extensions/common/extension_builder.h" |
| #include "extensions/common/manifest_constants.h" |
| #include "extensions/common/mojom/context_type.mojom.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "extensions/test/result_catcher.h" |
| #include "extensions/test/test_extension_dir.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "pdf/buildflags.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/page/page_zoom.h" |
| #include "ui/base/base_window.h" |
| #include "ui/base/ozone_buildflags.h" |
| #include "ui/base/window_open_disposition.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/views/widget/widget_interactive_uitest_utils.h" |
| #include "url/gurl.h" |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| #include "chrome/browser/resource_coordinator/tab_lifecycle_unit_source.h" |
| #include "chrome/browser/resource_coordinator/tab_manager.h" |
| #include "chrome/browser/resource_coordinator/time.h" |
| #include "chrome/browser/resource_coordinator/utils.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_commands.h" |
| #include "chrome/browser/ui/browser_tabstrip.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/browser_window/public/global_browser_collection.h" |
| #include "chrome/browser/ui/tabs/saved_tab_groups/tab_group_sync_service_initialized_observer.h" |
| #include "chrome/browser/ui/tabs/split_tab_metrics.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/commands/install_isolated_web_app_command.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/install/isolated_web_app_install_source.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_trust_checker.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h" |
| #include "chrome/browser/web_applications/test/web_app_install_test_utils.h" |
| #include "chrome/browser/web_applications/web_app_command_scheduler.h" |
| #include "chrome/browser/web_applications/web_app_provider.h" |
| #include "chrome/browser/web_applications/web_app_provider_factory.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/split_tabs/split_tab_id.h" |
| #include "components/split_tabs/split_tab_visual_data.h" |
| #include "components/webapps/isolated_web_apps/test_support/signing_keys.h" |
| #endif |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "ui/base/test/scoped_fake_nswindow_fullscreen.h" |
| #endif |
| |
| #if BUILDFLAG(ENABLE_PDF) |
| #include "base/test/with_feature_override.h" |
| #include "chrome/browser/pdf/pdf_extension_test_base.h" |
| #include "chrome/browser/pdf/pdf_extension_test_util.h" |
| #include "chrome/browser/pdf/test_mime_handler_stream_manager.h" |
| #include "pdf/pdf_features.h" |
| #endif |
| |
| #if BUILDFLAG(ENABLE_PLATFORM_APPS) |
| #include "chrome/browser/apps/platform_apps/app_browsertest_util.h" |
| #include "extensions/browser/app_window/app_window.h" |
| #include "extensions/browser/app_window/app_window_registry.h" |
| #endif |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "ash/wm/window_pin_util.h" |
| #include "chrome/browser/chromeos/policy/dlp/test/mock_dlp_content_manager.h" |
| #include "chrome/common/pref_names.h" |
| #endif |
| |
| namespace extensions { |
| |
| namespace keys = tabs_constants; |
| namespace utils = api_test_utils; |
| |
| namespace { |
| |
| // Creates a WebContents, attaches it to the tab list, and navigates so we |
| // have `urls` as history. |
| tabs::TabInterface* OpenTabWithHistory(TabListInterface* tab_list, |
| const std::vector<GURL>& urls) { |
| tabs::TabInterface* tab = tab_list->OpenTab(urls[0], -1); |
| content::WebContents* web_contents = tab->GetContents(); |
| content::WaitForLoadStop(web_contents); |
| for (size_t i = 1; i < urls.size(); ++i) { |
| content::TestNavigationObserver observer(web_contents); |
| content::NavigationController::LoadURLParams params(urls[i]); |
| web_contents->GetController().LoadURLWithParams(params); |
| observer.Wait(); |
| } |
| return tab; |
| } |
| |
| struct TabListData { |
| std::vector<int> tab_ids; |
| std::vector<content::WebContents*> web_contentses; |
| }; |
| |
| // Opens tabs in `tab_list` until there are `count` tabs, then returns the tab |
| // list's data (tab IDs and WebContentses). |
| TabListData CreateAndGetTabData(TabListInterface* tab_list, int count) { |
| // Account for the initial tab. |
| for (int i = tab_list->GetTabCount(); i < count; ++i) { |
| tab_list->OpenTab(GURL(url::kAboutBlankURL), -1); |
| } |
| TabListData data; |
| for (int i = 0; i < count; ++i) { |
| content::WebContents* contents = tab_list->GetTab(i)->GetContents(); |
| data.tab_ids.push_back(ExtensionTabUtil::GetTabId(contents)); |
| data.web_contentses.push_back(contents); |
| } |
| return data; |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // Returns true if |val| contains any privacy information, e.g. url, |
| // pendingUrl, title or faviconUrl. |
| bool HasAnyPrivacySensitiveFields(const base::DictValue& dict) { |
| constexpr std::array privacySensitiveKeys{ |
| tabs_constants::kUrlKey, tabs_constants::kTitleKey, |
| tabs_constants::kFaviconUrlKey, tabs_constants::kPendingUrlKey}; |
| for (auto* key : privacySensitiveKeys) { |
| if (dict.contains(key)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| class TestFunctionDispatcherDelegate |
| : public extensions::ExtensionFunctionDispatcher::Delegate { |
| public: |
| explicit TestFunctionDispatcherDelegate(BrowserWindowInterface* browser) |
| : browser_(browser) {} |
| ~TestFunctionDispatcherDelegate() override = default; |
| |
| private: |
| extensions::WindowController* GetExtensionWindowController() override { |
| return BrowserExtensionWindowController::From(browser_); |
| } |
| |
| raw_ptr<BrowserWindowInterface> browser_; |
| }; |
| |
| class ExtensionTabsTest : public ExtensionApiTest { |
| public: |
| ExtensionTabsTest() = default; |
| |
| ExtensionTabsTest(const ExtensionTabsTest&) = delete; |
| ExtensionTabsTest& operator=(const ExtensionTabsTest&) = delete; |
| |
| // ExtensionApiTest overrides: |
| void SetUpOnMainThread() override { |
| ExtensionApiTest::SetUpOnMainThread(); |
| // Map all hosts to localhost. |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| } |
| |
| protected: |
| std::string GetWindowType(BrowserWindowInterface* test_browser, |
| scoped_refptr<const Extension> extension) { |
| auto function = base::MakeRefCounted<WindowsGetFunction>(); |
| function->set_extension(extension.get()); |
| std::string args = base::StringPrintf( |
| R"([%u, {"windowTypes": ["normal", "popup", "devtools", "app"]}])", |
| ExtensionTabUtil::GetWindowId(test_browser)); |
| base::DictValue result = |
| utils::ToDict(utils::RunFunctionAndReturnSingleResult(function.get(), |
| args, profile())); |
| return api_test_utils::GetString(result, "type"); |
| } |
| |
| std::optional<base::Value> RunFunctionWithDispatcherDelegateAndReturnValue( |
| scoped_refptr<ExtensionFunction> function, |
| const std::string& args, |
| BrowserWindowInterface* browser) { |
| auto dispatcher = std::make_unique<extensions::ExtensionFunctionDispatcher>( |
| browser->GetProfile()); |
| TestFunctionDispatcherDelegate dispatcher_delegate(browser); |
| dispatcher->set_delegate(&dispatcher_delegate); |
| return utils::RunFunctionWithDelegateAndReturnSingleResult( |
| std::move(function), args, std::move(dispatcher), |
| utils::FunctionMode::kNone); |
| } |
| |
| content::WebContents* OpenUrlAndWaitForLoad(const GURL& url) { |
| NavigateToURLInNewTab(url); |
| return GetActiveWebContents(); |
| } |
| |
| base::ListValue RunQueryFunction(const Extension* extension, |
| const char* query_info) { |
| auto function = base::MakeRefCounted<TabsQueryFunction>(); |
| function->set_extension(extension); |
| return utils::ToList(utils::RunFunctionAndReturnSingleResult( |
| function.get(), query_info, profile())); |
| } |
| |
| base::DictValue RunUpdateFunction(const Extension* extension, |
| const std::string& update_info) { |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension); |
| return utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| function.get(), update_info, profile())); |
| } |
| }; |
| |
| #if BUILDFLAG(ENABLE_PLATFORM_APPS) |
| |
| using ExtensionTabsTestWithApps = PlatformAppBrowserTest; |
| |
| #endif |
| |
| class ExtensionWindowCreateTest : public ExtensionBrowserTest { |
| public: |
| // Runs chrome.windows.create(), expecting an error. |
| std::string RunCreateWindowExpectError(const std::string& args) { |
| auto function = base::MakeRefCounted<WindowsCreateFunction>(); |
| function->set_extension(ExtensionBuilder("Test").Build().get()); |
| return api_test_utils::RunFunctionAndReturnError(function.get(), args, |
| profile()); |
| } |
| }; |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| const int kUndefinedId = INT_MIN; |
| |
| const ExtensionTabUtil::ScrubTabBehavior kDontScrubBehavior = { |
| ExtensionTabUtil::kDontScrubTab, ExtensionTabUtil::kDontScrubTab}; |
| |
| int GetTabId(const base::DictValue& tab) { |
| return tab.FindInt(extension_misc::kId).value_or(kUndefinedId); |
| } |
| |
| int GetTabWindowId(const base::DictValue& tab) { |
| return tab.FindInt(keys::kWindowIdKey).value_or(kUndefinedId); |
| } |
| |
| int GetWindowId(const base::DictValue& window) { |
| return window.FindInt(extension_misc::kId).value_or(kUndefinedId); |
| } |
| #endif |
| |
| } // namespace |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, GetWindow) { |
| int window_id = ExtensionTabUtil::GetWindowId(browser_window_interface()); |
| |
| // Invalid window ID error. |
| auto function = base::MakeRefCounted<WindowsGetFunction>(); |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| function->set_extension(extension.get()); |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), base::StringPrintf("[%u]", window_id + 1), profile()), |
| ExtensionTabUtil::kWindowNotFoundError)); |
| |
| // Basic window details. |
| gfx::Rect bounds; |
| if (browser_window_interface()->GetWindow()->IsMinimized()) { |
| bounds = browser_window_interface()->GetWindow()->GetRestoredBounds(); |
| } else { |
| bounds = browser_window_interface()->GetWindow()->GetBounds(); |
| } |
| |
| function = base::MakeRefCounted<WindowsGetFunction>(); |
| function->set_extension(extension.get()); |
| base::DictValue result = |
| utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| function.get(), base::StringPrintf("[%u]", window_id), profile())); |
| EXPECT_EQ(window_id, GetWindowId(result)); |
| EXPECT_FALSE(api_test_utils::GetBoolean(result, "incognito")); |
| EXPECT_EQ("normal", api_test_utils::GetString(result, "type")); |
| EXPECT_EQ(bounds.x(), api_test_utils::GetInteger(result, "left")); |
| EXPECT_EQ(bounds.y(), api_test_utils::GetInteger(result, "top")); |
| EXPECT_EQ(bounds.width(), api_test_utils::GetInteger(result, "width")); |
| EXPECT_EQ(bounds.height(), api_test_utils::GetInteger(result, "height")); |
| |
| // With "populate" enabled. |
| function = base::MakeRefCounted<WindowsGetFunction>(); |
| function->set_extension(extension.get()); |
| result = utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| function.get(), |
| base::StringPrintf("[%u, {\"populate\": true}]", window_id), profile())); |
| |
| EXPECT_EQ(window_id, GetWindowId(result)); |
| // "populate" was enabled so tabs should be populated. |
| base::ListValue tabs = |
| api_test_utils::GetList(result, ExtensionTabUtil::kTabsKey); |
| ASSERT_FALSE(tabs.empty()); |
| std::optional<int> tab0_id = tabs[0].GetDict().FindInt(extension_misc::kId); |
| ASSERT_TRUE(tab0_id.has_value()); |
| EXPECT_GE(*tab0_id, 0); |
| |
| // TODO(aa): Can't assume window is focused. On mac, calling Activate() from a |
| // browser test doesn't seem to do anything, so can't test the opposite |
| // either. |
| EXPECT_EQ(browser_window_interface()->GetWindow()->IsActive(), |
| api_test_utils::GetBoolean(result, "focused")); |
| |
| // TODO(aa): Minimized and maximized dimensions. Is there a way to set |
| // minimize/maximize programmatically? |
| |
| // Check window type. |
| EXPECT_EQ("normal", GetWindowType(browser_window_interface(), extension)); |
| Browser* test_browser = Browser::Create( |
| Browser::CreateParams(Browser::TYPE_POPUP, profile(), true)); |
| EXPECT_EQ("popup", GetWindowType(test_browser, extension)); |
| DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync( |
| GetTabListInterface()->GetTab(0)->GetContents(), false /* is_docked */); |
| EXPECT_EQ("devtools", |
| GetWindowType(DevToolsWindowTesting::Get(devtools)->browser(), |
| extension)); |
| DevToolsWindowTesting::CloseDevToolsWindowSync(devtools); |
| test_browser = Browser::Create(Browser::CreateParams::CreateForApp( |
| "test-app", true, gfx::Rect(), profile(), true)); |
| EXPECT_EQ("app", GetWindowType(test_browser, extension)); |
| test_browser = Browser::Create(Browser::CreateParams::CreateForAppPopup( |
| "test-app-popup", true, gfx::Rect(), profile(), true)); |
| EXPECT_EQ("popup", GetWindowType(test_browser, extension)); |
| |
| // Incognito. |
| Browser* incognito_browser = CreateIncognitoBrowser(); |
| int incognito_window_id = ExtensionTabUtil::GetWindowId(incognito_browser); |
| |
| // Without "include_incognito". |
| function = base::MakeRefCounted<WindowsGetFunction>(); |
| function->set_extension(extension.get()); |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), base::StringPrintf("[%u]", incognito_window_id), |
| profile()), |
| ExtensionTabUtil::kWindowNotFoundError)); |
| |
| // With "include_incognito". |
| function = base::MakeRefCounted<WindowsGetFunction>(); |
| function->set_extension(extension.get()); |
| result = utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| function.get(), base::StringPrintf("[%u]", incognito_window_id), |
| profile(), api_test_utils::FunctionMode::kIncognito)); |
| EXPECT_TRUE(api_test_utils::GetBoolean(result, "incognito")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, GetCurrentWindow) { |
| int window_id = ExtensionTabUtil::GetWindowId(browser_window_interface()); |
| Browser* new_browser = CreateBrowser(profile()); |
| int new_id = ExtensionTabUtil::GetWindowId(new_browser); |
| |
| // Get the current window using new_browser. |
| auto function = base::MakeRefCounted<WindowsGetCurrentFunction>(); |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| function->set_extension(extension.get()); |
| base::DictValue result = |
| utils::ToDict(RunFunctionWithDispatcherDelegateAndReturnValue( |
| function.get(), "[]", new_browser)); |
| |
| // The id should match the window id of the browser instance that was passed |
| // to RunFunctionWithDispatcherDelegateAndReturnValue. |
| EXPECT_EQ(new_id, GetWindowId(result)); |
| EXPECT_FALSE(result.contains(ExtensionTabUtil::kTabsKey)); |
| |
| // Get the current window using the old window and make the tabs populated. |
| function = base::MakeRefCounted<WindowsGetCurrentFunction>(); |
| function->set_extension(extension.get()); |
| result = utils::ToDict(RunFunctionWithDispatcherDelegateAndReturnValue( |
| function.get(), "[{\"populate\": true}]", browser_window_interface())); |
| |
| // The id should match the window id of the browser instance that was passed |
| // to RunFunctionWithDispatcherDelegateAndReturnValue. |
| EXPECT_EQ(window_id, GetWindowId(result)); |
| // "populate" was enabled so tabs should be populated. |
| base::ListValue tabs = |
| api_test_utils::GetList(result, ExtensionTabUtil::kTabsKey); |
| ASSERT_FALSE(tabs.empty()); |
| std::optional<int> tab0_id = tabs[0].GetDict().FindInt(extension_misc::kId); |
| ASSERT_TRUE(tab0_id.has_value()); |
| // The tab id should not be -1 as this is a browser window. |
| EXPECT_GE(*tab0_id, 0); |
| } |
| |
| #if BUILDFLAG(ENABLE_PLATFORM_APPS) |
| |
| // TODO(crbug.com/40745605): Test is flaky on Linux debug builds. |
| #if BUILDFLAG(IS_LINUX) && !defined(NDEBUG) |
| #define MAYBE_GetAllWindows DISABLED_GetAllWindows |
| #else |
| #define MAYBE_GetAllWindows GetAllWindows |
| #endif |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTestWithApps, MAYBE_GetAllWindows) { |
| const size_t NUM_WINDOWS = 5; |
| std::set<int> window_ids; |
| std::set<int> result_ids; |
| window_ids.insert(ExtensionTabUtil::GetWindowId(browser_window_interface())); |
| |
| for (size_t i = 0; i < NUM_WINDOWS - 1; ++i) { |
| Browser* new_browser = CreateBrowser(profile()); |
| window_ids.insert(ExtensionTabUtil::GetWindowId(new_browser)); |
| } |
| |
| // Application windows should not be accessible to extensions (app windows are |
| // only accessible to the owning item). |
| AppWindow* app_window = CreateTestAppWindow("{}"); |
| |
| // Undocked DevTools window should not be accessible, unless included in the |
| // type filter mask. |
| DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync( |
| TabListInterface::From(browser_window_interface()) |
| ->GetTab(0) |
| ->GetContents(), |
| false /* is_docked */); |
| |
| auto function = base::MakeRefCounted<WindowsGetAllFunction>(); |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| function->set_extension(extension.get()); |
| base::ListValue windows = utils::ToList( |
| utils::RunFunctionAndReturnSingleResult(function.get(), "[]", profile())); |
| |
| EXPECT_EQ(window_ids.size(), windows.size()); |
| for (const base::Value& result_window : windows) { |
| base::DictValue result_window_dict = utils::ToDict(result_window); |
| result_ids.insert(GetWindowId(result_window_dict)); |
| |
| // "populate" was not passed in so tabs are not populated. |
| const base::ListValue* tabs = |
| result_window_dict.FindList(ExtensionTabUtil::kTabsKey); |
| EXPECT_FALSE(tabs); |
| } |
| // The returned ids should contain all the current browser instance ids. |
| EXPECT_EQ(window_ids, result_ids); |
| |
| result_ids.clear(); |
| function = base::MakeRefCounted<WindowsGetAllFunction>(); |
| function->set_extension(extension.get()); |
| windows = utils::ToList(utils::RunFunctionAndReturnSingleResult( |
| function.get(), "[{\"populate\": true}]", profile())); |
| |
| EXPECT_EQ(window_ids.size(), windows.size()); |
| for (const base::Value& result_window : windows) { |
| base::DictValue result_window_dict = utils::ToDict(result_window); |
| result_ids.insert(GetWindowId(result_window_dict)); |
| |
| // "populate" was enabled so tabs should be populated. |
| const base::ListValue* tabs = |
| result_window_dict.FindList(ExtensionTabUtil::kTabsKey); |
| EXPECT_TRUE(tabs); |
| } |
| // The returned ids should contain all the current app, browser and |
| // devtools instance ids. |
| EXPECT_EQ(window_ids, result_ids); |
| |
| DevToolsWindowTesting::CloseDevToolsWindowSync(devtools); |
| |
| CloseAppWindow(app_window); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTestWithApps, GetAllWindowsAllTypes) { |
| const size_t NUM_WINDOWS = 5; |
| std::set<int> window_ids; |
| std::set<int> result_ids; |
| window_ids.insert(ExtensionTabUtil::GetWindowId(browser_window_interface())); |
| |
| for (size_t i = 0; i < NUM_WINDOWS - 1; ++i) { |
| Browser* new_browser = CreateBrowser(profile()); |
| window_ids.insert(ExtensionTabUtil::GetWindowId(new_browser)); |
| } |
| |
| // Application windows should not be accessible to extensions (app windows are |
| // only accessible to the owning item). |
| AppWindow* app_window = CreateTestAppWindow("{}"); |
| |
| // Undocked DevTools window should be accessible too, since they have been |
| // explicitly requested as part of the type filter mask. |
| DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync( |
| TabListInterface::From(browser_window_interface()) |
| ->GetTab(0) |
| ->GetContents(), |
| false /* is_docked */); |
| window_ids.insert(ExtensionTabUtil::GetWindowId( |
| DevToolsWindowTesting::Get(devtools)->browser())); |
| |
| auto function = base::MakeRefCounted<WindowsGetAllFunction>(); |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| function->set_extension(extension.get()); |
| base::ListValue windows(utils::ToList(utils::RunFunctionAndReturnSingleResult( |
| function.get(), |
| "[{\"windowTypes\": [\"app\", \"devtools\", \"normal\", \"panel\", " |
| "\"popup\"]}]", |
| profile()))); |
| |
| EXPECT_EQ(window_ids.size(), windows.size()); |
| for (const base::Value& result_window : windows) { |
| base::DictValue result_window_dict = utils::ToDict(result_window); |
| result_ids.insert(GetWindowId(result_window_dict)); |
| |
| // "populate" was not passed in so tabs are not populated. |
| const base::ListValue* tabs = |
| result_window_dict.FindList(ExtensionTabUtil::kTabsKey); |
| EXPECT_FALSE(tabs); |
| } |
| // The returned ids should contain all the browser and devtools instance ids. |
| EXPECT_EQ(window_ids, result_ids); |
| |
| result_ids.clear(); |
| function = base::MakeRefCounted<WindowsGetAllFunction>(); |
| function->set_extension(extension.get()); |
| windows = utils::ToList(utils::RunFunctionAndReturnSingleResult( |
| function.get(), |
| "[{\"populate\": true, \"windowTypes\": [\"app\", \"devtools\", " |
| "\"normal\", \"panel\", \"popup\"]}]", |
| profile())); |
| |
| EXPECT_EQ(window_ids.size(), windows.size()); |
| for (const base::Value& result_window : windows) { |
| base::DictValue result_window_dict = utils::ToDict(result_window); |
| result_ids.insert(GetWindowId(result_window_dict)); |
| |
| // "populate" was enabled so tabs should be populated. |
| const base::ListValue* tabs = |
| result_window_dict.FindList(ExtensionTabUtil::kTabsKey); |
| EXPECT_TRUE(tabs); |
| } |
| // The returned ids should contain all the browser and devtools instance ids. |
| EXPECT_EQ(window_ids, result_ids); |
| |
| DevToolsWindowTesting::CloseDevToolsWindowSync(devtools); |
| |
| CloseAppWindow(app_window); |
| } |
| |
| #endif // BUIDFLAG(ENABLE_PLATFORM_APPS) |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, UpdateNoPermissions) { |
| // The test empty extension has no permissions, therefore it should not get |
| // tab data in the function result. |
| auto update_tab_function = base::MakeRefCounted<TabsUpdateFunction>(); |
| scoped_refptr<const Extension> empty_extension( |
| ExtensionBuilder("Test").Build()); |
| update_tab_function->set_extension(empty_extension.get()); |
| // Without a callback the function will not generate a result. |
| update_tab_function->set_has_callback(true); |
| |
| const base::DictValue result = |
| utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| update_tab_function.get(), |
| "[null, {\"url\": \"about:blank\", \"pinned\": true}]", profile())); |
| // The url is stripped since the extension does not have tab permissions. |
| EXPECT_FALSE(result.contains("url")); |
| EXPECT_TRUE(api_test_utils::GetBoolean(result, "pinned")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| DisallowNonIncognitoUrlInIncognitoWindow) { |
| Browser* incognito = CreateIncognitoBrowser(); |
| |
| auto update_tab_function = base::MakeRefCounted<TabsUpdateFunction>(); |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| update_tab_function->set_extension(extension.get()); |
| update_tab_function->set_include_incognito_information(true); |
| |
| std::string error = api_test_utils::RunFunctionAndReturnError( |
| update_tab_function.get(), |
| std::string("[null, {\"url\": \"") + chrome::kChromeUIExtensionsURL + |
| chrome::kExtensionConfigureCommandsSubPage + "\"}]", |
| incognito->profile(), // incognito doesn't have any tabs. |
| api_test_utils::FunctionMode::kNone); |
| EXPECT_EQ(ErrorUtils::FormatErrorMessage( |
| tabs_constants::kURLsNotAllowedInIncognitoError, |
| std::string(chrome::kChromeUIExtensionsURL) + |
| chrome::kExtensionConfigureCommandsSubPage), |
| error); |
| |
| // Ensure the tab was not updated. It should stay as the new tab page. |
| TabListInterface* tab_list = TabListInterface::From(incognito); |
| EXPECT_EQ(1, tab_list->GetTabCount()); |
| EXPECT_EQ(GURL(url::kAboutBlankURL), |
| tab_list->GetActiveTab()->GetContents()->GetLastCommittedURL()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DefaultToIncognitoWhenItIsForced) { |
| static const char kArgsWithoutExplicitIncognitoParam[] = |
| "[{\"url\": \"about:blank\"}]"; |
| // Force Incognito mode. |
| IncognitoModePrefs::SetAvailability( |
| profile()->GetPrefs(), policy::IncognitoModeAvailability::kForced); |
| // Run without an explicit "incognito" param. |
| auto function = base::MakeRefCounted<WindowsCreateFunction>(); |
| function->SetRenderFrameHost(GetTabListInterface() |
| ->GetActiveTab() |
| ->GetContents() |
| ->GetPrimaryMainFrame()); |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| function->set_extension(extension.get()); |
| base::DictValue result = |
| utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| function.get(), kArgsWithoutExplicitIncognitoParam, profile(), |
| api_test_utils::FunctionMode::kIncognito)); |
| |
| // Make sure it is a new(different) window. |
| EXPECT_NE(ExtensionTabUtil::GetWindowId(browser_window_interface()), |
| GetWindowId(result)); |
| // ... and it is incognito. |
| EXPECT_TRUE(api_test_utils::GetBoolean(result, "incognito")); |
| |
| // Now try creating a window from incognito window. |
| Browser* incognito_browser = CreateIncognitoBrowser(); |
| // Run without an explicit "incognito" param. |
| function = base::MakeRefCounted<WindowsCreateFunction>(); |
| function->SetRenderFrameHost(GetTabListInterface() |
| ->GetActiveTab() |
| ->GetContents() |
| ->GetPrimaryMainFrame()); |
| function->set_extension(extension.get()); |
| result = utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| function.get(), kArgsWithoutExplicitIncognitoParam, |
| incognito_browser->profile(), api_test_utils::FunctionMode::kIncognito)); |
| // Make sure it is a new(different) window. |
| EXPECT_NE(ExtensionTabUtil::GetWindowId(incognito_browser), |
| GetWindowId(result)); |
| // ... and it is incognito. |
| EXPECT_TRUE(api_test_utils::GetBoolean(result, "incognito")); |
| } |
| |
| // Regression test for crbug.com/427147470. Verifies that opening |
| // chrome-extension:// URLs using chrome.tabs.create() from non-extension |
| // contexts (e.g. WebUI pages) works as expected. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| CreateExtensionTabFromNonExtensionContext) { |
| auto function = base::MakeRefCounted<TabsCreateFunction>(); |
| function->SetRenderFrameHost(GetTabListInterface() |
| ->GetActiveTab() |
| ->GetContents() |
| ->GetPrimaryMainFrame()); |
| function->set_extension(nullptr); |
| |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("../options_page")); |
| ASSERT_TRUE(extension); |
| GURL extension_url = extension->ResolveExtensionURL("options.html"); |
| |
| const std::string args_with_extension_url = |
| base::StringPrintf(R"([{ "url": "%s" }])", extension_url.spec()); |
| base::DictValue result = |
| utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| function.get(), args_with_extension_url, profile())); |
| |
| int tab_id = GetTabId(result); |
| content::WebContents* created_tab = nullptr; |
| ExtensionTabUtil::GetTabById(tab_id, profile(), /*include_incognito=*/false, |
| &created_tab); |
| ASSERT_TRUE(created_tab); |
| content::WaitForLoadStop(created_tab); |
| EXPECT_EQ(created_tab->GetPrimaryMainFrame()->GetLastCommittedURL(), |
| extension_url); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| DefaultToIncognitoWhenItIsForcedAndNoArgs) { |
| static const char kEmptyArgs[] = "[]"; |
| // Force Incognito mode. |
| IncognitoModePrefs::SetAvailability( |
| profile()->GetPrefs(), policy::IncognitoModeAvailability::kForced); |
| // Run without an explicit "incognito" param. |
| auto function = base::MakeRefCounted<WindowsCreateFunction>(); |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| function->set_extension(extension.get()); |
| base::DictValue result = |
| utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| function.get(), kEmptyArgs, profile(), |
| api_test_utils::FunctionMode::kIncognito)); |
| |
| // Make sure it is a new(different) window. |
| EXPECT_NE(ExtensionTabUtil::GetWindowId(browser_window_interface()), |
| GetWindowId(result)); |
| // ... and it is incognito. |
| EXPECT_TRUE(api_test_utils::GetBoolean(result, "incognito")); |
| |
| // Now try creating a window from incognito window. |
| Browser* incognito_browser = CreateIncognitoBrowser(); |
| // Run without an explicit "incognito" param. |
| function = base::MakeRefCounted<WindowsCreateFunction>(); |
| function->set_extension(extension.get()); |
| result = utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| function.get(), kEmptyArgs, incognito_browser->profile(), |
| api_test_utils::FunctionMode::kIncognito)); |
| // Make sure it is a new(different) window. |
| EXPECT_NE(ExtensionTabUtil::GetWindowId(incognito_browser), |
| GetWindowId(result)); |
| // ... and it is incognito. |
| EXPECT_TRUE(api_test_utils::GetBoolean(result, "incognito")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| DontCreateNormalWindowWhenIncognitoForced) { |
| static const char kArgsWithExplicitIncognitoParam[] = |
| "[{\"url\": \"about:blank\", \"incognito\": false }]"; |
| // Force Incognito mode. |
| IncognitoModePrefs::SetAvailability( |
| profile()->GetPrefs(), policy::IncognitoModeAvailability::kForced); |
| |
| // Run with an explicit "incognito" param. |
| auto function = base::MakeRefCounted<WindowsCreateFunction>(); |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| function->set_extension(extension.get()); |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), kArgsWithExplicitIncognitoParam, profile()), |
| keys::kIncognitoModeIsForced)); |
| |
| // Now try opening a normal window from incognito window. |
| Browser* incognito_browser = CreateIncognitoBrowser(); |
| // Run with an explicit "incognito" param. |
| function = base::MakeRefCounted<WindowsCreateFunction>(); |
| function->set_extension(extension.get()); |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError(function.get(), |
| kArgsWithExplicitIncognitoParam, |
| incognito_browser->profile()), |
| keys::kIncognitoModeIsForced)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| DontCreateIncognitoWindowWhenIncognitoDisabled) { |
| static const char kArgs[] = |
| "[{\"url\": \"about:blank\", \"incognito\": true }]"; |
| |
| Browser* incognito_browser = CreateIncognitoBrowser(); |
| // Disable Incognito mode. |
| IncognitoModePrefs::SetAvailability( |
| profile()->GetPrefs(), policy::IncognitoModeAvailability::kDisabled); |
| // Run in normal window. |
| auto function = base::MakeRefCounted<WindowsCreateFunction>(); |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| function->set_extension(extension.get()); |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError(function.get(), kArgs, profile()), |
| keys::kIncognitoModeIsDisabled)); |
| |
| // Run in incognito window. |
| function = base::MakeRefCounted<WindowsCreateFunction>(); |
| function->set_extension(extension.get()); |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError(function.get(), kArgs, |
| incognito_browser->profile()), |
| keys::kIncognitoModeIsDisabled)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, QueryCurrentWindowTabs) { |
| const size_t kExtraWindows = 3; |
| for (size_t i = 0; i < kExtraWindows; ++i) { |
| CreateBrowser(profile()); |
| } |
| |
| GURL url(url::kAboutBlankURL); |
| ASSERT_TRUE(AddTabAtIndex(0, url, ui::PAGE_TRANSITION_LINK)); |
| int window_id = ExtensionTabUtil::GetWindowId(browser_window_interface()); |
| |
| // Get tabs in the 'current' window called from non-focused browser. |
| auto function = base::MakeRefCounted<TabsQueryFunction>(); |
| function->set_extension(ExtensionBuilder("Test").Build().get()); |
| |
| base::ListValue result_tabs = |
| utils::ToList(RunFunctionWithDispatcherDelegateAndReturnValue( |
| function.get(), "[{\"currentWindow\":true}]", |
| browser_window_interface())); |
| |
| // We should have one initial tab and one added tab. |
| EXPECT_EQ(2u, result_tabs.size()); |
| for (const base::Value& result_tab : result_tabs) { |
| EXPECT_EQ(window_id, GetTabWindowId(utils::ToDict(result_tab))); |
| } |
| |
| // Get tabs NOT in the 'current' window called from non-focused browser. |
| function = base::MakeRefCounted<TabsQueryFunction>(); |
| function->set_extension(ExtensionBuilder("Test").Build().get()); |
| result_tabs = utils::ToList(RunFunctionWithDispatcherDelegateAndReturnValue( |
| function.get(), "[{\"currentWindow\":false}]", |
| browser_window_interface())); |
| |
| // We should have one tab for each extra window. |
| EXPECT_EQ(kExtraWindows, result_tabs.size()); |
| for (const base::Value& result_tab : result_tabs) { |
| EXPECT_NE(window_id, GetTabWindowId(utils::ToDict(result_tab))); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, QueryAllTabsWithDevTools) { |
| const size_t kNumWindows = 3; |
| std::set<int> window_ids; |
| window_ids.insert(ExtensionTabUtil::GetWindowId(browser_window_interface())); |
| for (size_t i = 0; i < kNumWindows - 1; ++i) { |
| Browser* new_browser = CreateBrowser(profile()); |
| window_ids.insert(ExtensionTabUtil::GetWindowId(new_browser)); |
| } |
| |
| // Undocked DevTools window should not be accessible. |
| DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync( |
| GetTabListInterface()->GetTab(0)->GetContents(), false /* is_docked */); |
| |
| // Get tabs in the 'current' window called from non-focused browser. |
| auto function = base::MakeRefCounted<TabsQueryFunction>(); |
| function->set_extension(ExtensionBuilder("Test").Build().get()); |
| base::ListValue result_tabs( |
| utils::ToList(utils::RunFunctionAndReturnSingleResult( |
| function.get(), "[{}]", profile()))); |
| |
| std::set<int> result_ids; |
| // We should have one tab per browser except for DevTools. |
| EXPECT_EQ(kNumWindows, result_tabs.size()); |
| for (const base::Value& result_tab : result_tabs) { |
| result_ids.insert(GetTabWindowId(utils::ToDict(result_tab))); |
| } |
| EXPECT_EQ(window_ids, result_ids); |
| |
| DevToolsWindowTesting::CloseDevToolsWindowSync(devtools); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, QueryTabGroups) { |
| ASSERT_TRUE(browser()->tab_strip_model()->SupportsTabGroups()); |
| |
| GURL url(url::kAboutBlankURL); |
| ASSERT_TRUE(AddTabAtIndex(0, url, ui::PAGE_TRANSITION_LINK)); |
| ASSERT_TRUE(AddTabAtIndex(0, url, ui::PAGE_TRANSITION_LINK)); |
| ASSERT_TRUE(AddTabAtIndex(0, url, ui::PAGE_TRANSITION_LINK)); |
| tab_groups::TabGroupId group_id = |
| browser()->tab_strip_model()->AddToNewGroup({0, 1}); |
| |
| auto function = base::MakeRefCounted<TabsQueryFunction>(); |
| function->set_extension(ExtensionBuilder("Test").Build().get()); |
| constexpr char kFormatQueryArgs[] = R"([{"groupId":%d}])"; |
| const std::string args = base::StringPrintf( |
| kFormatQueryArgs, ExtensionTabUtil::GetGroupId(group_id)); |
| base::ListValue result(utils::ToList(utils::RunFunctionAndReturnSingleResult( |
| function.get(), args, profile()))); |
| |
| EXPECT_EQ(2u, result.size()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DontCreateTabInClosingPopupWindow) { |
| // Test creates new popup window, closes it right away and then tries to open |
| // a new tab in it. Tab should not be opened in the popup window, but in a |
| // tabbed browser window. |
| Browser* popup_browser = Browser::Create( |
| Browser::CreateParams(Browser::TYPE_POPUP, profile(), true)); |
| int window_id = ExtensionTabUtil::GetWindowId(popup_browser); |
| chrome::CloseWindow(popup_browser); |
| |
| auto create_tab_function = base::MakeRefCounted<TabsCreateFunction>(); |
| create_tab_function->set_extension(ExtensionBuilder("Test").Build().get()); |
| // Without a callback the function will not generate a result. |
| create_tab_function->set_has_callback(true); |
| |
| static const char kNewBlankTabArgs[] = |
| "[{\"url\": \"about:blank\", \"windowId\": %u}]"; |
| |
| const base::DictValue result = |
| utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| create_tab_function.get(), |
| base::StringPrintf(kNewBlankTabArgs, window_id), profile())); |
| |
| EXPECT_NE(window_id, GetTabWindowId(result)); |
| } |
| |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, InvalidUpdateWindowState) { |
| int window_id = ExtensionTabUtil::GetWindowId(browser_window_interface()); |
| |
| static const char kArgsMinimizedWithFocus[] = |
| "[%u, {\"state\": \"minimized\", \"focused\": true}]"; |
| auto function = base::MakeRefCounted<WindowsUpdateFunction>(); |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| function->set_extension(extension.get()); |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf(kArgsMinimizedWithFocus, window_id), profile()), |
| keys::kInvalidWindowStateError)); |
| |
| static const char kArgsMaximizedWithoutFocus[] = |
| "[%u, {\"state\": \"maximized\", \"focused\": false}]"; |
| function = base::MakeRefCounted<WindowsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf(kArgsMaximizedWithoutFocus, window_id), profile()), |
| keys::kInvalidWindowStateError)); |
| |
| static const char kArgsMinimizedWithBounds[] = |
| "[%u, {\"state\": \"minimized\", \"width\": 500}]"; |
| function = base::MakeRefCounted<WindowsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf(kArgsMinimizedWithBounds, window_id), profile()), |
| keys::kInvalidWindowStateError)); |
| |
| static const char kArgsMaximizedWithBounds[] = |
| "[%u, {\"state\": \"maximized\", \"width\": 500}]"; |
| function = base::MakeRefCounted<WindowsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf(kArgsMaximizedWithBounds, window_id), profile()), |
| keys::kInvalidWindowStateError)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, InvalidUpdateWindowBounds) { |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| |
| // Get the display bounds so we can test whether the window intersects. |
| gfx::Rect displays; |
| for (const auto& display : display::Screen::Get()->GetAllDisplays()) { |
| displays.Union(display.bounds()); |
| } |
| |
| int window_id = ExtensionTabUtil::GetWindowId(browser_window_interface()); |
| gfx::Rect window_bounds = |
| browser_window_interface()->GetWindow()->GetBounds(); |
| |
| static const char kArgsUpdateFunction[] = "[%u, {\"left\": %d, \"top\": %d}]"; |
| // We use a small value to move the window outside or inside the bounds. |
| int window_offset = window_bounds.size().width() * 0.1; |
| |
| { |
| // Window bounds that do not intersect with the display are not valid. |
| int window_left = displays.right() + window_offset; |
| int window_top = displays.bottom() + window_offset; |
| auto function = base::MakeRefCounted<WindowsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf(kArgsUpdateFunction, window_id, window_left, |
| window_top), |
| profile()), |
| keys::kInvalidWindowBoundsError)); |
| } |
| |
| { |
| // Window bounds that intersect less than 50% with the display are not |
| // valid. |
| int window_left = displays.right() - window_offset; |
| int window_top = displays.bottom() - window_offset; |
| auto function = base::MakeRefCounted<WindowsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf(kArgsUpdateFunction, window_id, window_left, |
| window_top), |
| profile()), |
| keys::kInvalidWindowBoundsError)); |
| } |
| } |
| |
| // On Android this fails when Run() calls BaseWindow::CanResize() returns false |
| // due to default Android Browser Tests not having free-form windows. |
| #if !BUILDFLAG(IS_ANDROID) |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| UpdatingWindowBoundsSucceedsForValidBounds) { |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| |
| // Get the display bounds so we can test whether the window intersects. |
| gfx::Rect displays; |
| for (const auto& display : display::Screen::Get()->GetAllDisplays()) { |
| displays.Union(display.bounds()); |
| } |
| |
| int window_id = ExtensionTabUtil::GetWindowId(browser_window_interface()); |
| gfx::Rect window_bounds = |
| browser_window_interface()->GetWindow()->GetBounds(); |
| |
| static const char kArgsUpdateFunction[] = "[%u, {\"left\": %d, \"top\": %d}]"; |
| // We use a small value to move the window outside or inside the bounds. |
| int window_offset = window_bounds.size().width() * 0.1; |
| |
| { |
| // Window bounds that intersect 50% or more with the display are valid. |
| int window_left = displays.right() - window_bounds.width() + window_offset; |
| int window_top = displays.bottom() - window_bounds.height() + window_offset; |
| auto function = base::MakeRefCounted<WindowsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| EXPECT_TRUE( |
| utils::RunFunction(function.get(), |
| base::StringPrintf(kArgsUpdateFunction, window_id, |
| window_left, window_top), |
| profile(), api_test_utils::FunctionMode::kNone)); |
| } |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| #if BUILDFLAG(IS_ANDROID) |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| UpdateWindowStateFailsWhenNotResizable) { |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| std::vector<BrowserWindowInterface*> windows = |
| GetAllBrowserWindowInterfaces(); |
| ASSERT_FALSE(windows.empty()); |
| int window_id = ExtensionTabUtil::GetWindowId(windows[0]); |
| |
| auto function = base::MakeRefCounted<WindowsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| |
| // Attempting to maximize on Android triggers the CanResize() check. Since |
| // the standard test environment is not in desktop windowing mode (and may |
| // run on older SDKs), this is expected to fail. We verify that an error is |
| // returned without checking the exact string. |
| std::string error = utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf("[%u, {\"state\": \"maximized\"}]", window_id), |
| profile()); |
| EXPECT_FALSE(error.empty()); |
| } |
| |
| // TODO(crbug.com/491868694) Remove once overlapping tests are enabled on |
| // Android. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| UpdateWindowStateSucceedsWhenNoResizeNeeded) { |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| std::vector<BrowserWindowInterface*> windows = |
| GetAllBrowserWindowInterfaces(); |
| ASSERT_FALSE(windows.empty()); |
| int window_id = ExtensionTabUtil::GetWindowId(windows[0]); |
| |
| auto function = base::MakeRefCounted<WindowsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| |
| // A simple update that does not require resizing bypasses the CanResize() |
| // check, succeeding without an error code. |
| EXPECT_TRUE(utils::RunFunction( |
| function.get(), |
| base::StringPrintf("[%u, {\"drawAttention\": true}]", window_id), |
| profile(), api_test_utils::FunctionMode::kNone)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| UpdateWindowStateFullscreenFailsOnAndroid) { |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| std::vector<BrowserWindowInterface*> windows = |
| GetAllBrowserWindowInterfaces(); |
| ASSERT_FALSE(windows.empty()); |
| int window_id = ExtensionTabUtil::GetWindowId(windows[0]); |
| |
| auto function = base::MakeRefCounted<WindowsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| |
| // Attempting to enter fullscreen on Android should explicitly fail with |
| // the kUnableToEnterFullScreenAndroid error message. |
| std::string error = utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf("[%u, {\"state\": \"fullscreen\"}]", window_id), |
| profile()); |
| |
| EXPECT_EQ(tabs_constants::kUnableToEnterFullScreenAndroid, error); |
| } |
| #endif |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, UpdateDevToolsWindow) { |
| DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync( |
| GetTabListInterface()->GetTab(0)->GetContents(), false /* is_docked */); |
| |
| auto get_function = base::MakeRefCounted<WindowsGetFunction>(); |
| scoped_refptr<const Extension> extension( |
| ExtensionBuilder("Test").Build().get()); |
| get_function->set_extension(extension.get()); |
| base::DictValue result = |
| utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| get_function.get(), |
| base::StringPrintf( |
| "[%u, {\"windowTypes\": [\"devtools\"]}]", |
| ExtensionTabUtil::GetWindowId( |
| DevToolsWindowTesting::Get(devtools)->browser())), |
| profile())); |
| |
| // Verify the updating width/height works. |
| int32_t new_width = api_test_utils::GetInteger(result, "width") - 50; |
| int32_t new_height = api_test_utils::GetInteger(result, "height") - 50; |
| |
| auto update_function = base::MakeRefCounted<WindowsUpdateFunction>(); |
| result = utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| update_function.get(), |
| base::StringPrintf("[%u, {\"width\": %d, \"height\": %d}]", |
| ExtensionTabUtil::GetWindowId( |
| DevToolsWindowTesting::Get(devtools)->browser()), |
| new_width, new_height), |
| profile())); |
| |
| EXPECT_EQ(new_width, api_test_utils::GetInteger(result, "width")); |
| EXPECT_EQ(new_height, api_test_utils::GetInteger(result, "height")); |
| |
| DevToolsWindowTesting::CloseDevToolsWindowSync(devtools); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, ExtensionAPICannotNavigateDevtools) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("Test").AddAPIPermission("tabs").Build(); |
| |
| DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync( |
| GetTabListInterface()->GetTab(0)->GetContents(), false /* is_docked */); |
| |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf( |
| "[%d, {\"url\":\"http://example.com\"}]", |
| ExtensionTabUtil::GetTabId( |
| DevToolsWindowTesting::Get(devtools)->main_web_contents())), |
| DevToolsWindowTesting::Get(devtools)->browser()->GetProfile()), |
| tabs_constants::kNotAllowedForDevToolsError)); |
| |
| DevToolsWindowTesting::CloseDevToolsWindowSync(devtools); |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| // Mac is intentionally unsupported (crbug.com/41385204). |
| #define MAYBE_AcceptState DISABLED_AcceptState |
| #else |
| #define MAYBE_AcceptState AcceptState |
| #endif |
| IN_PROC_BROWSER_TEST_F(ExtensionWindowCreateTest, MAYBE_AcceptState) { |
| auto function = base::MakeRefCounted<WindowsCreateFunction>(); |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| function->set_extension(extension.get()); |
| function->SetBrowserContextForTesting(profile()); |
| |
| base::DictValue result = |
| utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| function.get(), "[{\"state\": \"minimized\"}]", profile(), |
| api_test_utils::FunctionMode::kIncognito)); |
| int window_id = GetWindowId(result); |
| std::string error; |
| |
| WindowController* new_controller = |
| ExtensionTabUtil::GetControllerFromWindowID( |
| ChromeExtensionFunctionDetails(function.get()), window_id, &error); |
| ASSERT_TRUE(new_controller); |
| EXPECT_TRUE(error.empty()); |
| Browser* new_browser = new_controller->GetBrowser(); |
| ASSERT_TRUE(new_browser); |
| |
| // TODO(crbug.com/40254339): Remove this workaround if this wait is no longer |
| // needed. |
| // These builds flags are limiting the check for IsMinimized() for Linux. |
| // For Linux, we only check X11 window manager and not Wayland since our |
| // current fix only applies to X11. |
| #if BUILDFLAG(IS_LINUX) |
| // Must be checked inside IS_LINUX to compile on windows/mac. |
| #if BUILDFLAG(SUPPORTS_OZONE_X11) |
| // DesktopWindowTreeHostX11::IsMinimized() relies on an asynchronous update |
| // from the window server |
| views::test::PropertyWaiter minimize_waiter( |
| base::BindRepeating( |
| &BrowserWindow::IsMinimized, |
| base::Unretained(BrowserWindow::FromBrowser(new_browser))), |
| true, TestTimeouts::action_timeout()); |
| EXPECT_TRUE(minimize_waiter.Wait()); |
| #elif BUILDFLAG(SUPPORTS_OZONE_WAYLAND) |
| // TODO(crbug.com/40252593): Find a fix/workaround for wayland and add |
| // verification of IsMinimized() for as well. |
| #endif |
| #else |
| EXPECT_TRUE(new_controller->window()->IsMinimized()); |
| #endif // BUILDFLAG(IS_LINUX) |
| |
| function = base::MakeRefCounted<WindowsCreateFunction>(); |
| function->set_extension(extension.get()); |
| function->SetBrowserContextForTesting(profile()); |
| result = utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| function.get(), "[{\"state\": \"fullscreen\"}]", profile(), |
| api_test_utils::FunctionMode::kIncognito)); |
| window_id = GetWindowId(result); |
| |
| new_controller = ExtensionTabUtil::GetControllerFromWindowID( |
| ChromeExtensionFunctionDetails(function.get()), window_id, &error); |
| ASSERT_TRUE(new_controller); |
| EXPECT_TRUE(error.empty()); |
| ui_test_utils::WaitForBrowserSetLastActive(new_controller->GetBrowser()); |
| EXPECT_TRUE(new_controller->GetBrowser()->GetWindow()->IsFullscreen()); |
| |
| // Let the message loop run so that |fake_fullscreen| finishes transition. |
| content::RunAllPendingInMessageLoop(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionWindowCreateTest, ValidateCreateWindowState) { |
| EXPECT_TRUE( |
| base::MatchPattern(RunCreateWindowExpectError( |
| "[{\"state\": \"minimized\", \"focused\": true}]"), |
| keys::kInvalidWindowStateError)); |
| EXPECT_TRUE(base::MatchPattern( |
| RunCreateWindowExpectError( |
| "[{\"state\": \"maximized\", \"focused\": false}]"), |
| keys::kInvalidWindowStateError)); |
| EXPECT_TRUE(base::MatchPattern( |
| RunCreateWindowExpectError( |
| "[{\"state\": \"fullscreen\", \"focused\": false}]"), |
| keys::kInvalidWindowStateError)); |
| EXPECT_TRUE( |
| base::MatchPattern(RunCreateWindowExpectError( |
| "[{\"state\": \"minimized\", \"width\": 500}]"), |
| keys::kInvalidWindowStateError)); |
| EXPECT_TRUE( |
| base::MatchPattern(RunCreateWindowExpectError( |
| "[{\"state\": \"maximized\", \"width\": 500}]"), |
| keys::kInvalidWindowStateError)); |
| EXPECT_TRUE( |
| base::MatchPattern(RunCreateWindowExpectError( |
| "[{\"state\": \"fullscreen\", \"width\": 500}]"), |
| keys::kInvalidWindowStateError)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionWindowCreateTest, ValidateCreateWindowBounds) { |
| // Get the display bounds so we can test whether the window intersects. |
| gfx::Rect displays; |
| for (const auto& display : display::Screen::Get()->GetAllDisplays()) { |
| displays.Union(display.bounds()); |
| } |
| |
| static const char kArgsCreateFunction[] = |
| "[{\"left\": %d, \"top\": %d, \"width\": %d, \"height\": %d }]"; |
| int window_width = 100; |
| int window_height = 100; |
| // We use a small value to move the window outside or inside the bounds. |
| int window_offset = 10; |
| |
| { |
| // Window bounds that do not intersect with the display are not valid. |
| int window_left = displays.right() + window_offset; |
| int window_top = displays.bottom() + window_offset; |
| EXPECT_TRUE( |
| base::MatchPattern(RunCreateWindowExpectError(base::StringPrintf( |
| kArgsCreateFunction, window_left, window_top, |
| window_width, window_height)), |
| keys::kInvalidWindowBoundsError)); |
| } |
| |
| { |
| // Window bounds that intersect less than 50% with the display are not |
| // valid. |
| int window_left = displays.right() - window_offset; |
| int window_top = displays.bottom() - window_offset; |
| EXPECT_TRUE( |
| base::MatchPattern(RunCreateWindowExpectError(base::StringPrintf( |
| kArgsCreateFunction, window_left, window_top, |
| window_width, window_height)), |
| keys::kInvalidWindowBoundsError)); |
| } |
| |
| { |
| // Window bounds that intersect 50% or more with the display are valid. |
| int window_left = displays.right() - window_width + window_offset; |
| int window_top = displays.bottom() - window_height + window_offset; |
| auto function = base::MakeRefCounted<WindowsCreateFunction>(); |
| function->set_extension(ExtensionBuilder("Test").Build().get()); |
| EXPECT_TRUE(utils::RunFunction( |
| function.get(), |
| base::StringPrintf(kArgsCreateFunction, window_left, window_top, |
| window_width, window_height), |
| profile(), api_test_utils::FunctionMode::kNone)); |
| } |
| |
| { |
| // Window bounds that specify size and not position should be adjusted |
| // to the screen in case the window is not visible. |
| // For this, update the current window bounds so the new window position |
| // needs to be adjusted to fit. |
| gfx::Rect current_window_bounds = |
| browser_window_interface()->GetWindow()->GetBounds(); |
| current_window_bounds.set_x(current_window_bounds.x() + |
| current_window_bounds.width() - window_offset); |
| current_window_bounds.set_y(current_window_bounds.y() + |
| current_window_bounds.height() - window_offset); |
| browser_window_interface()->GetWindow()->SetBounds(current_window_bounds); |
| |
| static const char kArgsCreateFunctionOnlySize[] = |
| "[{\"width\": %d, \"height\": %d }]"; |
| auto function = base::MakeRefCounted<WindowsCreateFunction>(); |
| function->set_extension(ExtensionBuilder("Test").Build().get()); |
| EXPECT_TRUE( |
| utils::RunFunction(function.get(), |
| base::StringPrintf(kArgsCreateFunctionOnlySize, |
| window_width, window_height), |
| profile(), api_test_utils::FunctionMode::kNone)); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionWindowCreateTest, CreatePopupWindowFromWebUI) { |
| auto function = base::MakeRefCounted<WindowsCreateFunction>(); |
| function->SetBrowserContextForTesting(profile()); |
| function->set_source_context_type(mojom::ContextType::kUntrustedWebUi); |
| |
| const base::DictValue result = |
| utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| function.get(), R"([{"type": "popup"}])", profile())); |
| int window_id = GetWindowId(result); |
| |
| std::string error; |
| EXPECT_TRUE(ExtensionTabUtil::GetControllerFromWindowID( |
| ChromeExtensionFunctionDetails(function.get()), window_id, &error)); |
| EXPECT_TRUE(error.empty()); |
| } |
| |
| struct ExtensionWindowCreateIwaParam { |
| std::string test_name; |
| bool want_success; |
| std::string args; |
| }; |
| |
| class ExtensionIwaTestBase : public InProcessBrowserTest { |
| public: |
| ExtensionIwaTestBase() { |
| scoped_feature_list_.InitAndEnableFeature(features::kIsolatedWebApps); |
| set_open_about_blank_on_browser_launch(false); |
| } |
| |
| void SetUpOnMainThread() override { |
| InProcessBrowserTest::SetUpOnMainThread(); |
| |
| web_app::test::WaitUntilReady( |
| web_app::WebAppProvider::GetForTest(profile())); |
| ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir()); |
| } |
| |
| void SetUpDefaultCommandLine(base::CommandLine* command_line) override { |
| InProcessBrowserTest::SetUpDefaultCommandLine(command_line); |
| // Suppress "Welcome to Google Chrome" window |
| command_line->AppendSwitch(switches::kNoFirstRun); |
| command_line->AppendSwitch(switches::kNoStartupWindow); |
| command_line->AppendSwitch(switches::kKeepAliveForTest); |
| } |
| |
| void TearDownOnMainThread() override { |
| if (GlobalBrowserCollection::GetInstance()->IsEmpty()) { |
| // Tests crash during teardown if no browser has opened combined with the |
| // command line switches above. Open a browser to avoid the crash. |
| CreateBrowser(profile()); |
| } |
| InProcessBrowserTest::TearDownOnMainThread(); |
| } |
| |
| Profile* profile() { |
| // We cannot use `profile()` here, because `browser()` is |
| // `nullptr` due to the command line switches above. |
| return profile_util::GetLastUsedProfile(); |
| } |
| |
| protected: |
| web_app::IsolatedWebAppUrlInfo InstallAndTrustBundle() { |
| auto bundle = web_app::IsolatedWebAppBuilder(web_app::ManifestBuilder()) |
| .AddHtml("/", "Hello extensions!") |
| .BuildBundle(web_app::test::GetDefaultEd25519KeyPair()); |
| return bundle->InstallChecked(profile()); |
| } |
| |
| BrowserWindowInterface* OpenIwa( |
| const web_app::IsolatedWebAppUrlInfo& url_info) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("IwaOpenerExtension").Build(); |
| auto function = base::MakeRefCounted<WindowsCreateFunction>(); |
| function->set_extension(extension); |
| |
| std::string args = base::StringPrintf( |
| R"([{"url": "%s"}])", url_info.origin().GetURL().spec().c_str()); |
| |
| bool result = api_test_utils::RunFunction( |
| function.get(), args, profile(), api_test_utils::FunctionMode::kNone); |
| EXPECT_TRUE(result) << function->GetError(); |
| |
| BrowserWindowInterface* iwa_browser = |
| GetLastActiveBrowserWindowInterfaceWithAnyProfile(); |
| EXPECT_TRUE(iwa_browser); |
| return iwa_browser; |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| web_app::OsIntegrationManager::ScopedSuppressForTesting os_hooks_suppress_; |
| base::ScopedTempDir scoped_temp_dir_; |
| }; |
| |
| // Test that `windows.create` functions correctly for Isolated Web Apps. |
| class ExtensionWindowCreateIwaTest |
| : public ExtensionIwaTestBase, |
| public testing::WithParamInterface<ExtensionWindowCreateIwaParam> { |
| public: |
| ExtensionWindowCreateIwaTest() = default; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWindowCreateIwaTest, CreateWindowForIwa) { |
| auto url_info = InstallAndTrustBundle(); |
| |
| EXPECT_EQ(GlobalBrowserCollection::GetInstance()->GetSize(), 0ul); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("ExtensionWindowCreateIwaTest").Build(); |
| auto function = base::MakeRefCounted<WindowsCreateFunction>(); |
| function->set_extension(extension); |
| bool result = |
| api_test_utils::RunFunction(function.get(), GetParam().args, profile(), |
| api_test_utils::FunctionMode::kNone); |
| if (GetParam().want_success) { |
| EXPECT_TRUE(result) << function->GetError(); |
| |
| // A single browser for the IWA should now be open. |
| ASSERT_EQ(GlobalBrowserCollection::GetInstance()->GetSize(), 1ul); |
| BrowserWindowInterface* iwa_browser = |
| GetLastActiveBrowserWindowInterfaceWithAnyProfile(); |
| ASSERT_TRUE(iwa_browser); |
| TabListInterface* tab_list = TabListInterface::From(iwa_browser); |
| ASSERT_EQ(tab_list->GetTabCount(), 1); |
| |
| auto* web_contents = tab_list->GetActiveTab()->GetContents(); |
| content::WaitForLoadStop(web_contents); |
| EXPECT_EQ(web_contents->GetURL(), url_info.origin().GetURL()); |
| |
| static constexpr std::string_view kLaunchQueueScript = R"( |
| new Promise(async (resolve) => { |
| window.launchQueue.setConsumer(launchParams => { |
| resolve(launchParams.targetURL); |
| }); |
| }); |
| )"; |
| |
| EXPECT_EQ(content::EvalJs(web_contents, kLaunchQueueScript), |
| url_info.origin().GetURL().Resolve("/index.html")); |
| } else { |
| EXPECT_FALSE(result); |
| // No browser should have opened. |
| EXPECT_EQ(GlobalBrowserCollection::GetInstance()->GetSize(), 0ul); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /* no prefix */, |
| ExtensionWindowCreateIwaTest, |
| testing::Values( |
| ExtensionWindowCreateIwaParam{.test_name = "iwa_and_https", |
| .want_success = false, |
| .args = R"([{ |
| "url": [ |
| "isolated-app://4tkrnsmftl4ggvvdkfth3piainqragus2qbhf7rlz2a3wo3rh4wqaaic/index.html", |
| "https://example.com" |
| ] |
| }])"}, |
| ExtensionWindowCreateIwaParam{.test_name = "https_and_iwa_and_https", |
| .want_success = false, |
| .args = R"([{ |
| "url": [ |
| "https://example.com", |
| "isolated-app://4tkrnsmftl4ggvvdkfth3piainqragus2qbhf7rlz2a3wo3rh4wqaaic/index.html", |
| "https://example.com" |
| ] |
| }])"}, |
| // If we ever support tabbed IWAs, then this test must be updated to |
| // `.want_success true`. |
| ExtensionWindowCreateIwaParam{.test_name = "iwa_and_iwa", |
| .want_success = false, |
| .args = R"([{ |
| "url": [ |
| "isolated-app://4tkrnsmftl4ggvvdkfth3piainqragus2qbhf7rlz2a3wo3rh4wqaaic/index.html", |
| "isolated-app://4tkrnsmftl4ggvvdkfth3piainqragus2qbhf7rlz2a3wo3rh4wqaaic/index.html" |
| ] |
| }])"}, |
| ExtensionWindowCreateIwaParam{.test_name = "iwa_and_different_iwa", |
| .want_success = false, |
| .args = R"([{ |
| "url": [ |
| "isolated-app://4tkrnsmftl4ggvvdkfth3piainqragus2qbhf7rlz2a3wo3rh4wqaaic/index.html", |
| "isolated-app://5dp4lo5h6tpc4vuokowxmlqs5gpbainu2nqvuddccx5mqsnje7fqaaic/index.html" |
| ] |
| }])"}, |
| ExtensionWindowCreateIwaParam{.test_name = "invalid_iwa_url", |
| .want_success = false, |
| .args = R"([{ |
| "url": [ |
| "isolated-app://invalid-iwa-url" |
| ] |
| }])"}, |
| // If we ever support tabbed IWAs, this test must be updated: If `tabId` |
| // refers to the tab of the same IWA origin that is specified in `url`, |
| // it should be allowed. |
| ExtensionWindowCreateIwaParam{.test_name = "iwa_and_tab_id", |
| .want_success = false, |
| .args = R"([{ |
| "url": |
| "isolated-app://4tkrnsmftl4ggvvdkfth3piainqragus2qbhf7rlz2a3wo3rh4wqaaic/index.html", |
| "tabId": 1 |
| }])"}, |
| ExtensionWindowCreateIwaParam{.test_name = "iwa", |
| .want_success = true, |
| .args = R"([{ |
| "url": "isolated-app://4tkrnsmftl4ggvvdkfth3piainqragus2qbhf7rlz2a3wo3rh4wqaaic/index.html", |
| }])"}), |
| [](const testing::TestParamInfo<ExtensionWindowCreateIwaTest::ParamType>& |
| info) { return info.param.test_name; }); |
| |
| using ExtensionApiTabsIwaMoveTest = ExtensionIwaTestBase; |
| |
| using ExtensionApiTabsIwaNavigateTest = ExtensionIwaTestBase; |
| using ExtensionApiTabsIwaDuplicateTest = ExtensionIwaTestBase; |
| |
| // `tabs.create` does not support `isolated-app:` URLs, even when targeting an |
| // existing IWA window. `windows.create` is the supported entry point and |
| // always opens IWAs at their `start_url`. |
| IN_PROC_BROWSER_TEST_F(ExtensionApiTabsIwaNavigateTest, |
| TabsCreateRejectsIwaUrl) { |
| auto url_info = InstallAndTrustBundle(); |
| BrowserWindowInterface* iwa_browser = OpenIwa(url_info); |
| int iwa_window_id = ExtensionTabUtil::GetWindowId(iwa_browser); |
| |
| TabListInterface* iwa_tab_list = TabListInterface::From(iwa_browser); |
| ASSERT_EQ(iwa_tab_list->GetTabCount(), 1); |
| auto* iwa_web_contents = iwa_tab_list->GetActiveTab()->GetContents(); |
| content::WaitForLoadStop(iwa_web_contents); |
| |
| GURL deep_url = url_info.origin().GetURL().Resolve("/deep/page.html"); |
| std::string args = base::StringPrintf(R"([{"url": "%s", "windowId": %d}])", |
| deep_url.spec().c_str(), iwa_window_id); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("ExtensionApiTabsIwaNavigateTest").Build(); |
| auto function = base::MakeRefCounted<TabsCreateFunction>(); |
| function->set_extension(extension); |
| |
| std::string error = api_test_utils::RunFunctionAndReturnError( |
| function.get(), args, profile()); |
| EXPECT_EQ(error, |
| "URLs with the 'isolated-app:' scheme cannot be opened with " |
| "tabs.create. Use windows.create instead."); |
| |
| // Only the original IWA window remains, still showing the start URL. |
| ASSERT_EQ(GlobalBrowserCollection::GetInstance()->GetSize(), 1ul); |
| ASSERT_EQ(iwa_tab_list->GetTabCount(), 1); |
| EXPECT_EQ(iwa_web_contents->GetLastCommittedURL(), |
| url_info.origin().GetURL()); |
| } |
| |
| // `tabs.update` cannot be used to navigate any tab (IWA or otherwise) to an |
| // `isolated-app:` URL; IWA navigations are only supported via the launch entry |
| // point used by `windows.create`. |
| IN_PROC_BROWSER_TEST_F(ExtensionApiTabsIwaNavigateTest, |
| TabsUpdateRejectsIwaUrl) { |
| auto url_info = InstallAndTrustBundle(); |
| BrowserWindowInterface* iwa_browser = OpenIwa(url_info); |
| |
| TabListInterface* iwa_tab_list = TabListInterface::From(iwa_browser); |
| ASSERT_EQ(iwa_tab_list->GetTabCount(), 1); |
| auto* iwa_web_contents = iwa_tab_list->GetActiveTab()->GetContents(); |
| content::WaitForLoadStop(iwa_web_contents); |
| int iwa_tab_id = ExtensionTabUtil::GetTabId(iwa_web_contents); |
| |
| GURL deep_url = url_info.origin().GetURL().Resolve("/deep/page.html"); |
| std::string args = base::StringPrintf(R"([%d, {"url": "%s"}])", iwa_tab_id, |
| deep_url.spec().c_str()); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("ExtensionApiTabsIwaNavigateTest").Build(); |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension); |
| |
| std::string error = api_test_utils::RunFunctionAndReturnError( |
| function.get(), args, profile()); |
| EXPECT_EQ(error, |
| "Cannot navigate to a URL with the 'isolated-app:' scheme via " |
| "tabs.update. Use windows.create instead."); |
| |
| // The IWA tab is still at its start URL. |
| EXPECT_EQ(iwa_web_contents->GetLastCommittedURL(), |
| url_info.origin().GetURL()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionApiTabsIwaMoveTest, CannotMoveIwaTab) { |
| auto url_info = InstallAndTrustBundle(); |
| BrowserWindowInterface* iwa_browser = OpenIwa(url_info); |
| |
| TabListInterface* iwa_tab_list = TabListInterface::From(iwa_browser); |
| ASSERT_EQ(iwa_tab_list->GetTabCount(), 1); |
| int iwa_tab_id = |
| ExtensionTabUtil::GetTabId(iwa_tab_list->GetTab(0)->GetContents()); |
| |
| Browser* normal_browser = CreateBrowser(profile()); |
| int target_window_id = ExtensionTabUtil::GetWindowId(normal_browser); |
| |
| auto function = base::MakeRefCounted<TabsMoveFunction>(); |
| |
| std::string args = base::StringPrintf( |
| R"([%d, {"windowId": %d, "index": -1}])", iwa_tab_id, target_window_id); |
| |
| std::string error = api_test_utils::RunFunctionAndReturnError( |
| function.get(), args, profile()); |
| |
| EXPECT_EQ(error, "The tab of an Isolated Web App cannot be moved."); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionApiTabsIwaMoveTest, |
| CannotGroupIwaTabToOtherWindow) { |
| auto url_info = InstallAndTrustBundle(); |
| BrowserWindowInterface* iwa_browser = OpenIwa(url_info); |
| |
| TabListInterface* iwa_tab_list = TabListInterface::From(iwa_browser); |
| ASSERT_EQ(iwa_tab_list->GetTabCount(), 1); |
| int iwa_tab_id = |
| ExtensionTabUtil::GetTabId(iwa_tab_list->GetTab(0)->GetContents()); |
| |
| Browser* normal_browser = CreateBrowser(profile()); |
| int target_window_id = ExtensionTabUtil::GetWindowId(normal_browser); |
| |
| auto function = base::MakeRefCounted<TabsGroupFunction>(); |
| |
| std::string args = base::StringPrintf( |
| R"([{"tabIds": [%d], "createProperties": {"windowId": %d}}])", iwa_tab_id, |
| target_window_id); |
| |
| std::string error = api_test_utils::RunFunctionAndReturnError( |
| function.get(), args, profile()); |
| |
| EXPECT_EQ(error, "The tab of an Isolated Web App cannot be moved."); |
| } |
| |
| // Tests that duplicating an IWA tab via the chrome.tabs.duplicate Extension API |
| // is disallowed. |
| IN_PROC_BROWSER_TEST_F(ExtensionApiTabsIwaDuplicateTest, |
| DuplicateTabDisallowed) { |
| web_app::IsolatedWebAppUrlInfo url_info = InstallAndTrustBundle(); |
| BrowserWindowInterface* iwa_browser = OpenIwa(url_info); |
| ASSERT_TRUE(iwa_browser); |
| |
| TabListInterface* iwa_tab_list = TabListInterface::From(iwa_browser); |
| ASSERT_EQ(iwa_tab_list->GetTabCount(), 1); |
| auto* iwa_web_contents = iwa_tab_list->GetActiveTab()->GetContents(); |
| content::WaitForLoadStop(iwa_web_contents); |
| int iwa_tab_id = ExtensionTabUtil::GetTabId(iwa_web_contents); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("ExtensionApiTabsIwaDuplicateTest") |
| .AddAPIPermission("tabs") |
| .Build(); |
| |
| auto function = base::MakeRefCounted<TabsDuplicateFunction>(); |
| function->set_extension(extension); |
| |
| std::string args = base::StringPrintf("[%d]", iwa_tab_id); |
| std::string error = api_test_utils::RunFunctionAndReturnError( |
| function.get(), args, profile()); |
| |
| EXPECT_EQ(error, "The tab of an Isolated Web App cannot be duplicated."); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DuplicateTab) { |
| content::OpenURLParams params(GURL(url::kAboutBlankURL), content::Referrer(), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui::PAGE_TRANSITION_LINK, false); |
| content::WebContents* web_contents = |
| browser()->OpenURL(params, /*navigation_handle_callback=*/{}); |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| int window_id = ExtensionTabUtil::GetWindowIdOfTab(web_contents); |
| int tab_index = -1; |
| TabStripModel* tab_strip; |
| ExtensionTabUtil::GetTabStripModel(web_contents, &tab_strip, &tab_index); |
| |
| auto duplicate_tab_function = base::MakeRefCounted<TabsDuplicateFunction>(); |
| scoped_refptr<const Extension> empty_tab_extension = |
| ExtensionBuilder("Test").AddAPIPermission("tabs").Build(); |
| duplicate_tab_function->set_extension(empty_tab_extension.get()); |
| duplicate_tab_function->set_has_callback(true); |
| |
| const base::DictValue duplicate_result = |
| utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| duplicate_tab_function.get(), base::StringPrintf("[%u]", tab_id), |
| profile())); |
| |
| int duplicate_tab_id = GetTabId(duplicate_result); |
| int duplicate_tab_window_id = GetTabWindowId(duplicate_result); |
| int duplicate_tab_index = |
| api_test_utils::GetInteger(duplicate_result, "index"); |
| // Duplicate tab id should be different from the original tab id. |
| EXPECT_NE(tab_id, duplicate_tab_id); |
| EXPECT_EQ(window_id, duplicate_tab_window_id); |
| EXPECT_EQ(tab_index + 1, duplicate_tab_index); |
| // The test empty tab extension has tabs permissions, therefore |
| // |duplicate_result| should contain url, pendingUrl, title or faviconUrl |
| // in the function result. |
| EXPECT_TRUE(HasAnyPrivacySensitiveFields(duplicate_result)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DuplicateTabNoPermission) { |
| content::OpenURLParams params(GURL(url::kAboutBlankURL), content::Referrer(), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui::PAGE_TRANSITION_LINK, false); |
| content::WebContents* web_contents = |
| browser()->OpenURL(params, /*navigation_handle_callback=*/{}); |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| int window_id = ExtensionTabUtil::GetWindowIdOfTab(web_contents); |
| int tab_index = -1; |
| TabStripModel* tab_strip; |
| ExtensionTabUtil::GetTabStripModel(web_contents, &tab_strip, &tab_index); |
| |
| auto duplicate_tab_function = base::MakeRefCounted<TabsDuplicateFunction>(); |
| scoped_refptr<const Extension> empty_extension( |
| ExtensionBuilder("Test").Build()); |
| duplicate_tab_function->set_extension(empty_extension.get()); |
| duplicate_tab_function->set_has_callback(true); |
| |
| const base::DictValue duplicate_result = |
| utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| duplicate_tab_function.get(), base::StringPrintf("[%u]", tab_id), |
| profile())); |
| |
| int duplicate_tab_id = GetTabId(duplicate_result); |
| int duplicate_tab_window_id = GetTabWindowId(duplicate_result); |
| int duplicate_tab_index = |
| api_test_utils::GetInteger(duplicate_result, "index"); |
| // Duplicate tab id should be different from the original tab id. |
| EXPECT_NE(tab_id, duplicate_tab_id); |
| EXPECT_EQ(window_id, duplicate_tab_window_id); |
| EXPECT_EQ(tab_index + 1, duplicate_tab_index); |
| // The test empty extension has no permissions, therefore |duplicate_result| |
| // should not contain url, pendingUrl, title and faviconUrl in the function |
| // result. |
| EXPECT_FALSE(HasAnyPrivacySensitiveFields(duplicate_result)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, NoTabsEventOnDevTools) { |
| extensions::ResultCatcher catcher; |
| ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply); |
| ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("tabs/no_events"))); |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| |
| DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync( |
| GetTabListInterface()->GetTab(0)->GetContents(), false /* is_docked */); |
| |
| listener.Reply("stop"); |
| |
| ASSERT_TRUE(catcher.GetNextResult()); |
| |
| DevToolsWindowTesting::CloseDevToolsWindowSync(devtools); |
| } |
| |
| #if BUILDFLAG(ENABLE_PLATFORM_APPS) |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTestWithApps, NoTabsAppWindow) { |
| extensions::ResultCatcher catcher; |
| ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply); |
| ASSERT_TRUE( |
| LoadExtension(test_data_dir_.AppendASCII("api_test/tabs/no_events"))); |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| |
| AppWindow* app_window = CreateTestAppWindow( |
| "{\"outerBounds\": " |
| "{\"width\": 300, \"height\": 300," |
| " \"minWidth\": 200, \"minHeight\": 200," |
| " \"maxWidth\": 400, \"maxHeight\": 400}}"); |
| |
| listener.Reply("stop"); |
| |
| ASSERT_TRUE(catcher.GetNextResult()); |
| |
| CloseAppWindow(app_window); |
| } |
| |
| // Crashes on Mac/Win only. http://crbug.com/40514319 |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_FilteredEvents DISABLED_FilteredEvents |
| #else |
| #define MAYBE_FilteredEvents FilteredEvents |
| #endif |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTestWithApps, MAYBE_FilteredEvents) { |
| extensions::ResultCatcher catcher; |
| ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply); |
| ASSERT_TRUE( |
| LoadExtension(test_data_dir_.AppendASCII("api_test/windows/events"))); |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| |
| AppWindow* app_window = CreateTestAppWindow( |
| "{\"outerBounds\": " |
| "{\"width\": 300, \"height\": 300," |
| " \"minWidth\": 200, \"minHeight\": 200," |
| " \"maxWidth\": 400, \"maxHeight\": 400}}"); |
| |
| Browser* browser_window = |
| Browser::Create(Browser::CreateParams(profile(), true)); |
| AddBlankTabAndShow(browser_window); |
| |
| DevToolsWindow* devtools_window = |
| DevToolsWindowTesting::OpenDevToolsWindowSync( |
| TabListInterface::From(browser_window_interface()) |
| ->GetTab(0) |
| ->GetContents(), |
| false /* is_docked */); |
| |
| chrome::CloseWindow(browser_window); |
| DevToolsWindowTesting::CloseDevToolsWindowSync(devtools_window); |
| CloseAppWindow(app_window); |
| |
| // TODO(llandwerlin): It seems creating an app window on MacOSX |
| // won't create an activation event whereas it does on all other |
| // platform. Disable focus event tests for now. |
| #if BUILDFLAG(IS_MAC) |
| listener.Reply(""); |
| #else |
| listener.Reply("focus"); |
| #endif |
| |
| ASSERT_TRUE(catcher.GetNextResult()); |
| } |
| |
| #endif // BUILDFLAG(ENABLE_PLATFORM_APPS) |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, OnBoundsChanged) { |
| extensions::ResultCatcher catcher; |
| ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply); |
| ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("windows/bounds"))); |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| |
| gfx::Rect rect = browser_window_interface()->GetWindow()->GetBounds(); |
| rect.Inset(10); |
| browser_window_interface()->GetWindow()->SetBounds(rect); |
| |
| listener.Reply(base::StringPrintf( |
| R"({"top": %u, "left": %u, "width": %u, "height": %u})", rect.y(), |
| rect.x(), rect.width(), rect.height())); |
| |
| ASSERT_TRUE(catcher.GetNextResult()); |
| } |
| |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, WindowsCreate) { |
| ASSERT_TRUE(RunExtensionTest("windows/create")) << message_; |
| } |
| |
| // Tests that non-validation failure in tabs.executeScript results in error, and |
| // not bad_message. |
| // Regression test for https://crbug.com/40482984. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, ExecuteScriptNoTabIsNonFatalError) { |
| scoped_refptr<const Extension> extension_with_tabs_permission = |
| ExtensionBuilder("Test") |
| .AddAPIPermission("tabs") |
| .AddHostPermission("*://*/*") |
| .Build(); |
| auto function = base::MakeRefCounted<TabsExecuteScriptFunction>(); |
| function->set_extension(extension_with_tabs_permission.get()); |
| const char* kArgs = R"(["", {"code": ""}])"; |
| |
| // Use another profile: `profile()` already has a browser window set up as |
| // part of a browser test so executing the "script" would succeed instead of |
| // failing as the test intends. |
| auto* second_profile = profile()->GetOffTheRecordProfile( |
| Profile::OTRProfileID::CreateUniqueForTesting(), |
| /*create_if_needed=*/true); |
| |
| std::string error = utils::RunFunctionAndReturnError( |
| function.get(), kArgs, second_profile, utils::FunctionMode::kNone); |
| EXPECT_EQ(ExtensionTabUtil::kNoCurrentWindowError, error); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, ExecuteScriptOnDevTools) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("Test").AddAPIPermission("tabs").Build(); |
| |
| DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync( |
| GetTabListInterface()->GetTab(0)->GetContents(), false /* is_docked */); |
| |
| auto function = base::MakeRefCounted<TabsExecuteScriptFunction>(); |
| function->set_extension(extension.get()); |
| |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf("[%u, {\"code\": \"true\"}]", |
| api::windows::WINDOW_ID_CURRENT), |
| DevToolsWindowTesting::Get(devtools)->browser()->GetProfile()), |
| manifest_errors::kCannotAccessPageWithUrl)); |
| |
| DevToolsWindowTesting::CloseDevToolsWindowSync(devtools); |
| } |
| |
| // TODO(crbug.com/504781983): Fails on Linux. |
| #if BUILDFLAG(IS_LINUX) |
| #define MAYBE_DiscardedProperty DISABLED_DiscardedProperty |
| #else |
| #define MAYBE_DiscardedProperty DiscardedProperty |
| #endif |
| // TODO(georgesak): change this browsertest to an unittest. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, MAYBE_DiscardedProperty) { |
| ASSERT_TRUE(g_browser_process && g_browser_process->GetTabManager()); |
| resource_coordinator::TabManager* tab_manager = |
| g_browser_process->GetTabManager(); |
| |
| // To avoid flakes when focus changes, set the active tab strip model |
| // explicitly. |
| resource_coordinator::GetTabLifecycleUnitSource() |
| ->SetFocusedTabStripModelForTesting(browser()->tab_strip_model()); |
| |
| // Create two additional tabs. |
| content::OpenURLParams params(GURL(url::kAboutBlankURL), content::Referrer(), |
| WindowOpenDisposition::NEW_BACKGROUND_TAB, |
| ui::PAGE_TRANSITION_LINK, false); |
| content::WebContents* web_contents_a = |
| browser()->OpenURL(params, /*navigation_handle_callback=*/{}); |
| content::WebContents* web_contents_b = |
| browser()->OpenURL(params, /*navigation_handle_callback=*/{}); |
| |
| // Set up query function with an extension. |
| scoped_refptr<const Extension> extension = ExtensionBuilder("Test").Build(); |
| |
| // Get non-discarded tabs. |
| { |
| base::ListValue result( |
| RunQueryFunction(extension.get(), "[{\"discarded\": false}]")); |
| |
| // The two created plus the default tab. |
| EXPECT_EQ(3u, result.size()); |
| } |
| |
| // Get discarded tabs. |
| { |
| base::ListValue result( |
| RunQueryFunction(extension.get(), "[{\"discarded\": true}]")); |
| EXPECT_EQ(0u, result.size()); |
| } |
| |
| TabListInterface* tab_list = |
| TabListInterface::From(browser_window_interface()); |
| |
| // Creates Tab object to ensure the property is correct for the extension. |
| api::tabs::Tab tab_object_a = ExtensionTabUtil::CreateTabObject( |
| web_contents_a, kDontScrubBehavior, nullptr, tab_list, 0); |
| EXPECT_FALSE(tab_object_a.discarded); |
| |
| // Discards one tab. |
| EXPECT_TRUE(tab_manager->DiscardTabByExtension(web_contents_a)); |
| web_contents_a = GetTabListInterface()->GetTab(1)->GetContents(); |
| |
| // Make sure the property is changed accordingly after discarding the tab. |
| tab_object_a = ExtensionTabUtil::CreateTabObject( |
| web_contents_a, kDontScrubBehavior, nullptr, tab_list, 0); |
| EXPECT_TRUE(tab_object_a.discarded); |
| |
| // Get non-discarded tabs after discarding one tab. |
| { |
| base::ListValue result( |
| RunQueryFunction(extension.get(), "[{\"discarded\": false}]")); |
| EXPECT_EQ(2u, result.size()); |
| } |
| |
| // Get discarded tabs after discarding one tab. |
| { |
| base::ListValue result( |
| RunQueryFunction(extension.get(), "[{\"discarded\": true}]")); |
| EXPECT_EQ(1u, result.size()); |
| |
| // Make sure the returned tab is the correct one. |
| int tab_id_a = ExtensionTabUtil::GetTabId(web_contents_a); |
| |
| ASSERT_TRUE(result[0].is_dict()); |
| std::optional<int> id = result[0].GetDict().FindInt(extension_misc::kId); |
| ASSERT_TRUE(id); |
| |
| EXPECT_EQ(tab_id_a, *id); |
| } |
| |
| // Discards another created tab. |
| EXPECT_TRUE(tab_manager->DiscardTabByExtension(web_contents_b)); |
| |
| // Get non-discarded tabs after discarding two created tabs. |
| { |
| base::ListValue result( |
| RunQueryFunction(extension.get(), "[{\"discarded\": false}]")); |
| ASSERT_EQ(1u, result.size()); |
| |
| // Make sure the returned tab is the correct one. |
| int tab_id_c = ExtensionTabUtil::GetTabId( |
| GetTabListInterface()->GetTab(0)->GetContents()); |
| |
| ASSERT_TRUE(result[0].is_dict()); |
| std::optional<int> id = result[0].GetDict().FindInt(extension_misc::kId); |
| ASSERT_TRUE(id); |
| |
| EXPECT_EQ(tab_id_c, *id); |
| } |
| |
| // Get discarded tabs after discarding two created tabs. |
| { |
| base::ListValue result( |
| RunQueryFunction(extension.get(), "[{\"discarded\": true}]")); |
| EXPECT_EQ(2u, result.size()); |
| } |
| |
| // Activates the first created tab. |
| browser()->tab_strip_model()->ActivateTabAt(1); |
| |
| // Get non-discarded tabs after activating a discarded tab. |
| { |
| base::ListValue result( |
| RunQueryFunction(extension.get(), "[{\"discarded\": false}]")); |
| EXPECT_EQ(2u, result.size()); |
| } |
| |
| // Get discarded tabs after activating a discarded tab. |
| { |
| base::ListValue result( |
| RunQueryFunction(extension.get(), "[{\"discarded\": true}]")); |
| EXPECT_EQ(1u, result.size()); |
| } |
| } |
| |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // Tests chrome.tabs.discard(tabId). |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DiscardWithId) { |
| // Create an additional tab and navigate to `url::kAboutBlankURL`. |
| GetTabListInterface()->OpenTab(GURL(url::kAboutBlankURL), -1); |
| content::WebContents* web_contents = |
| GetTabListInterface()->GetTab(1)->GetContents(); |
| content::WaitForLoadStop(web_contents); |
| EXPECT_FALSE(web_contents->WasDiscarded()); |
| |
| // Activate the first tab since the second one will be discarded and active |
| // tabs cannot be discarded on Android. |
| GetTabListInterface()->ActivateTab( |
| GetTabListInterface()->GetTab(0)->GetHandle()); |
| |
| // Set up the function with an extension. |
| scoped_refptr<const Extension> extension = ExtensionBuilder("Test").Build(); |
| auto discard = base::MakeRefCounted<TabsDiscardFunction>(); |
| discard->set_extension(extension.get()); |
| |
| // Run function passing the tab id as argument. |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| const base::DictValue result = |
| utils::ToDict(utils::RunFunctionAndReturnSingleResult( |
| discard.get(), base::StringPrintf("[%u]", tab_id), profile())); |
| |
| // Confirm that the tab was discarded. |
| web_contents = GetTabListInterface()->GetTab(1)->GetContents(); |
| EXPECT_TRUE(web_contents->WasDiscarded()); |
| |
| // Make sure the returned tab is the one discarded and its discarded state is |
| // correct. |
| tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| EXPECT_EQ(tab_id, api_test_utils::GetInteger(result, "id")); |
| EXPECT_TRUE(api_test_utils::GetBoolean(result, "discarded")); |
| // The result should be scrubbed. |
| EXPECT_FALSE(result.contains("url")); |
| |
| // Tests chrome.tabs.discard(tabId) with an already discarded tab. It has to |
| // return the error stating that the tab couldn't be discarded. |
| auto discarded = base::MakeRefCounted<TabsDiscardFunction>(); |
| discarded->set_extension(extension.get()); |
| std::string error = utils::RunFunctionAndReturnError( |
| discarded.get(), base::StringPrintf("[%u]", tab_id), profile()); |
| EXPECT_TRUE(base::MatchPattern(error, keys::kCannotDiscardTab)); |
| } |
| |
| // Tests chrome.tabs.discard(invalidId). |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DiscardWithInvalidId) { |
| // Create an additional tab. |
| GetTabListInterface()->OpenTab(GURL(url::kAboutBlankURL), -1); |
| content::WebContents* web_contents = |
| GetTabListInterface()->GetTab(1)->GetContents(); |
| content::WaitForLoadStop(web_contents); |
| EXPECT_FALSE(web_contents->WasDiscarded()); |
| |
| // Set up the function with an extension. |
| scoped_refptr<const Extension> extension = ExtensionBuilder("Test").Build(); |
| auto discard = base::MakeRefCounted<TabsDiscardFunction>(); |
| discard->set_extension(extension.get()); |
| |
| // Run function passing an invalid id as argument. |
| int tab_invalid_id = ExtensionTabUtil::GetTabId( |
| GetTabListInterface()->GetTab(0)->GetContents()); |
| tab_invalid_id = std::max( |
| tab_invalid_id, ExtensionTabUtil::GetTabId( |
| GetTabListInterface()->GetTab(1)->GetContents())); |
| tab_invalid_id += 999999; |
| |
| std::string error = utils::RunFunctionAndReturnError( |
| discard.get(), base::StringPrintf("[%u]", tab_invalid_id), profile()); |
| |
| // The tab should not be discarded. |
| EXPECT_FALSE(GetTabListInterface()->GetTab(1)->GetContents()->WasDiscarded()); |
| |
| // Check error message. |
| EXPECT_TRUE(base::MatchPattern(error, ExtensionTabUtil::kTabNotFoundError)); |
| } |
| |
| // Tests chrome.tabs.discard for an incognito tab when the extension doesn't |
| // have incognito access. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DiscardIncognitoWithoutPermission) { |
| // Create an extra normal tab so we have more to discard. |
| GetTabListInterface()->OpenTab(GURL(url::kAboutBlankURL), -1); |
| |
| // Create an incognito browser with several tabs. |
| BrowserWindowInterface* incognito_browser = CreateIncognitoBrowserWindow(); |
| TabListInterface* incognito_tab_list = |
| TabListInterface::From(incognito_browser); |
| incognito_tab_list->OpenTab(GURL(url::kAboutBlankURL), -1); |
| incognito_tab_list->OpenTab(GURL(url::kAboutBlankURL), -1); |
| incognito_tab_list->OpenTab(GURL(url::kAboutBlankURL), -1); |
| content::WebContents* incognito_web_contents = |
| incognito_tab_list->GetTab(1)->GetContents(); |
| content::WaitForLoadStop(incognito_web_contents); |
| EXPECT_FALSE(incognito_web_contents->WasDiscarded()); |
| |
| // Set up the function with an extension that does not have incognito access, |
| // but does have the "tabs" permission. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("Test").AddAPIPermission("tabs").Build(); |
| auto discard = base::MakeRefCounted<TabsDiscardFunction>(); |
| discard->set_extension(extension.get()); |
| |
| int tab_id = ExtensionTabUtil::GetTabId(incognito_web_contents); |
| |
| // First try discarding the incognito tab based on the tab ID. The tab should |
| // not be discarded and we should get an error. |
| std::string error = utils::RunFunctionAndReturnError( |
| discard.get(), base::StringPrintf("[%u]", tab_id), profile()); |
| EXPECT_FALSE(incognito_web_contents->WasDiscarded()); |
| EXPECT_TRUE(base::MatchPattern(error, ExtensionTabUtil::kTabNotFoundError)); |
| |
| // Now run without passing an id. The extension only has access to the normal |
| // tabs, so only the normal tabs should be discardable. |
| int normal_tab_count = GetTabListInterface()->GetTabCount(); |
| // Note: To avoid having to deal with any platform differences between default |
| // tabs when creating a browser changing the total count, we just validate we |
| // have more than 0 tabs, so we know the loop below actually does something. |
| EXPECT_GT(normal_tab_count, 0); |
| |
| std::vector<base::DictValue> results; |
| for (int i = 0; i < normal_tab_count; ++i) { |
| auto discard_no_id = base::MakeRefCounted<TabsDiscardFunction>(); |
| discard_no_id->set_extension(extension.get()); |
| std::optional<base::Value> result_value = |
| utils::RunFunctionAndReturnSingleResult(discard_no_id.get(), "[]", |
| profile()); |
| if (result_value) { |
| results.push_back(utils::ToDict(std::move(*result_value))); |
| } |
| } |
| |
| // We should have discarded all normal tabs. |
| EXPECT_EQ(static_cast<size_t>(normal_tab_count), results.size()); |
| for (const auto& result : results) { |
| // Check that the returned tab does not have sensitive incognito |
| // information. It must be a normal tab. Since the extension has "tabs" |
| // permission, it should include url and title for the normal tab. |
| EXPECT_FALSE(api_test_utils::GetBoolean(result, "incognito")); |
| EXPECT_TRUE(result.contains("url")); |
| EXPECT_TRUE(result.contains("title")); |
| } |
| |
| // The next attempt to discard without an ID should fail. |
| auto discard_fail = base::MakeRefCounted<TabsDiscardFunction>(); |
| discard_fail->set_extension(extension.get()); |
| std::string fail_error = |
| utils::RunFunctionAndReturnError(discard_fail.get(), "[]", profile()); |
| EXPECT_EQ("Cannot find a tab to discard.", fail_error); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DiscardIncognitoSplitMode) { |
| // Create an extra normal tab so we have more to discard. |
| GetTabListInterface()->OpenTab(GURL(url::kAboutBlankURL), -1); |
| |
| // Create an incognito browser with several tabs. |
| BrowserWindowInterface* incognito_browser = CreateIncognitoBrowserWindow(); |
| TabListInterface* incognito_tab_list = |
| TabListInterface::From(incognito_browser); |
| incognito_tab_list->OpenTab(GURL(url::kAboutBlankURL), -1); |
| incognito_tab_list->OpenTab(GURL(url::kAboutBlankURL), -1); |
| incognito_tab_list->OpenTab(GURL(url::kAboutBlankURL), -1); |
| |
| // Set up the extension with incognito: split and tabs permission. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("Test") |
| .SetManifestKey("incognito", "split") |
| .AddAPIPermission("tabs") |
| .Build(); |
| // Grant incognito access. |
| ExtensionPrefs::Get(profile())->SetIsIncognitoEnabled(extension->id(), true); |
| |
| // 1. Test Regular Profile Context |
| // The regular profile context only sees normal tabs, all of which should be |
| // discardable. |
| int normal_tab_count = GetTabListInterface()->GetTabCount(); |
| // Note: To avoid having to deal with any platform differences between default |
| // tabs when creating a browser changing the total count, we just validate we |
| // have more than 0 tabs, so we know the loop below actually does something. |
| EXPECT_GT(normal_tab_count, 0); |
| |
| std::vector<base::DictValue> regular_results; |
| for (int i = 0; i < normal_tab_count; ++i) { |
| auto discard = base::MakeRefCounted<TabsDiscardFunction>(); |
| discard->set_extension(extension.get()); |
| std::optional<base::Value> result_value = |
| utils::RunFunctionAndReturnSingleResult(discard.get(), "[]", profile()); |
| if (result_value) { |
| regular_results.push_back(utils::ToDict(std::move(*result_value))); |
| } |
| } |
| |
| EXPECT_EQ(static_cast<size_t>(normal_tab_count), regular_results.size()); |
| for (const auto& result : regular_results) { |
| // Regular context should only see normal tabs. |
| EXPECT_FALSE(api_test_utils::GetBoolean(result, "incognito")); |
| EXPECT_TRUE(result.contains("url")); |
| } |
| |
| // The next attempt from the regular context should fail. |
| auto discard_regular_fail = base::MakeRefCounted<TabsDiscardFunction>(); |
| discard_regular_fail->set_extension(extension.get()); |
| std::string regular_fail_error = utils::RunFunctionAndReturnError( |
| discard_regular_fail.get(), "[]", profile()); |
| EXPECT_EQ("Cannot find a tab to discard.", regular_fail_error); |
| |
| // 2. Test Incognito Profile Context |
| // The incognito context only sees incognito tabs. We created several tabs in |
| // the incognito browser, all of which should be discardable. |
| int incognito_tab_count = incognito_tab_list->GetTabCount(); |
| // Note: To avoid having to deal with any platform differences between default |
| // tabs when creating a browser changing the total count, we just validate we |
| // have more than 0 tabs, so we know the loop below actually does something. |
| EXPECT_GT(incognito_tab_count, 0); |
| |
| // Add a few more regular tabs, as all the previous ones are already |
| // discarded. |
| GetTabListInterface()->OpenTab(GURL(url::kAboutBlankURL), -1); |
| GetTabListInterface()->OpenTab(GURL(url::kAboutBlankURL), -1); |
| |
| Profile* incognito_profile = incognito_browser->GetProfile(); |
| std::vector<base::DictValue> incognito_results; |
| for (int i = 0; i < incognito_tab_count; ++i) { |
| auto discard_incognito = base::MakeRefCounted<TabsDiscardFunction>(); |
| discard_incognito->set_extension(extension.get()); |
| std::optional<base::Value> incognito_result_value = |
| utils::RunFunctionAndReturnSingleResult( |
| discard_incognito.get(), "[]", incognito_profile, |
| api_test_utils::FunctionMode::kIncognito); |
| if (incognito_result_value) { |
| incognito_results.push_back( |
| utils::ToDict(std::move(*incognito_result_value))); |
| } |
| } |
| |
| EXPECT_EQ(static_cast<size_t>(incognito_tab_count), incognito_results.size()); |
| for (const auto& result : incognito_results) { |
| // Incognito split context should only see incognito tabs. |
| EXPECT_TRUE(api_test_utils::GetBoolean(result, "incognito")); |
| EXPECT_TRUE(result.contains("url")); |
| } |
| |
| // The next attempt from the incognito context should fail. |
| auto discard_incognito_fail = base::MakeRefCounted<TabsDiscardFunction>(); |
| discard_incognito_fail->set_extension(extension.get()); |
| std::string incognito_fail_error = utils::RunFunctionAndReturnError( |
| discard_incognito_fail.get(), "[]", incognito_profile, |
| api_test_utils::FunctionMode::kIncognito); |
| EXPECT_EQ("Cannot find a tab to discard.", incognito_fail_error); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DiscardIncognitoSpanningMode) { |
| // Create an extra normal tab so we have more to discard. |
| GetTabListInterface()->OpenTab(GURL(url::kAboutBlankURL), -1); |
| |
| // Create an incognito browser with several tabs. |
| BrowserWindowInterface* incognito_browser = CreateIncognitoBrowserWindow(); |
| TabListInterface* incognito_tab_list = |
| TabListInterface::From(incognito_browser); |
| incognito_tab_list->OpenTab(GURL(url::kAboutBlankURL), -1); |
| incognito_tab_list->OpenTab(GURL(url::kAboutBlankURL), -1); |
| |
| // Set up the extension with incognito: spanning and tabs permission. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("Test") |
| .SetManifestKey("incognito", "spanning") |
| .AddAPIPermission("tabs") |
| .Build(); |
| // Grant incognito access. |
| ExtensionPrefs::Get(profile())->SetIsIncognitoEnabled(extension->id(), true); |
| |
| // Test Regular Profile Context |
| // In spanning mode, the extension shares a single process for both normal and |
| // incognito contexts. All tabs across both windows are discardable. |
| // We determine the number of tabs dynamically rather than using a hardcoded |
| // value because some platforms (like Android) may create an extra initial |
| // tab in some window creation scenarios. |
| int total_tab_count = |
| GetTabListInterface()->GetTabCount() + incognito_tab_list->GetTabCount(); |
| // Note: To avoid having to deal with any platform differences between default |
| // tabs when creating a browser changing the total count, we just validate we |
| // have more than 0 tabs, so we know the loop below actually does something. |
| EXPECT_GT(total_tab_count, 0); |
| |
| std::vector<base::DictValue> results; |
| bool saw_incognito = false; |
| bool saw_normal = false; |
| for (int i = 0; i < total_tab_count; ++i) { |
| auto discard = base::MakeRefCounted<TabsDiscardFunction>(); |
| discard->set_extension(extension.get()); |
| std::optional<base::Value> result_value = |
| utils::RunFunctionAndReturnSingleResult( |
| discard.get(), "[]", profile(), |
| api_test_utils::FunctionMode::kIncognito); |
| if (result_value) { |
| base::DictValue dict = utils::ToDict(std::move(*result_value)); |
| if (api_test_utils::GetBoolean(dict, "incognito")) { |
| saw_incognito = true; |
| } else { |
| saw_normal = true; |
| } |
| results.push_back(std::move(dict)); |
| } |
| } |
| |
| EXPECT_EQ(static_cast<size_t>(total_tab_count), results.size()); |
| // A spanning extension should receive info about both normal and incognito |
| // tabs and they should contain URL info since it has tabs permission. |
| EXPECT_TRUE(saw_incognito); |
| EXPECT_TRUE(saw_normal); |
| for (const auto& result : results) { |
| EXPECT_TRUE(result.contains("url")); |
| } |
| |
| // The next attempt should fail. |
| auto discard_fail = base::MakeRefCounted<TabsDiscardFunction>(); |
| discard_fail->set_extension(extension.get()); |
| std::string fail_error = utils::RunFunctionAndReturnError( |
| discard_fail.get(), "[]", profile(), |
| api_test_utils::FunctionMode::kIncognito); |
| EXPECT_EQ("Cannot find a tab to discard.", fail_error); |
| } |
| |
| // TODO(crbug.com/487907630): Flaky on macos |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_DiscardWithoutId DISABLED_DiscardWithoutId |
| #else |
| #define MAYBE_DiscardWithoutId DiscardWithoutId |
| #endif |
| // Tests chrome.tabs.discard(). |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, MAYBE_DiscardWithoutId) { |
| // Create an additional tab. |
| GetTabListInterface()->OpenTab(GURL(url::kAboutBlankURL), -1); |
| content::WebContents* web_contents = |
| GetTabListInterface()->GetTab(1)->GetContents(); |
| content::WaitForLoadStop(web_contents); |
| EXPECT_FALSE(web_contents->WasDiscarded()); |
| |
| // Activate the first created tab to make the second one the least recently |
| // used and therefore the one that should be discarded. |
| GetTabListInterface()->ActivateTab( |
| GetTabListInterface()->GetTab(0)->GetHandle()); |
| |
| // Set up the function with an extension. |
| scoped_refptr<const Extension> extension = ExtensionBuilder("Test").Build(); |
| auto discard = base::MakeRefCounted<TabsDiscardFunction>(); |
| discard->set_extension(extension.get()); |
| |
| // Run without passing an id. |
| const base::DictValue result = utils::ToDict( |
| utils::RunFunctionAndReturnSingleResult(discard.get(), "[]", profile())); |
| |
| // Confirm that the tab was discarded. |
| web_contents = GetTabListInterface()->GetTab(1)->GetContents(); |
| EXPECT_TRUE(web_contents->WasDiscarded()); |
| |
| // Make sure the returned tab is the one discarded and its discarded state is |
| // correct. |
| EXPECT_EQ(ExtensionTabUtil::GetTabId(web_contents), |
| api_test_utils::GetInteger(result, "id")); |
| EXPECT_TRUE(api_test_utils::GetBoolean(result, "discarded")); |
| // The result should be scrubbed. |
| EXPECT_FALSE(result.contains("url")); |
| } |
| |
| // Tests that calling chrome.tabs.discard on a saved tab does discard. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DiscardSavedTabGroupTabAllowed) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("DiscardTest").Build(); |
| const GURL kExampleCom("http://example.com"); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| tabs::TabInterface* tab = tab_list->OpenTab(kExampleCom, -1); |
| content::WebContents* web_contents = tab->GetContents(); |
| |
| int index = tab_list->GetIndexOfTab(tab->GetHandle()); |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| |
| tab_groups::TabGroupSyncService* saved_service = |
| tab_groups::TabGroupSyncServiceFactory::GetForProfile(profile()); |
| ASSERT_TRUE(saved_service); |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| tab_groups::TabGroupSyncServiceInitializedObserver sync_observer( |
| saved_service); |
| sync_observer.Wait(); |
| #endif |
| |
| // Activate the first tab since the second one will be discarded and active |
| // tabs cannot be discarded on Android. |
| GetTabListInterface()->ActivateTab( |
| GetTabListInterface()->GetTab(0)->GetHandle()); |
| |
| // Group the tab and save it. |
| std::optional<tab_groups::TabGroupId> group = |
| tab_list->CreateTabGroup({tab->GetHandle()}); |
| ASSERT_TRUE(group.has_value()); |
| tab_groups::TabGroupVisualData visual_data( |
| u"Initial title", tab_groups::TabGroupColorId::kBlue); |
| tab_list->SetTabGroupVisualData(*group, visual_data); |
| |
| auto function = base::MakeRefCounted<TabsDiscardFunction>(); |
| function->set_extension(extension); |
| EXPECT_TRUE(utils::RunFunction(function.get(), |
| base::StringPrintf("[%d]", tab_id), profile(), |
| utils::FunctionMode::kNone)); |
| |
| // Check that the tab was discarded |
| EXPECT_TRUE(tab_list->GetTab(index)->GetContents()->WasDiscarded()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TestGroupDetachedAndReInserted) { |
| // Create the `TabsEventRouter`, which is required to get a tab update event. |
| TabsWindowsAPI::Get(profile())->InitTabsEventRouter(); |
| |
| GURL about_blank("about:blank"); |
| ASSERT_TRUE(NavigateToURLInNewTab(about_blank)); |
| ASSERT_TRUE(NavigateToURLInNewTab(about_blank)); |
| ASSERT_TRUE(NavigateToURLInNewTab(about_blank)); |
| |
| TabListInterface* tab_list = |
| TabListInterface::From(browser_window_interface()); |
| std::optional<tab_groups::TabGroupId> group = tab_list->CreateTabGroup({ |
| tab_list->GetTab(0)->GetHandle(), |
| tab_list->GetTab(1)->GetHandle(), |
| }); |
| ASSERT_TRUE(group); |
| |
| BrowserWindowInterface* second_browser = |
| CreateBrowserWindowWithType(BrowserWindowInterface::TYPE_NORMAL); |
| ASSERT_TRUE(second_browser); |
| TabListInterface* destination_tab_list = |
| TabListInterface::From(second_browser); |
| ASSERT_TRUE(destination_tab_list); |
| destination_tab_list->OpenTab(about_blank, -1); |
| |
| TestEventRouterObserver event_observer(EventRouter::Get(profile())); |
| |
| tab_list->MoveTabGroupToWindow(*group, second_browser->GetSessionID(), 0); |
| |
| // Verify a tabs.onUpdated event was sent. In practice, more than one is |
| // dispatched (multiple tabs and they are added / removed from a group). |
| // The exact events are tested more thoroughly in a similar test in the |
| // API test `ExtensionApiTabTest.MovingAGroupToANewWindow`. |
| event_observer.WaitForEventWithName(api::tabs::OnUpdated::kEventName); |
| EXPECT_TRUE( |
| event_observer.events().contains(api::tabs::OnUpdated::kEventName)); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, Freezing) { |
| // Create a background tab. |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), GURL(url::kAboutBlankURL), |
| WindowOpenDisposition::NEW_BACKGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| content::WebContents* web_contents = |
| GetTabListInterface()->GetTab(1)->GetContents(); |
| |
| // Create an extension. |
| scoped_refptr<const Extension> extension = ExtensionBuilder("Test").Build(); |
| |
| // Query all tabs. Their "frozen" property should be false. |
| { |
| auto query_function = base::MakeRefCounted<TabsQueryFunction>(); |
| query_function->set_extension(extension.get()); |
| base::ListValue result( |
| utils::ToList(utils::RunFunctionAndReturnSingleResult( |
| query_function.get(), "[{}]", profile()))); |
| ASSERT_EQ(2u, result.size()); |
| |
| for (auto& element : result) { |
| base::DictValue dict = utils::ToDict(element); |
| std::optional<bool> frozen = dict.FindBool("frozen"); |
| ASSERT_TRUE(frozen.has_value()); |
| EXPECT_FALSE(frozen.value()); |
| } |
| } |
| |
| // Create the `TabsEventRouter`, which is required to get a tab update event. |
| TabsWindowsAPI::Get(profile())->InitTabsEventRouter(); |
| |
| // Freeze the background tab and wait for a tab update event. |
| TestEventRouterObserver event_router_observer(EventRouter::Get(profile())); |
| web_contents->SetPageFrozen(true); |
| event_router_observer.WaitForEventWithName(api::tabs::OnUpdated::kEventName); |
| |
| // Check arguments for the tab update event. |
| // |
| // Note: Must simulate dispatching to an actual extension to get arguments, |
| // because the tab details exposed vary by extension. The arguments for the |
| // event received by `WillDispatchEvent()` are empty. |
| { |
| auto event_it = |
| event_router_observer.events().find(api::tabs::OnUpdated::kEventName); |
| ASSERT_NE(event_it, event_router_observer.events().end()); |
| std::optional<base::ListValue> args_out; |
| mojom::EventFilteringInfoPtr event_filtering_info_out; |
| EXPECT_TRUE(event_it->second->will_dispatch_callback.Run( |
| profile(), mojom::ContextType::kUnprivilegedExtension, extension.get(), |
| /*listener_filter=*/nullptr, args_out, event_filtering_info_out, |
| /*dispatch_separate_event_out=*/nullptr)); |
| ASSERT_TRUE(args_out.has_value()); |
| ASSERT_EQ(args_out->size(), 3U); |
| EXPECT_EQ(ExtensionTabUtil::GetTabId(web_contents), |
| args_out.value()[0].GetInt()); |
| auto changed_properties = utils::ToDict(args_out.value()[1]); |
| auto frozen_changed_property = changed_properties.FindBool("frozen"); |
| ASSERT_TRUE(frozen_changed_property.has_value()); |
| EXPECT_TRUE(frozen_changed_property.value()); |
| } |
| |
| // Query frozen tabs. There should be 1 and its "frozen" property should be |
| // true. |
| { |
| auto query_function = base::MakeRefCounted<TabsQueryFunction>(); |
| query_function->set_extension(extension.get()); |
| base::ListValue result( |
| utils::ToList(utils::RunFunctionAndReturnSingleResult( |
| query_function.get(), "[{\"frozen\": true}]", profile()))); |
| ASSERT_EQ(1u, result.size()); |
| base::DictValue tab = utils::ToDict(result.front()); |
| std::optional<bool> frozen = tab.FindBool("frozen"); |
| ASSERT_TRUE(frozen.has_value()); |
| EXPECT_TRUE(frozen.value()); |
| EXPECT_EQ(ExtensionTabUtil::GetTabId(web_contents), |
| api_test_utils::GetInteger(tab, "id")); |
| } |
| |
| // Query non-frozen tabs. There should be 1 and its "frozen" property should |
| // be false. |
| { |
| auto query_function = base::MakeRefCounted<TabsQueryFunction>(); |
| query_function->set_extension(extension.get()); |
| base::ListValue result( |
| utils::ToList(utils::RunFunctionAndReturnSingleResult( |
| query_function.get(), "[{\"frozen\": false}]", profile()))); |
| ASSERT_EQ(1u, result.size()); |
| std::optional<bool> frozen = |
| utils::ToDict(result.front()).FindBool("frozen"); |
| ASSERT_TRUE(frozen.has_value()); |
| EXPECT_FALSE(frozen.value()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, AutoDiscardableProperty) { |
| // Create two additional tabs. |
| content::OpenURLParams params(GURL(url::kAboutBlankURL), content::Referrer(), |
| WindowOpenDisposition::NEW_BACKGROUND_TAB, |
| ui::PAGE_TRANSITION_LINK, false); |
| content::WebContents* web_contents_a = |
| browser()->OpenURL(params, /*navigation_handle_callback=*/{}); |
| content::WebContents* web_contents_b = |
| browser()->OpenURL(params, /*navigation_handle_callback=*/{}); |
| |
| // Creates Tab object to ensure the property is correct for the extension. |
| TabListInterface* tab_list = |
| TabListInterface::From(browser_window_interface()); |
| api::tabs::Tab tab_object_a = ExtensionTabUtil::CreateTabObject( |
| web_contents_a, kDontScrubBehavior, nullptr, tab_list, 0); |
| EXPECT_TRUE(tab_object_a.auto_discardable); |
| |
| // Set up query and update functions with the extension. |
| scoped_refptr<const Extension> extension = ExtensionBuilder("Test").Build(); |
| |
| // Queries and results used. |
| const char* kAutoDiscardableQueryInfo = "[{\"autoDiscardable\": true}]"; |
| const char* kNonAutoDiscardableQueryInfo = "[{\"autoDiscardable\": false}]"; |
| |
| // Get auto-discardable tabs. Returns all since tabs are auto-discardable |
| // by default. |
| base::ListValue query_result = |
| RunQueryFunction(extension.get(), kAutoDiscardableQueryInfo); |
| EXPECT_EQ(3u, query_result.size()); |
| |
| // Get non auto-discardable tabs. |
| query_result = |
| RunQueryFunction(extension.get(), kNonAutoDiscardableQueryInfo); |
| EXPECT_EQ(0u, query_result.size()); |
| |
| // Update the auto-discardable state of web contents A. |
| int tab_id_a = ExtensionTabUtil::GetTabId(web_contents_a); |
| base::DictValue update_result = RunUpdateFunction( |
| extension.get(), |
| base::StringPrintf("[%u, {\"autoDiscardable\": false}]", tab_id_a)); |
| EXPECT_EQ(tab_id_a, api_test_utils::GetInteger(update_result, "id")); |
| EXPECT_FALSE(api_test_utils::GetBoolean(update_result, "autoDiscardable")); |
| |
| // Make sure the property is changed accordingly after updating the tab. |
| tab_object_a = ExtensionTabUtil::CreateTabObject( |
| web_contents_a, kDontScrubBehavior, nullptr, tab_list, 0); |
| EXPECT_FALSE(tab_object_a.auto_discardable); |
| |
| // Get auto-discardable tabs after changing the status of web contents A. |
| query_result = RunQueryFunction(extension.get(), kAutoDiscardableQueryInfo); |
| EXPECT_EQ(2u, query_result.size()); |
| |
| // Get non auto-discardable tabs after changing the status of web contents A. |
| query_result = |
| RunQueryFunction(extension.get(), kNonAutoDiscardableQueryInfo); |
| ASSERT_EQ(1u, query_result.size()); |
| |
| // Make sure the returned tab is the correct one. |
| ASSERT_TRUE(query_result[0].is_dict()); |
| std::optional<int> tab_id = |
| query_result[0].GetDict().FindInt(extension_misc::kId); |
| ASSERT_TRUE(tab_id); |
| EXPECT_EQ(tab_id_a, *tab_id); |
| |
| // Update the auto-discardable state of web contents B. |
| int tab_id_b = ExtensionTabUtil::GetTabId(web_contents_b); |
| update_result = RunUpdateFunction( |
| extension.get(), |
| base::StringPrintf("[%u, {\"autoDiscardable\": false}]", tab_id_b)); |
| EXPECT_EQ(tab_id_b, api_test_utils::GetInteger(update_result, "id")); |
| EXPECT_FALSE(api_test_utils::GetBoolean(update_result, "autoDiscardable")); |
| |
| // Get auto-discardable tabs after changing the status of both created tabs. |
| query_result = RunQueryFunction(extension.get(), kAutoDiscardableQueryInfo); |
| EXPECT_EQ(1u, query_result.size()); |
| |
| // Make sure the returned tab is the correct one. |
| ASSERT_TRUE(query_result[0].is_dict()); |
| std::optional<int> id_value = |
| query_result[0].GetDict().FindInt(extension_misc::kId); |
| ASSERT_TRUE(id_value); |
| EXPECT_EQ(ExtensionTabUtil::GetTabId( |
| GetTabListInterface()->GetTab(0)->GetContents()), |
| *id_value); |
| |
| // Get auto-discardable tabs after changing the status of both created tabs. |
| query_result = |
| RunQueryFunction(extension.get(), kNonAutoDiscardableQueryInfo); |
| EXPECT_EQ(2u, query_result.size()); |
| |
| // Resets the first tab back to auto-discardable. |
| update_result = RunUpdateFunction( |
| extension.get(), |
| base::StringPrintf("[%u, {\"autoDiscardable\": true}]", tab_id_a)); |
| EXPECT_EQ(tab_id_a, api_test_utils::GetInteger(update_result, "id")); |
| EXPECT_TRUE(api_test_utils::GetBoolean(update_result, "autoDiscardable")); |
| |
| // Get auto-discardable tabs after resetting the status of web contents A. |
| query_result = RunQueryFunction(extension.get(), kAutoDiscardableQueryInfo); |
| EXPECT_EQ(2u, query_result.size()); |
| |
| // Get non auto-discardable tabs after resetting the status of web contents A. |
| query_result = |
| RunQueryFunction(extension.get(), kNonAutoDiscardableQueryInfo); |
| EXPECT_EQ(1u, query_result.size()); |
| } |
| |
| // Tester class for the tabs.zoom* api functions. |
| // TODO(https://crbug.com/505313377): Port these to desktop android. Currently, |
| // zoom controllers are not created for tabs, so the functions always return |
| // an error. |
| class ExtensionTabsZoomTest : public ExtensionTabsTest { |
| public: |
| void SetUpOnMainThread() override; |
| |
| // Runs chrome.tabs.setZoom(). |
| bool RunSetZoom(int tab_id, double zoom_factor); |
| |
| // Runs chrome.tabs.getZoom(). |
| testing::AssertionResult RunGetZoom(int tab_id, double* zoom_factor); |
| |
| // Runs chrome.tabs.setZoomSettings(). |
| bool RunSetZoomSettings(int tab_id, const char* mode, const char* scope); |
| |
| // Runs chrome.tabs.getZoomSettings(). |
| testing::AssertionResult RunGetZoomSettings(int tab_id, |
| std::string* mode, |
| std::string* scope); |
| |
| // Runs chrome.tabs.getZoomSettings() and returns default zoom. |
| testing::AssertionResult RunGetDefaultZoom(int tab_id, |
| double* default_zoom_factor); |
| |
| // Runs chrome.tabs.setZoom(), expecting an error. |
| std::string RunSetZoomExpectError(int tab_id, double zoom_factor); |
| |
| // Runs chrome.tabs.setZoomSettings(), expecting an error. |
| std::string RunSetZoomSettingsExpectError(int tab_id, |
| const char* mode, |
| const char* scope); |
| |
| private: |
| scoped_refptr<const Extension> extension_; |
| }; |
| |
| void ExtensionTabsZoomTest::SetUpOnMainThread() { |
| ExtensionTabsTest::SetUpOnMainThread(); |
| extension_ = ExtensionBuilder("Test").Build(); |
| } |
| |
| bool ExtensionTabsZoomTest::RunSetZoom(int tab_id, double zoom_factor) { |
| auto set_zoom_function = base::MakeRefCounted<TabsSetZoomFunction>(); |
| set_zoom_function->set_extension(extension_.get()); |
| set_zoom_function->set_has_callback(true); |
| |
| return utils::RunFunction( |
| set_zoom_function.get(), |
| base::StringPrintf("[%u, %lf]", tab_id, zoom_factor), profile(), |
| api_test_utils::FunctionMode::kNone); |
| } |
| |
| testing::AssertionResult ExtensionTabsZoomTest::RunGetZoom( |
| int tab_id, |
| double* zoom_factor) { |
| auto get_zoom_function = base::MakeRefCounted<TabsGetZoomFunction>(); |
| get_zoom_function->set_extension(extension_.get()); |
| get_zoom_function->set_has_callback(true); |
| |
| std::optional<base::Value> get_zoom_result = |
| utils::RunFunctionAndReturnSingleResult( |
| get_zoom_function.get(), base::StringPrintf("[%u]", tab_id), |
| profile()); |
| |
| if (!get_zoom_result) { |
| return testing::AssertionFailure() << "no result"; |
| } |
| |
| std::optional<double> maybe_value = get_zoom_result->GetIfDouble(); |
| if (!maybe_value.has_value()) { |
| return testing::AssertionFailure() << "result was not a double"; |
| } |
| |
| *zoom_factor = maybe_value.value(); |
| return testing::AssertionSuccess(); |
| } |
| |
| bool ExtensionTabsZoomTest::RunSetZoomSettings(int tab_id, |
| const char* mode, |
| const char* scope) { |
| auto set_zoom_settings_function = |
| base::MakeRefCounted<TabsSetZoomSettingsFunction>(); |
| set_zoom_settings_function->set_extension(extension_.get()); |
| |
| std::string args; |
| if (scope) { |
| args = base::StringPrintf("[%u, {\"mode\": \"%s\", \"scope\": \"%s\"}]", |
| tab_id, mode, scope); |
| } else { |
| args = base::StringPrintf("[%u, {\"mode\": \"%s\"}]", tab_id, mode); |
| } |
| |
| return utils::RunFunction(set_zoom_settings_function.get(), args, profile(), |
| api_test_utils::FunctionMode::kNone); |
| } |
| |
| testing::AssertionResult ExtensionTabsZoomTest::RunGetZoomSettings( |
| int tab_id, |
| std::string* mode, |
| std::string* scope) { |
| DCHECK(mode); |
| DCHECK(scope); |
| auto get_zoom_settings_function = |
| base::MakeRefCounted<TabsGetZoomSettingsFunction>(); |
| get_zoom_settings_function->set_extension(extension_.get()); |
| get_zoom_settings_function->set_has_callback(true); |
| |
| std::optional<base::Value> get_zoom_settings_result = |
| utils::RunFunctionAndReturnSingleResult( |
| get_zoom_settings_function.get(), base::StringPrintf("[%u]", tab_id), |
| profile()); |
| |
| if (!get_zoom_settings_result) { |
| return testing::AssertionFailure() << "no result"; |
| } |
| |
| base::DictValue get_zoom_settings_dict = |
| utils::ToDict(std::move(get_zoom_settings_result)); |
| *mode = api_test_utils::GetString(get_zoom_settings_dict, "mode"); |
| *scope = api_test_utils::GetString(get_zoom_settings_dict, "scope"); |
| |
| return testing::AssertionSuccess(); |
| } |
| |
| testing::AssertionResult ExtensionTabsZoomTest::RunGetDefaultZoom( |
| int tab_id, |
| double* default_zoom_factor) { |
| DCHECK(default_zoom_factor); |
| auto get_zoom_settings_function = |
| base::MakeRefCounted<TabsGetZoomSettingsFunction>(); |
| get_zoom_settings_function->set_extension(extension_.get()); |
| get_zoom_settings_function->set_has_callback(true); |
| |
| std::optional<base::Value> get_zoom_settings_result = |
| utils::RunFunctionAndReturnSingleResult( |
| get_zoom_settings_function.get(), base::StringPrintf("[%u]", tab_id), |
| profile()); |
| |
| if (!get_zoom_settings_result && get_zoom_settings_result->is_dict()) { |
| return testing::AssertionFailure() |
| << "no result or result is not a dictionary"; |
| } |
| |
| std::optional<double> default_zoom_factor_setting = |
| get_zoom_settings_result->GetDict().FindDouble("defaultZoomFactor"); |
| if (!default_zoom_factor_setting) { |
| return testing::AssertionFailure() |
| << "default zoom factor not found in result"; |
| } |
| *default_zoom_factor = *default_zoom_factor_setting; |
| |
| return testing::AssertionSuccess(); |
| } |
| |
| std::string ExtensionTabsZoomTest::RunSetZoomExpectError(int tab_id, |
| double zoom_factor) { |
| auto set_zoom_function = base::MakeRefCounted<TabsSetZoomFunction>(); |
| set_zoom_function->set_extension(extension_.get()); |
| set_zoom_function->set_has_callback(true); |
| |
| return utils::RunFunctionAndReturnError( |
| set_zoom_function.get(), |
| base::StringPrintf("[%u, %lf]", tab_id, zoom_factor), profile()); |
| } |
| |
| std::string ExtensionTabsZoomTest::RunSetZoomSettingsExpectError( |
| int tab_id, |
| const char* mode, |
| const char* scope) { |
| auto set_zoom_settings_function = |
| base::MakeRefCounted<TabsSetZoomSettingsFunction>(); |
| set_zoom_settings_function->set_extension(extension_.get()); |
| |
| return utils::RunFunctionAndReturnError( |
| set_zoom_settings_function.get(), |
| base::StringPrintf("[%u, {\"mode\": \"%s\", " |
| "\"scope\": \"%s\"}]", |
| tab_id, mode, scope), |
| profile()); |
| } |
| |
| |
| namespace { |
| |
| double GetZoomLevel(const content::WebContents* web_contents) { |
| return zoom::ZoomController::FromWebContents(web_contents)->GetZoomLevel(); |
| } |
| |
| content::OpenURLParams GetOpenParams(const char* url) { |
| return content::OpenURLParams(GURL(url), content::Referrer(), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui::PAGE_TRANSITION_LINK, false); |
| } |
| |
| } // namespace |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsZoomTest, SetAndGetZoom) { |
| content::OpenURLParams params(GetOpenParams(url::kAboutBlankURL)); |
| content::WebContents* web_contents = OpenUrlAndWaitForLoad(params.url); |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| |
| // Test default values before we set anything. |
| double zoom_factor = -1; |
| EXPECT_TRUE(RunGetZoom(tab_id, &zoom_factor)); |
| EXPECT_EQ(1.0, zoom_factor); |
| |
| // Test chrome.tabs.setZoom(). |
| const double kZoomLevel = 0.8; |
| EXPECT_TRUE(RunSetZoom(tab_id, kZoomLevel)); |
| EXPECT_EQ(kZoomLevel, |
| blink::ZoomLevelToZoomFactor(GetZoomLevel(web_contents))); |
| |
| // Test chrome.tabs.getZoom(). |
| zoom_factor = -1; |
| EXPECT_TRUE(RunGetZoom(tab_id, &zoom_factor)); |
| EXPECT_EQ(kZoomLevel, zoom_factor); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsZoomTest, GetDefaultZoom) { |
| content::OpenURLParams params(GetOpenParams(url::kAboutBlankURL)); |
| content::WebContents* web_contents = OpenUrlAndWaitForLoad(params.url); |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| |
| zoom::ZoomController* zoom_controller = |
| zoom::ZoomController::FromWebContents(web_contents); |
| double default_zoom_factor = -1.0; |
| EXPECT_TRUE(RunGetDefaultZoom(tab_id, &default_zoom_factor)); |
| EXPECT_TRUE(blink::ZoomValuesEqual( |
| zoom_controller->GetDefaultZoomLevel(), |
| blink::ZoomFactorToZoomLevel(default_zoom_factor))); |
| |
| // Change the default zoom level and verify GetDefaultZoom returns the |
| // correct value. |
| content::StoragePartition* partition = |
| web_contents->GetBrowserContext()->GetStoragePartition( |
| web_contents->GetSiteInstance()); |
| ChromeZoomLevelPrefs* zoom_prefs = |
| static_cast<ChromeZoomLevelPrefs*>(partition->GetZoomLevelDelegate()); |
| |
| double default_zoom_level = zoom_controller->GetDefaultZoomLevel(); |
| zoom_prefs->SetDefaultZoomLevelPref(default_zoom_level + 0.5); |
| default_zoom_factor = -1.0; |
| EXPECT_TRUE(RunGetDefaultZoom(tab_id, &default_zoom_factor)); |
| EXPECT_TRUE(blink::ZoomValuesEqual( |
| default_zoom_level + 0.5, |
| blink::ZoomFactorToZoomLevel(default_zoom_factor))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsZoomTest, SetToDefaultZoom) { |
| content::OpenURLParams params(GetOpenParams(url::kAboutBlankURL)); |
| content::WebContents* web_contents = OpenUrlAndWaitForLoad(params.url); |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| |
| zoom::ZoomController* zoom_controller = |
| zoom::ZoomController::FromWebContents(web_contents); |
| double default_zoom_level = zoom_controller->GetDefaultZoomLevel(); |
| double new_default_zoom_level = default_zoom_level + 0.42; |
| |
| content::StoragePartition* partition = |
| web_contents->GetBrowserContext()->GetStoragePartition( |
| web_contents->GetSiteInstance()); |
| ChromeZoomLevelPrefs* zoom_prefs = |
| static_cast<ChromeZoomLevelPrefs*>(partition->GetZoomLevelDelegate()); |
| |
| zoom_prefs->SetDefaultZoomLevelPref(new_default_zoom_level); |
| |
| double observed_zoom_factor = -1.0; |
| EXPECT_TRUE(RunSetZoom(tab_id, 0.0)); |
| EXPECT_TRUE(RunGetZoom(tab_id, &observed_zoom_factor)); |
| EXPECT_TRUE(blink::ZoomValuesEqual( |
| new_default_zoom_level, |
| blink::ZoomFactorToZoomLevel(observed_zoom_factor))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsZoomTest, ZoomSettings) { |
| // In this test we need two URLs that (1) represent real pages (i.e. they |
| // load without causing an error page load), (2) have different domains, and |
| // (3) are zoomable by the extension API (this last condition rules out |
| // chrome:// urls). We achieve this by noting that about:blank meets these |
| // requirements, allowing us to spin up an embedded http server on localhost |
| // to get the other domain. |
| net::EmbeddedTestServer http_server; |
| http_server.ServeFilesFromSourceDirectory("chrome/test/data"); |
| ASSERT_TRUE(http_server.Start()); |
| |
| GURL url_A = http_server.GetURL("/simple.html"); |
| GURL url_B("about:blank"); |
| |
| // Tabs A1 and A2 are navigated to the same origin, while B is navigated |
| // to a different one. |
| content::WebContents* web_contents_A1 = OpenUrlAndWaitForLoad(url_A); |
| content::WebContents* web_contents_A2 = OpenUrlAndWaitForLoad(url_A); |
| content::WebContents* web_contents_B = OpenUrlAndWaitForLoad(url_B); |
| |
| int tab_id_A1 = ExtensionTabUtil::GetTabId(web_contents_A1); |
| int tab_id_A2 = ExtensionTabUtil::GetTabId(web_contents_A2); |
| int tab_id_B = ExtensionTabUtil::GetTabId(web_contents_B); |
| |
| ASSERT_FLOAT_EQ(1.f, |
| blink::ZoomLevelToZoomFactor(GetZoomLevel(web_contents_A1))); |
| ASSERT_FLOAT_EQ(1.f, |
| blink::ZoomLevelToZoomFactor(GetZoomLevel(web_contents_A2))); |
| ASSERT_FLOAT_EQ(1.f, |
| blink::ZoomLevelToZoomFactor(GetZoomLevel(web_contents_B))); |
| |
| // Test per-origin automatic zoom settings. |
| EXPECT_TRUE(RunSetZoom(tab_id_B, 1.f)); |
| EXPECT_TRUE(RunSetZoom(tab_id_A2, 1.1f)); |
| EXPECT_FLOAT_EQ(1.1f, |
| blink::ZoomLevelToZoomFactor(GetZoomLevel(web_contents_A1))); |
| EXPECT_FLOAT_EQ(1.1f, |
| blink::ZoomLevelToZoomFactor(GetZoomLevel(web_contents_A2))); |
| EXPECT_FLOAT_EQ(1.f, |
| blink::ZoomLevelToZoomFactor(GetZoomLevel(web_contents_B))); |
| |
| // Test per-tab automatic zoom settings. |
| EXPECT_TRUE(RunSetZoomSettings(tab_id_A1, "automatic", "per-tab")); |
| EXPECT_TRUE(RunSetZoom(tab_id_A1, 1.2f)); |
| EXPECT_FLOAT_EQ(1.2f, |
| blink::ZoomLevelToZoomFactor(GetZoomLevel(web_contents_A1))); |
| EXPECT_FLOAT_EQ(1.1f, |
| blink::ZoomLevelToZoomFactor(GetZoomLevel(web_contents_A2))); |
| |
| // Test 'manual' mode. |
| EXPECT_TRUE(RunSetZoomSettings(tab_id_A1, "manual", nullptr)); |
| EXPECT_TRUE(RunSetZoom(tab_id_A1, 1.3f)); |
| EXPECT_FLOAT_EQ(1.3f, |
| blink::ZoomLevelToZoomFactor(GetZoomLevel(web_contents_A1))); |
| EXPECT_FLOAT_EQ(1.1f, |
| blink::ZoomLevelToZoomFactor(GetZoomLevel(web_contents_A2))); |
| |
| // Test 'disabled' mode, which will reset A1's zoom to 1.f. |
| EXPECT_TRUE(RunSetZoomSettings(tab_id_A1, "disabled", nullptr)); |
| std::string error = RunSetZoomExpectError(tab_id_A1, 1.4f); |
| EXPECT_TRUE(base::MatchPattern(error, keys::kCannotZoomDisabledTabError)); |
| EXPECT_FLOAT_EQ(1.f, |
| blink::ZoomLevelToZoomFactor(GetZoomLevel(web_contents_A1))); |
| // We should still be able to zoom A2 though. |
| EXPECT_TRUE(RunSetZoom(tab_id_A2, 1.4f)); |
| EXPECT_FLOAT_EQ(1.4f, |
| blink::ZoomLevelToZoomFactor(GetZoomLevel(web_contents_A2))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsZoomTest, PerTabResetsOnNavigation) { |
| net::EmbeddedTestServer http_server; |
| http_server.ServeFilesFromSourceDirectory("chrome/test/data"); |
| ASSERT_TRUE(http_server.Start()); |
| |
| GURL url_A = http_server.GetURL("/simple.html"); |
| GURL url_B("about:blank"); |
| |
| content::WebContents* web_contents = OpenUrlAndWaitForLoad(url_A); |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| EXPECT_TRUE(RunSetZoomSettings(tab_id, "automatic", "per-tab")); |
| |
| std::string mode; |
| std::string scope; |
| EXPECT_TRUE(RunGetZoomSettings(tab_id, &mode, &scope)); |
| EXPECT_EQ("automatic", mode); |
| EXPECT_EQ("per-tab", scope); |
| |
| // Navigation of tab should reset mode to per-origin. |
| ui_test_utils::NavigateToURLBlockUntilNavigationsComplete(browser(), url_B, |
| 1); |
| EXPECT_TRUE(RunGetZoomSettings(tab_id, &mode, &scope)); |
| EXPECT_EQ("automatic", mode); |
| EXPECT_EQ("per-origin", scope); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsZoomTest, GetZoomSettings) { |
| content::OpenURLParams params(GetOpenParams(url::kAboutBlankURL)); |
| content::WebContents* web_contents = OpenUrlAndWaitForLoad(params.url); |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| |
| std::string mode; |
| std::string scope; |
| |
| EXPECT_TRUE(RunGetZoomSettings(tab_id, &mode, &scope)); |
| EXPECT_EQ("automatic", mode); |
| EXPECT_EQ("per-origin", scope); |
| |
| EXPECT_TRUE(RunSetZoomSettings(tab_id, "automatic", "per-tab")); |
| EXPECT_TRUE(RunGetZoomSettings(tab_id, &mode, &scope)); |
| |
| EXPECT_EQ("automatic", mode); |
| EXPECT_EQ("per-tab", scope); |
| |
| std::string error = |
| RunSetZoomSettingsExpectError(tab_id, "manual", "per-origin"); |
| EXPECT_TRUE(base::MatchPattern(error, keys::kPerOriginOnlyInAutomaticError)); |
| error = RunSetZoomSettingsExpectError(tab_id, "disabled", "per-origin"); |
| EXPECT_TRUE(base::MatchPattern(error, keys::kPerOriginOnlyInAutomaticError)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsZoomTest, CannotZoomInvalidTab) { |
| content::OpenURLParams params(GetOpenParams(url::kAboutBlankURL)); |
| content::WebContents* web_contents = OpenUrlAndWaitForLoad(params.url); |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| |
| int bogus_id = tab_id + 100; |
| std::string error = RunSetZoomExpectError(bogus_id, 3.14159); |
| EXPECT_TRUE(base::MatchPattern(error, ExtensionTabUtil::kTabNotFoundError)); |
| |
| error = RunSetZoomSettingsExpectError(bogus_id, "manual", "per-tab"); |
| EXPECT_TRUE(base::MatchPattern(error, ExtensionTabUtil::kTabNotFoundError)); |
| |
| const char kNewTestTabArgs[] = "chrome://version"; |
| params = GetOpenParams(kNewTestTabArgs); |
| web_contents = browser()->OpenURL(params, /*navigation_handle_callback=*/{}); |
| tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| |
| // Test chrome.tabs.setZoom(). |
| error = RunSetZoomExpectError(tab_id, 3.14159); |
| EXPECT_TRUE( |
| base::MatchPattern(error, manifest_errors::kCannotAccessChromeUrl)); |
| |
| // chrome.tabs.setZoomSettings(). |
| error = RunSetZoomSettingsExpectError(tab_id, "manual", "per-tab"); |
| EXPECT_TRUE( |
| base::MatchPattern(error, manifest_errors::kCannotAccessChromeUrl)); |
| } |
| |
| #if BUILDFLAG(ENABLE_PDF) |
| class ExtensionApiPdfTest : public base::test::WithFeatureOverride, |
| public PDFExtensionTestBase { |
| public: |
| ExtensionApiPdfTest() |
| : base::test::WithFeatureOverride(chrome_pdf::features::kPdfOopif) {} |
| |
| bool UseOopif() const override { return GetParam(); } |
| }; |
| |
| // Regression test for crbug.com/40085816. |
| IN_PROC_BROWSER_TEST_P(ExtensionApiPdfTest, TemporaryAddressSpoof) { |
| content::WebContents* first_web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(first_web_contents); |
| chrome::NewTab(browser(), NewTabTypes::kNoUserAction); |
| content::WebContents* second_web_contents = GetActiveWebContents(); |
| ASSERT_NE(first_web_contents, second_web_contents); |
| GURL url = embedded_test_server()->GetURL( |
| "/extensions/api_test/tabs/pdf_extension_test.html"); |
| content::TestNavigationManager navigation_manager( |
| second_web_contents, GURL("http://www.facebook.com:83")); |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), url, WindowOpenDisposition::CURRENT_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| |
| // Ensure the PDF has loaded, and get the WebContents to click. |
| auto* web_contents_for_click = second_web_contents; |
| if (UseOopif()) { |
| ASSERT_TRUE(GetTestMimeHandlerStreamManager(second_web_contents) |
| ->WaitUntilPdfLoadedInFirstChild()); |
| } else { |
| ASSERT_TRUE( |
| pdf_extension_test_util::EnsurePDFHasLoaded(second_web_contents)); |
| |
| auto inner_web_contents = web_contents_for_click->GetInnerWebContents(); |
| ASSERT_EQ(1U, inner_web_contents.size()); |
| // With MimeHandlerViewInCrossProcessFrame input should directly route to |
| // the guest WebContents as there is no longer a BrowserPlugin involved. |
| web_contents_for_click = inner_web_contents[0]; |
| } |
| |
| // (400, 300) in `web_contents_for_click` translates to a different coordinate |
| // in the PDF Viewer. The exact coordinate depends on the PDF Viewer's UI |
| // layout. In the test PDF embedded in pdf_extension_test.html, the entire PDF |
| // content area is a giant link to http://www.facebook.com:83. As long as this |
| // click hits that link target, it triggers the navigation required for test. |
| content::SimulateMouseClickAt(web_contents_for_click, 0, |
| blink::WebMouseEvent::Button::kLeft, |
| gfx::Point(400, 300)); |
| |
| ASSERT_TRUE(navigation_manager.WaitForRequestStart()); |
| |
| browser()->tab_strip_model()->ActivateTabAt( |
| 0, TabStripUserGestureDetails( |
| TabStripUserGestureDetails::GestureType::kOther)); |
| EXPECT_EQ(first_web_contents, GetActiveWebContents()); |
| browser()->tab_strip_model()->ActivateTabAt( |
| 1, TabStripUserGestureDetails( |
| TabStripUserGestureDetails::GestureType::kOther)); |
| EXPECT_EQ(second_web_contents, GetActiveWebContents()); |
| |
| EXPECT_EQ(url, second_web_contents->GetVisibleURL()); |
| |
| // Wait for the TestNavigationManager-monitored navigation to complete to |
| // avoid a race during browser teardown (see crbug.com/41412718). |
| ASSERT_TRUE(navigation_manager.WaitForNavigationFinished()); |
| } |
| |
| // TODO(crbug.com/40268279): Stop testing both modes after OOPIF PDF viewer |
| // launches. |
| INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(ExtensionApiPdfTest); |
| #endif // BUILDFLAG(ENABLE_PDF) |
| |
| // Tests how chrome.windows.create behaves when setSelfAsOpener parameter is |
| // used. setSelfAsOpener was introduced as a fix for https://crbug.com/40516654 |
| // and https://crbug.com/40518908. This is a (slightly morphed) regression test |
| // for https://crbug.com/40462301. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, WindowsCreate_WithOpener) { |
| const extensions::Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("../simple_with_file")); |
| ASSERT_TRUE(extension); |
| |
| // Navigate a tab to an extension page. |
| GURL extension_url = extension->GetResourceURL("file.html"); |
| content::WebContents* old_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(old_contents, extension_url)); |
| |
| // Execute chrome.windows.create and store the new tab in |new_contents|. |
| content::WebContents* new_contents = nullptr; |
| { |
| content::TestNavigationObserver nav_observer(extension_url); |
| nav_observer.StartWatchingNewWebContents(); |
| std::string script = base::StringPrintf( |
| R"( window.name = 'old-contents'; |
| new Promise(resolve => { |
| chrome.windows.create( |
| {url: '%s', setSelfAsOpener: true}, (win) => { |
| resolve(win.tabs[0].id); |
| }); |
| }); )", |
| extension_url.spec().c_str()); |
| // We need to get the tabId from the callback because simply waiting for a |
| // new WebContents via WebContentsAddedObserver is flaky when InitialWebUI |
| // is enabled (which creates multiple WebContents). We also wait for the |
| // specific extension URL to load to avoid races with initial intermediate |
| // page loads. |
| int tab_id = content::EvalJs(old_contents, script).ExtractInt(); |
| EXPECT_TRUE( |
| ExtensionTabUtil::GetTabById(tab_id, profile(), true, &new_contents)); |
| nav_observer.Wait(); |
| ASSERT_TRUE(content::WaitForLoadStop(new_contents)); |
| } |
| |
| // Navigate the old and the new tab to a web URL. |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| GURL web_url1 = embedded_test_server()->GetURL("/title1.html"); |
| GURL web_url2 = embedded_test_server()->GetURL("/title2.html"); |
| { |
| content::TestNavigationObserver nav_observer(new_contents, 1); |
| ASSERT_TRUE(content::ExecJs( |
| new_contents, "window.location = '" + web_url1.spec() + "';")); |
| nav_observer.Wait(); |
| } |
| { |
| content::TestNavigationObserver nav_observer(old_contents, 1); |
| ASSERT_TRUE(content::ExecJs( |
| old_contents, "window.location = '" + web_url2.spec() + "';")); |
| nav_observer.Wait(); |
| } |
| EXPECT_EQ(web_url1, |
| new_contents->GetPrimaryMainFrame()->GetLastCommittedURL()); |
| EXPECT_EQ(web_url2, |
| old_contents->GetPrimaryMainFrame()->GetLastCommittedURL()); |
| |
| // Verify that the old and new tab are in the same process. |
| EXPECT_EQ(old_contents->GetPrimaryMainFrame()->GetProcess(), |
| new_contents->GetPrimaryMainFrame()->GetProcess()); |
| |
| // Verify the old and new contents are in the same BrowsingInstance. |
| EXPECT_TRUE(old_contents->GetPrimaryMainFrame() |
| ->GetSiteInstance() |
| ->IsRelatedSiteInstance( |
| new_contents->GetPrimaryMainFrame()->GetSiteInstance())); |
| |
| // Verify that the |new_contents| has |window.opener| set. |
| EXPECT_EQ(old_contents->GetPrimaryMainFrame()->GetLastCommittedURL().spec(), |
| EvalJs(new_contents, "window.opener.location.href")); |
| |
| // Verify that |new_contents| can find |old_contents| using window.open/name. |
| std::string location_of_other_window = |
| EvalJs(new_contents, |
| "var w = window.open('', 'old-contents');\n" |
| "w.location.href;") |
| .ExtractString(); |
| EXPECT_EQ(old_contents->GetPrimaryMainFrame()->GetLastCommittedURL().spec(), |
| location_of_other_window); |
| } |
| |
| // Tests how chrome.windows.create behaves when setSelfAsOpener parameter is not |
| // used. setSelfAsOpener was introduced as a fix for https://crbug.com/40516654 |
| // and https://crbug.com/40518908. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, WindowsCreate_NoOpener) { |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("../simple_with_file")); |
| ASSERT_TRUE(extension); |
| |
| // Navigate a tab to an extension page. |
| GURL extension_url = extension->GetResourceURL("file.html"); |
| content::WebContents* old_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(old_contents, extension_url)); |
| |
| // Execute chrome.windows.create and store the new tab in |new_contents|. |
| content::WebContents* new_contents = nullptr; |
| { |
| content::TestNavigationObserver nav_observer(extension_url); |
| nav_observer.StartWatchingNewWebContents(); |
| std::string script = base::StringPrintf( |
| R"( window.name = 'old-contents'; |
| new Promise(resolve => { |
| chrome.windows.create({url: '%s'}, (win) => { |
| resolve(win.tabs[0].id); |
| }); |
| }); )", |
| extension_url.spec().c_str()); |
| // We need to get the tabId from the callback to accept the case where |
| // multiple WebContents are created. |
| int tab_id = content::EvalJs(old_contents, script).ExtractInt(); |
| EXPECT_TRUE( |
| ExtensionTabUtil::GetTabById(tab_id, profile(), true, &new_contents)); |
| nav_observer.Wait(); |
| ASSERT_TRUE(content::WaitForLoadStop(new_contents)); |
| } |
| |
| // Verify the old and new contents are NOT in the same BrowsingInstance. |
| EXPECT_FALSE(old_contents->GetPrimaryMainFrame() |
| ->GetSiteInstance() |
| ->IsRelatedSiteInstance( |
| new_contents->GetPrimaryMainFrame()->GetSiteInstance())); |
| |
| // Verify that the |new_contents| doesn't have |window.opener| set. |
| EXPECT_EQ(false, EvalJs(new_contents, "!!window.opener")); |
| |
| // TODO(lukasza): http://crbug.com/40550544: Verify that |new_contents| can |
| // NOT find |old_contents| using window.open/name. This is currently broken, |
| // because browsing instance boundaries are pierced for all extension frames |
| // (we hope this can be limited to background pages / contents). |
| } |
| |
| // Tests the origin of tabs created through chrome.windows.create(). |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, WindowsCreate_OpenerAndOrigin) { |
| const extensions::Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("../simple_with_file")); |
| ASSERT_TRUE(extension); |
| |
| // Navigate a tab to an extension page. |
| GURL extension_url = extension->GetResourceURL("file.html"); |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, extension_url)); |
| |
| const std::string extension_origin_str = |
| url::Origin::Create(extension->url()).Serialize(); |
| constexpr char kDataURL[] = "data:text/html,<html>test</html>"; |
| std::string extension_url_str = extension_url.spec(); |
| struct TestCase { |
| // The url to use in chrome.windows.create(). |
| std::string url; |
| // If set, its value will be used to specify |setSelfAsOpener|. |
| std::optional<bool> set_self_as_opener; |
| // The origin we expect the new tab to be in, opaque origins will be "null". |
| std::string expected_origin_str; |
| }; |
| auto test_cases = std::to_array<TestCase>({ |
| // about:blank URLs. |
| // With opener relationship, about:blank urls will get the extension's |
| // origin, without opener relationship, they will get opaque/"null" |
| // origin. |
| {url::kAboutBlankURL, true, extension_origin_str}, |
| {url::kAboutBlankURL, false, "null"}, |
| {url::kAboutBlankURL, std::nullopt, "null"}, |
| |
| // data:... URLs. |
| // With opener relationship or not, "data:..." URLs always gets unique |
| // origin, so origin will always be "null" in these cases. |
| {kDataURL, true, "null"}, |
| {kDataURL, false, "null"}, |
| {kDataURL, std::nullopt, "null"}, |
| |
| // chrome-extension:// URLs. |
| // These always get extension origin. |
| {extension_url_str, true, extension_origin_str}, |
| {extension_url_str, false, extension_origin_str}, |
| {extension_url_str, std::nullopt, extension_origin_str}, |
| }); |
| |
| Profile* profile = this->profile(); |
| auto run_test_case = [&web_contents, profile](const TestCase& test_case) { |
| std::string maybe_specify_set_self_as_opener; |
| if (test_case.set_self_as_opener) { |
| maybe_specify_set_self_as_opener = |
| base::StringPrintf(", setSelfAsOpener: %s", |
| base::ToString(*test_case.set_self_as_opener)); |
| } |
| std::string script = base::StringPrintf( |
| R"( new Promise(resolve => { |
| chrome.windows.create({url: '%s'%s}, (win) => { |
| resolve(win.tabs[0].id); |
| }); |
| }); )", |
| test_case.url.c_str(), maybe_specify_set_self_as_opener.c_str()); |
| |
| content::WebContents* new_contents = nullptr; |
| { |
| content::TestNavigationObserver nav_observer(GURL(test_case.url)); |
| nav_observer.StartWatchingNewWebContents(); |
| int tab_id = content::EvalJs(web_contents, script).ExtractInt(); |
| EXPECT_TRUE( |
| ExtensionTabUtil::GetTabById(tab_id, profile, true, &new_contents)); |
| nav_observer.Wait(); |
| } |
| ASSERT_TRUE(new_contents); |
| ASSERT_TRUE(content::WaitForLoadStop(new_contents)); |
| |
| EXPECT_EQ(test_case.expected_origin_str, EvalJs(new_contents, "origin;")); |
| const bool is_opaque_origin = |
| new_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin().opaque(); |
| EXPECT_EQ(test_case.expected_origin_str == "null", is_opaque_origin); |
| }; |
| for (size_t i = 0; i < std::size(test_cases); ++i) { |
| const auto& test_case = test_cases[i]; |
| SCOPED_TRACE( |
| base::StringPrintf("#%" PRIuS " %s", i, test_case.url.c_str())); |
| run_test_case(test_case); |
| } |
| } |
| |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // Tests that calling chrome.tabs.update updates the URL as expected. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsUpdate) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("UpdateTest").Build(); |
| |
| const GURL example_url = |
| embedded_test_server()->GetURL("example.com", "/title1.html"); |
| const GURL chromium_url = |
| embedded_test_server()->GetURL("chromium.org", "/title1.html"); |
| |
| // Navigate the browser to example.com |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, example_url)); |
| EXPECT_EQ(example_url, web_contents->GetLastCommittedURL()); |
| |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| |
| // Use the TabsUpdateFunction to navigate to chromium.org |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| static constexpr char kFormatArgs[] = R"([%d, {"url": "%s"}])"; |
| const std::string args = |
| base::StringPrintf(kFormatArgs, tab_id, chromium_url.spec().c_str()); |
| ASSERT_TRUE(api_test_utils::RunFunction(function.get(), args, profile(), |
| api_test_utils::FunctionMode::kNone)); |
| |
| EXPECT_TRUE(content::WaitForLoadStop(web_contents)); |
| EXPECT_EQ(chromium_url, web_contents->GetLastCommittedURL()); |
| } |
| |
| // Tests that calling chrome.tabs.update does not update a saved tab. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsUpdate_SavedTabGroupTab) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("UpdateTest").Build(); |
| |
| const GURL example_url = |
| embedded_test_server()->GetURL("example.com", "/title1.html"); |
| const GURL chromium_url = |
| embedded_test_server()->GetURL("chromium.org", "/title1.html"); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| |
| // Create 2 tabs. |
| content::WebContents* tab1_contents = OpenUrlAndWaitForLoad(example_url); |
| content::WebContents* tab2_contents = |
| OpenUrlAndWaitForLoad(GURL(url::kAboutBlankURL)); |
| |
| EXPECT_EQ(tab_list->GetActiveTab()->GetContents(), tab2_contents); |
| int tab1_id = ExtensionTabUtil::GetTabId(tab1_contents); |
| int non_updated_tab2_id = ExtensionTabUtil::GetTabId(tab2_contents); |
| |
| tab_groups::TabGroupSyncService* saved_service = |
| tab_groups::TabGroupSyncServiceFactory::GetForProfile(profile()); |
| ASSERT_TRUE(saved_service); |
| // Wait for the TabGroupSyncService to properly initialize before making any |
| // changes to tab groups. This is not used on Android. |
| #if !BUILDFLAG(IS_ANDROID) |
| tab_groups::TabGroupSyncServiceInitializedObserver observer(saved_service); |
| observer.Wait(); |
| #endif |
| |
| // Group the tab and save it. |
| tab_groups::TabGroupId group = |
| tab_list->CreateTabGroup({tab_list->GetTab(1)->GetHandle()}).value(); |
| tab_groups::TabGroupVisualData visual_data( |
| u"Initial title", tab_groups::TabGroupColorId::kBlue); |
| tab_list->SetTabGroupVisualData(group, visual_data); |
| |
| std::optional<tab_groups::TabGroupId> tab1_group_id = |
| tab_list->GetTab(1)->GetGroup(); |
| ASSERT_TRUE(tab1_group_id.has_value()); |
| |
| // TabGroupSyncService takes in different types for its methods depending on |
| // platform. |
| #if BUILDFLAG(IS_ANDROID) |
| EXPECT_TRUE(saved_service->GetGroup(tab1_group_id->token()).has_value()); |
| auto group_id = group.token(); |
| #else |
| EXPECT_TRUE(saved_service->GetGroup(*tab1_group_id).has_value()); |
| tab_groups::TabGroupId group_id = group; |
| #endif |
| |
| { // Test the active state change for a saved tab. |
| tab_list->ActivateTab(tab_list->GetTab(2)->GetHandle()); |
| |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| static constexpr char kFormatArgs[] = R"([%d, {"active": true}])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab1_id); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| function.get(), args, profile(), api_test_utils::FunctionMode::kNone)); |
| EXPECT_EQ(tab_list->GetActiveTab()->GetContents(), tab1_contents); |
| } |
| |
| ASSERT_TRUE(saved_service->GetGroup(group_id)); |
| |
| { // Reset the active states, and then test highlighted for a saved tab. |
| tab_list->ActivateTab(tab_list->GetTab(2)->GetHandle()); |
| |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| static constexpr char kFormatArgs[] = R"([%d, {"highlighted": true}])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab1_id); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| function.get(), args, profile(), api_test_utils::FunctionMode::kNone)); |
| EXPECT_TRUE(tab_list->GetTab(1)->IsSelected()); |
| } |
| |
| ASSERT_TRUE(saved_service->GetGroup(group_id)); |
| |
| { // Reset the active states, and then test selected state for a saved tab. |
| tab_list->ActivateTab(tab_list->GetTab(2)->GetHandle()); |
| |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| static constexpr char kFormatArgs[] = R"([%d, {"selected": true}])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab1_id); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| function.get(), args, profile(), api_test_utils::FunctionMode::kNone)); |
| EXPECT_TRUE(tab_list->GetTab(1)->IsSelected()); |
| } |
| |
| ASSERT_TRUE(saved_service->GetGroup(group_id)); |
| |
| { // Test Muted state. |
| // Tab should not be muted by default. |
| EXPECT_FALSE(tab1_contents->IsAudioMuted()); |
| |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| static constexpr char kFormatArgs[] = R"([%d, {"muted": true}])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab1_id); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| function.get(), args, profile(), api_test_utils::FunctionMode::kNone)); |
| EXPECT_TRUE(tab1_contents->IsAudioMuted()); |
| } |
| |
| ASSERT_TRUE(saved_service->GetGroup(group_id)); |
| |
| { // Test setting the opener. |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| static constexpr char kFormatArgs[] = R"([%d, {"openerTabId": %d}])"; |
| const std::string args = |
| base::StringPrintf(kFormatArgs, tab1_id, non_updated_tab2_id); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| function.get(), args, profile(), api_test_utils::FunctionMode::kNone)); |
| |
| auto* opener_tab = |
| tab_list->GetOpenerForTab(tab_list->GetTab(1)->GetHandle()); |
| EXPECT_EQ(tab_list->GetTab(2), opener_tab); |
| } |
| |
| ASSERT_TRUE(saved_service->GetGroup(group_id)); |
| |
| // TODO(https://crbug.com/447211263): Re-enable this subtest once there is |
| // support on desktop android. |
| #if !BUILDFLAG(IS_ANDROID) |
| { // Test setting the discard state. |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| static constexpr char kFormatArgs[] = R"([%d, {"autoDiscardable": false}])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab1_id); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| function.get(), args, profile(), api_test_utils::FunctionMode::kNone)); |
| |
| auto* lifecycle_unit = |
| resource_coordinator::TabLifecycleUnitExternal::FromWebContents( |
| tab1_contents); |
| ASSERT_TRUE(lifecycle_unit); |
| EXPECT_FALSE(lifecycle_unit->IsAutoDiscardable()); |
| } |
| |
| ASSERT_TRUE(saved_service->GetGroup(group_id)); |
| #endif |
| |
| { // Test setting URL should pass. |
| EXPECT_EQ(example_url, tab1_contents->GetLastCommittedURL()); |
| |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| static constexpr char kFormatArgs[] = R"([%d, {"url": "%s"}])"; |
| const std::string args = |
| base::StringPrintf(kFormatArgs, tab1_id, chromium_url.spec().c_str()); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| function.get(), args, profile(), api_test_utils::FunctionMode::kNone)); |
| |
| content::WaitForLoadStop(tab1_contents); |
| EXPECT_EQ(chromium_url, tab1_contents->GetLastCommittedURL()); |
| } |
| |
| ASSERT_TRUE(saved_service->GetGroup(group_id)); |
| |
| // Test setting pinned state should pass. This must be done last since pinning |
| // destroys the group. |
| { |
| EXPECT_FALSE(tab_list->GetTab(1)->IsPinned()); |
| |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| static constexpr char kFormatArgs[] = R"([%d, {"pinned": true}])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab1_id); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| function.get(), args, profile(), api_test_utils::FunctionMode::kNone)); |
| |
| // Pinned tab moves to the front of the tab strip, so it becomes tab 0. |
| EXPECT_EQ(tab1_contents, tab_list->GetTab(0)->GetContents()); |
| EXPECT_TRUE(tab_list->GetTab(0)->IsPinned()); |
| } |
| |
| ASSERT_FALSE(saved_service->GetGroup(group_id)); |
| } |
| |
| // Tests that calling chrome.tabs.update with a JavaScript URL results |
| // in an error. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsUpdate_JavaScriptUrlNotAllowed) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // An extension with access to www.example.com. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("Extension with a host permission") |
| .AddHostPermission("http://www.example.com/*") |
| .Build(); |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| |
| const GURL example_url = |
| embedded_test_server()->GetURL("example.com", "/title1.html"); |
| |
| // Navigate the browser to example.com |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, example_url)); |
| |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| |
| static constexpr char kFormatArgs[] = R"([%d, {"url": "%s"}])"; |
| const std::string args = base::StringPrintf( |
| kFormatArgs, tab_id, "javascript:void(document.title = 'Won't work')"); |
| std::string error = api_test_utils::RunFunctionAndReturnError( |
| function.get(), args, profile(), api_test_utils::FunctionMode::kNone); |
| EXPECT_EQ(ExtensionTabUtil::kJavaScriptUrlsNotAllowedInExtensionNavigations, |
| error); |
| } |
| |
| // Tests updating a URL of a web tab to an about:blank. Verify that the new |
| // frame is placed in the correct process, has the correct origin and that no |
| // DCHECKs are hit anywhere. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsUpdate_WebToAboutBlank) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| const extensions::Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("../simple_with_file")); |
| ASSERT_TRUE(extension); |
| GURL extension_url = extension->GetResourceURL("file.html"); |
| url::Origin extension_origin = url::Origin::Create(extension_url); |
| GURL web_url = embedded_test_server()->GetURL("/title1.html"); |
| url::Origin web_origin = url::Origin::Create(web_url); |
| GURL about_blank_url = GURL(url::kAboutBlankURL); |
| |
| // Navigate a tab to an extension page. |
| content::WebContents* extension_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(extension_contents, extension_url)); |
| EXPECT_EQ( |
| extension_origin, |
| extension_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin()); |
| |
| // Create another tab and navigate it to a web page. |
| content::WebContents* test_contents = nullptr; |
| { |
| content::WebContentsAddedObserver test_contents_observer; |
| GetTabListInterface()->OpenTab(web_url, -1); |
| test_contents = test_contents_observer.GetWebContents(); |
| content::WaitForLoadStop(test_contents); |
| } |
| EXPECT_EQ(web_origin, |
| test_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin()); |
| EXPECT_NE(extension_contents->GetPrimaryMainFrame()->GetProcess(), |
| test_contents->GetPrimaryMainFrame()->GetProcess()); |
| |
| // Use |chrome.tabs.update| API to navigate |test_contents| to an about:blank |
| // URL. |
| { |
| content::TestNavigationObserver nav_observer(test_contents, 1); |
| int test_tab_id = ExtensionTabUtil::GetTabId(test_contents); |
| content::ExecuteScriptAsync( |
| extension_contents, |
| content::JsReplace("chrome.tabs.update($1, { url: $2 })", test_tab_id, |
| about_blank_url)); |
| nav_observer.WaitForNavigationFinished(); |
| EXPECT_TRUE(nav_observer.last_navigation_succeeded()); |
| EXPECT_EQ(about_blank_url, nav_observer.last_navigation_url()); |
| } |
| |
| // Verify the origin and process of the about:blank tab. |
| content::RenderFrameHost* test_frame = test_contents->GetPrimaryMainFrame(); |
| EXPECT_EQ(about_blank_url, test_frame->GetLastCommittedURL()); |
| EXPECT_EQ(extension_contents->GetPrimaryMainFrame()->GetProcess(), |
| test_contents->GetPrimaryMainFrame()->GetProcess()); |
| // Note that committing with the extension origin wouldn't be possible when |
| // targeting an incognito window (see also IncognitoApiTest.Incognito test). |
| EXPECT_EQ(extension_origin, test_frame->GetLastCommittedOrigin()); |
| } |
| |
| // Tests updating a URL of a web tab to an about:newtab. Verify that the new |
| // frame is placed in the correct process, has the correct origin and that no |
| // DCHECKs are hit anywhere. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsUpdate_WebToAboutNewTab) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| const extensions::Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("../simple_with_file")); |
| ASSERT_TRUE(extension); |
| GURL extension_url = extension->GetResourceURL("file.html"); |
| url::Origin extension_origin = url::Origin::Create(extension_url); |
| GURL web_url = embedded_test_server()->GetURL("/title1.html"); |
| url::Origin web_origin = url::Origin::Create(web_url); |
| |
| // https://crbug.com/40155847: about:version is rewritten to chrome://version |
| // when entered in the omnibox or used in a bookmark. Such rewriting is |
| // definitely undesirable for http-initiated navigations (see r818969), but |
| // it is less clear what should happen in extension-initiated navigations. |
| GURL about_newtab_url = GURL("about:newtab"); |
| #if BUILDFLAG(IS_ANDROID) |
| GURL chrome_newtab_url = GURL("chrome-native://newtab/"); |
| #else |
| GURL chrome_newtab_url = GURL("chrome://new-tab-page/"); |
| #endif |
| // Navigate a tab to an extension page. |
| content::WebContents* extension_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(extension_contents, extension_url)); |
| EXPECT_EQ( |
| extension_origin, |
| extension_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin()); |
| |
| // Create another tab and navigate it to a web page. |
| content::WebContents* test_contents = nullptr; |
| { |
| content::WebContentsAddedObserver test_contents_observer; |
| GetTabListInterface()->OpenTab(web_url, -1); |
| test_contents = test_contents_observer.GetWebContents(); |
| content::WaitForLoadStop(test_contents); |
| } |
| |
| // Use |chrome.tabs.update| API to navigate |test_contents| to an about:newtab |
| // URL. |
| { |
| content::TestNavigationObserver nav_observer(test_contents, 1); |
| int test_tab_id = ExtensionTabUtil::GetTabId(test_contents); |
| content::ExecuteScriptAsync( |
| extension_contents, |
| content::JsReplace("chrome.tabs.update($1, { url: $2 })", test_tab_id, |
| about_newtab_url)); |
| nav_observer.WaitForNavigationFinished(); |
| EXPECT_TRUE(nav_observer.last_navigation_succeeded()); |
| EXPECT_EQ(chrome_newtab_url, nav_observer.last_navigation_url()); |
| } |
| |
| // Verify the origin and process of the about:newtab tab. |
| content::RenderFrameHost* test_frame = test_contents->GetPrimaryMainFrame(); |
| EXPECT_EQ(chrome_newtab_url, test_frame->GetLastCommittedURL()); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // "chrome-native://newtab/" has an opaque origin. |
| EXPECT_TRUE(test_frame->GetLastCommittedOrigin().opaque()); |
| #else |
| EXPECT_EQ(url::Origin::Create(chrome_newtab_url), |
| test_frame->GetLastCommittedOrigin()); |
| #endif |
| EXPECT_NE(extension_contents->GetPrimaryMainFrame()->GetProcess(), |
| test_contents->GetPrimaryMainFrame()->GetProcess()); |
| } |
| |
| // Tests updating a URL of a web tab to a non-web-accessible-resource of an |
| // extension - such navigation should be allowed. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsUpdate_WebToNonWAR) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| const extensions::Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("../simple_with_file")); |
| ASSERT_TRUE(extension); |
| GURL extension_url = extension->GetResourceURL("file.html"); |
| url::Origin extension_origin = url::Origin::Create(extension_url); |
| GURL web_url = embedded_test_server()->GetURL("/title1.html"); |
| url::Origin web_origin = url::Origin::Create(web_url); |
| GURL non_war_url = extension_url; |
| |
| // Navigate a tab to an extension page. |
| content::WebContents* extension_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(extension_contents, extension_url)); |
| EXPECT_EQ( |
| extension_origin, |
| extension_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin()); |
| |
| // Create another tab and navigate it to a web page. |
| content::WebContents* test_contents = nullptr; |
| { |
| content::WebContentsAddedObserver test_contents_observer; |
| GetTabListInterface()->OpenTab(web_url, -1); |
| test_contents = test_contents_observer.GetWebContents(); |
| content::WaitForLoadStop(test_contents); |
| } |
| EXPECT_EQ(web_origin, |
| test_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin()); |
| EXPECT_NE(extension_contents->GetPrimaryMainFrame()->GetProcess(), |
| test_contents->GetPrimaryMainFrame()->GetProcess()); |
| |
| // Use |chrome.tabs.update| API to navigate |test_contents| to a |
| // non-web-accessible-resource of an extension. |
| { |
| content::TestNavigationObserver nav_observer(test_contents, 1); |
| int test_tab_id = ExtensionTabUtil::GetTabId(test_contents); |
| content::ExecuteScriptAsync( |
| extension_contents, |
| content::JsReplace("chrome.tabs.update($1, { url: $2 })", test_tab_id, |
| non_war_url)); |
| nav_observer.WaitForNavigationFinished(); |
| EXPECT_TRUE(nav_observer.last_navigation_succeeded()); |
| EXPECT_EQ(non_war_url, nav_observer.last_navigation_url()); |
| } |
| |
| // Verify the origin and process of the navigated tab. |
| content::RenderFrameHost* test_frame = test_contents->GetPrimaryMainFrame(); |
| EXPECT_EQ(non_war_url, test_frame->GetLastCommittedURL()); |
| EXPECT_EQ(extension_origin, test_frame->GetLastCommittedOrigin()); |
| EXPECT_EQ(extension_contents->GetPrimaryMainFrame()->GetProcess(), |
| test_contents->GetPrimaryMainFrame()->GetProcess()); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| ExtensionAPICannotCreateWindowForDevtools) { |
| DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync( |
| GetTabListInterface()->GetTab(0)->GetContents(), false /* is_docked */); |
| auto function = base::MakeRefCounted<WindowsCreateFunction>(); |
| scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build()); |
| function->set_extension(extension.get()); |
| |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf( |
| R"([{"tabId": %d}])", |
| ExtensionTabUtil::GetTabId( |
| DevToolsWindowTesting::Get(devtools)->main_web_contents())), |
| DevToolsWindowTesting::Get(devtools)->browser()->GetProfile()), |
| tabs_constants::kNotAllowedForDevToolsError)); |
| |
| DevToolsWindowTesting::CloseDevToolsWindowSync(devtools); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, ExtensionAPICannotMoveDevtoolsTab) { |
| DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync( |
| GetTabListInterface()->GetTab(0)->GetContents(), false /* is_docked */); |
| auto function = base::MakeRefCounted<TabsMoveFunction>(); |
| |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf( |
| R"([%d, {"index": -1}])", |
| ExtensionTabUtil::GetTabId( |
| DevToolsWindowTesting::Get(devtools)->main_web_contents())), |
| DevToolsWindowTesting::Get(devtools)->browser()->GetProfile()), |
| tabs_constants::kNotAllowedForDevToolsError)); |
| |
| DevToolsWindowTesting::CloseDevToolsWindowSync(devtools); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, ExtensionAPICannotGroupDevtoolsTab) { |
| ASSERT_TRUE(browser()->tab_strip_model()->SupportsTabGroups()); |
| |
| DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync( |
| GetTabListInterface()->GetTab(0)->GetContents(), false /* is_docked */); |
| auto function = base::MakeRefCounted<TabsGroupFunction>(); |
| |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf( |
| R"([{"tabIds": %d}])", |
| ExtensionTabUtil::GetTabId( |
| DevToolsWindowTesting::Get(devtools)->main_web_contents())), |
| DevToolsWindowTesting::Get(devtools)->browser()->GetProfile()), |
| tabs_constants::kNotAllowedForDevToolsError)); |
| |
| DevToolsWindowTesting::CloseDevToolsWindowSync(devtools); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| ExtensionAPICannotDiscardDevtoolsTab) { |
| DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync( |
| GetTabListInterface()->GetTab(0)->GetContents(), false /* is_docked */); |
| auto function = base::MakeRefCounted<TabsDiscardFunction>(); |
| |
| EXPECT_TRUE(base::MatchPattern( |
| utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf( |
| "[%d]", |
| ExtensionTabUtil::GetTabId( |
| DevToolsWindowTesting::Get(devtools)->main_web_contents())), |
| DevToolsWindowTesting::Get(devtools)->browser()->GetProfile()), |
| tabs_constants::kNotAllowedForDevToolsError)); |
| |
| DevToolsWindowTesting::CloseDevToolsWindowSync(devtools); |
| } |
| |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // Bug fix for crbug.com/40055468. Ensure that an extension can't update the tab |
| // strip while a tab drag is in progress. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, IsTabStripEditable) { |
| // Add a couple of web contents to the browser and get their tab IDs. |
| NavigateToURLInNewTab(GURL(url::kAboutBlankURL)); |
| |
| constexpr int kNumTabs = 2; |
| ASSERT_EQ(kNumTabs, GetTabListInterface()->GetTabCount()); |
| |
| std::vector<int> tab_ids; |
| std::vector<content::WebContents*> web_contentses; |
| for (int i = 0; i < kNumTabs; ++i) { |
| content::WebContents* contents = |
| GetTabListInterface()->GetTab(i)->GetContents(); |
| tab_ids.push_back(ExtensionTabUtil::GetTabId(contents)); |
| web_contentses.push_back(contents); |
| } |
| |
| ASSERT_TRUE(ExtensionTabUtil::IsTabStripEditable(*profile())); |
| scoped_refptr<const Extension> extension( |
| ExtensionBuilder("Test").AddAPIPermission("tabs").Build()); |
| |
| // Succeed while tab drag not in progress. |
| { |
| std::string args = base::StringPrintf("[{\"tabs\": [%d]}]", 0); |
| scoped_refptr<TabsHighlightFunction> function = |
| base::MakeRefCounted<TabsHighlightFunction>(); |
| function->set_extension(extension.get()); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| } |
| |
| // Disable tab list editing. This simulates a tab drag without actually |
| // performing a drag. |
| base::AutoReset<bool> disable_tab_list_editing = |
| ExtensionTabUtil::DisableTabListEditingForTesting(); |
| |
| ASSERT_FALSE(ExtensionTabUtil::IsTabStripEditable(*profile())); |
| |
| // Succeed with updates that don't interact with the tab strip model. |
| { |
| const char* url = "https://example.com/"; |
| std::string args = |
| base::StringPrintf("[%d, {\"url\": \"%s\"}]", tab_ids[0], url); |
| scoped_refptr<TabsUpdateFunction> function = |
| base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| std::optional<base::Value> value = utils::RunFunctionAndReturnSingleResult( |
| function.get(), args, profile(), utils::FunctionMode::kNone); |
| ASSERT_TRUE(value && value->is_dict()); |
| EXPECT_EQ(*value->GetDict().FindString(tabs_constants::kPendingUrlKey), |
| url); |
| } |
| |
| // Succeed while edit in progress and calling chrome.tabs.query. |
| { |
| const char* args = "[{}]"; |
| scoped_refptr<TabsQueryFunction> function = |
| base::MakeRefCounted<TabsQueryFunction>(); |
| function->set_extension(extension.get()); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| } |
| |
| // Succeed while edit in progress and calling chrome.tabs.get. |
| { |
| std::string args = base::StringPrintf("[%d]", tab_ids[0]); |
| scoped_refptr<TabsGetFunction> function = |
| base::MakeRefCounted<TabsGetFunction>(); |
| function->set_extension(extension.get()); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| } |
| |
| // Bug fix for crbug.com/40055542. Error updating tabs while drag in progress. |
| { |
| std::string args = |
| base::StringPrintf("[%d, {\"highlighted\": true}]", tab_ids[1]); |
| auto function = base::MakeRefCounted<TabsUpdateFunction>(); |
| function->set_extension(extension.get()); |
| std::string error = |
| utils::RunFunctionAndReturnError(function.get(), args, profile()); |
| EXPECT_EQ(ExtensionTabUtil::kTabStripNotEditableError, error); |
| } |
| |
| // Error highlighting tab while drag in progress. |
| { |
| std::string args = base::StringPrintf("[{\"tabs\": [%d]}]", tab_ids[0]); |
| auto function = base::MakeRefCounted<TabsHighlightFunction>(); |
| function->set_extension(extension.get()); |
| std::string error = utils::RunFunctionAndReturnError( |
| function.get(), args, profile(), utils::FunctionMode::kNone); |
| EXPECT_EQ(ExtensionTabUtil::kTabStripNotEditableError, error); |
| } |
| |
| // Bug fix for crbug.com/40055487. Tab group modification during drag. |
| { |
| std::string args = base::StringPrintf("[{\"tabIds\": [%d]}]", tab_ids[0]); |
| scoped_refptr<TabsGroupFunction> function = |
| base::MakeRefCounted<TabsGroupFunction>(); |
| function->set_extension(extension.get()); |
| std::string error = |
| utils::RunFunctionAndReturnError(function.get(), args, profile()); |
| EXPECT_EQ(ExtensionTabUtil::kTabStripNotEditableError, error); |
| } |
| |
| // TODO(crbug.com/493957479): Consider adding tests for drag cancellation. |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, QueryWithoutTabsPermission) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| auto tab_urls = std::to_array<GURL>({ |
| embedded_test_server()->GetURL("www.google.com", "/empty.html"), |
| embedded_test_server()->GetURL("www.example.com", "/empty.html"), |
| embedded_test_server()->GetURL("www.google.com", "/empty.html"), |
| }); |
| auto tab_titles = |
| std::to_array<std::string>({"", "Sample title", "Sample title"}); |
| |
| // Add 3 web contentses to the browser. |
| std::array<content::WebContents*, std::size(tab_urls)> web_contentses; |
| for (size_t i = 0; i < std::size(tab_urls); ++i) { |
| tabs::TabInterface* tab = GetTabListInterface()->OpenTab(tab_urls[i], -1); |
| content::WebContents* raw_web_contents = tab->GetContents(); |
| web_contentses[i] = raw_web_contents; |
| content::WaitForLoadStop(raw_web_contents); |
| raw_web_contents->GetController().GetVisibleEntry()->SetTitle( |
| base::ASCIIToUTF16(tab_titles[i])); |
| } |
| |
| const char* kTitleAndURLQueryInfo = |
| "[{\"title\": \"Sample title\", \"url\": \"*://www.google.com/*\"}]"; |
| |
| // An extension without "tabs" permission will see none of the 3 tabs. |
| scoped_refptr<const Extension> extension = ExtensionBuilder("Test").Build(); |
| base::ListValue tabs_list_without_permission = |
| RunQueryFunction(extension.get(), kTitleAndURLQueryInfo); |
| EXPECT_EQ(0u, tabs_list_without_permission.size()); |
| |
| // An extension with "tabs" permission however will see the third tab. |
| scoped_refptr<const Extension> extension_with_permission = |
| ExtensionBuilder() |
| .SetManifest( |
| base::DictValue() |
| .Set("name", "Extension with tabs permission") |
| .Set("version", "1.0") |
| .Set("manifest_version", 3) |
| .Set("permissions", base::ListValue().Append("tabs"))) |
| .Build(); |
| base::ListValue tabs_list_with_permission = |
| RunQueryFunction(extension_with_permission.get(), kTitleAndURLQueryInfo); |
| ASSERT_EQ(1u, tabs_list_with_permission.size()); |
| |
| const base::Value& third_tab_info = tabs_list_with_permission[0]; |
| ASSERT_TRUE(third_tab_info.is_dict()); |
| std::optional<int> third_tab_id = third_tab_info.GetDict().FindInt("id"); |
| EXPECT_EQ(ExtensionTabUtil::GetTabId(web_contentses[2]), third_tab_id); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, QueryWithHostPermission) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| auto tab_urls = std::to_array<GURL>({ |
| embedded_test_server()->GetURL("www.google.com", "/empty.html"), |
| embedded_test_server()->GetURL("www.example.com", "/empty.html"), |
| embedded_test_server()->GetURL("www.google.com", "/test.html"), |
| }); |
| auto tab_titles = |
| std::to_array<std::string>({"", "Sample title", "Sample title"}); |
| |
| // Add 3 web contentses to the browser. |
| std::array<content::WebContents*, std::size(tab_urls)> web_contentses; |
| for (size_t i = 0; i < std::size(tab_urls); ++i) { |
| tabs::TabInterface* tab = GetTabListInterface()->OpenTab(tab_urls[i], -1); |
| content::WebContents* raw_web_contents = tab->GetContents(); |
| web_contentses[i] = raw_web_contents; |
| content::WaitForLoadStop(raw_web_contents); |
| raw_web_contents->GetController().GetVisibleEntry()->SetTitle( |
| base::ASCIIToUTF16(tab_titles[i])); |
| } |
| |
| const char* kTitleAndURLQueryInfo = |
| "[{\"title\": \"Sample title\", \"url\": \"*://www.google.com/*\"}]"; |
| |
| // An extension with "host" permission will only see the third tab. |
| scoped_refptr<const Extension> extension_with_permission = |
| ExtensionBuilder() |
| .SetManifest( |
| base::DictValue() |
| .Set("name", "Extension with tabs permission") |
| .Set("version", "1.0") |
| .Set("manifest_version", 3) |
| .Set("host_permissions", |
| base::ListValue().Append("*://www.google.com/*"))) |
| .Build(); |
| |
| { |
| base::ListValue tabs_list_with_permission = RunQueryFunction( |
| extension_with_permission.get(), kTitleAndURLQueryInfo); |
| ASSERT_EQ(1u, tabs_list_with_permission.size()); |
| |
| const base::Value& third_tab_info = tabs_list_with_permission[0]; |
| ASSERT_TRUE(third_tab_info.is_dict()); |
| std::optional<int> third_tab_id = third_tab_info.GetDict().FindInt("id"); |
| EXPECT_EQ(ExtensionTabUtil::GetTabId(web_contentses[2]), third_tab_id); |
| } |
| |
| // Try the same without title, first and third tabs will match. |
| const char* kURLQueryInfo = "[{\"url\": \"*://www.google.com/*\"}]"; |
| { |
| base::ListValue tabs_list_with_permission = |
| RunQueryFunction(extension_with_permission.get(), kURLQueryInfo); |
| ASSERT_EQ(2u, tabs_list_with_permission.size()); |
| |
| const base::Value& first_tab_info = tabs_list_with_permission[0]; |
| ASSERT_TRUE(first_tab_info.is_dict()); |
| const base::Value& third_tab_info = tabs_list_with_permission[1]; |
| ASSERT_TRUE(third_tab_info.is_dict()); |
| |
| std::vector<int> expected_tabs_ids; |
| expected_tabs_ids.push_back(ExtensionTabUtil::GetTabId(web_contentses[0])); |
| expected_tabs_ids.push_back(ExtensionTabUtil::GetTabId(web_contentses[2])); |
| |
| std::optional<int> first_tab_id = first_tab_info.GetDict().FindInt("id"); |
| ASSERT_TRUE(first_tab_id); |
| EXPECT_TRUE(std::ranges::contains(expected_tabs_ids, *first_tab_id)); |
| |
| std::optional<int> third_tab_id = third_tab_info.GetDict().FindInt("id"); |
| ASSERT_TRUE(third_tab_id); |
| EXPECT_TRUE(std::ranges::contains(expected_tabs_ids, *third_tab_id)); |
| } |
| } |
| |
| #if BUILDFLAG(ENABLE_PDF) |
| // Test that using the PDF extension for tab updates is treated as a |
| // renderer-initiated navigation. crbug.com/40085816 |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, PDFExtensionNavigation) { |
| auto manifest = base::DictValue() |
| .Set("name", "pdfext") |
| .Set("description", "desc") |
| .Set("version", "0.1") |
| .Set("manifest_version", 3) |
| .Set("permissions", base::ListValue().Append("tabs")); |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder() |
| .SetManifest(std::move(manifest)) |
| .SetID(extension_misc::kPdfExtensionId) |
| .Build(); |
| ASSERT_TRUE(extension); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| const GURL kGoogle = |
| embedded_test_server()->GetURL("www.google.com", "/empty.html"); |
| tabs::TabInterface* tab = GetTabListInterface()->OpenTab(kGoogle, -1); |
| content::WebContents* raw_web_contents = tab->GetContents(); |
| content::WaitForLoadStop(raw_web_contents); |
| |
| EXPECT_EQ(kGoogle, raw_web_contents->GetLastCommittedURL()); |
| EXPECT_EQ(kGoogle, raw_web_contents->GetVisibleURL()); |
| |
| int tab_id = ExtensionTabUtil::GetTabId(raw_web_contents); |
| std::string args = |
| base::StringPrintf(R"([%d, {"url":"http://example.com"}])", tab_id); |
| std::ignore = RunUpdateFunction(extension.get(), args); |
| |
| EXPECT_EQ(kGoogle, raw_web_contents->GetLastCommittedURL()); |
| EXPECT_EQ(kGoogle, raw_web_contents->GetVisibleURL()); |
| } |
| #endif // BUILDFLAG(ENABLE_PDF) |
| |
| // Test that the tabs.move() function correctly rearranges sets of tabs within a |
| // single window. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsMoveWithinWindow) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("MoveWithinWindowTest").Build(); |
| |
| // Continue adding tabs until there are `kNumTabs` tabs. |
| constexpr int kNumTabs = 5; |
| auto [tab_ids, web_contentses] = |
| CreateAndGetTabData(GetTabListInterface(), kNumTabs); |
| |
| ASSERT_EQ(kNumTabs, GetTabListInterface()->GetTabCount()); |
| |
| // Use the TabsMoveFunction to move tabs 0, 2, and 4 to index 1. |
| auto function = base::MakeRefCounted<TabsMoveFunction>(); |
| function->set_extension(extension.get()); |
| constexpr char kFormatArgs[] = R"([[%d, %d, %d], {"index": 1}])"; |
| const std::string args = |
| base::StringPrintf(kFormatArgs, tab_ids[0], tab_ids[2], tab_ids[4]); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| EXPECT_EQ(tab_list->GetTab(0)->GetContents(), web_contentses[1]); |
| EXPECT_EQ(tab_list->GetTab(1)->GetContents(), web_contentses[0]); |
| EXPECT_EQ(tab_list->GetTab(2)->GetContents(), web_contentses[2]); |
| EXPECT_EQ(tab_list->GetTab(3)->GetContents(), web_contentses[4]); |
| EXPECT_EQ(tab_list->GetTab(4)->GetContents(), web_contentses[3]); |
| } |
| |
| // Test that the tabs.move() function correctly rearranges sets of tabs across |
| // windows. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsMoveAcrossWindows) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("MoveAcrossWindowTest").Build(); |
| |
| TabListInterface* tab_list1 = GetTabListInterface(); |
| |
| // Continue adding tabs until there are `kNumTabs` tabs. |
| constexpr int kNumTabs = 5; |
| auto [tab_ids, web_contentses] = CreateAndGetTabData(tab_list1, kNumTabs); |
| |
| ASSERT_EQ(kNumTabs, tab_list1->GetTabCount()); |
| |
| // Create a new window with three tabs. |
| BrowserWindowInterface* window_2 = |
| CreateBrowserWindowWithType(BrowserWindowInterface::Type::TYPE_NORMAL); |
| auto window_2_id = ExtensionTabUtil::GetWindowId(window_2); |
| TabListInterface* tab_list2 = TabListInterface::From(window_2); |
| |
| constexpr int kNumTabs2 = 3; |
| auto [tab_ids2, web_contentses2] = CreateAndGetTabData(tab_list2, kNumTabs2); |
| ASSERT_EQ(kNumTabs2, tab_list2->GetTabCount()); |
| |
| constexpr int kNumTabsMovedAcrossWindows = 3; |
| auto function = base::MakeRefCounted<TabsMoveFunction>(); |
| function->set_extension(extension.get()); |
| constexpr char kFormatArgs[] = |
| R"([[%d, %d, %d, %d], {"windowId": %d, "index": 1}])"; |
| const std::string args = |
| base::StringPrintf(kFormatArgs, tab_ids2[2], tab_ids[0], tab_ids[2], |
| tab_ids[4], window_2_id); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| ASSERT_EQ(kNumTabs2 + kNumTabsMovedAcrossWindows, tab_list2->GetTabCount()); |
| EXPECT_EQ(tab_list2->GetTab(1)->GetContents(), web_contentses2[2]); |
| EXPECT_EQ(tab_list2->GetTab(2)->GetContents(), web_contentses[0]); |
| EXPECT_EQ(tab_list2->GetTab(3)->GetContents(), web_contentses[2]); |
| EXPECT_EQ(tab_list2->GetTab(4)->GetContents(), web_contentses[4]); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| TabsMoveAcrossWindowsShouldRespectGroupContiguity) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("MoveAcrossWindowWithInvalidIndexTest").Build(); |
| |
| TabListInterface* tab_list1 = GetTabListInterface(); |
| // original browser has 1 tab, add 4 to make 5. |
| auto [tab_ids1, web_contentses1] = CreateAndGetTabData(tab_list1, 5); |
| |
| constexpr int kNumTabs = 5; |
| ASSERT_EQ(kNumTabs, tab_list1->GetTabCount()); |
| |
| // Create a new window with three tabs. |
| BrowserWindowInterface* window_2 = |
| CreateBrowserWindowWithType(BrowserWindowInterface::Type::TYPE_NORMAL); |
| auto window_2_id = ExtensionTabUtil::GetWindowId(window_2); |
| TabListInterface* tab_list2 = TabListInterface::From(window_2); |
| |
| constexpr int kNumTabs2 = 3; |
| auto [tab_ids2, web_contentses2] = CreateAndGetTabData(tab_list2, kNumTabs2); |
| ASSERT_EQ(kNumTabs2, tab_list2->GetTabCount()); |
| |
| tab_list2->CreateTabGroup( |
| {tab_list2->GetTab(0)->GetHandle(), tab_list2->GetTab(1)->GetHandle()}); |
| |
| int tab_extension_id = tab_ids1[2]; |
| |
| // Attempt to move the tab at index 2 from `tab_list1` to the middle of a |
| // group in `tab_list2`. This should return an error. |
| auto function = base::MakeRefCounted<TabsMoveFunction>(); |
| function->set_extension(extension.get()); |
| constexpr char kFormatArgs[] = R"([[%d], {"windowId": %d, "index": 1}])"; |
| const std::string args = |
| base::StringPrintf(kFormatArgs, tab_extension_id, window_2_id); |
| |
| std::string error = utils::RunFunctionAndReturnError( |
| function.get(), args, profile(), utils::FunctionMode::kNone); |
| EXPECT_EQ(tabs_constants::kInvalidTabIndexBreaksGroupContiguity, error); |
| } |
| |
| // Tests that saved tabs in a group can be moved. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsMoveSavedTabGroupTabAllowed) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("MoveWithinWindowTest").Build(); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| // Continue adding tabs until there are `kNumTabs` tabs. |
| constexpr int kNumTabs = 5; |
| auto [tab_ids, web_contentses] = CreateAndGetTabData(tab_list, kNumTabs); |
| |
| ASSERT_EQ(kNumTabs, tab_list->GetTabCount()); |
| |
| tab_groups::TabGroupSyncService* saved_service = |
| tab_groups::TabGroupSyncServiceFactory::GetForProfile(profile()); |
| ASSERT_TRUE(saved_service); |
| // Wait for the TabGroupSyncService to properly initialize before making any |
| // changes to tab groups. This is not used on Android. |
| #if !BUILDFLAG(IS_ANDROID) |
| tab_groups::TabGroupSyncServiceInitializedObserver observer(saved_service); |
| observer.Wait(); |
| #endif |
| |
| // Group the tab and save it. |
| std::optional<tab_groups::TabGroupId> group = tab_list->CreateTabGroup( |
| {tab_list->GetTab(0)->GetHandle(), tab_list->GetTab(1)->GetHandle(), |
| tab_list->GetTab(2)->GetHandle()}); |
| ASSERT_TRUE(group.has_value()); |
| |
| tab_groups::TabGroupVisualData visual_data( |
| u"Initial title", tab_groups::TabGroupColorId::kBlue); |
| tab_list->SetTabGroupVisualData(*group, visual_data); |
| |
| // Verify that the first tab can be moved to index 1. |
| auto function = base::MakeRefCounted<TabsMoveFunction>(); |
| function->set_extension(extension.get()); |
| constexpr char kFormatArgs[] = R"([[%d], {"index": 1}])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab_ids[0]); |
| |
| EXPECT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| EXPECT_EQ(tab_list->GetTab(0)->GetContents(), web_contentses[1]); |
| EXPECT_EQ(tab_list->GetTab(1)->GetContents(), web_contentses[0]); |
| } |
| |
| // Test that the `tabs.group()` function correctly groups tabs. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsGroupWithinWindow) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("GroupWithinWindowTest").Build(); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| // Add several web contents to the browser. |
| constexpr int kNumTabs = 5; |
| auto [tab_ids, web_contentses] = CreateAndGetTabData(tab_list, kNumTabs); |
| ASSERT_EQ(kNumTabs, tab_list->GetTabCount()); |
| |
| // Use the `TabsGroupFunction` to group tabs 0, 2, and 4. |
| auto function = base::MakeRefCounted<TabsGroupFunction>(); |
| function->set_extension(extension.get()); |
| constexpr char kFormatArgs[] = R"([{"tabIds": [%d, %d, %d]}])"; |
| const std::string args = |
| base::StringPrintf(kFormatArgs, tab_ids[0], tab_ids[2], tab_ids[4]); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| EXPECT_EQ(tab_list->GetTab(0)->GetContents(), web_contentses[0]); |
| EXPECT_EQ(tab_list->GetTab(1)->GetContents(), web_contentses[2]); |
| EXPECT_EQ(tab_list->GetTab(2)->GetContents(), web_contentses[4]); |
| EXPECT_EQ(tab_list->GetTab(3)->GetContents(), web_contentses[1]); |
| EXPECT_EQ(tab_list->GetTab(4)->GetContents(), web_contentses[3]); |
| |
| std::optional<tab_groups::TabGroupId> group = tab_list->GetTab(0)->GetGroup(); |
| EXPECT_TRUE(group.has_value()); |
| EXPECT_EQ(group, tab_list->GetTab(1)->GetGroup()); |
| EXPECT_EQ(group, tab_list->GetTab(2)->GetGroup()); |
| EXPECT_FALSE(tab_list->GetTab(3)->GetGroup()); |
| EXPECT_FALSE(tab_list->GetTab(4)->GetGroup()); |
| } |
| |
| // Test that the `tabs.group()` function correctly groups tabs even when given |
| // out-of-order or duplicate tab IDs. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsGroupMixedTabIds) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("GroupMixedTabIdsTest").Build(); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| // Add several web contents to the browser. |
| constexpr int kNumTabs = 5; |
| auto [tab_ids, web_contentses] = CreateAndGetTabData(tab_list, kNumTabs); |
| ASSERT_EQ(kNumTabs, tab_list->GetTabCount()); |
| |
| // Use the `TabsGroupFunction` to group tab 1 twice, along with tabs 3 and 2. |
| auto function = base::MakeRefCounted<TabsGroupFunction>(); |
| function->set_extension(extension.get()); |
| constexpr char kFormatArgs[] = R"([{"tabIds": [%d, %d, %d, %d]}])"; |
| const std::string args = base::StringPrintf( |
| kFormatArgs, tab_ids[1], tab_ids[1], tab_ids[3], tab_ids[2]); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| EXPECT_EQ(tab_list->GetTab(0)->GetContents(), web_contentses[0]); |
| EXPECT_EQ(tab_list->GetTab(1)->GetContents(), web_contentses[1]); |
| EXPECT_EQ(tab_list->GetTab(2)->GetContents(), web_contentses[2]); |
| EXPECT_EQ(tab_list->GetTab(3)->GetContents(), web_contentses[3]); |
| EXPECT_EQ(tab_list->GetTab(4)->GetContents(), web_contentses[4]); |
| |
| std::optional<tab_groups::TabGroupId> group = tab_list->GetTab(1)->GetGroup(); |
| EXPECT_TRUE(group.has_value()); |
| EXPECT_FALSE(tab_list->GetTab(0)->GetGroup()); |
| EXPECT_EQ(group, tab_list->GetTab(1)->GetGroup()); |
| EXPECT_EQ(group, tab_list->GetTab(2)->GetGroup()); |
| EXPECT_EQ(group, tab_list->GetTab(3)->GetGroup()); |
| EXPECT_FALSE(tab_list->GetTab(4)->GetGroup()); |
| } |
| |
| // Test that the `tabs.group()` function throws an error if both |
| // `createProperties` and `groupId` are specified. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsGroupParamsError) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("GroupParamsErrorTest").Build(); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| // Add several web contents to the browser. |
| constexpr int kNumTabs = 5; |
| auto [tab_ids, web_contentses] = CreateAndGetTabData(tab_list, kNumTabs); |
| ASSERT_EQ(kNumTabs, tab_list->GetTabCount()); |
| |
| // Add a tab to a group to have an existing group ID. |
| std::optional<tab_groups::TabGroupId> group = |
| tab_list->CreateTabGroup({tab_list->GetTab(1)->GetHandle()}); |
| ASSERT_TRUE(group.has_value()); |
| int group_id = ExtensionTabUtil::GetGroupId(*group); |
| |
| // Attempt to specify both `createProperties` and `groupId`. |
| auto function = base::MakeRefCounted<TabsGroupFunction>(); |
| function->set_extension(extension.get()); |
| constexpr char kFormatArgs[] = |
| R"([{"tabIds": [%d, %d, %d], |
| "groupId": %d, "createProperties": {"windowId": -1}}])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab_ids[0], |
| tab_ids[2], tab_ids[4], group_id); |
| std::string error = utils::RunFunctionAndReturnError( |
| function.get(), args, profile(), utils::FunctionMode::kNone); |
| EXPECT_EQ(tabs_constants::kGroupParamsError, error); |
| } |
| |
| // Test that the `tabs.group()` function correctly rearranges sets of tabs |
| // across windows before grouping. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsGroupAcrossWindows) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("GroupAcrossWindowsTest").Build(); |
| |
| TabListInterface* tab_list1 = GetTabListInterface(); |
| // Add several web contents to the browser. |
| constexpr int kNumTabs = 5; |
| auto [tab_ids, web_contentses] = CreateAndGetTabData(tab_list1, kNumTabs); |
| ASSERT_EQ(kNumTabs, tab_list1->GetTabCount()); |
| |
| // Create a new window and add a few tabs, adding one to a group. |
| BrowserWindowInterface* bwi2 = |
| CreateBrowserWindowWithType(BrowserWindowInterface::Type::TYPE_NORMAL); |
| TabListInterface* tab_list2 = TabListInterface::From(bwi2); |
| |
| constexpr int kNumTabs2 = 3; |
| auto [tab_ids2, web_contentses2] = CreateAndGetTabData(tab_list2, kNumTabs2); |
| ASSERT_EQ(kNumTabs2, tab_list2->GetTabCount()); |
| |
| std::optional<tab_groups::TabGroupId> group2 = |
| tab_list2->CreateTabGroup({tab_list2->GetTab(1)->GetHandle()}); |
| ASSERT_TRUE(group2.has_value()); |
| int group_id2 = ExtensionTabUtil::GetGroupId(*group2); |
| |
| // Use the `TabsGroupFunction` to group tabs 0, 2, and 4 from the original |
| // browser into the same group as the one in `bwi2`. |
| constexpr int kNumTabsMovedAcrossWindows = 3; |
| auto function = base::MakeRefCounted<TabsGroupFunction>(); |
| function->set_extension(extension.get()); |
| constexpr char kFormatArgs[] = R"([{"tabIds": [%d, %d, %d], "groupId": %d}])"; |
| const std::string args = base::StringPrintf( |
| kFormatArgs, tab_ids[0], tab_ids[2], tab_ids[4], group_id2); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| ASSERT_EQ(kNumTabs2 + kNumTabsMovedAcrossWindows, tab_list2->GetTabCount()); |
| EXPECT_EQ(tab_list2->GetTab(2)->GetContents(), web_contentses[0]); |
| EXPECT_EQ(tab_list2->GetTab(3)->GetContents(), web_contentses[2]); |
| EXPECT_EQ(tab_list2->GetTab(4)->GetContents(), web_contentses[4]); |
| |
| EXPECT_EQ(*group2, tab_list2->GetTab(1)->GetGroup().value()); |
| EXPECT_EQ(*group2, tab_list2->GetTab(2)->GetGroup().value()); |
| EXPECT_EQ(*group2, tab_list2->GetTab(3)->GetGroup().value()); |
| EXPECT_EQ(*group2, tab_list2->GetTab(4)->GetGroup().value()); |
| } |
| |
| // Test that grouping tabs that are in a saved group should fail. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsGroupForSavedTabGroupTab) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("GroupWithinWindowTest").Build(); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| // Create 2 tabs. |
| constexpr int kNumTabs = 2; |
| auto [tab_ids, web_contentses] = CreateAndGetTabData(tab_list, kNumTabs); |
| ASSERT_EQ(kNumTabs, tab_list->GetTabCount()); |
| |
| tab_groups::TabGroupSyncService* saved_service = |
| tab_groups::TabGroupSyncServiceFactory::GetForProfile(profile()); |
| ASSERT_TRUE(saved_service); |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| tab_groups::TabGroupSyncServiceInitializedObserver observer(saved_service); |
| observer.Wait(); |
| #endif |
| |
| // Group the first tab. |
| std::optional<tab_groups::TabGroupId> old_group = |
| tab_list->CreateTabGroup({tab_list->GetTab(0)->GetHandle()}); |
| ASSERT_TRUE(old_group.has_value()); |
| |
| // Use the `TabsGroupFunction` to group the 2 tabs into a new group. |
| auto function = base::MakeRefCounted<TabsGroupFunction>(); |
| function->set_extension(extension.get()); |
| constexpr char kFormatArgs[] = R"([{"tabIds": [%d, %d]}])"; |
| const std::string args = |
| base::StringPrintf(kFormatArgs, tab_ids[0], tab_ids[1]); |
| EXPECT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| // Make sure the new group exists and is different than the old group. |
| EXPECT_TRUE(tab_list->GetTab(0)->GetGroup().has_value()); |
| EXPECT_NE(*old_group, tab_list->GetTab(0)->GetGroup().value()); |
| EXPECT_EQ(tab_list->GetTab(0)->GetGroup(), tab_list->GetTab(1)->GetGroup()); |
| } |
| |
| // Test that the `tabs.ungroup()` function correctly ungroups tabs from a single |
| // group and deletes it. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsUngroupSingleGroup) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("UngroupSingleGroupTest").Build(); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| // Add several web contents to the browser. |
| constexpr int kNumTabs = 5; |
| auto [tab_ids, web_contentses] = CreateAndGetTabData(tab_list, kNumTabs); |
| ASSERT_EQ(kNumTabs, tab_list->GetTabCount()); |
| |
| // Add tabs 1, 2, and 3 to a group. |
| std::optional<tab_groups::TabGroupId> group = tab_list->CreateTabGroup( |
| {tab_list->GetTab(1)->GetHandle(), tab_list->GetTab(2)->GetHandle(), |
| tab_list->GetTab(3)->GetHandle()}); |
| ASSERT_TRUE(group.has_value()); |
| |
| // Use the `TabsUngroupFunction` to ungroup tabs 1, 2, and 3. |
| auto function = base::MakeRefCounted<TabsUngroupFunction>(); |
| function->set_extension(extension.get()); |
| constexpr char kFormatArgs[] = R"([[%d, %d, %d]])"; |
| const std::string args = |
| base::StringPrintf(kFormatArgs, tab_ids[1], tab_ids[2], tab_ids[3]); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| // Expect the group to be deleted because all tabs were ungrouped from it. |
| EXPECT_FALSE(tab_list->GetTab(1)->GetGroup()); |
| EXPECT_FALSE(tab_list->GetTab(2)->GetGroup()); |
| EXPECT_FALSE(tab_list->GetTab(3)->GetGroup()); |
| EXPECT_FALSE(tab_list->ContainsTabGroup(*group)); |
| } |
| |
| // Saved groups should be ungroupable from extensions. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| TabsUngroupSingleGroupForSavedTabGroup) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("UngroupSingleGroupTest").Build(); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| // Ensure we have at least one tab. |
| if (tab_list->GetTabCount() == 0) { |
| tab_list->OpenTab(GURL(url::kAboutBlankURL), -1); |
| } |
| ASSERT_GE(tab_list->GetTabCount(), 1); |
| |
| int tab_id = ExtensionTabUtil::GetTabId(tab_list->GetTab(0)->GetContents()); |
| |
| tab_groups::TabGroupSyncService* saved_service = |
| tab_groups::TabGroupSyncServiceFactory::GetForProfile(profile()); |
| ASSERT_TRUE(saved_service); |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| tab_groups::TabGroupSyncServiceInitializedObserver observer(saved_service); |
| observer.Wait(); |
| #endif |
| |
| // Group the tab and save it. |
| std::optional<tab_groups::TabGroupId> group = |
| tab_list->CreateTabGroup({tab_list->GetTab(0)->GetHandle()}); |
| ASSERT_TRUE(group.has_value()); |
| tab_groups::TabGroupVisualData visual_data( |
| u"Initial title", tab_groups::TabGroupColorId::kBlue); |
| tab_list->SetTabGroupVisualData(*group, visual_data); |
| |
| auto function = base::MakeRefCounted<TabsUngroupFunction>(); |
| function->set_extension(extension.get()); |
| constexpr char kFormatArgs[] = R"([[%d]])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab_id); |
| EXPECT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| // The tab should no longer be in the group. |
| EXPECT_EQ(std::nullopt, tab_list->GetTab(0)->GetGroup()); |
| } |
| |
| // Test that the `tabs.ungroup()` function correctly ungroups tabs from several |
| // different groups and deletes any empty ones. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsUngroupFromMultipleGroups) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("UngroupFromMultipleGroupsTest").Build(); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| // Add several web contents to the browser. |
| constexpr int kNumTabs = 5; |
| auto [tab_ids, web_contentses] = CreateAndGetTabData(tab_list, kNumTabs); |
| ASSERT_EQ(kNumTabs, tab_list->GetTabCount()); |
| |
| // Add tabs 1, 2, and 3 to `group1`, and tab 4 to `group2`. |
| std::optional<tab_groups::TabGroupId> group1 = tab_list->CreateTabGroup( |
| {tab_list->GetTab(1)->GetHandle(), tab_list->GetTab(2)->GetHandle(), |
| tab_list->GetTab(3)->GetHandle()}); |
| ASSERT_TRUE(group1.has_value()); |
| std::optional<tab_groups::TabGroupId> group2 = |
| tab_list->CreateTabGroup({tab_list->GetTab(4)->GetHandle()}); |
| ASSERT_TRUE(group2.has_value()); |
| |
| // Use the `TabsUngroupFunction` to ungroup tabs 2, 3, and 4. |
| auto function = base::MakeRefCounted<TabsUngroupFunction>(); |
| function->set_extension(extension.get()); |
| constexpr char kFormatArgs[] = R"([[%d, %d, %d]])"; |
| const std::string args = |
| base::StringPrintf(kFormatArgs, tab_ids[2], tab_ids[3], tab_ids[4]); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| // Expect `group2` to be deleted because all tabs were ungrouped from it. |
| EXPECT_EQ(group1, tab_list->GetTab(1)->GetGroup()); |
| EXPECT_FALSE(tab_list->GetTab(2)->GetGroup()); |
| EXPECT_FALSE(tab_list->GetTab(3)->GetGroup()); |
| EXPECT_FALSE(tab_list->GetTab(4)->GetGroup()); |
| EXPECT_TRUE(tab_list->ContainsTabGroup(*group1)); |
| EXPECT_FALSE(tab_list->ContainsTabGroup(*group2)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsGoForwardNoSelectedTabError) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("Test").AddAPIPermission("tabs").Build(); |
| auto function = base::MakeRefCounted<TabsGoForwardFunction>(); |
| function->set_extension(extension); |
| |
| // Create a new profile without any browser windows to ensure no tab is |
| // selected. |
| Profile* new_profile = |
| profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true); |
| |
| // No active tab in this profile results in an error. |
| // Note: On some platforms/configurations, this might return "No current |
| // window" if no browser exists for the profile. |
| std::string error = utils::RunFunctionAndReturnError( |
| function.get(), "[]", new_profile, utils::FunctionMode::kNone); |
| EXPECT_TRUE(error == keys::kNoSelectedTabError || |
| error == ExtensionTabUtil::kNoCurrentWindowError); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsGoForwardAndBack) { |
| scoped_refptr<const Extension> extension_with_tabs_permission = |
| ExtensionBuilder("Test").AddAPIPermission("tabs").Build(); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| const std::vector<GURL> urls = {GURL("http://foo.com"), |
| GURL("http://bar.com")}; |
| tabs::TabInterface* tab = OpenTabWithHistory(tab_list, urls); |
| ASSERT_TRUE(tab); |
| content::WebContents* web_contents = tab->GetContents(); |
| |
| const int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| |
| // Go back with chrome.tabs.goBack. |
| { |
| auto goback_function = base::MakeRefCounted<TabsGoBackFunction>(); |
| goback_function->set_extension(extension_with_tabs_permission.get()); |
| content::TestNavigationObserver observer(web_contents); |
| ASSERT_TRUE(utils::RunFunction(goback_function.get(), |
| base::StringPrintf("[%d]", tab_id), |
| profile(), utils::FunctionMode::kNone)); |
| observer.Wait(); |
| EXPECT_EQ(urls[0], web_contents->GetLastCommittedURL()); |
| EXPECT_TRUE(ui::PAGE_TRANSITION_FORWARD_BACK & web_contents->GetController() |
| .GetLastCommittedEntry() |
| ->GetTransitionType()); |
| } |
| |
| // Go forward with chrome.tabs.goForward. |
| { |
| auto goforward_function = base::MakeRefCounted<TabsGoForwardFunction>(); |
| goforward_function->set_extension(extension_with_tabs_permission.get()); |
| content::TestNavigationObserver observer(web_contents); |
| ASSERT_TRUE(utils::RunFunction(goforward_function.get(), |
| base::StringPrintf("[%d]", tab_id), |
| profile(), utils::FunctionMode::kNone)); |
| observer.Wait(); |
| EXPECT_EQ(urls[1], web_contents->GetLastCommittedURL()); |
| EXPECT_TRUE(ui::PAGE_TRANSITION_FORWARD_BACK & web_contents->GetController() |
| .GetLastCommittedEntry() |
| ->GetTransitionType()); |
| } |
| |
| // If there's no next page, chrome.tabs.goForward should return an error. |
| { |
| auto goforward_function = base::MakeRefCounted<TabsGoForwardFunction>(); |
| goforward_function->set_extension(extension_with_tabs_permission.get()); |
| std::string error = utils::RunFunctionAndReturnError( |
| goforward_function.get(), base::StringPrintf("[%d]", tab_id), profile(), |
| utils::FunctionMode::kNone); |
| EXPECT_EQ(keys::kNotFoundNextPageError, error); |
| EXPECT_EQ(urls[1], web_contents->GetLastCommittedURL()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| TabsGoForwardAndBackSavedTabGroupTab) { |
| scoped_refptr<const Extension> extension_with_tabs_permission = |
| ExtensionBuilder("Test").AddAPIPermission("tabs").Build(); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| const std::vector<GURL> urls = { |
| GURL("http://foo.com"), GURL("http://bar.com"), GURL("http://baz.com")}; |
| tabs::TabInterface* tab = OpenTabWithHistory(tab_list, urls); |
| ASSERT_TRUE(tab); |
| content::WebContents* web_contents = tab->GetContents(); |
| |
| const int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| |
| { |
| // Go back with chrome.tabs.goBack. |
| auto goback_function = base::MakeRefCounted<TabsGoBackFunction>(); |
| goback_function->set_extension(extension_with_tabs_permission.get()); |
| content::TestNavigationObserver observer(web_contents); |
| ASSERT_TRUE(utils::RunFunction(goback_function.get(), |
| base::StringPrintf("[%d]", tab_id), |
| profile(), utils::FunctionMode::kNone)); |
| observer.Wait(); |
| } |
| |
| EXPECT_EQ(urls[1], web_contents->GetLastCommittedURL()); |
| |
| tab_groups::TabGroupSyncService* saved_service = |
| tab_groups::TabGroupSyncServiceFactory::GetForProfile(profile()); |
| ASSERT_TRUE(saved_service); |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| tab_groups::TabGroupSyncServiceInitializedObserver sync_observer( |
| saved_service); |
| sync_observer.Wait(); |
| #endif |
| |
| // Save the tab and expect that it cannot be navigated forwards or backwards. |
| std::optional<tab_groups::TabGroupId> group = |
| tab_list->CreateTabGroup({tab->GetHandle()}); |
| ASSERT_TRUE(group.has_value()); |
| tab_groups::TabGroupVisualData visual_data( |
| u"Initial title", tab_groups::TabGroupColorId::kBlue); |
| tab_list->SetTabGroupVisualData(*group, visual_data); |
| |
| { |
| auto goback_function = base::MakeRefCounted<TabsGoBackFunction>(); |
| goback_function->set_extension(extension_with_tabs_permission.get()); |
| EXPECT_TRUE(utils::RunFunction(goback_function.get(), |
| base::StringPrintf("[%d]", tab_id), |
| profile(), utils::FunctionMode::kNone)); |
| } |
| EXPECT_EQ(urls[1], web_contents->GetLastCommittedURL()); |
| |
| { |
| auto goforward_function = base::MakeRefCounted<TabsGoForwardFunction>(); |
| goforward_function->set_extension(extension_with_tabs_permission.get()); |
| EXPECT_TRUE(utils::RunFunction(goforward_function.get(), |
| base::StringPrintf("[%d]", tab_id), |
| profile(), utils::FunctionMode::kNone)); |
| } |
| |
| EXPECT_EQ(urls[1], web_contents->GetLastCommittedURL()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, TabsGoForwardAndBackWithoutTabId) { |
| scoped_refptr<const Extension> extension_with_tabs_permission = |
| ExtensionBuilder("Test").AddAPIPermission("tabs").Build(); |
| TabListInterface* tab_list = GetTabListInterface(); |
| |
| // Create first tab with history. |
| const std::vector<GURL> tab1_urls = {GURL("http://a.com"), |
| GURL("http://b.com")}; |
| tabs::TabInterface* tab1 = OpenTabWithHistory(tab_list, tab1_urls); |
| ASSERT_TRUE(tab1); |
| content::WebContents* tab1_webcontents = tab1->GetContents(); |
| EXPECT_EQ(tab1_urls[1], tab1_webcontents->GetLastCommittedURL()); |
| |
| // Create second tab with history. |
| const std::vector<GURL> tab2_urls = {GURL("http://c.com"), |
| GURL("http://d.com")}; |
| tabs::TabInterface* tab2 = OpenTabWithHistory(tab_list, tab2_urls); |
| ASSERT_TRUE(tab2); |
| content::WebContents* tab2_webcontents = tab2->GetContents(); |
| EXPECT_EQ(tab2_urls[1], tab2_webcontents->GetLastCommittedURL()); |
| |
| // Activate first tab. |
| tab_list->ActivateTab(tab1->GetHandle()); |
| |
| // Go back without tab_id. But first tab should be navigated since it's |
| // activated. |
| { |
| auto goback_function = base::MakeRefCounted<TabsGoBackFunction>(); |
| goback_function->set_extension(extension_with_tabs_permission.get()); |
| content::TestNavigationObserver observer(tab1_webcontents); |
| ASSERT_TRUE(utils::RunFunction(goback_function.get(), "[]", profile(), |
| utils::FunctionMode::kNone)); |
| observer.Wait(); |
| EXPECT_EQ(tab1_urls[0], tab1_webcontents->GetLastCommittedURL()); |
| EXPECT_TRUE(ui::PAGE_TRANSITION_FORWARD_BACK & |
| tab1_webcontents->GetController() |
| .GetLastCommittedEntry() |
| ->GetTransitionType()); |
| } |
| |
| // Go forward without tab_id. |
| { |
| auto goforward_function = base::MakeRefCounted<TabsGoForwardFunction>(); |
| goforward_function->set_extension(extension_with_tabs_permission.get()); |
| content::TestNavigationObserver observer(tab1_webcontents); |
| ASSERT_TRUE(utils::RunFunction(goforward_function.get(), "[]", profile(), |
| utils::FunctionMode::kNone)); |
| observer.Wait(); |
| EXPECT_EQ(tab1_urls[1], tab1_webcontents->GetLastCommittedURL()); |
| EXPECT_TRUE(ui::PAGE_TRANSITION_FORWARD_BACK & |
| tab1_webcontents->GetController() |
| .GetLastCommittedEntry() |
| ->GetTransitionType()); |
| } |
| |
| // Activate second tab. |
| tab_list->ActivateTab(tab2->GetHandle()); |
| |
| { |
| auto goback_function2 = base::MakeRefCounted<TabsGoBackFunction>(); |
| goback_function2->set_extension(extension_with_tabs_permission.get()); |
| content::TestNavigationObserver observer(tab2_webcontents); |
| ASSERT_TRUE(utils::RunFunction(goback_function2.get(), "[]", profile(), |
| utils::FunctionMode::kNone)); |
| observer.Wait(); |
| EXPECT_EQ(tab2_urls[0], tab2_webcontents->GetLastCommittedURL()); |
| EXPECT_TRUE(ui::PAGE_TRANSITION_FORWARD_BACK & |
| tab2_webcontents->GetController() |
| .GetLastCommittedEntry() |
| ->GetTransitionType()); |
| } |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Ensure tabs.captureVisibleTab respects any Data Leak Prevention restrictions. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, ScreenshotsRestricted) { |
| // Setup the function and extension. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("Screenshot") |
| .AddAPIPermission("tabs") |
| .AddHostPermission("<all_urls>") |
| .Build(); |
| auto function = base::MakeRefCounted<TabsCaptureVisibleTabFunction>(); |
| function->set_extension(extension.get()); |
| |
| // Add a visible tab. |
| TabListInterface* tab_list = GetTabListInterface(); |
| const GURL kGoogle("http://www.google.com"); |
| tabs::TabInterface* tab = tab_list->OpenTab(kGoogle, -1); |
| content::WebContents* web_contents = tab->GetContents(); |
| content::WaitForLoadStop(web_contents); |
| |
| // Setup Data Leak Prevention restriction. |
| policy::MockDlpContentManager mock_dlp_content_manager; |
| policy::ScopedDlpContentObserverForTesting scoped_dlp_content_observer_( |
| &mock_dlp_content_manager); |
| EXPECT_CALL(mock_dlp_content_manager, IsScreenshotApiRestricted(testing::_)) |
| .Times(1) |
| .WillOnce(testing::Return(true)); |
| |
| // Run the function and check result. |
| std::string error = utils::RunFunctionAndReturnError( |
| function.get(), "[{}]", profile(), utils::FunctionMode::kNone); |
| EXPECT_EQ(keys::kScreenshotsDisabledByDlp, error); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| DontCreateTabsInLockedFullscreenMode) { |
| scoped_refptr<const Extension> extension_with_tabs_permission = |
| ExtensionBuilder("Test").AddAPIPermission("tabs").Build(); |
| |
| // In locked fullscreen mode we should not be able to create any tabs. |
| ash::PinWindow(browser_window_interface()->GetWindow()->GetNativeWindow(), |
| /*trusted=*/true); |
| |
| auto function = base::MakeRefCounted<TabsCreateFunction>(); |
| function->set_extension(extension_with_tabs_permission.get()); |
| |
| EXPECT_EQ(ExtensionTabUtil::kLockedFullscreenModeNewTabError, |
| utils::RunFunctionAndReturnError(function.get(), "[{}]", profile(), |
| utils::FunctionMode::kNone)); |
| |
| // Unpin for cleanup. |
| ash::UnpinWindow(browser_window_interface()->GetWindow()->GetNativeWindow()); |
| } |
| |
| // Screenshot should return an error when disabled in user profile preferences. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| ScreenshotDisabledInProfilePreferences) { |
| // Setup the function and extension. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("Screenshot") |
| .AddAPIPermission("tabs") |
| .AddHostPermission("<all_urls>") |
| .Build(); |
| auto function = base::MakeRefCounted<TabsCaptureVisibleTabFunction>(); |
| function->set_extension(extension.get()); |
| |
| // Add a visible tab. |
| TabListInterface* tab_list = GetTabListInterface(); |
| const GURL kGoogle("http://www.google.com"); |
| tabs::TabInterface* tab = tab_list->OpenTab(kGoogle, -1); |
| content::WebContents* web_contents = tab->GetContents(); |
| content::WaitForLoadStop(web_contents); |
| |
| // Disable screenshot. |
| profile()->GetPrefs()->SetBoolean(prefs::kDisableScreenshots, true); |
| |
| // Run the function and check result. |
| std::string error = utils::RunFunctionAndReturnError( |
| function.get(), "[{}]", profile(), utils::FunctionMode::kNone); |
| EXPECT_EQ(keys::kScreenshotsDisabled, error); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| // Picture in picture is not supported for Android. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, |
| CannotDuplicatePictureInPictureWindows) { |
| // Create picture-in-picture browser. |
| BrowserWindowInterface* pip_browser = CreateBrowserWindowWithType( |
| BrowserWindowInterface::Type::TYPE_PICTURE_IN_PICTURE); |
| TabListInterface* pip_tab_list = TabListInterface::From(pip_browser); |
| |
| // Ensure we have a tab. |
| if (pip_tab_list->GetTabCount() == 0) { |
| pip_tab_list->OpenTab(GURL(url::kAboutBlankURL), -1); |
| } |
| content::WebContents* web_contents = pip_tab_list->GetTab(0)->GetContents(); |
| int pip_tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| |
| // Attempt to duplicate the picture-in-picture tab. This should fail as |
| // picture-in-picture tabs are not allowed to be duplicated. |
| auto function = base::MakeRefCounted<TabsDuplicateFunction>(); |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("Test").AddAPIPermission("tabs").Build(); |
| function->set_extension(extension); |
| std::string args = base::StringPrintf("[%d]", pip_tab_id); |
| std::string error = utils::RunFunctionAndReturnError( |
| function.get(), args, pip_browser->GetProfile(), |
| utils::FunctionMode::kNone); |
| EXPECT_EQ(ErrorUtils::FormatErrorMessage(keys::kCannotDuplicateTab, |
| base::NumberToString(pip_tab_id)), |
| error); |
| } |
| |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Tests that calling chrome.tabs.discard on a saved tab does discard for |
| // extensions with locked fullscreen permission. Locked fullscreen permission |
| // is ChromeOS only. |
| IN_PROC_BROWSER_TEST_F( |
| ExtensionTabsTest, |
| TabsDiscardSavedTabGroupTabAllowedForLockedFullscreenPermission) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("DiscardTest") |
| .SetID("pmgljoohajacndjcjlajcopidgnhphcl") |
| .AddAPIPermission("lockWindowFullscreenPrivate") |
| .Build(); |
| const GURL kExampleCom("http://example.com"); |
| |
| TabListInterface* tab_list = GetTabListInterface(); |
| tabs::TabInterface* tab = tab_list->OpenTab(kExampleCom, -1); |
| content::WebContents* web_contents = tab->GetContents(); |
| content::WaitForLoadStop(web_contents); |
| |
| int index = tab_list->GetIndexOfTab(tab->GetHandle()); |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| |
| tab_groups::TabGroupSyncService* saved_service = |
| tab_groups::TabGroupSyncServiceFactory::GetForProfile(profile()); |
| ASSERT_TRUE(saved_service); |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| tab_groups::TabGroupSyncServiceInitializedObserver sync_observer( |
| saved_service); |
| sync_observer.Wait(); |
| #endif |
| |
| // Group the tab and save it. |
| std::optional<tab_groups::TabGroupId> group = |
| tab_list->CreateTabGroup({tab->GetHandle()}); |
| ASSERT_TRUE(group.has_value()); |
| tab_groups::TabGroupVisualData visual_data( |
| u"Initial title", tab_groups::TabGroupColorId::kBlue); |
| tab_list->SetTabGroupVisualData(*group, visual_data); |
| |
| // The tab discard function should not fail. |
| auto function = base::MakeRefCounted<TabsDiscardFunction>(); |
| function->set_extension(extension); |
| ASSERT_TRUE(utils::RunFunction(function.get(), |
| base::StringPrintf("[%d]", tab_id), profile(), |
| utils::FunctionMode::kNone)); |
| // Check that the tab was discarded. |
| EXPECT_TRUE(tab_list->GetTab(index)->GetContents()->WasDiscarded()); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| // Split view is not enabled on Android. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, SplitViewAddedAndRemoved) { |
| // Create the `TabsEventRouter`, which is required to get a tab update event. |
| TabsWindowsAPI::Get(profile())->InitTabsEventRouter(); |
| |
| auto [tab_ids, web_contentses] = |
| CreateAndGetTabData(GetTabListInterface(), 4); |
| |
| TestEventRouterObserver event_observer(EventRouter::Get(profile())); |
| |
| browser()->tab_strip_model()->ActivateTabAt(0); |
| split_tabs::SplitTabId split = browser()->tab_strip_model()->AddToNewSplit( |
| {1}, split_tabs::SplitTabVisualData(), |
| split_tabs::SplitTabCreatedSource()); |
| |
| event_observer.WaitForEventWithName(api::tabs::OnUpdated::kEventName); |
| EXPECT_TRUE( |
| event_observer.events().contains(api::tabs::OnUpdated::kEventName)); |
| |
| event_observer.ClearEvents(); |
| |
| browser()->tab_strip_model()->RemoveSplit(split); |
| |
| event_observer.WaitForEventWithName(api::tabs::OnUpdated::kEventName); |
| EXPECT_TRUE( |
| event_observer.events().contains(api::tabs::OnUpdated::kEventName)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, SplitTabsWithHighlightFunction) { |
| // Add a couple of web contents to the browser and mark them as split. |
| auto [tab_ids, web_contentses] = |
| CreateAndGetTabData(GetTabListInterface(), 2); |
| |
| browser()->tab_strip_model()->ActivateTabAt(0); |
| browser()->tab_strip_model()->AddToNewSplit( |
| {1}, split_tabs::SplitTabVisualData(), |
| split_tabs::SplitTabCreatedSource::kLinkContextMenu); |
| |
| // Run extension to highlight tabs |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("Test").AddAPIPermission("tabs").Build(); |
| std::string args = base::StringPrintf("[{\"tabs\": [%d]}]", 0); |
| auto function = base::MakeRefCounted<TabsHighlightFunction>(); |
| function->set_extension(extension); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| // Check that both sides of the split are selected. |
| ASSERT_TRUE(browser() |
| ->tab_strip_model() |
| ->selection_model() |
| .GetListSelectionModel() |
| .IsSelected(0)); |
| ASSERT_TRUE(browser() |
| ->tab_strip_model() |
| ->selection_model() |
| .GetListSelectionModel() |
| .IsSelected(1)); |
| } |
| |
| // Tests that calling chrome.tabs.move() works when a tab is moved within a |
| // split view. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, MoveWithinSplitView) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("TabsMoveWithinSplitView").Build(); |
| |
| // Add several web contents to the browser and get their tab IDs. |
| auto [tab_ids, web_contentses] = |
| CreateAndGetTabData(GetTabListInterface(), 5); |
| |
| // Create a split with tabs 3 and 4. |
| browser()->tab_strip_model()->AddToNewSplit( |
| {3}, split_tabs::SplitTabVisualData(), |
| split_tabs::SplitTabCreatedSource::kTabContextMenu); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(3).has_value()); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(4).has_value()); |
| |
| // Use the TabsMoveFunction to move tab at index 0 to the middle of the split |
| // view with tabs 3 and 4. |
| int tab_id = tab_ids[0]; |
| auto function = base::MakeRefCounted<TabsMoveFunction>(); |
| function->set_extension(extension); |
| constexpr char kFormatArgs[] = R"([[%d], {"index": 3}])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab_id); |
| |
| EXPECT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| // Expect that the tab has been moved between the two tabs previously in a |
| // split view and that the split view has been destroyed. |
| EXPECT_EQ(browser()->tab_strip_model()->GetWebContentsAt(2), |
| web_contentses[3]); |
| EXPECT_EQ(browser()->tab_strip_model()->GetWebContentsAt(3), |
| web_contentses[0]); |
| EXPECT_EQ(browser()->tab_strip_model()->GetWebContentsAt(4), |
| web_contentses[4]); |
| EXPECT_FALSE(browser()->tab_strip_model()->GetSplitForTab(2).has_value()); |
| EXPECT_FALSE(browser()->tab_strip_model()->GetSplitForTab(4).has_value()); |
| } |
| |
| // Tests that calling chrome.tabs.move() works when a tab within a split view is |
| // moved. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, MoveFromSplitView) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("TabsMoveFromSplitView").Build(); |
| |
| // Add several web contents to the browser and get their tab IDs. |
| auto [tab_ids, web_contentses] = |
| CreateAndGetTabData(GetTabListInterface(), 5); |
| |
| // Create a split with tabs 3 and 4. |
| browser()->tab_strip_model()->AddToNewSplit( |
| {3}, split_tabs::SplitTabVisualData(), |
| split_tabs::SplitTabCreatedSource::kTabContextMenu); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(3).has_value()); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(4).has_value()); |
| |
| // Use the TabsMoveFunction to move split tab at index 3 to index 0. |
| int tab_id = tab_ids[3]; |
| auto function = base::MakeRefCounted<TabsMoveFunction>(); |
| function->set_extension(extension); |
| constexpr char kFormatArgs[] = R"([[%d], {"index": 0}])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab_id); |
| |
| EXPECT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| // Expect that the tab has been moved to index 0 and the original split view |
| // is removed. |
| EXPECT_EQ(browser()->tab_strip_model()->GetWebContentsAt(0), |
| web_contentses[3]); |
| EXPECT_EQ(browser()->tab_strip_model()->GetWebContentsAt(1), |
| web_contentses[0]); |
| EXPECT_FALSE(browser()->tab_strip_model()->GetSplitForTab(0).has_value()); |
| EXPECT_FALSE(browser()->tab_strip_model()->GetSplitForTab(4).has_value()); |
| } |
| |
| // Tests that chrome.tabs.duplicate removes split view. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DuplicateSplitView) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("TabsDuplicateSplitView").Build(); |
| |
| // Add a couple of web contents to the browser and mark them as split. |
| auto [tab_ids, web_contentses] = |
| CreateAndGetTabData(GetTabListInterface(), 2); |
| browser()->tab_strip_model()->ActivateTabAt(0); |
| browser()->tab_strip_model()->AddToNewSplit( |
| {1}, split_tabs::SplitTabVisualData(), |
| split_tabs::SplitTabCreatedSource::kTabContextMenu); |
| |
| // Check that the two tabs are split |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(0).has_value()); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(1).has_value()); |
| |
| // Use the TabsDuplicateFunction to duplicate the tab at index 0. |
| int tab_id = tab_ids[0]; |
| auto function = base::MakeRefCounted<TabsDuplicateFunction>(); |
| function->set_extension(extension); |
| constexpr char kFormatArgs[] = R"([%d])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab_id); |
| |
| EXPECT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| // Expect that there is one new tab in the tab strip the split view has been |
| // removed. |
| EXPECT_EQ(3, browser()->tab_strip_model()->count()); |
| EXPECT_FALSE(browser()->tab_strip_model()->GetSplitForTab(0).has_value()); |
| EXPECT_FALSE(browser()->tab_strip_model()->GetSplitForTab(1).has_value()); |
| EXPECT_FALSE(browser()->tab_strip_model()->GetSplitForTab(2).has_value()); |
| } |
| |
| // Tests that calling chrome.tabs.discard on an inactive tab in an active split |
| // will discard that tab. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DiscardInactiveTabInActiveSplitView) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("TabsDeleteFromSplitView").Build(); |
| |
| // Add a couple of web contents to the browser and mark them as split. |
| auto [tab_ids, web_contentses] = |
| CreateAndGetTabData(GetTabListInterface(), 2); |
| browser()->tab_strip_model()->ActivateTabAt(0); |
| browser()->tab_strip_model()->AddToNewSplit( |
| {1}, split_tabs::SplitTabVisualData(), |
| split_tabs::SplitTabCreatedSource::kTabContextMenu); |
| |
| // Check that the two tabs are split and the tab at index 0 is active. |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(0).has_value()); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(1).has_value()); |
| EXPECT_EQ(0, browser()->tab_strip_model()->active_index()); |
| |
| // The tab discard function should succeed. |
| int tab_id = tab_ids[1]; |
| auto function = base::MakeRefCounted<TabsDiscardFunction>(); |
| function->set_extension(extension); |
| EXPECT_TRUE(utils::RunFunction(function.get(), |
| base::StringPrintf("[%d]", tab_id), profile(), |
| utils::FunctionMode::kNone)); |
| |
| // The tab should be discarded. |
| content::WebContents* new_contents_at_index = |
| browser()->tab_strip_model()->GetWebContentsAt(1); |
| EXPECT_TRUE(new_contents_at_index->WasDiscarded()); |
| } |
| |
| // Tests that calling chrome.tabs.delete works when a tab within a split view |
| // is deleted. |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DeleteFromSplitView) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("TabsDeleteFromSplitView").Build(); |
| |
| // Add a couple of web contents to the browser and mark them as split. |
| auto [tab_ids, web_contentses] = |
| CreateAndGetTabData(GetTabListInterface(), 2); |
| browser()->tab_strip_model()->ActivateTabAt(0); |
| browser()->tab_strip_model()->AddToNewSplit( |
| {1}, split_tabs::SplitTabVisualData(), |
| split_tabs::SplitTabCreatedSource::kTabContextMenu); |
| |
| // Check that the two tabs are split |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(0).has_value()); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(1).has_value()); |
| |
| // Use the TabsRemoveFunction to remove the tab at index 0. |
| int tab_id = tab_ids[0]; |
| auto function = base::MakeRefCounted<TabsRemoveFunction>(); |
| function->set_extension(extension); |
| constexpr char kFormatArgs[] = R"([[%d]])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab_id); |
| |
| EXPECT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| // Expect that the tab has been removed and the remaining tab is not in a |
| // split view. |
| EXPECT_EQ(1, browser()->tab_strip_model()->count()); |
| EXPECT_FALSE(browser()->tab_strip_model()->GetSplitForTab(0).has_value()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, QueryWithSplitView) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("TabsDeleteFromSplitView").Build(); |
| |
| // Add a couple of web contents to the browser and mark the first two as |
| // split. |
| auto [tab_ids, web_contentses] = |
| CreateAndGetTabData(GetTabListInterface(), 5); |
| browser()->tab_strip_model()->ActivateTabAt(0); |
| browser()->tab_strip_model()->AddToNewSplit( |
| {1}, split_tabs::SplitTabVisualData(), |
| split_tabs::SplitTabCreatedSource::kTabContextMenu); |
| |
| // Check that the two tabs are split |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(0).has_value()); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(1).has_value()); |
| |
| // Use the TabsQueryFunction to get the list of tabs without a split. |
| const char* kNoSplitQueryInfo = "[{\"splitViewId\": -1}]"; |
| base::ListValue tabs_list_without_split = |
| RunQueryFunction(extension.get(), kNoSplitQueryInfo); |
| EXPECT_EQ(3u, tabs_list_without_split.size()); |
| |
| int split_id = ExtensionTabUtil::GetSplitId( |
| browser()->tab_strip_model()->GetSplitForTab(0).value()); |
| |
| constexpr char kFormatArgs[] = R"([{"splitViewId": %d}])"; |
| const std::string args = base::StringPrintf(kFormatArgs, split_id); |
| base::ListValue tabs_list_with_split = |
| RunQueryFunction(extension.get(), args.c_str()); |
| EXPECT_EQ(2u, tabs_list_with_split.size()); |
| EXPECT_EQ(split_id, tabs_list_with_split[0].GetDict().FindInt("splitViewId")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, UngroupSingleTabFromSplitView) { |
| TabListInterface* tab_list = GetTabListInterface(); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("TabsUngroupSingleTabFromSplitView").Build(); |
| |
| // Add a couple of web contents to the browser and mark the first two as |
| // split. |
| auto [tab_ids, web_contentses] = |
| CreateAndGetTabData(GetTabListInterface(), 5); |
| browser()->tab_strip_model()->ActivateTabAt(0); |
| browser()->tab_strip_model()->AddToNewSplit( |
| {1}, split_tabs::SplitTabVisualData(), |
| split_tabs::SplitTabCreatedSource::kTabContextMenu); |
| |
| // Add tabs 0 and 1 to a group. |
| std::optional<tab_groups::TabGroupId> group = tab_list->CreateTabGroup( |
| {tab_list->GetTab(0)->GetHandle(), tab_list->GetTab(1)->GetHandle()}); |
| ASSERT_TRUE(group.has_value()); |
| |
| // Use the TabsUngroupFunction to ungroup tab 1 |
| auto function = base::MakeRefCounted<TabsUngroupFunction>(); |
| function->set_extension(extension); |
| constexpr char kFormatArgs[] = R"([[%d]])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab_ids[1]); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| // Expect the group to be deleted because all tabs were ungrouped from it but |
| // the split view will remain. |
| EXPECT_FALSE(tab_list->GetTab(0)->GetGroup()); |
| EXPECT_FALSE(tab_list->GetTab(1)->GetGroup()); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(0).has_value()); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(1).has_value()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, UngroupBothTabsFromSplitView) { |
| TabListInterface* tab_list = GetTabListInterface(); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("TabsUngroupBothTabsFromSplitView").Build(); |
| |
| // Add a couple of web contents to the browser and mark the first two as |
| // split. |
| auto [tab_ids, web_contentses] = |
| CreateAndGetTabData(GetTabListInterface(), 5); |
| browser()->tab_strip_model()->ActivateTabAt(0); |
| browser()->tab_strip_model()->AddToNewSplit( |
| {1}, split_tabs::SplitTabVisualData(), |
| split_tabs::SplitTabCreatedSource::kTabContextMenu); |
| |
| // Add tabs 0 and 1 to a group. |
| std::optional<tab_groups::TabGroupId> group = tab_list->CreateTabGroup( |
| {tab_list->GetTab(0)->GetHandle(), tab_list->GetTab(1)->GetHandle()}); |
| ASSERT_TRUE(group.has_value()); |
| |
| // Use the TabsUngroupFunction to ungroup tabs 0 and 1 |
| auto function = base::MakeRefCounted<TabsUngroupFunction>(); |
| function->set_extension(extension); |
| constexpr char kFormatArgs[] = R"([[%d, %d]])"; |
| const std::string args = |
| base::StringPrintf(kFormatArgs, tab_ids[0], tab_ids[1]); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| // Expect the group to be deleted because all tabs were ungrouped from it but |
| // the split view will remain. |
| EXPECT_FALSE(tab_list->GetTab(0)->GetGroup()); |
| EXPECT_FALSE(tab_list->GetTab(1)->GetGroup()); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(0).has_value()); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(1).has_value()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, GroupSingleTabInSplitView) { |
| TabListInterface* tab_list = GetTabListInterface(); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("TabsGroupSingleTabInSplitView").Build(); |
| |
| // Add a couple of web contents to the browser and mark them as split. |
| auto [tab_ids, web_contentses] = |
| CreateAndGetTabData(GetTabListInterface(), 2); |
| browser()->tab_strip_model()->ActivateTabAt(0); |
| browser()->tab_strip_model()->AddToNewSplit( |
| {1}, split_tabs::SplitTabVisualData(), |
| split_tabs::SplitTabCreatedSource::kTabContextMenu); |
| |
| // Verify that tabs are in a split view. |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(0).has_value()); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(1).has_value()); |
| |
| // Use the TabsGroupFunction to group tab 0. |
| auto function = base::MakeRefCounted<TabsGroupFunction>(); |
| function->set_extension(extension); |
| constexpr char kFormatArgs[] = R"([{"tabIds": [%d]}])"; |
| const std::string args = base::StringPrintf(kFormatArgs, tab_ids[0]); |
| ASSERT_TRUE(utils::RunFunction(function.get(), args, profile(), |
| utils::FunctionMode::kNone)); |
| |
| // Expect both tabs to be in the same group and still in a split view. |
| std::optional<tab_groups::TabGroupId> group = tab_list->GetTab(0)->GetGroup(); |
| EXPECT_TRUE(group.has_value()); |
| EXPECT_EQ(group, tab_list->GetTab(1)->GetGroup()); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(0).has_value()); |
| EXPECT_TRUE(browser()->tab_strip_model()->GetSplitForTab(1).has_value()); |
| } |
| |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| class ExtensionTabsWebContentsDiscardDisabledTest : public ExtensionTabsTest { |
| public: |
| ExtensionTabsWebContentsDiscardDisabledTest() { |
| scoped_feature_list_.InitAndDisableFeature(features::kWebContentsDiscard); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionTabsWebContentsDiscardDisabledTest, |
| OnReplacedEvent) { |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "onReplaced Test", |
| "version": "1.0", |
| "manifest_version": 3, |
| "background": { |
| "service_worker": "background.js" |
| } |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"( |
| chrome.tabs.create({"url": "about:blank"}, function(tab) { |
| chrome.tabs.onReplaced.addListener(function(new_tab_id, old_tab_id) { |
| if (old_tab_id === tab.id && new_tab_id !== tab.id) { |
| chrome.test.sendMessage("success"); |
| } else { |
| chrome.test.sendMessage("failure"); |
| } |
| }); |
| chrome.test.sendMessage("ready"); |
| }); |
| )"); |
| |
| ExtensionTestMessageListener ready_listener("ready"); |
| ExtensionTestMessageListener success_listener("success"); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| // Wait for the JS to create the tab and attach its listener. |
| ASSERT_TRUE(ready_listener.WaitUntilSatisfied()); |
| |
| // Do the replacement on the last tab (the one the extension just created). |
| TabStripModel* tab_strip_model = browser()->tab_strip_model(); |
| int target_index = tab_strip_model->count() - 1; |
| |
| auto new_contents = |
| content::WebContents::Create(content::WebContents::CreateParams( |
| browser()->profile(), |
| content::SiteInstance::Create(browser()->profile()))); |
| |
| auto old_contents = tab_strip_model->DiscardWebContentsAt( |
| target_index, std::move(new_contents)); |
| |
| // Wait for the JS test to catch the event and send "success". |
| ASSERT_TRUE(success_listener.WaitUntilSatisfied()); |
| } |
| |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| } // namespace extensions |