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:
Chris Titus
2026-07-01 21:49:15 -05:00
committed by GitHub
parent c3b4f85fce
commit 2cb20893fc
44 changed files with 4586 additions and 36 deletions
+259
View File
@@ -0,0 +1,259 @@
#===========================================================================
# Tests - AppX Removal
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Remove-WinUtilAPPX.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxRemoval.ps1")
function Write-WinUtilLog {
param($Message, $Level, $Component)
}
function Show-WinUtilMessage {
param($Message, $Title, $Button, $Icon)
}
function Invoke-WPFRunspace {
param($ArgumentList, $ParameterList, [scriptblock]$ScriptBlock)
}
function Get-AppxPackage {
param($Name, [switch]$AllUsers)
}
function Remove-AppxPackage {
param(
[Parameter(ValueFromPipeline = $true)]
$InputObject,
[switch]$AllUsers
)
process { }
}
function Get-AppxProvisionedPackage {
param([switch]$Online)
}
function Remove-AppxProvisionedPackage {
param(
[Parameter(ValueFromPipeline = $true)]
$InputObject,
[switch]$Online
)
process { }
}
function Get-Package {
param($Name, $ErrorAction)
}
function Uninstall-Package {
param(
[Parameter(ValueFromPipeline = $true)]
$InputObject,
[switch]$Force
)
process { }
}
}
Describe "Remove-WinUtilAPPX" {
BeforeEach {
Mock Write-Host { }
Mock Write-WinUtilLog { }
Mock Get-AppxPackage {
[pscustomobject]@{
Name = $Name
}
}
Mock Remove-AppxPackage { }
Mock Get-AppxProvisionedPackage {
@(
[pscustomobject]@{ DisplayName = "Microsoft.XboxGamingOverlay" }
[pscustomobject]@{ DisplayName = "Microsoft.WindowsCalculator" }
)
}
Mock Remove-AppxProvisionedPackage { }
}
It "removes matching installed and provisioned AppX packages" {
Remove-WinUtilAPPX -Name "Microsoft.Xbox*"
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly -ParameterFilter {
$Name -eq "Microsoft.Xbox*" -and $AllUsers -eq $true
}
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly -ParameterFilter {
$InputObject.Name -eq "Microsoft.Xbox*" -and $AllUsers -eq $true
}
Should -Invoke -CommandName Get-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
$Online -eq $true
}
Should -Invoke -CommandName Remove-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
$InputObject.DisplayName -eq "Microsoft.XboxGamingOverlay" -and $Online -eq $true
}
}
}
Describe "Invoke-WPFAppxRemoval entrypoint" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
ProcessRunning = $false
selectedAppx = [System.Collections.Generic.List[string]]::new()
configs = @{
appxHashtable = @{}
}
})
$script:capturedAppxScriptBlock = $null
$script:capturedAppxParameterList = $null
Mock Show-WinUtilMessage { "OK" }
Mock Invoke-WPFRunspace {
$script:capturedAppxScriptBlock = $ScriptBlock
$script:capturedAppxParameterList = $ParameterList
[pscustomobject]@{ MockHandle = $true }
}
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedAppxScriptBlock -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedAppxParameterList -Scope Script -ErrorAction SilentlyContinue
}
It "prompts and exits when no AppX packages are selected" {
Invoke-WPFAppxRemoval
Should -Invoke -CommandName Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
$Message -eq "No AppX Package selected" -and
$Title -eq "Error" -and
$Button -eq "OK" -and
$Icon -eq "Error"
}
Should -Invoke -CommandName Invoke-WPFRunspace -Times 0 -Exactly
}
It "passes selected AppX keys and app metadata to the removal runspace" {
$script:sync.selectedAppx.Add("WPFAppxExample")
$script:sync.configs.appxHashtable["WPFAppxExample"] = [pscustomobject]@{
Content = "Example App"
PackageId = "Example.Package"
}
Invoke-WPFAppxRemoval
Should -Invoke -CommandName Show-WinUtilMessage -Times 0 -Exactly
Should -Invoke -CommandName Invoke-WPFRunspace -Times 1 -Exactly -ParameterFilter {
$ScriptBlock -is [scriptblock] -and
$ParameterList.Count -eq 2 -and
$ParameterList[0][0] -eq "selected" -and
$ParameterList[0][1][0] -eq "WPFAppxExample" -and
$ParameterList[1][0] -eq "apps" -and
$ParameterList[1][1]["WPFAppxExample"].PackageId -eq "Example.Package"
}
}
}
Describe "Invoke-WPFAppxRemoval runspace body" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
ProcessRunning = $false
selectedAppx = [System.Collections.Generic.List[string]]::new()
configs = @{
appxHashtable = @{}
}
})
$script:capturedAppxScriptBlock = $null
$script:apps = @{
WPFAppxExample = [pscustomobject]@{
Content = "Example App"
PackageId = "Example.Package"
}
WPFAppxMicrosoft_XboxGamingOverlay = [pscustomobject]@{
Content = "Xbox Gaming Overlay"
PackageId = "Microsoft.XboxGamingOverlay"
}
WPFAppxMicrosoft_WindowsNotepad = [pscustomobject]@{
Content = "Notepad"
PackageId = "Microsoft.WindowsNotepad"
}
WPFAppxMSTeams = [pscustomobject]@{
Content = "Microsoft Teams"
PackageId = "MSTeams"
}
}
Mock Invoke-WPFRunspace {
$script:capturedAppxScriptBlock = $ScriptBlock
[pscustomobject]@{ MockHandle = $true }
}
Mock Show-WinUtilMessage { "OK" }
Mock Write-Host { }
Mock Write-WinUtilLog { }
Mock Stop-Process { }
Mock Set-ItemProperty { }
Mock Get-AppxPackage {
[pscustomobject]@{
Name = $Name
}
}
Mock Remove-AppxPackage { }
Mock Get-Package {
[pscustomobject]@{
Name = "Microsoft Teams Meeting Add-in"
}
}
Mock Uninstall-Package { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedAppxScriptBlock -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name apps -Scope Script -ErrorAction SilentlyContinue
}
It "removes selected AppX packages and clears ProcessRunning when finished" {
$selected = @("WPFAppxExample")
$script:sync.selectedAppx.Add("WPFAppxExample")
$script:sync.configs.appxHashtable = $script:apps
Invoke-WPFAppxRemoval
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly -ParameterFilter {
$Name -eq "Example.Package" -and $AllUsers -eq $true
}
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly -ParameterFilter {
$InputObject.Name -eq "Example.Package" -and $AllUsers -eq $true
}
$script:sync.ProcessRunning | Should -BeFalse
}
It "applies special cleanup for Xbox overlay, Notepad, and Teams selections" {
$selected = @(
"WPFAppxMicrosoft_XboxGamingOverlay",
"WPFAppxMicrosoft_WindowsNotepad",
"WPFAppxMSTeams"
)
foreach ($key in $selected) {
$script:sync.selectedAppx.Add($key)
}
$script:sync.configs.appxHashtable = $script:apps
Invoke-WPFAppxRemoval
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
Should -Invoke -CommandName Stop-Process -Times 1 -Exactly -ParameterFilter {
$Name -eq "GameBarFTServer"
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVR" -and
$Name -eq "AppCaptureEnabled" -and
$Value -eq 0
}
Should -Invoke -CommandName Stop-Process -Times 1 -Exactly -ParameterFilter {
$Name -eq "dllhost"
}
Should -Invoke -CommandName Get-Package -Times 1 -Exactly -ParameterFilter {
$Name -eq "Microsoft Teams*"
}
Should -Invoke -CommandName Uninstall-Package -Times 1 -Exactly -ParameterFilter {
$InputObject.Name -eq "Microsoft Teams Meeting Add-in" -and $Force -eq $true
}
Should -Invoke -CommandName Remove-AppxPackage -Times 3 -Exactly
$script:sync.ProcessRunning | Should -BeFalse
}
}
+440
View File
@@ -2,7 +2,12 @@
# Tests - Config Files
#===========================================================================
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$configRoot = Join-Path $PSScriptRoot "..\config"
$functionRoot = Join-Path $repoRoot "functions"
$xamlPath = Join-Path $repoRoot "xaml\inputXML.xaml"
$mainScriptPath = Join-Path $repoRoot "scripts\main.ps1"
$buttonScriptPath = Join-Path $repoRoot "functions\public\Invoke-WPFButton.ps1"
$configCases = @(
Get-ChildItem -Path $configRoot -Filter *.json | ForEach-Object {
@{
@@ -12,6 +17,103 @@ $configCases = @(
}
)
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$script:configRoot = Join-Path $script:repoRoot "config"
$script:functionRoot = Join-Path $script:repoRoot "functions"
$script:xamlPath = Join-Path $script:repoRoot "xaml\inputXML.xaml"
$script:mainScriptPath = Join-Path $script:repoRoot "scripts\main.ps1"
$script:buttonScriptPath = Join-Path $script:repoRoot "functions\public\Invoke-WPFButton.ps1"
function script:Get-WinUtilConfigObject {
param([string]$Name)
Get-Content -Path (Join-Path $script:configRoot "$Name.json") -Raw | ConvertFrom-Json
}
function script:Test-WinUtilHasProperty {
param(
[Parameter(Mandatory)]
[psobject]$Object,
[Parameter(Mandatory)]
[string]$Name
)
return @($Object.PSObject.Properties.Name) -contains $Name
}
function script:Test-WinUtilHasNonEmptyProperty {
param(
[Parameter(Mandatory)]
[psobject]$Object,
[Parameter(Mandatory)]
[string]$Name
)
if (-not (Test-WinUtilHasProperty -Object $Object -Name $Name)) {
return $false
}
$value = $Object.$Name
if ($null -eq $value) {
return $false
}
if ($value -is [string]) {
return -not [string]::IsNullOrWhiteSpace($value)
}
if ($value -is [array]) {
return @($value).Count -gt 0
}
return -not [string]::IsNullOrWhiteSpace([string]$value)
}
function script:Get-WinUtilMissingRequiredFields {
param(
[Parameter(Mandatory)]
[string]$EntryName,
[Parameter(Mandatory)]
[psobject]$Entry,
[Parameter(Mandatory)]
[string[]]$RequiredFields
)
foreach ($field in $RequiredFields) {
if (-not (Test-WinUtilHasNonEmptyProperty -Object $Entry -Name $field)) {
"$EntryName missing $field"
}
}
}
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-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
}
}
Describe "Config files" {
foreach ($configCase in $configCases) {
It "imports $($configCase.Name) with no JSON errors" -TestCases $configCase {
@@ -108,3 +210,341 @@ Describe "Tweaks config" {
}
}
}
Describe "Preset config" {
It "references existing config entries or supported actions" {
$preset = Get-WinUtilConfigObject -Name "preset"
$applications = Get-WinUtilConfigObject -Name "applications"
$tweaks = Get-WinUtilConfigObject -Name "tweaks"
$feature = Get-WinUtilConfigObject -Name "feature"
$appx = Get-WinUtilConfigObject -Name "appx"
$validReferences = @(
$tweaks.PSObject.Properties.Name
$feature.PSObject.Properties.Name
$appx.PSObject.Properties.Name
$applications.PSObject.Properties.Name | ForEach-Object { "WPFInstall$_" }
Get-WinUtilButtonSwitchNames
) | Sort-Object -Unique
$invalidReferences = New-Object System.Collections.Generic.List[string]
foreach ($presetEntry in $preset.PSObject.Properties) {
foreach ($reference in @($presetEntry.Value)) {
if ($validReferences -notcontains $reference) {
$invalidReferences.Add("$($presetEntry.Name) references missing item $reference")
}
}
}
if ($invalidReferences.Count -gt 0) {
throw ($invalidReferences -join "`n")
}
}
}
Describe "App navigation config" {
It "is wired to an existing XAML target grid" {
$mainScript = Get-Content -Path $script:mainScriptPath -Raw
$targetGridMatch = [regex]::Match(
$mainScript,
'Invoke-WPFUIElements\s+-configVariable\s+\$sync\.configs\.appnavigation\s+-targetGridName\s+"([^"]+)"'
)
if (-not $targetGridMatch.Success) {
throw "scripts/main.ps1 does not wire appnavigation through Invoke-WPFUIElements."
}
$xamlText = Get-Content -Path $script:xamlPath -Raw
$targetGridName = $targetGridMatch.Groups[1].Value
if ($xamlText -notmatch "Name=`"$([regex]::Escape($targetGridName))`"") {
throw "appnavigation target grid '$targetGridName' was not found in xaml/inputXML.xaml."
}
}
It "contains renderable entries with valid button and radio groups" {
$appnavigation = Get-WinUtilConfigObject -Name "appnavigation"
$feature = Get-WinUtilConfigObject -Name "feature"
$requiredFields = @("Content", "Category", "Type", "Order", "Description")
$allowedTypes = @("Button", "RadioButton", "Note")
$supportedButtons = @(
Get-WinUtilButtonSwitchNames
$feature.PSObject.Properties.Name
) | Sort-Object -Unique
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($entry in $appnavigation.PSObject.Properties) {
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName $entry.Name -Entry $entry.Value -RequiredFields $requiredFields)) {
$invalidEntries.Add($missingField)
}
if ($allowedTypes -notcontains $entry.Value.Type) {
$invalidEntries.Add("$($entry.Name) has unsupported Type '$($entry.Value.Type)'")
}
if ($entry.Value.Type -eq "Button" -and $supportedButtons -notcontains $entry.Name) {
$invalidEntries.Add("$($entry.Name) is not handled by Invoke-WPFButton or feature config")
}
if ($entry.Value.Type -eq "RadioButton") {
if (-not (Test-WinUtilHasNonEmptyProperty -Object $entry.Value -Name "GroupName")) {
$invalidEntries.Add("$($entry.Name) missing GroupName")
}
if (-not (Test-WinUtilHasProperty -Object $entry.Value -Name "Checked")) {
$invalidEntries.Add("$($entry.Name) missing Checked")
}
}
}
$radioButtons = @($appnavigation.PSObject.Properties | Where-Object { $_.Value.Type -eq "RadioButton" })
foreach ($group in ($radioButtons | Group-Object -Property { $_.Value.GroupName })) {
if ([string]::IsNullOrWhiteSpace($group.Name)) {
$invalidEntries.Add("RadioButton group name is blank")
continue
}
$checkedCount = @($group.Group | Where-Object { $_.Value.Checked -eq $true }).Count
if ($checkedCount -ne 1) {
$invalidEntries.Add("RadioButton group '$($group.Name)' has $checkedCount checked entries")
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
}
Describe "UI-rendered config entries" {
It "contains required AppX fields" {
$appx = Get-WinUtilConfigObject -Name "appx"
$requiredFields = @("Category", "Content", "Description", "Panel", "PackageId")
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($entry in $appx.PSObject.Properties) {
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName $entry.Name -Entry $entry.Value -RequiredFields $requiredFields)) {
$invalidEntries.Add($missingField)
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
It "contains required DNS fields with parseable IP addresses" {
$dns = Get-WinUtilConfigObject -Name "dns"
$requiredFields = @("Primary", "Secondary", "Primary6", "Secondary6")
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($entry in $dns.PSObject.Properties) {
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName $entry.Name -Entry $entry.Value -RequiredFields $requiredFields)) {
$invalidEntries.Add($missingField)
}
foreach ($field in $requiredFields) {
if (-not (Test-WinUtilHasNonEmptyProperty -Object $entry.Value -Name $field)) {
continue
}
try {
[System.Net.IPAddress]::Parse([string]$entry.Value.$field) | Out-Null
} catch {
$invalidEntries.Add("$($entry.Name) $field is not a parseable IP address")
}
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
It "contains required feature fields and valid configured functions" {
$feature = Get-WinUtilConfigObject -Name "feature"
$functionNames = Get-WinUtilTopLevelFunctionNames
$requiredFields = @("Content", "category", "panel", "link")
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($entry in $feature.PSObject.Properties) {
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName $entry.Name -Entry $entry.Value -RequiredFields $requiredFields)) {
$invalidEntries.Add($missingField)
}
if ($entry.Value.Type -and $entry.Value.Type -ne "Button") {
$invalidEntries.Add("$($entry.Name) has unsupported Type '$($entry.Value.Type)'")
}
if ($entry.Value.Type -eq "Button") {
if (-not $entry.Value.function -and -not $entry.Value.InvokeScript) {
$invalidEntries.Add("$($entry.Name) button missing function or InvokeScript")
}
} else {
if (-not (Test-WinUtilHasNonEmptyProperty -Object $entry.Value -Name "Description")) {
$invalidEntries.Add("$($entry.Name) missing Description")
}
if (-not $entry.Value.feature -and -not $entry.Value.InvokeScript) {
$invalidEntries.Add("$($entry.Name) missing feature or InvokeScript action")
}
}
if ($entry.Value.function -and $functionNames -notcontains $entry.Value.function) {
$invalidEntries.Add("$($entry.Name) references missing function $($entry.Value.function)")
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
It "contains required tweak fields and valid action metadata" {
$tweaks = Get-WinUtilConfigObject -Name "tweaks"
$requiredFields = @("Content", "category", "panel", "link")
$allowedTypes = @("Button", "Combobox", "Toggle", "ToggleButton")
$supportedButtons = Get-WinUtilButtonSwitchNames
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($entry in $tweaks.PSObject.Properties) {
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName $entry.Name -Entry $entry.Value -RequiredFields $requiredFields)) {
$invalidEntries.Add($missingField)
}
if ($entry.Value.Type -and $allowedTypes -notcontains $entry.Value.Type) {
$invalidEntries.Add("$($entry.Name) has unsupported Type '$($entry.Value.Type)'")
}
if ($entry.Value.Type -eq "Button") {
if ($supportedButtons -notcontains $entry.Name) {
$invalidEntries.Add("$($entry.Name) is not handled by Invoke-WPFButton")
}
} elseif ($entry.Value.Type -eq "Combobox") {
if (-not (Test-WinUtilHasNonEmptyProperty -Object $entry.Value -Name "ComboItems")) {
$invalidEntries.Add("$($entry.Name) combobox missing ComboItems")
}
} else {
if (-not (Test-WinUtilHasNonEmptyProperty -Object $entry.Value -Name "Description")) {
$invalidEntries.Add("$($entry.Name) missing Description")
}
if (-not $entry.Value.registry -and -not $entry.Value.service -and -not $entry.Value.InvokeScript -and -not $entry.Value.appx) {
$invalidEntries.Add("$($entry.Name) missing registry, service, InvokeScript, or appx action")
}
}
foreach ($registryEntry in @($entry.Value.registry)) {
if ($null -eq $registryEntry) { continue }
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName "$($entry.Name),registry" -Entry $registryEntry -RequiredFields @("Path", "Name", "Type", "Value", "OriginalValue"))) {
$invalidEntries.Add($missingField)
}
}
foreach ($serviceEntry in @($entry.Value.service)) {
if ($null -eq $serviceEntry) { continue }
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName "$($entry.Name),service" -Entry $serviceEntry -RequiredFields @("Name", "StartupType", "OriginalType"))) {
$invalidEntries.Add($missingField)
}
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
It "defines theme resources required by XAML rendering" {
$themes = Get-WinUtilConfigObject -Name "themes"
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($themeName in @("shared", "Light", "Dark")) {
if (-not (Test-WinUtilHasProperty -Object $themes -Name $themeName)) {
$invalidEntries.Add("themes.json missing $themeName")
continue
}
foreach ($property in $themes.$themeName.PSObject.Properties) {
if ([string]::IsNullOrWhiteSpace([string]$property.Value)) {
$invalidEntries.Add("$themeName.$($property.Name) is blank")
}
}
}
$lightKeys = @($themes.Light.PSObject.Properties.Name)
$darkKeys = @($themes.Dark.PSObject.Properties.Name)
foreach ($key in $lightKeys) {
if ($darkKeys -notcontains $key) {
$invalidEntries.Add("Dark theme missing $key")
}
}
foreach ($key in $darkKeys) {
if ($lightKeys -notcontains $key) {
$invalidEntries.Add("Light theme missing $key")
}
}
$xamlText = Get-Content -Path $script:xamlPath -Raw
$dynamicResourceNames = @(
[regex]::Matches($xamlText, '\{DynamicResource\s+([^\}\s]+)') |
ForEach-Object { $_.Groups[1].Value }
) | Sort-Object -Unique
$xamlDefinedResourceNames = @(
[regex]::Matches($xamlText, 'x:Key="([^"]+)"') |
ForEach-Object { $_.Groups[1].Value }
) | Sort-Object -Unique
$themeResourceNames = @(
$themes.shared.PSObject.Properties.Name
$themes.Light.PSObject.Properties.Name
$themes.Dark.PSObject.Properties.Name
"CBorderColor"
"CButtonBackgroundMouseoverColor"
) | Sort-Object -Unique
foreach ($resourceName in $dynamicResourceNames) {
if ($xamlDefinedResourceNames -notcontains $resourceName -and $themeResourceNames -notcontains $resourceName) {
$invalidEntries.Add("XAML DynamicResource '$resourceName' is not defined in themes.json or XAML resources")
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
}
Describe "Embedded config scripts" {
It "parse as PowerShell scriptblocks" {
$invalidScripts = New-Object System.Collections.Generic.List[string]
foreach ($configFile in (Get-ChildItem -Path $script:configRoot -Filter *.json)) {
$config = Get-Content -Path $configFile.FullName -Raw | ConvertFrom-Json
foreach ($entry in $config.PSObject.Properties) {
foreach ($field in @("InvokeScript", "UndoScript")) {
if (-not (Test-WinUtilHasProperty -Object $entry.Value -Name $field)) {
continue
}
$index = 0
foreach ($scriptText in @($entry.Value.$field)) {
$index++
if ([string]::IsNullOrWhiteSpace([string]$scriptText)) {
continue
}
try {
[scriptblock]::Create([string]$scriptText) | Out-Null
} catch {
$invalidScripts.Add("$($configFile.Name):$($entry.Name).$field[$index] $($psitem.Exception.Message)")
}
}
}
}
}
if ($invalidScripts.Count -gt 0) {
throw ($invalidScripts -join "`n")
}
}
}
+414
View File
@@ -0,0 +1,414 @@
#===========================================================================
# Tests - Install and Uninstall Workflows
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
if (-not ("PackageManagers" -as [type])) {
Add-Type @"
public enum PackageManagers
{
Winget,
Choco
}
"@
}
. (Join-Path $script:repoRoot "functions\private\Get-WinUtilPackageLogSummary.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFInstall.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFUnInstall.ps1")
function Show-WinUtilMessage {
param($Message, $Title, $Button, $Icon)
}
function Invoke-WPFRunspace {
param($ArgumentList, $ParameterList, [scriptblock]$ScriptBlock)
}
function Get-WinUtilSelectedPackages {
param($PackageList, [PackageManagers]$Preference)
}
function Show-WPFInstallAppBusy {
param($text)
}
function Hide-WPFInstallAppBusy { }
function Install-WinUtilWinget { }
function Install-WinUtilChoco { }
function Install-WinUtilProgramWinget {
param($Action, $Programs)
}
function Install-WinUtilProgramChoco {
param($Action, $Programs)
}
function Invoke-WPFUIThread {
param([scriptblock]$ScriptBlock)
}
function Write-WinUtilLog {
param($Message, $Level, $Component)
}
function script:New-WinUtilPackage {
param(
[string]$Name = "Git",
[string]$Winget = "Git.Git",
[string]$Choco = "git"
)
[pscustomobject]@{
Name = $Name
Description = "$Name package"
winget = $Winget
choco = $Choco
}
}
function script:New-WinUtilInstallTestContext {
param(
[bool]$ProcessRunning = $false,
[object[]]$Packages = @()
)
$applications = @{}
$selectedApps = [System.Collections.Generic.List[string]]::new()
for ($i = 0; $i -lt $Packages.Count; $i++) {
$key = "WPFInstallTest$i"
$applications[$key] = $Packages[$i]
$selectedApps.Add($key)
}
$script:AppTitle = "Winutil"
$script:sync = [Hashtable]::Synchronized(@{
ProcessRunning = $ProcessRunning
selectedApps = $selectedApps
preferences = [pscustomobject]@{
packagemanager = [PackageManagers]::Winget
}
configs = @{
applicationsHashtable = $applications
}
})
}
function script:New-WinUtilPackageSplit {
param(
[string[]]$Winget = @(),
[string[]]$Choco = @()
)
$packages = @{}
$packages[[PackageManagers]::Winget] = [System.Collections.Generic.List[string]]::new()
$packages[[PackageManagers]::Choco] = [System.Collections.Generic.List[string]]::new()
foreach ($package in $Winget) {
$null = $packages[[PackageManagers]::Winget].Add($package)
}
foreach ($package in $Choco) {
$null = $packages[[PackageManagers]::Choco].Add($package)
}
$packages
}
}
Describe "Invoke-WPFInstall entrypoint" {
BeforeEach {
$script:package = New-WinUtilPackage
New-WinUtilInstallTestContext -Packages @($script:package)
$script:capturedInstallScriptBlock = $null
$script:capturedInstallParameterList = $null
Mock Show-WinUtilMessage { "OK" }
Mock Invoke-WPFRunspace {
$script:capturedInstallScriptBlock = $ScriptBlock
$script:capturedInstallParameterList = $ParameterList
[pscustomobject]@{ MockHandle = $true }
}
Mock Write-WinUtilLog { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name AppTitle -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedInstallScriptBlock -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedInstallParameterList -Scope Script -ErrorAction SilentlyContinue
}
It "queues selected packages with the configured package manager preference" {
Invoke-WPFInstall
Should -Invoke -CommandName Invoke-WPFRunspace -Times 1 -Exactly -ParameterFilter {
$ScriptBlock -is [scriptblock] -and
$ParameterList.Count -eq 2 -and
$ParameterList[0][0] -eq "PackagesToInstall" -and
@($ParameterList[0][1]).Count -eq 1 -and
@($ParameterList[0][1])[0].winget -eq "Git.Git" -and
$ParameterList[1][0] -eq "ManagerPreference" -and
$ParameterList[1][1] -eq [PackageManagers]::Winget
}
Should -Invoke -CommandName Show-WinUtilMessage -Times 0 -Exactly
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Component -eq "Install" -and
$Message -eq "Install selected package(s): Git (winget: Git.Git)"
}
}
It "prompts and exits when no packages are selected" {
New-WinUtilInstallTestContext
Invoke-WPFInstall
Should -Invoke -CommandName Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
$Message -eq "Please select the program(s) to install or upgrade." -and
$Title -eq "Winutil" -and
$Button -eq "OK" -and
$Icon -eq "Warning"
}
Should -Invoke -CommandName Invoke-WPFRunspace -Times 0 -Exactly
}
It "prompts and exits when another install process is running" {
New-WinUtilInstallTestContext -ProcessRunning $true -Packages @($script:package)
Invoke-WPFInstall
Should -Invoke -CommandName Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
$Message -eq "[Invoke-WPFInstall] An Install process is currently running." -and
$Title -eq "Winutil" -and
$Button -eq "OK" -and
$Icon -eq "Warning"
}
Should -Invoke -CommandName Invoke-WPFRunspace -Times 0 -Exactly
}
}
Describe "Invoke-WPFInstall runspace body" {
BeforeEach {
$script:package = New-WinUtilPackage
New-WinUtilInstallTestContext -Packages @($script:package)
$script:capturedInstallScriptBlock = $null
Mock Show-WinUtilMessage { "OK" }
Mock Invoke-WPFRunspace {
$script:capturedInstallScriptBlock = $ScriptBlock
[pscustomobject]@{ MockHandle = $true }
}
Mock Get-WinUtilSelectedPackages {
New-WinUtilPackageSplit -Winget @("Git.Git") -Choco @("vlc")
}
Mock Show-WPFInstallAppBusy { }
Mock Hide-WPFInstallAppBusy { }
Mock Install-WinUtilWinget { }
Mock Install-WinUtilChoco { }
Mock Install-WinUtilProgramWinget { }
Mock Install-WinUtilProgramChoco { }
Mock Invoke-WPFUIThread { }
Mock Write-WinUtilLog { }
Mock Write-Host { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name AppTitle -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedInstallScriptBlock -Scope Script -ErrorAction SilentlyContinue
}
It "installs split winget and choco packages and cleans up on success" {
Invoke-WPFInstall
& $script:capturedInstallScriptBlock -PackagesToInstall @($script:package) -ManagerPreference ([PackageManagers]::Winget)
Should -Invoke -CommandName Get-WinUtilSelectedPackages -Times 1 -Exactly -ParameterFilter {
@($PackageList).Count -eq 1 -and $Preference -eq [PackageManagers]::Winget
}
Should -Invoke -CommandName Show-WPFInstallAppBusy -Times 1 -Exactly -ParameterFilter {
$text -eq "Installing apps..."
}
Should -Invoke -CommandName Install-WinUtilWinget -Times 1 -Exactly
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 1 -Exactly -ParameterFilter {
$Action -eq "Install" -and @($Programs)[0] -eq "Git.Git"
}
Should -Invoke -CommandName Install-WinUtilChoco -Times 1 -Exactly
Should -Invoke -CommandName Install-WinUtilProgramChoco -Times 1 -Exactly -ParameterFilter {
$Action -eq "Install" -and @($Programs)[0] -eq "vlc"
}
Should -Invoke -CommandName Hide-WPFInstallAppBusy -Times 1 -Exactly
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"*'
}
$script:sync.ProcessRunning | Should -BeFalse
}
It "hides the busy overlay, sets taskbar error state, and clears ProcessRunning on failure" {
Mock Install-WinUtilProgramWinget { throw "winget failed" }
Invoke-WPFInstall
& $script:capturedInstallScriptBlock -PackagesToInstall @($script:package) -ManagerPreference ([PackageManagers]::Winget)
Should -Invoke -CommandName Hide-WPFInstallAppBusy -Times 1 -Exactly
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "Error" -overlay "warning"*'
}
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and $Component -eq "Install" -and $Message -like "Install workflow failed:*"
}
$script:sync.ProcessRunning | Should -BeFalse
}
}
Describe "Invoke-WPFUnInstall entrypoint" {
BeforeEach {
$script:package = New-WinUtilPackage
New-WinUtilInstallTestContext -Packages @($script:package)
$script:capturedUninstallScriptBlock = $null
$script:capturedUninstallParameterList = $null
Mock Show-WinUtilMessage { "Yes" }
Mock Invoke-WPFRunspace {
$script:capturedUninstallScriptBlock = $ScriptBlock
$script:capturedUninstallParameterList = $ParameterList
[pscustomobject]@{ MockHandle = $true }
}
Mock Write-WinUtilLog { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name AppTitle -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedUninstallScriptBlock -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedUninstallParameterList -Scope Script -ErrorAction SilentlyContinue
}
It "confirms and queues selected packages with the configured package manager preference" {
Invoke-WPFUnInstall -PackagesToUninstall @($script:package)
Should -Invoke -CommandName Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
$Message -like "*This will uninstall the following applications:*" -and
$Message -like "*Git*" -and
$Title -eq "Are you sure?" -and
"$Button" -eq "YesNo" -and
"$Icon" -eq "Information"
}
Should -Invoke -CommandName Invoke-WPFRunspace -Times 1 -Exactly -ParameterFilter {
$ScriptBlock -is [scriptblock] -and
$ParameterList.Count -eq 2 -and
$ParameterList[0][0] -eq "PackagesToUninstall" -and
@($ParameterList[0][1]).Count -eq 1 -and
@($ParameterList[0][1])[0].winget -eq "Git.Git" -and
$ParameterList[1][0] -eq "ManagerPreference" -and
$ParameterList[1][1] -eq [PackageManagers]::Winget
}
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Component -eq "Uninstall" -and
$Message -eq "Uninstall selected package(s): Git (winget: Git.Git)"
}
}
It "prompts and exits when no packages are selected" {
Invoke-WPFUnInstall -PackagesToUninstall @()
Should -Invoke -CommandName Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
$Message -eq "Please select the program(s) to uninstall" -and
$Title -eq "Winutil" -and
$Button -eq "OK" -and
$Icon -eq "Warning"
}
Should -Invoke -CommandName Invoke-WPFRunspace -Times 0 -Exactly
}
It "prompts and exits when another install process is running" {
$script:sync.ProcessRunning = $true
Invoke-WPFUnInstall -PackagesToUninstall @($script:package)
Should -Invoke -CommandName Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
$Message -eq "[Invoke-WPFUnInstall] Install process is currently running" -and
$Title -eq "Winutil" -and
$Button -eq "OK" -and
$Icon -eq "Warning"
}
Should -Invoke -CommandName Invoke-WPFRunspace -Times 0 -Exactly
}
It "exits without queueing uninstall when confirmation is declined" {
Mock Show-WinUtilMessage { "No" } -ParameterFilter { $Title -eq "Are you sure?" }
Invoke-WPFUnInstall -PackagesToUninstall @($script:package)
Should -Invoke -CommandName Invoke-WPFRunspace -Times 0 -Exactly
}
}
Describe "Invoke-WPFUnInstall runspace body" {
BeforeEach {
$script:package = New-WinUtilPackage
New-WinUtilInstallTestContext -Packages @($script:package)
$script:capturedUninstallScriptBlock = $null
Mock Show-WinUtilMessage { "Yes" }
Mock Invoke-WPFRunspace {
$script:capturedUninstallScriptBlock = $ScriptBlock
[pscustomobject]@{ MockHandle = $true }
}
Mock Get-WinUtilSelectedPackages {
New-WinUtilPackageSplit -Winget @("Git.Git") -Choco @("vlc")
}
Mock Show-WPFInstallAppBusy { }
Mock Hide-WPFInstallAppBusy { }
Mock Install-WinUtilProgramWinget { }
Mock Install-WinUtilProgramChoco { }
Mock Invoke-WPFUIThread { }
Mock Write-WinUtilLog { }
Mock Write-Host { }
Mock New-Item { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name AppTitle -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedUninstallScriptBlock -Scope Script -ErrorAction SilentlyContinue
}
It "uninstalls split winget and choco packages and cleans up on success" {
Invoke-WPFUnInstall -PackagesToUninstall @($script:package)
& $script:capturedUninstallScriptBlock -PackagesToUninstall @($script:package) -ManagerPreference ([PackageManagers]::Winget)
Should -Invoke -CommandName Get-WinUtilSelectedPackages -Times 1 -Exactly -ParameterFilter {
@($PackageList).Count -eq 1 -and $Preference -eq [PackageManagers]::Winget
}
Should -Invoke -CommandName Show-WPFInstallAppBusy -Times 1 -Exactly -ParameterFilter {
$text -eq "Uninstalling apps..."
}
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 1 -Exactly -ParameterFilter {
$Action -eq "Uninstall" -and @($Programs)[0] -eq "Git.Git"
}
Should -Invoke -CommandName Install-WinUtilProgramChoco -Times 1 -Exactly -ParameterFilter {
$Action -eq "Uninstall" -and @($Programs)[0] -eq "vlc"
}
Should -Invoke -CommandName Hide-WPFInstallAppBusy -Times 1 -Exactly
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"*'
}
$script:sync.ProcessRunning | Should -BeFalse
}
It "hides the busy overlay, sets taskbar error state, and clears ProcessRunning on failure" {
Mock Install-WinUtilProgramWinget { throw "winget failed" }
Invoke-WPFUnInstall -PackagesToUninstall @($script:package)
& $script:capturedUninstallScriptBlock -PackagesToUninstall @($script:package) -ManagerPreference ([PackageManagers]::Winget)
Should -Invoke -CommandName Hide-WPFInstallAppBusy -Times 1 -Exactly
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "Error" -overlay "warning"*'
}
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and $Component -eq "Uninstall" -and $Message -like "Uninstall workflow failed:*"
}
$script:sync.ProcessRunning | Should -BeFalse
}
}
+107
View File
@@ -0,0 +1,107 @@
#===========================================================================
# Tests - WinUtil Logging
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Write-WinUtilLog.ps1")
}
Describe "Write-WinUtilLog" {
BeforeEach {
$script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "winutil-logging-$([guid]::NewGuid())"
New-Item -Path $script:testRoot -ItemType Directory -Force | Out-Null
Remove-Variable -Name WinUtilLogPath -Scope Script -ErrorAction SilentlyContinue
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name WinUtilLogPath -Scope Script -ErrorAction SilentlyContinue
Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
}
It "writes to the active timestamped session log under logs" {
$logPath = Join-Path $script:testRoot "logs\winutil_2026-07-01_12-00-00.log"
$script:sync = [hashtable]::Synchronized(@{
winutildir = $script:testRoot
logPath = $logPath
})
Write-WinUtilLog -Component "Test" -Message "same session log"
Test-Path -Path $logPath | Should -BeTrue
Test-Path -Path (Join-Path $script:testRoot "winutil.log") | Should -BeFalse
Get-Content -Path $logPath -Raw | Should -Match "\[INFO\] \[Test\] same session log"
}
It "uses the transcript stream when logPath is not set" {
$transcriptPath = Join-Path $script:testRoot "logs\winutil_2026-07-01_12-00-00.log"
$script:sync = [hashtable]::Synchronized(@{
winutildir = $script:testRoot
transcriptPath = $transcriptPath
})
Mock Add-Content { }
Mock Write-Host { }
Write-WinUtilLog -Component "Test" -Message "transcript fallback"
Should -Invoke -CommandName Add-Content -Times 0 -Exactly
Should -Invoke -CommandName Write-Host -Times 1 -Exactly -ParameterFilter {
$Object -match "\[INFO\] \[Test\] transcript fallback"
}
Test-Path -Path (Join-Path $script:testRoot "winutil.log") | Should -BeFalse
}
It "creates one fallback log under logs when only winutildir is available" {
$script:sync = [hashtable]::Synchronized(@{
winutildir = $script:testRoot
})
Write-WinUtilLog -Component "Test" -Message "first fallback entry"
Write-WinUtilLog -Component "Test" -Message "second fallback entry"
$logFiles = @(Get-ChildItem -Path (Join-Path $script:testRoot "logs") -Filter "winutil_*.log")
$logFiles.Count | Should -Be 1
Test-Path -Path (Join-Path $script:testRoot "winutil.log") | Should -BeFalse
$content = Get-Content -Path $logFiles[0].FullName -Raw
$content | Should -Match "first fallback entry"
$content | Should -Match "second fallback entry"
}
It "does not append directly when the active log file is the transcript" {
$logPath = Join-Path $script:testRoot "logs\winutil_2026-07-01_12-00-00.log"
$script:sync = [hashtable]::Synchronized(@{
winutildir = $script:testRoot
logPath = $logPath
transcriptPath = $logPath
})
Mock Add-Content { throw [System.IO.IOException]::new("locked by transcript") } -ParameterFilter {
$Path -eq $logPath -and $ErrorAction -eq "Stop"
}
Mock Write-Host { }
Mock Write-Warning { }
Write-WinUtilLog -Component "Test" -Message "transcript stream fallback"
Should -Invoke -CommandName Add-Content -Times 0 -Exactly
Should -Invoke -CommandName Write-Host -Times 1 -Exactly -ParameterFilter {
$Object -match "\[INFO\] \[Test\] transcript stream fallback"
}
Should -Invoke -CommandName Write-Warning -Times 0 -Exactly
}
}
Describe "WinUtil startup logging path" {
It "uses one timestamped log file under the logs directory" {
$startScript = Get-Content -Path (Join-Path $script:repoRoot "scripts\start.ps1") -Raw
$startScript | Should -Match '\$sync\.logPath = "\$logdir\\winutil_\$dateTime\.log"'
$startScript | Should -Match '\$sync\.transcriptPath = \$sync\.logPath'
$startScript | Should -Match 'Start-Transcript -Path \$sync\.logPath'
$startScript | Should -Not -Match '\$sync\.logPath = "\$winutildir\\winutil\.log"'
}
}
+181
View File
@@ -0,0 +1,181 @@
#===========================================================================
# Tests - Package Selection and Package Managers
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
if (-not ("PackageManagers" -as [type])) {
Add-Type @"
public enum PackageManagers
{
Winget,
Choco
}
"@
}
. (Join-Path $script:repoRoot "functions\private\Get-WinUtilSelectedPackages.ps1")
. (Join-Path $script:repoRoot "functions\private\Test-WinUtilPackageManager.ps1")
. (Join-Path $script:repoRoot "functions\private\Install-WinUtilProgramWinget.ps1")
. (Join-Path $script:repoRoot "functions\private\Install-WinUtilProgramChoco.ps1")
function Invoke-WPFUIThread { }
function Write-WinUtilLog { }
}
Describe "Get-WinUtilSelectedPackages" {
BeforeEach {
Mock Invoke-WPFUIThread { }
}
It "uses winget IDs when winget is preferred" {
$packages = @(
[pscustomobject]@{ winget = "Git.Git"; choco = "git" }
[pscustomobject]@{ winget = "VideoLAN.VLC"; choco = "vlc" }
)
$result = Get-WinUtilSelectedPackages -PackageList $packages -Preference ([PackageManagers]::Winget)
(@($result[[PackageManagers]::Winget]) -join "|") | Should -Be "Git.Git|VideoLAN.VLC"
@($result[[PackageManagers]::Choco]).Count | Should -Be 0
}
It "uses choco IDs and falls back to winget for na or missing choco IDs" {
$packages = @(
[pscustomobject]@{ winget = "Git.Git"; choco = "git" }
[pscustomobject]@{ winget = "VideoLAN.VLC"; choco = "na" }
[pscustomobject]@{ winget = "Mozilla.Firefox" }
)
$result = Get-WinUtilSelectedPackages -PackageList $packages -Preference ([PackageManagers]::Choco)
(@($result[[PackageManagers]::Choco]) -join "|") | Should -Be "git"
(@($result[[PackageManagers]::Winget]) -join "|") | Should -Be "VideoLAN.VLC|Mozilla.Firefox"
}
It "skips blank, na, and missing package IDs" {
$packages = @(
[pscustomobject]@{ winget = ""; choco = "" }
[pscustomobject]@{ winget = "na"; choco = "na" }
[pscustomobject]@{ choco = "only-choco" }
[pscustomobject]@{ winget = " " }
)
$result = Get-WinUtilSelectedPackages -PackageList $packages -Preference ([PackageManagers]::Winget)
@($result[[PackageManagers]::Winget]).Count | Should -Be 0
@($result[[PackageManagers]::Choco]).Count | Should -Be 0
}
It "deduplicates package IDs" {
$packages = @(
[pscustomobject]@{ winget = "Git.Git"; choco = "git" }
[pscustomobject]@{ winget = "Git.Git"; choco = "git" }
[pscustomobject]@{ winget = "VideoLAN.VLC"; choco = "vlc" }
)
$result = Get-WinUtilSelectedPackages -PackageList $packages -Preference ([PackageManagers]::Choco)
(@($result[[PackageManagers]::Choco]) -join "|") | Should -Be "git|vlc"
@($result[[PackageManagers]::Winget]).Count | Should -Be 0
}
It "returns empty package lists for an empty selection" {
$result = Get-WinUtilSelectedPackages -PackageList @() -Preference ([PackageManagers]::Winget)
@($result[[PackageManagers]::Winget]).Count | Should -Be 0
@($result[[PackageManagers]::Choco]).Count | Should -Be 0
}
}
Describe "Test-WinUtilPackageManager" {
BeforeEach {
Mock Write-Host { }
}
It "reports winget installed when the command exists" {
Mock Get-Command {
[pscustomobject]@{ Name = "winget" }
} -ParameterFilter { $Name -eq "winget" -and $ErrorAction -eq "SilentlyContinue" }
Test-WinUtilPackageManager -winget | Should -Be "installed"
Should -Invoke -CommandName Get-Command -Times 1 -Exactly -ParameterFilter {
$Name -eq "winget" -and $ErrorAction -eq "SilentlyContinue"
}
}
It "reports choco not installed when the command is missing" {
Mock Get-Command {
$null
} -ParameterFilter { $Name -eq "choco" -and $ErrorAction -eq "SilentlyContinue" }
Test-WinUtilPackageManager -choco | Should -Be "not-installed"
Should -Invoke -CommandName Get-Command -Times 1 -Exactly -ParameterFilter {
$Name -eq "choco" -and $ErrorAction -eq "SilentlyContinue"
}
}
}
Describe "Install-WinUtilProgramWinget" {
BeforeEach {
Mock Write-WinUtilLog { }
Mock Start-Process { [pscustomobject]@{ ExitCode = 0 } }
}
It "starts winget with install arguments" {
Install-WinUtilProgramWinget -Action Install -Programs @("Git.Git")
Should -Invoke -CommandName Start-Process -Times 1 -Exactly -ParameterFilter {
$FilePath -eq "winget" -and
(@($ArgumentList) -join "|") -eq "install|--id|Git.Git|--accept-package-agreements|--accept-source-agreements|--source|winget|--silent" -and
$NoNewWindow -eq $true -and
$Wait -eq $true -and
$PassThru -eq $true
}
}
It "starts winget with uninstall arguments and msstore source when requested" {
Install-WinUtilProgramWinget -Action Uninstall -Programs @("msstore:9NBLGGH4NNS1")
Should -Invoke -CommandName Start-Process -Times 1 -Exactly -ParameterFilter {
$FilePath -eq "winget" -and
(@($ArgumentList) -join "|") -eq "uninstall|--id|9NBLGGH4NNS1|--source|msstore|--silent"
}
}
It "skips whitespace and na package IDs" {
Install-WinUtilProgramWinget -Action Install -Programs @(" ", "na")
Should -Invoke -CommandName Start-Process -Times 0 -Exactly
}
}
Describe "Install-WinUtilProgramChoco" {
BeforeEach {
Mock Write-WinUtilLog { }
Mock Start-Process { [pscustomobject]@{ ExitCode = 0 } }
}
It "starts choco with install arguments" {
Install-WinUtilProgramChoco -Action Install -Programs @("git", "vlc")
Should -Invoke -CommandName Start-Process -Times 1 -Exactly -ParameterFilter {
$FilePath -eq "choco" -and
$ArgumentList -eq "install git vlc -y" -and
$NoNewWindow -eq $true -and
$Wait -eq $true -and
$PassThru -eq $true
}
}
It "starts choco with uninstall arguments" {
Install-WinUtilProgramChoco -Action Uninstall -Programs @("git")
Should -Invoke -CommandName Start-Process -Times 1 -Exactly -ParameterFilter {
$FilePath -eq "choco" -and $ArgumentList -eq "uninstall git -y"
}
}
}
+347
View File
@@ -0,0 +1,347 @@
#===========================================================================
# Tests - Preferences and Themes
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
if (-not ("PackageManagers" -as [type])) {
Add-Type @"
public enum PackageManagers
{
Winget,
Choco
}
"@
}
if (-not ("Windows.Media.SolidColorBrush" -as [type])) {
Add-Type @"
namespace Windows.Media
{
public class SolidColorBrush
{
public object Value { get; private set; }
public SolidColorBrush(object value)
{
Value = value;
}
public override string ToString()
{
return Value == null ? "" : Value.ToString();
}
}
public struct Color
{
public byte R;
public byte G;
public byte B;
public static Color FromRgb(byte r, byte g, byte b)
{
return new Color { R = r, G = g, B = b };
}
public override string ToString()
{
return string.Format("#{0:X2}{1:X2}{2:X2}", R, G, B);
}
}
public class FontFamily
{
public string Value { get; private set; }
public FontFamily(string value)
{
Value = value;
}
public override string ToString()
{
return Value;
}
}
}
namespace System.Windows
{
public class CornerRadius
{
public double Value { get; private set; }
public CornerRadius(double value)
{
Value = value;
}
public override string ToString()
{
return Value.ToString();
}
}
public class GridLength
{
public double Value { get; private set; }
public GridLength(double value)
{
Value = value;
}
public override string ToString()
{
return Value.ToString();
}
}
public class Thickness
{
public double Left { get; private set; }
public double Top { get; private set; }
public double Right { get; private set; }
public double Bottom { get; private set; }
public Thickness(double uniformLength)
{
Left = uniformLength;
Top = uniformLength;
Right = uniformLength;
Bottom = uniformLength;
}
public Thickness(double horizontal, double vertical)
{
Left = horizontal;
Top = vertical;
Right = horizontal;
Bottom = vertical;
}
public Thickness(double left, double top, double right, double bottom)
{
Left = left;
Top = top;
Right = right;
Bottom = bottom;
}
public override string ToString()
{
return string.Format("{0},{1},{2},{3}", Left, Top, Right, Bottom);
}
}
}
"@
}
. (Join-Path $script:repoRoot "functions\private\Set-Preferences.ps1")
. (Join-Path $script:repoRoot "functions\private\Invoke-WinutilThemeChange.ps1")
function Get-WinUtilToggleStatus {
param($ToggleName)
return $false
}
function script:New-WinUtilPreferencesTestRoot {
$testRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilPreferences_$([guid]::NewGuid())"
New-Item -Path $testRoot -ItemType Directory -Force | Out-Null
$testRoot
}
function script:New-WinUtilFakeThemeForm {
$themeButton = [pscustomobject]@{
Content = $null
}
$form = [pscustomobject]@{
Resources = @{}
ThemeButton = $themeButton
}
$form | Add-Member -MemberType ScriptMethod -Name FindName -Value {
param($name)
if ($name -eq "ThemeButton") {
return $this.ThemeButton
}
return $null
}
$form
}
function script:New-WinUtilPreferenceSync {
param(
[hashtable]$Preferences = @{}
)
$script:sync = [Hashtable]::Synchronized(@{
preferences = $Preferences
configs = @{
themes = Get-Content -Path (Join-Path $script:repoRoot "config\themes.json") -Raw | ConvertFrom-Json
}
Form = New-WinUtilFakeThemeForm
})
$global:sync = $script:sync
}
function script:Remove-WinUtilPreferenceGlobals {
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
Remove-Variable -Name winutildir -Scope Global -ErrorAction SilentlyContinue
}
}
Describe "Set-Preferences" {
AfterEach {
if ($script:testRoot -and (Test-Path $script:testRoot)) {
Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
}
$script:testRoot = $null
Remove-WinUtilPreferenceGlobals
}
It "loads default preferences when no preferences file exists" {
$script:testRoot = New-WinUtilPreferencesTestRoot
$global:winutildir = $script:testRoot
New-WinUtilPreferenceSync
Set-Preferences
$script:sync.preferences.theme | Should -Be "Auto"
$script:sync.preferences.packagemanager | Should -Be ([PackageManagers]::Winget)
}
It "loads saved preferences and converts the package manager to an enum" {
$script:testRoot = New-WinUtilPreferencesTestRoot
$global:winutildir = $script:testRoot
New-WinUtilPreferenceSync
Set-Content -Path (Join-Path $script:testRoot "preferences.ini") -Value @(
"theme = Dark"
"packagemanager = Choco"
)
Set-Preferences
$script:sync.preferences.theme | Should -Be "Dark"
$script:sync.preferences.packagemanager | Should -Be ([PackageManagers]::Choco)
}
It "saves current preferences to preferences.ini" {
$script:testRoot = New-WinUtilPreferencesTestRoot
$global:winutildir = $script:testRoot
New-WinUtilPreferenceSync -Preferences @{
theme = "Light"
packagemanager = [PackageManagers]::Winget
}
Set-Preferences -save
$preferencesText = Get-Content -Path (Join-Path $script:testRoot "preferences.ini") -Raw
$preferencesText | Should -Match "theme=Light"
$preferencesText | Should -Match "packagemanager=Winget"
}
It "absorbs legacy preference files and removes old markers" {
$script:testRoot = New-WinUtilPreferencesTestRoot
$global:winutildir = $script:testRoot
New-WinUtilPreferenceSync
Set-Content -Path (Join-Path $script:testRoot "LightTheme.ini") -Value ""
Set-Content -Path (Join-Path $script:testRoot "preferChocolatey.ini") -Value ""
Set-Preferences
$script:sync.preferences.theme | Should -Be "Light"
$script:sync.preferences.packagemanager | Should -Be ([PackageManagers]::Choco)
Test-Path (Join-Path $script:testRoot "LightTheme.ini") | Should -BeFalse
Test-Path (Join-Path $script:testRoot "preferChocolatey.ini") | Should -BeFalse
}
It "absorbs old package manager preferences stored without a key" {
$script:testRoot = New-WinUtilPreferencesTestRoot
$global:winutildir = $script:testRoot
New-WinUtilPreferenceSync
Set-Content -Path (Join-Path $script:testRoot "preferences.ini") -Value "Choco"
Set-Preferences
$script:sync.preferences.theme | Should -Be "Auto"
$script:sync.preferences.packagemanager | Should -Be ([PackageManagers]::Choco)
}
}
Describe "Invoke-WinutilThemeChange" {
AfterEach {
if ($script:testRoot -and (Test-Path $script:testRoot)) {
Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
}
$script:testRoot = $null
Remove-WinUtilPreferenceGlobals
}
It "applies shared and selected theme resources, saves the preference, and updates the theme button" {
$script:testRoot = New-WinUtilPreferencesTestRoot
$global:winutildir = $script:testRoot
New-WinUtilPreferenceSync
Invoke-WinutilThemeChange -theme "Dark"
$script:sync.preferences.theme | Should -Be "Dark"
$script:sync.Form.Resources.ContainsKey("FontFamily") | Should -BeTrue
$script:sync.Form.Resources.ContainsKey("ButtonCornerRadius") | Should -BeTrue
$script:sync.Form.Resources.ContainsKey("MainBackgroundColor") | Should -BeTrue
$script:sync.Form.Resources.ContainsKey("CBorderColor") | Should -BeTrue
$script:sync.Form.ThemeButton.Content | Should -Be ([string][char]0xE708)
$preferencesText = Get-Content -Path (Join-Path $script:testRoot "preferences.ini") -Raw
$preferencesText | Should -Match "theme=Dark"
}
It "uses the system dark-mode toggle when Auto theme is selected" {
$script:testRoot = New-WinUtilPreferencesTestRoot
$global:winutildir = $script:testRoot
New-WinUtilPreferenceSync
Mock Get-WinUtilToggleStatus { return $true }
Invoke-WinutilThemeChange -theme "Auto"
Should -Invoke -CommandName Get-WinUtilToggleStatus -Times 1 -Exactly -ParameterFilter {
$ToggleName -eq "WPFToggleDarkMode"
}
$script:sync.preferences.theme | Should -Be "Auto"
$script:sync.Form.Resources.ContainsKey("MainBackgroundColor") | Should -BeTrue
$script:sync.Form.ThemeButton.Content | Should -Be ([string][char]0xF08C)
}
}
Describe "Theme configuration" {
It "contains resources with value shapes consumed by theme application" {
$themes = Get-Content -Path (Join-Path $script:repoRoot "config\themes.json") -Raw | ConvertFrom-Json
$invalidEntries = [System.Collections.Generic.List[string]]::new()
foreach ($themeName in @("shared", "Light", "Dark")) {
foreach ($property in $themes.$themeName.PSObject.Properties) {
if ($property.Name -like "*Color" -and [string]$property.Value -notmatch '^(#[0-9A-Fa-f]{6}|Transparent)$') {
$invalidEntries.Add("$themeName.$($property.Name) should be a hex color or Transparent")
}
elseif ($property.Name -like "*Radius" -and [string]$property.Value -notmatch '^\d+(\.\d+)?$') {
$invalidEntries.Add("$themeName.$($property.Name) should be numeric")
}
elseif (($property.Name -like "*Thickness" -or $property.Name -like "*Margin") -and [string]$property.Value -notmatch '^\d+(\.\d+)?(,\d+(\.\d+)?){0,3}$') {
$invalidEntries.Add("$themeName.$($property.Name) should be one, two, or four numeric values")
}
elseif ($property.Name -like "*RowHeight*" -and [string]$property.Value -notmatch '^\d+(\.\d+)?$') {
$invalidEntries.Add("$themeName.$($property.Name) should be numeric")
}
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
}
+168
View File
@@ -0,0 +1,168 @@
#===========================================================================
# Tests - Runspace Behavior
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFFeatureInstall.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxRemoval.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFundoall.ps1")
function script:New-WinUtilRunspaceTestContext {
param([hashtable]$InitialSync = @{})
$script:sync = [Hashtable]::Synchronized($InitialSync)
$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$syncVariable = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList "sync", $script:sync, $null
$initialSessionState.Variables.Add($syncVariable)
$script:sync.runspace = [runspacefactory]::CreateRunspacePool(1, 2, $initialSessionState, $Host)
$script:sync.runspace.Open()
}
function script:Clear-WinUtilRunspaceTestContext {
if ($script:powershell) {
$script:powershell.Dispose()
}
if ($script:sync -and $script:sync.runspace) {
$script:sync.runspace.Close()
$script:sync.runspace.Dispose()
}
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name powershell -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name handle -Scope Script -ErrorAction SilentlyContinue
}
function script:Assert-WinUtilAsyncHandle {
param($Handle)
($Handle -is [System.IAsyncResult]) | Should -BeTrue
($Handle -is [array]) | Should -BeFalse
$Handle.AsyncWaitHandle.WaitOne(5000) | Should -BeTrue
}
}
Describe "Invoke-WPFRunspace behavior" {
BeforeEach {
New-WinUtilRunspaceTestContext -InitialSync @{ Marker = "shared" }
}
AfterEach {
Clear-WinUtilRunspaceTestContext
}
It "returns a single async handle with no argument list" {
$handle = Invoke-WPFRunspace -ScriptBlock {
Start-Sleep -Milliseconds 100
"no-args|$($sync.Marker)"
}
Assert-WinUtilAsyncHandle -Handle $handle
@($script:powershell.EndInvoke($handle))[0] | Should -Be "no-args|shared"
}
It "passes one named parameter" {
$handle = Invoke-WPFRunspace -ParameterList @(,("Name", "value")) -ScriptBlock {
param([string]$Name)
Start-Sleep -Milliseconds 100
"Name=$Name"
}
Assert-WinUtilAsyncHandle -Handle $handle
@($script:powershell.EndInvoke($handle))[0] | Should -Be "Name=value"
}
It "passes multiple named parameters" {
$handle = Invoke-WPFRunspace -ParameterList @(
("First", "alpha"),
("Second", "beta")
) -ScriptBlock {
param(
[string]$First,
[string]$Second
)
Start-Sleep -Milliseconds 100
"$First|$Second|$($sync.Marker)"
}
Assert-WinUtilAsyncHandle -Handle $handle
@($script:powershell.EndInvoke($handle))[0] | Should -Be "alpha|beta|shared"
}
It "surfaces scriptblock failures through the owning PowerShell instance" {
$handle = Invoke-WPFRunspace -ScriptBlock {
Start-Sleep -Milliseconds 100
throw "runspace failure"
}
Assert-WinUtilAsyncHandle -Handle $handle
{ $script:powershell.EndInvoke($handle) } | Should -Throw -ExpectedMessage "*runspace failure*"
}
}
Describe "Public runspace callers" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
ProcessRunning = $false
selectedFeatures = [System.Collections.Generic.List[string]]::new()
selectedTweaks = [System.Collections.Generic.List[string]]::new()
selectedAppx = [System.Collections.Generic.List[string]]::new()
configs = @{
appxHashtable = @{}
}
})
Mock Invoke-WPFRunspace { [pscustomobject]@{ MockHandle = $true } }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
}
It "queues selected feature installation without executing the runspace body" {
$script:sync.selectedFeatures.Add("WPFFeaturesSandbox")
Invoke-WPFFeatureInstall
Should -Invoke -CommandName Invoke-WPFRunspace -Times 1 -Exactly -ParameterFilter {
$ScriptBlock -is [scriptblock] -and $null -eq $ArgumentList -and $null -eq $ParameterList
}
}
It "passes selected tweaks as the runspace argument list for undo all" {
$script:sync.selectedTweaks.Add("WPFTweaksTelemetry")
$script:sync.selectedTweaks.Add("WPFTweaksServices")
Invoke-WPFundoall
Should -Invoke -CommandName Invoke-WPFRunspace -Times 1 -Exactly -ParameterFilter {
$ScriptBlock -is [scriptblock] -and
$ArgumentList.Count -eq 2 -and
$ArgumentList[0] -eq "WPFTweaksTelemetry" -and
$ArgumentList[1] -eq "WPFTweaksServices"
}
}
It "passes selected AppX items and app metadata to the removal runspace" {
$script:sync.selectedAppx.Add("WPFAppxExample")
$script:sync.configs.appxHashtable["WPFAppxExample"] = [pscustomobject]@{
Content = "Example"
PackageId = "Example.Package"
}
Invoke-WPFAppxRemoval
Should -Invoke -CommandName Invoke-WPFRunspace -Times 1 -Exactly -ParameterFilter {
$ScriptBlock -is [scriptblock] -and
$ParameterList.Count -eq 2 -and
$ParameterList[0][0] -eq "selected" -and
$ParameterList[0][1][0] -eq "WPFAppxExample" -and
$ParameterList[1][0] -eq "apps" -and
$ParameterList[1][1].ContainsKey("WPFAppxExample")
}
}
}
+259
View File
@@ -0,0 +1,259 @@
#===========================================================================
# Tests - General Sanity
#===========================================================================
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
function script:Test-WinUtilParser {
param([string]$Path)
$tokens = $null
$syntaxErrors = $null
[System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$syntaxErrors) | Out-Null
if ($syntaxErrors.Count -ne 0) {
throw ($syntaxErrors | Out-String)
}
}
function script:Invoke-WindowsPowerShellParser {
param([string[]]$Path)
$windowsPowerShell = Get-Command powershell.exe -ErrorAction SilentlyContinue
if (-not $windowsPowerShell) {
Set-ItResult -Skipped -Because "powershell.exe is not available on this platform."
return
}
$previousPaths = $env:WINUTIL_TEST_PARSE_PATHS
try {
$env:WINUTIL_TEST_PARSE_PATHS = @($Path) -join [Environment]::NewLine
$parseScript = @'
$ErrorActionPreference = 'Stop'
$paths = $env:WINUTIL_TEST_PARSE_PATHS -split "`r?`n" | Where-Object { $_ }
$failed = @()
foreach ($path in $paths) {
$tokens = $null
$syntaxErrors = $null
[System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$syntaxErrors) | Out-Null
if ($syntaxErrors.Count -ne 0) {
$messages = $syntaxErrors | ForEach-Object { $_.Message }
$failed += "[$path] $($messages -join '; ')"
}
}
if ($failed.Count -gt 0) {
$failed -join [Environment]::NewLine
exit 1
}
'@
$output = & $windowsPowerShell.Source -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command $parseScript 2>&1
if ($LASTEXITCODE -ne 0) {
throw "Windows PowerShell parser failed:`n$($output | Out-String)"
}
} finally {
if ($null -eq $previousPaths) {
Remove-Item Env:\WINUTIL_TEST_PARSE_PATHS -ErrorAction SilentlyContinue
} else {
$env:WINUTIL_TEST_PARSE_PATHS = $previousPaths
}
}
}
}
Describe "PowerShell source sanity" {
$scriptCases = @(
"Compile.ps1",
"windev.ps1",
"scripts\start.ps1",
"scripts\main.ps1"
) | ForEach-Object {
@{
Name = $_
Path = Join-Path $repoRoot $_
}
}
foreach ($scriptCase in $scriptCases) {
It "parses $($scriptCase.Name) with the current PowerShell parser" -TestCases $scriptCase {
param([string]$Path)
Test-WinUtilParser -Path $Path
}
}
It "parses WinUtil source files with Windows PowerShell when available" {
$sourcePaths = @(
Join-Path $script:repoRoot "Compile.ps1"
Join-Path $script:repoRoot "windev.ps1"
Join-Path $script:repoRoot "scripts\start.ps1"
Join-Path $script:repoRoot "scripts\main.ps1"
Get-ChildItem -Path (Join-Path $script:repoRoot "functions") -Filter *.ps1 -Recurse | Select-Object -ExpandProperty FullName
)
Invoke-WindowsPowerShellParser -Path $sourcePaths
}
}
Describe "Compiled WinUtil sanity" {
BeforeAll {
$script:compiledPath = Join-Path $script:repoRoot "winutil.ps1"
Push-Location $script:repoRoot
try {
& (Join-Path $script:repoRoot "Compile.ps1")
} finally {
Pop-Location
}
}
It "generates winutil.ps1" {
Test-Path -Path $script:compiledPath | Should -BeTrue
}
It "parses compiled winutil.ps1 with the current PowerShell parser" {
Test-WinUtilParser -Path $script:compiledPath
}
It "parses compiled winutil.ps1 with Windows PowerShell when available" {
Invoke-WindowsPowerShellParser -Path $script:compiledPath
}
It "contains embedded configs, XAML, autounattend XML, and runspace bootstrap" {
$content = Get-Content -Path $script:compiledPath -Raw
$requiredSnippets = @(
('$sync.configs.applications = @' + "'"),
('$inputXML = @' + "'"),
('$WinUtilAutounattendXml = @' + "'"),
"SessionStateVariableEntry -ArgumentList 'sync'",
"SessionStateFunctionEntry",
"[runspacefactory]::CreateRunspacePool",
"function Invoke-WPFRunspace"
)
foreach ($snippet in $requiredSnippets) {
if (-not $content.Contains($snippet)) {
throw "Compiled script is missing expected content: $snippet"
}
}
}
It "transforms applications config keys with WPFInstall prefixes" {
$content = Get-Content -Path $script:compiledPath -Raw
$configMatch = [regex]::Match(
$content,
"(?s)\`$sync\.configs\.applications = @'\r?\n(?<json>.*?)\r?\n'@ \| ConvertFrom-Json"
)
if (-not $configMatch.Success) {
throw "Compiled script is missing embedded applications config."
}
$sourceApps = Get-Content -Path (Join-Path $script:repoRoot "config\applications.json") -Raw | ConvertFrom-Json
$compiledApps = $configMatch.Groups["json"].Value | ConvertFrom-Json
foreach ($sourceApp in $sourceApps.PSObject.Properties) {
$compiledKey = "WPFInstall$($sourceApp.Name)"
if ($compiledApps.PSObject.Properties.Name -notcontains $compiledKey) {
throw "Compiled applications config is missing transformed key: $compiledKey"
}
if ($compiledApps.PSObject.Properties.Name -contains $sourceApp.Name) {
throw "Compiled applications config contains untransformed source key: $($sourceApp.Name)"
}
}
}
It "preserves compile source ordering" {
$content = Get-Content -Path $script:compiledPath -Raw
$orderedSnippets = @(
'$sync.version =',
'function Add-SelectedAppsMenuItem',
('$sync.configs.applications = @' + "'"),
('$inputXML = @' + "'"),
('$WinUtilAutounattendXml = @' + "'"),
'$sync.SearchBarClearButton.Add_Click({'
)
$lastIndex = -1
foreach ($snippet in $orderedSnippets) {
$index = $content.IndexOf($snippet)
if ($index -lt 0) {
throw "Compiled script is missing expected ordered content: $snippet"
}
if ($index -le $lastIndex) {
throw "Compiled script content is out of order near: $snippet"
}
$lastIndex = $index
}
}
It "replaces the generated build date placeholder" {
$content = Get-Content -Path $script:compiledPath -Raw
$expectedBuildDate = Get-Date -Format "yy.MM.dd"
$content | Should -Not -Match ([regex]::Escape("#{replaceme}"))
$content | Should -Match ([regex]::Escape('$sync.version = "' + $expectedBuildDate + '"'))
}
}
Describe "Runspace sanity" {
BeforeAll {
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1")
}
It "returns a single async handle and runs a scriptblock with arguments in the shared runspace pool" {
$script:sync = [Hashtable]::Synchronized(@{ SmokeValue = "shared" })
$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$syncVariable = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList "sync", $script:sync, $null
$initialSessionState.Variables.Add($syncVariable)
$script:sync.runspace = [runspacefactory]::CreateRunspacePool(1, 2, $initialSessionState, $Host)
$script:sync.runspace.Open()
$ended = $false
try {
$handle = Invoke-WPFRunspace -ArgumentList "argument" -ParameterList @(,("NamedValue", "parameter")) -ScriptBlock {
param($ArgumentValue, [string]$NamedValue)
Start-Sleep -Milliseconds 200
"$ArgumentValue|$NamedValue|$($sync.SmokeValue)"
}
($handle -is [System.IAsyncResult]) | Should -BeTrue
($handle -is [array]) | Should -BeFalse
$handle.AsyncWaitHandle.WaitOne(5000) | Should -BeTrue
$result = $script:powershell.EndInvoke($handle)
$ended = $true
@($result)[0] | Should -Be "argument|parameter|shared"
} finally {
if (-not $ended -and $handle -and $handle.IsCompleted -and $script:powershell) {
try {
$script:powershell.EndInvoke($handle) | Out-Null
} catch {
# The assertion failure is more useful than cleanup errors here.
}
}
if ($script:powershell) {
$script:powershell.Dispose()
}
if ($script:sync -and $script:sync.runspace) {
$script:sync.runspace.Close()
$script:sync.runspace.Dispose()
}
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name powershell -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name handle -Scope Script -ErrorAction SilentlyContinue
}
}
}
+446
View File
@@ -0,0 +1,446 @@
#===========================================================================
# Tests - Search and Filter Helpers
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
if (-not ("Windows.Visibility" -as [type])) {
Add-Type @"
namespace Windows
{
public enum Visibility
{
Visible,
Collapsed
}
}
"@
}
if (-not ("System.Windows.Controls.CheckBox" -as [type])) {
Add-Type @"
namespace System.Windows.Controls
{
public class CheckBox
{
public bool? IsChecked { get; set; }
}
public class Label
{
public object Content { get; set; }
}
public class WrapPanel
{
public object Visibility { get; set; }
}
public class StackPanel
{
public System.Collections.ArrayList Children { get; private set; }
public StackPanel()
{
Children = new System.Collections.ArrayList();
}
}
}
"@
}
if (-not ("Windows.Controls.Border" -as [type])) {
Add-Type @"
namespace Windows.Controls
{
public class Border
{
public object Child { get; set; }
public object Visibility { get; set; }
}
public class DockPanel
{
public System.Collections.ArrayList Children { get; private set; }
public object Visibility { get; set; }
public DockPanel()
{
Children = new System.Collections.ArrayList();
}
}
public class StackPanel
{
public System.Collections.ArrayList Children { get; private set; }
public object Visibility { get; set; }
public StackPanel()
{
Children = new System.Collections.ArrayList();
}
}
public class ItemsControl
{
public System.Collections.ArrayList Items { get; private set; }
public object Visibility { get; set; }
public ItemsControl()
{
Items = new System.Collections.ArrayList();
}
}
public class Label
{
public object Content { get; set; }
public object ToolTip { get; set; }
public object Visibility { get; set; }
}
public class CheckBox
{
public object Content { get; set; }
public object ToolTip { get; set; }
public object Visibility { get; set; }
}
}
"@
}
. (Join-Path $script:repoRoot "functions\private\Find-AppsByNameOrDescription.ps1")
. (Join-Path $script:repoRoot "functions\private\Find-TweaksByNameOrDescription.ps1")
function script:New-WinUtilSearchCollection {
return ,[System.Collections.ArrayList]::new()
}
function script:New-WinUtilAppSearchItem {
param([string]$Tag)
[pscustomobject]@{
Tag = $Tag
Visibility = [Windows.Visibility]::Visible
}
}
function script:New-WinUtilAppCategory {
param(
[string]$Label,
[object[]]$Items
)
$labelControl = [pscustomobject]@{
Content = $Label
Visibility = [Windows.Visibility]::Visible
}
$wrapPanel = [pscustomobject]@{
Children = New-WinUtilSearchCollection
Visibility = [Windows.Visibility]::Visible
}
foreach ($item in $Items) {
$null = $wrapPanel.Children.Add($item)
}
$children = New-WinUtilSearchCollection
$null = $children.Add($labelControl)
$null = $children.Add($wrapPanel)
[pscustomobject]@{
Children = $children
Visibility = [Windows.Visibility]::Visible
}
}
function script:New-WinUtilAppSearchContext {
param([object[]]$Categories)
$items = New-WinUtilSearchCollection
foreach ($category in $Categories) {
$null = $items.Add($category)
}
$script:sync = [Hashtable]::Synchronized(@{
ItemsControl = [pscustomobject]@{
Items = $items
}
configs = @{
applicationsHashtable = @{
WPFInstallBrowser = [pscustomobject]@{
Content = "Firefox"
Description = "Fast private browser"
}
WPFInstallMedia = [pscustomobject]@{
Content = "VLC"
Description = "Media player"
}
WPFInstallLiteral = [pscustomobject]@{
Content = "Tool [abc]"
Description = "Literal wildcard sample"
}
WPFInstallEditor = [pscustomobject]@{
Content = "Code Editor"
Description = "Text editing"
}
}
}
})
$global:sync = $script:sync
}
function script:New-WinUtilFakeSearchForm {
param(
$TweaksPanel,
$AppxPanel
)
$form = [pscustomobject]@{
tweakspanel = $TweaksPanel
appxpanel = $AppxPanel
}
$form | Add-Member -MemberType ScriptMethod -Name FindName -Value {
param($name)
return $this.$name
}
$form
}
function script:New-WinUtilTweakLabelItem {
param(
[string]$Content,
[string]$ToolTip = ""
)
$item = [Windows.Controls.DockPanel]::new()
$checkbox = [Windows.Controls.CheckBox]::new()
$label = [Windows.Controls.Label]::new()
$label.Content = $Content
$label.ToolTip = $ToolTip
$null = $item.Children.Add($checkbox)
$null = $item.Children.Add($label)
$item.Visibility = [Windows.Visibility]::Visible
$item
}
function script:New-WinUtilTweakCheckboxItem {
param(
[string]$Content,
[string]$ToolTip = ""
)
$item = [Windows.Controls.StackPanel]::new()
$checkbox = [Windows.Controls.CheckBox]::new()
$checkbox.Content = $Content
$checkbox.ToolTip = $ToolTip
$null = $item.Children.Add($checkbox)
$item.Visibility = [Windows.Visibility]::Visible
$item
}
function script:New-WinUtilTweakCategory {
param(
[string]$Label,
[object[]]$Items
)
$categoryLabel = [Windows.Controls.Label]::new()
$categoryLabel.Content = $Label
$categoryLabel.Visibility = [Windows.Visibility]::Visible
$itemsControl = [Windows.Controls.ItemsControl]::new()
$null = $itemsControl.Items.Add($categoryLabel)
foreach ($item in $Items) {
$null = $itemsControl.Items.Add($item)
}
$dockPanel = [Windows.Controls.DockPanel]::new()
$null = $dockPanel.Children.Add($itemsControl)
$border = [Windows.Controls.Border]::new()
$border.Child = $dockPanel
$border.Visibility = [Windows.Visibility]::Visible
[pscustomobject]@{
Border = $border
Label = $categoryLabel
ItemsControl = $itemsControl
}
}
function script:New-WinUtilTweakPanel {
param([object[]]$Categories)
$panel = [pscustomobject]@{
Children = New-WinUtilSearchCollection
}
foreach ($category in $Categories) {
$null = $panel.Children.Add($category.Border)
}
$panel
}
function script:New-WinUtilTweakSearchContext {
param(
$TweaksPanel,
$AppxPanel = $null,
[string]$CurrentTab = "Tweaks"
)
$script:sync = [Hashtable]::Synchronized(@{
currentTab = $CurrentTab
Form = New-WinUtilFakeSearchForm -TweaksPanel $TweaksPanel -AppxPanel $AppxPanel
})
$global:sync = $script:sync
}
function script:Remove-WinUtilSearchGlobals {
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
}
}
Describe "Find-AppsByNameOrDescription" {
AfterEach {
Remove-WinUtilSearchGlobals
}
It "restores app visibility and respects collapsed category state for empty search" {
$browserItem = New-WinUtilAppSearchItem -Tag "WPFInstallBrowser"
$mediaItem = New-WinUtilAppSearchItem -Tag "WPFInstallMedia"
$browserItem.Visibility = [Windows.Visibility]::Collapsed
$mediaItem.Visibility = [Windows.Visibility]::Collapsed
$collapsedCategory = New-WinUtilAppCategory -Label "+ Browsers" -Items @($browserItem)
$expandedCategory = New-WinUtilAppCategory -Label "- Media" -Items @($mediaItem)
$collapsedCategory.Children[1].Visibility = [Windows.Visibility]::Collapsed
$expandedCategory.Children[1].Visibility = [Windows.Visibility]::Collapsed
New-WinUtilAppSearchContext -Categories @($collapsedCategory, $expandedCategory)
Find-AppsByNameOrDescription -SearchString ""
$collapsedCategory.Visibility | Should -Be ([Windows.Visibility]::Visible)
$collapsedCategory.Children[0].Visibility | Should -Be ([Windows.Visibility]::Visible)
$collapsedCategory.Children[1].Visibility | Should -Be ([Windows.Visibility]::Collapsed)
$browserItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
$expandedCategory.Children[1].Visibility | Should -Be ([Windows.Visibility]::Visible)
$mediaItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
}
It "shows matching apps by description and hides categories without matches" {
$browserItem = New-WinUtilAppSearchItem -Tag "WPFInstallBrowser"
$mediaItem = New-WinUtilAppSearchItem -Tag "WPFInstallMedia"
$editorItem = New-WinUtilAppSearchItem -Tag "WPFInstallEditor"
$browserCategory = New-WinUtilAppCategory -Label "+ Browsers" -Items @($browserItem, $mediaItem)
$editorCategory = New-WinUtilAppCategory -Label "- Editors" -Items @($editorItem)
New-WinUtilAppSearchContext -Categories @($browserCategory, $editorCategory)
Find-AppsByNameOrDescription -SearchString "private"
$browserCategory.Visibility | Should -Be ([Windows.Visibility]::Visible)
$browserCategory.Children[0].Content | Should -Be "- Browsers"
$browserCategory.Children[1].Visibility | Should -Be ([Windows.Visibility]::Visible)
$browserItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
$mediaItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
$editorCategory.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
$editorItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
}
It "treats wildcard characters as literal app search text" {
$literalItem = New-WinUtilAppSearchItem -Tag "WPFInstallLiteral"
$mediaItem = New-WinUtilAppSearchItem -Tag "WPFInstallMedia"
$category = New-WinUtilAppCategory -Label "- Tools" -Items @($literalItem, $mediaItem)
New-WinUtilAppSearchContext -Categories @($category)
Find-AppsByNameOrDescription -SearchString "[abc]"
$literalItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
$mediaItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
$category.Visibility | Should -Be ([Windows.Visibility]::Visible)
}
}
Describe "Find-TweaksByNameOrDescription" {
AfterEach {
Remove-WinUtilSearchGlobals
}
It "restores category labels and tweak item visibility for empty search" {
$labelItem = New-WinUtilTweakLabelItem -Content "Disable Telemetry" -ToolTip "Stop tracking"
$stackItem = New-WinUtilTweakCheckboxItem -Content "Show Extensions" -ToolTip "File extension display"
$category = New-WinUtilTweakCategory -Label "+ Privacy" -Items @($labelItem, $stackItem)
$labelItem.Visibility = [Windows.Visibility]::Collapsed
$stackItem.Visibility = [Windows.Visibility]::Collapsed
$category.Label.Visibility = [Windows.Visibility]::Collapsed
$category.Border.Visibility = [Windows.Visibility]::Collapsed
$panel = New-WinUtilTweakPanel -Categories @($category)
New-WinUtilTweakSearchContext -TweaksPanel $panel
Find-TweaksByNameOrDescription -SearchString ""
$category.Border.Visibility | Should -Be ([Windows.Visibility]::Visible)
$category.Label.Visibility | Should -Be ([Windows.Visibility]::Visible)
$labelItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
$stackItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
}
It "shows tweak matches by label tooltip and checkbox content" {
$telemetryItem = New-WinUtilTweakLabelItem -Content "Disable Telemetry" -ToolTip "Stop tracking"
$extensionsItem = New-WinUtilTweakCheckboxItem -Content "Show Extensions" -ToolTip "File extension display"
$nonMatchItem = New-WinUtilTweakLabelItem -Content "Enable NumLock" -ToolTip "Keyboard setting"
$category = New-WinUtilTweakCategory -Label "+ Privacy" -Items @($telemetryItem, $extensionsItem, $nonMatchItem)
$panel = New-WinUtilTweakPanel -Categories @($category)
New-WinUtilTweakSearchContext -TweaksPanel $panel
Find-TweaksByNameOrDescription -SearchString "tracking"
$category.Border.Visibility | Should -Be ([Windows.Visibility]::Visible)
$category.Label.Visibility | Should -Be ([Windows.Visibility]::Visible)
$category.Label.Content | Should -Be "- Privacy"
$telemetryItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
$extensionsItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
$nonMatchItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
Find-TweaksByNameOrDescription -SearchString "Show Extensions"
$telemetryItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
$extensionsItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
$nonMatchItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
}
It "hides tweak category panels when no items match" {
$item = New-WinUtilTweakLabelItem -Content "Disable Telemetry" -ToolTip "Stop tracking"
$category = New-WinUtilTweakCategory -Label "- Privacy" -Items @($item)
$panel = New-WinUtilTweakPanel -Categories @($category)
New-WinUtilTweakSearchContext -TweaksPanel $panel
Find-TweaksByNameOrDescription -SearchString "not-present"
$category.Border.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
$category.Label.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
$item.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
}
It "searches the AppX panel when AppX is the current tab" {
$tweakItem = New-WinUtilTweakLabelItem -Content "Disable Telemetry" -ToolTip "Stop tracking"
$appxItem = New-WinUtilTweakCheckboxItem -Content "Xbox Overlay" -ToolTip "Gaming overlay package"
$tweakCategory = New-WinUtilTweakCategory -Label "- Privacy" -Items @($tweakItem)
$appxCategory = New-WinUtilTweakCategory -Label "+ AppX" -Items @($appxItem)
$tweakPanel = New-WinUtilTweakPanel -Categories @($tweakCategory)
$appxPanel = New-WinUtilTweakPanel -Categories @($appxCategory)
New-WinUtilTweakSearchContext -TweaksPanel $tweakPanel -AppxPanel $appxPanel -CurrentTab "AppX"
Find-TweaksByNameOrDescription -SearchString "overlay"
$appxCategory.Border.Visibility | Should -Be ([Windows.Visibility]::Visible)
$appxCategory.Label.Content | Should -Be "- AppX"
$appxItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
$tweakCategory.Border.Visibility | Should -Be ([Windows.Visibility]::Visible)
$tweakItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
}
}
+157
View File
@@ -0,0 +1,157 @@
#===========================================================================
# Tests - System Helper Functions
#===========================================================================
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Set-WinUtilRegistry.ps1")
. (Join-Path $script:repoRoot "functions\private\Set-WinUtilService.ps1")
function Write-WinUtilLog { }
}
Describe "Set-WinUtilRegistry" {
BeforeEach {
$script:testPathResults = @{}
Mock Write-Host { }
Mock Write-Warning { }
Mock Write-WinUtilLog { }
Mock Test-Path {
param([string]$Path)
if ($script:testPathResults.ContainsKey($Path)) {
return $script:testPathResults[$Path]
}
throw "Unexpected Test-Path call: $Path"
}
Mock New-PSDrive { }
Mock New-Item { }
Mock Set-ItemProperty { }
Mock Remove-ItemProperty { }
}
It "creates a missing registry path before setting a value" {
$registryPath = "HKCU:\Software\WinUtilTest"
$script:testPathResults["HKU:\"] = $true
$script:testPathResults[$registryPath] = $false
Set-WinUtilRegistry -Path $registryPath -Name "Enabled" -Type "DWord" -Value "1"
Should -Invoke -CommandName New-PSDrive -Times 0 -Exactly
Should -Invoke -CommandName New-Item -Times 1 -Exactly -ParameterFilter {
$Path -eq $registryPath -and $Force -eq $true -and $ErrorAction -eq "Stop"
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq $registryPath -and
$Name -eq "Enabled" -and
$Type -eq "DWord" -and
$Value -eq "1" -and
$Force -eq $true -and
$ErrorAction -eq "Stop"
}
Should -Invoke -CommandName Remove-ItemProperty -Times 0 -Exactly
}
It "creates the HKU PSDrive when it is missing" {
$registryPath = "HKCU:\Software\WinUtilTest"
$script:testPathResults["HKU:\"] = $false
$script:testPathResults[$registryPath] = $true
Set-WinUtilRegistry -Path $registryPath -Name "Enabled" -Type "DWord" -Value "1"
Should -Invoke -CommandName New-PSDrive -Times 1 -Exactly -ParameterFilter {
$PSProvider -eq "Registry" -and
$Name -eq "HKU" -and
$Root -eq "HKEY_USERS"
}
Should -Invoke -CommandName New-Item -Times 0 -Exactly
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq $registryPath -and $Name -eq "Enabled" -and $Type -eq "DWord" -and $Value -eq "1"
}
}
It "removes a registry value when requested" {
$registryPath = "HKLM:\Software\WinUtilTest"
$script:testPathResults["HKU:\"] = $true
$script:testPathResults[$registryPath] = $true
Set-WinUtilRegistry -Path $registryPath -Name "ObsoleteValue" -Type "String" -Value "<RemoveEntry>"
Should -Invoke -CommandName Set-ItemProperty -Times 0 -Exactly
Should -Invoke -CommandName Remove-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq $registryPath -and
$Name -eq "ObsoleteValue" -and
$Force -eq $true -and
$ErrorAction -eq "Stop"
}
}
}
Describe "Set-WinUtilService" {
BeforeEach {
Mock Write-Host { }
Mock Write-Warning { }
Mock Write-WinUtilLog { }
Mock Get-Service { }
Mock Set-Service { }
}
It "sets the startup type for an existing service" {
Mock Get-Service {
[pscustomobject]@{
Name = "DiagTrack"
StartType = "Automatic"
}
} -ParameterFilter { $Name -eq "DiagTrack" -and $ErrorAction -eq "Stop" }
Set-WinUtilService -Name "DiagTrack" -StartupType "Disabled"
Should -Invoke -CommandName Get-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "DiagTrack" -and $ErrorAction -eq "Stop"
}
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$StartupType -eq "Disabled" -and $ErrorAction -eq "Stop"
}
}
It "does not change a service that already has the requested startup type" {
Mock Get-Service {
[pscustomobject]@{
Name = "DiagTrack"
StartType = "Disabled"
}
} -ParameterFilter { $Name -eq "DiagTrack" -and $ErrorAction -eq "Stop" }
Set-WinUtilService -Name "DiagTrack" -StartupType "Disabled"
Should -Invoke -CommandName Get-Service -Times 1 -Exactly
Should -Invoke -CommandName Set-Service -Times 0 -Exactly
}
It "does not call Set-Service when the service is missing" {
Mock Get-Service {
$exception = [Microsoft.PowerShell.Commands.ServiceCommandException]::new("Cannot find any service with service name '$Name'.")
$errorRecord = [System.Management.Automation.ErrorRecord]::new(
$exception,
"NoServiceFoundForGivenName,Microsoft.PowerShell.Commands.GetServiceCommand",
[System.Management.Automation.ErrorCategory]::ObjectNotFound,
$Name
)
throw $errorRecord
} -ParameterFilter { $Name -eq "MissingService" -and $ErrorAction -eq "Stop" }
Set-WinUtilService -Name "MissingService" -StartupType "Disabled"
Should -Invoke -CommandName Get-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "MissingService" -and $ErrorAction -eq "Stop"
}
Should -Invoke -CommandName Set-Service -Times 0 -Exactly
Should -Invoke -CommandName Write-Warning -Times 1 -Exactly -ParameterFilter {
$Message -eq "Service MissingService was not found."
}
}
}
+231
View File
@@ -0,0 +1,231 @@
#===========================================================================
# Tests - Tweak Orchestration
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Invoke-WinUtilTweaks.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFtweaksbutton.ps1")
function Set-WinUtilService {
param($Name, $StartupType)
}
function Set-WinUtilRegistry {
param($Name, $Path, $Type, $Value)
}
function Invoke-WinUtilScript {
param($Name, [scriptblock]$ScriptBlock)
}
function Remove-WinUtilAPPX {
param($Name)
}
function Set-WinUtilDNS {
param($DNSProvider)
}
function Invoke-WPFRunspace {
param($ArgumentList, $ParameterList, [scriptblock]$ScriptBlock)
}
function Invoke-WPFUIThread {
param([scriptblock]$ScriptBlock)
}
function Set-WinUtilProgressBar {
param($Label, $Percent)
}
function Write-WinUtilLog {
param($Message, $Level, $Component)
}
function script:New-WinUtilTweaksConfig {
[pscustomobject]@{
WPFTweaksExample = [pscustomobject]@{
service = @(
[pscustomobject]@{
Name = "DiagTrack"
StartupType = "Disabled"
OriginalType = "Automatic"
}
)
registry = @(
[pscustomobject]@{
Path = "HKLM:\Software\WinUtilTest"
Name = "AllowTelemetry"
Type = "DWord"
Value = "0"
OriginalValue = "1"
}
)
InvokeScript = @("Write-Output 'apply tweak'")
UndoScript = @("Write-Output 'undo tweak'")
appx = @("Microsoft.ExampleApp")
}
WPFTweaksServiceOnly = [pscustomobject]@{
service = @(
[pscustomobject]@{
Name = "DiagTrack"
StartupType = "Disabled"
OriginalType = "Automatic"
}
)
}
}
}
}
Describe "Invoke-WinUtilTweaks" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
configs = @{
tweaks = New-WinUtilTweaksConfig
}
})
Mock Get-Service {
[pscustomobject]@{
Name = "DiagTrack"
StartType = "Automatic"
}
}
Mock Set-WinUtilService { }
Mock Set-WinUtilRegistry { }
Mock Invoke-WinUtilScript { }
Mock Remove-WinUtilAPPX { }
Mock Write-WinUtilLog { }
Mock Write-Warning { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
}
It "dispatches apply actions to service, registry, script, and AppX helpers" {
Invoke-WinUtilTweaks -CheckBox "WPFTweaksExample"
Should -Invoke -CommandName Get-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "DiagTrack" -and $ErrorAction -eq "Stop"
}
Should -Invoke -CommandName Set-WinUtilService -Times 1 -Exactly -ParameterFilter {
$Name -eq "DiagTrack" -and $StartupType -eq "Disabled"
}
Should -Invoke -CommandName Set-WinUtilRegistry -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\Software\WinUtilTest" -and
$Name -eq "AllowTelemetry" -and
$Type -eq "DWord" -and
$Value -eq "0"
}
Should -Invoke -CommandName Invoke-WinUtilScript -Times 1 -Exactly -ParameterFilter {
$Name -eq "WPFTweaksExample" -and $ScriptBlock.ToString() -eq "Write-Output 'apply tweak'"
}
Should -Invoke -CommandName Remove-WinUtilAPPX -Times 1 -Exactly -ParameterFilter {
$Name -eq "Microsoft.ExampleApp"
}
}
It "uses original registry values and service startup types in undo mode" {
Invoke-WinUtilTweaks -CheckBox "WPFTweaksExample" -undo $true
Should -Invoke -CommandName Get-Service -Times 0 -Exactly
Should -Invoke -CommandName Set-WinUtilService -Times 1 -Exactly -ParameterFilter {
$Name -eq "DiagTrack" -and $StartupType -eq "Automatic"
}
Should -Invoke -CommandName Set-WinUtilRegistry -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\Software\WinUtilTest" -and
$Name -eq "AllowTelemetry" -and
$Type -eq "DWord" -and
$Value -eq "1"
}
Should -Invoke -CommandName Invoke-WinUtilScript -Times 1 -Exactly -ParameterFilter {
$Name -eq "WPFTweaksExample" -and $ScriptBlock.ToString() -eq "Write-Output 'undo tweak'"
}
Should -Invoke -CommandName Remove-WinUtilAPPX -Times 0 -Exactly
}
It "keeps a user-changed service startup type by default" {
Mock Get-Service {
[pscustomobject]@{
Name = "DiagTrack"
StartType = "Manual"
}
} -ParameterFilter { $Name -eq "DiagTrack" }
Invoke-WinUtilTweaks -CheckBox "WPFTweaksServiceOnly"
Should -Invoke -CommandName Get-Service -Times 1 -Exactly
Should -Invoke -CommandName Set-WinUtilService -Times 0 -Exactly
}
It "forces a service startup type when KeepServiceStartup is disabled" {
Invoke-WinUtilTweaks -CheckBox "WPFTweaksServiceOnly" -KeepServiceStartup $false
Should -Invoke -CommandName Get-Service -Times 0 -Exactly
Should -Invoke -CommandName Set-WinUtilService -Times 1 -Exactly -ParameterFilter {
$Name -eq "DiagTrack" -and $StartupType -eq "Disabled"
}
}
}
Describe "Invoke-WPFtweaksbutton" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
ProcessRunning = $false
selectedTweaks = [System.Collections.Generic.List[string]]::new()
WPFchangedns = [pscustomobject]@{
text = "Cloudflare"
}
})
Mock Invoke-WPFRunspace { [pscustomobject]@{ MockHandle = $true } }
Mock Invoke-WinUtilTweaks { }
Mock Invoke-WPFUIThread { }
Mock Set-WinUtilProgressBar { }
Mock Write-WinUtilLog { }
Mock Write-Host { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
}
It "passes selected tweaks, DNS provider, and progress counters to the tweak runspace" {
$script:sync.selectedTweaks.Add("WPFTweaksTelemetry")
$script:sync.selectedTweaks.Add("WPFTweaksServices")
Invoke-WPFtweaksbutton
Should -Invoke -CommandName Invoke-WPFRunspace -Times 1 -Exactly -ParameterFilter {
$ParameterList.Count -eq 4 -and
$ParameterList[0][0] -eq "tweaks" -and
$ParameterList[0][1].Count -eq 2 -and
$ParameterList[0][1][0] -eq "WPFTweaksTelemetry" -and
$ParameterList[0][1][1] -eq "WPFTweaksServices" -and
$ParameterList[1][0] -eq "dnsProvider" -and
$ParameterList[1][1] -eq "Cloudflare" -and
$ParameterList[2][0] -eq "completedSteps" -and
$ParameterList[2][1] -eq 0 -and
$ParameterList[3][0] -eq "totalSteps" -and
$ParameterList[3][1] -eq 2
}
}
It "runs the restore point first and advances progress before queueing remaining tweaks" {
$script:sync.selectedTweaks.Add("WPFTweaksRestorePoint")
$script:sync.selectedTweaks.Add("WPFTweaksTelemetry")
Invoke-WPFtweaksbutton
Should -Invoke -CommandName Invoke-WinUtilTweaks -Times 1 -Exactly -ParameterFilter {
$CheckBox -eq "WPFTweaksRestorePoint"
}
Should -Invoke -CommandName Invoke-WPFRunspace -Times 1 -Exactly -ParameterFilter {
$ParameterList.Count -eq 4 -and
$ParameterList[0][0] -eq "tweaks" -and
$ParameterList[0][1].Count -eq 1 -and
$ParameterList[0][1][0] -eq "WPFTweaksTelemetry" -and
$ParameterList[1][0] -eq "dnsProvider" -and
$ParameterList[1][1] -eq "Cloudflare" -and
$ParameterList[2][0] -eq "completedSteps" -and
$ParameterList[2][1] -eq 1 -and
$ParameterList[3][0] -eq "totalSteps" -and
$ParameterList[3][1] -eq 2
}
}
}
+274
View File
@@ -0,0 +1,274 @@
#===========================================================================
# Tests - UI Selection and State Helpers
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
if (-not ("System.Windows.Controls.CheckBox" -as [type])) {
Add-Type @"
namespace Windows
{
public enum Visibility
{
Visible,
Collapsed
}
}
namespace System.Windows.Controls
{
public class CheckBox
{
public bool? IsChecked { get; set; }
}
public class Label
{
public object Content { get; set; }
}
public class WrapPanel
{
public global::Windows.Visibility Visibility { get; set; }
}
public class StackPanel
{
public System.Collections.ArrayList Children { get; } = new System.Collections.ArrayList();
}
}
"@
}
. (Join-Path $script:repoRoot "functions\private\Update-WinUtilSelections.ps1")
. (Join-Path $script:repoRoot "functions\private\Reset-WPFCheckBoxes.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFSelectedCheckboxesUpdate.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFToggleAllCategories.ps1")
function script:New-WinUtilFakeCheckBox {
param([bool]$IsChecked = $false)
$checkbox = [System.Windows.Controls.CheckBox]::new()
$checkbox.IsChecked = $IsChecked
$checkbox
}
function script:New-WinUtilFakeCategory {
param(
[string]$Label,
[Windows.Visibility]$Visibility
)
$category = [System.Windows.Controls.StackPanel]::new()
$categoryLabel = [System.Windows.Controls.Label]::new()
$categoryLabel.Content = $Label
$wrapPanel = [System.Windows.Controls.WrapPanel]::new()
$wrapPanel.Visibility = $Visibility
$null = $category.Children.Add($categoryLabel)
$null = $category.Children.Add($wrapPanel)
$category
}
function script:New-WinUtilUiStateTestContext {
$testSync = [Hashtable]::Synchronized(@{
selectedApps = [System.Collections.Generic.List[string]]::new()
selectedTweaks = [System.Collections.Generic.List[string]]::new()
selectedToggles = [System.Collections.Generic.List[string]]::new()
selectedFeatures = [System.Collections.Generic.List[string]]::new()
selectedAppx = [System.Collections.Generic.List[string]]::new()
configs = @{
applicationsHashtable = @{
WPFInstallGit = [pscustomobject]@{
Content = "Git"
}
}
}
WPFselectedAppsButton = [pscustomobject]@{
Content = ""
}
selectedAppsstackPanel = [pscustomobject]@{
Children = [System.Collections.ArrayList]::new()
}
})
$script:sync = $testSync
$global:sync = $testSync
}
function Add-SelectedAppsMenuItem {
param($name, $key)
$null = $global:sync.selectedAppsstackPanel.Children.Add([pscustomobject]@{
Name = $name
Key = $key
})
}
}
Describe "Update-WinUtilSelections" {
BeforeEach {
New-WinUtilUiStateTestContext
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
}
It "adds imported checkbox keys to the matching selected lists" {
Update-WinUtilSelections @(
"WPFInstallGit",
"WPFTweaksTelemetry",
"WPFToggleDarkMode",
"WPFFeatureSandbox",
"WPFAppxExample"
)
@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
@($script:sync.selectedTweaks) | Should -Be @("WPFTweaksTelemetry")
@($script:sync.selectedToggles) | Should -Be @("WPFToggleDarkMode")
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
@($script:sync.selectedAppx) | Should -Be @("WPFAppxExample")
}
}
Describe "Invoke-WPFSelectedCheckboxesUpdate" {
BeforeEach {
New-WinUtilUiStateTestContext
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
}
It "adds each checkbox family without duplicating existing selections" {
Invoke-WPFSelectedCheckboxesUpdate -type "Add" -checkboxName "WPFInstallGit"
Invoke-WPFSelectedCheckboxesUpdate -type "Add" -checkboxName "WPFInstallGit"
Invoke-WPFSelectedCheckboxesUpdate -type "Add" -checkboxName "WPFTweaksTelemetry"
Invoke-WPFSelectedCheckboxesUpdate -type "Add" -checkboxName "WPFToggleDarkMode"
Invoke-WPFSelectedCheckboxesUpdate -type "Add" -checkboxName "WPFFeatureSandbox"
Invoke-WPFSelectedCheckboxesUpdate -type "Add" -checkboxName "WPFAppxExample"
@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
@($script:sync.selectedTweaks) | Should -Be @("WPFTweaksTelemetry")
@($script:sync.selectedToggles) | Should -Be @("WPFToggleDarkMode")
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
@($script:sync.selectedAppx) | Should -Be @("WPFAppxExample")
}
It "removes checkbox keys from the matching selected lists" {
$script:sync.selectedApps.Add("WPFInstallGit")
$script:sync.selectedTweaks.Add("WPFTweaksTelemetry")
$script:sync.selectedToggles.Add("WPFToggleDarkMode")
$script:sync.selectedFeatures.Add("WPFFeatureSandbox")
$script:sync.selectedAppx.Add("WPFAppxExample")
Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkboxName "WPFInstallGit"
Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkboxName "WPFTweaksTelemetry"
Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkboxName "WPFToggleDarkMode"
Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkboxName "WPFFeatureSandbox"
Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkboxName "WPFAppxExample"
$script:sync.selectedApps.Count | Should -Be 0
$script:sync.selectedTweaks.Count | Should -Be 0
$script:sync.selectedToggles.Count | Should -Be 0
$script:sync.selectedFeatures.Count | Should -Be 0
$script:sync.selectedAppx.Count | Should -Be 0
}
}
Describe "Reset-WPFCheckBoxes" {
BeforeEach {
New-WinUtilUiStateTestContext
$script:sync.selectedApps.Add("WPFInstallGit")
$script:sync.selectedTweaks.Add("WPFTweaksTelemetry")
$script:sync.selectedFeatures.Add("WPFFeatureSandbox")
$script:sync.selectedAppx.Add("WPFAppxExample")
$script:sync.selectedToggles.Add("WPFToggleDarkMode")
$script:sync.WPFInstallGit = New-WinUtilFakeCheckBox
$script:sync.WPFInstallVlc = New-WinUtilFakeCheckBox -IsChecked $true
$script:sync.WPFTweaksTelemetry = New-WinUtilFakeCheckBox
$script:sync.WPFFeatureSandbox = New-WinUtilFakeCheckBox
$script:sync.WPFAppxExample = New-WinUtilFakeCheckBox
$script:sync.WPFToggleDarkMode = New-WinUtilFakeCheckBox
$script:sync.WPFToggleOther = New-WinUtilFakeCheckBox -IsChecked $true
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
}
It "syncs non-toggle checkboxes from selected lists and updates selected app UI state" {
Reset-WPFCheckBoxes
$script:sync.WPFInstallGit.IsChecked | Should -BeTrue
$script:sync.WPFInstallVlc.IsChecked | Should -BeFalse
$script:sync.WPFTweaksTelemetry.IsChecked | Should -BeTrue
$script:sync.WPFFeatureSandbox.IsChecked | Should -BeTrue
$script:sync.WPFAppxExample.IsChecked | Should -BeTrue
$script:sync.WPFToggleDarkMode.IsChecked | Should -BeFalse
$script:sync.WPFselectedAppsButton.Content | Should -Be "Selected Apps: 1"
$script:sync.selectedAppsstackPanel.Children.Count | Should -Be 1
$script:sync.selectedAppsstackPanel.Children[0].Name | Should -Be "Git"
$script:sync.selectedAppsstackPanel.Children[0].Key | Should -Be "WPFInstallGit"
}
It "restores imported toggles when requested without changing absent toggles" {
Reset-WPFCheckBoxes -doToggles $true
$script:sync.WPFToggleDarkMode.IsChecked | Should -BeTrue
$script:sync.WPFToggleOther.IsChecked | Should -BeTrue
}
}
Describe "Invoke-WPFToggleAllCategories" {
BeforeEach {
New-WinUtilUiStateTestContext
$script:sync.ItemsControl = [pscustomobject]@{
Items = [System.Collections.ArrayList]::new()
}
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
}
It "expands all install categories and updates collapsed labels" {
$category = New-WinUtilFakeCategory -Label "+ Browsers" -Visibility ([Windows.Visibility]::Collapsed)
$null = $script:sync.ItemsControl.Items.Add($category)
Invoke-WPFToggleAllCategories -Action "Expand"
$category.Children[1].Visibility | Should -Be ([Windows.Visibility]::Visible)
$category.Children[0].Content | Should -Be "- Browsers"
}
It "collapses all install categories and updates expanded labels" {
$category = New-WinUtilFakeCategory -Label "- Browsers" -Visibility ([Windows.Visibility]::Visible)
$null = $script:sync.ItemsControl.Items.Add($category)
Invoke-WPFToggleAllCategories -Action "Collapse"
$category.Children[1].Visibility | Should -Be ([Windows.Visibility]::Collapsed)
$category.Children[0].Content | Should -Be "+ Browsers"
}
It "warns and exits when ItemsControl is not initialized" {
$script:sync.ItemsControl = $null
Mock Write-Warning { }
Invoke-WPFToggleAllCategories -Action "Expand"
Should -Invoke -CommandName Write-Warning -Times 1 -Exactly -ParameterFilter {
$Message -eq "ItemsControl not initialized"
}
}
}
+294
View File
@@ -0,0 +1,294 @@
#===========================================================================
# Tests - Windows Update Profiles
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFUpdatesdisable.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFUpdatesdefault.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFUpdatessecurity.ps1")
function Write-WinUtilLog {
param($Message, $Level, $Component)
}
function Get-ScheduledTask {
param($TaskPath)
}
function Disable-ScheduledTask {
param(
[Parameter(ValueFromPipeline = $true)]
$InputObject,
$ErrorAction
)
process { }
}
function Enable-ScheduledTask {
param(
[Parameter(ValueFromPipeline = $true)]
$InputObject,
$ErrorAction
)
process { }
}
function secedit {
param(
[Parameter(ValueFromRemainingArguments = $true)]
$Arguments
)
}
$script:updateTaskPaths = @(
'\Microsoft\Windows\InstallService\*',
'\Microsoft\Windows\UpdateOrchestrator\*',
'\Microsoft\Windows\UpdateAssistant\*',
'\Microsoft\Windows\WaaSMedic\*',
'\Microsoft\Windows\WindowsUpdate\*',
'\Microsoft\WindowsUpdate\*'
)
}
Describe "Invoke-WPFUpdatesdisable" {
BeforeEach {
Mock Write-Host { }
Mock Write-WinUtilLog { }
Mock New-Item { }
Mock Set-ItemProperty { }
Mock Set-Service { }
Mock Stop-Service { }
Mock Remove-Item { }
Mock Get-ScheduledTask {
[pscustomobject]@{
TaskPath = $TaskPath
}
}
Mock Disable-ScheduledTask { }
}
It "sets update disable registry policy values" {
Invoke-WPFUpdatesdisable
Should -Invoke -CommandName New-Item -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and $Force -eq $true
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
$Name -eq "NoAutoUpdate" -and
$Type -eq "DWord" -and
$Value -eq 1
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
$Name -eq "AUOptions" -and
$Type -eq "DWord" -and
$Value -eq 1
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" -and
$Name -eq "DODownloadMode" -and
$Type -eq "DWord" -and
$Value -eq 0
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer" -and
$Name -eq "SettingsPageVisibility" -and
$Value -eq "hide:windowsupdate"
}
}
It "disables update services and clears the SoftwareDistribution folder" {
Invoke-WPFUpdatesdisable
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "BITS" -and $StartupType -eq "Disabled"
}
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "wuauserv" -and $StartupType -eq "Disabled"
}
Should -Invoke -CommandName Stop-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "UsoSvc" -and $Force -eq $true
}
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "UsoSvc" -and $StartupType -eq "Disabled"
}
Should -Invoke -CommandName Remove-Item -Times 1 -Exactly -ParameterFilter {
$Path -eq "C:\Windows\SoftwareDistribution\*" -and
$Recurse -eq $true -and
$Force -eq $true
}
}
It "disables update scheduled task paths" {
Invoke-WPFUpdatesdisable
foreach ($expectedTaskPath in $script:updateTaskPaths) {
$expected = $expectedTaskPath
Should -Invoke -CommandName Get-ScheduledTask -Times 1 -Exactly -ParameterFilter {
$TaskPath -eq $expected
}
}
Should -Invoke -CommandName Disable-ScheduledTask -Times $script:updateTaskPaths.Count -Exactly
}
}
Describe "Invoke-WPFUpdatesdefault" {
BeforeEach {
Mock Write-Host { }
Mock Write-WinUtilLog { }
Mock Remove-Item { }
Mock Remove-ItemProperty { }
Mock Set-Service { }
Mock Start-Service { }
Mock Get-ScheduledTask {
[pscustomobject]@{
TaskPath = $TaskPath
}
}
Mock Enable-ScheduledTask { }
Mock secedit { }
}
It "removes update policy registry paths and shows the Windows Update settings page" {
Invoke-WPFUpdatesdefault
$expectedPaths = @(
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization",
"HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings",
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata",
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching",
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"
)
foreach ($expectedRegistryPath in $expectedPaths) {
$expected = $expectedRegistryPath
Should -Invoke -CommandName Remove-Item -Times 1 -Exactly -ParameterFilter {
$Path -eq $expected -and $Recurse -eq $true -and $Force -eq $true
}
}
Should -Invoke -CommandName Remove-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer" -and
$Name -eq "SettingsPageVisibility"
}
}
It "restores update service startup types" {
Invoke-WPFUpdatesdefault
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "BITS" -and $StartupType -eq "Manual"
}
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "wuauserv" -and $StartupType -eq "Manual"
}
Should -Invoke -CommandName Start-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "UsoSvc"
}
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "UsoSvc" -and $StartupType -eq "Automatic"
}
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "WaaSMedicSvc" -and $StartupType -eq "Manual"
}
}
It "enables update scheduled task paths and resets local policy defaults" {
Invoke-WPFUpdatesdefault
foreach ($expectedTaskPath in $script:updateTaskPaths) {
$expected = $expectedTaskPath
Should -Invoke -CommandName Get-ScheduledTask -Times 1 -Exactly -ParameterFilter {
$TaskPath -eq $expected
}
}
Should -Invoke -CommandName Enable-ScheduledTask -Times $script:updateTaskPaths.Count -Exactly
Should -Invoke -CommandName secedit -Times 1 -Exactly -ParameterFilter {
$Arguments[0] -eq "/configure" -and
$Arguments[1] -eq "/cfg" -and
$Arguments[2] -like "*\inf\defltbase.inf" -and
$Arguments[3] -eq "/db" -and
$Arguments[4] -eq "defltbase.sdb"
}
}
}
Describe "Invoke-WPFUpdatessecurity" {
BeforeEach {
Mock Write-Host { }
Mock Write-WinUtilLog { }
Mock New-Item { }
Mock Set-ItemProperty { }
}
It "disables driver metadata and Windows Update driver search" {
Invoke-WPFUpdatessecurity
Should -Invoke -CommandName New-Item -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -and $Force -eq $true
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -and
$Name -eq "PreventDeviceMetadataFromNetwork" -and
$Type -eq "DWord" -and
$Value -eq 1
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -and
$Name -eq "DontPromptForWindowsUpdate" -and
$Type -eq "DWord" -and
$Value -eq 1
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -and
$Name -eq "DontSearchWindowsUpdate" -and
$Type -eq "DWord" -and
$Value -eq 1
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -and
$Name -eq "DriverUpdateWizardWuSearchEnabled" -and
$Type -eq "DWord" -and
$Value -eq 0
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -and
$Name -eq "ExcludeWUDriversInQualityUpdate" -and
$Type -eq "DWord" -and
$Value -eq 1
}
}
It "sets recommended update deferral and auto-reboot policy values" {
Invoke-WPFUpdatessecurity
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -and
$Name -eq "BranchReadinessLevel" -and
$Type -eq "DWord" -and
$Value -eq 20
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -and
$Name -eq "DeferFeatureUpdatesPeriodInDays" -and
$Type -eq "DWord" -and
$Value -eq 365
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -and
$Name -eq "DeferQualityUpdatesPeriodInDays" -and
$Type -eq "DWord" -and
$Value -eq 4
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
$Name -eq "NoAutoRebootWithLoggedOnUsers" -and
$Type -eq "DWord" -and
$Value -eq 1
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
$Name -eq "AUPowerManagement" -and
$Type -eq "DWord" -and
$Value -eq 0
}
}
}
+203 -2
View File
@@ -3,9 +3,47 @@
#===========================================================================
Describe "Win11 Creator setup media" {
BeforeAll {
$script:repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$script:isoWorkflowPath = Join-Path $script:repoRoot "functions\private\Invoke-WinUtilISO.ps1"
$script:isoScriptPath = Join-Path $script:repoRoot "functions\private\Invoke-WinUtilISOScript.ps1"
$script:autoUnattendPath = Join-Path $script:repoRoot "tools\autounattend.xml"
function Get-WinUtilFunctionText {
param (
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$FunctionName
)
$tokens = $null
$errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $Path), [ref]$tokens, [ref]$errors)
if ($errors.Count -gt 0) {
throw "Unable to parse $Path`: $($errors[0].Message)"
}
$functionAst = $ast.Find({
param($node)
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -eq $FunctionName
}, $true)
if (-not $functionAst) {
throw "Unable to find function $FunctionName in $Path."
}
return $functionAst.Extent.Text
}
$script:editionIdFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Get-WinUtilEditionIdFromName"
$script:addDriversFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Add-DriversToImage"
$script:answerFileChildElementFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Get-WinUtilISOScriptChildElement"
$script:answerFileConversionFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "ConvertTo-WinUtilISOAnswerFile"
$script:editionConfigFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Write-WinUtilISOEditionConfig"
}
It "autounattend template does not force a product key" {
$templatePath = Join-Path $PSScriptRoot "..\tools\autounattend.xml"
[xml]$xml = Get-Content -Path $templatePath -Raw
[xml]$xml = Get-Content -Path $script:autoUnattendPath -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
@@ -30,4 +68,167 @@ Describe "Win11 Creator setup media" {
}
}
}
It "maps Windows edition names to setup edition IDs" {
. ([scriptblock]::Create($script:editionIdFunction))
$cases = @{
"Windows 11 Home Single Language" = "CoreSingleLanguage"
"Windows 11 Home N" = "CoreN"
"Windows 11 Home" = "Core"
"Windows 11 Pro for Workstations N" = "ProfessionalWorkstationN"
"Windows 11 Pro for Workstations" = "ProfessionalWorkstation"
"Windows 11 Pro Education N" = "ProfessionalEducationN"
"Windows 11 Pro Education" = "ProfessionalEducation"
"Windows 11 Pro N" = "ProfessionalN"
"Windows 11 Pro" = "Professional"
"Windows 11 Education N" = "EducationN"
"Windows 11 Education" = "Education"
"Windows 11 Enterprise LTSC N" = "EnterpriseSN"
"Windows 11 Enterprise LTSC" = "EnterpriseS"
"Windows 11 Enterprise N" = "EnterpriseN"
"Windows 11 Enterprise" = "Enterprise"
}
foreach ($case in $cases.GetEnumerator()) {
Get-WinUtilEditionIdFromName -EditionName $case.Key | Should -Be $case.Value
}
Get-WinUtilEditionIdFromName -EditionName "Windows 11 Unknown Edition" | Should -Be ""
}
It "writes ei.cfg and removes stale PID.txt for selected editions" {
. ([scriptblock]::Create($script:editionConfigFunction))
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
$sourcesDir = Join-Path $contentRoot "sources"
$logs = [System.Collections.Generic.List[string]]::new()
$logger = { param($message) $logs.Add([string]$message) }
try {
New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null
Set-Content -Path (Join-Path $sourcesDir "PID.txt") -Value "stale-key" -Encoding UTF8
Write-WinUtilISOEditionConfig -ContentRoot $contentRoot -EditionId "Professional" -Logger $logger
Test-Path (Join-Path $sourcesDir "PID.txt") | Should -BeFalse
Test-Path (Join-Path $sourcesDir "ei.cfg") | Should -BeTrue
(Get-Content -Path (Join-Path $sourcesDir "ei.cfg")) -join "|" |
Should -Be "[EditionID]|Professional|[Channel]|Retail|[VL]|0"
($logs -join "|") | Should -Match "Removed sources\\PID\.txt"
($logs -join "|") | Should -Match "Written sources\\ei\.cfg"
} finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "skips ei.cfg when selected edition ID is unknown" {
. ([scriptblock]::Create($script:editionConfigFunction))
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
$logs = [System.Collections.Generic.List[string]]::new()
$logger = { param($message) $logs.Add([string]$message) }
try {
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
Write-WinUtilISOEditionConfig -ContentRoot $contentRoot -EditionId "" -Logger $logger
Test-Path (Join-Path $contentRoot "sources\ei.cfg") | Should -BeFalse
($logs -join "|") | Should -Match "selected edition ID is unknown"
} finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "converts autounattend setup metadata without preserving placeholder product keys" {
. ([scriptblock]::Create($script:answerFileChildElementFunction))
. ([scriptblock]::Create($script:answerFileConversionFunction))
[xml]$templateXml = Get-Content -Path $script:autoUnattendPath -Raw
$unattendNs = "urn:schemas-microsoft-com:unattend"
$nsMgr = New-Object System.Xml.XmlNamespaceManager($templateXml.NameTable)
$nsMgr.AddNamespace("u", $unattendNs)
$nsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
$setupComponent = $templateXml.SelectSingleNode('//u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]', $nsMgr)
$userData = $templateXml.CreateElement("UserData", $unattendNs)
$productKey = $templateXml.CreateElement("ProductKey", $unattendNs)
$key = $templateXml.CreateElement("Key", $unattendNs)
$key.InnerText = "00000-00000-00000-00000-00000"
[void]$productKey.AppendChild($key)
[void]$userData.AppendChild($productKey)
[void]$setupComponent.AppendChild($userData)
$originalSetupFileCount = $templateXml.SelectNodes("//sg:File", $nsMgr).Count
[xml]$converted = ConvertTo-WinUtilISOAnswerFile -XmlContent $templateXml.OuterXml -ImageIndex 3
$convertedNsMgr = New-Object System.Xml.XmlNamespaceManager($converted.NameTable)
$convertedNsMgr.AddNamespace("u", $unattendNs)
$convertedNsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
$converted.DocumentElement.NamespaceURI | Should -Be $unattendNs
$converted.DocumentElement.GetAttribute("xmlns:wcm") | Should -Be "http://schemas.microsoft.com/WMIConfig/2002/State"
$converted.SelectNodes('//u:component[@name="Microsoft-Windows-Setup"]/u:UserData/u:ProductKey', $convertedNsMgr).Count | Should -Be 0
$converted.SelectSingleNode('//u:ImageInstall/u:OSImage/u:InstallFrom/u:MetaData[u:Key="/IMAGE/INDEX"]/u:Value', $convertedNsMgr).InnerText | Should -Be "3"
$converted.SelectNodes("//sg:File", $convertedNsMgr).Count | Should -Be $originalSetupFileCount
}
It "logs DISM output for driver injection through a mocked command" {
. ([scriptblock]::Create($script:addDriversFunction))
$script:dismArguments = @()
function dism {
param([Parameter(ValueFromRemainingArguments)]$Arguments)
$script:dismArguments = @($Arguments)
"driver one"
"driver two"
}
$logs = [System.Collections.Generic.List[string]]::new()
Add-DriversToImage -MountPath "C:\Mount" -DriverDir "C:\Drivers" -Label "install" -Logger {
param($message)
$logs.Add([string]$message)
}
($script:dismArguments -join "|") | Should -Be "/English|/image:C:\Mount|/Add-Driver|/Driver:C:\Drivers|/Recurse"
($logs -join "|") | Should -Be " dism[install]: driver one| dism[install]: driver two"
}
It "keeps driver export and boot.wim injection behind the selected branch" {
$content = Get-Content -Path $script:isoScriptPath -Raw
foreach ($expectedText in @(
'if ($InjectCurrentSystemDrivers)',
'Export-WindowsDriver -Online -Destination $driverExportRoot',
'Add-DriversToImage -MountPath $ScratchDir -DriverDir $driverExportRoot -Label "install" -Logger $Log',
'Invoke-BootWimInject -BootWimPath $bootWim -DriverDir $driverExportRoot -Logger $Log',
'Warning: boot.wim not found - skipping boot.wim driver injection.',
'Driver injection skipped.'
)) {
$content | Should -Match ([regex]::Escape($expectedText))
}
}
It "attempts winget oscdimg install and exits before export when fallback fails" {
$content = Get-Content -Path $script:isoWorkflowPath -Raw
foreach ($expectedText in @(
'oscdimg.exe not found. Attempting to install via winget...',
'Install-WinUtilWinget',
'Get-Command winget',
'install -e --id Microsoft.OSCDIMG --accept-package-agreements --accept-source-agreements',
'oscdimg.exe still not found after install attempt.',
'oscdimg Not Found'
)) {
$content | Should -Match ([regex]::Escape($expectedText))
}
$fallbackIndex = $content.IndexOf('oscdimg.exe not found. Attempting to install via winget...')
$notFoundDialogIndex = $content.IndexOf('oscdimg Not Found', $fallbackIndex)
$runspaceIndex = $content.IndexOf('[Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()', $fallbackIndex)
$fallbackIndex | Should -BeGreaterThan -1
$notFoundDialogIndex | Should -BeGreaterThan $fallbackIndex
$runspaceIndex | Should -BeGreaterThan $notFoundDialogIndex
}
}
+404
View File
@@ -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")
}
}
}