blob: 78a4a528f522c5a6d1f12596083a990b17eaa7a7 [file] [log] [blame]
// Copyright 2013 The Flutter 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 "flutter/shell/platform/windows/accessibility_bridge_delegate_win32.h"
#include <comdef.h>
#include <comutil.h>
#include <oleacc.h>
#include <vector>
#include "flutter/shell/platform/embedder/embedder.h"
#include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
#include "flutter/shell/platform/windows/flutter_platform_node_delegate_win32.h"
#include "flutter/shell/platform/windows/flutter_windows_engine.h"
#include "flutter/shell/platform/windows/flutter_windows_view.h"
#include "flutter/shell/platform/windows/testing/engine_modifier.h"
#include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h"
#include "flutter/shell/platform/windows/testing/test_keyboard.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
namespace flutter {
namespace testing {
namespace {
// A structure representing a Win32 MSAA event targeting a specified node.
struct MsaaEvent {
std::shared_ptr<FlutterPlatformNodeDelegateWin32> node_delegate;
DWORD event_type;
};
// Accessibility bridge delegate that captures events dispatched to the OS.
class AccessibilityBridgeDelegateWin32Spy
: public AccessibilityBridgeDelegateWin32 {
public:
explicit AccessibilityBridgeDelegateWin32Spy(FlutterWindowsEngine* engine)
: AccessibilityBridgeDelegateWin32(engine) {}
void DispatchWinAccessibilityEvent(
std::shared_ptr<FlutterPlatformNodeDelegateWin32> node_delegate,
DWORD event_type) override {
dispatched_events_.push_back({node_delegate, event_type});
}
void SetFocus(std::shared_ptr<FlutterPlatformNodeDelegateWin32> node_delegate)
override {
focused_nodes_.push_back(node_delegate->GetAXNode()->id());
}
void Reset() {
dispatched_events_.clear();
focused_nodes_.clear();
}
const std::vector<MsaaEvent>& dispatched_events() const {
return dispatched_events_;
}
const std::vector<int32_t> focused_nodes() const { return focused_nodes_; }
private:
std::vector<MsaaEvent> dispatched_events_;
std::vector<int32_t> focused_nodes_;
};
// Returns an engine instance configured with dummy project path values, and
// overridden methods for sending platform messages, so that the engine can
// respond as if the framework were connected.
std::unique_ptr<FlutterWindowsEngine> GetTestEngine() {
FlutterDesktopEngineProperties properties = {};
properties.assets_path = L"C:\\foo\\flutter_assets";
properties.icu_data_path = L"C:\\foo\\icudtl.dat";
properties.aot_library_path = L"C:\\foo\\aot.so";
FlutterProjectBundle project(properties);
auto engine = std::make_unique<FlutterWindowsEngine>(project);
EngineModifier modifier(engine.get());
modifier.embedder_api().UpdateSemanticsEnabled =
[](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) {
return kSuccess;
};
MockEmbedderApiForKeyboard(
modifier, [] { return false; },
[](const FlutterKeyEvent* event) { return false; });
engine->RunWithEntrypoint(nullptr);
return engine;
}
// Populates the AXTree associated with the specified bridge with test data.
//
// node0
// / \
// node1 node2
// / \
// node3 node4
//
// node0 and node2 are grouping nodes. node1 and node2 are static text nodes.
// node4 is a static text node with no text, and hence has the "ignored" state.
void PopulateAXTree(std::shared_ptr<AccessibilityBridge> bridge) {
// Add node 0: root.
FlutterSemanticsNode node0{sizeof(FlutterSemanticsNode), 0};
std::vector<int32_t> node0_children{1, 2};
node0.child_count = node0_children.size();
node0.children_in_traversal_order = node0_children.data();
node0.children_in_hit_test_order = node0_children.data();
// Add node 1: text child of node 0.
FlutterSemanticsNode node1{sizeof(FlutterSemanticsNode), 1};
node1.label = "prefecture";
node1.value = "Kyoto";
// Add node 2: subtree child of node 0.
FlutterSemanticsNode node2{sizeof(FlutterSemanticsNode), 2};
std::vector<int32_t> node2_children{3, 4};
node2.child_count = node2_children.size();
node2.children_in_traversal_order = node2_children.data();
node2.children_in_hit_test_order = node2_children.data();
// Add node 3: text child of node 2.
FlutterSemanticsNode node3{sizeof(FlutterSemanticsNode), 3};
node3.label = "city";
node3.value = "Uji";
// Add node 4: text child (with no text) of node 2.
FlutterSemanticsNode node4{sizeof(FlutterSemanticsNode), 4};
bridge->AddFlutterSemanticsNodeUpdate(&node0);
bridge->AddFlutterSemanticsNodeUpdate(&node1);
bridge->AddFlutterSemanticsNodeUpdate(&node2);
bridge->AddFlutterSemanticsNodeUpdate(&node3);
bridge->AddFlutterSemanticsNodeUpdate(&node4);
bridge->CommitUpdates();
}
ui::AXNode* AXNodeFromID(std::shared_ptr<AccessibilityBridge> bridge,
int32_t id) {
auto node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(id).lock();
return node_delegate ? node_delegate->GetAXNode() : nullptr;
}
void ExpectWinEventFromAXEvent(int32_t node_id,
ui::AXEventGenerator::Event ax_event,
DWORD expected_event) {
auto window_binding_handler =
std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>();
FlutterWindowsView view(std::move(window_binding_handler));
view.SetEngine(GetTestEngine());
view.OnUpdateSemanticsEnabled(true);
auto bridge = view.GetEngine()->accessibility_bridge().lock();
PopulateAXTree(bridge);
AccessibilityBridgeDelegateWin32Spy spy(view.GetEngine());
spy.OnAccessibilityEvent({AXNodeFromID(bridge, node_id),
{ax_event, ax::mojom::EventFrom::kNone, {}}});
ASSERT_EQ(spy.dispatched_events().size(), 1);
EXPECT_EQ(spy.dispatched_events()[0].event_type, expected_event);
}
} // namespace
TEST(AccessibilityBridgeDelegateWin32, GetParent) {
auto window_binding_handler =
std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>();
FlutterWindowsView view(std::move(window_binding_handler));
view.SetEngine(GetTestEngine());
view.OnUpdateSemanticsEnabled(true);
auto bridge = view.GetEngine()->accessibility_bridge().lock();
PopulateAXTree(bridge);
auto node0_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
auto node1_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
EXPECT_EQ(node0_delegate->GetNativeViewAccessible(),
node1_delegate->GetParent());
}
TEST(AccessibilityBridgeDelegateWin32, GetParentOnRootRetunsNullptr) {
auto window_binding_handler =
std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>();
FlutterWindowsView view(std::move(window_binding_handler));
view.SetEngine(GetTestEngine());
view.OnUpdateSemanticsEnabled(true);
auto bridge = view.GetEngine()->accessibility_bridge().lock();
PopulateAXTree(bridge);
auto node0_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
ASSERT_TRUE(node0_delegate->GetParent() == nullptr);
}
TEST(AccessibilityBridgeDelegateWin32, DispatchAccessibilityAction) {
auto window_binding_handler =
std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>();
FlutterWindowsView view(std::move(window_binding_handler));
view.SetEngine(GetTestEngine());
view.OnUpdateSemanticsEnabled(true);
auto bridge = view.GetEngine()->accessibility_bridge().lock();
PopulateAXTree(bridge);
FlutterSemanticsAction actual_action = kFlutterSemanticsActionTap;
EngineModifier modifier(view.GetEngine());
modifier.embedder_api().DispatchSemanticsAction = MOCK_ENGINE_PROC(
DispatchSemanticsAction,
([&actual_action](FLUTTER_API_SYMBOL(FlutterEngine) engine, uint64_t id,
FlutterSemanticsAction action, const uint8_t* data,
size_t data_length) {
actual_action = action;
return kSuccess;
}));
AccessibilityBridgeDelegateWin32 delegate(view.GetEngine());
delegate.DispatchAccessibilityAction(1, kFlutterSemanticsActionCopy, {});
EXPECT_EQ(actual_action, kFlutterSemanticsActionCopy);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilityEventAlert) {
ExpectWinEventFromAXEvent(0, ui::AXEventGenerator::Event::ALERT,
EVENT_SYSTEM_ALERT);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilityEventChildrenChanged) {
ExpectWinEventFromAXEvent(0, ui::AXEventGenerator::Event::CHILDREN_CHANGED,
EVENT_OBJECT_REORDER);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilityEventFocusChanged) {
auto window_binding_handler =
std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>();
FlutterWindowsView view(std::move(window_binding_handler));
view.SetEngine(GetTestEngine());
view.OnUpdateSemanticsEnabled(true);
auto bridge = view.GetEngine()->accessibility_bridge().lock();
PopulateAXTree(bridge);
AccessibilityBridgeDelegateWin32Spy spy(view.GetEngine());
spy.OnAccessibilityEvent({AXNodeFromID(bridge, 1),
{ui::AXEventGenerator::Event::FOCUS_CHANGED,
ax::mojom::EventFrom::kNone,
{}}});
ASSERT_EQ(spy.dispatched_events().size(), 1);
EXPECT_EQ(spy.dispatched_events()[0].event_type, EVENT_OBJECT_FOCUS);
ASSERT_EQ(spy.focused_nodes().size(), 1);
EXPECT_EQ(spy.focused_nodes()[0], 1);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilityEventIgnoredChanged) {
// Static test nodes with no text, hint, or scrollability are ignored.
ExpectWinEventFromAXEvent(4, ui::AXEventGenerator::Event::IGNORED_CHANGED,
EVENT_OBJECT_HIDE);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilityImageAnnotationChanged) {
ExpectWinEventFromAXEvent(
1, ui::AXEventGenerator::Event::IMAGE_ANNOTATION_CHANGED,
EVENT_OBJECT_NAMECHANGE);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilityLiveRegionChanged) {
ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::LIVE_REGION_CHANGED,
EVENT_OBJECT_LIVEREGIONCHANGED);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilityNameChanged) {
ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::NAME_CHANGED,
EVENT_OBJECT_NAMECHANGE);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilityHScrollPosChanged) {
ExpectWinEventFromAXEvent(
1, ui::AXEventGenerator::Event::SCROLL_HORIZONTAL_POSITION_CHANGED,
EVENT_SYSTEM_SCROLLINGEND);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilityVScrollPosChanged) {
ExpectWinEventFromAXEvent(
1, ui::AXEventGenerator::Event::SCROLL_VERTICAL_POSITION_CHANGED,
EVENT_SYSTEM_SCROLLINGEND);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilitySelectedChanged) {
ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::SELECTED_CHANGED,
EVENT_OBJECT_VALUECHANGE);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilitySelectedChildrenChanged) {
ExpectWinEventFromAXEvent(
2, ui::AXEventGenerator::Event::SELECTED_CHILDREN_CHANGED,
EVENT_OBJECT_SELECTIONWITHIN);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilitySubtreeCreated) {
ExpectWinEventFromAXEvent(0, ui::AXEventGenerator::Event::SUBTREE_CREATED,
EVENT_OBJECT_SHOW);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilityValueChanged) {
ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::VALUE_CHANGED,
EVENT_OBJECT_VALUECHANGE);
}
TEST(AccessibilityBridgeDelegateWin32, OnAccessibilityStateChanged) {
ExpectWinEventFromAXEvent(
1, ui::AXEventGenerator::Event::WIN_IACCESSIBLE_STATE_CHANGED,
EVENT_OBJECT_STATECHANGE);
}
} // namespace testing
} // namespace flutter