Files
winutil/pester/install-rendering.Tests.ps1
Chris Titus 58d37bb461 Clean up analyzer warnings across app search and ISO workflows (#4794)
* Clean up analyzer warnings

* Align analyzer warning cleanup policy

* Run PSScriptAnalyzer directly in unittests workflow

* Bind lazy-rendered buttons to click handlers

* Remove WinUtil performance tracing

* Fix Win11 ISO creator temp directory reuse
2026-07-02 12:05:25 -05:00

126 lines
6.1 KiB
PowerShell

#===========================================================================
# Tests - Install tab rendering
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
}
Describe "Install app rendering startup contract" {
It "queues app entries after creating category containers" {
$categoryScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-InstallCategoryAppList.ps1") -Raw
$categoryScript | Should -Match '\$sync\.InstallAppRenderQueue = \[System\.Collections\.Queue\]::new\(\)'
$categoryScript | Should -Match 'Start-WinUtilInstallAppRendering'
$categoryScript | Should -Match 'Pre-group apps by category before creating WPF controls'
}
It "renders queued apps through dispatcher callbacks when a form dispatcher exists" {
$renderScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Start-WinUtilInstallAppRendering.ps1") -Raw
$renderScript | Should -Match 'Dispatcher\.BeginInvoke'
$renderScript | Should -Match 'Invoke-WinUtilInstallAppRenderNextBatch'
$renderScript | Should -Match 'Initialize-InstallAppEntry'
$renderScript | Should -Match '\$sync\.InstallAppEntriesRendered = \$true'
}
It "does not use dispatcher timers for deferred install rendering" {
$renderScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Start-WinUtilInstallAppRendering.ps1") -Raw
$renderScript | Should -Not -Match 'DispatcherTimer'
$renderScript | Should -Not -Match '\$timer'
$renderScript | Should -Not -Match '\$dispatcherTimer'
$renderScript | Should -Not -Match '\$timer\.Stop\(\)'
$renderScript | Should -Not -Match '& \$renderCategory'
}
It "drains queued app batches on the WPF dispatcher without timer scope errors" {
Add-Type -AssemblyName WindowsBase
. (Join-Path $script:repoRoot "functions\private\Start-WinUtilInstallAppRendering.ps1")
$previousSync = Get-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
$previousInitializeAppEntry = Get-Item -Path Function:\Initialize-InstallAppEntry -ErrorAction SilentlyContinue
$previousSearch = Get-Item -Path Function:\Find-AppsByNameOrDescription -ErrorAction SilentlyContinue
$errorCountBefore = $global:Error.Count
try {
$global:sync = [Hashtable]::Synchronized(@{})
$global:sync.currentTab = "Install"
$global:sync.SearchBar = [pscustomobject]@{ Text = "" }
$global:sync.Form = [pscustomobject]@{ Dispatcher = [System.Windows.Threading.Dispatcher]::CurrentDispatcher }
$global:sync.InstallAppRenderQueue = [System.Collections.Queue]::new()
$renderedApps = [System.Collections.Generic.List[string]]::new()
function global:Initialize-InstallAppEntry {
param($TargetElement, $AppKey)
$renderedApps.Add($AppKey)
return "entry:$AppKey"
}
function global:Find-AppsByNameOrDescription {
param($SearchString)
throw "Search should not run for an empty search box in this test."
}
$global:sync.InstallAppRenderQueue.Enqueue([pscustomobject]@{ TargetElement = [pscustomobject]@{}; AppKeys = @("AppA", "AppB") })
$global:sync.InstallAppRenderQueue.Enqueue([pscustomobject]@{ TargetElement = [pscustomobject]@{}; AppKeys = @("AppC") })
$frame = New-Object System.Windows.Threading.DispatcherFrame
$timeout = [System.Diagnostics.Stopwatch]::StartNew()
Start-WinUtilInstallAppRendering
$closeTimer = New-Object System.Windows.Threading.DispatcherTimer
$closeTimer.Interval = [TimeSpan]::FromMilliseconds(25)
$closeTimer.Add_Tick({
param($eventSender)
$timer = [System.Windows.Threading.DispatcherTimer]$eventSender
if ($global:sync.InstallAppEntriesRendered -or $timeout.Elapsed.TotalSeconds -gt 5) {
$timer.Stop()
$frame.Continue = $false
}
})
$closeTimer.Start()
[System.Windows.Threading.Dispatcher]::PushFrame($frame)
$global:sync.InstallAppEntriesRendered | Should -BeTrue
$global:sync.InstallAppRenderQueue.Count | Should -Be 0
@($renderedApps) | Should -Be @("AppA", "AppB", "AppC")
$global:Error.Count | Should -Be $errorCountBefore
} finally {
if ($previousSync) {
Set-Variable -Name sync -Value $previousSync.Value -Scope Global
} else {
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
}
foreach ($functionBackup in @(
@{ Name = "Initialize-InstallAppEntry"; Backup = $previousInitializeAppEntry },
@{ Name = "Find-AppsByNameOrDescription"; Backup = $previousSearch }
)) {
if ($functionBackup.Backup) {
Set-Item -Path "Function:\$($functionBackup.Name)" -Value $functionBackup.Backup.ScriptBlock
} else {
Remove-Item -Path "Function:\$($functionBackup.Name)" -ErrorAction SilentlyContinue
}
}
}
}
It "keeps app-entry metadata lookup independent from the old caller scope" {
$entryScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-InstallAppEntry.ps1") -Raw
$entryScript | Should -Match '\$app = \$sync\.configs\.applicationsHashtable\.\$appKey'
$entryScript | Should -Not -Match '\$Apps\.\$appKey'
}
It "restores delayed app checkbox state from selected apps" {
$entryScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-InstallAppEntry.ps1") -Raw
$entryScript | Should -Match '\$sync\.selectedApps -contains \$appKey'
$entryScript | Should -Match '\$checkBox\.IsChecked = \$true'
}
}