# Shared helpers for the PhotoGIMP Windows tests: logging, screenshots, input, # UI Automation. Dot-source after setting $OutName. (Adapted from our win11debloat.dev test kit.) $ErrorActionPreference = 'Continue' $ProgressPreference = 'SilentlyContinue' $root = Split-Path -Parent $MyInvocation.MyCommand.Path $repo = Split-Path -Parent $root $out = Join-Path $root $(if ($OutName) { $OutName } else { 'results' }) New-Item -ItemType Directory -Force -Path $out | Out-Null $log = Join-Path $out 'run-log.txt' $facts = [ordered]@{} function Log($m) { $line = "[{0:HH:mm:ss}] {1}" -f (Get-Date), $m; Write-Host $line; Add-Content -Path $log -Value $line } function Save($name, $obj) { $obj | ConvertTo-Json -Depth 8 | Set-Content -Path (Join-Path $out $name) -Encoding UTF8 } Add-Type -AssemblyName System.Windows.Forms, System.Drawing, UIAutomationClient, UIAutomationTypes Add-Type @" using System; using System.Runtime.InteropServices; public static class N { [DllImport("user32.dll")] public static extern bool SetProcessDPIAware(); [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, int flags, IntPtr extra); [DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y); [DllImport("user32.dll")] public static extern void mouse_event(int f, int x, int y, int d, IntPtr e); [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern IntPtr FindWindow(string cls, string title); [DllImport("user32.dll")] public static extern bool PostMessage(IntPtr h, uint msg, IntPtr w, IntPtr l); public delegate bool EnumProc(IntPtr h, IntPtr l); [DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc f, IntPtr l); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr h, System.Text.StringBuilder s, int n); [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h); public static System.Collections.Generic.List Find(string part) { var found = new System.Collections.Generic.List(); EnumWindows((h, l) => { var sb = new System.Text.StringBuilder(256); GetWindowText(h, sb, 256); if (IsWindowVisible(h) && sb.ToString().Contains(part)) found.Add(h); return true; }, IntPtr.Zero); return found; } [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; } } public static class Disp { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct DEVMODE { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmDeviceName; public short dmSpecVersion, dmDriverVersion, dmSize, dmDriverExtra; public int dmFields; public int dmPositionX, dmPositionY, dmDisplayOrientation, dmDisplayFixedOutput; public short dmColor, dmDuplex, dmYResolution, dmTTOption, dmCollate; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmFormName; public short dmLogPixels; public int dmBitsPerPel, dmPelsWidth, dmPelsHeight, dmDisplayFlags, dmDisplayFrequency; public int dmICMMethod, dmICMIntent, dmMediaType, dmDitherType, dmReserved1, dmReserved2, dmPanningWidth, dmPanningHeight; } [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern bool EnumDisplaySettings(string d, int mode, ref DEVMODE dm); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int ChangeDisplaySettings(ref DEVMODE dm, int flags); public static int Set(int w, int h) { DEVMODE dm = new DEVMODE(); dm.dmSize = (short)Marshal.SizeOf(typeof(DEVMODE)); if (!EnumDisplaySettings(null, -1, ref dm)) return -99; dm.dmPelsWidth = w; dm.dmPelsHeight = h; dm.dmFields = 0x80000 | 0x100000; return ChangeDisplaySettings(ref dm, 1); } } "@ [N]::SetProcessDPIAware() | Out-Null function Set-Screen { foreach ($r in @(@(1600, 900), @(1440, 900), @(1366, 768), @(1280, 800))) { $res = [Disp]::Set($r[0], $r[1]); Log "ChangeDisplaySettings $($r[0])x$($r[1]) -> $res" if ($res -eq 0) { break } } Start-Sleep 3 $b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds Log "screen now $($b.Width)x$($b.Height)" } function Hide-Consoles { Get-Process WindowsTerminal, conhost, OpenConsole, powershell, pwsh -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 -and $_.Id -ne $PID -and $_.MainWindowTitle -ne 'Win11Debloat' } | ForEach-Object { [N]::ShowWindow($_.MainWindowHandle, 6) | Out-Null } } # Full-screen PNG; with -Window, also a crop of that window's rectangle. function Shot($name, $hwnd = 0) { try { [N]::SetCursorPos(2, 2) | Out-Null; Start-Sleep -Milliseconds 350 $b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds $bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height $g = [System.Drawing.Graphics]::FromImage($bmp) $g.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size) $bmp.Save((Join-Path $out "$name.png"), [System.Drawing.Imaging.ImageFormat]::Png) if ($hwnd -ne 0) { $r = New-Object N+RECT if ([N]::GetWindowRect($hwnd, [ref]$r)) { $x = [Math]::Max(0, $r.Left); $y = [Math]::Max(0, $r.Top) $w = [Math]::Min($b.Width, $r.Right) - $x; $h = [Math]::Min($b.Height, $r.Bottom) - $y if ($w -gt 50 -and $h -gt 50) { $crop = $bmp.Clone((New-Object System.Drawing.Rectangle $x, $y, $w, $h), $bmp.PixelFormat) $crop.Save((Join-Path $out "$name-window.png"), [System.Drawing.Imaging.ImageFormat]::Png); $crop.Dispose() } } } $g.Dispose(); $bmp.Dispose() Log "screenshot $name" } catch { Log "screenshot $name FAILED: $_" } } function Key([byte[]]$keys) { foreach ($k in $keys) { [N]::keybd_event($k, 0, 0, [IntPtr]::Zero); Start-Sleep -Milliseconds 40 } [array]::Reverse($keys) foreach ($k in $keys) { [N]::keybd_event($k, 0, 2, [IntPtr]::Zero); Start-Sleep -Milliseconds 40 } } function Click($x, $y) { [N]::SetCursorPos($x, $y) | Out-Null; Start-Sleep -Milliseconds 150 [N]::mouse_event(0x02, 0, 0, 0, [IntPtr]::Zero); [N]::mouse_event(0x04, 0, 0, 0, [IntPtr]::Zero) } function Wheel($x, $y, $delta) { [N]::SetCursorPos($x, $y) | Out-Null; Start-Sleep -Milliseconds 100; [N]::mouse_event(0x0800, 0, 0, $delta, [IntPtr]::Zero) } $UIA = [System.Windows.Automation.AutomationElement] function FindName($name, $timeout = 20, $scope = $null) { $cond = New-Object System.Windows.Automation.PropertyCondition($UIA::NameProperty, $name) $end = (Get-Date).AddSeconds($timeout) while ((Get-Date) -lt $end) { try { $el = $(if ($scope) { $scope } else { $UIA::RootElement }).FindFirst([System.Windows.Automation.TreeScope]::Descendants, $cond); if ($el) { return $el } } catch { } Start-Sleep -Milliseconds 600 } return $null } function Get-TopWindows { $rows = foreach ($e in $UIA::RootElement.FindAll([System.Windows.Automation.TreeScope]::Children, [System.Windows.Automation.Condition]::TrueCondition)) { $procName = try { (Get-Process -Id $e.Current.ProcessId -ErrorAction Stop).ProcessName } catch { '?' } "{0} | {1} | {2}" -f $e.Current.ClassName, $e.Current.Name, $procName } return @($rows) } function Dismiss-Oobe { # Runner images also open a "System Properties" paging-file notice; close it too. foreach ($e in $UIA::RootElement.FindAll([System.Windows.Automation.TreeScope]::Children, (New-Object System.Windows.Automation.PropertyCondition($UIA::NameProperty, 'System Properties')))) { try { $e.GetCurrentPattern([System.Windows.Automation.WindowPattern]::Pattern).Close(); Log 'closed System Properties notice' } catch { } } # And a "Windows Subsystem for Linux must be updated" prompt in Windows Terminal. # Only that window: on Windows Server the runner's own console lives in Windows Terminal. Get-Process WindowsTerminal -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowTitle -match 'wsl\.exe' } | ForEach-Object { Log "closing WSL prompt window ($($_.Id))"; Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } Get-Process wsl -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } $before = Get-TopWindows if (-not ($before -match 'Shell_OOBEProxy|CloudExperienceHost|WWAHost')) { Log 'no first-run privacy page'; return } Log ('top windows: ' + ($before -join ' ;; ')) Get-Process CloudExperienceHostBroker, WWAHost, CloudExperienceHost -ErrorAction SilentlyContinue | ForEach-Object { Log "closing $($_.ProcessName) ($($_.Id))"; Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } Start-Sleep 3 Log ('top windows after: ' + ((Get-TopWindows) -join ' ;; ')) } function Dump-Names($win, $file) { try { $all = $win.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition) $rows = foreach ($e in $all) { $n = $e.Current.Name; if ($n) { "{0} | {1} | {2}" -f $e.Current.ControlType.ProgrammaticName, $e.Current.AutomationId, $n } } $rows | Select-Object -Unique | Set-Content -Path (Join-Path $out $file) -Encoding UTF8 } catch { Log "dump names failed: $_" } } function Invoke-El($el) { try { $el.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke(); return $true } catch { return $false } }