| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/clipboard/clipboard_history_controller_impl.h" |
| |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "ash/app_list/app_list_controller_impl.h" |
| #include "ash/clipboard/clipboard_history.h" |
| #include "ash/clipboard/clipboard_history_item.h" |
| #include "ash/clipboard/clipboard_history_util.h" |
| #include "ash/public/cpp/clipboard_image_model_factory.h" |
| #include "ash/public/cpp/session/session_types.h" |
| #include "ash/session/session_controller_impl.h" |
| #include "ash/shell.h" |
| #include "ash/style/color_util.h" |
| #include "ash/system/toast/toast_manager_impl.h" |
| #include "ash/test/ash_test_base.h" |
| #include "base/location.h" |
| #include "base/notreached.h" |
| #include "base/run_loop.h" |
| #include "base/scoped_observation.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/repeating_test_future.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.h" |
| #include "base/unguessable_token.h" |
| #include "chromeos/constants/chromeos_features.h" |
| #include "chromeos/crosapi/mojom/clipboard_history.mojom.h" |
| #include "chromeos/ui/clipboard_history/clipboard_history_util.h" |
| #include "chromeos/ui/vector_icons/vector_icons.h" |
| #include "components/vector_icons/vector_icons.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/clipboard/clipboard_buffer.h" |
| #include "ui/base/clipboard/clipboard_data.h" |
| #include "ui/base/clipboard/clipboard_format_type.h" |
| #include "ui/base/clipboard/custom_data_helper.h" |
| #include "ui/base/clipboard/scoped_clipboard_writer.h" |
| #include "ui/base/models/image_model.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/events/test/event_generator.h" |
| #include "ui/gfx/image/image_unittest_util.h" |
| #include "ui/gfx/skia_util.h" |
| #include "ui/gfx/vector_icon_types.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "ui/views/controls/button/label_button.h" |
| #include "ui/views/controls/textfield/textfield.h" |
| #include "ui/views/controls/textfield/textfield_test_api.h" |
| |
| namespace ash { |
| using crosapi::mojom::ClipboardHistoryControllerShowSource; |
| |
| namespace { |
| |
| // Matchers -------------------------------------------------------------------- |
| |
| MATCHER_P2(MenuItemsMatch, labels, icons, "") { |
| if (arg.size() != labels.size() || arg.size() != icons.size()) { |
| return false; |
| } |
| |
| for (size_t index = 0; index < labels.size(); ++index) { |
| if (arg[index].label != labels[index] || |
| !gfx::test::AreImagesEqual(arg[index].icon, icons[index])) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| // Helper classes -------------------------------------------------------------- |
| |
| // A mocked clipboard history controller observer. |
| class MockObserver : public ClipboardHistoryController::Observer { |
| public: |
| MockObserver() { |
| scoped_observation_.Observe(ClipboardHistoryController::Get()); |
| } |
| |
| // ClipboardHistoryController::Observer: |
| MOCK_METHOD(void, OnClipboardHistoryItemsUpdated, (), (override)); |
| |
| private: |
| base::ScopedObservation<ClipboardHistoryController, |
| ClipboardHistoryController::Observer> |
| scoped_observation_{this}; |
| }; |
| |
| class MockClipboardImageModelFactory : public ClipboardImageModelFactory { |
| public: |
| MockClipboardImageModelFactory() = default; |
| MockClipboardImageModelFactory(const MockClipboardImageModelFactory&) = |
| delete; |
| MockClipboardImageModelFactory& operator=( |
| const MockClipboardImageModelFactory&) = delete; |
| ~MockClipboardImageModelFactory() override = default; |
| |
| // ClipboardImageModelFactory: |
| void Render(const base::UnguessableToken& clipboard_history_item_id, |
| const std::string& markup, |
| const gfx::Size& bounding_box_size, |
| ImageModelCallback callback) override { |
| // Return a dummy image as the render result. |
| std::move(callback).Run( |
| ui::ImageModel::FromImageSkia(gfx::ImageSkia::CreateFrom1xBitmap( |
| gfx::test::CreateBitmap(/*width=*/2, /*height=*/2)))); |
| } |
| |
| void CancelRequest(const base::UnguessableToken& request_id) override {} |
| |
| void Activate() override {} |
| |
| void Deactivate() override {} |
| |
| void RenderCurrentPendingRequests() override {} |
| |
| void OnShutdown() override {} |
| }; |
| |
| // Describes a menu item consisting of a label and an icon. |
| struct MenuItemDescriptor { |
| MenuItemDescriptor(const std::u16string& input_label, |
| const gfx::Image& input_icon) |
| : label(input_label), icon(input_icon) {} |
| const std::u16string label; |
| const gfx::Image icon; |
| }; |
| |
| // Helper functions ------------------------------------------------------------ |
| |
| void FlushMessageLoop() { |
| base::RunLoop run_loop; |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| void ExpectHistoryItemImageMatchesBitmap(const ClipboardHistoryItem& item, |
| const SkBitmap& expected_bitmap) { |
| EXPECT_EQ(item.display_format(), |
| crosapi::mojom::ClipboardHistoryDisplayFormat::kPng); |
| |
| const auto& image = item.display_image(); |
| ASSERT_TRUE(image.has_value()); |
| ASSERT_TRUE(image.value().IsImage()); |
| ASSERT_FALSE(image.value().IsEmpty()); |
| EXPECT_TRUE(gfx::BitmapsAreEqual(*image.value().GetImage().ToSkBitmap(), |
| expected_bitmap)); |
| } |
| |
| std::vector<ClipboardHistoryControllerShowSource> |
| GetClipboardHistoryShowSources() { |
| std::vector<ClipboardHistoryControllerShowSource> sources; |
| for (int i = |
| static_cast<int>(ClipboardHistoryControllerShowSource::kMinValue); |
| i <= static_cast<int>(ClipboardHistoryControllerShowSource::kMaxValue); |
| ++i) { |
| sources.push_back(static_cast<ClipboardHistoryControllerShowSource>(i)); |
| } |
| return sources; |
| } |
| |
| } // namespace |
| |
| class ClipboardHistoryControllerTest : public AshTestBase { |
| public: |
| ClipboardHistoryControllerTest() = default; |
| ClipboardHistoryControllerTest(const ClipboardHistoryControllerTest&) = |
| delete; |
| ClipboardHistoryControllerTest& operator=( |
| const ClipboardHistoryControllerTest&) = delete; |
| ~ClipboardHistoryControllerTest() override = default; |
| |
| // AshTestBase: |
| void SetUp() override { |
| AshTestBase::SetUp(); |
| mock_image_factory_ = std::make_unique<MockClipboardImageModelFactory>(); |
| GetClipboardHistoryController()->set_confirmed_operation_callback_for_test( |
| operation_confirmed_future_.GetCallback()); |
| } |
| |
| ClipboardHistoryControllerImpl* GetClipboardHistoryController() { |
| return Shell::Get()->clipboard_history_controller(); |
| } |
| |
| void ShowMenu() { PressAndReleaseKey(ui::VKEY_V, ui::EF_COMMAND_DOWN); } |
| |
| void WaitForOperationConfirmed() { |
| EXPECT_TRUE(operation_confirmed_future_.Take()); |
| } |
| |
| void WriteImageToClipboardAndConfirm(const SkBitmap& bitmap) { |
| { |
| ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste); |
| scw.WriteImage(bitmap); |
| } |
| WaitForOperationConfirmed(); |
| } |
| |
| void WriteTextToClipboardAndConfirm(const std::u16string& str) { |
| { |
| ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste); |
| scw.WriteText(str); |
| } |
| WaitForOperationConfirmed(); |
| } |
| |
| std::vector<ClipboardHistoryItem> GetHistoryValues() { |
| base::test::TestFuture<std::vector<ClipboardHistoryItem>> future; |
| GetClipboardHistoryController()->GetHistoryValues(future.GetCallback()); |
| return future.Take(); |
| } |
| |
| void TestEnteringLockScreen() { |
| // Querying clipboard history should return nothing if the screen is locked |
| // while the request is in progress. |
| GetClipboardHistoryController()->BlockGetHistoryValuesForTest(); |
| base::test::TestFuture<std::vector<ClipboardHistoryItem>> future; |
| GetClipboardHistoryController()->GetHistoryValues(future.GetCallback()); |
| EXPECT_FALSE(future.IsReady()); |
| |
| auto* session_controller = Shell::Get()->session_controller(); |
| session_controller->LockScreen(); |
| GetSessionControllerClient()->FlushForTest(); // `LockScreen()` is async. |
| EXPECT_TRUE(session_controller->IsScreenLocked()); |
| |
| GetClipboardHistoryController()->ResumeGetHistoryValuesForTest(); |
| auto locked_during_query_result = future.Take(); |
| EXPECT_TRUE(locked_during_query_result.empty()); |
| |
| // Querying clipboard history should return nothing if the screen is locked |
| // before the request is made. |
| auto locked_before_query_result = GetHistoryValues(); |
| EXPECT_TRUE(locked_before_query_result.empty()); |
| } |
| |
| protected: |
| base::test::RepeatingTestFuture<bool> operation_confirmed_future_; |
| |
| private: |
| std::unique_ptr<MockClipboardImageModelFactory> mock_image_factory_; |
| }; |
| |
| TEST_F(ClipboardHistoryControllerTest, EncodeImage) { |
| // Write a bitmap to ClipboardHistory. |
| SkBitmap test_bitmap = gfx::test::CreateBitmap(3, 2); |
| WriteImageToClipboardAndConfirm(test_bitmap); |
| |
| // The bitmap should be encoded to a PNG. Manually pry into the contents of |
| // the result to confirm that the newly-encoded PNG is included. |
| auto result = GetHistoryValues(); |
| ASSERT_EQ(result.size(), 1u); |
| |
| ExpectHistoryItemImageMatchesBitmap(result[0], test_bitmap); |
| } |
| |
| TEST_F(ClipboardHistoryControllerTest, EncodeMultipleImages) { |
| // Write a bunch of bitmaps to ClipboardHistory. |
| std::vector<const SkBitmap> test_bitmaps; |
| test_bitmaps.emplace_back(gfx::test::CreateBitmap(2, 1)); |
| test_bitmaps.emplace_back(gfx::test::CreateBitmap(3, 2)); |
| test_bitmaps.emplace_back(gfx::test::CreateBitmap(4, 3)); |
| for (const auto& test_bitmap : test_bitmaps) { |
| WriteImageToClipboardAndConfirm(test_bitmap); |
| } |
| |
| auto result = GetHistoryValues(); |
| auto num_results = result.size(); |
| ASSERT_EQ(num_results, test_bitmaps.size()); |
| |
| // The bitmaps should be encoded to PNGs. Manually pry into the contents of |
| // the result to confirm that the newly-encoded PNGs are included. History |
| // values should be sorted by recency. |
| for (uint i = 0; i < num_results; ++i) { |
| ExpectHistoryItemImageMatchesBitmap(result[i], |
| test_bitmaps[num_results - 1 - i]); |
| } |
| } |
| |
| TEST_F(ClipboardHistoryControllerTest, WriteBitmapWhileEncodingImage) { |
| // Write a bitmap to ClipboardHistory. |
| std::vector<const SkBitmap> test_bitmaps; |
| test_bitmaps.emplace_back(gfx::test::CreateBitmap(3, 2)); |
| test_bitmaps.emplace_back(gfx::test::CreateBitmap(4, 3)); |
| WriteImageToClipboardAndConfirm(test_bitmaps[0]); |
| |
| // Write another bitmap to the clipboard while encoding the first bitmap. |
| GetClipboardHistoryController() |
| ->set_new_bitmap_to_write_while_encoding_for_test(test_bitmaps[1]); |
| |
| // Make sure the second bitmap is written to the clipboard before history |
| // values are returned. |
| GetClipboardHistoryController()->BlockGetHistoryValuesForTest(); |
| base::test::TestFuture<std::vector<ClipboardHistoryItem>> future; |
| GetClipboardHistoryController()->GetHistoryValues(future.GetCallback()); |
| EXPECT_FALSE(future.IsReady()); |
| WaitForOperationConfirmed(); |
| |
| GetClipboardHistoryController()->ResumeGetHistoryValuesForTest(); |
| auto result = future.Take(); |
| auto num_results = result.size(); |
| ASSERT_EQ(num_results, test_bitmaps.size()); |
| |
| // Both bitmaps should be encoded to PNGs. Manually pry into the contents of |
| // the result to confirm that the newly-encoded PNGs are included. History |
| // values should be sorted by recency. |
| for (uint i = 0; i < num_results; ++i) { |
| ExpectHistoryItemImageMatchesBitmap(result[i], |
| test_bitmaps[num_results - 1 - i]); |
| } |
| } |
| |
| TEST_F(ClipboardHistoryControllerTest, LockedScreenText) { |
| // Write text to ClipboardHistory and verify that it can be retrieved. |
| WriteTextToClipboardAndConfirm(u"test"); |
| auto history_list = GetHistoryValues(); |
| ASSERT_EQ(history_list.size(), 1u); |
| ASSERT_EQ(history_list[0].display_text(), u"test"); |
| |
| TestEnteringLockScreen(); |
| } |
| |
| TEST_F(ClipboardHistoryControllerTest, LockedScreenImage) { |
| // Write a bitmap to ClipboardHistory and verify that it can be returned. |
| SkBitmap test_bitmap = gfx::test::CreateBitmap(3, 2); |
| WriteImageToClipboardAndConfirm(test_bitmap); |
| auto result = GetHistoryValues(); |
| ASSERT_EQ(result.size(), 1u); |
| ExpectHistoryItemImageMatchesBitmap(result[0], test_bitmap); |
| |
| TestEnteringLockScreen(); |
| } |
| |
| using ClipboardHistoryControllerObserverTest = ClipboardHistoryControllerTest; |
| |
| // Verifies that clipboard history controller notifies observers of clipboard |
| // history item updates as expected when adding or removing items. |
| TEST_F(ClipboardHistoryControllerObserverTest, AddAndRemoveItem) { |
| MockObserver mock_observer; |
| EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated).Times(3); |
| WriteTextToClipboardAndConfirm(u"A"); |
| WriteTextToClipboardAndConfirm(u"B"); |
| ClipboardHistoryController::Get()->DeleteClipboardItemById( |
| GetHistoryValues()[0].id().ToString()); |
| GetClipboardHistoryController()->FireItemUpdateNotificationTimerForTest(); |
| } |
| |
| // Verifies that clipboard history controller notifies observers once when |
| // clipboard history item addition causes overflow. |
| TEST_F(ClipboardHistoryControllerObserverTest, Overflow) { |
| // Add five items to reach the clipboard history size limit. |
| MockObserver mock_observer; |
| EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated).Times(5); |
| WriteTextToClipboardAndConfirm(u"A"); |
| WriteTextToClipboardAndConfirm(u"B"); |
| WriteTextToClipboardAndConfirm(u"C"); |
| WriteTextToClipboardAndConfirm(u"D"); |
| WriteTextToClipboardAndConfirm(u"E"); |
| EXPECT_EQ(GetHistoryValues().size(), |
| static_cast<size_t>(clipboard_history_util::kMaxClipboardItems)); |
| testing::Mock::VerifyAndClearExpectations(&mock_observer); |
| |
| // Notify `mock_observer` once when item addition causes overflow. |
| EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated); |
| WriteTextToClipboardAndConfirm(u"F"); |
| EXPECT_EQ(GetHistoryValues().size(), |
| static_cast<size_t>(clipboard_history_util::kMaxClipboardItems)); |
| } |
| |
| TEST_F(ClipboardHistoryControllerObserverTest, |
| ChangeSessionStateWithEmptyHistory) { |
| // Clipboard history is empty. Therefore, the clipboard history controller |
| // should not notify observers when the session state changes. |
| MockObserver mock_observer; |
| EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated).Times(0); |
| TestSessionControllerClient* test_session_client = |
| GetSessionControllerClient(); |
| test_session_client->SetSessionState(session_manager::SessionState::LOCKED); |
| test_session_client->FlushForTest(); |
| test_session_client->SetSessionState(session_manager::SessionState::ACTIVE); |
| test_session_client->FlushForTest(); |
| testing::Mock::VerifyAndClearExpectations(&mock_observer); |
| |
| // Notify `mock_observer` when a new clipboard history item arrives. |
| EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated); |
| WriteTextToClipboardAndConfirm(u"A"); |
| } |
| |
| TEST_F(ClipboardHistoryControllerObserverTest, |
| ChangeSessionStateWithNonEmptyHistory) { |
| // Notify `mock_observer` once when adding a clipboard history item. |
| MockObserver mock_observer; |
| EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated); |
| WriteTextToClipboardAndConfirm(u"A"); |
| testing::Mock::VerifyAndClearExpectations(&mock_observer); |
| |
| // Notify `mock_observer` once when clipboard history becomes disabled. |
| EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated); |
| TestSessionControllerClient* test_session_client = |
| GetSessionControllerClient(); |
| test_session_client->SetSessionState(session_manager::SessionState::LOCKED); |
| test_session_client->FlushForTest(); |
| testing::Mock::VerifyAndClearExpectations(&mock_observer); |
| |
| // Do not notify `mock_observer` when switching to another session state where |
| // clipboard history is still disabled. |
| EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated).Times(0); |
| test_session_client->SetSessionState( |
| session_manager::SessionState::LOGGED_IN_NOT_ACTIVE); |
| test_session_client->FlushForTest(); |
| testing::Mock::VerifyAndClearExpectations(&mock_observer); |
| |
| // Notify `mock_observer` once when clipboard history becomes enabled. |
| EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated); |
| test_session_client->SetSessionState(session_manager::SessionState::ACTIVE); |
| test_session_client->FlushForTest(); |
| } |
| |
| class ClipboardHistoryControllerWithTextfieldTest |
| : public ClipboardHistoryControllerTest { |
| public: |
| // ClipboardHistoryControllerTest: |
| void SetUp() override { |
| ClipboardHistoryControllerTest::SetUp(); |
| |
| textfield_widget_ = CreateFramelessTestWidget(); |
| textfield_widget_->SetBounds(gfx::Rect(0, 0, 100, 100)); |
| textfield_ = textfield_widget_->SetContentsView( |
| std::make_unique<views::Textfield>()); |
| textfield_->SetAccessibleName(u"Textfield"); |
| textfield_->SetFocusBehavior(views::View::FocusBehavior::ALWAYS); |
| |
| // Focus the textfield and confirm initial state. |
| textfield_->RequestFocus(); |
| ASSERT_TRUE(textfield_->HasFocus()); |
| ASSERT_TRUE(textfield_->GetText().empty()); |
| } |
| |
| std::unique_ptr<views::Widget> textfield_widget_; |
| views::Textfield* textfield_; |
| }; |
| |
| TEST_F(ClipboardHistoryControllerWithTextfieldTest, PasteClipboardItemById) { |
| // Write four items to the clipboard. |
| WriteTextToClipboardAndConfirm(u"A"); |
| WriteTextToClipboardAndConfirm(u"B"); |
| WriteTextToClipboardAndConfirm(u"C"); |
| WriteTextToClipboardAndConfirm(u"D"); |
| const std::vector<ClipboardHistoryItem> items = GetHistoryValues(); |
| ASSERT_EQ(items.size(), 4u); |
| |
| // Set a zero duration to make test code simpler. |
| GetClipboardHistoryController()->set_buffer_restoration_delay_for_test( |
| base::TimeDelta()); |
| |
| struct { |
| size_t paste_data_index; |
| crosapi::mojom::ClipboardHistoryControllerShowSource paste_source; |
| int event_flags; |
| ClipboardHistoryControllerImpl::ClipboardHistoryPasteType paste_type; |
| } test_cases[] = { |
| {/*paste_data_index=*/0, |
| /*paste_source=*/ |
| crosapi::mojom::ClipboardHistoryControllerShowSource::kVirtualKeyboard, |
| /*event_flags=*/ui::EF_NONE, |
| /*paste_type=*/ |
| ClipboardHistoryControllerImpl::ClipboardHistoryPasteType:: |
| kRichTextVirtualKeyboard}, |
| {/*paste_data_index=*/1, |
| /*paste_source=*/ |
| crosapi::mojom::ClipboardHistoryControllerShowSource:: |
| kTextfieldContextMenu, |
| /*event_flags=*/ui::EF_MOUSE_BUTTON, |
| /*paste_type=*/ |
| ClipboardHistoryControllerImpl::ClipboardHistoryPasteType:: |
| kRichTextMouse}, |
| {/*paste_data_index=*/2, |
| /*paste_source=*/ |
| crosapi::mojom::ClipboardHistoryControllerShowSource:: |
| kRenderViewContextMenu, |
| /*event_flags=*/ui::EF_SHIFT_DOWN | ui::EF_FROM_TOUCH, |
| /*paste_type=*/ |
| ClipboardHistoryControllerImpl::ClipboardHistoryPasteType:: |
| kPlainTextTouch}}; |
| |
| for (auto& [paste_data_index, paste_source, event_flags, paste_type] : |
| test_cases) { |
| base::HistogramTester histogram_tester; |
| textfield_->SetText(std::u16string()); |
| ClipboardHistoryController::Get()->PasteClipboardItemById( |
| items[paste_data_index].id().ToString(), event_flags, paste_source); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Verify the contents of `textfield_` and histograms. |
| EXPECT_EQ(textfield_->GetText(), items[paste_data_index].display_text()); |
| histogram_tester.ExpectBucketCount("Ash.ClipboardHistory.PasteType", |
| paste_type, |
| /*expected_count=*/1); |
| histogram_tester.ExpectBucketCount("Ash.ClipboardHistory.PasteSource", |
| paste_source, |
| /*expected_count=*/1); |
| histogram_tester.ExpectBucketCount( |
| "Ash.ClipboardHistory.ContextMenu.MenuOptionSelected", |
| paste_data_index + 1, /*expected_count=*/1); |
| } |
| } |
| |
| // Base class for tests of Clipboard History parameterized by whether the |
| // `kClipboardHistoryRefresh` feature flag is enabled. |
| class ClipboardHistoryControllerRefreshTest |
| : public ClipboardHistoryControllerTest, |
| public testing::WithParamInterface</*refresh_enabled=*/bool> { |
| public: |
| ClipboardHistoryControllerRefreshTest() { |
| std::vector<base::test::FeatureRef> refresh_features = { |
| chromeos::features::kClipboardHistoryRefresh, |
| chromeos::features::kJelly}; |
| std::vector<base::test::FeatureRef> enabled_features; |
| std::vector<base::test::FeatureRef> disabled_features; |
| (IsClipboardHistoryRefreshEnabled() ? enabled_features : disabled_features) |
| .swap(refresh_features); |
| scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features); |
| } |
| |
| bool IsClipboardHistoryRefreshEnabled() const { return GetParam(); } |
| |
| // Some toasts can display on multiple root windows, so the caller can use |
| // `root_window` to target a toast on a specific root window. |
| ToastOverlay* GetCurrentOverlay( |
| aura::Window* root_window = Shell::GetRootWindowForNewWindows()) { |
| return Shell::Get()->toast_manager()->GetCurrentOverlayForTesting( |
| root_window); |
| } |
| |
| views::LabelButton* GetDismissButton( |
| aura::Window* root_window = Shell::GetRootWindowForNewWindows()) { |
| ToastOverlay* overlay = GetCurrentOverlay(root_window); |
| DCHECK(overlay); |
| return overlay->dismiss_button_for_testing(); |
| } |
| |
| void ClickDismissButton( |
| aura::Window* root_window = Shell::GetRootWindowForNewWindows()) { |
| views::LabelButton* dismiss_button = GetDismissButton(root_window); |
| const gfx::Point button_center = |
| dismiss_button->GetBoundsInScreen().CenterPoint(); |
| auto* event_generator = GetEventGenerator(); |
| event_generator->MoveMouseTo(button_center); |
| event_generator->ClickLeftButton(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| ClipboardHistoryControllerRefreshTest, |
| /*refresh_enabled=*/testing::Bool()); |
| |
| // Tests that search + v with no history fails to show a menu. |
| TEST_P(ClipboardHistoryControllerRefreshTest, NoHistoryNoMenu) { |
| base::HistogramTester histogram_tester; |
| ShowMenu(); |
| |
| EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing()); |
| histogram_tester.ExpectTotalCount( |
| "Ash.ClipboardHistory.ContextMenu.NumberOfItemsShown", 0); |
| histogram_tester.ExpectTotalCount( |
| "Ash.ClipboardHistory.ContextMenu.UserJourneyTime", 0); |
| histogram_tester.ExpectTotalCount( |
| "Ash.ClipboardHistory.ContextMenu.DisplayFormatShown", 0); |
| } |
| |
| // Tests that search + v shows a menu when there is something to show. |
| TEST_P(ClipboardHistoryControllerRefreshTest, ShowMenu) { |
| base::HistogramTester histogram_tester; |
| |
| // Copy something to enable the clipboard history menu. |
| WriteTextToClipboardAndConfirm(u"test"); |
| |
| // TODO(b/267694484): Do not fork these values once the menu can show items |
| // with the refresh feature enabled. |
| const int num_items_shown = IsClipboardHistoryRefreshEnabled() ? 0 : 1; |
| int num_items_shown_samples = 0; |
| ShowMenu(); |
| |
| if (!IsClipboardHistoryRefreshEnabled()) { |
| ++num_items_shown_samples; |
| } |
| |
| EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing()); |
| // No user journey time should be recorded as the menu is still showing. |
| histogram_tester.ExpectTotalCount( |
| "Ash.ClipboardHistory.ContextMenu.UserJourneyTime", 0); |
| histogram_tester.ExpectBucketCount( |
| "Ash.ClipboardHistory.ContextMenu.NumberOfItemsShown", num_items_shown, |
| num_items_shown_samples); |
| histogram_tester.ExpectTotalCount( |
| "Ash.ClipboardHistory.ContextMenu.DisplayFormatShown", |
| num_items_shown_samples); |
| |
| // Hide the menu. |
| PressAndReleaseKey(ui::VKEY_ESCAPE); |
| |
| EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing()); |
| histogram_tester.ExpectTotalCount( |
| "Ash.ClipboardHistory.ContextMenu.UserJourneyTime", 1); |
| histogram_tester.ExpectBucketCount( |
| "Ash.ClipboardHistory.ContextMenu.NumberOfItemsShown", num_items_shown, |
| num_items_shown_samples); |
| histogram_tester.ExpectTotalCount( |
| "Ash.ClipboardHistory.ContextMenu.DisplayFormatShown", |
| num_items_shown_samples); |
| |
| // Reshow the menu. |
| ShowMenu(); |
| |
| if (!IsClipboardHistoryRefreshEnabled()) { |
| ++num_items_shown_samples; |
| } |
| |
| EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing()); |
| |
| // No new UserJourneyTime histogram should be recorded as the menu is |
| // still showing. |
| histogram_tester.ExpectTotalCount( |
| "Ash.ClipboardHistory.ContextMenu.UserJourneyTime", 1); |
| histogram_tester.ExpectBucketCount( |
| "Ash.ClipboardHistory.ContextMenu.NumberOfItemsShown", num_items_shown, |
| num_items_shown_samples); |
| histogram_tester.ExpectTotalCount( |
| "Ash.ClipboardHistory.ContextMenu.DisplayFormatShown", |
| num_items_shown_samples); |
| |
| // Hide the menu. |
| PressAndReleaseKey(ui::VKEY_ESCAPE); |
| |
| EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing()); |
| histogram_tester.ExpectTotalCount( |
| "Ash.ClipboardHistory.ContextMenu.UserJourneyTime", 2); |
| histogram_tester.ExpectBucketCount( |
| "Ash.ClipboardHistory.ContextMenu.NumberOfItemsShown", num_items_shown, |
| num_items_shown_samples); |
| histogram_tester.ExpectTotalCount( |
| "Ash.ClipboardHistory.ContextMenu.DisplayFormatShown", |
| num_items_shown_samples); |
| } |
| |
| // Verifies that the clipboard history is disabled in some user modes, which |
| // means that the clipboard history should not be recorded and meanwhile the |
| // menu view should not show (https://crbug.com/1100739). |
| TEST_P(ClipboardHistoryControllerRefreshTest, VerifyAvailabilityInUserModes) { |
| // Write one item into the clipboard history. |
| WriteTextToClipboardAndConfirm(u"text"); |
| |
| constexpr struct { |
| user_manager::UserType user_type; |
| bool is_enabled; |
| } kTestCases[] = {{user_manager::USER_TYPE_REGULAR, true}, |
| {user_manager::USER_TYPE_GUEST, true}, |
| {user_manager::USER_TYPE_PUBLIC_ACCOUNT, false}, |
| {user_manager::USER_TYPE_KIOSK_APP, false}, |
| {user_manager::USER_TYPE_CHILD, true}, |
| {user_manager::USER_TYPE_ARC_KIOSK_APP, false}, |
| {user_manager::USER_TYPE_WEB_KIOSK_APP, false}}; |
| |
| UserSession session; |
| session.session_id = 1u; |
| session.user_info.account_id = AccountId::FromUserEmail("user1@test.com"); |
| session.user_info.display_name = "User 1"; |
| session.user_info.display_email = "user1@test.com"; |
| |
| for (const auto& test_case : kTestCases) { |
| // Switch to the target user mode. |
| session.user_info.type = test_case.user_type; |
| Shell::Get()->session_controller()->UpdateUserSession(session); |
| |
| // Write a new item into the clipboard buffer. |
| { |
| ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste); |
| scw.WriteText(u"test"); |
| } |
| |
| if (test_case.is_enabled) { |
| WaitForOperationConfirmed(); |
| } else { |
| FlushMessageLoop(); |
| // Note: This check might not catch a scenario where a mode expected to be |
| // disabled actually allows writes to go through, because the operation |
| // might not have finished yet in that case. The history verification |
| // below mitigates the chance that such a bug would not be caught. |
| EXPECT_TRUE(operation_confirmed_future_.IsEmpty()); |
| } |
| |
| const std::list<ClipboardHistoryItem>& items = |
| Shell::Get()->clipboard_history_controller()->history()->GetItems(); |
| if (test_case.is_enabled) { |
| // Verify that the new item should be included in the clipboard history |
| // and the menu should be able to show. |
| EXPECT_EQ(items.size(), 2u); |
| |
| ShowMenu(); |
| |
| EXPECT_TRUE( |
| Shell::Get()->clipboard_history_controller()->IsMenuShowing()); |
| |
| PressAndReleaseKey(ui::VKEY_ESCAPE); |
| |
| EXPECT_FALSE( |
| Shell::Get()->clipboard_history_controller()->IsMenuShowing()); |
| |
| if (IsClipboardHistoryRefreshEnabled()) { |
| // Wait for the clipboard manager to be fully destroyed so that the |
| // presence of an open modal window does not block the next test case's |
| // accelerator action from being performed. |
| FlushMessageLoop(); |
| } |
| |
| // Restore the clipboard history by removing the new item. |
| ClipboardHistory* clipboard_history = const_cast<ClipboardHistory*>( |
| Shell::Get()->clipboard_history_controller()->history()); |
| clipboard_history->RemoveItemForId(items.begin()->id()); |
| } else { |
| // Verify that the new item should not be written into the clipboard |
| // history. The menu cannot show although the clipboard history is |
| // non-empty. |
| EXPECT_EQ(items.size(), 1u); |
| |
| ShowMenu(); |
| |
| EXPECT_FALSE( |
| Shell::Get()->clipboard_history_controller()->IsMenuShowing()); |
| } |
| } |
| } |
| |
| // Verifies that the clipboard history menu is disabled when the screen for |
| // user adding shows. |
| TEST_P(ClipboardHistoryControllerRefreshTest, DisableInUserAddingScreen) { |
| WriteTextToClipboardAndConfirm(u"text"); |
| |
| // Emulate that the user adding screen displays. |
| Shell::Get()->session_controller()->ShowMultiProfileLogin(); |
| |
| // Try to show the clipboard history menu; verify that the menu does not show. |
| ShowMenu(); |
| EXPECT_FALSE(Shell::Get()->clipboard_history_controller()->IsMenuShowing()); |
| } |
| |
| // Tests that pressing and holding VKEY_V, then the search key (EF_COMMAND_DOWN) |
| // does not show the AppList. |
| TEST_P(ClipboardHistoryControllerRefreshTest, VThenSearchDoesNotShowLauncher) { |
| GetEventGenerator()->PressKey(ui::VKEY_V, ui::EF_NONE); |
| GetEventGenerator()->PressKey(ui::VKEY_LWIN, ui::EF_NONE); |
| |
| // Release VKEY_V, which could trigger a key released accelerator. |
| GetEventGenerator()->ReleaseKey(ui::VKEY_V, ui::EF_NONE); |
| |
| EXPECT_FALSE(Shell::Get()->app_list_controller()->IsVisible( |
| /*display_id=*/absl::nullopt)); |
| |
| // Release VKEY_LWIN(search/launcher), which could trigger the app list. |
| GetEventGenerator()->ReleaseKey(ui::VKEY_LWIN, ui::EF_NONE); |
| |
| EXPECT_FALSE(Shell::Get()->app_list_controller()->IsVisible( |
| /*display_id=*/absl::nullopt)); |
| } |
| |
| // Tests that clearing the clipboard clears ClipboardHistory |
| TEST_P(ClipboardHistoryControllerRefreshTest, ClearClipboardClearsHistory) { |
| // Write a single item to ClipboardHistory. |
| WriteTextToClipboardAndConfirm(u"test"); |
| |
| // Clear the clipboard. |
| ui::Clipboard::GetForCurrentThread()->Clear(ui::ClipboardBuffer::kCopyPaste); |
| FlushMessageLoop(); |
| |
| // History should also be cleared. |
| const std::list<ClipboardHistoryItem>& items = |
| Shell::Get()->clipboard_history_controller()->history()->GetItems(); |
| EXPECT_TRUE(items.empty()); |
| |
| ShowMenu(); |
| |
| EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing()); |
| } |
| |
| // Tests that clearing the clipboard closes the ClipboardHistory menu. |
| TEST_P(ClipboardHistoryControllerRefreshTest, |
| ClearingClipboardClosesClipboardHistory) { |
| // Write a single item to ClipboardHistory. |
| WriteTextToClipboardAndConfirm(u"test"); |
| |
| ASSERT_TRUE(Shell::Get()->cursor_manager()->IsCursorVisible()); |
| |
| ShowMenu(); |
| EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing()); |
| |
| // The cursor is visible after showing the clipboard history menu through |
| // the accelerator. |
| EXPECT_TRUE(Shell::Get()->cursor_manager()->IsCursorVisible()); |
| |
| ui::Clipboard::GetForCurrentThread()->Clear(ui::ClipboardBuffer::kCopyPaste); |
| FlushMessageLoop(); |
| |
| EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing()); |
| } |
| |
| // Tests that a toast is shown if something was copied to clipboard history. |
| TEST_P(ClipboardHistoryControllerRefreshTest, ShowToast) { |
| // Copy something to enable the clipboard history menu. |
| WriteTextToClipboardAndConfirm(u"test"); |
| |
| ToastManagerImpl* manager_ = Shell::Get()->toast_manager(); |
| if (IsClipboardHistoryRefreshEnabled()) { |
| EXPECT_TRUE(manager_->IsRunning(kClipboardCopyToastId)); |
| ClickDismissButton(); |
| EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing()); |
| } else { |
| EXPECT_FALSE(manager_->IsRunning(kClipboardCopyToastId)); |
| } |
| } |
| |
| class ClipboardHistoryControllerShowSourceTest |
| : public ClipboardHistoryControllerTest, |
| public testing::WithParamInterface< |
| std::tuple<ClipboardHistoryControllerShowSource, |
| /*refresh_enabled=*/bool>> { |
| public: |
| ClipboardHistoryControllerShowSourceTest() { |
| std::vector<base::test::FeatureRef> refresh_features = { |
| chromeos::features::kClipboardHistoryRefresh, |
| chromeos::features::kJelly}; |
| std::vector<base::test::FeatureRef> enabled_features; |
| std::vector<base::test::FeatureRef> disabled_features; |
| (IsClipboardHistoryRefreshEnabled() ? enabled_features : disabled_features) |
| .swap(refresh_features); |
| scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features); |
| } |
| |
| ClipboardHistoryControllerShowSource GetSource() const { |
| return std::get<0>(GetParam()); |
| } |
| |
| bool IsClipboardHistoryRefreshEnabled() const { |
| return std::get<1>(GetParam()); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| ClipboardHistoryControllerShowSourceTest, |
| testing::Combine(testing::ValuesIn(GetClipboardHistoryShowSources()), |
| /*refresh_enabled=*/testing::Bool())); |
| |
| // Tests that `ShowMenu()` returns whether the menu was shown successfully. |
| TEST_P(ClipboardHistoryControllerShowSourceTest, ShowMenuReturnsSuccess) { |
| base::HistogramTester histogram_tester; |
| |
| // Try to show the menu without populating the clipboard. The menu should not |
| // show. |
| EXPECT_FALSE(GetClipboardHistoryController()->ShowMenu( |
| gfx::Rect(), ui::MenuSourceType::MENU_SOURCE_NONE, GetSource())); |
| EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing()); |
| histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ContextMenu.ShowMenu", |
| /*expected_count=*/0); |
| |
| // Copy something to enable the clipboard history menu. |
| WriteTextToClipboardAndConfirm(u"test"); |
| |
| // Try to show the menu with the screen locked. The menu should not show. |
| auto* session_controller = Shell::Get()->session_controller(); |
| session_controller->LockScreen(); |
| GetSessionControllerClient()->FlushForTest(); |
| EXPECT_TRUE(session_controller->IsScreenLocked()); |
| |
| EXPECT_FALSE(GetClipboardHistoryController()->ShowMenu( |
| gfx::Rect(), ui::MenuSourceType::MENU_SOURCE_NONE, GetSource())); |
| EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing()); |
| histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ContextMenu.ShowMenu", |
| /*expected_count=*/0); |
| |
| session_controller->HideLockScreen(); |
| GetSessionControllerClient()->FlushForTest(); |
| EXPECT_FALSE(session_controller->IsScreenLocked()); |
| |
| // Show the menu. |
| EXPECT_TRUE(GetClipboardHistoryController()->ShowMenu( |
| gfx::Rect(), ui::MenuSourceType::MENU_SOURCE_NONE, GetSource())); |
| EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing()); |
| histogram_tester.ExpectUniqueSample( |
| "Ash.ClipboardHistory.ContextMenu.ShowMenu", GetSource(), |
| /*expected_bucket_count=*/1); |
| |
| // Try to show the menu again without closing the active menu. The menu should |
| // still be showing, but this attempt should fail. |
| EXPECT_FALSE(GetClipboardHistoryController()->ShowMenu( |
| gfx::Rect(), ui::MenuSourceType::MENU_SOURCE_NONE, GetSource())); |
| EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing()); |
| histogram_tester.ExpectUniqueSample( |
| "Ash.ClipboardHistory.ContextMenu.ShowMenu", GetSource(), |
| /*expected_bucket_count=*/1); |
| } |
| |
| // Tests that the client-provided `OnMenuClosingCallback` runs before the menu |
| // closes. |
| TEST_P(ClipboardHistoryControllerShowSourceTest, OnMenuClosingCallback) { |
| if (IsClipboardHistoryRefreshEnabled()) { |
| // TODO(b/267694484): Do not skip this test case once the menu can paste |
| // with the refresh feature enabled. |
| GTEST_SKIP(); |
| } |
| |
| base::test::RepeatingTestFuture<bool> on_menu_closing_future; |
| base::HistogramTester histogram_tester; |
| |
| // Copy something to enable the clipboard history menu. |
| WriteTextToClipboardAndConfirm(u"test"); |
| |
| gfx::Rect test_window_rect(100, 100, 100, 100); |
| std::unique_ptr<aura::Window> window(CreateTestWindow(test_window_rect)); |
| |
| // Show the menu with an `OnMenuClosingCallback`. |
| GetClipboardHistoryController()->ShowMenu( |
| test_window_rect, ui::MenuSourceType::MENU_SOURCE_NONE, GetSource(), |
| on_menu_closing_future.GetCallback()); |
| EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing()); |
| EXPECT_TRUE(on_menu_closing_future.IsEmpty()); |
| |
| // Hide the menu. The callback should indicate that nothing will be pasted. |
| PressAndReleaseKey(ui::VKEY_ESCAPE); |
| EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing()); |
| EXPECT_FALSE(on_menu_closing_future.Take()); |
| |
| FlushMessageLoop(); |
| histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.PasteSource", |
| /*expected_count=*/0); |
| |
| // Show the menu again. |
| GetClipboardHistoryController()->ShowMenu( |
| test_window_rect, ui::MenuSourceType::MENU_SOURCE_NONE, GetSource(), |
| on_menu_closing_future.GetCallback()); |
| EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing()); |
| EXPECT_TRUE(on_menu_closing_future.IsEmpty()); |
| |
| // Toggle the menu closed. The callback should indicate a pending paste. |
| PressAndReleaseKey(ui::VKEY_V, ui::EF_COMMAND_DOWN); |
| EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing()); |
| EXPECT_TRUE(on_menu_closing_future.Take()); |
| |
| FlushMessageLoop(); |
| histogram_tester.ExpectUniqueSample("Ash.ClipboardHistory.PasteSource", |
| GetSource(), |
| /*expected_bucket_count=*/1); |
| } |
| |
| // TODO(b/278109818): Move clipboard history refresh tests into a separate test |
| // file. |
| |
| // A parameterized test base to verify the clipboard history refresh feature on |
| // every display format. |
| // Each test param is such a tuple: |
| // 1. The first value is a boolean indicating whether the clipboard history |
| // refresh feature is enabled; |
| // 2. The second value is the display format under test. |
| class ClipboardHistoryRefreshDisplayFormatTest |
| : public ClipboardHistoryControllerWithTextfieldTest, |
| public testing::WithParamInterface< |
| std::tuple</*enable_clipboard_history_refresh=*/bool, |
| /*display_format_under_test=*/crosapi::mojom:: |
| ClipboardHistoryDisplayFormat>> { |
| public: |
| ClipboardHistoryRefreshDisplayFormatTest() { |
| std::vector<base::test::FeatureRef> refresh_features = { |
| chromeos::features::kClipboardHistoryRefresh, |
| chromeos::features::kJelly}; |
| std::vector<base::test::FeatureRef> enabled_features; |
| std::vector<base::test::FeatureRef> disabled_features; |
| (std::get<0>(GetParam()) ? enabled_features : disabled_features) |
| .swap(refresh_features); |
| scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features); |
| } |
| |
| void ShowTextfieldContextMenu(views::View* textfield) { |
| GetEventGenerator()->MoveMouseTo( |
| textfield->GetBoundsInScreen().CenterPoint()); |
| GetEventGenerator()->ClickRightButton(); |
| } |
| |
| // Writes clipboard data. Returns the the descriptors of the expected |
| // clipboard history submenu items. The returned arrays follow the reverse |
| // clipboard data writing order. Returns an empty array if the clipboard |
| // history refresh feature is disabled. |
| std::vector<MenuItemDescriptor> WriteClipboardDataBasedOnParam() { |
| const ui::ColorProvider* color_provider = GetPrimaryWindowColorProvider(); |
| CHECK(color_provider); |
| auto get_icon = [color_provider](const gfx::VectorIcon& icon) { |
| return gfx::Image(ui::ImageModel::FromVectorIcon(icon, |
| ui::kColorSysSecondary, |
| /*icon_size=*/20) |
| .Rasterize(color_provider)); |
| }; |
| |
| const bool refresh_feature_enabled = |
| chromeos::features::IsClipboardHistoryRefreshEnabled(); |
| switch (GetDisplayFormat()) { |
| case crosapi::mojom::ClipboardHistoryDisplayFormat::kText: |
| WriteTextToClipboardAndConfirm(u"A"); |
| WriteTextToClipboardAndConfirm(u"B"); |
| if (refresh_feature_enabled) { |
| return {{u"B", get_icon(chromeos::kTextIcon)}, |
| {u"A", get_icon(chromeos::kTextIcon)}}; |
| } |
| break; |
| case crosapi::mojom::ClipboardHistoryDisplayFormat::kPng: |
| WriteImageToClipboardAndConfirm( |
| gfx::test::CreateBitmap(/*width=*/3, /*height=*/3)); |
| WriteImageToClipboardAndConfirm( |
| gfx::test::CreateBitmap(/*width=*/2, /*height=*/2)); |
| if (refresh_feature_enabled) { |
| return {{u"Image", get_icon(chromeos::kFiletypeImageIcon)}, |
| {u"Image", get_icon(chromeos::kFiletypeImageIcon)}}; |
| } |
| break; |
| case crosapi::mojom::ClipboardHistoryDisplayFormat::kHtml: |
| WriteHtmlAndConfirm("<table>A</table>"); |
| WriteHtmlAndConfirm("<table>B></table>"); |
| if (refresh_feature_enabled) { |
| return {{u"HTML Content", get_icon(vector_icons::kCodeIcon)}, |
| {u"HTML Content", get_icon(vector_icons::kCodeIcon)}}; |
| } |
| break; |
| case crosapi::mojom::ClipboardHistoryDisplayFormat::kFile: |
| // Use dummy file paths. The corresponding files do not have to exist |
| // because only file extensions are required to calculate icons. |
| |
| // Copy a single file. |
| WriteFilePathsAndConfirm({u"dummy_file.webm"}); |
| |
| // Copy multiple files at the same time. |
| WriteFilePathsAndConfirm({u"dummy_child1.jpg", u"dummy_child2.png"}); |
| |
| if (refresh_feature_enabled) { |
| return {{u"dummy_child1.jpg, dummy_child2.png", |
| get_icon(vector_icons::kContentCopyIcon)}, |
| {u"dummy_file.webm", get_icon(chromeos::kFiletypeVideoIcon)}}; |
| } |
| break; |
| case crosapi::mojom::ClipboardHistoryDisplayFormat::kUnknown: |
| NOTREACHED_NORETURN(); |
| } |
| |
| return {}; |
| } |
| |
| void WriteFilePathsAndConfirm(const std::vector<std::u16string>& file_paths) { |
| { |
| base::Pickle pickle; |
| ui::WriteCustomDataToPickle( |
| std::unordered_map<std::u16string, std::u16string>( |
| {{u"fs/sources", base::JoinString(file_paths, u"\n")}}), |
| &pickle); |
| ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste); |
| scw.WritePickledData(pickle, |
| ui::ClipboardFormatType::WebCustomDataType()); |
| } |
| |
| WaitForOperationConfirmed(); |
| } |
| |
| void WriteHtmlAndConfirm(const std::string& html) { |
| { |
| ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste); |
| scw.WriteHTML(base::UTF8ToUTF16(html), /*source_url=*/"", |
| ui::ClipboardContentType::kUnsanitized); |
| } |
| |
| WaitForOperationConfirmed(); |
| } |
| |
| crosapi::mojom::ClipboardHistoryDisplayFormat GetDisplayFormat() const { |
| return std::get<1>(GetParam()); |
| } |
| |
| const ui::ColorProvider* GetPrimaryWindowColorProvider() { |
| auto* color_provider_source = ColorUtil::GetColorProviderSourceForWindow( |
| Shell::GetPrimaryRootWindow()); |
| auto* color_provider = color_provider_source->GetColorProvider(); |
| return color_provider; |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| ClipboardHistoryRefreshDisplayFormatTest, |
| testing::Combine( |
| /*enable_clipboard_history_refresh=*/testing::Bool(), |
| /*display_format_under_test=*/testing::Values( |
| crosapi::mojom::ClipboardHistoryDisplayFormat::kText, |
| crosapi::mojom::ClipboardHistoryDisplayFormat::kPng, |
| crosapi::mojom::ClipboardHistoryDisplayFormat::kHtml, |
| crosapi::mojom::ClipboardHistoryDisplayFormat::kFile))); |
| |
| // Verifies that the clipboard history submenu model of the text services |
| // context menu in Ash works as expected. |
| TEST_P(ClipboardHistoryRefreshDisplayFormatTest, TextServicesSubMenu) { |
| // Show the textfield context menu before writing any clipboard data. |
| ShowTextfieldContextMenu(textfield_); |
| |
| views::TextfieldTestApi api(textfield_); |
| ui::MenuModel* const root_model = api.context_menu_contents(); |
| ASSERT_TRUE(root_model); |
| |
| // Search the parent model and the command index of |
| // `IDS_APP_SHOW_CLIPBOARD_HISTORY`. |
| ui::MenuModel* target_command_parent_model = root_model; |
| size_t target_command_index = 0u; |
| ui::MenuModel::GetModelAndIndexForCommandId(IDS_APP_SHOW_CLIPBOARD_HISTORY, |
| &target_command_parent_model, |
| &target_command_index); |
| EXPECT_EQ(target_command_parent_model, root_model); |
| EXPECT_GT(target_command_index, 0u); |
| |
| // The clipboard history menu item should be disabled when there is no |
| // clipboard history. |
| EXPECT_FALSE(target_command_parent_model->IsEnabledAt(target_command_index)); |
| |
| const bool is_refresh_enabled = |
| chromeos::features::IsClipboardHistoryRefreshEnabled(); |
| |
| // Write clipboard data. |
| const std::vector<MenuItemDescriptor> expected_submenu_items = |
| WriteClipboardDataBasedOnParam(); |
| ASSERT_EQ(expected_submenu_items.empty(), !is_refresh_enabled); |
| |
| // Close the textfield menu then reshow. |
| GetEventGenerator()->PressAndReleaseKey(ui::KeyboardCode::VKEY_ESCAPE); |
| ShowTextfieldContextMenu(textfield_); |
| |
| // Check `submenu_model` if any. Reuse `target_command_index` since the |
| // context menu model structure should not change. |
| target_command_parent_model = api.context_menu_contents(); |
| ui::MenuModel* const submenu_model = |
| target_command_parent_model->GetSubmenuModelAt(target_command_index); |
| |
| // The clipboard history menu item should be enabled when there is clipboard |
| // history. |
| EXPECT_TRUE(target_command_parent_model->IsEnabledAt(target_command_index)); |
| |
| if (is_refresh_enabled) { |
| // If the refresh feature is enabled, the clipboard history menu item is a |
| // submenu item. |
| EXPECT_EQ(target_command_parent_model->GetTypeAt(target_command_index), |
| ui::MenuModel::ItemType::TYPE_SUBMENU); |
| |
| // Check the submenu model data. |
| const ui::ColorProvider* color_provider = GetPrimaryWindowColorProvider(); |
| std::vector<std::u16string> actual_labels; |
| std::vector<gfx::Image> actual_icons; |
| for (size_t index = 0; index < submenu_model->GetItemCount(); ++index) { |
| actual_labels.emplace_back(submenu_model->GetLabelAt(index)); |
| actual_icons.emplace_back( |
| submenu_model->GetIconAt(index).Rasterize(color_provider)); |
| } |
| EXPECT_THAT(expected_submenu_items, |
| MenuItemsMatch(actual_labels, actual_icons)); |
| } else { |
| // If the refresh feature is disabled, the clipboard history menu item is a |
| // command item. |
| EXPECT_FALSE(submenu_model); |
| EXPECT_EQ(target_command_parent_model->GetTypeAt(target_command_index), |
| ui::MenuModel::ItemType::TYPE_COMMAND); |
| } |
| } |
| |
| } // namespace ash |