blob: cae4babf080c60c27726322e1280d539984346e2 [file] [log] [blame]
// 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 <memory>
#include <vector>
#include "base/containers/contains.h"
#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/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) 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() {
last_simulated_event_time_ += simulated_event_time_delta_;
return last_simulated_event_time_;
}
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;
}
event.SetTimeStamp(GetNextSimulatedEventTime());
input::RenderWidgetHostInputEventRouter* router =
static_cast<WebContentsImpl*>(shell()->web_contents())
->GetInputEventRouter();
ASSERT_TRUE(router);
router->RouteMouseEvent(view(), &event, ui::LatencyInfo());
}
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(base::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_EQ(2u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchStart, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureTapDown, dispatched_events[1]);
// Mouse drag generates touch move, cancels tap and starts scroll.
SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 100, 0,
true);
dispatched_events = observer.GetAndResetDispatchedEventTypes();
ASSERT_EQ(5u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureTapCancel,
dispatched_events[1]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureScrollBegin,
dispatched_events[2]);
EXPECT_EQ(blink::WebInputEvent::Type::kTouchScrollStarted,
dispatched_events[3]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureScrollUpdate,
dispatched_events[4]);
EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove,
observer.acked_touch_event_type());
EXPECT_EQ(0u, observer.GetAndResetDispatchedEventTypes().size());
// 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_EQ(2u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGesturePinchBegin,
dispatched_events[1]);
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_EQ(2u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGesturePinchUpdate,
dispatched_events[1]);
// 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();
ASSERT_EQ(3u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGesturePinchEnd, dispatched_events[1]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureScrollUpdate,
dispatched_events[2]);
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_EQ(2u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureScrollUpdate,
dispatched_events[1]);
SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseUp, 10, 60, 0,
true);
WaitForAckWith(blink::WebInputEvent::Type::kTouchEnd);
EXPECT_EQ(blink::WebInputEvent::Type::kTouchEnd,
observer.acked_touch_event_type());
dispatched_events = observer.GetAndResetDispatchedEventTypes();
ASSERT_EQ(2u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchEnd, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureScrollEnd,
dispatched_events[1]);
// 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_EQ(2u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchStart, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureTapDown, dispatched_events[1]);
SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 30, 0,
true);
EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove,
observer.acked_touch_event_type());
dispatched_events = observer.GetAndResetDispatchedEventTypes();
ASSERT_EQ(5u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureTapCancel,
dispatched_events[1]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureScrollBegin,
dispatched_events[2]);
EXPECT_EQ(blink::WebInputEvent::Type::kTouchScrollStarted,
dispatched_events[3]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureScrollUpdate,
dispatched_events[4]);
EXPECT_EQ(0u, observer.GetAndResetDispatchedEventTypes().size());
// 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();
EXPECT_EQ(2u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGesturePinchBegin,
dispatched_events[1]);
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();
EXPECT_EQ(2u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGesturePinchUpdate,
dispatched_events[1]);
// 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();
ASSERT_EQ(3u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchCancel, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGesturePinchEnd, dispatched_events[1]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureScrollEnd,
dispatched_events[2]);
// Mouse event should pass untouched.
SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 120,
blink::WebInputEvent::kShiftKey, true);
dispatched_events = observer.GetAndResetDispatchedEventTypes();
ASSERT_EQ(1u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kMouseMove, dispatched_events[0]);
// 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_EQ(2u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchStart, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureTapDown, dispatched_events[1]);
// Scroll.
SimulateRoutedMouseEvent(blink::WebInputEvent::Type::kMouseMove, 10, 100, 0,
true);
EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove,
observer.acked_touch_event_type());
dispatched_events = observer.GetAndResetDispatchedEventTypes();
ASSERT_EQ(5u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchMove, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureTapCancel,
dispatched_events[1]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureScrollBegin,
dispatched_events[2]);
EXPECT_EQ(blink::WebInputEvent::Type::kTouchScrollStarted,
dispatched_events[3]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureScrollUpdate,
dispatched_events[4]);
EXPECT_EQ(0u, observer.GetAndResetDispatchedEventTypes().size());
// 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_EQ(2u, dispatched_events.size());
EXPECT_EQ(blink::WebInputEvent::Type::kTouchCancel, dispatched_events[0]);
EXPECT_EQ(blink::WebInputEvent::Type::kGestureScrollEnd,
dispatched_events[1]);
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]);
}
}
}
#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)
: overriden_bounds_(overriden_bounds),
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());
GetForwardingInterface()->ShowPopupMenu(
receiver_.BindNewPipeAndPassRemote(), overriden_bounds_, font_size,
selected_item, std::move(menu_items), right_aligned,
allow_multiple_selection);
}
void DidAcceptIndices(const std::vector<int32_t>& indices) override {
receiver_.reset();
}
void DidCancel() override {
is_cancelled_ = true;
receiver_.reset();
run_loop_.Quit();
}
bool is_cancelled() const { return is_cancelled_; }
private:
base::RunLoop run_loop_;
bool is_cancelled_{false};
gfx::Rect overriden_bounds_;
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)
}
#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.increment_local_surface_id_for_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