blob: bfcb10ab06b5be1352e6e1c51035e90cec13d777 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/renderer_host/render_widget_host_view_child_frame.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/logging.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/viz/common/surfaces/surface_id.h"
#include "content/browser/portal/portal.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/mock_display_feature.h"
#include "content/test/portal/portal_created_observer.h"
#include "content/test/test_content_browser_client.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/features.h"
#include "ui/gfx/geometry/size.h"
namespace content {
namespace {
class RenderWidgetHostViewChildFrameBrowserTest : public ContentBrowserTest {
public:
RenderWidgetHostViewChildFrameBrowserTest() = default;
RenderWidgetHostViewChildFrameBrowserTest(
const RenderWidgetHostViewChildFrameBrowserTest&) = delete;
RenderWidgetHostViewChildFrameBrowserTest& operator=(
const RenderWidgetHostViewChildFrameBrowserTest&) = delete;
void SetUpCommandLine(base::CommandLine* command_line) override {
IsolateAllSitesForTesting(command_line);
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
// Tests that the FrameSinkId of each child frame has been updated by the
// RenderFrameProxy.
void CheckFrameSinkId(RenderFrameHost* render_frame_host) {
RenderWidgetHostViewBase* child_view =
static_cast<RenderFrameHostImpl*>(render_frame_host)
->GetRenderWidgetHost()
->GetView();
// Only interested in updated FrameSinkIds on child frames.
if (!child_view || !child_view->IsRenderWidgetHostViewChildFrame())
return;
// Ensure that the received viz::FrameSinkId was correctly set on the child
// frame.
viz::FrameSinkId actual_frame_sink_id_ = child_view->GetFrameSinkId();
EXPECT_EQ(expected_frame_sink_id_, actual_frame_sink_id_);
// The viz::FrameSinkID will be replaced while the test blocks for
// navigation. It should differ from the information stored in the child's
// RenderWidgetHost.
EXPECT_NE(base::checked_cast<uint32_t>(
child_view->GetRenderWidgetHost()->GetProcess()->GetID()),
actual_frame_sink_id_.client_id());
EXPECT_NE(base::checked_cast<uint32_t>(
child_view->GetRenderWidgetHost()->GetRoutingID()),
actual_frame_sink_id_.sink_id());
}
Portal* CreatePortalToUrl(WebContentsImpl* host_contents,
GURL portal_url,
int number_of_navigations) {
EXPECT_GE(number_of_navigations, 1);
RenderFrameHostImpl* main_frame = host_contents->GetMainFrame();
// Create portal and wait for navigation.
PortalCreatedObserver portal_created_observer(main_frame);
TestNavigationObserver navigation_observer(nullptr, number_of_navigations);
navigation_observer.set_wait_event(
TestNavigationObserver::WaitEvent::kNavigationFinished);
navigation_observer.StartWatchingNewWebContents();
EXPECT_TRUE(ExecJs(
main_frame, JsReplace("{"
" let portal = document.createElement('portal');"
" portal.src = $1;"
" portal.setAttribute('id', 'portal');"
" document.body.appendChild(portal);"
"}",
portal_url)));
Portal* portal = portal_created_observer.WaitUntilPortalCreated();
navigation_observer.StopWatchingNewWebContents();
WebContentsImpl* portal_contents = portal->GetPortalContents();
EXPECT_TRUE(portal_contents);
navigation_observer.WaitForNavigationFinished();
EXPECT_TRUE(WaitForLoadStop(portal_contents));
return portal;
}
void GiveItSomeTime() {
base::RunLoop run_loop;
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
}
void set_expected_frame_sink_id(viz::FrameSinkId frame_sink_id) {
expected_frame_sink_id_ = frame_sink_id;
}
private:
base::test::ScopedFeatureList scoped_feature_list_{blink::features::kPortals};
viz::FrameSinkId expected_frame_sink_id_;
};
// Tests that the screen is properly reflected for RWHVChildFrame.
IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewChildFrameBrowserTest, Screen) {
GURL main_url(embedded_test_server()->GetURL("/site_per_process_main.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Load cross-site page into iframe.
GURL cross_site_url(
embedded_test_server()->GetURL("foo.com", "/title2.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), cross_site_url));
int main_frame_screen_width =
ExecuteScriptAndGetValue(shell()->web_contents()->GetMainFrame(),
"window.screen.width")
.GetInt();
EXPECT_NE(main_frame_screen_width, 0);
auto check_screen_width = [&](RenderFrameHost* frame_host) {
int width =
ExecuteScriptAndGetValue(frame_host, "window.screen.width").GetInt();
EXPECT_EQ(width, main_frame_screen_width);
};
shell()->web_contents()->GetMainFrame()->ForEachRenderFrameHost(
base::BindLambdaForTesting(check_screen_width));
}
// Auto-resize is only implemented for Ash and GuestViews. So we need to inject
// an implementation that actually resizes the top level widget.
class AutoResizeWebContentsDelegate : public WebContentsDelegate {
void ResizeDueToAutoResize(WebContents* web_contents,
const gfx::Size& new_size) override {
RenderWidgetHostView* view =
web_contents->GetTopLevelRenderWidgetHostView();
view->SetSize(new_size);
}
};
// Test that the |visible_viewport_size| from the top frame is propagated to all
// local roots (aka RenderWidgets):
// a) Initially upon adding the child frame (we create this scenario by
// navigating a child on b.com to c.com, where we already have a RenderProcess
// active for c.com).
// b) When resizing the top level widget.
// c) When auto-resize is enabled for the top level main frame and the renderer
// resizes the top level widget.
// d) When auto-resize is enabled for the nested main frame and the renderer
// resizes the nested widget.
// See https://crbug.com/726743 and https://crbug.com/1050635.
IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewChildFrameBrowserTest,
VisualPropertiesPropagation_VisibleViewportSize) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b,c)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
auto* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
RenderWidgetHostView* root_view =
root->current_frame_host()->GetRenderWidgetHost()->GetView();
// We wait for the main frame and the two subframes.
int number_of_navigations = 3;
Portal* portal =
CreatePortalToUrl(web_contents, main_url, number_of_navigations);
WebContentsImpl* nested_contents = portal->GetPortalContents();
FrameTreeNode* nested_root = nested_contents->GetPrimaryFrameTree().root();
RenderWidgetHostView* nested_root_view =
nested_root->current_frame_host()->GetRenderWidgetHost()->GetView();
// Watch processes for a.com and c.com, as we will throw away b.com when we
// navigate it below.
auto* root_rwh = root->current_frame_host()->GetRenderWidgetHost();
auto* child_rwh =
root->child_at(1)->current_frame_host()->GetRenderWidgetHost();
ASSERT_NE(root_rwh->GetProcess(), child_rwh->GetProcess());
auto* nested_root_rwh =
nested_root->current_frame_host()->GetRenderWidgetHost();
auto* nested_child_rwh =
nested_root->child_at(1)->current_frame_host()->GetRenderWidgetHost();
ASSERT_NE(nested_root_rwh->GetProcess(), nested_child_rwh->GetProcess());
const gfx::Size initial_size = root_view->GetVisibleViewportSize();
const gfx::Size nested_initial_size =
nested_root_view->GetVisibleViewportSize();
ASSERT_NE(initial_size, nested_initial_size);
// We should see the top level widget's size in the visible_viewport_size
// in both local roots. When a child local root is added in the parent
// renderer, the value is propagated down through the browser to the child
// renderer's RenderWidget.
//
// This property is not directly visible in the renderer, so we can only
// check that the value is sent to the appropriate RenderWidget.
{
GURL cross_site_url(
embedded_test_server()->GetURL("c.com", "/title2.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), cross_site_url));
// Wait to see the size sent to the child RenderWidget.
while (true) {
absl::optional<blink::VisualProperties> properties =
child_rwh->LastComputedVisualProperties();
if (properties && properties->visible_viewport_size == initial_size)
break;
base::RunLoop().RunUntilIdle();
}
}
// Same check as above but for a nested WebContents.
{
GURL cross_site_url(
embedded_test_server()->GetURL("c.com", "/title2.html"));
EXPECT_TRUE(
NavigateToURLFromRenderer(nested_root->child_at(0), cross_site_url));
// Wait to see the size sent to the child RenderWidget.
while (true) {
absl::optional<blink::VisualProperties> properties =
nested_child_rwh->LastComputedVisualProperties();
if (properties &&
properties->visible_viewport_size == nested_initial_size)
break;
base::RunLoop().RunUntilIdle();
}
}
// This part of the test does not work well on Android, for a few reasons:
// 1. RenderWidgetHostViewAndroid can not be resized, the Java objects need to
// be resized somehow through ui::ViewAndroid.
// 2. AutoResize on Android does not size to the min/max bounds specified, it
// ends up ignoring them and sizing to the screen (I think).
// Luckily this test is verifying interactions and behaviour of
// RenderWidgetHostImpl - RenderWidget - RenderFrameProxy -
// CrossProcessFrameConnector, and this isn't Android-specific code.
#if !BUILDFLAG(IS_ANDROID)
// Resize the top level widget to cause its |visible_viewport_size| to be
// changed. The change should propagate down to the child RenderWidget.
{
const gfx::Size resize_to(initial_size.width() - 10,
initial_size.height() - 10);
root_view->SetSize(resize_to);
// Wait to see both RenderWidgets receive the message.
while (true) {
absl::optional<blink::VisualProperties> properties =
root_rwh->LastComputedVisualProperties();
if (properties && properties->visible_viewport_size == resize_to)
break;
base::RunLoop().RunUntilIdle();
}
while (true) {
absl::optional<blink::VisualProperties> properties =
child_rwh->LastComputedVisualProperties();
if (properties && properties->visible_viewport_size == resize_to)
break;
base::RunLoop().RunUntilIdle();
}
}
// Same check as above but resizing the nested WebContents' main frame
// instead.
// Resize the top level widget to cause its |visible_viewport_size| to be
// changed. The change should propagate down to the child RenderWidget.
{
const gfx::Size resize_to(nested_initial_size.width() - 10,
nested_initial_size.height() - 10);
EXPECT_TRUE(ExecJs(
root->current_frame_host(),
JsReplace("document.getElementById('portal').style.width = '$1px';"
"document.getElementById('portal').style.height = '$2px';",
resize_to.width(), resize_to.height())));
// Wait to see both RenderWidgets receive the message.
while (true) {
absl::optional<blink::VisualProperties> properties =
nested_root_rwh->LastComputedVisualProperties();
if (properties && properties->visible_viewport_size == resize_to)
break;
base::RunLoop().RunUntilIdle();
}
while (true) {
absl::optional<blink::VisualProperties> properties =
nested_child_rwh->LastComputedVisualProperties();
if (properties && properties->visible_viewport_size == resize_to)
break;
base::RunLoop().RunUntilIdle();
}
}
// Informs the top-level frame it can auto-resize. It will shrink down to its
// minimum window size based on the empty content we've loaded, which is
// 100x100.
//
// When the renderer resizes, thanks to our AutoResizeWebContentsDelegate
// the top level widget will resize. That size will be reflected in the
// visible_viewport_size which is sent back through the top level RenderWidget
// to propagte down to child local roots.
//
// This property is not directly visible in the renderer, so we can only
// check that the value is sent to both RenderWidgets.
{
const gfx::Size auto_resize_to(105, 100);
// Replace the WebContentsDelegate so that we can use the auto-resize
// changes to adjust the size of the top widget.
WebContentsDelegate* old_delegate = shell()->web_contents()->GetDelegate();
AutoResizeWebContentsDelegate resize_delegate;
shell()->web_contents()->SetDelegate(&resize_delegate);
root_view->EnableAutoResize(auto_resize_to, auto_resize_to);
// Wait for the renderer side to resize itself and the RenderWidget
// waterfall to pass the new |visible_viewport_size| down.
while (true) {
absl::optional<blink::VisualProperties> properties =
root_rwh->LastComputedVisualProperties();
if (properties && properties->visible_viewport_size == auto_resize_to)
break;
base::RunLoop().RunUntilIdle();
}
while (true) {
absl::optional<blink::VisualProperties> properties =
child_rwh->LastComputedVisualProperties();
if (properties && properties->visible_viewport_size == auto_resize_to)
break;
base::RunLoop().RunUntilIdle();
}
shell()->web_contents()->SetDelegate(old_delegate);
}
// TODO(danakj): We'd like to run the same check as above but tell the main
// frame of the nested WebContents that it can auto-resize. However this seems
// to get through to the main frame's RenderWidget and propagate correctly but
// no size change occurs in the renderer.
#endif
}
// Validate that OOPIFs receive presentation feedbacks.
IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewChildFrameBrowserTest,
PresentationFeedback) {
base::HistogramTester histogram_tester;
GURL main_url(embedded_test_server()->GetURL("/site_per_process_main.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Load cross-site page into iframe.
GURL cross_site_url(
embedded_test_server()->GetURL("foo.com", "/title2.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), cross_site_url));
const auto trigger_subframe_tab_switch = [&root]() -> bool {
auto* child_rwh_impl =
root->child_at(0)->current_frame_host()->GetRenderWidgetHost();
// Hide the frame and make it visible again, to force it to record the
// tab-switch time, which is generated from presentation-feedback.
child_rwh_impl->WasHidden();
child_rwh_impl->WasShown(
blink::mojom::RecordContentToVisibleTimeRequest::New(
base::TimeTicks::Now(),
/* destination_is_loaded */ true,
/* show_reason_tab_switching */ true,
/* show_reason_unoccluded */ false,
/* show_reason_bfcache_restore */ false));
// Force the child to submit a new frame.
return ExecJs(root->child_at(0)->current_frame_host(),
"document.write('Force a new frame.');");
};
ASSERT_TRUE(trigger_subframe_tab_switch());
bool got_incomplete_tab_switch = false;
const base::TimeTicks start_time = base::TimeTicks::Now();
do {
if (base::TimeTicks::Now() - start_time > TestTimeouts::action_timeout()) {
FAIL()
<< "Timed out waiting for Browser.Tabs.TotalSwitchDuration. Received "
"these histograms instead: "
<< ::testing::PrintToString(
histogram_tester.GetTotalCountsForPrefix("Browser.Tabs."));
}
FetchHistogramsFromChildProcesses();
GiveItSomeTime();
// Work around a race condition while loading the cross-site iframe.
//
// The NavigateToURLFromRenderer call above replaces the
// LocalFrame/LocalFrameView in renderer process A with a
// RemoteFrame/RemoteFrameView proxy for the frame which is now hosted in
// renderer process B. During initialization the RemoteFrameView sends a
// series of VisibilityChanged messages to the browser process, which cause
// CrossProcessFrameConnector to call WasHidden and then WasShown on
// `child_rwh_impl`. Depending on the timing these might arrive during the
// NavigateToURLFromRenderer call or the ExecJS call above, both of which
// pump the message loop. If CrossProcessFrameConnector calls WasHidden
// after the WasShown call above, it will cancel the simulated tab switch.
// This causes ContentToVisibleTimeReporter to log
// TotalIncompleteSwitchDuration, which is not based on
// PresentationFeedback, instead of TotalSwitchDuration. See
// crbug.com/1288560 for more details.
//
// The race condition can only cause a single incomplete tab switch, so
// only check for this once. If the second simulated tab switch is also
// cancelled something else is wrong, so the loop will time out and fail
// the test.
//
// TODO(crbug.com/1288560): Remove this once the race condition is
// fixed.
if (!got_incomplete_tab_switch &&
histogram_tester
.GetTotalCountsForPrefix(
"Browser.Tabs.TotalIncompleteSwitchDuration")
.size() == 1) {
LOG(ERROR) << "Incomplete tab switch - try again.";
got_incomplete_tab_switch = true;
ASSERT_TRUE(trigger_subframe_tab_switch());
}
// Once the tab switch completes the PresentationFeedback should cause a
// single TotalSwitchDuration histogram to be logged.
} while (histogram_tester
.GetTotalCountsForPrefix("Browser.Tabs.TotalSwitchDuration")
.size() != 1);
}
// Auto-resize is only implemented for Ash and GuestViews. So we need to inject
// an implementation that actually resizes the top level widget.
class DisplayModeControllingWebContentsDelegate : public WebContentsDelegate {
public:
blink::mojom::DisplayMode GetDisplayMode(
const WebContents* web_contents) override {
return mode_;
}
void set_display_mode(blink::mojom::DisplayMode mode) { mode_ = mode; }
private:
// The is the default value throughout the browser and renderer.
blink::mojom::DisplayMode mode_ = blink::mojom::DisplayMode::kBrowser;
};
// TODO(crbug.com/1060336): Unlike most VisualProperties, the DisplayMode does
// not propagate down the tree of RenderWidgets, but is sent independently to
// each RenderWidget.
IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewChildFrameBrowserTest,
VisualPropertiesPropagation_DisplayMode) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(a))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
auto* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents());
DisplayModeControllingWebContentsDelegate display_mode_delegate;
shell()->web_contents()->SetDelegate(&display_mode_delegate);
// Main frame.
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
RenderWidgetHostImpl* root_widget =
root->current_frame_host()->GetRenderWidgetHost();
// Out-of-process frame.
FrameTreeNode* oopchild = root->child_at(0);
RenderWidgetHostImpl* oopchild_widget =
oopchild->current_frame_host()->GetRenderWidgetHost();
// In-process frame.
FrameTreeNode* ipchild = oopchild->child_at(0);
RenderWidgetHostImpl* ipchild_widget =
ipchild->current_frame_host()->GetRenderWidgetHost();
EXPECT_NE(root_widget, ipchild_widget);
// Check all frames for the initial value.
EXPECT_EQ(
true,
EvalJs(root, "window.matchMedia('(display-mode: browser)').matches"));
EXPECT_EQ(
true,
EvalJs(oopchild, "window.matchMedia('(display-mode: browser)').matches"));
EXPECT_EQ(
true,
EvalJs(ipchild, "window.matchMedia('(display-mode: browser)').matches"));
// The display mode changes.
display_mode_delegate.set_display_mode(
blink::mojom::DisplayMode::kStandalone);
// Each RenderWidgetHost would need to hear about that by having
// SynchronizeVisualProperties() called. It's not clear what triggers that but
// the place that changes the DisplayMode would be responsible.
//
// We ignore the pending ack to ensure this IPC is sent immediately.
EXPECT_TRUE(root_widget->SynchronizeVisualPropertiesIgnoringPendingAck());
EXPECT_TRUE(oopchild_widget->SynchronizeVisualPropertiesIgnoringPendingAck());
EXPECT_TRUE(ipchild_widget->SynchronizeVisualPropertiesIgnoringPendingAck());
// Check all frames for the changed value.
EXPECT_EQ(
true,
EvalJsAfterLifecycleUpdate(
root, "", "window.matchMedia('(display-mode: standalone)').matches"));
EXPECT_EQ(true,
EvalJsAfterLifecycleUpdate(
oopchild, "",
"window.matchMedia('(display-mode: standalone)').matches"));
EXPECT_EQ(true,
EvalJsAfterLifecycleUpdate(
ipchild, "",
"window.matchMedia('(display-mode: standalone)').matches"));
// Navigate a frame to b.com, which we already have a process for.
GURL same_site_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), same_site_url));
// The navigated frame sees the correct (non-default) value.
EXPECT_EQ(true,
EvalJs(root->child_at(0),
"window.matchMedia('(display-mode: standalone)').matches"));
// Navigate the frame to c.com, which we don't have a process for.
GURL cross_site_url(embedded_test_server()->GetURL("c.com", "/title2.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), cross_site_url));
// The navigated frame sees the correct (non-default) value.
EXPECT_EQ(true,
EvalJs(root->child_at(0),
"window.matchMedia('(display-mode: standalone)').matches"));
}
// Validate that the root widget's window segments are correctly propagated
// via the SynchronizeVisualProperties cascade.
// Flaky on Mac, Linux and Android (http://crbug/1089994).
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || \
BUILDFLAG(IS_ANDROID)
#define MAYBE_VisualPropertiesPropagation_RootWindowSegments \
DISABLED_VisualPropertiesPropagation_RootWindowSegments
#else
#define MAYBE_VisualPropertiesPropagation_RootWindowSegments \
VisualPropertiesPropagation_RootWindowSegments
#endif
IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewChildFrameBrowserTest,
MAYBE_VisualPropertiesPropagation_RootWindowSegments) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c),a)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
auto* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents());
// Main frame/root view.
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
RenderWidgetHostImpl* root_widget =
root->current_frame_host()->GetRenderWidgetHost();
RenderWidgetHostViewBase* root_view = root_widget->GetView();
// Out-of-process child frame.
FrameTreeNode* oopchild = root->child_at(0);
// Out-of-process descendant frame (child of the first oop-iframe).
FrameTreeNode* oopdescendant = oopchild->child_at(0);
const gfx::Size root_view_size = root_view->GetVisibleViewportSize();
const int kDisplayFeatureLength = 10;
DisplayFeature emulated_display_feature{
DisplayFeature::Orientation::kVertical,
/* offset */ root_view_size.width() / 2 - kDisplayFeatureLength / 2,
/* mask_length */ kDisplayFeatureLength};
std::vector<gfx::Rect> expected_segments;
expected_segments.emplace_back(0, 0, emulated_display_feature.offset,
root_view_size.height());
const int second_segment_offset =
emulated_display_feature.offset + emulated_display_feature.mask_length;
expected_segments.emplace_back(second_segment_offset, 0,
root_view_size.width() - second_segment_offset,
root_view_size.height());
absl::optional<blink::VisualProperties> properties =
oopchild->current_frame_host()
->GetRenderWidgetHost()
->LastComputedVisualProperties();
EXPECT_TRUE(properties);
EXPECT_TRUE(properties->local_surface_id);
viz::LocalSurfaceId oopchild_initial_lsid =
properties->local_surface_id.value();
properties = oopdescendant->current_frame_host()
->GetRenderWidgetHost()
->LastComputedVisualProperties();
EXPECT_TRUE(properties);
EXPECT_TRUE(properties->local_surface_id);
viz::LocalSurfaceId oopdescendant_initial_lsid =
properties->local_surface_id.value();
{
// Watch for visual properties changes, first to the child oop-iframe, then
// to the descendant (at which point we're done and can validate the
// values).
MockDisplayFeature mock_display_feature(root_view);
mock_display_feature.SetDisplayFeature(&emulated_display_feature);
root_widget->SynchronizeVisualProperties();
while (true) {
properties = oopchild->current_frame_host()
->GetRenderWidgetHost()
->LastComputedVisualProperties();
if (properties && properties->local_surface_id &&
oopchild_initial_lsid < properties->local_surface_id) {
EXPECT_EQ(properties->root_widget_window_segments, expected_segments);
break;
}
base::RunLoop().RunUntilIdle();
}
while (true) {
properties = oopdescendant->current_frame_host()
->GetRenderWidgetHost()
->LastComputedVisualProperties();
if (properties && properties->local_surface_id &&
oopdescendant_initial_lsid < properties->local_surface_id) {
EXPECT_EQ(properties->root_widget_window_segments, expected_segments);
break;
}
base::RunLoop().RunUntilIdle();
}
}
{
// Creating a new local frame root (navigating a.com to c.com) should also
// propagate the property to the new local frame root. Note that we're
// re-using the existing RenderProcessHost from c.com (aka
// |oopdescendant_rph|).
GURL new_frame_url(embedded_test_server()->GetURL("c.com", "/title2.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(1), new_frame_url));
while (true) {
properties = oopdescendant->current_frame_host()
->GetRenderWidgetHost()
->LastComputedVisualProperties();
// This check is needed, since we'll get an IPC originating from
// RenderWidgetHostImpl immediately after the frame is added with the
// incorrect value (the segments are cascaded from the parent renderer
// when the frame is added in that process). So we need to wait for
// the outgoing VisualProperties triggered from the parent renderer
// and comes in via the CrossProcessFrameConnector, which can happen
// after NavigateToURLFromRenderer completes.
if (properties &&
properties->root_widget_window_segments == expected_segments)
break;
base::RunLoop().RunUntilIdle();
}
}
}
IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewChildFrameBrowserTest,
SetTextDirection) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a,b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
auto* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents());
// Main frame.
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
RenderWidgetHostImpl* root_widget =
root->current_frame_host()->GetRenderWidgetHost();
ASSERT_TRUE(ExecJs(root->current_frame_host(),
"var elem = document.createElement('input'); "
"elem.id = 'mainframe_input_id';"
"document.body.appendChild(elem);"
"document.getElementById('mainframe_input_id').focus();"));
root_widget->UpdateTextDirection(base::i18n::RIGHT_TO_LEFT);
root_widget->NotifyTextDirection();
std::string mainframe_input_element_dir =
ExecuteScriptAndGetValue(
root->current_frame_host(),
"document.getElementById('mainframe_input_id').dir")
.GetString();
EXPECT_EQ(mainframe_input_element_dir, "rtl");
// In-process frame.
FrameTreeNode* ipchild = root->child_at(0);
RenderWidgetHostImpl* ipchild_widget =
ipchild->current_frame_host()->GetRenderWidgetHost();
ASSERT_TRUE(ExecJs(ipchild->current_frame_host(),
"var elem = document.createElement('input'); "
"elem.id = 'ipchild_input_id';"
"document.body.appendChild(elem);"
"document.getElementById('ipchild_input_id').focus();"));
ipchild_widget->UpdateTextDirection(base::i18n::LEFT_TO_RIGHT);
ipchild_widget->NotifyTextDirection();
std::string ip_input_element_dir =
ExecuteScriptAndGetValue(
ipchild->current_frame_host(),
"document.getElementById('ipchild_input_id').dir")
.GetString();
EXPECT_EQ(ip_input_element_dir, "ltr");
// Out-of-process frame.
FrameTreeNode* oopchild = root->child_at(1);
RenderWidgetHostImpl* oopchild_widget =
oopchild->current_frame_host()->GetRenderWidgetHost();
ASSERT_TRUE(ExecJs(oopchild->current_frame_host(),
"var elem = document.createElement('input'); "
"elem.id = 'oop_input_id';"
"document.body.appendChild(elem);"
"document.getElementById('oop_input_id').focus();"));
oopchild_widget->UpdateTextDirection(base::i18n::RIGHT_TO_LEFT);
oopchild_widget->NotifyTextDirection();
std::string oop_input_element_dir =
ExecuteScriptAndGetValue(oopchild->current_frame_host(),
"document.getElementById('oop_input_id').dir")
.GetString();
EXPECT_EQ(oop_input_element_dir, "rtl");
// In case of UNKNOWN_DIRECTION, old value of direction is maintained.
oopchild_widget->UpdateTextDirection(base::i18n::UNKNOWN_DIRECTION);
oopchild_widget->NotifyTextDirection();
oop_input_element_dir =
ExecuteScriptAndGetValue(oopchild->current_frame_host(),
"document.getElementById('oop_input_id').dir")
.GetString();
EXPECT_EQ(oop_input_element_dir, "rtl");
}
} // namespace
} // namespace content