0

[fuchsia][a11y] Add code path to instantiate v2 accessibility bridge.

This change adds a code path by which FrameImpl can instantiate the v2
accessibility bridge. We also add a new set of browsertests for the new
accessibility bridge.

AX-Relnotes: n/a
Test: Accessibility*Test.*
Bug: fuchsia:90880
Change-Id: Ie96af26fdd2b247f14c1ebdad6e074dc218bc995
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3389994
Reviewed-by: Sergey Ulanov <sergeyu@chromium.org>
Reviewed-by: David Tseng <dtseng@chromium.org>
Commit-Queue: Alexander Brusher <abrusher@google.com>
Cr-Commit-Position: refs/heads/main@{#969651}
This commit is contained in:
Alexander Brusher
2022-02-10 21:48:44 +00:00
committed by Chromium LUCI CQ
parent 4fdf8bb596
commit 78c1a67268
16 changed files with 756 additions and 53 deletions

@ -430,6 +430,7 @@ source_set("browsertest_core") {
test("web_engine_browsertests") {
sources = [
"browser/accessibility_bridge_browsertest.cc",
"browser/accessibility_browsertest.cc",
"browser/autoplay_browsertest.cc",
"browser/cast_streaming_browsertest.cc",
"browser/client_hints_browsertest.cc",

@ -54,7 +54,7 @@ AccessibilityBridge::AccessibilityBridge(
fuchsia::accessibility::semantics::SemanticsManager* semantics_manager,
FrameWindowTreeHost* window_tree_host,
content::WebContents* web_contents,
base::OnceCallback<void(zx_status_t)> on_error_callback,
base::OnceCallback<bool(zx_status_t)> on_error_callback,
inspect::Node inspect_node)
: binding_(this),
window_tree_host_(window_tree_host),

@ -53,7 +53,7 @@ class WEB_ENGINE_EXPORT AccessibilityBridge final
fuchsia::accessibility::semantics::SemanticsManager* semantics_manager,
FrameWindowTreeHost* window_tree_host,
content::WebContents* web_contents,
base::OnceCallback<void(zx_status_t)> on_error_callback,
base::OnceCallback<bool(zx_status_t)> on_error_callback,
inspect::Node inspect_node);
~AccessibilityBridge() override;
@ -246,7 +246,7 @@ class WEB_ENGINE_EXPORT AccessibilityBridge final
// Run in the case of an internal error that cannot be recovered from. This
// will cause the frame |this| is owned by to be torn down.
base::OnceCallback<void(zx_status_t)> on_error_callback_;
base::OnceCallback<bool(zx_status_t)> on_error_callback_;
// The root id of the AXTree of the main frame.
int32_t root_id_ = 0;

@ -53,6 +53,9 @@ const size_t kPage2NodeCount = 190;
const size_t kInitialRangeValue = 51;
const size_t kStepSize = 3;
// Simulated screen bounds to use when testing the SemanticsManager.
constexpr gfx::Size kTestWindowSize = {720, 640};
fuchsia::math::PointF GetCenterOfBox(fuchsia::ui::gfx::BoundingBox box) {
fuchsia::math::PointF center;
center.x = (box.min.x + box.max.x) / 2;
@ -131,6 +134,11 @@ class AccessibilityBridgeTest : public cr_fuchsia::WebEngineBrowserTest {
frame_impl_ = context_impl()->GetFrameImplForTest(&frame_.ptr());
frame_impl_->set_semantics_manager_for_test(&semantics_manager_);
frame_impl_->set_window_size_for_test(kTestWindowSize);
// TODO(crbug.com/1291330): Remove uses of
// set_use_v2_accessibility_bridge().
frame_impl_->set_use_v2_accessibility_bridge(false);
frame_->EnableHeadlessRendering();
semantics_manager_.WaitUntilViewRegistered();

@ -0,0 +1,611 @@
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <fuchsia/accessibility/semantics/cpp/fidl.h>
#include <lib/ui/scenic/cpp/view_ref_pair.h>
#include <zircon/types.h>
#include "base/fuchsia/mem_buffer_util.h"
#include "base/fuchsia/scoped_service_binding.h"
#include "base/fuchsia/test_component_context_for_process.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "content/public/test/browser_test.h"
#include "fuchsia/base/test/frame_test_util.h"
#include "fuchsia/base/test/test_navigation_listener.h"
#include "fuchsia/engine/browser/accessibility_bridge.h"
#include "fuchsia/engine/browser/context_impl.h"
#include "fuchsia/engine/browser/fake_semantics_manager.h"
#include "fuchsia/engine/browser/frame_impl.h"
#include "fuchsia/engine/test/frame_for_test.h"
#include "fuchsia/engine/test/test_data.h"
#include "fuchsia/engine/test/web_engine_browser_test.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_tree_observer.h"
#include "ui/accessibility/platform/fuchsia/ax_platform_node_fuchsia.h"
#include "ui/gfx/switches.h"
#include "ui/ozone/public/ozone_switches.h"
namespace {
const char kPage1Path[] = "/ax1.html";
const char kPage2Path[] = "/batching.html";
const char kPageIframePath[] = "/iframe.html";
const char kPage1Title[] = "accessibility 1";
const char kPage2Title[] = "lots of nodes!";
const char kPageIframeTitle[] = "iframe title";
const char kButtonName1[] = "a button";
const char kButtonName2[] = "another button";
const char kButtonName3[] = "button 3";
const char kNodeName[] = "last node";
const char kParagraphName[] = "a third paragraph";
const char kOffscreenNodeName[] = "offscreen node";
const size_t kPage1NodeCount = 29;
const size_t kPage2NodeCount = 190;
const size_t kInitialRangeValue = 51;
const size_t kStepSize = 3;
// Simulated screen bounds to use.
constexpr gfx::Size kTestWindowSize = {720, 640};
fuchsia::math::PointF GetCenterOfBox(fuchsia::ui::gfx::BoundingBox box) {
fuchsia::math::PointF center;
center.x = (box.min.x + box.max.x) / 2;
center.y = (box.min.y + box.max.y) / 2;
return center;
}
// Returns whether or not the given node supports the given action.
bool HasAction(const fuchsia::accessibility::semantics::Node& node,
fuchsia::accessibility::semantics::Action action) {
for (const auto& node_action : node.actions()) {
if (node_action == action)
return true;
}
return false;
}
} // namespace
class FuchsiaFrameAccessibilityTest : public cr_fuchsia::WebEngineBrowserTest {
public:
FuchsiaFrameAccessibilityTest() {
cr_fuchsia::WebEngineBrowserTest::set_test_server_root(
base::FilePath(cr_fuchsia::kTestServerRoot));
}
~FuchsiaFrameAccessibilityTest() override = default;
FuchsiaFrameAccessibilityTest(const FuchsiaFrameAccessibilityTest&) = delete;
FuchsiaFrameAccessibilityTest& operator=(
const FuchsiaFrameAccessibilityTest&) = delete;
void SetUp() override {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
command_line->AppendSwitchNative(switches::kOzonePlatform,
switches::kHeadless);
command_line->AppendSwitch(switches::kHeadless);
cr_fuchsia::WebEngineBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
test_context_.emplace(
base::TestComponentContextForProcess::InitialState::kCloneAll);
cr_fuchsia::WebEngineBrowserTest::SetUpOnMainThread();
// Remove the injected a11y manager from /svc; otherwise, we won't be able
// to replace it with the fake owned by the test fixture.
test_context_->additional_services()
->RemovePublicService<
fuchsia::accessibility::semantics::SemanticsManager>();
semantics_manager_binding_.emplace(test_context_->additional_services(),
&semantics_manager_);
frame_ = cr_fuchsia::FrameForTest::Create(context(), {});
base::RunLoop().RunUntilIdle();
frame_impl_ = context_impl()->GetFrameImplForTest(&frame_.ptr());
frame_impl_->set_window_size_for_test(kTestWindowSize);
frame_impl_->set_use_v2_accessibility_bridge(true);
frame_->EnableHeadlessRendering();
semantics_manager_.WaitUntilViewRegistered();
ASSERT_TRUE(semantics_manager_.is_view_registered());
ASSERT_TRUE(semantics_manager_.is_listener_valid());
ASSERT_TRUE(embedded_test_server()->Start());
// Change the accessibility mode on the Fuchsia side and check that it is
// propagated correctly.
ASSERT_FALSE(frame_impl_->web_contents_for_test()
->IsFullAccessibilityModeForTesting());
semantics_manager_.SetSemanticsModeEnabled(true);
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(frame_impl_->web_contents_for_test()
->IsFullAccessibilityModeForTesting());
}
void LoadPage(base::StringPiece url, base::StringPiece page_title) {
GURL page_url(embedded_test_server()->GetURL(std::string(url)));
ASSERT_TRUE(cr_fuchsia::LoadUrlAndExpectResponse(
frame_.GetNavigationController(), fuchsia::web::LoadUrlParams(),
page_url.spec()));
frame_.navigation_listener().RunUntilUrlAndTitleEquals(page_url,
page_title);
}
protected:
// TODO(crbug.com/1038786): Maybe move to WebEngineBrowserTest.
absl::optional<base::TestComponentContextForProcess> test_context_;
cr_fuchsia::FrameForTest frame_;
FrameImpl* frame_impl_;
FakeSemanticsManager semantics_manager_;
// Binding to the fake semantics manager.
// Optional so that it can be instantiated outside the constructor.
absl::optional<base::ScopedServiceBinding<
fuchsia::accessibility::semantics::SemanticsManager>>
semantics_manager_binding_;
};
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, CorrectDataSent) {
LoadPage(kPage1Path, kPage1Title);
// Check that the data values are correct in the FakeSemanticTree.
semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage1NodeCount);
EXPECT_TRUE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kPage1Title));
EXPECT_TRUE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kButtonName1));
EXPECT_TRUE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kParagraphName));
}
// Batching is performed when the number of nodes to send or delete exceeds the
// maximum, as set on the Fuchsia side. Check that all nodes are received by the
// Semantic Tree when batching is performed.
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, DataSentWithBatching) {
LoadPage(kPage2Path, kPage2Title);
// Run until we expect more than a batch's worth of nodes to be present.
semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage2NodeCount);
EXPECT_TRUE(semantics_manager_.semantic_tree()->GetNodeFromLabel(kNodeName));
// Checks if the actual batching happened.
EXPECT_GE(semantics_manager_.semantic_tree()->num_update_calls(), 18u);
// Checks if one or more commit calls were made to send the data.
EXPECT_GE(semantics_manager_.semantic_tree()->num_commit_calls(), 1u);
}
// Check that semantics information is correctly sent when navigating from page
// to page.
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, NavigateFromPageToPage) {
LoadPage(kPage1Path, kPage1Title);
semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage1NodeCount);
EXPECT_TRUE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kPage1Title));
EXPECT_TRUE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kButtonName1));
EXPECT_TRUE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kParagraphName));
LoadPage(kPage2Path, kPage2Title);
semantics_manager_.semantic_tree()->RunUntilNodeWithLabelIsInTree(
kPage2Title);
EXPECT_TRUE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kPage2Title));
EXPECT_TRUE(semantics_manager_.semantic_tree()->GetNodeFromLabel(kNodeName));
// Check that data from the first page has been deleted successfully.
EXPECT_FALSE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kButtonName1));
EXPECT_FALSE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kParagraphName));
}
// Checks that the correct node ID is returned when performing hit testing.
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, HitTest) {
LoadPage(kPage1Path, kPage1Title);
semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage1NodeCount);
fuchsia::accessibility::semantics::Node* target_node =
semantics_manager_.semantic_tree()->GetNodeFromLabel(kParagraphName);
EXPECT_TRUE(target_node);
fuchsia::math::PointF target_point = GetCenterOfBox(target_node->location());
float scale_factor = 20.f;
// Make the bridge use scaling in hit test calculations.
frame_impl_->set_device_scale_factor_for_test(scale_factor);
// Downscale the target point, since the hit test calculation will scale it
// back up.
target_point.x /= scale_factor;
target_point.y /= scale_factor;
uint32_t hit_node_id =
semantics_manager_.HitTestAtPointSync(std::move(target_point));
fuchsia::accessibility::semantics::Node* hit_node =
semantics_manager_.semantic_tree()->GetNodeWithId(hit_node_id);
EXPECT_EQ(hit_node->attributes().label(), kParagraphName);
// Expect hit testing to return the root when the point given is out of
// bounds or there is no semantic node at that position.
target_point.x = -1;
target_point.y = -1;
EXPECT_EQ(0u, semantics_manager_.HitTestAtPointSync(std::move(target_point)));
target_point.x = 1. / scale_factor;
target_point.y = 1. / scale_factor;
EXPECT_EQ(0u, semantics_manager_.HitTestAtPointSync(std::move(target_point)));
}
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, PerformDefaultAction) {
LoadPage(kPage1Path, kPage1Title);
semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage1NodeCount);
fuchsia::accessibility::semantics::Node* button1 =
semantics_manager_.semantic_tree()->GetNodeFromLabel(kButtonName1);
EXPECT_TRUE(button1);
fuchsia::accessibility::semantics::Node* button2 =
semantics_manager_.semantic_tree()->GetNodeFromLabel(kButtonName2);
EXPECT_TRUE(button2);
fuchsia::accessibility::semantics::Node* button3 =
semantics_manager_.semantic_tree()->GetNodeFromLabel(kButtonName3);
EXPECT_TRUE(button3);
EXPECT_TRUE(
HasAction(*button1, fuchsia::accessibility::semantics::Action::DEFAULT));
EXPECT_TRUE(semantics_manager_.RequestAccessibilityActionSync(
button1->node_id(), fuchsia::accessibility::semantics::Action::DEFAULT));
}
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest,
PerformUnsupportedAction) {
LoadPage(kPage1Path, kPage1Title);
semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage1NodeCount);
fuchsia::accessibility::semantics::Node* button1 =
semantics_manager_.semantic_tree()->GetNodeFromLabel(kButtonName1);
EXPECT_TRUE(button1);
fuchsia::accessibility::semantics::Node* button2 =
semantics_manager_.semantic_tree()->GetNodeFromLabel(kButtonName2);
EXPECT_TRUE(button2);
// Attempt to perform unsupported action.
EXPECT_FALSE(semantics_manager_.RequestAccessibilityActionSync(
button2->node_id(),
fuchsia::accessibility::semantics::Action::SECONDARY));
}
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, Disconnect) {
base::RunLoop run_loop;
frame_.ptr().set_error_handler([&run_loop](zx_status_t status) {
EXPECT_EQ(ZX_ERR_INTERNAL, status);
run_loop.Quit();
});
semantics_manager_.semantic_tree()->Disconnect();
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest,
PerformScrollToMakeVisible) {
// Set the screen height to be small so that we can detect if we've
// scrolled past our target, even if the max scroll is bounded.
constexpr int kScreenWidth = 720;
constexpr int kScreenHeight = 20;
gfx::Rect screen_bounds(kScreenWidth, kScreenHeight);
LoadPage(kPage1Path, kPage1Title);
auto* semantic_tree = semantics_manager_.semantic_tree();
ASSERT_TRUE(semantic_tree);
semantic_tree->RunUntilNodeCountAtLeast(kPage1NodeCount);
auto* content_view =
frame_impl_->web_contents_for_test()->GetContentNativeView();
content_view->SetBounds(screen_bounds);
// Get a node that is off the screen, and verify that it is off the screen.
fuchsia::accessibility::semantics::Node* fuchsia_node =
semantic_tree->GetNodeFromLabel(kOffscreenNodeName);
ASSERT_TRUE(fuchsia_node);
// Get the corresponding AXPlatformNode.
auto* fuchsia_platform_node = static_cast<ui::AXPlatformNodeFuchsia*>(
ui::AXPlatformNodeBase::GetFromUniqueId(fuchsia_node->node_id()));
ASSERT_TRUE(fuchsia_platform_node);
auto* delegate = fuchsia_platform_node->GetDelegate();
ui::AXOffscreenResult offscreen_result;
delegate->GetClippedScreenBoundsRect(&offscreen_result);
EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOffscreen);
// Perform SHOW_ON_SCREEN on that node.
EXPECT_TRUE(semantics_manager_.RequestAccessibilityActionSync(
fuchsia_node->node_id(),
fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN));
semantic_tree->RunUntilConditionIsTrue(
base::BindLambdaForTesting([semantic_tree]() {
auto* root = semantic_tree->GetNodeWithId(0u);
if (!root)
return false;
// Once the scroll action has been handled, the root should have a
// non-zero y-scroll offset.
return root->has_states() && root->states().has_viewport_offset() &&
root->states().viewport_offset().y > 0;
}));
// Verify that the AXNode we tried to make visible is now onscreen.
delegate->GetClippedScreenBoundsRect(&offscreen_result);
EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);
}
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, Slider) {
LoadPage(kPage1Path, kPage1Title);
semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage1NodeCount);
fuchsia::accessibility::semantics::Node* node =
semantics_manager_.semantic_tree()->GetNodeFromRole(
fuchsia::accessibility::semantics::Role::SLIDER);
EXPECT_TRUE(node);
EXPECT_TRUE(node->has_states() && node->states().has_range_value());
EXPECT_EQ(node->states().range_value(), kInitialRangeValue);
base::RunLoop run_loop;
semantics_manager_.semantic_tree()->SetNodeUpdatedCallback(
node->node_id(), run_loop.QuitClosure());
semantics_manager_.RequestAccessibilityActionSync(
node->node_id(), fuchsia::accessibility::semantics::Action::INCREMENT);
run_loop.Run();
node = semantics_manager_.semantic_tree()->GetNodeWithId(node->node_id());
EXPECT_TRUE(node->has_states() && node->states().has_range_value());
EXPECT_EQ(node->states().range_value(), kInitialRangeValue + kStepSize);
}
// This test makes sure that when semantic updates toggle on / off / on, the
// full semantic tree is sent in the first update when back on.
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, TogglesSemanticsUpdates) {
LoadPage(kPage1Path, kPage1Title);
semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage1NodeCount);
semantics_manager_.SetSemanticsModeEnabled(false);
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(frame_impl_->web_contents_for_test()
->IsFullAccessibilityModeForTesting());
// The tree gets cleared when semantic updates are off.
EXPECT_EQ(semantics_manager_.semantic_tree()->tree_size(), 0u);
semantics_manager_.SetSemanticsModeEnabled(true);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(frame_impl_->web_contents_for_test()
->IsFullAccessibilityModeForTesting());
}
// This test performs several tree modifications (insertions, changes, and
// removals). All operations must leave the tree in a valid state and
// also forward the nodes in a way that leaves the tree in the Fuchsia side in a
// valid state. Note that every time that a new tree is sent to Fuchsia, the
// FakeSemantiTree checks if the tree is valid.
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest,
TreeModificationsAreForwarded) {
LoadPage(kPage1Path, kPage1Title);
auto* semantic_tree = semantics_manager_.semantic_tree();
semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage1NodeCount);
// Create a new HTML element.
{
const auto script = base::StringPrintf(
"var p = document.createElement(\"p\"); var text = "
"document.createTextNode(\"new_label\"); p.appendChild(text); "
"document.body.appendChild(p);");
frame_->ExecuteJavaScript(
{"*"}, base::MemBufferFromString(script, "add node"),
[](fuchsia::web::Frame_ExecuteJavaScript_Result result) {
CHECK(result.is_response());
});
semantic_tree->RunUntilNodeWithLabelIsInTree("new_label");
}
// Remove an HTML element.
{
// Verify that slider is present initially.
EXPECT_TRUE(semantic_tree->GetNodeFromRole(
fuchsia::accessibility::semantics::Role::SLIDER));
const auto script = base::StringPrintf(
"var slider = document.getElementById(\"myRange\"); slider.remove();");
frame_->ExecuteJavaScript(
{"*"}, base::MemBufferFromString(script, "reparent nodes"),
[](fuchsia::web::Frame_ExecuteJavaScript_Result result) {
CHECK(result.is_response());
});
semantic_tree->RunUntilConditionIsTrue(
base::BindLambdaForTesting([semantic_tree]() {
return !semantic_tree->GetNodeFromRole(
fuchsia::accessibility::semantics::Role::SLIDER);
}));
}
}
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, OutOfProcessIframe) {
constexpr int64_t kBindingsId = 1234;
// Start a different embedded test server, and load a page on it. The URL for
// this page will have a different port and be considered out of process when
// used as the src for an iframe.
net::EmbeddedTestServer second_test_server;
second_test_server.ServeFilesFromSourceDirectory(
base::FilePath(cr_fuchsia::kTestServerRoot));
ASSERT_TRUE(second_test_server.Start());
GURL out_of_process_url = second_test_server.GetURL(kPage1Path);
// Before loading a page on the default embedded test server, set the iframe
// src to be |out_of_process_url|.
frame_->AddBeforeLoadJavaScript(
kBindingsId, {"*"},
base::MemBufferFromString(
base::StringPrintf("iframeSrc = '%s'",
out_of_process_url.spec().c_str()),
"test"),
[](fuchsia::web::Frame_AddBeforeLoadJavaScript_Result result) {
CHECK(result.is_response());
});
LoadPage(kPageIframePath, "iframe loaded");
// Run until the title of the iframe page is in the semantic tree. Because
// the iframe's semantic tree is only sent when it is connected to the parent
// tree, it is guaranteed that both trees will be present.
semantics_manager_.semantic_tree()->RunUntilNodeWithLabelIsInTree(
kPage1Title);
// Two frames should be present.
int num_frames = CollectAllRenderFrameHosts(
frame_impl_->web_contents_for_test()->GetPrimaryPage())
.size();
EXPECT_EQ(num_frames, 2);
// Check that the iframe node has been loaded.
EXPECT_TRUE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kPageIframeTitle));
// Data that is part of the iframe should be in the semantic tree.
EXPECT_TRUE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kButtonName1));
// Makes the iframe navigate to a different page.
GURL out_of_process_url_2 = second_test_server.GetURL(kPage2Path);
const auto script =
base::StringPrintf("document.getElementById(\"iframeId\").src = '%s'",
out_of_process_url_2.spec().c_str());
frame_->ExecuteJavaScript(
{"*"}, base::MemBufferFromString(script, "test2"),
[](fuchsia::web::Frame_ExecuteJavaScript_Result result) {
CHECK(result.is_response());
});
semantics_manager_.semantic_tree()->RunUntilNodeWithLabelIsInTree(
kPage2Title);
// check that the iframe navigated to a different page.
EXPECT_TRUE(semantics_manager_.semantic_tree()->GetNodeFromLabel(kNodeName));
// Old iframe data should be gone.
EXPECT_FALSE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kButtonName1));
// Makes the main page navigate to a different page, causing the iframe to go
// away.
LoadPage(kPage2Path, kPage2Title);
// Wait for the root to be updated, which means that we navigated to a new
// page.
base::RunLoop run_loop;
semantics_manager_.semantic_tree()->SetNodeUpdatedCallback(
0u, run_loop.QuitClosure());
run_loop.Run();
// We've navigated to a different page that has no iframes. Only one frame
// should be present.
num_frames = CollectAllRenderFrameHosts(
frame_impl_->web_contents_for_test()->GetPrimaryPage())
.size();
EXPECT_EQ(num_frames, 1);
}
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, UpdatesFocusInformation) {
LoadPage(kPage1Path, kPage1Title);
auto* semantic_tree = semantics_manager_.semantic_tree();
semantic_tree->RunUntilNodeCountAtLeast(kPage1NodeCount);
// Get a node that is off the screen, and verify that it is off the screen.
fuchsia::accessibility::semantics::Node* fuchsia_node =
semantic_tree->GetNodeFromLabel(kButtonName1);
ASSERT_TRUE(fuchsia_node);
EXPECT_FALSE(fuchsia_node->states().has_input_focus());
// Get the corresponding AXPlatformNode.
auto* fuchsia_platform_node = static_cast<ui::AXPlatformNodeFuchsia*>(
ui::AXPlatformNodeBase::GetFromUniqueId(fuchsia_node->node_id()));
ASSERT_TRUE(fuchsia_platform_node);
// Focus the node.
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kFocus;
fuchsia_platform_node->PerformAction(action_data);
semantic_tree->RunUntilConditionIsTrue(
base::BindLambdaForTesting([semantic_tree, fuchsia_node]() {
auto* node = semantic_tree->GetNodeWithId(fuchsia_node->node_id());
if (!node)
return false;
return node->has_states() && node->states().has_has_input_focus() &&
node->states().has_input_focus();
}));
// Changes the focus to a different node and checks that the old value is
// cleared.
fuchsia::accessibility::semantics::Node* new_focus_node =
semantic_tree->GetNodeFromLabel(kButtonName2);
ASSERT_TRUE(new_focus_node);
// Get the corresponding AXPlatformNode.
auto* new_focus_platform_node = static_cast<ui::AXPlatformNodeFuchsia*>(
ui::AXPlatformNodeBase::GetFromUniqueId(new_focus_node->node_id()));
ASSERT_TRUE(new_focus_platform_node);
// Focus the new node. We can reuse the original action data.
new_focus_platform_node->PerformAction(action_data);
semantic_tree->RunUntilConditionIsTrue(base::BindLambdaForTesting(
[semantic_tree, new_focus_id = new_focus_node->node_id(),
old_focus_id = fuchsia_node->node_id()]() {
auto* old_focus = semantic_tree->GetNodeWithId(old_focus_id);
auto* node = semantic_tree->GetNodeWithId(new_focus_id);
if (!node || !old_focus)
return false;
// Node has the focus, root does not.
return (node->has_states() && node->states().has_has_input_focus() &&
node->states().has_input_focus()) &&
(old_focus->has_states() &&
old_focus->states().has_has_input_focus() &&
!old_focus->states().has_input_focus());
}));
}

