| Flutter macOS Embedder |

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