chromium/content/browser/accessibility/cross_platform_accessibility_browsertest.cc

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

#include <stddef.h>
#include <stdint.h>

#include <string>
#include <unordered_set>
#include <vector>

#include "base/command_line.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/escape.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "build/build_config.h"
#include "build/chromecast_buildflags.h"
#include "build/chromeos_buildflags.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/common/isolated_world_ids.h"
#include "content/public/test/accessibility_notification_waiter.h"
#include "content/public/test/browser_test.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/shell/browser/shell.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/features.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/accessibility_switches.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/platform/browser_accessibility.h"
#include "ui/accessibility/platform/browser_accessibility_manager.h"
#include "ui/base/buildflags.h"

#if BUILDFLAG(IS_WIN)
#include "base/win/atl.h"
#include "base/win/scoped_com_initializer.h"
#include "ui/base/win/atl_module.h"
#endif

ElementsAre;
Pair;

#if defined(NDEBUG) && !defined(ADDRESS_SANITIZER) &&              \
    !defined(LEAK_SANITIZER) && !defined(MEMORY_SANITIZER) &&      \
    !defined(THREAD_SANITIZER) && !defined(UNDEFINED_SANITIZER) && \
    !BUILDFLAG(IS_ANDROID)
#define IS_FAST_BUILD
constexpr int kDelayForDeferredUpdatesAfterPageLoad = 150;
#endif

namespace content {

class CrossPlatformAccessibilityBrowserTest : public ContentBrowserTest {};

void CrossPlatformAccessibilityBrowserTest::SetUp() {}

void CrossPlatformAccessibilityBrowserTest::ChooseFeatures(
    std::vector<base::test::FeatureRef>* enabled_features,
    std::vector<base::test::FeatureRef>* disabled_features) {}

void CrossPlatformAccessibilityBrowserTest::SetUpOnMainThread() {}

// Convenience method to get the value of a particular AXNode
// attribute as a UTF-8 string.
std::string CrossPlatformAccessibilityBrowserTest::GetAttr(
    const ui::AXNode* node,
    const ax::mojom::StringAttribute attr) {}

// Convenience method to get the value of a particular AXNode
// integer attribute.
int CrossPlatformAccessibilityBrowserTest::GetIntAttr(
    const ui::AXNode* node,
    const ax::mojom::IntAttribute attr) {}

// Convenience method to get the value of a particular AXNode
// boolean attribute.
bool CrossPlatformAccessibilityBrowserTest::GetBoolAttr(
    const ui::AXNode* node,
    const ax::mojom::BoolAttribute attr) {}

namespace {

// Convenience method to find a node by its role value.
ui::BrowserAccessibility* FindNodeByRole(ui::BrowserAccessibility* root,
                                         ax::mojom::Role role) {}

}  // namespace

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       WebpageAccessibility) {}

// Android's text representation is different, so disable the test there.
#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       ReparentingANodeShouldReuseSameNativeWrapper) {}
#endif  // !BUILDFLAG(IS_ANDROID)

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       UnselectedEditableTextAccessibility) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       SelectedEditableTextAccessibility) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       MultipleInheritanceAccessibility2) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       IframeAccessibility) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       EnsureLocationChangesSendLocationUpdatesOnly) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       PlatformIframeAccessibility) {}

// Android's text representation is different, so disable the test there.
#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       AXNodePositionTreeBoundary) {}
#endif  // !BUILDFLAG(IS_ANDROID)

// Android's text representation is different, so disable the test there.
#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       NavigationSkipsCompositeItems) {}
#endif  // !BUILDFLAG(IS_ANDROID)

#if !BUILDFLAG(IS_MAC)
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       SelectSizeChangeWithOpenedPopupDoesNotCrash) {}
#endif  // !BUILDFLAG(IS_MAC)

#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_MAC) && \
    !(BUILDFLAG(IS_IOS) && BUILDFLAG(USE_BLINK))
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       GetBoundsRectIframes) {}
#endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_MAC) && !(BUILDFLAG(IS_IOS)
        // && BUILDFLAG(USE_BLINK))

// Select controls behave differently on Mac/Android/iOS-Blink, this test
// doesn't apply.
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_MAC) && \
    !(BUILDFLAG(IS_IOS) && BUILDFLAG(USE_BLINK))
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       GetBoundsRectWithScroll) {}
#endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_MAC) && !(BUILDFLAG(IS_IOS)
        // && BUILDFLAG(USE_BLINK))

