blob: ea45a226177af65e19d904268016eb7cb7f727d5 [file] [log] [blame] [edit]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/content_extraction/ai_page_content_agent.h"
#include <cstddef>
#include <optional>
#include <string>
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/strings/stringprintf.h"
#include "base/test/with_feature_override.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/frame/frame_ad_evidence.h"
#include "third_party/blink/public/common/input/web_mouse_event.h"
#include "third_party/blink/public/mojom/content_extraction/ai_page_content.mojom-data-view.h"
#include "third_party/blink/renderer/core/accessibility/ax_context.h"
#include "third_party/blink/renderer/core/accessibility/ax_object_cache.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
#include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/html_collection.h"
#include "third_party/blink/renderer/core/html/html_iframe_element.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/input/event_handler.h"
#include "third_party/blink/renderer/core/layout/hit_test_location.h"
#include "third_party/blink/renderer/core/layout/hit_test_request.h"
#include "third_party/blink/renderer/core/layout/hit_test_result.h"
#include "third_party/blink/renderer/core/layout/layout_box_model_object.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/layout/map_coordinates_flags.h"
#include "third_party/blink/renderer/core/scroll/scrollable_area.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
#include "third_party/blink/renderer/platform/geometry/physical_offset.h"
#include "third_party/blink/renderer/platform/graphics/visual_rect_flags.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/task_environment.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/url_test_helpers.h"
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_mode.h"
#include "ui/gfx/geometry/quad_f.h"
#include "ui/gfx/geometry/rect_conversions.h"
namespace blink {
using ClickabilityReason = mojom::blink::AIPageContentClickabilityReason;
namespace {
constexpr gfx::Size kWindowSize{1000, 1000};
constexpr char kSmallImage[] =
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/"
"2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEB"
"EQCgwSExIQEw8QEBD/"
"2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB"
"AQEBAQEBAQEBAQEBD/wAARCAABAAEDASIAAhEBAxEB/"
"8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/"
"8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2J"
"yggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDh"
"IWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+"
"Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/"
"8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnL"
"RChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g"
"oOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uP"
"k5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD+/iiiigD/2Q==";
class AIPageContentAgentTest : public testing::Test {
public:
AIPageContentAgentTest() = default;
~AIPageContentAgentTest() override = default;
void SetUp() override {
helper_.InitializeWithSettings(&UpdateWebSettings);
helper_.Resize(kWindowSize);
helper_.LoadAhem();
ASSERT_TRUE(helper_.LocalMainFrame());
}
void TearDown() override {
url_test_helpers::UnregisterAllURLsAndClearMemoryCache();
}
void LoadAhem() {
helper_.LoadAhem();
test::RunPendingTasks();
helper_.LocalMainFrame()
->GetFrame()
->GetDocument()
->View()
->UpdateAllLifecyclePhasesForTest();
}
void CheckListItemWithText(const mojom::blink::AIPageContentNode& node,
const String& expected_text) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kListItem);
EXPECT_EQ(node.children_nodes.size(), 1u);
CheckTextNode(*node.children_nodes[0], expected_text);
}
void CheckTextNode(const mojom::blink::AIPageContentNode& node,
const String& expected_text) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kText);
ASSERT_TRUE(attributes.text_info);
EXPECT_EQ(attributes.text_info->text_content, expected_text);
}
void CheckTextSize(const mojom::blink::AIPageContentNode& node,
mojom::blink::AIPageContentTextSize expected_text_size) {
const auto& attributes = *node.content_attributes;
ASSERT_TRUE(attributes.text_info);
EXPECT_EQ(attributes.text_info->text_style->text_size, expected_text_size);
}
void CheckTextEmphasis(const mojom::blink::AIPageContentNode& node,
bool expected_has_emphasis) {
const auto& attributes = *node.content_attributes;
ASSERT_TRUE(attributes.text_info);
EXPECT_EQ(attributes.text_info->text_style->has_emphasis,
expected_has_emphasis);
}
void CheckTextColor(const mojom::blink::AIPageContentNode& node,
RGBA32 expected_color) {
const auto& attributes = *node.content_attributes;
ASSERT_TRUE(attributes.text_info);
EXPECT_EQ(attributes.text_info->text_style->color, expected_color);
}
void CheckImageNode(const mojom::blink::AIPageContentNode& node,
const String& expected_caption) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kImage);
ASSERT_TRUE(attributes.image_info);
EXPECT_EQ(attributes.image_info->image_caption, expected_caption);
}
void CheckAnchorNode(
const mojom::blink::AIPageContentNode& node,
const blink::KURL& expected_url,
const Vector<mojom::blink::AIPageContentAnchorRel>& expected_rels) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kAnchor);
ASSERT_TRUE(attributes.anchor_data);
EXPECT_EQ(attributes.anchor_data->url, expected_url);
ASSERT_EQ(attributes.anchor_data->rel.size(), expected_rels.size());
for (size_t i = 0; i < expected_rels.size(); ++i) {
EXPECT_EQ(attributes.anchor_data->rel[i], expected_rels[i]);
}
}
void CheckTableNode(const mojom::blink::AIPageContentNode& node,
String expected_table_name = String()) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kTable);
ASSERT_TRUE(attributes.table_data);
if (!expected_table_name.IsNull()) {
EXPECT_EQ(attributes.table_data->table_name, expected_table_name);
}
}
void CheckTableCellNode(const mojom::blink::AIPageContentNode& node) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kTableCell);
}
void CheckTableRowNode(
const mojom::blink::AIPageContentNode& node,
const mojom::blink::AIPageContentTableRowType& expected_row_type) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kTableRow);
ASSERT_TRUE(attributes.table_row_data);
EXPECT_EQ(attributes.table_row_data->row_type, expected_row_type);
}
void CheckContainerNode(const mojom::blink::AIPageContentNode& node) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kContainer);
}
void CheckHeadingNode(const mojom::blink::AIPageContentNode& node) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kHeading);
}
void CheckParagraphNode(const mojom::blink::AIPageContentNode& node) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kParagraph);
}
void CheckUnorderedListNode(const mojom::blink::AIPageContentNode& node) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kUnorderedList);
}
void CheckOrderedListNode(const mojom::blink::AIPageContentNode& node) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kOrderedList);
}
void CheckAnnotatedRole(
const mojom::blink::AIPageContentNode& node,
const mojom::blink::AIPageContentAnnotatedRole& expected_role) {
const auto& attributes = *node.content_attributes;
ASSERT_EQ(attributes.annotated_roles.size(), 1u);
EXPECT_EQ(attributes.annotated_roles[0], expected_role);
}
void CheckAnnotatedRoles(
const mojom::blink::AIPageContentNode& node,
const std::vector<mojom::blink::AIPageContentAnnotatedRole>&
expected_roles) {
const auto& attributes = *node.content_attributes;
ASSERT_EQ(attributes.annotated_roles.size(), expected_roles.size());
EXPECT_THAT(attributes.annotated_roles,
testing::UnorderedElementsAreArray(expected_roles));
}
void CheckGeometry(const mojom::blink::AIPageContentNode& node,
const gfx::Rect& expected_outer_bounding_box,
const gfx::Rect& expected_visible_bounding_box) {
const auto& geometry = *node.content_attributes->geometry;
EXPECT_EQ(geometry.outer_bounding_box, expected_outer_bounding_box);
EXPECT_EQ(geometry.visible_bounding_box, expected_visible_bounding_box);
}
void CheckFormControlNode(
const mojom::blink::AIPageContentNode& node,
const mojom::blink::FormControlType& expected_form_control_type) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kFormControl);
EXPECT_EQ(attributes.form_control_data->form_control_type,
expected_form_control_type);
}
const mojom::blink::AIPageContentNode& GetSingleTableCell(
const mojom::blink::AIPageContentNode& table) {
CheckTableNode(table);
EXPECT_EQ(table.children_nodes.size(), 1u);
const auto& table_row = *table.children_nodes[0];
CheckTableRowNode(table_row,
mojom::blink::AIPageContentTableRowType::kBody);
EXPECT_EQ(table_row.children_nodes.size(), 1u);
const auto& table_cell = *table_row.children_nodes[0];
CheckTableCellNode(table_cell);
return table_cell;
}
void CheckIframeNode(const mojom::blink::AIPageContentNode& node) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kIframe);
}
void CheckRootNode(const mojom::blink::AIPageContentNode& node) {
const auto& attributes = *node.content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kRoot);
}
void CheckAlmostEquals(const gfx::Point& actual, const gfx::Point& expected) {
// Allow 1px difference for rounding.
const int kTolerance = 1;
EXPECT_LE(abs(actual.x() - expected.x()), kTolerance)
<< "actual : " << actual.ToString()
<< ", expected: " << expected.ToString();
EXPECT_LE(abs(actual.y() - expected.y()), kTolerance)
<< "actual : " << actual.ToString()
<< ", expected: " << expected.ToString();
}
void GetAIPageContentWithActionableElements() {
auto options = GetAIPageContentOptionsForTest();
options.mode = mojom::blink::AIPageContentMode::kActionableElements;
GetAIPageContent(options);
}
static mojom::blink::AIPageContentOptions GetAIPageContentOptionsForTest() {
mojom::blink::AIPageContentOptions options;
options.on_critical_path = true;
options.mode = mojom::blink::AIPageContentMode::kDefault;
return options;
}
void GetAIPageContent(std::optional<mojom::blink::AIPageContentOptions>
options = std::nullopt) {
auto* agent = AIPageContentAgent::GetOrCreateForTesting(
*helper_.LocalMainFrame()->GetFrame()->GetDocument());
EXPECT_TRUE(agent);
last_options_ = options ? *options : default_options_;
auto content = agent->GetAIPageContentInternal(last_options_);
CHECK(content);
CHECK(content->root_node);
// Always validate serialization.
mojom::blink::AIPageContentPtr output;
EXPECT_TRUE(
mojo::test::SerializeAndDeserialize<mojom::blink::AIPageContent>(
content, output));
last_content_ = std::move(content);
}
void FireMouseMoveEvent(const gfx::PointF& point) {
EventHandler& event_handler =
helper_.LocalMainFrame()->GetFrame()->GetEventHandler();
WebMouseEvent event(WebInputEvent::Type::kMouseMove, point, point,
WebPointerProperties::Button::kLeft, 0,
WebInputEvent::kLeftButtonDown,
WebInputEvent::GetStaticTimeStampForTests());
event.SetFrameScale(1);
event_handler.HandleMouseMoveEvent(event, Vector<WebMouseEvent>(),
Vector<WebMouseEvent>());
}
const mojom::blink::AIPageContentNode& ContentRootNode() {
CHECK(last_content_);
EXPECT_TRUE(last_content_->root_node);
if (last_options_.mode !=
mojom::blink::AIPageContentMode::kActionableElements) {
return *last_content_->root_node;
}
EXPECT_EQ(last_content_->root_node->children_nodes.size(), 1u);
const auto& html = *last_content_->root_node->children_nodes[0];
EXPECT_EQ(html.children_nodes.size(), 1u);
return *html.children_nodes[0];
}
const mojom::blink::AIPageContentNode* FindNodeByDomNodeId(
DOMNodeId dom_node_id) {
const auto& root = ContentRootNode();
Vector<const mojom::blink::AIPageContentNode*> stack;
stack.push_back(&root);
while (!stack.empty()) {
const auto* node = stack.back();
stack.pop_back();
if (node->content_attributes &&
node->content_attributes->dom_node_id.has_value() &&
*node->content_attributes->dom_node_id == dom_node_id) {
return node;
}
for (size_t i = node->children_nodes.size(); i > 0; --i) {
stack.push_back(node->children_nodes[i - 1].get());
}
}
return nullptr;
}
const mojom::blink::AIPageContentNode* FindNodeBySelector(String selector) {
Document* document = helper_.LocalMainFrame()->GetFrame()->GetDocument();
EXPECT_TRUE(document);
Element* element = document->QuerySelector(AtomicString(selector));
EXPECT_TRUE(element) << "Couldn't find element with selector = "
<< selector;
DOMNodeId dom_node_id = DOMNodeIds::IdForNode(element);
EXPECT_GE(dom_node_id, 1);
return FindNodeByDomNodeId(dom_node_id);
}
void CheckHitTestableButNotInteractive(
const mojom::blink::AIPageContentNode& node) {
CHECK(node.content_attributes->node_interaction_info);
EXPECT_TRUE(node.content_attributes->node_interaction_info
->document_scoped_z_order);
EXPECT_TRUE(node.content_attributes->node_interaction_info
->clickability_reasons.empty());
}
void CheckHitTestableAndInteractive(
const mojom::blink::AIPageContentNode& node,
base::span<const ClickabilityReason> expected_reasons) {
CHECK(node.content_attributes->node_interaction_info);
EXPECT_TRUE(node.content_attributes->node_interaction_info
->document_scoped_z_order);
EXPECT_THAT(
node.content_attributes->node_interaction_info->clickability_reasons,
testing::UnorderedElementsAreArray(expected_reasons));
}
const mojom::blink::AIPageContentPtr& Content() { return last_content_; }
protected:
const mojom::blink::AIPageContentOptions default_options_ =
GetAIPageContentOptionsForTest();
test::TaskEnvironment task_environment_;
frame_test_helpers::WebViewHelper helper_;
private:
static void UpdateWebSettings(WebSettings* settings) {
settings->SetTextAreasAreResizable(true);
}
mojom::blink::AIPageContentPtr last_content_;
mojom::blink::AIPageContentOptions last_options_;
};
TEST_F(AIPageContentAgentTest, Basic) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" div {"
" position: absolute;"
" top: -10px;"
" left: -20px;"
" }"
" </style>"
" <div>text</div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = *Content()->root_node;
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& attributes = *root.content_attributes;
EXPECT_TRUE(attributes.dom_node_id.has_value());
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kRoot);
CheckGeometry(root, gfx::Rect(kWindowSize), gfx::Rect(kWindowSize));
const auto& text_node = *ContentRootNode().children_nodes[0];
CheckTextNode(text_node, "text");
const auto& text_attributes = *text_node.content_attributes;
ASSERT_TRUE(text_attributes.geometry);
EXPECT_FALSE(text_attributes.node_interaction_info);
EXPECT_EQ(text_attributes.geometry->outer_bounding_box.x(), -20);
EXPECT_EQ(text_attributes.geometry->outer_bounding_box.y(), -10);
}
TEST_F(AIPageContentAgentTest, Image) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" img {"
" position: absolute;"
" top: -10px;"
" left: -20px;"
" width: 30px;"
" height: 40px;"
" }"
" </style>"
" <img alt=missing></img>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
auto& document = *helper_.LocalMainFrame()->GetFrame()->GetDocument();
document.getElementsByTagName(AtomicString("img"))
->item(0)
->setAttribute(html_names::kSrcAttr, AtomicString(kSmallImage));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
auto& image_node = *root.children_nodes[0];
CheckImageNode(image_node, "missing");
CheckGeometry(image_node, gfx::Rect(-20, -10, 30, 40),
gfx::Rect(0, 0, 10, 30));
}
TEST_F(AIPageContentAgentTest, ImageWithAriaLabel) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <img aria-label='hello'></img>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
auto& image_node = *root.children_nodes[0];
EXPECT_EQ("hello", image_node.content_attributes->label);
}
TEST_F(AIPageContentAgentTest, ImageIsAdRelated) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <img id='ads'></img>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
auto& document = *helper_.LocalMainFrame()->GetFrame()->GetDocument();
To<HTMLImageElement>(document.getElementById(AtomicString("ads")))
->SetIsAdRelated();
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
auto& image_node = *root.children_nodes[0];
EXPECT_TRUE(image_node.content_attributes->is_ad_related);
}
TEST_F(AIPageContentAgentTest, ImageNoAltText) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
base::StringPrintf("<body>"
" <style>"
" div::before {"
" content: url(%s);"
" }"
" </style>"
" <div>text</div>"
"</body>",
kSmallImage),
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
}
TEST_F(AIPageContentAgentTest, Video) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <video src='https://example.com/video.mp4'></video>"
" <video "
"src='https://example.com/video.mp4?param1=value1&param2=value2'></video>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 2u);
const auto& video1 = *root.children_nodes[0];
EXPECT_EQ(video1.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kVideo);
ASSERT_TRUE(video1.content_attributes->video_data);
EXPECT_EQ(video1.content_attributes->video_data->url,
blink::KURL("https://example.com/video.mp4"));
const auto& video2 = *root.children_nodes[1];
EXPECT_EQ(video2.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kVideo);
ASSERT_TRUE(video2.content_attributes->video_data);
EXPECT_EQ(
video2.content_attributes->video_data->url,
blink::KURL("https://example.com/video.mp4?param1=value1&param2=value2"));
}
TEST_F(AIPageContentAgentTest, Headings) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <h1>Heading 1</h1>"
" <h2>Heading 2</h2>"
" <h3>Heading 3</h3>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 3u);
const auto& heading1 = *root.children_nodes[0];
CheckHeadingNode(heading1);
ASSERT_EQ(heading1.children_nodes.size(), 1u);
const auto& heading1_text_node = *heading1.children_nodes[0];
CheckTextNode(heading1_text_node, "Heading 1");
const auto& heading2 = *root.children_nodes[1];
CheckHeadingNode(heading2);
ASSERT_EQ(heading2.children_nodes.size(), 1u);
const auto& heading2_text_node = *heading2.children_nodes[0];
CheckTextNode(heading2_text_node, "Heading 2");
const auto& heading3 = *root.children_nodes[2];
CheckHeadingNode(heading3);
ASSERT_EQ(heading3.children_nodes.size(), 1u);
const auto& heading3_text_node = *heading3.children_nodes[0];
CheckTextNode(heading3_text_node, "Heading 3");
}
TEST_F(AIPageContentAgentTest, Paragraph) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" p {"
" position: fixed;"
" top: -10px;"
" left: -20px;"
" width: 200px;"
" height: 40px;"
" margin: 0;"
" }"
" </style>"
" <p>text inside paragraph</p>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& paragraph = *root.children_nodes[0];
CheckParagraphNode(paragraph);
CheckGeometry(paragraph, gfx::Rect(-20, -10, 200, 40),
gfx::Rect(0, 0, 180, 30));
ASSERT_EQ(paragraph.children_nodes.size(), 1u);
const auto& paragraph_text_node = *paragraph.children_nodes[0];
CheckTextNode(paragraph_text_node, "text inside paragraph");
}
TEST_F(AIPageContentAgentTest, Lists) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <ul>"
" <li>Item 1</li>"
" <li>Item 2</li>"
" </ul>"
" <ol>"
" <li>Step 1</li>"
" <li>Step 2</li>"
" </ol>"
" <dl>"
" <dt>Detail 1 title</dt>"
" <dd>Detail 1 description</dd>"
" <dt>Detail 2 title</dt>"
" <dd>Detail 2 description</dd>"
" </dl>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 3u);
const auto& ul = *root.children_nodes[0];
CheckUnorderedListNode(ul);
ASSERT_EQ(ul.children_nodes.size(), 2u);
CheckListItemWithText(*ul.children_nodes[0], "Item 1");
CheckListItemWithText(*ul.children_nodes[1], "Item 2");
const auto& ol = *root.children_nodes[1];
CheckOrderedListNode(ol);
ASSERT_EQ(ol.children_nodes.size(), 2u);
CheckListItemWithText(*ol.children_nodes[0], "Step 1");
CheckListItemWithText(*ol.children_nodes[1], "Step 2");
const auto& dl = *root.children_nodes[2];
CheckUnorderedListNode(dl);
ASSERT_EQ(dl.children_nodes.size(), 4u);
CheckListItemWithText(*dl.children_nodes[0], "Detail 1 title");
CheckListItemWithText(*dl.children_nodes[1], "Detail 1 description");
CheckListItemWithText(*dl.children_nodes[2], "Detail 2 title");
CheckListItemWithText(*dl.children_nodes[3], "Detail 2 description");
}
TEST_F(AIPageContentAgentTest, IFrameWithContent) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <iframe src='about:blank'></iframe>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
auto* iframe_element =
To<HTMLIFrameElement>(helper_.LocalMainFrame()
->GetFrame()
->GetDocument()
->getElementsByTagName(AtomicString("iframe"))
->item(0));
ASSERT_TRUE(iframe_element);
// Access the iframe's document and set some content
auto* iframe_doc = iframe_element->contentDocument();
ASSERT_TRUE(iframe_doc);
iframe_doc->body()->SetInnerHTMLWithoutTrustedTypes(
"<body>inside iframe</body>");
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& iframe = *root.children_nodes[0];
const auto& iframe_attributes = *iframe.content_attributes;
EXPECT_EQ(iframe_attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kIframe);
EXPECT_FALSE(iframe_attributes.is_ad_related);
const auto& iframe_root = *iframe.children_nodes[0];
CheckTextNode(*iframe_root.children_nodes[0], "inside iframe");
}
TEST_F(AIPageContentAgentTest, IFrameAds) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <iframe src='about:blank'></iframe>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
auto* iframe_element =
To<HTMLIFrameElement>(helper_.LocalMainFrame()
->GetFrame()
->GetDocument()
->getElementsByTagName(AtomicString("iframe"))
->item(0));
ASSERT_TRUE(iframe_element);
// Mark iframe's ad evidence.
blink::FrameAdEvidence ad_evidence;
ad_evidence.set_created_by_ad_script(
blink::mojom::FrameCreationStackEvidence::kCreatedByAdScript);
ad_evidence.set_is_complete();
To<LocalFrame>(iframe_element->ContentFrame())->SetAdEvidence(ad_evidence);
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& iframe = *root.children_nodes[0];
const auto& iframe_attributes = *iframe.content_attributes;
EXPECT_EQ(iframe_attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kIframe);
EXPECT_TRUE(iframe_attributes.is_ad_related);
}
TEST_F(AIPageContentAgentTest, CrossSiteIframeIncluded) {
KURL main_url = url_test_helpers::ToKURL("http://example.com/main.html");
KURL cross_origin_url =
url_test_helpers::ToKURL("http://www.example.com/frame.html");
KURL cross_site_url =
url_test_helpers::ToKURL("http://altostrat.com/frame_another.html");
// Mock the cross origin, same-site iframe's content.
url_test_helpers::RegisterMockedURLLoadFromBase(
WebString::FromUTF8("http://www.example.com/"), test::CoreTestDataPath(),
WebString::FromUTF8("frame.html"));
// Mock the cross-site iframe's content.
url_test_helpers::RegisterMockedURLLoadFromBase(
WebString::FromUTF8("http://altostrat.com/"), test::CoreTestDataPath(),
WebString::FromUTF8("frame_another.html"));
// Load the main page which contains the same-site iframe and the cross-origin
// iframe.
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
"<iframe src='http://www.example.com/frame.html'></iframe>"
"<iframe src='http://altostrat.com/frame_another.html'></iframe>"
"</body>",
main_url);
// Let the iframe load.
test::RunPendingTasks();
auto options = GetAIPageContentOptionsForTest();
options.include_same_site_only = false;
GetAIPageContent(options);
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 2u);
// Both nodes should be present.
const auto& same_site_iframe_node = *root.children_nodes[0];
const auto& cross_site_iframe_node = *root.children_nodes[1];
CheckIframeNode(same_site_iframe_node);
CheckIframeNode(cross_site_iframe_node);
// The contents of both nodes should be present as well.
ASSERT_EQ(same_site_iframe_node.children_nodes.size(), 1u);
ASSERT_EQ(cross_site_iframe_node.children_nodes.size(), 1u);
const auto& same_site_iframe_root = *same_site_iframe_node.children_nodes[0];
const auto& cross_site_iframe_root =
*cross_site_iframe_node.children_nodes[0];
CheckRootNode(same_site_iframe_root);
CheckRootNode(cross_site_iframe_root);
ASSERT_EQ(same_site_iframe_root.children_nodes.size(), 1u);
ASSERT_EQ(cross_site_iframe_root.children_nodes.size(), 1u);
CheckTextNode(*same_site_iframe_root.children_nodes[0], "I am an iframe\n");
CheckTextNode(*cross_site_iframe_root.children_nodes[0],
"I am another iframe\n");
}
TEST_F(AIPageContentAgentTest, CrossSiteIframeExcluded) {
KURL main_url = url_test_helpers::ToKURL("http://example.com/main.html");
KURL cross_origin_url =
url_test_helpers::ToKURL("http://www.example.com/frame.html");
KURL cross_site_url =
url_test_helpers::ToKURL("http://altostrat.com/frame_another.html");
// Mock the cross origin, same-site iframe's content.
url_test_helpers::RegisterMockedURLLoadFromBase(
WebString::FromUTF8("http://www.example.com/"), test::CoreTestDataPath(),
WebString::FromUTF8("frame.html"));
// Mock the cross-site iframe's content.
url_test_helpers::RegisterMockedURLLoadFromBase(
WebString::FromUTF8("http://altostrat.com/"), test::CoreTestDataPath(),
WebString::FromUTF8("frame_another.html"));
// Load the main page which contains the same-site iframe and the cross-origin
// iframe.
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
"<iframe src='http://www.example.com/frame.html'></iframe>"
"<iframe src='http://altostrat.com/frame_another.html'></iframe>"
"</body>",
main_url);
// Let the iframe load.
test::RunPendingTasks();
auto options = GetAIPageContentOptionsForTest();
options.include_same_site_only = true;
GetAIPageContent(options);
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 2u);
// Both nodes should be present.
const auto& same_site_iframe_node = *root.children_nodes[0];
const auto& cross_site_iframe_node = *root.children_nodes[1];
CheckIframeNode(same_site_iframe_node);
CheckIframeNode(cross_site_iframe_node);
// Only the contents of the same-site iframe should be present.
ASSERT_EQ(same_site_iframe_node.children_nodes.size(), 1u);
ASSERT_TRUE(cross_site_iframe_node.children_nodes.empty());
ASSERT_EQ(cross_site_iframe_node.content_attributes->iframe_data->content
->get_redacted_frame_metadata()
->reason,
blink::mojom::RedactedFrameMetadata_Reason::kCrossSite);
const auto& same_site_iframe_root = *same_site_iframe_node.children_nodes[0];
CheckRootNode(same_site_iframe_root);
ASSERT_EQ(same_site_iframe_root.children_nodes.size(), 1u);
CheckTextNode(*same_site_iframe_root.children_nodes[0], "I am an iframe\n");
}
TEST_F(AIPageContentAgentTest, NoLayoutElement) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <div style='display: none;'>Hidden Content</div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_TRUE(root.children_nodes.empty());
}
TEST_F(AIPageContentAgentTest, VisibilityHidden) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <div style='visibility: hidden;'>Hidden Content</div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_TRUE(root.children_nodes.empty());
}
TEST_F(AIPageContentAgentTest, TextSize) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <h1>Extra large text</h1>"
" <h2>Large text</h2>"
" <p>Regular text</p>"
" <h6>Small text</h6>"
" <p style='font-size: 0.25em;'>Extra small text</p>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 5u);
const auto& xl_text = *root.children_nodes[0];
CheckHeadingNode(xl_text);
CheckTextNode(*xl_text.children_nodes[0], "Extra large text");
CheckTextSize(*xl_text.children_nodes[0],
mojom::blink::AIPageContentTextSize::kXL);
const auto& l_text = *root.children_nodes[1];
CheckHeadingNode(l_text);
CheckTextNode(*l_text.children_nodes[0], "Large text");
CheckTextSize(*l_text.children_nodes[0],
mojom::blink::AIPageContentTextSize::kL);
const auto& m_text = *root.children_nodes[2];
CheckParagraphNode(m_text);
CheckTextNode(*m_text.children_nodes[0], "Regular text");
CheckTextSize(*m_text.children_nodes[0],
mojom::blink::AIPageContentTextSize::kM);
const auto& s_text = *root.children_nodes[3];
CheckHeadingNode(s_text);
CheckTextNode(*s_text.children_nodes[0], "Small text");
CheckTextSize(*s_text.children_nodes[0],
mojom::blink::AIPageContentTextSize::kS);
const auto& xs_text = *root.children_nodes[4];
CheckParagraphNode(xs_text);
CheckTextNode(*xs_text.children_nodes[0], "Extra small text");
CheckTextSize(*xs_text.children_nodes[0],
mojom::blink::AIPageContentTextSize::kXS);
}
TEST_F(AIPageContentAgentTest, TextEmphasis) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
"<p>Regular text"
"<b>Bolded text</b>"
"<i>Italicized text</i>"
"<u>Underlined text</u>"
"<sub>Subscript text</sub>"
"<sup>Superscript text</sup>"
"<em>Emphasized text</em>"
"<strong>Strong text</strong>"
"</p>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& paragraph = *root.children_nodes[0];
CheckParagraphNode(paragraph);
ASSERT_EQ(paragraph.children_nodes.size(), 8u);
const auto& regular_text = *paragraph.children_nodes[0];
CheckTextNode(regular_text, "Regular text");
CheckTextEmphasis(regular_text, false);
const auto& bolded_text = *paragraph.children_nodes[1];
CheckTextNode(bolded_text, "Bolded text");
CheckTextEmphasis(bolded_text, true);
const auto& italicized_text = *paragraph.children_nodes[2];
CheckTextNode(italicized_text, "Italicized text");
CheckTextEmphasis(italicized_text, true);
const auto& underlined_text = *paragraph.children_nodes[3];
CheckTextNode(underlined_text, "Underlined text");
CheckTextEmphasis(underlined_text, true);
const auto& subscript_text = *paragraph.children_nodes[4];
CheckTextNode(subscript_text, "Subscript text");
CheckTextEmphasis(subscript_text, true);
const auto& superscript_text = *paragraph.children_nodes[5];
CheckTextNode(superscript_text, "Superscript text");
CheckTextEmphasis(superscript_text, true);
const auto& emphasized_text = *paragraph.children_nodes[6];
CheckTextNode(emphasized_text, "Emphasized text");
CheckTextEmphasis(emphasized_text, true);
const auto& strong_text = *paragraph.children_nodes[7];
CheckTextNode(strong_text, "Strong text");
CheckTextEmphasis(strong_text, true);
}
TEST_F(AIPageContentAgentTest, TextColor) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
"<p>Regular text</p>"
"<p style='color: red'>Red text</p>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 2u);
const auto& paragraph = *root.children_nodes[0];
CheckParagraphNode(paragraph);
ASSERT_EQ(paragraph.children_nodes.size(), 1u);
const auto& regular_text = *paragraph.children_nodes[0];
CheckTextNode(regular_text, "Regular text");
CheckTextColor(regular_text, Color(0, 0, 0).Rgb());
const auto& red_paragraph = *root.children_nodes[1];
CheckParagraphNode(paragraph);
ASSERT_EQ(paragraph.children_nodes.size(), 1u);
const auto& bolded_text = *red_paragraph.children_nodes[0];
CheckTextNode(bolded_text, "Red text");
CheckTextColor(bolded_text, Color(255, 0, 0).Rgb());
}
TEST_F(AIPageContentAgentTest, Table) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <table>"
" <caption>Table caption</caption>"
" <thead>"
" <th colspan='2'>Header</th>"
" </thead>"
" <tr>"
" <td>Row 1 Column 1</td>"
" <td>Row 1 Column 2</td>"
" </tr>"
" <tr>"
" <td>Row 2 Column 1</td>"
" <td>Row 2 Column 2</td>"
" </tr>"
" <tfoot>"
" <td>Footer 1</td>"
" <td>Footer 2</td>"
" </tfoot>"
" </table>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& table = *root.children_nodes[0];
CheckTableNode(table, "Table caption");
ASSERT_EQ(table.children_nodes.size(), 5u);
const auto& caption_text = *table.children_nodes[0];
CheckTextNode(caption_text, "Table caption");
const auto& header1 = *table.children_nodes[1];
CheckTableRowNode(header1, mojom::blink::AIPageContentTableRowType::kHeader);
ASSERT_EQ(header1.children_nodes.size(), 1u);
const auto& header1_cell1 = *header1.children_nodes[0];
CheckTableCellNode(header1_cell1);
CheckTextNode(*header1_cell1.children_nodes[0], "Header");
const auto& row1 = *table.children_nodes[2];
CheckTableRowNode(row1, mojom::blink::AIPageContentTableRowType::kBody);
ASSERT_EQ(row1.children_nodes.size(), 2u);
const auto& row1_cell1 = *row1.children_nodes[0];
CheckTableCellNode(row1_cell1);
CheckTextNode(*row1_cell1.children_nodes[0], "Row 1 Column 1");
const auto& row1_cell2 = *row1.children_nodes[1];
CheckTableCellNode(row1_cell2);
CheckTextNode(*row1_cell2.children_nodes[0], "Row 1 Column 2");
const auto& row2 = *table.children_nodes[3];
CheckTableRowNode(row2, mojom::blink::AIPageContentTableRowType::kBody);
ASSERT_EQ(row2.children_nodes.size(), 2u);
const auto& row2_cell1 = *row2.children_nodes[0];
CheckTableCellNode(row2_cell1);
CheckTextNode(*row2_cell1.children_nodes[0], "Row 2 Column 1");
const auto& row2_cell2 = *row2.children_nodes[1];
CheckTableCellNode(row2_cell2);
CheckTextNode(*row2_cell2.children_nodes[0], "Row 2 Column 2");
const auto& footer = *table.children_nodes[4];
CheckTableRowNode(footer, mojom::blink::AIPageContentTableRowType::kFooter);
ASSERT_EQ(footer.children_nodes.size(), 2u);
const auto& footer_cell1 = *footer.children_nodes[0];
CheckTableCellNode(footer_cell1);
CheckTextNode(*footer_cell1.children_nodes[0], "Footer 1");
const auto& footer_cell2 = *footer.children_nodes[1];
CheckTableCellNode(footer_cell2);
CheckTextNode(*footer_cell2.children_nodes[0], "Footer 2");
}
TEST_F(AIPageContentAgentTest, TableMadeWithCss) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" .table {"
" display: table;"
" border-collapse: collapse;"
" width: 100%;"
" }"
" .row {"
" display: table-row;"
" }"
" .cell {"
" display: table-cell;"
" border: 1px solid #000;"
" padding: 8px;"
" text-align: center;"
" }"
" .header {"
" background-color: #f4f4f4;"
" font-weight: bold;"
" }"
" </style>"
" <div class='table'>"
// Header Rows
" <div class='row header'>"
" <div class='cell' colspan='2'>Personal Info</div>"
" <div class='cell' colspan='2'>Contact Info</div>"
" </div>"
" <div class='row header'>"
" <div class='cell'>Name</div>"
" <div class='cell'>Age</div>"
" <div class='cell'>Email</div>"
" <div class='cell'>Phone</div>"
" </div>"
// Body Rows
" <div class='row'>"
" <div class='cell'>John Doe</div>"
" <div class='cell'>30</div>"
" <div class='cell'>john.doe@example.com</div>"
" <div class='cell'>123-456-7890</div>"
" </div>"
" <div class='row'>"
" <div class='cell'>Jane Smith</div>"
" <div class='cell'>28</div>"
" <div class='cell'>jane.smith@example.com</div>"
" <div class='cell'>987-654-3210</div>"
" </div>"
" </div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& table = *root.children_nodes[0];
CheckTableNode(table);
ASSERT_EQ(table.children_nodes.size(), 4u);
const auto& row1 = *table.children_nodes[0];
CheckTableRowNode(row1, mojom::blink::AIPageContentTableRowType::kBody);
ASSERT_EQ(row1.children_nodes.size(), 2u);
const auto& row1_cell1 = *row1.children_nodes[0];
CheckTableCellNode(row1_cell1);
CheckTextNode(*row1_cell1.children_nodes[0], "Personal Info");
const auto& row1_cell2 = *row1.children_nodes[1];
CheckTableCellNode(row1_cell2);
CheckTextNode(*row1_cell2.children_nodes[0], "Contact Info");
const auto& row2 = *table.children_nodes[1];
CheckTableRowNode(row2, mojom::blink::AIPageContentTableRowType::kBody);
ASSERT_EQ(row2.children_nodes.size(), 4u);
const auto& row2_cell1 = *row2.children_nodes[0];
CheckTableCellNode(row2_cell1);
CheckTextNode(*row2_cell1.children_nodes[0], "Name");
const auto& row2_cell2 = *row2.children_nodes[1];
CheckTableCellNode(row2_cell2);
CheckTextNode(*row2_cell2.children_nodes[0], "Age");
const auto& row2_cell3 = *row2.children_nodes[2];
CheckTableCellNode(row2_cell3);
CheckTextNode(*row2_cell3.children_nodes[0], "Email");
const auto& row2_cell4 = *row2.children_nodes[3];
CheckTableCellNode(row2_cell4);
CheckTextNode(*row2_cell4.children_nodes[0], "Phone");
const auto& row3 = *table.children_nodes[2];
CheckTableRowNode(row3, mojom::blink::AIPageContentTableRowType::kBody);
ASSERT_EQ(row3.children_nodes.size(), 4u);
const auto& row3_cell1 = *row3.children_nodes[0];
CheckTableCellNode(row3_cell1);
CheckTextNode(*row3_cell1.children_nodes[0], "John Doe");
const auto& row3_cell2 = *row3.children_nodes[1];
CheckTableCellNode(row3_cell2);
CheckTextNode(*row3_cell2.children_nodes[0], "30");
const auto& row3_cell3 = *row3.children_nodes[2];
CheckTableCellNode(row3_cell3);
CheckTextNode(*row3_cell3.children_nodes[0], "john.doe@example.com");
const auto& row3_cell4 = *row3.children_nodes[3];
CheckTableCellNode(row3_cell4);
CheckTextNode(*row3_cell4.children_nodes[0], "123-456-7890");
const auto& row4 = *table.children_nodes[3];
CheckTableRowNode(row4, mojom::blink::AIPageContentTableRowType::kBody);
ASSERT_EQ(row4.children_nodes.size(), 4u);
const auto& row4_cell1 = *row4.children_nodes[0];
CheckTableCellNode(row4_cell1);
CheckTextNode(*row4_cell1.children_nodes[0], "Jane Smith");
const auto& row4_cell2 = *row4.children_nodes[1];
CheckTableCellNode(row4_cell2);
CheckTextNode(*row4_cell2.children_nodes[0], "28");
const auto& row4_cell3 = *row4.children_nodes[2];
CheckTableCellNode(row4_cell3);
CheckTextNode(*row4_cell3.children_nodes[0], "jane.smith@example.com");
const auto& row4_cell4 = *row4.children_nodes[3];
CheckTableCellNode(row4_cell4);
CheckTextNode(*row4_cell4.children_nodes[0], "987-654-3210");
}
TEST_F(AIPageContentAgentTest, FigureCaptionDisplayAsTableCaption) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<figure style='display:table'>
<figcaption style='display:table-caption'>
<a href='https://www.youtube.com/'>Youtube</a>
</figcaption>
</figure>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& figure = *root.children_nodes.at(0);
ASSERT_TRUE(figure.content_attributes->node_interaction_info);
EXPECT_EQ(figure.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kTable);
ASSERT_EQ(figure.children_nodes.size(), 1u);
const auto& fig_caption = *figure.children_nodes.at(0);
ASSERT_TRUE(fig_caption.content_attributes->node_interaction_info);
EXPECT_EQ(fig_caption.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kContainer);
ASSERT_EQ(fig_caption.children_nodes.size(), 1u);
const auto& anchor = *fig_caption.children_nodes.at(0);
CheckAnchorNode(anchor, blink::KURL("https://www.youtube.com/"), {});
}
TEST_F(AIPageContentAgentTest, LandmarkSections) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <header>Header</header>"
" <nav>Navigation</nav>"
" <search>Search</search>"
" <main>Main content</main>"
" <article>Article</article>"
" <section>Section</section>"
" <aside>Aside</aside>"
" <footer>Footer</footer>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 8u);
const auto& header = *root.children_nodes[0];
CheckContainerNode(header);
CheckAnnotatedRole(header, mojom::blink::AIPageContentAnnotatedRole::kHeader);
CheckTextNode(*header.children_nodes[0], "Header");
const auto& nav = *root.children_nodes[1];
CheckContainerNode(nav);
CheckAnnotatedRole(nav, mojom::blink::AIPageContentAnnotatedRole::kNav);
CheckTextNode(*nav.children_nodes[0], "Navigation");
const auto& search = *root.children_nodes[2];
CheckContainerNode(search);
CheckAnnotatedRole(search, mojom::blink::AIPageContentAnnotatedRole::kSearch);
CheckTextNode(*search.children_nodes[0], "Search");
const auto& main = *root.children_nodes[3];
CheckContainerNode(main);
CheckAnnotatedRole(main, mojom::blink::AIPageContentAnnotatedRole::kMain);
CheckTextNode(*main.children_nodes[0], "Main content");
const auto& article = *root.children_nodes[4];
CheckContainerNode(article);
CheckAnnotatedRole(article,
mojom::blink::AIPageContentAnnotatedRole::kArticle);
CheckTextNode(*article.children_nodes[0], "Article");
const auto& section = *root.children_nodes[5];
CheckContainerNode(section);
CheckAnnotatedRole(section,
mojom::blink::AIPageContentAnnotatedRole::kSection);
CheckTextNode(*section.children_nodes[0], "Section");
const auto& aside = *root.children_nodes[6];
CheckContainerNode(aside);
CheckAnnotatedRole(aside, mojom::blink::AIPageContentAnnotatedRole::kAside);
CheckTextNode(*aside.children_nodes[0], "Aside");
const auto& footer = *root.children_nodes[7];
CheckContainerNode(footer);
CheckAnnotatedRole(footer, mojom::blink::AIPageContentAnnotatedRole::kFooter);
CheckTextNode(*footer.children_nodes[0], "Footer");
}
TEST_F(AIPageContentAgentTest, LandmarkSectionsWithAriaRoles) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <div role='banner'>Header</div>"
" <div role='navigation'>Navigation</div>"
" <div role='search'>Search</div>"
" <div role='main'>Main content</div>"
" <div role='article'>Article</div>"
" <div role='region'>Section</div>"
" <div role='complementary'>Aside</div>"
" <div role='contentinfo'>Footer</div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 8u);
const auto& header = *root.children_nodes[0];
CheckContainerNode(header);
CheckAnnotatedRole(header, mojom::blink::AIPageContentAnnotatedRole::kHeader);
CheckTextNode(*header.children_nodes[0], "Header");
const auto& nav = *root.children_nodes[1];
CheckContainerNode(nav);
CheckAnnotatedRole(nav, mojom::blink::AIPageContentAnnotatedRole::kNav);
CheckTextNode(*nav.children_nodes[0], "Navigation");
const auto& search = *root.children_nodes[2];
CheckContainerNode(search);
CheckAnnotatedRole(search, mojom::blink::AIPageContentAnnotatedRole::kSearch);
CheckTextNode(*search.children_nodes[0], "Search");
const auto& main = *root.children_nodes[3];
CheckContainerNode(main);
CheckAnnotatedRole(main, mojom::blink::AIPageContentAnnotatedRole::kMain);
CheckTextNode(*main.children_nodes[0], "Main content");
const auto& article = *root.children_nodes[4];
CheckContainerNode(article);
CheckAnnotatedRole(article,
mojom::blink::AIPageContentAnnotatedRole::kArticle);
CheckTextNode(*article.children_nodes[0], "Article");
const auto& section = *root.children_nodes[5];
CheckContainerNode(section);
CheckAnnotatedRole(section,
mojom::blink::AIPageContentAnnotatedRole::kSection);
CheckTextNode(*section.children_nodes[0], "Section");
const auto& aside = *root.children_nodes[6];
CheckContainerNode(aside);
CheckAnnotatedRole(aside, mojom::blink::AIPageContentAnnotatedRole::kAside);
CheckTextNode(*aside.children_nodes[0], "Aside");
const auto& footer = *root.children_nodes[7];
CheckContainerNode(footer);
CheckAnnotatedRole(footer, mojom::blink::AIPageContentAnnotatedRole::kFooter);
CheckTextNode(*footer.children_nodes[0], "Footer");
}
TEST_F(AIPageContentAgentTest, FixedPosition) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
" <body>"
" <style>"
" .fixed {"
" position: fixed;"
" top: 50px;"
" left: 50px;"
" width: 200px;"
" }"
" .sticky {"
" position: sticky;"
" top: 50px;"
" left: 3000px;"
" width: 200px;"
" }"
" .normal {"
" width: 250px;"
" height: 80px;"
" margin-top: 20px;"
" }"
" </style>"
" <div class='fixed'>This element stays in place when the page is "
"scrolled.</div>"
" <div class='sticky'>This element stays in place when the page is "
"scrolled.</div>"
" <div class='normal'>This element flows naturally with the "
"document.</div>"
" </body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 3u);
EXPECT_FALSE(root.content_attributes->geometry->is_fixed_or_sticky_position);
const auto& fixed_element = *root.children_nodes[0];
CheckContainerNode(fixed_element);
EXPECT_TRUE(
fixed_element.content_attributes->geometry->is_fixed_or_sticky_position);
CheckTextNode(*fixed_element.children_nodes[0],
"This element stays in place when the page is scrolled.");
const auto& sticky_element = *root.children_nodes[1];
CheckContainerNode(sticky_element);
EXPECT_TRUE(
sticky_element.content_attributes->geometry->is_fixed_or_sticky_position);
CheckTextNode(*sticky_element.children_nodes[0],
"This element stays in place when the page is scrolled.");
const auto& normal_element = *root.children_nodes[2];
CheckContainerNode(normal_element);
EXPECT_FALSE(
normal_element.content_attributes->geometry->is_fixed_or_sticky_position);
CheckTextNode(*normal_element.children_nodes[0],
"This element flows naturally with the document.");
}
TEST_F(AIPageContentAgentTest, RootScroller) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body style='margin: 0px;'>
<div style='width: 200vw; height: 300vh; background: grey;'></div>
<script>
document.scrollingElement.scrollTop=100;
document.scrollingElement.scrollLeft=200;
</script>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_TRUE(root.content_attributes->node_interaction_info);
ASSERT_TRUE(root.content_attributes->node_interaction_info->scroller_info);
const auto& root_scroller =
*root.content_attributes->node_interaction_info->scroller_info;
EXPECT_EQ(root_scroller.scrolling_bounds.width(), 2 * kWindowSize.width());
EXPECT_EQ(root_scroller.scrolling_bounds.height(), 3 * kWindowSize.height());
EXPECT_EQ(root_scroller.visible_area,
gfx::Rect(200, 100, kWindowSize.width(), kWindowSize.height()));
}
class AIPageContentAgentTestWithSubScroller
: public AIPageContentAgentTest,
public testing::WithParamInterface<std::string> {};
TEST_P(AIPageContentAgentTestWithSubScroller, Overflow) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
base::StringPrintf(
R"HTML(
<body style='margin: 0px;'>
<style>
#scroller {
overflow:%s; width: 100vw; height:100vh;
position:relative; top: 30px; left:50px;
}
</style>
<div id='scroller'>
<div style='width: 200vw; height: 300vh; background: grey;'></div>
</div>
<script>
let scroller = document.getElementById('scroller');
scroller.scrollTop=100;
scroller.scrollLeft=200;
</script>
</body>
)HTML",
GetParam()),
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_TRUE(root.content_attributes->node_interaction_info);
ASSERT_TRUE(root.content_attributes->node_interaction_info->scroller_info);
const auto& root_scroller =
*root.content_attributes->node_interaction_info->scroller_info;
EXPECT_EQ(root_scroller.scrolling_bounds.width(), kWindowSize.width() + 50);
EXPECT_EQ(root_scroller.scrolling_bounds.height(), kWindowSize.height() + 30);
EXPECT_EQ(root_scroller.visible_area, gfx::Rect(kWindowSize));
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& child = *root.children_nodes.at(0);
ASSERT_TRUE(child.content_attributes->node_interaction_info);
ASSERT_TRUE(child.content_attributes->node_interaction_info->scroller_info);
const auto& sub_scroller =
*child.content_attributes->node_interaction_info->scroller_info;
EXPECT_EQ(sub_scroller.scrolling_bounds.width(), 2 * kWindowSize.width());
EXPECT_EQ(sub_scroller.scrolling_bounds.height(), 3 * kWindowSize.height());
EXPECT_EQ(sub_scroller.visible_area,
gfx::Rect(200, 100, kWindowSize.width(), kWindowSize.height()));
bool user_scrollable = GetParam() != "hidden";
EXPECT_EQ(sub_scroller.user_scrollable_horizontal, user_scrollable);
EXPECT_EQ(sub_scroller.user_scrollable_vertical, user_scrollable);
}
INSTANTIATE_TEST_SUITE_P(AIPageContentAgentTestWithSubScroller,
AIPageContentAgentTestWithSubScroller,
::testing::Values("auto", "scroll", "hidden"));
TEST_F(AIPageContentAgentTest, OverflowVisible) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body style='margin: 0px;'>
<style>
#scroller {
overflow:visible; width: 100vw; height:100vh;
position:relative; top: 30px; left:50px;
}
</style>
<div id='scroller'>
<div style='width: 200vw; height: 300vh; background: grey;'></div>
</div>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_TRUE(root.content_attributes->node_interaction_info);
ASSERT_TRUE(root.content_attributes->node_interaction_info->scroller_info);
const auto& root_scroller =
*root.content_attributes->node_interaction_info->scroller_info;
EXPECT_EQ(root_scroller.scrolling_bounds.width(),
kWindowSize.width() * 2 + 50);
EXPECT_EQ(root_scroller.scrolling_bounds.height(),
kWindowSize.height() * 3 + 30);
EXPECT_EQ(root_scroller.visible_area, gfx::Rect(kWindowSize));
EXPECT_EQ(root.children_nodes.size(), 0u);
}
TEST_F(AIPageContentAgentTest, OverflowClip) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body style='margin: 0px;'>
<style>
#scroller {
overflow:clip; width: 100vw; height:100vh;
position:relative; top: 30px; left:50px;
}
</style>
<div id='scroller'>
<div style='width: 200vw; height: 300vh; background: grey;'></div>
</div>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_TRUE(root.content_attributes->node_interaction_info);
ASSERT_TRUE(root.content_attributes->node_interaction_info->scroller_info);
const auto& root_scroller =
*root.content_attributes->node_interaction_info->scroller_info;
EXPECT_EQ(root_scroller.scrolling_bounds.width(), kWindowSize.width() + 50);
EXPECT_EQ(root_scroller.scrolling_bounds.height(), kWindowSize.height() + 30);
EXPECT_EQ(root_scroller.visible_area, gfx::Rect(kWindowSize));
EXPECT_EQ(root.children_nodes.size(), 0u);
}
TEST_F(AIPageContentAgentTest, Anchors) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <a href='https://www.google.com'>Google</a>"
" <a href='https://www.youtube.com' rel='noopener "
"noreferrer'>YouTube</a>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 2u);
const auto& link = *root.children_nodes[0];
CheckAnchorNode(link, blink::KURL("https://www.google.com/"), {});
const auto& link_text = *link.children_nodes[0];
CheckTextNode(link_text, "Google");
const auto& link_with_rel = *root.children_nodes[1];
CheckAnchorNode(link_with_rel, blink::KURL("https://www.youtube.com/"),
{mojom::blink::AIPageContentAnchorRel::kRelationNoOpener,
mojom::blink::AIPageContentAnchorRel::kRelationNoReferrer});
const auto& link_with_rel_text = *link_with_rel.children_nodes[0];
CheckTextNode(link_with_rel_text, "YouTube");
}
TEST_F(AIPageContentAgentTest, TopLayerContainer) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <dialog id='welcomeDialog' style='position: absolute; overflow: "
"visible;'>This is a dialog.</dialog>"
" <script>"
" const dialog = document.getElementById('welcomeDialog');"
" dialog.showModal();"
" </script>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
// Two nodes: the dialog and its backdrop.
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 2u);
const auto& backdrop = *root.children_nodes[0];
CheckContainerNode(backdrop);
const auto& dialog = *root.children_nodes[1];
CheckContainerNode(dialog);
ASSERT_EQ(dialog.children_nodes.size(), 1u);
const auto& dialog_text = *dialog.children_nodes[0];
CheckTextNode(dialog_text, "This is a dialog.");
}
TEST_F(AIPageContentAgentTest, TableWithAnonymousCells) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<html>"
" <style>"
" #target {"
" display: table;"
" }"
""
" #target::before {"
" content: 'BEFORE';"
" display: table;"
" }"
" </style>"
" <div id='target'></div>"
"</html>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& outer_table = *root.children_nodes[0];
const auto& outer_table_cell = GetSingleTableCell(outer_table);
ASSERT_EQ(outer_table_cell.children_nodes.size(), 1u);
const auto& inner_table = *outer_table_cell.children_nodes[0];
const auto& inner_table_cell = GetSingleTableCell(inner_table);
EXPECT_EQ(inner_table_cell.children_nodes.size(), 1u);
CheckTextNode(*inner_table_cell.children_nodes[0], "BEFORE");
}
TEST_F(AIPageContentAgentTest, ContentVisibilityHidden) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" div {"
" content-visibility: hidden"
" }"
" </style>"
" <div>text</div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& hidden_container = *root.children_nodes[0];
CheckContainerNode(hidden_container);
CheckAnnotatedRole(hidden_container,
mojom::blink::AIPageContentAnnotatedRole::kContentHidden);
EXPECT_TRUE(hidden_container.children_nodes.empty());
}
TEST_F(AIPageContentAgentTest, ContentVisibilityAuto) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" #foo {"
" position: relative;"
" top: 8000px;"
" content-visibility: auto"
" }"
" </style>"
" <div id=foo><div>far text</div></div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& text_node = *root.children_nodes[0];
CheckTextNode(text_node, "far text");
const auto& attributes = *text_node.content_attributes;
EXPECT_TRUE(attributes.dom_node_id.has_value());
}
TEST_F(AIPageContentAgentTest, HiddenUntilFound) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" body {"
" margin: 0; font-size: 100px;"
" }"
" </style>"
" <header hidden=until-found>hidden text</header><div>visible text</div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 2u);
const auto& hidden_container = *root.children_nodes[0];
CheckContainerNode(hidden_container);
CheckAnnotatedRoles(
hidden_container,
{mojom::blink::AIPageContentAnnotatedRole::kHeader,
mojom::blink::AIPageContentAnnotatedRole::kContentHidden});
EXPECT_EQ(hidden_container.children_nodes.size(), 1u);
const auto& hidden_text_node = *hidden_container.children_nodes[0];
CheckTextNode(hidden_text_node, "hidden text");
const auto& visible_text_node = *root.children_nodes[1];
CheckTextNode(visible_text_node, "visible text");
}
TEST_F(AIPageContentAgentTest, HiddenUntilFoundNoInvalidationAllowed) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<style>
body {
margin: 0; font-size: 100px;
}
</style>
<header hidden=until-found>hidden text</header><div>visible text</div>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
LocalFrameView::InvalidationDisallowedScope disallow(
*helper_.LocalMainFrame()->GetFrame()->View());
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 2u);
const auto& hidden_container = *root.children_nodes[0];
CheckContainerNode(hidden_container);
CheckAnnotatedRoles(
hidden_container,
{mojom::blink::AIPageContentAnnotatedRole::kHeader,
mojom::blink::AIPageContentAnnotatedRole::kContentHidden});
EXPECT_TRUE(hidden_container.children_nodes.empty());
const auto& visible_text_node = *root.children_nodes[1];
CheckTextNode(visible_text_node, "visible text");
}
TEST_F(AIPageContentAgentTest, HiddenUntilFoundGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" body {"
" margin: 0; font-size: 100px;"
" }"
" </style>"
" <header hidden=until-found>hidden text</header>visible text"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 2u);
const auto& hidden_container = *root.children_nodes[0];
CheckContainerNode(hidden_container);
CheckAnnotatedRoles(
hidden_container,
{mojom::blink::AIPageContentAnnotatedRole::kHeader,
mojom::blink::AIPageContentAnnotatedRole::kContentHidden});
EXPECT_EQ(hidden_container.children_nodes.size(), 1u);
// The hidden container continues to have an empty layout size even when
// display locks are forced.
ASSERT_TRUE(hidden_container.content_attributes->geometry);
const auto& hidden_container_geometry =
*hidden_container.content_attributes->geometry;
EXPECT_TRUE(hidden_container_geometry.outer_bounding_box.IsEmpty());
EXPECT_TRUE(hidden_container_geometry.visible_bounding_box.IsEmpty());
const auto& hidden_text_node = *hidden_container.children_nodes[0];
CheckTextNode(hidden_text_node, "hidden text");
ASSERT_TRUE(hidden_text_node.content_attributes->geometry);
const auto& hidden_text_geometry =
*hidden_text_node.content_attributes->geometry;
CheckAlmostEquals(hidden_text_geometry.outer_bounding_box.origin(),
gfx::Point(0, 0));
EXPECT_FALSE(hidden_text_geometry.outer_bounding_box.IsEmpty());
EXPECT_TRUE(hidden_text_geometry.visible_bounding_box.IsEmpty());
const auto& visible_text_node = *root.children_nodes[1];
CheckTextNode(visible_text_node, "visible text");
EXPECT_TRUE(visible_text_node.content_attributes->geometry);
const auto& visible_text_geometry =
*visible_text_node.content_attributes->geometry;
CheckAlmostEquals(visible_text_geometry.outer_bounding_box.origin(),
gfx::Point(0, 0));
EXPECT_FALSE(visible_text_geometry.outer_bounding_box.IsEmpty());
EXPECT_EQ(visible_text_geometry.outer_bounding_box,
visible_text_geometry.visible_bounding_box);
}
TEST_F(AIPageContentAgentTest, HiddenUntilFoundInsideIframe) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" body {"
" margin: 0;"
" font-size: 100px;"
" }"
" </style>"
" <iframe srcdoc='<div hidden=until-found>hidden "
"text</div>'></iframe>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& iframe_node = *root.children_nodes[0];
EXPECT_EQ(iframe_node.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kIframe);
EXPECT_EQ(iframe_node.children_nodes.size(), 1u);
const auto& iframe_root = *iframe_node.children_nodes[0];
EXPECT_EQ(iframe_root.children_nodes.size(), 1u);
const auto& hidden_container = *iframe_root.children_nodes[0];
CheckContainerNode(hidden_container);
CheckAnnotatedRole(hidden_container,
mojom::blink::AIPageContentAnnotatedRole::kContentHidden);
EXPECT_EQ(hidden_container.children_nodes.size(), 1u);
const auto& hidden_text_node = *hidden_container.children_nodes[0];
CheckTextNode(hidden_text_node, "hidden text");
}
TEST_F(AIPageContentAgentTest, HiddenUntilFoundOnIframe) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" body {"
" margin: 0;"
" font-size: 100px;"
" }"
" </style>"
" <iframe hidden=until-found srcdoc='<div>hidden "
"text</div>'></iframe>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& iframe_node = *root.children_nodes[0];
EXPECT_EQ(iframe_node.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kIframe);
EXPECT_EQ(iframe_node.children_nodes.size(), 1u);
const auto& iframe_root = *iframe_node.children_nodes[0];
const auto& hidden_text_node = *iframe_root.children_nodes[0];
CheckTextNode(hidden_text_node, "hidden text");
}
TEST_F(AIPageContentAgentTest, LineBreak) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
"<div style=\"width: 100px; height:100px\">"
"Lorem Ipsum is simply dummy text of the printing and "
"typesetting industry.<br>Lorem Ipsum has been the "
"industry's standard dummy text ever since the 1500s, "
"when an unknown printer took a galley of type and "
"scrambled it to make a type specimen book. It has "
"survived not only five centuries, but also the leap "
"into electronic typesetting, remaining essentially "
"unchanged. It was popularised in the 1960s with the "
"release of Letraset sheets containing Lorem Ipsum "
"passages, and more recently with desktop publishing "
"software like Aldus PageMaker including versions of "
"Lorem Ipsum."
"</div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 2u);
CheckTextNode(*root.children_nodes[0],
"Lorem Ipsum is simply dummy text of the printing and "
"typesetting industry.");
CheckTextNode(
*root.children_nodes[1],
"Lorem Ipsum has been the industry's standard dummy text ever since the "
"1500s, when an unknown printer took a galley of type and scrambled it "
"to make a type specimen book. It has survived not only five centuries, "
"but also the leap into electronic typesetting, remaining essentially "
"unchanged. It was popularised in the 1960s with the release of Letraset "
"sheets containing Lorem Ipsum passages, and more recently with desktop "
"publishing software like Aldus PageMaker including versions of Lorem "
"Ipsum.");
}
TEST_F(AIPageContentAgentTest, VisibilityHiddenOnSubtree) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" header {"
" visibility: hidden"
" }"
" </style>"
" <header>text</header>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 0u);
}
TEST_F(AIPageContentAgentTest, VisibilityHiddenOnParentOnly) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" #parent {"
" visibility: hidden"
" }"
" #child {"
" visibility: visible"
" }"
" </style>"
" <header id=parent><div id=child>text</div></header>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& container = *root.children_nodes[0];
CheckContainerNode(container);
EXPECT_EQ(container.children_nodes.size(), 1u);
const auto& text_node = *container.children_nodes[0];
CheckTextNode(text_node, "text");
}
TEST_F(AIPageContentAgentTest, VisibilityHiddenOnIframe) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" iframe {"
" visibility: hidden;"
" }"
" </style>"
" <iframe srcdoc='<div style='visibility: visible'>hidden "
"text</div>'></iframe>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 0u);
}
TEST_F(AIPageContentAgentTest, NoGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <div>text</div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_FALSE(root.content_attributes->geometry);
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& text_node = *root.children_nodes[0];
CheckTextNode(text_node, "text");
EXPECT_FALSE(text_node.content_attributes->geometry);
}
TEST_F(AIPageContentAgentTest, FormWithTextInput) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <form name='myform' action='https://example.com/submit'>"
" <label for='input1'>Lorem Ipsum</label>"
" <input type='text' id='input1' name='LI' value='Lorem'>"
" <label for='input2'>Ipsum Dolor</label>"
" <input type='text' id='input2' name='ID' value='Ipsum' required>"
" </form>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
auto* agent = AIPageContentAgent::GetOrCreateForTesting(
*helper_.LocalMainFrame()->GetFrame()->GetDocument());
ASSERT_TRUE(agent);
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& form = *root.children_nodes[0];
EXPECT_EQ(form.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kForm);
EXPECT_EQ(form.content_attributes->form_data->form_name, "myform");
// The form metadata must expose the normalized action URL so callers can
// surface the submit destination.
EXPECT_EQ(form.content_attributes->form_data->action_url,
url_test_helpers::ToKURL("https://example.com/submit"));
EXPECT_EQ(form.children_nodes.size(), 4u);
CheckTextNode(*form.children_nodes[0], "Lorem Ipsum");
const auto& text_input1 = *form.children_nodes[1];
CheckFormControlNode(text_input1, mojom::blink::FormControlType::kInputText);
EXPECT_EQ(text_input1.content_attributes->form_control_data->field_name,
"LI");
EXPECT_EQ(text_input1.content_attributes->form_control_data->field_value,
"Lorem");
EXPECT_EQ(
text_input1.content_attributes->form_control_data->redaction_decision,
mojom::AIPageContentRedactionDecision::kNoRedactionNecessary);
EXPECT_FALSE(text_input1.content_attributes->form_control_data->is_required);
EXPECT_EQ(text_input1.children_nodes.size(), 1u);
CheckContainerNode(*text_input1.children_nodes[0]);
EXPECT_EQ(text_input1.children_nodes[0]->children_nodes.size(), 1u);
CheckTextNode(*text_input1.children_nodes[0]->children_nodes[0], "Lorem");
CheckTextNode(*form.children_nodes[2], "Ipsum Dolor");
const auto& text_input2 = *form.children_nodes[3];
CheckFormControlNode(text_input2, mojom::blink::FormControlType::kInputText);
EXPECT_EQ(text_input2.content_attributes->form_control_data->field_name,
"ID");
EXPECT_EQ(text_input2.content_attributes->form_control_data->field_value,
"Ipsum");
EXPECT_EQ(
text_input2.content_attributes->form_control_data->redaction_decision,
mojom::AIPageContentRedactionDecision::kNoRedactionNecessary);
EXPECT_TRUE(text_input2.content_attributes->form_control_data->is_required);
EXPECT_EQ(text_input2.children_nodes.size(), 1u);
CheckContainerNode(*text_input2.children_nodes[0]);
EXPECT_EQ(text_input2.children_nodes[0]->children_nodes.size(), 1u);
CheckTextNode(*text_input2.children_nodes[0]->children_nodes[0], "Ipsum");
}
TEST_F(AIPageContentAgentTest, FormWithSelect) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <form name='myform'>"
" <select name='LI'>"
" <option value='Lorem'>Lorem Text</option>"
" <option value='Ipsum'>Ipsum Text</option>"
" </select>"
" </form>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
auto* agent = AIPageContentAgent::GetOrCreateForTesting(
*helper_.LocalMainFrame()->GetFrame()->GetDocument());
ASSERT_TRUE(agent);
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& form = *root.children_nodes[0];
EXPECT_EQ(form.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kForm);
EXPECT_EQ(form.content_attributes->form_data->form_name, "myform");
EXPECT_EQ(form.children_nodes.size(), 1u);
const auto& select = *form.children_nodes[0];
CheckFormControlNode(select, mojom::blink::FormControlType::kSelectOne);
EXPECT_EQ(select.content_attributes->form_control_data->redaction_decision,
mojom::AIPageContentRedactionDecision::kNoRedactionNecessary);
const auto& select_options =
select.content_attributes->form_control_data->select_options;
ASSERT_EQ(select_options.size(), 2u);
EXPECT_EQ(select_options[0]->value, "Lorem");
EXPECT_EQ(select_options[0]->text, "Lorem Text");
EXPECT_TRUE(select_options[0]->is_selected);
EXPECT_EQ(select_options[1]->value, "Ipsum");
EXPECT_EQ(select_options[1]->text, "Ipsum Text");
EXPECT_FALSE(select_options[1]->is_selected);
EXPECT_EQ(select.children_nodes.size(), 1u);
CheckTextNode(*select.children_nodes[0], "Lorem Text");
}
TEST_F(AIPageContentAgentTest, FormWithCheckbox) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <form name='vehicles'>"
" <input type='checkbox' id='vehicle1' name='vehicle1' value='Bike'>"
" <label for='vehicle1'>I have a bike</label><br>"
" <input type='checkbox' id='vehicle2' name='vehicle2' value='Car' "
" checked>"
" <label for='vehicle2'>I have a car</label><br>"
" </form>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
auto* agent = AIPageContentAgent::GetOrCreateForTesting(
*helper_.LocalMainFrame()->GetFrame()->GetDocument());
ASSERT_TRUE(agent);
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& form = *root.children_nodes[0];
EXPECT_EQ(form.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kForm);
EXPECT_EQ(form.content_attributes->form_data->form_name, "vehicles");
EXPECT_EQ(form.children_nodes.size(), 4u);
const auto& checkbox1 = *form.children_nodes[0];
CheckFormControlNode(checkbox1,
mojom::blink::FormControlType::kInputCheckbox);
EXPECT_EQ(checkbox1.content_attributes->form_control_data->field_name,
"vehicle1");
EXPECT_EQ(checkbox1.content_attributes->form_control_data->field_value,
"Bike");
EXPECT_FALSE(checkbox1.content_attributes->form_control_data->is_checked);
EXPECT_EQ(checkbox1.children_nodes.size(), 0u);
CheckTextNode(*form.children_nodes[1], "I have a bike");
const auto& checkbox2 = *form.children_nodes[2];
CheckFormControlNode(checkbox2,
mojom::blink::FormControlType::kInputCheckbox);
EXPECT_EQ(checkbox2.content_attributes->form_control_data->field_name,
"vehicle2");
EXPECT_EQ(checkbox2.content_attributes->form_control_data->field_value,
"Car");
EXPECT_TRUE(checkbox2.content_attributes->form_control_data->is_checked);
EXPECT_EQ(checkbox2.children_nodes.size(), 0u);
CheckTextNode(*form.children_nodes[3], "I have a car");
}
TEST_F(AIPageContentAgentTest, FormWithRadio) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <form name='vehicles'>"
" <input type='radio' id='vehicle1' name='vehicle1' value='Bike'>"
" <label for='vehicle1'>I have a bike</label><br>"
" <input type='radio' id='vehicle2' name='vehicle2' value='Car' "
" checked>"
" <label for='vehicle2'>I have a car</label><br>"
" </form>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
auto* agent = AIPageContentAgent::GetOrCreateForTesting(
*helper_.LocalMainFrame()->GetFrame()->GetDocument());
ASSERT_TRUE(agent);
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& form = *root.children_nodes[0];
EXPECT_EQ(form.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kForm);
EXPECT_EQ(form.content_attributes->form_data->form_name, "vehicles");
EXPECT_EQ(form.children_nodes.size(), 4u);
const auto& radio1 = *form.children_nodes[0];
CheckFormControlNode(radio1, mojom::blink::FormControlType::kInputRadio);
EXPECT_EQ(radio1.content_attributes->form_control_data->field_name,
"vehicle1");
EXPECT_EQ(radio1.content_attributes->form_control_data->field_value, "Bike");
EXPECT_EQ(radio1.content_attributes->form_control_data->redaction_decision,
mojom::AIPageContentRedactionDecision::kNoRedactionNecessary);
EXPECT_FALSE(radio1.content_attributes->form_control_data->is_checked);
EXPECT_EQ(radio1.children_nodes.size(), 0u);
CheckTextNode(*form.children_nodes[1], "I have a bike");
const auto& radio2 = *form.children_nodes[2];
CheckFormControlNode(radio2, mojom::blink::FormControlType::kInputRadio);
EXPECT_EQ(radio2.content_attributes->form_control_data->field_name,
"vehicle2");
EXPECT_EQ(radio2.content_attributes->form_control_data->field_value, "Car");
EXPECT_TRUE(radio2.content_attributes->form_control_data->is_checked);
EXPECT_EQ(radio2.children_nodes.size(), 0u);
CheckTextNode(*form.children_nodes[3], "I have a car");
}
TEST_F(AIPageContentAgentTest, FormWithPassword) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <form>"
" <input id='pwd' type='password' name='Enter password' "
"value='mypassword'>"
" </form>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
auto* agent = AIPageContentAgent::GetOrCreateForTesting(
*helper_.LocalMainFrame()->GetFrame()->GetDocument());
ASSERT_TRUE(agent);
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& form = *root.children_nodes[0];
EXPECT_EQ(form.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kForm);
EXPECT_EQ(form.children_nodes.size(), 1u);
const auto& password = *form.children_nodes[0];
CheckFormControlNode(password, mojom::blink::FormControlType::kInputPassword);
EXPECT_EQ(password.content_attributes->form_control_data->field_name,
"Enter password");
EXPECT_EQ(password.content_attributes->form_control_data->field_value,
nullptr);
EXPECT_EQ(password.content_attributes->form_control_data->redaction_decision,
mojom::AIPageContentRedactionDecision::kRedacted_HasBeenPassword);
EXPECT_EQ(password.children_nodes.size(), 0u);
// Now reveal the password (simulating clicking the eye icon)
// This mimics JavaScript: passwordInput.type = 'text';
Document* document = helper_.LocalMainFrame()->GetFrame()->GetDocument();
auto* input_element =
To<HTMLInputElement>(document->getElementById(AtomicString("pwd")));
ASSERT_TRUE(input_element);
input_element->setType(AtomicString("text"));
// Ensure the DOM is updated
document->UpdateStyleAndLayout(DocumentUpdateReason::kTest);
// Get AI page content again - password should still be hidden
GetAIPageContent();
const auto& root2 = ContentRootNode();
EXPECT_EQ(root2.children_nodes.size(), 1u);
const auto& form2 = *root2.children_nodes[0];
EXPECT_EQ(form2.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kForm);
EXPECT_EQ(form2.children_nodes.size(), 1u);
const auto& revealed_password = *form2.children_nodes[0];
CheckFormControlNode(revealed_password,
mojom::blink::FormControlType::kInputText);
EXPECT_EQ(revealed_password.content_attributes->form_control_data->field_name,
"Enter password");
// Even though the password is revealed, the value should still be hidden
// because HasBeenBeenPasswordField() is true.
EXPECT_EQ(
revealed_password.content_attributes->form_control_data->field_value,
nullptr);
EXPECT_EQ(revealed_password.content_attributes->form_control_data
->redaction_decision,
mojom::AIPageContentRedactionDecision::kRedacted_HasBeenPassword);
EXPECT_EQ(revealed_password.children_nodes.size(), 0u);
input_element->SetValue("");
// Ensure the DOM is updated
document->UpdateStyleAndLayout(DocumentUpdateReason::kTest);
// Get AI page content again - empty passwords are unredacted.
GetAIPageContent();
const auto& root3 = ContentRootNode();
EXPECT_EQ(root3.children_nodes.size(), 1u);
const auto& form3 = *root3.children_nodes[0];
EXPECT_EQ(form3.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kForm);
EXPECT_EQ(form3.children_nodes.size(), 1u);
const auto& empty_password = *form3.children_nodes[0];
CheckFormControlNode(empty_password,
mojom::blink::FormControlType::kInputText);
EXPECT_EQ(empty_password.content_attributes->form_control_data->field_name,
"Enter password");
EXPECT_EQ(empty_password.content_attributes->form_control_data->field_value,
"");
EXPECT_EQ(
empty_password.content_attributes->form_control_data->redaction_decision,
mojom::AIPageContentRedactionDecision::kUnredacted_EmptyPassword);
EXPECT_EQ(empty_password.children_nodes.size(), 1u);
}
TEST_F(AIPageContentAgentTest, InteractiveElementsTextArea) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <textarea>text</textarea>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& text_area = *root.children_nodes[0];
CheckFormControlNode(text_area, mojom::blink::FormControlType::kTextArea);
EXPECT_EQ(text_area.content_attributes->form_control_data->redaction_decision,
mojom::AIPageContentRedactionDecision::kNoRedactionNecessary);
CheckHitTestableAndInteractive(text_area,
{ClickabilityReason::kClickableControl});
// Text area uses a UA shadow DOM internally to create an editable box.
const auto& shadow_div = *text_area.children_nodes[0];
EXPECT_EQ(shadow_div.children_nodes.size(), 1u);
CheckContainerNode(shadow_div);
CheckHitTestableAndInteractive(shadow_div, {ClickabilityReason::kEditable});
EXPECT_EQ(shadow_div.children_nodes.size(), 1u);
const auto& text_area_text = *shadow_div.children_nodes[0];
CheckTextNode(text_area_text, "text");
CheckHitTestableButNotInteractive(text_area_text);
}
TEST_F(AIPageContentAgentTest, InteractiveElementsButton) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <button>button</button>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& button = *root.children_nodes[0];
CheckFormControlNode(button, mojom::blink::FormControlType::kButtonSubmit);
EXPECT_EQ(button.content_attributes->form_control_data->redaction_decision,
mojom::AIPageContentRedactionDecision::kNoRedactionNecessary);
CheckHitTestableAndInteractive(button,
{ClickabilityReason::kClickableControl});
EXPECT_TRUE(button.content_attributes->node_interaction_info->is_focusable);
ASSERT_EQ(button.children_nodes.size(), 1u);
const auto& button_text = *button.children_nodes[0];
CheckTextNode(button_text, "button");
EXPECT_TRUE(button_text.content_attributes->node_interaction_info);
CheckHitTestableButNotInteractive(button_text);
}
TEST_F(AIPageContentAgentTest, InteractiveElementsResizableDiv) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" div {"
" resize: both;"
" overflow: auto;"
" border: 1px solid black;"
" width: 200px;"
" }"
" </style>"
" <div>resize</div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& resize = *ContentRootNode().children_nodes[0];
CheckContainerNode(resize);
ASSERT_TRUE(resize.content_attributes->node_interaction_info);
EXPECT_FALSE(resize.content_attributes->node_interaction_info->scroller_info);
CheckHitTestableButNotInteractive(resize);
ASSERT_EQ(resize.children_nodes.size(), 1u);
const auto& resize_text = *resize.children_nodes[0];
CheckTextNode(resize_text, "resize");
EXPECT_TRUE(resize_text.content_attributes->node_interaction_info);
CheckHitTestableButNotInteractive(resize_text);
}
TEST_F(AIPageContentAgentTest, Selection) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <p id='p1'>Paragraph 1</p>"
" <p id='p2'>Paragraph 2</p>"
" <p id='p3'>Paragraph 3</p>"
" <script>"
" const p1 = document.getElementById('p1');"
" const p2 = document.getElementById('p2');"
" const range = new Range();"
" range.setStart(p1.childNodes[0], 10);"
" range.setEnd(p2.childNodes[0], 9);"
" const selection = window.getSelection();"
" selection.removeAllRanges();"
" selection.addRange(range);"
" </script>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 3u);
const auto& paragraph1 = *root.children_nodes[0];
CheckTextNode(*paragraph1.children_nodes[0], "Paragraph 1");
const auto& paragraph2 = *root.children_nodes[1];
CheckTextNode(*paragraph2.children_nodes[0], "Paragraph 2");
const auto& paragraph3 = *root.children_nodes[2];
CheckTextNode(*paragraph3.children_nodes[0], "Paragraph 3");
const auto& frame_interaction_info =
Content()->frame_data->frame_interaction_info;
ASSERT_TRUE(frame_interaction_info->selection);
const auto& selection = *frame_interaction_info->selection;
EXPECT_EQ(selection.selected_text, "1\n\nParagraph");
EXPECT_EQ(selection.start_dom_node_id,
paragraph1.children_nodes[0]->content_attributes->dom_node_id);
EXPECT_EQ(selection.end_dom_node_id,
paragraph2.children_nodes[0]->content_attributes->dom_node_id);
EXPECT_EQ(selection.start_offset, 10);
EXPECT_EQ(selection.end_offset, 9);
}
TEST_F(AIPageContentAgentTest, SelectionInIframe) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <iframe srcdoc='"
" <p id=\"p1\">Paragraph 1</p>"
" <p id=\"p2\">Paragraph 2</p>"
" <p id=\"p3\">Paragraph 3</p>"
" <script>"
" const p1 = document.getElementById(\"p1\");"
" const p2 = document.getElementById(\"p2\");"
" const range = new Range();"
" range.setStart(p1.childNodes[0], 10);"
" range.setEnd(p2.childNodes[0], 9);"
" const selection = window.getSelection();"
" selection.removeAllRanges();"
" selection.addRange(range);"
" </script>"
" '></iframe>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& iframe = *root.children_nodes[0];
CheckIframeNode(iframe);
EXPECT_EQ(iframe.children_nodes.size(), 1u);
const auto& iframe_root = *iframe.children_nodes[0];
CheckRootNode(iframe_root);
EXPECT_EQ(iframe_root.children_nodes.size(), 3u);
const auto& paragraph1 = *iframe_root.children_nodes[0];
CheckTextNode(*paragraph1.children_nodes[0], "Paragraph 1");
const auto& paragraph2 = *iframe_root.children_nodes[1];
CheckTextNode(*paragraph2.children_nodes[0], "Paragraph 2");
const auto& paragraph3 = *iframe_root.children_nodes[2];
CheckTextNode(*paragraph3.children_nodes[0], "Paragraph 3");
const auto& frame_interaction_info =
Content()->frame_data->frame_interaction_info;
ASSERT_FALSE(frame_interaction_info->selection);
EXPECT_TRUE(iframe.content_attributes->iframe_data->content);
const auto& iframe_interaction_info =
iframe.content_attributes->iframe_data->content->get_local_frame_data()
->frame_interaction_info;
ASSERT_TRUE(iframe_interaction_info->selection);
const auto& selection = *iframe_interaction_info->selection;
EXPECT_EQ(selection.selected_text, "1\n\nParagraph");
EXPECT_EQ(selection.start_dom_node_id,
paragraph1.children_nodes[0]->content_attributes->dom_node_id);
EXPECT_EQ(selection.end_dom_node_id,
paragraph2.children_nodes[0]->content_attributes->dom_node_id);
EXPECT_EQ(selection.start_offset, 10);
EXPECT_EQ(selection.end_offset, 9);
}
TEST_F(AIPageContentAgentTest, Focus) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <button id='button'>button</button>"
" <script>"
" const button = document.getElementById('button');"
" button.focus();"
" </script>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& button = *root.children_nodes[0];
const auto& page_interaction_info = Content()->page_interaction_info;
EXPECT_EQ(page_interaction_info->focused_dom_node_id,
button.content_attributes->dom_node_id);
}
TEST_F(AIPageContentAgentTest, AccessibilityFocus) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" #button1 {"
" position: absolute;"
" top: -10px;"
" left: -20px;"
" width: 30px;"
" height: 40px;"
" }"
" </style>"
" <button id='button1'>button1</button>"
" <div id='div2'>div2</div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
// Enable accessibility.
ui::AXMode ax_mode = ui::kAXModeComplete;
Document* document = helper_.LocalMainFrame()->GetFrame()->GetDocument();
auto context = std::make_unique<AXContext>(*document, ax_mode);
EXPECT_TRUE(document->ExistingAXObjectCache());
auto* ax_object_cache =
To<AXObjectCacheImpl>(document->ExistingAXObjectCache());
EXPECT_EQ(ax_mode, ax_object_cache->GetAXMode());
ax_object_cache->UpdateAXForAllDocuments();
// Set accessibility focus to the button.
auto* button_element = document->getElementById(AtomicString("button1"));
auto* button_ax_object = ax_object_cache->Get(button_element);
ui::AXActionData action_data;
action_data.action = ax::mojom::blink::Action::kSetAccessibilityFocus;
button_ax_object->PerformAction(action_data);
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 2u);
const auto& button = *root.children_nodes[0];
const auto& div2 = *root.children_nodes[1];
const auto& page_interaction_info = Content()->page_interaction_info;
EXPECT_EQ(page_interaction_info->accessibility_focused_dom_node_id,
button.content_attributes->dom_node_id);
CheckGeometry(button, gfx::Rect(-20, -10, 30, 40), gfx::Rect(0, 0, 10, 30));
EXPECT_FALSE(div2.content_attributes->geometry);
}
TEST_F(AIPageContentAgentTest, MousePosition) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" div {"
" position: absolute;"
" top: 100px;"
" left: 200px;"
" }"
" </style>"
" <div>text</div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
// Move the mouse to the middle of the page.
FireMouseMoveEvent(gfx::PointF(150, 50));
GetAIPageContent();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& text = *root.children_nodes[0];
CheckTextNode(text, "text");
EXPECT_EQ(Content()->page_interaction_info->mouse_position->x(), 150);
EXPECT_EQ(Content()->page_interaction_info->mouse_position->y(), 50);
}
TEST_F(AIPageContentAgentTest, MetaTags) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<head>"
" <meta charset='UTF-8'>"
" <meta content='ignored'>"
" <meta name='author' content='George'>"
" <meta name='keywords' content='HTML, CSS, JavaScript'>"
" <meta name='nocontent'>"
" <meta name='emptycontent' content=''>"
" <meta id='nullcontent' name='nullcontent'>"
"</head>"
"<body>"
" <meta name='ignored'>"
" <iframe srcdoc=\""
" <head>"
" <meta charset='UTF-8'>"
" <meta name='author' content='Gary'>"
" <meta name='keywords' content='HTML, CSS, JavaScript'>"
" </head>"
" <body>child frame</body>"
" \""
" </iframe>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
// Explicitly set the content of the nullcontent meta tag to the null atom to
// test this case.
auto& document = *helper_.LocalMainFrame()->GetFrame()->GetDocument();
document.getElementById(AtomicString("nullcontent"))
->setAttribute(html_names::kContentAttr, g_null_atom);
mojom::blink::AIPageContentOptions options;
options.max_meta_elements = 32;
GetAIPageContent(options);
EXPECT_EQ(Content()->frame_data->meta_data.size(), 5u);
EXPECT_EQ(Content()->frame_data->meta_data[0]->name, "author");
EXPECT_EQ(Content()->frame_data->meta_data[0]->content, "George");
EXPECT_EQ(Content()->frame_data->meta_data[1]->name, "keywords");
EXPECT_EQ(Content()->frame_data->meta_data[1]->content,
"HTML, CSS, JavaScript");
EXPECT_EQ(Content()->frame_data->meta_data[2]->name, "nocontent");
EXPECT_EQ(Content()->frame_data->meta_data[3]->content, "");
EXPECT_EQ(Content()->frame_data->meta_data[3]->name, "emptycontent");
EXPECT_EQ(Content()->frame_data->meta_data[3]->content, "");
EXPECT_EQ(Content()->frame_data->meta_data[4]->name, "nullcontent");
EXPECT_EQ(Content()->frame_data->meta_data[4]->content, "");
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& iframe = *root.children_nodes[0];
EXPECT_EQ(iframe.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kIframe);
const auto& iframe_data = *iframe.content_attributes->iframe_data;
EXPECT_TRUE(iframe.content_attributes->iframe_data->content);
EXPECT_EQ(iframe_data.content->get_local_frame_data()->meta_data.size(), 2u);
EXPECT_EQ(iframe_data.content->get_local_frame_data()->meta_data[0]->name,
"author");
EXPECT_EQ(iframe_data.content->get_local_frame_data()->meta_data[0]->content,
"Gary");
EXPECT_EQ(iframe_data.content->get_local_frame_data()->meta_data[1]->name,
"keywords");
EXPECT_EQ(iframe_data.content->get_local_frame_data()->meta_data[1]->content,
"HTML, CSS, JavaScript");
}
TEST_F(AIPageContentAgentTest, NestedIframesMetaTags) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<head><meta name=author content=George></head>"
"<body>parent"
" <iframe srcdoc=\""
" <head><meta name=author content=Gary></head>"
" <body>child"
" <iframe srcdoc='"
" <head><meta name=author content=Jordan></head>"
" <body>grandchild</body"
" '></iframe>"
" </body>"
" \"></iframe>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
mojom::blink::AIPageContentOptions options;
options.max_meta_elements = 32;
GetAIPageContent(options);
EXPECT_EQ(Content()->frame_data->meta_data.size(), 1u);
EXPECT_EQ(Content()->frame_data->meta_data[0]->name, "author");
EXPECT_EQ(Content()->frame_data->meta_data[0]->content, "George");
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 2u);
const auto& iframe = *root.children_nodes[1];
EXPECT_EQ(iframe.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kIframe);
const auto& iframe_data = *iframe.content_attributes->iframe_data;
EXPECT_TRUE(iframe_data.content);
EXPECT_EQ(iframe_data.content->get_local_frame_data()->meta_data.size(), 1u);
EXPECT_EQ(iframe_data.content->get_local_frame_data()->meta_data[0]->name,
"author");
EXPECT_EQ(iframe_data.content->get_local_frame_data()->meta_data[0]->content,
"Gary");
EXPECT_EQ(iframe.children_nodes.size(), 1u);
// In the iframe children_nodes there is a root node that has two children.
// The first child is a text node and the second is the subiframe. The key
// thing we want to check here is that the subiframe has the correct meta
// data.
const auto& subiframe = *iframe.children_nodes[0]->children_nodes[1];
EXPECT_EQ(subiframe.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kIframe);
const auto& subiframe_data = *subiframe.content_attributes->iframe_data;
EXPECT_TRUE(subiframe_data.content);
EXPECT_EQ(subiframe_data.content->get_local_frame_data()->meta_data.size(),
1u);
EXPECT_EQ(subiframe_data.content->get_local_frame_data()->meta_data[0]->name,
"author");
EXPECT_EQ(
subiframe_data.content->get_local_frame_data()->meta_data[0]->content,
"Jordan");
}
TEST_F(AIPageContentAgentTest, Title) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<head>"
" <title>test title</title>"
"</head>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
EXPECT_EQ(Content()->frame_data->title, "test title");
}
bool ContainsRole(const Vector<mojom::blink::AIPageContentAnnotatedRole>& roles,
mojom::blink::AIPageContentAnnotatedRole role) {
for (const auto& r : roles) {
if (r == role) {
return true;
}
}
return false;
}
TEST_F(AIPageContentAgentTest, PaidContent) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"HTML(
<head>
<script></script>
<script type='unrelated'></script>
<script type="application/ld+json">{this: "will fail parsing",}</script>
<script type="application/ld+json">"not": "an object"</script>
<script type="application/ld+json">{
"@context": "http://schema.org",
"@type": "NewsArticle",
"mainEntityOfPage": "https://www.evergreengazette.com/world/world-news/",
"headline": "City Council Debates Future of Automated Transit System",
"alternativeHeadline": "City Council Debates Future of Automated Transit System",
"dateModified": "2025-03-25T19:17:05.541Z",
"datePublished": "2025-03-25T09:02:58.131Z",
"description": "The City Council has been asked to discuss the future of automated transit systems, including the feasibility of a bus-on-rails system, in a special meeting on Thursday, March 28.",
"author": [
{
"@type": "Person",
"name": "Finlay Joy",
"url": "https://www.evergreengazette.com/people/finlay-joy/"
},
{
"@type": "Person",
"name": "Calum Gerhard",
"url": "https://www.evergreengazette.com/people/calum-gerhard/"
}
],
"isPartOf": {
"@type": [
"CreativeWork",
"Product"
],
"name": "The Evergreen Gazette",
"productID": "evergreengazette.com:basic",
"description": "The Evergreen Gazette is your trusted source for comprehensive local news, insightful analysis, and community-focused reporting.",
"sku": "https://subscribe.evergreengazette.com",
"image": "https://www.evergreengazette.com/evergreen-gazette-logo.png",
"brand": {
"@type": "brand",
"name": "The Evergreen Gazette"
},
"offers": {
"@type": "offer",
"url": "https://subscribe.evergreengazette.com/acquisition?promo=h97"
}
},
"publisher": {
"@id": "evergreengazette.com",
"@type": "NewsMediaOrganization",
"name": "The Evergreen Gazette"
},
"isAccessibleForFree": false,
"hasPart": {
"@type": "WebPageElement",
"cssSelector": ".paidContent",
"isAccessibleForFree": false
}
}</script>
<body>
Content
<div class="paidContent">Paid Content</div>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
// The root node contains paid content.
EXPECT_TRUE(Content()->frame_data->contains_paid_content);
const auto& root = ContentRootNode();
// The text node should not have the paid content role.
const auto& text_node = *root.children_nodes[0];
EXPECT_FALSE(
ContainsRole(text_node.content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
// The paid content node should have the paid content role.
const auto& paid_node = *root.children_nodes[1];
EXPECT_TRUE(
ContainsRole(paid_node.content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
}
TEST_F(AIPageContentAgentTest, PaidContentContextMismatch) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"HTML(
<script type="application/ld+json">{
"@context": "http://acme.org",
"@type": "NewsArticle",
"isAccessibleForFree": false,
"hasPart": {
"@type": "WebPageElement",
"cssSelector": ".paidContent",
"isAccessibleForFree": false
}
}</script>
<body>
Content
<div class="paidContent">Paid Content</div>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
// The root node does not contain paid content.
EXPECT_FALSE(Content()->frame_data->contains_paid_content);
const auto& root = ContentRootNode();
// The text node should not have the paid content role.
const auto& text_node = *root.children_nodes[0];
EXPECT_FALSE(
ContainsRole(text_node.content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
// The paid content node should have the paid content role.
const auto& paid_node = *root.children_nodes[1];
EXPECT_FALSE(
ContainsRole(paid_node.content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
}
TEST_F(AIPageContentAgentTest, PaidContentRootOnly) {
// Note that isAccessibleForFree = "False" to match real world examples.
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"HTML(
<script type="application/ld+json">{
"@context": "http://schema.org",
"@type": "NewsArticle",
"isAccessibleForFree": "False",
"hasPart": {
"@type": "unrelated"
}
}</script>
<body>
Content
<div class="paidContent">Paid Content</div>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
// The root node contains paid content.
EXPECT_TRUE(Content()->frame_data->contains_paid_content);
const auto& root = ContentRootNode();
// The text node should not have the paid content role.
const auto& text_node = *root.children_nodes[0];
EXPECT_FALSE(
ContainsRole(text_node.content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
// The paid content node should have the paid content role.
const auto& paid_node = *root.children_nodes[1];
EXPECT_FALSE(
ContainsRole(paid_node.content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
}
TEST_F(AIPageContentAgentTest, PaidContentMicrodata) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"HTML(
<script type="application/ld+json">{
"@context": "http://schema.org",
"@type": "NewsArticle",
"isAccessibleForFree": false
}</script>
<body>
Content
<div class="paidContent">
<meta itemprop="isAccessibleForFree" content="false">
Paid Content
</div>
<div class="paidContent">
<meta itemprop="unrelated">
Content
</div>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
// The root node contains paid content.
EXPECT_TRUE(Content()->frame_data->contains_paid_content);
const auto& root = ContentRootNode();
// The text node should not have the paid content role.
const auto& text_node = *root.children_nodes[0];
EXPECT_FALSE(
ContainsRole(text_node.content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
// The paid content node should have the paid content role.
const auto& paid_node = *root.children_nodes[1];
EXPECT_TRUE(
ContainsRole(paid_node.content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
}
TEST_F(AIPageContentAgentTest, PaidContentSomeYesSomeNo) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"HTML(
<script type="application/ld+json">{
"@context": "http://schema.org",
"@type": "NewsArticle",
"isAccessibleForFree": false,
"hasPart": {
"@type": "WebPageElement",
"cssSelector": ".paidContent",
"isAccessibleForFree": false
}
}</script>
<body>
Content
<div class="paidContent">Paid Content</div>
<div>Free Content</div>
<div class="paidContent">Paid Content</div>
<div>Free Content</div>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
// The root node contains paid content.
EXPECT_TRUE(Content()->frame_data->contains_paid_content);
const auto& root = ContentRootNode();
auto& nodes = root.children_nodes;
// Every other node should have the paid content role.
EXPECT_FALSE(
ContainsRole(nodes[0]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
EXPECT_TRUE(
ContainsRole(nodes[1]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
EXPECT_FALSE(
ContainsRole(nodes[2]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
EXPECT_TRUE(
ContainsRole(nodes[3]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
EXPECT_FALSE(
ContainsRole(nodes[4]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
}
TEST_F(AIPageContentAgentTest, PaidContentMultipleHasParts) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"HTML(
<script type="application/ld+json">{
"@context": "https://schema.org",
"@type": "NewsArticle",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://example.org/article"
},
"isAccessibleForFree": false,
"hasPart": [
{
"@type": "WebPageElement",
"isAccessibleForFree": false,
"cssSelector": ".section1"
}, {
"@type": "WebPageElement",
"isAccessibleForFree": false,
"cssSelector": ".section2"
}
]
}</script>
<body>
Content
<div class="section1">Paid Content</div>
<div class="section2">Paid Content</div>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
// The root node contains paid content.
EXPECT_TRUE(Content()->frame_data->contains_paid_content);
const auto& root = ContentRootNode();
auto& nodes = root.children_nodes;
EXPECT_FALSE(
ContainsRole(nodes[0]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
EXPECT_TRUE(
ContainsRole(nodes[1]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
EXPECT_TRUE(
ContainsRole(nodes[2]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
}
TEST_F(AIPageContentAgentTest, PaidContentSubframe) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"HTML(
<body>
Parent doc is free!
<iframe srcdoc='
<script type="application/ld+json">{
"@context": "http://schema.org",
"@type": "NewsArticle",
"isAccessibleForFree": false,
"hasPart": {
"@type": "WebPageElement",
"cssSelector": ".paidContent",
"isAccessibleForFree": false
}
}</script>
<body>
Content
<div class="paidContent">Paid Content</div>
</body>
'></iframe>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
// The root node does not contain paid content.
EXPECT_FALSE(Content()->frame_data->contains_paid_content);
const auto& root = ContentRootNode();
auto& nodes = root.children_nodes;
EXPECT_FALSE(
ContainsRole(nodes[0]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
const auto& iframe = nodes[1];
EXPECT_EQ(iframe->content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kIframe);
EXPECT_TRUE(iframe->content_attributes->iframe_data->content);
EXPECT_TRUE(
iframe->content_attributes->iframe_data->content->get_local_frame_data()
->contains_paid_content);
auto& children = iframe->children_nodes[0]->children_nodes;
EXPECT_FALSE(
ContainsRole(children[0]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
EXPECT_TRUE(
ContainsRole(children[1]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
}
TEST_F(AIPageContentAgentTest, PaidContentSubframeMicrodata) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"HTML(
<script type="application/ld+json">{
"@context": "https://schema.org",
"@type": "NewsArticle",
"isAccessibleForFree": true
}</script>
<body>
Free Content
<div class="paidContent">
<meta itemprop="isAccessibleForFree" content="false">
Microdata not checked
</div>
<iframe srcdoc='
<script type="application/ld+json">{
"@context": "http://schema.org",
"@type": "NewsArticle",
"isAccessibleForFree": false
}</script>
<body>
Content
<div class="paidContent">
<meta itemprop="isAccessibleForFree" content="false">
Paid Content
</div>
</body>
'></iframe>
<iframe srcdoc='
<body>
Content
<div class="paidContent">
<meta itemprop="isAccessibleForFree" content="false">
Microdata not checked
</div>
</body>
'></iframe>
<iframe srcdoc='
<script type="application/ld+json">{
"@context": "http://schema.org",
"@type": "NewsArticle",
"isAccessibleForFree": false
}</script>
<body>
Content
<div class="paidContent">
<meta itemprop="isAccessibleForFree" content="false">
Paid Content
</div>
</body>
'></iframe>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
// The root node does not contain paid content.
EXPECT_FALSE(Content()->frame_data->contains_paid_content);
const auto& root = ContentRootNode();
auto& nodes = root.children_nodes;
EXPECT_FALSE(
ContainsRole(nodes[0]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
EXPECT_FALSE(
ContainsRole(nodes[1]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
const auto& iframe1 = nodes[2];
EXPECT_EQ(iframe1->content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kIframe);
EXPECT_TRUE(iframe1->content_attributes->iframe_data->content);
EXPECT_TRUE(
iframe1->content_attributes->iframe_data->content->get_local_frame_data()
->contains_paid_content);
const auto& children1 = iframe1->children_nodes[0]->children_nodes;
EXPECT_FALSE(
ContainsRole(children1[0]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
EXPECT_TRUE(
ContainsRole(children1[1]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
const auto& iframe2 = nodes[3];
EXPECT_EQ(iframe2->content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kIframe);
EXPECT_TRUE(iframe2->content_attributes->iframe_data->content);
EXPECT_FALSE(
iframe2->content_attributes->iframe_data->content->get_local_frame_data()
->contains_paid_content);
const auto& children2 = iframe2->children_nodes[0]->children_nodes;
EXPECT_FALSE(
ContainsRole(children2[0]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
EXPECT_FALSE(
ContainsRole(children2[1]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
const auto& iframe3 = nodes[4];
EXPECT_EQ(iframe3->content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kIframe);
EXPECT_TRUE(iframe3->content_attributes->iframe_data->content);
EXPECT_TRUE(
iframe3->content_attributes->iframe_data->content->get_local_frame_data()
->contains_paid_content);
const auto& children3 = iframe3->children_nodes[0]->children_nodes;
EXPECT_FALSE(
ContainsRole(children3[0]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
EXPECT_TRUE(
ContainsRole(children3[1]->content_attributes->annotated_roles,
mojom::blink::AIPageContentAnnotatedRole::kPaidContent));
}
TEST_F(AIPageContentAgentTest, AnchorInInlineWithFloatingSibling) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<!DOCTYPE html>"
"<body style='margin:0; font: 1px/1px Ahem'>"
" <span>"
" <a href='https://www.google.com'>"
" <div style='position: relative; float: left;'>text in div</div>"
" <span>text</span>"
" </a>"
" </span>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
LoadAhem();
GetAIPageContentWithActionableElements();
const mojom::blink::AIPageContentNode* anchor = FindNodeBySelector("a");
ASSERT_TRUE(anchor->content_attributes->geometry);
CheckAnchorNode(*anchor, blink::KURL("https://www.google.com/"), {});
ASSERT_TRUE(anchor->content_attributes->node_interaction_info);
EXPECT_TRUE(anchor->content_attributes->node_interaction_info
->document_scoped_z_order);
CheckGeometry(*anchor, gfx::Rect(11, 0, 4, 1), gfx::Rect(11, 0, 4, 1));
}
TEST_F(AIPageContentAgentTest, OverflowHiddenGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <div style='width: 100px; height: 100px; overflow-y: hidden;'>"
" <article style='width: 50px; height: 300px;'></article>"
" </div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& outer = ContentRootNode().children_nodes[0];
const auto& article = outer->children_nodes[0];
CheckAnnotatedRole(*article,
mojom::blink::AIPageContentAnnotatedRole::kArticle);
EXPECT_GT(*article->content_attributes->node_interaction_info
->document_scoped_z_order,
*outer->content_attributes->node_interaction_info
->document_scoped_z_order);
CheckGeometry(*outer, gfx::Rect(8, 8, 100, 100), gfx::Rect(8, 8, 100, 100));
CheckGeometry(*article, gfx::Rect(8, 8, 50, 300), gfx::Rect(8, 8, 50, 100));
}
TEST_F(AIPageContentAgentTest, OverflowVisibleGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <section style='width: 100px; height: 100px; overflow-y: visible;'>"
" <article style='width: 50px; height: 300px;'></article>"
" </section>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& outer = ContentRootNode().children_nodes[0];
const auto& article = outer->children_nodes[0];
CheckAnnotatedRole(*article,
mojom::blink::AIPageContentAnnotatedRole::kArticle);
EXPECT_GT(*article->content_attributes->node_interaction_info
->document_scoped_z_order,
*outer->content_attributes->node_interaction_info
->document_scoped_z_order);
CheckGeometry(*outer, gfx::Rect(8, 8, 100, 100), gfx::Rect(8, 8, 100, 100));
CheckGeometry(*article, gfx::Rect(8, 8, 50, 300), gfx::Rect(8, 8, 50, 300));
}
TEST_F(AIPageContentAgentTest, BlurGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <section style='width: 100px; height: 100px; filter: "
"blur(10px);'></section>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& section = ContentRootNode().children_nodes[0];
CheckAnnotatedRole(*section,
mojom::blink::AIPageContentAnnotatedRole::kSection);
// Although blurring causes the rectangle to show outside of the 100x100 rect,
// the extra area is not hit testable, and is therefore not included in the
// visible bounding box.
CheckGeometry(*section, gfx::Rect(8, 8, 100, 100), gfx::Rect(8, 8, 100, 100));
}
TEST_F(AIPageContentAgentTest, GeomtryAbsPos) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <section style='width: 100px; height: 100px; position: absolute; top: "
"200px; left: 200px;'>"
"</section>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& section = ContentRootNode().children_nodes[0];
CheckAnnotatedRole(*section,
mojom::blink::AIPageContentAnnotatedRole::kSection);
CheckGeometry(*section, gfx::Rect(200, 200, 100, 100),
gfx::Rect(200, 200, 100, 100));
}
TEST_F(AIPageContentAgentTest, GeometryTransform) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <section style='width: 100px; height: 100px; transform: "
"translate(200px, 200px)'>"
"</section>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& section = ContentRootNode().children_nodes[0];
CheckAnnotatedRole(*section,
mojom::blink::AIPageContentAnnotatedRole::kSection);
CheckGeometry(*section, gfx::Rect(208, 208, 100, 100),
gfx::Rect(208, 208, 100, 100));
}
TEST_F(AIPageContentAgentTest, CursorForClickability) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <div style='cursor: pointer'>"
" <p>no-click</p>"
" <p style='cursor: pointer'>click</p>"
" </div>"
" <article>article</article>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
EXPECT_EQ(ContentRootNode().children_nodes.size(), 2u);
const auto& cursor = *ContentRootNode().children_nodes[0];
EXPECT_TRUE(cursor.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(cursor, {ClickabilityReason::kCursorPointer});
const auto& no_click = *cursor.children_nodes[0];
EXPECT_TRUE(no_click.content_attributes->node_interaction_info);
CheckHitTestableButNotInteractive(no_click);
const auto& click = *cursor.children_nodes[1];
EXPECT_TRUE(click.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(click, {ClickabilityReason::kCursorPointer});
const auto& article = *ContentRootNode().children_nodes[1];
EXPECT_TRUE(article.content_attributes->node_interaction_info);
CheckHitTestableButNotInteractive(article);
}
TEST_F(AIPageContentAgentTest, LinkForClickability) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <a href='test.com'>valid</a>"
" <a>invalid</a>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
EXPECT_EQ(ContentRootNode().children_nodes.size(), 2u);
const auto& valid = *ContentRootNode().children_nodes[0];
EXPECT_TRUE(valid.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(valid, {ClickabilityReason::kCursorPointer});
const auto& invalid = *ContentRootNode().children_nodes[1];
EXPECT_TRUE(invalid.content_attributes->node_interaction_info);
CheckHitTestableButNotInteractive(invalid);
}
TEST_F(AIPageContentAgentTest, LabelWithForSibling) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <input type='checkbox' id='myCheckbox' />"
" <label for='myCheckbox'>Check me!</label>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 2u);
const auto& input = *root.children_nodes[0];
CheckFormControlNode(input, mojom::blink::FormControlType::kInputCheckbox);
ASSERT_TRUE(input.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(input,
{ClickabilityReason::kClickableControl});
EXPECT_EQ(input.content_attributes->form_control_data->redaction_decision,
mojom::AIPageContentRedactionDecision::kNoRedactionNecessary);
const auto& label = *root.children_nodes[1];
CheckContainerNode(label);
ASSERT_TRUE(label.content_attributes->node_interaction_info);
EXPECT_TRUE(label.content_attributes->node_interaction_info
->clickability_reasons.empty());
EXPECT_EQ(label.content_attributes->label_for_dom_node_id,
input.content_attributes->dom_node_id);
}
TEST_F(AIPageContentAgentTest, LabelGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<style>body * { margin:0; padding:0; font:1px/1px 'Ahem'; }</style>"
"<body style='margin: 3px;'>"
" <label for='myCheckbox'>1234567890</label>"
" <input type='checkbox' id='myCheckbox' "
" style='width:1px;height:1px;appearance:none;'/>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
LoadAhem();
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 2u);
const auto& label = *root.children_nodes[0];
CheckContainerNode(label);
ASSERT_TRUE(label.content_attributes->node_interaction_info);
EXPECT_TRUE(label.content_attributes->node_interaction_info
->clickability_reasons.empty());
const auto& input = *root.children_nodes[1];
CheckFormControlNode(input, mojom::blink::FormControlType::kInputCheckbox);
ASSERT_TRUE(input.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(input,
{ClickabilityReason::kClickableControl});
EXPECT_EQ(input.content_attributes->form_control_data->redaction_decision,
mojom::AIPageContentRedactionDecision::kNoRedactionNecessary);
EXPECT_EQ(label.content_attributes->label_for_dom_node_id,
input.content_attributes->dom_node_id);
ASSERT_TRUE(label.content_attributes->geometry);
const auto& label_geometry = *label.content_attributes->geometry;
// With the 1px Ahem font loaded above, each glyph occupies a 1x1 cell so the
// 10-character label spans exactly 10x1 CSS pixels.
EXPECT_EQ(label_geometry.outer_bounding_box, gfx::Rect(3, 3, 10, 1));
EXPECT_EQ(label_geometry.visible_bounding_box,
label_geometry.outer_bounding_box);
}
TEST_F(AIPageContentAgentTest, LabelWithForDescendant) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <label>"
" <input type='checkbox' id='myCheckbox' />"
"Check me!"
"</label>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& label = *root.children_nodes[0];
EXPECT_EQ(label.children_nodes.size(), 2u);
CheckContainerNode(label);
ASSERT_TRUE(label.content_attributes->node_interaction_info);
CheckHitTestableButNotInteractive(label);
const auto& input = *label.children_nodes[0];
CheckFormControlNode(input, mojom::blink::FormControlType::kInputCheckbox);
ASSERT_TRUE(input.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(input,
{ClickabilityReason::kClickableControl});
EXPECT_EQ(label.content_attributes->label_for_dom_node_id,
input.content_attributes->dom_node_id);
CheckTextNode(*label.children_nodes[1], "Check me!");
}
TEST_F(AIPageContentAgentTest, SVG) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <svg width='400' height='200'>"
" <text x='50%' y='50/%' font-size='24'>"
" Hello SVG Text!"
" </text>"
" </svg>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& svg = *ContentRootNode().children_nodes[0];
EXPECT_EQ(svg.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kSVG);
ASSERT_TRUE(svg.content_attributes->svg_data);
EXPECT_EQ(svg.content_attributes->svg_data->inner_text, "Hello SVG Text!");
}
TEST_F(AIPageContentAgentTest, SVGWithNoText) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <svg width='400' height='200' style='content-visibility: hidden'>"
" <text x='50%' y='50/%' font-size='24'>"
" Hello SVG Text!"
" </text>"
" </svg>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& svg = *ContentRootNode().children_nodes[0];
EXPECT_EQ(svg.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kSVG);
ASSERT_TRUE(svg.content_attributes->svg_data);
EXPECT_FALSE(svg.content_attributes->svg_data->inner_text);
}
TEST_F(AIPageContentAgentTest, Canvas) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" canvas {"
" width: 200px;"
" height: 300px;"
" }"
" </style>"
" <canvas id='myCanvas' width='100' height='200'></canvas>"
" <script>"
" const canvas = document.getElementById('myCanvas');"
" const ctx = canvas.getContext('2d');"
" ctx.fillStyle = 'pink';"
" ctx.fillRect(0, 0, 100, 200);"
" </script>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContent();
const auto& canvas = *ContentRootNode().children_nodes[0];
EXPECT_EQ(canvas.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kCanvas);
ASSERT_TRUE(canvas.content_attributes->canvas_data);
EXPECT_EQ(canvas.content_attributes->canvas_data->layout_size,
gfx::Size(200, 300));
}
TEST_F(AIPageContentAgentTest, AriaLabelledBy) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <div id='hiddenLabel1' style='display: none;'>and first</div>"
" <div id='hiddenLabel2' style='display: none;'>and second</div>"
" <input type='text' aria-labelledby='hiddenLabel1 hiddenLabel2' "
"aria-label='on element'/>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
const auto& input = *root.children_nodes[0];
CheckFormControlNode(input, mojom::blink::FormControlType::kInputText);
EXPECT_EQ(input.content_attributes->label, "on element and first and second");
}
TEST_F(AIPageContentAgentTest, DisabledButton) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<button disabled>Text</button>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& button = *root.children_nodes.at(0);
CheckHitTestableButNotInteractive(button);
EXPECT_TRUE(button.content_attributes->node_interaction_info->is_disabled);
}
TEST_F(AIPageContentAgentTest, InertButton) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<button inert>Text</button>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& button = *root.children_nodes.at(0);
EXPECT_FALSE(button.content_attributes->node_interaction_info);
}
TEST_F(AIPageContentAgentTest, ActionablePseudoElements) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style> a::before { content: 'hello'; cursor: pointer;} </style>"
" <a href='#'></a>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
EXPECT_EQ(ContentRootNode().children_nodes.size(), 1u);
const auto& a = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(a.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(a, {ClickabilityReason::kCursorPointer});
EXPECT_EQ(a.children_nodes.size(), 1u);
const auto& before = *a.children_nodes[0];
ASSERT_TRUE(before.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(before, {ClickabilityReason::kCursorPointer});
}
TEST_F(AIPageContentAgentTest, PseudoElementNotActionable) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style> a::before { content: 'hello';} </style>"
" <a href='#'></a>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
EXPECT_EQ(ContentRootNode().children_nodes.size(), 1u);
const auto& a = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(a.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(a, {ClickabilityReason::kCursorPointer});
EXPECT_EQ(a.children_nodes.size(), 1u);
const auto& before = *a.children_nodes[0];
ASSERT_TRUE(before.content_attributes->node_interaction_info);
CheckHitTestableButNotInteractive(before);
}
TEST_F(AIPageContentAgentTest, PseudoElementNoPointerEvents) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style> a::before { content: 'hello'; pointer-events: none;} </style>"
" <a href='#'></a>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
EXPECT_EQ(Content()->root_node->children_nodes.size(), 1u);
const auto& a = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(a.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(a, {ClickabilityReason::kCursorPointer});
EXPECT_EQ(a.children_nodes.size(), 1u);
const auto& text = *a.children_nodes[0];
CheckTextNode(text, "hello");
EXPECT_FALSE(text.content_attributes->node_interaction_info);
}
TEST_F(AIPageContentAgentTest, AriaDisabled) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<section style='cursor: pointer' aria-disabled=true>
<input type=text aria-disabled=false></input>
</section>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
// The first node is not actionable anymore.
const auto& section = *root.children_nodes.at(0);
CheckContainerNode(section);
CheckHitTestableButNotInteractive(section);
EXPECT_TRUE(section.content_attributes->node_interaction_info->is_disabled);
// The child is also not actionable.
ASSERT_EQ(section.children_nodes.size(), 1u);
const auto& input = *section.children_nodes.at(0);
CheckHitTestableButNotInteractive(input);
// Parent element `aria-disable` value overrides child element's.
EXPECT_TRUE(input.content_attributes->node_interaction_info->is_disabled);
}
TEST_F(AIPageContentAgentTest, DisabledInheritance) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<form>
<fieldset disabled>
<button type="submit"></button>
</fieldset>
</form>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& form = *root.children_nodes.at(0);
CheckHitTestableButNotInteractive(form);
ASSERT_EQ(form.children_nodes.size(), 1u);
const auto& fieldset = *form.children_nodes.at(0);
CheckHitTestableButNotInteractive(fieldset);
ASSERT_EQ(fieldset.children_nodes.size(), 1u);
EXPECT_TRUE(fieldset.content_attributes->node_interaction_info->is_disabled);
const auto& button = *fieldset.children_nodes.at(0);
CheckHitTestableButNotInteractive(button);
EXPECT_TRUE(button.content_attributes->node_interaction_info->is_disabled);
}
TEST_F(AIPageContentAgentTest, Fieldset) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<form>
<fieldset>
<button type="submit"></button>
</fieldset>
</form>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& form = *root.children_nodes.at(0);
CheckHitTestableButNotInteractive(form);
ASSERT_EQ(form.children_nodes.size(), 1u);
const auto& fieldset = *form.children_nodes.at(0);
CheckHitTestableButNotInteractive(fieldset);
}
TEST_F(AIPageContentAgentTest, ShadowDOMInInput) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<input type=range></input>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& input = *root.children_nodes.at(0);
ASSERT_TRUE(input.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(input,
{ClickabilityReason::kClickableControl});
EXPECT_NE(input.children_nodes.size(), 0u);
const auto& shadow_div = *input.children_nodes.at(0);
CheckHitTestableButNotInteractive(shadow_div);
}
TEST_F(AIPageContentAgentTest, DisabledOption) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<select>
<option value="banana">Banana</option>
<option value="cherry" disabled>Cherry</option>
</select>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& select = *root.children_nodes.at(0);
CheckFormControlNode(select, mojom::blink::FormControlType::kSelectOne);
const auto& options =
select.content_attributes->form_control_data->select_options;
ASSERT_EQ(options.size(), 2u);
const auto& banana = *options.at(0);
EXPECT_EQ(banana.value, "banana");
EXPECT_FALSE(banana.disabled);
const auto& cherry = *options.at(1);
EXPECT_EQ(cherry.value, "cherry");
EXPECT_TRUE(cherry.disabled);
}
TEST_F(AIPageContentAgentTest, AriaRole) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<div role="button">hello</div>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& button = *root.children_nodes.at(0);
ASSERT_TRUE(button.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(button, {ClickabilityReason::kAriaRole});
EXPECT_EQ(button.content_attributes->aria_role,
ax::mojom::blink::Role::kButton);
}
TEST_F(AIPageContentAgentTest, LabelNotActionable) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<input type='checkbox' id='myCheckbox' />
<label for='myCheckbox' style='pointer-events: none;'>Check me!</label>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 2u);
const auto& button = *root.children_nodes.at(0);
ASSERT_TRUE(button.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(button,
{ClickabilityReason::kClickableControl});
const auto& label = *root.children_nodes.at(1);
EXPECT_FALSE(label.content_attributes->node_interaction_info);
EXPECT_EQ(*label.content_attributes->label_for_dom_node_id,
button.content_attributes->dom_node_id);
}
TEST_F(AIPageContentAgentTest, SelectLabelNotActionable) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<label for="fruit-select">Choose a fruit:</label>
<select id="fruit-select" name="fruits">
<option value="">--Please choose an option--</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
</select>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 2u);
const auto& label = *root.children_nodes.at(0);
ASSERT_TRUE(label.content_attributes->node_interaction_info);
CheckHitTestableButNotInteractive(label);
const auto& select = *root.children_nodes.at(1);
ASSERT_TRUE(select.content_attributes->node_interaction_info);
CheckHitTestableAndInteractive(select,
{ClickabilityReason::kClickableControl,
ClickabilityReason::kHoverPseudoClass});
}
TEST_F(AIPageContentAgentTest, ClickabilityReasonClickableControl) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body><button id='testButton'>Click Me</button></body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& button_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(button_node.content_attributes->node_interaction_info);
EXPECT_THAT(
button_node.content_attributes->node_interaction_info
->clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kClickableControl));
}
TEST_F(AIPageContentAgentTest, ClickabilityReasonClickEvents) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body><div id='testDiv'>Clickable</div>"
"<script>document.getElementById('testDiv').onclick = "
"function(){};</script>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(div_node.content_attributes->node_interaction_info);
EXPECT_THAT(
div_node.content_attributes->node_interaction_info->clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kClickEvents));
}
TEST_F(AIPageContentAgentTest, ClickabilityReasonMouseHover) {
// An element with various mouse event listeners.
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body><div id='testDiv'>Mouse Events</div>"
"<script>"
" const div = document.getElementById('testDiv');"
" div.onmouseover = function(){};"
"</script>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(div_node.content_attributes->node_interaction_info);
EXPECT_THAT(
div_node.content_attributes->node_interaction_info->clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kMouseHover));
EXPECT_THAT(
div_node.content_attributes->node_interaction_info->clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kMouseEvents));
}
TEST_F(AIPageContentAgentTest, ClickabilityReasonMouseClick) {
// An element with various mouse event listeners.
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body><div id='testDiv'>Mouse Events</div>"
"<script>"
" const div = document.getElementById('testDiv');"
" div.onmousedown = function(){};"
"</script>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(div_node.content_attributes->node_interaction_info);
EXPECT_THAT(
div_node.content_attributes->node_interaction_info->clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kMouseClick));
EXPECT_THAT(
div_node.content_attributes->node_interaction_info->clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kMouseEvents));
}
TEST_F(AIPageContentAgentTest, ClickabilityReasonKeyEvents) {
// An element with keyboard event listeners.
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body><input type='text' id='testInput'>"
"<script>"
" const input = document.getElementById('testInput');"
" input.onkeydown = function(){};"
"</script>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& input_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(input_node.content_attributes->node_interaction_info);
EXPECT_THAT(input_node.content_attributes->node_interaction_info
->clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kKeyEvents));
}
TEST_F(AIPageContentAgentTest, ClickabilityReasonEditable) {
// A div with contenteditable attribute.
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body><div contenteditable='true'>Editable Content</div></body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(div_node.content_attributes->node_interaction_info);
EXPECT_THAT(
div_node.content_attributes->node_interaction_info->clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kEditable));
}
TEST_F(AIPageContentAgentTest, ClickabilityReasonCursorPointer) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body><div style='cursor: pointer;'>Pointer Cursor</div></body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(div_node.content_attributes->node_interaction_info);
EXPECT_THAT(
div_node.content_attributes->node_interaction_info->clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kCursorPointer));
}
TEST_F(AIPageContentAgentTest, ClickabilityReasonAriaRole) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), "<body><div role='link'>ARIA Link</div></body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(div_node.content_attributes->node_interaction_info);
EXPECT_EQ(div_node.content_attributes->aria_role,
ax::mojom::blink::Role::kLink);
EXPECT_THAT(
div_node.content_attributes->node_interaction_info->clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kAriaRole));
}
TEST_F(AIPageContentAgentTest, ClickabilityReasonMultipleReasons) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
"<button id='multiReasonBtn' contenteditable='true' style='cursor: "
"pointer;' role='menuitem'>"
"Multi-Reason Button"
"</button>"
"<script>"
" const btn = document.getElementById('multiReasonBtn');"
" btn.onclick = function(){};"
" btn.onmouseover = function(){};"
" btn.onmouseup = function(){};"
" btn.onkeydown = function(){};"
"</script>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& button_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(button_node.content_attributes->node_interaction_info);
EXPECT_THAT(
button_node.content_attributes->node_interaction_info
->clickability_reasons,
testing::UnorderedElementsAre(
mojom::blink::AIPageContentClickabilityReason::kClickableControl,
mojom::blink::AIPageContentClickabilityReason::kClickEvents,
mojom::blink::AIPageContentClickabilityReason::kMouseEvents,
mojom::blink::AIPageContentClickabilityReason::kMouseHover,
mojom::blink::AIPageContentClickabilityReason::kMouseClick,
mojom::blink::AIPageContentClickabilityReason::kKeyEvents,
mojom::blink::AIPageContentClickabilityReason::kEditable,
mojom::blink::AIPageContentClickabilityReason::kCursorPointer,
mojom::blink::AIPageContentClickabilityReason::kAriaRole));
}
TEST_F(AIPageContentAgentTest, ClickabilityReasonNoReasons) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), "<body><div>Plain Div</div></body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(div_node.content_attributes->node_interaction_info);
EXPECT_TRUE(div_node.content_attributes->node_interaction_info
->clickability_reasons.empty());
}
TEST_F(AIPageContentAgentTest, AriaHasPopup) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body><div aria-haspopup=true>Plain Div</div></body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(div_node.content_attributes->node_interaction_info);
const auto& interaction_info =
*div_node.content_attributes->node_interaction_info;
EXPECT_THAT(
interaction_info.clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kAriaHasPopup));
}
TEST_F(AIPageContentAgentTest, AriaExpandedTrue) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body><div aria-expanded=true>Plain Div</div></body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(div_node.content_attributes->node_interaction_info);
const auto& interaction_info =
*div_node.content_attributes->node_interaction_info;
EXPECT_THAT(
interaction_info.clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kAriaExpandedTrue));
}
TEST_F(AIPageContentAgentTest, AriaExpandedFalse) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body><div aria-expanded=false>Plain Div</div></body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(div_node.content_attributes->node_interaction_info);
const auto& interaction_info =
*div_node.content_attributes->node_interaction_info;
EXPECT_THAT(
interaction_info.clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kAriaExpandedFalse));
}
TEST_F(AIPageContentAgentTest, Autocomplete) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"(<body>
<input>
<input autocomplete=off>
<input autocomplete=on>
<input aria-autocomplete>
<input aria-autocomplete=none>
<input aria-autocomplete=list>
</body>)",
url_test_helpers::ToKURL("http://foobar.com"));
const bool kExpected[] = {
false, // no attribute
false, // disabled
true,
false, // empty
false, // disabled
true,
};
GetAIPageContentWithActionableElements();
for (int i = 0; bool expected : kExpected) {
SCOPED_TRACE(i);
const auto& input_node = *ContentRootNode().children_nodes[i];
ASSERT_TRUE(input_node.content_attributes->node_interaction_info);
const auto& interaction_info =
*input_node.content_attributes->node_interaction_info;
EXPECT_THAT(
interaction_info.clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kAutocomplete)
.Times(expected));
++i;
}
}
TEST_F(AIPageContentAgentTest, TabIndex) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), "<body><div tabindex=0>Plain Div</div></body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
ASSERT_TRUE(div_node.content_attributes->node_interaction_info);
const auto& interaction_info =
*div_node.content_attributes->node_interaction_info;
EXPECT_THAT(interaction_info.clickability_reasons,
testing::Contains(
mojom::blink::AIPageContentClickabilityReason::kTabIndex));
}
TEST_F(AIPageContentAgentTest, ClipPathCircle) {
// The <div> element is clipped to a small circle.
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"(
<body>
<style>
div {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 100px;
background: red;
clip-path: circle(20%);
}
</style>
<div onclick=console.log(1)></div>
</body>)",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
CheckGeometry(div_node, gfx::Rect(30, 30, 40, 40), gfx::Rect(30, 30, 40, 40));
}
TEST_F(AIPageContentAgentTest, ClipPathEllipse) {
// The <div> element is clipped to an ellipse.
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"(
<body>
<style>
div {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 100px;
background: red;
clip-path: ellipse(20px 50px);
}
</style>
<div onclick=console.log(1)></div>
</body>)",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
CheckGeometry(div_node, gfx::Rect(30, 0, 40, 100), gfx::Rect(30, 0, 40, 100));
}
TEST_F(AIPageContentAgentTest, ClipPathCircleHalf) {
// Only the bottom half of the circle is within the viewport.
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"(
<body>
<style>
div {
position: absolute;
top: -50px;
left: 0;
width: 100px;
height: 100px;
background: red;
clip-path: circle(20%);
}
</style>
<div onclick=console.log(1)></div>
</body>)",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
CheckGeometry(div_node, gfx::Rect(30, -20, 40, 40), gfx::Rect(30, 0, 40, 20));
}
TEST_F(AIPageContentAgentTest, ClipPathEmpty) {
// The entire <div> element is clipped.
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"(
<body>
<style>
body { margin: 0; }
div {
position: absolute;
top: 200px;
left: 300px;
width: 100px;
height: 100px;
background: red;
clip-path: circle(0 at left top);
}
</style>
<div onclick=console.log(1)></div>
</body>)",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
const auto& geometry = *div_node.content_attributes->geometry;
EXPECT_TRUE(geometry.outer_bounding_box.IsEmpty());
EXPECT_TRUE(geometry.visible_bounding_box.IsEmpty());
}
TEST_F(AIPageContentAgentTest, InlineWithFloatAndInlineContentGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"HTML(
<body>
<style>
body { margin: 0; font: 10px/10px Ahem; }
a { position: absolute; top: 0; left: 0; color: black; }
#floater { float: left; width: 30px; height: 10px; background: #ccc; }
</style>
<a id="target" href="#"><span id="floater"></span>XX</a>
</body>)HTML",
url_test_helpers::ToKURL("http://example.com"));
LoadAhem();
GetAIPageContentWithActionableElements();
Document* document = helper_.LocalMainFrame()->GetFrame()->GetDocument();
ASSERT_TRUE(document);
Element* anchor_element = document->getElementById(AtomicString("target"));
ASSERT_TRUE(anchor_element);
LayoutObject* anchor_layout_object = anchor_element->GetLayoutObject();
ASSERT_TRUE(anchor_layout_object);
const auto* anchor_box =
DynamicTo<LayoutBoxModelObject>(anchor_layout_object);
ASSERT_TRUE(anchor_box);
const mojom::blink::AIPageContentNode* anchor_node =
FindNodeBySelector("#target");
ASSERT_TRUE(anchor_node);
ASSERT_TRUE(anchor_node->content_attributes->geometry);
const auto& anchor_geometry = *anchor_node->content_attributes->geometry;
EXPECT_EQ(gfx::Rect(0, 0, 50, 10), anchor_geometry.outer_bounding_box);
EXPECT_EQ(gfx::Rect(0, 0, 50, 10), anchor_geometry.visible_bounding_box);
const mojom::blink::AIPageContentNode* floater_node =
FindNodeBySelector("#floater");
ASSERT_TRUE(floater_node);
ASSERT_TRUE(floater_node->content_attributes->geometry);
const auto& floater_geometry = *floater_node->content_attributes->geometry;
EXPECT_EQ(gfx::Rect(0, 0, 30, 10), floater_geometry.outer_bounding_box);
EXPECT_EQ(gfx::Rect(0, 0, 30, 10), floater_geometry.visible_bounding_box);
}
TEST_F(AIPageContentAgentTest, ActionableModeKeepsContainerQuadGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"HTML(
<body style="margin: 0; font: 10px/10px Ahem;">
<details id="wrapper">
<summary>Expandable</summary>
Contents.
</details>
</body>)HTML",
url_test_helpers::ToKURL("http://example.com"));
LoadAhem();
GetAIPageContentWithActionableElements();
Document* document = helper_.LocalMainFrame()->GetFrame()->GetDocument();
ASSERT_TRUE(document);
Element* wrapper_element = document->getElementById(AtomicString("wrapper"));
ASSERT_TRUE(wrapper_element);
DOMNodeId wrapper_dom_node_id = DOMNodeIds::IdForNode(wrapper_element);
const auto* wrapper_node = FindNodeByDomNodeId(wrapper_dom_node_id);
ASSERT_TRUE(wrapper_node);
CheckContainerNode(*wrapper_node);
ASSERT_TRUE(wrapper_node->content_attributes->geometry);
const auto& geometry = *wrapper_node->content_attributes->geometry;
LayoutObject* wrapper_layout_object = wrapper_element->GetLayoutObject();
ASSERT_TRUE(wrapper_layout_object);
const auto* wrapper_box =
DynamicTo<LayoutBoxModelObject>(wrapper_layout_object);
ASSERT_TRUE(wrapper_box);
EXPECT_EQ(gfx::Rect(0, 0, 1000, 10), geometry.outer_bounding_box);
EXPECT_EQ(gfx::Rect(0, 0, 1000, 10), geometry.visible_bounding_box);
}
TEST_F(AIPageContentAgentTest, InlineWithFloatInlineBoxUnionGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"HTML(
<body style="margin: 0; font: 10px/10px Ahem;">
<a id="inline-float-union">
<span id="inline-wrapper">
<span id="floated" class="inner-float">1</span>
<span id="inline-box" style="display:inline-block; width: 4px; height: 20px;">2</span>
</span>
</a>
<style>
.inner-float {
width: 20px;
height: 20px;
display: block;
float: left;
}
</style>
</body>)HTML",
url_test_helpers::ToKURL("http://example.com"));
LoadAhem();
GetAIPageContentWithActionableElements();
Document* document = helper_.LocalMainFrame()->GetFrame()->GetDocument();
ASSERT_TRUE(document);
// As there is no clipping for either node, the outer box == visible box.
{
const auto* anchor_node = FindNodeBySelector("#inline-float-union");
ASSERT_TRUE(anchor_node);
ASSERT_TRUE(anchor_node->content_attributes);
ASSERT_TRUE(anchor_node->content_attributes->geometry);
const auto& anchor_geometry = *anchor_node->content_attributes->geometry;
EXPECT_EQ(anchor_geometry.outer_bounding_box,
anchor_geometry.visible_bounding_box);
}
{
const auto* container_node = FindNodeBySelector("#inline-wrapper");
ASSERT_TRUE(container_node);
ASSERT_TRUE(container_node->content_attributes);
ASSERT_TRUE(container_node->content_attributes->geometry);
const auto& container_geometry =
*container_node->content_attributes->geometry;
EXPECT_EQ(container_geometry.outer_bounding_box,
container_geometry.visible_bounding_box);
}
}
TEST_F(AIPageContentAgentTest, InlineWithOnlyFloatGeometry) {
// An inline anchor with no inline text content but containing a floated
// descendant should not inherit geometry from the float.
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"(
<body>
<style>
body { margin: 0; }
a { position: absolute; left: 0; top: 0; }
#floater { float: left; width: 20px; height: 10px; background: #000; }
</style>
<a href="#"><span><span id="floater"></span></span></a>
</body>)",
url_test_helpers::ToKURL("http://example.com"));
// Use actionable mode so the anchor is included in the APC tree.
GetAIPageContentWithActionableElements();
// Find the first node that has anchor data (iterative DFS, no std::function).
const auto& root = ContentRootNode();
const mojom::blink::AIPageContentNode* anchor_node = nullptr;
Vector<const mojom::blink::AIPageContentNode*> stack;
stack.push_back(&root);
while (!stack.empty()) {
const auto* node = stack.back();
stack.pop_back();
if (node->content_attributes && node->content_attributes->anchor_data) {
anchor_node = node;
break;
}
// Push children in reverse so traversal order matches natural order.
for (size_t i = node->children_nodes.size(); i > 0; --i) {
stack.push_back(node->children_nodes[i - 1].get());
}
}
ASSERT_TRUE(anchor_node);
ASSERT_TRUE(anchor_node->content_attributes->geometry);
const auto& anchor_geom = *anchor_node->content_attributes->geometry;
// Inline elements that keep their own fragment should retain geometry even
// when their only ink comes from a floating descendant.
EXPECT_EQ(anchor_geom.outer_bounding_box, gfx::Rect(0, 0, 20, 10));
EXPECT_EQ(anchor_geom.visible_bounding_box, gfx::Rect(0, 0, 20, 10));
}
TEST_F(AIPageContentAgentTest, StructuralWrapperWithoutPaintGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body style="margin: 0; font: 10px/10px Ahem;">
<a id="wrapper" href="#" style="position: relative; display: inline-block;">
<span id="child" style="position: absolute; left: 0; top: 0;
width: 10px; height: 10px;
background: black; color: white;">X</span>
</a>
</body>)HTML",
url_test_helpers::ToKURL("http://example.com"));
LoadAhem();
GetAIPageContentWithActionableElements();
auto* wrapper_node = FindNodeBySelector("#wrapper");
ASSERT_TRUE(wrapper_node);
EXPECT_TRUE(wrapper_node->content_attributes->anchor_data);
ASSERT_TRUE(wrapper_node->content_attributes->geometry);
const auto& wrapper_geometry = *wrapper_node->content_attributes->geometry;
EXPECT_TRUE(wrapper_geometry.visible_bounding_box.IsEmpty());
EXPECT_TRUE(wrapper_geometry.outer_bounding_box.IsEmpty());
auto* child_node = FindNodeBySelector("#child");
ASSERT_TRUE(child_node);
ASSERT_TRUE(child_node->content_attributes->geometry);
const auto& child_geometry = *child_node->content_attributes->geometry;
EXPECT_FALSE(child_geometry.visible_bounding_box.IsEmpty());
EXPECT_FALSE(child_geometry.outer_bounding_box.IsEmpty());
}
TEST_F(AIPageContentAgentTest, InlineBlockFixedDescendantKeepsGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<style>
body { margin: 0; font: 10px/10px Ahem; }
.content { padding: 60px 0 0 40px; }
a.inline-block-anchor {
display: inline-block;
width: 100px;
height: 100px;
border: 1px solid black;
}
.fixed-box {
position: fixed;
top: 0;
left: 0;
width: 200px;
height: 25px;
}
</style>
<body>
<div class="content">
<a id="anchor" class="inline-block-anchor" href="#test">
<div class="fixed-box">a</div>
</a>
</div>
</body>)HTML",
url_test_helpers::ToKURL("http://example.com"));
GetAIPageContentWithActionableElements();
const auto* anchor_node = FindNodeBySelector("#anchor");
ASSERT_TRUE(anchor_node);
const auto& attributes = *anchor_node->content_attributes;
EXPECT_EQ(attributes.attribute_type,
mojom::blink::AIPageContentAttributeType::kAnchor);
ASSERT_TRUE(attributes.geometry);
const auto& geometry = *attributes.geometry;
EXPECT_EQ(gfx::Rect(40, 60, 102, 102), geometry.outer_bounding_box);
EXPECT_EQ(geometry.outer_bounding_box, geometry.visible_bounding_box);
}
TEST_F(AIPageContentAgentTest, TableTextClippedByScrollerBeforeScroll) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<style>
body { margin: 0; font: 10px/10px Ahem; }
* { margin: 0; padding: 0; box-sizing: border-box; }
main {
width: 70px;
height: 30px;
overflow: auto;
}
table {
width: 120%;
height: 30px;
table-layout: fixed;
border-collapse: collapse;
}
td.small10x10 {
width: 100px;
height: 3ch;
vertical-align: top;
font: 10px/10px Ahem;
}
</style>
<body>
<main id="scroller">
<table>
<tr>
<td id="cell" class="small10x10">ABC DEF GHI JKL MNO PQR STU VWX YZ 0123456789</td>
<td></td>
</tr>
<tr>
<td></td>
<td style="height: 100vh; width: 100vh;"></td>
</tr>
</table>
</main>
</body>)HTML",
url_test_helpers::ToKURL("http://example.com"));
LoadAhem();
GetAIPageContentWithActionableElements();
Document* document = helper_.LocalMainFrame()->GetFrame()->GetDocument();
ASSERT_TRUE(document);
Element* cell = document->getElementById(AtomicString("cell"));
ASSERT_TRUE(cell);
Node* text = cell->firstChild();
ASSERT_TRUE(text);
ASSERT_TRUE(text->IsTextNode());
const auto* text_node = FindNodeByDomNodeId(DOMNodeIds::IdForNode(text));
ASSERT_TRUE(text_node);
ASSERT_TRUE(text_node->content_attributes->geometry);
Element* scroller_element =
document->getElementById(AtomicString("scroller"));
ASSERT_TRUE(scroller_element);
const auto& geometry = *text_node->content_attributes->geometry;
const auto& fragments = geometry.fragment_visible_bounding_boxes;
const ComputedStyle& before_style = text->GetLayoutObject()->StyleRef();
LocalFrame* local_frame = document->GetFrame();
ASSERT_TRUE(local_frame);
EXPECT_NEAR(before_style.ComputedFontSize(), 10.0f, 0.01f);
EXPECT_NEAR(before_style.GetFont()->GetFontDescription().ComputedSize(),
10.0f, 0.01f);
EXPECT_EQ(gfx::Rect(0, 0, 100, 50), geometry.outer_bounding_box);
EXPECT_EQ(gfx::Rect(0, 0, 70, 30), geometry.visible_bounding_box);
ASSERT_EQ(fragments.size(), 3u);
EXPECT_EQ(fragments[0], gfx::Rect(0, 0, 70, 10));
EXPECT_EQ(fragments[1], gfx::Rect(0, 10, 70, 10));
EXPECT_EQ(fragments[2], gfx::Rect(0, 20, 70, 10));
}
TEST_F(AIPageContentAgentTest, TableTextClippedByScrollerAfterScroll) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<style>
body { margin: 0; font: 10px/10px Ahem; }
* { margin: 0; padding: 0; box-sizing: border-box; }
main {
width: 70px;
height: 30px;
overflow: auto;
}
table {
width: 120%;
height: 30px;
table-layout: fixed;
border-collapse: collapse;
}
td.small10x10 {
width: 100px;
height: 3ch;
vertical-align: top;
font: 10px/10px Ahem;
}
</style>
<body>
<main id="scroller">
<table>
<tr>
<td id="cell" class="small10x10">ABC DEF GHI JKL MNO PQR STU VWX YZ 0123456789</td>
<td></td>
</tr>
<tr>
<td></td>
<td style="height: 100vh; width: 100vh;"></td>
</tr>
</table>
</main>
</body>)HTML",
url_test_helpers::ToKURL("http://example.com"));
LoadAhem();
Document* document = helper_.LocalMainFrame()->GetFrame()->GetDocument();
ASSERT_TRUE(document);
Element* scroller = document->getElementById(AtomicString("scroller"));
ASSERT_TRUE(scroller);
scroller->setScrollLeft(15);
scroller->setScrollTop(15);
LocalFrameView* view = document->View();
ASSERT_TRUE(view);
test::RunPendingTasks();
view->UpdateAllLifecyclePhasesForTest();
GetAIPageContentWithActionableElements();
Element* cell = document->getElementById(AtomicString("cell"));
ASSERT_TRUE(cell);
Node* text = cell->firstChild();
ASSERT_TRUE(text);
ASSERT_TRUE(text->IsTextNode());
const auto* text_node = FindNodeByDomNodeId(DOMNodeIds::IdForNode(text));
ASSERT_TRUE(text_node);
ASSERT_TRUE(text_node->content_attributes->geometry);
const auto& geometry = *text_node->content_attributes->geometry;
const auto& fragments = geometry.fragment_visible_bounding_boxes;
const ComputedStyle& after_style = text->GetLayoutObject()->StyleRef();
std::string fragments_trace;
for (size_t i = 0; i < fragments.size(); ++i) {
if (!fragments_trace.empty()) {
fragments_trace.append("; ");
}
std::string fragment_rect = fragments[i].ToString();
fragments_trace.append(
base::StringPrintf("%zu:%s", i, fragment_rect.c_str()));
}
LocalFrame* local_frame = document->GetFrame();
ASSERT_TRUE(local_frame);
EXPECT_NEAR(after_style.ComputedFontSize(), 10.0f, 0.01f);
EXPECT_NEAR(after_style.GetFont()->GetFontDescription().ComputedSize(), 10.0f,
0.01f);
EXPECT_EQ(gfx::Rect(-15, -15, 100, 50), geometry.outer_bounding_box);
EXPECT_EQ(gfx::Rect(0, 0, 70, 30), geometry.visible_bounding_box);
ASSERT_EQ(fragments.size(), 4u);
EXPECT_EQ(fragments[0], gfx::Rect(0, 0, 55, 5));
EXPECT_EQ(fragments[1], gfx::Rect(0, 5, 55, 10));
EXPECT_EQ(fragments[2], gfx::Rect(0, 15, 70, 10));
EXPECT_EQ(fragments[3], gfx::Rect(0, 25, 70, 5));
}
TEST_F(AIPageContentAgentTest, IframeInlineTextClippedWhenViewportScrolled) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<style>
body { margin: 0; height: 2000px; font: 10px/10px Ahem; }
iframe {
border: none;
width: 100px;
height: 200px;
}
</style>
<body>
<iframe id="target" srcdoc="
<style>
body { margin: 0; font: 10px/10px Ahem; }
p { margin: 0; background: red; }
</style>
<body>
<p id='inner'>ABC DEF GHI JKL MNO PQR STU</p>
</body>">
</iframe>
</body>)HTML",
url_test_helpers::ToKURL("http://example.com"));
LoadAhem();
Document* document = helper_.LocalMainFrame()->GetFrame()->GetDocument();
ASSERT_TRUE(document);
Element* scrolling_element = document->scrollingElement();
ASSERT_TRUE(scrolling_element);
scrolling_element->setScrollLeft(15);
scrolling_element->setScrollTop(15);
LocalFrameView* view = document->View();
ASSERT_TRUE(view);
test::RunPendingTasks();
view->UpdateAllLifecyclePhasesForTest();
HTMLIFrameElement* iframe_element = DynamicTo<HTMLIFrameElement>(
document->getElementById(AtomicString("target")));
ASSERT_TRUE(iframe_element);
LocalFrame* child_frame =
DynamicTo<LocalFrame>(iframe_element->ContentFrame());
ASSERT_TRUE(child_frame);
// Load Ahem inside the iframe before we snapshot geometry to avoid races on
// platforms where font activation is asynchronous.
PageTestBase::LoadAhem(*child_frame);
test::RunPendingTasks();
if (LocalFrameView* child_view = child_frame->View()) {
child_view->UpdateAllLifecyclePhasesForTest();
}
Document* child_document = child_frame->GetDocument();
ASSERT_TRUE(child_document);
Element* inner_p = child_document->getElementById(AtomicString("inner"));
ASSERT_TRUE(inner_p);
Node* inner_text = inner_p->firstChild();
ASSERT_TRUE(inner_text);
ASSERT_TRUE(inner_text->IsTextNode());
GetAIPageContentWithActionableElements();
const auto* text_node =
FindNodeByDomNodeId(DOMNodeIds::IdForNode(inner_text));
ASSERT_TRUE(text_node);
ASSERT_TRUE(text_node->content_attributes->geometry);
const auto& geometry = *text_node->content_attributes->geometry;
const auto& fragments = geometry.fragment_visible_bounding_boxes;
const ComputedStyle& iframe_style = inner_text->GetLayoutObject()->StyleRef();
std::string fragments_trace;
for (size_t i = 0; i < fragments.size(); ++i) {
if (!fragments_trace.empty()) {
fragments_trace.append("; ");
}
std::string fragment_rect = fragments[i].ToString();
fragments_trace.append(
base::StringPrintf("%zu:%s", i, fragment_rect.c_str()));
}
EXPECT_NEAR(iframe_style.ComputedFontSize(), 10.0f, 0.01f);
EXPECT_NEAR(iframe_style.GetFont()->GetFontDescription().ComputedSize(),
10.0f, 0.01f);
EXPECT_EQ(
AtomicString("Ahem"),
iframe_style.GetFont()->GetFontDescription().FirstFamily().FamilyName());
EXPECT_EQ(gfx::Rect(0, -15, 70, 40), geometry.outer_bounding_box);
EXPECT_EQ(gfx::Rect(0, 0, 70, 25), geometry.visible_bounding_box);
ASSERT_EQ(fragments.size(), 3u);
EXPECT_EQ(fragments[0], gfx::Rect(0, 0, 70, 5));
EXPECT_EQ(fragments[1], gfx::Rect(0, 5, 70, 10));
EXPECT_EQ(fragments[2], gfx::Rect(0, 15, 30, 10));
}
TEST_F(AIPageContentAgentTest, CSSHoverPseudoClass) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"(
<body>
<style>
div:hover {
background-color: red;
}
</style>
<div onclick=console.log(1)></div>
<p>sibling</p>
</body>)",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
EXPECT_TRUE(
div_node.content_attributes->node_interaction_info->clickability_reasons
.Contains(ClickabilityReason::kHoverPseudoClass));
const auto& p_node = *ContentRootNode().children_nodes[1];
EXPECT_FALSE(
p_node.content_attributes->node_interaction_info->clickability_reasons
.Contains(ClickabilityReason::kHoverPseudoClass));
}
TEST_F(AIPageContentAgentTest, CSSHoverPseudoClassNotInherited) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(), R"(
<body>
<style>
div:hover {
background-color: red;
}
</style>
<div onclick=console.log(1)>
<p>child</p>
</div>
<p>sibling</p>
</body>)",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& div_node = *ContentRootNode().children_nodes[0];
EXPECT_TRUE(
div_node.content_attributes->node_interaction_info->clickability_reasons
.Contains(ClickabilityReason::kHoverPseudoClass));
const auto& child_p_node = *div_node.children_nodes[0];
EXPECT_FALSE(child_p_node.content_attributes->node_interaction_info
->clickability_reasons.Contains(
ClickabilityReason::kHoverPseudoClass));
const auto& sibling_p_node = *ContentRootNode().children_nodes[1];
EXPECT_FALSE(sibling_p_node.content_attributes->node_interaction_info
->clickability_reasons.Contains(
ClickabilityReason::kHoverPseudoClass));
}
// Tests hit-testing and z-order computations for AIPageContentAgent.
class AIPageContentAgentTestZOrder : public base::test::WithFeatureOverride,
public AIPageContentAgentTest {
public:
AIPageContentAgentTestZOrder()
: base::test::WithFeatureOverride(
blink::features::kAIPageContentZOrderEarlyFiltering) {}
~AIPageContentAgentTestZOrder() override = default;
};
TEST_P(AIPageContentAgentTestZOrder, HitTestElementsBasic) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <p style='background:red'>Text 1</p>"
" <p>Text 2</p>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = *Content()->root_node;
ASSERT_TRUE(root.content_attributes->node_interaction_info);
EXPECT_EQ(
root.content_attributes->node_interaction_info->document_scoped_z_order,
1);
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& html = *root.children_nodes.at(0);
EXPECT_EQ(
html.content_attributes->node_interaction_info->document_scoped_z_order,
2);
ASSERT_EQ(html.children_nodes.size(), 1u);
const auto& body = *html.children_nodes.at(0);
EXPECT_EQ(
body.content_attributes->node_interaction_info->document_scoped_z_order,
3);
if (IsParamFeatureEnabled()) {
// When relying directly on the hit test result for z order, the tree should
// look as follows, with the given z order. This is consistent with "tree
// order" depth-first traversal as defined in the CSS spec:
// https://www.w3.org/TR/CSS2/zindex.html
// root - 1
// |_html - 2
// |_body - 3
// |_p - 4
// | |_Text1 - 5
// |_p - 6
// |_Text2 - 7
ASSERT_EQ(body.children_nodes.size(), 2u);
const auto& p1 = *body.children_nodes.at(0);
EXPECT_EQ(
p1.content_attributes->node_interaction_info->document_scoped_z_order,
4);
ASSERT_EQ(p1.children_nodes.size(), 1u);
const auto& text1 = *p1.children_nodes.at(0);
CheckTextNode(text1, "Text 1");
EXPECT_EQ(text1.content_attributes->node_interaction_info
->document_scoped_z_order,
5);
const auto& p2 = *body.children_nodes.at(1);
EXPECT_EQ(
p2.content_attributes->node_interaction_info->document_scoped_z_order,
6);
ASSERT_EQ(p2.children_nodes.size(), 1u);
const auto& text2 = *p2.children_nodes.at(0);
CheckTextNode(text2, "Text 2");
EXPECT_EQ(text2.content_attributes->node_interaction_info
->document_scoped_z_order,
7);
} else {
// The tree should look as follows, with the given z order.
// root - 1
// |_html - 2
// |_body - 3
// |_p - 4
// | |_Text1 - 6
// |_p - 5
// |_Text2 - 7
ASSERT_EQ(body.children_nodes.size(), 2u);
const auto& p1 = *body.children_nodes.at(0);
EXPECT_EQ(
p1.content_attributes->node_interaction_info->document_scoped_z_order,
4);
const auto& p2 = *body.children_nodes.at(1);
EXPECT_EQ(
p2.content_attributes->node_interaction_info->document_scoped_z_order,
5);
ASSERT_EQ(p1.children_nodes.size(), 1u);
const auto& text1 = *p1.children_nodes.at(0);
CheckTextNode(text1, "Text 1");
EXPECT_EQ(text1.content_attributes->node_interaction_info
->document_scoped_z_order,
6);
ASSERT_EQ(p2.children_nodes.size(), 1u);
const auto& text2 = *p2.children_nodes.at(0);
CheckTextNode(text2, "Text 2");
EXPECT_EQ(text2.content_attributes->node_interaction_info
->document_scoped_z_order,
7);
}
}
TEST_P(AIPageContentAgentTestZOrder, HitTestElementsFixedPos) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <p style='position: fixed; top: 10px;'>Text 1</p>"
" <p>Text 2</p>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 2u);
// The first node is now on top.
const auto& p1 = *root.children_nodes.at(0);
ASSERT_TRUE(p1.content_attributes->node_interaction_info);
ASSERT_TRUE(
p1.content_attributes->node_interaction_info->document_scoped_z_order);
EXPECT_EQ(
p1.content_attributes->node_interaction_info->document_scoped_z_order, 6);
const auto& p2 = *root.children_nodes.at(1);
ASSERT_TRUE(p2.content_attributes->node_interaction_info);
ASSERT_TRUE(
p2.content_attributes->node_interaction_info->document_scoped_z_order);
EXPECT_EQ(
p2.content_attributes->node_interaction_info->document_scoped_z_order, 4);
}
TEST_P(AIPageContentAgentTestZOrder, HitTestElementsPointerNone) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <p style='pointer-events:none'>Text 1</p>"
" <p>Text 2</p>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 2u);
// The first node is not actionable anymore.
const auto& p1 = *root.children_nodes.at(0);
EXPECT_FALSE(p1.content_attributes->node_interaction_info);
const auto& p2 = *root.children_nodes.at(1);
ASSERT_TRUE(p2.content_attributes->node_interaction_info);
ASSERT_TRUE(
p2.content_attributes->node_interaction_info->document_scoped_z_order);
}
TEST_P(AIPageContentAgentTestZOrder, HitTestElementsOffscreen) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <p style='cursor:pointer; position:fixed; top:110vh;'>Text 1</p>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 1u);
// The first node is actionable but not in viewport
const auto& p1 = *root.children_nodes.at(0);
ASSERT_TRUE(p1.content_attributes->node_interaction_info);
const auto& interaction_info = *p1.content_attributes->node_interaction_info;
EXPECT_FALSE(interaction_info.clickability_reasons.empty());
EXPECT_FALSE(interaction_info.document_scoped_z_order);
}
TEST_P(AIPageContentAgentTestZOrder, HitTestElementsIframe) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<iframe srcdoc='<p>Text 1</p>'></iframe>
<p>Text 2</p>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
// The iframe and outer p have z order relative to each other.
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
ASSERT_EQ(root.children_nodes.size(), 2u);
const auto& iframe = *root.children_nodes.at(0);
ASSERT_TRUE(iframe.content_attributes->node_interaction_info);
ASSERT_TRUE(iframe.content_attributes->node_interaction_info
->document_scoped_z_order);
const auto& p = *root.children_nodes.at(1);
ASSERT_TRUE(p.content_attributes->node_interaction_info);
ASSERT_TRUE(
p.content_attributes->node_interaction_info->document_scoped_z_order);
if (IsParamFeatureEnabled()) {
// If we're respecting tree-order traversal when computing z-order, the
// iframe will have a lower z-order because it is the first child.
EXPECT_LT(
*iframe.content_attributes->node_interaction_info
->document_scoped_z_order,
*p.content_attributes->node_interaction_info->document_scoped_z_order);
} else {
EXPECT_GT(
*iframe.content_attributes->node_interaction_info
->document_scoped_z_order,
*p.content_attributes->node_interaction_info->document_scoped_z_order);
}
ASSERT_EQ(iframe.children_nodes.size(), 1u);
const auto& doc_inside_iframe = *iframe.children_nodes.at(0);
ASSERT_TRUE(doc_inside_iframe.content_attributes->node_interaction_info);
ASSERT_TRUE(doc_inside_iframe.content_attributes->node_interaction_info
->document_scoped_z_order);
EXPECT_EQ(*doc_inside_iframe.content_attributes->node_interaction_info
->document_scoped_z_order,
1);
}
TEST_P(AIPageContentAgentTestZOrder, OverflowHiddenGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <div style='width: 100px; height: 100px; overflow-y: hidden;'>"
" <article style='width: 50px; height: 300px;'></article>"
" </div>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& outer = ContentRootNode().children_nodes[0];
const auto& article = outer->children_nodes[0];
CheckAnnotatedRole(*article,
mojom::blink::AIPageContentAnnotatedRole::kArticle);
EXPECT_GT(*article->content_attributes->node_interaction_info
->document_scoped_z_order,
*outer->content_attributes->node_interaction_info
->document_scoped_z_order);
CheckGeometry(*outer, gfx::Rect(8, 8, 100, 100), gfx::Rect(8, 8, 100, 100));
CheckGeometry(*article, gfx::Rect(8, 8, 50, 300), gfx::Rect(8, 8, 50, 100));
}
TEST_P(AIPageContentAgentTestZOrder, OverflowVisibleGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <section style='width: 100px; height: 100px; overflow-y: visible;'>"
" <article style='width: 50px; height: 300px;'></article>"
" </section>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& outer = ContentRootNode().children_nodes[0];
const auto& article = outer->children_nodes[0];
CheckAnnotatedRole(*article,
mojom::blink::AIPageContentAnnotatedRole::kArticle);
EXPECT_GT(*article->content_attributes->node_interaction_info
->document_scoped_z_order,
*outer->content_attributes->node_interaction_info
->document_scoped_z_order);
CheckGeometry(*outer, gfx::Rect(8, 8, 100, 100), gfx::Rect(8, 8, 100, 100));
CheckGeometry(*article, gfx::Rect(8, 8, 50, 300), gfx::Rect(8, 8, 50, 300));
}
TEST_P(AIPageContentAgentTestZOrder, HitTestElementsRelativePos) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <section style='width: 100px; height: 100px; position: relative; "
"overflow: clip;'>"
" <article style='width: 50px; height: 50px; position: absolute; "
"left: "
"150px; top:0px;'></article>"
" </section>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& outer = ContentRootNode().children_nodes[0];
const auto& article = outer->children_nodes[0];
CheckAnnotatedRole(*article,
mojom::blink::AIPageContentAnnotatedRole::kArticle);
EXPECT_GT(*article->content_attributes->node_interaction_info
->document_scoped_z_order,
*outer->content_attributes->node_interaction_info
->document_scoped_z_order);
CheckGeometry(*outer, gfx::Rect(8, 8, 100, 100), gfx::Rect(8, 8, 100, 100));
CheckGeometry(*article, gfx::Rect(158, 8, 50, 50), gfx::Rect());
}
TEST_P(AIPageContentAgentTestZOrder, LabelWithText) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body style='margin:0; font:1px/1px Ahem'>"
" <label>xyz</label>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
LoadAhem();
GetAIPageContentWithActionableElements();
const auto& body = ContentRootNode();
ASSERT_EQ(body.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kContainer);
const auto& label = *body.children_nodes.at(0);
ASSERT_EQ(label.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kContainer);
const auto& text = *label.children_nodes.at(0);
ASSERT_EQ(text.content_attributes->attribute_type,
mojom::blink::AIPageContentAttributeType::kText);
SCOPED_TRACE("Label");
CheckGeometry(label, gfx::Rect(0, 0, 3, 1), gfx::Rect(0, 0, 3, 1));
SCOPED_TRACE("Text");
CheckGeometry(text, gfx::Rect(0, 0, 3, 1), gfx::Rect(0, 0, 3, 1));
}
TEST_P(AIPageContentAgentTestZOrder, HitTestElementsAnchorWithSpanParent) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
R"HTML(
<body>
<span id="target-span">
<a id="link" href="https://example.com">
<span id="inner-span">
This is the inner span.
</span>
<div id="inner-div">
This is the inner div.
</div>
</a>
</span>
</body>
)HTML",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = *Content()->root_node;
EXPECT_EQ(
root.content_attributes->node_interaction_info->document_scoped_z_order,
1);
ASSERT_EQ(root.children_nodes.size(), 1u);
const auto& html = *root.children_nodes.at(0);
EXPECT_EQ(
html.content_attributes->node_interaction_info->document_scoped_z_order,
2);
ASSERT_EQ(html.children_nodes.size(), 1u);
const auto& body = *html.children_nodes.at(0);
EXPECT_EQ(
body.content_attributes->node_interaction_info->document_scoped_z_order,
3);
ASSERT_EQ(body.children_nodes.size(), 1u);
if (IsParamFeatureEnabled()) {
// When filtering for duplicate hit test nodes early, the resulting z-order
// will follow "tree order", which is depth-first. The resulting tree with
// corresponding z-order will be:
// root - 1
// |_html - 2
// |_body - 3
// |_span - 4
// |_a - 5
// |_span - 6
// | |_text - 7
// |_div - 8
// |_text - 9
const auto& target_span = *body.children_nodes.at(0);
EXPECT_EQ(target_span.content_attributes->node_interaction_info
->document_scoped_z_order,
4);
ASSERT_EQ(target_span.children_nodes.size(), 1u);
const auto& anchor = *target_span.children_nodes.at(0);
EXPECT_EQ(anchor.content_attributes->node_interaction_info
->document_scoped_z_order,
5);
ASSERT_EQ(anchor.children_nodes.size(), 2u);
const auto& inner_span = *anchor.children_nodes.at(0);
EXPECT_EQ(inner_span.content_attributes->node_interaction_info
->document_scoped_z_order,
6);
ASSERT_EQ(inner_span.children_nodes.size(), 1u);
const auto& span_text = *inner_span.children_nodes.at(0);
EXPECT_EQ(span_text.content_attributes->node_interaction_info
->document_scoped_z_order,
7);
const auto& inner_div = *anchor.children_nodes.at(1);
EXPECT_EQ(inner_div.content_attributes->node_interaction_info
->document_scoped_z_order,
8);
ASSERT_EQ(inner_div.children_nodes.size(), 1u);
const auto& div_text = *inner_div.children_nodes.at(0);
EXPECT_EQ(div_text.content_attributes->node_interaction_info
->document_scoped_z_order,
9);
} else {
// When we don't filter for duplicate hit test nodes early, the span
// containers will be included multiple times, causing tree order to be
// violated. The resulting tree with corresponding z-order will be:
// root - 1
// |_html - 2
// |_body - 3
// |_span - 6
// |_a - 4
// |_span - 7
// | |_text - 8
// |_div - 5
// |_text - 9
const auto& target_span = *body.children_nodes.at(0);
EXPECT_EQ(target_span.content_attributes->node_interaction_info
->document_scoped_z_order,
6);
ASSERT_EQ(target_span.children_nodes.size(), 1u);
const auto& anchor = *target_span.children_nodes.at(0);
EXPECT_EQ(anchor.content_attributes->node_interaction_info
->document_scoped_z_order,
4);
ASSERT_EQ(anchor.children_nodes.size(), 2u);
const auto& inner_span = *anchor.children_nodes.at(0);
EXPECT_EQ(inner_span.content_attributes->node_interaction_info
->document_scoped_z_order,
7);
ASSERT_EQ(inner_span.children_nodes.size(), 1u);
const auto& span_text = *inner_span.children_nodes.at(0);
EXPECT_EQ(span_text.content_attributes->node_interaction_info
->document_scoped_z_order,
8);
const auto& inner_div = *anchor.children_nodes.at(1);
EXPECT_EQ(inner_div.content_attributes->node_interaction_info
->document_scoped_z_order,
5);
ASSERT_EQ(inner_div.children_nodes.size(), 1u);
const auto& div_text = *inner_div.children_nodes.at(0);
EXPECT_EQ(div_text.content_attributes->node_interaction_info
->document_scoped_z_order,
9);
}
}
INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(AIPageContentAgentTestZOrder);
TEST_F(AIPageContentAgentTest, LinkWithOverflowGeometry) {
frame_test_helpers::LoadHTMLString(
helper_.LocalMainFrame(),
"<body>"
" <style>"
" a {"
" display: inline-block;"
" width: 20px;"
" height: 20px;"
" overflow: visible;"
" }"
" </style>"
" <a href='#' style='font-size: 50px'>Text that will overflow</a>"
"</body>",
url_test_helpers::ToKURL("http://foobar.com"));
GetAIPageContentWithActionableElements();
const auto& root = ContentRootNode();
EXPECT_EQ(root.children_nodes.size(), 1u);
auto& anchor_node = *root.children_nodes[0];
CheckAnchorNode(anchor_node, url_test_helpers::ToKURL("http://foobar.com/#"),
{});
const auto& geometry = *anchor_node.content_attributes->geometry;
// Visible overflow is not currently included in the outer_bounding_box or
// visible_bounding_box. This may not be the correct choice given that visible
// overflow is hit testable. However, this is not a major concern because
// events bubble up. If we see a problem case caused by the lack of overflow
// inclusion we can attempt to incorporate hit test rects (which was attempted
// previously but was non-trivial).
EXPECT_EQ(geometry.outer_bounding_box.width(), 20);
EXPECT_EQ(geometry.outer_bounding_box.width(), 20);
EXPECT_EQ(geometry.visible_bounding_box.width(), 20);
EXPECT_EQ(geometry.visible_bounding_box.height(), 20);
}
} // namespace
} // namespace blink