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