blob: 3d4ef383375beb851ae611e75d7348b192088f53 [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/extensions/api/tabs/tabs_api.h"
#include <array>
#include <memory>
#include <optional>
#include <utility>
#include "base/containers/contains.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/values_test_util.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/api/tabs/tabs_constants.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_tab_helper_factory.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/tabs/split_tab_metrics.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/test_browser_window.h"
#include "components/saved_tab_groups/public/features.h"
#include "components/saved_tab_groups/public/saved_tab_group.h"
#include "components/saved_tab_groups/public/tab_group_sync_service.h"
#include "components/saved_tab_groups/public/types.h"
#include "components/sessions/content/session_tab_helper.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tabs/public/split_tab_collection.h"
#include "components/tabs/public/split_tab_visual_data.h"
#include "components/tabs/public/tab_group.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/web_contents_tester.h"
#include "extensions/browser/api_test_utils.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_builder.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "ash/test/ash_test_helper.h"
#include "ash/test/test_window_builder.h"
#include "ash/wm/window_pin_util.h"
#include "chrome/browser/chromeos/policy/dlp/test/mock_dlp_content_manager.h"
#endif // BUILDFLAG(IS_CHROMEOS)
namespace extensions {
namespace {
base::Value::List RunTabsQueryFunction(content::BrowserContext* browser_context,
const Extension* extension,
const std::string& query_info) {
auto function = base::MakeRefCounted<TabsQueryFunction>();
function->set_extension(extension);
std::optional<base::Value> value =
api_test_utils::RunFunctionAndReturnSingleResult(
function.get(), query_info, browser_context,
api_test_utils::FunctionMode::kNone);
return std::move(*value).TakeList();
}
// Creates an extension with "tabs" permission.
scoped_refptr<const Extension> CreateTabsExtension() {
return ExtensionBuilder("Extension with tabs permission")
.AddAPIPermission("tabs")
.Build();
}
// Creates a WebContents, attaches it to the tab strip, and navigates so we
// have |urls| as history.
content::WebContents* CreateAndAppendWebContentsWithHistory(
Profile* profile,
TabStripModel* tab_strip_model,
const std::vector<GURL>& urls) {
std::unique_ptr<content::WebContents> web_contents =
content::WebContentsTester::CreateTestWebContents(profile, nullptr);
content::WebContents* raw_web_contents = web_contents.get();
tab_strip_model->AppendWebContents(std::move(web_contents), true);
for (const auto& url : urls) {
content::NavigationSimulator::NavigateAndCommitFromBrowser(raw_web_contents,
url);
EXPECT_EQ(url, raw_web_contents->GetLastCommittedURL());
EXPECT_EQ(url, raw_web_contents->GetVisibleURL());
}
return raw_web_contents;
}
} // namespace
class TabsApiUnitTest : public ExtensionServiceTestBase {
public:
TabsApiUnitTest(const TabsApiUnitTest&) = delete;
TabsApiUnitTest& operator=(const TabsApiUnitTest&) = delete;
protected:
TabsApiUnitTest()
: ExtensionServiceTestBase(
std::make_unique<content::BrowserTaskEnvironment>(
base::test::TaskEnvironment::MainThreadType::UI)) {}
~TabsApiUnitTest() override = default;
Browser* browser() { return browser_.get(); }
TestBrowserWindow* browser_window() { return browser_window_.get(); }
TabStripModel* GetTabStripModel() { return browser_->tab_strip_model(); }
content::WebContents* GetActiveWebContents() {
return GetTabStripModel()->GetActiveWebContents();
}
tab_groups::TabGroupSyncService* sync_service() {
return tab_groups::TabGroupSyncServiceFactory::GetForProfile(profile());
}
#if BUILDFLAG(IS_CHROMEOS)
aura::Window* root_window() { return test_helper_.GetContext(); }
#endif
// Returns whether the commit succeeded or not.
bool CommitPendingLoadForController(
content::NavigationController& controller);
private:
// ExtensionServiceTestBase:
void SetUp() override;
void TearDown() override;
// The browser (and accompanying window).
raw_ptr<TestBrowserWindow> browser_window_;
std::unique_ptr<Browser> browser_;
#if BUILDFLAG(IS_CHROMEOS)
ash::AshTestHelper test_helper_;
#endif
};
void TabsApiUnitTest::SetUp() {
#if BUILDFLAG(IS_CHROMEOS)
ash::AshTestHelper::InitParams ash_params;
ash_params.start_session = true;
test_helper_.SetUp(std::move(ash_params));
#endif
// Force TabManager/TabLifecycleUnitSource creation.
g_browser_process->GetTabManager();
ExtensionServiceTestBase::SetUp();
InitializeEmptyExtensionService();
auto browser_window = std::make_unique<TestBrowserWindow>();
browser_window_ = browser_window.get();
Browser::CreateParams params(profile(), true);
params.type = Browser::TYPE_NORMAL;
params.window = browser_window.release();
browser_ = Browser::DeprecatedCreateOwnedForTesting(params);
tab_groups::TabGroupSyncService* saved_service = sync_service();
ASSERT_TRUE(saved_service);
saved_service->SetIsInitializedForTesting(true);
}
void TabsApiUnitTest::TearDown() {
// Do this first before resetting `browser_`.
GetTabStripModel()->CloseAllTabs();
browser_window_ = nullptr;
browser_.reset();
ExtensionServiceTestBase::TearDown();
#if BUILDFLAG(IS_CHROMEOS)
test_helper_.TearDown();
#endif
}
bool TabsApiUnitTest::CommitPendingLoadForController(
content::NavigationController& controller) {
if (!controller.GetPendingEntry()) {
return false;
}
content::RenderFrameHostTester::CommitPendingLoad(&controller);
return true;
}
// Bug fix for crbug.com/1196309. Ensure that an extension can't update the tab
// strip while a tab drag is in progress.
TEST_F(TabsApiUnitTest, IsTabStripEditable) {
// Add a couple of web contents to the browser and get their tab IDs.
constexpr int kNumTabs = 2;
std::vector<int> tab_ids;
std::vector<content::WebContents*> web_contentses;
for (int i = 0; i < kNumTabs; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
tab_ids.push_back(
sessions::SessionTabHelper::IdForTab(contents.get()).id());
web_contentses.push_back(contents.get());
GetTabStripModel()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
ASSERT_EQ(kNumTabs, GetTabStripModel()->count());
ASSERT_TRUE(browser_window()->IsTabStripEditable());
auto extension = CreateTabsExtension();
// 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);
ASSERT_TRUE(api_test_utils::RunFunction(
function.get(), args, profile(), api_test_utils::FunctionMode::kNone));
}
// Start logical drag.
browser_window()->SetTabStripNotEditableForTesting();
ASSERT_FALSE(browser_window()->IsTabStripEditable());
// 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);
std::optional<base::Value> value =
api_test_utils::RunFunctionAndReturnSingleResult(
function.get(), args, profile(),
api_test_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);
ASSERT_TRUE(api_test_utils::RunFunction(
function.get(), args, profile(), api_test_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);
ASSERT_TRUE(api_test_utils::RunFunction(
function.get(), args, profile(), api_test_utils::FunctionMode::kNone));
}
// Bug fix for crbug.com/1198717. Error updating tabs while drag in progress.
{
std::string args =
base::StringPrintf("[%d, {\"highlighted\": true}]", tab_ids[0]);
auto function = base::MakeRefCounted<TabsUpdateFunction>();
function->set_extension(extension);
std::string error = api_test_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);
std::string error = api_test_utils::RunFunctionAndReturnError(
function.get(), args, profile(), api_test_utils::FunctionMode::kNone);
EXPECT_EQ(ExtensionTabUtil::kTabStripNotEditableError, error);
}
// Bug fix for crbug.com/1197146. 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);
std::string error = api_test_utils::RunFunctionAndReturnError(
function.get(), args, profile());
EXPECT_EQ(ExtensionTabUtil::kTabStripNotEditableError, error);
}
// TODO(solomonkinard): Consider adding tests for drag cancellation.
}
TEST_F(TabsApiUnitTest, QueryWithoutTabsPermission) {
auto tab_urls = std::to_array<GURL>({
GURL("http://www.google.com"),
GURL("http://www.example.com"),
GURL("https://www.google.com"),
});
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) {
std::unique_ptr<content::WebContents> web_contents =
content::WebContentsTester::CreateTestWebContents(profile(), nullptr);
content::WebContents* raw_web_contents = web_contents.get();
web_contentses[i] = raw_web_contents;
GetTabStripModel()->AppendWebContents(std::move(web_contents), true);
EXPECT_EQ(GetActiveWebContents(), raw_web_contents);
content::WebContentsTester* web_contents_tester =
content::WebContentsTester::For(raw_web_contents);
web_contents_tester->NavigateAndCommit(tab_urls[i]);
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::Value::List tabs_list_without_permission =
RunTabsQueryFunction(profile(), 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::Value::Dict()
.Set("name", "Extension with tabs permission")
.Set("version", "1.0")
.Set("manifest_version", 2)
.Set("permissions", base::Value::List().Append("tabs")))
.Build();
base::Value::List tabs_list_with_permission = RunTabsQueryFunction(
profile(), 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);
}
TEST_F(TabsApiUnitTest, QueryWithHostPermission) {
auto tab_urls = std::to_array<GURL>({
GURL("http://www.google.com"),
GURL("http://www.example.com"),
GURL("https://www.google.com/test"),
});
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) {
std::unique_ptr<content::WebContents> web_contents =
content::WebContentsTester::CreateTestWebContents(profile(), nullptr);
content::WebContents* raw_web_contents = web_contents.get();
web_contentses[i] = raw_web_contents;
GetTabStripModel()->AppendWebContents(std::move(web_contents), true);
EXPECT_EQ(GetActiveWebContents(), raw_web_contents);
content::WebContentsTester* web_contents_tester =
content::WebContentsTester::For(raw_web_contents);
web_contents_tester->NavigateAndCommit(tab_urls[i]);
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::Value::Dict()
.Set("name", "Extension with tabs permission")
.Set("version", "1.0")
.Set("manifest_version", 2)
.Set("permissions", base::Value::List().Append(
"*://www.google.com/*")))
.Build();
{
base::Value::List tabs_list_with_permission = RunTabsQueryFunction(
profile(), 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::Value::List tabs_list_with_permission = RunTabsQueryFunction(
profile(), 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(base::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(base::Contains(expected_tabs_ids, *third_tab_id));
}
}
// Test that using the PDF extension for tab updates is treated as a
// renderer-initiated navigation. crbug.com/660498
TEST_F(TabsApiUnitTest, PDFExtensionNavigation) {
auto manifest = base::Value::Dict()
.Set("name", "pdfext")
.Set("description", "desc")
.Set("version", "0.1")
.Set("manifest_version", 2)
.Set("permissions", base::Value::List().Append("tabs"));
scoped_refptr<const Extension> extension =
ExtensionBuilder()
.SetManifest(std::move(manifest))
.SetID(extension_misc::kPdfExtensionId)
.Build();
ASSERT_TRUE(extension);
std::unique_ptr<content::WebContents> web_contents =
content::WebContentsTester::CreateTestWebContents(profile(), nullptr);
content::WebContents* raw_web_contents = web_contents.get();
ASSERT_TRUE(raw_web_contents);
GetTabStripModel()->AppendWebContents(std::move(web_contents), true);
content::WebContentsTester* web_contents_tester =
content::WebContentsTester::For(raw_web_contents);
const GURL kGoogle("http://www.google.com");
web_contents_tester->NavigateAndCommit(kGoogle);
EXPECT_EQ(kGoogle, raw_web_contents->GetLastCommittedURL());
EXPECT_EQ(kGoogle, raw_web_contents->GetVisibleURL());
CreateSessionServiceTabHelper(raw_web_contents);
int tab_id = sessions::SessionTabHelper::IdForTab(raw_web_contents).id();
auto function = base::MakeRefCounted<TabsUpdateFunction>();
function->SetBrowserContextForTesting(profile());
function->set_extension(extension.get());
function->SetArgs(base::test::ParseJsonList(
base::StringPrintf(R"([%d, {"url":"http://example.com"}])", tab_id)));
api_test_utils::SendResponseHelper response_helper(function.get());
function->RunWithValidation().Execute();
EXPECT_EQ(kGoogle, raw_web_contents->GetLastCommittedURL());
EXPECT_EQ(kGoogle, raw_web_contents->GetVisibleURL());
// Clean up.
response_helper.WaitForResponse();
base::RunLoop().RunUntilIdle();
}
// Tests that non-validation failure in tabs.executeScript results in error, and
// not bad_message.
// Regression test for https://crbug.com/642794.
TEST_F(TabsApiUnitTest, ExecuteScriptNoTabIsNonFatalError) {
scoped_refptr<const Extension> extension_with_tabs_permission =
CreateTabsExtension();
auto function = base::MakeRefCounted<TabsExecuteScriptFunction>();
function->set_extension(extension_with_tabs_permission);
const char* kArgs = R"(["", {"code": ""}])";
std::string error = api_test_utils::RunFunctionAndReturnError(
function.get(), kArgs,
profile(), // profile() doesn't have any tabs.
api_test_utils::FunctionMode::kNone);
EXPECT_EQ(tabs_constants::kNoTabInBrowserWindowError, error);
}
// Tests that calling chrome.tabs.update updates the URL as expected.
TEST_F(TabsApiUnitTest, TabsUpdate) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("UpdateTest").Build();
const GURL kExampleCom("http://example.com");
const GURL kChromiumOrg("https://chromium.org");
// Add a web contents to the browser.
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
content::WebContents* raw_contents = contents.get();
GetTabStripModel()->AppendWebContents(std::move(contents), true);
EXPECT_EQ(GetActiveWebContents(), raw_contents);
CreateSessionServiceTabHelper(raw_contents);
int tab_id = sessions::SessionTabHelper::IdForTab(raw_contents).id();
// Navigate the browser to example.com
content::WebContentsTester* web_contents_tester =
content::WebContentsTester::For(raw_contents);
web_contents_tester->NavigateAndCommit(kExampleCom);
EXPECT_EQ(kExampleCom, raw_contents->GetLastCommittedURL());
// Use the TabsUpdateFunction to navigate to chromium.org
auto function = base::MakeRefCounted<TabsUpdateFunction>();
function->set_extension(extension);
static constexpr char kFormatArgs[] = R"([%d, {"url": "%s"}])";
const std::string args =
base::StringPrintf(kFormatArgs, tab_id, kChromiumOrg.spec().c_str());
ASSERT_TRUE(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
ASSERT_TRUE(
CommitPendingLoadForController(GetActiveWebContents()->GetController()));
EXPECT_EQ(kChromiumOrg, raw_contents->GetLastCommittedURL());
}
// Tests that calling chrome.tabs.update does not update a saved tab.
TEST_F(TabsApiUnitTest, TabsUpdateSavedTabGroupTab) {
const GURL kExampleCom("http://example.com");
const GURL kChromiumOrg("https://chromium.org");
// Add a web contents to the browser.
content::WebContents* raw_contents;
{
std::unique_ptr<content::WebContents> contents =
content::WebContentsTester::CreateTestWebContents(profile(), nullptr);
raw_contents = contents.get();
GetTabStripModel()->AppendWebContents(std::move(contents), true);
}
// contents used to test active state by taking active state first.
content::WebContents* raw_non_updated_contents;
{
std::unique_ptr<content::WebContents> non_updated_contents =
content::WebContentsTester::CreateTestWebContents(profile(), nullptr);
raw_non_updated_contents = non_updated_contents.get();
GetTabStripModel()->AppendWebContents(std::move(non_updated_contents),
false);
}
ASSERT_NE(raw_contents, nullptr);
ASSERT_NE(raw_non_updated_contents, nullptr);
EXPECT_EQ(GetActiveWebContents(), raw_contents);
CreateSessionServiceTabHelper(raw_contents);
int tab_id = sessions::SessionTabHelper::IdForTab(raw_contents).id();
int non_updated_tab_id =
sessions::SessionTabHelper::IdForTab(raw_non_updated_contents).id();
// Navigate the browser to example.com
content::WebContentsTester* web_contents_tester =
content::WebContentsTester::For(raw_contents);
web_contents_tester->NavigateAndCommit(kExampleCom);
EXPECT_EQ(kExampleCom, raw_contents->GetLastCommittedURL());
tab_groups::TabGroupSyncService* saved_service = sync_service();
ASSERT_TRUE(saved_service);
// Group the tab and save it.
tab_groups::TabGroupId group = GetTabStripModel()->AddToNewGroup(
{GetTabStripModel()->GetIndexOfWebContents(raw_contents)});
tab_groups::TabGroupVisualData visual_data(
u"Initial title", tab_groups::TabGroupColorId::kBlue);
browser()->tab_strip_model()->ChangeTabGroupVisuals(group, visual_data);
EXPECT_TRUE(
ExtensionTabUtil::TabIsInSavedTabGroup(raw_contents, GetTabStripModel()));
{ // Test the active state change for a saved tab.
GetTabStripModel()->ActivateTabAt(
GetTabStripModel()->GetIndexOfWebContents(raw_non_updated_contents));
scoped_refptr<const Extension> extension =
ExtensionBuilder("UpdateTest").Build();
auto function = base::MakeRefCounted<TabsUpdateFunction>();
function->set_extension(extension);
static constexpr char kFormatArgs[] = R"([%d, {"active": true}])";
const std::string args = base::StringPrintf(kFormatArgs, tab_id);
EXPECT_TRUE(api_test_utils::RunFunction(
function.get(), args, profile(), api_test_utils::FunctionMode::kNone));
EXPECT_EQ(GetActiveWebContents(), raw_contents);
}
ASSERT_TRUE(saved_service->GetGroup(group));
{ // Reset the active states, and then test highlighted for a saved tab.
GetTabStripModel()->ActivateTabAt(
GetTabStripModel()->GetIndexOfWebContents(raw_non_updated_contents));
GetTabStripModel()->DeselectTabAt(
GetTabStripModel()->GetIndexOfWebContents(raw_contents));
scoped_refptr<const Extension> extension =
ExtensionBuilder("UpdateTest").Build();
auto function = base::MakeRefCounted<TabsUpdateFunction>();
function->set_extension(extension);
static constexpr char kFormatArgs[] = R"([%d, {"highlighted": true}])";
const std::string args = base::StringPrintf(kFormatArgs, tab_id);
EXPECT_TRUE(api_test_utils::RunFunction(
function.get(), args, profile(), api_test_utils::FunctionMode::kNone));
EXPECT_EQ(GetActiveWebContents(), raw_contents);
}
ASSERT_TRUE(saved_service->GetGroup(group));
{ // Reset the active states, and then test selected state for a saved tab.
GetTabStripModel()->ActivateTabAt(
GetTabStripModel()->GetIndexOfWebContents(raw_non_updated_contents));
GetTabStripModel()->DeselectTabAt(
GetTabStripModel()->GetIndexOfWebContents(raw_contents));
scoped_refptr<const Extension> extension =
ExtensionBuilder("UpdateTest").Build();
auto function = base::MakeRefCounted<TabsUpdateFunction>();
function->set_extension(extension);
static constexpr char kFormatArgs[] = R"([%d, {"selected": true}])";
const std::string args = base::StringPrintf(kFormatArgs, tab_id);
EXPECT_TRUE(api_test_utils::RunFunction(
function.get(), args, profile(), api_test_utils::FunctionMode::kNone));
EXPECT_TRUE(GetTabStripModel()->IsTabSelected(
GetTabStripModel()->GetIndexOfWebContents(raw_contents)));
}
ASSERT_TRUE(saved_service->GetGroup(group));
{ // Test Muted state.
scoped_refptr<const Extension> extension =
ExtensionBuilder("UpdateTest").Build();
auto function = base::MakeRefCounted<TabsUpdateFunction>();
function->set_extension(extension);
static constexpr char kFormatArgs[] = R"([%d, {"muted": true}])";
const std::string args = base::StringPrintf(kFormatArgs, tab_id);
EXPECT_TRUE(api_test_utils::RunFunction(
function.get(), args, profile(), api_test_utils::FunctionMode::kNone));
}
ASSERT_TRUE(saved_service->GetGroup(group));
{ // Test setting the opener.
scoped_refptr<const Extension> extension =
ExtensionBuilder("UpdateTest").Build();
auto function = base::MakeRefCounted<TabsUpdateFunction>();
function->set_extension(extension);
static constexpr char kFormatArgs[] = R"([%d, {"openerTabId": %d}])";
const std::string args =
base::StringPrintf(kFormatArgs, tab_id, non_updated_tab_id);
EXPECT_TRUE(api_test_utils::RunFunction(
function.get(), args, profile(), api_test_utils::FunctionMode::kNone));
}
ASSERT_TRUE(saved_service->GetGroup(group));
{ // Test setting the disard state.
scoped_refptr<const Extension> extension =
ExtensionBuilder("UpdateTest").Build();
auto function = base::MakeRefCounted<TabsUpdateFunction>();
function->set_extension(extension);
static constexpr char kFormatArgs[] = R"([%d, {"autoDiscardable": true}])";
const std::string args = base::StringPrintf(kFormatArgs, tab_id);
EXPECT_TRUE(api_test_utils::RunFunction(
function.get(), args, profile(), api_test_utils::FunctionMode::kNone));
}
ASSERT_TRUE(saved_service->GetGroup(group));
{ // Test setting URL should pass.
scoped_refptr<const Extension> extension =
ExtensionBuilder("UpdateTest").Build();
auto function = base::MakeRefCounted<TabsUpdateFunction>();
function->set_extension(extension);
static constexpr char kFormatArgs[] = R"([%d, {"url": "%s"}])";
const std::string args =
base::StringPrintf(kFormatArgs, tab_id, kChromiumOrg.spec().c_str());
EXPECT_TRUE(api_test_utils::RunFunction(
function.get(), args, profile(), api_test_utils::FunctionMode::kNone));
}
ASSERT_TRUE(saved_service->GetGroup(group));
// Test setting pinned state should pass. This must be done last since pinning
// destroys the group.
{
scoped_refptr<const Extension> extension =
ExtensionBuilder("UpdateTest").Build();
auto function = base::MakeRefCounted<TabsUpdateFunction>();
function->set_extension(extension);
static constexpr char kFormatArgs[] = R"([%d, {"pinned": true}])";
const std::string args = base::StringPrintf(kFormatArgs, tab_id);
EXPECT_TRUE(api_test_utils::RunFunction(
function.get(), args, profile(), api_test_utils::FunctionMode::kNone));
}
ASSERT_FALSE(saved_service->GetGroup(group));
}
// Tests that calling chrome.tabs.update with a JavaScript URL results
// in an error.
TEST_F(TabsApiUnitTest, TabsUpdateJavaScriptUrlNotAllowed) {
// An extension with access to www.example.com.
scoped_refptr<const Extension> extension =
ExtensionBuilder()
.SetManifest(base::Value::Dict()
.Set("name", "Extension with a host permission")
.Set("version", "1.0")
.Set("manifest_version", 2)
.Set("permissions", base::Value::List().Append(
"http://www.example.com/*")))
.Build();
auto function = base::MakeRefCounted<TabsUpdateFunction>();
function->set_extension(extension);
// Add a web contents to the browser.
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
content::WebContents* raw_contents = contents.get();
GetTabStripModel()->AppendWebContents(std::move(contents), true);
EXPECT_EQ(GetActiveWebContents(), raw_contents);
content::WebContentsTester* web_contents_tester =
content::WebContentsTester::For(raw_contents);
web_contents_tester->NavigateAndCommit(GURL("http://www.example.com"));
CreateSessionServiceTabHelper(raw_contents);
int tab_id = sessions::SessionTabHelper::IdForTab(raw_contents).id();
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);
}
// Test that the tabs.move() function correctly rearranges sets of tabs within a
// single window.
TEST_F(TabsApiUnitTest, TabsMoveWithinWindow) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("MoveWithinWindowTest").Build();
// Add several web contents to the browser and get their tab IDs.
constexpr int kNumTabs = 5;
std::vector<int> tab_ids;
std::vector<content::WebContents*> web_contentses;
for (int i = 0; i < kNumTabs; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
tab_ids.push_back(
sessions::SessionTabHelper::IdForTab(contents.get()).id());
web_contentses.push_back(contents.get());
GetTabStripModel()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
ASSERT_EQ(kNumTabs, GetTabStripModel()->count());
// Use the TabsMoveFunction to move tabs 0, 2, and 4 to index 1.
auto function = base::MakeRefCounted<TabsMoveFunction>();
function->set_extension(extension);
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(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
TabStripModel* tab_strip_model = GetTabStripModel();
EXPECT_EQ(tab_strip_model->GetWebContentsAt(0), web_contentses[1]);
EXPECT_EQ(tab_strip_model->GetWebContentsAt(1), web_contentses[0]);
EXPECT_EQ(tab_strip_model->GetWebContentsAt(2), web_contentses[2]);
EXPECT_EQ(tab_strip_model->GetWebContentsAt(3), web_contentses[4]);
EXPECT_EQ(tab_strip_model->GetWebContentsAt(4), web_contentses[3]);
}
// Test that the tabs.move() function correctly rearranges sets of tabs across
// windows.
TEST_F(TabsApiUnitTest, TabsMoveAcrossWindows) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("MoveAcrossWindowTest").Build();
// Add several web contents to the original browser and get their tab IDs.
constexpr int kNumTabs = 5;
std::vector<int> tab_ids;
std::vector<content::WebContents*> web_contentses;
for (int i = 0; i < kNumTabs; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
tab_ids.push_back(
sessions::SessionTabHelper::IdForTab(contents.get()).id());
web_contentses.push_back(contents.get());
GetTabStripModel()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
ASSERT_EQ(kNumTabs, GetTabStripModel()->count());
// Create a new window and add a few tabs, getting the ID of the last tab.
auto window2 = std::make_unique<TestBrowserWindow>();
Browser::CreateParams params(profile(), /* user_gesture */ true);
params.type = Browser::TYPE_NORMAL;
params.window = window2.release();
auto browser2 = Browser::DeprecatedCreateOwnedForTesting(params);
BrowserList::SetLastActive(browser2.get());
int window_id2 = ExtensionTabUtil::GetWindowId(browser2.get());
constexpr int kNumTabs2 = 3;
for (int i = 0; i < kNumTabs2; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
browser2->tab_strip_model()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
ASSERT_EQ(kNumTabs2, browser2->tab_strip_model()->count());
content::WebContents* web_contents2 =
browser2->tab_strip_model()->GetWebContentsAt(2);
int tab_id2 = sessions::SessionTabHelper::IdForTab(web_contents2).id();
// Use the TabsMoveFunction to move tab 2 from browser2 and tabs 0, 2, and 4
// from the original browser to index 1 of browser2.
constexpr int kNumTabsMovedAcrossWindows = 3;
auto function = base::MakeRefCounted<TabsMoveFunction>();
function->set_extension(extension);
constexpr char kFormatArgs[] =
R"([[%d, %d, %d, %d], {"windowId": %d, "index": 1}])";
const std::string args = base::StringPrintf(
kFormatArgs, tab_id2, tab_ids[0], tab_ids[2], tab_ids[4], window_id2);
ASSERT_TRUE(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
TabStripModel* tab_strip_model2 = browser2->tab_strip_model();
ASSERT_EQ(kNumTabs2 + kNumTabsMovedAcrossWindows, tab_strip_model2->count());
EXPECT_EQ(tab_strip_model2->GetWebContentsAt(1), web_contents2);
EXPECT_EQ(tab_strip_model2->GetWebContentsAt(2), web_contentses[0]);
EXPECT_EQ(tab_strip_model2->GetWebContentsAt(3), web_contentses[2]);
EXPECT_EQ(tab_strip_model2->GetWebContentsAt(4), web_contentses[4]);
// Clean up.
browser2->tab_strip_model()->CloseAllTabs();
}
TEST_F(TabsApiUnitTest, TabsMoveAcrossWindowsShouldRespectGroupContiguity) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("MoveAcrossWindowWithInvalidIndexTest").Build();
// Add several web contents to the original browser and get their tab IDs.
constexpr int kNumTabs = 5;
std::vector<int> tab_ids;
std::vector<content::WebContents*> web_contentses;
for (int i = 0; i < kNumTabs; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
tab_ids.push_back(
sessions::SessionTabHelper::IdForTab(contents.get()).id());
web_contentses.push_back(contents.get());
GetTabStripModel()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
ASSERT_EQ(kNumTabs, GetTabStripModel()->count());
// Create a new window and add a few tabs, getting the ID of the last tab.
auto window2 = std::make_unique<TestBrowserWindow>();
Browser::CreateParams params(profile(), /* user_gesture */ true);
params.type = Browser::TYPE_NORMAL;
params.window = window2.release();
auto browser2 = Browser::DeprecatedCreateOwnedForTesting(params);
BrowserList::SetLastActive(browser2.get());
int window_id2 = ExtensionTabUtil::GetWindowId(browser2.get());
constexpr int kNumTabs2 = 3;
for (int i = 0; i < kNumTabs2; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
browser2->tab_strip_model()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
browser2->tab_strip_model()->AddToNewGroup({0, 1});
ASSERT_EQ(kNumTabs2, browser2->tab_strip_model()->count());
content::WebContents* web_contents2 = GetTabStripModel()->GetWebContentsAt(2);
int tab_extension_id =
sessions::SessionTabHelper::IdForTab(web_contents2).id();
// Use the TabsMoveFunction to move tab at index 2 from browser2 to the middle
// of a group in browser1.
auto function = base::MakeRefCounted<TabsMoveFunction>();
function->set_extension(extension);
constexpr char kFormatArgs[] = R"([[%d], {"windowId": %d, "index": 1}])";
const std::string args =
base::StringPrintf(kFormatArgs, tab_extension_id, window_id2);
std::string error = api_test_utils::RunFunctionAndReturnError(
function.get(), args, profile(), api_test_utils::FunctionMode::kNone);
EXPECT_EQ(tabs_constants::kInvalidTabIndexBreaksGroupContiguity, error);
// Clean up.
browser2->tab_strip_model()->CloseAllTabs();
}
// Tests that calling chrome.tabs.move doesn't move a saved tab.
TEST_F(TabsApiUnitTest, TabsMoveSavedTabGroupTabAllowed) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("MoveWithinWindowTest").Build();
// Add several web contents to the browser and get their tab IDs.
constexpr int kNumTabs = 5;
std::vector<int> tab_ids;
std::vector<content::WebContents*> web_contentses;
for (int i = 0; i < kNumTabs; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
tab_ids.push_back(
sessions::SessionTabHelper::IdForTab(contents.get()).id());
web_contentses.push_back(contents.get());
GetTabStripModel()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
ASSERT_EQ(kNumTabs, GetTabStripModel()->count());
tab_groups::TabGroupSyncService* saved_service = sync_service();
ASSERT_TRUE(saved_service);
// Group the tab and save it.
tab_groups::TabGroupId group = GetTabStripModel()->AddToNewGroup({0, 1, 2});
tab_groups::TabGroupVisualData visual_data(
u"Initial title", tab_groups::TabGroupColorId::kBlue);
browser()->tab_strip_model()->ChangeTabGroupVisuals(group, visual_data);
// Use the TabsUpdateFunction to navigate to chromium.org
int tab_extension_id = sessions::SessionTabHelper::IdForTab(
GetTabStripModel()->GetWebContentsAt(0))
.id();
auto function = base::MakeRefCounted<TabsMoveFunction>();
function->set_extension(extension);
constexpr char kFormatArgs[] = R"([[%d], {"index": 1}])";
const std::string args = base::StringPrintf(kFormatArgs, tab_extension_id);
EXPECT_TRUE(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
EXPECT_EQ(GetTabStripModel()->GetWebContentsAt(0), web_contentses[1]);
EXPECT_EQ(GetTabStripModel()->GetWebContentsAt(1), web_contentses[0]);
}
// Test that the tabs.group() function correctly rearranges sets of tabs within
// a single window before grouping.
TEST_F(TabsApiUnitTest, TabsGroupWithinWindow) {
ASSERT_TRUE(GetTabStripModel()->SupportsTabGroups());
scoped_refptr<const Extension> extension =
ExtensionBuilder("GroupWithinWindowTest").Build();
// Add several web contents to the browser and get their tab IDs.
constexpr int kNumTabs = 5;
std::vector<int> tab_ids;
std::vector<content::WebContents*> web_contentses;
for (int i = 0; i < kNumTabs; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
tab_ids.push_back(
sessions::SessionTabHelper::IdForTab(contents.get()).id());
web_contentses.push_back(contents.get());
GetTabStripModel()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
ASSERT_EQ(kNumTabs, GetTabStripModel()->count());
// Use the TabsGroupFunction to group tabs 0, 2, and 4.
auto function = base::MakeRefCounted<TabsGroupFunction>();
function->set_extension(extension);
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(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
TabStripModel* tab_strip_model = GetTabStripModel();
EXPECT_EQ(tab_strip_model->GetWebContentsAt(0), web_contentses[0]);
EXPECT_EQ(tab_strip_model->GetWebContentsAt(1), web_contentses[2]);
EXPECT_EQ(tab_strip_model->GetWebContentsAt(2), web_contentses[4]);
EXPECT_EQ(tab_strip_model->GetWebContentsAt(3), web_contentses[1]);
EXPECT_EQ(tab_strip_model->GetWebContentsAt(4), web_contentses[3]);
std::optional<tab_groups::TabGroupId> group =
tab_strip_model->GetTabGroupForTab(0);
EXPECT_TRUE(group.has_value());
EXPECT_EQ(group, tab_strip_model->GetTabGroupForTab(1));
EXPECT_EQ(group, tab_strip_model->GetTabGroupForTab(2));
EXPECT_FALSE(tab_strip_model->GetTabGroupForTab(3));
EXPECT_FALSE(tab_strip_model->GetTabGroupForTab(4));
}
// Test that the tabs.group() function correctly groups tabs even when given
// out-of-order or duplicate tab IDs.
TEST_F(TabsApiUnitTest, TabsGroupMixedTabIds) {
ASSERT_TRUE(GetTabStripModel()->SupportsTabGroups());
scoped_refptr<const Extension> extension =
ExtensionBuilder("GroupMixedTabIdsTest").Build();
// Add several web contents to the browser and get their tab IDs.
constexpr int kNumTabs = 5;
std::vector<int> tab_ids;
std::vector<content::WebContents*> web_contentses;
for (int i = 0; i < kNumTabs; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
tab_ids.push_back(
sessions::SessionTabHelper::IdForTab(contents.get()).id());
web_contentses.push_back(contents.get());
GetTabStripModel()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
ASSERT_EQ(kNumTabs, GetTabStripModel()->count());
// Use the TabsGroupFunction to group tab 1 twice, along with tabs 3 and 2.
auto function = base::MakeRefCounted<TabsGroupFunction>();
function->set_extension(extension);
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(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
TabStripModel* tab_strip_model = GetTabStripModel();
EXPECT_EQ(tab_strip_model->GetWebContentsAt(0), web_contentses[0]);
EXPECT_EQ(tab_strip_model->GetWebContentsAt(1), web_contentses[1]);
EXPECT_EQ(tab_strip_model->GetWebContentsAt(2), web_contentses[2]);
EXPECT_EQ(tab_strip_model->GetWebContentsAt(3), web_contentses[3]);
EXPECT_EQ(tab_strip_model->GetWebContentsAt(4), web_contentses[4]);
std::optional<tab_groups::TabGroupId> group =
tab_strip_model->GetTabGroupForTab(1);
EXPECT_TRUE(group.has_value());
EXPECT_FALSE(tab_strip_model->GetTabGroupForTab(0));
EXPECT_EQ(group, tab_strip_model->GetTabGroupForTab(1));
EXPECT_EQ(group, tab_strip_model->GetTabGroupForTab(2));
EXPECT_EQ(group, tab_strip_model->GetTabGroupForTab(3));
EXPECT_FALSE(tab_strip_model->GetTabGroupForTab(4));
}
// Test that the tabs.group() function throws an error if both createProperties
// and groupId are specified.
TEST_F(TabsApiUnitTest, TabsGroupParamsError) {
ASSERT_TRUE(GetTabStripModel()->SupportsTabGroups());
scoped_refptr<const Extension> extension =
ExtensionBuilder("GroupParamsErrorTest").Build();
// Add several web contents to the browser and get their tab IDs.
constexpr int kNumTabs = 5;
std::vector<int> tab_ids;
std::vector<content::WebContents*> web_contentses;
for (int i = 0; i < kNumTabs; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
tab_ids.push_back(
sessions::SessionTabHelper::IdForTab(contents.get()).id());
web_contentses.push_back(contents.get());
GetTabStripModel()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
ASSERT_EQ(kNumTabs, GetTabStripModel()->count());
// Add a tab to a group to have an existing group ID.
tab_groups::TabGroupId group = GetTabStripModel()->AddToNewGroup({1});
int group_id = ExtensionTabUtil::GetGroupId(group);
// Attempt to specify both createProperties and groupId.
auto function = base::MakeRefCounted<TabsGroupFunction>();
function->set_extension(extension);
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 = api_test_utils::RunFunctionAndReturnError(
function.get(), args, profile(), api_test_utils::FunctionMode::kNone);
EXPECT_EQ(tabs_constants::kGroupParamsError, error);
}
// Test that the tabs.group() function correctly rearranges sets of tabs across
// windows before grouping.
TEST_F(TabsApiUnitTest, TabsGroupAcrossWindows) {
ASSERT_TRUE(GetTabStripModel()->SupportsTabGroups());
scoped_refptr<const Extension> extension =
ExtensionBuilder("GroupAcrossWindowsTest").Build();
// Add several web contents to the original browser and get their tab IDs.
constexpr int kNumTabs = 5;
std::vector<int> tab_ids;
std::vector<content::WebContents*> web_contentses;
for (int i = 0; i < kNumTabs; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
tab_ids.push_back(
sessions::SessionTabHelper::IdForTab(contents.get()).id());
web_contentses.push_back(contents.get());
GetTabStripModel()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
ASSERT_EQ(kNumTabs, GetTabStripModel()->count());
// Create a new window and add a few tabs, adding one to a group.
auto window2 = std::make_unique<TestBrowserWindow>();
Browser::CreateParams params(profile(), /* user_gesture */ true);
params.type = Browser::TYPE_NORMAL;
params.window = window2.release();
auto browser2 = Browser::DeprecatedCreateOwnedForTesting(params);
constexpr int kNumTabs2 = 3;
for (int i = 0; i < kNumTabs2; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
browser2->tab_strip_model()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
ASSERT_EQ(kNumTabs2, browser2->tab_strip_model()->count());
tab_groups::TabGroupId group2 =
browser2->tab_strip_model()->AddToNewGroup({1});
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 browser2.
constexpr int kNumTabsMovedAcrossWindows = 3;
auto function = base::MakeRefCounted<TabsGroupFunction>();
function->set_extension(extension);
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(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
TabStripModel* tab_strip_model2 = browser2->tab_strip_model();
ASSERT_EQ(kNumTabs2 + kNumTabsMovedAcrossWindows, tab_strip_model2->count());
EXPECT_EQ(tab_strip_model2->GetWebContentsAt(2), web_contentses[0]);
EXPECT_EQ(tab_strip_model2->GetWebContentsAt(3), web_contentses[2]);
EXPECT_EQ(tab_strip_model2->GetWebContentsAt(4), web_contentses[4]);
EXPECT_EQ(group2, tab_strip_model2->GetTabGroupForTab(1).value());
EXPECT_EQ(group2, tab_strip_model2->GetTabGroupForTab(2).value());
EXPECT_EQ(group2, tab_strip_model2->GetTabGroupForTab(3).value());
EXPECT_EQ(group2, tab_strip_model2->GetTabGroupForTab(4).value());
// Clean up.
browser2->tab_strip_model()->CloseAllTabs();
}
// Test that grouping tabs that are in a saved group should fail.
TEST_F(TabsApiUnitTest, TabsGroupForSavedTabGroupTab) {
ASSERT_TRUE(GetTabStripModel()->SupportsTabGroups());
scoped_refptr<const Extension> extension =
ExtensionBuilder("GroupWithinWindowTest").Build();
// create 2 tabs
std::vector<int> tab_ids;
for (int i = 0; i < 2; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
tab_ids.push_back(
sessions::SessionTabHelper::IdForTab(contents.get()).id());
GetTabStripModel()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
// group the first tab. make sure its saved.
tab_groups::TabGroupId old_group = GetTabStripModel()->AddToNewGroup({0});
// with extensions group the 2 tabs into a new group.
auto function = base::MakeRefCounted<TabsGroupFunction>();
function->set_extension(extension);
constexpr char kFormatArgs[] = R"([{"tabIds": [%d, %d]}])";
const std::string args =
base::StringPrintf(kFormatArgs, tab_ids[0], tab_ids[1]);
EXPECT_TRUE(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
// make sure the new group exists and is different than the old group.
EXPECT_TRUE(GetTabStripModel()->GetTabGroupForTab(0).has_value());
EXPECT_NE(old_group, GetTabStripModel()->GetTabGroupForTab(0).value());
EXPECT_EQ(GetTabStripModel()->GetTabGroupForTab(0),
GetTabStripModel()->GetTabGroupForTab(1));
}
// Test that the tabs.ungroup() function correctly ungroups tabs from a single
// group and deletes it.
TEST_F(TabsApiUnitTest, TabsUngroupSingleGroup) {
ASSERT_TRUE(GetTabStripModel()->SupportsTabGroups());
scoped_refptr<const Extension> extension =
ExtensionBuilder("UngroupSingleGroupTest").Build();
// Add several web contents to the browser and get their tab IDs.
constexpr int kNumTabs = 5;
std::vector<int> tab_ids;
std::vector<content::WebContents*> web_contentses;
for (int i = 0; i < kNumTabs; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
tab_ids.push_back(
sessions::SessionTabHelper::IdForTab(contents.get()).id());
web_contentses.push_back(contents.get());
GetTabStripModel()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
ASSERT_EQ(kNumTabs, GetTabStripModel()->count());
// Add tabs 1, 2, and 3 to a group.
tab_groups::TabGroupId group = GetTabStripModel()->AddToNewGroup({1, 2, 3});
// Use the TabsUngroupFunction to ungroup tabs 1, 2, and 3.
auto function = base::MakeRefCounted<TabsUngroupFunction>();
function->set_extension(extension);
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(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
// Expect the group to be deleted because all tabs were ungrouped from it.
TabStripModel* tab_strip_model = GetTabStripModel();
EXPECT_FALSE(tab_strip_model->GetTabGroupForTab(1));
EXPECT_FALSE(tab_strip_model->GetTabGroupForTab(2));
EXPECT_FALSE(tab_strip_model->GetTabGroupForTab(3));
EXPECT_FALSE(tab_strip_model->group_model()->ContainsTabGroup(group));
}
// Saved groups should be ungroupable from extensions.
TEST_F(TabsApiUnitTest, TabsUngroupSingleGroupForSavedTabGroup) {
ASSERT_TRUE(GetTabStripModel()->SupportsTabGroups());
scoped_refptr<const Extension> extension =
ExtensionBuilder("UngroupSingleGroupTest").Build();
int tab_id;
{
std::unique_ptr<content::WebContents> web_contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(web_contents.get());
tab_id = sessions::SessionTabHelper::IdForTab(web_contents.get()).id();
GetTabStripModel()->AppendWebContents(std::move(web_contents),
/*foreground=*/true);
}
ASSERT_EQ(1, GetTabStripModel()->count());
tab_groups::TabGroupSyncService* saved_service = sync_service();
ASSERT_TRUE(saved_service);
// Group the tab and save it.
tab_groups::TabGroupId group = GetTabStripModel()->AddToNewGroup({0});
tab_groups::TabGroupVisualData visual_data(
u"Initial title", tab_groups::TabGroupColorId::kBlue);
browser()->tab_strip_model()->ChangeTabGroupVisuals(group, visual_data);
auto function = base::MakeRefCounted<TabsUngroupFunction>();
function->set_extension(extension);
constexpr char kFormatArgs[] = R"([[%d]])";
const std::string args = base::StringPrintf(kFormatArgs, tab_id);
EXPECT_TRUE(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
// The tab should no longer be in the group.
TabStripModel* tab_strip_model = GetTabStripModel();
EXPECT_EQ(std::nullopt, tab_strip_model->GetTabGroupForTab(0));
}
// Test that the tabs.ungroup() function correctly ungroups tabs from several
// different groups and deletes any empty ones.
TEST_F(TabsApiUnitTest, TabsUngroupFromMultipleGroups) {
ASSERT_TRUE(GetTabStripModel()->SupportsTabGroups());
TabStripModel* tab_strip_model = GetTabStripModel();
scoped_refptr<const Extension> extension =
ExtensionBuilder("UngroupFromMultipleGroupsTest").Build();
// Add several web contents to the browser and get their tab IDs.
constexpr int kNumTabs = 5;
std::vector<int> tab_ids;
std::vector<content::WebContents*> web_contentses;
for (int i = 0; i < kNumTabs; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
tab_ids.push_back(
sessions::SessionTabHelper::IdForTab(contents.get()).id());
web_contentses.push_back(contents.get());
tab_strip_model->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
ASSERT_EQ(kNumTabs, tab_strip_model->count());
// Add tabs 1, 2, and 3 to a group1, and tab 4 to group2.
tab_groups::TabGroupId group1 = tab_strip_model->AddToNewGroup({1, 2, 3});
tab_groups::TabGroupId group2 = tab_strip_model->AddToNewGroup({4});
// Use the TabsUngroupFunction to ungroup tabs 2, 3, and 4.
auto function = base::MakeRefCounted<TabsUngroupFunction>();
function->set_extension(extension);
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(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
// Expect group2 to be deleted because all tabs were ungrouped from it.
EXPECT_EQ(group1, tab_strip_model->GetTabGroupForTab(1));
EXPECT_FALSE(tab_strip_model->GetTabGroupForTab(2));
EXPECT_FALSE(tab_strip_model->GetTabGroupForTab(3));
EXPECT_FALSE(tab_strip_model->GetTabGroupForTab(4));
EXPECT_TRUE(tab_strip_model->group_model()->ContainsTabGroup(group1));
EXPECT_FALSE(tab_strip_model->group_model()->ContainsTabGroup(group2));
}
TEST_F(TabsApiUnitTest, TabsGoForwardNoSelectedTabError) {
scoped_refptr<const Extension> extension = CreateTabsExtension();
auto function = base::MakeRefCounted<TabsGoForwardFunction>();
function->set_extension(extension);
// No active tab results in an error.
std::string error = api_test_utils::RunFunctionAndReturnError(
function.get(), "[]",
profile(), // profile() doesn't have any tabs.
api_test_utils::FunctionMode::kNone);
EXPECT_EQ(tabs_constants::kNoSelectedTabError, error);
}
TEST_F(TabsApiUnitTest, TabsGoForwardAndBack) {
scoped_refptr<const Extension> extension_with_tabs_permission =
CreateTabsExtension();
const std::vector<GURL> urls = {GURL("http://www.foo.com"),
GURL("http://www.bar.com")};
content::WebContents* web_contents = CreateAndAppendWebContentsWithHistory(
profile(), GetTabStripModel(), urls);
ASSERT_TRUE(web_contents);
CreateSessionServiceTabHelper(web_contents);
const int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
// Go back with chrome.tabs.goBack.
auto goback_function = base::MakeRefCounted<TabsGoBackFunction>();
goback_function->set_extension(extension_with_tabs_permission.get());
api_test_utils::RunFunction(goback_function.get(),
base::StringPrintf("[%d]", tab_id), profile(),
api_test_utils::FunctionMode::kIncognito);
content::WebContents* active_webcontent = GetActiveWebContents();
content::NavigationController& controller =
active_webcontent->GetController();
ASSERT_TRUE(CommitPendingLoadForController(controller));
EXPECT_EQ(urls[0], web_contents->GetLastCommittedURL());
EXPECT_EQ(urls[0], web_contents->GetVisibleURL());
EXPECT_TRUE(ui::PAGE_TRANSITION_FORWARD_BACK &
controller.GetLastCommittedEntry()->GetTransitionType());
// Go forward with chrome.tabs.goForward.
auto goforward_function = base::MakeRefCounted<TabsGoForwardFunction>();
goforward_function->set_extension(extension_with_tabs_permission.get());
api_test_utils::RunFunction(goforward_function.get(),
base::StringPrintf("[%d]", tab_id), profile(),
api_test_utils::FunctionMode::kIncognito);
ASSERT_TRUE(CommitPendingLoadForController(controller));
EXPECT_EQ(urls[1], web_contents->GetLastCommittedURL());
EXPECT_EQ(urls[1], web_contents->GetVisibleURL());
EXPECT_TRUE(ui::PAGE_TRANSITION_FORWARD_BACK &
controller.GetLastCommittedEntry()->GetTransitionType());
// If there's no next page, chrome.tabs.goForward should return an error.
auto goforward_function2 = base::MakeRefCounted<TabsGoForwardFunction>();
goforward_function2->set_extension(extension_with_tabs_permission.get());
std::string error = api_test_utils::RunFunctionAndReturnError(
goforward_function2.get(), base::StringPrintf("[%d]", tab_id), profile(),
api_test_utils::FunctionMode::kNone);
EXPECT_EQ(tabs_constants::kNotFoundNextPageError, error);
EXPECT_EQ(urls[1], web_contents->GetLastCommittedURL());
EXPECT_EQ(urls[1], web_contents->GetVisibleURL());
}
TEST_F(TabsApiUnitTest, TabsGoForwardAndBackSavedTabGroupTab) {
scoped_refptr<const Extension> extension_with_tabs_permission =
CreateTabsExtension();
const std::vector<GURL> urls = {GURL("http://www.foo.com"),
GURL("http://www.bar.com"),
GURL("http://www.baz.com")};
content::WebContents* web_contents = CreateAndAppendWebContentsWithHistory(
profile(), GetTabStripModel(), urls);
ASSERT_TRUE(web_contents);
CreateSessionServiceTabHelper(web_contents);
const int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
{
// Go back with chrome.tabs.goBack.
auto goback_function = base::MakeRefCounted<TabsGoBackFunction>();
goback_function->set_extension(extension_with_tabs_permission.get());
api_test_utils::RunFunction(goback_function.get(),
base::StringPrintf("[%d]", tab_id), profile(),
api_test_utils::FunctionMode::kIncognito);
ASSERT_TRUE(CommitPendingLoadForController(web_contents->GetController()));
}
EXPECT_EQ(urls[1], web_contents->GetLastCommittedURL());
EXPECT_EQ(urls[1], web_contents->GetVisibleURL());
tab_groups::TabGroupSyncService* saved_service = sync_service();
ASSERT_TRUE(saved_service);
// Save the tab and expect that it can not be navigated forwards or backwards.
tab_groups::TabGroupId group = GetTabStripModel()->AddToNewGroup(
{GetTabStripModel()->GetIndexOfWebContents(web_contents)});
tab_groups::TabGroupVisualData visual_data(
u"Initial title", tab_groups::TabGroupColorId::kBlue);
browser()->tab_strip_model()->ChangeTabGroupVisuals(group, visual_data);
{
auto goback_function = base::MakeRefCounted<TabsGoBackFunction>();
goback_function->set_extension(extension_with_tabs_permission.get());
EXPECT_TRUE(api_test_utils::RunFunction(
goback_function.get(), base::StringPrintf("[%d]", tab_id), profile(),
api_test_utils::FunctionMode::kNone));
}
{
auto goforward_function = base::MakeRefCounted<TabsGoForwardFunction>();
goforward_function->set_extension(extension_with_tabs_permission.get());
EXPECT_TRUE(api_test_utils::RunFunction(
goforward_function.get(), base::StringPrintf("[%d]", tab_id), profile(),
api_test_utils::FunctionMode::kNone));
}
EXPECT_EQ(urls[1], web_contents->GetLastCommittedURL());
EXPECT_EQ(urls[1], web_contents->GetVisibleURL());
}
TEST_F(TabsApiUnitTest, TabsGoForwardAndBackWithoutTabId) {
scoped_refptr<const Extension> extension_with_tabs_permission =
CreateTabsExtension();
TabStripModel* tab_strip_model = GetTabStripModel();
// Create first tab with history.
const std::vector<GURL> tab1_urls = {GURL("http://www.foo.com"),
GURL("http://www.bar.com")};
content::WebContents* tab1_webcontents =
CreateAndAppendWebContentsWithHistory(profile(), tab_strip_model,
tab1_urls);
ASSERT_TRUE(tab1_webcontents);
EXPECT_EQ(tab1_urls[1], tab1_webcontents->GetLastCommittedURL());
EXPECT_EQ(tab1_urls[1], tab1_webcontents->GetVisibleURL());
const int tab1_index =
tab_strip_model->GetIndexOfWebContents(tab1_webcontents);
// Create second tab with history.
const std::vector<GURL> tab2_urls = {GURL("http://www.chrome.com"),
GURL("http://www.google.com")};
content::WebContents* tab2_webcontents =
CreateAndAppendWebContentsWithHistory(profile(), tab_strip_model,
tab2_urls);
ASSERT_TRUE(tab2_webcontents);
EXPECT_EQ(tab2_urls[1], tab2_webcontents->GetLastCommittedURL());
EXPECT_EQ(tab2_urls[1], tab2_webcontents->GetVisibleURL());
const int tab2_index =
tab_strip_model->GetIndexOfWebContents(tab2_webcontents);
ASSERT_EQ(2, tab_strip_model->count());
// Activate first tab.
tab_strip_model->ActivateTabAt(
tab1_index, TabStripUserGestureDetails(
TabStripUserGestureDetails::GestureType::kOther));
// 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());
api_test_utils::RunFunction(goback_function.get(), "[]", profile(),
api_test_utils::FunctionMode::kIncognito);
content::NavigationController& controller = tab1_webcontents->GetController();
ASSERT_TRUE(CommitPendingLoadForController(controller));
EXPECT_EQ(tab1_urls[0], tab1_webcontents->GetLastCommittedURL());
EXPECT_EQ(tab1_urls[0], tab1_webcontents->GetVisibleURL());
EXPECT_TRUE(ui::PAGE_TRANSITION_FORWARD_BACK &
controller.GetLastCommittedEntry()->GetTransitionType());
// Go forward without tab_id.
auto goforward_function = base::MakeRefCounted<TabsGoForwardFunction>();
goforward_function->set_extension(extension_with_tabs_permission.get());
api_test_utils::RunFunction(goforward_function.get(), "[]", profile(),
api_test_utils::FunctionMode::kIncognito);
ASSERT_TRUE(CommitPendingLoadForController(controller));
EXPECT_EQ(tab1_urls[1], tab1_webcontents->GetLastCommittedURL());
EXPECT_EQ(tab1_urls[1], tab1_webcontents->GetVisibleURL());
EXPECT_TRUE(ui::PAGE_TRANSITION_FORWARD_BACK &
controller.GetLastCommittedEntry()->GetTransitionType());
// Activate second tab.
tab_strip_model->ActivateTabAt(
tab2_index, TabStripUserGestureDetails(
TabStripUserGestureDetails::GestureType::kOther));
auto goback_function2 = base::MakeRefCounted<TabsGoBackFunction>();
goback_function2->set_extension(extension_with_tabs_permission.get());
api_test_utils::RunFunction(goback_function2.get(), "[]", profile(),
api_test_utils::FunctionMode::kIncognito);
content::NavigationController& controller2 =
tab2_webcontents->GetController();
ASSERT_TRUE(CommitPendingLoadForController(controller2));
EXPECT_EQ(tab2_urls[0], tab2_webcontents->GetLastCommittedURL());
EXPECT_EQ(tab2_urls[0], tab2_webcontents->GetVisibleURL());
EXPECT_TRUE(ui::PAGE_TRANSITION_FORWARD_BACK &
controller2.GetLastCommittedEntry()->GetTransitionType());
}
#if BUILDFLAG(IS_CHROMEOS)
// Ensure tabs.captureVisibleTab respects any Data Leak Prevention restrictions.
TEST_F(TabsApiUnitTest, 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.
std::unique_ptr<content::WebContents> web_contents =
content::WebContentsTester::CreateTestWebContents(profile(), nullptr);
content::WebContentsTester* web_contents_tester =
content::WebContentsTester::For(web_contents.get());
const GURL kGoogle("http://www.google.com");
GetTabStripModel()->AppendWebContents(std::move(web_contents),
/*foreground=*/true);
web_contents_tester->NavigateAndCommit(kGoogle);
// 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 = api_test_utils::RunFunctionAndReturnError(
function.get(), "[{}]", profile(), api_test_utils::FunctionMode::kNone);
EXPECT_EQ(tabs_constants::kScreenshotsDisabledByDlp, error);
}
#endif // BUILDFLAG(IS_CHROMEOS)
#if BUILDFLAG(IS_CHROMEOS)
TEST_F(TabsApiUnitTest, DontCreateTabsInLockedFullscreenMode) {
scoped_refptr<const Extension> extension_with_tabs_permission =
CreateTabsExtension();
ash::TestWindowBuilder builder;
std::unique_ptr<aura::Window> window =
builder.SetTestWindowDelegate().AllowAllWindowStates().Build();
browser_window()->SetNativeWindow(window.get());
auto function = base::MakeRefCounted<TabsCreateFunction>();
function->set_extension(extension_with_tabs_permission.get());
// In locked fullscreen mode we should not be able to create any tabs.
ash::PinWindow(browser_window()->GetNativeWindow(), /*trusted=*/true);
EXPECT_EQ(ExtensionTabUtil::kLockedFullscreenModeNewTabError,
api_test_utils::RunFunctionAndReturnError(
function.get(), "[{}]", profile(),
api_test_utils::FunctionMode::kNone));
}
// Screenshot should return an error when disabled in user profile preferences.
TEST_F(TabsApiUnitTest, 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.
std::unique_ptr<content::WebContents> web_contents =
content::WebContentsTester::CreateTestWebContents(profile(), nullptr);
content::WebContentsTester* web_contents_tester =
content::WebContentsTester::For(web_contents.get());
const GURL kGoogle("http://www.google.com");
GetTabStripModel()->AppendWebContents(std::move(web_contents),
/*foreground=*/true);
web_contents_tester->NavigateAndCommit(kGoogle);
// Disable screenshot.
profile()->GetPrefs()->SetBoolean(prefs::kDisableScreenshots, true);
// Run the function and check result.
std::string error = api_test_utils::RunFunctionAndReturnError(
function.get(), "[{}]", profile(), api_test_utils::FunctionMode::kNone);
EXPECT_EQ(tabs_constants::kScreenshotsDisabled, error);
}
#endif // BUILDFLAG(IS_CHROMEOS)
TEST_F(TabsApiUnitTest, CannotDuplicatePictureInPictureWindows) {
// Create picture-in-picture browser.
auto pip_window = std::make_unique<TestBrowserWindow>();
Browser::CreateParams params(profile(), true);
params.type = Browser::TYPE_PICTURE_IN_PICTURE;
params.window = pip_window.release();
std::unique_ptr<Browser> pip_browser;
pip_browser = Browser::DeprecatedCreateOwnedForTesting(params);
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
int pip_tab_id = sessions::SessionTabHelper::IdForTab(contents.get()).id();
pip_browser->tab_strip_model()->AppendWebContents(std::move(contents),
/*foreground=*/true);
// 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>();
auto extension = CreateTabsExtension();
function->set_extension(extension);
std::string args = base::StringPrintf("[%d]", pip_tab_id);
std::string error = api_test_utils::RunFunctionAndReturnError(
function.get(), args, pip_browser->profile(),
api_test_utils::FunctionMode::kNone);
EXPECT_EQ(ErrorUtils::FormatErrorMessage(tabs_constants::kCannotDuplicateTab,
base::NumberToString(pip_tab_id)),
error);
// Tear down picture-in-picture browser.
pip_browser->tab_strip_model()->DetachAndDeleteWebContentsAt(0);
pip_browser.reset();
}
// Tests that calling chrome.tabs.discard discards the tab.
TEST_F(TabsApiUnitTest, TabsDiscard) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("DiscardTest").Build();
const GURL kExampleCom("http://example.com");
// Add a web contents to the browser.
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
content::WebContents* web_contents = contents.get();
GetTabStripModel()->AppendWebContents(std::move(contents), true);
EXPECT_EQ(GetActiveWebContents(), web_contents);
CreateSessionServiceTabHelper(web_contents);
int index = GetTabStripModel()->GetIndexOfWebContents(web_contents);
int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
// Navigate the browser to example.com
content::WebContentsTester* web_contents_tester =
content::WebContentsTester::For(web_contents);
web_contents_tester->NavigateAndCommit(kExampleCom);
EXPECT_EQ(kExampleCom, web_contents->GetLastCommittedURL());
// Use the TabsDiscardFunction to discard the tab.
auto function = base::MakeRefCounted<TabsDiscardFunction>();
function->set_extension(extension);
static constexpr char kFormatArgs[] = R"([%d])";
const std::string args = base::StringPrintf(kFormatArgs, tab_id);
ASSERT_TRUE(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
// Check that the tab has discarded
content::WebContents* new_contents_at_index =
GetTabStripModel()->GetWebContentsAt(index);
EXPECT_TRUE(new_contents_at_index->WasDiscarded());
}
// Tests that calling chrome.tabs.discard on a saved tab does discard.
TEST_F(TabsApiUnitTest, TabsDiscardSavedTabGroupTabAllowed) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("DiscardTest").Build();
const GURL kExampleCom("http://example.com");
// Add a web contents to the browser.
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
content::WebContents* web_contents = contents.get();
GetTabStripModel()->AppendWebContents(std::move(contents), true);
EXPECT_EQ(GetActiveWebContents(), web_contents);
CreateSessionServiceTabHelper(web_contents);
int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
// Navigate the browser to example.com
content::WebContentsTester* web_contents_tester =
content::WebContentsTester::For(web_contents);
web_contents_tester->NavigateAndCommit(kExampleCom);
EXPECT_EQ(kExampleCom, web_contents->GetLastCommittedURL());
tab_groups::TabGroupSyncService* saved_service = sync_service();
ASSERT_TRUE(saved_service);
// Group the tab and save it.
tab_groups::TabGroupId group = GetTabStripModel()->AddToNewGroup(
{GetTabStripModel()->GetIndexOfWebContents(web_contents)});
tab_groups::TabGroupVisualData visual_data(
u"Initial title", tab_groups::TabGroupColorId::kBlue);
browser()->tab_strip_model()->ChangeTabGroupVisuals(group, visual_data);
auto function = base::MakeRefCounted<TabsDiscardFunction>();
function->set_extension(extension);
EXPECT_TRUE(api_test_utils::RunFunction(
function.get(), base::StringPrintf("[%d]", tab_id), profile(),
api_test_utils::FunctionMode::kNone));
// Check that the tab has discarded
content::WebContents* new_contents_at_index =
GetTabStripModel()->GetWebContentsAt(0);
EXPECT_TRUE(new_contents_at_index->WasDiscarded());
}
TEST_F(TabsApiUnitTest, SplitTabsWithHighlightFunction) {
// Add a couple of web contents to the browser and mark them as split.
for (int i = 0; i < /*numTabs=*/2; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
CreateSessionServiceTabHelper(contents.get());
GetTabStripModel()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
GetTabStripModel()->ActivateTabAt(0);
GetTabStripModel()->AddToNewSplit(
{1}, split_tabs::SplitTabVisualData(),
split_tabs::SplitTabCreatedSource::kLinkContextMenu);
// Run extension to highlight tabs
auto extension = CreateTabsExtension();
std::string args = base::StringPrintf("[{\"tabs\": [%d]}]", 0);
scoped_refptr<TabsHighlightFunction> function =
base::MakeRefCounted<TabsHighlightFunction>();
function->set_extension(extension);
ASSERT_TRUE(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
// Check that both sides of the split are selected.
ASSERT_TRUE(GetTabStripModel()->selection_model().IsSelected(0));
ASSERT_TRUE(GetTabStripModel()->selection_model().IsSelected(1));
}
#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.
TEST_F(TabsApiUnitTest,
TabsDiscardSavedTabGroupTabAllowedForLockedFullscreenPermission) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("DiscardTest")
.SetID("pmgljoohajacndjcjlajcopidgnhphcl")
.AddAPIPermission("lockWindowFullscreenPrivate")
.Build();
const GURL kExampleCom("http://example.com");
// Add a web contents to the browser.
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
content::WebContents* web_contents = contents.get();
GetTabStripModel()->AppendWebContents(std::move(contents), true);
EXPECT_EQ(GetActiveWebContents(), web_contents);
CreateSessionServiceTabHelper(web_contents);
int index = GetTabStripModel()->GetIndexOfWebContents(web_contents);
int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
// Navigate the browser to example.com
content::WebContentsTester* web_contents_tester =
content::WebContentsTester::For(web_contents);
web_contents_tester->NavigateAndCommit(kExampleCom);
EXPECT_EQ(kExampleCom, web_contents->GetLastCommittedURL());
tab_groups::TabGroupSyncService* saved_service = sync_service();
ASSERT_TRUE(saved_service);
// Group the tab and save it.
tab_groups::TabGroupId group = GetTabStripModel()->AddToNewGroup(
{GetTabStripModel()->GetIndexOfWebContents(web_contents)});
tab_groups::TabGroupVisualData visual_data(
u"Initial title", tab_groups::TabGroupColorId::kBlue);
browser()->tab_strip_model()->ChangeTabGroupVisuals(group, visual_data);
// The tab discard function should not fail.
auto function = base::MakeRefCounted<TabsDiscardFunction>();
function->set_extension(extension);
ASSERT_TRUE(api_test_utils::RunFunction(
function.get(), base::StringPrintf("[%d]", tab_id), profile(),
api_test_utils::FunctionMode::kNone));
// Check that the tab was discarded.
content::WebContents* new_contents_at_index =
GetTabStripModel()->GetWebContentsAt(index);
EXPECT_TRUE(new_contents_at_index->WasDiscarded());
}
#endif // BUILDFLAG(IS_CHROMEOS)
class TabsApiSideBySideUnitTest : public TabsApiUnitTest {
public:
TabsApiSideBySideUnitTest() {
scoped_feature_list_.InitAndEnableFeature(features::kSideBySide);
}
protected:
std::vector<content::WebContents*> CreateAndGetWebContents(int count) {
std::vector<int> tab_ids;
std::vector<content::WebContents*> web_contentses;
for (int i = 0; i < count; ++i) {
std::unique_ptr<content::WebContents> contents(
content::WebContentsTester::CreateTestWebContents(profile(),
nullptr));
CreateSessionServiceTabHelper(contents.get());
tab_ids.push_back(
sessions::SessionTabHelper::IdForTab(contents.get()).id());
web_contentses.push_back(contents.get());
GetTabStripModel()->AppendWebContents(std::move(contents),
/*foreground=*/true);
}
CHECK_EQ(count, GetTabStripModel()->count());
return web_contentses;
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Tests that calling chrome.tabs.move() works when a tab is moved within a
// split view.
TEST_F(TabsApiSideBySideUnitTest, TabsMoveWithinSplitView) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("TabsMoveWithinSplitView").Build();
// Add several web contents to the browser and get their tab IDs.
std::vector<content::WebContents*> web_contentses =
CreateAndGetWebContents(5);
// Create a split with tabs 3 and 4.
GetTabStripModel()->AddToNewSplit(
{3}, split_tabs::SplitTabVisualData(),
split_tabs::SplitTabCreatedSource::kTabContextMenu);
EXPECT_TRUE(GetTabStripModel()->GetSplitForTab(3).has_value());
EXPECT_TRUE(GetTabStripModel()->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_extension_id = sessions::SessionTabHelper::IdForTab(
GetTabStripModel()->GetWebContentsAt(0))
.id();
auto function = base::MakeRefCounted<TabsMoveFunction>();
function->set_extension(extension);
constexpr char kFormatArgs[] = R"([[%d], {"index": 3}])";
const std::string args = base::StringPrintf(kFormatArgs, tab_extension_id);
EXPECT_TRUE(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
EXPECT_EQ(ExtensionFunction::ResponseType::kSucceeded,
*function->response_type());
// 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(GetTabStripModel()->GetWebContentsAt(2), web_contentses[3]);
EXPECT_EQ(GetTabStripModel()->GetWebContentsAt(3), web_contentses[0]);
EXPECT_EQ(GetTabStripModel()->GetWebContentsAt(4), web_contentses[4]);
EXPECT_FALSE(GetTabStripModel()->GetSplitForTab(2).has_value());
EXPECT_FALSE(GetTabStripModel()->GetSplitForTab(4).has_value());
}
// Tests that calling chrome.tabs.move() works when a tab within a split view is
// moved.
TEST_F(TabsApiSideBySideUnitTest, TabsMoveFromSplitView) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("TabsMoveFromSplitView").Build();
// Add several web contents to the browser and get their tab IDs.
std::vector<content::WebContents*> web_contentses =
CreateAndGetWebContents(5);
// Create a split with tabs 3 and 4.
GetTabStripModel()->AddToNewSplit({3}, split_tabs::SplitTabVisualData(),
split_tabs::SplitTabCreatedSource());
EXPECT_TRUE(GetTabStripModel()->GetSplitForTab(3).has_value());
EXPECT_TRUE(GetTabStripModel()->GetSplitForTab(4).has_value());
// Use the TabsMoveFunction to move split tab at index 3 to index 0.
int tab_extension_id = sessions::SessionTabHelper::IdForTab(
GetTabStripModel()->GetWebContentsAt(3))
.id();
auto function = base::MakeRefCounted<TabsMoveFunction>();
function->set_extension(extension);
constexpr char kFormatArgs[] = R"([[%d], {"index": 0}])";
const std::string args = base::StringPrintf(kFormatArgs, tab_extension_id);
EXPECT_TRUE(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
EXPECT_EQ(ExtensionFunction::ResponseType::kSucceeded,
*function->response_type());
// Expect that the tab has been moved to index 0 and the original split view
// is removed.
EXPECT_EQ(GetTabStripModel()->GetWebContentsAt(0), web_contentses[3]);
EXPECT_EQ(GetTabStripModel()->GetWebContentsAt(1), web_contentses[0]);
EXPECT_FALSE(GetTabStripModel()->GetSplitForTab(0).has_value());
EXPECT_FALSE(GetTabStripModel()->GetSplitForTab(4).has_value());
}
// Tests that chrome.tabs.duplicate removes split view.
TEST_F(TabsApiSideBySideUnitTest, TabsDuplicateSplitView) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("TabsDuplicateSplitView").Build();
// Add a couple of web contents to the browser and mark them as split.
CreateAndGetWebContents(2);
GetTabStripModel()->ActivateTabAt(0);
GetTabStripModel()->AddToNewSplit({1}, split_tabs::SplitTabVisualData(),
split_tabs::SplitTabCreatedSource());
// Check that the two tabs are split
EXPECT_TRUE(GetTabStripModel()->GetSplitForTab(0).has_value());
EXPECT_TRUE(GetTabStripModel()->GetSplitForTab(1).has_value());
// Use the TabsDuplicateFunction to duplicate the tab at index 0.
int tab_extension_id = sessions::SessionTabHelper::IdForTab(
GetTabStripModel()->GetWebContentsAt(0))
.id();
auto function = base::MakeRefCounted<TabsDuplicateFunction>();
function->set_extension(extension);
constexpr char kFormatArgs[] = R"([%d])";
const std::string args = base::StringPrintf(kFormatArgs, tab_extension_id);
EXPECT_TRUE(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
EXPECT_EQ(ExtensionFunction::ResponseType::kSucceeded,
*function->response_type());
// Expect that there is one new tab in the tab strip the split view has been
// removed.
EXPECT_EQ(3, GetTabStripModel()->count());
EXPECT_FALSE(GetTabStripModel()->GetSplitForTab(0).has_value());
EXPECT_FALSE(GetTabStripModel()->GetSplitForTab(1).has_value());
EXPECT_FALSE(GetTabStripModel()->GetSplitForTab(2).has_value());
}
// Tests that calling chrome.tabs.discard on an inactive tab in an active split
// will discard that tab.
TEST_F(TabsApiSideBySideUnitTest, TabsDiscardInactiveTabInActiveSplitView) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("TabsDeleteFromSplitView").Build();
// Add a couple of web contents to the browser and mark them as split.
CreateAndGetWebContents(2);
GetTabStripModel()->ActivateTabAt(0);
GetTabStripModel()->AddToNewSplit({1}, split_tabs::SplitTabVisualData(),
split_tabs::SplitTabCreatedSource());
// Check that the two tabs are split and the tab at index 0 is active.
EXPECT_TRUE(GetTabStripModel()->GetSplitForTab(0).has_value());
EXPECT_TRUE(GetTabStripModel()->GetSplitForTab(1).has_value());
EXPECT_EQ(0, GetTabStripModel()->active_index());
// The tab discard function should succeed.
int tab_extension_id = sessions::SessionTabHelper::IdForTab(
GetTabStripModel()->GetWebContentsAt(1))
.id();
auto function = base::MakeRefCounted<TabsDiscardFunction>();
function->set_extension(extension);
EXPECT_TRUE(api_test_utils::RunFunction(
function.get(), base::StringPrintf("[%d]", tab_extension_id), profile(),
api_test_utils::FunctionMode::kNone));
EXPECT_EQ(ExtensionFunction::ResponseType::kSucceeded,
*function->response_type());
// The tab should be discarded.
content::WebContents* new_contents_at_index =
GetTabStripModel()->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.
TEST_F(TabsApiSideBySideUnitTest, TabsDeleteFromSplitView) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("TabsDeleteFromSplitView").Build();
// Add a couple of web contents to the browser and mark them as split.
CreateAndGetWebContents(2);
GetTabStripModel()->ActivateTabAt(0);
GetTabStripModel()->AddToNewSplit({1}, split_tabs::SplitTabVisualData(),
split_tabs::SplitTabCreatedSource());
// Check that the two tabs are split
EXPECT_TRUE(GetTabStripModel()->GetSplitForTab(0).has_value());
EXPECT_TRUE(GetTabStripModel()->GetSplitForTab(1).has_value());
// Use the TabsRemoveFunction to remove the tab at index 0.
int tab_extension_id = sessions::SessionTabHelper::IdForTab(
GetTabStripModel()->GetWebContentsAt(0))
.id();
auto function = base::MakeRefCounted<TabsRemoveFunction>();
function->set_extension(extension);
constexpr char kFormatArgs[] = R"([[%d]])";
const std::string args = base::StringPrintf(kFormatArgs, tab_extension_id);
EXPECT_TRUE(api_test_utils::RunFunction(function.get(), args, profile(),
api_test_utils::FunctionMode::kNone));
EXPECT_EQ(ExtensionFunction::ResponseType::kSucceeded,
*function->response_type());
// Expect that the tab has been removed and the remaining tab is not in a
// split view.
EXPECT_EQ(1, GetTabStripModel()->count());
EXPECT_FALSE(GetTabStripModel()->GetSplitForTab(0).has_value());
}
TEST_F(TabsApiSideBySideUnitTest, TabsQueryWithSplitView) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("TabsDeleteFromSplitView").Build();
// Add a couple of web contents to the browser and mark the first two as
// split.
CreateAndGetWebContents(5);
GetTabStripModel()->ActivateTabAt(0);
GetTabStripModel()->AddToNewSplit({1}, split_tabs::SplitTabVisualData(),
split_tabs::SplitTabCreatedSource());
// Check that the two tabs are split
EXPECT_TRUE(GetTabStripModel()->GetSplitForTab(0).has_value());
EXPECT_TRUE(GetTabStripModel()->GetSplitForTab(1).has_value());
// Use the TabsQueryFunction to get the list of tabs without a split.
const char* kNoSplitQueryInfo = "[{\"splitViewId\": -1}]";
base::Value::List tabs_list_without_split =
RunTabsQueryFunction(profile(), extension.get(), kNoSplitQueryInfo);
EXPECT_EQ(3u, tabs_list_without_split.size());
int split_id = ExtensionTabUtil::GetSplitId(
GetTabStripModel()->GetSplitForTab(0).value());
constexpr char kFormatArgs[] = R"([{"splitViewId": %d}])";
const std::string args = base::StringPrintf(kFormatArgs, split_id);
base::Value::List tabs_list_with_split =
RunTabsQueryFunction(profile(), extension.get(), args);
EXPECT_EQ(2u, tabs_list_with_split.size());
EXPECT_EQ(split_id, tabs_list_with_split[0].GetDict().FindInt("splitViewId"));
}
} // namespace extensions