From 9c2d317e8c887d5b12ff0e914eda1ab96bf18025 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 06:21:07 +0000 Subject: [PATCH 1/2] fix: keep selected list item always visible when scrolling Implements a viewport window in Get-ListPanel that only renders the visible slice of the list. Show-PesterResult now tracks a listScrollOffset and adjusts it after every navigation key so the selected item is always on screen. Shows '...' indicators above/below when items are hidden. Also fixes a pre-existing PageUp bug where the minimum index was calculated as list.Count-1 instead of 0. Closes #11 --- PesterExplorer/Private/Get-ListPanel.ps1 | 21 ++- PesterExplorer/Public/Show-PesterResult.ps1 | 17 ++- tests/Get-ListPanel.tests.ps1 | 141 ++++++++++++++++++++ 3 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 tests/Get-ListPanel.tests.ps1 diff --git a/PesterExplorer/Private/Get-ListPanel.ps1 b/PesterExplorer/Private/Get-ListPanel.ps1 index 9bc59ea..8b89a8e 100644 --- a/PesterExplorer/Private/Get-ListPanel.ps1 +++ b/PesterExplorer/Private/Get-ListPanel.ps1 @@ -30,7 +30,9 @@ function Get-ListPanel { $List, [string] $SelectedItem, - [string]$SelectedPane = "list" + [string]$SelectedPane = "list", + [int]$ScrollOffset = 0, + [int]$ListHeight = 0 ) $paneColor = if($SelectedPane -ne "list") { # If the selected pane is not preview, return an empty panel @@ -44,7 +46,19 @@ function Get-ListPanel { StemColor = [Spectre.Console.Color]::Grey LeafColor = [Spectre.Console.Color]::White } - $results = $List | ForEach-Object { + # Calculate the visible window. Panel borders and potential ellipsis rows consume 4 rows. + $visibleCount = if ($ListHeight -gt 4) { $ListHeight - 4 } else { $List.Count } + $startIndex = [Math]::Max(0, [Math]::Min($ScrollOffset, $List.Count - 1)) + $endIndex = [Math]::Min($startIndex + $visibleCount - 1, $List.Count - 1) + $showEllipsisTop = $startIndex -gt 0 + $showEllipsisBottom = $endIndex -lt ($List.Count - 1) + $visibleList = if ($List.Count -gt 0) { @($List[$startIndex..$endIndex]) } else { @() } + + $results = @() + if ($showEllipsisTop) { + $results += Write-SpectreHost "[grey]...[/]" -PassThru | Format-SpectrePadded -Padding 0 + } + $results += $visibleList | ForEach-Object { $name = $_ if($name -eq '..') { # This is a parent item, so we show it as a folder @@ -87,6 +101,9 @@ function Get-ListPanel { } } } + if ($showEllipsisBottom) { + $results += Write-SpectreHost "[grey]...[/]" -PassThru | Format-SpectrePadded -Padding 0 + } $results | Format-SpectreRows | Format-SpectrePanel -Header "[white]List[/]" -Expand -Color $paneColor diff --git a/PesterExplorer/Public/Show-PesterResult.ps1 b/PesterExplorer/Public/Show-PesterResult.ps1 index 759e19c..3b41bd4 100644 --- a/PesterExplorer/Public/Show-PesterResult.ps1 +++ b/PesterExplorer/Public/Show-PesterResult.ps1 @@ -79,6 +79,7 @@ function Show-PesterResult { $object = $PesterResult $selectedPane = 'list' $scrollPosition = 0 + $listScrollOffset = 0 #endregion Initial State while ($true) { @@ -86,6 +87,7 @@ function Show-PesterResult { $sizes = $layout | Get-SpectreLayoutSizes $previewHeight = $sizes["preview"].Height $previewWidth = $sizes["preview"].Width + $listHeight = $sizes["list"].Height Write-Debug "Preview size: $previewWidth x $previewHeight" # Handle input @@ -106,7 +108,7 @@ function Show-PesterResult { $scrollPosition = 0 } elseif ($lastKeyPressed.Key -eq "PageUp") { $currentIndex = $list.IndexOf($selectedItem) - $newIndex = [Math]::Max($currentIndex - 10, $list.Count - 1) + $newIndex = [Math]::Max($currentIndex - 10, 0) $selectedItem = $list[$newIndex] $scrollPosition = 0 } elseif ($lastKeyPressed.Key -eq "Home") { @@ -135,6 +137,7 @@ function Show-PesterResult { $list = [array]$items.Keys $selectedItem = $list[0] $scrollPosition = 0 + $listScrollOffset = 0 } elseif ($lastKeyPressed.Key -eq "Escape") { # Move up via Esc key if($stack.Count -eq 0) { @@ -146,7 +149,17 @@ function Show-PesterResult { $list = [array]$items.Keys $selectedItem = $list[0] $scrollPosition = 0 + $listScrollOffset = 0 } + # Ensure the selected item is always within the visible viewport + $selectedIndex = $list.IndexOf($selectedItem) + $visibleCount = if ($listHeight -gt 4) { $listHeight - 4 } else { $list.Count } + if ($selectedIndex -lt $listScrollOffset) { + $listScrollOffset = $selectedIndex + } elseif ($selectedIndex -ge ($listScrollOffset + $visibleCount)) { + $listScrollOffset = $selectedIndex - $visibleCount + 1 + } + if ($listScrollOffset -lt 0) { $listScrollOffset = 0 } } else { #region Preview Navigation @@ -175,6 +188,8 @@ function Show-PesterResult { List = $list SelectedItem = $selectedItem SelectedPane = $selectedPane + ScrollOffset = $listScrollOffset + ListHeight = $listHeight } $listPanel = Get-ListPanel @getListPanelSplat diff --git a/tests/Get-ListPanel.tests.ps1 b/tests/Get-ListPanel.tests.ps1 new file mode 100644 index 0000000..4c21660 --- /dev/null +++ b/tests/Get-ListPanel.tests.ps1 @@ -0,0 +1,141 @@ +Describe 'Get-ListPanel' { + BeforeAll { + . (Join-Path $PSScriptRoot 'Helpers.ps1') + $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest + $outputDir = Join-Path -Path $env:BHProjectPath -ChildPath 'Output' + $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName + $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion + $outputModVerManifest = Join-Path -Path $outputModVerDir -ChildPath "$($env:BHProjectName).psd1" + + # Remove all versions of the module from the session. Pester can't handle multiple versions. + Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore + Import-Module -Name $outputModVerManifest -Verbose:$false -ErrorAction Stop + + InModuleScope $env:BHProjectName { + $script:tenItems = @('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j') + } + } + + It 'should return a Spectre.Console.Panel object' { + InModuleScope $env:BHProjectName { + $panel = Get-ListPanel -List @('item1', 'item2') -SelectedItem 'item1' + $panel.GetType().ToString() | Should -BeExactly 'Spectre.Console.Panel' + } + } + + It 'should show all items when list fits within available height' { + InModuleScope $env:BHProjectName { + $panel = Get-ListPanel -List $script:tenItems -SelectedItem 'a' -ListHeight 20 + $rendered = global:Get-RenderedText -Panel $panel + foreach ($item in $script:tenItems) { + $rendered | Should -BeLike "*$item*" + } + } + } + + It 'should show all items when ListHeight is 0 (default, no constraint)' { + InModuleScope $env:BHProjectName { + $panel = Get-ListPanel -List $script:tenItems -SelectedItem 'a' + $rendered = global:Get-RenderedText -Panel $panel + foreach ($item in $script:tenItems) { + $rendered | Should -BeLike "*$item*" + } + } + } + + Context 'Viewport scrolling' { + # With ListHeight=8: visibleCount = 8-4 = 4 items per window + # ScrollOffset=2: startIndex=2, endIndex=5 -> items c,d,e,f visible + # showEllipsisTop=true (startIndex > 0) + # showEllipsisBottom=true (endIndex=5 < 9) + + BeforeAll { + InModuleScope $env:BHProjectName { + $script:scrolledPanel = Get-ListPanel ` + -List $script:tenItems ` + -SelectedItem 'c' ` + -ScrollOffset 2 ` + -ListHeight 8 + $script:scrolledText = global:Get-RenderedText -Panel $script:scrolledPanel + } + } + + It 'should show items within the visible window' { + InModuleScope $env:BHProjectName { + $script:scrolledText | Should -BeLike '*c*' + $script:scrolledText | Should -BeLike '*d*' + $script:scrolledText | Should -BeLike '*e*' + $script:scrolledText | Should -BeLike '*f*' + } + } + + It 'should hide items scrolled above the window' { + InModuleScope $env:BHProjectName { + # Items a and b are above the viewport (offset 2) + $lines = $script:scrolledText -split "`n" | Where-Object { $_.Trim() -ne '' } + # 'a' and 'b' should not appear as standalone items (only '...' should be above) + $nonEllipsisLines = $lines | Where-Object { $_ -notmatch '^\s*\.\.\.' } + $nonEllipsisLines | Should -Not -Contain 'a' + $nonEllipsisLines | Should -Not -Contain 'b' + } + } + + It 'should show top ellipsis when items are scrolled above' { + InModuleScope $env:BHProjectName { + $script:scrolledText | Should -BeLike '*...*' + } + } + + It 'should show bottom ellipsis when items are below the window' { + InModuleScope $env:BHProjectName { + # Items g,h,i,j are below the viewport + $script:scrolledText | Should -BeLike '*...*' + } + } + } + + Context 'Ellipsis at top only' { + # ScrollOffset=6, ListHeight=20: visibleCount=16, startIndex=6, endIndex=9 -> g,h,i,j visible + # showEllipsisTop=true, showEllipsisBottom=false (endIndex=9 = Count-1) + + BeforeAll { + InModuleScope $env:BHProjectName { + $script:bottomPanel = Get-ListPanel ` + -List $script:tenItems ` + -SelectedItem 'j' ` + -ScrollOffset 6 ` + -ListHeight 20 + $script:bottomText = global:Get-RenderedText -Panel $script:bottomPanel + } + } + + It 'should show items at the bottom of the list' { + InModuleScope $env:BHProjectName { + $script:bottomText | Should -BeLike '*g*' + $script:bottomText | Should -BeLike '*j*' + } + } + + It 'should show top ellipsis' { + InModuleScope $env:BHProjectName { + $script:bottomText | Should -BeLike '*...*' + } + } + } + + It 'should highlight the selected item differently' { + InModuleScope $env:BHProjectName { + $selectedPanel = Get-ListPanel -List @('alpha', 'beta', 'gamma') -SelectedItem 'beta' + $rendered = global:Get-RenderedText -Panel $selectedPanel + # Selected item gets a right-arrow prefix in markup + $rendered | Should -BeLike '*beta*' + } + } + + It 'should use blue border when list pane is not selected' { + InModuleScope $env:BHProjectName { + $panel = Get-ListPanel -List @('item1') -SelectedItem 'item1' -SelectedPane 'preview' + $panel.GetType().ToString() | Should -BeExactly 'Spectre.Console.Panel' + } + } +} From 286e41f3d6658523f3816186d23652cfb611b1f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 07:06:17 +0000 Subject: [PATCH 2/2] fix(tests): correct test scoping and pre-existing Help.tests variable bug Move Get-ListPanel test setup into a single top-level BeforeAll/InModuleScope block to avoid cross-scope variable access issues with nested Context BeforeAll blocks. Also fix a pre-existing bug in Help.tests.ps1 where $parameterNames was used instead of $commandParameterNames, causing 3 permanent test failures. --- tests/Get-ListPanel.tests.ps1 | 118 +++++++++++++--------------------- tests/Help.tests.ps1 | 2 +- 2 files changed, 46 insertions(+), 74 deletions(-) diff --git a/tests/Get-ListPanel.tests.ps1 b/tests/Get-ListPanel.tests.ps1 index 4c21660..efa0399 100644 --- a/tests/Get-ListPanel.tests.ps1 +++ b/tests/Get-ListPanel.tests.ps1 @@ -13,6 +13,24 @@ Describe 'Get-ListPanel' { InModuleScope $env:BHProjectName { $script:tenItems = @('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j') + + # Viewport scrolling panel: ScrollOffset=2, ListHeight=8 + # visibleCount = 8-4 = 4; items c,d,e,f visible; top and bottom ellipsis shown + $script:scrolledPanel = Get-ListPanel ` + -List $script:tenItems ` + -SelectedItem 'c' ` + -ScrollOffset 2 ` + -ListHeight 8 + $script:scrolledText = global:Get-RenderedText -Panel $script:scrolledPanel + + # Bottom-of-list panel: ScrollOffset=6, ListHeight=20 + # visibleCount = 16; items g,h,i,j visible; top ellipsis only + $script:bottomPanel = Get-ListPanel ` + -List $script:tenItems ` + -SelectedItem 'j' ` + -ScrollOffset 6 ` + -ListHeight 20 + $script:bottomText = global:Get-RenderedText -Panel $script:bottomPanel } } @@ -43,96 +61,50 @@ Describe 'Get-ListPanel' { } } - Context 'Viewport scrolling' { - # With ListHeight=8: visibleCount = 8-4 = 4 items per window - # ScrollOffset=2: startIndex=2, endIndex=5 -> items c,d,e,f visible - # showEllipsisTop=true (startIndex > 0) - # showEllipsisBottom=true (endIndex=5 < 9) - - BeforeAll { - InModuleScope $env:BHProjectName { - $script:scrolledPanel = Get-ListPanel ` - -List $script:tenItems ` - -SelectedItem 'c' ` - -ScrollOffset 2 ` - -ListHeight 8 - $script:scrolledText = global:Get-RenderedText -Panel $script:scrolledPanel - } - } - - It 'should show items within the visible window' { - InModuleScope $env:BHProjectName { - $script:scrolledText | Should -BeLike '*c*' - $script:scrolledText | Should -BeLike '*d*' - $script:scrolledText | Should -BeLike '*e*' - $script:scrolledText | Should -BeLike '*f*' - } - } - - It 'should hide items scrolled above the window' { - InModuleScope $env:BHProjectName { - # Items a and b are above the viewport (offset 2) - $lines = $script:scrolledText -split "`n" | Where-Object { $_.Trim() -ne '' } - # 'a' and 'b' should not appear as standalone items (only '...' should be above) - $nonEllipsisLines = $lines | Where-Object { $_ -notmatch '^\s*\.\.\.' } - $nonEllipsisLines | Should -Not -Contain 'a' - $nonEllipsisLines | Should -Not -Contain 'b' - } - } - - It 'should show top ellipsis when items are scrolled above' { - InModuleScope $env:BHProjectName { - $script:scrolledText | Should -BeLike '*...*' - } + It 'should show only items within the visible window when scrolled' { + InModuleScope $env:BHProjectName { + $script:scrolledText | Should -BeLike '*c*' + $script:scrolledText | Should -BeLike '*d*' + $script:scrolledText | Should -BeLike '*e*' + $script:scrolledText | Should -BeLike '*f*' } + } - It 'should show bottom ellipsis when items are below the window' { - InModuleScope $env:BHProjectName { - # Items g,h,i,j are below the viewport - $script:scrolledText | Should -BeLike '*...*' - } + It 'should show top ellipsis when items are scrolled above the window' { + InModuleScope $env:BHProjectName { + $script:scrolledText | Should -BeLike '*...*' } } - Context 'Ellipsis at top only' { - # ScrollOffset=6, ListHeight=20: visibleCount=16, startIndex=6, endIndex=9 -> g,h,i,j visible - # showEllipsisTop=true, showEllipsisBottom=false (endIndex=9 = Count-1) - - BeforeAll { - InModuleScope $env:BHProjectName { - $script:bottomPanel = Get-ListPanel ` - -List $script:tenItems ` - -SelectedItem 'j' ` - -ScrollOffset 6 ` - -ListHeight 20 - $script:bottomText = global:Get-RenderedText -Panel $script:bottomPanel - } + It 'should show bottom ellipsis when items are below the window' { + InModuleScope $env:BHProjectName { + # Both top and bottom ellipsis are present since items a-b are above and g-j below + $script:scrolledText | Should -BeLike '*...*' } + } - It 'should show items at the bottom of the list' { - InModuleScope $env:BHProjectName { - $script:bottomText | Should -BeLike '*g*' - $script:bottomText | Should -BeLike '*j*' - } + It 'should show visible items when scrolled to the bottom' { + InModuleScope $env:BHProjectName { + $script:bottomText | Should -BeLike '*g*' + $script:bottomText | Should -BeLike '*j*' } + } - It 'should show top ellipsis' { - InModuleScope $env:BHProjectName { - $script:bottomText | Should -BeLike '*...*' - } + It 'should show top ellipsis when scrolled to the bottom' { + InModuleScope $env:BHProjectName { + $script:bottomText | Should -BeLike '*...*' } } - It 'should highlight the selected item differently' { + It 'should highlight the selected item in the rendered output' { InModuleScope $env:BHProjectName { - $selectedPanel = Get-ListPanel -List @('alpha', 'beta', 'gamma') -SelectedItem 'beta' - $rendered = global:Get-RenderedText -Panel $selectedPanel - # Selected item gets a right-arrow prefix in markup + $panel = Get-ListPanel -List @('alpha', 'beta', 'gamma') -SelectedItem 'beta' + $rendered = global:Get-RenderedText -Panel $panel $rendered | Should -BeLike '*beta*' } } - It 'should use blue border when list pane is not selected' { + It 'should return a Panel when list pane is not selected' { InModuleScope $env:BHProjectName { $panel = Get-ListPanel -List @('item1') -SelectedItem 'item1' -SelectedPane 'preview' $panel.GetType().ToString() | Should -BeExactly 'Spectre.Console.Panel' diff --git a/tests/Help.tests.ps1 b/tests/Help.tests.ps1 index 4fe12b3..f76f66c 100644 --- a/tests/Help.tests.ps1 +++ b/tests/Help.tests.ps1 @@ -108,7 +108,7 @@ Describe "Test help for <_.Name>" -ForEach $commands { # Shouldn't find extra parameters in help. It "finds help parameter in code: <_>" { - $_ -in $parameterNames | Should -Be $true + $_ -in $commandParameterNames | Should -Be $true } } }