| 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
6 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h"
7 #import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h"
8 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h"
9 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
10 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h"
11 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h"
12 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h"
13 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
14
15 #include "flutter/shell/platform/common/accessibility_bridge.h"
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
32 TEST(FlutterPlatformNodeDelegateMac, Basics) {
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
153 TEST(FlutterPlatformNodeDelegateMac, CanPerformAction) {
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
AccessibilityBridgeMac.h
FlutterDartProject_Internal.h
FlutterEngine.h
FlutterEngine_Internal.h
FlutterPlatformNodeDelegateMac.h
FlutterTextInputSemanticsObject.h
FlutterViewController_Internal.h
FlutterViewControllerTestUtils.h
accessibility_bridge.h
FlutterDartProject
Definition: FlutterDartProject.mm:39
flutter::FlutterPlatformNodeDelegateMac
Definition: FlutterPlatformNodeDelegateMac.h:22
FlutterEngine
Definition: FlutterEngine.h:32
FlutterTextField
Definition: FlutterTextInputSemanticsObject.h:82
FlutterViewController
Definition: FlutterViewController.h:73
FlutterViewController::engine
FlutterEngine * engine
Definition: FlutterViewController.h:78
flutter::testing
Definition: AccessibilityBridgeMacTest.mm:13
flutter::testing::TEST
TEST(FlutterAppDelegateTest, DoesNotCallDelegatesWithoutHandler)
Definition: FlutterAppDelegateTest.mm:32