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/ax_position.h"
6
7 #include "testing/gtest/include/gtest/gtest.h"
8 #include "third_party/blink/renderer/core/dom/element.h"
9 #include "third_party/blink/renderer/core/dom/node.h"
10 #include "third_party/blink/renderer/core/editing/position.h"
11 #include "third_party/blink/renderer/core/editing/text_affinity.h"
12 #include "third_party/blink/renderer/core/html/html_element.h"
13 #include "third_party/blink/renderer/modules/accessibility/ax_object.h"
14 #include "third_party/blink/renderer/modules/accessibility/testing/accessibility_test.h"
15 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
16
17 namespace blink {
18 namespace test {
19
20 namespace {
21
22 constexpr char kCSSBeforeAndAfter[] = R"HTML(
23 <style>
24 q::before {
25 content: "«";
26 color: blue;
27 }
28 q::after {
29 content: "»";
30 color: red;
31 }
32 </style>
33 <q id="quote">Hello there,</q> she said.
34 )HTML";
35
36 constexpr char kHTMLTable[] = R"HTML(
37 <p id="before">Before table.</p>
38 <table id="table" border="1">
39 <thead id="thead">
40 <tr id="headerRow">
41 <th id="firstHeaderCell">Number</th>
42 <th>Month</th>
43 <th id="lastHeaderCell">Expenses</th>
44 </tr>
45 </thead>
46 <tbody id="tbody">
47 <tr id="firstRow">
48 <th id="firstCell">1</th>
49 <td>Jan</td>
50 <td>100</td>
51 </tr>
52 <tr>
53 <th>2</th>
54 <td>Feb</td>
55 <td>150</td>
56 </tr>
57 <tr id="lastRow">
58 <th>3</th>
59 <td>Mar</td>
60 <td id="lastCell">200</td>
61 </tr>
62 </tbody>
63 </table>
64 <p id="after">After table.</p>
65 )HTML";
66
67 constexpr char kAOM[] = R"HTML(
68 <p id="before">Before virtual AOM node.</p>
69 <div id="aomParent"></div>
70 <p id="after">After virtual AOM node.</p>
71 <script>
72 let parent = document.getElementById("aomParent");
73 let node = MakeGarbageCollected<AccessibleNode>();
74 node.role = "button";
75 node.label = "Button";
76 parent.accessibleNode.appendChild(node);
77 </script>
78 )HTML";
79
80 constexpr char kMap[] = R"HTML(
81 <br id="br">
82 <map id="map">
83 <area shape="rect" coords="0,0,10,10" href="about:blank">
84 </map>
85 )HTML";
86 } // namespace
87
88 //
89 // Basic tests.
90 //
91
TEST_F(AccessibilityTest,PositionInText)92 TEST_F(AccessibilityTest, PositionInText) {
93 SetBodyInnerHTML(R"HTML(<p id="paragraph">Hello</p>)HTML");
94 const Node* text = GetElementById("paragraph")->firstChild();
95 ASSERT_NE(nullptr, text);
96 ASSERT_TRUE(text->IsTextNode());
97 const AXObject* ax_static_text =
98 GetAXObjectByElementId("paragraph")->FirstChildIncludingIgnored();
99 ASSERT_NE(nullptr, ax_static_text);
100 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
101
102 const auto ax_position =
103 AXPosition::CreatePositionInTextObject(*ax_static_text, 3);
104 const auto position = ax_position.ToPositionWithAffinity();
105 EXPECT_EQ(text, position.AnchorNode());
106 EXPECT_EQ(3, position.GetPosition().OffsetInContainerNode());
107
108 const auto ax_position_from_dom = AXPosition::FromPosition(position);
109 EXPECT_EQ(ax_position, ax_position_from_dom);
110 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
111 }
112
113 // To prevent surprises when comparing equality of two |AXPosition|s, position
114 // before text object should be the same as position in text object at offset 0.
TEST_F(AccessibilityTest,PositionBeforeText)115 TEST_F(AccessibilityTest, PositionBeforeText) {
116 SetBodyInnerHTML(R"HTML(<p id="paragraph">Hello</p>)HTML");
117 const Node* text = GetElementById("paragraph")->firstChild();
118 ASSERT_NE(nullptr, text);
119 ASSERT_TRUE(text->IsTextNode());
120 const AXObject* ax_static_text =
121 GetAXObjectByElementId("paragraph")->FirstChildIncludingIgnored();
122 ASSERT_NE(nullptr, ax_static_text);
123 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
124
125 const auto ax_position =
126 AXPosition::CreatePositionBeforeObject(*ax_static_text);
127 const auto position = ax_position.ToPositionWithAffinity();
128 EXPECT_EQ(text, position.AnchorNode());
129 EXPECT_EQ(0, position.GetPosition().OffsetInContainerNode());
130
131 const auto ax_position_from_dom = AXPosition::FromPosition(position);
132 EXPECT_EQ(ax_position, ax_position_from_dom);
133 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
134 }
135
TEST_F(AccessibilityTest,PositionBeforeTextWithFirstLetterCSSRule)136 TEST_F(AccessibilityTest, PositionBeforeTextWithFirstLetterCSSRule) {
137 SetBodyInnerHTML(
138 R"HTML(<style>p ::first-letter { color: red; font-size: 200%; }</style>
139 <p id="paragraph">Hello</p>)HTML");
140 const Node* text = GetElementById("paragraph")->firstChild();
141 ASSERT_NE(nullptr, text);
142 ASSERT_TRUE(text->IsTextNode());
143 const AXObject* ax_static_text =
144 GetAXObjectByElementId("paragraph")->FirstChildIncludingIgnored();
145 ASSERT_NE(nullptr, ax_static_text);
146 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
147
148 const auto ax_position =
149 AXPosition::CreatePositionBeforeObject(*ax_static_text);
150 const auto position = ax_position.ToPositionWithAffinity();
151 EXPECT_EQ(text, position.AnchorNode());
152 EXPECT_EQ(0, position.GetPosition().OffsetInContainerNode());
153
154 const auto ax_position_from_dom = AXPosition::FromPosition(position);
155 EXPECT_EQ(ax_position, ax_position_from_dom);
156 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
157 }
158
159 // To prevent surprises when comparing equality of two |AXPosition|s, position
160 // after text object should be the same as position in text object at offset
161 // text length.
TEST_F(AccessibilityTest,PositionAfterText)162 TEST_F(AccessibilityTest, PositionAfterText) {
163 SetBodyInnerHTML(R"HTML(<p id="paragraph">Hello</p>)HTML");
164 const Node* text = GetElementById("paragraph")->firstChild();
165 ASSERT_NE(nullptr, text);
166 ASSERT_TRUE(text->IsTextNode());
167 const AXObject* ax_static_text =
168 GetAXObjectByElementId("paragraph")->FirstChildIncludingIgnored();
169 ASSERT_NE(nullptr, ax_static_text);
170 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
171
172 const auto ax_position =
173 AXPosition::CreatePositionAfterObject(*ax_static_text);
174 const auto position = ax_position.ToPositionWithAffinity();
175 EXPECT_EQ(text, position.AnchorNode());
176 EXPECT_EQ(5, position.GetPosition().OffsetInContainerNode());
177
178 const auto ax_position_from_dom = AXPosition::FromPosition(position);
179 EXPECT_EQ(ax_position, ax_position_from_dom);
180 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
181 }
182
TEST_F(AccessibilityTest,PositionBeforeLineBreak)183 TEST_F(AccessibilityTest, PositionBeforeLineBreak) {
184 SetBodyInnerHTML(R"HTML(Hello<br id="br">there)HTML");
185 const AXObject* ax_br = GetAXObjectByElementId("br");
186 ASSERT_NE(nullptr, ax_br);
187 ASSERT_EQ(ax::mojom::Role::kLineBreak, ax_br->RoleValue());
188 const AXObject* ax_div = ax_br->ParentObjectUnignored();
189 ASSERT_NE(nullptr, ax_div);
190 ASSERT_EQ(ax::mojom::Role::kGenericContainer, ax_div->RoleValue());
191
192 const auto ax_position = AXPosition::CreatePositionBeforeObject(*ax_br);
193 EXPECT_FALSE(ax_position.IsTextPosition());
194 EXPECT_EQ(ax_div, ax_position.ContainerObject());
195 EXPECT_EQ(1, ax_position.ChildIndex());
196 EXPECT_EQ(ax_br, ax_position.ChildAfterTreePosition());
197
198 const auto position = ax_position.ToPositionWithAffinity();
199 EXPECT_EQ(GetDocument().body(), position.AnchorNode());
200 EXPECT_EQ(1, position.GetPosition().OffsetInContainerNode());
201
202 const auto ax_position_from_dom = AXPosition::FromPosition(position);
203 EXPECT_EQ(ax_position, ax_position_from_dom);
204 }
205
TEST_F(AccessibilityTest,PositionAfterLineBreak)206 TEST_F(AccessibilityTest, PositionAfterLineBreak) {
207 SetBodyInnerHTML(R"HTML(Hello<br id="br">there)HTML");
208 const AXObject* ax_br = GetAXObjectByElementId("br");
209 ASSERT_NE(nullptr, ax_br);
210 ASSERT_EQ(ax::mojom::Role::kLineBreak, ax_br->RoleValue());
211 const AXObject* ax_static_text =
212 GetAXRootObject()->DeepestLastChildIncludingIgnored();
213 ASSERT_NE(nullptr, ax_static_text);
214 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
215
216 const auto ax_position = AXPosition::CreatePositionAfterObject(*ax_br);
217 EXPECT_EQ(ax_static_text, ax_position.ContainerObject());
218 EXPECT_TRUE(ax_position.IsTextPosition());
219 EXPECT_EQ(0, ax_position.TextOffset());
220
221 const auto position = ax_position.ToPositionWithAffinity();
222 EXPECT_EQ(ax_static_text->GetNode(), position.AnchorNode());
223 EXPECT_TRUE(position.GetPosition().IsOffsetInAnchor());
224 EXPECT_EQ(0, position.GetPosition().OffsetInContainerNode());
225
226 const auto ax_position_from_dom = AXPosition::FromPosition(position);
227 EXPECT_EQ(ax_position, ax_position_from_dom);
228 }
229
TEST_F(AccessibilityTest,FirstPositionInDivContainer)230 TEST_F(AccessibilityTest, FirstPositionInDivContainer) {
231 SetBodyInnerHTML(R"HTML(<div id="div">Hello<br>there</div>)HTML");
232 const Element* div = GetElementById("div");
233 ASSERT_NE(nullptr, div);
234 const AXObject* ax_div = GetAXObjectByElementId("div");
235 ASSERT_NE(nullptr, ax_div);
236 ASSERT_EQ(ax::mojom::Role::kGenericContainer, ax_div->RoleValue());
237 const AXObject* ax_static_text =
238 GetAXRootObject()->DeepestFirstChildIncludingIgnored();
239 ASSERT_NE(nullptr, ax_static_text);
240 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
241
242 // "Before object" positions that are anchored to before a text object are
243 // always converted to a "text position" before the object's first unignored
244 // character.
245 const auto ax_position = AXPosition::CreateFirstPositionInObject(*ax_div);
246 const auto position = ax_position.ToPositionWithAffinity();
247 EXPECT_EQ(div->firstChild(), position.AnchorNode());
248 EXPECT_TRUE(position.GetPosition().IsOffsetInAnchor());
249 EXPECT_EQ(0, position.GetPosition().OffsetInContainerNode());
250
251 const auto ax_position_from_dom = AXPosition::FromPosition(position);
252 EXPECT_EQ(ax_position, ax_position_from_dom);
253 EXPECT_TRUE(ax_position_from_dom.IsTextPosition());
254 EXPECT_EQ(ax_static_text, ax_position_from_dom.ContainerObject());
255 EXPECT_EQ(0, ax_position_from_dom.TextOffset());
256 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
257 }
258
TEST_F(AccessibilityTest,LastPositionInDivContainer)259 TEST_F(AccessibilityTest, LastPositionInDivContainer) {
260 SetBodyInnerHTML(R"HTML(<div id="div">Hello<br>there</div>
261 <div>Next div</div>)HTML");
262 const Element* div = GetElementById("div");
263 ASSERT_NE(nullptr, div);
264 const AXObject* ax_div = GetAXObjectByElementId("div");
265 ASSERT_NE(nullptr, ax_div);
266 ASSERT_EQ(ax::mojom::Role::kGenericContainer, ax_div->RoleValue());
267
268 const auto ax_position = AXPosition::CreateLastPositionInObject(*ax_div);
269 const auto position = ax_position.ToPositionWithAffinity();
270 EXPECT_EQ(div, position.AnchorNode());
271 EXPECT_TRUE(position.GetPosition().IsAfterChildren());
272
273 const auto ax_position_from_dom = AXPosition::FromPosition(position);
274 EXPECT_EQ(ax_position, ax_position_from_dom);
275 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
276 }
277
TEST_F(AccessibilityTest,FirstPositionInTextContainer)278 TEST_F(AccessibilityTest, FirstPositionInTextContainer) {
279 SetBodyInnerHTML(R"HTML(<div id="div">Hello</div>)HTML");
280 const Node* text = GetElementById("div")->firstChild();
281 ASSERT_NE(nullptr, text);
282 ASSERT_TRUE(text->IsTextNode());
283 const AXObject* ax_static_text =
284 GetAXObjectByElementId("div")->FirstChildIncludingIgnored();
285 ASSERT_NE(nullptr, ax_static_text);
286 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
287
288 const auto ax_position =
289 AXPosition::CreateFirstPositionInObject(*ax_static_text);
290 const auto position = ax_position.ToPositionWithAffinity();
291 EXPECT_EQ(text, position.AnchorNode());
292 EXPECT_EQ(0, position.GetPosition().OffsetInContainerNode());
293
294 const auto ax_position_from_dom = AXPosition::FromPosition(position);
295 EXPECT_EQ(ax_position, ax_position_from_dom);
296 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
297 }
298
TEST_F(AccessibilityTest,LastPositionInTextContainer)299 TEST_F(AccessibilityTest, LastPositionInTextContainer) {
300 SetBodyInnerHTML(R"HTML(<div id="div">Hello</div>)HTML");
301 const Node* text = GetElementById("div")->lastChild();
302 ASSERT_NE(nullptr, text);
303 ASSERT_TRUE(text->IsTextNode());
304 const AXObject* ax_static_text =
305 GetAXObjectByElementId("div")->LastChildIncludingIgnored();
306 ASSERT_NE(nullptr, ax_static_text);
307 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
308
309 const auto ax_position =
310 AXPosition::CreateLastPositionInObject(*ax_static_text);
311 const auto position = ax_position.ToPositionWithAffinity();
312 EXPECT_EQ(text, position.AnchorNode());
313 EXPECT_EQ(5, position.GetPosition().OffsetInContainerNode());
314
315 const auto ax_position_from_dom = AXPosition::FromPosition(position);
316 EXPECT_EQ(ax_position, ax_position_from_dom);
317 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
318 }
319
320 //
321 // Test comparing two AXPosition objects based on their position in the
322 // accessibility tree.
323 //
324
TEST_F(AccessibilityTest,AXPositionComparisonOperators)325 TEST_F(AccessibilityTest, AXPositionComparisonOperators) {
326 SetBodyInnerHTML(R"HTML(<input id="input" type="text" value="value">
327 <p id="paragraph">hello<br>there</p>)HTML");
328
329 const AXObject* body = GetAXBodyObject();
330 ASSERT_NE(nullptr, body);
331 const auto root_first = AXPosition::CreateFirstPositionInObject(*body);
332 const auto root_last = AXPosition::CreateLastPositionInObject(*body);
333
334 const AXObject* input = GetAXObjectByElementId("input");
335 ASSERT_NE(nullptr, input);
336 const auto input_before = AXPosition::CreatePositionBeforeObject(*input);
337 const auto input_after = AXPosition::CreatePositionAfterObject(*input);
338
339 const AXObject* paragraph = GetAXObjectByElementId("paragraph");
340 ASSERT_NE(nullptr, paragraph);
341 ASSERT_NE(nullptr, paragraph->FirstChildIncludingIgnored());
342 ASSERT_NE(nullptr, paragraph->LastChildIncludingIgnored());
343 const auto paragraph_before = AXPosition::CreatePositionBeforeObject(
344 *paragraph->FirstChildIncludingIgnored());
345 const auto paragraph_after = AXPosition::CreatePositionAfterObject(
346 *paragraph->LastChildIncludingIgnored());
347 const auto paragraph_start = AXPosition::CreatePositionInTextObject(
348 *paragraph->FirstChildIncludingIgnored(), 0);
349 const auto paragraph_end = AXPosition::CreatePositionInTextObject(
350 *paragraph->LastChildIncludingIgnored(), 5);
351
352 EXPECT_TRUE(root_first == root_first);
353 EXPECT_TRUE(root_last == root_last);
354 EXPECT_FALSE(root_first != root_first);
355 EXPECT_TRUE(root_first != root_last);
356
357 EXPECT_TRUE(root_first < root_last);
358 EXPECT_TRUE(root_first <= root_first);
359 EXPECT_TRUE(root_last > root_first);
360 EXPECT_TRUE(root_last >= root_last);
361
362 EXPECT_TRUE(input_before == root_first);
363 EXPECT_TRUE(input_after > root_first);
364 EXPECT_TRUE(input_after >= root_first);
365 EXPECT_FALSE(input_before < root_first);
366 EXPECT_TRUE(input_before <= root_first);
367
368 //
369 // Text positions.
370 //
371
372 EXPECT_TRUE(paragraph_before == paragraph_start);
373 EXPECT_TRUE(paragraph_after == paragraph_end);
374 EXPECT_TRUE(paragraph_start < paragraph_end);
375 }
376
TEST_F(AccessibilityTest,AXPositionOperatorBool)377 TEST_F(AccessibilityTest, AXPositionOperatorBool) {
378 SetBodyInnerHTML(R"HTML(Hello)HTML");
379 const AXObject* root = GetAXRootObject();
380 ASSERT_NE(nullptr, root);
381 const auto root_first = AXPosition::CreateFirstPositionInObject(*root);
382 EXPECT_TRUE(static_cast<bool>(root_first));
383 // The following should create an after children position on the root so it
384 // should be valid.
385 EXPECT_TRUE(static_cast<bool>(root_first.CreateNextPosition()));
386 EXPECT_FALSE(static_cast<bool>(root_first.CreatePreviousPosition()));
387 }
388
389 //
390 // Test converting to and from visible text with white space.
391 // The accessibility tree is based on visible text with white space compressed,
392 // vs. the DOM tree where white space is preserved.
393 //
394
TEST_F(AccessibilityTest,PositionInTextWithWhiteSpace)395 TEST_F(AccessibilityTest, PositionInTextWithWhiteSpace) {
396 SetBodyInnerHTML(R"HTML(<p id="paragraph"> Hello </p>)HTML");
397 const Node* text = GetElementById("paragraph")->firstChild();
398 ASSERT_NE(nullptr, text);
399 ASSERT_TRUE(text->IsTextNode());
400 const AXObject* ax_static_text =
401 GetAXObjectByElementId("paragraph")->FirstChildIncludingIgnored();
402 ASSERT_NE(nullptr, ax_static_text);
403 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
404
405 const auto ax_position =
406 AXPosition::CreatePositionInTextObject(*ax_static_text, 3);
407 const auto position = ax_position.ToPositionWithAffinity();
408 EXPECT_EQ(text, position.AnchorNode());
409 EXPECT_EQ(8, position.GetPosition().OffsetInContainerNode());
410
411 const auto ax_position_from_dom = AXPosition::FromPosition(position);
412 EXPECT_EQ(ax_position, ax_position_from_dom);
413 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
414 }
415
TEST_F(AccessibilityTest,PositionBeforeTextWithWhiteSpace)416 TEST_F(AccessibilityTest, PositionBeforeTextWithWhiteSpace) {
417 SetBodyInnerHTML(R"HTML(<p id="paragraph"> Hello </p>)HTML");
418 const Node* text = GetElementById("paragraph")->firstChild();
419 ASSERT_NE(nullptr, text);
420 ASSERT_TRUE(text->IsTextNode());
421 const AXObject* ax_static_text =
422 GetAXObjectByElementId("paragraph")->FirstChildIncludingIgnored();
423 ASSERT_NE(nullptr, ax_static_text);
424 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
425
426 const auto ax_position =
427 AXPosition::CreatePositionBeforeObject(*ax_static_text);
428 const auto position = ax_position.ToPositionWithAffinity();
429 EXPECT_EQ(text, position.AnchorNode());
430 EXPECT_EQ(5, position.GetPosition().OffsetInContainerNode());
431
432 const auto ax_position_from_dom = AXPosition::FromPosition(position);
433 EXPECT_EQ(ax_position, ax_position_from_dom);
434 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
435 }
436
TEST_F(AccessibilityTest,PositionAfterTextWithWhiteSpace)437 TEST_F(AccessibilityTest, PositionAfterTextWithWhiteSpace) {
438 SetBodyInnerHTML(R"HTML(<p id="paragraph"> Hello </p>)HTML");
439 const Node* text = GetElementById("paragraph")->lastChild();
440 ASSERT_NE(nullptr, text);
441 ASSERT_TRUE(text->IsTextNode());
442 const AXObject* ax_static_text =
443 GetAXObjectByElementId("paragraph")->LastChildIncludingIgnored();
444 ASSERT_NE(nullptr, ax_static_text);
445 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
446
447 const auto ax_position =
448 AXPosition::CreatePositionAfterObject(*ax_static_text);
449 const auto position = ax_position.ToPositionWithAffinity();
450 EXPECT_EQ(text, position.AnchorNode());
451 EXPECT_EQ(10, position.GetPosition().OffsetInContainerNode());
452
453 const auto ax_position_from_dom = AXPosition::FromPosition(position);
454 EXPECT_EQ(ax_position, ax_position_from_dom);
455 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
456 }
457
TEST_F(AccessibilityTest,PositionBeforeLineBreakWithWhiteSpace)458 TEST_F(AccessibilityTest, PositionBeforeLineBreakWithWhiteSpace) {
459 SetBodyInnerHTML(R"HTML(Hello <br id="br"> there)HTML");
460 const AXObject* ax_br = GetAXObjectByElementId("br");
461 ASSERT_NE(nullptr, ax_br);
462 ASSERT_EQ(ax::mojom::Role::kLineBreak, ax_br->RoleValue());
463 const AXObject* ax_div = ax_br->ParentObjectUnignored();
464 ASSERT_NE(nullptr, ax_div);
465 ASSERT_EQ(ax::mojom::Role::kGenericContainer, ax_div->RoleValue());
466
467 const auto ax_position = AXPosition::CreatePositionBeforeObject(*ax_br);
468 EXPECT_FALSE(ax_position.IsTextPosition());
469 EXPECT_EQ(ax_div, ax_position.ContainerObject());
470 EXPECT_EQ(1, ax_position.ChildIndex());
471 EXPECT_EQ(ax_br, ax_position.ChildAfterTreePosition());
472
473 const auto position = ax_position.ToPositionWithAffinity();
474 EXPECT_EQ(GetDocument().body(), position.AnchorNode());
475 EXPECT_EQ(1, position.GetPosition().OffsetInContainerNode());
476
477 const auto ax_position_from_dom = AXPosition::FromPosition(position);
478 EXPECT_EQ(ax_position, ax_position_from_dom);
479 }
480
TEST_F(AccessibilityTest,PositionAfterLineBreakWithWhiteSpace)481 TEST_F(AccessibilityTest, PositionAfterLineBreakWithWhiteSpace) {
482 SetBodyInnerHTML(R"HTML(Hello <br id="br"> there)HTML");
483 const AXObject* ax_br = GetAXObjectByElementId("br");
484 ASSERT_NE(nullptr, ax_br);
485 ASSERT_EQ(ax::mojom::Role::kLineBreak, ax_br->RoleValue());
486 const AXObject* ax_static_text =
487 GetAXRootObject()->DeepestLastChildIncludingIgnored();
488 ASSERT_NE(nullptr, ax_static_text);
489 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
490
491 const auto ax_position = AXPosition::CreatePositionAfterObject(*ax_br);
492 EXPECT_EQ(ax_static_text, ax_position.ContainerObject());
493 EXPECT_TRUE(ax_position.IsTextPosition());
494 EXPECT_EQ(0, ax_position.TextOffset());
495
496 const auto position = ax_position.ToPositionWithAffinity();
497 EXPECT_EQ(ax_static_text->GetNode(), position.AnchorNode());
498 EXPECT_TRUE(position.GetPosition().IsOffsetInAnchor());
499 // Any white space in the DOM should have been skipped.
500 EXPECT_EQ(5, position.GetPosition().OffsetInContainerNode());
501
502 const auto ax_position_from_dom = AXPosition::FromPosition(position);
503 EXPECT_EQ(ax_position, ax_position_from_dom);
504 }
505
TEST_F(AccessibilityTest,FirstPositionInDivContainerWithWhiteSpace)506 TEST_F(AccessibilityTest, FirstPositionInDivContainerWithWhiteSpace) {
507 SetBodyInnerHTML(R"HTML(<div id="div"> Hello<br>there </div>)HTML");
508 const Element* div = GetElementById("div");
509 ASSERT_NE(nullptr, div);
510 const AXObject* ax_div = GetAXObjectByElementId("div");
511 ASSERT_NE(nullptr, ax_div);
512 ASSERT_EQ(ax::mojom::Role::kGenericContainer, ax_div->RoleValue());
513 const AXObject* ax_static_text =
514 GetAXRootObject()->DeepestFirstChildIncludingIgnored();
515 ASSERT_NE(nullptr, ax_static_text);
516 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
517
518 // "Before object" positions that are anchored to before a text object are
519 // always converted to a "text position" before the object's first unignored
520 // character.
521 const auto ax_position = AXPosition::CreateFirstPositionInObject(*ax_div);
522 const auto position = ax_position.ToPositionWithAffinity();
523 EXPECT_EQ(div->firstChild(), position.AnchorNode());
524 EXPECT_TRUE(position.GetPosition().IsOffsetInAnchor());
525 // Any white space in the DOM should have been skipped.
526 EXPECT_EQ(5, position.GetPosition().OffsetInContainerNode());
527
528 const auto ax_position_from_dom = AXPosition::FromPosition(position);
529 EXPECT_EQ(ax_position, ax_position_from_dom);
530 EXPECT_TRUE(ax_position_from_dom.IsTextPosition());
531 EXPECT_EQ(ax_static_text, ax_position_from_dom.ContainerObject());
532 EXPECT_EQ(0, ax_position_from_dom.TextOffset());
533 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
534 }
535
TEST_F(AccessibilityTest,LastPositionInDivContainerWithWhiteSpace)536 TEST_F(AccessibilityTest, LastPositionInDivContainerWithWhiteSpace) {
537 SetBodyInnerHTML(R"HTML(<div id="div"> Hello<br>there </div>
538 <div>Next div</div>)HTML");
539 const Element* div = GetElementById("div");
540 ASSERT_NE(nullptr, div);
541 const AXObject* ax_div = GetAXObjectByElementId("div");
542 ASSERT_NE(nullptr, ax_div);
543 ASSERT_EQ(ax::mojom::Role::kGenericContainer, ax_div->RoleValue());
544
545 const auto ax_position = AXPosition::CreateLastPositionInObject(*ax_div);
546 const auto position = ax_position.ToPositionWithAffinity();
547 EXPECT_EQ(div, position.AnchorNode());
548 EXPECT_TRUE(position.GetPosition().IsAfterChildren());
549
550 const auto ax_position_from_dom = AXPosition::FromPosition(position);
551 EXPECT_EQ(ax_position, ax_position_from_dom);
552 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
553 }
554
TEST_F(AccessibilityTest,FirstPositionInTextContainerWithWhiteSpace)555 TEST_F(AccessibilityTest, FirstPositionInTextContainerWithWhiteSpace) {
556 SetBodyInnerHTML(R"HTML(<div id="div"> Hello </div>)HTML");
557 const Node* text = GetElementById("div")->firstChild();
558 ASSERT_NE(nullptr, text);
559 ASSERT_TRUE(text->IsTextNode());
560 const AXObject* ax_static_text =
561 GetAXObjectByElementId("div")->FirstChildIncludingIgnored();
562 ASSERT_NE(nullptr, ax_static_text);
563 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
564
565 const auto ax_position =
566 AXPosition::CreateFirstPositionInObject(*ax_static_text);
567 const auto position = ax_position.ToPositionWithAffinity();
568 EXPECT_EQ(text, position.AnchorNode());
569 // Any white space in the DOM should have been skipped.
570 EXPECT_EQ(5, position.GetPosition().OffsetInContainerNode());
571
572 const auto ax_position_from_dom = AXPosition::FromPosition(position);
573 EXPECT_EQ(ax_position, ax_position_from_dom);
574 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
575 }
576
TEST_F(AccessibilityTest,LastPositionInTextContainerWithWhiteSpace)577 TEST_F(AccessibilityTest, LastPositionInTextContainerWithWhiteSpace) {
578 SetBodyInnerHTML(R"HTML(<div id="div"> Hello </div>)HTML");
579 const Node* text = GetElementById("div")->lastChild();
580 ASSERT_NE(nullptr, text);
581 ASSERT_TRUE(text->IsTextNode());
582 const AXObject* ax_static_text =
583 GetAXObjectByElementId("div")->LastChildIncludingIgnored();
584 ASSERT_NE(nullptr, ax_static_text);
585 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
586
587 const auto ax_position =
588 AXPosition::CreateLastPositionInObject(*ax_static_text);
589 const auto position = ax_position.ToPositionWithAffinity();
590 EXPECT_EQ(text, position.AnchorNode());
591 EXPECT_EQ(10, position.GetPosition().OffsetInContainerNode());
592
593 const auto ax_position_from_dom = AXPosition::FromPosition(position);
594 EXPECT_EQ(ax_position, ax_position_from_dom);
595 EXPECT_EQ(nullptr, ax_position_from_dom.ChildAfterTreePosition());
596 }
597
598 // Test that DOM positions in white space will be collapsed to the first or last
599 // valid offset in an |AXPosition|.
TEST_F(AccessibilityTest,AXPositionFromDOMPositionWithWhiteSpace)600 TEST_F(AccessibilityTest, AXPositionFromDOMPositionWithWhiteSpace) {
601 SetBodyInnerHTML(R"HTML(<div id="div"> Hello </div>)HTML");
602 const Node* text = GetElementById("div")->firstChild();
603 ASSERT_NE(nullptr, text);
604 ASSERT_TRUE(text->IsTextNode());
605 ASSERT_EQ(15U, text->textContent().length());
606 const AXObject* ax_static_text =
607 GetAXObjectByElementId("div")->FirstChildIncludingIgnored();
608 ASSERT_NE(nullptr, ax_static_text);
609 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
610
611 const Position position_at_start(*text, 0);
612 const auto ax_position_at_start = AXPosition::FromPosition(position_at_start);
613 EXPECT_TRUE(ax_position_at_start.IsTextPosition());
614 EXPECT_EQ(ax_static_text, ax_position_at_start.ContainerObject());
615 EXPECT_EQ(0, ax_position_at_start.TextOffset());
616 EXPECT_EQ(nullptr, ax_position_at_start.ChildAfterTreePosition());
617
618 const Position position_after_white_space(*text, 5);
619 const auto ax_position_after_white_space =
620 AXPosition::FromPosition(position_after_white_space);
621 EXPECT_TRUE(ax_position_after_white_space.IsTextPosition());
622 EXPECT_EQ(ax_static_text, ax_position_after_white_space.ContainerObject());
623 EXPECT_EQ(0, ax_position_after_white_space.TextOffset());
624 EXPECT_EQ(nullptr, ax_position_after_white_space.ChildAfterTreePosition());
625
626 const Position position_at_end(*text, 15);
627 const auto ax_position_at_end = AXPosition::FromPosition(position_at_end);
628 EXPECT_TRUE(ax_position_at_end.IsTextPosition());
629 EXPECT_EQ(ax_static_text, ax_position_at_end.ContainerObject());
630 EXPECT_EQ(5, ax_position_at_end.TextOffset());
631 EXPECT_EQ(nullptr, ax_position_at_end.ChildAfterTreePosition());
632
633 const Position position_before_white_space(*text, 10);
634 const auto ax_position_before_white_space =
635 AXPosition::FromPosition(position_before_white_space);
636 EXPECT_TRUE(ax_position_before_white_space.IsTextPosition());
637 EXPECT_EQ(ax_static_text, ax_position_before_white_space.ContainerObject());
638 EXPECT_EQ(5, ax_position_before_white_space.TextOffset());
639 EXPECT_EQ(nullptr, ax_position_before_white_space.ChildAfterTreePosition());
640 }
641
642 //
643 // Test affinity.
644 // We need to distinguish between the caret at the end of one line and the
645 // beginning of the next.
646 //
647
TEST_F(AccessibilityTest,PositionInTextWithAffinity)648 TEST_F(AccessibilityTest, PositionInTextWithAffinity) {
649 SetBodyInnerHTML(R"HTML(<p id="paragraph">Hello</p>)HTML");
650 const Node* text = GetElementById("paragraph")->firstChild();
651 ASSERT_NE(nullptr, text);
652 ASSERT_TRUE(text->IsTextNode());
653 const AXObject* ax_static_text =
654 GetAXObjectByElementId("paragraph")->FirstChildIncludingIgnored();
655 ASSERT_NE(nullptr, ax_static_text);
656 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
657
658 // Converting from AX to DOM positions should maintain affinity.
659 const auto ax_position = AXPosition::CreatePositionInTextObject(
660 *ax_static_text, 3, TextAffinity::kUpstream);
661 const auto position = ax_position.ToPositionWithAffinity();
662 EXPECT_EQ(TextAffinity::kUpstream, position.Affinity());
663
664 // Converting from DOM to AX positions should maintain affinity.
665 const auto ax_position_from_dom = AXPosition::FromPosition(position);
666 EXPECT_EQ(TextAffinity::kUpstream, ax_position.Affinity());
667 }
668
669 //
670 // Test converting to and from accessibility positions with offsets in HTML
671 // labels. HTML labels are ignored in the accessibility tree when associated
672 // with checkboxes and radio buttons.
673 //
674
TEST_F(AccessibilityTest,PositionInHTMLLabel)675 TEST_F(AccessibilityTest, PositionInHTMLLabel) {
676 SetBodyInnerHTML(R"HTML(
677 <label id="label" for="input">
678 Label text.
679 </label>
680 <p id="paragraph">Intervening paragraph.</p>
681 <input id="input" type="checkbox" checked>
682 )HTML");
683
684 const Node* label = GetElementById("label");
685 ASSERT_NE(nullptr, label);
686 const Node* label_text = label->firstChild();
687 ASSERT_NE(nullptr, label_text);
688 ASSERT_TRUE(label_text->IsTextNode());
689 const Node* paragraph = GetElementById("paragraph");
690 ASSERT_NE(nullptr, paragraph);
691
692 const AXObject* ax_body = GetAXBodyObject();
693 ASSERT_NE(nullptr, ax_body);
694 ASSERT_EQ(ax::mojom::Role::kGenericContainer, ax_body->RoleValue());
695
696 // The HTML label element should be ignored.
697 const AXObject* ax_label = GetAXObjectByElementId("label");
698 ASSERT_NE(nullptr, ax_label);
699 ASSERT_TRUE(ax_label->AccessibilityIsIgnored());
700 const AXObject* ax_paragraph = GetAXObjectByElementId("paragraph");
701 ASSERT_NE(nullptr, ax_paragraph);
702 ASSERT_EQ(ax::mojom::Role::kParagraph, ax_paragraph->RoleValue());
703
704 // All of the following DOM positions should be ignored in the accessibility
705 // tree.
706 const auto position_before = Position::BeforeNode(*label);
707 const auto position_before_text = Position::BeforeNode(*label_text);
708 const auto position_in_text = Position::FirstPositionInNode(*label_text);
709 const auto position_after = Position::AfterNode(*label);
710
711 for (const auto& position : {position_before, position_before_text,
712 position_in_text, position_after}) {
713 const auto ax_position =
714 AXPosition::FromPosition(position, TextAffinity::kDownstream,
715 AXPositionAdjustmentBehavior::kMoveLeft);
716 EXPECT_FALSE(ax_position.IsTextPosition());
717 EXPECT_EQ(ax_body, ax_position.ContainerObject());
718 EXPECT_EQ(0, ax_position.ChildIndex());
719 EXPECT_EQ(ax_paragraph, ax_position.ChildAfterTreePosition());
720
721 const auto position_from_ax = ax_position.ToPositionWithAffinity();
722 EXPECT_EQ(GetDocument().body(), position_from_ax.AnchorNode());
723 EXPECT_EQ(3, position_from_ax.GetPosition().OffsetInContainerNode());
724 EXPECT_EQ(paragraph,
725 position_from_ax.GetPosition().ComputeNodeAfterPosition());
726 }
727 }
728
729 //
730 // Objects with "display: none" or the "hidden" attribute are accessibility
731 // ignored.
732 //
733
TEST_F(AccessibilityTest,PositionInIgnoredObject)734 TEST_F(AccessibilityTest, PositionInIgnoredObject) {
735 SetBodyInnerHTML(R"HTML(
736 <div id="hidden" hidden>Hidden.</div><p id="visible">Visible.</p>
737 )HTML");
738
739 const Node* hidden = GetElementById("hidden");
740 ASSERT_NE(nullptr, hidden);
741 const Node* visible = GetElementById("visible");
742 ASSERT_NE(nullptr, visible);
743
744 const AXObject* ax_root = GetAXRootObject();
745 ASSERT_NE(nullptr, ax_root);
746 ASSERT_EQ(ax::mojom::Role::kRootWebArea, ax_root->RoleValue());
747 ASSERT_EQ(1, ax_root->ChildCountIncludingIgnored());
748
749 const AXObject* ax_html = ax_root->FirstChildIncludingIgnored();
750 ASSERT_NE(nullptr, ax_html);
751 ASSERT_EQ(ax::mojom::Role::kGenericContainer, ax_html->RoleValue());
752 ASSERT_EQ(1, ax_html->ChildCountIncludingIgnored());
753
754 const AXObject* ax_body = GetAXBodyObject();
755 ASSERT_NE(nullptr, ax_body);
756 ASSERT_EQ(ax::mojom::Role::kGenericContainer, ax_body->RoleValue());
757 ASSERT_EQ(2, ax_body->ChildCountIncludingIgnored());
758
759 const AXObject* ax_hidden = GetAXObjectByElementId("hidden");
760 ASSERT_NE(nullptr, ax_hidden);
761 ASSERT_EQ(ax::mojom::Role::kGenericContainer, ax_hidden->RoleValue());
762 ASSERT_TRUE(ax_hidden->AccessibilityIsIgnoredButIncludedInTree());
763
764 const AXObject* ax_visible = GetAXObjectByElementId("visible");
765 ASSERT_NE(nullptr, ax_visible);
766 ASSERT_EQ(ax::mojom::Role::kParagraph, ax_visible->RoleValue());
767
768 // The fact that there is a hidden object before |visible| should not affect
769 // setting a position before it.
770 const auto ax_position_before_visible =
771 AXPosition::CreatePositionBeforeObject(*ax_visible);
772 const auto position_before_visible =
773 ax_position_before_visible.ToPositionWithAffinity();
774 EXPECT_EQ(GetDocument().body(), position_before_visible.AnchorNode());
775 EXPECT_EQ(2, position_before_visible.GetPosition().OffsetInContainerNode());
776 EXPECT_EQ(visible,
777 position_before_visible.GetPosition().ComputeNodeAfterPosition());
778
779 const auto ax_position_before_visible_from_dom =
780 AXPosition::FromPosition(position_before_visible);
781 EXPECT_EQ(ax_position_before_visible, ax_position_before_visible_from_dom);
782 EXPECT_EQ(ax_visible,
783 ax_position_before_visible_from_dom.ChildAfterTreePosition());
784
785 // A position at the beginning of the body will appear to be before the hidden
786 // element in the DOM.
787 const auto ax_position_first =
788 AXPosition::CreateFirstPositionInObject(*ax_root);
789 const auto position_first = ax_position_first.ToPositionWithAffinity();
790 EXPECT_EQ(GetDocument(), position_first.AnchorNode());
791 EXPECT_TRUE(position_first.GetPosition().IsBeforeChildren());
792
793 EXPECT_EQ(GetDocument().documentElement(),
794 position_first.GetPosition().ComputeNodeAfterPosition());
795
796 const auto ax_position_first_from_dom =
797 AXPosition::FromPosition(position_first);
798 EXPECT_EQ(ax_position_first, ax_position_first_from_dom);
799
800 EXPECT_EQ(ax_html, ax_position_first_from_dom.ChildAfterTreePosition());
801
802 // A DOM position before |hidden| should convert to an accessibility position
803 // before |hidden| because the node is ignored but included in the tree.
804 const auto position_before = Position::BeforeNode(*hidden);
805 const auto ax_position_before_from_dom =
806 AXPosition::FromPosition(position_before);
807 EXPECT_EQ(ax_body, ax_position_before_from_dom.ContainerObject());
808 EXPECT_EQ(0, ax_position_before_from_dom.ChildIndex());
809 EXPECT_EQ(ax_hidden, ax_position_before_from_dom.ChildAfterTreePosition());
810
811 // A DOM position after |hidden| should convert to an accessibility position
812 // before |visible|.
813 const auto position_after = Position::AfterNode(*hidden);
814 const auto ax_position_after_from_dom =
815 AXPosition::FromPosition(position_after);
816 EXPECT_EQ(ax_body, ax_position_after_from_dom.ContainerObject());
817 EXPECT_EQ(1, ax_position_after_from_dom.ChildIndex());
818 EXPECT_EQ(ax_visible, ax_position_after_from_dom.ChildAfterTreePosition());
819 }
820
821 //
822 // Aria-hidden can cause things in the DOM to be hidden from accessibility.
823 //
824
TEST_F(AccessibilityTest,BeforePositionInARIAHiddenShouldNotSkipARIAHidden)825 TEST_F(AccessibilityTest, BeforePositionInARIAHiddenShouldNotSkipARIAHidden) {
826 SetBodyInnerHTML(R"HTML(
827 <div role="main" id="container">
828 <p id="before">Before aria-hidden.</p>
829 <p id="ariaHidden" aria-hidden="true">Aria-hidden.</p>
830 <p id="after">After aria-hidden.</p>
831 </div>
832 )HTML");
833
834 const Node* container = GetElementById("container");
835 ASSERT_NE(nullptr, container);
836 const Node* after = GetElementById("after");
837 ASSERT_NE(nullptr, after);
838 const Node* hidden = GetElementById("ariaHidden");
839 ASSERT_NE(nullptr, hidden);
840
841 const AXObject* ax_before = GetAXObjectByElementId("before");
842 ASSERT_NE(nullptr, ax_before);
843 ASSERT_EQ(ax::mojom::Role::kParagraph, ax_before->RoleValue());
844 const AXObject* ax_after = GetAXObjectByElementId("after");
845 ASSERT_NE(nullptr, ax_after);
846 ASSERT_EQ(ax::mojom::Role::kParagraph, ax_after->RoleValue());
847 const AXObject* ax_hidden = GetAXObjectByElementId("ariaHidden");
848 ASSERT_NE(nullptr, ax_hidden);
849 ASSERT_TRUE(ax_hidden->AccessibilityIsIgnored());
850
851 const auto ax_position = AXPosition::CreatePositionAfterObject(*ax_before);
852 const auto position = ax_position.ToPositionWithAffinity();
853 EXPECT_EQ(container, position.AnchorNode());
854 EXPECT_EQ(3, position.GetPosition().OffsetInContainerNode());
855 EXPECT_EQ(hidden, position.GetPosition().ComputeNodeAfterPosition());
856
857 const auto ax_position_from_dom = AXPosition::FromPosition(position);
858 EXPECT_EQ(ax_position, ax_position_from_dom);
859 EXPECT_EQ(ax_hidden, ax_position_from_dom.ChildAfterTreePosition());
860 }
861
TEST_F(AccessibilityTest,PreviousPositionAfterARIAHiddenShouldNotSkipARIAHidden)862 TEST_F(AccessibilityTest,
863 PreviousPositionAfterARIAHiddenShouldNotSkipARIAHidden) {
864 SetBodyInnerHTML(R"HTML(
865 <p id="before">Before aria-hidden.</p>
866 <p id="ariaHidden" aria-hidden="true">Aria-hidden.</p>
867 <p id="after">After aria-hidden.</p>
868 )HTML");
869
870 const Node* hidden = GetElementById("ariaHidden");
871 ASSERT_NE(nullptr, hidden);
872 ASSERT_NE(nullptr, hidden->firstChild());
873 const Node* after = GetElementById("after");
874 ASSERT_NE(nullptr, after);
875
876 const AXObject* ax_after = GetAXObjectByElementId("after");
877 ASSERT_NE(nullptr, ax_after);
878 ASSERT_EQ(ax::mojom::Role::kParagraph, ax_after->RoleValue());
879 ASSERT_NE(nullptr, GetAXObjectByElementId("ariaHidden"));
880 ASSERT_TRUE(GetAXObjectByElementId("ariaHidden")->AccessibilityIsIgnored());
881
882 const auto ax_position = AXPosition::CreatePositionBeforeObject(*ax_after);
883 const auto position = ax_position.ToPositionWithAffinity();
884 EXPECT_EQ(GetDocument().body(), position.AnchorNode());
885 EXPECT_EQ(5, position.GetPosition().OffsetInContainerNode());
886 EXPECT_EQ(after, position.GetPosition().ComputeNodeAfterPosition());
887
888 const auto ax_position_from_dom = AXPosition::FromPosition(position);
889 EXPECT_EQ(ax_position, ax_position_from_dom);
890 EXPECT_EQ(ax_after, ax_position_from_dom.ChildAfterTreePosition());
891
892 const auto ax_position_previous = ax_position.CreatePreviousPosition();
893 const auto position_previous = ax_position_previous.ToPositionWithAffinity();
894 EXPECT_EQ(hidden->firstChild(), position_previous.AnchorNode());
895 EXPECT_EQ(12, position_previous.GetPosition().OffsetInContainerNode());
896 EXPECT_EQ(nullptr,
897 position_previous.GetPosition().ComputeNodeAfterPosition());
898
899 const auto ax_position_previous_from_dom =
900 AXPosition::FromPosition(position_previous);
901 EXPECT_EQ(ax_position_previous, ax_position_previous_from_dom);
902 EXPECT_EQ(nullptr, ax_position_previous_from_dom.ChildAfterTreePosition());
903 }
904
TEST_F(AccessibilityTest,FromPositionInARIAHidden)905 TEST_F(AccessibilityTest, FromPositionInARIAHidden) {
906 SetBodyInnerHTML(R"HTML(
907 <div role="main" id="container">
908 <p id="before">Before aria-hidden.</p>
909 <p id="ariaHidden" aria-hidden="true">Aria-hidden.</p>
910 <p id="after">After aria-hidden.</p>
911 </div>
912 )HTML");
913
914 const Node* hidden = GetElementById("ariaHidden");
915 ASSERT_NE(nullptr, hidden);
916
917 const AXObject* ax_container = GetAXObjectByElementId("container");
918 ASSERT_NE(nullptr, ax_container);
919 ASSERT_EQ(ax::mojom::Role::kMain, ax_container->RoleValue());
920 ASSERT_EQ(3, ax_container->ChildCountIncludingIgnored());
921 const AXObject* ax_before = GetAXObjectByElementId("before");
922 ASSERT_NE(nullptr, ax_before);
923 ASSERT_EQ(ax::mojom::Role::kParagraph, ax_before->RoleValue());
924 const AXObject* ax_after = GetAXObjectByElementId("after");
925 ASSERT_NE(nullptr, ax_after);
926 ASSERT_EQ(ax::mojom::Role::kParagraph, ax_after->RoleValue());
927 const AXObject* ax_hidden = GetAXObjectByElementId("ariaHidden");
928 ASSERT_NE(nullptr, ax_hidden);
929 ASSERT_TRUE(ax_hidden->AccessibilityIsIgnored());
930
931 const auto position_first = Position::FirstPositionInNode(*hidden);
932 // Since "ax_hidden" has a static text child, the AXPosition should move to an
933 // equivalent position on the static text child.
934 auto ax_position_left =
935 AXPosition::FromPosition(position_first, TextAffinity::kDownstream,
936 AXPositionAdjustmentBehavior::kMoveLeft);
937 EXPECT_TRUE(ax_position_left.IsValid());
938 EXPECT_TRUE(ax_position_left.IsTextPosition());
939 EXPECT_EQ(ax_hidden->FirstChildIncludingIgnored(),
940 ax_position_left.ContainerObject());
941 EXPECT_EQ(0, ax_position_left.TextOffset());
942
943 // In this case, the adjustment behavior should not affect the outcome because
944 // there is an equivalent AXPosition in the static text child.
945 auto ax_position_right =
946 AXPosition::FromPosition(position_first, TextAffinity::kDownstream,
947 AXPositionAdjustmentBehavior::kMoveRight);
948 EXPECT_TRUE(ax_position_right.IsValid());
949 EXPECT_TRUE(ax_position_right.IsTextPosition());
950 EXPECT_EQ(ax_hidden->FirstChildIncludingIgnored(),
951 ax_position_right.ContainerObject());
952 EXPECT_EQ(0, ax_position_right.TextOffset());
953
954 const auto position_before = Position::BeforeNode(*hidden);
955 ax_position_left =
956 AXPosition::FromPosition(position_before, TextAffinity::kDownstream,
957 AXPositionAdjustmentBehavior::kMoveLeft);
958 EXPECT_TRUE(ax_position_left.IsValid());
959 EXPECT_FALSE(ax_position_left.IsTextPosition());
960 EXPECT_EQ(ax_container, ax_position_left.ContainerObject());
961 EXPECT_EQ(1, ax_position_left.ChildIndex());
962 EXPECT_EQ(ax_hidden, ax_position_left.ChildAfterTreePosition());
963
964 // Since an AXPosition before "ax_hidden" is valid, i.e. it does not need to
965 // be adjusted, then adjustment behavior should not make a difference in the
966 // outcome.
967 ax_position_right =
968 AXPosition::FromPosition(position_before, TextAffinity::kDownstream,
969 AXPositionAdjustmentBehavior::kMoveRight);
970 EXPECT_TRUE(ax_position_right.IsValid());
971 EXPECT_FALSE(ax_position_right.IsTextPosition());
972 EXPECT_EQ(ax_container, ax_position_right.ContainerObject());
973 EXPECT_EQ(1, ax_position_right.ChildIndex());
974 EXPECT_EQ(ax_hidden, ax_position_right.ChildAfterTreePosition());
975
976 // The DOM node right after "hidden" is accessibility ignored, so we should
977 // see an adjustment in the relevant direction.
978 const auto position_after = Position::AfterNode(*hidden);
979 ax_position_left =
980 AXPosition::FromPosition(position_after, TextAffinity::kDownstream,
981 AXPositionAdjustmentBehavior::kMoveLeft);
982 EXPECT_TRUE(ax_position_left.IsValid());
983 EXPECT_TRUE(ax_position_left.IsTextPosition());
984 EXPECT_EQ(ax_hidden->FirstChildIncludingIgnored(),
985 ax_position_left.ContainerObject());
986 EXPECT_EQ(12, ax_position_left.TextOffset());
987
988 ax_position_right =
989 AXPosition::FromPosition(position_after, TextAffinity::kDownstream,
990 AXPositionAdjustmentBehavior::kMoveRight);
991 EXPECT_TRUE(ax_position_right.IsValid());
992 EXPECT_FALSE(ax_position_right.IsTextPosition());
993 EXPECT_EQ(ax_container, ax_position_right.ContainerObject());
994 EXPECT_EQ(2, ax_position_right.ChildIndex());
995 EXPECT_EQ(ax_after, ax_position_right.ChildAfterTreePosition());
996 }
997
998 //
999 // Canvas fallback can cause things to be in the accessibility tree that are not
1000 // in the layout tree.
1001 //
1002
TEST_F(AccessibilityTest,PositionInCanvas)1003 TEST_F(AccessibilityTest, PositionInCanvas) {
1004 SetBodyInnerHTML(R"HTML(
1005 <canvas id="canvas1" width="100" height="100">Fallback text</canvas>
1006 <canvas id="canvas2" width="100" height="100">
1007 <button id="button">Fallback button</button>
1008 </canvas>
1009 )HTML");
1010
1011 const Node* canvas_1 = GetElementById("canvas1");
1012 ASSERT_NE(nullptr, canvas_1);
1013 const Node* text = canvas_1->firstChild();
1014 ASSERT_NE(nullptr, text);
1015 ASSERT_TRUE(text->IsTextNode());
1016 const Node* canvas_2 = GetElementById("canvas2");
1017 ASSERT_NE(nullptr, canvas_2);
1018 const Node* button = GetElementById("button");
1019 ASSERT_NE(nullptr, button);
1020
1021 const AXObject* ax_canvas_1 = GetAXObjectByElementId("canvas1");
1022 ASSERT_NE(nullptr, ax_canvas_1);
1023 ASSERT_EQ(ax::mojom::Role::kCanvas, ax_canvas_1->RoleValue());
1024 const AXObject* ax_text = ax_canvas_1->FirstChildIncludingIgnored();
1025 ASSERT_NE(nullptr, ax_text);
1026 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_text->RoleValue());
1027 const AXObject* ax_canvas_2 = GetAXObjectByElementId("canvas2");
1028 ASSERT_NE(nullptr, ax_canvas_2);
1029 ASSERT_EQ(ax::mojom::Role::kCanvas, ax_canvas_2->RoleValue());
1030 const AXObject* ax_button = GetAXObjectByElementId("button");
1031 ASSERT_NE(nullptr, ax_button);
1032 ASSERT_EQ(ax::mojom::Role::kButton, ax_button->RoleValue());
1033
1034 // The first child of "canvas1" is a text object. Creating a "before children"
1035 // position in this canvas should return the equivalent text position anchored
1036 // to before the first character of the text object.
1037 const auto ax_position_1 =
1038 AXPosition::CreateFirstPositionInObject(*ax_canvas_1);
1039 EXPECT_TRUE(ax_position_1.IsTextPosition());
1040 EXPECT_EQ(ax_text, ax_position_1.ContainerObject());
1041 EXPECT_EQ(0, ax_position_1.TextOffset());
1042
1043 const auto position_1 = ax_position_1.ToPositionWithAffinity();
1044 EXPECT_EQ(text, position_1.AnchorNode());
1045 EXPECT_TRUE(position_1.GetPosition().IsOffsetInAnchor());
1046 EXPECT_EQ(0, position_1.GetPosition().OffsetInContainerNode());
1047
1048 const auto ax_position_from_dom_1 = AXPosition::FromPosition(position_1);
1049 EXPECT_EQ(ax_position_1, ax_position_from_dom_1);
1050
1051 const auto ax_position_2 = AXPosition::CreatePositionBeforeObject(*ax_text);
1052 EXPECT_TRUE(ax_position_2.IsTextPosition());
1053 EXPECT_EQ(ax_text, ax_position_2.ContainerObject());
1054 EXPECT_EQ(0, ax_position_2.TextOffset());
1055
1056 const auto position_2 = ax_position_2.ToPositionWithAffinity();
1057 EXPECT_EQ(text, position_2.AnchorNode());
1058 EXPECT_EQ(0, position_2.GetPosition().OffsetInContainerNode());
1059
1060 const auto ax_position_from_dom_2 = AXPosition::FromPosition(position_2);
1061 EXPECT_EQ(ax_position_2, ax_position_from_dom_2);
1062
1063 const auto ax_position_3 =
1064 AXPosition::CreateLastPositionInObject(*ax_canvas_2);
1065 EXPECT_FALSE(ax_position_3.IsTextPosition());
1066 EXPECT_EQ(ax_canvas_2, ax_position_3.ContainerObject());
1067 EXPECT_EQ(1, ax_position_3.ChildIndex());
1068 EXPECT_EQ(nullptr, ax_position_3.ChildAfterTreePosition());
1069
1070 const auto position_3 = ax_position_3.ToPositionWithAffinity();
1071 EXPECT_EQ(canvas_2, position_3.AnchorNode());
1072 // There is a line break between the start of the canvas and the button.
1073 EXPECT_EQ(2, position_3.GetPosition().ComputeOffsetInContainerNode());
1074
1075 const auto ax_position_from_dom_3 = AXPosition::FromPosition(position_3);
1076 EXPECT_EQ(ax_position_3, ax_position_from_dom_3);
1077
1078 const auto ax_position_4 = AXPosition::CreatePositionBeforeObject(*ax_button);
1079 EXPECT_FALSE(ax_position_4.IsTextPosition());
1080 EXPECT_EQ(ax_canvas_2, ax_position_4.ContainerObject());
1081 EXPECT_EQ(0, ax_position_4.ChildIndex());
1082 EXPECT_EQ(ax_button, ax_position_4.ChildAfterTreePosition());
1083
1084 const auto position_4 = ax_position_4.ToPositionWithAffinity();
1085 EXPECT_EQ(canvas_2, position_4.AnchorNode());
1086 // There is a line break between the start of the canvas and the button.
1087 EXPECT_EQ(1, position_4.GetPosition().ComputeOffsetInContainerNode());
1088 EXPECT_EQ(button, position_4.GetPosition().ComputeNodeAfterPosition());
1089
1090 const auto ax_position_from_dom_4 = AXPosition::FromPosition(position_4);
1091 EXPECT_EQ(ax_position_4, ax_position_from_dom_4);
1092 }
1093
1094 //
1095 // Some layout objects, e.g. list bullets and CSS::before/after content, appear
1096 // in the accessibility tree but are not present in the DOM.
1097 //
1098
TEST_F(AccessibilityTest,PositionBeforeListMarker)1099 TEST_F(AccessibilityTest, PositionBeforeListMarker) {
1100 SetBodyInnerHTML(R"HTML(
1101 <ul id="list">
1102 <li id="listItem">Item.</li>
1103 </ul>
1104 )HTML");
1105
1106 const Node* list = GetElementById("list");
1107 ASSERT_NE(nullptr, list);
1108 const Node* item = GetElementById("listItem");
1109 ASSERT_NE(nullptr, item);
1110 const Node* text = item->firstChild();
1111 ASSERT_NE(nullptr, text);
1112 ASSERT_TRUE(text->IsTextNode());
1113
1114 const AXObject* ax_item = GetAXObjectByElementId("listItem");
1115 ASSERT_NE(nullptr, ax_item);
1116 ASSERT_EQ(ax::mojom::Role::kListItem, ax_item->RoleValue());
1117 ASSERT_EQ(2, ax_item->ChildCountIncludingIgnored());
1118 const AXObject* ax_marker = ax_item->FirstChildIncludingIgnored();
1119 ASSERT_NE(nullptr, ax_marker);
1120 ASSERT_EQ(ax::mojom::Role::kListMarker, ax_marker->RoleValue());
1121
1122 //
1123 // Test adjusting invalid DOM positions to the left.
1124 //
1125
1126 const auto ax_position_1 = AXPosition::CreateFirstPositionInObject(*ax_item);
1127 EXPECT_EQ(ax_item, ax_position_1.ContainerObject());
1128 EXPECT_FALSE(ax_position_1.IsTextPosition());
1129 EXPECT_EQ(0, ax_position_1.ChildIndex());
1130 EXPECT_EQ(ax_marker, ax_position_1.ChildAfterTreePosition());
1131
1132 const auto position_1 = ax_position_1.ToPositionWithAffinity(
1133 AXPositionAdjustmentBehavior::kMoveLeft);
1134 EXPECT_EQ(list, position_1.AnchorNode());
1135 // There is a line break between the start of the list and the first item.
1136 EXPECT_EQ(1, position_1.GetPosition().OffsetInContainerNode());
1137 EXPECT_EQ(item, position_1.GetPosition().ComputeNodeAfterPosition());
1138
1139 const auto ax_position_from_dom_1 = AXPosition::FromPosition(position_1);
1140 EXPECT_EQ(
1141 ax_position_1.AsValidDOMPosition(AXPositionAdjustmentBehavior::kMoveLeft),
1142 ax_position_from_dom_1);
1143 EXPECT_EQ(ax_item, ax_position_from_dom_1.ChildAfterTreePosition());
1144
1145 const auto ax_position_2 = AXPosition::CreatePositionBeforeObject(*ax_marker);
1146 EXPECT_EQ(ax_item, ax_position_2.ContainerObject());
1147 EXPECT_FALSE(ax_position_2.IsTextPosition());
1148 EXPECT_EQ(0, ax_position_2.ChildIndex());
1149 EXPECT_EQ(ax_marker, ax_position_2.ChildAfterTreePosition());
1150
1151 const auto position_2 = ax_position_2.ToPositionWithAffinity(
1152 AXPositionAdjustmentBehavior::kMoveLeft);
1153 EXPECT_EQ(list, position_2.AnchorNode());
1154 // There is a line break between the start of the list and the first item.
1155 EXPECT_EQ(1, position_2.GetPosition().OffsetInContainerNode());
1156 EXPECT_EQ(item, position_2.GetPosition().ComputeNodeAfterPosition());
1157
1158 const auto ax_position_from_dom_2 = AXPosition::FromPosition(position_2);
1159 EXPECT_EQ(
1160 ax_position_2.AsValidDOMPosition(AXPositionAdjustmentBehavior::kMoveLeft),
1161 ax_position_from_dom_2);
1162 EXPECT_EQ(ax_item, ax_position_from_dom_2.ChildAfterTreePosition());
1163
1164 //
1165 // Test adjusting the same invalid positions to the right.
1166 //
1167
1168 const auto position_3 = ax_position_1.ToPositionWithAffinity(
1169 AXPositionAdjustmentBehavior::kMoveRight);
1170 EXPECT_EQ(text, position_3.AnchorNode());
1171 EXPECT_TRUE(position_3.GetPosition().IsOffsetInAnchor());
1172 EXPECT_EQ(0, position_3.GetPosition().OffsetInContainerNode());
1173
1174 const auto position_4 = ax_position_2.ToPositionWithAffinity(
1175 AXPositionAdjustmentBehavior::kMoveRight);
1176 EXPECT_EQ(text, position_4.AnchorNode());
1177 EXPECT_TRUE(position_4.GetPosition().IsOffsetInAnchor());
1178 EXPECT_EQ(0, position_4.GetPosition().OffsetInContainerNode());
1179 }
1180
TEST_F(AccessibilityTest,PositionAfterListMarker)1181 TEST_F(AccessibilityTest, PositionAfterListMarker) {
1182 SetBodyInnerHTML(R"HTML(
1183 <ol>
1184 <li id="listItem">Item.</li>
1185 </ol>
1186 )HTML");
1187
1188 const Node* item = GetElementById("listItem");
1189 ASSERT_NE(nullptr, item);
1190 const Node* text = item->firstChild();
1191 ASSERT_NE(nullptr, text);
1192 ASSERT_TRUE(text->IsTextNode());
1193
1194 const AXObject* ax_item = GetAXObjectByElementId("listItem");
1195 ASSERT_NE(nullptr, ax_item);
1196 ASSERT_EQ(ax::mojom::Role::kListItem, ax_item->RoleValue());
1197 ASSERT_EQ(2, ax_item->ChildCountIncludingIgnored());
1198 const AXObject* ax_marker = ax_item->FirstChildIncludingIgnored();
1199 ASSERT_NE(nullptr, ax_marker);
1200 ASSERT_EQ(ax::mojom::Role::kListMarker, ax_marker->RoleValue());
1201 const AXObject* ax_text = ax_item->LastChildIncludingIgnored();
1202 ASSERT_NE(nullptr, ax_text);
1203 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_text->RoleValue());
1204
1205 const auto ax_position = AXPosition::CreatePositionAfterObject(*ax_marker);
1206 const auto position = ax_position.ToPositionWithAffinity();
1207 EXPECT_EQ(text, position.AnchorNode());
1208 EXPECT_TRUE(position.GetPosition().IsOffsetInAnchor());
1209 EXPECT_EQ(0, position.GetPosition().OffsetInContainerNode());
1210
1211 const auto ax_position_from_dom = AXPosition::FromPosition(position);
1212 EXPECT_EQ(ax_position, ax_position_from_dom);
1213 EXPECT_EQ(ax_text, ax_position_from_dom.ContainerObject());
1214 EXPECT_TRUE(ax_position_from_dom.IsTextPosition());
1215 EXPECT_EQ(0, ax_position_from_dom.TextOffset());
1216 }
1217
TEST_F(AccessibilityTest,PositionInCSSContent)1218 TEST_F(AccessibilityTest, PositionInCSSContent) {
1219 SetBodyInnerHTML(kCSSBeforeAndAfter);
1220
1221 const Node* quote = GetElementById("quote");
1222 ASSERT_NE(nullptr, quote);
1223 // CSS text nodes are not in the DOM tree.
1224 const Node* text = quote->firstChild();
1225 ASSERT_NE(nullptr, text);
1226 ASSERT_FALSE(text->IsPseudoElement());
1227 ASSERT_TRUE(text->IsTextNode());
1228
1229 const AXObject* ax_quote = GetAXObjectByElementId("quote");
1230 ASSERT_NE(nullptr, ax_quote);
1231 ASSERT_TRUE(ax_quote->AccessibilityIsIgnored());
1232 const AXObject* ax_quote_parent = ax_quote->ParentObjectUnignored();
1233 ASSERT_NE(nullptr, ax_quote_parent);
1234 ASSERT_EQ(4, ax_quote_parent->UnignoredChildCount());
1235 const AXObject* ax_css_before = ax_quote_parent->UnignoredChildAt(0);
1236 ASSERT_NE(nullptr, ax_css_before);
1237 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_css_before->RoleValue());
1238 const AXObject* ax_text = ax_quote_parent->UnignoredChildAt(1);
1239 ASSERT_NE(nullptr, ax_text);
1240 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_text->RoleValue());
1241 const AXObject* ax_css_after = ax_quote_parent->UnignoredChildAt(2);
1242 ASSERT_NE(nullptr, ax_css_after);
1243 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_css_after->RoleValue());
1244
1245 const auto ax_position_before =
1246 AXPosition::CreateFirstPositionInObject(*ax_css_before);
1247 EXPECT_TRUE(ax_position_before.IsTextPosition());
1248 EXPECT_EQ(0, ax_position_before.TextOffset());
1249 EXPECT_EQ(nullptr, ax_position_before.ChildAfterTreePosition());
1250 const auto position_before = ax_position_before.ToPositionWithAffinity(
1251 AXPositionAdjustmentBehavior::kMoveRight);
1252 EXPECT_EQ(text, position_before.AnchorNode());
1253 EXPECT_EQ(0, position_before.GetPosition().OffsetInContainerNode());
1254
1255 const auto ax_position_after =
1256 AXPosition::CreateLastPositionInObject(*ax_css_after);
1257 EXPECT_TRUE(ax_position_after.IsTextPosition());
1258 EXPECT_EQ(2, ax_position_after.TextOffset());
1259 EXPECT_EQ(nullptr, ax_position_after.ChildAfterTreePosition());
1260 const auto position_after = ax_position_after.ToPositionWithAffinity(
1261 AXPositionAdjustmentBehavior::kMoveLeft);
1262 EXPECT_EQ(text, position_after.AnchorNode());
1263 EXPECT_EQ(12, position_after.GetPosition().OffsetInContainerNode());
1264 }
1265
TEST_F(AccessibilityTest,PositionInCSSImageContent)1266 TEST_F(AccessibilityTest, PositionInCSSImageContent) {
1267 constexpr char css_content_no_text[] = R"HTML(
1268 <style>
1269 .heading::before {
1270 content: url(data:image/gif;base64,);
1271 }
1272 </style>
1273 <h1 id="heading" class="heading">Heading</h1>)HTML";
1274 SetBodyInnerHTML(css_content_no_text);
1275
1276 const Node* heading = GetElementById("heading");
1277 ASSERT_NE(nullptr, heading);
1278
1279 const AXObject* ax_heading = GetAXObjectByElementId("heading");
1280 ASSERT_NE(nullptr, ax_heading);
1281 ASSERT_EQ(ax::mojom::Role::kHeading, ax_heading->RoleValue());
1282 ASSERT_EQ(2, ax_heading->ChildCountIncludingIgnored());
1283
1284 const AXObject* ax_css_before = ax_heading->FirstChildIncludingIgnored();
1285 ASSERT_NE(nullptr, ax_css_before);
1286 ASSERT_EQ(ax::mojom::Role::kImage, ax_css_before->RoleValue());
1287
1288 const auto ax_position_before =
1289 AXPosition::CreateFirstPositionInObject(*ax_css_before);
1290 const auto position = ax_position_before.ToPositionWithAffinity(
1291 AXPositionAdjustmentBehavior::kMoveLeft);
1292 EXPECT_EQ(GetDocument().body(), position.AnchorNode());
1293 EXPECT_EQ(3, position.GetPosition().OffsetInContainerNode());
1294 }
1295
TEST_F(AccessibilityTest,PositionInTableWithCSSContent)1296 TEST_F(AccessibilityTest, PositionInTableWithCSSContent) {
1297 SetBodyInnerHTML(kHTMLTable);
1298
1299 // Add some CSS content, i.e. a plus symbol before and a colon after each
1300 // table header cell.
1301 Element* const style_element =
1302 GetDocument().CreateRawElement(html_names::kStyleTag);
1303 ASSERT_NE(nullptr, style_element);
1304 style_element->setTextContent(R"STYLE(
1305 th::before {
1306 content: "+";
1307 }
1308 th::after {
1309 content: ":";
1310 }
1311 )STYLE");
1312 GetDocument().body()->insertBefore(style_element,
1313 GetDocument().body()->firstChild());
1314 UpdateAllLifecyclePhasesForTest();
1315
1316 const Node* first_header_cell = GetElementById("firstHeaderCell");
1317 ASSERT_NE(nullptr, first_header_cell);
1318 const Node* last_header_cell = GetElementById("lastHeaderCell");
1319 ASSERT_NE(nullptr, last_header_cell);
1320
1321 // CSS text nodes are not in the DOM tree.
1322 const Node* first_header_cell_text = first_header_cell->firstChild();
1323 ASSERT_NE(nullptr, first_header_cell_text);
1324 ASSERT_FALSE(first_header_cell_text->IsPseudoElement());
1325 ASSERT_TRUE(first_header_cell_text->IsTextNode());
1326 const Node* last_header_cell_text = last_header_cell->firstChild();
1327 ASSERT_NE(nullptr, last_header_cell_text);
1328 ASSERT_FALSE(last_header_cell_text->IsPseudoElement());
1329 ASSERT_TRUE(last_header_cell_text->IsTextNode());
1330
1331 const AXObject* ax_first_header_cell =
1332 GetAXObjectByElementId("firstHeaderCell");
1333 ASSERT_NE(nullptr, ax_first_header_cell);
1334 ASSERT_EQ(ax::mojom::Role::kColumnHeader, ax_first_header_cell->RoleValue());
1335 const AXObject* ax_last_header_cell =
1336 GetAXObjectByElementId("lastHeaderCell");
1337 ASSERT_NE(nullptr, ax_last_header_cell);
1338 ASSERT_EQ(ax::mojom::Role::kColumnHeader, ax_last_header_cell->RoleValue());
1339
1340 ASSERT_EQ(3, ax_first_header_cell->ChildCountIncludingIgnored());
1341 AXObject* const ax_first_cell_css_before =
1342 ax_first_header_cell->FirstChildIncludingIgnored();
1343 ASSERT_NE(nullptr, ax_first_cell_css_before);
1344 ASSERT_EQ(ax::mojom::Role::kStaticText,
1345 ax_first_cell_css_before->RoleValue());
1346
1347 ASSERT_EQ(3, ax_last_header_cell->ChildCountIncludingIgnored());
1348 AXObject* const ax_last_cell_css_after =
1349 ax_last_header_cell->LastChildIncludingIgnored();
1350 ASSERT_NE(nullptr, ax_last_cell_css_after);
1351 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_last_cell_css_after->RoleValue());
1352
1353 // The first position inside the first header cell should be before the plus
1354 // symbol inside the CSS content. It should be valid in the accessibility tree
1355 // but not valid in the DOM tree.
1356 auto ax_position_before =
1357 AXPosition::CreateFirstPositionInObject(*ax_first_header_cell);
1358 EXPECT_TRUE(ax_position_before.IsTextPosition());
1359 EXPECT_EQ(0, ax_position_before.TextOffset());
1360 auto position_before = ax_position_before.ToPositionWithAffinity(
1361 AXPositionAdjustmentBehavior::kMoveRight);
1362 EXPECT_EQ(first_header_cell_text, position_before.AnchorNode());
1363 EXPECT_EQ(0, position_before.GetPosition().OffsetInContainerNode());
1364
1365 // Same situation as above, but explicitly create a text position inside the
1366 // CSS content, instead of having it implicitly created by
1367 // CreateFirstPositionInObject.
1368 ax_position_before =
1369 AXPosition::CreateFirstPositionInObject(*ax_first_cell_css_before);
1370 EXPECT_TRUE(ax_position_before.IsTextPosition());
1371 EXPECT_EQ(0, ax_position_before.TextOffset());
1372 position_before = ax_position_before.ToPositionWithAffinity(
1373 AXPositionAdjustmentBehavior::kMoveRight);
1374 EXPECT_EQ(first_header_cell_text, position_before.AnchorNode());
1375 EXPECT_EQ(0, position_before.GetPosition().OffsetInContainerNode());
1376
1377 // Same situation as above, but now create a text position inside the inline
1378 // text box representing the CSS content after the last header cell.
1379 ax_first_cell_css_before->LoadInlineTextBoxes();
1380 ASSERT_NE(nullptr, ax_first_cell_css_before->FirstChildIncludingIgnored());
1381 ax_position_before = AXPosition::CreateFirstPositionInObject(
1382 *ax_first_cell_css_before->FirstChildIncludingIgnored());
1383 EXPECT_TRUE(ax_position_before.IsTextPosition());
1384 EXPECT_EQ(0, ax_position_before.TextOffset());
1385 position_before = ax_position_before.ToPositionWithAffinity(
1386 AXPositionAdjustmentBehavior::kMoveRight);
1387 EXPECT_EQ(first_header_cell_text, position_before.AnchorNode());
1388 EXPECT_EQ(0, position_before.GetPosition().OffsetInContainerNode());
1389
1390 // An "after children" position inside the last header cell should be after
1391 // the CSS content that displays a colon. It should be valid in the
1392 // accessibility tree but not valid in the DOM tree.
1393 auto ax_position_after =
1394 AXPosition::CreateLastPositionInObject(*ax_last_header_cell);
1395 EXPECT_FALSE(ax_position_after.IsTextPosition());
1396 EXPECT_EQ(3, ax_position_after.ChildIndex());
1397 auto position_after = ax_position_after.ToPositionWithAffinity(
1398 AXPositionAdjustmentBehavior::kMoveLeft);
1399 EXPECT_EQ(last_header_cell_text, position_after.AnchorNode());
1400 EXPECT_EQ(8, position_after.GetPosition().OffsetInContainerNode());
1401
1402 // Similar to the last case, but explicitly create a text position inside the
1403 // CSS content after the last header cell.
1404 ax_position_after =
1405 AXPosition::CreateLastPositionInObject(*ax_last_cell_css_after);
1406 EXPECT_TRUE(ax_position_after.IsTextPosition());
1407 EXPECT_EQ(1, ax_position_after.TextOffset());
1408 position_after = ax_position_after.ToPositionWithAffinity(
1409 AXPositionAdjustmentBehavior::kMoveLeft);
1410 EXPECT_EQ(last_header_cell_text, position_after.AnchorNode());
1411 EXPECT_EQ(8, position_after.GetPosition().OffsetInContainerNode());
1412
1413 // Same situation as above, but now create a text position inside the inline
1414 // text box representing the CSS content after the last header cell.
1415 ax_last_cell_css_after->LoadInlineTextBoxes();
1416 ASSERT_NE(nullptr, ax_last_cell_css_after->FirstChildIncludingIgnored());
1417 ax_position_after = AXPosition::CreateLastPositionInObject(
1418 *ax_last_cell_css_after->FirstChildIncludingIgnored());
1419 EXPECT_TRUE(ax_position_after.IsTextPosition());
1420 EXPECT_EQ(1, ax_position_after.TextOffset());
1421 position_after = ax_position_after.ToPositionWithAffinity(
1422 AXPositionAdjustmentBehavior::kMoveLeft);
1423 EXPECT_EQ(last_header_cell_text, position_after.AnchorNode());
1424 EXPECT_EQ(8, position_after.GetPosition().OffsetInContainerNode());
1425 }
1426
1427 //
1428 // Objects deriving from |AXMockObject|, e.g. table columns, are in the
1429 // accessibility tree but are neither in the DOM or layout trees.
1430 // Same for virtual nodes created using the Accessibility Object Model (AOM).
1431 //
1432
TEST_F(AccessibilityTest,PositionBeforeAndAfterTable)1433 TEST_F(AccessibilityTest, PositionBeforeAndAfterTable) {
1434 SetBodyInnerHTML(kHTMLTable);
1435 const Node* after = GetElementById("after");
1436 ASSERT_NE(nullptr, after);
1437 const AXObject* ax_table = GetAXObjectByElementId("table");
1438 ASSERT_NE(nullptr, ax_table);
1439 ASSERT_EQ(ax::mojom::Role::kTable, ax_table->RoleValue());
1440 const AXObject* ax_after = GetAXObjectByElementId("after");
1441 ASSERT_NE(nullptr, ax_after);
1442 ASSERT_EQ(ax::mojom::Role::kParagraph, ax_after->RoleValue());
1443
1444 const auto ax_position_before =
1445 AXPosition::CreatePositionBeforeObject(*ax_table);
1446 const auto position_before = ax_position_before.ToPositionWithAffinity();
1447 EXPECT_EQ(GetDocument().body(), position_before.AnchorNode());
1448 EXPECT_EQ(3, position_before.GetPosition().OffsetInContainerNode());
1449 const Node* table = position_before.GetPosition().ComputeNodeAfterPosition();
1450 ASSERT_NE(nullptr, table);
1451 EXPECT_EQ(GetElementById("table"), table);
1452
1453 const auto ax_position_before_from_dom =
1454 AXPosition::FromPosition(position_before);
1455 EXPECT_EQ(ax_position_before, ax_position_before_from_dom);
1456
1457 const auto ax_position_after =
1458 AXPosition::CreatePositionAfterObject(*ax_table);
1459 const auto position_after = ax_position_after.ToPositionWithAffinity();
1460 EXPECT_EQ(GetDocument().body(), position_after.AnchorNode());
1461 EXPECT_EQ(5, position_after.GetPosition().OffsetInContainerNode());
1462 const Node* node_after =
1463 position_after.GetPosition().ComputeNodeAfterPosition();
1464 EXPECT_EQ(after, node_after);
1465
1466 const auto ax_position_after_from_dom =
1467 AXPosition::FromPosition(position_after);
1468 EXPECT_EQ(ax_position_after, ax_position_after_from_dom);
1469 EXPECT_EQ(ax_after, ax_position_after_from_dom.ChildAfterTreePosition());
1470 }
1471
TEST_F(AccessibilityTest,PositionAtStartAndEndOfTable)1472 TEST_F(AccessibilityTest, PositionAtStartAndEndOfTable) {
1473 SetBodyInnerHTML(kHTMLTable);
1474
1475 // In the accessibility tree, the thead and tbody elements are ignored, but
1476 // they are used as anchors when converting an AX position to a DOM position
1477 // because they are the closest anchor to the first and last unignored AX
1478 // positions inside the table.
1479 const Node* thead = GetElementById("thead");
1480 ASSERT_NE(nullptr, thead);
1481 const Node* header_row = GetElementById("headerRow");
1482 ASSERT_NE(nullptr, header_row);
1483 const Node* tbody = GetElementById("tbody");
1484 ASSERT_NE(nullptr, tbody);
1485
1486 const AXObject* ax_table = GetAXObjectByElementId("table");
1487 ASSERT_NE(nullptr, ax_table);
1488 ASSERT_EQ(ax::mojom::Role::kTable, ax_table->RoleValue());
1489 const AXObject* ax_header_row = GetAXObjectByElementId("headerRow");
1490 ASSERT_NE(nullptr, ax_header_row);
1491 ASSERT_EQ(ax::mojom::Role::kRow, ax_header_row->RoleValue());
1492
1493 const auto ax_position_at_start =
1494 AXPosition::CreateFirstPositionInObject(*ax_table);
1495 const auto position_at_start = ax_position_at_start.ToPositionWithAffinity();
1496 EXPECT_EQ(thead, position_at_start.AnchorNode());
1497 EXPECT_EQ(1, position_at_start.GetPosition().OffsetInContainerNode());
1498 EXPECT_EQ(header_row,
1499 position_at_start.GetPosition().ComputeNodeAfterPosition());
1500
1501 const auto ax_position_at_start_from_dom =
1502 AXPosition::FromPosition(position_at_start);
1503 EXPECT_EQ(ax_position_at_start, ax_position_at_start_from_dom);
1504 EXPECT_EQ(ax_header_row,
1505 ax_position_at_start_from_dom.ChildAfterTreePosition());
1506
1507 const auto ax_position_at_end =
1508 AXPosition::CreateLastPositionInObject(*ax_table);
1509 const auto position_at_end = ax_position_at_end.ToPositionWithAffinity();
1510 EXPECT_EQ(tbody, position_at_end.AnchorNode());
1511 // There are three rows and a line break before and after each one.
1512 EXPECT_EQ(6, position_at_end.GetPosition().OffsetInContainerNode());
1513
1514 const auto ax_position_at_end_from_dom =
1515 AXPosition::FromPosition(position_at_end);
1516 EXPECT_EQ(ax_position_at_end, ax_position_at_end_from_dom);
1517 EXPECT_EQ(nullptr, ax_position_at_end_from_dom.ChildAfterTreePosition());
1518 }
1519
TEST_F(AccessibilityTest,PositionInTableHeader)1520 TEST_F(AccessibilityTest, PositionInTableHeader) {
1521 SetBodyInnerHTML(kHTMLTable);
1522
1523 const Node* header_row = GetElementById("headerRow");
1524 ASSERT_NE(nullptr, header_row);
1525 const Node* first_header_cell = GetElementById("firstHeaderCell");
1526 ASSERT_NE(nullptr, first_header_cell);
1527
1528 const AXObject* ax_first_header_cell =
1529 GetAXObjectByElementId("firstHeaderCell");
1530 ASSERT_NE(nullptr, ax_first_header_cell);
1531 ASSERT_EQ(ax::mojom::Role::kColumnHeader, ax_first_header_cell->RoleValue());
1532 const AXObject* ax_last_header_cell =
1533 GetAXObjectByElementId("lastHeaderCell");
1534 ASSERT_NE(nullptr, ax_last_header_cell);
1535 ASSERT_EQ(ax::mojom::Role::kColumnHeader, ax_last_header_cell->RoleValue());
1536
1537 const auto ax_position_before =
1538 AXPosition::CreatePositionBeforeObject(*ax_first_header_cell);
1539 const auto position_before = ax_position_before.ToPositionWithAffinity();
1540 EXPECT_EQ(header_row, position_before.AnchorNode());
1541 EXPECT_EQ(1, position_before.GetPosition().OffsetInContainerNode());
1542 EXPECT_EQ(first_header_cell,
1543 position_before.GetPosition().ComputeNodeAfterPosition());
1544
1545 const auto ax_position_before_from_dom =
1546 AXPosition::FromPosition(position_before);
1547 EXPECT_EQ(ax_position_before, ax_position_before_from_dom);
1548 EXPECT_EQ(ax_first_header_cell,
1549 ax_position_before_from_dom.ChildAfterTreePosition());
1550
1551 const auto ax_position_after =
1552 AXPosition::CreatePositionAfterObject(*ax_last_header_cell);
1553 const auto position_after = ax_position_after.ToPositionWithAffinity();
1554 EXPECT_EQ(header_row, position_after.AnchorNode());
1555 // There are three header cells and a line break before and after each one.
1556 EXPECT_EQ(6, position_after.GetPosition().OffsetInContainerNode());
1557
1558 const auto ax_position_after_from_dom =
1559 AXPosition::FromPosition(position_after);
1560 EXPECT_EQ(ax_position_after, ax_position_after_from_dom);
1561 EXPECT_EQ(nullptr, ax_position_after_from_dom.ChildAfterTreePosition());
1562 }
1563
TEST_F(AccessibilityTest,PositionInTableRow)1564 TEST_F(AccessibilityTest, PositionInTableRow) {
1565 SetBodyInnerHTML(kHTMLTable);
1566
1567 const Node* first_row = GetElementById("firstRow");
1568 ASSERT_NE(nullptr, first_row);
1569 const Node* first_cell = GetElementById("firstCell");
1570 ASSERT_NE(nullptr, first_cell);
1571 const Node* last_row = GetElementById("lastRow");
1572 ASSERT_NE(nullptr, last_row);
1573
1574 const AXObject* ax_first_cell = GetAXObjectByElementId("firstCell");
1575 ASSERT_NE(nullptr, ax_first_cell);
1576 ASSERT_EQ(ax::mojom::Role::kRowHeader, ax_first_cell->RoleValue());
1577 const AXObject* ax_last_cell = GetAXObjectByElementId("lastCell");
1578 ASSERT_NE(nullptr, ax_last_cell);
1579 ASSERT_EQ(ax::mojom::Role::kCell, ax_last_cell->RoleValue());
1580
1581 const auto ax_position_before =
1582 AXPosition::CreatePositionBeforeObject(*ax_first_cell);
1583 const auto position_before = ax_position_before.ToPositionWithAffinity();
1584 EXPECT_EQ(first_row, position_before.AnchorNode());
1585 EXPECT_EQ(1, position_before.GetPosition().OffsetInContainerNode());
1586 EXPECT_EQ(first_cell,
1587 position_before.GetPosition().ComputeNodeAfterPosition());
1588
1589 const auto ax_position_before_from_dom =
1590 AXPosition::FromPosition(position_before);
1591 EXPECT_EQ(ax_position_before, ax_position_before_from_dom);
1592 EXPECT_EQ(ax_first_cell,
1593 ax_position_before_from_dom.ChildAfterTreePosition());
1594
1595 const auto ax_position_after =
1596 AXPosition::CreatePositionAfterObject(*ax_last_cell);
1597 const auto position_after = ax_position_after.ToPositionWithAffinity();
1598 EXPECT_EQ(last_row, position_after.AnchorNode());
1599 // There are three cells on the last row and a line break before and after
1600 // each one.
1601 EXPECT_EQ(6, position_after.GetPosition().OffsetInContainerNode());
1602
1603 const auto ax_position_after_from_dom =
1604 AXPosition::FromPosition(position_after);
1605 EXPECT_EQ(ax_position_after, ax_position_after_from_dom);
1606 EXPECT_EQ(nullptr, ax_position_after_from_dom.ChildAfterTreePosition());
1607 }
1608
TEST_F(AccessibilityTest,DISABLED_PositionInVirtualAOMNode)1609 TEST_F(AccessibilityTest, DISABLED_PositionInVirtualAOMNode) {
1610 ScopedAccessibilityObjectModelForTest(true);
1611 SetBodyInnerHTML(kAOM);
1612
1613 const Node* parent = GetElementById("aomParent");
1614 ASSERT_NE(nullptr, parent);
1615 const Node* after = GetElementById("after");
1616 ASSERT_NE(nullptr, after);
1617
1618 const AXObject* ax_parent = GetAXObjectByElementId("aomParent");
1619 ASSERT_NE(nullptr, ax_parent);
1620 ASSERT_EQ(ax::mojom::Role::kGenericContainer, ax_parent->RoleValue());
1621 ASSERT_EQ(1, ax_parent->ChildCountIncludingIgnored());
1622 const AXObject* ax_button = ax_parent->FirstChildIncludingIgnored();
1623 ASSERT_NE(nullptr, ax_button);
1624 ASSERT_EQ(ax::mojom::Role::kButton, ax_button->RoleValue());
1625 const AXObject* ax_after = GetAXObjectByElementId("after");
1626 ASSERT_NE(nullptr, ax_after);
1627 ASSERT_EQ(ax::mojom::Role::kParagraph, ax_after->RoleValue());
1628
1629 const auto ax_position_before =
1630 AXPosition::CreatePositionBeforeObject(*ax_button);
1631 const auto position_before = ax_position_before.ToPositionWithAffinity();
1632 EXPECT_EQ(parent, position_before.AnchorNode());
1633 EXPECT_TRUE(position_before.GetPosition().IsBeforeChildren());
1634 EXPECT_EQ(nullptr, position_before.GetPosition().ComputeNodeAfterPosition());
1635
1636 const auto ax_position_before_from_dom =
1637 AXPosition::FromPosition(position_before);
1638 EXPECT_EQ(ax_position_before, ax_position_before_from_dom);
1639 EXPECT_EQ(ax_button, ax_position_before_from_dom.ChildAfterTreePosition());
1640
1641 const auto ax_position_after =
1642 AXPosition::CreatePositionAfterObject(*ax_button);
1643 const auto position_after = ax_position_after.ToPositionWithAffinity();
1644 EXPECT_EQ(after, position_after.AnchorNode());
1645 EXPECT_TRUE(position_after.GetPosition().IsBeforeChildren());
1646 EXPECT_EQ(nullptr, position_after.GetPosition().ComputeNodeAfterPosition());
1647
1648 const auto ax_position_after_from_dom =
1649 AXPosition::FromPosition(position_after);
1650 EXPECT_EQ(ax_position_after, ax_position_after_from_dom);
1651 EXPECT_EQ(ax_after, ax_position_after_from_dom.ChildAfterTreePosition());
1652 }
1653
TEST_F(AccessibilityTest,PositionInInvalidMapLayout)1654 TEST_F(AccessibilityTest, PositionInInvalidMapLayout) {
1655 SetBodyInnerHTML(kMap);
1656
1657 Node* br = GetElementById("br");
1658 ASSERT_NE(nullptr, br);
1659 Node* map = GetElementById("map");
1660 ASSERT_NE(nullptr, map);
1661
1662 // Create an invalid layout by appending a child to the <br>
1663 br->appendChild(map);
1664 GetDocument().UpdateStyleAndLayoutTree();
1665
1666 const AXObject* ax_map = GetAXObjectByElementId("map");
1667 ASSERT_NE(nullptr, ax_map);
1668 ASSERT_EQ(ax::mojom::Role::kGenericContainer, ax_map->RoleValue());
1669
1670 const auto ax_position_before =
1671 AXPosition::CreatePositionBeforeObject(*ax_map);
1672 const auto position_before = ax_position_before.ToPositionWithAffinity();
1673 EXPECT_EQ(nullptr, position_before.AnchorNode());
1674 EXPECT_EQ(0, position_before.GetPosition().OffsetInContainerNode());
1675
1676 const auto ax_position_after = AXPosition::CreatePositionAfterObject(*ax_map);
1677 const auto position_after = ax_position_after.ToPositionWithAffinity();
1678 EXPECT_EQ(nullptr, position_after.AnchorNode());
1679 EXPECT_EQ(0, position_after.GetPosition().OffsetInContainerNode());
1680 }
1681
TEST_P(ParameterizedAccessibilityTest,ToPositionWithAffinityWithMultipleInlineTextBoxes)1682 TEST_P(ParameterizedAccessibilityTest,
1683 ToPositionWithAffinityWithMultipleInlineTextBoxes) {
1684 // This test expects the starting offset of the last InlineTextBox object to
1685 // equate the sum of the previous inline text boxes' length, without the
1686 // collapsed white-spaces.
1687 //
1688 // "
" is a Line Feed ("\n").
1689 SetBodyInnerHTML(
1690 R"HTML(<style>p { white-space: pre-line; }</style>
1691 <p id="paragraph">Hello world</p>)HTML");
1692
1693 const Node* text = GetElementById("paragraph")->firstChild();
1694 ASSERT_NE(nullptr, text);
1695 ASSERT_TRUE(text->IsTextNode());
1696 AXObject* ax_static_text =
1697 GetAXObjectByElementId("paragraph")->FirstChildIncludingIgnored();
1698
1699 ASSERT_NE(nullptr, ax_static_text);
1700 ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
1701
1702 ax_static_text->LoadInlineTextBoxes();
1703 ASSERT_EQ(3, ax_static_text->UnignoredChildCount());
1704
1705 // The last inline text box should be:
1706 // "InlineTextBox" name="world"
1707 const AXObject* ax_last_inline_box =
1708 ax_static_text->LastChildIncludingIgnored();
1709 const auto ax_position =
1710 AXPosition::CreatePositionBeforeObject(*ax_last_inline_box);
1711 const auto position = ax_position.ToPositionWithAffinity();
1712 // The resulting DOM position should be:
1713 // DOM position #text "Hello \n world"@offsetInAnchor[8]
1714 ASSERT_TRUE(position.GetPosition().IsOffsetInAnchor());
1715 EXPECT_EQ(8, position.GetPosition().OffsetInContainerNode());
1716 }
1717
1718 } // namespace test
1719 } // namespace blink
1720