diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40f8066..0c3fb1d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -176,6 +176,24 @@ jobs: exit 1 } + - name: Run GUI XAML schema tests + shell: pwsh + run: | + $result = Invoke-Pester -Path "Tests/Unit/Test-GuiXamlSchemas.ps1" -Output Detailed -PassThru + if ($result.FailedCount -gt 0) { + Write-Error "$($result.FailedCount) test(s) failed in Test-GuiXamlSchemas.ps1" + exit 1 + } + + - name: Run restore backup feature list tests + shell: pwsh + run: | + $result = Invoke-Pester -Path "Tests/Unit/Test-RestoreBackupFeatureLists.ps1" -Output Detailed -PassThru + if ($result.FailedCount -gt 0) { + Write-Error "$($result.FailedCount) test(s) failed in Test-RestoreBackupFeatureLists.ps1" + exit 1 + } + - name: Run Store search suggestion tests shell: pwsh run: | diff --git a/Tests/Unit/Test-GuiXamlSchemas.ps1 b/Tests/Unit/Test-GuiXamlSchemas.ps1 new file mode 100644 index 0000000..f4d1455 --- /dev/null +++ b/Tests/Unit/Test-GuiXamlSchemas.ps1 @@ -0,0 +1,71 @@ +#Requires -Modules Pester +<# +.SYNOPSIS + Loads every WPF schema the GUI uses and asserts it constructs. +.DESCRIPTION + Winnow builds its windows at run time with XamlReader.Load over the files in + Schemas. A malformed element, a bad attribute, or a type that does not exist + throws only when that window is opened, which no automated test did. This + loads each schema the same way the app does and fails if any does not build. + + XamlReader needs a single-threaded-apartment thread. The unit suite runs + under pwsh, which is multi-threaded by default, so each load runs in its own + STA runspace. That also isolates the WPF assemblies from the test host. +#> + +BeforeAll { + $script:schemasDir = Join-Path (Resolve-Path (Join-Path $PSScriptRoot '..\..')) 'Schemas' + + function Test-XamlLoads { + param([string]$Path) + + $runspace = [runspacefactory]::CreateRunspace() + $runspace.ApartmentState = 'STA' + $runspace.ThreadOptions = 'ReuseThread' + $runspace.Open() + try { + $shell = [powershell]::Create() + $shell.Runspace = $runspace + [void]$shell.AddScript({ + param($xamlPath) + try { + Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase -ErrorAction Stop + $markup = Get-Content -LiteralPath $xamlPath -Raw + $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($markup)) + try { + $object = [System.Windows.Markup.XamlReader]::Load($reader) + } + finally { + $reader.Dispose() + } + if ($null -eq $object) { return 'XamlReader.Load returned nothing' } + return 'OK' + } + catch { + return $_.Exception.Message + } + }).AddArgument($Path) + $result = $shell.Invoke() + $shell.Dispose() + return $result[0] + } + finally { + $runspace.Dispose() + } + } +} + +Describe 'GUI XAML schemas' { + + It 'has schema files to load' { + @(Get-ChildItem -LiteralPath $script:schemasDir -Filter '*.xaml').Count | Should -BeGreaterThan 0 + } + + It 'loads through XamlReader' -TestCases @( + Get-ChildItem -LiteralPath (Join-Path (Resolve-Path (Join-Path $PSScriptRoot '..\..')) 'Schemas') -Filter '*.xaml' | + ForEach-Object { @{ Name = $_.Name; Path = $_.FullName } } + ) { + param($Name, $Path) + Test-XamlLoads -Path $Path | Should -Be 'OK' + } +} diff --git a/Tests/Unit/Test-RestoreBackupFeatureLists.ps1 b/Tests/Unit/Test-RestoreBackupFeatureLists.ps1 new file mode 100644 index 0000000..ee608f5 --- /dev/null +++ b/Tests/Unit/Test-RestoreBackupFeatureLists.ps1 @@ -0,0 +1,93 @@ +#Requires -Modules Pester +<# +.SYNOPSIS + Unit tests for the restore-backup dialog's feature-list logic. +.DESCRIPTION + RestoreBackupDialogFeatureLists.ps1 decides, from a saved backup, which + features the restore dialog shows and which of them can be reverted + automatically (they have an apply .reg file) versus by hand. The functions + are pure: they take a Features hashtable and a backup object and return data, + no WPF. They ran without any test until now. +#> + +BeforeAll { + $repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..') | Select-Object -ExpandProperty Path + . (Join-Path $repoRoot 'Scripts\GUI\RestoreBackupDialogFeatureLists.ps1') + + # A small stand-in for the Features.json catalog. A feature is auto-revertible + # when it has a RegistryKey and is shown in the overview when it has a Category. + $script:features = @{ + 'DisableTelemetry' = [PSCustomObject]@{ Label = 'Disable telemetry'; RegistryKey = 'Disable_Telemetry.reg'; Category = 'Privacy & Suggested Content' } + 'EnableGamingMode' = [PSCustomObject]@{ Label = 'Enable gaming mode'; RegistryKey = $null; Category = 'Gaming' } + 'CreateRestorePoint' = [PSCustomObject]@{ Label = 'Create restore point'; RegistryKey = $null; Category = $null } + } +} + +Describe 'Restore dialog feature classification' { + + It 'treats a feature with a RegistryKey as auto-revertible' { + Test-RestoreDialogFeatureCanAutoRevert -FeatureId 'DisableTelemetry' -Features $script:features | Should -BeTrue + } + + It 'treats a custom feature with no RegistryKey as not auto-revertible' { + Test-RestoreDialogFeatureCanAutoRevert -FeatureId 'EnableGamingMode' -Features $script:features | Should -BeFalse + } + + It 'treats an unknown feature as not auto-revertible' { + Test-RestoreDialogFeatureCanAutoRevert -FeatureId 'NoSuchFeature' -Features $script:features | Should -BeFalse + } + + It 'shows a categorized feature in the overview and hides a category-less one' { + Test-RestoreDialogFeatureVisibleInOverview -FeatureId 'DisableTelemetry' -Features $script:features | Should -BeTrue + Test-RestoreDialogFeatureVisibleInOverview -FeatureId 'CreateRestorePoint' -Features $script:features | Should -BeFalse + } + + It 'uses the catalog label, and falls back to the id then to a placeholder' { + Get-RestoreDialogFeatureDisplayLabel -FeatureId 'DisableTelemetry' -Features $script:features | Should -Be 'Disable telemetry' + Get-RestoreDialogFeatureDisplayLabel -FeatureId 'NoSuchFeature' -Features $script:features | Should -Be 'NoSuchFeature' + Get-RestoreDialogFeatureDisplayLabel -FeatureId '' -Features $script:features | Should -Be 'Unknown feature' + } +} + +Describe 'Selected feature IDs from a backup' { + + It 'returns forward feature ids, de-duplicated case-insensitively' { + $backup = [PSCustomObject]@{ SelectedFeatures = @('DisableTelemetry', 'disabletelemetry', 'EnableGamingMode', '', $null) } + $ids = Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $backup + $ids | Should -Be @('DisableTelemetry', 'EnableGamingMode') + } + + It 'merges forward and undo lists without duplicates' { + $backup = [PSCustomObject]@{ + SelectedFeatures = @('DisableTelemetry', 'EnableGamingMode') + SelectedUndoFeatures = @('EnableGamingMode', 'DisableBing') + } + $ids = Get-SelectedFeatureIdsFromBackup -SelectedBackup $backup + $ids | Should -Be @('DisableTelemetry', 'EnableGamingMode', 'DisableBing') + } + + It 'returns nothing for a backup that selected no features' { + $backup = [PSCustomObject]@{ SelectedFeatures = @(); SelectedUndoFeatures = @() } + @(Get-SelectedFeatureIdsFromBackup -SelectedBackup $backup).Count | Should -Be 0 + } +} + +Describe 'Get-RestoreBackupFeatureLists' { + + It 'splits visible features into auto-revertible and manual, and drops hidden ones' { + $result = Get-RestoreBackupFeatureLists -SelectedFeatureIds @('DisableTelemetry', 'EnableGamingMode', 'CreateRestorePoint', 'NoSuchFeature') -Features $script:features + + # DisableTelemetry has a reg file -> revertible; EnableGamingMode is custom + # -> manual; CreateRestorePoint has no category -> hidden; the unknown id -> hidden. + @($result.Revertible).Count | Should -Be 1 + $result.Revertible[0].DisplayText | Should -Be '- Disable telemetry' + @($result.NonRevertible).Count | Should -Be 1 + $result.NonRevertible[0].DisplayText | Should -Be '- Enable gaming mode' + } + + It 'returns two empty lists when nothing is selected' { + $result = Get-RestoreBackupFeatureLists -SelectedFeatureIds @() -Features $script:features + @($result.Revertible).Count | Should -Be 0 + @($result.NonRevertible).Count | Should -Be 0 + } +}