Flutter macOS Embedder
FlutterPlatformNodeDelegateMacTest.mm
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 #include "flutter/testing/testing.h"
5 
14 
16 #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
17 #include "flutter/third_party/accessibility/ax/ax_action_data.h"
18 
19 namespace flutter::testing {
20 
21 namespace {
22 // Returns a view controller configured for the text fixture resource configuration.
23 FlutterViewController* CreateTestViewController() {
24  NSString* fixtures = @(testing::GetFixturesPath());
25  FlutterDartProject* project = [[FlutterDartProject alloc]
26  initWithAssetsPath:fixtures
27  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
28  return [[FlutterViewController alloc] initWithProject:project];
29 }
30 } // namespace
31 
33  FlutterViewController* viewController = CreateTestViewController();
34  FlutterEngine* engine = viewController.engine;
35  engine.semanticsEnabled = YES;
36  auto bridge = viewController.accessibilityBridge.lock();
37  // Initialize ax node data.
38  FlutterSemanticsNode2 root;
39  root.id = 0;
40  root.flags = static_cast<FlutterSemanticsFlag>(0);
41  ;
42  root.actions = static_cast<FlutterSemanticsAction>(0);
43  root.text_selection_base = -1;
44  root.text_selection_extent = -1;
45  root.label = "accessibility";
46  root.hint = "";
47  root.value = "";
48  root.increased_value = "";
49  root.decreased_value = "";
50  root.tooltip = "";
51  root.child_count = 0;
52  root.custom_accessibility_actions_count = 0;
53  bridge->AddFlutterSemanticsNodeUpdate(root);
54 
55  bridge->CommitUpdates();
56 
57  auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
58  // Verify the accessibility attribute matches.
59  NSAccessibilityElement* native_accessibility =
60  root_platform_node_delegate->GetNativeViewAccessible();
61  std::string value = [native_accessibility.accessibilityValue UTF8String];
62  EXPECT_TRUE(value == "accessibility");
63  EXPECT_EQ(native_accessibility.accessibilityRole, NSAccessibilityStaticTextRole);
64  EXPECT_EQ([native_accessibility.accessibilityChildren count], 0u);
65  [engine shutDownEngine];
66 }
67 
68 TEST(FlutterPlatformNodeDelegateMac, SelectableTextHasCorrectSemantics) {
69  FlutterViewController* viewController = CreateTestViewController();
70  FlutterEngine* engine = viewController.engine;
71  engine.semanticsEnabled = YES;
72  auto bridge = viewController.accessibilityBridge.lock();
73  // Initialize ax node data.
74  FlutterSemanticsNode2 root;
75  root.id = 0;
76  root.flags =
77  static_cast<FlutterSemanticsFlag>(FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField |
78  FlutterSemanticsFlag::kFlutterSemanticsFlagIsReadOnly);
79  root.actions = static_cast<FlutterSemanticsAction>(0);
80  root.text_selection_base = 1;
81  root.text_selection_extent = 3;
82  root.label = "";
83  root.hint = "";
84  // Selectable text store its text in value
85  root.value = "selectable text";
86  root.increased_value = "";
87  root.decreased_value = "";
88  root.tooltip = "";
89  root.child_count = 0;
90  root.custom_accessibility_actions_count = 0;
91  bridge->AddFlutterSemanticsNodeUpdate(root);
92 
93  bridge->CommitUpdates();
94 
95  auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
96  // Verify the accessibility attribute matches.
97  NSAccessibilityElement* native_accessibility =
98  root_platform_node_delegate->GetNativeViewAccessible();
99  std::string value = [native_accessibility.accessibilityValue UTF8String];
100  EXPECT_EQ(value, "selectable text");
101  EXPECT_EQ(native_accessibility.accessibilityRole, NSAccessibilityStaticTextRole);
102  EXPECT_EQ([native_accessibility.accessibilityChildren count], 0u);
103  NSRange selection = native_accessibility.accessibilitySelectedTextRange;
104  EXPECT_EQ(selection.location, 1u);
105  EXPECT_EQ(selection.length, 2u);
106  std::string selected_text = [native_accessibility.accessibilitySelectedText UTF8String];
107  EXPECT_EQ(selected_text, "el");
108 }
109 
110 TEST(FlutterPlatformNodeDelegateMac, SelectableTextWithoutSelectionReturnZeroRange) {
111  FlutterViewController* viewController = CreateTestViewController();
112  FlutterEngine* engine = viewController.engine;
113  engine.semanticsEnabled = YES;
114  auto bridge = viewController.accessibilityBridge.lock();
115  // Initialize ax node data.
116  FlutterSemanticsNode2 root;
117  root.id = 0;
118  root.flags =
119  static_cast<FlutterSemanticsFlag>(FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField |
120  FlutterSemanticsFlag::kFlutterSemanticsFlagIsReadOnly);
121  root.actions = static_cast<FlutterSemanticsAction>(0);
122  root.text_selection_base = -1;
123  root.text_selection_extent = -1;
124  root.label = "";
125  root.hint = "";
126  // Selectable text store its text in value
127  root.value = "selectable text";
128  root.increased_value = "";
129  root.decreased_value = "";
130  root.tooltip = "";
131  root.child_count = 0;
132  root.custom_accessibility_actions_count = 0;
133  bridge->AddFlutterSemanticsNodeUpdate(root);
134 
135  bridge->CommitUpdates();
136 
137  auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
138  // Verify the accessibility attribute matches.
139  NSAccessibilityElement* native_accessibility =
140  root_platform_node_delegate->GetNativeViewAccessible();
141  NSRange selection = native_accessibility.accessibilitySelectedTextRange;
142  EXPECT_TRUE(selection.location == NSNotFound);
143  EXPECT_EQ(selection.length, 0u);
144 }
145 
146 // MOCK_ENGINE_PROC is leaky by design
147 // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape)
148 
150  FlutterViewController* viewController = CreateTestViewController();
151  FlutterEngine* engine = viewController.engine;
152 
153  // Attach the view to a NSWindow.
154  NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
155  styleMask:NSBorderlessWindowMask
156  backing:NSBackingStoreBuffered
157  defer:NO];
158  window.contentView = viewController.view;
159 
160  engine.semanticsEnabled = YES;
161  auto bridge = viewController.accessibilityBridge.lock();
162  // Initialize ax node data.
163  FlutterSemanticsNode2 root;
164  root.id = 0;
165  root.label = "root";
166  root.hint = "";
167  root.value = "";
168  root.increased_value = "";
169  root.decreased_value = "";
170  root.tooltip = "";
171  root.child_count = 1;
172  int32_t children[] = {1};
173  root.children_in_traversal_order = children;
174  root.custom_accessibility_actions_count = 0;
175  bridge->AddFlutterSemanticsNodeUpdate(root);
176 
177  FlutterSemanticsNode2 child1;
178  child1.id = 1;
179  child1.label = "child 1";
180  child1.hint = "";
181  child1.value = "";
182  child1.increased_value = "";
183  child1.decreased_value = "";
184  child1.tooltip = "";
185  child1.child_count = 0;
186  child1.custom_accessibility_actions_count = 0;
187  bridge->AddFlutterSemanticsNodeUpdate(child1);
188 
189  bridge->CommitUpdates();
190 
191  auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
192 
193  // Set up embedder API mock.
194  FlutterSemanticsAction called_action;
195  uint64_t called_id;
196 
197  engine.embedderAPI.DispatchSemanticsAction = MOCK_ENGINE_PROC(
198  DispatchSemanticsAction,
199  ([&called_id, &called_action](auto engine, uint64_t id, FlutterSemanticsAction action,
200  const uint8_t* data, size_t data_length) {
201  called_id = id;
202  called_action = action;
203  return kSuccess;
204  }));
205 
206  // Performs an AXAction.
207  ui::AXActionData action_data;
208  action_data.action = ax::mojom::Action::kDoDefault;
209  root_platform_node_delegate->AccessibilityPerformAction(action_data);
210 
211  EXPECT_EQ(called_action, FlutterSemanticsAction::kFlutterSemanticsActionTap);
212  EXPECT_EQ(called_id, 1u);
213 
214  [engine setViewController:nil];
215  [engine shutDownEngine];
216 }
217 
218 // NOLINTEND(clang-analyzer-core.StackAddressEscape)
219 
220 TEST(FlutterPlatformNodeDelegateMac, TextFieldUsesFlutterTextField) {
221  FlutterViewController* viewController = CreateTestViewController();
222  FlutterEngine* engine = viewController.engine;
223  [viewController loadView];
224 
225  // Unit test localization is unnecessary.
226  // NOLINTNEXTLINE(clang-analyzer-optin.osx.cocoa.localizability.NonLocalizedStringChecker)
227  viewController.textInputPlugin.string = @"textfield";
228  // Creates a NSWindow so that the native text field can become first responder.
229  NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
230  styleMask:NSBorderlessWindowMask
231  backing:NSBackingStoreBuffered
232  defer:NO];
233  window.contentView = viewController.view;
234  engine.semanticsEnabled = YES;
235 
236  auto bridge = viewController.accessibilityBridge.lock();
237  // Initialize ax node data.
238  FlutterSemanticsNode2 root;
239  root.id = 0;
240  root.flags = static_cast<FlutterSemanticsFlag>(0);
241  root.actions = static_cast<FlutterSemanticsAction>(0);
242  root.label = "root";
243  root.hint = "";
244  root.value = "";
245  root.increased_value = "";
246  root.decreased_value = "";
247  root.tooltip = "";
248  root.child_count = 1;
249  int32_t children[] = {1};
250  root.children_in_traversal_order = children;
251  root.custom_accessibility_actions_count = 0;
252  root.rect = {0, 0, 100, 100}; // LTRB
253  root.transform = {1, 0, 0, 0, 1, 0, 0, 0, 1};
254  bridge->AddFlutterSemanticsNodeUpdate(root);
255 
256  double rectSize = 50;
257  double transformFactor = 0.5;
258 
259  FlutterSemanticsNode2 child1;
260  child1.id = 1;
261  child1.flags = FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField;
262  child1.actions = static_cast<FlutterSemanticsAction>(0);
263  child1.label = "";
264  child1.hint = "";
265  child1.value = "textfield";
266  child1.increased_value = "";
267  child1.decreased_value = "";
268  child1.tooltip = "";
269  child1.text_selection_base = -1;
270  child1.text_selection_extent = -1;
271  child1.child_count = 0;
272  child1.custom_accessibility_actions_count = 0;
273  child1.rect = {0, 0, rectSize, rectSize}; // LTRB
274  child1.transform = {transformFactor, 0, 0, 0, transformFactor, 0, 0, 0, 1};
275  bridge->AddFlutterSemanticsNodeUpdate(child1);
276 
277  bridge->CommitUpdates();
278 
279  auto child_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
280  // Verify the accessibility attribute matches.
281  id native_accessibility = child_platform_node_delegate->GetNativeViewAccessible();
282  EXPECT_EQ([native_accessibility isKindOfClass:[FlutterTextField class]], YES);
283  FlutterTextField* native_text_field = (FlutterTextField*)native_accessibility;
284 
285  NSView* view = viewController.flutterView;
286  CGRect scaledBounds = [view convertRectToBacking:view.bounds];
287  CGSize scaledSize = scaledBounds.size;
288  double pixelRatio = view.bounds.size.width == 0 ? 1 : scaledSize.width / view.bounds.size.width;
289 
290  double expectedFrameSize = rectSize * transformFactor / pixelRatio;
291  EXPECT_EQ(NSEqualRects(native_text_field.frame, NSMakeRect(0, 600 - expectedFrameSize,
292  expectedFrameSize, expectedFrameSize)),
293  YES);
294 
295  [native_text_field startEditing];
296  EXPECT_EQ([native_text_field.stringValue isEqualToString:@"textfield"], YES);
297 }
298 
299 TEST(FlutterPlatformNodeDelegateMac, ChangingFlagsUpdatesNativeViewAccessible) {
300  FlutterViewController* viewController = CreateTestViewController();
301  FlutterEngine* engine = viewController.engine;
302  [viewController loadView];
303 
304  // Creates a NSWindow so that the native text field can become first responder.
305  NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
306  styleMask:NSBorderlessWindowMask
307  backing:NSBackingStoreBuffered
308  defer:NO];
309  window.contentView = viewController.view;
310  engine.semanticsEnabled = YES;
311 
312  auto bridge = viewController.accessibilityBridge.lock();
313  // Initialize ax node data.
314  FlutterSemanticsNode2 root;
315  root.id = 0;
316  root.flags = static_cast<FlutterSemanticsFlag>(0);
317  root.actions = static_cast<FlutterSemanticsAction>(0);
318  root.label = "root";
319  root.hint = "";
320  root.value = "";
321  root.increased_value = "";
322  root.decreased_value = "";
323  root.tooltip = "";
324  root.child_count = 1;
325  int32_t children[] = {1};
326  root.children_in_traversal_order = children;
327  root.custom_accessibility_actions_count = 0;
328  root.rect = {0, 0, 100, 100}; // LTRB
329  root.transform = {1, 0, 0, 0, 1, 0, 0, 0, 1};
330  bridge->AddFlutterSemanticsNodeUpdate(root);
331 
332  double rectSize = 50;
333  double transformFactor = 0.5;
334 
335  FlutterSemanticsNode2 child1;
336  child1.id = 1;
337  child1.flags = static_cast<FlutterSemanticsFlag>(0);
338  child1.actions = static_cast<FlutterSemanticsAction>(0);
339  child1.label = "";
340  child1.hint = "";
341  child1.value = "textfield";
342  child1.increased_value = "";
343  child1.decreased_value = "";
344  child1.tooltip = "";
345  child1.text_selection_base = -1;
346  child1.text_selection_extent = -1;
347  child1.child_count = 0;
348  child1.custom_accessibility_actions_count = 0;
349  child1.rect = {0, 0, rectSize, rectSize}; // LTRB
350  child1.transform = {transformFactor, 0, 0, 0, transformFactor, 0, 0, 0, 1};
351  bridge->AddFlutterSemanticsNodeUpdate(child1);
352 
353  bridge->CommitUpdates();
354 
355  auto child_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
356  // Verify the accessibility attribute matches.
357  id native_accessibility = child_platform_node_delegate->GetNativeViewAccessible();
358  EXPECT_TRUE([[native_accessibility className] isEqualToString:@"AXPlatformNodeCocoa"]);
359 
360  // Converting child to text field should produce `FlutterTextField` native view accessible.
361  child1.flags = FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField;
362  bridge->AddFlutterSemanticsNodeUpdate(child1);
363  bridge->CommitUpdates();
364 
365  native_accessibility = child_platform_node_delegate->GetNativeViewAccessible();
366  EXPECT_TRUE([native_accessibility isKindOfClass:[FlutterTextField class]]);
367 
368  child1.flags = static_cast<FlutterSemanticsFlag>(0);
369  bridge->AddFlutterSemanticsNodeUpdate(child1);
370  bridge->CommitUpdates();
371 
372  native_accessibility = child_platform_node_delegate->GetNativeViewAccessible();
373  EXPECT_TRUE([[native_accessibility className] isEqualToString:@"AXPlatformNodeCocoa"]);
374 }
375 
376 } // namespace flutter::testing
FlutterEngine
Definition: FlutterEngine.h:31
FlutterViewController
Definition: FlutterViewController.h:73
FlutterEngine.h
FlutterEngine_Internal.h
flutter::testing
Definition: AccessibilityBridgeMacTest.mm:13
flutter::testing::TEST
TEST(FlutterAppDelegateTest, DoesNotCallDelegatesWithoutHandler)
Definition: FlutterAppDelegateTest.mm:32
FlutterViewController::engine
FlutterEngine * engine
Definition: FlutterViewController.h:78
FlutterViewControllerTestUtils.h
AccessibilityBridgeMac.h
FlutterPlatformNodeDelegateMac.h
FlutterDartProject_Internal.h
FlutterViewController_Internal.h
FlutterTextInputSemanticsObject.h
FlutterDartProject
Definition: FlutterDartProject.mm:24
accessibility_bridge.h
FlutterTextField
Definition: FlutterTextInputSemanticsObject.h:81
flutter::FlutterPlatformNodeDelegateMac
Definition: FlutterPlatformNodeDelegateMac.h:22