1 // Copyright (c) 2012 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/strings/utf_string_conversions.h" 6 #include "content/common/frame_messages.h" 7 #include "content/common/view_message_enums.h" 8 #include "content/public/test/render_view_test.h" 9 #include "content/renderer/accessibility/renderer_accessibility_complete.h" 10 #include "content/renderer/render_view_impl.h" 11 #include "testing/gtest/include/gtest/gtest.h" 12 #include "third_party/WebKit/public/platform/WebSize.h" 13 #include "third_party/WebKit/public/web/WebAXObject.h" 14 #include "third_party/WebKit/public/web/WebDocument.h" 15 #include "third_party/WebKit/public/web/WebView.h" 16 #include "ui/accessibility/ax_node_data.h" 17 18 using blink::WebAXObject; 19 using blink::WebDocument; 20 21 namespace content { 22 23 class TestRendererAccessibilityComplete : public RendererAccessibilityComplete { 24 public: 25 explicit TestRendererAccessibilityComplete(RenderViewImpl* render_view) 26 : RendererAccessibilityComplete(render_view) { 27 } 28 29 void SendPendingAccessibilityEvents() { 30 RendererAccessibilityComplete::SendPendingAccessibilityEvents(); 31 } 32 }; 33 34 class RendererAccessibilityTest : public RenderViewTest { 35 public: 36 RendererAccessibilityTest() {} 37 38 RenderViewImpl* view() { 39 return static_cast<RenderViewImpl*>(view_); 40 } 41 42 RenderFrameImpl* frame() { 43 return static_cast<RenderFrameImpl*>(view()->GetMainRenderFrame()); 44 } 45 46 virtual void SetUp() { 47 RenderViewTest::SetUp(); 48 sink_ = &render_thread_->sink(); 49 } 50 51 void SetMode(AccessibilityMode mode) { 52 view()->OnSetAccessibilityMode(mode); 53 } 54 55 void GetLastAccEvent( 56 AccessibilityHostMsg_EventParams* params) { 57 const IPC::Message* message = 58 sink_->GetUniqueMessageMatching(AccessibilityHostMsg_Events::ID); 59 ASSERT_TRUE(message); 60 Tuple1<std::vector<AccessibilityHostMsg_EventParams> > param; 61 AccessibilityHostMsg_Events::Read(message, ¶m); 62 ASSERT_GE(param.a.size(), 1U); 63 *params = param.a[0]; 64 } 65 66 int CountAccessibilityNodesSentToBrowser() { 67 AccessibilityHostMsg_EventParams event; 68 GetLastAccEvent(&event); 69 return event.update.nodes.size(); 70 } 71 72 protected: 73 IPC::TestSink* sink_; 74 75 DISALLOW_COPY_AND_ASSIGN(RendererAccessibilityTest); 76 77 }; 78 79 TEST_F(RendererAccessibilityTest, EditableTextModeFocusEvents) { 80 // This is not a test of true web accessibility, it's a test of 81 // a mode used on Windows 8 in Metro mode where an extremely simplified 82 // accessibility tree containing only the current focused node is 83 // generated. 84 SetMode(AccessibilityModeEditableTextOnly); 85 86 // Set a minimum size and give focus so simulated events work. 87 view()->webwidget()->resize(blink::WebSize(500, 500)); 88 view()->webwidget()->setFocus(true); 89 90 std::string html = 91 "<body>" 92 " <input>" 93 " <textarea></textarea>" 94 " <p contentEditable>Editable</p>" 95 " <div tabindex=0 role=textbox>Textbox</div>" 96 " <button>Button</button>" 97 " <a href=#>Link</a>" 98 "</body>"; 99 100 // Load the test page. 101 LoadHTML(html.c_str()); 102 103 // We should have sent a message to the browser with the initial focus 104 // on the document. 105 { 106 SCOPED_TRACE("Initial focus on document"); 107 AccessibilityHostMsg_EventParams event; 108 GetLastAccEvent(&event); 109 EXPECT_EQ(event.event_type, 110 ui::AX_EVENT_LAYOUT_COMPLETE); 111 EXPECT_EQ(event.id, 1); 112 EXPECT_EQ(event.update.nodes.size(), 2U); 113 EXPECT_EQ(event.update.nodes[0].id, 1); 114 EXPECT_EQ(event.update.nodes[0].role, 115 ui::AX_ROLE_ROOT_WEB_AREA); 116 EXPECT_EQ(event.update.nodes[0].state, 117 (1U << ui::AX_STATE_READ_ONLY) | 118 (1U << ui::AX_STATE_FOCUSABLE) | 119 (1U << ui::AX_STATE_FOCUSED)); 120 EXPECT_EQ(event.update.nodes[0].child_ids.size(), 1U); 121 } 122 123 // Now focus the input element, and check everything again. 124 { 125 SCOPED_TRACE("input"); 126 sink_->ClearMessages(); 127 ExecuteJavaScript("document.querySelector('input').focus();"); 128 AccessibilityHostMsg_EventParams event; 129 GetLastAccEvent(&event); 130 EXPECT_EQ(event.event_type, 131 ui::AX_EVENT_FOCUS); 132 EXPECT_EQ(event.id, 3); 133 EXPECT_EQ(event.update.nodes[0].id, 1); 134 EXPECT_EQ(event.update.nodes[0].role, 135 ui::AX_ROLE_ROOT_WEB_AREA); 136 EXPECT_EQ(event.update.nodes[0].state, 137 (1U << ui::AX_STATE_READ_ONLY) | 138 (1U << ui::AX_STATE_FOCUSABLE)); 139 EXPECT_EQ(event.update.nodes[0].child_ids.size(), 1U); 140 EXPECT_EQ(event.update.nodes[1].id, 3); 141 EXPECT_EQ(event.update.nodes[1].role, 142 ui::AX_ROLE_GROUP); 143 EXPECT_EQ(event.update.nodes[1].state, 144 (1U << ui::AX_STATE_FOCUSABLE) | 145 (1U << ui::AX_STATE_FOCUSED)); 146 } 147 148 // Check other editable text nodes. 149 { 150 SCOPED_TRACE("textarea"); 151 sink_->ClearMessages(); 152 ExecuteJavaScript("document.querySelector('textarea').focus();"); 153 AccessibilityHostMsg_EventParams event; 154 GetLastAccEvent(&event); 155 EXPECT_EQ(event.id, 4); 156 EXPECT_EQ(event.update.nodes[1].state, 157 (1U << ui::AX_STATE_FOCUSABLE) | 158 (1U << ui::AX_STATE_FOCUSED)); 159 } 160 161 { 162 SCOPED_TRACE("contentEditable"); 163 sink_->ClearMessages(); 164 ExecuteJavaScript("document.querySelector('p').focus();"); 165 AccessibilityHostMsg_EventParams event; 166 GetLastAccEvent(&event); 167 EXPECT_EQ(event.id, 5); 168 EXPECT_EQ(event.update.nodes[1].state, 169 (1U << ui::AX_STATE_FOCUSABLE) | 170 (1U << ui::AX_STATE_FOCUSED)); 171 } 172 173 { 174 SCOPED_TRACE("role=textarea"); 175 sink_->ClearMessages(); 176 ExecuteJavaScript("document.querySelector('div').focus();"); 177 AccessibilityHostMsg_EventParams event; 178 GetLastAccEvent(&event); 179 EXPECT_EQ(event.id, 6); 180 EXPECT_EQ(event.update.nodes[1].state, 181 (1U << ui::AX_STATE_FOCUSABLE) | 182 (1U << ui::AX_STATE_FOCUSED)); 183 } 184 185 // Try focusing things that aren't editable text. 186 { 187 SCOPED_TRACE("button"); 188 sink_->ClearMessages(); 189 ExecuteJavaScript("document.querySelector('button').focus();"); 190 AccessibilityHostMsg_EventParams event; 191 GetLastAccEvent(&event); 192 EXPECT_EQ(event.id, 7); 193 EXPECT_EQ(event.update.nodes[1].state, 194 (1U << ui::AX_STATE_FOCUSABLE) | 195 (1U << ui::AX_STATE_FOCUSED) | 196 (1U << ui::AX_STATE_READ_ONLY)); 197 } 198 199 { 200 SCOPED_TRACE("link"); 201 sink_->ClearMessages(); 202 ExecuteJavaScript("document.querySelector('a').focus();"); 203 AccessibilityHostMsg_EventParams event; 204 GetLastAccEvent(&event); 205 EXPECT_EQ(event.id, 8); 206 EXPECT_EQ(event.update.nodes[1].state, 207 (1U << ui::AX_STATE_FOCUSABLE) | 208 (1U << ui::AX_STATE_FOCUSED) | 209 (1U << ui::AX_STATE_READ_ONLY)); 210 } 211 212 // Clear focus. 213 { 214 SCOPED_TRACE("Back to document."); 215 sink_->ClearMessages(); 216 ExecuteJavaScript("document.activeElement.blur()"); 217 AccessibilityHostMsg_EventParams event; 218 GetLastAccEvent(&event); 219 EXPECT_EQ(event.id, 1); 220 } 221 } 222 223 TEST_F(RendererAccessibilityTest, SendFullAccessibilityTreeOnReload) { 224 // The job of RendererAccessibilityComplete is to serialize the 225 // accessibility tree built by WebKit and send it to the browser. 226 // When the accessibility tree changes, it tries to send only 227 // the nodes that actually changed or were reparented. This test 228 // ensures that the messages sent are correct in cases when a page 229 // reloads, and that internal state is properly garbage-collected. 230 std::string html = 231 "<body>" 232 " <div role='group' id='A'>" 233 " <div role='group' id='A1'></div>" 234 " <div role='group' id='A2'></div>" 235 " </div>" 236 "</body>"; 237 LoadHTML(html.c_str()); 238 239 // Creating a RendererAccessibilityComplete should sent the tree 240 // to the browser. 241 scoped_ptr<TestRendererAccessibilityComplete> accessibility( 242 new TestRendererAccessibilityComplete(view())); 243 accessibility->SendPendingAccessibilityEvents(); 244 EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser()); 245 246 // If we post another event but the tree doesn't change, 247 // we should only send 1 node to the browser. 248 sink_->ClearMessages(); 249 WebDocument document = view()->GetWebView()->mainFrame()->document(); 250 WebAXObject root_obj = document.accessibilityObject(); 251 accessibility->HandleAXEvent( 252 root_obj, 253 ui::AX_EVENT_LAYOUT_COMPLETE); 254 accessibility->SendPendingAccessibilityEvents(); 255 EXPECT_EQ(1, CountAccessibilityNodesSentToBrowser()); 256 { 257 // Make sure it's the root object that was updated. 258 AccessibilityHostMsg_EventParams event; 259 GetLastAccEvent(&event); 260 EXPECT_EQ(root_obj.axID(), event.update.nodes[0].id); 261 } 262 263 // If we reload the page and send a event, we should send 264 // all 4 nodes to the browser. Also double-check that we didn't 265 // leak any of the old BrowserTreeNodes. 266 LoadHTML(html.c_str()); 267 document = view()->GetWebView()->mainFrame()->document(); 268 root_obj = document.accessibilityObject(); 269 sink_->ClearMessages(); 270 accessibility->HandleAXEvent( 271 root_obj, 272 ui::AX_EVENT_LAYOUT_COMPLETE); 273 accessibility->SendPendingAccessibilityEvents(); 274 EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser()); 275 276 // Even if the first event is sent on an element other than 277 // the root, the whole tree should be updated because we know 278 // the browser doesn't have the root element. 279 LoadHTML(html.c_str()); 280 document = view()->GetWebView()->mainFrame()->document(); 281 root_obj = document.accessibilityObject(); 282 sink_->ClearMessages(); 283 const WebAXObject& first_child = root_obj.childAt(0); 284 accessibility->HandleAXEvent( 285 first_child, 286 ui::AX_EVENT_LIVE_REGION_CHANGED); 287 accessibility->SendPendingAccessibilityEvents(); 288 EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser()); 289 } 290 291 // http://crbug.com/253537 292 #if defined(OS_ANDROID) 293 #define MAYBE_AccessibilityMessagesQueueWhileSwappedOut \ 294 DISABLED_AccessibilityMessagesQueueWhileSwappedOut 295 #else 296 #define MAYBE_AccessibilityMessagesQueueWhileSwappedOut \ 297 AccessibilityMessagesQueueWhileSwappedOut 298 #endif 299 300 TEST_F(RendererAccessibilityTest, 301 MAYBE_AccessibilityMessagesQueueWhileSwappedOut) { 302 std::string html = 303 "<body>" 304 " <p>Hello, world.</p>" 305 "</body>"; 306 LoadHTML(html.c_str()); 307 static const int kProxyRoutingId = 13; 308 309 // Creating a RendererAccessibilityComplete should send the tree 310 // to the browser. 311 scoped_ptr<TestRendererAccessibilityComplete> accessibility( 312 new TestRendererAccessibilityComplete(view())); 313 accessibility->SendPendingAccessibilityEvents(); 314 EXPECT_EQ(5, CountAccessibilityNodesSentToBrowser()); 315 316 // Post a "value changed" event, but then swap out 317 // before sending it. It shouldn't send the event while 318 // swapped out. 319 sink_->ClearMessages(); 320 WebDocument document = view()->GetWebView()->mainFrame()->document(); 321 WebAXObject root_obj = document.accessibilityObject(); 322 accessibility->HandleAXEvent( 323 root_obj, 324 ui::AX_EVENT_VALUE_CHANGED); 325 view()->main_render_frame()->OnSwapOut(kProxyRoutingId); 326 accessibility->SendPendingAccessibilityEvents(); 327 EXPECT_FALSE(sink_->GetUniqueMessageMatching( 328 AccessibilityHostMsg_Events::ID)); 329 330 // Navigate, so we're not swapped out anymore. Now we should 331 // send accessibility events again. Note that the 332 // message that was queued up before will be quickly discarded 333 // because the element it was referring to no longer exists, 334 // so the event here is from loading this new page. 335 FrameMsg_Navigate_Params nav_params; 336 nav_params.url = GURL("data:text/html,<p>Hello, again.</p>"); 337 nav_params.navigation_type = FrameMsg_Navigate_Type::NORMAL; 338 nav_params.transition = PAGE_TRANSITION_TYPED; 339 nav_params.current_history_list_length = 1; 340 nav_params.current_history_list_offset = 0; 341 nav_params.pending_history_list_offset = 1; 342 nav_params.page_id = -1; 343 frame()->OnNavigate(nav_params); 344 accessibility->SendPendingAccessibilityEvents(); 345 EXPECT_TRUE(sink_->GetUniqueMessageMatching( 346 AccessibilityHostMsg_Events::ID)); 347 } 348 349 TEST_F(RendererAccessibilityTest, HideAccessibilityObject) { 350 // Test RendererAccessibilityComplete and make sure it sends the 351 // proper event to the browser when an object in the tree 352 // is hidden, but its children are not. 353 std::string html = 354 "<body>" 355 " <div role='group' id='A'>" 356 " <div role='group' id='B'>" 357 " <div role='group' id='C' style='visibility:visible'>" 358 " </div>" 359 " </div>" 360 " </div>" 361 "</body>"; 362 LoadHTML(html.c_str()); 363 364 scoped_ptr<TestRendererAccessibilityComplete> accessibility( 365 new TestRendererAccessibilityComplete(view())); 366 accessibility->SendPendingAccessibilityEvents(); 367 EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser()); 368 369 WebDocument document = view()->GetWebView()->mainFrame()->document(); 370 WebAXObject root_obj = document.accessibilityObject(); 371 WebAXObject node_a = root_obj.childAt(0); 372 WebAXObject node_b = node_a.childAt(0); 373 WebAXObject node_c = node_b.childAt(0); 374 375 // Hide node 'B' ('C' stays visible). 376 ExecuteJavaScript( 377 "document.getElementById('B').style.visibility = 'hidden';"); 378 // Force layout now. 379 ExecuteJavaScript("document.getElementById('B').offsetLeft;"); 380 381 // Send a childrenChanged on 'A'. 382 sink_->ClearMessages(); 383 accessibility->HandleAXEvent( 384 node_a, 385 ui::AX_EVENT_CHILDREN_CHANGED); 386 387 accessibility->SendPendingAccessibilityEvents(); 388 AccessibilityHostMsg_EventParams event; 389 GetLastAccEvent(&event); 390 ASSERT_EQ(2U, event.update.nodes.size()); 391 392 // RendererAccessibilityComplete notices that 'C' is being reparented, 393 // so it clears the subtree rooted at 'A', then updates 'A' and then 'C'. 394 EXPECT_EQ(node_a.axID(), event.update.node_id_to_clear); 395 EXPECT_EQ(node_a.axID(), event.update.nodes[0].id); 396 EXPECT_EQ(node_c.axID(), event.update.nodes[1].id); 397 EXPECT_EQ(2, CountAccessibilityNodesSentToBrowser()); 398 } 399 400 TEST_F(RendererAccessibilityTest, ShowAccessibilityObject) { 401 // Test RendererAccessibilityComplete and make sure it sends the 402 // proper event to the browser when an object in the tree 403 // is shown, causing its own already-visible children to be 404 // reparented to it. 405 std::string html = 406 "<body>" 407 " <div role='group' id='A'>" 408 " <div role='group' id='B' style='visibility:hidden'>" 409 " <div role='group' id='C' style='visibility:visible'>" 410 " </div>" 411 " </div>" 412 " </div>" 413 "</body>"; 414 LoadHTML(html.c_str()); 415 416 scoped_ptr<TestRendererAccessibilityComplete> accessibility( 417 new TestRendererAccessibilityComplete(view())); 418 accessibility->SendPendingAccessibilityEvents(); 419 EXPECT_EQ(3, CountAccessibilityNodesSentToBrowser()); 420 421 // Show node 'B', then send a childrenChanged on 'A'. 422 ExecuteJavaScript( 423 "document.getElementById('B').style.visibility = 'visible';"); 424 ExecuteJavaScript("document.getElementById('B').offsetLeft;"); 425 426 sink_->ClearMessages(); 427 WebDocument document = view()->GetWebView()->mainFrame()->document(); 428 WebAXObject root_obj = document.accessibilityObject(); 429 WebAXObject node_a = root_obj.childAt(0); 430 WebAXObject node_b = node_a.childAt(0); 431 WebAXObject node_c = node_b.childAt(0); 432 433 accessibility->HandleAXEvent( 434 node_a, 435 ui::AX_EVENT_CHILDREN_CHANGED); 436 437 accessibility->SendPendingAccessibilityEvents(); 438 AccessibilityHostMsg_EventParams event; 439 GetLastAccEvent(&event); 440 441 ASSERT_EQ(3U, event.update.nodes.size()); 442 EXPECT_EQ(node_a.axID(), event.update.node_id_to_clear); 443 EXPECT_EQ(node_a.axID(), event.update.nodes[0].id); 444 EXPECT_EQ(node_b.axID(), event.update.nodes[1].id); 445 EXPECT_EQ(node_c.axID(), event.update.nodes[2].id); 446 EXPECT_EQ(3, CountAccessibilityNodesSentToBrowser()); 447 } 448 449 TEST_F(RendererAccessibilityTest, DetachAccessibilityObject) { 450 // Test RendererAccessibilityComplete and make sure it sends the 451 // proper event to the browser when an object in the tree 452 // is detached, but its children are not. This can happen when 453 // a layout occurs and an anonymous render block is no longer needed. 454 std::string html = 455 "<body aria-label='Body'>" 456 "<span>1</span><span style='display:block'>2</span>" 457 "</body>"; 458 LoadHTML(html.c_str()); 459 460 scoped_ptr<TestRendererAccessibilityComplete> accessibility( 461 new TestRendererAccessibilityComplete(view())); 462 accessibility->SendPendingAccessibilityEvents(); 463 EXPECT_EQ(7, CountAccessibilityNodesSentToBrowser()); 464 465 // Initially, the accessibility tree looks like this: 466 // 467 // Document 468 // +--Body 469 // +--Anonymous Block 470 // +--Static Text "1" 471 // +--Inline Text Box "1" 472 // +--Static Text "2" 473 // +--Inline Text Box "2" 474 WebDocument document = view()->GetWebView()->mainFrame()->document(); 475 WebAXObject root_obj = document.accessibilityObject(); 476 WebAXObject body = root_obj.childAt(0); 477 WebAXObject anonymous_block = body.childAt(0); 478 WebAXObject text_1 = anonymous_block.childAt(0); 479 WebAXObject text_2 = body.childAt(1); 480 481 // Change the display of the second 'span' back to inline, which causes the 482 // anonymous block to be destroyed. 483 ExecuteJavaScript( 484 "document.querySelectorAll('span')[1].style.display = 'inline';"); 485 // Force layout now. 486 ExecuteJavaScript("document.body.offsetLeft;"); 487 488 // Send a childrenChanged on the body. 489 sink_->ClearMessages(); 490 accessibility->HandleAXEvent( 491 body, 492 ui::AX_EVENT_CHILDREN_CHANGED); 493 494 accessibility->SendPendingAccessibilityEvents(); 495 496 // Afterwards, the accessibility tree looks like this: 497 // 498 // Document 499 // +--Body 500 // +--Static Text "1" 501 // +--Inline Text Box "1" 502 // +--Static Text "2" 503 // +--Inline Text Box "2" 504 // 505 // We just assert that there are now four nodes in the 506 // accessibility tree and that only three nodes needed 507 // to be updated (the body, the static text 1, and 508 // the static text 2). 509 510 AccessibilityHostMsg_EventParams event; 511 GetLastAccEvent(&event); 512 ASSERT_EQ(5U, event.update.nodes.size()); 513 514 EXPECT_EQ(body.axID(), event.update.nodes[0].id); 515 EXPECT_EQ(text_1.axID(), event.update.nodes[1].id); 516 // The third event is to update text_2, but its id changes 517 // so we don't have a test expectation for it. 518 } 519 520 TEST_F(RendererAccessibilityTest, EventOnObjectNotInTree) { 521 // Test RendererAccessibilityComplete and make sure it doesn't send anything 522 // if we get a notification from Blink for an object that isn't in the 523 // tree, like the scroll area that's the parent of the main document, 524 // which we don't expose. 525 std::string html = "<body><input></body>"; 526 LoadHTML(html.c_str()); 527 528 scoped_ptr<TestRendererAccessibilityComplete> accessibility( 529 new TestRendererAccessibilityComplete(view())); 530 accessibility->SendPendingAccessibilityEvents(); 531 EXPECT_EQ(3, CountAccessibilityNodesSentToBrowser()); 532 533 WebDocument document = view()->GetWebView()->mainFrame()->document(); 534 WebAXObject root_obj = document.accessibilityObject(); 535 WebAXObject scroll_area = root_obj.parentObject(); 536 EXPECT_EQ(blink::WebAXRoleScrollArea, scroll_area.role()); 537 538 // Try to fire a message on the scroll area, and assert that we just 539 // ignore it. 540 sink_->ClearMessages(); 541 accessibility->HandleAXEvent(scroll_area, 542 ui::AX_EVENT_VALUE_CHANGED); 543 544 accessibility->SendPendingAccessibilityEvents(); 545 546 const IPC::Message* message = 547 sink_->GetUniqueMessageMatching(AccessibilityHostMsg_Events::ID); 548 ASSERT_TRUE(message); 549 Tuple1<std::vector<AccessibilityHostMsg_EventParams> > param; 550 AccessibilityHostMsg_Events::Read(message, ¶m); 551 ASSERT_EQ(0U, param.a.size()); 552 } 553 554 } // namespace content 555