summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/addons/serialize.test.ts
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-22 19:38:50 -0600
committerAdam <[email protected]>2025-12-22 19:39:00 -0600
commit794fe8f381c846f5241800363023d892c12cf495 (patch)
treebff98689edfa635a2a9f39cb4ea61639b97f5b2d /packages/app/src/addons/serialize.test.ts
parenta4eebf9f08262f6bf63017710e2e6d9672ec6708 (diff)
downloadopencode-794fe8f381c846f5241800363023d892c12cf495.tar.gz
opencode-794fe8f381c846f5241800363023d892c12cf495.zip
chore: rename packages/desktop -> packages/app
Diffstat (limited to 'packages/app/src/addons/serialize.test.ts')
-rw-r--r--packages/app/src/addons/serialize.test.ts272
1 files changed, 272 insertions, 0 deletions
diff --git a/packages/app/src/addons/serialize.test.ts b/packages/app/src/addons/serialize.test.ts
new file mode 100644
index 000000000..ad165f43f
--- /dev/null
+++ b/packages/app/src/addons/serialize.test.ts
@@ -0,0 +1,272 @@
+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 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("!")
+ })
+ })
+})