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>
This commit is contained in:
adilalloandCursor committed 2026-09-29 15:59:00 -06:00
1 parent fd9f82ea19
commit 20ea4a2f30
35 files changed
+498 -156

No files matched your search

+7
View File
@@ -34,6 +34,13 @@ const config: ComponentTestSuiteConfig<Props> = {
describe("Chip", () => {
componentTestSuite<Props>(config);
it("uses the visible label as the accessible name when ariaLabel is omitted", () => {
render(<Chip label="Worker cooperative" state="unselected" />);
expect(
screen.getByRole("button", { name: "Worker cooperative" }),
).toHaveAttribute("aria-label", "Worker cooperative");
});
it("exposes aria-pressed false when unselected", () => {
render(<Chip label="Worker cooperative" state="unselected" />);
const chip = screen.getByRole("button", { name: "Worker cooperative" });
@@ -63,7 +63,7 @@ function ScreenWithStateProbe({
* and `buildPublishPayload` for the read side.
*/
describe("CommunicationMethodsScreen — Add Platform persistence", () => {
it("seeds the modal from preset and persists edits + selection on Confirm", async () => {
it("starts empty and Generate draft inserts the catalog preset", async () => {
let latest: CreateFlowState = {};
render(
<ScreenWithStateProbe
@@ -82,9 +82,16 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
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);
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(
@@ -466,6 +473,11 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
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" }));
@@ -16,4 +16,18 @@ describe("CommunityStructureSelectScreen", () => {
).toBeInTheDocument();
expect(screen.queryByAltText("Help")).not.toBeInTheDocument();
});
it("gives structure chips Select/Deselect accessible names", () => {
renderWithProviders(<CommunityStructureSelectScreen />);
expect(
screen.getByRole("button", { name: "Select Worker cooperative" }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Select Local" }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Select Early stage" }),
).toBeInTheDocument();
});
});
@@ -41,6 +41,26 @@ describe("CoreValuesSelectScreen", () => {
).not.toBeInTheDocument();
});
it("gives value chips Select/Deselect accessible names", () => {
renderWithProviders(<CoreValuesSelectScreen />);
expect(
screen.getByRole("button", { name: "Select Accessibility" }),
).toBeInTheDocument();
});
it("starts the value modal empty and Generate draft inserts catalog copy", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByRole("button", { name: "Select Accessibility" }));
const dialog = await screen.findByRole("dialog");
const fields = within(dialog).getAllByRole("textbox") as HTMLTextAreaElement[];
expect(fields[0].value).toBe("");
expect(fields[1].value).toBe("");
fireEvent.click(
within(dialog).getAllByRole("button", { name: "Generate draft" })[0],
);
expect(fields[0].value).toMatch(/everyone can use/i);
});
it("closes a pending value without confirm when fields are unchanged", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
@@ -289,12 +309,10 @@ describe("CoreValuesSelectScreen", () => {
*/
function countCustomChips(label: string) {
return screen
.queryAllByRole("button", { name: label })
.filter(
(el) =>
!el.closest('[role="dialog"]') &&
(el.getAttribute("aria-label") ?? "") !== `Remove ${label}`,
).length;
.queryAllByRole("button", {
name: new RegExp(`^(Select|Deselect) ${label}$`),
})
.filter((el) => !el.closest('[role="dialog"]')).length;
}
async function completeAddValueWizard(label: string) {
@@ -417,7 +435,7 @@ describe("CoreValuesSelectScreen", () => {
),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Anti-oppression" }),
screen.getByRole("button", { name: "Select Anti-oppression" }),
).toBeDisabled();
expect(screen.getByRole("button", { name: "Add value" })).toBeDisabled();
expect(screen.getByRole("button", { name: /^add$/i })).toBeDisabled();
+29 -25
View File
@@ -12,6 +12,10 @@ import { FinalReviewScreen } from "../../app/(app)/create/screens/review/FinalRe
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
import type { CreateFlowState } from "../../app/(app)/create/types";
function catalogChipName(label: string) {
return new RegExp(`^(Select|Deselect) ${label}$`);
}
async function confirmDiscardCustomizeEdits() {
fireEvent.click(
await screen.findByRole("button", { name: "Discard" }),
@@ -232,7 +236,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
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" });
const signalChip = await screen.findByRole("button", { name: catalogChipName("Signal") });
fireEvent.click(signalChip);
// Modal subtitle is the `supportText` from communication.json for the
@@ -273,7 +277,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
);
fireEvent.click(
await screen.findByRole("button", { name: "Video Meetings" }),
await screen.findByRole("button", { name: catalogChipName("Video Meetings") }),
);
const dialog = await screen.findByRole("dialog");
expect(within(dialog).getAllByRole("textbox")).toHaveLength(3);
@@ -287,7 +291,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
it("method chip modal kebab offers Customize but not Duplicate", async () => {
render(<FinalReviewWithCustomizeSelections />);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }),
@@ -317,7 +321,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
}
render(<CoreValuesHarness />);
fireEvent.click(
await screen.findByRole("button", { name: "Accessibility" }),
await screen.findByRole("button", { name: catalogChipName("Accessibility") }),
);
const dialog = await screen.findByRole("dialog");
fireEvent.click(
@@ -348,7 +352,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
}
render(<CoreValuesHarness />);
fireEvent.click(
await screen.findByRole("button", { name: "Accessibility" }),
await screen.findByRole("button", { name: catalogChipName("Accessibility") }),
);
const dialog = await screen.findByRole("dialog");
fireEvent.click(
@@ -366,7 +370,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
it("opens method chip modal editable, with Save disabled until a field changes", async () => {
render(<FinalReviewWithCustomizeSelections />);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog");
const saveButton = within(dialog).getByRole("button", { name: "Save" });
@@ -400,7 +404,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
}
render(<CoreValuesHarness />);
const chip = await screen.findByRole("button", { name: "Accessibility" });
const chip = await screen.findByRole("button", { name: catalogChipName("Accessibility") });
fireEvent.click(chip);
await waitFor(() => {
@@ -430,7 +434,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
render(<CoreValuesHarness />);
fireEvent.click(
await screen.findByRole("button", { name: "Accessibility" }),
await screen.findByRole("button", { name: catalogChipName("Accessibility") }),
);
const dialog = await screen.findByRole("dialog");
expect(
@@ -468,7 +472,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
render(<UseWithoutChangesHarness />);
fireEvent.click(
await screen.findByRole("button", { name: "Accessibility" }),
await screen.findByRole("button", { name: catalogChipName("Accessibility") }),
);
const dialog = await screen.findByRole("dialog");
expect(
@@ -514,7 +518,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
);
fireEvent.click(
await screen.findByRole("button", { name: "Accessibility" }),
await screen.findByRole("button", { name: catalogChipName("Accessibility") }),
);
const valuesDialog = await screen.findByRole("dialog");
expect(within(valuesDialog).getAllByRole("textbox").length).toBeGreaterThan(
@@ -542,7 +546,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const methodDialog = await screen.findByRole("dialog");
expect(within(methodDialog).getAllByRole("textbox").length).toBeGreaterThan(
0,
@@ -567,7 +571,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog");
expect(
within(dialog).getByRole("button", { name: "Remove" }),
@@ -595,7 +599,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: "Remove" }));
await waitFor(() => {
@@ -633,7 +637,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
);
void latest;
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog");
const saveButton = within(dialog).getByRole("button", { name: "Save" });
expect(saveButton).toBeDisabled();
@@ -663,7 +667,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog");
const principleField = within(dialog).getByRole("textbox", {
name: /core principle/i,
@@ -691,7 +695,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog");
const principleField = within(dialog).getByRole("textbox", {
name: /core principle/i,
@@ -719,7 +723,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog");
expect(dispatchBeforeUnload()).toBe(false);
const principleField = within(dialog).getByRole("textbox", {
@@ -750,7 +754,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
);
fireEvent.click(
await screen.findByRole("button", { name: "Custom Comm" }),
await screen.findByRole("button", { name: catalogChipName("Custom Comm") }),
);
const dialog = await screen.findByRole("dialog");
expect(
@@ -784,9 +788,9 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
);
expect(
await screen.findByRole("button", { name: "Garden IRC" }),
await screen.findByRole("button", { name: catalogChipName("Garden IRC") }),
).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Signal" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: catalogChipName("Signal") })).toBeInTheDocument();
});
it("shows editable field blocks for user-authored communication chips when configured", async () => {
@@ -818,7 +822,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
);
fireEvent.click(
await screen.findByRole("button", { name: "Custom Comm" }),
await screen.findByRole("button", { name: catalogChipName("Custom Comm") }),
);
const dialog = await screen.findByRole("dialog");
expect(
@@ -861,7 +865,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
);
fireEvent.click(
await screen.findByRole("button", { name: "Custom Comm" }),
await screen.findByRole("button", { name: catalogChipName("Custom Comm") }),
);
const dialog = await screen.findByRole("dialog");
const notesField = within(dialog).getByRole("textbox", { name: /notes/i });
@@ -887,7 +891,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }),
@@ -921,7 +925,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }),
@@ -957,7 +961,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }),
@@ -110,4 +110,44 @@ describe("ModalTextAreaField behavior", () => {
expect(screen.getByAltText("Help")).toBeInTheDocument();
});
it("starts empty and Generate draft inserts the provided catalog copy", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
renderWithProviders(
<ModalTextAreaField
label="Core principle"
value=""
onChange={onChange}
generateDraftText="Catalog first draft"
generateDraftLabel="Generate draft"
generateDraftHint="Get a first draft for this section"
/>,
);
expect(screen.getByRole("textbox", { name: /Core principle/i })).toHaveValue(
"",
);
expect(
screen.getByText("Get a first draft for this section"),
).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Generate draft" }));
expect(onChange).toHaveBeenCalledWith("Catalog first draft");
});
it("hides Generate draft when there is no catalog copy", () => {
renderWithProviders(
<ModalTextAreaField
label="Core principle"
value=""
onChange={() => {}}
generateDraftText=""
generateDraftLabel="Generate draft"
/>,
);
expect(
screen.queryByRole("button", { name: "Generate draft" }),
).not.toBeInTheDocument();
});
});
+13 -3
View File
@@ -62,6 +62,16 @@ describe("MultiSelect – behaviour specifics", () => {
expect(screen.getByText("Option 2")).toBeInTheDocument();
});
it("names toggle chips Select/Deselect so they are not unnamed buttons", () => {
render(<MultiSelect options={defaultChipOptions} />);
expect(
screen.getByRole("button", { name: "Select Option 1" }),
).toHaveAttribute("aria-pressed", "false");
expect(
screen.getByRole("button", { name: "Deselect Option 2" }),
).toHaveAttribute("aria-pressed", "true");
});
it("calls onChipClick when a chip is clicked", async () => {
const handleChipClick = vi.fn();
render(
@@ -151,11 +161,11 @@ describe("MultiSelect – behaviour specifics", () => {
expect(screen.getByText("2 of 2")).toBeInTheDocument();
expect(screen.getByText("Limit reached")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "One" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Three" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Deselect One" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Select Three" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Add option" })).toBeDisabled();
await userEvent.click(screen.getByRole("button", { name: "Three" }));
await userEvent.click(screen.getByRole("button", { name: "Select Three" }));
await userEvent.click(screen.getByRole("button", { name: "Add option" }));
expect(handleChipClick).not.toHaveBeenCalled();
expect(handleAddClick).not.toHaveBeenCalled();
+10
View File
@@ -18,4 +18,14 @@ describe("CommunitySizeSelectScreen", () => {
expect(screen.getByText("1 member")).toBeInTheDocument();
expect(screen.getByText("2-5 members")).toBeInTheDocument();
});
it("gives size chips Select/Deselect accessible names", () => {
render(<CommunitySizeSelectScreen />);
expect(
screen.getByRole("button", { name: "Select 1 member" }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Select 2-5 members" }),
).toBeInTheDocument();
});
});