Files
CapaKraken/apps/web/src/lib/scopeImportParser.test.ts
T
Hartmut a3d75973ee test(web): add 291 tests for parsers, hooks, and UI components
Lib utilities: scopeImportParser (31), status-styles (58),
planningEntryIds (10), uuid (11).

Hooks: useFilters (28), useRowOrder (18), usePermissions (30),
useViewPrefs (24).

Components: AnimatedModal (14), DateInput (22), InfoTooltip (13),
ProgressRing (19).

Web test suite: 75 → 87 files, 553 → 844 tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 17:14:11 +02:00

276 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it, vi } from "vitest";
import type { ParsedScopeRow, ScopeImportResult } from "./scopeImportParser.js";
// parseScopeImport depends on parseSpreadsheet from "./excel.js", which does
// real file I/O. We mock that module so the parser logic can be tested in
// isolation with plain in-memory data.
vi.mock("./excel.js", () => ({
parseSpreadsheet: vi.fn(),
}));
import { parseScopeImport } from "./scopeImportParser.js";
import { parseSpreadsheet } from "./excel.js";
const mockSpreadsheet = parseSpreadsheet as ReturnType<typeof vi.fn>;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function makeFile(): File {
return new File(["dummy"], "test.xlsx");
}
// ---------------------------------------------------------------------------
// Empty / degenerate input
// ---------------------------------------------------------------------------
describe("parseScopeImport empty input", () => {
it("returns a warning and no rows when the spreadsheet is empty", async () => {
mockSpreadsheet.mockResolvedValueOnce([]);
const result: ScopeImportResult = await parseScopeImport(makeFile());
expect(result.rows).toHaveLength(0);
expect(result.warnings).toContain("File contains no data rows.");
expect(result.mapping.name).toBeNull();
});
it("returns a warning when no name column can be identified", async () => {
mockSpreadsheet.mockResolvedValueOnce([{ quantity: "3", budget: "1000" }]);
const result = await parseScopeImport(makeFile());
expect(result.rows).toHaveLength(0);
expect(result.warnings.some((w) => w.includes('"Name" column'))).toBe(true);
});
it("returns a warning when name column is present but all values are empty", async () => {
mockSpreadsheet.mockResolvedValueOnce([{ name: " " }, { name: "" }]);
const result = await parseScopeImport(makeFile());
expect(result.rows).toHaveLength(0);
expect(result.warnings).toContain("No rows with a non-empty name found.");
});
});
// ---------------------------------------------------------------------------
// Column header resolution aliases
// ---------------------------------------------------------------------------
describe("parseScopeImport header alias resolution", () => {
it("maps 'title' to the name field", async () => {
mockSpreadsheet.mockResolvedValueOnce([{ title: "Shot A" }]);
const result = await parseScopeImport(makeFile());
expect(result.mapping.name).toBe("title");
expect(result.rows[0]?.name).toBe("Shot A");
});
it("maps 'seq' to sequenceNo and uses its integer value", async () => {
mockSpreadsheet.mockResolvedValueOnce([{ seq: "5", name: "Shot B" }]);
const result = await parseScopeImport(makeFile());
expect(result.mapping.sequenceNo).toBe("seq");
expect(result.rows[0]?.sequenceNo).toBe(5);
});
it("maps 'type' to scopeType", async () => {
mockSpreadsheet.mockResolvedValueOnce([{ type: "ASSET", name: "Prop01" }]);
const result = await parseScopeImport(makeFile());
expect(result.mapping.scopeType).toBe("type");
expect(result.rows[0]?.scopeType).toBe("ASSET");
});
it("maps 'pkg' to packageCode", async () => {
mockSpreadsheet.mockResolvedValueOnce([{ pkg: "PKG-01", name: "Shot C" }]);
const result = await parseScopeImport(makeFile());
expect(result.mapping.packageCode).toBe("pkg");
expect(result.rows[0]?.packageCode).toBe("PKG-01");
});
it("maps 'desc' to description", async () => {
mockSpreadsheet.mockResolvedValueOnce([{ desc: "Some details", name: "Shot D" }]);
const result = await parseScopeImport(makeFile());
expect(result.mapping.description).toBe("desc");
expect(result.rows[0]?.description).toBe("Some details");
});
it("treats header matching as case-insensitive", async () => {
mockSpreadsheet.mockResolvedValueOnce([{ NAME: "Shot E", TYPE: "VFX" }]);
const result = await parseScopeImport(makeFile());
expect(result.mapping.name).toBe("NAME");
expect(result.mapping.scopeType).toBe("TYPE");
});
it("handles headers with mixed casing and spaces", async () => {
mockSpreadsheet.mockResolvedValueOnce([{ "Sequence No": "10", Name: "Shot F" }]);
const result = await parseScopeImport(makeFile());
expect(result.mapping.sequenceNo).toBe("Sequence No");
expect(result.rows[0]?.sequenceNo).toBe(10);
});
});
// ---------------------------------------------------------------------------
// Sequence number handling
// ---------------------------------------------------------------------------
describe("parseScopeImport sequence number auto-numbering", () => {
it("auto-numbers rows when no sequenceNo column is present", async () => {
mockSpreadsheet.mockResolvedValueOnce([
{ name: "Shot A" },
{ name: "Shot B" },
{ name: "Shot C" },
]);
const result = await parseScopeImport(makeFile());
expect(result.rows[0]?.sequenceNo).toBe(1);
expect(result.rows[1]?.sequenceNo).toBe(2);
expect(result.rows[2]?.sequenceNo).toBe(3);
expect(result.warnings).toContain("No sequence number column detected — auto-numbering.");
});
it("falls back to auto-number when sequenceNo value is not a positive integer", async () => {
mockSpreadsheet.mockResolvedValueOnce([
{ seq: "0", name: "Shot A" },
{ seq: "-5", name: "Shot B" },
{ seq: "abc", name: "Shot C" },
]);
const result = await parseScopeImport(makeFile());
result.rows.forEach((row, i) => {
expect(row.sequenceNo).toBe(i + 1);
});
});
it("respects explicit positive integer sequence numbers", async () => {
mockSpreadsheet.mockResolvedValueOnce([
{ seq: "100", name: "Shot A" },
{ seq: "200", name: "Shot B" },
]);
const result = await parseScopeImport(makeFile());
expect(result.rows[0]?.sequenceNo).toBe(100);
expect(result.rows[1]?.sequenceNo).toBe(200);
});
});
// ---------------------------------------------------------------------------
// scopeType defaulting
// ---------------------------------------------------------------------------
describe("parseScopeImport scopeType defaulting", () => {
it("defaults scopeType to SHOT when no type column is present", async () => {
mockSpreadsheet.mockResolvedValueOnce([{ name: "Shot A" }]);
const result = await parseScopeImport(makeFile());
expect(result.rows[0]?.scopeType).toBe("SHOT");
expect(result.warnings).toContain("No scope type column detected — defaulting to SHOT.");
});
it("defaults scopeType to SHOT when the type cell is empty", async () => {
mockSpreadsheet.mockResolvedValueOnce([{ type: " ", name: "Shot A" }]);
const result = await parseScopeImport(makeFile());
expect(result.rows[0]?.scopeType).toBe("SHOT");
});
it("preserves the provided scopeType when it is non-empty", async () => {
mockSpreadsheet.mockResolvedValueOnce([{ type: "ASSET", name: "Prop01" }]);
const result = await parseScopeImport(makeFile());
expect(result.rows[0]?.scopeType).toBe("ASSET");
});
});
// ---------------------------------------------------------------------------
// Rows with empty names are skipped
// ---------------------------------------------------------------------------
describe("parseScopeImport skipping empty-name rows", () => {
it("skips rows where the name cell is blank", async () => {
mockSpreadsheet.mockResolvedValueOnce([
{ name: "Shot A" },
{ name: "" },
{ name: "Shot C" },
{ name: " " },
]);
const result = await parseScopeImport(makeFile());
expect(result.rows).toHaveLength(2);
expect(result.rows[0]?.name).toBe("Shot A");
expect(result.rows[1]?.name).toBe("Shot C");
});
});
// ---------------------------------------------------------------------------
// Full happy-path scenario
// ---------------------------------------------------------------------------
describe("parseScopeImport full integration scenario", () => {
it("parses a complete row with all columns present", async () => {
mockSpreadsheet.mockResolvedValueOnce([
{
number: "42",
type: "VFX",
code: "PKG-VFX",
title: "Explosion",
notes: "Big boom",
},
]);
const result = await parseScopeImport(makeFile());
expect(result.warnings).toHaveLength(0);
expect(result.rows).toHaveLength(1);
const row: ParsedScopeRow = result.rows[0]!;
expect(row.sequenceNo).toBe(42);
expect(row.scopeType).toBe("VFX");
expect(row.packageCode).toBe("PKG-VFX");
expect(row.name).toBe("Explosion");
expect(row.description).toBe("Big boom");
});
it("trims whitespace from all string fields", async () => {
mockSpreadsheet.mockResolvedValueOnce([
{
name: " Trimmed Name ",
type: " SHOT ",
code: " PKG-01 ",
desc: " Some notes ",
},
]);
const result = await parseScopeImport(makeFile());
const row = result.rows[0]!;
expect(row.name).toBe("Trimmed Name");
expect(row.scopeType).toBe("SHOT");
expect(row.packageCode).toBe("PKG-01");
expect(row.description).toBe("Some notes");
});
it("returns the correct mapping alongside the rows", async () => {
mockSpreadsheet.mockResolvedValueOnce([
{ seq: "1", type: "VFX", package: "P1", name: "Shot A", description: "desc" },
]);
const result = await parseScopeImport(makeFile());
expect(result.mapping).toMatchObject({
sequenceNo: "seq",
scopeType: "type",
packageCode: "package",
name: "name",
description: "description",
});
});
});