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

#include "content/browser/accessibility/dump_accessibility_browsertest_base.h"

#include <optional>
#include <set>
#include <string>
#include <vector>

#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/scoped_observation.h"
#include "base/strings/escape.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_command_line.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "content/browser/accessibility/browser_accessibility_state_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_child_frame.h"
#include "content/public/browser/ax_inspect_factory.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/accessibility_notification_waiter.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/scoped_accessibility_mode_override.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "third_party/blink/public/common/frame/frame_owner_element_type.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/platform/browser_accessibility.h"
#include "ui/accessibility/platform/browser_accessibility_manager.h"
#include "ui/base/ui_base_features.h"

#if BUILDFLAG(IS_MAC)
#include "ui/accessibility/platform/browser_accessibility_cocoa_test_helpers.h"
#endif

#if BUILDFLAG(IS_ANDROID)
#include "ui/accessibility/android/accessibility_state.h"
#endif

namespace content {

namespace {

bool SkipUrlMatch(const std::vector<std::string>& skip_urls,
                  const std::string& url) {
  for (const auto& skip_url : skip_urls) {
    if (url.contains(skip_url)) {
      return true;
    }
  }
  return false;
}

bool ShouldHaveChildTree(const ui::AXNode& node,
                         const std::vector<std::string>& skip_urls) {
  const ui::AXNodeData& data = node.data();
  if (data.GetRestriction() == ax::mojom::Restriction::kDisabled) {
    DCHECK(!data.HasStringAttribute(ax::mojom::StringAttribute::kChildTreeId));
    return false;  // A disabled child tree owner won't have a child tree.
  }

  if (node.IsInvisibleOrIgnored()) {
    return false;
  }

  // If it has an embedding element role or a child tree id, then expect some
  // child tree content. In some cases IsEmbeddingElement(role) will be false,
  // if an ARIA role was used, e.g. <iframe role="region">.
  if (data.HasStringAttribute(ax::mojom::StringAttribute::kChildTreeId)) {
    return true;
  }
  if (!ui::IsEmbeddingElement(node.GetRole())) {
    return false;
  }
  std::string url = node.GetStringAttribute(ax::mojom::StringAttribute::kUrl);
  return (!url.empty() && !SkipUrlMatch(skip_urls, url));
}

class AXTreeChangeWaiter : public ui::AXTreeObserver {
 public:
  AXTreeChangeWaiter()
      : loop_runner_(std::make_unique<base::RunLoop>()),
        loop_runner_quit_closure_(loop_runner_->QuitClosure()) {}

  void OnStringAttributeChanged(ui::AXTree* tree,
                                ui::AXNode* node,
                                ax::mojom::StringAttribute attr,
                                const std::string& old_value,
                                const std::string& new_value) override {
    if (attr == ax::mojom::StringAttribute::kChildTreeId) {
      tree->RemoveObserver(this);
      loop_runner_quit_closure_.Run();
    }
  }

  void OnChildTreeConnectionChanged(ui::AXNode* host_node) override {
    host_node->tree()->RemoveObserver(this);
    loop_runner_quit_closure_.Run();
  }

  void WaitForChange(ui::AXTree* tree) {
    base::ScopedObservation<ui::AXTree, ui::AXTreeObserver> observation(this);
    observation.Observe(tree);
    loop_runner_->Run();
    loop_runner_.reset();
    loop_runner_quit_closure_.Reset();
  }

