diff --git a/PesterExplorer/Private/Get-ListPanel.ps1 b/PesterExplorer/Private/Get-ListPanel.ps1 index 9bc59ea..3701960 100644 --- a/PesterExplorer/Private/Get-ListPanel.ps1 +++ b/PesterExplorer/Private/Get-ListPanel.ps1 @@ -1,36 +1,59 @@ function Get-ListPanel { - <# - .SYNOPSIS - Create a list panel for displaying items in a TUI. - - .DESCRIPTION - This function generates a list panel that displays items in a TUI (Text User - Interface) using Spectre.Console. It formats the items based on whether they - are selected or not, and handles special cases like parent directories. - - .PARAMETER List - An array of strings to display in the list. Each item can be a file path, - a test name, or a special item like '..' for parent directories. - - .PARAMETER SelectedItem - The item that is currently selected in the list. This will be highlighted - differently from unselected items. - - .EXAMPLE - Get-ListPanel -List @('file1.txt', 'file2.txt', '..') -SelectedItem 'file1.txt' - - This example creates a list panel with three items, highlighting 'file1.txt' - as the selected item. - .NOTES - This is meant to be called by the main TUI function: Show-PesterResult + <# + .SYNOPSIS + Create a list panel for displaying items in a TUI. + + .DESCRIPTION + This function generates a list panel that displays items in a TUI (Text User + Interface) using Spectre.Console. It formats the items based on whether they + are selected or not, and handles special cases like parent directories. + + The function supports viewport management, showing only a subset of items + when the list is longer than the available display height. + + .PARAMETER List + An array of strings to display in the list. Each item can be a file path, + a test name, or a special item like '..' for parent directories. + + .PARAMETER SelectedItem + The item that is currently selected in the list. This will be highlighted + differently from unselected items. + + .PARAMETER SelectedPane + Indicates which pane is currently selected. Affects the border color of the panel. + + .PARAMETER ListScrollPosition + The scroll position within the list. Used for viewport management when the list + is longer than the available display height. + + .PARAMETER ListHeight + The height of the list display area. When specified and the list is longer, + viewport management is applied to show only visible items. + + .EXAMPLE + Get-ListPanel -List @('file1.txt', 'file2.txt', '..') -SelectedItem 'file1.txt' + + This example creates a list panel with three items, highlighting 'file1.txt' + as the selected item. + + .EXAMPLE + Get-ListPanel -List $longList -SelectedItem 'item5' -ListScrollPosition 2 -ListHeight 10 + + This example creates a list panel with viewport management, starting from scroll + position 2 and displaying up to 10 items. + + .NOTES + This is meant to be called by the main TUI function: Show-PesterResult #> - [CmdletBinding()] - param ( - [array] - $List, - [string] - $SelectedItem, - [string]$SelectedPane = "list" + [CmdletBinding()] + param ( + [array] + $List, + [string] + $SelectedItem, + [string]$SelectedPane = "list", + [int]$ListScrollPosition = 0, + [int]$ListHeight = 0 ) $paneColor = if($SelectedPane -ne "list") { # If the selected pane is not preview, return an empty panel @@ -86,8 +109,48 @@ function Get-ListPanel { Format-SpectrePadded -Padding 0 } } - } - $results | - Format-SpectreRows | + } + + # Apply viewport scrolling if list height is specified and list is long + if ($ListHeight -gt 0 -and $List.Count -gt $ListHeight) { + Write-Debug "Applying list viewport scrolling. List count: $($List.Count), Height: $ListHeight, Scroll: $ListScrollPosition" + + # Calculate which items to show based on scroll position and height + $visibleResults = @() + + # Add scroll indicator if not at top + if ($ListScrollPosition -gt 0) { + $visibleResults += "[grey]...[/]" | Write-SpectreHost -PassThru | Format-SpectrePadded -Padding 0 + } + + # Calculate available height (subtract 1 if we added scroll indicator) + $availableHeight = $ListHeight + if ($ListScrollPosition -gt 0) { + $availableHeight-- + } + + # Add items starting from scroll position + $itemsAdded = 0 + for ($i = $ListScrollPosition; $i -lt $results.Count -and $itemsAdded -lt $availableHeight; $i++) { + $visibleResults += $results[$i] + $itemsAdded++ + } + + # Add more indicator if there are more items below + if (($ListScrollPosition + $itemsAdded) -lt $results.Count) { + if ($itemsAdded -eq $availableHeight) { + # Replace last item with more indicator if we're at max height + $visibleResults[-1] = "[grey]...more[/]" | Write-SpectreHost -PassThru | Format-SpectrePadded -Padding 0 + } else { + # Add more indicator if there's space + $visibleResults += "[grey]...more[/]" | Write-SpectreHost -PassThru | Format-SpectrePadded -Padding 0 + } + } + + $results = $visibleResults + } + + $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..bb32bce 100644 --- a/PesterExplorer/Public/Show-PesterResult.ps1 +++ b/PesterExplorer/Public/Show-PesterResult.ps1 @@ -70,51 +70,97 @@ function Show-PesterResult { [Spectre.Console.LiveDisplayContext] $Context ) - #region Initial State - $items = Get-ListFromObject -Object $PesterResult - Write-Debug "Items: $($items.Keys -join ', ')" - $list = [array]$items.Keys - $selectedItem = $list[0] - $stack = [System.Collections.Stack]::new() - $object = $PesterResult - $selectedPane = 'list' - $scrollPosition = 0 + #region Initial State + $items = Get-ListFromObject -Object $PesterResult + Write-Debug "Items: $($items.Keys -join ', ')" + $list = [array]$items.Keys + $selectedItem = $list[0] + $stack = [System.Collections.Stack]::new() + $object = $PesterResult + $selectedPane = 'list' + $scrollPosition = 0 + $listScrollPosition = 0 #endregion Initial State while ($true) { - # Check the layout sizes - $sizes = $layout | Get-SpectreLayoutSizes - $previewHeight = $sizes["preview"].Height - $previewWidth = $sizes["preview"].Width - Write-Debug "Preview size: $previewWidth x $previewHeight" + # Check the layout sizes + $sizes = $layout | Get-SpectreLayoutSizes + $previewHeight = $sizes["preview"].Height + $previewWidth = $sizes["preview"].Width + $listHeight = $sizes["list"].Height + Write-Debug "Preview size: $previewWidth x $previewHeight" + Write-Debug "List height: $listHeight" # Handle input $lastKeyPressed = Get-LastKeyPressed if ($null -ne $lastKeyPressed) { - #region List Navigation - if($selectedPane -eq 'list') { - if ($lastKeyPressed.Key -in @("j", "DownArrow")) { - $selectedItem = $list[($list.IndexOf($selectedItem) + 1) % $list.Count] - $scrollPosition = 0 - } elseif ($lastKeyPressed.Key -in @("k", "UpArrow")) { - $selectedItem = $list[($list.IndexOf($selectedItem) - 1 + $list.Count) % $list.Count] - $scrollPosition = 0 - } elseif ($lastKeyPressed.Key -eq "PageDown") { - $currentIndex = $list.IndexOf($selectedItem) - $newIndex = [Math]::Min($currentIndex + 10, $list.Count - 1) - $selectedItem = $list[$newIndex] - $scrollPosition = 0 - } elseif ($lastKeyPressed.Key -eq "PageUp") { - $currentIndex = $list.IndexOf($selectedItem) - $newIndex = [Math]::Max($currentIndex - 10, $list.Count - 1) - $selectedItem = $list[$newIndex] - $scrollPosition = 0 - } elseif ($lastKeyPressed.Key -eq "Home") { - $selectedItem = $list[0] - $scrollPosition = 0 - } elseif ($lastKeyPressed.Key -eq "End") { - $selectedItem = $list[-1] - $scrollPosition = 0 + #region List Navigation + if($selectedPane -eq 'list') { + if ($lastKeyPressed.Key -in @("j", "DownArrow")) { + $selectedItem = $list[($list.IndexOf($selectedItem) + 1) % $list.Count] + $scrollPosition = 0 + # Update list scroll position to keep selected item visible + $selectedIndex = $list.IndexOf($selectedItem) + if ($listHeight -gt 0 -and $list.Count -gt $listHeight) { + # If selected item is below visible area, scroll down + if ($selectedIndex -ge ($listScrollPosition + $listHeight - 1)) { + $listScrollPosition = $selectedIndex - $listHeight + 2 + } + # If selected item is above visible area, scroll up + elseif ($selectedIndex -lt $listScrollPosition) { + $listScrollPosition = $selectedIndex + } + # Ensure scroll position is within bounds + $listScrollPosition = [Math]::Max(0, [Math]::Min($listScrollPosition, $list.Count - $listHeight)) + } + } elseif ($lastKeyPressed.Key -in @("k", "UpArrow")) { + $selectedItem = $list[($list.IndexOf($selectedItem) - 1 + $list.Count) % $list.Count] + $scrollPosition = 0 + # Update list scroll position to keep selected item visible + $selectedIndex = $list.IndexOf($selectedItem) + if ($listHeight -gt 0 -and $list.Count -gt $listHeight) { + # If selected item is above visible area, scroll up + if ($selectedIndex -lt $listScrollPosition) { + $listScrollPosition = $selectedIndex + } + # If selected item is below visible area, scroll down + elseif ($selectedIndex -ge ($listScrollPosition + $listHeight - 1)) { + $listScrollPosition = $selectedIndex - $listHeight + 2 + } + # Ensure scroll position is within bounds + $listScrollPosition = [Math]::Max(0, [Math]::Min($listScrollPosition, $list.Count - $listHeight)) + } + } elseif ($lastKeyPressed.Key -eq "PageDown") { + $currentIndex = $list.IndexOf($selectedItem) + $newIndex = [Math]::Min($currentIndex + 10, $list.Count - 1) + $selectedItem = $list[$newIndex] + $scrollPosition = 0 + # Update list scroll position to keep selected item visible + if ($listHeight -gt 0 -and $list.Count -gt $listHeight) { + $listScrollPosition = [Math]::Max(0, [Math]::Min($newIndex - [Math]::Floor($listHeight / 2), $list.Count - $listHeight)) + } + } elseif ($lastKeyPressed.Key -eq "PageUp") { + $currentIndex = $list.IndexOf($selectedItem) + $newIndex = [Math]::Max($currentIndex - 10, 0) + $selectedItem = $list[$newIndex] + $scrollPosition = 0 + # Update list scroll position to keep selected item visible + if ($listHeight -gt 0 -and $list.Count -gt $listHeight) { + $listScrollPosition = [Math]::Max(0, [Math]::Min($newIndex - [Math]::Floor($listHeight / 2), $list.Count - $listHeight)) + } + } elseif ($lastKeyPressed.Key -eq "Home") { + $selectedItem = $list[0] + $scrollPosition = 0 + $listScrollPosition = 0 + } elseif ($lastKeyPressed.Key -eq "End") { + $selectedItem = $list[-1] + $scrollPosition = 0 + # Scroll to bottom to show last item + if ($listHeight -gt 0 -and $list.Count -gt $listHeight) { + $listScrollPosition = $list.Count - $listHeight + } else { + $listScrollPosition = 0 + } } elseif ($lastKeyPressed.Key -in @("Tab", "RightArrow", "l")) { $selectedPane = 'preview' } elseif ($lastKeyPressed.Key -eq "Enter") { @@ -130,22 +176,24 @@ function Show-PesterResult { Write-Debug "Pushing item into stack: $($items.Item($selectedItem).Name)" $stack.Push($object) $object = $items.Item($selectedItem) - } - $items = Get-ListFromObject -Object $object - $list = [array]$items.Keys - $selectedItem = $list[0] - $scrollPosition = 0 + } + $items = Get-ListFromObject -Object $object + $list = [array]$items.Keys + $selectedItem = $list[0] + $scrollPosition = 0 + $listScrollPosition = 0 } elseif ($lastKeyPressed.Key -eq "Escape") { # Move up via Esc key if($stack.Count -eq 0) { # This is the top level. Exit the loop. return } - $object = $stack.Pop() - $items = Get-ListFromObject -Object $object - $list = [array]$items.Keys - $selectedItem = $list[0] - $scrollPosition = 0 + $object = $stack.Pop() + $items = Get-ListFromObject -Object $object + $list = [array]$items.Keys + $selectedItem = $list[0] + $scrollPosition = 0 + $listScrollPosition = 0 } } else { @@ -171,10 +219,12 @@ function Show-PesterResult { # Generate new data $titlePanel = Get-TitlePanel -Item $object - $getListPanelSplat = @{ - List = $list - SelectedItem = $selectedItem - SelectedPane = $selectedPane + $getListPanelSplat = @{ + List = $list + SelectedItem = $selectedItem + SelectedPane = $selectedPane + ListScrollPosition = $listScrollPosition + 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..e47062b --- /dev/null +++ b/tests/Get-ListPanel.tests.ps1 @@ -0,0 +1,122 @@ +Describe 'Get-ListPanel Scrolling' { + BeforeAll { + # Import the function for testing + . (Join-Path $PSScriptRoot '../PesterExplorer/Private/Get-ListPanel.ps1') + + # Mock Spectre.Console functions to avoid dependency issues + Mock Write-SpectreHost { + param($Message, [switch]$PassThru) + if ($PassThru) { return $Message } else { return $Message } + } + Mock Format-SpectrePadded { + param($Padding, [Parameter(ValueFromPipeline)]$InputObject) + process { return "[$Padding] $InputObject" } + } + Mock Format-SpectreRows { + param([Parameter(ValueFromPipeline)]$InputObject) + begin { $items = @() } + process { $items += $InputObject } + end { return $items } + } + Mock Format-SpectrePanel { + param($Header, $Color, [switch]$Expand, [Parameter(ValueFromPipeline)]$InputObject) + process { return "Panel[$Header,$Color]: $($InputObject -join '|')" } + } + Mock Format-SpectreTextPath { + param($Path, $PathStyle) + return "Path: $Path" + } + # Mock Spectre.Console.Color to avoid type errors + Add-Type -TypeDefinition @' + namespace Spectre.Console { + public enum Color { + Grey = 0 + } + } +'@ -ErrorAction SilentlyContinue + } + + Context 'Without scrolling (ListHeight = 0 or list fits)' { + It 'Shows all items when ListHeight is 0' { + $testList = @('Test1', 'Test2', 'Test3', 'Test4', 'Test5') + $result = Get-ListPanel -List $testList -SelectedItem 'Test1' -ListScrollPosition 0 -ListHeight 0 + + # Should contain all items + $result.ToString() | Should -Match 'Test5' + } + + It 'Shows all items when list is shorter than height' { + $testList = @('Test1', 'Test2', 'Test3') + $result = Get-ListPanel -List $testList -SelectedItem 'Test1' -ListScrollPosition 0 -ListHeight 5 + + # Should contain all items, no scroll indicators + $result.ToString() | Should -Match 'Test3' + $result.ToString() | Should -Not -Match '\.\.\.' + } + } + + Context 'With scrolling (ListHeight > 0 and list is long)' { + It 'Shows limited items with more indicator when at top' { + $testList = @('Test1', 'Test2', 'Test3', 'Test4', 'Test5', 'Test6', 'Test7', 'Test8') + $result = Get-ListPanel -List $testList -SelectedItem 'Test1' -ListScrollPosition 0 -ListHeight 5 + + # Should contain first few items and more indicator + $result.ToString() | Should -Match 'Test1' + $result.ToString() | Should -Match 'Test4' + $result.ToString() | Should -Match '\.\.\.more' + $result.ToString() | Should -Not -Match 'Test8' + } + + It 'Shows scroll up indicator when scrolled down' { + $testList = @('Test1', 'Test2', 'Test3', 'Test4', 'Test5', 'Test6', 'Test7', 'Test8') + $result = Get-ListPanel -List $testList -SelectedItem 'Test5' -ListScrollPosition 2 -ListHeight 5 + + # Should contain scroll up indicator + $resultStr = $result.ToString() + $resultStr | Should -Match '\.\.\.' # Up indicator + $resultStr | Should -Match 'Test5' + } + + It 'Shows both scroll indicators when in middle' { + $testList = @('Test1', 'Test2', 'Test3', 'Test4', 'Test5', 'Test6', 'Test7', 'Test8', 'Test9', 'Test10') + $result = Get-ListPanel -List $testList -SelectedItem 'Test6' -ListScrollPosition 3 -ListHeight 5 + + $resultStr = $result.ToString() + # Should have both up and down indicators + $resultStr | Should -Match '\.\.\.' # Should have some scroll indicator + $resultStr | Should -Match 'Test6' + } + + It 'Handles boundary conditions correctly' { + $testList = @('Test1', 'Test2', 'Test3', 'Test4', 'Test5', 'Test6') + $result = Get-ListPanel -List $testList -SelectedItem 'Test6' -ListScrollPosition 2 -ListHeight 5 + + # Should show items starting from scroll position + $resultStr = $result.ToString() + $resultStr | Should -Match 'Test6' + } + } + + Context 'Parameter validation' { + It 'Has required new parameters' { + $params = (Get-Command Get-ListPanel).Parameters + $params.Keys | Should -Contain 'ListScrollPosition' + $params.Keys | Should -Contain 'ListHeight' + } + + It 'Accepts all parameter types correctly' { + $testList = @('Test1', 'Test2') + { Get-ListPanel -List $testList -SelectedItem 'Test1' -ListScrollPosition 0 -ListHeight 5 } | Should -Not -Throw + } + } + + Context 'Selected item highlighting' { + It 'Highlights selected item correctly with scrolling' { + $testList = @('Test1', 'Test2', 'Test3', 'Test4', 'Test5', 'Test6') + $result = Get-ListPanel -List $testList -SelectedItem 'Test3' -ListScrollPosition 1 -ListHeight 4 + + # Selected item should be highlighted (contains Turquoise2) + $result.ToString() | Should -Match 'Turquoise2.*Test3' + } + } +} \ No newline at end of file