chromium/components/exo/seat_unittest.cc

// 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, SetSelectionReadDteFromLacros) {
  std::unique_ptr<TestDataExchangeDelegate> data_exchange_delegate(
      std::make_unique<TestDataExchangeDelegate>());
  data_exchange_delegate->set_endpoint_type(ui::EndpointType::kLacros);
  TestSeat seat(std::move(data_exchange_delegate));
  Surface focused_surface;
  seat.set_focused_surface(&focused_surface);

  const std::string kTestText = "TestData";
  const std::string kEncodedTestDte =
      R"({"endpoint_type":"url","url":"https://www.google.com"})";

  const std::string kTextMimeType = "text/plain;charset=utf-8";
  const std::string kDteMimeType = "chromium/x-data-transfer-endpoint";

  TestDataSourceDelegate delegate;
  DataSource source(&delegate);

  source.Offer(kTextMimeType);
  delegate.SetData(kTextMimeType, kTestText);
  source.Offer(kDteMimeType);
  delegate.SetData(kDteMimeType, kEncodedTestDte);
  seat.SetSelection(&source);

  RunReadingTask();

  std::string clipboard;
  ui::Clipboard::GetForCurrentThread()->ReadAsciiText(
      ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard);

  EXPECT_EQ(clipboard, kTestText);

  std::optional<ui::DataTransferEndpoint> source_dte =
      ui::Clipboard::GetForCurrentThread()->GetSource(
          ui::ClipboardBuffer::kCopyPaste);

  ASSERT_TRUE(source_dte);
  EXPECT_EQ(ui::EndpointType::kUrl, source_dte->type());

  const ui::DataTransferEndpoint expected_dte =
      ui::DataTransferEndpoint((GURL("https://www.google.com")));
  EXPECT_EQ(*expected_dte.GetURL(), *source_dte->GetURL());
}

TEST_F(SeatTest, SetSelectionIgnoreDteFromNonLacros) {
  std::unique_ptr<TestDataExchangeDelegate> data_exchange_delegate(
      std::make_unique<TestDataExchangeDelegate>());
  data_exchange_delegate->set_endpoint_type(ui::EndpointType::kCrostini);
  TestSeat seat(std::move(data_exchange_delegate));
  Surface focused_surface;
  seat.set_focused_surface(&focused_surface);

  const std::string kTestText = "TestData";
  const std::string kEncodedTestDte =
      R"({"endpoint_type":"url","url":"https://www.google.com"})";

  const std::string kTextMimeType = "text/plain;charset=utf-8";
  const std::string kDteMimeType = "chromium/x-data-transfer-endpoint";

  TestDataSourceDelegate delegate;
  DataSource source(&delegate);

  source.Offer(kTextMimeType);
  delegate.SetData(kTextMimeType, kTestText);
  source.Offer(kDteMimeType);
  delegate.SetData(kDteMimeType, kEncodedTestDte);
  seat.SetSelection(&source);

  RunReadingTask();

  std::string clipboard;
  ui::Clipboard::GetForCurrentThread()->ReadAsciiText(
      ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &clipboard);

  EXPECT_EQ(clipboard, kTestText);

  std::optional<ui::DataTransferEndpoint> source_dte =
      ui::Clipboard::GetForCurrentThread()->GetSource(
          ui::ClipboardBuffer::kCopyPaste);

  ASSERT_TRUE(source_dte);
  EXPECT_EQ(ui::EndpointType::kCrostini, source_dte->type());
}

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