mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-09 09:31:15 +10:00
* feat: restore document category Moves existing document-focused apps into the Document category and adds its filter chip. * feat: restore document applications Restores reviewed document-focused apps removed in PR #4451. * chore: update apps list as per code review comments This also updates the link for PDF24 Creator * test: verify document category filter wiring Covers the Document filter handler and config category presence. * fix: categorize Calibre as multimedia Keeps the e-book reader in Multimedia Tools.
562 lines
25 KiB
PowerShell
562 lines
25 KiB
PowerShell
#===========================================================================
|
|
# 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",
|
|
"SearchBar",
|
|
"SearchBarIcon",
|
|
"SearchBarClearButton",
|
|
"WPFSearchChips",
|
|
"WPFSearchChipAll",
|
|
"WPFSearchChipBrowsers",
|
|
"WPFSearchChipCommunications",
|
|
"WPFSearchChipDevelopment",
|
|
"WPFSearchChipDocument",
|
|
"WPFSearchChipGames",
|
|
"WPFSearchChipMicrosoftTools",
|
|
"WPFSearchChipMultimediaTools",
|
|
"WPFSearchChipProTools",
|
|
"WPFSearchChipSelfhostedTools",
|
|
"WPFSearchChipUtilities",
|
|
"appscategory",
|
|
"appspanel",
|
|
"tweakspanel",
|
|
"featurespanel",
|
|
"appxpanel",
|
|
"WPFstandard",
|
|
"WPFminimal",
|
|
"WPFAdvanced",
|
|
"WPFClearTweaksSelection",
|
|
"WPFGetInstalledTweaks",
|
|
"WPFAppxRemoval",
|
|
"WPFTweaksbutton",
|
|
"WPFUndoall",
|
|
"WPFUpdatesdefault",
|
|
"WPFUpdatessecurity",
|
|
"WPFUpdatesdisable",
|
|
"WPFDefaultAppxSelection",
|
|
"WPFGetInstalledAppx",
|
|
"WPFSelectAllAppx",
|
|
"WPFClearAppxSelection",
|
|
"WPFBackToTweaks",
|
|
"WPFInstallSelectedAppx",
|
|
"WPFRemoveSelectedAppx"
|
|
)
|
|
|
|
$missingControls = @($requiredControls | Where-Object { -not (Test-WinUtilNameInSet -Name $_ -Set $xamlNames) })
|
|
if ($missingControls.Count -gt 0) {
|
|
throw ($missingControls -join "`n")
|
|
}
|
|
}
|
|
|
|
It "wires the Document search chip to an existing Document category" {
|
|
$mainScript = Get-Content -Path $script:mainScriptPath -Raw
|
|
$mainScript | Should -Match '\$sync\["WPFSearchChipDocument"\]\.Add_Click\(\{ Set-WinUtilAppCategoryFilter -Category "Document" \}\)'
|
|
|
|
$applications = Get-WinUtilConfigObject -Name "applications"
|
|
$categories = @($applications.PSObject.Properties | ForEach-Object { $_.Value.category } | Sort-Object -Unique)
|
|
$categories | Should -Contain "Document"
|
|
}
|
|
|
|
It "presents the three Updates profiles with accurate action labels" {
|
|
$updatesTab = $script:xaml.SelectSingleNode('//*[local-name()="TabItem"][@Name="WPFTab4"]')
|
|
$profileGrid = $updatesTab.SelectSingleNode('.//*[local-name()="UniformGrid"]')
|
|
$expectedButtons = @{
|
|
WPFUpdatessecurity = "Apply Recommended"
|
|
WPFUpdatesdefault = "Restore Defaults"
|
|
WPFUpdatesdisable = "Disable Updates"
|
|
}
|
|
|
|
$profileGrid.GetAttribute("Columns") | Should -Be "3"
|
|
foreach ($buttonName in $expectedButtons.Keys) {
|
|
$button = $updatesTab.SelectSingleNode(".//*[local-name()='Button'][@Name='$buttonName']")
|
|
$button.GetAttribute("Content") | Should -Be $expectedButtons[$buttonName]
|
|
}
|
|
$updatesTab.SelectSingleNode('.//*[@Name="updatespanel"]') | Should -BeNullOrEmpty
|
|
}
|
|
|
|
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])"
|
|
}
|
|
}
|
|
}
|
|
|
|
It "opens AppX removal from Tweaks and provides a return path" {
|
|
$navPanel = $script:xaml.SelectSingleNode('//*[local-name()="StackPanel"][@Name="NavDockPanel"]')
|
|
$tweaksTab = $script:xaml.SelectSingleNode('//*[local-name()="TabItem"][@Name="WPFTab2"]')
|
|
$appxTab = $script:xaml.SelectSingleNode('//*[local-name()="TabItem"][@Name="WPFTab6"]')
|
|
$openButton = $tweaksTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFAppxRemoval"]')
|
|
$buttonNames = @($openButton.ParentNode.SelectNodes('./*[local-name()="Button"]') | ForEach-Object { $_.GetAttribute("Name") })
|
|
$getInstalledIndex = [array]::IndexOf($buttonNames, "WPFGetInstalledTweaks")
|
|
$openAppxIndex = [array]::IndexOf($buttonNames, "WPFAppxRemoval")
|
|
$buttonSource = Get-Content -Path $script:buttonScriptPath -Raw
|
|
$tabSource = Get-Content -Path (Join-Path $script:functionRoot "public\Invoke-WPFTab.ps1") -Raw
|
|
|
|
$navPanel.SelectSingleNode('./*[local-name()="ToggleButton"][@Name="WPFTab6BT"]') | Should -BeNullOrEmpty
|
|
$openButton.GetAttribute("Content").Trim() | Should -Be "AppX Removal"
|
|
$openAppxIndex | Should -Be ($getInstalledIndex + 1)
|
|
$appxTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFBackToTweaks"]') | Should -Not -BeNullOrEmpty
|
|
$appxTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFInstallSelectedAppx"]') | Should -Not -BeNullOrEmpty
|
|
$appxTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFRemoveSelectedAppx"]') | Should -Not -BeNullOrEmpty
|
|
$buttonSource | Should -Match '"WPFAppxRemoval"\s*\{Invoke-WPFTab "WPFTab6BT"\}'
|
|
$buttonSource | Should -Match '"WPFBackToTweaks"\s*\{Invoke-WPFTab "WPFTab2BT"\}'
|
|
$buttonSource | Should -Match '"WPFInstallSelectedAppx"\s*\{Invoke-WPFAppxInstall\}'
|
|
$tabSource | Should -Match '\$sync\.\$tabNav\.Items\[\$tabNumber\]\.IsSelected = \$true'
|
|
}
|
|
|
|
It "centers top bar controls vertically" {
|
|
$navPanel = $script:xaml.SelectSingleNode('//*[local-name()="StackPanel"][@Name="NavDockPanel"]')
|
|
$minimizeButton = $script:xaml.SelectSingleNode('//*[local-name()="Button"][@Name="WPFMinimizeButton"]')
|
|
$actionPanel = $minimizeButton.ParentNode
|
|
$topBarButtonNames = @(
|
|
"ThemeButton",
|
|
"FontScalingButton",
|
|
"SettingsButton",
|
|
"WPFMinimizeButton",
|
|
"WPFMaximizeButton",
|
|
"WPFCloseButton"
|
|
)
|
|
|
|
$navPanel.GetAttribute("VerticalAlignment") | Should -Be "Center"
|
|
$actionPanel.GetAttribute("VerticalAlignment") | Should -Be "Center"
|
|
|
|
foreach ($buttonName in $topBarButtonNames) {
|
|
$button = $script:xaml.SelectSingleNode("//*[local-name()='Button'][@Name='$buttonName']")
|
|
$button.GetAttribute("VerticalAlignment") | Should -Be "Center"
|
|
}
|
|
}
|
|
|
|
It "keeps the responsive search controls within the available screen width" {
|
|
$window = $script:xaml.DocumentElement
|
|
$searchBar = $script:xaml.SelectSingleNode('//*[local-name()="TextBox"][@Name="SearchBar"]')
|
|
$searchBorder = $searchBar.ParentNode.ParentNode
|
|
$mainScript = Get-Content -Path $script:mainScriptPath -Raw
|
|
|
|
$window.GetAttribute("MinWidth") | Should -Be "800"
|
|
$searchBorder.GetAttribute("Width") | Should -BeNullOrEmpty
|
|
$searchBorder.GetAttribute("HorizontalAlignment") | Should -Be "Stretch"
|
|
$searchBar.GetAttribute("Width") | Should -BeNullOrEmpty
|
|
$searchBar.GetAttribute("HorizontalAlignment") | Should -Be "Stretch"
|
|
$mainScript | Should -Match '\$sync\.Form\.MinWidth = "1150"'
|
|
$mainScript | Should -Match '\$sync\.Form\.MinWidth = \[Math\]::Min\(\[double\]\$sync\.Form\.MinWidth, \[double\]\$screenWidth\)'
|
|
}
|
|
|
|
It "shows only one search action glyph at a time" {
|
|
$searchIcon = $script:xaml.SelectSingleNode('//*[local-name()="TextBlock"][@Name="SearchBarIcon"]')
|
|
$clearButton = $script:xaml.SelectSingleNode('//*[local-name()="Button"][@Name="SearchBarClearButton"]')
|
|
$mainScript = Get-Content -Path $script:mainScriptPath -Raw
|
|
|
|
$searchIcon | Should -Not -BeNullOrEmpty
|
|
$clearButton | Should -Not -BeNullOrEmpty
|
|
$mainScript | Should -Match '\$sync\.SearchBarClearButton\.Visibility = "Visible"\s+\$sync\.SearchBarIcon\.Visibility = "Collapsed"'
|
|
$mainScript | Should -Match '\$sync\.SearchBarClearButton\.Visibility = "Collapsed"\s+\$sync\.SearchBarIcon\.Visibility = "Visible"'
|
|
}
|
|
|
|
It "scopes toggle button styles without leaking into combo boxes" {
|
|
$resources = $script:xaml.SelectSingleNode('//*[local-name()="Window.Resources"]')
|
|
$implicitToggleStyles = @($resources.SelectNodes('./*[local-name()="Style"][@TargetType="ToggleButton" or @TargetType="{x:Type ToggleButton}"][not(@x:Key)]', $script:xamlNamespace))
|
|
$tabStyle = $resources.SelectSingleNode('./*[local-name()="Style"][@x:Key="TabToggleButton"]', $script:xamlNamespace)
|
|
$comboToggleStyle = $resources.SelectSingleNode('./*[local-name()="Style"][@x:Key="ComboBoxToggleButtonStyle"]', $script:xamlNamespace)
|
|
$comboStyle = $resources.SelectSingleNode('./*[local-name()="Style"][@TargetType="ComboBox"]')
|
|
$comboToggle = $comboStyle.SelectSingleNode('.//*[local-name()="ToggleButton"][@Name="ToggleButton"]')
|
|
$comboItemStyle = $resources.SelectSingleNode('./*[local-name()="Style"][@TargetType="ComboBoxItem"]')
|
|
$navButtons = @($script:xaml.SelectNodes('//*[local-name()="StackPanel"][@Name="NavDockPanel"]/*[local-name()="ToggleButton"]'))
|
|
|
|
$implicitToggleStyles | Should -BeNullOrEmpty
|
|
$tabStyle | Should -Not -BeNullOrEmpty
|
|
$comboToggleStyle | Should -Not -BeNullOrEmpty
|
|
$comboToggle.GetAttribute("Style") | Should -Be "{StaticResource ComboBoxToggleButtonStyle}"
|
|
$comboItemStyle | Should -Not -BeNullOrEmpty
|
|
$navButtons.Count | Should -Be 5
|
|
foreach ($navButton in $navButtons) {
|
|
$navButton.GetAttribute("Style") | Should -Be "{StaticResource TabToggleButton}"
|
|
}
|
|
}
|
|
|
|
It "uses state-aware maximize and restore icons" {
|
|
$themes = Get-WinUtilConfigObject -Name "themes"
|
|
$minimizeButton = $script:xaml.SelectSingleNode('//*[local-name()="Button"][@Name="WPFMinimizeButton"]')
|
|
$maximizeButton = $script:xaml.SelectSingleNode('//*[local-name()="Button"][@Name="WPFMaximizeButton"]')
|
|
$closeButton = $script:xaml.SelectSingleNode('//*[local-name()="Button"][@Name="WPFCloseButton"]')
|
|
$maximizeStyle = $maximizeButton.SelectSingleNode('./*[local-name()="Button.Style"]/*[local-name()="Style"]')
|
|
$maximizeIcon = $maximizeStyle.SelectSingleNode('./*[local-name()="Setter"][@Property="Content"]')
|
|
$maximizedTrigger = $maximizeStyle.SelectSingleNode('./*[local-name()="Style.Triggers"]/*[local-name()="DataTrigger"][@Value="Maximized"]')
|
|
$restoreIcon = $maximizedTrigger.SelectSingleNode('./*[local-name()="Setter"][@Property="Content"]')
|
|
|
|
foreach ($button in @($minimizeButton, $maximizeButton, $closeButton)) {
|
|
$button.GetAttribute("FontFamily") | Should -Be "Segoe MDL2 Assets"
|
|
$button.GetAttribute("FontSize") | Should -Be "{DynamicResource CloseIconFontSize}"
|
|
$button.GetAttribute("Margin") | Should -BeIn @("0", "0,0,0,0")
|
|
}
|
|
|
|
$minimizeButton.GetAttribute("Content") | Should -Be ([string][char]0xE921)
|
|
$maximizeIcon.GetAttribute("Value") | Should -Be ([string][char]0xE922)
|
|
$restoreIcon.GetAttribute("Value") | Should -Be ([string][char]0xE923)
|
|
$closeButton.GetAttribute("Content") | Should -Be ([string][char]0xE8BB)
|
|
$maximizeStyle.GetAttribute("BasedOn") | Should -Be "{StaticResource HoverButtonStyle}"
|
|
[int]$themes.shared.CloseIconFontSize | Should -BeLessThan ([int]$themes.shared.SettingsIconFontSize)
|
|
}
|
|
}
|
|
|
|
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",
|
|
"ImportInProgress",
|
|
"ScriptsInstallPrograms",
|
|
"keys",
|
|
"ContainsKey",
|
|
"GetEnumerator",
|
|
"Remove",
|
|
"logorender",
|
|
"checkmarkrender",
|
|
"warningrender",
|
|
"InitializedTabs",
|
|
"RenderedAssetCache",
|
|
"ToggleStatusCache",
|
|
"InstallAppRenderQueue",
|
|
"InstallAppEntriesRendered",
|
|
"FontScaleFactor",
|
|
"Win11ISOImageInfo",
|
|
"Win11ISODriveLetter",
|
|
"Win11ISOWimPath",
|
|
"Win11ISOImagePath",
|
|
"Win11ISOModifying",
|
|
"Win11ISOProcessRunning",
|
|
"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")
|
|
}
|
|
}
|
|
}
|