# vfp_ui_harness.ps1 - harness reutilizabil pentru teste UI VFP cu capturi de ecran. # Lanseaza un test VFP care afiseaza un formular real, il conduce pas cu pas prin semafoare # (ready_.txt scris de test <-> cont_.txt scris de harness), face un screenshot dupa # fiecare pas si il valideaza. Testul VFP foloseste procedurile din ui_harness.prg. # # Exemplu: # powershell -ExecutionPolicy Bypass -File vfp_ui_harness.ps1 ` # -TestPrg 'D:\...\test_x_ui.prg' -Steps @('afisat','actiune1','actiune2') # # Capcane rezolvate (vezi docs\testare-ui-vfp.md): precompilare izolata, lansare .fxp, # detectie pornire pe mtime log, timeout-uri generoase, kill zombi vfp9. # # FARA FURT DE FOCUS (cerinta Marius, 17/07/2026): capturile NU mai folosesc # Graphics.CopyFromScreen (poza ecranului intreg - fura focus / se corupe daca utilizatorul # lucreaza in paralel). Se foloseste PrintWindow pe handle-ul ferestrei vfp9 + fereastra e # mutata OFF-SCREEN (x=-4000, HWND_BOTTOM, SWP_NOACTIVATE) imediat ce apare handle-ul si # re-impinsa inainte de fiecare pas (VFP isi poate re-activa fereastra la Show()/dialoguri). # Fereastra ramane "visible" pentru GDI (PrintWindow merge), doar nu e pe ecranul vizibil # utilizatorului si nu ia focus. param( [Parameter(Mandatory=$true)][string]$TestPrg, [Parameter(Mandatory=$true)][string[]]$Steps, # etichete pentru pasii 0..N-1 [int]$StepTimeoutSec = 130, # cat astept fiecare ready_ (dupa pas 0) [int]$ReadyTimeoutSec = 180, # cat astept ready_0 (afisarea formularului, incarcare lenta) [string]$ShotsDir, [string]$SyncDir, [string]$Vfp = 'C:\Program Files (x86)\Microsoft Visual FoxPro 9\vfp9.exe' ) $ErrorActionPreference = 'Stop' $TestDir = [System.IO.Path]::GetDirectoryName($TestPrg) $TestFxp = [System.IO.Path]::ChangeExtension($TestPrg, 'fxp') $TestLog = [System.IO.Path]::ChangeExtension($TestPrg, $null) + '_log.txt' if (-not $SyncDir) { $SyncDir = Join-Path $TestDir 'uisync' } if (-not $ShotsDir) { $ShotsDir = Join-Path $TestDir 'screenshots' } $Precompile = Join-Path $PSScriptRoot '_precompile.ps1' Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing Add-Type @" using System; using System.Runtime.InteropServices; public class Win32Ui { [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; } [DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr hwnd, IntPtr hdc, uint nFlags); [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr hWnd); public static readonly IntPtr HWND_BOTTOM = new IntPtr(1); public const uint SWP_NOSIZE = 0x0001; public const uint SWP_NOACTIVATE = 0x0010; public const uint SWP_NOZORDER = 0x0004; } "@ function Clear-Dir([string]$Dir) { if (Test-Path $Dir) { Get-ChildItem -Path $Dir -File -ErrorAction SilentlyContinue | ForEach-Object { try { [System.IO.File]::Delete($_.FullName) } catch {} } } else { New-Item -ItemType Directory -Path $Dir | Out-Null } } # Muta fereastra vfp9 off-screen (x=-4000), fara sa o activeze/minimizeze - ramane randabila # pentru PrintWindow, dar nu se vede si nu fura focus. Se cheama repetat (VFP isi reactiveaza # singur fereastra la Show()/dialoguri modale). function Push-Offscreen([IntPtr]$Hwnd) { if ($Hwnd -eq [IntPtr]::Zero) { return } if (-not [Win32Ui]::IsWindow($Hwnd)) { return } [Win32Ui]::SetWindowPos($Hwnd, [Win32Ui]::HWND_BOTTOM, -4000, 0, 0, 0, ([Win32Ui]::SWP_NOSIZE -bor [Win32Ui]::SWP_NOACTIVATE)) | Out-Null } # Polleaza MainWindowHandle pana devine nenul (fereastra principala vfp9 aparuta). function Wait-MainWindowHandle([System.Diagnostics.Process]$Proc, [int]$TimeoutSec) { $deadline = (Get-Date).AddSeconds($TimeoutSec) while ((Get-Date) -lt $deadline) { try { $Proc.Refresh() if ($Proc.HasExited) { return [IntPtr]::Zero } if ($Proc.MainWindowHandle -ne [IntPtr]::Zero) { return $Proc.MainWindowHandle } } catch {} Start-Sleep -Milliseconds 200 } return [IntPtr]::Zero } function Wait-File([string]$Path, [int]$TimeoutSec, [IntPtr]$Hwnd = [IntPtr]::Zero) { $deadline = (Get-Date).AddSeconds($TimeoutSec) while (-not (Test-Path $Path)) { if ((Get-Date) -gt $deadline) { return $false } if ($Hwnd -ne [IntPtr]::Zero) { Push-Offscreen $Hwnd } Start-Sleep -Milliseconds 300 } return $true } # Captura pe HANDLE cu PrintWindow (nu depinde de foreground/ecran vizibil). flag 2 = # PW_RENDERFULLCONTENT (randare completa, necesar pt. multe ferestre GDI+/themed); daca da # imagine goala, se incearca fallback cu flag 0. function Take-Screenshot([IntPtr]$Hwnd, [string]$Path) { if ($Hwnd -eq [IntPtr]::Zero -or -not [Win32Ui]::IsWindow($Hwnd)) { Write-Warning "Take-Screenshot: handle invalid, sar captura ($Path)." return } $rect = New-Object Win32Ui+RECT [Win32Ui]::GetWindowRect($Hwnd, [ref]$rect) | Out-Null $w = $rect.Right - $rect.Left $h = $rect.Bottom - $rect.Top if ($w -le 0 -or $h -le 0) { $w = 1920; $h = 1080 } $bmp = New-Object System.Drawing.Bitmap $w, $h $g = [System.Drawing.Graphics]::FromImage($bmp) $hdc = $g.GetHdc() $ok = [Win32Ui]::PrintWindow($Hwnd, $hdc, 2) $g.ReleaseHdc($hdc) $g.Dispose() if (-not $ok) { $g2 = [System.Drawing.Graphics]::FromImage($bmp) $hdc2 = $g2.GetHdc() [Win32Ui]::PrintWindow($Hwnd, $hdc2, 0) | Out-Null $g2.ReleaseHdc($hdc2) $g2.Dispose() } $bmp.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png) $bmp.Dispose() } # omoara doar vfp9 proprii (linia de comanda contine folderul testului), nu instante straine function Stop-VfpProprii([string]$Marker) { Get-CimInstance Win32_Process -Filter "Name='vfp9.exe'" -ErrorAction SilentlyContinue | ForEach-Object { if ($_.CommandLine -and $_.CommandLine.ToLower().Contains($Marker.ToLower())) { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } } } Clear-Dir $SyncDir Clear-Dir $ShotsDir # 1) Precompilare izolata (proces copil complet, ca sa nu otraveasca sesiunea de lansare). # -WindowStyle Hidden pe consola powershell copil (sigur - e consola, nu GUI VFP). Write-Host "Precompilez .fxp (izolat): $Precompile -Prg $TestPrg" Stop-VfpProprii $TestDir Start-Sleep -Milliseconds 500 $pc = Start-Process powershell -WindowStyle Hidden -ArgumentList @('-ExecutionPolicy','Bypass','-File', "`"$Precompile`"", '-Prg', "`"$TestPrg`"") -Wait -PassThru Start-Sleep -Seconds 2 Stop-VfpProprii $TestDir Start-Sleep -Seconds 2 if (-not (Test-Path $TestFxp)) { Write-Warning "Nu s-a generat $TestFxp - compilare esuata." } # 2) Lansez .FXP-ul (nu .prg-ul). vfp9 -A porneste uneori procesul FARA sa execute (log gol); # discriminator: START e prima linie a testului (mtime log > momentul lansarii) => in ~2s daca # ruleaza. Relansez doar daca log-ul nu e scris in 30s (instanta moarta). Apoi astept ready_0. # Imediat ce apare MainWindowHandle, fereastra e impinsa off-screen (fara activare). $ready0 = Join-Path $SyncDir 'ready_0.txt' $started = $false $hwnd = [IntPtr]::Zero foreach ($try in 1..8) { if (Test-Path $ready0) { [System.IO.File]::Delete($ready0) } if (Test-Path $TestLog) { [System.IO.File]::Delete($TestLog) } $t0 = Get-Date Write-Host "Lansez VFP (incercarea $try): $Vfp -A $TestFxp" $proc = Start-Process -FilePath $Vfp -ArgumentList @('-A', "`"$TestFxp`"") -PassThru $dl = (Get-Date).AddSeconds(30) while ((Get-Date) -lt $dl) { if ($hwnd -eq [IntPtr]::Zero -and -not $proc.HasExited) { $proc.Refresh() if ($proc.MainWindowHandle -ne [IntPtr]::Zero) { $hwnd = $proc.MainWindowHandle Push-Offscreen $hwnd } } else { Push-Offscreen $hwnd } if ((Test-Path $TestLog) -and ((Get-Item $TestLog).LastWriteTime -gt $t0)) { $started = $true; break } if (Test-Path $ready0) { $started = $true; break } Start-Sleep -Milliseconds 500 } if ($started) { break } Write-Warning "Incercarea ${try}: testul nu a scris START in 30s (instanta moarta); relansez." if ($proc -and -not $proc.HasExited) { try { $proc.Kill() } catch {} } Stop-VfpProprii $TestDir Start-Sleep -Milliseconds 1500 $hwnd = [IntPtr]::Zero } # Daca handle-ul principal inca nu a aparut (formular aditional creat mai tarziu), mai astept putin. if ($hwnd -eq [IntPtr]::Zero) { $hwnd = Wait-MainWindowHandle $proc 15 } Push-Offscreen $hwnd if (-not (Wait-File $ready0 $ReadyTimeoutSec $hwnd)) { Write-Warning "Testul nu a ajuns la ready_0 in ${ReadyTimeoutSec}s. Ultima linie log: '$(Get-Content $TestLog -Tail 1 -ErrorAction SilentlyContinue)'" } # 3) Parcurg pasii: astept ready_ (re-impingand fereastra off-screen periodic), screenshot # pe handle (PrintWindow), scriu cont_. $ok = $true for ($n = 0; $n -lt $Steps.Count; $n++) { $label = $Steps[$n] $ready = Join-Path $SyncDir ("ready_{0}.txt" -f $n) $cont = Join-Path $SyncDir ("cont_{0}.txt" -f $n) Write-Host "Astept pasul $n ($label)" if (-not (Wait-File $ready $StepTimeoutSec $hwnd)) { Write-Warning "TIMEOUT la pasul $n." $ok = $false break } Write-Host (" ready: {0}" -f (Get-Content $ready -Raw).Trim()) # daca fereastra principala s-a schimbat intre timp (formular nou), reincerc handle-ul if ($hwnd -eq [IntPtr]::Zero -or -not [Win32Ui]::IsWindow($hwnd)) { $hwnd = Wait-MainWindowHandle $proc 5 } Push-Offscreen $hwnd Start-Sleep -Milliseconds 1200 # las formularul sa se deseneze complet Push-Offscreen $hwnd $png = Join-Path $ShotsDir ("step_{0}_{1}.png" -f $n, $label) Take-Screenshot $hwnd $png Write-Host " screenshot: $png" Set-Content -Path $cont -Value 'go' -Encoding ascii } # 4) Astept done.txt si inchid. if ($ok) { $done = Join-Path $SyncDir 'done.txt' if (Wait-File $done 30 $hwnd) { Write-Host "done: $((Get-Content $done -Raw).Trim())" } } Start-Sleep -Milliseconds 800 if ($proc -and -not $proc.HasExited) { try { $proc.Kill() } catch {} } Stop-VfpProprii $TestDir Write-Host "GATA. Screenshots in $ShotsDir"