blob: 7100f0391b66a634c91611c7f4c763e2d984045f [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 <sstream>
#include <string>
#include <tuple>
#include "base/strings/strcat.h"
#include "base/test/scoped_feature_list.h"
#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/common/actor.mojom.h"
#include "chrome/common/chrome_features.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/point_conversions.h"
using base::test::TestFuture;
using content::ChildFrameAt;
using content::EvalJs;
using content::ExecJs;
using content::GetDOMNodeId;
using content::NavigateIframeToURL;
using content::RenderFrameHost;
namespace actor {
namespace {
class ActorClickToolBrowserTest : public ActorToolsTest,
public ::testing::WithParamInterface<
::features::ActorPaintStabilityMode> {
public:
ActorClickToolBrowserTest() {
auto paint_stability_mode = GetParam();
feature_list_.InitAndEnableFeatureWithParameters(
::features::kGlicActor,
{{::features::kActorPaintStabilityMode.name,
::features::kActorPaintStabilityMode.GetName(paint_stability_mode)},
{features::kGlicActorClickDelay.name, "200ms"}});
}
~ActorClickToolBrowserTest() override = default;
void SetUpOnMainThread() override {
ActorToolsTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(embedded_https_test_server().Start());
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Basic test to ensure sending a click to an element works.
IN_PROC_BROWSER_TEST_P(ActorClickToolBrowserTest, ClickTool_SentToElement) {
const GURL url =
embedded_test_server()->GetURL("/actor/page_with_clickable_element.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Send a click to the document body.
{
std::optional<int> body_id = GetDOMNodeId(*main_frame(), "body");
ASSERT_TRUE(body_id);
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*main_frame(), body_id.value());
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
EXPECT_EQ("mousedown[BODY#],mouseup[BODY#],click[BODY#]",
EvalJs(web_contents(), "mouse_event_log.join(',')"));
}
ASSERT_TRUE(ExecJs(web_contents(), "mouse_event_log = []"));
// Send a second click to the button.
{
std::optional<int> button_id =
GetDOMNodeId(*main_frame(), "button#clickable");
ASSERT_TRUE(button_id);
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*main_frame(), button_id.value());
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
EXPECT_EQ(
"mousedown[BUTTON#clickable],mouseup[BUTTON#clickable],click[BUTTON#"
"clickable]",
EvalJs(web_contents(), "mouse_event_log.join(',')"));
// Ensure the button's event handler was invoked.
EXPECT_EQ(true, EvalJs(web_contents(), "button_clicked"));
}
}
// Sending a click to an element that doesn't exist fails.
IN_PROC_BROWSER_TEST_P(ActorClickToolBrowserTest,
ClickTool_NonExistentElement) {
const GURL url =
embedded_test_server()->GetURL("/actor/page_with_clickable_element.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Use a random node id that doesn't exist.
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*main_frame(), kNonExistentContentNodeId);
ActResultFuture result_fail;
actor_task().Act(ToRequestList(action), result_fail.GetCallback());
// The node id doesn't exist so the tool will return false.
ExpectErrorResult(result_fail, mojom::ActionResultCode::kInvalidDomNodeId);
// The page should not have received any events.
EXPECT_EQ("", EvalJs(web_contents(), "mouse_event_log.join(',')"));
}
// Sending a click to a disabled element should fail without dispatching events.
IN_PROC_BROWSER_TEST_P(ActorClickToolBrowserTest, ClickTool_DisabledElement) {
const GURL url =
embedded_test_server()->GetURL("/actor/page_with_clickable_element.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
std::optional<int> button_id = GetDOMNodeId(*main_frame(), "button#disabled");
ASSERT_TRUE(button_id);
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*main_frame(), button_id.value());
ActResultFuture result_fail;
actor_task().Act(ToRequestList(action), result_fail.GetCallback());
ExpectErrorResult(result_fail, mojom::ActionResultCode::kElementDisabled);
// The page should not have received any events.
EXPECT_EQ("", EvalJs(web_contents(), "mouse_event_log.join(',')"));
}
// Sending a click to an element that's not in the viewport should cause it to
// first be scrolled into view then clicked.
IN_PROC_BROWSER_TEST_P(ActorClickToolBrowserTest, ClickTool_OffscreenElement) {
const GURL url =
embedded_test_server()->GetURL("/actor/page_with_clickable_element.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Page starts unscrolled
ASSERT_EQ(0, EvalJs(web_contents(), "window.scrollY"));
std::optional<int> button_id =
GetDOMNodeId(*main_frame(), "button#offscreen");
ASSERT_TRUE(button_id);
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*main_frame(), button_id.value());
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
// Page is now scrolled.
ASSERT_GT(EvalJs(web_contents(), "window.scrollY"), 0);
// The page should not have received any events.
EXPECT_EQ(
"mousedown[BUTTON#offscreen],"
"mouseup[BUTTON#offscreen],"
"click[BUTTON#offscreen]",
EvalJs(web_contents(), "mouse_event_log.join(',')"));
}
// Ensure clicks can be sent to elements that are only partially onscreen.
IN_PROC_BROWSER_TEST_P(ActorClickToolBrowserTest, ClickTool_ClippedElements) {
const GURL url =
embedded_test_server()->GetURL("/actor/click_with_overflow_clip.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
std::vector<std::string> test_cases = {
"offscreenButton", "overflowHiddenButton", "overflowScrollButton"};
for (auto button : test_cases) {
SCOPED_TRACE(testing::Message() << "WHILE TESTING: " << button);
std::optional<int> button_id =
GetDOMNodeId(*main_frame(), base::StrCat({"#", button}));
ASSERT_TRUE(button_id);
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*main_frame(), button_id.value());
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
EXPECT_EQ(button, EvalJs(web_contents(), "clicked_button"));
ASSERT_TRUE(ExecJs(web_contents(), "clicked_button = ''"));
}
}
// Ensure clicks can be sent to a coordinate onscreen.
IN_PROC_BROWSER_TEST_P(ActorClickToolBrowserTest, ClickTool_SentToCoordinate) {
const GURL url =
embedded_test_server()->GetURL("/actor/page_with_clickable_element.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Send a click to a (0,0) coordinate inside the document.
{
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*active_tab(), gfx::Point(0, 0));
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
EXPECT_EQ("mousedown[HTML#],mouseup[HTML#],click[HTML#]",
EvalJs(web_contents(), "mouse_event_log.join(',')"));
}
ASSERT_TRUE(ExecJs(web_contents(), "mouse_event_log = []"));
// Send a second click to a coordinate on the button.
{
gfx::Point click_point = gfx::ToFlooredPoint(
GetCenterCoordinatesOfElementWithId(web_contents(), "clickable"));
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*active_tab(), click_point);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
EXPECT_EQ(
"mousedown[BUTTON#clickable],mouseup[BUTTON#clickable],click[BUTTON#"
"clickable]",
EvalJs(web_contents(), "mouse_event_log.join(',')"));
// Ensure the button's event handler was invoked.
EXPECT_EQ(true, EvalJs(web_contents(), "button_clicked"));
}
}
// Sending a click to a coordinate not in the viewport should fail without
// dispatching events.
IN_PROC_BROWSER_TEST_P(ActorClickToolBrowserTest,
ClickTool_SentToCoordinateOffScreen) {
const GURL url =
embedded_test_server()->GetURL("/actor/page_with_clickable_element.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Send a click to a negative coordinate offscreen.
{
gfx::Point negative_offscreen = {-1, 0};
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*active_tab(), negative_offscreen);
ActResultFuture result_fail;
actor_task().Act(ToRequestList(action), result_fail.GetCallback());
ExpectErrorResult(result_fail,
mojom::ActionResultCode::kCoordinatesOutOfBounds);
// The page should not have received any events.
EXPECT_EQ("", EvalJs(web_contents(), "mouse_event_log.join(',')"));
}
// Send a click to a positive coordinate offscreen.
{
gfx::Point positive_offscreen = gfx::ToFlooredPoint(
GetCenterCoordinatesOfElementWithId(web_contents(), "offscreen"));
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*active_tab(), positive_offscreen);
ActResultFuture result_fail;
actor_task().Act(ToRequestList(action), result_fail.GetCallback());
ExpectErrorResult(result_fail,
mojom::ActionResultCode::kCoordinatesOutOfBounds);
// The page should not have received any events.
EXPECT_EQ("", EvalJs(web_contents(), "mouse_event_log.join(',')"));
}
}
// Ensure click is using viewport coordinate.
IN_PROC_BROWSER_TEST_P(ActorClickToolBrowserTest,
ClickTool_ViewportCoordinate) {
const GURL url =
embedded_test_server()->GetURL("/actor/page_with_clickable_element.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Scroll the window by 100vh so #offscreen button is in viewport.
ASSERT_TRUE(ExecJs(web_contents(), "window.scrollBy(0, window.innerHeight)"));
// Send a click to button's viewport coordinate.
{
gfx::Point click_point = gfx::ToFlooredPoint(
GetCenterCoordinatesOfElementWithId(web_contents(), "offscreen"));
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*active_tab(), click_point);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
EXPECT_EQ(
"mousedown[BUTTON#offscreen],mouseup[BUTTON#offscreen],click[BUTTON#"
"offscreen]",
EvalJs(web_contents(), "mouse_event_log.join(',')"));
// Ensure the button's event handler was invoked.
EXPECT_EQ(true, EvalJs(web_contents(), "offscreen_button_clicked"));
}
}
// Ensure click works correctly when clicking on a cross process iframe using a
// DomNodeId
IN_PROC_BROWSER_TEST_P(ActorClickToolBrowserTest,
ClickTool_Subframe_DomNodeId) {
// This test only applies if cross-origin frames are put into separate
// processes.
if (!content::AreAllSitesIsolatedForTesting()) {
GTEST_SKIP();
}
const GURL url = embedded_https_test_server().GetURL(
"foo.com", "/actor/positioned_iframe.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
const GURL subframe_url = embedded_https_test_server().GetURL(
"bar.com", "/actor/page_with_clickable_element.html");
ASSERT_TRUE(NavigateIframeToURL(web_contents(), "iframe", subframe_url));
RenderFrameHost* subframe =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0);
ASSERT_TRUE(subframe);
ASSERT_TRUE(subframe->IsCrossProcessSubframe());
// Send a click to the button in the subframe.
std::optional<int> button_id = GetDOMNodeId(*subframe, "button#clickable");
ASSERT_TRUE(button_id);
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*subframe, button_id.value());
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
// Ensure the button's event handler was invoked.
EXPECT_EQ(true, EvalJs(subframe, "button_clicked"));
}
// Ensure that page tools (click is arbitrary here) correctly add the acted on
// tab to the task's tab set.
IN_PROC_BROWSER_TEST_P(ActorClickToolBrowserTest,
ClickTool_RecordActingOnTask) {
ASSERT_TRUE(actor_task().GetTabs().empty());
// Send a click to the document body.
std::optional<int> body_id = GetDOMNodeId(*main_frame(), "body");
ASSERT_TRUE(body_id);
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*main_frame(), body_id.value());
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
EXPECT_TRUE(actor_task().GetTabs().contains(active_tab()->GetHandle()));
}
IN_PROC_BROWSER_TEST_P(ActorClickToolBrowserTest, ClickTool_Delay) {
const GURL url =
embedded_test_server()->GetURL("/actor/page_with_clickable_element.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
std::optional<int> body_id = GetDOMNodeId(*main_frame(), "body");
ASSERT_TRUE(body_id);
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*main_frame(), body_id.value());
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
const double mousedown_timestamp =
EvalJs(main_frame(), "mouse_event_timestamps[0]").ExtractDouble();
const double mouseup_timestamp =
EvalJs(main_frame(), "mouse_event_timestamps[1]").ExtractDouble();
const base::TimeDelta delta =
base::Milliseconds(mouseup_timestamp - mousedown_timestamp);
EXPECT_GE(delta, features::kGlicActorClickDelay.Get());
}
INSTANTIATE_TEST_SUITE_P(
,
ActorClickToolBrowserTest,
testing::Values(::features::ActorPaintStabilityMode::kDisabled,
::features::ActorPaintStabilityMode::kLogOnly,
::features::ActorPaintStabilityMode::kEnabled),
[](const testing::TestParamInfo<::features::ActorPaintStabilityMode>&
info) { return DescribePaintStabilityMode(info.param); });
class ActorClickToolScaledBrowserTest : public ActorToolsTest {
public:
ActorClickToolScaledBrowserTest() = default;
~ActorClickToolScaledBrowserTest() override = default;
void SetUpOnMainThread() override {
ActorToolsTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(embedded_https_test_server().Start());
}
void SetUpCommandLine(base::CommandLine* command_line) override {
ActorToolsTest::SetUpCommandLine(command_line);
command_line->RemoveSwitch(switches::kForceDeviceScaleFactor);
command_line->AppendSwitchASCII(switches::kForceDeviceScaleFactor, "2");
}
};
// Ensure clicks can be sent to elements that are only partially onscreen with
// scaling.
IN_PROC_BROWSER_TEST_F(ActorClickToolScaledBrowserTest,
ClickTool_ScaledClippedElements) {
const GURL url =
embedded_test_server()->GetURL("/actor/click_with_overflow_clip.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
std::vector<std::string> test_cases = {
"offscreenButton", "overflowHiddenButton", "overflowScrollButton"};
for (auto button : test_cases) {
SCOPED_TRACE(testing::Message() << "WHILE TESTING: " << button);
std::optional<int> button_id =
GetDOMNodeId(*main_frame(), base::StrCat({"#", button}));
ASSERT_TRUE(button_id);
std::unique_ptr<ToolRequest> action =
MakeClickRequest(*main_frame(), button_id.value());
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
EXPECT_EQ(button, EvalJs(web_contents(), "clicked_button"));
ASSERT_TRUE(ExecJs(web_contents(), "clicked_button = ''"));
}
}
} // namespace
} // namespace actor