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  FlutterSemanticsFlags flags = FlutterSemanticsFlags{0};
40  root.id = 0;
41  root.flags2 = &flags;
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  FlutterSemanticsFlags flags = FlutterSemanticsFlags{.is_text_field = true, .is_read_only = true};
76  root.id = 0;
77  root.flags2 = &flags;
78  root.actions = static_cast<FlutterSemanticsAction>(0);
79  root.text_selection_base = 1;
80  root.text_selection_extent = 3;
81  root.label = "";
82  root.hint = "";
83  // Selectable text store its text in value
84  root.value = "selectable text";
85  root.increased_value = "";
86  root.decreased_value = "";
87  root.tooltip = "";
88  root.child_count = 0;
89  root.custom_accessibility_actions_count = 0;
90  bridge->AddFlutterSemanticsNodeUpdate(root);
91 
92  bridge->CommitUpdates();
93 
94  auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
95  // Verify the accessibility attribute matches.
96  NSAccessibilityElement* native_accessibility =
97  root_platform_node_delegate->GetNativeViewAccessible();
98  std::string value = [native_accessibility.accessibilityValue UTF8String];
99  EXPECT_EQ(value, "selectable text");
100  EXPECT_EQ(native_accessibility.accessibilityRole, NSAccessibilityStaticTextRole);
101  EXPECT_EQ([native_accessibility.accessibilityChildren count], 0u);
102  NSRange selection = native_accessibility.accessibilitySelectedTextRange;
103  EXPECT_EQ(selection.location, 1u);
104  EXPECT_EQ(selection.length, 2u);
105  std::string selected_text = [native_accessibility.accessibilitySelectedText UTF8String];
106  EXPECT_EQ(selected_text, "el");
107 }
108 
109 TEST(FlutterPlatformNodeDelegateMac, SelectableTextWithoutSelectionReturnZeroRange) {
110  FlutterViewController* viewController = CreateTestViewController();
111  FlutterEngine* engine = viewController.engine;
112  engine.semanticsEnabled = YES;
113  auto bridge = viewController.accessibilityBridge.lock();
114  // Initialize ax node data.
115  FlutterSemanticsNode2 root;
116  FlutterSemanticsFlags flags = FlutterSemanticsFlags{.is_text_field = true, .is_read_only = true};
117  root.id = 0;
118  root.flags2 = &flags;
119  root.actions = static_cast<FlutterSemanticsAction>(0);
120  root.text_selection_base = -1;
121  root.text_selection_extent = -1;
122  root.label = "";
123  root.hint = "";
124  // Selectable text store its text in value
125  root.value = "selectable text";
126  root.increased_value = "";
127  root.decreased_value = "";
128  root.tooltip = "";
129  root.child_count = 0;
130  root.custom_accessibility_actions_count = 0;
131  bridge->AddFlutterSemanticsNodeUpdate(root);
132 
133  bridge->CommitUpdates();
134 
135  auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
136  // Verify the accessibility attribute matches.
137  NSAccessibilityElement* native_accessibility =
138  root_platform_node_delegate->GetNativeViewAccessible();
139  NSRange selection = native_accessibility.accessibilitySelectedTextRange;
140  EXPECT_TRUE(selection.location == NSNotFound);
141  EXPECT_EQ(selection.length, 0u);
142 }
143 
144 // MOCK_ENGINE_PROC is leaky by design
145 // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape)
146 
148  FlutterViewController* viewController = CreateTestViewController();
149  FlutterEngine* engine = viewController.engine;
150 
151  // Attach the view to a NSWindow.
152  NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
153  styleMask:NSBorderlessWindowMask
154  backing:NSBackingStoreBuffered
155  defer:NO];
156  window.contentView = viewController.view;
157 
158  engine.semanticsEnabled = YES;
159  auto bridge = viewController.accessibilityBridge.lock();
160  // Initialize ax node data.
161  FlutterSemanticsNode2 root;
162  FlutterSemanticsFlags flags = FlutterSemanticsFlags{};
163  root.flags2 = &flags;
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  FlutterSemanticsFlags child_flags = FlutterSemanticsFlags{};
179  child1.flags2 = &child_flags;
180  child1.id = 1;
181  child1.label = "child 1";
182  child1.hint = "";
183  child1.value = "";
184  child1.increased_value = "";
185  child1.decreased_value = "";
186  child1.tooltip = "";
187  child1.child_count = 0;
188  child1.custom_accessibility_actions_count = 0;
189  bridge->AddFlutterSemanticsNodeUpdate(child1);
190 
191  bridge->CommitUpdates();
192 
193  auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
194 
195  // Set up embedder API mock.
196  FlutterSemanticsAction called_action;
197  uint64_t called_id;
198 
199  engine.embedderAPI.DispatchSemanticsAction = MOCK_ENGINE_PROC(
200  DispatchSemanticsAction,
201  ([&called_id, &called_action](auto engine, uint64_t id, FlutterSemanticsAction action,
202  const uint8_t* data, size_t data_length) {
203  called_id = id;
204  called_action = action;
205  return kSuccess;
206  }));
207 
208  // Performs an AXAction.
209  ui::AXActionData action_data;
210  action_data.action = ax::mojom::Action::kDoDefault;
211  root_platform_node_delegate->AccessibilityPerformAction(action_data);
212 
213  EXPECT_EQ(called_action, FlutterSemanticsAction::kFlutterSemanticsActionTap);
214  EXPECT_EQ(called_id, 1u);
215 
216  [engine setViewController:nil];
217  [engine shutDownEngine];
218 }
219 
220 // NOLINTEND(clang-analyzer-core.StackAddressEscape)
221 
222 TEST(FlutterPlatformNodeDelegateMac, TextFieldUsesFlutterTextField) {
223  FlutterViewController* viewController = CreateTestViewController();
224  FlutterEngine* engine = viewController.engine;
225  [viewController loadView];
226 
227  // Unit test localization is unnecessary.
228  // NOLINTNEXTLINE(clang-analyzer-optin.osx.cocoa.localizability.NonLocalizedStringChecker)
229  engine.textInputPlugin.string = @"textfield";
230  // Creates a NSWindow so that the native text field can become first responder.
231  NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
232  styleMask:NSBorderlessWindowMask
233  backing:NSBackingStoreBuffered
234  defer:NO];
235  window.contentView = viewController.view;
236  engine.semanticsEnabled = YES;
237 
238  auto bridge = viewController.accessibilityBridge.lock();
239  // Initialize ax node data.
240  FlutterSemanticsNode2 root;
241  FlutterSemanticsFlags flags = FlutterSemanticsFlags{0};
242  FlutterSemanticsFlags child_flags = FlutterSemanticsFlags{.is_text_field = true};
243  root.id = 0;
244  root.flags2 = &flags;
245  root.actions = static_cast<FlutterSemanticsAction>(0);
246  root.label = "root";
247  root.hint = "";
248  root.value = "";
249  root.increased_value = "";
250  root.decreased_value = "";
251  root.tooltip = "";
252  root.child_count = 1;
253  int32_t children[] = {1};
254  root.children_in_traversal_order = children;
255  root.custom_accessibility_actions_count = 0;
256  root.rect = {0, 0, 100, 100}; // LTRB
257  root.transform = {1, 0, 0, 0, 1, 0, 0, 0, 1};
258  bridge->AddFlutterSemanticsNodeUpdate(root);
259 
260  double rectSize = 50;
261  double transformFactor = 0.5;
262 
263  FlutterSemanticsNode2 child1;
264  child1.id = 1;
265  child1.flags2 = &child_flags;
266  child1.actions = static_cast<FlutterSemanticsAction>(0);
267  child1.label = "";
268  child1.hint = "";
269  child1.value = "textfield";
270  child1.increased_value = "";
271  child1.decreased_value = "";
272  child1.tooltip = "";
273  child1.text_selection_base = -1;
274  child1.text_selection_extent = -1;
275  child1.child_count = 0;
276  child1.custom_accessibility_actions_count = 0;
277  child1.rect = {0, 0, rectSize, rectSize}; // LTRB
278  child1.transform = {transformFactor, 0, 0, 0, transformFactor, 0, 0, 0, 1};
279  bridge->AddFlutterSemanticsNodeUpdate(child1);
280 
281  bridge->CommitUpdates();
282 
283  auto child_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
284  // Verify the accessibility attribute matches.
285  id native_accessibility = child_platform_node_delegate->GetNativeViewAccessible();
286  EXPECT_EQ([native_accessibility isKindOfClass:[FlutterTextField class]], YES);
287  FlutterTextField* native_text_field = (FlutterTextField*)native_accessibility;
288 
289  NSView* view = viewController.flutterView;
290  CGRect scaledBounds = [view convertRectToBacking:view.bounds];
291  CGSize scaledSize = scaledBounds.size;
292  double pixelRatio = view.bounds.size.width == 0 ? 1 : scaledSize.width / view.bounds.size.width;
293 
294  double expectedFrameSize = rectSize * transformFactor / pixelRatio;
295  EXPECT_EQ(NSEqualRects(native_text_field.frame, NSMakeRect(0, 600 - expectedFrameSize,
296  expectedFrameSize, expectedFrameSize)),
297  YES);
298 
299  [native_text_field startEditing];
300  EXPECT_EQ([native_text_field.stringValue isEqualToString:@"textfield"], YES);
301 }
302 
303 TEST(FlutterPlatformNodeDelegateMac, ChangingFlagsUpdatesNativeViewAccessible) {
304  FlutterViewController* viewController = CreateTestViewController();
305  FlutterEngine* engine = viewController.engine;
306  [viewController loadView];
307 
308  // Creates a NSWindow so that the native text field can become first responder.
309  NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
310  styleMask:NSBorderlessWindowMask
311  backing:NSBackingStoreBuffered
312  defer:NO];
313  window.contentView = viewController.view;
314  engine.semanticsEnabled = YES;
315 
316  auto bridge = viewController.accessibilityBridge.lock();
317  // Initialize ax node data.
318  FlutterSemanticsNode2 root;
319  root.id = 0;
320  FlutterSemanticsFlags flags = FlutterSemanticsFlags{0};
321  root.flags2 = &flags;
322  root.actions = static_cast<FlutterSemanticsAction>(0);
323  root.label = "root";
324  root.hint = "";
325  root.value = "";
326  root.increased_value = "";
327  root.decreased_value = "";
328  root.tooltip = "";
329  root.child_count = 1;
330  int32_t children[] = {1};
331  root.children_in_traversal_order = children;
332  root.custom_accessibility_actions_count = 0;
333  root.rect = {0, 0, 100, 100}; // LTRB
334  root.transform = {1, 0, 0, 0, 1, 0, 0, 0, 1};
335  bridge->AddFlutterSemanticsNodeUpdate(root);
336 
337  double rectSize = 50;
338  double transformFactor = 0.5;
339 
340  FlutterSemanticsNode2 child1;
341  FlutterSemanticsFlags child_flags = FlutterSemanticsFlags{0};
342  child1.flags2 = &child_flags;
343  child1.id = 1;
344  child1.actions = static_cast<FlutterSemanticsAction>(0);
345  child1.label = "";
346  child1.hint = "";
347  child1.value = "textfield";
348  child1.increased_value = "";
349  child1.decreased_value = "";
350  child1.tooltip = "";
351  child1.text_selection_base = -1;
352  child1.text_selection_extent = -1;
353  child1.child_count = 0;
354  child1.custom_accessibility_actions_count = 0;
355  child1.rect = {0, 0, rectSize, rectSize}; // LTRB
356  child1.transform = {transformFactor, 0, 0, 0, transformFactor, 0, 0, 0, 1};
357  bridge->AddFlutterSemanticsNodeUpdate(child1);
358 
359  bridge->CommitUpdates();
360 
361  auto child_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
362  // Verify the accessibility attribute matches.
363  id native_accessibility = child_platform_node_delegate->GetNativeViewAccessible();
364  EXPECT_TRUE([[native_accessibility className] isEqualToString:@"AXPlatformNodeCocoa"]);
365 
366  // Converting child to text field should produce `FlutterTextField` native view accessible.
367 
368  FlutterSemanticsFlags child_flags_updated_1 = FlutterSemanticsFlags{.is_text_field = true};
369  child1.flags2 = &child_flags_updated_1;
370  bridge->AddFlutterSemanticsNodeUpdate(child1);
371  bridge->CommitUpdates();
372 
373  native_accessibility = child_platform_node_delegate->GetNativeViewAccessible();
374  EXPECT_TRUE([native_accessibility isKindOfClass:[FlutterTextField class]]);
375 
376  FlutterSemanticsFlags child_flags_updated_2 = FlutterSemanticsFlags{.is_text_field = false};
377  child1.flags2 = &child_flags_updated_2;
378  bridge->AddFlutterSemanticsNodeUpdate(child1);
379  bridge->CommitUpdates();
380 
381  native_accessibility = child_platform_node_delegate->GetNativeViewAccessible();
382  EXPECT_TRUE([[native_accessibility className] isEqualToString:@"AXPlatformNodeCocoa"]);
383 }
384 
385 } // namespace flutter::testing
TEST(FlutterAppDelegateTest, DoesNotCallDelegatesWithoutHandler)