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