- Vitest multi-project setup (unit / integration / component) - Real-Postgres integration harness via in-process PGlite (TEST_DATABASE_URL override), migrations applied per worker; production code untouched - Unit: routing, locale, validation/Zod schemas, metadata, mail, site-theme, marquee, media, portfolio helpers, plus architecture-rule tests - Integration: Prisma data layer, API routes, and all server actions - Component: UI primitives and form components (jsdom + Testing Library) - 371 tests passing
135 lines
4.8 KiB
TypeScript
135 lines
4.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
getFirstIncompleteWizardStep,
|
|
getPortfolioWizardProgress,
|
|
isPortfolioAssetReady,
|
|
isPortfolioSectionReady,
|
|
} from "@/lib/portfolio-form-progress";
|
|
|
|
const titles = { titleAr: "ع", titleEn: "en", titleDe: "de" };
|
|
|
|
function section(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
type: "RICH_TEXT" as const,
|
|
...titles,
|
|
bodyAr: "ب",
|
|
bodyEn: "body",
|
|
bodyDe: "koerper",
|
|
linkUrl: "",
|
|
mediaAssetId: "",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("isPortfolioSectionReady", () => {
|
|
it("requires titles in all languages", () => {
|
|
expect(isPortfolioSectionReady(section({ titleEn: "" }))).toBe(false);
|
|
});
|
|
|
|
it("RICH_TEXT/STATS/DELIVERABLES require body in all languages", () => {
|
|
for (const type of ["RICH_TEXT", "STATS", "DELIVERABLES"] as const) {
|
|
expect(isPortfolioSectionReady(section({ type }))).toBe(true);
|
|
expect(isPortfolioSectionReady(section({ type, bodyDe: "" }))).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("GALLERY requires a media asset id", () => {
|
|
expect(isPortfolioSectionReady(section({ type: "GALLERY", mediaAssetId: "asset_1" }))).toBe(true);
|
|
expect(isPortfolioSectionReady(section({ type: "GALLERY", mediaAssetId: "" }))).toBe(false);
|
|
});
|
|
|
|
it("LINK requires a link url", () => {
|
|
expect(isPortfolioSectionReady(section({ type: "LINK", linkUrl: "https://x" }))).toBe(true);
|
|
expect(isPortfolioSectionReady(section({ type: "LINK", linkUrl: "" }))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isPortfolioAssetReady", () => {
|
|
it("requires media asset id and alt text in all languages", () => {
|
|
expect(isPortfolioAssetReady({ mediaAssetId: "a", altAr: "ع", altEn: "e", altDe: "d" })).toBe(true);
|
|
expect(isPortfolioAssetReady({ mediaAssetId: "", altAr: "ع", altEn: "e", altDe: "d" })).toBe(false);
|
|
expect(isPortfolioAssetReady({ mediaAssetId: "a", altAr: "ع", altEn: "", altDe: "d" })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("getPortfolioWizardProgress", () => {
|
|
const completeInput = {
|
|
basics: {
|
|
categoryId: "cat_1",
|
|
slug: "case-study",
|
|
clientName: "Client",
|
|
projectYear: "2025",
|
|
sortOrder: "1",
|
|
viewMode: "GRID" as const,
|
|
},
|
|
content: {
|
|
titleAr: "ع", titleEn: "t", titleDe: "t",
|
|
serviceLabelAr: "خ", serviceLabelEn: "s", serviceLabelDe: "s",
|
|
summaryAr: "م", summaryEn: "sum", summaryDe: "zus",
|
|
},
|
|
sections: [section()],
|
|
assets: [{ mediaAssetId: "asset_1", altAr: "ع", altEn: "e", altDe: "d" }],
|
|
};
|
|
|
|
it("marks all steps complete for a fully filled project", () => {
|
|
const progress = getPortfolioWizardProgress(completeInput);
|
|
expect(progress.every((step) => step.complete)).toBe(true);
|
|
expect(getFirstIncompleteWizardStep(progress)).toBeNull();
|
|
});
|
|
|
|
it("rejects an invalid slug", () => {
|
|
const progress = getPortfolioWizardProgress({
|
|
...completeInput,
|
|
basics: { ...completeInput.basics, slug: "Invalid Slug" },
|
|
});
|
|
expect(progress.find((s) => s.key === "basics")?.complete).toBe(false);
|
|
expect(getFirstIncompleteWizardStep(progress)).toBe("basics");
|
|
});
|
|
|
|
it("enforces year bounds 2000..2100", () => {
|
|
const before = getPortfolioWizardProgress({
|
|
...completeInput,
|
|
basics: { ...completeInput.basics, projectYear: "1999" },
|
|
});
|
|
expect(before.find((s) => s.key === "basics")?.complete).toBe(false);
|
|
const after = getPortfolioWizardProgress({
|
|
...completeInput,
|
|
basics: { ...completeInput.basics, projectYear: "2101" },
|
|
});
|
|
expect(after.find((s) => s.key === "basics")?.complete).toBe(false);
|
|
});
|
|
|
|
it("enforces sortOrder bounds 0..9999", () => {
|
|
const progress = getPortfolioWizardProgress({
|
|
...completeInput,
|
|
basics: { ...completeInput.basics, sortOrder: "-1" },
|
|
});
|
|
expect(progress.find((s) => s.key === "basics")?.complete).toBe(false);
|
|
});
|
|
|
|
it("content is incomplete when a summary locale is missing", () => {
|
|
const progress = getPortfolioWizardProgress({
|
|
...completeInput,
|
|
content: { ...completeInput.content, summaryEn: "" },
|
|
});
|
|
expect(progress.find((s) => s.key === "content")?.complete).toBe(false);
|
|
expect(getFirstIncompleteWizardStep(progress)).toBe("content");
|
|
});
|
|
|
|
it("sections/assets steps require at least one ready entry", () => {
|
|
const noEntries = getPortfolioWizardProgress({ ...completeInput, sections: [], assets: [] });
|
|
expect(noEntries.find((s) => s.key === "sections")?.complete).toBe(false);
|
|
expect(noEntries.find((s) => s.key === "assets")?.complete).toBe(false);
|
|
});
|
|
|
|
it("summarizes section/asset readiness counts", () => {
|
|
const progress = getPortfolioWizardProgress({
|
|
...completeInput,
|
|
sections: [section(), section({ titleEn: "" })],
|
|
});
|
|
expect(progress.find((s) => s.key === "sections")?.summary).toBe("1/2 sections ready.");
|
|
expect(progress.find((s) => s.key === "sections")?.complete).toBe(false);
|
|
});
|
|
});
|