mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-09 17:41:14 +10:00
Expand Pester coverage across install, tweaks, and UI workflows (#4792)
* Harden Pester CI and discard runspace return values * Add prioritized test backlog * Add WinUtil action logging * Add config integrity tests * Add XAML control wiring tests * Add registry and service helper tests * Add package manager tests * Add runspace behavior tests * Add tweak orchestration tests * Add install workflow tests * Add update profile tests * Add AppX removal tests * Log package installer output * Add UI state helper tests * Pin Pester test runner version * Expand Win11 Creator tests * Add preferences and theme tests * Add search filter tests * Add compile contract tests * Use single WinUtil session log * Remove installer output logging helper * Handle locked WinUtil transcript log * Avoid appending to active transcript log * Log install uninstall package identities
This commit is contained in:
@@ -27,13 +27,16 @@ jobs:
|
|||||||
- name: Install Pester
|
- name: Install Pester
|
||||||
run: |
|
run: |
|
||||||
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
|
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
|
||||||
Install-Module -Name Pester -Force -SkipPublisherCheck -AllowClobber
|
Install-Module -Name Pester -RequiredVersion 5.8.0 -Force -SkipPublisherCheck -AllowClobber
|
||||||
|
Import-Module Pester -RequiredVersion 5.8.0 -Force
|
||||||
|
Get-Module Pester | Select-Object Name, Version, Path
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Run Pester tests
|
- name: Run Pester tests
|
||||||
run: |
|
run: |
|
||||||
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
|
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
|
||||||
Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed
|
Import-Module Pester -RequiredVersion 5.8.0 -Force
|
||||||
|
Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed -CI
|
||||||
|
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Microsoft.PowerShell.ConsoleHost.dll
|
|||||||
winutil.exe.config
|
winutil.exe.config
|
||||||
winutil.ps1
|
winutil.ps1
|
||||||
binary/
|
binary/
|
||||||
|
testResults.xml
|
||||||
|
|
||||||
# general software/os specific
|
# general software/os specific
|
||||||
desktop.ini
|
desktop.ini
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ WinUtil is a Windows PowerShell utility with a WPF interface. The repository is
|
|||||||
```
|
```
|
||||||
- Run tests:
|
- Run tests:
|
||||||
```powershell
|
```powershell
|
||||||
|
Import-Module Pester -RequiredVersion 5.8.0 -Force
|
||||||
Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed
|
Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed
|
||||||
```
|
```
|
||||||
- Run Script Analyzer with project settings when available:
|
- Run Script Analyzer with project settings when available:
|
||||||
@@ -182,4 +183,8 @@ Proceed without asking when:
|
|||||||
When the user corrects an agent approach, add or tighten one concrete rule here before ending the session. Keep this section short and prune rules that no longer matter.
|
When the user corrects an agent approach, add or tighten one concrete rule here before ending the session. Keep this section short and prune rules that no longer matter.
|
||||||
|
|
||||||
- Keep `winutil.ps1` generated-only: change source files, compile to verify, and never stage the generated script.
|
- Keep `winutil.ps1` generated-only: change source files, compile to verify, and never stage the generated script.
|
||||||
|
- Keep WinUtil runtime logging in the existing timestamped `%LocalAppData%\winutil\logs\winutil_*.log` session file; do not create a separate root `winutil.log`.
|
||||||
|
- Import Pester 5.8.0 before running tests so `Invoke-Pester -Output Detailed -CI` does not resolve to Windows' inbox Pester 3.4.0.
|
||||||
|
- Keep package install/uninstall process launches simple unless explicitly requested; do not add a separate stdout/stderr process logging helper for winget or Chocolatey.
|
||||||
|
- When the active log file is owned by `Start-Transcript`, do not call `Add-Content` against that file; write to host output so the transcript captures the line in the same log file without recording a terminating-error diagnostic.
|
||||||
|
- Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity.
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ Expected validation for source changes:
|
|||||||
|
|
||||||
- `.\Compile.ps1` verifies the compiler can generate `winutil.ps1`.
|
- `.\Compile.ps1` verifies the compiler can generate `winutil.ps1`.
|
||||||
- `.\Compile.ps1 -Run` compiles and launches the generated utility for manual GUI verification.
|
- `.\Compile.ps1 -Run` compiles and launches the generated utility for manual GUI verification.
|
||||||
- `Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed` runs the Pester suite.
|
- `Install-Module -Name Pester -RequiredVersion 5.8.0 -Scope CurrentUser -Force -SkipPublisherCheck` installs the supported Pester version.
|
||||||
|
- `Import-Module Pester -RequiredVersion 5.8.0 -Force; Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed -CI` runs the Pester suite.
|
||||||
- GitHub Actions also runs a compile check and PowerShell Script Analyzer with `lint/PSScriptAnalyser.ps1`.
|
- GitHub Actions also runs a compile check and PowerShell Script Analyzer with `lint/PSScriptAnalyser.ps1`.
|
||||||
|
|
||||||
The generated `winutil.ps1` may appear locally after compile. It remains ignored build output and must not be committed.
|
The generated `winutil.ps1` may appear locally after compile. It remains ignored build output and must not be committed.
|
||||||
@@ -102,4 +103,3 @@ The generated `winutil.ps1` may appear locally after compile. It remains ignored
|
|||||||
## Release Artifact
|
## Release Artifact
|
||||||
|
|
||||||
GitHub Actions is responsible for producing the release `winutil.ps1` from repository sources. A release should be considered valid only if the generated script came from the compile process, not from direct manual edits to `winutil.ps1`.
|
GitHub Actions is responsible for producing the release `winutil.ps1` from repository sources. A release should be considered valid only if the generated script came from the compile process, not from direct manual edits to `winutil.ps1`.
|
||||||
|
|
||||||
|
|||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
# Test TODO
|
||||||
|
|
||||||
|
Current suite status: broad parser, import, config JSON, compile, and runspace smoke coverage exists, but runtime behavior coverage is still very thin. Work from the top down; the first items are the highest-value gaps.
|
||||||
|
|
||||||
|
## P0 - Highest Priority
|
||||||
|
|
||||||
|
- [x] Add config reference integrity tests.
|
||||||
|
- Verify every preset entry points to an existing tweak, feature, app, appx item, or supported action.
|
||||||
|
- Verify `appnavigation.json` entries point to real tabs/panels and valid config groups.
|
||||||
|
- Verify tweak, feature, DNS, appx, and theme entries include all fields used by the UI/rendering code.
|
||||||
|
- Verify all embedded script strings in `config/*.json` parse as PowerShell when wrapped as scriptblocks.
|
||||||
|
|
||||||
|
- [x] Add XAML/control wiring tests.
|
||||||
|
- Load `xaml/inputXML.xaml` as XML and verify expected named controls exist.
|
||||||
|
- Verify controls referenced through `$sync["Name"]`, `$sync.Name`, or event handlers exist in XAML or are intentionally created dynamically.
|
||||||
|
- Verify `Invoke-WPF*` handler names used by buttons/tabs correspond to real functions.
|
||||||
|
- Verify core tabs, search controls, install panels, tweak panels, and Win11 Creator controls are present.
|
||||||
|
|
||||||
|
- [x] Add mock-based tests for registry and service helpers.
|
||||||
|
- Cover `Set-WinUtilRegistry` set/remove paths with `Mock New-Item`, `Mock Set-ItemProperty`, and `Mock Remove-ItemProperty`.
|
||||||
|
- Cover `Set-WinUtilService` for missing services, startup type changes, and no-op behavior.
|
||||||
|
- Assert command parameters instead of touching the real registry or services.
|
||||||
|
|
||||||
|
- [x] Add package selection and package manager tests.
|
||||||
|
- Cover `Get-WinUtilSelectedPackages` for winget-only, choco-only, mixed, `na`, empty, duplicate, and missing package entries.
|
||||||
|
- Cover `Test-WinUtilPackageManager` with mocked `Get-Command`.
|
||||||
|
- Cover `Install-WinUtilProgramWinget` and `Install-WinUtilProgramChoco` with mocked `Start-Process`, including install/uninstall arguments.
|
||||||
|
|
||||||
|
- [x] Add runspace behavior tests beyond the smoke test.
|
||||||
|
- Cover no argument list, one named parameter, multiple named parameters, and scriptblock failures.
|
||||||
|
- Assert callers receive a single `IAsyncResult`.
|
||||||
|
- Verify failure output/errors can be retrieved by the owning PowerShell instance.
|
||||||
|
- Add focused tests for public functions that call `Invoke-WPFRunspace` without executing their destructive inner work.
|
||||||
|
|
||||||
|
## P1 - Important Workflow Coverage
|
||||||
|
|
||||||
|
- [x] Add tweak execution orchestration tests.
|
||||||
|
- Cover `Invoke-WinUtilTweaks` with mocked `Invoke-WinUtilScript`, `Set-WinUtilRegistry`, `Set-WinUtilService`, and `Set-WinUtilDNS`.
|
||||||
|
- Verify undo mode uses `OriginalValue` and `OriginalType`.
|
||||||
|
- Verify DNS provider and progress counters are passed through correctly.
|
||||||
|
|
||||||
|
- [x] Add install/uninstall workflow tests.
|
||||||
|
- Cover `Invoke-WPFInstall` and `Invoke-WPFUnInstall` with mocked package sorting and mocked installers.
|
||||||
|
- Verify empty selection prompts and exits.
|
||||||
|
- Verify process-running guard prevents a second install/uninstall.
|
||||||
|
- Verify taskbar/progress cleanup happens on success and error.
|
||||||
|
|
||||||
|
- [x] Add update profile tests.
|
||||||
|
- Cover `Invoke-WPFUpdatesdisable`, `Invoke-WPFUpdatesdefault`, and `Invoke-WPFUpdatessecurity` with mocks for registry, services, scheduled tasks, and process calls.
|
||||||
|
- Assert intended registry paths, values, service startup types, and scheduled task paths.
|
||||||
|
- Keep all tests non-mutating.
|
||||||
|
|
||||||
|
- [x] Add AppX removal tests.
|
||||||
|
- Cover `Remove-WinUtilAPPX` with mocked `Get-AppxPackage` and `Remove-AppxPackage`.
|
||||||
|
- Cover `Invoke-WPFAppxRemoval` selection behavior and runspace parameter passing.
|
||||||
|
- Verify empty selection is handled without removal calls.
|
||||||
|
|
||||||
|
- [x] Add UI selection/state helper tests.
|
||||||
|
- Cover `Update-WinUtilSelections`, `Reset-WPFCheckBoxes`, `Invoke-WPFSelectedCheckboxesUpdate`, and `Invoke-WPFToggleAllCategories`.
|
||||||
|
- Use small fake checkbox/control objects instead of WPF windows where possible.
|
||||||
|
- Verify selected apps, tweaks, toggles, appx, and features stay in sync with checkbox state.
|
||||||
|
|
||||||
|
## P2 - Domain-Specific Coverage
|
||||||
|
|
||||||
|
- [x] Expand Win11 Creator tests.
|
||||||
|
- Test edition name to edition ID mapping.
|
||||||
|
- Test `ei.cfg` and `PID.txt` generation/removal behavior in a temp directory.
|
||||||
|
- Test autounattend conversion preserves required nodes and removes unsupported product key paths.
|
||||||
|
- Test driver injection branching with mocked DISM commands.
|
||||||
|
- Test ISO export fallback when `oscdimg.exe` is missing and winget install fails.
|
||||||
|
|
||||||
|
- [x] Add preferences/theme tests.
|
||||||
|
- Cover `Set-Preferences` loading, saving, old preference cleanup, and defaults.
|
||||||
|
- Cover theme resource updates without opening the full GUI.
|
||||||
|
- Verify theme config contains required brushes/colors used by the theme code.
|
||||||
|
|
||||||
|
- [x] Add search/filter tests.
|
||||||
|
- Cover `Find-AppsByNameOrDescription` and `Find-TweaksByNameOrDescription` with fake UI item trees.
|
||||||
|
- Verify empty search restores visibility.
|
||||||
|
- Verify category labels and panels hide/show correctly.
|
||||||
|
- Verify description and display-name matches both work.
|
||||||
|
|
||||||
|
- [x] Add compile contract tests.
|
||||||
|
- Verify compiled config key transformation for `applications.json` adds `WPFInstall` prefixes.
|
||||||
|
- Verify source ordering in `winutil.ps1`: start script, functions, configs, XAML, autounattend XML, main script.
|
||||||
|
- Verify generated build date placeholder is replaced.
|
||||||
|
- Verify generated script does not contain unresolved `#{replaceme}`.
|
||||||
|
|
||||||
|
## P3 - Nice To Have
|
||||||
|
|
||||||
|
- [ ] Add Script Analyzer as a failing CI gate once existing findings are triaged.
|
||||||
|
- [ ] Add docs generation/link checks for `docs/content`.
|
||||||
|
- [ ] Add coverage reporting in CI as informational output.
|
||||||
|
- [ ] Add a lightweight test helper module for fake `$sync`, fake WPF controls, and common mocks.
|
||||||
|
- [ ] Add regression tests whenever a production bug is fixed.
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
|
||||||
|
- Tests must not mutate the real registry, services, scheduled tasks, package managers, AppX packages, USB disks, ISOs, or user profile state.
|
||||||
|
- Prefer Pester mocks and temp directories for system-facing code.
|
||||||
|
- Keep generated `winutil.ps1` ignored and never commit it.
|
||||||
|
- Run the full gate before marking test work complete:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Import-Module Pester -RequiredVersion 5.8.0 -Force
|
||||||
|
Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed -CI
|
||||||
|
```
|
||||||
@@ -578,7 +578,9 @@ Tests are in `/pester/`:
|
|||||||
|
|
||||||
Run tests:
|
Run tests:
|
||||||
```powershell
|
```powershell
|
||||||
Invoke-Pester
|
Install-Module -Name Pester -RequiredVersion 5.8.0 -Scope CurrentUser -Force -SkipPublisherCheck
|
||||||
|
Import-Module Pester -RequiredVersion 5.8.0 -Force
|
||||||
|
Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed -CI
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build Process
|
## Build Process
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
function Get-WinUtilPackageLogSummary {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$Packages,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[PackageManagers]$Preference
|
||||||
|
)
|
||||||
|
|
||||||
|
@($Packages | ForEach-Object {
|
||||||
|
$package = $_
|
||||||
|
$packageName = @($package.Name, $package.Description, $package.winget, $package.choco) |
|
||||||
|
Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) -and $_ -ne "na" } |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$packageName)) {
|
||||||
|
$packageName = "Unknown package"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Preference -eq [PackageManagers]::Choco -and -not [string]::IsNullOrWhiteSpace([string]$package.choco) -and $package.choco -ne "na") {
|
||||||
|
"$packageName (choco: $($package.choco))"
|
||||||
|
} elseif (-not [string]::IsNullOrWhiteSpace([string]$package.winget) -and $package.winget -ne "na") {
|
||||||
|
"$packageName (winget: $($package.winget))"
|
||||||
|
} else {
|
||||||
|
"$packageName (no package id)"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -21,17 +21,32 @@ function Get-WinUtilSelectedPackages {
|
|||||||
$packages[[PackageManagers]::Winget] = $packagesWinget
|
$packages[[PackageManagers]::Winget] = $packagesWinget
|
||||||
$packages[[PackageManagers]::Choco] = $packagesChoco
|
$packages[[PackageManagers]::Choco] = $packagesChoco
|
||||||
|
|
||||||
|
function Add-PackageId {
|
||||||
|
param(
|
||||||
|
[System.Collections.ArrayList]$Target,
|
||||||
|
$PackageId
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$PackageId) -or $PackageId -eq "na") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $Target.Contains($PackageId)) {
|
||||||
|
$null = $Target.Add($PackageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($package in $PackageList) {
|
foreach ($package in $PackageList) {
|
||||||
switch ($Preference) {
|
switch ($Preference) {
|
||||||
"Choco" {
|
"Choco" {
|
||||||
if ($package.choco -eq "na") {
|
if ([string]::IsNullOrWhiteSpace([string]$package.choco) -or $package.choco -eq "na") {
|
||||||
$null = $packagesWinget.add($package.winget)
|
Add-PackageId -Target $packagesWinget -PackageId $package.winget
|
||||||
} else {
|
} else {
|
||||||
$null = $packagesChoco.add($package.choco)
|
Add-PackageId -Target $packagesChoco -PackageId $package.choco
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"Winget" {
|
"Winget" {
|
||||||
$null = $packagesWinget.add($package.winget)
|
Add-PackageId -Target $packagesWinget -PackageId $package.winget
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ function Install-WinUtilProgramChoco {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if ($Action -eq 'Install') {
|
if ($Action -eq 'Install') {
|
||||||
Start-Process -FilePath choco -ArgumentList "install $Programs -y" -NoNewWindow -Wait
|
$arguments = "install $Programs -y"
|
||||||
} else {
|
} else {
|
||||||
Start-Process -FilePath choco -ArgumentList "uninstall $Programs -y" -NoNewWindow -Wait
|
$arguments = "uninstall $Programs -y"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Write-WinUtilLog -Component "Package" -Message "$Action choco package(s): $($Programs -join ', ')"
|
||||||
|
$process = Start-Process -FilePath choco -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
|
Write-WinUtilLog -Component "Package" -Message "$Action choco package(s) completed: $($Programs -join ', ') (exit code: $($process.ExitCode))"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ Function Install-WinUtilProgramWinget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($Action -eq 'Install') {
|
if ($Action -eq 'Install') {
|
||||||
Start-Process -FilePath winget -ArgumentList @("install", "--id", $program, "--accept-package-agreements", "--accept-source-agreements", "--source", $source, "--silent") -NoNewWindow -Wait
|
$arguments = @("install", "--id", $program, "--accept-package-agreements", "--accept-source-agreements", "--source", $source, "--silent")
|
||||||
} else {
|
} else {
|
||||||
Start-Process -FilePath winget -ArgumentList @("uninstall", "--id", $program, "--source", $source, "--silent") -NoNewWindow -Wait
|
$arguments = @("uninstall", "--id", $program, "--source", $source, "--silent")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Write-WinUtilLog -Component "Package" -Message "$Action winget package: $program (source: $source)"
|
||||||
|
$process = Start-Process -FilePath winget -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
|
Write-WinUtilLog -Component "Package" -Message "$Action winget package completed: $program (exit code: $($process.ExitCode))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
function Invoke-WinUtilFeatureInstall ($CheckBox) {
|
function Invoke-WinUtilFeatureInstall ($CheckBox) {
|
||||||
|
Write-WinUtilLog -Component "Feature" -Message "Applying feature action: $CheckBox"
|
||||||
|
|
||||||
if ($sync.configs.feature.$CheckBox.feature) {
|
if ($sync.configs.feature.$CheckBox.feature) {
|
||||||
foreach ($feature in $sync.configs.feature.$CheckBox.feature) {
|
foreach ($feature in $sync.configs.feature.$CheckBox.feature) {
|
||||||
Write-Host "Installing $feature"
|
Write-Host "Installing $feature"
|
||||||
|
Write-WinUtilLog -Component "Feature" -Message "Enabling Windows optional feature: $feature"
|
||||||
Enable-WindowsOptionalFeature -Online -FeatureName $feature -All -NoRestart -ErrorAction Stop
|
Enable-WindowsOptionalFeature -Online -FeatureName $feature -All -NoRestart -ErrorAction Stop
|
||||||
|
Write-WinUtilLog -Component "Feature" -Message "Enabled Windows optional feature: $feature"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($sync.configs.feature.$CheckBox.InvokeScript) {
|
if ($sync.configs.feature.$CheckBox.InvokeScript) {
|
||||||
foreach ($script in $sync.configs.feature.$CheckBox.InvokeScript) {
|
foreach ($script in $sync.configs.feature.$CheckBox.InvokeScript) {
|
||||||
Write-Host "Running Script for $CheckBox"
|
Write-Host "Running Script for $CheckBox"
|
||||||
|
Write-WinUtilLog -Component "Feature" -Message "Running feature script for: $CheckBox"
|
||||||
Invoke-Command -ScriptBlock ([scriptblock]::Create($script)) -ErrorAction Stop
|
Invoke-Command -ScriptBlock ([scriptblock]::Create($script)) -ErrorAction Stop
|
||||||
|
Write-WinUtilLog -Component "Feature" -Message "Completed feature script for: $CheckBox"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-WinUtilLog -Component "Feature" -Message "Feature action completed: $CheckBox"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,23 +22,30 @@ function Invoke-WinUtilScript {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Write-Host "Running Script for $Name"
|
Write-Host "Running Script for $Name"
|
||||||
|
Write-WinUtilLog -Component "Script" -Message "Running script for $Name"
|
||||||
Invoke-Command $scriptblock -ErrorAction Stop
|
Invoke-Command $scriptblock -ErrorAction Stop
|
||||||
|
Write-WinUtilLog -Component "Script" -Message "Completed script for $Name"
|
||||||
} catch [System.Management.Automation.CommandNotFoundException] {
|
} catch [System.Management.Automation.CommandNotFoundException] {
|
||||||
Write-Warning "The specified command was not found."
|
Write-Warning "The specified command was not found."
|
||||||
Write-Warning $PSItem.Exception.message
|
Write-Warning $PSItem.Exception.message
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "Script" -Message "Command not found while running script for $Name`: $($PSItem.Exception.Message)"
|
||||||
} catch [System.Management.Automation.RuntimeException] {
|
} catch [System.Management.Automation.RuntimeException] {
|
||||||
Write-Warning "A runtime exception occurred."
|
Write-Warning "A runtime exception occurred."
|
||||||
Write-Warning $PSItem.Exception.message
|
Write-Warning $PSItem.Exception.message
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "Script" -Message "Runtime exception while running script for $Name`: $($PSItem.Exception.Message)"
|
||||||
} catch [System.Security.SecurityException] {
|
} catch [System.Security.SecurityException] {
|
||||||
Write-Warning "A security exception occurred."
|
Write-Warning "A security exception occurred."
|
||||||
Write-Warning $PSItem.Exception.message
|
Write-Warning $PSItem.Exception.message
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "Script" -Message "Security exception while running script for $Name`: $($PSItem.Exception.Message)"
|
||||||
} catch [System.UnauthorizedAccessException] {
|
} catch [System.UnauthorizedAccessException] {
|
||||||
Write-Warning "Access denied. You do not have permission to perform this operation."
|
Write-Warning "Access denied. You do not have permission to perform this operation."
|
||||||
Write-Warning $PSItem.Exception.message
|
Write-Warning $PSItem.Exception.message
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "Script" -Message "Access denied while running script for $Name`: $($PSItem.Exception.Message)"
|
||||||
} catch {
|
} catch {
|
||||||
# Generic catch block to handle any other type of exception
|
# Generic catch block to handle any other type of exception
|
||||||
Write-Warning "Unable to run script for $Name due to unhandled exception."
|
Write-Warning "Unable to run script for $Name due to unhandled exception."
|
||||||
Write-Warning $psitem.Exception.StackTrace
|
Write-Warning $psitem.Exception.StackTrace
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "Script" -Message "Unhandled exception while running script for $Name`: $($psitem.Exception.Message)"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ function Invoke-WinUtilTweaks {
|
|||||||
$KeepServiceStartup = $true
|
$KeepServiceStartup = $true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
$action = if ($undo) { "Undo" } else { "Apply" }
|
||||||
|
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak: $CheckBox"
|
||||||
|
|
||||||
if ($undo) {
|
if ($undo) {
|
||||||
$Values = @{
|
$Values = @{
|
||||||
Registry = "OriginalValue"
|
Registry = "OriginalValue"
|
||||||
@@ -77,4 +80,5 @@ function Invoke-WinUtilTweaks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak completed: $CheckBox"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ function Remove-WinUtilAPPX {
|
|||||||
)
|
)
|
||||||
|
|
||||||
Write-Host "Removing $Name"
|
Write-Host "Removing $Name"
|
||||||
|
Write-WinUtilLog -Component "AppX" -Message "Removing AppX package pattern: $Name"
|
||||||
Get-AppxPackage $Name -AllUsers | Remove-AppxPackage -AllUsers
|
Get-AppxPackage $Name -AllUsers | Remove-AppxPackage -AllUsers
|
||||||
Get-AppxProvisionedPackage -Online | Where-Object DisplayName -like $Name | Remove-AppxProvisionedPackage -Online
|
Get-AppxProvisionedPackage -Online | Where-Object DisplayName -like $Name | Remove-AppxProvisionedPackage -Online
|
||||||
|
Write-WinUtilLog -Component "AppX" -Message "AppX removal completed for package pattern: $Name"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,22 +12,31 @@ function Set-WinUtilDNS {
|
|||||||
|
|
||||||
#>
|
#>
|
||||||
param($DNSProvider)
|
param($DNSProvider)
|
||||||
if($DNSProvider -eq "Default") {return}
|
if($DNSProvider -eq "Default") {
|
||||||
|
Write-WinUtilLog -Component "DNS" -Message "DNS provider is Default; no DNS changes applied."
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
$Adapters = Get-NetAdapter | Where-Object {$_.Status -eq "Up"}
|
$Adapters = Get-NetAdapter | Where-Object {$_.Status -eq "Up"}
|
||||||
Write-Host "Ensuring DNS is set to $DNSProvider on the following interfaces:"
|
Write-Host "Ensuring DNS is set to $DNSProvider on the following interfaces:"
|
||||||
Write-Host $($Adapters | Out-String)
|
Write-Host $($Adapters | Out-String)
|
||||||
|
Write-WinUtilLog -Component "DNS" -Message "Setting DNS provider to $DNSProvider for $(@($Adapters).Count) active adapter(s)."
|
||||||
|
|
||||||
Foreach ($Adapter in $Adapters) {
|
Foreach ($Adapter in $Adapters) {
|
||||||
if($DNSProvider -eq "DHCP") {
|
if($DNSProvider -eq "DHCP") {
|
||||||
|
Write-WinUtilLog -Component "DNS" -Message "Resetting DNS to DHCP on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex))."
|
||||||
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ResetServerAddresses
|
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ResetServerAddresses
|
||||||
} else {
|
} else {
|
||||||
|
Write-WinUtilLog -Component "DNS" -Message "Setting IPv4 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($sync.configs.dns.$DNSProvider.Primary), $($sync.configs.dns.$DNSProvider.Secondary)."
|
||||||
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ("$($sync.configs.dns.$DNSProvider.Primary)", "$($sync.configs.dns.$DNSProvider.Secondary)")
|
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ("$($sync.configs.dns.$DNSProvider.Primary)", "$($sync.configs.dns.$DNSProvider.Secondary)")
|
||||||
|
Write-WinUtilLog -Component "DNS" -Message "Setting IPv6 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($sync.configs.dns.$DNSProvider.Primary6), $($sync.configs.dns.$DNSProvider.Secondary6)."
|
||||||
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ("$($sync.configs.dns.$DNSProvider.Primary6)", "$($sync.configs.dns.$DNSProvider.Secondary6)")
|
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ("$($sync.configs.dns.$DNSProvider.Primary6)", "$($sync.configs.dns.$DNSProvider.Secondary6)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-WinUtilLog -Component "DNS" -Message "DNS provider change completed: $DNSProvider"
|
||||||
} catch {
|
} catch {
|
||||||
Write-Warning "Unable to set DNS Provider due to an unhandled exception."
|
Write-Warning "Unable to set DNS Provider due to an unhandled exception."
|
||||||
Write-Warning $psitem.Exception.StackTrace
|
Write-Warning $psitem.Exception.StackTrace
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "DNS" -Message "Unable to set DNS provider $DNSProvider`: $($psitem.Exception.Message)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,25 +32,32 @@ function Set-WinUtilRegistry {
|
|||||||
|
|
||||||
If (!(Test-Path $Path)) {
|
If (!(Test-Path $Path)) {
|
||||||
Write-Host "$Path was not found. Creating..."
|
Write-Host "$Path was not found. Creating..."
|
||||||
|
Write-WinUtilLog -Component "Registry" -Message "Creating registry path: $Path"
|
||||||
New-Item -Path $Path -Force -ErrorAction Stop | Out-Null
|
New-Item -Path $Path -Force -ErrorAction Stop | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($Value -ne "<RemoveEntry>") {
|
if ($Value -ne "<RemoveEntry>") {
|
||||||
Write-Host "Set $Path\$Name to $Value"
|
Write-Host "Set $Path\$Name to $Value"
|
||||||
|
Write-WinUtilLog -Component "Registry" -Message "Setting $Path\$Name ($Type) to $Value"
|
||||||
Set-ItemProperty -Path $Path -Name $Name -Type $Type -Value $Value -Force -ErrorAction Stop | Out-Null
|
Set-ItemProperty -Path $Path -Name $Name -Type $Type -Value $Value -Force -ErrorAction Stop | Out-Null
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
Write-Host "Remove $Path\$Name"
|
Write-Host "Remove $Path\$Name"
|
||||||
|
Write-WinUtilLog -Component "Registry" -Message "Removing $Path\$Name"
|
||||||
Remove-ItemProperty -Path $Path -Name $Name -Force -ErrorAction Stop | Out-Null
|
Remove-ItemProperty -Path $Path -Name $Name -Force -ErrorAction Stop | Out-Null
|
||||||
}
|
}
|
||||||
} catch [System.Security.SecurityException] {
|
} catch [System.Security.SecurityException] {
|
||||||
Write-Warning "Unable to set $Path\$Name to $Value due to a Security Exception."
|
Write-Warning "Unable to set $Path\$Name to $Value due to a Security Exception."
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "Registry" -Message "Security exception while changing $Path\$Name to $Value`: $($psitem.Exception.Message)"
|
||||||
} catch [System.Management.Automation.ItemNotFoundException] {
|
} catch [System.Management.Automation.ItemNotFoundException] {
|
||||||
Write-Warning $psitem.Exception.ErrorRecord
|
Write-Warning $psitem.Exception.ErrorRecord
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "Registry" -Message "Registry item not found while changing $Path\$Name`: $($psitem.Exception.Message)"
|
||||||
} catch [System.UnauthorizedAccessException] {
|
} catch [System.UnauthorizedAccessException] {
|
||||||
Write-Warning $psitem.Exception.Message
|
Write-Warning $psitem.Exception.Message
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "Registry" -Message "Unauthorized while changing $Path\$Name`: $($psitem.Exception.Message)"
|
||||||
} catch {
|
} catch {
|
||||||
Write-Warning "Unable to set $Name due to unhandled exception."
|
Write-Warning "Unable to set $Name due to unhandled exception."
|
||||||
Write-Warning $psitem.Exception.StackTrace
|
Write-Warning $psitem.Exception.StackTrace
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "Registry" -Message "Unhandled exception while changing $Path\$Name`: $($psitem.Exception.Message)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,21 +20,33 @@ Function Set-WinUtilService {
|
|||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
Write-Host "Setting Service $Name to $StartupType"
|
Write-Host "Setting Service $Name to $StartupType"
|
||||||
|
Write-WinUtilLog -Component "Service" -Message "Setting service $Name startup type to $StartupType"
|
||||||
|
|
||||||
# Check if the service exists
|
# Check if the service exists
|
||||||
$service = Get-Service -Name $Name -ErrorAction Stop
|
$service = Get-Service -Name $Name -ErrorAction Stop
|
||||||
|
|
||||||
|
if (($service.PSObject.Properties.Name -contains "StartType") -and ([string]$service.StartType -eq [string]$StartupType) ) {
|
||||||
|
Write-Host "Service $Name is already set to $StartupType"
|
||||||
|
Write-WinUtilLog -Component "Service" -Message "Service $Name startup type is already $StartupType; no change needed."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
# Service exists, proceed with changing properties -- while handling auto delayed start for PWSH 5
|
# Service exists, proceed with changing properties -- while handling auto delayed start for PWSH 5
|
||||||
if (($PSVersionTable.PSVersion.Major -lt 7) -and ($StartupType -eq "AutomaticDelayedStart")) {
|
if (($PSVersionTable.PSVersion.Major -lt 7) -and ($StartupType -eq "AutomaticDelayedStart")) {
|
||||||
sc.exe config $Name start=delayed-auto
|
sc.exe config $Name start=delayed-auto
|
||||||
} else {
|
} else {
|
||||||
$service | Set-Service -StartupType $StartupType -ErrorAction Stop
|
$service | Set-Service -StartupType $StartupType -ErrorAction Stop
|
||||||
}
|
}
|
||||||
} catch [System.ServiceProcess.ServiceNotFoundException] {
|
Write-WinUtilLog -Component "Service" -Message "Service $Name startup type set to $StartupType"
|
||||||
Write-Warning "Service $Name was not found."
|
|
||||||
} catch {
|
} catch {
|
||||||
|
if ($_.FullyQualifiedErrorId -like "NoServiceFoundForGivenName,*") {
|
||||||
|
Write-Warning "Service $Name was not found."
|
||||||
|
Write-WinUtilLog -Level "WARN" -Component "Service" -Message "Service $Name was not found."
|
||||||
|
} else {
|
||||||
Write-Warning "Unable to set $Name due to unhandled exception."
|
Write-Warning "Unable to set $Name due to unhandled exception."
|
||||||
Write-Warning $_.Exception.Message
|
Write-Warning $_.Exception.Message
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "Service" -Message "Unable to set service $Name to $StartupType`: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
function Show-WinUtilMessage {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Shows a WinUtil message box and returns the selected result.
|
||||||
|
#>
|
||||||
|
param (
|
||||||
|
[string]$Message,
|
||||||
|
[string]$Title = "Winutil",
|
||||||
|
$Button = "OK",
|
||||||
|
$Icon = "Information"
|
||||||
|
)
|
||||||
|
|
||||||
|
[System.Windows.MessageBox]::Show($Message, $Title, $Button, $Icon)
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
function Write-WinUtilLog {
|
||||||
|
<#
|
||||||
|
|
||||||
|
.SYNOPSIS
|
||||||
|
Writes a timestamped WinUtil log entry to the active session log.
|
||||||
|
|
||||||
|
.PARAMETER Message
|
||||||
|
The message to write.
|
||||||
|
|
||||||
|
.PARAMETER Level
|
||||||
|
The severity level for the log entry.
|
||||||
|
|
||||||
|
.PARAMETER Component
|
||||||
|
The WinUtil component producing the log entry.
|
||||||
|
|
||||||
|
#>
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Message,
|
||||||
|
|
||||||
|
[ValidateSet("INFO", "WARN", "ERROR", "DEBUG")]
|
||||||
|
[string]$Level = "INFO",
|
||||||
|
|
||||||
|
[string]$Component = "WinUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$logPath = $null
|
||||||
|
$transcriptPath = $null
|
||||||
|
if ($null -ne $sync -and $sync.ContainsKey("logPath")) {
|
||||||
|
$logPath = $sync.logPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $sync -and $sync.ContainsKey("transcriptPath")) {
|
||||||
|
$transcriptPath = $sync.transcriptPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($logPath) -and -not [string]::IsNullOrWhiteSpace($transcriptPath)) {
|
||||||
|
$logPath = $transcriptPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($logPath) -and $null -ne $sync -and $sync.ContainsKey("winutildir")) {
|
||||||
|
$logDirectory = Join-Path $sync.winutildir "logs"
|
||||||
|
$logPath = Join-Path $logDirectory "winutil_$(Get-Date -Format "yyyy-MM-dd_HH-mm-ss").log"
|
||||||
|
$sync.logPath = $logPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($logPath) -and -not [string]::IsNullOrWhiteSpace($env:LocalAppData)) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($script:WinUtilLogPath)) {
|
||||||
|
$logDirectory = Join-Path (Join-Path $env:LocalAppData "winutil") "logs"
|
||||||
|
$script:WinUtilLogPath = Join-Path $logDirectory "winutil_$(Get-Date -Format "yyyy-MM-dd_HH-mm-ss").log"
|
||||||
|
}
|
||||||
|
$logPath = $script:WinUtilLogPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($logPath)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$logDirectory = Split-Path -Path $logPath -Parent
|
||||||
|
if (-not (Test-Path $logDirectory)) {
|
||||||
|
New-Item -Path $logDirectory -ItemType Directory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
|
||||||
|
$line = "[$timestamp] [$Level] [$Component] $Message"
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($transcriptPath) -and $logPath -eq $transcriptPath) {
|
||||||
|
Write-Host $line
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Add-Content -Path $logPath -Value $line -Encoding UTF8 -ErrorAction Stop
|
||||||
|
} catch [System.IO.IOException] {
|
||||||
|
Write-Host $line
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Warning "Unable to write WinUtil log entry: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
function Invoke-WPFAppxRemoval {
|
function Invoke-WPFAppxRemoval {
|
||||||
if (-not ($sync.selectedAppx)) {
|
if ($null -eq $sync.selectedAppx -or $sync.selectedAppx.Count -eq 0) {
|
||||||
[System.Windows.Forms.MessageBox]::Show("No AppX Package selected","Error","OK","Error")
|
Show-WinUtilMessage -Message "No AppX Package selected" -Title "Error" -Button "OK" -Icon "Error"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,26 +11,32 @@ function Invoke-WPFAppxRemoval {
|
|||||||
param($selected, $apps)
|
param($selected, $apps)
|
||||||
|
|
||||||
$sync.ProcessRunning = $true
|
$sync.ProcessRunning = $true
|
||||||
|
Write-WinUtilLog -Component "AppX" -Message "Starting AppX removal for $(@($selected).Count) selected package(s)."
|
||||||
|
|
||||||
foreach ($key in $selected) {
|
foreach ($key in $selected) {
|
||||||
if ($key -eq "WPFAppxMicrosoft_XboxGamingOverlay") {
|
if ($key -eq "WPFAppxMicrosoft_XboxGamingOverlay") {
|
||||||
# Making sure Game Bar isn't running
|
# Making sure Game Bar isn't running
|
||||||
|
Write-WinUtilLog -Component "AppX" -Message "Stopping GameBarFTServer before removing Xbox Gaming Overlay."
|
||||||
Stop-Process -Name GameBarFTServer
|
Stop-Process -Name GameBarFTServer
|
||||||
|
|
||||||
# This stops annoying ms-gamebar popup when launching games.
|
# This stops annoying ms-gamebar popup when launching games.
|
||||||
|
Write-WinUtilLog -Component "AppX" -Message "Disabling Game DVR capture before removing Xbox Gaming Overlay."
|
||||||
Set-ItemProperty -Path HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVR -Name AppCaptureEnabled -Value 0
|
Set-ItemProperty -Path HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVR -Name AppCaptureEnabled -Value 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($key -eq "WPFAppxMicrosoft_WindowsNotepad") {
|
if ($key -eq "WPFAppxMicrosoft_WindowsNotepad") {
|
||||||
# i hope your having fun reading this
|
# i hope your having fun reading this
|
||||||
|
Write-WinUtilLog -Component "AppX" -Message "Stopping dllhost before removing Notepad."
|
||||||
Stop-Process -Name dllhost
|
Stop-Process -Name dllhost
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Removing $($apps[$key].Content)"
|
Write-Host "Removing $($apps[$key].Content)"
|
||||||
|
Write-WinUtilLog -Component "AppX" -Message "Removing $($apps[$key].Content) ($($apps[$key].PackageId))."
|
||||||
Get-AppxPackage -Name $apps[$key].PackageId -AllUsers | Remove-AppxPackage -AllUsers
|
Get-AppxPackage -Name $apps[$key].PackageId -AllUsers | Remove-AppxPackage -AllUsers
|
||||||
|
|
||||||
if ($key -eq "WPFAppxMSTeams") {
|
if ($key -eq "WPFAppxMSTeams") {
|
||||||
# Uninstalls Microsoft Teams Meeting Add-in for Microsoft Office
|
# Uninstalls Microsoft Teams Meeting Add-in for Microsoft Office
|
||||||
|
Write-WinUtilLog -Component "AppX" -Message "Uninstalling Microsoft Teams meeting add-in package."
|
||||||
Get-Package -Name "Microsoft Teams*" -ErrorAction SilentlyContinue | Uninstall-Package -Force
|
Get-Package -Name "Microsoft Teams*" -ErrorAction SilentlyContinue | Uninstall-Package -Force
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,6 +44,7 @@ function Invoke-WPFAppxRemoval {
|
|||||||
Write-Host "================================="
|
Write-Host "================================="
|
||||||
Write-Host "-- AppX Removal Finished ---"
|
Write-Host "-- AppX Removal Finished ---"
|
||||||
Write-Host "================================="
|
Write-Host "================================="
|
||||||
|
Write-WinUtilLog -Component "AppX" -Message "AppX removal finished."
|
||||||
|
|
||||||
$sync.ProcessRunning = $false
|
$sync.ProcessRunning = $false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,20 @@ function Invoke-WPFInstall {
|
|||||||
|
|
||||||
if($sync.ProcessRunning) {
|
if($sync.ProcessRunning) {
|
||||||
$msg = "[Invoke-WPFInstall] An Install process is currently running."
|
$msg = "[Invoke-WPFInstall] An Install process is currently running."
|
||||||
[System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
Show-WinUtilMessage -Message $msg -Title "Winutil" -Button "OK" -Icon "Warning"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($PackagesToInstall.Count -eq 0) {
|
if ($PackagesToInstall.Count -eq 0) {
|
||||||
$WarningMsg = "Please select the program(s) to install or upgrade."
|
$WarningMsg = "Please select the program(s) to install or upgrade."
|
||||||
[System.Windows.MessageBox]::Show($WarningMsg, $AppTitle, [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
Show-WinUtilMessage -Message $WarningMsg -Title $AppTitle -Button "OK" -Icon "Warning"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
$ManagerPreference = $sync.preferences.packagemanager
|
$ManagerPreference = $sync.preferences.packagemanager
|
||||||
|
Write-WinUtilLog -Component "Install" -Message "Install requested for $(@($PackagesToInstall).Count) selected package(s) using preference: $ManagerPreference"
|
||||||
|
$packageSummary = Get-WinUtilPackageLogSummary -Packages $PackagesToInstall -Preference $ManagerPreference
|
||||||
|
Write-WinUtilLog -Component "Install" -Message "Install selected package(s): $($packageSummary -join '; ')"
|
||||||
|
|
||||||
$handle = Invoke-WPFRunspace -ParameterList @(("PackagesToInstall", $PackagesToInstall),("ManagerPreference", $ManagerPreference)) -ScriptBlock {
|
$handle = Invoke-WPFRunspace -ParameterList @(("PackagesToInstall", $PackagesToInstall),("ManagerPreference", $ManagerPreference)) -ScriptBlock {
|
||||||
param($PackagesToInstall, $ManagerPreference)
|
param($PackagesToInstall, $ManagerPreference)
|
||||||
@@ -28,6 +31,7 @@ function Invoke-WPFInstall {
|
|||||||
|
|
||||||
$packagesWinget = $packagesSorted[[PackageManagers]::Winget]
|
$packagesWinget = $packagesSorted[[PackageManagers]::Winget]
|
||||||
$packagesChoco = $packagesSorted[[PackageManagers]::Choco]
|
$packagesChoco = $packagesSorted[[PackageManagers]::Choco]
|
||||||
|
Write-WinUtilLog -Component "Install" -Message "Install package manager split: winget=$(@($packagesWinget).Count), choco=$(@($packagesChoco).Count)"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$sync.ProcessRunning = $true
|
$sync.ProcessRunning = $true
|
||||||
@@ -44,13 +48,17 @@ function Invoke-WPFInstall {
|
|||||||
Write-Host "==========================================="
|
Write-Host "==========================================="
|
||||||
Write-Host "-- Installs have finished ---"
|
Write-Host "-- Installs have finished ---"
|
||||||
Write-Host "==========================================="
|
Write-Host "==========================================="
|
||||||
|
Write-WinUtilLog -Component "Install" -Message "Install workflow completed."
|
||||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
|
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
|
||||||
} catch {
|
} catch {
|
||||||
|
Hide-WPFInstallAppBusy
|
||||||
Write-Host "==========================================="
|
Write-Host "==========================================="
|
||||||
Write-Host "Error: $_"
|
Write-Host "Error: $_"
|
||||||
Write-Host "==========================================="
|
Write-Host "==========================================="
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "Install" -Message "Install workflow failed: $($_.Exception.Message)"
|
||||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
|
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
|
||||||
}
|
} finally {
|
||||||
$sync.ProcessRunning = $False
|
$sync.ProcessRunning = $False
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ function Invoke-WPFRunspace {
|
|||||||
$script:powershell = [powershell]::Create()
|
$script:powershell = [powershell]::Create()
|
||||||
|
|
||||||
# Add Scriptblock and Arguments to runspace
|
# Add Scriptblock and Arguments to runspace
|
||||||
$script:powershell.AddScript($ScriptBlock)
|
[void]$script:powershell.AddScript($ScriptBlock)
|
||||||
$script:powershell.AddArgument($ArgumentList)
|
[void]$script:powershell.AddArgument($ArgumentList)
|
||||||
|
|
||||||
foreach ($parameter in $ParameterList) {
|
foreach ($parameter in $ParameterList) {
|
||||||
$script:powershell.AddParameter($parameter[0], $parameter[1])
|
[void]$script:powershell.AddParameter($parameter[0], $parameter[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
$script:powershell.RunspacePool = $sync.runspace
|
$script:powershell.RunspacePool = $sync.runspace
|
||||||
|
|||||||
@@ -11,26 +11,29 @@ function Invoke-WPFUnInstall {
|
|||||||
|
|
||||||
if($sync.ProcessRunning) {
|
if($sync.ProcessRunning) {
|
||||||
$msg = "[Invoke-WPFUnInstall] Install process is currently running"
|
$msg = "[Invoke-WPFUnInstall] Install process is currently running"
|
||||||
[System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
Show-WinUtilMessage -Message $msg -Title "Winutil" -Button "OK" -Icon "Warning"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($PackagesToUninstall.Count -eq 0) {
|
if ($PackagesToUninstall.Count -eq 0) {
|
||||||
$WarningMsg = "Please select the program(s) to uninstall"
|
$WarningMsg = "Please select the program(s) to uninstall"
|
||||||
[System.Windows.MessageBox]::Show($WarningMsg, $AppTitle, [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
Show-WinUtilMessage -Message $WarningMsg -Title $AppTitle -Button "OK" -Icon "Warning"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
$ButtonType = [System.Windows.MessageBoxButton]::YesNo
|
$ButtonType = "YesNo"
|
||||||
$MessageboxTitle = "Are you sure?"
|
$MessageboxTitle = "Are you sure?"
|
||||||
$Messageboxbody = ("This will uninstall the following applications: `n $($PackagesToUninstall | Select-Object Name, Description| Out-String)")
|
$Messageboxbody = ("This will uninstall the following applications: `n $($PackagesToUninstall | Select-Object Name, Description| Out-String)")
|
||||||
$MessageIcon = [System.Windows.MessageBoxImage]::Information
|
$MessageIcon = "Information"
|
||||||
|
|
||||||
$confirm = [System.Windows.MessageBox]::Show($Messageboxbody, $MessageboxTitle, $ButtonType, $MessageIcon)
|
$confirm = Show-WinUtilMessage -Message $Messageboxbody -Title $MessageboxTitle -Button $ButtonType -Icon $MessageIcon
|
||||||
|
|
||||||
if($confirm -eq "No") {return}
|
if($confirm -eq "No") {return}
|
||||||
|
|
||||||
$ManagerPreference = $sync.preferences.packagemanager
|
$ManagerPreference = $sync.preferences.packagemanager
|
||||||
|
Write-WinUtilLog -Component "Uninstall" -Message "Uninstall requested for $(@($PackagesToUninstall).Count) selected package(s) using preference: $ManagerPreference"
|
||||||
|
$packageSummary = Get-WinUtilPackageLogSummary -Packages $PackagesToUninstall -Preference $ManagerPreference
|
||||||
|
Write-WinUtilLog -Component "Uninstall" -Message "Uninstall selected package(s): $($packageSummary -join '; ')"
|
||||||
|
|
||||||
Invoke-WPFRunspace -ParameterList @(("PackagesToUninstall", $PackagesToUninstall),("ManagerPreference", $ManagerPreference)) -ScriptBlock {
|
Invoke-WPFRunspace -ParameterList @(("PackagesToUninstall", $PackagesToUninstall),("ManagerPreference", $ManagerPreference)) -ScriptBlock {
|
||||||
param($PackagesToUninstall, $ManagerPreference)
|
param($PackagesToUninstall, $ManagerPreference)
|
||||||
@@ -38,6 +41,7 @@ function Invoke-WPFUnInstall {
|
|||||||
$packagesSorted = Get-WinUtilSelectedPackages -PackageList $PackagesToUninstall -Preference $ManagerPreference
|
$packagesSorted = Get-WinUtilSelectedPackages -PackageList $PackagesToUninstall -Preference $ManagerPreference
|
||||||
$packagesWinget = $packagesSorted[[PackageManagers]::Winget]
|
$packagesWinget = $packagesSorted[[PackageManagers]::Winget]
|
||||||
$packagesChoco = $packagesSorted[[PackageManagers]::Choco]
|
$packagesChoco = $packagesSorted[[PackageManagers]::Choco]
|
||||||
|
Write-WinUtilLog -Component "Uninstall" -Message "Uninstall package manager split: winget=$(@($packagesWinget).Count), choco=$(@($packagesChoco).Count)"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$sync.ProcessRunning = $true
|
$sync.ProcessRunning = $true
|
||||||
@@ -58,14 +62,18 @@ function Invoke-WPFUnInstall {
|
|||||||
Write-Host "==========================================="
|
Write-Host "==========================================="
|
||||||
Write-Host "-- Uninstalls have finished ---"
|
Write-Host "-- Uninstalls have finished ---"
|
||||||
Write-Host "==========================================="
|
Write-Host "==========================================="
|
||||||
|
Write-WinUtilLog -Component "Uninstall" -Message "Uninstall workflow completed."
|
||||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
|
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
|
||||||
} catch {
|
} catch {
|
||||||
|
Hide-WPFInstallAppBusy
|
||||||
Write-Host "==========================================="
|
Write-Host "==========================================="
|
||||||
Write-Host "Error: $_"
|
Write-Host "Error: $_"
|
||||||
Write-Host "==========================================="
|
Write-Host "==========================================="
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "Uninstall" -Message "Uninstall workflow failed: $($_.Exception.Message)"
|
||||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
|
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
|
||||||
}
|
} finally {
|
||||||
$sync.ProcessRunning = $False
|
$sync.ProcessRunning = $False
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ function Invoke-WPFUpdatesdefault {
|
|||||||
|
|
||||||
#>
|
#>
|
||||||
$ErrorActionPreference = 'SilentlyContinue'
|
$ErrorActionPreference = 'SilentlyContinue'
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Resetting Windows Update settings to default."
|
||||||
|
|
||||||
Write-Host "Removing Windows Update policy settings..." -ForegroundColor Green
|
Write-Host "Removing Windows Update policy settings..." -ForegroundColor Green
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Removing Windows Update policy registry paths."
|
||||||
|
|
||||||
Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Recurse -Force
|
Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Recurse -Force
|
||||||
Remove-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization" -Recurse -Force
|
Remove-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization" -Recurse -Force
|
||||||
@@ -17,24 +19,31 @@ function Invoke-WPFUpdatesdefault {
|
|||||||
Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Recurse -Force
|
Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Recurse -Force
|
||||||
|
|
||||||
Write-Host "Showing Windows Updates in settings..."
|
Write-Host "Showing Windows Updates in settings..."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Showing Windows Update settings page."
|
||||||
Remove-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name SettingsPageVisibility
|
Remove-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name SettingsPageVisibility
|
||||||
|
|
||||||
Write-Host "Reenabling Windows Update Services..." -ForegroundColor Green
|
Write-Host "Reenabling Windows Update Services..." -ForegroundColor Green
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Restoring Windows Update service startup types."
|
||||||
|
|
||||||
Write-Host "Restored BITS to Manual."
|
Write-Host "Restored BITS to Manual."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Restoring BITS service to Manual."
|
||||||
Set-Service -Name BITS -StartupType Manual
|
Set-Service -Name BITS -StartupType Manual
|
||||||
|
|
||||||
Write-Host "Restored wuauserv to Manual."
|
Write-Host "Restored wuauserv to Manual."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Restoring wuauserv service to Manual."
|
||||||
Set-Service -Name wuauserv -StartupType Manual
|
Set-Service -Name wuauserv -StartupType Manual
|
||||||
|
|
||||||
Write-Host "Restored UsoSvc to Automatic."
|
Write-Host "Restored UsoSvc to Automatic."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Starting UsoSvc service and restoring startup type to Automatic."
|
||||||
Start-Service -Name UsoSvc
|
Start-Service -Name UsoSvc
|
||||||
Set-Service -Name UsoSvc -StartupType Automatic
|
Set-Service -Name UsoSvc -StartupType Automatic
|
||||||
|
|
||||||
Write-Host "Restored WaaSMedicSvc to Manual."
|
Write-Host "Restored WaaSMedicSvc to Manual."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Restoring WaaSMedicSvc service to Manual."
|
||||||
Set-Service -Name WaaSMedicSvc -StartupType Manual
|
Set-Service -Name WaaSMedicSvc -StartupType Manual
|
||||||
|
|
||||||
Write-Host "Enabling update related scheduled tasks..." -ForegroundColor Green
|
Write-Host "Enabling update related scheduled tasks..." -ForegroundColor Green
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Enabling update related scheduled tasks."
|
||||||
|
|
||||||
$Tasks =
|
$Tasks =
|
||||||
'\Microsoft\Windows\InstallService\*',
|
'\Microsoft\Windows\InstallService\*',
|
||||||
@@ -49,6 +58,7 @@ function Invoke-WPFUpdatesdefault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Windows Local Policies Reset to Default."
|
Write-Host "Windows Local Policies Reset to Default."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Resetting local security policy to defaults with secedit."
|
||||||
secedit /configure /cfg "$Env:SystemRoot\inf\defltbase.inf" /db defltbase.sdb
|
secedit /configure /cfg "$Env:SystemRoot\inf\defltbase.inf" /db defltbase.sdb
|
||||||
|
|
||||||
Write-Host "===================================================" -ForegroundColor Green
|
Write-Host "===================================================" -ForegroundColor Green
|
||||||
@@ -56,4 +66,5 @@ function Invoke-WPFUpdatesdefault {
|
|||||||
Write-Host "===================================================" -ForegroundColor Green
|
Write-Host "===================================================" -ForegroundColor Green
|
||||||
|
|
||||||
Write-Host "Note: You must restart your system in order for all changes to take effect." -ForegroundColor Yellow
|
Write-Host "Note: You must restart your system in order for all changes to take effect." -ForegroundColor Yellow
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Windows Update default workflow completed. Restart required."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ function Invoke-WPFUpdatesdisable {
|
|||||||
|
|
||||||
#>
|
#>
|
||||||
$ErrorActionPreference = 'SilentlyContinue'
|
$ErrorActionPreference = 'SilentlyContinue'
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Disabling Windows Update settings."
|
||||||
|
|
||||||
Write-Host "Configuring registry settings..." -ForegroundColor Yellow
|
Write-Host "Configuring registry settings..." -ForegroundColor Yellow
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Configuring Windows Update registry policy values for disable mode."
|
||||||
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Force
|
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Force
|
||||||
|
|
||||||
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "NoAutoUpdate" -Type DWord -Value 1
|
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "NoAutoUpdate" -Type DWord -Value 1
|
||||||
@@ -20,22 +22,28 @@ function Invoke-WPFUpdatesdisable {
|
|||||||
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" -Name "DODownloadMode" -Type DWord -Value 0
|
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" -Name "DODownloadMode" -Type DWord -Value 0
|
||||||
|
|
||||||
Write-Host "Hiding Windows Updates from settings..."
|
Write-Host "Hiding Windows Updates from settings..."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Hiding Windows Update settings page."
|
||||||
Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name SettingsPageVisibility -Value hide:windowsupdate
|
Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name SettingsPageVisibility -Value hide:windowsupdate
|
||||||
|
|
||||||
Write-Host "Disabled BITS Service."
|
Write-Host "Disabled BITS Service."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Disabling BITS service."
|
||||||
Set-Service -Name BITS -StartupType Disabled
|
Set-Service -Name BITS -StartupType Disabled
|
||||||
|
|
||||||
Write-Host "Disabled wuauserv Service."
|
Write-Host "Disabled wuauserv Service."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Disabling wuauserv service."
|
||||||
Set-Service -Name wuauserv -StartupType Disabled
|
Set-Service -Name wuauserv -StartupType Disabled
|
||||||
|
|
||||||
Write-Host "Disabled UsoSvc Service."
|
Write-Host "Disabled UsoSvc Service."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Stopping and disabling UsoSvc service."
|
||||||
Stop-Service -Name UsoSvc -Force
|
Stop-Service -Name UsoSvc -Force
|
||||||
Set-Service -Name UsoSvc -StartupType Disabled
|
Set-Service -Name UsoSvc -StartupType Disabled
|
||||||
|
|
||||||
Remove-Item "C:\Windows\SoftwareDistribution\*" -Recurse -Force
|
Remove-Item "C:\Windows\SoftwareDistribution\*" -Recurse -Force
|
||||||
Write-Host "Cleared SoftwareDistribution folder."
|
Write-Host "Cleared SoftwareDistribution folder."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Cleared SoftwareDistribution folder."
|
||||||
|
|
||||||
Write-Host "Disabling update related scheduled tasks..." -ForegroundColor Yellow
|
Write-Host "Disabling update related scheduled tasks..." -ForegroundColor Yellow
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Disabling update related scheduled tasks."
|
||||||
|
|
||||||
$Tasks =
|
$Tasks =
|
||||||
'\Microsoft\Windows\InstallService\*',
|
'\Microsoft\Windows\InstallService\*',
|
||||||
@@ -54,4 +62,5 @@ function Invoke-WPFUpdatesdisable {
|
|||||||
Write-Host "=================================" -ForegroundColor Green
|
Write-Host "=================================" -ForegroundColor Green
|
||||||
|
|
||||||
Write-Host "Note: You must restart your system in order for all changes to take effect." -ForegroundColor Yellow
|
Write-Host "Note: You must restart your system in order for all changes to take effect." -ForegroundColor Yellow
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Windows Update disable workflow completed. Restart required."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ function Invoke-WPFUpdatessecurity {
|
|||||||
#>
|
#>
|
||||||
|
|
||||||
Write-Host "Disabling driver offering through Windows Update..."
|
Write-Host "Disabling driver offering through Windows Update..."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Applying recommended Windows Update settings."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Disabling driver offering through Windows Update."
|
||||||
|
|
||||||
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -Force
|
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -Force
|
||||||
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -Name "PreventDeviceMetadataFromNetwork" -Type DWord -Value 1
|
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -Name "PreventDeviceMetadataFromNetwork" -Type DWord -Value 1
|
||||||
@@ -28,6 +30,7 @@ function Invoke-WPFUpdatessecurity {
|
|||||||
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "ExcludeWUDriversInQualityUpdate" -Type DWord -Value 1
|
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "ExcludeWUDriversInQualityUpdate" -Type DWord -Value 1
|
||||||
|
|
||||||
Write-Host "Setting cumulative updates back by 1 year and security updates by 4 days..."
|
Write-Host "Setting cumulative updates back by 1 year and security updates by 4 days..."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Deferring feature updates by 365 days and quality updates by 4 days."
|
||||||
|
|
||||||
New-Item -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Force
|
New-Item -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Force
|
||||||
|
|
||||||
@@ -36,6 +39,7 @@ function Invoke-WPFUpdatessecurity {
|
|||||||
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "DeferQualityUpdatesPeriodInDays" -Type DWord -Value 4
|
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "DeferQualityUpdatesPeriodInDays" -Type DWord -Value 4
|
||||||
|
|
||||||
Write-Host "Disabling Windows Update automatic restart..."
|
Write-Host "Disabling Windows Update automatic restart..."
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Disabling Windows Update automatic restart while users are logged in."
|
||||||
|
|
||||||
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Force
|
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Force
|
||||||
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "NoAutoRebootWithLoggedOnUsers" -Type DWord -Value 1
|
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "NoAutoRebootWithLoggedOnUsers" -Type DWord -Value 1
|
||||||
@@ -44,4 +48,5 @@ function Invoke-WPFUpdatessecurity {
|
|||||||
Write-Host "================================="
|
Write-Host "================================="
|
||||||
Write-Host "-- Updates Set to Recommended ---"
|
Write-Host "-- Updates Set to Recommended ---"
|
||||||
Write-Host "================================="
|
Write-Host "================================="
|
||||||
|
Write-WinUtilLog -Component "Updates" -Message "Recommended Windows Update settings workflow completed."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ function Invoke-WPFtweaksbutton {
|
|||||||
$tweaksToRun = @($Tweaks | Where-Object { $_ -ne $restorePointTweak })
|
$tweaksToRun = @($Tweaks | Where-Object { $_ -ne $restorePointTweak })
|
||||||
$totalSteps = [Math]::Max($Tweaks.Count, 1)
|
$totalSteps = [Math]::Max($Tweaks.Count, 1)
|
||||||
$completedSteps = 0
|
$completedSteps = 0
|
||||||
|
Write-WinUtilLog -Component "Tweaks" -Message "Tweaks requested: $(@($Tweaks).Count) selected tweak(s), DNS provider: $dnsProvider"
|
||||||
|
|
||||||
if ($tweaks.count -eq 0 -and $dnsProvider -eq "Default") {
|
if ($tweaks.count -eq 0 -and $dnsProvider -eq "Default") {
|
||||||
$msg = "Please check the tweaks you wish to perform."
|
$msg = "Please check the tweaks you wish to perform."
|
||||||
@@ -39,6 +40,7 @@ function Invoke-WPFtweaksbutton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Set-WinUtilProgressBar -Label "Creating restore point" -Percent 0
|
Set-WinUtilProgressBar -Label "Creating restore point" -Percent 0
|
||||||
|
Write-WinUtilLog -Component "Tweaks" -Message "Creating restore point before applying selected tweaks."
|
||||||
Invoke-WinUtilTweaks $restorePointTweak
|
Invoke-WinUtilTweaks $restorePointTweak
|
||||||
$completedSteps = 1
|
$completedSteps = 1
|
||||||
|
|
||||||
@@ -49,6 +51,7 @@ function Invoke-WPFtweaksbutton {
|
|||||||
Write-Host "================================="
|
Write-Host "================================="
|
||||||
Write-Host "-- Tweaks are Finished ---"
|
Write-Host "-- Tweaks are Finished ---"
|
||||||
Write-Host "================================="
|
Write-Host "================================="
|
||||||
|
Write-WinUtilLog -Component "Tweaks" -Message "Tweaks workflow completed after restore point."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,5 +85,6 @@ function Invoke-WPFtweaksbutton {
|
|||||||
Write-Host "================================="
|
Write-Host "================================="
|
||||||
Write-Host "-- Tweaks are Finished ---"
|
Write-Host "-- Tweaks are Finished ---"
|
||||||
Write-Host "================================="
|
Write-Host "================================="
|
||||||
|
Write-WinUtilLog -Component "Tweaks" -Message "Tweaks workflow completed."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function Invoke-WPFundoall {
|
|||||||
param($tweaks)
|
param($tweaks)
|
||||||
|
|
||||||
$sync.ProcessRunning = $true
|
$sync.ProcessRunning = $true
|
||||||
|
Write-WinUtilLog -Component "Tweaks" -Message "Undo tweaks requested: $(@($tweaks).Count) selected tweak(s)."
|
||||||
if ($tweaks.count -eq 1) {
|
if ($tweaks.count -eq 1) {
|
||||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }
|
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }
|
||||||
} else {
|
} else {
|
||||||
@@ -43,6 +44,7 @@ function Invoke-WPFundoall {
|
|||||||
Write-Host "=================================="
|
Write-Host "=================================="
|
||||||
Write-Host "--- Undo Tweaks are Finished ---"
|
Write-Host "--- Undo Tweaks are Finished ---"
|
||||||
Write-Host "=================================="
|
Write-Host "=================================="
|
||||||
|
Write-WinUtilLog -Component "Tweaks" -Message "Undo tweaks workflow completed."
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,12 @@
|
|||||||
# Tests - Config Files
|
# Tests - Config Files
|
||||||
#===========================================================================
|
#===========================================================================
|
||||||
|
|
||||||
|
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||||
$configRoot = Join-Path $PSScriptRoot "..\config"
|
$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 = @(
|
$configCases = @(
|
||||||
Get-ChildItem -Path $configRoot -Filter *.json | ForEach-Object {
|
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" {
|
Describe "Config files" {
|
||||||
foreach ($configCase in $configCases) {
|
foreach ($configCase in $configCases) {
|
||||||
It "imports $($configCase.Name) with no JSON errors" -TestCases $configCase {
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,47 @@
|
|||||||
#===========================================================================
|
#===========================================================================
|
||||||
|
|
||||||
Describe "Win11 Creator setup media" {
|
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" {
|
It "autounattend template does not force a product key" {
|
||||||
$templatePath = Join-Path $PSScriptRoot "..\tools\autounattend.xml"
|
[xml]$xml = Get-Content -Path $script:autoUnattendPath -Raw
|
||||||
[xml]$xml = Get-Content -Path $templatePath -Raw
|
|
||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
|
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
|
||||||
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
|
$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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -81,10 +81,13 @@ $dateTime = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
|
|||||||
# Set the path for the winutil directory
|
# Set the path for the winutil directory
|
||||||
$winutildir = "$env:LocalAppData\winutil"
|
$winutildir = "$env:LocalAppData\winutil"
|
||||||
New-Item $winutildir -ItemType Directory -Force | Out-Null
|
New-Item $winutildir -ItemType Directory -Force | Out-Null
|
||||||
|
$sync.winutildir = $winutildir
|
||||||
|
|
||||||
$logdir = "$winutildir\logs"
|
$logdir = "$winutildir\logs"
|
||||||
New-Item $logdir -ItemType Directory -Force | Out-Null
|
New-Item $logdir -ItemType Directory -Force | Out-Null
|
||||||
Start-Transcript -Path "$logdir\winutil_$dateTime.log" -Append -NoClobber | Out-Null
|
$sync.logPath = "$logdir\winutil_$dateTime.log"
|
||||||
|
$sync.transcriptPath = $sync.logPath
|
||||||
|
Start-Transcript -Path $sync.logPath -Append -NoClobber | Out-Null
|
||||||
|
|
||||||
# Set PowerShell window title
|
# Set PowerShell window title
|
||||||
$Host.UI.RawUI.WindowTitle = "WinUtil (Admin)"
|
$Host.UI.RawUI.WindowTitle = "WinUtil (Admin)"
|
||||||
|
|||||||
Reference in New Issue
Block a user