blob: c1722c746bda0be4cf5fa0efecfa2d4285cbcaa9 [file] [log] [blame]
// 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 <stddef.h>
#include "apps/test/app_window_waiter.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/pattern.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/apps/platform_apps/app_browsertest_util.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/extension_apitest.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/extensions/extension_action_test_helper.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/api_test_utils.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_builder.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#include "ui/base/ozone_buildflags.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
namespace extensions {
namespace keys = tabs_constants;
namespace utils = api_test_utils;
using ContextType = extensions::browser_test_util::ContextType;
using ExtensionTabsTest = InProcessBrowserTest;
IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, GetLastFocusedWindow) {
// Create a new window which making it the "last focused" window.
// Note that "last focused" means the "top" most window.
Browser* new_browser = CreateBrowser(GetProfile());
ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(new_browser));
GURL url("about:blank");
ASSERT_TRUE(AddTabAtIndexToBrowser(new_browser, 0, url,
ui::PAGE_TRANSITION_LINK, true));
int focused_window_id =
extensions::ExtensionTabUtil::GetWindowId(new_browser);
scoped_refptr<extensions::WindowsGetLastFocusedFunction> function =
new extensions::WindowsGetLastFocusedFunction();
scoped_refptr<const extensions::Extension> extension(
extensions::ExtensionBuilder("Test").Build());
function->set_extension(extension.get());
base::Value::Dict result =
utils::ToDict(utils::RunFunctionAndReturnSingleResult(
function.get(), "[]", new_browser->profile()));
// The id should always match the last focused window and does not depend
// on what was passed to RunFunctionAndReturnSingleResult.
EXPECT_EQ(focused_window_id, api_test_utils::GetInteger(result, "id"));
EXPECT_FALSE(result.contains(ExtensionTabUtil::kTabsKey));
function = new extensions::WindowsGetLastFocusedFunction();
function->set_extension(extension.get());
result = utils::ToDict(utils::RunFunctionAndReturnSingleResult(
function.get(), "[{\"populate\": true}]", GetProfile()));
// The id should always match the last focused window and does not depend
// on what was passed to RunFunctionAndReturnSingleResult.
EXPECT_EQ(focused_window_id, api_test_utils::GetInteger(result, "id"));
// "populate" was enabled so tabs should be populated.
api_test_utils::GetList(result, ExtensionTabUtil::kTabsKey);
}
IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, QueryLastFocusedWindowTabs) {
const size_t kExtraWindows = 2;
for (size_t i = 0; i < kExtraWindows; ++i) {
CreateBrowser(GetProfile());
}
Browser* focused_window = CreateBrowser(GetProfile());
ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(focused_window));
GURL url("about:blank");
ASSERT_TRUE(AddTabAtIndexToBrowser(focused_window, 0, url,
ui::PAGE_TRANSITION_LINK, true));
int focused_window_id =
extensions::ExtensionTabUtil::GetWindowId(focused_window);
// Get tabs in the 'last focused' window called from non-focused browser.
scoped_refptr<extensions::TabsQueryFunction> function =
new extensions::TabsQueryFunction();
scoped_refptr<const extensions::Extension> extension(
extensions::ExtensionBuilder("Test").Build());
function->set_extension(extension.get());
base::Value::List result_tabs(
utils::ToList(utils::RunFunctionAndReturnSingleResult(
function.get(), "[{\"lastFocusedWindow\":true}]", GetProfile())));
// 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(focused_window_id,
api_test_utils::GetInteger(utils::ToDict(result_tab),
keys::kWindowIdKey));
}
// Get tabs NOT in the 'last focused' window called from the focused browser.
function = new extensions::TabsQueryFunction();
function->set_extension(extension.get());
result_tabs = utils::ToList(utils::RunFunctionAndReturnSingleResult(
function.get(), "[{\"lastFocusedWindow\":false}]", GetProfile()));
// We should get one tab for each extra window and one for the initial window.
EXPECT_EQ(kExtraWindows + 1, result_tabs.size());
for (const base::Value& result_tab : result_tabs) {
EXPECT_NE(focused_window_id,
api_test_utils::GetInteger(utils::ToDict(result_tab),
keys::kWindowIdKey));
}
}
class NonPersistentExtensionTabsTest
: public ExtensionApiTest,
public testing::WithParamInterface<ContextType> {
public:
NonPersistentExtensionTabsTest() : ExtensionApiTest(GetParam()) {}
~NonPersistentExtensionTabsTest() override = default;
NonPersistentExtensionTabsTest(const NonPersistentExtensionTabsTest&) =
delete;
NonPersistentExtensionTabsTest& operator=(
const NonPersistentExtensionTabsTest&) = delete;
};
#if BUILDFLAG(IS_LINUX)
#define MAYBE_TabCurrentWindow DISABLED_TabCurrentWindow
#else
#define MAYBE_TabCurrentWindow TabCurrentWindow
#endif
// Tests chrome.windows.create and chrome.windows.getCurrent.
// TODO(crbug.com/40636155): Expand the test to verify that setSelfAsOpener
// param is ignored from Service Worker extension scripts.
IN_PROC_BROWSER_TEST_P(NonPersistentExtensionTabsTest, MAYBE_TabCurrentWindow) {
ASSERT_TRUE(RunExtensionTest("tabs/current_window")) << message_;
}
// TODO(crbug.com/40759767): Crashes on Linux-ozone-rel.
#if BUILDFLAG(IS_OZONE)
#define MAYBE_TabGetLastFocusedWindow DISABLED_TabGetLastFocusedWindow
#else
#define MAYBE_TabGetLastFocusedWindow TabGetLastFocusedWindow
#endif
// Tests chrome.windows.getLastFocused.
IN_PROC_BROWSER_TEST_P(NonPersistentExtensionTabsTest,
MAYBE_TabGetLastFocusedWindow) {
ASSERT_TRUE(RunExtensionTest("tabs/last_focused_window")) << message_;
}
// TODO(http://crbug.com/41237209): The Linux window manager behaves
// differently, which complicates the test. A separate test should
// be written for it to avoid complicating this one.
#if !BUILDFLAG(IS_LINUX)
IN_PROC_BROWSER_TEST_P(NonPersistentExtensionTabsTest, WindowSetFocus) {
ASSERT_TRUE(RunExtensionTest("window_update/set_focus")) << message_;
}
#endif
INSTANTIATE_TEST_SUITE_P(EventPage,
NonPersistentExtensionTabsTest,
::testing::Values(ContextType::kEventPage));
INSTANTIATE_TEST_SUITE_P(ServiceWorker,
NonPersistentExtensionTabsTest,
::testing::Values(ContextType::kServiceWorker));
// TODO(llandwerlin): Activating a browser window and waiting for the
// action to happen requires views::Widget which is not available on
// MacOSX. Deactivate for now.
#if !BUILDFLAG(IS_MAC)
class ExtensionWindowLastFocusedTest : public PlatformAppBrowserTest {
public:
void SetUpOnMainThread() override;
void ActivateBrowserWindow(Browser* browser);
Browser* CreateBrowserWithEmptyTab(bool as_popup);
int GetTabId(const base::Value::Dict& dict) const;
std::optional<base::Value> RunFunction(ExtensionFunction* function,
const std::string& params);
const Extension* extension() { return extension_.get(); }
private:
// A helper class to wait for an views::Widget to become activated.
class WidgetActivatedWaiter : public views::WidgetObserver {
public:
explicit WidgetActivatedWaiter(views::Widget* widget)
: widget_(widget), waiting_(false) {
widget_->AddObserver(this);
}
~WidgetActivatedWaiter() override { widget_->RemoveObserver(this); }
void ActivateAndWait() {
widget_->Activate();
if (!widget_->IsActive()) {
waiting_ = true;
base::RunLoop nested_run_loop(
base::RunLoop::Type::kNestableTasksAllowed);
quit_closure_ = nested_run_loop.QuitWhenIdleClosure();
nested_run_loop.Run();
}
}
// views::WidgetObserver:
void OnWidgetActivationChanged(views::Widget* widget,
bool active) override {
if (widget_ == widget && waiting_) {
waiting_ = false;
std::move(quit_closure_).Run();
}
}
private:
raw_ptr<views::Widget> widget_;
bool waiting_;
base::RepeatingClosure quit_closure_;
};
scoped_refptr<const Extension> extension_;
};
void ExtensionWindowLastFocusedTest::SetUpOnMainThread() {
PlatformAppBrowserTest::SetUpOnMainThread();
extension_ = ExtensionBuilder("Test").Build();
}
void ExtensionWindowLastFocusedTest::ActivateBrowserWindow(Browser* browser) {
BrowserView* view = BrowserView::GetBrowserViewForBrowser(browser);
EXPECT_NE(nullptr, view);
views::Widget* widget = view->frame();
EXPECT_NE(nullptr, widget);
WidgetActivatedWaiter waiter(widget);
waiter.ActivateAndWait();
}
Browser* ExtensionWindowLastFocusedTest::CreateBrowserWithEmptyTab(
bool as_popup) {
Browser* new_browser;
if (as_popup) {
new_browser = Browser::Create(
Browser::CreateParams(Browser::TYPE_POPUP, GetProfile(), true));
} else {
new_browser = Browser::Create(Browser::CreateParams(GetProfile(), true));
}
AddBlankTabAndShow(new_browser);
return new_browser;
}
int ExtensionWindowLastFocusedTest::GetTabId(
const base::Value::Dict& dict) const {
const base::Value::List* tabs = dict.FindList(ExtensionTabUtil::kTabsKey);
if (!tabs || tabs->empty()) {
return -2;
}
const base::Value::Dict* tab_dict = (*tabs)[0].GetIfDict();
if (!tab_dict) {
return -2;
}
return tab_dict->FindInt(extension_misc::kId).value_or(-2);
}
std::optional<base::Value> ExtensionWindowLastFocusedTest::RunFunction(
ExtensionFunction* function,
const std::string& params) {
function->set_extension(extension_.get());
return utils::RunFunctionAndReturnSingleResult(function, params,
GetProfile());
}
IN_PROC_BROWSER_TEST_F(ExtensionWindowLastFocusedTest,
NoDevtoolsAndAppWindows) {
DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync(
browser()->tab_strip_model()->GetWebContentsAt(0), false /* is_docked */);
{
int devtools_window_id = ExtensionTabUtil::GetWindowId(
DevToolsWindowTesting::Get(devtools)->browser());
ActivateBrowserWindow(DevToolsWindowTesting::Get(devtools)->browser());
scoped_refptr<WindowsGetLastFocusedFunction> function =
new WindowsGetLastFocusedFunction();
const base::Value::Dict result =
utils::ToDict(RunFunction(function.get(), "[{\"populate\": true}]"));
EXPECT_NE(devtools_window_id, api_test_utils::GetInteger(result, "id"));
}
AppWindow* app_window = CreateTestAppWindow(
"{\"outerBounds\": "
"{\"width\": 300, \"height\": 300,"
" \"minWidth\": 200, \"minHeight\": 200,"
" \"maxWidth\": 400, \"maxHeight\": 400}}");
{
apps::AppWindowWaiter waiter(AppWindowRegistry::Get(GetProfile()),
app_window->extension_id());
waiter.WaitForActivated();
scoped_refptr<WindowsGetLastFocusedFunction> get_current_app_function =
new WindowsGetLastFocusedFunction();
const base::Value::Dict result = utils::ToDict(
RunFunction(get_current_app_function.get(), "[{\"populate\": true}]"));
int app_window_id = app_window->session_id().id();
EXPECT_NE(app_window_id, api_test_utils::GetInteger(result, "id"));
}
DevToolsWindowTesting::CloseDevToolsWindowSync(devtools);
CloseAppWindow(app_window);
}
IN_PROC_BROWSER_TEST_F(ExtensionWindowLastFocusedTest,
NoTabIdForDevToolsAndAppWindows) {
Browser* normal_browser = CreateBrowserWithEmptyTab(false);
{
ActivateBrowserWindow(normal_browser);
scoped_refptr<WindowsGetLastFocusedFunction> function =
new WindowsGetLastFocusedFunction();
const base::Value::Dict result =
utils::ToDict(RunFunction(function.get(), "[{\"populate\": true}]"));
int normal_browser_window_id =
ExtensionTabUtil::GetWindowId(normal_browser);
EXPECT_EQ(normal_browser_window_id,
api_test_utils::GetInteger(result, "id"));
EXPECT_NE(-1, GetTabId(result));
EXPECT_EQ("normal", api_test_utils::GetString(result, "type"));
}
Browser* popup_browser = CreateBrowserWithEmptyTab(true);
{
ActivateBrowserWindow(popup_browser);
scoped_refptr<WindowsGetLastFocusedFunction> function =
new WindowsGetLastFocusedFunction();
const base::Value::Dict result =
utils::ToDict(RunFunction(function.get(), "[{\"populate\": true}]"));
int popup_browser_window_id = ExtensionTabUtil::GetWindowId(popup_browser);
EXPECT_EQ(popup_browser_window_id,
api_test_utils::GetInteger(result, "id"));
EXPECT_NE(-1, GetTabId(result));
EXPECT_EQ("popup", api_test_utils::GetString(result, "type"));
}
DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync(
browser()->tab_strip_model()->GetWebContentsAt(0), false /* is_docked */);
{
ActivateBrowserWindow(DevToolsWindowTesting::Get(devtools)->browser());
scoped_refptr<WindowsGetLastFocusedFunction> function =
new WindowsGetLastFocusedFunction();
const base::Value::Dict result = utils::ToDict(RunFunction(
function.get(),
"[{\"populate\": true, \"windowTypes\": [ \"devtools\" ]}]"));
int devtools_window_id = ExtensionTabUtil::GetWindowId(
DevToolsWindowTesting::Get(devtools)->browser());
EXPECT_EQ(devtools_window_id, api_test_utils::GetInteger(result, "id"));
EXPECT_EQ(-1, GetTabId(result));
EXPECT_EQ("devtools", api_test_utils::GetString(result, "type"));
}
AppWindow* app_window = CreateTestAppWindow(
"{\"outerBounds\": "
"{\"width\": 300, \"height\": 300,"
" \"minWidth\": 200, \"minHeight\": 200,"
" \"maxWidth\": 400, \"maxHeight\": 400}}");
{
apps::AppWindowWaiter waiter(AppWindowRegistry::Get(GetProfile()),
app_window->extension_id());
waiter.WaitForActivated();
scoped_refptr<WindowsGetLastFocusedFunction> get_current_app_function =
new WindowsGetLastFocusedFunction();
get_current_app_function->set_extension(extension());
EXPECT_EQ(tabs_constants::kNoLastFocusedWindowError,
api_test_utils::RunFunctionAndReturnError(
get_current_app_function.get(),
"[{\"populate\": true, \"windowTypes\": [ \"app\" ]}]",
GetProfile()));
}
chrome::CloseWindow(normal_browser);
chrome::CloseWindow(popup_browser);
DevToolsWindowTesting::CloseDevToolsWindowSync(devtools);
CloseAppWindow(app_window);
}
#endif // !BUILDFLAG(IS_MAC)
using TabsApiInteractiveTest = ExtensionApiTest;
// Tests that a window created with `focused: false` does not cover the focused
// window. Regression test for https://crbug.com/1302159.
IN_PROC_BROWSER_TEST_F(TabsApiInteractiveTest,
OpeningAnUnfocusedWindowDoesntCoverTheFocusedWindow) {
ASSERT_TRUE(StartEmbeddedTestServer());
const GURL url1 = embedded_test_server()->GetURL("/title1.html");
const GURL url2 = embedded_test_server()->GetURL("/title2.html");
// Navigate to `url1` and ensure the browser is active.
{
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), url1));
ui_test_utils::BrowserActivationWaiter activation_waiter(browser());
browser()->window()->Activate();
activation_waiter.WaitForActivation();
}
ASSERT_TRUE(browser()->window()->IsActive());
// Create and load an extension that creates a new window with a tab at
// `url2` with `focused: false` and waits for the tab to complete loading.
static constexpr char kManifest[] =
R"({
"name": "Interactive Test",
"manifest_version": 3,
"version": "0.1",
"background": { "service_worker": "background.js" },
"permissions": ["tabs"]
})";
static constexpr char kBackgroundJs[] =
R"(chrome.test.runTests([
async function openUnfocusedWindow() {
const url = '%s';
const tabCreatedPromise = new Promise((resolve) => {
chrome.tabs.onUpdated.addListener(
function listener(tabId, changeInfo, tab) {
if (changeInfo.status === 'complete' &&
tab.url === url) {
chrome.tabs.onUpdated.removeListener(listener);
resolve();
}
});
});
const win =
await chrome.windows.create({focused: false, url: url});
chrome.test.assertFalse(win.focused);
await tabCreatedPromise;
chrome.test.succeed();
},
]);)";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
base::StringPrintf(kBackgroundJs, url2.spec().c_str()));
ResultCatcher result_catcher;
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
// Now, verify the browsers. There should be exactly two browser windows (the
// original and the one created by the extension).
BrowserList* browser_list = BrowserList::GetInstance();
ASSERT_EQ(2u, browser_list->size());
ASSERT_TRUE(base::Contains(*browser_list, browser()));
// Find the new browser. Be flexible in case BrowserList's internal sort
// changes.
Browser* new_browser = browser_list->get(0) == browser()
? browser_list->get(1)
: browser_list->get(0);
EXPECT_NE(new_browser, browser());
// The new browser should have a tab pointed to `url2`; we use this mostly as
// validation that setup went according to plan.
EXPECT_EQ(1, new_browser->tab_strip_model()->count());
EXPECT_EQ(url2, new_browser->tab_strip_model()
->GetActiveWebContents()
->GetLastCommittedURL());
bool check_window_active_state = true;
#if (BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)) && \
BUILDFLAG(IS_OZONE_WAYLAND)
check_window_active_state = false;
#endif
// The new browser should be inactive, since it was created with
// `focused: false`. The old browser should remain active.
// This assertion fails on Wayland. This is possibly due to
// https://crbug.com/1280332, where bubbles are drawn on the same window,
// but that is yet to be confirmed.
if (check_window_active_state) {
EXPECT_FALSE(new_browser->window()->IsActive());
EXPECT_TRUE(browser()->window()->IsActive());
}
// The old browser (which retains focus) should be on top of the new browser.
// This currently fails because WidgetTest::IsWindowStackedAbove() doesn't
// work for different BrowserViews. While the functionality is currently
// correct, this means we don't have a good regression test for it.
// TODO(crbug.com/40058935): Fix this.
// EXPECT_TRUE(views::test::WidgetTest::IsWindowStackedAbove(
// BrowserView::GetBrowserViewForBrowser(browser())->frame(),
// BrowserView::GetBrowserViewForBrowser(new_browser)->frame()));
}
// Test for crbug.com/405283740
// Verifies that an extension popup does not immediately close after calling
// chrome.windows.create with focus: false, allowing subsequent JS to run.
IN_PROC_BROWSER_TEST_F(TabsApiInteractiveTest,
PopupDoesNotCloseOnUnfocusedWindowCreate) {
ASSERT_TRUE(StartEmbeddedTestServer());
static constexpr char kManifest[] =
R"({
"name": "Popup Window Create Test",
"version": "1.0",
"manifest_version": 3,
"action": { "default_popup": "popup.html" },
"permissions": ["tabs"]
})";
static constexpr char kPopupHtml[] =
R"(
<!DOCTYPE html>
<html>
<head>
<script src="popup.js"></script>
</head>
<body>
Creating window...
</body>
</html>
)";
// popup.js: Calls chrome.windows.create, then sends a message.
// If the popup closes, the message won't be sent.
static constexpr char kPopupJs[] =
R"(
// Use an async function to so that we can await the create call.
async function createWindowAndSignal() {
const win = await chrome.windows.create(
{ focused: false, url: 'about:blank' });
chrome.test.assertEq(undefined, chrome.runtime.lastError);
chrome.test.assertNe(null, win);
// Crucial part: This message is sent *after* create call
chrome.test.sendMessage('popup_still_open');
}
createWindowAndSignal();
)";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), kPopupHtml);
test_dir.WriteFile(FILE_PATH_LITERAL("popup.js"), kPopupJs);
// Load the extension
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
// Prepare to listen for the message from the popup
ExtensionTestMessageListener listener("popup_still_open");
// Create a BrowserActionTestUtil to trigger the popup.
std::unique_ptr<ExtensionActionTestHelper> browser_action_test_util =
ExtensionActionTestHelper::Create(browser());
ASSERT_EQ(1, browser_action_test_util->NumberOfBrowserActions());
browser_action_test_util->Press(extension->id());
// Wait for the 'popup_still_open' message.
// If the popup closed prematurely, this will time out or fail.
ASSERT_TRUE(listener.WaitUntilSatisfied())
<< "Listener failed to hear from popup.";
// Additional Verification.
BrowserList* browser_list = BrowserList::GetInstance();
// We should have the original browser and the new one
ASSERT_EQ(2u, browser_list->size());
// Check Z-Order.
// Under the hood, the original browser was temporarily pinned to the front by
// setting its z-order to kFloatingWindow. This checks that the original
// browser's z-order is reset.
EXPECT_EQ(ui::ZOrderLevel::kNormal, browser()->window()->GetZOrderLevel());
}
} // namespace extensions