chromium/chrome/browser/ui/views/device_id/pen_id_browsertest_win.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdint.h>

#include <memory>
#include <string>
#include <vector>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_forward.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/win/core_winrt_util.h"
#include "base/win/hstring_reference.h"
#include "base/win/scoped_hstring.h"
#include "base/win/windows_version.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/views/test/desktop_window_tree_host_win_test_api.h"
#include "ui/views/widget/desktop_aura/desktop_window_tree_host_win.h"
#include "ui/views/win/pen_event_processor.h"
#include "ui/views/win/pen_id_handler.h"
#include "ui/views/win/test_support/fake_ipen_device.h"
#include "ui/views/win/test_support/fake_ipen_device_statics.h"

using views::FakeIPenDevice;
using views::FakeIPenDeviceStatics;

using Microsoft::WRL::ComPtr;

namespace {

constexpr char kMainTestPageUrlPath[] = "/device_id/test.html";

constexpr int kPointerId1 = 0;
constexpr int kPointerId2 = 1;
constexpr int kPointerId3 = 2;
constexpr int kDeviceId1 = 0;
constexpr int kDeviceId2 = 1;

}  // namespace

class PenIdBrowserTest : public InProcessBrowserTest {
 public:
  PenIdBrowserTest() = default;
  ~PenIdBrowserTest() override = default;

  // Implement InProcessBrowserTest.
  void SetUpOnMainThread() override;
  void TearDown() override;

  // Helpers to execute the tests.
  content::WebContents* GetDefaultWebContents() const;
  void SimulatePenPointerDragEvent(int pointer_id);
  void SimulatePenPointerEventAndStop(
      int pointer_id,
      base::OnceCallback<void(int)> simulate_event_function);
  bool MouseEventCallback(int expected_value,
                          const base::RepeatingClosure& quit_closure,
                          const blink::WebMouseEvent& evt);

 private:
  std::unique_ptr<net::EmbeddedTestServer> https_server_;
  content::RenderWidgetHost::MouseEventCallback mouse_event_callback_;
};

void PenIdBrowserTest::SetUpOnMainThread() {
  if (base::win::OSInfo::Kernel32Version() < base::win::Version::WIN10_21H2 ||
      (base::win::OSInfo::Kernel32Version() == base::win::Version::WIN10_21H2 &&
       base::win::OSInfo::GetInstance()->version_number().patch < 1503)) {
    GTEST_SKIP() << "Pen Device Api not supported on this machine";
  }
  https_server_.reset(
      new net::EmbeddedTestServer(net::EmbeddedTestServer::TYPE_HTTPS));
  https_server_->ServeFilesFromSourceDirectory("chrome/test/data");
  ASSERT_TRUE(https_server_->Start());

  EXPECT_TRUE(ui_test_utils::NavigateToURL(
      browser(), https_server_->GetURL(kMainTestPageUrlPath)));
}

void PenIdBrowserTest::TearDown() {
  FakeIPenDeviceStatics::GetInstance()->SimulateAllPenDevicesRemoved();
}

content::WebContents* PenIdBrowserTest::GetDefaultWebContents() const {
  return browser()->tab_strip_model()->GetActiveWebContents();
}

void PenIdBrowserTest::SimulatePenPointerEventAndStop(
    int pointer_id,
    base::OnceCallback<void(int)> simulate_event_function) {
  base::RunLoop run_loop;
  GetDefaultWebContents()
      ->GetPrimaryMainFrame()
      ->GetRenderWidgetHost()
      ->RemoveMouseEventCallback(mouse_event_callback_);
  mouse_event_callback_ = base::BindRepeating(
      &PenIdBrowserTest::MouseEventCallback, base::Unretained(this), pointer_id,
      run_loop.QuitClosure());
  GetDefaultWebContents()
      ->GetPrimaryMainFrame()
      ->GetRenderWidgetHost()
      ->AddMouseEventCallback(mouse_event_callback_);
  std::move(simulate_event_function).Run(pointer_id);
  run_loop.Run();
}

