├── .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 |
2 | 3 | # PesterExplorer 4 | 5 | A TUI to explore Pester results. 6 | 7 | [![PowerShell Gallery](https://img.shields.io/powershellgallery/dt/PesterExplorer)](https://www.powershellgallery.com/packages/PesterExplorer/) 8 | [![PowerShell Gallery Version](https://img.shields.io/powershellgallery/v/PesterExplorer)](https://www.powershellgallery.com/packages/PesterExplorer/) 9 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/HeyItsGilbert/PesterExplorer/Publish.yaml)](https://www.powershellgallery.com/packages/PesterExplorer/) 10 | [![PowerShell Gallery](https://img.shields.io/powershellgallery/p/PesterExplorer)](https://www.powershellgallery.com/packages/PesterExplorer/) 11 | 12 | ## Overview 13 | 14 | Pester does a wonderful job printing out tests results as they're running. The 15 | difficulty can be where you're looking at a large number of results. 16 | 17 | ## Installation 18 | 19 | > [!IMPORTANT] 20 | > This module is for PowerShell 7. This won't work in Windows PowerShell. 21 | 22 | ```pwsh 23 | Install-Module PesterExplorer -Scope CurrentUser 24 | ``` 25 | 26 | Installing this module will install it's dependencies which are Pester and 27 | PwshSpectreConsole. 28 | 29 | ## Examples 30 | 31 | To explore your result object you simply need to run `Show-PesterResult` 32 | 33 | ```pwsh 34 | # Run Pester and make sure to PassThru the object 35 | $pester = Invoke-Pester .\tests\ -PassThru 36 | # Now run the TUI 37 | Show-PesterResult $p 38 | ``` 39 | 40 |
41 | 42 | You can also get a tree view of your pester results with 43 | `Show-PesterResultTree`. 44 | 45 | ```pwsh 46 | # Run Pester and make sure to PassThru the object 47 | $pester = Invoke-Pester .\tests\ -PassThru 48 | # Now get that in a Tree view 49 | Show-PesterResultTree $p 50 | ``` 51 | 52 |
53 | 54 | ## Contributing 55 | 56 | Please read the Contributors guidelines. 57 | 58 | Make sure you bootstrap your environment by running the build command. 59 | 60 | ```pwsh 61 | .\build.ps1 -Task Init -Bootstrap 62 | ``` 63 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | [cmdletbinding(DefaultParameterSetName = 'Task')] 2 | param( 3 | # Build task(s) to execute 4 | [parameter(ParameterSetName = 'task', position = 0)] 5 | [ArgumentCompleter( { 6 | param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) 7 | $psakeFile = './psakeFile.ps1' 8 | switch ($Parameter) { 9 | 'Task' { 10 | if ([string]::IsNullOrEmpty($WordToComplete)) { 11 | Get-PSakeScriptTasks -BuildFile $psakeFile | Select-Object -ExpandProperty Name 12 | } else { 13 | Get-PSakeScriptTasks -BuildFile $psakeFile | 14 | Where-Object { $_.Name -match $WordToComplete } | 15 | Select-Object -ExpandProperty Name 16 | } 17 | } 18 | default { 19 | } 20 | } 21 | })] 22 | [string[]]$Task = 'default', 23 | 24 | # Bootstrap dependencies 25 | [switch]$Bootstrap, 26 | 27 | # List available build tasks 28 | [parameter(ParameterSetName = 'Help')] 29 | [switch]$Help, 30 | 31 | # Optional properties to pass to psake 32 | [hashtable]$Properties, 33 | 34 | # Optional parameters to pass to psake 35 | [hashtable]$Parameters 36 | ) 37 | 38 | $ErrorActionPreference = 'Stop' 39 | 40 | # Bootstrap dependencies 41 | if ($Bootstrap.IsPresent) { 42 | PackageManagement\Get-PackageProvider -Name Nuget -ForceBootstrap | Out-Null 43 | Set-PSRepository -Name PSGallery -InstallationPolicy Trusted 44 | if ((Test-Path -Path ./requirements.psd1)) { 45 | if (-not (Get-Module -Name PSDepend -ListAvailable)) { 46 | Install-Module -Name PSDepend -Repository PSGallery -Scope CurrentUser -Force 47 | } 48 | Import-Module -Name PSDepend -Verbose:$false 49 | Invoke-PSDepend -Path './requirements.psd1' -Install -Import -Force -WarningAction SilentlyContinue 50 | } else { 51 | Write-Warning 'No [requirements.psd1] found. Skipping build dependency installation.' 52 | } 53 | } 54 | 55 | # Execute psake task(s) 56 | $psakeFile = './psakeFile.ps1' 57 | if ($PSCmdlet.ParameterSetName -eq 'Help') { 58 | Get-PSakeScriptTasks -BuildFile $psakeFile | 59 | Format-Table -Property Name, Description, Alias, DependsOn 60 | } else { 61 | Set-BuildEnvironment -Force 62 | Invoke-psake -buildFile $psakeFile -taskList $Task -nologo -properties $Properties -parameters $Parameters 63 | exit ([int](-not $psake.build_success)) 64 | } 65 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "ignorePaths": [], 4 | "dictionaryDefinitions": [], 5 | "dictionaries": [ 6 | "powershell" 7 | ], 8 | "words": [], 9 | "ignoreWords": [ 10 | "pwsh", 11 | "psake" 12 | ], 13 | "import": [] 14 | } 15 | -------------------------------------------------------------------------------- /docs/.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | // These docs are autogenerated and this is the expected format. 3 | "MD025": false, 4 | "MD022": false, 5 | "MD040": false 6 | } 7 | -------------------------------------------------------------------------------- /docs/en-US/Show-PesterResult.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PesterExplorer-help.xml 3 | Module Name: PesterExplorer 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Show-PesterResult 9 | 10 | ## SYNOPSIS 11 | Open a TUI to explore the Pester result object. 12 | 13 | ## SYNTAX 14 | 15 | ``` 16 | Show-PesterResult [-PesterResult] [-NoShortcutPanel] [-ProgressAction ] 17 | [] 18 | ``` 19 | 20 | ## DESCRIPTION 21 | Show a Pester result in a TUI (Text User Interface) using Spectre.Console. 22 | This function builds a layout with a header, a list of items, and a preview panel. 23 | 24 | ## EXAMPLES 25 | 26 | ### EXAMPLE 1 27 | ``` 28 | $pesterResult = Invoke-Pester -Path "path\to\tests.ps1" -PassThru 29 | Show-PesterResult -PesterResult $pesterResult 30 | ``` 31 | 32 | This example runs Pester tests and opens a TUI to explore the results. 33 | 34 | ## PARAMETERS 35 | 36 | ### -PesterResult 37 | The Pester result object to display. 38 | This should be a Pester Run object. 39 | 40 | ```yaml 41 | Type: Run 42 | Parameter Sets: (All) 43 | Aliases: 44 | 45 | Required: True 46 | Position: 1 47 | Default value: None 48 | Accept pipeline input: False 49 | Accept wildcard characters: False 50 | ``` 51 | 52 | ### -NoShortcutPanel 53 | If specified, the shortcut panel will not be displayed at the bottom of the TUI. 54 | 55 | ```yaml 56 | Type: SwitchParameter 57 | Parameter Sets: (All) 58 | Aliases: 59 | 60 | Required: False 61 | Position: Named 62 | Default value: False 63 | Accept pipeline input: False 64 | Accept wildcard characters: False 65 | ``` 66 | 67 | ### -ProgressAction 68 | {{ Fill ProgressAction Description }} 69 | 70 | ```yaml 71 | Type: ActionPreference 72 | Parameter Sets: (All) 73 | Aliases: proga 74 | 75 | Required: False 76 | Position: Named 77 | Default value: None 78 | Accept pipeline input: False 79 | Accept wildcard characters: False 80 | ``` 81 | 82 | ### CommonParameters 83 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 84 | 85 | ## INPUTS 86 | 87 | ## OUTPUTS 88 | 89 | ### System.Void 90 | ## NOTES 91 | 92 | ## RELATED LINKS 93 | -------------------------------------------------------------------------------- /docs/en-US/Show-PesterResultTree.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PesterExplorer-help.xml 3 | Module Name: PesterExplorer 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Show-PesterResultTree 9 | 10 | ## SYNOPSIS 11 | Show a Pester result in a tree format using Spectre.Console. 12 | 13 | ## SYNTAX 14 | 15 | ``` 16 | Show-PesterResultTree [-PesterResult] [-ProgressAction ] [] 17 | ``` 18 | 19 | ## DESCRIPTION 20 | This function takes a Pester result object and formats it into a tree 21 | structure using Spectre.Console. 22 | It is useful for visualizing the structure 23 | of Pester results such as runs, containers, blocks, and tests. 24 | 25 | ## EXAMPLES 26 | 27 | ### EXAMPLE 1 28 | ``` 29 | $pesterResult = Invoke-Pester -Path "path\to\tests.ps1" -PassThru 30 | Show-PesterResultTree -PesterResult $pesterResult 31 | ``` 32 | 33 | This example runs Pester tests and displays the results in a tree format. 34 | 35 | ## PARAMETERS 36 | 37 | ### -PesterResult 38 | The Pester result object to display. 39 | This should be a Pester Run object. 40 | 41 | ```yaml 42 | Type: Run 43 | Parameter Sets: (All) 44 | Aliases: 45 | 46 | Required: True 47 | Position: 1 48 | Default value: None 49 | Accept pipeline input: False 50 | Accept wildcard characters: False 51 | ``` 52 | 53 | ### -ProgressAction 54 | {{ Fill ProgressAction Description }} 55 | 56 | ```yaml 57 | Type: ActionPreference 58 | Parameter Sets: (All) 59 | Aliases: proga 60 | 61 | Required: False 62 | Position: Named 63 | Default value: None 64 | Accept pipeline input: False 65 | Accept wildcard characters: False 66 | ``` 67 | 68 | ### CommonParameters 69 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 70 | 71 | ## INPUTS 72 | 73 | ## OUTPUTS 74 | 75 | ### System.Void 76 | ## NOTES 77 | 78 | ## RELATED LINKS 79 | -------------------------------------------------------------------------------- /docs/en-US/about_PesterExplorer.help.md: -------------------------------------------------------------------------------- 1 | # PesterExplorer 2 | 3 | ## about_PesterExplorer 4 | 5 | # SHORT DESCRIPTION 6 | 7 | A TUI to explore Pester results. 8 | 9 | # LONG DESCRIPTION 10 | 11 | A TUI built on top of PwshSpectreConsole to navigate and see your Pester run 12 | results. 13 | 14 | # EXAMPLES 15 | 16 | To explore your result object you simply need to run `Show-PesterResult` 17 | 18 | ```pwsh 19 | # Run Pester and make sure to PassThru the object 20 | $pester = Invoke-Pester .\tests\ -PassThru 21 | # Now run the TUI 22 | Show-PesterResult $p 23 | ``` 24 | 25 | You can also get a tree view of your pester results with 26 | `Show-PesterResultTree`. 27 | 28 | ```pwsh 29 | # Run Pester and make sure to PassThru the object 30 | $pester = Invoke-Pester .\tests\ -PassThru 31 | # Now get that in a Tree view 32 | Show-PesterResultTree $p 33 | ``` 34 | 35 | # NOTE 36 | 37 | This only works in PowerShell 7 because PwshSpectreConsole is only for 7. 38 | 39 | # TROUBLESHOOTING NOTE 40 | 41 | Make sure your Pester Results are passed back with the `-PassThru` parameter. 42 | 43 | # SEE ALSO 44 | 45 | - [Pester](pester.dev) 46 | - [PwshSpectreConsole](pwshspectreconsole.com) 47 | 48 | # KEYWORDS 49 | 50 | - Pester 51 | - TDD 52 | - TUI 53 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs-include-dir-to-nav 2 | -------------------------------------------------------------------------------- /images/Show-PesterResult.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyItsGilbert/PesterExplorer/81145a480d9757c000649052c904f874589e4227/images/Show-PesterResult.png -------------------------------------------------------------------------------- /images/Show-PesterResultTree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyItsGilbert/PesterExplorer/81145a480d9757c000649052c904f874589e4227/images/Show-PesterResultTree.png -------------------------------------------------------------------------------- /images/fullsize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyItsGilbert/PesterExplorer/81145a480d9757c000649052c904f874589e4227/images/fullsize.png -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyItsGilbert/PesterExplorer/81145a480d9757c000649052c904f874589e4227/images/icon.png -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: PesterExplorer 2 | site_url: http://heyitsgilbert.github.io/PesterExplorer 3 | repo_url: http://github.com/HeyItsGilbert/PesterExplorer 4 | theme: 5 | name: readthedocs 6 | nav: 7 | - Home: "index.md" 8 | - Changelog: "https://github.com/HeyItsGilbert/PesterExplorer/blob/main/CHANGELOG.md" 9 | - en-US: 10 | - en-US/ 11 | plugins: 12 | - search 13 | - include_dir_to_nav 14 | -------------------------------------------------------------------------------- /psakeFile.ps1: -------------------------------------------------------------------------------- 1 | properties { 2 | # Set this to $true to create a module with a monolithic PSM1 3 | $PSBPreference.Build.CompileModule = $false 4 | $PSBPreference.Help.DefaultLocale = 'en-US' 5 | $PSBPreference.Test.OutputFile = 'out/testResults.xml' 6 | } 7 | 8 | task Default -depends Test 9 | 10 | task Test -FromModule PowerShellBuild -minimumVersion '0.6.1' 11 | -------------------------------------------------------------------------------- /requirements.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | PSDepend = @{ 3 | Version = '0.3.8' 4 | } 5 | PSDependOptions = @{ 6 | Target = 'CurrentUser' 7 | } 8 | 'Pester' = @{ 9 | Version = '5.4.0' 10 | Parameters = @{ 11 | SkipPublisherCheck = $true 12 | } 13 | } 14 | 'psake' = @{ 15 | Version = '4.9.0' 16 | } 17 | 'BuildHelpers' = @{ 18 | Version = '2.0.16' 19 | } 20 | 'PowerShellBuild' = @{ 21 | Version = '0.6.1' 22 | } 23 | 'PSScriptAnalyzer' = @{ 24 | Version = '1.24.0' 25 | } 26 | 'PwshSpectreConsole' = @{ 27 | Version = '2.3.0' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Get-PreviewPanel.tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Get-PreviewPanel' { 2 | BeforeAll { 3 | . (Join-Path $PSScriptRoot 'Helpers.ps1') 4 | $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest 5 | $outputDir = Join-Path -Path $env:BHProjectPath -ChildPath 'Output' 6 | $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName 7 | $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion 8 | $outputModVerManifest = Join-Path -Path $outputModVerDir -ChildPath "$($env:BHProjectName).psd1" 9 | 10 | # Get module commands 11 | # Remove all versions of the module from the session. Pester can't handle multiple versions. 12 | Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore 13 | Import-Module -Name $outputModVerManifest -Verbose:$false -ErrorAction Stop 14 | 15 | InModuleScope $env:BHProjectName { 16 | $script:ContainerWidth = 80 17 | $script:ContainerHeight = 200 18 | $container = New-PesterContainer -Scriptblock { 19 | Describe 'Demo Tests' { 20 | Context 'Contextualize It' { 21 | It 'Test1' { 22 | $true | Should -Be $true 23 | } 24 | It 'Test2' { 25 | $false | Should -Be $true 26 | } 27 | } 28 | } 29 | } 30 | $script:run = Invoke-Pester -Container $container -PassThru -Output 'None' 31 | $script:Items = Get-ListFromObject -Object $run.Containers[0] 32 | 33 | $script:getPreviewPanelSplat = @{ 34 | Items = $script:Items 35 | SelectedItem = 'Demo Tests' 36 | PreviewHeight = $script:ContainerHeight 37 | PreviewWidth = $script:ContainerWidth 38 | } 39 | $script:panel = Get-PreviewPanel @getPreviewPanelSplat 40 | } 41 | } 42 | It 'should return a Spectre.Console.Panel object' { 43 | InModuleScope $env:BHProjectName { 44 | $script:panel.GetType().ToString() | Should -BeExactly 'Spectre.Console.Panel' 45 | } 46 | } 47 | 48 | It 'should call breakdown chart' { 49 | InModuleScope $env:BHProjectName { 50 | Mock Format-SpectreBreakdownChart -Verifiable 51 | $script:panel = Get-PreviewPanel @script:getPreviewPanelSplat 52 | Should -Invoke Format-SpectreBreakdownChart -Exactly 1 -Scope It 53 | } 54 | } 55 | 56 | It 'should print "Please select an item." when SelectedItem is ".."' { 57 | InModuleScope $env:BHProjectName { 58 | $getPreviewPanelSplat = @{ 59 | Items = $script:Items 60 | SelectedItem = '..' 61 | PreviewHeight = $script:ContainerHeight 62 | PreviewWidth = $script:ContainerWidth 63 | } 64 | $panel = Get-PreviewPanel @getPreviewPanelSplat 65 | global:Get-RenderedText -Panel $panel | 66 | Should -BeLike "*Please select an item.*" 67 | } 68 | } 69 | 70 | It 'should print warning when the screen is too small' { 71 | InModuleScope $env:BHProjectName { 72 | $Items = Get-ListFromObject -Object $script:run.Containers[0].Blocks[0].Order[0] 73 | $height = 5 74 | $getPreviewPanelSplat = @{ 75 | Items = $Items 76 | SelectedItem = 'Test1' 77 | ScrollPosition = 1 78 | PreviewHeight = $height 79 | PreviewWidth = $script:ContainerWidth 80 | } 81 | $panel = Get-PreviewPanel @getPreviewPanelSplat 82 | global:Get-RenderedText -Panel $panel | 83 | Should -BeLike "*resize your terminal*" 84 | } 85 | } 86 | 87 | It 'should print the script block for a Test' { 88 | InModuleScope $env:BHProjectName { 89 | $Items = Get-ListFromObject -Object $script:run.Containers[0].Blocks[0].Order[0].Tests 90 | $getPreviewPanelSplat = @{ 91 | Items = $Items 92 | SelectedItem = 'Test1' 93 | PreviewHeight = $script:ContainerHeight 94 | PreviewWidth = $script:ContainerWidth 95 | } 96 | $panel = Get-PreviewPanel @getPreviewPanelSplat 97 | global:Get-RenderedText -panel $panel | 98 | Should -BeLike '*$true | Should -Be $true*' 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/Get-ShortcutKeyPanel.tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Get-ShortcutKeyPanel' { 2 | BeforeAll { 3 | $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest 4 | $outputDir = Join-Path -Path $env:BHProjectPath -ChildPath 'Output' 5 | $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName 6 | $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion 7 | $outputModVerManifest = Join-Path -Path $outputModVerDir -ChildPath "$($env:BHProjectName).psd1" 8 | 9 | # Get module commands 10 | # Remove all versions of the module from the session. Pester can't handle multiple versions. 11 | Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore 12 | Import-Module -Name $outputModVerManifest -Verbose:$false -ErrorAction Stop 13 | 14 | InModuleScope $env:BHProjectName { 15 | $script:ContainerWidth = 80 16 | $script:ContainerHeight = 5 17 | $size = [Spectre.Console.Size]::new($containerWidth, $containerHeight) 18 | $script:renderOptions = [Spectre.Console.Rendering.RenderOptions]::new( 19 | [Spectre.Console.AnsiConsole]::Console.Profile.Capabilities, 20 | $size 21 | ) 22 | $script:renderOptions.Justification = $null 23 | $script:renderOptions.Height = $null 24 | } 25 | } 26 | It 'should return a Spectre.Console.Panel object' { 27 | InModuleScope $env:BHProjectName { 28 | $panel = Get-ShortcutKeyPanel 29 | $panel.GetType().ToString() | Should -BeExactly 'Spectre.Console.Panel' 30 | } 31 | } 32 | 33 | It 'should print some known keys' { 34 | InModuleScope $env:BHProjectName { 35 | Mock -CommandName 'Get-Date' -MockWith { '2025-01-10 12:00:00' } 36 | $panel = Get-ShortcutKeyPanel 37 | $render = $panel.Render($script:renderOptions, $script:ContainerWidth) 38 | # These are rendered segments. 39 | ( 40 | 'Navigate' 41 | ) | ForEach-Object { 42 | $render.Text | Should -Contain $_ 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Get-TitlePanel.tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Get-TitlePanel' { 2 | BeforeAll { 3 | . (Join-Path $PSScriptRoot 'Helpers.ps1') 4 | $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest 5 | $outputDir = Join-Path -Path $env:BHProjectPath -ChildPath 'Output' 6 | $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName 7 | $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion 8 | $outputModVerManifest = Join-Path -Path $outputModVerDir -ChildPath "$($env:BHProjectName).psd1" 9 | 10 | # Get module commands 11 | # Remove all versions of the module from the session. Pester can't handle multiple versions. 12 | Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore 13 | Import-Module -Name $outputModVerManifest -Verbose:$false -ErrorAction Stop 14 | 15 | InModuleScope $env:BHProjectname { 16 | $script:pesterResult = Invoke-Pester -PassThru -Path "$PSScriptRoot\fixtures\Example.ps1" -Output 'None' 17 | } 18 | } 19 | It 'should return a Spectre.Console.Panel object' { 20 | InModuleScope $env:BHProjectName { 21 | $title = Get-TitlePanel 22 | $title.GetType().ToString() | Should -BeExactly 'Spectre.Console.Panel' 23 | } 24 | } 25 | 26 | It 'should print Pester Explorer with current date' { 27 | InModuleScope $env:BHProjectName { 28 | Mock -CommandName 'Get-Date' -MockWith { '2025-01-10 12:00:00' } 29 | $title = Get-TitlePanel 30 | global:Get-RenderedText -Panel $title | 31 | Should -Contain 'Pester Explorer - 2025-01-10 12:00:00' 32 | } 33 | } 34 | 35 | It 'should print pester run name and type' { 36 | InModuleScope $env:BHProjectName { 37 | $titleWithItem = Get-TitlePanel -Item $script:pesterResult 38 | $renderWithItem = global:Get-RenderedText -Panel $titleWithItem 39 | $renderWithItem | Should -Match 'Run: . Pester.Run' 40 | } 41 | } 42 | 43 | It 'should print container name and type' { 44 | InModuleScope $env:BHProjectName { 45 | $titleWithItem = Get-TitlePanel -Item $script:pesterResult.Containers[0] 46 | $renderWithItem = global:Get-RenderedText -Panel $titleWithItem 47 | $renderWithItem | Should -BeLike '*Container:*Example.ps1' 48 | } 49 | } 50 | 51 | It 'should print block name and type' { 52 | InModuleScope $env:BHProjectName { 53 | $titleWithItem = Get-TitlePanel -Item $script:pesterResult.Containers[0].Blocks[0] 54 | $renderWithItem = global:Get-RenderedText -Panel $titleWithItem 55 | $renderWithItem | Should -Match 'Block: . Example Tests' 56 | } 57 | } 58 | 59 | Context 'Tests' { 60 | BeforeDiscovery { 61 | $pesterResult = Invoke-Pester -PassThru -Path "$PSScriptRoot\fixtures\Example.ps1" -Output 'None' 62 | $tests = $pesterResult.Containers[0].Blocks[0].Tests 63 | } 64 | 65 | It 'should print test name <_>' -ForEach $tests { 66 | InModuleScope $env:BHProjectName -ArgumentList $_ -ScriptBlock { 67 | param($test) 68 | $titleWithItem = Get-TitlePanel -Item $test 69 | $renderWithItem = global:Get-RenderedText -Panel $titleWithItem 70 | $renderWithItem | Should -Match "Test: .*$($test.Name)" 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Help.tests.ps1: -------------------------------------------------------------------------------- 1 | # Taken with love from @juneb_get_help (https://raw.githubusercontent.com/juneb/PesterTDD/master/Module.Help.Tests.ps1) 2 | 3 | BeforeDiscovery { 4 | 5 | function global:FilterOutCommonParams { 6 | param ($Params) 7 | $commonParameters = [System.Management.Automation.PSCmdlet]::CommonParameters + 8 | [System.Management.Automation.PSCmdlet]::OptionalCommonParameters 9 | $params | Where-Object { $_.Name -notin $commonParameters } | Sort-Object -Property Name -Unique 10 | } 11 | 12 | $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest 13 | $outputDir = Join-Path -Path $env:BHProjectPath -ChildPath 'Output' 14 | $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName 15 | $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion 16 | $outputModVerManifest = Join-Path -Path $outputModVerDir -ChildPath "$($env:BHProjectName).psd1" 17 | 18 | # Get module commands 19 | # Remove all versions of the module from the session. Pester can't handle multiple versions. 20 | Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore 21 | Import-Module -Name $outputModVerManifest -Verbose:$false -ErrorAction Stop 22 | $params = @{ 23 | Module = (Get-Module $env:BHProjectName) 24 | CommandType = [System.Management.Automation.CommandTypes[]]'Cmdlet, Function' # Not alias 25 | } 26 | if ($PSVersionTable.PSVersion.Major -lt 6) { 27 | $params.CommandType[0] += 'Workflow' 28 | } 29 | $commands = Get-Command @params 30 | 31 | ## When testing help, remember that help is cached at the beginning of each session. 32 | ## To test, restart session. 33 | } 34 | 35 | Describe "Test help for <_.Name>" -ForEach $commands { 36 | 37 | BeforeDiscovery { 38 | # Get command help, parameters, and links 39 | $command = $_ 40 | $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue 41 | $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters 42 | $commandParameterNames = $commandParameters.Name 43 | $helpLinks = $commandHelp.relatedLinks.navigationLink.uri 44 | } 45 | 46 | BeforeAll { 47 | # These vars are needed in both discovery and test phases so we need to duplicate them here 48 | $command = $_ 49 | $commandName = $_.Name 50 | $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue 51 | $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters 52 | $commandParameterNames = $commandParameters.Name 53 | $helpParameters = global:FilterOutCommonParams -Params $commandHelp.Parameters.Parameter 54 | $helpParameterNames = $helpParameters.Name 55 | } 56 | 57 | # If help is not found, synopsis in auto-generated help is the syntax diagram 58 | It 'Help is not auto-generated' { 59 | $commandHelp.Synopsis | Should -Not -BeLike '*`[``]*' 60 | } 61 | 62 | # Should be a description for every function 63 | It "Has description" { 64 | $commandHelp.Description | Should -Not -BeNullOrEmpty 65 | } 66 | 67 | # Should be at least one example 68 | It "Has example code" { 69 | ($commandHelp.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty 70 | } 71 | 72 | # Should be at least one example description 73 | It "Has example help" { 74 | ($commandHelp.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty 75 | } 76 | 77 | It "Help link <_> is valid" -ForEach $helpLinks { 78 | (Invoke-WebRequest -Uri $_ -UseBasicParsing).StatusCode | Should -Be '200' 79 | } 80 | 81 | Context "Parameter <_.Name>" -ForEach $commandParameters { 82 | 83 | BeforeAll { 84 | $parameter = $_ 85 | $parameterName = $parameter.Name 86 | $parameterHelp = $commandHelp.parameters.parameter | Where-Object Name -EQ $parameterName 87 | $parameterHelpType = if ($parameterHelp.ParameterValue) { $parameterHelp.ParameterValue.Trim() } 88 | } 89 | 90 | # Should be a description for every parameter 91 | It "Has description" { 92 | $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty 93 | } 94 | 95 | # Required value in Help should match IsMandatory property of parameter 96 | It "Has correct [mandatory] value" { 97 | $codeMandatory = $_.IsMandatory.toString() 98 | $parameterHelp.Required | Should -Be $codeMandatory 99 | } 100 | 101 | # Parameter type in help should match code 102 | It "Has correct parameter type" { 103 | $parameterHelpType | Should -Be $parameter.ParameterType.Name 104 | } 105 | } 106 | 107 | Context "Test <_> help parameter help for " -ForEach $helpParameterNames { 108 | 109 | # Shouldn't find extra parameters in help. 110 | It "finds help parameter in code: <_>" { 111 | $_ -in $parameterNames | Should -Be $true 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/Helpers.ps1: -------------------------------------------------------------------------------- 1 | function global:Get-RenderedText { 2 | <# 3 | .SYNOPSIS 4 | Returns the rendered text from a panel object. 5 | 6 | .DESCRIPTION 7 | This function processes a panel object to extract and return the rendered 8 | text. It filters out control codes and specific characters, joining the 9 | remaining text segments into a single string. 10 | 11 | .PARAMETER Panel 12 | The panel object to be processed. It should have a Render method that 13 | returns a collection of text segments. 14 | 15 | .PARAMETER RenderOptions 16 | Options to control the rendering of the panel. This is passed to the Render method of the panel. 17 | 18 | .PARAMETER ContainerWidth 19 | The width of the container in which the panel is rendered. This is also passed to the Render method of the panel. 20 | 21 | .EXAMPLE 22 | $panel = Get-PanelObject -Name "ExamplePanel" 23 | $renderOptions = Get-RenderOptions -SomeOption "Value" 24 | $containerWidth = 80 25 | $renderedText = global:Get-RenderedText -panel $panel -renderOptions $renderOptions -containerWidth $containerWidth 26 | 27 | This example retrieves a panel object, specifies rendering options and 28 | container width, and then calls the function to get the rendered text. 29 | .NOTES 30 | This is a helper function we can use for our tests. 31 | #> 32 | param ( 33 | [Parameter(Mandatory = $true)] 34 | #[Spectre.Console.Panel] 35 | $Panel, 36 | [Parameter()] 37 | [int] 38 | $ContainerHeight = 200, 39 | [Parameter()] 40 | [int] 41 | $ContainerWidth = 100 42 | ) 43 | $size = [Spectre.Console.Size]::new($ContainerWidth, $ContainerHeight) 44 | $renderOptions = [Spectre.Console.Rendering.RenderOptions]::new( 45 | [Spectre.Console.AnsiConsole]::Console.Profile.Capabilities, 46 | $size 47 | ) 48 | $render = $Panel.Render($RenderOptions, $ContainerWidth) 49 | 50 | # These are rendered segments. 51 | $onlyText = $render | 52 | Where-Object { 53 | #$_.IsLineBreak -ne $true -and 54 | $_.IsControlCode -ne $true -and 55 | #$_.IsWhiteSpace -ne $true -and 56 | $_.Text -notin @('┌', '┐', '└', '┘', '─', '│') -and 57 | $_.Text -notmatch '─{2,}' 58 | } 59 | # Join the text segments into a single string 60 | $output = [System.Text.StringBuilder]::new() 61 | 62 | foreach ($textSegment in $onlyText) { 63 | if ($textSegment.IsLineBreak) { 64 | [void]$output.AppendLine() # Append a newline for line breaks 65 | } else { 66 | [void]$output.Append($textSegment.Text) 67 | } 68 | } 69 | return $output.ToString().Trim() 70 | } 71 | -------------------------------------------------------------------------------- /tests/Manifest.tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | 3 | # NEW: Pre-Specify RegEx Matching Patterns 4 | $gitTagMatchRegEx = 'tag:\s?.(\d+(\.\d+)*)' # NOTE - was 'tag:\s*(\d+(?:\.\d+)*)' previously 5 | $changelogTagMatchRegEx = "^##\s\[(?(\d+\.){1,3}\d+)\]" 6 | 7 | $moduleName = $env:BHProjectName 8 | $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest 9 | $outputDir = Join-Path -Path $ENV:BHProjectPath -ChildPath 'Output' 10 | $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName 11 | $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion 12 | $outputManifestPath = Join-Path -Path $outputModVerDir -Child "$($moduleName).psd1" 13 | $manifestData = Test-ModuleManifest -Path $outputManifestPath -Verbose:$false -ErrorAction Stop -WarningAction SilentlyContinue 14 | 15 | $changelogPath = Join-Path -Path $env:BHProjectPath -Child 'CHANGELOG.md' 16 | $changelogVersion = Get-Content $changelogPath | ForEach-Object { 17 | if ($_ -match $changelogTagMatchRegEx) { 18 | $changelogVersion = $matches.Version 19 | break 20 | } 21 | } 22 | 23 | $script:manifest = $null 24 | } 25 | Describe 'Module manifest' { 26 | 27 | Context 'Validation' { 28 | 29 | It 'Has a valid manifest' { 30 | $manifestData | Should -Not -BeNullOrEmpty 31 | } 32 | 33 | It 'Has a valid name in the manifest' { 34 | $manifestData.Name | Should -Be $moduleName 35 | } 36 | 37 | It 'Has a valid root module' { 38 | $manifestData.RootModule | Should -Be "$($moduleName).psm1" 39 | } 40 | 41 | It 'Has a valid version in the manifest' { 42 | $manifestData.Version -as [Version] | Should -Not -BeNullOrEmpty 43 | } 44 | 45 | It 'Has a valid description' { 46 | $manifestData.Description | Should -Not -BeNullOrEmpty 47 | } 48 | 49 | It 'Has a valid author' { 50 | $manifestData.Author | Should -Not -BeNullOrEmpty 51 | } 52 | 53 | It 'Has a valid guid' { 54 | {[guid]::Parse($manifestData.Guid)} | Should -Not -Throw 55 | } 56 | 57 | It 'Has a valid copyright' { 58 | $manifestData.CopyRight | Should -Not -BeNullOrEmpty 59 | } 60 | 61 | It 'Has a valid version in the changelog' { 62 | $changelogVersion | Should -Not -BeNullOrEmpty 63 | $changelogVersion -as [Version] | Should -Not -BeNullOrEmpty 64 | } 65 | 66 | It 'Changelog and manifest versions are the same' { 67 | $changelogVersion -as [Version] | Should -Be ( $manifestData.Version -as [Version] ) 68 | } 69 | } 70 | } 71 | 72 | Describe 'Git tagging' -Skip { 73 | BeforeAll { 74 | $gitTagVersion = $null 75 | 76 | # Ensure to only pull in a single git executable (in case multiple git's are found on path). 77 | if ($git = (Get-Command git -CommandType Application -ErrorAction SilentlyContinue)[0]) { 78 | $thisCommit = & $git log --decorate --oneline HEAD~1..HEAD 79 | if ($thisCommit -match $gitTagMatchRegEx) { $gitTagVersion = $matches[1] } 80 | } 81 | } 82 | 83 | It 'Is tagged with a valid version' { 84 | $gitTagVersion | Should -Not -BeNullOrEmpty 85 | $gitTagVersion -as [Version] | Should -Not -BeNullOrEmpty 86 | } 87 | 88 | It 'Matches manifest version' { 89 | $manifestData.Version -as [Version] | Should -Be ( $gitTagVersion -as [Version]) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/Meta.tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | 3 | Set-StrictMode -Version latest 4 | 5 | # Make sure MetaFixers.psm1 is loaded - it contains Get-TextFilesList 6 | Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'MetaFixers.psm1') -Verbose:$false -Force 7 | 8 | $projectRoot = $ENV:BHProjectPath 9 | if (-not $projectRoot) { 10 | $projectRoot = $PSScriptRoot 11 | } 12 | 13 | $allTextFiles = Get-TextFilesList $projectRoot 14 | $unicodeFilesCount = 0 15 | $totalTabsCount = 0 16 | foreach ($textFile in $allTextFiles) { 17 | if (Test-FileUnicode $textFile) { 18 | $unicodeFilesCount++ 19 | Write-Warning ( 20 | "File $($textFile.FullName) contains 0x00 bytes." + 21 | " It probably uses Unicode/UTF-16 and needs to be converted to UTF-8." + 22 | " Use Fixer 'Get-UnicodeFilesList `$pwd | ConvertTo-UTF8'." 23 | ) 24 | } 25 | $unicodeFilesCount | Should -Be 0 26 | 27 | $fileName = $textFile.FullName 28 | (Get-Content $fileName -Raw) | Select-String "`t" | Foreach-Object { 29 | Write-Warning ( 30 | "There are tabs in $fileName." + 31 | " Use Fixer 'Get-TextFilesList `$pwd | ConvertTo-SpaceIndentation'." 32 | ) 33 | $totalTabsCount++ 34 | } 35 | } 36 | } 37 | 38 | Describe 'Text files formatting' { 39 | Context 'File encoding' { 40 | It "No text file uses Unicode/UTF-16 encoding" { 41 | $unicodeFilesCount | Should -Be 0 42 | } 43 | } 44 | 45 | Context 'Indentations' { 46 | It "No text file use tabs for indentations" { 47 | $totalTabsCount | Should -Be 0 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/MetaFixers.psm1: -------------------------------------------------------------------------------- 1 | # Taken with love from https://github.com/PowerShell/DscResource.Tests/blob/master/MetaFixers.psm1 2 | 3 | <# 4 | This module helps fix problems, found by Meta.Tests.ps1 5 | #> 6 | 7 | $ErrorActionPreference = 'stop' 8 | Set-StrictMode -Version latest 9 | 10 | function ConvertTo-UTF8() { 11 | [CmdletBinding()] 12 | [OutputType([void])] 13 | param( 14 | [Parameter(Mandatory, ValueFromPipeline)] 15 | [System.IO.FileInfo]$FileInfo 16 | ) 17 | 18 | process { 19 | $content = Get-Content -Raw -Encoding Unicode -Path $FileInfo.FullName 20 | [System.IO.File]::WriteAllText($FileInfo.FullName, $content, [System.Text.Encoding]::UTF8) 21 | } 22 | } 23 | 24 | function ConvertTo-SpaceIndentation() { 25 | [CmdletBinding()] 26 | [OutputType([void])] 27 | param( 28 | [Parameter(Mandatory, ValueFromPipeline)] 29 | [IO.FileInfo]$FileInfo 30 | ) 31 | 32 | process { 33 | $content = (Get-Content -Raw -Path $FileInfo.FullName) -replace "`t", ' ' 34 | [IO.File]::WriteAllText($FileInfo.FullName, $content) 35 | } 36 | } 37 | 38 | function Get-TextFilesList { 39 | [CmdletBinding()] 40 | [OutputType([IO.FileInfo])] 41 | param( 42 | [Parameter(Mandatory, ValueFromPipeline)] 43 | [string]$Root 44 | ) 45 | 46 | begin { 47 | $txtFileExtentions = @('.gitignore', '.gitattributes', '.ps1', '.psm1', '.psd1', '.json', '.xml', '.cmd', '.mof') 48 | } 49 | 50 | process { 51 | Get-ChildItem -Path $Root -File -Recurse | 52 | Where-Object { $_.Extension -in $txtFileExtentions } 53 | } 54 | } 55 | 56 | function Test-FileUnicode { 57 | [CmdletBinding()] 58 | [OutputType([bool])] 59 | param( 60 | [Parameter(Mandatory, ValueFromPipeline)] 61 | [IO.FileInfo]$FileInfo 62 | ) 63 | 64 | process { 65 | $bytes = [IO.File]::ReadAllBytes($FileInfo.FullName) 66 | $zeroBytes = @($bytes -eq 0) 67 | return [bool]$zeroBytes.Length 68 | } 69 | } 70 | 71 | function Get-UnicodeFilesList() { 72 | [CmdletBinding()] 73 | [OutputType([IO.FileInfo])] 74 | param( 75 | [Parameter(Mandatory)] 76 | [string]$Root 77 | ) 78 | 79 | $root | Get-TextFilesList | Where-Object { Test-FileUnicode $_ } 80 | } 81 | -------------------------------------------------------------------------------- /tests/ScriptAnalyzerSettings.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/Example.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Example Tests' { 2 | BeforeAll { 3 | $script:TestVariable = "Initial Value" 4 | } 5 | 6 | It 'should pass' { 7 | $script:TestVariable | Should -Be "Initial Value" 8 | } 9 | 10 | It 'should skip this' { 11 | Set-ItResult -Skipped 'This test is skipped intentionally.' 12 | } 13 | 14 | It 'should be inconclusive' { 15 | Set-ItResult -Inconclusive 'This test is inconclusive.' 16 | } 17 | 18 | It 'should be pending' { 19 | Set-ItResult -Pending 'This test is pending.' 20 | } 21 | 22 | AfterAll { 23 | $script:TestVariable = $null 24 | } 25 | } 26 | --------------------------------------------------------------------------------