#include <memory>
#include <string>
#include "base/json/json_reader.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "build/build_config.h"
#include "content/browser/renderer_host/render_widget_host_view_child_frame.h"
#include "content/browser/web_contents/web_contents_impl.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_utils.h"
#include "content/shell/browser/shell.h"
#include "content/shell/browser/shell_content_browser_client.h"
#include "content/shell/common/shell_switches.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/scroll/scroll_into_view_params.mojom.h"
#include "third_party/re2/src/re2/re2.h"
#include "ui/events/event_constants.h"
#include "url/gurl.h"
#if defined(USE_AURA)
#include "content/browser/renderer_host/render_widget_host_view_aura.h"
#endif
#define EXPECT_TRUE_OR_FAIL(condition) \
EXPECT_TRUE(condition); \
if (!condition) \
return false;
namespace content {
namespace {
enum TestFrameType { kLocalFrame, kRemoteFrame };
enum TestWritingMode { kLTR, kRTL };
enum TestInvokeMethod { kJavaScript, kInputHandler, kAuraOnScreenKeyboard };
[[maybe_unused]] std::string DescribeFrameType(
const testing::TestParamInfo<TestFrameType>& info) {
std::string frame_type;
switch (info.param) {
case kLocalFrame: {
frame_type = "LocalFrame";
} break;
case kRemoteFrame: {
frame_type = "RemoteFrame";
} break;
}
return frame_type;
}
blink::mojom::FrameWidgetInputHandler* GetInputHandler(FrameTreeNode* node) {
return node->current_frame_host()
->GetRenderWidgetHost()
->GetFrameWidgetInputHandler();
}
class ScopedFocusScrollWaiter {
public:
explicit ScopedFocusScrollWaiter(FrameTreeNode* node) {
DCHECK(node->IsOutermostMainFrame());
GetInputHandler(node)->WaitForPageScaleAnimationForTesting(
run_loop_.QuitClosure());
}
~ScopedFocusScrollWaiter() { run_loop_.Run(); }
private:
base::RunLoop run_loop_;
};
class ScopedSuppressImeEvents {
public:
explicit ScopedSuppressImeEvents(WebContentsImpl* web_contents)
: web_contents_(web_contents->GetWeakPtr()) {
web_contents->set_suppress_ime_events_for_testing(true);
}
~ScopedSuppressImeEvents() {
if (!web_contents_)
return;
static_cast<WebContentsImpl*>(web_contents_.get())
->set_suppress_ime_events_for_testing(false);
}
base::WeakPtr<WebContents> web_contents_;
};
class ScrollRectToVisibleInParentFrameInterceptor
: public blink::mojom::LocalFrameHostInterceptorForTesting {
public:
ScrollRectToVisibleInParentFrameInterceptor() = default;
~ScrollRectToVisibleInParentFrameInterceptor() override = default;
void Init(RenderFrameHostImpl* render_frame_host) {
render_frame_host_ = render_frame_host;
std::ignore = render_frame_host_->local_frame_host_receiver_for_testing()
.SwapImplForTesting(this);
}
blink::mojom::LocalFrameHost* GetForwardingInterface() override {
return render_frame_host_;
}
void ScrollRectToVisibleInParentFrame(
const gfx::RectF& rect_to_scroll,
blink::mojom::ScrollIntoViewParamsPtr params) override {
has_called_method_ = true;
}
bool HasCalledScrollRectToVisibleInParentFrame() const {
return has_called_method_;
}
private:
raw_ptr<RenderFrameHostImpl> render_frame_host_;
bool has_called_method_ = false;
};
class ScrollIntoViewBrowserTestBase : public ContentBrowserTest {
public:
ScrollIntoViewBrowserTestBase() = default;
~ScrollIntoViewBrowserTestBase() override = default;
virtual bool IsForceLocalFrames() const = 0;
virtual bool IsWritingModeLTR() const = 0;
virtual TestInvokeMethod GetInvokeMethod() const = 0;
virtual net::EmbeddedTestServer* server() { return embedded_test_server(); }
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(server()->Start());
suppress_ime_ = std::make_unique<ScopedSuppressImeEvents>(web_contents());
}
void TearDownOnMainThread() override {
suppress_ime_.reset();
ContentBrowserTest::TearDownOnMainThread();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
ContentBrowserTest::SetUpCommandLine(command_line);
IsolateAllSitesForTesting(command_line);
command_line->AppendSwitch(switches::kExposeInternalsForTesting);
}
WebContentsImpl* web_contents() {
return static_cast<WebContentsImpl*>(shell()->web_contents());
}
FrameTreeNode* InnerMostFrameTreeNode() {
FrameTreeNode* inner_most_node = nullptr;
ForEachFrameFromRootToInnerMost(
[&inner_most_node](FrameTreeNode* node) { inner_most_node = node; });
return inner_most_node;
}
FrameTreeNode* RootFrameTreeNode() {
return web_contents()->GetPrimaryFrameTree().root();
}
gfx::RectF GetClientRect(FrameTreeNode* node, std::string query) {
auto result = EvalJs(node, JsReplace(R"JS(
JSON.stringify(document.querySelector($1).getBoundingClientRect());
)JS",
query));
absl::optional<base::Value> value =
base::JSONReader::Read(result.ExtractString());
CHECK(value.has_value());
CHECK(value->is_dict());
const base::Value::Dict& dict = value->GetDict();
absl::optional<double> x = dict.FindDouble("x");
absl::optional<double> y = dict.FindDouble("y");
absl::optional<double> width = dict.FindDouble("width");
absl::optional<double> height = dict.FindDouble("height");
CHECK(x);
CHECK(y);
CHECK(width);
CHECK(height);
return gfx::RectF(*x, *y, *width, *height);
}
gfx::RectF GetLayoutViewportRect() {
return GetClientRect(RootFrameTreeNode(), ".layoutViewport");
}
struct VisualViewport {
double offset_left;
double offset_top;
double width;
double height;
double scale;
double page_left;
double page_top;
gfx::RectF rect;
};
VisualViewport GetVisualViewport() {
auto result = EvalJs(RootFrameTreeNode(), R"JS(
JSON.stringify({
offsetLeft: visualViewport.offsetLeft,
offsetTop: visualViewport.offsetTop,
width: visualViewport.width,
height: visualViewport.height,
scale: visualViewport.scale,
pageLeft: visualViewport.pageLeft,
pageTop: visualViewport.pageTop});
)JS");
absl::optional<base::Value> value =
base::JSONReader::Read(result.ExtractString());
CHECK(value.has_value());
CHECK(value->is_dict());
const base::Value::Dict& dict = value->GetDict();
absl::optional<double> offset_left = dict.FindDouble("offsetLeft");
absl::optional<double> offset_top = dict.FindDouble("offsetTop");
absl::optional<double> width = dict.FindDouble("width");
absl::optional<double> height = dict.FindDouble("height");
absl::optional<double> scale = dict.FindDouble("scale");
absl::optional<double> page_left = dict.FindDouble("pageLeft");
absl::optional<double> page_top = dict.FindDouble("pageTop");
CHECK(offset_left);
CHECK(offset_top);
CHECK(width);
CHECK(height);
CHECK(scale);
CHECK(page_left);
CHECK(page_top);
VisualViewport values;
values.offset_left = *offset_left;
values.offset_top = *offset_top;
values.width = *width;
values.height = *height;
values.scale = *scale;
values.page_left = *page_left;
values.page_top = *page_top;
values.rect = gfx::RectF(
gfx::PointF(),
gfx::ScaleSize(gfx::SizeF(values.width, values.height), values.scale));
return values;
}
gfx::RectF GetCaretRectInViewport() {
FrameTreeNode* node = InnerMostFrameTreeNode();
gfx::RectF rect = GetClientRect(node, "input");
constexpr float kCaretBoxWidth = 30.f;
if (IsWritingModeLTR()) {
rect.Inset(gfx::InsetsF::TLBR(0, 0, 0, rect.width() - kCaretBoxWidth));
} else {
rect.Inset(gfx::InsetsF::TLBR(0, rect.width() - kCaretBoxWidth, 0, 0));
}
EXPECT_EQ("", EvalJs(node, "document.querySelector('input').value"))
<< "Caret location is assumed based on empty <input> value";
FrameTreeNode* frame =
FrameTreeNode::From(node->GetParentOrOuterDocument());
while (frame) {
gfx::RectF parent_rect = GetClientRect(frame, "#childframe");
rect.Offset(parent_rect.OffsetFromOrigin());
rect = gfx::IntersectRects(parent_rect, rect);
frame = FrameTreeNode::From(frame->GetParentOrOuterDocument());
}
gfx::RectF root_frame_rect = GetLayoutViewportRect();
root_frame_rect.set_origin(gfx::PointF());
rect = gfx::IntersectRects(root_frame_rect, rect);
VisualViewport visual_viewport = GetVisualViewport();
rect.Offset(-visual_viewport.offset_left, -visual_viewport.offset_top);
rect.Scale(visual_viewport.scale);
rect = gfx::IntersectRects(visual_viewport.rect, rect);
return rect;
}
gfx::RectF GetAcceptableCaretRect() {
gfx::RectF caret_in_viewport = GetCaretRectInViewport();
VisualViewport visual_viewport = GetVisualViewport();
gfx::RectF rect = visual_viewport.rect;
const float kVerticalInset =
((rect.height() - caret_in_viewport.height()) / 2.f) - 40.f;
const float kHorizontalInset = 0.f;
rect.Inset(gfx::InsetsF::VH(kVerticalInset, kHorizontalInset));
return rect;
}
GURL GetMainURLForFrameTree(std::string frame_tree_string) {
re2::RE2::GlobalReplace(&frame_tree_string, "\\s*", "");
re2::RE2::GlobalReplace(&frame_tree_string, "{}", "");
if (IsForceLocalFrames()) {
re2::RE2::GlobalReplace(&frame_tree_string, "site[A-Z]", "siteA");
}
if (!IsWritingModeLTR()) {
re2::RE2::GlobalReplace(&frame_tree_string, "{(.*?)}", "{RTL,\\1}");
{
std::string regex =
"(site[A-Z])"
"([^{]|$)";
re2::RE2::GlobalReplace(&frame_tree_string, regex, "\\1{RTL}\\2");
}
}
return server()->GetURL(
"a.test", base::StrCat({"/cross_site_scroll_into_view_factory.html?",
frame_tree_string}));
}
void SetAuraOnScreenKeyboardInset(int keyboard_height) {
#if defined(USE_AURA)
RenderWidgetHostViewBase* inner_most_view = InnerMostFrameTreeNode()
->current_frame_host()
->GetRenderWidgetHost()
->GetView();
RenderWidgetHostViewBase* root_view = inner_most_view->GetRootView();
root_view->SetLastPointerType(ui::EventPointerType::kTouch);
root_view->SetInsets(gfx::Insets::TLBR(0, 0, keyboard_height, 0));
#else
NOTREACHED();
#endif
}
template <typename Function>
void ForEachFrameFromRootToInnerMost(const Function& func) {
FrameTreeNode* node = web_contents()->GetPrimaryFrameTree().root();
while (node) {
bool is_proxy_for_inner_frame_tree =
node->current_frame_host()->inner_tree_main_frame_tree_node_id() !=
FrameTreeNode::kFrameTreeNodeInvalidId;
if (!is_proxy_for_inner_frame_tree)
func(node);
if (node->child_count()) {
CHECK_EQ(
node->current_frame_host()->inner_tree_main_frame_tree_node_id(),
FrameTreeNode::kFrameTreeNodeInvalidId);
CHECK_EQ(node->child_count(), 1ul);
node = node->child_at(0);
} else if (is_proxy_for_inner_frame_tree) {
CHECK_EQ(node->child_count(), 0ul);
node = FrameTreeNode::GloballyFindByID(
node->current_frame_host()->inner_tree_main_frame_tree_node_id());
} else {
node = nullptr;
}
}
}
void EnsureAllFramesCompletedLifecycle() {
ForEachFrameFromRootToInnerMost([](FrameTreeNode* node) {
base::RunLoop loop;
node->current_frame_host()->InsertVisualStateCallback(
base::BindLambdaForTesting(
[&loop](bool visual_state_updated) { loop.Quit(); }));
loop.Run();
EXPECT_TRUE(ExecJs(node, R"JS(
if (document.getElementById('childframe'))
document.getElementById('childframe').scrollIntoView()
)JS"));
});
ForEachFrameFromRootToInnerMost([](FrameTreeNode* node) {
EXPECT_TRUE(ExecJs(node, "window.scrollTo(0, 0)"));
});
}
bool SetupTest(std::string frame_tree) {
const GURL kMainUrl(GetMainURLForFrameTree(frame_tree));
if (!NavigateToURL(shell(), kMainUrl))
return false;
EnsureAllFramesCompletedLifecycle();
VisualViewport viewport = GetVisualViewport();
double page_scale_factor_before = viewport.scale;
double page_left_before = viewport.page_left;
double page_top_before = viewport.page_top;
if (GetInvokeMethod() == kInputHandler ||
GetInvokeMethod() == kAuraOnScreenKeyboard) {
EXPECT_TRUE_OR_FAIL(ExecJs(InnerMostFrameTreeNode(), R"JS(
document.querySelector('input').focus({preventScroll: true});
)JS"));
}
viewport = GetVisualViewport();
CHECK_EQ(viewport.scale, page_scale_factor_before);
CHECK_EQ(viewport.page_left, page_left_before);
CHECK_EQ(viewport.page_top, page_top_before);
return true;
}
void RunTest() {
switch (GetInvokeMethod()) {
case kInputHandler: {
ScopedFocusScrollWaiter wait_for_scroll_done(RootFrameTreeNode());
GetInputHandler(InnerMostFrameTreeNode())
->ScrollFocusedEditableNodeIntoView();
} break;
case kAuraOnScreenKeyboard: {
ScopedFocusScrollWaiter wait_for_scroll_done(RootFrameTreeNode());
SetAuraOnScreenKeyboardInset(400);
} break;
case kJavaScript: {
RenderFrameSubmissionObserver frame_observer(web_contents());
EXPECT_TRUE(ExecJs(InnerMostFrameTreeNode(), R"JS(
document.querySelector('input').scrollIntoView({
behavior: 'instant',
block: 'center',
inline: 'center'
})
)JS"));
frame_observer.WaitForScrollOffsetAtTop(
false);
} break;
}
gfx::RectF caret_in_viewport = GetCaretRectInViewport();
gfx::RectF acceptable_rect = GetAcceptableCaretRect();
EXPECT_TRUE(acceptable_rect.Contains(caret_in_viewport))
<< "Expected caret to within [" << acceptable_rect.ToString()
<< "] but caret is [" << caret_in_viewport.ToString() << "]";
}
private:
std::unique_ptr<ScopedSuppressImeEvents> suppress_ime_;
};
class ScrollIntoViewBrowserTest
: public ScrollIntoViewBrowserTestBase,
public ::testing::WithParamInterface<
std::tuple<TestFrameType, TestWritingMode, TestInvokeMethod>> {
public:
bool IsForceLocalFrames() const override {
return std::get<0>(GetParam()) == kLocalFrame;
}
bool IsWritingModeLTR() const override {
return std::get<1>(GetParam()) == kLTR;
}
TestInvokeMethod GetInvokeMethod() const override {
return std::get<2>(GetParam());
}
static std::string DescribeParams(
const testing::TestParamInfo<ParamType>& info) {
auto [frame_type_param, writing_mode_param, invoke_method_param] =
info.param;
std::string frame_type;
switch (frame_type_param) {
case kLocalFrame: {
frame_type = "LocalFrame";
} break;
case kRemoteFrame: {
frame_type = "RemoteFrame";
} break;
}
std::string writing_mode;
switch (writing_mode_param) {
case kLTR: {
writing_mode = "LTR";
} break;
case kRTL: {
writing_mode = "RTL";
} break;
}
std::string invoke_method;
switch (invoke_method_param) {
case kJavaScript: {
invoke_method = "JavaScript";
} break;
case kInputHandler: {
invoke_method = "ScrollFocusedEditableNodeIntoView";
} break;
case kAuraOnScreenKeyboard: {
invoke_method = "AuraOnScreenKeyboard";
} break;
}
return base::StringPrintf("%s_%s_%s", frame_type.c_str(),
writing_mode.c_str(), invoke_method.c_str());
}
};
IN_PROC_BROWSER_TEST_P(ScrollIntoViewBrowserTest, EditableInSingleNestedFrame) {
ASSERT_TRUE(SetupTest("siteA(siteB)"));
RunTest();
}
IN_PROC_BROWSER_TEST_P(ScrollIntoViewBrowserTest, EditableInLocalRoot) {
ASSERT_TRUE(SetupTest("siteA(siteB(siteA))"));
RunTest();
}
IN_PROC_BROWSER_TEST_P(ScrollIntoViewBrowserTest, EditableInDoublyNestedFrame) {
ASSERT_TRUE(SetupTest("siteA(siteB(siteC))"));
RunTest();
}
IN_PROC_BROWSER_TEST_P(
ScrollIntoViewBrowserTest,
CrossesEditableInDoublyNestedFrameLocalAndRemoteBoundaries) {
ASSERT_TRUE(SetupTest("siteA(siteA(siteB(siteB)))"));
RunTest();
}
INSTANTIATE_TEST_SUITE_P(
,
ScrollIntoViewBrowserTest,
testing::Combine(testing::Values(kLocalFrame, kRemoteFrame),
testing::Values(kLTR, kRTL),
testing::Values(kJavaScript, kInputHandler)),
ScrollIntoViewBrowserTest::DescribeParams);
#if defined(USE_AURA)
class InsetScrollIntoViewBrowserTest
: public ScrollIntoViewBrowserTestBase,
public ::testing::WithParamInterface<TestFrameType> {
public:
bool IsForceLocalFrames() const override { return GetParam() == kLocalFrame; }
bool IsWritingModeLTR() const override { return true; }
TestInvokeMethod GetInvokeMethod() const override {
return kAuraOnScreenKeyboard;
}
};
IN_PROC_BROWSER_TEST_P(InsetScrollIntoViewBrowserTest,
InsetsCauseScrollToFocusedEditable) {
ASSERT_TRUE(SetupTest("siteA(siteB(siteC))"));
int contents_height = web_contents()->GetViewBounds().height();
ASSERT_GT(contents_height, 450);
int visual_viewport_height_before = GetVisualViewport().height;
int layout_viewport_height_before = GetLayoutViewportRect().height();
const int kEpsilon = 30;
EXPECT_NEAR(visual_viewport_height_before, contents_height, kEpsilon);
EXPECT_NEAR(layout_viewport_height_before, contents_height, kEpsilon);
EXPECT_EQ(1.f, GetVisualViewport().scale);
RunTest();
EXPECT_EQ(visual_viewport_height_before - GetVisualViewport().height, 400);
EXPECT_EQ(layout_viewport_height_before, GetLayoutViewportRect().height());
EXPECT_EQ(1.f, GetVisualViewport().scale);
ASSERT_LT(GetAcceptableCaretRect().bottom(), 200);
}
INSTANTIATE_TEST_SUITE_P(,
InsetScrollIntoViewBrowserTest,
testing::Values(kLocalFrame, kRemoteFrame),
DescribeFrameType);
#endif
#if BUILDFLAG(IS_ANDROID)
constexpr double kMobileMinimumScale = 0.25;
class ZoomScrollIntoViewBrowserTest
: public ScrollIntoViewBrowserTestBase,
public ::testing::WithParamInterface<TestFrameType> {
public:
bool IsForceLocalFrames() const override { return GetParam() == kLocalFrame; }
bool IsWritingModeLTR() const override { return true; }
TestInvokeMethod GetInvokeMethod() const override { return kInputHandler; }
};
IN_PROC_BROWSER_TEST_P(ZoomScrollIntoViewBrowserTest, DesktopViewportMustZoom) {
ASSERT_TRUE(SetupTest("siteA(siteB)"));
EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);
RunTest();
EXPECT_NEAR(1, GetVisualViewport().scale, 0.05);
}
IN_PROC_BROWSER_TEST_P(ZoomScrollIntoViewBrowserTest,
MobileViewportDisablesZoom) {
ASSERT_TRUE(SetupTest("siteA{MobileViewport}(siteB)"));
EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);
RunTest();
EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);
}
IN_PROC_BROWSER_TEST_P(ZoomScrollIntoViewBrowserTest,
TouchActionNoneDisablesZoom) {
ASSERT_TRUE(SetupTest("siteA(siteB{TouchActionNone})"));
EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);
RunTest();
EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);
}
class RootScrollerScrollIntoViewBrowserTest
: public ScrollIntoViewBrowserTestBase {
public:
bool IsForceLocalFrames() const override { return false; }
bool IsWritingModeLTR() const override { return true; }
TestInvokeMethod GetInvokeMethod() const override { return kInputHandler; }
};
IN_PROC_BROWSER_TEST_F(RootScrollerScrollIntoViewBrowserTest,
FocusInRootScroller) {
ASSERT_TRUE(SetupTest("siteA{RootScroller,MobileViewportNoZoom}"));
{
base::RunLoop loop;
shell()->web_contents()->GetPrimaryMainFrame()->InsertVisualStateCallback(
base::BindLambdaForTesting(
[&loop](bool visual_state_updated) { loop.Quit(); }));
loop.Run();
}
ASSERT_EQ(1.0, GetVisualViewport().scale);
ASSERT_EQ(
true,
EvalJs(
InnerMostFrameTreeNode(),
"window.internals.effectiveRootScroller(document).tagName == 'DIV'"));
RunTest();
}
INSTANTIATE_TEST_SUITE_P(,
ZoomScrollIntoViewBrowserTest,
testing::Values(kLocalFrame, kRemoteFrame),
DescribeFrameType);
#endif
enum FencedFrameType { kFencedFrameMPArch, kFencedFrameShadowDOM };
[[maybe_unused]] std::string DescribeFencedFrameType(
const testing::TestParamInfo<FencedFrameType>& info) {
std::string impl_type;
switch (info.param) {
case kFencedFrameMPArch: {
impl_type = "MPArch";
} break;
case kFencedFrameShadowDOM: {
impl_type = "ShadowDOM";
} break;
}
return impl_type;
}
class ScrollIntoViewFencedFrameBrowserTest
: public ScrollIntoViewBrowserTestBase,
public ::testing::WithParamInterface<FencedFrameType> {
public:
ScrollIntoViewFencedFrameBrowserTest() {
const char* impl_param =
GetParam() == kFencedFrameMPArch ? "mparch" : "shadow_dom";
feature_list_.InitWithFeaturesAndParameters(
{{blink::features::kFencedFrames,
{{"implementation_type", impl_param}}},
{features::kPrivacySandboxAdsAPIsOverride, {}},
{blink::features::kFencedFramesAPIChanges, {}}},
{});
}
bool IsForceLocalFrames() const override { return false; }
bool IsWritingModeLTR() const override { return true; }
TestInvokeMethod GetInvokeMethod() const override { return kInputHandler; }
net::EmbeddedTestServer* server() override { return &https_server_; }
void SetUpOnMainThread() override {
https_server_.ServeFilesFromSourceDirectory(GetTestDataFilePath());
https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
ScrollIntoViewBrowserTestBase::SetUpOnMainThread();
}
private:
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_P(ScrollIntoViewFencedFrameBrowserTest,
SingleFencedFrame) {
ASSERT_TRUE(SetupTest("siteA{FencedFrame}(siteB)"));
RunTest();
}
IN_PROC_BROWSER_TEST_P(ScrollIntoViewFencedFrameBrowserTest,
NestedFencedFrames) {
ASSERT_TRUE(SetupTest("siteA{FencedFrame}(siteB{FencedFrame}(siteC))"));
RunTest();
}
IN_PROC_BROWSER_TEST_P(ScrollIntoViewFencedFrameBrowserTest,
LocalFrameInFencedFrame) {
ASSERT_TRUE(SetupTest("siteA{FencedFrame}(siteB(siteB))"));
RunTest();
}
IN_PROC_BROWSER_TEST_P(ScrollIntoViewFencedFrameBrowserTest,
RemoteFrameInFencedFrame) {
ASSERT_TRUE(SetupTest("siteA{FencedFrame}(siteB(siteC))"));
{
VisualViewport viewport = GetVisualViewport();
double page_scale_factor_before = viewport.scale;
EXPECT_TRUE(ExecJs(InnerMostFrameTreeNode(), R"JS(
document.querySelector('input').focus({preventScroll: true});
)JS"));
ASSERT_EQ(viewport.scale, page_scale_factor_before);
ASSERT_EQ(viewport.page_left, 0);
ASSERT_EQ(viewport.page_top, 0);
}
RunTest();
}
IN_PROC_BROWSER_TEST_P(ScrollIntoViewFencedFrameBrowserTest,
FencedFrameInRemoteFrame) {
ASSERT_TRUE(SetupTest("siteA(siteB{FencedFrame}(siteC))"));
RunTest();
}
IN_PROC_BROWSER_TEST_P(ScrollIntoViewFencedFrameBrowserTest,
ProgrammaticScrollIntoViewDoesntCrossFencedFrame) {
ASSERT_TRUE(SetupTest("siteA{FencedFrame}(siteB)"));
ScrollRectToVisibleInParentFrameInterceptor interceptor;
interceptor.Init(InnerMostFrameTreeNode()->current_frame_host());
ASSERT_EQ(0, EvalJs(InnerMostFrameTreeNode(), "window.scrollX"));
ASSERT_EQ(0, EvalJs(InnerMostFrameTreeNode(), "window.scrollY"));
ASSERT_TRUE(ExecJs(InnerMostFrameTreeNode(), R"JS(
document.querySelector('input').scrollIntoView({
behavior: 'instant',
block: 'center',
inline: 'center'
})
)JS"));
ASSERT_LT(0, EvalJs(InnerMostFrameTreeNode(), "window.scrollX"));
ASSERT_LT(0, EvalJs(InnerMostFrameTreeNode(), "window.scrollY"));
InnerMostFrameTreeNode()
->current_frame_host()
->local_frame_host_receiver_for_testing()
.FlushForTesting();
EXPECT_FALSE(interceptor.HasCalledScrollRectToVisibleInParentFrame());
}
INSTANTIATE_TEST_SUITE_P(,
ScrollIntoViewFencedFrameBrowserTest,
testing::Values(kFencedFrameMPArch,
kFencedFrameShadowDOM),
DescribeFencedFrameType);
}
}