 private:
  std::unique_ptr<base::RunLoop> loop_runner_;
  base::RepeatingClosure loop_runner_quit_closure_;
};

void WaitForChildTrees(const ui::AXNode& node,
                       const std::vector<std::string>& skip_urls) {
  while (true) {
    size_t num_children = node.GetChildCountCrossingTreeBoundary();
    if (!num_children && ShouldHaveChildTree(node, skip_urls)) {
      AXTreeChangeWaiter waiter;
      waiter.WaitForChange(node.tree());
      continue;
    }

    // Any node that is the connection point for a child tree should have
    // exactly one child.
    DCHECK(!ShouldHaveChildTree(node, skip_urls) || num_children == 1u)
        << "AXNode (" << node << ") has an unexpected number of "
        << "children :" << num_children;

    for (size_t i = 0; i < num_children; i++) {
      WaitForChildTrees(*node.GetChildAtIndexCrossingTreeBoundary(i),
                        skip_urls);
    }
    break;
  }
}

bool IsLoadedDocWithUrl(const ui::BrowserAccessibility* node,
                        const std::string& url) {
  return node->GetRole() == ax::mojom::Role::kRootWebArea &&
         node->GetStringAttribute(ax::mojom::StringAttribute::kUrl) == url &&
         node->manager()->GetTreeData().loaded;
}

// Recursively searches accessibility nodes in the subtree of |node| that
// represent a fully loaded web document with the given |url|. If less than
// |num_expected| occurrences are found, it returns the remainder. Otherwise,
// it stops searching when reaching |num_expected| occurrences, and returns 0.
unsigned SearchLoadedDocsWithUrlInAccessibilityTree(
    const ui::BrowserAccessibility* node,
    const std::string& url,
    unsigned num_expected) {
  if (!num_expected) {
    return 0;
  }

  if (IsLoadedDocWithUrl(node, url)) {
    num_expected -= 1;
    if (!num_expected) {
      return 0;
    }
  }

  for (const auto* child : node->AllChildren()) {
    num_expected =
        SearchLoadedDocsWithUrlInAccessibilityTree(child, url, num_expected);
    if (!num_expected) {
      return 0;
    }
  }
  return num_expected;
}

}  // namespace

using ui::AXPropertyFilter;
using ui::AXTreeFormatter;

// DumpAccessibilityTestBase
DumpAccessibilityTestBase::DumpAccessibilityTestBase()
    : enable_accessibility_after_navigating_(false), test_helper_(GetParam()) {}

DumpAccessibilityTestBase::~DumpAccessibilityTestBase() {}

void DumpAccessibilityTestBase::SetUpCommandLine(
    base::CommandLine* command_line) {
  IsolateAllSitesForTesting(command_line);
}

void DumpAccessibilityTestBase::SetUpOnMainThread() {
  host_resolver()->AddRule("*", "127.0.0.1");
  SetupCrossSiteRedirector(embedded_test_server());
  base::FilePath source_dir;
  CHECK(base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &source_dir));
  static const base::FilePath::CharType kAriaPracticesDir[] =
      FILE_PATH_LITERAL("third_party/aria-practices/src/content");
  embedded_test_server()->ServeFilesFromDirectory(
      source_dir.Append(kAriaPracticesDir));
  ASSERT_TRUE(embedded_test_server()->Start());
}

void DumpAccessibilityTestBase::SetUp() {
  // Each test pass may require custom feature setup.
  test_helper_.InitializeFeatureList();

  std::vector<base::test::FeatureRef> enabled_features;
  std::vector<base::test::FeatureRef> disabled_features;
  ChooseFeatures(&enabled_features, &disabled_features);

  scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);

  // The <input type="color"> popup tested in
  // AccessibilityInputColorWithPopupOpen requires the ability to read pixels
  // from a Canvas, so we need to be able to produce pixel output.
  EnablePixelOutput();

#if BUILDFLAG(IS_MAC)
  // Opt the dump-test infrastructure into the AXCustomActionNamesForTesting
  // projection attribute on BrowserAccessibilityCocoa, so cross-process
  // AXUIElementCopyAttributeValue queries can observe aria-actions custom
  // action names (NSAccessibilityCustomAction objects do not marshal
  // across the AX bridge). Without this opt-in the attribute is invisible
  // to AT (not enumerated, and direct queries return nil).
  ui::EnableAXCustomActionNamesForTestingProjection();
#endif

  ContentBrowserTest::SetUp();
}

void DumpAccessibilityTestBase::TearDown() {
  ContentBrowserTest::TearDown();
  scoped_feature_list_.Reset();
  test_helper_.ResetFeatureList();
}

void DumpAccessibilityTestBase::SignalRunTestOnMainThread(int) {
  LOG(INFO) << "\n\nFinal accessibility tree upon the test termination:\n"
            << DumpUnfilteredAccessibilityTreeAsString();
}

void DumpAccessibilityTestBase::ChooseFeatures(
    std::vector<base::test::FeatureRef>* enabled_features,
    std::vector<base::test::FeatureRef>* disabled_features) {
  // For the best test coverage during development of this feature, enable the
  // code that expposes document markers on AXInlineTextBox objects and the
  // corresponding code in AXPosition on the browser that collects those
  // markers.
  enabled_features->emplace_back(features::kUseAXPositionForDocumentMarkers);
  // For improved test coverage ahead of a finch trial, enable the feature that
  // prunes redundant (next|previous) on line IDs.
  enabled_features->emplace_back(
      features::kAccessibilityPruneRedundantInlineConnectivity);
}

