blob: 406b3f0174dd67a1ecc04ebc80a1298e06328619 [file] [log] [blame]
// Copyright 2015 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/ui/extensions/extension_action_view_controller.h"
#include <stddef.h>
#include <array>
#include <memory>
#include "base/command_line.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/api/side_panel/side_panel_service.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/extension_action_dispatcher.h"
#include "chrome/browser/extensions/extension_action_runner.h"
#include "chrome/browser/extensions/extension_action_test_util.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/load_error_reporter.h"
#include "chrome/browser/extensions/permissions/permissions_updater.h"
#include "chrome/browser/extensions/permissions/scripting_permissions_modifier.h"
#include "chrome/browser/extensions/permissions/site_permissions_helper.h"
#include "chrome/browser/extensions/scoped_test_mv2_enabler.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/browser/extensions/user_script_listener.h"
#include "chrome/browser/ui/extensions/extensions_container.h"
#include "chrome/browser/ui/extensions/icon_with_badge_image_source.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_model.h"
#include "chrome/browser/ui/views/extensions/extensions_toolbar_container.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/common/extensions/api/side_panel.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/sessions/content/session_tab_helper.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_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/permissions_manager.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/common/api/extension_action/action_info.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/mojom/run_location.mojom-shared.h"
#include "extensions/test/test_extension_dir.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/image_model.h"
#include "ui/gfx/image/image_skia_rep.h"
#include "ui/native_theme/native_theme.h"
using extensions::mojom::ManifestLocation;
using SiteInteraction = extensions::SitePermissionsHelper::SiteInteraction;
using UserSiteSetting = extensions::PermissionsManager::UserSiteSetting;
using HoverCardState = ToolbarActionViewController::HoverCardState;
class ExtensionActionViewControllerBrowserTest : public InProcessBrowserTest {
public:
ExtensionActionViewControllerBrowserTest() = default;
ExtensionActionViewControllerBrowserTest(
const ExtensionActionViewControllerBrowserTest& other) = delete;
ExtensionActionViewControllerBrowserTest& operator=(
const ExtensionActionViewControllerBrowserTest& other) = delete;
~ExtensionActionViewControllerBrowserTest() override = default;
void Init() { AddTab(browser(), GURL(u"https://example.com")); }
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(embedded_https_test_server().Start());
InProcessBrowserTest::SetUpOnMainThread();
}
void AddTab(Browser* browser, GURL gurl) {
ui_test_utils::NavigateToURLWithDisposition(
browser, gurl, WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
}
void NavigateAndCommitActiveTab(const GURL& gurl) {
ui_test_utils::NavigateToURLWithDisposition(
browser(), gurl, WindowOpenDisposition::CURRENT_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
}
// Sets whether the given |action| wants to run on the |web_contents|.
void SetActionWantsToRunOnTab(extensions::ExtensionAction* action,
content::WebContents* web_contents,
bool wants_to_run) {
action->SetIsVisible(
sessions::SessionTabHelper::IdForTab(web_contents).id(), wants_to_run);
extensions::ExtensionActionDispatcher::Get(browser()->profile())
->NotifyChange(action, web_contents, browser()->profile());
}
// Returns the active WebContents for the primary browser.
content::WebContents* GetActiveWebContents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
ExtensionActionViewController* GetViewControllerForId(
const std::string& action_id) {
// It's safe to static cast here, because these tests only deal with
// extensions.
return static_cast<ExtensionActionViewController*>(
container()->GetActionForId(action_id));
}
scoped_refptr<const extensions::Extension> CreateAndAddExtension(
const std::string& name,
extensions::ActionInfo::Type action_type) {
return CreateAndAddExtensionWithGrantedHostPermissions(name, action_type,
{});
}
void GrantActivePermissions(const extensions::Extension* extension) {
extensions::PermissionsUpdater(browser()->profile())
.GrantActivePermissions(extension);
}
scoped_refptr<const extensions::Extension>
CreateAndAddExtensionWithGrantedHostPermissions(
const std::string& name,
extensions::ActionInfo::Type action_type,
const std::vector<std::string>& permissions) {
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder(name)
.SetAction(action_type)
.SetLocation(ManifestLocation::kInternal)
.AddHostPermissions(permissions)
.Build();
if (!permissions.empty()) {
GrantActivePermissions(extension.get());
}
extension_registrar()->AddExtension(extension.get());
return extension;
}
extensions::ExtensionRegistrar* extension_registrar() {
return extensions::ExtensionRegistrar::Get(browser()->profile());
}
ToolbarActionsModel* toolbar_model() {
return ToolbarActionsModel::Get(browser()->profile());
}
ExtensionsToolbarContainer* container() {
return browser()->GetBrowserView().toolbar()->extensions_container();
}
extensions::SidePanelService* side_panel_service() {
return extensions::SidePanelService::Get(browser()->profile());
}
// The standard size associated with a toolbar action view.
gfx::Size view_size() { return container()->GetToolbarActionSize(); }
};
// Temporary test class to test functionality while kExtensionsMenuAccessControl
// feature is being rolled out.
// TODO(crbug.com/40857680): Remove once feature is fully enabled.
class ExtensionActionViewControllerFeatureRolloutBrowserTest
: public ExtensionActionViewControllerBrowserTest,
public testing::WithParamInterface<bool> {
public:
ExtensionActionViewControllerFeatureRolloutBrowserTest() {
if (GetParam()) {
feature_list_.InitAndEnableFeature(
extensions_features::kExtensionsMenuAccessControl);
} else {
feature_list_.InitAndDisableFeature(
extensions_features::kExtensionsMenuAccessControl);
}
}
private:
base::test::ScopedFeatureList feature_list_;
};
INSTANTIATE_TEST_SUITE_P(,
ExtensionActionViewControllerFeatureRolloutBrowserTest,
testing::Bool(),
[](const testing::TestParamInfo<bool>& info) {
return info.param ? "FeatureEnabled"
: "FeatureDisabled";
});
// Tests the icon appearance of extension actions in the toolbar.
// Extensions that don't want to run should have their icons grayscaled.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewControllerFeatureRolloutBrowserTest,
ExtensionActionWantsToRunAppearance) {
Init();
const std::string id =
CreateAndAddExtension("extension", extensions::ActionInfo::Type::kPage)
->id();
content::WebContents* web_contents = GetActiveWebContents();
ExtensionActionViewController* const action = GetViewControllerForId(id);
ASSERT_TRUE(action);
std::unique_ptr<IconWithBadgeImageSource> image_source =
action->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_TRUE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
SetActionWantsToRunOnTab(action->extension_action(), web_contents, true);
image_source =
action->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
}
// Tests the appearance of browser actions with blocked script actions.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewControllerFeatureRolloutBrowserTest,
BrowserActionBlockedActions) {
Init();
auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
"browser_action", extensions::ActionInfo::Type::kBrowser,
{"https://www.google.com/*"});
extensions::ScriptingPermissionsModifier permissions_modifier(
browser()->profile(), extension);
permissions_modifier.SetWithholdHostPermissions(true);
AddTab(browser(), GURL("https://www.google.com/"));
ExtensionActionViewController* const action_controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(action_controller);
EXPECT_EQ(extension.get(), action_controller->extension());
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(web_contents);
std::unique_ptr<IconWithBadgeImageSource> image_source =
action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
// Request a script injection, so it can be marked as blocked.
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
ASSERT_TRUE(action_runner);
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::mojom::RunLocation::kDocumentIdle,
base::DoNothing());
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
// Browser action has a decoration only when the feature is disabled.
bool is_feature_enabled = GetParam();
EXPECT_EQ(image_source->paint_blocked_actions_decoration(),
!is_feature_enabled);
// Verify running the action removes the decoration, if existent.
action_runner->RunForTesting(extension.get());
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
}
// Tests the appearance of page actions with blocked script actions.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewControllerFeatureRolloutBrowserTest,
PageActionBlockedActions) {
Init();
auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
"page_action", extensions::ActionInfo::Type::kPage,
{"https://www.google.com/*"});
extensions::ScriptingPermissionsModifier permissions_modifier(
browser()->profile(), extension);
permissions_modifier.SetWithholdHostPermissions(true);
AddTab(browser(), GURL("https://www.google.com/"));
ExtensionActionViewController* const action_controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(action_controller);
EXPECT_EQ(extension.get(), action_controller->extension());
content::WebContents* web_contents = GetActiveWebContents();
std::unique_ptr<IconWithBadgeImageSource> image_source =
action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::mojom::RunLocation::kDocumentIdle,
base::DoNothing());
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
// Page action has a decoration only when the feature is disabled.
bool is_feature_enabled = GetParam();
EXPECT_EQ(image_source->paint_blocked_actions_decoration(),
!is_feature_enabled);
// Simulate NativeTheme update after `image_source` is created.
// `image_source` should paint fine without hitting use-after-free in such
// case. See http://crbug.com/1315967
ui::NativeTheme::GetInstanceForNativeUi()->NotifyOnNativeThemeUpdated();
image_source->GetImageForScale(1.0f);
}
// Tests the appearance of extension actions for extensions without a browser or
// page action defined in their manifest, but with host permissions on a page.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewControllerFeatureRolloutBrowserTest,
OnlyHostPermissionsAppearance) {
Init();
bool is_feature_enabled = GetParam();
const std::u16string kWantsAccessTooltip =
is_feature_enabled ? u"just hosts\nRequesting to read & change this site"
: u"just hosts\nWants access to this site";
const std::u16string kHasAccessTooltip =
is_feature_enabled ? u"just hosts\nAllowed to read & change this site"
: u"just hosts\nHas access to this site";
const std::u16string kNoAccessTooltip = u"just hosts";
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("just hosts")
.SetLocation(ManifestLocation::kInternal)
.AddHostPermission("https://www.google.com/*")
.Build();
GrantActivePermissions(extension.get());
extension_registrar()->AddExtension(extension.get());
extensions::ScriptingPermissionsModifier permissions_modifier(
browser()->profile(), extension);
permissions_modifier.SetWithholdHostPermissions(true);
ExtensionActionViewController* const action_controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(action_controller);
EXPECT_EQ(extension.get(), action_controller->extension());
// Initially load on a site that the extension doesn't have permissions to.
AddTab(browser(), GURL("https://www.chromium.org/"));
content::WebContents* web_contents = GetActiveWebContents();
std::unique_ptr<IconWithBadgeImageSource> image_source =
action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_TRUE(image_source->grayscale());
EXPECT_FALSE(action_controller->IsEnabled(web_contents));
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
EXPECT_EQ(kNoAccessTooltip, action_controller->GetTooltip(web_contents));
// Navigate to a url the extension does have permissions to. The extension is
// set to run on click and has the current URL withheld, so it should not be
// grayscaled and should be clickable.
NavigateAndCommitActiveTab(GURL("https://www.google.com/"));
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_TRUE(action_controller->IsEnabled(web_contents));
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
EXPECT_EQ(kWantsAccessTooltip, action_controller->GetTooltip(web_contents));
// After triggering the action it should have access, which is reflected in
// the tooltip.
action_controller->ExecuteUserAction(
ToolbarActionViewController::InvocationSource::kToolbarButton);
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(action_controller->IsEnabled(web_contents));
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
EXPECT_EQ(kHasAccessTooltip, action_controller->GetTooltip(web_contents));
}
IN_PROC_BROWSER_TEST_P(ExtensionActionViewControllerFeatureRolloutBrowserTest,
ExtensionActionContextMenuVisibility) {
Init();
bool is_feature_enabled = GetParam();
const std::u16string pin_label = l10n_util::GetStringUTF16(
is_feature_enabled ? IDS_EXTENSIONS_CONTEXT_MENU_PIN_TO_TOOLBAR
: IDS_EXTENSIONS_PIN_TO_TOOLBAR);
const std::u16string unpin_label = l10n_util::GetStringUTF16(
is_feature_enabled ? IDS_EXTENSIONS_CONTEXT_MENU_UNPIN_FROM_TOOLBAR
: IDS_EXTENSIONS_UNPIN_FROM_TOOLBAR);
std::string id =
CreateAndAddExtension("extension", extensions::ActionInfo::Type::kBrowser)
->id();
// Check that the context menu has the proper string for the action's pinned
// state.
auto check_visibility_string = [](ToolbarActionViewController* action,
std::u16string expected_label) {
ui::SimpleMenuModel* context_menu = static_cast<ui::SimpleMenuModel*>(
action->GetContextMenu(extensions::ExtensionContextMenuModel::
ContextMenuSource::kToolbarAction));
std::optional<size_t> visibility_index = context_menu->GetIndexOfCommandId(
extensions::ExtensionContextMenuModel::TOGGLE_VISIBILITY);
ASSERT_TRUE(visibility_index.has_value());
std::u16string actual_label =
context_menu->GetLabelAt(visibility_index.value());
EXPECT_EQ(expected_label, actual_label);
};
ExtensionActionViewController* const action = GetViewControllerForId(id);
ASSERT_TRUE(action);
// Default state: unpinned.
check_visibility_string(action, pin_label);
// Pin the extension; re-check.
toolbar_model()->SetActionVisibility(id, true);
check_visibility_string(action, unpin_label);
// Unpin the extension and ephemerally pop it out.
toolbar_model()->SetActionVisibility(id, false);
EXPECT_FALSE(container()->IsActionVisibleOnToolbar(id));
base::RunLoop run_loop;
container()->PopOutAction(id, run_loop.QuitClosure());
EXPECT_TRUE(container()->IsActionVisibleOnToolbar(id));
// The string should still just be "pin".
check_visibility_string(action, pin_label);
}
enum class PermissionType {
kScriptableHost,
kExplicitHost,
};
// TODO(crbug.com/40857680): This test used to be parameterized for
// PermissionType. However, parent class needs to parameterize the test for
// feature kExtensionsMenuAccessControl. Once feature is fully rolled out, we
// can go back to testing::WithParamInterface<PermissionType>.
class ExtensionActionViewControllerGrayscaleTest
: public ExtensionActionViewControllerFeatureRolloutBrowserTest {
public:
enum class ActionState {
kEnabled,
kDisabled,
};
enum class PageAccessStatus {
// The extension has been granted permission to the host.
kGranted,
// The extension had the host withheld and it has not tried to access the
// page.
kWithheld,
// The extension had the host withheld and it has been blocked when trying
// to access the page.
kBlocked,
// The extension has not been granted permissions to the host, nor was it
// withheld.
kNone,
};
enum class Coloring {
// The extension action color is grayscale.
kGrayscale,
// The extension action has full color.
kFull,
};
enum class BlockedDecoration {
// The extension is blocked, thus its action has painted decoration.
kPainted,
// The extension is not blocked, thus its action has no painted decoration.
kNotPainted,
};
ExtensionActionViewControllerGrayscaleTest() = default;
ExtensionActionViewControllerGrayscaleTest(
const ExtensionActionViewControllerGrayscaleTest&) = delete;
ExtensionActionViewControllerGrayscaleTest& operator=(
const ExtensionActionViewControllerGrayscaleTest&) = delete;
~ExtensionActionViewControllerGrayscaleTest() override = default;
scoped_refptr<const extensions::Extension> CreateExtension(
PermissionType permission_type,
const std::string& host_permission);
extensions::PermissionsData::PageAccess GetPageAccess(
content::WebContents* web_contents,
scoped_refptr<const extensions::Extension> extensions,
PermissionType permission_type);
};
scoped_refptr<const extensions::Extension>
ExtensionActionViewControllerGrayscaleTest::CreateExtension(
PermissionType permission_type,
const std::string& host_permission) {
extensions::ExtensionBuilder builder("extension");
builder.SetAction(extensions::ActionInfo::Type::kBrowser)
.SetLocation(ManifestLocation::kInternal);
switch (permission_type) {
case PermissionType::kScriptableHost:
builder.AddContentScript("script.js", {host_permission});
break;
case PermissionType::kExplicitHost:
builder.AddHostPermission(host_permission);
break;
}
return builder.Build();
}
extensions::PermissionsData::PageAccess
ExtensionActionViewControllerGrayscaleTest::GetPageAccess(
content::WebContents* web_contents,
scoped_refptr<const extensions::Extension> extension,
PermissionType permission_type) {
int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
GURL url = web_contents->GetLastCommittedURL();
switch (permission_type) {
case PermissionType::kExplicitHost:
return extension->permissions_data()->GetPageAccess(url, tab_id,
/*error=*/nullptr);
case PermissionType::kScriptableHost:
return extension->permissions_data()->GetContentScriptAccess(
url, tab_id, /*error=*/nullptr);
}
}
INSTANTIATE_TEST_SUITE_P(,
ExtensionActionViewControllerGrayscaleTest,
testing::Bool(),
[](const testing::TestParamInfo<bool>& info) {
return info.param ? "FeatureEnabled"
: "FeatureDisabled";
});
// Tests the behavior for icon grayscaling.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewControllerGrayscaleTest,
GrayscaleIcon) {
Init();
bool is_feature_enabled = GetParam();
static constexpr PermissionType permission_types[] = {
PermissionType::kScriptableHost, PermissionType::kExplicitHost};
for (auto permission_type : permission_types) {
// Create an extension with google.com as either an explicit or scriptable
// host permission.
std::string host_permission = "https://www.google.com/*";
scoped_refptr<const extensions::Extension> extension =
CreateExtension(permission_type, host_permission);
GrantActivePermissions(extension.get());
extension_registrar()->AddExtension(extension.get());
extensions::ScriptingPermissionsModifier permissions_modifier(
browser()->profile(), extension);
permissions_modifier.SetWithholdHostPermissions(true);
const GURL kHasPermissionUrl("https://www.google.com/");
const GURL kNoPermissionsUrl("https://www.chromium.org/");
// Make sure UserScriptListener doesn't hold up the navigation.
extensions::ExtensionsBrowserClient::Get()
->GetUserScriptListener()
->TriggerUserScriptsReadyForTesting(browser()->profile());
// Load up a page that we will navigate for the different test cases.
AddTab(browser(), GURL("about:blank"));
// Note: Action can only have a blocked decoration if the feature is
// disabled.
struct TestCases {
ActionState action_state;
PageAccessStatus page_access;
Coloring expected_coloring;
BlockedDecoration expected_blocked_decoration;
};
static auto test_cases = std::to_array<TestCases>({
{ActionState::kEnabled, PageAccessStatus::kNone, Coloring::kFull,
BlockedDecoration::kNotPainted},
{ActionState::kEnabled, PageAccessStatus::kWithheld, Coloring::kFull,
BlockedDecoration::kNotPainted},
{ActionState::kEnabled, PageAccessStatus::kBlocked, Coloring::kFull,
is_feature_enabled ? BlockedDecoration::kNotPainted
: BlockedDecoration::kPainted},
{ActionState::kEnabled, PageAccessStatus::kGranted, Coloring::kFull,
BlockedDecoration::kNotPainted},
{ActionState::kDisabled, PageAccessStatus::kNone, Coloring::kGrayscale,
BlockedDecoration::kNotPainted},
{ActionState::kDisabled, PageAccessStatus::kWithheld, Coloring::kFull,
BlockedDecoration::kNotPainted},
{ActionState::kDisabled, PageAccessStatus::kBlocked, Coloring::kFull,
is_feature_enabled ? BlockedDecoration::kNotPainted
: BlockedDecoration::kPainted},
{ActionState::kDisabled, PageAccessStatus::kGranted, Coloring::kFull,
BlockedDecoration::kNotPainted},
});
ExtensionActionViewController* const controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(controller);
content::WebContents* web_contents = GetActiveWebContents();
extensions::ExtensionAction* extension_action =
extensions::ExtensionActionManager::Get(browser()->profile())
->GetExtensionAction(*extension);
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
for (size_t i = 0; i < std::size(test_cases); ++i) {
SCOPED_TRACE(
base::StringPrintf("Running test case %d", static_cast<int>(i)));
const auto& test_case = test_cases[i];
// Set up the proper state for the test case.
switch (test_case.page_access) {
case PageAccessStatus::kNone: {
NavigateAndCommitActiveTab(kNoPermissionsUrl);
// Page access should be denied; verify.
extensions::PermissionsData::PageAccess page_access =
GetPageAccess(web_contents, extension, permission_type);
EXPECT_EQ(extensions::PermissionsData::PageAccess::kDenied,
page_access);
break;
}
case PageAccessStatus::kWithheld: {
NavigateAndCommitActiveTab(kHasPermissionUrl);
// Page access should already be withheld; verify.
extensions::PermissionsData::PageAccess page_access =
GetPageAccess(web_contents, extension, permission_type);
EXPECT_EQ(extensions::PermissionsData::PageAccess::kWithheld,
page_access);
break;
}
case PageAccessStatus::kBlocked:
// Navigate to a page where the permission is currently withheld and
// try to inject a script.
NavigateAndCommitActiveTab(kHasPermissionUrl);
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::mojom::RunLocation::kDocumentIdle,
base::DoNothing());
break;
case PageAccessStatus::kGranted:
// Grant the withheld requested permission and navigate there.
permissions_modifier.GrantHostPermission(kHasPermissionUrl);
NavigateAndCommitActiveTab(kHasPermissionUrl);
break;
}
// Enable or disable the action based on the test case.
extension_action->SetIsVisible(
tab_id, test_case.action_state == ActionState::kEnabled);
std::unique_ptr<IconWithBadgeImageSource> image_source =
controller->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_EQ(test_case.expected_coloring == Coloring::kGrayscale,
image_source->grayscale());
EXPECT_EQ(
test_case.expected_blocked_decoration == BlockedDecoration::kPainted,
image_source->paint_blocked_actions_decoration());
// Clean up permissions state.
if (test_case.page_access == PageAccessStatus::kGranted) {
permissions_modifier.RemoveGrantedHostPermission(kHasPermissionUrl);
}
action_runner->ClearInjectionsForTesting(*extension);
}
}
}
IN_PROC_BROWSER_TEST_P(ExtensionActionViewControllerFeatureRolloutBrowserTest,
RuntimeHostsTooltip) {
Init();
bool is_feature_enabled = GetParam();
const std::u16string kWantsAccessTooltip =
is_feature_enabled
? u"extension name\nRequesting to read & change this site"
: u"extension name\nWants access to this site";
const std::u16string kHasAccessTooltip =
is_feature_enabled ? u"extension name\nAllowed to read & change this site"
: u"extension name\nHas access to this site";
auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
"extension name", extensions::ActionInfo::Type::kBrowser,
{"https://www.google.com/*"});
extensions::ScriptingPermissionsModifier permissions_modifier(
browser()->profile(), extension);
permissions_modifier.SetWithholdHostPermissions(true);
const GURL kUrl("https://www.google.com/");
AddTab(browser(), kUrl);
ExtensionActionViewController* const controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(controller);
content::WebContents* web_contents = GetActiveWebContents();
int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
// Page access should already be withheld.
EXPECT_EQ(extensions::PermissionsData::PageAccess::kWithheld,
extension->permissions_data()->GetPageAccess(kUrl, tab_id,
/*error=*/nullptr));
EXPECT_EQ(kWantsAccessTooltip, controller->GetTooltip(web_contents));
// Request access.
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::mojom::RunLocation::kDocumentIdle,
base::DoNothing());
EXPECT_EQ(kWantsAccessTooltip, controller->GetTooltip(web_contents));
// Grant access.
action_runner->ClearInjectionsForTesting(*extension);
permissions_modifier.GrantHostPermission(kUrl);
EXPECT_EQ(kHasAccessTooltip, controller->GetTooltip(web_contents));
}
// Tests the appearance of extension actions for an extension with the activeTab
// permission and no browser or page action defined in their manifest.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewControllerFeatureRolloutBrowserTest,
ActiveTabIconAppearance) {
Init();
bool is_feature_enabled = GetParam();
const std::u16string kWantsAccessTooltip =
is_feature_enabled ? u"active tab\nRequesting to read & change this site"
: u"active tab\nWants access to this site";
const std::u16string kHasAccessTooltip =
is_feature_enabled ? u"active tab\nAllowed to read & change this site"
: u"active tab\nHas access to this site";
const std::u16string kNoAccessTooltip = u"active tab";
const GURL kUnlistedHost("https://www.example.com");
const GURL kGrantedHost("https://www.google.com");
const GURL kRestrictedHost("chrome://extensions");
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("active tab")
.AddAPIPermission("activeTab")
.AddHostPermission(kGrantedHost.spec())
.Build();
GrantActivePermissions(extension.get());
extension_registrar()->AddExtension(extension.get());
// Navigate the browser to a site the extension doesn't have explicit access
// to and verify the expected appearance.
AddTab(browser(), kUnlistedHost);
ExtensionActionViewController* const controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(controller);
content::WebContents* web_contents = GetActiveWebContents();
{
EXPECT_EQ(SiteInteraction::kActiveTab,
controller->GetSiteInteraction(web_contents));
EXPECT_TRUE(controller->IsEnabled(web_contents));
std::unique_ptr<IconWithBadgeImageSource> image_source =
controller->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
EXPECT_EQ(kWantsAccessTooltip, controller->GetTooltip(web_contents));
}
// Navigate to a site which the extension does have explicit host access to
// and verify the expected appearance.
NavigateAndCommitActiveTab(kGrantedHost);
{
EXPECT_EQ(SiteInteraction::kGranted,
controller->GetSiteInteraction(web_contents));
// This is a little unintuitive, but if an extension is using a page action
// and has not specified any declarative rules or manually changed it's
// enabled state, it can have access to a page but be in the disabled state.
// The icon will still be colored however.
EXPECT_FALSE(controller->IsEnabled(web_contents));
std::unique_ptr<IconWithBadgeImageSource> image_source =
controller->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
EXPECT_EQ(kHasAccessTooltip, controller->GetTooltip(web_contents));
}
// Navigate to a restricted URL and verify the expected appearance.
NavigateAndCommitActiveTab(kRestrictedHost);
{
EXPECT_EQ(SiteInteraction::kNone,
controller->GetSiteInteraction(web_contents));
EXPECT_FALSE(controller->IsEnabled(web_contents));
std::unique_ptr<IconWithBadgeImageSource> image_source =
controller->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_TRUE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
EXPECT_EQ(kNoAccessTooltip, controller->GetTooltip(web_contents));
}
}
// Tests that an extension with the activeTab permission has active tab site
// interaction except for restricted URLs.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewControllerFeatureRolloutBrowserTest,
GetSiteInteractionWithActiveTab) {
Init();
// Note: Not using `CreateAndAddExtensionWithGrantedHostPermissions` because
// this adds an API permission (activeTab).
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("active tab")
.SetAction(extensions::ActionInfo::Type::kAction)
.SetLocation(ManifestLocation::kInternal)
.AddAPIPermission("activeTab")
.Build();
GrantActivePermissions(extension.get());
extension_registrar()->AddExtension(extension.get());
// Navigate the browser to google.com. Since clicking the extension would
// grant access to the page, the page interaction status should show as
// "pending".
AddTab(browser(), GURL("https://www.google.com/"));
ExtensionActionViewController* const controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(controller);
content::WebContents* web_contents = GetActiveWebContents();
EXPECT_EQ(SiteInteraction::kActiveTab,
controller->GetSiteInteraction(web_contents));
// Click on the action, which grants activeTab and allows the extension to
// access the page. This changes the page interaction status to "granted".
controller->ExecuteUserAction(
ToolbarActionViewController::InvocationSource::kToolbarButton);
EXPECT_EQ(SiteInteraction::kGranted,
controller->GetSiteInteraction(web_contents));
// Now navigate to a restricted URL. Clicking the extension won't give access
// here, so the page interaction status should be "none".
NavigateAndCommitActiveTab(GURL("chrome://extensions"));
EXPECT_EQ(SiteInteraction::kNone,
controller->GetSiteInteraction(web_contents));
controller->ExecuteUserAction(
ToolbarActionViewController::InvocationSource::kToolbarButton);
EXPECT_EQ(SiteInteraction::kNone,
controller->GetSiteInteraction(web_contents));
}
// Tests that file URLs only have active tab site interaction if the extension
// has active tab permission and file URL access.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewControllerFeatureRolloutBrowserTest,
GetSiteInteractionActiveTabWithFileURL) {
// TODO(https://crbug.com/40804030): Remove this when updated to use MV3.
extensions::ScopedTestMV2Enabler mv2_enabler;
Init();
// We need to use a TestExtensionDir here to allow for the reload when giving
// an extension file URL access.
extensions::TestExtensionDir test_dir;
test_dir.WriteManifest(R"(
{
"name": "Active Tab Page Interaction with File URLs",
"description": "Testing SiteInteraction and ActiveTab on file URLs",
"version": "0.1",
"manifest_version": 2,
"browser_action": {},
"permissions": ["activeTab"]
})");
extensions::ChromeTestExtensionLoader loader(browser()->profile());
loader.set_allow_file_access(false);
scoped_refptr<const extensions::Extension> extension =
loader.LoadExtension(test_dir.UnpackedPath());
// Navigate to a file URL. The page interaction status should be "none", as
// the extension doesn't have file URL access granted. Clicking it should
// result in no change.
AddTab(browser(), GURL("file://foo"));
ExtensionActionViewController* controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(controller);
content::WebContents* web_contents = GetActiveWebContents();
EXPECT_EQ(SiteInteraction::kNone,
controller->GetSiteInteraction(web_contents));
controller->ExecuteUserAction(
ToolbarActionViewController::InvocationSource::kToolbarButton);
EXPECT_EQ(SiteInteraction::kNone,
controller->GetSiteInteraction(web_contents));
// After being granted access to file URLs the page interaction status should
// show as "pending". A click will grant activeTab, giving access to the page
// and will change the page interaction status to "active".
extensions::TestExtensionRegistryObserver observer(
extensions::ExtensionRegistry::Get(browser()->profile()),
extension->id());
extensions::util::SetAllowFileAccess(extension->id(), browser()->profile(),
true /*allow*/);
extension = observer.WaitForExtensionLoaded();
ASSERT_TRUE(extension);
// Refresh the controller as the extension has been reloaded.
controller = GetViewControllerForId(extension->id());
EXPECT_EQ(SiteInteraction::kActiveTab,
controller->GetSiteInteraction(web_contents));
controller->ExecuteUserAction(
ToolbarActionViewController::InvocationSource::kToolbarButton);
EXPECT_EQ(SiteInteraction::kGranted,
controller->GetSiteInteraction(web_contents));
}
// ExtensionActionViewController::GetIcon() can potentially be called with a
// null web contents if the tab strip model doesn't know of an active tab
// (though it's a bit unclear when this is the case).
// See https://crbug.com/888121
IN_PROC_BROWSER_TEST_P(ExtensionActionViewControllerFeatureRolloutBrowserTest,
TestGetIconWithNullWebContents) {
Init();
auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
"extension name", extensions::ActionInfo::Type::kAction,
{"https://example.com/"});
extensions::ScriptingPermissionsModifier permissions_modifier(
browser()->profile(), extension);
permissions_modifier.SetWithholdHostPermissions(true);
// Try getting an icon with no active web contents. Nothing should crash, and
// a non-empty icon should be returned.
ExtensionActionViewController* const controller =
GetViewControllerForId(extension->id());
ui::ImageModel icon = controller->GetIcon(nullptr, view_size());
EXPECT_FALSE(icon.IsEmpty());
}
class ExtensionActionViewControllerFeatureBrowserTest
: public ExtensionActionViewControllerBrowserTest {
public:
ExtensionActionViewControllerFeatureBrowserTest() {
scoped_feature_list_.InitAndEnableFeature(
extensions_features::kExtensionsMenuAccessControl);
}
~ExtensionActionViewControllerFeatureBrowserTest() override = default;
ExtensionActionViewControllerFeatureBrowserTest(
const ExtensionActionViewControllerFeatureBrowserTest&) = delete;
ExtensionActionViewControllerFeatureBrowserTest& operator=(
const ExtensionActionViewControllerFeatureBrowserTest&) = delete;
HoverCardState::SiteAccess GetHoverCardSiteAccessState(
ExtensionActionViewController* controller,
content::WebContents* web_contents) {
return controller->GetHoverCardState(web_contents).site_access;
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Tests hover card status after changing user site settings and site access.
IN_PROC_BROWSER_TEST_F(ExtensionActionViewControllerFeatureBrowserTest,
GetHoverCardStatus) {
Init();
std::string url_string = "https://example.com/";
const GURL gurl =
embedded_https_test_server().GetURL("example.com", "/simple.html");
auto extensionA = CreateAndAddExtension(
"Extension A", extensions::ActionInfo::Type::kAction);
auto extensionB = CreateAndAddExtensionWithGrantedHostPermissions(
"Extension B", extensions::ActionInfo::Type::kAction, {url_string});
auto extensionC = CreateAndAddExtensionWithGrantedHostPermissions(
"Extension c", extensions::ActionInfo::Type::kAction, {url_string});
AddTab(browser(), gurl);
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(web_contents);
auto url = url::Origin::Create(web_contents->GetLastCommittedURL());
ExtensionActionViewController* const controllerA =
GetViewControllerForId(extensionA->id());
ASSERT_TRUE(controllerA);
ExtensionActionViewController* const controllerB =
GetViewControllerForId(extensionB->id());
ASSERT_TRUE(controllerB);
ExtensionActionViewController* const controllerC =
GetViewControllerForId(extensionC->id());
ASSERT_TRUE(controllerC);
// By default, user site setting is "customize by extension" and site access
// is granted to every extension that requests them. Thus, verify extension A
// hover card state is "does not want access" and the rest is "have access".
auto* permissions_manager =
extensions::PermissionsManager::Get(browser()->profile());
ASSERT_EQ(permissions_manager->GetUserSiteSetting(url),
UserSiteSetting::kCustomizeByExtension);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerA, web_contents),
HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerB, web_contents),
HoverCardState::SiteAccess::kExtensionHasAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerC, web_contents),
HoverCardState::SiteAccess::kExtensionHasAccess);
// Withhold extension C host permissions. Verify only extension C changed
// hover card state to "requests access".
extensions::ScriptingPermissionsModifier(browser()->profile(), extensionC)
.SetWithholdHostPermissions(true);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerA, web_contents),
HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerB, web_contents),
HoverCardState::SiteAccess::kExtensionHasAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerC, web_contents),
HoverCardState::SiteAccess::kExtensionRequestsAccess);
// Block all extensions site access. Verify all extensions appear as "all
// extensions blocked" (even though extension A never requested access).
permissions_manager->UpdateUserSiteSetting(
url, UserSiteSetting::kBlockAllExtensions);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerA, web_contents),
HoverCardState::SiteAccess::kAllExtensionsBlocked);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerB, web_contents),
HoverCardState::SiteAccess::kAllExtensionsBlocked);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerC, web_contents),
HoverCardState::SiteAccess::kAllExtensionsBlocked);
// Change back to customize site access by extension. Verify extension A
// hover card state is "does not want access", extension B is "has access" and
// extension C is "requests access".
permissions_manager->UpdateUserSiteSetting(
url, UserSiteSetting::kCustomizeByExtension);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerA, web_contents),
HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerB, web_contents),
HoverCardState::SiteAccess::kExtensionHasAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerC, web_contents),
HoverCardState::SiteAccess::kExtensionRequestsAccess);
}
// Tests correct tooltip text after changing user site settings and site access.
IN_PROC_BROWSER_TEST_F(ExtensionActionViewControllerFeatureBrowserTest,
GetTooltip) {
Init();
std::u16string extension_name = u"Extension";
std::string requested_url_string = "https://a.com/";
const GURL requested_gurl =
embedded_https_test_server().GetURL("a.com", "/simple.html");
const GURL not_requested_gurl =
embedded_https_test_server().GetURL("b.com", "/simple.html");
auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
base::UTF16ToUTF8(extension_name), extensions::ActionInfo::Type::kAction,
{requested_url_string});
// Navigate to a site the extension requests access to.
AddTab(browser(), requested_gurl);
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(web_contents);
auto requested_url = url::Origin::Create(web_contents->GetLastCommittedURL());
ExtensionActionViewController* const controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(controller);
// By default, user site setting is "customize by extension" and site access
// is granted to every extension that requests them. Verify extension tooltip
// is "has access".
auto* permissions_manager =
extensions::PermissionsManager::Get(browser()->profile());
ASSERT_EQ(permissions_manager->GetUserSiteSetting(requested_url),
UserSiteSetting::kCustomizeByExtension);
EXPECT_EQ(
controller->GetTooltip(web_contents),
base::JoinString(
{extension_name,
l10n_util::GetStringUTF16(
IDS_EXTENSIONS_MENU_MAIN_PAGE_EXTENSION_BUTTON_HAS_ACCESS_TOOLTIP)},
u"\n"));
// Withhold extension host permissions. Verify extension tooltip is "requests
// access".
extensions::ScriptingPermissionsModifier(browser()->profile(), extension)
.SetWithholdHostPermissions(true);
EXPECT_EQ(
controller->GetTooltip(web_contents),
base::JoinString(
{extension_name,
l10n_util::GetStringUTF16(
IDS_EXTENSIONS_MENU_MAIN_PAGE_EXTENSION_BUTTON_REQUESTS_TOOLTIP)},
u"\n"));
// Block all extensions access to requested.com. Verify extension tooltip is
// "blocked access".
permissions_manager->UpdateUserSiteSetting(
requested_url, UserSiteSetting::kBlockAllExtensions);
EXPECT_EQ(
controller->GetTooltip(web_contents),
base::JoinString(
{extension_name,
l10n_util::GetStringUTF16(
IDS_EXTENSIONS_MENU_MAIN_PAGE_EXTENSION_BUTTON_BLOCKED_ACCESS_TOOLTIP)},
u"\n"));
// Navigate to a site that the extension didn't request access to.
AddTab(browser(), not_requested_gurl);
web_contents = GetActiveWebContents();
ASSERT_TRUE(web_contents);
auto non_requested_url =
url::Origin::Create(web_contents->GetLastCommittedURL());
// By default, user site setting is "customize by extension". Verify extension
// tooltip is just the extension name since extension didn't request access
// to this site.
ASSERT_EQ(permissions_manager->GetUserSiteSetting(non_requested_url),
UserSiteSetting::kCustomizeByExtension);
EXPECT_EQ(controller->GetTooltip(web_contents), extension_name);
// Block all extensions access to non-requested.com. Verify extension tooltip
// is "blocked access" regardless of extension not requesting access to this
// site.
permissions_manager->UpdateUserSiteSetting(
non_requested_url, UserSiteSetting::kBlockAllExtensions);
EXPECT_EQ(
controller->GetTooltip(web_contents),
base::JoinString(
{extension_name,
l10n_util::GetStringUTF16(
IDS_EXTENSIONS_MENU_MAIN_PAGE_EXTENSION_BUTTON_BLOCKED_ACCESS_TOOLTIP)},
u"\n"));
}
class ExtensionActionViewControllerFeatureWithPermittedSitesBrowserTest
: public ExtensionActionViewControllerFeatureBrowserTest {
public:
ExtensionActionViewControllerFeatureWithPermittedSitesBrowserTest() {
scoped_feature_list_.InitAndEnableFeature(
extensions_features::kExtensionsMenuAccessControlWithPermittedSites);
}
~ExtensionActionViewControllerFeatureWithPermittedSitesBrowserTest()
override = default;
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Tests hover card status after changing user site settings and site access.
IN_PROC_BROWSER_TEST_F(
ExtensionActionViewControllerFeatureWithPermittedSitesBrowserTest,
GetHoverCardStatus) {
Init();
std::string url_string = "https://example.com/";
const GURL gurl =
embedded_https_test_server().GetURL("example.com", "/simple.html");
auto extensionA = CreateAndAddExtension(
"Extension A", extensions::ActionInfo::Type::kAction);
auto extensionB = CreateAndAddExtensionWithGrantedHostPermissions(
"Extension B", extensions::ActionInfo::Type::kAction, {url_string});
auto extensionC = CreateAndAddExtensionWithGrantedHostPermissions(
"Extension c", extensions::ActionInfo::Type::kAction, {url_string});
AddTab(browser(), gurl);
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(web_contents);
auto url = url::Origin::Create(web_contents->GetLastCommittedURL());
ExtensionActionViewController* const controllerA =
GetViewControllerForId(extensionA->id());
ASSERT_TRUE(controllerA);
ExtensionActionViewController* const controllerB =
GetViewControllerForId(extensionB->id());
ASSERT_TRUE(controllerB);
ExtensionActionViewController* const controllerC =
GetViewControllerForId(extensionC->id());
ASSERT_TRUE(controllerC);
// By default, user site setting is "customize by extension" and site access
// is granted to every extension that requests them. Thus, verify extension A
// hover card state is "does not want access" and the rest is "have access".
auto* permissions_manager =
extensions::PermissionsManager::Get(browser()->profile());
ASSERT_EQ(permissions_manager->GetUserSiteSetting(url),
UserSiteSetting::kCustomizeByExtension);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerA, web_contents),
HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerB, web_contents),
HoverCardState::SiteAccess::kExtensionHasAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerC, web_contents),
HoverCardState::SiteAccess::kExtensionHasAccess);
// Withhold extension C host permissions. Verify only extension C changed
// hover card state to "requests access".
extensions::ScriptingPermissionsModifier(browser()->profile(), extensionC)
.SetWithholdHostPermissions(true);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerA, web_contents),
HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerB, web_contents),
HoverCardState::SiteAccess::kExtensionHasAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerC, web_contents),
HoverCardState::SiteAccess::kExtensionRequestsAccess);
// Grant all extensions site access. Verify extension A hover card state is
// "does not want access" and extensions B and C is "all extensions allowed".
permissions_manager->UpdateUserSiteSetting(
url, UserSiteSetting::kGrantAllExtensions);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerA, web_contents),
HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerB, web_contents),
HoverCardState::SiteAccess::kAllExtensionsAllowed);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerC, web_contents),
HoverCardState::SiteAccess::kAllExtensionsAllowed);
}
// Test that the extension action is enabled if opening the side panel on icon
// click is enabled and the extension has a side panel for the current tab.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewControllerFeatureRolloutBrowserTest,
ActionEnabledIfSidePanelPresent) {
Init();
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("just side panel")
.SetLocation(ManifestLocation::kInternal)
.AddAPIPermission("sidePanel")
.Build();
GrantActivePermissions(extension.get());
extension_registrar()->AddExtension(extension.get());
side_panel_service()->SetOpenSidePanelOnIconClick(extension->id(), true);
ExtensionActionViewController* const action_controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(action_controller);
EXPECT_EQ(extension.get(), action_controller->extension());
AddTab(browser(), GURL("https://www.chromium.org/"));
content::WebContents* web_contents = GetActiveWebContents();
int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
// If the preference is true but there is no side panel for the current tab,
// the action should be disabled.
std::unique_ptr<IconWithBadgeImageSource> image_source =
action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_TRUE(image_source->grayscale());
EXPECT_FALSE(action_controller->IsEnabled(web_contents));
// Set a side panel for the current tab. This should enable the extension's
// action.
extensions::api::side_panel::PanelOptions options;
options.enabled = true;
options.path = "panel.html";
options.tab_id = tab_id;
side_panel_service()->SetOptions(*extension, std::move(options));
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_TRUE(action_controller->IsEnabled(web_contents));
// Setting the preference to false should disable the extension's action.
side_panel_service()->SetOpenSidePanelOnIconClick(extension->id(), false);
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_TRUE(image_source->grayscale());
EXPECT_FALSE(action_controller->IsEnabled(web_contents));
}