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  root.identifier = "";
342 
343  FlutterSemanticsNode2 child1;
344  child1.id = 1;
345  child1.flags2 = &child_flags;
346  // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange)
347  child1.actions = static_cast<FlutterSemanticsAction>(0);
348  child1.text_selection_base = -1;
349  child1.text_selection_extent = -1;
350  child1.label = "child 1";
351  child1.hint = "";
352  child1.value = "";
353  child1.increased_value = "";
354  child1.decreased_value = "";
355  child1.tooltip = "";
356  child1.child_count = 0;
357  child1.custom_accessibility_actions_count = 0;
358  child1.identifier = "";
359 
360  FlutterSemanticsUpdate2 update;
361  update.node_count = 2;
362  FlutterSemanticsNode2* nodes[] = {&root, &child1};
363  update.nodes = nodes;
364  update.custom_action_count = 0;
365  update_semantics_callback(&update, (__bridge void*)engine);
366 
367  // Verify the accessibility tree is attached to the flutter view.
368  EXPECT_EQ([engine.viewController.flutterView.accessibilityChildren count], 1u);
369  NSAccessibilityElement* native_root = engine.viewController.flutterView.accessibilityChildren[0];
370  std::string root_label = [native_root.accessibilityLabel UTF8String];
371  EXPECT_TRUE(root_label == "root");
372  EXPECT_EQ(native_root.accessibilityRole, NSAccessibilityGroupRole);
373  EXPECT_EQ([native_root.accessibilityChildren count], 1u);
374  NSAccessibilityElement* native_child1 = native_root.accessibilityChildren[0];
375  std::string child1_value = [native_child1.accessibilityValue UTF8String];
376  EXPECT_TRUE(child1_value == "child 1");
377  EXPECT_EQ(native_child1.accessibilityRole, NSAccessibilityStaticTextRole);
378  EXPECT_EQ([native_child1.accessibilityChildren count], 0u);
379  // Disable the semantics.
380  bool semanticsEnabled = true;
381  engine.embedderAPI.UpdateSemanticsEnabled =
382  MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&semanticsEnabled](auto engine, bool enabled) {
383  semanticsEnabled = enabled;
384  return kSuccess;
385  }));
386  engine.semanticsEnabled = NO;
387  EXPECT_FALSE(semanticsEnabled);
388  // Verify the accessibility tree is removed from the view.
389  EXPECT_EQ([engine.viewController.flutterView.accessibilityChildren count], 0u);
390 
391  [engine setViewController:nil];
392 }
393 
394 TEST_F(FlutterEngineTest, CanToggleAccessibilityWhenHeadless) {
395  FlutterEngine* engine = GetFlutterEngine();
396  // Capture the update callbacks before the embedder API initializes.
397  auto original_init = engine.embedderAPI.Initialize;
398  std::function<void(const FlutterSemanticsUpdate2*, void*)> update_semantics_callback;
399  engine.embedderAPI.Initialize = MOCK_ENGINE_PROC(
400  Initialize, ([&update_semantics_callback, &original_init](
401  size_t version, const FlutterRendererConfig* config,
402  const FlutterProjectArgs* args, void* user_data, auto engine_out) {
403  update_semantics_callback = args->update_semantics_callback2;
404  return original_init(version, config, args, user_data, engine_out);
405  }));
406  EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
407 
408  // Enable the semantics without attaching a view controller.
409  bool enabled_called = false;
410  engine.embedderAPI.UpdateSemanticsEnabled =
411  MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&enabled_called](auto engine, bool enabled) {
412  enabled_called = enabled;
413  return kSuccess;
414  }));
415  engine.semanticsEnabled = YES;
416  EXPECT_TRUE(enabled_called);
417  // Send flutter semantics updates.
418  FlutterSemanticsNode2 root;
419  FlutterSemanticsFlags flags = FlutterSemanticsFlags{0};
420  FlutterSemanticsFlags child_flags = FlutterSemanticsFlags{0};
421  root.id = 0;
422  root.flags2 = &flags;
423  // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange)
424  root.actions = static_cast<FlutterSemanticsAction>(0);
425  root.text_selection_base = -1;
426  root.text_selection_extent = -1;
427  root.label = "root";
428  root.hint = "";
429  root.value = "";
430  root.increased_value = "";
431  root.decreased_value = "";
432  root.tooltip = "";
433  root.child_count = 1;
434  int32_t children[] = {1};
435  root.children_in_traversal_order = children;
436  root.custom_accessibility_actions_count = 0;
437 
438  FlutterSemanticsNode2 child1;
439  child1.id = 1;
440  child1.flags2 = &child_flags;
441  // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange)
442  child1.actions = static_cast<FlutterSemanticsAction>(0);
443  child1.text_selection_base = -1;
444  child1.text_selection_extent = -1;
445  child1.label = "child 1";
446  child1.hint = "";
447  child1.value = "";
448  child1.increased_value = "";
449  child1.decreased_value = "";
450  child1.tooltip = "";
451  child1.child_count = 0;
452  child1.custom_accessibility_actions_count = 0;
453 
454  FlutterSemanticsUpdate2 update;
455  update.node_count = 2;
456  FlutterSemanticsNode2* nodes[] = {&root, &child1};
457  update.nodes = nodes;
458  update.custom_action_count = 0;
459  // This call updates semantics for the implicit view, which does not exist,
460  // and therefore this call is invalid. But the engine should not crash.
461  update_semantics_callback(&update, (__bridge void*)engine);
462 
463  // No crashes.
464  EXPECT_EQ(engine.viewController, nil);
465 
466  // Disable the semantics.
467  bool semanticsEnabled = true;
468  engine.embedderAPI.UpdateSemanticsEnabled =
469  MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&semanticsEnabled](auto engine, bool enabled) {
470  semanticsEnabled = enabled;
471  return kSuccess;
472  }));
473  engine.semanticsEnabled = NO;
474  EXPECT_FALSE(semanticsEnabled);
475  // Still no crashes
476  EXPECT_EQ(engine.viewController, nil);
477 }
478 
479 TEST_F(FlutterEngineTest, ProducesAccessibilityTreeWhenAddingViews) {
480  FlutterEngine* engine = GetFlutterEngine();
481  EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
482 
483  // Enable the semantics without attaching a view controller.
484  bool enabled_called = false;
485  engine.embedderAPI.UpdateSemanticsEnabled =
486  MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&enabled_called](auto engine, bool enabled) {
487  enabled_called = enabled;
488  return kSuccess;
489  }));
490  engine.semanticsEnabled = YES;
491  EXPECT_TRUE(enabled_called);
492 
493  EXPECT_EQ(engine.viewController, nil);
494 
495  // Assign the view controller after enabling semantics
496  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
497  nibName:nil
498  bundle:nil];
499  engine.viewController = viewController;
500 
501  EXPECT_NE(viewController.accessibilityBridge.lock(), nullptr);
502 }
503 
504 TEST_F(FlutterEngineTest, NativeCallbacks) {
505  BOOL latch_called = NO;
506  AddNativeCallback("SignalNativeTest",
507  CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch_called = YES; }));
508 
509  FlutterEngine* engine = GetFlutterEngine();
510  EXPECT_TRUE([engine runWithEntrypoint:@"nativeCallback"]);
511  ASSERT_TRUE(engine.running);
512 
513  while (!latch_called) {
514  CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
515  }
516  ASSERT_TRUE(latch_called);
517 }
518 
519 TEST_F(FlutterEngineTest, Compositor) {
520  NSString* fixtures = @(flutter::testing::GetFixturesPath());
521  FlutterDartProject* project = [[FlutterDartProject alloc]
522  initWithAssetsPath:fixtures
523  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
524  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
525 
526  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
527  nibName:nil
528  bundle:nil];
529  [viewController loadView];
530  [viewController viewDidLoad];
531  viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
532 
533  EXPECT_TRUE([engine runWithEntrypoint:@"canCompositePlatformViews"]);
534 
535  [engine.platformViewController registerViewFactory:[[TestPlatformViewFactory alloc] init]
536  withId:@"factory_id"];
537  [engine.platformViewController
538  handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"create"
539  arguments:@{
540  @"id" : @(42),
541  @"viewType" : @"factory_id",
542  }]
543  result:^(id result){
544  }];
545 
546  // Wait up to 1 second for Flutter to emit a frame.
547  CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
548  CALayer* rootLayer = viewController.flutterView.layer;
549  while (rootLayer.sublayers.count == 0) {
550  CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
551  if (CFAbsoluteTimeGetCurrent() - start > 1) {
552  break;
553  }
554  }
555 
556  // There are two layers with Flutter contents and one view
557  EXPECT_EQ(rootLayer.sublayers.count, 2u);
558  EXPECT_EQ(viewController.flutterView.subviews.count, 1u);
559 
560  // TODO(gw280): add support for screenshot tests in this test harness
561 
562  [engine shutDownEngine];
563 }
564 
565 TEST_F(FlutterEngineTest, CompositorIgnoresUnknownView) {
566  FlutterEngine* engine = GetFlutterEngine();
567  auto original_init = engine.embedderAPI.Initialize;
568  ::FlutterCompositor compositor;
569  engine.embedderAPI.Initialize = MOCK_ENGINE_PROC(
570  Initialize, ([&compositor, &original_init](
571  size_t version, const FlutterRendererConfig* config,
572  const FlutterProjectArgs* args, void* user_data, auto engine_out) {
573  compositor = *args->compositor;
574  return original_init(version, config, args, user_data, engine_out);
575  }));
576 
577  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
578  nibName:nil
579  bundle:nil];
580  [viewController loadView];
581 
582  EXPECT_TRUE([engine runWithEntrypoint:@"empty"]);
583 
584  FlutterBackingStoreConfig config = {
585  .struct_size = sizeof(FlutterBackingStoreConfig),
586  .size = FlutterSize{10, 10},
587  };
588  FlutterBackingStore backing_store = {};
589  EXPECT_NE(compositor.create_backing_store_callback, nullptr);
590  EXPECT_TRUE(
591  compositor.create_backing_store_callback(&config, &backing_store, compositor.user_data));
592 
593  FlutterLayer layer{
594  .type = kFlutterLayerContentTypeBackingStore,
595  .backing_store = &backing_store,
596  };
597  std::vector<FlutterLayer*> layers = {&layer};
598 
599  FlutterPresentViewInfo info = {
600  .struct_size = sizeof(FlutterPresentViewInfo),
601  .view_id = 123,
602  .layers = const_cast<const FlutterLayer**>(layers.data()),
603  .layers_count = 1,
604  .user_data = compositor.user_data,
605  };
606  EXPECT_NE(compositor.present_view_callback, nullptr);
607  EXPECT_FALSE(compositor.present_view_callback(&info));
608  EXPECT_TRUE(compositor.collect_backing_store_callback(&backing_store, compositor.user_data));
609 
610  (void)viewController;
611  [engine shutDownEngine];
612 }
613 
614 TEST_F(FlutterEngineTest, DartEntrypointArguments) {
615  NSString* fixtures = @(flutter::testing::GetFixturesPath());
616  FlutterDartProject* project = [[FlutterDartProject alloc]
617  initWithAssetsPath:fixtures
618  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
619 
620  project.dartEntrypointArguments = @[ @"arg1", @"arg2" ];
621  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
622 
623  bool called = false;
624  auto original_init = engine.embedderAPI.Initialize;
625  engine.embedderAPI.Initialize = MOCK_ENGINE_PROC(
626  Initialize, ([&called, &original_init](size_t version, const FlutterRendererConfig* config,
627  const FlutterProjectArgs* args, void* user_data,
628  FLUTTER_API_SYMBOL(FlutterEngine) * engine_out) {
629  called = true;
630  EXPECT_EQ(args->dart_entrypoint_argc, 2);
631  NSString* arg1 = [[NSString alloc] initWithCString:args->dart_entrypoint_argv[0]
632  encoding:NSUTF8StringEncoding];
633  NSString* arg2 = [[NSString alloc] initWithCString:args->dart_entrypoint_argv[1]
634  encoding:NSUTF8StringEncoding];
635 
636  EXPECT_TRUE([arg1 isEqualToString:@"arg1"]);
637  EXPECT_TRUE([arg2 isEqualToString:@"arg2"]);
638 
639  return original_init(version, config, args, user_data, engine_out);
640  }));
641 
642  EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
643  EXPECT_TRUE(called);
644  [engine shutDownEngine];
645 }
646 
647 // Verify that the engine is not retained indirectly via the binary messenger held by channels and
648 // plugins. Previously, FlutterEngine.binaryMessenger returned the engine itself, and thus plugins
649 // could cause a retain cycle, preventing the engine from being deallocated.
650 // FlutterEngine.binaryMessenger now returns a FlutterBinaryMessengerRelay whose weak pointer back
651 // to the engine is cleared when the engine is deallocated.
652 // Issue: https://github.com/flutter/flutter/issues/116445
653 TEST_F(FlutterEngineTest, FlutterBinaryMessengerDoesNotRetainEngine) {
654  __weak FlutterEngine* weakEngine;
655  id<FlutterBinaryMessenger> binaryMessenger = nil;
656  @autoreleasepool {
657  // Create a test engine.
658  NSString* fixtures = @(flutter::testing::GetFixturesPath());
659  FlutterDartProject* project = [[FlutterDartProject alloc]
660  initWithAssetsPath:fixtures
661  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
662  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
663  project:project
664  allowHeadlessExecution:YES];
665  weakEngine = engine;
666  binaryMessenger = engine.binaryMessenger;
667  }
668 
669  // Once the engine has been deallocated, verify the weak engine pointer is nil, and thus not
670  // retained by the relay.
671  EXPECT_NE(binaryMessenger, nil);
672  EXPECT_EQ(weakEngine, nil);
673 }
674 
675 // Verify that the engine is not retained indirectly via the texture registry held by plugins.
676 // Issue: https://github.com/flutter/flutter/issues/116445
677 TEST_F(FlutterEngineTest, FlutterTextureRegistryDoesNotReturnEngine) {
678  __weak FlutterEngine* weakEngine;
679  id<FlutterTextureRegistry> textureRegistry;
680  @autoreleasepool {
681  // Create a test engine.
682  NSString* fixtures = @(flutter::testing::GetFixturesPath());
683  FlutterDartProject* project = [[FlutterDartProject alloc]
684  initWithAssetsPath:fixtures
685  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
686  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
687  project:project
688  allowHeadlessExecution:YES];
689  id<FlutterPluginRegistrar> registrar = [engine registrarForPlugin:@"MyPlugin"];
690  textureRegistry = registrar.textures;
691  }
692 
693  // Once the engine has been deallocated, verify the weak engine pointer is nil, and thus not
694  // retained via the texture registry.
695  EXPECT_NE(textureRegistry, nil);
696  EXPECT_EQ(weakEngine, nil);
697 }
698 
699 TEST_F(FlutterEngineTest, PublishedValueNilForUnknownPlugin) {
700  NSString* fixtures = @(flutter::testing::GetFixturesPath());
701  FlutterDartProject* project = [[FlutterDartProject alloc]
702  initWithAssetsPath:fixtures
703  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
704  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
705  project:project
706  allowHeadlessExecution:YES];
707 
708  EXPECT_EQ([engine valuePublishedByPlugin:@"NoSuchPlugin"], nil);
709 }
710 
711 TEST_F(FlutterEngineTest, PublishedValueNSNullIfNoPublishedValue) {
712  NSString* fixtures = @(flutter::testing::GetFixturesPath());
713  FlutterDartProject* project = [[FlutterDartProject alloc]
714  initWithAssetsPath:fixtures
715  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
716  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
717  project:project
718  allowHeadlessExecution:YES];
719  NSString* pluginName = @"MyPlugin";
720  // Request the registarar to register the plugin as existing.
721  [engine registrarForPlugin:pluginName];
722 
723  // The documented behavior is that a plugin that exists but hasn't published
724  // anything returns NSNull, rather than nil, as on iOS.
725  EXPECT_EQ([engine valuePublishedByPlugin:pluginName], [NSNull null]);
726 }
727 
728 TEST_F(FlutterEngineTest, PublishedValueReturnsLastPublished) {
729  NSString* fixtures = @(flutter::testing::GetFixturesPath());
730  FlutterDartProject* project = [[FlutterDartProject alloc]
731  initWithAssetsPath:fixtures
732  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
733  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
734  project:project
735  allowHeadlessExecution:YES];
736  NSString* pluginName = @"MyPlugin";
737  id<FlutterPluginRegistrar> registrar = [engine registrarForPlugin:pluginName];
738 
739  NSString* firstValue = @"A published value";
740  NSArray* secondValue = @[ @"A different published value" ];
741 
742  [registrar publish:firstValue];
743  EXPECT_EQ([engine valuePublishedByPlugin:pluginName], firstValue);
744 
745  [registrar publish:secondValue];
746  EXPECT_EQ([engine valuePublishedByPlugin:pluginName], secondValue);
747 }
748 
749 TEST_F(FlutterEngineTest, RegistrarForwardViewControllerLookUpToEngine) {
750  NSString* fixtures = @(flutter::testing::GetFixturesPath());
751  FlutterDartProject* project = [[FlutterDartProject alloc]
752  initWithAssetsPath:fixtures
753  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
754  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
755 
756  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
757  nibName:nil
758  bundle:nil];
759  id<FlutterPluginRegistrar> registrar = [engine registrarForPlugin:@"MyPlugin"];
760 
761  EXPECT_EQ([registrar viewController], viewController);
762 }
763 
764 // If a channel overrides a previous channel with the same name, cleaning
765 // the previous channel should not affect the new channel.
766 //
767 // This is important when recreating classes that uses a channel, because the
768 // new instance would create the channel before the first class is deallocated
769 // and clears the channel.
770 TEST_F(FlutterEngineTest, MessengerCleanupConnectionWorks) {
771  FlutterEngine* engine = GetFlutterEngine();
772  EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
773 
774  NSString* channel = @"_test_";
775  NSData* channel_data = [channel dataUsingEncoding:NSUTF8StringEncoding];
776 
777  // Mock SendPlatformMessage so that if a message is sent to
778  // "test/send_message", act as if the framework has sent an empty message to
779  // the channel marked by the `sendOnChannel:message:` call's message.
780  engine.embedderAPI.SendPlatformMessage = MOCK_ENGINE_PROC(
781  SendPlatformMessage, ([](auto engine_, auto message_) {
782  if (strcmp(message_->channel, "test/send_message") == 0) {
783  // The simplest message that is acceptable to a method channel.
784  std::string message = R"|({"method": "a"})|";
785  std::string channel(reinterpret_cast<const char*>(message_->message),
786  message_->message_size);
787  reinterpret_cast<EmbedderEngine*>(engine_)
788  ->GetShell()
789  .GetPlatformView()
790  ->HandlePlatformMessage(std::make_unique<PlatformMessage>(
791  channel.c_str(), fml::MallocMapping::Copy(message.c_str(), message.length()),
792  fml::RefPtr<PlatformMessageResponse>()));
793  }
794  return kSuccess;
795  }));
796 
797  __block int record = 0;
798 
799  FlutterMethodChannel* channel1 =
801  binaryMessenger:engine.binaryMessenger
802  codec:[FlutterJSONMethodCodec sharedInstance]];
803  [channel1 setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
804  record += 1;
805  }];
806 
807  [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data];
808  EXPECT_EQ(record, 1);
809 
810  FlutterMethodChannel* channel2 =
812  binaryMessenger:engine.binaryMessenger
813  codec:[FlutterJSONMethodCodec sharedInstance]];
814  [channel2 setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
815  record += 10;
816  }];
817 
818  [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data];
819  EXPECT_EQ(record, 11);
820 
821  [channel1 setMethodCallHandler:nil];
822 
823  [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data];
824  EXPECT_EQ(record, 21);
825 }
826 
827 TEST_F(FlutterEngineTest, HasStringsWhenPasteboardEmpty) {
828  id engineMock = CreateMockFlutterEngine(nil);
829 
830  // Call hasStrings and expect it to be false.
831  __block bool calledAfterClear = false;
832  __block bool valueAfterClear;
833  FlutterResult resultAfterClear = ^(id result) {
834  calledAfterClear = true;
835  NSNumber* valueNumber = [result valueForKey:@"value"];
836  valueAfterClear = [valueNumber boolValue];
837  };
838  FlutterMethodCall* methodCallAfterClear =
839  [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil];
840  [engineMock handleMethodCall:methodCallAfterClear result:resultAfterClear];
841  EXPECT_TRUE(calledAfterClear);
842  EXPECT_FALSE(valueAfterClear);
843 }
844 
845 TEST_F(FlutterEngineTest, HasStringsWhenPasteboardFull) {
846  id engineMock = CreateMockFlutterEngine(@"some string");
847 
848  // Call hasStrings and expect it to be true.
849  __block bool called = false;
850  __block bool value;
851  FlutterResult result = ^(id result) {
852  called = true;
853  NSNumber* valueNumber = [result valueForKey:@"value"];
854  value = [valueNumber boolValue];
855  };
856  FlutterMethodCall* methodCall =
857  [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil];
858  [engineMock handleMethodCall:methodCall result:result];
859  EXPECT_TRUE(called);
860  EXPECT_TRUE(value);
861 }
862 
863 TEST_F(FlutterEngineTest, ResponseAfterEngineDied) {
864  FlutterEngine* engine = GetFlutterEngine();
866  initWithName:@"foo"
867  binaryMessenger:engine.binaryMessenger
869  __block BOOL didCallCallback = NO;
870  [channel setMessageHandler:^(id message, FlutterReply callback) {
871  ShutDownEngine();
872  callback(nil);
873  didCallCallback = YES;
874  }];
875  EXPECT_TRUE([engine runWithEntrypoint:@"sendFooMessage"]);
876  engine = nil;
877 
878  while (!didCallCallback) {
879  [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
880  }
881 }
882 
883 TEST_F(FlutterEngineTest, ResponseFromBackgroundThread) {
884  FlutterEngine* engine = GetFlutterEngine();
886  initWithName:@"foo"
887  binaryMessenger:engine.binaryMessenger
889  __block BOOL didCallCallback = NO;
890  [channel setMessageHandler:^(id message, FlutterReply callback) {
891  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
892  callback(nil);
893  dispatch_async(dispatch_get_main_queue(), ^{
894  didCallCallback = YES;
895  });
896  });
897  }];
898  EXPECT_TRUE([engine runWithEntrypoint:@"sendFooMessage"]);
899 
900  while (!didCallCallback) {
901  [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
902  }
903 }
904 
905 TEST_F(FlutterEngineTest, CanGetEngineForId) {
906  FlutterEngine* engine = GetFlutterEngine();
907 
908  BOOL signaled = NO;
909  std::optional<int64_t> engineId;
910  AddNativeCallback("NotifyEngineId", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
911  const auto argument = Dart_GetNativeArgument(args, 0);
912  if (!Dart_IsNull(argument)) {
913  const auto id = tonic::DartConverter<int64_t>::FromDart(argument);
914  engineId = id;
915  }
916  signaled = YES;
917  }));
918 
919  EXPECT_TRUE([engine runWithEntrypoint:@"testEngineId"]);
920  while (!signaled) {
921  CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
922  }
923 
924  EXPECT_TRUE(engineId.has_value());
925  if (!engineId.has_value()) {
926  return;
927  }
928  EXPECT_EQ(engine, [FlutterEngine engineForIdentifier:*engineId]);
929  ShutDownEngine();
930 }
931 
932 TEST_F(FlutterEngineTest, ResizeSynchronizerNotBlockingRasterThreadAfterShutdown) {
933  FlutterResizeSynchronizer* threadSynchronizer = [[FlutterResizeSynchronizer alloc] init];
934  [threadSynchronizer shutDown];
935 
936  std::thread rasterThread([&threadSynchronizer] {
937  [threadSynchronizer performCommitForSize:CGSizeMake(100, 100)
938  afterDelay:0
939  notify:^{
940  }];
941  });
942 
943  rasterThread.join();
944 }
945 
946 TEST_F(FlutterEngineTest, ManageControllersIfInitiatedByController) {
947  NSString* fixtures = @(flutter::testing::GetFixturesPath());
948  FlutterDartProject* project = [[FlutterDartProject alloc]
949  initWithAssetsPath:fixtures
950  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
951 
952  FlutterEngine* engine;
953  FlutterViewController* viewController1;
954 
955  @autoreleasepool {
956  // Create FVC1.
957  viewController1 = [[FlutterViewController alloc] initWithProject:project];
958  EXPECT_EQ(viewController1.viewIdentifier, 0ll);
959 
960  engine = viewController1.engine;
961  engine.viewController = nil;
962 
963  // Create FVC2 based on the same engine.
964  FlutterViewController* viewController2 = [[FlutterViewController alloc] initWithEngine:engine
965  nibName:nil
966  bundle:nil];
967  EXPECT_EQ(engine.viewController, viewController2);
968  }
969  // FVC2 is deallocated but FVC1 is retained.
970 
971  EXPECT_EQ(engine.viewController, nil);
972 
973  engine.viewController = viewController1;
974  EXPECT_EQ(engine.viewController, viewController1);
975  EXPECT_EQ(viewController1.viewIdentifier, 0ll);
976 }
977 
978 TEST_F(FlutterEngineTest, ManageControllersIfInitiatedByEngine) {
979  // Don't create the engine with `CreateMockFlutterEngine`, because it adds
980  // additional references to FlutterViewControllers, which is crucial to this
981  // test case.
982  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"io.flutter"
983  project:nil
984  allowHeadlessExecution:NO];
985  FlutterViewController* viewController1;
986 
987  @autoreleasepool {
988  viewController1 = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
989  EXPECT_EQ(viewController1.viewIdentifier, 0ll);
990  EXPECT_EQ(engine.viewController, viewController1);
991 
992  engine.viewController = nil;
993 
994  FlutterViewController* viewController2 = [[FlutterViewController alloc] initWithEngine:engine
995  nibName:nil
996  bundle:nil];
997  EXPECT_EQ(viewController2.viewIdentifier, 0ll);
998  EXPECT_EQ(engine.viewController, viewController2);
999  }
1000  // FVC2 is deallocated but FVC1 is retained.
1001 
1002  EXPECT_EQ(engine.viewController, nil);
1003 
1004  engine.viewController = viewController1;
1005  EXPECT_EQ(engine.viewController, viewController1);
1006  EXPECT_EQ(viewController1.viewIdentifier, 0ll);
1007 }
1008 
1009 TEST_F(FlutterEngineTest, RemovingViewDisposesCompositorResources) {
1010  NSString* fixtures = @(flutter::testing::GetFixturesPath());
1011  FlutterDartProject* project = [[FlutterDartProject alloc]
1012  initWithAssetsPath:fixtures
1013  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
1014  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project];
1015 
1016  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
1017  nibName:nil
1018  bundle:nil];
1019  [viewController loadView];
1020  [viewController viewDidLoad];
1021  viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
1022 
1023  EXPECT_TRUE([engine runWithEntrypoint:@"drawIntoAllViews"]);
1024  // Wait up to 1 second for Flutter to emit a frame.
1025  CFTimeInterval start = CACurrentMediaTime();
1026  while (engine.macOSCompositor->DebugNumViews() == 0) {
1027  CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
1028  if (CACurrentMediaTime() - start > 1) {
1029  break;
1030  }
1031  }
1032 
1033  EXPECT_EQ(engine.macOSCompositor->DebugNumViews(), 1u);
1034 
1035  engine.viewController = nil;
1036  EXPECT_EQ(engine.macOSCompositor->DebugNumViews(), 0u);
1037 
1038  [engine shutDownEngine];
1039  engine = nil;
1040 }
1041 
1042 TEST_F(FlutterEngineTest, HandlesTerminationRequest) {
1043  id engineMock = CreateMockFlutterEngine(nil);
1044  __block NSString* nextResponse = @"exit";
1045  __block BOOL triedToTerminate = NO;
1046  FlutterEngineTerminationHandler* terminationHandler =
1047  [[FlutterEngineTerminationHandler alloc] initWithEngine:engineMock
1048  terminator:^(id sender) {
1049  triedToTerminate = TRUE;
1050  // Don't actually terminate, of course.
1051  }];
1052  OCMStub([engineMock terminationHandler]).andReturn(terminationHandler);
1053  id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1054  OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1055  [engineMock binaryMessenger])
1056  .andReturn(binaryMessengerMock);
1057  OCMStub([engineMock sendOnChannel:@"flutter/platform"
1058  message:[OCMArg any]
1059  binaryReply:[OCMArg any]])
1060  .andDo((^(NSInvocation* invocation) {
1061  [invocation retainArguments];
1062  FlutterBinaryReply callback;
1063  NSData* returnedMessage;
1064  [invocation getArgument:&callback atIndex:4];
1065  if ([nextResponse isEqualToString:@"error"]) {
1066  FlutterError* errorResponse = [FlutterError errorWithCode:@"Error"
1067  message:@"Failed"
1068  details:@"Details"];
1069  returnedMessage =
1070  [[FlutterJSONMethodCodec sharedInstance] encodeErrorEnvelope:errorResponse];
1071  } else {
1072  NSDictionary* responseDict = @{@"response" : nextResponse};
1073  returnedMessage =
1074  [[FlutterJSONMethodCodec sharedInstance] encodeSuccessEnvelope:responseDict];
1075  }
1076  callback(returnedMessage);
1077  }));
1078  __block NSString* calledAfterTerminate = @"";
1079  FlutterResult appExitResult = ^(id result) {
1080  NSDictionary* resultDict = result;
1081  calledAfterTerminate = resultDict[@"response"];
1082  };
1083  FlutterMethodCall* methodExitApplication =
1084  [FlutterMethodCall methodCallWithMethodName:@"System.exitApplication"
1085  arguments:@{@"type" : @"cancelable"}];
1086 
1087  // Always terminate when the binding isn't ready (which is the default).
1088  triedToTerminate = NO;
1089  calledAfterTerminate = @"";
1090  nextResponse = @"cancel";
1091  [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1092  EXPECT_STREQ([calledAfterTerminate UTF8String], "");
1093  EXPECT_TRUE(triedToTerminate);
1094 
1095  // Once the binding is ready, handle the request.
1096  terminationHandler.acceptingRequests = YES;
1097  triedToTerminate = NO;
1098  calledAfterTerminate = @"";
1099  nextResponse = @"exit";
1100  [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1101  EXPECT_STREQ([calledAfterTerminate UTF8String], "exit");
1102  EXPECT_TRUE(triedToTerminate);
1103 
1104  triedToTerminate = NO;
1105  calledAfterTerminate = @"";
1106  nextResponse = @"cancel";
1107  [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1108  EXPECT_STREQ([calledAfterTerminate UTF8String], "cancel");
1109  EXPECT_FALSE(triedToTerminate);
1110 
1111  // Check that it doesn't crash on error.
1112  triedToTerminate = NO;
1113  calledAfterTerminate = @"";
1114  nextResponse = @"error";
1115  [engineMock handleMethodCall:methodExitApplication result:appExitResult];
1116  EXPECT_STREQ([calledAfterTerminate UTF8String], "");
1117  EXPECT_TRUE(triedToTerminate);
1118 }
1119 
1120 TEST_F(FlutterEngineTest, IgnoresTerminationRequestIfNotFlutterAppDelegate) {
1121  id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1122  id<NSApplicationDelegate> plainDelegate = [[PlainAppDelegate alloc] init];
1123  [NSApplication sharedApplication].delegate = plainDelegate;
1124 
1125  // Creating the engine shouldn't fail here, even though the delegate isn't a
1126  // FlutterAppDelegate.
1128 
1129  // Asking to terminate the app should cancel.
1130  EXPECT_EQ([[[NSApplication sharedApplication] delegate] applicationShouldTerminate:NSApp],
1131  NSTerminateCancel);
1132 
1133  [NSApplication sharedApplication].delegate = previousDelegate;
1134 }
1135 
1136 TEST_F(FlutterEngineTest, HandleAccessibilityEvent) {
1137  __block BOOL announced = NO;
1138  id engineMock = CreateMockFlutterEngine(nil);
1139 
1140  OCMStub([engineMock announceAccessibilityMessage:[OCMArg any]
1141  withPriority:NSAccessibilityPriorityMedium])
1142  .andDo((^(NSInvocation* invocation) {
1143  announced = TRUE;
1144  [invocation retainArguments];
1145  NSString* message;
1146  [invocation getArgument:&message atIndex:2];
1147  EXPECT_EQ(message, @"error message");
1148  }));
1149 
1150  NSDictionary<NSString*, id>* annotatedEvent =
1151  @{@"type" : @"announce",
1152  @"data" : @{@"message" : @"error message"}};
1153 
1154  [engineMock handleAccessibilityEvent:annotatedEvent];
1155 
1156  EXPECT_TRUE(announced);
1157 }
1158 
1159 TEST_F(FlutterEngineTest, HandleLifecycleStates) API_AVAILABLE(macos(10.9)) {
1160  __block flutter::AppLifecycleState sentState;
1161  id engineMock = CreateMockFlutterEngine(nil);
1162 
1163  // Have to enumerate all the values because OCMStub can't capture
1164  // non-Objective-C object arguments.
1165  OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kDetached])
1166  .andDo((^(NSInvocation* invocation) {
1168  }));
1169  OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kResumed])
1170  .andDo((^(NSInvocation* invocation) {
1172  }));
1173  OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kInactive])
1174  .andDo((^(NSInvocation* invocation) {
1176  }));
1177  OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kHidden])
1178  .andDo((^(NSInvocation* invocation) {
1180  }));
1181  OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kPaused])
1182  .andDo((^(NSInvocation* invocation) {
1184  }));
1185 
1186  __block NSApplicationOcclusionState visibility = NSApplicationOcclusionStateVisible;
1187  id mockApplication = OCMPartialMock([NSApplication sharedApplication]);
1188  OCMStub((NSApplicationOcclusionState)[mockApplication occlusionState])
1189  .andDo(^(NSInvocation* invocation) {
1190  [invocation setReturnValue:&visibility];
1191  });
1192 
1193  NSNotification* willBecomeActive =
1194  [[NSNotification alloc] initWithName:NSApplicationWillBecomeActiveNotification
1195  object:nil
1196  userInfo:nil];
1197  NSNotification* willResignActive =
1198  [[NSNotification alloc] initWithName:NSApplicationWillResignActiveNotification
1199  object:nil
1200  userInfo:nil];
1201 
1202  NSNotification* didChangeOcclusionState;
1203  didChangeOcclusionState =
1204  [[NSNotification alloc] initWithName:NSApplicationDidChangeOcclusionStateNotification
1205  object:nil
1206  userInfo:nil];
1207 
1208  [engineMock handleDidChangeOcclusionState:didChangeOcclusionState];
1209  EXPECT_EQ(sentState, flutter::AppLifecycleState::kInactive);
1210 
1211  [engineMock handleWillBecomeActive:willBecomeActive];
1212  EXPECT_EQ(sentState, flutter::AppLifecycleState::kResumed);
1213 
1214  [engineMock handleWillResignActive:willResignActive];
1215  EXPECT_EQ(sentState, flutter::AppLifecycleState::kInactive);
1216 
1217  visibility = 0;
1218  [engineMock handleDidChangeOcclusionState:didChangeOcclusionState];
1219  EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden);
1220 
1221  [engineMock handleWillBecomeActive:willBecomeActive];
1222  EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden);
1223 
1224  [engineMock handleWillResignActive:willResignActive];
1225  EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden);
1226 
1227  [mockApplication stopMocking];
1228 }
1229 
1230 TEST_F(FlutterEngineTest, ForwardsPluginDelegateRegistration) {
1231  id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1232  FakeLifecycleProvider* fakeAppDelegate = [[FakeLifecycleProvider alloc] init];
1233  [NSApplication sharedApplication].delegate = fakeAppDelegate;
1234 
1235  FakeAppDelegatePlugin* plugin = [[FakeAppDelegatePlugin alloc] init];
1236  FlutterEngine* engine = CreateMockFlutterEngine(nil);
1237 
1238  [[engine registrarForPlugin:@"TestPlugin"] addApplicationDelegate:plugin];
1239 
1240  EXPECT_TRUE([fakeAppDelegate hasDelegate:plugin]);
1241 
1242  [NSApplication sharedApplication].delegate = previousDelegate;
1243 }
1244 
1245 TEST_F(FlutterEngineTest, UnregistersPluginsOnEngineDestruction) {
1246  id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1247  FakeLifecycleProvider* fakeAppDelegate = [[FakeLifecycleProvider alloc] init];
1248  [NSApplication sharedApplication].delegate = fakeAppDelegate;
1249 
1250  FakeAppDelegatePlugin* plugin = [[FakeAppDelegatePlugin alloc] init];
1251 
1252  @autoreleasepool {
1253  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil];
1254 
1255  [[engine registrarForPlugin:@"TestPlugin"] addApplicationDelegate:plugin];
1256  EXPECT_TRUE([fakeAppDelegate hasDelegate:plugin]);
1257  }
1258 
1259  // When the engine is released, it should unregister any plugins it had
1260  // registered on its behalf.
1261  EXPECT_FALSE([fakeAppDelegate hasDelegate:plugin]);
1262 
1263  [NSApplication sharedApplication].delegate = previousDelegate;
1264 }
1265 
1266 TEST_F(FlutterEngineTest, RunWithEntrypointUpdatesDisplayConfig) {
1267  BOOL updated = NO;
1268  FlutterEngine* engine = GetFlutterEngine();
1269  auto original_update_displays = engine.embedderAPI.NotifyDisplayUpdate;
1270  engine.embedderAPI.NotifyDisplayUpdate = MOCK_ENGINE_PROC(
1271  NotifyDisplayUpdate, ([&updated, &original_update_displays](
1272  auto engine, auto update_type, auto* displays, auto display_count) {
1273  updated = YES;
1274  return original_update_displays(engine, update_type, displays, display_count);
1275  }));
1276 
1277  EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
1278  EXPECT_TRUE(updated);
1279 
1280  updated = NO;
1281  [[NSNotificationCenter defaultCenter]
1282  postNotificationName:NSApplicationDidChangeScreenParametersNotification
1283  object:nil];
1284  EXPECT_TRUE(updated);
1285 }
1286 
1287 TEST_F(FlutterEngineTest, NotificationsUpdateDisplays) {
1288  BOOL updated = NO;
1289  FlutterEngine* engine = GetFlutterEngine();
1290  auto original_set_viewport_metrics = engine.embedderAPI.SendWindowMetricsEvent;
1291  engine.embedderAPI.SendWindowMetricsEvent = MOCK_ENGINE_PROC(
1292  SendWindowMetricsEvent,
1293  ([&updated, &original_set_viewport_metrics](auto engine, auto* window_metrics) {
1294  updated = YES;
1295  return original_set_viewport_metrics(engine, window_metrics);
1296  }));
1297 
1298  EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
1299 
1300  updated = NO;
1301  [[NSNotificationCenter defaultCenter] postNotificationName:NSWindowDidChangeScreenNotification
1302  object:nil];
1303  // No VC.
1304  EXPECT_FALSE(updated);
1305 
1306  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
1307  nibName:nil
1308  bundle:nil];
1309  [viewController loadView];
1310  viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
1311 
1312  [[NSNotificationCenter defaultCenter] postNotificationName:NSWindowDidChangeScreenNotification
1313  object:nil];
1314  EXPECT_TRUE(updated);
1315 }
1316 
1317 TEST_F(FlutterEngineTest, DisplaySizeIsInPhysicalPixel) {
1318  NSString* fixtures = @(testing::GetFixturesPath());
1319  FlutterDartProject* project = [[FlutterDartProject alloc]
1320  initWithAssetsPath:fixtures
1321  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
1322  project.rootIsolateCreateCallback = FlutterEngineTest::IsolateCreateCallback;
1323  MockableFlutterEngine* engine = [[MockableFlutterEngine alloc] initWithName:@"foobar"
1324  project:project
1325  allowHeadlessExecution:true];
1326  BOOL updated = NO;
1327  auto original_update_displays = engine.embedderAPI.NotifyDisplayUpdate;
1328  engine.embedderAPI.NotifyDisplayUpdate = MOCK_ENGINE_PROC(
1329  NotifyDisplayUpdate, ([&updated, &original_update_displays](
1330  auto engine, auto update_type, auto* displays, auto display_count) {
1331  EXPECT_EQ(display_count, 1UL);
1332  EXPECT_EQ(displays->display_id, 10UL);
1333  EXPECT_EQ(displays->width, 60UL);
1334  EXPECT_EQ(displays->height, 80UL);
1335  EXPECT_EQ(displays->device_pixel_ratio, 2UL);
1336  updated = YES;
1337  return original_update_displays(engine, update_type, displays, display_count);
1338  }));
1339  EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
1340  EXPECT_TRUE(updated);
1341  [engine shutDownEngine];
1342  engine = nil;
1343 }
1344 
1345 TEST_F(FlutterEngineTest, ReportsHourFormat) {
1346  __block BOOL expectedValue;
1347 
1348  // Set up mocks.
1349  id channelMock = OCMClassMock([FlutterBasicMessageChannel class]);
1350  OCMStub([channelMock messageChannelWithName:@"flutter/settings"
1351  binaryMessenger:[OCMArg any]
1352  codec:[OCMArg any]])
1353  .andReturn(channelMock);
1354  OCMStub([channelMock sendMessage:[OCMArg any]]).andDo((^(NSInvocation* invocation) {
1355  __weak id message;
1356  [invocation getArgument:&message atIndex:2];
1357  EXPECT_EQ(message[@"alwaysUse24HourFormat"], @(expectedValue));
1358  }));
1359 
1360  id mockHourFormat = OCMClassMock([FlutterHourFormat class]);
1361  OCMStub([mockHourFormat isAlwaysUse24HourFormat]).andDo((^(NSInvocation* invocation) {
1362  [invocation setReturnValue:&expectedValue];
1363  }));
1364 
1365  id engineMock = CreateMockFlutterEngine(nil);
1366 
1367  // Verify the YES case.
1368  expectedValue = YES;
1369  EXPECT_TRUE([engineMock runWithEntrypoint:@"main"]);
1370  [engineMock shutDownEngine];
1371 
1372  // Verify the NO case.
1373  expectedValue = NO;
1374  EXPECT_TRUE([engineMock runWithEntrypoint:@"main"]);
1375  [engineMock shutDownEngine];
1376 
1377  // Clean up mocks.
1378  [mockHourFormat stopMocking];
1379  [engineMock stopMocking];
1380  [channelMock stopMocking];
1381 }
1382 
1383 } // namespace flutter::testing
1384 
1385 // NOLINTEND(clang-analyzer-core.StackAddressEscape)
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
void(^ FlutterResult)(id _Nullable result)
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