Flutter Windows Embedder
accessibility_bridge_windows_unittests.cc
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
6 
7 #include <comdef.h>
8 #include <comutil.h>
9 #include <oleacc.h>
10 
11 #include <vector>
12 
13 #include "flutter/fml/macros.h"
14 #include "flutter/shell/platform/embedder/embedder.h"
15 #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
19 #include "flutter/shell/platform/windows/testing/engine_modifier.h"
20 #include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h"
21 #include "flutter/shell/platform/windows/testing/test_keyboard.h"
22 #include "gmock/gmock.h"
23 #include "gtest/gtest.h"
24 
25 namespace flutter {
26 namespace testing {
27 
28 namespace {
29 using ::testing::NiceMock;
30 
31 // A structure representing a Win32 MSAA event targeting a specified node.
32 struct MsaaEvent {
33  std::shared_ptr<FlutterPlatformNodeDelegateWindows> node_delegate;
34  ax::mojom::Event event_type;
35 };
36 
37 // Accessibility bridge delegate that captures events dispatched to the OS.
38 class AccessibilityBridgeWindowsSpy : public AccessibilityBridgeWindows {
39  public:
41 
42  explicit AccessibilityBridgeWindowsSpy(FlutterWindowsEngine* engine,
43  FlutterWindowsView* view)
44  : AccessibilityBridgeWindows(view) {}
45 
46  void DispatchWinAccessibilityEvent(
47  std::shared_ptr<FlutterPlatformNodeDelegateWindows> node_delegate,
48  ax::mojom::Event event_type) override {
49  dispatched_events_.push_back({node_delegate, event_type});
50  }
51 
52  void SetFocus(std::shared_ptr<FlutterPlatformNodeDelegateWindows>
53  node_delegate) override {
54  focused_nodes_.push_back(std::move(node_delegate));
55  }
56 
57  void ResetRecords() {
58  dispatched_events_.clear();
59  focused_nodes_.clear();
60  }
61 
62  const std::vector<MsaaEvent>& dispatched_events() const {
63  return dispatched_events_;
64  }
65 
66  const std::vector<int32_t> focused_nodes() const {
67  std::vector<int32_t> ids;
68  std::transform(focused_nodes_.begin(), focused_nodes_.end(),
69  std::back_inserter(ids),
70  [](std::shared_ptr<FlutterPlatformNodeDelegate> node) {
71  return node->GetAXNode()->id();
72  });
73  return ids;
74  }
75 
76  protected:
77  std::weak_ptr<FlutterPlatformNodeDelegate> GetFocusedNode() override {
78  return focused_nodes_.back();
79  }
80 
81  private:
82  std::vector<MsaaEvent> dispatched_events_;
83  std::vector<std::shared_ptr<FlutterPlatformNodeDelegate>> focused_nodes_;
84 
85  FML_DISALLOW_COPY_AND_ASSIGN(AccessibilityBridgeWindowsSpy);
86 };
87 
88 // A FlutterWindowsView whose accessibility bridge is an
89 // AccessibilityBridgeWindowsSpy.
90 class FlutterWindowsViewSpy : public FlutterWindowsView {
91  public:
92  FlutterWindowsViewSpy(FlutterWindowsEngine* engine,
93  std::unique_ptr<WindowBindingHandler> handler)
94  : FlutterWindowsView(kImplicitViewId,
95  engine,
96  std::move(handler),
97  false,
98  BoxConstraints()) {}
99 
100  protected:
101  virtual std::shared_ptr<AccessibilityBridgeWindows>
102  CreateAccessibilityBridge() override {
103  return std::make_shared<AccessibilityBridgeWindowsSpy>(GetEngine(), this);
104  }
105 
106  private:
107  FML_DISALLOW_COPY_AND_ASSIGN(FlutterWindowsViewSpy);
108 };
109 
110 // Returns an engine instance configured with dummy project path values, and
111 // overridden methods for sending platform messages, so that the engine can
112 // respond as if the framework were connected.
113 std::unique_ptr<FlutterWindowsEngine> GetTestEngine() {
114  FlutterDesktopEngineProperties properties = {};
115  properties.assets_path = L"C:\\foo\\flutter_assets";
116  properties.icu_data_path = L"C:\\foo\\icudtl.dat";
117  properties.aot_library_path = L"C:\\foo\\aot.so";
118  properties.impeller_switch = DefaultImpeller;
119  FlutterProjectBundle project(properties);
120  auto engine = std::make_unique<FlutterWindowsEngine>(project);
121 
122  EngineModifier modifier(engine.get());
123  modifier.embedder_api().UpdateSemanticsEnabled =
124  [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) {
125  return kSuccess;
126  };
127 
128  MockEmbedderApiForKeyboard(modifier,
129  std::make_shared<MockKeyResponseController>());
130 
131  engine->Run();
132  return engine;
133 }
134 
135 // Populates the AXTree associated with the specified bridge with test data.
136 //
137 // node0
138 // / \
139 // node1 node2
140 // / \
141 // node3 node4
142 //
143 // node0 and node2 are grouping nodes. node1 and node2 are static text nodes.
144 // node4 is a static text node with no text, and hence has the "ignored" state.
145 void PopulateAXTree(std::shared_ptr<AccessibilityBridge> bridge) {
146  // Add node 0: root.
147  FlutterSemanticsNode2 node0{sizeof(FlutterSemanticsNode2), 0};
148  auto empty_flags = FlutterSemanticsFlags{};
149  std::vector<int32_t> node0_children{1, 2};
150  node0.child_count = node0_children.size();
151  node0.children_in_traversal_order = node0_children.data();
152  node0.children_in_hit_test_order = node0_children.data();
153  node0.flags2 = &empty_flags;
154 
155  // Add node 1: text child of node 0.
156  FlutterSemanticsNode2 node1{sizeof(FlutterSemanticsNode2), 1};
157  node1.label = "prefecture";
158  node1.value = "Kyoto";
159  node1.flags2 = &empty_flags;
160 
161  // Add node 2: subtree child of node 0.
162  FlutterSemanticsNode2 node2{sizeof(FlutterSemanticsNode2), 2};
163  std::vector<int32_t> node2_children{3, 4};
164  node2.child_count = node2_children.size();
165  node2.children_in_traversal_order = node2_children.data();
166  node2.children_in_hit_test_order = node2_children.data();
167  node2.flags2 = &empty_flags;
168 
169  // Add node 3: text child of node 2.
170  FlutterSemanticsNode2 node3{sizeof(FlutterSemanticsNode2), 3};
171  node3.label = "city";
172  node3.value = "Uji";
173  node3.flags2 = &empty_flags;
174 
175  // Add node 4: text child (with no text) of node 2.
176  FlutterSemanticsNode2 node4{sizeof(FlutterSemanticsNode2), 4};
177  node4.flags2 = &empty_flags;
178 
179  bridge->AddFlutterSemanticsNodeUpdate(node0);
180  bridge->AddFlutterSemanticsNodeUpdate(node1);
181  bridge->AddFlutterSemanticsNodeUpdate(node2);
182  bridge->AddFlutterSemanticsNodeUpdate(node3);
183  bridge->AddFlutterSemanticsNodeUpdate(node4);
184  bridge->CommitUpdates();
185 }
186 
187 ui::AXNode* AXNodeFromID(std::shared_ptr<AccessibilityBridge> bridge,
188  int32_t id) {
189  auto node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(id).lock();
190  return node_delegate ? node_delegate->GetAXNode() : nullptr;
191 }
192 
193 std::shared_ptr<AccessibilityBridgeWindowsSpy> GetAccessibilityBridgeSpy(
194  FlutterWindowsView& view) {
195  return std::static_pointer_cast<AccessibilityBridgeWindowsSpy>(
196  view.accessibility_bridge().lock());
197 }
198 
199 void ExpectWinEventFromAXEvent(int32_t node_id,
200  ui::AXEventGenerator::Event ax_event,
201  ax::mojom::Event expected_event) {
202  auto engine = GetTestEngine();
203  FlutterWindowsViewSpy view{
204  engine.get(), std::make_unique<NiceMock<MockWindowBindingHandler>>()};
205  EngineModifier modifier{engine.get()};
206  modifier.SetImplicitView(&view);
207  view.OnUpdateSemanticsEnabled(true);
208 
209  auto bridge = GetAccessibilityBridgeSpy(view);
210  PopulateAXTree(bridge);
211 
212  bridge->ResetRecords();
213  bridge->OnAccessibilityEvent({AXNodeFromID(bridge, node_id),
214  {ax_event, ax::mojom::EventFrom::kNone, {}}});
215  ASSERT_EQ(bridge->dispatched_events().size(), 1);
216  EXPECT_EQ(bridge->dispatched_events()[0].event_type, expected_event);
217 }
218 
219 void ExpectWinEventFromAXEventOnFocusNode(int32_t node_id,
220  ui::AXEventGenerator::Event ax_event,
221  ax::mojom::Event expected_event,
222  int32_t focus_id) {
223  auto engine = GetTestEngine();
224  FlutterWindowsViewSpy view{
225  engine.get(), std::make_unique<NiceMock<MockWindowBindingHandler>>()};
226  EngineModifier modifier{engine.get()};
227  modifier.SetImplicitView(&view);
228  view.OnUpdateSemanticsEnabled(true);
229 
230  auto bridge = GetAccessibilityBridgeSpy(view);
231  PopulateAXTree(bridge);
232 
233  bridge->ResetRecords();
234  auto focus_delegate =
235  bridge->GetFlutterPlatformNodeDelegateFromID(focus_id).lock();
236  bridge->SetFocus(std::static_pointer_cast<FlutterPlatformNodeDelegateWindows>(
237  focus_delegate));
238  bridge->OnAccessibilityEvent({AXNodeFromID(bridge, node_id),
239  {ax_event, ax::mojom::EventFrom::kNone, {}}});
240  ASSERT_EQ(bridge->dispatched_events().size(), 1);
241  EXPECT_EQ(bridge->dispatched_events()[0].event_type, expected_event);
242  EXPECT_EQ(bridge->dispatched_events()[0].node_delegate->GetAXNode()->id(),
243  focus_id);
244 }
245 
246 } // namespace
247 
249  auto engine = GetTestEngine();
250  FlutterWindowsViewSpy view{
251  engine.get(), std::make_unique<NiceMock<MockWindowBindingHandler>>()};
252  EngineModifier modifier{engine.get()};
253  modifier.SetImplicitView(&view);
254  view.OnUpdateSemanticsEnabled(true);
255 
256  auto bridge = view.accessibility_bridge().lock();
257  PopulateAXTree(bridge);
258 
259  auto node0_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
260  auto node1_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
261  EXPECT_EQ(node0_delegate->GetNativeViewAccessible(),
262  node1_delegate->GetParent());
263 }
264 
265 TEST(AccessibilityBridgeWindows, GetParentOnRootRetunsNullptr) {
266  auto engine = GetTestEngine();
267  FlutterWindowsViewSpy view{
268  engine.get(), std::make_unique<NiceMock<MockWindowBindingHandler>>()};
269  EngineModifier modifier{engine.get()};
270  modifier.SetImplicitView(&view);
271  view.OnUpdateSemanticsEnabled(true);
272 
273  auto bridge = view.accessibility_bridge().lock();
274  PopulateAXTree(bridge);
275 
276  auto node0_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
277  ASSERT_TRUE(node0_delegate->GetParent() == nullptr);
278 }
279 
280 TEST(AccessibilityBridgeWindows, DispatchAccessibilityAction) {
281  auto engine = GetTestEngine();
282  FlutterWindowsViewSpy view{
283  engine.get(), std::make_unique<NiceMock<MockWindowBindingHandler>>()};
284  EngineModifier modifier{engine.get()};
285  modifier.SetImplicitView(&view);
286  view.OnUpdateSemanticsEnabled(true);
287 
288  auto bridge = view.accessibility_bridge().lock();
289  PopulateAXTree(bridge);
290 
291  FlutterSemanticsAction actual_action = kFlutterSemanticsActionTap;
292  modifier.embedder_api().SendSemanticsAction = MOCK_ENGINE_PROC(
293  SendSemanticsAction,
294  ([&actual_action](FLUTTER_API_SYMBOL(FlutterEngine) engine,
295  const FlutterSendSemanticsActionInfo* info) {
296  actual_action = info->action;
297  return kSuccess;
298  }));
299 
300  AccessibilityBridgeWindows delegate(&view);
301  delegate.DispatchAccessibilityAction(1, kFlutterSemanticsActionCopy, {});
302  EXPECT_EQ(actual_action, kFlutterSemanticsActionCopy);
303 }
304 
305 TEST(AccessibilityBridgeWindows, OnAccessibilityEventAlert) {
306  ExpectWinEventFromAXEvent(0, ui::AXEventGenerator::Event::ALERT,
307  ax::mojom::Event::kAlert);
308 }
309 
310 TEST(AccessibilityBridgeWindows, OnAccessibilityEventChildrenChanged) {
311  ExpectWinEventFromAXEvent(0, ui::AXEventGenerator::Event::CHILDREN_CHANGED,
312  ax::mojom::Event::kChildrenChanged);
313 }
314 
315 TEST(AccessibilityBridgeWindows, OnAccessibilityEventFocusChanged) {
316  auto engine = GetTestEngine();
317  FlutterWindowsViewSpy view{
318  engine.get(), std::make_unique<NiceMock<MockWindowBindingHandler>>()};
319  EngineModifier modifier{engine.get()};
320  modifier.SetImplicitView(&view);
321  view.OnUpdateSemanticsEnabled(true);
322 
323  auto bridge = GetAccessibilityBridgeSpy(view);
324  PopulateAXTree(bridge);
325 
326  bridge->ResetRecords();
327  bridge->OnAccessibilityEvent({AXNodeFromID(bridge, 1),
328  {ui::AXEventGenerator::Event::FOCUS_CHANGED,
329  ax::mojom::EventFrom::kNone,
330  {}}});
331  ASSERT_EQ(bridge->dispatched_events().size(), 1);
332  EXPECT_EQ(bridge->dispatched_events()[0].event_type,
333  ax::mojom::Event::kFocus);
334 
335  ASSERT_EQ(bridge->focused_nodes().size(), 1);
336  EXPECT_EQ(bridge->focused_nodes()[0], 1);
337 }
338 
339 TEST(AccessibilityBridgeWindows, OnAccessibilityEventIgnoredChanged) {
340  // Static test nodes with no text, hint, or scrollability are ignored.
341  ExpectWinEventFromAXEvent(4, ui::AXEventGenerator::Event::IGNORED_CHANGED,
342  ax::mojom::Event::kHide);
343 }
344 
345 TEST(AccessibilityBridgeWindows, OnAccessibilityImageAnnotationChanged) {
346  ExpectWinEventFromAXEvent(
347  1, ui::AXEventGenerator::Event::IMAGE_ANNOTATION_CHANGED,
348  ax::mojom::Event::kTextChanged);
349 }
350 
351 TEST(AccessibilityBridgeWindows, OnAccessibilityLiveRegionChanged) {
352  ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::LIVE_REGION_CHANGED,
353  ax::mojom::Event::kLiveRegionChanged);
354 }
355 
356 TEST(AccessibilityBridgeWindows, OnAccessibilityNameChanged) {
357  ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::NAME_CHANGED,
358  ax::mojom::Event::kTextChanged);
359 }
360 
361 TEST(AccessibilityBridgeWindows, OnAccessibilityHScrollPosChanged) {
362  ExpectWinEventFromAXEvent(
363  1, ui::AXEventGenerator::Event::SCROLL_HORIZONTAL_POSITION_CHANGED,
364  ax::mojom::Event::kScrollPositionChanged);
365 }
366 
367 TEST(AccessibilityBridgeWindows, OnAccessibilityVScrollPosChanged) {
368  ExpectWinEventFromAXEvent(
369  1, ui::AXEventGenerator::Event::SCROLL_VERTICAL_POSITION_CHANGED,
370  ax::mojom::Event::kScrollPositionChanged);
371 }
372 
373 TEST(AccessibilityBridgeWindows, OnAccessibilitySelectedChanged) {
374  ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::SELECTED_CHANGED,
375  ax::mojom::Event::kValueChanged);
376 }
377 
378 TEST(AccessibilityBridgeWindows, OnAccessibilitySelectedChildrenChanged) {
379  ExpectWinEventFromAXEvent(
380  2, ui::AXEventGenerator::Event::SELECTED_CHILDREN_CHANGED,
381  ax::mojom::Event::kSelectedChildrenChanged);
382 }
383 
384 TEST(AccessibilityBridgeWindows, OnAccessibilitySubtreeCreated) {
385  ExpectWinEventFromAXEvent(0, ui::AXEventGenerator::Event::SUBTREE_CREATED,
386  ax::mojom::Event::kShow);
387 }
388 
389 TEST(AccessibilityBridgeWindows, OnAccessibilityValueChanged) {
390  ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::VALUE_CHANGED,
391  ax::mojom::Event::kValueChanged);
392 }
393 
394 TEST(AccessibilityBridgeWindows, OnAccessibilityStateChanged) {
395  ExpectWinEventFromAXEvent(
396  1, ui::AXEventGenerator::Event::WIN_IACCESSIBLE_STATE_CHANGED,
397  ax::mojom::Event::kStateChanged);
398 }
399 
400 TEST(AccessibilityBridgeWindows, OnDocumentSelectionChanged) {
401  ExpectWinEventFromAXEventOnFocusNode(
402  1, ui::AXEventGenerator::Event::DOCUMENT_SELECTION_CHANGED,
403  ax::mojom::Event::kDocumentSelectionChanged, 2);
404 }
405 
406 } // namespace testing
407 } // namespace flutter
std::shared_ptr< FlutterPlatformNodeDelegateWindows > node_delegate
ax::mojom::Event event_type
void DispatchAccessibilityAction(AccessibilityNodeId target, FlutterSemanticsAction action, fml::MallocMapping data) override
Dispatch accessibility action back to the Flutter framework. These actions are generated in the nativ...
void OnAccessibilityEvent(ui::AXEventGenerator::TargetedEvent targeted_event) override
Handle accessibility events generated due to accessibility tree changes. These events are needed to b...
@ DefaultImpeller
TEST(AccessibilityBridgeWindows, GetParent)
constexpr FlutterViewId kImplicitViewId
FlutterDesktopImpellerSwitch impeller_switch