std::string DumpAccessibilityTestBase::DumpTreeAsString() const {
  std::unique_ptr<AXTreeFormatter> formatter(CreateFormatter());
  formatter->SetPropertyFilters(scenario_.property_filters,
                                AXTreeFormatter::kFiltersDefaultSet);
  formatter->SetNodeFilters(scenario_.node_filters);
  return FormatWebContentsTree(*formatter);
}

std::string
DumpAccessibilityTestBase::DumpUnfilteredAccessibilityTreeAsString() {
  std::unique_ptr<AXTreeFormatter> formatter(CreateFormatter());
  formatter->SetPropertyFilters({{"*", AXPropertyFilter::ALLOW}});
  formatter->set_show_ids(true);
  return FormatWebContentsTree(*formatter);
}

std::vector<ui::AXPropertyFilter> DumpAccessibilityTestBase::DefaultFilters()
    const {
  std::vector<ui::AXPropertyFilter> property_filters;
  if (GetParam() == ui::AXApiType::kMac) {
    return property_filters;
  }

  property_filters.emplace_back("value='*'", ui::AXPropertyFilter::ALLOW);
  // The value attribute on the document object contains the URL of the
  // current page which will not be the same every time the test is run.
  property_filters.emplace_back("value='http*'", ui::AXPropertyFilter::DENY);
  // Object attributes.value
  property_filters.emplace_back("layout-guess:*", ui::AXPropertyFilter::ALLOW);
  property_filters.emplace_back("details-from:*", ui::AXPropertyFilter::ALLOW);

  property_filters.emplace_back("select*", ui::AXPropertyFilter::ALLOW);
  property_filters.emplace_back("selectedFromFocus=*",
                                ui::AXPropertyFilter::DENY);
  property_filters.emplace_back("descript*", ui::AXPropertyFilter::ALLOW);
  property_filters.emplace_back("check*", ui::AXPropertyFilter::ALLOW);
  property_filters.emplace_back("horizontal", ui::AXPropertyFilter::ALLOW);
  property_filters.emplace_back("multiselectable", ui::AXPropertyFilter::ALLOW);
  property_filters.emplace_back("placeholder=*", ui::AXPropertyFilter::ALLOW);
  property_filters.emplace_back("ispopup*", ui::AXPropertyFilter::ALLOW);

  // Deny most empty values.
  property_filters.emplace_back("*=''", ui::AXPropertyFilter::DENY);
  // After denying empty values, we need to add the following filter because we
  // want to allow name=''.
  property_filters.emplace_back("name=*", ui::AXPropertyFilter::ALLOW_EMPTY);
  return property_filters;
}

std::string DumpAccessibilityTestBase::FormatWebContentsTree(
    const ui::AXTreeFormatter& formatter) const {
  std::string contents =
      IsTestingExternalTree()
          ? formatter.Format(
                {GetAcceleratedWidget(), ui::AXTreeSelector::ActiveTab})
          : formatter.Format(GetRootAccessibilityNode(GetWebContents()));
  return base::EscapeNonASCII(contents);
}

std::string DumpAccessibilityTestBase::FormatWebContentsTestNode(
    const ui::AXTreeFormatter& formatter) const {
  // Retrieve the element by "test" ID if present or "test" class name.
  // Using only the class name is preferred, as the presence of an ID can impact
  // the accessibility tree and make testing certain scenarios difficult.
  // Supporting only the class name reduces unnecessary complexity in testing.
  const std::string kTestNodeName = "test";
  std::string contents;
  if (IsTestingExternalTree()) {
    contents = formatter.FormatNode(
        {ui::AXTreeSelector::ActiveTab | ui::AXTreeSelector::IDOrClass,
         kTestNodeName, GetAcceleratedWidget()});
  } else {
    ui::BrowserAccessibility* test_node = FindNodeByStringAttribute(
        ax::mojom::StringAttribute::kHtmlId, kTestNodeName);
    if (!test_node) {
      test_node = FindNodeByStringAttribute(
          ax::mojom::StringAttribute::kClassName, kTestNodeName);
    }
    contents =
        test_node ? formatter.FormatNode(test_node) : "Test node not found.";
  }
  return base::EscapeNonASCII(contents);
}

