From fa919ef166fbbfc55a82d5839d9c51229e7de296 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Wed, 27 May 2026 14:54:53 +0800 Subject: [PATCH] test: add all kitty protocol parse-keypress unit tests (~57 tests) --- .../src/io/parse-keypress-kitty.test.ts | 498 ++++++++++++++++++ 1 file changed, 498 insertions(+) create mode 100644 packages/runtime/src/io/parse-keypress-kitty.test.ts diff --git a/packages/runtime/src/io/parse-keypress-kitty.test.ts b/packages/runtime/src/io/parse-keypress-kitty.test.ts new file mode 100644 index 0000000..2f5aa8c --- /dev/null +++ b/packages/runtime/src/io/parse-keypress-kitty.test.ts @@ -0,0 +1,498 @@ +import { describe, test, expect } from "vite-plus/test"; +import { parseKeypress } from "./parse-keypress.ts"; + +function kittyKey( + codepoint: number, + modifiers?: number, + eventType?: number, + textCodepoints?: number[], +): string { + let seq = `\x1b[${codepoint}`; + if (modifiers !== undefined || eventType !== undefined || textCodepoints !== undefined) { + seq += `;${modifiers ?? 1}`; + } + if (eventType !== undefined || textCodepoints !== undefined) { + seq += `:${eventType ?? 1}`; + } + if (textCodepoints !== undefined) { + seq += `;${textCodepoints.join(":")}`; + } + seq += "u"; + return seq; +} + +// --- Task 4: Basic characters, modifiers, special keys --- + +describe("kitty protocol - basic characters and modifiers", () => { + test("simple character", () => { + const result = parseKeypress(kittyKey(97)); + expect(result.name).toBe("a"); + expect(result.ctrl).toBe(false); + expect(result.shift).toBe(false); + expect(result.meta).toBe(false); + expect(result.eventType).toBe("press"); + expect(result.isKittyProtocol).toBe(true); + }); + + test("uppercase character (shift)", () => { + const result = parseKeypress(kittyKey(65, 2)); + expect(result.name).toBe("a"); + expect(result.shift).toBe(true); + expect(result.ctrl).toBe(false); + expect(result.eventType).toBe("press"); + }); + + test("ctrl modifier", () => { + const result = parseKeypress(kittyKey(97, 5)); + expect(result.name).toBe("a"); + expect(result.ctrl).toBe(true); + expect(result.shift).toBe(false); + expect(result.eventType).toBe("press"); + }); + + test("alt/option modifier", () => { + const result = parseKeypress(kittyKey(97, 3)); + expect(result.name).toBe("a"); + expect(result.meta).toBe(true); + expect(result.ctrl).toBe(false); + expect(result.eventType).toBe("press"); + }); + + test("super modifier", () => { + const result = parseKeypress(kittyKey(97, 9)); + expect(result.name).toBe("a"); + expect(result.super).toBe(true); + expect(result.ctrl).toBe(false); + expect(result.eventType).toBe("press"); + }); + + test("hyper modifier", () => { + const result = parseKeypress(kittyKey(97, 17)); + expect(result.name).toBe("a"); + expect(result.hyper).toBe(true); + expect(result.super).toBe(false); + expect(result.eventType).toBe("press"); + }); + + test("meta modifier", () => { + const result = parseKeypress(kittyKey(97, 33)); + expect(result.name).toBe("a"); + expect(result.meta).toBe(true); + expect(result.eventType).toBe("press"); + }); + + test("caps lock", () => { + const result = parseKeypress(kittyKey(97, 65)); + expect(result.name).toBe("a"); + expect(result.capsLock).toBe(true); + expect(result.eventType).toBe("press"); + }); + + test("num lock", () => { + const result = parseKeypress(kittyKey(97, 129)); + expect(result.name).toBe("a"); + expect(result.numLock).toBe(true); + expect(result.eventType).toBe("press"); + }); + + test("combined modifiers (ctrl+shift)", () => { + const result = parseKeypress(kittyKey(97, 6)); + expect(result.name).toBe("a"); + expect(result.ctrl).toBe(true); + expect(result.shift).toBe(true); + expect(result.meta).toBe(false); + expect(result.eventType).toBe("press"); + }); + + test("combined modifiers (super+ctrl)", () => { + const result = parseKeypress(kittyKey(115, 13)); + expect(result.name).toBe("s"); + expect(result.super).toBe(true); + expect(result.ctrl).toBe(true); + expect(result.shift).toBe(false); + expect(result.eventType).toBe("press"); + }); +}); + +describe("kitty protocol - special keys", () => { + test("escape key", () => { + const result = parseKeypress(kittyKey(27)); + expect(result.name).toBe("escape"); + expect(result.eventType).toBe("press"); + }); + + test("return/enter key", () => { + const result = parseKeypress(kittyKey(13)); + expect(result.name).toBe("return"); + expect(result.eventType).toBe("press"); + }); + + test("tab key", () => { + const result = parseKeypress(kittyKey(9)); + expect(result.name).toBe("tab"); + expect(result.eventType).toBe("press"); + }); + + test("backspace key (codepoint 8)", () => { + const result = parseKeypress(kittyKey(8)); + expect(result.name).toBe("backspace"); + expect(result.eventType).toBe("press"); + }); + + test("backspace key (codepoint 127)", () => { + const result = parseKeypress(kittyKey(127)); + expect(result.name).toBe("backspace"); + expect(result.eventType).toBe("press"); + }); + + test("legacy meta+backspace (0x7F)", () => { + const result = parseKeypress("\x1b\x7f"); + expect(result.name).toBe("backspace"); + expect(result.meta).toBe(true); + }); + + test("space key", () => { + const result = parseKeypress(kittyKey(32)); + expect(result.name).toBe("space"); + expect(result.eventType).toBe("press"); + }); +}); + +// --- Task 5: Event types, text/unicode, arrows, errors --- + +describe("kitty protocol - event types", () => { + test("press", () => { + const result = parseKeypress(kittyKey(97, 1, 1)); + expect(result.name).toBe("a"); + expect(result.eventType).toBe("press"); + }); + + test("repeat", () => { + const result = parseKeypress(kittyKey(97, 1, 2)); + expect(result.name).toBe("a"); + expect(result.eventType).toBe("repeat"); + }); + + test("release", () => { + const result = parseKeypress(kittyKey(97, 1, 3)); + expect(result.name).toBe("a"); + expect(result.eventType).toBe("release"); + }); +}); + +describe("kitty protocol - text and unicode", () => { + test("number keys", () => { + const result = parseKeypress(kittyKey(49)); + expect(result.name).toBe("1"); + expect(result.eventType).toBe("press"); + }); + + test("special character (@)", () => { + const result = parseKeypress(kittyKey(64)); + expect(result.name).toBe("@"); + expect(result.eventType).toBe("press"); + }); + + test("ctrl+letter via codepoint 1-26", () => { + const result = parseKeypress(kittyKey(1, 5)); + expect(result.name).toBe("a"); + expect(result.ctrl).toBe(true); + }); + + test("preserves sequence and raw", () => { + const seq = kittyKey(97, 5); + const result = parseKeypress(seq); + expect(result.sequence).toBe(seq); + expect(result.raw).toBe(seq); + }); + + test("text-as-codepoints field", () => { + const result = parseKeypress(kittyKey(97, 2, 1, [65])); + expect(result.name).toBe("a"); + expect(result.text).toBe("A"); + expect(result.shift).toBe(true); + expect(result.isKittyProtocol).toBe(true); + }); + + test("text-as-codepoints with multiple codepoints", () => { + const result = parseKeypress(kittyKey(97, 1, 1, [72, 101])); + expect(result.text).toBe("He"); + expect(result.isKittyProtocol).toBe(true); + }); + + test("supplementary unicode codepoint (emoji)", () => { + const result = parseKeypress(kittyKey(128_512)); + expect(result.name).toBe("\u{1F600}"); + expect(result.isKittyProtocol).toBe(true); + }); + + test("text defaults to character from codepoint", () => { + const result = parseKeypress(kittyKey(97)); + expect(result.text).toBe("a"); + expect(result.isKittyProtocol).toBe(true); + }); +}); + +describe("kitty protocol - enhanced special keys", () => { + test("arrow keys with event type", () => { + const up = parseKeypress("\x1b[1;1:1A"); + expect(up.name).toBe("up"); + expect(up.eventType).toBe("press"); + expect(up.isKittyProtocol).toBe(true); + + const down = parseKeypress("\x1b[1;1:3B"); + expect(down.name).toBe("down"); + expect(down.eventType).toBe("release"); + + const right = parseKeypress("\x1b[1;1:2C"); + expect(right.name).toBe("right"); + expect(right.eventType).toBe("repeat"); + + const left = parseKeypress("\x1b[1;1:1D"); + expect(left.name).toBe("left"); + expect(left.eventType).toBe("press"); + }); + + test("arrow keys with modifiers", () => { + const result = parseKeypress("\x1b[1;5:1A"); + expect(result.name).toBe("up"); + expect(result.ctrl).toBe(true); + expect(result.eventType).toBe("press"); + expect(result.isKittyProtocol).toBe(true); + }); + + test("home and end keys", () => { + const home = parseKeypress("\x1b[1;1:1H"); + expect(home.name).toBe("home"); + expect(home.eventType).toBe("press"); + expect(home.isKittyProtocol).toBe(true); + + const end = parseKeypress("\x1b[1;1:1F"); + expect(end.name).toBe("end"); + expect(end.eventType).toBe("press"); + }); + + test("tilde-terminated special keys", () => { + const del = parseKeypress("\x1b[3;1:1~"); + expect(del.name).toBe("delete"); + expect(del.isKittyProtocol).toBe(true); + + const ins = parseKeypress("\x1b[2;1:1~"); + expect(ins.name).toBe("insert"); + + const pgup = parseKeypress("\x1b[5;1:1~"); + expect(pgup.name).toBe("pageup"); + + const f5 = parseKeypress("\x1b[15;1:1~"); + expect(f5.name).toBe("f5"); + }); + + test("tilde keys with modifiers", () => { + const result = parseKeypress("\x1b[3;2:1~"); + expect(result.name).toBe("delete"); + expect(result.shift).toBe(true); + expect(result.eventType).toBe("press"); + expect(result.isKittyProtocol).toBe(true); + }); +}); + +describe("kitty protocol - error handling", () => { + test("invalid codepoint above U+10FFFF returns safe empty keypress", () => { + const result = parseKeypress("\x1b[1114112u"); + expect(result.name).toBe(""); + expect(result.ctrl).toBe(false); + expect(result.isKittyProtocol).toBe(true); + expect(result.isPrintable).toBe(false); + }); + + test("surrogate codepoint returns safe empty keypress", () => { + const result = parseKeypress("\x1b[55296u"); + expect(result.name).toBe(""); + expect(result.ctrl).toBe(false); + expect(result.isKittyProtocol).toBe(true); + expect(result.isPrintable).toBe(false); + }); + + test("invalid text codepoint replaced with fallback", () => { + const result = parseKeypress(kittyKey(97, 1, 1, [1_114_112])); + expect(result.name).toBe("a"); + expect(result.text).toBe("?"); + expect(result.isKittyProtocol).toBe(true); + }); + + test("malformed modifier 0 does not set all flags", () => { + const result = parseKeypress("\x1b[97;0u"); + expect(result.name).toBe("a"); + expect(result.ctrl).toBe(false); + expect(result.shift).toBe(false); + expect(result.meta).toBe(false); + expect(result.super ?? false).toBe(false); + expect(result.isKittyProtocol).toBe(true); + }); +}); + +// --- Task 6: Query filter, legacy fallback, isPrintable, non-printable keys --- + +describe("kitty protocol - query response filtering", () => { + test("query response returns ignored keypress", () => { + const result = parseKeypress("\x1b[?1u"); + expect(result.name).toBe(""); + expect(result.ignore).toBe(true); + }); + + test("multi-digit query response returns ignored keypress", () => { + const result = parseKeypress("\x1b[?31u"); + expect(result.name).toBe(""); + expect(result.ignore).toBe(true); + }); +}); + +describe("kitty protocol - legacy fallback", () => { + test("non-kitty sequences fall back to legacy parsing", () => { + const result = parseKeypress("\x1b[A"); + expect(result.name).toBe("up"); + expect(result.isKittyProtocol).toBeUndefined(); + }); + + test("ctrl+c legacy fallback", () => { + const result = parseKeypress("\x03"); + expect(result.name).toBe("c"); + expect(result.ctrl).toBe(true); + expect(result.isKittyProtocol).toBeUndefined(); + }); +}); + +describe("kitty protocol - isPrintable field", () => { + test("true for regular characters", () => { + expect(parseKeypress(kittyKey(97)).isPrintable).toBe(true); + }); + + test("true for digits", () => { + expect(parseKeypress(kittyKey(49)).isPrintable).toBe(true); + }); + + test("true for symbols", () => { + expect(parseKeypress(kittyKey(64)).isPrintable).toBe(true); + }); + + test("true for emoji", () => { + expect(parseKeypress(kittyKey(128_512)).isPrintable).toBe(true); + }); + + test("false for escape", () => { + expect(parseKeypress(kittyKey(27)).isPrintable).toBe(false); + }); + + test("true for return", () => { + expect(parseKeypress(kittyKey(13)).isPrintable).toBe(true); + }); + + test("false for tab", () => { + expect(parseKeypress(kittyKey(9)).isPrintable).toBe(false); + }); + + test("true for space", () => { + expect(parseKeypress(kittyKey(32)).isPrintable).toBe(true); + }); + + test("false for backspace", () => { + expect(parseKeypress(kittyKey(8)).isPrintable).toBe(false); + }); + + test("false for ctrl+letter", () => { + expect(parseKeypress(kittyKey(1, 5)).isPrintable).toBe(false); + }); + + test("false for special keys (arrows)", () => { + expect(parseKeypress("\x1b[1;1:1A").isPrintable).toBe(false); + }); +}); + +describe("kitty protocol - non-printable key suppression", () => { + test("capslock (57358) is non-printable", () => { + const result = parseKeypress("\x1b[57358u"); + expect(result.name).toBe("capslock"); + expect(result.isPrintable).toBe(false); + expect(result.isKittyProtocol).toBe(true); + }); + + test("printscreen (57361) is non-printable", () => { + const result = parseKeypress("\x1b[57361u"); + expect(result.name).toBe("printscreen"); + expect(result.isPrintable).toBe(false); + }); + + test("f13 (57376) is non-printable", () => { + const result = parseKeypress("\x1b[57376u"); + expect(result.name).toBe("f13"); + expect(result.isPrintable).toBe(false); + }); + + test("media key (57428 mediaplay) is non-printable", () => { + const result = parseKeypress("\x1b[57428u"); + expect(result.name).toBe("mediaplay"); + expect(result.isPrintable).toBe(false); + }); + + test("modifier-only key (57441 leftshift) is non-printable", () => { + const result = parseKeypress("\x1b[57441u"); + expect(result.name).toBe("leftshift"); + expect(result.isPrintable).toBe(false); + }); + + test("modifier-only key (57442 leftcontrol) is non-printable", () => { + const result = parseKeypress("\x1b[57442u"); + expect(result.name).toBe("leftcontrol"); + expect(result.isPrintable).toBe(false); + }); + + test("kp keys (57399 kp0) are non-printable", () => { + const result = parseKeypress("\x1b[57399u"); + expect(result.name).toBe("kp0"); + expect(result.isPrintable).toBe(false); + }); + + test("scrolllock (57359) is non-printable", () => { + const result = parseKeypress("\x1b[57359u"); + expect(result.name).toBe("scrolllock"); + expect(result.isPrintable).toBe(false); + }); + + test("numlock (57360) is non-printable", () => { + const result = parseKeypress("\x1b[57360u"); + expect(result.name).toBe("numlock"); + expect(result.isPrintable).toBe(false); + }); + + test("pause (57362) is non-printable", () => { + const result = parseKeypress("\x1b[57362u"); + expect(result.name).toBe("pause"); + expect(result.isPrintable).toBe(false); + }); + + test("volume keys are non-printable", () => { + const lower = parseKeypress("\x1b[57438u"); + expect(lower.name).toBe("lowervolume"); + expect(lower.isPrintable).toBe(false); + + const raise = parseKeypress("\x1b[57439u"); + expect(raise.name).toBe("raisevolume"); + expect(raise.isPrintable).toBe(false); + + const mute = parseKeypress("\x1b[57440u"); + expect(mute.name).toBe("mutevolume"); + expect(mute.isPrintable).toBe(false); + }); +}); + +describe("kitty protocol - space and return text", () => { + test("space key has text field set to space character", () => { + const result = parseKeypress(kittyKey(32)); + expect(result.text).toBe(" "); + }); + + test("return key has text field set to carriage return", () => { + const result = parseKeypress(kittyKey(13)); + expect(result.text).toBe("\r"); + }); +});