mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
c1bda23c92
- Parse CSI modifier codes (Shift/Alt/Ctrl) in escape sequences - Add selection state to TextFieldHandler (anchor, range, helpers) - Implement Shift+Arrow selection keyboard handling - Render selection with accent background highlight - Support delete/replace selected text with Backspace/Delete/typing - Add 41 new tests (14 KeyEvent + 27 TextFieldHandler)
416 lines
12 KiB
Swift
416 lines
12 KiB
Swift
// 🖥️ TUIKit — Terminal UI Kit for Swift
|
||
// KeyEventTests.swift
|
||
//
|
||
// Created by LAYERED.work
|
||
// License: MIT
|
||
|
||
import Testing
|
||
|
||
@testable import TUIkit
|
||
|
||
// MARK: - Key Enum Tests
|
||
|
||
@MainActor
|
||
@Suite("Key Enum Tests")
|
||
struct KeyEnumTests {
|
||
|
||
@Test("Key.from creates character key")
|
||
func keyFromCharacter() {
|
||
let key = Key.from("a")
|
||
#expect(key == .character("a"))
|
||
}
|
||
|
||
@Test("Key is Hashable")
|
||
func keyHashable() {
|
||
let set: Set<Key> = [.enter, .tab, .escape, .enter]
|
||
#expect(set.count == 3) // .enter deduped
|
||
}
|
||
|
||
@Test("Character keys with different characters are not equal")
|
||
func characterKeysInequality() {
|
||
#expect(Key.character("a") != Key.character("b"))
|
||
}
|
||
|
||
@Test("Same character keys are equal")
|
||
func characterKeysEquality() {
|
||
let key1 = Key.character("x")
|
||
let key2 = Key.character("x")
|
||
#expect(key1 == key2)
|
||
}
|
||
|
||
@Test("Special keys are distinct")
|
||
func specialKeysDistinct() {
|
||
let keys: [Key] = [.escape, .enter, .tab, .backspace, .delete,
|
||
.up, .down, .left, .right,
|
||
.home, .end, .pageUp, .pageDown]
|
||
let set = Set(keys)
|
||
#expect(set.count == 13)
|
||
}
|
||
}
|
||
|
||
// MARK: - KeyEvent Creation Tests
|
||
|
||
@MainActor
|
||
@Suite("KeyEvent Creation Tests")
|
||
struct KeyEventCreationTests {
|
||
|
||
@Test("KeyEvent character init detects uppercase as shift")
|
||
func characterInitShift() {
|
||
let upper = KeyEvent(character: "A")
|
||
#expect(upper.shift == true)
|
||
#expect(upper.key == .character("A"))
|
||
|
||
let lower = KeyEvent(character: "a")
|
||
#expect(lower.shift == false)
|
||
#expect(lower.key == .character("a"))
|
||
}
|
||
|
||
@Test("KeyEvent character init with non-letter")
|
||
func characterInitNonLetter() {
|
||
let event = KeyEvent(character: "5")
|
||
#expect(event.key == .character("5"))
|
||
#expect(event.shift == false)
|
||
}
|
||
|
||
@Test("KeyEvent is Equatable")
|
||
func equatable() {
|
||
let eventA = KeyEvent(key: .enter, ctrl: false, alt: false, shift: false)
|
||
let eventB = KeyEvent(key: .enter)
|
||
#expect(eventA == eventB)
|
||
|
||
let eventC = KeyEvent(key: .enter, ctrl: true)
|
||
#expect(eventA != eventC)
|
||
}
|
||
}
|
||
|
||
// MARK: - KeyEvent Parsing Tests
|
||
|
||
@MainActor
|
||
@Suite("KeyEvent Parse Tests")
|
||
struct KeyEventParseTests {
|
||
|
||
@Test("Parse empty bytes returns nil")
|
||
func parseEmpty() {
|
||
#expect(KeyEvent.parse([]) == nil)
|
||
}
|
||
|
||
@Test("Parse escape byte")
|
||
func parseEscape() {
|
||
let event = KeyEvent.parse([0x1B])
|
||
#expect(event?.key == .escape)
|
||
}
|
||
|
||
@Test("Parse enter (carriage return)")
|
||
func parseEnter() {
|
||
let event = KeyEvent.parse([0x0D])
|
||
#expect(event?.key == .enter)
|
||
}
|
||
|
||
@Test("Parse enter (line feed)")
|
||
func parseLineFeed() {
|
||
let event = KeyEvent.parse([0x0A])
|
||
#expect(event?.key == .enter)
|
||
}
|
||
|
||
@Test("Parse tab")
|
||
func parseTab() {
|
||
let event = KeyEvent.parse([0x09])
|
||
#expect(event?.key == .tab)
|
||
}
|
||
|
||
@Test("Parse backspace (0x7F)")
|
||
func parseBackspaceDelete() {
|
||
let event = KeyEvent.parse([0x7F])
|
||
#expect(event?.key == .backspace)
|
||
}
|
||
|
||
@Test("Parse backspace (0x08)")
|
||
func parseBackspace() {
|
||
let event = KeyEvent.parse([0x08])
|
||
#expect(event?.key == .backspace)
|
||
}
|
||
|
||
@Test("Parse printable character")
|
||
func parsePrintable() {
|
||
let event = KeyEvent.parse([0x41]) // 'A'
|
||
#expect(event?.key == .character("A"))
|
||
#expect(event?.shift == true)
|
||
}
|
||
|
||
@Test("Parse lowercase character")
|
||
func parseLowercase() {
|
||
let event = KeyEvent.parse([0x61]) // 'a'
|
||
#expect(event?.key == .character("a"))
|
||
#expect(event?.shift == false)
|
||
}
|
||
|
||
@Test("Parse space character")
|
||
func parseSpace() {
|
||
let event = KeyEvent.parse([0x20])
|
||
#expect(event?.key == .space)
|
||
}
|
||
|
||
@Test("Parse Ctrl+A")
|
||
func parseCtrlA() {
|
||
let event = KeyEvent.parse([0x01])
|
||
#expect(event?.key == .character("a"))
|
||
#expect(event?.ctrl == true)
|
||
}
|
||
|
||
@Test("Parse Ctrl+C")
|
||
func parseCtrlC() {
|
||
let event = KeyEvent.parse([0x03])
|
||
#expect(event?.key == .character("c"))
|
||
#expect(event?.ctrl == true)
|
||
}
|
||
|
||
@Test("Parse Ctrl+Z")
|
||
func parseCtrlZ() {
|
||
let event = KeyEvent.parse([0x1A])
|
||
#expect(event?.key == .character("z"))
|
||
#expect(event?.ctrl == true)
|
||
}
|
||
|
||
// MARK: Arrow Keys (CSI Sequences)
|
||
|
||
@Test("Parse arrow up (ESC [ A)")
|
||
func parseArrowUp() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x41])
|
||
#expect(event?.key == .up)
|
||
}
|
||
|
||
@Test("Parse arrow down (ESC [ B)")
|
||
func parseArrowDown() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x42])
|
||
#expect(event?.key == .down)
|
||
}
|
||
|
||
@Test("Parse arrow right (ESC [ C)")
|
||
func parseArrowRight() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x43])
|
||
#expect(event?.key == .right)
|
||
}
|
||
|
||
@Test("Parse arrow left (ESC [ D)")
|
||
func parseArrowLeft() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x44])
|
||
#expect(event?.key == .left)
|
||
}
|
||
|
||
// MARK: Navigation Keys
|
||
|
||
@Test("Parse home (ESC [ H)")
|
||
func parseHome() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x48])
|
||
#expect(event?.key == .home)
|
||
}
|
||
|
||
@Test("Parse end (ESC [ F)")
|
||
func parseEnd() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x46])
|
||
#expect(event?.key == .end)
|
||
}
|
||
|
||
// MARK: Extended Keys (ESC [ n ~)
|
||
|
||
@Test("Parse home via extended (ESC [ 1 ~)")
|
||
func parseHomeExtended() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x31, 0x7E])
|
||
#expect(event?.key == .home)
|
||
}
|
||
|
||
@Test("Parse delete (ESC [ 3 ~)")
|
||
func parseDelete() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x33, 0x7E])
|
||
#expect(event?.key == .delete)
|
||
}
|
||
|
||
@Test("Parse end via extended (ESC [ 4 ~)")
|
||
func parseEndExtended() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x34, 0x7E])
|
||
#expect(event?.key == .end)
|
||
}
|
||
|
||
@Test("Parse page up (ESC [ 5 ~)")
|
||
func parsePageUp() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x35, 0x7E])
|
||
#expect(event?.key == .pageUp)
|
||
}
|
||
|
||
@Test("Parse page down (ESC [ 6 ~)")
|
||
func parsePageDown() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x36, 0x7E])
|
||
#expect(event?.key == .pageDown)
|
||
}
|
||
|
||
@Test("Parse insert returns nil (ESC [ 2 ~)")
|
||
func parseInsertReturnsNil() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x32, 0x7E])
|
||
#expect(event == nil)
|
||
}
|
||
|
||
// MARK: Alt+Key
|
||
|
||
@Test("Parse Alt+a")
|
||
func parseAltA() {
|
||
let event = KeyEvent.parse([0x1B, 0x61]) // ESC + 'a'
|
||
#expect(event?.key == .character("a"))
|
||
#expect(event?.alt == true)
|
||
}
|
||
|
||
@Test("Parse Alt+Enter")
|
||
func parseAltEnter() {
|
||
let event = KeyEvent.parse([0x1B, 0x0D]) // ESC + CR
|
||
#expect(event?.key == .enter)
|
||
#expect(event?.alt == true)
|
||
}
|
||
|
||
// MARK: UTF-8
|
||
|
||
@Test("Parse UTF-8 multi-byte character")
|
||
func parseUtf8() {
|
||
// "ü" = 0xC3 0xBC
|
||
let event = KeyEvent.parse([0xC3, 0xBC])
|
||
#expect(event?.key == .character("ü"))
|
||
}
|
||
|
||
@Test("Parse emoji UTF-8")
|
||
func parseEmoji() {
|
||
// "😀" = 0xF0 0x9F 0x98 0x80
|
||
let event = KeyEvent.parse([0xF0, 0x9F, 0x98, 0x80])
|
||
#expect(event?.key == .character("😀"))
|
||
}
|
||
|
||
// MARK: Edge Cases
|
||
|
||
@Test("Parse incomplete CSI returns nil")
|
||
func parseIncompleteCSI() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B])
|
||
#expect(event == nil)
|
||
}
|
||
|
||
@Test("Parse Shift+Tab (CSI Z) returns tab with shift")
|
||
func parseShiftTab() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x5A]) // ESC [ Z
|
||
#expect(event?.key == .tab)
|
||
#expect(event?.shift == true)
|
||
}
|
||
|
||
@Test("Parse unknown CSI final byte returns nil")
|
||
func parseUnknownCSI() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x58]) // ESC [ X (not a valid sequence)
|
||
#expect(event == nil)
|
||
}
|
||
|
||
@Test("Parse bare escape sequence returns escape")
|
||
func parseBareEscape() {
|
||
let event = KeyEvent.parse([0x1B])
|
||
#expect(event?.key == .escape)
|
||
}
|
||
|
||
// MARK: - Modifier Keys (CSI with modifiers)
|
||
|
||
@Test("Parse Shift+Up (ESC [ 1 ; 2 A)")
|
||
func parseShiftUp() {
|
||
// ESC [ 1 ; 2 A = 0x1B 0x5B 0x31 0x3B 0x32 0x41
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x31, 0x3B, 0x32, 0x41])
|
||
#expect(event?.key == .up)
|
||
#expect(event?.shift == true)
|
||
#expect(event?.ctrl == false)
|
||
#expect(event?.alt == false)
|
||
}
|
||
|
||
@Test("Parse Shift+Down (ESC [ 1 ; 2 B)")
|
||
func parseShiftDown() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x31, 0x3B, 0x32, 0x42])
|
||
#expect(event?.key == .down)
|
||
#expect(event?.shift == true)
|
||
}
|
||
|
||
@Test("Parse Shift+Right (ESC [ 1 ; 2 C)")
|
||
func parseShiftRight() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x31, 0x3B, 0x32, 0x43])
|
||
#expect(event?.key == .right)
|
||
#expect(event?.shift == true)
|
||
}
|
||
|
||
@Test("Parse Shift+Left (ESC [ 1 ; 2 D)")
|
||
func parseShiftLeft() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x31, 0x3B, 0x32, 0x44])
|
||
#expect(event?.key == .left)
|
||
#expect(event?.shift == true)
|
||
}
|
||
|
||
@Test("Parse Shift+Home (ESC [ 1 ; 2 H)")
|
||
func parseShiftHome() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x31, 0x3B, 0x32, 0x48])
|
||
#expect(event?.key == .home)
|
||
#expect(event?.shift == true)
|
||
}
|
||
|
||
@Test("Parse Shift+End (ESC [ 1 ; 2 F)")
|
||
func parseShiftEnd() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x31, 0x3B, 0x32, 0x46])
|
||
#expect(event?.key == .end)
|
||
#expect(event?.shift == true)
|
||
}
|
||
|
||
@Test("Parse Alt+Up (ESC [ 1 ; 3 A)")
|
||
func parseAltUp() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x31, 0x3B, 0x33, 0x41])
|
||
#expect(event?.key == .up)
|
||
#expect(event?.alt == true)
|
||
#expect(event?.shift == false)
|
||
}
|
||
|
||
@Test("Parse Ctrl+Up (ESC [ 1 ; 5 A)")
|
||
func parseCtrlUp() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x31, 0x3B, 0x35, 0x41])
|
||
#expect(event?.key == .up)
|
||
#expect(event?.ctrl == true)
|
||
#expect(event?.shift == false)
|
||
}
|
||
|
||
@Test("Parse Shift+Ctrl+Up (ESC [ 1 ; 6 A)")
|
||
func parseShiftCtrlUp() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x31, 0x3B, 0x36, 0x41])
|
||
#expect(event?.key == .up)
|
||
#expect(event?.shift == true)
|
||
#expect(event?.ctrl == true)
|
||
#expect(event?.alt == false)
|
||
}
|
||
|
||
@Test("Parse Shift+Alt+Ctrl+Right (ESC [ 1 ; 8 C)")
|
||
func parseShiftAltCtrlRight() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x31, 0x3B, 0x38, 0x43])
|
||
#expect(event?.key == .right)
|
||
#expect(event?.shift == true)
|
||
#expect(event?.alt == true)
|
||
#expect(event?.ctrl == true)
|
||
}
|
||
|
||
@Test("Parse Shift+Delete (ESC [ 3 ; 2 ~)")
|
||
func parseShiftDelete() {
|
||
// ESC [ 3 ; 2 ~ = 0x1B 0x5B 0x33 0x3B 0x32 0x7E
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x33, 0x3B, 0x32, 0x7E])
|
||
#expect(event?.key == .delete)
|
||
#expect(event?.shift == true)
|
||
}
|
||
|
||
@Test("Parse Ctrl+PageUp (ESC [ 5 ; 5 ~)")
|
||
func parseCtrlPageUp() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x35, 0x3B, 0x35, 0x7E])
|
||
#expect(event?.key == .pageUp)
|
||
#expect(event?.ctrl == true)
|
||
}
|
||
|
||
@Test("Arrow without modifier has no modifiers set")
|
||
func arrowWithoutModifier() {
|
||
let event = KeyEvent.parse([0x1B, 0x5B, 0x41]) // ESC [ A
|
||
#expect(event?.key == .up)
|
||
#expect(event?.shift == false)
|
||
#expect(event?.ctrl == false)
|
||
#expect(event?.alt == false)
|
||
}
|
||
}
|