| 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 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
147 TEST(FlutterPlatformNodeDelegateMac, CanPerformAction) {
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
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:24
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