// TODO(accessibility) Consider renaming these things to
// WaitForAccessibiltiyClean(), Action::kRequestAccessibilityCleanNotification,
// Event::kAccessibilityClean, etc. because this can be used multiple times
// per test.
void DumpAccessibilityTestBase::WaitForEndOfTest() const {
  AccessibilityNotificationWaiter waiter(GetWebContents(),
                                         ax::mojom::Event::kEndOfTest);

  // To make sure we've handled all accessibility events, add a sentinel by
  // calling SignalEndOfTest on each frame and waiting for a kEndOfTest event
  // in response.
  auto hosts = content::CollectAllRenderFrameHosts(GetWebContents());
  for (auto* host : hosts) {
    ui::AXActionData action_data;
    action_data.action = ax::mojom::Action::kSignalEndOfTest;
    host->AccessibilityPerformAction(action_data);
  }

  ASSERT_TRUE(waiter.WaitForNotification(true));
}

void DumpAccessibilityTestBase::PerformAndWaitForDefaultActions() {
  // Only perform actions the first call, as they are only allowed once per
  // test, e.g. only perform the action once if this is  script is executed
  // multiple times.

  if (has_performed_default_actions_) {
    return;
  }

  has_performed_default_actions_ = true;

  // Perform default action on any elements specified by the test.
  for (const auto& str : scenario_.default_action_on) {
    ui::BrowserAccessibility* action_element;
    // TODO(accessibility) base/strings/string_split.h might be cleaner here.
    size_t parent_node_delimiter_index = str.find(",");
    if (parent_node_delimiter_index != std::string::npos) {
      auto node_name = str.substr(0, parent_node_delimiter_index);
      auto parent_node_name = str.substr(parent_node_delimiter_index + 1);

      ui::BrowserAccessibility* parent_node = FindNode(parent_node_name);
      DCHECK(parent_node) << "Parent node name provided but not found";
      action_element = FindNode(node_name, parent_node);
    } else {
      action_element = FindNode(str);
    }

    // TODO(accessibility) Consider waiting for kEndOfTest instead (but change
    // the name to something more like kAccessibilityClean).
    bool is_listbox =
        action_element->GetData().role == ax::mojom::Role::kListBoxOption;
    // Options in ListBox select elements emit a click when the select isn't
    // focused yet, but don't emit a click when the select is already focused.
    // In this case, we have to wait for something other than the click. See
    // HTMLSelectElement::SelectOptionByAccessKey.
    AccessibilityNotificationWaiter waiter(
        GetWebContents(), is_listbox ? ax::mojom::Event::kCheckedStateChanged
                                     : ax::mojom::Event::kClicked);

    ui::AXActionData action_data;
    action_data.action = ax::mojom::Action::kDoDefault;
    action_element->AccessibilityPerformAction(action_data);

    ASSERT_TRUE(waiter.WaitForNotification());
  }
}

void DumpAccessibilityTestBase::WaitForExpectedText() {
  // If the original page has a @WAIT-FOR directive, don't break until
  // the text we're waiting for appears in the full text dump of the
  // accessibility tree, either.

  for (;;) {
    VLOG(1) << "Top of WaitForExpectedText() loop";
    // Check to see if the @WAIT-FOR text has appeared yet.
    bool all_wait_for_strings_found = true;
    std::string tree_dump = DumpTreeAsString();
    for (const auto& str : scenario_.wait_for) {
      if (!tree_dump.contains(str)) {
        VLOG(1) << "Still waiting on this text to be found: " << str;
        all_wait_for_strings_found = false;
        break;
      }
    }

    // If the @WAIT-FOR text has appeared, we're done.
    if (all_wait_for_strings_found) {
      break;
    }

    // Block until the next accessibility notification in any frame.
    VLOG(1) << "Waiting until the next accessibility event";
    AccessibilityNotificationWaiter accessibility_waiter(GetWebContents());
    ASSERT_TRUE(accessibility_waiter.WaitForNotification());
  }
}

void DumpAccessibilityTestBase::WaitForFinalTreeContents() {
  // If @DEFAULT-ACTION-ON:[name] is used, perform the action and wait until it
  // is complete.
  PerformAndWaitForDefaultActions();

  if (scenario_.wait_for.size()) {
    // Wait for expected text from @WAIT-FOR.
    WaitForExpectedText();
  } else {
    // Wait until all accessibility events and dirty objects have been
    // processed.
    WaitForEndOfTest();
  }
}

