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