Add integration tests for reconnect scenarios

Test that initial_state provides full terminal state, multiple
sessions are independent, and the CSI 'T' artifact bug is fixed.
This commit is contained in:
Jared Miller 2026-01-31 12:02:00 -05:00
parent baa6ef9d70
commit 4502be8d06
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C

View file

@ -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(/&nbsp;/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(/&nbsp;/g, " ")
.trim();
// Should not contain literal CSI final bytes
expect(textContent).not.toMatch(/[HJKTS](?!.*Clean)/);
disposeTerminal(term);
});
});
});