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

#include <algorithm>
#include <memory>
#include <vector>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/input/input_router_impl.h"
#include "components/input/render_widget_host_input_event_router.h"
#include "components/input/touch_action_filter.h"
#include "content/browser/permissions/permission_controller_impl.h"
#include "content/browser/renderer_host/input/touch_emulator_impl.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_base.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/common/input/synthetic_smooth_drag_gesture.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/render_widget_host_observer.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "ipc/constants.mojom.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/input/synthetic_web_input_event_builders.h"
#include "third_party/blink/public/common/input/web_input_event.h"
#include "third_party/blink/public/common/input/web_mouse_event.h"
#include "third_party/blink/public/common/switches.h"
#include "ui/events/base_event_utils.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/switches.h"
#include "ui/latency/latency_info.h"

#if BUILDFLAG(IS_MAC)
#include "third_party/blink/public/mojom/choosers/popup_menu.mojom.h"
#include "third_party/blink/public/mojom/frame/frame.mojom-test-utils.h"
#endif

namespace content {

namespace {

// Test observer which waits for a visual properties update from a
// `RenderWidgetHost`.
class TestRenderWidgetHostObserver : public RenderWidgetHostObserver {
 public:
  explicit TestRenderWidgetHostObserver(RenderWidgetHost* widget_host)
      : widget_host_(widget_host) {
    widget_host_->AddObserver(this);
  }

  ~TestRenderWidgetHostObserver() override {
    widget_host_->RemoveObserver(this);
  }

  // RenderWidgetHostObserver:
  void RenderWidgetHostDidUpdateVisualProperties(
      RenderWidgetHost* widget_host) override {
    run_loop_.Quit();
  }

  void Wait() { run_loop_.Run(); }

 private:
  raw_ptr<RenderWidgetHost> widget_host_ = nullptr;
  base::RunLoop run_loop_;
};

}  // namespace

// For tests that just need a browser opened/navigated to a simple web page.
class RenderWidgetHostBrowserTest : public ContentBrowserTest {
 public:
  void SetUpOnMainThread() override {
    ContentBrowserTest::SetUpOnMainThread();
    EXPECT_TRUE(NavigateToURL(
        shell(), GURL("data:text/html,<!doctype html>"
                      "<body style='background-color: magenta;'></body>")));
    SimulateEndOfPaintHoldingOnPrimaryMainFrame(shell()->web_contents());
  }

  WebContents* web_contents() const { return shell()->web_contents(); }
  RenderWidgetHostViewBase* view() const {
    return static_cast<RenderWidgetHostViewBase*>(
        web_contents()->GetRenderWidgetHostView());
  }
  RenderWidgetHostImpl* host() const {
    return static_cast<RenderWidgetHostImpl*>(view()->GetRenderWidgetHost());
  }

  void WaitForVisualPropertiesAck() {
    while (host()->visual_properties_ack_pending_for_testing()) {
      TestRenderWidgetHostObserver(host()).Wait();
    }
  }
};

// This test enables --site-per-process flag.
class RenderWidgetHostSitePerProcessTest : public ContentBrowserTest {
 public:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    IsolateAllSitesForTesting(command_line);
    // Slow bots are flaky due to slower loading interacting with
    // deferred commits.
    command_line->AppendSwitch(blink::switches::kAllowPreCommitInput);
  }

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    SetupCrossSiteRedirector(embedded_test_server());
    ASSERT_TRUE(embedded_test_server()->Start());
  }

 protected:
  WebContentsImpl* web_contents() const {
    return static_cast<WebContentsImpl*>(shell()->web_contents());
  }

  input::TouchActionFilter* GetTouchActionFilterForWidget(
      RenderWidgetHostImpl* rwhi) {
    return &static_cast<input::InputRouterImpl*>(rwhi->input_router())
                ->touch_action_filter_;
  }
};

class TestInputEventObserver : public RenderWidgetHost::InputEventObserver {
 public:
  using EventTypeVector = std::vector<blink::WebInputEvent::Type>;

  ~TestInputEventObserver() override {}

  void OnInputEvent(const RenderWidgetHost& widget,
                    const blink::WebInputEvent& event,
                    InputEventSource source) override {
    dispatched_events_.push_back(event.GetType());
  }

  void OnInputEventAck(const RenderWidgetHost& widget,
                       blink::mojom::InputEventResultSource source,
                       blink::mojom::InputEventResultState state,
                       const blink::WebInputEvent& event) override {
    if (blink::WebInputEvent::IsTouchEventType(event.GetType())) {
      acked_touch_event_type_ = event.GetType();
    }
  }

  EventTypeVector GetAndResetDispatchedEventTypes() {
    EventTypeVector new_event_types;
    std::swap(new_event_types, dispatched_events_);
    return new_event_types;
  }

  blink::WebInputEvent::Type acked_touch_event_type() const {
    return acked_touch_event_type_;
  }

 private:
  EventTypeVector dispatched_events_;
  blink::WebInputEvent::Type acked_touch_event_type_ =
      blink::WebInputEvent::Type::kUndefined;
};

class RenderWidgetHostTouchEmulatorBrowserTest : public ContentBrowserTest {
 public:
  RenderWidgetHostTouchEmulatorBrowserTest()
      : last_simulated_event_time_(ui::EventTimeForNow()),
        simulated_event_time_delta_(base::Milliseconds(100)) {}

  void SetUpOnMainThread() override {
    ContentBrowserTest::SetUpOnMainThread();

    EXPECT_TRUE(NavigateToURL(
        shell(), GURL("data:text/html,<!doctype html>"
                      "<body style='background-color: red;'></body>")));
    SimulateEndOfPaintHoldingOnPrimaryMainFrame(shell()->web_contents());
  }

  base::TimeTicks GetNextSimulatedEventTime() {
    return last_simulated_event_time_ + simulated_event_time_delta_;
  }

  void SimulateRoutedMouseEvent(blink::WebInputEvent::Type type,
                                int x,
                                int y,
                                int modifiers,
                                bool pressed) {
    blink::WebMouseEvent event =
        blink::SyntheticWebMouseEventBuilder::Build(type, x, y, modifiers);
    if (pressed) {
      event.button = blink::WebMouseEvent::Button::kLeft;
    }
    base::TimeTicks event_time = GetNextSimulatedEventTime();
    event.SetTimeStamp(event_time);
    last_simulated_event_time_ = event_time;
    input::RenderWidgetHostInputEventRouter* router =
        static_cast<WebContentsImpl*>(shell()->web_contents())
            ->GetInputEventRouter();
    ASSERT_TRUE(router);
    router->RouteMouseEvent(view(), &event, ui::LatencyInfo());
  }

  void SimulateRoutedMouseDrag(TestInputEventObserver& observer, int x, int y) {
    // Override the reference timestamp for testing to avoid flakiness which
    // results in GSU and the corresponding touch scroll started event not being
    // sent.
    auto timestamp_override =
        ui::TouchDispositionGestureFilter::OverrideReferenceTimestampForTesting(
            GetNextSimulatedEventTime());

    SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, x, y, 0,
                             true);
    auto dispatched_events = observer.GetAndResetDispatchedEventTypes();
    ASSERT_THAT(
        dispatched_events,
        testing::ElementsAre(blink::WebInputEvent::Type::kTouchMove,
                             blink::WebInputEvent::Type::kGestureTapCancel,
                             blink::WebInputEvent::Type::kGestureScrollBegin,
                             blink::WebInputEvent::Type::kTouchScrollStarted,
                             blink::WebInputEvent::Type::kGestureScrollUpdate));
    EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove,
              observer.acked_touch_event_type());
    EXPECT_EQ(0u, observer.GetAndResetDispatchedEventTypes().size());
  }

  void WaitForAckWith(blink::WebInputEvent::Type type) {
    InputMsgWatcher watcher(host(), type);
    watcher.GetAckStateWaitIfNecessary();
  }

  RenderWidgetHostImpl* host() {
    return static_cast<RenderWidgetHostImpl*>(view()->GetRenderWidgetHost());
  }

  RenderWidgetHostViewBase* view() {
    return static_cast<RenderWidgetHostViewBase*>(
        shell()->web_contents()->GetRenderWidgetHostView());
  }

 private:
  base::TimeTicks last_simulated_event_time_;
  const base::TimeDelta simulated_event_time_delta_;
};

