|
| 1 | +# GUI test (Windows) of window_manager's widgets and close handling, on the |
| 2 | +# fixture window_manager_flutter.dart built into the window_manager example |
| 3 | +# (see flutter_window_manager_gui_test.py for the macOS twin): |
| 4 | +# |
| 5 | +# - WindowCaption: its title strip drags the window, its maximize button |
| 6 | +# maximizes and restores, its minimize button minimizes (the fixture restores |
| 7 | +# after 2 s), and with setPreventClose(true) its close button reports |
| 8 | +# onWindowClose and keeps the window. VirtualWindowFrame's top edge resizes. |
| 9 | +# - With the native title bar, the window's own close button (DWM's caption |
| 10 | +# button bounds) is prevented the same way. |
| 11 | +# - popUpWindowMenu() opens the system menu at the cursor. |
| 12 | +# - setIgnoreMouseEvents(true, forward: true): the window drops out of hit |
| 13 | +# testing (WindowFromPoint passes it) while hovering still reaches it. Nothing |
| 14 | +# is pressed while it ignores the mouse. |
| 15 | +# - destroy() closes the window past setPreventClose and the app exits. |
| 16 | +# |
| 17 | +# Owner-guarded clicks on the app's own window and menu only, no keyboard. |
| 18 | +# Set WINDOW_MANAGER_EXE to the built example (flutter build windows --debug |
| 19 | +# -t lib/gui_test_main.dart, with the fixture copied there). |
| 20 | +$ErrorActionPreference = 'Stop' |
| 21 | +. "$PSScriptRoot\env.ps1" |
| 22 | +. "$PSScriptRoot\winput.ps1" |
| 23 | +. "$PSScriptRoot\guiapp.ps1" |
| 24 | +Add-Type @" |
| 25 | +using System; |
| 26 | +using System.Runtime.InteropServices; |
| 27 | +public static class WmWin { |
| 28 | + [StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; } |
| 29 | + [DllImport("dwmapi.dll")] static extern int DwmGetWindowAttribute(IntPtr h, int a, out RECT r, int size); |
| 30 | + [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr h); |
| 31 | + [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h); |
| 32 | + [DllImport("user32.dll")] public static extern bool IsZoomed(IntPtr h); |
| 33 | + // DWMWA_CAPTION_BUTTON_BOUNDS, relative to the window: minimize, maximize, close. |
| 34 | + public static int[] CaptionButtons(IntPtr h) { |
| 35 | + RECT r; if (DwmGetWindowAttribute(h, 5, out r, Marshal.SizeOf(typeof(RECT))) != 0) return null; |
| 36 | + return new[] { r.L, r.T, r.R, r.B }; |
| 37 | + } |
| 38 | +} |
| 39 | +"@ |
| 40 | + |
| 41 | +Start-Result "$RemoteScratch\flutter_window_manager_gui_test.result.txt" |
| 42 | +$exe = $env:WINDOW_MANAGER_EXE |
| 43 | +if (-not $exe) { throw 'Set WINDOW_MANAGER_EXE to the built fixture' } |
| 44 | +$caption = 'WM GUI caption' |
| 45 | +$button = 46 |
| 46 | + |
| 47 | +Assert-Idle |
| 48 | +$app = Start-GuiApp $exe |
| 49 | +$null = $app.Proc.Handle |
| 50 | +function Get-ViewsQuietly { |
| 51 | + $saved = $ErrorActionPreference |
| 52 | + $ErrorActionPreference = 'Continue' |
| 53 | + try { @(Get-Views $app 2>$null) } catch { @() } finally { $ErrorActionPreference = $saved } |
| 54 | +} |
| 55 | +function Get-State { |
| 56 | + $script:win = Get-Win $app 'wm gui test' |
| 57 | + if (-not $script:win) { throw 'The fixture window is missing' } |
| 58 | + $script:view = Find-View (Get-ViewsQuietly) 'TARGET' |
| 59 | + if (-not $script:view) { throw 'The fixture has no Flutter view to probe' } |
| 60 | +} |
| 61 | +function Get-Value([string]$Name) { |
| 62 | + Get-State |
| 63 | + $t = @($view.texts | % { $_[0] } | ? { $_ -like "$Name *" }) | Select-Object -First 1 |
| 64 | + if ($null -eq $t) { throw "The $Name line is missing" } |
| 65 | + $t.Substring($Name.Length + 1) |
| 66 | +} |
| 67 | +function Press([string]$Label, [double]$Settle = 1.2) { |
| 68 | + Get-State |
| 69 | + Invoke-Click $app (ConvertTo-Screen $win (Get-TextCenter $view $Label)) 400 |
| 70 | + Pause $Settle |
| 71 | +} |
| 72 | +# 0: close, 1: maximize / restore, 2: minimize. |
| 73 | +function Get-CaptionButton([int]$Index) { |
| 74 | + Get-State |
| 75 | + $c = Get-TextCenter $view $caption |
| 76 | + ConvertTo-Screen $win @(($view.size[0] - $button / 2 - $Index * $button), $c[1]) |
| 77 | +} |
| 78 | + |
| 79 | +try { |
| 80 | + $deadline = (Get-Date).AddSeconds(40) |
| 81 | + while (-not (Find-View (Get-ViewsQuietly) 'TARGET') -and (Get-Date) -lt $deadline) { Pause 0.5 } |
| 82 | + Get-State |
| 83 | + Invoke-Activate $app $win ([int](20 * $win.Scale)) |
| 84 | + Check 'the fixture starts with WindowCaption' (Test-ViewText $view $caption) |
| 85 | + |
| 86 | + # DragToMoveArea inside WindowCaption. |
| 87 | + Get-State |
| 88 | + $before = $win |
| 89 | + $start = ConvertTo-Screen $win (Get-TextCenter $view $caption) |
| 90 | + Invoke-Drag $app $start @(@(($start[0] + 40), ($start[1] + 20), 400), @(($start[0] + 160), ($start[1] + 100), 800)) |
| 91 | + Pause 1.5 |
| 92 | + Get-State |
| 93 | + Check 'dragging the caption moved the window' ([math]::Abs($win.Left - $before.Left - 160) -le 8 -and [math]::Abs($win.Top - $before.Top - 100) -le 8) "from $($before.Left),$($before.Top) to $($win.Left),$($win.Top)" |
| 94 | + |
| 95 | + # VirtualWindowFrame's top edge (DragToResizeArea). |
| 96 | + $before = $win |
| 97 | + $edge = ConvertTo-Screen $win @(($view.size[0] / 2 - 120), 3) |
| 98 | + Invoke-Drag $app $edge @(@($edge[0], ($edge[1] - 20), 400), @($edge[0], ($edge[1] - 80), 800)) |
| 99 | + Pause 1.5 |
| 100 | + Get-State |
| 101 | + # The drag legs are physical pixels: 80 up. |
| 102 | + Check 'the top edge resized the window' ([math]::Abs(($win.ClientH - $before.ClientH) - 80) -le 6) "client height $($before.ClientH) -> $($win.ClientH)" |
| 103 | + |
| 104 | + # Maximize and restore with the caption's own button. |
| 105 | + $before = $win |
| 106 | + Invoke-Click $app (Get-CaptionButton 1) 400 |
| 107 | + Pause 1.8 |
| 108 | + Get-State |
| 109 | + Check 'the caption maximize button maximized the window' ([WmWin]::IsZoomed([IntPtr]$win.Hwnd)) |
| 110 | + Check 'and isMaximized says so' ((Get-Value 'maximized') -eq 'true') (Get-Value 'maximized') |
| 111 | + Invoke-Click $app (Get-CaptionButton 1) 400 |
| 112 | + Pause 1.8 |
| 113 | + Get-State |
| 114 | + Check 'pressing it again restored the window' (-not [WmWin]::IsZoomed([IntPtr]$win.Hwnd) -and [math]::Abs($win.ClientW - $before.ClientW) -le 4) "client width $($before.ClientW) -> $($win.ClientW)" |
| 115 | + $ev = Get-Value 'events' |
| 116 | + Check 'and the legacy events saw maximize, unmaximize' ($ev -like '*maximize*' -and $ev -like '*unmaximize*') $ev |
| 117 | + |
| 118 | + # Minimize; the fixture restores after 2 s. |
| 119 | + Invoke-Click $app (Get-CaptionButton 2) 400 |
| 120 | + Pause 0.8 |
| 121 | + $iconic = [WmWin]::IsIconic([IntPtr]$win.Hwnd) |
| 122 | + Pause 3 |
| 123 | + $ev = Get-Value 'events' |
| 124 | + Check 'the caption minimize button minimized the window' $iconic |
| 125 | + Check 'and it came back, with minimize and restore reported' (-not [WmWin]::IsIconic([IntPtr]$win.Hwnd) -and $ev -like '*minimize*' -and $ev -like '*restore*') $ev |
| 126 | + Get-State |
| 127 | + Invoke-Activate $app $win ([int](20 * $win.Scale)) |
| 128 | + |
| 129 | + # Prevented close through the caption's close button. |
| 130 | + Press 'Prevent close on' |
| 131 | + Invoke-Click $app (Get-CaptionButton 0) 400 |
| 132 | + Pause 1.5 |
| 133 | + Check 'with preventClose, the caption close button reports onWindowClose' ((Get-Value 'closeReports') -eq '1') (Get-Value 'closeReports') |
| 134 | + Check 'and the window stays' ([WmWin]::IsWindow([IntPtr]$win.Hwnd) -and -not $app.Proc.HasExited) |
| 135 | + |
| 136 | + # Prevented close through the window's own close button. |
| 137 | + Press 'Native title bar' 1.5 |
| 138 | + Get-State |
| 139 | + $b = [WmWin]::CaptionButtons([IntPtr]$win.Hwnd) |
| 140 | + if (-not $b) { throw 'No caption button bounds' } |
| 141 | + $third = [int](($b[2] - $b[0]) / 3) |
| 142 | + Invoke-Click $app @(($win.Left + $b[2] - [int]($third / 2)), ($win.Top + [int](($b[1] + $b[3]) / 2))) 400 |
| 143 | + Pause 1.5 |
| 144 | + Check 'with preventClose, the native close button reports onWindowClose' ((Get-Value 'closeReports') -eq '2') (Get-Value 'closeReports') |
| 145 | + Check 'and the window stays' ([WmWin]::IsWindow([IntPtr]$win.Hwnd) -and -not $app.Proc.HasExited) |
| 146 | + Press 'Hidden title bar' 1.5 |
| 147 | + |
| 148 | + # popUpWindowMenu at the cursor, which rests on the chip. |
| 149 | + Press 'Menu in 1.5s' 0.2 |
| 150 | + $opened = Wait-Menu $app |
| 151 | + Check 'popUpWindowMenu() opened the system menu' $opened |
| 152 | + if ($opened) { |
| 153 | + $menu = @(Get-OpenMenus $app)[0] |
| 154 | + Check 'with the system menu items' ($menu.Items.Count -ge 5) "$($menu.Items.Count) items" |
| 155 | + # Close it by clicking the app's own window clear of the menu. |
| 156 | + Get-State |
| 157 | + Invoke-Click $app (ConvertTo-Screen $win @(($view.size[0] - 40), ($view.size[1] - 30))) 400 |
| 158 | + Check 'and it closes again' (Wait-Menu $app -Closed) |
| 159 | + } |
| 160 | + |
| 161 | + # Ignore the mouse, forwarding moves; no presses while it lasts. |
| 162 | + $hovers = [int](Get-Value 'hovers') |
| 163 | + Press 'Ignore mouse 8s' 0.8 |
| 164 | + Check 'setIgnoreMouseEvents is on' ((Get-Value 'ignoring') -eq 'true') |
| 165 | + Get-State |
| 166 | + $c = Get-TextCenter $view 'TARGET' |
| 167 | + $pt = ConvertTo-Screen $win $c |
| 168 | + $passed = $true |
| 169 | + foreach ($d in @(@(-80, -30), @(60, 20), @(-20, 40), @(90, -10))) { |
| 170 | + $p = ConvertTo-Screen $win @(($c[0] + $d[0]), ($c[1] + $d[1])) |
| 171 | + Move-Cursor $p 300 |
| 172 | + if ([WInput]::Owner($p[0], $p[1]) -eq $app.Proc.Id) { $passed = $false } |
| 173 | + } |
| 174 | + Pause 0.5 |
| 175 | + Check 'hit testing passes through the window' $passed |
| 176 | + $now = [int](Get-Value 'hovers') |
| 177 | + Check 'with forward, hovering still reaches the window' ($now -gt $hovers) "$hovers -> $now" |
| 178 | + Pause 8 |
| 179 | + Check 'the window takes the mouse back after 8 s' ((Get-Value 'ignoring') -eq 'false' -and [WInput]::Owner($pt[0], $pt[1]) -eq $app.Proc.Id) |
| 180 | + $clicks = [int](Get-Value 'clicks') |
| 181 | + Press 'TARGET' |
| 182 | + Check 'a click lands again' ([int](Get-Value 'clicks') -eq $clicks + 1) (Get-Value 'clicks') |
| 183 | + |
| 184 | + # destroy() past preventClose. |
| 185 | + Check 'preventClose is still on' ((Get-Value 'preventClose') -eq 'true') |
| 186 | + Press 'destroy()' 0.2 |
| 187 | + $exited = $app.Proc.WaitForExit(10000) |
| 188 | + Check 'destroy() closed the window and the app exited' $exited |
| 189 | +} catch { |
| 190 | + Check 'the scenario ran to the end' $false "$($_.Exception.Message) at line $($_.InvocationInfo.ScriptLineNumber)" |
| 191 | +} finally { |
| 192 | + if (-not $app.Proc.HasExited) { Stop-GuiApp $app | Out-Null } |
| 193 | +} |
| 194 | +Say "$(if ($script:Failures) { 'FAILED' } else { 'OK' })" |
0 commit comments