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