// Synthetic mouse events not allowed on Android.
#if !BUILDFLAG(IS_ANDROID)
// This test makes sure that TouchEmulator doesn't emit a GestureScrollEnd
// without a valid unique_touch_event_id when it sees a GestureFlingStart
// terminating the underlying mouse scroll sequence. If the GestureScrollEnd is
// given a unique_touch_event_id of 0, then a crash will occur.
// TODO(crbug.com/404887525): Test randomly times out due to not receiving the
// InputEventAckWaiter kGestureScrollEnd event.
IN_PROC_BROWSER_TEST_F(RenderWidgetHostTouchEmulatorBrowserTest,
                       DISABLED_TouchEmulatorPinchWithGestureFling) {
  auto* touch_emulator = host()->GetTouchEmulator(/*create_if_necessary=*/true);
  touch_emulator->Enable(input::TouchEmulator::Mode::kEmulatingTouchFromMouse,
                         ui::GestureProviderConfigType::GENERIC_MOBILE);
  touch_emulator->SetPinchGestureModeForTesting(true);

  TestInputEventObserver observer;
  host()->AddInputEventObserver(&observer);

  SyntheticSmoothDragGestureParams params;
  params.start_point = gfx::PointF(10.f, 110.f);
  params.gesture_source_type = content::mojom::GestureSourceType::kMouseInput;
  params.distances.push_back(gfx::Vector2d(0, -10));
  params.distances.push_back(gfx::Vector2d(0, -10));
  params.distances.push_back(gfx::Vector2d(0, -10));
  params.distances.push_back(gfx::Vector2d(0, -10));
  params.speed_in_pixels_s = 1200;

  // On slow bots (e.g. ChromeOS DBG) the synthetic gesture sequence events may
  // be delivered slowly/erratically-timed so that the velocity_tracker in the
  // TouchEmulator's GestureDetector may either (i) drop some scroll updates
  // from the velocity estimate, or (ii) create an unexpectedly low velocity
  // estimate. In either case, the minimum fling start velocity may not be
  // achieved, meaning the condition we're trying to test never occurs. To
  // avoid that, we'll keep trying until it happens. The failure mode for the
  // test is that it times out.
  do {
    std::unique_ptr<SyntheticSmoothDragGesture> gesture(
        new SyntheticSmoothDragGesture(params));

    InputEventAckWaiter scroll_end_ack_waiter(
        host(), blink::WebInputEvent::Type::kGestureScrollEnd);
    base::RunLoop run_loop;
    host()->QueueSyntheticGesture(
        std::move(gesture),
        base::BindOnce(
            base::BindLambdaForTesting([&](SyntheticGesture::Result result) {
              EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result);
              run_loop.Quit();
            })));
    run_loop.Run();
    scroll_end_ack_waiter.Wait();

    // Verify that a GestureFlingStart was suppressed by the TouchEmulator, and
    // that we generated a GestureScrollEnd and routed it without crashing.
    TestInputEventObserver::EventTypeVector dispatched_events =
        observer.GetAndResetDispatchedEventTypes();
    EXPECT_TRUE(std::ranges::contains(
        dispatched_events, blink::WebInputEvent::Type::kGestureScrollEnd));
  } while (!touch_emulator->suppress_next_fling_cancel_for_testing());
}
#endif  // !BUILDFLAG(IS_ANDROID)

// Todo(crbug.com/994353): The test is flaky(crash/timeout) on MSAN, TSAN, and
// DEBUG builds.
#if (!defined(NDEBUG) || defined(THREAD_SANITIZER) || defined(MEMORY_SANITIZER))
#define MAYBE_TouchEmulator DISABLED_TouchEmulator
#else
#define MAYBE_TouchEmulator TouchEmulator
#endif
IN_PROC_BROWSER_TEST_F(RenderWidgetHostTouchEmulatorBrowserTest,
                       MAYBE_TouchEmulator) {
  host()
      ->GetTouchEmulator(/*create_if_necessary=*/true)
      ->Enable(input::TouchEmulator::Mode::kEmulatingTouchFromMouse,
               ui::GestureProviderConfigType::GENERIC_MOBILE);

  TestInputEventObserver observer;
  host()->AddInputEventObserver(&observer);

  // Simulate a mouse move without any pressed buttons. This should not
  // generate any touch events.
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 120, 0,
                           false);
  TestInputEventObserver::EventTypeVector dispatched_events =
      observer.GetAndResetDispatchedEventTypes();
  EXPECT_EQ(0u, dispatched_events.size());

  // Mouse press becomes touch start which in turn becomes tap.
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseDown, 10, 120, 0,
                           true);
  WaitForAckWith(blink::WebInputEvent::Type::kTouchStart);
  EXPECT_EQ(blink::WebInputEvent::Type::kTouchStart,
            observer.acked_touch_event_type());
  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  ASSERT_THAT(
      dispatched_events,
      testing::ElementsAre(blink::WebInputEvent::Type::kTouchStart,
                           blink::WebInputEvent::Type::kGestureTapDown));

  // Mouse drag generates touch move, cancels tap and starts scroll.
  SimulateRoutedMouseDrag(observer, 10, 100);

  // Mouse drag with shift becomes pinch.
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 95,
                           blink::WebInputEvent::kShiftKey, true);
  EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove,
            observer.acked_touch_event_type());

  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  ASSERT_THAT(
      dispatched_events,
      testing::ElementsAre(blink::WebInputEvent::Type::kTouchMove,
                           blink::WebInputEvent::Type::kGesturePinchBegin));

  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 80,
                           blink::WebInputEvent::kShiftKey, true);
  EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove,
            observer.acked_touch_event_type());

  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  ASSERT_THAT(
      dispatched_events,
      testing::ElementsAre(blink::WebInputEvent::Type::kTouchMove,
                           blink::WebInputEvent::Type::kGesturePinchUpdate));

  // Mouse drag without shift becomes scroll again.
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 70, 0,
                           true);
  EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove,
            observer.acked_touch_event_type());

  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  EXPECT_THAT(
      dispatched_events,
      testing::ElementsAre(blink::WebInputEvent::Type::kTouchMove,
                           blink::WebInputEvent::Type::kGesturePinchEnd,
                           blink::WebInputEvent::Type::kGestureScrollUpdate));

  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 60, 0,
                           true);
  EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove,
            observer.acked_touch_event_type());
  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  ASSERT_THAT(
      dispatched_events,
      testing::ElementsAre(blink::WebInputEvent::Type::kTouchMove,
                           blink::WebInputEvent::Type::kGestureScrollUpdate));

  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseUp, 10, 60, 0,
                           true);
  if (observer.acked_touch_event_type() !=
      blink::WebInputEvent::Type::kTouchEnd) {
    WaitForAckWith(blink::WebInputEvent::Type::kTouchEnd);
  }
  EXPECT_EQ(blink::WebInputEvent::Type::kTouchEnd,
            observer.acked_touch_event_type());
  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  ASSERT_THAT(
      dispatched_events,
      testing::ElementsAre(blink::WebInputEvent::Type::kTouchEnd,
                           blink::WebInputEvent::Type::kGestureScrollEnd));

  // Mouse move does nothing.
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 50, 0,
                           false);
  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  EXPECT_EQ(0u, dispatched_events.size());

  // Another mouse down continues scroll.
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseDown, 10, 50, 0,
                           true);
  WaitForAckWith(blink::WebInputEvent::Type::kTouchStart);
  EXPECT_EQ(blink::WebInputEvent::Type::kTouchStart,
            observer.acked_touch_event_type());
  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  ASSERT_THAT(
      dispatched_events,
      testing::ElementsAre(blink::WebInputEvent::Type::kTouchStart,
                           blink::WebInputEvent::Type::kGestureTapDown));

  SimulateRoutedMouseDrag(observer, 10, 30);

  // Another pinch.
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 20,
                           blink::WebInputEvent::kShiftKey, true);
  EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove,
            observer.acked_touch_event_type());
  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  ASSERT_THAT(
      dispatched_events,
      testing::ElementsAre(blink::WebInputEvent::Type::kTouchMove,
                           blink::WebInputEvent::Type::kGesturePinchBegin));
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 10,
                           blink::WebInputEvent::kShiftKey, true);
  EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove,
            observer.acked_touch_event_type());
  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  ASSERT_THAT(
      dispatched_events,
      testing::ElementsAre(blink::WebInputEvent::Type::kTouchMove,
                           blink::WebInputEvent::Type::kGesturePinchUpdate));

  // Turn off emulation during a pinch.
  host()->GetTouchEmulator(/*create_if_necessary=*/true)->Disable();
  EXPECT_EQ(blink::WebInputEvent::Type::kTouchCancel,
            observer.acked_touch_event_type());
  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  EXPECT_THAT(
      dispatched_events,
      testing::ElementsAre(blink::WebInputEvent::Type::kTouchCancel,
                           blink::WebInputEvent::Type::kGesturePinchEnd,
                           blink::WebInputEvent::Type::kGestureScrollEnd));

  // Mouse event should pass untouched.
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 120,
                           blink::WebInputEvent::kShiftKey, true);
  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  ASSERT_THAT(dispatched_events,
              testing::ElementsAre(blink::WebInputEvent::Type::kMouseMove));

  // Turn on emulation.
  host()
      ->GetTouchEmulator(/*create_if_necessary=*/true)
      ->Enable(input::TouchEmulator::Mode::kEmulatingTouchFromMouse,
               ui::GestureProviderConfigType::GENERIC_MOBILE);

  // Another touch.
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseDown, 10, 120, 0,
                           true);
  WaitForAckWith(blink::WebInputEvent::Type::kTouchStart);
  EXPECT_EQ(blink::WebInputEvent::Type::kTouchStart,
            observer.acked_touch_event_type());
  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  ASSERT_THAT(
      dispatched_events,
      testing::ElementsAre(blink::WebInputEvent::Type::kTouchStart,
                           blink::WebInputEvent::Type::kGestureTapDown));

  // Scroll.
  SimulateRoutedMouseDrag(observer, 10, 100);

  // Turn off emulation during a scroll.
  host()->GetTouchEmulator(/*create_if_necessary=*/true)->Disable();
  EXPECT_EQ(blink::WebInputEvent::Type::kTouchCancel,
            observer.acked_touch_event_type());

  dispatched_events = observer.GetAndResetDispatchedEventTypes();
  ASSERT_THAT(
      dispatched_events,
      testing::ElementsAre(blink::WebInputEvent::Type::kTouchCancel,
                           blink::WebInputEvent::Type::kGestureScrollEnd));

  host()->RemoveInputEventObserver(&observer);
}

