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