blob: 69fc6ae6d8924e7c302e3c6e1bc962acbb46b162 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/test/test_future.h"
#include "chrome/browser/actor/actor_test_util.h"
#include "chrome/browser/actor/tools/tool_request.h"
#include "chrome/browser/actor/tools/tools_test_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/zoom/chrome_zoom_level_prefs.h"
#include "chrome/common/actor.mojom.h"
#include "chrome/test/base/test_browser_window.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "third_party/blink/public/common/page/page_zoom.h"
using base::test::TestFuture;
using content::EvalJs;
using content::ExecJs;
using content::GetDOMNodeId;
namespace actor {
namespace {
class ActorScrollToolBrowserTest : public ActorToolsTest {
public:
ActorScrollToolBrowserTest() = default;
~ActorScrollToolBrowserTest() override = default;
void SetUpOnMainThread() override {
ActorToolsTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(embedded_https_test_server().Start());
}
};
IN_PROC_BROWSER_TEST_F(ActorScrollToolBrowserTest,
ScrollTool_FailOnInvalidNodeID) {
const GURL url =
embedded_test_server()->GetURL("/actor/scrollable_page.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Use a random node id that doesn't exist.
float scroll_offset_y = 50;
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), kNonExistentContentNodeId,
/*scroll_offset_x=*/0, scroll_offset_y);
ActResultFuture result_fail;
actor_task().Act(ToRequestList(action), result_fail.GetCallback());
ExpectErrorResult(result_fail, mojom::ActionResultCode::kInvalidDomNodeId);
EXPECT_EQ(0, EvalJs(web_contents(), "window.scrollY"));
}
// Test scrolling the viewport vertically.
IN_PROC_BROWSER_TEST_F(ActorScrollToolBrowserTest,
ScrollTool_ScrollPageVertical) {
const GURL url =
embedded_test_server()->GetURL("/actor/scrollable_page.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
int scroll_offset_y = 50;
{
// If no node id is passed, it will scroll the page's viewport.
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), /*content_node_id=*/std::nullopt,
/*scroll_offset_x=*/0, scroll_offset_y);
ActResultFuture result_success;
actor_task().Act(ToRequestList(action), result_success.GetCallback());
ExpectOkResult(result_success);
EXPECT_EQ(scroll_offset_y, EvalJs(web_contents(), "window.scrollY"));
}
{
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), /*content_node_id=*/std::nullopt,
/*scroll_offset_x=*/0, scroll_offset_y);
ActResultFuture result_success;
actor_task().Act(ToRequestList(action), result_success.GetCallback());
ExpectOkResult(result_success);
EXPECT_EQ(2 * scroll_offset_y, EvalJs(web_contents(), "window.scrollY"));
}
}
// Test scrolling the viewport horizontally.
IN_PROC_BROWSER_TEST_F(ActorScrollToolBrowserTest,
ScrollTool_ScrollPageHorizontal) {
const GURL url =
embedded_test_server()->GetURL("/actor/scrollable_page.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
int scroll_offset_x = 50;
{
// If no node id is passed, it will scroll the page's viewport.
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(),
/*content_node_id=*/std::nullopt, scroll_offset_x,
/*scroll_offset_y=*/0);
ActResultFuture result_success;
actor_task().Act(ToRequestList(action), result_success.GetCallback());
ExpectOkResult(result_success);
EXPECT_EQ(scroll_offset_x, EvalJs(web_contents(), "window.scrollX"));
}
{
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(),
/*content_node_id=*/std::nullopt, scroll_offset_x,
/*scroll_offset_y=*/0);
ActResultFuture result_success;
actor_task().Act(ToRequestList(action), result_success.GetCallback());
ExpectOkResult(result_success);
EXPECT_EQ(2 * scroll_offset_x, EvalJs(web_contents(), "window.scrollX"));
}
}
// Test scrolling in a sub-scroller on the page.
IN_PROC_BROWSER_TEST_F(ActorScrollToolBrowserTest, ScrollTool_ScrollElement) {
const GURL url =
embedded_test_server()->GetURL("/actor/scrollable_page.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
int scroll_offset_x = 50;
int scroll_offset_y = 80;
int scroller = GetDOMNodeId(*main_frame(), "#scroller").value();
{
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), scroller, scroll_offset_x,
/*scroll_offset_y=*/0);
ActResultFuture result_success;
actor_task().Act(ToRequestList(action), result_success.GetCallback());
ExpectOkResult(result_success);
EXPECT_EQ(scroll_offset_x,
EvalJs(web_contents(),
"document.getElementById('scroller').scrollLeft"));
}
{
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), scroller,
/*scroll_offset_x=*/0, scroll_offset_y);
ActResultFuture result_success;
actor_task().Act(ToRequestList(action), result_success.GetCallback());
ExpectOkResult(result_success);
EXPECT_EQ(scroll_offset_y,
EvalJs(web_contents(),
"document.getElementById('scroller').scrollTop"));
}
}
// Test scrolling over a non-scrollable element returns failure.
IN_PROC_BROWSER_TEST_F(ActorScrollToolBrowserTest, ScrollTool_NonScrollable) {
const GURL url =
embedded_test_server()->GetURL("/actor/scrollable_page.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
int scroll_offset_y = 80;
int scroller = GetDOMNodeId(*main_frame(), "#nonscroll").value();
{
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), scroller,
/*scroll_offset_x=*/0, scroll_offset_y);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectErrorResult(result,
mojom::ActionResultCode::kScrollTargetNotUserScrollable);
EXPECT_EQ(0, EvalJs(web_contents(),
"document.getElementById('nonscroll').scrollTop"));
EXPECT_EQ(0, EvalJs(web_contents(), "window.scrollY"));
}
}
// Test scrolling a scroller that's currently offscreen. It will first be
// scrolled into view then scroll applied.
IN_PROC_BROWSER_TEST_F(ActorScrollToolBrowserTest,
ScrollTool_OffscreenScrollable) {
const GURL url =
embedded_test_server()->GetURL("/actor/scrollable_page.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Page starts unscrolled
ASSERT_EQ(0, EvalJs(web_contents(), "window.scrollY"));
int scroll_offset_y = 80;
int scroller = GetDOMNodeId(*main_frame(), "#offscreenscroller").value();
{
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), scroller,
/*scroll_offset_x=*/0, scroll_offset_y);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
EXPECT_EQ(scroll_offset_y,
EvalJs(web_contents(),
"document.getElementById('offscreenscroller').scrollTop"));
EXPECT_GT(EvalJs(web_contents(), "window.scrollY"), 0);
}
}
// Test that a scrolling over a scroller with overflow in one axis only works
// correctly.
IN_PROC_BROWSER_TEST_F(ActorScrollToolBrowserTest, ScrollTool_OneAxisScroller) {
const GURL url =
embedded_test_server()->GetURL("/actor/scrollable_page.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
int scroll_offset = 80;
int scroller = GetDOMNodeId(*main_frame(), "#horizontalscroller").value();
// Try a vertical scroll - it should fail since the scroller has only
// horizontal overflow.
{
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), scroller,
/*scroll_offset_x=*/0, scroll_offset);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectErrorResult(result,
mojom::ActionResultCode::kScrollTargetNotUserScrollable);
EXPECT_EQ(
0, EvalJs(web_contents(),
"document.getElementById('horizontalscroller').scrollTop"));
EXPECT_EQ(0, EvalJs(web_contents(), "window.scrollY"));
}
// Horizontal scroll should succeed.
{
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), scroller, scroll_offset,
/*scroll_offset_y=*/0);
ActResultFuture result_success;
actor_task().Act(ToRequestList(action), result_success.GetCallback());
ExpectOkResult(result_success);
EXPECT_EQ(
scroll_offset,
EvalJs(web_contents(),
"document.getElementById('horizontalscroller').scrollLeft"));
}
}
// Ensure scroll distances are correctly scaled when browser zoom is applied.
IN_PROC_BROWSER_TEST_F(ActorScrollToolBrowserTest, ScrollTool_BrowserZoom) {
// Set the default browser page zoom to 150%.
double level = blink::ZoomFactorToZoomLevel(1.5);
browser()->profile()->GetZoomLevelPrefs()->SetDefaultZoomLevelPref(level);
const GURL url =
embedded_test_server()->GetURL("/actor/scrollable_page.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// 60 physical pixels translates to 40 CSS pixels when the zoom factor is 1.5
// (3 physical pixels : 2 CSS Pixels)
int scroll_offset_physical = 60;
int expected_offset_css = 40;
int scroller = GetDOMNodeId(*main_frame(), "#scroller").value();
{
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), scroller,
/*scroll_offset_x=*/0, scroll_offset_physical);
ActResultFuture result_success;
actor_task().Act(ToRequestList(action), result_success.GetCallback());
ExpectOkResult(result_success);
EXPECT_EQ(expected_offset_css,
EvalJs(web_contents(),
"document.getElementById('scroller').scrollTop"));
}
}
// Ensure scroll distances are correctly scaled when applied to a CSS zoomed
// scroller.
IN_PROC_BROWSER_TEST_F(ActorScrollToolBrowserTest, ScrollTool_CSSZoom) {
const GURL url =
embedded_test_server()->GetURL("/actor/scrollable_page.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// 60 physical pixels translates to 120 CSS pixels since the scroller is
// inside a `zoom:0.5` subtree (1 physical pixels : 2 CSS Pixels)
int scroll_offset_physical = 60;
int expected_offset_css = 120;
int scroller = GetDOMNodeId(*main_frame(), "#zoomedscroller").value();
{
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), scroller,
/*scroll_offset_x=*/0, scroll_offset_physical);
ActResultFuture result_success;
actor_task().Act(ToRequestList(action), result_success.GetCallback());
ExpectOkResult(result_success);
EXPECT_EQ(expected_offset_css,
EvalJs(web_contents(),
"document.getElementById('zoomedscroller').scrollTop"));
}
}
class ActorToolsTestDSF2 : public ActorScrollToolBrowserTest {
public:
ActorToolsTestDSF2() = default;
explicit ActorToolsTestDSF2(const ActorToolsTestDSF2&) = delete;
ActorToolsTestDSF2& operator=(const ActorToolsTestDSF2&) = delete;
~ActorToolsTestDSF2() override = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
ActorToolsTest::SetUpCommandLine(command_line);
command_line->RemoveSwitch(switches::kForceDeviceScaleFactor);
command_line->AppendSwitchASCII(switches::kForceDeviceScaleFactor, "2");
}
};
// Ensure scroll distances are correctly scaled when using a non-1 device scale
// factor
IN_PROC_BROWSER_TEST_F(ActorToolsTestDSF2, ScrollTool_ScrollDSF) {
const GURL url =
embedded_test_server()->GetURL("/actor/scrollable_page.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// 80 physical pixels translates to 40 CSS pixels when the device scale factor
// = 2 (2 physical pixels : 1 CSS pixel);
int scroll_offset_physical = 80;
int expected_offset_css = 40;
int scroller = GetDOMNodeId(*main_frame(), "#scroller").value();
{
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), scroller,
/*scroll_offset_x=*/0, scroll_offset_physical);
ActResultFuture result_success;
actor_task().Act(ToRequestList(action), result_success.GetCallback());
ExpectOkResult(result_success);
EXPECT_EQ(expected_offset_css,
EvalJs(web_contents(),
"document.getElementById('scroller').scrollTop"));
}
}
IN_PROC_BROWSER_TEST_F(ActorScrollToolBrowserTest,
ScrollTool_ZeroIdTargetsViewport) {
const GURL url =
embedded_test_server()->GetURL("/actor/scrollable_page.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// DOMNodeIDs start at 1 so 0 should be interpreted as viewport.
constexpr int kViewportId = 0;
float scroll_offset_y = 50;
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), kViewportId,
/*scroll_offset_x=*/0, scroll_offset_y);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
// Not sure why, since all zooms should be exactly 1.0, but some numerical
// instability seems to creep in. Using ExtractDouble and EXPECT_FLOAT_EQ for
// that reason.
EXPECT_FLOAT_EQ(scroll_offset_y,
EvalJs(web_contents(), "window.scrollY").ExtractDouble());
}
// Test that a scroll on a page with scroll-behavior:smooth returns success if
// an animation was started, even though it may not have instantly scrolled.
IN_PROC_BROWSER_TEST_F(ActorScrollToolBrowserTest,
ScrollTool_SmoothScrollSucceeds) {
const GURL url =
embedded_test_server()->GetURL("/actor/scrollable_page.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
float scroll_offset_y = 300;
int scroller = GetDOMNodeId(*main_frame(), "#smoothscroller").value();
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), scroller,
/*scroll_offset_x=*/0, scroll_offset_y);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
}
// Test that a scroll on a page with scroll-behavior:smooth returns failure if
// trying to scroll in a direction with no scrollable extent.
IN_PROC_BROWSER_TEST_F(ActorScrollToolBrowserTest,
ScrollTool_SmoothScrollAtExtent) {
const GURL url =
embedded_test_server()->GetURL("/actor/scrollable_page.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Scroll to the scroller's full extent.
ASSERT_TRUE(ExecJs(web_contents(),
"document.querySelector('#smoothscroller').scrollTo({top:"
"10000, behavior:'instant'})"));
float scroll_offset_y = 300;
int scroller = GetDOMNodeId(*main_frame(), "#smoothscroller").value();
std::unique_ptr<ToolRequest> action =
MakeScrollRequest(*main_frame(), scroller,
/*scroll_offset_x=*/0, scroll_offset_y);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectErrorResult(result, mojom::ActionResultCode::kScrollOffsetDidNotChange);
}
} // namespace
} // namespace actor