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