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>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getPlanningEntryMutationId } from "./planningEntryIds.js";
|
||||
|
||||
describe("getPlanningEntryMutationId", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Priority: entityId first
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it("returns entityId when all three fields are present", () => {
|
||||
const result = getPlanningEntryMutationId({
|
||||
id: "id-1",
|
||||
entityId: "entity-1",
|
||||
sourceAllocationId: "alloc-1",
|
||||
});
|
||||
expect(result).toBe("entity-1");
|
||||
});
|
||||
|
||||
it("returns entityId when sourceAllocationId is absent", () => {
|
||||
const result = getPlanningEntryMutationId({
|
||||
id: "id-1",
|
||||
entityId: "entity-1",
|
||||
});
|
||||
expect(result).toBe("entity-1");
|
||||
});
|
||||
|
||||
it("returns entityId when sourceAllocationId is null", () => {
|
||||
const result = getPlanningEntryMutationId({
|
||||
id: "id-1",
|
||||
entityId: "entity-1",
|
||||
sourceAllocationId: null,
|
||||
});
|
||||
expect(result).toBe("entity-1");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fallback: sourceAllocationId when entityId is absent/null/undefined
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it("returns sourceAllocationId when entityId is null", () => {
|
||||
const result = getPlanningEntryMutationId({
|
||||
id: "id-1",
|
||||
entityId: null,
|
||||
sourceAllocationId: "alloc-1",
|
||||
});
|
||||
expect(result).toBe("alloc-1");
|
||||
});
|
||||
|
||||
it("returns sourceAllocationId when entityId is undefined", () => {
|
||||
const result = getPlanningEntryMutationId({
|
||||
id: "id-1",
|
||||
entityId: undefined,
|
||||
sourceAllocationId: "alloc-1",
|
||||
});
|
||||
expect(result).toBe("alloc-1");
|
||||
});
|
||||
|
||||
it("returns sourceAllocationId when entityId field is omitted", () => {
|
||||
const result = getPlanningEntryMutationId({
|
||||
id: "id-1",
|
||||
sourceAllocationId: "alloc-1",
|
||||
});
|
||||
expect(result).toBe("alloc-1");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Final fallback: id
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it("returns id when both entityId and sourceAllocationId are null", () => {
|
||||
const result = getPlanningEntryMutationId({
|
||||
id: "id-1",
|
||||
entityId: null,
|
||||
sourceAllocationId: null,
|
||||
});
|
||||
expect(result).toBe("id-1");
|
||||
});
|
||||
|
||||
it("returns id when both entityId and sourceAllocationId are undefined", () => {
|
||||
const result = getPlanningEntryMutationId({
|
||||
id: "id-1",
|
||||
entityId: undefined,
|
||||
sourceAllocationId: undefined,
|
||||
});
|
||||
expect(result).toBe("id-1");
|
||||
});
|
||||
|
||||
it("returns id when only id is provided", () => {
|
||||
const result = getPlanningEntryMutationId({ id: "id-only" });
|
||||
expect(result).toBe("id-only");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Return-type guarantee
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it("always returns a string", () => {
|
||||
const cases = [
|
||||
{ id: "x", entityId: "e", sourceAllocationId: "a" },
|
||||
{ id: "x", entityId: null, sourceAllocationId: "a" },
|
||||
{ id: "x", entityId: null, sourceAllocationId: null },
|
||||
];
|
||||
cases.forEach((entry) => {
|
||||
expect(typeof getPlanningEntryMutationId(entry)).toBe("string");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,275 @@
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,344 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
ALLOCATION_STATUS_BADGE,
|
||||
VACATION_STATUS_BADGE,
|
||||
VACATION_TYPE_LABELS,
|
||||
VACATION_TYPE_BADGE,
|
||||
PROJECT_STATUS_BADGE,
|
||||
ORDER_TYPE_BADGE,
|
||||
VACATION_TIMELINE_COLORS,
|
||||
VACATION_TIMELINE_BORDER,
|
||||
VACATION_TYPE_LABELS_SHORT,
|
||||
VACATION_CALENDAR_COLORS,
|
||||
} from "./status-styles.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ALLOCATION_STATUS_BADGE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("ALLOCATION_STATUS_BADGE", () => {
|
||||
const expectedKeys = ["ACTIVE", "PROPOSED", "CONFIRMED", "COMPLETED", "CANCELLED"];
|
||||
|
||||
it("contains all expected allocation status keys", () => {
|
||||
expectedKeys.forEach((key) => {
|
||||
expect(ALLOCATION_STATUS_BADGE).toHaveProperty(key);
|
||||
});
|
||||
});
|
||||
|
||||
it("has no extra unexpected keys", () => {
|
||||
expect(Object.keys(ALLOCATION_STATUS_BADGE).sort()).toEqual(expectedKeys.sort());
|
||||
});
|
||||
|
||||
it.each(expectedKeys)("value for %s is a non-empty string", (key) => {
|
||||
expect(typeof ALLOCATION_STATUS_BADGE[key]).toBe("string");
|
||||
expect(ALLOCATION_STATUS_BADGE[key]!.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("ACTIVE uses green classes", () => {
|
||||
expect(ALLOCATION_STATUS_BADGE.ACTIVE).toContain("green");
|
||||
});
|
||||
|
||||
it("CANCELLED uses red classes", () => {
|
||||
expect(ALLOCATION_STATUS_BADGE.CANCELLED).toContain("red");
|
||||
});
|
||||
|
||||
it("PROPOSED uses yellow classes", () => {
|
||||
expect(ALLOCATION_STATUS_BADGE.PROPOSED).toContain("yellow");
|
||||
});
|
||||
|
||||
it("CONFIRMED uses blue classes", () => {
|
||||
expect(ALLOCATION_STATUS_BADGE.CONFIRMED).toContain("blue");
|
||||
});
|
||||
|
||||
it("COMPLETED uses gray classes", () => {
|
||||
expect(ALLOCATION_STATUS_BADGE.COMPLETED).toContain("gray");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VACATION_STATUS_BADGE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("VACATION_STATUS_BADGE", () => {
|
||||
const expectedKeys = ["PENDING", "APPROVED", "REJECTED", "CANCELLED"];
|
||||
|
||||
it("contains all expected vacation status keys", () => {
|
||||
expectedKeys.forEach((key) => {
|
||||
expect(VACATION_STATUS_BADGE).toHaveProperty(key);
|
||||
});
|
||||
});
|
||||
|
||||
it("has no extra unexpected keys", () => {
|
||||
expect(Object.keys(VACATION_STATUS_BADGE).sort()).toEqual(expectedKeys.sort());
|
||||
});
|
||||
|
||||
it.each(expectedKeys)("value for %s is a non-empty string", (key) => {
|
||||
expect(typeof VACATION_STATUS_BADGE[key]).toBe("string");
|
||||
expect(VACATION_STATUS_BADGE[key]!.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("APPROVED uses green/emerald classes", () => {
|
||||
expect(VACATION_STATUS_BADGE.APPROVED).toMatch(/emerald|green/);
|
||||
});
|
||||
|
||||
it("REJECTED uses red classes", () => {
|
||||
expect(VACATION_STATUS_BADGE.REJECTED).toContain("red");
|
||||
});
|
||||
|
||||
it("PENDING uses amber/yellow classes", () => {
|
||||
expect(VACATION_STATUS_BADGE.PENDING).toMatch(/amber|yellow/);
|
||||
});
|
||||
|
||||
it("CANCELLED uses gray classes", () => {
|
||||
expect(VACATION_STATUS_BADGE.CANCELLED).toContain("gray");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VACATION_TYPE_LABELS
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("VACATION_TYPE_LABELS", () => {
|
||||
it("provides human-readable label for ANNUAL", () => {
|
||||
expect(VACATION_TYPE_LABELS.ANNUAL).toBe("Annual Leave");
|
||||
});
|
||||
|
||||
it("provides human-readable label for SICK", () => {
|
||||
expect(VACATION_TYPE_LABELS.SICK).toBe("Sick Leave");
|
||||
});
|
||||
|
||||
it("provides human-readable label for PUBLIC_HOLIDAY", () => {
|
||||
expect(VACATION_TYPE_LABELS.PUBLIC_HOLIDAY).toBe("Public Holiday");
|
||||
});
|
||||
|
||||
it("provides human-readable label for OTHER", () => {
|
||||
expect(VACATION_TYPE_LABELS.OTHER).toBe("Other");
|
||||
});
|
||||
|
||||
it("covers exactly the four expected types", () => {
|
||||
expect(Object.keys(VACATION_TYPE_LABELS).sort()).toEqual([
|
||||
"ANNUAL",
|
||||
"OTHER",
|
||||
"PUBLIC_HOLIDAY",
|
||||
"SICK",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VACATION_TYPE_BADGE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("VACATION_TYPE_BADGE", () => {
|
||||
const expectedKeys = ["ANNUAL", "SICK", "PUBLIC_HOLIDAY", "OTHER"];
|
||||
|
||||
it("contains all expected vacation type keys", () => {
|
||||
expectedKeys.forEach((key) => {
|
||||
expect(VACATION_TYPE_BADGE).toHaveProperty(key);
|
||||
});
|
||||
});
|
||||
|
||||
it("has no extra unexpected keys", () => {
|
||||
expect(Object.keys(VACATION_TYPE_BADGE).sort()).toEqual(expectedKeys.sort());
|
||||
});
|
||||
|
||||
it.each(expectedKeys)("value for %s is a non-empty string", (key) => {
|
||||
expect(typeof VACATION_TYPE_BADGE[key]).toBe("string");
|
||||
expect(VACATION_TYPE_BADGE[key]!.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("SICK uses red classes", () => {
|
||||
expect(VACATION_TYPE_BADGE.SICK).toContain("red");
|
||||
});
|
||||
|
||||
it("OTHER uses purple classes", () => {
|
||||
expect(VACATION_TYPE_BADGE.OTHER).toContain("purple");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PROJECT_STATUS_BADGE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("PROJECT_STATUS_BADGE", () => {
|
||||
const expectedKeys = ["DRAFT", "ACTIVE", "ON_HOLD", "COMPLETED", "CANCELLED"];
|
||||
|
||||
it("contains all expected project status keys", () => {
|
||||
expectedKeys.forEach((key) => {
|
||||
expect(PROJECT_STATUS_BADGE).toHaveProperty(key);
|
||||
});
|
||||
});
|
||||
|
||||
it("has no extra unexpected keys", () => {
|
||||
expect(Object.keys(PROJECT_STATUS_BADGE).sort()).toEqual(expectedKeys.sort());
|
||||
});
|
||||
|
||||
it.each(expectedKeys)("value for %s is a non-empty string", (key) => {
|
||||
expect(typeof PROJECT_STATUS_BADGE[key]).toBe("string");
|
||||
expect(PROJECT_STATUS_BADGE[key]!.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("DRAFT uses gray classes", () => {
|
||||
expect(PROJECT_STATUS_BADGE.DRAFT).toContain("gray");
|
||||
});
|
||||
|
||||
it("ACTIVE uses green classes", () => {
|
||||
expect(PROJECT_STATUS_BADGE.ACTIVE).toContain("green");
|
||||
});
|
||||
|
||||
it("ON_HOLD uses yellow classes", () => {
|
||||
expect(PROJECT_STATUS_BADGE.ON_HOLD).toContain("yellow");
|
||||
});
|
||||
|
||||
it("COMPLETED uses blue classes", () => {
|
||||
expect(PROJECT_STATUS_BADGE.COMPLETED).toContain("blue");
|
||||
});
|
||||
|
||||
it("CANCELLED uses red classes", () => {
|
||||
expect(PROJECT_STATUS_BADGE.CANCELLED).toContain("red");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ORDER_TYPE_BADGE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("ORDER_TYPE_BADGE", () => {
|
||||
const expectedKeys = ["BD", "CHARGEABLE", "INTERNAL", "OVERHEAD"];
|
||||
|
||||
it("contains all expected order type keys", () => {
|
||||
expectedKeys.forEach((key) => {
|
||||
expect(ORDER_TYPE_BADGE).toHaveProperty(key);
|
||||
});
|
||||
});
|
||||
|
||||
it("has no extra unexpected keys", () => {
|
||||
expect(Object.keys(ORDER_TYPE_BADGE).sort()).toEqual(expectedKeys.sort());
|
||||
});
|
||||
|
||||
it.each(expectedKeys)("value for %s is a non-empty string", (key) => {
|
||||
expect(typeof ORDER_TYPE_BADGE[key]).toBe("string");
|
||||
expect(ORDER_TYPE_BADGE[key]!.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("BD uses purple classes", () => {
|
||||
expect(ORDER_TYPE_BADGE.BD).toContain("purple");
|
||||
});
|
||||
|
||||
it("CHARGEABLE uses green classes", () => {
|
||||
expect(ORDER_TYPE_BADGE.CHARGEABLE).toContain("green");
|
||||
});
|
||||
|
||||
it("INTERNAL uses blue classes", () => {
|
||||
expect(ORDER_TYPE_BADGE.INTERNAL).toContain("blue");
|
||||
});
|
||||
|
||||
it("OVERHEAD uses gray classes", () => {
|
||||
expect(ORDER_TYPE_BADGE.OVERHEAD).toContain("gray");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VACATION_TIMELINE_COLORS
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("VACATION_TIMELINE_COLORS", () => {
|
||||
const expectedKeys = ["ANNUAL", "SICK", "PUBLIC_HOLIDAY", "OTHER"];
|
||||
|
||||
it("covers all vacation types", () => {
|
||||
expectedKeys.forEach((key) => {
|
||||
expect(VACATION_TIMELINE_COLORS).toHaveProperty(key);
|
||||
});
|
||||
});
|
||||
|
||||
it.each(expectedKeys)("value for %s is a non-empty string", (key) => {
|
||||
expect(typeof VACATION_TIMELINE_COLORS[key]).toBe("string");
|
||||
expect(VACATION_TIMELINE_COLORS[key]!.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("SICK uses red classes", () => {
|
||||
expect(VACATION_TIMELINE_COLORS.SICK).toContain("red");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VACATION_TIMELINE_BORDER
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("VACATION_TIMELINE_BORDER", () => {
|
||||
const expectedKeys = ["ANNUAL", "SICK", "PUBLIC_HOLIDAY", "OTHER"];
|
||||
|
||||
it("covers all vacation types", () => {
|
||||
expectedKeys.forEach((key) => {
|
||||
expect(VACATION_TIMELINE_BORDER).toHaveProperty(key);
|
||||
});
|
||||
});
|
||||
|
||||
it.each(expectedKeys)("value for %s starts with 'border-'", (key) => {
|
||||
expect(VACATION_TIMELINE_BORDER[key]).toMatch(/^border-/);
|
||||
});
|
||||
|
||||
it("SICK uses red border", () => {
|
||||
expect(VACATION_TIMELINE_BORDER.SICK).toContain("red");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VACATION_TYPE_LABELS_SHORT
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("VACATION_TYPE_LABELS_SHORT", () => {
|
||||
it("provides short label for ANNUAL", () => {
|
||||
expect(VACATION_TYPE_LABELS_SHORT.ANNUAL).toBe("Annual");
|
||||
});
|
||||
|
||||
it("provides short label for SICK", () => {
|
||||
expect(VACATION_TYPE_LABELS_SHORT.SICK).toBe("Sick");
|
||||
});
|
||||
|
||||
it("provides short label for PUBLIC_HOLIDAY", () => {
|
||||
expect(VACATION_TYPE_LABELS_SHORT.PUBLIC_HOLIDAY).toBe("Holiday");
|
||||
});
|
||||
|
||||
it("provides short label for OTHER", () => {
|
||||
expect(VACATION_TYPE_LABELS_SHORT.OTHER).toBe("Other");
|
||||
});
|
||||
|
||||
it("short labels are shorter than or equal to full labels", () => {
|
||||
(Object.keys(VACATION_TYPE_LABELS_SHORT) as string[]).forEach((key) => {
|
||||
const short = VACATION_TYPE_LABELS_SHORT[key]!;
|
||||
const full = VACATION_TYPE_LABELS[key] ?? short;
|
||||
expect(short.length).toBeLessThanOrEqual(full.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VACATION_CALENDAR_COLORS
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("VACATION_CALENDAR_COLORS", () => {
|
||||
const expectedKeys = ["ANNUAL", "SICK", "PUBLIC_HOLIDAY", "OTHER"];
|
||||
|
||||
it("covers all vacation types", () => {
|
||||
expectedKeys.forEach((key) => {
|
||||
expect(VACATION_CALENDAR_COLORS).toHaveProperty(key);
|
||||
});
|
||||
});
|
||||
|
||||
it.each(expectedKeys)("value for %s starts with 'bg-'", (key) => {
|
||||
expect(VACATION_CALENDAR_COLORS[key]).toMatch(/^bg-/);
|
||||
});
|
||||
|
||||
it("SICK uses red", () => {
|
||||
expect(VACATION_CALENDAR_COLORS.SICK).toContain("red");
|
||||
});
|
||||
|
||||
it("PUBLIC_HOLIDAY uses emerald", () => {
|
||||
expect(VACATION_CALENDAR_COLORS.PUBLIC_HOLIDAY).toContain("emerald");
|
||||
});
|
||||
|
||||
it("OTHER uses purple", () => {
|
||||
expect(VACATION_CALENDAR_COLORS.OTHER).toContain("purple");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { uuid } from "./uuid.js";
|
||||
|
||||
// RFC 4122 v4 UUID pattern
|
||||
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
describe("uuid", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shape and format
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it("returns a string", () => {
|
||||
expect(typeof uuid()).toBe("string");
|
||||
});
|
||||
|
||||
it("matches the RFC 4122 v4 UUID format", () => {
|
||||
expect(uuid()).toMatch(UUID_V4_REGEX);
|
||||
});
|
||||
|
||||
it("has the correct length (36 characters)", () => {
|
||||
expect(uuid()).toHaveLength(36);
|
||||
});
|
||||
|
||||
it("contains hyphens at positions 8, 13, 18, and 23", () => {
|
||||
const id = uuid();
|
||||
expect(id[8]).toBe("-");
|
||||
expect(id[13]).toBe("-");
|
||||
expect(id[18]).toBe("-");
|
||||
expect(id[23]).toBe("-");
|
||||
});
|
||||
|
||||
it("has '4' as the version digit at position 14", () => {
|
||||
expect(uuid()[14]).toBe("4");
|
||||
});
|
||||
|
||||
it("has a valid variant digit at position 19 (8, 9, a, or b)", () => {
|
||||
const variantChar = uuid()[19]!;
|
||||
expect(["8", "9", "a", "b"]).toContain(variantChar.toLowerCase());
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Uniqueness
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it("generates unique values on successive calls", () => {
|
||||
const ids = new Set(Array.from({ length: 1000 }, () => uuid()));
|
||||
expect(ids.size).toBe(1000);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fallback path (crypto.randomUUID not available)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("fallback when crypto.randomUUID is unavailable", () => {
|
||||
let originalCrypto: Crypto;
|
||||
|
||||
beforeEach(() => {
|
||||
originalCrypto = globalThis.crypto;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(globalThis, "crypto", {
|
||||
value: originalCrypto,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to Math.random-based generation when randomUUID is missing", () => {
|
||||
// Stub crypto so that randomUUID is absent
|
||||
Object.defineProperty(globalThis, "crypto", {
|
||||
value: {},
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const id = uuid();
|
||||
expect(id).toMatch(UUID_V4_REGEX);
|
||||
});
|
||||
|
||||
it("fallback still produces unique values", () => {
|
||||
Object.defineProperty(globalThis, "crypto", {
|
||||
value: {},
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const ids = new Set(Array.from({ length: 500 }, () => uuid()));
|
||||
expect(ids.size).toBe(500);
|
||||
});
|
||||
|
||||
it("falls back when crypto is undefined", () => {
|
||||
Object.defineProperty(globalThis, "crypto", {
|
||||
value: undefined,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const id = uuid();
|
||||
expect(id).toMatch(UUID_V4_REGEX);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Native path (crypto.randomUUID explicitly available)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("native crypto.randomUUID path", () => {
|
||||
it("delegates to crypto.randomUUID when it is available", () => {
|
||||
const spy = vi.spyOn(globalThis.crypto, "randomUUID");
|
||||
|
||||
uuid();
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user