diff --git a/docs/src/content/docs/code-reference/architecture.mdx b/docs/src/content/docs/code-reference/architecture.mdx index 7e11d54cb2..93bf8372e7 100644 --- a/docs/src/content/docs/code-reference/architecture.mdx +++ b/docs/src/content/docs/code-reference/architecture.mdx @@ -240,7 +240,7 @@ Invoke-WinUtilISOCleanAndReset (optional) **Modification Safety**: - Windows PE and specialize set hardware bypass and setup safeguards; `WinUtil-PostInstall.ps1` applies additional registry changes at first logon - Original ISO never modified; only working copy -- Logged to the WinUtil session log and the live UI status panel +- Logged to the shared `%LocalAppData%\winutil\logs\winutil_.log` session file and the live UI status panel, so the log survives cleanup - DISM handles image dismount with automatic cleanup and discard on error ### Win11 Creator Registry Tweaks diff --git a/functions/private/Find-AppsByNameOrDescription.ps1 b/functions/private/Find-AppsByNameOrDescription.ps1 index c94fc45126..87ac212de3 100644 --- a/functions/private/Find-AppsByNameOrDescription.ps1 +++ b/functions/private/Find-AppsByNameOrDescription.ps1 @@ -91,22 +91,17 @@ function Find-AppsByNameOrDescription { return } - # Escape wildcard characters for literal matching - $escapedSearchString = [System.Management.Automation.WildcardPattern]::Escape($SearchString) - + # IndexOf with OrdinalIgnoreCase is faster than -like with wildcard escaping $sync.ItemsControl.Items | ForEach-Object { - # Each item is a StackPanel container with Children[0] = label, Children[1] = WrapPanel if ($_.Children.Count -ge 2) { $categoryLabel = $_.Children[0] $wrapPanel = $_.Children[1] $categoryHasMatch = $false - $categoryLabel.Visibility = [Windows.Visibility]::Visible foreach ($appControl in $wrapPanel.Children) { $appTag = $appControl.Tag $appEntry = $null - if (-not [string]::IsNullOrWhiteSpace($appTag) -and $sync.configs.applicationsHashtable.ContainsKey($appTag)) { $appEntry = $sync.configs.applicationsHashtable[$appTag] } @@ -114,20 +109,17 @@ function Find-AppsByNameOrDescription { if ($null -ne $appEntry) { $categoryMatch = -not $hasCategories -or $activeCategories -contains $appEntry.Category $textMatch = -not $hasSearch -or - $appEntry.Content -like "*$escapedSearchString*" -or - $appEntry.Description -like "*$escapedSearchString*" -or - $appTag -like "*$escapedSearchString*" + ([string]$appEntry.Content).IndexOf($SearchString, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or + ([string]$appEntry.Description).IndexOf($SearchString, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or + ([string]$appTag).IndexOf($SearchString, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 if ($categoryMatch -and $textMatch) { $appControl.Visibility = [Windows.Visibility]::Visible $categoryHasMatch = $true - } - else { + } else { $appControl.Visibility = [Windows.Visibility]::Collapsed } - } - else { - # Hide app if no entry found (data integrity issue) + } else { $appControl.Visibility = [Windows.Visibility]::Collapsed } } diff --git a/functions/private/Find-TweaksByNameOrDescription.ps1 b/functions/private/Find-TweaksByNameOrDescription.ps1 index f47db60a9a..809f4ae4db 100644 --- a/functions/private/Find-TweaksByNameOrDescription.ps1 +++ b/functions/private/Find-TweaksByNameOrDescription.ps1 @@ -1,324 +1,128 @@ function Find-TweaksByNameOrDescription { <# - .SYNOPSIS - Searches through the Tweaks on the Tweaks Tab and hides all entries that do not match the search string - - .DESCRIPTION - Filters tweak entries by name or description using literal string matching (no wildcard expansion). - Respects collapsed category state and handles null $sync gracefully. - Safe for rapid keystroke events; no terminal spam on error conditions. - - .PARAMETER SearchString - The string to be searched for. Wildcards are treated as literal characters. - - .NOTES - - Uses module-scope $sync (resolved via global/script fallback if needed) - - Performs literal matching (no wildcard expansion) - - Safely handles missing UI elements and null properties - - Protected by try/catch to prevent UI thread crashes - - PowerShell 5.1 compatible (no ternary operators, no advanced language features) + .SYNOPSIS + Filters tweak/appx entries by name or description using literal string matching. + .PARAMETER SearchString + The string to search for. Treated as a literal (no wildcard expansion). #> param( - [Parameter(Mandatory = $false)] [string]$SearchString = "" ) - # ------------------------------------------------------------------------------ - # 1. RESOLVE $SYNC WITH MULTI-LEVEL FALLBACK - # ------------------------------------------------------------------------------ - - if ($null -eq $Sync) { - $Sync = $global:sync - if ($null -eq $Sync) { - $Sync = $script:sync - } - } + # $sync is always in scope in the compiled script - no multi-level fallback needed + if ($null -eq $sync -or $null -eq $sync.Form) { return } - # Validate that $Sync exists and has required structure - if ($null -eq $Sync) { - # Silent return - function called on every keystroke; no warning spam - return + if ($null -eq $sync.TweakCategoryAutoExpanded) { + $sync.TweakCategoryAutoExpanded = @{} } - if ($null -eq $Sync.Form) { - # Silent return - form not yet initialized - return - } - - # ------------------------------------------------------------------------------ - # 2. GET REFERENCE TO TWEAKS OR APPX PANEL - # ------------------------------------------------------------------------------ - - $panelName = "tweakspanel" - if ($null -ne $Sync.currentTab -and $Sync.currentTab -eq "AppX") { - $panelName = "appxpanel" + $panelName = if ($sync.currentTab -eq "AppX") { "appxpanel" } else { "tweakspanel" } + try { $tweaksPanel = $sync.Form.FindName($panelName) } catch { return } + if ($null -eq $tweaksPanel) { return } + + # --- Helper: extract searchable text from a DockPanel or StackPanel item --- + function Get-ItemSearchText($item) { + $text = ""; $tip = "" + if ($item -is [Windows.Controls.DockPanel]) { + $label = $item.Children | Where-Object { $_ -is [Windows.Controls.Label] } | Select-Object -First 1 + if ($label) { $text = [string]$label.Content; $tip = [string]$label.ToolTip } + } elseif ($item -is [Windows.Controls.StackPanel]) { + $cb = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] } | Select-Object -First 1 + if ($cb) { $text = [string]$cb.Content; $tip = [string]$cb.ToolTip } + } + return @{ Text = $text; Tip = $tip.Replace("Preset key: ", "") } } - $tweaksPanel = $null try { - $tweaksPanel = $Sync.Form.FindName($panelName) - } - catch { - # Silent return - panel not found or disposed - return - } - - if ($null -eq $tweaksPanel) { - # Silent return - panel doesn't exist - return - } - - # ------------------------------------------------------------------------------ - # 3. HANDLE EMPTY/WHITESPACE SEARCH STRING - RESET TO DEFAULT STATE - # ------------------------------------------------------------------------------ - - if ([string]::IsNullOrWhiteSpace($SearchString)) { - try { - $tweaksPanel.Children | ForEach-Object { - $categoryBorder = $_ - - # Safely set visibility - if ($null -ne $categoryBorder) { - $categoryBorder.Visibility = [Windows.Visibility]::Visible - } - - # Process each category - if ($categoryBorder -is [Windows.Controls.Border]) { - $dockPanel = $null - if ($null -ne $categoryBorder.Child) { - $dockPanel = $categoryBorder.Child - } - - if ($dockPanel -is [Windows.Controls.DockPanel]) { - $container = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] -or $_ -is [Windows.Controls.StackPanel] -or $_ -is [Windows.Controls.ScrollViewer] -or $_.GetType().Name -eq "ItemsControl" } | Select-Object -First 1 - - if ($null -ne $container) { - $targetPanel = if ($container.PSObject.Properties['Content'] -and $null -ne $container.Content) { $container.Content } else { $container } - $items = $null - if ($targetPanel -is [Windows.Controls.ItemsControl] -or $targetPanel.GetType().Name -eq "ItemsControl") { - $items = $targetPanel.Items - } - else { - $items = $targetPanel.Children - } - # Show all items in the category - foreach ($item in $items) { - if ($null -ne $item) { - # Check if it's a category label (first Label in the container) - if ($item -is [Windows.Controls.Label] -or $item.GetType().Name -eq "Label") { - $item.Visibility = [Windows.Visibility]::Visible - } - elseif ($item -is [Windows.Controls.DockPanel] -or $item -is [Windows.Controls.StackPanel] -or $item.GetType().Name -eq "DockPanel" -or $item.GetType().Name -eq "StackPanel") { - # Show all checkbox containers - $item.Visibility = [Windows.Visibility]::Visible - } + # Reset visibility when search is cleared + if ([string]::IsNullOrWhiteSpace($SearchString)) { + foreach ($categoryBorder in $tweaksPanel.Children) { + if ($null -eq $categoryBorder) { continue } + $categoryBorder.Visibility = [Windows.Visibility]::Visible + if ($categoryBorder -is [Windows.Controls.Border] -and $categoryBorder.Child -is [Windows.Controls.DockPanel]) { + $container = $categoryBorder.Child.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] -or $_ -is [Windows.Controls.StackPanel] -or $_ -is [Windows.Controls.ScrollViewer] } | Select-Object -First 1 + if ($container) { + $targetPanel = if ($container -is [Windows.Controls.ScrollViewer]) { $container.Content } else { $container } + $items = if ($targetPanel -is [Windows.Controls.ItemsControl]) { $targetPanel.Items } else { $targetPanel.Children } + foreach ($item in $items) { + if ($null -eq $item) { continue } + if ($item -is [Windows.Controls.Label]) { + $item.Visibility = [Windows.Visibility]::Visible + # A category that filtering expanded goes back to how the user left it + $labelStr = [string]$item.Content + $categoryName = $labelStr -replace '^[+-] ', '' + if ($sync.TweakCategoryAutoExpanded.ContainsKey($categoryName)) { + $item.Content = $labelStr -replace "^- ", "+ " + $labelStr = [string]$item.Content + $sync.TweakCategoryAutoExpanded.Remove($categoryName) + } + # Respect collapsed state: labels starting with "+" keep items collapsed + if ($labelStr.StartsWith("+ ")) { + $collapsed = $true + } else { + $collapsed = $false } + } else { + $item.Visibility = if ($collapsed) { [Windows.Visibility]::Collapsed } else { [Windows.Visibility]::Visible } } } } } } - } - catch { - # Silent catch - UI element may be disposed - $null = $_ - } - - return - } - - # ------------------------------------------------------------------------------ - # 4. PERFORM LITERAL SEARCH (NO WILDCARD EXPANSION) - # ------------------------------------------------------------------------------ - - try { - # Normalize search term once for the entire operation - $searchTerm = $SearchString - if ($null -eq $searchTerm) { - $searchTerm = "" + return } - # Iterate through all categories - $tweaksPanel.Children | ForEach-Object { - $categoryBorder = $_ + # Perform literal search + foreach ($categoryBorder in $tweaksPanel.Children) { $categoryHasMatch = $false + if (-not ($categoryBorder -is [Windows.Controls.Border])) { continue } + if (-not ($categoryBorder.Child -is [Windows.Controls.DockPanel])) { continue } - if ($categoryBorder -is [Windows.Controls.Border]) { - $dockPanel = $null - if ($null -ne $categoryBorder.Child) { - $dockPanel = $categoryBorder.Child - } - - if ($dockPanel -is [Windows.Controls.DockPanel]) { - $container = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] -or $_ -is [Windows.Controls.StackPanel] -or $_ -is [Windows.Controls.ScrollViewer] -or $_.GetType().Name -eq "ItemsControl" } | Select-Object -First 1 - - if ($null -ne $container) { - $categoryLabel = $null + $container = $categoryBorder.Child.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] -or $_ -is [Windows.Controls.StackPanel] -or $_ -is [Windows.Controls.ScrollViewer] } | Select-Object -First 1 + if ($null -eq $container) { continue } - $targetPanel = if ($container.PSObject.Properties['Content'] -and $null -ne $container.Content) { $container.Content } else { $container } - $items = $null - if ($targetPanel -is [Windows.Controls.ItemsControl] -or $targetPanel.GetType().Name -eq "ItemsControl") { - $items = $targetPanel.Items - } - else { - $items = $targetPanel.Children - } - # Process all items (checkboxes, labels, panels) in the container - foreach ($item in $items) { - if ($null -eq $item) { - continue - } + $targetPanel = if ($container -is [Windows.Controls.ScrollViewer]) { $container.Content } else { $container } + $items = if ($targetPanel -is [Windows.Controls.ItemsControl]) { $targetPanel.Items } else { $targetPanel.Children } - # ------------------------------------------------------------ - # Check if this is a category label (usually first Label) - # ------------------------------------------------------------ + $categoryLabel = $null + for ($i = 0; $i -lt $items.Count; $i++) { + $item = $items[$i] + if ($null -eq $item) { continue } - if ($item -is [Windows.Controls.Label] -or $item.GetType().Name -eq "Label") { - $categoryLabel = $item - # Initially hide category label; show it only if matches found - $item.Visibility = [Windows.Visibility]::Collapsed - } - - # ------------------------------------------------------------ - # Check if this is a DockPanel containing a tweak checkbox - # ------------------------------------------------------------ - - elseif ($item -is [Windows.Controls.DockPanel] -or $item.GetType().Name -eq "DockPanel") { - $checkbox = $null - $label = $null - - # Safely extract checkbox and label - $checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] -or $_.GetType().Name -eq "CheckBox" } | Select-Object -First 1 - $label = $item.Children | Where-Object { $_ -is [Windows.Controls.Label] -or $_.GetType().Name -eq "Label" } | Select-Object -First 1 - - # Check if tweak matches search criteria - $itemMatches = $false - - if ($null -ne $label) { - $labelContent = $label.Content - $labelToolTip = $label.ToolTip - - # Safely null-check properties - if ($null -eq $labelContent) { - $labelContent = "" - } - if ($null -eq $labelToolTip) { - $labelToolTip = "" - } - - # Convert to string and perform LITERAL matching - $labelContentStr = [string]$labelContent - $labelToolTipStr = [string]$labelToolTip - - # Use IndexOf for literal matching (no wildcard interpretation) - $contentMatch = $labelContentStr.IndexOf($searchTerm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 - $toolTipMatch = $labelToolTipStr.IndexOf($searchTerm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 - - if ($contentMatch -or $toolTipMatch) { - $itemMatches = $true - } - } - - # Set visibility based on match result - if ($itemMatches) { - $item.Visibility = [Windows.Visibility]::Visible - $categoryHasMatch = $true - } - else { - $item.Visibility = [Windows.Visibility]::Collapsed - } - } - - # ------------------------------------------------------------ - # Check if this is a StackPanel containing a tweak checkbox - # ------------------------------------------------------------ - - elseif ($item -is [Windows.Controls.StackPanel] -or $item.GetType().Name -eq "StackPanel") { - $checkbox = $null - $checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] -or $_.GetType().Name -eq "CheckBox" } | Select-Object -First 1 - - $itemMatches = $false - - if ($null -ne $checkbox) { - $checkboxContent = $checkbox.Content - $checkboxToolTip = $checkbox.ToolTip - - # Safely null-check properties - if ($null -eq $checkboxContent) { - $checkboxContent = "" - } - if ($null -eq $checkboxToolTip) { - $checkboxToolTip = "" - } - - # Convert to string and perform LITERAL matching - $checkboxContentStr = [string]$checkboxContent - $checkboxToolTipStr = [string]$checkboxToolTip - - # Match the preset key but not the "Preset key: " label added by Get-WinUtilEntryToolTip - $checkboxToolTipStr = $checkboxToolTipStr.Replace("Preset key: ", "") - - # Use IndexOf for literal matching (no wildcard interpretation) - $contentMatch = $checkboxContentStr.IndexOf($searchTerm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 - $toolTipMatch = $checkboxToolTipStr.IndexOf($searchTerm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 - - if ($contentMatch -or $toolTipMatch) { - $itemMatches = $true - } - } - - # Set visibility based on match result - if ($itemMatches) { - $item.Visibility = [Windows.Visibility]::Visible - $categoryHasMatch = $true - } - else { - $item.Visibility = [Windows.Visibility]::Collapsed - } - } - } - - # ------------------------------------------------------------ - # Update category label visibility and expanded/collapsed state - # ------------------------------------------------------------ - - if ($categoryHasMatch) { - # Show category label - if ($null -ne $categoryLabel) { - $categoryLabel.Visibility = [Windows.Visibility]::Visible - - # Update category label to expanded state (change "+" to "-") - $labelContent = $categoryLabel.Content - if ($null -ne $labelContent) { - $labelStr = [string]$labelContent + if ($item -is [Windows.Controls.Label]) { + $categoryLabel = $item + $item.Visibility = [Windows.Visibility]::Collapsed + continue + } - # Safe string replacement without -replace regex - if ($labelStr.StartsWith("+ ")) { - $expandedLabel = "- " + $labelStr.Substring(2) - $categoryLabel.Content = $expandedLabel - } - } - } - } + # Unified search for both DockPanel and StackPanel items + if ($item -is [Windows.Controls.DockPanel] -or $item -is [Windows.Controls.StackPanel]) { + $search = Get-ItemSearchText $item + $isMatch = ($search.Text -and $search.Text.IndexOf($SearchString, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) -or + ($search.Tip -and $search.Tip.IndexOf($SearchString, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) + + if ($isMatch) { + $item.Visibility = [Windows.Visibility]::Visible + $categoryHasMatch = $true + } else { + $item.Visibility = [Windows.Visibility]::Collapsed } } + } - # ---------------------------------------------------------------- - # Set category border visibility based on whether it has matches - # ---------------------------------------------------------------- - - if ($categoryHasMatch) { - $categoryBorder.Visibility = [Windows.Visibility]::Visible - } - else { - $categoryBorder.Visibility = [Windows.Visibility]::Collapsed + # Update category label and border visibility + if ($categoryHasMatch -and $null -ne $categoryLabel) { + $categoryLabel.Visibility = [Windows.Visibility]::Visible + $labelStr = [string]$categoryLabel.Content + if ($labelStr.StartsWith("+ ")) { + $categoryLabel.Content = "- " + $labelStr.Substring(2) + $sync.TweakCategoryAutoExpanded[($labelStr.Substring(2))] = $true } } + $categoryBorder.Visibility = if ($categoryHasMatch) { [Windows.Visibility]::Visible } else { [Windows.Visibility]::Collapsed } } - } - catch { - # Silent catch - UI elements may be disposed or in unexpected state - # Do not log to terminal as this function is called on every keystroke - $null = $_ + } catch { + # Log instead of silently swallowing - but only at DEBUG to avoid keystroke spam + Write-WinUtilLog -Level "DEBUG" -Component "Search" -Message "Tweaks search error: $($_.Exception.Message)" } } diff --git a/functions/private/Invoke-WinUtilCurrentSystem.ps1 b/functions/private/Invoke-WinUtilCurrentSystem.ps1 index cb23f552f4..b47314a5e7 100644 --- a/functions/private/Invoke-WinUtilCurrentSystem.ps1 +++ b/functions/private/Invoke-WinUtilCurrentSystem.ps1 @@ -17,10 +17,10 @@ Function Invoke-WinUtilCurrentSystem { ) if ($CheckBox -eq "choco") { $apps = (choco list | Select-String -Pattern "^\S+").Matches.Value - $sync.configs.applicationsHashtable.GetEnumerator() | ForEach-Object { - $packageId = ($_.Value.choco -split ";")[-1].Trim() + foreach ($app in $sync.configs.applicationsHashtable.GetEnumerator()) { + $packageId = ($app.Value.choco -split ";")[-1].Trim() if ($packageId -ne "na" -and $packageId -in $apps) { - Write-Output $_.Key + Write-Output $app.Key } } @@ -59,10 +59,10 @@ Function Invoke-WinUtilCurrentSystem { $idColumnStart = $Matches[1].Length } - $sync.configs.applicationsHashtable.GetEnumerator() | ForEach-Object { - $packageId = (($_.Value.winget -split ";")[-1] -replace "^msstore:", "").Trim() + foreach ($app in $sync.configs.applicationsHashtable.GetEnumerator()) { + $packageId = (($app.Value.winget -split ";")[-1] -replace "^msstore:", "").Trim() if ([string]::IsNullOrWhiteSpace($packageId) -or $packageId -eq "na") { - return + continue } $escapedPackageId = [regex]::Escape($packageId) @@ -70,7 +70,7 @@ Function Invoke-WinUtilCurrentSystem { # Skip rows whose Name has combining marks or emoji: they are more characters than columns and shift the offset $idColumnPattern = "(?im)(?<=^[^\r\n\p{M}\p{Cs}\p{Cf}]{$($idColumnStart - 1)}[^\S\r\n])$escapedPackageId(?=[^\S\r\n]|$)" if ($installedProgramText -match $packagePattern -or ($idColumnStart -and $installedProgramText -match $idColumnPattern)) { - Write-Output $_.Key + Write-Output $app.Key } } } @@ -80,29 +80,28 @@ Function Invoke-WinUtilCurrentSystem { if (!(Test-Path 'HKU:\')) {$null = (New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS)} $readErrorAction = if ($StopOnReadError) { "Stop" } else { "SilentlyContinue" } - $sync.configs.tweaks | Get-Member -MemberType NoteProperty | ForEach-Object { - - $Config = $psitem.Name - $entry = $sync.configs.tweaks.$Config + foreach ($prop in $sync.configs.tweaks.PSObject.Properties) { + $Config = $prop.Name + $entry = $prop.Value $registryKeys = $entry.registry $serviceKeys = $entry.service $entryType = $entry.Type if (($registryKeys -or $serviceKeys) -and $entryType -ne "Combobox") { - $Values = @() + $allMatch = $true if ($entryType -eq "Toggle") { if (-not (Get-WinUtilToggleStatus $Config ` -BypassCache:$BypassToggleStatusCache ` -StopOnReadError:$StopOnReadError)) { - $values += $False + $allMatch = $false } } else { $registryMatchCount = 0 $registryTotal = 0 - Foreach ($tweaks in $registryKeys) { - Foreach ($tweak in $tweaks) { + foreach ($tweaks in $registryKeys) { + foreach ($tweak in $tweaks) { $registryTotal++ $regstate = $null @@ -116,15 +115,9 @@ Function Invoke-WinUtilCurrentSystem { if ($null -eq $regstate) { switch ($tweak.DefaultState) { - "true" { - $regstate = $tweak.Value - } - "false" { - $regstate = $tweak.OriginalValue - } - default { - $regstate = $tweak.OriginalValue - } + "true" { $regstate = $tweak.Value } + "false" { $regstate = $tweak.OriginalValue } + default { $regstate = $tweak.OriginalValue } } } @@ -135,37 +128,32 @@ Function Invoke-WinUtilCurrentSystem { } if ($registryTotal -gt 0 -and $registryMatchCount -ne $registryTotal) { - $values += $False + $allMatch = $false } } - Foreach ($tweaks in $serviceKeys) { - Foreach ($tweak in $tweaks) { + foreach ($tweaks in $serviceKeys) { + foreach ($tweak in $tweaks) { try { $Service = Get-Service -Name $tweak.Name -ErrorAction $readErrorAction } catch { if ($StopOnReadError -and $_.FullyQualifiedErrorId -like "NoServiceFoundForGivenName*") { # A removed optional service means this tweak is not applied; it does # not make the registry and service state for every other tweak unknown. - $values += $False + $allMatch = $false continue } throw } - if ($Service) { - $actualValue = $Service.StartType - $expectedValue = $tweak.StartupType - if ($expectedValue -ne $actualValue) { - $values += $False - } - } elseif ($StopOnReadError) { - $values += $False + if (($Service -and $tweak.StartupType -ne $Service.StartType) -or + (-not $Service -and $StopOnReadError)) { + $allMatch = $false } } } - if ($values -notcontains $false) { + if ($allMatch) { Write-Output $Config } } diff --git a/functions/private/Invoke-WinUtilTweaks.ps1 b/functions/private/Invoke-WinUtilTweaks.ps1 index abbdd3322b..07c6c08c86 100644 --- a/functions/private/Invoke-WinUtilTweaks.ps1 +++ b/functions/private/Invoke-WinUtilTweaks.ps1 @@ -47,7 +47,7 @@ function Invoke-WinUtilTweaks { } } if ($sync.configs.tweaks.$CheckBox.service) { - $sync.configs.tweaks.$CheckBox.service | ForEach-Object { + foreach ($item in $sync.configs.tweaks.$CheckBox.service) { $changeservice = $true # KeepServiceStartup protects a service the user (or another tool) changed away from @@ -55,9 +55,9 @@ function Invoke-WinUtilTweaks { # OriginalType when the service still sits at the StartupType this tweak applies. # This compares current state, not history: a service already at StartupType # before apply is indistinguishable from one changed by this tweak. - $expectedType = if ($undo) { $psitem.StartupType } else { $psitem.$($values.OriginalService) } + $expectedType = if ($undo) { $item.StartupType } else { $item.$($values.OriginalService) } if ($KeepServiceStartup) { - $serviceName = $psitem.Name + $serviceName = $item.Name try { # Check if the service exists $service = Get-Service -Name $serviceName -ErrorAction Stop @@ -76,29 +76,28 @@ function Invoke-WinUtilTweaks { } if ($changeservice) { - Set-WinUtilService -Name $psitem.Name -StartupType $psitem.$($values.Service) + Set-WinUtilService -Name $item.Name -StartupType $item.$($values.Service) } } } if ($sync.configs.tweaks.$CheckBox.registry) { - $sync.configs.tweaks.$CheckBox.registry | Where-Object { -not $psitem.Values } | ForEach-Object { - Set-WinUtilRegistry -Name $psitem.Name -Path $psitem.Path -Type $psitem.Type -Value $psitem.$($values.registry) + foreach ($reg in $sync.configs.tweaks.$CheckBox.registry) { + if ($reg.Values) { continue } + Set-WinUtilRegistry -Name $reg.Name -Path $reg.Path -Type $reg.Type -Value $reg.$($values.registry) } } if ($sync.configs.tweaks.$CheckBox.$($values.ScriptType)) { - $sync.configs.tweaks.$CheckBox.$($values.ScriptType) | ForEach-Object { - $Scriptblock = [scriptblock]::Create($psitem) + foreach ($scr in $sync.configs.tweaks.$CheckBox.$($values.ScriptType)) { + $Scriptblock = [scriptblock]::Create($scr) Invoke-WinUtilScript -ScriptBlock $scriptblock -Name $CheckBox } } - if (!$undo) { - if($sync.configs.tweaks.$CheckBox.appx) { - $sync.configs.tweaks.$CheckBox.appx | ForEach-Object { - Remove-WinUtilAPPX -Name $psitem - } - Remove-WinUtilProvisionedAPPX -PackageList $sync.configs.tweaks.$CheckBox.appx + if (!$undo -and $sync.configs.tweaks.$CheckBox.appx) { + foreach ($pkg in $sync.configs.tweaks.$CheckBox.appx) { + Remove-WinUtilAPPX -Name $pkg } + Remove-WinUtilProvisionedAPPX -PackageList $sync.configs.tweaks.$CheckBox.appx } $errorCount = [int]$global:WinUtilJobErrorCount - $errorsBefore if ($errorCount -gt 0) { diff --git a/functions/private/Show-CustomDialog.ps1 b/functions/private/Show-CustomDialog.ps1 index 9ae289e6e0..1abbb0c301 100644 --- a/functions/private/Show-CustomDialog.ps1 +++ b/functions/private/Show-CustomDialog.ps1 @@ -130,57 +130,36 @@ function Show-CustomDialog { $dialog.Content = $border - # Create a grid for layout inside the Border $grid = New-Object Windows.Controls.Grid $border.Child = $grid - - # Uncomment the following line to show gridlines - #$grid.ShowGridLines = $true - - # Add the following line to set the background color of the grid $grid.Background = [Windows.Media.Brushes]::Transparent - # Add the following line to make the Grid stretch $grid.HorizontalAlignment = [Windows.HorizontalAlignment]::Stretch $grid.VerticalAlignment = [Windows.VerticalAlignment]::Stretch - - # Add the following line to make the Border stretch $border.HorizontalAlignment = [Windows.HorizontalAlignment]::Stretch $border.VerticalAlignment = [Windows.VerticalAlignment]::Stretch - # Set up Row Definitions - $row0 = New-Object Windows.Controls.RowDefinition - $row0.Height = [Windows.GridLength]::Auto - - $row1 = New-Object Windows.Controls.RowDefinition - $row1.Height = [Windows.GridLength]::new(1, [Windows.GridUnitType]::Star) - - $row2 = New-Object Windows.Controls.RowDefinition - $row2.Height = [Windows.GridLength]::Auto - - # Add Row Definitions to Grid + $row0 = New-Object Windows.Controls.RowDefinition; $row0.Height = [Windows.GridLength]::Auto + $row1 = New-Object Windows.Controls.RowDefinition; $row1.Height = [Windows.GridLength]::new(1, [Windows.GridUnitType]::Star) + $row2 = New-Object Windows.Controls.RowDefinition; $row2.Height = [Windows.GridLength]::Auto $grid.RowDefinitions.Add($row0) $grid.RowDefinitions.Add($row1) $grid.RowDefinitions.Add($row2) - # Add StackPanel for horizontal layout with margins $stackPanel = New-Object Windows.Controls.StackPanel - $stackPanel.Margin = New-Object Windows.Thickness(10) # Add margins around the stack panel + $stackPanel.Margin = New-Object Windows.Thickness(10) $stackPanel.Orientation = [Windows.Controls.Orientation]::Horizontal - $stackPanel.HorizontalAlignment = [Windows.HorizontalAlignment]::Left # Align to the left - $stackPanel.VerticalAlignment = [Windows.VerticalAlignment]::Top # Align to the top - + $stackPanel.HorizontalAlignment = [Windows.HorizontalAlignment]::Left + $stackPanel.VerticalAlignment = [Windows.VerticalAlignment]::Top $grid.Children.Add($stackPanel) - [Windows.Controls.Grid]::SetRow($stackPanel, 0) # Set the row to the second row (0-based index) + [Windows.Controls.Grid]::SetRow($stackPanel, 0) - # Add SVG path to the stack panel $stackPanel.Children.Add((Invoke-WinUtilAssets -Type "logo" -Size $LogoSize)) - # Add "Winutil" text $winutilTextBlock = New-Object Windows.Controls.TextBlock $winutilTextBlock.Text = "WinUtil" $winutilTextBlock.FontSize = $HeaderFontSize $winutilTextBlock.Foreground = $LogoColor - $winutilTextBlock.Margin = New-Object Windows.Thickness(10, 10, 10, 5) # Add margins around the text block + $winutilTextBlock.Margin = New-Object Windows.Thickness(10, 10, 10, 5) $stackPanel.Children.Add($winutilTextBlock) # Add TextBlock for information with text wrapping and margins $messageTextBlock = New-Object Windows.Controls.TextBlock diff --git a/functions/private/Test-WinUtilPackageManager.ps1 b/functions/private/Test-WinUtilPackageManager.ps1 index c54d86e7be..5fdee973f6 100644 --- a/functions/private/Test-WinUtilPackageManager.ps1 +++ b/functions/private/Test-WinUtilPackageManager.ps1 @@ -1,37 +1,28 @@ function Test-WinUtilPackageManager { <# - .SYNOPSIS - Checks if WinGet and/or Choco are installed - + Checks if WinGet and/or Choco are installed. .PARAMETER winget - Check if WinGet is installed - + Check if WinGet is installed. .PARAMETER choco - Check if Chocolatey is installed - + Check if Chocolatey is installed. #> - Param( [System.Management.Automation.SwitchParameter]$winget, [System.Management.Automation.SwitchParameter]$choco ) - if ($winget) { - if (Get-Command winget -ErrorAction SilentlyContinue) { - $status = "installed" - } else { - $status = "not-installed" - } - } + # Handle missing switch - callers rely on the return value + if (-not $winget -and -not $choco) { return "not-installed" } - if ($choco) { - if (Get-Command choco -ErrorAction SilentlyContinue) { - $status = "installed" - } else { - $status = "not-installed" + $cmds = @() + if ($winget) { $cmds += "winget" } + if ($choco) { $cmds += "choco" } + + foreach ($cmd in $cmds) { + if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) { + return "not-installed" } } - - return $status + return "installed" } diff --git a/functions/public/Invoke-WPFFixesUpdate.ps1 b/functions/public/Invoke-WPFFixesUpdate.ps1 index c7b899e6b4..26e8571f35 100644 --- a/functions/public/Invoke-WPFFixesUpdate.ps1 +++ b/functions/public/Invoke-WPFFixesUpdate.ps1 @@ -42,14 +42,13 @@ function Invoke-WPFFixesUpdate { Write-Progress -Id 0 -Activity "Repairing Windows Update" -Status "Stopping Windows Update Services..." -PercentComplete 10 # Stop the Windows Update Services - Write-Progress -Id 2 -ParentId 0 -Activity "Stopping Services" -Status "Stopping BITS..." -PercentComplete 0 - Stop-Service -Name BITS -Force - Write-Progress -Id 2 -ParentId 0 -Activity "Stopping Services" -Status "Stopping wuauserv..." -PercentComplete 20 - Stop-Service -Name wuauserv -Force - Write-Progress -Id 2 -ParentId 0 -Activity "Stopping Services" -Status "Stopping appidsvc..." -PercentComplete 40 - Stop-Service -Name appidsvc -Force - Write-Progress -Id 2 -ParentId 0 -Activity "Stopping Services" -Status "Stopping cryptsvc..." -PercentComplete 60 - Stop-Service -Name cryptsvc -Force + $services = @("BITS", "wuauserv", "appidsvc", "cryptsvc") + for ($i = 0; $i -lt $services.Count; $i++) { + $svc = $services[$i] + $pct = [int](($i / $services.Count) * 100) + Write-Progress -Id 2 -ParentId 0 -Activity "Stopping Services" -Status "Stopping $svc..." -PercentComplete $pct + Stop-Service -Name $svc -Force + } Write-Progress -Id 2 -ParentId 0 -Activity "Stopping Services" -Status "Completed" -PercentComplete 100 @@ -102,7 +101,9 @@ function Invoke-WPFFixesUpdate { "wuweb.dll", "qmgr.dll", "qmgrprxy.dll", "wucltux.dll", "muweb.dll", "wuwebv.dll" ) foreach ($dll in $DLLs) { - Write-Progress -Id 5 -ParentId 0 -Activity "Reregistering DLLs" -Status "Registering $dll..." -PercentComplete ($i / $DLLs.Count * 100) + if ($i % 5 -eq 0 -or $i -eq ($DLLs.Count - 1)) { + Write-Progress -Id 5 -ParentId 0 -Activity "Reregistering DLLs" -Status "Registering $dll..." -PercentComplete (($i / $DLLs.Count) * 100) + } $i++ Start-Process -NoNewWindow -FilePath "regsvr32.exe" -ArgumentList "/s", $dll } @@ -202,15 +203,7 @@ function Invoke-WPFFixesUpdate { Show-WinUtilMessage -Message "Stock settings loaded.`n Please reboot your computer" -Title "Reset Windows Update" -Button "OK" -Icon "Information" | Out-Null # Remove the progress bars - Write-Progress -Id 0 -Activity "Repairing Windows Update" -Completed - Write-Progress -Id 1 -Activity "Scanning for corruption" -Completed - Write-Progress -Id 2 -Activity "Stopping Services" -Completed - Write-Progress -Id 3 -Activity "Renaming/Removing Files" -Completed - Write-Progress -Id 4 -Activity "Resetting the WU Service Security Descriptors" -Completed - Write-Progress -Id 5 -Activity "Reregistering DLLs" -Completed - Write-Progress -Id 6 -Activity "Removing Group Policy Windows Update settings" -Completed - Write-Progress -Id 7 -Activity "Resetting WinSock" -Completed - Write-Progress -Id 8 -Activity "Deleting BITS jobs" -Completed - Write-Progress -Id 9 -Activity "Starting Windows Update Services" -Completed - Write-Progress -Id 10 -Activity "Forcing discovery" -Completed + foreach ($id in 0..10) { + Write-Progress -Id $id -Activity "Completed" -Completed -ErrorAction SilentlyContinue + } } diff --git a/functions/public/Invoke-WPFUIElements.ps1 b/functions/public/Invoke-WPFUIElements.ps1 index 053dbe8b6e..c3c35ff295 100644 --- a/functions/public/Invoke-WPFUIElements.ps1 +++ b/functions/public/Invoke-WPFUIElements.ps1 @@ -57,22 +57,15 @@ function Invoke-WPFUIElements { $targetGrid.ColumnDefinitions.Add($colDef) | Out-Null } - # Convert PSCustomObject to Hashtable - $configHashtable = @{} - $configVariable.PSObject.Properties.Name | ForEach-Object { - $configHashtable[$_] = $configVariable.$_ - } - $radioButtonGroups = @{} - $organizedData = @{} - # Iterate through JSON data and organize by panel and category - foreach ($entry in $configHashtable.Keys) { - $entryInfo = $configHashtable[$entry] - # Create an object for the application + # Iterate through JSON data and organize by panel and category using generic lists + foreach ($prop in $configVariable.PSObject.Properties) { + $entryInfo = $prop.Value + $entryObject = [PSCustomObject]@{ - Name = $entry + Name = $prop.Name Category = $entryInfo.Category Content = $entryInfo.Content Panel = if ($entryInfo.Panel) { $entryInfo.Panel } else { "0" } @@ -84,20 +77,20 @@ function Invoke-WPFUIElements { Registry = $entryInfo.registry Checked = $entryInfo.Checked ButtonWidth = $entryInfo.ButtonWidth - GroupName = $entryInfo.GroupName # Added for RadioButton groupings + GroupName = $entryInfo.GroupName } - if (-not $organizedData.ContainsKey($entryObject.Panel)) { - $organizedData[$entryObject.Panel] = @{} + $panel = $entryObject.Panel + if (-not $organizedData.ContainsKey($panel)) { + $organizedData[$panel] = @{} } - if (-not $organizedData[$entryObject.Panel].ContainsKey($entryObject.Category)) { - $organizedData[$entryObject.Panel][$entryObject.Category] = @() + $cat = $entryObject.Category + if (-not $organizedData[$panel].ContainsKey($cat)) { + $organizedData[$panel][$cat] = [System.Collections.Generic.List[object]]::new() } - # Store application data in an array under the category - $organizedData[$entryObject.Panel][$entryObject.Category] += $entryObject - + $organizedData[$panel][$cat].Add($entryObject) } # Initialize panel count diff --git a/pester/package.Tests.ps1 b/pester/package.Tests.ps1 index 7924ec4e21..20aeeb435f 100644 --- a/pester/package.Tests.ps1 +++ b/pester/package.Tests.ps1 @@ -117,6 +117,7 @@ Describe "Get-WinUtilSelectedPackages" { Describe "Test-WinUtilPackageManager" { BeforeEach { Mock Write-WinUtilJobBanner { } + Mock Write-Host { } } It "reports winget installed when the command exists" { @@ -130,6 +131,7 @@ Describe "Test-WinUtilPackageManager" { $Name -eq "winget" -and $ErrorAction -eq "SilentlyContinue" } Should -Invoke -CommandName Write-WinUtilJobBanner -Times 0 -Exactly + Should -Invoke -CommandName Write-Host -Times 0 -Exactly } It "reports choco not installed when the command is missing" { @@ -144,6 +146,23 @@ Describe "Test-WinUtilPackageManager" { } Should -Invoke -CommandName Write-WinUtilJobBanner -Times 0 -Exactly } + + It "requires both requested managers to be installed" -TestCases @( + @{ WingetPresent = $true; ChocoPresent = $true; Expected = "installed" } + @{ WingetPresent = $false; ChocoPresent = $true; Expected = "not-installed" } + @{ WingetPresent = $true; ChocoPresent = $false; Expected = "not-installed" } + ) { + param($WingetPresent, $ChocoPresent, $Expected) + Mock Get-Command { if ($WingetPresent) { [pscustomobject]@{ Name = "winget" } } } -ParameterFilter { $Name -eq "winget" } + Mock Get-Command { if ($ChocoPresent) { [pscustomobject]@{ Name = "choco" } } } -ParameterFilter { $Name -eq "choco" } + + Test-WinUtilPackageManager -winget -choco | Should -Be $Expected + Should -Invoke -CommandName Write-Host -Times 0 -Exactly + } + + It "reports not installed when no manager is requested" { + Test-WinUtilPackageManager | Should -Be "not-installed" + } } Describe "Install-WinUtilProgramWinget" { diff --git a/pester/sanity.Tests.ps1 b/pester/sanity.Tests.ps1 index 877a6c7b41..efa72a2df7 100644 --- a/pester/sanity.Tests.ps1 +++ b/pester/sanity.Tests.ps1 @@ -33,7 +33,7 @@ BeforeAll { $env:WINUTIL_TEST_PARSE_PATHS = @($Path) -join [Environment]::NewLine $parseScript = @' $ErrorActionPreference = 'Stop' -$paths = $env:WINUTIL_TEST_PARSE_PATHS -split "`r?`n" | Where-Object { $_ } +$paths = $env:WINUTIL_TEST_PARSE_PATHS -split '\r?\n' | Where-Object { $_ } $failed = @() foreach ($path in $paths) { @@ -43,7 +43,7 @@ foreach ($path in $paths) { if ($syntaxErrors.Count -ne 0) { $messages = $syntaxErrors | ForEach-Object { $_.Message } - $failed += "[$path] $($messages -join '; ')" + $failed += ('[' + $path + '] ' + ($messages -join '; ')) } } diff --git a/pester/search-filter-wpf.Tests.ps1 b/pester/search-filter-wpf.Tests.ps1 new file mode 100644 index 0000000000..30a8127fed --- /dev/null +++ b/pester/search-filter-wpf.Tests.ps1 @@ -0,0 +1,30 @@ +# Run search behavior against real WPF controls in a fresh Windows PowerShell process. +# Other Pester files install lightweight UI types that cannot coexist with WPF. +Describe "Search filtering with real WPF controls" { + It "preserves search and category behavior in Windows PowerShell 5.1" -Skip:($env:OS -ne "Windows_NT") { + $env:WINUTIL_WPF_TEST_ROOT = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path + $env:WINUTIL_WPF_PESTER_PATH = Join-Path (Get-Module Pester).ModuleBase "Pester.psd1" + try { + $scriptText = @' +$ErrorActionPreference = 'Stop' +Add-Type -AssemblyName PresentationFramework +if ([Windows.Controls.Border].Assembly.GetName().Name -ne 'PresentationFramework') { + throw 'The search integration test requires real WPF controls.' +} +Import-Module $env:WINUTIL_WPF_PESTER_PATH -Force +if ((Get-Module Pester).Version -ne [version]"5.8.0") { + throw "The WPF search test requires Pester 5.8.0." +} +Invoke-Pester -Path (Join-Path $env:WINUTIL_WPF_TEST_ROOT 'pester/search-filter.Tests.ps1') -Output Detailed -CI +'@ + $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($scriptText)) + $output = & powershell.exe -NoLogo -NoProfile -NonInteractive -STA -EncodedCommand $encodedCommand 2>&1 + $result = $LASTEXITCODE + Write-Host ($output -join [Environment]::NewLine) + $result | Should -Be 0 + } finally { + Remove-Item Env:WINUTIL_WPF_TEST_ROOT -ErrorAction SilentlyContinue + Remove-Item Env:WINUTIL_WPF_PESTER_PATH -ErrorAction SilentlyContinue + } + } +} diff --git a/pester/search-filter.Tests.ps1 b/pester/search-filter.Tests.ps1 index 9eff0e71c7..9e0fad96b2 100644 --- a/pester/search-filter.Tests.ps1 +++ b/pester/search-filter.Tests.ps1 @@ -93,6 +93,11 @@ namespace Windows.Controls } } + public class ScrollViewer + { + public object Content { get; set; } + } + public class Label { public object Content { get; set; } @@ -114,6 +119,11 @@ namespace Windows.Controls . (Join-Path $script:repoRoot "functions\private\Find-TweaksByNameOrDescription.ps1") . (Join-Path $script:repoRoot "functions\private\Get-WinUtilEntryToolTip.ps1") + function Write-WinUtilLog { + param($Level, $Component, $Message) + throw $Message + } + function script:New-WinUtilSearchCollection { return ,[System.Collections.ArrayList]::new() } @@ -255,7 +265,9 @@ namespace Windows.Controls function script:New-WinUtilTweakCategory { param( [string]$Label, - [object[]]$Items + [object[]]$Items, + [ValidateSet("ItemsControl", "StackPanel", "ScrollViewer")] + [string]$ContainerType = "ItemsControl" ) $categoryLabel = [Windows.Controls.Label]::new() @@ -269,7 +281,23 @@ namespace Windows.Controls } $dockPanel = [Windows.Controls.DockPanel]::new() - $null = $dockPanel.Children.Add($itemsControl) + if ($ContainerType -eq "ItemsControl") { + $null = $dockPanel.Children.Add($itemsControl) + } else { + $stack = [Windows.Controls.StackPanel]::new() + $categoryItems = @($itemsControl.Items) + $itemsControl.Items.Clear() + foreach ($item in $categoryItems) { + $null = $stack.Children.Add($item) + } + if ($ContainerType -eq "ScrollViewer") { + $scroll = [Windows.Controls.ScrollViewer]::new() + $scroll.Content = $stack + $null = $dockPanel.Children.Add($scroll) + } else { + $null = $dockPanel.Children.Add($stack) + } + } $border = [Windows.Controls.Border]::new() $border.Child = $dockPanel @@ -476,27 +504,53 @@ Describe "Find-AppsByNameOrDescription" { } Describe "Find-TweaksByNameOrDescription" { + It "restores collapsed categories after literal search in " -TestCases @( + @{ ContainerType = "StackPanel" } + @{ ContainerType = "ScrollViewer" } + ) { + param($ContainerType) + $match = New-WinUtilTweakLabelItem -Content "Tool [abc]" + $other = New-WinUtilTweakCheckboxItem -Content "Other tool" + $category = New-WinUtilTweakCategory -Label "+ Privacy" -Items @($match, $other) -ContainerType $ContainerType + New-WinUtilTweakSearchContext -TweaksPanel (New-WinUtilTweakPanel -Categories @($category)) + + Find-TweaksByNameOrDescription -SearchString "[abc]" + $match.Visibility | Should -Be ([Windows.Visibility]::Visible) + $other.Visibility | Should -Be ([Windows.Visibility]::Collapsed) + $category.Label.Content | Should -Be "- Privacy" + + Find-TweaksByNameOrDescription -SearchString "" + $category.Label.Content | Should -Be "+ Privacy" + $match.Visibility | Should -Be ([Windows.Visibility]::Collapsed) + $other.Visibility | Should -Be ([Windows.Visibility]::Collapsed) + } + 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) + It "restores category labels and respects collapsed category state for empty search" { + $collapsedItem = New-WinUtilTweakLabelItem -Content "Disable Telemetry" -ToolTip "Stop tracking" + $expandedItem = New-WinUtilTweakCheckboxItem -Content "Show Extensions" -ToolTip "File extension display" + $collapsedCategory = New-WinUtilTweakCategory -Label "+ Privacy" -Items @($collapsedItem) + $expandedCategory = New-WinUtilTweakCategory -Label "- Explorer" -Items @($expandedItem) + $expandedItem.Visibility = [Windows.Visibility]::Collapsed + $collapsedCategory.Label.Visibility = [Windows.Visibility]::Collapsed + $collapsedCategory.Border.Visibility = [Windows.Visibility]::Collapsed + $expandedCategory.Border.Visibility = [Windows.Visibility]::Collapsed + $panel = New-WinUtilTweakPanel -Categories @($collapsedCategory, $expandedCategory) 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) + $collapsedCategory.Border.Visibility | Should -Be ([Windows.Visibility]::Visible) + $collapsedCategory.Label.Visibility | Should -Be ([Windows.Visibility]::Visible) + $collapsedCategory.Label.Content | Should -Be "+ Privacy" + $collapsedItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed) + $expandedCategory.Border.Visibility | Should -Be ([Windows.Visibility]::Visible) + $expandedCategory.Label.Visibility | Should -Be ([Windows.Visibility]::Visible) + $expandedCategory.Label.Content | Should -Be "- Explorer" + $expandedItem.Visibility | Should -Be ([Windows.Visibility]::Visible) } It "shows tweak matches by label tooltip and checkbox content" { diff --git a/pester/system-helpers.Tests.ps1 b/pester/system-helpers.Tests.ps1 index 933d6803fc..cf09b79c7b 100644 --- a/pester/system-helpers.Tests.ps1 +++ b/pester/system-helpers.Tests.ps1 @@ -247,6 +247,39 @@ Describe "Invoke-WinUtilCurrentSystem installed apps" { } } +Describe "Invoke-WinUtilCurrentSystem service tweaks" { + BeforeEach { + $script:sync = [Hashtable]::Synchronized(@{ + configs = @{ + tweaks = [pscustomobject]@{ + WPFServiceTweak = [pscustomobject]@{ + service = @([pscustomobject]@{ Name = "OptionalService"; StartupType = "Disabled" }) + } + } + } + }) + Mock Test-Path { $true } + Mock Get-Service { $null } + } + + AfterEach { + Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue + } + + It "tolerates a missing optional service during UI detection" { + @(Invoke-WinUtilCurrentSystem -CheckBox "tweaks") | Should -Be @("WPFServiceTweak") + } + + It "marks a missing service as a mismatch during strict report collection" { + @(Invoke-WinUtilCurrentSystem -CheckBox "tweaks" -StopOnReadError).Count | Should -Be 0 + } + + It "marks a tweak applied when its service startup type matches" { + Mock Get-Service { [pscustomobject]@{ StartType = "Disabled" } } + @(Invoke-WinUtilCurrentSystem -CheckBox "tweaks") | Should -Be @("WPFServiceTweak") + } +} + Describe "Set-WinUtilRegistry" { BeforeEach { $script:testPathResults = @{} diff --git a/pester/xaml.Tests.ps1 b/pester/xaml.Tests.ps1 index 12eea9d808..a222090aa5 100644 --- a/pester/xaml.Tests.ps1 +++ b/pester/xaml.Tests.ps1 @@ -489,6 +489,7 @@ Describe "XAML and sync wiring" { "AppCategoryChips", "SelectedAppCategories", "AppCategoryAutoExpanded", + "TweakCategoryAutoExpanded", "RenderedAssetCache", "ToggleStatusCache", "InstallAppRenderQueue",