// Observes the WebContents until a frame finishes loading the contents of a
// given GURL.
class DocumentLoadObserver : WebContentsObserver {
 public:
  DocumentLoadObserver(WebContents* contents, const GURL& url)
      : WebContentsObserver(contents), document_origin_(url) {}

  DocumentLoadObserver(const DocumentLoadObserver&) = delete;
  DocumentLoadObserver& operator=(const DocumentLoadObserver&) = delete;

  void Wait() {
    if (loaded_) {
      return;
    }
    run_loop_ = std::make_unique<base::RunLoop>();
    run_loop_->Run();
  }

 private:
  void DidFinishLoad(RenderFrameHost* rfh, const GURL& url) override {
    loaded_ |= (url == document_origin_);
    if (loaded_ && run_loop_) {
      run_loop_->Quit();
    }
  }

  bool loaded_ = false;
  const GURL document_origin_;
  std::unique_ptr<base::RunLoop> run_loop_;
};

// This test verifies that when a cross-process child frame loads, the initial
// updates for touch event handlers are sent from the renderer.
IN_PROC_BROWSER_TEST_F(RenderWidgetHostSitePerProcessTest,
                       OnHasTouchEventHandlers) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  GURL::Replacements replacement;
  replacement.SetHostStr("b.com");
  replacement.SetQueryStr("b()");
  GURL target_child_url = main_url.ReplaceComponents(replacement);
  DocumentLoadObserver child_frame_observer(shell()->web_contents(),
                                            target_child_url);
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  child_frame_observer.Wait();
  auto* filter = GetTouchActionFilterForWidget(web_contents()
                                                   ->GetPrimaryFrameTree()
                                                   .root()
                                                   ->child_at(0)
                                                   ->current_frame_host()
                                                   ->GetRenderWidgetHost());
  EXPECT_TRUE(filter->allowed_touch_action().has_value());
}

// The plumbing that this test is verifying is not utilized on Mac/Android,
// where popup menus don't create a popup RenderWidget, but rather they trigger
// a FrameHostMsg_ShowPopup to ask the browser to build and display the actual
// popup using native controls.
#if !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_ANDROID)

namespace {

// Helper to use inside a loop instead of using RunLoop::RunUntilIdle() to avoid
// the loop being a busy loop that prevents renderer from doing its job. Use
// only when there is no better way to synchronize.
void GiveItSomeTime(base::TimeDelta delta) {
  base::RunLoop run_loop;
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, run_loop.QuitClosure(), delta);
  run_loop.Run();
}

}  // namespace

IN_PROC_BROWSER_TEST_F(RenderWidgetHostSitePerProcessTest,
                       BrowserClosesSelectPopup) {
  // Navigate to a page with a <select> element.
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/site_isolation/page-with-select.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  SimulateEndOfPaintHoldingOnPrimaryMainFrame(shell()->web_contents());

  auto* contents = static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* root_frame_host = root->current_frame_host();
  RenderProcessHost* process = root_frame_host->GetProcess();

  // Open the <select> menu by focusing it and sending a space key
  // at the focused node. This creates a popup widget.
  input::NativeWebKeyboardEvent event(
      blink::WebKeyboardEvent::Type::kChar, blink::WebInputEvent::kNoModifiers,
      blink::WebInputEvent::GetStaticTimeStampForTests());
  event.text[0] = ' ';

  for (int i = 0; i < 2; ++i) {
    bool browser_closes = i == 0;

    // This focuses and opens the select box, creating a popup RenderWidget. We
    // wait for the RenderWidgetHost to be shown.
    auto filter =
        std::make_unique<ShowPopupWidgetWaiter>(contents, root_frame_host);
    EXPECT_TRUE(ExecJs(root_frame_host, "focusSelectMenu();"));
    root_frame_host->GetRenderWidgetHost()->ForwardKeyboardEvent(event);
    filter->Wait();

    // The popup RenderWidget will get its own routing id.
    int popup_routing_id = filter->last_routing_id();
    EXPECT_TRUE(popup_routing_id);
    // Grab a pointer to the popup RenderWidget.
    RenderWidgetHost* popup_widget_host =
        RenderWidgetHost::FromID(process->GetDeprecatedID(), popup_routing_id);
    ASSERT_TRUE(popup_widget_host);
    ASSERT_NE(popup_widget_host, root_frame_host->GetRenderWidgetHost());

    auto* popup_widget_host_impl =
        static_cast<RenderWidgetHostImpl*>(popup_widget_host);
    if (browser_closes) {
      // Close the popup RenderWidget from the browser side.
      popup_widget_host_impl->ShutdownAndDestroyWidget(true);
    } else {
      base::WeakPtr<RenderWidgetHostImpl> popup_weak_ptr =
          popup_widget_host_impl->GetWeakPtr();

      // Close the popup RenderWidget from the renderer side by removing focus.
      EXPECT_TRUE(ExecJs(root_frame_host, "document.activeElement.blur()"));

      // Ensure that the RenderWidgetHostImpl gets destroyed, which implies the
      // close step has also been sent to the renderer process.
      while (popup_weak_ptr) {
        GiveItSomeTime(TestTimeouts::tiny_timeout());
      }
    }
    // Ensure the renderer didn't explode :).
    {
      const auto title_when_done =
          std::to_array<std::u16string>({u"done 0", u"done 1"});
      TitleWatcher title_watcher(shell()->web_contents(), title_when_done[i]);
      EXPECT_TRUE(
          ExecJs(root_frame_host, JsReplace("document.title='done $1'", i)));
      EXPECT_EQ(title_watcher.WaitAndGetTitle(), title_when_done[i]);
    }
  }
}