void DumpAccessibilityTestBase::RunTest(
    ui::AXMode mode,
    const base::FilePath test_page_path,
    const char* test_page_dir,
    const base::FilePath::StringType& expectations_qualifier) {
  RunTest(mode, test_page_path, test_page_dir, test_page_path,
          expectations_qualifier);
}

void DumpAccessibilityTestBase::RunTest(
    const base::FilePath test_page_path,
    const char* test_page_dir,
    const base::FilePath::StringType& expectations_qualifier) {
  RunTest(ui::kAXModeDefaultForTests, test_page_path, test_page_dir,
          test_page_path, expectations_qualifier);
}

void DumpAccessibilityTestBase::RunTest(
    ui::AXMode ax_mode_for_test,
    const base::FilePath test_page_path,
    const char* test_page_dir,
    const base::FilePath& expectation_path,
    const base::FilePath::StringType& expectations_qualifier) {
  CHECK(!expectation_path.empty());

  // Ignore the hovered state (set when the mouse is hovering over
  // an object) because it makes test output change based on the mouse position.
  ui::BrowserAccessibility::ignore_hovered_state_for_testing_ = true;

  // For Android, set a consistent user preference for how password display.
#if BUILDFLAG(IS_ANDROID)
  ui::AccessibilityState::ForceRespectDisplayedPasswordTextForTesting();
#endif

  // Normally some accessibility events that would be fired are suppressed or
  // delayed, depending on what has focus or the type of event. For testing,
  // we want all events to fire immediately to make tests predictable and not
  // flaky.
  ui::BrowserAccessibilityManager::NeverSuppressOrDelayEventsForTesting();

  // Enable the behavior whereby all focused nodes will be exposed to the
  // platform accessibility layer. This behavior is currently disabled in
  // production code, but is enabled in tests so that it could be tested
  // thoroughly before it is turned on for all code.
  // TODO(nektar): Turn this on in a followup patch.
  // ui::AXTree::SetFocusedNodeShouldNeverBeIgnored();

  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));

  std::optional<ui::AXInspectScenario> scenario =
      test_helper_.ParseScenario(test_page_path, DefaultFilters());
  if (!scenario) {
    ADD_FAILURE()
        << "Failed to process a testing file. The file might not exist: "
        << test_page_path.LossyDisplayName();
    return;
  }
  scenario_ = std::move(*scenario);

  std::optional<std::vector<std::string>> expected_lines;

  // Get expectation lines from expectation file if any.
  base::FilePath expected_file = test_helper_.GetExpectationFilePath(
      expectation_path, expectations_qualifier);
  if (!expected_file.empty()) {
    expected_lines = test_helper_.LoadExpectationFile(expected_file);
  }

  // Get the test URL.
  std::string url_path = "";
  if (test_page_dir && strlen(test_page_dir) > 0) {
    url_path += "/" + std::string(test_page_dir);
  }
  url_path += "/" + test_page_path.BaseName().MaybeAsASCII();
  GURL url(embedded_test_server()->GetURL("a.test", url_path));
  WebContentsImpl* web_contents = GetWebContents();

  std::optional<ScopedAccessibilityModeOverride> accessibility_mode;

  if (enable_accessibility_after_navigating_ &&
      web_contents->GetAccessibilityMode().is_mode_off()) {
    // Load the url, then enable accessibility.
    EXPECT_TRUE(NavigateToURL(shell(), url));
    AccessibilityNotificationWaiter accessibility_waiter(web_contents);
    accessibility_mode.emplace(ax_mode_for_test);
    BrowserAccessibilityStateImpl::GetInstance()->SetAXModeChangeAllowed(false);
    ASSERT_TRUE(accessibility_waiter.WaitForNotification());
  } else {
    // Enable accessibility, then load the test html and wait for the
    // "load complete" AX event.
    AccessibilityNotificationWaiter accessibility_waiter(
        web_contents, ax::mojom::Event::kLoadComplete);
    accessibility_mode.emplace(ax_mode_for_test);
    BrowserAccessibilityStateImpl::GetInstance()->SetAXModeChangeAllowed(false);
    EXPECT_TRUE(NavigateToURL(shell(), url));

    if (!accessibility_waiter.WaitForNotificationWithTimeout(
            TestTimeouts::action_timeout())) {
      // crbug.com/40844856: the first SetMode call to a new RenderFrameHost can
      // be silently dropped if its RenderAccessibility isn't bound yet. If that
      // happens, resend SetMode on every frame via UpdateAccessibilityMode,
      // then call ResetAccessibility so kLoadComplete is emitted.
      web_contents->GetPrimaryMainFrame()->ForEachRenderFrameHostImpl(
          [](RenderFrameHostImpl* rfh) { rfh->UpdateAccessibilityMode(); });
      web_contents->ResetAccessibility();
      ASSERT_TRUE(accessibility_waiter.WaitForNotificationWithTimeout(
          TestTimeouts::action_max_timeout()));
    }
  }

  WaitForAllFramesLoaded();

  // Call the subclass to dump the output.
  std::vector<std::string> actual_lines = Dump();

  // Execute and wait for specified string
  for (const auto& function_name : scenario_.execute) {
    DLOG(INFO) << "executing: " << function_name;
    const std::string str =
        EvalJs(web_contents->GetPrimaryMainFrame(), function_name)
            .ExtractString();
    // If no string is specified, do not wait.
    bool wait_for_string = str != "";
    while (wait_for_string) {
      // Loop until specified string is found.
      std::string tree_dump = DumpUnfilteredAccessibilityTreeAsString();
      if (tree_dump.contains(str)) {
        wait_for_string = false;
        // Append an additional dump if the specified string was found.
        std::vector<std::string> additional_dump = Dump();
        actual_lines.emplace_back("=== Start Continuation ===");
        actual_lines.insert(actual_lines.end(), additional_dump.begin(),
                            additional_dump.end());
        break;
      }
      // Block until the next accessibility notification in any frame.
      VLOG(1) << "Still waiting on this text to be found: " << str;
      VLOG(1) << "Waiting until the next accessibility event";
      // TODO(aleventhal) Try waiting for kEndOfTest to make sure all events
      // after code execution are captured.
      AccessibilityNotificationWaiter accessibility_waiter(web_contents);
      ASSERT_TRUE(accessibility_waiter.WaitForNotification());
    }
  }

  // No expected lines indicate the test is marked to skip the expectations
  // checks or it has no expectation file. If we reach this point, then it means
  // no crashes during the test run and we can consider the test as succeeding.
  if (!expected_lines) {
    EXPECT_TRUE(true);
    return;
  }

  // Validate against the expectation file.
  bool matches_expectation = test_helper_.ValidateAgainstExpectation(
      test_page_path, expected_file, actual_lines, *expected_lines);
  EXPECT_TRUE(matches_expectation);
  if (!matches_expectation) {
    OnDiffFailed();
  }
}

