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