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:
1 parent
fd9f82ea19
commit
20ea4a2f30
35 files changed
+498
-156
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user