summaryrefslogtreecommitdiffstats
path: root/chromium/chrome/browser/extensions/api/extension_action
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2022-05-12 15:59:20 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2022-05-25 06:57:22 +0000
commitf7eaed5286974984ba5f9e3189d8f49d03e99f81 (patch)
treecaed19b2af2024f35449fb0b781d0a25e09d4f8f /chromium/chrome/browser/extensions/api/extension_action
parent9729c4479fe23554eae6e6dd1f30ff488f470c84 (diff)
BASELINE: Update Chromium to 100.0.4896.167
Change-Id: I98cbeb5d7543d966ffe04d8cefded0c493a11333 Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/chrome/browser/extensions/api/extension_action')
-rw-r--r--chromium/chrome/browser/extensions/api/extension_action/browser_action_apitest.cc20
-rw-r--r--chromium/chrome/browser/extensions/api/extension_action/browser_action_interactive_test.cc25
-rw-r--r--chromium/chrome/browser/extensions/api/extension_action/extension_action_api.cc203
-rw-r--r--chromium/chrome/browser/extensions/api/extension_action/extension_action_api.h35
-rw-r--r--chromium/chrome/browser/extensions/api/extension_action/extension_action_api_interactive_uitest.cc258
-rw-r--r--chromium/chrome/browser/extensions/api/extension_action/extension_action_apitest.cc113
-rw-r--r--chromium/chrome/browser/extensions/api/extension_action/page_action_browsertest.cc184
-rw-r--r--chromium/chrome/browser/extensions/api/extension_action/page_action_interactive_test.cc3
8 files changed, 777 insertions, 64 deletions
diff --git a/chromium/chrome/browser/extensions/api/extension_action/browser_action_apitest.cc b/chromium/chrome/browser/extensions/api/extension_action/browser_action_apitest.cc
index 0ecac8acca6..31bd31407f9 100644
--- a/chromium/chrome/browser/extensions/api/extension_action/browser_action_apitest.cc
+++ b/chromium/chrome/browser/extensions/api/extension_action/browser_action_apitest.cc
@@ -37,8 +37,8 @@
#include "content/public/browser/browser_context.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/overlay_window.h"
-#include "content/public/browser/picture_in_picture_window_controller.h"
#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/video_picture_in_picture_window_controller.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
@@ -59,6 +59,7 @@
#include "extensions/test/result_catcher.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
+#include "ui/base/layout.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
@@ -186,7 +187,8 @@ class BrowserActionApiTestWithContextType
if (expect_failure) {
EXPECT_FALSE(catcher.GetNextResult());
- EXPECT_EQ("The source image could not be decoded.", catcher.message());
+ EXPECT_THAT(catcher.message(),
+ testing::EndsWith("The source image could not be decoded."));
return;
}
@@ -287,7 +289,7 @@ IN_PROC_BROWSER_TEST_F(BrowserActionApiCanvasTest, DynamicBrowserAction) {
const Extension* extension = GetSingleLoadedExtension();
ASSERT_TRUE(extension) << message_;
-#if defined(OS_MAC)
+#if BUILDFLAG(IS_MAC)
// We need this on mac so we don't loose 2x representations from browser icon
// in transformations gfx::ImageSkia -> NSImage -> gfx::ImageSkia.
std::vector<ui::ResourceScaleFactor> supported_scale_factors;
@@ -490,8 +492,6 @@ IN_PROC_BROWSER_TEST_F(BrowserActionApiCanvasTest, InvisibleIconBrowserAction) {
const std::string histogram_name =
"Extensions.DynamicExtensionActionIconWasVisible";
- const std::string new_histogram_name =
- "Extensions.DynamicExtensionActionIconWasVisibleRendered";
{
base::HistogramTester histogram_tester;
std::string result;
@@ -504,8 +504,6 @@ IN_PROC_BROWSER_TEST_F(BrowserActionApiCanvasTest, InvisibleIconBrowserAction) {
initial_bar_icon, GetBrowserActionsBar()->GetIcon(extension->id())));
EXPECT_THAT(histogram_tester.GetAllSamples(histogram_name),
testing::ElementsAre(base::Bucket(0, 1)));
- EXPECT_THAT(histogram_tester.GetAllSamples(new_histogram_name),
- testing::ElementsAre(base::Bucket(0, 1)));
}
{
@@ -520,8 +518,6 @@ IN_PROC_BROWSER_TEST_F(BrowserActionApiCanvasTest, InvisibleIconBrowserAction) {
initial_bar_icon, GetBrowserActionsBar()->GetIcon(extension->id())));
EXPECT_THAT(histogram_tester.GetAllSamples(histogram_name),
testing::ElementsAre(base::Bucket(1, 1)));
- EXPECT_THAT(histogram_tester.GetAllSamples(new_histogram_name),
- testing::ElementsAre(base::Bucket(1, 1)));
}
}
@@ -1019,9 +1015,9 @@ IN_PROC_BROWSER_TEST_F(BrowserActionApiTest,
process_manager->GetBackgroundHostForExtension(extension->id())
->web_contents();
ASSERT_TRUE(web_contents);
- content::PictureInPictureWindowController* window_controller =
- content::PictureInPictureWindowController::GetOrCreateForWebContents(
- web_contents);
+ content::VideoPictureInPictureWindowController* window_controller =
+ content::PictureInPictureWindowController::
+ GetOrCreateVideoPictureInPictureController(web_contents);
ASSERT_TRUE(window_controller->GetWindowForTesting());
EXPECT_FALSE(window_controller->GetWindowForTesting()->IsVisible());
diff --git a/chromium/chrome/browser/extensions/api/extension_action/browser_action_interactive_test.cc b/chromium/chrome/browser/extensions/api/extension_action/browser_action_interactive_test.cc
index 095bd7b23cf..cca00675ee2 100644
--- a/chromium/chrome/browser/extensions/api/extension_action/browser_action_interactive_test.cc
+++ b/chromium/chrome/browser/extensions/api/extension_action/browser_action_interactive_test.cc
@@ -53,7 +53,7 @@
#include "ui/gfx/scrollbar_size.h"
#include "ui/views/widget/widget.h"
-#if defined(OS_WIN)
+#if BUILDFLAG(IS_WIN)
#include "ui/views/win/hwnd_util.h"
#endif
@@ -217,7 +217,7 @@ class BrowserActionInteractiveTest : public ExtensionApiTest {
ExtensionHostTestHelper host_helper(profile());
-#if defined(OS_MAC)
+#if BUILDFLAG(IS_MAC)
// ClickOnView() in an inactive window is not robust on Mac. The click does
// not guarantee window activation on trybots. So activate the browser
// explicitly, thus causing the bubble to lose focus and dismiss itself.
@@ -306,7 +306,7 @@ IN_PROC_BROWSER_TEST_F(BrowserActionInteractiveTest, TestOpenPopupIncognito) {
frame_observer.Wait();
// Non-Aura Linux uses a singleton for the popup, so it looks like all windows
// have popups if there is any popup open.
-#if !((defined(OS_LINUX) || defined(OS_CHROMEOS)) && !defined(USE_AURA))
+#if !((BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)) && !defined(USE_AURA))
// Starting window does not have a popup.
EXPECT_FALSE(ExtensionActionTestHelper::Create(browser())->HasPopup());
#endif
@@ -539,7 +539,7 @@ IN_PROC_BROWSER_TEST_F(BrowserActionInteractiveViewsTest,
chrome::CloseWindow(browser());
}
-#if defined(OS_WIN)
+#if BUILDFLAG(IS_WIN)
// Forcibly closing a browser HWND with a popup should not cause a crash.
IN_PROC_BROWSER_TEST_F(BrowserActionInteractiveTest, DestroyHWNDDoesNotCrash) {
OpenPopupViaAPI(false);
@@ -563,7 +563,7 @@ IN_PROC_BROWSER_TEST_F(BrowserActionInteractiveTest, DestroyHWNDDoesNotCrash) {
EXPECT_EQ(FALSE, ::IsWindow(browser_hwnd));
EXPECT_EQ(FALSE, ::IsWindow(popup_hwnd));
}
-#endif // OS_WIN
+#endif // BUILDFLAG(IS_WIN)
class MainFrameSizeWaiter : public content::WebContentsObserver {
public:
@@ -592,11 +592,14 @@ class MainFrameSizeWaiter : public content::WebContentsObserver {
};
// TODO(crbug.com/1249851): Test crashes on Windows
-#if defined(OS_WIN)
+#if BUILDFLAG(IS_WIN)
#define MAYBE_BrowserActionPopup DISABLED_BrowserActionPopup
-#elif defined(OS_LINUX) && defined(THREAD_SANITIZER)
+#elif BUILDFLAG(IS_LINUX) && defined(THREAD_SANITIZER)
// TODO(crbug.com/1269076): Test is flaky for linux tsan builds
#define MAYBE_BrowserActionPopup DISABLED_BrowserActionPopup
+#elif BUILDFLAG(IS_MAC)
+// TODO(crbug.com/1269076): Test is flaky on Mac as well.
+#define MAYBE_BrowserActionPopup DISABLED_BrowserActionPopup
#else
#define MAYBE_BrowserActionPopup BrowserActionPopup
#endif
@@ -645,7 +648,7 @@ IN_PROC_BROWSER_TEST_F(BrowserActionInteractiveTest, MAYBE_BrowserActionPopup) {
// scrollbars are overlaid, appear on hover and don't increase the height
// or width of the popup.
const int kScrollbarAdjustment =
-#if defined(OS_MAC)
+#if BUILDFLAG(IS_MAC)
0;
#else
gfx::scrollbar_size();
@@ -715,7 +718,7 @@ IN_PROC_BROWSER_TEST_F(BrowserActionInteractiveTest, OpenPopupOnPopup) {
// that's what we check when we try to open the popup.
// TODO(crbug.com/1115237): Now that this is an interactive test, is this
// ifdef still necessary?
-#if !defined(OS_MAC)
+#if !BUILDFLAG(IS_MAC)
EXPECT_TRUE(popup_browser->window()->IsActive());
#endif
EXPECT_FALSE(browser()->window()->IsActive());
@@ -1019,7 +1022,7 @@ IN_PROC_BROWSER_TEST_F(NavigatingExtensionPopupInteractiveTest,
// The test verification below is applicable only to scenarios where the
// download shelf is supported - on ChromeOS, instead of the download shelf,
// there is a download notification in the right-bottom corner of the screen.
-#if !defined(OS_CHROMEOS)
+#if !BUILDFLAG(IS_CHROMEOS)
EXPECT_TRUE(browser()->window()->IsDownloadShelfVisible());
#endif
}
@@ -1054,7 +1057,7 @@ IN_PROC_BROWSER_TEST_F(NavigatingExtensionPopupInteractiveTest,
// The test verification below is applicable only to scenarios where the
// download shelf is supported - on ChromeOS, instead of the download shelf,
// there is a download notification in the right-bottom corner of the screen.
-#if !defined(OS_CHROMEOS)
+#if !BUILDFLAG(IS_CHROMEOS)
EXPECT_TRUE(browser()->window()->IsDownloadShelfVisible());
#endif
}
diff --git a/chromium/chrome/browser/extensions/api/extension_action/extension_action_api.cc b/chromium/chrome/browser/extensions/api/extension_action/extension_action_api.cc
index f175648f6eb..2ea6ab9abce 100644
--- a/chromium/chrome/browser/extensions/api/extension_action/extension_action_api.cc
+++ b/chromium/chrome/browser/extensions/api/extension_action/extension_action_api.cc
@@ -60,11 +60,85 @@ const char kNoTabError[] = "No tab with id: *.";
const char kOpenPopupError[] =
"Failed to show popup either because there is an existing popup or another "
"error occurred.";
+const char kFailedToOpenPopupGenericError[] = "Failed to open popup.";
const char kInvalidColorError[] =
"The color specification could not be parsed.";
+constexpr char kNoActiveWindowFound[] =
+ "Could not find an active browser window.";
+constexpr char kNoActivePopup[] =
+ "Extension does not have a popup on the active tab.";
bool g_report_error_for_invisible_icon = false;
+// Returns the browser that was last active in the given `profile`, optionally
+// also checking the incognito profile.
+Browser* FindLastActiveBrowserWindow(Profile* profile,
+ bool check_incognito_profile) {
+ Browser* browser = chrome::FindLastActiveWithProfile(profile);
+
+ if (browser && browser->window()->IsActive())
+ return browser; // Found an active browser.
+
+ // It's possible that the last active browser actually corresponds to the
+ // associated incognito profile, and this won't be returned by
+ // FindLastActiveWithProfile(). If the extension can operate incognito, then
+ // check the last active incognito, too.
+ if (check_incognito_profile && profile->HasPrimaryOTRProfile()) {
+ Profile* incognito_profile =
+ profile->GetPrimaryOTRProfile(/*create_if_needed=*/false);
+ DCHECK(incognito_profile);
+ Browser* incognito_browser =
+ chrome::FindLastActiveWithProfile(incognito_profile);
+ if (incognito_browser->window()->IsActive())
+ return incognito_browser;
+ }
+
+ return nullptr;
+}
+
+// Returns true if the given `extension` has an active popup on the active tab
+// of `browser`.
+bool HasPopupOnActiveTab(Browser* browser,
+ content::BrowserContext* browser_context,
+ const Extension& extension) {
+ content::WebContents* web_contents =
+ browser->tab_strip_model()->GetActiveWebContents();
+ ExtensionAction* extension_action =
+ ExtensionActionManager::Get(browser_context)
+ ->GetExtensionAction(extension);
+ DCHECK(extension_action);
+ int tab_id = ExtensionTabUtil::GetTabId(web_contents);
+
+ return extension_action->HasPopup(tab_id) &&
+ extension_action->GetIsVisibleIgnoringDeclarative(tab_id);
+}
+
+// Attempts to open `extension`'s popup in the given `browser`. Returns true on
+// success; otherwise, populates `error` and returns false.
+bool OpenPopupInBrowser(Browser& browser,
+ const Extension& extension,
+ std::string* error,
+ ShowPopupCallback callback) {
+ if (!browser.window()->IsToolbarVisible()) {
+ *error = "Browser window has no toolbar.";
+ return false;
+ }
+
+ if (!ExtensionActionAPI::Get(browser.profile())
+ ->ShowExtensionActionPopupForAPICall(&extension, &browser,
+ std::move(callback))) {
+ // NOTE(devlin): We could have the callback pass more information here about
+ // why the popup didn't open (e.g., another active popup vs popup closing
+ // before display, as may happen if the window closes), but it's not clear
+ // whether that would be significantly helpful to developers and it may
+ // leak other information about the user's browser.
+ *error = kFailedToOpenPopupGenericError;
+ return false;
+ }
+
+ return true;
+}
+
} // namespace
//
@@ -117,7 +191,8 @@ void ExtensionActionAPI::RemoveObserver(Observer* observer) {
bool ExtensionActionAPI::ShowExtensionActionPopupForAPICall(
const Extension* extension,
- Browser* browser) {
+ Browser* browser,
+ ShowPopupCallback callback) {
ExtensionAction* extension_action =
ExtensionActionManager::Get(browser_context_)->GetExtensionAction(
*extension);
@@ -134,7 +209,7 @@ bool ExtensionActionAPI::ShowExtensionActionPopupForAPICall(
// no toolbar.
return extensions_container &&
extensions_container->ShowToolbarActionPopupForAPICall(
- extension->id());
+ extension->id(), std::move(callback));
}
void ExtensionActionAPI::NotifyChange(ExtensionAction* extension_action,
@@ -225,7 +300,8 @@ void ExtensionActionAPI::DispatchEventToExtension(
return;
auto event = std::make_unique<Event>(
- histogram_value, event_name, std::move(*event_args).TakeList(), context);
+ histogram_value, event_name, std::move(*event_args).TakeListDeprecated(),
+ context);
event->user_gesture = EventRouter::USER_GESTURE_ENABLED;
EventRouter::Get(context)
->DispatchEventToExtension(extension_id, std::move(event));
@@ -380,12 +456,13 @@ ExtensionActionSetIconFunction::RunExtensionAction() {
// setIcon can take a variant argument: either a dictionary of canvas
// ImageData, or an icon index.
- base::DictionaryValue* canvas_set = NULL;
- if (details_->GetDictionary("imageData", &canvas_set)) {
+ base::Value* canvas_set = details_->FindDictKey("imageData");
+ if (canvas_set) {
gfx::ImageSkia icon;
ExtensionAction::IconParseResult parse_result =
- ExtensionAction::ParseIconFromCanvasDictionary(*canvas_set, &icon);
+ ExtensionAction::ParseIconFromCanvasDictionary(
+ base::Value::AsDictionaryValue(*canvas_set), &icon);
if (parse_result != ExtensionAction::IconParseResult::kSuccess) {
switch (parse_result) {
@@ -410,12 +487,6 @@ ExtensionActionSetIconFunction::RunExtensionAction() {
const bool is_visible = image_util::IsIconSufficientlyVisible(bitmap);
UMA_HISTOGRAM_BOOLEAN("Extensions.DynamicExtensionActionIconWasVisible",
is_visible);
- const bool is_visible_rendered =
- extensions::ui_util::IsRenderedIconSufficientlyVisibleForBrowserContext(
- bitmap, browser_context());
- UMA_HISTOGRAM_BOOLEAN(
- "Extensions.DynamicExtensionActionIconWasVisibleRendered",
- is_visible_rendered);
if (!is_visible && g_report_error_for_invisible_icon)
return RespondNow(Error("Icon not sufficiently visible."));
@@ -478,7 +549,7 @@ ExtensionActionSetBadgeBackgroundColorFunction::RunExtensionAction() {
EXTENSION_FUNCTION_VALIDATE(color_value);
SkColor color = 0;
if (color_value->is_list()) {
- base::Value::ConstListView list = color_value->GetList();
+ base::Value::ConstListView list = color_value->GetListDeprecated();
EXTENSION_FUNCTION_VALIDATE(list.size() == 4);
@@ -575,33 +646,99 @@ ExtensionFunction::ResponseAction ActionGetUserSettingsFunction::Run() {
return RespondNow(OneArgument(std::move(ui_settings)));
}
+ActionOpenPopupFunction::ActionOpenPopupFunction() = default;
+ActionOpenPopupFunction::~ActionOpenPopupFunction() = default;
+
+ExtensionFunction::ResponseAction ActionOpenPopupFunction::Run() {
+ // Unfortunately, the action API types aren't compiled. However, the bindings
+ // should still valid the form of the arguments.
+ EXTENSION_FUNCTION_VALIDATE(args().size() == 1u);
+ EXTENSION_FUNCTION_VALIDATE(extension());
+ const base::Value& options = args()[0];
+
+ // TODO(https://crbug.com/1245093): Support specifying the tab ID? This is
+ // kind of racy (because really what the extension probably cares about is
+ // the document ID; tab ID persists across pages, whereas document ID would
+ // detect things like navigations).
+ int window_id = extension_misc::kCurrentWindowId;
+ if (options.is_dict()) {
+ const base::Value* window_value = options.FindKey("windowId");
+ if (window_value) {
+ EXTENSION_FUNCTION_VALIDATE(window_value->is_int());
+ window_id = window_value->GetInt();
+ }
+ }
+
+ Browser* browser = nullptr;
+ Profile* profile = Profile::FromBrowserContext(browser_context());
+ std::string error;
+ if (window_id == extension_misc::kCurrentWindowId) {
+ browser =
+ FindLastActiveBrowserWindow(profile, include_incognito_information());
+ if (!browser)
+ error = kNoActiveWindowFound;
+ } else {
+ browser = ExtensionTabUtil::GetBrowserInProfileWithId(
+ profile, window_id, include_incognito_information(), &error);
+ }
+
+ if (!browser) {
+ DCHECK(!error.empty());
+ return RespondNow(Error(std::move(error)));
+ }
+
+ if (!HasPopupOnActiveTab(browser, browser_context(), *extension()))
+ return RespondNow(Error(kNoActivePopup));
+
+ if (!OpenPopupInBrowser(
+ *browser, *extension(), &error,
+ base::BindOnce(&ActionOpenPopupFunction::OnShowPopupComplete,
+ this))) {
+ DCHECK(!error.empty());
+ return RespondNow(Error(std::move(error)));
+ }
+
+ // The function responds in OnShowPopupComplete(). Note that the function is
+ // kept alive by the ref-count owned by the ShowPopupCallback.
+ return RespondLater();
+}
+
+void ActionOpenPopupFunction::OnShowPopupComplete(ExtensionHost* popup_host) {
+ DCHECK(!did_respond());
+
+ ResponseValue response_value;
+ if (popup_host) {
+ // TODO(https://crbug.com/1245093): Return the tab for which the extension
+ // popup was shown?
+ DCHECK(popup_host->document_element_available());
+ response_value = NoArguments();
+ } else {
+ response_value = Error(kFailedToOpenPopupGenericError);
+ }
+
+ Respond(std::move(response_value));
+}
+
BrowserActionOpenPopupFunction::BrowserActionOpenPopupFunction() = default;
BrowserActionOpenPopupFunction::~BrowserActionOpenPopupFunction() = default;
ExtensionFunction::ResponseAction BrowserActionOpenPopupFunction::Run() {
// We only allow the popup in the active window.
Profile* profile = Profile::FromBrowserContext(browser_context());
- Browser* browser = chrome::FindLastActiveWithProfile(profile);
- // It's possible that the last active browser actually corresponds to the
- // associated incognito profile, and this won't be returned by
- // FindLastActiveWithProfile. If the browser we found isn't active and the
- // extension can operate incognito, then check the last active incognito, too.
- if ((!browser || !browser->window()->IsActive()) &&
- util::IsIncognitoEnabled(extension()->id(), profile) &&
- profile->HasPrimaryOTRProfile()) {
- browser = chrome::FindLastActiveWithProfile(
- profile->GetPrimaryOTRProfile(/*create_if_needed=*/true));
- }
+ Browser* browser =
+ FindLastActiveBrowserWindow(profile, include_incognito_information());
+
+ if (!browser)
+ return RespondNow(Error(kNoActiveWindowFound));
+
+ if (!HasPopupOnActiveTab(browser, browser_context(), *extension()))
+ return RespondNow(Error(kNoActivePopup));
- // If there's no active browser, or the Toolbar isn't visible, abort.
- // Otherwise, try to open a popup in the active browser.
- // TODO(justinlin): Remove toolbar check when http://crbug.com/308645 is
- // fixed.
- if (!browser || !browser->window()->IsActive() ||
- !browser->window()->IsToolbarVisible() ||
- !ExtensionActionAPI::Get(profile)->ShowExtensionActionPopupForAPICall(
- extension_.get(), browser)) {
- return RespondNow(Error(kOpenPopupError));
+ std::string error;
+ if (!OpenPopupInBrowser(*browser, *extension(), &error,
+ ShowPopupCallback())) {
+ DCHECK(!error.empty());
+ return RespondNow(Error(std::move(error)));
}
// Even if this is for an incognito window, we want to use the normal profile.
diff --git a/chromium/chrome/browser/extensions/api/extension_action/extension_action_api.h b/chromium/chrome/browser/extensions/api/extension_action/extension_action_api.h
index 2d88df41a87..b5589c3caa2 100644
--- a/chromium/chrome/browser/extensions/api/extension_action/extension_action_api.h
+++ b/chromium/chrome/browser/extensions/api/extension_action/extension_action_api.h
@@ -10,6 +10,7 @@
#include "base/memory/raw_ptr.h"
#include "base/observer_list.h"
#include "base/scoped_observation.h"
+#include "chrome/browser/ui/extensions/extension_popup_types.h"
#include "extensions/browser/browser_context_keyed_api_factory.h"
#include "extensions/browser/extension_action.h"
#include "extensions/browser/extension_event_histogram_value.h"
@@ -72,9 +73,11 @@ class ExtensionActionAPI : public BrowserContextKeyedAPI {
void AddObserver(Observer* observer);
void RemoveObserver(Observer* observer);
- // Opens the popup for the given |extension| in the given |browser|'s window.
+ // Opens the popup for the given |extension| in the given |browser|'s window,
+ // invoking |callback| when complete.
bool ShowExtensionActionPopupForAPICall(const Extension* extension,
- Browser* browser);
+ Browser* browser,
+ ShowPopupCallback callback);
// Notifies that there has been a change in the given |extension_action|.
void NotifyChange(ExtensionAction* extension_action,
@@ -356,6 +359,31 @@ class ActionGetUserSettingsFunction : public ExtensionFunction {
~ActionGetUserSettingsFunction() override;
};
+// Note: action.openPopup() and browserAction.openPopup() have subtly different
+// implementations:
+// * action.openPopup() allows the extension to specify a window ID.
+// * browserAction.openPopup() will time out after 10 seconds;
+// action.openPopup() does not time out and instead waits for the popup to
+// either be shown or encounter an error.
+// * browserAction.openPopup() returns a handle to the HTMLWindow of the
+// popup; action.openPopup() returns nothing.
+// Due to these differences, the implementations are distinct classes.
+class ActionOpenPopupFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("action.openPopup", ACTION_OPENPOPUP)
+
+ ActionOpenPopupFunction();
+ ActionOpenPopupFunction(const ActionOpenPopupFunction&) = delete;
+ ActionOpenPopupFunction& operator=(const ActionOpenPopupFunction&) = delete;
+
+ protected:
+ // ExtensionFunction:
+ ~ActionOpenPopupFunction() override;
+ ResponseAction Run() override;
+
+ void OnShowPopupComplete(ExtensionHost* popup_host);
+};
+
//
// browserAction.* aliases for supported browserAction APIs.
//
@@ -456,6 +484,9 @@ class BrowserActionDisableFunction : public ExtensionActionHideFunction {
~BrowserActionDisableFunction() override {}
};
+// Note: action.openPopup() and browserAction.openPopup() have subtly different
+// implementations. See ActionOpenPopupFunction above.
+// TODO(devlin): Remove browserAction.openPopup().
class BrowserActionOpenPopupFunction : public ExtensionFunction,
public ExtensionHostRegistry::Observer {
public:
diff --git a/chromium/chrome/browser/extensions/api/extension_action/extension_action_api_interactive_uitest.cc b/chromium/chrome/browser/extensions/api/extension_action/extension_action_api_interactive_uitest.cc
new file mode 100644
index 00000000000..6f13209824a
--- /dev/null
+++ b/chromium/chrome/browser/extensions/api/extension_action/extension_action_api_interactive_uitest.cc
@@ -0,0 +1,258 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/strings/stringprintf.h"
+#include "base/test/bind.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_window.h"
+#include "chrome/browser/ui/extensions/extension_action_test_helper.h"
+#include "chrome/test/base/interactive_test_utils.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/version_info/channel.h"
+#include "content/public/test/browser_test.h"
+#include "extensions/browser/browsertest_util.h"
+#include "extensions/browser/extension_action.h"
+#include "extensions/browser/extension_action_manager.h"
+#include "extensions/browser/extension_host_registry.h"
+#include "extensions/common/features/feature_channel.h"
+#include "extensions/test/result_catcher.h"
+#include "extensions/test/test_extension_dir.h"
+
+namespace extensions {
+
+// An interactive UI test suite for the chrome.action API. This is used for
+// tests where things like focus and window activation are important.
+class ActionAPIInteractiveUITest : public ExtensionApiTest {
+ public:
+ ActionAPIInteractiveUITest() = default;
+ ~ActionAPIInteractiveUITest() override = default;
+
+ // Loads a common "stub" extension with an action specified in its manifest;
+ // tests then execute script in this extension's context.
+ // This allows us to more seamlessly integrate C++ and JS execution by
+ // inlining the JS here in this file, rather than needing to separate it out
+ // into separate test files and coordinate messaging back-and-forth.
+ const Extension* LoadStubExtension() {
+ return LoadExtension(
+ test_data_dir_.AppendASCII("extension_action/stub_action"));
+ }
+
+ // Runs the given `script` in the background service worker context for the
+ // `extension`, and waits for a corresponding test success notification.
+ void RunScriptTest(const std::string& script, const Extension& extension) {
+ ResultCatcher result_catcher;
+ base::RunLoop run_loop;
+ auto callback = [&run_loop](base::Value value) { run_loop.Quit(); };
+ browsertest_util::ExecuteScriptInServiceWorker(
+ profile(), extension.id(), script,
+ base::BindLambdaForTesting(callback));
+ run_loop.Run();
+ EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
+ }
+
+ // Same as `RunScriptTest()`, but wraps `script` in a test.runTests() call
+ // with a single function.
+ void WrapAndRunScript(const std::string& script, const Extension& extension) {
+ constexpr char kTemplate[] =
+ R"(chrome.test.runTests([
+ async function openPopupTest() {
+ %s
+ }
+ ]);)";
+ std::string wrapped_script = base::StringPrintf(kTemplate, script.c_str());
+ RunScriptTest(wrapped_script, extension);
+ }
+
+ // Returns the active popup for the given `extension`, if one exists.
+ ExtensionHost* GetPopup(const Extension& extension) {
+ ExtensionHostRegistry* registry = ExtensionHostRegistry::Get(profile());
+ std::vector<ExtensionHost*> hosts =
+ registry->GetHostsForExtension(extension.id());
+ ExtensionHost* found_host = nullptr;
+ for (auto* host : hosts) {
+ if (host->extension_host_type() == mojom::ViewType::kExtensionPopup) {
+ if (found_host) {
+ ADD_FAILURE() << "Multiple popups found!";
+ return nullptr;
+ }
+ found_host = host;
+ }
+ }
+ return found_host;
+ }
+
+ // Returns true if the given `browser` has an active popup.
+ bool BrowserHasPopup(Browser* browser) {
+ return ExtensionActionTestHelper::Create(browser)->HasPopup();
+ }
+
+ // The action.openPopup() function is currently scoped to dev channel.
+ ScopedCurrentChannel scoped_current_channel_{version_info::Channel::DEV};
+};
+
+// Tests displaying a popup in the active window when no window ID is specified.
+IN_PROC_BROWSER_TEST_F(ActionAPIInteractiveUITest, OpenPopupInActiveWindow) {
+ const Extension* extension = LoadStubExtension();
+ ASSERT_TRUE(extension);
+
+ constexpr char kScript[] =
+ R"(await chrome.action.openPopup();
+ chrome.test.succeed();)";
+ WrapAndRunScript(kScript, *extension);
+
+ EXPECT_TRUE(BrowserHasPopup(browser()));
+ ExtensionHost* host = GetPopup(*extension);
+ ASSERT_TRUE(host);
+ EXPECT_TRUE(host->has_loaded_once());
+ EXPECT_EQ(extension->GetResourceURL("popup.html"),
+ host->main_frame_host()->GetLastCommittedURL());
+}
+
+// Tests displaying a popup in a window specified in the API call.
+IN_PROC_BROWSER_TEST_F(ActionAPIInteractiveUITest, OpenPopupInSpecifiedWindow) {
+ const Extension* extension = LoadStubExtension();
+ ASSERT_TRUE(extension);
+
+ Browser* second_browser = CreateBrowser(profile());
+ ASSERT_TRUE(second_browser);
+ ui_test_utils::BrowserActivationWaiter(second_browser).WaitForActivation();
+
+ // TODO(https://crbug.com/1245093): We should allow extensions to open a
+ // popup in an inactive window. Currently, this fails, so try to open the
+ // popup in the active window (but with a specified ID).
+ EXPECT_FALSE(browser()->window()->IsActive());
+ EXPECT_TRUE(second_browser->window()->IsActive());
+
+ int window_id = ExtensionTabUtil::GetWindowId(second_browser);
+
+ constexpr char kScript[] =
+ R"(await chrome.action.openPopup({windowId: %d});
+ chrome.test.succeed();)";
+ WrapAndRunScript(base::StringPrintf(kScript, window_id), *extension);
+
+ // The popup should be shown on the second browser.
+ {
+ EXPECT_TRUE(BrowserHasPopup(second_browser));
+ ExtensionHost* host = GetPopup(*extension);
+ ASSERT_TRUE(host);
+ EXPECT_TRUE(host->has_loaded_once());
+ EXPECT_EQ(extension->GetResourceURL("popup.html"),
+ host->main_frame_host()->GetLastCommittedURL());
+ }
+
+ EXPECT_FALSE(BrowserHasPopup(browser()));
+}
+
+// Tests a series of action.openPopup() invocations that are expected to fail.
+IN_PROC_BROWSER_TEST_F(ActionAPIInteractiveUITest, OpenPopupFailures) {
+ const Extension* extension = LoadStubExtension();
+ ASSERT_TRUE(extension);
+
+ constexpr char kScript[] =
+ R"(chrome.test.runTests([
+ async function openPopupFailsWithFakeWindow() {
+ const fakeWindowId = 99999;
+ await chrome.test.assertPromiseRejects(
+ chrome.action.openPopup({windowId: fakeWindowId}),
+ `Error: No window with id: ${fakeWindowId}.`);
+ chrome.test.succeed();
+ },
+ async function openPopupFailsWhenNoPopupSpecified() {
+ // Specifying an empty string for the popup means "no popup".
+ await chrome.action.setPopup({popup: ''});
+ await chrome.test.assertPromiseRejects(
+ chrome.action.openPopup(),
+ 'Error: Extension does not have a popup on the active tab.');
+ chrome.test.succeed();
+ },
+ async function openPopupFailsWhenPopupIsDisabled() {
+ await chrome.action.setPopup({popup: 'popup.html'});
+ await chrome.action.disable();
+ await chrome.test.assertPromiseRejects(
+ chrome.action.openPopup(),
+ 'Error: Extension does not have a popup on the active tab.');
+ chrome.test.succeed();
+ },
+ async function tryToOpenPopupThatClosesBeforeItsOpened() {
+ await chrome.action.enable();
+ // insta_close_popup.html has a script file that synchronously
+ // calls window.close() during document parsing. This results in
+ // the popup closing before it's actually shown.
+ await chrome.action.setPopup({popup: 'insta_close_popup.html'});
+ await chrome.test.assertPromiseRejects(
+ chrome.action.openPopup(),
+ 'Error: Failed to open popup.');
+ chrome.test.succeed();
+ },
+ ]);)";
+ RunScriptTest(kScript, *extension);
+ EXPECT_FALSE(BrowserHasPopup(browser()));
+}
+
+// Tests that openPopup() will not succeed if a popup is only visible on a tab
+// because of a declarative condition.
+// https://crbug.com/1289846.
+IN_PROC_BROWSER_TEST_F(ActionAPIInteractiveUITest,
+ DontOpenPopupForDeclarativelyShownAction) {
+ ASSERT_TRUE(StartEmbeddedTestServer());
+
+ constexpr char kManifest[] =
+ R"({
+ "name": "My Extension",
+ "manifest_version": 3,
+ "version": "0.1",
+ "background": { "service_worker": "worker.js" },
+ "action": { "default_popup": "popup.html" },
+ "permissions": ["declarativeContent"]
+ })";
+ constexpr char kWorkerJs[] = "// Intentionally blank";
+ constexpr char kPopupHtml[] = "<html>Hello, World!</html>";
+
+ TestExtensionDir test_dir;
+ test_dir.WriteManifest(kManifest);
+ test_dir.WriteFile(FILE_PATH_LITERAL("worker.js"), kWorkerJs);
+ test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), kPopupHtml);
+
+ const Extension* extension = LoadExtension(test_dir.UnpackedPath());
+ ASSERT_TRUE(extension);
+
+ constexpr char kDisableActionAndSetRule[] =
+ R"(await chrome.action.disable();
+ await chrome.declarativeContent.onPageChanged.addRules([{
+ conditions: [
+ new chrome.declarativeContent.PageStateMatcher({
+ pageUrl: {hostEquals: 'example.com'}
+ })
+ ],
+ actions: [
+ new chrome.declarativeContent.ShowAction(),
+ ]
+ }]);
+ chrome.test.succeed();)";
+
+ WrapAndRunScript(kDisableActionAndSetRule, *extension);
+
+ GURL url = embedded_test_server()->GetURL("example.com", "/simple.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+ int tab_id = ExtensionTabUtil::GetTabId(
+ browser()->tab_strip_model()->GetActiveWebContents());
+
+ const ExtensionAction* extension_action =
+ ExtensionActionManager::Get(profile())->GetExtensionAction(*extension);
+ ASSERT_TRUE(extension_action);
+ EXPECT_TRUE(extension_action->GetIsVisible(tab_id));
+ EXPECT_FALSE(extension_action->GetIsVisibleIgnoringDeclarative(tab_id));
+
+ constexpr char kTryOpenPopup[] =
+ R"(await chrome.test.assertPromiseRejects(
+ chrome.action.openPopup(),
+ 'Error: Extension does not have a popup on the active tab.');
+ chrome.test.succeed();)";
+ WrapAndRunScript(kTryOpenPopup, *extension);
+}
+
+} // namespace extensions
diff --git a/chromium/chrome/browser/extensions/api/extension_action/extension_action_apitest.cc b/chromium/chrome/browser/extensions/api/extension_action/extension_action_apitest.cc
index 1963eb194c5..27e7e7268ea 100644
--- a/chromium/chrome/browser/extensions/api/extension_action/extension_action_apitest.cc
+++ b/chromium/chrome/browser/extensions/api/extension_action/extension_action_apitest.cc
@@ -449,8 +449,7 @@ IN_PROC_BROWSER_TEST_P(MultiActionAPITest, OnClickedDispatching) {
chrome.test.assertTrue(tab.id > 0);
chrome.test.assertTrue(tab.index > -1);
chrome.test.notifyPass();
- });
- chrome.test.sendMessage('ready');)";
+ });)";
const char* background_specification =
GetParam() == ActionInfo::TYPE_ACTION
@@ -472,10 +471,8 @@ IN_PROC_BROWSER_TEST_P(MultiActionAPITest, OnClickedDispatching) {
ExtensionActionTestHelper::Create(browser());
EXPECT_EQ(0, toolbar_helper->NumberOfBrowserActions());
- ExtensionTestMessageListener listener("ready", /*will_reply=*/false);
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
- ASSERT_TRUE(listener.WaitUntilSatisfied());
ASSERT_EQ(1, toolbar_helper->NumberOfBrowserActions());
EXPECT_TRUE(toolbar_helper->HasAction(extension->id()));
@@ -560,7 +557,7 @@ IN_PROC_BROWSER_TEST_P(MultiActionAPITest, PopupCreation) {
// Tests that sessionStorage does not persist between closing and opening of a
// popup.
// TODO(crbug/1256760): Flaky on Linux.
-#if defined(OS_LINUX)
+#if BUILDFLAG(IS_LINUX)
#define MAYBE_SessionStorageDoesNotPersistBetweenOpenings \
DISABLED_SessionStorageDoesNotPersistBetweenOpenings
#else
@@ -1007,6 +1004,112 @@ IN_PROC_BROWSER_TEST_P(MultiActionAPITest, SetIconWithSelfDefined) {
base::StringPrintf(kSetIconScript, tab_id));
}
+// Tests calling setIcon() for a tab with an invalid icon path specified.
+IN_PROC_BROWSER_TEST_P(MultiActionAPITest, SetIconInTabWithInvalidPath) {
+ constexpr char kManifestTemplate[] =
+ R"({
+ "name": "Bad Icon Path",
+ "manifest_version": %d,
+ "version": "0.1",
+ "%s": {}
+ })";
+
+ constexpr char kPageJsTemplate[] =
+ R"(function setIcon(details) {
+ chrome.%s.setIcon(details, () => {
+ chrome.test.assertLastError("%s");
+ chrome.test.notifyPass();
+ });
+ })";
+
+ constexpr char kExpectedError[] =
+ "Could not load action icon 'does_not_exist.png'.";
+
+ TestExtensionDir test_dir;
+ test_dir.WriteManifest(base::StringPrintf(
+ kManifestTemplate, GetManifestVersionForActionType(GetParam()),
+ GetManifestKeyForActionType(GetParam())));
+
+ test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtmlTemplate);
+ test_dir.WriteFile(
+ FILE_PATH_LITERAL("page.js"),
+ base::StringPrintf(kPageJsTemplate, GetAPINameForActionType(GetParam()),
+ kExpectedError));
+
+ const Extension* extension = LoadExtension(test_dir.UnpackedPath());
+ ASSERT_TRUE(extension);
+
+ ExtensionAction* action = GetExtensionAction(*extension);
+ ASSERT_TRUE(action);
+
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(
+ browser(), extension->GetResourceURL("page.html")));
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ int tab_id = GetActiveTabId();
+ EXPECT_TRUE(ActionHasDefaultState(*action, tab_id));
+ EnsureActionIsEnabledOnActiveTab(action);
+
+ // Calling setIcon with an invalid path in a non-service worker context should
+ // emit a console error in that context and call the callback with lastError
+ // set.
+ content::WebContentsConsoleObserver console_observer(web_contents);
+ console_observer.SetPattern(kExpectedError);
+
+ constexpr char kSetIconScript[] =
+ "setIcon({tabId: %d, path: 'does_not_exist.png'});";
+ RunTestAndWaitForSuccess(web_contents,
+ base::StringPrintf(kSetIconScript, tab_id));
+ console_observer.Wait();
+}
+
+// Tests calling setIcon() in the service worker with an invalid icon path
+// specified. Regression test for https://crbug.com/1262029.
+IN_PROC_BROWSER_TEST_F(ExtensionActionAPITest, SetIconInWorkerWithInvalidPath) {
+ constexpr char kManifestTemplate[] =
+ R"({
+ "name": "Bad Icon Path In Worker",
+ "manifest_version": 3,
+ "version": "0.1",
+ "action": {},
+ "background": {"service_worker": "worker.js" }
+ })";
+
+ constexpr char kBackgroundJs[] =
+ R"(let expectedError = "%s";
+ chrome.test.runTests([
+ function withCallback() {
+ chrome.action.setIcon({path: 'does_not_exist.png'}, () => {
+ chrome.test.assertLastError(expectedError);
+ chrome.test.succeed();
+ });
+ },
+ async function withPromise() {
+ await chrome.test.assertPromiseRejects(
+ chrome.action.setIcon({path: 'does_not_exist.png'}),
+ 'Error: ' + expectedError);
+ chrome.test.succeed();
+ }
+ ]);)";
+
+ constexpr char kExpectedError[] =
+ "Failed to set icon 'does_not_exist.png': Failed to fetch";
+
+ TestExtensionDir test_dir;
+ test_dir.WriteManifest(kManifestTemplate);
+
+ test_dir.WriteFile(FILE_PATH_LITERAL("worker.js"),
+ base::StringPrintf(kBackgroundJs, kExpectedError));
+
+ // Calling setIcon with an invalid path in a service worker context should
+ // reject the promise or call the callback with lastError set.
+ ResultCatcher result_catcher;
+ const Extension* extension = LoadExtension(test_dir.UnpackedPath());
+ ASSERT_TRUE(extension);
+ EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
+}
+
// Tests various getter and setter methods.
IN_PROC_BROWSER_TEST_P(MultiActionAPITest, GettersAndSetters) {
// Load up an extension with default values.
diff --git a/chromium/chrome/browser/extensions/api/extension_action/page_action_browsertest.cc b/chromium/chrome/browser/extensions/api/extension_action/page_action_browsertest.cc
new file mode 100644
index 00000000000..38aed9231f6
--- /dev/null
+++ b/chromium/chrome/browser/extensions/api/extension_action/page_action_browsertest.cc
@@ -0,0 +1,184 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <stddef.h>
+
+#include "base/strings/utf_string_conversions.h"
+#include "build/build_config.h"
+#include "chrome/browser/extensions/extension_action_test_util.h"
+#include "chrome/browser/extensions/extension_browsertest.h"
+#include "chrome/browser/extensions/extension_tab_util.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "content/public/test/browser_test.h"
+#include "extensions/browser/extension_action.h"
+#include "extensions/browser/extension_action_manager.h"
+#include "extensions/browser/extension_registry.h"
+#include "extensions/common/extension.h"
+#include "extensions/common/switches.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+
+namespace extensions {
+namespace {
+
+const std::string kSubscribePageAction = "subscribe_page_action/src";
+const std::string kFeedPage = "/feeds/feed.html";
+const std::string kNoFeedPage = "/feeds/no_feed.html";
+
+const std::string kHashPageA =
+ "/extensions/api_test/page_action/hash_change/test_page_A.html";
+const std::string kHashPageAHash = kHashPageA + "#asdf";
+const std::string kHashPageB =
+ "/extensions/api_test/page_action/hash_change/test_page_B.html";
+
+using ContextType = ExtensionBrowserTest::ContextType;
+
+class PageActionBrowserTest : public ExtensionBrowserTest,
+ public testing::WithParamInterface<ContextType> {
+ public:
+ PageActionBrowserTest() : ExtensionBrowserTest(GetParam()) {}
+ ~PageActionBrowserTest() override = default;
+ PageActionBrowserTest(const PageActionBrowserTest& other) = delete;
+ PageActionBrowserTest& operator=(const PageActionBrowserTest& other) = delete;
+};
+
+INSTANTIATE_TEST_SUITE_P(PersistentBackground,
+ PageActionBrowserTest,
+ ::testing::Values(ContextType::kPersistentBackground));
+
+INSTANTIATE_TEST_SUITE_P(ServiceWorker,
+ PageActionBrowserTest,
+ ::testing::Values(ContextType::kServiceWorker));
+
+IN_PROC_BROWSER_TEST_P(PageActionBrowserTest, PageActionCrash25562) {
+ ASSERT_TRUE(embedded_test_server()->Start());
+
+ // This page action will not show an icon, since it doesn't specify one but
+ // is included here to test for a crash (http://crbug.com/25562).
+ ASSERT_TRUE(LoadExtension(
+ test_data_dir_.AppendASCII("browsertest")
+ .AppendASCII("crash_25562")));
+
+ // Navigate to the feed page.
+ GURL feed_url = embedded_test_server()->GetURL(kFeedPage);
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), feed_url));
+ // We should now have one page action ready to go in the LocationBar.
+ ASSERT_TRUE(WaitForPageActionVisibilityChangeTo(1));
+}
+
+// Tests that we can load page actions in the Omnibox.
+IN_PROC_BROWSER_TEST_P(PageActionBrowserTest, PageAction) {
+ ASSERT_TRUE(embedded_test_server()->Start());
+
+ ASSERT_TRUE(LoadExtension(
+ test_data_dir_.AppendASCII(kSubscribePageAction)));
+
+ ASSERT_TRUE(WaitForPageActionVisibilityChangeTo(0));
+
+ // Navigate to the feed page.
+ GURL feed_url = embedded_test_server()->GetURL(kFeedPage);
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), feed_url));
+ // We should now have one page action ready to go in the LocationBar.
+ ASSERT_TRUE(WaitForPageActionVisibilityChangeTo(1));
+
+ // Navigate to a page with no feed.
+ GURL no_feed = embedded_test_server()->GetURL(kNoFeedPage);
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), no_feed));
+ // Make sure the page action goes away.
+ ASSERT_TRUE(WaitForPageActionVisibilityChangeTo(0));
+}
+
+// Tests that we don't lose the page action icon on same-document navigations.
+IN_PROC_BROWSER_TEST_P(PageActionBrowserTest, SameDocumentNavigation) {
+ ASSERT_TRUE(embedded_test_server()->Start());
+
+ base::FilePath extension_path(test_data_dir_.AppendASCII("api_test")
+ .AppendASCII("page_action")
+ .AppendASCII("hash_change"));
+ ASSERT_TRUE(LoadExtension(extension_path));
+
+ // Page action should become visible when we navigate here.
+ GURL feed_url = embedded_test_server()->GetURL(kHashPageA);
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), feed_url));
+ ASSERT_TRUE(WaitForPageActionVisibilityChangeTo(1));
+
+ // Same-document navigation, page action should remain.
+ feed_url = embedded_test_server()->GetURL(kHashPageAHash);
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), feed_url));
+ ASSERT_TRUE(WaitForPageActionVisibilityChangeTo(1));
+
+ // Not a same-document navigation, page action should go away.
+ feed_url = embedded_test_server()->GetURL(kHashPageB);
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), feed_url));
+ ASSERT_TRUE(WaitForPageActionVisibilityChangeTo(0));
+}
+
+// Tests that the location bar forgets about unloaded page actions.
+// Flaky on Linux: https://crbug.com/1283900
+#if BUILDFLAG(IS_LINUX)
+#define MAYBE_UnloadPageAction DISABLED_UnloadPageAction
+#else
+#define MAYBE_UnloadPageAction UnloadPageAction
+#endif
+IN_PROC_BROWSER_TEST_P(PageActionBrowserTest, MAYBE_UnloadPageAction) {
+ ASSERT_TRUE(embedded_test_server()->Start());
+
+ base::FilePath extension_path(
+ test_data_dir_.AppendASCII(kSubscribePageAction));
+ ASSERT_TRUE(LoadExtension(extension_path));
+
+ // Navigation prompts the location bar to load page actions.
+ GURL feed_url = embedded_test_server()->GetURL(kFeedPage);
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), feed_url));
+ content::WebContents* tab =
+ browser()->tab_strip_model()->GetActiveWebContents();
+ EXPECT_EQ(1u, extension_action_test_util::GetTotalPageActionCount(tab));
+
+ UnloadExtension(last_loaded_extension_id());
+
+ // Make sure the page action goes away when it's unloaded.
+ EXPECT_EQ(0u, extension_action_test_util::GetTotalPageActionCount(tab));
+}
+
+// Regression test for crbug.com/44415.
+// TODO(crbug.com/1284555): Re-enable test to run with service workers after
+// fixing flakiness.
+using PageActionWithoutServiceWorkerTest = ExtensionBrowserTest;
+IN_PROC_BROWSER_TEST_F(PageActionWithoutServiceWorkerTest,
+ PageActionRefreshCrash) {
+ ExtensionRegistry* registry =
+ extensions::ExtensionRegistry::Get(browser()->profile());
+
+ size_t size_before = registry->enabled_extensions().size();
+
+ base::FilePath base_path = test_data_dir_.AppendASCII("browsertest")
+ .AppendASCII("crash_44415");
+ // Load extension A.
+ const Extension* extensionA = LoadExtension(base_path.AppendASCII("ExtA"));
+ ASSERT_TRUE(extensionA);
+ ASSERT_TRUE(WaitForPageActionVisibilityChangeTo(1));
+ ASSERT_EQ(size_before + 1, registry->enabled_extensions().size());
+
+ // Load extension B.
+ const Extension* extensionB = LoadExtension(base_path.AppendASCII("ExtB"));
+ ASSERT_TRUE(extensionB);
+ ASSERT_TRUE(WaitForPageActionVisibilityChangeTo(2));
+ ASSERT_EQ(size_before + 2, registry->enabled_extensions().size());
+
+ std::string idA = extensionA->id();
+ ReloadExtension(extensionA->id());
+ // ExtensionA has changed, so refetch it.
+ ASSERT_EQ(size_before + 2, registry->enabled_extensions().size());
+ extensionA = registry->enabled_extensions().GetByID(idA);
+
+ ReloadExtension(extensionB->id());
+
+ // This is where it would crash, before http://crbug.com/44415 was fixed.
+ ReloadExtension(extensionA->id());
+}
+
+} // namespace
+} // namespace extensions
diff --git a/chromium/chrome/browser/extensions/api/extension_action/page_action_interactive_test.cc b/chromium/chrome/browser/extensions/api/extension_action/page_action_interactive_test.cc
index 09c8a9b7e61..7a70b718560 100644
--- a/chromium/chrome/browser/extensions/api/extension_action/page_action_interactive_test.cc
+++ b/chromium/chrome/browser/extensions/api/extension_action/page_action_interactive_test.cc
@@ -28,7 +28,8 @@ IN_PROC_BROWSER_TEST_F(PageActionInteractiveTest, ShowPageActionPopup) {
ResultCatcher catcher;
ASSERT_TRUE(ExtensionActionAPI::Get(browser()->profile())
- ->ShowExtensionActionPopupForAPICall(extension, browser()));
+ ->ShowExtensionActionPopupForAPICall(extension, browser(),
+ ShowPopupCallback()));
ASSERT_TRUE(catcher.GetNextResult());
}