test(tui): cover style reset cases

This commit is contained in:
Mario Zechner 2026-01-11 19:01:05 +01:00
parent ae134e7e02
commit da6a2fb5ea
2 changed files with 120 additions and 0 deletions

View file

@ -1,12 +1,25 @@
import assert from "node:assert";
import { describe, it } from "node:test";
import type { Terminal as XtermTerminalType } from "@xterm/headless";
import { Chalk } from "chalk";
import { Markdown } from "../src/components/markdown.js";
import { type Component, TUI } from "../src/tui.js";
import { defaultMarkdownTheme } from "./test-themes.js";
import { VirtualTerminal } from "./virtual-terminal.js";
// Force full color in CI so ANSI assertions are deterministic
const chalk = new Chalk({ level: 3 });
function getCellItalic(terminal: VirtualTerminal, row: number, col: number): number {
const xterm = (terminal as unknown as { xterm: XtermTerminalType }).xterm;
const buffer = xterm.buffer.active;
const line = buffer.getLine(buffer.viewportY + row);
assert.ok(line, `Missing buffer line at row ${row}`);
const cell = line.getCell(col);
assert.ok(cell, `Missing cell at row ${row} col ${col}`);
return cell.isItalic();
}
describe("Markdown component", () => {
describe("Nested lists", () => {
it("should render simple nested list", () => {
@ -437,6 +450,41 @@ describe("Markdown component", () => {
// Should have bold codes (1 or 22 for bold on/off)
assert.ok(joinedOutput.includes("\x1b[1m"), "Should have bold code");
});
it("should not leak styles into following lines when rendered in TUI", async () => {
class MarkdownWithInput implements Component {
public markdownLineCount = 0;
constructor(private readonly markdown: Markdown) {}
render(width: number): string[] {
const lines = this.markdown.render(width);
this.markdownLineCount = lines.length;
return [...lines, "INPUT"];
}
invalidate(): void {
this.markdown.invalidate();
}
}
const markdown = new Markdown("This is thinking with `inline code`", 1, 0, defaultMarkdownTheme, {
color: (text) => chalk.gray(text),
italic: true,
});
const terminal = new VirtualTerminal(80, 6);
const tui = new TUI(terminal);
const component = new MarkdownWithInput(markdown);
tui.addChild(component);
tui.start();
await terminal.flush();
assert.ok(component.markdownLineCount > 0);
const inputRow = component.markdownLineCount;
assert.strictEqual(getCellItalic(terminal, inputRow, 0), 0);
tui.stop();
});
});
describe("Spacing after code blocks", () => {

View file

@ -0,0 +1,72 @@
import assert from "node:assert";
import { describe, it } from "node:test";
import type { Terminal as XtermTerminalType } from "@xterm/headless";
import { type Component, TUI } from "../src/tui.js";
import { VirtualTerminal } from "./virtual-terminal.js";
class StaticLines implements Component {
constructor(private readonly lines: string[]) {}
render(): string[] {
return this.lines;
}
invalidate(): void {}
}
class StaticOverlay implements Component {
constructor(private readonly line: string) {}
render(): string[] {
return [this.line];
}
invalidate(): void {}
}
function getCellItalic(terminal: VirtualTerminal, row: number, col: number): number {
const xterm = (terminal as unknown as { xterm: XtermTerminalType }).xterm;
const buffer = xterm.buffer.active;
const line = buffer.getLine(buffer.viewportY + row);
assert.ok(line, `Missing buffer line at row ${row}`);
const cell = line.getCell(col);
assert.ok(cell, `Missing cell at row ${row} col ${col}`);
return cell.isItalic();
}
async function renderAndFlush(tui: TUI, terminal: VirtualTerminal): Promise<void> {
tui.requestRender(true);
await new Promise<void>((resolve) => process.nextTick(resolve));
await terminal.flush();
}
describe("TUI overlay compositing", () => {
it("should not leak styles when a trailing reset sits beyond the last visible column (no overlay)", async () => {
const width = 20;
const baseLine = `\x1b[3m${"X".repeat(width)}\x1b[23m`;
const terminal = new VirtualTerminal(width, 6);
const tui = new TUI(terminal);
tui.addChild(new StaticLines([baseLine, "INPUT"]));
tui.start();
await renderAndFlush(tui, terminal);
assert.strictEqual(getCellItalic(terminal, 1, 0), 0);
tui.stop();
});
it("should not leak styles when overlay slicing drops trailing SGR resets", async () => {
const width = 20;
const baseLine = `\x1b[3m${"X".repeat(width)}\x1b[23m`;
const terminal = new VirtualTerminal(width, 6);
const tui = new TUI(terminal);
tui.addChild(new StaticLines([baseLine, "INPUT"]));
tui.showOverlay(new StaticOverlay("OVR"), { row: 0, col: 5, width: 3 });
tui.start();
await renderAndFlush(tui, terminal);
assert.strictEqual(getCellItalic(terminal, 1, 0), 0);
tui.stop();
});
});