mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-07-16 00:40:39 +00:00
* 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
411 lines
15 KiB
PowerShell
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")
|
|
}
|
|
}
|
|
}
|