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