chromium/chrome/browser/accessibility/media_app/ax_media_app_untrusted_handler_browsertest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/accessibility/media_app/ax_media_app_untrusted_handler.h"

#include <stdint.h>

#include <memory>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/webui/media_app_ui/media_app_ui_untrusted.mojom.h"
#include "base/strings/escape.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "chrome/browser/accessibility/accessibility_state_utils.h"
#include "chrome/browser/accessibility/media_app/ax_media_app.h"
#include "chrome/browser/accessibility/media_app/ax_media_app_handler_factory.h"
#include "chrome/browser/accessibility/media_app/test/fake_ax_media_app.h"
#include "chrome/browser/accessibility/media_app/test/test_ax_media_app_untrusted_handler.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/accessibility_notification_waiter.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/scoped_accessibility_mode_override.h"
#include "content/public/test/test_web_ui.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/test_support/fake_message_dispatch_context.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "services/screen_ai/public/mojom/screen_ai_service.mojom.h"
#include "testing/gtest/include/gtest/gtest.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_node.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/accessibility/ax_tree_data.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/ax_tree_manager.h"
#include "ui/accessibility/ax_tree_serializer.h"
#include "ui/accessibility/platform/inspect/ax_inspect.h"
#include "ui/display/display_switches.h"
#include "ui/gfx/geometry/rect.h"
#include "url/gurl.h"

namespace ash::test {

using ash::media_app_ui::mojom::PageMetadataPtr;

namespace {

// Page coordinates are expressed as a `gfx::RectF`, so float values should be
// used.

// Gap or padding between pages.
constexpr float kTestPageGap = 2.0f;
constexpr float kTestPageWidth = 3.0f;
constexpr float kTestPageHeight = 8.0f;
// The test device pixel ratio.
constexpr float kTestDisplayPixelRatio = 1.5f;

// Use letters to generate fake IDs for fake page metadata. If more than
// 26 pages are needed, more characters can be added.
constexpr std::string_view kTestPageIds = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

constexpr std::string_view kLoadingMessage =
    "AXTree has_parent_tree title=PDF document\n"
    "id=1 pdfRoot FOCUSABLE clips_children child_ids=10000 (0, 0)-(0, 0) "
    "text_align=left restriction=readonly scroll_x_min=0 scroll_y_min=0 "
    "scrollable=true is_line_breaking_object=true\n"
    "  id=10000 banner <div> child_ids=10001 offset_container_id=1 (-1, "
    "-1)-(1, 1) text_align=left is_page_breaking_object=true "
    "is_line_breaking_object=true has_aria_attribute=true\n"
    "    id=10001 status <div> child_ids=10002 offset_container_id=10000 (0, "
    "0)-(1, 1) text_align=left container_relevant=additions text "
    "container_live=polite relevant=additions text live=polite "
    "container_atomic=true container_busy=false atomic=true "
    "is_line_breaking_object=true has_aria_attribute=true\n"
    "      id=10002 staticText name=This PDF is inaccessible. Extracting text, "
    "powered by Google AI child_ids=10003 offset_container_id=10001 (0, 0)-(1, "
    "1) text_align=left container_relevant=additions text "
    "container_live=polite relevant=additions text live=polite "
    "container_atomic=true container_busy=false atomic=true "
    "is_line_breaking_object=true\n"
    "        id=10003 inlineTextBox name=This PDF is inaccessible. Extracting "
    "text, powered by Google AI offset_container_id=10002 (0, 0)-(1, 1) "
    "text_align=left\n";

class AXMediaAppUntrustedHandlerTest : public InProcessBrowserTest {
 public:
  AXMediaAppUntrustedHandlerTest() {}
  AXMediaAppUntrustedHandlerTest(
      const AXMediaAppUntrustedHandlerTest&) = delete;
  AXMediaAppUntrustedHandlerTest& operator=(
      const AXMediaAppUntrustedHandlerTest&) = delete;
  ~AXMediaAppUntrustedHandlerTest() override = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    InProcessBrowserTest::SetUpCommandLine(command_line);
    command_line->AppendSwitchASCII(
        switches::kForceDeviceScaleFactor,
        base::NumberToString(kTestDisplayPixelRatio));
  }

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    ASSERT_NE(nullptr, AXMediaAppHandlerFactory::GetInstance());
    mojo::PendingRemote<ash::media_app_ui::mojom::OcrUntrustedPage> pageRemote;
    mojo::PendingReceiver<ash::media_app_ui::mojom::OcrUntrustedPage>
        pageReceiver = pageRemote.InitWithNewPipeAndPassReceiver();

    handler_ = std::make_unique<TestAXMediaAppUntrustedHandler>(
        *browser()->profile(), browser()->window()->GetNativeWindow(),
        std::move(pageRemote));
    ASSERT_NE(nullptr, handler_.get());
    // TODO(b/309860428): Delete MediaApp interface - after we implement all
    // Mojo APIs, it should not be needed any more.
    handler_->SetMediaAppForTesting(&fake_media_app_);
    handler_->SetIsOcrServiceEnabledForTesting();
    handler_->CreateFakeOpticalCharacterRecognizerForTesting(
        /*return_empty=*/false);
  }

  void TearDownOnMainThread() override {
    handler_.reset();
    InProcessBrowserTest::TearDownOnMainThread();
  }

 protected:
  // Create fake page metadata with pages of the same size positioned(
  // kTestPageWidth + kTestPageGap) unit spaced apart.
  std::vector<PageMetadataPtr> CreateFakePageMetadata(
      const uint64_t num_pages) const;
  std::vector<PageMetadataPtr> ClonePageMetadataPtrs(
      const std::vector<PageMetadataPtr>& metadata) const;
  void WaitForOcringPages(uint64_t number_of_pages) const;

  FakeAXMediaApp fake_media_app_;
  std::unique_ptr<TestAXMediaAppUntrustedHandler> handler_;
};

std::vector<PageMetadataPtr>
AXMediaAppUntrustedHandlerTest::CreateFakePageMetadata(
    const uint64_t num_pages) const {
  EXPECT_LE(static_cast<size_t>(num_pages), kTestPageIds.size())
      << "Can't make more than " << kTestPageIds.size() << " pages.";
  std::vector<PageMetadataPtr> fake_page_metadata;
  for (uint64_t i = 0; i < num_pages; ++i) {
    PageMetadataPtr page = ash::media_app_ui::mojom::PageMetadata::New();
    page->id = base::StringPrintf("Page%c", kTestPageIds[i]);
    page->rect =
        gfx::RectF(/*x=*/0.0f, /*y=*/kTestPageGap * i + kTestPageHeight * i,
                   kTestPageWidth, kTestPageHeight);
    fake_page_metadata.push_back(std::move(page));
  }
  return fake_page_metadata;
}

std::vector<PageMetadataPtr>
AXMediaAppUntrustedHandlerTest::ClonePageMetadataPtrs(
    const std::vector<PageMetadataPtr>& metadata) const {
  std::vector<PageMetadataPtr> fake_page_metadata;
  for (const PageMetadataPtr& page : metadata) {
    PageMetadataPtr cloned_page = mojo::Clone(page);
    fake_page_metadata.push_back(std::move(cloned_page));
  }
  return fake_page_metadata;
}

