Files
winutil/pester/xaml.Tests.ps1
Chris Titus 93dc23dd66 Speed up runspace and tab initialization (#4793)
* Add startup performance tracing

* Lazy initialize non-default tabs

* Render install app entries incrementally

* Defer and cache toggle status checks

* Clean up runspace invocation ownership

* Defer GUI runspace pool startup

* Defer status taskbar asset rendering

* Record final speed verification

* Fix deferred install render timer callback

* Add dispatcher smoke coverage for install rendering

* Replace install render timer with dispatcher callbacks

* Gate performance tracing behind compile switch

* Clean up analyzer warnings in speed changes

* Speed up runspace and tab initialization
2026-07-01 23:34:59 -05:00

411 lines
15 KiB
PowerShell

#===========================================================================
# Tests - XAML Control Wiring
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$script:configRoot = Join-Path $script:repoRoot "config"
$script:functionRoot = Join-Path $script:repoRoot "functions"
$script:scriptsRoot = Join-Path $script:repoRoot "scripts"
$script:xamlPath = Join-Path $script:repoRoot "xaml\inputXML.xaml"
$script:mainScriptPath = Join-Path $script:scriptsRoot "main.ps1"
$script:buttonScriptPath = Join-Path $script:functionRoot "public\Invoke-WPFButton.ps1"
$script:xamlText = Get-Content -Path $script:xamlPath -Raw
$script:xaml = [xml]$script:xamlText
$script:xamlNamespace = New-Object System.Xml.XmlNamespaceManager -ArgumentList $script:xaml.NameTable
$script:xamlNamespace.AddNamespace("x", "http://schemas.microsoft.com/winfx/2006/xaml")
function script:Get-WinUtilConfigObject {
param([string]$Name)
Get-Content -Path (Join-Path $script:configRoot "$Name.json") -Raw | ConvertFrom-Json
}
function script:Get-WinUtilSourceText {
Get-ChildItem -Path @($script:functionRoot, $script:scriptsRoot) -Filter *.ps1 -Recurse |
ForEach-Object { Get-Content -Path $_.FullName -Raw } |
Out-String
}
function script:Get-WinUtilTopLevelFunctionNames {
Get-ChildItem -Path $script:functionRoot -Filter *.ps1 -Recurse | ForEach-Object {
$tokens = $null
$syntaxErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$tokens, [ref]$syntaxErrors)
if ($syntaxErrors.Count -ne 0) {
throw ($syntaxErrors | Out-String)
}
$ast.EndBlock.Statements |
Where-Object { $_ -is [System.Management.Automation.Language.FunctionDefinitionAst] } |
ForEach-Object { $_.Name }
} | Sort-Object -Unique
}
function script:Get-WinUtilXamlNamedControls {
$nodes = $script:xaml.SelectNodes("//*[@Name or @x:Name]", $script:xamlNamespace)
foreach ($node in $nodes) {
$controlName = $node.GetAttribute("Name")
if ([string]::IsNullOrWhiteSpace($controlName)) {
$controlName = $node.GetAttribute("Name", "http://schemas.microsoft.com/winfx/2006/xaml")
}
[pscustomobject]@{
Name = $controlName
Type = $node.LocalName
}
}
}
function script:Get-WinUtilXamlRuntimeNamedControls {
$nodes = $script:xaml.SelectNodes("//*[@Name]")
foreach ($node in $nodes) {
[pscustomobject]@{
Name = $node.GetAttribute("Name")
Type = $node.LocalName
}
}
}
function script:Get-WinUtilGeneratedControlNames {
$names = New-Object System.Collections.Generic.List[string]
foreach ($configName in @("appnavigation", "tweaks", "feature", "appx")) {
$config = Get-WinUtilConfigObject -Name $configName
foreach ($entry in $config.PSObject.Properties.Name) {
$names.Add($entry)
}
}
$applications = Get-WinUtilConfigObject -Name "applications"
foreach ($entry in $applications.PSObject.Properties.Name) {
$names.Add($entry)
$names.Add(($entry -replace "-", "_"))
}
$names | Sort-Object -Unique
}
function script:Get-WinUtilButtonSwitchNames {
$buttonSource = Get-Content -Path $script:buttonScriptPath -Raw
[regex]::Matches($buttonSource, '"(WPF[A-Za-z0-9_]+)"\s*\{') |
ForEach-Object { $_.Groups[1].Value } |
Sort-Object -Unique
}
function script:Test-WinUtilNameInSet {
param(
[Parameter(Mandatory)]
[string]$Name,
[Parameter(Mandatory)]
[string[]]$Set
)
foreach ($item in $Set) {
if ($item -ieq $Name) {
return $true
}
}
return $false
}
}
Describe "XAML document" {
It "loads inputXML.xaml as a WPF window XML document" {
$script:xaml.DocumentElement.LocalName | Should -Be "Window"
}
It "contains expected core controls" {
$xamlNames = @(Get-WinUtilXamlRuntimeNamedControls | ForEach-Object { $_.Name })
$requiredControls = @(
"WPFMainGrid",
"NavDockPanel",
"GridBesideNavDockPanel",
"WPFTabNav",
"WPFTab1",
"WPFTab2",
"WPFTab3",
"WPFTab4",
"WPFTab5",
"WPFTab6",
"WPFTab1BT",
"WPFTab2BT",
"WPFTab3BT",
"WPFTab4BT",
"WPFTab5BT",
"WPFTab6BT",
"SearchBar",
"SearchBarClearButton",
"appscategory",
"appspanel",
"tweakspanel",
"featurespanel",
"updatespanel",
"appxpanel",
"WPFstandard",
"WPFminimal",
"WPFAdvanced",
"WPFClearTweaksSelection",
"WPFGetInstalledTweaks",
"WPFTweaksbutton",
"WPFUndoall",
"WPFUpdatesdefault",
"WPFUpdatessecurity",
"WPFUpdatesdisable",
"WPFDefaultAppxSelection",
"WPFGetInstalledAppx",
"WPFSelectAllAppx",
"WPFClearAppxSelection",
"WPFRemoveSelectedAppx"
)
$missingControls = @($requiredControls | Where-Object { -not (Test-WinUtilNameInSet -Name $_ -Set $xamlNames) })
if ($missingControls.Count -gt 0) {
throw ($missingControls -join "`n")
}
}
It "contains Win11 Creator controls used by the ISO workflow" {
$xamlNames = @(Get-WinUtilXamlRuntimeNamedControls | ForEach-Object { $_.Name })
$requiredControls = @(
"Win11ISOPanel",
"WPFWin11ISOSelectSection",
"WPFWin11ISOPath",
"WPFWin11ISOBrowseButton",
"WPFWin11ISOFileInfo",
"WPFWin11ISODownloadLink",
"WPFWin11ISOMountSection",
"WPFWin11ISOMountButton",
"WPFWin11ISOInjectDrivers",
"WPFWin11ISOVerifyResultPanel",
"WPFWin11ISOMountDriveLetter",
"WPFWin11ISOArchLabel",
"WPFWin11ISOEditionComboBox",
"WPFWin11ISOModifySection",
"WPFWin11ISOModifyButton",
"WPFWin11ISOOutputSection",
"WPFWin11ISOCleanResetButton",
"WPFWin11ISOChooseISOButton",
"WPFWin11ISOChooseUSBButton",
"WPFWin11ISOOptionUSB",
"WPFWin11ISOUSBDriveComboBox",
"WPFWin11ISORefreshUSBButton",
"WPFWin11ISOWriteUSBButton",
"WPFWin11ISOStatusLog"
)
$missingControls = @($requiredControls | Where-Object { -not (Test-WinUtilNameInSet -Name $_ -Set $xamlNames) })
if ($missingControls.Count -gt 0) {
throw ($missingControls -join "`n")
}
}
It "defines core tabs in the expected order" {
$tabItems = @($script:xaml.SelectNodes('//*[local-name()="TabControl"][@Name="WPFTabNav"]/*[local-name()="TabItem"]'))
$actualTabs = @($tabItems | ForEach-Object { "$($_.GetAttribute("Name")):$($_.GetAttribute("Header"))" })
$expectedTabs = @(
"WPFTab1:Install",
"WPFTab2:Tweaks",
"WPFTab3:Config",
"WPFTab4:Updates",
"WPFTab5:Win11ISO",
"WPFTab6:AppX"
)
if (@($actualTabs).Count -ne $expectedTabs.Count) {
throw "Expected $($expectedTabs.Count) tabs but found $(@($actualTabs).Count): $($actualTabs -join ', ')"
}
for ($index = 0; $index -lt $expectedTabs.Count; $index++) {
if ($actualTabs[$index] -ne $expectedTabs[$index]) {
throw "Tab order mismatch at index ${index}: expected $($expectedTabs[$index]), found $($actualTabs[$index])"
}
}
}
}
Describe "XAML and sync wiring" {
It "wires generated config panels to existing target grids" {
$xamlNames = @(Get-WinUtilXamlRuntimeNamedControls | ForEach-Object { $_.Name })
$mainLines = Get-Content -Path $script:mainScriptPath
$invalidTargets = New-Object System.Collections.Generic.List[string]
foreach ($line in $mainLines) {
if ($line.TrimStart().StartsWith("#")) {
continue
}
$match = [regex]::Match(
$line,
'Invoke-WPFUIElements\s+-configVariable\s+\$sync\.configs\.([A-Za-z0-9_]+)\s+-targetGridName\s+"([^"]+)"'
)
if (-not $match.Success) {
continue
}
$configName = $match.Groups[1].Value
$targetGridName = $match.Groups[2].Value
if (-not (Test-Path -Path (Join-Path $script:configRoot "$configName.json"))) {
$invalidTargets.Add("$configName references missing config file")
}
if (-not (Test-WinUtilNameInSet -Name $targetGridName -Set $xamlNames)) {
$invalidTargets.Add("$configName target grid '$targetGridName' was not found in XAML")
}
}
if ($invalidTargets.Count -gt 0) {
throw ($invalidTargets -join "`n")
}
}
It "references only XAML, generated, or intentionally dynamic sync members" {
$sourceText = Get-WinUtilSourceText
$xamlNames = @(Get-WinUtilXamlRuntimeNamedControls | ForEach-Object { $_.Name })
$generatedNames = @(Get-WinUtilGeneratedControlNames)
$dynamicStateNames = @(
"Form",
"configs",
"preferences",
"runspace",
"Buttons",
"PSScriptRoot",
"version",
"winutildir",
"logPath",
"transcriptPath",
"ProcessRunning",
"selected",
"selectedAppx",
"selectedApps",
"selectedTweaks",
"selectedToggles",
"selectedFeatures",
"currentTab",
"selectedAppsStackPanel",
"selectedAppsstackPanel",
"selectedAppsPopup",
"appPopup",
"appPopupSelectedApp",
"ItemsControl",
"InstalledPrograms",
"ImportInProgress",
"ScriptsInstallPrograms",
"keys",
"ContainsKey",
"GetEnumerator",
"Remove",
"logorender",
"checkmarkrender",
"warningrender",
"InitializedTabs",
"RenderedAssetCache",
"ToggleStatusCache",
"InstallAppAreaBorder",
"InstallAppAreaScrollViewer",
"InstallAppAreaOverlay",
"InstallAppAreaOverlayText",
"InstallAppRenderQueue",
"InstallAppEntriesRendered",
"ProgressBar",
"progressBarTextBlock",
"Win11ISOImageInfo",
"Win11ISODriveLetter",
"Win11ISOWimPath",
"Win11ISOImagePath",
"Win11ISOModifying",
"Win11ISOWorkDir",
"Win11ISOContentsDir",
"Win11ISOUSBDisks"
)
$allowedNames = @($xamlNames + $generatedNames + $dynamicStateNames) | Sort-Object -Unique
$bracketReferences = @(
[regex]::Matches($sourceText, '\$sync\[\s*["'']([A-Za-z_][A-Za-z0-9_]*)["'']\s*\]') |
ForEach-Object { $_.Groups[1].Value }
)
$dotReferences = @(
[regex]::Matches($sourceText, '\$sync\.([A-Za-z_][A-Za-z0-9_]*)') |
ForEach-Object { $_.Groups[1].Value }
)
$literalReferences = @($bracketReferences + $dotReferences) | Sort-Object -Unique
$invalidReferences = @(
$literalReferences | Where-Object { -not (Test-WinUtilNameInSet -Name $_ -Set $allowedNames) }
)
if ($invalidReferences.Count -gt 0) {
throw ($invalidReferences -join "`n")
}
}
}
Describe "WPF handler wiring" {
It "calls only defined Invoke-WPF functions" {
$sourceText = Get-WinUtilSourceText
$functionNames = @(Get-WinUtilTopLevelFunctionNames)
$featureFunctions = @(
(Get-WinUtilConfigObject -Name "feature").PSObject.Properties |
Where-Object { $_.Value.function -and $_.Value.function -like "Invoke-WPF*" } |
ForEach-Object { $_.Value.function }
)
$invokedFunctions = @(
[regex]::Matches($sourceText, '\b(Invoke-WPF[A-Za-z0-9]+)\b') |
ForEach-Object { $_.Groups[1].Value }
$featureFunctions
) | Sort-Object -Unique
$missingFunctions = @(
$invokedFunctions | Where-Object { -not (Test-WinUtilNameInSet -Name $_ -Set $functionNames) }
)
if ($missingFunctions.Count -gt 0) {
throw ($missingFunctions -join "`n")
}
}
It "routes static WPF buttons through Invoke-WPFButton or explicit handlers" {
$runtimeControls = @(Get-WinUtilXamlRuntimeNamedControls)
$buttonControls = @(
$runtimeControls |
Where-Object { $_.Name -like "WPF*" -and $_.Type -in @("Button", "ToggleButton") }
)
$buttonSwitchNames = @(Get-WinUtilButtonSwitchNames)
$featureNames = @((Get-WinUtilConfigObject -Name "feature").PSObject.Properties.Name)
$mainScript = Get-Content -Path $script:mainScriptPath -Raw
$unhandledButtons = New-Object System.Collections.Generic.List[string]
foreach ($button in $buttonControls) {
$hasSwitchHandler = (Test-WinUtilNameInSet -Name $button.Name -Set $buttonSwitchNames) -or $button.Name -like "WPFTab?BT"
$hasFeatureHandler = Test-WinUtilNameInSet -Name $button.Name -Set $featureNames
$escapedName = [regex]::Escape($button.Name)
$explicitHandlerPattern = '\$sync\s*(?:\[\s*["'']' + $escapedName + '["'']\s*\]|\.' + $escapedName + ')\.Add_Click'
$hasExplicitHandler = $mainScript -imatch $explicitHandlerPattern
if (-not ($hasSwitchHandler -or $hasFeatureHandler -or $hasExplicitHandler)) {
$unhandledButtons.Add($button.Name)
}
}
if ($unhandledButtons.Count -gt 0) {
throw ($unhandledButtons -join "`n")
}
}
It "invokes existing WPF button names" {
$sourceText = Get-WinUtilSourceText
$xamlNames = @(Get-WinUtilXamlRuntimeNamedControls | ForEach-Object { $_.Name })
$generatedNames = @(Get-WinUtilGeneratedControlNames)
$validButtonNames = @($xamlNames + $generatedNames) | Sort-Object -Unique
$buttonNames = @(
[regex]::Matches($sourceText, 'Invoke-WPFButton\s+["'']([^"'']+)["'']') |
ForEach-Object { $_.Groups[1].Value }
) | Sort-Object -Unique
$missingButtons = @(
$buttonNames | Where-Object { -not (Test-WinUtilNameInSet -Name $_ -Set $validButtonNames) }
)
if ($missingButtons.Count -gt 0) {
throw ($missingButtons -join "`n")
}
}
}