Files
community-rule/tests/components/CommunicationMethodsScreenPersistence.test.tsx
T
adilalloandCursor 20ea4a2f30 Give MultiSelect chips accessible names and start creation fields empty.
Catalog chips now announce Select or Deselect so they are named for assistive tech. Method and value modals open blank; Generate draft inserts the catalog copy that used to be prefilled, and Customize still keeps the section headings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 15:59:00 -06:00

651 lines
21 KiB
TypeScript

import { useEffect, useLayoutEffect } from "react";
import { describe, it, expect, afterEach } from "vitest";
import {
renderWithProviders as render,
screen,
cleanup,
within,
waitFor,
dispatchBeforeUnload,
} from "../utils/test-utils";
import { fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { CommunicationMethodsScreen } from "../../app/(app)/create/screens/card/CommunicationMethodsScreen";
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
import type { CreateFlowState } from "../../app/(app)/create/types";
import { communicationPresetFor } from "../../lib/create/finalReviewChipPresets";
import messages from "../../messages/en/index";
afterEach(() => {
cleanup();
});
async function confirmDiscardCustomizeEdits() {
fireEvent.click(
await screen.findByRole("button", { name: "Discard" }),
);
}
async function declineDiscardCustomizeEdits() {
fireEvent.click(
await screen.findByRole("button", { name: "Keep editing" }),
);
}
/**
* Mounts the screen with optional starting state and exposes the latest
* `state` to the test harness so we can assert the persistence side of
* the Add Platform flow without driving the wizard's Next chain.
*/
const EMPTY_STATE: CreateFlowState = {};
function ScreenWithStateProbe({
onState,
initial = EMPTY_STATE,
}: {
onState: (_state: CreateFlowState) => void;
initial?: CreateFlowState;
}) {
const { state, replaceState } = useCreateFlow();
useLayoutEffect(() => {
replaceState(initial);
}, [replaceState, initial]);
useEffect(() => {
onState(state);
}, [state, onState]);
return <CommunicationMethodsScreen />;
}
/**
* Confirms the persistence half of the Add-Platform flow that lets the
* final-review chip edit modal start from a known seed instead of always
* snapping back to preset copy. See {@link CommunicationMethodEditFields}
* and `buildPublishPayload` for the read side.
*/
describe("CommunicationMethodsScreen — Add Platform persistence", () => {
it("starts empty and Generate draft inserts the catalog preset", 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");
expect(within(dialog).queryByAltText("Help")).not.toBeInTheDocument();
const textboxes = within(dialog).getAllByRole("textbox");
expect(textboxes.length).toBe(3);
const corePrincipleField = textboxes[0] as HTMLTextAreaElement;
expect(corePrincipleField.value).toBe("");
const generateButtons = within(dialog).getAllByRole("button", {
name: "Generate draft",
});
expect(generateButtons.length).toBe(3);
fireEvent.click(generateButtons[0]);
expect(corePrincipleField.value).toBe(
communicationPresetFor("signal").corePrinciple,
);
fireEvent.change(corePrincipleField, { target: { value: "Custom principle" } });
fireEvent.click(
within(dialog).getByRole("button", {
name: "Add Platform",
}),
);
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
await waitFor(() => {
expect(latest.selectedCommunicationMethodIds).toContain("signal");
});
expect(
latest.communicationMethodDetailsById?.signal?.corePrinciple,
).toBe("Custom principle");
});
it("does not persist edits when the modal closes without Confirm", 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");
void dialog;
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
expect(latest.selectedCommunicationMethodIds ?? []).not.toContain("signal");
expect(latest.communicationMethodDetailsById).toBeUndefined();
});
it("re-seeds the modal from a saved override when reopening the same chip", async () => {
let latest: CreateFlowState = {};
render(
<ScreenWithStateProbe
onState={(s) => {
latest = s;
}}
initial={{
selectedCommunicationMethodIds: ["signal"],
communicationMethodDetailsById: {
signal: {
corePrinciple: "Saved principle",
logisticsAdmin: "Saved logistics",
codeOfConduct: "Saved coc",
},
},
}}
/>,
);
void latest;
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.length).toBe(3);
expect(textareas[0].value).toBe("Saved principle");
expect(textareas[1].value).toBe("Saved logistics");
expect(textareas[2].value).toBe("Saved coc");
});
it("persists edits and closes when Save is clicked on an already-selected platform", async () => {
let latest: CreateFlowState = {};
render(
<ScreenWithStateProbe
onState={(s) => {
latest = s;
}}
initial={{
selectedCommunicationMethodIds: ["signal"],
communicationMethodDetailsById: {
signal: {
corePrinciple: "Saved principle",
logisticsAdmin: "Saved logistics",
codeOfConduct: "Saved coc",
},
},
}}
/>,
);
fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
);
const dialog = await screen.findByRole("dialog");
const textareas = within(dialog).getAllByRole(
"textbox",
) as HTMLTextAreaElement[];
fireEvent.change(textareas[0], { target: { value: "Edited principle" } });
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
expect(
latest.communicationMethodDetailsById?.signal?.corePrinciple,
).toBe("Edited principle");
});
it("keeps catalog section editors when a title override exists and there are no custom fields", async () => {
const details = communicationPresetFor("video-meetings");
const noFieldsHint =
messages.create.customRule.customMethodCardWizard.editModal
.noCustomFieldsYet;
render(
<ScreenWithStateProbe
onState={() => {
/* noop */
}}
initial={{
selectedCommunicationMethodIds: ["video-meetings"],
customMethodCardMetaById: {
"video-meetings": {
label: "Video Meetings",
supportText:
"Synchronous video calls for remote face-to-face interaction.",
},
},
communicationMethodDetailsById: {
"video-meetings": details,
},
}}
/>,
);
fireEvent.click(
screen.getAllByRole("button", {
name: /Video Meetings: Synchronous video/,
})[0],
);
const dialog = await screen.findByRole("dialog");
const textareas = within(dialog).getAllByRole(
"textbox",
) as HTMLTextAreaElement[];
expect(textareas).toHaveLength(3);
expect(textareas[0].value).toBe(details.corePrinciple);
expect(textareas[1].value).toBe(details.logisticsAdmin);
expect(textareas[2].value).toBe(details.codeOfConduct);
expect(within(dialog).queryByText(noFieldsHint)).not.toBeInTheDocument();
});
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
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" }));
expect(
await screen.findByPlaceholderText("Policy name"),
).toBeInTheDocument();
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => {
expect(screen.queryByPlaceholderText("Policy name")).not.toBeInTheDocument();
});
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(latest.customMethodCardMetaById).toBeUndefined();
});
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
onState={(s) => {
latest = s;
}}
initial={{
selectedCommunicationMethodIds: ["signal"],
communicationMethodDetailsById: {
signal: {
corePrinciple: "Saved principle",
logisticsAdmin: "Saved logistics",
codeOfConduct: "Saved coc",
},
},
}}
/>,
);
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" });
await confirmDiscardCustomizeEdits();
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
expect(
latest.communicationMethodDetailsById?.signal?.corePrinciple,
).toBe("Saved principle");
});
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(
<ScreenWithStateProbe
onState={(s) => {
latest = s;
}}
initial={{
selectedCommunicationMethodIds: [customId],
customMethodCardMetaById: {
[customId]: { label: "Original title", supportText: "Sub" },
},
communicationMethodDetailsById: {
[customId]: {
corePrinciple: "p",
logisticsAdmin: "l",
codeOfConduct: "c",
},
},
}}
/>,
);
fireEvent.click(
screen.getAllByRole("button", { name: /Original title/ })[0],
);
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();
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 policy" }));
await waitFor(() => {
expect(latest.customMethodCardMetaById?.[customId]?.label).toBe(
"Renamed policy",
);
});
});
it("stores preset id title override in customMethodCardMetaById on Finalize", 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");
for (const button of within(dialog).getAllByRole("button", {
name: "Generate draft",
})) {
fireEvent.click(button);
}
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
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(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Finalize policy" }));
await waitFor(() => {
expect(latest.customMethodCardMetaById?.signal?.label).toBe(
"Custom Signal header",
);
});
});
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 policy" }));
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/);
});
it("does not block tab close when the create modal is unchanged", async () => {
render(
<ScreenWithStateProbe
onState={() => {
/* noop */
}}
/>,
);
fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
);
await screen.findByRole("dialog");
expect(dispatchBeforeUnload()).toBe(false);
});
it("blocks tab close while the create modal has unsaved field edits", 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: "Unsaved principle" } });
expect(dispatchBeforeUnload()).toBe(true);
});
it("blocks tab close while the custom-policy wizard has unsaved edits", 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 nameInput = await screen.findByPlaceholderText("Policy name");
expect(dispatchBeforeUnload()).toBe(false);
fireEvent.change(nameInput, { target: { value: "Renamed in wizard" } });
expect(dispatchBeforeUnload()).toBe(true);
});
});