├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .gitattributes ├── .github ├── prompts │ └── PowerShellPrompts.prompt.md └── workflows │ ├── CI.yaml │ ├── Publish.yaml │ └── pages.yaml ├── .gitignore ├── .markdownlint.json ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── PesterExplorer ├── PesterExplorer.psd1 ├── PesterExplorer.psm1 ├── Private │ ├── Format-PesterObjectName.ps1 │ ├── Format-PesterTreeHash.ps1 │ ├── Get-LastKeyPressed.ps1 │ ├── Get-ListFromObject.ps1 │ ├── Get-ListPanel.ps1 │ ├── Get-PreviewPanel.ps1 │ ├── Get-ShortcutKeyPanel.ps1 │ └── Get-TitlePanel.ps1 └── Public │ ├── Show-PesterResult.ps1 │ └── Show-PesterResultTree.ps1 ├── README.md ├── build.ps1 ├── cspell.json ├── docs ├── .markdownlint.jsonc ├── en-US │ ├── Show-PesterResult.md │ ├── Show-PesterResultTree.md │ └── about_PesterExplorer.help.md └── requirements.txt ├── images ├── Show-PesterResult.png ├── Show-PesterResultTree.png ├── fullsize.png └── icon.png ├── mkdocs.yml ├── psakeFile.ps1 ├── requirements.psd1 └── tests ├── Get-PreviewPanel.tests.ps1 ├── Get-ShortcutKeyPanel.tests.ps1 ├── Get-TitlePanel.tests.ps1 ├── Help.tests.ps1 ├── Helpers.ps1 ├── Manifest.tests.ps1 ├── Meta.tests.ps1 ├── MetaFixers.psm1 ├── ScriptAnalyzerSettings.psd1 └── fixtures └── Example.ps1 /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 4 | #------------------------------------------------------------------------------------------------------------- 5 | 6 | FROM mcr.microsoft.com/powershell:latest 7 | 8 | # This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" 9 | # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs 10 | # will be updated to match your local UID/GID (when using the dockerFile property). 11 | # See https://aka.ms/vscode-remote/containers/non-root-user for details. 12 | ARG USERNAME=vscode 13 | ARG USER_UID=1000 14 | ARG USER_GID=$USER_UID 15 | 16 | # install git iproute2, process tools 17 | RUN apt-get update && apt-get -y install git openssh-client less iproute2 procps \ 18 | # Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user. 19 | && groupadd --gid $USER_GID $USERNAME \ 20 | && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \ 21 | # [Optional] Add sudo support for the non-root user 22 | && apt-get install -y sudo \ 23 | && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME\ 24 | && chmod 0440 /etc/sudoers.d/$USERNAME \ 25 | # 26 | # Clean up 27 | && apt-get autoremove -y \ 28 | && apt-get clean -y \ 29 | && rm -rf /var/lib/apt/lists/* 30 | 31 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PowerShell", 3 | "dockerFile": "Dockerfile", 4 | "customizations": { 5 | "vscode": { 6 | "settings": { 7 | "terminal.integrated.profiles.linux": { 8 | "bash": { 9 | "path": "usr/bin/bash", 10 | "icon": "terminal-bash" 11 | }, 12 | "zsh": { 13 | "path": "usr/bin/zsh" 14 | }, 15 | "pwsh": { 16 | "path": "/usr/bin/pwsh", 17 | "icon": "terminal-powershell" 18 | } 19 | }, 20 | "terminal.integrated.defaultProfile.linux": "pwsh" 21 | }, 22 | "extensions": [ 23 | "ms-vscode.powershell", 24 | "davidanson.vscode-markdownlint" 25 | ] 26 | } 27 | }, 28 | "postCreateCommand": "pwsh -c './build.ps1 -Task Init -Bootstrap'" 29 | } 30 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * -crlf 2 | -------------------------------------------------------------------------------- /.github/prompts/PowerShellPrompts.prompt.md: -------------------------------------------------------------------------------- 1 | --- 2 | mode: 'ask' 3 | --- 4 | This project uses PwshSpectreConsole and Pester to render a TUI for Pester 5 | results.The TUI allows users to navigate through Pester run results, viewing 6 | details of containers, blocks, and tests. 7 | 8 | All suggestions should try to stay with 80 characters or 120 max. Use splatting 9 | when possible. You can create new lines after `|` to make it more readable. 10 | 11 | # Examples 12 | You can use the following when you create `.EXAMPLE` text for the comment based 13 | help. All Object parameters will use the $run variable from the following code: 14 | 15 | ``` 16 | $run = Invoke-Pester -Path 'tests' -PassThru 17 | ``` 18 | 19 | # Example 1 20 | ```powershell 21 | $run = Invoke-Pester -Path 'tests' -PassThru 22 | $run | Show-PesterResults 23 | ``` 24 | 25 | An explanation of the example should be provided in the `.EXAMPLE` section with 26 | an empty line between the example and the explanation. 27 | 28 | # PowerShell Functions 29 | All functions should have a `[CmdletBinding()]` attribute. 30 | -------------------------------------------------------------------------------- /.github/workflows/CI.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | workflow_dispatch: 5 | permissions: 6 | checks: write 7 | pull-requests: write 8 | contents: read 9 | issues: write 10 | jobs: 11 | ci: 12 | uses: HeyItsGilbert/.github/.github/workflows/ModuleCI.yml@main 13 | -------------------------------------------------------------------------------- /.github/workflows/Publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Module 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | jobs: 8 | build: 9 | uses: HeyItsGilbert/.github/.github/workflows/PublishModule.yml@main 10 | secrets: inherit 11 | -------------------------------------------------------------------------------- /.github/workflows/pages.yaml: -------------------------------------------------------------------------------- 1 | name: Publish docs via GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | permissions: 7 | checks: write 8 | pull-requests: write 9 | contents: write 10 | 11 | jobs: 12 | build: 13 | name: Deploy docs 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout main 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 2 20 | - shell: pwsh 21 | # Give an id to the step, so we can reference it later 22 | id: check_file_changed 23 | run: | 24 | # Diff HEAD with the previous commit 25 | $diff = git diff --name-only HEAD^ HEAD 26 | 27 | # Check if a file under docs/ or with the .md extension has changed (added, modified, deleted) 28 | $SourceDiff = $diff | Where-Object { $_ -match '^docs/' -or $_ -match '.md$' } 29 | $HasDiff = $SourceDiff.Length -gt 0 30 | 31 | # Set the output named "docs_changed" 32 | Write-Host "::set-output name=docs_changed::$HasDiff" 33 | 34 | - name: Copy readme 35 | shell: pwsh 36 | run: | 37 | Copy-Item README.md docs/index.md 38 | - name: Deploy docs 39 | uses: mhausenblas/mkdocs-deploy-gh-pages@master 40 | if: steps.check_file_changed.outputs.docs_changed == 'True' || ${{ github.event_name == 'workflow_dispatch' }} 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | REQUIREMENTS: docs/requirements.txt 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check in the Output dir 2 | Output/ 3 | 4 | scratch/ 5 | 6 | testResults.xml 7 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false, 3 | "MD024": { 4 | "siblings_only": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "ms-vscode.PowerShell", 6 | "DavidAnson.vscode-markdownlint" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "PowerShell: Debug Tests", 9 | "type": "PowerShell", 10 | "request": "launch", 11 | "script": "./build.ps1 -Task Test", 12 | "args": [], 13 | "createTemporaryIntegratedConsole": false 14 | }, 15 | { 16 | "name": "PowerShell: Debug Tests (Temp Console)", 17 | "type": "PowerShell", 18 | "request": "launch", 19 | "script": "./build.ps1 -Task Test", 20 | "args": [], 21 | "createTemporaryIntegratedConsole": true 22 | }, 23 | { 24 | "name": "PowerShell: Load Source Module (Temp Console)", 25 | "type": "PowerShell", 26 | "request": "launch", 27 | "script": "./build.ps1 -Task Init && Import-Module $env:BHPSModuleManifest", 28 | "args": [], 29 | "createTemporaryIntegratedConsole": true 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.trimTrailingWhitespace": true, 3 | "files.insertFinalNewline": true, 4 | "editor.insertSpaces": true, 5 | "editor.tabSize": 4, 6 | "powershell.codeFormatting.preset": "OTBS" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | // Start PowerShell (pwsh on *nix) 6 | "windows": { 7 | "options": { 8 | "shell": { 9 | "executable": "pwsh.exe", 10 | "args": [ 11 | "-NoProfile", 12 | "-ExecutionPolicy", 13 | "Bypass", 14 | "-Command" 15 | ] 16 | } 17 | } 18 | }, 19 | "linux": { 20 | "options": { 21 | "shell": { 22 | "executable": "/usr/bin/pwsh", 23 | "args": [ 24 | "-NoProfile", 25 | "-Command" 26 | ] 27 | } 28 | } 29 | }, 30 | "osx": { 31 | "options": { 32 | "shell": { 33 | "executable": "/usr/local/bin/pwsh", 34 | "args": [ 35 | "-NoProfile", 36 | "-Command" 37 | ] 38 | } 39 | } 40 | }, 41 | "tasks": [ 42 | { 43 | "label": "Clean", 44 | "type": "shell", 45 | "command": "${cwd}/build.ps1 -Task Clean -Verbose" 46 | }, 47 | { 48 | "label": "Test", 49 | "type": "shell", 50 | "command": "${cwd}/build.ps1 -Task Test -Verbose", 51 | "group": { 52 | "kind": "test", 53 | "isDefault": true 54 | }, 55 | "problemMatcher": "$pester" 56 | }, 57 | { 58 | "label": "Analyze", 59 | "type": "shell", 60 | "command": "${cwd}/build.ps1 -Task Analyze -Verbose" 61 | }, 62 | { 63 | "label": "Pester", 64 | "type": "shell", 65 | "command": "${cwd}/build.ps1 -Task Pester -Verbose", 66 | "problemMatcher": "$pester" 67 | }, 68 | { 69 | "label": "Build", 70 | "type": "shell", 71 | "command": "${cwd}/build.ps1 -Task Build -Verbose", 72 | "group": { 73 | "kind": "build", 74 | "isDefault": true 75 | } 76 | }, 77 | { 78 | "label": "Publish", 79 | "type": "shell", 80 | "command": "${cwd}/build.ps1 -Task Publish -Verbose" 81 | }, 82 | { 83 | "label": "Bootstrap", 84 | "type": "shell", 85 | "command": "${cwd}/build.ps1 -Task Init -Bootstrap -Verbose", 86 | "problemMatcher": [] 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [0.3.3] 2025-06-03 9 | 10 | ### Fixed 11 | 12 | - Inconclusive tests are now darkorange because orange isn't a valid Spectre 13 | color. 14 | 15 | ## [0.3.2] 2025-05-31 16 | 17 | ### Added 18 | 19 | - Added tests for different private commands. 20 | 21 | ### Fixed 22 | 23 | - Fixed for loop used to scroll which was using wrong variable. 24 | 25 | ## [0.3.1] 2025-05-30 26 | 27 | ### Added 28 | 29 | - Add VIM (i.e. `hjkl`) navigation support. 30 | 31 | ### Fixed 32 | 33 | - Add parameter validation and mandatory to ensure commands don't fail 34 | unexpectedly. 35 | 36 | ## [0.3.0] 2025-05-29 37 | 38 | ### Added 39 | 40 | - Added the ability to scroll the preview pane on the right. 41 | - A "... more" line will show if you need to scroll. 42 | - This will give a warning if a panel can't completely rendered. 43 | - There is a known issue that you can "scroll past" the last item, but the 44 | last item will still render. You will need to scroll back up an equivalent 45 | number of times. This will be future Gilbert's problem. 46 | 47 | ### Changed 48 | 49 | - Move test result breakdown chart to Preview pane. 50 | 51 | ## [0.2.0] 2025-05-28 52 | 53 | ### Added 54 | 55 | - Added comment based help on private functions to make it easier for new 56 | contributors to grok. 57 | - Added Pester result breakdown to title panel. 58 | 59 | ### Changed 60 | 61 | - The `Show-PesterResult` List panel now the files using relative path from the 62 | current directory. It also added padding on the selected item as well as an 63 | additional icon to show the highlight. 64 | - Formatted all the functions to stay under 80 character line limit. This is a 65 | preference. We will have a 120 hard limit (when possible). 66 | 67 | ### Fixed 68 | 69 | - Removed extra item from the stack that tracked which layer of the view you 70 | were in. 71 | 72 | ## [0.1.0] Initial Version 73 | 74 | ### Added 75 | 76 | - `Show-PesterResult` renders a TUI that let's you navigate your Pester run 77 | results. It shows item details as you navigate including the ability to from 78 | Container to Block to Test. 79 | - `Show-PesterResultTree` renders your Pester run as a tree structure. 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Gilbert Sanchez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /PesterExplorer/PesterExplorer.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'PesterExplorer' 3 | # 4 | # Generated by: Gilbert Sanchez 5 | # 6 | # Generated on: 5/23/2025 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'PesterExplorer.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '0.3.3' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '1b8311c2-23fd-4a4c-90de-e17cfc306b04' 22 | 23 | # Author of this module 24 | Author = 'Gilbert Sanchez' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'HeyItsGilbert' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) Gilbert Sanchez. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'A TUI to explore Pester results.' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | PowerShellVersion = '7.0' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # ClrVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | RequiredModules = @( 55 | @{ 56 | ModuleName = 'Pester' 57 | ModuleVersion = '5.0.0' 58 | }, 59 | @{ 60 | ModuleName = 'PwshSpectreConsole' 61 | ModuleVersion = '2.3.0' 62 | } 63 | ) 64 | 65 | # Assemblies that must be loaded prior to importing this module 66 | # RequiredAssemblies = @() 67 | 68 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 69 | # ScriptsToProcess = @() 70 | 71 | # Type files (.ps1xml) to be loaded when importing this module 72 | # TypesToProcess = @() 73 | 74 | # Format files (.ps1xml) to be loaded when importing this module 75 | # FormatsToProcess = @() 76 | 77 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 78 | # NestedModules = @() 79 | 80 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 81 | FunctionsToExport = '*' 82 | 83 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 84 | CmdletsToExport = '*' 85 | 86 | # Variables to export from this module 87 | VariablesToExport = '*' 88 | 89 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 90 | AliasesToExport = '*' 91 | 92 | # DSC resources to export from this module 93 | # DscResourcesToExport = @() 94 | 95 | # List of all modules packaged with this module 96 | # ModuleList = @() 97 | 98 | # List of all files packaged with this module 99 | # FileList = @() 100 | 101 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 102 | PrivateData = @{ 103 | 104 | PSData = @{ 105 | 106 | # Tags applied to this module. These help with module discovery in online galleries. 107 | Tags = @( 108 | 'Windows', 109 | 'Linux', 110 | 'macOS', 111 | 'Pester', 112 | 'TUI', 113 | 'Spectre.Console', 114 | 'PSEdition_Core', 115 | 'tdd' 116 | ) 117 | 118 | # A URL to the license for this module. 119 | LicenseUri = 'https://github.com/HeyItsGilbert/PesterExplorer/blob/main/LICENSE' 120 | 121 | # A URL to the main website for this project. 122 | ProjectUri = 'https://github.com/HeyItsGilbert/PesterExplorer' 123 | 124 | # A URL to an icon representing this module. 125 | IconUri = 'https://raw.githubusercontent.com/HeyItsGilbert/PesterExplorer/main/images/icon.png' 126 | 127 | # ReleaseNotes of this module 128 | ReleaseNotes = 'https://github.com/HeyItsGilbert/PesterExplorer/blob/main/CHANGELOG.md' 129 | 130 | # Prerelease string of this module 131 | # Prerelease = '' 132 | 133 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 134 | RequireLicenseAcceptance = $false 135 | 136 | # External dependent modules of this module 137 | # ExternalModuleDependencies = @() 138 | 139 | } # End of PSData hashtable 140 | 141 | } # End of PrivateData hashtable 142 | 143 | # HelpInfo URI of this module 144 | # HelpInfoURI = '' 145 | 146 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 147 | # DefaultCommandPrefix = '' 148 | 149 | } 150 | 151 | 152 | -------------------------------------------------------------------------------- /PesterExplorer/PesterExplorer.psm1: -------------------------------------------------------------------------------- 1 | #require -Module PwshSpectreConsole 2 | # Dot source public/private functions 3 | $public = @(Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'Public/*.ps1') -Recurse -ErrorAction Stop) 4 | $private = @(Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'Private/*.ps1') -Recurse -ErrorAction Stop) 5 | foreach ($import in @($public + $private)) { 6 | try { 7 | . $import.FullName 8 | } catch { 9 | throw "Unable to dot source [$($import.FullName)]" 10 | } 11 | } 12 | 13 | Export-ModuleMember -Function $public.Basename 14 | -------------------------------------------------------------------------------- /PesterExplorer/Private/Format-PesterObjectName.ps1: -------------------------------------------------------------------------------- 1 | function Format-PesterObjectName { 2 | <# 3 | .SYNOPSIS 4 | Format the name of a Pester object for display. 5 | 6 | .DESCRIPTION 7 | This function formats the name of a Pester object for display in a way that is compatible with Spectre.Console. 8 | It uses the object's name and result to determine the appropriate icon and color for display. 9 | It returns a string that can be used in Spectre.Console output. 10 | 11 | .PARAMETER Object 12 | The Pester object to format. This should be a Pester Run or TestResult object. 13 | It is mandatory and can be piped in. 14 | 15 | .PARAMETER NoColor 16 | A switch to disable color formatting in the output. If specified, the name will be returned without any color 17 | or icon. 18 | 19 | .EXAMPLE 20 | $pesterResult.Containers[0].Blocks[0] | Format-PesterObjectName 21 | 22 | This would format the name of the first block in the first container of a Pester result, 23 | returning a string with the appropriate icon and color based on the result of the test. 24 | #> 25 | [CmdletBinding()] 26 | [OutputType([string])] 27 | param ( 28 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)] 29 | [ValidateNotNullOrEmpty()] 30 | $Object, 31 | [Switch] 32 | $NoColor 33 | ) 34 | process { 35 | $type = $Object.GetType().ToString() 36 | $name = $Object.Name 37 | if ($null -eq $name) { 38 | $name = $type | Get-SpectreEscapedText 39 | } 40 | if ($null -ne $Object.ExpandedName) { 41 | $name = $Object.ExpandedName | Get-SpectreEscapedText 42 | } 43 | $icon = switch ($Object.Result) { 44 | 'Passed' { 45 | ":check_mark_button:" 46 | } 47 | 'Failed' { 48 | ":cross_mark:" 49 | } 50 | 'Skipped' { 51 | ":three_o_clock:" 52 | } 53 | 'Inconclusive' { 54 | ":exclamation_question_mark:" 55 | } 56 | default { 57 | Write-Verbose "No icon for result: $($Object.Result)" 58 | } 59 | } 60 | $color = switch ($Object.Result) { 61 | 'Passed' { 'green' } 62 | 'Failed' { 'red' } 63 | 'Skipped' { 'yellow' } 64 | 'Inconclusive' { 'darkorange' } 65 | default { 'white' } 66 | } 67 | $finalName = if ($NoColor) { 68 | $name 69 | } else { 70 | "[${color}]${icon} $name[/]" 71 | } 72 | return $finalName 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /PesterExplorer/Private/Format-PesterTreeHash.ps1: -------------------------------------------------------------------------------- 1 | function Format-PesterTreeHash { 2 | <# 3 | .SYNOPSIS 4 | Format a Pester object into a hashtable for tree display. 5 | 6 | .DESCRIPTION 7 | This function takes a Pester object and formats it into a hashtable that can 8 | be used to display a tree structure in a TUI (Text User Interface). It 9 | handles different types of Pester objects such as Run, Container, Block, and 10 | Test, recursively building a tree structure with children nodes. 11 | 12 | .PARAMETER Object 13 | The Pester object to format. This can be a Run, Container, Block, or Test 14 | object. The function will traverse the object and its children, formatting 15 | them into a hashtable structure. 16 | 17 | .EXAMPLE 18 | $run = Invoke-Pester -Path 'tests' -PassThru 19 | $treeHash = Format-PesterTreeHash -Object $run 20 | 21 | .NOTES 22 | This returns a hashtable with the following structure: 23 | @{ 24 | Value = "Pester Run" # or the name of the object 25 | Children = @( 26 | @{ 27 | Value = "Container Name" 28 | Children = @( 29 | @{ 30 | Value = "Block Name" 31 | Children = @( 32 | @{ 33 | Value = "Test Name" 34 | Children = @() 35 | } 36 | ) 37 | } 38 | ) 39 | } 40 | ) 41 | } 42 | #> 43 | [CmdletBinding()] 44 | [OutputType([hashtable])] 45 | param ( 46 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)] 47 | [ValidateNotNull()] 48 | [ValidateNotNullOrEmpty()] 49 | $Object 50 | ) 51 | process { 52 | Write-Debug "Formatting object: $($Object.Name)" 53 | $hash = @{ 54 | Value = $(Format-PesterObjectName -Object $Object) 55 | Children = @() 56 | } 57 | 58 | if ($null -eq $Object) { 59 | throw "Object is null" 60 | } 61 | Write-Debug "Object type: $($Object.GetType())" 62 | switch -Regex ($Object.GetType().Name) { 63 | 'List`1' { 64 | # This is a list. Return the items. 65 | $Object | Where-Object { $_ } | ForEach-Object { 66 | $hash.Children += Format-PesterTreeHash -Object $_ 67 | } 68 | } 69 | 'Run' { 70 | $hash["Value"] = "Pester Run" 71 | # This is the top-level object. Return the container names. 72 | $Object.Containers | Where-Object { $_ } | ForEach-Object { 73 | $hash.Children += Format-PesterTreeHash $_ 74 | } 75 | } 76 | 'Container' { 77 | # This is a container. Return the blocks. 78 | if ($Object.Blocks.Count -eq 0) { 79 | break 80 | } 81 | $Object.Blocks | Where-Object { $_ } | ForEach-Object { 82 | $hash.Children += Format-PesterTreeHash $_ 83 | } 84 | } 85 | 'Block' { 86 | # This is a block. Return the tests. 87 | if ($Object.Order.Count -eq 0) { 88 | break 89 | } 90 | $Object.Order | Where-Object { $_ } | ForEach-Object { 91 | $hash.Children += Format-PesterTreeHash $_ 92 | } 93 | } 94 | 'Test' { 95 | # Nothing 96 | } 97 | default { Write-Warning "Unsupported object type: $($Object.GetType().Name)" } 98 | } 99 | return $hash 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /PesterExplorer/Private/Get-LastKeyPressed.ps1: -------------------------------------------------------------------------------- 1 | function Get-LastKeyPressed { 2 | <# 3 | .SYNOPSIS 4 | Get the last key pressed in the console. 5 | 6 | .DESCRIPTION 7 | This function checks if any key has been pressed in the console and returns 8 | the last key pressed. It is useful for handling user input in a TUI (Text 9 | User Interface) environment. 10 | 11 | .EXAMPLE 12 | $key = Get-LastKeyPressed 13 | if ($key -eq "Enter") { 14 | # Make the TUI do something 15 | } 16 | 17 | This example retrieves the last key pressed and checks if it was the Enter 18 | key. 19 | 20 | .NOTES 21 | This function is meant to be used in a TUI context where you need to 22 | handle user input. It reads the console key buffer and returns the last key 23 | pressed without displaying it on the console. 24 | #> 25 | [CmdletBinding()] 26 | [OutputType([ConsoleKeyInfo])] 27 | param () 28 | $lastKeyPressed = $null 29 | while ([Console]::KeyAvailable) { 30 | $lastKeyPressed = [Console]::ReadKey($true) 31 | } 32 | return $lastKeyPressed 33 | } 34 | -------------------------------------------------------------------------------- /PesterExplorer/Private/Get-ListFromObject.ps1: -------------------------------------------------------------------------------- 1 | function Get-ListFromObject { 2 | <# 3 | .SYNOPSIS 4 | Create a list from a Pester object for creating the items for the list. 5 | 6 | .DESCRIPTION 7 | This function takes a Pester object (Run, Container, Block, or List) and 8 | formats it into an ordered dictionary that can be used to display a tree 9 | structure in a TUI (Text User Interface). It handles different types of 10 | Pester objects, extracting relevant information such as container names, 11 | block names, and test names. The function returns an ordered dictionary 12 | where the keys are formatted names and the values are the corresponding 13 | Pester objects. 14 | 15 | .PARAMETER Object 16 | The Pester object to format. This can be a Run, Container, Block, or List 17 | object. The function will traverse the object and its children, formatting 18 | them into an ordered dictionary structure. 19 | 20 | .EXAMPLE 21 | $run = Invoke-Pester -Path 'tests' -PassThru 22 | $list = Get-ListFromObject -Object $run 23 | 24 | This example retrieves a Pester run object and formats it into an ordered 25 | dictionary for tree display. 26 | #> 27 | [CmdletBinding()] 28 | [OutputType([System.Collections.Specialized.OrderedDictionary])] 29 | param ( 30 | [Parameter(Mandatory = $true)] 31 | $Object 32 | ) 33 | $previousTest = ".." # :up_left_arrow: 34 | $hash = [ordered]@{ 35 | $previousTest = @() 36 | } 37 | # This can be several types of Pester objects 38 | switch ($Object.GetType().Name) { 39 | 'Run' { 40 | $hash.Remove($previousTest) 41 | # This is the top-level object. Return the container names. 42 | $Object.Containers | ForEach-Object { 43 | $hash[$_.Name] = $_ 44 | } 45 | } 46 | 'Container' { 47 | # This is a container. Return the blocks. 48 | $Object.Blocks | ForEach-Object { 49 | $name = $_ | Format-PesterObjectName -NoColor 50 | $hash[$name] = $_ 51 | } 52 | } 53 | 'Block' { 54 | # This is a block. Return the tests. 55 | $Object.Order | ForEach-Object { 56 | $name = $_ | Format-PesterObjectName -NoColor 57 | $hash[$name] = $_ 58 | } 59 | } 60 | 'List`1' { 61 | # This is a list. Return the items. 62 | $Object | ForEach-Object { 63 | $name = $_ | Format-PesterObjectName -NoColor 64 | $hash[$name] = $_ 65 | } 66 | } 67 | 'Test' { 68 | # This is a test. Return the test name. 69 | #$name = $_ | Format-PesterObjectName -NoColor 70 | #$hash[$name] = $_ 71 | } 72 | default { Write-Error "Unsupported object type: $($Object.GetType().Name)" } 73 | } 74 | return $hash 75 | } 76 | -------------------------------------------------------------------------------- /PesterExplorer/Private/Get-ListPanel.ps1: -------------------------------------------------------------------------------- 1 | function Get-ListPanel { 2 | <# 3 | .SYNOPSIS 4 | Create a list panel for displaying items in a TUI. 5 | 6 | .DESCRIPTION 7 | This function generates a list panel that displays items in a TUI (Text User 8 | Interface) using Spectre.Console. It formats the items based on whether they 9 | are selected or not, and handles special cases like parent directories. 10 | 11 | .PARAMETER List 12 | An array of strings to display in the list. Each item can be a file path, 13 | a test name, or a special item like '..' for parent directories. 14 | 15 | .PARAMETER SelectedItem 16 | The item that is currently selected in the list. This will be highlighted 17 | differently from unselected items. 18 | 19 | .EXAMPLE 20 | Get-ListPanel -List @('file1.txt', 'file2.txt', '..') -SelectedItem 'file1.txt' 21 | 22 | This example creates a list panel with three items, highlighting 'file1.txt' 23 | as the selected item. 24 | .NOTES 25 | This is meant to be called by the main TUI function: Show-PesterResult 26 | #> 27 | [CmdletBinding()] 28 | param ( 29 | [array] 30 | $List, 31 | [string] 32 | $SelectedItem, 33 | [string]$SelectedPane = "list" 34 | ) 35 | $paneColor = if($SelectedPane -ne "list") { 36 | # If the selected pane is not preview, return an empty panel 37 | "blue" 38 | } else { 39 | "white" 40 | } 41 | $unselectedStyle = @{ 42 | RootColor = [Spectre.Console.Color]::Grey 43 | SeparatorColor = [Spectre.Console.Color]::Grey 44 | StemColor = [Spectre.Console.Color]::Grey 45 | LeafColor = [Spectre.Console.Color]::White 46 | } 47 | $results = $List | ForEach-Object { 48 | $name = $_ 49 | if($name -eq '..') { 50 | # This is a parent item, so we show it as a folder 51 | if ($name -eq $SelectedItem) { 52 | Write-SpectreHost ":up_arrow: [Turquoise2]$name[/]" -PassThru | 53 | Format-SpectrePadded -Padding 1 54 | } else { 55 | Write-SpectreHost "$name" -PassThru | 56 | Format-SpectrePadded -Padding 0 57 | } 58 | } 59 | elseif(Test-Path $name){ 60 | $relativePath = [System.IO.Path]::GetRelativePath( 61 | (Get-Location).Path, 62 | $name 63 | ) 64 | if ($name -eq $SelectedItem) { 65 | Format-SpectreTextPath -Path $relativePath | 66 | Format-SpectrePadded -Padding 1 67 | } else { 68 | $formatSpectreTextPathSplat = @{ 69 | Path = $relativePath 70 | PathStyle = $unselectedStyle 71 | } 72 | Format-SpectreTextPath @formatSpectreTextPathSplat | 73 | Format-SpectrePadded -Padding 0 74 | } 75 | } 76 | else { 77 | if ($name -eq $SelectedItem) { 78 | $writeSpectreHostSplat = @{ 79 | PassThru = $true 80 | Message = ":right_arrow: [Turquoise2]$name[/]" 81 | } 82 | Write-SpectreHost @writeSpectreHostSplat | 83 | Format-SpectrePadded -Padding 1 84 | } else { 85 | Write-SpectreHost $name -PassThru | 86 | Format-SpectrePadded -Padding 0 87 | } 88 | } 89 | } 90 | $results | 91 | Format-SpectreRows | 92 | Format-SpectrePanel -Header "[white]List[/]" -Expand -Color $paneColor 93 | } 94 | -------------------------------------------------------------------------------- /PesterExplorer/Private/Get-PreviewPanel.ps1: -------------------------------------------------------------------------------- 1 | # spell-checker:ignore Renderable 2 | function Get-PreviewPanel { 3 | <# 4 | .SYNOPSIS 5 | Get a preview panel for a selected Pester object. 6 | 7 | .DESCRIPTION 8 | This function generates a preview panel for a selected Pester object, such 9 | as a Run, Container, Block, or Test. It formats the object and its results 10 | into a structured output suitable for display in a TUI (Text User 11 | Interface). The function handles different types of Pester objects and 12 | extracts relevant information such as test results, standard output, and 13 | error records. The output is formatted into a grid and panel for better 14 | readability. The function returns a formatted panel that can be displayed in 15 | a TUI environment. If no item is selected, it prompts the user to select an 16 | item. If the selected item is a Test, it shows the test result and the code 17 | tested. If the selected item is a Run, Container, or Block, it shows the 18 | results in a tree structure. It also displays standard output and errors if 19 | they exist. The function is designed to be used in a Pester Explorer 20 | context, where users can explore and preview Pester test results in a 21 | structured and user-friendly manner. 22 | 23 | .PARAMETER Items 24 | A hashtable containing Pester objects (Run, Container, Block, Test) to be 25 | displayed in the preview panel. 26 | 27 | .PARAMETER SelectedItem 28 | The key of the selected item in the Items hashtable. This can be a Pester 29 | object such as a Run, Container, Block, or Test. 30 | 31 | .EXAMPLE 32 | $run = Invoke-Pester -Path 'tests' -PassThru 33 | $items = Get-ListFromObject -Object $run 34 | $selectedItem = 'Test1' 35 | Get-PreviewPanel -Items $items -SelectedItem $selectedItem 36 | 37 | This example retrieves a Pester run object, formats it into a list of items, 38 | and generates a preview panel for the selected item 'Test1'. 39 | 40 | .NOTES 41 | This function is part of the Pester Explorer module and is used to display 42 | Pester test results in a TUI. It formats the output using Spectre.Console 43 | and provides a structured view of the Pester objects. The function handles 44 | different types of Pester objects and extracts relevant information for 45 | display. It is designed to be used in a Pester Explorer context, where users 46 | can explore and preview Pester test results in a structured and 47 | user-friendly manner. 48 | #> 49 | [CmdletBinding()] 50 | param ( 51 | [Parameter(Mandatory)] 52 | [ValidateNotNullOrEmpty()] 53 | [hashtable] 54 | $Items, 55 | [Parameter(Mandatory)] 56 | [string] 57 | $SelectedItem, 58 | $ScrollPosition = 0, 59 | [Parameter()] 60 | [ValidateNotNull()] 61 | $PreviewHeight, 62 | [Parameter()] 63 | [ValidateNotNull()] 64 | $PreviewWidth, 65 | [string]$SelectedPane = "list" 66 | ) 67 | Write-Debug "Get-PreviewPanel called with SelectedItem: $SelectedItem, ScrollPosition: $ScrollPosition" 68 | $paneColor = if($SelectedPane -ne "preview") { 69 | # If the selected pane is not preview, return an empty panel 70 | "blue" 71 | } else { 72 | "white" 73 | } 74 | if($SelectedItem -like "*..") { 75 | $formatSpectreAlignedSplat = @{ 76 | HorizontalAlignment = 'Center' 77 | VerticalAlignment = 'Middle' 78 | } 79 | return "[grey]Please select an item.[/]" | 80 | Format-SpectreAligned @formatSpectreAlignedSplat | 81 | Format-SpectrePanel -Header "[white]Preview[/]" -Expand -Color $paneColor 82 | } 83 | $object = $Items.Item($SelectedItem) 84 | $results = @() 85 | # SelectedItem can be a few different types: 86 | # - A Pester object (Run, Container, Block, Test) 87 | 88 | #region Breakdown 89 | # Skip if the object is null or they are all zero. 90 | if ( 91 | $null -ne $object.PassedCount -and 92 | $null -ne $object.InconclusiveCount -and 93 | $null -ne $object.SkippedCount -and 94 | $null -ne $object.FailedCount -and 95 | ( 96 | [int]$object.PassedCount + 97 | [int]$object.InconclusiveCount + 98 | [int]$object.SkippedCount + 99 | [int]$object.FailedCount 100 | ) -gt 0 101 | ) { 102 | Write-Debug "Adding breakdown chart for $($object.Name)" 103 | $data = @() 104 | $data += New-SpectreChartItem -Label "Passed" -Value ($object.PassedCount) -Color "Green" 105 | $data += New-SpectreChartItem -Label "Failed" -Value ($object.FailedCount) -Color "Red" 106 | $data += New-SpectreChartItem -Label "Inconclusive" -Value ($object.InconclusiveCount) -Color "Grey" 107 | $data += New-SpectreChartItem -Label "Skipped" -Value ($object.SkippedCount) -Color "Yellow" 108 | $results += Format-SpectreBreakdownChart -Data $data 109 | } 110 | #endregion Breakdown 111 | 112 | # For Tests Let's print some more details 113 | if ($object.GetType().Name -eq "Test") { 114 | Write-Debug "Selected item is a Test: $($object.Name)" 115 | $formatSpectrePanelSplat = @{ 116 | Header = "Test Result" 117 | Border = "Rounded" 118 | Color = "White" 119 | } 120 | $results += $object.Result | 121 | Format-SpectrePanel @formatSpectrePanelSplat 122 | # Show the code tested 123 | $formatSpectrePanelSplat = @{ 124 | Header = "Test Code" 125 | Border = "Rounded" 126 | Color = "White" 127 | } 128 | $results += $object.ScriptBlock | 129 | Get-SpectreEscapedText | 130 | Format-SpectrePanel @formatSpectrePanelSplat 131 | } else { 132 | Write-Debug "Selected item '$($object.Name)'is a Pester object: $($object.GetType().Name)" 133 | $data = Format-PesterTreeHash -Object $object 134 | Write-Debug $($data|ConvertTo-Json -Depth 10) 135 | $formatSpectrePanelSplat = @{ 136 | Header = "Results" 137 | Border = "Rounded" 138 | Color = "White" 139 | } 140 | $results += Format-SpectreTree -Data $data | 141 | Format-SpectrePanel @formatSpectrePanelSplat 142 | } 143 | 144 | if($null -ne $object.StandardOutput){ 145 | Write-Debug "Adding standard output for $($object.Name)" 146 | $formatSpectrePanelSplat = @{ 147 | Header = "Standard Output" 148 | Border = "Ascii" 149 | Color = "White" 150 | } 151 | $results += $object.StandardOutput | 152 | Get-SpectreEscapedText | 153 | Format-SpectrePanel @formatSpectrePanelSplat 154 | } 155 | 156 | # Print errors if they exist. 157 | if($object.ErrorRecord.Count -gt 0) { 158 | Write-Debug "Adding error records for $($object.Name)" 159 | $errorRecords = @() 160 | $object.ErrorRecord | ForEach-Object { 161 | $errorRecords += $_ | 162 | Format-SpectreException -ExceptionFormat ShortenEverything 163 | } 164 | $results += $errorRecords | Format-SpectreRows | Format-SpectrePanel -Header "Errors" -Border "Rounded" -Color "Red" 165 | } 166 | 167 | $formatSpectrePanelSplat = @{ 168 | Header = "[white]Preview[/]" 169 | Color = $paneColor 170 | Height = $PreviewHeight 171 | Width = $PreviewWidth 172 | Expand = $true 173 | } 174 | 175 | if($scrollPosition -ge $results.Count) { 176 | # If the scroll position is greater than the number of items, 177 | # reset it to the last item 178 | Write-Debug "Resetting ScrollPosition to last item." 179 | $scrollPosition = $results.Count - 1 180 | } 181 | # If the scroll position is out of bounds, reset it 182 | if ($scrollPosition -lt 0) { 183 | Write-Debug "Resetting ScrollPosition to 0." 184 | $scrollPosition = 0 185 | } 186 | 187 | if($results.Count -eq 0) { 188 | # If there are no results, return an empty panel 189 | return "[grey]No results to display.[/]" | 190 | Format-SpectreAligned -HorizontalAlignment Center -VerticalAlignment Middle | 191 | Format-SpectrePanel @formatSpectrePanelSplat 192 | } else { 193 | Write-Debug "Reducing Preview List: $($results.Count), ScrollPosition: $scrollPosition" 194 | 195 | # Determine the height of each item in the results 196 | $totalHeight = 3 197 | $reducedList = @() 198 | if($ScrollPosition -ne 0) { 199 | # If the scroll position is not zero, add a "back" item 200 | $reducedList += "[grey]...[/]" 201 | } 202 | for ($i = $scrollPosition; $i -lt $results.Count; $i++) { 203 | $itemHeight = Get-SpectreRenderableSize $results[$i] 204 | $totalHeight += $itemHeight.Height 205 | if ($totalHeight -gt $PreviewHeight) { 206 | if($i -eq $scrollPosition) { 207 | # If the first item already exceeds the height, stop here 208 | Write-Debug "First item exceeds preview height. Stopping. Total Height: $totalHeight, Preview Height: $PreviewHeight" 209 | $reducedList += ":police_car_light:The next item is too large to display! Please resize your terminal.:police_car_light:" | 210 | Format-SpectreAligned -HorizontalAlignment Center -VerticalAlignment Middle | 211 | Format-SpectrePanel -Header ":police_car_light: [red]Warning[/]" -Color 'red' -Border Double 212 | break 213 | } 214 | # If the total height exceeds the preview height, stop adding items 215 | Write-Debug "Total height exceeded preview height. Stopping at item $i." 216 | $reducedList += "[blue]...more. Switch to Panel and scroll with keys.[/]" 217 | break 218 | } 219 | $reducedList += $results[$i] 220 | } 221 | } 222 | 223 | return $reducedList | Format-SpectreRows | 224 | Format-SpectrePanel @formatSpectrePanelSplat 225 | #Format-ScrollableSpectrePanel @formatScrollableSpectrePanelSplat 226 | } 227 | -------------------------------------------------------------------------------- /PesterExplorer/Private/Get-ShortcutKeyPanel.ps1: -------------------------------------------------------------------------------- 1 | function Get-ShortcutKeyPanel { 2 | <# 3 | .SYNOPSIS 4 | Get a panel displaying shortcut keys for the Pester Explorer TUI. 5 | 6 | .DESCRIPTION 7 | This function generates a panel that displays the shortcut keys available 8 | in the Pester Explorer TUI. The keys are formatted for display using 9 | Spectre.Console, providing a user-friendly interface for navigating the 10 | Pester Explorer. The panel includes common shortcuts for navigation, 11 | exploration, and exiting the TUI. It returns a formatted panel that can be 12 | displayed in the Pester Explorer interface. 13 | 14 | .EXAMPLE 15 | $shortcutPanel = Get-ShortcutKeyPanel 16 | 17 | This example retrieves a panel displaying the shortcut keys for the Pester 18 | Explorer TUI. The panel includes keys for navigation, exploration, and 19 | exiting the TUI, formatted for easy readability. 20 | #> 21 | [CmdletBinding()] 22 | $shortcutKeys = @( 23 | "Up/J, Down/K - Navigate", 24 | "Home, End - Jump to Top/Bottom", 25 | "PageUp, PageDown - Scroll", 26 | "Enter - Explore Item", 27 | "Tab, Left/H, Right/L - Switch Panel", 28 | "Esc - Back", 29 | "Ctrl+C - Exit" 30 | ) 31 | $formatSpectreAlignedSplat = @{ 32 | HorizontalAlignment = 'Center' 33 | VerticalAlignment = 'Middle' 34 | } 35 | $result = $shortcutKeys | Foreach-Object { 36 | "[grey]$($_)[/]" 37 | } | Format-SpectreColumns -Padding 5 | 38 | Format-SpectreAligned @formatSpectreAlignedSplat | 39 | Format-SpectrePanel -Expand -Border 'None' 40 | return $result 41 | } 42 | -------------------------------------------------------------------------------- /PesterExplorer/Private/Get-TitlePanel.ps1: -------------------------------------------------------------------------------- 1 | function Get-TitlePanel { 2 | <# 3 | .SYNOPSIS 4 | Get a title panel for the Pester Explorer. 5 | 6 | .DESCRIPTION 7 | This function generates a title panel for the Pester Explorer, displaying 8 | the current date and time, and optionally the name of a Pester object if 9 | provided. The title panel is formatted for display in a TUI (Text User 10 | Interface) using Spectre.Console. It returns a formatted panel that can be 11 | displayed in the Pester Explorer interface. If an item is provided, it 12 | includes the type and formatted name of the item in the title panel. 13 | 14 | .PARAMETER Item 15 | The Pester object to include in the title panel. This can be a Run, 16 | Container, Block, or Test object. If provided, the function will format 17 | the object's name and type into the title panel. If not provided, only 18 | the current date and time will be displayed. 19 | 20 | .EXAMPLE 21 | $titlePanel = Get-TitlePanel -Item $somePesterObject 22 | 23 | This example retrieves a title panel for the Pester Explorer, including 24 | #> 25 | [CmdletBinding()] 26 | [OutputType([Spectre.Console.Panel[]])] 27 | param( 28 | $Item 29 | ) 30 | $rows = @() 31 | $title = "Pester Explorer - [gray]$(Get-Date)[/]" 32 | if($null -ne $Item){ 33 | $objectName = Format-PesterObjectName -Object $Item 34 | # Print what type it is and it's formatted name. 35 | $title += " | $($Item.GetType().Name): $($objectName)" 36 | } 37 | $rows += $title 38 | 39 | return $rows | Format-SpectreRows | 40 | Format-SpectreAligned -HorizontalAlignment Center -VerticalAlignment Middle | 41 | Format-SpectrePanel -Expand 42 | } 43 | -------------------------------------------------------------------------------- /PesterExplorer/Public/Show-PesterResult.ps1: -------------------------------------------------------------------------------- 1 | function Show-PesterResult { 2 | <# 3 | .SYNOPSIS 4 | Open a TUI to explore the Pester result object. 5 | 6 | .DESCRIPTION 7 | Show a Pester result in a TUI (Text User Interface) using Spectre.Console. 8 | This function builds a layout with a header, a list of items, and a preview panel. 9 | 10 | .PARAMETER PesterResult 11 | The Pester result object to display. This should be a Pester Run object. 12 | 13 | .PARAMETER NoShortcutPanel 14 | If specified, the shortcut panel will not be displayed at the bottom of the TUI. 15 | 16 | .EXAMPLE 17 | $pesterResult = Invoke-Pester -Path "path\to\tests.ps1" -PassThru 18 | Show-PesterResult -PesterResult $pesterResult 19 | 20 | This example runs Pester tests and opens a TUI to explore the results. 21 | #> 22 | [CmdletBinding()] 23 | [OutputType([void])] 24 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 25 | 'PSReviewUnusedParameter', 26 | 'PesterResult', 27 | Justification='This is actually used in the script block.' 28 | )] 29 | param ( 30 | [Parameter(Mandatory = $true)] 31 | [ValidateNotNullOrEmpty()] 32 | [Pester.Run] 33 | $PesterResult, 34 | [switch] 35 | $NoShortcutPanel 36 | ) 37 | # Build and show the TUI 38 | $rows = @( 39 | # Row 1 40 | ( 41 | New-SpectreLayout -Name "header" -MinimumSize 5 -Ratio 1 -Data ("empty") 42 | ), 43 | # Row 2 44 | ( 45 | New-SpectreLayout -Name "content" -Ratio 10 -Columns @( 46 | ( 47 | New-SpectreLayout -Name "list" -Ratio 1 -Data "empty" 48 | ), 49 | ( 50 | New-SpectreLayout -Name "preview" -Ratio 4 -Data "empty" 51 | ) 52 | ) 53 | ) 54 | ) 55 | if(-not $NoShortcutPanel) { 56 | $rows += ( 57 | # Row 3 58 | ( 59 | New-SpectreLayout -Name "footer" -Ratio 1 -MinimumSize 1 -Data ( 60 | Get-ShortcutKeyPanel 61 | ) 62 | ) 63 | ) 64 | } 65 | $layout = New-SpectreLayout -Name "root" -Rows $rows 66 | 67 | # Start live rendering the layout 68 | Invoke-SpectreLive -Data $layout -ScriptBlock { 69 | param ( 70 | [Spectre.Console.LiveDisplayContext] $Context 71 | ) 72 | 73 | #region Initial State 74 | $items = Get-ListFromObject -Object $PesterResult 75 | Write-Debug "Items: $($items.Keys -join ', ')" 76 | $list = [array]$items.Keys 77 | $selectedItem = $list[0] 78 | $stack = [System.Collections.Stack]::new() 79 | $object = $PesterResult 80 | $selectedPane = 'list' 81 | $scrollPosition = 0 82 | #endregion Initial State 83 | 84 | while ($true) { 85 | # Check the layout sizes 86 | $sizes = $layout | Get-SpectreLayoutSizes 87 | $previewHeight = $sizes["preview"].Height 88 | $previewWidth = $sizes["preview"].Width 89 | Write-Debug "Preview size: $previewWidth x $previewHeight" 90 | 91 | # Handle input 92 | $lastKeyPressed = Get-LastKeyPressed 93 | if ($null -ne $lastKeyPressed) { 94 | #region List Navigation 95 | if($selectedPane -eq 'list') { 96 | if ($lastKeyPressed.Key -in @("j", "DownArrow")) { 97 | $selectedItem = $list[($list.IndexOf($selectedItem) + 1) % $list.Count] 98 | $scrollPosition = 0 99 | } elseif ($lastKeyPressed.Key -in @("k", "UpArrow")) { 100 | $selectedItem = $list[($list.IndexOf($selectedItem) - 1 + $list.Count) % $list.Count] 101 | $scrollPosition = 0 102 | } elseif ($lastKeyPressed.Key -eq "PageDown") { 103 | $currentIndex = $list.IndexOf($selectedItem) 104 | $newIndex = [Math]::Min($currentIndex + 10, $list.Count - 1) 105 | $selectedItem = $list[$newIndex] 106 | $scrollPosition = 0 107 | } elseif ($lastKeyPressed.Key -eq "PageUp") { 108 | $currentIndex = $list.IndexOf($selectedItem) 109 | $newIndex = [Math]::Max($currentIndex - 10, $list.Count - 1) 110 | $selectedItem = $list[$newIndex] 111 | $scrollPosition = 0 112 | } elseif ($lastKeyPressed.Key -eq "Home") { 113 | $selectedItem = $list[0] 114 | $scrollPosition = 0 115 | } elseif ($lastKeyPressed.Key -eq "End") { 116 | $selectedItem = $list[-1] 117 | $scrollPosition = 0 118 | } elseif ($lastKeyPressed.Key -in @("Tab", "RightArrow", "l")) { 119 | $selectedPane = 'preview' 120 | } elseif ($lastKeyPressed.Key -eq "Enter") { 121 | <# Recurse into Pester Object #> 122 | if($items.Item($selectedItem).GetType().Name -eq "Test") { 123 | # This is a test. We don't want to go deeper. 124 | } 125 | if($selectedItem -like '*..*') { 126 | # Move up one via selecting .. 127 | $object = $stack.Pop() 128 | Write-Debug "Popped item from stack: $($object.Name)" 129 | } else { 130 | Write-Debug "Pushing item into stack: $($items.Item($selectedItem).Name)" 131 | $stack.Push($object) 132 | $object = $items.Item($selectedItem) 133 | } 134 | $items = Get-ListFromObject -Object $object 135 | $list = [array]$items.Keys 136 | $selectedItem = $list[0] 137 | $scrollPosition = 0 138 | } elseif ($lastKeyPressed.Key -eq "Escape") { 139 | # Move up via Esc key 140 | if($stack.Count -eq 0) { 141 | # This is the top level. Exit the loop. 142 | return 143 | } 144 | $object = $stack.Pop() 145 | $items = Get-ListFromObject -Object $object 146 | $list = [array]$items.Keys 147 | $selectedItem = $list[0] 148 | $scrollPosition = 0 149 | } 150 | } 151 | else { 152 | #region Preview Navigation 153 | if ($lastKeyPressed.Key -in "Escape", "Tab", "LeftArrow", "h") { 154 | $selectedPane = 'list' 155 | } elseif ($lastKeyPressed.Key -eq "Down") { 156 | # Scroll down in the preview panel 157 | $scrollPosition = $ScrollPosition + 1 158 | } elseif ($lastKeyPressed.Key -eq "Up") { 159 | # Scroll up in the preview panel 160 | $scrollPosition = $ScrollPosition - 1 161 | } elseif ($lastKeyPressed.Key -eq "PageDown") { 162 | # Scroll down by a page in the preview panel 163 | $scrollPosition = $ScrollPosition + 1 164 | } elseif ($lastKeyPressed.Key -eq "PageUp") { 165 | # Scroll up by a page in the preview panel 166 | $scrollPosition = $ScrollPosition - 1 167 | } 168 | #endregion Preview Navigation 169 | } 170 | } 171 | 172 | # Generate new data 173 | $titlePanel = Get-TitlePanel -Item $object 174 | $getListPanelSplat = @{ 175 | List = $list 176 | SelectedItem = $selectedItem 177 | SelectedPane = $selectedPane 178 | } 179 | $listPanel = Get-ListPanel @getListPanelSplat 180 | 181 | $getPreviewPanelSplat = @{ 182 | Items = $items 183 | SelectedItem = $selectedItem 184 | ScrollPosition = $scrollPosition 185 | PreviewHeight = $previewHeight 186 | PreviewWidth = $previewWidth 187 | SelectedPane = $selectedPane 188 | } 189 | $previewPanel = Get-PreviewPanel @getPreviewPanelSplat 190 | 191 | # Update layout 192 | $layout["header"].Update($titlePanel) | Out-Null 193 | $layout["list"].Update($listPanel) | Out-Null 194 | $layout["preview"].Update($previewPanel) | Out-Null 195 | 196 | # Draw changes 197 | $Context.Refresh() 198 | Start-Sleep -Milliseconds 100 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /PesterExplorer/Public/Show-PesterResultTree.ps1: -------------------------------------------------------------------------------- 1 | function Show-PesterResultTree { 2 | <# 3 | .SYNOPSIS 4 | Show a Pester result in a tree format using Spectre.Console. 5 | 6 | .DESCRIPTION 7 | This function takes a Pester result object and formats it into a tree 8 | structure using Spectre.Console. It is useful for visualizing the structure 9 | of Pester results such as runs, containers, blocks, and tests. 10 | 11 | .PARAMETER PesterResult 12 | The Pester result object to display. This should be a Pester Run object. 13 | 14 | .EXAMPLE 15 | $pesterResult = Invoke-Pester -Path "path\to\tests.ps1" -PassThru 16 | Show-PesterResultTree -PesterResult $pesterResult 17 | 18 | This example runs Pester tests and displays the results in a tree format. 19 | #> 20 | [CmdletBinding()] 21 | [OutputType([void])] 22 | param ( 23 | [Parameter(Mandatory = $true)] 24 | [ValidateNotNullOrEmpty()] 25 | [Pester.Run] 26 | $PesterResult 27 | ) 28 | $treeHash = Format-PesterTreeHash -Object $PesterResult 29 | Format-SpectreTree -Data $treeHash 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |