Tests/Tests-ColorCommands.ps1
|
#Requires -Modules @{ ModuleName='Pester'; ModuleVersion='5.0.0' } # Format-ColorEX, Show-ColorTable, Register-ColorName, Unregister-ColorName, Export-ColorProfile, # Import-ColorProfile, Remove-ColorProfile, tab completion, and PSColorStyle's background # gradient, underline and layout properties. BeforeAll { $ModuleRoot = Split-Path -Parent $PSScriptRoot Import-Module "$ModuleRoot\PSWriteColorEX.psd1" -Force $esc = [char]27 function Set-TestColorSupport { param($Support, [bool]$BoldFonts = $true) & (Get-Module PSWriteColorEX) { param($s, $b) $script:CachedANSISupport = $s; $script:SupportsBoldFonts = $b } $Support $BoldFonts } function Get-TestColorSupport { & (Get-Module PSWriteColorEX) { @($script:CachedANSISupport, $script:SupportsBoldFonts) } } function Save-ColorEnvironment { $saved = @{} foreach ($name in 'NO_COLOR', 'FORCE_COLOR', 'TERM', 'CLICOLOR', 'CLICOLOR_FORCE') { $saved[$name] = [System.Environment]::GetEnvironmentVariable($name) } foreach ($name in 'NO_COLOR', 'FORCE_COLOR', 'CLICOLOR', 'CLICOLOR_FORCE') { [System.Environment]::SetEnvironmentVariable($name, $null) } if ($saved['TERM'] -eq 'dumb') { [System.Environment]::SetEnvironmentVariable('TERM', $null) } return $saved } function Restore-ColorEnvironment { param([hashtable]$Saved) foreach ($name in $Saved.Keys) { [System.Environment]::SetEnvironmentVariable($name, $Saved[$name]) } } # The color names and profiles a test added, removed so the next test starts from the # module's own function Reset-ColorState { & (Get-Module PSWriteColorEX) { $script:CustomColors.Clear() $script:CachedColorTable = $null [PSColorStyle]::Profiles.Clear() [PSColorStyle]::InitializeDefaultProfiles() } } } Describe 'Formatting, color name and profile commands' -Tag 'Unit', 'Commands' { BeforeAll { $script:hostCalls = [System.Collections.Generic.List[object]]::new() Mock -ModuleName PSWriteColorEX Write-Host { $script:hostCalls.Add([pscustomobject]@{ Object = [string]$Object NoNewline = [bool]$NoNewline ForegroundColor = if ($PesterBoundParameters.ContainsKey('ForegroundColor')) { [string]$ForegroundColor } else { '' } BackgroundColor = if ($PesterBoundParameters.ContainsKey('BackgroundColor')) { [string]$BackgroundColor } else { '' } }) } Mock -ModuleName PSWriteColorEX Test-ColorHostAnsi { $true } Mock -ModuleName PSWriteColorEX Test-ColorLineComposition { $true } $script:savedSupport = Get-TestColorSupport } AfterAll { Set-TestColorSupport -Support $script:savedSupport[0] -BoldFonts $script:savedSupport[1] Reset-ColorState } BeforeEach { $script:hostCalls.Clear() $script:savedEnvironment = Save-ColorEnvironment Set-TestColorSupport -Support 'TrueColor' } AfterEach { Restore-ColorEnvironment -Saved $script:savedEnvironment Reset-ColorState } Context 'Format-ColorEX' { It 'Answers the line as a string, writing nothing to the host' { $line = Format-ColorEX 'OK' -Color Green -Bold $line | Should -BeOfType [string] $line | Should -Be "$esc[1m$esc[92mOK$esc[0m" $script:hostCalls | Should -HaveCount 0 } It 'Fits inside a larger string' { "Status: $(Format-ColorEX 'OK' -Color Green)" | Should -Be "Status: $esc[92mOK$esc[0m" } It 'Answers one string for each line -Wrap makes' { $lines = @(Format-ColorEX 'the quick brown fox' -AutoPad 10 -Wrap) $lines | Should -Be @('the quick ', 'brown fox ') } It 'Answers one string for each string piped in' { $lines = @('a', 'b' | Format-ColorEX -Color Red) $lines | Should -Be @("$esc[91ma$esc[0m", "$esc[91mb$esc[0m") } It 'Answers the text alone with colors off' { [System.Environment]::SetEnvironmentVariable('NO_COLOR', '1') Format-ColorEX 'x' -Color Red -Bold | Should -Be 'x' } It 'Answers the text alone where only console colors would be written' { Set-TestColorSupport -Support 'None' Format-ColorEX 'a', 'b' -Color Red, Green | Should -Be 'ab' } It 'Answers empty strings for a profile''s blank lines' { $style = New-ColorStyle -Name 'Spaced' -ForegroundColor Red -LinesBefore 1 -LinesAfter 1 $lines = @(Format-ColorEX 'x' -StyleProfile $style) $lines | Should -Be @('', "$esc[91mx$esc[0m", '') } It 'Passes Write-ColorEX''s warnings on' { Format-ColorEX 'x' -Color Purpel -WarningVariable warnings -WarningAction SilentlyContinue | Out-Null @($warnings | ForEach-Object { "$_" }) | Should -Be @("Unknown color 'Purpel'.") } It 'Has the aliases Format-ColourEX and FCEX' { (Get-Command Format-ColourEX).ResolvedCommandName | Should -Be 'Format-ColorEX' (Get-Command FCEX).ResolvedCommandName | Should -Be 'Format-ColorEX' } } Context 'Register-ColorName and Unregister-ColorName' { It 'Adds a name every command takes, in every color mode' { Register-ColorName -Name Brand -Color '#FF6B35' Write-ColorEX -Text 'x' -Color Brand -TrueColor Write-ColorEX -Text 'x' -Color Brand -ANSI8 Write-ColorEX -Text '[Brand]x[/]' -Markup $script:hostCalls[0].Object | Should -Be "$esc[38;2;255;107;53mx$esc[0m" $script:hostCalls[1].Object | Should -Be "$esc[38;5;$(Convert-RGBToANSI8 -RGB @(255, 107, 53))mx$esc[0m" $script:hostCalls[2].Object | Should -Match 'x' (Get-ColorTableWithRGB)['Brand'][4] | Should -Be @(255, 107, 53) } It 'Reads <Form> as the color' -TestCases @( @{ Form = '#F63' } @{ Form = 'rgb(255, 102, 51)' } @{ Form = 'hsl(15, 100%, 60%)' } ) { Register-ColorName -Name Mine -Color $Form (Get-ColorTableWithRGB)['Mine'][4] | Should -Be @(255, 102, 51) } It 'Takes an RGB array, and copies a name already in the table' { Register-ColorName -Name One -Color @(1, 2, 3) Register-ColorName -Name Two -Color Orange (Get-ColorTableWithRGB)['One'][4] | Should -Be @(1, 2, 3) (Get-ColorTableWithRGB)['Two'][4] | Should -Be (Get-ColorTableWithRGB)['Orange'][4] } It 'Refuses a name in use without -Force' { { Register-ColorName -Name Red -Color '#000000' -ErrorAction Stop } | Should -Throw "The color name 'Red' is in use. Use -Force to replace it." } It 'Replaces a built-in name with -Force, and Unregister-ColorName brings it back' { Register-ColorName -Name Red -Color '#123456' -Force (Get-ColorTableWithRGB)['Red'][4] | Should -Be @(0x12, 0x34, 0x56) Unregister-ColorName -Name Red (Get-ColorTableWithRGB)['Red'][4] | Should -Be @(255, 0, 0) } It 'Refuses <Name> as a name' -TestCases @( @{ Name = 'bold' } @{ Name = 'on' } @{ Name = 'None' } @{ Name = '9lives' } @{ Name = 'two words' } @{ Name = '#abc' } ) { { Register-ColorName -Name $Name -Color '#000000' -ErrorAction Stop } | Should -Throw "'$Name' cannot be a color name.*" } It 'Refuses a value that is not a color' { { Register-ColorName -Name Mine -Color 'nope' -ErrorAction Stop } | Should -Throw "'nope' is not a color.*" { Register-ColorName -Name Mine -Color @(1, 2, 300) -ErrorAction Stop } | Should -Throw "'@(1, 2, 300)' is not a color.*" } It 'Says when a name to remove was not registered' { { Unregister-ColorName -Name Nothing -ErrorAction Stop } | Should -Throw "No color name 'Nothing' was registered." } } Context 'Show-ColorTable' { It 'Writes a heading and a line for each name, by family' { Show-ColorTable -Name *Lime $lines = @($script:hostCalls | Where-Object { -not $_.NoNewline } | Select-Object -ExpandProperty Object) $lines[0] | Should -Match 'Name +TrueColor +256 +16 +Console +Hex +ANSI8 +Console color' $names = @($script:hostCalls | Where-Object { $_.Object -match '^\w+ +$' } | ForEach-Object { $_.Object.Trim() }) $names | Should -Be @('DarkLime', 'Lime', 'LightLime') } It 'Writes each sample in its own color mode' { Show-ColorTable -Name Red $objects = @($script:hostCalls.Object) $objects | Should -Contain "$esc[38;2;255;0;0m$([string][char]0x2588 * 6)$esc[0m" $objects | Should -Contain "$esc[38;5;1m$([string][char]0x2588 * 6)$esc[0m" $objects | Should -Contain "$esc[31m$([string][char]0x2588 * 6)$esc[0m" } It 'Writes the samples as backgrounds with -Background' { Show-ColorTable -Name Red -Background @($script:hostCalls.Object) | Should -Contain "$esc[48;2;255;0;0m $esc[0m" } It 'Writes nothing when no name matches' { Show-ColorTable -Name NoSuchColor* $script:hostCalls | Should -HaveCount 0 } } Context 'Export-ColorProfile, Import-ColorProfile and Remove-ColorProfile' { It 'Writes the profiles and color names as JSON' { $path = Join-Path $TestDrive 'profiles.json' Register-ColorName -Name Brand -Color '#FF6B35' New-ColorStyle -Name 'Build' -ForegroundColor Brand -BackgroundColor @(0, 0, 64) -Bold -UnderlineStyle Curly -AutoPad 20 -PadChar '.' -AddToProfiles | Out-Null Export-ColorProfile -Path $path -Name 'Build' $expected = "{`n `"Colors`": {`n `"Brand`": `"#FF6B35`"`n },`n `"Profiles`": [`n {`n `"Name`": `"Build`",`n `"ForegroundColor`": `"Brand`",`n `"BackgroundColor`": [0, 0, 64],`n `"Bold`": true,`n `"UnderlineStyle`": `"Curly`",`n `"AutoPad`": 20,`n `"PadChar`": `".`"`n }`n ]`n}`n" [System.IO.File]::ReadAllText($path) | Should -BeExactly $expected } It 'Reads back what it wrote, color names first' { $path = Join-Path $TestDrive 'roundtrip.json' Register-ColorName -Name Brand -Color '#FF6B35' New-ColorStyle -Name 'Build' -ForegroundColor Brand -Gradient Red, '#00FF00' -Italic -Wrap -UnderlineColor @(1, 2, 3) -AddToProfiles | Out-Null Export-ColorProfile -Path $path Reset-ColorState $imported = @(Import-ColorProfile -Path $path -PassThru) ($imported | Where-Object Name -eq 'Build') | Should -Not -BeNullOrEmpty $style = [PSColorStyle]::Profiles['Build'] $style.ForegroundColor | Should -Be 'Brand' $style.Gradient | Should -Be @('Red', '#00FF00') $style.Italic | Should -BeTrue $style.Wrap | Should -BeTrue $style.UnderlineColor | Should -Be @(1, 2, 3) $style.UnderlineColor[0] | Should -BeOfType [int] (Get-ColorTableWithRGB)['Brand'][4] | Should -Be @(255, 107, 53) } It 'Makes the style the file names the default' { $path = Join-Path $TestDrive 'default.json' $style = New-ColorStyle -Name 'Mine' -ForegroundColor Cyan -AddToProfiles -SetAsDefault Export-ColorProfile -Path $path -Name 'Mine' Reset-ColorState Import-ColorProfile -Path $path [PSColorStyle]::Default.Name | Should -Be 'Mine' } It 'Stops with an error for a file that is not JSON' { $path = Join-Path $TestDrive 'bad.json' Set-Content -LiteralPath $path -Value 'not json' { Import-ColorProfile -Path $path } | Should -Throw "Cannot read the profiles in '$path'.*" } It 'Removes a profile, and with -WhatIf leaves it' { New-ColorStyle -Name 'Gone' -AddToProfiles | Out-Null Remove-ColorProfile -Name 'Gone' -WhatIf [PSColorStyle]::Profiles.ContainsKey('Gone') | Should -BeTrue Remove-ColorProfile -Name 'Gone' [PSColorStyle]::Profiles.ContainsKey('Gone') | Should -BeFalse } It 'Says when a profile to remove does not exist' { { Remove-ColorProfile -Name 'Nothing' -ErrorAction Stop } | Should -Throw "No profile named 'Nothing'." } } Context 'Tab completion' { It 'Completes color names for -Color, registered names among them' { Register-ColorName -Name Brandy -Color '#AA5500' $completions = (TabExpansion2 -inputScript 'Write-ColorEX -Color Bra' -cursorColumn 24).CompletionMatches @($completions.CompletionText) | Should -Be @('Brandy') } It 'Completes the next color of a list, in family order' { $completions = (TabExpansion2 -inputScript 'Write-ColorEX -Color Red, DarkG' -cursorColumn 31).CompletionMatches @($completions.CompletionText) | Should -Be @('DarkGold', 'DarkGray', 'DarkGreen') } It 'Completes profile names for Remove-ColorProfile' { $completions = (TabExpansion2 -inputScript 'Remove-ColorProfile Er' -cursorColumn 22).CompletionMatches @($completions.CompletionText) | Should -Be @('Error') } } Context 'PSColorStyle background gradient, underline and layout properties' { It 'Hands each property to Write-ColorEX' { $style = New-ColorStyle -Name 'All' -BackgroundGradient Red, Blue -GradientSpace RGB -Reverse -UnderlineColor Red -UnderlineStyle Dotted -AutoPad 9 -PadCenter -Truncate -Wrap $params = $style.ToWriteColorParams() $params['BackGroundGradient'] | Should -Be @('Red', 'Blue') $params.ContainsKey('BackGroundColor') | Should -BeFalse $params['GradientSpace'] | Should -Be 'RGB' $params['Reverse'] | Should -BeTrue $params['UnderlineColor'] | Should -Be 'Red' $params['UnderlineStyle'] | Should -Be 'Dotted' $params['PadCenter'] | Should -BeTrue $params['Truncate'] | Should -BeTrue $params['Wrap'] | Should -BeTrue } It 'Copies each property with Clone' { $style = New-ColorStyle -Name 'All' -BackgroundGradient Red, Blue -GradientSpace RGB -Reverse -UnderlineColor @(1, 2, 3) -UnderlineStyle Dotted -PadCenter -Truncate -Wrap $copy = $style.Clone() $copy.UnderlineColor[0] = 9 $copy.BackgroundGradient | Should -Be @('Red', 'Blue') $copy.GradientSpace | Should -Be 'RGB' $copy.Reverse | Should -BeTrue $copy.UnderlineStyle | Should -Be 'Dotted' $copy.PadCenter | Should -BeTrue $copy.Truncate | Should -BeTrue $copy.Wrap | Should -BeTrue $style.UnderlineColor[0] | Should -Be 1 } It 'Writes with a profile''s new properties' { $style = New-ColorStyle -Name 'Centered' -ForegroundColor Red -AutoPad 7 -PadCenter -Reverse Write-ColorEX -Text 'abc' -StyleProfile $style $script:hostCalls[0].Object | Should -Be "$esc[7m$esc[91m $esc[0m$esc[7m$esc[91mabc$esc[0m$esc[7m$esc[91m $esc[0m" } } } |