#include <fuchsia/accessibility/semantics/cpp/fidl.h>
#include <zircon/types.h>
#include <string_view>
#include "base/command_line.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/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "content/public/browser/browser_accessibility_state.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "fuchsia_web/common/test/frame_for_test.h"
#include "fuchsia_web/common/test/frame_test_util.h"
#include "fuchsia_web/common/test/test_navigation_listener.h"
#include "fuchsia_web/webengine/browser/context_impl.h"
#include "fuchsia_web/webengine/browser/fake_semantics_manager.h"
#include "fuchsia_web/webengine/browser/frame_impl.h"
#include "fuchsia_web/webengine/test/test_data.h"
#include "fuchsia_web/webengine/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;
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;
}
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;
}
}
class FuchsiaFrameAccessibilityTest : public WebEngineBrowserTest {
public:
FuchsiaFrameAccessibilityTest() {
WebEngineBrowserTest::set_test_server_root(base::FilePath(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);
WebEngineBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
content::BrowserAccessibilityState::GetInstance()
->SetActivationFromPlatformEnabled(
true);
test_context_.emplace(
base::TestComponentContextForProcess::InitialState::kCloneAll);
WebEngineBrowserTest::SetUpOnMainThread();
test_context_->additional_services()
->RemovePublicService<
fuchsia::accessibility::semantics::SemanticsManager>();
semantics_manager_binding_.emplace(test_context_->additional_services(),
&semantics_manager_);
frame_ = FrameForTest::Create(context(), {});
base::RunLoop().RunUntilIdle();
frame_impl_ = context_impl()->GetFrameImplForTest(&frame_.ptr());
frame_impl_->set_window_size_for_test(kTestWindowSize);
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());
ASSERT_TRUE(frame_impl_->web_contents_for_test()
->GetAccessibilityMode()
.is_mode_off());
semantics_manager_.SetSemanticsModeEnabled(true);
base::RunLoop().RunUntilIdle();
ASSERT_EQ(frame_impl_->web_contents_for_test()->GetAccessibilityMode(),
ui::kAXModeComplete | ui::AXMode::kScreenReader);
}
void TearDownOnMainThread() override {
frame_ = {};
WebEngineBrowserTest::TearDownOnMainThread();
}
void LoadPage(std::string_view url, std::string_view page_title) {
GURL page_url(embedded_test_server()->GetURL(std::string(url)));
ASSERT_TRUE(LoadUrlAndExpectResponse(frame_.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
page_url.spec()));
frame_.navigation_listener().RunUntilUrlAndTitleEquals(page_url,
page_title);
}
protected:
std::optional<base::TestComponentContextForProcess> test_context_;
FrameForTest frame_;
raw_ptr<FrameImpl> frame_impl_;
FakeSemanticsManager semantics_manager_;
std::optional<base::ScopedServiceBinding<
fuchsia::accessibility::semantics::SemanticsManager>>
semantics_manager_binding_;
};
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, CorrectDataSent) {
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));
}
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, DataSentWithBatching) {
LoadPage(kPage2Path, kPage2Title);
semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage2NodeCount);
semantics_manager_.semantic_tree()->RunUntilNodeWithLabelIsInTree(kNodeName);
EXPECT_GE(semantics_manager_.semantic_tree()->num_update_calls(), 18u);
EXPECT_GE(semantics_manager_.semantic_tree()->num_commit_calls(), 1u);
}
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));
semantics_manager_.semantic_tree()->RunUntilNodeWithLabelIsInTree(kNodeName);
EXPECT_FALSE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kButtonName1));
EXPECT_FALSE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kParagraphName));
}
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;
frame_impl_->OnPixelScaleUpdate(scale_factor);
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);
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);
EXPECT_FALSE(semantics_manager_.RequestAccessibilityActionSync(
button2->node_id(),
fuchsia::accessibility::semantics::Action::SECONDARY));
}
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, DISABLED_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) {
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);
fuchsia::accessibility::semantics::Node* fuchsia_node =
semantic_tree->GetNodeFromLabel(kOffscreenNodeName);
ASSERT_TRUE(fuchsia_node);
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);
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;
return root->has_states() && root->states().has_viewport_offset() &&
root->states().viewport_offset().y > 0;
}));
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);
}
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest, TogglesSemanticsUpdates) {
LoadPage(kPage1Path, kPage1Title);
semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage1NodeCount);
semantics_manager_.SetSemanticsModeEnabled(false);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(frame_impl_->web_contents_for_test()
->GetAccessibilityMode()
.is_mode_off());
EXPECT_EQ(semantics_manager_.semantic_tree()->tree_size(), 0u);
semantics_manager_.SetSemanticsModeEnabled(true);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(frame_impl_->web_contents_for_test()->GetAccessibilityMode(),
ui::kAXModeComplete | ui::AXMode::kScreenReader);
}
IN_PROC_BROWSER_TEST_F(FuchsiaFrameAccessibilityTest,
TreeModificationsAreForwarded) {
LoadPage(kPage1Path, kPage1Title);
auto* semantic_tree = semantics_manager_.semantic_tree();
semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage1NodeCount);
{
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) {
EXPECT_TRUE(result.is_response());
});
semantic_tree->RunUntilNodeWithLabelIsInTree("new_label");
}
{
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) {
EXPECT_TRUE(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;
net::EmbeddedTestServer second_test_server;
second_test_server.ServeFilesFromSourceDirectory(
base::FilePath(kTestServerRoot));
ASSERT_TRUE(second_test_server.Start());
GURL out_of_process_url = second_test_server.GetURL(kPage1Path);
frame_->AddBeforeLoadJavaScript(
kBindingsId, {"*"},
base::MemBufferFromString(
base::StringPrintf("iframeSrc = '%s'",
out_of_process_url.spec().c_str()),
"test"),
[](fuchsia::web::Frame_AddBeforeLoadJavaScript_Result result) {
EXPECT_TRUE(result.is_response());
});
LoadPage(kPageIframePath, "iframe loaded");
semantics_manager_.semantic_tree()->RunUntilNodeWithLabelIsInTree(
kPage1Title);
int num_frames = CollectAllRenderFrameHosts(
frame_impl_->web_contents_for_test()->GetPrimaryPage())
.size();
EXPECT_EQ(num_frames, 2);
EXPECT_TRUE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kPageIframeTitle));
EXPECT_TRUE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kButtonName1));
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) {
EXPECT_TRUE(result.is_response());
});
semantics_manager_.semantic_tree()->RunUntilNodeWithLabelIsInTree(
kPage2Title);
semantics_manager_.semantic_tree()->RunUntilNodeWithLabelIsInTree(kNodeName);
EXPECT_FALSE(
semantics_manager_.semantic_tree()->GetNodeFromLabel(kButtonName1));
LoadPage(kPage2Path, kPage2Title);
base::RunLoop run_loop;
semantics_manager_.semantic_tree()->SetNodeUpdatedCallback(
0u, run_loop.QuitClosure());
run_loop.Run();
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);
fuchsia::accessibility::semantics::Node* fuchsia_node =
semantic_tree->GetNodeFromLabel(kButtonName1);
ASSERT_TRUE(fuchsia_node);
EXPECT_FALSE(fuchsia_node->states().has_input_focus());
auto* fuchsia_platform_node = static_cast<ui::AXPlatformNodeFuchsia*>(
ui::AXPlatformNodeBase::GetFromUniqueId(fuchsia_node->node_id()));
ASSERT_TRUE(fuchsia_platform_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();
}));
fuchsia::accessibility::semantics::Node* new_focus_node =
semantic_tree->GetNodeFromLabel(kButtonName2);
ASSERT_TRUE(new_focus_node);
auto* new_focus_platform_node = static_cast<ui::AXPlatformNodeFuchsia*>(
ui::AXPlatformNodeBase::GetFromUniqueId(new_focus_node->node_id()));
ASSERT_TRUE(new_focus_platform_node);
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;
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());
}));
}