blob: 1b1d5b1785be989facf4962535b97b87c19c5656 [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 "chrome/browser/actor/ui/dom_node_geometry.h"
#include "base/files/file_path.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "components/optimization_guide/content/browser/page_content_proto_provider.h"
#include "components/optimization_guide/proto/features/common_quality_data.pb.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/views/test/view_skia_gold_pixel_diff.h"
namespace actor::ui {
namespace {
using blink::mojom::AIPageContentMode;
using blink::mojom::AIPageContentOptionsPtr;
using optimization_guide::AIPageContentResult;
using optimization_guide::OnAIPageContentDone;
using optimization_guide::proto::AnnotatedPageContent;
using optimization_guide::proto::ContentNode;
using optimization_guide::proto::FrameData;
using views::ViewSkiaGoldPixelDiff;
using AriaToDomNodeMap = absl::flat_hash_map<std::string, DomNode>;
void BuildAriaLabelMap(const ContentNode& root,
std::string doc_id,
AriaToDomNodeMap* map) {
if (root.has_content_attributes()) {
std::string label = root.content_attributes().label();
if (!label.empty()) {
int node_id = root.content_attributes().common_ancestor_dom_node_id();
auto node = DomNode{.node_id = node_id, .document_identifier = doc_id};
map->emplace(label, node);
}
}
if (root.content_attributes().has_iframe_data() &&
root.content_attributes().iframe_data().has_frame_data()) {
doc_id = root.content_attributes()
.iframe_data()
.frame_data()
.document_identifier()
.serialized_token();
}
for (auto& child : root.children_nodes()) {
BuildAriaLabelMap(child, doc_id, map);
}
}
std::string_view Trim(const std::string_view str) {
const std::string whitespace = " \t\n\r\f\v";
const auto strBegin = str.find_first_not_of(whitespace);
if (std::string::npos == strBegin) {
return "";
}
const auto strEnd = str.find_last_not_of(whitespace);
const auto strRange = strEnd - strBegin + 1;
return str.substr(strBegin, strRange);
}
MATCHER_P(IsEqualToTrimmed, expected, "") {
return Trim(arg) == Trim(expected);
}
class ActorUiDomNodeGeometryBrowserTest : public InProcessBrowserTest {
public:
ActorUiDomNodeGeometryBrowserTest() = default;
~ActorUiDomNodeGeometryBrowserTest() override = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
InProcessBrowserTest::SetUpCommandLine(command_line);
if (command_line->HasSwitch(switches::kVerifyPixels)) {
// Increment "v0" for Skia Gold diff when different images are expected.
pixel_diff_ =
std::make_unique<ViewSkiaGoldPixelDiff>("ActorUiDomNodeGeometry_v0");
}
}
void SetUpOnMainThread() override {
ASSERT_TRUE(embedded_test_server()->Start());
}
content::WebContents* web_contents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
AIPageContentOptionsPtr AIPageContentOptions() {
auto options = optimization_guide::DefaultAIPageContentOptions(
/*on_critical_path =*/true);
options->mode = AIPageContentMode::kActionableElements;
return options;
}
void LoadPage(GURL url) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
ASSERT_TRUE(content::WaitForLoadStop(web_contents()));
ASSERT_TRUE(content::EvalJs(web_contents(),
R"js(new Promise(resolve => {
if (window.isIframeLoaded) {
resolve(true);
} else {
const interval = setInterval(() => {
if (window.isIframeLoaded) {
clearInterval(interval);
resolve(true);
}
}, 100);
}
});)js")
.ExtractBool());
}
void SetPageContent(base::OnceClosure quit_closure,
std::optional<AIPageContentResult> page_content) {
apc_ = std::move(page_content->proto);
aria_label_to_dom_node_.clear();
BuildAriaLabelMap(
apc_.root_node(),
apc_.main_frame_data().document_identifier().serialized_token(),
&aria_label_to_dom_node_);
std::move(quit_closure).Run();
}
void GetPageApc() {
base::RunLoop run_loop;
auto options = AIPageContentOptions();
GetAIPageContent(
web_contents(), std::move(options),
base::BindOnce(&ActorUiDomNodeGeometryBrowserTest::SetPageContent,
base::Unretained(this), run_loop.QuitClosure()));
run_loop.Run();
}
std::string ElementTextAtPoint(const gfx::Point& pt) {
content::EvalJsResult result = content::EvalJs(
web_contents(),
absl::StrFormat("document.elementFromPoint(%d, %d).textContent", pt.x(),
pt.y()));
EXPECT_THAT(result, content::EvalJsResult::IsOk());
EXPECT_TRUE(result.is_string());
return result.ExtractString();
}
DomNode GetDomNodeForAriaLabel(std::string_view label) {
return aria_label_to_dom_node_.at(label);
}
void ColorPoint(gfx::Point point, std::string_view color) {
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
absl::StrFormat("placeDot(%d, %d, '%s');", point.x(),
point.y(), color)));
}
gfx::Point GetAriaLabelPoint(std::string_view label) {
std::optional<gfx::Point> point =
GetDomNodePointFromApc(apc_, GetDomNodeForAriaLabel(label));
EXPECT_TRUE(point.has_value());
EXPECT_GT(point->x(), 0);
EXPECT_GT(point->y(), 0);
return point.value();
}
void SkiaWebContentsDiff() {
if (!pixel_diff_) {
LOG(WARNING) << "Skipping Skia diffs";
return;
}
const testing::TestInfo* const test_info =
testing::UnitTest::GetInstance()->current_test_info();
BrowserView* browser_view =
BrowserView::GetBrowserViewForBrowser(browser());
pixel_diff_->CompareViewScreenshot(
absl::StrFormat("%s-%s", test_info->test_suite_name(),
test_info->name()),
browser_view->GetActiveContentsContainerView());
}
protected:
std::unique_ptr<ViewSkiaGoldPixelDiff> pixel_diff_;
AnnotatedPageContent apc_;
AriaToDomNodeMap aria_label_to_dom_node_;
};
IN_PROC_BROWSER_TEST_F(ActorUiDomNodeGeometryBrowserTest,
GetDomNodePointFromApc_TopFrame) {
LoadPage(embedded_test_server()->GetURL("/actor/dom_node_geometry.html"));
GetPageApc();
gfx::Point point_a = GetAriaLabelPoint("a_div");
LOG(INFO) << "Point A: " << point_a;
gfx::Point point_b = GetAriaLabelPoint("b");
LOG(INFO) << "Point B: " << point_b;
EXPECT_THAT(ElementTextAtPoint(point_b), IsEqualToTrimmed("B"));
gfx::Point point_c = GetAriaLabelPoint("c_div");
LOG(INFO) << "Point C: " << point_c;
EXPECT_THAT(ElementTextAtPoint(point_c), IsEqualToTrimmed("C"));
gfx::Point point_d = GetAriaLabelPoint("d_div");
LOG(INFO) << "Point D: " << point_d;
EXPECT_THAT(ElementTextAtPoint(point_d), IsEqualToTrimmed("D"));
gfx::Point point_e = GetAriaLabelPoint("e_div");
LOG(INFO) << "Point E: " << point_e;
EXPECT_THAT(ElementTextAtPoint(point_e), IsEqualToTrimmed("E"));
ColorPoint(point_a, "purple");
ColorPoint(point_b, "blue");
ColorPoint(point_c, "orange");
ColorPoint(point_d, "green");
ColorPoint(point_e, "red");
SkiaWebContentsDiff();
}
IN_PROC_BROWSER_TEST_F(ActorUiDomNodeGeometryBrowserTest,
GetDomNodePointFromApc_SubFrame) {
LoadPage(embedded_test_server()->GetURL("/actor/dom_node_geometry.html"));
GetPageApc();
gfx::Point point_div1 = GetAriaLabelPoint("div1_label");
LOG(INFO) << "Point Div1: " << point_div1;
gfx::Point point_div2 = GetAriaLabelPoint("div2_label");
LOG(INFO) << "Point Div2: " << point_div2;
ColorPoint(point_div1, "red");
ColorPoint(point_div2, "green");
SkiaWebContentsDiff();
}
} // namespace
} // namespace actor::ui