diff --git a/src/terminal.test.ts b/src/terminal.test.ts index 2f6c681..8f2f118 100644 --- a/src/terminal.test.ts +++ b/src/terminal.test.ts @@ -164,4 +164,105 @@ describe("terminal", () => { disposeTerminal(term); }); + + describe("reconnect scenarios", () => { + test("initial_state provides full terminal state for reconnecting clients", async () => { + const term = createTerminal(80, 24); + + // Simulate some output that a client missed + await writeSync(term, "Line 1\r\n"); + await writeSync(term, "Line 2\r\n"); + await writeSync(term, "\x1b[31mRed line\x1b[0m\r\n"); + + // Serialize the full state (as sent in initial_state event) + const html = serializeAsHTML(term); + + // Verify all content is present + expect(html).toContain("Line 1"); + expect(html).toContain("Line 2"); + expect(html).toContain("Red line"); + + // Verify ANSI styling is preserved + expect(html).toMatch(/color|red|#[a-f0-9]{6}/i); + + disposeTerminal(term); + }); + + test("multiple sessions have independent terminal emulators", async () => { + const term1 = createTerminal(80, 24); + const term2 = createTerminal(80, 24); + + // Write different content to each + await writeSync(term1, "Session 1 output"); + await writeSync(term2, "Session 2 output"); + + const html1 = serializeAsHTML(term1); + const html2 = serializeAsHTML(term2); + + // Each should have only its own content + expect(html1).toContain("Session 1 output"); + expect(html1).not.toContain("Session 2 output"); + expect(html2).toContain("Session 2 output"); + expect(html2).not.toContain("Session 1 output"); + + disposeTerminal(term1); + disposeTerminal(term2); + }); + + test("T artifact bug is fixed (CSI sequences with T final byte)", async () => { + const term = createTerminal(80, 24); + + // CSI sequences ending in 'T' (scroll down) were leaking through as literal 'T' + // This was a bug in our old ANSI parser + await writeSync(term, "Before\x1b[1T"); // Scroll down 1 line + await writeSync(term, "After"); + + const html = serializeAsHTML(term); + + // Should not contain literal 'T' from the escape sequence + // Only 'Before' and 'After' should be visible + expect(html).toContain("Before"); + expect(html).toContain("After"); + + // Extract text content without HTML tags + const textContent = html + .replace(/<[^>]+>/g, "") + .replace(/ /g, " ") + .trim(); + + // Should not have standalone 'T' characters + expect(textContent).not.toMatch(/\bT\b/); + + disposeTerminal(term); + }); + + test("complex ANSI sequences don't leak artifacts", async () => { + const term = createTerminal(80, 24); + + // Test various CSI final bytes that could leak as literals + await writeSync(term, "Start\r\n"); + await writeSync(term, "\x1b[2J"); // Clear screen (J) + await writeSync(term, "\x1b[H"); // Cursor home (H) + await writeSync(term, "\x1b[K"); // Clear to end of line (K) + await writeSync(term, "\x1b[1T"); // Scroll down (T) + await writeSync(term, "\x1b[1S"); // Scroll up (S) + await writeSync(term, "Clean output"); + + const html = serializeAsHTML(term); + + // Should only contain the actual text + expect(html).toContain("Clean output"); + + // Extract text without HTML + const textContent = html + .replace(/<[^>]+>/g, "") + .replace(/ /g, " ") + .trim(); + + // Should not contain literal CSI final bytes + expect(textContent).not.toMatch(/[HJKTS](?!.*Clean)/); + + disposeTerminal(term); + }); + }); });