1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "third_party/blink/renderer/modules/accessibility/testing/accessibility_selection_test.h"
6 
7 #include <algorithm>
8 #include <iterator>
9 
10 #include "base/memory/scoped_refptr.h"
11 #include "third_party/blink/public/platform/file_path_conversion.h"
12 #include "third_party/blink/renderer/core/dom/character_data.h"
13 #include "third_party/blink/renderer/core/dom/container_node.h"
14 #include "third_party/blink/renderer/core/dom/node.h"
15 #include "third_party/blink/renderer/core/editing/frame_selection.h"
16 #include "third_party/blink/renderer/core/editing/position.h"
17 #include "third_party/blink/renderer/core/editing/selection_template.h"
18 #include "third_party/blink/renderer/core/frame/local_frame.h"
19 #include "third_party/blink/renderer/core/html/html_element.h"
20 #include "third_party/blink/renderer/modules/accessibility/ax_object.h"
21 #include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
22 #include "third_party/blink/renderer/modules/accessibility/ax_position.h"
23 #include "third_party/blink/renderer/modules/accessibility/ax_selection.h"
24 #include "third_party/blink/renderer/platform/heap/handle.h"
25 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
26 #include "third_party/blink/renderer/platform/wtf/shared_buffer.h"
27 #include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
28 #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
29 #include "third_party/blink/renderer/platform/wtf/vector.h"
30 
31 namespace blink {
32 namespace {
33 
34 constexpr char kSelectionTestsRelativePath[] = "selection/";
35 constexpr char kTestFileSuffix[] = ".html";
36 constexpr char kLayoutNGSuffix[] = "-ax-layout-ng.txt";
37 constexpr char kLayoutNGDisabledSuffix[] = "-ax-layout-ng-disabled.txt";
38 constexpr char kAXTestExpectationSuffix[] = "-ax.txt";
39 
40 // Serialize accessibility subtree to selection text.
41 // Adds a '^' at the selection anchor offset and a '|' at the focus offset.
42 class AXSelectionSerializer final {
43   STACK_ALLOCATED();
44 
45  public:
AXSelectionSerializer(const AXSelection & selection)46   explicit AXSelectionSerializer(const AXSelection& selection)
47       : tree_level_(0), selection_(selection) {}
48   ~AXSelectionSerializer() = default;
49 
Serialize(const AXObject & subtree)50   std::string Serialize(const AXObject& subtree) {
51     if (!selection_.IsValid())
52       return {};
53     SerializeSubtree(subtree);
54     DCHECK_EQ(tree_level_, 0);
55     return builder_.ToString().Utf8();
56   }
57 
58  private:
HandleTextObject(const AXObject & text_object)59   void HandleTextObject(const AXObject& text_object) {
60     builder_.Append('<');
61     builder_.Append(AXObject::InternalRoleName(text_object.RoleValue()));
62     builder_.Append(": ");
63     const String name = text_object.ComputedName() + ">\n";
64     const AXObject& base_container = *selection_.Base().ContainerObject();
65     const AXObject& extent_container = *selection_.Extent().ContainerObject();
66 
67     if (base_container == text_object && extent_container == text_object) {
68       DCHECK(selection_.Base().IsTextPosition() &&
69              selection_.Extent().IsTextPosition());
70       const int base_offset = selection_.Base().TextOffset();
71       const int extent_offset = selection_.Extent().TextOffset();
72 
73       if (base_offset == extent_offset) {
74         builder_.Append(name.Left(base_offset));
75         builder_.Append('|');
76         builder_.Append(name.Substring(base_offset));
77         return;
78       }
79 
80       if (base_offset < extent_offset) {
81         builder_.Append(name.Left(base_offset));
82         builder_.Append('^');
83         builder_.Append(
84             name.Substring(base_offset, extent_offset - base_offset));
85         builder_.Append('|');
86         builder_.Append(name.Substring(extent_offset));
87         return;
88       }
89 
90       builder_.Append(name.Left(extent_offset));
91       builder_.Append('|');
92       builder_.Append(
93           name.Substring(extent_offset, base_offset - extent_offset));
94       builder_.Append('^');
95       builder_.Append(name.Substring(base_offset));
96       return;
97     }
98 
99     if (base_container == text_object) {
100       DCHECK(selection_.Base().IsTextPosition());
101       const int base_offset = selection_.Base().TextOffset();
102 
103       builder_.Append(name.Left(base_offset));
104       builder_.Append('^');
105       builder_.Append(name.Substring(base_offset));
106       return;
107     }
108 
109     if (extent_container == text_object) {
110       DCHECK(selection_.Extent().IsTextPosition());
111       const int extent_offset = selection_.Extent().TextOffset();
112 
113       builder_.Append(name.Left(extent_offset));
114       builder_.Append('|');
115       builder_.Append(name.Substring(extent_offset));
116       return;
117     }
118 
119     builder_.Append(name);
120   }
121 
HandleObject(const AXObject & object)122   void HandleObject(const AXObject& object) {
123     builder_.Append('<');
124     builder_.Append(AXObject::InternalRoleName(object.RoleValue()));
125 
126     String name = object.ComputedName();
127     if (name.length()) {
128       builder_.Append(": ");
129       builder_.Append(name);
130     }
131 
132     builder_.Append(">\n");
133     SerializeSubtree(object);
134   }
135 
HandleSelection(const AXPosition & position)136   void HandleSelection(const AXPosition& position) {
137     if (!position.IsValid())
138       return;
139 
140     if (selection_.Extent() == position) {
141       builder_.Append('|');
142       return;
143     }
144 
145     if (selection_.Base() != position)
146       return;
147 
148     builder_.Append('^');
149   }
150 
SerializeSubtree(const AXObject & subtree)151   void SerializeSubtree(const AXObject& subtree) {
152     if (!subtree.ChildCountIncludingIgnored()) {
153       // Though they are in this particular case both equivalent to an "after
154       // object" position, "Before children" and "after children" positions are
155       // still valid within empty subtrees.
156       const auto position = AXPosition::CreateFirstPositionInObject(subtree);
157       HandleSelection(position);
158       return;
159     }
160 
161     for (const AXObject* child : subtree.ChildrenIncludingIgnored()) {
162       DCHECK(child);
163       const auto position = AXPosition::CreatePositionBeforeObject(*child);
164       HandleSelection(position);
165       ++tree_level_;
166       builder_.Append(String::FromUTF8(std::string(tree_level_ * 2, '+')));
167       if (position.IsTextPosition()) {
168         HandleTextObject(*child);
169       } else {
170         HandleObject(*child);
171       }
172       --tree_level_;
173     }
174 
175     // Handle any "after children" positions.
176     HandleSelection(AXPosition::CreateLastPositionInObject(subtree));
177   }
178 
179   StringBuilder builder_;
180   int tree_level_;
181   AXSelection selection_;
182 };
183 
184 // Deserializes an HTML snippet with or without selection markers to an
185 // |AXSelection| object. A '^' could be present at the selection anchor offset
186 // and a '|' at the focus offset. If multiple markers are present, the
187 // deserializer will return multiple |AXSelection| objects. If there are
188 // multiple markers, the first '|' in DOM order will be matched with the first
189 // '^' marker, the second '|' with the second '^', and so on. If there are more
190 // '|'s than '^'s or vice versa, the deserializer will DCHECK. If there are no
191 // markers, no |AXSelection| objects will be returned. We don't allow '^' and
192 // '|' markers to appear in anything other than the contents of an HTML node,
193 // e.g. they are not permitted in aria-labels.
194 class AXSelectionDeserializer final {
195   STACK_ALLOCATED();
196 
197  public:
AXSelectionDeserializer(AXObjectCacheImpl & cache)198   explicit AXSelectionDeserializer(AXObjectCacheImpl& cache)
199       : ax_object_cache_(&cache),
200         anchors_(MakeGarbageCollected<VectorOfPairs<Node, int>>()),
201         foci_(MakeGarbageCollected<VectorOfPairs<Node, int>>()) {}
202   ~AXSelectionDeserializer() = default;
203 
204   // Creates an accessibility tree rooted at the given HTML element from the
205   // provided HTML snippet and returns |AXSelection| objects that can select the
206   // parts of the tree indicated by the selection markers in the snippet.
Deserialize(const std::string & html_snippet,HTMLElement & element)207   const Vector<AXSelection> Deserialize(const std::string& html_snippet,
208                                         HTMLElement& element) {
209     element.setInnerHTML(String::FromUTF8(html_snippet));
210     element.GetDocument().View()->UpdateAllLifecyclePhasesForTest();
211     AXObject* root = ax_object_cache_->GetOrCreate(&element);
212     if (!root || root->IsDetached())
213       return {};
214 
215     FindSelectionMarkers(*root);
216     DCHECK((foci_->size() == 1 && anchors_->size() == 0) ||
217            anchors_->size() == foci_->size())
218         << "There should be an equal number of '^'s and '|'s in the HTML that "
219            "is being deserialized, or if caret placement is required, only a "
220            "single '|'.";
221     if (foci_->IsEmpty())
222       return {};
223 
224     Vector<AXSelection> ax_selections;
225     if (anchors_->IsEmpty()) {
226       // Handle the case when there is just a single '|' marker representing the
227       // position of the caret.
228       DCHECK(foci_->at(0).first);
229       const Position caret(foci_->at(0).first, foci_->at(0).second);
230       const auto ax_caret = AXPosition::FromPosition(caret);
231       AXSelection::Builder builder;
232       ax_selections.push_back(
233           builder.SetBase(ax_caret).SetExtent(ax_caret).Build());
234       return ax_selections;
235     }
236 
237     for (size_t i = 0; i < foci_->size(); ++i) {
238       DCHECK(anchors_->at(i).first);
239       const Position base(*anchors_->at(i).first, anchors_->at(i).second);
240       const auto ax_base = AXPosition::FromPosition(base);
241 
242       DCHECK(foci_->at(i).first);
243       const Position extent(*foci_->at(i).first, foci_->at(i).second);
244       const auto ax_extent = AXPosition::FromPosition(extent);
245       AXSelection::Builder builder;
246       ax_selections.push_back(
247           builder.SetBase(ax_base).SetExtent(ax_extent).Build());
248     }
249 
250     return ax_selections;
251   }
252 
253  private:
HandleCharacterData(const AXObject & text_object)254   void HandleCharacterData(const AXObject& text_object) {
255     auto* const node = To<CharacterData>(text_object.GetNode());
256     Vector<int> base_offsets;
257     Vector<int> extent_offsets;
258     unsigned number_of_markers = 0;
259     StringBuilder builder;
260     for (unsigned i = 0; i < node->length(); ++i) {
261       const UChar character = node->data()[i];
262       if (character == '^') {
263         base_offsets.push_back(static_cast<int>(i - number_of_markers));
264         ++number_of_markers;
265         continue;
266       }
267 
268       if (character == '|') {
269         extent_offsets.push_back(static_cast<int>(i - number_of_markers));
270         ++number_of_markers;
271         continue;
272       }
273 
274       builder.Append(character);
275     }
276 
277     if (base_offsets.IsEmpty() && extent_offsets.IsEmpty())
278       return;
279 
280     // Remove the markers, otherwise they would be duplicated if the AXSelection
281     // is re-serialized.
282     node->setData(builder.ToString());
283     node->GetDocument().View()->UpdateAllLifecyclePhasesForTest();
284 
285     //
286     // Non-text selection.
287     //
288 
289     if (node->ContainsOnlyWhitespaceOrEmpty()) {
290       // Since the text object contains only selection markers, this indicates
291       // that this is a request for a non-text selection.
292       Node* const parent = node->ParentOrShadowHostNode();
293       int index_in_parent = static_cast<int>(node->NodeIndex());
294 
295       for (size_t i = 0; i < base_offsets.size(); ++i)
296         anchors_->emplace_back(parent, index_in_parent);
297 
298       for (size_t i = 0; i < extent_offsets.size(); ++i)
299         foci_->emplace_back(parent, index_in_parent);
300 
301       return;
302     }
303 
304     //
305     // Text selection.
306     //
307 
308     for (int base_offset : base_offsets)
309       anchors_->emplace_back(node, base_offset);
310     for (int extent_offset : extent_offsets)
311       foci_->emplace_back(node, extent_offset);
312   }
313 
HandleObject(const AXObject & object)314   void HandleObject(const AXObject& object) {
315     for (const AXObject* child : object.ChildrenIncludingIgnored()) {
316       DCHECK(child);
317       FindSelectionMarkers(*child);
318     }
319   }
320 
FindSelectionMarkers(const AXObject & root)321   void FindSelectionMarkers(const AXObject& root) {
322     const Node* node = root.GetNode();
323     if (node && node->IsCharacterDataNode()) {
324       HandleCharacterData(root);
325       // |root| will need to be detached and replaced with an updated AXObject.
326       return;
327     }
328     HandleObject(root);
329   }
330 
331   Persistent<AXObjectCacheImpl> const ax_object_cache_;
332 
333   // Pairs of anchor nodes + anchor offsets.
334   Persistent<VectorOfPairs<Node, int>> anchors_;
335 
336   // Pairs of focus nodes + focus offsets.
337   Persistent<VectorOfPairs<Node, int>> foci_;
338 };
339 
340 }  // namespace
341 
AccessibilitySelectionTest(LocalFrameClient * local_frame_client)342 AccessibilitySelectionTest::AccessibilitySelectionTest(
343     LocalFrameClient* local_frame_client)
344     : AccessibilityTest(local_frame_client) {}
345 
SetUp()346 void AccessibilitySelectionTest::SetUp() {
347   AccessibilityTest::SetUp();
348   RuntimeEnabledFeatures::SetAccessibilityExposeHTMLElementEnabled(true);
349 }
350 
GetCurrentSelectionText() const351 std::string AccessibilitySelectionTest::GetCurrentSelectionText() const {
352   const SelectionInDOMTree selection =
353       GetFrame().Selection().GetSelectionInDOMTree();
354   const auto ax_selection = AXSelection::FromSelection(selection);
355   return GetSelectionText(ax_selection);
356 }
357 
GetSelectionText(const AXSelection & selection) const358 std::string AccessibilitySelectionTest::GetSelectionText(
359     const AXSelection& selection) const {
360   const AXObject* root = GetAXRootObject();
361   if (!root || root->IsDetached())
362     return {};
363   return AXSelectionSerializer(selection).Serialize(*root);
364 }
365 
GetSelectionText(const AXSelection & selection,const AXObject & subtree) const366 std::string AccessibilitySelectionTest::GetSelectionText(
367     const AXSelection& selection,
368     const AXObject& subtree) const {
369   return AXSelectionSerializer(selection).Serialize(subtree);
370 }
371 
SetSelectionText(const std::string & selection_text) const372 AXSelection AccessibilitySelectionTest::SetSelectionText(
373     const std::string& selection_text) const {
374   HTMLElement* body = GetDocument().body();
375   if (!body)
376     return AXSelection::Builder().Build();
377   const Vector<AXSelection> ax_selections =
378       AXSelectionDeserializer(GetAXObjectCache())
379           .Deserialize(selection_text, *body);
380   if (ax_selections.IsEmpty())
381     return AXSelection::Builder().Build();
382   return ax_selections.front();
383 }
384 
SetSelectionText(const std::string & selection_text,HTMLElement & element) const385 AXSelection AccessibilitySelectionTest::SetSelectionText(
386     const std::string& selection_text,
387     HTMLElement& element) const {
388   const Vector<AXSelection> ax_selections =
389       AXSelectionDeserializer(GetAXObjectCache())
390           .Deserialize(selection_text, element);
391   if (ax_selections.IsEmpty())
392     return AXSelection::Builder().Build();
393   return ax_selections.front();
394 }
395 
RunSelectionTest(const std::string & test_name,const std::string & suffix) const396 void AccessibilitySelectionTest::RunSelectionTest(
397     const std::string& test_name,
398     const std::string& suffix) const {
399   static const std::string separator_line = '\n' + std::string(80, '=') + '\n';
400   const String relative_path = String::FromUTF8(kSelectionTestsRelativePath) +
401                                String::FromUTF8(test_name);
402   const String test_path = test::AccessibilityTestDataPath(relative_path);
403 
404   const String test_file = test_path + String::FromUTF8(kTestFileSuffix);
405   scoped_refptr<SharedBuffer> test_file_buffer = test::ReadFromFile(test_file);
406   auto test_file_chars = test_file_buffer->CopyAs<Vector<char>>();
407   std::string test_file_contents;
408   std::copy(test_file_chars.begin(), test_file_chars.end(),
409             std::back_inserter(test_file_contents));
410   ASSERT_FALSE(test_file_contents.empty())
411       << "Test file cannot be empty.\n"
412       << test_file.Utf8()
413       << "\nDid you forget to add a data dependency to the BUILD file?";
414 
415   const String ax_file =
416       test_path +
417       String::FromUTF8(suffix.empty() ? kAXTestExpectationSuffix : suffix);
418   scoped_refptr<SharedBuffer> ax_file_buffer = test::ReadFromFile(ax_file);
419   auto ax_file_chars = ax_file_buffer->CopyAs<Vector<char>>();
420   std::string ax_file_contents;
421   std::copy(ax_file_chars.begin(), ax_file_chars.end(),
422             std::back_inserter(ax_file_contents));
423   ASSERT_FALSE(ax_file_contents.empty())
424       << "Expectations file cannot be empty.\n"
425       << ax_file.Utf8()
426       << "\nDid you forget to add a data dependency to the BUILD file?";
427 
428   HTMLElement* body = GetDocument().body();
429   ASSERT_NE(nullptr, body);
430   Vector<AXSelection> ax_selections =
431       AXSelectionDeserializer(GetAXObjectCache())
432           .Deserialize(test_file_contents, *body);
433   std::string actual_ax_file_contents;
434 
435   for (auto& ax_selection : ax_selections) {
436     ax_selection.Select();
437     actual_ax_file_contents += separator_line;
438     actual_ax_file_contents += ax_selection.ToString().Utf8();
439     actual_ax_file_contents += separator_line;
440     actual_ax_file_contents += GetCurrentSelectionText();
441   }
442 
443   EXPECT_EQ(ax_file_contents, actual_ax_file_contents);
444 
445   // Uncomment these lines to write the output to the expectations file.
446   // TODO(dmazzoni): make this a command-line parameter.
447   // if (ax_file_contents != actual_ax_file_contents)
448   //  base::WriteFile(WebStringToFilePath(ax_file), actual_ax_file_contents);
449 }
450 
451 ParameterizedAccessibilitySelectionTest::
ParameterizedAccessibilitySelectionTest(LocalFrameClient * local_frame_client)452     ParameterizedAccessibilitySelectionTest(
453         LocalFrameClient* local_frame_client)
454     : ScopedLayoutNGForTest(GetParam()),
455       AccessibilitySelectionTest(local_frame_client) {}
456 
RunSelectionTest(const std::string & test_name) const457 void ParameterizedAccessibilitySelectionTest::RunSelectionTest(
458     const std::string& test_name) const {
459   std::string suffix =
460       LayoutNGEnabled() ? kLayoutNGSuffix : kLayoutNGDisabledSuffix;
461   AccessibilitySelectionTest::RunSelectionTest(test_name, suffix);
462 }
463 
464 }  // namespace blink
465