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