Tests/Tests-PSColorStyle.ps1
|
#Requires -Modules Pester BeforeAll { # Importing the module makes [PSColorStyle] available $ModuleRoot = Split-Path -Parent $PSScriptRoot Import-Module "$ModuleRoot\PSWriteColorEX.psd1" -Force } Describe 'PSColorStyle Class' -Tag 'Unit', 'Class' { Context 'Constructors' { It 'Creates instance with default constructor' { $style = [PSColorStyle]::new() $style | Should -Not -BeNullOrEmpty $style.Name | Should -Be 'Custom' $style.ForegroundColor | Should -Be 'Gray' $style.BackgroundColor | Should -BeNullOrEmpty } It 'Creates instance with name only' { $style = [PSColorStyle]::new('TestStyle') $style.Name | Should -Be 'TestStyle' $style.ForegroundColor | Should -Be 'Gray' $style.BackgroundColor | Should -BeNullOrEmpty } It 'Creates instance with name, foreground, and background' { $style = [PSColorStyle]::new('TestStyle', 'Red', 'Blue') $style.Name | Should -Be 'TestStyle' $style.ForegroundColor | Should -Be 'Red' $style.BackgroundColor | Should -Be 'Blue' } It 'Accepts null background color' { $style = [PSColorStyle]::new('TestStyle', 'Red', $null) $style.Name | Should -Be 'TestStyle' $style.ForegroundColor | Should -Be 'Red' $style.BackgroundColor | Should -BeNullOrEmpty } } Context 'Property Initialization' { BeforeAll { $script:style = [PSColorStyle]::new('TestStyle', 'Red', 'Blue') } It 'Initializes boolean properties to false' { $style.Bold | Should -Be $false $style.Italic | Should -Be $false $style.Underline | Should -Be $false $style.Blink | Should -Be $false $style.Faint | Should -Be $false $style.CrossedOut | Should -Be $false $style.DoubleUnderline | Should -Be $false $style.Overline | Should -Be $false $style.ShowTime | Should -Be $false $style.NoNewLine | Should -Be $false $style.HorizontalCenter | Should -Be $false } It 'Initializes numeric properties to zero' { $style.StartTab | Should -Be 0 $style.StartSpaces | Should -Be 0 $style.LinesBefore | Should -Be 0 $style.LinesAfter | Should -Be 0 } It 'Initializes Style as empty or null' { # Style can be null or empty array if ($style.Style) { $style.Style.Count | Should -Be 0 } else { $style.Style | Should -BeNullOrEmpty } } It 'Initializes Gradient as null' { $style.Gradient | Should -BeNullOrEmpty } } Context 'Property Modification' { BeforeEach { $script:style = [PSColorStyle]::new('TestStyle', 'Red', $null) } It 'Allows setting Bold to true' { $style.Bold = $true $style.Bold | Should -Be $true } It 'Allows setting multiple style properties' { $style.Bold = $true $style.Italic = $true $style.Underline = $true $style.Bold | Should -Be $true $style.Italic | Should -Be $true $style.Underline | Should -Be $true } It 'Allows setting numeric properties' { $style.StartTab = 2 $style.StartSpaces = 4 $style.LinesBefore = 1 $style.LinesAfter = 1 $style.StartTab | Should -Be 2 $style.StartSpaces | Should -Be 4 $style.LinesBefore | Should -Be 1 $style.LinesAfter | Should -Be 1 } It 'Allows setting Gradient array' { $style.Gradient = @('Red', 'Blue', 'Green') $style.Gradient | Should -Not -BeNullOrEmpty $style.Gradient.Count | Should -Be 3 $style.Gradient[0] | Should -Be 'Red' } It 'Allows setting Style array' { $style.Style = @('Bold', 'Italic') $style.Style.Count | Should -Be 2 $style.Style[0] | Should -Be 'Bold' $style.Style[1] | Should -Be 'Italic' } } Context 'Static Default Property' { It 'Has a Default static property' { [PSColorStyle]::Default | Should -Not -BeNullOrEmpty } It 'Default property is of type PSColorStyle' { [PSColorStyle]::Default.GetType().Name | Should -Be 'PSColorStyle' } } Context 'Static Profiles Property' { It 'Has a Profiles static property' { [PSColorStyle]::Profiles | Should -Not -BeNullOrEmpty } It 'Profiles is a hashtable' { [PSColorStyle]::Profiles | Should -BeOfType [hashtable] } It 'Profiles contains default profiles' { [PSColorStyle]::Profiles.ContainsKey('Default') | Should -Be $true [PSColorStyle]::Profiles.ContainsKey('Error') | Should -Be $true [PSColorStyle]::Profiles.ContainsKey('Warning') | Should -Be $true [PSColorStyle]::Profiles.ContainsKey('Info') | Should -Be $true [PSColorStyle]::Profiles.ContainsKey('Success') | Should -Be $true [PSColorStyle]::Profiles.ContainsKey('Critical') | Should -Be $true [PSColorStyle]::Profiles.ContainsKey('Debug') | Should -Be $true } } Context 'SetAsDefault Method' { It 'Sets the style as default' { $originalDefault = [PSColorStyle]::Default $newStyle = [PSColorStyle]::new('NewDefault', 'Magenta', $null) $newStyle.SetAsDefault() [PSColorStyle]::Default.Name | Should -Be 'NewDefault' [PSColorStyle]::Default.ForegroundColor | Should -Be 'Magenta' # Restore original default $originalDefault.SetAsDefault() } } Context 'AddToProfiles Method' { It 'Adds style to profiles collection' { $style = [PSColorStyle]::new('CustomProfile', 'Orange', $null) $style.AddToProfiles() [PSColorStyle]::Profiles.ContainsKey('CustomProfile') | Should -Be $true [PSColorStyle]::Profiles['CustomProfile'].Name | Should -Be 'CustomProfile' } It 'Overwrites existing profile with same name' { $style1 = [PSColorStyle]::new('TestProfile', 'Red', $null) $style1.AddToProfiles() $style2 = [PSColorStyle]::new('TestProfile', 'Blue', $null) $style2.AddToProfiles() [PSColorStyle]::Profiles['TestProfile'].ForegroundColor | Should -Be 'Blue' } } Context 'GetProfile Static Method' { BeforeAll { $testStyle = [PSColorStyle]::new('GetProfileTest', 'Cyan', $null) $testStyle.AddToProfiles() } It 'Retrieves existing profile by name' { $retrieved = [PSColorStyle]::GetProfile('GetProfileTest') $retrieved | Should -Not -BeNullOrEmpty $retrieved.Name | Should -Be 'GetProfileTest' $retrieved.ForegroundColor | Should -Be 'Cyan' } It 'Returns null for non-existent profile' { $retrieved = [PSColorStyle]::GetProfile('NonExistentProfile') $retrieved | Should -BeNullOrEmpty } It 'Retrieves built-in Error profile' { $errorProfile = [PSColorStyle]::GetProfile('Error') $errorProfile | Should -Not -BeNullOrEmpty $errorProfile.Name | Should -Be 'Error' $errorProfile.ForegroundColor | Should -Be 'Red' $errorProfile.Bold | Should -Be $true } } Context 'ToWriteColorParams Method' { It 'Returns hashtable with color properties' { $style = [PSColorStyle]::new('TestStyle', 'Red', 'Blue') $params = $style.ToWriteColorParams() $params | Should -BeOfType [hashtable] $params['Color'] | Should -Be 'Red' $params['BackGroundColor'] | Should -Be 'Blue' } It 'Includes Bold when set to true' { $style = [PSColorStyle]::new('TestStyle', 'Red', $null) $style.Bold = $true $params = $style.ToWriteColorParams() $params.ContainsKey('Bold') | Should -Be $true $params['Bold'] | Should -Be $true } It 'Excludes Bold when set to false' { $style = [PSColorStyle]::new('TestStyle', 'Red', $null) $style.Bold = $false $params = $style.ToWriteColorParams() $params.ContainsKey('Bold') | Should -Be $false } It 'Includes multiple style properties' { $style = [PSColorStyle]::new('TestStyle', 'Red', $null) $style.Bold = $true $style.Italic = $true $style.Underline = $true $params = $style.ToWriteColorParams() $params['Bold'] | Should -Be $true $params['Italic'] | Should -Be $true $params['Underline'] | Should -Be $true } It 'Includes formatting properties when set' { $style = [PSColorStyle]::new('TestStyle', 'Red', $null) $style.StartTab = 2 $style.LinesBefore = 1 $style.LinesAfter = 1 $params = $style.ToWriteColorParams() $params['StartTab'] | Should -Be 2 $params['LinesBefore'] | Should -Be 1 $params['LinesAfter'] | Should -Be 1 } It 'Excludes formatting properties when zero' { $style = [PSColorStyle]::new('TestStyle', 'Red', $null) $params = $style.ToWriteColorParams() $params.ContainsKey('StartTab') | Should -Be $false $params.ContainsKey('LinesBefore') | Should -Be $false $params.ContainsKey('LinesAfter') | Should -Be $false } It 'Includes Gradient when set' { $style = [PSColorStyle]::new('TestStyle', 'Red', $null) $style.Gradient = @('Red', 'Blue') $params = $style.ToWriteColorParams() $params.ContainsKey('Gradient') | Should -Be $true $params['Gradient'].Count | Should -Be 2 } It 'Excludes Gradient when less than 2 colors' { $style = [PSColorStyle]::new('TestStyle', 'Red', $null) $style.Gradient = @('Red') $params = $style.ToWriteColorParams() $params.ContainsKey('Gradient') | Should -Be $false } It 'Caches parameters for performance' { $style = [PSColorStyle]::new('TestStyle', 'Red', $null) $style.Bold = $true $params1 = $style.ToWriteColorParams() $params2 = $style.ToWriteColorParams() # Both should have the same values but be different instances (cloned) $params1['Bold'] | Should -Be $params2['Bold'] $params1 -eq $params2 | Should -Be $false } It 'Returns clone to prevent external modification' { $style = [PSColorStyle]::new('TestStyle', 'Red', $null) $params = $style.ToWriteColorParams() # Modify returned params $params['Color'] = 'Blue' # Original should still be Red $params2 = $style.ToWriteColorParams() $params2['Color'] | Should -Be 'Red' } It 'Lets a subclass''s own ToWriteColorParams decide what -StyleProfile applies' { $esc = [string][char]27 # The class is defined at run time, once the module has made PSColorStyle $style = . ([scriptblock]::Create(@' class ColorStyleSubclassProbe : PSColorStyle { ColorStyleSubclassProbe() : base('Probe', 'Red', $null) { } [hashtable] ToWriteColorParams() { return @{ Color = 'Blue' } } } [ColorStyleSubclassProbe]::new() '@)) Format-ColorEX -Text 'ab' -StyleProfile $style -ColorSupport ANSI4 | Should -Be "$esc[94mab$esc[0m" } } Context 'Clone Method' { It 'Creates a copy of the style' { $original = [PSColorStyle]::new('Original', 'Red', 'Blue') $original.Bold = $true $original.StartTab = 2 $clone = $original.Clone() $clone.Name | Should -Be 'Original_Copy' $clone.ForegroundColor | Should -Be 'Red' $clone.BackgroundColor | Should -Be 'Blue' $clone.Bold | Should -Be $true $clone.StartTab | Should -Be 2 } It 'Creates independent copy that can be modified' { $original = [PSColorStyle]::new('Original', 'Red', $null) $clone = $original.Clone() $clone.ForegroundColor = 'Blue' $original.ForegroundColor | Should -Be 'Red' $clone.ForegroundColor | Should -Be 'Blue' } It 'Clone does not share cached parameters' { $original = [PSColorStyle]::new('Original', 'Red', $null) $original.Bold = $true # Warm cache $null = $original.ToWriteColorParams() $clone = $original.Clone() # Clone should have null cache initially $clone.Bold = $false $params = $clone.ToWriteColorParams() $params.ContainsKey('Bold') | Should -Be $false } It 'Copies all style properties' { $original = [PSColorStyle]::new('Original', 'Red', 'Blue') $original.Bold = $true $original.Italic = $true $original.Underline = $true $original.Blink = $true $original.Faint = $true $original.CrossedOut = $true $original.DoubleUnderline = $true $original.Overline = $true $clone = $original.Clone() $clone.Bold | Should -Be $true $clone.Italic | Should -Be $true $clone.Underline | Should -Be $true $clone.Blink | Should -Be $true $clone.Faint | Should -Be $true $clone.CrossedOut | Should -Be $true $clone.DoubleUnderline | Should -Be $true $clone.Overline | Should -Be $true } It 'Copies Gradient array' { $original = [PSColorStyle]::new('Original', 'Red', $null) $original.Gradient = @('Red', 'Blue', 'Green') $clone = $original.Clone() $clone.Gradient.Count | Should -Be 3 $clone.Gradient[0] | Should -Be 'Red' $clone.Gradient[1] | Should -Be 'Blue' $clone.Gradient[2] | Should -Be 'Green' } } Context 'InitializeDefaultProfiles Static Method' { It 'Initializes all default profiles' { # This is called during module load, so profiles should exist [PSColorStyle]::Profiles.Count | Should -BeGreaterOrEqual 7 } It 'Error profile has correct properties' { $errorProfile = [PSColorStyle]::GetProfile('Error') $errorProfile.Name | Should -Be 'Error' $errorProfile.ForegroundColor | Should -Be 'Red' $errorProfile.Bold | Should -Be $true } It 'Warning profile has correct properties' { $warningProfile = [PSColorStyle]::GetProfile('Warning') $warningProfile.Name | Should -Be 'Warning' $warningProfile.ForegroundColor | Should -Be 'Yellow' } It 'Info profile has correct properties' { $infoProfile = [PSColorStyle]::GetProfile('Info') $infoProfile.Name | Should -Be 'Info' $infoProfile.ForegroundColor | Should -Be 'Cyan' } It 'Success profile has correct properties' { $successProfile = [PSColorStyle]::GetProfile('Success') $successProfile.Name | Should -Be 'Success' $successProfile.ForegroundColor | Should -Be 'Green' } It 'Critical profile has correct properties' { $criticalProfile = [PSColorStyle]::GetProfile('Critical') $criticalProfile.Name | Should -Be 'Critical' $criticalProfile.ForegroundColor | Should -Be 'White' $criticalProfile.BackgroundColor | Should -Be 'DarkRed' $criticalProfile.Bold | Should -Be $true $criticalProfile.Blink | Should -Be $true } It 'Debug profile has correct properties' { $debugProfile = [PSColorStyle]::GetProfile('Debug') $debugProfile.Name | Should -Be 'Debug' $debugProfile.ForegroundColor | Should -Be 'DarkGray' $debugProfile.Italic | Should -Be $true } It 'Default profile returns its parameters' { $defaultProfile = [PSColorStyle]::GetProfile('Default') $params = $defaultProfile.ToWriteColorParams() $params | Should -Not -BeNullOrEmpty } } Context 'Parameters follow property changes' { It 'ToWriteColorParams reflects a change to Bold' { $style = [PSColorStyle]::new('Test', 'Red', $null) $style.ToWriteColorParams().ContainsKey('Bold') | Should -Be $false $style.Bold = $true $style.ToWriteColorParams().Bold | Should -Be $true } It 'ToWriteColorParams reflects a change to ForegroundColor' { $style = [PSColorStyle]::new('Test', 'Red', $null) $style.ToWriteColorParams().Color | Should -Be 'Red' $style.ForegroundColor = 'Blue' $style.ToWriteColorParams().Color | Should -Be 'Blue' } It 'ToWriteColorParams reflects a change to BackgroundColor' { $style = [PSColorStyle]::new('Test', 'Red', 'Yellow') $style.ToWriteColorParams().BackGroundColor | Should -Be 'Yellow' $style.BackgroundColor = 'Green' $style.ToWriteColorParams().BackGroundColor | Should -Be 'Green' } It 'ToWriteColorParams reflects changes to several properties' { $style = [PSColorStyle]::new('Test', 'Red', $null) $null = $style.ToWriteColorParams() $style.Bold = $true $style.Italic = $true $style.Underline = $true $style.StartTab = 2 $params = $style.ToWriteColorParams() $params.Bold | Should -Be $true $params.Italic | Should -Be $true $params.Underline | Should -Be $true $params.StartTab | Should -Be 2 } It 'Each call returns a new hashtable' { $style = [PSColorStyle]::new('Test', 'Red', $null) $first = $style.ToWriteColorParams() $first['Color'] = 'Green' $style.ToWriteColorParams().Color | Should -Be 'Red' } It 'InvalidateCache can still be called' { $style = [PSColorStyle]::new('Test', 'Red', $null) { $style.InvalidateCache() } | Should -Not -Throw $style.ToWriteColorParams().Color | Should -Be 'Red' } It 'A change to a built-in profile reaches its helper' { $errorProfile = [PSColorStyle]::GetProfile('Error') $saved = $errorProfile.ForegroundColor try { $errorProfile.ForegroundColor = 'Magenta' [PSColorStyle]::GetProfile('Error').ToWriteColorParams().Color | Should -Be 'Magenta' } finally { $errorProfile.ForegroundColor = $saved } } It 'SetAsDefault keeps the parameters' { $originalDefault = [PSColorStyle]::Default $style = [PSColorStyle]::new('Test', 'Cyan', $null) $style.Bold = $true try { $style.SetAsDefault() $params = $style.ToWriteColorParams() $params | Should -Not -BeNullOrEmpty $params.Bold | Should -Be $true } finally { $originalDefault.SetAsDefault() } } It 'AddToProfiles keeps the parameters' { $style = [PSColorStyle]::new('ParamsTest', 'Magenta', $null) $style.Italic = $true $style.AddToProfiles() $params = $style.ToWriteColorParams() $params | Should -Not -BeNullOrEmpty $params.Italic | Should -Be $true [PSColorStyle]::Profiles.Remove('ParamsTest') } It 'A clone changes apart from the original' { $original = [PSColorStyle]::new('Original', 'Red', $null) $original.Bold = $true $clone = $original.Clone() $clone.Bold = $false $original.ToWriteColorParams().Bold | Should -Be $true # Bold = $false leaves the key out $clone.ToWriteColorParams().ContainsKey('Bold') | Should -Be $false } It 'A clone has its own arrays' { $original = [PSColorStyle]::new('Original', @(255, 0, 0), $null) $original.Gradient = @('Red', 'Blue') $original.Style = @('Bold', 'Italic') $clone = $original.Clone() $clone.Gradient[0] = 'Green' $clone.Style[0] = 'Underline' $clone.ForegroundColor[0] = 0 $original.Gradient[0] | Should -Be 'Red' $original.Style[0] | Should -Be 'Bold' $original.ForegroundColor[0] | Should -Be 255 } } } Describe 'PSColorStyle across runspaces' -Tag 'Unit', 'Class' { BeforeAll { $script:manifest = Join-Path $ModuleRoot 'PSWriteColorEX.psd1' $script:esc = [string][char]27 # Imports the module in a runspace of its own, runs the script given there and disposes the # runspace, returning what the script wrote function Invoke-InOtherRunspace { param([string]$Script) $shell = [powershell]::Create() try { $null = $shell.AddScript("Import-Module '$script:manifest'; $Script") $shell.Invoke() } finally { $shell.Dispose() } } } It 'Keeps the profiles added since and puts back the built-in ones when the module is imported again' { $null = New-ColorStyle -Name 'Kept' -ForegroundColor Magenta -AddToProfiles [PSColorStyle]::Profiles['Info'].ForegroundColor = 'DarkRed' try { Import-Module $script:manifest -Force [PSColorStyle]::Profiles.ContainsKey('Kept') | Should -BeTrue [PSColorStyle]::Profiles['Info'].ForegroundColor | Should -Be 'Cyan' } finally { [PSColorStyle]::Profiles.Remove('Kept') } } It 'Keeps working after another runspace that imported the module is disposed' { $null = Invoke-InOtherRunspace "(Get-GradientColor -Gradient Progress -Place 0.5) -join ','" (Get-GradientColor -Gradient Progress -Place 0.5) -join ',' | Should -Be '255,165,0' Format-ColorEX -Text 'ab' -Color Red -ColorSupport ANSI8 | Should -Be "$esc[91mab$esc[0m" [PSColorStyle]::GetProfile('Info').Name | Should -Be 'Info' [PSColorStyle]::Profiles['Error'].ToWriteColorParams().Color | Should -Be 'Red' [PSColorStyle]::Profiles['Error'].Clone().Name | Should -Be 'Error_Copy' [PSColorStyle]::IsGradient(@('Sunset')) | Should -BeTrue } It 'Keeps the methods of a style made in a runspace that is then disposed' { $style = Invoke-InOtherRunspace "`$made = [PSColorStyle]::new('Made', 'Red', `$null); `$made.Bold = `$true; `$made" $style.ToWriteColorParams().Bold | Should -BeTrue $style.Clone().Name | Should -Be 'Made_Copy' Format-ColorEX -Text 'ab' -StyleProfile $style -ColorSupport ANSI4 | Should -Be "$esc[1m$esc[91mab$esc[0m" } It 'Lets eight runspaces of a new process import the module at the same moment, all with one PSColorStyle' { # A new process, as the module's first import in a process is the one that compiles it $harness = Join-Path $TestDrive 'concurrent-import.ps1' $output = Join-Path $TestDrive 'concurrent-import.txt' Set-Content -LiteralPath $harness -Value @' param($Manifest) # All eight runspaces open first, and the imports start once every one is waiting to begin $pool = [runspacefactory]::CreateRunspacePool(8, 8) $pool.Open() $ready = [System.Threading.CountdownEvent]::new(8) $gate = [System.Threading.ManualResetEventSlim]::new($false) $jobs = foreach ($n in 1..8) { $shell = [powershell]::Create() $shell.RunspacePool = $pool $null = $shell.AddScript({ param($Manifest, $Ready, $Gate) $Ready.Signal() $null = $Gate.Wait(30000) Import-Module $Manifest [System.Runtime.CompilerServices.RuntimeHelpers]::GetHashCode([type]'PSColorStyle') }).AddArgument($Manifest).AddArgument($ready).AddArgument($gate) [pscustomobject]@{ Shell = $shell; Handle = $shell.BeginInvoke() } } $null = $ready.Wait(30000) $gate.Set() foreach ($job in $jobs) { $job.Shell.EndInvoke($job.Handle) } '@ $process = Start-Process -FilePath (Get-Process -Id $PID).Path -NoNewWindow -PassThru -RedirectStandardOutput $output ` -ArgumentList '-NoProfile', '-NonInteractive', '-File', "`"$harness`"", "`"$script:manifest`"" $finished = $process.WaitForExit(90000) if (-not $finished) { $process.Kill() } $finished | Should -BeTrue $types = @(Get-Content -LiteralPath $output | Where-Object { $_ -match '^\d+$' }) $types | Should -HaveCount 8 @($types | Sort-Object -Unique) | Should -HaveCount 1 } It 'Says the module is not imported when a runspace without it calls a method' { $shell = [powershell]::Create() try { $null = $shell.AddScript("try { [PSColorStyle]::GetProfile('Info') } catch { `$_.Exception.Message }") $message = $shell.Invoke() } finally { $shell.Dispose() } "$message" | Should -Match 'PSWriteColorEX is not imported in this runspace' } It 'Has the members the class of version 1.2.0 had' { $object = [PSColorStyle]::new() ($object | Get-Member -MemberType Property) | Should -HaveCount 36 ($object | Get-Member -MemberType Method | Where-Object Name -NotIn 'Equals', 'GetHashCode', 'GetType', 'ToString').Definition | Should -Be @('void AddToProfiles()', 'PSColorStyle Clone()', 'void SetAsDefault()', 'hashtable ToWriteColorParams()') ([PSColorStyle] | Get-Member -Static -MemberType Property).Name | Should -Be @('Default', 'Profiles') ([PSColorStyle] | Get-Member -Static -MemberType Method | Where-Object Name -NotIn 'Equals', 'ReferenceEquals', 'new').Definition | Should -Be @('static PSColorStyle GetProfile(string name)', 'static void InitializeDefaultProfiles()') ([PSColorStyle] | Get-Member -Static -Name new).Definition | Should -Be 'PSColorStyle new(), PSColorStyle new(string name), PSColorStyle new(string name, System.Object foreground, System.Object background)' } } |