import SessionCore import Testing import UIKit @testable import WebTerm /// T-iOS-11 · KeyBar component + hardware key commands (plan §7). /// The bar's layout mirrors `public/keybar.ts` `KEYBAR_BUTTONS` (order, glyphs, /// captions, primary flag) and EVERY label→bytes lookup goes through /// `KeyByteMap` — no byte literal exists in the App layer. @MainActor @Suite("KeyBar") struct KeyBarTests { @MainActor private final class KeyRecorder { private(set) var keys: [KeyByteMap.Key] = [] func record(_ key: KeyByteMap.Key) { keys = keys + [key] } } // MARK: - Layout data (mirror of KEYBAR_BUTTONS) @Test("button order mirrors public/keybar.ts KEYBAR_BUTTONS exactly") func layoutOrderMirrorsWebKeybarButtons() { let expectedOrder: [KeyByteMap.Key] = [ .esc, .escEsc, .shiftTab, .arrowUp, .arrowDown, .enter, .ctrlC, .ctrlR, .ctrlO, .ctrlL, .ctrlT, .ctrlB, .ctrlD, .tab, .arrowLeft, .arrowRight, .slash, ] #expect(KeyBarLayout.buttons.map(\.key) == expectedOrder) } @Test("glyph labels mirror the web bar; Esc is the only primary button") func labelsAndPrimaryFlagMirrorWeb() { let expectedLabels = [ "Esc", "Esc²", "⇧Tab", "↑", "↓", "⏎", "^C", "^R", "^O", "^L", "^T", "^B", "^D", "Tab", "←", "→", "/", ] #expect(KeyBarLayout.buttons.map(\.label) == expectedLabels) #expect(KeyBarLayout.buttons.filter(\.isPrimary).map(\.key) == [.esc]) } @Test("every button spec resolves to real bytes through KeyByteMap (single source of truth)") func everySpecResolvesThroughKeyByteMap() { for spec in KeyBarLayout.buttons { #expect(!KeyByteMap.bytes(for: spec.key).isEmpty, "no bytes for \(spec.key.rawValue)") } } // MARK: - KeyBarView (UIKit input accessory) @Test("KeyBarView builds one button per spec with the web title as accessibility label") func keyBarViewBuildsOneButtonPerSpec() { // Arrange / Act let bar = KeyBarView() // Assert #expect(bar.keyButtons.count == KeyBarLayout.buttons.count) #expect(bar.keyButtons.map(\.accessibilityLabel) == KeyBarLayout.buttons.map(\.title)) } @Test("tapping button i fires onKey with layout key i (bytes resolved downstream via KeyByteMap)") func tappingButtonsFiresOnKeyInLayoutOrder() { // Arrange let bar = KeyBarView() let recorder = KeyRecorder() bar.onKey = { recorder.record($0) } // Act: tap every button once, in order. for button in bar.keyButtons { button.sendActions(for: .touchUpInside) } // Assert #expect(recorder.keys == KeyBarLayout.buttons.map(\.key)) } // MARK: - Hardware keyboard (UIKeyCommand, same KeyByteMap mapping) @Test("hardware chords: Esc, Shift+Tab and the 7 Ctrl chords map through KeyByteMap") func hardwareChordsCoverEscShiftTabAndCtrlChords() { let expected: [(KeyByteMap.Key, String, UIKeyModifierFlags)] = [ (.esc, UIKeyCommand.inputEscape, []), (.shiftTab, "\t", .shift), (.ctrlC, "c", .control), (.ctrlR, "r", .control), (.ctrlO, "o", .control), (.ctrlL, "l", .control), (.ctrlT, "t", .control), (.ctrlB, "b", .control), (.ctrlD, "d", .control), ] #expect(HardwareKeyCommands.chords.count == expected.count) for (key, input, modifiers) in expected { let chord = HardwareKeyCommands.chords.first { $0.key == key } #expect(chord?.input == input, "input mismatch for \(key.rawValue)") #expect(chord?.modifiers == modifiers, "modifiers mismatch for \(key.rawValue)") #expect(!KeyByteMap.bytes(for: key).isEmpty) } } @Test("build() emits one prioritized UIKeyCommand per chord and key(matching:) round-trips") func buildEmitsPrioritizedCommandsAndRoundTrips() { // Arrange / Act let commands = HardwareKeyCommands.build( action: #selector(UIResponder.becomeFirstResponder) // any selector; not invoked ) // Assert #expect(commands.count == HardwareKeyCommands.chords.count) for command in commands { #expect(command.wantsPriorityOverSystemBehavior) let key = HardwareKeyCommands.key(matching: command) #expect(key != nil, "no chord matches \(String(describing: command.input))") } #expect(Set(commands.compactMap { HardwareKeyCommands.key(matching: $0) }).count == HardwareKeyCommands.chords.count) } @Test("hardware mapping never hijacks plain typing keys or arrows (SwiftTerm owns them — DECCKM)") func hardwareMappingExcludesPlainKeysAndArrows() { // Arrows must stay SwiftTerm-native: a fixed \u{1B}[A override would // break application-cursor-keys mode (vim/htop send \u{1B}OA there). // Enter/Tab/"/" are ordinary typing; Esc·Esc is two Esc presses. let excluded: Set = [ .arrowUp, .arrowDown, .arrowLeft, .arrowRight, .enter, .tab, .slash, .escEsc, ] let chordKeys = Set(HardwareKeyCommands.chords.map(\.key)) #expect(chordKeys.isDisjoint(with: excluded)) } }