| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/exo/seat.h" |
| |
| #include <optional> |
| |
| #include "ash/public/mojom/input_device_settings.mojom.h" |
| #include "base/containers/flat_map.h" |
| #include "base/files/file_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/pickle.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/thread_pool/thread_pool_instance.h" |
| #include "base/test/bind.h" |
| #include "components/exo/data_device.h" |
| #include "components/exo/data_device_delegate.h" |
| #include "components/exo/data_source.h" |
| #include "components/exo/data_source_delegate.h" |
| #include "components/exo/seat_observer.h" |
| #include "components/exo/surface.h" |
| #include "components/exo/test/exo_test_base.h" |
| #include "components/exo/test/exo_test_data_exchange_delegate.h" |
| #include "components/exo/test/test_data_device_delegate.h" |
| #include "components/exo/test/test_data_source_delegate.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/clipboard/clipboard.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/dragdrop/mojom/drag_drop_types.mojom-shared.h" |
| #include "ui/events/event.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/events/types/event_type.h" |
| |
| namespace exo { |
| namespace { |
| |
| using SeatTest = test::ExoTestBase; |
| using test::TestDataSourceDelegate; |
| |
| class TestSeatObserver : public SeatObserver { |
| public: |
| explicit TestSeatObserver(const base::RepeatingClosure& callback) |
| : callback_(callback) {} |
| |
| // Overridden from SeatObserver: |
| void OnSurfaceFocused(Surface* gained_focus, |
| Surface* lost_focus, |
| bool has_focused_surface) override { |
| callback_.Run(); |
| } |
| |
| private: |
| base::RepeatingClosure callback_; |
| }; |
| |
| void RunReadingTask() { |
| base::ThreadPoolInstance::Get()->FlushForTesting(); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| class TestSeat : public Seat { |
| public: |
| TestSeat() : Seat(std::make_unique<TestDataExchangeDelegate>()) {} |
| explicit TestSeat( |
| std::unique_ptr<TestDataExchangeDelegate> data_exchange_delegate) |
| : Seat(std::move(data_exchange_delegate)) {} |
| |
| TestSeat(const TestSeat&) = delete; |
| void operator=(const TestSeat&) = delete; |
| |
| void set_focused_surface(Surface* surface) { surface_ = surface; } |
| |
| // Seat: |
| Surface* GetFocusedSurface() override { return surface_; } |
| |
| private: |
| raw_ptr<Surface> surface_ = nullptr; |
| }; |
| |
| TEST_F(SeatTest, OnSurfaceFocused) { |
| TestSeat seat; |
| int callback_counter = 0; |
| std::optional<int> observer1_counter; |
| TestSeatObserver observer1(base::BindLambdaForTesting( |
| [&]() { observer1_counter = callback_counter++; })); |
| std::optional<int> observer2_counter; |
| TestSeatObserver observer2(base::BindLambdaForTesting( |
| [&]() { observer2_counter = callback_counter++; })); |
| |
| // Register observers in the reversed order. |
| seat.AddObserver(&observer2, 1); |
| seat.AddObserver(&observer1, 0); |
| seat.OnWindowFocused(nullptr, nullptr); |
| EXPECT_EQ(observer1_counter, 0); |
| EXPECT_EQ(observer2_counter, 1); |
| |
| observer1_counter.reset(); |
| observer2_counter.reset(); |
| seat.RemoveObserver(&observer1); |
| seat.RemoveObserver(&observer2); |
| |
| seat.OnWindowFocused(nullptr, nullptr); |
| EXPECT_FALSE(observer1_counter.has_value()); |
| EXPECT_FALSE(observer2_counter.has_value()); |
| } |
| |
| TEST_F(SeatTest, SetSelection) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| source.Offer("text/plain;charset=utf-8"); |
| seat.SetSelection(&source); |
| |
| RunReadingTask(); |
| |
| std::string clipboard; |
| ui::Clipboard::GetForCurrentThread()->ReadAsciiText( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard); |
| |
| EXPECT_EQ(clipboard, std::string("TestData")); |
| } |
| |
| TEST_F(SeatTest, SetSelectionTextUTF8) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| // UTF8 encoded data |
| std::string data( |
| "\xe2\x9d\x84" // SNOWFLAKE |
| "\xf0\x9f\x94\xa5"); // FIRE |
| std::u16string converted_data = base::UTF8ToUTF16(data); |
| |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| |
| const std::string kTextPlainType = "text/plain;charset=utf-8"; |
| const std::string kTextHtmlType = "text/html;charset=utf-8"; |
| source.Offer(kTextPlainType); |
| source.Offer(kTextHtmlType); |
| delegate.SetData(kTextPlainType, data); |
| delegate.SetData(kTextHtmlType, data); |
| seat.SetSelection(&source); |
| |
| RunReadingTask(); |
| |
| std::u16string clipboard; |
| ui::Clipboard::GetForCurrentThread()->ReadText( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard); |
| EXPECT_EQ(clipboard, converted_data); |
| |
| std::string url; |
| uint32_t start, end; |
| ui::Clipboard::GetForCurrentThread()->ReadHTML( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard, &url, |
| &start, &end); |
| EXPECT_EQ(clipboard, converted_data); |
| } |
| |
| TEST_F(SeatTest, SetSelectionTextUTF8Legacy) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| // UTF8 encoded data |
| std::string data( |
| "\xe2\x9d\x84" // SNOWFLAKE |
| "\xf0\x9f\x94\xa5"); // FIRE |
| std::u16string converted_data = base::UTF8ToUTF16(data); |
| |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| const std::string kMimeType = "UTF8_STRING"; |
| source.Offer(kMimeType); |
| delegate.SetData(kMimeType, data); |
| seat.SetSelection(&source); |
| |
| RunReadingTask(); |
| |
| std::u16string clipboard; |
| ui::Clipboard::GetForCurrentThread()->ReadText( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard); |
| EXPECT_EQ(clipboard, converted_data); |
| } |
| |
| TEST_F(SeatTest, SetSelectionTextUTF16LE) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| // UTF16 little endian encoded data |
| std::string data( |
| "\xff\xfe" // Byte order mark |
| "\x44\x27" // SNOWFLAKE |
| "\x3d\xd8\x25\xdd"); // FIRE |
| std::u16string converted_data; |
| converted_data.push_back(0x2744); |
| converted_data.push_back(0xd83d); |
| converted_data.push_back(0xdd25); |
| |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| const std::string kTextPlainType = "text/plain;charset=utf-16"; |
| const std::string kTextHtmlType = "text/html;charset=utf-16"; |
| source.Offer(kTextPlainType); |
| source.Offer(kTextHtmlType); |
| delegate.SetData(kTextPlainType, data); |
| delegate.SetData(kTextHtmlType, data); |
| seat.SetSelection(&source); |
| |
| RunReadingTask(); |
| |
| std::u16string clipboard; |
| ui::Clipboard::GetForCurrentThread()->ReadText( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard); |
| EXPECT_EQ(clipboard, converted_data); |
| |
| std::string url; |
| uint32_t start, end; |
| ui::Clipboard::GetForCurrentThread()->ReadHTML( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard, &url, |
| &start, &end); |
| EXPECT_EQ(clipboard, converted_data); |
| } |
| |
| TEST_F(SeatTest, SetSelectionTextUTF16BE) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| // UTF16 big endian encoded data |
| std::string data( |
| "\xfe\xff" // Byte order mark |
| "\x27\x44" // SNOWFLAKE |
| "\xd8\x3d\xdd\x25"); // FIRE |
| std::u16string converted_data; |
| converted_data.push_back(0x2744); |
| converted_data.push_back(0xd83d); |
| converted_data.push_back(0xdd25); |
| |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| const std::string kTextPlainType = "text/plain;charset=utf-16"; |
| const std::string kTextHtmlType = "text/html;charset=utf-16"; |
| source.Offer(kTextPlainType); |
| source.Offer(kTextHtmlType); |
| delegate.SetData(kTextPlainType, data); |
| delegate.SetData(kTextHtmlType, data); |
| seat.SetSelection(&source); |
| |
| RunReadingTask(); |
| |
| std::u16string clipboard; |
| ui::Clipboard::GetForCurrentThread()->ReadText( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard); |
| EXPECT_EQ(clipboard, converted_data); |
| |
| std::string url; |
| uint32_t start, end; |
| ui::Clipboard::GetForCurrentThread()->ReadHTML( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard, &url, |
| &start, &end); |
| EXPECT_EQ(clipboard, converted_data); |
| } |
| |
| TEST_F(SeatTest, SetSelectionTextEmptyString) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| const std::string kTextPlainType = "text/plain;charset=utf-8"; |
| const std::string kTextHtmlType = "text/html;charset=utf-16"; |
| source.Offer(kTextPlainType); |
| source.Offer(kTextHtmlType); |
| delegate.SetData(kTextPlainType, std::string()); |
| delegate.SetData(kTextHtmlType, std::string()); |
| seat.SetSelection(&source); |
| |
| RunReadingTask(); |
| |
| std::u16string clipboard; |
| ui::Clipboard::GetForCurrentThread()->ReadText( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard); |
| EXPECT_EQ(clipboard.size(), 0u); |
| |
| std::string url; |
| uint32_t start, end; |
| ui::Clipboard::GetForCurrentThread()->ReadHTML( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard, &url, |
| &start, &end); |
| EXPECT_EQ(clipboard.size(), 0u); |
| } |
| |
| TEST_F(SeatTest, SetSelectionRTF) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| source.Offer("text/rtf"); |
| seat.SetSelection(&source); |
| |
| RunReadingTask(); |
| |
| std::string clipboard; |
| ui::Clipboard::GetForCurrentThread()->ReadRTF( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard); |
| |
| EXPECT_EQ(clipboard, std::string("TestData")); |
| } |
| |
| TEST_F(SeatTest, SetSelectionFilenames) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| const std::string data("file:///path1\r\nfile:///path2"); |
| |
| TestDataSourceDelegate delegate; |
| const std::string kMimeType = "text/uri-list"; |
| delegate.SetData(kMimeType, data); |
| DataSource source(&delegate); |
| source.Offer(kMimeType); |
| seat.SetSelection(&source); |
| |
| RunReadingTask(); |
| |
| std::vector<ui::FileInfo> filenames; |
| ui::Clipboard::GetForCurrentThread()->ReadFilenames( |
| ui::ClipboardBuffer::kCopyPaste, |
| /*data_dst=*/nullptr, &filenames); |
| |
| EXPECT_EQ(ui::FileInfosToURIList(filenames), data); |
| } |
| |
| TEST_F(SeatTest, SetSelectionWebCustomData) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| base::flat_map<std::u16string, std::u16string> custom_data; |
| custom_data[u"text/uri-list"] = u"data"; |
| base::Pickle pickle; |
| ui::WriteCustomDataToPickle(custom_data, &pickle); |
| auto custom_data_str = |
| std::string(reinterpret_cast<const char*>(pickle.data()), pickle.size()); |
| |
| TestDataSourceDelegate delegate; |
| const std::string kMimeType = "chromium/x-web-custom-data"; |
| delegate.SetData(kMimeType, std::move(custom_data_str)); |
| DataSource source(&delegate); |
| source.Offer(kMimeType); |
| seat.SetSelection(&source); |
| |
| RunReadingTask(); |
| |
| std::u16string result; |
| ui::Clipboard::GetForCurrentThread()->ReadDataTransferCustomData( |
| ui::ClipboardBuffer::kCopyPaste, u"text/uri-list", /*data_dst=*/nullptr, |
| &result); |
| EXPECT_EQ(result, u"data"); |
| } |
| |
| TEST_F(SeatTest, SetSelection_TwiceSame) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| seat.SetSelection(&source); |
| RunReadingTask(); |
| seat.SetSelection(&source); |
| RunReadingTask(); |
| |
| EXPECT_FALSE(delegate.cancelled()); |
| } |
| |
| TEST_F(SeatTest, SetSelection_TwiceDifferent) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| TestDataSourceDelegate delegate1; |
| DataSource source1(&delegate1); |
| seat.SetSelection(&source1); |
| RunReadingTask(); |
| |
| EXPECT_FALSE(delegate1.cancelled()); |
| |
| TestDataSourceDelegate delegate2; |
| DataSource source2(&delegate2); |
| seat.SetSelection(&source2); |
| RunReadingTask(); |
| |
| EXPECT_TRUE(delegate1.cancelled()); |
| } |
| |
| TEST_F(SeatTest, SetSelection_ClipboardChangedDuringSetSelection) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| seat.SetSelection(&source); |
| |
| { |
| ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste); |
| writer.WriteText(u"New data"); |
| } |
| |
| RunReadingTask(); |
| |
| // The previous source should be cancelled. |
| EXPECT_TRUE(delegate.cancelled()); |
| |
| std::string clipboard; |
| ui::Clipboard::GetForCurrentThread()->ReadAsciiText( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard); |
| EXPECT_EQ(clipboard, "New data"); |
| } |
| |
| TEST_F(SeatTest, SetSelection_ClipboardChangedAfterSetSelection) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| seat.SetSelection(&source); |
| RunReadingTask(); |
| |
| { |
| ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste); |
| writer.WriteText(u"New data"); |
| } |
| |
| // The previous source should be cancelled. |
| EXPECT_TRUE(delegate.cancelled()); |
| |
| std::string clipboard; |
| ui::Clipboard::GetForCurrentThread()->ReadAsciiText( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard); |
| EXPECT_EQ(clipboard, "New data"); |
| } |
| |
| TEST_F(SeatTest, SetSelection_SourceDestroyedDuringSetSelection) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| { |
| ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste); |
| writer.WriteText(u"Original data"); |
| } |
| |
| { |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| seat.SetSelection(&source); |
| // source destroyed here. |
| } |
| |
| RunReadingTask(); |
| |
| std::string clipboard; |
| ui::Clipboard::GetForCurrentThread()->ReadAsciiText( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard); |
| EXPECT_EQ(clipboard, "Original data"); |
| } |
| |
| TEST_F(SeatTest, SetSelection_SourceDestroyedAfterSetSelection) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| TestDataSourceDelegate delegate1; |
| { |
| DataSource source(&delegate1); |
| seat.SetSelection(&source); |
| RunReadingTask(); |
| // source destroyed here. |
| } |
| |
| RunReadingTask(); |
| |
| { |
| TestDataSourceDelegate delegate2; |
| DataSource source(&delegate2); |
| seat.SetSelection(&source); |
| RunReadingTask(); |
| // source destroyed here. |
| } |
| |
| RunReadingTask(); |
| |
| // delegate1 should not receive cancel request because the first data source |
| // has already been destroyed. |
| EXPECT_FALSE(delegate1.cancelled()); |
| } |
| |
| TEST_F(SeatTest, SetSelection_NullSource) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| source.Offer("text/plain;charset=utf-8"); |
| seat.SetSelection(&source); |
| |
| RunReadingTask(); |
| |
| { |
| ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste); |
| writer.WriteText(u"Golden data"); |
| } |
| |
| // Should not affect the current state of the clipboard. |
| seat.SetSelection(nullptr); |
| |
| ASSERT_TRUE(delegate.cancelled()); |
| |
| std::string clipboard; |
| ui::Clipboard::GetForCurrentThread()->ReadAsciiText( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard); |
| EXPECT_EQ(clipboard, "Golden data"); |
| } |
| |
| TEST_F(SeatTest, SetSelection_NoFocusedSurface) { |
| TestSeat seat; |
| seat.set_focused_surface(nullptr); |
| |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| source.Offer("text/plain;charset=utf-8"); |
| seat.SetSelection(&source); |
| |
| EXPECT_TRUE(delegate.cancelled()); |
| } |
| |
| TEST_F(SeatTest, SetSelection_ClientOutOfFocus) { |
| TestSeat seat; |
| Surface focused_surface; |
| seat.set_focused_surface(&focused_surface); |
| |
| TestDataSourceDelegate delegate; |
| delegate.set_can_accept(false); |
| DataSource source(&delegate); |
| source.Offer("text/plain;charset=utf-8"); |
| seat.SetSelection(&source); |
| |
| EXPECT_TRUE(delegate.cancelled()); |
| } |
| |
| TEST_F(SeatTest, PressedKeys) { |
| TestSeat seat; |
| ui::KeyEvent press_a(ui::EventType::kKeyPressed, ui::VKEY_A, |
| ui::DomCode::US_A, 0); |
| ui::KeyEvent release_a(ui::EventType::kKeyReleased, ui::VKEY_A, |
| ui::DomCode::US_A, 0); |
| ui::KeyEvent press_b(ui::EventType::kKeyPressed, ui::VKEY_B, |
| ui::DomCode::US_B, 0); |
| ui::KeyEvent release_b(ui::EventType::kKeyReleased, ui::VKEY_B, |
| ui::DomCode::US_B, 0); |
| |
| // Press A, it should be in the map. |
| seat.WillProcessEvent(&press_a); |
| seat.OnKeyEvent(press_a.AsKeyEvent()); |
| seat.DidProcessEvent(&press_a); |
| base::flat_map<PhysicalCode, base::flat_set<KeyState>> pressed_keys; |
| pressed_keys[ui::CodeFromNative(&press_a)].emplace(press_a.code(), false); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| |
| // Press B, then A & B should be in the map. |
| seat.WillProcessEvent(&press_b); |
| seat.OnKeyEvent(press_b.AsKeyEvent()); |
| seat.DidProcessEvent(&press_b); |
| pressed_keys[ui::CodeFromNative(&press_b)].emplace(press_b.code(), false); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| |
| // Release A, with the normal order where DidProcessEvent is after OnKeyEvent, |
| // only B should be in the map. |
| seat.WillProcessEvent(&release_a); |
| seat.OnKeyEvent(release_a.AsKeyEvent()); |
| seat.DidProcessEvent(&release_a); |
| pressed_keys.erase(PhysicalCode(ui::CodeFromNative(&press_a))); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| |
| // Release B, do it out of order so DidProcessEvent is before OnKeyEvent, the |
| // map should then be empty. |
| seat.WillProcessEvent(&release_b); |
| seat.DidProcessEvent(&release_b); |
| seat.OnKeyEvent(release_b.AsKeyEvent()); |
| EXPECT_TRUE(seat.pressed_keys().empty()); |
| } |
| |
| TEST_F(SeatTest, DragDropAbort) { |
| TestSeat seat; |
| test::TestDataDeviceDelegate data_device_delegate; |
| |
| DataDevice data_device(&data_device_delegate, &seat); |
| TestDataSourceDelegate delegate; |
| DataSource source(&delegate); |
| Surface origin, icon; |
| |
| // Give origin a root window for DragDropOperation. |
| GetContext()->AddChild(origin.window()); |
| |
| data_device.StartDrag(&source, &origin, &icon, |
| ui::mojom::DragEventSource::kMouse); |
| EXPECT_TRUE(seat.get_drag_drop_operation_for_testing()); |
| seat.AbortPendingDragOperation(); |
| EXPECT_FALSE(seat.get_drag_drop_operation_for_testing()); |
| } |
| |
| TEST_F(SeatTest, MultiRewriteEventsFromInvalidSource) { |
| TestSeat seat; |
| |
| ui::KeyEvent press_a(ui::EventType::kKeyPressed, ui::VKEY_A, |
| ui::DomCode::US_A, 0); |
| ui::KeyEvent release_a(ui::EventType::kKeyReleased, ui::VKEY_A, |
| ui::DomCode::US_A, 0); |
| ui::KeyEvent press_b(ui::EventType::kKeyPressed, ui::VKEY_B, |
| ui::DomCode::US_B, 0); |
| ui::KeyEvent release_b(ui::EventType::kKeyReleased, ui::VKEY_B, |
| ui::DomCode::US_B, 0); |
| |
| // Press A, it should be in the map. |
| seat.WillProcessEvent(&press_a); |
| seat.OnKeyEvent(press_a.AsKeyEvent()); |
| base::flat_map<PhysicalCode, base::flat_set<KeyState>> pressed_keys; |
| pressed_keys[ui::CodeFromNative(&press_a)].emplace(press_a.code(), false); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| |
| // Press A, but it was remapped to B. Should not be added to pressed_keys map. |
| seat.OnKeyEvent(press_b.AsKeyEvent()); |
| seat.DidProcessEvent(&press_a); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| |
| // Release B -> A from the same physical "A" event. Entry should be removed |
| // after first event. |
| seat.WillProcessEvent(&release_a); |
| seat.OnKeyEvent(release_b.AsKeyEvent()); |
| pressed_keys.erase(PhysicalCode(ui::CodeFromNative(&press_a))); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| |
| seat.OnKeyEvent(release_a.AsKeyEvent()); |
| seat.DidProcessEvent(&release_a); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| } |
| |
| TEST_F(SeatTest, MultiRewriteEventsFromValidSource) { |
| TestSeat seat; |
| |
| ui::KeyEvent press_a(ui::EventType::kKeyPressed, ui::VKEY_A, |
| ui::DomCode::US_A, ui::EF_IS_CUSTOMIZED_FROM_BUTTON); |
| ui::KeyEvent release_a(ui::EventType::kKeyReleased, ui::VKEY_A, |
| ui::DomCode::US_A, ui::EF_IS_CUSTOMIZED_FROM_BUTTON); |
| ui::KeyEvent press_b(ui::EventType::kKeyPressed, ui::VKEY_B, |
| ui::DomCode::US_B, ui::EF_IS_CUSTOMIZED_FROM_BUTTON); |
| ui::KeyEvent release_b(ui::EventType::kKeyReleased, ui::VKEY_B, |
| ui::DomCode::US_B, ui::EF_IS_CUSTOMIZED_FROM_BUTTON); |
| |
| // Press A, it should be in the map. |
| seat.WillProcessEvent(&press_a); |
| seat.OnKeyEvent(press_a.AsKeyEvent()); |
| base::flat_map<PhysicalCode, base::flat_set<KeyState>> pressed_keys; |
| auto& key_state_set = pressed_keys[ui::CodeFromNative(&press_a)]; |
| key_state_set.emplace(press_a.code(), false); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| |
| // Press A, but it was remapped to B. Should be added to pressed_keys map |
| // since it is explicitly allowlisted. |
| seat.OnKeyEvent(press_b.AsKeyEvent()); |
| seat.DidProcessEvent(&press_a); |
| key_state_set.emplace(press_b.code(), false); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| |
| // Release B -> A from the same physical "A" event. Entry should be removed |
| // after first event. |
| seat.WillProcessEvent(&release_a); |
| seat.OnKeyEvent(release_b.AsKeyEvent()); |
| pressed_keys.erase(PhysicalCode(ui::CodeFromNative(&press_a))); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| |
| seat.OnKeyEvent(release_a.AsKeyEvent()); |
| seat.DidProcessEvent(&release_a); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| } |
| |
| TEST_F(SeatTest, MouseMultiRewriteEventsFromValidSource) { |
| TestSeat seat; |
| |
| ui::MouseEvent press_back(ui::EventType::kMousePressed, gfx::PointF{}, |
| gfx::PointF{}, base::TimeTicks(), |
| ui::EF_BACK_MOUSE_BUTTON, ui::EF_BACK_MOUSE_BUTTON); |
| ui::MouseEvent release_back( |
| ui::EventType::kMouseReleased, gfx::PointF{}, gfx::PointF{}, |
| base::TimeTicks(), ui::EF_BACK_MOUSE_BUTTON, ui::EF_BACK_MOUSE_BUTTON); |
| |
| ui::KeyEvent press_a(ui::EventType::kKeyPressed, ui::VKEY_A, |
| ui::DomCode::US_A, ui::EF_IS_CUSTOMIZED_FROM_BUTTON); |
| ui::KeyEvent release_a(ui::EventType::kKeyReleased, ui::VKEY_A, |
| ui::DomCode::US_A, ui::EF_IS_CUSTOMIZED_FROM_BUTTON); |
| ui::KeyEvent press_b(ui::EventType::kKeyPressed, ui::VKEY_B, |
| ui::DomCode::US_B, ui::EF_IS_CUSTOMIZED_FROM_BUTTON); |
| ui::KeyEvent release_b(ui::EventType::kKeyReleased, ui::VKEY_B, |
| ui::DomCode::US_B, ui::EF_IS_CUSTOMIZED_FROM_BUTTON); |
| |
| // Press Back remapped to "A", it should be in the map. |
| seat.WillProcessEvent(&press_back); |
| seat.OnKeyEvent(press_a.AsKeyEvent()); |
| base::flat_map<PhysicalCode, base::flat_set<KeyState>> pressed_keys; |
| auto& key_state_set = pressed_keys[ash::mojom::CustomizableButton::kBack]; |
| key_state_set.emplace(press_a.code(), false); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| |
| // Press B remapped within the same Back mouse button. Should also be added to |
| // the map. |
| seat.OnKeyEvent(press_b.AsKeyEvent()); |
| seat.DidProcessEvent(&press_back); |
| key_state_set.emplace(press_b.code(), false); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| |
| // Release B then A from the same physical mouse button release. Both should |
| // be instantly removed from the map. |
| seat.WillProcessEvent(&press_back); |
| seat.OnKeyEvent(release_b.AsKeyEvent()); |
| pressed_keys.erase(PhysicalCode(ash::mojom::CustomizableButton::kBack)); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| |
| seat.OnKeyEvent(release_a.AsKeyEvent()); |
| seat.DidProcessEvent(&press_back); |
| EXPECT_EQ(pressed_keys, seat.pressed_keys()); |
| } |
| |
| } // namespace |
| } // namespace exo |