Tests/Invoke-ElmSubscriptions.Tests.ps1
|
BeforeAll { . $PSScriptRoot/../Private/Subscriptions/ConvertFrom-ElmKeyString.ps1 . $PSScriptRoot/../Public/Subscriptions/New-ElmKeySub.ps1 . $PSScriptRoot/../Public/Subscriptions/New-ElmTimerSub.ps1 . $PSScriptRoot/../Private/Subscriptions/Invoke-ElmSubscriptions.ps1 } Describe 'Invoke-ElmSubscriptions' -Tag 'Unit', 'P6' { Context 'Empty subscriptions — pass-through mode' { It 'Should return an empty array when queue is empty and no subs' { $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @() -InputQueue $queue -TimerState $state) $result.Count | Should -Be 0 } It 'Should forward raw KeyDown events when no key subs are defined' { $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::Q; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @() -InputQueue $queue -TimerState $state) $result.Count | Should -Be 1 $result[0].Type | Should -Be 'KeyDown' $result[0].Key | Should -Be ([System.ConsoleKey]::Q) } It 'Should forward multiple raw KeyDown events in order' { $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::Q; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::W; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @() -InputQueue $queue -TimerState $state) $result.Count | Should -Be 2 $result[0].Key | Should -Be ([System.ConsoleKey]::Q) $result[1].Key | Should -Be ([System.ConsoleKey]::W) } It 'Should accept null Subscriptions in pass-through mode' { $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::A; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions $null -InputQueue $queue -TimerState $state) $result.Count | Should -Be 1 } } Context 'Key subscriptions — matching' { It 'Should invoke handler and return message for matching key' { $sub = New-ElmKeySub -Key 'Q' -Handler { 'Quit' } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::Q; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @($sub) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 1 $result[0] | Should -Be 'Quit' } It 'Should drop non-matching key events when key subs are defined' { $sub = New-ElmKeySub -Key 'Q' -Handler { 'Quit' } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::W; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @($sub) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 0 } It 'Should match multiple different key subs in one pass' { $subQ = New-ElmKeySub -Key 'Q' -Handler { 'Quit' } $subW = New-ElmKeySub -Key 'W' -Handler { 'Up' } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::Q; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::W; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @($subQ, $subW) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 2 $result[0] | Should -Be 'Quit' $result[1] | Should -Be 'Up' } It 'Should suppress null handler results' { $sub = New-ElmKeySub -Key 'Q' -Handler { $null } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::Q; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @($sub) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 0 } } Context 'Key subscriptions — case-insensitive letter matching' { It 'Should match lowercase key event for a sub with no modifiers' { $sub = New-ElmKeySub -Key 'Q' -Handler { 'Quit' } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::Q; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @($sub) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 1 } It 'Should match uppercase (Shift+letter) key event for a sub with no modifiers' { $sub = New-ElmKeySub -Key 'Q' -Handler { 'Quit' } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::Q; Char = [char]0; Modifiers = [System.ConsoleModifiers]::Shift }) $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @($sub) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 1 } It 'Should NOT strip Shift when sub explicitly requests Shift modifier' { $sub = New-ElmKeySub -Key 'Shift+Q' -Handler { 'ShiftQ' } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::Q; Char = [char]0; Modifiers = [System.ConsoleModifiers]::Control }) $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @($sub) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 0 } } Context 'Key subscriptions — Ctrl modifier matching' { It 'Should match Ctrl+Q exactly' { $sub = New-ElmKeySub -Key 'Ctrl+Q' -Handler { 'CtrlQuit' } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::Q; Char = [char]0; Modifiers = [System.ConsoleModifiers]::Control }) $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @($sub) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 1 $result[0] | Should -Be 'CtrlQuit' } It 'Should NOT match plain Q when sub requires Ctrl+Q' { $sub = New-ElmKeySub -Key 'Ctrl+Q' -Handler { 'CtrlQuit' } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::Q; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @($sub) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 0 } } Context 'Timer subscriptions' { It 'Should NOT fire timer when interval has not elapsed' { $sub = New-ElmTimerSub -IntervalMs 60000 -Handler { 'Tick' } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @($sub) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 0 } It 'Should fire timer when state shows interval elapsed' { $sub = New-ElmTimerSub -IntervalMs 1000 -Handler { 'Tick' } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $state = @{ 'Timer:1000' = ([System.Environment]::TickCount - 2000) } $result = @(Invoke-ElmSubscriptions -Subscriptions @($sub) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 1 $result[0] | Should -Be 'Tick' } It 'Should update TimerState after firing' { $sub = New-ElmTimerSub -IntervalMs 1000 -Handler { 'Tick' } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $before = [System.Environment]::TickCount $state = @{ 'Timer:1000' = ($before - 2000) } $null = @(Invoke-ElmSubscriptions -Subscriptions @($sub) -InputQueue $queue -TimerState $state) $state['Timer:1000'] | Should -BeGreaterOrEqual $before } It 'Should NOT fire timer twice in the same call' { $sub = New-ElmTimerSub -IntervalMs 1000 -Handler { 'Tick' } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $state = @{ 'Timer:1000' = ([System.Environment]::TickCount - 2000) } $result = @(Invoke-ElmSubscriptions -Subscriptions @($sub) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 1 } It 'Should suppress null timer handler results' { $sub = New-ElmTimerSub -IntervalMs 1000 -Handler { $null } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $state = @{ 'Timer:1000' = ([System.Environment]::TickCount - 2000) } $result = @(Invoke-ElmSubscriptions -Subscriptions @($sub) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 0 } } Context 'Mixed key and timer subscriptions' { It 'Should return both a timer message and a key message in the same call' { $timerSub = New-ElmTimerSub -IntervalMs 1000 -Handler { 'Tick' } $keySub = New-ElmKeySub -Key 'Q' -Handler { 'Quit' } $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::Q; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) $state = @{ 'Timer:1000' = ([System.Environment]::TickCount - 2000) } $result = @(Invoke-ElmSubscriptions -Subscriptions @($timerSub, $keySub) -InputQueue $queue -TimerState $state) $result.Count | Should -Be 2 $result | Should -Contain 'Tick' $result | Should -Contain 'Quit' } } Context 'Legacy Tick message pass-through' { It 'Should forward Tick messages from -TickMs runspace' { $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'Tick'; Key = 'Tick' }) $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @() -InputQueue $queue -TimerState $state) $result.Count | Should -Be 1 $result[0].Type | Should -Be 'Tick' } } Context 'Queue draining' { It 'Should drain all pending items in one call' { $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() 1..5 | ForEach-Object { $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::A; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) } $state = @{} $result = @(Invoke-ElmSubscriptions -Subscriptions @() -InputQueue $queue -TimerState $state) $result.Count | Should -Be 5 } It 'Should leave queue empty after call' { $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $queue.Enqueue([PSCustomObject]@{ Type = 'KeyDown'; Key = [System.ConsoleKey]::A; Char = [char]0; Modifiers = [System.ConsoleModifiers]0 }) $state = @{} $null = @(Invoke-ElmSubscriptions -Subscriptions @() -InputQueue $queue -TimerState $state) $queue.Count | Should -Be 0 } } } |