1 // Copyright 2015 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/bind.h"
6 #include "base/callback.h"
7 #include "base/macros.h"
8 #include "content/browser/web_contents/web_contents_impl.h"
9 #include "content/public/test/browser_test.h"
10 #include "content/public/test/content_browser_test.h"
11 #include "content/public/test/content_browser_test_utils.h"
12 #include "content/public/test/test_utils.h"
13 #include "content/shell/browser/shell.h"
14 #include "content/test/content_browser_test_utils_internal.h"
15 #include "net/dns/mock_host_resolver.h"
16 #include "net/test/embedded_test_server/embedded_test_server.h"
17 #include "testing/gtest/include/gtest/gtest.h"
18 #include "ui/accessibility/ax_node.h"
19 #include "ui/accessibility/ax_tree.h"
20 
21 namespace content {
22 
23 namespace {
24 
25 class AXTreeSnapshotWaiter {
26  public:
AXTreeSnapshotWaiter()27   AXTreeSnapshotWaiter() : loop_runner_(new MessageLoopRunner()) {}
28 
Wait()29   void Wait() { loop_runner_->Run(); }
30 
snapshot() const31   const ui::AXTreeUpdate& snapshot() const { return snapshot_; }
32 
ReceiveSnapshot(const ui::AXTreeUpdate & snapshot)33   void ReceiveSnapshot(const ui::AXTreeUpdate& snapshot) {
34     snapshot_ = snapshot;
35     loop_runner_->Quit();
36   }
37 
38  private:
39   ui::AXTreeUpdate snapshot_;
40   scoped_refptr<MessageLoopRunner> loop_runner_;
41 
42   DISALLOW_COPY_AND_ASSIGN(AXTreeSnapshotWaiter);
43 };
44 
DumpRolesAndNamesAsText(const ui::AXNode * node,int indent,std::string * dst)45 void DumpRolesAndNamesAsText(const ui::AXNode* node,
46                              int indent,
47                              std::string* dst) {
48   for (int i = 0; i < indent; i++)
49     *dst += "  ";
50   *dst += ui::ToString(node->data().role);
51   if (node->data().HasStringAttribute(ax::mojom::StringAttribute::kName))
52     *dst += " '" +
53             node->data().GetStringAttribute(ax::mojom::StringAttribute::kName) +
54             "'";
55   *dst += "\n";
56   for (size_t i = 0; i < node->GetUnignoredChildCount(); ++i)
57     DumpRolesAndNamesAsText(node->GetUnignoredChildAtIndex(i), indent + 1, dst);
58 }
59 
60 }  // namespace
61 
62 class SnapshotAXTreeBrowserTest : public ContentBrowserTest {
63  public:
SnapshotAXTreeBrowserTest()64   SnapshotAXTreeBrowserTest() {}
~SnapshotAXTreeBrowserTest()65   ~SnapshotAXTreeBrowserTest() override {}
66 };
67 
IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest,SnapshotAccessibilityTreeFromWebContents)68 IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest,
69                        SnapshotAccessibilityTreeFromWebContents) {
70   GURL url("data:text/html,<button>Click</button>");
71   EXPECT_TRUE(NavigateToURL(shell(), url));
72 
73   WebContentsImpl* web_contents =
74       static_cast<WebContentsImpl*>(shell()->web_contents());
75 
76   AXTreeSnapshotWaiter waiter;
77   web_contents->RequestAXTreeSnapshot(
78       base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot,
79                      base::Unretained(&waiter)),
80       ui::kAXModeComplete);
81   waiter.Wait();
82 
83   // Dump the whole tree if one of the assertions below fails
84   // to aid in debugging why it failed.
85   SCOPED_TRACE(waiter.snapshot().ToString());
86 
87   ui::AXTree tree(waiter.snapshot());
88   ui::AXNode* root = tree.root();
89   ASSERT_NE(nullptr, root);
90   ASSERT_EQ(ax::mojom::Role::kRootWebArea, root->data().role);
91   ui::AXNode* group = root->GetUnignoredChildAtIndex(0);
92   ASSERT_EQ(ax::mojom::Role::kGenericContainer, group->data().role);
93   ui::AXNode* button = group->GetUnignoredChildAtIndex(0);
94   ASSERT_EQ(ax::mojom::Role::kButton, button->data().role);
95 }
96 
IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest,SnapshotAccessibilityTreeFromMultipleFrames)97 IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest,
98                        SnapshotAccessibilityTreeFromMultipleFrames) {
99   ASSERT_TRUE(embedded_test_server()->Start());
100 
101   EXPECT_TRUE(NavigateToURL(
102       shell(),
103       embedded_test_server()->GetURL("/accessibility/snapshot/outer.html")));
104 
105   WebContentsImpl* web_contents =
106       static_cast<WebContentsImpl*>(shell()->web_contents());
107   FrameTreeNode* root_frame = web_contents->GetFrameTree()->root();
108 
109   EXPECT_TRUE(NavigateToURLFromRenderer(root_frame->child_at(0),
110                                         GURL("data:text/plain,Alpha")));
111   EXPECT_TRUE(NavigateToURLFromRenderer(
112       root_frame->child_at(1),
113       embedded_test_server()->GetURL("/accessibility/snapshot/inner.html")));
114 
115   AXTreeSnapshotWaiter waiter;
116   web_contents->RequestAXTreeSnapshot(
117       base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot,
118                      base::Unretained(&waiter)),
119       ui::kAXModeComplete);
120   waiter.Wait();
121 
122   // Dump the whole tree if one of the assertions below fails
123   // to aid in debugging why it failed.
124   SCOPED_TRACE(waiter.snapshot().ToString());
125 
126   ui::AXTree tree(waiter.snapshot());
127   ui::AXNode* root = tree.root();
128   std::string dump;
129   DumpRolesAndNamesAsText(root, 0, &dump);
130   EXPECT_EQ(
131       "rootWebArea\n"
132       "  genericContainer\n"
133       "    button 'Before'\n"
134       "      staticText 'Before'\n"
135       "    iframe\n"
136       "      rootWebArea\n"
137       "        pre\n"
138       "          staticText 'Alpha'\n"
139       "    button 'Middle'\n"
140       "      staticText 'Middle'\n"
141       "    iframe\n"
142       "      rootWebArea\n"
143       "        genericContainer\n"
144       "          button 'Inside Before'\n"
145       "            staticText 'Inside Before'\n"
146       "          iframe\n"
147       "            rootWebArea\n"
148       "          button 'Inside After'\n"
149       "            staticText 'Inside After'\n"
150       "    button 'After'\n"
151       "      staticText 'After'\n",
152       dump);
153 }
154 
155 // This tests to make sure that RequestAXTreeSnapshot() correctly traverses
156 // inner contents, as used in features like <webview>.
IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest,SnapshotAccessibilityTreeWithInnerContents)157 IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest,
158                        SnapshotAccessibilityTreeWithInnerContents) {
159   ASSERT_TRUE(embedded_test_server()->Start());
160 
161   EXPECT_TRUE(NavigateToURL(
162       shell(),
163       embedded_test_server()->GetURL("/accessibility/snapshot/outer.html")));
164 
165   WebContentsImpl* web_contents =
166       static_cast<WebContentsImpl*>(shell()->web_contents());
167   FrameTreeNode* root_frame = web_contents->GetFrameTree()->root();
168 
169   EXPECT_TRUE(NavigateToURLFromRenderer(root_frame->child_at(0),
170                                         GURL("data:text/plain,Alpha")));
171 
172   WebContentsImpl* inner_contents =
173       static_cast<WebContentsImpl*>(CreateAndAttachInnerContents(
174           root_frame->child_at(1)->current_frame_host()));
175   EXPECT_TRUE(NavigateToURLFromRenderer(
176       inner_contents->GetFrameTree()->root(),
177       embedded_test_server()->GetURL("/accessibility/snapshot/inner.html")));
178 
179   AXTreeSnapshotWaiter waiter;
180   web_contents->RequestAXTreeSnapshot(
181       base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot,
182                      base::Unretained(&waiter)),
183       ui::kAXModeComplete);
184   waiter.Wait();
185 
186   // Dump the whole tree if one of the assertions below fails
187   // to aid in debugging why it failed.
188   SCOPED_TRACE(waiter.snapshot().ToString());
189 
190   ui::AXTree tree(waiter.snapshot());
191   ui::AXNode* root = tree.root();
192   std::string dump;
193   DumpRolesAndNamesAsText(root, 0, &dump);
194   EXPECT_EQ(
195       "rootWebArea\n"
196       "  genericContainer\n"
197       "    button 'Before'\n"
198       "      staticText 'Before'\n"
199       "    iframe\n"
200       "      rootWebArea\n"
201       "        pre\n"
202       "          staticText 'Alpha'\n"
203       "    button 'Middle'\n"
204       "      staticText 'Middle'\n"
205       "    iframe\n"
206       "      rootWebArea\n"
207       "        genericContainer\n"
208       "          button 'Inside Before'\n"
209       "            staticText 'Inside Before'\n"
210       "          iframe\n"
211       "            rootWebArea\n"
212       "          button 'Inside After'\n"
213       "            staticText 'Inside After'\n"
214       "    button 'After'\n"
215       "      staticText 'After'\n",
216       dump);
217 }
218 
219 // This tests to make sure that snapshotting with different modes gives
220 // different results. This is not intended to ensure that specific modes give
221 // specific attributes, but merely to ensure that the mode parameter makes a
222 // difference.
IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest,SnapshotAccessibilityTreeModes)223 IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest,
224                        SnapshotAccessibilityTreeModes) {
225   GURL url("data:text/html,<button>Click</button>");
226   EXPECT_TRUE(NavigateToURL(shell(), url));
227 
228   WebContentsImpl* web_contents =
229       static_cast<WebContentsImpl*>(shell()->web_contents());
230 
231   AXTreeSnapshotWaiter waiter_complete;
232   web_contents->RequestAXTreeSnapshot(
233       base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot,
234                      base::Unretained(&waiter_complete)),
235       ui::kAXModeComplete);
236   waiter_complete.Wait();
237   const std::vector<ui::AXNodeData>& complete_nodes =
238       waiter_complete.snapshot().nodes;
239 
240   // Dump the whole tree if one of the assertions below fails
241   // to aid in debugging why it failed.
242   SCOPED_TRACE(waiter_complete.snapshot().ToString());
243 
244   AXTreeSnapshotWaiter waiter_contents;
245   web_contents->RequestAXTreeSnapshot(
246       base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot,
247                      base::Unretained(&waiter_contents)),
248       ui::AXMode::kWebContents);
249   waiter_contents.Wait();
250   const std::vector<ui::AXNodeData>& contents_nodes =
251       waiter_contents.snapshot().nodes;
252 
253   // Dump the whole tree if one of the assertions below fails
254   // to aid in debugging why it failed.
255   SCOPED_TRACE(waiter_contents.snapshot().ToString());
256 
257   // The two snapshot passes walked the tree in the same order, so comparing
258   // element to element is possible by walking the snapshots in parallel.
259 
260   auto total_attribute_count = [](const ui::AXNodeData& node_data) {
261     return node_data.string_attributes.size() +
262            node_data.int_attributes.size() + node_data.float_attributes.size() +
263            node_data.bool_attributes.size() +
264            node_data.intlist_attributes.size() +
265            node_data.stringlist_attributes.size() +
266            node_data.html_attributes.size();
267   };
268 
269   ASSERT_EQ(complete_nodes.size(), contents_nodes.size());
270   for (size_t i = 0; i < complete_nodes.size(); ++i)
271     EXPECT_LT(total_attribute_count(contents_nodes[i]),
272               total_attribute_count(complete_nodes[i]));
273 }
274 
IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest,SnapshotPDFMode)275 IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest, SnapshotPDFMode) {
276   // The "PDF" accessibility mode is used when getting a snapshot of the
277   // accessibility tree in order to export a tagged PDF. Ensure that
278   // we're serializing the right set of attributes needed for a PDF and
279   // also ensure that we're *not* wasting time serializing attributes
280   // that are not needed for PDF export.
281   GURL url(R"HTML(data:text/html,<body>
282                   <img src="" alt="Unicorns">
283                   <ul>
284                     <li aria-posinset="5">
285                       <span style="color: red;">Red text</span>
286                   </ul>
287                   <table role="table">
288                     <tr>
289                       <td colspan="2">
290                     </tr>
291                     <tr>
292                       <td>1</td><td>2</td>
293                     </tr>
294                   </table>
295                   </body>)HTML");
296   EXPECT_TRUE(NavigateToURL(shell(), url));
297 
298   auto* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents());
299   AXTreeSnapshotWaiter waiter;
300   web_contents->RequestAXTreeSnapshot(
301       base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot,
302                      base::Unretained(&waiter)),
303       ui::AXMode::kPDF);
304   waiter.Wait();
305 
306   // Dump the whole tree if one of the assertions below fails
307   // to aid in debugging why it failed.
308   SCOPED_TRACE(waiter.snapshot().ToString());
309 
310   // Scan all of the nodes and make some general assertions.
311   int dom_node_id_count = 0;
312   for (const ui::AXNodeData& node_data : waiter.snapshot().nodes) {
313     // Every node should have a valid role, state, and ID.
314     EXPECT_NE(ax::mojom::Role::kUnknown, node_data.role);
315     EXPECT_NE(0, node_data.id);
316 
317     if (node_data.GetIntAttribute(ax::mojom::IntAttribute::kDOMNodeId) != 0)
318       dom_node_id_count++;
319 
320     // We don't need bounding boxes to make a tagged PDF. Ensure those are
321     // uninitialized.
322     EXPECT_TRUE(node_data.relative_bounds.bounds.IsEmpty());
323 
324     // We shouldn't get any inline text box nodes. They aren't needed to
325     // make a tagged PDF and they make up a large fraction of nodes in the
326     // tree when present.
327     EXPECT_NE(ax::mojom::Role::kInlineTextBox, node_data.role);
328 
329     // We shouldn't have any style information like color in the tree.
330     EXPECT_FALSE(node_data.HasIntAttribute(ax::mojom::IntAttribute::kColor));
331   }
332 
333   // Many nodes should have a DOM node id. That's not normally included
334   // in the accessibility tree but it's needed for associating nodes with
335   // rendered text in the PDF file.
336   EXPECT_GT(dom_node_id_count, 5);
337 
338   // Build an AXTree from the snapshot and make some specific assertions.
339   ui::AXTree tree(waiter.snapshot());
340   ui::AXNode* root = tree.root();
341   ASSERT_TRUE(root);
342   ASSERT_EQ(ax::mojom::Role::kRootWebArea, root->data().role);
343 
344   // Img alt text should be present.
345   ui::AXNode* image = root->GetUnignoredChildAtIndex(0);
346   ASSERT_TRUE(image);
347   ASSERT_EQ(ax::mojom::Role::kImage, image->data().role);
348   ASSERT_EQ("Unicorns", image->data().GetStringAttribute(
349                             ax::mojom::StringAttribute::kName));
350 
351   // List attributes like posinset should be present.
352   ui::AXNode* ul = root->GetUnignoredChildAtIndex(1);
353   ASSERT_TRUE(ul);
354   ASSERT_EQ(ax::mojom::Role::kList, ul->data().role);
355   ui::AXNode* li = ul->GetUnignoredChildAtIndex(0);
356   ASSERT_TRUE(li);
357   ASSERT_EQ(ax::mojom::Role::kListItem, li->data().role);
358   EXPECT_EQ(5, *li->GetPosInSet());
359 
360   // Table attributes like colspan should be present.
361   ui::AXNode* table = root->GetUnignoredChildAtIndex(2);
362   ASSERT_TRUE(table);
363   ASSERT_EQ(ax::mojom::Role::kTable, table->data().role);
364   ui::AXNode* tr = table->GetUnignoredChildAtIndex(0);
365   ASSERT_TRUE(tr);
366   ASSERT_EQ(ax::mojom::Role::kRow, tr->data().role);
367   ui::AXNode* td = tr->GetUnignoredChildAtIndex(0);
368   ASSERT_TRUE(td);
369   ASSERT_EQ(ax::mojom::Role::kCell, td->data().role);
370   EXPECT_EQ(2, *td->GetTableCellColSpan());
371 }
372 
373 }  // namespace content
374