blob: a2b96ce6ec50d71f88502739657550497d030c1a [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// 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 "base/json/json_reader.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "cc/base/math_util.h"
#include "content/browser/renderer_host/cross_process_frame_connector.h"
#include "content/browser/renderer_host/input/synthetic_touchscreen_pinch_gesture.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_child_frame.h"
#include "content/common/input/actions_parser.h"
#include "content/public/browser/render_process_host_priority_client.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.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/test_frame_navigation_observer.h"
#include "content/test/render_document_feature.h"
#include "content/test/render_widget_host_visibility_observer.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#if defined(USE_AURA)
#include "ui/aura/window_tree_host.h"
#endif
#if BUILDFLAG(IS_MAC)
#include "content/browser/renderer_host/input/synthetic_touchpad_pinch_gesture.h"
#include "ui/base/test/scoped_preferred_scroller_style_mac.h"
#endif
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ui/aura/test/test_screen.h"
#endif
namespace content {
namespace {
double GetFrameDeviceScaleFactor(const ToRenderFrameHost& adapter) {
return EvalJs(adapter, "window.devicePixelRatio;").ExtractDouble();
}
// 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(
const ToRenderFrameHost& execution_target) {
static const char kRafScript[] = R"(
let width = window.innerWidth;
let height = window.innerHeight * 0.75;
for (let i = 0; i < window.frames.length; i++) {
let child = document.getElementById("child-" + i);
child.width = width;
child.height = height;
}
)";
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(execution_target, kRafScript, "")
.error.empty());
}
// Check |intersects_viewport| on widget and process.
bool CheckIntersectsViewport(bool expected, FrameTreeNode* node) {
RenderProcessHostPriorityClient::Priority priority =
node->current_frame_host()->GetRenderWidgetHost()->GetPriority();
return priority.intersects_viewport == expected &&
node->current_frame_host()->GetProcess()->GetIntersectsViewport() ==
expected;
}
// 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::Type::kMouseDown,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
mouse_event.button = blink::WebPointerProperties::Button::kLeft;
mouse_event.SetPositionInWidget(x, y);
rwh->ForwardMouseEvent(mouse_event);
}
} // namespace
// Class to monitor incoming UpdateViewportIntersection messages. The caller has
// to guarantee that `rfph` lives at least as long as
// UpdateViewportIntersectionMessageFilter.
class UpdateViewportIntersectionMessageFilter
: public blink::mojom::RemoteFrameHostInterceptorForTesting {
public:
explicit UpdateViewportIntersectionMessageFilter(
content::RenderFrameProxyHost* rfph)
: intersection_state_(blink::mojom::ViewportIntersectionState::New()),
render_frame_proxy_host_(rfph),
swapped_impl_(
render_frame_proxy_host_->frame_host_receiver_for_testing(),
this) {}
~UpdateViewportIntersectionMessageFilter() override = default;
const blink::mojom::ViewportIntersectionStatePtr& GetIntersectionState()
const {
return intersection_state_;
}
RenderFrameProxyHost* GetForwardingInterface() override {
return render_frame_proxy_host_;
}
void UpdateViewportIntersection(
blink::mojom::ViewportIntersectionStatePtr intersection_state,
const absl::optional<blink::FrameVisualProperties>& visual_properties)
override {
intersection_state_ = std::move(intersection_state);
msg_received_ = true;
if (run_loop_)
run_loop_->Quit();
}
bool MessageReceived() const { return msg_received_; }
void Clear() {
msg_received_ = false;
intersection_state_ = blink::mojom::ViewportIntersectionState::New();
}
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:
raw_ptr<base::RunLoop> run_loop_ = nullptr;
bool msg_received_;
blink::mojom::ViewportIntersectionStatePtr intersection_state_;
raw_ptr<content::RenderFrameProxyHost> render_frame_proxy_host_;
mojo::test::ScopedSwapImplForTesting<
mojo::AssociatedReceiver<blink::mojom::RemoteFrameHost>>
swapped_impl_;
};
// TODO(tonikitoo): Move to fake_remote_frame.h|cc in case it is useful
// for other tests.
class FakeRemoteMainFrame : public blink::mojom::RemoteMainFrame {
public:
FakeRemoteMainFrame() = default;
~FakeRemoteMainFrame() override = default;
void Init(
mojo::PendingAssociatedReceiver<blink::mojom::RemoteMainFrame> receiver) {
receiver_.Bind(std::move(receiver));
}
// blink::mojom::RemoteMainFrame overrides:
void UpdateTextAutosizerPageInfo(
blink::mojom::TextAutosizerPageInfoPtr page_info) override {}
private:
mojo::AssociatedReceiver<blink::mojom::RemoteMainFrame> receiver_{this};
};
// This class intercepts RenderFrameProxyHost creations, and overrides their
// respective blink::mojom::RemoteMainFrame instances, so that it can watch for
// text autosizer page info updates.
class UpdateTextAutosizerInfoProxyObserver
: public RenderFrameProxyHost::TestObserver {
public:
UpdateTextAutosizerInfoProxyObserver() {
RenderFrameProxyHost::SetObserverForTesting(this);
}
~UpdateTextAutosizerInfoProxyObserver() override {
RenderFrameProxyHost::SetObserverForTesting(nullptr);
}
const blink::mojom::TextAutosizerPageInfo& TextAutosizerPageInfo(
RenderFrameProxyHost* proxy) {
return remote_frames_[proxy]->page_info();
}
private:
class Remote : public FakeRemoteMainFrame {
public:
explicit Remote(RenderFrameProxyHost* proxy) {
Init(proxy->BindRemoteMainFrameReceiverForTesting());
}
void UpdateTextAutosizerPageInfo(
blink::mojom::TextAutosizerPageInfoPtr page_info) override {
page_info_ = *page_info;
}
const blink::mojom::TextAutosizerPageInfo& page_info() {
return page_info_;
}
private:
blink::mojom::TextAutosizerPageInfo page_info_;
};
void OnRemoteMainFrameBound(RenderFrameProxyHost* proxy_host) override {
remote_frames_[proxy_host] = std::make_unique<Remote>(proxy_host);
}
std::map<RenderFrameProxyHost*, std::unique_ptr<Remote>> remote_frames_;
};
// Class to intercept incoming TextAutosizerPageInfoChanged messages. The caller
// has to guarantee that `render_frame_host` lives at least as long as
// TextAutosizerPageInfoInterceptor.
class TextAutosizerPageInfoInterceptor
: public blink::mojom::LocalMainFrameHostInterceptorForTesting {
public:
explicit TextAutosizerPageInfoInterceptor(
RenderFrameHostImpl* render_frame_host)
: render_frame_host_(render_frame_host),
swapped_impl_(
render_frame_host_->local_main_frame_host_receiver_for_testing(),
this) {}
~TextAutosizerPageInfoInterceptor() override = default;
LocalMainFrameHost* GetForwardingInterface() override {
return render_frame_host_;
}
void WaitForPageInfo(absl::optional<int> target_main_frame_width,
absl::optional<float> target_device_scale_adjustment) {
if (remote_page_info_seen_)
return;
target_main_frame_width_ = target_main_frame_width;
target_device_scale_adjustment_ = target_device_scale_adjustment;
run_loop_ = std::make_unique<base::RunLoop>();
run_loop_->Run();
run_loop_.reset();
}
const blink::mojom::TextAutosizerPageInfo& GetTextAutosizerPageInfo() {
return *remote_page_info_;
}
void TextAutosizerPageInfoChanged(
blink::mojom::TextAutosizerPageInfoPtr remote_page_info) override {
if ((!target_main_frame_width_ ||
remote_page_info->main_frame_width != target_main_frame_width_) &&
(!target_device_scale_adjustment_ ||
remote_page_info->device_scale_adjustment !=
target_device_scale_adjustment_)) {
return;
}
remote_page_info_ = remote_page_info.Clone();
remote_page_info_seen_ = true;
if (run_loop_)
run_loop_->Quit();
GetForwardingInterface()->TextAutosizerPageInfoChanged(
std::move(remote_page_info));
}
private:
raw_ptr<RenderFrameHostImpl> render_frame_host_;
bool remote_page_info_seen_ = false;
blink::mojom::TextAutosizerPageInfoPtr remote_page_info_ =
blink::mojom::TextAutosizerPageInfo::New(/*main_frame_width=*/0,
/*main_frame_layout_width=*/0,
/*device_scale_adjustment=*/1.f);
std::unique_ptr<base::RunLoop> run_loop_;
absl::optional<int> target_main_frame_width_;
absl::optional<float> target_device_scale_adjustment_;
mojo::test::ScopedSwapImplForTesting<
mojo::AssociatedReceiver<blink::mojom::LocalMainFrameHost>>
swapped_impl_;
};
class SitePerProcessHighDPIBrowserTest : public SitePerProcessBrowserTest {
public:
const double kDeviceScaleFactor = 2.0;
SitePerProcessHighDPIBrowserTest() = default;
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
SitePerProcessBrowserTestBase::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(
switches::kForceDeviceScaleFactor,
base::StringPrintf("%f", kDeviceScaleFactor));
}
};
IN_PROC_BROWSER_TEST_P(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 BUILDFLAG(IS_ANDROID)
GetFrameDeviceScaleFactor(web_contents());
#else
SitePerProcessHighDPIBrowserTest::kDeviceScaleFactor;
#endif
EXPECT_EQ(expected_dip_scale, GetFrameDeviceScaleFactor(web_contents()));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
EXPECT_EQ(expected_dip_scale, GetFrameDeviceScaleFactor(root));
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* child = root->child_at(0);
EXPECT_EQ(expected_dip_scale, GetFrameDeviceScaleFactor(child));
}
class SitePerProcessCompositorViewportBrowserTest
: public SitePerProcessBrowserTestBase,
public testing::WithParamInterface<double> {
public:
SitePerProcessCompositorViewportBrowserTest() = default;
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
SitePerProcessBrowserTestBase::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(switches::kForceDeviceScaleFactor,
base::StringPrintf("%f", GetParam()));
}
};
// DISABLED: crbug.com/1071995
IN_PROC_BROWSER_TEST_P(SitePerProcessCompositorViewportBrowserTest,
DISABLED_OopifCompositorViewportSizeRelativeToParent) {
// Load page with very tall OOPIF.
GURL main_url(
embedded_test_server()->GetURL("a.com", "/super_tall_parent.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())
->GetPrimaryFrameTree()
.root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* child = root->child_at(0);
GURL nested_site_url(
embedded_test_server()->GetURL("b.com", "/super_tall_page.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(child, nested_site_url));
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));
// Observe frame submission from parent.
RenderFrameSubmissionObserver parent_observer(
root->current_frame_host()
->GetRenderWidgetHost()
->render_frame_metadata_provider());
parent_observer.WaitForAnyFrameSubmission();
gfx::Size parent_viewport_size =
parent_observer.LastRenderFrameMetadata().viewport_size_in_pixels;
// Observe frame submission from child.
RenderFrameSubmissionObserver child_observer(
child->current_frame_host()
->GetRenderWidgetHost()
->render_frame_metadata_provider());
child_observer.WaitForAnyFrameSubmission();
gfx::Size child_viewport_size =
child_observer.LastRenderFrameMetadata().viewport_size_in_pixels;
// Verify child's compositor viewport is no more than about 30% larger than
// the parent's. See RemoteFrameView::GetCompositingRect() for explanation of
// the choice of 30%. Add +1 to child viewport height to account for rounding.
EXPECT_GE(ceilf(1.3f * parent_viewport_size.height()),
child_viewport_size.height() - 1);
// Verify the child's ViewBounds are much larger.
RenderWidgetHostViewBase* child_rwhv = static_cast<RenderWidgetHostViewBase*>(
child->current_frame_host()->GetRenderWidgetHost()->GetView());
// 30,000 is based on div/iframe sizes in the test HTML files.
EXPECT_LT(30000, child_rwhv->GetViewBounds().height());
}
#if BUILDFLAG(IS_ANDROID)
// Android doesn't support forcing device scale factor in tests.
INSTANTIATE_TEST_SUITE_P(SitePerProcess,
SitePerProcessCompositorViewportBrowserTest,
testing::Values(1.0));
#else
INSTANTIATE_TEST_SUITE_P(SitePerProcess,
SitePerProcessCompositorViewportBrowserTest,
testing::Values(1.0, 1.5, 2.0));
#endif
#if BUILDFLAG(IS_CHROMEOS_ASH)
IN_PROC_BROWSER_TEST_P(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()->GetPrimaryFrameTree().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());
// Wait until dppx becomes 2 if the frame's dpr hasn't beeen updated
// to 2 yet.
const char kScript[] = R"(
new Promise(resolve => {
if (window.devicePixelRatio == 2)
resolve(window.devicePixelRatio);
window.matchMedia('screen and (min-resolution: 2dppx)')
.addListener(function(e) {
if (e.matches) {
resolve(window.devicePixelRatio);
}
});
});
)";
// Make sure that both main frame and iframe are updated to 2x.
EXPECT_EQ(expected_dip_scale, EvalJs(child, kScript).ExtractDouble());
EXPECT_EQ(expected_dip_scale,
EvalJs(web_contents(), kScript).ExtractDouble());
}
#endif
// Tests that when a large OOPIF has been scaled, the compositor raster area
// sent from the embedder is correct.
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_MAC)
// Temporarily disabled on Android because this doesn't account for browser
// control height or page scale factor.
// Flaky on Mac. https://crbug.com/840314
#define MAYBE_ScaledIframeRasterSize DISABLED_ScaledframeRasterSize
#else
#define MAYBE_ScaledIframeRasterSize ScaledIframeRasterSize
#endif
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
MAYBE_ScaledIframeRasterSize) {
GURL http_url(embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_scaled_large_frame.html"));
EXPECT_TRUE(NavigateToURL(shell(), http_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
FrameTreeNode* child = root->child_at(0);
RenderFrameProxyHost* child_proxy =
child->render_manager()->GetProxyToParent();
auto filter =
std::make_unique<UpdateViewportIntersectionMessageFilter>(child_proxy);
// Force a lifecycle update and wait for it to finish; by the time this call
// returns, the viewport intersection IPC should already have been received
// by the browser process and handled by the filter.
EvalJsResult eval_result = EvalJsAfterLifecycleUpdate(
root->current_frame_host(),
"document.getElementsByTagName('div')[0].scrollTo(0, 5000);",
"document.getElementsByTagName('div')[0].getBoundingClientRect().top;");
ASSERT_TRUE(eval_result.error.empty());
int div_offset_top = eval_result.ExtractInt();
gfx::Rect compositing_rect =
filter->GetIntersectionState()->compositor_visible_rect;
float device_scale_factor = 1.0f;
device_scale_factor = GetFrameDeviceScaleFactor(shell()->web_contents());
// The math below replicates the calculations in
// RemoteFrameView::GetCompositingRect(). That could be subject to tweaking,
// which would have to be reflected in these test expectations. Also, any
// changes to Blink that would affect the size of the frame rect or the
// visible viewport would need to be accounted for.
// The multiplication by 5 accounts for the 0.2 scale factor in the test,
// which increases the area that has to be drawn in the OOPIF.
int view_height = root->current_frame_host()
->GetRenderWidgetHost()
->GetView()
->GetViewBounds()
.height() *
5 * device_scale_factor;
// The raster size is expanded by a factor of 1.3 to allow for some scrolling
// without requiring re-raster. The expanded area to be rasterized should be
// centered around the iframe's visible area within the parent document, hence
// the expansion in each direction (top, bottom, left, right) is
// (0.15 * viewport dimension).
int expansion = ceilf(view_height * 0.15f);
int expected_height = view_height + expansion * 2;
// 5000 = div scroll offset in scaled pixels
// 5 = scale factor from top-level document to iframe contents
// 2 = iframe border in scaled pixels
int expected_offset =
((5000 - (div_offset_top * 5) - 2) * device_scale_factor) - expansion;
// Allow a small amount for rounding differences from applying page and
// device scale factors at different times.
float tolerance = ceilf(device_scale_factor);
EXPECT_NEAR(compositing_rect.height(), expected_height, tolerance);
EXPECT_NEAR(compositing_rect.y(), expected_offset, tolerance);
}
// Similar to ScaledIFrameRasterSize but with nested OOPIFs to ensure
// propagation works correctly.
#if BUILDFLAG(IS_ANDROID)
// Temporarily disabled on Android because this doesn't account for browser
// control height or page scale factor.
#define MAYBE_ScaledNestedIframeRasterSize DISABLED_ScaledNestedIframeRasterSize
#else
#define MAYBE_ScaledNestedIframeRasterSize ScaledNestedIframeRasterSize
#endif
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
MAYBE_ScaledNestedIframeRasterSize) {
GURL http_url(embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_scaled_large_frames_nested.html"));
EXPECT_TRUE(NavigateToURL(shell(), http_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
FrameTreeNode* child_b = root->child_at(0);
EXPECT_TRUE(NavigateToURLFromRenderer(
child_b,
embedded_test_server()->GetURL(
"bar.com", "/frame_tree/page_with_large_scrollable_frame.html")));
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://bar.com/\n"
" C = http://baz.com/",
DepictFrameTree(root));
// This adds the filter to the immediate child iframe. It verifies that the
// child sets the nested iframe's compositing rect correctly.
FrameTreeNode* child_c = child_b->child_at(0);
RenderFrameProxyHost* child_c_proxy =
child_c->render_manager()->GetProxyToParent();
auto filter =
std::make_unique<UpdateViewportIntersectionMessageFilter>(child_c_proxy);
// Scroll the child frame so that it is partially clipped. This will cause the
// top 10 pixels of the child frame to be clipped. Applying the scale factor
// means that in the coordinate system of the subframes, 50px are clipped.
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(root->current_frame_host(),
"window.scrollBy(0, 10)", "")
.error.empty());
// This scrolls the div containing in the 'Site B' iframe that contains the
// 'Site C' iframe, and then we verify that the 'Site C' frame receives the
// correct compositor frame. Force a lifecycle update after the scroll and
// wait for it to finish; by the time this call returns, the viewport
// intersection IPC should already have been received by the browser process
// and handled by the filter. Extract the page offset of the leaf iframe
// within the middle document.
EvalJsResult child_eval_result = EvalJsAfterLifecycleUpdate(
child_b->current_frame_host(),
"document.getElementsByTagName('div')[0].scrollTo(0, 5000);",
"document.getElementsByTagName('div')[0].getBoundingClientRect().top;");
ASSERT_TRUE(child_eval_result.error.empty());
int child_div_offset_top = child_eval_result.ExtractInt();
gfx::Rect compositing_rect =
filter->GetIntersectionState()->compositor_visible_rect;
float scale_factor = 1.0f;
scale_factor = GetFrameDeviceScaleFactor(shell()->web_contents());
// See comment in ScaledIframeRasterSize for explanation of this. In this
// case, the raster area of the large iframe should be restricted to
// approximately the area of its containing frame which is unclipped by the
// main frame. The containing frame is clipped by 50 pixels at the top, due
// to the scroll offset of the main frame, so we subtract that from the full
// height of the containing frame.
int view_height = (child_b->current_frame_host()
->GetRenderWidgetHost()
->GetView()
->GetViewBounds()
.height() -
50) *
scale_factor;
// 30% padding is added to the view_height to prevent frequent re-rasters.
// The extra padding is centered around the view height, hence expansion by
// 0.15 in each direction.
int expansion = ceilf(view_height * 0.15f);
int expected_height = view_height + expansion * 2;
// Explanation of terms:
// 5000 = offset from top of nested iframe to top of containing div, due to
// scroll offset of div
// child_div_offset_top = offset of containing div from top of child frame
// 50 = offset of child frame's intersection with the top document viewport
// from the top of the child frame (i.e, clipped amount at top of child)
// view_height * 0.15 = padding added to the top of the compositing rect
// (half the the 30% total padding)
int expected_offset =
5000 - ((child_div_offset_top - 50) * scale_factor) - expansion;
// Allow a small amount for rounding differences from applying page and
// device scale factors at different times.
EXPECT_NEAR(compositing_rect.height(), expected_height, ceilf(scale_factor));
EXPECT_NEAR(compositing_rect.y(), expected_offset, ceilf(scale_factor));
}
// Tests that when an OOPIF is inside a multicolumn container, its compositing
// rect is set correctly.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
IframeInMulticolCompositingRect) {
GURL http_url(embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_iframe_in_multicol.html"));
EXPECT_TRUE(NavigateToURL(shell(), http_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
FrameTreeNode* child = root->child_at(0);
RenderFrameProxyHost* child_proxy =
child->render_manager()->GetProxyToParent();
auto filter =
std::make_unique<UpdateViewportIntersectionMessageFilter>(child_proxy);
// Force a lifecycle update and wait for it to finish. Changing the width of
// the iframe should cause the parent renderer to propagate a new
// ViewportIntersectionState while running the rendering pipeline. By the time
// this call returns, the viewport intersection IPC should already have been
// received by the browser process and handled by the filter.
EvalJsResult eval_result = EvalJsAfterLifecycleUpdate(
root->current_frame_host(),
"document.querySelector('iframe').style.width = '250px'", "");
ASSERT_TRUE(filter->MessageReceived());
gfx::Rect compositing_rect =
filter->GetIntersectionState()->compositor_visible_rect;
float scale_factor = 1.0f;
scale_factor = GetFrameDeviceScaleFactor(shell()->web_contents());
gfx::Point visible_offset(0, 0);
gfx::Size visible_size =
gfx::ScaleToFlooredSize(gfx::Size(250, 150), scale_factor, scale_factor);
gfx::Rect visible_rect(visible_offset, visible_size);
float tolerance = ceilf(scale_factor);
EXPECT_NEAR(compositing_rect.x(), visible_rect.x(), tolerance);
EXPECT_NEAR(compositing_rect.y(), visible_rect.y(), tolerance);
EXPECT_NEAR(compositing_rect.width(), visible_rect.width(), tolerance);
EXPECT_NEAR(compositing_rect.height(), visible_rect.height(), tolerance);
EXPECT_TRUE(compositing_rect.Contains(visible_rect));
}
// Flaky on multiple platforms (crbug.com/1094562).
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
DISABLED_FrameViewportIntersectionTestSimple) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c),d,e(f))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
RenderFrameProxyHost* child2_proxy =
root->child_at(2)->render_manager()->GetProxyToParent();
auto child2_filter =
std::make_unique<UpdateViewportIntersectionMessageFilter>(child2_proxy);
// Force lifecycle update in root and child2 to make sure child2 has sent
// viewport intersection into to grand child before child2 becomes throttled.
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(root->current_frame_host(), "", "")
.error.empty());
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(
root->child_at(2)->current_frame_host(), "", "")
.error.empty());
child2_filter->Clear();
LayoutNonRecursiveForTestingViewportIntersection(shell()->web_contents());
// Root should always intersect.
EXPECT_TRUE(CheckIntersectsViewport(true, root));
// Child 0 should be entirely in viewport.
EXPECT_TRUE(CheckIntersectsViewport(true, root->child_at(0)));
// Make sure child0 has has a chance to propagate viewport intersection to
// grand child.
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(
root->child_at(0)->current_frame_host(), "", "")
.error.empty());
// Grand child should match parent.
EXPECT_TRUE(CheckIntersectsViewport(true, root->child_at(0)->child_at(0)));
// Child 1 should be partially in viewport.
EXPECT_TRUE(CheckIntersectsViewport(true, root->child_at(1)));
// Child 2 should be not be in viewport.
EXPECT_TRUE(CheckIntersectsViewport(false, root->child_at(2)));
// Can't use EvalJsAfterLifecycleUpdate on child2, because it's
// render-throttled. But it should still have propagated state down to the
// grandchild.
child2_filter->Wait();
// Grand child should match parent.
EXPECT_TRUE(CheckIntersectsViewport(false, root->child_at(2)->child_at(0)));
}
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
FrameViewportOffsetTestSimple) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// This will catch b sending viewport intersection information to c.
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
RenderFrameProxyHost* iframe_c_proxy =
root->child_at(0)->child_at(0)->render_manager()->GetProxyToParent();
auto filter =
std::make_unique<UpdateViewportIntersectionMessageFilter>(iframe_c_proxy);
// Use EvalJsAfterLifecycleUpdate to force animation frames in `a` and `b` to
// ensure that the viewport intersection for initial layout state has been
// propagated. The layout of `a` will not change again, so we can read back
// its layout info after the animation frame. The layout of `b` will change,
// so we don't read back its layout yet.
std::string script(R"(
let iframe = document.querySelector("iframe");
[iframe.offsetLeft, iframe.offsetTop];
)");
EvalJsResult iframe_b_result =
EvalJsAfterLifecycleUpdate(root->current_frame_host(), "", script);
base::Value iframe_b_offset = iframe_b_result.ExtractList();
int iframe_b_offset_left = iframe_b_offset.GetList()[0].GetInt();
int iframe_b_offset_top = iframe_b_offset.GetList()[1].GetInt();
// Make sure a new IPC is sent after dirty-ing layout.
filter->Clear();
// Dirty layout in `b` to generate a new IPC to `c`. This will be the final
// layout state for `b`, so read back layout info here.
std::string raf_script(R"(
let iframe = document.querySelector("iframe");
let margin = getComputedStyle(iframe).marginTop.replace("px", "");
iframe.style.margin = String(parseInt(margin) + 1) + "px";
)");
EvalJsResult iframe_c_result = EvalJsAfterLifecycleUpdate(
root->child_at(0)->current_frame_host(), raf_script, script);
base::Value iframe_c_offset = iframe_c_result.ExtractList();
int iframe_c_offset_left = iframe_c_offset.GetList()[0].GetInt();
int iframe_c_offset_top = iframe_c_offset.GetList()[1].GetInt();
// The IPC should already have been sent
EXPECT_TRUE(filter->MessageReceived());
// +4 for a 2px border on each iframe.
gfx::Vector2dF expected(iframe_b_offset_left + iframe_c_offset_left + 4,
iframe_b_offset_top + iframe_c_offset_top + 4);
const float device_scale_factor =
root->render_manager()->GetRenderWidgetHostView()->GetDeviceScaleFactor();
// Convert from CSS to physical pixels
expected.Scale(device_scale_factor);
gfx::Transform actual = filter->GetIntersectionState()->main_frame_transform;
const absl::optional<gfx::PointF> viewport_offset_source_point =
actual.InverseMapPoint(gfx::PointF());
ASSERT_TRUE(viewport_offset_source_point.has_value());
const gfx::Vector2dF viewport_offset =
gfx::PointF() - viewport_offset_source_point.value();
float tolerance = ceilf(device_scale_factor);
EXPECT_NEAR(expected.x(), viewport_offset.x(), tolerance);
EXPECT_NEAR(expected.y(), viewport_offset.y(), tolerance);
}
// TODO(crbug.com/1168036): Flaky test.
IN_PROC_BROWSER_TEST_P(
SitePerProcessBrowserTest,
DISABLED_NestedIframeTransformedIntoViewViewportIntersection) {
GURL http_url(embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_frame_transformed_into_viewport.html"));
EXPECT_TRUE(NavigateToURL(shell(), http_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
FrameTreeNode* child_b = root->child_at(0);
EXPECT_TRUE(NavigateToURLFromRenderer(
child_b,
embedded_test_server()->GetURL(
"bar.com", "/frame_tree/page_with_cross_origin_frame_at_half.html")));
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://bar.com/\n"
" C = http://baz.com/",
DepictFrameTree(root));
FrameTreeNode* child_c = child_b->child_at(0);
RenderFrameProxyHost* child_c_proxy =
child_c->render_manager()->GetProxyToParent();
auto filter =
std::make_unique<UpdateViewportIntersectionMessageFilter>(child_c_proxy);
// Scroll the div containing the 'Site B' iframe to trigger a viewport
// intersection update.
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(
child_b->current_frame_host(),
"document.getElementsByTagName('div')[0].scrollTo(0, 5000);",
"")
.error.empty());
ASSERT_TRUE(filter->MessageReceived());
// Check that we currently intersect with the viewport.
gfx::Rect viewport_intersection =
filter->GetIntersectionState()->viewport_intersection;
EXPECT_GT(viewport_intersection.height(), 0);
EXPECT_GT(viewport_intersection.width(), 0);
}
// Verify that OOPIF select element popup menu coordinates account for scroll
// offset in containers embedding frame.
// TODO(crbug.com/859552): Reenable this.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
DISABLED_PopupMenuInTallIframeTest) {
GURL main_url(embedded_test_server()->GetURL(
"/frame_tree/page_with_tall_positioned_frame.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
FrameTreeNode* child_node = root->child_at(0);
GURL site_url(embedded_test_server()->GetURL(
"baz.com", "/site_isolation/page-with-select.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(child_node, site_url));
RenderFrameProxyHost* root_proxy = root->render_manager()->GetProxyToParent();
auto filter =
std::make_unique<UpdateViewportIntersectionMessageFilter>(root_proxy);
// Position the select element so that it is out of the viewport, then scroll
// it into view.
EXPECT_TRUE(ExecJs(child_node,
"document.querySelector('select').style.top='2000px';"));
EXPECT_TRUE(ExecJs(root, "window.scrollTo(0, 1900);"));
// Wait for a viewport intersection update to be dispatched to the child, and
// ensure it is processed by the browser before continuing.
filter->Wait();
{
// This yields the UI thread in order to ensure that the new viewport
// intersection is sent to the to child renderer before the mouse click
// below.
base::RunLoop loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, loop.QuitClosure());
loop.Run();
}
auto show_popup_waiter = std::make_unique<ShowPopupWidgetWaiter>(
web_contents(), child_node->current_frame_host());
SimulateMouseClick(child_node->current_frame_host()->GetRenderWidgetHost(),
55, 2005);
// Dismiss the popup.
SimulateMouseClick(child_node->current_frame_host()->GetRenderWidgetHost(), 1,
1);
// The test passes if this wait returns, indicating that the popup was
// scrolled into view and the OOPIF renderer displayed it. Other tests verify
// the correctness of popup menu coordinates.
show_popup_waiter->Wait();
}
// Test to verify that viewport intersection is propagated to nested OOPIFs
// even when a parent OOPIF has been throttled.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
NestedFrameViewportIntersectionUpdated) {
GURL main_url(embedded_test_server()->GetURL(
"foo.com", "/frame_tree/scrollable_page_with_positioned_frame.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
FrameTreeNode* child_node = root->child_at(0);
GURL site_url(embedded_test_server()->GetURL(
"bar.com", "/frame_tree/page_with_positioned_frame.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(child_node, 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://foo.com/\n"
" B = http://bar.com/\n"
" C = http://baz.com/",
DepictFrameTree(root));
// This will intercept messages sent from B to C, describing C's viewport
// intersection.
RenderFrameProxyHost* child_proxy =
child_node->render_manager()->GetProxyToParent();
auto filter =
std::make_unique<UpdateViewportIntersectionMessageFilter>(child_proxy);
// Run requestAnimationFrame in A and B to make sure initial layout has
// completed and initial IPCs sent.
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(root->current_frame_host(), "", "")
.error.empty());
ASSERT_TRUE(
EvalJsAfterLifecycleUpdate(child_node->current_frame_host(), "", "")
.error.empty());
filter->Clear();
// Scroll the child frame out of view, causing it to become throttled.
ASSERT_TRUE(ExecJs(root->current_frame_host(), "window.scrollTo(0, 5000)"));
filter->Wait();
EXPECT_TRUE(filter->GetIntersectionState()->viewport_intersection.IsEmpty());
// Scroll the frame back into view.
ASSERT_TRUE(ExecJs(root->current_frame_host(), "window.scrollTo(0, 0)"));
filter->Wait();
EXPECT_FALSE(filter->GetIntersectionState()->viewport_intersection.IsEmpty());
}
// Test to verify that the main frame document intersection
// is propagated to out of process iframes by scrolling a nested iframe
// in and out of intersecting with the main frame document.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
NestedFrameMainFrameDocumentIntersectionUpdated) {
GURL main_url(embedded_test_server()->GetURL(
"foo.com", "/frame_tree/scrollable_page_with_positioned_frame.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
FrameTreeNode* child_node_b = root->child_at(0);
GURL site_url(embedded_test_server()->GetURL(
"bar.com", "/frame_tree/scrollable_page_with_positioned_frame.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(child_node_b, 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://foo.com/\n"
" B = http://bar.com/\n"
" C = http://baz.com/",
DepictFrameTree(root));
FrameTreeNode* child_node_c = child_node_b->child_at(0);
RenderFrameProxyHost* child_proxy_c =
child_node_c->render_manager()->GetProxyToParent();
auto filter =
std::make_unique<UpdateViewportIntersectionMessageFilter>(child_proxy_c);
// Run requestAnimationFrame in A and B to make sure initial layout has
// completed and initial IPC's sent.
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(root->current_frame_host(), "", "")
.error.empty());
ASSERT_TRUE(
EvalJsAfterLifecycleUpdate(child_node_b->current_frame_host(), "", "")
.error.empty());
filter->Clear();
// Scroll the child frame out of view, causing it to become throttled.
ASSERT_TRUE(
ExecJs(child_node_b->current_frame_host(), "window.scrollTo(0, 5000)"));
filter->Wait();
EXPECT_TRUE(
filter->GetIntersectionState()->main_frame_intersection.IsEmpty());
// Scroll the frame back into view.
ASSERT_TRUE(
ExecJs(child_node_b->current_frame_host(), "window.scrollTo(0, 0)"));
filter->Wait();
EXPECT_FALSE(
filter->GetIntersectionState()->main_frame_intersection.IsEmpty());
}
// Tests that outermost_main_frame_scroll_position is not shared by frames in
// the same process. This is a regression test for https://crbug.com/1063760.
//
// Set up the frame tree to be A(B1(C1),B2(C2)). Send IPC's with different
// ViewportIntersection information to B1 and B2, and then check that the
// information they propagate to C1 and C2 is different.
// Disabled because of https://crbug.com/1136263
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
DISABLED_MainFrameScrollOffset) {
GURL a_url = embedded_test_server()->GetURL(
"a.com", "/frame_tree/scrollable_page_with_two_frames.html");
GURL b_url = embedded_test_server()->GetURL(
"b.com", "/frame_tree/page_with_large_iframe.html");
GURL c_url = embedded_test_server()->GetURL("c.com", "/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), a_url));
FrameTreeNode* a_node = web_contents()->GetPrimaryFrameTree().root();
FrameTreeNode* b1_node = a_node->child_at(0);
EXPECT_TRUE(NavigateToURLFromRenderer(b1_node, b_url));
FrameTreeNode* c1_node = b1_node->child_at(0);
EXPECT_TRUE(NavigateToURLFromRenderer(c1_node, c_url));
FrameTreeNode* b2_node = a_node->child_at(1);
EXPECT_TRUE(NavigateToURLFromRenderer(b2_node, b_url));
FrameTreeNode* c2_node = b2_node->child_at(0);
EXPECT_TRUE(NavigateToURLFromRenderer(c2_node, c_url));
// This will intercept messages sent from B1 to C1, describing C1's viewport
// intersection.
RenderFrameProxyHost* c1_proxy =
c1_node->render_manager()->GetProxyToParent();
auto b1_to_c1_message_filter =
std::make_unique<UpdateViewportIntersectionMessageFilter>(c1_proxy);
// This will intercept messages sent from B2 to C2, describing C2's viewport
// intersection.
RenderFrameProxyHost* c2_proxy =
c2_node->render_manager()->GetProxyToParent();
auto b2_to_c2_message_filter =
std::make_unique<UpdateViewportIntersectionMessageFilter>(c2_proxy);
// Running requestAnimationFrame will ensure that any pending IPC's have been
// sent by the renderer and received by the browser.
auto flush_ipcs = [](FrameTreeNode* node) {
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(node->current_frame_host(), "", "")
.error.empty());
};
flush_ipcs(a_node);
flush_ipcs(b1_node);
flush_ipcs(b2_node);
b1_to_c1_message_filter->Clear();
b2_to_c2_message_filter->Clear();
// Now that everything is in a stable, consistent state, we will send viewport
// intersection IPC's to B1 and B2 that contain a different
// outermost_main_frame_scroll_position, and then verify that each of them
// propagates their own value of outermost_main_frame_scroll_position to C1
// and C2, respectively. The IPC code mimics messages that A would send to B1
// and B2.
auto b1_intersection_state = b1_node->render_manager()
->GetProxyToParent()
->cross_process_frame_connector()
->intersection_state();
b1_intersection_state.outermost_main_frame_scroll_position.Offset(10, 0);
// A change in outermost_main_frame_scroll_position by itself will not cause
// B1 to be marked dirty, so we also modify viewport_intersection.
b1_intersection_state.viewport_intersection.set_y(
b1_intersection_state.viewport_intersection.y() + 7);
b1_intersection_state.viewport_intersection.set_height(
b1_intersection_state.viewport_intersection.height() - 7);
ForceUpdateViewportIntersection(b1_node, b1_intersection_state);
auto b2_intersection_state = b2_node->render_manager()
->GetProxyToParent()
->cross_process_frame_connector()
->intersection_state();
b2_intersection_state.outermost_main_frame_scroll_position.Offset(20, 0);
b2_intersection_state.viewport_intersection.set_y(
b2_intersection_state.viewport_intersection.y() + 7);
b2_intersection_state.viewport_intersection.set_height(
b2_intersection_state.viewport_intersection.height() - 7);
ForceUpdateViewportIntersection(b2_node, b2_intersection_state);
// Once IPC's have been flushed to the C frames, we should see conflicting
// values for outermost_main_frame_scroll_position.
flush_ipcs(b1_node);
flush_ipcs(b2_node);
ASSERT_TRUE(b1_to_c1_message_filter->MessageReceived());
ASSERT_TRUE(b2_to_c2_message_filter->MessageReceived());
EXPECT_EQ(b1_to_c1_message_filter->GetIntersectionState()
->outermost_main_frame_scroll_position,
gfx::Point(10, 0));
EXPECT_EQ(b2_to_c2_message_filter->GetIntersectionState()
->outermost_main_frame_scroll_position,
gfx::Point(20, 0));
b1_to_c1_message_filter->Clear();
b2_to_c2_message_filter->Clear();
// If we scroll the main frame, it should propagate IPC's which re-synchronize
// the values for all child frames.
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(a_node->current_frame_host(),
"window.scrollTo(0, 5)", "")
.error.empty());
flush_ipcs(b1_node);
flush_ipcs(b2_node);
ASSERT_TRUE(b1_to_c1_message_filter->MessageReceived());
ASSERT_TRUE(b2_to_c2_message_filter->MessageReceived());
// Window scroll offset will be scaled by device scale factor
const float device_scale_factor = a_node->render_manager()
->GetRenderWidgetHostView()
->GetDeviceScaleFactor();
float expected_y = device_scale_factor * 5.0;
EXPECT_NEAR(b1_to_c1_message_filter->GetIntersectionState()
->outermost_main_frame_scroll_position.y(),
expected_y, 1.f);
EXPECT_NEAR(b2_to_c2_message_filter->GetIntersectionState()
->outermost_main_frame_scroll_position.y(),
expected_y, 1.f);
}
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
FrameViewportIntersectionTestAggregate) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b,c,a,b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Each immediate child is sized to 100% width and 75% height.
LayoutNonRecursiveForTestingViewportIntersection(shell()->web_contents());
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
// Child 2 does not intersect, but shares widget with the main frame.
FrameTreeNode* node = root->child_at(2);
RenderProcessHostPriorityClient::Priority priority =
node->current_frame_host()->GetRenderWidgetHost()->GetPriority();
EXPECT_TRUE(priority.intersects_viewport);
EXPECT_TRUE(
node->current_frame_host()->GetProcess()->GetIntersectsViewport());
// Child 3 does not intersect, but shares a process with child 0.
node = root->child_at(3);
priority = node->current_frame_host()->GetRenderWidgetHost()->GetPriority();
EXPECT_FALSE(priority.intersects_viewport);
EXPECT_TRUE(
node->current_frame_host()->GetProcess()->GetIntersectsViewport());
}
// Tests that when a non-root frame in an iframe, performs a RAF to emulate a
// scroll, that metrics are reported.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, ScrollByRAF) {
base::HistogramTester histogram_tester;
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(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 = web_contents()->GetPrimaryFrameTree().root();
ASSERT_EQ(1U, root->child_count());
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://b.com/",
DepictFrameTree(root));
// Layout all three frames, so that the animation has a region to mark dirty.
LayoutNonRecursiveForTestingViewportIntersection(root->current_frame_host());
LayoutNonRecursiveForTestingViewportIntersection(
root->child_at(0)->current_frame_host());
LayoutNonRecursiveForTestingViewportIntersection(
root->child_at(0)->child_at(0)->current_frame_host());
// Add a div to the nested iframe, so that it can be animated.
RenderFrameSubmissionObserver frame_observer(root->child_at(0)->child_at(0));
std::string addContent(R"(
var d = document.createElement('div');
d.id = 'animationtarget';
d.innerHTML = 'Hey Listen!';
document.body.appendChild(d);
)");
ASSERT_TRUE(
EvalJsAfterLifecycleUpdate(
root->child_at(0)->child_at(0)->current_frame_host(), "", addContent)
.error.empty());
frame_observer.WaitForAnyFrameSubmission();
// Fetch the initial metrics, as adding a div can incidentally trigger RAF
// metrics.
FetchHistogramsFromChildProcesses();
auto initial_samples = histogram_tester.GetAllSamples(
"Graphics.Smoothness.PercentDroppedFrames3.MainThread.RAF");
ASSERT_EQ(initial_samples.size(), 0u);
const int pre_scroll_frame_count = frame_observer.render_frame_count();
// Run a RAF that takes more than one frame, as metrics due to not track
// frames where WillBeginMainFrame occurs before it is triggered. Subsequent
// RAFs in the sequence will be measured.
std::string scrollByRAF(R"(
var offset = 0;
function run() {
let child = document.getElementById("animationtarget");
var rect = child.getBoundingClientRect();
child.style = 'transform: translateY(' + parseInt(offset)+'px);';
offset += 1;
requestAnimationFrame(run);
}
run();
)");
ASSERT_TRUE(
EvalJsAfterLifecycleUpdate(
root->child_at(0)->child_at(0)->current_frame_host(), scrollByRAF, "")
.error.empty());
// There will have been one frame before the RAF sequence. The minimum for
// reporting if 100 frames, however we need to wait at least one extra frame.
// On Android the animation begins during the initial call to
// EvalJsAfterLifecycleUpdate. However on Linux the first translate is not
// applied until the subsequent frame. So we wait for the minimum, then verify
// afterwards.
const int kExpectedNumberFrames = 101 + pre_scroll_frame_count;
while (frame_observer.render_frame_count() < kExpectedNumberFrames)
frame_observer.WaitForAnyFrameSubmission();
// We now wait for FrameSequenceTracker to time out in order for it to report.
// This will occur once the minimum 100 frames have been produced, and 5s have
// passed. If the test times out then the bug is back.
while (histogram_tester
.GetAllSamples(
"Graphics.Smoothness.PercentDroppedFrames3.MainThread.RAF")
.empty()) {
frame_observer.WaitForAnyFrameSubmission();
FetchHistogramsFromChildProcesses();
}
}
// Make sure that when a relevant feature of the main frame changes, e.g. the
// frame width, that the browser is notified.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, TextAutosizerPageInfo) {
UpdateTextAutosizerInfoProxyObserver update_text_autosizer_info_observer;
blink::web_pref::WebPreferences prefs =
web_contents()->GetOrCreateWebPreferences();
prefs.text_autosizing_enabled = true;
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
web_contents()->SetWebPreferences(prefs);
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* b_child = root->child_at(0);
blink::mojom::TextAutosizerPageInfo received_page_info;
auto interceptor = std::make_unique<TextAutosizerPageInfoInterceptor>(
web_contents()->GetPrimaryMainFrame());
#if BUILDFLAG(IS_ANDROID)
prefs.device_scale_adjustment += 0.05f;
// Change the device scale adjustment to trigger a RemotePageInfo update.
web_contents()->SetWebPreferences(prefs);
// Make sure we receive a ViewHostMsg from the main frame's renderer.
interceptor->WaitForPageInfo(absl::optional<int>(),
prefs.device_scale_adjustment);
// Make sure the correct page message is sent to the child.
base::RunLoop().RunUntilIdle();
received_page_info = interceptor->GetTextAutosizerPageInfo();
EXPECT_EQ(prefs.device_scale_adjustment,
received_page_info.device_scale_adjustment);
#else
// Resize the main frame, then wait to observe that the RemotePageInfo message
// arrives.
auto* view = web_contents()->GetRenderWidgetHostView();
gfx::Rect old_bounds = view->GetViewBounds();
gfx::Rect new_bounds(
old_bounds.origin(),
gfx::Size(old_bounds.width() - 20, old_bounds.height() - 20));
view->SetBounds(new_bounds);
// Make sure we receive a ViewHostMsg from the main frame's renderer.
interceptor->WaitForPageInfo(new_bounds.width(), absl::optional<float>());
// Make sure the correct page message is sent to the child.
base::RunLoop().RunUntilIdle();
received_page_info = interceptor->GetTextAutosizerPageInfo();
EXPECT_EQ(new_bounds.width(), received_page_info.main_frame_width);
#endif // BUILDFLAG(IS_ANDROID)
// Dynamically create a new, cross-process frame to test sending the cached
// TextAutosizerPageInfo.
GURL c_url = embedded_test_server()->GetURL("c.com", "/title1.html");
// The following is a hack so we can get an IPC watcher connected to the
// RenderProcessHost for C before the `blink::WebView` is created for it, and
// the TextAutosizerPageInfo IPC is sent to it.
scoped_refptr<SiteInstance> c_site =
web_contents()->GetSiteInstance()->GetRelatedSiteInstance(c_url);
// Force creation of a render process for c's SiteInstance, this will get
// used when we dynamically create the new frame.
auto* c_rph = static_cast<RenderProcessHostImpl*>(c_site->GetProcess());
ASSERT_TRUE(c_rph);
ASSERT_NE(c_rph, root->current_frame_host()->GetProcess());
ASSERT_NE(c_rph, b_child->current_frame_host()->GetProcess());
// Create the subframe now.
std::string create_frame_script = base::StringPrintf(
"var new_iframe = document.createElement('iframe');"
"new_iframe.src = '%s';"
"document.body.appendChild(new_iframe);",
c_url.spec().c_str());
EXPECT_TRUE(ExecJs(root, create_frame_script));
ASSERT_EQ(2U, root->child_count());
// Ensure IPC is sent.
base::RunLoop().RunUntilIdle();
blink::mojom::TextAutosizerPageInfo page_info_sent_to_remote_main_frames =
update_text_autosizer_info_observer.TextAutosizerPageInfo(
web_contents()
->GetRenderManager()
->GetAllProxyHostsForTesting()
.begin()
->second.get());
EXPECT_EQ(received_page_info.main_frame_width,
page_info_sent_to_remote_main_frames.main_frame_width);
EXPECT_EQ(received_page_info.main_frame_layout_width,
page_info_sent_to_remote_main_frames.main_frame_layout_width);
EXPECT_EQ(received_page_info.device_scale_adjustment,
page_info_sent_to_remote_main_frames.device_scale_adjustment);
}
// 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_P(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())
->GetPrimaryFrameTree()
.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->rect_in_parent_view_in_dip().size().width(), 1);
EXPECT_NEAR(50, connector->rect_in_parent_view_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.
// TODO(crbug.com/1278038): Test is very flaky on many platforms.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
DISABLED_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())
->GetPrimaryFrameTree()
.root();
FrameTreeNode* iframe = root->child_at(0);
GURL site_url =
embedded_test_server()->GetURL("b.com", "/page_with_resize_handler.html");
EXPECT_TRUE(NavigateToURLFromRenderer(iframe, site_url));
base::RunLoop().RunUntilIdle();
// Should be zero because the iframe only has its initial size from parent.
EXPECT_EQ(0, EvalJs(iframe->current_frame_host(), "resize_count;"));
}
// 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_P(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())
->GetPrimaryFrameTree()
.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"));
EXPECT_TRUE(NavigateToURLFromRenderer(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());
WaitForHitTestData(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();
RenderFrameProxyHost* parent_iframe_proxy =
nested_iframe_node->render_manager()->GetProxyToParent();
auto interceptor = std::make_unique<SynchronizeVisualPropertiesInterceptor>(
parent_iframe_proxy);
// Scroll the parent frame downward to verify that the child rect gets updated
// correctly.
blink::WebMouseWheelEvent scroll_event(
blink::WebInputEvent::Type::kMouseWheel,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
scroll_event.SetPositionInWidget(
std::floor((bounds.x() - rwhv_root->GetViewBounds().x() - 5) *
scale_factor),
std::floor((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());
interceptor->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 = interceptor->last_rect();
EXPECT_LT(update_rect.y(), bounds.y() - rwhv_root->GetViewBounds().y());
}
// Verify that "scrolling" property on frame elements propagates to child frames
// correctly.
// Does not work on android since android has scrollbars overlaid.
// TODO(bokan): Pretty soon most/all platforms will use overlay scrollbars. This
// test should find a better way to check for scrollability. crbug.com/662196.
// Flaky on Linux. crbug.com/790929.
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
#define MAYBE_FrameOwnerPropertiesPropagationScrolling \
DISABLED_FrameOwnerPropertiesPropagationScrolling
#else
#define MAYBE_FrameOwnerPropertiesPropagationScrolling \
FrameOwnerPropertiesPropagationScrolling
#endif
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
MAYBE_FrameOwnerPropertiesPropagationScrolling) {
#if BUILDFLAG(IS_MAC)
ui::test::ScopedPreferredScrollerStyle scroller_style_override(false);
#endif
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/frame_owner_properties_scrolling.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()->GetPrimaryFrameTree().root();
ASSERT_EQ(1u, root->child_count());
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));
FrameTreeNode* child = root->child_at(0);
// If the available client width within the iframe is smaller than the
// frame element's width, we assume there's a scrollbar.
// Also note that just comparing clientHeight and scrollHeight of the frame's
// document will not work.
auto has_scrollbar = [](RenderFrameHostImpl* rfh) {
int client_width = EvalJs(rfh, "document.body.clientWidth").ExtractInt();
const int kFrameElementWidth = 200;
return client_width < kFrameElementWidth;
};
auto set_scrolling_property = [](RenderFrameHostImpl* parent_rfh,
const std::string& value) {
EXPECT_TRUE(ExecJs(
parent_rfh,
base::StringPrintf("document.getElementById('child-1').setAttribute("
" 'scrolling', '%s');",
value.c_str())));
};
// Run the test over variety of parent/child cases.
GURL urls[] = {// Remote to remote.
embedded_test_server()->GetURL("c.com", "/tall_page.html"),
// Remote to local.
embedded_test_server()->GetURL("a.com", "/tall_page.html"),
// Local to remote.
embedded_test_server()->GetURL("b.com", "/tall_page.html")};
const std::string scrolling_values[] = {"yes", "auto", "no"};
for (const auto& scrolling_value : scrolling_values) {
bool expect_scrollbar = scrolling_value != "no";
set_scrolling_property(root->current_frame_host(), scrolling_value);
for (const auto& url : urls) {
EXPECT_TRUE(NavigateToURLFromRenderer(child, url));
EXPECT_EQ(expect_scrollbar, has_scrollbar(child->current_frame_host()));
}
}
}
// Verify that "marginwidth" and "marginheight" properties on frame elements
// propagate to child frames correctly.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
FrameOwnerPropertiesPropagationMargin) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/frame_owner_properties_margin.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()->GetPrimaryFrameTree().root();
ASSERT_EQ(1u, root->child_count());
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));
FrameTreeNode* child = root->child_at(0);
EXPECT_EQ("10", EvalJs(child, "document.body.getAttribute('marginwidth');"));
EXPECT_EQ("50", EvalJs(child, "document.body.getAttribute('marginheight');"));
// Run the test over variety of parent/child cases.
GURL urls[] = {// Remote to remote.
embedded_test_server()->GetURL("c.com", "/title2.html"),
// Remote to local.
embedded_test_server()->GetURL("a.com", "/title1.html"),
// Local to remote.
embedded_test_server()->GetURL("b.com", "/title2.html")};
int current_margin_width = 15;
int current_margin_height = 25;
// Before each navigation, we change the marginwidth and marginheight
// properties of the frame. We then check whether those properties are applied
// correctly after the navigation has completed.
for (const auto& url : urls) {
// Change marginwidth and marginheight before navigating.
EXPECT_TRUE(ExecJs(
root,
base::StringPrintf("var child = document.getElementById('child-1');"
"child.setAttribute('marginwidth', '%d');"
"child.setAttribute('marginheight', '%d');",
current_margin_width, current_margin_height)));
EXPECT_TRUE(NavigateToURLFromRenderer(child, url));
EXPECT_EQ(base::NumberToString(current_margin_width),
EvalJs(child, "document.body.getAttribute('marginwidth');"));
EXPECT_EQ(base::NumberToString(current_margin_height),
EvalJs(child, "document.body.getAttribute('marginheight');"));
current_margin_width += 5;
current_margin_height += 10;
}
}
// Verify that "csp" property on frame elements propagates to child frames
// correctly. See https://crbug.com/647588
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
FrameOwnerPropertiesPropagationCSP) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/frame_owner_properties_csp.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()->GetPrimaryFrameTree().root();
ASSERT_EQ(1u, root->child_count());
// The document in the iframe is blocked by CSPEE. An error page is loaded, it
// stays in the process of the main document.
EXPECT_EQ(
" Site A\n"
" +--Site A\n"
"Where A = http://a.com/",
DepictFrameTree(root));
FrameTreeNode* child = root->child_at(0);
EXPECT_EQ(
"object-src \'none\'",
EvalJs(root, "document.getElementById('child-1').getAttribute('csp');"));
// Run the test over variety of parent/child cases.
struct {
std::string csp_value;
GURL url;
bool should_block;
} testCases[]{
// Remote to remote.
{"default-src a.com",
embedded_test_server()->GetURL("c.com", "/title2.html"), true},
// Remote to local.
{"default-src b.com",
embedded_test_server()->GetURL("a.com", "/title1.html"), false},
// Local to remote.
{"img-src c.com", embedded_test_server()->GetURL("b.com", "/title2.html"),
true},
};
// Before each navigation, we change the csp property of the frame.
// We then check whether that property is applied
// correctly after the navigation has completed.
for (const auto& testCase : testCases) {
// Change csp before navigating.
EXPECT_TRUE(ExecJs(
root,
base::StringPrintf("document.getElementById('child-1').setAttribute("
" 'csp', '%s');",
testCase.csp_value.c_str())));
NavigateFrameToURL(child, testCase.url);
EXPECT_EQ(testCase.csp_value, child->csp_attribute()->header->header_value);
// TODO(amalika): add checks that the CSP replication takes effect
const url::Origin child_origin =
child->current_frame_host()->GetLastCommittedOrigin();
EXPECT_EQ(testCase.should_block, child_origin.opaque());
EXPECT_EQ(url::Origin::Create(testCase.url.DeprecatedGetOriginAsURL())
.GetTupleOrPrecursorTupleIfOpaque(),
child_origin.GetTupleOrPrecursorTupleIfOpaque());
}
}
// This test verifies that changing the CSS visibility of a cross-origin
// <iframe> is forwarded to its corresponding RenderWidgetHost and all other
// RenderWidgetHosts corresponding to the nested cross-origin frame.
// TODO(crbug.com/1363740): Flaky on mac, linux-lacros, android.
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS_LACROS)
#define MAYBE_CSSVisibilityChanged DISABLED_CSSVisibilityChanged
#else
#define MAYBE_CSSVisibilityChanged CSSVisibilityChanged
#endif
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, MAYBE_CSSVisibilityChanged) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(b(c(d(d(a))))))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Find all child RenderWidgetHosts.
std::vector<RenderWidgetHostImpl*> child_widget_hosts;
FrameTreeNode* first_cross_process_child =
web_contents()->GetPrimaryFrameTree().root()->child_at(0);
for (auto* ftn : web_contents()->GetPrimaryFrameTree().SubtreeNodes(
first_cross_process_child)) {
RenderFrameHostImpl* frame_host = ftn->current_frame_host();
if (!frame_host->is_local_root())
continue;
child_widget_hosts.push_back(frame_host->GetRenderWidgetHost());
}
// Ignoring the root, there is exactly 4 local roots and hence 5
// RenderWidgetHosts on the page.
EXPECT_EQ(4U, child_widget_hosts.size());
// Initially all the RenderWidgetHosts should be visible.
for (size_t index = 0; index < child_widget_hosts.size(); ++index) {
EXPECT_FALSE(child_widget_hosts[index]->is_hidden())
<< "The RWH at distance " << index + 1U
<< " from root RWH should not be hidden.";
}
std::string show_script =
"document.querySelector('iframe').style.visibility = 'visible';";
std::string hide_script =
"document.querySelector('iframe').style.visibility = 'hidden';";
// Define observers for notifications about hiding child RenderWidgetHosts.
std::vector<std::unique_ptr<RenderWidgetHostVisibilityObserver>>
hide_widget_host_observers(child_widget_hosts.size());
for (size_t index = 0U; index < child_widget_hosts.size(); ++index) {
hide_widget_host_observers[index] =
std::make_unique<RenderWidgetHostVisibilityObserver>(
child_widget_hosts[index], false);
}
EXPECT_TRUE(ExecJs(shell(), hide_script));
for (size_t index = 0U; index < child_widget_hosts.size(); ++index) {
EXPECT_TRUE(hide_widget_host_observers[index]->WaitUntilSatisfied())
<< "Expected RenderWidgetHost at distance " << index + 1U
<< " from root RenderWidgetHost to become hidden.";
}
// Define observers for notifications about showing child RenderWidgetHosts.
std::vector<std::unique_ptr<RenderWidgetHostVisibilityObserver>>
show_widget_host_observers(child_widget_hosts.size());
for (size_t index = 0U; index < child_widget_hosts.size(); ++index) {
show_widget_host_observers[index] =
std::make_unique<RenderWidgetHostVisibilityObserver>(
child_widget_hosts[index], true);
}
EXPECT_TRUE(ExecJs(shell(), show_script));
for (size_t index = 0U; index < child_widget_hosts.size(); ++index) {
EXPECT_TRUE(show_widget_host_observers[index]->WaitUntilSatisfied())
<< "Expected RenderWidgetHost at distance " << index + 1U
<< " from root RenderWidgetHost to become shown.";
}
}
// This test verifies that hiding an OOPIF in CSS will stop generating
// compositor frames for the OOPIF and any nested OOPIFs inside it. This holds
// even when the whole page is shown.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
HiddenOOPIFWillNotGenerateCompositorFrames) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_two_frames.html"));
ASSERT_TRUE(NavigateToURL(shell(), main_url));
ASSERT_EQ(shell()->web_contents()->GetLastCommittedURL(), main_url);
GURL cross_site_url_b =
embedded_test_server()->GetURL("b.com", "/counter.html");
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), cross_site_url_b));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(1), cross_site_url_b));
// Now inject code in the first frame to create a nested OOPIF.
RenderFrameHostCreatedObserver new_frame_created_observer(
shell()->web_contents(), 1);
ASSERT_TRUE(
ExecJs(root->child_at(0)->current_frame_host(),
"document.body.appendChild(document.createElement('iframe'));"));
new_frame_created_observer.Wait();
GURL cross_site_url_a =
embedded_test_server()->GetURL("a.com", "/counter.html");
// Navigate the nested frame.
TestFrameNavigationObserver observer(root->child_at(0)->child_at(0));
ASSERT_TRUE(ExecJs(root->child_at(0)->current_frame_host(),
JsReplace("document.querySelector('iframe').src = $1",
cross_site_url_a)));
observer.Wait();
RenderWidgetHostViewChildFrame* first_child_view =
static_cast<RenderWidgetHostViewChildFrame*>(
root->child_at(0)->current_frame_host()->GetView());
RenderWidgetHostViewChildFrame* second_child_view =
static_cast<RenderWidgetHostViewChildFrame*>(
root->child_at(1)->current_frame_host()->GetView());
RenderWidgetHostViewChildFrame* nested_child_view =
static_cast<RenderWidgetHostViewChildFrame*>(
root->child_at(0)->child_at(0)->current_frame_host()->GetView());
RenderFrameSubmissionObserver first_frame_counter(
first_child_view->host_->render_frame_metadata_provider());
RenderFrameSubmissionObserver second_frame_counter(
second_child_view->host_->render_frame_metadata_provider());
RenderFrameSubmissionObserver third_frame_counter(
nested_child_view->host_->render_frame_metadata_provider());
const int kFrameCountLimit = 20;
// Wait for a minimum number of compositor frames for the second frame.
while (second_frame_counter.render_frame_count() < kFrameCountLimit)
second_frame_counter.WaitForAnyFrameSubmission();
ASSERT_LE(kFrameCountLimit, second_frame_counter.render_frame_count());
// Now make sure all frames have roughly the counter value in the sense that
// no counter value is more than twice any other.
float ratio = static_cast<float>(first_frame_counter.render_frame_count()) /
static_cast<float>(second_frame_counter.render_frame_count());
EXPECT_GT(2.5f, ratio + 1 / ratio) << "Ratio is: " << ratio;
ratio = static_cast<float>(first_frame_counter.render_frame_count()) /
static_cast<float>(third_frame_counter.render_frame_count());
EXPECT_GT(2.5f, ratio + 1 / ratio) << "Ratio is: " << ratio;
// Make sure all views can become visible.
EXPECT_TRUE(first_child_view->CanBecomeVisible());
EXPECT_TRUE(second_child_view->CanBecomeVisible());
EXPECT_TRUE(nested_child_view->CanBecomeVisible());
// Hide the first frame and wait for the notification to be posted by its
// RenderWidgetHost.
RenderWidgetHostVisibilityObserver hide_observer(
root->child_at(0)->current_frame_host()->GetRenderWidgetHost(), false);
// Hide the first frame.
ASSERT_TRUE(ExecJs(
shell(),
"document.getElementsByName('frame1')[0].style.visibility = 'hidden'"));
ASSERT_TRUE(hide_observer.WaitUntilSatisfied());
EXPECT_TRUE(first_child_view->FrameConnectorForTesting()->IsHidden());
// Verify that only the second view can become visible now.
EXPECT_FALSE(first_child_view->CanBecomeVisible());
EXPECT_TRUE(second_child_view->CanBecomeVisible());
EXPECT_FALSE(nested_child_view->CanBecomeVisible());
// Now hide and show the WebContents (to simulate a tab switch).
shell()->web_contents()->WasHidden();
shell()->web_contents()->WasShown();
first_frame_counter.ResetCounter();
second_frame_counter.ResetCounter();
third_frame_counter.ResetCounter();
// We expect the second counter to keep running.
while (second_frame_counter.render_frame_count() < kFrameCountLimit)
second_frame_counter.WaitForAnyFrameSubmission();
ASSERT_LT(kFrameCountLimit, second_frame_counter.render_frame_count() + 1);
// Verify that the counter for other two frames did not count much.
ratio = static_cast<float>(first_frame_counter.render_frame_count()) /
static_cast<float>(second_frame_counter.render_frame_count());
EXPECT_GT(0.5f, ratio) << "Ratio is: " << ratio;
ratio = static_cast<float>(third_frame_counter.render_frame_count()) /
static_cast<float>(second_frame_counter.render_frame_count());
EXPECT_GT(0.5f, ratio) << "Ratio is: " << ratio;
}
// This test verifies that navigating a hidden OOPIF to cross-origin will not
// lead to creating compositor frames for the new OOPIF renderer.
IN_PROC_BROWSER_TEST_P(
SitePerProcessBrowserTest,
HiddenOOPIFWillNotGenerateCompositorFramesAfterNavigation) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_two_frames.html"));
ASSERT_TRUE(NavigateToURL(shell(), main_url));
ASSERT_EQ(shell()->web_contents()->GetLastCommittedURL(), main_url);
GURL cross_site_url_b =
embedded_test_server()->GetURL("b.com", "/counter.html");
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), cross_site_url_b));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(1), cross_site_url_b));
// Hide the first frame and wait for the notification to be posted by its
// RenderWidgetHost.
RenderWidgetHostVisibilityObserver hide_observer(
root->child_at(0)->current_frame_host()->GetRenderWidgetHost(), false);
// Hide the first frame.
ASSERT_TRUE(ExecJs(
shell(),
"document.getElementsByName('frame1')[0].style.visibility = 'hidden'"));
ASSERT_TRUE(hide_observer.WaitUntilSatisfied());
// Now navigate the first frame to another OOPIF process.
TestFrameNavigationObserver navigation_observer(
root->child_at(0)->current_frame_host());
GURL cross_site_url_c =
embedded_test_server()->GetURL("c.com", "/counter.html");
ASSERT_TRUE(
ExecJs(web_contents(),
JsReplace("document.getElementsByName('frame1')[0].src = $1",
cross_site_url_c)));
navigation_observer.Wait();
// Now investigate compositor frame creation.
RenderWidgetHostViewChildFrame* first_child_view =
static_cast<RenderWidgetHostViewChildFrame*>(
root->child_at(0)->current_frame_host()->GetView());
RenderWidgetHostViewChildFrame* second_child_view =
static_cast<RenderWidgetHostViewChildFrame*>(
root->child_at(1)->current_frame_host()->GetView());
EXPECT_FALSE(first_child_view->CanBecomeVisible());
RenderFrameSubmissionObserver first_frame_counter(
first_child_view->host_->render_frame_metadata_provider());
RenderFrameSubmissionObserver second_frame_counter(
second_child_view->host_->render_frame_metadata_provider());
const int kFrameCountLimit = 20;
// Wait for a certain number of swapped compositor frames generated for the
// second child view. During the same interval the first frame should not have
// swapped any compositor frames.
while (second_frame_counter.render_frame_count() < kFrameCountLimit)
second_frame_counter.WaitForAnyFrameSubmission();
ASSERT_LT(kFrameCountLimit, second_frame_counter.render_frame_count() + 1);
float ratio = static_cast<float>(first_frame_counter.render_frame_count()) /
static_cast<float>(second_frame_counter.render_frame_count());
EXPECT_GT(0.5f, ratio) << "Ratio is: " << ratio;
}
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, ScreenCoordinates) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
FrameTreeNode* child = root->child_at(0);
const char* properties[] = {"screenX", "screenY", "outerWidth",
"outerHeight"};
for (const char* property : properties) {
std::string script = base::StringPrintf("window.%s;", property);
int root_value = EvalJs(root, script).ExtractInt();
int child_value = EvalJs(child, script).ExtractInt();
EXPECT_EQ(root_value, child_value);
}
}
// Tests that an out-of-process iframe receives the visibilitychange event.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, VisibilityChange) {
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())
->GetPrimaryFrameTree()
.root();
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));
EXPECT_TRUE(
ExecJs(root->child_at(0),
"var event_fired = 0;\n"
"document.addEventListener('visibilitychange',\n"
" function() { event_fired++; });\n"));
shell()->web_contents()->WasHidden();
EXPECT_EQ(1, EvalJs(root->child_at(0), "event_fired"));
shell()->web_contents()->WasShown();
EXPECT_EQ(2, EvalJs(root->child_at(0), "event_fired"));
}
// This test verifies that the main-frame's page scale factor propagates to
// the compositor layertrees in each of the child processes.
// Flaky on all platforms: https://crbug.com/1116774
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
DISABLED_PageScaleFactorPropagatesToOOPIFs) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c),d)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
ASSERT_EQ(2u, root->child_count());
FrameTreeNode* child_b = root->child_at(0);
FrameTreeNode* child_c = root->child_at(1);
ASSERT_EQ(1U, child_b->child_count());
FrameTreeNode* child_d = child_b->child_at(0);
ASSERT_TRUE(child_b);
ASSERT_TRUE(child_c);
ASSERT_TRUE(child_d);
EXPECT_EQ(
" Site A ------------ proxies for B C D\n"
" |--Site B ------- proxies for A C D\n"
" | +--Site C -- proxies for A B D\n"
" +--Site D ------- proxies for A B C\n"
"Where A = http://a.com/\n"
" B = http://b.com/\n"
" C = http://c.com/\n"
" D = http://d.com/",
DepictFrameTree(root));
RenderFrameSubmissionObserver observer_a(root);
RenderFrameSubmissionObserver observer_b(child_b);
RenderFrameSubmissionObserver observer_c(child_c);
RenderFrameSubmissionObserver observer_d(child_d);
// Monitor visual sync messages coming from the mainframe to make sure
// |is_pinch_gesture_active| goes true during the pinch gesture.
RenderFrameProxyHost* root_proxy_host =
child_d->render_manager()->GetProxyToParent();
auto interceptor_mainframe =
std::make_unique<SynchronizeVisualPropertiesInterceptor>(root_proxy_host);
// Monitor frame sync messages coming from child_b as it will need to
// relay them to child_d.
RenderFrameProxyHost* child_b_proxy_host =
child_c->render_manager()->GetProxyToParent();
auto interceptor_child_b =
std::make_unique<SynchronizeVisualPropertiesInterceptor>(
child_b_proxy_host);
// We need to observe a root frame submission to pick up the initial page
// scale factor.
observer_a.WaitForAnyFrameSubmission();
const float kPageScaleDelta = 2.f;
// On desktop systems we expect |current_page_scale| to be 1.f, but on
// Android it will typically be less than 1.f, and may take on arbitrary
// values.
float current_page_scale =
observer_a.LastRenderFrameMetadata().page_scale_factor;
float target_page_scale = current_page_scale * kPageScaleDelta;
SyntheticPinchGestureParams params;
auto* host = static_cast<RenderWidgetHostImpl*>(
root->current_frame_host()->GetRenderWidgetHost());
gfx::Rect bounds(host->GetView()->GetViewBounds().size());
// The synthetic gesture code expects a location in root-view coordinates.
params.anchor = gfx::PointF(bounds.CenterPoint());
// In SyntheticPinchGestureParams, |scale_factor| is really a delta.
params.scale_factor = kPageScaleDelta;
#if BUILDFLAG(IS_MAC)
auto synthetic_pinch_gesture =
std::make_unique<SyntheticTouchpadPinchGesture>(params);
#else
auto synthetic_pinch_gesture =
std::make_unique<SyntheticTouchscreenPinchGesture>(params);
#endif
// Send pinch gesture and verify we receive the ack.
InputEventAckWaiter ack_waiter(host,
blink::WebInputEvent::Type::kGesturePinchEnd);
host->QueueSyntheticGesture(
std::move(synthetic_pinch_gesture),
base::BindOnce([](SyntheticGesture::Result result) {
EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result);
}));
ack_waiter.Wait();
// Make sure all the page scale values behave as expected.
const float kScaleTolerance = 0.1f;
observer_a.WaitForPageScaleFactor(target_page_scale, kScaleTolerance);
observer_b.WaitForExternalPageScaleFactor(target_page_scale, kScaleTolerance);
observer_c.WaitForExternalPageScaleFactor(target_page_scale, kScaleTolerance);
observer_d.WaitForExternalPageScaleFactor(target_page_scale, kScaleTolerance);
// The change in |is_pinch_gesture_active| that signals the end of the pinch
// gesture will occur sometime after the ack for GesturePinchEnd, so we need
// to wait for it from each renderer. If it's never seen, the test fails by
// timing out.
interceptor_mainframe->WaitForPinchGestureEnd();
interceptor_child_b->WaitForPinchGestureEnd();
}
// Test that the compositing scale factor for an out-of-process iframe are set
// and updated correctly, including accounting for all intermediate transforms.
// TODO(crbug.com/1164391): Flaky test.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
DISABLED_CompositingScaleFactorInNestedFrameTest) {
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())
->GetPrimaryFrameTree()
.root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* child_b = root->child_at(0);
EXPECT_TRUE(NavigateToURLFromRenderer(
child_b, embedded_test_server()->GetURL(
"b.com", "/frame_tree/page_with_transformed_iframe.html")));
ASSERT_EQ(1U, child_b->child_count());
FrameTreeNode* child_c = child_b->child_at(0);
EXPECT_TRUE(NavigateToURLFromRenderer(
child_c, embedded_test_server()->GetURL(
"c.com", "/frame_tree/page_with_scaled_frame.html")));
ASSERT_EQ(1U, child_c->child_count());
FrameTreeNode* child_d = child_c->child_at(0);
EXPECT_TRUE(NavigateToURLFromRenderer(
child_d, embedded_test_server()->GetURL("d.com", "/simple_page.html")));
EXPECT_EQ(
" Site A ------------ proxies for B C D\n"
" +--Site B ------- proxies for A C D\n"
" +--Site C -- proxies for A B D\n"
" +--Site D -- proxies for A B C\n"
"Where A = http://a.com/\n"
" B = http://b.com/\n"
" C = http://c.com/\n"
" D = http://d.com/",
DepictFrameTree(root));
// Wait for b.com's frame to have its compositing scale factor set to 0.5,
// which is the scale factor for b.com's iframe element in the main frame.
while (true) {
auto* rwh_b = child_b->current_frame_host()->GetRenderWidgetHost();
absl::optional<blink::VisualProperties> properties =
rwh_b->LastComputedVisualProperties();
if (properties && cc::MathUtil::IsFloatNearlyTheSame(
properties->compositing_scale_factor, 0.5f)) {
break;
}
base::RunLoop().RunUntilIdle();
}
// Wait for c.com's frame to have its compositing scale factor set to 0.5,
// which is the accumulated scale factor of c.com to the main frame obtained
// by multiplying the scale factor of c.com's iframe element (1 since
// transform is rotation only without scale) with the scale factor of its
// parent frame b.com (0.5).
while (true) {
auto* rwh_c = child_c->current_frame_host()->GetRenderWidgetHost();
absl::optional<blink::VisualProperties> properties =
rwh_c->LastComputedVisualProperties();
if (properties && cc::MathUtil::IsFloatNearlyTheSame(
properties->compositing_scale_factor, 0.5f)) {
break;
}
base::RunLoop().RunUntilIdle();
}
// Wait for d.com's frame to have its compositing scale factor set to 0.25,
// which is the accumulated scale factor of d.com to the main frame obtained
// by combining the scale factor of d.com's iframe element (0.5) with the
// scale factor of its parent d.com (0.5).
while (true) {
auto* rwh_d = child_d->current_frame_host()->GetRenderWidgetHost();
absl::optional<blink::VisualProperties> properties =
rwh_d->LastComputedVisualProperties();
if (properties && cc::MathUtil::IsFloatNearlyTheSame(
properties->compositing_scale_factor, 0.25f)) {
break;
}
base::RunLoop().RunUntilIdle();
}
}
// Test that the compositing scale factor for an out-of-process iframe is set
// to a non-zero value even if intermediate CSS transform has zero scale.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
CompositingScaleFactorWithZeroScaleTransform) {
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())
->GetPrimaryFrameTree()
.root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* child_b = root->child_at(0);
EXPECT_TRUE(NavigateToURLFromRenderer(
child_b,
embedded_test_server()->GetURL("b.com", "/frame_tree/simple_page.html")));
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));
// Wait for b.com's frame to have its compositing scale factor set to 0.5,
// which is the scale factor for b.com's iframe element in the main frame.
while (true) {
auto* rwh_b = child_b->current_frame_host()->GetRenderWidgetHost();
absl::optional<blink::VisualProperties> properties =
rwh_b->LastComputedVisualProperties();
if (properties && cc::MathUtil::IsFloatNearlyTheSame(
properties->compositing_scale_factor, 0.5f)) {
break;
}
base::RunLoop().RunUntilIdle();
}
// Set iframe transform scale to 0.
EXPECT_TRUE(
EvalJs(root->current_frame_host(),
"document.querySelector('iframe').style.transform = 'scale(0)'")
.error.empty());
// Wait for b.com frame's compositing scale factor to change, and check that
// the final value is non-zero.
while (true) {
auto* rwh_b = child_b->current_frame_host()->GetRenderWidgetHost();
absl::optional<blink::VisualProperties> properties =
rwh_b->LastComputedVisualProperties();
if (properties && !cc::MathUtil::IsFloatNearlyTheSame(
properties->compositing_scale_factor, 0.5f)) {
EXPECT_GT(properties->compositing_scale_factor, 0.0f);
break;
}
base::RunLoop().RunUntilIdle();
}
}
// Check that when a frame changes a subframe's size twice and then sends a
// postMessage to the subframe, the subframe's onmessage handler sees the new
// size. In particular, ensure that the postMessage won't get reordered with
// the second resize, which might be throttled if the first resize is still in
// progress. See https://crbug.com/828529.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
ResizeAndCrossProcessPostMessagePreserveOrder) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
// Add an onmessage handler to the subframe to send back its width.
EXPECT_TRUE(ExecJs(root->child_at(0),
WaitForMessageScript("document.body.clientWidth")));
// Drop the visual properties ACKs from the child renderer. To do this,
// unsubscribe the child's RenderWidgetHost from its
// RenderFrameMetadataProvider, which ensures that
// DidUpdateVisualProperties() won't be called on it, and the ACK won't be
// reset. This simulates that the ACK for the first resize below does not
// arrive before the second resize IPC arrives from the
// parent, and that the second resize IPC early-exits in
// SynchronizeVisualProperties() due to the pending visual properties ACK.
RenderWidgetHostImpl* rwh =
root->child_at(0)->current_frame_host()->GetRenderWidgetHost();
rwh->render_frame_metadata_provider_.RemoveObserver(rwh);
// Now, resize the subframe twice from the main frame and send it a
// postMessage. The postMessage handler should see the second updated size.
EXPECT_TRUE(ExecJs(root, R"(
var f = document.querySelector('iframe');
f.width = 500;
f.offsetTop; // force layout; this sends a resize IPC for width of 500.
f.width = 700;
f.offsetTop; // force layout; this sends a resize IPC for width of 700.
f.contentWindow.postMessage('foo', '*');)"));
EXPECT_EQ(700, EvalJs(root->child_at(0), "onMessagePromise"));
}
// This test verifies that when scrolling an OOPIF in a pinched-zoomed page,
// that the scroll-delta matches the distance between TouchStart/End as seen
// by the oopif, i.e. the oopif content 'sticks' to the finger during scrolling.
// The relation is not exact, but should be close.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
ScrollOopifInPinchZoomedPage) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
ASSERT_EQ(1u, root->child_count());
FrameTreeNode* child = root->child_at(0);
ASSERT_TRUE(child);
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));
// Make B scrollable. The call to document.write will erase the html inside
// the OOPIF, leaving just a vertical column of 'Hello's.
std::string script =
"var s = '<div>Hello</div>\\n';\n"
"document.write(s.repeat(200));";
EXPECT_TRUE(ExecJs(child, script));
RenderFrameSubmissionObserver observer_a(root);
RenderFrameSubmissionObserver observer_b(child);
// We need to observe a root frame submission to pick up the initial page
// scale factor.
observer_a.WaitForAnyFrameSubmission();
const float kPageScaleDelta = 2.f;
// On desktop systems we expect |current_page_scale| to be 1.f, but on
// Android it will typically be less than 1.f, and may take on arbitrary
// values.
float original_page_scale =
observer_a.LastRenderFrameMetadata().page_scale_factor;
float target_page_scale = original_page_scale * kPageScaleDelta;
SyntheticPinchGestureParams params;
auto* host = static_cast<RenderWidgetHostImpl*>(
root->current_frame_host()->GetRenderWidgetHost());
RenderWidgetHostViewBase* root_view = host->GetView();
RenderWidgetHostViewBase* child_view =
static_cast<RenderWidgetHostImpl*>(
child->current_frame_host()->GetRenderWidgetHost())
->GetView();
gfx::Rect bounds(root_view->GetViewBounds().size());
// The synthetic gesture code expects a location in root-view coordinates.
params.anchor = gfx::PointF(bounds.CenterPoint().x(), 70.f);
// In SyntheticPinchGestureParams, |scale_factor| is really a delta.
params.scale_factor = kPageScaleDelta;
#if BUILDFLAG(IS_MAC)
auto synthetic_pinch_gesture =
std::make_unique<SyntheticTouchpadPinchGesture>(params);
#else
auto synthetic_pinch_gesture =
std::make_unique<SyntheticTouchscreenPinchGesture>(params);
#endif
// Send pinch gesture and verify we receive the ack.
{
InputEventAckWaiter ack_waiter(
host, blink::WebInputEvent::Type::kGesturePinchEnd);
host->QueueSyntheticGesture(
std::move(synthetic_pinch_gesture),
base::BindOnce([](SyntheticGesture::Result result) {
EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result);
}));
ack_waiter.Wait();
}
// Make sure all the page scale values behave as expected.
const float kScaleTolerance = 0.07f;
observer_a.WaitForPageScaleFactor(target_page_scale, kScaleTolerance);
observer_b.WaitForExternalPageScaleFactor(target_page_scale, kScaleTolerance);
float final_page_scale =
observer_a.LastRenderFrameMetadata().page_scale_factor;
// Verify scroll position of OOPIF.
float initial_child_scroll = EvalJs(child, "window.scrollY").ExtractDouble();
// Send touch-initiated gesture scroll sequence to OOPIF.
// TODO(wjmaclean): GetViewBounds() is broken for OOPIFs when PSF != 1.f, so
// we calculate it manually. This will need to be update when GetViewBounds()
// in RenderWidgetHostViewBase is fixed. See https://crbug.com/928825.
auto child_bounds = child_view->GetViewBounds();
gfx::PointF child_upper_left =
child_view->TransformPointToRootCoordSpaceF(gfx::PointF(0, 0));
gfx::PointF child_lower_right = child_view->TransformPointToRootCoordSpaceF(
gfx::PointF(child_bounds.width(), child_bounds.height()));
gfx::PointF scroll_start_location_in_screen =
gfx::PointF((child_upper_left.x() + child_lower_right.x()) / 2.f,
child_lower_right.y() - 10);
const float kScrollDelta = 100.f;
gfx::PointF scroll_end_location_in_screen =
scroll_start_location_in_screen + gfx::Vector2dF(0, -kScrollDelta);
// Create touch move sequence with discrete touch moves. Include a brief
// pause at the end to avoid the scroll flinging.
std::string actions_template = R"HTML(
[{
"source" : "touch",
"actions" : [
{ "name": "pointerDown", "x": %f, "y": %f},
{ "name": "pointerMove", "x": %f, "y": %f},
{ "name": "pause", "duration": 300 },
{ "name": "pointerUp"}
]
}]
)HTML";
std::string touch_move_sequence_json = base::StringPrintf(
actions_template.c_str(), scroll_start_location_in_screen.x(),
scroll_start_location_in_screen.y(), scroll_end_location_in_screen.x(),
scroll_end_location_in_screen.y());
auto parsed_json =
base::JSONReader::ReadAndReturnValueWithError(touch_move_sequence_json);
ASSERT_TRUE(parsed_json.has_value()) << parsed_json.error().message;
ActionsParser actions_parser(std::move(*parsed_json));
ASSERT_TRUE(actions_parser.Parse());
auto synthetic_scroll_gesture =
SyntheticGesture::Create(actions_parser.gesture_params());
{
auto* child_host = static_cast<RenderWidgetHostImpl*>(
child->current_frame_host()->GetRenderWidgetHost());
InputEventAckWaiter ack_waiter(
child_host, blink::WebInputEvent::Type::kGestureScrollEnd);
host->QueueSyntheticGesture(
std::move(synthetic_scroll_gesture),
base::BindOnce([](SyntheticGesture::Result result) {
EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result);
}));
ack_waiter.Wait();
}
// Verify new scroll position of OOPIF, should match touch sequence delta.
float expected_scroll_delta = kScrollDelta / final_page_scale;
float actual_scroll_delta =
EvalJs(child, "window.scrollY").ExtractDouble() - initial_child_scroll;
const float kScrollTolerance = 0.2f;
EXPECT_GT((1.f + kScrollTolerance) * expected_scroll_delta,
actual_scroll_delta);
EXPECT_LT((1.f - kScrollTolerance) * expected_scroll_delta,
actual_scroll_delta);
}
INSTANTIATE_TEST_SUITE_P(All,
SitePerProcessHighDPIBrowserTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
} // namespace content