Flutter macOS Embedder
FlutterWindowControllerTest.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 
13 #import "flutter/testing/testing.h"
14 #import "third_party/googletest/googletest/include/gtest/gtest.h"
15 
16 namespace flutter::testing {
17 
19  public:
21 
22  void SetUp() {
24 
25  [GetFlutterEngine() runWithEntrypoint:@"testWindowController"];
26 
27  signalled_ = false;
28 
29  AddNativeCallback("SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
31  signalled_ = true;
32  }));
33 
34  while (!signalled_) {
35  CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);
36  }
37  }
38 
39  void TearDown() {
40  [GetFlutterEngine().windowController closeAllWindows];
42  }
43 
44  protected:
46  if (isolate_) {
47  return *isolate_;
48  } else {
49  FML_LOG(ERROR) << "Isolate is not set.";
50  FML_UNREACHABLE();
51  }
52  }
53 
54  std::optional<flutter::Isolate> isolate_;
55  bool signalled_;
56 };
57 
58 class FlutterWindowControllerRetainTest : public ::testing::Test {};
59 
60 TEST_F(FlutterWindowControllerTest, FixMoveRunLoopMode) {
61  FlutterEngine* engine = GetFlutterEngine();
62  [engine.windowController fixMoveRunLoopModeIfNeeded];
63 
64  // Make sure that after fixMoveRunLoopMode _NSMoveTimerRunLoopMode becomes
65  // common run loop mode.
66  __block BOOL signalled = NO;
67  CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{
68  signalled = YES;
69  });
70  CFRunLoopRunInMode(CFSTR("_NSMoveTimerRunLoopMode"), 1.0, YES);
71  EXPECT_TRUE(signalled);
72 }
73 
74 TEST_F(FlutterWindowControllerTest, CreateRegularWindow) {
76  .has_size = true,
77  .size = {.width = 800, .height = 600},
78  .on_should_close = [] {},
79  .on_will_close = [] {},
80  .notify_listeners = [] {},
81  };
82 
83  FlutterEngine* engine = GetFlutterEngine();
84  int64_t engineId = reinterpret_cast<int64_t>(engine);
85 
86  {
87  IsolateScope isolate_scope(isolate());
88  int64_t handle = InternalFlutter_WindowController_CreateRegularWindow(engineId, &request);
89  EXPECT_EQ(handle, 1);
90 
91  FlutterViewController* viewController = [engine viewControllerForIdentifier:handle];
92  EXPECT_NE(viewController, nil);
93  CGSize size = viewController.view.frame.size;
94  EXPECT_EQ(size.width, 800);
95  EXPECT_EQ(size.height, 600);
96  }
97 }
98 
99 TEST_F(FlutterWindowControllerTest, CreateTooltipWindow) {
100  IsolateScope isolate_scope(isolate());
101  FlutterEngine* engine = GetFlutterEngine();
102  int64_t engineId = reinterpret_cast<int64_t>(engine);
103 
104  auto request = FlutterWindowCreationRequest{
105  .has_size = true,
106  .size = {.width = 800, .height = 600},
107  .on_should_close = [] {},
108  .on_will_close = [] {},
109  .notify_listeners = [] {},
110  };
111  int64_t parentViewId = InternalFlutter_WindowController_CreateRegularWindow(engineId, &request);
112  EXPECT_EQ(parentViewId, 1);
113 
114  auto position_callback = [](const FlutterWindowSize& child_size,
115  const FlutterWindowRect& parent_rect,
116  const FlutterWindowRect& output_rect) -> FlutterWindowRect* {
117  FlutterWindowRect* rect = static_cast<FlutterWindowRect*>(malloc(sizeof(FlutterWindowRect)));
118  rect->left = parent_rect.left + 10;
119  rect->top = parent_rect.top + 10;
120  rect->width = child_size.width;
121  rect->height = child_size.height;
122  return rect;
123  };
124 
126  .has_constraints = true,
127  .constraints{
128  .max_width = 1000,
129  .max_height = 1000,
130  },
131  .parent_view_id = parentViewId,
132  .on_should_close = [] {},
133  .on_will_close = [] {},
134  .notify_listeners = [] {},
135  .on_get_window_position = position_callback,
136  };
137 
138  const int64_t tooltipViewId =
140  EXPECT_NE(tooltipViewId, 0);
141 }
142 
143 TEST_F(FlutterWindowControllerTest, CreatePopupWindow) {
144  IsolateScope isolate_scope(isolate());
145  FlutterEngine* engine = GetFlutterEngine();
146  int64_t engineId = reinterpret_cast<int64_t>(engine);
147 
148  auto request = FlutterWindowCreationRequest{
149  .has_size = true,
150  .size = {.width = 800, .height = 600},
151  .on_should_close = [] {},
152  .on_will_close = [] {},
153  .notify_listeners = [] {},
154  };
155  int64_t parentViewId = InternalFlutter_WindowController_CreateRegularWindow(engineId, &request);
156  EXPECT_EQ(parentViewId, 1);
157 
158  auto position_callback = [](const FlutterWindowSize& child_size,
159  const FlutterWindowRect& parent_rect,
160  const FlutterWindowRect& output_rect) -> FlutterWindowRect* {
161  FlutterWindowRect* rect = static_cast<FlutterWindowRect*>(malloc(sizeof(FlutterWindowRect)));
162  rect->left = parent_rect.left + 10;
163  rect->top = parent_rect.top + 10;
164  rect->width = child_size.width;
165  rect->height = child_size.height;
166  return rect;
167  };
168 
170  .has_constraints = true,
171  .constraints{
172  .max_width = 1000,
173  .max_height = 1000,
174  },
175  .parent_view_id = parentViewId,
176  .on_should_close = [] {},
177  .on_will_close = [] {},
178  .notify_listeners = [] {},
179  .on_get_window_position = position_callback,
180  };
181 
182  const int64_t tooltipViewId =
184  EXPECT_NE(tooltipViewId, 0);
185 }
186 
187 TEST_F(FlutterWindowControllerRetainTest, WindowControllerDoesNotRetainEngine) {
189  .has_size = true,
190  .size = {.width = 800, .height = 600},
191  .on_should_close = [] {},
192  .on_will_close = [] {},
193  .notify_listeners = [] {},
194  };
195 
196  __weak FlutterEngine* weakEngine = nil;
197  @autoreleasepool {
198  NSString* fixtures = @(flutter::testing::GetFixturesPath());
199  NSLog(@"Fixtures path: %@", fixtures);
200  FlutterDartProject* project = [[FlutterDartProject alloc]
201  initWithAssetsPath:fixtures
202  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
203 
204  static std::optional<flutter::Isolate> isolate;
205  isolate = std::nullopt;
206 
207  project.rootIsolateCreateCallback = [](void*) { isolate = flutter::Isolate::Current(); };
208  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
209  project:project
210  allowHeadlessExecution:YES];
211  weakEngine = engine;
212  [engine runWithEntrypoint:@"testWindowControllerRetainCycle"];
213 
214  int64_t engineId = reinterpret_cast<int64_t>(engine);
215 
216  {
217  FML_DCHECK(isolate.has_value());
218  // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
219  IsolateScope isolateScope(*isolate);
220  int64_t handle = InternalFlutter_WindowController_CreateRegularWindow(engineId, &request);
221  EXPECT_EQ(handle, 1);
222  }
223 
224  [engine.windowController closeAllWindows];
225  [engine shutDownEngine];
226  }
227  EXPECT_EQ(weakEngine, nil);
228 }
229 
230 TEST_F(FlutterWindowControllerTest, DestroyRegularWindow) {
232  .has_size = true,
233  .size = {.width = 800, .height = 600},
234  .on_should_close = [] {},
235  .on_will_close = [] {},
236  .notify_listeners = [] {},
237  };
238 
239  FlutterEngine* engine = GetFlutterEngine();
240  int64_t engine_id = reinterpret_cast<int64_t>(engine);
241 
242  IsolateScope isolate_scope(isolate());
243  int64_t handle = InternalFlutter_WindowController_CreateRegularWindow(engine_id, &request);
244  FlutterViewController* viewController = [engine viewControllerForIdentifier:handle];
245 
246  InternalFlutter_Window_Destroy(engine_id, (__bridge void*)viewController.view.window);
247  viewController = [engine viewControllerForIdentifier:handle];
248  EXPECT_EQ(viewController, nil);
249 }
250 
251 TEST_F(FlutterWindowControllerTest, InternalFlutterWindowGetHandle) {
253  .has_size = true,
254  .size = {.width = 800, .height = 600},
255  .on_should_close = [] {},
256  .on_will_close = [] {},
257  .notify_listeners = [] {},
258  };
259 
260  FlutterEngine* engine = GetFlutterEngine();
261  int64_t engine_id = reinterpret_cast<int64_t>(engine);
262 
263  IsolateScope isolate_scope(isolate());
264  int64_t handle = InternalFlutter_WindowController_CreateRegularWindow(engine_id, &request);
265  FlutterViewController* viewController = [engine viewControllerForIdentifier:handle];
266 
267  void* window_handle = InternalFlutter_Window_GetHandle(engine_id, handle);
268  EXPECT_EQ(window_handle, (__bridge void*)viewController.view.window);
269 }
270 
273  .has_size = true,
274  .size = {.width = 800, .height = 600},
275  .on_should_close = [] {},
276  .on_will_close = [] {},
277  .notify_listeners = [] {},
278  };
279 
280  FlutterEngine* engine = GetFlutterEngine();
281  int64_t engine_id = reinterpret_cast<int64_t>(engine);
282 
283  IsolateScope isolate_scope(isolate());
284  int64_t handle = InternalFlutter_WindowController_CreateRegularWindow(engine_id, &request);
285 
286  FlutterViewController* viewController = [engine viewControllerForIdentifier:handle];
287  NSWindow* window = viewController.view.window;
288  void* windowHandle = (__bridge void*)window;
289 
290  EXPECT_EQ(window.zoomed, NO);
291  EXPECT_EQ(window.miniaturized, NO);
292  EXPECT_EQ(window.styleMask & NSWindowStyleMaskFullScreen, 0u);
293 
294  InternalFlutter_Window_SetMaximized(windowHandle, true);
295  CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, false);
296  EXPECT_EQ(window.zoomed, YES);
297 
298  InternalFlutter_Window_SetMaximized(windowHandle, false);
299  CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, false);
300  EXPECT_EQ(window.zoomed, NO);
301 
302  // FullScreen toggle does not seem to work when the application is not run from a bundle.
303 
304  InternalFlutter_Window_Minimize(windowHandle);
305  CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, false);
306  EXPECT_EQ(window.miniaturized, YES);
307 }
308 
309 TEST_F(FlutterWindowControllerTest, ClosesAllWindowsOnEngineRestart) {
311  .has_size = true,
312  .size = {.width = 800, .height = 600},
313  .on_should_close = [] {},
314  .on_will_close = [] {},
315  .notify_listeners = [] {},
316  };
317  FlutterEngine* engine = GetFlutterEngine();
318  int64_t engine_id = reinterpret_cast<int64_t>(engine);
319 
320  IsolateScope isolate_scope(isolate());
321 
322  // Create multiple windows
323  int64_t handle1 = InternalFlutter_WindowController_CreateRegularWindow(engine_id, &request);
324  int64_t handle2 = InternalFlutter_WindowController_CreateRegularWindow(engine_id, &request);
325  int64_t handle3 = InternalFlutter_WindowController_CreateRegularWindow(engine_id, &request);
326 
327  // Verify windows are created
328  FlutterViewController* viewController1 = [engine viewControllerForIdentifier:handle1];
329  FlutterViewController* viewController2 = [engine viewControllerForIdentifier:handle2];
330  FlutterViewController* viewController3 = [engine viewControllerForIdentifier:handle3];
331  EXPECT_NE(viewController1, nil);
332  EXPECT_NE(viewController2, nil);
333  EXPECT_NE(viewController3, nil);
334 
335  // Close all windows on engine restart
336  [engine engineCallbackOnPreEngineRestart];
337 
338  // Verify all windows are closed and view controllers are disposed
339  viewController1 = [engine viewControllerForIdentifier:handle1];
340  viewController2 = [engine viewControllerForIdentifier:handle2];
341  viewController3 = [engine viewControllerForIdentifier:handle3];
342  EXPECT_EQ(viewController1, nil);
343  EXPECT_EQ(viewController2, nil);
344  EXPECT_EQ(viewController3, nil);
345 };
346 
347 TEST_F(FlutterWindowControllerTest, ViewMetricsRespectPositionCallbackConstraints) {
348  IsolateScope isolate_scope(isolate());
349  FlutterEngine* engine = GetFlutterEngine();
350  int64_t engineId = reinterpret_cast<int64_t>(engine);
351 
352  // Create parent window.
353  auto parentRequest = FlutterWindowCreationRequest{
354  .has_size = true,
355  .size = {.width = 800, .height = 600},
356  .on_should_close = [] {},
357  .on_will_close = [] {},
358  .notify_listeners = [] {},
359  };
360  int64_t parentViewId =
362  EXPECT_EQ(parentViewId, 1);
363 
364  auto position_callback = [](const FlutterWindowSize& child_size,
365  const FlutterWindowRect& parent_rect,
366  const FlutterWindowRect& output_rect) -> FlutterWindowRect* {
367  FlutterWindowRect* rect = static_cast<FlutterWindowRect*>(malloc(sizeof(FlutterWindowRect)));
368  rect->left = parent_rect.left;
369  rect->top = parent_rect.top;
370  rect->width = 500;
371  rect->height = 400;
372  return rect;
373  };
374 
375  auto tooltipRequest = FlutterWindowCreationRequest{
376  .has_constraints = true,
377  .constraints{
378  .min_width = 0,
379  .min_height = 0,
380  .max_width = 1000,
381  .max_height = 1000,
382  },
383  .parent_view_id = parentViewId,
384  .on_should_close = [] {},
385  .on_will_close = [] {},
386  .notify_listeners = [] {},
387  .on_get_window_position = position_callback,
388  };
389 
390  const int64_t tooltipViewId =
391  InternalFlutter_WindowController_CreateTooltipWindow(engineId, &tooltipRequest);
392  EXPECT_NE(tooltipViewId, 0);
393 
394  FlutterViewController* viewController = [engine viewControllerForIdentifier:tooltipViewId];
395  FlutterView* flutterView = viewController.flutterView;
396 
397  EXPECT_EQ(flutterView.sizedToContents, YES);
398 
399  CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);
400 
401  [flutterView.sizingDelegate viewDidUpdateContents:flutterView withSize:NSMakeSize(1000, 1000)];
402 
403  // The constraints from request are 1000x1000, but additional constraints came from the positioner
404  // and must be respected.
405  CGSize maxSize = flutterView.maximumContentSize;
406  EXPECT_LE(maxSize.width, 500);
407  EXPECT_LE(maxSize.height, 400);
408 }
409 
410 TEST_F(FlutterWindowControllerTest, GetOffsetInParent) {
411  NSWindow* parentWindow = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
412  styleMask:NSWindowStyleMaskTitled
413  backing:NSBackingStoreBuffered
414  defer:NO];
415  [parentWindow setReleasedWhenClosed:NO];
416  NSWindow* childWindow = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 100, 100)
417  styleMask:NSWindowStyleMaskBorderless
418  backing:NSBackingStoreBuffered
419  defer:NO];
420  [childWindow setReleasedWhenClosed:NO];
421 
422  // Bottom left origin.
423  [parentWindow setFrame:NSMakeRect(100, 100, 800, 600) display:NO];
424  [childWindow setFrame:NSMakeRect(150, 150, 100, 100) display:NO];
425 
426  // Establish the parent-child relationship required by GetOffsetInParent.
427  [parentWindow addChildWindow:childWindow ordered:NSWindowAbove];
428 
429  FlutterWindowOffset offset =
430  InternalFlutter_Window_GetOffsetInParent((__bridge void*)childWindow);
431 
432  // GetOffsetInParent has relative coordinates with top left origin.
433  NSRect parentContentRect = [parentWindow contentRectForFrameRect:parentWindow.frame];
434 
435  double expectedX = 150 - parentContentRect.origin.x;
436  double expectedY = (parentContentRect.origin.y + parentContentRect.size.height) - (150 + 100);
437 
438  EXPECT_NEAR(offset.x, expectedX, 0.001);
439  EXPECT_NEAR(offset.y, expectedY, 0.001);
440 
441  [parentWindow removeChildWindow:childWindow];
442  [childWindow close];
443  [parentWindow close];
444 }
445 } // namespace flutter::testing
FLUTTER_DARWIN_EXPORT int64_t InternalFlutter_WindowController_CreateRegularWindow(int64_t engine_id, const FlutterWindowCreationRequest *request)
FLUTTER_DARWIN_EXPORT FlutterWindowOffset InternalFlutter_Window_GetOffsetInParent(void *window)
FLUTTER_DARWIN_EXPORT int64_t InternalFlutter_WindowController_CreateTooltipWindow(int64_t engine_id, const FlutterWindowCreationRequest *request)
FLUTTER_DARWIN_EXPORT void InternalFlutter_Window_SetMaximized(void *window, bool maximized)
FLUTTER_DARWIN_EXPORT int64_t InternalFlutter_WindowController_CreatePopupWindow(int64_t engine_id, const FlutterWindowCreationRequest *request)
FLUTTER_DARWIN_EXPORT void InternalFlutter_Window_Destroy(int64_t engine_id, void *window)
FLUTTER_DARWIN_EXPORT void * InternalFlutter_Window_GetHandle(int64_t engine_id, FlutterViewIdentifier view_id)
FLUTTER_DARWIN_EXPORT void InternalFlutter_Window_Minimize(void *window)
static Isolate Current()
Definition: isolate_scope.cc:9
void AddNativeCallback(const char *name, Dart_NativeFunction function)
CGSize maximumContentSize
Definition: FlutterView.h:133
BOOL sizedToContents
Definition: FlutterView.h:121
id< FlutterViewSizingDelegate > sizingDelegate
Definition: FlutterView.h:94
TEST_F(AccessibilityBridgeMacWindowTest, SendsAccessibilityCreateNotificationFlutterViewWindow)