std::map<std::string, unsigned> DumpAccessibilityTestBase::CollectAllFrameUrls(
    const std::vector<std::string>& skip_urls) {
  std::map<std::string, unsigned> all_frame_urls;
  // Get the url of every frame in the frame tree.
  for (FrameTreeNode* node : GetWebContents()->GetPrimaryFrameTree().Nodes()) {
    // Ignore about:blank urls because of the case where a parent frame A
    // has a child iframe B and it writes to the document using
    // contentDocument.open() on the child frame B.
    //
    // In this scenario, B's contentWindow.location.href matches A's url,
    // but B's url in the browser frame tree is still "about:blank".

    std::string url = node->current_url().spec();
    if (url != url::kAboutBlankURL && url != url::kAboutSrcdocURL &&
        !url.empty() && !SkipUrlMatch(skip_urls, url)) {
      all_frame_urls[url] += 1;
    }
  }
  return all_frame_urls;
}

void DumpAccessibilityTestBase::WaitForAllFramesLoaded() {
  // Wait for the accessibility tree to fully load for all frames,
  // by searching for the WEB_AREA node in the accessibility tree
  // with the url of each frame in our frame tree. If all frames
  // haven't loaded yet, set up a listener for accessibility events
  // on any frame and block until the next one is received.
  WebContentsImpl* web_contents = GetWebContents();
  for (;;) {
    VLOG(1) << "Top of WaitForAllFramesLoaded() loop";
    RenderFrameHostImpl* main_frame =
        static_cast<RenderFrameHostImpl*>(web_contents->GetPrimaryMainFrame());
    ui::BrowserAccessibilityManager* manager =
        main_frame->browser_accessibility_manager();
    if (manager) {
      ui::BrowserAccessibility* accessibility_root =
          manager->GetBrowserAccessibilityRoot();

      WaitForChildTrees(*accessibility_root->node(),
                        scenario_.no_load_expected);

      bool all_expected_urls_loaded = true;
      // A test may change the url for a frame, for example by setting
      // window.location.href, so collect the current list of urls.
      const std::map<std::string, unsigned> all_frame_urls =
          CollectAllFrameUrls(scenario_.no_load_expected);
      for (const auto& [url, num_expected] : all_frame_urls) {
        if (unsigned num_remaining = SearchLoadedDocsWithUrlInAccessibilityTree(
                accessibility_root, url, num_expected)) {
          VLOG(1) << "Still waiting on " << num_remaining
                  << " frame(s) to load: " << url;
          all_expected_urls_loaded = false;
          break;
        }
      }
      if (all_expected_urls_loaded) {
        break;
      }
    }

    // Block until the next accessibility notification in any frame.
    VLOG(1) << "Waiting until the next accessibility event";
    AccessibilityNotificationWaiter accessibility_waiter(web_contents);
    ASSERT_TRUE(accessibility_waiter.WaitForNotification());
  }
}

