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