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