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