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 "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"
19
20using blink::WebAXObject;
21using blink::WebDocument;
22
23namespace content {
24
25class TestRendererAccessibilityComplete : public RendererAccessibilityComplete {
26 public:
27  explicit TestRendererAccessibilityComplete(RenderFrameImpl* render_frame)
28    : RendererAccessibilityComplete(render_frame) {
29  }
30
31  void SendPendingAccessibilityEvents() {
32    RendererAccessibilityComplete::SendPendingAccessibilityEvents();
33  }
34};
35
36class RendererAccessibilityTest : public RenderViewTest {
37 public:
38  RendererAccessibilityTest() {}
39
40  RenderViewImpl* view() {
41    return static_cast<RenderViewImpl*>(view_);
42  }
43
44  RenderFrameImpl* frame() {
45    return static_cast<RenderFrameImpl*>(view()->GetMainRenderFrame());
46  }
47
48  virtual void SetUp() {
49    RenderViewTest::SetUp();
50    sink_ = &render_thread_->sink();
51  }
52
53  void SetMode(AccessibilityMode mode) {
54    frame()->OnSetAccessibilityMode(mode);
55  }
56
57  void GetLastAccEvent(
58      AccessibilityHostMsg_EventParams* params) {
59    const IPC::Message* message =
60        sink_->GetUniqueMessageMatching(AccessibilityHostMsg_Events::ID);
61    ASSERT_TRUE(message);
62    Tuple2<std::vector<AccessibilityHostMsg_EventParams>, int> param;
63    AccessibilityHostMsg_Events::Read(message, &param);
64    ASSERT_GE(param.a.size(), 1U);
65    *params = param.a[0];
66  }
67
68  int CountAccessibilityNodesSentToBrowser() {
69    AccessibilityHostMsg_EventParams event;
70    GetLastAccEvent(&event);
71    return event.update.nodes.size();
72  }
73
74 protected:
75  IPC::TestSink* sink_;
76
77  DISALLOW_COPY_AND_ASSIGN(RendererAccessibilityTest);
78
79};
80
81TEST_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
85  // generated.
86  SetMode(AccessibilityModeEditableTextOnly);
87
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);
91
92  std::string html =
93      "<body>"
94      "  <input>"
95      "  <textarea></textarea>"
96      "  <p contentEditable>Editable</p>"
97      "  <div tabindex=0 role=textbox>Textbox</div>"
98      "  <button>Button</button>"
99      "  <a href=#>Link</a>"
100      "</body>";
101
102  // Load the test page.
103  LoadHTML(html.c_str());
104
105  // We should have sent a message to the browser with the initial focus
106  // on the document.
107  {
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);
123  }
124
125  // Now focus the input element, and check everything again.
126  {
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,
133              ui::AX_EVENT_FOCUS);
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,
144              ui::AX_ROLE_GROUP);
145    EXPECT_EQ(event.update.nodes[1].state,
146              (1U << ui::AX_STATE_FOCUSABLE) |
147              (1U << ui::AX_STATE_FOCUSED));
148  }
149
150  // Check other editable text nodes.
151  {
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));
161  }
162
163  {
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));
173  }
174
175  {
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));
185  }
186
187  // Try focusing things that aren't editable text.
188  {
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));
199  }
200
201  {
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));
212  }
213
214  // Clear focus.
215  {
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);
222  }
223}
224
225TEST_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.
232  std::string html =
233      "<body>"
234      "  <div role='group' id='A'>"
235      "    <div role='group' id='A1'></div>"
236      "    <div role='group' id='A2'></div>"
237      "  </div>"
238      "</body>";
239  LoadHTML(html.c_str());
240
241  // Creating a RendererAccessibilityComplete should sent the tree
242  // to the browser.
243  scoped_ptr<TestRendererAccessibilityComplete> accessibility(
244      new TestRendererAccessibilityComplete(frame()));
245  accessibility->SendPendingAccessibilityEvents();
246  EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser());
247
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(
254      root_obj,
255      ui::AX_EVENT_LAYOUT_COMPLETE);
256  accessibility->SendPendingAccessibilityEvents();
257  EXPECT_EQ(1, CountAccessibilityNodesSentToBrowser());
258  {
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);
263  }
264
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(
273      root_obj,
274      ui::AX_EVENT_LAYOUT_COMPLETE);
275  accessibility->SendPendingAccessibilityEvents();
276  EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser());
277
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(
287      first_child,
288      ui::AX_EVENT_LIVE_REGION_CHANGED);
289  accessibility->SendPendingAccessibilityEvents();
290  EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser());
291}
292
293// http://crbug.com/253537
294#if defined(OS_ANDROID)
295#define MAYBE_AccessibilityMessagesQueueWhileSwappedOut \
296        DISABLED_AccessibilityMessagesQueueWhileSwappedOut
297#else
298#define MAYBE_AccessibilityMessagesQueueWhileSwappedOut \
299        AccessibilityMessagesQueueWhileSwappedOut
300#endif
301
302TEST_F(RendererAccessibilityTest,
303       MAYBE_AccessibilityMessagesQueueWhileSwappedOut) {
304  std::string html =
305      "<body>"
306      "  <p>Hello, world.</p>"
307      "</body>";
308  LoadHTML(html.c_str());
309  static const int kProxyRoutingId = 13;
310
311  // Creating a RendererAccessibilityComplete should send the tree
312  // to the browser.
313  scoped_ptr<TestRendererAccessibilityComplete> accessibility(
314      new TestRendererAccessibilityComplete(frame()));
315  accessibility->SendPendingAccessibilityEvents();
316  EXPECT_EQ(5, CountAccessibilityNodesSentToBrowser());
317
318  // Post a "value changed" event, but then swap out
319  // before sending it. It shouldn't send the event while
320  // swapped out.
321  sink_->ClearMessages();
322  WebDocument document = view()->GetWebView()->mainFrame()->document();
323  WebAXObject root_obj = document.accessibilityObject();
324  accessibility->HandleAXEvent(
325      root_obj,
326      ui::AX_EVENT_VALUE_CHANGED);
327  view()->GetMainRenderFrame()->OnSwapOut(kProxyRoutingId);
328  accessibility->SendPendingAccessibilityEvents();
329  EXPECT_FALSE(sink_->GetUniqueMessageMatching(
330      AccessibilityHostMsg_Events::ID));
331
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 = ui::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));
350}
351
352TEST_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.
356  std::string html =
357      "<body>"
358      "  <div role='group' id='A'>"
359      "    <div role='group' id='B'>"
360      "      <div role='group' id='C' style='visibility:visible'>"
361      "      </div>"
362      "    </div>"
363      "  </div>"
364      "</body>";
365  LoadHTML(html.c_str());
366
367  scoped_ptr<TestRendererAccessibilityComplete> accessibility(
368      new TestRendererAccessibilityComplete(frame()));
369  accessibility->SendPendingAccessibilityEvents();
370  EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser());
371
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);
377
378  // Hide node 'B' ('C' stays visible).
379  ExecuteJavaScript(
380      "document.getElementById('B').style.visibility = 'hidden';");
381  // Force layout now.
382  ExecuteJavaScript("document.getElementById('B').offsetLeft;");
383
384  // Send a childrenChanged on 'A'.
385  sink_->ClearMessages();
386  accessibility->HandleAXEvent(
387      node_a,
388      ui::AX_EVENT_CHILDREN_CHANGED);
389
390  accessibility->SendPendingAccessibilityEvents();
391  AccessibilityHostMsg_EventParams event;
392  GetLastAccEvent(&event);
393  ASSERT_EQ(2U, event.update.nodes.size());
394
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());
401}
402
403TEST_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
407  // reparented to it.
408  std::string html =
409      "<body>"
410      "  <div role='group' id='A'>"
411      "    <div role='group' id='B' style='visibility:hidden'>"
412      "      <div role='group' id='C' style='visibility:visible'>"
413      "      </div>"
414      "    </div>"
415      "  </div>"
416      "</body>";
417  LoadHTML(html.c_str());
418
419  scoped_ptr<TestRendererAccessibilityComplete> accessibility(
420      new TestRendererAccessibilityComplete(frame()));
421  accessibility->SendPendingAccessibilityEvents();
422  EXPECT_EQ(3, CountAccessibilityNodesSentToBrowser());
423
424  // Show node 'B', then send a childrenChanged on 'A'.
425  ExecuteJavaScript(
426      "document.getElementById('B').style.visibility = 'visible';");
427  ExecuteJavaScript("document.getElementById('B').offsetLeft;");
428
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);
435
436  accessibility->HandleAXEvent(
437      node_a,
438      ui::AX_EVENT_CHILDREN_CHANGED);
439
440  accessibility->SendPendingAccessibilityEvents();
441  AccessibilityHostMsg_EventParams event;
442  GetLastAccEvent(&event);
443
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());
450}
451
452TEST_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.
457  std::string html =
458      "<body aria-label='Body'>"
459      "<span>1</span><span style='display:block'>2</span>"
460      "</body>";
461  LoadHTML(html.c_str());
462
463  scoped_ptr<TestRendererAccessibilityComplete> accessibility(
464      new TestRendererAccessibilityComplete(frame()));
465  accessibility->SendPendingAccessibilityEvents();
466  EXPECT_EQ(7, CountAccessibilityNodesSentToBrowser());
467
468  // Initially, the accessibility tree looks like this:
469  //
470  //   Document
471  //   +--Body
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);
483
484  // Change the display of the second 'span' back to inline, which causes the
485  // anonymous block to be destroyed.
486  ExecuteJavaScript(
487      "document.querySelectorAll('span')[1].style.display = 'inline';");
488  // Force layout now.
489  ExecuteJavaScript("document.body.offsetLeft;");
490
491  // Send a childrenChanged on the body.
492  sink_->ClearMessages();
493  accessibility->HandleAXEvent(
494      body,
495      ui::AX_EVENT_CHILDREN_CHANGED);
496
497  accessibility->SendPendingAccessibilityEvents();
498
499  // Afterwards, the accessibility tree looks like this:
500  //
501  //   Document
502  //   +--Body
503  //      +--Static Text "1"
504  //         +--Inline Text Box "1"
505  //      +--Static Text "2"
506  //         +--Inline Text Box "2"
507  //
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).
512
513  AccessibilityHostMsg_EventParams event;
514  GetLastAccEvent(&event);
515  ASSERT_EQ(5U, event.update.nodes.size());
516
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.
521}
522
523TEST_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());
530
531  scoped_ptr<TestRendererAccessibilityComplete> accessibility(
532      new TestRendererAccessibilityComplete(frame()));
533  accessibility->SendPendingAccessibilityEvents();
534  EXPECT_EQ(3, CountAccessibilityNodesSentToBrowser());
535
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());
540
541  // Try to fire a message on the scroll area, and assert that we just
542  // ignore it.
543  sink_->ClearMessages();
544  accessibility->HandleAXEvent(scroll_area,
545                               ui::AX_EVENT_VALUE_CHANGED);
546
547  accessibility->SendPendingAccessibilityEvents();
548
549  const IPC::Message* message =
550      sink_->GetUniqueMessageMatching(AccessibilityHostMsg_Events::ID);
551  ASSERT_TRUE(message);
552  Tuple2<std::vector<AccessibilityHostMsg_EventParams>, int> param;
553  AccessibilityHostMsg_Events::Read(message, &param);
554  ASSERT_EQ(0U, param.a.size());
555}
556
557}  // namespace content
558