/* Copyright 2026 Marimo. All rights reserved. */ import { describe, expect, test } from "vitest"; import { AnsiParser, AnsiReducer, StatefulOutputMessage, TerminalBuffer, } from "../ansi-reduce"; describe("TerminalBuffer", () => { test("writeChar writes single character", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); expect(buffer.render()).toMatchInlineSnapshot(`"a"`); }); test("writeChar writes multiple characters", () => { const buffer = new TerminalBuffer(); buffer.writeChar("H"); buffer.writeChar("e"); buffer.writeChar("l"); buffer.writeChar("l"); buffer.writeChar("o"); expect(buffer.render()).toMatchInlineSnapshot(`"Hello"`); }); test("writeChar overwrites at cursor position", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.writeChar("b"); buffer.writeChar("c"); // Move cursor back and overwrite buffer.handleEscape("\u001B[2D"); // Move left 2 buffer.writeChar("X"); buffer.writeChar("Y"); expect(buffer.render()).toMatchInlineSnapshot(`"aXY"`); }); test("control handles newline", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.control("\n"); buffer.writeChar("b"); expect(buffer.render()).toMatchInlineSnapshot(` "a b" `); }); test("control handles tab", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.control("\t"); buffer.writeChar("b"); expect(buffer.render()).toMatchInlineSnapshot(`"a b"`); }); test("control handles vertical tab", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.writeChar("b"); buffer.control("\v"); buffer.writeChar("c"); expect(buffer.render()).toMatchInlineSnapshot(` "ab c" `); // This means we expect the "c" to be the column _after_ the "b" }); test("control handles backspace", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.writeChar("b"); buffer.control("\b"); buffer.writeChar("X"); expect(buffer.render()).toMatchInlineSnapshot(`"aX"`); }); test("control handles backspace at start of line", () => { const buffer = new TerminalBuffer(); buffer.control("\b"); buffer.writeChar("a"); expect(buffer.render()).toMatchInlineSnapshot(`"a"`); }); test("control handles carriage return", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.writeChar("b"); buffer.writeChar("c"); buffer.control("\r"); buffer.writeChar("X"); buffer.writeChar("Y"); expect(buffer.render()).toMatchInlineSnapshot(`"XYc"`); }); test("handleEscape cursor up", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.control("\n"); buffer.writeChar("b"); buffer.handleEscape("\u001B[1A"); // Move up 1 buffer.writeChar("X"); expect(buffer.render()).toMatchInlineSnapshot(`"aX"`); }); test("handleEscape cursor down", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.handleEscape("\u001B[1B"); // Move down 1 buffer.writeChar("b"); expect(buffer.render()).toMatchInlineSnapshot(` "a b" `); }); test("handleEscape cursor forward", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.handleEscape("\u001B[3C"); // Move right 3 buffer.writeChar("b"); expect(buffer.render()).toMatchInlineSnapshot(`"a b"`); }); test("handleEscape cursor back", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.writeChar("b"); buffer.writeChar("c"); buffer.handleEscape("\u001B[2D"); // Move left 2 buffer.writeChar("X"); expect(buffer.render()).toMatchInlineSnapshot(`"aXc"`); }); test("handleEscape cursor home with params", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.control("\n"); buffer.writeChar("b"); buffer.handleEscape("\u001B[1;1H"); // Move to (1,1) which is (0,0) in 0-indexed buffer.writeChar("X"); expect(buffer.render()).toMatchInlineSnapshot(` "X b" `); }); test("handleEscape cursor home without params", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.writeChar("b"); buffer.handleEscape("\u001B[H"); // Move to home buffer.writeChar("X"); expect(buffer.render()).toMatchInlineSnapshot(`"Xb"`); }); test("handleEscape erase display (2J)", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.control("\n"); buffer.writeChar("b"); buffer.handleEscape("\u001B[2J"); // Clear screen buffer.writeChar("X"); expect(buffer.render()).toMatchInlineSnapshot(`"X"`); }); test("handleEscape erase line to end (0K)", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.writeChar("b"); buffer.writeChar("c"); buffer.handleEscape("\u001B[2D"); // Move left 2 buffer.handleEscape("\u001B[0K"); // Clear to end expect(buffer.render()).toMatchInlineSnapshot(`"a"`); }); test("handleEscape erase line to start (1K)", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.writeChar("b"); buffer.writeChar("c"); buffer.handleEscape("\u001B[2D"); // Move left 2 buffer.handleEscape("\u001B[1K"); // Clear to start expect(buffer.render()).toMatchInlineSnapshot(`" bc"`); }); test("handleEscape erase entire line (2K)", () => { const buffer = new TerminalBuffer(); buffer.writeChar("a"); buffer.writeChar("b"); buffer.writeChar("c"); buffer.handleEscape("\u001B[2K"); // Clear entire line expect(buffer.render()).toMatchInlineSnapshot(`""`); }); }); describe("AnsiParser", () => { test("parse plain text", () => { const parser = new AnsiParser(); const tokens = parser.parse("hello"); expect(tokens).toMatchInlineSnapshot(` [ { "type": "text", "value": "hello", }, ] `); }); test("parse text with escape sequence", () => { const parser = new AnsiParser(); const tokens = parser.parse("hello\u001B[1Aworld"); expect(tokens).toMatchInlineSnapshot(` [ { "type": "text", "value": "hello", }, { "type": "escape", "value": "", }, { "type": "text", "value": "world", }, ] `); }); test("parse multiple escape sequences", () => { const parser = new AnsiParser(); const tokens = parser.parse("\u001B[1A\u001B[2C\u001B[0K"); expect(tokens).toMatchInlineSnapshot(` [ { "type": "escape", "value": "", }, { "type": "escape", "value": "", }, { "type": "escape", "value": "", }, ] `); }); test("parse escape with multiple parameters", () => { const parser = new AnsiParser(); const tokens = parser.parse("\u001B[10;20H"); expect(tokens).toMatchInlineSnapshot(` [ { "type": "escape", "value": "", }, ] `); }); test("parse empty string", () => { const parser = new AnsiParser(); const tokens = parser.parse(""); expect(tokens).toMatchInlineSnapshot("[]"); }); }); describe("AnsiReducer", () => { test("reduce plain text", () => { const reducer = new AnsiReducer(); const result = reducer.reduce("hello world"); expect(result).toMatchInlineSnapshot(`"hello world"`); }); test("reduce text with newlines", () => { const reducer = new AnsiReducer(); const result = reducer.reduce("line1\nline2\nline3"); expect(result).toMatchInlineSnapshot(` "line1 line2 line3" `); }); test("reduce progress bar simulation", () => { const reducer = new AnsiReducer(); const result = reducer.reduce( "Progress: 10%\rProgress: 50%\rProgress: 100%", ); expect(result).toMatchInlineSnapshot(`"Progress: 100%"`); }); test("reduce spinner simulation", () => { const reducer = new AnsiReducer(); const result = reducer.reduce( "Loading |\rLoading /\rLoading -\rLoading \\", ); expect(result).toMatchInlineSnapshot(`"Loading \\"`); }); test("reduce cursor movement", () => { const reducer = new AnsiReducer(); const result = reducer.reduce("Hello\u001B[5DWorld"); expect(result).toMatchInlineSnapshot(`"World"`); }); test("reduce clear line", () => { const reducer = new AnsiReducer(); const result = reducer.reduce("Hello World\u001B[2K"); expect(result).toMatchInlineSnapshot(`""`); }); test("reduce clear screen", () => { const reducer = new AnsiReducer(); const result = reducer.reduce("Line 1\nLine 2\n\u001B[2JNew Start"); expect(result).toMatchInlineSnapshot(`"New Start"`); }); test("reduce complex cursor positioning", () => { const reducer = new AnsiReducer(); const result = reducer.reduce("abc\u001B[1;2Hxy"); expect(result).toMatchInlineSnapshot(`"axy"`); }); test("reduce ignores control characters below space", () => { const reducer = new AnsiReducer(); const result = reducer.reduce("hello\u0000\u0001\u0007world"); expect(result).toMatchInlineSnapshot(`"helloworld"`); }); test("reduce handles carriage return without newline", () => { const reducer = new AnsiReducer(); const result = reducer.reduce("AAAA\rBB"); expect(result).toMatchInlineSnapshot(`"BBAA"`); }); }); function ansiReduce(input: string): string { const reducer = new AnsiReducer(); return reducer.reduce(input); } describe("ansiReduce", () => { test("basic usage", () => { const result = ansiReduce("Hello World"); expect(result).toMatchInlineSnapshot(`"Hello World"`); }); test("progress bar example", () => { const result = ansiReduce( "[ ] 0%\r[== ] 20%\r[==== ] 40%\r[====== ] 60%\r[======== ] 80%\r[==========] 100%", ); expect(result).toMatchInlineSnapshot(`"[==========] 100%"`); }); test("multi-line with cursor movement", () => { const result = ansiReduce("Line 1\nLine 2\nLine 3\u001B[2AModified"); expect(result).toMatchInlineSnapshot(`"Line 1Modified"`); }); test("erase and rewrite", () => { const result = ansiReduce("Old text\u001B[2KNew text"); expect(result).toMatchInlineSnapshot(`" New text"`); }); test("empty input", () => { const result = ansiReduce(""); expect(result).toMatchInlineSnapshot(`""`); }); test("newlines only", () => { const result = ansiReduce("\n\n\n"); expect(result).toMatchInlineSnapshot(` " " `); }); test("real-world tqdm-like progress", () => { const result = ansiReduce( "Processing: | | 0/100\r" + "Processing: |█ | 10/100\r" + "Processing: |██ | 20/100\r" + "Processing: |██████████| 100/100", ); expect(result).toMatchInlineSnapshot(`"Processing: |██████████| 100/100"`); }); test("cursor positioning with absolute coordinates", () => { const result = ansiReduce("\u001B[1;1Ha\u001B[2;2Hb\u001B[3;3Hc"); expect(result).toMatchInlineSnapshot(` "a b c" `); }); test("partial line erase from cursor to end", () => { const result = ansiReduce("Hello World\u001B[6D\u001B[0K!"); expect(result).toMatchInlineSnapshot(`"Hello!"`); }); test("partial line erase from start to cursor", () => { const result = ansiReduce("Hello World\u001B[6D\u001B[1K!"); expect(result).toMatchInlineSnapshot(`" !World"`); }); test("complex progress simulation", () => { const text = "\r 0%| | 0/101000 [00:00 { const text = "\r 0%| | 0/101000 [00:00 { test("append simple text incrementally", () => { const reducer = new AnsiReducer(); reducer.append("Hello "); reducer.append("World"); expect(reducer.render()).toMatchInlineSnapshot(`"Hello World"`); }); test("append with progress bar simulation", () => { const reducer = new AnsiReducer(); reducer.append("Progress: 0%"); reducer.append("\rProgress: 50%"); reducer.append("\rProgress: 100%"); expect(reducer.render()).toMatchInlineSnapshot(`"Progress: 100%"`); }); test("append with newlines", () => { const reducer = new AnsiReducer(); reducer.append("Line 1\n"); reducer.append("Line 2\n"); reducer.append("Line 3"); expect(reducer.render()).toMatchInlineSnapshot(` "Line 1 Line 2 Line 3" `); }); test("append with cursor movements", () => { const reducer = new AnsiReducer(); reducer.append("abc\n"); reducer.append("def\n"); reducer.append("\u001B[1A"); // Move up one line reducer.append("XYZ"); expect(reducer.render()).toMatchInlineSnapshot(` "abc XYZ" `); }); test("reset clears state", () => { const reducer = new AnsiReducer(); reducer.append("Old content"); expect(reducer.render()).toMatchInlineSnapshot(`"Old content"`); reducer.reset(); reducer.append("New content"); expect(reducer.render()).toMatchInlineSnapshot(`"New content"`); }); test("reduce() resets before processing", () => { const reducer = new AnsiReducer(); reducer.append("Old content"); const result = reducer.reduce("New content"); expect(result).toMatchInlineSnapshot(`"New content"`); }); test("streaming tqdm-like progress", () => { const reducer = new AnsiReducer(); // Simulate streaming progress updates reducer.append("Processing: | | 0/100\r"); reducer.append("Processing: |██ | 20/100\r"); reducer.append("Processing: |████ | 40/100\r"); reducer.append("Processing: |██████ | 60/100\r"); reducer.append("Processing: |████████ | 80/100\r"); reducer.append("Processing: |██████████| 100/100"); expect(reducer.render()).toMatchInlineSnapshot( `"Processing: |██████████| 100/100"`, ); }); test("append empty string does nothing", () => { const reducer = new AnsiReducer(); reducer.append("Hello"); reducer.append(""); expect(reducer.render()).toMatchInlineSnapshot(`"Hello"`); }); test("append with mixed content and escapes", () => { const reducer = new AnsiReducer(); reducer.append("Loading"); reducer.append(" |"); reducer.append("\rLoading /"); reducer.append("\rLoading -"); reducer.append("\rLoading \\"); reducer.append("\rDone! "); expect(reducer.render()).toMatchInlineSnapshot(`"Done! "`); }); }); describe("AnsiReducer color preservation", () => { const CASES = [ // SGR sequences "\u001B[34mBlue text\u001B[m normal text\u001B[31mRed text\u001B[0m", // Complex SGR with parameters "\u001B[1;31mBold Red\u001B[0m \u001B[48;5;240mGray bg\u001B[0m", // Character set selection "Text\u001B(BMore\u001B(0Graphics\u001B(B", // Complex case "\u001B[34m[D 251201 15:32:24 cell_runner:695]\u001B(B\u001B[m Running post_execution hooks in context\n\u001B[34m[D 251201 15:32:24 hooks_post_execution:65]\u001B(B\u001B[m Acquiring graph lock to update cell import workspace\n\u001B[34m[D 251201 15:32:24 hooks_post_execution:67]\u001B(B\u001B[m Acquired graph lock to update import workspace.\n", ]; test.each(CASES)("preserves ANSI color codes", (input) => { const reducer = new AnsiReducer(); const result = reducer.reduce(input); expect(result).toBe(input); }); test("preserves color codes with cursor movements", () => { const reducer = new AnsiReducer(); // Test that color codes work alongside cursor movements // Note: when cursor moves up, lines below are discarded (tqdm behavior) const result = reducer.reduce( "Line1\n\u001B[31mRed\u001B[0m\u001B[1A\u001B[32mGreen\u001B[0m", ); // After moving up from row 1 to row 0, row 1 is discarded // Green is written at the end of row 0 expect(result).toMatchInlineSnapshot( `"Line1 \u001B[32mGreen\u001B[0m"`, ); }); }); describe("StatefulOutputMessage", () => { test("initializes and processes text", () => { const message = { mimetype: "text/plain", channel: "stdout", timestamp: 123, data: "Hello", } as const; const stateful = StatefulOutputMessage.create(message); expect(stateful.mimetype).toBe("text/plain"); expect(stateful.channel).toBe("stdout"); expect(stateful.data).toBe("Hello"); }); test("appendData appends text", () => { const message = { mimetype: "text/plain", channel: "stdout", timestamp: 0, data: "Hello", } as const; let stateful = StatefulOutputMessage.create(message); stateful = stateful.appendData(" World"); expect(stateful.data).toBe("Hello World"); }); test("appendData handles progress bars", () => { const message = { mimetype: "text/plain", channel: "stdout", timestamp: 0, data: "Progress: 0%", } as const; let stateful = StatefulOutputMessage.create(message); stateful = stateful.appendData("\rProgress: 50%"); stateful = stateful.appendData("\rProgress: 100%"); expect(stateful.data).toBe("Progress: 100%"); }); test("appendData maintains ANSI state", () => { const message = { mimetype: "text/plain", channel: "stdout", timestamp: 0, data: "Line 1\n", } as const; let stateful = StatefulOutputMessage.create(message); stateful = stateful.appendData("Line 2\n"); stateful = stateful.appendData("\u001B[1A"); // Move up stateful = stateful.appendData("X"); expect(stateful.data).toMatchInlineSnapshot(` "Line 1 Xine 2" `); }); });