blob: 3dd5418c257641cb3efc67216ec3c9a5d5e1b333 [file] [log] [blame]
// Copyright 2015 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 "chrome/browser/ui/extensions/extension_action_view_controller.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/json/json_reader.h"
#include "base/run_loop.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/extensions/extension_action.h"
#include "chrome/browser/extensions/extension_action_manager.h"
#include "chrome/browser/extensions/extension_action_runner.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/scripting_permissions_modifier.h"
#include "chrome/browser/sessions/session_tab_helper.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_bar.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_bar_unittest.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/user_script.h"
#include "ui/base/l10n/l10n_util.h"
// Tests the icon appearance of extension actions with the toolbar redesign.
// Extensions that don't want to run should have their icons grayscaled.
// Overflowed extensions that want to run should have an additional decoration.
TEST_P(ToolbarActionsBarUnitTest, ExtensionActionWantsToRunAppearance) {
CreateAndAddExtension("extension",
extensions::ExtensionBuilder::ActionType::PAGE_ACTION);
EXPECT_EQ(1u, toolbar_actions_bar()->GetIconCount());
EXPECT_EQ(0u, overflow_bar()->GetIconCount());
AddTab(browser(), GURL("chrome://newtab"));
const gfx::Size size = toolbar_actions_bar()->GetViewSize();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ExtensionActionViewController* action =
static_cast<ExtensionActionViewController*>(
toolbar_actions_bar()->GetActions()[0]);
std::unique_ptr<IconWithBadgeImageSource> image_source =
action->GetIconImageSourceForTesting(web_contents, size);
EXPECT_TRUE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
SetActionWantsToRunOnTab(action->extension_action(), web_contents, true);
image_source = action->GetIconImageSourceForTesting(web_contents, size);
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
toolbar_model()->SetVisibleIconCount(0u);
EXPECT_EQ(0u, toolbar_actions_bar()->GetIconCount());
EXPECT_EQ(1u, overflow_bar()->GetIconCount());
action = static_cast<ExtensionActionViewController*>(
overflow_bar()->GetActions()[0]);
image_source = action->GetIconImageSourceForTesting(web_contents, size);
EXPECT_FALSE(image_source->grayscale());
EXPECT_TRUE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
SetActionWantsToRunOnTab(action->extension_action(), web_contents, false);
image_source = action->GetIconImageSourceForTesting(web_contents, size);
EXPECT_TRUE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
}
TEST_P(ToolbarActionsBarUnitTest, ExtensionActionBlockedActions) {
// Blocked actions are only present with the runtime host permissions feature.
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
extensions_features::kRuntimeHostPermissions);
scoped_refptr<const extensions::Extension> browser_action_ext =
extensions::ExtensionBuilder("browser action")
.SetAction(extensions::ExtensionBuilder::ActionType::BROWSER_ACTION)
.SetLocation(extensions::Manifest::INTERNAL)
.AddPermission("https://www.google.com/*")
.Build();
extensions::ExtensionService* service =
extensions::ExtensionSystem::Get(profile())->extension_service();
service->GrantPermissions(browser_action_ext.get());
service->AddExtension(browser_action_ext.get());
extensions::ScriptingPermissionsModifier permissions_modifier_browser_ext(
profile(), browser_action_ext);
permissions_modifier_browser_ext.SetWithholdHostPermissions(true);
ASSERT_EQ(1u, toolbar_actions_bar()->GetIconCount());
AddTab(browser(), GURL("https://www.google.com/"));
ExtensionActionViewController* browser_action =
static_cast<ExtensionActionViewController*>(
toolbar_actions_bar()->GetActions()[0]);
EXPECT_EQ(browser_action_ext.get(), browser_action->extension());
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(web_contents);
const gfx::Size size = toolbar_actions_bar()->GetViewSize();
std::unique_ptr<IconWithBadgeImageSource> image_source =
browser_action->GetIconImageSourceForTesting(web_contents, size);
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
ASSERT_TRUE(action_runner);
action_runner->RequestScriptInjectionForTesting(
browser_action_ext.get(), extensions::UserScript::DOCUMENT_IDLE,
base::DoNothing());
image_source =
browser_action->GetIconImageSourceForTesting(web_contents, size);
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_TRUE(image_source->paint_blocked_actions_decoration());
action_runner->RunForTesting(browser_action_ext.get());
image_source =
browser_action->GetIconImageSourceForTesting(web_contents, size);
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
scoped_refptr<const extensions::Extension> page_action_ext =
extensions::ExtensionBuilder("page action")
.SetAction(extensions::ExtensionBuilder::ActionType::PAGE_ACTION)
.SetLocation(extensions::Manifest::INTERNAL)
.AddPermission("https://www.google.com/*")
.Build();
service->GrantPermissions(page_action_ext.get());
service->AddExtension(page_action_ext.get());
extensions::ScriptingPermissionsModifier permissions_modifier_page_action(
profile(), page_action_ext);
permissions_modifier_page_action.SetWithholdHostPermissions(true);
ASSERT_EQ(2u, toolbar_actions_bar()->GetIconCount());
ExtensionActionViewController* page_action =
static_cast<ExtensionActionViewController*>(
toolbar_actions_bar()->GetActions()[1]);
EXPECT_EQ(browser_action_ext.get(), browser_action->extension());
image_source = page_action->GetIconImageSourceForTesting(web_contents, size);
EXPECT_TRUE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
action_runner->RequestScriptInjectionForTesting(
page_action_ext.get(), extensions::UserScript::DOCUMENT_IDLE,
base::DoNothing());
image_source = page_action->GetIconImageSourceForTesting(web_contents, size);
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_TRUE(image_source->paint_blocked_actions_decoration());
// Overflow the page action and set the page action as wanting to run. We
// shouldn't show the page action decoration because we are showing the
// blocked action decoration (and should only show one at a time).
toolbar_model()->SetVisibleIconCount(0u);
EXPECT_EQ(0u, toolbar_actions_bar()->GetIconCount());
EXPECT_EQ(2u, overflow_bar()->GetIconCount());
ExtensionActionViewController* overflow_page_action =
static_cast<ExtensionActionViewController*>(
overflow_bar()->GetActions()[1]);
SetActionWantsToRunOnTab(overflow_page_action->extension_action(),
web_contents, true);
image_source =
overflow_page_action->GetIconImageSourceForTesting(web_contents, size);
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_TRUE(image_source->paint_blocked_actions_decoration());
SetActionWantsToRunOnTab(overflow_page_action->extension_action(),
web_contents, false);
toolbar_model()->SetVisibleIconCount(2u);
action_runner->RunForTesting(page_action_ext.get());
image_source = page_action->GetIconImageSourceForTesting(web_contents, size);
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
}
TEST_P(ToolbarActionsBarUnitTest, ExtensionActionContextMenu) {
CreateAndAddExtension(
"extension", extensions::ExtensionBuilder::ActionType::BROWSER_ACTION);
EXPECT_EQ(1u, toolbar_actions_bar()->GetIconCount());
// Check that the context menu has the proper string for the action's position
// (in the main toolbar, in the overflow container, or temporarily popped
// out).
auto check_visibility_string = [](ToolbarActionViewController* action,
int expected_visibility_string) {
ui::SimpleMenuModel* context_menu =
static_cast<ui::SimpleMenuModel*>(action->GetContextMenu());
int visibility_index = context_menu->GetIndexOfCommandId(
extensions::ExtensionContextMenuModel::TOGGLE_VISIBILITY);
ASSERT_GE(visibility_index, 0);
base::string16 visibility_label =
context_menu->GetLabelAt(visibility_index);
EXPECT_EQ(l10n_util::GetStringUTF16(expected_visibility_string),
visibility_label);
};
check_visibility_string(toolbar_actions_bar()->GetActions()[0],
IDS_EXTENSIONS_HIDE_BUTTON_IN_MENU);
toolbar_model()->SetVisibleIconCount(0u);
check_visibility_string(overflow_bar()->GetActions()[0],
IDS_EXTENSIONS_SHOW_BUTTON_IN_TOOLBAR);
base::RunLoop run_loop;
toolbar_actions_bar()->PopOutAction(toolbar_actions_bar()->GetActions()[0],
false,
run_loop.QuitClosure());
run_loop.Run();
check_visibility_string(toolbar_actions_bar()->GetActions()[0],
IDS_EXTENSIONS_KEEP_BUTTON_IN_TOOLBAR);
}
class ExtensionActionViewControllerGrayscaleTest
: public ToolbarActionsBarUnitTest {
public:
enum class PermissionType {
kScriptableHost,
kExplicitHost,
};
ExtensionActionViewControllerGrayscaleTest() {}
~ExtensionActionViewControllerGrayscaleTest() override = default;
void RunGrayscaleTest(PermissionType permission_type);
private:
scoped_refptr<const extensions::Extension> CreateExtension(
PermissionType permission_type);
DISALLOW_COPY_AND_ASSIGN(ExtensionActionViewControllerGrayscaleTest);
};
void ExtensionActionViewControllerGrayscaleTest::RunGrayscaleTest(
PermissionType permission_type) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
extensions_features::kRuntimeHostPermissions);
scoped_refptr<const extensions::Extension> extension =
CreateExtension(permission_type);
extensions::ExtensionService* service =
extensions::ExtensionSystem::Get(profile())->extension_service();
service->GrantPermissions(extension.get());
service->AddExtension(extension.get());
extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
extension);
permissions_modifier.SetWithholdHostPermissions(true);
ASSERT_EQ(1u, toolbar_actions_bar()->GetIconCount());
const GURL kUrl("https://www.google.com/");
AddTab(browser(), kUrl);
enum class ActionState {
kEnabled,
kDisabled,
};
enum class PageAccess {
kGranted,
kPending,
kNone,
};
enum class Opacity {
kGrayscale,
kFull,
};
enum class BlockedActions {
kPainted,
kNotPainted,
};
struct {
ActionState action_state;
PageAccess page_access;
Opacity expected_opacity;
BlockedActions expected_blocked_actions;
} test_cases[] = {
{ActionState::kEnabled, PageAccess::kNone, Opacity::kFull,
BlockedActions::kNotPainted},
{ActionState::kEnabled, PageAccess::kPending, Opacity::kFull,
BlockedActions::kPainted},
{ActionState::kEnabled, PageAccess::kGranted, Opacity::kFull,
BlockedActions::kNotPainted},
{ActionState::kDisabled, PageAccess::kNone, Opacity::kGrayscale,
BlockedActions::kNotPainted},
{ActionState::kDisabled, PageAccess::kPending, Opacity::kFull,
BlockedActions::kPainted},
{ActionState::kDisabled, PageAccess::kGranted, Opacity::kFull,
BlockedActions::kNotPainted},
};
auto* controller = static_cast<ExtensionActionViewController*>(
toolbar_actions_bar()->GetActions()[0]);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ExtensionAction* extension_action =
extensions::ExtensionActionManager::Get(profile())->GetExtensionAction(
*extension);
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
int tab_id = SessionTabHelper::IdForTab(web_contents).id();
const gfx::Size kSize = toolbar_actions_bar()->GetViewSize();
for (size_t i = 0; i < base::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.
extension_action->SetIsVisible(
tab_id, test_case.action_state == ActionState::kEnabled);
switch (test_case.page_access) {
case PageAccess::kNone: {
// Page access should already be withheld; verify.
extensions::PermissionsData::PageAccess page_access =
extensions::PermissionsData::PageAccess::kDenied;
switch (permission_type) {
case PermissionType::kExplicitHost:
page_access = extension->permissions_data()->GetPageAccess(
kUrl, tab_id, /*error=*/nullptr);
break;
case PermissionType::kScriptableHost:
page_access = extension->permissions_data()->GetContentScriptAccess(
kUrl, tab_id, /*error=*/nullptr);
break;
}
EXPECT_EQ(extensions::PermissionsData::PageAccess::kWithheld,
page_access);
break;
}
case PageAccess::kPending:
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::UserScript::DOCUMENT_IDLE,
base::DoNothing());
break;
case PageAccess::kGranted:
permissions_modifier.GrantHostPermission(kUrl);
break;
}
std::unique_ptr<IconWithBadgeImageSource> image_source =
controller->GetIconImageSourceForTesting(web_contents, kSize);
EXPECT_EQ(test_case.expected_opacity == Opacity::kGrayscale,
image_source->grayscale());
EXPECT_EQ(test_case.expected_blocked_actions == BlockedActions::kPainted,
image_source->paint_blocked_actions_decoration());
// Clean up permissions state.
if (test_case.page_access == PageAccess::kGranted)
permissions_modifier.RemoveGrantedHostPermission(kUrl);
action_runner->ClearInjectionsForTesting(*extension);
}
}
scoped_refptr<const extensions::Extension>
ExtensionActionViewControllerGrayscaleTest::CreateExtension(
PermissionType permission_type) {
extensions::ExtensionBuilder builder("extension");
builder.SetAction(extensions::ExtensionBuilder::ActionType::BROWSER_ACTION)
.SetLocation(extensions::Manifest::INTERNAL);
switch (permission_type) {
case PermissionType::kScriptableHost: {
std::unique_ptr<base::Value> content_scripts = base::JSONReader::Read(
R"([{
"matches": ["https://www.google.com/*"],
"js": ["script.js"]
}])");
builder.SetManifestKey("content_scripts", std::move(content_scripts));
break;
}
case PermissionType::kExplicitHost:
builder.AddPermission("https://www.google.com/*");
break;
}
return builder.Build();
}
// Tests the behavior for icon grayscaling with the runtime host permissions
// feature enabled. Ideally, these would be a single parameterized test, but
// toolbar tests are already parameterized with the UI mode.
TEST_P(ExtensionActionViewControllerGrayscaleTest,
GrayscaleIcon_ExplicitHosts) {
RunGrayscaleTest(PermissionType::kExplicitHost);
}
TEST_P(ExtensionActionViewControllerGrayscaleTest,
GrayscaleIcon_ScriptableHosts) {
RunGrayscaleTest(PermissionType::kScriptableHost);
}
INSTANTIATE_TEST_CASE_P(
,
ExtensionActionViewControllerGrayscaleTest,
testing::Values(ui::MaterialDesignController::MATERIAL_NORMAL,
ui::MaterialDesignController::MATERIAL_HYBRID,
ui::MaterialDesignController::MATERIAL_TOUCH_OPTIMIZED,
ui::MaterialDesignController::MATERIAL_REFRESH,
ui::MaterialDesignController::MATERIAL_TOUCH_REFRESH));
TEST_P(ToolbarActionsBarUnitTest, RuntimeHostsTooltip) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
extensions_features::kRuntimeHostPermissions);
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("extension name")
.SetAction(extensions::ExtensionBuilder::ActionType::BROWSER_ACTION)
.SetLocation(extensions::Manifest::INTERNAL)
.AddPermission("https://www.google.com/*")
.Build();
extensions::ExtensionService* service =
extensions::ExtensionSystem::Get(profile())->extension_service();
service->GrantPermissions(extension.get());
service->AddExtension(extension.get());
extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
extension);
permissions_modifier.SetWithholdHostPermissions(true);
ASSERT_EQ(1u, toolbar_actions_bar()->GetIconCount());
const GURL kUrl("https://www.google.com/");
AddTab(browser(), kUrl);
auto* controller = static_cast<ExtensionActionViewController*>(
toolbar_actions_bar()->GetActions()[0]);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
int tab_id = 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("extension name",
base::UTF16ToUTF8(controller->GetTooltip(web_contents)));
// Request access.
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::UserScript::DOCUMENT_IDLE,
base::DoNothing());
EXPECT_EQ("extension name\nWants access to this site",
base::UTF16ToUTF8(controller->GetTooltip(web_contents)));
// Grant access.
action_runner->ClearInjectionsForTesting(*extension);
permissions_modifier.GrantHostPermission(kUrl);
EXPECT_EQ("extension name\nHas access to this site",
base::UTF16ToUTF8(controller->GetTooltip(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
TEST_P(ToolbarActionsBarUnitTest, TestGetIconWithNullWebContents) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
extensions_features::kRuntimeHostPermissions);
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("extension name")
.SetAction(extensions::ExtensionBuilder::ActionType::BROWSER_ACTION)
.AddPermission("https://example.com/")
.Build();
extensions::ExtensionService* service =
extensions::ExtensionSystem::Get(profile())->extension_service();
service->GrantPermissions(extension.get());
service->AddExtension(extension.get());
extensions::ScriptingPermissionsModifier permissions_modifier(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.
ToolbarActionViewController* controller =
toolbar_actions_bar()->GetActions()[0];
gfx::Image icon =
controller->GetIcon(nullptr, toolbar_actions_bar()->GetViewSize());
EXPECT_FALSE(icon.IsEmpty());
}