// Android and Mac do not expose <select>s the same as other platforms do.
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_MAC)
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       SelectWithOptgroupActiveDescendant) {}
#endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_MAC)

// Android uses kComboboxSelect instead of kListbox for <select size > 1>.
#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       SelectListWithOptgroupActiveDescendant) {}
#endif  // !BUILDFLAG(IS_ANDROID)

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       PlatformIterator) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       DuplicateChildrenAccessibility) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest, WritableElement) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       AriaSortDirection) {}

// Fuchsia WebEngine (currently the only content embedder on the platform)
// does not use or include these localization strings,
// see: https://crbug.com/358567091 for more details.
#if !BUILDFLAG(IS_FUCHSIA)
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       LocalizedLandmarkType) {}

// TODO(crbug.com/40656480) re-enable when crashing on linux is resolved.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
#define MAYBE_LocalizedRoleDescription
#else
#define MAYBE_LocalizedRoleDescription
#endif
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       MAYBE_LocalizedRoleDescription) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       GetStyleNameAttributeAsLocalizedString) {}
#endif  // #if !BUILDFLAG(IS_FUCHSIA)

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       TooltipStringAttributeMutuallyExclusiveOfNameFromTitle) {}

IN_PROC_BROWSER_TEST_F(
    CrossPlatformAccessibilityBrowserTest,
    PlaceholderStringAttributeMutuallyExclusiveOfNameFromPlaceholder) {}

// On Android root scroll offset is handled by the Java layer. The final rect
// bounds is device specific.
#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       GetBoundsRectUnclippedRootFrameFromIFrame) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       GetBoundsRectUnclippedFrameFromIFrame) {}

// Flaky on Lacros: https://crbug.com/1292527
// TODO(crbug.com/40835208): Enable on Fuchsia when content_browsertests
// runs in non-headless mode.
#if BUILDFLAG(IS_CHROMEOS_LACROS) || BUILDFLAG(IS_FUCHSIA)
#define MAYBE_ControlsIdsForDateTimePopup
#else
#define MAYBE_ControlsIdsForDateTimePopup
#endif
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       MAYBE_ControlsIdsForDateTimePopup) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       ControlsIdsForColorPopup) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       TextFragmentAnchor) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest, GeneratedText) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       FocusFiresJavascriptOnfocus) {}

IN_PROC_BROWSER_TEST_F(
    CrossPlatformAccessibilityBrowserTest,
    // TODO(crbug.com/40874531): Re-enable this test
    DISABLED_IFrameContentHadFocus_ThenRootDocumentGainedFocus) {}
#endif  // !BUILDFLAG(IS_ANDROID)

// This test is checking behavior when ImplicitRootScroller is enabled which
// applies only on Android.
#if BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       ImplicitRootScroller) {
  LoadInitialAccessibilityTreeFromHtmlFilePath(
      "/accessibility/scrolling/implicit-root-scroller.html");

  ui::BrowserAccessibilityManager* manager = GetManager();
  const ui::BrowserAccessibility* heading = FindNodeByRole(
      manager->GetBrowserAccessibilityRoot(), ax::mojom::Role::kHeading);

  // Ensure that this page has an implicit root scroller that's something
  // other than the root of the accessibility tree.
  ui::AXNodeID root_scroller_id = manager->GetTreeData().root_scroller_id;
  ui::BrowserAccessibility* root_scroller =
      manager->GetFromID(root_scroller_id);
  ASSERT_TRUE(root_scroller);
  EXPECT_NE(root_scroller_id, manager->GetBrowserAccessibilityRoot()->GetId());

  // If we take the root scroll offsets into account (most platforms)
  // the heading should be scrolled above the top.
  manager->SetUseRootScrollOffsetsWhenComputingBoundsForTesting(true);
  gfx::Rect bounds = heading->GetUnclippedRootFrameBoundsRect();
  EXPECT_LT(bounds.y(), 0);

  // If we don't take the root scroll offsets into account (Android)
  // the heading should not have a negative top coordinate.
  manager->SetUseRootScrollOffsetsWhenComputingBoundsForTesting(false);
  bounds = heading->GetUnclippedRootFrameBoundsRect();
  EXPECT_GT(bounds.y(), 0);
}
#endif  // BUILDFLAG(IS_ANDROID)

