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