IN_PROC_BROWSER_TEST_F(RenderWidgetHostSitePerProcessTest,
                       VerifyPopupCreatorFrameIdPlumbing) {
  // Navigate to a page with a <select> element.
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/site_isolation/page-with-select.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  SimulateEndOfPaintHoldingOnPrimaryMainFrame(shell()->web_contents());

  auto* contents = static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* root_frame_host = root->current_frame_host();
  RenderProcessHost* process = root_frame_host->GetProcess();

  // Focus and open select box to create popup widget.
  input::NativeWebKeyboardEvent event(
      blink::WebKeyboardEvent::Type::kChar, blink::WebInputEvent::kNoModifiers,
      blink::WebInputEvent::GetStaticTimeStampForTests());
  event.text[0] = ' ';

  auto filter =
      std::make_unique<ShowPopupWidgetWaiter>(contents, root_frame_host);
  EXPECT_TRUE(ExecJs(root_frame_host, "focusSelectMenu();"));
  root_frame_host->GetRenderWidgetHost()->ForwardKeyboardEvent(event);
  filter->Wait();

  int popup_routing_id = filter->last_routing_id();
  EXPECT_NE(popup_routing_id, IPC::mojom::kRoutingIdNone);
  RenderWidgetHost* popup_widget_host =
      RenderWidgetHost::FromID(process->GetDeprecatedID(), popup_routing_id);
  ASSERT_TRUE(popup_widget_host);

  // Verify that GetPopupCreatorFrameId returns the correct Creator Frame ID.
  EXPECT_EQ(popup_widget_host->GetPopupCreatorFrameId(),
            root_frame_host->GetGlobalId());
}

#endif

#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)

namespace {

// Intercept PopupWidgetHost::ShowPopup to override the initial bounds
class ShowPopupInterceptor
    : public blink::mojom::PopupWidgetHostInterceptorForTesting {
 public:
  ShowPopupInterceptor(WebContentsImpl* web_contents,
                       RenderFrameHostImpl* frame_host,
                       const gfx::Rect& overriden_bounds)
      : create_new_popup_widget_interceptor_(
            frame_host,
            base::BindOnce(&ShowPopupInterceptor::DidCreatePopupWidget,
                           base::Unretained(this))),
        overriden_bounds_(overriden_bounds) {}

  ShowPopupInterceptor(const ShowPopupInterceptor&) = delete;
  ShowPopupInterceptor& operator=(const ShowPopupInterceptor&) = delete;

  ~ShowPopupInterceptor() override {
    if (auto* rwhi = RenderWidgetHostImpl::FromID(process_id_, routing_id_)) {
      std::ignore =
          rwhi->popup_widget_host_receiver_for_testing().SwapImplForTesting(
              rwhi);
    }
  }

  void Wait() { run_loop_.Run(); }

  // blink::mojom::PopupWidgetHostInterceptorForTesting:
  blink::mojom::PopupWidgetHost* GetForwardingInterface() override {
    DCHECK_NE(IPC::mojom::kRoutingIdNone, routing_id_);
    return RenderWidgetHostImpl::FromID(process_id_, routing_id_);
  }

  void ShowPopup(const gfx::Rect& initial_rect,
                 const gfx::Rect& initial_anchor_rect,
                 ShowPopupCallback callback) override {
    GetForwardingInterface()->ShowPopup(overriden_bounds_, initial_anchor_rect,
                                        std::move(callback));
    run_loop_.Quit();
  }

  void DidCreatePopupWidget(RenderWidgetHost* render_widget_host) {
    process_id_ = render_widget_host->GetProcess()->GetDeprecatedID();
    routing_id_ = render_widget_host->GetRoutingID();
    std::ignore = static_cast<RenderWidgetHostImpl*>(render_widget_host)
                      ->popup_widget_host_receiver_for_testing()
                      .SwapImplForTesting(this);
  }

  int last_routing_id() const { return routing_id_; }

 private:
  CreateNewPopupWidgetInterceptor create_new_popup_widget_interceptor_;
  base::RunLoop run_loop_;
  gfx::Rect overriden_bounds_;
  int32_t routing_id_ = IPC::mojom::kRoutingIdNone;
  int32_t process_id_ = 0;
};

#if BUILDFLAG(IS_MAC)

// Intercepts calls to LocalFrameHost::ShowPopupMenu method(), to override
// initial bounds and hook the `PopupMenuClient`
class ShowPopupMenuInterceptor
    : public blink::mojom::LocalFrameHostInterceptorForTesting,
      public blink::mojom::PopupMenuClient {
 public:
  explicit ShowPopupMenuInterceptor(RenderFrameHostImpl* render_frame_host,
                                    const gfx::Rect& overriden_bounds,
                                    base::OnceClosure task_in_nested_loop = {})
      : overriden_bounds_(overriden_bounds),
        task_in_nested_loop_(std::move(task_in_nested_loop)),
        swapped_impl_(
            render_frame_host->local_frame_host_receiver_for_testing(),
            this) {}

  ~ShowPopupMenuInterceptor() override = default;

  LocalFrameHost* GetForwardingInterface() override {
    return swapped_impl_.old_impl();
  }

  void Wait() { run_loop_.Run(); }

  void ShowPopupMenu(
      mojo::PendingRemote<blink::mojom::PopupMenuClient> popup_client,
      const gfx::Rect& bounds,
      double font_size,
      int32_t selected_item,
      std::vector<blink::mojom::MenuItemPtr> menu_items,
      bool right_aligned,
      bool allow_multiple_selection) override {
    CHECK(GetForwardingInterface());
    // If supplied, post a task that will run inside the menu's nested event
    // loop after the menu is opened by the synchronous call below.
    if (task_in_nested_loop_) {
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, std::move(task_in_nested_loop_));
    }
    GetForwardingInterface()->ShowPopupMenu(
        receiver_.BindNewPipeAndPassRemote(), overriden_bounds_, font_size,
        selected_item, std::move(menu_items), right_aligned,
        allow_multiple_selection);
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, run_loop_.QuitClosure());
  }

  void DidAcceptIndices(const std::vector<int32_t>& indices) override {
    receiver_.reset();
  }

  void DidCancel() override {
    is_cancelled_ = true;
    receiver_.reset();
  }

  bool is_cancelled() const { return is_cancelled_; }

 private:
  base::RunLoop run_loop_;
  bool is_cancelled_{false};
  gfx::Rect overriden_bounds_;
  base::OnceClosure task_in_nested_loop_;
  mojo::test::ScopedSwapImplForTesting<blink::mojom::LocalFrameHost>
      swapped_impl_;
  mojo::Receiver<blink::mojom::PopupMenuClient> receiver_{this};
};
#endif  // BUILDFLAG(IS_MAC)

}  // namespace

IN_PROC_BROWSER_TEST_F(RenderWidgetHostSitePerProcessTest,
                       BrowserClosesPopupIntersectsPermissionPrompt) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/site_isolation/page-with-select.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  SimulateEndOfPaintHoldingOnPrimaryMainFrame(shell()->web_contents());

  auto* contents = static_cast<WebContentsImpl*>(web_contents());
  FrameTreeNode* root = contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* root_frame_host = root->current_frame_host();

  // TODO(crbug.com/40750695): Crash when we attempt to use a mock prompt here.
  // After the ticket is fixed, remove the shortcut of getting bounds and use
  // the `MockPermissionPromptFactory` instead.
  // Create a popup widget and wait for the RenderWidgetHost to be shown.
  gfx::Rect permission_exclusion_area_bounds(100, 100, 100, 100);
  static_cast<PermissionControllerImpl*>(
      root_frame_host->GetBrowserContext()->GetPermissionController())
      ->set_exclusion_area_bounds_for_tests(permission_exclusion_area_bounds);
#if BUILDFLAG(IS_MAC)
  ShowPopupMenuInterceptor show_popup_menu_interceptor(
      root_frame_host, permission_exclusion_area_bounds -
                           contents->GetContainerBounds().OffsetFromOrigin());
#else
  ShowPopupInterceptor show_popup_interceptor(contents, root_frame_host,
                                              permission_exclusion_area_bounds);
#endif  // BUILDFLAG(IS_MAC)

  input::NativeWebKeyboardEvent event(
      blink::WebKeyboardEvent::Type::kChar, blink::WebInputEvent::kNoModifiers,
      blink::WebInputEvent::GetStaticTimeStampForTests());
  event.text[0] = ' ';
  EXPECT_TRUE(ExecJs(root_frame_host, "focusSelectMenu();"));
  root_frame_host->GetRenderWidgetHost()->ForwardKeyboardEvent(event);

#if BUILDFLAG(IS_MAC)
  show_popup_menu_interceptor.Wait();
  ASSERT_TRUE(show_popup_menu_interceptor.is_cancelled());
#else
  show_popup_interceptor.Wait();
  ASSERT_FALSE(
      RenderWidgetHost::FromID(root_frame_host->GetProcess()->GetDeprecatedID(),
                               show_popup_interceptor.last_routing_id()));
