mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-09 09:31:15 +10:00
Fix AppX detection and add package restore support (#4842)
* Fix installed AppX package detection * Add AppX package restore workflow * Add AppX operation progress feedback * Fix AppX operation launch race * Handle AppX workflow failures
This commit is contained in:
+398
-6
@@ -1,12 +1,16 @@
|
||||
#===========================================================================
|
||||
# Tests - AppX Removal
|
||||
# Tests - AppX Management
|
||||
#===========================================================================
|
||||
|
||||
BeforeAll {
|
||||
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
. (Join-Path $script:repoRoot "functions\private\Get-WinUtilInstalledAPPX.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\private\Install-WinUtilAPPX.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\private\Remove-WinUtilAPPX.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\private\Remove-WinUtilProvisionedAPPX.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxInstall.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxRemoval.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFButton.ps1")
|
||||
|
||||
$tokens = $null
|
||||
$parseErrors = $null
|
||||
@@ -20,6 +24,16 @@ BeforeAll {
|
||||
}, $true)
|
||||
$script:provisionedRemovalScriptBlock = $ps5CommandAssignment.Right.Expression.ScriptBlock.GetScriptBlock()
|
||||
|
||||
$installSourcePath = Join-Path $script:repoRoot "functions\private\Install-WinUtilAPPX.ps1"
|
||||
$installSourceAst = [System.Management.Automation.Language.Parser]::ParseFile($installSourcePath, [ref]$tokens, [ref]$parseErrors)
|
||||
$installCommandAssignment = $installSourceAst.Find({
|
||||
param($node)
|
||||
$node -is [System.Management.Automation.Language.AssignmentStatementAst] -and
|
||||
$node.Left -is [System.Management.Automation.Language.VariableExpressionAst] -and
|
||||
$node.Left.VariablePath.UserPath -eq "ps5Command"
|
||||
}, $true)
|
||||
$script:appxInstallScriptBlock = $installCommandAssignment.Right.Expression.ScriptBlock.GetScriptBlock()
|
||||
|
||||
function Write-WinUtilLog {
|
||||
param($Message, $Level, $Component)
|
||||
}
|
||||
@@ -29,8 +43,25 @@ BeforeAll {
|
||||
function Invoke-WPFRunspace {
|
||||
param($ArgumentList, $ParameterList, [scriptblock]$ScriptBlock)
|
||||
}
|
||||
function Invoke-WPFUIThread {
|
||||
param([scriptblock]$ScriptBlock)
|
||||
}
|
||||
function Set-WinUtilProgressBar {
|
||||
param($Label, $Percent)
|
||||
}
|
||||
function Set-WinUtilTweaksProgressIndicator {
|
||||
param($Visible, $Label, $Percent)
|
||||
}
|
||||
function powershell.exe { }
|
||||
function Get-AppxPackage {
|
||||
param($Name, [switch]$AllUsers)
|
||||
param($Name, [switch]$AllUsers, $ErrorAction)
|
||||
}
|
||||
function Add-AppxPackage {
|
||||
param($Register, [switch]$DisableDevelopmentMode, $ErrorAction)
|
||||
}
|
||||
function Install-WinUtilWinget { }
|
||||
function Install-WinUtilProgramWinget {
|
||||
param($Action, $Programs)
|
||||
}
|
||||
function Remove-AppxPackage {
|
||||
param(
|
||||
@@ -73,6 +104,162 @@ BeforeAll {
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Get-WinUtilInstalledAPPX" {
|
||||
BeforeEach {
|
||||
Mock Write-WinUtilLog { }
|
||||
Mock powershell.exe {
|
||||
$global:LASTEXITCODE = 0
|
||||
@("Example.One", "Example.Two")
|
||||
}
|
||||
}
|
||||
|
||||
It "queries installed package names through Windows PowerShell" {
|
||||
$result = Get-WinUtilInstalledAPPX
|
||||
|
||||
$result | Should -Be @("Example.One", "Example.Two")
|
||||
Should -Invoke -CommandName powershell.exe -Times 1 -Exactly
|
||||
Should -Invoke -CommandName Write-WinUtilLog -Times 0 -Exactly
|
||||
}
|
||||
|
||||
It "logs query failures and returns no package names" {
|
||||
Mock powershell.exe {
|
||||
$global:LASTEXITCODE = 1
|
||||
"AppX query failed"
|
||||
}
|
||||
|
||||
$result = @(Get-WinUtilInstalledAPPX)
|
||||
|
||||
$result | Should -HaveCount 0
|
||||
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
|
||||
$Level -eq "ERROR" -and
|
||||
$Component -eq "AppX" -and
|
||||
$Message -eq "Failed to get installed AppX packages: AppX query failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Install-WinUtilAPPX" {
|
||||
BeforeEach {
|
||||
Mock Write-WinUtilLog { }
|
||||
Mock Install-WinUtilWinget { }
|
||||
Mock Install-WinUtilProgramWinget { }
|
||||
Mock powershell.exe {
|
||||
$global:LASTEXITCODE = 0
|
||||
"C:\Program Files\WindowsApps\Example.Package\AppxManifest.xml"
|
||||
}
|
||||
}
|
||||
|
||||
It "uses a local manifest without contacting the Microsoft Store" {
|
||||
Install-WinUtilAPPX -Name "Example.Package" -StoreId "9EXAMPLE1234"
|
||||
|
||||
Should -Invoke -CommandName Install-WinUtilWinget -Times 0 -Exactly
|
||||
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 0 -Exactly
|
||||
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
|
||||
$Component -eq "AppX" -and
|
||||
$Message -like "Registered local AppX manifest for Example.Package*"
|
||||
}
|
||||
}
|
||||
|
||||
It "falls back to the Microsoft Store when no local manifest is available" {
|
||||
Mock powershell.exe { $global:LASTEXITCODE = 0 }
|
||||
|
||||
Install-WinUtilAPPX -Name "Example.Package" -StoreId "9EXAMPLE1234"
|
||||
|
||||
Should -Invoke -CommandName Install-WinUtilWinget -Times 1 -Exactly
|
||||
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 1 -Exactly -ParameterFilter {
|
||||
$Action -eq "Install" -and $Programs.Count -eq 1 -and $Programs[0] -eq "msstore:9EXAMPLE1234"
|
||||
}
|
||||
}
|
||||
|
||||
It "logs local registration failures before using the Microsoft Store" {
|
||||
Mock powershell.exe {
|
||||
$global:LASTEXITCODE = 1
|
||||
"Registration failed"
|
||||
}
|
||||
|
||||
Install-WinUtilAPPX -Name "Example.Package" -StoreId "9EXAMPLE1234"
|
||||
|
||||
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
|
||||
$Level -eq "WARN" -and
|
||||
$Component -eq "AppX" -and
|
||||
$Message -eq "Local AppX registration failed for Example.Package: Registration failed"
|
||||
}
|
||||
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 1 -Exactly
|
||||
}
|
||||
|
||||
It "throws after logging an error when neither install method is available" {
|
||||
Mock powershell.exe { $global:LASTEXITCODE = 0 }
|
||||
|
||||
{ Install-WinUtilAPPX -Name "Example.Package" } |
|
||||
Should -Throw "Unable to install Example.Package because no local manifest or Microsoft Store ID is available."
|
||||
|
||||
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 0 -Exactly
|
||||
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
|
||||
$Level -eq "ERROR" -and
|
||||
$Component -eq "AppX" -and
|
||||
$Message -eq "Unable to install Example.Package because no local manifest or Microsoft Store ID is available."
|
||||
}
|
||||
}
|
||||
|
||||
It "registers an installed package manifest through Windows PowerShell" {
|
||||
Mock Get-AppxPackage {
|
||||
[pscustomobject]@{
|
||||
InstallLocation = "C:\Program Files\WindowsApps\Example.Package"
|
||||
Version = [version]"2.0.0.0"
|
||||
}
|
||||
}
|
||||
Mock Get-AppxProvisionedPackage { }
|
||||
Mock Test-Path { $LiteralPath -eq "C:\Program Files\WindowsApps\Example.Package\AppxManifest.xml" }
|
||||
Mock Add-AppxPackage { }
|
||||
|
||||
$result = & $script:appxInstallScriptBlock "Example.Package"
|
||||
|
||||
$result | Should -Be "C:\Program Files\WindowsApps\Example.Package\AppxManifest.xml"
|
||||
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly
|
||||
Should -Invoke -CommandName Add-AppxPackage -Times 1 -Exactly
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Get installed AppX selection" {
|
||||
BeforeEach {
|
||||
$script:sync = [Hashtable]::Synchronized(@{
|
||||
ProcessRunning = $false
|
||||
configs = @{
|
||||
feature = @{}
|
||||
appxHashtable = @{
|
||||
WPFAppxExample = [pscustomobject]@{ PackageId = "Example.Package" }
|
||||
WPFAppxMissing = [pscustomobject]@{ PackageId = "Missing.Package" }
|
||||
}
|
||||
}
|
||||
WPFAppxExample = [pscustomobject]@{ IsChecked = $false }
|
||||
WPFAppxMissing = [pscustomobject]@{ IsChecked = $false }
|
||||
})
|
||||
|
||||
Mock Set-WinUtilProgressBar { }
|
||||
Mock Set-WinUtilTweaksProgressIndicator { }
|
||||
Mock Get-WinUtilInstalledAPPX { @("Example.Package") }
|
||||
Mock Invoke-WPFAppxInstall { }
|
||||
}
|
||||
|
||||
AfterEach {
|
||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
It "selects configured packages returned by the compatibility-safe query" {
|
||||
Invoke-WPFButton -Button "WPFGetInstalledAppx"
|
||||
|
||||
Should -Invoke -CommandName Get-WinUtilInstalledAPPX -Times 1 -Exactly
|
||||
$script:sync.WPFAppxExample.IsChecked | Should -BeTrue
|
||||
$script:sync.WPFAppxMissing.IsChecked | Should -BeFalse
|
||||
}
|
||||
|
||||
It "routes the install button to the AppX install workflow" {
|
||||
Invoke-WPFButton -Button "WPFInstallSelectedAppx"
|
||||
|
||||
Should -Invoke -CommandName Invoke-WPFAppxInstall -Times 1 -Exactly
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Remove-WinUtilAPPX" {
|
||||
BeforeEach {
|
||||
Mock Write-Host { }
|
||||
@@ -155,13 +342,123 @@ Describe "Remove-WinUtilProvisionedAPPX" {
|
||||
}
|
||||
}
|
||||
|
||||
It "handles child process failures before logging completion" {
|
||||
It "throws after logging child process failures" {
|
||||
$source = Get-Content -Path $provisionedSourcePath -Raw
|
||||
|
||||
$source | Should -Match '\$removalOutput = powershell\.exe .* 2>&1'
|
||||
$source | Should -Match 'if \(\$LASTEXITCODE -ne 0 -or \$null -ne \$removalOutput\)'
|
||||
$source | Should -Match 'Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "AppX provisioned package removal failed:'
|
||||
$source | Should -Match '(?s)AppX provisioned package removal failed:.*return.*AppX provisioned package removal completed\.'
|
||||
$source | Should -Match 'Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message \$errorMessage'
|
||||
$source | Should -Match '(?s)AppX provisioned package removal failed:.*throw \$errorMessage.*AppX provisioned package removal completed\.'
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Invoke-WPFAppxInstall" {
|
||||
BeforeEach {
|
||||
$script:sync = [Hashtable]::Synchronized(@{
|
||||
ProcessRunning = $false
|
||||
Form = [pscustomobject]@{ Dispatcher = [pscustomobject]@{} }
|
||||
selectedAppx = [System.Collections.Generic.List[string]]::new()
|
||||
configs = @{
|
||||
appxHashtable = @{
|
||||
WPFAppxExample = [pscustomobject]@{
|
||||
Content = "Example App"
|
||||
PackageId = "Example.Package"
|
||||
StoreId = "9EXAMPLE1234"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
$script:capturedAppxInstallScriptBlock = $null
|
||||
$script:capturedAppxInstallParameterList = $null
|
||||
$script:appxInstallProcessRunningAtLaunch = $null
|
||||
|
||||
Mock Show-WinUtilMessage { "OK" }
|
||||
Mock Write-Host { }
|
||||
Mock Write-WinUtilLog { }
|
||||
Mock Set-WinUtilTweaksProgressIndicator { }
|
||||
Mock Invoke-WPFUIThread { }
|
||||
Mock Install-WinUtilAPPX { }
|
||||
Mock Invoke-WPFRunspace {
|
||||
$script:appxInstallProcessRunningAtLaunch = $script:sync.ProcessRunning
|
||||
$script:capturedAppxInstallScriptBlock = $ScriptBlock
|
||||
$script:capturedAppxInstallParameterList = $ParameterList
|
||||
[pscustomobject]@{ MockHandle = $true }
|
||||
}
|
||||
}
|
||||
|
||||
AfterEach {
|
||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name capturedAppxInstallScriptBlock -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name capturedAppxInstallParameterList -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name appxInstallProcessRunningAtLaunch -Scope Script -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
It "prompts and exits when no AppX packages are selected for install" {
|
||||
Invoke-WPFAppxInstall
|
||||
|
||||
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 "prevents overlapping AppX install operations" {
|
||||
$script:sync.ProcessRunning = $true
|
||||
$script:sync.selectedAppx.Add("WPFAppxExample")
|
||||
|
||||
Invoke-WPFAppxInstall
|
||||
|
||||
Should -Invoke -CommandName Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
|
||||
$Message -eq "An AppX 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 "installs selected AppX packages with their Store IDs" {
|
||||
$script:sync.selectedAppx.Add("WPFAppxExample")
|
||||
|
||||
Invoke-WPFAppxInstall
|
||||
$script:appxInstallProcessRunningAtLaunch | Should -BeTrue
|
||||
& $script:capturedAppxInstallScriptBlock -selected @("WPFAppxExample") -apps $script:sync.configs.appxHashtable
|
||||
|
||||
$script:capturedAppxInstallParameterList[0][1][0] | Should -Be "WPFAppxExample"
|
||||
Should -Invoke -CommandName Install-WinUtilAPPX -Times 1 -Exactly -ParameterFilter {
|
||||
$Name -eq "Example.Package" -and $StoreId -eq "9EXAMPLE1234"
|
||||
}
|
||||
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
|
||||
$Visible -eq $true -and $Label -eq "Installing Example App (1/1)" -and $Percent -eq 0
|
||||
}
|
||||
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
|
||||
$Visible -eq $true -and $Label -eq "Installed Example App (1/1)" -and $Percent -eq 100
|
||||
}
|
||||
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
|
||||
$Visible -eq $true -and $Label -eq "AppX install finished" -and $Percent -eq 100
|
||||
}
|
||||
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
|
||||
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"*'
|
||||
}
|
||||
$script:sync.ProcessRunning | Should -BeFalse
|
||||
}
|
||||
|
||||
It "shows failure feedback and clears ProcessRunning when install fails" {
|
||||
$script:sync.selectedAppx.Add("WPFAppxExample")
|
||||
Mock Install-WinUtilAPPX { throw "Install failed" }
|
||||
|
||||
Invoke-WPFAppxInstall
|
||||
& $script:capturedAppxInstallScriptBlock -selected @("WPFAppxExample") -apps $script:sync.configs.appxHashtable
|
||||
|
||||
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
|
||||
$Visible -eq $true -and $Label -eq "AppX install failed" -and $Percent -eq 100
|
||||
}
|
||||
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
|
||||
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "Error" -overlay "warning"*'
|
||||
}
|
||||
$script:sync.ProcessRunning | Should -BeFalse
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,9 +473,11 @@ Describe "Invoke-WPFAppxRemoval entrypoint" {
|
||||
})
|
||||
$script:capturedAppxScriptBlock = $null
|
||||
$script:capturedAppxParameterList = $null
|
||||
$script:appxRemovalProcessRunningAtLaunch = $null
|
||||
|
||||
Mock Show-WinUtilMessage { "OK" }
|
||||
Mock Invoke-WPFRunspace {
|
||||
$script:appxRemovalProcessRunningAtLaunch = $script:sync.ProcessRunning
|
||||
$script:capturedAppxScriptBlock = $ScriptBlock
|
||||
$script:capturedAppxParameterList = $ParameterList
|
||||
[pscustomobject]@{ MockHandle = $true }
|
||||
@@ -189,6 +488,7 @@ Describe "Invoke-WPFAppxRemoval entrypoint" {
|
||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name capturedAppxScriptBlock -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name capturedAppxParameterList -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name appxRemovalProcessRunningAtLaunch -Scope Script -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
It "prompts and exits when no AppX packages are selected" {
|
||||
@@ -203,6 +503,21 @@ Describe "Invoke-WPFAppxRemoval entrypoint" {
|
||||
Should -Invoke -CommandName Invoke-WPFRunspace -Times 0 -Exactly
|
||||
}
|
||||
|
||||
It "prevents overlapping AppX removal operations" {
|
||||
$script:sync.ProcessRunning = $true
|
||||
$script:sync.selectedAppx.Add("WPFAppxExample")
|
||||
|
||||
Invoke-WPFAppxRemoval
|
||||
|
||||
Should -Invoke -CommandName Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
|
||||
$Message -eq "An AppX 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 "passes selected AppX keys and app metadata to the removal runspace" {
|
||||
$script:sync.selectedAppx.Add("WPFAppxExample")
|
||||
$script:sync.configs.appxHashtable["WPFAppxExample"] = [pscustomobject]@{
|
||||
@@ -211,7 +526,10 @@ Describe "Invoke-WPFAppxRemoval entrypoint" {
|
||||
}
|
||||
|
||||
Invoke-WPFAppxRemoval
|
||||
$script:sync.selectedAppx.Add("WPFAppxChangedAfterLaunch")
|
||||
|
||||
$script:appxRemovalProcessRunningAtLaunch | Should -BeTrue
|
||||
$script:capturedAppxParameterList[0][1] | Should -HaveCount 1
|
||||
Should -Invoke -CommandName Show-WinUtilMessage -Times 0 -Exactly
|
||||
Should -Invoke -CommandName Invoke-WPFRunspace -Times 1 -Exactly -ParameterFilter {
|
||||
$ScriptBlock -is [scriptblock] -and
|
||||
@@ -228,6 +546,7 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
|
||||
BeforeEach {
|
||||
$script:sync = [Hashtable]::Synchronized(@{
|
||||
ProcessRunning = $false
|
||||
Form = [pscustomobject]@{ Dispatcher = [pscustomobject]@{} }
|
||||
selectedAppx = [System.Collections.Generic.List[string]]::new()
|
||||
configs = @{
|
||||
appxHashtable = @{}
|
||||
@@ -260,6 +579,8 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
|
||||
Mock Show-WinUtilMessage { "OK" }
|
||||
Mock Write-Host { }
|
||||
Mock Write-WinUtilLog { }
|
||||
Mock Set-WinUtilTweaksProgressIndicator { }
|
||||
Mock Invoke-WPFUIThread { }
|
||||
Mock Stop-Process { }
|
||||
Mock Set-ItemProperty { }
|
||||
Mock Get-AppxPackage {
|
||||
@@ -301,6 +622,77 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
|
||||
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 1 -Exactly -ParameterFilter {
|
||||
$PackageList.Count -eq 1 -and $PackageList[0] -eq "Example.Package"
|
||||
}
|
||||
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
|
||||
$Visible -eq $true -and $Label -eq "Removing Example App (1/1)" -and $Percent -eq 0
|
||||
}
|
||||
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
|
||||
$Visible -eq $true -and $Label -eq "Removed Example App (1/1)" -and $Percent -eq 90
|
||||
}
|
||||
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
|
||||
$Visible -eq $true -and $Label -eq "AppX removal finished" -and $Percent -eq 100
|
||||
}
|
||||
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
|
||||
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"*'
|
||||
}
|
||||
$script:sync.ProcessRunning | Should -BeFalse
|
||||
}
|
||||
|
||||
It "shows failure feedback and clears ProcessRunning when removal fails" {
|
||||
$selected = @("WPFAppxExample")
|
||||
$script:sync.selectedAppx.Add("WPFAppxExample")
|
||||
$script:sync.configs.appxHashtable = $script:apps
|
||||
Mock Remove-WinUtilAPPX { throw "Removal failed" }
|
||||
|
||||
Invoke-WPFAppxRemoval
|
||||
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
|
||||
|
||||
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
|
||||
$Visible -eq $true -and $Label -eq "AppX removal failed" -and $Percent -eq 100
|
||||
}
|
||||
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
|
||||
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "Error" -overlay "warning"*'
|
||||
}
|
||||
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 0 -Exactly
|
||||
$script:sync.ProcessRunning | Should -BeFalse
|
||||
}
|
||||
|
||||
It "removes packages without UI progress during headless autorun" {
|
||||
$selected = @("WPFAppxExample")
|
||||
$script:sync.Remove("Form")
|
||||
$script:sync.selectedAppx.Add("WPFAppxExample")
|
||||
$script:sync.configs.appxHashtable = $script:apps
|
||||
|
||||
Invoke-WPFAppxRemoval
|
||||
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
|
||||
|
||||
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly
|
||||
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 1 -Exactly
|
||||
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 0 -Exactly
|
||||
Should -Invoke -CommandName Invoke-WPFUIThread -Times 0 -Exactly
|
||||
$script:sync.ProcessRunning | Should -BeFalse
|
||||
}
|
||||
|
||||
It "shows failure feedback when provisioned package removal fails" {
|
||||
$selected = @("WPFAppxExample")
|
||||
$script:sync.selectedAppx.Add("WPFAppxExample")
|
||||
$script:sync.configs.appxHashtable = $script:apps
|
||||
Mock Remove-WinUtilProvisionedAPPX { throw "Provisioned removal failed" }
|
||||
|
||||
Invoke-WPFAppxRemoval
|
||||
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
|
||||
|
||||
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
|
||||
$Visible -eq $true -and $Label -eq "AppX removal failed" -and $Percent -eq 100
|
||||
}
|
||||
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 0 -Exactly -ParameterFilter {
|
||||
$Label -eq "AppX removal finished"
|
||||
}
|
||||
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
|
||||
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "Error" -overlay "warning"*'
|
||||
}
|
||||
Should -Invoke -CommandName Invoke-WPFUIThread -Times 0 -Exactly -ParameterFilter {
|
||||
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"*'
|
||||
}
|
||||
$script:sync.ProcessRunning | Should -BeFalse
|
||||
}
|
||||
|
||||
|
||||
@@ -326,6 +326,11 @@ Describe "UI-rendered config entries" {
|
||||
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName $entry.Name -Entry $entry.Value -RequiredFields $requiredFields)) {
|
||||
$invalidEntries.Add($missingField)
|
||||
}
|
||||
|
||||
if ((Test-WinUtilHasProperty -Object $entry.Value -Name "StoreId") -and
|
||||
$entry.Value.StoreId -notmatch '^(?:[A-Z0-9]{12}|XP[A-Z0-9]{12})$') {
|
||||
$invalidEntries.Add("$($entry.Name) has invalid Microsoft Store ID '$($entry.Value.StoreId)'")
|
||||
}
|
||||
}
|
||||
|
||||
if ($invalidEntries.Count -gt 0) {
|
||||
|
||||
@@ -158,6 +158,7 @@ Describe "XAML document" {
|
||||
"WPFSelectAllAppx",
|
||||
"WPFClearAppxSelection",
|
||||
"WPFBackToTweaks",
|
||||
"WPFInstallSelectedAppx",
|
||||
"WPFRemoveSelectedAppx"
|
||||
)
|
||||
|
||||
@@ -240,9 +241,11 @@ Describe "XAML document" {
|
||||
$openButton.GetAttribute("Content").Trim() | Should -Be "AppX Removal"
|
||||
$openAppxIndex | Should -Be ($getInstalledIndex + 1)
|
||||
$appxTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFBackToTweaks"]') | Should -Not -BeNullOrEmpty
|
||||
$appxTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFInstallSelectedAppx"]') | Should -Not -BeNullOrEmpty
|
||||
$appxTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFRemoveSelectedAppx"]') | Should -Not -BeNullOrEmpty
|
||||
$buttonSource | Should -Match '"WPFAppxRemoval"\s*\{Invoke-WPFTab "WPFTab6BT"\}'
|
||||
$buttonSource | Should -Match '"WPFBackToTweaks"\s*\{Invoke-WPFTab "WPFTab2BT"\}'
|
||||
$buttonSource | Should -Match '"WPFInstallSelectedAppx"\s*\{Invoke-WPFAppxInstall\}'
|
||||
$tabSource | Should -Match '\$sync\.\$tabNav\.Items\[\$tabNumber\]\.IsSelected = \$true'
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user