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, RegistrarCanReadValuePublishedByAnotherPlugin) {
759 NSString* fixtures = @(flutter::testing::GetFixturesPath());
760 FlutterDartProject* project = [[FlutterDartProject alloc]
761 initWithAssetsPath:fixtures
762 ICUDataPath:[fixtures stringByAppendingPathComponent:@"icudtl.dat"]];
763 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
764 project:project
765 allowHeadlessExecution:YES];
766 NSString* publisherPluginName = @"PublisherPlugin";
767 NSString* readerPluginName = @"ReaderPlugin";
768 id<FlutterPluginRegistrar> publisher = [engine registrarForPlugin:publisherPluginName];
769 id<FlutterPluginRegistrar> reader = [engine registrarForPlugin:readerPluginName];
770
771 NSString* publishedValue = @"A published value";
772 [publisher publish:publishedValue];
773
774 EXPECT_EQ([reader valuePublishedByPlugin:publisherPluginName], publishedValue);
775 EXPECT_EQ([reader valuePublishedByPlugin:@"NoSuchPlugin"], nil);
776 EXPECT_EQ([reader valuePublishedByPlugin:readerPluginName], [NSNull null]);
777}
778
779TEST_F(FlutterEngineTest, RegistrarForwardViewControllerLookUpToEngine) {
780 NSString* fixtures = @(flutter::testing::GetFixturesPath());
781 FlutterDartProject* project = [[FlutterDartProject alloc]
782 initWithAssetsPath:fixtures
783 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
784 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
785
786 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
787 nibName:nil
788 bundle:nil];
789 id<FlutterPluginRegistrar> registrar = [engine registrarForPlugin:@"MyPlugin"];
790
791 EXPECT_EQ([registrar viewController], viewController);
792}
793
794// If a channel overrides a previous channel with the same name, cleaning
795// the previous channel should not affect the new channel.
796//
797// This is important when recreating classes that uses a channel, because the
798// new instance would create the channel before the first class is deallocated
799// and clears the channel.
800TEST_F(FlutterEngineTest, MessengerCleanupConnectionWorks) {
801 FlutterEngine* engine = GetFlutterEngine();
802 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
803
804 NSString* channel = @"_test_";
805 NSData* channel_data = [channel dataUsingEncoding:NSUTF8StringEncoding];
806
807 // Mock SendPlatformMessage so that if a message is sent to
808 // "test/send_message", act as if the framework has sent an empty message to
809 // the channel marked by the `sendOnChannel:message:` call's message.
811 SendPlatformMessage, ([](auto engine_, auto message_) {
812 if (strcmp(message_->channel, "test/send_message") == 0) {
813 // The simplest message that is acceptable to a method channel.
814 std::string message = R"|({"method": "a"})|";
815 std::string channel(reinterpret_cast<const char*>(message_->message),
816 message_->message_size);
817 reinterpret_cast<EmbedderEngine*>(engine_)
818 ->GetShell()
819 .GetPlatformView()
820 ->HandlePlatformMessage(std::make_unique<PlatformMessage>(
821 channel.c_str(), fml::MallocMapping::Copy(message.c_str(), message.length()),
822 fml::RefPtr<PlatformMessageResponse>()));
823 }
824 return kSuccess;
825 }));
826
827 __block int record = 0;
828
829 FlutterMethodChannel* channel1 =
831 binaryMessenger:engine.binaryMessenger
832 codec:[FlutterJSONMethodCodec sharedInstance]];
833 [channel1 setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
834 record += 1;
835 }];
836
837 [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data];
838 EXPECT_EQ(record, 1);
839
840 FlutterMethodChannel* channel2 =
842 binaryMessenger:engine.binaryMessenger
843 codec:[FlutterJSONMethodCodec sharedInstance]];
844 [channel2 setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
845 record += 10;
846 }];
847
848 [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data];
849 EXPECT_EQ(record, 11);
850
851 [channel1 setMethodCallHandler:nil];
852
853 [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data];
854 EXPECT_EQ(record, 21);
855}
856
857TEST_F(FlutterEngineTest, HasStringsWhenPasteboardEmpty) {
858 id engineMock = CreateMockFlutterEngine(nil);
859
860 // Call hasStrings and expect it to be false.
861 __block bool calledAfterClear = false;
862 __block bool valueAfterClear;
863 FlutterResult resultAfterClear = ^(id result) {
864 calledAfterClear = true;
865 NSNumber* valueNumber = [result valueForKey:@"value"];
866 valueAfterClear = [valueNumber boolValue];
867 };
868 FlutterMethodCall* methodCallAfterClear =
869 [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil];
870 [engineMock handleMethodCall:methodCallAfterClear result:resultAfterClear];
871 EXPECT_TRUE(calledAfterClear);
872 EXPECT_FALSE(valueAfterClear);
873}
874
875TEST_F(FlutterEngineTest, HasStringsWhenPasteboardFull) {
876 id engineMock = CreateMockFlutterEngine(@"some string");
877
878 // Call hasStrings and expect it to be true.
879 __block bool called = false;
880 __block bool value;
881 FlutterResult result = ^(id result) {
882 called = true;
883 NSNumber* valueNumber = [result valueForKey:@"value"];
884 value = [valueNumber boolValue];
885 };
886 FlutterMethodCall* methodCall =
887 [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil];
888 [engineMock handleMethodCall:methodCall result:result];
889 EXPECT_TRUE(called);
890 EXPECT_TRUE(value);
891}
892
893TEST_F(FlutterEngineTest, ResponseAfterEngineDied) {
894 FlutterEngine* engine = GetFlutterEngine();
896 initWithName:@"foo"
897 binaryMessenger:engine.binaryMessenger
899 __block BOOL didCallCallback = NO;
900 [channel setMessageHandler:^(id message, FlutterReply callback) {
901 ShutDownEngine();
902 callback(nil);
903 didCallCallback = YES;
904 }];
905 EXPECT_TRUE([engine runWithEntrypoint:@"sendFooMessage"]);
906 engine = nil;
907
908 while (!didCallCallback) {
909 [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
910 }
911}
912
913TEST_F(FlutterEngineTest, ResponseFromBackgroundThread) {
914 FlutterEngine* engine = GetFlutterEngine();
916 initWithName:@"foo"
917 binaryMessenger:engine.binaryMessenger
919 __block BOOL didCallCallback = NO;
920 [channel setMessageHandler:^(id message, FlutterReply callback) {
921 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
922 callback(nil);
923 dispatch_async(dispatch_get_main_queue(), ^{
924 didCallCallback = YES;
925 });
926 });
927 }];
928 EXPECT_TRUE([engine runWithEntrypoint:@"sendFooMessage"]);
929
930 while (!didCallCallback) {
931 [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
932 }
933}
934
935TEST_F(FlutterEngineTest, CanGetEngineForId) {
936 FlutterEngine* engine = GetFlutterEngine();
937
938 BOOL signaled = NO;
939 std::optional<int64_t> engineId;
940 AddNativeCallback("NotifyEngineId", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
941 const auto argument = Dart_GetNativeArgument(args, 0);
942 if (!Dart_IsNull(argument)) {
943 const auto id = tonic::DartConverter<int64_t>::FromDart(argument);
944 engineId = id;
945 }
946 signaled = YES;
947 }));
948
949 EXPECT_TRUE([engine runWithEntrypoint:@"testEngineId"]);
950 while (!signaled) {
951 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
952 }
953
954 EXPECT_TRUE(engineId.has_value());
955 if (!engineId.has_value()) {
956 return;
957 }
958 EXPECT_EQ(engine, [FlutterEngine engineForIdentifier:*engineId]);
959 ShutDownEngine();
960}
961
962TEST_F(FlutterEngineTest, ResizeSynchronizerNotBlockingRasterThreadAfterShutdown) {
963 FlutterResizeSynchronizer* threadSynchronizer = [[FlutterResizeSynchronizer alloc] init];
964 [threadSynchronizer shutDown];
965
966 std::thread rasterThread([&threadSynchronizer] {
967 [threadSynchronizer performCommitForSize:CGSizeMake(100, 100)
968 afterDelay:0
969 notify:^{
970 }];
971 });
972
973 rasterThread.join();
974}
975
976TEST_F(FlutterEngineTest, ManageControllersIfInitiatedByController) {
977 NSString* fixtures = @(flutter::testing::GetFixturesPath());
978 FlutterDartProject* project = [[FlutterDartProject alloc]
979 initWithAssetsPath:fixtures
980 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
981
983 FlutterViewController* viewController1;
984
985 @autoreleasepool {
986 // Create FVC1.
987 viewController1 = [[FlutterViewController alloc] initWithProject:project];
988 EXPECT_EQ(viewController1.viewIdentifier, 0ll);
989
990 engine = viewController1.engine;
992
993 // Create FVC2 based on the same engine.
994 FlutterViewController* viewController2 = [[FlutterViewController alloc] initWithEngine:engine
995 nibName:nil
996 bundle:nil];
997 EXPECT_EQ(engine.viewController, viewController2);
998 }
999 // FVC2 is deallocated but FVC1 is retained.
1000
1001 EXPECT_EQ(engine.viewController, nil);
1002
1003 engine.viewController = viewController1;
1004 EXPECT_EQ(engine.viewController, viewController1);
1005 EXPECT_EQ(viewController1.viewIdentifier, 0ll);
1006}
1007
1008TEST_F(FlutterEngineTest, ManageControllersIfInitiatedByEngine) {
1009 // Don't create the engine with `CreateMockFlutterEngine`, because it adds
1010 // additional references to FlutterViewControllers, which is crucial to this
1011 // test case.
1012 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"io.flutter"
1013 project:nil
1014 allowHeadlessExecution:NO];
1015 FlutterViewController* viewController1;
1016
1017 @autoreleasepool {
1018 viewController1 = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1019 EXPECT_EQ(viewController1.viewIdentifier, 0ll);
1020 EXPECT_EQ(engine.viewController, viewController1);
1021
1022 engine.viewController = nil;
1023
1024 FlutterViewController* viewController2 = [[FlutterViewController alloc] initWithEngine:engine
1025 nibName:nil
1026 bundle:nil];
1027 EXPECT_EQ(viewController2.viewIdentifier, 0ll);
1028 EXPECT_EQ(engine.viewController, viewController2);
1029 }
1030 // FVC2 is deallocated but FVC1 is retained.
1031
1032 EXPECT_EQ(engine.viewController, nil);
1033
1034 engine.viewController = viewController1;
1035 EXPECT_EQ(engine.viewController, viewController1);
1036 EXPECT_EQ(viewController1.viewIdentifier, 0ll);
1037}
1038
1039TEST_F(FlutterEngineTest, RemovingViewDisposesCompositorResources) {
1040 NSString* fixtures = @(flutter::testing::GetFixturesPath());
1041 FlutterDartProject* project = [[FlutterDartProject alloc]
1042 initWithAssetsPath:fixtures
1043 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
1044 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
1045
1046 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
1047 nibName:nil
1048 bundle:nil];
1049 [viewController loadView];
1050 [viewController viewDidLoad];
1051 viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
1052
1053 EXPECT_TRUE([engine runWithEntrypoint:@"drawIntoAllViews"]);
1054 // Wait up to 1 second for Flutter to emit a frame.
1055 CFTimeInterval start = CACurrentMediaTime();
1056 while (engine.macOSCompositor->DebugNumViews() == 0) {
1057 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
1058 if (CACurrentMediaTime() - start > 1) {
1059 break;
1060 }
1061 }
1062
1063 EXPECT_EQ(engine.macOSCompositor->DebugNumViews(), 1u);
1064
1065 engine.viewController = nil;
1066 EXPECT_EQ(engine.macOSCompositor->DebugNumViews(), 0u);
1067
1068 [engine shutDownEngine];
1069 engine = nil;
1070}
1071
1072TEST_F(FlutterEngineTest, HandlesTerminationRequest) {
1073 id engineMock = CreateMockFlutterEngine(nil);
1074 __block NSString* nextResponse = @"exit";
1075 __block BOOL triedToTerminate = NO;
1076 FlutterEngineTerminationHandler* terminationHandler =
1077 [[FlutterEngineTerminationHandler alloc] initWithEngine:engineMock
1078 terminator:^(id sender) {
1079 triedToTerminate = TRUE;
1080 // Don't actually terminate, of course.
1081 }];
1082 OCMStub([engineMock terminationHandler]).andReturn(terminationHandler);
1083 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1084 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1085 [engineMock binaryMessenger])
1086 .andReturn(binaryMessengerMock);
1087 OCMStub([engineMock sendOnChannel:@"flutter/platform"
1088 message:[OCMArg any]
1089 binaryReply:[OCMArg any]])
1090 .andDo((^(NSInvocation* invocation) {
1091 [invocation retainArguments];
1093 NSData* returnedMessage;
1094 [invocation getArgument:&callback atIndex:4];
1095 if ([nextResponse isEqualToString:@"error"]) {
1096 FlutterError* errorResponse = [FlutterError errorWithCode:@"Error"
1097 message:@"Failed"
1098 details:@"Details"];
1099 returnedMessage =
1100 [[FlutterJSONMethodCodec sharedInstance] encodeErrorEnvelope:errorResponse];
1101 } else {
1102 NSDictionary* responseDict = @{@"response" : nextResponse};
1103 returnedMessage =
1104 [[FlutterJSONMethodCodec sharedInstance] encodeSuccessEnvelope:responseDict];
1105 }
1106 callback(returnedMessage);
1107 }));
1108 __block NSString* calledAfterTerminate = @"";
1109 FlutterResult appExitResult = ^(id result) {
1110 NSDictionary* resultDict = result;
1111 calledAfterTerminate = resultDict[@"response"];
1112 };
1113 FlutterMethodCall* methodExitApplication =
1114 [FlutterMethodCall methodCallWithMethodName:@"System.exitApplication"
1115 arguments:@{@"type" : @"cancelable"}];
1116
1117 // Always terminate when the binding isn't ready (which is the default).
1118 triedToTerminate = NO;
1119 calledAfterTerminate = @"";
1120 nextResponse = @"cancel";
1121 [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1122 EXPECT_STREQ([calledAfterTerminate UTF8String], "");
1123 EXPECT_TRUE(triedToTerminate);
1124
1125 // Once the binding is ready, handle the request.
1126 terminationHandler.acceptingRequests = YES;
1127 triedToTerminate = NO;
1128 calledAfterTerminate = @"";
1129 nextResponse = @"exit";
1130 [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1131 EXPECT_STREQ([calledAfterTerminate UTF8String], "exit");
1132 EXPECT_TRUE(triedToTerminate);
1133
1134 triedToTerminate = NO;
1135 calledAfterTerminate = @"";
1136 nextResponse = @"cancel";
1137 [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1138 EXPECT_STREQ([calledAfterTerminate UTF8String], "cancel");
1139 EXPECT_FALSE(triedToTerminate);
1140
1141 // Check that it doesn't crash on error.
1142 triedToTerminate = NO;
1143 calledAfterTerminate = @"";
1144 nextResponse = @"error";
1145 [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1146 EXPECT_STREQ([calledAfterTerminate UTF8String], "");
1147 EXPECT_TRUE(triedToTerminate);
1148}
1149
1150TEST_F(FlutterEngineTest, IgnoresTerminationRequestIfNotFlutterAppDelegate) {
1151 id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1152 id<NSApplicationDelegate> plainDelegate = [[PlainAppDelegate alloc] init];
1153 [NSApplication sharedApplication].delegate = plainDelegate;
1154
1155 // Creating the engine shouldn't fail here, even though the delegate isn't a
1156 // FlutterAppDelegate.
1158
1159 // Asking to terminate the app should cancel.
1160 EXPECT_EQ([[[NSApplication sharedApplication] delegate] applicationShouldTerminate:NSApp],
1161 NSTerminateCancel);
1162
1163 [NSApplication sharedApplication].delegate = previousDelegate;
1164}
1165
1166TEST_F(FlutterEngineTest, HandleAccessibilityEvent) {
1167 __block BOOL announced = NO;
1168 id engineMock = CreateMockFlutterEngine(nil);
1169
1170 OCMStub([engineMock announceAccessibilityMessage:[OCMArg any]
1171 withPriority:NSAccessibilityPriorityMedium])
1172 .andDo((^(NSInvocation* invocation) {
1173 announced = TRUE;
1174 [invocation retainArguments];
1175 NSString* message;
1176 [invocation getArgument:&message atIndex:2];
1177 EXPECT_EQ(message, @"error message");
1178 }));
1179
1180 NSDictionary<NSString*, id>* annotatedEvent =
1181 @{@"type" : @"announce",
1182 @"data" : @{@"message" : @"error message"}};
1183
1184 [engineMock handleAccessibilityEvent:annotatedEvent];
1185
1186 EXPECT_TRUE(announced);
1187}
1188
1189TEST_F(FlutterEngineTest, HandleLifecycleStates) API_AVAILABLE(macos(10.9)) {
1190 __block flutter::AppLifecycleState sentState;
1191 id engineMock = CreateMockFlutterEngine(nil);
1192
1193 // Have to enumerate all the values because OCMStub can't capture
1194 // non-Objective-C object arguments.
1195 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kDetached])
1196 .andDo((^(NSInvocation* invocation) {
1198 }));
1199 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kResumed])
1200 .andDo((^(NSInvocation* invocation) {
1202 }));
1203 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kInactive])
1204 .andDo((^(NSInvocation* invocation) {
1206 }));
1207 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kHidden])
1208 .andDo((^(NSInvocation* invocation) {
1210 }));
1211 OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kPaused])
1212 .andDo((^(NSInvocation* invocation) {
1214 }));
1215
1216 __block NSApplicationOcclusionState visibility = NSApplicationOcclusionStateVisible;
1217 id mockApplication = OCMPartialMock([NSApplication sharedApplication]);
1218 OCMStub((NSApplicationOcclusionState)[mockApplication occlusionState])
1219 .andDo(^(NSInvocation* invocation) {
1220 [invocation setReturnValue:&visibility];
1221 });
1222
1223 NSNotification* willBecomeActive =
1224 [[NSNotification alloc] initWithName:NSApplicationWillBecomeActiveNotification
1225 object:nil
1226 userInfo:nil];
1227 NSNotification* willResignActive =
1228 [[NSNotification alloc] initWithName:NSApplicationWillResignActiveNotification
1229 object:nil
1230 userInfo:nil];
1231
1232 NSNotification* didChangeOcclusionState;
1233 didChangeOcclusionState =
1234 [[NSNotification alloc] initWithName:NSApplicationDidChangeOcclusionStateNotification
1235 object:nil
1236 userInfo:nil];
1237
1238 [engineMock handleDidChangeOcclusionState:didChangeOcclusionState];
1239 EXPECT_EQ(sentState, flutter::AppLifecycleState::kInactive);
1240
1241 [engineMock handleWillBecomeActive:willBecomeActive];
1242 EXPECT_EQ(sentState, flutter::AppLifecycleState::kResumed);
1243
1244 [engineMock handleWillResignActive:willResignActive];
1245 EXPECT_EQ(sentState, flutter::AppLifecycleState::kInactive);
1246
1247 visibility = 0;
1248 [engineMock handleDidChangeOcclusionState:didChangeOcclusionState];
1249 EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden);
1250
1251 [engineMock handleWillBecomeActive:willBecomeActive];
1252 EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden);
1253
1254 [engineMock handleWillResignActive:willResignActive];
1255 EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden);
1256
1257 [mockApplication stopMocking];
1258}
1259
1260TEST_F(FlutterEngineTest, ForwardsPluginDelegateRegistration) {
1261 id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1262 FakeLifecycleProvider* fakeAppDelegate = [[FakeLifecycleProvider alloc] init];
1263 [NSApplication sharedApplication].delegate = fakeAppDelegate;
1264
1265 FakeAppDelegatePlugin* plugin = [[FakeAppDelegatePlugin alloc] init];
1267
1268 [[engine registrarForPlugin:@"TestPlugin"] addApplicationDelegate:plugin];
1269
1270 EXPECT_TRUE([fakeAppDelegate hasDelegate:plugin]);
1271
1272 [NSApplication sharedApplication].delegate = previousDelegate;
1273}
1274
1275TEST_F(FlutterEngineTest, UnregistersPluginsOnEngineDestruction) {
1276 id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1277 FakeLifecycleProvider* fakeAppDelegate = [[FakeLifecycleProvider alloc] init];
1278 [NSApplication sharedApplication].delegate = fakeAppDelegate;
1279
1280 FakeAppDelegatePlugin* plugin = [[FakeAppDelegatePlugin alloc] init];
1281
1282 @autoreleasepool {
1283 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil];
1284
1285 [[engine registrarForPlugin:@"TestPlugin"] addApplicationDelegate:plugin];
1286 EXPECT_TRUE([fakeAppDelegate hasDelegate:plugin]);
1287 }
1288
1289 // When the engine is released, it should unregister any plugins it had
1290 // registered on its behalf.
1291 EXPECT_FALSE([fakeAppDelegate hasDelegate:plugin]);
1292
1293 [NSApplication sharedApplication].delegate = previousDelegate;
1294}
1295
1296TEST_F(FlutterEngineTest, RunWithEntrypointUpdatesDisplayConfig) {
1297 BOOL updated = NO;
1298 FlutterEngine* engine = GetFlutterEngine();
1299 auto original_update_displays = engine.embedderAPI.NotifyDisplayUpdate;
1301 NotifyDisplayUpdate, ([&updated, &original_update_displays](
1302 auto engine, auto update_type, auto* displays, auto display_count) {
1303 updated = YES;
1304 return original_update_displays(engine, update_type, displays, display_count);
1305 }));
1306
1307 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
1308 EXPECT_TRUE(updated);
1309
1310 updated = NO;
1311 [[NSNotificationCenter defaultCenter]
1312 postNotificationName:NSApplicationDidChangeScreenParametersNotification
1313 object:nil];
1314 EXPECT_TRUE(updated);
1315}
1316
1317TEST_F(FlutterEngineTest, NotificationsUpdateDisplays) {
1318 BOOL updated = NO;
1319 FlutterEngine* engine = GetFlutterEngine();
1320 auto original_set_viewport_metrics = engine.embedderAPI.SendWindowMetricsEvent;
1322 SendWindowMetricsEvent,
1323 ([&updated, &original_set_viewport_metrics](auto engine, auto* window_metrics) {
1324 updated = YES;
1325 return original_set_viewport_metrics(engine, window_metrics);
1326 }));
1327
1328 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
1329
1330 updated = NO;
1331 [[NSNotificationCenter defaultCenter] postNotificationName:NSWindowDidChangeScreenNotification
1332 object:nil];
1333 // No VC.
1334 EXPECT_FALSE(updated);
1335
1336 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
1337 nibName:nil
1338 bundle:nil];
1339 [viewController loadView];
1340 viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
1341
1342 [[NSNotificationCenter defaultCenter] postNotificationName:NSWindowDidChangeScreenNotification
1343 object:nil];
1344 EXPECT_TRUE(updated);
1345}
1346
1347TEST_F(FlutterEngineTest, DisplaySizeIsInPhysicalPixel) {
1348 NSString* fixtures = @(testing::GetFixturesPath());
1349 FlutterDartProject* project = [[FlutterDartProject alloc]
1350 initWithAssetsPath:fixtures
1351 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
1352 project.rootIsolateCreateCallback = FlutterEngineTest::IsolateCreateCallback;
1353 MockableFlutterEngine* engine = [[MockableFlutterEngine alloc] initWithName:@"foobar"
1354 project:project
1355 allowHeadlessExecution:true];
1356 BOOL updated = NO;
1357 auto original_update_displays = engine.embedderAPI.NotifyDisplayUpdate;
1359 NotifyDisplayUpdate, ([&updated, &original_update_displays](
1360 auto engine, auto update_type, auto* displays, auto display_count) {
1361 EXPECT_EQ(display_count, 1UL);
1362 EXPECT_EQ(displays->display_id, 10UL);
1363 EXPECT_EQ(displays->width, 60UL);
1364 EXPECT_EQ(displays->height, 80UL);
1365 EXPECT_EQ(displays->device_pixel_ratio, 2UL);
1366 updated = YES;
1367 return original_update_displays(engine, update_type, displays, display_count);
1368 }));
1369 EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
1370 EXPECT_TRUE(updated);
1371 [engine shutDownEngine];
1372 engine = nil;
1373}
1374
1375TEST_F(FlutterEngineTest, ReportsHourFormat) {
1376 __block BOOL expectedValue;
1377
1378 // Set up mocks.
1379 id channelMock = OCMClassMock([FlutterBasicMessageChannel class]);
1380 OCMStub([channelMock messageChannelWithName:@"flutter/settings"
1381 binaryMessenger:[OCMArg any]
1382 codec:[OCMArg any]])
1383 .andReturn(channelMock);
1384 OCMStub([channelMock sendMessage:[OCMArg any]]).andDo((^(NSInvocation* invocation) {
1385 __weak id message;
1386 [invocation getArgument:&message atIndex:2];
1387 EXPECT_EQ(message[@"alwaysUse24HourFormat"], @(expectedValue));
1388 }));
1389
1390 id mockHourFormat = OCMClassMock([FlutterHourFormat class]);
1391 OCMStub([mockHourFormat isAlwaysUse24HourFormat]).andDo((^(NSInvocation* invocation) {
1392 [invocation setReturnValue:&expectedValue];
1393 }));
1394
1395 id engineMock = CreateMockFlutterEngine(nil);
1396
1397 // Verify the YES case.
1398 expectedValue = YES;
1399 EXPECT_TRUE([engineMock runWithEntrypoint:@"main"]);
1400 [engineMock shutDownEngine];
1401
1402 // Verify the NO case.
1403 expectedValue = NO;
1404 EXPECT_TRUE([engineMock runWithEntrypoint:@"main"]);
1405 [engineMock shutDownEngine];
1406
1407 // Clean up mocks.
1408 [mockHourFormat stopMocking];
1409 [engineMock stopMocking];
1410 [channelMock stopMocking];
1411}
1412
1413} // namespace flutter::testing
1414
1415// 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:2157
@ kSuccess
Definition embedder.h:73
FlutterSemanticsAction
Definition embedder.h:122
FlutterEngine engine
Definition main.cc:84
const char * message
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
const gchar * channel
G_BEGIN_DECLS FlutterViewId view_id
const FlutterLayer size_t layers_count
const FlutterLayer ** layers
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:2268
FlutterPresentViewCallback present_view_callback
Definition embedder.h:2305
FlutterBackingStoreCollectCallback collect_backing_store_callback
Definition embedder.h:2273
FlutterEngineSendWindowMetricsEventFnPtr SendWindowMetricsEvent
Definition embedder.h:3774
FlutterEngineInitializeFnPtr Initialize
Definition embedder.h:3771
FlutterEngineNotifyDisplayUpdateFnPtr NotifyDisplayUpdate
Definition embedder.h:3804
FlutterEngineSendPlatformMessageFnPtr SendPlatformMessage
Definition embedder.h:3777
FlutterEngineUpdateSemanticsEnabledFnPtr UpdateSemanticsEnabled
Definition embedder.h:3787
FlutterLayerContentType type
Definition embedder.h:2189
const char * identifier
Definition embedder.h:1769
const char * increased_value
Definition embedder.h:1705
const char * tooltip
A textual tooltip attached to the node.
Definition embedder.h:1732
size_t custom_accessibility_actions_count
The number of custom accessibility action associated with this node.
Definition embedder.h:1724
const int32_t * children_in_traversal_order
Array of child node IDs in traversal order. Has length child_count.
Definition embedder.h:1720
int32_t text_selection_extent
The position at which the text selection terminates.
Definition embedder.h:1680
FlutterSemanticsAction actions
The set of semantics actions applicable to this node.
Definition embedder.h:1676
int32_t id
The unique identifier for this node.
Definition embedder.h:1668
size_t child_count
The number of children this node has.
Definition embedder.h:1718
const char * decreased_value
Definition embedder.h:1708
const char * label
A textual description of the node.
Definition embedder.h:1698
int32_t text_selection_base
The position at which the text selection originates.
Definition embedder.h:1678
const char * hint
A brief description of the result of performing an action on the node.
Definition embedder.h:1700
FlutterSemanticsFlags * flags2
Definition embedder.h:1760
const char * value
A textual description of the current value of the node.
Definition embedder.h:1702
A batch of updates to semantics nodes and custom actions.
Definition embedder.h:1851
size_t node_count
The number of semantics node updates.
Definition embedder.h:1855
size_t custom_action_count
The number of semantics custom action updates.
Definition embedder.h:1859
FlutterSemanticsNode2 ** nodes
Definition embedder.h:1857
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