1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167

content / renderer / accessibility / annotations / ax_main_node_annotator_browsertest.cc [blame]

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

#include "content/renderer/accessibility/annotations/ax_main_node_annotator.h"

#include "base/test/scoped_feature_list.h"
#include "content/renderer/accessibility/annotations/ax_annotators_manager.h"
#include "content/renderer/accessibility/render_accessibility_impl_test.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "services/screen_ai/screen_ai_service_impl.h"
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_tree.h"

using blink::WebAXObject;
using blink::WebDocument;

namespace content {

class MockMainNodeAnnotationService
    : public screen_ai::mojom::Screen2xMainContentExtractor {
 public:
  MockMainNodeAnnotationService() = default;
  MockMainNodeAnnotationService(const MockMainNodeAnnotationService&) = delete;
  MockMainNodeAnnotationService& operator=(
      const MockMainNodeAnnotationService&) = delete;
  ~MockMainNodeAnnotationService() override = default;

  mojo::PendingRemote<screen_ai::mojom::Screen2xMainContentExtractor>
  GetRemote() {
    mojo::PendingRemote<screen_ai::mojom::Screen2xMainContentExtractor> remote;
    receivers_.Add(this, remote.InitWithNewPipeAndPassReceiver());
    return remote;
  }

  void ExtractMainContent(const ::ui::AXTreeUpdate& snapshot,
                          int64_t ukm_source_id,
                          ExtractMainContentCallback callback) override {}

  void ExtractMainNode(const ui::AXTreeUpdate& snapshot,
                       ExtractMainNodeCallback callback) override {
    // For the mock, let any node which contains text be a content node.
    for (ui::AXNodeData node : snapshot.nodes) {
      if (node.HasStringAttribute(ax::mojom::StringAttribute::kName) &&
          node.role == ax::mojom::Role::kStaticText) {
        content_nodes_.push_back(node.id);
      }
    }
    ui::AXTree tree(snapshot);
    main_ = screen_ai::ScreenAIService::ComputeMainNodeForTesting(
        &tree, content_nodes_);
    std::move(callback).Run(main_);
  }

  // Tests should not modify entries in these lists.
  std::vector<ui::AXNodeID> content_nodes_;
  ui::AXNodeID main_ = ui::kInvalidAXNodeID;

 private:
  mojo::ReceiverSet<screen_ai::mojom::Screen2xMainContentExtractor> receivers_;
};

class AXMainNodeAnnotatorTest : public RenderAccessibilityImplTest {
 public:
  AXMainNodeAnnotatorTest() = default;
  AXMainNodeAnnotatorTest(const AXMainNodeAnnotatorTest&) = delete;
  AXMainNodeAnnotatorTest& operator=(const AXMainNodeAnnotatorTest&) = delete;
  ~AXMainNodeAnnotatorTest() override = default;

 protected:
  void SetUp() override {
    scoped_feature_list_.InitAndEnableFeature(features::kMainNodeAnnotations);
    RenderAccessibilityImplTest::SetUp();
    ui::AXMode mode = ui::kAXModeComplete;
    mode.set_mode(ui::AXMode::kAnnotateMainNode, true);
    SetMode(mode);
    auto annotator =
        std::make_unique<AXMainNodeAnnotator>(GetRenderAccessibilityImpl());
    annotator->BindAnnotatorForTesting(mock_annotator_service().GetRemote());
    GetRenderAccessibilityImpl()
        ->ax_annotators_manager_for_testing()
        ->AddAnnotatorForTesting(std::move(annotator));
    task_environment_.RunUntilIdle();
  }

  void TearDown() override {
    GetRenderAccessibilityImpl()
        ->ax_annotators_manager_for_testing()
        ->ClearAnnotatorsForTesting();
    task_environment_.RunUntilIdle();
    RenderAccessibilityImplTest::TearDown();
  }

  MockMainNodeAnnotationService& mock_annotator_service() {
    return mock_annotator_service_;
  }

  void Annotate(const blink::WebDocument& document,
                ui::AXTreeUpdate* update,
                bool load_complete) {
    GetRenderAccessibilityImpl()->ax_annotators_manager_for_testing()->Annotate(
        document, update, load_complete);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  MockMainNodeAnnotationService mock_annotator_service_;
};

TEST_F(AXMainNodeAnnotatorTest, AnnotateMainNode) {
  LoadHTMLAndRefreshAccessibilityTree(R"HTML(
      <body>
        <div>
          <div>Heading</div>
          <div>Paragraph</div>
          <div>Paragraph</div>
        </div>
      </body>
      )HTML");

  // Every time we call a method on a Mojo interface, a message is posted to the
  // current task queue. We need to ask the queue to drain itself before we
  // check test expectations.
  task_environment_.RunUntilIdle();

  EXPECT_EQ(3u, mock_annotator_service().content_nodes_.size());
  EXPECT_NE(ui::kInvalidAXNodeID, mock_annotator_service().main_);

  WebDocument document = GetMainFrame()->GetDocument();
  WebAXObject root = WebAXObject::FromWebDocument(document);
  ASSERT_FALSE(root.IsNull());

  // The expected main node is the div which is the parent of the 3 divs with
  // text.
  WebAXObject expected_main = root.ChildAt(0).ChildAt(0).ChildAt(0);
  ASSERT_FALSE(expected_main.IsNull());

  for (int i = 0; i < 3; i++) {
    ASSERT_EQ(mock_annotator_service().content_nodes_[i],
              expected_main.ChildAt(i).ChildAt(0).AxID());
  }

  // The main node found by the mock annotator should match the id of the
  // expected main.
  ASSERT_EQ(mock_annotator_service().main_, expected_main.AxID());
}

TEST_F(AXMainNodeAnnotatorTest, DoesNothingIfAuthorProvidedNode) {
  LoadHTMLAndRefreshAccessibilityTree(R"HTML(
      <body>
        <div role="main">
          <div>Heading</div>
          <div>Paragraph</div>
          <div>Paragraph</div>
        </div>
      </body>
      )HTML");

  // Expect mock annotator service did not run.
  task_environment_.RunUntilIdle();
  EXPECT_EQ(0u, mock_annotator_service().content_nodes_.size());
  EXPECT_EQ(ui::kInvalidAXNodeID, mock_annotator_service().main_);
}

}  // namespace content