#if defined(IS_FAST_BUILD)  // Avoid flakiness on slower debug/sanitizer builds.

// TODO(crbug.com/40923912):  Enable once thread flakiness is resolved.
// TODO(crbug.com/332652840): It is flaky with SkiaGraphite enabled on Windows.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN)
#define MAYBE_NonInteractiveChangesAreBatched
#else
#define MAYBE_NonInteractiveChangesAreBatched
#endif
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       MAYBE_NonInteractiveChangesAreBatched) {
  // Ensure that normal DOM changes are batched together, and do not occur
  // more than once every kDelayForDeferredUpdatesAfterPageLoad.
  LoadInitialAccessibilityTreeFromHtml(R"HTML(
      <!DOCTYPE html>
      <html>
      <body>
        <div id="foo">
        </div>
        <script>
          const startTime = performance.now();
          const fooElem = document.getElementById('foo');
          function addChild() {
            const newChild = document.createElement('div');
            newChild.innerHTML = '<button>x</button>';
            fooElem.appendChild(newChild);
            if (performance.now() - startTime < 1000) {
              requestAnimationFrame(addChild);
            } else {
              document.close();
            }
          }
          addChild();
        </script>
      </body>
      </html>)HTML");

  base::ElapsedTimer timer;
  int num_batches = 0;

  {
    AccessibilityNotificationWaiter waiter(shell()->web_contents(),
                                           ui::kAXModeComplete,
                                           ax::mojom::Event::kLocationChanged);
    // Run test for 1 second, counting the number of location change events.
    // Number of location change events is used as a measure to count number of
    // serializations.
    while (timer.Elapsed().InMilliseconds() < 1000) {
      std::ignore = waiter.WaitForNotificationWithTimeout(
          base::Milliseconds(1000) - timer.Elapsed());
      ++num_batches;
    }
  }

  // In practice, num_batches lines up nicely with the top end expected,
  // so if kDelayForDeferredUpdatesAfterPageLoad == 150, 6-7 batches are likely.
  EXPECT_GT(num_batches, 1);
  EXPECT_LE(num_batches, 1000 / kDelayForDeferredUpdatesAfterPageLoad + 1);
}
#endif

#if defined(IS_FAST_BUILD)  // Avoid flakiness on slower debug/sanitizer builds.
// TODO(crbug.com/40749521): Fix disabled flaky test.
// TODO(crbug.com/332652840): It is flaky with SkiaGraphite enabled on Windows.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN)
#define MAYBE_DocumentSelectionChangesAreNotBatched
#else
#define MAYBE_DocumentSelectionChangesAreNotBatched
#endif
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       MAYBE_DocumentSelectionChangesAreNotBatched) {
  // Ensure that document selection changes are not batched, and occur faster
  // than once per kDelayForDeferredUpdatesAfterPageLoad.
  LoadInitialAccessibilityTreeFromHtml(R"HTML(
      <!DOCTYPE html>
      <html>
      <body>
        <div id="foo">
        </div>
        <script>
          const startTime = performance.now();
          const fooElem = document.getElementById('foo');
          function addChild() {
            const newChild = document.createElement('div');
            newChild.innerHTML = '<button>x</button>';
            fooElem.appendChild(newChild);
            window.getSelection().selectAllChildren(newChild);
            if (performance.now() - startTime < 1000) {
              requestAnimationFrame(addChild);
            } else {
              document.close();
            }
          }
          addChild();
        </script>
      </body>
      </html>)HTML");

  base::ElapsedTimer timer;
  int num_batches = 0;

  AccessibilityNotificationWaiter waiter(
      shell()->web_contents(), ui::kAXModeComplete,
      ax::mojom::Event::kDocumentSelectionChanged);
  // Run test for 1 second, counting the number of selection changes.
  while (timer.Elapsed().InMilliseconds() < 1000) {
    std::ignore = waiter.WaitForNotificationWithTimeout(
        base::Milliseconds(1000) - timer.Elapsed());
    ++num_batches;
  }

  // In practice, num_batches is about 50 on a fast Linux box.
  EXPECT_GT(num_batches, 1000 / kDelayForDeferredUpdatesAfterPageLoad);
}
#endif  // IS_FAST_BUILD