void PenIdBrowserTest::SimulatePenPointerDragEvent(int pointer_id) {
  content::WebContents* web_contents = GetDefaultWebContents();

  gfx::Rect container_bounds = web_contents->GetContainerBounds();
  long x = container_bounds.width() / 2;
  long y = container_bounds.height() / 2;
  long offset_x = container_bounds.x();
  long offset_y = container_bounds.y();

  POINTER_PEN_INFO pen_info;
  memset(&pen_info, 0, sizeof(POINTER_PEN_INFO));
  pen_info.pointerInfo.pointerType = PT_PEN;
  pen_info.pointerInfo.ButtonChangeType = POINTER_CHANGE_FIRSTBUTTON_DOWN;
  // Since, SimulatePenEventForTesting considers the coordinates in relation
  // to the screen, to centralize the click in the middle of the page, it is
  // necessary to translate the pointer by the page container offset.
  pen_info.pointerInfo.ptPixelLocationRaw.x = offset_x + x;
  pen_info.pointerInfo.ptPixelLocationRaw.y = offset_y + y;

  views::test::DesktopWindowTreeHostWinTestApi desktop_window_tree_host(
      static_cast<views::DesktopWindowTreeHostWin*>(
          web_contents->GetNativeView()->GetHost()));

  desktop_window_tree_host.SimulatePenEventForTesting(WM_POINTERDOWN,
                                                      pointer_id, pen_info);

  // Drag the pointer to the first point.
  x = 9 * container_bounds.width() / 20;
  pen_info.pointerInfo.ptPixelLocationRaw.x = offset_x + x;
  desktop_window_tree_host.SimulatePenEventForTesting(WM_POINTERUPDATE,
                                                      pointer_id, pen_info);

  // Drag the pointer to the second point.
  x = 8 * container_bounds.width() / 20;
  pen_info.pointerInfo.ptPixelLocationRaw.x = offset_x + x;
  desktop_window_tree_host.SimulatePenEventForTesting(WM_POINTERUPDATE,
                                                      pointer_id, pen_info);

  // Lift the pointer device.
  pen_info.pointerInfo.ButtonChangeType = POINTER_CHANGE_FIRSTBUTTON_UP;
  desktop_window_tree_host.SimulatePenEventForTesting(WM_POINTERUP, pointer_id,
                                                      pen_info);
}

bool PenIdBrowserTest::MouseEventCallback(
    int expected_value,
    const base::RepeatingClosure& quit_closure,
    const blink::WebMouseEvent& evt) {
  const int32_t id = evt.device_id;
  if (evt.pointer_type == ui::EventPointerType::kPen) {
    EXPECT_EQ(id, expected_value);
  }
  quit_closure.Run();
  return false;
}

// Perform a pen drag for a pen that has a guid. Verify the correct
// device id is propagated in the pointer event.
// The test works by simulating a pen event in the PenDeviceStatics which
// records a fake pen event for a given pointer id. Then, a pointer event is
// simulated by injecting a pen event via
// desktop_window_tree_host.SimulatePenEventForTesting. We bind a callback
// (MouseEventCallback) so that when the browser sends the pen event, it
// checks for the right device id.
IN_PROC_BROWSER_TEST_F(PenIdBrowserTest, PenDeviceTest) {
  views::PenIdHandler::ScopedPenIdStaticsForTesting scoper(
      &FakeIPenDeviceStatics::FakeIPenDeviceStaticsComPtr);
  const auto fake_pen_device = Microsoft::WRL::Make<FakeIPenDevice>();
  FakeIPenDeviceStatics::GetInstance()->SimulatePenEventGenerated(
      kPointerId1, fake_pen_device);

  const auto fake_pen_device_2 = Microsoft::WRL::Make<FakeIPenDevice>();
  FakeIPenDeviceStatics::GetInstance()->SimulatePenEventGenerated(
      kPointerId2, fake_pen_device_2);

  FakeIPenDeviceStatics::GetInstance()->SimulatePenEventGenerated(
      kPointerId3, fake_pen_device);

  // We take advantage of the current implementation of device id to simplify
  // the test. Device Id starts at 0 and iterates, so we expect the first
  // device id to be 0 and the next one to be 1.
  SimulatePenPointerEventAndStop(
      kDeviceId1, base::BindOnce(&PenIdBrowserTest::SimulatePenPointerDragEvent,
                                 base::Unretained(this)));
  SimulatePenPointerEventAndStop(
      kDeviceId2, base::BindOnce(&PenIdBrowserTest::SimulatePenPointerDragEvent,
                                 base::Unretained(this)));
  SimulatePenPointerEventAndStop(
      kDeviceId1, base::BindOnce(&PenIdBrowserTest::SimulatePenPointerDragEvent,
                                 base::Unretained(this)));
}