@ -49,6 +49,21 @@ void FakeSemanticsManager::CheckNumActions() {
}
}
bool FakeSemanticsManager::RequestAccessibilityActionSync(
uint32_t node_id,
fuchsia::accessibility::semantics::Action action) {
base::RunLoop run_loop;
bool action_handled = false;
listener_->OnAccessibilityActionRequested(
node_id, action, [&action_handled, &run_loop](bool handled) {
action_handled = handled;
run_loop.QuitClosure().Run();
});
run_loop.Run();
return action_handled;
}
void FakeSemanticsManager::RequestAccessibilityAction(
uint32_t node_id,
fuchsia::accessibility::semantics::Action action) {

@ -43,11 +43,18 @@ class FakeSemanticsManager : public fuchsia::accessibility::semantics::testing::
// A helper function for RequestAccessibilityAction.
void CheckNumActions();
// TODO(crbug.com/1291330): Remove async RequestAccessibilityAction(), and
// replace with RequestAccessibilityActionSync().
// Request the client to perform |action| on the node with |node_id|.
void RequestAccessibilityAction(
uint32_t node_id,
fuchsia::accessibility::semantics::Action action);
// Request the client to perform |action| on the node with |node_id|.
bool RequestAccessibilityActionSync(
uint32_t node_id,
fuchsia::accessibility::semantics::Action action);
// Runs until |num_actions| accessibility actions have been handled.
void RunUntilNumActionsHandledEquals(int32_t num_actions);

@ -23,6 +23,7 @@
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "content/public/browser/browser_accessibility_state.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
@ -61,6 +62,7 @@
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "third_party/blink/public/mojom/loader/resource_load_info.mojom.h"
#include "third_party/blink/public/mojom/navigation/was_activated_option.mojom.h"
#include "ui/accessibility/platform/fuchsia/semantic_provider_impl.h"
#include "ui/aura/window.h"
#include "ui/compositor/compositor.h"
#include "ui/gfx/switches.h"
@ -73,9 +75,6 @@ namespace {
// Simulated screen bounds to use when headless rendering is enabled.
constexpr gfx::Size kHeadlessWindowSize = {1, 1};
// Simulated screen bounds to use when testing the SemanticsManager.
constexpr gfx::Size kSemanticsTestingWindowSize = {720, 640};
// Name of the Inspect node that holds accessibility information.
constexpr char kAccessibilityInspectNodeName[] = "accessibility";
@ -533,6 +532,7 @@ void FrameImpl::DestroyWindowTreeHost() {
window_tree_host_->compositor()->SetVisible(false);
window_tree_host_.reset();
accessibility_bridge_.reset();
v2_accessibility_bridge_.reset();
// Allows posted focus events to process before the FocusController is torn
// down.
@ -556,12 +556,17 @@ void FrameImpl::OnMediaPlayerDisconnect() {
media_player_ = nullptr;
}
void FrameImpl::OnAccessibilityError(zx_status_t error) {
bool FrameImpl::OnAccessibilityError(zx_status_t error) {
// The task is posted so |accessibility_bridge_| does not tear |this| down
// while events are still being processed.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&FrameImpl::CloseAndDestroyFrame,
weak_factory_.GetWeakPtr(), error));
// The return value indicates to the accessibility bridge whether we should
// attempt to reconnect. Since the frame has been destroyed, no reconnect
// attempt should be made.
return false;
}
bool FrameImpl::MaybeHandleCastStreamingMessage(
@ -617,22 +622,38 @@ void FrameImpl::UpdateRenderViewZoomLevel(
}
void FrameImpl::ConnectToAccessibilityBridge() {
fuchsia::accessibility::semantics::SemanticsManagerPtr semantics_manager;
if (!semantics_manager_for_test_) {
semantics_manager =
base::ComponentContextForProcess()
->svc()
->Connect<fuchsia::accessibility::semantics::SemanticsManager>();
}
if (use_v2_accessibility_bridge_) {
// TODO(crbug.com/1291613): Replace callbacks with an interface that
// FrameImpl implements.
v2_accessibility_bridge_ =
std::make_unique<ui::AccessibilityBridgeFuchsiaImpl>(
root_window(), window_tree_host_->CreateViewRef(),
base::BindRepeating(&FrameImpl::GetDeviceScaleFactor,
base::Unretained(this)),
base::BindRepeating(&FrameImpl::SetAccessibilityEnabled,
base::Unretained(this)),
base::BindRepeating(&FrameImpl::OnAccessibilityError,
base::Unretained(this)),
inspect_node_.CreateChild(kAccessibilityInspectNodeName));
} else {
fuchsia::accessibility::semantics::SemanticsManagerPtr semantics_manager;
if (!semantics_manager_for_test_) {
semantics_manager =
base::ComponentContextForProcess()
->svc()
->Connect<fuchsia::accessibility::semantics::SemanticsManager>();
}
// If the SemanticTree owned by |accessibility_bridge_| is disconnected, it
// will cause |this| to be closed.
accessibility_bridge_ = std::make_unique<AccessibilityBridge>(
semantics_manager_for_test_ ? semantics_manager_for_test_
: semantics_manager.get(),
window_tree_host_.get(), web_contents_.get(),
base::BindOnce(&FrameImpl::OnAccessibilityError, base::Unretained(this)),
inspect_node_.CreateChild(kAccessibilityInspectNodeName));
// If the SemanticTree owned by |accessibility_bridge_| is disconnected, it
// will cause |this| to be closed.
accessibility_bridge_ = std::make_unique<AccessibilityBridge>(
semantics_manager_for_test_ ? semantics_manager_for_test_
: semantics_manager.get(),
window_tree_host_.get(), web_contents_.get(),
base::BindOnce(&FrameImpl::OnAccessibilityError,
base::Unretained(this)),
inspect_node_.CreateChild(kAccessibilityInspectNodeName));
}
}
void FrameImpl::CreateView(fuchsia::ui::views::ViewToken view_token) {
@ -896,11 +917,10 @@ void FrameImpl::EnableHeadlessRendering() {
std::move(view_ref_pair));
gfx::Rect bounds(kHeadlessWindowSize);
if (semantics_manager_for_test_) {
ConnectToAccessibilityBridge();
// Set bounds for testing hit testing.
bounds.set_size(kSemanticsTestingWindowSize);
if (window_size_for_test_) {
ConnectToAccessibilityBridge();
bounds.set_size(*window_size_for_test_);
}
window_tree_host_->SetBoundsInPixels(bounds);
@ -1334,3 +1354,22 @@ void FrameImpl::ResourceLoadComplete(
void FrameImpl::EnableExplicitSitesFilter(std::string error_page) {
explicit_sites_filter_error_page_ = std::move(error_page);
}
float FrameImpl::GetDeviceScaleFactor() {
if (device_scale_factor_for_test_)
return *device_scale_factor_for_test_;
return window_tree_host_->scenic_scale_factor();
}
void FrameImpl::SetAccessibilityEnabled(bool enabled) {
auto* browser_accessibility_state =
content::BrowserAccessibilityState::GetInstance();
if (enabled) {
browser_accessibility_state->AddAccessibilityModeFlags(ui::kAXModeComplete);
} else {
browser_accessibility_state->RemoveAccessibilityModeFlags(
ui::kAXModeComplete);
}
}

@ -36,6 +36,7 @@
#include "fuchsia/engine/browser/theme_manager.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "ui/accessibility/platform/fuchsia/accessibility_bridge_fuchsia_impl.h"
#include "ui/aura/window_tree_host.h"
#include "ui/wm/core/focus_controller.h"
#include "url/gurl.h"
@ -115,6 +116,15 @@ class FrameImpl : public fuchsia::web::Frame,
return web_contents_.get();
}
bool has_view_for_test() const { return window_tree_host_ != nullptr; }
FrameWindowTreeHost* window_tree_host_for_test() {
return window_tree_host_.get();
}
// Accessibility bridge accessor/setter methods.
// TODO(crbug.com/1291330): Remove the three methods below.
void set_use_v2_accessibility_bridge(bool use_v2_accessibility_bridge) {
use_v2_accessibility_bridge_ = use_v2_accessibility_bridge;
}
AccessibilityBridge* accessibility_bridge_for_test() const {
return accessibility_bridge_.get();
}
@ -122,9 +132,6 @@ class FrameImpl : public fuchsia::web::Frame,
fuchsia::accessibility::semantics::SemanticsManager* semantics_manager) {
semantics_manager_for_test_ = semantics_manager;
}
FrameWindowTreeHost* window_tree_host_for_test() {
return window_tree_host_.get();
}
// Override |blink_prefs| with settings defined in |content_settings_|.
//
@ -132,6 +139,14 @@ class FrameImpl : public fuchsia::web::Frame,
// recomputed.
void OverrideWebPreferences(blink::web_pref::WebPreferences* web_prefs);
void set_window_size_for_test(gfx::Size size) {
window_size_for_test_ = size;
}
void set_device_scale_factor_for_test(float device_scale_factor) {
device_scale_factor_for_test_ = device_scale_factor;
}
private:
FRIEND_TEST_ALL_PREFIXES(FrameImplTest, DelayedNavigationEventAck);
FRIEND_TEST_ALL_PREFIXES(FrameImplTest, NavigationObserverDisconnected);
@ -169,7 +184,7 @@ class FrameImpl : public fuchsia::web::Frame,
void OnMediaPlayerDisconnect();
// An error handler for |accessibility_bridge_|.
void OnAccessibilityError(zx_status_t error);
bool OnAccessibilityError(zx_status_t error);
// Creates and initializes WindowTreeHost for the view with the specified
// |view_token|. |view_token| may be uninitialized in headless mode.
@ -330,6 +345,9 @@ class FrameImpl : public fuchsia::web::Frame,
const content::GlobalRequestID& request_id,
const blink::mojom::ResourceLoadInfo& resource_load_info) override;
float GetDeviceScaleFactor();
void SetAccessibilityEnabled(bool enabled);
const std::unique_ptr<content::WebContents> web_contents_;
ContextImpl* const context_;
@ -350,9 +368,16 @@ class FrameImpl : public fuchsia::web::Frame,
// Owned via |window_tree_host_|.
FrameLayoutManager* layout_manager_ = nullptr;
// TODO(crbug.com/1291330): Remove acessibility_bridge_ and
// semantics_manager_for_test_.
std::unique_ptr<AccessibilityBridge> accessibility_bridge_;
fuchsia::accessibility::semantics::SemanticsManager*
semantics_manager_for_test_ = nullptr;
std::unique_ptr<ui::AccessibilityBridgeFuchsiaImpl> v2_accessibility_bridge_;
// Test settings.
absl::optional<gfx::Size> window_size_for_test_;
absl::optional<float> device_scale_factor_for_test_;
EventFilter event_filter_;
NavigationControllerImpl navigation_controller_;
@ -391,6 +416,10 @@ class FrameImpl : public fuchsia::web::Frame,
inspect::Node inspect_node_;
const inspect::StringProperty inspect_name_property_;
// TODO(crbug.com/1291330): Remove.
// Used to control which accessibility bridge version is live.
bool use_v2_accessibility_bridge_ = false;
base::WeakPtrFactory<FrameImpl> weak_factory_{this};
};

@ -122,11 +122,6 @@ IN_PROC_BROWSER_TEST_F(FrameImplTest, MAYBE_VisibilityState) {
auto frame = cr_fuchsia::FrameForTest::Create(context(), {});
base::RunLoop().RunUntilIdle();
FrameImpl* frame_impl = context_impl()->GetFrameImplForTest(&frame.ptr());
// CreateView() will cause the AccessibilityBridge to be created.
FakeSemanticsManager fake_semantics_manager;
frame_impl->set_semantics_manager_for_test(&fake_semantics_manager);
// Navigate to a page and wait for it to finish loading.
ASSERT_TRUE(cr_fuchsia::LoadUrlAndExpectResponse(
@ -308,10 +303,6 @@ IN_PROC_BROWSER_TEST_F(FrameImplTest, ContextDeletedBeforeFrameWithView) {
base::RunLoop().RunUntilIdle();
FrameImpl* frame_impl = context_impl()->GetFrameImplForTest(&frame.ptr());
// CreateView() will cause the AccessibilityBridge to be created.
FakeSemanticsManager fake_semantics_manager;
frame_impl->set_semantics_manager_for_test(&fake_semantics_manager);
auto view_tokens = scenic::ViewTokenPair::New();
frame->CreateView(std::move(view_tokens.view_token));
base::RunLoop().RunUntilIdle();
@ -1048,10 +1039,6 @@ IN_PROC_BROWSER_TEST_F(FrameImplTest, RecreateView) {
ASSERT_TRUE(frame_impl);
EXPECT_FALSE(frame_impl->has_view_for_test());
// CreateView() will cause the AccessibilityBridge to be created.
FakeSemanticsManager fake_semantics_manager;
frame_impl->set_semantics_manager_for_test(&fake_semantics_manager);
// Verify that the Frame can navigate, prior to the View being created.
const GURL page1_url(embedded_test_server()->GetURL(kPage1Path));
EXPECT_TRUE(cr_fuchsia::LoadUrlAndExpectResponse(

@ -53,7 +53,7 @@ AccessibilityBridgeFuchsiaImpl::AccessibilityBridgeFuchsiaImpl(
fuchsia::ui::views::ViewRef view_ref,
base::RepeatingCallback<float()> get_pixel_scale,
base::RepeatingCallback<void(bool)> on_semantics_enabled,
base::RepeatingCallback<bool()> on_connection_closed,
OnConnectionClosedCallback on_connection_closed,
inspect::Node inspect_node)
: root_window_(window),
on_semantics_enabled_(std::move(on_semantics_enabled)),
@ -232,9 +232,10 @@ void AccessibilityBridgeFuchsiaImpl::OnSemanticsEnabled(bool enabled) {
on_semantics_enabled_.Run(enabled);
}
bool AccessibilityBridgeFuchsiaImpl::OnSemanticsManagerConnectionClosed() {
bool AccessibilityBridgeFuchsiaImpl::OnSemanticsManagerConnectionClosed(
zx_status_t status) {
if (on_connection_closed_)
return on_connection_closed_.Run();
return on_connection_closed_.Run(status);
// If the user does not specify a callback, then we can assume no attempt to
// reconnect should be made.

@ -23,6 +23,8 @@ class AX_EXPORT AccessibilityBridgeFuchsiaImpl final
: public ui::AccessibilityBridgeFuchsia,
public ui::AXFuchsiaSemanticProvider::Delegate {
public:
using OnConnectionClosedCallback = base::RepeatingCallback<bool(zx_status_t)>;
// Constructor args:
//
// |root_window|: Refers to the root aura::Window for which this accessibility
@ -52,7 +54,7 @@ class AX_EXPORT AccessibilityBridgeFuchsiaImpl final
fuchsia::ui::views::ViewRef view_ref,
base::RepeatingCallback<float()> get_pixel_scale,
base::RepeatingCallback<void(bool)> on_semantics_enabled,
base::RepeatingCallback<bool()> on_connection_closed,
OnConnectionClosedCallback on_connection_closed,
inspect::Node inspect_node);
~AccessibilityBridgeFuchsiaImpl() override;
@ -66,7 +68,7 @@ class AX_EXPORT AccessibilityBridgeFuchsiaImpl final
inspect::Node GetInspectNode() override;
// SemanticProvider::Delegate overrides.
bool OnSemanticsManagerConnectionClosed() override;
bool OnSemanticsManagerConnectionClosed(zx_status_t status) override;
bool OnAccessibilityAction(
uint32_t node_id,
fuchsia::accessibility::semantics::Action action) override;
@ -111,7 +113,10 @@ class AX_EXPORT AccessibilityBridgeFuchsiaImpl final
base::RepeatingCallback<void(bool)> on_semantics_enabled_;
// Callback invoked whenever the semantics manager connection is closed.
base::RepeatingCallback<bool()> on_connection_closed_;
// We use a base::RepeatingCallback, because we may attempt to reconnect, in
// which case it's possible that we may need to invoke the callback more than
// once.
OnConnectionClosedCallback on_connection_closed_;
// The inspect output will have a node for each AXTree in this accessibility
// bridge's window. Inspect node names are static, but AXTreeIDs can change.

@ -101,7 +101,7 @@ class AccessibilityBridgeFuchsiaTest : public ::testing::Test {
/*root_window=*/nullptr, std::move(view_ref_pair.view_ref),
base::BindRepeating([]() { return 1.0f; }),
base::RepeatingCallback<void(bool)>(),
base::RepeatingCallback<bool()>(), inspect::Node());
base::RepeatingCallback<bool(zx_status_t)>(), inspect::Node());
accessibility_bridge_->set_semantic_provider_for_test(
std::move(mock_semantic_provider));
}

@ -34,7 +34,7 @@ class AX_EXPORT AXFuchsiaSemanticProvider {
// Called when the FIDL channel to the Semantics Manager is closed. If this
// callback returns true, an attempt to reconnect will be made.
virtual bool OnSemanticsManagerConnectionClosed() = 0;
virtual bool OnSemanticsManagerConnectionClosed(zx_status_t status) = 0;
// Processes an incoming accessibility action from Fuchsia. It
// receives the Fuchsia node ID and the action requested. If this

@ -78,7 +78,7 @@ AXFuchsiaSemanticProviderImpl::AXFuchsiaSemanticProviderImpl(
semantic_tree_.NewRequest());
semantic_tree_.set_error_handler([this](zx_status_t status) {
ZX_LOG(ERROR, status) << "SemanticTree disconnected";
delegate_->OnSemanticsManagerConnectionClosed();
delegate_->OnSemanticsManagerConnectionClosed(status);
semantic_updates_enabled_ = false;
});
}

@ -31,7 +31,7 @@ class AXFuchsiaSemanticProviderDelegate
AXFuchsiaSemanticProviderDelegate() = default;
~AXFuchsiaSemanticProviderDelegate() override = default;
bool OnSemanticsManagerConnectionClosed() override {
bool OnSemanticsManagerConnectionClosed(zx_status_t status) override {
on_semantics_manager_connection_closed_called_ = true;
return true;
}