#endif  // BUILDFLAG(IS_MAC)
}

#if BUILDFLAG(IS_MAC)
// Variant of the above where no permission prompt is showing when the popup
// opens, but one appears while the native menu's nested run loop is active.
// The browser should detect the new prompt and dismiss the open popup.
IN_PROC_BROWSER_TEST_F(RenderWidgetHostSitePerProcessTest,
                       BrowserClosesOpenPopupWhenPermissionPromptAppears) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/site_isolation/page-with-select.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  SimulateEndOfPaintHoldingOnPrimaryMainFrame(shell()->web_contents());

  auto* contents = static_cast<WebContentsImpl*>(web_contents());
  FrameTreeNode* root = contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* root_frame_host = root->current_frame_host();

  auto* permission_controller = static_cast<PermissionControllerImpl*>(
      root_frame_host->GetBrowserContext()->GetPermissionController());

  gfx::Rect permission_exclusion_area_bounds(100, 100, 100, 100);
  // Simulate a permission prompt being shown only after the popup has opened
  // and entered its nested run loop.
  ShowPopupMenuInterceptor show_popup_menu_interceptor(
      root_frame_host,
      permission_exclusion_area_bounds -
          contents->GetContainerBounds().OffsetFromOrigin(),
      base::BindLambdaForTesting([&]() {
        permission_controller->set_exclusion_area_bounds_for_tests(
            permission_exclusion_area_bounds);
      }));

  input::NativeWebKeyboardEvent event(
      blink::WebKeyboardEvent::Type::kChar, blink::WebInputEvent::kNoModifiers,
      blink::WebInputEvent::GetStaticTimeStampForTests());
  event.text[0] = ' ';
  EXPECT_TRUE(ExecJs(root_frame_host, "focusSelectMenu();"));
  root_frame_host->GetRenderWidgetHost()->ForwardKeyboardEvent(event);

  show_popup_menu_interceptor.Wait();
  EXPECT_TRUE(show_popup_menu_interceptor.is_cancelled());

  permission_controller->set_exclusion_area_bounds_for_tests(std::nullopt);
}
#endif  // BUILDFLAG(IS_MAC)

#endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)

// Tests that `window.screen` dimensions match the display, not the viewport,
// while the frame is fullscreen. See crbug.com/1367416
IN_PROC_BROWSER_TEST_F(RenderWidgetHostBrowserTest, FullscreenSize) {
  // Check initial dimensions before entering fullscreen.
  ASSERT_FALSE(shell()->IsFullscreenForTabOrPending(web_contents()));
  ASSERT_FALSE(web_contents()->IsFullscreen());
  WaitForVisualPropertiesAck();
  EXPECT_EQ(host()->GetScreenInfo().rect.size().ToString(),
            EvalJs(web_contents(), "`${screen.width}x${screen.height}`"));

  // Enter fullscreen; Content Shell does not resize the viewport to fill the
  // screen in fullscreen on some platforms.
  constexpr char kEnterFullscreenScript[] = R"JS(
    document.documentElement.requestFullscreen().then(() => {
        return !!document.fullscreenElement;
    });
  )JS";
  ASSERT_TRUE(EvalJs(web_contents(), kEnterFullscreenScript).ExtractBool());

  // `window.screen` dimensions match the display size.
  EXPECT_EQ(host()->GetScreenInfo().rect.size().ToString(),
            EvalJs(web_contents(), "`${screen.width}x${screen.height}`"));

  // Check dimensions again after exiting fullscreen.
  constexpr char kExitFullscreenScript[] = R"JS(
    document.exitFullscreen().then(() => {
        return !document.fullscreenElement;
    });
  )JS";
  ASSERT_TRUE(EvalJs(web_contents(), kExitFullscreenScript).ExtractBool());
  ASSERT_FALSE(web_contents()->IsFullscreen());
  EXPECT_EQ(host()->GetScreenInfo().rect.size().ToString(),
            EvalJs(web_contents(), "`${screen.width}x${screen.height}`"));
}

class RenderWidgetHostFoldableCSSTest : public RenderWidgetHostBrowserTest {
  void SetUpCommandLine(base::CommandLine* command_line) override {
    ContentBrowserTest::SetUpCommandLine(command_line);
    command_line->AppendSwitch(
        switches::kEnableExperimentalWebPlatformFeatures);
  }
};

// Tests that when a video element goes fullscreen and uses the default
// fullscreen UA stylesheet (in blink/core/css/fullscreen.css) the viewport
// segments MQs and env variables are correctly working.
IN_PROC_BROWSER_TEST_F(
    RenderWidgetHostFoldableCSSTest,
    ViewportSegmentsWorksInUAFullscreenCSSAfterEnteringFullscreen) {
  const char kTestPageURL[] =
      R"HTML(data:text/html,<!DOCTYPE html>
      <video id='video'></video>)HTML";
  EXPECT_TRUE(NavigateToURL(shell(), GURL(kTestPageURL)));
  // Check initial state before entering fullscreen.
  ASSERT_FALSE(shell()->IsFullscreenForTabOrPending(web_contents()));
  ASSERT_FALSE(web_contents()->IsFullscreen());
  constexpr char kEnterFullscreenScript[] = R"JS(
    document.querySelector('video').requestFullscreen().then(() => {
        return !!document.fullscreenElement;
    });
  )JS";
  // Initial state. This will ensure that no display feature/viewport segments
  // are coming from the platform.
  view()->OverrideDisplayFeatureForEmulation(nullptr);
  ASSERT_TRUE(EvalJs(web_contents(), kEnterFullscreenScript).ExtractBool());

  // Changing the display feature/viewport segments without leaving fullscreen
  // should update the video element.
  const gfx::Size root_view_size = view()->GetVisibleViewportSize();
  const int kDisplayFeatureLength = 10;
  int offset = root_view_size.width() / 2 - kDisplayFeatureLength / 2;
  DisplayFeature emulated_display_feature{
      DisplayFeature::Orientation::kVertical, offset,
      /* mask_length */ kDisplayFeatureLength};
  view()->OverrideDisplayFeatureForEmulation(&emulated_display_feature);
  WaitForVisualPropertiesAck();
  EXPECT_EQ(base::NumberToString(offset) + "px",
            EvalJs(shell(), "getComputedStyle(video).width").ExtractString());
  // Rounding of GetVisibleViewportSize in the presence of a non-integer
  // devicePixelRatio device can make this off by one vs the video height.
  EXPECT_NEAR(
      root_view_size.height(),
      EvalJs(shell(), "parseInt(getComputedStyle(video).height)").ExtractInt(),
      1);

  emulated_display_feature.orientation =
      DisplayFeature::Orientation::kHorizontal;
  offset = root_view_size.height() / 2 - kDisplayFeatureLength / 2;
  emulated_display_feature.offset = offset;
  view()->OverrideDisplayFeatureForEmulation(&emulated_display_feature);
  WaitForVisualPropertiesAck();
  EXPECT_EQ(base::NumberToString(offset) + "px",
            EvalJs(shell(), "getComputedStyle(video).height").ExtractString());
  EXPECT_NEAR(
      root_view_size.width(),
      EvalJs(shell(), "parseInt(getComputedStyle(video).width)").ExtractInt(),
      1);

  // No display feature/viewport segments are set, the video should go
  // fullscreen.
  view()->OverrideDisplayFeatureForEmulation(nullptr);
  WaitForVisualPropertiesAck();
  // Rounding of GetVisibleViewportSize in the presence of a non-integer
  // devicePixelRatio device can make this off by one vs the video height.
  EXPECT_NEAR(
      root_view_size.height(),
      EvalJs(shell(), "parseInt(getComputedStyle(video).height)").ExtractInt(),
      1);
  EXPECT_NEAR(
      root_view_size.width(),
      EvalJs(shell(), "parseInt(getComputedStyle(video).width)").ExtractInt(),
      1);

  constexpr char kExitFullscreenScript[] = R"JS(
    document.exitFullscreen().then(() => {
        return !document.fullscreenElement;
    });
  )JS";
  ASSERT_TRUE(EvalJs(web_contents(), kExitFullscreenScript).ExtractBool());
  ASSERT_FALSE(web_contents()->IsFullscreen());
}

