Buckets:
| import { describe, test, expect, beforeAll, afterEach } from "bun:test" | |
| import { Terminal, Ghostty } from "ghostty-web" | |
| import { SerializeAddon } from "./serialize" | |
| let ghostty: Ghostty | |
| beforeAll(async () => { | |
| ghostty = await Ghostty.load() | |
| }) | |
| const terminals: Terminal[] = [] | |
| afterEach(() => { | |
| for (const term of terminals) { | |
| term.dispose() | |
| } | |
| terminals.length = 0 | |
| document.body.innerHTML = "" | |
| }) | |
| function createTerminal(cols = 80, rows = 24): { term: Terminal; addon: SerializeAddon; container: HTMLElement } { | |
| const container = document.createElement("div") | |
| document.body.appendChild(container) | |
| const term = new Terminal({ cols, rows, ghostty }) | |
| const addon = new SerializeAddon() | |
| term.loadAddon(addon) | |
| term.open(container) | |
| terminals.push(term) | |
| return { term, addon, container } | |
| } | |
| function writeAndWait(term: Terminal, data: string): Promise<void> { | |
| return new Promise((resolve) => { | |
| term.write(data, resolve) | |
| }) | |
| } | |
| describe("SerializeAddon", () => { | |
| describe("ANSI color preservation", () => { | |
| test("should preserve text attributes (bold, italic, underline)", async () => { | |
| const { term, addon } = createTerminal() | |
| const input = "\x1b[1mBOLD\x1b[0m \x1b[3mITALIC\x1b[0m \x1b[4mUNDER\x1b[0m" | |
| await writeAndWait(term, input) | |
| const origLine = term.buffer.active.getLine(0) | |
| expect(origLine!.getCell(0)!.isBold()).toBe(1) | |
| expect(origLine!.getCell(5)!.isItalic()).toBe(1) | |
| expect(origLine!.getCell(12)!.isUnderline()).toBe(1) | |
| const serialized = addon.serialize({ range: { start: 0, end: 0 } }) | |
| const { term: term2 } = createTerminal() | |
| terminals.push(term2) | |
| await writeAndWait(term2, serialized) | |
| const line = term2.buffer.active.getLine(0) | |
| const boldCell = line!.getCell(0) | |
| expect(boldCell!.getChars()).toBe("B") | |
| expect(boldCell!.isBold()).toBe(1) | |
| const italicCell = line!.getCell(5) | |
| expect(italicCell!.getChars()).toBe("I") | |
| expect(italicCell!.isItalic()).toBe(1) | |
| const underCell = line!.getCell(12) | |
| expect(underCell!.getChars()).toBe("U") | |
| expect(underCell!.isUnderline()).toBe(1) | |
| }) | |
| test("should preserve basic 16-color foreground colors", async () => { | |
| const { term, addon } = createTerminal() | |
| const input = "\x1b[31mRED\x1b[32mGREEN\x1b[34mBLUE\x1b[0mNORMAL" | |
| await writeAndWait(term, input) | |
| const origLine = term.buffer.active.getLine(0) | |
| const origRedFg = origLine!.getCell(0)!.getFgColor() | |
| const origGreenFg = origLine!.getCell(3)!.getFgColor() | |
| const origBlueFg = origLine!.getCell(8)!.getFgColor() | |
| const serialized = addon.serialize({ range: { start: 0, end: 0 } }) | |
| const { term: term2 } = createTerminal() | |
| terminals.push(term2) | |
| await writeAndWait(term2, serialized) | |
| const line = term2.buffer.active.getLine(0) | |
| expect(line).toBeDefined() | |
| const redCell = line!.getCell(0) | |
| expect(redCell!.getChars()).toBe("R") | |
| expect(redCell!.getFgColor()).toBe(origRedFg) | |
| const greenCell = line!.getCell(3) | |
| expect(greenCell!.getChars()).toBe("G") | |
| expect(greenCell!.getFgColor()).toBe(origGreenFg) | |
| const blueCell = line!.getCell(8) | |
| expect(blueCell!.getChars()).toBe("B") | |
| expect(blueCell!.getFgColor()).toBe(origBlueFg) | |
| }) | |
| test("should preserve 256-color palette colors", async () => { | |
| const { term, addon } = createTerminal() | |
| const input = "\x1b[38;5;196mRED256\x1b[0mNORMAL" | |
| await writeAndWait(term, input) | |
| const origLine = term.buffer.active.getLine(0) | |
| const origRedFg = origLine!.getCell(0)!.getFgColor() | |
| const serialized = addon.serialize({ range: { start: 0, end: 0 } }) | |
| const { term: term2 } = createTerminal() | |
| terminals.push(term2) | |
| await writeAndWait(term2, serialized) | |
| const line = term2.buffer.active.getLine(0) | |
| const redCell = line!.getCell(0) | |
| expect(redCell!.getChars()).toBe("R") | |
| expect(redCell!.getFgColor()).toBe(origRedFg) | |
| }) | |
| test("should preserve RGB/truecolor colors", async () => { | |
| const { term, addon } = createTerminal() | |
| const input = "\x1b[38;2;255;128;64mRGB_TEXT\x1b[0mNORMAL" | |
| await writeAndWait(term, input) | |
| const origLine = term.buffer.active.getLine(0) | |
| const origRgbFg = origLine!.getCell(0)!.getFgColor() | |
| const serialized = addon.serialize({ range: { start: 0, end: 0 } }) | |
| const { term: term2 } = createTerminal() | |
| terminals.push(term2) | |
| await writeAndWait(term2, serialized) | |
| const line = term2.buffer.active.getLine(0) | |
| const rgbCell = line!.getCell(0) | |
| expect(rgbCell!.getChars()).toBe("R") | |
| expect(rgbCell!.getFgColor()).toBe(origRgbFg) | |
| }) | |
| test("should preserve background colors", async () => { | |
| const { term, addon } = createTerminal() | |
| const input = "\x1b[48;2;255;0;0mRED_BG\x1b[48;2;0;255;0mGREEN_BG\x1b[0mNORMAL" | |
| await writeAndWait(term, input) | |
| const origLine = term.buffer.active.getLine(0) | |
| const origRedBg = origLine!.getCell(0)!.getBgColor() | |
| const origGreenBg = origLine!.getCell(6)!.getBgColor() | |
| const serialized = addon.serialize({ range: { start: 0, end: 0 } }) | |
| const { term: term2 } = createTerminal() | |
| terminals.push(term2) | |
| await writeAndWait(term2, serialized) | |
| const line = term2.buffer.active.getLine(0) | |
| const redBgCell = line!.getCell(0) | |
| expect(redBgCell!.getChars()).toBe("R") | |
| expect(redBgCell!.getBgColor()).toBe(origRedBg) | |
| const greenBgCell = line!.getCell(6) | |
| expect(greenBgCell!.getChars()).toBe("G") | |
| expect(greenBgCell!.getBgColor()).toBe(origGreenBg) | |
| }) | |
| test("should handle combined colors and attributes", async () => { | |
| const { term, addon } = createTerminal() | |
| const input = | |
| "\x1b[1;38;2;255;0;0;48;2;255;255;0mCOMBO\x1b[0mNORMAL " | |
| await writeAndWait(term, input) | |
| const origLine = term.buffer.active.getLine(0) | |
| const _origFg = origLine!.getCell(0)!.getFgColor() | |
| const _origBg = origLine!.getCell(0)!.getBgColor() | |
| expect(origLine!.getCell(0)!.isBold()).toBe(1) | |
| const serialized = addon.serialize({ range: { start: 0, end: 0 } }) | |
| const cleanSerialized = serialized.replace(/\x1b\[\d+X/g, "") | |
| expect(cleanSerialized.startsWith("\x1b[1;")).toBe(true) | |
| const { term: term2 } = createTerminal() | |
| terminals.push(term2) | |
| await writeAndWait(term2, cleanSerialized) | |
| const line = term2.buffer.active.getLine(0) | |
| const comboCell = line!.getCell(0) | |
| expect(comboCell!.getChars()).toBe("C") | |
| expect(cleanSerialized).toContain("\x1b[1;38;2;255;0;0;48;2;255;255;0m") | |
| }) | |
| }) | |
| describe("round-trip serialization", () => { | |
| test("should not produce ECH sequences", async () => { | |
| const { term, addon } = createTerminal() | |
| await writeAndWait(term, "\x1b[31mHello\x1b[0m World") | |
| const serialized = addon.serialize() | |
| const hasECH = /\x1b\[\d+X/.test(serialized) | |
| expect(hasECH).toBe(false) | |
| }) | |
| test("multi-line content should not have garbage characters", async () => { | |
| const { term, addon } = createTerminal() | |
| const content = [ | |
| "\x1b[1;32m❯\x1b[0m \x1b[34mcd\x1b[0m /some/path", | |
| "\x1b[1;32m❯\x1b[0m \x1b[34mls\x1b[0m -la", | |
| "total 42", | |
| ].join("\r\n") | |
| await writeAndWait(term, content) | |
| const serialized = addon.serialize() | |
| expect(/\x1b\[\d+X/.test(serialized)).toBe(false) | |
| const { term: term2 } = createTerminal() | |
| terminals.push(term2) | |
| await writeAndWait(term2, serialized) | |
| for (let row = 0; row < 3; row++) { | |
| const line = term2.buffer.active.getLine(row)?.translateToString(true) | |
| expect(line?.includes("𑼝")).toBe(false) | |
| } | |
| expect(term2.buffer.active.getLine(0)?.translateToString(true)).toContain("cd /some/path") | |
| expect(term2.buffer.active.getLine(1)?.translateToString(true)).toContain("ls -la") | |
| expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42") | |
| }) | |
| test("serialized output should restore after Terminal.reset()", async () => { | |
| const { term, addon } = createTerminal() | |
| const content = [ | |
| "\x1b[1;32m❯\x1b[0m \x1b[34mcd\x1b[0m /some/path", | |
| "\x1b[1;32m❯\x1b[0m \x1b[34mls\x1b[0m -la", | |
| "total 42", | |
| ].join("\r\n") | |
| await writeAndWait(term, content) | |
| const serialized = addon.serialize() | |
| const { term: term2 } = createTerminal() | |
| terminals.push(term2) | |
| term2.reset() | |
| await writeAndWait(term2, serialized) | |
| expect(term2.buffer.active.getLine(0)?.translateToString(true)).toContain("cd /some/path") | |
| expect(term2.buffer.active.getLine(1)?.translateToString(true)).toContain("ls -la") | |
| expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42") | |
| }) | |
| test("alternate buffer should round-trip without garbage", async () => { | |
| const { term, addon } = createTerminal(20, 5) | |
| await writeAndWait(term, "normal\r\n") | |
| await writeAndWait(term, "\x1b[?1049h\x1b[HALT") | |
| expect(term.buffer.active.type).toBe("alternate") | |
| const serialized = addon.serialize() | |
| const { term: term2 } = createTerminal(20, 5) | |
| terminals.push(term2) | |
| await writeAndWait(term2, serialized) | |
| expect(term2.buffer.active.type).toBe("alternate") | |
| const line = term2.buffer.active.getLine(0) | |
| expect(line?.translateToString(true)).toBe("ALT") | |
| // Ensure a cell beyond content isn't garbage | |
| const cellCode = line?.getCell(10)?.getCode() | |
| expect(cellCode === 0 || cellCode === 32).toBe(true) | |
| }) | |
| test("serialized output written to new terminal should match original colors", async () => { | |
| const { term, addon } = createTerminal(40, 5) | |
| const input = "\x1b[38;2;255;0;0mHello\x1b[0m \x1b[38;2;0;255;0mWorld\x1b[0m! " | |
| await writeAndWait(term, input) | |
| const origLine = term.buffer.active.getLine(0) | |
| const origHelloFg = origLine!.getCell(0)!.getFgColor() | |
| const origWorldFg = origLine!.getCell(6)!.getFgColor() | |
| const serialized = addon.serialize({ range: { start: 0, end: 0 } }) | |
| const { term: term2 } = createTerminal(40, 5) | |
| terminals.push(term2) | |
| await writeAndWait(term2, serialized) | |
| const newLine = term2.buffer.active.getLine(0) | |
| expect(newLine!.getCell(0)!.getChars()).toBe("H") | |
| expect(newLine!.getCell(0)!.getFgColor()).toBe(origHelloFg) | |
| expect(newLine!.getCell(6)!.getChars()).toBe("W") | |
| expect(newLine!.getCell(6)!.getFgColor()).toBe(origWorldFg) | |
| expect(newLine!.getCell(11)!.getChars()).toBe("!") | |
| }) | |
| }) | |
| }) | |
Xet Storage Details
- Size:
- 10.9 kB
- Xet hash:
- cd42b6d849f5a31f5f14ecf113e19d07772f202493e429937e2d877a1fa5c131
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.