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