IN_PROC_BROWSER_TEST_F(
    RenderWidgetHostFoldableCSSTest,
    ViewportSegmentsWorksInUAFullscreenCSSBeforeEnteringFullscreen) {
  const char kTestPageURL[] =
      R"HTML(data:text/html,<!DOCTYPE html>
      <video id='video'></video>)HTML";
  EXPECT_TRUE(NavigateToURL(shell(), GURL(kTestPageURL)));
  // Check initial state before entering fullscreen.
  ASSERT_FALSE(shell()->IsFullscreenForTabOrPending(web_contents()));
  ASSERT_FALSE(web_contents()->IsFullscreen());
  constexpr char kEnterFullscreenScript[] = R"JS(
    document.querySelector('video').requestFullscreen().then(() => {
        return !!document.fullscreenElement;
    });
  )JS";

  const gfx::Size root_view_size = view()->GetVisibleViewportSize();
  const int kDisplayFeatureLength = 10;
  int offset = root_view_size.height() / 2 - kDisplayFeatureLength / 2;
  DisplayFeature emulated_display_feature{
      DisplayFeature::Orientation::kHorizontal, offset,
      /* mask_length */ kDisplayFeatureLength};
  view()->OverrideDisplayFeatureForEmulation(&emulated_display_feature);
  WaitForVisualPropertiesAck();
  ASSERT_TRUE(EvalJs(web_contents(), kEnterFullscreenScript).ExtractBool());
  EXPECT_EQ(base::NumberToString(offset) + "px",
            EvalJs(shell(), "getComputedStyle(video).height").ExtractString());
  EXPECT_NEAR(
      root_view_size.width(),
      EvalJs(shell(), "parseInt(getComputedStyle(video).width)").ExtractInt(),
      1);
}

IN_PROC_BROWSER_TEST_F(RenderWidgetHostFoldableCSSTest,
                       FoldablesCSSWithReload) {
  const char kTestPageURL[] =
      R"HTML(data:text/html,<!DOCTYPE html>
      <style>
        @media (horizontal-viewport-segments: 2) and
               (vertical-viewport-segments: 1) {
          div { margin-left: env(viewport-segment-right 0 0, 10px); }
        }
      </style>
      <div id='target'></div>)HTML";

  LoadStopObserver load_stop_observer(shell()->web_contents());
  EXPECT_TRUE(NavigateToURL(shell(), GURL(kTestPageURL)));
  load_stop_observer.Wait();

  const gfx::Size root_view_size = view()->GetVisibleViewportSize();
  const int kDisplayFeatureLength = 10;
  const int offset = root_view_size.width() / 2 - kDisplayFeatureLength / 2;
  DisplayFeature emulated_display_feature{
      DisplayFeature::Orientation::kVertical, offset,
      /* mask_length */ kDisplayFeatureLength};
  view()->OverrideDisplayFeatureForEmulation(&emulated_display_feature);

  EXPECT_EQ(
      base::NumberToString(emulated_display_feature.offset) + "px",
      EvalJs(shell(), "getComputedStyle(target).marginLeft").ExtractString());

  // Ensure that the environment variables have the correct values in the new
  // document that is created on reloading the page.
  LoadStopObserver load_stop_observer2(shell()->web_contents());
  TestNavigationManager navigation_manager(shell()->web_contents(),
                                           GURL(kTestPageURL));
  shell()->Reload();
  EXPECT_TRUE(navigation_manager.WaitForResponse());
  if (ShouldCreateNewHostForAllFrames()) {
    // When RenderDocument is enabled, a new RenderWidgetHost will be created
    // after the reload, so we need to call SynchronizeVisualProperties() again.
    RenderWidgetHostImpl* target_rwh = static_cast<RenderWidgetHostImpl*>(
        navigation_manager.GetNavigationHandle()
            ->GetRenderFrameHost()
            ->GetRenderWidgetHost());
    target_rwh->GetView()->OverrideDisplayFeatureForEmulation(
        &emulated_display_feature);
  }
  EXPECT_TRUE(navigation_manager.WaitForNavigationFinished());
  load_stop_observer2.Wait();

  EXPECT_EQ(
      base::NumberToString(emulated_display_feature.offset) + "px",
      EvalJs(shell(), "getComputedStyle(target).marginLeft").ExtractString());
}

class RenderWidgetHostDelegatedInkMetadataTest
    : public RenderWidgetHostTouchEmulatorBrowserTest {
 public:
  RenderWidgetHostDelegatedInkMetadataTest() = default;
  void SetUpOnMainThread() override {
    ContentBrowserTest::SetUpOnMainThread();

    LoadStopObserver load_stop_observer(shell()->web_contents());
    EXPECT_TRUE(
        NavigateToURL(shell(), GURL(R"HTML(data:text/html,<!DOCTYPE html>
      <body> <canvas id="board" width="400" height="400"></canvas> </body>
    )HTML")));
    load_stop_observer.Wait();
    SimulateEndOfPaintHoldingOnPrimaryMainFrame(shell()->web_contents());
  }

 protected:
  void WaitForDelegatedInkMetadata(
      RenderFrameSubmissionObserver& frame_observer) {
    // The mouse event is not necessarily routed in the first frame that is
    // generated. Generate frames until the mouse event and canvas paint is
    // routed to the compositor.
    do {
      frame_observer.WaitForMetadataChange();
    } while (!frame_observer.LastRenderFrameMetadata()
                  .delegated_ink_metadata.has_value() &&
             frame_observer.render_frame_count() <= kMaxFrames);
    frame_observer.ResetCounter();
  }

 private:
  static constexpr int kMaxFrames = 3;
};

// Confirm that using the |updateInkTrailStartPoint| JS API results in the
// |request_points_for_delegated_ink_| flag being set on the RWHVB.
IN_PROC_BROWSER_TEST_F(RenderWidgetHostDelegatedInkMetadataTest,
                       FlagGetsSetFromRenderFrameMetadata) {
  ASSERT_TRUE(ExecJs(shell()->web_contents(), R"(
    let ctx = board.getContext('2d');
    let presenter = null;
    navigator.ink.requestPresenter().then(e => { presenter = e; });
    const pointSize = 15;
    const style = { color: 'rgb(255,0,0)', diameter: pointSize };

    board.addEventListener('pointermove', event => {
        // Paint on the canvas to force damage and new frames generation.
        ctx.fillstyle = 'rgb(0,255,0)';
        ctx.fillRect(event.clientX, event.clientY - board
                .getBoundingClientRect().top, pointSize, pointSize);
        presenter.updateInkTrailStartPoint(event, style);
    });
  )"));
  RenderFrameMetadataProviderImpl* metadata_provider =
      host()->render_frame_metadata_provider();
  RenderFrameSubmissionObserver frame_observer(metadata_provider);
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 10, 0,
                           false);
  WaitForDelegatedInkMetadata(frame_observer);

  EXPECT_TRUE(metadata_provider->LastRenderFrameMetadata()
                  .delegated_ink_metadata.value()
                  .delegated_ink_is_hovering);

  // Confirm that the state of hover changing on the next produced delegated ink
  // metadata results in a new RenderFrameMetadata being sent, with
  // |delegated_ink_hovering| false.
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 20, 20,
                           blink::WebInputEvent::kLeftButtonDown, false);
  WaitForDelegatedInkMetadata(frame_observer);

  EXPECT_FALSE(metadata_provider->LastRenderFrameMetadata()
                   .delegated_ink_metadata.value()
                   .delegated_ink_is_hovering);

  // Confirm that the flag is set back to false when the JS API isn't called.
  RunUntilInputProcessed(host());
  const cc::RenderFrameMetadata& last_metadata =
      metadata_provider->LastRenderFrameMetadata();
  EXPECT_FALSE(last_metadata.delegated_ink_metadata.has_value());

  // Finally, confirm that a change in hovering state (pointerdown to pointerup
  // here) without a call to updateInkTrailStartPoint doesn't cause a new
  // RenderFrameMetadata to be sent.
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 20, 20, 0,
                           false);
  RunUntilInputProcessed(host());
  EXPECT_EQ(last_metadata, metadata_provider->LastRenderFrameMetadata());
}

