Flutter Windows Embedder
window_manager_unittests.cc
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 
6 #include "flutter/shell/platform/windows/testing/egl/mock_context.h"
7 #include "flutter/shell/platform/windows/testing/egl/mock_manager.h"
8 #include "flutter/shell/platform/windows/testing/egl/mock_window_surface.h"
9 #include "flutter/shell/platform/windows/testing/engine_modifier.h"
10 #include "flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h"
11 #include "flutter/shell/platform/windows/testing/windows_test.h"
13 #include "gmock/gmock.h"
14 #include "gtest/gtest.h"
15 
16 namespace flutter {
17 namespace testing {
18 
19 namespace {
20 
21 using ::testing::NiceMock;
22 using ::testing::Return;
23 
24 // Builds a mock |WindowSurface| whose lifecycle operations all succeed. Used to
25 // keep the EGL surface lifecycle deterministic in tests so that resize paths
26 // (e.g. |FlutterWindowsView::OnFrameGenerated|) do not perform real, flaky
27 // cross-thread ANGLE/D3D calls against an actual GPU surface.
28 std::unique_ptr<egl::MockWindowSurface> CreateMockWindowSurface() {
29  auto surface = std::make_unique<NiceMock<egl::MockWindowSurface>>();
30  ON_CALL(*surface, IsValid).WillByDefault(Return(true));
31  ON_CALL(*surface, MakeCurrent).WillByDefault(Return(true));
32  ON_CALL(*surface, SetVSyncEnabled).WillByDefault(Return(true));
33  ON_CALL(*surface, Destroy).WillByDefault(Return(true));
34  return surface;
35 }
36 
37 class WindowManagerTest : public WindowsTest {
38  public:
39  WindowManagerTest() = default;
40  virtual ~WindowManagerTest() = default;
41 
42  protected:
43  void SetUp() override {
44  auto& context = GetContext();
45  FlutterWindowsEngineBuilder builder(context);
46  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
47 
48  engine_ = builder.Build();
49  ASSERT_TRUE(engine_);
50 
51  engine_->SetRootIsolateCreateCallback(context.GetRootIsolateCallback());
52  ASSERT_TRUE(engine_->Run("testWindowController"));
53 
54  bool signalled = false;
55  context.AddNativeFunction(
56  "Signal", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
57  isolate_ = flutter::Isolate::Current();
58  signalled = true;
59  }));
60  while (!signalled) {
61  engine_->task_runner()->ProcessTasks();
62  }
63 
64  // Replace the engine's EGL manager with a permissive mock so that EGL
65  // surface creation and resizing are deterministic and never issue real,
66  // cross-thread GPU calls. These tests do not exercise real EGL rendering,
67  // and without this, tests that drive |FlutterWindowsView::OnFrameGenerated|
68  // from the test thread race the engine's raster thread for the EGL context,
69  // intermittently producing an EGL_BAD_ACCESS and a crash. Installed here,
70  // before any windows (and thus render surfaces) are created.
71  auto egl_manager = std::make_unique<NiceMock<egl::MockManager>>();
72  ON_CALL(*egl_manager, CreateWindowSurface)
73  .WillByDefault(
74  [](HWND, size_t, size_t) { return CreateMockWindowSurface(); });
75  ON_CALL(*egl_manager, render_context)
76  .WillByDefault(Return(&mock_egl_context_));
77  ON_CALL(mock_egl_context_, ClearCurrent).WillByDefault(Return(true));
78  ON_CALL(mock_egl_context_, MakeCurrent).WillByDefault(Return(true));
79  EngineModifier{engine_.get()}.SetEGLManager(std::move(egl_manager));
80  }
81 
82  void TearDown() override { engine_->Stop(); }
83 
84  int64_t engine_id() { return reinterpret_cast<int64_t>(engine_.get()); }
85  flutter::Isolate& isolate() { return *isolate_; }
86  RegularWindowCreationRequest* regular_creation_request() {
87  return &regular_creation_request_;
88  }
89  FlutterWindowsEngine* engine() { return engine_.get(); }
90 
91  private:
92  std::unique_ptr<FlutterWindowsEngine> engine_;
93  NiceMock<egl::MockContext> mock_egl_context_;
94  std::optional<flutter::Isolate> isolate_;
95  RegularWindowCreationRequest regular_creation_request_{
96  .preferred_size =
97  {
98  .has_preferred_view_size = true,
99  .preferred_view_width = 800,
100  .preferred_view_height = 600,
101  },
102  };
103 
104  FML_DISALLOW_COPY_AND_ASSIGN(WindowManagerTest);
105 };
106 
107 } // namespace
108 
109 TEST_F(WindowManagerTest, WindowingInitialize) {
110  IsolateScope isolate_scope(isolate());
111 
112  static bool received_message = false;
113  WindowingInitRequest init_request{
114  .on_message = [](WindowsMessage* message) { received_message = true; }};
115 
116  InternalFlutterWindows_WindowManager_Initialize(engine_id(), &init_request);
117  const int64_t view_id =
119  engine_id(), regular_creation_request());
121  engine_id(), view_id));
122 
123  EXPECT_TRUE(received_message);
124 }
125 
126 TEST_F(WindowManagerTest, CreateRegularWindow) {
127  IsolateScope isolate_scope(isolate());
128 
129  const int64_t view_id =
131  engine_id(), regular_creation_request());
132  EXPECT_EQ(view_id, 0);
133 }
134 
135 TEST_F(WindowManagerTest, GetWindowHandle) {
136  IsolateScope isolate_scope(isolate());
137 
138  const int64_t view_id =
140  engine_id(), regular_creation_request());
141  const HWND window_handle =
143  view_id);
144  EXPECT_NE(window_handle, nullptr);
145 }
146 
147 TEST_F(WindowManagerTest, GetWindowSize) {
148  IsolateScope isolate_scope(isolate());
149 
150  const int64_t view_id =
152  engine_id(), regular_creation_request());
153  const HWND window_handle =
155  view_id);
156 
157  ActualWindowSize size =
159 
160  EXPECT_EQ(size.width,
161  regular_creation_request()->preferred_size.preferred_view_width);
162  EXPECT_EQ(size.height,
163  regular_creation_request()->preferred_size.preferred_view_height);
164 }
165 
166 TEST_F(WindowManagerTest, SetWindowSize) {
167  IsolateScope isolate_scope(isolate());
168 
169  const int64_t view_id =
171  engine_id(), regular_creation_request());
172  const HWND window_handle =
174  view_id);
175 
176  WindowSizeRequest requestedSize{
177 
178  .has_preferred_view_size = true,
179  .preferred_view_width = 640,
180  .preferred_view_height = 480,
181  };
183  &requestedSize);
184 
185  ActualWindowSize actual_size =
187  EXPECT_EQ(actual_size.width, 640);
188  EXPECT_EQ(actual_size.height, 480);
189 }
190 
191 TEST_F(WindowManagerTest, CanConstrainByMinimiumSize) {
192  IsolateScope isolate_scope(isolate());
193 
194  const int64_t view_id =
196  engine_id(), regular_creation_request());
197  const HWND window_handle =
199  view_id);
200  WindowConstraints constraints{.has_view_constraints = true,
201  .view_min_width = 900,
202  .view_min_height = 700,
203  .view_max_width = 10000,
204  .view_max_height = 10000};
206  &constraints);
207 
208  ActualWindowSize actual_size =
210  EXPECT_EQ(actual_size.width, 900);
211  EXPECT_EQ(actual_size.height, 700);
212 }
213 
214 TEST_F(WindowManagerTest, CanConstrainByMaximumSize) {
215  IsolateScope isolate_scope(isolate());
216 
217  const int64_t view_id =
219  engine_id(), regular_creation_request());
220  const HWND window_handle =
222  view_id);
223  WindowConstraints constraints{.has_view_constraints = true,
224  .view_min_width = 0,
225  .view_min_height = 0,
226  .view_max_width = 500,
227  .view_max_height = 500};
229  &constraints);
230 
231  ActualWindowSize actual_size =
233  EXPECT_EQ(actual_size.width, 500);
234  EXPECT_EQ(actual_size.height, 500);
235 }
236 
237 TEST_F(WindowManagerTest, CanFullscreenWindow) {
238  IsolateScope isolate_scope(isolate());
239 
240  const int64_t view_id =
242  engine_id(), regular_creation_request());
243  const HWND window_handle =
245  view_id);
246 
247  FullscreenRequest request{.fullscreen = true, .has_display_id = false};
249 
250  int screen_width = GetSystemMetrics(SM_CXSCREEN);
251  int screen_height = GetSystemMetrics(SM_CYSCREEN);
252  ActualWindowSize actual_size =
254  EXPECT_EQ(actual_size.width, screen_width);
255  EXPECT_EQ(actual_size.height, screen_height);
256  EXPECT_TRUE(
258 }
259 
260 TEST_F(WindowManagerTest, CanUnfullscreenWindow) {
261  IsolateScope isolate_scope(isolate());
262 
263  const int64_t view_id =
265  engine_id(), regular_creation_request());
266  const HWND window_handle =
268  view_id);
269 
270  FullscreenRequest request{.fullscreen = true, .has_display_id = false};
272 
273  request.fullscreen = false;
275 
276  ActualWindowSize actual_size =
278  EXPECT_EQ(actual_size.width, 800);
279  EXPECT_EQ(actual_size.height, 600);
280  EXPECT_FALSE(
282 }
283 
284 TEST_F(WindowManagerTest, CanSetWindowSizeWhileFullscreen) {
285  IsolateScope isolate_scope(isolate());
286 
287  const int64_t view_id =
289  engine_id(), regular_creation_request());
290  const HWND window_handle =
292  view_id);
293 
294  FullscreenRequest request{.fullscreen = true, .has_display_id = false};
296 
297  WindowSizeRequest requestedSize{
298 
299  .has_preferred_view_size = true,
300  .preferred_view_width = 500,
301  .preferred_view_height = 500,
302  };
304  &requestedSize);
305 
306  request.fullscreen = false;
308 
309  ActualWindowSize actual_size =
311  EXPECT_EQ(actual_size.width, 500);
312  EXPECT_EQ(actual_size.height, 500);
313 }
314 
315 TEST_F(WindowManagerTest, CanSetWindowConstraintsWhileFullscreen) {
316  IsolateScope isolate_scope(isolate());
317 
318  const int64_t view_id =
320  engine_id(), regular_creation_request());
321  const HWND window_handle =
323  view_id);
324 
325  FullscreenRequest request{.fullscreen = true, .has_display_id = false};
327 
328  WindowConstraints constraints{.has_view_constraints = true,
329  .view_min_width = 0,
330  .view_min_height = 0,
331  .view_max_width = 500,
332  .view_max_height = 500};
334  &constraints);
335 
336  request.fullscreen = false;
338 
339  ActualWindowSize actual_size =
341  EXPECT_EQ(actual_size.width, 500);
342  EXPECT_EQ(actual_size.height, 500);
343 }
344 
345 TEST_F(WindowManagerTest, CreateModelessDialogWindow) {
346  IsolateScope isolate_scope(isolate());
347  DialogWindowCreationRequest creation_request{
349  .preferred_view_width = 800,
350  .preferred_view_height = 600},
351  .preferred_constraints = {.has_view_constraints = false},
352  .title = L"Hello World",
353  .parent_or_null = nullptr};
354  const int64_t view_id =
356  engine_id(), &creation_request);
357  EXPECT_EQ(view_id, 0);
358 }
359 
360 TEST_F(WindowManagerTest, CreateModalDialogWindow) {
361  IsolateScope isolate_scope(isolate());
362 
363  const int64_t parent_view_id =
365  engine_id(), regular_creation_request());
366  const HWND parent_window_handle =
368  engine_id(), parent_view_id);
369 
370  DialogWindowCreationRequest creation_request{
371  .preferred_size =
372  {
373  .has_preferred_view_size = true,
374  .preferred_view_width = 800,
375  .preferred_view_height = 600,
376  },
377  .preferred_constraints = {.has_view_constraints = false},
378  .title = L"Hello World",
379  .parent_or_null = parent_window_handle};
380 
381  const int64_t view_id =
383  engine_id(), &creation_request);
384  EXPECT_EQ(view_id, 1);
385 
386  const HWND window_handle =
388  view_id);
389  HostWindow* host_window = HostWindow::GetThisFromHandle(window_handle);
390  EXPECT_EQ(host_window->GetOwnerWindow()->GetWindowHandle(),
391  parent_window_handle);
392 }
393 
394 TEST_F(WindowManagerTest, DeactivatedRegularWindowDoesNotReactivateItself) {
395  IsolateScope isolate_scope(isolate());
396 
397  // Two independent top-level regular windows (regular windows have no Win32
398  // owner). |window_handle| is deactivated while |active_window_handle| holds
399  // activation.
400  const int64_t window_view_id =
402  engine_id(), regular_creation_request());
403  const HWND window_handle =
405  engine_id(), window_view_id);
406 
407  const int64_t active_view_id =
409  engine_id(), regular_creation_request());
410  const HWND active_window_handle =
412  engine_id(), active_view_id);
413 
414  SetActiveWindow(active_window_handle);
415  ASSERT_EQ(GetActiveWindow(), active_window_handle);
416 
417  // A window receiving WM_ACTIVATE with WA_INACTIVE must not focus its own
418  // content: SetFocus activates the parent of the focused window (the window
419  // itself), which would reactivate the deactivated window, pull it back to the
420  // top of the z-order, and take activation away from the active window.
421  SendMessage(window_handle, WM_ACTIVATE, MAKEWPARAM(WA_INACTIVE, 0), 0);
422 
423  EXPECT_EQ(GetActiveWindow(), active_window_handle);
424 }
425 
426 TEST_F(WindowManagerTest, DialogCanNeverBeFullscreen) {
427  IsolateScope isolate_scope(isolate());
428 
429  DialogWindowCreationRequest creation_request{
431  .preferred_view_width = 800,
432  .preferred_view_height = 600},
433  .preferred_constraints = {.has_view_constraints = false},
434  .title = L"Hello World",
435  .parent_or_null = nullptr};
436 
437  const int64_t view_id =
439  engine_id(), &creation_request);
440  const HWND window_handle =
442  view_id);
443 
444  FullscreenRequest request{.fullscreen = true, .has_display_id = false};
446  EXPECT_FALSE(
448 }
449 
450 TEST_F(WindowManagerTest, CreateTooltipWindow) {
451  IsolateScope isolate_scope(isolate());
452 
453  const int64_t parent_view_id =
455  engine_id(), regular_creation_request());
456  const HWND parent_window_handle =
458  engine_id(), parent_view_id);
459 
460  auto position_callback = [](const WindowSize& child_size,
461  const WindowRect& parent_rect,
462  const WindowRect& output_rect) -> WindowRect* {
463  WindowRect* rect = static_cast<WindowRect*>(malloc(sizeof(WindowRect)));
464  rect->left = parent_rect.left + 10;
465  rect->top = parent_rect.top + 10;
466  rect->width = child_size.width;
467  rect->height = child_size.height;
468  return rect;
469  };
470 
471  TooltipWindowCreationRequest creation_request{
473  .view_min_width = 100,
474  .view_min_height = 50,
475  .view_max_width = 300,
476  .view_max_height = 200},
477  .parent = parent_window_handle,
478  .get_position_callback = position_callback};
479 
480  const int64_t tooltip_view_id =
482  engine_id(), &creation_request);
483 
484  EXPECT_NE(tooltip_view_id, -1);
485  HWND tooltip_window_handle =
487  engine_id(), tooltip_view_id);
488  EXPECT_NE(tooltip_window_handle, nullptr);
489 }
490 
491 TEST_F(WindowManagerTest, TooltipWindowHasNoActivateStyle) {
492  IsolateScope isolate_scope(isolate());
493 
494  const int64_t parent_view_id =
496  engine_id(), regular_creation_request());
497  const HWND parent_window_handle =
499  engine_id(), parent_view_id);
500 
501  auto position_callback = [](const WindowSize& child_size,
502  const WindowRect& parent_rect,
503  const WindowRect& output_rect) -> WindowRect* {
504  WindowRect* rect = static_cast<WindowRect*>(malloc(sizeof(WindowRect)));
505  rect->left = parent_rect.left + 10;
506  rect->top = parent_rect.top + 10;
507  rect->width = child_size.width;
508  rect->height = child_size.height;
509  return rect;
510  };
511 
512  TooltipWindowCreationRequest creation_request{
514  .view_min_width = 100,
515  .view_min_height = 50,
516  .view_max_width = 300,
517  .view_max_height = 200},
518  .parent = parent_window_handle,
519  .get_position_callback = position_callback};
520 
521  const int64_t tooltip_view_id =
523  engine_id(), &creation_request);
524 
525  HWND tooltip_window_handle =
527  engine_id(), tooltip_view_id);
528 
529  DWORD ex_style = GetWindowLong(tooltip_window_handle, GWL_EXSTYLE);
530  EXPECT_TRUE(ex_style & WS_EX_NOACTIVATE);
531 }
532 
533 TEST_F(WindowManagerTest, TooltipWindowDoesNotStealFocus) {
534  IsolateScope isolate_scope(isolate());
535 
536  const int64_t parent_view_id =
538  engine_id(), regular_creation_request());
539  const HWND parent_window_handle =
541  engine_id(), parent_view_id);
542 
543  // Give focus to the parent window
544  SetFocus(parent_window_handle);
545  HWND focused_before = GetFocus();
546 
547  auto position_callback = [](const WindowSize& child_size,
548  const WindowRect& parent_rect,
549  const WindowRect& output_rect) -> WindowRect* {
550  WindowRect* rect = static_cast<WindowRect*>(malloc(sizeof(WindowRect)));
551  rect->left = parent_rect.left + 10;
552  rect->top = parent_rect.top + 10;
553  rect->width = child_size.width;
554  rect->height = child_size.height;
555  return rect;
556  };
557 
558  TooltipWindowCreationRequest creation_request{
560  .view_min_width = 100,
561  .view_min_height = 50,
562  .view_max_width = 300,
563  .view_max_height = 200},
564  .parent = parent_window_handle,
565  .get_position_callback = position_callback};
566 
567  const int64_t tooltip_view_id =
569  engine_id(), &creation_request);
570 
571  HWND tooltip_window_handle =
573  engine_id(), tooltip_view_id);
574 
575  // Verify focus remains with the parent window
576  HWND focused_after = GetFocus();
577  EXPECT_EQ(focused_before, focused_after);
578  EXPECT_NE(focused_after, tooltip_window_handle);
579 }
580 
581 TEST_F(WindowManagerTest, TooltipWindowReturnsNoActivateOnMouseClick) {
582  IsolateScope isolate_scope(isolate());
583 
584  const int64_t parent_view_id =
586  engine_id(), regular_creation_request());
587  const HWND parent_window_handle =
589  engine_id(), parent_view_id);
590 
591  auto position_callback = [](const WindowSize& child_size,
592  const WindowRect& parent_rect,
593  const WindowRect& output_rect) -> WindowRect* {
594  WindowRect* rect = static_cast<WindowRect*>(malloc(sizeof(WindowRect)));
595  rect->left = parent_rect.left + 10;
596  rect->top = parent_rect.top + 10;
597  rect->width = child_size.width;
598  rect->height = child_size.height;
599  return rect;
600  };
601 
602  TooltipWindowCreationRequest creation_request{
604  .view_min_width = 100,
605  .view_min_height = 50,
606  .view_max_width = 300,
607  .view_max_height = 200},
608  .parent = parent_window_handle,
609  .get_position_callback = position_callback};
610 
611  const int64_t tooltip_view_id =
613  engine_id(), &creation_request);
614 
615  HWND tooltip_window_handle =
617  engine_id(), tooltip_view_id);
618 
619  // Send WM_MOUSEACTIVATE message to the tooltip window
620  LRESULT result = SendMessage(tooltip_window_handle, WM_MOUSEACTIVATE,
621  reinterpret_cast<WPARAM>(parent_window_handle),
622  MAKELPARAM(HTCLIENT, WM_LBUTTONDOWN));
623 
624  // Verify the tooltip returns MA_NOACTIVATE
625  EXPECT_EQ(result, MA_NOACTIVATE);
626 }
627 
628 TEST_F(WindowManagerTest, TooltipWindowUpdatesPositionOnViewSizeChange) {
629  IsolateScope isolate_scope(isolate());
630 
631  const int64_t parent_view_id =
633  engine_id(), regular_creation_request());
634  const HWND parent_window_handle =
636  engine_id(), parent_view_id);
637 
638  // Track the child size passed to the callback
639  static int callback_count = 0;
640  static int last_width = 0;
641  static int last_height = 0;
642 
643  auto position_callback = [](const WindowSize& child_size,
644  const WindowRect& parent_rect,
645  const WindowRect& output_rect) -> WindowRect* {
646  callback_count++;
647  last_width = child_size.width;
648  last_height = child_size.height;
649 
650  // Use malloc since the caller will use free()
651  WindowRect* rect = static_cast<WindowRect*>(malloc(sizeof(WindowRect)));
652  rect->left = parent_rect.left + callback_count * 5;
653  rect->top = parent_rect.top + callback_count * 5;
654  rect->width = child_size.width;
655  rect->height = child_size.height;
656  return rect;
657  };
658 
659  TooltipWindowCreationRequest creation_request{
661  .view_min_width = 100,
662  .view_min_height = 50,
663  .view_max_width = 300,
664  .view_max_height = 200},
665  .parent = parent_window_handle,
666  .get_position_callback = position_callback};
667 
668  // Reset callback tracking
669  callback_count = 0;
670  last_width = 0;
671  last_height = 0;
672 
673  const int64_t tooltip_view_id =
675  engine_id(), &creation_request);
676 
677  HWND tooltip_window_handle =
679  engine_id(), tooltip_view_id);
680 
681  // Get the view associated with the tooltip window
682  FlutterWindowsView* view =
683  engine()->GetViewFromTopLevelWindow(tooltip_window_handle);
684  ASSERT_NE(view, nullptr);
685 
686  // Get initial position
687  RECT initial_rect;
688  GetWindowRect(tooltip_window_handle, &initial_rect);
689  int initial_callback_count = callback_count;
690 
691  // Simulate a frame being generated with new dimensions
692  // This should trigger DidUpdateViewSize which calls UpdatePosition
693  view->OnFrameGenerated(150, 100);
694 
695  // Process any pending tasks to ensure the callback is executed
696  engine()->task_runner()->ProcessTasks();
697 
698  // Verify the callback was called again with the new dimensions
699  EXPECT_GT(callback_count, initial_callback_count);
700  EXPECT_EQ(last_width, 150);
701  EXPECT_EQ(last_height, 100);
702 
703  // Get new position and verify it changed
704  RECT new_rect;
705  GetWindowRect(tooltip_window_handle, &new_rect);
706 
707  // The position should have changed due to our callback logic
708  // (we offset by callback_count * 5)
709  EXPECT_NE(initial_rect.left, new_rect.left);
710  EXPECT_NE(initial_rect.top, new_rect.top);
711 }
712 
713 TEST_F(WindowManagerTest, CreatePopupWindow) {
714  IsolateScope isolate_scope(isolate());
715 
716  const int64_t parent_view_id =
718  engine_id(), regular_creation_request());
719  const HWND parent_window_handle =
721  engine_id(), parent_view_id);
722 
723  auto position_callback = [](const WindowSize& child_size,
724  const WindowRect& parent_rect,
725  const WindowRect& output_rect) -> WindowRect* {
726  WindowRect* rect = static_cast<WindowRect*>(malloc(sizeof(WindowRect)));
727  rect->left = parent_rect.left + 10;
728  rect->top = parent_rect.top + 10;
729  rect->width = child_size.width;
730  rect->height = child_size.height;
731  return rect;
732  };
733 
734  PopupWindowCreationRequest creation_request{
736  .view_min_width = 100,
737  .view_min_height = 50,
738  .view_max_width = 300,
739  .view_max_height = 200},
740  .parent = parent_window_handle,
741  .get_position_callback = position_callback};
742 
743  const int64_t popup_view_id =
745  &creation_request);
746 
747  EXPECT_NE(popup_view_id, -1);
748  HWND popup_window_handle =
750  engine_id(), popup_view_id);
751  EXPECT_NE(popup_window_handle, nullptr);
752 }
753 
754 TEST_F(WindowManagerTest, PopupWindowHasNoActivateStyle) {
755  IsolateScope isolate_scope(isolate());
756 
757  const int64_t parent_view_id =
759  engine_id(), regular_creation_request());
760  const HWND parent_window_handle =
762  engine_id(), parent_view_id);
763 
764  auto position_callback = [](const WindowSize& child_size,
765  const WindowRect& parent_rect,
766  const WindowRect& output_rect) -> WindowRect* {
767  WindowRect* rect = static_cast<WindowRect*>(malloc(sizeof(WindowRect)));
768  rect->left = parent_rect.left + 10;
769  rect->top = parent_rect.top + 10;
770  rect->width = child_size.width;
771  rect->height = child_size.height;
772  return rect;
773  };
774 
775  PopupWindowCreationRequest creation_request{
777  .view_min_width = 100,
778  .view_min_height = 50,
779  .view_max_width = 300,
780  .view_max_height = 200},
781  .parent = parent_window_handle,
782  .get_position_callback = position_callback};
783 
784  const int64_t popup_view_id =
786  &creation_request);
787 
788  HWND popup_window_handle =
790  engine_id(), popup_view_id);
791 
792  DWORD ex_style = GetWindowLong(popup_window_handle, GWL_EXSTYLE);
793  EXPECT_TRUE(ex_style & WS_EX_NOACTIVATE);
794 }
795 
796 TEST_F(WindowManagerTest, PopupWindowDoesNotStealFocus) {
797  IsolateScope isolate_scope(isolate());
798 
799  const int64_t parent_view_id =
801  engine_id(), regular_creation_request());
802  const HWND parent_window_handle =
804  engine_id(), parent_view_id);
805 
806  // Give focus to the parent window
807  SetFocus(parent_window_handle);
808  HWND focused_before = GetFocus();
809 
810  auto position_callback = [](const WindowSize& child_size,
811  const WindowRect& parent_rect,
812  const WindowRect& output_rect) -> WindowRect* {
813  WindowRect* rect = static_cast<WindowRect*>(malloc(sizeof(WindowRect)));
814  rect->left = parent_rect.left + 10;
815  rect->top = parent_rect.top + 10;
816  rect->width = child_size.width;
817  rect->height = child_size.height;
818  return rect;
819  };
820 
821  PopupWindowCreationRequest creation_request{
823  .view_min_width = 100,
824  .view_min_height = 50,
825  .view_max_width = 300,
826  .view_max_height = 200},
827  .parent = parent_window_handle,
828  .get_position_callback = position_callback};
829 
830  const int64_t popup_view_id =
832  &creation_request);
833 
834  HWND popup_window_handle =
836  engine_id(), popup_view_id);
837 
838  // Verify focus remains with the parent window
839  HWND focused_after = GetFocus();
840  EXPECT_EQ(focused_before, focused_after);
841  EXPECT_NE(focused_after, popup_window_handle);
842 }
843 
844 TEST_F(WindowManagerTest, PopupWindowUpdatesPositionOnViewSizeChange) {
845  IsolateScope isolate_scope(isolate());
846 
847  const int64_t parent_view_id =
849  engine_id(), regular_creation_request());
850  const HWND parent_window_handle =
852  engine_id(), parent_view_id);
853 
854  // Track the child size passed to the callback
855  static int callback_count = 0;
856  static int last_width = 0;
857  static int last_height = 0;
858 
859  auto position_callback = [](const WindowSize& child_size,
860  const WindowRect& parent_rect,
861  const WindowRect& output_rect) -> WindowRect* {
862  callback_count++;
863  last_width = child_size.width;
864  last_height = child_size.height;
865 
866  // Use malloc since the caller will use free()
867  WindowRect* rect = static_cast<WindowRect*>(malloc(sizeof(WindowRect)));
868  rect->left = parent_rect.left + callback_count * 5;
869  rect->top = parent_rect.top + callback_count * 5;
870  rect->width = child_size.width;
871  rect->height = child_size.height;
872  return rect;
873  };
874 
875  PopupWindowCreationRequest creation_request{
877  .view_min_width = 100,
878  .view_min_height = 50,
879  .view_max_width = 300,
880  .view_max_height = 200},
881  .parent = parent_window_handle,
882  .get_position_callback = position_callback};
883 
884  // Reset callback tracking
885  callback_count = 0;
886  last_width = 0;
887  last_height = 0;
888 
889  const int64_t popup_view_id =
891  &creation_request);
892 
893  HWND popup_window_handle =
895  engine_id(), popup_view_id);
896 
897  // Get the view associated with the popup window
898  FlutterWindowsView* view =
899  engine()->GetViewFromTopLevelWindow(popup_window_handle);
900  ASSERT_NE(view, nullptr);
901 
902  // Get initial position
903  RECT initial_rect;
904  GetWindowRect(popup_window_handle, &initial_rect);
905  int initial_callback_count = callback_count;
906 
907  // Simulate a frame being generated with new dimensions
908  // This should trigger DidUpdateViewSize which calls UpdatePosition
909  view->OnFrameGenerated(150, 100);
910 
911  // Process any pending tasks to ensure the callback is executed
912  engine()->task_runner()->ProcessTasks();
913 
914  // Verify the callback was called again with the new dimensions
915  EXPECT_GT(callback_count, initial_callback_count);
916  EXPECT_EQ(last_width, 150);
917  EXPECT_EQ(last_height, 100);
918 
919  // Get new position and verify it changed
920  RECT new_rect;
921  GetWindowRect(popup_window_handle, &new_rect);
922 
923  // The position should have changed due to our callback logic
924  // (we offset by callback_count * 5)
925  EXPECT_NE(initial_rect.left, new_rect.left);
926  EXPECT_NE(initial_rect.top, new_rect.top);
927 }
928 
929 TEST_F(WindowManagerTest, CreateRegularWindowSizedToContent) {
930  IsolateScope isolate_scope(isolate());
931 
932  RegularWindowCreationRequest creation_request{
934  .preferred_constraints = {.has_view_constraints = false},
935  .title = L"Sized To Content",
936  .sized_to_content = true,
937  .resizable = false};
938 
939  const int64_t view_id =
941  engine_id(), &creation_request);
942  EXPECT_GE(view_id, 0);
943 }
944 
945 TEST_F(WindowManagerTest, RegularWindowSizedToContentResizesToContent) {
946  IsolateScope isolate_scope(isolate());
947 
948  RegularWindowCreationRequest creation_request{
950  .preferred_constraints = {.has_view_constraints = false},
951  .title = L"Sized To Content",
952  .sized_to_content = true,
953  .resizable = false};
954 
955  const int64_t view_id =
957  engine_id(), &creation_request);
958  const HWND window_handle =
960  view_id);
961 
962  FlutterWindowsView* view = engine()->GetViewFromTopLevelWindow(window_handle);
963  ASSERT_NE(view, nullptr);
964 
965  view->OnFrameGenerated(300, 200);
966  engine()->task_runner()->ProcessTasks();
967 
968  ActualWindowSize size =
970  EXPECT_EQ(size.width, 300);
971  EXPECT_EQ(size.height, 200);
972 }
973 
974 TEST_F(WindowManagerTest,
975  RegularWindowSizedToContentNonResizableHasNoThickFrame) {
976  IsolateScope isolate_scope(isolate());
977 
978  RegularWindowCreationRequest creation_request{
980  .preferred_constraints = {.has_view_constraints = false},
981  .title = L"Sized To Content Non-Resizable",
982  .sized_to_content = true,
983  .resizable = false};
984 
985  const int64_t view_id =
987  engine_id(), &creation_request);
988  const HWND window_handle =
990  view_id);
991 
992  const LONG style = GetWindowLong(window_handle, GWL_STYLE);
993  EXPECT_EQ(style & WS_THICKFRAME, 0L);
994  EXPECT_EQ(style & WS_MAXIMIZEBOX, 0L);
995 }
996 
997 TEST_F(WindowManagerTest, RegularWindowSizedToContentResizableHasThickFrame) {
998  IsolateScope isolate_scope(isolate());
999 
1000  RegularWindowCreationRequest creation_request{
1002  .preferred_constraints = {.has_view_constraints = false},
1003  .title = L"Sized To Content Resizable",
1004  .sized_to_content = true,
1005  .resizable = true};
1006 
1007  const int64_t view_id =
1009  engine_id(), &creation_request);
1010  const HWND window_handle =
1012  view_id);
1013 
1014  const LONG style = GetWindowLong(window_handle, GWL_STYLE);
1015  EXPECT_NE(style & WS_THICKFRAME, 0L);
1016  EXPECT_NE(style & WS_MAXIMIZEBOX, 0L);
1017 }
1018 
1019 TEST_F(WindowManagerTest,
1020  RegularWindowSizedToContentResizableStopsTrackingAfterFirstFrame) {
1021  IsolateScope isolate_scope(isolate());
1022 
1023  RegularWindowCreationRequest creation_request{
1025  .preferred_constraints = {.has_view_constraints = false},
1026  .title = L"Sized To Content Resizable",
1027  .sized_to_content = true,
1028  .resizable = true};
1029 
1030  const int64_t view_id =
1032  engine_id(), &creation_request);
1033  const HWND window_handle =
1035  view_id);
1036 
1037  FlutterWindowsView* view = engine()->GetViewFromTopLevelWindow(window_handle);
1038  ASSERT_NE(view, nullptr);
1039 
1040  view->OnFrameGenerated(300, 200);
1041  engine()->task_runner()->ProcessTasks();
1042 
1043  EXPECT_FALSE(view->IsSizedToContent());
1044 }
1045 
1046 TEST_F(WindowManagerTest, CreateModelessDialogSizedToContent) {
1047  IsolateScope isolate_scope(isolate());
1048 
1049  DialogWindowCreationRequest creation_request{
1051  .preferred_constraints = {.has_view_constraints = false},
1052  .title = L"Modeless Dialog Sized To Content",
1053  .parent_or_null = nullptr,
1054  .sized_to_content = true,
1055  .resizable = false};
1056 
1057  const int64_t view_id =
1059  engine_id(), &creation_request);
1060  EXPECT_GE(view_id, 0);
1061 }
1062 
1063 TEST_F(WindowManagerTest, CreateModalDialogSizedToContent) {
1064  IsolateScope isolate_scope(isolate());
1065 
1066  const int64_t parent_view_id =
1068  engine_id(), regular_creation_request());
1069  const HWND parent_window_handle =
1071  engine_id(), parent_view_id);
1072 
1073  DialogWindowCreationRequest creation_request{
1075  .preferred_constraints = {.has_view_constraints = false},
1076  .title = L"Modal Dialog Sized To Content",
1077  .parent_or_null = parent_window_handle,
1078  .sized_to_content = true,
1079  .resizable = false};
1080 
1081  const int64_t view_id =
1083  engine_id(), &creation_request);
1084  EXPECT_GE(view_id, 0);
1085 }
1086 
1087 TEST_F(WindowManagerTest, DialogWindowSizedToContentResizesToContent) {
1088  IsolateScope isolate_scope(isolate());
1089 
1090  DialogWindowCreationRequest creation_request{
1092  .preferred_constraints = {.has_view_constraints = false},
1093  .title = L"Dialog Sized To Content",
1094  .parent_or_null = nullptr,
1095  .sized_to_content = true,
1096  .resizable = false};
1097 
1098  const int64_t view_id =
1100  engine_id(), &creation_request);
1101  const HWND window_handle =
1103  view_id);
1104 
1105  FlutterWindowsView* view = engine()->GetViewFromTopLevelWindow(window_handle);
1106  ASSERT_NE(view, nullptr);
1107 
1108  view->OnFrameGenerated(300, 200);
1109  engine()->task_runner()->ProcessTasks();
1110 
1111  ActualWindowSize size =
1113  EXPECT_EQ(size.width, 300);
1114  EXPECT_EQ(size.height, 200);
1115 }
1116 
1117 TEST_F(WindowManagerTest,
1118  DialogWindowSizedToContentNonResizableHasNoThickFrame) {
1119  IsolateScope isolate_scope(isolate());
1120 
1121  DialogWindowCreationRequest creation_request{
1123  .preferred_constraints = {.has_view_constraints = false},
1124  .title = L"Dialog Sized To Content Non-Resizable",
1125  .parent_or_null = nullptr,
1126  .sized_to_content = true,
1127  .resizable = false};
1128 
1129  const int64_t view_id =
1131  engine_id(), &creation_request);
1132  const HWND window_handle =
1134  view_id);
1135 
1136  const LONG style = GetWindowLong(window_handle, GWL_STYLE);
1137  EXPECT_EQ(style & WS_THICKFRAME, 0L);
1138 }
1139 
1140 } // namespace testing
1141 } // namespace flutter
bool OnFrameGenerated(size_t width, size_t height)
HWND GetWindowHandle() const
Definition: host_window.cc:341
HostWindow * GetOwnerWindow() const
Definition: host_window.cc:843
static HostWindow * GetThisFromHandle(HWND hwnd)
Definition: host_window.cc:326
static Isolate Current()
Definition: isolate_scope.cc:9
Win32Message message
TEST_F(AccessibilityPluginTest, DirectAnnounceCall)
void(* on_message)(WindowsMessage *)
void InternalFlutterWindows_WindowManager_Initialize(int64_t engine_id, const flutter::WindowingInitRequest *request)
void InternalFlutterWindows_WindowManager_SetWindowConstraints(HWND hwnd, const flutter::WindowConstraints *constraints)
bool InternalFlutterWindows_WindowManager_GetFullscreen(HWND hwnd)
void InternalFlutterWindows_WindowManager_SetWindowSize(HWND hwnd, const flutter::WindowSizeRequest *size)
FlutterViewId InternalFlutterWindows_WindowManager_CreateRegularWindow(int64_t engine_id, const flutter::RegularWindowCreationRequest *request)
FLUTTER_EXPORT FlutterViewId InternalFlutterWindows_WindowManager_CreateDialogWindow(int64_t engine_id, const flutter::DialogWindowCreationRequest *request)
void InternalFlutterWindows_WindowManager_SetFullscreen(HWND hwnd, const flutter::FullscreenRequest *request)
flutter::ActualWindowSize InternalFlutterWindows_WindowManager_GetWindowContentSize(HWND hwnd)
FLUTTER_EXPORT FlutterViewId InternalFlutterWindows_WindowManager_CreateTooltipWindow(int64_t engine_id, const flutter::TooltipWindowCreationRequest *request)
FLUTTER_EXPORT FlutterViewId InternalFlutterWindows_WindowManager_CreatePopupWindow(int64_t engine_id, const flutter::PopupWindowCreationRequest *request)
HWND InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(int64_t engine_id, FlutterViewId view_id)