Files
Win11Debloat/Tests/XamlContracts.Tests.ps1
T

179 lines
9.3 KiB
PowerShell

Describe 'XAML UI contracts' {
BeforeAll {
$script:SchemaPath = Join-Path $PSScriptRoot '..\Schemas'
$script:GuiPath = Join-Path $PSScriptRoot '..\Scripts\GUI'
$script:XamlFiles = @(Get-ChildItem -LiteralPath $script:SchemaPath -Filter '*.xaml' -File)
}
It 'keeps every schema well-formed XML' {
foreach ($xamlFile in $script:XamlFiles) {
{ [xml](Get-Content -LiteralPath $xamlFile.FullName -Raw) } |
Should -Not -Throw "XAML schema '$($xamlFile.Name)' must remain well-formed."
}
}
It 'keeps every literal FindName reference backed by its receiver schema' {
# Map receivers, not whole files: dialogs also look up controls on their owners.
$receiverSchemas = @{
'Show-MainWindow.ps1' = @{ window = 'MainWindow.xaml' }
'MainWindow-AppSelection.ps1' = @{ window = 'MainWindow.xaml'; w = 'MainWindow.xaml' }
'MainWindow-Deployment.ps1' = @{ window = 'MainWindow.xaml' }
'MainWindow-Localization.ps1' = @{ window = 'MainWindow.xaml' }
'MainWindow-Navigation.ps1' = @{ window = 'MainWindow.xaml' }
'MainWindow-TweaksBuilder.ps1' = @{ window = 'MainWindow.xaml' }
'Show-AboutDialog.ps1' = @{ aboutWindow = 'AboutWindow.xaml'; ownerWindow = 'MainWindow.xaml' }
'Show-AppSelectionWindow.ps1' = @{ window = 'AppSelectionWindow.xaml'; 'script:GuiWindow' = 'MainWindow.xaml' }
'Show-ApplyModal.ps1' = @{ applyWindow = 'ApplyChangesWindow.xaml'; ownerWindow = 'MainWindow.xaml' }
'Show-SettingsDialog.ps1' = @{ dialog = 'SettingsWindow.xaml'; Owner = 'MainWindow.xaml' }
'Show-MessageBox.ps1' = @{
msgWindow = 'MessageBox.xaml'
ownerWindow = @('MainWindow.xaml', 'AppSelectionWindow.xaml', 'RestoreBackupWindow.xaml')
}
'Show-ImportExportConfigWindow.ps1' = @{ dlg = 'ImportExportConfigWindow.xaml'; owner = 'MainWindow.xaml' }
'Show-RestoreBackupDialog.ps1' = @{ window = 'RestoreBackupWindow.xaml'; ownerWindow = 'MainWindow.xaml' }
'Show-Bubble.ps1' = @{ bubblePanel = 'BubbleHint.xaml' }
}
$schemaNames = @{}
foreach ($xamlFile in $script:XamlFiles) {
$names = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
[xml]$xml = Get-Content -LiteralPath $xamlFile.FullName -Raw
$namespaces = [System.Xml.XmlNamespaceManager]::new($xml.NameTable)
$namespaces.AddNamespace('p', 'http://schemas.microsoft.com/winfx/2006/xaml/presentation')
# Template children belong to separate namescopes and cannot satisfy window lookups.
$attributes = $xml.SelectNodes(
'//@*[not(ancestor::p:ControlTemplate or ancestor::p:DataTemplate or ancestor::p:ItemsPanelTemplate)]',
$namespaces)
foreach ($attribute in $attributes) {
if ($attribute.LocalName -eq 'Name' -and
$attribute.NamespaceURI -in @('', 'http://schemas.microsoft.com/winfx/2006/xaml')) {
[void]$names.Add($attribute.Value)
}
}
$schemaNames[$xamlFile.Name] = $names
}
$failures = @()
foreach ($guiFile in Get-ChildItem -LiteralPath $script:GuiPath -Filter '*.ps1' -File -Recurse) {
$tokens = $null
$parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
$guiFile.FullName, [ref]$tokens, [ref]$parseErrors)
$parseErrors | Should -BeNullOrEmpty
$calls = $ast.FindAll({
param($node)
$node -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -and
$node.Member.Value -eq 'FindName' -and $node.Arguments.Count -eq 1 -and
$node.Arguments[0] -is [System.Management.Automation.Language.StringConstantExpressionAst]
}, $true)
foreach ($call in $calls) {
$name = $call.Arguments[0].Value
$receiver = $call.Expression.Extent.Text
$location = '{0}:{1} {2}.FindName({3})' -f
$guiFile.Name, $call.Extent.StartLineNumber, $receiver, $name
if ($call.Expression -isnot [System.Management.Automation.Language.VariableExpressionAst]) {
$failures += "$location has no receiver mapping"
continue
}
$receiver = $call.Expression.VariablePath.UserPath
$mapping = $receiverSchemas[$guiFile.Name]
$schemas = if ($mapping) { $mapping[$receiver] } else { $null }
if (-not $schemas) {
$failures += "$location has no receiver mapping"
continue
}
foreach ($schema in @($schemas)) {
if (-not $schemaNames.ContainsKey($schema)) {
$failures += "$location maps to missing schema '$schema'"
}
elseif (-not $schemaNames[$schema].Contains($name)) {
# Only the restore dialog owner may omit the message box overlay.
if ($guiFile.Name -eq 'Show-MessageBox.ps1' -and $receiver -eq 'ownerWindow' -and
$schema -eq 'RestoreBackupWindow.xaml' -and $name -ceq 'ModalOverlay') {
continue
}
$failures += "$location is missing from '$schema'"
}
}
}
}
$failures | Should -BeNullOrEmpty
}
It 'keeps the main window navigation and deployment controls available' {
$mainWindow = Get-Content -LiteralPath (Join-Path $script:SchemaPath 'MainWindow.xaml') -Raw
$requiredNames = @(
'MainTabControl'
'HomeTab'
'DeploymentSettingsTab'
'DeploymentApplyBtn'
'UserSelectionCombo'
'AppSelectionPanel'
'AppSelectionStatus'
)
foreach ($name in $requiredNames) {
$mainWindow | Should -Match ('(?:x:Name|\bName)\s*=\s*"{0}"' -f [regex]::Escape($name))
}
}
It 'exposes <Schema> links as keyboard-accessible hyperlinks with automation names' -TestCases @(
@{ Schema = 'AboutWindow.xaml'; LinkNames = @('ProjectLink', 'KofiLink') }
@{ Schema = 'RestoreBackupWindow.xaml'; LinkNames = @('NonRevertibleWikiLink') }
) {
param($Schema, $LinkNames)
Add-Type -AssemblyName PresentationFramework
$styles = [System.Windows.Markup.XamlReader]::Parse(
(Get-Content -LiteralPath (Join-Path $script:SchemaPath 'SharedStyles.xaml') -Raw))
$window = [System.Windows.Markup.XamlReader]::Parse(
(Get-Content -LiteralPath (Join-Path $script:SchemaPath $Schema) -Raw))
try {
$window.Resources.MergedDictionaries.Add($styles)
foreach ($name in $LinkNames) {
$link = $window.FindName($name)
$link | Should -BeOfType ([System.Windows.Documents.Hyperlink])
$link.Focusable | Should -BeTrue
[System.Windows.Input.KeyboardNavigation]::GetIsTabStop($link) | Should -BeTrue
$peer = [System.Windows.Automation.Peers.HyperlinkAutomationPeer]::new($link)
$peer.GetAutomationControlType() | Should -Be ([System.Windows.Automation.Peers.AutomationControlType]::Hyperlink)
$peer.GetName() | Should -Not -BeNullOrEmpty
$peer.GetPattern([System.Windows.Automation.Peers.PatternInterface]::Invoke) | Should -Not -BeNullOrEmpty
}
if ($Schema -eq 'AboutWindow.xaml') {
[System.Windows.Automation.AutomationProperties]::GetName($window.FindName('CloseButton')) |
Should -Be '%LANG:TitleBarClose%'
}
}
finally {
$window.Close()
}
}
It 'keeps read-only restore feature lists out of keyboard focus' {
Add-Type -AssemblyName PresentationFramework
$window = [System.Windows.Markup.XamlReader]::Parse(
(Get-Content -LiteralPath (Join-Path $script:SchemaPath 'RestoreBackupWindow.xaml') -Raw))
try {
foreach ($name in @('FeaturesItemsControl', 'ReappliedFeaturesItemsControl', 'NonRevertibleFeaturesItemsControl')) {
$list = $window.FindName($name)
$list.Focusable | Should -BeFalse
$list.IsTabStop | Should -BeFalse
}
}
finally {
$window.Close()
}
}
It 'keeps destructive modal actions accessible by automation name' {
$applyWindow = Get-Content -LiteralPath (Join-Path $script:SchemaPath 'ApplyChangesWindow.xaml') -Raw
$appWindow = Get-Content -LiteralPath (Join-Path $script:SchemaPath 'AppSelectionWindow.xaml') -Raw
$applyWindow | Should -Match 'AutomationProperties.Name="%LANG:ApplyCancelTooltip%"'
$applyWindow | Should -Match 'AutomationProperties.Name="%LANG:TitleBarClose%"'
$appWindow | Should -Match 'AutomationProperties.Name="%LANG:AppSelectionWindowConfirm%"'
$appWindow | Should -Match 'AutomationProperties.Name="%LANG:AppSelectionWindowCancel%"'
}
}