blob: 04d05c2c792405c1d342bf403ca0b6a9d6ff6b87 [file] [log] [blame]
// Copyright 2019 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/views/extensions/extensions_menu_view.h"
#include <memory>
#include <string>
#include <vector>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/user_action_tester.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/extension_action_test_util.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/load_error_reporter.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
#include "chrome/browser/ui/views/extensions/extensions_menu_button.h"
#include "chrome/browser/ui/views/extensions/extensions_menu_item_view.h"
#include "chrome/browser/ui/views/extensions/extensions_toolbar_button.h"
#include "chrome/browser/ui/views/extensions/extensions_toolbar_container.h"
#include "chrome/browser/ui/views/extensions/extensions_toolbar_unittest.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/common/extension.h"
#include "extensions/test/test_extension_dir.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/events/event.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/layout/animating_layout_manager_test_util.h"
#include "ui/views/test/ax_event_counter.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
namespace {
// Manages a Browser instance created by BrowserWithTestWindowTest beyond the
// default instance it creates in SetUp.
class AdditionalBrowser {
public:
explicit AdditionalBrowser(std::unique_ptr<Browser> browser)
: browser_(std::move(browser)),
browser_view_(BrowserView::GetBrowserViewForBrowser(browser_.get())) {}
~AdditionalBrowser() {
// Tear down |browser_|, similar to TestWithBrowserView::TearDown.
browser_.release();
browser_view_->GetWidget()->CloseNow();
}
ExtensionsToolbarContainer* extensions_container() {
return browser_view_->toolbar()->extensions_container();
}
private:
std::unique_ptr<Browser> browser_;
raw_ptr<BrowserView> browser_view_;
};
} // namespace
class ExtensionsMenuViewUnitTest : public ExtensionsToolbarUnitTest {
public:
ExtensionsMenuViewUnitTest()
: allow_extension_menu_instances_(
ExtensionsMenuView::AllowInstancesForTesting()) {}
ExtensionsMenuViewUnitTest(const ExtensionsMenuViewUnitTest&) = delete;
ExtensionsMenuViewUnitTest& operator=(const ExtensionsMenuViewUnitTest&) =
delete;
~ExtensionsMenuViewUnitTest() override = default;
// ExtensionsToolbarUnitTest:
void SetUp() override;
scoped_refptr<const extensions::Extension> InstallExtensionAndLayout(
const std::string& name);
ExtensionsMenuView* extensions_menu() {
return ExtensionsMenuView::GetExtensionsMenuViewForTesting();
}
// Asserts there is exactly 1 menu item and then returns it.
InstalledExtensionMenuItemView* GetOnlyMenuItem();
void ClickPinButton(InstalledExtensionMenuItemView* menu_item) const;
void ClickContextMenuButton(InstalledExtensionMenuItemView* menu_item) const;
std::vector<ToolbarActionView*> GetPinnedExtensionViews();
InstalledExtensionMenuItemView* GetInstalledExtensionMenuItemView(
const std::string& name);
// Returns a list of the names of the currently pinned extensions, in order
// from left to right.
std::vector<std::string> GetPinnedExtensionNames();
// Since this is a unittest (and doesn't have as much "real" rendering),
// the ExtensionsMenuView sometimes needs a nudge to re-layout the views.
void LayoutMenuIfNecessary();
private:
base::AutoReset<bool> allow_extension_menu_instances_;
};
void ExtensionsMenuViewUnitTest::SetUp() {
ExtensionsToolbarUnitTest::SetUp();
ExtensionsMenuView::ShowBubble(extensions_container()->GetExtensionsButton(),
browser(), extensions_container(), true);
}
scoped_refptr<const extensions::Extension>
ExtensionsMenuViewUnitTest::InstallExtensionAndLayout(const std::string& name) {
scoped_refptr<const extensions::Extension> extension = InstallExtension(name);
LayoutMenuIfNecessary();
return extension;
}
InstalledExtensionMenuItemView* ExtensionsMenuViewUnitTest::GetOnlyMenuItem() {
base::flat_set<InstalledExtensionMenuItemView*> menu_items =
extensions_menu()->extensions_menu_items_for_testing();
if (menu_items.size() != 1u) {
ADD_FAILURE() << "Not exactly one item; size is: " << menu_items.size();
return nullptr;
}
return *menu_items.begin();
}
void ExtensionsMenuViewUnitTest::ClickPinButton(
InstalledExtensionMenuItemView* menu_item) const {
ClickButton(menu_item->pin_button_for_testing());
}
void ExtensionsMenuViewUnitTest::ClickContextMenuButton(
InstalledExtensionMenuItemView* menu_item) const {
ClickButton(menu_item->context_menu_button_for_testing());
}
std::vector<ToolbarActionView*>
ExtensionsMenuViewUnitTest::GetPinnedExtensionViews() {
std::vector<ToolbarActionView*> result;
for (views::View* child : extensions_container()->children()) {
// Ensure we don't downcast the ExtensionsToolbarButton.
if (views::IsViewClass<ToolbarActionView>(child)) {
ToolbarActionView* const action = static_cast<ToolbarActionView*>(child);
#if BUILDFLAG(IS_MAC)
// TODO(crbug.com/1045212): Use IsActionVisibleOnToolbar() because it
// queries the underlying model and not GetVisible(), as that relies on an
// animation running, which is not reliable in unit tests on Mac.
const bool is_visible = extensions_container()->IsActionVisibleOnToolbar(
action->view_controller());
#else
const bool is_visible = action->GetVisible();
#endif
if (is_visible)
result.push_back(action);
}
}
return result;
}
InstalledExtensionMenuItemView*
ExtensionsMenuViewUnitTest::GetInstalledExtensionMenuItemView(
const std::string& name) {
base::flat_set<InstalledExtensionMenuItemView*> menu_items =
extensions_menu()->extensions_menu_items_for_testing();
auto iter = base::ranges::find_if(
menu_items, [name](InstalledExtensionMenuItemView* item) {
return base::UTF16ToUTF8(item->view_controller()->GetActionName()) ==
name;
});
return iter == menu_items.end() ? nullptr : *iter;
}
std::vector<std::string> ExtensionsMenuViewUnitTest::GetPinnedExtensionNames() {
std::vector<ToolbarActionView*> views = GetPinnedExtensionViews();
std::vector<std::string> result;
result.resize(views.size());
std::transform(
views.begin(), views.end(), result.begin(), [](ToolbarActionView* view) {
return base::UTF16ToUTF8(view->view_controller()->GetActionName());
});
return result;
}
void ExtensionsMenuViewUnitTest::LayoutMenuIfNecessary() {
extensions_menu()->GetWidget()->LayoutRootViewIfNecessary();
}
TEST_F(ExtensionsMenuViewUnitTest, ExtensionsAreShownInTheMenu) {
// To start, there should be no extensions in the menu.
EXPECT_EQ(0u, extensions_menu()->extensions_menu_items_for_testing().size());
// Add an extension, and verify that it's added to the menu.
constexpr char kExtensionName[] = "Test 1";
InstallExtensionAndLayout(kExtensionName);
{
base::flat_set<InstalledExtensionMenuItemView*> menu_items =
extensions_menu()->extensions_menu_items_for_testing();
ASSERT_EQ(1u, menu_items.size());
EXPECT_EQ(kExtensionName,
base::UTF16ToUTF8((*menu_items.begin())
->primary_action_button_for_testing()
->label_text_for_testing()));
}
}
TEST_F(ExtensionsMenuViewUnitTest, ExtensionsAreSortedInTheMenu) {
constexpr char kExtensionZName[] = "Z Extension";
InstallExtension(kExtensionZName);
constexpr char kExtensionAName[] = "A Extension";
InstallExtension(kExtensionAName);
constexpr char kExtensionBName[] = "b Extension";
InstallExtension(kExtensionBName);
constexpr char kExtensionCName[] = "C Extension";
InstallExtensionAndLayout(kExtensionCName);
std::vector<InstalledExtensionMenuItemView*> menu_items =
ExtensionsMenuView::GetSortedItemsForSectionForTesting(
extensions::SitePermissionsHelper::SiteInteraction::kNone);
ASSERT_EQ(4u, menu_items.size());
std::vector<std::string> item_names;
for (auto* menu_item : menu_items) {
item_names.push_back(
base::UTF16ToUTF8(menu_item->primary_action_button_for_testing()
->label_text_for_testing()));
}
// Basic std::sort would do A,C,Z,b however we want A,b,C,Z
EXPECT_THAT(item_names,
testing::ElementsAre(kExtensionAName, kExtensionBName,
kExtensionCName, kExtensionZName));
}
TEST_F(ExtensionsMenuViewUnitTest, PinnedExtensionAppearsInToolbar) {
constexpr char kName[] = "Test Name";
InstallExtensionAndLayout(kName);
InstalledExtensionMenuItemView* menu_item = GetOnlyMenuItem();
ASSERT_TRUE(menu_item);
ToolbarActionViewController* controller = menu_item->view_controller();
EXPECT_FALSE(extensions_container()->IsActionVisibleOnToolbar(controller));
EXPECT_THAT(GetPinnedExtensionNames(), testing::IsEmpty());
ClickPinButton(menu_item);
WaitForAnimation();
EXPECT_TRUE(extensions_container()->IsActionVisibleOnToolbar(controller));
EXPECT_THAT(GetPinnedExtensionNames(), testing::ElementsAre(kName));
ClickPinButton(menu_item); // Unpin.
WaitForAnimation();
EXPECT_FALSE(extensions_container()->IsActionVisibleOnToolbar(
menu_item->view_controller()));
EXPECT_THAT(GetPinnedExtensionNames(), testing::IsEmpty());
}
TEST_F(ExtensionsMenuViewUnitTest, PinnedExtensionAppearsInAnotherWindow) {
InstallExtensionAndLayout("Test Name");
AdditionalBrowser browser2(
CreateBrowser(browser()->profile(), browser()->type(),
/* hosted_app */ false, /* browser_window */ nullptr));
InstalledExtensionMenuItemView* menu_item = GetOnlyMenuItem();
ASSERT_TRUE(menu_item);
ClickPinButton(menu_item);
// Window that was already open gets the pinned extension.
EXPECT_TRUE(browser2.extensions_container()->IsActionVisibleOnToolbar(
menu_item->view_controller()));
AdditionalBrowser browser3(
CreateBrowser(browser()->profile(), browser()->type(),
/* hosted_app */ false, /* browser_window */ nullptr));
// Brand-new window also gets the pinned extension.
EXPECT_TRUE(browser3.extensions_container()->IsActionVisibleOnToolbar(
menu_item->view_controller()));
}
TEST_F(ExtensionsMenuViewUnitTest, PinnedExtensionRemovedWhenDisabled) {
constexpr char kName[] = "Test Name";
const extensions::ExtensionId id = InstallExtensionAndLayout(kName)->id();
{
InstalledExtensionMenuItemView* menu_item = GetOnlyMenuItem();
ASSERT_TRUE(menu_item);
ClickPinButton(menu_item);
}
DisableExtension(id);
WaitForAnimation();
ASSERT_EQ(0u, extensions_menu()->extensions_menu_items_for_testing().size());
EXPECT_THAT(GetPinnedExtensionNames(), testing::IsEmpty());
EnableExtension(id);
WaitForAnimation();
ASSERT_EQ(1u, extensions_menu()->extensions_menu_items_for_testing().size());
EXPECT_THAT(GetPinnedExtensionNames(), testing::ElementsAre(kName));
}
TEST_F(ExtensionsMenuViewUnitTest, PinnedExtensionLayout) {
for (int i = 0; i < 3; i++)
InstallExtensionAndLayout(base::StringPrintf("Test %d", i));
for (auto* menu_item :
extensions_menu()->extensions_menu_items_for_testing()) {
ClickPinButton(menu_item);
}
WaitForAnimation();
std::vector<ToolbarActionView*> action_views = GetPinnedExtensionViews();
ASSERT_EQ(3u, action_views.size());
ExtensionsToolbarButton* menu_button =
extensions_container()->GetExtensionsButton();
// All views should be lined up horizontally with the menu button.
EXPECT_EQ(action_views[0]->y(), action_views[1]->y());
EXPECT_EQ(action_views[1]->y(), action_views[2]->y());
EXPECT_EQ(action_views[2]->y(), menu_button->y());
// Views are ordered left-to-right (in LTR mode).
EXPECT_LE(action_views[0]->x() + action_views[0]->width(),
action_views[1]->x());
EXPECT_LE(action_views[1]->x() + action_views[1]->width(),
action_views[2]->x());
EXPECT_LE(action_views[2]->x() + action_views[2]->width(), menu_button->x());
}
// Tests that when an extension is reloaded it remains visible in the toolbar
// and extensions menu.
TEST_F(ExtensionsMenuViewUnitTest, ReloadExtension) {
// The extension must have a manifest to be reloaded.
extensions::TestExtensionDir extension_directory;
constexpr char kManifest[] = R"({
"name": "Test",
"version": "1",
"manifest_version": 2
})";
extension_directory.WriteManifest(kManifest);
extensions::ChromeTestExtensionLoader loader(profile());
scoped_refptr<const extensions::Extension> extension =
loader.LoadExtension(extension_directory.UnpackedPath());
// Force the menu to re-layout, since a new item was added.
LayoutMenuIfNecessary();
ASSERT_EQ(1u, extensions_menu()->extensions_menu_items_for_testing().size());
{
InstalledExtensionMenuItemView* menu_item = GetOnlyMenuItem();
ClickPinButton(menu_item);
EXPECT_TRUE(extensions_container()->IsActionVisibleOnToolbar(
menu_item->view_controller()));
// |menu_item| will not be valid after the extension reloads.
}
extensions::TestExtensionRegistryObserver registry_observer(
extensions::ExtensionRegistry::Get(profile()));
ReloadExtension(extension->id());
ASSERT_TRUE(registry_observer.WaitForExtensionLoaded());
LayoutMenuIfNecessary();
ASSERT_EQ(1u, extensions_menu()->extensions_menu_items_for_testing().size());
EXPECT_TRUE(extensions_container()->IsActionVisibleOnToolbar(
GetOnlyMenuItem()->view_controller()));
}
// Tests that a when an extension is reloaded with manifest errors, and
// therefore fails to be loaded into Chrome, it's removed from the toolbar and
// extensions menu.
TEST_F(ExtensionsMenuViewUnitTest, ReloadExtensionFailed) {
extensions::TestExtensionDir extension_directory;
constexpr char kManifest[] = R"({
"name": "Test",
"version": "1",
"manifest_version": 2
})";
extension_directory.WriteManifest(kManifest);
extensions::ChromeTestExtensionLoader loader(profile());
scoped_refptr<const extensions::Extension> extension =
loader.LoadExtension(extension_directory.UnpackedPath());
LayoutMenuIfNecessary();
InstalledExtensionMenuItemView* menu_item = GetOnlyMenuItem();
ASSERT_TRUE(menu_item);
ClickPinButton(menu_item);
// Replace the extension's valid manifest with one containing errors. In this
// case, the error is that both the 'browser_action' and 'page_action' keys
// are specified instead of only one.
constexpr char kManifestWithErrors[] = R"({
"name": "Test",
"version": "1",
"manifest_version": 2,
"page_action" : {},
"browser_action" : {}
})";
extension_directory.WriteManifest(kManifestWithErrors);
// Reload the extension. It should fail due to the manifest errors.
extension_service()->ReloadExtensionWithQuietFailure(extension->id());
base::RunLoop().RunUntilIdle();
// Since the extension is removed it's no longer visible on the toolbar or in
// the menu.
for (views::View* child : extensions_container()->children())
EXPECT_FALSE(views::IsViewClass<ToolbarActionView>(child));
EXPECT_EQ(0u, extensions_menu()->extensions_menu_items_for_testing().size());
}
TEST_F(ExtensionsMenuViewUnitTest, PinButtonUserActionWithAccessibility) {
base::UserActionTester user_action_tester;
InstallExtensionAndLayout("Test Extension");
InstalledExtensionMenuItemView* menu_item = GetOnlyMenuItem();
ASSERT_NE(nullptr, menu_item);
views::test::AXEventCounter counter(views::AXEventManager::Get());
constexpr char kPinButtonUserAction[] = "Extensions.Toolbar.PinButtonPressed";
// Verify behavior before pin, after pin, and after unpin.
for (int i = 0; i < 3; i++) {
EXPECT_EQ(i, user_action_tester.GetActionCount(kPinButtonUserAction));
#if BUILDFLAG(IS_MAC)
// TODO(crbug.com/1045212): No Mac animations in unit tests cause errors.
#else
EXPECT_EQ(i, counter.GetCount(ax::mojom::Event::kAlert));
EXPECT_EQ(i, counter.GetCount(ax::mojom::Event::kTextChanged));
#endif
ClickPinButton(menu_item);
}
}
TEST_F(ExtensionsMenuViewUnitTest, WindowTitle) {
InstallExtensionAndLayout("Test Extension");
ExtensionsMenuView* const menu_view = extensions_menu();
EXPECT_FALSE(menu_view->GetWindowTitle().empty());
EXPECT_TRUE(menu_view->GetAccessibleWindowTitle().empty());
}
// TODO(crbug.com/984654): When supported, add a test to verify the
// ExtensionsToolbarContainer shrinks when the window is too small to show all
// pinned extensions.
// TODO(crbug.com/984654): When supported, add a test to verify an extension
// is shown when a bubble pops up and needs to draw attention to it.