blob: e3adb05a4eb7ec99f0286f4591543f6ad90b1154 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/site_per_process_browsertest.h"
#include <stddef.h>
#include <stdint.h>
#include <algorithm>
#include <list>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/scoped_observer.h"
#include "base/sequenced_task_runner.h"
#include "base/single_thread_task_runner.h"
#include "base/stl_util.h"
#include "base/strings/pattern.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/test/bind_test_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/test_timeouts.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "build/build_config.h"
#include "cc/input/touch_action.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "components/viz/common/features.h"
#include "content/browser/child_process_security_policy_impl.h"
#include "content/browser/frame_host/cross_process_frame_connector.h"
#include "content/browser/frame_host/frame_navigation_entry.h"
#include "content/browser/frame_host/frame_tree.h"
#include "content/browser/frame_host/interstitial_page_impl.h"
#include "content/browser/frame_host/navigation_controller_impl.h"
#include "content/browser/frame_host/navigation_entry_impl.h"
#include "content/browser/frame_host/navigator.h"
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/frame_host/render_frame_proxy_host.h"
#include "content/browser/gpu/compositor_util.h"
#include "content/browser/loader/resource_dispatcher_host_impl.h"
#include "content/browser/renderer_host/input/input_router.h"
#include "content/browser/renderer_host/input/synthetic_gesture.h"
#include "content/browser/renderer_host/input/synthetic_gesture_target.h"
#include "content/browser/renderer_host/input/synthetic_smooth_scroll_gesture.h"
#include "content/browser/renderer_host/input/synthetic_tap_gesture.h"
#include "content/browser/renderer_host/input/synthetic_touchscreen_pinch_gesture.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_input_event_router.h"
#include "content/browser/renderer_host/render_widget_host_view_child_frame.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/url_loader_factory_getter.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/frame_messages.h"
#include "content/common/input/actions_parser.h"
#include "content/common/input/synthetic_pinch_gesture_params.h"
#include "content/common/input_messages.h"
#include "content/common/renderer.mojom.h"
#include "content/common/view_messages.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/interstitial_page_delegate.h"
#include "content/public/browser/javascript_dialog_manager.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/notification_observer.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/render_widget_host_observer.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/navigation_policy.h"
#include "content/public/common/url_constants.h"
#include "content/public/common/use_zoom_for_dsf_policy.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/hit_test_region_observer.h"
#include "content/public/test/navigation_handle_observer.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/shell/browser/shell.h"
#include "content/shell/common/shell_switches.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/did_commit_navigation_interceptor.h"
#include "ipc/constants.mojom.h"
#include "ipc/ipc_security_test_util.h"
#include "media/base/media_switches.h"
#include "mojo/public/cpp/bindings/strong_binding.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/public/cpp/features.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/feature_policy/feature_policy.h"
#include "third_party/blink/public/common/feature_policy/policy_value.h"
#include "third_party/blink/public/common/frame/sandbox_flags.h"
#include "third_party/blink/public/platform/web_input_event.h"
#include "third_party/blink/public/platform/web_insecure_request_policy.h"
#include "ui/display/display_switches.h"
#include "ui/display/screen.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/blink/blink_features.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/events/gesture_detection/gesture_configuration.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/dom_key.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/latency/latency_info.h"
#include "ui/native_theme/native_theme_features.h"
#if defined(USE_AURA)
#include "content/browser/renderer_host/render_widget_host_view_aura.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#endif
#if defined(OS_MACOSX)
#include "content/browser/renderer_host/input/synthetic_touchpad_pinch_gesture.h"
#include "ui/base/test/scoped_preferred_scroller_style_mac.h"
#endif
#if defined(OS_ANDROID)
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/android/scoped_java_ref.h"
#include "content/browser/android/ime_adapter_android.h"
#include "content/browser/renderer_host/input/touch_selection_controller_client_manager_android.h"
#include "content/browser/renderer_host/render_widget_host_view_android.h"
#include "content/browser/web_contents/web_contents_view_android.h"
#include "content/public/browser/android/child_process_importance.h"
#include "content/test/mock_overscroll_refresh_handler_android.h"
#include "ui/android/view_android.h"
#include "ui/android/window_android.h"
#include "ui/events/android/event_handler_android.h"
#include "ui/events/android/motion_event_android.h"
#include "ui/gfx/geometry/point_f.h"
#endif
#if defined(OS_CHROMEOS)
#include "ui/aura/test/test_screen.h"
#endif
using ::testing::SizeIs;
using ::testing::WhenSorted;
using ::testing::ElementsAre;
namespace content {
namespace {
// Helper function to send a postMessage and wait for a reply message. The
// |post_message_script| is executed on the |sender_ftn| frame, and the sender
// frame is expected to post |reply_status| from the DOMAutomationController
// when it receives a reply.
void PostMessageAndWaitForReply(FrameTreeNode* sender_ftn,
const std::string& post_message_script,
const std::string& reply_status) {
// Subtle: msg_queue needs to be declared before the ExecuteScript below, or
// else it might miss the message of interest. See https://crbug.com/518729.
DOMMessageQueue msg_queue;
EXPECT_EQ(true, EvalJs(sender_ftn, "(" + post_message_script + ");"));
std::string status;
while (msg_queue.WaitForMessage(&status)) {
if (status == reply_status)
break;
}
}
// Helper function to extract and return "window.receivedMessages" from the
// |sender_ftn| frame. This variable is used in post_message.html to count the
// number of messages received via postMessage by the current window.
int GetReceivedMessages(FrameTreeNode* ftn) {
return EvalJs(ftn, "window.receivedMessages;").ExtractInt();
}
// Helper function to perform a window.open from the |caller_frame| targeting a
// frame with the specified name.
void NavigateNamedFrame(const ToRenderFrameHost& caller_frame,
const GURL& url,
const std::string& name) {
EXPECT_EQ(true, EvalJs(caller_frame,
JsReplace("!!window.open($1, $2)", url, name)));
}
// Helper function to generate a click on the given RenderWidgetHost. The
// mouse event is forwarded directly to the RenderWidgetHost without any
// hit-testing.
void SimulateMouseClick(RenderWidgetHost* rwh, int x, int y) {
blink::WebMouseEvent mouse_event(
blink::WebInputEvent::kMouseDown, blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
mouse_event.button = blink::WebPointerProperties::Button::kLeft;
mouse_event.SetPositionInWidget(x, y);
rwh->ForwardMouseEvent(mouse_event);
}
// Retrieve self.origin for the frame |ftn|.
EvalJsResult GetOriginFromRenderer(FrameTreeNode* ftn) {
return EvalJs(ftn, "self.origin;");
}
double GetFrameDeviceScaleFactor(const ToRenderFrameHost& adapter) {
return EvalJs(adapter, "window.devicePixelRatio;").ExtractDouble();
}
class RedirectNotificationObserver : public NotificationObserver {
public:
// Register to listen for notifications of the given type from either a
// specific source, or from all sources if |source| is
// NotificationService::AllSources().
RedirectNotificationObserver(int notification_type,
const NotificationSource& source);
~RedirectNotificationObserver() override;
// Wait until the specified notification occurs. If the notification was
// emitted between the construction of this object and this call then it
// returns immediately.
void Wait();
// Returns NotificationService::AllSources() if we haven't observed a
// notification yet.
const NotificationSource& source() const {
return source_;
}
const NotificationDetails& details() const {
return details_;
}
// NotificationObserver:
void Observe(int type,
const NotificationSource& source,
const NotificationDetails& details) override;
private:
bool seen_;
bool seen_twice_;
bool running_;
NotificationRegistrar registrar_;
NotificationSource source_;
NotificationDetails details_;
base::RunLoop run_loop_;
DISALLOW_COPY_AND_ASSIGN(RedirectNotificationObserver);
};
RedirectNotificationObserver::RedirectNotificationObserver(
int notification_type,
const NotificationSource& source)
: seen_(false),
running_(false),
source_(NotificationService::AllSources()) {
registrar_.Add(this, notification_type, source);
}
RedirectNotificationObserver::~RedirectNotificationObserver() {}
void RedirectNotificationObserver::Wait() {
if (seen_ && seen_twice_)
return;
running_ = true;
run_loop_.Run();
EXPECT_TRUE(seen_);
}
void RedirectNotificationObserver::Observe(
int type,
const NotificationSource& source,
const NotificationDetails& details) {
source_ = source;
details_ = details;
seen_twice_ = seen_;
seen_ = true;
if (!running_)
return;
run_loop_.Quit();
running_ = false;
}
// This observer keeps track of the number of created RenderFrameHosts. Tests
// can use this to ensure that a certain number of child frames has been
// created after navigating.
class RenderFrameHostCreatedObserver : public WebContentsObserver {
public:
RenderFrameHostCreatedObserver(WebContents* web_contents,
int expected_frame_count)
: WebContentsObserver(web_contents),
expected_frame_count_(expected_frame_count),
frames_created_(0) {}
~RenderFrameHostCreatedObserver() override;
// Runs a nested run loop and blocks until the expected number of
// RenderFrameHosts is created.
void Wait();
private:
// WebContentsObserver
void RenderFrameCreated(RenderFrameHost* render_frame_host) override;
// The number of RenderFrameHosts to wait for.
int expected_frame_count_;
// The number of RenderFrameHosts that have been created.
int frames_created_;
// The RunLoop used to spin the message loop.
base::RunLoop run_loop_;
DISALLOW_COPY_AND_ASSIGN(RenderFrameHostCreatedObserver);
};
RenderFrameHostCreatedObserver::~RenderFrameHostCreatedObserver() {
}
void RenderFrameHostCreatedObserver::Wait() {
run_loop_.Run();
}
void RenderFrameHostCreatedObserver::RenderFrameCreated(
RenderFrameHost* render_frame_host) {
frames_created_++;
if (frames_created_ == expected_frame_count_) {
run_loop_.Quit();
}
}
// This observer detects when WebContents receives notification of a user
// gesture having occurred, following a user input event targeted to
// a RenderWidgetHost under that WebContents.
class UserInteractionObserver : public WebContentsObserver {
public:
explicit UserInteractionObserver(WebContents* web_contents)
: WebContentsObserver(web_contents), user_interaction_received_(false) {}
~UserInteractionObserver() override {}
// Retrieve the flag. There is no need to wait on a loop since
// DidGetUserInteraction() should be called synchronously with the input
// event processing in the browser process.
bool WasUserInteractionReceived() { return user_interaction_received_; }
void Reset() { user_interaction_received_ = false; }
private:
// WebContentsObserver
void DidGetUserInteraction(const blink::WebInputEvent::Type type) override {
user_interaction_received_ = true;
}
bool user_interaction_received_;
DISALLOW_COPY_AND_ASSIGN(UserInteractionObserver);
};
// Helper function to focus a frame by sending it a mouse click and then
// waiting for it to become focused.
void FocusFrame(FrameTreeNode* frame) {
FrameFocusedObserver focus_observer(frame->current_frame_host());
SimulateMouseClick(frame->current_frame_host()->GetRenderWidgetHost(), 1, 1);
focus_observer.Wait();
}
class RenderWidgetHostVisibilityObserver : public RenderWidgetHostObserver {
public:
explicit RenderWidgetHostVisibilityObserver(RenderWidgetHostImpl* rwhi,
bool expected_visibility_state)
: expected_visibility_state_(expected_visibility_state),
observer_(this),
was_observed_(false),
did_fail_(false),
render_widget_(rwhi) {
observer_.Add(render_widget_);
message_loop_runner_ = new MessageLoopRunner;
}
bool WaitUntilSatisfied() {
if (!was_observed_)
message_loop_runner_->Run();
if (observer_.IsObserving(render_widget_))
observer_.Remove(render_widget_);
return !did_fail_;
}
private:
void RenderWidgetHostVisibilityChanged(RenderWidgetHost* widget_host,
bool became_visible) override {
was_observed_ = true;
did_fail_ = expected_visibility_state_ != became_visible;
if (message_loop_runner_->loop_running())
message_loop_runner_->Quit();
}
void RenderWidgetHostDestroyed(RenderWidgetHost* widget_host) override {
observer_.Remove(widget_host);
}
bool expected_visibility_state_;
scoped_refptr<MessageLoopRunner> message_loop_runner_;
ScopedObserver<RenderWidgetHost, RenderWidgetHostObserver> observer_;
bool was_observed_;
bool did_fail_;
RenderWidgetHost* render_widget_;
DISALLOW_COPY_AND_ASSIGN(RenderWidgetHostVisibilityObserver);
};
class TestInterstitialDelegate : public InterstitialPageDelegate {
private:
// InterstitialPageDelegate:
std::string GetHTMLContents() override { return "<p>Interstitial</p>"; }
};
bool ConvertJSONToPoint(const std::string& str, gfx::PointF* point) {
base::Optional<base::Value> value = base::JSONReader::Read(str);
if (!value.has_value())
return false;
if (!value->is_dict())
return false;
base::Optional<double> x = value->FindDoubleKey("x");
base::Optional<double> y = value->FindDoubleKey("y");
if (!x.has_value())
return false;
if (!y.has_value())
return false;
point->set_x(x.value());
point->set_y(y.value());
return true;
}
void OpenURLBlockUntilNavigationComplete(Shell* shell, const GURL& url) {
WaitForLoadStop(shell->web_contents());
TestNavigationObserver same_tab_observer(shell->web_contents(), 1);
OpenURLParams params(
url,
content::Referrer(shell->web_contents()->GetLastCommittedURL(),
network::mojom::ReferrerPolicy::kAlways),
WindowOpenDisposition::CURRENT_TAB, ui::PAGE_TRANSITION_LINK,
true /* is_renderer_initiated */);
params.initiator_origin = url::Origin::Create(url);
shell->OpenURLFromTab(shell->web_contents(), params);
same_tab_observer.Wait();
}
// Helper function to generate a feature policy for a single feature and a list
// of origins. (Equivalent to the declared policy "feature origin1 origin2...".)
void SetParsedFeaturePolicyDeclaration(
blink::ParsedFeaturePolicyDeclaration* declaration,
blink::mojom::FeaturePolicyFeature feature,
const std::vector<GURL>& origins) {
declaration->feature = feature;
blink::mojom::PolicyValueType feature_type =
blink::FeaturePolicy::GetDefaultFeatureList().at(feature).second;
declaration->fallback_value =
blink::PolicyValue::CreateMinPolicyValue(feature_type);
declaration->opaque_value = declaration->fallback_value;
if (feature == blink::mojom::FeaturePolicyFeature::kOversizedImages) {
declaration->fallback_value.SetDoubleValue(2.0);
declaration->opaque_value.SetDoubleValue(2.0);
}
DCHECK(!origins.empty());
for (const auto origin : origins)
declaration->values.insert(std::pair<url::Origin, blink::PolicyValue>(
url::Origin::Create(origin),
blink::PolicyValue::CreateMaxPolicyValue(feature_type)));
}
blink::ParsedFeaturePolicy CreateFPHeader(
blink::mojom::FeaturePolicyFeature feature1,
blink::mojom::FeaturePolicyFeature feature2,
const std::vector<GURL>& origins) {
blink::ParsedFeaturePolicy result(2);
SetParsedFeaturePolicyDeclaration(&(result[0]), feature1, origins);
SetParsedFeaturePolicyDeclaration(&(result[1]), feature2, origins);
return result;
}
// Helper function to generate a feature policy for a single feature which
// matches every origin. (Equivalent to the declared policy "feature1 *;
// feature2 *".)
blink::ParsedFeaturePolicy CreateFPHeaderMatchesAll(
blink::mojom::FeaturePolicyFeature feature1,
blink::mojom::FeaturePolicyFeature feature2) {
blink::ParsedFeaturePolicy result(2);
blink::mojom::PolicyValueType feature_type1 =
blink::FeaturePolicy::GetDefaultFeatureList().at(feature1).second;
blink::mojom::PolicyValueType feature_type2 =
blink::FeaturePolicy::GetDefaultFeatureList().at(feature2).second;
blink::PolicyValue max_value1 =
blink::PolicyValue::CreateMaxPolicyValue(feature_type1);
blink::PolicyValue max_value2 =
blink::PolicyValue::CreateMaxPolicyValue(feature_type2);
result[0].feature = feature1;
result[0].fallback_value = max_value1;
result[0].opaque_value = max_value1;
result[1].feature = feature2;
result[1].fallback_value = max_value2;
result[1].opaque_value = max_value2;
return result;
}
// Check frame depth on node, widget, and process all match expected depth.
void CheckFrameDepth(unsigned int expected_depth, FrameTreeNode* node) {
EXPECT_EQ(expected_depth, node->depth());
RenderProcessHost::Priority priority =
node->current_frame_host()->GetRenderWidgetHost()->GetPriority();
EXPECT_EQ(expected_depth, priority.frame_depth);
EXPECT_EQ(expected_depth,
node->current_frame_host()->GetProcess()->GetFrameDepth());
}
// Check |intersects_viewport| on widget and process.
bool CheckIntersectsViewport(bool expected, FrameTreeNode* node) {
RenderProcessHost::Priority priority =
node->current_frame_host()->GetRenderWidgetHost()->GetPriority();
return priority.intersects_viewport == expected &&
node->current_frame_host()->GetProcess()->GetIntersectsViewport() ==
expected;
}
// Layout child frames in cross_site_iframe_factory.html so that they are the
// same width as the viewport, and 75% of the height of the window. This is for
// testing viewport intersection. Note this does not recurse into child frames
// and re-layout in the same way since children might be in a different origin.
void LayoutNonRecursiveForTestingViewportIntersection(
WebContents* web_contents) {
static const char* script = R"(
function relayoutNonRecursiveForTestingViewportIntersection() {
var width = window.innerWidth;
var height = window.innerHeight * 0.75;
for (var i = 0; i < window.frames.length; i++) {
child = document.getElementById("child-" + i);
child.width = width;
child.height = height;
}
}
relayoutNonRecursiveForTestingViewportIntersection();
)";
EXPECT_TRUE(ExecuteScript(web_contents, script));
}
void GenerateTapDownGesture(RenderWidgetHost* rwh) {
blink::WebGestureEvent gesture_tap_down(
blink::WebGestureEvent::kGestureTapDown,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests(),
blink::WebGestureDevice::kTouchscreen);
gesture_tap_down.is_source_touch_event_set_non_blocking = true;
rwh->ForwardGestureEvent(gesture_tap_down);
}
// Class to monitor incoming FrameHostMsg_UpdateViewportIntersection messages.
class UpdateViewportIntersectionMessageFilter
: public content::BrowserMessageFilter {
public:
UpdateViewportIntersectionMessageFilter()
: content::BrowserMessageFilter(FrameMsgStart), msg_received_(false) {}
bool OnMessageReceived(const IPC::Message& message) override {
IPC_BEGIN_MESSAGE_MAP(UpdateViewportIntersectionMessageFilter, message)
IPC_MESSAGE_HANDLER(FrameHostMsg_UpdateViewportIntersection,
OnUpdateViewportIntersection)
IPC_END_MESSAGE_MAP()
return false;
}
gfx::Rect GetCompositingRect() const { return compositing_rect_; }
gfx::Rect GetViewportIntersection() const { return viewport_intersection_; }
blink::FrameOcclusionState GetOcclusionState() const {
return occlusion_state_;
}
void Wait() {
DCHECK(!run_loop_);
if (msg_received_) {
msg_received_ = false;
return;
}
std::unique_ptr<base::RunLoop> run_loop(new base::RunLoop);
run_loop_ = run_loop.get();
run_loop_->Run();
run_loop_ = nullptr;
msg_received_ = false;
}
void set_run_loop(base::RunLoop* run_loop) { run_loop_ = run_loop; }
private:
~UpdateViewportIntersectionMessageFilter() override {}
void OnUpdateViewportIntersection(
const gfx::Rect& viewport_intersection,
const gfx::Rect& compositing_rect,
blink::FrameOcclusionState occlusion_state) {
// The message is going to be posted to UI thread after
// OnUpdateViewportIntersection returns. This additional post on the IO
// thread guarantees that by the time OnUpdateViewportIntersectionOnUI runs,
// the message has been handled on the UI thread.
base::PostTaskWithTraits(
FROM_HERE, {content::BrowserThread::IO},
base::BindOnce(&UpdateViewportIntersectionMessageFilter::
OnUpdateViewportIntersectionPostOnIO,
this, viewport_intersection, compositing_rect,
occlusion_state));
}
void OnUpdateViewportIntersectionPostOnIO(
const gfx::Rect& viewport_intersection,
const gfx::Rect& compositing_rect,
blink::FrameOcclusionState occlusion_state) {
base::PostTaskWithTraits(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&UpdateViewportIntersectionMessageFilter::
OnUpdateViewportIntersectionOnUI,
this, viewport_intersection, compositing_rect,
occlusion_state));
}
void OnUpdateViewportIntersectionOnUI(
const gfx::Rect& viewport_intersection,
const gfx::Rect& compositing_rect,
blink::FrameOcclusionState occlusion_state) {
viewport_intersection_ = viewport_intersection;
compositing_rect_ = compositing_rect;
occlusion_state_ = occlusion_state;
msg_received_ = true;
if (run_loop_)
run_loop_->Quit();
}
base::RunLoop* run_loop_ = nullptr;
bool msg_received_;
gfx::Rect compositing_rect_;
gfx::Rect viewport_intersection_;
blink::FrameOcclusionState occlusion_state_ =
blink::FrameOcclusionState::kUnknown;
DISALLOW_COPY_AND_ASSIGN(UpdateViewportIntersectionMessageFilter);
};
} // namespace
//
// SitePerProcessBrowserTest
//
SitePerProcessBrowserTest::SitePerProcessBrowserTest() {}
std::string SitePerProcessBrowserTest::DepictFrameTree(FrameTreeNode* node) {
return visualizer_.DepictFrameTree(node);
}
void SitePerProcessBrowserTest::SetUpCommandLine(
base::CommandLine* command_line) {
IsolateAllSitesForTesting(command_line);
command_line->AppendSwitch(switches::kValidateInputEventStream);
#if !defined(OS_ANDROID)
// TODO(bokan): Needed for scrollability check in
// FrameOwnerPropertiesPropagationScrolling. crbug.com/662196.
feature_list_.InitAndDisableFeature(features::kOverlayScrollbar);
#endif
}
void SitePerProcessBrowserTest::SetUpOnMainThread() {
host_resolver()->AddRule("*", "127.0.0.1");
SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
//
// SitePerProcessHighDPIBrowserTest
//
class SitePerProcessHighDPIBrowserTest : public SitePerProcessBrowserTest {
public:
const double kDeviceScaleFactor = 2.0;
SitePerProcessHighDPIBrowserTest() {}
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
SitePerProcessBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(
switches::kForceDeviceScaleFactor,
base::StringPrintf("%f", kDeviceScaleFactor));
}
};
// SitePerProcessIgnoreCertErrorsBrowserTest
class SitePerProcessIgnoreCertErrorsBrowserTest
: public SitePerProcessBrowserTest {
public:
SitePerProcessIgnoreCertErrorsBrowserTest() {}
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
SitePerProcessBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
};
// SitePerProcessFeaturePolicyBrowserTest
class SitePerProcessFeaturePolicyBrowserTest
: public SitePerProcessBrowserTest {
public:
SitePerProcessFeaturePolicyBrowserTest() = default;
// Enable tests for parameterized features.
void SetUpCommandLine(base::CommandLine* command_line) override {
SitePerProcessBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII("enable-blink-features",
"ExperimentalProductivityFeatures");
}
};
// SitePerProcessFeaturePolicyJavaScriptBrowserTest
class SitePerProcessFeaturePolicyJavaScriptBrowserTest
: public SitePerProcessBrowserTest {
public:
SitePerProcessFeaturePolicyJavaScriptBrowserTest() = default;
// Enable the feature policy JavaScript interface
void SetUpCommandLine(base::CommandLine* command_line) override {
SitePerProcessBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(
"enable-blink-features",
"FeaturePolicyJavaScriptInterface,ExperimentalProductivityFeatures");
}
};
// SitePerProcessAutoplayBrowserTest
class SitePerProcessAutoplayBrowserTest : public SitePerProcessBrowserTest {
public:
SitePerProcessAutoplayBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
SitePerProcessBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(
switches::kAutoplayPolicy,
switches::autoplay::kDocumentUserActivationRequiredPolicy);
command_line->AppendSwitchASCII("enable-blink-features",
"FeaturePolicyAutoplayFeature");
}
bool AutoplayAllowed(const ToRenderFrameHost& adapter,
bool with_user_gesture) {
RenderFrameHost* rfh = adapter.render_frame_host();
const char* test_script = "attemptPlay();";
bool worked = false;
if (with_user_gesture) {
EXPECT_TRUE(ExecuteScriptAndExtractBool(rfh, test_script, &worked));
} else {
EXPECT_TRUE(ExecuteScriptWithoutUserGestureAndExtractBool(
rfh, test_script, &worked));
}
return worked;
}
void NavigateFrameAndWait(FrameTreeNode* node, const GURL& url) {
NavigateFrameToURL(node, url);
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(url, node->current_url());
}
};
class SitePerProcesScrollAnchorTest : public SitePerProcessBrowserTest {
public:
SitePerProcesScrollAnchorTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
SitePerProcessBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII("enable-blink-features",
"ScrollAnchorSerialization");
}
};
// SitePerProcessEmbedderCSPEnforcementBrowserTest
class SitePerProcessEmbedderCSPEnforcementBrowserTest
: public SitePerProcessBrowserTest {
public:
SitePerProcessEmbedderCSPEnforcementBrowserTest() {}
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
SitePerProcessBrowserTest::SetUpCommandLine(command_line);
// TODO(amalika): Remove this switch when the EmbedderCSPEnforcement becomes
// stable
command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
"EmbedderCSPEnforcement");
}
};
// SitePerProcessProgrammaticScrollTest.
class SitePerProcessProgrammaticScrollTest : public SitePerProcessBrowserTest {
public:
SitePerProcessProgrammaticScrollTest()
: kInfinity(1000000U), kPositiveXYPlane(0, 0, kInfinity, kInfinity) {}
protected:
const size_t kInfinity;
const std::string kIframeOutOfViewHTML = "/iframe_out_of_view.html";
const std::string kIframeClippedHTML = "/iframe_clipped.html";
const std::string kInputBoxHTML = "/input_box.html";
const std::string kIframeSelector = "iframe";
const std::string kInputSelector = "input";
const gfx::Rect kPositiveXYPlane;
// Waits until the |load| handle is called inside the frame.
void WaitForOnLoad(FrameTreeNode* node) {
RunCommandAndWaitForResponse(node, "notifyWhenLoaded();", "LOADED");
}
void WaitForElementVisible(FrameTreeNode* node, const std::string& sel) {
RunCommandAndWaitForResponse(
node,
base::StringPrintf("notifyWhenVisible(document.querySelector('%s'));",
sel.c_str()),
"VISIBLE");
}
void WaitForViewportToStabilize(FrameTreeNode* node) {
RunCommandAndWaitForResponse(node, "notifyWhenViewportStable(0);",
"VIEWPORT_STABLE");
}
void AddFocusedInputField(FrameTreeNode* node) {
ASSERT_TRUE(ExecuteScript(node, "addFocusedInputField();"));
}
void SetWindowScroll(FrameTreeNode* node, int x, int y) {
ASSERT_TRUE(ExecuteScript(
node, base::StringPrintf("window.scrollTo(%d, %d);", x, y)));
}
// Helper function to retrieve the bounding client rect of the element
// identified by |sel| inside |rfh|.
gfx::Rect GetBoundingClientRect(FrameTreeNode* node, const std::string& sel) {
std::string result;
EXPECT_TRUE(ExecuteScriptAndExtractString(
node,
base::StringPrintf(
"window.domAutomationController.send(rectAsString("
" document.querySelector('%s').getBoundingClientRect()));",
sel.c_str()),
&result));
return GetRectFromString(result);
}
// Returns a rect representing the current |visualViewport| in the main frame
// of |contents|.
gfx::Rect GetVisualViewport(FrameTreeNode* node) {
std::string result;
EXPECT_TRUE(ExecuteScriptAndExtractString(
node,
"window.domAutomationController.send("
" rectAsString(visualViewportAsRect()));",
&result));
return GetRectFromString(result);
}
float GetVisualViewportScale(FrameTreeNode* node) {
double scale;
EXPECT_TRUE(ExecuteScriptAndExtractDouble(
node, "window.domAutomationController.send(visualViewport.scale);",
&scale));
return static_cast<float>(scale);
}
private:
void RunCommandAndWaitForResponse(FrameTreeNode* node,
const std::string& command,
const std::string& response) {
std::string msg_from_renderer;
ASSERT_TRUE(
ExecuteScriptAndExtractString(node, command, &msg_from_renderer));
ASSERT_EQ(response, msg_from_renderer);
}
gfx::Rect GetRectFromString(const std::string& str) {
std::vector<std::string> tokens = base::SplitString(
str, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
EXPECT_EQ(4U, tokens.size());
double x = 0.0, y = 0.0, width = 0.0, height = 0.0;
EXPECT_TRUE(base::StringToDouble(tokens[0], &x));
EXPECT_TRUE(base::StringToDouble(tokens[1], &y));
EXPECT_TRUE(base::StringToDouble(tokens[2], &width));
EXPECT_TRUE(base::StringToDouble(tokens[3], &height));
return {static_cast<int>(x), static_cast<int>(y), static_cast<int>(width),
static_cast<int>(height)};
}
DISALLOW_COPY_AND_ASSIGN(SitePerProcessProgrammaticScrollTest);
};
IN_PROC_BROWSER_TEST_F(SitePerProcessHighDPIBrowserTest,
SubframeLoadsWithCorrectDeviceScaleFactor) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// On Android forcing device scale factor does not work for tests, therefore
// we ensure that make frame and iframe have the same DIP scale there, but
// not necessarily kDeviceScaleFactor.
const double expected_dip_scale =
#if defined(OS_ANDROID)
GetFrameDeviceScaleFactor(web_contents());
#else
SitePerProcessHighDPIBrowserTest::kDeviceScaleFactor;
#endif
EXPECT_EQ(expected_dip_scale, GetFrameDeviceScaleFactor(web_contents()));
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* child = root->child_at(0);
EXPECT_EQ(expected_dip_scale, GetFrameDeviceScaleFactor(child));
}
#if defined(OS_CHROMEOS)
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
SubframeUpdateToCorrectDeviceScaleFactor) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
EXPECT_EQ(1.0, GetFrameDeviceScaleFactor(web_contents()));
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* child = root->child_at(0);
EXPECT_EQ(1.0, GetFrameDeviceScaleFactor(child));
double expected_dip_scale = 2.0;
// TODO(oshima): allow DeviceScaleFactor change on other platforms
// (win, linux, mac, android and mus).
aura::TestScreen* test_screen =
static_cast<aura::TestScreen*>(display::Screen::GetScreen());
test_screen->CreateHostForPrimaryDisplay();
test_screen->SetDeviceScaleFactor(expected_dip_scale);
// This forces |expected_dip_scale| to be applied to the aura::WindowTreeHost
// and aura::Window.
aura::WindowTreeHost* window_tree_host = shell()->window()->GetHost();
window_tree_host->SetBoundsInPixels(window_tree_host->GetBoundsInPixels());
double device_scale_factor = 0;
// Wait until dppx becomes 2 if the frame's dpr hasn't beeen updated
// to 2 yet.
const char kScript[] =
"function sendDpr() "
"{window.domAutomationController.send(window.devicePixelRatio);}; "
"if (window.devicePixelRatio == 2) sendDpr();"
"window.matchMedia('screen and "
"(min-resolution: 2dppx)').addListener(function(e) { if (e.matches) { "
"sendDpr();}})";
// Make sure that both main frame and iframe are updated to 2x.
EXPECT_TRUE(
ExecuteScriptAndExtractDouble(child, kScript, &device_scale_factor));
EXPECT_EQ(expected_dip_scale, device_scale_factor);
device_scale_factor = 0;
EXPECT_TRUE(ExecuteScriptAndExtractDouble(web_contents(), kScript,
&device_scale_factor));
EXPECT_EQ(expected_dip_scale, device_scale_factor);
}
#endif
// Ensure that navigating subframes in --site-per-process mode works and the
// correct documents are committed.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest, CrossSiteIframe) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a,a(a,a(a)))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
TestNavigationObserver observer(shell()->web_contents());
// Load same-site page into iframe.
FrameTreeNode* child = root->child_at(0);
GURL http_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
NavigateFrameToURL(child, http_url);
EXPECT_EQ(http_url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
{
// There should be only one RenderWidgetHost when there are no
// cross-process iframes.
std::set<RenderWidgetHostView*> views_set =
web_contents()->GetRenderWidgetHostViewsInTree();
EXPECT_EQ(1U, views_set.size());
}
EXPECT_EQ(
" Site A\n"
" |--Site A\n"
" +--Site A\n"
" |--Site A\n"
" +--Site A\n"
" +--Site A\n"
"Where A = http://a.com/",
DepictFrameTree(root));
// Load cross-site page into iframe.
GURL url = embedded_test_server()->GetURL("foo.com", "/title2.html");
{
RenderFrameDeletedObserver deleted_observer(child->current_frame_host());
NavigateFrameToURL(root->child_at(0), url);
deleted_observer.WaitUntilDeleted();
}
// Verify that the navigation succeeded and the expected URL was loaded.
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(url, observer.last_navigation_url());
// Ensure that we have created a new process for the subframe.
ASSERT_EQ(2U, root->child_count());
SiteInstance* site_instance = child->current_frame_host()->GetSiteInstance();
RenderViewHost* rvh = child->current_frame_host()->render_view_host();
RenderProcessHost* rph = child->current_frame_host()->GetProcess();
EXPECT_NE(shell()->web_contents()->GetRenderViewHost(), rvh);
EXPECT_NE(shell()->web_contents()->GetSiteInstance(), site_instance);
EXPECT_NE(shell()->web_contents()->GetMainFrame()->GetProcess(), rph);
{
// There should be now two RenderWidgetHosts, one for each process
// rendering a frame.
std::set<RenderWidgetHostView*> views_set =
web_contents()->GetRenderWidgetHostViewsInTree();
EXPECT_EQ(2U, views_set.size());
}
RenderFrameProxyHost* proxy_to_parent =
child->render_manager()->GetProxyToParent();
EXPECT_TRUE(proxy_to_parent);
EXPECT_TRUE(proxy_to_parent->cross_process_frame_connector());
// The out-of-process iframe should have its own RenderWidgetHost,
// independent of any RenderViewHost.
EXPECT_NE(
rvh->GetWidget()->GetView(),
proxy_to_parent->cross_process_frame_connector()->get_view_for_testing());
EXPECT_TRUE(child->current_frame_host()->GetRenderWidgetHost());
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" |--Site B ------- proxies for A\n"
" +--Site A ------- proxies for B\n"
" |--Site A -- proxies for B\n"
" +--Site A -- proxies for B\n"
" +--Site A -- proxies for B\n"
"Where A = http://a.com/\n"
" B = http://foo.com/",
DepictFrameTree(root));
// Load another cross-site page into the same iframe.
url = embedded_test_server()->GetURL("bar.com", "/title3.html");
{
RenderFrameDeletedObserver deleted_observer(child->current_frame_host());
NavigateFrameToURL(root->child_at(0), url);
deleted_observer.WaitUntilDeleted();
}
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(url, observer.last_navigation_url());
// Check again that a new process is created and is different from the
// top level one and the previous one.
ASSERT_EQ(2U, root->child_count());
child = root->child_at(0);
EXPECT_NE(shell()->web_contents()->GetRenderViewHost(),
child->current_frame_host()->render_view_host());
EXPECT_NE(rvh, child->current_frame_host()->render_view_host());
EXPECT_NE(shell()->web_contents()->GetSiteInstance(),
child->current_frame_host()->GetSiteInstance());
EXPECT_NE(site_instance,
child->current_frame_host()->GetSiteInstance());
EXPECT_NE(shell()->web_contents()->GetMainFrame()->GetProcess(),
child->current_frame_host()->GetProcess());
EXPECT_NE(rph, child->current_frame_host()->GetProcess());
{
std::set<RenderWidgetHostView*> views_set =
web_contents()->GetRenderWidgetHostViewsInTree();
EXPECT_EQ(2U, views_set.size());
}
EXPECT_EQ(proxy_to_parent, child->render_manager()->GetProxyToParent());
EXPECT_TRUE(proxy_to_parent->cross_process_frame_connector());
EXPECT_NE(
child->current_frame_host()->render_view_host()->GetWidget()->GetView(),
proxy_to_parent->cross_process_frame_connector()->get_view_for_testing());
EXPECT_TRUE(child->current_frame_host()->GetRenderWidgetHost());
EXPECT_EQ(
" Site A ------------ proxies for C\n"
" |--Site C ------- proxies for A\n"
" +--Site A ------- proxies for C\n"
" |--Site A -- proxies for C\n"
" +--Site A -- proxies for C\n"
" +--Site A -- proxies for C\n"
"Where A = http://a.com/\n"
" C = http://bar.com/",
DepictFrameTree(root));
}
// Ensure that title updates affect the correct NavigationEntry after a new
// subframe navigation with an out-of-process iframe. https://crbug.com/616609.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest, TitleAfterCrossSiteIframe) {
// Start at an initial page.
GURL initial_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), initial_url));
// Navigate to a same-site page with a same-site iframe.
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
// Make the main frame update its title after the subframe loads.
EXPECT_TRUE(ExecuteScript(shell()->web_contents(),
"document.querySelector('iframe').onload = "
" function() { document.title = 'loaded'; };"));
EXPECT_TRUE(
ExecuteScript(shell()->web_contents(), "document.title = 'not loaded';"));
base::string16 expected_title(base::UTF8ToUTF16("loaded"));
TitleWatcher title_watcher(shell()->web_contents(), expected_title);
// Navigate the iframe cross-site.
TestNavigationObserver load_observer(shell()->web_contents());
GURL frame_url = embedded_test_server()->GetURL("b.com", "/title2.html");
EXPECT_TRUE(ExecuteScript(root->child_at(0)->current_frame_host(),
JsReplace("window.location.href = $1", frame_url)));
load_observer.Wait();
// Wait for the title to update and ensure it affects the right NavEntry.
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
NavigationEntry* entry =
shell()->web_contents()->GetController().GetLastCommittedEntry();
EXPECT_EQ(expected_title, entry->GetTitle());
}
// Test that the physical backing size and view bounds for a scaled out-of-
// process iframe are set and updated correctly.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
CompositorViewportPixelSizeTest) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_scaled_frame.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* parent_iframe_node = root->child_at(0);
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" +--Site A ------- proxies for B\n"
" +--Site B -- proxies for A\n"
"Where A = http://a.com/\n"
" B = http://baz.com/",
DepictFrameTree(root));
FrameTreeNode* nested_iframe_node = parent_iframe_node->child_at(0);
RenderFrameProxyHost* proxy_to_parent =
nested_iframe_node->render_manager()->GetProxyToParent();
CrossProcessFrameConnector* connector =
proxy_to_parent->cross_process_frame_connector();
RenderWidgetHostViewBase* rwhv_nested =
static_cast<RenderWidgetHostViewBase*>(
nested_iframe_node->current_frame_host()
->GetRenderWidgetHost()
->GetView());
RenderFrameSubmissionObserver frame_observer(nested_iframe_node);
frame_observer.WaitForMetadataChange();
// Verify that applying a CSS scale transform does not impact the size of the
// content of the nested iframe.
// The screen_space_rect_in_dip may be off by 1 due to rounding. There is no
// good way to avoid this due to various device-scale-factor. (e.g. when
// dsf=3.375, ceil(round(50 * 3.375) / 3.375) = 51. Thus, we allow the screen
// size in dip to be off by 1 here.
EXPECT_NEAR(50, connector->screen_space_rect_in_dip().size().width(), 1);
EXPECT_NEAR(50, connector->screen_space_rect_in_dip().size().height(), 1);
EXPECT_EQ(gfx::Size(100, 100), rwhv_nested->GetViewBounds().size());
EXPECT_EQ(gfx::Size(100, 100), connector->local_frame_size_in_dip());
EXPECT_EQ(connector->local_frame_size_in_pixels(),
rwhv_nested->GetCompositorViewportPixelSize());
}
// Verify an OOPIF resize handler doesn't fire immediately after load without
// the frame having been resized. See https://crbug.com/826457.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest, NoResizeAfterIframeLoad) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
FrameTreeNode* iframe = root->child_at(0);
GURL site_url =
embedded_test_server()->GetURL("b.com", "/page_with_resize_handler.html");
NavigateFrameToURL(iframe, site_url);
base::RunLoop().RunUntilIdle();
int resizes = -1;
EXPECT_TRUE(ExecuteScriptAndExtractInt(
iframe->current_frame_host(),
"window.domAutomationController.send(resize_count);", &resizes));
// Should be zero because the iframe only has its initial size from parent.
EXPECT_EQ(resizes, 0);
}
// Test that the view bounds for an out-of-process iframe are set and updated
// correctly, including accounting for local frame offsets in the parent and
// scroll positions.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest, ViewBoundsInNestedFrameTest) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
RenderWidgetHostViewBase* rwhv_root = static_cast<RenderWidgetHostViewBase*>(
root->current_frame_host()->GetRenderWidgetHost()->GetView());
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* parent_iframe_node = root->child_at(0);
GURL site_url(embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_positioned_frame.html"));
NavigateFrameToURL(parent_iframe_node, site_url);
RenderFrameSubmissionObserver frame_observer(shell()->web_contents());
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" +--Site A ------- proxies for B\n"
" +--Site B -- proxies for A\n"
"Where A = http://a.com/\n"
" B = http://baz.com/",
DepictFrameTree(root));
FrameTreeNode* nested_iframe_node = parent_iframe_node->child_at(0);
RenderWidgetHostViewBase* rwhv_nested =
static_cast<RenderWidgetHostViewBase*>(
nested_iframe_node->current_frame_host()
->GetRenderWidgetHost()
->GetView());
WaitForHitTestDataOrChildSurfaceReady(
nested_iframe_node->current_frame_host());
float scale_factor =
frame_observer.LastRenderFrameMetadata().page_scale_factor;
// Get the view bounds of the nested iframe, which should account for the
// relative offset of its direct parent within the root frame.
gfx::Rect bounds = rwhv_nested->GetViewBounds();
auto filter =
base::MakeRefCounted<SynchronizeVisualPropertiesMessageFilter>();
root->current_frame_host()->GetProcess()->AddFilter(filter.get());
// Scroll the parent frame downward to verify that the child rect gets updated
// correctly.
blink::WebMouseWheelEvent scroll_event(
blink::WebInputEvent::kMouseWheel, blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
scroll_event.SetPositionInWidget(
gfx::ToFlooredInt((bounds.x() - rwhv_root->GetViewBounds().x() - 5) *
scale_factor),
gfx::ToFlooredInt((bounds.y() - rwhv_root->GetViewBounds().y() - 5) *
scale_factor));
scroll_event.delta_x = 0.0f;
scroll_event.delta_y = -30.0f;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
rwhv_root->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
filter->WaitForRect();
// The precise amount of scroll for the first view position update is not
// deterministic, so this simply verifies that the OOPIF moved from its
// earlier position.
gfx::Rect update_rect = filter->last_rect();
EXPECT_LT(update_rect.y(), bounds.y() - rwhv_root->GetViewBounds().y());
}
// This test verifies that scroll bubbling from an OOPIF properly forwards
// GestureFlingStart events from the child frame to the parent frame. This
// test times out on failure.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
GestureFlingStartEventsBubble) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* child_iframe_node = root->child_at(0);
RenderWidgetHost* child_rwh =
child_iframe_node->current_frame_host()->GetRenderWidgetHost();
// The fling start won't bubble since its corresponding GSB hasn't bubbled.
InputEventAckWaiter gesture_fling_start_ack_observer(
child_rwh, blink::WebInputEvent::kGestureFlingStart);
WaitForHitTestDataOrChildSurfaceReady(
child_iframe_node->current_frame_host());
gesture_fling_start_ack_observer.Reset();
GenerateTapDownGesture(child_rwh);
// Send a GSB, GSU, GFS sequence and verify that the GFS bubbles.
blink::WebGestureEvent gesture_scroll_begin(
blink::WebGestureEvent::kGestureScrollBegin,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests(),
blink::WebGestureDevice::kTouchscreen);
gesture_scroll_begin.data.scroll_begin.delta_hint_units =
blink::WebScrollGranularity::kScrollByPrecisePixel;
gesture_scroll_begin.data.scroll_begin.delta_x_hint = 0.f;
gesture_scroll_begin.data.scroll_begin.delta_y_hint = 5.f;
child_rwh->ForwardGestureEvent(gesture_scroll_begin);
blink::WebGestureEvent gesture_scroll_update(
blink::WebGestureEvent::kGestureScrollUpdate,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests(),
blink::WebGestureDevice::kTouchscreen);
gesture_scroll_update.data.scroll_update.delta_units =
blink::WebScrollGranularity::kScrollByPrecisePixel;
gesture_scroll_update.data.scroll_update.delta_x = 0.f;
gesture_scroll_update.data.scroll_update.delta_y = 5.f;
gesture_scroll_update.data.scroll_update.velocity_y = 5.f;
child_rwh->ForwardGestureEvent(gesture_scroll_update);
blink::WebGestureEvent gesture_fling_start(
blink::WebGestureEvent::kGestureFlingStart,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests(),
blink::WebGestureDevice::kTouchscreen);
gesture_fling_start.data.fling_start.velocity_x = 0.f;
gesture_fling_start.data.fling_start.velocity_y = 5.f;
child_rwh->ForwardGestureEvent(gesture_fling_start);
// We now wait for the fling start event to be acked by the parent
// frame. If the test fails, then the test times out.
gesture_fling_start_ack_observer.Wait();
}
// Test that scrolling a nested out-of-process iframe bubbles unused scroll
// delta to a parent frame.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest, ScrollBubblingFromOOPIFTest) {
ui::GestureConfiguration::GetInstance()->set_scroll_debounce_interval_in_ms(
0);
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* parent_iframe_node = root->child_at(0);
// This test uses the position of the nested iframe within the parent iframe
// to infer the scroll position of the parent.
// SynchronizeVisualPropertiesMessageFilter catches updates to the position in
// order to avoid busy waiting. It gets created early to catch the initial
// rects from the navigation.
auto filter =
base::MakeRefCounted<SynchronizeVisualPropertiesMessageFilter>();
parent_iframe_node->current_frame_host()->GetProcess()->AddFilter(
filter.get());
InputEventAckWaiter ack_observer(
parent_iframe_node->current_frame_host()->GetRenderWidgetHost(),
blink::WebInputEvent::kGestureScrollEnd);
GURL site_url(embedded_test_server()->GetURL(
"b.com", "/frame_tree/page_with_positioned_frame.html"));
NavigateFrameToURL(parent_iframe_node, site_url);
// Navigate the nested frame to a page large enough to have scrollbars.
FrameTreeNode* nested_iframe_node = parent_iframe_node->child_at(0);
GURL nested_site_url(embedded_test_server()->GetURL(
"baz.com", "/tall_page.html"));
NavigateFrameToURL(nested_iframe_node, nested_site_url);
EXPECT_EQ(
" Site A ------------ proxies for B C\n"
" +--Site B ------- proxies for A C\n"
" +--Site C -- proxies for A B\n"
"Where A = http://a.com/\n"
" B = http://b.com/\n"
" C = http://baz.com/",
DepictFrameTree(root));
RenderWidgetHostViewBase* rwhv_parent =
static_cast<RenderWidgetHostViewBase*>(
parent_iframe_node->current_frame_host()
->GetRenderWidgetHost()
->GetView());
RenderWidgetHostViewBase* rwhv_nested =
static_cast<RenderWidgetHostViewBase*>(
nested_iframe_node->current_frame_host()
->GetRenderWidgetHost()
->GetView());
WaitForHitTestDataOrChildSurfaceReady(
nested_iframe_node->current_frame_host());
// Save the original offset as a point of reference.
filter->WaitForRect();
gfx::Rect update_rect = filter->last_rect();
int initial_y = update_rect.y();
filter->ResetRectRunLoop();
// Scroll the parent frame downward.
blink::WebMouseWheelEvent scroll_event(
blink::WebInputEvent::kMouseWheel, blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
scroll_event.SetPositionInWidget(1, 1);
scroll_event.delta_x = 0.0f;
scroll_event.delta_y = -5.0f;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
// Set has_precise_scroll_deltas to keep these events off the animated scroll
// pathways, which currently break this test.
// https://bugs.chromium.org/p/chromium/issues/detail?id=710513
scroll_event.has_precise_scrolling_deltas = true;
rwhv_parent->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
// The event router sends wheel events of a single scroll sequence to the
// target under the first wheel event. Send a wheel end event to the current
// target view before sending a wheel event to a different one.
scroll_event.delta_y = 0.0f;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseEnded;
scroll_event.dispatch_type =
blink::WebInputEvent::DispatchType::kEventNonBlocking;
rwhv_parent->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
// Ensure that the view position is propagated to the child properly.
filter->WaitForRect();
update_rect = filter->last_rect();
EXPECT_LT(update_rect.y(), initial_y);
filter->ResetRectRunLoop();
ack_observer.Reset();
// Now scroll the nested frame upward, which should bubble to the parent.
// The upscroll exceeds the amount that the frame was initially scrolled
// down to account for rounding.
scroll_event.delta_y = 6.0f;
scroll_event.dispatch_type = blink::WebInputEvent::DispatchType::kBlocking;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
rwhv_nested->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
filter->WaitForRect();
// This loop isn't great, but it accounts for the possibility of multiple
// incremental updates happening as a result of the scroll animation.
// A failure condition of this test is that the loop might not terminate
// due to bubbling not working properly. If the overscroll bubbles to the
// parent iframe then the nested frame's y coord will return to its
// initial position.
update_rect = filter->last_rect();
while (update_rect.y() > initial_y) {
base::RunLoop run_loop;
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
update_rect = filter->last_rect();
}
// The event router sends wheel events of a single scroll sequence to the
// target under the first wheel event. Send a wheel end event to the current
// target view before sending a wheel event to a different one.
scroll_event.delta_y = 0.0f;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseEnded;
scroll_event.dispatch_type =
blink::WebInputEvent::DispatchType::kEventNonBlocking;
rwhv_nested->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
filter->ResetRectRunLoop();
// Once we've sent a wheel to the nested iframe that we expect to turn into
// a bubbling scroll, we need to delay to make sure the GestureScrollBegin
// from this new scroll doesn't hit the RenderWidgetHostImpl before the
// GestureScrollEnd bubbled from the child.
// This timing only seems to be needed for CrOS, but we'll enable it on
// all platforms just to lessen the possibility of tests being flakey
// on non-CrOS platforms.
ack_observer.Wait();
// Scroll the parent down again in order to test scroll bubbling from
// gestures.
scroll_event.delta_y = -5.0f;
scroll_event.dispatch_type = blink::WebInputEvent::DispatchType::kBlocking;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
rwhv_parent->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
// The event router sends wheel events of a single scroll sequence to the
// target under the first wheel event. Send a wheel end event to the current
// target view before sending a wheel event to a different one.
scroll_event.delta_y = 0.0f;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseEnded;
scroll_event.dispatch_type =
blink::WebInputEvent::DispatchType::kEventNonBlocking;
rwhv_parent->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
// Ensure ensuing offset change is received, and then reset the filter.
filter->WaitForRect();
filter->ResetRectRunLoop();
// Scroll down the nested iframe via gesture. This requires 3 separate input
// events.
blink::WebGestureEvent gesture_event(
blink::WebGestureEvent::kGestureScrollBegin,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests(),
blink::WebGestureDevice::kTouchpad);
gesture_event.SetPositionInWidget(gfx::PointF(1, 1));
gesture_event.data.scroll_begin.delta_x_hint = 0.0f;
gesture_event.data.scroll_begin.delta_y_hint = 6.0f;
rwhv_nested->GetRenderWidgetHost()->ForwardGestureEvent(gesture_event);
gesture_event =
blink::WebGestureEvent(blink::WebGestureEvent::kGestureScrollUpdate,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests(),
blink::WebGestureDevice::kTouchpad);
gesture_event.SetPositionInWidget(gfx::PointF(1, 1));
gesture_event.data.scroll_update.delta_x = 0.0f;
gesture_event.data.scroll_update.delta_y = 6.0f;
gesture_event.data.scroll_update.velocity_x = 0;
gesture_event.data.scroll_update.velocity_y = 0;
rwhv_nested->GetRenderWidgetHost()->ForwardGestureEvent(gesture_event);
gesture_event =
blink::WebGestureEvent(blink::WebGestureEvent::kGestureScrollEnd,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests(),
blink::WebGestureDevice::kTouchpad);
gesture_event.SetPositionInWidget(gfx::PointF(1, 1));
rwhv_nested->GetRenderWidgetHost()->ForwardGestureEvent(gesture_event);
filter->WaitForRect();
update_rect = filter->last_rect();
// As above, if this loop does not terminate then it indicates an issue
// with scroll bubbling.
while (update_rect.y() > initial_y) {
base::RunLoop run_loop;
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
update_rect = filter->last_rect();
}
// Test that when the child frame absorbs all of the scroll delta, it does
// not propagate to the parent (see https://crbug.com/621624).
filter->ResetRectRunLoop();
scroll_event.delta_y = -5.0f;
scroll_event.dispatch_type = blink::WebInputEvent::DispatchType::kBlocking;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
rwhv_nested->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
// It isn't possible to busy loop waiting on the renderer here because we
// are explicitly testing that something does *not* happen. This creates a
// small chance of false positives but shouldn't result in false negatives,
// so flakiness implies this test is failing.
{
base::RunLoop run_loop;
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::action_timeout());
run_loop.Run();
}
DCHECK_EQ(filter->last_rect().x(), 0);
DCHECK_EQ(filter->last_rect().y(), 0);
}
// Tests that scrolling with the keyboard will bubble unused scroll to the
// OOPIF's parent.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
KeyboardScrollBubblingFromOOPIF) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_iframe_in_scrollable_div.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* iframe_node = root->child_at(0);
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" +--Site B ------- proxies for A\n"
"Where A = http://a.com/\n"
" B = http://b.com/",
DepictFrameTree(root));
RenderWidgetHostViewBase* rwhv_child = static_cast<RenderWidgetHostViewBase*>(
iframe_node->current_frame_host()->GetRenderWidgetHost()->GetView());
double initial_y = 0.0;
ASSERT_TRUE(content::ExecuteScriptAndExtractDouble(
root,
"var wrapperDiv = document.getElementById('wrapper-div');"
"var initial_y = wrapperDiv.scrollTop;"
"var waitForScrollDownPromise = new Promise(function(resolve) {"
" wrapperDiv.addEventListener('scroll', () => {"
" if (wrapperDiv.scrollTop > initial_y)"
" resolve(wrapperDiv.scrollTop);"
" });"
"});"
"window.domAutomationController.send(initial_y);",
&initial_y));
EXPECT_DOUBLE_EQ(0.0, initial_y);
NativeWebKeyboardEvent key_event(
blink::WebKeyboardEvent::kRawKeyDown, blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
key_event.windows_key_code = ui::VKEY_DOWN;
key_event.native_key_code =
ui::KeycodeConverter::DomCodeToNativeKeycode(ui::DomCode::ARROW_DOWN);
key_event.dom_code = static_cast<int>(ui::DomCode::ARROW_DOWN);
key_event.dom_key = ui::DomKey::ARROW_DOWN;
rwhv_child->GetRenderWidgetHost()->ForwardKeyboardEvent(key_event);
key_event.SetType(blink::WebKeyboardEvent::kKeyUp);
rwhv_child->GetRenderWidgetHost()->ForwardKeyboardEvent(key_event);
double scrolled_y = 0.0;
ASSERT_TRUE(content::ExecuteScriptAndExtractDouble(
root,
"waitForScrollDownPromise.then((scrolled_y) => {"
" window.domAutomationController.send(scrolled_y);"
"});",
&scrolled_y));
EXPECT_GT(scrolled_y, 0.0);
}
// Test that fling on an out-of-process iframe progresses properly.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
TouchscreenGestureFlingStart) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* child_iframe_node = root->child_at(0);
RenderWidgetHost* child_rwh =
child_iframe_node->current_frame_host()->GetRenderWidgetHost();
WaitForHitTestDataOrChildSurfaceReady(
child_iframe_node->current_frame_host());
GenerateTapDownGesture(child_rwh);
// Send a GSB to start scrolling sequence.
blink::WebGestureEvent gesture_scroll_begin(
blink::WebGestureEvent::kGestureScrollBegin,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
gesture_scroll_begin.SetSourceDevice(blink::WebGestureDevice::kTouchscreen);
gesture_scroll_begin.data.scroll_begin.delta_hint_units =
blink::WebScrollGranularity::kScrollByPrecisePixel;
gesture_scroll_begin.data.scroll_begin.delta_x_hint = 0.f;
gesture_scroll_begin.data.scroll_begin.delta_y_hint = 5.f;
child_rwh->ForwardGestureEvent(gesture_scroll_begin);
// Send a GFS and wait for the ack of the first GSU generated from progressing
// the fling on the browser.
InputEventAckWaiter gesture_scroll_update_ack_observer(
child_rwh, blink::WebInputEvent::kGestureScrollUpdate);
gesture_scroll_update_ack_observer.Reset();
blink::WebGestureEvent gesture_fling_start(
blink::WebGestureEvent::kGestureFlingStart,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
gesture_fling_start.SetSourceDevice(blink::WebGestureDevice::kTouchscreen);
gesture_fling_start.data.fling_start.velocity_x = 0.f;
gesture_fling_start.data.fling_start.velocity_y = 50.f;
child_rwh->ForwardGestureEvent(gesture_fling_start);
gesture_scroll_update_ack_observer.Wait();
}
// Test that fling on an out-of-process iframe progresses properly.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest, TouchpadGestureFlingStart) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* child_iframe_node = root->child_at(0);
RenderWidgetHost* child_rwh =
child_iframe_node->current_frame_host()->GetRenderWidgetHost();
// Send a wheel event with phaseBegan to start scrolling sequence.
InputEventAckWaiter gesture_scroll_begin_ack_observer(
child_rwh, blink::WebInputEvent::kGestureScrollBegin);
blink::WebMouseWheelEvent scroll_event(
blink::WebInputEvent::kMouseWheel, blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
scroll_event.delta_x = 0.0f;
scroll_event.delta_y = 5.0f;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
scroll_event.has_precise_scrolling_deltas = true;
child_rwh->ForwardWheelEvent(scroll_event);
gesture_scroll_begin_ack_observer.Wait();
// Send a GFS and wait for the ack of the first GSU generated from progressing
// the fling on the browser.
InputEventAckWaiter gesture_scroll_update_ack_observer(
child_rwh, blink::WebInputEvent::kGestureScrollUpdate);
gesture_scroll_update_ack_observer.Reset();
blink::WebGestureEvent gesture_fling_start(
blink::WebGestureEvent::kGestureFlingStart,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
gesture_fling_start.SetSourceDevice(blink::WebGestureDevice::kTouchpad);
gesture_fling_start.data.fling_start.velocity_x = 0.f;
gesture_fling_start.data.fling_start.velocity_y = 50.f;
child_rwh->ForwardGestureEvent(gesture_fling_start);
// The test will pass when the GSU ack arrives, since it shows that the fling
// controller has properly generated a GSU event from progressing the fling.
gesture_scroll_update_ack_observer.Wait();
}
class ScrollObserver : public RenderWidgetHost::InputEventObserver {
public:
ScrollObserver(double delta_x, double delta_y) { Reset(delta_x, delta_y); }
~ScrollObserver() override {}
void OnInputEvent(const blink::WebInputEvent& event) override {
if (event.GetType() == blink::WebInputEvent::kGestureScrollUpdate) {
blink::WebGestureEvent received_update =
*static_cast<const blink::WebGestureEvent*>(&event);
remaining_delta_x_ -= received_update.data.scroll_update.delta_x;
remaining_delta_y_ -= received_update.data.scroll_update.delta_y;
} else if (event.GetType() == blink::WebInputEvent::kGestureScrollEnd) {
if (message_loop_runner_->loop_running())
message_loop_runner_->Quit();
DCHECK_EQ(0, remaining_delta_x_);
DCHECK_EQ(0, remaining_delta_y_);
scroll_end_received_ = true;
}
}
void Wait() {
if (!scroll_end_received_) {
message_loop_runner_->Run();
}
}
void Reset(double delta_x, double delta_y) {
message_loop_runner_ = new content::MessageLoopRunner;
remaining_delta_x_ = delta_x;
remaining_delta_y_ = delta_y;
scroll_end_received_ = false;
}
private:
scoped_refptr<content::MessageLoopRunner> message_loop_runner_;
double remaining_delta_x_;
double remaining_delta_y_;
bool scroll_end_received_;
DISALLOW_COPY_AND_ASSIGN(ScrollObserver);
};
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
ScrollBubblingFromNestedOOPIFTest) {
ui::GestureConfiguration::GetInstance()->set_scroll_debounce_interval_in_ms(
0);
GURL main_url(embedded_test_server()->GetURL(
"/frame_tree/page_with_positioned_nested_frames.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameSubmissionObserver frame_observer(shell()->web_contents());
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* parent_iframe_node = root->child_at(0);
GURL site_url(embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_positioned_frame.html"));
EXPECT_EQ(site_url, parent_iframe_node->current_url());
FrameTreeNode* nested_iframe_node = parent_iframe_node->child_at(0);
GURL nested_site_url(
embedded_test_server()->GetURL("baz.com", "/title1.html"));
EXPECT_EQ(nested_site_url, nested_iframe_node->current_url());
RenderWidgetHostViewBase* root_view = static_cast<RenderWidgetHostViewBase*>(
root->current_frame_host()->GetRenderWidgetHost()->GetView());
RenderWidgetHostViewBase* rwhv_nested =
static_cast<RenderWidgetHostViewBase*>(
nested_iframe_node->current_frame_host()
->GetRenderWidgetHost()
->GetView());
WaitForHitTestDataOrChildSurfaceReady(
nested_iframe_node->current_frame_host());
InputEventAckWaiter ack_observer(
root->current_frame_host()->GetRenderWidgetHost(),
blink::WebInputEvent::kGestureScrollBegin);
std::unique_ptr<ScrollObserver> scroll_observer;
// All GSU events will be wrapped between a single GSB-GSE pair. The expected
// delta value is equal to summation of all scroll update deltas.
scroll_observer = std::make_unique<ScrollObserver>(0, 15);
root->current_frame_host()->GetRenderWidgetHost()->AddInputEventObserver(
scroll_observer.get());
// Now scroll the nested frame upward, this must bubble all the way up to the
// root.
blink::WebMouseWheelEvent scroll_event(
blink::WebInputEvent::kMouseWheel, blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
gfx::Rect bounds = rwhv_nested->GetViewBounds();
float scale_factor =
frame_observer.LastRenderFrameMetadata().page_scale_factor;
scroll_event.SetPositionInWidget(
gfx::ToCeiledInt((bounds.x() - root_view->GetViewBounds().x() + 10) *
scale_factor),
gfx::ToCeiledInt((bounds.y() - root_view->GetViewBounds().y() + 10) *
scale_factor));
scroll_event.delta_x = 0.0f;
scroll_event.delta_y = 5.0f;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
scroll_event.has_precise_scrolling_deltas = true;
rwhv_nested->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
ack_observer.Wait();
// Send 10 wheel events with delta_y = 1 to the nested oopif.
scroll_event.delta_y = 1.0f;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseChanged;
for (int i = 0; i < 10; i++)
rwhv_nested->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
// Send a wheel end event to complete the scrolling sequence.
scroll_event.delta_y = 0.0f;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseEnded;
rwhv_nested->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
scroll_observer->Wait();
}
// Tests that scrolling bubbles from an oopif if its source body has
// "overflow:hidden" style.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
ScrollBubblingFromOOPIFWithBodyOverflowHidden) {
GURL url_domain_a(embedded_test_server()->GetURL(
"a.com", "/scrollable_page_with_iframe.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_domain_a));
RenderFrameSubmissionObserver frame_observer(shell()->web_contents());
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
FrameTreeNode* iframe_node = root->child_at(0);
GURL url_domain_b(
embedded_test_server()->GetURL("b.com", "/body_overflow_hidden.html"));
NavigateFrameToURL(iframe_node, url_domain_b);
WaitForHitTestDataOrChildSurfaceReady(iframe_node->current_frame_host());
RenderWidgetHostViewBase* root_view = static_cast<RenderWidgetHostViewBase*>(
root->current_frame_host()->GetRenderWidgetHost()->GetView());
RenderWidgetHostViewBase* child_view = static_cast<RenderWidgetHostViewBase*>(
iframe_node->current_frame_host()->GetRenderWidgetHost()->GetView());
ScrollObserver scroll_observer(0, -5);
root->current_frame_host()->GetRenderWidgetHost()->AddInputEventObserver(
&scroll_observer);
// Now scroll the nested frame downward, this must bubble to the root since
// the iframe source body is not scrollable.
blink::WebMouseWheelEvent scroll_event(
blink::WebInputEvent::kMouseWheel, blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
gfx::Rect bounds = child_view->GetViewBounds();
float scale_factor =
frame_observer.LastRenderFrameMetadata().page_scale_factor;
scroll_event.SetPositionInWidget(
gfx::ToCeiledInt((bounds.x() - root_view->GetViewBounds().x() + 10) *
scale_factor),
gfx::ToCeiledInt((bounds.y() - root_view->GetViewBounds().y() + 10) *
scale_factor));
scroll_event.delta_x = 0.0f;
scroll_event.delta_y = -5.0f;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
scroll_event.has_precise_scrolling_deltas = true;
child_view->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
// Send a wheel end event to complete the scrolling sequence.
scroll_event.delta_y = 0.0f;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseEnded;
child_view->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
scroll_observer.Wait();
}
// Ensure that the scrollability of a local subframe in an OOPIF is considered
// when acknowledging GestureScrollBegin events sent to OOPIFs.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest, ScrollLocalSubframeInOOPIF) {
ui::GestureConfiguration::GetInstance()->set_scroll_debounce_interval_in_ms(
0);
// This must be tall enough such that the outer iframe is not scrollable.
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_tall_positioned_frame.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* parent_iframe_node = root->child_at(0);
GURL outer_frame_url(embedded_test_server()->GetURL(
"baz.com", "/frame_tree/page_with_positioned_frame.html"));
NavigateFrameToURL(parent_iframe_node, outer_frame_url);
// This must be tall enough such that the inner iframe is scrollable.
FrameTreeNode* nested_iframe_node = parent_iframe_node->child_at(0);
GURL inner_frame_url(
embedded_test_server()->GetURL("baz.com", "/tall_page.html"));
NavigateFrameToURL(nested_iframe_node, inner_frame_url);
ASSERT_EQ(
" Site A ------------ proxies for B\n"
" +--Site B ------- proxies for A\n"
" +--Site B -- proxies for A\n"
"Where A = http://a.com/\n"
" B = http://baz.com/",
DepictFrameTree(root));
RenderWidgetHostViewBase* rwhv_child = static_cast<RenderWidgetHostViewBase*>(
nested_iframe_node->current_frame_host()
->GetRenderWidgetHost()
->GetView());
WaitForHitTestDataOrChildSurfaceReady(
parent_iframe_node->current_frame_host());
// When we scroll the inner frame, we should have the GSB be consumed.
// The outer iframe not being scrollable should not cause the GSB to go
// unconsumed.
InputEventAckWaiter ack_observer(
parent_iframe_node->current_frame_host()->GetRenderWidgetHost(),
base::BindRepeating([](content::InputEventAckSource,
content::InputEventAckState state,
const blink::WebInputEvent& event) {
return event.GetType() == blink::WebGestureEvent::kGestureScrollBegin &&
state == content::INPUT_EVENT_ACK_STATE_CONSUMED;
}));
// Wait until renderer's compositor thread is synced. Otherwise the non fast
// scrollable regions won't be set when the event arrives.
MainThreadFrameObserver observer(rwhv_child->GetRenderWidgetHost());
observer.Wait();
// Now scroll the inner frame downward.
blink::WebMouseWheelEvent scroll_event(
blink::WebInputEvent::kMouseWheel, blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
scroll_event.SetPositionInWidget(90, 110);
scroll_event.delta_x = 0.0f;
scroll_event.delta_y = -50.0f;
scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
scroll_event.has_precise_scrolling_deltas = true;
rwhv_child->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo());
ack_observer.Wait();
}
// This test verifies that scrolling an element to view works across OOPIFs. The
// testing methodology is based on measuring bounding client rect position of
// nested <iframe>'s after the inner-most frame scrolls into view. The
// measurements are for two identical pages where one page does not have any
// OOPIFs while the other has some nested OOPIFs.
// TODO(crbug.com/827431): This test is flaking on all platforms.
IN_PROC_BROWSER_TEST_F(SitePerProcessProgrammaticScrollTest,
DISABLED_ScrollElementIntoView) {
const GURL url_a(
embedded_test_server()->GetURL("a.com", kIframeOutOfViewHTML));
const GURL url_b(
embedded_test_server()->GetURL("b.com", kIframeOutOfViewHTML));
const GURL url_c(
embedded_test_server()->GetURL("c.com", kIframeOutOfViewHTML));
// Number of <iframe>'s which will not be empty. The actual frame tree has two
// more nodes one for root and one for the inner-most empty <iframe>.
const size_t kNonEmptyIframesCount = 5;
const std::string kScrollIntoViewScript =
"document.body.scrollIntoView({'behavior' : 'instant'});";
const int kRectDimensionErrorTolerance = 0;
// First, recursively set the |scrollTop| and |scrollLeft| of |document.body|
// to its maximum and then navigate the <iframe> to |url_a|. The page will be
// structured as a(a(a(a(a(a(a)))))) where the inner-most <iframe> is empty.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
FrameTreeNode* node = web_contents()->GetFrameTree()->root();
WaitForOnLoad(node);
std::vector<gfx::Rect> reference_page_bounds_before_scroll = {
GetBoundingClientRect(node, kIframeSelector)};
node = node->child_at(0);
for (size_t index = 0; index < kNonEmptyIframesCount; ++index) {
NavigateFrameToURL(node, url_a);
WaitForOnLoad(node);
// Store |document.querySelector('iframe').getBoundingClientRect()|.
reference_page_bounds_before_scroll.push_back(
GetBoundingClientRect(node, kIframeSelector));
node = node->child_at(0);
}
// Sanity-check: If the page is setup properly then all the <iframe>s should
// be out of view and their bounding rect should not intersect with the
// positive XY plane.
for (const auto& rect : reference_page_bounds_before_scroll)
ASSERT_FALSE(rect.Intersects(kPositiveXYPlane));
// Now scroll the inner-most frame into view.
ASSERT_TRUE(ExecuteScript(node, kScrollIntoViewScript));
// Store current client bounds origins to later compare against those from the
// page which contains OOPIFs.
node = web_contents()->GetFrameTree()->root();
std::vector<gfx::Rect> reference_page_bounds_after_scroll = {
GetBoundingClientRect(node, kIframeSelector)};
node = node->child_at(0);
for (size_t index = 0; index < kNonEmptyIframesCount; ++index) {
reference_page_bounds_after_scroll.push_back(
GetBoundingClientRect(node, kIframeSelector));
node = node->child_at(0);
}
// Repeat the same process for the page containing OOPIFs. The page is
// structured as b(b(a(c(a(a(a)))))) where the inner-most <iframe> is empty.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
node = web_contents()->GetFrameTree()->root();
WaitForOnLoad(node);
std::vector<gfx::Rect> test_page_bounds_before_scroll = {
GetBoundingClientRect(node, kIframeSelector)};
const GURL iframe_urls[] = {url_b, url_a, url_c, url_a, url_a};
node = node->child_at(0);
for (size_t index = 0; index < kNonEmptyIframesCount; ++index) {
NavigateFrameToURL(node, iframe_urls[index]);
WaitForOnLoad(node);
test_page_bounds_before_scroll.push_back(
GetBoundingClientRect(node, kIframeSelector));
node = node->child_at(0);
}
// Sanity-check: The bounds should match those from non-OOPIF page.
for (size_t index = 0; index < kNonEmptyIframesCount; ++index) {
ASSERT_TRUE(test_page_bounds_before_scroll[index].ApproximatelyEqual(
reference_page_bounds_before_scroll[index],
kRectDimensionErrorTolerance));
}
// Scroll the inner most OOPIF.
ASSERT_TRUE(ExecuteScript(node, kScrollIntoViewScript));
// Now traverse the chain bottom to top and verify the bounds match for each
// <iframe>.
int index = kNonEmptyIframesCount;
RenderFrameHostImpl* current_rfh = node->current_frame_host()->GetParent();
while (current_rfh) {
gfx::Rect current_bounds =
GetBoundingClientRect(current_rfh->frame_tree_node(), kIframeSelector);
gfx::Rect reference_bounds = reference_page_bounds_after_scroll[index];
if (current_bounds.ApproximatelyEqual(reference_bounds,
kRectDimensionErrorTolerance)) {
current_rfh = current_rfh->GetParent();
--index;
} else {
base::RunLoop run_loop;
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
}
}
}
// This test verifies that ScrollFocusedEditableElementIntoView works correctly
// for OOPIFs. Essentially, the test verifies that in a similar setup, the
// resultant page scale factor is the same for OOPIF and non-OOPIF cases. This
// also verifies that in response to the scroll command, the root-layer scrolls
// correctly and the <input> is visible in visual viewport.
#if defined(OS_ANDROID)
// crbug.com/793616
#define MAYBE_ScrollFocusedEditableElementIntoView \
DISABLED_ScrollFocusedEditableElementIntoView
#else
#define MAYBE_ScrollFocusedEditableElementIntoView \
ScrollFocusedEditableElementIntoView
#endif
IN_PROC_BROWSER_TEST_F(SitePerProcessProgrammaticScrollTest,
MAYBE_ScrollFocusedEditableElementIntoView) {
GURL url_a(embedded_test_server()->GetURL("a.com", kIframeOutOfViewHTML));
GURL url_b(embedded_test_server()->GetURL("b.com", kIframeOutOfViewHTML));
#if defined(OS_ANDROID)
// The reason for Android specific code is that
// AutoZoomFocusedNodeToLegibleScale is in blink's WebSettings and difficult
// to access from here. It so happens that the setting is on for Android.
// A lower bound on the ratio of page scale factor after scroll. The actual
// value depends on minReadableCaretHeight / caret_bounds.Height(). The page
// is setup so caret height is quite small so the expected scale should be
// larger than 2.0.
float kLowerBoundOnScaleAfterScroll = 2.0;
float kEpsilon = 0.1;
#endif
ASSERT_TRUE(NavigateToURL(shell(), url_a));
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
WaitForOnLoad(root);
NavigateFrameToURL(root->child_at(0), url_a);
WaitForOnLoad(root->child_at(0));
#if defined(OS_ANDROID)
float scale_before_scroll_nonoopif = GetVisualViewportScale(root);
#endif
AddFocusedInputField(root->child_at(0));
// Focusing <input> causes scrollIntoView(). The following line makes sure
// that the <iframe> is out of view again.
SetWindowScroll(root, 0, 0);
ASSERT_FALSE(GetVisualViewport(root).Intersects(
GetBoundingClientRect(root, kIframeSelector)));
root->child_at(0)
->current_frame_host()
->GetFrameInputHandler()
->ScrollFocusedEditableNodeIntoRect(gfx::Rect());
WaitForElementVisible(root, kIframeSelector);
#if defined(OS_ANDROID)
float scale_after_scroll_nonoopif = GetVisualViewportScale(root);
// Increased scale means zoom triggered correctly.
EXPECT_GT(scale_after_scroll_nonoopif - scale_before_scroll_nonoopif,
kEpsilon);
EXPECT_GT(scale_after_scroll_nonoopif, kLowerBoundOnScaleAfterScroll);
#endif
// Retry the test on an OOPIF page.
Shell* new_shell = CreateBrowser();
ASSERT_TRUE(NavigateToURL(new_shell, url_b));
root = static_cast<WebContentsImpl*>(new_shell->web_contents())
->GetFrameTree()
->root();
WaitForOnLoad(root);
#if defined(OS_ANDROID)
float scale_before_scroll_oopif = GetVisualViewportScale(root);
// Sanity-check:
ASSERT_NEAR(scale_before_scroll_oopif, scale_before_scroll_nonoopif,
kEpsilon);
#endif
NavigateFrameToURL(root->child_at(0), url_a);
WaitForOnLoad(root->child_at(0));
AddFocusedInputField(root->child_at(0));
SetWindowScroll(root, 0, 0);
ASSERT_FALSE(GetVisualViewport(root).Intersects(
GetBoundingClientRect(root, kIframeSelector)));
root->child_at(0)
->current_frame_host()
->GetFrameInputHandler()
->ScrollFocusedEditableNodeIntoRect(gfx::Rect());
WaitForElementVisible(root, kIframeSelector);
#if defined(OS_ANDROID)
float scale_after_scroll_oopif = GetVisualViewportScale(root);
EXPECT_GT(scale_after_scroll_oopif - scale_before_scroll_oopif, kEpsilon);
EXPECT_GT(scale_after_scroll_oopif, kLowerBoundOnScaleAfterScroll);
// The scale is based on the caret height and it should be the same in both
// OOPIF and non-OOPIF pages.
EXPECT_NEAR(scale_after_scroll_oopif, scale_after_scroll_nonoopif, kEpsilon);
#endif
// Make sure the <input> is at least partly visible in the |visualViewport|.
gfx::Rect final_visual_viewport_oopif = GetVisualViewport(root);
gfx::Rect iframe_bounds_after_scroll_oopif =
GetBoundingClientRect(root, kIframeSelector);
gfx::Rect input_bounds_after_scroll_oopif =
GetBoundingClientRect(root->child_at(0), kInputSelector);
input_bounds_after_scroll_oopif +=
iframe_bounds_after_scroll_oopif.OffsetFromOrigin();
ASSERT_TRUE(
final_visual_viewport_oopif.Intersects(input_bounds_after_scroll_oopif));
}
IN_PROC_BROWSER_TEST_F(SitePerProcessProgrammaticScrollTest,
ScrollClippedFocusedEditableElementIntoView) {
GURL url_a(embedded_test_server()->GetURL("a.com", kIframeClippedHTML));
GURL child_url_b(embedded_test_server()->GetURL("b.com", kInputBoxHTML));
ASSERT_TRUE(NavigateToURL(shell(), url_a));
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
WaitForOnLoad(root);
NavigateFrameToURL(root->child_at(0), child_url_b);
WaitForOnLoad(root->child_at(0));
SetWindowScroll(root, 0, 0);
SetWindowScroll(root->child_at(0), 1000, 2000);
float scale_before = GetVisualViewportScale(root);
// The input_box page focuses the input box on load. This call should
// simulate the scroll into view we do when an input box is tapped.
root->child_at(0)
->current_frame_host()
->GetFrameInputHandler()
->ScrollFocusedEditableNodeIntoRect(gfx::Rect());
// The scroll into view is animated on the compositor. Make sure we wait
// until that's completed before testing the rects.
WaitForElementVisible(root, kIframeSelector);
WaitForViewportToStabilize(root);
// These rects are in the coordinate space of the root frame.
gfx::Rect visual_viewport_rect = GetVisualViewport(root);
gfx::Rect window_rect = GetBoundingClientRect(root, ":root");
gfx::Rect iframe_rect = GetBoundingClientRect(root, "iframe");
gfx::Rect clip_rect = GetBoundingClientRect(root, "#clip");
// This is in the coordinate space of the iframe, we'll add the iframe offset
// after to put it into the root frame's coordinate space.
gfx::Rect input_rect = GetBoundingClientRect(root->child_at(0), "input");
// Make sure the input rect is visible in the iframe.
EXPECT_TRUE(gfx::Rect(iframe_rect.size()).Intersects(input_rect))
<< "Input box [" << input_rect.ToString() << "] isn't visible in iframe ["
<< gfx::Rect(iframe_rect.size()).ToString() << "]";
input_rect += iframe_rect.OffsetFromOrigin();
// Make sure the input rect is visible through the clipping layer.
EXPECT_TRUE(clip_rect.Intersects(input_rect))
<< "Input box [" << input_rect.ToString() << "] isn't scrolled into view "
<< "of the clipping layer [" << clip_rect.ToString() << "]";
// And finally, it should be visible in the layout and visual viewports.
EXPECT_TRUE(window_rect.Intersects(input_rect))
<< "Input box [" << input_rect.ToString() << "] isn't visible in the "
<< "layout viewport [" << window_rect.ToString() << "]";
EXPECT_TRUE(visual_viewport_rect.Intersects(input_rect))
<< "Input box [" << input_rect.ToString() << "] isn't visible in the "
<< "visual viewport [" << visual_viewport_rect.ToString() << "]";
float scale_after = GetVisualViewportScale(root);
// Make sure we still zoom in on the input box on platforms that zoom into the
// focused editable.
#if defined(OS_ANDROID)
EXPECT_GT(scale_after, scale_before);
#else
EXPECT_FLOAT_EQ(scale_after, scale_before);
#endif
}
IN_PROC_BROWSER_TEST_F(SitePerProcessProgrammaticScrollTest,
ScrolledOutOfView) {
GURL main_frame(
embedded_test_server()->GetURL("a.com", kIframeOutOfViewHTML));
GURL child_url_b(
embedded_test_server()->GetURL("b.com", kIframeOutOfViewHTML));
// This will set up the page frame tree as A(B()).
ASSERT_TRUE(NavigateToURL(shell(), main_frame));
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
WaitForOnLoad(root);
NavigateFrameToURL(root->child_at(0), child_url_b);
WaitForOnLoad(root->child_at(0));
FrameTreeNode* nested_iframe_node = root->child_at(0);
RenderFrameProxyHost* proxy_to_parent =
nested_iframe_node->render_manager()->GetProxyToParent();
CrossProcessFrameConnector* connector =
proxy_to_parent->cross_process_frame_connector();
while (blink::mojom::FrameVisibility::kRenderedOutOfViewport !=
connector->visibility()) {
base::RunLoop run_loop;
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
}
}
// This test verifies that smooth scrolling works correctly inside nested OOPIFs
// which are same origin with the parent. Note that since the frame tree has
// a A(B(A1())) structure, if and A1 and A2 shared the same
// SmoothScrollSequencer, then this test would time out or at best be flaky with
// random time outs. See https://crbug.com/865446 for more context.
IN_PROC_BROWSER_TEST_F(SitePerProcessProgrammaticScrollTest,
SmoothScrollInNestedSameProcessOOPIF) {
GURL main_frame(
embedded_test_server()->GetURL("a.com", kIframeOutOfViewHTML));
GURL child_url_b(
embedded_test_server()->GetURL("b.com", kIframeOutOfViewHTML));
GURL same_origin(
embedded_test_server()->GetURL("a.com", kIframeOutOfViewHTML));
// This will set up the page frame tree as A(B(A1(A2()))) where A1 is later
// asked to scroll the <iframe> element of A2 into view. The important bit
// here is that the inner frame A1 is recursively scrolling (smoothly) an
// element inside its document into view (A2's origin is irrelevant here).
ASSERT_TRUE(NavigateToURL(shell(), main_frame));
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
WaitForOnLoad(root);
NavigateFrameToURL(root->child_at(0), child_url_b);
WaitForOnLoad(root->child_at(0));
auto* nested_ftn = root->child_at(0)->child_at(0);
NavigateFrameToURL(nested_ftn, same_origin);
WaitForOnLoad(nested_ftn);
// *Smoothly* scroll the inner most frame into view.
ASSERT_TRUE(ExecuteScript(
nested_ftn,
"document.querySelector('iframe').scrollIntoView({behavior: 'smooth'})"));
WaitForElementVisible(root, kIframeSelector);
WaitForElementVisible(root->child_at(0), kIframeSelector);
WaitForElementVisible(nested_ftn, kIframeSelector);
}
// Tests OOPIF rendering by checking that the RWH of the iframe generates
// OnSwapCompositorFrame message.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest, CompositorFrameSwapped) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(baz)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* child_node = root->child_at(0);
GURL site_url(embedded_test_server()->GetURL(
"baz.com", "/cross_site_iframe_factory.html?baz()"));
EXPECT_EQ(site_url, child_node->current_url());
EXPECT_NE(shell()->web_contents()->GetSiteInstance(),
child_node->current_frame_host()->GetSiteInstance());
// Wait for CompositorFrame submission.
RenderFrameSubmissionObserver observer(
child_node->current_frame_host()
->GetRenderWidgetHost()
->render_frame_metadata_provider());
observer.WaitForAnyFrameSubmission();
}
// Ensure that OOPIFs are deleted after navigating to a new main frame.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest, CleanupCrossSiteIframe) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a,a(a,a(a)))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
TestNavigationObserver observer(shell()->web_contents());
// Load a cross-site page into both iframes.
GURL foo_url = embedded_test_server()->GetURL("foo.com", "/title2.html");
NavigateFrameToURL(root->child_at(0), foo_url);
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(foo_url, observer.last_navigation_url());
NavigateFrameToURL(root->child_at(1), foo_url);
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(foo_url, observer.last_navigation_url());
// Ensure that we have created a new process for the subframes.
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" |--Site B ------- proxies for A\n"
" +--Site B ------- proxies for A\n"
"Where A = http://a.com/\n"
" B = http://foo.com/",
DepictFrameTree(root));
int subframe_process_id = root->child_at(0)
->current_frame_host()
->GetSiteInstance()
->GetProcess()
->GetID();
int subframe_rvh_id = root->child_at(0)
->current_frame_host()
->render_view_host()
->GetRoutingID();
EXPECT_TRUE(RenderViewHost::FromID(subframe_process_id, subframe_rvh_id));
// Use Javascript in the parent to remove one of the frames and ensure that
// the subframe goes away.
EXPECT_TRUE(ExecuteScript(shell(),
"document.body.removeChild("
"document.querySelectorAll('iframe')[0])"));
ASSERT_EQ(1U, root->child_count());
// Load a new same-site page in the top-level frame and ensure the other
// subframe goes away.
GURL new_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), new_url));
ASSERT_EQ(0U, root->child_count());
// Ensure the RVH for the subframe gets cleaned up when the frame goes away.
EXPECT_FALSE(RenderViewHost::FromID(subframe_process_id, subframe_rvh_id));
}
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest, NavigateRemoteFrame) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a,a(a,a(a)))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
TestNavigationObserver observer(shell()->web_contents());
// Load same-site page into iframe.
FrameTreeNode* child = root->child_at(0);
GURL http_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
NavigateFrameToURL(child, http_url);
EXPECT_EQ(http_url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
// Load cross-site page into iframe.
GURL url = embedded_test_server()->GetURL("foo.com", "/title2.html");
{
RenderFrameDeletedObserver deleted_observer(child->current_frame_host());
NavigateFrameToURL(root->child_at(0), url);
deleted_observer.WaitUntilDeleted();
}
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(url, observer.last_navigation_url());
// Ensure that we have created a new process for the subframe.
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" |--Site B ------- proxies for A\n"
" +--Site A ------- proxies for B\n"
" |--Site A -- proxies for B\n"
" +--Site A -- proxies for B\n"
" +--Site A -- proxies for B\n"
"Where A = http://a.com/\n"
" B = http://foo.com/",
DepictFrameTree(root));
SiteInstance* site_instance = child->current_frame_host()->GetSiteInstance();
EXPECT_NE(shell()->web_contents()->GetSiteInstance(), site_instance);
// Emulate the main frame changing the src of the iframe such that it
// navigates cross-site.
url = embedded_test_server()->GetURL("bar.com", "/title3.html");
{
RenderFrameDeletedObserver deleted_observer(child->current_frame_host());
NavigateIframeToURL(shell()->web_contents(), "child-0", url);
deleted_observer.WaitUntilDeleted();
}
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(url, observer.last_navigation_url());
// Check again that a new process is created and is different from the
// top level one and the previous one.
EXPECT_EQ(
" Site A ------------ proxies for C\n"
" |--Site C ------- proxies for A\n"
" +--Site A ------- proxies for C\n"
" |--Site A -- proxies for C\n"
" +--Site A -- proxies for C\n"
" +--Site A -- proxies for C\n"
"Where A = http://a.com/\n"
" C = http://bar.com/",
DepictFrameTree(root));
// Navigate back to the parent's origin and ensure we return to the
// parent's process.
{
RenderFrameDeletedObserver deleted_observer(child->current_frame_host());
NavigateFrameToURL(child, http_url);
deleted_observer.WaitUntilDeleted();
}
EXPECT_EQ(http_url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(shell()->web_contents()->GetSiteInstance(),
child->current_frame_host()->GetSiteInstance());
}
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
NavigateRemoteFrameToBlankAndDataURLs) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a,a(a))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
TestNavigationObserver observer(shell()->web_contents());
// Load same-site page into iframe.
FrameTreeNode* child = root->child_at(0);
GURL http_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
NavigateFrameToURL(child, http_url);
EXPECT_EQ(http_url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(
" Site A\n"
" |--Site A\n"
" +--Site A\n"
" +--Site A\n"
"Where A = http://a.com/",
DepictFrameTree(root));
// Load cross-site page into iframe.
GURL url = embedded_test_server()->GetURL("foo.com", "/title2.html");
NavigateFrameToURL(child, url);
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" |--Site B ------- proxies for A\n"
" +--Site A ------- proxies for B\n"
" +--Site A -- proxies for B\n"
"Where A = http://a.com/\n"
" B = http://foo.com/",
DepictFrameTree(root));
// Navigate iframe to a data URL. The navigation happens from a script in the
// parent frame, so the data URL should be committed in the same SiteInstance
// as the parent frame.
RenderFrameDeletedObserver deleted_observer1(
root->child_at(0)->current_frame_host());
GURL data_url("data:text/html,dataurl");
NavigateIframeToURL(shell()->web_contents(), "child-0", data_url);
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(data_url, observer.last_navigation_url());
// Wait for the old process to exit, to verify that the proxies go away.
deleted_observer1.WaitUntilDeleted();
// Ensure that we have navigated using the top level process.
EXPECT_EQ(
" Site A\n"
" |--Site A\n"
" +--Site A\n"
" +--Site A\n"
"Where A = http://a.com/",
DepictFrameTree(root));
// Load cross-site page into iframe.
url = embedded_test_server()->GetURL("bar.com", "/title2.html");
NavigateFrameToURL(child, url);
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_EQ(
" Site A ------------ proxies for C\n"
" |--Site C ------- proxies for A\n"
" +--Site A ------- proxies for C\n"
" +--Site A -- proxies for C\n"
"Where A = http://a.com/\n"
" C = http://bar.com/",
DepictFrameTree(root));
// Navigate iframe to about:blank. The navigation happens from a script in the
// parent frame, so it should be committed in the same SiteInstance as the
// parent frame.
RenderFrameDeletedObserver deleted_observer2(
root->child_at(0)->current_frame_host());
GURL about_blank_url("about:blank#foo");
NavigateIframeToURL(shell()->web_contents(), "child-0", about_blank_url);
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(about_blank_url, observer.last_navigation_url());
// Wait for the old process to exit, to verify that the proxies go away.
deleted_observer2.WaitUntilDeleted();
// Ensure that we have navigated using the top level process.
EXPECT_EQ(
" Site A\n"
" |--Site A\n"
" +--Site A\n"
" +--Site A\n"
"Where A = http://a.com/",
DepictFrameTree(root));
// Load cross-site page into iframe again.
url = embedded_test_server()->GetURL("f00.com", "/title3.html");
NavigateFrameToURL(child, url);
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_EQ(
" Site A ------------ proxies for D\n"
" |--Site D ------- proxies for A\n"
" +--Site A ------- proxies for D\n"
" +--Site A -- proxies for D\n"
"Where A = http://a.com/\n"
" D = http://f00.com/",
DepictFrameTree(root));
// Navigate the iframe itself to about:blank using a script executing in its
// own context. It should stay in the same SiteInstance as before, not the
// parent one.
TestFrameNavigationObserver frame_observer(child);
ExecuteScriptAsync(child, "window.location.href = 'about:blank#foo';");
frame_observer.Wait();
EXPECT_EQ(about_blank_url, child->current_url());
// Ensure that we have navigated using the top level process.
EXPECT_EQ(
" Site A ------------ proxies for D\n"
" |--Site D ------- proxies for A\n"
" +--Site A ------- proxies for D\n"
" +--Site A -- proxies for D\n"
"Where A = http://a.com/\n"
" D = http://f00.com/",
DepictFrameTree(root));
}
// This test checks that killing a renderer process of a remote frame
// and then navigating some other frame to the same SiteInstance of the killed
// process works properly.
// This can be illustrated as follows,
// where 1/2/3 are FrameTreeNode-s and A/B are processes and B* is the killed
// B process:
//
// 1 A A A
// / \ -> / \ -> Kill B -> / \ -> Navigate 3 to B -> / \ .
// 2 3 B A B* A B* B
//
// Initially, node1.proxy_hosts_ = {B}
// After we kill B, we make sure B stays in node1.proxy_hosts_, then we navigate
// 3 to B and we expect that to complete normally.
// See http://crbug.com/432107.
//
// Note that due to http://crbug.com/450681, node2 cannot be re-navigated to
// site B and stays in not rendered state.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
NavigateRemoteFrameToKilledProcess) {
GURL main_url(embedded_test_server()->GetURL(
"foo.com", "/cross_site_iframe_factory.html?foo.com(bar.com, foo.com)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = web_contents()->GetFrameTree()->root();
TestNavigationObserver observer(shell()->web_contents());
ASSERT_EQ(2U, root->child_count());
// Make sure node2 points to the correct cross-site page.
GURL site_b_url = embedded_test_server()->GetURL(