blob: ee516ebcaf591d84e45b9a916d85405034d0f303 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <map>
#include <memory>
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/pdf/renderer/pdf_accessibility_tree.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/common/content_switches.h"
#include "content/public/renderer/render_accessibility.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/test/render_view_test.h"
#include "pdf/accessibility_structs.h"
#include "pdf/pdf_accessibility_action_handler.h"
#include "pdf/pdf_accessibility_image_fetcher.h"
#include "pdf/pdf_features.h"
#include "third_party/blink/public/strings/grit/blink_accessibility_strings.h"
#include "third_party/blink/public/web/web_ax_object.h"
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_settings.h"
#include "third_party/blink/public/web/web_view.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_event_generator.h"
#include "ui/accessibility/ax_mode.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_node_id_forward.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/ax_tree_manager.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
#include <tuple>
#include "base/containers/queue.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/screen_ai/public/mojom/screen_ai_service.mojom.h" // nogncheck crbug.com/1125897
#include "services/screen_ai/public/test/fake_screen_ai_annotator.h" // nogncheck crbug.com/40147906
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/ax_tree_update.h"
#include "ui/gfx/geometry/transform.h"
#endif // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
namespace pdf {
namespace {
const chrome_pdf::AccessibilityTextRunInfo kFirstTextRun = {
15, gfx::RectF(26.0f, 189.0f, 84.0f, 13.0f),
chrome_pdf::AccessibilityTextDirection::kNone,
chrome_pdf::AccessibilityTextStyleInfo()};
const chrome_pdf::AccessibilityTextRunInfo kSecondTextRun = {
15, gfx::RectF(28.0f, 117.0f, 152.0f, 19.0f),
chrome_pdf::AccessibilityTextDirection::kNone,
chrome_pdf::AccessibilityTextStyleInfo()};
const chrome_pdf::AccessibilityCharInfo kDummyCharsData[] = {
{'H', 12}, {'e', 6}, {'l', 5}, {'l', 4}, {'o', 8}, {',', 4},
{' ', 4}, {'w', 12}, {'o', 6}, {'r', 6}, {'l', 4}, {'d', 9},
{'!', 4}, {' ', 0}, {' ', 0}, {'G', 16}, {'o', 12}, {'o', 12},
{'d', 12}, {'b', 10}, {'y', 12}, {'e', 12}, {',', 4}, {' ', 6},
{'w', 16}, {'o', 12}, {'r', 8}, {'l', 4}, {'d', 12}, {'!', 2},
};
const chrome_pdf::AccessibilityTextRunInfo kFirstRunMultiLine = {
7, gfx::RectF(26.0f, 189.0f, 84.0f, 13.0f),
chrome_pdf::AccessibilityTextDirection::kNone,
chrome_pdf::AccessibilityTextStyleInfo()};
const chrome_pdf::AccessibilityTextRunInfo kSecondRunMultiLine = {
8, gfx::RectF(26.0f, 189.0f, 84.0f, 13.0f),
chrome_pdf::AccessibilityTextDirection::kNone,
chrome_pdf::AccessibilityTextStyleInfo()};
const chrome_pdf::AccessibilityTextRunInfo kThirdRunMultiLine = {
9, gfx::RectF(26.0f, 189.0f, 84.0f, 13.0f),
chrome_pdf::AccessibilityTextDirection::kNone,
chrome_pdf::AccessibilityTextStyleInfo()};
const chrome_pdf::AccessibilityTextRunInfo kFourthRunMultiLine = {
6, gfx::RectF(26.0f, 189.0f, 84.0f, 13.0f),
chrome_pdf::AccessibilityTextDirection::kNone,
chrome_pdf::AccessibilityTextStyleInfo()};
const char kChromiumTestUrl[] = "www.cs.chromium.org";
using testing::Matches;
using testing::PrintToString;
using testing::UnorderedElementsAre;
// `MATCHER_P2` is copied from ui/accessibility/ax_event_generator_unittest.cc.
MATCHER_P2(HasEventAtNode,
expected_event_type,
expected_node_id,
std::string(negation ? "does not have" : "has") + " " +
PrintToString(expected_event_type) + " on " +
PrintToString(expected_node_id)) {
const auto& event = arg;
return Matches(expected_event_type)(event.event_params->event) &&
Matches(expected_node_id)(event.node_id);
}
void CompareRect(const gfx::RectF& expected_rect,
const gfx::RectF& actual_rect) {
EXPECT_FLOAT_EQ(expected_rect.x(), actual_rect.x());
EXPECT_FLOAT_EQ(expected_rect.y(), actual_rect.y());
EXPECT_FLOAT_EQ(expected_rect.size().height(), actual_rect.size().height());
EXPECT_FLOAT_EQ(expected_rect.size().width(), actual_rect.size().width());
}
constexpr uint32_t MakeARGB(unsigned int a,
unsigned int r,
unsigned int g,
unsigned int b) {
return (a << 24) | (r << 16) | (g << 8) | b;
}
void CheckRootAndStatusNodes(const ui::AXNode* root_node,
size_t num_child,
bool is_pdf_ocr_test,
bool is_ocr_completed,
bool create_empty_ocr_results) {
ASSERT_NE(nullptr, root_node);
EXPECT_EQ(ax::mojom::Role::kPdfRoot, root_node->GetRole());
// There should be `num_child` + 1 (the status wrapper node).
ASSERT_EQ(num_child + 1u, root_node->GetChildCount());
const ui::AXNode* status_wrapper = root_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_wrapper);
EXPECT_EQ(ax::mojom::Role::kBanner, status_wrapper->GetRole());
ASSERT_EQ(1u, status_wrapper->GetChildCount());
const ui::AXNode* status_node = status_wrapper->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_node);
EXPECT_EQ(ax::mojom::Role::kStatus, status_node->GetRole());
if (!is_pdf_ocr_test) {
return;
}
// Following are test steps needed for PDF OCR.
if (is_ocr_completed) {
// Note that the string below must be synced with `IDS_PDF_OCR_NO_RESULT`.
constexpr char kPdfOcrNoResult[] =
"This PDF is inaccessible. No text extracted";
// Note that the string below must be synced with `IDS_PDF_OCR_COMPLETED`.
constexpr char kPdfOcrCompleted[] =
"This PDF is inaccessible. Text extracted, powered by Google AI";
ASSERT_EQ(
create_empty_ocr_results ? kPdfOcrNoResult : kPdfOcrCompleted,
status_node->GetStringAttribute(ax::mojom::StringAttribute::kName));
} else {
// Note that the string below must be synced with
// `IDS_PDF_OCR_FEATURE_ALERT`.
#if BUILDFLAG(IS_CHROMEOS)
constexpr char kPdfOcrFeatureAlert[] =
"This PDF is inaccessible. Press search plus m to open context menu "
"and turn on \"extract text from PDF\"";
#else
constexpr char kPdfOcrFeatureAlert[] =
"This PDF is inaccessible. Open context menu and turn on \"extract "
"text from PDF\"";
#endif // BUILDFLAG(IS_CHROMEOS)
ASSERT_EQ(kPdfOcrFeatureAlert, status_node->GetStringAttribute(
ax::mojom::StringAttribute::kName));
}
}
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
ui::AXTreeUpdate CreateMockOCRResult(const gfx::RectF& image_bounds,
const gfx::RectF& text_bounds1,
const gfx::RectF& text_bounds2) {
ui::AXNodeData page_node;
page_node.role = ax::mojom::Role::kRegion;
page_node.id = 1001;
page_node.relative_bounds.bounds = image_bounds;
ui::AXNodeData text_node1;
text_node1.role = ax::mojom::Role::kStaticText;
text_node1.id = 1002;
text_node1.relative_bounds.bounds = text_bounds1;
page_node.child_ids.push_back(text_node1.id);
ui::AXNodeData text_node2;
text_node2.role = ax::mojom::Role::kStaticText;
text_node2.id = 1003;
text_node2.relative_bounds.bounds = text_bounds2;
page_node.child_ids.push_back(text_node2.id);
ui::AXTreeUpdate child_tree_update;
child_tree_update.root_id = page_node.id;
child_tree_update.nodes = {page_node, text_node1, text_node2};
child_tree_update.has_tree_data = true;
child_tree_update.tree_data.title = "OCR results";
return child_tree_update;
}
uint32_t CalculateBatchCount(uint32_t page_count, uint32_t batch_size) {
return (page_count + batch_size - 1) / batch_size;
}
#endif // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
// This class overrides PdfAccessibilityActionHandler to record received
// action data when tests make an accessibility action call.
class TestPdfAccessibilityActionHandler
: public chrome_pdf::PdfAccessibilityActionHandler {
public:
TestPdfAccessibilityActionHandler() = default;
~TestPdfAccessibilityActionHandler() override = default;
// chrome_pdf::PdfAccessibilityActionHandler:
void EnableAccessibility() override {}
void HandleAccessibilityAction(
const chrome_pdf::AccessibilityActionData& action_data) override {
received_action_data_ = action_data;
}
void LoadOrReloadAccessibility() override {}
chrome_pdf::AccessibilityActionData received_action_data() {
return received_action_data_;
}
private:
chrome_pdf::AccessibilityActionData received_action_data_;
};
struct ImagePosition {
int32_t page_index;
int32_t page_object_index;
};
bool operator<(const ImagePosition& p1, const ImagePosition& p2) {
return (p1.page_index < p2.page_index ||
(p1.page_index == p2.page_index &&
p1.page_object_index < p2.page_object_index));
}
// This class overrides PdfAccessibilityImageFetcher to return an image from
// previously stored images, instead of looking for it in the PDF.
class TestPdfAccessibilityImageFetcher
: public chrome_pdf::PdfAccessibilityImageFetcher {
public:
TestPdfAccessibilityImageFetcher() {
default_bitmap_.allocN32Pixels(/*width=*/1, /*height=*/1,
/*isOpaque=*/false);
}
~TestPdfAccessibilityImageFetcher() override = default;
SkBitmap GetImageForOcr(int32_t page_index,
int32_t page_object_index) override {
auto image = images_.find(ImagePosition(page_index, page_object_index));
return image != images_.end() ? image->second : default_bitmap_;
}
void AddImage(int32_t page_index,
int32_t page_object_index,
SkBitmap bitmap) {
images_[ImagePosition(page_index, page_object_index)] = std::move(bitmap);
}
private:
// Keeps images for (page_index, page_object_index) positions.
std::map<ImagePosition, SkBitmap> images_;
// Returned for all requests that have no assigned bitmap in `images_`.
SkBitmap default_bitmap_;
chrome_pdf::AccessibilityActionData received_action_data_;
};
// Waits for tasks posted to the thread's task runner to complete.
void WaitForThreadTasks() {
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, run_loop.QuitClosure());
run_loop.Run();
}
class TestPdfAccessibilityTree : public PdfAccessibilityTree {
public:
TestPdfAccessibilityTree(
content::RenderFrame* render_frame,
chrome_pdf::PdfAccessibilityActionHandler* action_handler,
chrome_pdf::PdfAccessibilityImageFetcher* image_fetcher)
: PdfAccessibilityTree(render_frame,
action_handler,
image_fetcher,
/*plugin_container=*/nullptr) {
ForcePluginAXObjectForTesting(blink::WebAXObject::FromWebNode(
render_frame->GetWebFrame()->GetDocument().Body()));
}
~TestPdfAccessibilityTree() override = default;
TestPdfAccessibilityTree(const TestPdfAccessibilityTree&) = delete;
TestPdfAccessibilityTree& operator=(const TestPdfAccessibilityTree&) = delete;
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
std::vector<std::vector<ui::AXTreeUpdate>>& GetTreeUpdates() {
return tree_updates_;
}
void OnOcrDataReceived(std::vector<PdfOcrRequest> ocr_requests,
std::vector<ui::AXTreeUpdate> tree_updates) override {
tree_updates_.push_back(tree_updates);
PdfAccessibilityTree::OnOcrDataReceived(ocr_requests, tree_updates);
}
void CreateFakeOCRHelper(bool create_empty_result) {
CreateOcrHelper();
fake_annotator_ = std::make_unique<screen_ai::test::FakeScreenAIAnnotator>(
create_empty_result);
ocr_helper_for_testing()->SetScreenAIAnnotatorForTesting(
fake_annotator_->BindNewPipeAndPassRemote());
}
private:
std::vector<std::vector<ui::AXTreeUpdate>> tree_updates_;
std::unique_ptr<screen_ai::test::FakeScreenAIAnnotator> fake_annotator_;
#endif // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
};
} // namespace
class PdfAccessibilityTreeTest : public content::RenderViewTest {
public:
PdfAccessibilityTreeTest() = default;
PdfAccessibilityTreeTest(const PdfAccessibilityTreeTest&) = delete;
PdfAccessibilityTreeTest& operator=(const PdfAccessibilityTreeTest&) = delete;
~PdfAccessibilityTreeTest() override = default;
void SetUp() override {
content::RenderViewTest::SetUp();
base::FilePath pak_dir;
base::PathService::Get(base::DIR_ASSETS, &pak_dir);
base::FilePath pak_file =
pak_dir.Append(FILE_PATH_LITERAL("components_tests_resources.pak"));
ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath(
pak_file, ui::kScaleFactorNone);
viewport_info_.zoom = 1.0;
viewport_info_.scale = 1.0;
viewport_info_.scroll = gfx::Point(0, 0);
viewport_info_.offset = gfx::Point(0, 0);
viewport_info_.selection_start_page_index = 0u;
viewport_info_.selection_start_char_index = 0u;
viewport_info_.selection_end_page_index = 0u;
viewport_info_.selection_end_char_index = 0u;
doc_info_.text_accessible = true;
doc_info_.text_copyable = true;
doc_info_.page_count = 1u;
page_info_.page_index = 0u;
page_info_.text_run_count = 0u;
page_info_.char_count = 0u;
page_info_.bounds = gfx::Rect(0, 0, 1, 1);
}
void TearDown() override {
// Ensure we clean up the PDF accessibility tree before the page closes
// since we directly set a plugin container.
pdf_accessibility_tree_->ForcePluginAXObjectForTesting(
blink::WebAXObject());
content::RenderViewTest::TearDown();
}
void CreatePdfAccessibilityTree() {
content::RenderFrame* render_frame = GetMainRenderFrame();
render_frame->SetAccessibilityModeForTest(ui::kAXModeComplete);
ASSERT_TRUE(render_frame->GetRenderAccessibility());
pdf_accessibility_tree_ = std::make_unique<TestPdfAccessibilityTree>(
render_frame, &action_handler_, &image_fetcher_);
WaitForThreadTasks();
}
// Advance time clock in order for tasks posted with delay to run. Then, wait
// for the delayed tasks posted to the thread's task runner to complete.
void WaitForThreadDelayedTasks() {
// `kDelay` must be synced with `kDelayBeforeRemovingStatusNode` in
// pdf_accessibility_tree.cc.
constexpr base::TimeDelta kDelay = base::Seconds(1);
task_environment_.AdvanceClock(kDelay);
task_environment_.RunUntilIdle();
}
protected:
chrome_pdf::AccessibilityImageInfo CreateMockInaccessibleImage() {
chrome_pdf::AccessibilityImageInfo image;
image.alt_text = "";
image.bounds = gfx::RectF(0.0f, 0.0f, 1.0f, 1.0f);
image.page_object_index = 0;
return image;
}
chrome_pdf::AccessibilityViewportInfo viewport_info_;
chrome_pdf::AccessibilityDocInfo doc_info_;
chrome_pdf::AccessibilityPageInfo page_info_;
std::vector<chrome_pdf::AccessibilityTextRunInfo> text_runs_;
std::vector<chrome_pdf::AccessibilityCharInfo> chars_;
chrome_pdf::AccessibilityPageObjects page_objects_;
std::unique_ptr<TestPdfAccessibilityTree> pdf_accessibility_tree_;
TestPdfAccessibilityActionHandler action_handler_;
TestPdfAccessibilityImageFetcher image_fetcher_;
};
TEST_F(PdfAccessibilityTreeTest, TestEmptyPDFPage) {
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
EXPECT_EQ(ax::mojom::Role::kPdfRoot,
pdf_accessibility_tree_->GetRoot()->GetRole());
}
TEST_F(PdfAccessibilityTreeTest, TestAccessibilityDisabledDuringPDFLoad) {
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
WaitForThreadTasks();
// Disable accessibility while the PDF is loading, make sure this
// doesn't crash.
GetMainRenderFrame()->SetAccessibilityModeForTest(ui::AXMode());
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
pdf_accessibility_tree_->ForcePluginAXObjectForTesting(blink::WebAXObject());
}
TEST_F(PdfAccessibilityTreeTest, TestPdfAccessibilityTreeReload) {
CreatePdfAccessibilityTree();
// Make the accessibility tree with a portrait page and then remake with a
// landscape page.
gfx::RectF page_bounds = gfx::RectF(1, 2);
for (size_t i = 1; i <= 2; ++i) {
if (i == 2)
page_bounds.Transpose();
page_info_.bounds = gfx::ToEnclosingRect(page_bounds);
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
ASSERT_TRUE(root_node);
EXPECT_EQ(ax::mojom::Role::kPdfRoot, root_node->GetRole());
// There should be two nodes; the status node (wrapper) and one page node.
ASSERT_EQ(2u, root_node->GetChildCount());
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_TRUE(page_node);
EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
EXPECT_EQ(page_bounds, page_node->data().relative_bounds.bounds);
}
}
TEST_F(PdfAccessibilityTreeTest, TestPdfAccessibilityTreeCreation) {
static const char kTestAltText[] = "Alternate text for image";
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
{
chrome_pdf::AccessibilityLinkInfo link;
link.bounds = gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f);
link.url = kChromiumTestUrl;
link.text_range.index = 0;
link.text_range.count = 1;
link.index_in_page = 0;
page_objects_.links.push_back(std::move(link));
}
{
chrome_pdf::AccessibilityImageInfo image;
image.bounds = gfx::RectF(8.0f, 9.0f, 2.0f, 1.0f);
image.alt_text = kTestAltText;
image.text_run_index = 2;
page_objects_.images.push_back(std::move(image));
}
{
chrome_pdf::AccessibilityImageInfo image;
image.bounds = gfx::RectF(11.0f, 14.0f, 5.0f, 8.0f);
image.text_run_index = 2;
page_objects_.images.push_back(std::move(image));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
/*
* Expected tree structure
* Document
* ++ Region
* ++++ Paragraph
* ++++++ Link
* ++++ Paragraph
* ++++++ Static Text
* ++++++ Image
* ++++++ Image
*/
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/false, /*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_TRUE(page_node);
EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
ASSERT_EQ(2u, page_node->GetChildCount());
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
EXPECT_TRUE(paragraph_node->GetBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject));
ASSERT_EQ(1u, paragraph_node->GetChildCount());
ui::AXNode* link_node = paragraph_node->GetChildAtIndex(0);
ASSERT_TRUE(link_node);
EXPECT_EQ(kChromiumTestUrl,
link_node->GetStringAttribute(ax::mojom::StringAttribute::kUrl));
EXPECT_EQ(ax::mojom::Role::kLink, link_node->GetRole());
EXPECT_EQ(gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f),
link_node->data().relative_bounds.bounds);
ASSERT_EQ(1u, link_node->GetChildCount());
paragraph_node = page_node->GetChildAtIndex(1);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
EXPECT_TRUE(paragraph_node->GetBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject));
ASSERT_EQ(3u, paragraph_node->GetChildCount());
ui::AXNode* static_text_node = paragraph_node->GetChildAtIndex(0);
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
ASSERT_EQ(1u, static_text_node->GetChildCount());
ui::AXNode* image_node = paragraph_node->GetChildAtIndex(1);
ASSERT_TRUE(image_node);
EXPECT_EQ(ax::mojom::Role::kImage, image_node->GetRole());
EXPECT_EQ(gfx::RectF(8.0f, 9.0f, 2.0f, 1.0f),
image_node->data().relative_bounds.bounds);
EXPECT_EQ(kTestAltText,
image_node->GetStringAttribute(ax::mojom::StringAttribute::kName));
image_node = paragraph_node->GetChildAtIndex(2);
ASSERT_TRUE(image_node);
EXPECT_EQ(ax::mojom::Role::kImage, image_node->GetRole());
EXPECT_EQ(gfx::RectF(11.0f, 14.0f, 5.0f, 8.0f),
image_node->data().relative_bounds.bounds);
EXPECT_EQ(l10n_util::GetStringUTF8(IDS_AX_UNLABELED_IMAGE_ROLE_DESCRIPTION),
image_node->GetStringAttribute(ax::mojom::StringAttribute::kName));
}
TEST_F(PdfAccessibilityTreeTest, TestOverlappingAnnots) {
text_runs_.emplace_back(kFirstRunMultiLine);
text_runs_.emplace_back(kSecondRunMultiLine);
text_runs_.emplace_back(kThirdRunMultiLine);
text_runs_.emplace_back(kFourthRunMultiLine);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
{
chrome_pdf::AccessibilityLinkInfo link;
link.bounds = gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f);
link.url = kChromiumTestUrl;
link.text_range.index = 0;
link.text_range.count = 3;
link.index_in_page = 0;
page_objects_.links.push_back(std::move(link));
}
{
chrome_pdf::AccessibilityLinkInfo link;
link.bounds = gfx::RectF(1.0f, 2.0f, 5.0f, 6.0f);
link.url = kChromiumTestUrl;
link.text_range.index = 1;
link.text_range.count = 2;
link.index_in_page = 1;
page_objects_.links.push_back(std::move(link));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
/*
* Expected tree structure
* Document
* ++ Region
* ++++ Paragraph
* ++++++ Link
* ++++++ Link
* ++++++ Static Text
*/
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/false, /*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_TRUE(page_node);
EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
ASSERT_EQ(1u, page_node->GetChildCount());
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>& child_nodes =
paragraph_node->GetAllChildren();
ASSERT_EQ(3u, child_nodes.size());
ui::AXNode* link_node = child_nodes[0];
ASSERT_TRUE(link_node);
EXPECT_EQ(kChromiumTestUrl,
link_node->GetStringAttribute(ax::mojom::StringAttribute::kUrl));
EXPECT_EQ(ax::mojom::Role::kLink, link_node->GetRole());
EXPECT_EQ(gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f),
link_node->data().relative_bounds.bounds);
ASSERT_EQ(1u, link_node->GetChildCount());
link_node = child_nodes[1];
ASSERT_TRUE(link_node);
EXPECT_EQ(kChromiumTestUrl,
link_node->GetStringAttribute(ax::mojom::StringAttribute::kUrl));
EXPECT_EQ(ax::mojom::Role::kLink, link_node->GetRole());
EXPECT_EQ(gfx::RectF(1.0f, 2.0f, 5.0f, 6.0f),
link_node->data().relative_bounds.bounds);
ASSERT_EQ(1u, link_node->GetChildCount());
ui::AXNode* static_text_node = child_nodes[2];
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
ASSERT_EQ(1u, static_text_node->GetChildCount());
}
TEST_F(PdfAccessibilityTreeTest, TestHighlightCreation) {
constexpr uint32_t kHighlightWhiteColor = MakeARGB(255, 255, 255, 255);
const char kPopupNoteText[] = "Text Note";
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
{
chrome_pdf::AccessibilityHighlightInfo highlight;
highlight.bounds = gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f);
highlight.text_range.index = 0;
highlight.text_range.count = 2;
highlight.index_in_page = 0;
highlight.color = kHighlightWhiteColor;
highlight.note_text = kPopupNoteText;
page_objects_.highlights.push_back(std::move(highlight));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
/*
* Expected tree structure
* Document
* ++ Region
* ++++ Paragraph
* ++++++ Highlight
* ++++++++ Static Text
* ++++++++ Note
* ++++++++++ Static Text
*/
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/false, /*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_TRUE(page_node);
EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
ASSERT_EQ(1u, page_node->GetChildCount());
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
ASSERT_EQ(1u, paragraph_node->GetChildCount());
ui::AXNode* highlight_node = paragraph_node->GetChildAtIndex(0);
ASSERT_TRUE(highlight_node);
EXPECT_EQ(ax::mojom::Role::kPdfActionableHighlight,
highlight_node->GetRole());
EXPECT_EQ(l10n_util::GetStringUTF8(IDS_AX_ROLE_DESCRIPTION_PDF_HIGHLIGHT),
highlight_node->GetStringAttribute(
ax::mojom::StringAttribute::kRoleDescription));
EXPECT_EQ(gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f),
highlight_node->data().relative_bounds.bounds);
ASSERT_TRUE(highlight_node->HasIntAttribute(
ax::mojom::IntAttribute::kBackgroundColor));
EXPECT_EQ(kHighlightWhiteColor,
static_cast<uint32_t>(highlight_node->GetIntAttribute(
ax::mojom::IntAttribute::kBackgroundColor)));
ASSERT_EQ(2u, highlight_node->GetChildCount());
ui::AXNode* static_text_node = highlight_node->GetChildAtIndex(0);
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
ASSERT_EQ(2u, static_text_node->GetChildCount());
ui::AXNode* popup_note_node = highlight_node->GetChildAtIndex(1);
ASSERT_TRUE(popup_note_node);
EXPECT_EQ(ax::mojom::Role::kNote, popup_note_node->GetRole());
EXPECT_EQ(l10n_util::GetStringUTF8(IDS_AX_ROLE_DESCRIPTION_PDF_POPUP_NOTE),
popup_note_node->GetStringAttribute(
ax::mojom::StringAttribute::kRoleDescription));
EXPECT_EQ(gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f),
popup_note_node->data().relative_bounds.bounds);
ASSERT_EQ(1u, popup_note_node->GetChildCount());
ui::AXNode* static_popup_note_text_node = popup_note_node->GetChildAtIndex(0);
ASSERT_TRUE(static_popup_note_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText,
static_popup_note_text_node->GetRole());
EXPECT_EQ(ax::mojom::NameFrom::kContents,
static_popup_note_text_node->GetNameFrom());
EXPECT_EQ(kPopupNoteText, static_popup_note_text_node->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_EQ(gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f),
static_popup_note_text_node->data().relative_bounds.bounds);
}
TEST_F(PdfAccessibilityTreeTest, TestTextFieldNodeCreation) {
// Enable feature flag
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
chrome_pdf::features::kAccessiblePDFForm);
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
{
chrome_pdf::AccessibilityTextFieldInfo text_field;
text_field.bounds = gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f);
text_field.index_in_page = 0;
text_field.text_run_index = 2;
text_field.name = "Text Box";
text_field.value = "Text";
text_field.is_read_only = false;
text_field.is_required = false;
text_field.is_password = false;
page_objects_.form_fields.text_fields.push_back(std::move(text_field));
}
{
chrome_pdf::AccessibilityTextFieldInfo text_field;
text_field.bounds = gfx::RectF(1.0f, 10.0f, 5.0f, 6.0f);
text_field.index_in_page = 1;
text_field.text_run_index = 2;
text_field.name = "Text Box 2";
text_field.value = "Text 2";
text_field.is_read_only = true;
text_field.is_required = true;
text_field.is_password = true;
page_objects_.form_fields.text_fields.push_back(std::move(text_field));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
/*
* Expected tree structure
* Document
* ++ Region
* ++++ Paragraph
* ++++++ Static Text
* ++++ Paragraph
* ++++++ Static Text
* ++++++ Text Field
* ++++++ Text Field
*/
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/false, /*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_TRUE(page_node);
EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
ASSERT_EQ(2u, page_node->GetChildCount());
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
ASSERT_EQ(1u, paragraph_node->GetChildCount());
ui::AXNode* static_text_node = paragraph_node->GetChildAtIndex(0);
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
ASSERT_EQ(1u, static_text_node->GetChildCount());
paragraph_node = page_node->GetChildAtIndex(1);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>& child_nodes =
paragraph_node->GetAllChildren();
ASSERT_EQ(3u, child_nodes.size());
static_text_node = child_nodes[0];
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
ASSERT_EQ(1u, static_text_node->GetChildCount());
ui::AXNode* text_field_node = child_nodes[1];
ASSERT_TRUE(text_field_node);
EXPECT_EQ(ax::mojom::Role::kTextField, text_field_node->GetRole());
EXPECT_EQ("Text Box", text_field_node->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_EQ("Text", text_field_node->GetStringAttribute(
ax::mojom::StringAttribute::kValue));
EXPECT_FALSE(text_field_node->HasState(ax::mojom::State::kRequired));
EXPECT_FALSE(text_field_node->HasState(ax::mojom::State::kProtected));
EXPECT_NE(ax::mojom::Restriction::kReadOnly,
text_field_node->data().GetRestriction());
EXPECT_EQ(gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f),
text_field_node->data().relative_bounds.bounds);
EXPECT_EQ(0u, text_field_node->GetChildCount());
text_field_node = child_nodes[2];
ASSERT_TRUE(text_field_node);
EXPECT_EQ(ax::mojom::Role::kTextField, text_field_node->GetRole());
EXPECT_EQ("Text Box 2", text_field_node->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_EQ("Text 2", text_field_node->GetStringAttribute(
ax::mojom::StringAttribute::kValue));
EXPECT_TRUE(text_field_node->HasState(ax::mojom::State::kRequired));
EXPECT_TRUE(text_field_node->HasState(ax::mojom::State::kProtected));
EXPECT_EQ(ax::mojom::Restriction::kReadOnly,
text_field_node->data().GetRestriction());
EXPECT_EQ(gfx::RectF(1.0f, 10.0f, 5.0f, 6.0f),
text_field_node->data().relative_bounds.bounds);
EXPECT_EQ(0u, text_field_node->GetChildCount());
}
TEST_F(PdfAccessibilityTreeTest, TestButtonNodeCreation) {
// Enable feature flag
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
chrome_pdf::features::kAccessiblePDFForm);
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
{
chrome_pdf::AccessibilityButtonInfo check_box;
check_box.bounds = gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f);
check_box.index_in_page = 0;
check_box.text_run_index = 2;
check_box.name = "Read Only Checkbox";
check_box.value = "Yes";
check_box.is_read_only = true;
check_box.is_checked = true;
check_box.control_count = 1;
check_box.control_index = 0;
check_box.type = chrome_pdf::ButtonType::kCheckBox;
page_objects_.form_fields.buttons.push_back(std::move(check_box));
}
{
chrome_pdf::AccessibilityButtonInfo radio_button;
radio_button.bounds = gfx::RectF(1.0f, 2.0f, 5.0f, 6.0f);
radio_button.index_in_page = 1;
radio_button.text_run_index = 2;
radio_button.name = "Radio Button";
radio_button.value = "value 1";
radio_button.is_read_only = false;
radio_button.is_checked = false;
radio_button.control_count = 2;
radio_button.control_index = 0;
radio_button.type = chrome_pdf::ButtonType::kRadioButton;
page_objects_.form_fields.buttons.push_back(std::move(radio_button));
}
{
chrome_pdf::AccessibilityButtonInfo radio_button;
radio_button.bounds = gfx::RectF(1.0f, 3.0f, 5.0f, 6.0f);
radio_button.index_in_page = 2;
radio_button.text_run_index = 2;
radio_button.name = "Radio Button";
radio_button.value = "value 2";
radio_button.is_read_only = false;
radio_button.is_checked = true;
radio_button.control_count = 2;
radio_button.control_index = 1;
radio_button.type = chrome_pdf::ButtonType::kRadioButton;
page_objects_.form_fields.buttons.push_back(std::move(radio_button));
}
{
chrome_pdf::AccessibilityButtonInfo push_button;
push_button.bounds = gfx::RectF(1.0f, 4.0f, 5.0f, 6.0f);
push_button.index_in_page = 3;
push_button.text_run_index = 2;
push_button.name = "Push Button";
push_button.is_read_only = false;
push_button.type = chrome_pdf::ButtonType::kPushButton;
page_objects_.form_fields.buttons.push_back(std::move(push_button));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
/*
* Expected tree structure
* Document
* ++ Region
* ++++ Paragraph
* ++++++ Static Text
* ++++ Paragraph
* ++++++ Static Text
* ++++++ Check Box
* ++++++ Radio Button
* ++++++ Radio Button
* ++++++ Button
*/
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/false, /*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_TRUE(page_node);
EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
ASSERT_EQ(2u, page_node->GetChildCount());
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
ASSERT_EQ(1u, paragraph_node->GetChildCount());
ui::AXNode* static_text_node = paragraph_node->GetChildAtIndex(0);
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
ASSERT_EQ(1u, static_text_node->GetChildCount());
paragraph_node = page_node->GetChildAtIndex(1);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>& child_nodes =
paragraph_node->GetAllChildren();
ASSERT_EQ(5u, child_nodes.size());
static_text_node = child_nodes[0];
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
ASSERT_EQ(1u, static_text_node->GetChildCount());
ui::AXNode* check_box_node = child_nodes[1];
ASSERT_TRUE(check_box_node);
EXPECT_EQ(ax::mojom::Role::kCheckBox, check_box_node->GetRole());
EXPECT_EQ("Read Only Checkbox", check_box_node->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_EQ("Yes", check_box_node->GetStringAttribute(
ax::mojom::StringAttribute::kValue));
EXPECT_EQ(ax::mojom::CheckedState::kTrue,
check_box_node->data().GetCheckedState());
EXPECT_EQ(1,
check_box_node->GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
EXPECT_EQ(
1, check_box_node->GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(ax::mojom::Restriction::kReadOnly,
check_box_node->data().GetRestriction());
EXPECT_EQ(gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f),
check_box_node->data().relative_bounds.bounds);
EXPECT_EQ(0u, check_box_node->GetChildCount());
ui::AXNode* radio_button_node = child_nodes[2];
ASSERT_TRUE(radio_button_node);
EXPECT_EQ(ax::mojom::Role::kRadioButton, radio_button_node->GetRole());
EXPECT_EQ("Radio Button", radio_button_node->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_EQ("value 1", radio_button_node->GetStringAttribute(
ax::mojom::StringAttribute::kValue));
EXPECT_EQ(ax::mojom::CheckedState::kNone,
radio_button_node->data().GetCheckedState());
EXPECT_EQ(
2, radio_button_node->GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
EXPECT_EQ(1, radio_button_node->GetIntAttribute(
ax::mojom::IntAttribute::kPosInSet));
EXPECT_NE(ax::mojom::Restriction::kReadOnly,
radio_button_node->data().GetRestriction());
EXPECT_EQ(gfx::RectF(1.0f, 2.0f, 5.0f, 6.0f),
radio_button_node->data().relative_bounds.bounds);
EXPECT_EQ(0u, radio_button_node->GetChildCount());
radio_button_node = child_nodes[3];
ASSERT_TRUE(radio_button_node);
EXPECT_EQ(ax::mojom::Role::kRadioButton, radio_button_node->GetRole());
EXPECT_EQ("Radio Button", radio_button_node->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_EQ("value 2", radio_button_node->GetStringAttribute(
ax::mojom::StringAttribute::kValue));
EXPECT_EQ(ax::mojom::CheckedState::kTrue,
radio_button_node->data().GetCheckedState());
EXPECT_EQ(
2, radio_button_node->GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
EXPECT_EQ(2, radio_button_node->GetIntAttribute(
ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(ax::mojom::Restriction::kNone,
radio_button_node->data().GetRestriction());
EXPECT_EQ(gfx::RectF(1.0f, 3.0f, 5.0f, 6.0f),
radio_button_node->data().relative_bounds.bounds);
EXPECT_EQ(0u, radio_button_node->GetChildCount());
ui::AXNode* push_button_node = child_nodes[4];
ASSERT_TRUE(push_button_node);
EXPECT_EQ(ax::mojom::Role::kButton, push_button_node->GetRole());
EXPECT_EQ("Push Button", push_button_node->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_EQ(gfx::RectF(1.0f, 4.0f, 5.0f, 6.0f),
push_button_node->data().relative_bounds.bounds);
EXPECT_EQ(0u, push_button_node->GetChildCount());
}
TEST_F(PdfAccessibilityTreeTest, TestListboxNodeCreation) {
// Enable feature flag
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
chrome_pdf::features::kAccessiblePDFForm);
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
struct ListboxOptionInfo {
std::string name;
bool is_selected;
};
const ListboxOptionInfo kExpectedOptions[][3] = {
{{"Alpha", false}, {"Beta", true}, {"Gamma", true}},
{{"Foo", false}, {"Bar", true}, {"Qux", false}}};
const gfx::RectF kExpectedBounds[] = {{1.0f, 1.0f, 5.0f, 6.0f},
{1.0f, 10.0f, 5.0f, 6.0f}};
{
chrome_pdf::AccessibilityChoiceFieldInfo choice_field;
choice_field.bounds = gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f);
choice_field.index_in_page = 0;
choice_field.text_run_index = 2;
choice_field.type = chrome_pdf::ChoiceFieldType::kListBox;
choice_field.name = "List Box";
choice_field.is_read_only = false;
choice_field.is_multi_select = true;
choice_field.has_editable_text_box = false;
for (const ListboxOptionInfo& expected_option : kExpectedOptions[0]) {
chrome_pdf::AccessibilityChoiceFieldOptionInfo choice_field_option;
choice_field_option.name = expected_option.name;
choice_field_option.is_selected = expected_option.is_selected;
choice_field.options.push_back(std::move(choice_field_option));
}
page_objects_.form_fields.choice_fields.push_back(std::move(choice_field));
}
{
chrome_pdf::AccessibilityChoiceFieldInfo choice_field;
choice_field.bounds = gfx::RectF(1.0f, 10.0f, 5.0f, 6.0f);
choice_field.index_in_page = 1;
choice_field.text_run_index = 2;
choice_field.type = chrome_pdf::ChoiceFieldType::kListBox;
choice_field.name = "Read Only List Box";
choice_field.is_read_only = true;
choice_field.is_multi_select = false;
choice_field.has_editable_text_box = false;
for (const ListboxOptionInfo& expected_option : kExpectedOptions[1]) {
chrome_pdf::AccessibilityChoiceFieldOptionInfo choice_field_option;
choice_field_option.name = expected_option.name;
choice_field_option.is_selected = expected_option.is_selected;
choice_field.options.push_back(std::move(choice_field_option));
}
page_objects_.form_fields.choice_fields.push_back(std::move(choice_field));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
/*
* Expected tree structure
* Document
* ++ Region
* ++++ Paragraph
* ++++++ Static Text
* ++++ Paragraph
* ++++++ Static Text
* ++++++ Listbox
* ++++++++ Listbox Option
* ++++++++ Listbox Option
* ++++++++ Listbox Option
* ++++++ Listbox
* ++++++++ Listbox Option
* ++++++++ Listbox Option
* ++++++++ Listbox Option
*/
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/false, /*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_TRUE(page_node);
EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
ASSERT_EQ(2u, page_node->GetChildCount());
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
ASSERT_EQ(1u, paragraph_node->GetChildCount());
ui::AXNode* static_text_node = paragraph_node->GetChildAtIndex(0);
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
ASSERT_EQ(1u, static_text_node->GetChildCount());
paragraph_node = page_node->GetChildAtIndex(1);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>& child_nodes =
paragraph_node->GetAllChildren();
ASSERT_EQ(3u, child_nodes.size());
static_text_node = child_nodes[0];
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
ASSERT_EQ(1u, static_text_node->GetChildCount());
{
ui::AXNode* listbox_node = child_nodes[1];
ASSERT_TRUE(listbox_node);
EXPECT_EQ(ax::mojom::Role::kListBox, listbox_node->GetRole());
EXPECT_NE(ax::mojom::Restriction::kReadOnly,
listbox_node->data().GetRestriction());
EXPECT_EQ("List Box", listbox_node->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_TRUE(listbox_node->HasState(ax::mojom::State::kMultiselectable));
EXPECT_TRUE(listbox_node->HasState(ax::mojom::State::kFocusable));
EXPECT_EQ(kExpectedBounds[0], listbox_node->data().relative_bounds.bounds);
ASSERT_EQ(std::size(kExpectedOptions[0]), listbox_node->GetChildCount());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>&
listbox_child_nodes = listbox_node->GetAllChildren();
for (size_t i = 0; i < listbox_child_nodes.size(); i++) {
EXPECT_EQ(ax::mojom::Role::kListBoxOption,
listbox_child_nodes[i]->GetRole());
EXPECT_NE(ax::mojom::Restriction::kReadOnly,
listbox_child_nodes[i]->data().GetRestriction());
EXPECT_EQ(kExpectedOptions[0][i].name,
listbox_child_nodes[i]->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_EQ(kExpectedOptions[0][i].is_selected,
listbox_child_nodes[i]->GetBoolAttribute(
ax::mojom::BoolAttribute::kSelected));
EXPECT_TRUE(
listbox_child_nodes[i]->HasState(ax::mojom::State::kFocusable));
EXPECT_EQ(kExpectedBounds[0],
listbox_child_nodes[i]->data().relative_bounds.bounds);
}
}
{
ui::AXNode* listbox_node = child_nodes[2];
ASSERT_TRUE(listbox_node);
EXPECT_EQ(ax::mojom::Role::kListBox, listbox_node->GetRole());
EXPECT_EQ(ax::mojom::Restriction::kReadOnly,
listbox_node->data().GetRestriction());
EXPECT_EQ("Read Only List Box", listbox_node->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_FALSE(listbox_node->HasState(ax::mojom::State::kMultiselectable));
EXPECT_TRUE(listbox_node->HasState(ax::mojom::State::kFocusable));
EXPECT_EQ(kExpectedBounds[1], listbox_node->data().relative_bounds.bounds);
ASSERT_EQ(std::size(kExpectedOptions[1]), listbox_node->GetChildCount());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>&
listbox_child_nodes = listbox_node->GetAllChildren();
for (size_t i = 0; i < listbox_child_nodes.size(); i++) {
EXPECT_EQ(ax::mojom::Role::kListBoxOption,
listbox_child_nodes[i]->GetRole());
EXPECT_EQ(ax::mojom::Restriction::kReadOnly,
listbox_child_nodes[i]->data().GetRestriction());
EXPECT_EQ(kExpectedOptions[1][i].name,
listbox_child_nodes[i]->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_EQ(kExpectedOptions[1][i].is_selected,
listbox_child_nodes[i]->GetBoolAttribute(
ax::mojom::BoolAttribute::kSelected));
EXPECT_TRUE(
listbox_child_nodes[i]->HasState(ax::mojom::State::kFocusable));
EXPECT_EQ(kExpectedBounds[1],
listbox_child_nodes[i]->data().relative_bounds.bounds);
}
}
}
TEST_F(PdfAccessibilityTreeTest, TestComboboxNodeCreation) {
// Enable feature flag
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
chrome_pdf::features::kAccessiblePDFForm);
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
struct ComboboxOptionInfo {
std::string name;
bool is_selected;
};
const ComboboxOptionInfo kExpectedOptions[][3] = {
{{"Albania", false}, {"Belgium", true}, {"Croatia", true}},
{{"Apple", false}, {"Banana", true}, {"Cherry", false}}};
const gfx::RectF kExpectedBounds[] = {{1.0f, 1.0f, 5.0f, 6.0f},
{1.0f, 10.0f, 5.0f, 6.0f}};
{
chrome_pdf::AccessibilityChoiceFieldInfo choice_field;
choice_field.bounds = gfx::RectF(1.0f, 1.0f, 5.0f, 6.0f);
choice_field.index_in_page = 0;
choice_field.text_run_index = 2;
choice_field.type = chrome_pdf::ChoiceFieldType::kComboBox;
choice_field.name = "Editable Combo Box";
choice_field.is_read_only = false;
choice_field.is_multi_select = true;
choice_field.has_editable_text_box = true;
for (const ComboboxOptionInfo& expected_option : kExpectedOptions[0]) {
chrome_pdf::AccessibilityChoiceFieldOptionInfo choice_field_option;
choice_field_option.name = expected_option.name;
choice_field_option.is_selected = expected_option.is_selected;
choice_field.options.push_back(std::move(choice_field_option));
}
page_objects_.form_fields.choice_fields.push_back(std::move(choice_field));
}
{
chrome_pdf::AccessibilityChoiceFieldInfo choice_field;
choice_field.bounds = gfx::RectF(1.0f, 10.0f, 5.0f, 6.0f);
choice_field.index_in_page = 1;
choice_field.text_run_index = 2;
choice_field.type = chrome_pdf::ChoiceFieldType::kComboBox;
choice_field.name = "Read Only Combo Box";
choice_field.is_read_only = true;
choice_field.is_multi_select = false;
choice_field.has_editable_text_box = false;
for (const ComboboxOptionInfo& expected_option : kExpectedOptions[1]) {
chrome_pdf::AccessibilityChoiceFieldOptionInfo choice_field_option;
choice_field_option.name = expected_option.name;
choice_field_option.is_selected = expected_option.is_selected;
choice_field.options.push_back(std::move(choice_field_option));
}
page_objects_.form_fields.choice_fields.push_back(std::move(choice_field));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
/*
* Expected tree structure
* Document
* ++ Region
* ++++ Paragraph
* ++++++ Static Text
* ++++ Paragraph
* ++++++ Static Text
* ++++++ Combobox Grouping
* ++++++++ Text Field With Combobox
* ++++++++ Listbox
* ++++++++++ Listbox Option
* ++++++++++ Listbox Option
* ++++++++++ Listbox Option
* ++++++ Combobox Grouping
* ++++++++ Combobox Menu Button
* ++++++++ Listbox
* ++++++++++ Listbox Option
* ++++++++++ Listbox Option
* ++++++++++ Listbox Option
*/
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/false, /*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_TRUE(page_node);
EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
ASSERT_EQ(2u, page_node->GetChildCount());
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
ASSERT_EQ(1u, paragraph_node->GetChildCount());
ui::AXNode* static_text_node = paragraph_node->GetChildAtIndex(0);
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
ASSERT_EQ(1u, static_text_node->GetChildCount());
paragraph_node = page_node->GetChildAtIndex(1);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>& child_nodes =
paragraph_node->GetAllChildren();
ASSERT_EQ(3u, child_nodes.size());
static_text_node = child_nodes[0];
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
ASSERT_EQ(1u, static_text_node->GetChildCount());
{
ui::AXNode* combobox_node = child_nodes[1];
ASSERT_TRUE(combobox_node);
EXPECT_EQ(ax::mojom::Role::kComboBoxGrouping, combobox_node->GetRole());
EXPECT_NE(ax::mojom::Restriction::kReadOnly,
combobox_node->data().GetRestriction());
EXPECT_TRUE(combobox_node->HasState(ax::mojom::State::kFocusable));
EXPECT_EQ(kExpectedBounds[0], combobox_node->data().relative_bounds.bounds);
ASSERT_EQ(2u, combobox_node->GetChildCount());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>&
combobox_child_nodes = combobox_node->GetAllChildren();
ui::AXNode* combobox_input_node = combobox_child_nodes[0];
EXPECT_EQ(ax::mojom::Role::kTextFieldWithComboBox,
combobox_input_node->GetRole());
EXPECT_NE(ax::mojom::Restriction::kReadOnly,
combobox_input_node->data().GetRestriction());
EXPECT_EQ("Editable Combo Box", combobox_input_node->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_EQ("Belgium", combobox_input_node->GetStringAttribute(
ax::mojom::StringAttribute::kValue));
EXPECT_TRUE(combobox_input_node->HasState(ax::mojom::State::kFocusable));
EXPECT_EQ(kExpectedBounds[0],
combobox_input_node->data().relative_bounds.bounds);
ui::AXNode* combobox_popup_node = combobox_child_nodes[1];
EXPECT_EQ(ax::mojom::Role::kListBox, combobox_popup_node->GetRole());
EXPECT_NE(ax::mojom::Restriction::kReadOnly,
combobox_popup_node->data().GetRestriction());
EXPECT_TRUE(
combobox_popup_node->HasState(ax::mojom::State::kMultiselectable));
EXPECT_EQ(kExpectedBounds[0],
combobox_popup_node->data().relative_bounds.bounds);
ASSERT_EQ(std::size(kExpectedOptions[0]),
combobox_popup_node->GetChildCount());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>&
popup_child_nodes = combobox_popup_node->GetAllChildren();
for (size_t i = 0; i < popup_child_nodes.size(); i++) {
EXPECT_EQ(ax::mojom::Role::kListBoxOption,
popup_child_nodes[i]->GetRole());
EXPECT_NE(ax::mojom::Restriction::kReadOnly,
popup_child_nodes[i]->data().GetRestriction());
EXPECT_EQ(kExpectedOptions[0][i].name,
popup_child_nodes[i]->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_EQ(kExpectedOptions[0][i].is_selected,
popup_child_nodes[i]->GetBoolAttribute(
ax::mojom::BoolAttribute::kSelected));
EXPECT_TRUE(popup_child_nodes[i]->HasState(ax::mojom::State::kFocusable));
EXPECT_EQ(kExpectedBounds[0],
popup_child_nodes[i]->data().relative_bounds.bounds);
}
EXPECT_EQ(popup_child_nodes[1]->data().id,
combobox_input_node->GetIntAttribute(
ax::mojom::IntAttribute::kActivedescendantId));
const auto& controls_ids = combobox_input_node->GetIntListAttribute(
ax::mojom::IntListAttribute::kControlsIds);
ASSERT_EQ(1u, controls_ids.size());
EXPECT_EQ(controls_ids[0], combobox_popup_node->data().id);
}
{
ui::AXNode* combobox_node = child_nodes[2];
ASSERT_TRUE(combobox_node);
EXPECT_EQ(ax::mojom::Role::kComboBoxGrouping, combobox_node->GetRole());
EXPECT_EQ(ax::mojom::Restriction::kReadOnly,
combobox_node->data().GetRestriction());
EXPECT_TRUE(combobox_node->HasState(ax::mojom::State::kFocusable));
EXPECT_EQ(kExpectedBounds[1], combobox_node->data().relative_bounds.bounds);
ASSERT_EQ(2u, combobox_node->GetChildCount());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>&
combobox_child_nodes = combobox_node->GetAllChildren();
ui::AXNode* combobox_input_node = combobox_child_nodes[0];
EXPECT_EQ(ax::mojom::Role::kComboBoxMenuButton,
combobox_input_node->GetRole());
EXPECT_EQ(ax::mojom::Restriction::kReadOnly,
combobox_input_node->data().GetRestriction());
EXPECT_EQ("Read Only Combo Box", combobox_input_node->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_EQ("Banana", combobox_input_node->GetStringAttribute(
ax::mojom::StringAttribute::kValue));
EXPECT_TRUE(combobox_input_node->HasState(ax::mojom::State::kFocusable));
EXPECT_EQ(kExpectedBounds[1],
combobox_input_node->data().relative_bounds.bounds);
ui::AXNode* combobox_popup_node = combobox_child_nodes[1];
EXPECT_EQ(ax::mojom::Role::kListBox, combobox_popup_node->GetRole());
EXPECT_EQ(ax::mojom::Restriction::kReadOnly,
combobox_popup_node->data().GetRestriction());
EXPECT_EQ(kExpectedBounds[1],
combobox_popup_node->data().relative_bounds.bounds);
ASSERT_EQ(std::size(kExpectedOptions[1]),
combobox_popup_node->GetChildCount());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>&
popup_child_nodes = combobox_popup_node->GetAllChildren();
for (size_t i = 0; i < popup_child_nodes.size(); i++) {
EXPECT_EQ(ax::mojom::Role::kListBoxOption,
popup_child_nodes[i]->GetRole());
EXPECT_EQ(ax::mojom::Restriction::kReadOnly,
popup_child_nodes[i]->data().GetRestriction());
EXPECT_EQ(kExpectedOptions[1][i].name,
popup_child_nodes[i]->GetStringAttribute(
ax::mojom::StringAttribute::kName));
EXPECT_EQ(kExpectedOptions[1][i].is_selected,
popup_child_nodes[i]->GetBoolAttribute(
ax::mojom::BoolAttribute::kSelected));
EXPECT_TRUE(popup_child_nodes[i]->HasState(ax::mojom::State::kFocusable));
EXPECT_EQ(kExpectedBounds[1],
popup_child_nodes[i]->data().relative_bounds.bounds);
}
EXPECT_EQ(popup_child_nodes[1]->data().id,
combobox_input_node->GetIntAttribute(
ax::mojom::IntAttribute::kActivedescendantId));
const auto& controls_ids = combobox_input_node->GetIntListAttribute(
ax::mojom::IntListAttribute::kControlsIds);
ASSERT_EQ(1u, controls_ids.size());
EXPECT_EQ(controls_ids[0], combobox_popup_node->data().id);
}
}
TEST_F(PdfAccessibilityTreeTest, TestPreviousNextOnLine) {
text_runs_.emplace_back(kFirstRunMultiLine);
text_runs_.emplace_back(kSecondRunMultiLine);
text_runs_.emplace_back(kThirdRunMultiLine);
text_runs_.emplace_back(kFourthRunMultiLine);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
{
chrome_pdf::AccessibilityLinkInfo link;
link.bounds = gfx::RectF(0.0f, 0.0f, 0.0f, 0.0f);
link.url = kChromiumTestUrl;
link.text_range.index = 2;
link.text_range.count = 2;
link.index_in_page = 0;
page_objects_.links.push_back(std::move(link));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
/*
* Expected tree structure
* Document
* ++ Region
* ++++ Paragraph
* ++++++ Static Text
* ++++++++ Inline Text Box
* ++++++++ Inline Text Box
* ++++++ Link
* ++++++++ Static Text
* ++++++++++ Inline Text Box
* ++++++++++ Inline Text Box
*/
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/false, /*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_TRUE(page_node);
EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
ASSERT_EQ(1u, page_node->GetChildCount());
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
EXPECT_TRUE(paragraph_node->GetBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject));
ASSERT_EQ(2u, paragraph_node->GetChildCount());
ui::AXNode* static_text_node = paragraph_node->GetChildAtIndex(0);
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
EXPECT_EQ(ax::mojom::NameFrom::kContents, static_text_node->GetNameFrom());
ASSERT_EQ(2u, static_text_node->GetChildCount());
ui::AXNode* previous_inline_node = static_text_node->GetChildAtIndex(0);
ASSERT_TRUE(previous_inline_node);
EXPECT_EQ(ax::mojom::Role::kInlineTextBox, previous_inline_node->GetRole());
EXPECT_EQ(ax::mojom::NameFrom::kContents,
previous_inline_node->GetNameFrom());
ASSERT_FALSE(previous_inline_node->HasIntAttribute(
ax::mojom::IntAttribute::kPreviousOnLineId));
ui::AXNode* next_inline_node = static_text_node->GetChildAtIndex(1);
ASSERT_TRUE(next_inline_node);
EXPECT_EQ(ax::mojom::Role::kInlineTextBox, next_inline_node->GetRole());
EXPECT_EQ(ax::mojom::NameFrom::kContents, next_inline_node->GetNameFrom());
ASSERT_TRUE(next_inline_node->HasIntAttribute(
ax::mojom::IntAttribute::kNextOnLineId));
ASSERT_EQ(next_inline_node->data().id,
previous_inline_node->GetIntAttribute(
ax::mojom::IntAttribute::kNextOnLineId));
ASSERT_EQ(previous_inline_node->data().id,
next_inline_node->GetIntAttribute(
ax::mojom::IntAttribute::kPreviousOnLineId));
ui::AXNode* link_node = paragraph_node->GetChildAtIndex(1);
ASSERT_TRUE(link_node);
EXPECT_EQ(kChromiumTestUrl,
link_node->GetStringAttribute(ax::mojom::StringAttribute::kUrl));
EXPECT_EQ(ax::mojom::Role::kLink, link_node->GetRole());
ASSERT_EQ(1u, link_node->GetChildCount());
static_text_node = link_node->GetChildAtIndex(0);
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
EXPECT_EQ(ax::mojom::NameFrom::kContents, static_text_node->GetNameFrom());
ASSERT_EQ(2u, static_text_node->GetChildCount());
previous_inline_node = static_text_node->GetChildAtIndex(0);
ASSERT_TRUE(previous_inline_node);
EXPECT_EQ(ax::mojom::Role::kInlineTextBox, previous_inline_node->GetRole());
EXPECT_EQ(ax::mojom::NameFrom::kContents,
previous_inline_node->GetNameFrom());
ASSERT_TRUE(previous_inline_node->HasIntAttribute(
ax::mojom::IntAttribute::kPreviousOnLineId));
// Test that text and link on the same line are connected.
ASSERT_EQ(next_inline_node->data().id,
previous_inline_node->GetIntAttribute(
ax::mojom::IntAttribute::kPreviousOnLineId));
next_inline_node = static_text_node->GetChildAtIndex(1);
ASSERT_TRUE(next_inline_node);
EXPECT_EQ(ax::mojom::Role::kInlineTextBox, next_inline_node->GetRole());
EXPECT_EQ(ax::mojom::NameFrom::kContents, next_inline_node->GetNameFrom());
ASSERT_FALSE(next_inline_node->HasIntAttribute(
ax::mojom::IntAttribute::kNextOnLineId));
ASSERT_EQ(next_inline_node->data().id,
previous_inline_node->GetIntAttribute(
ax::mojom::IntAttribute::kNextOnLineId));
ASSERT_EQ(previous_inline_node->data().id,
next_inline_node->GetIntAttribute(
ax::mojom::IntAttribute::kPreviousOnLineId));
}
TEST_F(PdfAccessibilityTreeTest, TextRunsAndCharsMismatch) {
// `chars_` and `text_runs_` span over the same page text. They should denote
// the same page text size, but `text_runs_` is incorrect and only denotes 1
// of 2 text runs.
text_runs_.emplace_back(kFirstTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
// In case of invalid data, only the initialized data should be in the tree.
ASSERT_FALSE(pdf_accessibility_tree_->GetRoot());
}
TEST_F(PdfAccessibilityTreeTest, UnsortedLinkVector) {
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
{
// Add first link in the vector.
chrome_pdf::AccessibilityLinkInfo link;
link.bounds = gfx::RectF(0.0f, 0.0f, 0.0f, 0.0f);
link.text_range.index = 2;
link.text_range.count = 0;
page_objects_.links.push_back(std::move(link));
}
{
// Add second link in the vector.
chrome_pdf::AccessibilityLinkInfo link;
link.bounds = gfx::RectF(0.0f, 0.0f, 0.0f, 0.0f);
link.text_range.index = 0;
link.text_range.count = 1;
page_objects_.links.push_back(std::move(link));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
// In case of invalid data, only the initialized data should be in the tree.
ASSERT_FALSE(pdf_accessibility_tree_->GetRoot());
}
TEST_F(PdfAccessibilityTreeTest, OutOfBoundLink) {
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
{
chrome_pdf::AccessibilityLinkInfo link;
link.bounds = gfx::RectF(0.0f, 0.0f, 0.0f, 0.0f);
link.text_range.index = 3;
link.index_in_page = 0;
link.text_range.count = 0;
page_objects_.links.push_back(std::move(link));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
// In case of invalid data, only the initialized data should be in the tree.
ASSERT_FALSE(pdf_accessibility_tree_->GetRoot());
}
TEST_F(PdfAccessibilityTreeTest, UnsortedImageVector) {
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
{
// Add first image to the vector.
chrome_pdf::AccessibilityImageInfo image;
image.bounds = gfx::RectF(0.0f, 0.0f, 0.0f, 0.0f);
image.text_run_index = 1;
page_objects_.images.push_back(std::move(image));
}
{
// Add second image to the vector.
chrome_pdf::AccessibilityImageInfo image;
image.bounds = gfx::RectF(0.0f, 0.0f, 0.0f, 0.0f);
image.text_run_index = 0;
page_objects_.images.push_back(std::move(image));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
// In case of invalid data, only the initialized data should be in the tree.
ASSERT_FALSE(pdf_accessibility_tree_->GetRoot());
}
TEST_F(PdfAccessibilityTreeTest, OutOfBoundImage) {
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
{
chrome_pdf::AccessibilityImageInfo image;
image.bounds = gfx::RectF(0.0f, 0.0f, 0.0f, 0.0f);
image.text_run_index = 3;
page_objects_.images.push_back(std::move(image));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
// In case of invalid data, only the initialized data should be in the tree.
ASSERT_FALSE(pdf_accessibility_tree_->GetRoot());
}
TEST_F(PdfAccessibilityTreeTest, UnsortedHighlightVector) {
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
{
// Add first highlight in the vector.
chrome_pdf::AccessibilityHighlightInfo highlight;
highlight.bounds = gfx::RectF(0.0f, 0.0f, 1.0f, 1.0f);
highlight.text_range.index = 2;
highlight.text_range.count = 0;
highlight.index_in_page = 0;
page_objects_.highlights.push_back(std::move(highlight));
}
{
// Add second highlight in the vector.
chrome_pdf::AccessibilityHighlightInfo highlight;
highlight.bounds = gfx::RectF(2.0f, 2.0f, 1.0f, 1.0f);
highlight.text_range.index = 0;
highlight.text_range.count = 1;
highlight.index_in_page = 1;
page_objects_.highlights.push_back(std::move(highlight));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
// In case of invalid data, only the initialized data should be in the tree.
ASSERT_FALSE(pdf_accessibility_tree_->GetRoot());
}
TEST_F(PdfAccessibilityTreeTest, OutOfBoundHighlight) {
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
{
chrome_pdf::AccessibilityHighlightInfo highlight;
highlight.bounds = gfx::RectF(0.0f, 0.0f, 1.0f, 1.0f);
highlight.text_range.index = 3;
highlight.text_range.count = 0;
highlight.index_in_page = 0;
page_objects_.highlights.push_back(std::move(highlight));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
// In case of invalid data, only the initialized data should be in the tree.
ASSERT_FALSE(pdf_accessibility_tree_->GetRoot());
}
TEST_F(PdfAccessibilityTreeTest, TestActionDataConversion) {
// This test verifies the AXActionData conversion to
// `chrome_pdf::AccessibilityActionData`.
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
std::unique_ptr<ui::AXActionTarget> pdf_action_target =
pdf_accessibility_tree_->CreateActionTarget(root_node->data().id);
ASSERT_TRUE(pdf_action_target);
EXPECT_EQ(ui::AXActionTarget::Type::kPdf, pdf_action_target->GetType());
EXPECT_TRUE(pdf_action_target->ScrollToMakeVisibleWithSubFocus(
gfx::Rect(0, 0, 50, 50), ax::mojom::ScrollAlignment::kScrollAlignmentLeft,
ax::mojom::ScrollAlignment::kScrollAlignmentTop,
ax::mojom::ScrollBehavior::kDoNotScrollIfVisible));
chrome_pdf::AccessibilityActionData action_data =
action_handler_.received_action_data();
EXPECT_EQ(chrome_pdf::AccessibilityAction::kScrollToMakeVisible,
action_data.action);
EXPECT_EQ(chrome_pdf::AccessibilityScrollAlignment::kLeft,
action_data.horizontal_scroll_alignment);
EXPECT_EQ(chrome_pdf::AccessibilityScrollAlignment::kTop,
action_data.vertical_scroll_alignment);
EXPECT_TRUE(pdf_action_target->ScrollToMakeVisibleWithSubFocus(
gfx::Rect(0, 0, 50, 50),
ax::mojom::ScrollAlignment::kScrollAlignmentRight,
ax::mojom::ScrollAlignment::kScrollAlignmentTop,
ax::mojom::ScrollBehavior::kDoNotScrollIfVisible));
action_data = action_handler_.received_action_data();
EXPECT_EQ(chrome_pdf::AccessibilityScrollAlignment::kRight,
action_data.horizontal_scroll_alignment);
EXPECT_TRUE(pdf_action_target->ScrollToMakeVisibleWithSubFocus(
gfx::Rect(0, 0, 50, 50),
ax::mojom::ScrollAlignment::kScrollAlignmentBottom,
ax::mojom::ScrollAlignment::kScrollAlignmentBottom,
ax::mojom::ScrollBehavior::kDoNotScrollIfVisible));
action_data = action_handler_.received_action_data();
EXPECT_EQ(chrome_pdf::AccessibilityScrollAlignment::kBottom,
action_data.horizontal_scroll_alignment);
EXPECT_TRUE(pdf_action_target->ScrollToMakeVisibleWithSubFocus(
gfx::Rect(0, 0, 50, 50),
ax::mojom::ScrollAlignment::kScrollAlignmentCenter,
ax::mojom::ScrollAlignment::kScrollAlignmentClosestEdge,
ax::mojom::ScrollBehavior::kDoNotScrollIfVisible));
action_data = action_handler_.received_action_data();
EXPECT_EQ(chrome_pdf::AccessibilityScrollAlignment::kCenter,
action_data.horizontal_scroll_alignment);
EXPECT_EQ(chrome_pdf::AccessibilityScrollAlignment::kClosestToEdge,
action_data.vertical_scroll_alignment);
EXPECT_EQ(gfx::Rect({0, 0}, {1, 1}), action_data.target_rect);
}
TEST_F(PdfAccessibilityTreeTest, TestScrollToGlobalPointDataConversion) {
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
std::unique_ptr<ui::AXActionTarget> pdf_action_target =
pdf_accessibility_tree_->CreateActionTarget(root_node->data().id);
ASSERT_TRUE(pdf_action_target);
EXPECT_EQ(ui::AXActionTarget::Type::kPdf, pdf_action_target->GetType());
{
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kScrollToPoint;
action_data.target_point = gfx::Point(50, 50);
EXPECT_TRUE(pdf_action_target->PerformAction(action_data));
}
chrome_pdf::AccessibilityActionData action_data =
action_handler_.received_action_data();
EXPECT_EQ(chrome_pdf::AccessibilityAction::kScrollToGlobalPoint,
action_data.action);
EXPECT_EQ(gfx::Point(50, 50), action_data.target_point);
EXPECT_EQ(gfx::Rect({0, 0}, {1, 1}), action_data.target_rect);
}
TEST_F(PdfAccessibilityTreeTest, TestClickActionDataConversion) {
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
{
chrome_pdf::AccessibilityLinkInfo link;
link.url = kChromiumTestUrl;
link.text_range.index = 0;
link.text_range.count = 1;
link.bounds = {{0, 0}, {10, 10}};
link.index_in_page = 0;
page_objects_.links.push_back(std::move(link));
}
{
chrome_pdf::AccessibilityLinkInfo link;
link.url = kChromiumTestUrl;
link.text_range.index = 1;
link.text_range.count = 1;
link.bounds = {{10, 10}, {10, 10}};
link.index_in_page = 1;
page_objects_.links.push_back(std::move(link));
}
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/false, /*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_NE(nullptr, page_node);
ASSERT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>& para_nodes =
page_node->GetAllChildren();
ASSERT_EQ(2u, para_nodes.size());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>& link_nodes =
para_nodes[1]->GetAllChildren();
ASSERT_EQ(1u, link_nodes.size());
const ui::AXNode* link_node = link_nodes[0];
std::unique_ptr<ui::AXActionTarget> pdf_action_target =
pdf_accessibility_tree_->CreateActionTarget(link_node->data().id);
ASSERT_EQ(ui::AXActionTarget::Type::kPdf, pdf_action_target->GetType());
{
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kDoDefault;
pdf_action_target->PerformAction(action_data);
}
chrome_pdf::AccessibilityActionData pdf_action_data =
action_handler_.received_action_data();
EXPECT_EQ(chrome_pdf::AccessibilityAction::kDoDefaultAction,
pdf_action_data.action);
EXPECT_EQ(chrome_pdf::AccessibilityScrollAlignment::kNone,
pdf_action_data.horizontal_scroll_alignment);
EXPECT_EQ(chrome_pdf::AccessibilityScrollAlignment::kNone,
pdf_action_data.vertical_scroll_alignment);
EXPECT_EQ(0u, pdf_action_data.page_index);
EXPECT_EQ(chrome_pdf::AccessibilityAnnotationType::kLink,
pdf_action_data.annotation_type);
EXPECT_EQ(1u, pdf_action_data.annotation_index);
EXPECT_EQ(gfx::Rect({0, 0}, {0, 0}), pdf_action_data.target_rect);
}
TEST_F(PdfAccessibilityTreeTest, TestEmptyPdfAxActions) {
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
std::unique_ptr<ui::AXActionTarget> pdf_action_target =
pdf_accessibility_tree_->CreateActionTarget(root_node->data().id);
ASSERT_TRUE(pdf_action_target);
gfx::Rect rect = pdf_action_target->GetRelativeBounds();
EXPECT_TRUE(rect.origin().IsOrigin());
EXPECT_TRUE(rect.IsEmpty());
gfx::Point point = pdf_action_target->GetScrollOffset();
EXPECT_EQ(point.x(), 0);
EXPECT_EQ(point.y(), 0);
point = pdf_action_target->MinimumScrollOffset();
EXPECT_EQ(point.x(), 0);
EXPECT_EQ(point.y(), 0);
point = pdf_action_target->MaximumScrollOffset();
EXPECT_EQ(point.x(), 0);
EXPECT_EQ(point.y(), 0);
EXPECT_FALSE(pdf_action_target->ScrollToMakeVisible());
}
TEST_F(PdfAccessibilityTreeTest, TestZoomAndScaleChanges) {
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
viewport_info_.zoom = 1.0;
viewport_info_.scale = 1.0;
viewport_info_.scroll = gfx::Point(0, -56);
viewport_info_.offset = gfx::Point(57, 0);
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
WaitForThreadTasks();
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/false, /*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_TRUE(page_node);
ASSERT_EQ(2u, page_node->GetChildCount());
ui::AXNode* para_node = page_node->GetChildAtIndex(0);
ASSERT_TRUE(para_node);
gfx::RectF rect = para_node->data().relative_bounds.bounds;
CompareRect({{26.0f, 189.0f}, {84.0f, 13.0f}}, rect);
gfx::Transform* transform = root_node->data().relative_bounds.transform.get();
ASSERT_TRUE(transform);
CompareRect({{83.0f, 245.0f}, {84.0f, 13.0f}}, transform->MapRect(rect));
float new_device_scale = 1.5f;
float new_zoom = 1.5f;
viewport_info_.zoom = new_zoom;
viewport_info_.scale = new_device_scale;
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
WaitForThreadTasks();
rect = para_node->data().relative_bounds.bounds;
transform = root_node->data().relative_bounds.transform.get();
ASSERT_TRUE(transform);
CompareRect({{186.75f, 509.25f}, {189.00f, 29.25f}},
transform->MapRect(rect));
}
TEST_F(PdfAccessibilityTreeTest, TestSelectionActionDataConversion) {
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/false, /*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_NE(nullptr, page_node);
ASSERT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>& para_nodes =
page_node->GetAllChildren();
ASSERT_EQ(2u, para_nodes.size());
ASSERT_TRUE(para_nodes[0]);
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>&
static_text_nodes1 = para_nodes[0]->GetAllChildren();
ASSERT_EQ(1u, static_text_nodes1.size());
ASSERT_TRUE(static_text_nodes1[0]);
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>&
inline_text_nodes1 = static_text_nodes1[0]->GetAllChildren();
ASSERT_TRUE(inline_text_nodes1[0]);
ASSERT_EQ(1u, inline_text_nodes1.size());
ASSERT_TRUE(para_nodes[1]);
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>&
static_text_nodes2 = para_nodes[1]->GetAllChildren();
ASSERT_EQ(1u, static_text_nodes2.size());
ASSERT_TRUE(static_text_nodes2[0]);
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>&
inline_text_nodes2 = static_text_nodes2[0]->GetAllChildren();
ASSERT_TRUE(inline_text_nodes2[0]);
ASSERT_EQ(1u, inline_text_nodes2.size());
std::unique_ptr<ui::AXActionTarget> pdf_anchor_action_target =
pdf_accessibility_tree_->CreateActionTarget(
inline_text_nodes1[0]->data().id);
ASSERT_EQ(ui::AXActionTarget::Type::kPdf,
pdf_anchor_action_target->GetType());
std::unique_ptr<ui::AXActionTarget> pdf_focus_action_target =
pdf_accessibility_tree_->CreateActionTarget(
inline_text_nodes2[0]->data().id);
ASSERT_EQ(ui::AXActionTarget::Type::kPdf, pdf_focus_action_target->GetType());
EXPECT_TRUE(pdf_anchor_action_target->SetSelection(
pdf_anchor_action_target.get(), 1, pdf_focus_action_target.get(), 5));
chrome_pdf::AccessibilityActionData pdf_action_data =
action_handler_.received_action_data();
EXPECT_EQ(chrome_pdf::AccessibilityAction::kSetSelection,
pdf_action_data.action);
EXPECT_EQ(0u, pdf_action_data.selection_start_index.page_index);
EXPECT_EQ(1u, pdf_action_data.selection_start_index.char_index);
EXPECT_EQ(0u, pdf_action_data.selection_end_index.page_index);
EXPECT_EQ(20u, pdf_action_data.selection_end_index.char_index);
// Verify selection offsets in tree data.
ui::AXTreeData tree_data;
pdf_accessibility_tree_->GetTreeData(&tree_data);
EXPECT_EQ(static_text_nodes1[0]->id(), tree_data.sel_anchor_object_id);
EXPECT_EQ(0, tree_data.sel_anchor_offset);
EXPECT_EQ(static_text_nodes1[0]->id(), tree_data.sel_focus_object_id);
EXPECT_EQ(0, tree_data.sel_focus_offset);
pdf_anchor_action_target = pdf_accessibility_tree_->CreateActionTarget(
static_text_nodes1[0]->data().id);
ASSERT_EQ(ui::AXActionTarget::Type::kPdf,
pdf_anchor_action_target->GetType());
pdf_focus_action_target = pdf_accessibility_tree_->CreateActionTarget(
inline_text_nodes2[0]->data().id);
ASSERT_EQ(ui::AXActionTarget::Type::kPdf, pdf_focus_action_target->GetType());
EXPECT_TRUE(pdf_anchor_action_target->SetSelection(
pdf_anchor_action_target.get(), 1, pdf_focus_action_target.get(), 4));
pdf_action_data = action_handler_.received_action_data();
EXPECT_EQ(chrome_pdf::AccessibilityAction::kSetSelection,
pdf_action_data.action);
EXPECT_EQ(0u, pdf_action_data.selection_start_index.page_index);
EXPECT_EQ(1u, pdf_action_data.selection_start_index.char_index);
EXPECT_EQ(0u, pdf_action_data.selection_end_index.page_index);
EXPECT_EQ(19u, pdf_action_data.selection_end_index.char_index);
pdf_anchor_action_target =
pdf_accessibility_tree_->CreateActionTarget(para_nodes[0]->data().id);
ASSERT_EQ(ui::AXActionTarget::Type::kPdf,
pdf_anchor_action_target->GetType());
pdf_focus_action_target =
pdf_accessibility_tree_->CreateActionTarget(para_nodes[1]->data().id);
ASSERT_EQ(ui::AXActionTarget::Type::kPdf, pdf_focus_action_target->GetType());
EXPECT_FALSE(pdf_anchor_action_target->SetSelection(
pdf_anchor_action_target.get(), 1, pdf_focus_action_target.get(), 5));
}
TEST_F(PdfAccessibilityTreeTest, TestShowContextMenuAction) {
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
ASSERT_TRUE(root_node);
std::unique_ptr<ui::AXActionTarget> pdf_action_target =
pdf_accessibility_tree_->CreateActionTarget(root_node->data().id);
ASSERT_EQ(ui::AXActionTarget::Type::kPdf, pdf_action_target->GetType());
{
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kShowContextMenu;
// This PDF accessibility tree is attached to a body element.
EXPECT_TRUE(pdf_action_target->PerformAction(action_data));
}
}
TEST_F(PdfAccessibilityTreeTest, StitchChildTreeAction) {
CreatePdfAccessibilityTree();
text_runs_ = {kFirstTextRun, kSecondTextRun};
chars_ = {std::begin(kDummyCharsData), std::end(kDummyCharsData)};
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
chrome_pdf::AccessibilityImageInfo fake_image = CreateMockInaccessibleImage();
fake_image.text_run_index = 1u;
fake_image.page_object_index = 0u;
page_objects_.images.push_back(fake_image);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
ui::AXNode fake_root(&pdf_accessibility_tree_->tree_for_testing(),
/*parent=*/nullptr,
/*id=*/1,
/*index_in_parent=*/0u);
auto child_tree_id = ui::AXTreeID::CreateNewAXTreeID();
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kStitchChildTree;
action_data.target_tree_id =
pdf_accessibility_tree_->tree_for_testing().data().tree_id;
action_data.target_node_id = fake_root.id();
action_data.child_tree_id = child_tree_id;
{
std::unique_ptr<ui::AXActionTarget> pdf_action_target =
pdf_accessibility_tree_->CreateActionTarget(fake_root.id());
// This is a fake node, so no action was created.
ASSERT_EQ(ui::AXActionTarget::Type::kNull, pdf_action_target->GetType());
ASSERT_EQ(nullptr, pdf_accessibility_tree_->GetRoot());
EXPECT_FALSE(pdf_action_target->PerformAction(action_data))
<< "PDF must first be fully loaded.";
}
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
ui::AXNode* pdf_root = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(pdf_root, doc_info_.page_count,
/*is_pdf_ocr_test=*/false, /*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(pdf_root->GetChildCount(), 1u);
ui::AXNode* page = pdf_root->GetChildAtIndex(1u);
ASSERT_NE(nullptr, page);
ASSERT_EQ(2u, page->GetChildCount());
ui::AXNode* paragraph = page->GetChildAtIndex(1u);
ASSERT_NE(nullptr, paragraph);
ASSERT_EQ(2u, paragraph->GetChildCount());
ui::AXNode* image = paragraph->GetChildAtIndex(0u);
ASSERT_NE(nullptr, image);
ASSERT_EQ(ax::mojom::Role::kImage, image->GetRole());
std::unique_ptr<ui::AXTreeManager> child_tree_manager;
{
//
// Set up a child tree that will be stitched into the PDF making the above
// `image` invisible.
//
ui::AXNodeData root;
root.id = 1;
ui::AXNodeData button;
button.id = 2;
ui::AXNodeData static_text;
static_text.id = 3;
ui::AXNodeData inline_box;
inline_box.id = 4;
root.role = ax::mojom::Role::kRootWebArea;
root.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
root.child_ids = {button.id};
button.role = ax::mojom::Role::kButton;
button.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
button.SetName("Button");
// Name is not visible in the tree's text representation, i.e. it may be
// coming from an aria-label.
button.SetNameFrom(ax::mojom::NameFrom::kAttribute);
button.relative_bounds.bounds = gfx::RectF(20, 20, 200, 30);
button.child_ids = {static_text.id};
static_text.role = ax::mojom::Role::kStaticText;
static_text.SetName("Button's visible text");
static_text.child_ids = {inline_box.id};
inline_box.role = ax::mojom::Role::kInlineTextBox;
inline_box.SetName("Button's visible text");
ui::AXTreeUpdate update;
update.root_id = root.id;
update.nodes = {root, button, static_text, inline_box};
update.has_tree_data = true;
update.tree_data.tree_id = child_tree_id;
update.tree_data.parent_tree_id =
pdf_accessibility_tree_->tree_for_testing().GetAXTreeID();
update.tree_data.title = "Generated content";
auto child_tree = std::make_unique<ui::AXTree>(update);
child_tree_manager =
std::make_unique<ui::AXTreeManager>(std::move(child_tree));
}
action_data.target_node_id = paragraph->id();
{
std::unique_ptr<ui::AXActionTarget> pdf_action_target =
pdf_accessibility_tree_->CreateActionTarget(paragraph->data().id);
ASSERT_EQ(ui::AXActionTarget::Type::kPdf, pdf_action_target->GetType());
EXPECT_TRUE(pdf_action_target->PerformAction(action_data));
}
// Fetch `paragraph` again since its pointer would have been invalidated.
paragraph = page->GetChildAtIndex(1u);
ASSERT_NE(nullptr, paragraph);
ASSERT_EQ(ax::mojom::Role::kParagraph, paragraph->GetRole());
EXPECT_EQ(child_tree_id.ToString(),
paragraph->data().GetStringAttribute(
ax::mojom::StringAttribute::kChildTreeId));
EXPECT_EQ(1u, paragraph->GetChildCountCrossingTreeBoundary());
const ui::AXNode* child_root =
paragraph->GetChildAtIndexCrossingTreeBoundary(0u);
ASSERT_NE(nullptr, child_root);
EXPECT_EQ(ax::mojom::Role::kRootWebArea, child_root->GetRole());
const ui::AXNode* button = child_root->GetChildAtIndex(0u);
ASSERT_NE(nullptr, button);
EXPECT_EQ(ax::mojom::Role::kButton, button->GetRole());
const ui::AXNode* static_text = button->GetChildAtIndex(0u);
ASSERT_NE(nullptr, static_text);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text->GetRole());
const ui::AXNode* inline_box = static_text->GetChildAtIndex(0u);
ASSERT_NE(nullptr, inline_box);
EXPECT_EQ(ax::mojom::Role::kInlineTextBox, inline_box->GetRole());
EXPECT_EQ(0u, inline_box->GetChildCount());
}
// TODO(crbug.com/40064422): Remove `CheckLiveRegionPoliteStatus` and
// `CheckLiveRegionNotSetWhenInBackground` below once PDF OCR is launched
// on Windows, Linux, and macOS as these tests will be replaced with
// `PdfOcrTest.CheckLiveRegionPoliteStatus` and
// `PdfOcrTest.CheckLiveRegionNotSetWhenInBackground`, respectively.
#if !BUILDFLAG(IS_CHROMEOS)
TEST_F(PdfAccessibilityTreeTest, CheckLiveRegionPoliteStatus) {
CreatePdfAccessibilityTree();
page_objects_.images.push_back(CreateMockInaccessibleImage());
// Get and use the underlying AXTree to create an AXEventGenerator. This
// event generator is usually instrumented in the test.
ui::AXTree& tree = pdf_accessibility_tree_->tree_for_testing();
ui::AXEventGenerator event_generator(&tree);
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
WaitForThreadTasks();
const ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
ASSERT_NE(nullptr, root_node);
EXPECT_EQ(ax::mojom::Role::kPdfRoot, root_node->GetRole());
ASSERT_EQ(1u, root_node->GetChildCount());
const ui::AXNode* status_wrapper_node = root_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_wrapper_node);
EXPECT_EQ(ax::mojom::Role::kBanner, status_wrapper_node->GetRole());
ASSERT_EQ(1u, status_wrapper_node->GetChildCount());
const ui::AXNode* status_node = status_wrapper_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_node);
EXPECT_EQ(ax::mojom::Role::kStatus, status_node->GetRole());
EXPECT_EQ(1u, status_node->GetChildCount());
EXPECT_TRUE(
status_node->GetBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic));
constexpr char kDefaultLiveRegionRelevant[] = "additions text";
EXPECT_EQ(kDefaultLiveRegionRelevant,
status_node->GetStringAttribute(
ax::mojom::StringAttribute::kLiveRelevant));
constexpr char kStatusLiveRegion[] = "polite";
EXPECT_EQ(kStatusLiveRegion, status_node->GetStringAttribute(
ax::mojom::StringAttribute::kLiveStatus));
EXPECT_TRUE(status_node->GetBoolAttribute(
ax::mojom::BoolAttribute::kContainerLiveAtomic));
EXPECT_EQ(kDefaultLiveRegionRelevant,
status_node->GetStringAttribute(
ax::mojom::StringAttribute::kContainerLiveRelevant));
EXPECT_EQ(kStatusLiveRegion,
status_node->GetStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus));
EXPECT_THAT(
event_generator,
UnorderedElementsAre(
HasEventAtNode(ui::AXEventGenerator::Event::SUBTREE_CREATED,
root_node->id()),
HasEventAtNode(ui::AXEventGenerator::Event::LIVE_REGION_CREATED,
status_node->id())));
page_info_.page_index = 0;
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
EXPECT_EQ(root_node, pdf_accessibility_tree_->GetRoot());
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/false, /*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
// Check if the status node's attributes have been cleared out.
EXPECT_FALSE(
status_node->HasBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic));
EXPECT_FALSE(status_node->HasStringAttribute(
ax::mojom::StringAttribute::kLiveRelevant));
EXPECT_FALSE(
status_node->HasStringAttribute(ax::mojom::StringAttribute::kLiveStatus));
EXPECT_FALSE(status_node->HasBoolAttribute(
ax::mojom::BoolAttribute::kContainerLiveAtomic));
EXPECT_FALSE(status_node->HasStringAttribute(
ax::mojom::StringAttribute::kContainerLiveRelevant));
EXPECT_FALSE(status_node->HasStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus));
EXPECT_FALSE(
status_node->HasStringAttribute(ax::mojom::StringAttribute::kName));
ASSERT_GT(root_node->GetChildCount(), 1u);
const ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_NE(nullptr, page_node);
ASSERT_EQ(1u, page_node->GetChildCount());
const ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, paragraph_node);
ASSERT_EQ(1u, paragraph_node->GetChildCount());
const ui::AXNode* image_node = paragraph_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, image_node);
}
TEST_F(PdfAccessibilityTreeTest, CheckLiveRegionNotSetWhenInBackground) {
CreatePdfAccessibilityTree();
// Simulate going to the background.
pdf_accessibility_tree_->WasHidden();
page_objects_.images.push_back(CreateMockInaccessibleImage());
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
WaitForThreadTasks();
const ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
ASSERT_NE(nullptr, root_node);
EXPECT_EQ(ax::mojom::Role::kPdfRoot, root_node->GetRole());
ASSERT_EQ(1u, root_node->GetChildCount());
const ui::AXNode* status_wrapper_node = root_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_wrapper_node);
EXPECT_EQ(ax::mojom::Role::kBanner, status_wrapper_node->GetRole());
ASSERT_EQ(1u, status_wrapper_node->GetChildCount());
const ui::AXNode* status_node = status_wrapper_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_node);
EXPECT_EQ(ax::mojom::Role::kStatus, status_node->GetRole());
EXPECT_EQ(1u, status_node->GetChildCount());
EXPECT_FALSE(
status_node->HasBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic));
EXPECT_FALSE(status_node->HasStringAttribute(
ax::mojom::StringAttribute::kLiveRelevant));
EXPECT_FALSE(
status_node->HasStringAttribute(ax::mojom::StringAttribute::kLiveStatus));
EXPECT_FALSE(status_node->HasBoolAttribute(
ax::mojom::BoolAttribute::kContainerLiveAtomic));
EXPECT_FALSE(status_node->HasStringAttribute(
ax::mojom::StringAttribute::kContainerLiveRelevant));
EXPECT_FALSE(status_node->HasStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus));
}
#endif // !BUILDFLAG(IS_CHROMEOS)
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
struct PdfOcrHelperTestBatchData {
uint32_t page_count;
uint32_t expected_batch_size;
};
class PdfOcrHelperTest : public PdfAccessibilityTreeTest,
public testing::WithParamInterface<std::tuple<
/* is_ocr_helper_started_before_pdf_loads */ bool,
PdfOcrHelperTestBatchData>> {
public:
PdfOcrHelperTest() : feature_list_(::features::kPdfOcr) {}
PdfOcrHelperTest(const PdfOcrHelperTest&) = delete;
PdfOcrHelperTest& operator=(const PdfOcrHelperTest&) = delete;
~PdfOcrHelperTest() override = default;
protected:
void CreateInaccessiblePdfAndOcrHelper(
uint32_t page_count,
bool is_ocr_helper_started_before_pdf_loads,
bool create_empty_results) {
ASSERT_TRUE(pdf_accessibility_tree_);
doc_info_.page_count = page_count;
chrome_pdf::AccessibilityImageInfo image = CreateMockInaccessibleImage();
ASSERT_EQ(0u, image.text_run_index)
<< "Images should not be anchored to any `TextRunInfo` for the "
"`PdfOcrHelper` to work with them.";
// Each page has two images in it.
page_objects_.images.push_back(image);
page_objects_.images.push_back(image);
if (is_ocr_helper_started_before_pdf_loads) {
pdf_accessibility_tree_->CreateFakeOCRHelper(create_empty_results);
ASSERT_NE(nullptr, pdf_accessibility_tree_->ocr_helper_for_testing());
}
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
ASSERT_EQ(0u, text_runs_.size())
<< "OcrHelper won't run unless the PDF has no accessible text in it.";
ASSERT_EQ(0u, chars_.size())
<< "OcrHelper won't run unless the PDF has no accessible text in it.";
for (uint32_t i = 0; i < doc_info_.page_count; ++i) {
page_info_.page_index = i;
// All pages are identical.
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
}
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/true,
is_ocr_helper_started_before_pdf_loads,
create_empty_results);
ASSERT_GT(root_node->GetChildCount(), 1u);
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_NE(nullptr, page_node);
ASSERT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
ASSERT_EQ(1u, page_node->GetChildCount());
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, paragraph_node);
ASSERT_EQ((is_ocr_helper_started_before_pdf_loads && !create_empty_results)
? ax::mojom::Role::kGenericContainer
: ax::mojom::Role::kParagraph,
paragraph_node->GetRole());
ASSERT_EQ(2u, paragraph_node->GetChildCount());
ui::AXNode* first_node = paragraph_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, first_node);
ASSERT_EQ(is_ocr_helper_started_before_pdf_loads && !create_empty_results
? ax::mojom::Role::kStaticText
: ax::mojom::Role::kImage,
first_node->GetRole());
ASSERT_EQ(0u, first_node->GetChildCount());
ui::AXNode* second_node = paragraph_node->GetChildAtIndex(1);
ASSERT_NE(nullptr, second_node);
ASSERT_EQ(is_ocr_helper_started_before_pdf_loads && !create_empty_results
? ax::mojom::Role::kStaticText
: ax::mojom::Role::kImage,
second_node->GetRole());
ASSERT_EQ(0u, second_node->GetChildCount());
if (!is_ocr_helper_started_before_pdf_loads) {
pdf_accessibility_tree_->CreateFakeOCRHelper(create_empty_results);
ASSERT_NE(nullptr, pdf_accessibility_tree_->ocr_helper_for_testing());
}
}
bool GetIsOcrHelperStartedBeforePdfLoads() const {
return std::get<0>(GetParam());
}
uint32_t GetPageCount() const { return std::get<1>(GetParam()).page_count; }
uint32_t GetExpectedBatchSize() const {
return std::get<1>(GetParam()).expected_batch_size;
}
private:
base::test::ScopedFeatureList feature_list_;
};
TEST_P(PdfOcrHelperTest, PageBatching) {
CreatePdfAccessibilityTree();
const bool is_ocr_helper_started_before_pdf_loads =
GetIsOcrHelperStartedBeforePdfLoads();
const uint32_t page_count = GetPageCount();
ASSERT_NO_FATAL_FAILURE(CreateInaccessiblePdfAndOcrHelper(
page_count, is_ocr_helper_started_before_pdf_loads,
/*create_empty_results=*/false));
const uint32_t pages_per_batch =
pdf_accessibility_tree_->ocr_helper_for_testing()
->pages_per_batch_for_testing();
EXPECT_EQ(GetExpectedBatchSize(), pages_per_batch);
const uint32_t batch_count = CalculateBatchCount(page_count, pages_per_batch);
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
// The first node of the root node's children is a status node. There
// should be no postamble page informing the user of OCR progress
// when OCR has either not yet started, or has been completed.
ASSERT_EQ(page_count + 1u, root_node->GetChildCount());
for (uint32_t i = 0; i < page_count; ++i) {
if (!is_ocr_helper_started_before_pdf_loads) {
ui::AXNode* page_node = root_node->GetChildAtIndex(i + 1);
ASSERT_NE(nullptr, page_node);
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, paragraph_node);
ui::AXNode* image1_node = paragraph_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, image1_node);
ui::AXNode* image2_node = paragraph_node->GetChildAtIndex(1);
ASSERT_NE(nullptr, image2_node);
base::queue<PdfOcrRequest> requests;
requests.emplace(image1_node->id(), CreateMockInaccessibleImage(),
root_node->id(), paragraph_node->id(), page_node->id(),
/*page_index=*/i);
requests.emplace(image2_node->id(), CreateMockInaccessibleImage(),
root_node->id(), paragraph_node->id(), page_node->id(),
/*page_index=*/i);
pdf_accessibility_tree_->ocr_helper_for_testing()->OcrPage(requests);
// Each page has two images.
WaitForThreadTasks();
WaitForThreadTasks();
if (page_count >= pages_per_batch && i >= pages_per_batch &&
i != page_count - 1u) {
// A postamble page informing the user that the OCR process is in
// progress should be present after processing the first batch of
// OCR requests (i.e. when `i >= pages_per_batch`).
const ui::AXTreeUpdate* postamble_update =
pdf_accessibility_tree_->postamble_page_tree_update_for_testing();
ASSERT_NE(nullptr, postamble_update);
ASSERT_GT(postamble_update->nodes.size(), 1u);
const ui::AXNodeData& root = postamble_update->nodes[0];
EXPECT_EQ(ax::mojom::Role::kPdfRoot, root.role);
const ui::AXNodeData& postamble_page = postamble_update->nodes[1];
EXPECT_EQ(ax::mojom::Role::kRegion, postamble_page.role);
ASSERT_NE(ui::kInvalidAXNodeID, postamble_page.id);
const auto iter =
base::ranges::find(root_node->data().child_ids, postamble_page.id);
ASSERT_NE(std::end(root_node->data().child_ids), iter);
ui::AXNode* postamble_page_node = root_node->GetChildAtIndex(
std::distance(std::begin(root_node->data().child_ids), iter));
ASSERT_NE(nullptr, postamble_page_node);
EXPECT_EQ(postamble_page.id, postamble_page_node->id());
EXPECT_EQ(ax::mojom::Role::kRegion, postamble_page_node->GetRole());
}
} else {
// Each page has two images.
WaitForThreadTasks();
WaitForThreadTasks();
}
}
const auto& tree_updates = pdf_accessibility_tree_->GetTreeUpdates();
ASSERT_EQ(batch_count, tree_updates.size());
for (uint32_t i = 0; i < tree_updates.size(); ++i) {
const std::vector<ui::AXTreeUpdate>& page_tree_updates = tree_updates[i];
if (page_count % pages_per_batch != 0u && i == 0) {
// The first batch should have the remaining pages that cannot be
// processed by the rest of the batches because they are full. By design,
// this is always set to 5u in the instantiation of these parameterized
// tests.
ASSERT_EQ(10u, page_tree_updates.size())
<< "There should be five pages in the first batch with two images "
"per page, because we first process the remaining pages after "
"dividing with the batch size.";
for (uint32_t j = 0; j < 10u; ++j) {
EXPECT_EQ(page_tree_updates[j].nodes.size(), 1u);
EXPECT_EQ(page_tree_updates[j].root_id,
page_tree_updates[j].nodes[0].id);
EXPECT_EQ(page_tree_updates[j].nodes[0].role,
ax::mojom::Role::kStaticText);
}
} else {
// All other batches should be full, i.e., their page count should equal
// the number of pages allowed in each batch.
ASSERT_EQ(pages_per_batch * 2u, page_tree_updates.size())
<< "There should be 20 pages in the remaining batches, with two "
"images per page.";
for (uint32_t j = 0; j < pages_per_batch * 2u; ++j) {
EXPECT_EQ(page_tree_updates[j].nodes.size(), 1u);
EXPECT_EQ(page_tree_updates[j].root_id,
page_tree_updates[j].nodes[0].id);
EXPECT_EQ(page_tree_updates[j].nodes[0].role,
ax::mojom::Role::kStaticText);
}
}
}
}
TEST_P(PdfOcrHelperTest, UMAMetrics) {
CreatePdfAccessibilityTree();
base::HistogramTester histograms;
const bool is_ocr_helper_started_before_pdf_loads =
GetIsOcrHelperStartedBeforePdfLoads();
const uint32_t page_count = GetPageCount();
ASSERT_NO_FATAL_FAILURE(CreateInaccessiblePdfAndOcrHelper(
page_count, is_ocr_helper_started_before_pdf_loads,
/*create_empty_results=*/false));
const uint32_t pages_per_batch =
pdf_accessibility_tree_->ocr_helper_for_testing()
->pages_per_batch_for_testing();
for (uint32_t i = 0; i < page_count; ++i) {
if (!is_ocr_helper_started_before_pdf_loads) {
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
ui::AXNode* page_node = root_node->GetChildAtIndex(i + 1);
ASSERT_NE(nullptr, page_node);
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, paragraph_node);
ui::AXNode* image1_node = paragraph_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, image1_node);
ui::AXNode* image2_node = paragraph_node->GetChildAtIndex(1);
ASSERT_NE(nullptr, image2_node);
base::queue<PdfOcrRequest> requests;
requests.emplace(image1_node->id(), CreateMockInaccessibleImage(),
root_node->id(), paragraph_node->id(), page_node->id(),
/*page_index=*/i);
requests.emplace(image2_node->id(), CreateMockInaccessibleImage(),
root_node->id(), paragraph_node->id(), page_node->id(),
/*page_index=*/i);
pdf_accessibility_tree_->ocr_helper_for_testing()->OcrPage(requests);
// The UMA metric recorded in `PdfAccessibilityTree::OnOcrDataReceived()`
// is triggered by `OcrPage()`. `WaitForThreadTasks()` below is similar
// to the purpose of `content::FetchHistogramsFromChildProcesses()`.
WaitForThreadTasks();
}
// Each page has two images.
WaitForThreadTasks();
WaitForThreadTasks();
}
const auto& tree_updates = pdf_accessibility_tree_->GetTreeUpdates();
const uint32_t batch_count = CalculateBatchCount(page_count, pages_per_batch);
ASSERT_EQ(batch_count, tree_updates.size());
histograms.ExpectBucketCount(
"Accessibility.PdfOcr.ActiveWhenInaccessiblePdfOpened",
is_ocr_helper_started_before_pdf_loads,
/*expected_count=*/1);
histograms.ExpectTotalCount(
"Accessibility.PdfOcr.ActiveWhenInaccessiblePdfOpened",
/*expected_count=*/1);
// There are two mock images per page.
histograms.ExpectBucketCount("Accessibility.PdfOcr.PDFImages",
PdfOcrRequestStatus::kRequested,
/*expected_count=*/page_count * 2);
histograms.ExpectBucketCount("Accessibility.PdfOcr.PDFImages",
PdfOcrRequestStatus::kPerformed,
/*expected_count=*/page_count * 2);
histograms.ExpectTotalCount("Accessibility.PdfOcr.PDFImages",
/*expected_count=*/page_count * 4);
// TODO(crbug.com/40267312): The current test fixture does not trigger
// `PdfAccessibilityTree::MaybeHandleAccessibilityChange` when OCR is enabled
// after tree load, and hence does result in calling
// `PdfAccessibilityTree::SetAccessibilityPageInfo` for the second time.
// Either update text fixture to be more realistic, or add metrics test to
// browser test without fake OCR helper.
histograms.ExpectBucketCount("Accessibility.PDF.HasAccessibleText",
/*sample=*/false,
/*expected_count=*/1);
histograms.ExpectTotalCount("Accessibility.PDF.HasAccessibleText",
/*expected_count=*/1);
histograms.ExpectBucketCount("Accessibility.PdfOcr.InaccessiblePdfPageCount",
doc_info_.page_count,
/*expected_count=*/1);
histograms.ExpectTotalCount("Accessibility.PdfOcr.InaccessiblePdfPageCount",
/*expected_count=*/1);
}
TEST_P(PdfOcrHelperTest, EmptyOCRResults) {
CreatePdfAccessibilityTree();
const bool is_ocr_helper_started_before_pdf_loads =
GetIsOcrHelperStartedBeforePdfLoads();
const uint32_t page_count = GetPageCount();
ASSERT_NO_FATAL_FAILURE(CreateInaccessiblePdfAndOcrHelper(
page_count, is_ocr_helper_started_before_pdf_loads,
/*create_empty_results=*/true));
for (uint32_t i = 0; i < page_count; ++i) {
if (!is_ocr_helper_started_before_pdf_loads) {
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
ui::AXNode* page_node = root_node->GetChildAtIndex(i + 1);
ASSERT_NE(nullptr, page_node);
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, paragraph_node);
ui::AXNode* image1_node = paragraph_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, image1_node);
ui::AXNode* image2_node = paragraph_node->GetChildAtIndex(1);
ASSERT_NE(nullptr, image2_node);
base::queue<PdfOcrRequest> requests;
requests.emplace(image1_node->id(), CreateMockInaccessibleImage(),
root_node->id(), paragraph_node->id(), page_node->id(),
/*page_index=*/i);
requests.emplace(image2_node->id(), CreateMockInaccessibleImage(),
root_node->id(), paragraph_node->id(), page_node->id(),
/*page_index=*/i);
pdf_accessibility_tree_->ocr_helper_for_testing()->OcrPage(requests);
}
// Each page has two images.
WaitForThreadTasks();
WaitForThreadTasks();
}
// Make sure that the OCR helper counts a response with empty results to
// determine whether it finished processing all OCR requests.
EXPECT_TRUE(
pdf_accessibility_tree_->ocr_helper_for_testing()->AreAllPagesOcred());
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
ASSERT_NE(nullptr, root_node);
ASSERT_EQ(ax::mojom::Role::kPdfRoot, root_node->GetRole());
uint32_t pages_plus_status_node_count = doc_info_.page_count + 1u;
ASSERT_EQ(pages_plus_status_node_count, root_node->GetChildCount());
ui::AXNode* status_wrapper_node = root_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_wrapper_node);
ASSERT_EQ(ax::mojom::Role::kBanner, status_wrapper_node->GetRole());
ASSERT_EQ(1u, status_wrapper_node->GetChildCount());
ui::AXNode* status_node = status_wrapper_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_node);
ASSERT_EQ(ax::mojom::Role::kStatus, status_node->GetRole());
// Note that the string below must be synced with `IDS_PDF_OCR_NO_RESULT`.
constexpr char kPdfOcrNoResult[] =
"This PDF is inaccessible. No text extracted";
ASSERT_EQ(kPdfOcrNoResult,
status_node->GetStringAttribute(ax::mojom::StringAttribute::kName));
}
TEST_P(PdfOcrHelperTest, OCRCompleteNotification) {
CreatePdfAccessibilityTree();
const bool is_ocr_helper_started_before_pdf_loads =
GetIsOcrHelperStartedBeforePdfLoads();
const uint32_t page_count = GetPageCount();
ASSERT_NO_FATAL_FAILURE(CreateInaccessiblePdfAndOcrHelper(
page_count, is_ocr_helper_started_before_pdf_loads,
/*create_empty_results=*/false));
const ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
ASSERT_NE(nullptr, root_node);
ASSERT_EQ(ax::mojom::Role::kPdfRoot, root_node->GetRole());
const uint32_t pages_plus_status_node_count = doc_info_.page_count + 1u;
ASSERT_EQ(pages_plus_status_node_count, root_node->GetChildCount());
const ui::AXNode* status_wrapper_node = root_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_wrapper_node);
ASSERT_EQ(ax::mojom::Role::kBanner, status_wrapper_node->GetRole());
ASSERT_EQ(1u, status_wrapper_node->GetChildCount());
const ui::AXNode* status_node = status_wrapper_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_node);
ASSERT_EQ(ax::mojom::Role::kStatus, status_node->GetRole());
for (uint32_t i = 0; i < page_count; ++i) {
if (!is_ocr_helper_started_before_pdf_loads) {
const ui::AXNode* page_node = root_node->GetChildAtIndex(i + 1);
ASSERT_NE(nullptr, page_node);
const ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, paragraph_node);
const ui::AXNode* image1_node = paragraph_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, image1_node);
const ui::AXNode* image2_node = paragraph_node->GetChildAtIndex(1);
ASSERT_NE(nullptr, image2_node);
base::queue<PdfOcrRequest> requests;
requests.emplace(image1_node->id(), CreateMockInaccessibleImage(),
root_node->id(), paragraph_node->id(), page_node->id(),
/*page_index=*/i);
requests.emplace(image2_node->id(), CreateMockInaccessibleImage(),
root_node->id(), paragraph_node->id(), page_node->id(),
/*page_index=*/i);
pdf_accessibility_tree_->ocr_helper_for_testing()->OcrPage(requests);
}
// Each page has two images.
WaitForThreadTasks();
WaitForThreadTasks();
}
// Make sure that the OCR helper counts a response with empty results to
// determine whether it finished processing all OCR requests.
EXPECT_TRUE(
pdf_accessibility_tree_->ocr_helper_for_testing()->AreAllPagesOcred());
// Note that the string below must be synced with `IDS_PDF_OCR_COMPLETED`.
constexpr char kPdfOcrCompleted[] =
"This PDF is inaccessible. Text extracted, powered by Google AI";
ASSERT_EQ(kPdfOcrCompleted,
status_node->GetStringAttribute(ax::mojom::StringAttribute::kName));
}
// 5 = smaller than the batch size, 105 = larger than the batch size
// with fewer remaining pages in the first batch, 280 = greater than the
// batch size by a lot and no remaining pages in the first batch.
INSTANTIATE_TEST_SUITE_P(
PdfOcrHelperTests,
PdfOcrHelperTest,
testing::Combine(
/* is_ocr_helper_started_before_pdf_loads */ testing::Bool(),
/* (page_count, expected_batch_size) */ testing::Values(
PdfOcrHelperTestBatchData(5u, 1u),
PdfOcrHelperTestBatchData(105u, 10u),
PdfOcrHelperTestBatchData(280u, 20u))));
// TODO(crbug.com/40267312): Add test for end result on a non-synthetic
// multi-page PDF.
class PdfOcrTest : public PdfAccessibilityTreeTest {
public:
PdfOcrTest() : feature_list_(::features::kPdfOcr) {}
PdfOcrTest(const PdfOcrTest&) = delete;
PdfOcrTest& operator=(const PdfOcrTest&) = delete;
~PdfOcrTest() override = default;
private:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(PdfOcrTest, CheckLiveRegionPoliteStatus) {
CreatePdfAccessibilityTree();
page_objects_.images.push_back(CreateMockInaccessibleImage());
// Get and use the underlying AXTree to create an AXEventGenerator. This
// event generator is usually instrumented in the test.
ui::AXTree& tree = pdf_accessibility_tree_->tree_for_testing();
ui::AXEventGenerator event_generator(&tree);
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
WaitForThreadTasks();
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
ASSERT_NE(nullptr, root_node);
EXPECT_EQ(ax::mojom::Role::kPdfRoot, root_node->GetRole());
ASSERT_EQ(1u, root_node->GetChildCount());
ui::AXNode* status_wrapper_node = root_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_wrapper_node);
EXPECT_EQ(ax::mojom::Role::kBanner, status_wrapper_node->GetRole());
ASSERT_EQ(1u, status_wrapper_node->GetChildCount());
ui::AXNode* status_node = status_wrapper_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_node);
EXPECT_EQ(ax::mojom::Role::kStatus, status_node->GetRole());
EXPECT_EQ(1u, status_node->GetChildCount());
EXPECT_TRUE(
status_node->GetBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic));
constexpr char kDefaultLiveRegionRelevant[] = "additions text";
EXPECT_EQ(kDefaultLiveRegionRelevant,
status_node->GetStringAttribute(
ax::mojom::StringAttribute::kLiveRelevant));
constexpr char kStatusLiveRegion[] = "polite";
EXPECT_EQ(kStatusLiveRegion, status_node->GetStringAttribute(
ax::mojom::StringAttribute::kLiveStatus));
EXPECT_TRUE(status_node->GetBoolAttribute(
ax::mojom::BoolAttribute::kContainerLiveAtomic));
EXPECT_EQ(kDefaultLiveRegionRelevant,
status_node->GetStringAttribute(
ax::mojom::StringAttribute::kContainerLiveRelevant));
EXPECT_EQ(kStatusLiveRegion,
status_node->GetStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus));
EXPECT_THAT(
event_generator,
UnorderedElementsAre(
HasEventAtNode(ui::AXEventGenerator::Event::SUBTREE_CREATED,
root_node->id()),
HasEventAtNode(ui::AXEventGenerator::Event::LIVE_REGION_CREATED,
status_node->id())));
page_info_.page_index = 0;
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
uint32_t pages_plus_status_node_count = doc_info_.page_count + 1u;
ASSERT_EQ(pages_plus_status_node_count, root_node->GetChildCount());
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_NE(nullptr, page_node);
ASSERT_EQ(1u, page_node->GetChildCount());
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, paragraph_node);
ASSERT_EQ(1u, paragraph_node->GetChildCount());
ui::AXNode* image_node = paragraph_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, image_node);
EXPECT_THAT(
event_generator,
UnorderedElementsAre(
HasEventAtNode(ui::AXEventGenerator::Event::SUBTREE_CREATED,
root_node->id()),
HasEventAtNode(ui::AXEventGenerator::Event::CHILDREN_CHANGED,
root_node->id()),
HasEventAtNode(ui::AXEventGenerator::Event::LIVE_REGION_CREATED,
status_node->id()),
HasEventAtNode(ui::AXEventGenerator::Event::LIVE_REGION_NODE_CHANGED,
status_node->id()),
HasEventAtNode(ui::AXEventGenerator::Event::NAME_CHANGED,
status_node->id()),
HasEventAtNode(ui::AXEventGenerator::Event::NAME_CHANGED,
status_node->data().child_ids[0])));
}
TEST_F(PdfOcrTest, CheckLiveRegionNotSetWhenInBackground) {
CreatePdfAccessibilityTree();
// Simulate going to the background.
pdf_accessibility_tree_->WasHidden();
page_objects_.images.push_back(CreateMockInaccessibleImage());
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
WaitForThreadTasks();
const ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
ASSERT_NE(nullptr, root_node);
EXPECT_EQ(ax::mojom::Role::kPdfRoot, root_node->GetRole());
ASSERT_EQ(1u, root_node->GetChildCount());
const ui::AXNode* status_wrapper_node = root_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_wrapper_node);
EXPECT_EQ(ax::mojom::Role::kBanner, status_wrapper_node->GetRole());
ASSERT_EQ(1u, status_wrapper_node->GetChildCount());
const ui::AXNode* status_node = status_wrapper_node->GetChildAtIndex(0);
ASSERT_NE(nullptr, status_node);
EXPECT_EQ(ax::mojom::Role::kStatus, status_node->GetRole());
EXPECT_EQ(1u, status_node->GetChildCount());
EXPECT_FALSE(
status_node->HasBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic));
EXPECT_FALSE(status_node->HasStringAttribute(
ax::mojom::StringAttribute::kLiveRelevant));
EXPECT_FALSE(
status_node->HasStringAttribute(ax::mojom::StringAttribute::kLiveStatus));
EXPECT_FALSE(status_node->HasBoolAttribute(
ax::mojom::BoolAttribute::kContainerLiveAtomic));
EXPECT_FALSE(status_node->HasStringAttribute(
ax::mojom::StringAttribute::kContainerLiveRelevant));
EXPECT_FALSE(status_node->HasStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus));
}
TEST_F(PdfOcrTest, TestTransformFromOnOcrDataReceived) {
// Assume `image` contains some text that will be extracted by OCR. `image`
// will be passed to the function that creates a transform, which will be
// then applied to the text paragraphs extracted by OCR.
chrome_pdf::AccessibilityImageInfo image;
// Simulate that the width and height of `image` got shrunk by 80% in
// `image_data`.
constexpr float kScaleFactor = 0.8f;
constexpr float kImageWidth = 200.0f;
constexpr float kImageHeight = 200.0f;
constexpr int kBitmapWidth = static_cast<int>(kImageWidth * kScaleFactor);
constexpr int kBitmapHeight = static_cast<int>(kImageHeight * kScaleFactor);
image.page_object_index = 0;
image.bounds = gfx::RectF(0.0f, 0.0f, kImageWidth, kImageHeight);
SkBitmap bitmap;
bitmap.allocN32Pixels(kBitmapWidth, kBitmapHeight, /*isOpaque=*/false);
image_fetcher_.AddImage(/*page_index=*/0, image.page_object_index,
std::move(bitmap));
page_objects_.images.push_back(image);
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
/*
* Expected PDF accessibility tree structure (with PDF OCR feature flag)
* Document
* ++ Banner
* ++++ Status
* ++ Region
* ++++ Paragraph
* ++++++ image
*/
ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/true,
/*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_TRUE(page_node);
EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
ASSERT_EQ(1u, page_node->GetChildCount());
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
ASSERT_EQ(1u, paragraph_node->GetChildCount());
ui::AXNode* image_node = paragraph_node->GetChildAtIndex(0);
ASSERT_TRUE(image_node);
EXPECT_EQ(ax::mojom::Role::kImage, image_node->GetRole());
ASSERT_EQ(0u, image_node->GetChildCount());
EXPECT_EQ(image.bounds, image_node->data().relative_bounds.bounds);
// Simulate creating a child tree using OCR results.
pdf_accessibility_tree_->CreateOcrHelper();
// Text bounds before applying the transform.
constexpr gfx::RectF kTextBoundsBeforeTransform1 = {{8.0f, 8.0f},
{80.0f, 24.0f}};
constexpr gfx::RectF kTextBoundsBeforeTransform2 = {{16.0f, 88.0f},
{40.0f, 56.0f}};
ui::AXTreeUpdate child_tree_update = CreateMockOCRResult(
image.bounds, kTextBoundsBeforeTransform1, kTextBoundsBeforeTransform2);
WaitForThreadTasks();
EXPECT_EQ(child_tree_update.tree_data.tree_id, ui::AXTreeIDUnknown());
PdfOcrRequest request(image_node->id(), image, root_node->id(),
paragraph_node->id(), page_node->id(),
/*page_index=*/0);
// Image pixel size is automatically set when OCR request is running, but
// this test skips that step.
request.image_pixel_size = gfx::SizeF(kBitmapWidth, kBitmapHeight);
// Reset `remaining_page_count_` to be zero. `remaining_page_count_` is later
// used in `OnOcrDataReceived()` to check whether OCR is done or not. Note
// that the OCR is considered to be done when `remaining_page_count_` == 0.
pdf_accessibility_tree_->ocr_helper_for_testing()
->ResetRemainingPageCountForTesting();
pdf_accessibility_tree_->OnOcrDataReceived(
std::vector<PdfOcrRequest>{{request}},
std::vector<ui::AXTreeUpdate>{child_tree_update});
WaitForThreadTasks();
/*
* Expected PDF accessibility tree structure (after running OCR)
* Document
* ++ Status
* ++ Region
* ++++ Paragraph
* ++++++ Region (child tree)
* ++++++++ Static Text
* ++++++++ Static Text
*/
root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/true,
/*is_ocr_completed=*/true,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
page_node = root_node->GetChildAtIndex(1);
ASSERT_TRUE(page_node);
EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
ASSERT_EQ(1u, page_node->GetChildCount());
paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kGenericContainer, paragraph_node->GetRole());
ASSERT_EQ(1u, paragraph_node->GetChildCount());
ui::AXNode* region_node = paragraph_node->GetChildAtIndex(0);
ASSERT_TRUE(region_node);
EXPECT_EQ(ax::mojom::Role::kRegion, region_node->GetRole());
ASSERT_EQ(2u, region_node->GetChildCount());
// Expected text bounds after applying the transform. These numbers are
// expected to be kTextBoundsBeforeTransform * 1 / kScaleFactor.
constexpr gfx::RectF kExpectedTextBoundRelativeToTreeBounds1 = {
{10.0f, 10.0f}, {100.0f, 30.0f}};
constexpr gfx::RectF kExpectedTextBoundRelativeToTreeBounds2 = {
{20.0f, 110.0f}, {50.0f, 70.0f}};
// Check the nodes from OCR results.
ui::AXNode* ocred_node = region_node->GetChildAtIndex(0);
ASSERT_TRUE(ocred_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, ocred_node->GetRole());
gfx::RectF bounds = ocred_node->data().relative_bounds.bounds;
// The bounds already got updated inside of OnOcrDataReceived().
CompareRect(kExpectedTextBoundRelativeToTreeBounds1, bounds);
ocred_node = region_node->GetChildAtIndex(1);
ASSERT_TRUE(ocred_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, ocred_node->GetRole());
bounds = ocred_node->data().relative_bounds.bounds;
// The bounds already got updated inside of OnOcrDataReceived().
CompareRect(kExpectedTextBoundRelativeToTreeBounds2, bounds);
}
TEST_F(PdfOcrTest, FeatureNotificationOnInaccessiblePdf) {
CreatePdfAccessibilityTree();
page_objects_.images.push_back(CreateMockInaccessibleImage());
// Get and use the underlying AXTree to create an AXEventGenerator. This
// event generator is usually instrumented in the test.
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
WaitForThreadTasks();
page_info_.page_index = 0;
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
const ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/true,
/*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
}
TEST_F(PdfOcrTest, NoFeatureNotificationOnAccessiblePdf) {
text_runs_.emplace_back(kFirstTextRun);
text_runs_.emplace_back(kSecondTextRun);
chars_.insert(chars_.end(), std::begin(kDummyCharsData),
std::end(kDummyCharsData));
page_info_.text_run_count = text_runs_.size();
page_info_.char_count = chars_.size();
CreatePdfAccessibilityTree();
pdf_accessibility_tree_->SetAccessibilityViewportInfo(viewport_info_);
pdf_accessibility_tree_->SetAccessibilityDocInfo(doc_info_);
pdf_accessibility_tree_->SetAccessibilityPageInfo(page_info_, text_runs_,
chars_, page_objects_);
WaitForThreadTasks();
// Wait for `PdfAccessibilityTree::UnserializeNodes()`, a delayed task.
WaitForThreadDelayedTasks();
const ui::AXNode* root_node = pdf_accessibility_tree_->GetRoot();
// `is_pdf_ocr_test` needs to be set to false below, as it shouldn't announce
// the PDF OCR feature notification in this case.
CheckRootAndStatusNodes(root_node, doc_info_.page_count,
/*is_pdf_ocr_test=*/false,
/*is_ocr_completed=*/false,
/*create_empty_ocr_results=*/false);
ASSERT_GT(root_node->GetChildCount(), 1u);
const ui::AXNode* page_node = root_node->GetChildAtIndex(1);
ASSERT_TRUE(page_node);
EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
ASSERT_EQ(2u, page_node->GetChildCount());
ui::AXNode* paragraph_node = page_node->GetChildAtIndex(0);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
EXPECT_TRUE(paragraph_node->GetBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject));
ASSERT_EQ(1u, paragraph_node->GetChildCount());
ui::AXNode* static_text_node = paragraph_node->GetChildAtIndex(0);
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
ASSERT_EQ(1u, static_text_node->GetChildCount());
paragraph_node = page_node->GetChildAtIndex(1);
ASSERT_TRUE(paragraph_node);
EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
EXPECT_TRUE(paragraph_node->GetBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject));
ASSERT_EQ(1u, paragraph_node->GetChildCount());
static_text_node = paragraph_node->GetChildAtIndex(0);
ASSERT_TRUE(static_text_node);
EXPECT_EQ(ax::mojom::Role::kStaticText, static_text_node->GetRole());
ASSERT_EQ(1u, static_text_node->GetChildCount());
}
#endif // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
} // namespace pdf