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