Refine use cases rule examples
This commit is contained in:
@@ -106,6 +106,22 @@ describe("Rule Component", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clicking editable title calls onTitleClick and does not fire card onClick", () => {
|
||||
const onCard = vi.fn();
|
||||
const onTitle = vi.fn();
|
||||
render(
|
||||
<Rule
|
||||
{...defaultProps}
|
||||
expanded={true}
|
||||
onClick={onCard}
|
||||
onTitleClick={onTitle}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByTestId("rule-title-edit"));
|
||||
expect(onTitle).toHaveBeenCalledTimes(1);
|
||||
expect(onCard).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clicking editable description calls onDescriptionClick and does not fire card onClick", () => {
|
||||
const onCard = vi.fn();
|
||||
const onDesc = vi.fn();
|
||||
|
||||
@@ -314,6 +314,40 @@ describe("parseDocumentSectionsForDisplay", () => {
|
||||
expect(parseDocumentSectionsForDisplay(doc)).toEqual(doc.sections);
|
||||
});
|
||||
|
||||
it("accepts entries with labeled blocks and omits body in JSON (normalized to \"\")", () => {
|
||||
const doc = {
|
||||
sections: [
|
||||
{
|
||||
categoryName: "Membership",
|
||||
entries: [
|
||||
{
|
||||
title: "Open membership",
|
||||
blocks: [
|
||||
{ label: "Eligibility", body: "Anyone may join." },
|
||||
{ label: "Process", body: "Sign the sheet." },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(parseDocumentSectionsForDisplay(doc)).toEqual([
|
||||
{
|
||||
categoryName: "Membership",
|
||||
entries: [
|
||||
{
|
||||
title: "Open membership",
|
||||
body: "",
|
||||
blocks: [
|
||||
{ label: "Eligibility", body: "Anyone may join." },
|
||||
{ label: "Process", body: "Sign the sheet." },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("accepts entries with labeled blocks and empty body", () => {
|
||||
const doc = {
|
||||
sections: [
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isChromelessNavigationPath } from "../../../lib/navigationChromelessPath";
|
||||
|
||||
describe("isChromelessNavigationPath", () => {
|
||||
it.each([
|
||||
["/create", true],
|
||||
["/create/completed", true],
|
||||
["/login", true],
|
||||
["/use-cases/mutual-aid-colorado/rule", true],
|
||||
["/use-cases/food-not-bombs/rule/", true],
|
||||
["/", false],
|
||||
["/use-cases", false],
|
||||
["/use-cases/mutual-aid-colorado", false],
|
||||
["/use-cases/mutual-aid-colorado/rule/extra", false],
|
||||
] as const)("returns %s -> %s", (pathname, expected) => {
|
||||
expect(isChromelessNavigationPath(pathname)).toBe(expected);
|
||||
});
|
||||
|
||||
it("returns false for null or undefined", () => {
|
||||
expect(isChromelessNavigationPath(null)).toBe(false);
|
||||
expect(isChromelessNavigationPath(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import useCasesCompletedRules from "../../messages/en/pages/useCasesCompletedRules.json";
|
||||
import { createFlowStateFromPublishedRule } from "../../lib/create/publishedDocumentToCreateFlowState";
|
||||
import { normalizePublishedDocumentForEdit } from "../../lib/create/normalizePublishedDocumentForEdit";
|
||||
|
||||
describe("normalizePublishedDocumentForEdit", () => {
|
||||
it("derives methodSelections and coreValues from use-case display sections", () => {
|
||||
const fixture = useCasesCompletedRules.mutualAidColorado;
|
||||
const normalized = normalizePublishedDocumentForEdit(fixture.document);
|
||||
|
||||
expect(Array.isArray(normalized.coreValues)).toBe(true);
|
||||
expect((normalized.coreValues as unknown[]).length).toBeGreaterThan(0);
|
||||
|
||||
const ms = normalized.methodSelections as Record<string, unknown>;
|
||||
expect(Array.isArray(ms.membership)).toBe(true);
|
||||
expect((ms.membership as unknown[]).length).toBeGreaterThan(0);
|
||||
expect(Array.isArray(ms.decisionApproaches)).toBe(true);
|
||||
expect((ms.decisionApproaches as unknown[]).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("is idempotent when methodSelections already exist", () => {
|
||||
const once = normalizePublishedDocumentForEdit(
|
||||
useCasesCompletedRules.mutualAidColorado.document,
|
||||
);
|
||||
const twice = normalizePublishedDocumentForEdit(once);
|
||||
expect(twice.methodSelections).toEqual(once.methodSelections);
|
||||
expect(twice.coreValues).toEqual(once.coreValues);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFlowStateFromPublishedRule with section-only documents", () => {
|
||||
it("hydrates method ids from normalized use-case duplicate shape", () => {
|
||||
const doc = normalizePublishedDocumentForEdit(
|
||||
useCasesCompletedRules.mutualAidColorado.document,
|
||||
);
|
||||
const patch = createFlowStateFromPublishedRule({
|
||||
id: "rule-1",
|
||||
title: "Mutual Aid Colorado Template (Copy)",
|
||||
summary: "Summary",
|
||||
document: doc as Record<string, unknown>,
|
||||
});
|
||||
|
||||
expect(patch.selectedMembershipMethodIds?.length).toBeGreaterThan(0);
|
||||
expect(patch.selectedDecisionApproachIds?.length).toBeGreaterThan(0);
|
||||
expect(patch.selectedCoreValueIds?.length).toBeGreaterThan(0);
|
||||
expect(patch.sections).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { useCaseTemplateDuplicateTitle } from "../../lib/useCaseTemplateDuplicate";
|
||||
|
||||
describe("useCaseTemplateDuplicateTitle", () => {
|
||||
it("appends Template (Copy) to the source title", () => {
|
||||
expect(useCaseTemplateDuplicateTitle("BoCo Street Medics")).toBe(
|
||||
"BoCo Street Medics Template (Copy)",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back when the source title is empty", () => {
|
||||
expect(useCaseTemplateDuplicateTitle(" ")).toBe(
|
||||
"Community Rule Template (Copy)",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const isDatabaseConfiguredMock = vi.fn();
|
||||
const createMock = vi.fn();
|
||||
const getSessionUserMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/server/env", () => ({
|
||||
isDatabaseConfigured: () => isDatabaseConfiguredMock(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/db", () => ({
|
||||
prisma: {
|
||||
publishedRule: {
|
||||
create: (...args: unknown[]) => createMock(...args),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/session", () => ({
|
||||
getSessionUser: () => getSessionUserMock(),
|
||||
}));
|
||||
|
||||
import { POST } from "../../app/api/use-cases/[slug]/duplicate/route";
|
||||
|
||||
function makeContext(slug: string) {
|
||||
return { params: Promise.resolve({ slug }) };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
isDatabaseConfiguredMock.mockReset();
|
||||
createMock.mockReset();
|
||||
getSessionUserMock.mockReset();
|
||||
});
|
||||
|
||||
describe("POST /api/use-cases/[slug]/duplicate", () => {
|
||||
it("returns 401 when not signed in", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue(null);
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/use-cases/food-not-bombs/duplicate"),
|
||||
makeContext("food-not-bombs"),
|
||||
);
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("returns 404 for an unknown slug", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue({ id: "u1", email: "a@b.c" });
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/use-cases/unknown/duplicate"),
|
||||
makeContext("unknown"),
|
||||
);
|
||||
expect(res.status).toBe(404);
|
||||
expect(createMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates a published rule from the use-case fixture", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue({ id: "u1", email: "a@b.c" });
|
||||
createMock.mockResolvedValueOnce({
|
||||
id: "r-new",
|
||||
title: "Food Not Bombs Boulder Template (Copy)",
|
||||
summary: "Summary",
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-01T00:00:00Z"),
|
||||
});
|
||||
|
||||
const res = await POST(
|
||||
new NextRequest(
|
||||
"https://x.test/api/use-cases/food-not-bombs/duplicate",
|
||||
),
|
||||
makeContext("food-not-bombs"),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { rule: { id: string; title: string } };
|
||||
expect(body.rule.id).toBe("r-new");
|
||||
expect(createMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
userId: "u1",
|
||||
title: "Food Not Bombs Boulder Template (Copy)",
|
||||
document: expect.objectContaining({
|
||||
methodSelections: expect.objectContaining({
|
||||
membership: expect.arrayContaining([
|
||||
expect.objectContaining({ id: expect.any(String), label: expect.any(String) }),
|
||||
]),
|
||||
}),
|
||||
coreValues: expect.arrayContaining([
|
||||
expect.objectContaining({ chipId: expect.any(String), label: expect.any(String) }),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user