|
1 | 1 | import { describe, expect, it } from "vitest"; |
2 | | -import { createTerminal, supportsMouseInput } from "../src/index.js"; |
| 2 | +import { createTerminal, supportsMouseInput, type SnapshotCell, type TerminalSnapshot } from "../src/index.js"; |
3 | 3 | import { loadNative } from "../src/loader.js"; |
4 | 4 | import type { CreateTerminalOptions } from "../src/types.js"; |
5 | 5 | import { nativeSupport } from "./native-support.js"; |
6 | 6 |
|
7 | 7 | const support = nativeSupport(); |
8 | 8 | const describeIfNative = support.available ? describe : describe.skip; |
9 | 9 |
|
| 10 | +function cellsByText(snapshot: TerminalSnapshot): Map<string, SnapshotCell> { |
| 11 | + const cells = new Map<string, SnapshotCell>(); |
| 12 | + for (const cell of snapshot.cells ?? []) { |
| 13 | + expect(cells.has(cell.text), `duplicate cell text ${cell.text}`).toBe(false); |
| 14 | + cells.set(cell.text, cell); |
| 15 | + } |
| 16 | + return cells; |
| 17 | +} |
| 18 | + |
10 | 19 | const unitGeometry = { |
11 | 20 | screenWidth: 80, |
12 | 21 | screenHeight: 24, |
@@ -51,6 +60,79 @@ describeIfNative("GhosttyVtTerminal", () => { |
51 | 60 | } |
52 | 61 | }); |
53 | 62 |
|
| 63 | + describe.each([ |
| 64 | + { name: "inverse", set: 7, reset: 27 }, |
| 65 | + { name: "strikethrough", set: 9, reset: 29 }, |
| 66 | + { name: "faint", set: 2, reset: 22 }, |
| 67 | + { name: "invisible", set: 8, reset: 28 }, |
| 68 | + ] as const)("SGR $set/$reset $name cell attribute", ({ name, set, reset }) => { |
| 69 | + it("reports the attribute when set and omits it after its reset or SGR 0", () => { |
| 70 | + const term = createTerminal({ cols: 80, rows: 24 }); |
| 71 | + try { |
| 72 | + term.feed(`P\x1b[${set}mA\x1b[${reset}mB\x1b[${set}mC\x1b[0mD`); |
| 73 | + const cells = cellsByText(term.snapshot({ includeCells: true })); |
| 74 | + expect(cells.get("P")).not.toHaveProperty(name); |
| 75 | + expect(cells.get("A")?.[name]).toBe(true); |
| 76 | + expect(cells.get("B")).not.toHaveProperty(name); |
| 77 | + expect(cells.get("C")?.[name]).toBe(true); |
| 78 | + expect(cells.get("D")).not.toHaveProperty(name); |
| 79 | + } finally { |
| 80 | + term.dispose(); |
| 81 | + } |
| 82 | + }); |
| 83 | + }); |
| 84 | + |
| 85 | + it("reports combined SGR attributes on one cell", () => { |
| 86 | + const term = createTerminal({ cols: 80, rows: 24 }); |
| 87 | + try { |
| 88 | + term.feed("\x1b[1;2;3;4;7;8;9mX\x1b[0m"); |
| 89 | + const cell = cellsByText(term.snapshot({ includeCells: true })).get("X"); |
| 90 | + expect(cell).toMatchObject({ |
| 91 | + bold: true, |
| 92 | + faint: true, |
| 93 | + italic: true, |
| 94 | + underline: true, |
| 95 | + inverse: true, |
| 96 | + invisible: true, |
| 97 | + strikethrough: true, |
| 98 | + }); |
| 99 | + } finally { |
| 100 | + term.dispose(); |
| 101 | + } |
| 102 | + }); |
| 103 | + |
| 104 | + it("reports inverse as a raw attribute without swapping colors", () => { |
| 105 | + const term = createTerminal({ cols: 80, rows: 24 }); |
| 106 | + try { |
| 107 | + term.feed("\x1b[31;42mN\x1b[7mI\x1b[0m"); |
| 108 | + const cells = cellsByText(term.snapshot({ includeCells: true })); |
| 109 | + const normal = cells.get("N"); |
| 110 | + const inverse = cells.get("I"); |
| 111 | + expect(normal?.foreground).toMatch(/^#[0-9a-f]{6}$/i); |
| 112 | + expect(normal?.background).toMatch(/^#[0-9a-f]{6}$/i); |
| 113 | + expect(normal?.foreground).not.toBe(normal?.background); |
| 114 | + expect(inverse?.inverse).toBe(true); |
| 115 | + expect(inverse?.foreground).toBe(normal?.foreground); |
| 116 | + expect(inverse?.background).toBe(normal?.background); |
| 117 | + } finally { |
| 118 | + term.dispose(); |
| 119 | + } |
| 120 | + }); |
| 121 | + |
| 122 | + it("reports DECTCEM cursor visibility", () => { |
| 123 | + const term = createTerminal({ cols: 80, rows: 24 }); |
| 124 | + try { |
| 125 | + expect(term.snapshot().cursorVisible).toBe(true); |
| 126 | + term.feed("\x1b[?25l"); |
| 127 | + expect(term.snapshot().cursorVisible).toBe(false); |
| 128 | + expect(term.snapshot({ includeCells: true }).cursorVisible).toBe(false); |
| 129 | + term.feed("\x1b[?25h"); |
| 130 | + expect(term.snapshot().cursorVisible).toBe(true); |
| 131 | + } finally { |
| 132 | + term.dispose(); |
| 133 | + } |
| 134 | + }); |
| 135 | + |
54 | 136 | it("updates snapshot dimensions on resize", () => { |
55 | 137 | const term = createTerminal({ cols: 80, rows: 24 }); |
56 | 138 | try { |
|
0 commit comments