ui::BrowserAccessibility* DumpAccessibilityTestBase::FindNode(
    const std::string& name,
    ui::BrowserAccessibility* search_root) const {
  if (!search_root) {
    search_root = GetManager()->GetBrowserAccessibilityRoot();
  }

  CHECK(search_root);
  return FindFirstAccessibilityNodeWithStringAttribute(
      *search_root, ax::mojom::StringAttribute::kName, name);
}

ui::BrowserAccessibilityManager* DumpAccessibilityTestBase::GetManager() const {
  return GetWebContents()->GetRootBrowserAccessibilityManager();
}

WebContentsImpl* DumpAccessibilityTestBase::GetWebContents() const {
  return static_cast<WebContentsImpl*>(shell()->web_contents());
}

gfx::AcceleratedWidget DumpAccessibilityTestBase::GetAcceleratedWidget() const {
#if BUILDFLAG(IS_OZONE) || BUILDFLAG(IS_APPLE)
  return static_cast<gfx::AcceleratedWidget>(getpid());
#else
  // The getpid() function is unavailable on some platforms, like Windows. Use 0
  // as a fallback until support for these platforms is required for external
  // tree testing.
  return reinterpret_cast<gfx::AcceleratedWidget>(0);
#endif
}

std::unique_ptr<AXTreeFormatter> DumpAccessibilityTestBase::CreateFormatter()
    const {
  return AXInspectFactory::CreateFormatter(GetParam());
}

std::pair<base::Value, std::vector<std::string>>
DumpAccessibilityTestBase::CaptureEvents(InvokeAction invoke_action) {
  // Create a new Event Recorder for the run.
  ui::BrowserAccessibilityManager* manager = GetManager();
  ui::AXTreeSelector selector(manager->GetBrowserAccessibilityRoot()
                                  ->GetTargetForNativeAccessibilityEvent());
  std::unique_ptr<ui::AXEventRecorder> event_recorder =
      AXInspectFactory::CreateRecorder(GetParam(), manager,
                                       base::GetCurrentProcId(), selector);
  event_recorder->SetOnlyWebEvents(true);

  event_recorder->ListenToEvents(base::BindRepeating(
      &DumpAccessibilityTestBase::OnEventRecorded, base::Unretained(this)));

  LOG(INFO) << "-------------- Start listening to events --------------";

  // If @DEFAULT-ACTION-ON:[name] is used, perform the action and wait until
  // it is complete.
  PerformAndWaitForDefaultActions();

  // Create a waiter that waits for any one accessibility event.
  // This will ensure that after calling the go() function, we
  // block until we've received an accessibility event generated as
  // a result of this function.
  AccessibilityNotificationWaiter waiter(GetWebContents());

  // Run any script, e.g. go().
  // If an action was performed, we already waited for the kClicked event in
  // PerformAndWaitForDefaultActions(), which means the action is already
  // completed.
  base::Value action_result = std::move(invoke_action).Run();

  // If we didn't already wait for a default action to complete, then
  // wait for at least one event. This may unblock either when |waiter|
  // observes either an ax::mojom::Event or ui::AXEventGenerator::Event, or
  // when |event_recorder| records a platform event.
  // TODO(crbug.com/40844856): May time out if SetMode was silently dropped.
  if (scenario_.default_action_on.empty()) {
    EXPECT_TRUE(waiter.WaitForNotification());
  }

  // More than one accessibility event could have been generated.
  // To make sure we've received all accessibility events, add a
  // sentinel by calling SignalEndOfTest and waiting for a kEndOfTest
  // event in response.
  WaitForEndOfTest();
  event_recorder->WaitForDoneRecording();

  LOG(INFO) << "-------------- Stop listening to events --------------";

  // Dump the event logs, running them through any filters specified
  // in the HTML file.
  std::vector<std::string> event_logs = event_recorder->GetEventLogs();

  // Sort the logs so that results are predictable. There are too many
  // nondeterministic things that affect the exact order of events fired,
  // so these tests shouldn't be used to make assertions about event order.
  std::sort(event_logs.begin(), event_logs.end());

  return std::make_pair(std::move(action_result), std::move(event_logs));
}

