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