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:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
@@ -40,7 +40,7 @@ describe("Create flow communication-methods page", () => {
|
||||
expect(within(dialog).getByText("Add Platform")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("re-opening a selected method shows no modal primary; Remove is in the kebab", async () => {
|
||||
test("re-opening a selected method shows Save; Remove is in the kebab", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CommunicationMethodsScreen />);
|
||||
|
||||
@@ -59,6 +59,9 @@ describe("Create flow communication-methods page", () => {
|
||||
expect(
|
||||
within(dialogAgain).queryByRole("button", { name: "Add Platform" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(dialogAgain).getByRole("button", { name: "Save" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await user.click(within(dialogAgain).getByRole("button", { name: "More options" }));
|
||||
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
|
||||
@@ -106,7 +109,7 @@ describe("Create flow communication-methods page", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("unselected preset method fields are disabled until Customize", async () => {
|
||||
test("unselected preset method fields are editable without Customize", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CommunicationMethodsScreen />);
|
||||
|
||||
@@ -117,13 +120,10 @@ describe("Create flow communication-methods page", () => {
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
const textbox = within(dialog).getAllByRole("textbox")[0];
|
||||
expect(textbox).toBeDisabled();
|
||||
expect(textbox).not.toBeDisabled();
|
||||
|
||||
await user.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||
expect(
|
||||
within(screen.getByRole("dialog")).getAllByRole("textbox")[0],
|
||||
).not.toBeDisabled();
|
||||
expect(screen.getByRole("menuitem", { name: "Customize" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders without error", () => {
|
||||
@@ -196,7 +196,7 @@ describe("Create flow communication-methods page", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("opening Create modal for custom policy shows saved field blocks read-only until Customize", async () => {
|
||||
test("opening Create modal for custom policy shows saved field blocks editable", async () => {
|
||||
const user = userEvent.setup();
|
||||
const initial = {
|
||||
selectedCommunicationMethodIds: [CUSTOM_POLICY_ID],
|
||||
@@ -227,20 +227,11 @@ describe("Create flow communication-methods page", () => {
|
||||
const textboxesBefore = within(dialog).getAllByRole("textbox");
|
||||
expect(textboxesBefore).toHaveLength(1);
|
||||
const textarea = textboxesBefore[0];
|
||||
expect(textarea).toBeDisabled();
|
||||
expect(textarea).not.toBeDisabled();
|
||||
expect(textarea).toHaveValue("Enter norms here");
|
||||
|
||||
await user.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||
|
||||
const guidelinesAfter = within(screen.getByRole("dialog")).getAllByRole(
|
||||
"textbox",
|
||||
)[2];
|
||||
expect(guidelinesAfter).not.toBeDisabled();
|
||||
expect(guidelinesAfter).toHaveValue("Enter norms here");
|
||||
});
|
||||
|
||||
test("opening Create modal for custom policy shows badge options as chips read-only until Customize", async () => {
|
||||
test("opening Create modal for custom policy shows badge options as interactive chips", async () => {
|
||||
const user = userEvent.setup();
|
||||
const initial = {
|
||||
selectedCommunicationMethodIds: [CUSTOM_POLICY_ID],
|
||||
@@ -268,22 +259,14 @@ describe("Create flow communication-methods page", () => {
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
expect(within(dialog).getByText("Choose channels")).toBeInTheDocument();
|
||||
const alpha = within(dialog).getByRole("button", { name: /^Alpha$/ });
|
||||
const beta = within(dialog).getByRole("button", { name: /^Beta$/ });
|
||||
expect(alpha).toBeDisabled();
|
||||
expect(beta).toBeDisabled();
|
||||
|
||||
await user.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||
|
||||
const alphaAfter = within(screen.getByRole("dialog")).getByRole("button", {
|
||||
const alpha = within(dialog).getByRole("button", {
|
||||
name: /Deselect Alpha/,
|
||||
});
|
||||
const betaAfter = within(screen.getByRole("dialog")).getByRole("button", {
|
||||
const beta = within(dialog).getByRole("button", {
|
||||
name: /Deselect Beta/,
|
||||
});
|
||||
expect(alphaAfter).not.toBeDisabled();
|
||||
expect(betaAfter).not.toBeDisabled();
|
||||
expect(alpha).not.toBeDisabled();
|
||||
expect(beta).not.toBeDisabled();
|
||||
});
|
||||
|
||||
test("editing custom policy field blocks updates draft state after Save", async () => {
|
||||
@@ -322,10 +305,8 @@ describe("Create flow communication-methods page", () => {
|
||||
});
|
||||
await user.click(policyTiles[0]);
|
||||
const dialog = screen.getByRole("dialog");
|
||||
await user.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||
|
||||
const textarea = within(dialog).getAllByRole("textbox")[2];
|
||||
const textarea = within(dialog).getByRole("textbox");
|
||||
await user.clear(textarea);
|
||||
await user.type(textarea, "Updated norms");
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
expect(screen.getByText("SELECTED")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("re-opening a selected approach shows no modal primary; Remove is in the kebab", async () => {
|
||||
test("re-opening a selected approach shows Save; Remove is in the kebab", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
@@ -204,6 +204,9 @@ describe("Create flow decision-approaches page", () => {
|
||||
expect(
|
||||
within(dialogAgain).queryByRole("button", { name: "Add Approach" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(dialogAgain).getByRole("button", { name: "Save" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await user.click(within(dialogAgain).getByRole("button", { name: "More options" }));
|
||||
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
|
||||
|
||||
@@ -214,4 +214,39 @@ describe("applyFinalReviewChipEditPatch", () => {
|
||||
"1": { meaning: "m2", signals: "s2" },
|
||||
});
|
||||
});
|
||||
|
||||
it("merges customMethodCardFieldBlocksById for a core values patch", () => {
|
||||
const state: CreateFlowState = {
|
||||
customMethodCardFieldBlocksById: {
|
||||
other: [
|
||||
{
|
||||
kind: "text",
|
||||
id: "x",
|
||||
blockTitle: "T",
|
||||
placeholderText: "keep",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const patch: FinalReviewChipEditPatch = {
|
||||
groupKey: "coreValues",
|
||||
overrideKey: "1",
|
||||
value: { meaning: "m", signals: "s" },
|
||||
customMethodCardFieldBlocks: [
|
||||
{
|
||||
kind: "text",
|
||||
id: "facet-meaning",
|
||||
blockTitle: "Meaning",
|
||||
placeholderText: "m",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = applyFinalReviewChipEditPatch(state, patch);
|
||||
|
||||
expect(result.customMethodCardFieldBlocksById).toEqual({
|
||||
other: state.customMethodCardFieldBlocksById?.other,
|
||||
"1": patch.customMethodCardFieldBlocks,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildMethodCardWizardInitialValues,
|
||||
coreValueDetailsFromWizardFieldBlocks,
|
||||
facetDetailsToWizardFieldBlocks,
|
||||
} from "../../lib/create/methodCardWizardPrefill";
|
||||
import type { CustomMethodCardFieldBlock } from "../../lib/create/customMethodCardFieldBlocks";
|
||||
|
||||
const cardId = "signal";
|
||||
const blocks: CustomMethodCardFieldBlock[] = [
|
||||
{ kind: "text", id: "b1", blockTitle: "Notes", placeholderText: "…" },
|
||||
];
|
||||
|
||||
const commHeadings = {
|
||||
corePrinciple: "Core Principle & Scope",
|
||||
logisticsAdmin: "Logistics, Admin & Norms",
|
||||
codeOfConduct: "Code of Conduct",
|
||||
};
|
||||
|
||||
describe("buildMethodCardWizardInitialValues", () => {
|
||||
it("uses fallback title and description when meta is missing", () => {
|
||||
expect(
|
||||
buildMethodCardWizardInitialValues({
|
||||
cardId,
|
||||
fallbackTitle: "Signal",
|
||||
fallbackDescription: "Encrypted messaging.",
|
||||
meta: {},
|
||||
persistedBlocks: {},
|
||||
draftFieldBlocks: null,
|
||||
}),
|
||||
).toEqual({
|
||||
title: "Signal",
|
||||
description: "Encrypted messaging.",
|
||||
fieldBlocks: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("maps facet body fields onto wizard blocks when none exist yet", () => {
|
||||
expect(
|
||||
buildMethodCardWizardInitialValues({
|
||||
cardId,
|
||||
fallbackTitle: "Signal",
|
||||
fallbackDescription: "Encrypted messaging.",
|
||||
meta: {},
|
||||
persistedBlocks: {},
|
||||
draftFieldBlocks: null,
|
||||
facetPrefill: {
|
||||
group: "communication",
|
||||
draft: {
|
||||
corePrinciple: "Privacy first.",
|
||||
logisticsAdmin: "Admins steward access.",
|
||||
codeOfConduct: "No leaks.",
|
||||
},
|
||||
headings: commHeadings,
|
||||
},
|
||||
}).fieldBlocks,
|
||||
).toEqual([
|
||||
{
|
||||
kind: "text",
|
||||
id: "facet-corePrinciple",
|
||||
blockTitle: "Core Principle & Scope",
|
||||
placeholderText: "Privacy first.",
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
id: "facet-logisticsAdmin",
|
||||
blockTitle: "Logistics, Admin & Norms",
|
||||
placeholderText: "Admins steward access.",
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
id: "facet-codeOfConduct",
|
||||
blockTitle: "Code of Conduct",
|
||||
placeholderText: "No leaks.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("prefers persisted meta title and description over fallbacks", () => {
|
||||
expect(
|
||||
buildMethodCardWizardInitialValues({
|
||||
cardId,
|
||||
fallbackTitle: "Signal",
|
||||
fallbackDescription: "Encrypted messaging.",
|
||||
meta: {
|
||||
[cardId]: { label: "Our Signal", supportText: "Private ops." },
|
||||
},
|
||||
persistedBlocks: { [cardId]: blocks },
|
||||
draftFieldBlocks: null,
|
||||
}),
|
||||
).toEqual({
|
||||
title: "Our Signal",
|
||||
description: "Private ops.",
|
||||
fieldBlocks: blocks,
|
||||
});
|
||||
});
|
||||
|
||||
it("overlays current facet field values onto matching persisted blocks", () => {
|
||||
const persisted: CustomMethodCardFieldBlock[] = [
|
||||
{
|
||||
kind: "text",
|
||||
id: "facet-corePrinciple",
|
||||
blockTitle: "Core Principle & Scope",
|
||||
placeholderText: "Stale principle",
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
id: "extra-notes",
|
||||
blockTitle: "Notes",
|
||||
placeholderText: "keep",
|
||||
},
|
||||
];
|
||||
expect(
|
||||
buildMethodCardWizardInitialValues({
|
||||
cardId,
|
||||
fallbackTitle: "Signal",
|
||||
fallbackDescription: "Encrypted messaging.",
|
||||
meta: {},
|
||||
persistedBlocks: { [cardId]: persisted },
|
||||
draftFieldBlocks: null,
|
||||
facetPrefill: {
|
||||
group: "communication",
|
||||
draft: {
|
||||
corePrinciple: "Updated principle",
|
||||
logisticsAdmin: "Updated logistics",
|
||||
codeOfConduct: "Updated coc",
|
||||
},
|
||||
headings: commHeadings,
|
||||
},
|
||||
}).fieldBlocks,
|
||||
).toEqual([
|
||||
{
|
||||
kind: "text",
|
||||
id: "facet-corePrinciple",
|
||||
blockTitle: "Core Principle & Scope",
|
||||
placeholderText: "Updated principle",
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
id: "extra-notes",
|
||||
blockTitle: "Notes",
|
||||
placeholderText: "keep",
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
id: "facet-logisticsAdmin",
|
||||
blockTitle: "Logistics, Admin & Norms",
|
||||
placeholderText: "Updated logistics",
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
id: "facet-codeOfConduct",
|
||||
blockTitle: "Code of Conduct",
|
||||
placeholderText: "Updated coc",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("prefers in-modal field-block drafts over persisted blocks", () => {
|
||||
const draft: CustomMethodCardFieldBlock[] = [
|
||||
{ kind: "proportion", id: "p1", blockTitle: "Share", defaultPercent: 40 },
|
||||
];
|
||||
expect(
|
||||
buildMethodCardWizardInitialValues({
|
||||
cardId,
|
||||
fallbackTitle: "Signal",
|
||||
fallbackDescription: "Encrypted messaging.",
|
||||
meta: {
|
||||
[cardId]: { label: "Our Signal", supportText: "Private ops." },
|
||||
},
|
||||
persistedBlocks: { [cardId]: blocks },
|
||||
draftFieldBlocks: draft,
|
||||
}).fieldBlocks,
|
||||
).toEqual(draft);
|
||||
});
|
||||
});
|
||||
|
||||
describe("facetDetailsToWizardFieldBlocks", () => {
|
||||
it("returns no blocks when facet copy is empty", () => {
|
||||
expect(
|
||||
facetDetailsToWizardFieldBlocks({
|
||||
group: "communication",
|
||||
draft: {
|
||||
corePrinciple: " ",
|
||||
logisticsAdmin: "",
|
||||
codeOfConduct: "",
|
||||
},
|
||||
headings: commHeadings,
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("maps decision scope chips to a badge block", () => {
|
||||
const blocksOut = facetDetailsToWizardFieldBlocks({
|
||||
group: "decisionApproaches",
|
||||
draft: {
|
||||
corePrinciple: "Momentum.",
|
||||
applicableScope: ["Daily Operations"],
|
||||
selectedApplicableScope: [],
|
||||
stepByStepInstructions: "Post a deadline.",
|
||||
consensusLevel: 100,
|
||||
objectionsDeadlocks: "Any member can block.",
|
||||
},
|
||||
headings: {
|
||||
corePrinciple: "Core Principle",
|
||||
applicableScope: "Applicable Scope",
|
||||
stepByStepInstructions: "Step-by-Step Instructions",
|
||||
consensusLevel: "Consensus Level",
|
||||
objectionsDeadlocks: "Objections & Deadlocks",
|
||||
},
|
||||
});
|
||||
expect(blocksOut.find((b) => b.kind === "badges")).toMatchObject({
|
||||
kind: "badges",
|
||||
blockTitle: "Applicable Scope",
|
||||
options: ["Daily Operations"],
|
||||
});
|
||||
expect(blocksOut.find((b) => b.kind === "proportion")).toMatchObject({
|
||||
defaultPercent: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it("maps core value meaning and signals onto text blocks", () => {
|
||||
expect(
|
||||
facetDetailsToWizardFieldBlocks({
|
||||
group: "coreValues",
|
||||
draft: {
|
||||
meaning: "Everyone can join.",
|
||||
signals: "Inaccessible venues.",
|
||||
},
|
||||
headings: {
|
||||
meaning: "What does this value mean to your group?",
|
||||
signals: "Signals of Violation",
|
||||
},
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
kind: "text",
|
||||
id: "facet-meaning",
|
||||
blockTitle: "What does this value mean to your group?",
|
||||
placeholderText: "Everyone can join.",
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
id: "facet-signals",
|
||||
blockTitle: "Signals of Violation",
|
||||
placeholderText: "Inaccessible venues.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("coreValueDetailsFromWizardFieldBlocks", () => {
|
||||
it("reads meaning and signals from facet block ids", () => {
|
||||
expect(
|
||||
coreValueDetailsFromWizardFieldBlocks(
|
||||
[
|
||||
{
|
||||
kind: "text",
|
||||
id: "facet-meaning",
|
||||
blockTitle: "Meaning",
|
||||
placeholderText: "Updated meaning",
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
id: "facet-signals",
|
||||
blockTitle: "Signals",
|
||||
placeholderText: "Updated signals",
|
||||
},
|
||||
],
|
||||
{ meaning: "old m", signals: "old s", supportText: "keep" },
|
||||
),
|
||||
).toEqual({
|
||||
meaning: "Updated meaning",
|
||||
signals: "Updated signals",
|
||||
supportText: "keep",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -113,4 +113,16 @@ describe("usesWizardFieldBlocksModalBody", () => {
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("is true for catalog ids when the wizard draft has blocks", () => {
|
||||
expect(
|
||||
usesWizardFieldBlocksModalBody({
|
||||
methodId: "signal",
|
||||
meta: {},
|
||||
fieldBlocksById: {},
|
||||
modalEditUnlocked: false,
|
||||
draftFieldBlocks: blocks,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user