#if defined(IS_FAST_BUILD)  // Avoid flakiness on slower debug/sanitizer builds.
// TODO(crbug.com/40749521): Fix disabled flaky test.
// TODO(crbug.com/332652840): It is flaky with SkiaGraphite enabled on Windows.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN)
#define MAYBE_ActiveDescendantChangesAreNotBatched
#else
#define MAYBE_ActiveDescendantChangesAreNotBatched
#endif
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       MAYBE_ActiveDescendantChangesAreNotBatched) {
  // Ensure that active descendant changes are not batched, and occur faster
  // than once per kDelayForDeferredUpdatesAfterPageLoad.
  LoadInitialAccessibilityTreeFromHtml(R"HTML(
      <!DOCTYPE html>
      <html>
      <body>
        <div id="foo" tabindex="0" autofocus>
        </div>
        <script>
          const startTime = performance.now();
          const fooElem = document.getElementById('foo');
          let count = 0;
          function addChild() {
            const newChild = document.createElement('div');
            ++count;
            newChild.innerHTML = '<button id=' + count + '>x</button>';
            fooElem.appendChild(newChild);
            fooElem.setAttribute('aria-activedescendant', count);
            if (performance.now() - startTime < 1000) {
              requestAnimationFrame(addChild);
            } else {
              document.close();
            }
          }
          addChild();
        </script>
      </body>
      </html>)HTML");

  base::ElapsedTimer timer;
  int num_batches = 0;

  {
    AccessibilityNotificationWaiter waiter(
        shell()->web_contents(), ui::kAXModeComplete,
        ui::AXEventGenerator::Event::ACTIVE_DESCENDANT_CHANGED);
    // Run test for 1 second, counting the number of active descendant changes.
    while (timer.Elapsed().InMilliseconds() < 1000) {
      std::ignore = waiter.WaitForNotificationWithTimeout(
          base::Milliseconds(1000) - timer.Elapsed());
      ++num_batches;
    }
  }

  // In practice, num_batches is about 50 on a fast Linux box.
  EXPECT_GT(num_batches, 1000 / kDelayForDeferredUpdatesAfterPageLoad);
}
#endif  // IS_FAST_BUILD

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       AccessibilityAddClickListener) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       NavigateInIframe) {}

IN_PROC_BROWSER_TEST_F(
    CrossPlatformAccessibilityBrowserTest,
    SingleSelectionContainerSelectionFollowsFocusWithoutActiveDescendant) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       SingleSelectionContainerFocusSelectsActiveDescendant) {}

IN_PROC_BROWSER_TEST_F(
    CrossPlatformAccessibilityBrowserTest,
    SingleSelectionContainerSelectionFollowsFocusNotSupported) {}

// We do not run this test on Android because only the Java code can change the
// size of the web contents, instead see the associated test in
// WebContentsAccessibilityTest#testBoundingBoxUpdatesOnWindowResize().
// TODO(crbug.com/40918989): Timeout on iOS-Blink
#if BUILDFLAG(IS_ANDROID) || (BUILDFLAG(IS_IOS) && BUILDFLAG(USE_BLINK))
#define MAYBE_FlexBoxBoundingBoxUpdatesOnWindowResize
#else
#define MAYBE_FlexBoxBoundingBoxUpdatesOnWindowResize
#endif
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       MAYBE_FlexBoxBoundingBoxUpdatesOnWindowResize) {}

IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       TestNotificationTextDeletedInTextfield) {}

#if BUILDFLAG(HAS_PLATFORM_ACCESSIBILITY_SUPPORT)
IN_PROC_BROWSER_TEST_F(CrossPlatformAccessibilityBrowserTest,
                       IdDeletedOnNodeRemoval) {}
#endif

class AriaNotifyCrossPlatformAccessibilityBrowserTest
    : public CrossPlatformAccessibilityBrowserTest {};

IN_PROC_BROWSER_TEST_F(AriaNotifyCrossPlatformAccessibilityBrowserTest,
                       TestSingleAriaNotification) {}

IN_PROC_BROWSER_TEST_F(AriaNotifyCrossPlatformAccessibilityBrowserTest,
                       TestConsecutiveCallsToAriaNotify) {}

IN_PROC_BROWSER_TEST_F(AriaNotifyCrossPlatformAccessibilityBrowserTest,
                       TestConsecutiveAriaNotifications) {}

}  // namespace content