ISEPester.psm1

#region prefix code
#region content of file ImportModule
try {
    Import-Module -Name Pester -MinimumVersion 5.0 -ErrorAction Stop
} catch {
    Write-Error -Message "Failed to import module Pester - can't continue. Error: $_"
    return
}
#endregion
#endregion
#region private functions
#region content of file Get-psISE
function Get-psISE {
    <#
        .Synopsis
        Helper function that returns psISE object (needed for testing /mocking)
 
        .Description
        $psISE automatic variable is read-only and can't be replaced inside ISE.
        To prevent not being able to test module inside that host - adding simple function that returns this object.
 
        .Example
        $test = Get-psISE
         
        Saves value of psISE object into variable test.
    #>

    [CmdletBinding()]
    param ()

    $psISE
}
#endregion
#endregion
#region public functions
#region content of file Invoke-ISECurrentTest
function Invoke-ISECurrentTest {
    <#
        .Synopsis
        Function to run tests based on cursor location in editor file.
 
        .Description
        PowerShell ISE allows to see current location of the cursor in editor tab.
        Using that information makes it possible to run a test based on that location.
        Logic is:
        - if the cursor is on the line with container name (It, Context, Describe) that block is called.
        - if the cursor is on the line inside any `It` block - that block only would be called.
 
        .Example
        Invoke-ISECurrentTest
         
        Runs test on the line where cursor in the current file is located.
    #>

    [CmdletBinding()]
    param ()
    try {
        $ise = Get-psISE
    } catch {
        Write-Warning -Message 'Command designed to use in PowerShell ISE'
    }

    if (-not (Get-Module -Name Pester)) {
        try {
            Import-Module -Name Pester -MinimumVersion 5.0 -ErrorAction Stop
        } catch {
            Write-Warning -Message "Failed to import Pester module - $_"
        }
    }

    if (
        ($file = $ise.CurrentFile) -and
        (Test-Path -LiteralPath $file.FullPath) -and
        ($line = $file.Editor.CaretLineText) -and
        ($lineNumber = $file.Editor.CaretLine)
    ) {
        if (-not $file.IsSaved) {
            Write-Warning -Message "File $($file.FullPath) is not saved - working on current copy on disk!"
        }
        $config = [PesterConfiguration]@{
            Run = @{
                Path = $file.FullPath
            }
        }
        $config.Output = $script:outputConfiguration
        if ($line -match '\s*(Describe|Context|It)') {
            $config.Filter.Line = '{0}:{1}' -f $file.FullPath, $lineNumber
        } else {
            $parsedTestFile = [System.Management.Automation.Language.Parser]::ParseFile($file.FullPath, [ref]$null, [ref]$null)
            $myItBlock = $parsedTestFile.FindAll(
                {
                    param (
                        $Ast
                    )
                    $Ast.CommandElements -and
                    $Ast.CommandElements[0].Value -eq 'It' -and
                    $Ast.Extent.StartLineNumber -le $lineNumber -and
                    $Ast.Extent.EndLineNumber -ge $lineNumber
                },
                $true
            )
            if ($myItBlock) {
                $config.Filter.Line = '{0}:{1}' -f $file.FullPath, $myItBlock[0].Extent.StartLineNumber
            } else {
                Write-Warning -Message "Line '$line' at $lineNumber is not inside It block - perhaps $($file.FullPath) is not a test file?"
                return
            }
        }
        if ($script:invokeScope -eq 'ParentScope') {
            Invoke-Pester -Configuration $config
        } else {
            & {
                Invoke-Pester -Configuration $config
            }
        }
    } else {
        Write-Warning -Message 'Command can work only with test files saved on disk - save it first!'
    }
}
#endregion
#region content of file Set-ISEPesterConfiguration
function Set-ISEPesterConfiguration {
    <#
        .Synopsis
        Function to configure the way Pester tests run in the ISE.
         
        .Description
        Functions allows to configure how tests in context of ISE will behave. It includes:
        - output options
        - scoping (to prevent polluting current scope)
         
        .Example
        Set-ISEPesterConfiguration -Verbosity Detailed -StackTraceVerbosity Filtered
        Changes outpuf of pester calls to:
        - display detailed results
        - show filter view for stack trace
 
        .Example
        Set-ISEPesterConfiguration -Invoke ChildScope
        Configures command to run in the child scope to prevent polluting parent scope.
    #>

    [CmdletBinding()]
    param (
        # Verbosity of output
        [ValidateSet(
            'None',
            'Normal',
            'Detailed',
            'Diagnostic'
        )]
        [String]$Verbosity,

        # Verbosity of stack trace
        [ValidateSet(
            'None',
            'FirstLine',
            'Filtered',
            'Full'
        )]
        [String]$StackTraceVerbosity,

        # The CI format of error output in build logs
        [ValidateSet(
            'None',
            'Auto',
            'AzureDevops',
            'GithubActions'
        )]
        [String]$CIFormat,

        # The way of running tests
        [ValidateSet(
            'ParentScope',
            'ChildScope'
        )]
        [String]$InvokeScope
    )

    foreach (
        $key in @(
            'Verbosity'
            'StackTraceVerbosity'
            'CIFormat'
        )
    ) {
        if ($PSBoundParameters.ContainsKey($key)) {
            $script:outputConfiguration.$key = $PSBoundParameters.$key
        }
    }

    if ($PSBoundParameters.ContainsKey('InvokeScope')) {
        $script:invokeScope = $InvokeScope
    }
}
#endregion
#endregion
#region sufix
#region content of file ISEAddOn
try {
    $null = $psise.CurrentPowerShellTab.AddOnsMenu.Submenus.Add(
        'Run in Pester',
        { Invoke-ISECurrentTest },
        'CTRL+F8'
    )
} catch {
    Write-Warning -Message "Failed to add shortcut - $_"
}

$script:outputConfiguration = [Pester.OutputConfiguration]::Default
$script:invokeScope = 'ParentScope'
#endregion
#endregion