import { describe, expect, it } from "vitest"; import { generateCsv } from "./csv-export.js"; // downloadCsv is a browser-only side-effect function (creates DOM elements and // triggers a download). It is intentionally excluded here — testing it would // require mocking the entire DOM download flow and adds no value for a pure // utility test suite. type Row = { name: string; value: number | null; note: string }; const columns = [ { header: "Name", accessor: (r: Row) => r.name }, { header: "Value", accessor: (r: Row) => r.value }, { header: "Note", accessor: (r: Row) => r.note }, ]; describe("generateCsv", () => { it("produces a header row followed by a data row", () => { const rows: Row[] = [{ name: "Alice", value: 42, note: "ok" }]; const csv = generateCsv(rows, columns); const lines = csv.split("\n"); expect(lines[0]).toBe("Name,Value,Note"); expect(lines[1]).toBe("Alice,42,ok"); }); it("returns only the header row when there are no data rows", () => { const csv = generateCsv([], columns); // generateCsv always appends '\n' between header and body. // With an empty body the result is "header\n" expect(csv).toBe("Name,Value,Note\n"); }); it("handles multiple rows", () => { const rows: Row[] = [ { name: "Alice", value: 1, note: "a" }, { name: "Bob", value: 2, note: "b" }, { name: "Carol", value: 3, note: "c" }, ]; const csv = generateCsv(rows, columns); const lines = csv.split("\n"); expect(lines).toHaveLength(4); // header + 3 data rows expect(lines[1]).toBe("Alice,1,a"); expect(lines[2]).toBe("Bob,2,b"); expect(lines[3]).toBe("Carol,3,c"); }); it("wraps cell values that contain a comma in double quotes", () => { const rows: Row[] = [{ name: "Smith, John", value: 10, note: "ok" }]; const csv = generateCsv(rows, columns); const dataLine = csv.split("\n")[1]; expect(dataLine).toBe('"Smith, John",10,ok'); }); it("escapes double quotes inside cell values by doubling them", () => { const rows: Row[] = [{ name: 'Say "hello"', value: 0, note: "x" }]; const csv = generateCsv(rows, columns); const dataLine = csv.split("\n")[1]; expect(dataLine).toBe('"Say ""hello""",0,x'); }); it("wraps cells that contain a newline in double quotes", () => { const rows: Row[] = [{ name: "line1\nline2", value: 0, note: "x" }]; const csv = generateCsv(rows, columns); // The header is the first line; the wrapped cell starts on line 2 const raw = csv.split("\n"); // Header + quoted cell (which contains an embedded newline) + remainder expect(raw[0]).toBe("Name,Value,Note"); expect(raw[1]).toBe('"line1'); expect(raw[2]).toContain("line2"); }); it("converts null accessor results to empty strings", () => { const rows: Row[] = [{ name: "Test", value: null, note: "" }]; const csv = generateCsv(rows, columns); const dataLine = csv.split("\n")[1]; expect(dataLine).toBe("Test,,"); }); it("converts undefined accessor results to empty strings", () => { type Partial = { name?: string }; const partialCols = [{ header: "Name", accessor: (r: Partial) => r.name }]; const csv = generateCsv([{}] as Partial[], partialCols); expect(csv.split("\n")[1]).toBe(""); }); it("wraps header cells that contain a comma", () => { const colsWithComma = [{ header: "Last, First", accessor: (r: Row) => r.name }]; const csv = generateCsv([], colsWithComma); expect(csv.startsWith('"Last, First"')).toBe(true); }); it("handles numeric zero values without treating them as empty", () => { const rows: Row[] = [{ name: "Zero", value: 0, note: "" }]; const csv = generateCsv(rows, columns); const dataLine = csv.split("\n")[1]; expect(dataLine).toBe("Zero,0,"); }); it("handles boolean accessor return values", () => { const boolCols = [ { header: "Active", accessor: (_r: unknown) => true }, { header: "Deleted", accessor: (_r: unknown) => false }, ]; const csv = generateCsv([{}], boolCols); expect(csv.split("\n")[1]).toBe("true,false"); }); it("handles a value that contains both a comma and a double quote", () => { const rows: Row[] = [{ name: 'a, "b"', value: 1, note: "" }]; const csv = generateCsv(rows, columns); const dataLine = csv.split("\n")[1]; expect(dataLine).toBe('"a, ""b""",1,'); }); it("does not wrap plain numeric strings", () => { const numCols = [{ header: "Num", accessor: (_r: unknown) => "12345" }]; const csv = generateCsv([{}], numCols); expect(csv.split("\n")[1]).toBe("12345"); }); it("generates correct CSV for a single-column dataset", () => { const singleCol = [{ header: "ID", accessor: (_r: unknown) => "abc" }]; const csv = generateCsv([{}, {}], singleCol); const lines = csv.split("\n"); expect(lines[0]).toBe("ID"); expect(lines[1]).toBe("abc"); expect(lines[2]).toBe("abc"); }); it("does NOT quote a cell value that contains only a carriage return (\\r is not in the escape list)", () => { // The escapeCsvValue function checks for '\n' but not '\r'. // A bare \r is therefore left unquoted. const crCols = [{ header: "Field", accessor: (_r: unknown) => "line1\rline2" }]; const csv = generateCsv([{}], crCols); const dataLine = csv.split("\n")[1]; // Because \r is not special-cased, the value is NOT wrapped in quotes expect(dataLine).toBe("line1\rline2"); }); it("empty columns array produces just a newline for no rows", () => { // header = "" (no columns), body = "" (no rows) // result = "" + "\n" + "" = "\n" const csv = generateCsv([], []); expect(csv).toBe("\n"); }); });