void AXMediaAppUntrustedHandlerTest::WaitForOcringPages(
    uint64_t number_of_pages) const {
  for (uint64_t i = 0; i < number_of_pages; ++i) {
    handler_->FlushForTesting();
  }
}

}  // namespace

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest, IsAccessibilityEnabled) {
  EXPECT_FALSE(handler_->IsAccessibilityEnabled());
  EXPECT_FALSE(fake_media_app_.IsAccessibilityEnabled());

  accessibility_state_utils::OverrideIsScreenReaderEnabledForTesting(true);
#if BUILDFLAG(IS_CHROMEOS_ASH)
  AccessibilityManager::Get()->EnableSpokenFeedback(true);
#else
  content::ScopedAccessibilityModeOverride scoped_mode(ui::kAXModeComplete);
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

  EXPECT_TRUE(handler_->IsAccessibilityEnabled());
  EXPECT_TRUE(fake_media_app_.IsAccessibilityEnabled());

  accessibility_state_utils::OverrideIsScreenReaderEnabledForTesting(false);
#if BUILDFLAG(IS_CHROMEOS_ASH)
  AccessibilityManager::Get()->EnableSpokenFeedback(false);
#else
  content::ScopedAccessibilityModeOverride scoped_mode(ui::kNone);
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

  EXPECT_FALSE(handler_->IsAccessibilityEnabled());
  EXPECT_FALSE(fake_media_app_.IsAccessibilityEnabled());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       OcrServiceInitializedFailed) {
  handler_->OnOCRServiceInitialized(/*successful*/ false);
  EXPECT_EQ(
      "AXTree has_parent_tree title=PDF document\n"
      "id=10000 banner <div> child_ids=10001 offset_container_id=1 (-1, "
      "-1)-(1, 1) text_align=left is_page_breaking_object=true "
      "is_line_breaking_object=true has_aria_attribute=true\n"
      "  id=10001 status <div> child_ids=10002 offset_container_id=10000 (0, "
      "0)-(1, 1) text_align=left container_relevant=additions text "
      "container_live=polite relevant=additions text live=polite "
      "container_atomic=true container_busy=false atomic=true "
      "is_line_breaking_object=true has_aria_attribute=true\n"
      "    id=10002 staticText name=This PDF is inaccessible. Couldn't "
      "download text extraction files. Please try again later. child_ids=10003 "
      "offset_container_id=10001 (0, 0)-(1, 1) text_align=left "
      "container_relevant=additions text container_live=polite "
      "relevant=additions text live=polite container_atomic=true "
      "container_busy=false atomic=true is_line_breaking_object=true\n"
      "      id=10003 inlineTextBox name=This PDF is inaccessible. Couldn't "
      "download text extraction files. Please try again later. "
      "offset_container_id=10002 (0, 0)-(1, 1) text_align=left\n",
      handler_->GetDocumentTreeToStringForTesting());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest, PageMetadataUpdated) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  const std::vector<std::string> kPageIds{"four", "ids", "in", "list"};
  const size_t kTestNumPages = kPageIds.size();
  constexpr gfx::RectF kRect(0, 0, 10, 15);
  std::vector<PageMetadataPtr> fake_metadata;
  for (size_t i = 0; i < kTestNumPages; ++i) {
    PageMetadataPtr page = ash::media_app_ui::mojom::PageMetadata::New();
    page->id = kPageIds[i];
    page->rect = kRect;
    fake_metadata.push_back(std::move(page));
  }
  handler_->PageMetadataUpdated(std::move(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  const std::map<const std::string, AXMediaAppPageMetadata>&
      actual_page_metadata = handler_->GetPageMetadataForTesting();
  ASSERT_EQ(actual_page_metadata.size(), kTestNumPages);
  for (size_t i = 0; i < kTestNumPages; ++i) {
    EXPECT_EQ(actual_page_metadata.at(kPageIds[i]).id, kPageIds[i]);
    EXPECT_EQ(actual_page_metadata.at(kPageIds[i]).page_num, i + 1u);
    EXPECT_EQ(actual_page_metadata.at(kPageIds[i]).rect, kRect);
  }

  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>& pages =
      handler_->GetPagesForTesting();
  ASSERT_EQ(kTestNumPages, pages.size());
  for (size_t i = 0; const auto& [page_id, page] : pages) {
    EXPECT_EQ(page_id, kPageIds[i++]);
    EXPECT_NE(nullptr, page.get());
    EXPECT_NE(nullptr, page->ax_tree());
  }
  // Note that the region nodes under the document root node have the (0,0)
  // offset. Each page will be correctly offset as the root node of its (child)
  // tree has a correct offset.
  EXPECT_EQ(
      "AXTree has_parent_tree title=PDF document\n"
      "id=1 pdfRoot FOCUSABLE name=PDF document containing 4 pages "
      "name_from=attribute clips_children child_ids=2,3,4,5 (0, 0)-(10, 15) "
      "text_align=left restriction=readonly scroll_x_min=0 scroll_y_min=0 "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=2 region name=Page 1 name_from=attribute has_child_tree (0, "
      "0)-(10, 15) restriction=readonly is_page_breaking_object=true\n"
      "  id=3 region name=Page 2 name_from=attribute has_child_tree (0, "
      "0)-(10, 15) restriction=readonly is_page_breaking_object=true\n"
      "  id=4 region name=Page 3 name_from=attribute has_child_tree (0, "
      "0)-(10, 15) restriction=readonly is_page_breaking_object=true\n"
      "  id=5 region name=Page 4 name_from=attribute has_child_tree (0, "
      "0)-(10, 15) restriction=readonly is_page_breaking_object=true\n",
      handler_->GetDocumentTreeToStringForTesting());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       CheckUMAMetricsForPageMetadataUpdated) {
  base::HistogramTester histograms;
  const size_t kTestNumPages = 3u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));

  histograms.ExpectBucketCount("Accessibility.PdfOcr.MediaApp.PdfLoaded", true,
                               /*expected_count=*/1);
  histograms.ExpectTotalCount("Accessibility.PdfOcr.MediaApp.PdfLoaded",
                              /*expected_count=*/1);
  WaitForOcringPages(1u);
  histograms.ExpectBucketCount("Accessibility.PdfOcr.MediaApp.PdfLoaded", true,
                               /*expected_count=*/1);
  histograms.ExpectTotalCount("Accessibility.PdfOcr.MediaApp.PdfLoaded",
                              /*expected_count=*/1);
  WaitForOcringPages(1u);
  histograms.ExpectBucketCount("Accessibility.PdfOcr.MediaApp.PdfLoaded", true,
                               /*expected_count=*/1);
  histograms.ExpectTotalCount("Accessibility.PdfOcr.MediaApp.PdfLoaded",
                              /*expected_count=*/1);
  WaitForOcringPages(1u);
  histograms.ExpectBucketCount("Accessibility.PdfOcr.MediaApp.PdfLoaded", true,
                               /*expected_count=*/1);
  histograms.ExpectTotalCount("Accessibility.PdfOcr.MediaApp.PdfLoaded",
                              /*expected_count=*/1);
  WaitForOcringPages(1u);
  histograms.ExpectBucketCount("Accessibility.PdfOcr.MediaApp.PdfLoaded", true,
                               /*expected_count=*/1);
  histograms.ExpectTotalCount("Accessibility.PdfOcr.MediaApp.PdfLoaded",
                              /*expected_count=*/1);

  // 'Rotate' the third page.
  fake_metadata[2]->rect.set_height(kTestPageWidth);
  fake_metadata[2]->rect.set_width(kTestPageHeight);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  handler_->PageContentsUpdated("PageC");
  WaitForOcringPages(1u);

  histograms.ExpectBucketCount("Accessibility.PdfOcr.MediaApp.PdfLoaded", true,
                               /*expected_count=*/1);
  histograms.ExpectTotalCount("Accessibility.PdfOcr.MediaApp.PdfLoaded",
                              /*expected_count=*/1);
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       CheckUMAMetricsForMostDetectedLanguageInOcrData) {
  base::HistogramTester histograms;
  constexpr size_t kTestNumPages = 3u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));

  histograms.ExpectTotalCount(
      "Accessibility.PdfOcr.MediaApp.MostDetectedLanguageInOcrData",
      /*expected_count=*/0);
  WaitForOcringPages(1u);
  histograms.ExpectTotalCount(
      "Accessibility.PdfOcr.MediaApp.MostDetectedLanguageInOcrData",
      /*expected_count=*/1);
  WaitForOcringPages(1u);
  histograms.ExpectTotalCount(
      "Accessibility.PdfOcr.MediaApp.MostDetectedLanguageInOcrData",
      /*expected_count=*/2);
  WaitForOcringPages(1u);
  histograms.ExpectTotalCount(
      "Accessibility.PdfOcr.MediaApp.MostDetectedLanguageInOcrData",
      /*expected_count=*/3);
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       PageMetadataUpdatedNoDuplicatePageIds) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  constexpr std::string kDuplicateId = "duplicate";
  std::vector<PageMetadataPtr> fake_metadata;
  PageMetadataPtr page1 = ash::media_app_ui::mojom::PageMetadata::New();
  page1->id = kDuplicateId;
  gfx::RectF rect(0, 0, 10, 15);
  page1->rect = rect;
  fake_metadata.push_back(std::move(page1));
  PageMetadataPtr page2 = ash::media_app_ui::mojom::PageMetadata::New();
  page2->id = kDuplicateId;
  page2->rect = rect;
  fake_metadata.push_back(std::move(page2));

  mojo::FakeMessageDispatchContext fake_dispatch_context;
  mojo::test::BadMessageObserver bad_message_observer;
  handler_->PageMetadataUpdated(std::move(fake_metadata));

  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(bad_message_observer.got_bad_message());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       PageMetadataUpdatedWithDeleteAndUndoDelete) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  // Note that the region nodes under the document root node have the (0,0)
  // offset. Each page will be correctly offset as the root node of its (child)
  // tree has a correct offset.
  const std::string kDocumentTree(
      "AXTree has_parent_tree title=PDF document\n"
      "id=1 pdfRoot FOCUSABLE name=PDF document containing 3 pages "
      "name_from=attribute clips_children child_ids=2,3,4 (0, 0)-(10, 15) "
      "text_align=left restriction=readonly scroll_x_min=0 scroll_y_min=0 "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=2 region name=Page 1 name_from=attribute has_child_tree (0, "
      "0)-(10, 15) restriction=readonly is_page_breaking_object=true\n"
      "  id=3 region name=Page 2 name_from=attribute has_child_tree (0, "
      "0)-(10, 15) restriction=readonly is_page_breaking_object=true\n"
      "  id=4 region name=Page 3 name_from=attribute has_child_tree (0, "
      "0)-(10, 15) restriction=readonly is_page_breaking_object=true\n");

  const std::string kDocumentTreeWithDeletedPage(
      "AXTree has_parent_tree title=PDF document\n"
      "id=1 pdfRoot FOCUSABLE name=PDF document containing 2 pages "
      "name_from=attribute clips_children child_ids=2,3 (0, 0)-(10, 15) "
      "text_align=left restriction=readonly scroll_x_min=0 scroll_y_min=0 "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=2 region name=Page 1 name_from=attribute has_child_tree (0, "
      "0)-(10, 15) restriction=readonly is_page_breaking_object=true\n"
      "  id=3 region name=Page 2 name_from=attribute has_child_tree (0, "
      "0)-(10, 15) restriction=readonly is_page_breaking_object=true\n");

  constexpr gfx::RectF kRect(0, 0, 10, 15);
  const std::vector<std::string> kPageIds{"pageX", "pageY", "pageZ"};
  const size_t kTestNumPages = kPageIds.size();
  std::vector<PageMetadataPtr> fake_metadata;
  for (size_t i = 0; i < kTestNumPages; ++i) {
    PageMetadataPtr page = ash::media_app_ui::mojom::PageMetadata::New();
    page->id = kPageIds[i];
    page->rect = kRect;
    fake_metadata.push_back(std::move(page));
  }
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  const std::map<const std::string, AXMediaAppPageMetadata>&
      page_metadata_before_deletion = handler_->GetPageMetadataForTesting();
  ASSERT_EQ(page_metadata_before_deletion.size(), kTestNumPages);
  for (size_t i = 1; i <= kTestNumPages; ++i) {
    EXPECT_EQ(page_metadata_before_deletion.at(kPageIds[i - 1]).page_num, i);
  }

  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>&
      pages_before_deletion = handler_->GetPagesForTesting();
  ASSERT_EQ(kTestNumPages, pages_before_deletion.size());
  for (size_t i = 0; const auto& [page_id, page] : pages_before_deletion) {
    EXPECT_EQ(page_id, kPageIds[i++]);
    EXPECT_NE(nullptr, page.get());
    EXPECT_NE(nullptr, page->ax_tree());
  }
  EXPECT_EQ(kDocumentTree, handler_->GetDocumentTreeToStringForTesting());

  // Delete "pageY" by excluding it from the metadata.
  std::vector<PageMetadataPtr> fake_metadataWithDeletedPage;
  for (size_t i = 0; i < kTestNumPages; ++i) {
    if (kPageIds[i] == "pageY") {
      continue;
    }
    PageMetadataPtr page = ash::media_app_ui::mojom::PageMetadata::New();
    page->id = kPageIds[i];
    page->rect = kRect;
    fake_metadataWithDeletedPage.push_back(std::move(page));
  }
  handler_->PageMetadataUpdated(std::move(fake_metadataWithDeletedPage));

  std::map<const std::string, AXMediaAppPageMetadata>&
      page_metadata_after_deletion = handler_->GetPageMetadataForTesting();
  ASSERT_EQ(page_metadata_after_deletion.size(), kTestNumPages);
  EXPECT_EQ(page_metadata_after_deletion.at("pageX").page_num, 1u);
  EXPECT_EQ(page_metadata_after_deletion.at("pageY").page_num, 0u);
  EXPECT_EQ(page_metadata_after_deletion.at("pageZ").page_num, 2u);

  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>&
      pages_after_deletion = handler_->GetPagesForTesting();
  ASSERT_EQ(kTestNumPages, pages_after_deletion.size());
  for (size_t i = 0; const auto& [page_id, page] : pages_after_deletion) {
    EXPECT_EQ(page_id, kPageIds[i++]);
    EXPECT_NE(nullptr, page.get());
    EXPECT_NE(nullptr, page->ax_tree());
  }
  EXPECT_EQ(kDocumentTreeWithDeletedPage,
            handler_->GetDocumentTreeToStringForTesting());

  // Add pageY back.
  handler_->PageMetadataUpdated(std::move(fake_metadata));

  const std::map<const std::string, AXMediaAppPageMetadata>&
      page_metadata_after_undo_deletion = handler_->GetPageMetadataForTesting();
  ASSERT_EQ(page_metadata_after_undo_deletion.size(), kTestNumPages);
  for (size_t i = 1; i <= kTestNumPages; ++i) {
    EXPECT_EQ(page_metadata_after_undo_deletion.at(kPageIds[i - 1]).page_num,
              i);
  }

  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>&
      pages_after_undo_deletion = handler_->GetPagesForTesting();
  ASSERT_EQ(kTestNumPages, pages_after_undo_deletion.size());
  for (size_t i = 0; const auto& [page_id, page] : pages_after_undo_deletion) {
    EXPECT_EQ(page_id, kPageIds[i++]);
    EXPECT_NE(nullptr, page.get());
    EXPECT_NE(nullptr, page->ax_tree());
  }
  EXPECT_EQ(kDocumentTree, handler_->GetDocumentTreeToStringForTesting());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       PageMetadataUpdatedWithNewPages) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  mojo::test::BadMessageObserver bad_message_observer;
  const std::vector<std::string> kPageIds{"pageX", "pageY"};
  const size_t kTestNumPages = kPageIds.size();
  std::vector<PageMetadataPtr> fake_metadata;
  for (size_t i = 0; i < kTestNumPages; ++i) {
    PageMetadataPtr page = ash::media_app_ui::mojom::PageMetadata::New();
    page->id = kPageIds[i];
    gfx::RectF rect(0, 0, 10, 15);
    page->rect = rect;
    fake_metadata.push_back(std::move(page));
  }

  handler_->PageMetadataUpdated(std::move(fake_metadata));

  std::map<const std::string, AXMediaAppPageMetadata>& actual_page_metadata =
      handler_->GetPageMetadataForTesting();
  EXPECT_EQ(actual_page_metadata.size(), kTestNumPages);

  // Add a page with a new ID.
  PageMetadataPtr page = ash::media_app_ui::mojom::PageMetadata::New();
  page->id = "pageZ";
  gfx::RectF rect(0, 0, 10, 15);
  page->rect = rect;
  fake_metadata.push_back(std::move(page));

  handler_->PageMetadataUpdated(std::move(fake_metadata));

  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(bad_message_observer.got_bad_message());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest, DirtyPageOcrOrder) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  mojo::test::BadMessageObserver bad_message_observer;
  const std::vector<std::string> kPageIds{"pageW", "pageX", "pageY", "pageZ"};
  const size_t kTestNumPages = kPageIds.size();
  std::vector<PageMetadataPtr> fake_metadata;
  for (size_t i = 0; i < kTestNumPages; ++i) {
    PageMetadataPtr page = ash::media_app_ui::mojom::PageMetadata::New();
    page->id = kPageIds[i];
    gfx::RectF rect(0, 0, 10, 15);
    page->rect = rect;
    fake_metadata.push_back(std::move(page));
  }
  handler_->SetDelayCallingOcrNextDirtyPage(true);

  handler_->PageMetadataUpdated(std::move(fake_metadata));

  // All pages should now be marked dirty, and OCRed in the order they were
  // added.
  EXPECT_EQ(handler_->PopDirtyPageForTesting(), "pageW");
  EXPECT_EQ(handler_->PopDirtyPageForTesting(), "pageX");
  EXPECT_EQ(handler_->PopDirtyPageForTesting(), "pageY");
  EXPECT_EQ(handler_->PopDirtyPageForTesting(), "pageZ");

  // Each time a page becomes dirty, it should be sent to the back of the queue.
  handler_->PushDirtyPageForTesting("pageX");
  handler_->PushDirtyPageForTesting("pageZ");
  handler_->PushDirtyPageForTesting("pageX");

  EXPECT_EQ(handler_->PopDirtyPageForTesting(), "pageZ");
  EXPECT_EQ(handler_->PopDirtyPageForTesting(), "pageX");
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       PageMetadataUpdatedPagesRelocated) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  const size_t kTestNumPages = 3u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  ASSERT_EQ(kTestNumPages, fake_media_app_.PageIdsWithBitmap().size());
  // Make sure the OCR service went through all the pages provided in the
  // earlier call to `PageMetadataUpdated()`, since on first load all pages are
  // dirty.
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[2]);

  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>& pages =
      handler_->GetPagesForTesting();
  ASSERT_EQ(kTestNumPages, pages.size());
  for (const auto& [_, page] : pages) {
    ASSERT_NE(nullptr, page.get());
    ASSERT_NE(nullptr, page->ax_tree());
  }

  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-2 staticText "
      "name=Testing (0, 0)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[0]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-3 staticText "
      "name=Testing (0, 10)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[1]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-4 staticText "
      "name=Testing (0, 20)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[2]->id)->ax_tree()->ToString());

  // Relocate all the pages 3 units to the left and resize the second page. This
  // is similar to a scenario that might happen if the second page was rotated.
  fake_metadata[0]->rect =
      gfx::RectF(/*x=*/-3, /*y=*/0,
                 /*width=*/kTestPageWidth, /*height=*/kTestPageHeight);
  fake_metadata[1]->rect = gfx::RectF(
      /*x=*/-3, /*y=*/10, /*width=*/kTestPageHeight, /*height=*/kTestPageWidth);
  fake_metadata[2]->rect =
      gfx::RectF(/*x=*/-3, /*y=*/15,
                 /*width=*/kTestPageWidth, /*height=*/kTestPageHeight);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));

  // Subsequent calls to PageMetadataUpdated() should not cause any page to be
  // marked as dirty.
  ASSERT_EQ(kTestNumPages, fake_media_app_.PageIdsWithBitmap().size());

  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>&
      pages2 = handler_->GetPagesForTesting();
  ASSERT_EQ(kTestNumPages, pages2.size());
  for (const auto& [_, page] : pages2) {
    ASSERT_NE(nullptr, page.get());
    ASSERT_NE(nullptr, page->ax_tree());
  }

  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-2 staticText "
      "name=Testing (-3, 0)-(3, 8) language=en-US\n",
      pages2.at(fake_metadata[0]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-3 staticText "
      "name=Testing (-3, 10)-(8, 3) language=en-US\n",
      pages2.at(fake_metadata[1]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-4 staticText "
      "name=Testing (-3, 15)-(3, 8) language=en-US\n",
      pages2.at(fake_metadata[2]->id)->ax_tree()->ToString());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       PageMetadataUpdatedPageHasNoOcrResults) {
  handler_->CreateFakeOpticalCharacterRecognizerForTesting(
      /*return_empty=*/true);
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  const size_t kTestNumPages = 2u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  ASSERT_EQ(kTestNumPages, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);

  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>& pages =
      handler_->GetPagesForTesting();
  ASSERT_EQ(kTestNumPages, pages.size());
  ASSERT_NE(nullptr, pages.at("PageA").get());
  ASSERT_NE(nullptr, pages.at("PageA")->ax_tree());
  EXPECT_EQ(
      "AXTree has_parent_tree\n"
      "id=1 paragraph child_ids=2 (0, 0)-(3, 8) is_line_breaking_object=true\n"
      "  id=2 image name=Unlabeled image name_from=attribute "
      "offset_container_id=1 (0, 0)-(3, 8) restriction=readonly\n",
      pages.at("PageA")->ax_tree()->ToString());
  ASSERT_NE(nullptr, pages.at("PageB").get());
  ASSERT_NE(nullptr, pages.at("PageB")->ax_tree());
  EXPECT_EQ(
      "AXTree has_parent_tree\n"
      "id=1 paragraph child_ids=2 (0, 10)-(3, 8) is_line_breaking_object=true\n"
      "  id=2 image name=Unlabeled image name_from=attribute "
      "offset_container_id=1 (0, 0)-(3, 8) restriction=readonly\n",
      pages.at("PageB")->ax_tree()->ToString());

  // Resize the second page.
  fake_metadata[1]->rect.set_size({kTestPageWidth + 1, kTestPageHeight + 1});
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));

  ASSERT_NE(nullptr, pages.at("PageB").get());
  ASSERT_NE(nullptr, pages.at("PageB")->ax_tree());
  EXPECT_EQ(
      "AXTree has_parent_tree\n"
      "id=1 paragraph child_ids=2 (0, 10)-(4, 9) is_line_breaking_object=true\n"
      "  id=2 image name=Unlabeled image name_from=attribute "
      "offset_container_id=1 (0, 0)-(4, 9) restriction=readonly\n",
      pages.at("PageB")->ax_tree()->ToString());

  EXPECT_EQ(
      "AXTree has_parent_tree title=PDF document\n"
      "id=1 pdfRoot FOCUSABLE name=PDF document containing 2 pages "
      "name_from=attribute clips_children child_ids=2,3 (0, 0)-(4, 19) "
      "text_align=left restriction=readonly scroll_x_min=0 scroll_y_min=0 "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=2 region name=Page 1 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n"
      "  id=3 region name=Page 2 name_from=attribute has_child_tree (0, 0)-(4, "
      "9) restriction=readonly is_page_breaking_object=true\n",
      handler_->GetDocumentTreeToStringForTesting());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       PageContentsUpdatedEdit) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  const size_t kTestNumPages = 3u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  // All pages must have gone through OCR.
  ASSERT_EQ(kTestNumPages, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[2]);

  // Mark the second page as dirty.
  handler_->PageContentsUpdated("PageB");
  WaitForOcringPages(1u);

  ASSERT_EQ(4u, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[2]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[3]);
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       PageMetadataUpdated_PageRotated) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  constexpr size_t kTestNumPages = 4u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  // All pages must have gone through OCR.
  ASSERT_EQ(kTestNumPages, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[2]);
  EXPECT_EQ("PageD", fake_media_app_.PageIdsWithBitmap()[3]);

  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>& pages =
      handler_->GetPagesForTesting();
  ASSERT_EQ(kTestNumPages, pages.size());
  for (const auto& [_, page] : pages) {
    ASSERT_NE(nullptr, page.get());
    ASSERT_NE(nullptr, page->ax_tree());
  }

  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-2 staticText "
      "name=Testing (0, 0)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[0]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-3 staticText "
      "name=Testing (0, 10)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[1]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-4 staticText "
      "name=Testing (0, 20)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[2]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-5 staticText "
      "name=Testing (0, 30)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[3]->id)->ax_tree()->ToString());

  // 'Rotate' the third page, moving the other pages to fit it.
  fake_metadata[2]->rect = gfx::RectF(
      /*x=*/fake_metadata[2]->rect.x(),
      /*y=*/fake_metadata[1]->rect.y() + kTestPageHeight + kTestPageGap,
      /*width=*/kTestPageHeight, /*height=*/kTestPageWidth);
  fake_metadata[3]->rect = gfx::RectF(
      /*x=*/0, /*y=*/fake_metadata[2]->rect.y() + kTestPageWidth + kTestPageGap,
      /*width=*/kTestPageWidth, /*height=*/kTestPageHeight);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  handler_->PageContentsUpdated("PageC");
  WaitForOcringPages(1u);

  ASSERT_EQ(5u, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[2]);
  EXPECT_EQ("PageD", fake_media_app_.PageIdsWithBitmap()[3]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[4]);

  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-2 staticText "
      "name=Testing (0, 0)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[0]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-3 staticText "
      "name=Testing (0, 10)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[1]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-6 staticText "
      "name=Testing (0, 20)-(8, 3) language=en-US\n",
      pages.at(fake_metadata[2]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-5 staticText "
      "name=Testing (0, 25)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[3]->id)->ax_tree()->ToString());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       PageMetadataUpdated_PageRotatedBeforeOcr) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  constexpr size_t kTestNumPages = 2u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(1u);

  // Only the first page must have gone through OCR.
  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>& pages =
      handler_->GetPagesForTesting();
  ASSERT_EQ(1u, pages.size());
  EXPECT_TRUE(pages.contains("PageA"));
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-2 staticText "
      "name=Testing (0, 0)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[0]->id)->ax_tree()->ToString());

  // 'Rotate' the first page, moving the second page as a result.
  fake_metadata[0]->rect = gfx::RectF(
      /*x=*/fake_metadata[0]->rect.x(),
      /*y=*/fake_metadata[0]->rect.y(),
      /*width=*/kTestPageHeight, /*height=*/kTestPageWidth);
  fake_metadata[1]->rect = gfx::RectF(
      /*x=*/fake_metadata[1]->rect.x(),
      /*y=*/fake_metadata[0]->rect.y() + kTestPageWidth + kTestPageGap,
      /*width=*/kTestPageWidth, /*height=*/kTestPageHeight);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  handler_->PageContentsUpdated("PageA");

  ASSERT_EQ(1u, pages.size());
  EXPECT_TRUE(pages.contains("PageA"));
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-2 staticText "
      "name=Testing (0, 0)-(8, 3) language=en-US\n",
      pages.at(fake_metadata[0]->id)->ax_tree()->ToString());

  // Rotate the second page as well.
  fake_metadata[1]->rect = gfx::RectF(
      /*x=*/fake_metadata[1]->rect.x(),
      /*y=*/fake_metadata[1]->rect.y(),
      /*width=*/kTestPageHeight, /*height=*/kTestPageWidth);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  handler_->PageContentsUpdated("PageB");

  ASSERT_EQ(1u, pages.size());
  EXPECT_TRUE(pages.contains("PageA"));

  WaitForOcringPages(1u);

  ASSERT_EQ(2u, pages.size());
  EXPECT_TRUE(pages.contains("PageA"));
  EXPECT_TRUE(pages.contains("PageB"));
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-4 staticText "
      "name=Testing (0, 0)-(8, 3) language=en-US\n",
      pages.at(fake_metadata[0]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-5 staticText "
      "name=Testing (0, 5)-(8, 3) language=en-US\n",
      pages.at(fake_metadata[1]->id)->ax_tree()->ToString());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       PageMetadataUpdated_PagesReordered) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  constexpr size_t kTestNumPages = 3u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  // All pages must have gone through OCR.
  ASSERT_EQ(kTestNumPages, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[2]);

  const std::map<const std::string, AXMediaAppPageMetadata>& page_metadata =
      handler_->GetPageMetadataForTesting();
  ASSERT_EQ(kTestNumPages, page_metadata.size());
  EXPECT_EQ(1u, page_metadata.at("PageA").page_num);
  EXPECT_EQ(2u, page_metadata.at("PageB").page_num);
  EXPECT_EQ(3u, page_metadata.at("PageC").page_num);

  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>& pages =
      handler_->GetPagesForTesting();
  EXPECT_EQ(kTestNumPages, pages.size());
  const ui::AXTreeID& child_tree_id_page_a =
      pages.at("PageA")->GetParentTreeID();
  const ui::AXTreeID& child_tree_id_page_c =
      pages.at("PageC")->GetParentTreeID();

  // 'Reorder' the pages by swapping the first with the third page. In a
  // non-test scenario only the page IDs would have been reordered, but here we
  // use the page location as a proxy to determine if the code works properly,
  // since the fake content is always the same.
  std::swap(fake_metadata.at(0u), fake_metadata.at(2u));
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));

  // No OCRing should have taken place, since the pages have only been
  // reordered, but not changed or rotated.
  ASSERT_EQ(kTestNumPages, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[2]);

  ASSERT_EQ(kTestNumPages, page_metadata.size());
  EXPECT_EQ(3u, page_metadata.at("PageA").page_num);
  EXPECT_EQ(2u, page_metadata.at("PageB").page_num);
  EXPECT_EQ(1u, page_metadata.at("PageC").page_num);

  ASSERT_EQ(kTestNumPages, pages.size());
  const ui::AXTreeID& new_child_tree_id_page_a =
      pages.at("PageA")->GetParentTreeID();
  const ui::AXTreeID& new_child_tree_id_page_c =
      pages.at("PageC")->GetParentTreeID();
  EXPECT_EQ(child_tree_id_page_a, new_child_tree_id_page_c);
  EXPECT_EQ(child_tree_id_page_c, new_child_tree_id_page_a);

  // We'll also use the locations of pages one and three as a proxy to determine
  // if their were in fact skipped.
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-2 staticText "
      "name=Testing (0, 0)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[2]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-3 staticText "
      "name=Testing (0, 10)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[1]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-4 staticText "
      "name=Testing (0, 20)-(3, 8) language=en-US\n",
      pages.at(fake_metadata[0]->id)->ax_tree()->ToString());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest, StitchDocumentTree) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  const char* html = R"HTML(
      <!DOCTYPE html>
      <html>
      <body>
        <canvas width="200" height="200">
          <p>Text that is not replaced by child tree.</p>
        </canvas>
        <div role="graphics-document" aria-label="graphics-document"
            width="200" height="200">
          <p>Text that is replaced by child tree.</p>
        </div>
      </body>
      </html>
      )HTML";

  content::AccessibilityNotificationWaiter load_waiter(
      browser()->tab_strip_model()->GetActiveWebContents(), ui::kAXModeComplete,
      ax::mojom::Event::kLoadComplete);
  GURL html_data_url("data:text/html," +
                     base::EscapeQueryParamValue(html, /*use_plus=*/false));
  ASSERT_NE(nullptr, ui_test_utils::NavigateToURL(browser(), html_data_url));
  ASSERT_TRUE(load_waiter.WaitForNotification());
  EXPECT_EQ(
      "rootWebArea htmlTag='#document'\n"
      "++genericContainer\n"
      "++++genericContainer\n"
      "++++++canvas htmlTag='canvas'\n"
      "++++++++staticText name='<newline>          '\n"
      "++++++++staticText name='Text that is not replaced by child tree.'\n"
      "++++++++staticText name='<newline>        '\n"
      "++++++graphicsDocument htmlTag='div' name='graphics-document'\n"
      "++++++++paragraph htmlTag='p'\n"
      "++++++++++staticText name='Text that is replaced by child tree.'\n"
      "++++++++++++inlineTextBox name='Text that is replaced by child tree.'\n",
      browser()
          ->tab_strip_model()
          ->GetActiveWebContents()
          ->DumpAccessibilityTree(
              /*internal=*/true,
              /*property_filters=*/{
                  ui::AXPropertyFilter("htmlTag", ui::AXPropertyFilter::ALLOW),
                  ui::AXPropertyFilter("name", ui::AXPropertyFilter::ALLOW)}));

  content::AccessibilityNotificationWaiter child_tree_added_waiter(
      browser()->tab_strip_model()->GetActiveWebContents(), ui::kAXModeComplete,
      ui::AXEventGenerator::Event::CHILDREN_CHANGED);
  const size_t kTestNumPages = 1u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);
  ASSERT_TRUE(child_tree_added_waiter.WaitForNotification());
  ASSERT_EQ(
      "rootWebArea htmlTag='#document'\n"
      "++genericContainer\n"
      "++++genericContainer\n"
      "++++++canvas htmlTag='canvas'\n"
      "++++++++staticText name='<newline>          '\n"
      "++++++++staticText name='Text that is not replaced by child tree.'\n"
      "++++++++staticText name='<newline>        '\n"
      "++++++graphicsDocument htmlTag='div' name='graphics-document'\n",
      browser()
          ->tab_strip_model()
          ->GetActiveWebContents()
          ->DumpAccessibilityTree(
              /*internal=*/true,
              /*property_filters=*/{
                  ui::AXPropertyFilter("htmlTag", ui::AXPropertyFilter::ALLOW),
                  ui::AXPropertyFilter("name", ui::AXPropertyFilter::ALLOW)}));

  const ui::AXNode* graphics_doc = browser()
                                       ->tab_strip_model()
                                       ->GetActiveWebContents()
                                       ->GetAccessibilityRootNode()
                                       ->GetFirstChild()
                                       ->GetFirstChild()
                                       ->GetLastChild();
  ASSERT_NE(nullptr, graphics_doc);
  EXPECT_NE("", graphics_doc->GetStringAttribute(
                    ax::mojom::StringAttribute::kChildTreeId));
  const ui::AXNode* pdf_root =
      graphics_doc->GetFirstUnignoredChildCrossingTreeBoundary();
  ASSERT_NE(nullptr, pdf_root);
  EXPECT_EQ(ax::mojom::Role::kPdfRoot, pdf_root->GetRole());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       SendAXTreeToAccessibilityService) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  handler_->SetMinPagesPerBatchForTesting(4u);
  handler_->EnablePendingSerializedUpdatesForTesting();
  constexpr size_t kTestNumPages = 3u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  // All pages must have gone through OCR.
  ASSERT_EQ(kTestNumPages, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[2]);

  const std::vector<ui::AXTreeUpdate>& pending_serialized_updates =
      handler_->GetPendingSerializedUpdatesForTesting();
  // Three updates, one for each page, plus one update for the document that
  // contains them.
  ASSERT_EQ(kTestNumPages + 1u, pending_serialized_updates.size());
  EXPECT_EQ(
      "AXTreeUpdate tree data:\n"
      "AXTreeUpdate: root id -2\n"
      "id=-2 staticText name=Testing (0, 0)-(3, 8) language=en-US\n",
      pending_serialized_updates[0].ToString());
  EXPECT_EQ(
      "AXTreeUpdate tree data:\n"
      "AXTreeUpdate: root id -3\n"
      "id=-3 staticText name=Testing (0, 10)-(3, 8) language=en-US\n",
      pending_serialized_updates[1].ToString());
  EXPECT_EQ(
      "AXTreeUpdate tree data:\n"
      "AXTreeUpdate: root id -4\n"
      "id=-4 staticText name=Testing (0, 20)-(3, 8) language=en-US\n",
      pending_serialized_updates[2].ToString());
  // Note that the region nodes under the document root node have the (0,0)
  // offset. Each page will be correctly offset as the root node of its (child)
  // tree has a correct offset.
  EXPECT_EQ(
      "AXTreeUpdate tree data:\nAXTreeUpdate: root id 1\n"
      "id=1 pdfRoot FOCUSABLE name=PDF document containing 3 pages "
      "name_from=attribute clips_children child_ids=2,3,4 (0, 0)-(3, 28) "
      "text_align=left restriction=readonly scroll_x_min=0 scroll_y_min=0 "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=2 region name=Page 1 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n"
      "  id=3 region name=Page 2 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n"
      "  id=4 region name=Page 3 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n",
      pending_serialized_updates[3].ToString());

  // Rotate the second page. It should update the location of all pages.
  fake_metadata[1]->rect =
      gfx::RectF(/*x=*/0.0f, kTestPageHeight + kTestPageGap, kTestPageHeight,
                 kTestPageWidth);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  handler_->PageContentsUpdated("PageB");
  WaitForOcringPages(1u);

  // Only the second page must have gone through OCR, but all the pages must
  // have had their location updated.
  ASSERT_EQ(kTestNumPages + 1u, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap().back());

  // For the location changes: Three updates for changing the location of three
  // pages, plus one for the document that contains them.
  //
  // For the rotated page: One update for deleting the rotated page, plus one
  // update for the document.
  ASSERT_EQ(kTestNumPages * 2u + 4u, pending_serialized_updates.size());
  EXPECT_EQ(
      "AXTreeUpdate: root id -2\n"
      "id=-2 staticText name=Testing (0, 0)-(3, 8) language=en-US\n",
      pending_serialized_updates[4].ToString());
  EXPECT_EQ(
      "AXTreeUpdate: root id -3\n"
      "id=-3 staticText name=Testing (0, 10)-(8, 3) language=en-US\n",
      pending_serialized_updates[5].ToString());
  EXPECT_EQ(
      "AXTreeUpdate: root id -4\n"
      "id=-4 staticText name=Testing (0, 20)-(3, 8) language=en-US\n",
      pending_serialized_updates[6].ToString());
  EXPECT_EQ(
      "AXTreeUpdate: root id 1\n"
      "id=1 pdfRoot FOCUSABLE name=PDF document containing 3 pages "
      "name_from=attribute clips_children child_ids=2,3,4 (0, 0)-(8, 28) "
      "text_align=left restriction=readonly scroll_x_min=0 scroll_y_min=0 "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=2 region name=Page 1 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n"
      "  id=3 region name=Page 2 name_from=attribute has_child_tree (0, 0)-(8, "
      "3) restriction=readonly is_page_breaking_object=true\n"
      "  id=4 region name=Page 3 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n",
      pending_serialized_updates[7].ToString());
  EXPECT_EQ(
      "AXTreeUpdate tree data:\n"
      "AXTreeUpdate: clear node -3\n"
      "AXTreeUpdate: root id -5\n"
      "id=-5 staticText name=Testing (0, 10)-(8, 3) language=en-US\n",
      pending_serialized_updates[8].ToString());
  EXPECT_EQ(
      "AXTreeUpdate: root id 1\n"
      "id=1 pdfRoot FOCUSABLE name=PDF document containing 3 pages "
      "name_from=attribute clips_children child_ids=2,3,4 (0, 0)-(8, 28) "
      "text_align=left restriction=readonly scroll_x_min=0 scroll_y_min=0 "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=2 region name=Page 1 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n"
      "  id=3 region name=Page 2 name_from=attribute has_child_tree (0, 0)-(8, "
      "3) restriction=readonly is_page_breaking_object=true\n"
      "  id=4 region name=Page 3 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n",
      pending_serialized_updates[9].ToString());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest, ScrollUpAndDown) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  constexpr size_t kTestNumPages = 3u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  // All pages must have gone through OCR.
  ASSERT_EQ(kTestNumPages, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[2]);

  // View the second page by scrolling to it.
  handler_->ViewportUpdated(
      gfx::RectF(/*x=*/0.0f, /*y=*/kTestPageHeight + kTestPageGap,
                 kTestPageWidth, kTestPageHeight),
      /*scale_factor=*/1.0f);

  ui::AXActionData scroll_action_data;
  scroll_action_data.action = ax::mojom::Action::kScrollUp;
  scroll_action_data.target_tree_id = handler_->GetDocumentTreeIDForTesting();
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(/*x=*/0.0f, /*y=*/kTestPageGap, kTestPageWidth,
                       kTestPageHeight),
            fake_media_app_.ViewportBox());

  // Scroll up again, which should only scroll to the top of the document, i.e.
  // viewport should not get a negative y value.
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(
      gfx::RectF(/*x =*/0.0f, /*y=*/0.0f, kTestPageWidth, kTestPageHeight),
      fake_media_app_.ViewportBox());

  // View the second page again by scrolling to it.
  handler_->ViewportUpdated(
      gfx::RectF(/*x=*/0.0f, /*y=*/kTestPageHeight + kTestPageGap,
                 kTestPageWidth, kTestPageHeight),
      /*scale_factor=*/1.0f);

  scroll_action_data.action = ax::mojom::Action::kScrollDown;
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(/*x=*/0.0f, /*y=*/kTestPageGap + kTestPageHeight * 2.0f,
                       kTestPageWidth, kTestPageHeight),
            fake_media_app_.ViewportBox());

  // Scroll down again, which should only scroll to the bottom of the document
  // but not further.
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(
      gfx::RectF(/*x=*/0.0f, /*y=*/(kTestPageGap + kTestPageHeight) * 2.0f,
                 kTestPageWidth, kTestPageHeight),
      fake_media_app_.ViewportBox());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest, ScrollLeftAndRight) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  constexpr float kTestViewportWidth = kTestPageWidth / 3.0f;
  constexpr float kTestViewportHeight = kTestPageHeight;
  constexpr size_t kTestNumPages = 3u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  // All pages must have gone through OCR.
  ASSERT_EQ(kTestNumPages, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[2]);

  // View the center part of the second page by scrolling to it.
  handler_->ViewportUpdated(gfx::RectF(/*x=*/kTestViewportWidth,
                                       /*y=*/kTestPageHeight + kTestPageGap,
                                       kTestViewportWidth, kTestViewportHeight),
                            /*scale_factor=*/1.0f);

  ui::AXActionData scroll_action_data;
  scroll_action_data.action = ax::mojom::Action::kScrollLeft;
  scroll_action_data.target_tree_id = handler_->GetDocumentTreeIDForTesting();
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(/*x=*/0.0f, /*y=*/kTestPageHeight + kTestPageGap,
                       kTestViewportWidth, kTestViewportHeight),
            fake_media_app_.ViewportBox());

  // No scrolling should happen because we are already at the leftmost position
  // of the second page.
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(/*x=*/0.0f, /*y=*/kTestPageHeight + kTestPageGap,
                       kTestViewportWidth, kTestViewportHeight),
            fake_media_app_.ViewportBox());

  // View the rightmost part of the second page again by scrolling to it.
  handler_->ViewportUpdated(gfx::RectF(/*x=*/kTestViewportWidth * 2.0f,
                                       /*y=*/kTestViewportHeight + kTestPageGap,
                                       kTestViewportWidth, kTestViewportHeight),
                            /*scale_factor=*/1.0f);

  scroll_action_data.action = ax::mojom::Action::kScrollRight;
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(/*x=*/kTestPageWidth - kTestViewportWidth,
                       /*y=*/kTestViewportHeight + kTestPageGap,
                       kTestViewportWidth, kTestViewportHeight),
            fake_media_app_.ViewportBox());

  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(/*x=*/kTestPageWidth - 1.0f,
                       /*y=*/kTestViewportHeight + kTestPageGap,
                       kTestViewportWidth, kTestViewportHeight),
            fake_media_app_.ViewportBox());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest, ScrollToMakeVisible) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  constexpr float kPageX = 0.0f;
  constexpr float kPageY = 0.0f;
  constexpr float kViewportWidth = 2.0f;
  constexpr float kViewportHeight = 4.0f;
  std::vector<PageMetadataPtr> fake_metadata;
  PageMetadataPtr fake_page1 = ash::media_app_ui::mojom::PageMetadata::New();
  fake_page1->id = base::StringPrintf("Page%c", kTestPageIds[0]);
  fake_page1->rect = gfx::RectF(/*x=*/kPageX,
                                /*y=*/kPageY, kTestPageWidth, kTestPageHeight);
  fake_metadata.push_back(std::move(fake_page1));
  PageMetadataPtr fake_page2 = ash::media_app_ui::mojom::PageMetadata::New();
  fake_page2->id = base::StringPrintf("Page%c", kTestPageIds[1]);
  fake_page2->rect =
      gfx::RectF(/*x=*/kPageX + 20.0f,
                 /*y=*/kPageY + 20.0f, kTestPageWidth, kTestPageHeight);
  fake_metadata.push_back(std::move(fake_page2));
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(2u);

  // All pages must have gone through OCR.
  ASSERT_EQ(2u, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);

  ui::AXActionData scroll_action_data;
  scroll_action_data.action = ax::mojom::Action::kScrollToMakeVisible;
  scroll_action_data.target_tree_id =
      handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetTreeID();
  ASSERT_NE(nullptr,
            handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetRoot());
  scroll_action_data.target_node_id =
      handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetRoot()->id();

  // "Scroll to make visible" the target node, which should scroll forward.
  handler_->ViewportUpdated(
      gfx::RectF(/*x=*/0.0f, /*y=*/0.0f, kViewportWidth, kViewportHeight),
      /*scale_factor=*/1.0f);
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(/*x=*/kPageX + kTestPageWidth - kViewportWidth,
                       /*y=*/kPageY + kTestPageHeight - kViewportHeight,
                       kViewportWidth, kViewportHeight),
            fake_media_app_.ViewportBox());
  handler_->ViewportUpdated(
      gfx::RectF(/*x=*/0.0f, /*y=*/kPageY, kViewportWidth, kViewportHeight),
      /*scale_factor=*/1.0f);
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(/*x=*/kPageX + kTestPageWidth - kViewportWidth,
                       /*y=*/kPageY + kTestPageHeight - kViewportHeight,
                       kViewportWidth, kViewportHeight),
            fake_media_app_.ViewportBox());

  // "Scroll to make visible" the target node, which should scroll backward.
  handler_->ViewportUpdated(gfx::RectF(/*x=*/kPageX + kTestPageWidth - 1.0f,
                                       /*y=*/kPageY + kTestPageHeight - 1.0f,
                                       kViewportWidth, kViewportHeight),
                            /*scale_factor=*/1.0f);
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(kPageX, kPageY, kViewportWidth, kViewportHeight),
            fake_media_app_.ViewportBox());
  handler_->ViewportUpdated(
      gfx::RectF(/*x=*/kPageX + kTestPageWidth, /*y=*/kPageY + kTestPageHeight,
                 kViewportWidth, kViewportHeight),
      /*scale_factor=*/1.0f);
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(kPageX, kPageY, kViewportWidth, kViewportHeight),
            fake_media_app_.ViewportBox());

  // No scrolling should be needed because page can fit into viewport.
  handler_->ViewportUpdated(
      gfx::RectF(kPageX, kPageY, kTestPageWidth, kTestPageHeight),
      /*scale_factor=*/1.0f);
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(kPageX, kPageY, kTestPageWidth, kTestPageHeight),
            fake_media_app_.ViewportBox());

  // Viewport can only display part of the page; so "scroll to make visible"
  // should only scroll to the top-left corner.
  handler_->ViewportUpdated(
      gfx::RectF(/*x=*/kPageX + kTestPageWidth - kViewportWidth,
                 /*y=*/kPageY + kTestPageHeight - kViewportHeight,
                 kViewportWidth, kViewportHeight),
      /*scale_factor=*/1.0f);
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(kPageX, kPageY, kViewportWidth, kViewportHeight),
            fake_media_app_.ViewportBox());

  // View the second page.
  scroll_action_data.target_tree_id =
      handler_->GetPagesForTesting().at(fake_metadata[1]->id)->GetTreeID();
  ASSERT_NE(nullptr,
            handler_->GetPagesForTesting().at(fake_metadata[1]->id)->GetRoot());
  scroll_action_data.target_node_id =
      handler_->GetPagesForTesting().at(fake_metadata[1]->id)->GetRoot()->id();

  handler_->ViewportUpdated(
      gfx::RectF(/*x=*/0.0f, /*y=*/0.0f, kViewportWidth, kViewportHeight),
      /*scale_factor=*/1.0f);
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(/*x=*/kPageX + 20.0f + kTestPageWidth - kViewportWidth,
                       /*y=*/kPageY + 20.0f + kTestPageHeight - kViewportHeight,
                       kViewportWidth, kViewportHeight),
            fake_media_app_.ViewportBox());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       ScrollToMakeVisiblePagesReordered) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  constexpr size_t kTestNumPages = 2u;
  constexpr float kViewportWidth = 2.0f;
  constexpr float kViewportHeight = 4.0f;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  // All pages must have gone through OCR.
  ASSERT_EQ(kTestNumPages, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);

  ui::AXActionData scroll_action_data;
  scroll_action_data.action = ax::mojom::Action::kScrollToMakeVisible;
  ASSERT_EQ(kTestNumPages, handler_->GetPagesForTesting().size());
  scroll_action_data.target_tree_id =
      handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetTreeID();
  ASSERT_NE(nullptr,
            handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetRoot());
  scroll_action_data.target_node_id =
      handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetRoot()->id();

  // "Scroll to make visible" the target node, which should scroll forward.
  handler_->ViewportUpdated(
      gfx::RectF(/*x=*/0.0f, /*y=*/0.0f, kViewportWidth, kViewportHeight),
      /*scale_factor=*/1.0f);
  handler_->PerformAction(scroll_action_data);
  EXPECT_EQ(gfx::RectF(/*x=*/kTestPageWidth - kViewportWidth,
                       /*y=*/kTestPageHeight - kViewportHeight, kViewportWidth,
                       kViewportHeight),
            fake_media_app_.ViewportBox());

  // Reorder the pages by swapping their IDs.
  std::swap(fake_metadata.at(0u)->id, fake_metadata.at(1u)->id);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));

  // The result should change since "PageA" has moved."
  handler_->ViewportUpdated(
      gfx::RectF(/*x=*/0.0f, /*y=*/0.0f, kViewportWidth, kViewportHeight),
      /*scale_factor=*/1.0f);
  handler_->PerformAction(scroll_action_data);
  // The viewport should move all the way to the bottom-right corner of page
  // two.
  EXPECT_EQ(
      gfx::RectF(/*x=*/kTestPageWidth - kViewportWidth,
                 /*y=*/kTestPageHeight * 2 + kTestPageGap - kViewportHeight,
                 kViewportWidth, kViewportHeight),
      fake_media_app_.ViewportBox());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       CheckActiveTimeWithMultipleScrollToMakeVisibleActions) {
  base::HistogramTester histograms;
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  const size_t kTestNumPages = 2u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  // No metric has been recorded at this moment.
  histograms.ExpectTotalCount("Accessibility.PdfOcr.MediaApp.ActiveTime",
                              /*expected_count=*/0);

  ui::AXActionData first_scroll_action_data;
  first_scroll_action_data.action = ax::mojom::Action::kScrollToMakeVisible;
  first_scroll_action_data.target_tree_id =
      handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetTreeID();
  ASSERT_NE(nullptr,
            handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetRoot());
  first_scroll_action_data.target_node_id =
      handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetRoot()->id();
  // "Scroll to make visible" the target node.
  handler_->PerformAction(first_scroll_action_data);

  ui::AXActionData second_scroll_action_data;
  second_scroll_action_data.action = ax::mojom::Action::kScrollToMakeVisible;
  second_scroll_action_data.target_tree_id =
      handler_->GetPagesForTesting().at(fake_metadata[1]->id)->GetTreeID();
  ASSERT_NE(nullptr,
            handler_->GetPagesForTesting().at(fake_metadata[1]->id)->GetRoot());
  second_scroll_action_data.target_node_id =
      handler_->GetPagesForTesting().at(fake_metadata[1]->id)->GetRoot()->id();
  // "Scroll to make visible" the target node.
  handler_->PerformAction(second_scroll_action_data);

  // Destroying handler will trigger recording the metric.
  handler_.reset();

  // There must be one bucket being recorded at this moment.
  histograms.ExpectTotalCount("Accessibility.PdfOcr.MediaApp.ActiveTime",
                              /*expected_count=*/1);
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       CheckNoActiveTimeWithSingleScrollToMakeVisibleAction) {
  base::HistogramTester histograms;
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  const size_t kTestNumPages = 1u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  // No metric has been recorded at this moment.
  histograms.ExpectTotalCount("Accessibility.PdfOcr.MediaApp.ActiveTime",
                              /*expected_count=*/0);

  ui::AXActionData scroll_action_data;
  scroll_action_data.action = ax::mojom::Action::kScrollToMakeVisible;
  scroll_action_data.target_tree_id =
      handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetTreeID();
  ASSERT_NE(nullptr,
            handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetRoot());
  scroll_action_data.target_node_id =
      handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetRoot()->id();
  // "Scroll to make visible" the target node, which should scroll forward.
  handler_->PerformAction(scroll_action_data);

  // Destroying handler will trigger recording the metric.
  handler_.reset();

  // Nothing has been recorded yet as the active time expects at least two
  // ScrollToMakeVisible actions to happen for recording.
  histograms.ExpectTotalCount("Accessibility.PdfOcr.MediaApp.ActiveTime",
                              /*expected_count=*/0);
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       CheckReadingProgression100Percent) {
  base::HistogramTester histograms;
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  const size_t kTestNumPages = 1u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  // No metric has been recorded at this moment.
  histograms.ExpectTotalCount(
      "Accessibility.PdfOcr.MediaApp.PercentageReadingProgression",
      /*expected_count=*/0);

  ui::AXActionData scroll_action_data;
  scroll_action_data.action = ax::mojom::Action::kScrollToMakeVisible;
  scroll_action_data.target_tree_id =
      handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetTreeID();
  ASSERT_NE(nullptr,
            handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetRoot());
  scroll_action_data.target_node_id =
      handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetRoot()->id();
  // "Scroll to make visible" the target node, which should scroll forward.
  handler_->PerformAction(scroll_action_data);

  // Destroying handler will trigger recording the metric.
  handler_.reset();

  histograms.ExpectUniqueSample(
      "Accessibility.PdfOcr.MediaApp.PercentageReadingProgression", 100, 1);
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       CheckReadingProgression50Percent) {
  base::HistogramTester histograms;
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  const size_t kTestNumPages = 2u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  // No metric has been recorded at this moment.
  histograms.ExpectTotalCount(
      "Accessibility.PdfOcr.MediaApp.PercentageReadingProgression",
      /*expected_count=*/0);

  ui::AXActionData scroll_action_data;
  scroll_action_data.action = ax::mojom::Action::kScrollToMakeVisible;
  scroll_action_data.target_tree_id =
      handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetTreeID();
  ASSERT_NE(nullptr,
            handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetRoot());
  scroll_action_data.target_node_id =
      handler_->GetPagesForTesting().at(fake_metadata[0]->id)->GetRoot()->id();
  // "Scroll to make visible" the target node, which should scroll forward to
  // the first page.
  handler_->PerformAction(scroll_action_data);

  // Destroying handler will trigger recording the metric.
  handler_.reset();

  // Out of two pages, the first page was visited, so 50% reading progression.
  histograms.ExpectUniqueSample(
      "Accessibility.PdfOcr.MediaApp.PercentageReadingProgression", 50, 1);
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       CheckReadingProgression0Percent) {
  base::HistogramTester histograms;
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  const size_t kTestNumPages = 1u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  // No metric has been recorded at this moment.
  histograms.ExpectTotalCount(
      "Accessibility.PdfOcr.MediaApp.PercentageReadingProgression",
      /*expected_count=*/0);

  // Destroying handler will trigger recording the metric.
  handler_.reset();

  histograms.ExpectUniqueSample(
      "Accessibility.PdfOcr.MediaApp.PercentageReadingProgression", 0, 1);
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest, PageBatching) {
  handler_->DisableStatusNodesForTesting();
  handler_->DisablePostamblePageForTesting();
  const size_t kTestNumPages = 4u;
  handler_->SetMinPagesPerBatchForTesting(2u);
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(1u);

  // The bitmap for the second page has been retrieved but the page hasn't gone
  // through OCR yet.
  ASSERT_EQ(2u, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);

  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>&
      pages1 = handler_->GetPagesForTesting();
  ASSERT_EQ(1u, pages1.size());
  for (const auto& [_, page] : pages1) {
    ASSERT_NE(nullptr, page.get());
    ASSERT_NE(nullptr, page->ax_tree());
  }

  EXPECT_EQ("", handler_->GetDocumentTreeToStringForTesting());

  WaitForOcringPages(2u);

  // The bitmap for the fourth page has been retrieved but it hasn't gone
  // through OCR yet.
  ASSERT_EQ(4u, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[2]);
  EXPECT_EQ("PageD", fake_media_app_.PageIdsWithBitmap()[3]);

  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>&
      pages2 = handler_->GetPagesForTesting();
  ASSERT_EQ(3u, pages2.size());
  for (const auto& [_, page] : pages2) {
    ASSERT_NE(nullptr, page.get());
    ASSERT_NE(nullptr, page->ax_tree());
  }

  // Only two pages should be in the document because the batch is of size two.
  // Note that the region nodes under the document root node have the (0,0)
  // offset. Each page will be correctly offset as the root node of its (child)
  // tree has a correct offset.
  EXPECT_EQ(
      "AXTree has_parent_tree title=PDF document\n"
      "id=1 pdfRoot FOCUSABLE name=PDF document containing 2 pages "
      "name_from=attribute clips_children child_ids=2,3 (0, 0)-(3, 18) "
      "text_align=left restriction=readonly scroll_x_min=0 scroll_y_min=0 "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=2 region name=Page 1 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n"
      "  id=3 region name=Page 2 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n",
      handler_->GetDocumentTreeToStringForTesting());

  WaitForOcringPages(1u);

  ASSERT_EQ(kTestNumPages, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[2]);
  EXPECT_EQ("PageD", fake_media_app_.PageIdsWithBitmap()[3]);

  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>&
      pages3 = handler_->GetPagesForTesting();
  ASSERT_EQ(kTestNumPages, pages3.size());
  for (const auto& [_, page] : pages3) {
    ASSERT_NE(nullptr, page.get());
    ASSERT_NE(nullptr, page->ax_tree());
  }

  EXPECT_EQ(
      "AXTree has_parent_tree title=PDF document\n"
      "id=1 pdfRoot FOCUSABLE name=PDF document containing 4 pages "
      "name_from=attribute clips_children child_ids=2,3,4,5 (0, 0)-(3, 38) "
      "text_align=left restriction=readonly scroll_x_min=0 scroll_y_min=0 "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=2 region name=Page 1 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n"
      "  id=3 region name=Page 2 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n"
      "  id=4 region name=Page 3 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n"
      "  id=5 region name=Page 4 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n",
      handler_->GetDocumentTreeToStringForTesting());

  fake_metadata.at(1)->rect =
      gfx::RectF(/*x=*/1, /*y=*/2, /*width=*/3, /*height=*/4);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  handler_->PageContentsUpdated("PageB");
  WaitForOcringPages(1u);

  ASSERT_EQ(kTestNumPages + 1u, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[1]);
  EXPECT_EQ("PageC", fake_media_app_.PageIdsWithBitmap()[2]);
  EXPECT_EQ("PageD", fake_media_app_.PageIdsWithBitmap()[3]);
  EXPECT_EQ("PageB", fake_media_app_.PageIdsWithBitmap()[4]);

  const std::map<const std::string, std::unique_ptr<ui::AXTreeManager>>&
      pages4 = handler_->GetPagesForTesting();
  ASSERT_EQ(kTestNumPages, pages4.size());
  for (const auto& [_, page] : pages4) {
    ASSERT_NE(nullptr, page.get());
    ASSERT_NE(nullptr, page->ax_tree());
  }
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-2 staticText "
      "name=Testing (0, 0)-(3, 8) language=en-US\n",
      pages4.at(fake_metadata[0]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-6 staticText "
      "name=Testing (1, 2)-(3, 4) language=en-US\n",
      pages4.at(fake_metadata[1]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-4 staticText "
      "name=Testing (0, 20)-(3, 8) language=en-US\n",
      pages4.at(fake_metadata[2]->id)->ax_tree()->ToString());
  EXPECT_EQ(
      "AXTree has_parent_tree title=Screen AI\nid=-5 staticText "
      "name=Testing (0, 30)-(3, 8) language=en-US\n",
      pages4.at(fake_metadata[3]->id)->ax_tree()->ToString());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest, StatusNodes) {
  handler_->DisablePostamblePageForTesting();
  const size_t kTestNumPages = 2u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));

  EXPECT_EQ(kLoadingMessage, handler_->GetDocumentTreeToStringForTesting());
  WaitForOcringPages(1u);
  EXPECT_EQ(kLoadingMessage, handler_->GetDocumentTreeToStringForTesting());
  WaitForOcringPages(1u);
  // Note that the region nodes under the document root node have the (0,0)
  // offset. Each page will be correctly offset as the root node of its (child)
  // tree has a correct offset.
  EXPECT_EQ(
      "AXTree has_parent_tree title=PDF document\n"
      "id=1 pdfRoot FOCUSABLE name=PDF document containing 2 pages "
      "name_from=attribute clips_children child_ids=10000,2,3 (0, 0)-(3, 18) "
      "scroll_x_min=0 scroll_y_min=0 restriction=readonly text_align=left "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=10000 banner <div> child_ids=10001 offset_container_id=1 (-1, "
      "-1)-(1, 1) text_align=left is_page_breaking_object=true "
      "is_line_breaking_object=true has_aria_attribute=true\n"
      "    id=10001 status <div> child_ids=10002 offset_container_id=10000 (0, "
      "0)-(1, 1) text_align=left container_relevant=additions text "
      "container_live=polite relevant=additions text live=polite "
      "container_atomic=true container_busy=false atomic=true "
      "is_line_breaking_object=true has_aria_attribute=true\n"
      "      id=10002 staticText name=This PDF is inaccessible. Text "
      "extracted, powered by Google AI child_ids=10003 "
      "offset_container_id=10001 (0, 0)-(1, 1) text_align=left "
      "container_relevant=additions text container_live=polite "
      "relevant=additions text live=polite container_atomic=true "
      "container_busy=false atomic=true is_line_breaking_object=true\n"
      "        id=10003 inlineTextBox name=This PDF is inaccessible. Text "
      "extracted, powered by Google AI offset_container_id=10002 (0, 0)-(1, 1) "
      "text_align=left\n"
      "  id=2 region name=Page 1 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n"
      "  id=3 region name=Page 2 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n",
      handler_->GetDocumentTreeToStringForTesting());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       StatusNodesNoTextExtracted) {
  handler_->DisablePostamblePageForTesting();
  handler_->CreateFakeOpticalCharacterRecognizerForTesting(
      /*return_empty*/ true);
  const size_t kTestNumPages = 2u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));

  EXPECT_EQ(kLoadingMessage, handler_->GetDocumentTreeToStringForTesting());
  WaitForOcringPages(1u);
  EXPECT_EQ(kLoadingMessage, handler_->GetDocumentTreeToStringForTesting());
  WaitForOcringPages(1u);
  // Note that the region nodes under the document root node have the (0,0)
  // offset. Each page will be correctly offset as the root node of its (child)
  // tree has a correct offset.
  EXPECT_EQ(
      "AXTree has_parent_tree title=PDF document\n"
      "id=1 pdfRoot FOCUSABLE name=PDF document containing 2 pages "
      "name_from=attribute clips_children child_ids=10000,2,3 (0, 0)-(3, 18) "
      "scroll_x_min=0 scroll_y_min=0 restriction=readonly text_align=left "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=10000 banner <div> child_ids=10001 offset_container_id=1 (-1, "
      "-1)-(1, 1) text_align=left is_page_breaking_object=true "
      "is_line_breaking_object=true has_aria_attribute=true\n"
      "    id=10001 status <div> child_ids=10002 offset_container_id=10000 (0, "
      "0)-(1, 1) text_align=left container_relevant=additions text "
      "container_live=polite relevant=additions text live=polite "
      "container_atomic=true container_busy=false atomic=true "
      "is_line_breaking_object=true has_aria_attribute=true\n"
      "      id=10002 staticText name=This PDF is inaccessible. No "
      "text extracted child_ids=10003 offset_container_id=10001 (0, 0)-(1, 1) "
      "text_align=left container_relevant=additions text container_live=polite "
      "relevant=additions text live=polite container_atomic=true "
      "container_busy=false atomic=true is_line_breaking_object=true\n"
      "        id=10003 inlineTextBox name=This PDF is inaccessible. No text "
      "extracted offset_container_id=10002 (0, 0)-(1, 1) text_align=left\n"
      "  id=2 region name=Page 1 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n"
      "  id=3 region name=Page 2 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n",
      handler_->GetDocumentTreeToStringForTesting());
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest,
                       RelativeBoundsWithOffsetAndScale) {
  constexpr size_t kTestNumPages = 1u;
  constexpr float kViewportWidth = 100.0f;
  constexpr float kViewportHeight = 200.0f;
  // MediaApp sometimes also sends negative viewport origins.
  constexpr float kViewportXOffset = -10.0f;
  constexpr float kViewportYOffset = -5.0f;
  constexpr float kViewportScale = 1.2f;
  handler_->ViewportUpdated(gfx::RectF(kViewportXOffset, kViewportYOffset,
                                       kViewportWidth, kViewportHeight),
                            /*scale_factor=*/kViewportScale);

  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  // `PageMetadataUpdated()` eventually calls `UpdateDocumentTree()` that
  // applies a transform to the document root node.
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  WaitForOcringPages(kTestNumPages);

  const ui::AXNode* document_root = handler_->GetDocumentRootNodeForTesting();
  // The page must have gone through OCR.
  ASSERT_EQ(kTestNumPages, fake_media_app_.PageIdsWithBitmap().size());
  EXPECT_EQ("PageA", fake_media_app_.PageIdsWithBitmap()[0]);

  const ui::AXNode* page_a_root =
      handler_->GetPagesForTesting().at("PageA")->GetRoot();
  ASSERT_NE(nullptr, page_a_root);
  constexpr gfx::RectF kExpectRect =
      gfx::RectF(0.0f, 0.0f, kTestPageWidth, kTestPageHeight);
  const gfx::RectF page_a_rect = page_a_root->data().relative_bounds.bounds;

  EXPECT_EQ(kExpectRect, page_a_rect);
  EXPECT_EQ(
      gfx::RectF(-kViewportXOffset * kViewportScale * kTestDisplayPixelRatio,
                 -kViewportYOffset * kViewportScale * kTestDisplayPixelRatio,
                 kTestPageWidth * kViewportScale * kTestDisplayPixelRatio,
                 kTestPageHeight * kViewportScale * kTestDisplayPixelRatio),
      document_root->data().relative_bounds.transform->MapRect(page_a_rect));
}

