| 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 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
150 TEST(FlutterPlatformNodeDelegateMac, CanPerformAction) {
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
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