import { describe, it, expect, vi } from "vitest"; import { act } from "react"; import { createRenderCounter } from "@assistant-ui/x-performance"; import { render } from "ink-testing-library"; import { MarkdownText } from "../MarkdownText"; describe("MarkdownText", () => { it.each([{ width: 40 }, { wrap: false }])( "does not reformat unchanged messages on resize with %j", async (options) => { vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); try { const counter = createRenderCounter(); const highlighter = (code: string) => { counter.useRender("highlight"); return code; }; let view!: ReturnType; await act(async () => { view = render( <> {Array.from({ length: 10 }, (_, i) => ( ))} , ); }); try { expect(counter.renders("highlight")).toBe(10); counter.reset(); await act(async () => { Object.defineProperty(view.stdout, "columns", { configurable: true, value: 200, }); view.stdout.emit("resize"); }); expect(counter.renders("highlight")).toBe(0); expect(view.lastFrame()).toContain("const x = 1;"); } finally { await act(async () => { view.unmount(); }); } } finally { vi.unstubAllGlobals(); } }, ); it.each([{ width: 40 }, { wrap: false }])( "resumes live width updates after removing %j", async (options) => { vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); try { const counter = createRenderCounter(); const highlighter = (code: string) => { counter.useRender("highlight"); return code; }; const text = "```js\nconst x = 1;\n```"; let view!: ReturnType; await act(async () => { view = render( , ); }); try { await act(async () => { view.rerender( , ); }); counter.reset(); await act(async () => { Object.defineProperty(view.stdout, "columns", { configurable: true, value: 200, }); view.stdout.emit("resize"); }); expect(counter.renders("highlight")).toBe(1); } finally { await act(async () => { view.unmount(); }); } } finally { vi.unstubAllGlobals(); } }, ); it("renders plain text", () => { const { lastFrame } = render(); expect(lastFrame()).toContain("Hello world"); }); it("renders a heading with formatting", () => { const { lastFrame } = render(); const frame = lastFrame()!; expect(frame).not.toContain("# My Heading"); expect(frame).toContain("My Heading"); }); it("renders bold text", () => { const { lastFrame } = render(); const frame = lastFrame()!; expect(frame).toContain("bold text"); expect(frame).not.toContain("**"); }); it("renders a list", () => { const { lastFrame } = render( , ); const frame = lastFrame()!; expect(frame).toContain("item one"); expect(frame).toContain("item two"); }); it("renders a code block", () => { const { lastFrame } = render( , ); const frame = lastFrame()!; expect(frame).toContain('console.log("hello")'); }); it("renders inline code", () => { const { lastFrame } = render( , ); const frame = lastFrame()!; expect(frame).toContain("const x = 1"); expect(frame).not.toMatch(/(? { const { lastFrame } = render( , ); expect(lastFrame()).toContain("item"); }); it("updates as text grows", () => { const { lastFrame, rerender } = render(); expect(lastFrame()).toContain("Hello"); rerender(); expect(lastFrame()).toContain("Hello world"); }); it("re-renders when text changes", () => { const { lastFrame, rerender } = render( , ); rerender(); const frame = lastFrame()!; expect(frame).toContain("Title"); expect(frame).toContain("More text"); }); it("passes highlighter to markdansi", () => { const highlighter = vi.fn((code: string) => `HIGHLIGHTED:${code}`); const { lastFrame } = render( , ); expect(highlighter).toHaveBeenCalled(); expect(lastFrame()).toContain("HIGHLIGHTED:"); }); it("re-renders when highlighter changes without text changes", () => { const firstHighlighter = vi.fn((code: string) => `FIRST:${code}`); const secondHighlighter = vi.fn((code: string) => `SECOND:${code}`); const { lastFrame, rerender } = render( , ); expect(lastFrame()).toContain("FIRST:"); rerender( , ); expect(lastFrame()).toContain("SECOND:"); expect(lastFrame()).not.toContain("FIRST:"); }); it("passes table options to markdansi", () => { const table = "| column |\n| --- |\n| a very long cell value that must truncate |"; const { lastFrame } = render( , ); expect(lastFrame()).toContain(">>>"); }); it("wraps table cells when tableTruncate is disabled", () => { const table = "| column |\n| --- |\n| a very long cell value that must truncate |"; const { lastFrame } = render( , ); const frame = lastFrame()!; expect(frame).toContain("truncate"); expect(frame).not.toContain("…"); }); it("emits ANSI colors when color is enabled", () => { const { lastFrame } = render(); expect(lastFrame()).toContain("["); }); it("strips ANSI colors when color is disabled", () => { const { lastFrame } = render(); expect(lastFrame()).not.toContain("["); }); it("re-wraps memoized output when the terminal is resized", async () => { const text = "word ".repeat(30).trim(); const { lastFrame, stdout } = render(); await new Promise((resolve) => setTimeout(resolve, 0)); expect(lastFrame()!.trimEnd().split("\n").length).toBeGreaterThan(1); Object.defineProperty(stdout, "columns", { configurable: true, value: 200, }); stdout.emit("resize"); await vi.waitFor(() => { expect(lastFrame()!.trimEnd().split("\n")).toHaveLength(1); }); }); it("shares a single stdout resize listener across markdown instances", async () => { const single = render(); await new Promise((resolve) => setTimeout(resolve, 0)); const baseline = single.stdout.listenerCount("resize"); expect(baseline).toBeGreaterThanOrEqual(1); single.unmount(); const texts = Array.from({ length: 12 }, (_, i) => `message ${i}`); const many = render( <> {texts.map((t) => ( ))} , ); await new Promise((resolve) => setTimeout(resolve, 0)); expect(many.stdout.listenerCount("resize")).toBe(baseline); }); it("memoizes rendering across re-renders with unchanged props", () => { const highlighter = vi.fn((code: string) => code); const text = "```js\nconst x = 1;\n```"; const { rerender } = render( , ); expect(highlighter).toHaveBeenCalledTimes(1); rerender(); expect(highlighter).toHaveBeenCalledTimes(1); rerender( , ); expect(highlighter).toHaveBeenCalledTimes(2); rerender( , ); expect(highlighter).toHaveBeenCalledTimes(3); }); });