IN_PROC_BROWSER_TEST_F(AXMediaAppUntrustedHandlerTest, PostamblePage) {
  handler_->DisableStatusNodesForTesting();
  const size_t kTestNumPages = 3u;
  std::vector<PageMetadataPtr> fake_metadata =
      CreateFakePageMetadata(kTestNumPages);
  handler_->PageMetadataUpdated(ClonePageMetadataPtrs(fake_metadata));
  EXPECT_EQ(
      "AXTree has_parent_tree title=PDF document\n"
      "id=1 pdfRoot FOCUSABLE clips_children child_ids=10004 (0, 0)-(0, 0) "
      "text_align=left restriction=readonly scroll_x_min=0 scroll_y_min=0 "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=10004 region child_ids=10005 (0, 0)-(0, 0) restriction=readonly "
      "is_page_breaking_object=true\n"
      "    id=10005 paragraph child_ids=10006 (0, 0)-(0, 0) "
      "is_line_breaking_object=true\n"
      "      id=10006 staticText name=Extracting text in next few pages "
      "child_ids=10007 (0, 0)-(0, 0) restriction=readonly\n"
      "        id=10007 inlineTextBox name=Extracting text in next few pages "
      "(0, 0)-(0, 0) restriction=readonly\n",
      handler_->GetDocumentTreeToStringForTesting());
  WaitForOcringPages(1u);
  // No change from the previous one because of the fact that pages are OCRed in
  // batches.
  EXPECT_EQ(
      "AXTree has_parent_tree title=PDF document\n"
      "id=1 pdfRoot FOCUSABLE clips_children child_ids=10004 (0, 0)-(0, 0) "
      "text_align=left restriction=readonly scroll_x_min=0 scroll_y_min=0 "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=10004 region child_ids=10005 (0, 0)-(0, 0) restriction=readonly "
      "is_page_breaking_object=true\n"
      "    id=10005 paragraph child_ids=10006 (0, 0)-(0, 0) "
      "is_line_breaking_object=true\n"
      "      id=10006 staticText name=Extracting text in next few pages "
      "child_ids=10007 (0, 0)-(0, 0) restriction=readonly\n"
      "        id=10007 inlineTextBox name=Extracting text in next few pages "
      "(0, 0)-(0, 0) restriction=readonly\n",
      handler_->GetDocumentTreeToStringForTesting());
  WaitForOcringPages(1u);
  // Note that the region nodes under the document root node have the (0,0)
  // offset. Each page will be correctly offset as the root node of its (child)
  // tree has a correct offset.
  EXPECT_EQ(
      "AXTree has_parent_tree title=PDF document\n"
      "id=1 pdfRoot FOCUSABLE name=PDF document containing 2 pages "
      "name_from=attribute clips_children child_ids=2,3,10004 (0, 0)-(3, 18) "
      "scroll_x_min=0 scroll_y_min=0 restriction=readonly text_align=left "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=2 region name=Page 1 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n"
      "  id=3 region name=Page 2 name_from=attribute has_child_tree (0, "
      "0)-(3, 8) restriction=readonly is_page_breaking_object=true\n"
      "  id=10004 region child_ids=10005 (0, 0)-(0, 0) restriction=readonly "
      "is_page_breaking_object=true\n"
      "    id=10005 paragraph child_ids=10006 (0, 0)-(0, 0) "
      "is_line_breaking_object=true\n"
      "      id=10006 staticText name=Extracting text in next few pages "
      "child_ids=10007 (0, 0)-(0, 0) restriction=readonly\n"
      "        id=10007 inlineTextBox name=Extracting text in next few pages "
      "(0, 0)-(0, 0) restriction=readonly\n",
      handler_->GetDocumentTreeToStringForTesting());
  WaitForOcringPages(1u);
  EXPECT_EQ(
      "AXTree has_parent_tree title=PDF document\n"
      "id=1 pdfRoot FOCUSABLE name=PDF document containing 3 pages "
      "name_from=attribute clips_children child_ids=2,3,4 (0, 0)-(3, 28) "
      "scroll_x_min=0 scroll_y_min=0 restriction=readonly text_align=left "
      "scrollable=true is_line_breaking_object=true\n"
      "  id=2 region name=Page 1 name_from=attribute has_child_tree (0, 0)-(3, "
      "8) restriction=readonly is_page_breaking_object=true\n"
      "  id=3 region name=Page 2 name_from=attribute has_child_tree (0, "
      "0)-(3, 8) restriction=readonly is_page_breaking_object=true\n"
      "  id=4 region name=Page 3 name_from=attribute has_child_tree (0, "
      "0)-(3, 8) restriction=readonly is_page_breaking_object=true\n",
      handler_->GetDocumentTreeToStringForTesting());
}

}  // namespace ash::test