Files
sass-mohfarawati/tests/portfolio-validation.test.ts
T
moh 4b535694bc ADDED - Save an unfinished project as a draft
- Add a Save as draft button that bypasses the completeness gate: the user
  can save partial work and finish later
- projectDraftInputSchema makes the user-facing copy (title, summary, service
  label, client) optional; the action auto-generates a slug from the title
  (or draft-<timestamp>), defaults the year to the current year, and forces
  the project unpublished
- Drafts drop not-yet-valid sections/assets instead of failing the save
- Pass intent via a hidden field set on click so the server reliably sees it
- Tests: draft schema accepts empty copy a strict save rejects, and still
  requires a slug and a valid year
2026-09-20 16:52:13 +02:00

355 lines
8.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
getFirstIncompleteWizardStep,
getPortfolioWizardProgress,
isPortfolioAssetReady,
isPortfolioSectionReady,
} from "../lib/portfolio-form-progress";
import {
assetInputSchema,
categoryInputSchema,
projectDraftInputSchema,
projectInputSchema,
sectionInputSchema,
} from "../lib/portfolio-validation";
describe("portfolio validation", () => {
it("accepts a valid category payload", () => {
expect(
categoryInputSchema.parse({
slug: "branding",
nameAr: "الهوية",
nameEn: "Branding",
nameDe: "Branding",
descriptionAr: "وصف",
descriptionEn: "Description",
descriptionDe: "Beschreibung",
sortOrder: 1,
isActive: true,
}).slug,
).toBe("branding");
});
it("rejects invalid project slugs", () => {
expect(() =>
projectInputSchema.parse({
categoryId: "cat_1",
slug: "Invalid Slug",
viewMode: "GRID",
titleAr: "عنوان",
titleEn: "Title",
titleDe: "Titel",
summaryAr: "ملخص",
summaryEn: "Summary",
summaryDe: "Zusammenfassung",
clientName: "Client",
projectYear: 2025,
serviceLabelAr: "خدمة",
serviceLabelEn: "Service",
serviceLabelDe: "Service",
previewUrl: "https://example.com",
currentCoverImagePath: "",
coverMedia: {
mode: "external",
assetId: "",
url: "https://example.com/cover.jpg",
label: "Cover",
kind: "IMAGE",
},
sortOrder: 1,
isFeatured: false,
isPublished: true,
sections: [],
assets: [],
}),
).toThrow(/slug/i);
});
it("draft schema accepts empty user-facing copy that a strict save rejects", () => {
const draftPayload = {
categoryId: "cat_1",
slug: "draft-123",
viewMode: "GRID" as const,
titleAr: "",
titleEn: "",
titleDe: "",
summaryAr: "",
summaryEn: "",
summaryDe: "",
clientName: "",
projectYear: 2026,
serviceLabelAr: "",
serviceLabelEn: "",
serviceLabelDe: "",
previewUrl: "",
currentCoverImagePath: "",
sortOrder: 0,
isFeatured: false,
isPublished: false,
sections: [],
assets: [],
};
expect(projectDraftInputSchema.parse(draftPayload).slug).toBe("draft-123");
expect(() => projectInputSchema.parse(draftPayload)).toThrow();
});
it("draft schema still requires a slug and a valid year", () => {
const base = {
categoryId: "cat_1",
slug: "draft-1",
viewMode: "GRID" as const,
titleAr: "",
titleEn: "",
titleDe: "",
summaryAr: "",
summaryEn: "",
summaryDe: "",
clientName: "",
projectYear: 2026,
serviceLabelAr: "",
serviceLabelEn: "",
serviceLabelDe: "",
previewUrl: "",
currentCoverImagePath: "",
sortOrder: 0,
isFeatured: false,
isPublished: false,
sections: [],
assets: [],
};
expect(() => projectDraftInputSchema.parse({ ...base, slug: "" })).toThrow(/slug/i);
expect(() => projectDraftInputSchema.parse({ ...base, projectYear: 1999 })).toThrow();
});
it("accepts valid section and asset payloads", () => {
expect(
sectionInputSchema.parse({
type: "RICH_TEXT",
titleAr: "العنوان",
titleEn: "Title",
titleDe: "Titel",
bodyAr: "النص",
bodyEn: "Body",
bodyDe: "Text",
imagePath: "/uploads/media/covers/example.svg",
media: {
mode: "library",
assetId: "asset_1",
url: "",
label: "Section Image",
kind: "IMAGE",
},
linkUrl: "https://example.com",
sortOrder: 0,
}).type,
).toBe("RICH_TEXT");
expect(
sectionInputSchema.parse({
type: "GALLERY",
titleAr: "معرض",
titleEn: "Single Image",
titleDe: "Einzelbild",
bodyAr: "",
bodyEn: "",
bodyDe: "",
imagePath: "/uploads/media/sections/example.svg",
media: {
mode: "library",
assetId: "asset_2",
url: "",
label: "Single Image",
kind: "IMAGE",
},
linkUrl: "",
sortOrder: 1,
}).type,
).toBe("GALLERY");
expect(
assetInputSchema.parse({
kind: "IMAGE",
filePath: "/uploads/media/assets/example.svg",
fileFieldName: "",
media: {
mode: "external",
assetId: "",
url: "https://example.com/example.svg",
label: "Example",
kind: "IMAGE",
},
altAr: "بديل",
altEn: "Alt",
altDe: "Alt",
sortOrder: 0,
}).kind,
).toBe("IMAGE");
});
it("accepts valid project view modes", () => {
expect(
projectInputSchema.parse({
categoryId: "cat_1",
slug: "case-study-entry",
viewMode: "CASE_STUDY",
titleAr: "عنوان",
titleEn: "Title",
titleDe: "Titel",
summaryAr: "ملخص",
summaryEn: "Summary",
summaryDe: "Zusammenfassung",
clientName: "Client",
projectYear: 2025,
serviceLabelAr: "خدمة",
serviceLabelEn: "Service",
serviceLabelDe: "Service",
previewUrl: "https://example.com",
currentCoverImagePath: "",
coverMedia: {
mode: "external",
assetId: "",
url: "https://example.com/cover.jpg",
label: "Cover",
kind: "IMAGE",
},
sortOrder: 1,
isFeatured: false,
isPublished: true,
sections: [],
assets: [],
}).viewMode,
).toBe("CASE_STUDY");
});
it("rejects invalid media payloads", () => {
expect(() =>
assetInputSchema.parse({
kind: "IMAGE",
filePath: "",
fileFieldName: "",
media: {
mode: "external",
assetId: "",
url: "not-a-url",
label: "Broken",
kind: "IMAGE",
},
altAr: "بديل",
altEn: "Alt",
altDe: "Alt",
sortOrder: 0,
}),
).toThrow(/url/i);
});
it("rejects link sections without a link", () => {
expect(() =>
sectionInputSchema.parse({
type: "LINK",
titleAr: "رابط",
titleEn: "Link",
titleDe: "Link",
bodyAr: "",
bodyEn: "",
bodyDe: "",
imagePath: "",
media: {
mode: "upload",
assetId: "",
url: "",
label: "",
kind: "IMAGE",
},
linkUrl: "",
sortOrder: 0,
}),
).toThrow(/link/i);
});
it("calculates wizard progress and points to the first incomplete step", () => {
const progress = getPortfolioWizardProgress({
basics: {
categoryId: "cat_1",
slug: "case-study-entry",
clientName: "Client",
projectYear: "2025",
sortOrder: "1",
viewMode: "GRID",
},
content: {
titleAr: "عنوان",
titleEn: "Title",
titleDe: "Titel",
serviceLabelAr: "خدمة",
serviceLabelEn: "Service",
serviceLabelDe: "Service",
summaryAr: "",
summaryEn: "",
summaryDe: "",
},
sections: [
{
type: "RICH_TEXT",
titleAr: "العنوان",
titleEn: "Title",
titleDe: "Titel",
bodyAr: "النص",
bodyEn: "Body",
bodyDe: "Text",
linkUrl: "",
mediaAssetId: "",
},
],
assets: [
{
mediaAssetId: "asset_1",
altAr: "بديل",
altEn: "Alt",
altDe: "Alt",
},
],
});
expect(progress.find((step) => step.key === "basics")?.complete).toBe(true);
expect(progress.find((step) => step.key === "content")?.complete).toBe(false);
expect(getFirstIncompleteWizardStep(progress)).toBe("content");
});
it("marks sections and assets ready only when required fields are present", () => {
expect(
isPortfolioSectionReady({
type: "LINK",
titleAr: "رابط",
titleEn: "Link",
titleDe: "Link",
bodyAr: "",
bodyEn: "",
bodyDe: "",
linkUrl: "https://example.com",
mediaAssetId: "",
}),
).toBe(true);
expect(
isPortfolioAssetReady({
mediaAssetId: "asset_1",
altAr: "بديل",
altEn: "Alt",
altDe: "Alt",
}),
).toBe(true);
expect(
isPortfolioAssetReady({
mediaAssetId: "",
altAr: "بديل",
altEn: "Alt",
altDe: "Alt",
}),
).toBe(false);
});
});