// If the DelegatedInkTrailPresenter creates a metadata that has the same
// timestamp as the previous one, it does not set the metadata.
IN_PROC_BROWSER_TEST_F(RenderWidgetHostDelegatedInkMetadataTest,
                       DuplicateMetadata) {
  ASSERT_TRUE(ExecJs(shell()->web_contents(), R"(
    let ctx = board.getContext('2d');
    let presenter = null;
    navigator.ink.requestPresenter().then(e => { presenter = e; });
    const pointSize = 15;
    const style = { color: 'rgb(255,0,0)', diameter: pointSize };
    let first_move_event = null;

    board.addEventListener('pointermove', event => {
        // Paint on the canvas to force damage and new frames generation.
        ctx.fillstyle = 'rgb(0,255,0)';
        ctx.fillRect(event.clientX, event.clientY - board
                .getBoundingClientRect().top, pointSize, pointSize);
        if (first_move_event == null) {
          first_move_event = event;
        }
        presenter.updateInkTrailStartPoint(first_move_event, style);
    });
  )"));

  RenderFrameMetadataProviderImpl* metadata_provider =
      host()->render_frame_metadata_provider();
  RenderFrameSubmissionObserver frame_observer(metadata_provider);
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 10, 0,
                           false);

  WaitForDelegatedInkMetadata(frame_observer);
  EXPECT_TRUE(metadata_provider->LastRenderFrameMetadata()
                  .delegated_ink_metadata.value()
                  .delegated_ink_is_hovering);

  // Confirm metadata has no value when updateInkTrailStartPoint is called
  // with the same event.
  SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 20, 20,
                           blink::WebInputEvent::kLeftButtonDown, false);
  RunUntilInputProcessed(host());
  EXPECT_FALSE(metadata_provider->LastRenderFrameMetadata()
                   .delegated_ink_metadata.has_value());
}

#if BUILDFLAG(IS_ANDROID)
namespace {

class LocalSurfaceIdChangedObserver
    : public RenderFrameMetadataProvider::Observer {
 public:
  explicit LocalSurfaceIdChangedObserver(
      bool expect_newer_id,
      const viz::LocalSurfaceId& local_surface_id,
      RenderFrameMetadataProviderImpl* provider)
      : expect_newer_id_(expect_newer_id),
        current_id_(local_surface_id),
        provider_(provider) {
    provider_->AddObserver(this);
  }
  ~LocalSurfaceIdChangedObserver() override { provider_->RemoveObserver(this); }

  // `RenderFrameMetadataProvider::Observer`:
  void OnRenderFrameMetadataChangedBeforeActivation(
      const cc::RenderFrameMetadata& metadata) override {
    if (!metadata.local_surface_id.has_value()) {
      return;
    }
    if (expect_newer_id_ &&
        !metadata.local_surface_id->IsNewerThan(current_id_)) {
      // Only record the first newer id.
      return;
    }
    if (!expect_newer_id_) {
      // Fail immediately instead of timing out.
      ASSERT_FALSE(metadata.local_surface_id->IsNewerThan(current_id_));
      if (metadata.local_surface_id != current_id_) {
        // Only record the first id that's the same.
        return;
      }
    }
    observed_id_ = metadata.local_surface_id.value();
    if (run_loop_) {
      run_loop_->Quit();
    }
  }
  void OnRenderFrameMetadataChangedAfterActivation(
      base::TimeTicks activation_time) override {}
  void OnRenderFrameSubmission() override {}
  void OnLocalSurfaceIdChanged(
      const cc::RenderFrameMetadata& metadata) override {}

  [[nodiscard]] bool WaitForExpectedLocalSurfaceIdUpdate(
      const viz::LocalSurfaceId& expected_id) {
    // If `OnRenderFrameMetadataChangedBeforeActivation()` is called before
    // `WaitForExpectedLocalSurfaceIdUpdate()`.
    if (observed_id_.is_valid()) {
      return observed_id_ == expected_id;
    }
    CHECK(!run_loop_);
    run_loop_ = std::make_unique<base::RunLoop>();
    run_loop_->Run();
    return observed_id_ == expected_id;
  }

  const viz::LocalSurfaceId& observed_id() const { return observed_id_; }

 private:
  const bool expect_newer_id_;
  const viz::LocalSurfaceId current_id_;
  const raw_ptr<RenderFrameMetadataProviderImpl> provider_;
  std::unique_ptr<base::RunLoop> run_loop_;
  viz::LocalSurfaceId observed_id_ = viz::LocalSurfaceId{};
};

}  // namespace

class RenderWidgetHostSameDocNavUpdatesLocalSurfaceIdTest
    : public RenderWidgetHostBrowserTest,
      public ::testing::WithParamInterface<bool> {
 public:
  RenderWidgetHostSameDocNavUpdatesLocalSurfaceIdTest() = default;

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    ASSERT_TRUE(embedded_test_server()->Start());
    auto preferences = web_contents()->GetOrCreateWebPreferences();
    preferences.should_screenshot_on_mainframe_same_doc_navigation = GetParam();
    web_contents()->SetWebPreferences(preferences);
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    RenderWidgetHostBrowserTest::SetUpCommandLine(command_line);
    command_line->AppendSwitch(switches::kForcePrefersNoReducedMotion);
  }
};

// Assert that with `IncrementLocalSurfaceIdForMainframeSameDocNavigation`
// enabled, the `LocalSurfaceId` will be updated for same-doc navigations.
IN_PROC_BROWSER_TEST_P(RenderWidgetHostSameDocNavUpdatesLocalSurfaceIdTest,
                       SameDocNavigationUpdatesLocalSurfaceId) {
  bool increment_local_surface_id = GetParam();
  ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                         "/session_history/fragment.html")));
  // Changes the background color when navigate to "fragment.html#a".
  ASSERT_TRUE(ExecJs(web_contents(), R"(
    window.addEventListener("hashchange", (event) => {
      document.body.style.background = 'red';
    })
  )"));
  // Get the current LocalSurfaceId of the mainframe.
  const viz::LocalSurfaceId& id_before_nav = view()->GetLocalSurfaceId();
  LocalSurfaceIdChangedObserver obs(
      increment_local_surface_id, id_before_nav,
      view()->host()->render_frame_metadata_provider());
  viz::LocalSurfaceId expected;
  if (increment_local_surface_id) {
    // Expect the child component of the LocalSurfaceId is incremented by one,
    // as the result of the same-doc navigation to #a.
    expected = viz::LocalSurfaceId(id_before_nav.parent_sequence_number(),
                                   id_before_nav.child_sequence_number() + 1,
                                   id_before_nav.embed_token());
  } else {
    expected = id_before_nav;
  }
  ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                         "/session_history/fragment.html#a")));
  // Forces a frame submission from the renderer.
  WaitForCopyableViewInWebContents(shell()->web_contents());
  ASSERT_TRUE(obs.WaitForExpectedLocalSurfaceIdUpdate(expected))
      << "Expected " << expected << " but observed " << obs.observed_id();
}

INSTANTIATE_TEST_SUITE_P(All,
                         RenderWidgetHostSameDocNavUpdatesLocalSurfaceIdTest,
                         ::testing::Bool());

#endif  // BUILDFLAG(IS_ANDROID)

