c794e82464
Covers: aria-sort/aria-labelledby attributes, non-Error throws in ErrorBoundary, NaN/MAX_SAFE_INTEGER in formatCents, invalid dates, carriage returns in CSV, self-closing HTML tags in sanitize, non-digit input in DateInput, panel-click-not-dismissing in ConfirmDialog, role="search" on FilterBar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
147 lines
5.6 KiB
TypeScript
147 lines
5.6 KiB
TypeScript
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");
|
|
});
|
|
});
|