| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "build/build_config.h" |
| #include "content/browser/fenced_frame/fenced_frame.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/test/content_browser_test_utils_internal.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "testing/gmock/include/gmock/gmock-matchers.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/accessibility/accessibility_features.h" |
| #include "ui/accessibility/ax_node.h" |
| #include "ui/accessibility/ax_tree.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| class AXTreeSnapshotWaiter { |
| public: |
| AXTreeSnapshotWaiter() : loop_runner_(new MessageLoopRunner()) {} |
| |
| AXTreeSnapshotWaiter(const AXTreeSnapshotWaiter&) = delete; |
| AXTreeSnapshotWaiter& operator=(const AXTreeSnapshotWaiter&) = delete; |
| |
| void Wait() { loop_runner_->Run(); } |
| |
| const ui::AXTreeUpdate& snapshot() const { return snapshot_; } |
| |
| void ReceiveSnapshot(const ui::AXTreeUpdate& snapshot) { |
| snapshot_ = snapshot; |
| loop_runner_->Quit(); |
| } |
| |
| private: |
| ui::AXTreeUpdate snapshot_; |
| scoped_refptr<MessageLoopRunner> loop_runner_; |
| }; |
| |
| void DumpRolesAndNamesAsText(const ui::AXNode* node, |
| int indent, |
| std::string* dst) { |
| for (int i = 0; i < indent; i++) |
| *dst += " "; |
| *dst += ui::ToString(node->GetRole()); |
| if (node->HasStringAttribute(ax::mojom::StringAttribute::kName)) |
| *dst += " '" + node->GetStringAttribute(ax::mojom::StringAttribute::kName) + |
| "'"; |
| *dst += "\n"; |
| for (auto iter = node->UnignoredChildrenBegin(); |
| iter != node->UnignoredChildrenEnd(); ++iter) { |
| DumpRolesAndNamesAsText(iter.get(), indent + 1, dst); |
| } |
| } |
| |
| } // namespace |
| |
| class SnapshotAXTreeBrowserTest : public ContentBrowserTest { |
| public: |
| SnapshotAXTreeBrowserTest() { |
| #if BUILDFLAG(IS_ANDROID) |
| scoped_feature_list_.InitAndDisableFeature( |
| features::kAccessibilitySnapshotStressTests); |
| #endif |
| } |
| ~SnapshotAXTreeBrowserTest() override = default; |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest, |
| SnapshotAccessibilityTreeFromWebContents) { |
| GURL url("data:text/html,<button>Click</button>"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| AXTreeSnapshotWaiter waiter; |
| web_contents->RequestAXTreeSnapshot( |
| base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot, |
| base::Unretained(&waiter)), |
| ui::kAXModeComplete, |
| /* max_nodes= */ 0, |
| /* timeout= */ {}); |
| waiter.Wait(); |
| |
| // Dump the whole tree if one of the assertions below fails |
| // to aid in debugging why it failed. |
| SCOPED_TRACE(waiter.snapshot().ToString()); |
| |
| ui::AXTree tree(waiter.snapshot()); |
| ui::AXNode* root = tree.root(); |
| ASSERT_NE(nullptr, root); |
| ASSERT_EQ(ax::mojom::Role::kRootWebArea, root->GetRole()); |
| ui::AXNode* group = root->GetUnignoredChildAtIndex(0); |
| ASSERT_EQ(ax::mojom::Role::kGenericContainer, group->GetRole()); |
| ui::AXNode* button = group->GetUnignoredChildAtIndex(0); |
| ASSERT_EQ(ax::mojom::Role::kButton, button->GetRole()); |
| } |
| |
| class SnapshotAXTreeFencedFrameBrowserTest : public SnapshotAXTreeBrowserTest { |
| public: |
| SnapshotAXTreeFencedFrameBrowserTest() { |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| {{blink::features::kFencedFrames, {{"implementation_type", "mparch"}}}, |
| {features::kPrivacySandboxAdsAPIsOverride, {}}, |
| {blink::features::kFencedFramesAPIChanges, {}}, |
| {blink::features::kFencedFramesDefaultMode, {}}}, |
| {/* disabled_features */}); |
| } |
| |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| SnapshotAXTreeBrowserTest::SetUpOnMainThread(); |
| |
| https_server()->AddDefaultHandlers(GetTestDataFilePath()); |
| https_server()->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES); |
| SetupCrossSiteRedirector(https_server()); |
| ASSERT_TRUE(https_server()->Start()); |
| } |
| |
| net::EmbeddedTestServer* https_server() { return &https_server_; } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS}; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(SnapshotAXTreeFencedFrameBrowserTest, |
| SnapshotAccessibilityTreeFromMultipleFrames) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), https_server()->GetURL("a.test", "/fenced_frames/basic.html"))); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| RenderFrameHostImpl* primary_rfh = web_contents->GetPrimaryMainFrame(); |
| std::vector<FencedFrame*> fenced_frames = primary_rfh->GetFencedFrames(); |
| EXPECT_EQ(1u, fenced_frames.size()); |
| |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| EXPECT_TRUE( |
| ExecJs(primary_rfh, JsReplace("document.querySelector('fencedframe')." |
| "config = new FencedFrameConfig($1);", |
| fenced_frame_url.spec()))); |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| |
| AXTreeSnapshotWaiter waiter; |
| web_contents->RequestAXTreeSnapshot( |
| base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot, |
| base::Unretained(&waiter)), |
| ui::kAXModeComplete, |
| /* max_nodes= */ 0, |
| /* timeout= */ {}); |
| waiter.Wait(); |
| |
| // Dump the whole tree if one of the assertions below fails |
| // to aid in debugging why it failed. |
| SCOPED_TRACE(waiter.snapshot().ToString()); |
| |
| ui::AXTree tree(waiter.snapshot()); |
| ui::AXNode* root = tree.root(); |
| std::string dump; |
| DumpRolesAndNamesAsText(root, 0, &dump); |
| EXPECT_EQ( |
| "rootWebArea\n" |
| " genericContainer\n" |
| " iframe\n" |
| " rootWebArea\n" |
| " genericContainer\n" |
| " staticText 'This page has no title.'\n", |
| dump); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest, |
| SnapshotAccessibilityTreeFromMultipleFrames) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| EXPECT_TRUE(NavigateToURL( |
| shell(), |
| embedded_test_server()->GetURL("/accessibility/snapshot/outer.html"))); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| FrameTreeNode* root_frame = web_contents->GetPrimaryFrameTree().root(); |
| |
| EXPECT_TRUE(NavigateToURLFromRenderer(root_frame->child_at(0), |
| GURL("data:text/plain,Alpha"))); |
| EXPECT_TRUE(NavigateToURLFromRenderer( |
| root_frame->child_at(1), |
| embedded_test_server()->GetURL("/accessibility/snapshot/inner.html"))); |
| |
| AXTreeSnapshotWaiter waiter; |
| web_contents->RequestAXTreeSnapshot( |
| base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot, |
| base::Unretained(&waiter)), |
| ui::kAXModeComplete, |
| /* max_nodes= */ 0, |
| /* timeout= */ {}); |
| waiter.Wait(); |
| |
| // Dump the whole tree if one of the assertions below fails |
| // to aid in debugging why it failed. |
| SCOPED_TRACE(waiter.snapshot().ToString()); |
| |
| ui::AXTree tree(waiter.snapshot()); |
| ui::AXNode* root = tree.root(); |
| std::string dump; |
| DumpRolesAndNamesAsText(root, 0, &dump); |
| EXPECT_EQ( |
| "rootWebArea\n" |
| " genericContainer\n" |
| " button 'Before'\n" |
| " staticText 'Before'\n" |
| " iframe\n" |
| " rootWebArea\n" |
| " genericContainer\n" |
| " staticText 'Alpha'\n" |
| " button 'Middle'\n" |
| " staticText 'Middle'\n" |
| " iframe\n" |
| " rootWebArea\n" |
| " genericContainer\n" |
| " button 'Inside Before'\n" |
| " staticText 'Inside Before'\n" |
| " iframe\n" |
| " rootWebArea\n" |
| " button 'Inside After'\n" |
| " staticText 'Inside After'\n" |
| " button 'After'\n" |
| " staticText 'After'\n", |
| dump); |
| } |
| |
| // This tests to make sure that RequestAXTreeSnapshot() correctly traverses |
| // inner contents, as used in features like <webview>. |
| IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest, |
| SnapshotAccessibilityTreeWithInnerContents) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| EXPECT_TRUE(NavigateToURL( |
| shell(), |
| embedded_test_server()->GetURL("/accessibility/snapshot/outer.html"))); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| FrameTreeNode* root_frame = web_contents->GetPrimaryFrameTree().root(); |
| |
| EXPECT_TRUE(NavigateToURLFromRenderer(root_frame->child_at(0), |
| GURL("data:text/plain,Alpha"))); |
| |
| WebContentsImpl* inner_contents = |
| static_cast<WebContentsImpl*>(CreateAndAttachInnerContents( |
| root_frame->child_at(1)->current_frame_host())); |
| EXPECT_TRUE(NavigateToURLFromRenderer( |
| inner_contents->GetPrimaryFrameTree().root(), |
| embedded_test_server()->GetURL("/accessibility/snapshot/inner.html"))); |
| |
| AXTreeSnapshotWaiter waiter; |
| web_contents->RequestAXTreeSnapshot( |
| base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot, |
| base::Unretained(&waiter)), |
| ui::kAXModeComplete, |
| /* max_nodes= */ 0, |
| /* timeout= */ {}); |
| waiter.Wait(); |
| |
| // Dump the whole tree if one of the assertions below fails |
| // to aid in debugging why it failed. |
| SCOPED_TRACE(waiter.snapshot().ToString()); |
| |
| ui::AXTree tree(waiter.snapshot()); |
| ui::AXNode* root = tree.root(); |
| std::string dump; |
| DumpRolesAndNamesAsText(root, 0, &dump); |
| EXPECT_EQ( |
| "rootWebArea\n" |
| " genericContainer\n" |
| " button 'Before'\n" |
| " staticText 'Before'\n" |
| " iframe\n" |
| " rootWebArea\n" |
| " genericContainer\n" |
| " staticText 'Alpha'\n" |
| " button 'Middle'\n" |
| " staticText 'Middle'\n" |
| " iframe\n" |
| " rootWebArea\n" |
| " genericContainer\n" |
| " button 'Inside Before'\n" |
| " staticText 'Inside Before'\n" |
| " iframe\n" |
| " rootWebArea\n" |
| " button 'Inside After'\n" |
| " staticText 'Inside After'\n" |
| " button 'After'\n" |
| " staticText 'After'\n", |
| dump); |
| } |
| |
| // This tests to make sure that snapshotting with different modes gives |
| // different results. This is not intended to ensure that specific modes give |
| // specific attributes, but merely to ensure that the mode parameter makes a |
| // difference. |
| IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest, |
| SnapshotAccessibilityTreeModes) { |
| GURL url("data:text/html,<button>Click</button>"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| AXTreeSnapshotWaiter waiter_complete; |
| web_contents->RequestAXTreeSnapshot( |
| base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot, |
| base::Unretained(&waiter_complete)), |
| ui::kAXModeComplete, |
| /* max_nodes= */ 0, |
| /* timeout= */ {}); |
| waiter_complete.Wait(); |
| const std::vector<ui::AXNodeData>& complete_nodes = |
| waiter_complete.snapshot().nodes; |
| |
| // Dump the whole tree if one of the assertions below fails |
| // to aid in debugging why it failed. |
| SCOPED_TRACE(waiter_complete.snapshot().ToString()); |
| |
| AXTreeSnapshotWaiter waiter_contents; |
| web_contents->RequestAXTreeSnapshot( |
| base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot, |
| base::Unretained(&waiter_contents)), |
| ui::AXMode::kWebContents, |
| /* max_nodes= */ 0, |
| /* timeout= */ {}); |
| waiter_contents.Wait(); |
| const std::vector<ui::AXNodeData>& contents_nodes = |
| waiter_contents.snapshot().nodes; |
| |
| // Dump the whole tree if one of the assertions below fails |
| // to aid in debugging why it failed. |
| SCOPED_TRACE(waiter_contents.snapshot().ToString()); |
| |
| // The two snapshot passes walked the tree in the same order, so comparing |
| // element to element is possible by walking the snapshots in parallel. |
| |
| auto total_attribute_count = [](const ui::AXNodeData& node_data) { |
| return node_data.string_attributes.size() + |
| node_data.int_attributes.size() + node_data.float_attributes.size() + |
| node_data.bool_attributes.size() + |
| node_data.intlist_attributes.size() + |
| node_data.stringlist_attributes.size() + |
| node_data.html_attributes.size(); |
| }; |
| |
| ASSERT_EQ(complete_nodes.size(), contents_nodes.size()); |
| for (size_t i = 0; i < complete_nodes.size(); ++i) |
| EXPECT_LT(total_attribute_count(contents_nodes[i]), |
| total_attribute_count(complete_nodes[i])); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest, SnapshotPDFMode) { |
| // The "PDF" accessibility mode is used when getting a snapshot of the |
| // accessibility tree in order to export a tagged PDF. Ensure that |
| // we're serializing the right set of attributes needed for a PDF and |
| // also ensure that we're *not* wasting time serializing attributes |
| // that are not needed for PDF export. |
| GURL url(R"HTML(data:text/html,<body> |
| <img src="" alt="Unicorns"> |
| <ul> |
| <li aria-posinset="5"> |
| <span style="color: red;">Red text</span> |
| </ul> |
| <table role="table"> |
| <tr> |
| <td colspan="2"> |
| </tr> |
| <tr> |
| <td>1</td><td>2</td> |
| </tr> |
| </table> |
| </body>)HTML"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| auto* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); |
| AXTreeSnapshotWaiter waiter; |
| web_contents->RequestAXTreeSnapshot( |
| base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot, |
| base::Unretained(&waiter)), |
| ui::AXMode::kPDFPrinting, |
| /* max_nodes= */ 0, |
| /* timeout= */ {}); |
| waiter.Wait(); |
| |
| // Dump the whole tree if one of the assertions below fails |
| // to aid in debugging why it failed. |
| SCOPED_TRACE(waiter.snapshot().ToString()); |
| |
| // Scan all of the nodes and make some general assertions. |
| int dom_node_id_count = 0; |
| for (const ui::AXNodeData& node_data : waiter.snapshot().nodes) { |
| // Every node should have a valid role, state, and ID. |
| EXPECT_NE(ax::mojom::Role::kUnknown, node_data.role); |
| EXPECT_NE(0, node_data.id); |
| |
| if (node_data.GetIntAttribute(ax::mojom::IntAttribute::kDOMNodeId) != 0) |
| dom_node_id_count++; |
| |
| // We don't need bounding boxes to make a tagged PDF. Ensure those are |
| // uninitialized. |
| EXPECT_TRUE(node_data.relative_bounds.bounds.IsEmpty()); |
| |
| // We shouldn't get any inline text box nodes. They aren't needed to |
| // make a tagged PDF and they make up a large fraction of nodes in the |
| // tree when present. |
| EXPECT_NE(ax::mojom::Role::kInlineTextBox, node_data.role); |
| |
| // We shouldn't have any style information like color in the tree. |
| EXPECT_FALSE(node_data.HasIntAttribute(ax::mojom::IntAttribute::kColor)); |
| } |
| |
| // Many nodes should have a DOM node id. That's not normally included |
| // in the accessibility tree but it's needed for associating nodes with |
| // rendered text in the PDF file. |
| EXPECT_GT(dom_node_id_count, 5); |
| |
| // Build an AXTree from the snapshot and make some specific assertions. |
| ui::AXTree tree(waiter.snapshot()); |
| ui::AXNode* root = tree.root(); |
| ASSERT_TRUE(root); |
| ASSERT_EQ(ax::mojom::Role::kRootWebArea, root->GetRole()); |
| |
| // Img alt text should be present. |
| ui::AXNode* image = root->GetUnignoredChildAtIndex(0); |
| ASSERT_TRUE(image); |
| ASSERT_EQ(ax::mojom::Role::kImage, image->GetRole()); |
| ASSERT_EQ("Unicorns", |
| image->GetStringAttribute(ax::mojom::StringAttribute::kName)); |
| |
| // List attributes like posinset should be present. |
| ui::AXNode* ul = root->GetUnignoredChildAtIndex(1); |
| ASSERT_TRUE(ul); |
| ASSERT_EQ(ax::mojom::Role::kList, ul->GetRole()); |
| ui::AXNode* li = ul->GetUnignoredChildAtIndex(0); |
| ASSERT_TRUE(li); |
| ASSERT_EQ(ax::mojom::Role::kListItem, li->GetRole()); |
| EXPECT_EQ(5, *li->GetPosInSet()); |
| |
| // Table attributes like colspan should be present. |
| ui::AXNode* table = root->GetUnignoredChildAtIndex(2); |
| ASSERT_TRUE(table); |
| ASSERT_EQ(ax::mojom::Role::kTable, table->GetRole()); |
| ui::AXNode* tr = table->GetUnignoredChildAtIndex(0); |
| ASSERT_TRUE(tr); |
| ASSERT_EQ(ax::mojom::Role::kRow, tr->GetRole()); |
| ui::AXNode* td = tr->GetUnignoredChildAtIndex(0); |
| ASSERT_TRUE(td); |
| ASSERT_EQ(ax::mojom::Role::kCell, td->GetRole()); |
| EXPECT_EQ(2, *td->GetTableCellColSpan()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest, MaxNodes) { |
| GURL url(R"HTML(data:text/html,<body> |
| <style> p { margin: 50px; } </style> |
| <script> |
| for (let i = 0; i < 10; i++) { |
| let div = document.createElement('div'); |
| for (let j = 0; j < 10; j++) { |
| let p = document.createElement('p'); |
| p.innerHTML = i; |
| div.appendChild(p); |
| } |
| document.body.appendChild(div); |
| } |
| </script> |
| </body>)HTML"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| AXTreeSnapshotWaiter waiter; |
| web_contents->RequestAXTreeSnapshot( |
| base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot, |
| base::Unretained(&waiter)), |
| ui::kAXModeComplete, |
| /* max_nodes= */ 10, |
| /* timeout= */ {}); |
| waiter.Wait(); |
| |
| // Dump the whole tree if one of the assertions below fails |
| // to aid in debugging why it failed. |
| SCOPED_TRACE(waiter.snapshot().ToString()); |
| |
| // If we didn't set a maximum number of nodes, thee would be at least 200 |
| // nodes on the page (2 for every paragraph, and there are 10 divs each |
| // containing 10 paragraphs). By setting the max to 10 nodes, we should |
| // get only the first div - and the rest of the divs will be empty. |
| // The end result is a little more than 20 nodes, nowhere close to 200. |
| EXPECT_LT(waiter.snapshot().nodes.size(), 35U); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest, Timeout) { |
| GURL url(R"HTML(data:text/html,<body> |
| <style> p { margin: 50px; } </style> |
| <script> |
| for (let i = 0; i < 100; i++) { |
| let p = document.createElement('p'); |
| p.innerHTML = i; |
| document.body.append(p); |
| } |
| </script> |
| </body>)HTML"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| // Get the number of nodes with no timeout. |
| size_t actual_nodes = 0; |
| { |
| AXTreeSnapshotWaiter waiter; |
| web_contents->RequestAXTreeSnapshot( |
| base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot, |
| base::Unretained(&waiter)), |
| ui::kAXModeComplete, |
| /* max_nodes= */ 0, |
| /* timeout= */ {}); |
| waiter.Wait(); |
| actual_nodes = waiter.snapshot().nodes.size(); |
| LOG(INFO) << "Actual nodes: " << actual_nodes; |
| } |
| |
| // Request a snapshot with a timeout of 1 ms. The test succeeds if |
| // we get fewer nodes. There's a tiny chance we don't hit the timeout, |
| // so keep trying indefinitely until the test either passes or times out. |
| size_t nodes_with_timeout = actual_nodes; |
| while (nodes_with_timeout >= actual_nodes) { |
| AXTreeSnapshotWaiter waiter; |
| web_contents->RequestAXTreeSnapshot( |
| base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot, |
| base::Unretained(&waiter)), |
| ui::kAXModeComplete, |
| /* max_nodes= */ 0, |
| /* timeout= */ base::Milliseconds(1)); |
| waiter.Wait(); |
| |
| nodes_with_timeout = waiter.snapshot().nodes.size(); |
| LOG(INFO) << "Nodes with timeout: " << nodes_with_timeout; |
| } |
| |
| EXPECT_LT(nodes_with_timeout, actual_nodes); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest, Metadata) { |
| GURL url(R"HTML(data:text/html, |
| <head> |
| <title>Hello World</title> |
| <script>console.log("Skip me!");</script> |
| <meta charset="utf-8"> |
| <link ref="canonical" href="https://abc.com"> |
| <script type="application/ld+json">{}</script> |
| </head> |
| <body> |
| Hello, world! |
| </body>)HTML"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| ui::AXMode mode(ui::AXMode::kWebContents | ui::AXMode::kHTMLMetadata); |
| |
| AXTreeSnapshotWaiter waiter; |
| web_contents->RequestAXTreeSnapshot( |
| base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot, |
| base::Unretained(&waiter)), |
| mode, |
| /* max_nodes= */ 0, |
| /* timeout= */ {}); |
| waiter.Wait(); |
| |
| EXPECT_THAT( |
| waiter.snapshot().tree_data.metadata, |
| testing::ElementsAre( |
| "<title>Hello World</title>", "<meta charset=\"utf-8\"></meta>", |
| "<link ref=\"canonical\" href=\"https://abc.com\"></link>", |
| "<script type=\"application/ld+json\">{}</script>")); |
| } |
| |
| // For Android, test the field trial param to change the number of max nodes. |
| #if BUILDFLAG(IS_ANDROID) |
| class SnapshotAXTreeMaxNodesParamBrowserTest : public ContentBrowserTest { |
| public: |
| SnapshotAXTreeMaxNodesParamBrowserTest() { |
| scoped_feature_list_.InitAndEnableFeatureWithParameters( |
| features::kAccessibilitySnapshotStressTests, |
| {{"AccessibilitySnapshotStressTestsMaxNodes", "500"}}); |
| } |
| ~SnapshotAXTreeMaxNodesParamBrowserTest() override = default; |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(SnapshotAXTreeMaxNodesParamBrowserTest, MaxNodes) { |
| GURL url(R"HTML(data:text/html,<body> |
| <style> p { margin: 50px; } </style> |
| <script> |
| let outerDiv = document.createElement('div'); |
| for (let i = 0; i < 200; i++) { |
| let div = document.createElement('div'); |
| for (let j = 0; j < 20; j++) { |
| let p = document.createElement('p'); |
| p.innerHTML = j; |
| div.appendChild(p); |
| } |
| outerDiv.appendChild(div); |
| } |
| document.body.appendChild(outerDiv); |
| </script> |
| </body>)HTML"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| AXTreeSnapshotWaiter waiter; |
| web_contents->RequestAXTreeSnapshot( |
| base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot, |
| base::Unretained(&waiter)), |
| ui::kAXModeComplete, |
| /* max_nodes= */ 10, |
| /* timeout= */ {}); |
| waiter.Wait(); |
| |
| // If the feature flag and param was honored, we should see more than 100 |
| // nodes (which was the value set on the method call), but less than all the |
| // possible nodes. |
| EXPECT_GT(waiter.snapshot().nodes.size(), 100U); |
| EXPECT_LT(waiter.snapshot().nodes.size(), 800U); |
| } |
| |
| #endif |
| } // namespace content |