mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-09 17:41:14 +10:00
Expand Pester coverage across install, tweaks, and UI workflows (#4792)
* Harden Pester CI and discard runspace return values * Add prioritized test backlog * Add WinUtil action logging * Add config integrity tests * Add XAML control wiring tests * Add registry and service helper tests * Add package manager tests * Add runspace behavior tests * Add tweak orchestration tests * Add install workflow tests * Add update profile tests * Add AppX removal tests * Log package installer output * Add UI state helper tests * Pin Pester test runner version * Expand Win11 Creator tests * Add preferences and theme tests * Add search filter tests * Add compile contract tests * Use single WinUtil session log * Remove installer output logging helper * Handle locked WinUtil transcript log * Avoid appending to active transcript log * Log install uninstall package identities
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
#===========================================================================
|
||||
# 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",
|
||||
"logorender",
|
||||
"checkmarkrender",
|
||||
"warningrender",
|
||||
"InstallAppAreaBorder",
|
||||
"InstallAppAreaScrollViewer",
|
||||
"InstallAppAreaOverlay",
|
||||
"InstallAppAreaOverlayText",
|
||||
"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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user