Flutter Engine
 
Loading...
Searching...
No Matches
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
7
8#include <objc/objc.h>
9
10#include <algorithm>
11#include <functional>
12#include <thread>
13#include <vector>
14
20#import "flutter/shell/platform/darwin/common/test_utils_swift/test_utils_swift.h"
21#import "flutter/shell/platform/darwin/macos/InternalFlutterSwift/InternalFlutterSwift.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
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
114@end
115
116@implementation FakeAppDelegatePlugin
117+ (void)registerWithRegistrar:(id<FlutterPluginRegistrar>)registrar {
118}
119@end
120
121#pragma mark -
122
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
140namespace flutter::testing {
141
143 FlutterEngine* engine = GetFlutterEngine();
144 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
145 ASSERT_TRUE(engine.running);
146}
147
148TEST_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
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
188TEST_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
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
207TEST_F(FlutterEngineTest, CanLogToStdout) {
208 // Block until completion of print statement.
209 BOOL signaled = NO;
210 AddNativeCallback("SignalNativeTest",
211 CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { signaled = YES; }));
212
213 // Replace stdout stream buffer with our own.
214 FlutterStringOutputWriter* writer = [[FlutterStringOutputWriter alloc] init];
215 writer.expectedOutput = @"Hello logging";
216 FlutterLogger.outputWriter = writer;
217
218 // Launch the test entrypoint.
219 FlutterEngine* engine = GetFlutterEngine();
220 EXPECT_TRUE([engine runWithEntrypoint:@"canLogToStdout"]);
221 ASSERT_TRUE(engine.running);
222
223 while (!signaled) {
224 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
225 }
226
227 // Verify hello world was written to stdout.
228 EXPECT_TRUE(writer.gotExpectedOutput);
229}
230
231TEST_F(FlutterEngineTest, DISABLED_BackgroundIsBlack) {
232 FlutterEngine* engine = GetFlutterEngine();
233
234 // Latch to ensure the entire layer tree has been generated and presented.
235 BOOL signaled = NO;
236 AddNativeCallback("SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
237 CALayer* rootLayer = engine.viewController.flutterView.layer;
238 EXPECT_TRUE(rootLayer.backgroundColor != nil);
239 if (rootLayer.backgroundColor != nil) {
240 NSColor* actualBackgroundColor =
241 [NSColor colorWithCGColor:rootLayer.backgroundColor];
242 EXPECT_EQ(actualBackgroundColor, [NSColor blackColor]);
243 }
244 signaled = YES;
245 }));
246
247 // Launch the test entrypoint.
248 EXPECT_TRUE([engine runWithEntrypoint:@"backgroundTest"]);
249 ASSERT_TRUE(engine.running);
250
251 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
252 nibName:nil
253 bundle:nil];
254 [viewController loadView];
255 viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
256
257 while (!signaled) {
258 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
259 }
260}
261
262TEST_F(FlutterEngineTest, DISABLED_CanOverrideBackgroundColor) {
263 FlutterEngine* engine = GetFlutterEngine();
264
265 // Latch to ensure the entire layer tree has been generated and presented.
266 BOOL signaled = NO;
267 AddNativeCallback("SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
268 CALayer* rootLayer = engine.viewController.flutterView.layer;
269 EXPECT_TRUE(rootLayer.backgroundColor != nil);
270 if (rootLayer.backgroundColor != nil) {
271 NSColor* actualBackgroundColor =
272 [NSColor colorWithCGColor:rootLayer.backgroundColor];
273 EXPECT_EQ(actualBackgroundColor, [NSColor whiteColor]);
274 }
275 signaled = YES;
276 }));
277
278 // Launch the test entrypoint.
279 EXPECT_TRUE([engine runWithEntrypoint:@"backgroundTest"]);
280 ASSERT_TRUE(engine.running);
281
282 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
283 nibName:nil
284 bundle:nil];
285 [viewController loadView];
286 viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
287 viewController.flutterView.backgroundColor = [NSColor whiteColor];
288
289 while (!signaled) {
290 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
291 }
292}
293
294TEST_F(FlutterEngineTest, CanToggleAccessibility) {
295 FlutterEngine* engine = GetFlutterEngine();
296 // Capture the update callbacks before the embedder API initializes.
297 auto original_init = engine.embedderAPI.Initialize;
298 std::function<void(const FlutterSemanticsUpdate2*, void*)> update_semantics_callback;
300 Initialize, ([&update_semantics_callback, &original_init](
301 size_t version, const FlutterRendererConfig* config,
302 const FlutterProjectArgs* args, void* user_data, auto engine_out) {
303 update_semantics_callback = args->update_semantics_callback2;
304 return original_init(version, config, args, user_data, engine_out);
305 }));
306 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
307 // Set up view controller.
308 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
309 nibName:nil
310 bundle:nil];
311 [viewController loadView];
312 // Enable the semantics.
313 bool enabled_called = false;
315 MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&enabled_called](auto engine, bool enabled) {
316 enabled_called = enabled;
317 return kSuccess;
318 }));
319 engine.semanticsEnabled = YES;
320 EXPECT_TRUE(enabled_called);
321 // Send flutter semantics updates.
325 root.id = 0;
326 root.flags2 = &flags;
327 // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange)
328 root.actions = static_cast<FlutterSemanticsAction>(0);
329 root.text_selection_base = -1;
330 root.text_selection_extent = -1;
331 root.label = "root";
332 root.hint = "";
333 root.value = "";
334 root.increased_value = "";
335 root.decreased_value = "";
336 root.tooltip = "";
337 root.child_count = 1;
338 int32_t children[] = {1};
339 root.children_in_traversal_order = children;
341 root.identifier = "";
342
344 child1.id = 1;
345 child1.flags2 = &child_flags;
346 // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange)
347 child1.actions = static_cast<FlutterSemanticsAction>(0);
348 child1.text_selection_base = -1;
349 child1.text_selection_extent = -1;
350 child1.label = "child 1";
351 child1.hint = "";
352 child1.value = "";
353 child1.increased_value = "";
354 child1.decreased_value = "";
355 child1.tooltip = "";
356 child1.child_count = 0;
358 child1.identifier = "";
359
361 update.node_count = 2;
362 FlutterSemanticsNode2* nodes[] = {&root, &child1};
363 update.nodes = nodes;
364 update.custom_action_count = 0;
365 update_semantics_callback(&update, (__bridge void*)engine);
366
367 // Verify the accessibility tree is attached to the flutter view.
368 EXPECT_EQ([engine.viewController.flutterView.accessibilityChildren count], 1u);
369 NSAccessibilityElement* native_root = engine.viewController.flutterView.accessibilityChildren[0];
370 std::string root_label = [native_root.accessibilityLabel UTF8String];
371 EXPECT_TRUE(root_label == "root");
372 EXPECT_EQ(native_root.accessibilityRole, NSAccessibilityGroupRole);
373 EXPECT_EQ([native_root.accessibilityChildren count], 1u);
374 NSAccessibilityElement* native_child1 = native_root.accessibilityChildren[0];
375 std::string child1_value = [native_child1.accessibilityValue UTF8String];
376 EXPECT_TRUE(child1_value == "child 1");
377 EXPECT_EQ(native_child1.accessibilityRole, NSAccessibilityStaticTextRole);
378 EXPECT_EQ([native_child1.accessibilityChildren count], 0u);
379 // Disable the semantics.
380 bool semanticsEnabled = true;
382 MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&semanticsEnabled](auto engine, bool enabled) {
383 semanticsEnabled = enabled;
384 return kSuccess;
385 }));
386 engine.semanticsEnabled = NO;
387 EXPECT_FALSE(semanticsEnabled);
388 // Verify the accessibility tree is removed from the view.
389 EXPECT_EQ([engine.viewController.flutterView.accessibilityChildren count], 0u);
390
391 [engine setViewController:nil];
392}
393
394TEST_F(FlutterEngineTest, CanToggleAccessibilityWhenHeadless) {
395 FlutterEngine* engine = GetFlutterEngine();
396 // Capture the update callbacks before the embedder API initializes.
397 auto original_init = engine.embedderAPI.Initialize;
398 std::function<void(const FlutterSemanticsUpdate2*, void*)> update_semantics_callback;
400 Initialize, ([&update_semantics_callback, &original_init](
401 size_t version, const FlutterRendererConfig* config,
402 const FlutterProjectArgs* args, void* user_data, auto engine_out) {
403 update_semantics_callback = args->update_semantics_callback2;
404 return original_init(version, config, args, user_data, engine_out);
405 }));
406 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
407
408 // Enable the semantics without attaching a view controller.
409 bool enabled_called = false;
411 MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&enabled_called](auto engine, bool enabled) {
412 enabled_called = enabled;
413 return kSuccess;
414 }));
415 engine.semanticsEnabled = YES;
416 EXPECT_TRUE(enabled_called);
417 // Send flutter semantics updates.
421 root.id = 0;
422 root.flags2 = &flags;
423 // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange)
424 root.actions = static_cast<FlutterSemanticsAction>(0);
425 root.text_selection_base = -1;
426 root.text_selection_extent = -1;
427 root.label = "root";
428 root.hint = "";
429 root.value = "";
430 root.increased_value = "";
431 root.decreased_value = "";
432 root.tooltip = "";
433 root.child_count = 1;
434 int32_t children[] = {1};
435 root.children_in_traversal_order = children;
437
439 child1.id = 1;
440 child1.flags2 = &child_flags;
441 // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange)
442 child1.actions = static_cast<FlutterSemanticsAction>(0);
443 child1.text_selection_base = -1;
444 child1.text_selection_extent = -1;
445 child1.label = "child 1";
446 child1.hint = "";
447 child1.value = "";
448 child1.increased_value = "";
449 child1.decreased_value = "";
450 child1.tooltip = "";
451 child1.child_count = 0;
453
455 update.node_count = 2;
456 FlutterSemanticsNode2* nodes[] = {&root, &child1};
457 update.nodes = nodes;
458 update.custom_action_count = 0;
459 // This call updates semantics for the implicit view, which does not exist,
460 // and therefore this call is invalid. But the engine should not crash.
461 update_semantics_callback(&update, (__bridge void*)engine);
462
463 // No crashes.
464 EXPECT_EQ(engine.viewController, nil);
465
466 // Disable the semantics.
467 bool semanticsEnabled = true;
469 MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&semanticsEnabled](auto engine, bool enabled) {
470 semanticsEnabled = enabled;
471 return kSuccess;
472 }));
473 engine.semanticsEnabled = NO;
474 EXPECT_FALSE(semanticsEnabled);
475 // Still no crashes
476 EXPECT_EQ(engine.viewController, nil);
477}
478
479TEST_F(FlutterEngineTest, ProducesAccessibilityTreeWhenAddingViews) {
480 FlutterEngine* engine = GetFlutterEngine();
481 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
482
483 // Enable the semantics without attaching a view controller.
484 bool enabled_called = false;
486 MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&enabled_called](auto engine, bool enabled) {
487 enabled_called = enabled;
488 return kSuccess;
489 }));
490 engine.semanticsEnabled = YES;
491 EXPECT_TRUE(enabled_called);
492
493 EXPECT_EQ(engine.viewController, nil);
494
495 // Assign the view controller after enabling semantics
496 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
497 nibName:nil
498 bundle:nil];
500
501 EXPECT_NE(viewController.accessibilityBridge.lock(), nullptr);
502}
503
504TEST_F(FlutterEngineTest, NativeCallbacks) {
505 BOOL latch_called = NO;
506 AddNativeCallback("SignalNativeTest",
507 CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch_called = YES; }));
508
509 FlutterEngine* engine = GetFlutterEngine();
510 EXPECT_TRUE([engine runWithEntrypoint:@"nativeCallback"]);
511 ASSERT_TRUE(engine.running);
512
513 while (!latch_called) {
514 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
515 }
516 ASSERT_TRUE(latch_called);
517}
518
520 NSString* fixtures = @(flutter::testing::GetFixturesPath());
521 FlutterDartProject* project = [[FlutterDartProject alloc]
522 initWithAssetsPath:fixtures
523 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
524 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
525
526 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
527 nibName:nil
528 bundle:nil];
529 [viewController loadView];
530 [viewController viewDidLoad];
531 viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
532
533 EXPECT_TRUE([engine runWithEntrypoint:@"canCompositePlatformViews"]);
534
535 [engine.platformViewController registerViewFactory:[[TestPlatformViewFactory alloc] init]
536 withId:@"factory_id"];
537 [engine.platformViewController
538 handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"create"
539 arguments:@{
540 @"id" : @(42),
541 @"viewType" : @"factory_id",
542 }]
543 result:^(id result){
544 }];
545
546 // Wait up to 1 second for Flutter to emit a frame.
547 CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
548 CALayer* rootLayer = viewController.flutterView.layer;
549 while (rootLayer.sublayers.count == 0) {
550 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
551 if (CFAbsoluteTimeGetCurrent() - start > 1) {
552 break;
553 }
554 }
555
556 // There are two layers with Flutter contents and one view
557 EXPECT_EQ(rootLayer.sublayers.count, 2u);
558 EXPECT_EQ(viewController.flutterView.subviews.count, 1u);
559
560 // TODO(gw280): add support for screenshot tests in this test harness
561
562 [engine shutDownEngine];
563}
564
565TEST_F(FlutterEngineTest, CompositorIgnoresUnknownView) {
566 FlutterEngine* engine = GetFlutterEngine();
567 auto original_init = engine.embedderAPI.Initialize;
568 ::FlutterCompositor compositor;
570 Initialize, ([&compositor, &original_init](
571 size_t version, const FlutterRendererConfig* config,
572 const FlutterProjectArgs* args, void* user_data, auto engine_out) {
573 compositor = *args->compositor;
574 return original_init(version, config, args, user_data, engine_out);
575 }));
576
577 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
578 nibName:nil
579 bundle:nil];
580 [viewController loadView];
581
582 EXPECT_TRUE([engine runWithEntrypoint:@"empty"]);
583
585 .struct_size = sizeof(FlutterBackingStoreConfig),
586 .size = FlutterSize{10, 10},
587 };
588 FlutterBackingStore backing_store = {};
589 EXPECT_NE(compositor.create_backing_store_callback, nullptr);
590 EXPECT_TRUE(
591 compositor.create_backing_store_callback(&config, &backing_store, compositor.user_data));
592
593 FlutterLayer layer{
595 .backing_store = &backing_store,
596 };
597 std::vector<FlutterLayer*> layers = {&layer};
598
601 .view_id = 123,
602 .layers = const_cast<const FlutterLayer**>(layers.data()),
603 .layers_count = 1,
604 .user_data = compositor.user_data,
605 };
606 EXPECT_NE(compositor.present_view_callback, nullptr);
607 EXPECT_FALSE(compositor.present_view_callback(&info));
608 EXPECT_TRUE(compositor.collect_backing_store_callback(&backing_store, compositor.user_data));
609
610 (void)viewController;
611 [engine shutDownEngine];
612}
613
614TEST_F(FlutterEngineTest, DartEntrypointArguments) {
615 NSString* fixtures = @(flutter::testing::GetFixturesPath());
616 FlutterDartProject* project = [[FlutterDartProject alloc]
617 initWithAssetsPath:fixtures
618 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
619
620 project.dartEntrypointArguments = @[ @"arg1", @"arg2" ];
621 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
622
623 bool called = false;
624 auto original_init = engine.embedderAPI.Initialize;
626 Initialize, ([&called, &original_init](size_t version, const FlutterRendererConfig* config,
627 const FlutterProjectArgs* args, void* user_data,
628 FLUTTER_API_SYMBOL(FlutterEngine) * engine_out) {
629 called = true;
630 EXPECT_EQ(args->dart_entrypoint_argc, 2);
631 NSString* arg1 = [[NSString alloc] initWithCString:args->dart_entrypoint_argv[0]
632 encoding:NSUTF8StringEncoding];
633 NSString* arg2 = [[NSString alloc] initWithCString:args->dart_entrypoint_argv[1]
634 encoding:NSUTF8StringEncoding];
635
636 EXPECT_TRUE([arg1 isEqualToString:@"arg1"]);
637 EXPECT_TRUE([arg2 isEqualToString:@"arg2"]);
638
639 return original_init(version, config, args, user_data, engine_out);
640 }));
641
642 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
643 EXPECT_TRUE(called);
644 [engine shutDownEngine];
645}
646
647// Verify that the engine is not retained indirectly via the binary messenger held by channels and
648// plugins. Previously, FlutterEngine.binaryMessenger returned the engine itself, and thus plugins
649// could cause a retain cycle, preventing the engine from being deallocated.
650// FlutterEngine.binaryMessenger now returns a FlutterBinaryMessengerRelay whose weak pointer back
651// to the engine is cleared when the engine is deallocated.
652// Issue: https://github.com/flutter/flutter/issues/116445
653TEST_F(FlutterEngineTest, FlutterBinaryMessengerDoesNotRetainEngine) {
654 __weak FlutterEngine* weakEngine;
655 id<FlutterBinaryMessenger> binaryMessenger = nil;
656 @autoreleasepool {
657 // Create a test engine.
658 NSString* fixtures = @(flutter::testing::GetFixturesPath());
659 FlutterDartProject* project = [[FlutterDartProject alloc]
660 initWithAssetsPath:fixtures
661 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
662 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
663 project:project
664 allowHeadlessExecution:YES];
665 weakEngine = engine;
666 binaryMessenger = engine.binaryMessenger;
667 }
668
669 // Once the engine has been deallocated, verify the weak engine pointer is nil, and thus not
670 // retained by the relay.
671 EXPECT_NE(binaryMessenger, nil);
672 EXPECT_EQ(weakEngine, nil);
673}
674
675// Verify that the engine is not retained indirectly via the texture registry held by plugins.
676// Issue: https://github.com/flutter/flutter/issues/116445
677TEST_F(FlutterEngineTest, FlutterTextureRegistryDoesNotReturnEngine) {
678 __weak FlutterEngine* weakEngine;
679 id<FlutterTextureRegistry> textureRegistry;
680 @autoreleasepool {
681 // Create a test engine.
682 NSString* fixtures = @(flutter::testing::GetFixturesPath());
683 FlutterDartProject* project = [[FlutterDartProject alloc]
684 initWithAssetsPath:fixtures
685 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
686 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
687 project:project
688 allowHeadlessExecution:YES];
689 id<FlutterPluginRegistrar> registrar = [engine registrarForPlugin:@"MyPlugin"];
690 textureRegistry = registrar.textures;
691 }
692
693 // Once the engine has been deallocated, verify the weak engine pointer is nil, and thus not
694 // retained via the texture registry.
695 EXPECT_NE(textureRegistry, nil);
696 EXPECT_EQ(weakEngine, nil);
697}
698
699TEST_F(FlutterEngineTest, PublishedValueNilForUnknownPlugin) {
700 NSString* fixtures = @(flutter::testing::GetFixturesPath());
701 FlutterDartProject* project = [[FlutterDartProject alloc]
702 initWithAssetsPath:fixtures
703 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
704 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
705 project:project
706 allowHeadlessExecution:YES];
707
708 EXPECT_EQ([engine valuePublishedByPlugin:@"NoSuchPlugin"], nil);
709}
710
711TEST_F(FlutterEngineTest, PublishedValueNSNullIfNoPublishedValue) {
712 NSString* fixtures = @(flutter::testing::GetFixturesPath());
713 FlutterDartProject* project = [[FlutterDartProject alloc]
714 initWithAssetsPath:fixtures
715 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
716 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
717 project:project
718 allowHeadlessExecution:YES];
719 NSString* pluginName = @"MyPlugin";
720 // Request the registarar to register the plugin as existing.
721 [engine registrarForPlugin:pluginName];
722
723 // The documented behavior is that a plugin that exists but hasn't published
724 // anything returns NSNull, rather than nil, as on iOS.
725 EXPECT_EQ([engine valuePublishedByPlugin:pluginName], [NSNull null]);
726}
727
728TEST_F(FlutterEngineTest, PublishedValueReturnsLastPublished) {
729 NSString* fixtures = @(flutter::testing::GetFixturesPath());
730 FlutterDartProject* project = [[FlutterDartProject alloc]
731 initWithAssetsPath:fixtures
732 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
733 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
734 project:project
735 allowHeadlessExecution:YES];
736 NSString* pluginName = @"MyPlugin";
737 id<FlutterPluginRegistrar> registrar = [engine registrarForPlugin:pluginName];
738
739 NSString* firstValue = @"A published value";
740 NSArray* secondValue = @[ @"A different published value" ];
741
742 [registrar publish:firstValue];
743 EXPECT_EQ([engine valuePublishedByPlugin:pluginName], firstValue);
744
745 [registrar publish:secondValue];
746 EXPECT_EQ([engine valuePublishedByPlugin:pluginName], secondValue);
747}
748
749TEST_F(FlutterEngineTest, RegistrarForwardViewControllerLookUpToEngine) {
750 NSString* fixtures = @(flutter::testing::GetFixturesPath());
751 FlutterDartProject* project = [[FlutterDartProject alloc]
752 initWithAssetsPath:fixtures
753 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
754 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
755
756 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
757 nibName:nil
758 bundle:nil];
759 id<FlutterPluginRegistrar> registrar = [engine registrarForPlugin:@"MyPlugin"];
760
761 EXPECT_EQ([registrar viewController], viewController);
762}
763
764// If a channel overrides a previous channel with the same name, cleaning
765// the previous channel should not affect the new channel.
766//
767// This is important when recreating classes that uses a channel, because the
768// new instance would create the channel before the first class is deallocated
769// and clears the channel.
770TEST_F(FlutterEngineTest, MessengerCleanupConnectionWorks) {
771 FlutterEngine* engine = GetFlutterEngine();
772 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
773
774 NSString* channel = @"_test_";
775 NSData* channel_data = [channel dataUsingEncoding:NSUTF8StringEncoding];
776
777 // Mock SendPlatformMessage so that if a message is sent to
778 // "test/send_message", act as if the framework has sent an empty message to
779 // the channel marked by the `sendOnChannel:message:` call's message.
781 SendPlatformMessage, ([](auto engine_, auto message_) {
782 if (strcmp(message_->channel, "test/send_message") == 0) {
783 // The simplest message that is acceptable to a method channel.
784 std::string message = R"|({"method": "a"})|";
785 std::string channel(reinterpret_cast<const char*>(message_->message),
786 message_->message_size);
787 reinterpret_cast<EmbedderEngine*>(engine_)
788 ->GetShell()
789 .GetPlatformView()
790 ->HandlePlatformMessage(std::make_unique<PlatformMessage>(
791 channel.c_str(), fml::MallocMapping::Copy(message.c_str(), message.length()),
792 fml::RefPtr<PlatformMessageResponse>()));
793 }
794 return kSuccess;
795 }));
796
797 __block int record = 0;
798
799 FlutterMethodChannel* channel1 =
801 binaryMessenger:engine.binaryMessenger
802 codec:[FlutterJSONMethodCodec sharedInstance]];
803 [channel1 setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
804 record += 1;
805 }];
806
807 [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data];
808 EXPECT_EQ(record, 1);
809
810 FlutterMethodChannel* channel2 =
812 binaryMessenger:engine.binaryMessenger
813 codec:[FlutterJSONMethodCodec sharedInstance]];
814 [channel2 setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
815 record += 10;
816 }];
817
818 [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data];
819 EXPECT_EQ(record, 11);
820
821 [channel1 setMethodCallHandler:nil];
822
823 [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data];
824 EXPECT_EQ(record, 21);
825}
826
827TEST_F(FlutterEngineTest, HasStringsWhenPasteboardEmpty) {
828 id engineMock = CreateMockFlutterEngine(nil);
829
830 // Call hasStrings and expect it to be false.
831 __block bool calledAfterClear = false;
832 __block bool valueAfterClear;
833 FlutterResult resultAfterClear = ^(id result) {
834 calledAfterClear = true;
835 NSNumber* valueNumber = [result valueForKey:@"value"];
836 valueAfterClear = [valueNumber boolValue];
837 };
838 FlutterMethodCall* methodCallAfterClear =
839 [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil];
840 [engineMock handleMethodCall:methodCallAfterClear result:resultAfterClear];
841 EXPECT_TRUE(calledAfterClear);
842 EXPECT_FALSE(valueAfterClear);
843}
844
845TEST_F(FlutterEngineTest, HasStringsWhenPasteboardFull) {
846 id engineMock = CreateMockFlutterEngine(@"some string");
847
848 // Call hasStrings and expect it to be true.
849 __block bool called = false;
850 __block bool value;
851 FlutterResult result = ^(id result) {
852 called = true;
853 NSNumber* valueNumber = [result valueForKey:@"value"];
854 value = [valueNumber boolValue];
855 };
856 FlutterMethodCall* methodCall =
857 [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil];
858 [engineMock handleMethodCall:methodCall result:result];
859 EXPECT_TRUE(called);
860 EXPECT_TRUE(value);
861}
862
863TEST_F(FlutterEngineTest, ResponseAfterEngineDied) {
864 FlutterEngine* engine = GetFlutterEngine();
866 initWithName:@"foo"
867 binaryMessenger:engine.binaryMessenger
869 __block BOOL didCallCallback = NO;
870 [channel setMessageHandler:^(id message, FlutterReply callback) {
871 ShutDownEngine();
872 callback(nil);
873 didCallCallback = YES;
874 }];
875 EXPECT_TRUE([engine runWithEntrypoint:@"sendFooMessage"]);
876 engine = nil;
877
878 while (!didCallCallback) {
879 [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
880 }
881}
882
883TEST_F(FlutterEngineTest, ResponseFromBackgroundThread) {
884 FlutterEngine* engine = GetFlutterEngine();
886 initWithName:@"foo"
887 binaryMessenger:engine.binaryMessenger
889 __block BOOL didCallCallback = NO;
890 [channel setMessageHandler:^(id message, FlutterReply callback) {
891 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
892 callback(nil);
893 dispatch_async(dispatch_get_main_queue(), ^{
894 didCallCallback = YES;
895 });
896 });
897 }];
898 EXPECT_TRUE([engine runWithEntrypoint:@"sendFooMessage"]);
899
900 while (!didCallCallback) {
901 [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
902 }
903}
904
905TEST_F(FlutterEngineTest, CanGetEngineForId) {
906 FlutterEngine* engine = GetFlutterEngine();
907
908 BOOL signaled = NO;
909 std::optional<int64_t> engineId;
910 AddNativeCallback("NotifyEngineId", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
911 const auto argument = Dart_GetNativeArgument(args, 0);
912 if (!Dart_IsNull(argument)) {
913 const auto id = tonic::DartConverter<int64_t>::FromDart(argument);
914 engineId = id;
915 }
916 signaled = YES;
917 }));
918
919 EXPECT_TRUE([engine runWithEntrypoint:@"testEngineId"]);
920 while (!signaled) {
921 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
922 }
923
924 EXPECT_TRUE(engineId.has_value());
925 if (!engineId.has_value()) {
926 return;
927 }
928 EXPECT_EQ(engine, [FlutterEngine engineForIdentifier:*engineId]);
929 ShutDownEngine();
930}
931
932TEST_F(FlutterEngineTest, ResizeSynchronizerNotBlockingRasterThreadAfterShutdown) {
933 FlutterResizeSynchronizer* threadSynchronizer = [[FlutterResizeSynchronizer alloc] init];
934 [threadSynchronizer shutDown];
935
936 std::thread rasterThread([&threadSynchronizer] {
937 [threadSynchronizer performCommitForSize:CGSizeMake(100, 100)
938 afterDelay:0
939 notify:^{
940 }];
941 });
942
943 rasterThread.join();
944}
945
946TEST_F(FlutterEngineTest, ManageControllersIfInitiatedByController) {
947 NSString* fixtures = @(flutter::testing::GetFixturesPath());
948 FlutterDartProject* project = [[FlutterDartProject alloc]
949 initWithAssetsPath:fixtures
950 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
951
953 FlutterViewController* viewController1;
954
955 @autoreleasepool {
956 // Create FVC1.
957 viewController1 = [[FlutterViewController alloc] initWithProject:project];
958 EXPECT_EQ(viewController1.viewIdentifier, 0ll);
959
960 engine = viewController1.engine;
962
963 // Create FVC2 based on the same engine.
964 FlutterViewController* viewController2 = [[FlutterViewController alloc] initWithEngine:engine
965 nibName:nil
966 bundle:nil];
967 EXPECT_EQ(engine.viewController, viewController2);
968 }
969 // FVC2 is deallocated but FVC1 is retained.
970
971 EXPECT_EQ(engine.viewController, nil);
972
973 engine.viewController = viewController1;
974 EXPECT_EQ(engine.viewController, viewController1);
975 EXPECT_EQ(viewController1.viewIdentifier, 0ll);
976}
977
978TEST_F(FlutterEngineTest, ManageControllersIfInitiatedByEngine) {
979 // Don't create the engine with `CreateMockFlutterEngine`, because it adds
980 // additional references to FlutterViewControllers, which is crucial to this
981 // test case.
982 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"io.flutter"
983 project:nil
984 allowHeadlessExecution:NO];
985 FlutterViewController* viewController1;
986
987 @autoreleasepool {
988 viewController1 = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
989 EXPECT_EQ(viewController1.viewIdentifier, 0ll);
990 EXPECT_EQ(engine.viewController, viewController1);
991
993
994 FlutterViewController* viewController2 = [[FlutterViewController alloc] initWithEngine:engine
995 nibName:nil
996 bundle:nil];
997 EXPECT_EQ(viewController2.viewIdentifier, 0ll);
998 EXPECT_EQ(engine.viewController, viewController2);
999 }
1000 // FVC2 is deallocated but FVC1 is retained.
1001
1002 EXPECT_EQ(engine.viewController, nil);
1003
1004 engine.viewController = viewController1;
1005 EXPECT_EQ(engine.viewController, viewController1);
1006 EXPECT_EQ(viewController1.viewIdentifier, 0ll);
1007}
1008
1009TEST_F(FlutterEngineTest, RemovingViewDisposesCompositorResources) {
1010 NSString* fixtures = @(flutter::testing::GetFixturesPath());
1011 FlutterDartProject* project = [[FlutterDartProject alloc]
1012 initWithAssetsPath:fixtures
1013 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
1014 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
1015
1016 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
1017 nibName:nil
1018 bundle:nil];
1019 [viewController loadView];
1020 [viewController viewDidLoad];
1021 viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
1022
1023 EXPECT_TRUE([engine runWithEntrypoint:@"drawIntoAllViews"]);
1024 // Wait up to 1 second for Flutter to emit a frame.
1025 CFTimeInterval start = CACurrentMediaTime();
1026 while (engine.macOSCompositor->DebugNumViews() == 0) {
1027 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
1028 if (CACurrentMediaTime() - start > 1) {
1029 break;
1030 }
1031 }
1032
1033 EXPECT_EQ(engine.macOSCompositor->DebugNumViews(), 1u);
1034
1035 engine.viewController = nil;
1036 EXPECT_EQ(engine.macOSCompositor->DebugNumViews(), 0u);
1037
1038 [engine shutDownEngine];
1039 engine = nil;
1040}
1041
1042TEST_F(FlutterEngineTest, HandlesTerminationRequest) {
1043 id engineMock = CreateMockFlutterEngine(nil);
1044 __block NSString* nextResponse = @"exit";
1045 __block BOOL triedToTerminate = NO;
1046 FlutterEngineTerminationHandler* terminationHandler =
1047 [[FlutterEngineTerminationHandler alloc] initWithEngine:engineMock
1048 terminator:^(id sender) {
1049 triedToTerminate = TRUE;
1050 // Don't actually terminate, of course.
1051 }];
1052 OCMStub([engineMock terminationHandler]).andReturn(terminationHandler);
1053 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1054 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1055 [engineMock binaryMessenger])
1056 .andReturn(binaryMessengerMock);
1057 OCMStub([engineMock sendOnChannel:@"flutter/platform"
1058 message:[OCMArg any]
1059 binaryReply:[OCMArg any]])
1060 .andDo((^(NSInvocation* invocation) {
1061 [invocation retainArguments];
1063 NSData* returnedMessage;
1064 [invocation getArgument:&callback atIndex:4];
1065 if ([nextResponse isEqualToString:@"error"]) {
1066 FlutterError* errorResponse = [FlutterError errorWithCode:@"Error"
1067 message:@"Failed"
1068 details:@"Details"];
1069 returnedMessage =
1070 [[FlutterJSONMethodCodec sharedInstance] encodeErrorEnvelope:errorResponse];
1071 } else {
1072 NSDictionary* responseDict = @{@"response" : nextResponse};
1073 returnedMessage =
1074 [[FlutterJSONMethodCodec sharedInstance] encodeSuccessEnvelope:responseDict];
1075 }
1076 callback(returnedMessage);
1077 }));
1078 __block NSString* calledAfterTerminate = @"";
1079 FlutterResult appExitResult = ^(id result) {
1080 NSDictionary* resultDict = result;
1081 calledAfterTerminate = resultDict[@"response"];
1082 };
1083 FlutterMethodCall* methodExitApplication =
1084 [FlutterMethodCall methodCallWithMethodName:@"System.exitApplication"
1085 arguments:@{@"type" : @"cancelable"}];
1086
1087 // Always terminate when the binding isn't ready (which is the default).
1088 triedToTerminate = NO;
1089 calledAfterTerminate = @"";
1090 nextResponse = @"cancel";
1091 [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1092 EXPECT_STREQ([calledAfterTerminate UTF8String], "");
1093 EXPECT_TRUE(triedToTerminate);
1094
1095 // Once the binding is ready, handle the request.
1096 terminationHandler.acceptingRequests = YES;
1097 triedToTerminate = NO;
1098 calledAfterTerminate = @"";
1099 nextResponse = @"exit";
1100 [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1101 EXPECT_STREQ([calledAfterTerminate UTF8String], "exit");
1102 EXPECT_TRUE(triedToTerminate);
1103
1104 triedToTerminate = NO;
1105 calledAfterTerminate = @"";
1106 nextResponse = @"cancel";
1107 [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1108 EXPECT_STREQ([calledAfterTerminate UTF8String], "cancel");
1109 EXPECT_FALSE(triedToTerminate);
1110
1111 // Check that it doesn't crash on error.
1112 triedToTerminate = NO;
1113 calledAfterTerminate = @"";
1114 nextResponse = @"error";
1115 [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1116 EXPECT_STREQ([calledAfterTerminate UTF8String], "");
1117 EXPECT_TRUE(triedToTerminate);
1118}
1119
1120TEST_F(FlutterEngineTest, IgnoresTerminationRequestIfNotFlutterAppDelegate) {
1121 id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1122 id<NSApplicationDelegate> plainDelegate = [[PlainAppDelegate alloc] init];
1123 [NSApplication sharedApplication].delegate = plainDelegate;
1124
1125 // Creating the engine shouldn't fail here, even though the delegate isn't a
1126 // FlutterAppDelegate.
1128
1129 // Asking to terminate the app should cancel.
1130 EXPECT_EQ([[[NSApplication sharedApplication] delegate] applicationShouldTerminate:NSApp],
1131 NSTerminateCancel);
1132
1133 [NSApplication sharedApplication].delegate = previousDelegate;
1134}
1135
1136TEST_F(FlutterEngineTest, HandleAccessibilityEvent) {
1137 __block BOOL announced = NO;
1138 id engineMock = CreateMockFlutterEngine(nil);
1139
1140 OCMStub([engineMock announceAccessibilityMessage:[OCMArg any]
1141 withPriority:NSAccessibilityPriorityMedium])
1142 .andDo((^(NSInvocation* invocation) {
1143 announced = TRUE;
1144 [invocation retainArguments];
1145 NSString* message;
1146 [invocation getArgument:&message atIndex:2];
1147 EXPECT_EQ(message, @"error message");
1148 }));
1149
1150 NSDictionary<NSString*, id>* annotatedEvent =
1151 @{@"type" : @"announce",
1152 @"data" : @{@"message" : @"error message"}};
1153
1154 [engineMock handleAccessibilityEvent:annotatedEvent];
1155
1156 EXPECT_TRUE(announced);
1157}
1158
1159TEST_F(FlutterEngineTest, HandleLifecycleStates) API_AVAILABLE(macos(10.9)) {
1160 __block flutter::AppLifecycleState sentState;
1161 id engineMock = CreateMockFlutterEngine(nil);
1162
1163 // Have to enumerate all the values because OCMStub can't capture
1164 // non-Objective-C object arguments.
1165 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kDetached])
1166 .andDo((^(NSInvocation* invocation) {
1168 }));
1169 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kResumed])
1170 .andDo((^(NSInvocation* invocation) {
1172 }));
1173 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kInactive])
1174 .andDo((^(NSInvocation* invocation) {
1176 }));
1177 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kHidden])
1178 .andDo((^(NSInvocation* invocation) {
1180 }));
1181 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kPaused])
1182 .andDo((^(NSInvocation* invocation) {
1184 }));
1185
1186 __block NSApplicationOcclusionState visibility = NSApplicationOcclusionStateVisible;
1187 id mockApplication = OCMPartialMock([NSApplication sharedApplication]);
1188 OCMStub((NSApplicationOcclusionState)[mockApplication occlusionState])
1189 .andDo(^(NSInvocation* invocation) {
1190 [invocation setReturnValue:&visibility];
1191 });
1192
1193 NSNotification* willBecomeActive =
1194 [[NSNotification alloc] initWithName:NSApplicationWillBecomeActiveNotification
1195 object:nil
1196 userInfo:nil];
1197 NSNotification* willResignActive =
1198 [[NSNotification alloc] initWithName:NSApplicationWillResignActiveNotification
1199 object:nil
1200 userInfo:nil];
1201
1202 NSNotification* didChangeOcclusionState;
1203 didChangeOcclusionState =
1204 [[NSNotification alloc] initWithName:NSApplicationDidChangeOcclusionStateNotification
1205 object:nil
1206 userInfo:nil];
1207
1208 [engineMock handleDidChangeOcclusionState:didChangeOcclusionState];
1209 EXPECT_EQ(sentState, flutter::AppLifecycleState::kInactive);
1210
1211 [engineMock handleWillBecomeActive:willBecomeActive];
1212 EXPECT_EQ(sentState, flutter::AppLifecycleState::kResumed);
1213
1214 [engineMock handleWillResignActive:willResignActive];
1215 EXPECT_EQ(sentState, flutter::AppLifecycleState::kInactive);
1216
1217 visibility = 0;
1218 [engineMock handleDidChangeOcclusionState:didChangeOcclusionState];
1219 EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden);
1220
1221 [engineMock handleWillBecomeActive:willBecomeActive];
1222 EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden);
1223
1224 [engineMock handleWillResignActive:willResignActive];
1225 EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden);
1226
1227 [mockApplication stopMocking];
1228}
1229
1230TEST_F(FlutterEngineTest, ForwardsPluginDelegateRegistration) {
1231 id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1232 FakeLifecycleProvider* fakeAppDelegate = [[FakeLifecycleProvider alloc] init];
1233 [NSApplication sharedApplication].delegate = fakeAppDelegate;
1234
1235 FakeAppDelegatePlugin* plugin = [[FakeAppDelegatePlugin alloc] init];
1237
1238 [[engine registrarForPlugin:@"TestPlugin"] addApplicationDelegate:plugin];
1239
1240 EXPECT_TRUE([fakeAppDelegate hasDelegate:plugin]);
1241
1242 [NSApplication sharedApplication].delegate = previousDelegate;
1243}
1244
1245TEST_F(FlutterEngineTest, UnregistersPluginsOnEngineDestruction) {
1246 id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1247 FakeLifecycleProvider* fakeAppDelegate = [[FakeLifecycleProvider alloc] init];
1248 [NSApplication sharedApplication].delegate = fakeAppDelegate;
1249
1250 FakeAppDelegatePlugin* plugin = [[FakeAppDelegatePlugin alloc] init];
1251
1252 @autoreleasepool {
1253 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil];
1254
1255 [[engine registrarForPlugin:@"TestPlugin"] addApplicationDelegate:plugin];
1256 EXPECT_TRUE([fakeAppDelegate hasDelegate:plugin]);
1257 }
1258
1259 // When the engine is released, it should unregister any plugins it had
1260 // registered on its behalf.
1261 EXPECT_FALSE([fakeAppDelegate hasDelegate:plugin]);
1262
1263 [NSApplication sharedApplication].delegate = previousDelegate;
1264}
1265
1266TEST_F(FlutterEngineTest, RunWithEntrypointUpdatesDisplayConfig) {
1267 BOOL updated = NO;
1268 FlutterEngine* engine = GetFlutterEngine();
1269 auto original_update_displays = engine.embedderAPI.NotifyDisplayUpdate;
1271 NotifyDisplayUpdate, ([&updated, &original_update_displays](
1272 auto engine, auto update_type, auto* displays, auto display_count) {
1273 updated = YES;
1274 return original_update_displays(engine, update_type, displays, display_count);
1275 }));
1276
1277 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
1278 EXPECT_TRUE(updated);
1279
1280 updated = NO;
1281 [[NSNotificationCenter defaultCenter]
1282 postNotificationName:NSApplicationDidChangeScreenParametersNotification
1283 object:nil];
1284 EXPECT_TRUE(updated);
1285}
1286
1287TEST_F(FlutterEngineTest, NotificationsUpdateDisplays) {
1288 BOOL updated = NO;
1289 FlutterEngine* engine = GetFlutterEngine();
1290 auto original_set_viewport_metrics = engine.embedderAPI.SendWindowMetricsEvent;
1292 SendWindowMetricsEvent,
1293 ([&updated, &original_set_viewport_metrics](auto engine, auto* window_metrics) {
1294 updated = YES;
1295 return original_set_viewport_metrics(engine, window_metrics);
1296 }));
1297
1298 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
1299
1300 updated = NO;
1301 [[NSNotificationCenter defaultCenter] postNotificationName:NSWindowDidChangeScreenNotification
1302 object:nil];
1303 // No VC.
1304 EXPECT_FALSE(updated);
1305
1306 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
1307 nibName:nil
1308 bundle:nil];
1309 [viewController loadView];
1310 viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
1311
1312 [[NSNotificationCenter defaultCenter] postNotificationName:NSWindowDidChangeScreenNotification
1313 object:nil];
1314 EXPECT_TRUE(updated);
1315}
1316
1317TEST_F(FlutterEngineTest, DisplaySizeIsInPhysicalPixel) {
1318 NSString* fixtures = @(testing::GetFixturesPath());
1319 FlutterDartProject* project = [[FlutterDartProject alloc]
1320 initWithAssetsPath:fixtures
1321 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
1322 project.rootIsolateCreateCallback = FlutterEngineTest::IsolateCreateCallback;
1323 MockableFlutterEngine* engine = [[MockableFlutterEngine alloc] initWithName:@"foobar"
1324 project:project
1325 allowHeadlessExecution:true];
1326 BOOL updated = NO;
1327 auto original_update_displays = engine.embedderAPI.NotifyDisplayUpdate;
1329 NotifyDisplayUpdate, ([&updated, &original_update_displays](
1330 auto engine, auto update_type, auto* displays, auto display_count) {
1331 EXPECT_EQ(display_count, 1UL);
1332 EXPECT_EQ(displays->display_id, 10UL);
1333 EXPECT_EQ(displays->width, 60UL);
1334 EXPECT_EQ(displays->height, 80UL);
1335 EXPECT_EQ(displays->device_pixel_ratio, 2UL);
1336 updated = YES;
1337 return original_update_displays(engine, update_type, displays, display_count);
1338 }));
1339 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
1340 EXPECT_TRUE(updated);
1341 [engine shutDownEngine];
1342 engine = nil;
1343}
1344
1345TEST_F(FlutterEngineTest, ReportsHourFormat) {
1346 __block BOOL expectedValue;
1347
1348 // Set up mocks.
1349 id channelMock = OCMClassMock([FlutterBasicMessageChannel class]);
1350 OCMStub([channelMock messageChannelWithName:@"flutter/settings"
1351 binaryMessenger:[OCMArg any]
1352 codec:[OCMArg any]])
1353 .andReturn(channelMock);
1354 OCMStub([channelMock sendMessage:[OCMArg any]]).andDo((^(NSInvocation* invocation) {
1355 __weak id message;
1356 [invocation getArgument:&message atIndex:2];
1357 EXPECT_EQ(message[@"alwaysUse24HourFormat"], @(expectedValue));
1358 }));
1359
1360 id mockHourFormat = OCMClassMock([FlutterHourFormat class]);
1361 OCMStub([mockHourFormat isAlwaysUse24HourFormat]).andDo((^(NSInvocation* invocation) {
1362 [invocation setReturnValue:&expectedValue];
1363 }));
1364
1365 id engineMock = CreateMockFlutterEngine(nil);
1366
1367 // Verify the YES case.
1368 expectedValue = YES;
1369 EXPECT_TRUE([engineMock runWithEntrypoint:@"main"]);
1370 [engineMock shutDownEngine];
1371
1372 // Verify the NO case.
1373 expectedValue = NO;
1374 EXPECT_TRUE([engineMock runWithEntrypoint:@"main"]);
1375 [engineMock shutDownEngine];
1376
1377 // Clean up mocks.
1378 [mockHourFormat stopMocking];
1379 [engineMock stopMocking];
1380 [channelMock stopMocking];
1381}
1382
1383} // namespace flutter::testing
1384
1385// NOLINTEND(clang-analyzer-core.StackAddressEscape)
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
void(^ FlutterResult)(id _Nullable result)
NSPointerArray * _delegates
flutter::FlutterCompositor * macOSCompositor
int32_t value
#define FLUTTER_API_SYMBOL(symbol)
Definition embedder.h:67
@ kFlutterLayerContentTypeBackingStore
Definition embedder.h:2102
@ kSuccess
Definition embedder.h:73
FlutterSemanticsAction
Definition embedder.h:115
FlutterEngine engine
Definition main.cc:84
const FlutterLayer size_t layers_count
const FlutterLayer ** layers
return TRUE
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
const gchar * channel
G_BEGIN_DECLS GBytes * message
G_BEGIN_DECLS FlutterViewId view_id
FlutterDesktopBinaryReply callback
void setMessageHandler:(FlutterMessageHandler _Nullable handler)
void(* rootIsolateCreateCallback)(void *_Nullable)
NSObject< FlutterBinaryMessenger > * binaryMessenger
flutter::FlutterCompositor * macOSCompositor
FlutterViewController * viewController
FlutterEngineProcTable & embedderAPI
instancetype errorWithCode:message:details:(NSString *code,[message] NSString *_Nullable message,[details] id _Nullable details)
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
void setMethodCallHandler:(FlutterMethodCallHandler _Nullable handler)
instancetype methodChannelWithName:binaryMessenger:codec:(NSString *name,[binaryMessenger] NSObject< FlutterBinaryMessenger > *messenger,[codec] NSObject< FlutterMethodCodec > *codec)
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
FlutterViewController * viewController
int64_t FlutterViewIdentifier
const char * GetFixturesPath()
Returns the directory containing the test fixture for the target if this target has fixtures configur...
id CreateMockFlutterEngine(NSString *pasteboardString)
it will be possible to load the file into Perfetto s trace viewer use test Running tests that layout and measure text will not yield consistent results across various platforms Enabling this option will make font resolution default to the Ahem test font on all disable asset Prevents usage of any non test fonts unless they were explicitly Loaded via prefetched default font Indicates whether the embedding started a prefetch of the default font manager before creating the engine run In non interactive keep the shell running after the Dart script has completed enable serial On low power devices with low core running concurrent GC tasks on threads can cause them to contend with the UI thread which could potentially lead to jank This option turns off all concurrent GC activities domain network JSON encoded network policy per domain This overrides the DisallowInsecureConnections switch Embedder can specify whether to allow or disallow insecure connections at a domain level old gen heap size
std::vector< FlutterEngineDisplay > * displays
#define MOCK_ENGINE_PROC(proc, mock_impl)
instancetype sharedInstance()
FlutterBackingStoreCreateCallback create_backing_store_callback
Definition embedder.h:2213
FlutterPresentViewCallback present_view_callback
Definition embedder.h:2250
FlutterBackingStoreCollectCallback collect_backing_store_callback
Definition embedder.h:2218
FlutterEngineSendWindowMetricsEventFnPtr SendWindowMetricsEvent
Definition embedder.h:3715
FlutterEngineInitializeFnPtr Initialize
Definition embedder.h:3712
FlutterEngineNotifyDisplayUpdateFnPtr NotifyDisplayUpdate
Definition embedder.h:3745
FlutterEngineSendPlatformMessageFnPtr SendPlatformMessage
Definition embedder.h:3718
FlutterEngineUpdateSemanticsEnabledFnPtr UpdateSemanticsEnabled
Definition embedder.h:3728
FlutterLayerContentType type
Definition embedder.h:2134
const char * identifier
Definition embedder.h:1714
const char * increased_value
Definition embedder.h:1650
const char * tooltip
A textual tooltip attached to the node.
Definition embedder.h:1677
size_t custom_accessibility_actions_count
The number of custom accessibility action associated with this node.
Definition embedder.h:1669
const int32_t * children_in_traversal_order
Array of child node IDs in traversal order. Has length child_count.
Definition embedder.h:1665
int32_t text_selection_extent
The position at which the text selection terminates.
Definition embedder.h:1625
FlutterSemanticsAction actions
The set of semantics actions applicable to this node.
Definition embedder.h:1621
int32_t id
The unique identifier for this node.
Definition embedder.h:1613
size_t child_count
The number of children this node has.
Definition embedder.h:1663
const char * decreased_value
Definition embedder.h:1653
const char * label
A textual description of the node.
Definition embedder.h:1643
int32_t text_selection_base
The position at which the text selection originates.
Definition embedder.h:1623
const char * hint
A brief description of the result of performing an action on the node.
Definition embedder.h:1645
FlutterSemanticsFlags * flags2
Definition embedder.h:1705
const char * value
A textual description of the current value of the node.
Definition embedder.h:1647
A batch of updates to semantics nodes and custom actions.
Definition embedder.h:1796
size_t node_count
The number of semantics node updates.
Definition embedder.h:1800
size_t custom_action_count
The number of semantics custom action updates.
Definition embedder.h:1804
FlutterSemanticsNode2 ** nodes
Definition embedder.h:1802
A structure to represent the width and height.
Definition embedder.h:627
const size_t start
#define CREATE_NATIVE_ENTRY(native_entry)
const uintptr_t id
int BOOL