ui::BrowserAccessibility* DumpAccessibilityTestBase::FindNodeByStringAttribute(
    const ax::mojom::StringAttribute attr,
    const std::string& value) const {
  ui::BrowserAccessibility* root = GetManager()->GetBrowserAccessibilityRoot();

  CHECK(root);
  return FindFirstAccessibilityNodeWithStringAttribute(*root, attr, value);
}

bool DumpAccessibilityTestBase::IsTestingExternalTree() const {
#if BUILDFLAG(IS_MAC)
  // macOS uses an external accessibility tree, which allows testing exactly
  // what assistive technologies operates with. Other platforms
  // test the internal accessibility tree except the Android one which tests
  // both.
  //
  // TODO(crbug.com/407816615): AXUIElementCopyAttributeValue cannot observe
  // NSAccessibilityCustomAction values across the cross-process AX boundary;
  // such attributes must be covered in-process. See
  // BrowserAccessibilityCocoaAriaActionsBrowserTest.
  return GetParam() == ui::AXApiType::kMac;
#else
  return false;
#endif
}

void DumpAccessibilityTestBase::UseHttpsTestServer() {
  https_test_server_ = std::make_unique<net::EmbeddedTestServer>(
      net::EmbeddedTestServer::TYPE_HTTPS);
  https_test_server_.get()->AddDefaultHandlers(GetTestDataFilePath());
  https_test_server_.get()->SetSSLConfig(
      net::EmbeddedTestServer::CERT_TEST_NAMES);
}

void DumpAccessibilityTestBase::SetUpMaterialDesignRequestHandler() {
  base::FilePath src_root;
  base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &src_root);
  node_modules_dir_ = src_root.AppendASCII("third_party")
                          .AppendASCII("material_web_components")
                          .AppendASCII("components-chromium")
                          .AppendASCII("node_modules");
  embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
      &DumpAccessibilityTestBase::HandleMaterialDesignRequest,
      base::Unretained(this)));
}

std::unique_ptr<net::test_server::HttpResponse>
DumpAccessibilityTestBase::HandleMaterialDesignRequest(
    const net::test_server::HttpRequest& request) {
  std::string path = request.relative_url;
  if (path.empty() || path[0] != '/') {
    return nullptr;
  }

  // Only handle Material Design component requests.
  if (!base::StartsWith(path, "/@material/") &&
      !base::StartsWith(path, "/lit") && !base::StartsWith(path, "/@lit/") &&
      !base::StartsWith(path, "/tslib/")) {
    return nullptr;
  }

  base::FilePath full_path = node_modules_dir_.AppendASCII(path.substr(1));
  base::ScopedAllowBlockingForTesting allow_blocking;
  if (!base::PathExists(full_path)) {
    return nullptr;
  }

  std::string content;
  if (!base::ReadFileToString(full_path, &content)) {
    return nullptr;
  }

  auto response = std::make_unique<net::test_server::BasicHttpResponse>();
  response->set_code(net::HTTP_OK);
  response->set_content(content);

  if (base::EndsWith(path, ".js", base::CompareCase::INSENSITIVE_ASCII)) {
    response->set_content_type("application/javascript");
  } else if (base::EndsWith(path, ".css",
                            base::CompareCase::INSENSITIVE_ASCII)) {
    response->set_content_type("text/css");
  }

  return response;
}

}  // namespace content
