| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | |
| 3 | import { |
| 4 | applyCtrlModifier, |
| 5 | chunkTerminalWrite, |
| 6 | encodeTerminalPaste, |
| 7 | hostPlatformFromOs, |
| 8 | resolveModifiedTerminalInput, |
| 9 | TERMINAL_WRITE_MAX_LENGTH, |
| 10 | } from "./terminalInput"; |
| 11 | |
| 12 | const byte = (code: number) => String.fromCharCode(code); |
| 13 | const ESC = byte(0x1b); |
| 14 | const CTRL_C = byte(0x03); |
| 15 | const CTRL_V = byte(0x16); |
| 16 | |
| 17 | describe("applyCtrlModifier", () => { |
| 18 | it("maps letters to control bytes regardless of case", () => { |
| 19 | expect(applyCtrlModifier("c")).toBe(CTRL_C); |
| 20 | expect(applyCtrlModifier("C")).toBe(CTRL_C); |
| 21 | expect(applyCtrlModifier("z")).toBe(byte(0x1a)); |
| 22 | }); |
| 23 | |
| 24 | it("maps the punctuation control keys and leaves the rest untouched", () => { |
| 25 | expect(applyCtrlModifier("[")).toBe(ESC); |
| 26 | expect(applyCtrlModifier("?")).toBe(byte(0x7f)); |
| 27 | expect(applyCtrlModifier("1")).toBe("1"); |
| 28 | expect(applyCtrlModifier("")).toBe(""); |
| 29 | }); |
| 30 | }); |
| 31 | |
| 32 | describe("resolveModifiedTerminalInput", () => { |
| 33 | it("pastes on ctrl+v for windows, linux, and unknown hosts", () => { |
| 34 | for (const hostPlatform of ["windows", "linux", "unknown"] as const) { |
| 35 | expect(resolveModifiedTerminalInput({ data: "v", modifier: "ctrl", hostPlatform })).toEqual({ |
| 36 | kind: "paste", |
| 37 | }); |
| 38 | expect(resolveModifiedTerminalInput({ data: "V", modifier: "ctrl", hostPlatform })).toEqual({ |
| 39 | kind: "paste", |
| 40 | }); |
| 41 | } |
| 42 | }); |
| 43 | |
| 44 | it("keeps alt+v as a meta chord on non-mac hosts", () => { |
| 45 | expect( |
| 46 | resolveModifiedTerminalInput({ data: "v", modifier: "meta", hostPlatform: "windows" }), |
| 47 | ).toEqual({ kind: "write", data: `${ESC}v` }); |
| 48 | }); |
| 49 | |
| 50 | it("pastes on cmd+v and forwards raw ctrl+v on mac hosts", () => { |
| 51 | expect( |
| 52 | resolveModifiedTerminalInput({ data: "v", modifier: "meta", hostPlatform: "mac" }), |
| 53 | ).toEqual({ kind: "paste" }); |
| 54 | expect( |
| 55 | resolveModifiedTerminalInput({ data: "v", modifier: "ctrl", hostPlatform: "mac" }), |
| 56 | ).toEqual({ kind: "write", data: CTRL_V }); |
| 57 | }); |
| 58 | |
| 59 | it("still encodes every other modified key", () => { |
| 60 | expect( |
| 61 | resolveModifiedTerminalInput({ data: "c", modifier: "ctrl", hostPlatform: "windows" }), |
| 62 | ).toEqual({ kind: "write", data: CTRL_C }); |
| 63 | expect( |
| 64 | resolveModifiedTerminalInput({ data: "[A", modifier: "meta", hostPlatform: "linux" }), |
| 65 | ).toEqual({ kind: "write", data: `${ESC}[A` }); |
| 66 | }); |
| 67 | }); |
| 68 | |
| 69 | describe("encodeTerminalPaste", () => { |
| 70 | it("passes single-line text through unchanged", () => { |
| 71 | expect(encodeTerminalPaste("git switch -c fix/paste")).toBe("git switch -c fix/paste"); |
| 72 | expect(encodeTerminalPaste("")).toBe(""); |
| 73 | }); |
| 74 | |
| 75 | it("turns LF and CRLF line breaks into a single carriage return each", () => { |
| 76 | expect(encodeTerminalPaste("one\ntwo\r\nthree\n")).toBe("one\rtwo\rthree\r"); |
| 77 | }); |
| 78 | |
| 79 | it("replaces unsafe control bytes with spaces but keeps tabs", () => { |
| 80 | expect(encodeTerminalPaste(`a${byte(0)}b${ESC}c${byte(0x7f)}d\te`)).toBe("a b c d\te"); |
| 81 | }); |
| 82 | |
| 83 | it("never lets a bracketed-paste end marker reach the shell", () => { |
| 84 | expect(encodeTerminalPaste(`safe${ESC}[201~; rm -rf /\n`)).toBe("safe [201~; rm -rf /\r"); |
| 85 | }); |
| 86 | }); |
| 87 | |
| 88 | describe("chunkTerminalWrite", () => { |
| 89 | it("leaves writes within the wire limit whole", () => { |
| 90 | expect(chunkTerminalWrite("")).toEqual([]); |
| 91 | expect(chunkTerminalWrite("ls")).toEqual(["ls"]); |
| 92 | expect(chunkTerminalWrite("x".repeat(TERMINAL_WRITE_MAX_LENGTH))).toHaveLength(1); |
| 93 | }); |
| 94 | |
| 95 | it("splits oversized writes so every chunk fits the contract", () => { |
| 96 | const chunks = chunkTerminalWrite("y".repeat(TERMINAL_WRITE_MAX_LENGTH * 2 + 5)); |
| 97 | expect(chunks.map((chunk) => chunk.length)).toEqual([ |
| 98 | TERMINAL_WRITE_MAX_LENGTH, |
| 99 | TERMINAL_WRITE_MAX_LENGTH, |
| 100 | 5, |
| 101 | ]); |
| 102 | expect(chunks.join("")).toHaveLength(TERMINAL_WRITE_MAX_LENGTH * 2 + 5); |
| 103 | }); |
| 104 | |
| 105 | it("does not cut a surrogate pair in half at the boundary", () => { |
| 106 | const data = `${"z".repeat(TERMINAL_WRITE_MAX_LENGTH - 1)}😀tail`; |
| 107 | const chunks = chunkTerminalWrite(data); |
| 108 | expect(chunks[0]).toHaveLength(TERMINAL_WRITE_MAX_LENGTH - 1); |
| 109 | expect(chunks[1]).toBe("😀tail"); |
| 110 | expect(chunks.join("")).toBe(data); |
| 111 | }); |
| 112 | }); |
| 113 | |
| 114 | describe("hostPlatformFromOs", () => { |
| 115 | it("maps the descriptor os onto the toolbar layout", () => { |
| 116 | expect(hostPlatformFromOs("darwin")).toBe("mac"); |
| 117 | expect(hostPlatformFromOs("windows")).toBe("windows"); |
| 118 | expect(hostPlatformFromOs("linux")).toBe("linux"); |
| 119 | }); |
| 120 | |
| 121 | it("defers to the caller when the os is unknown or not loaded yet", () => { |
| 122 | expect(hostPlatformFromOs("unknown")).toBeNull(); |
| 123 | expect(hostPlatformFromOs(null)).toBeNull(); |
| 124 | }); |
| 125 | }); |