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