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