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