Files
TUIkit/Tests/TUIkitTests/KeyEventTests.swift
T
phranck c1bda23c92 Feat: Add text selection support to TextField and SecureField
- 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)
2026-02-09 22:57:22 +01:00

416 lines
12 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 🖥 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)
}
}