blob: d432f46b1812c996baad62d1e557915cac0f56dc [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/views/toolbar/toolbar_action_view.h"
#include <memory>
#include <string>
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/sessions/session_tab_helper_factory.h"
#include "chrome/browser/ui/toolbar/test_toolbar_action_view_controller.h"
#include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "content/public/test/test_web_contents_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/mojom/menu_source_type.mojom-forward.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/menu_button.h"
namespace {
// A test delegate for a toolbar action view.
class TestToolbarActionViewDelegate : public ToolbarActionView::Delegate {
public:
TestToolbarActionViewDelegate()
: overflow_reference_view_(std::make_unique<views::MenuButton>()),
web_contents_(nullptr) {}
TestToolbarActionViewDelegate(const TestToolbarActionViewDelegate&) = delete;
TestToolbarActionViewDelegate& operator=(
const TestToolbarActionViewDelegate&) = delete;
~TestToolbarActionViewDelegate() override = default;
// ToolbarActionView::Delegate:
content::WebContents* GetCurrentWebContents() override {
return web_contents_;
}
views::MenuButton* GetOverflowReferenceView() const override {
return overflow_reference_view_.get();
}
gfx::Size GetToolbarActionSize() override { return gfx::Size(32, 32); }
MOCK_METHOD(void,
MovePinnedActionBy,
(const std::string& action_id, int move_by),
(override));
void WriteDragDataForView(views::View* sender,
const gfx::Point& press_pt,
ui::OSExchangeData* data) override {}
int GetDragOperationsForView(views::View* sender,
const gfx::Point& p) override {
return ui::DragDropTypes::DRAG_NONE;
}
bool CanStartDragForView(views::View* sender,
const gfx::Point& press_pt,
const gfx::Point& p) override {
return false;
}
void set_web_contents(content::WebContents* web_contents) {
web_contents_ = web_contents;
}
private:
std::unique_ptr<views::MenuButton> overflow_reference_view_;
raw_ptr<content::WebContents> web_contents_;
};
class OpenMenuListener : public views::ContextMenuController {
public:
explicit OpenMenuListener(views::View* view) : view_(view) {
view_->set_context_menu_controller(this);
}
OpenMenuListener(const OpenMenuListener&) = delete;
OpenMenuListener& operator=(const OpenMenuListener&) = delete;
~OpenMenuListener() override { view_->set_context_menu_controller(nullptr); }
void ShowContextMenuForViewImpl(
views::View* source,
const gfx::Point& point,
ui::mojom::MenuSourceType source_type) override {
opened_menu_ = true;
}
bool opened_menu() const { return opened_menu_; }
private:
raw_ptr<views::View> view_;
bool opened_menu_ = false;
};
} // namespace
class ToolbarActionViewUnitTest : public ChromeViewsTestBase {
public:
ToolbarActionViewUnitTest() : widget_(nullptr) {}
ToolbarActionViewUnitTest(const ToolbarActionViewUnitTest&) = delete;
ToolbarActionViewUnitTest& operator=(const ToolbarActionViewUnitTest&) =
delete;
~ToolbarActionViewUnitTest() override = default;
void SetUp() override {
ChromeViewsTestBase::SetUp();
controller_ =
std::make_unique<TestToolbarActionViewController>("fake controller");
action_view_delegate_ =
std::make_unique<testing::NiceMock<TestToolbarActionViewDelegate>>();
widget_ =
CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
}
void TearDown() override {
widget_.reset();
ChromeViewsTestBase::TearDown();
}
void Reset() {
widget_.reset();
action_view_delegate_.reset();
controller_.reset();
}
views::Widget* widget() { return widget_.get(); }
TestToolbarActionViewController* controller() { return controller_.get(); }
TestToolbarActionViewDelegate* action_view_delegate() {
return action_view_delegate_.get();
}
testing::NiceMock<TestToolbarActionViewDelegate>&
mock_action_view_delegate() {
return *action_view_delegate_;
}
private:
std::unique_ptr<TestToolbarActionViewController> controller_;
std::unique_ptr<testing::NiceMock<TestToolbarActionViewDelegate>>
action_view_delegate_;
// The widget managed by this test.
std::unique_ptr<views::Widget> widget_;
};
// A MenuButton subclass that provides access to some MenuButton internals.
class TestToolbarActionView : public ToolbarActionView {
public:
TestToolbarActionView(ToolbarActionViewController* view_controller,
Delegate* delegate)
: ToolbarActionView(view_controller, delegate) {}
TestToolbarActionView(const TestToolbarActionView&) = delete;
TestToolbarActionView& operator=(const TestToolbarActionView&) = delete;
~TestToolbarActionView() override = default;
};
// Verifies there is no crash when a ToolbarActionView with an InkDrop is
// destroyed while holding a |pressed_lock_|.
TEST_F(ToolbarActionViewUnitTest,
NoCrashWhenDestroyingToolbarActionViewThatHasAPressedLock) {
TestToolbarActionViewController* view_controller = controller();
// Create a new toolbar action view.
auto view = std::make_unique<ToolbarActionView>(view_controller,
action_view_delegate());
view->SetBoundsRect(gfx::Rect(0, 0, 200, 20));
widget()->SetContentsView(std::move(view));
widget()->Show();
view_controller->ShowPopup(true);
}
// Verifies the InkDropAnimation used by the ToolbarActionView doesn't fail a
// DCHECK for an unsupported transition from ACTIVATED to ACTION_PENDING.
TEST_F(ToolbarActionViewUnitTest,
NoCrashWhenPressingMouseOnToolbarActionViewThatHasAPressedLock) {
TestToolbarActionViewController* view_controller = controller();
// Create a new toolbar action view.
auto view = std::make_unique<ToolbarActionView>(view_controller,
action_view_delegate());
view->SetBoundsRect(gfx::Rect(0, 0, 200, 20));
widget()->SetContentsView(std::move(view));
widget()->Show();
ui::test::EventGenerator generator(GetContext(), widget()->GetNativeWindow());
view_controller->ShowPopup(true);
generator.PressLeftButton();
}
// Test the basic ui of a ToolbarActionView and that it responds correctly to
// a controller's state.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || \
BUILDFLAG(IS_WIN)
// TODO(crbug.com/40668368): Test is flaky on Mac, Linux and Win10.
#define MAYBE_BasicToolbarActionViewTest DISABLED_BasicToolbarActionViewTest
#else
#define MAYBE_BasicToolbarActionViewTest BasicToolbarActionViewTest
#endif
TEST_F(ToolbarActionViewUnitTest, MAYBE_BasicToolbarActionViewTest) {
TestingProfile profile;
// ViewsTestBase initializes the aura environment, so the factory shouldn't.
content::TestWebContentsFactory web_contents_factory;
TestToolbarActionViewController* view_controller = controller();
TestToolbarActionViewDelegate* view_delegate = action_view_delegate();
// Configure the test controller and delegate.
std::u16string name = u"name";
view_controller->SetAccessibleName(name);
std::u16string tooltip = u"tooltip";
view_controller->SetTooltip(tooltip);
content::WebContents* web_contents =
web_contents_factory.CreateWebContents(&profile);
CreateSessionServiceTabHelper(web_contents);
view_delegate->set_web_contents(web_contents);
// Move the mouse off the not-yet-existent button.
ui::test::EventGenerator generator(GetContext(), widget()->GetNativeWindow());
generator.MoveMouseTo(gfx::Point(300, 300));
// Create a new toolbar action view.
auto owning_view =
std::make_unique<ToolbarActionView>(view_controller, view_delegate);
owning_view->SetBoundsRect(gfx::Rect(0, 0, 200, 20));
ToolbarActionView* view = owning_view.get();
widget()->SetContentsView(std::move(owning_view));
widget()->Show();
// Check that the tooltip and accessible state of the view match the
// controller's.
EXPECT_EQ(tooltip, view->GetRenderedTooltipText(gfx::Point()));
ui::AXNodeData ax_node_data;
view->GetViewAccessibility().GetAccessibleNodeData(&ax_node_data);
EXPECT_EQ(name, ax_node_data.GetString16Attribute(
ax::mojom::StringAttribute::kName));
// The button should start in normal state, with no actions executed.
EXPECT_EQ(views::Button::STATE_NORMAL, view->GetState());
EXPECT_EQ(0, view_controller->execute_action_count());
// Click the button. This should execute it.
generator.MoveMouseTo(gfx::Point(10, 10));
generator.ClickLeftButton();
EXPECT_EQ(1, view_controller->execute_action_count());
// Move the mouse off the button, and show a popup through a non-user action.
// Since this was not a user action, the button should not be pressed.
generator.MoveMouseTo(gfx::Point(300, 300));
view_controller->ShowPopup(false);
EXPECT_EQ(views::Button::STATE_NORMAL, view->GetState());
view_controller->HidePopup();
// Show the popup through a user action - the button should be pressed.
view_controller->ShowPopup(true);
EXPECT_EQ(views::Button::STATE_PRESSED, view->GetState());
view_controller->HidePopup();
EXPECT_EQ(views::Button::STATE_NORMAL, view->GetState());
// Ensure that clicking on an otherwise-disabled action opens the
// context menu.
view_controller->SetEnabled(false);
// Even though the controller is disabled, the button remains enabled
// because it will open the context menu.
EXPECT_EQ(views::Button::STATE_NORMAL, view->GetState());
int old_execute_action_count = view_controller->execute_action_count();
{
OpenMenuListener menu_listener(view);
view->Activate(nullptr);
EXPECT_TRUE(menu_listener.opened_menu());
EXPECT_EQ(old_execute_action_count,
view_controller->execute_action_count());
}
// Create an overflow button.
views::MenuButton* overflow_button =
view_delegate->GetOverflowReferenceView();
// If the view isn't visible, the overflow button should be pressed for
// popups.
view->SetVisible(false);
view_controller->ShowPopup(true);
EXPECT_EQ(views::Button::STATE_NORMAL, view->GetState());
EXPECT_EQ(views::Button::STATE_PRESSED, overflow_button->GetState());
view_controller->HidePopup();
EXPECT_EQ(views::Button::STATE_NORMAL, view->GetState());
EXPECT_EQ(views::Button::STATE_NORMAL, overflow_button->GetState());
}
// Verifies that pressing [Ctrl|Cmd] + Left / Right triggers a call to move the
// toolbar action.
TEST_F(ToolbarActionViewUnitTest, TestKeyboardReordering) {
TestToolbarActionViewController* view_controller = controller();
auto owned_view = std::make_unique<ToolbarActionView>(view_controller,
action_view_delegate());
auto* view = owned_view.get();
view->SetBoundsRect(gfx::Rect(0, 0, 200, 20));
widget()->SetContentsView(std::move(owned_view));
widget()->Show();
auto send_key_press = [view](ui::KeyboardCode code, int flags) {
view->OnKeyPressed(ui::KeyEvent(ui::EventType::kKeyPressed, code, flags,
ui::EventTimeForNow()));
view->OnKeyReleased(ui::KeyEvent(ui::EventType::kKeyPressed, code, flags,
ui::EventTimeForNow()));
};
// Start by pressing just left / right. No modifier is present, so this should
// do nothing.
EXPECT_CALL(mock_action_view_delegate(), MovePinnedActionBy).Times(0);
send_key_press(ui::VKEY_RIGHT, ui::EF_NONE);
testing::Mock::VerifyAndClearExpectations(&mock_action_view_delegate());
EXPECT_CALL(mock_action_view_delegate(), MovePinnedActionBy).Times(0);
send_key_press(ui::VKEY_LEFT, ui::EF_NONE);
testing::Mock::VerifyAndClearExpectations(&mock_action_view_delegate());
// Next, do the same with the wrong modifier key. Again, this should do
// nothing.
constexpr int kWrongModifierFlag =
#if BUILDFLAG(IS_MAC)
ui::EF_CONTROL_DOWN;
#else
ui::EF_COMMAND_DOWN;
#endif
EXPECT_CALL(mock_action_view_delegate(), MovePinnedActionBy).Times(0);
send_key_press(ui::VKEY_RIGHT, kWrongModifierFlag);
testing::Mock::VerifyAndClearExpectations(&mock_action_view_delegate());
EXPECT_CALL(mock_action_view_delegate(), MovePinnedActionBy).Times(0);
send_key_press(ui::VKEY_LEFT, kWrongModifierFlag);
testing::Mock::VerifyAndClearExpectations(&mock_action_view_delegate());
// Finally, use the correct modifier flag, and verify the delegate is told
// to move the action by the correct index.
constexpr int kCorrectModifierFlag =
#if BUILDFLAG(IS_MAC)
ui::EF_COMMAND_DOWN;
#else
ui::EF_CONTROL_DOWN;
#endif
EXPECT_CALL(mock_action_view_delegate(),
MovePinnedActionBy(view_controller->GetId(), 1))
.Times(1);
send_key_press(ui::VKEY_RIGHT, kCorrectModifierFlag);
testing::Mock::VerifyAndClearExpectations(&mock_action_view_delegate());
EXPECT_CALL(mock_action_view_delegate(),
MovePinnedActionBy(view_controller->GetId(), -1))
.Times(1);
send_key_press(ui::VKEY_LEFT, kCorrectModifierFlag);
testing::Mock::VerifyAndClearExpectations(&mock_action_view_delegate());
Reset();
}