namespace {

enum class TestConfig { kSameDoc = 0, kBFCacheEnabled, kBFCacheDisabled };

static std::string DescribeTest(
    const ::testing::TestParamInfo<TestConfig>& info) {
  switch (info.param) {
    case TestConfig::kSameDoc:
      return "SameDoc";
    case TestConfig::kBFCacheEnabled:
      return "CrossDoc_BFCacheEnabled";
    case TestConfig::kBFCacheDisabled:
      return "CrossDoc_BFCacheDisabled";
  }
}

class ItemSequenceNumberObserver : public RenderFrameMetadataProvider::Observer,
                                   public WebContentsObserver {
 public:
  ItemSequenceNumberObserver(WebContents* web_contents,
                             int64_t expected_sequence_number)
      : WebContentsObserver(web_contents),
        expected_sequence_number_(expected_sequence_number) {}
  ~ItemSequenceNumberObserver() override {
    if (provider_) {
      provider_->RemoveObserver(this);
    }
  }

  // `RenderFrameMetadataProvider::Observer`:
  void OnRenderFrameMetadataChangedBeforeActivation(
      const cc::RenderFrameMetadata& metadata) override {}
  void OnRenderFrameMetadataChangedAfterActivation(
      base::TimeTicks activation_time) override {
    ASSERT_TRUE(provider_);
    if (expected_sequence_number_ ==
        provider_->LastRenderFrameMetadata()
            .primary_main_frame_item_sequence_number) {
      observed_expected_number_ = true;
    } else {
      return;
    }
    if (run_loop_) {
      run_loop_->Quit();
    }
  }
  void OnRenderFrameSubmission() override {}
  void OnLocalSurfaceIdChanged(
      const cc::RenderFrameMetadata& metadata) override {}

  // `WebContentsObserver`:
  void ReadyToCommitNavigation(NavigationHandle* navigation_handle) override {
    ASSERT_FALSE(navigation_handle->IsSameDocument());
    auto* request = static_cast<NavigationRequest*>(navigation_handle);
    RenderFrameHostImpl* rfhi = request->GetRenderFrameHost();
    ASSERT_TRUE(rfhi);
    provider_ = rfhi->GetView()->host()->render_frame_metadata_provider();
    provider_->AddObserver(this);
  }

  // For same-doc navigations, we don't get `WCO::ReadyToCommitNavigation`.
  // Since this is testing code, we just set the provider directly (instead of
  // registering a new `CommitDeferringCondition`).
  void SetProviderForSameDocNavigations(
      RenderFrameMetadataProviderImpl* provider) {
    ASSERT_FALSE(provider_);
    provider_ = provider;
    provider_->AddObserver(this);
  }

  [[nodiscard]] bool WaitForExpectedItemSequenceNumber() {
    if (!provider_) {
      return false;
    }
    // If `OnRenderFrameMetadataChangedAfterActivation()` is called before
    // `WaitForExpectedItemSequenceNumber()`.
    if (observed_expected_number_) {
      return true;
    }
    CHECK(!run_loop_);
    run_loop_ = std::make_unique<base::RunLoop>();
    run_loop_->Run();
    return observed_expected_number_;
  }

 private:
  const int64_t expected_sequence_number_;

  raw_ptr<RenderFrameMetadataProviderImpl> provider_;
  std::unique_ptr<base::RunLoop> run_loop_;
  bool observed_expected_number_ = false;
};

class ItemSequenceNumberTracker : public RenderFrameMetadataProvider::Observer {
 public:
  explicit ItemSequenceNumberTracker(RenderFrameMetadataProviderImpl* provider)
      : provider_(provider),
        last_sequence_number_(provider_->LastRenderFrameMetadata()
                                  .primary_main_frame_item_sequence_number) {
    provider_->AddObserver(this);
  }
  ~ItemSequenceNumberTracker() override { provider_->RemoveObserver(this); }

  // `RenderFrameMetadataProvider::Observer`:
  void OnRenderFrameMetadataChangedBeforeActivation(
      const cc::RenderFrameMetadata& metadata) override {}
  void OnRenderFrameMetadataChangedAfterActivation(
      base::TimeTicks activation_time) override {
    last_sequence_number_ = provider_->LastRenderFrameMetadata()
                                .primary_main_frame_item_sequence_number;
    if (run_loop_ && last_sequence_number_ == expected_sequence_number_) {
      run_loop_->Quit();
    }
  }
  void OnRenderFrameSubmission() override {}
  void OnLocalSurfaceIdChanged(
      const cc::RenderFrameMetadata& metadata) override {}

  void WaitForExpectedItemSequenceNumber(int64_t expected_sequence_number) {
    expected_sequence_number_ = expected_sequence_number;
    if (expected_sequence_number_ == last_sequence_number_) {
      return;
    }
    run_loop_ = std::make_unique<base::RunLoop>();
    run_loop_->Run();
  }

 private:
  raw_ptr<RenderFrameMetadataProviderImpl> provider_;

  int64_t expected_sequence_number_;
  int64_t last_sequence_number_;

  std::unique_ptr<base::RunLoop> run_loop_;
};

class RenderWidgetHostItemSequenceNumberInRenderFrameMetadataTest
    : public RenderWidgetHostBrowserTest,
      public ::testing::WithParamInterface<TestConfig> {
 public:
  RenderWidgetHostItemSequenceNumberInRenderFrameMetadataTest() = default;
  ~RenderWidgetHostItemSequenceNumberInRenderFrameMetadataTest() override =
      default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    auto test_config = GetParam();
    switch (test_config) {
      case TestConfig::kSameDoc: {
        first_url_ = "/changing_color.html#red";
        second_url_ = "/changing_color.html#green";
        break;
      }
      case TestConfig::kBFCacheEnabled: {
        first_url_ = "/empty.html";
        second_url_ = "/title1.html";
        scoped_feature_list_.InitWithFeaturesAndParameters(
            GetDefaultEnabledBackForwardCacheFeaturesForTesting(),
            GetDefaultDisabledBackForwardCacheFeaturesForTesting());
        break;
      }
      case TestConfig::kBFCacheDisabled: {
        first_url_ = "/empty.html";
        second_url_ = "/title1.html";
        command_line->AppendSwitch(switches::kDisableBackForwardCache);
        break;
      }
    }
    RenderWidgetHostBrowserTest::SetUpCommandLine(command_line);
  }

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  GURL FirstURL() { return embedded_test_server()->GetURL(first_url_); }

  GURL SecondURL() { return embedded_test_server()->GetURL(second_url_); }

 private:
  std::string first_url_;
  std::string second_url_;
  std::vector<std::string> hosts_;

  base::test::ScopedFeatureList scoped_feature_list_;
};

}  // namespace

IN_PROC_BROWSER_TEST_P(
    RenderWidgetHostItemSequenceNumberInRenderFrameMetadataTest,
    ItemSequenceNumberExpected) {
  ASSERT_TRUE(NavigateToURL(shell(), FirstURL()));
  ASSERT_TRUE(NavigateToURL(shell(), SecondURL()));

  auto* controller = static_cast<NavigationControllerImpl*>(
      &(web_contents()->GetController()));
  ASSERT_EQ(controller->GetEntryCount(), 2);
  int64_t expected_sequence_number =
      controller->GetEntryAtIndex(0)
          ->GetFrameEntry(controller->frame_tree().root())
          ->item_sequence_number();

  ItemSequenceNumberObserver obs(web_contents(), expected_sequence_number);
  if (GetParam() == TestConfig::kSameDoc) {
    obs.SetProviderForSameDocNavigations(
        view()->host()->render_frame_metadata_provider());
  }

  TestNavigationObserver nav_observer(web_contents(), 1);
  ASSERT_TRUE(web_contents()->GetController().CanGoBack());
  web_contents()->GetController().GoBack();

  nav_observer.WaitForNavigationFinished();
  ASSERT_TRUE(obs.WaitForExpectedItemSequenceNumber());
}

IN_PROC_BROWSER_TEST_P(
    RenderWidgetHostItemSequenceNumberInRenderFrameMetadataTest,
    ItemSequenceNumberExpectedNoContentChange) {
  if (GetParam() != TestConfig::kSameDoc) {
    return;
  }

  ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                         "/session_history/fragment.html")));

  auto* controller = static_cast<NavigationControllerImpl*>(
      &(web_contents()->GetController()));
  ItemSequenceNumberTracker tracker(
      view()->host()->render_frame_metadata_provider());

  tracker.WaitForExpectedItemSequenceNumber(
      controller->GetEntryAtIndex(0)
          ->GetFrameEntry(controller->frame_tree().root())
          ->item_sequence_number());

  ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                         "/session_history/fragment.html#a")));
  tracker.WaitForExpectedItemSequenceNumber(
      controller->GetEntryAtIndex(1)
          ->GetFrameEntry(controller->frame_tree().root())
          ->item_sequence_number());

  ASSERT_TRUE(web_contents()->GetController().CanGoBack());
  web_contents()->GetController().GoBack();
  tracker.WaitForExpectedItemSequenceNumber(
      controller->GetEntryAtIndex(0)
          ->GetFrameEntry(controller->frame_tree().root())
          ->item_sequence_number());
}

INSTANTIATE_TEST_SUITE_P(
    All,
    RenderWidgetHostItemSequenceNumberInRenderFrameMetadataTest,
#if BUILDFLAG(IS_FUCHSIA)
    // TODO(crbug/345304287): Temporary disable the same-doc variant while
    // investigate the cause for flakiness.
    ::testing::ValuesIn({TestConfig::kBFCacheEnabled,
                         TestConfig::kBFCacheDisabled}),
#else
    ::testing::ValuesIn({TestConfig::kSameDoc, TestConfig::kBFCacheEnabled,
                         TestConfig::kBFCacheDisabled}),
#endif
    &DescribeTest);

}  // namespace content
