blob: d6fe585601504a35a027662018de198bec4a1cc3 [file] [log] [blame]
// Copyright 2020 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 <list>
#include <memory>
#include "ash/clipboard/clipboard_history.h"
#include "ash/clipboard/clipboard_history_controller_impl.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/clipboard/clipboard_history_menu_model_adapter.h"
#include "ash/clipboard/views/clipboard_history_delete_button.h"
#include "ash/clipboard/views/clipboard_history_item_view.h"
#include "ash/constants/ash_features.h"
#include "ash/shell.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/ash/login/login_manager_test.h"
#include "chrome/browser/ash/login/test/login_manager_mixin.h"
#include "chrome/browser/ash/login/ui/user_adding_screen.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chromeos/crosapi/mojom/clipboard_history.mojom.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/base/data_transfer_policy/data_transfer_policy_controller.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/controls/menu/menu_config.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/widget/widget.h"
#include "url/origin.h"
namespace {
constexpr char kUrlString[] = "https://www.example.com";
// The helper class to wait for the observed view's bounds update.
class ViewBoundsWaiter : public views::ViewObserver {
public:
explicit ViewBoundsWaiter(views::View* observed_view)
: observed_view_(observed_view) {
observed_view_->AddObserver(this);
}
ViewBoundsWaiter(const ViewBoundsWaiter&) = delete;
ViewBoundsWaiter& operator=(const ViewBoundsWaiter&) = delete;
~ViewBoundsWaiter() override { observed_view_->RemoveObserver(this); }
void WaitForMeaningfulBounds() {
// No-op if `observed_view_` already has meaningful bounds.
if (!observed_view_->bounds().IsEmpty())
return;
run_loop_.Run();
}
private:
// views::ViewObserver:
void OnViewBoundsChanged(views::View* observed_view) override {
EXPECT_FALSE(observed_view->bounds().IsEmpty());
run_loop_.Quit();
}
views::View* const observed_view_;
base::RunLoop run_loop_;
};
// Helpers ---------------------------------------------------------------------
std::unique_ptr<views::Widget> CreateTestWidget() {
auto widget = std::make_unique<views::Widget>();
views::Widget::InitParams params;
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
params.type = views::Widget::InitParams::TYPE_WINDOW_FRAMELESS;
widget->Init(std::move(params));
return widget;
}
void FlushMessageLoop() {
base::RunLoop run_loop;
base::SequencedTaskRunnerHandle::Get()->PostTask(FROM_HERE,
run_loop.QuitClosure());
run_loop.Run();
}
void SetClipboardText(const std::string& text) {
ui::ScopedClipboardWriter(ui::ClipboardBuffer::kCopyPaste)
.WriteText(base::UTF8ToUTF16(text));
// ClipboardHistory will post a task to process clipboard data in order to
// debounce multiple clipboard writes occurring in sequence. Here we give
// ClipboardHistory the chance to run its posted tasks before proceeding.
FlushMessageLoop();
}
void SetClipboardTextAndHtml(const std::string& text, const std::string& html) {
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteText(base::UTF8ToUTF16(text));
scw.WriteHTML(base::UTF8ToUTF16(html), /*source_url=*/"");
}
// ClipboardHistory will post a task to process clipboard data in order to
// debounce multiple clipboard writes occurring in sequence. Here we give
// ClipboardHistory the chance to run its posted tasks before proceeding.
FlushMessageLoop();
}
ash::ClipboardHistoryControllerImpl* GetClipboardHistoryController() {
return ash::Shell::Get()->clipboard_history_controller();
}
ash::ClipboardHistoryMenuModelAdapter* GetContextMenu() {
return GetClipboardHistoryController()->context_menu_for_test();
}
const std::list<ash::ClipboardHistoryItem>& GetClipboardItems() {
return GetClipboardHistoryController()->history()->GetItems();
}
gfx::Rect GetClipboardHistoryMenuBoundsInScreen() {
return GetClipboardHistoryController()->GetMenuBoundsInScreenForTest();
}
bool VerifyClipboardTextData(const std::initializer_list<std::string>& texts) {
const std::list<ash::ClipboardHistoryItem>& items = GetClipboardItems();
if (items.size() != texts.size())
return false;
auto items_iter = items.cbegin();
const auto* texts_iter = texts.begin();
while (items_iter != items.cend() && texts_iter != texts.end()) {
if (items_iter->data().text() != *texts_iter)
return false;
++items_iter;
++texts_iter;
}
return true;
}
void WaitForOperationConfirmed() {
base::RunLoop run_loop;
GetClipboardHistoryController()->set_confirmed_operation_callback_for_test(
run_loop.QuitClosure());
run_loop.Run();
}
} // namespace
// Verify clipboard history's features in the multiprofile environment.
class ClipboardHistoryWithMultiProfileBrowserTest
: public chromeos::LoginManagerTest {
public:
ClipboardHistoryWithMultiProfileBrowserTest() : LoginManagerTest() {
login_mixin_.AppendRegularUsers(2);
account_id1_ = login_mixin_.users()[0].account_id;
account_id2_ = login_mixin_.users()[1].account_id;
feature_list_.InitAndEnableFeature(chromeos::features::kClipboardHistory);
}
~ClipboardHistoryWithMultiProfileBrowserTest() override = default;
ui::test::EventGenerator* GetEventGenerator() {
return event_generator_.get();
}
protected:
// Click at the delete button of the menu entry specified by `index`.
void ClickAtDeleteButton(int index) {
auto* item_view = GetContextMenu()->GetMenuItemViewAtForTest(index);
views::View* delete_button =
item_view->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID);
if (delete_button->GetVisible()) {
// Assume that `delete_button` already has meaningful bounds.
ASSERT_FALSE(delete_button->GetBoundsInScreen().IsEmpty());
} else {
ShowDeleteButtonByMouseHover(index);
}
GetEventGenerator()->MoveMouseTo(
delete_button->GetBoundsInScreen().CenterPoint());
GetEventGenerator()->ClickLeftButton();
}
void Press(ui::KeyboardCode key, int modifiers = ui::EF_NONE) {
event_generator_->PressKey(key, modifiers);
}
void Release(ui::KeyboardCode key, int modifiers = ui::EF_NONE) {
event_generator_->ReleaseKey(key, modifiers);
}
void PressAndRelease(ui::KeyboardCode key, int modifiers = ui::EF_NONE) {
Press(key, modifiers);
Release(key, modifiers);
}
void WaitUntilItemDeletionCompletes() {
auto* context_menu = GetContextMenu();
DCHECK(context_menu);
base::RunLoop run_loop;
context_menu->set_item_removal_callback_for_test(run_loop.QuitClosure());
run_loop.Run();
}
void PasteFromClipboardHistoryMenuAndWait() {
ASSERT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
PressAndRelease(ui::VKEY_RETURN);
WaitForOperationConfirmed();
}
void ShowContextMenuViaAccelerator(bool wait_for_selection) {
PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
if (!wait_for_selection)
return;
base::RunLoop run_loop;
GetClipboardHistoryController()
->set_initial_item_selected_callback_for_test(run_loop.QuitClosure());
run_loop.Run();
}
const views::MenuItemView* GetMenuItemViewForIndex(int index) const {
return GetContextMenu()->GetMenuItemViewAtForTest(index);
}
views::MenuItemView* GetMenuItemViewForTest(int index) {
return const_cast<views::MenuItemView*>(
const_cast<const ClipboardHistoryWithMultiProfileBrowserTest*>(this)
->GetMenuItemViewForIndex(index));
}
const ash::ClipboardHistoryItemView* GetHistoryItemViewForIndex(
int index) const {
const views::MenuItemView* hosting_menu_item =
GetMenuItemViewForIndex(index);
EXPECT_EQ(1u, hosting_menu_item->children().size());
return static_cast<const ash::ClipboardHistoryItemView*>(
hosting_menu_item->children()[0]);
}
ash::ClipboardHistoryItemView* GetHistoryItemViewForIndex(int index) {
return const_cast<ash::ClipboardHistoryItemView*>(
const_cast<const ClipboardHistoryWithMultiProfileBrowserTest*>(this)
->GetHistoryItemViewForIndex(index));
}
// Show the delete button by hovering the mouse on the menu entry specified
// by the index.
void ShowDeleteButtonByMouseHover(int index) {
auto* item_view = GetContextMenu()->GetMenuItemViewAtForTest(index);
views::View* delete_button =
item_view->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID);
ASSERT_FALSE(delete_button->GetVisible());
// Hover the mouse on `item_view` to show the delete button.
GetEventGenerator()->MoveMouseTo(
item_view->GetBoundsInScreen().CenterPoint(), /*count=*/5);
// Wait until `delete_button` has meaningful bounds. Note that the bounds
// are set by the layout manager asynchronously.
ViewBoundsWaiter waiter(delete_button);
waiter.WaitForMeaningfulBounds();
EXPECT_TRUE(delete_button->GetVisible());
EXPECT_TRUE(item_view->IsSelected());
}
// chromeos::LoginManagerTest:
void SetUpOnMainThread() override {
chromeos::LoginManagerTest::SetUpOnMainThread();
event_generator_ = std::make_unique<ui::test::EventGenerator>(
ash::Shell::GetPrimaryRootWindow());
}
AccountId account_id1_;
AccountId account_id2_;
chromeos::LoginManagerMixin login_mixin_{&mixin_host_};
std::unique_ptr<ui::test::EventGenerator> event_generator_;
base::test::ScopedFeatureList feature_list_;
};
// Verify that the clipboard data history is recorded as expected in the
// Multiuser environment.
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
VerifyClipboardHistoryAcrossMultiUser) {
LoginUser(account_id1_);
EXPECT_TRUE(GetClipboardItems().empty());
// Store text when the user1 is active.
const std::string copypaste_data1("user1_text1");
SetClipboardText(copypaste_data1);
{
const std::list<ash::ClipboardHistoryItem>& items = GetClipboardItems();
EXPECT_EQ(1u, items.size());
EXPECT_EQ(copypaste_data1, items.front().data().text());
}
// Log in as the user2. The clipboard history should be non-empty.
chromeos::UserAddingScreen::Get()->Start();
AddUser(account_id2_);
EXPECT_FALSE(GetClipboardItems().empty());
// Store text when the user2 is active.
const std::string copypaste_data2("user2_text1");
SetClipboardText(copypaste_data2);
{
const std::list<ash::ClipboardHistoryItem>& items = GetClipboardItems();
EXPECT_EQ(2u, items.size());
EXPECT_EQ(copypaste_data2, items.front().data().text());
}
// Switch to the user1.
user_manager::UserManager::Get()->SwitchActiveUser(account_id1_);
// Store text when the user1 is active.
const std::string copypaste_data3("user1_text2");
SetClipboardText(copypaste_data3);
{
const std::list<ash::ClipboardHistoryItem>& items = GetClipboardItems();
EXPECT_EQ(3u, items.size());
// Note that items in |data| follow the time ordering. The most recent item
// is always the first one.
auto it = items.begin();
EXPECT_EQ(copypaste_data3, it->data().text());
std::advance(it, 1u);
EXPECT_EQ(copypaste_data2, it->data().text());
std::advance(it, 1u);
EXPECT_EQ(copypaste_data1, it->data().text());
}
}
// Verifies the history menu's ui interaction with the menu item selection.
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
VerifySelectionBehavior) {
LoginUser(account_id1_);
SetClipboardText("A");
SetClipboardText("B");
SetClipboardText("C");
base::HistogramTester histogram_tester;
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
ASSERT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
ASSERT_EQ(3, GetContextMenu()->GetMenuItemsCount());
histogram_tester.ExpectUniqueSample(
"Ash.ClipboardHistory.ContextMenu.ShowMenu",
crosapi::mojom::ClipboardHistoryControllerShowSource::kAccelerator, 1);
// The history menu's first item should be selected as default after the menu
// shows. Meanwhile, its delete button should not show.
const views::MenuItemView* first_menu_item_view =
GetMenuItemViewForIndex(/*index=*/0);
EXPECT_TRUE(first_menu_item_view->IsSelected());
EXPECT_FALSE(GetHistoryItemViewForIndex(/*index=*/0)
->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID)
->GetVisible());
EXPECT_EQ(gfx::Size(256, 36), first_menu_item_view->size());
// Move the mouse to the second menu item.
const views::MenuItemView* second_menu_item_view =
GetMenuItemViewForIndex(/*index=*/1);
EXPECT_FALSE(second_menu_item_view->IsSelected());
GetEventGenerator()->MoveMouseTo(
second_menu_item_view->GetBoundsInScreen().CenterPoint());
// The first menu item should not be selected while the second one should be.
EXPECT_FALSE(first_menu_item_view->IsSelected());
EXPECT_TRUE(second_menu_item_view->IsSelected());
// Under mouse hovering, the second item's delete button should show.
EXPECT_TRUE(GetHistoryItemViewForIndex(/*index=*/1)
->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID)
->GetVisible());
const views::MenuItemView* third_menu_item_view =
GetMenuItemViewForIndex(/*index=*/2);
EXPECT_FALSE(third_menu_item_view->IsSelected());
PressAndRelease(ui::KeyboardCode::VKEY_DOWN, ui::EF_NONE);
// Move the selection to the third item by pressing the arrow key. The third
// item should be selected and its delete button should not show.
EXPECT_FALSE(second_menu_item_view->IsSelected());
EXPECT_TRUE(third_menu_item_view->IsSelected());
EXPECT_FALSE(GetHistoryItemViewForIndex(/*index=*/2)
->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID)
->GetVisible());
}
// Verifies the selection traversal via the tab key.
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
VerifyTabSelectionTraversal) {
LoginUser(account_id1_);
SetClipboardText("A");
SetClipboardText("B");
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
// Verify the default state right after the menu shows.
ASSERT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
ASSERT_EQ(2, GetContextMenu()->GetMenuItemsCount());
const views::MenuItemView* first_menu_item_view =
GetMenuItemViewForIndex(/*index=*/0);
ASSERT_TRUE(first_menu_item_view->IsSelected());
const ash::ClipboardHistoryItemView* first_history_item_view =
GetHistoryItemViewForIndex(/*index=*/0);
ASSERT_FALSE(first_history_item_view
->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID)
->GetVisible());
// Press the tab key.
PressAndRelease(ui::VKEY_TAB);
EXPECT_TRUE(first_menu_item_view->IsSelected());
// Verify that the first menu item's delete button shows. In addition, the
// delete button's inkdrop highlight should fade in or be visible.
const ash::ClipboardHistoryDeleteButton* delete_button =
static_cast<const ash::ClipboardHistoryDeleteButton*>(
first_history_item_view->GetViewByID(
ash::ClipboardHistoryUtil::kDeleteButtonViewID));
ASSERT_TRUE(delete_button->GetVisible());
EXPECT_TRUE(const_cast<ash::ClipboardHistoryDeleteButton*>(delete_button)
->GetInkDrop()
->IsHighlightFadingInOrVisible());
const views::MenuItemView* second_menu_item_view =
GetMenuItemViewForIndex(/*index=*/1);
EXPECT_FALSE(second_menu_item_view->IsSelected());
const ash::ClipboardHistoryItemView* second_history_item_view =
GetHistoryItemViewForIndex(/*index=*/1);
EXPECT_FALSE(second_history_item_view
->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID)
->GetVisible());
// Press the tab key. Verify that the second menu item is selected while its
// delete button is hidden.
PressAndRelease(ui::VKEY_TAB);
EXPECT_TRUE(second_menu_item_view->IsSelected());
EXPECT_FALSE(second_history_item_view
->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID)
->GetVisible());
// Press the tab key. Verify that the second item's delete button shows.
PressAndRelease(ui::VKEY_TAB);
EXPECT_TRUE(second_menu_item_view->IsSelected());
EXPECT_TRUE(second_history_item_view
->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID)
->GetVisible());
// Press the tab key with the shift key pressed. Verify that the second item
// is selected while its delete button is hidden.
PressAndRelease(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
EXPECT_TRUE(second_menu_item_view->IsSelected());
EXPECT_FALSE(second_history_item_view
->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID)
->GetVisible());
// Press the tab key with the shift key pressed. Verify that the first item
// is selected while its delete button is visible.
PressAndRelease(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
EXPECT_TRUE(first_menu_item_view->IsSelected());
EXPECT_TRUE(first_history_item_view
->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID)
->GetVisible());
EXPECT_FALSE(second_menu_item_view->IsSelected());
// Press the ENTER key. Verifies that the first item is deleted. The second
// item is selected and its delete button should not show.
PressAndRelease(ui::VKEY_RETURN);
EXPECT_EQ(1, GetContextMenu()->GetMenuItemsCount());
EXPECT_TRUE(second_menu_item_view->IsSelected());
EXPECT_FALSE(second_history_item_view
->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID)
->GetVisible());
}
// Verifies the tab traversal on the history menu with only one item.
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
VerifyTabTraversalOnOneItemMenu) {
LoginUser(account_id1_);
SetClipboardText("A");
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
// Verify the default state right after the menu shows.
ASSERT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
ASSERT_EQ(1, GetContextMenu()->GetMenuItemsCount());
const ash::ClipboardHistoryItemView* first_history_item_view =
GetHistoryItemViewForIndex(/*index=*/0);
ASSERT_FALSE(first_history_item_view
->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID)
->GetVisible());
const views::MenuItemView* first_menu_item_view =
GetMenuItemViewForIndex(/*index=*/0);
ASSERT_TRUE(first_menu_item_view->IsSelected());
// Press the tab key. Verify that the delete button is visible.
PressAndRelease(ui::VKEY_TAB);
ASSERT_TRUE(first_history_item_view
->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID)
->GetVisible());
// Press the tab key. Verify that the delete button is hidden. The menu item
// is still under selection.
PressAndRelease(ui::VKEY_TAB);
ASSERT_FALSE(first_history_item_view
->GetViewByID(ash::ClipboardHistoryUtil::kDeleteButtonViewID)
->GetVisible());
EXPECT_TRUE(first_menu_item_view->IsSelected());
}
// Verifies that the history menu is anchored at the cursor's location when
// not having any textfield.
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
ShowHistoryMenuWhenNoTextfieldExists) {
LoginUser(account_id1_);
// Close the browser window to ensure that textfield does not exist.
CloseAllBrowsers();
// No clipboard data. So the clipboard history menu should not show.
ASSERT_TRUE(GetClipboardItems().empty());
ShowContextMenuViaAccelerator(/*wait_for_selection=*/false);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
SetClipboardText("test");
const gfx::Point mouse_location =
ash::Shell::Get()->GetPrimaryRootWindow()->bounds().CenterPoint();
GetEventGenerator()->MoveMouseTo(mouse_location);
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
// Verifies that the menu is anchored at the cursor's location.
ASSERT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
const gfx::Point menu_origin =
GetClipboardHistoryMenuBoundsInScreen().origin();
EXPECT_EQ(mouse_location.x(), menu_origin.x());
EXPECT_EQ(mouse_location.y() +
views::MenuConfig::instance().touchable_anchor_offset,
menu_origin.y());
}
// Verify the handling of the click cancel event.
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
HandleClickCancelEvent) {
LoginUser(account_id1_);
// Write some things to the clipboard.
SetClipboardText("A");
SetClipboardText("B");
// Show the menu.
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
ASSERT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
ASSERT_EQ(2, GetContextMenu()->GetMenuItemsCount());
// Press on the first menu item.
ash::ClipboardHistoryItemView* first_item_view =
GetHistoryItemViewForIndex(/*index=*/0);
GetEventGenerator()->MoveMouseTo(
first_item_view->GetBoundsInScreen().CenterPoint());
GetEventGenerator()->PressLeftButton();
// Move the mouse to the second menu item then release.
auto* second_item_view =
GetContextMenu()->GetMenuItemViewAtForTest(/*index=*/1);
ASSERT_FALSE(second_item_view->IsSelected());
GetEventGenerator()->MoveMouseTo(
second_item_view->GetBoundsInScreen().CenterPoint());
GetEventGenerator()->ReleaseLeftButton();
// Verify that the second menu item is selected now.
EXPECT_TRUE(second_item_view->IsSelected());
}
// Verifies item deletion through the mouse click at the delete button.
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
DeleteItemByClickAtDeleteButton) {
LoginUser(account_id1_);
// Write some things to the clipboard.
SetClipboardText("A");
SetClipboardText("B");
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
ASSERT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
ASSERT_EQ(2, GetContextMenu()->GetMenuItemsCount());
// Delete the second menu item.
ClickAtDeleteButton(/*index=*/1);
EXPECT_EQ(1, GetContextMenu()->GetMenuItemsCount());
EXPECT_TRUE(VerifyClipboardTextData({"B"}));
// Delete the last menu item. Verify that the menu is closed.
ClickAtDeleteButton(/*index=*/0);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
// No menu shows because of the empty clipboard history.
ShowContextMenuViaAccelerator(/*wait_for_selection=*/false);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
}
// Verifies that the selected item should be deleted by the backspace key.
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
DeleteItemViaBackspaceKey) {
base::HistogramTester histogram_tester;
LoginUser(account_id1_);
// Write some things to the clipboard.
SetClipboardText("A");
SetClipboardText("B");
SetClipboardText("C");
// Show the menu.
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
ASSERT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
ASSERT_EQ(3, GetContextMenu()->GetMenuItemsCount());
// Select the first menu item via key then delete it. Verify the menu and the
// clipboard history.
PressAndRelease(ui::KeyboardCode::VKEY_BACK);
EXPECT_EQ(2, GetContextMenu()->GetMenuItemsCount());
EXPECT_TRUE(VerifyClipboardTextData({"B", "A"}));
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.DisplayFormatDeleted", 1);
// Select the second menu item via key then delete it. Verify the menu and the
// clipboard history.
PressAndRelease(ui::KeyboardCode::VKEY_DOWN, ui::EF_NONE);
PressAndRelease(ui::KeyboardCode::VKEY_BACK, ui::EF_NONE);
EXPECT_EQ(1, GetContextMenu()->GetMenuItemsCount());
EXPECT_TRUE(VerifyClipboardTextData({"B"}));
// Delete the last item. Verify that the menu is closed.
PressAndRelease(ui::KeyboardCode::VKEY_BACK, ui::EF_NONE);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
// Trigger the accelerator of opening the clipboard history menu. No menu
// shows because of the empty history data.
ShowContextMenuViaAccelerator(/*wait_for_selection=*/false);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
}
// Flaky: crbug.com/1123542
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
DISABLED_ShouldPasteHistoryAsPlainText) {
LoginUser(account_id1_);
// Create a browser and cache its active web contents.
auto* browser = CreateBrowser(
chromeos::ProfileHelper::Get()->GetProfileByAccountId(account_id1_));
auto* web_contents = browser->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(web_contents);
// Load the web contents synchronously.
// The contained script:
// - Listens for paste events and caches the last pasted data.
// - Notifies observers of paste events by changing document title.
// - Provides an API to expose the last pasted data.
ASSERT_TRUE(content::NavigateToURL(web_contents, GURL(R"(data:text/html,
<!DOCTYPE html>
<html>
<body>
<script>
let lastPaste = undefined;
let lastPasteId = 1;
window.addEventListener('paste', e => {
e.stopPropagation();
e.preventDefault();
const clipboardData = e.clipboardData || window.clipboardData;
lastPaste = clipboardData.types.map((type) => {
return `${type}: ${clipboardData.getData(type)}`;
});
document.title = `Paste ${lastPasteId++}`;
});
window.getLastPaste = () => {
return lastPaste || [];
};
</script>
</body>
</html>
)")));
// Cache a function to return the last paste.
auto GetLastPaste = [&]() {
auto result = content::EvalJs(
web_contents, "(function() { return window.getLastPaste(); })();");
EXPECT_EQ(result.error, "");
return result.ExtractList();
};
// Confirm initial state.
ASSERT_TRUE(GetLastPaste().GetList().empty());
// Write some things to the clipboard.
SetClipboardTextAndHtml("A", "<span>A</span>");
SetClipboardTextAndHtml("B", "<span>B</span>");
SetClipboardTextAndHtml("C", "<span>C</span>");
// Open clipboard history and paste the last history item.
PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
PressAndRelease(ui::KeyboardCode::VKEY_RETURN);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
// Wait for the paste event to propagate to the web contents.
// The web contents will notify us a paste occurred by updating page title.
ignore_result(
content::TitleWatcher(web_contents, u"Paste 1").WaitAndGetTitle());
// Confirm the expected paste data.
base::ListValue last_paste = GetLastPaste();
ASSERT_EQ(last_paste.GetList().size(), 2u);
EXPECT_EQ(last_paste.GetList()[0].GetString(), "text/plain: A");
EXPECT_EQ(last_paste.GetList()[1].GetString(), "text/html: <span>A</span>");
// Open clipboard history and paste the middle history item as plain text.
PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
PressAndRelease(ui::KeyboardCode::VKEY_RETURN, ui::EF_SHIFT_DOWN);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
// Wait for the paste event to propagate to the web contents.
// The web contents will notify us a paste occurred by updating page title.
ignore_result(
content::TitleWatcher(web_contents, u"Paste 2").WaitAndGetTitle());
// Confirm the expected paste data.
last_paste = GetLastPaste();
ASSERT_EQ(last_paste.GetList().size(), 1u);
EXPECT_EQ(last_paste.GetList()[0].GetString(), "text/plain: A");
}
// The browser test which creates a widget with a textfield during setting-up
// to help verify the multipaste menu item's response to the gesture tap and
// the mouse click.
class ClipboardHistoryTextfieldBrowserTest
: public ClipboardHistoryWithMultiProfileBrowserTest {
public:
ClipboardHistoryTextfieldBrowserTest() = default;
~ClipboardHistoryTextfieldBrowserTest() override = default;
protected:
// ClipboardHistoryWithMultiProfileBrowserTest:
void SetUpOnMainThread() override {
ClipboardHistoryWithMultiProfileBrowserTest::SetUpOnMainThread();
LoginUser(account_id1_);
CloseAllBrowsers();
// Create a widget containing a single, focusable textfield.
widget_ = CreateTestWidget();
textfield_ = widget_->SetContentsView(std::make_unique<views::Textfield>());
textfield_->SetAccessibleName(u"Textfield");
textfield_->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
// Show the widget.
widget_->SetBounds(gfx::Rect(0, 0, 100, 100));
widget_->Show();
ASSERT_TRUE(widget_->IsActive());
// Focus the textfield and confirm initial state.
textfield_->RequestFocus();
ASSERT_TRUE(textfield_->HasFocus());
ASSERT_TRUE(textfield_->GetText().empty());
}
std::unique_ptr<views::Widget> widget_;
views::Textfield* textfield_ = nullptr;
};
// Verifies that the clipboard history menu responses to the gesture tap
// correctly (https://crbug.com/1142088).
IN_PROC_BROWSER_TEST_F(ClipboardHistoryTextfieldBrowserTest,
VerifyResponseToGestures) {
SetClipboardText("A");
SetClipboardText("B");
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
ASSERT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
// Tap at the second menu item view. Verify that "A" is pasted.
const views::MenuItemView* second_menu_item_view =
GetMenuItemViewForIndex(/*index=*/1);
GetEventGenerator()->GestureTapAt(
second_menu_item_view->GetBoundsInScreen().CenterPoint());
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
base::RunLoop().RunUntilIdle();
EXPECT_EQ("A", base::UTF16ToUTF8(textfield_->GetText()));
}
// Verifies that the metric to record the count of the consecutive pastes from
// the clipboard history menu works as expected.
IN_PROC_BROWSER_TEST_F(ClipboardHistoryTextfieldBrowserTest,
VerifyConsecutivePasteMetric) {
base::HistogramTester histogram_tester;
SetClipboardText("A");
WaitForOperationConfirmed();
PasteFromClipboardHistoryMenuAndWait();
PasteFromClipboardHistoryMenuAndWait();
SetClipboardText("B");
WaitForOperationConfirmed();
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ConsecutivePastes",
/*count=*/1);
histogram_tester.ExpectUniqueSample("Ash.ClipboardHistory.ConsecutivePastes",
/*sample=*/2, /*count=*/1);
}
// Verifies that the delete button should show after its host item view is under
// gesture press for enough long time (https://crbug.com/1147584).
IN_PROC_BROWSER_TEST_F(ClipboardHistoryTextfieldBrowserTest,
DeleteButtonShowAfterLongPress) {
SetClipboardText("A");
SetClipboardText("B");
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
ASSERT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
ash::ClipboardHistoryItemView* second_item_view =
GetHistoryItemViewForIndex(/*index=*/1);
views::View* second_item_delete_button = second_item_view->GetViewByID(
ash::ClipboardHistoryUtil::kDeleteButtonViewID);
EXPECT_FALSE(second_item_delete_button->GetVisible());
// Long press on the second item until its delete button shows.
GetEventGenerator()->PressTouch(
second_item_view->GetBoundsInScreen().CenterPoint());
base::RunLoop run_loop;
auto subscription = second_item_delete_button->AddVisibleChangedCallback(
run_loop.QuitClosure());
run_loop.Run();
GetEventGenerator()->ReleaseTouch();
EXPECT_TRUE(second_item_delete_button->GetVisible());
// Press the up arrow key then press the ENTER key. Verify that the first
// item's clipboard data is pasted.
PressAndRelease(ui::KeyboardCode::VKEY_UP, ui::EF_NONE);
PressAndRelease(ui::VKEY_RETURN);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
base::RunLoop().RunUntilIdle();
EXPECT_EQ("B", base::UTF16ToUTF8(textfield_->GetText()));
}
IN_PROC_BROWSER_TEST_F(ClipboardHistoryTextfieldBrowserTest,
ShouldPasteHistoryViaKeyboard) {
base::HistogramTester histogram_tester;
// Write some things to the clipboard.
SetClipboardText("A");
SetClipboardText("B");
SetClipboardText("C");
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.DisplayFormatShown", 3);
PressAndRelease(ui::KeyboardCode::VKEY_RETURN);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
base::RunLoop().RunUntilIdle();
EXPECT_EQ("C", base::UTF16ToUTF8(textfield_->GetText()));
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.DisplayFormatPasted", 1);
textfield_->SetText(std::u16string());
EXPECT_TRUE(textfield_->GetText().empty());
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
// Verify we can paste the first history item via the COMMAND+V shortcut.
PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
base::RunLoop().RunUntilIdle();
EXPECT_EQ("C", base::UTF16ToUTF8(textfield_->GetText()));
textfield_->SetText(std::u16string());
EXPECT_TRUE(textfield_->GetText().empty());
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
PressAndRelease(ui::KeyboardCode::VKEY_RETURN);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
base::RunLoop().RunUntilIdle();
EXPECT_EQ("A", base::UTF16ToUTF8(textfield_->GetText()));
textfield_->SetText(std::u16string());
EXPECT_TRUE(textfield_->GetText().empty());
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
base::RunLoop().RunUntilIdle();
EXPECT_EQ("A", base::UTF16ToUTF8(textfield_->GetText()));
}
IN_PROC_BROWSER_TEST_F(ClipboardHistoryTextfieldBrowserTest,
ShouldPasteHistoryWhileHoldingDownCommandKey) {
// Write some things to the clipboard.
SetClipboardText("A");
SetClipboardText("B");
SetClipboardText("C");
// Verify we can traverse clipboard history and paste the first history item
// while holding down the COMMAND key.
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
base::RunLoop().RunUntilIdle();
EXPECT_EQ("C", base::UTF16ToUTF8(textfield_->GetText()));
Release(ui::KeyboardCode::VKEY_COMMAND);
textfield_->SetText(std::u16string());
EXPECT_TRUE(textfield_->GetText().empty());
// Verify we can traverse clipboard history and paste the last history item
// while holding down the COMMAND key.
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
PressAndRelease(ui::KeyboardCode::VKEY_DOWN, ui::EF_COMMAND_DOWN);
PressAndRelease(ui::KeyboardCode::VKEY_DOWN, ui::EF_COMMAND_DOWN);
PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
base::RunLoop().RunUntilIdle();
EXPECT_EQ("A", base::UTF16ToUTF8(textfield_->GetText()));
Release(ui::KeyboardCode::VKEY_COMMAND);
}
class FakeDataTransferPolicyController
: public ui::DataTransferPolicyController {
public:
FakeDataTransferPolicyController()
: allowed_origin_(url::Origin::Create(GURL(kUrlString))) {}
~FakeDataTransferPolicyController() override = default;
// ui::DataTransferPolicyController:
bool IsClipboardReadAllowed(
const ui::DataTransferEndpoint* const data_src,
const ui::DataTransferEndpoint* const data_dst) override {
// The multipaste menu should have access to any clipboard data.
if (data_dst && data_dst->type() == ui::EndpointType::kClipboardHistory)
return true;
// For other data destinations, only the data from `allowed_origin_`
// should be accessible.
return data_src && data_src->IsUrlType() &&
(*data_src->origin() == allowed_origin_);
}
void PasteIfAllowed(const ui::DataTransferEndpoint* const data_src,
const ui::DataTransferEndpoint* const data_dst,
content::WebContents* web_contents,
base::OnceCallback<void(bool)> callback) override {}
bool IsDragDropAllowed(const ui::DataTransferEndpoint* const data_src,
const ui::DataTransferEndpoint* const data_dst,
const bool is_drop) override {
return false;
}
private:
const url::Origin allowed_origin_;
};
// The browser test equipped with the custom policy controller.
class ClipboardHistoryWithMockDLPBrowserTest
: public ClipboardHistoryTextfieldBrowserTest {
public:
ClipboardHistoryWithMockDLPBrowserTest()
: data_transfer_policy_controller_(
std::make_unique<FakeDataTransferPolicyController>()) {}
~ClipboardHistoryWithMockDLPBrowserTest() override = default;
// Write text into the clipboard buffer and it should be inaccessible from
// the multipaste menu, meaning that the clipboard data item created from
// the written text cannot be pasted through the multipaste menu.
void SetClipboardTextWithInaccessibleSrc(const std::string& text) {
SetClipboardText(text);
}
// Write text into the clipboard buffer and it should be accessible from
// the multipaste menu.
void SetClipboardTextWithAccessibleSrc(const std::string& text) {
ui::ScopedClipboardWriter(ui::ClipboardBuffer::kCopyPaste,
std::make_unique<ui::DataTransferEndpoint>(
url::Origin::Create(GURL(kUrlString))))
.WriteText(base::UTF8ToUTF16(text));
// ClipboardHistory will post a task to process clipboard data in order to
// debounce multiple clipboard writes occurring in sequence. Here we give
// ClipboardHistory the chance to run its posted tasks before proceeding.
FlushMessageLoop();
}
private:
std::unique_ptr<FakeDataTransferPolicyController>
data_transfer_policy_controller_;
};
// Verifies the basic features related to the inaccessible menu item, the one
// whose clipboard data should not be leaked through the multipaste menu.
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMockDLPBrowserTest, Basics) {
SetClipboardTextWithAccessibleSrc("A");
SetClipboardTextWithInaccessibleSrc("B");
EXPECT_TRUE(VerifyClipboardTextData({"B", "A"}));
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
// Verify that the text is pasted into `textfield_` after the mouse click at
// `accessible_menu_item_view`.
const views::MenuItemView* accessible_menu_item_view =
GetContextMenu()->GetMenuItemViewAtForTest(/*index=*/1);
GetEventGenerator()->MoveMouseTo(
accessible_menu_item_view->GetBoundsInScreen().CenterPoint());
ASSERT_TRUE(accessible_menu_item_view->IsSelected());
GetEventGenerator()->ClickLeftButton();
base::RunLoop().RunUntilIdle();
EXPECT_EQ("A", base::UTF16ToUTF8(textfield_->GetText()));
// Clear `textfield_`'s contents.
textfield_->SetText(std::u16string());
ASSERT_TRUE(textfield_->GetText().empty());
// Re-show the multipaste menu since the menu is closed after the previous
// mouse click.
ASSERT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
ShowContextMenuViaAccelerator(/*wait_for_selection=*/true);
// Move mouse to `inaccessible_menu_item_view` then click the left button.
const views::MenuItemView* inaccessible_menu_item_view =
GetContextMenu()->GetMenuItemViewAtForTest(/*index=*/0);
GetEventGenerator()->MoveMouseTo(
inaccessible_menu_item_view->GetBoundsInScreen().CenterPoint());
GetEventGenerator()->ClickLeftButton();
base::RunLoop().RunUntilIdle();
// Verify that the text is not pasted and menu is closed after click.
EXPECT_EQ("", base::UTF16ToUTF8(textfield_->GetText()));
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
}