| Flutter macOS Embedder |

FlutterEngineTest.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
5 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h"
6 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
7
8 #include <objc/objc.h>
9
10 #include <algorithm>
11 #include <functional>
12 #include <thread>
13 #include <vector>
14
15 #include "flutter/fml/synchronization/waitable_event.h"
16 #include "flutter/lib/ui/window/platform_message.h"
17 #include "flutter/shell/platform/common/accessibility_bridge.h"
18 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
19 #import "flutter/shell/platform/darwin/common/framework/Source/FlutterBinaryMessengerRelay.h"
20 #import "flutter/shell/platform/darwin/common/test_utils_swift/test_utils_swift.h"
21 #import "flutter/shell/platform/darwin/macos/InternalFlutterSwift/InternalFlutterSwift.h"
22 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h"
23 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppLifecycleDelegate.h"
24 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterPluginMacOS.h"
25 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h"
26 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h"
27 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h"
28 #include "flutter/shell/platform/embedder/embedder.h"
29 #include "flutter/shell/platform/embedder/embedder_engine.h"
30 #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
31 #include "flutter/testing/stream_capture.h"
32 #include "flutter/testing/test_dart_native_resolver.h"
33 #include "gtest/gtest.h"
34
35 // CREATE_NATIVE_ENTRY and MOCK_ENGINE_PROC are leaky by design
36 // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape)
37
38 @interface FlutterEngine (Test)
39 /**
40 * The FlutterCompositor object currently in use by the FlutterEngine.
41 *
42 * May be nil if the compositor has not been initialized yet.
43 */
44 @property(nonatomic, readonly, nullable) flutter::FlutterCompositor* macOSCompositor;
45
46 @end
47
48 @interface TestPlatformViewFactory : NSObject <FlutterPlatformViewFactory>
49 @end
50
51 @implementation TestPlatformViewFactory
52 - (nonnull NSView*)createWithViewIdentifier:(FlutterViewIdentifier)viewIdentifier
53 arguments:(nullable id)args {
54 return viewIdentifier == 42 ? [[NSView alloc] init] : nil;
55 }
56
57 @end
58
59 @interface PlainAppDelegate : NSObject <NSApplicationDelegate>
60 @end
61
62 @implementation PlainAppDelegate
63 - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication* _Nonnull)sender {
64 // Always cancel, so that the test doesn't exit.
65 return NSTerminateCancel;
66 }
67 @end
68
69 #pragma mark -
70
71 @interface FakeLifecycleProvider : NSObject <FlutterAppLifecycleProvider, NSApplicationDelegate>
72
73 @property(nonatomic, strong, readonly) NSPointerArray* registeredDelegates;
74
75 // True if the given delegate is currently registered.
76 - (BOOL)hasDelegate:(nonnull NSObject<FlutterAppLifecycleDelegate>*)delegate;
77 @end
78
79 @implementation FakeLifecycleProvider {
80 /**
81 * All currently registered delegates.
82 *
83 * This does not use NSPointerArray or any other weak-pointer
84 * system, because a weak pointer will be nil'd out at the start of dealloc, which will break
85 * queries. E.g., if a delegate is dealloc'd without being unregistered, a weak pointer array
86 * would no longer contain that pointer even though removeApplicationLifecycleDelegate: was never
87 * called, causing tests to pass incorrectly.
88 */
89 std::vector<void*> _delegates;
90 }
91
92 - (void)addApplicationLifecycleDelegate:(nonnull NSObject<FlutterAppLifecycleDelegate>*)delegate {
93 _delegates.push_back((__bridge void*)delegate);
94 }
95
96 - (void)removeApplicationLifecycleDelegate:
97 (nonnull NSObject<FlutterAppLifecycleDelegate>*)delegate {
98 auto delegateIndex = std::find(_delegates.begin(), _delegates.end(), (__bridge void*)delegate);
99 NSAssert(delegateIndex != _delegates.end(),
100 @"Attempting to unregister a delegate that was not registered.");
101 _delegates.erase(delegateIndex);
102 }
103
104 - (BOOL)hasDelegate:(nonnull NSObject<FlutterAppLifecycleDelegate>*)delegate {
105 return std::find(_delegates.begin(), _delegates.end(), (__bridge void*)delegate) !=
106 _delegates.end();
107 }
108
109 @end
110
111 #pragma mark -
112
113 @interface FakeAppDelegatePlugin : NSObject <FlutterPlugin>
114 @end
115
116 @implementation FakeAppDelegatePlugin
117 + (void)registerWithRegistrar:(id<FlutterPluginRegistrar>)registrar {
118 }
119 @end
120
121 #pragma mark -
122
123 @interface MockableFlutterEngine : FlutterEngine
124 @end
125
126 @implementation MockableFlutterEngine
127 - (NSArray<NSScreen*>*)screens {
128 id mockScreen = OCMClassMock([NSScreen class]);
129 OCMStub([mockScreen backingScaleFactor]).andReturn(2.0);
130 OCMStub([mockScreen deviceDescription]).andReturn(@{
131 @"NSScreenNumber" : [NSNumber numberWithInt:10]
132 });
133 OCMStub([mockScreen frame]).andReturn(NSMakeRect(10, 20, 30, 40));
134 return [NSArray arrayWithObject:mockScreen];
135 }
136 @end
137
138 #pragma mark -
139
140 namespace flutter::testing {
141
142 TEST_F(FlutterEngineTest, CanLaunch) {
143 FlutterEngine* engine = GetFlutterEngine();
144 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
145 ASSERT_TRUE(engine.running);
146 }
147
148 TEST_F(FlutterEngineTest, HasNonNullExecutableName) {
149 FlutterEngine* engine = GetFlutterEngine();
150 std::string executable_name = [[engine executableName] UTF8String];
151 ASSERT_FALSE(executable_name.empty());
152
153 // Block until notified by the Dart test of the value of Platform.executable.
154 BOOL signaled = NO;
155 AddNativeCallback("NotifyStringValue", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
156 const auto dart_string = tonic::DartConverter<std::string>::FromDart(
157 Dart_GetNativeArgument(args, 0));
158 EXPECT_EQ(executable_name, dart_string);
159 signaled = YES;
160 }));
161
162 // Launch the test entrypoint.
163 EXPECT_TRUE([engine runWithEntrypoint:@"executableNameNotNull"]);
164
165 while (!signaled) {
166 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
167 }
168 }
169
170 #ifndef FLUTTER_RELEASE
171 TEST_F(FlutterEngineTest, Switches) {
172 setenv("FLUTTER_ENGINE_SWITCHES", "2", 1);
173 setenv("FLUTTER_ENGINE_SWITCH_1", "abc", 1);
174 setenv("FLUTTER_ENGINE_SWITCH_2", "foo=\"bar, baz\"", 1);
175
176 FlutterEngine* engine = GetFlutterEngine();
177 std::vector<std::string> switches = engine.switches;
178 ASSERT_EQ(switches.size(), 2UL);
179 EXPECT_EQ(switches[0], "--abc");
180 EXPECT_EQ(switches[1], "--foo=\"bar, baz\"");
181
182 unsetenv("FLUTTER_ENGINE_SWITCHES");
183 unsetenv("FLUTTER_ENGINE_SWITCH_1");
184 unsetenv("FLUTTER_ENGINE_SWITCH_2");
185 }
186 #endif // !FLUTTER_RELEASE
187
188 TEST_F(FlutterEngineTest, MessengerSend) {
189 FlutterEngine* engine = GetFlutterEngine();
190 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
191
192 NSData* test_message = [@"a message" dataUsingEncoding:NSUTF8StringEncoding];
193 bool called = false;
194
195 engine.embedderAPI.SendPlatformMessage = MOCK_ENGINE_PROC(
196 SendPlatformMessage, ([&called, test_message](auto engine, auto message) {
197 called = true;
198 EXPECT_STREQ(message->channel, "test");
199 EXPECT_EQ(memcmp(message->message, test_message.bytes, message->message_size), 0);
200 return kSuccess;
201 }));
202
203 [engine.binaryMessenger sendOnChannel:@"test" message:test_message];
204 EXPECT_TRUE(called);
205 }
206
207 TEST_F(FlutterEngineTest, CanLogToStdout) {
208 // Block until completion of print statement.
209 BOOL signaled = NO;
210 AddNativeCallback("SignalNativeTest",
211 CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { signaled = YES; }));
212
213 // Replace stdout stream buffer with our own.
214 FlutterStringOutputWriter* writer = [[FlutterStringOutputWriter alloc] init];
215 writer.expectedOutput = @"Hello logging";
216 FlutterLogger.outputWriter = writer;
217
218 // Launch the test entrypoint.
219 FlutterEngine* engine = GetFlutterEngine();
220 EXPECT_TRUE([engine runWithEntrypoint:@"canLogToStdout"]);
221 ASSERT_TRUE(engine.running);
222
223 while (!signaled) {
224 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
225 }
226
227 // Verify hello world was written to stdout.
228 EXPECT_TRUE(writer.gotExpectedOutput);
229 }
230
231 TEST_F(FlutterEngineTest, DISABLED_BackgroundIsBlack) {
232 FlutterEngine* engine = GetFlutterEngine();
233
234 // Latch to ensure the entire layer tree has been generated and presented.
235 BOOL signaled = NO;
236 AddNativeCallback("SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
237 CALayer* rootLayer = engine.viewController.flutterView.layer;
238 EXPECT_TRUE(rootLayer.backgroundColor != nil);
239 if (rootLayer.backgroundColor != nil) {
240 NSColor* actualBackgroundColor =
241 [NSColor colorWithCGColor:rootLayer.backgroundColor];
242 EXPECT_EQ(actualBackgroundColor, [NSColor blackColor]);
243 }
244 signaled = YES;
245 }));
246
247 // Launch the test entrypoint.
248 EXPECT_TRUE([engine runWithEntrypoint:@"backgroundTest"]);
249 ASSERT_TRUE(engine.running);
250
251 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
252 nibName:nil
253 bundle:nil];
254 [viewController loadView];
255 viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
256
257 while (!signaled) {
258 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
259 }
260 }
261
262 TEST_F(FlutterEngineTest, DISABLED_CanOverrideBackgroundColor) {
263 FlutterEngine* engine = GetFlutterEngine();
264
265 // Latch to ensure the entire layer tree has been generated and presented.
266 BOOL signaled = NO;
267 AddNativeCallback("SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
268 CALayer* rootLayer = engine.viewController.flutterView.layer;
269 EXPECT_TRUE(rootLayer.backgroundColor != nil);
270 if (rootLayer.backgroundColor != nil) {
271 NSColor* actualBackgroundColor =
272 [NSColor colorWithCGColor:rootLayer.backgroundColor];
273 EXPECT_EQ(actualBackgroundColor, [NSColor whiteColor]);
274 }
275 signaled = YES;
276 }));
277
278 // Launch the test entrypoint.
279 EXPECT_TRUE([engine runWithEntrypoint:@"backgroundTest"]);
280 ASSERT_TRUE(engine.running);
281
282 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
283 nibName:nil
284 bundle:nil];
285 [viewController loadView];
286 viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
287 viewController.flutterView.backgroundColor = [NSColor whiteColor];
288
289 while (!signaled) {
290 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
291 }
292 }
293
294 TEST_F(FlutterEngineTest, CanToggleAccessibility) {
295 FlutterEngine* engine = GetFlutterEngine();
296 // Capture the update callbacks before the embedder API initializes.
297 auto original_init = engine.embedderAPI.Initialize;
298 std::function<void(const FlutterSemanticsUpdate2*, void*)> update_semantics_callback;
299 engine.embedderAPI.Initialize = MOCK_ENGINE_PROC(
300 Initialize, ([&update_semantics_callback, &original_init](
301 size_t version, const FlutterRendererConfig* config,
302 const FlutterProjectArgs* args, void* user_data, auto engine_out) {
303 update_semantics_callback = args->update_semantics_callback2;
304 return original_init(version, config, args, user_data, engine_out);
305 }));
306 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
307 // Set up view controller.
308 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
309 nibName:nil
310 bundle:nil];
311 [viewController loadView];
312 // Enable the semantics.
313 bool enabled_called = false;
314 engine.embedderAPI.UpdateSemanticsEnabled =
315 MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&enabled_called](auto engine, bool enabled) {
316 enabled_called = enabled;
317 return kSuccess;
318 }));
319 engine.semanticsEnabled = YES;
320 EXPECT_TRUE(enabled_called);
321 // Send flutter semantics updates.
322 FlutterSemanticsNode2 root;
323 FlutterSemanticsFlags flags = FlutterSemanticsFlags{0};
324 FlutterSemanticsFlags child_flags = FlutterSemanticsFlags{0};
325 root.id = 0;
326 root.flags2 = &flags;
327 // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange)
328 root.actions = static_cast<FlutterSemanticsAction>(0);
329 root.text_selection_base = -1;
330 root.text_selection_extent = -1;
331 root.label = "root";
332 root.hint = "";
333 root.value = "";
334 root.increased_value = "";
335 root.decreased_value = "";
336 root.tooltip = "";
337 root.child_count = 1;
338 int32_t children[] = {1};
339 root.children_in_traversal_order = children;
340 root.custom_accessibility_actions_count = 0;
341 root.identifier = "";
342
343 FlutterSemanticsNode2 child1;
344 child1.id = 1;
345 child1.flags2 = &child_flags;
346 // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange)
347 child1.actions = static_cast<FlutterSemanticsAction>(0);
348 child1.text_selection_base = -1;
349 child1.text_selection_extent = -1;
350 child1.label = "child 1";
351 child1.hint = "";
352 child1.value = "";
353 child1.increased_value = "";
354 child1.decreased_value = "";
355 child1.tooltip = "";
356 child1.child_count = 0;
357 child1.custom_accessibility_actions_count = 0;
358 child1.identifier = "";
359
360 FlutterSemanticsUpdate2 update;
361 update.node_count = 2;
362 FlutterSemanticsNode2* nodes[] = {&root, &child1};
363 update.nodes = nodes;
364 update.custom_action_count = 0;
365 update_semantics_callback(&update, (__bridge void*)engine);
366
367 // Verify the accessibility tree is attached to the flutter view.
368 EXPECT_EQ([engine.viewController.flutterView.accessibilityChildren count], 1u);
369 NSAccessibilityElement* native_root = engine.viewController.flutterView.accessibilityChildren[0];
370 std::string root_label = [native_root.accessibilityLabel UTF8String];
371 EXPECT_TRUE(root_label == "root");
372 EXPECT_EQ(native_root.accessibilityRole, NSAccessibilityGroupRole);
373 EXPECT_EQ([native_root.accessibilityChildren count], 1u);
374 NSAccessibilityElement* native_child1 = native_root.accessibilityChildren[0];
375 std::string child1_value = [native_child1.accessibilityValue UTF8String];
376 EXPECT_TRUE(child1_value == "child 1");
377 EXPECT_EQ(native_child1.accessibilityRole, NSAccessibilityStaticTextRole);
378 EXPECT_EQ([native_child1.accessibilityChildren count], 0u);
379 // Disable the semantics.
380 bool semanticsEnabled = true;
381 engine.embedderAPI.UpdateSemanticsEnabled =
382 MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&semanticsEnabled](auto engine, bool enabled) {
383 semanticsEnabled = enabled;
384 return kSuccess;
385 }));
386 engine.semanticsEnabled = NO;
387 EXPECT_FALSE(semanticsEnabled);
388 // Verify the accessibility tree is removed from the view.
389 EXPECT_EQ([engine.viewController.flutterView.accessibilityChildren count], 0u);
390
391 [engine setViewController:nil];
392 }
393
394 TEST_F(FlutterEngineTest, CanToggleAccessibilityWhenHeadless) {
395 FlutterEngine* engine = GetFlutterEngine();
396 // Capture the update callbacks before the embedder API initializes.
397 auto original_init = engine.embedderAPI.Initialize;
398 std::function<void(const FlutterSemanticsUpdate2*, void*)> update_semantics_callback;
399 engine.embedderAPI.Initialize = MOCK_ENGINE_PROC(
400 Initialize, ([&update_semantics_callback, &original_init](
401 size_t version, const FlutterRendererConfig* config,
402 const FlutterProjectArgs* args, void* user_data, auto engine_out) {
403 update_semantics_callback = args->update_semantics_callback2;
404 return original_init(version, config, args, user_data, engine_out);
405 }));
406 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
407
408 // Enable the semantics without attaching a view controller.
409 bool enabled_called = false;
410 engine.embedderAPI.UpdateSemanticsEnabled =
411 MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&enabled_called](auto engine, bool enabled) {
412 enabled_called = enabled;
413 return kSuccess;
414 }));
415 engine.semanticsEnabled = YES;
416 EXPECT_TRUE(enabled_called);
417 // Send flutter semantics updates.
418 FlutterSemanticsNode2 root;
419 FlutterSemanticsFlags flags = FlutterSemanticsFlags{0};
420 FlutterSemanticsFlags child_flags = FlutterSemanticsFlags{0};
421 root.id = 0;
422 root.flags2 = &flags;
423 // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange)
424 root.actions = static_cast<FlutterSemanticsAction>(0);
425 root.text_selection_base = -1;
426 root.text_selection_extent = -1;
427 root.label = "root";
428 root.hint = "";
429 root.value = "";
430 root.increased_value = "";
431 root.decreased_value = "";
432 root.tooltip = "";
433 root.child_count = 1;
434 int32_t children[] = {1};
435 root.children_in_traversal_order = children;
436 root.custom_accessibility_actions_count = 0;
437
438 FlutterSemanticsNode2 child1;
439 child1.id = 1;
440 child1.flags2 = &child_flags;
441 // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange)
442 child1.actions = static_cast<FlutterSemanticsAction>(0);
443 child1.text_selection_base = -1;
444 child1.text_selection_extent = -1;
445 child1.label = "child 1";
446 child1.hint = "";
447 child1.value = "";
448 child1.increased_value = "";
449 child1.decreased_value = "";
450 child1.tooltip = "";
451 child1.child_count = 0;
452 child1.custom_accessibility_actions_count = 0;
453
454 FlutterSemanticsUpdate2 update;
455 update.node_count = 2;
456 FlutterSemanticsNode2* nodes[] = {&root, &child1};
457 update.nodes = nodes;
458 update.custom_action_count = 0;
459 // This call updates semantics for the implicit view, which does not exist,
460 // and therefore this call is invalid. But the engine should not crash.
461 update_semantics_callback(&update, (__bridge void*)engine);
462
463 // No crashes.
464 EXPECT_EQ(engine.viewController, nil);
465
466 // Disable the semantics.
467 bool semanticsEnabled = true;
468 engine.embedderAPI.UpdateSemanticsEnabled =
469 MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&semanticsEnabled](auto engine, bool enabled) {
470 semanticsEnabled = enabled;
471 return kSuccess;
472 }));
473 engine.semanticsEnabled = NO;
474 EXPECT_FALSE(semanticsEnabled);
475 // Still no crashes
476 EXPECT_EQ(engine.viewController, nil);
477 }
478
479 TEST_F(FlutterEngineTest, ProducesAccessibilityTreeWhenAddingViews) {
480 FlutterEngine* engine = GetFlutterEngine();
481 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
482
483 // Enable the semantics without attaching a view controller.
484 bool enabled_called = false;
485 engine.embedderAPI.UpdateSemanticsEnabled =
486 MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&enabled_called](auto engine, bool enabled) {
487 enabled_called = enabled;
488 return kSuccess;
489 }));
490 engine.semanticsEnabled = YES;
491 EXPECT_TRUE(enabled_called);
492
493 EXPECT_EQ(engine.viewController, nil);
494
495 // Assign the view controller after enabling semantics
496 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
497 nibName:nil
498 bundle:nil];
499 engine.viewController = viewController;
500
501 EXPECT_NE(viewController.accessibilityBridge.lock(), nullptr);
502 }
503
504 TEST_F(FlutterEngineTest, NativeCallbacks) {
505 BOOL latch_called = NO;
506 AddNativeCallback("SignalNativeTest",
507 CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch_called = YES; }));
508
509 FlutterEngine* engine = GetFlutterEngine();
510 EXPECT_TRUE([engine runWithEntrypoint:@"nativeCallback"]);
511 ASSERT_TRUE(engine.running);
512
513 while (!latch_called) {
514 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
515 }
516 ASSERT_TRUE(latch_called);
517 }
518
519 TEST_F(FlutterEngineTest, Compositor) {
520 NSString* fixtures = @(flutter::testing::GetFixturesPath());
521 FlutterDartProject* project = [[FlutterDartProject alloc]
522 initWithAssetsPath:fixtures
523 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
524 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
525
526 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
527 nibName:nil
528 bundle:nil];
529 [viewController loadView];
530 [viewController viewDidLoad];
531 viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
532
533 EXPECT_TRUE([engine runWithEntrypoint:@"canCompositePlatformViews"]);
534
535 [engine.platformViewController registerViewFactory:[[TestPlatformViewFactory alloc] init]
536 withId:@"factory_id"];
537 [engine.platformViewController
538 handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"create"
539 arguments:@{
540 @"id" : @(42),
541 @"viewType" : @"factory_id",
542 }]
543 result:^(id result){
544 }];
545
546 // Wait up to 1 second for Flutter to emit a frame.
547 CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
548 CALayer* rootLayer = viewController.flutterView.layer;
549 while (rootLayer.sublayers.count == 0) {
550 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
551 if (CFAbsoluteTimeGetCurrent() - start > 1) {
552 break;
553 }
554 }
555
556 // There are two layers with Flutter contents and one view
557 EXPECT_EQ(rootLayer.sublayers.count, 2u);
558 EXPECT_EQ(viewController.flutterView.subviews.count, 1u);
559
560 // TODO(gw280): add support for screenshot tests in this test harness
561
562 [engine shutDownEngine];
563 }
564
565 TEST_F(FlutterEngineTest, CompositorIgnoresUnknownView) {
566 FlutterEngine* engine = GetFlutterEngine();
567 auto original_init = engine.embedderAPI.Initialize;
568 ::FlutterCompositor compositor;
569 engine.embedderAPI.Initialize = MOCK_ENGINE_PROC(
570 Initialize, ([&compositor, &original_init](
571 size_t version, const FlutterRendererConfig* config,
572 const FlutterProjectArgs* args, void* user_data, auto engine_out) {
573 compositor = *args->compositor;
574 return original_init(version, config, args, user_data, engine_out);
575 }));
576
577 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
578 nibName:nil
579 bundle:nil];
580 [viewController loadView];
581
582 EXPECT_TRUE([engine runWithEntrypoint:@"empty"]);
583
584 FlutterBackingStoreConfig config = {
585 .struct_size = sizeof(FlutterBackingStoreConfig),
586 .size = FlutterSize{10, 10},
587 };
588 FlutterBackingStore backing_store = {};
589 EXPECT_NE(compositor.create_backing_store_callback, nullptr);
590 EXPECT_TRUE(
591 compositor.create_backing_store_callback(&config, &backing_store, compositor.user_data));
592
593 FlutterLayer layer{
594 .type = kFlutterLayerContentTypeBackingStore,
595 .backing_store = &backing_store,
596 };
597 std::vector<FlutterLayer*> layers = {&layer};
598
599 FlutterPresentViewInfo info = {
600 .struct_size = sizeof(FlutterPresentViewInfo),
601 .view_id = 123,
602 .layers = const_cast<const FlutterLayer**>(layers.data()),
603 .layers_count = 1,
604 .user_data = compositor.user_data,
605 };
606 EXPECT_NE(compositor.present_view_callback, nullptr);
607 EXPECT_FALSE(compositor.present_view_callback(&info));
608 EXPECT_TRUE(compositor.collect_backing_store_callback(&backing_store, compositor.user_data));
609
610 (void)viewController;
611 [engine shutDownEngine];
612 }
613
614 TEST_F(FlutterEngineTest, DartEntrypointArguments) {
615 NSString* fixtures = @(flutter::testing::GetFixturesPath());
616 FlutterDartProject* project = [[FlutterDartProject alloc]
617 initWithAssetsPath:fixtures
618 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
619
620 project.dartEntrypointArguments = @[ @"arg1", @"arg2" ];
621 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
622
623 bool called = false;
624 auto original_init = engine.embedderAPI.Initialize;
625 engine.embedderAPI.Initialize = MOCK_ENGINE_PROC(
626 Initialize, ([&called, &original_init](size_t version, const FlutterRendererConfig* config,
627 const FlutterProjectArgs* args, void* user_data,
628 FLUTTER_API_SYMBOL(FlutterEngine) * engine_out) {
629 called = true;
630 EXPECT_EQ(args->dart_entrypoint_argc, 2);
631 NSString* arg1 = [[NSString alloc] initWithCString:args->dart_entrypoint_argv[0]
632 encoding:NSUTF8StringEncoding];
633 NSString* arg2 = [[NSString alloc] initWithCString:args->dart_entrypoint_argv[1]
634 encoding:NSUTF8StringEncoding];
635
636 EXPECT_TRUE([arg1 isEqualToString:@"arg1"]);
637 EXPECT_TRUE([arg2 isEqualToString:@"arg2"]);
638
639 return original_init(version, config, args, user_data, engine_out);
640 }));
641
642 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
643 EXPECT_TRUE(called);
644 [engine shutDownEngine];
645 }
646
647 // Verify that the engine is not retained indirectly via the binary messenger held by channels and
648 // plugins. Previously, FlutterEngine.binaryMessenger returned the engine itself, and thus plugins
649 // could cause a retain cycle, preventing the engine from being deallocated.
650 // FlutterEngine.binaryMessenger now returns a FlutterBinaryMessengerRelay whose weak pointer back
651 // to the engine is cleared when the engine is deallocated.
652 // Issue: https://github.com/flutter/flutter/issues/116445
653 TEST_F(FlutterEngineTest, FlutterBinaryMessengerDoesNotRetainEngine) {
654 __weak FlutterEngine* weakEngine;
655 id<FlutterBinaryMessenger> binaryMessenger = nil;
656 @autoreleasepool {
657 // Create a test engine.
658 NSString* fixtures = @(flutter::testing::GetFixturesPath());
659 FlutterDartProject* project = [[FlutterDartProject alloc]
660 initWithAssetsPath:fixtures
661 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
662 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
663 project:project
664 allowHeadlessExecution:YES];
665 weakEngine = engine;
666 binaryMessenger = engine.binaryMessenger;
667 }
668
669 // Once the engine has been deallocated, verify the weak engine pointer is nil, and thus not
670 // retained by the relay.
671 EXPECT_NE(binaryMessenger, nil);
672 EXPECT_EQ(weakEngine, nil);
673 }
674
675 // Verify that the engine is not retained indirectly via the texture registry held by plugins.
676 // Issue: https://github.com/flutter/flutter/issues/116445
677 TEST_F(FlutterEngineTest, FlutterTextureRegistryDoesNotReturnEngine) {
678 __weak FlutterEngine* weakEngine;
679 id<FlutterTextureRegistry> textureRegistry;
680 @autoreleasepool {
681 // Create a test engine.
682 NSString* fixtures = @(flutter::testing::GetFixturesPath());
683 FlutterDartProject* project = [[FlutterDartProject alloc]
684 initWithAssetsPath:fixtures
685 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
686 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
687 project:project
688 allowHeadlessExecution:YES];
689 id<FlutterPluginRegistrar> registrar = [engine registrarForPlugin:@"MyPlugin"];
690 textureRegistry = registrar.textures;
691 }
692
693 // Once the engine has been deallocated, verify the weak engine pointer is nil, and thus not
694 // retained via the texture registry.
695 EXPECT_NE(textureRegistry, nil);
696 EXPECT_EQ(weakEngine, nil);
697 }
698
699 TEST_F(FlutterEngineTest, PublishedValueNilForUnknownPlugin) {
700 NSString* fixtures = @(flutter::testing::GetFixturesPath());
701 FlutterDartProject* project = [[FlutterDartProject alloc]
702 initWithAssetsPath:fixtures
703 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
704 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
705 project:project
706 allowHeadlessExecution:YES];
707
708 EXPECT_EQ([engine valuePublishedByPlugin:@"NoSuchPlugin"], nil);
709 }
710
711 TEST_F(FlutterEngineTest, PublishedValueNSNullIfNoPublishedValue) {
712 NSString* fixtures = @(flutter::testing::GetFixturesPath());
713 FlutterDartProject* project = [[FlutterDartProject alloc]
714 initWithAssetsPath:fixtures
715 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
716 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
717 project:project
718 allowHeadlessExecution:YES];
719 NSString* pluginName = @"MyPlugin";
720 // Request the registarar to register the plugin as existing.
721 [engine registrarForPlugin:pluginName];
722
723 // The documented behavior is that a plugin that exists but hasn't published
724 // anything returns NSNull, rather than nil, as on iOS.
725 EXPECT_EQ([engine valuePublishedByPlugin:pluginName], [NSNull null]);
726 }
727
728 TEST_F(FlutterEngineTest, PublishedValueReturnsLastPublished) {
729 NSString* fixtures = @(flutter::testing::GetFixturesPath());
730 FlutterDartProject* project = [[FlutterDartProject alloc]
731 initWithAssetsPath:fixtures
732 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
733 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
734 project:project
735 allowHeadlessExecution:YES];
736 NSString* pluginName = @"MyPlugin";
737 id<FlutterPluginRegistrar> registrar = [engine registrarForPlugin:pluginName];
738
739 NSString* firstValue = @"A published value";
740 NSArray* secondValue = @[ @"A different published value" ];
741
742 [registrar publish:firstValue];
743 EXPECT_EQ([engine valuePublishedByPlugin:pluginName], firstValue);
744
745 [registrar publish:secondValue];
746 EXPECT_EQ([engine valuePublishedByPlugin:pluginName], secondValue);
747 }
748
749 TEST_F(FlutterEngineTest, RegistrarForwardViewControllerLookUpToEngine) {
750 NSString* fixtures = @(flutter::testing::GetFixturesPath());
751 FlutterDartProject* project = [[FlutterDartProject alloc]
752 initWithAssetsPath:fixtures
753 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
754 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
755
756 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
757 nibName:nil
758 bundle:nil];
759 id<FlutterPluginRegistrar> registrar = [engine registrarForPlugin:@"MyPlugin"];
760
761 EXPECT_EQ([registrar viewController], viewController);
762 }
763
764 // If a channel overrides a previous channel with the same name, cleaning
765 // the previous channel should not affect the new channel.
766 //
767 // This is important when recreating classes that uses a channel, because the
768 // new instance would create the channel before the first class is deallocated
769 // and clears the channel.
770 TEST_F(FlutterEngineTest, MessengerCleanupConnectionWorks) {
771 FlutterEngine* engine = GetFlutterEngine();
772 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
773
774 NSString* channel = @"_test_";
775 NSData* channel_data = [channel dataUsingEncoding:NSUTF8StringEncoding];
776
777 // Mock SendPlatformMessage so that if a message is sent to
778 // "test/send_message", act as if the framework has sent an empty message to
779 // the channel marked by the `sendOnChannel:message:` call's message.
780 engine.embedderAPI.SendPlatformMessage = MOCK_ENGINE_PROC(
781 SendPlatformMessage, ([](auto engine_, auto message_) {
782 if (strcmp(message_->channel, "test/send_message") == 0) {
783 // The simplest message that is acceptable to a method channel.
784 std::string message = R"|({"method": "a"})|";
785 std::string channel(reinterpret_cast<const char*>(message_->message),
786 message_->message_size);
787 reinterpret_cast<EmbedderEngine*>(engine_)
788 ->GetShell()
789 .GetPlatformView()
790 ->HandlePlatformMessage(std::make_unique<PlatformMessage>(
791 channel.c_str(), fml::MallocMapping::Copy(message.c_str(), message.length()),
792 fml::RefPtr<PlatformMessageResponse>()));
793 }
794 return kSuccess;
795 }));
796
797 __block int record = 0;
798
799 FlutterMethodChannel* channel1 =
800 [FlutterMethodChannel methodChannelWithName:channel
801 binaryMessenger:engine.binaryMessenger
802 codec:[FlutterJSONMethodCodec sharedInstance]];
803 [channel1 setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
804 record += 1;
805 }];
806
807 [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data];
808 EXPECT_EQ(record, 1);
809
810 FlutterMethodChannel* channel2 =
811 [FlutterMethodChannel methodChannelWithName:channel
812 binaryMessenger:engine.binaryMessenger
813 codec:[FlutterJSONMethodCodec sharedInstance]];
814 [channel2 setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
815 record += 10;
816 }];
817
818 [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data];
819 EXPECT_EQ(record, 11);
820
821 [channel1 setMethodCallHandler:nil];
822
823 [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data];
824 EXPECT_EQ(record, 21);
825 }
826
827 TEST_F(FlutterEngineTest, HasStringsWhenPasteboardEmpty) {
828 id engineMock = CreateMockFlutterEngine(nil);
829
830 // Call hasStrings and expect it to be false.
831 __block bool calledAfterClear = false;
832 __block bool valueAfterClear;
833 FlutterResult resultAfterClear = ^(id result) {
834 calledAfterClear = true;
835 NSNumber* valueNumber = [result valueForKey:@"value"];
836 valueAfterClear = [valueNumber boolValue];
837 };
838 FlutterMethodCall* methodCallAfterClear =
839 [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil];
840 [engineMock handleMethodCall:methodCallAfterClear result:resultAfterClear];
841 EXPECT_TRUE(calledAfterClear);
842 EXPECT_FALSE(valueAfterClear);
843 }
844
845 TEST_F(FlutterEngineTest, HasStringsWhenPasteboardFull) {
846 id engineMock = CreateMockFlutterEngine(@"some string");
847
848 // Call hasStrings and expect it to be true.
849 __block bool called = false;
850 __block bool value;
851 FlutterResult result = ^(id result) {
852 called = true;
853 NSNumber* valueNumber = [result valueForKey:@"value"];
854 value = [valueNumber boolValue];
855 };
856 FlutterMethodCall* methodCall =
857 [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil];
858 [engineMock handleMethodCall:methodCall result:result];
859 EXPECT_TRUE(called);
860 EXPECT_TRUE(value);
861 }
862
863 TEST_F(FlutterEngineTest, ResponseAfterEngineDied) {
864 FlutterEngine* engine = GetFlutterEngine();
865 FlutterBasicMessageChannel* channel = [[FlutterBasicMessageChannel alloc]
866 initWithName:@"foo"
867 binaryMessenger:engine.binaryMessenger
868 codec:[FlutterStandardMessageCodec sharedInstance]];
869 __block BOOL didCallCallback = NO;
870 [channel setMessageHandler:^(id message, FlutterReply callback) {
871 ShutDownEngine();
872 callback(nil);
873 didCallCallback = YES;
874 }];
875 EXPECT_TRUE([engine runWithEntrypoint:@"sendFooMessage"]);
876 engine = nil;
877
878 while (!didCallCallback) {
879 [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
880 }
881 }
882
883 TEST_F(FlutterEngineTest, ResponseFromBackgroundThread) {
884 FlutterEngine* engine = GetFlutterEngine();
885 FlutterBasicMessageChannel* channel = [[FlutterBasicMessageChannel alloc]
886 initWithName:@"foo"
887 binaryMessenger:engine.binaryMessenger
888 codec:[FlutterStandardMessageCodec sharedInstance]];
889 __block BOOL didCallCallback = NO;
890 [channel setMessageHandler:^(id message, FlutterReply callback) {
891 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
892 callback(nil);
893 dispatch_async(dispatch_get_main_queue(), ^{
894 didCallCallback = YES;
895 });
896 });
897 }];
898 EXPECT_TRUE([engine runWithEntrypoint:@"sendFooMessage"]);
899
900 while (!didCallCallback) {
901 [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
902 }
903 }
904
905 TEST_F(FlutterEngineTest, CanGetEngineForId) {
906 FlutterEngine* engine = GetFlutterEngine();
907
908 BOOL signaled = NO;
909 std::optional<int64_t> engineId;
910 AddNativeCallback("NotifyEngineId", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
911 const auto argument = Dart_GetNativeArgument(args, 0);
912 if (!Dart_IsNull(argument)) {
913 const auto id = tonic::DartConverter<int64_t>::FromDart(argument);
914 engineId = id;
915 }
916 signaled = YES;
917 }));
918
919 EXPECT_TRUE([engine runWithEntrypoint:@"testEngineId"]);
920 while (!signaled) {
921 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
922 }
923
924 EXPECT_TRUE(engineId.has_value());
925 if (!engineId.has_value()) {
926 return;
927 }
928 EXPECT_EQ(engine, [FlutterEngine engineForIdentifier:*engineId]);
929 ShutDownEngine();
930 }
931
932 TEST_F(FlutterEngineTest, ResizeSynchronizerNotBlockingRasterThreadAfterShutdown) {
933 FlutterResizeSynchronizer* threadSynchronizer = [[FlutterResizeSynchronizer alloc] init];
934 [threadSynchronizer shutDown];
935
936 std::thread rasterThread([&threadSynchronizer] {
937 [threadSynchronizer performCommitForSize:CGSizeMake(100, 100)
938 afterDelay:0
939 notify:^{
940 }];
941 });
942
943 rasterThread.join();
944 }
945
946 TEST_F(FlutterEngineTest, ManageControllersIfInitiatedByController) {
947 NSString* fixtures = @(flutter::testing::GetFixturesPath());
948 FlutterDartProject* project = [[FlutterDartProject alloc]
949 initWithAssetsPath:fixtures
950 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
951
952 FlutterEngine* engine;
953 FlutterViewController* viewController1;
954
955 @autoreleasepool {
956 // Create FVC1.
957 viewController1 = [[FlutterViewController alloc] initWithProject:project];
958 EXPECT_EQ(viewController1.viewIdentifier, 0ll);
959
960 engine = viewController1.engine;
961 engine.viewController = nil;
962
963 // Create FVC2 based on the same engine.
964 FlutterViewController* viewController2 = [[FlutterViewController alloc] initWithEngine:engine
965 nibName:nil
966 bundle:nil];
967 EXPECT_EQ(engine.viewController, viewController2);
968 }
969 // FVC2 is deallocated but FVC1 is retained.
970
971 EXPECT_EQ(engine.viewController, nil);
972
973 engine.viewController = viewController1;
974 EXPECT_EQ(engine.viewController, viewController1);
975 EXPECT_EQ(viewController1.viewIdentifier, 0ll);
976 }
977
978 TEST_F(FlutterEngineTest, ManageControllersIfInitiatedByEngine) {
979 // Don't create the engine with `CreateMockFlutterEngine`, because it adds
980 // additional references to FlutterViewControllers, which is crucial to this
981 // test case.
982 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"io.flutter"
983 project:nil
984 allowHeadlessExecution:NO];
985 FlutterViewController* viewController1;
986
987 @autoreleasepool {
988 viewController1 = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
989 EXPECT_EQ(viewController1.viewIdentifier, 0ll);
990 EXPECT_EQ(engine.viewController, viewController1);
991
992 engine.viewController = nil;
993
994 FlutterViewController* viewController2 = [[FlutterViewController alloc] initWithEngine:engine
995 nibName:nil
996 bundle:nil];
997 EXPECT_EQ(viewController2.viewIdentifier, 0ll);
998 EXPECT_EQ(engine.viewController, viewController2);
999 }
1000 // FVC2 is deallocated but FVC1 is retained.
1001
1002 EXPECT_EQ(engine.viewController, nil);
1003
1004 engine.viewController = viewController1;
1005 EXPECT_EQ(engine.viewController, viewController1);
1006 EXPECT_EQ(viewController1.viewIdentifier, 0ll);
1007 }
1008
1009 TEST_F(FlutterEngineTest, RemovingViewDisposesCompositorResources) {
1010 NSString* fixtures = @(flutter::testing::GetFixturesPath());
1011 FlutterDartProject* project = [[FlutterDartProject alloc]
1012 initWithAssetsPath:fixtures
1013 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
1014 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
1015
1016 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
1017 nibName:nil
1018 bundle:nil];
1019 [viewController loadView];
1020 [viewController viewDidLoad];
1021 viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
1022
1023 EXPECT_TRUE([engine runWithEntrypoint:@"drawIntoAllViews"]);
1024 // Wait up to 1 second for Flutter to emit a frame.
1025 CFTimeInterval start = CACurrentMediaTime();
1026 while (engine.macOSCompositor->DebugNumViews() == 0) {
1027 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
1028 if (CACurrentMediaTime() - start > 1) {
1029 break;
1030 }
1031 }
1032
1033 EXPECT_EQ(engine.macOSCompositor->DebugNumViews(), 1u);
1034
1035 engine.viewController = nil;
1036 EXPECT_EQ(engine.macOSCompositor->DebugNumViews(), 0u);
1037
1038 [engine shutDownEngine];
1039 engine = nil;
1040 }
1041
1042 TEST_F(FlutterEngineTest, HandlesTerminationRequest) {
1043 id engineMock = CreateMockFlutterEngine(nil);
1044 __block NSString* nextResponse = @"exit";
1045 __block BOOL triedToTerminate = NO;
1046 FlutterEngineTerminationHandler* terminationHandler =
1047 [[FlutterEngineTerminationHandler alloc] initWithEngine:engineMock
1048 terminator:^(id sender) {
1049 triedToTerminate = TRUE;
1050 // Don't actually terminate, of course.
1051 }];
1052 OCMStub([engineMock terminationHandler]).andReturn(terminationHandler);
1053 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1054 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1055 [engineMock binaryMessenger])
1056 .andReturn(binaryMessengerMock);
1057 OCMStub([engineMock sendOnChannel:@"flutter/platform"
1058 message:[OCMArg any]
1059 binaryReply:[OCMArg any]])
1060 .andDo((^(NSInvocation* invocation) {
1061 [invocation retainArguments];
1062 FlutterBinaryReply callback;
1063 NSData* returnedMessage;
1064 [invocation getArgument:&callback atIndex:4];
1065 if ([nextResponse isEqualToString:@"error"]) {
1066 FlutterError* errorResponse = [FlutterError errorWithCode:@"Error"
1067 message:@"Failed"
1068 details:@"Details"];
1069 returnedMessage =
1070 [[FlutterJSONMethodCodec sharedInstance] encodeErrorEnvelope:errorResponse];
1071 } else {
1072 NSDictionary* responseDict = @{@"response" : nextResponse};
1073 returnedMessage =
1074 [[FlutterJSONMethodCodec sharedInstance] encodeSuccessEnvelope:responseDict];
1075 }
1076 callback(returnedMessage);
1077 }));
1078 __block NSString* calledAfterTerminate = @"";
1079 FlutterResult appExitResult = ^(id result) {
1080 NSDictionary* resultDict = result;
1081 calledAfterTerminate = resultDict[@"response"];
1082 };
1083 FlutterMethodCall* methodExitApplication =
1084 [FlutterMethodCall methodCallWithMethodName:@"System.exitApplication"
1085 arguments:@{@"type" : @"cancelable"}];
1086
1087 // Always terminate when the binding isn't ready (which is the default).
1088 triedToTerminate = NO;
1089 calledAfterTerminate = @"";
1090 nextResponse = @"cancel";
1091 [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1092 EXPECT_STREQ([calledAfterTerminate UTF8String], "");
1093 EXPECT_TRUE(triedToTerminate);
1094
1095 // Once the binding is ready, handle the request.
1096 terminationHandler.acceptingRequests = YES;
1097 triedToTerminate = NO;
1098 calledAfterTerminate = @"";
1099 nextResponse = @"exit";
1100 [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1101 EXPECT_STREQ([calledAfterTerminate UTF8String], "exit");
1102 EXPECT_TRUE(triedToTerminate);
1103
1104 triedToTerminate = NO;
1105 calledAfterTerminate = @"";
1106 nextResponse = @"cancel";
1107 [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1108 EXPECT_STREQ([calledAfterTerminate UTF8String], "cancel");
1109 EXPECT_FALSE(triedToTerminate);
1110
1111 // Check that it doesn't crash on error.
1112 triedToTerminate = NO;
1113 calledAfterTerminate = @"";
1114 nextResponse = @"error";
1115 [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1116 EXPECT_STREQ([calledAfterTerminate UTF8String], "");
1117 EXPECT_TRUE(triedToTerminate);
1118 }
1119
1120 TEST_F(FlutterEngineTest, IgnoresTerminationRequestIfNotFlutterAppDelegate) {
1121 id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1122 id<NSApplicationDelegate> plainDelegate = [[PlainAppDelegate alloc] init];
1123 [NSApplication sharedApplication].delegate = plainDelegate;
1124
1125 // Creating the engine shouldn't fail here, even though the delegate isn't a
1126 // FlutterAppDelegate.
1127 CreateMockFlutterEngine(nil);
1128
1129 // Asking to terminate the app should cancel.
1130 EXPECT_EQ([[[NSApplication sharedApplication] delegate] applicationShouldTerminate:NSApp],
1131 NSTerminateCancel);
1132
1133 [NSApplication sharedApplication].delegate = previousDelegate;
1134 }
1135
1136 TEST_F(FlutterEngineTest, HandleAccessibilityEvent) {
1137 __block BOOL announced = NO;
1138 id engineMock = CreateMockFlutterEngine(nil);
1139
1140 OCMStub([engineMock announceAccessibilityMessage:[OCMArg any]
1141 withPriority:NSAccessibilityPriorityMedium])
1142 .andDo((^(NSInvocation* invocation) {
1143 announced = TRUE;
1144 [invocation retainArguments];
1145 NSString* message;
1146 [invocation getArgument:&message atIndex:2];
1147 EXPECT_EQ(message, @"error message");
1148 }));
1149
1150 NSDictionary<NSString*, id>* annotatedEvent =
1151 @{@"type" : @"announce",
1152 @"data" : @{@"message" : @"error message"}};
1153
1154 [engineMock handleAccessibilityEvent:annotatedEvent];
1155
1156 EXPECT_TRUE(announced);
1157 }
1158
1159 TEST_F(FlutterEngineTest, HandleLifecycleStates) API_AVAILABLE(macos(10.9)) {
1160 __block flutter::AppLifecycleState sentState;
1161 id engineMock = CreateMockFlutterEngine(nil);
1162
1163 // Have to enumerate all the values because OCMStub can't capture
1164 // non-Objective-C object arguments.
1165 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kDetached])
1166 .andDo((^(NSInvocation* invocation) {
1167 sentState = flutter::AppLifecycleState::kDetached;
1168 }));
1169 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kResumed])
1170 .andDo((^(NSInvocation* invocation) {
1171 sentState = flutter::AppLifecycleState::kResumed;
1172 }));
1173 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kInactive])
1174 .andDo((^(NSInvocation* invocation) {
1175 sentState = flutter::AppLifecycleState::kInactive;
1176 }));
1177 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kHidden])
1178 .andDo((^(NSInvocation* invocation) {
1179 sentState = flutter::AppLifecycleState::kHidden;
1180 }));
1181 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kPaused])
1182 .andDo((^(NSInvocation* invocation) {
1183 sentState = flutter::AppLifecycleState::kPaused;
1184 }));
1185
1186 __block NSApplicationOcclusionState visibility = NSApplicationOcclusionStateVisible;
1187 id mockApplication = OCMPartialMock([NSApplication sharedApplication]);
1188 OCMStub((NSApplicationOcclusionState)[mockApplication occlusionState])
1189 .andDo(^(NSInvocation* invocation) {
1190 [invocation setReturnValue:&visibility];
1191 });
1192
1193 NSNotification* willBecomeActive =
1194 [[NSNotification alloc] initWithName:NSApplicationWillBecomeActiveNotification
1195 object:nil
1196 userInfo:nil];
1197 NSNotification* willResignActive =
1198 [[NSNotification alloc] initWithName:NSApplicationWillResignActiveNotification
1199 object:nil
1200 userInfo:nil];
1201
1202 NSNotification* didChangeOcclusionState;
1203 didChangeOcclusionState =
1204 [[NSNotification alloc] initWithName:NSApplicationDidChangeOcclusionStateNotification
1205 object:nil
1206 userInfo:nil];
1207
1208 [engineMock handleDidChangeOcclusionState:didChangeOcclusionState];
1209 EXPECT_EQ(sentState, flutter::AppLifecycleState::kInactive);
1210
1211 [engineMock handleWillBecomeActive:willBecomeActive];
1212 EXPECT_EQ(sentState, flutter::AppLifecycleState::kResumed);
1213
1214 [engineMock handleWillResignActive:willResignActive];
1215 EXPECT_EQ(sentState, flutter::AppLifecycleState::kInactive);
1216
1217 visibility = 0;
1218 [engineMock handleDidChangeOcclusionState:didChangeOcclusionState];
1219 EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden);
1220
1221 [engineMock handleWillBecomeActive:willBecomeActive];
1222 EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden);
1223
1224 [engineMock handleWillResignActive:willResignActive];
1225 EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden);
1226
1227 [mockApplication stopMocking];
1228 }
1229
1230 TEST_F(FlutterEngineTest, ForwardsPluginDelegateRegistration) {
1231 id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1232 FakeLifecycleProvider* fakeAppDelegate = [[FakeLifecycleProvider alloc] init];
1233 [NSApplication sharedApplication].delegate = fakeAppDelegate;
1234
1235 FakeAppDelegatePlugin* plugin = [[FakeAppDelegatePlugin alloc] init];
1236 FlutterEngine* engine = CreateMockFlutterEngine(nil);
1237
1238 [[engine registrarForPlugin:@"TestPlugin"] addApplicationDelegate:plugin];
1239
1240 EXPECT_TRUE([fakeAppDelegate hasDelegate:plugin]);
1241
1242 [NSApplication sharedApplication].delegate = previousDelegate;
1243 }
1244
1245 TEST_F(FlutterEngineTest, UnregistersPluginsOnEngineDestruction) {
1246 id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1247 FakeLifecycleProvider* fakeAppDelegate = [[FakeLifecycleProvider alloc] init];
1248 [NSApplication sharedApplication].delegate = fakeAppDelegate;
1249
1250 FakeAppDelegatePlugin* plugin = [[FakeAppDelegatePlugin alloc] init];
1251
1252 @autoreleasepool {
1253 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil];
1254
1255 [[engine registrarForPlugin:@"TestPlugin"] addApplicationDelegate:plugin];
1256 EXPECT_TRUE([fakeAppDelegate hasDelegate:plugin]);
1257 }
1258
1259 // When the engine is released, it should unregister any plugins it had
1260 // registered on its behalf.
1261 EXPECT_FALSE([fakeAppDelegate hasDelegate:plugin]);
1262
1263 [NSApplication sharedApplication].delegate = previousDelegate;
1264 }
1265
1266 TEST_F(FlutterEngineTest, RunWithEntrypointUpdatesDisplayConfig) {
1267 BOOL updated = NO;
1268 FlutterEngine* engine = GetFlutterEngine();
1269 auto original_update_displays = engine.embedderAPI.NotifyDisplayUpdate;
1270 engine.embedderAPI.NotifyDisplayUpdate = MOCK_ENGINE_PROC(
1271 NotifyDisplayUpdate, ([&updated, &original_update_displays](
1272 auto engine, auto update_type, auto* displays, auto display_count) {
1273 updated = YES;
1274 return original_update_displays(engine, update_type, displays, display_count);
1275 }));
1276
1277 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
1278 EXPECT_TRUE(updated);
1279
1280 updated = NO;
1281 [[NSNotificationCenter defaultCenter]
1282 postNotificationName:NSApplicationDidChangeScreenParametersNotification
1283 object:nil];
1284 EXPECT_TRUE(updated);
1285 }
1286
1287 TEST_F(FlutterEngineTest, NotificationsUpdateDisplays) {
1288 BOOL updated = NO;
1289 FlutterEngine* engine = GetFlutterEngine();
1290 auto original_set_viewport_metrics = engine.embedderAPI.SendWindowMetricsEvent;
1291 engine.embedderAPI.SendWindowMetricsEvent = MOCK_ENGINE_PROC(
1292 SendWindowMetricsEvent,
1293 ([&updated, &original_set_viewport_metrics](auto engine, auto* window_metrics) {
1294 updated = YES;
1295 return original_set_viewport_metrics(engine, window_metrics);
1296 }));
1297
1298 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
1299
1300 updated = NO;
1301 [[NSNotificationCenter defaultCenter] postNotificationName:NSWindowDidChangeScreenNotification
1302 object:nil];
1303 // No VC.
1304 EXPECT_FALSE(updated);
1305
1306 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
1307 nibName:nil
1308 bundle:nil];
1309 [viewController loadView];
1310 viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
1311
1312 [[NSNotificationCenter defaultCenter] postNotificationName:NSWindowDidChangeScreenNotification
1313 object:nil];
1314 EXPECT_TRUE(updated);
1315 }
1316
1317 TEST_F(FlutterEngineTest, DisplaySizeIsInPhysicalPixel) {
1318 NSString* fixtures = @(testing::GetFixturesPath());
1319 FlutterDartProject* project = [[FlutterDartProject alloc]
1320 initWithAssetsPath:fixtures
1321 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
1322 project.rootIsolateCreateCallback = FlutterEngineTest::IsolateCreateCallback;
1323 MockableFlutterEngine* engine = [[MockableFlutterEngine alloc] initWithName:@"foobar"
1324 project:project
1325 allowHeadlessExecution:true];
1326 BOOL updated = NO;
1327 auto original_update_displays = engine.embedderAPI.NotifyDisplayUpdate;
1328 engine.embedderAPI.NotifyDisplayUpdate = MOCK_ENGINE_PROC(
1329 NotifyDisplayUpdate, ([&updated, &original_update_displays](
1330 auto engine, auto update_type, auto* displays, auto display_count) {
1331 EXPECT_EQ(display_count, 1UL);
1332 EXPECT_EQ(displays->display_id, 10UL);
1333 EXPECT_EQ(displays->width, 60UL);
1334 EXPECT_EQ(displays->height, 80UL);
1335 EXPECT_EQ(displays->device_pixel_ratio, 2UL);
1336 updated = YES;
1337 return original_update_displays(engine, update_type, displays, display_count);
1338 }));
1339 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
1340 EXPECT_TRUE(updated);
1341 [engine shutDownEngine];
1342 engine = nil;
1343 }
1344
1345 TEST_F(FlutterEngineTest, ReportsHourFormat) {
1346 __block BOOL expectedValue;
1347
1348 // Set up mocks.
1349 id channelMock = OCMClassMock([FlutterBasicMessageChannel class]);
1350 OCMStub([channelMock messageChannelWithName:@"flutter/settings"
1351 binaryMessenger:[OCMArg any]
1352 codec:[OCMArg any]])
1353 .andReturn(channelMock);
1354 OCMStub([channelMock sendMessage:[OCMArg any]]).andDo((^(NSInvocation* invocation) {
1355 __weak id message;
1356 [invocation getArgument:&message atIndex:2];
1357 EXPECT_EQ(message[@"alwaysUse24HourFormat"], @(expectedValue));
1358 }));
1359
1360 id mockHourFormat = OCMClassMock([FlutterHourFormat class]);
1361 OCMStub([mockHourFormat isAlwaysUse24HourFormat]).andDo((^(NSInvocation* invocation) {
1362 [invocation setReturnValue:&expectedValue];
1363 }));
1364
1365 id engineMock = CreateMockFlutterEngine(nil);
1366
1367 // Verify the YES case.
1368 expectedValue = YES;
1369 EXPECT_TRUE([engineMock runWithEntrypoint:@"main"]);
1370 [engineMock shutDownEngine];
1371
1372 // Verify the NO case.
1373 expectedValue = NO;
1374 EXPECT_TRUE([engineMock runWithEntrypoint:@"main"]);
1375 [engineMock shutDownEngine];
1376
1377 // Clean up mocks.
1378 [mockHourFormat stopMocking];
1379 [engineMock stopMocking];
1380 [channelMock stopMocking];
1381 }
1382
1383 } // namespace flutter::testing
1384
1385 // NOLINTEND(clang-analyzer-core.StackAddressEscape)
FlutterAppDelegate.h
FlutterAppLifecycleDelegate.h
FlutterBinaryReply
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
FlutterBinaryMessengerRelay.h
FlutterChannels.h
FlutterResult
void(^ FlutterResult)(id _Nullable result)
Definition: FlutterChannels.h:194
FlutterCompositor.h
FlutterEngine.h
FlutterEngine_Internal.h
FlutterEngineTestUtils.h
FlutterPluginMacOS.h
FlutterViewIdentifier
int64_t FlutterViewIdentifier
Definition: FlutterViewController.h:21
FlutterViewControllerTestUtils.h
accessibility_bridge.h
FlutterEngine(Test)
Definition: FlutterEngineTest.mm:38
FlutterEngine(Test)::macOSCompositor
flutter::FlutterCompositor * macOSCompositor
Definition: FlutterEngineTest.mm:44
FlutterDartProject
Definition: FlutterDartProject.mm:39
FlutterEngineTerminationHandler
Definition: FlutterEngine.mm:190
FlutterEngineTerminationHandler::acceptingRequests
BOOL acceptingRequests
Definition: FlutterEngine_Internal.h:63
flutter::FlutterCompositor
Definition: FlutterCompositor.h:36
flutter::testing::FlutterEngineTest
Definition: FlutterEngineTestUtils.h:18
FakeAppDelegatePlugin
Definition: FlutterEngineTest.mm:113
FakeLifecycleProvider
Definition: FlutterEngineTest.mm:71
FlutterBasicMessageChannel
Definition: FlutterChannels.h:38
-[FlutterBasicMessageChannel setMessageHandler:]
void setMessageHandler:(FlutterMessageHandler _Nullable handler)
FlutterEngine
Definition: FlutterEngine.h:32
FlutterEngine::binaryMessenger
id< FlutterBinaryMessenger > binaryMessenger
Definition: FlutterEngine.h:92
-[FlutterEngine shutDownEngine]
void shutDownEngine()
Definition: FlutterEngine.mm:1339
FlutterEngine::viewController
FlutterViewController * viewController
Definition: FlutterEngine.h:87
FlutterError
Definition: FlutterCodecs.h:247
+[FlutterError errorWithCode:message:details:]
instancetype errorWithCode:message:details:(NSString *code,[message] NSString *_Nullable message,[details] id _Nullable details)
FlutterHourFormat
Definition: FlutterHourFormat.h:11
FlutterJSONMethodCodec
Definition: FlutterCodecs.h:455
FlutterMethodCall
Definition: FlutterCodecs.h:221
+[FlutterMethodCall methodCallWithMethodName:arguments:]
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
FlutterMethodChannel
Definition: FlutterChannels.h:221
-[FlutterMethodChannel setMethodCallHandler:]
void setMethodCallHandler:(FlutterMethodCallHandler _Nullable handler)
+[FlutterMethodChannel methodChannelWithName:binaryMessenger:codec:]
instancetype methodChannelWithName:binaryMessenger:codec:(NSString *name,[binaryMessenger] NSObject< FlutterBinaryMessenger > *messenger,[codec] NSObject< FlutterMethodCodec > *codec)
FlutterStandardMessageCodec
Definition: FlutterCodecs.h:209
FlutterViewController
Definition: FlutterViewController.h:73
FlutterViewController::backgroundColor
NSColor * backgroundColor
Definition: FlutterViewController.h:210
FlutterViewController::engine
FlutterEngine * engine
Definition: FlutterViewController.h:78
FlutterViewController::viewIdentifier
FlutterViewIdentifier viewIdentifier
Definition: FlutterViewController.h:130
MockableFlutterEngine
Definition: FlutterEngineTest.mm:124
PlainAppDelegate
Definition: FlutterEngineTest.mm:59
TestPlatformViewFactory
Definition: FlutterEngineTest.mm:48
flutter::testing
Definition: AccessibilityBridgeMacTest.mm:13
flutter::testing::CreateMockFlutterEngine
id CreateMockFlutterEngine(NSString *pasteboardString)
Definition: FlutterEngineTestUtils.mm:76
flutter::testing::TEST_F
TEST_F(FlutterEngineTest, ReportsHourFormat)
Definition: FlutterEngineTest.mm:1345
flutter::AppLifecycleState
AppLifecycleState
Definition: app_lifecycle_state.h:32
flutter::AppLifecycleState::kPaused
@ kPaused
flutter::AppLifecycleState::kDetached
@ kDetached
flutter::AppLifecycleState::kHidden
@ kHidden
flutter::AppLifecycleState::kResumed
@ kResumed
flutter::AppLifecycleState::kInactive
@ kInactive
FlutterAppLifecycleProvider-p
Definition: FlutterAppDelegate.h:21
FlutterBinaryMessenger-p
Definition: FlutterBinaryMessenger.h:49
+[FlutterMessageCodec-p sharedInstance]
instancetype sharedInstance()
FlutterPlatformViewFactory-p
Definition: FlutterPlatformViews.h:13
FlutterPlugin-p
Definition: FlutterPluginMacOS.h:29
user_data
void * user_data
Definition: texture_registrar_unittests.cc:27