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 "base/check.h"
6#include "base/strings/sys_string_conversions.h"
7#include "content/browser/accessibility/browser_accessibility.h"
8#include "content/browser/accessibility/browser_accessibility_cocoa.h"
9#include "content/browser/accessibility/browser_accessibility_mac.h"
10#include "content/browser/accessibility/browser_accessibility_manager.h"
11#include "content/browser/accessibility/browser_accessibility_manager_mac.h"
12#include "content/browser/web_contents/web_contents_impl.h"
13#include "content/public/test/accessibility_notification_waiter.h"
14#include "content/public/test/browser_test.h"
15#include "content/public/test/browser_test_utils.h"
16#include "content/public/test/content_browser_test.h"
17#include "content/public/test/content_browser_test_utils.h"
18#include "content/public/test/test_utils.h"
19#include "content/shell/browser/shell.h"
20#include "net/base/data_url.h"
21#include "testing/gtest/include/gtest/gtest.h"
22#include "testing/gtest_mac.h"
23#include "url/gurl.h"
24
25namespace content {
26
27namespace {
28
29class BrowserAccessibilityCocoaBrowserTest : public ContentBrowserTest {
30 public:
31  BrowserAccessibilityCocoaBrowserTest() {}
32  ~BrowserAccessibilityCocoaBrowserTest() override {}
33
34 protected:
35  BrowserAccessibility* FindNode(ax::mojom::Role role) {
36    BrowserAccessibility* root = GetManager()->GetRoot();
37    CHECK(root);
38    return FindNodeInSubtree(*root, role);
39  }
40
41  BrowserAccessibilityManager* GetManager() {
42    WebContentsImpl* web_contents =
43        static_cast<WebContentsImpl*>(shell()->web_contents());
44    return web_contents->GetRootBrowserAccessibilityManager();
45  }
46
47  // Trigger a context menu for the provided element without showing it. Returns
48  // the coordinates where the  context menu was invoked (calculated based on
49  // the provided element). These coordinates are relative to the RenderView
50  // origin.
51  gfx::Point TriggerContextMenuAndGetMenuLocation(
52      NSAccessibilityElement* element,
53      ContextMenuFilter* filter) {
54    // accessibilityPerformAction is deprecated, but it's still used internally
55    // by AppKit.
56#pragma clang diagnostic push
57#pragma clang diagnostic ignored "-Wdeprecated-declarations"
58    [element accessibilityPerformAction:NSAccessibilityShowMenuAction];
59    filter->Wait();
60
61    UntrustworthyContextMenuParams context_menu_params = filter->get_params();
62    return gfx::Point(context_menu_params.x, context_menu_params.y);
63#pragma clang diagnostic pop
64  }
65
66  void FocusAccessibilityElementAndWaitForFocusChange(
67      NSAccessibilityElement* element) {
68#pragma clang diagnostic push
69#pragma clang diagnostic ignored "-Wdeprecated-declarations"
70    [element accessibilitySetValue:@(1)
71                      forAttribute:NSAccessibilityFocusedAttribute];
72#pragma clang diagnostic pop
73    WaitForAccessibilityFocusChange();
74  }
75
76 private:
77  BrowserAccessibility* FindNodeInSubtree(BrowserAccessibility& node,
78                                          ax::mojom::Role role) {
79    if (node.GetRole() == role)
80      return &node;
81    for (BrowserAccessibility::PlatformChildIterator it =
82             node.PlatformChildrenBegin();
83         it != node.PlatformChildrenEnd(); ++it) {
84      BrowserAccessibility* result = FindNodeInSubtree(*it, role);
85      if (result)
86        return result;
87    }
88    return nullptr;
89  }
90};
91
92}  // namespace
93
94IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
95                       AXTextMarkerForTextEdit) {
96  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
97
98  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
99                                         ui::kAXModeComplete,
100                                         ax::mojom::Event::kLoadComplete);
101  GURL url(R"HTML(data:text/html,
102             <input />)HTML");
103
104  EXPECT_TRUE(NavigateToURL(shell(), url));
105  waiter.WaitForNotification();
106
107  BrowserAccessibility* text_field = FindNode(ax::mojom::Role::kTextField);
108  ASSERT_NE(nullptr, text_field);
109  EXPECT_TRUE(content::ExecuteScript(
110      shell()->web_contents(), "document.querySelector('input').focus()"));
111
112  content::SimulateKeyPress(shell()->web_contents(),
113                            ui::DomKey::FromCharacter('B'), ui::DomCode::US_B,
114                            ui::VKEY_B, false, false, false, false);
115
116  base::scoped_nsobject<BrowserAccessibilityCocoa> cocoa_text_field(
117      [ToBrowserAccessibilityCocoa(text_field) retain]);
118  AccessibilityNotificationWaiter value_waiter(shell()->web_contents(),
119                                               ui::kAXModeComplete,
120                                               ax::mojom::Event::kValueChanged);
121  value_waiter.WaitForNotification();
122  AXTextEdit text_edit = [cocoa_text_field computeTextEdit];
123  EXPECT_NE(text_edit.edit_text_marker, nil);
124
125  EXPECT_EQ(
126      content::AXTextMarkerToPosition(text_edit.edit_text_marker)->ToString(),
127      "TextPosition anchor_id=4 text_offset=1 affinity=downstream "
128      "annotated_text=B<>");
129}
130
131IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
132                       AXCellForColumnAndRow) {
133  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
134
135  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
136                                         ui::kAXModeComplete,
137                                         ax::mojom::Event::kLoadComplete);
138  GURL url(R"HTML(data:text/html,
139             <table>
140               <thead style=display:block>
141                 <tr>
142                   <th>Name</th>
143                   <th>LDAP</th>
144                 </tr>
145               </thead>
146               <tbody style=display:block>
147                 <tr>
148                   <td>John Doe</td>
149                   <td>johndoe@</td>
150                 </tr>
151                 <tr>
152                   <td>Jenny Doe</td>
153                   <td>jennydoe@</td>
154                 </tr>
155               </tbody>
156             </table>)HTML");
157
158  EXPECT_TRUE(NavigateToURL(shell(), url));
159  waiter.WaitForNotification();
160
161  BrowserAccessibility* table = FindNode(ax::mojom::Role::kTable);
162  ASSERT_NE(nullptr, table);
163  base::scoped_nsobject<BrowserAccessibilityCocoa> cocoa_table(
164      [ToBrowserAccessibilityCocoa(table) retain]);
165
166  // Test AXCellForColumnAndRow for four coordinates
167  for (unsigned col = 0; col < 2; col++) {
168    for (unsigned row = 0; row < 2; row++) {
169      base::scoped_nsobject<BrowserAccessibilityCocoa> cell(
170          [[cocoa_table accessibilityCellForColumn:col row:row] retain]);
171
172      // It should be a cell.
173      EXPECT_NSEQ(@"AXCell", [cell accessibilityRole]);
174
175      // The column index and row index of the cell should match what we asked
176      // for.
177      EXPECT_NSEQ(NSMakeRange(col, 1), [cell accessibilityColumnIndexRange]);
178      EXPECT_NSEQ(NSMakeRange(row, 1), [cell accessibilityRowIndexRange]);
179    }
180  }
181}
182
183IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
184                       TestCoordinatesAreInScreenSpace) {
185  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
186
187  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
188                                         ui::kAXModeComplete,
189                                         ax::mojom::Event::kLoadComplete);
190
191  GURL url(R"HTML(data:text/html, <p>Hello, world!</p>)HTML");
192
193  EXPECT_TRUE(NavigateToURL(shell(), url));
194  waiter.WaitForNotification();
195
196  BrowserAccessibility* text = FindNode(ax::mojom::Role::kStaticText);
197  ASSERT_NE(nullptr, text);
198
199  BrowserAccessibilityCocoa* cocoa_text = ToBrowserAccessibilityCocoa(text);
200  ASSERT_NE(nil, cocoa_text);
201
202  NSPoint position = [[cocoa_text position] pointValue];
203  NSSize size = [[cocoa_text size] sizeValue];
204  NSRect frame = NSMakeRect(position.x, position.y, size.width, size.height);
205
206  NSPoint p0_before = position;
207  NSRect r0_before = [cocoa_text frameForRange:NSMakeRange(0, 5)];
208  EXPECT_TRUE(CGRectContainsRect(frame, r0_before));
209
210  // On macOS geometry accessibility attributes are expected to use the
211  // screen coordinate system with the origin at the bottom left corner.
212  // We need some creativity with testing this because it is challenging
213  // to setup a text element with a precise screen position.
214  //
215  // Content shell's window is pinned to have an origin at (0, 0), so
216  // when its height is changed the content's screen y-coordinate is
217  // changed by the same amount (see below).
218  //
219  //      Y^      original
220  //       |
221  //       +--------------------------------------------+
222  //       |                                            |
223  //       |                                            |
224  //       |                                            |
225  //       |                                            |
226  //     h +---------------------------+                |
227  //       |      Content Shell        |                |
228  //       |---------------------------|                |
229  //     y |Hello, world               |                |
230  //       |                           |                |
231  //       |                           |          Screen|
232  //       +---------------------------+----------------+-->
233  //      0                                               X
234  //
235  //      Y^       content shell enlarged
236  //       |
237  //       +--------------------------------------------+
238  //       |                                            |
239  //       |                                            |
240  //  h+dh +---------------------------+                |
241  //       |      Content Shell        |                |
242  //       |---------------------------|                |
243  //  y+dh |Hello, world               |                |
244  //       |                           |                |
245  //       |                           |                |
246  //       |                           |                |
247  //       |                           |          Screen|
248  //       +---------------------------+----------------+-->
249  //      0                                               X
250  //
251  // This observation allows us to validate the returned
252  // attribute values and catch the most glaring mistakes
253  // in coordinate space handling.
254
255  const int dh = 100;
256  gfx::Size content_size = Shell::GetShellDefaultSize();
257  content_size.Enlarge(0, dh);
258  shell()->ResizeWebContentForTests(content_size);
259
260  NSPoint p0_after = [[cocoa_text position] pointValue];
261  NSRect r0_after = [cocoa_text frameForRange:NSMakeRange(0, 5)];
262
263  ASSERT_EQ(p0_before.y + dh, p0_after.y);
264  ASSERT_EQ(r0_before.origin.y + dh, r0_after.origin.y);
265  ASSERT_EQ(r0_before.size.height, r0_after.size.height);
266}
267
268IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
269                       TestUnlabeledImageRoleDescription) {
270  ui::AXTreeUpdate tree;
271  tree.root_id = 1;
272  tree.nodes.resize(3);
273  tree.nodes[0].id = 1;
274  tree.nodes[0].child_ids = {2, 3};
275
276  tree.nodes[1].id = 2;
277  tree.nodes[1].role = ax::mojom::Role::kImage;
278  tree.nodes[1].AddStringAttribute(ax::mojom::StringAttribute::kRoleDescription,
279                                   "foo");
280  tree.nodes[1].SetImageAnnotationStatus(
281      ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation);
282
283  tree.nodes[2].id = 3;
284  tree.nodes[2].role = ax::mojom::Role::kImage;
285  tree.nodes[2].AddStringAttribute(ax::mojom::StringAttribute::kRoleDescription,
286                                   "bar");
287  tree.nodes[2].SetImageAnnotationStatus(
288      ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation);
289
290  std::unique_ptr<BrowserAccessibilityManagerMac> manager(
291      new BrowserAccessibilityManagerMac(tree, nullptr));
292
293  for (int child_index = 0; child_index < int{tree.nodes[0].child_ids.size()};
294       ++child_index) {
295    BrowserAccessibility* child =
296        manager->GetRoot()->PlatformGetChild(child_index);
297    base::scoped_nsobject<BrowserAccessibilityCocoa> child_obj(
298        [ToBrowserAccessibilityCocoa(child) retain]);
299
300    EXPECT_NSEQ(@"Unlabeled image", [child_obj roleDescription]);
301  }
302}
303
304IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
305                       TestAnnotatedImageDescription) {
306  std::vector<const char*> expected_descriptions;
307
308  ui::AXTreeUpdate tree;
309  tree.root_id = 1;
310  tree.nodes.resize(11);
311  tree.nodes[0].id = 1;
312  tree.nodes[0].child_ids = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
313
314  // If the status is EligibleForAnnotation and there's no existing label,
315  // the description should be the discoverability string.
316  tree.nodes[1].id = 2;
317  tree.nodes[1].role = ax::mojom::Role::kImage;
318  tree.nodes[1].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
319                                   "Annotation");
320  tree.nodes[1].SetImageAnnotationStatus(
321      ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation);
322  expected_descriptions.push_back(
323      "To get missing image descriptions, open the context menu.");
324
325  // If the status is EligibleForAnnotation, the discoverability string
326  // should be appended to the existing name.
327  tree.nodes[2].id = 3;
328  tree.nodes[2].role = ax::mojom::Role::kImage;
329  tree.nodes[2].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
330                                   "Annotation");
331  tree.nodes[2].SetName("ExistingLabel");
332  tree.nodes[2].SetImageAnnotationStatus(
333      ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation);
334  expected_descriptions.push_back(
335      "ExistingLabel. To get missing image descriptions, open the context "
336      "menu.");
337
338  // If the status is SilentlyEligibleForAnnotation, the discoverability string
339  // should not be appended to the existing name.
340  tree.nodes[3].id = 4;
341  tree.nodes[3].role = ax::mojom::Role::kImage;
342  tree.nodes[3].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
343                                   "Annotation");
344  tree.nodes[3].SetName("ExistingLabel");
345  tree.nodes[3].SetImageAnnotationStatus(
346      ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation);
347  expected_descriptions.push_back("ExistingLabel");
348
349  // If the status is IneligibleForAnnotation, nothing should be appended.
350  tree.nodes[4].id = 5;
351  tree.nodes[4].role = ax::mojom::Role::kImage;
352  tree.nodes[4].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
353                                   "Annotation");
354  tree.nodes[4].SetName("ExistingLabel");
355  tree.nodes[4].SetImageAnnotationStatus(
356      ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation);
357  expected_descriptions.push_back("ExistingLabel");
358
359  // If the status is AnnotationPending, pending text should be appended
360  // to the name.
361  tree.nodes[5].id = 6;
362  tree.nodes[5].role = ax::mojom::Role::kImage;
363  tree.nodes[5].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
364                                   "Annotation");
365  tree.nodes[5].SetName("ExistingLabel");
366  tree.nodes[5].SetImageAnnotationStatus(
367      ax::mojom::ImageAnnotationStatus::kAnnotationPending);
368  expected_descriptions.push_back("ExistingLabel. Getting description…");
369
370  // If the status is AnnotationSucceeded, and there's no annotation,
371  // nothing should be appended. (Ideally this shouldn't happen.)
372  tree.nodes[6].id = 7;
373  tree.nodes[6].role = ax::mojom::Role::kImage;
374  tree.nodes[6].SetName("ExistingLabel");
375  tree.nodes[6].SetImageAnnotationStatus(
376      ax::mojom::ImageAnnotationStatus::kAnnotationSucceeded);
377  expected_descriptions.push_back("ExistingLabel");
378
379  // If the status is AnnotationSucceeded, the annotation should be appended
380  // to the existing label.
381  tree.nodes[7].id = 8;
382  tree.nodes[7].role = ax::mojom::Role::kImage;
383  tree.nodes[7].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
384                                   "Annotation");
385  tree.nodes[7].SetName("ExistingLabel");
386  tree.nodes[7].SetImageAnnotationStatus(
387      ax::mojom::ImageAnnotationStatus::kAnnotationSucceeded);
388  expected_descriptions.push_back("ExistingLabel. Annotation");
389
390  // If the status is AnnotationEmpty, failure text should be added to the
391  // name.
392  tree.nodes[8].id = 9;
393  tree.nodes[8].role = ax::mojom::Role::kImage;
394  tree.nodes[8].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
395                                   "Annotation");
396  tree.nodes[8].SetName("ExistingLabel");
397  tree.nodes[8].SetImageAnnotationStatus(
398      ax::mojom::ImageAnnotationStatus::kAnnotationEmpty);
399  expected_descriptions.push_back("ExistingLabel. No description available.");
400
401  // If the status is AnnotationAdult, appropriate text should be appended
402  // to the name.
403  tree.nodes[9].id = 10;
404  tree.nodes[9].role = ax::mojom::Role::kImage;
405  tree.nodes[9].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
406                                   "Annotation");
407  tree.nodes[9].SetName("ExistingLabel");
408  tree.nodes[9].SetImageAnnotationStatus(
409      ax::mojom::ImageAnnotationStatus::kAnnotationAdult);
410  expected_descriptions.push_back("ExistingLabel. Appears to contain adult "
411                                  "content. No description available.");
412
413  // If the status is AnnotationProcessFailed, failure text should be added
414  // to the name.
415  tree.nodes[10].id = 11;
416  tree.nodes[10].role = ax::mojom::Role::kImage;
417  tree.nodes[10].AddStringAttribute(
418      ax::mojom::StringAttribute::kImageAnnotation, "Annotation");
419  tree.nodes[10].SetName("ExistingLabel");
420  tree.nodes[10].SetImageAnnotationStatus(
421      ax::mojom::ImageAnnotationStatus::kAnnotationProcessFailed);
422  expected_descriptions.push_back("ExistingLabel. No description available.");
423
424  // We should have one expected description per child of the root.
425  ASSERT_EQ(expected_descriptions.size(), tree.nodes[0].child_ids.size());
426  int child_count = static_cast<int>(expected_descriptions.size());
427
428  std::unique_ptr<BrowserAccessibilityManagerMac> manager(
429      new BrowserAccessibilityManagerMac(tree, nullptr));
430
431  for (int child_index = 0; child_index < child_count; child_index++) {
432    BrowserAccessibility* child =
433        manager->GetRoot()->PlatformGetChild(child_index);
434    base::scoped_nsobject<BrowserAccessibilityCocoa> child_obj(
435        [ToBrowserAccessibilityCocoa(child) retain]);
436
437    EXPECT_NSEQ(base::SysUTF8ToNSString(expected_descriptions[child_index]),
438                [child_obj descriptionForAccessibility]);
439  }
440}
441
442IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
443                       TestTableGetRowNodesNestedRows) {
444  // rootWebArea(#1)
445  // ++grid(#2)
446  // ++++row(#3)
447  // ++++++columnHeader(#4)
448  // ++++++columnHeader(#5)
449  // ++++genericContainer(#6)
450  // ++++++row(#7)
451  // ++++++++cell(#8)
452  // ++++++++cell(#9)
453  // ++++++row(#10)
454  // ++++++++cell(#11)
455  // ++++++++cell(#12)
456
457  ui::AXTreeUpdate tree;
458  tree.root_id = 1;
459  tree.nodes.resize(12);
460  tree.nodes[0].id = 1;
461  tree.nodes[0].role = ax::mojom::Role::kRootWebArea;
462  tree.nodes[0].child_ids = {2};
463
464  tree.nodes[1].id = 2;
465  tree.nodes[1].role = ax::mojom::Role::kGrid;
466  tree.nodes[1].child_ids = {3, 6};
467
468  tree.nodes[2].id = 3;
469  tree.nodes[2].role = ax::mojom::Role::kRow;
470  tree.nodes[2].AddStringAttribute(ax::mojom::StringAttribute::kName, "row1");
471  tree.nodes[2].child_ids = {4, 5};
472
473  tree.nodes[3].id = 4;
474  tree.nodes[3].role = ax::mojom::Role::kColumnHeader;
475  tree.nodes[3].AddStringAttribute(ax::mojom::StringAttribute::kName,
476                                   "header1");
477
478  tree.nodes[4].id = 5;
479  tree.nodes[4].role = ax::mojom::Role::kColumnHeader;
480  tree.nodes[4].AddStringAttribute(ax::mojom::StringAttribute::kName,
481                                   "header2");
482
483  tree.nodes[5].id = 6;
484  tree.nodes[5].role = ax::mojom::Role::kGenericContainer;
485  tree.nodes[5].child_ids = {7, 10};
486
487  tree.nodes[6].id = 7;
488  tree.nodes[6].role = ax::mojom::Role::kRow;
489  tree.nodes[6].AddStringAttribute(ax::mojom::StringAttribute::kName, "row2");
490  tree.nodes[6].child_ids = {8, 9};
491
492  tree.nodes[7].id = 8;
493  tree.nodes[7].role = ax::mojom::Role::kCell;
494  tree.nodes[7].AddStringAttribute(ax::mojom::StringAttribute::kName,
495                                   "cell1_row2");
496
497  tree.nodes[8].id = 9;
498  tree.nodes[8].role = ax::mojom::Role::kCell;
499  tree.nodes[8].AddStringAttribute(ax::mojom::StringAttribute::kName,
500                                   "cell2_row2");
501
502  tree.nodes[9].id = 10;
503  tree.nodes[9].role = ax::mojom::Role::kRow;
504  tree.nodes[9].AddStringAttribute(ax::mojom::StringAttribute::kName, "row3");
505  tree.nodes[9].child_ids = {11, 12};
506
507  tree.nodes[10].id = 11;
508  tree.nodes[10].role = ax::mojom::Role::kCell;
509  tree.nodes[10].AddStringAttribute(ax::mojom::StringAttribute::kName,
510                                    "cell1_row3");
511
512  tree.nodes[11].id = 12;
513  tree.nodes[11].role = ax::mojom::Role::kCell;
514  tree.nodes[11].AddStringAttribute(ax::mojom::StringAttribute::kName,
515                                    "cell2_row3");
516
517  std::unique_ptr<BrowserAccessibilityManagerMac> manager(
518      new BrowserAccessibilityManagerMac(tree, nullptr));
519
520  BrowserAccessibility* table = manager->GetRoot()->PlatformGetChild(0);
521  base::scoped_nsobject<BrowserAccessibilityCocoa> table_obj(
522      [ToBrowserAccessibilityCocoa(table) retain]);
523  NSArray* row_nodes = [table_obj rows];
524
525  EXPECT_EQ(3U, [row_nodes count]);
526  EXPECT_NSEQ(@"AXRow", [row_nodes[0] role]);
527  EXPECT_NSEQ(@"row1", [row_nodes[0] descriptionForAccessibility]);
528
529  EXPECT_NSEQ(@"AXRow", [row_nodes[1] role]);
530  EXPECT_NSEQ(@"row2", [row_nodes[1] descriptionForAccessibility]);
531
532  EXPECT_NSEQ(@"AXRow", [row_nodes[2] role]);
533  EXPECT_NSEQ(@"row3", [row_nodes[2] descriptionForAccessibility]);
534}
535
536IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
537                       TestTableGetRowNodesIndirectChildIds) {
538  // rootWebArea(#1)
539  // ++column(#2), indirectChildIds={3, 4}
540  // ++row(#3)
541  // ++row(#4)
542
543  ui::AXTreeUpdate tree;
544  tree.root_id = 1;
545  tree.nodes.resize(4);
546
547  tree.nodes[0].id = 1;
548  tree.nodes[0].role = ax::mojom::Role::kRootWebArea;
549  tree.nodes[0].child_ids = {2, 3, 4};
550
551  tree.nodes[1].id = 2;
552  tree.nodes[1].role = ax::mojom::Role::kColumn;
553  tree.nodes[1].AddStringAttribute(ax::mojom::StringAttribute::kName,
554                                   "column1");
555  tree.nodes[1].AddIntListAttribute(
556      ax::mojom::IntListAttribute::kIndirectChildIds,
557      std::vector<int32_t>{3, 4});
558
559  tree.nodes[2].id = 3;
560  tree.nodes[2].role = ax::mojom::Role::kRow;
561  tree.nodes[2].AddStringAttribute(ax::mojom::StringAttribute::kName, "row1");
562
563  tree.nodes[3].id = 4;
564  tree.nodes[3].role = ax::mojom::Role::kRow;
565  tree.nodes[3].AddStringAttribute(ax::mojom::StringAttribute::kName, "row2");
566
567  std::unique_ptr<BrowserAccessibilityManagerMac> manager(
568      new BrowserAccessibilityManagerMac(tree, nullptr));
569
570  BrowserAccessibility* column = manager->GetRoot()->PlatformGetChild(0);
571  base::scoped_nsobject<BrowserAccessibilityCocoa> col_obj(
572      [ToBrowserAccessibilityCocoa(column) retain]);
573  EXPECT_NSEQ(@"AXColumn", [col_obj role]);
574  EXPECT_NSEQ(@"column1", [col_obj descriptionForAccessibility]);
575
576  NSArray* row_nodes = [col_obj rows];
577  EXPECT_NSEQ(@"AXRow", [row_nodes[0] role]);
578  EXPECT_NSEQ(@"row1", [row_nodes[0] descriptionForAccessibility]);
579
580  EXPECT_NSEQ(@"AXRow", [row_nodes[1] role]);
581  EXPECT_NSEQ(@"row2", [row_nodes[1] descriptionForAccessibility]);
582}
583
584IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
585                       TestTreeContextMenuEvent) {
586  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
587                                         ui::kAXModeComplete,
588                                         ax::mojom::Event::kLoadComplete);
589
590  GURL url(R"HTML(data:text/html,
591             <div alt="tree" role="tree">
592               <div tabindex="1" role="treeitem">1</div>
593               <div tabindex="2" role="treeitem">2</div>
594             </div>)HTML");
595
596  EXPECT_TRUE(NavigateToURL(shell(), url));
597  waiter.WaitForNotification();
598
599  BrowserAccessibility* tree = FindNode(ax::mojom::Role::kTree);
600  base::scoped_nsobject<BrowserAccessibilityCocoa> cocoa_tree(
601      [ToBrowserAccessibilityCocoa(tree) retain]);
602
603  NSArray* tree_children = [cocoa_tree children];
604  EXPECT_NSEQ(@"AXRow", [tree_children[0] role]);
605  EXPECT_NSEQ(@"AXRow", [tree_children[1] role]);
606
607  content::RenderProcessHost* render_process_host =
608      shell()->web_contents()->GetMainFrame()->GetProcess();
609  auto menu_filter = base::MakeRefCounted<ContextMenuFilter>(
610      ContextMenuFilter::ShowBehavior::kPreventShow);
611  render_process_host->AddFilter(menu_filter.get());
612
613  gfx::Point tree_point =
614      TriggerContextMenuAndGetMenuLocation(cocoa_tree, menu_filter.get());
615
616  menu_filter->Reset();
617  gfx::Point item_1_point =
618      TriggerContextMenuAndGetMenuLocation(tree_children[1], menu_filter.get());
619  ASSERT_NE(tree_point, item_1_point);
620
621  // Now focus the second child and trigger a context menu on the tree.
622  EXPECT_TRUE(
623      content::ExecuteScript(shell()->web_contents(),
624                             "document.body.children[0].children[1].focus();"));
625  WaitForAccessibilityFocusChange();
626
627  // Triggering a context menu on the tree should now trigger the menu
628  // on the focused child.
629  menu_filter->Reset();
630  gfx::Point new_point =
631      TriggerContextMenuAndGetMenuLocation(cocoa_tree, menu_filter.get());
632  ASSERT_EQ(new_point, item_1_point);
633}
634
635IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
636                       TestEventRetargetingFocus) {
637  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
638                                         ui::kAXModeComplete,
639                                         ax::mojom::Event::kLoadComplete);
640
641  GURL url(R"HTML(data:text/html,
642             <div role="tree">
643               <div tabindex="1" role="treeitem">1</div>
644               <div tabindex="2" role="treeitem">2</div>
645             </div>
646             <div role="treegrid">
647               <div tabindex="1" role="treeitem">1</div>
648               <div tabindex="2" role="treeitem">2</div>
649             </div>
650             <div role="tablist">
651               <div tabindex="1" role="tab">1</div>
652               <div tabindex="2" role="tab">2</div>
653             </div>
654             <div role="table">
655               <div tabindex="1" role="row">1</div>
656               <div tabindex="2" role="row">2</div>
657             </div>
658             <div role="banner">
659               <div tabindex="1" role="link">1</div>
660               <div tabindex="2" role="link">2</div>
661             </div>)HTML");
662
663  EXPECT_TRUE(NavigateToURL(shell(), url));
664  waiter.WaitForNotification();
665
666  std::pair<ax::mojom::Role, bool> tests[] = {
667      std::make_pair(ax::mojom::Role::kTree, true),
668      std::make_pair(ax::mojom::Role::kTreeGrid, true),
669      std::make_pair(ax::mojom::Role::kTabList, true),
670      std::make_pair(ax::mojom::Role::kTable, false),
671      std::make_pair(ax::mojom::Role::kBanner, false),
672  };
673
674  for (auto& test : tests) {
675    base::scoped_nsobject<BrowserAccessibilityCocoa> parent(
676        [ToBrowserAccessibilityCocoa(FindNode(test.first)) retain]);
677    BrowserAccessibilityCocoa* child = [parent children][1];
678
679    EXPECT_NE(nullptr, parent.get());
680    EXPECT_EQ([child owner], [child actionTarget]);
681    EXPECT_EQ([parent owner], [parent actionTarget]);
682
683    FocusAccessibilityElementAndWaitForFocusChange(child);
684    ASSERT_EQ(test.second, [parent actionTarget] == [child owner]);
685  }
686}
687
688IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
689                       TestEventRetargetingActiveDescendant) {
690  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
691                                         ui::kAXModeComplete,
692                                         ax::mojom::Event::kLoadComplete);
693
694  GURL url(R"HTML(data:text/html,
695             <div role="tree" aria-activedescendant="tree-child">
696               <div tabindex="1" role="treeitem">1</div>
697               <div id="tree-child" tabindex="2" role="treeitem">2</div>
698             </div>
699             <div role="treegrid" aria-activedescendant="treegrid-child">
700               <div tabindex="1" role="treeitem">1</div>
701               <div id="treegrid-child" tabindex="2" role="treeitem">2</div>
702             </div>
703             <div role="tablist" aria-activedescendant="tablist-child">
704               <div tabindex="1" role="tab">1</div>
705               <div id="tablist-child" tabindex="2" role="tab">2</div>
706             </div>
707             <div role="table" aria-activedescendant="table-child">
708               <div tabindex="1" role="row">1</div>
709               <div id="table-child" tabindex="2" role="row">2</div>
710             </div>
711             <div role="banner" aria-activedescendant="banner-child">
712               <div tabindex="1" role="link">1</div>
713               <div id="banner-child" tabindex="2" role="link">2</div>
714             </div>)HTML");
715
716  EXPECT_TRUE(NavigateToURL(shell(), url));
717  waiter.WaitForNotification();
718
719  std::pair<ax::mojom::Role, bool> tests[] = {
720      std::make_pair(ax::mojom::Role::kTree, true),
721      std::make_pair(ax::mojom::Role::kTreeGrid, true),
722      std::make_pair(ax::mojom::Role::kTabList, true),
723      std::make_pair(ax::mojom::Role::kTable, false),
724      std::make_pair(ax::mojom::Role::kBanner, false),
725  };
726
727  for (auto& test : tests) {
728    base::scoped_nsobject<BrowserAccessibilityCocoa> parent(
729        [ToBrowserAccessibilityCocoa(FindNode(test.first)) retain]);
730    BrowserAccessibilityCocoa* first_child = [parent children][0];
731    BrowserAccessibilityCocoa* second_child = [parent children][1];
732
733    EXPECT_NE(nullptr, parent.get());
734    EXPECT_EQ([second_child owner], [second_child actionTarget]);
735    EXPECT_EQ(test.second, [second_child owner] == [parent actionTarget]);
736
737    // aria-activedescendant should take priority of focus for determining
738    // if an object is the action target.
739    FocusAccessibilityElementAndWaitForFocusChange(first_child);
740    EXPECT_EQ(test.second, [second_child owner] == [parent actionTarget]);
741  }
742}
743
744}  // namespace content
745