Test-LoudToast.ps1
|
# End-to-end test for LoudToast. Checks with screenshots + Windows OCR that banners really appear. # Switches Do Not Disturb temporarily and restores everything afterwards. Takes about 4 minutes; keep the screen on # and do not cover the bottom right corner of the primary monitor. # powershell -ExecutionPolicy Bypass -File .\Test-LoudToast.ps1 [-SkipDnd] param([switch]$SkipDnd) $ErrorActionPreference = 'Stop' Import-Module "$PSScriptRoot\LoudToast.psd1" -Force $null = [Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime] $null = [Windows.Graphics.Imaging.SoftwareBitmap, Windows.Graphics, ContentType = WindowsRuntime] $null = [Windows.Storage.StorageFile, Windows.Storage, ContentType = WindowsRuntime] Add-Type -AssemblyName System.Runtime.WindowsRuntime Add-Type -Name Dpi -Namespace LoudToastTest -MemberDefinition '[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();' [void][LoudToastTest.Dpi]::SetProcessDPIAware() $asTask = [WindowsRuntimeSystemExtensions].GetMethods() | Where-Object { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' } | Select-Object -First 1 function Await($op, [type]$t) { $task = $asTask.MakeGenericMethod($t).Invoke($null, @($op)); [void]$task.Wait(-1); $task.Result } $ocr = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages() $png = Join-Path $env:TEMP 'loudtoast_test.png' $scr = [Windows.Forms.Screen]::PrimaryScreen.Bounds $rect = New-Object Drawing.Rectangle ($scr.Right - 700), ($scr.Bottom - 1100), 700, 1100 function Seen([string]$word, [int]$tries = 8) { foreach ($i in 1..$tries) { Start-Sleep -Milliseconds 700 $b = New-Object Drawing.Bitmap $rect.Width, $rect.Height; $g = [Drawing.Graphics]::FromImage($b) $g.CopyFromScreen($rect.Location, [Drawing.Point]::Empty, $rect.Size); $g.Dispose(); $b.Save($png); $b.Dispose() $f = Await ([Windows.Storage.StorageFile]::GetFileFromPathAsync($png)) ([Windows.Storage.StorageFile]) $st = Await ($f.OpenAsync([Windows.Storage.FileAccessMode]::Read)) ([Windows.Storage.Streams.IRandomAccessStream]) $dec = Await ([Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($st)) ([Windows.Graphics.Imaging.BitmapDecoder]) $sb = Await ($dec.GetSoftwareBitmapAsync()) ([Windows.Graphics.Imaging.SoftwareBitmap]) $text = (Await ($ocr.RecognizeAsync($sb)) ([Windows.Media.Ocr.OcrResult])).Text; $st.Dispose() if (($text -replace '\s', '') -match ($word -replace '\s', '')) { return $true } } $false } $script:fail = 0 function Check([string]$name, [bool]$ok) { if (-not $ok) { $script:fail++; Copy-Item $png "$env:TEMP\loudtoast_fail_$script:fail.png" -ErrorAction SilentlyContinue } '{0} {1}' -f $(if ($ok) { '[OK] ' } else { "[FAIL] (screenshot: %TEMP%\loudtoast_fail_$script:fail.png)" }), $name } function Restored([string]$mode) { foreach ($i in 1..20) { if ((Get-DndState).UserSelected -eq $mode) { return $true }; Start-Sleep 1 }; $false } function Id { -join (1..6 | ForEach-Object { Get-Random -Minimum 2 -Maximum 10 }) } # no 0/1: OCR confuses them with O/l function Balloon([string]$id, [string]$mode, [string]$appId) { # Show-Balloon blocks while the balloon is visible -> separate process, checked in parallel $pre = if ($appId) { "Set-ToastDefaultAppId '$appId';" } else { '' } Start-Process powershell.exe -WindowStyle Hidden -ArgumentList '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', "Import-Module '$PSScriptRoot\LoudToast.psd1'; $pre Show-Balloon 'WNTEST $id' 'balloon' -Seconds 6 -BreakThrough $mode" Start-Sleep 2 # module import in the child process Seen $id 8 } # ---------------- 1. XML builder (no UI) ---------------- $img = Join-Path $env:TEMP 'loudtoast_test_img.png' $bmp = New-Object Drawing.Bitmap 364, 180; $g = [Drawing.Graphics]::FromImage($bmp); $g.Clear([Drawing.Color]::SteelBlue); $g.Dispose(); $bmp.Save($img); $bmp.Dispose() $x = [xml](Show-Toast 'T' 'B1', 'B2' -Attribution 'via Test' -AppLogo $img -AppLogoCircle -HeroImage $img -Image $img -Scenario Reminder -Duration Long ` -Launch 'l=1' -Timestamp ([datetime]'2026-01-02 03:04:05') -Sound Alarm2 -LoopSound -ButtonStyles ` -Inputs (New-ToastTextBox reply -PlaceholderContent 'Antwort'), (New-ToastSelectionBox snooze ([ordered]@{ '5' = '5 Min'; '15' = '15 Min' })) ` -Buttons (New-ToastButton 'Senden' send -InputId reply -Style Success), (New-ToastSystemButton Snooze -SelectionBoxId snooze), (New-ToastSystemButton Dismiss) ` -ProgressBar (New-ToastProgressBar 'Loading' 0.3 -Title 'Datei' -ValueDisplay '3/10') -Header (New-ToastHeader h1 'Group') -AsXml) Check 'XML: scenario/duration/launch/useButtonStyle' ($x.toast.scenario -eq 'reminder' -and $x.toast.duration -eq 'long' -and $x.toast.launch -eq 'l=1' -and $x.toast.useButtonStyle -eq 'true') Check 'XML: text + attribution' (($x.toast.visual.binding.text | ForEach-Object { if ($_ -is [string]) { $_ } else { $_.'#text' } }) -join '|' -eq 'T|B1|B2|via Test') Check 'XML: images (appLogo circle, hero, inline)' (@($x.toast.visual.binding.image).Count -eq 3 -and $x.SelectSingleNode('//image[@placement="appLogoOverride"]').'hint-crop' -eq 'circle') Check 'XML: Audio Looping.Alarm2 loop' ($x.toast.audio.src -eq 'ms-winsoundevent:Notification.Looping.Alarm2' -and $x.toast.audio.loop -eq 'true') Check 'XML: inputs + selection' (@($x.toast.actions.input).Count -eq 2 -and @($x.SelectNodes('//selection')).Count -eq 2) Check 'XML: buttons (style, system snooze)' ($x.SelectSingleNode('//action[@arguments="send"]').'hint-buttonStyle' -eq 'Success' -and $x.SelectSingleNode('//action[@arguments="snooze"]').activationType -eq 'system') Check 'XML: bound progress + header' ($x.toast.visual.binding.progress.value -eq '{progressValue}' -and $x.toast.header.id -eq 'h1') Check 'XML: Timestamp UTC' ($x.toast.displayTimestamp -match '^2026-01-02T\d\d:04:05Z$') # ---------------- 2. Display, events, updates, history ---------------- $prevDnd = Disable-Dnd try { $id = Id Show-Toast "WNTEST $id" 'all features' -AppLogo $img -HeroImage $img -Attribution 'Test-LoudToast' -Header (New-ToastHeader lt 'LoudToast Test') ` -Inputs (New-ToastTextBox reply -PlaceholderContent 'Reply') -Buttons (New-ToastButton 'Send' send -InputId reply), (New-ToastSystemButton Dismiss) -Tag full Check 'toast with images/inputs/header visible' (Seen $id) Check 'history contains toast' (@(Get-ToastHistory | Where-Object Tag -eq 'full').Count -eq 1) Remove-Toast -Tag full Check 'Remove-Toast' (@(Get-ToastHistory | Where-Object Tag -eq 'full').Count -eq 0) $id = Id Show-Toast "WNTEST $id" 'progress' -ProgressBar (New-ToastProgressBar 'Start' 0.1 -ValueDisplay 'X1') -Tag prog -Duration Long Start-Sleep 1 Check 'Update-ToastProgress' ((Update-ToastProgress -Tag prog -Value 0.8 -Status 'Almost done' -ValueDisplay 'X8') -eq 'Succeeded') Check 'update visible' (Seen 'Almost done') Remove-Toast -Tag prog $spi = Get-ToastDisplayDuration $sw = [Diagnostics.Stopwatch]::StartNew() $id = Id; Show-Toast "WNTEST $id" 'duration' -DisplaySeconds 12 -Tag $id $v = 0 # time of the last frame showing the banner; robust against single OCR misses while ($sw.Elapsed.TotalSeconds -lt 25) { if (Seen $id 1) { $v = $sw.Elapsed.TotalSeconds } } Check ("DisplaySeconds 12: visible {0:N1} s" -f $v) ($v -ge 10 -and $v -le 15) Check "system MessageDuration unchanged ($spi)" ((Get-ToastDisplayDuration) -eq $spi) Remove-Toast -Tag $id $r = Show-Toast "WNTEST $(Id)" 'event test' -ExpirationTime (Get-Date).AddMinutes(1) -Wait -TimeoutSeconds 30 Check "event Dismissed/TimedOut (got $($r.Result)/$($r.Reason))" ($r.Result -eq 'Dismissed' -and $r.Reason -eq 'TimedOut') Show-Toast "WNTEST $(Id)" 'scheduled' -At (Get-Date).AddMinutes(10) -Tag sched Check 'scheduled toast registered' (@(Get-ToastHistory -Scheduled | Where-Object Tag -eq 'sched').Count -eq 1) Remove-Toast -Tag sched -Scheduled Check 'scheduled toast removed' (@(Get-ToastHistory -Scheduled | Where-Object Tag -eq 'sched').Count -eq 0) Remove-Toast -All } finally { Set-DndMode $prevDnd } if ($SkipDnd) { "Failures: $script:fail"; return } # ---------------- 3. Do Not Disturb: prediction vs. reality, BreakThrough ---------------- $origPrio = Get-DndPriorityApp $origUrgent = (Get-ToastAppSetting).AllowUrgent try { foreach ($mode in 'PriorityOnly', 'AlarmsOnly') { Set-DndMode $mode foreach ($sc in 'Default', 'Reminder', 'Alarm', 'IncomingCall', 'Urgent') { $id = Id $pred = (Test-ToastBreakThrough -Scenario $sc.ToLower()).BreaksThrough Show-Toast "WNTEST $id" $sc -Scenario $sc -Buttons (New-ToastButton OK ok) -BreakThrough Never -Tag $id $seen = Seen $id Remove-Toast -Tag $id Check "$mode/$sc Never: banner=$seen, predicted=$pred" ($seen -eq $pred) } $id = Id Show-Toast "WNTEST $id" 'auto' -BreakThrough Auto -BreakThroughSeconds 5 Check "$mode/Default Auto: banner despite DND" (Seen $id) Check "$mode restored" (Restored $mode) Check "$mode/Balloon Never: suppressed" (-not (Balloon (Id) Never)) Start-Sleep 5 Check "$mode/Balloon Auto: banner despite DND" (Balloon (Id) Auto) Check "$mode restored (balloon)" (Restored $mode) } Set-DndMode PriorityOnly Add-DndPriorityApp $id = Id; Show-Toast "WNTEST $id" 'prio' -BreakThrough Never Check 'priority list: toast shows' (Seen $id) Remove-DndPriorityApp Register-ToastApp LoudToast.Test 'LoudToast Test' Add-DndPriorityApp LoudToast.Test Check 'priority list + process AUMID: balloon shows' (Balloon (Id) Never LoudToast.Test) Remove-DndPriorityApp LoudToast.Test [void](Set-ToastAppSetting -AllowUrgent $false) $id = Id; Show-Toast "WNTEST $id" 'urgent' -Scenario Urgent -Buttons (New-ToastButton OK ok) -BreakThrough Never Check 'urgent without consent: suppressed' (-not (Seen $id)) [void](Set-ToastAppSetting -AllowUrgent $true) $id = Id; Show-Toast "WNTEST $id" 'urgent' -Scenario Urgent -Buttons (New-ToastButton OK ok) -BreakThrough Never Check 'urgent with Set-ToastAppSetting -AllowUrgent: shows' (Seen $id) } finally { Set-DndMode $prevDnd foreach ($a in Get-DndPriorityApp) { if ($origPrio -notcontains $a) { Remove-DndPriorityApp $a } } [void](Set-ToastAppSetting -AllowUrgent $origUrgent) Unregister-ToastApp LoudToast.Test Remove-Toast -All Get-ChildItem 'HKCU:\Software\Classes\AppUserModelId' | Where-Object { (Get-ItemProperty $_.PSPath).DisplayName -eq 'LoudToast Test' } | Remove-Item -Recurse } "Failures: $script:fail" |