Route kebab Customize through the prefilled policy wizard so existing methods and values can be renamed, reordered, and kept in that field order on the card after Finalize.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-08-21 13:51:37 -06:00
co-authored by Cursor
parent e9951f260b
commit 4006fe5c6c
31 changed files with 3062 additions and 1530 deletions
@@ -75,20 +75,16 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
);
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
const textboxes = within(screen.getByRole("dialog")).getAllByRole("textbox");
expect(textboxes.length).toBe(5);
const corePrincipleField = textboxes[2] as HTMLTextAreaElement;
const textboxes = within(dialog).getAllByRole("textbox");
expect(textboxes.length).toBe(3);
const corePrincipleField = textboxes[0] as HTMLTextAreaElement;
// Preset corePrinciple must seed into the first body textarea so the user
// edits a real starting point rather than an empty field.
expect(corePrincipleField.value.length).toBeGreaterThan(0);
fireEvent.change(corePrincipleField, { target: { value: "Custom principle" } });
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
fireEvent.click(
within(screen.getByRole("dialog")).getByRole("button", {
within(dialog).getByRole("button", {
name: "Add Platform",
}),
);
@@ -162,7 +158,30 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
expect(textareas[2].value).toBe("Saved coc");
});
it("Cancel customize reverts edited preset without persisting (no confirm when unchanged)", async () => {
it("opens meaning fields editable without Customize", async () => {
render(
<ScreenWithStateProbe
onState={() => {
/* noop */
}}
/>,
);
fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
);
const dialog = await screen.findByRole("dialog");
const textareas = within(dialog).getAllByRole(
"textbox",
) as HTMLTextAreaElement[];
expect(textareas[0]).not.toBeDisabled();
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
expect(
screen.getByRole("menuitem", { name: "Customize" }),
).toBeInTheDocument();
});
it("closing the wizard without Finalize leaves the card modal and does not persist", async () => {
let latest: CreateFlowState = {};
render(
<ScreenWithStateProbe
@@ -179,20 +198,84 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
expect(
await screen.findByPlaceholderText("Policy name"),
).toBeInTheDocument();
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(
(within(screen.getByRole("dialog")).getAllByRole(
"textbox",
)[0] as HTMLTextAreaElement).disabled,
).toBe(true);
expect(screen.queryByPlaceholderText("Policy name")).not.toBeInTheDocument();
});
expect(latest.communicationMethodDetailsById).toBeUndefined();
expect(screen.queryByRole("button", { name: "Discard" })).not.toBeInTheDocument();
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(latest.customMethodCardMetaById).toBeUndefined();
});
it("Cancel customize with edits restores snapshot after confirm", async () => {
it("Back from the wizard with edits asks to discard and does not persist", async () => {
let latest: CreateFlowState = {};
render(
<ScreenWithStateProbe
onState={(s) => {
latest = s;
}}
/>,
);
fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
);
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
const nameInput = await screen.findByPlaceholderText("Policy name");
fireEvent.change(nameInput, { target: { value: "Renamed in wizard" } });
fireEvent.click(screen.getByRole("button", { name: "Back" }));
expect(
await screen.findByRole("button", { name: "Keep editing" }),
).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Discard" }));
await waitFor(() => {
expect(screen.queryByPlaceholderText("Policy name")).not.toBeInTheDocument();
});
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(latest.customMethodCardMetaById).toBeUndefined();
});
it("Escape after a field edit stays open when user declines discard confirm", async () => {
render(
<ScreenWithStateProbe
onState={() => {
/* noop */
}}
/>,
);
fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
);
const dialog = await screen.findByRole("dialog");
const textboxes = within(dialog).getAllByRole(
"textbox",
) as HTMLTextAreaElement[];
fireEvent.change(textboxes[0], { target: { value: "Edited principle" } });
fireEvent.keyDown(document, { key: "Escape" });
const keepEditing = await screen.findByRole("button", {
name: "Keep editing",
});
expect(keepEditing.parentElement).toHaveClass(
"absolute",
"left-[16px]",
"top-[12px]",
);
await declineDiscardCustomizeEdits();
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
it("Escape after a field edit discards without persisting when confirmed", async () => {
let latest: CreateFlowState = {};
render(
<ScreenWithStateProbe
@@ -216,67 +299,22 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
);
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
const textboxes = within(dialog).getAllByRole(
"textbox",
) as HTMLTextAreaElement[];
fireEvent.change(textboxes[2], { target: { value: "Edited principle" } });
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
fireEvent.change(textboxes[0], { target: { value: "Edited principle" } });
fireEvent.keyDown(document, { key: "Escape" });
await confirmDiscardCustomizeEdits();
await waitFor(() => {
expect(
(
within(screen.getByRole("dialog")).getAllByRole(
"textbox",
)[0] as HTMLTextAreaElement
).value,
).toBe("Saved principle");
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
expect(
latest.communicationMethodDetailsById?.signal?.corePrinciple,
).toBe("Saved principle");
});
it("dirty Escape close stays open when user declines discard confirm", async () => {
render(
<ScreenWithStateProbe
onState={() => {
/* noop */
}}
/>,
);
fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
);
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
const textboxes = within(dialog).getAllByRole(
"textbox",
) as HTMLTextAreaElement[];
fireEvent.change(textboxes[2], { target: { value: "Edited principle" } });
fireEvent.keyDown(document, { key: "Escape" });
const keepEditing = await screen.findByRole("button", {
name: "Keep editing",
});
expect(keepEditing.parentElement).toHaveClass(
"absolute",
"left-[16px]",
"top-[12px]",
);
await declineDiscardCustomizeEdits();
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
it("persists customized policy title for a custom UUID card on Save", async () => {
it("Customize wizard prefills the card title and persists a rename on Finalize", async () => {
const customId = "00000000-0000-4000-8000-0000000000aa";
let latest: CreateFlowState = {};
render(
@@ -307,13 +345,21 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
const titleInput = within(screen.getByRole("dialog")).getAllByRole(
"textbox",
)[0] as HTMLInputElement;
fireEvent.change(titleInput, { target: { value: "Renamed policy" } });
fireEvent.click(
within(screen.getByRole("dialog")).getByRole("button", { name: "Save" }),
);
expect(
await screen.findByPlaceholderText("Policy name"),
).toBeInTheDocument();
const nameInput = screen.getByPlaceholderText("Policy name");
expect(nameInput).toHaveValue("Original title");
fireEvent.change(nameInput, { target: { value: "Renamed policy" } });
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Next" }));
expect(
await screen.findByText("Custom policy details"),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Core Principle & Scope" }),
).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Finalize" }));
await waitFor(() => {
expect(latest.customMethodCardMetaById?.[customId]?.label).toBe(
@@ -322,7 +368,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
});
});
it("stores preset id title override in customMethodCardMetaById on Save", async () => {
it("stores preset id title override in customMethodCardMetaById on Finalize", async () => {
let latest: CreateFlowState = {};
render(
<ScreenWithStateProbe
@@ -339,15 +385,32 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
const titleInput = within(screen.getByRole("dialog")).getAllByRole(
"textbox",
)[0] as HTMLInputElement;
fireEvent.change(titleInput, {
expect(
await screen.findByPlaceholderText("Policy name"),
).toHaveValue("Signal");
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Next" }));
expect(
await screen.findByText("Custom policy details"),
).toBeInTheDocument();
fireEvent.click(
screen.getByRole("button", { name: "Core Principle & Scope" }),
);
expect(await screen.findByText("Add text block")).toBeInTheDocument();
expect(
screen.getByDisplayValue(/We prioritize privacy and security/i),
).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Back" }));
fireEvent.click(screen.getByRole("button", { name: "Back" }));
fireEvent.click(screen.getByRole("button", { name: "Back" }));
const nameInput = await screen.findByPlaceholderText("Policy name");
expect(nameInput).toHaveValue("Signal");
fireEvent.change(nameInput, {
target: { value: "Custom Signal header" },
});
fireEvent.click(
within(screen.getByRole("dialog")).getByRole("button", { name: "Save" }),
);
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Finalize" }));
await waitFor(() => {
expect(latest.customMethodCardMetaById?.signal?.label).toBe(
@@ -355,4 +418,79 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
);
});
});
it("Customize Finalize shows reordered field blocks in the card modal", async () => {
let latest: CreateFlowState = {};
render(
<ScreenWithStateProbe
onState={(s) => {
latest = s;
}}
/>,
);
fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
);
const cardDialog = await screen.findByRole("dialog");
fireEvent.click(
within(cardDialog).getByRole("button", { name: "More options" }),
);
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
fireEvent.click(await screen.findByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Next" }));
expect(
await screen.findByText("Custom policy details"),
).toBeInTheDocument();
const handles = screen.getAllByRole("button", {
name: "Drag to reorder this field",
});
const rows = screen.getAllByRole("listitem");
const store: Record<string, string> = {};
const dataTransfer = {
effectAllowed: "all",
dropEffect: "move",
setData(type: string, value: string) {
store[type] = value;
},
getData(type: string) {
return store[type] ?? "";
},
};
fireEvent.pointerDown(handles[0]);
fireEvent.dragStart(rows[0], { dataTransfer });
fireEvent.drop(rows[2], { dataTransfer });
fireEvent.click(screen.getByRole("button", { name: "Finalize" }));
await waitFor(() => {
expect(
screen.queryByPlaceholderText("Policy name"),
).not.toBeInTheDocument();
});
await waitFor(() => {
expect(
latest.customMethodCardFieldBlocksById?.signal?.map((b) => b.id),
).toEqual([
"facet-logisticsAdmin",
"facet-codeOfConduct",
"facet-corePrinciple",
]);
});
const result = screen.getByRole("dialog");
const labels = within(result)
.getAllByRole("textbox")
.map((el) => {
const labelledby = el.getAttribute("aria-labelledby");
return labelledby
? (document.getElementById(labelledby)?.textContent ?? "").trim()
: "";
});
expect(labels[0]).toMatch(/Logistics, Admin/);
expect(labels[1]).toMatch(/Code of Conduct/);
expect(labels[2]).toMatch(/Core Principle/);
});
});
@@ -13,6 +13,12 @@ describe("CoreValuesSelectScreen", () => {
fireEvent.click(await screen.findByRole("button", { name: "Discard" }));
}
async function editMeaningInOpenDialog(next: string) {
const dialog = await screen.findByRole("dialog");
const fields = within(dialog).getAllByRole("textbox");
fireEvent.change(fields[0], { target: { value: next } });
}
it("opens core value detail modal when a preset chip is clicked", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
@@ -20,20 +26,36 @@ describe("CoreValuesSelectScreen", () => {
expect(
within(dialog).getByRole("button", { name: "Add Value" }),
).toBeInTheDocument();
expect(
screen.queryByRole("menuitem", { name: "Customize" }),
).not.toBeInTheDocument();
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
expect(
screen.queryByRole("menuitem", { name: "Customize" }),
screen.getByRole("menuitem", { name: "Customize" }),
).toBeInTheDocument();
expect(
screen.getByRole("menuitem", { name: "Duplicate" }),
).toBeInTheDocument();
expect(
screen.queryByRole("menuitem", { name: "Remove" }),
).not.toBeInTheDocument();
});
it("asks to discard when closing a pending value, then unselects on Discard", async () => {
it("closes a pending value without confirm when fields are unchanged", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
await screen.findByRole("dialog");
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
expect(
screen.queryByRole("button", { name: "Keep editing" }),
).not.toBeInTheDocument();
});
it("asks to discard when closing a pending value after an edit", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
await editMeaningInOpenDialog("Changed meaning");
fireEvent.keyDown(document, { key: "Escape" });
expect(
await screen.findByRole("button", { name: "Keep editing" }),
).toBeInTheDocument();
@@ -46,7 +68,7 @@ describe("CoreValuesSelectScreen", () => {
it("keeps the pending value modal open when Keep editing is chosen", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
await screen.findByRole("dialog");
await editMeaningInOpenDialog("Changed meaning");
fireEvent.keyDown(document, { key: "Escape" });
fireEvent.click(await screen.findByRole("button", { name: "Keep editing" }));
const dialog = await screen.findByRole("dialog");
@@ -78,6 +100,162 @@ describe("CoreValuesSelectScreen", () => {
}
});
it("shows Remove only after the value has been added", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
const pending = await screen.findByRole("dialog");
fireEvent.click(within(pending).getByRole("button", { name: "Add Value" }));
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
fireEvent.click(screen.getByText("Accessibility"));
const editing = await screen.findByRole("dialog");
fireEvent.click(
within(editing).getByRole("button", { name: "More options" }),
);
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
expect(
screen.getByRole("menuitem", { name: "Customize" }),
).toBeInTheDocument();
expect(
screen.getByRole("menuitem", { name: "Duplicate" }),
).toBeInTheDocument();
});
it("Customize walks name and description before policy details", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
const dialog = await screen.findByRole("dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: "More options" }),
);
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
expect(await screen.findByPlaceholderText("Policy name")).toHaveValue(
"Accessibility",
);
expect(screen.queryByText("Custom policy details")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Next" }));
expect(
await screen.findByText("Custom policy details"),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: "What does this value mean to your group?",
}),
).toBeInTheDocument();
});
it("Customize shows meaning saved from the value modal", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
const pending = await screen.findByRole("dialog");
const fields = within(pending).getAllByRole("textbox");
fireEvent.change(fields[0], {
target: { value: "Edited meaning from value modal" },
});
fireEvent.click(within(pending).getByRole("button", { name: "Add Value" }));
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
fireEvent.click(screen.getByText("Accessibility"));
const editing = await screen.findByRole("dialog");
fireEvent.click(
within(editing).getByRole("button", { name: "More options" }),
);
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
expect(await screen.findByPlaceholderText("Policy name")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(
await screen.findByRole("button", {
name: "What does this value mean to your group?",
}),
);
expect(
screen.getByDisplayValue("Edited meaning from value modal"),
).toBeInTheDocument();
});
it("closing Customize without Finalize returns to the value modal", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
const dialog = await screen.findByRole("dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: "More options" }),
);
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
expect(
await screen.findByPlaceholderText("Policy name"),
).toBeInTheDocument();
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => {
expect(
screen.queryByPlaceholderText("Policy name"),
).not.toBeInTheDocument();
});
const valueDialog = screen.getByRole("dialog");
expect(
within(valueDialog).getByRole("button", { name: "Add Value" }),
).toBeInTheDocument();
});
it("Customize Finalize shows reordered fields in the value modal", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
const dialog = await screen.findByRole("dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: "More options" }),
);
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
fireEvent.click(await screen.findByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Next" }));
expect(
await screen.findByText("Custom policy details"),
).toBeInTheDocument();
const handles = screen.getAllByRole("button", {
name: "Drag to reorder this field",
});
const rows = screen.getAllByRole("listitem");
const store: Record<string, string> = {};
const dataTransfer = {
effectAllowed: "all",
dropEffect: "move",
setData(type: string, value: string) {
store[type] = value;
},
getData(type: string) {
return store[type] ?? "";
},
};
fireEvent.pointerDown(handles[0]);
fireEvent.dragStart(rows[0], { dataTransfer });
fireEvent.drop(rows[1], { dataTransfer });
fireEvent.click(screen.getByRole("button", { name: "Finalize" }));
await waitFor(() => {
expect(
screen.queryByPlaceholderText("Policy name"),
).not.toBeInTheDocument();
});
const result = screen.getByRole("dialog");
const labels = within(result)
.getAllByRole("textbox")
.map((el) => {
const labelledby = el.getAttribute("aria-labelledby");
return labelledby
? (document.getElementById(labelledby)?.textContent ?? "").trim()
: "";
});
expect(labels[0]).toMatch(/Signals of Violation/);
expect(labels[1]).toMatch(/What does this value mean to your group/);
});
// The "Add value" → custom-chip → modal flow uses a `customPending`
// session: dismissing the modal must drop the brand-new chip entirely
// (not just unselect it), because the user never confirmed it via
@@ -118,7 +296,6 @@ describe("CoreValuesSelectScreen", () => {
expect(countCustomChips(CUSTOM_LABEL)).toBe(1);
fireEvent.keyDown(document, { key: "Escape" });
fireEvent.click(await screen.findByRole("button", { name: "Discard" }));
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
@@ -0,0 +1,113 @@
import { describe, it, expect, vi } from "vitest";
import "@testing-library/jest-dom/vitest";
import {
fireEvent,
renderWithProviders as render,
screen,
} from "../utils/test-utils";
import { CustomMethodCardWizardBlocksList } from "../../app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardBlocksList.container";
import type { CustomMethodCardFieldBlock } from "../../lib/create/customMethodCardFieldBlocks";
function createDataTransfer() {
const store: Record<string, string> = {};
return {
effectAllowed: "all",
dropEffect: "move",
setData(type: string, value: string) {
store[type] = value;
},
getData(type: string) {
return store[type] ?? "";
},
};
}
const fieldTypeLabels = {
text: "Text",
badges: "Badges",
upload: "Upload",
proportion: "Proportion",
};
const blocks: CustomMethodCardFieldBlock[] = [
{
kind: "text",
id: "facet-meaning",
blockTitle: "Meaning",
placeholderText: "a",
},
{
kind: "text",
id: "facet-signals",
blockTitle: "Signals",
placeholderText: "b",
},
];
describe("CustomMethodCardWizardBlocksList", () => {
it("reorders when a row is dropped after dragging from the handle", () => {
const onBlocksReorder = vi.fn();
render(
<CustomMethodCardWizardBlocksList
blocks={blocks}
fieldTypeLabels={fieldTypeLabels}
dragHandleAriaLabel="Drag to reorder this field"
listLabel="Fields"
onBlocksReorder={onBlocksReorder}
onEditBlock={() => {}}
/>,
);
const handle = screen.getAllByRole("button", {
name: "Drag to reorder this field",
})[0];
const rows = screen.getAllByRole("listitem");
const dataTransfer = createDataTransfer();
fireEvent.pointerDown(handle);
fireEvent.dragStart(rows[0], { dataTransfer });
fireEvent.dragOver(rows[1], { dataTransfer });
fireEvent.drop(rows[1], { dataTransfer });
expect(onBlocksReorder).toHaveBeenCalledWith([blocks[1], blocks[0]]);
});
it("edits a block when its title is clicked", () => {
const onEditBlock = vi.fn();
render(
<CustomMethodCardWizardBlocksList
blocks={blocks}
fieldTypeLabels={fieldTypeLabels}
dragHandleAriaLabel="Drag to reorder this field"
listLabel="Fields"
onBlocksReorder={() => {}}
onEditBlock={onEditBlock}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Meaning" }));
expect(onEditBlock).toHaveBeenCalledWith(blocks[0]);
});
it("does not reorder when the drag did not start from the handle", () => {
const onBlocksReorder = vi.fn();
render(
<CustomMethodCardWizardBlocksList
blocks={blocks}
fieldTypeLabels={fieldTypeLabels}
dragHandleAriaLabel="Drag to reorder this field"
listLabel="Fields"
onBlocksReorder={onBlocksReorder}
onEditBlock={() => {}}
/>,
);
const rows = screen.getAllByRole("listitem");
const dataTransfer = createDataTransfer();
fireEvent.dragStart(rows[0], { dataTransfer });
fireEvent.drop(rows[1], { dataTransfer });
expect(onBlocksReorder).not.toHaveBeenCalled();
});
});
+184 -69
View File
@@ -191,27 +191,7 @@ describe("FinalReviewScreen — prefilled selections", () => {
});
describe("FinalReviewScreen — chip detail modal", () => {
async function enterMethodCustomizeFromDialog(dialog: HTMLElement) {
fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }),
);
const customize = await screen.findByRole("menuitem", {
name: /^customize$/i,
});
fireEvent.click(customize);
}
async function enterCoreValueCustomizeFromDialog(dialog: HTMLElement) {
fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }),
);
const customize = await screen.findByRole("menuitem", {
name: /^customize$/i,
});
fireEvent.click(customize);
}
it("opens the read-only detail modal when a chip is clicked, matching the preset copy", async () => {
it("opens the chip modal when a chip is clicked, matching the preset copy", async () => {
render(<FinalReviewWithCustomizeSelections />);
const signalChip = await screen.findByRole("button", { name: "Signal" });
@@ -274,34 +254,57 @@ describe("FinalReviewScreen — chip detail modal", () => {
);
await waitFor(() => {
expect(
screen.getByRole("menuitem", { name: /^customize$/i }),
screen.getByRole("menuitem", { name: /^duplicate$/i }),
).toBeInTheDocument();
});
expect(
screen.getByRole("menuitem", { name: /^duplicate$/i }),
screen.getByRole("menuitem", { name: /^customize$/i }),
).toBeInTheDocument();
});
it("opens method chip modal read-only until Customize, then enables Save after an edit", async () => {
it("values Customize starts on the policy name step", async () => {
function CoreValuesHarness() {
const { replaceState } = useCreateFlow();
useLayoutEffect(() => {
replaceState({
selectedCoreValueIds: ["1"],
coreValuesChipsSnapshot: [
{ id: "1", label: "Accessibility", state: "selected" },
],
});
}, [replaceState]);
return <FinalReviewScreen />;
}
render(<CoreValuesHarness />);
fireEvent.click(
await screen.findByRole("button", { name: "Accessibility" }),
);
const dialog = await screen.findByRole("dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }),
);
fireEvent.click(
await screen.findByRole("menuitem", { name: /^customize$/i }),
);
expect(
await screen.findByPlaceholderText("Policy name"),
).toHaveValue("Accessibility");
expect(screen.queryByText("Custom policy details")).not.toBeInTheDocument();
});
it("opens method chip modal editable, with Save disabled until a field changes", async () => {
render(<FinalReviewWithCustomizeSelections />);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
const dialog = await screen.findByRole("dialog");
expect(
within(dialog).queryByRole("button", { name: "Save" }),
).not.toBeInTheDocument();
const saveButton = within(dialog).getByRole("button", { name: "Save" });
expect(saveButton).toBeDisabled();
const principleField = within(dialog).getByRole("textbox", {
name: /core principle/i,
});
expect(principleField).toBeDisabled();
await enterMethodCustomizeFromDialog(dialog);
expect(
within(dialog).getByRole("button", { name: "Save" }),
).toBeDisabled();
expect(principleField).not.toBeDisabled();
fireEvent.change(principleField, { target: { value: "Edited principle" } });
await waitFor(() => {
@@ -339,8 +342,8 @@ describe("FinalReviewScreen — chip detail modal", () => {
).toBeInTheDocument();
});
it("opens the editable Save modal for a values chip after Customize", async () => {
// Customize / plain custom-rule path: snapshot is set, sections is not.
it("opens the editable Save modal for a values chip", async () => {
// Plain custom-rule path: snapshot is set, sections is not.
function CoreValuesHarness() {
const { replaceState } = useCreateFlow();
useLayoutEffect(() => {
@@ -359,10 +362,6 @@ describe("FinalReviewScreen — chip detail modal", () => {
await screen.findByRole("button", { name: "Accessibility" }),
);
const dialog = await screen.findByRole("dialog");
expect(
within(dialog).queryByRole("button", { name: "Save" }),
).not.toBeInTheDocument();
await enterCoreValueCustomizeFromDialog(dialog);
expect(
within(dialog).getByRole("button", { name: "Save" }),
).toBeInTheDocument();
@@ -371,7 +370,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
).not.toBeInTheDocument();
});
it("opens Save for values chip after Customize (use-without-changes seeded snapshot)", async () => {
it("opens Save for a values chip (use-without-changes seeded snapshot)", async () => {
// Mirrors the post-fix payload from `handleUseTemplateWithoutChanges`:
// template Values section is stripped from `sections`, snapshot +
// selected ids are seeded so the chip carries an `overrideKey`.
@@ -401,10 +400,6 @@ describe("FinalReviewScreen — chip detail modal", () => {
await screen.findByRole("button", { name: "Accessibility" }),
);
const dialog = await screen.findByRole("dialog");
expect(
within(dialog).queryByRole("button", { name: "Save" }),
).not.toBeInTheDocument();
await enterCoreValueCustomizeFromDialog(dialog);
expect(
within(dialog).getByRole("button", { name: "Save" }),
).toBeInTheDocument();
@@ -418,21 +413,10 @@ describe("FinalReviewScreen — chip detail modal", () => {
*
* 1. Save starts disabled (no edits yet → nothing to persist).
* 2. Editing any field flips Save on; clicking it writes the typed
* `{group}MethodDetailsById[id]` entry into create-flow state and
* closes the modal.
* `{group}MethodDetailsById[id]` entry into create-flow state.
* 3. Closing without Save discards every typed change.
*/
describe("FinalReviewScreen — chip edit modal save semantics", () => {
async function enterMethodCustomizeFromDialog(dialog: HTMLElement) {
fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }),
);
const customize = await screen.findByRole("menuitem", {
name: /^customize$/i,
});
fireEvent.click(customize);
}
const baseSelections: CreateFlowState = {
title: "Oak Park Commons",
selectedCommunicationMethodIds: ["signal"],
@@ -452,7 +436,6 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
const dialog = await screen.findByRole("dialog");
await enterMethodCustomizeFromDialog(dialog);
const saveButton = within(dialog).getByRole("button", { name: "Save" });
expect(saveButton).toBeDisabled();
const principleField = within(dialog).getByRole("textbox", {
@@ -483,7 +466,6 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
const dialog = await screen.findByRole("dialog");
await enterMethodCustomizeFromDialog(dialog);
const principleField = within(dialog).getByRole("textbox", {
name: /core principle/i,
});
@@ -492,13 +474,6 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
});
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(
within(screen.getByRole("dialog")).queryByRole("button", {
name: "Save",
}),
).not.toBeInTheDocument();
});
await waitFor(() => {
expect(
latest.communicationMethodDetailsById?.signal?.corePrinciple,
@@ -519,7 +494,6 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
const dialog = await screen.findByRole("dialog");
await enterMethodCustomizeFromDialog(dialog);
const principleField = within(dialog).getByRole("textbox", {
name: /core principle/i,
});
@@ -626,7 +600,6 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
await screen.findByRole("button", { name: "Custom Comm" }),
);
const dialog = await screen.findByRole("dialog");
await enterMethodCustomizeFromDialog(dialog);
expect(
within(dialog).queryByText(/no custom fields yet/i),
).not.toBeInTheDocument();
@@ -670,7 +643,6 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
await screen.findByRole("button", { name: "Custom Comm" }),
);
const dialog = await screen.findByRole("dialog");
await enterMethodCustomizeFromDialog(dialog);
const notesField = within(dialog).getByRole("textbox", { name: /notes/i });
fireEvent.change(notesField, { target: { value: "Saved detail" } });
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
@@ -685,6 +657,149 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
});
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
it("Customize opens the policy wizard prefilled with the chip title", async () => {
render(
<FinalReviewWithStateProbe
onState={() => {}}
initial={baseSelections}
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
const dialog = await screen.findByRole("dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }),
);
fireEvent.click(
await screen.findByRole("menuitem", { name: /^customize$/i }),
);
expect(
await screen.findByPlaceholderText("Policy name"),
).toHaveValue("Signal");
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => {
expect(screen.queryByPlaceholderText("Policy name")).not.toBeInTheDocument();
});
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(
within(screen.getByRole("dialog")).getByText("Signal"),
).toBeInTheDocument();
});
it("Customize wizard Finalize persists a renamed chip title", async () => {
let latest: CreateFlowState = {};
render(
<FinalReviewWithStateProbe
onState={(s) => {
latest = s;
}}
initial={baseSelections}
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
const dialog = await screen.findByRole("dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }),
);
fireEvent.click(
await screen.findByRole("menuitem", { name: /^customize$/i }),
);
expect(
await screen.findByPlaceholderText("Policy name"),
).toBeInTheDocument();
const nameInput = screen.getByPlaceholderText("Policy name");
fireEvent.change(nameInput, { target: { value: "Custom Signal header" } });
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Finalize" }));
await waitFor(() => {
expect(latest.customMethodCardMetaById?.signal?.label).toBe(
"Custom Signal header",
);
});
});
it("Customize Finalize shows reordered field blocks in the chip modal", async () => {
let latest: CreateFlowState = {};
render(
<FinalReviewWithStateProbe
onState={(s) => {
latest = s;
}}
initial={baseSelections}
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
const dialog = await screen.findByRole("dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }),
);
fireEvent.click(
await screen.findByRole("menuitem", { name: /^customize$/i }),
);
fireEvent.click(await screen.findByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Next" }));
expect(
await screen.findByText("Custom policy details"),
).toBeInTheDocument();
const handles = screen.getAllByRole("button", {
name: "Drag to reorder this field",
});
const rows = screen.getAllByRole("listitem");
const store: Record<string, string> = {};
const dataTransfer = {
effectAllowed: "all",
dropEffect: "move",
setData(type: string, value: string) {
store[type] = value;
},
getData(type: string) {
return store[type] ?? "";
},
};
fireEvent.pointerDown(handles[0]);
fireEvent.dragStart(rows[0], { dataTransfer });
fireEvent.drop(rows[2], { dataTransfer });
fireEvent.click(screen.getByRole("button", { name: "Finalize" }));
await waitFor(() => {
expect(
screen.queryByPlaceholderText("Policy name"),
).not.toBeInTheDocument();
});
await waitFor(() => {
expect(
latest.customMethodCardFieldBlocksById?.signal?.map((b) => b.id),
).toEqual([
"facet-logisticsAdmin",
"facet-codeOfConduct",
"facet-corePrinciple",
]);
});
const result = screen.getByRole("dialog");
const labels = within(result)
.getAllByRole("textbox")
.map((el) => {
const labelledby = el.getAttribute("aria-labelledby");
return labelledby
? (document.getElementById(labelledby)?.textContent ?? "").trim()
: "";
});
expect(labels[0]).toMatch(/Logistics, Admin/);
expect(labels[1]).toMatch(/Code of Conduct/);
expect(labels[2]).toMatch(/Core Principle/);
});
});
function FinalReviewEditPublishedWithStateProbe({