Files
diegosouzapw--omniroute/tests/unit/cli-output-render-table.test.ts
2026-07-13 13:39:12 +08:00

135 lines
5.8 KiB
TypeScript

/**
* Regression tests for renderTable (exposed via emit({ output: "table" })).
* Verifies behaviour is preserved after replacing cli-table3 with the hand-rolled formatter.
*/
import test from "node:test";
import assert from "node:assert/strict";
function captureStdout(fn: () => void): string {
const chunks: string[] = [];
const originalWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = (chunk: string | Uint8Array) => {
chunks.push(typeof chunk === "string" ? chunk : chunk.toString());
return true;
};
try {
fn();
} finally {
process.stdout.write = originalWrite;
}
return chunks.join("");
}
// ─── renderTable via emit ────────────────────────────────────────────────────
test("renderTable: header text appears in output", async () => {
const { emit } = await import("../../bin/cli/output.mjs");
const schema = [
{ key: "a", header: "A" },
{ key: "b", header: "B" },
];
const out = captureStdout(() => emit([{ a: "foo", b: "bar" }], { output: "table" }, schema));
assert.ok(out.includes("A"), `expected "A" in output, got: ${out}`);
assert.ok(out.includes("B"), `expected "B" in output, got: ${out}`);
});
test("renderTable: row values appear in output", async () => {
const { emit } = await import("../../bin/cli/output.mjs");
const schema = [
{ key: "a", header: "A" },
{ key: "b", header: "B" },
];
const out = captureStdout(() => emit([{ a: "foo", b: "bar" }], { output: "table" }, schema));
assert.ok(out.includes("foo"), `expected "foo" in output, got: ${out}`);
assert.ok(out.includes("bar"), `expected "bar" in output, got: ${out}`);
});
test("renderTable: output contains newline separation", async () => {
const { emit } = await import("../../bin/cli/output.mjs");
const schema = [{ key: "a", header: "A" }];
const out = captureStdout(() => emit([{ a: "foo" }], { output: "table" }, schema));
assert.ok(out.includes("\n"), "expected newline in output");
});
test("renderTable: empty rows prints (empty)", async () => {
const { emit } = await import("../../bin/cli/output.mjs");
const schema = [{ key: "a", header: "A" }];
const out = captureStdout(() => emit([], { output: "table" }, schema));
assert.ok(out.includes("empty"), `expected "(empty)" in output, got: ${out}`);
});
test("renderTable: quiet:true suppresses headers", async () => {
const { emit } = await import("../../bin/cli/output.mjs");
const schema = [
{ key: "a", header: "Alpha" },
{ key: "b", header: "Beta" },
];
const out = captureStdout(() =>
emit([{ a: "val1", b: "val2" }], { output: "table", quiet: true }, schema),
);
// headers must NOT appear in quiet mode
assert.ok(!out.includes("Alpha"), `header "Alpha" should be hidden in quiet mode, got: ${out}`);
assert.ok(!out.includes("Beta"), `header "Beta" should be hidden in quiet mode, got: ${out}`);
// data rows still present
assert.ok(out.includes("val1"), `expected "val1" in quiet output, got: ${out}`);
assert.ok(out.includes("val2"), `expected "val2" in quiet output, got: ${out}`);
});
test("renderTable: multiple rows all appear", async () => {
const { emit } = await import("../../bin/cli/output.mjs");
const schema = [{ key: "name", header: "Name" }];
const data = [{ name: "alice" }, { name: "bob" }, { name: "charlie" }];
const out = captureStdout(() => emit(data, { output: "table" }, schema));
assert.ok(out.includes("alice"), "expected 'alice'");
assert.ok(out.includes("bob"), "expected 'bob'");
assert.ok(out.includes("charlie"), "expected 'charlie'");
});
test("renderTable: infers schema from data keys when no schema given", async () => {
const { emit } = await import("../../bin/cli/output.mjs");
const data = [{ id: "x1", status: "ok" }];
const out = captureStdout(() => emit(data, { output: "table" }));
assert.ok(out.includes("id"), "expected key 'id' as header");
assert.ok(out.includes("status"), "expected key 'status' as header");
assert.ok(out.includes("x1"), "expected value 'x1'");
assert.ok(out.includes("ok"), "expected value 'ok'");
});
test("renderTable: column separator characters present", async () => {
const { emit } = await import("../../bin/cli/output.mjs");
const schema = [
{ key: "col1", header: "Col1" },
{ key: "col2", header: "Col2" },
];
const out = captureStdout(() => emit([{ col1: "a", col2: "b" }], { output: "table" }, schema));
// Output must contain some column delimiter — either "|" (hand-rolled) or "│" (cli-table3 box-drawing)
const hasSeparator = out.includes("|") || out.includes("│");
assert.ok(hasSeparator, `expected column separator in output, got: ${out}`);
});
test("renderTable: ANSI-wrapped formatter value does not bleed — reset code always present", async () => {
const { emit } = await import("../../bin/cli/output.mjs");
// Formatter wraps each value in green ANSI codes.
const GREEN = "\x1b[32m";
const RESET_CODE = "\x1b[0m";
const schema = [
{
key: "status",
header: "Status",
// Column width (4) is smaller than the raw ANSI string length (e.g. "\x1b[32mok\x1b[0m" = 12 bytes)
// but the visible content "ok" is only 2 chars — no truncation needed.
// We use a longer visible value to force truncation and verify the reset is preserved.
width: 4,
formatter: (v: string) => `${GREEN}${v}${RESET_CODE}`,
},
];
// "active" (6 visible chars) exceeds the column width of 4, triggering truncation.
const out = captureStdout(() => emit([{ status: "active" }], { output: "table" }, schema));
// The rendered output must always contain the ANSI reset code — never a bleed.
assert.ok(
out.includes(RESET_CODE),
`expected ANSI reset code (\\x1b[0m) in output to prevent color bleed, got: ${JSON.stringify(out)}`,
);
});