|
| 1 | +# GUI test (Windows) of core's view_example (C++): a sign-in form of native controls |
| 2 | +# (Label, TextField, Button) laid out by a Column and a Row. Checks the layout the example |
| 3 | +# prints, clicks the real buttons and asserts on the status lines, then resizes the window |
| 4 | +# and checks the re-flow. The Windows counterpart of core_view_test.py. |
| 5 | +# Build first (desktop session): |
| 6 | +# cmake -S $RemoteWorkspace\core -B $RemoteScratch\core-build -G "Visual Studio 17 2022" -A x64 |
| 7 | +# cmake --build $RemoteScratch\core-build --config Debug --target view_example |
| 8 | +# Run (remote-hosts skill): remote.sh <host> setup; remote.sh <host> desktop tools/gui/core_view_test.ps1 120 |
| 9 | +# $env:VIEW_EXAMPLE overrides the executable. No keyboard input. ASCII only. |
| 10 | +$ErrorActionPreference = "Stop" |
| 11 | +. "$PSScriptRoot\env.ps1"; . "$PSScriptRoot\winput.ps1"; . "$PSScriptRoot\guiapp.ps1" |
| 12 | +Start-Result "$RemoteScratch\core_view_test.result.txt" |
| 13 | + |
| 14 | +# The harness only hands back a console app's output when it exits; this test reads the |
| 15 | +# layout lines while the app runs, so it collects them itself. |
| 16 | +Add-Type -TypeDefinition @" |
| 17 | +using System.Collections.Concurrent; |
| 18 | +using System.Diagnostics; |
| 19 | +public static class ViewTestSink { |
| 20 | + public static ConcurrentQueue<string> Lines = new ConcurrentQueue<string>(); |
| 21 | + public static void Attach(Process p) { |
| 22 | + p.OutputDataReceived += (s, e) => { if (e.Data != null) Lines.Enqueue(e.Data); }; |
| 23 | + p.BeginOutputReadLine(); |
| 24 | + } |
| 25 | +} |
| 26 | +"@ |
| 27 | + |
| 28 | +$Content = @(360, 220); $Padding = 16; $Spacing = 8 |
| 29 | + |
| 30 | +function Get-Lines { @([ViewTestSink]::Lines.ToArray()) } |
| 31 | + |
| 32 | +# {label -> @(x, y, w, h)} from the LAST "[view] layout" block (logical points, root-relative). |
| 33 | +function Get-Layout { |
| 34 | + $out = @{} |
| 35 | + foreach ($l in Get-Lines) { |
| 36 | + if ($l -match '^\[view\] layout (\w+) (\S+) (\S+) (\S+) (\S+)') { |
| 37 | + if ($Matches[1] -eq "root") { $out = @{} } |
| 38 | + $out[$Matches[1]] = @([double]$Matches[2], [double]$Matches[3], [double]$Matches[4], [double]$Matches[5]) |
| 39 | + } |
| 40 | + } |
| 41 | + $out |
| 42 | +} |
| 43 | + |
| 44 | +# Callers wrap it in @(): PowerShell unrolls a one-element array into a plain string. |
| 45 | +function Get-Statuses { @(Get-Lines | ? { $_ -like "``[view``] status: *" } | % { $_.Substring(15) }) } |
| 46 | + |
| 47 | +function Near([string]$What, [double]$Actual, [double]$Expected, [double]$Tolerance = 2) { |
| 48 | + Check $What ([math]::Abs($Actual - $Expected) -le $Tolerance) "$Actual vs $Expected" |
| 49 | +} |
| 50 | + |
| 51 | +# Screen centre (physical) of a control, from its root-relative logical frame. The scale |
| 52 | +# comes from the client area against the root's logical width, not from the window's DPI: |
| 53 | +# the example has no DPI manifest, so Windows reports 96 dpi and stretches the window. |
| 54 | +function Get-ControlCenter($Win, $Layout, [string]$Name) { |
| 55 | + $f = $Layout[$Name] |
| 56 | + $scale = $Win.ClientW / $Layout["root"][2] |
| 57 | + @([int]($Win.ClientX + ($f[0] + $f[2] / 2) * $scale), [int]($Win.ClientY + ($f[1] + $f[3] / 2) * $scale)) |
| 58 | +} |
| 59 | + |
| 60 | +# Clicks a control after lifting the window over whatever else is on the desktop. |
| 61 | +function Invoke-ControlClick($App, [string]$Name, $Layout) { |
| 62 | + $w = Get-Win $App "Sign in" |
| 63 | + if (-not $w) { throw "the 'Sign in' window is gone" } |
| 64 | + if (-not $Layout.ContainsKey($Name) -or -not $Layout.ContainsKey("root")) { throw "no layout for $Name" } |
| 65 | + [WInput]::Raise($w.Hwnd) |
| 66 | + Pause 0.3 |
| 67 | + $point = Get-ControlCenter $w $Layout $Name |
| 68 | + Say "click $Name at $($point -join ',') (client $($w.ClientX),$($w.ClientY) $($w.ClientW)x$($w.ClientH))" |
| 69 | + Invoke-Click $App $point |
| 70 | +} |
| 71 | + |
| 72 | +Assert-Idle |
| 73 | +$exe = if ($env:VIEW_EXAMPLE) { $env:VIEW_EXAMPLE } else { "$RemoteScratch\core-build\examples\view_example\Debug\view_example.exe" } |
| 74 | +if (-not (Test-Path $exe)) { throw "$exe is missing; build it first" } |
| 75 | +$psi = New-Object System.Diagnostics.ProcessStartInfo $exe |
| 76 | +$psi.UseShellExecute = $false; $psi.CreateNoWindow = $true; $psi.RedirectStandardOutput = $true |
| 77 | +$proc = [System.Diagnostics.Process]::Start($psi) |
| 78 | +[ViewTestSink]::Attach($proc) |
| 79 | +$app = @{ Proc = $proc; Log = $null; Stdout = $null } |
| 80 | +Pause 3 |
| 81 | +try { |
| 82 | + $win = Get-Win $app "Sign in" |
| 83 | + if (-not $win) { throw "no 'Sign in' window: $((Get-Wins $app | % { $_.Title }) -join ', ')" } |
| 84 | + [WInput]::Move($win.Hwnd, 120, 120) |
| 85 | + Pause 0.5 |
| 86 | + $win = Get-Win $app "Sign in" |
| 87 | + # Activate by clicking the root's top padding: the default spot on the caption assumes |
| 88 | + # the reported 96 dpi, but the stretched window's caption buttons are 1.5x wider there |
| 89 | + # and that click would land on Minimize. |
| 90 | + Invoke-Activate $app $win 4 |
| 91 | + |
| 92 | + $layout = Get-Layout |
| 93 | + $names = @("root", "heading", "name", "password", "status", "actions", "clear", "sign_in") |
| 94 | + Check "layout printed for every control" (@($names | ? { -not $layout.ContainsKey($_) }).Count -eq 0) (($layout.Keys | Sort-Object) -join ", ") |
| 95 | + Near "root is the content area (w)" $layout["root"][2] $Content[0] |
| 96 | + Near "root is the content area (h)" $layout["root"][3] $Content[1] |
| 97 | + Near "client area has the root's aspect" ($win.ClientH / $win.ClientW) ($Content[1] / $Content[0]) 0.01 |
| 98 | + Near "heading starts at the padding" $layout["heading"][1] $Padding |
| 99 | + Near "heading spans the padded width" $layout["heading"][2] ($Content[0] - 2 * $Padding) |
| 100 | + $h = $layout["heading"]; $n = $layout["name"] |
| 101 | + Near "name follows the heading" $n[1] ($h[1] + $h[3] + $Spacing) |
| 102 | + Near "password follows name" $layout["password"][1] ($n[1] + $n[3] + $Spacing) |
| 103 | + Check "a text field has a height" ($n[3] -gt 10) "$($n[3])" |
| 104 | + Near "actions row is 32 high (preferred size)" $layout["actions"][3] 32 |
| 105 | + $s = $layout["sign_in"]; $c = $layout["clear"] |
| 106 | + Near "sign in hugs the trailing edge" ($s[0] + $s[2]) ($Content[0] - $Padding) |
| 107 | + Near "clear sits before sign in, spaced" ($c[0] + $c[2] + $Spacing) $s[0] |
| 108 | + Check "buttons have an intrinsic width" (($s[2] -gt 40) -and ($s[2] -lt 140)) "$($s[2])" |
| 109 | + |
| 110 | + # Act: "Sign in" with an empty username is refused and focuses the username field. |
| 111 | + Invoke-ControlClick $app "sign_in" $layout |
| 112 | + Pause 1.0 |
| 113 | + $st = @(Get-Statuses) |
| 114 | + Check "empty sign-in is refused" ($st.Count -gt 0 -and $st[-1] -eq "A username is required.") ($st -join " | ") |
| 115 | + Check "the refused sign-in focused the username field" (@(Get-Lines | ? { $_ -eq "[view] focused: name" }).Count -gt 0) |
| 116 | + |
| 117 | + # "Clear" resets the status. |
| 118 | + Invoke-ControlClick $app "clear" (Get-Layout) |
| 119 | + Pause 1.0 |
| 120 | + $st = @(Get-Statuses) |
| 121 | + Check "clear resets the status" ($st.Count -gt 0 -and $st[-1] -eq "Enter your credentials.") ($st -join " | ") |
| 122 | + |
| 123 | + # Resize: the Column and the Row re-flow to the new width. |
| 124 | + $win = Get-Win $app "Sign in" |
| 125 | + $px = $win.ClientW / $Content[0] |
| 126 | + $w = $win.Right - $win.Left; $hh = $win.Bottom - $win.Top |
| 127 | + [WInput]::SetBounds($win.Hwnd, $win.Left, $win.Top, $w + [int](120 * $px), $hh + [int](60 * $px)) |
| 128 | + Pause 1.0 |
| 129 | + $after = Get-Layout |
| 130 | + Near "root grew with the window (w)" $after["root"][2] ($Content[0] + 120) |
| 131 | + Near "root grew with the window (h)" $after["root"][3] ($Content[1] + 60) |
| 132 | + Near "heading re-stretched" $after["heading"][2] ($Content[0] + 120 - 2 * $Padding) |
| 133 | + $s2 = $after["sign_in"] |
| 134 | + Near "sign in followed the trailing edge" ($s2[0] + $s2[2]) ($Content[0] + 120 - $Padding) |
| 135 | + Near "button width unchanged by the resize" $s2[2] $s[2] |
| 136 | + |
| 137 | + # Look again: the button is still where the layout says after the re-flow. |
| 138 | + Invoke-ControlClick $app "sign_in" $after |
| 139 | + Pause 1.0 |
| 140 | + $st = @(Get-Statuses) |
| 141 | + Check "sign in still answers after the re-flow" ($st.Count -ge 3 -and $st[-1] -eq "A username is required.") ($st -join " | ") |
| 142 | +} catch { |
| 143 | + Check "ran to the end" $false "$_" |
| 144 | +} finally { |
| 145 | + Stop-Process -Id $proc.Id -ErrorAction SilentlyContinue |
| 146 | + Pause 1 |
| 147 | +} |
| 148 | +Get-Lines | ? { $_ -notlike "``[view``] layout *" } | % { Say "app: $_" } |
| 149 | +Say "$script:Failures failure(s)" |
| 150 | +exit $script:Failures |
0 commit comments