Files
buzz-sheet/lib/guides/extra-sheet.test.ts
T
gunshiz db9b5ecdc8
CI / Verify (push) Successful in 1m30s
CI / Build immutable images and deploy (push) Successful in 2m5s
j
2026-09-26 01:54:50 +07:00

239 lines
9.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
addExtraSheetColumn,
addExtraSheetRow,
clearExtraSheetCellFormatting,
createExtraSheet,
deleteExtraSheetColumn,
deleteExtraSheetRow,
extraSheetSchema,
extraSheetRangeToTsv,
extraSheetToMatrix,
extraSheetUsedRange,
matrixToExtraSheet,
moveExtraSheetColumn,
moveExtraSheetRow,
normalizeExtraSheetRichText,
parseExtraSheetTsv,
pasteExtraSheetText,
resizeExtraSheetColumn,
styleExtraSheetCells,
updateExtraSheetCell,
updateExtraSheetCells,
type ExtraSheet,
} from "@/lib/guides/extra-sheet";
function idFactory() {
let index = 0;
return (prefix: string) => `${prefix}-${++index}`;
}
describe("Extra sheet", () => {
it("creates a valid four-by-six sheet", () => {
const sheet = createExtraSheet(idFactory());
expect(extraSheetSchema.safeParse(sheet).success).toBe(true);
expect(sheet.columns.map((column) => column.label)).toEqual(
Array.from({ length: 6 }, (_, index) => `คอลัมน์ ${index + 1}`),
);
expect(extraSheetToMatrix(sheet)).toEqual(
Array.from({ length: 4 }, () =>
Array.from({ length: 6 }, () => ({ value: "", backgroundColor: undefined })),
),
);
});
it("preserves cell identity while rows and columns move", () => {
const createId = idFactory();
const sheet = createExtraSheet(createId);
const firstColumnId = sheet.columns[0].id;
const secondColumnId = sheet.columns[1].id;
sheet.rows[0].cells[firstColumnId] = "A";
sheet.rows[0].cells[secondColumnId] = "B";
sheet.rows[1].cells[firstColumnId] = "C";
const moved = moveExtraSheetRow(moveExtraSheetColumn(sheet, 0, 1), 0, 1);
const matrix = extraSheetToMatrix(moved);
expect(matrix).toHaveLength(4);
expect(matrix[0]).toHaveLength(6);
expect(matrix[0].slice(0, 2).map((cell) => cell?.value)).toEqual(["", "C"]);
expect(matrix[1].slice(0, 2).map((cell) => cell?.value)).toEqual(["B", "A"]);
});
it("adds and removes rows and columns without orphaned cells", () => {
const createId = idFactory();
const initial = createExtraSheet(createId);
const columnId = createId("column");
const expanded = addExtraSheetRow(
addExtraSheetColumn(initial, "Notes", columnId),
createId("row"),
);
expect(expanded.rows).toHaveLength(5);
expect(expanded.rows.every((row) => row.cells[columnId] === "")).toBe(true);
const reduced = deleteExtraSheetColumn(expanded, expanded.columns.length - 1);
expect(reduced.rows.every((row) => !(columnId in row.cells))).toBe(true);
const secondRowId = reduced.rows[1].id;
const withoutFirstRow = deleteExtraSheetRow(reduced, 0);
expect(withoutFirstRow.rows).toHaveLength(4);
expect(withoutFirstRow.rows[0].id).toBe(secondRowId);
});
it("expands rows and columns when a pasted matrix is larger", () => {
const createId = idFactory();
const initial = createExtraSheet(createId);
const expanded = matrixToExtraSheet(initial, [
[],
[],
[],
[],
Array.from({ length: 9 }, (_, index) => ({ value: index === 8 ? "=1+1" : "" })),
], createId);
expect(expanded.columns).toHaveLength(9);
expect(expanded.rows).toHaveLength(5);
expect(extraSheetToMatrix(expanded)[4][8]?.value).toBe("=1+1");
expect(extraSheetSchema.safeParse(expanded).success).toBe(true);
});
it("rejects duplicate labels and cells for missing columns", () => {
const sheet = createExtraSheet(idFactory());
sheet.columns[1].label = sheet.columns[0].label.toUpperCase();
sheet.rows[0].cells.missing = "orphan";
const result = extraSheetSchema.safeParse(sheet);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues.map((issue) => issue.message)).toContain("ชื่อคอลัมน์ต้องไม่ซ้ำกัน");
expect(result.error.issues.map((issue) => issue.message)).toContain("ไม่พบคอลัมน์ของเซลล์นี้");
}
});
it("returns only the used display range", () => {
const sheet = createExtraSheet(idFactory());
expect(extraSheetUsedRange(sheet)).toBeNull();
sheet.rows[1].cells[sheet.columns[2].id] = "<p>Start</p>";
sheet.rows[3].backgrounds[sheet.columns[4].id] = "#123456";
const used = extraSheetUsedRange(sheet);
expect(used?.columns.map((column) => column.label)).toEqual([
"คอลัมน์ 3",
"คอลัมน์ 4",
"คอลัมน์ 5",
]);
expect(used?.rows).toHaveLength(3);
expect(used && extraSheetSchema.safeParse(used).success).toBe(true);
});
it("updates and sanitizes rich-text cells while preserving legacy text", () => {
const initial = createExtraSheet(idFactory());
const updated = updateExtraSheetCell(initial, 0, 0, "legacy\ntext");
const columnId = updated.columns[0].id;
expect(normalizeExtraSheetRichText(updated).rows[0].cells[columnId]).toBe(
"<p>legacy<br>text</p>",
);
const unsafe = updateExtraSheetCell(updated, 0, 0, "<p><strong>safe</strong><script>bad()</script></p>");
expect(normalizeExtraSheetRichText(unsafe).rows[0].cells[columnId]).toBe(
"<p><strong>safe</strong></p>",
);
});
it("updates rich text and backgrounds across a cell selection", () => {
const sheet = createExtraSheet(idFactory());
const updated = updateExtraSheetCells(sheet, [
{ row: 0, column: 0 },
{ row: 1, column: 1 },
], { value: "<p>Shared</p>", backgroundColor: "#16A34A" });
const matrix = extraSheetToMatrix(updated);
expect(matrix[0][0]).toMatchObject({ value: "<p>Shared</p>", backgroundColor: "#16A34A" });
expect(matrix[1][1]).toMatchObject({ value: "<p>Shared</p>", backgroundColor: "#16A34A" });
expect(matrix[0][1]).toMatchObject({ value: "", backgroundColor: undefined });
expect(matrix[1][0]).toMatchObject({ value: "", backgroundColor: undefined });
expect(extraSheetSchema.safeParse(updated).success).toBe(true);
expect(updateExtraSheetCells(updated, [{ row: 0, column: 0 }], { backgroundColor: null })
.rows[0].backgrounds).toEqual({});
});
it("styles selected cells without replacing their individual text", () => {
const sheet = createExtraSheet(idFactory());
const firstColumn = sheet.columns[0].id;
const secondColumn = sheet.columns[1].id;
sheet.rows[0].cells[firstColumn] = "<p>Alpha</p>";
sheet.rows[1].cells[secondColumn] = "<p><em>Beta</em></p>";
const styled = styleExtraSheetCells(sheet, [
{ row: 0, column: 0 },
{ row: 1, column: 1 },
], { bold: true });
expect(styled.rows[0].cells[firstColumn]).toBe("<p><strong>Alpha</strong></p>");
expect(styled.rows[1].cells[secondColumn]).toBe("<p><em><strong>Beta</strong></em></p>");
expect(styled.rows[0].cells[secondColumn]).toBe("");
});
it("renders legacy sheets that do not have background metadata yet", () => {
const sheet = createExtraSheet(idFactory());
const legacy = {
...sheet,
rows: sheet.rows.map((row) => ({ id: row.id, cells: row.cells })),
} as ExtraSheet;
expect(extraSheetToMatrix(legacy)[0][0]).toEqual({
value: "",
backgroundColor: undefined,
alignment: undefined,
});
const parsed = extraSheetSchema.parse(legacy);
expect(parsed.columns[0].width).toBe(140);
expect(parsed.rows[0].alignments).toEqual({});
});
it("persists bounded column widths and cell alignment", () => {
const sheet = createExtraSheet(idFactory());
const resized = resizeExtraSheetColumn(sheet, 0, 999);
const aligned = updateExtraSheetCells(resized, [{ row: 0, column: 0 }], {
alignment: "center",
backgroundColor: "#123456",
});
expect(aligned.columns[0].width).toBe(480);
expect(extraSheetToMatrix(aligned)[0][0]).toMatchObject({
alignment: "center",
backgroundColor: "#123456",
});
expect(extraSheetSchema.safeParse(aligned).success).toBe(true);
});
it("clears visual formatting without clearing cell content", () => {
const sheet = createExtraSheet(idFactory());
const columnId = sheet.columns[0].id;
sheet.rows[0].cells[columnId] = '<p><strong><span style="color: #FF0000">Alpha</span></strong></p>';
sheet.rows[0].backgrounds[columnId] = "#123456";
sheet.rows[0].alignments[columnId] = "right";
const cleared = clearExtraSheetCellFormatting(sheet, [{ row: 0, column: 0 }]);
expect(cleared.rows[0].cells[columnId]).toBe("<p>Alpha</p>");
expect(cleared.rows[0].backgrounds).toEqual({});
expect(cleared.rows[0].alignments).toEqual({});
});
it("copies plain TSV and pastes multiline cells while expanding the grid", () => {
const createId = idFactory();
const sheet = createExtraSheet(createId);
const firstColumn = sheet.columns[0].id;
sheet.rows[0].cells[firstColumn] = "<p>Alpha<br>Beta</p>";
const copied = extraSheetRangeToTsv(sheet, { row: 0, column: 0 }, { row: 0, column: 0 });
expect(copied).toBe('"Alpha\nBeta"');
expect(parseExtraSheetTsv('"Alpha\nBeta"\tGamma')).toEqual([["Alpha\nBeta", "Gamma"]]);
const pasted = pasteExtraSheetText(sheet, { row: 2, column: 7 }, '"Alpha\nBeta"\tGamma', createId);
expect(pasted.rows).toHaveLength(4);
expect(pasted.columns).toHaveLength(9);
expect(pasted.rows[2].cells[pasted.columns[7].id]).toBe("<p>Alpha<br>Beta</p>");
expect(pasted.rows[2].cells[pasted.columns[8].id]).toBe("<p>Gamma</p>");
});
});