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