import { describe, it, expect, vi, beforeEach } from "vitest";
import { screen, fireEvent, waitFor, within } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { renderWithProviders, dispatchBeforeUnload } from "../utils/test-utils";
import { CoreValuesSelectScreen } from "../../app/(app)/create/screens/select/CoreValuesSelectScreen";
const MEANING_STEP = "What does this value mean to your community?";
const SIGNALS_STEP = "Signals of violation";
const ADD_TO_RULE = "Add to Rule";
const ADD_OWN = "add your own";
describe("CoreValuesSelectScreen", () => {
beforeEach(() => {
vi.clearAllMocks();
});
async function discardPendingValue() {
fireEvent.click(await screen.findByRole("button", { name: "Discard" }));
}
async function openValueDialog(label = "Accessibility") {
fireEvent.click(screen.getByRole("button", { name: `Select ${label}` }));
return screen.findByRole("dialog");
}
async function editMeaningInOpenDialog(next: string) {
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: MEANING_STEP }));
fireEvent.change(within(dialog).getByRole("textbox"), {
target: { value: next },
});
}
async function confirmPendingValue(dialog: HTMLElement) {
fireEvent.click(within(dialog).getByRole("button", { name: ADD_TO_RULE }));
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
}
it("opens a card of meaning and signals rows when a preset chip is clicked", async () => {
renderWithProviders();
const dialog = await openValueDialog();
expect(within(dialog).getByText("Accessibility")).toBeInTheDocument();
expect(
within(dialog).getByRole("button", { name: MEANING_STEP }),
).toBeInTheDocument();
expect(
within(dialog).getByRole("button", { name: SIGNALS_STEP }),
).toBeInTheDocument();
expect(
within(dialog).getByText(
"How do we know when we are not living up to this value?",
),
).toBeInTheDocument();
expect(
within(dialog).getByRole("button", { name: ADD_TO_RULE }),
).toBeInTheDocument();
expect(
within(dialog).getByRole("button", { name: "Cancel" }),
).toBeInTheDocument();
expect(
within(dialog).queryByRole("button", { name: "Deselect" }),
).not.toBeInTheDocument();
expect(
within(dialog).queryByRole("button", { name: "Remove" }),
).not.toBeInTheDocument();
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
expect(
screen.getByRole("menuitem", { name: "Customize" }),
).toBeInTheDocument();
expect(
screen.getByRole("menuitem", { name: "Duplicate" }),
).toBeInTheDocument();
expect(
screen.queryByRole("menuitem", { name: "Remove" }),
).not.toBeInTheDocument();
});
it("gives value chips Select/Deselect accessible names", () => {
renderWithProviders();
expect(
screen.getByRole("button", { name: "Select Accessibility" }),
).toBeInTheDocument();
});
it("offers add your own in the header and a of-6 counter", () => {
renderWithProviders();
expect(
screen.getByRole("heading", { name: "Choose up to six core values" }),
).toBeInTheDocument();
expect(screen.getByRole("button", { name: ADD_OWN })).toBeInTheDocument();
expect(screen.getByText("0 of 6 picked")).toBeInTheDocument();
});
it("starts a meaning step empty and Generate draft inserts catalog copy", async () => {
renderWithProviders();
const dialog = await openValueDialog();
expect(within(dialog).queryByRole("textbox")).not.toBeInTheDocument();
fireEvent.click(within(dialog).getByRole("button", { name: MEANING_STEP }));
const field = within(dialog).getByRole("textbox") as HTMLTextAreaElement;
expect(field.value).toBe("");
expect(field).toHaveAttribute("placeholder", "Write a sentence or two.");
expect(
within(dialog).getByText("See our first draft for this value"),
).toBeInTheDocument();
fireEvent.click(
within(dialog).getByRole("button", { name: "Generate draft" }),
);
expect(field.value).toMatch(/usable by everyone/i);
expect(
within(dialog).queryByRole("button", { name: "Generate draft" }),
).not.toBeInTheDocument();
expect(
within(dialog).getByText(
"These are your community's words. Change anything that does not fit.",
),
).toBeInTheDocument();
});
it("opens a signals step with Generate draft on an empty field", async () => {
renderWithProviders();
const dialog = await openValueDialog();
fireEvent.click(within(dialog).getByRole("button", { name: SIGNALS_STEP }));
expect(within(dialog).getByRole("textbox")).toHaveValue("");
expect(
within(dialog).getByRole("button", { name: "Generate draft" }),
).toBeInTheDocument();
expect(
within(dialog).getByText(
"How do we know when we are not living up to Accessibility?",
),
).toBeInTheDocument();
});
it("returns from a step to the card when Cancel is clicked", async () => {
renderWithProviders();
const dialog = await openValueDialog();
fireEvent.click(within(dialog).getByRole("button", { name: MEANING_STEP }));
expect(within(dialog).getByRole("textbox")).toBeInTheDocument();
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
expect(within(dialog).queryByRole("textbox")).not.toBeInTheDocument();
expect(
within(dialog).getByRole("button", { name: MEANING_STEP }),
).toBeInTheDocument();
expect(
within(dialog).getByRole("button", { name: ADD_TO_RULE }),
).toBeInTheDocument();
});
it("closes a pending value without confirm when fields are unchanged", async () => {
renderWithProviders();
await openValueDialog();
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
expect(
screen.queryByRole("button", { name: "Keep editing" }),
).not.toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Select Accessibility" }),
).toBeInTheDocument();
});
it("asks to discard when closing a pending value after an edit", async () => {
renderWithProviders();
fireEvent.click(screen.getByText("Accessibility"));
await editMeaningInOpenDialog("Changed meaning");
fireEvent.keyDown(document, { key: "Escape" });
fireEvent.keyDown(document, { key: "Escape" });
expect(
await screen.findByRole("button", { name: "Keep editing" }),
).toBeInTheDocument();
await discardPendingValue();
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});
it("does not block tab close when a pending value is unchanged", async () => {
renderWithProviders();
await openValueDialog();
expect(dispatchBeforeUnload()).toBe(false);
});
it("blocks tab close after editing a pending value", async () => {
renderWithProviders();
fireEvent.click(screen.getByText("Accessibility"));
await editMeaningInOpenDialog("Changed meaning");
expect(dispatchBeforeUnload()).toBe(true);
});
it("keeps the pending value modal open when Keep editing is chosen", async () => {
renderWithProviders();
fireEvent.click(screen.getByText("Accessibility"));
await editMeaningInOpenDialog("Changed meaning");
fireEvent.keyDown(document, { key: "Escape" });
fireEvent.keyDown(document, { key: "Escape" });
fireEvent.click(await screen.findByRole("button", { name: "Keep editing" }));
const dialog = await screen.findByRole("dialog");
expect(
within(dialog).getByRole("button", { name: ADD_TO_RULE }),
).toBeInTheDocument();
});
it("keeps a value with empty meaning and signals when Add to Rule is clicked", async () => {
renderWithProviders();
const dialog = await openValueDialog();
await confirmPendingValue(dialog);
expect(
screen.getByRole("button", { name: "Deselect Accessibility" }),
).toBeInTheDocument();
expect(screen.getByText("1 of 6 picked")).toBeInTheDocument();
});
it("opens meaning and signals steps editable without Customize", async () => {
renderWithProviders();
const dialog = await openValueDialog();
fireEvent.click(within(dialog).getByRole("button", { name: MEANING_STEP }));
expect(within(dialog).getByRole("textbox")).toBeEnabled();
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
fireEvent.click(within(dialog).getByRole("button", { name: SIGNALS_STEP }));
expect(within(dialog).getByRole("textbox")).toBeEnabled();
});
it("shows Deselect after the value has been added, not Remove in the kebab", async () => {
renderWithProviders();
const pending = await openValueDialog();
await confirmPendingValue(pending);
fireEvent.click(screen.getByText("Accessibility"));
const editing = await screen.findByRole("dialog");
expect(
within(editing).getByRole("button", { name: "Deselect" }),
).toBeInTheDocument();
expect(
within(editing).getByRole("button", { name: "Save" }),
).toBeInTheDocument();
expect(
within(editing).queryByRole("button", { name: "Remove" }),
).not.toBeInTheDocument();
fireEvent.click(
within(editing).getByRole("button", { name: "More options" }),
);
expect(
screen.queryByRole("menuitem", { name: "Remove" }),
).not.toBeInTheDocument();
expect(
screen.getByRole("menuitem", { name: "Customize" }),
).toBeInTheDocument();
expect(
screen.getByRole("menuitem", { name: "Duplicate" }),
).toBeInTheDocument();
});
it("Deselect takes a confirmed value out of the selection", async () => {
renderWithProviders();
const pending = await openValueDialog();
await confirmPendingValue(pending);
fireEvent.click(screen.getByText("Accessibility"));
const editing = await screen.findByRole("dialog");
fireEvent.click(within(editing).getByRole("button", { name: "Deselect" }));
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
expect(
screen.getByRole("button", { name: "Select Accessibility" }),
).toBeInTheDocument();
expect(screen.getByText("0 of 6 picked")).toBeInTheDocument();
});
it("Customize walks name and description before policy details", async () => {
renderWithProviders();
const dialog = await openValueDialog();
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" }));
expect(screen.getByPlaceholderText("Policy description")).toHaveValue("");
fireEvent.click(screen.getByRole("button", { name: "Next" }));
expect(
await screen.findByText("Custom policy details"),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: MEANING_STEP,
}),
).toBeInTheDocument();
});
it("Customize shows meaning saved from the value modal", async () => {
renderWithProviders();
fireEvent.click(screen.getByText("Accessibility"));
await editMeaningInOpenDialog("Edited meaning from value modal");
const pending = screen.getByRole("dialog");
await confirmPendingValue(pending);
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" }));
expect(screen.getByPlaceholderText("Policy description")).toHaveValue("");
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(
await screen.findByRole("button", {
name: MEANING_STEP,
}),
);
expect(
screen.getByDisplayValue("Edited meaning from value modal"),
).toBeInTheDocument();
});
it("closing Customize without Finalize returns to the value modal", async () => {
renderWithProviders();
const dialog = await openValueDialog();
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_TO_RULE }),
).toBeInTheDocument();
});
it("Customize Finalize shows reordered fields in the value modal", async () => {
renderWithProviders();
const dialog = await openValueDialog();
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 = {};
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 policy" }));
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/i);
expect(labels[1]).toMatch(/What does this value mean to your community/);
});
describe("add your own wizard", () => {
const CUSTOM_LABEL = "ZZTopBespokeValue";
function countCustomChips(label: string) {
return screen
.queryAllByRole("button", {
name: new RegExp(`^(Select|Deselect) ${label}$`),
})
.filter((el) => !el.closest('[role="dialog"]')).length;
}
async function completeAddValueWizard(label: string) {
fireEvent.click(screen.getByRole("button", { name: ADD_OWN }));
const nameInput = await screen.findByPlaceholderText("Policy name");
fireEvent.change(nameInput, { target: { value: label } });
fireEvent.click(screen.getByRole("button", { name: "Next" }));
const descriptionInput =
await screen.findByPlaceholderText("Policy description");
expect(descriptionInput).toHaveValue("");
fireEvent.change(descriptionInput, {
target: { value: "A community-authored value." },
});
fireEvent.click(screen.getByRole("button", { name: "Next" }));
expect(
await screen.findByText("Custom policy details"),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Add Custom Field" }),
).toBeInTheDocument();
fireEvent.click(await screen.findByRole("button", { name: "Finalize policy" }));
}
it("collapses the field-type picker after adding a custom field", async () => {
renderWithProviders();
fireEvent.click(screen.getByRole("button", { name: ADD_OWN }));
fireEvent.change(await screen.findByPlaceholderText("Policy name"), {
target: { value: CUSTOM_LABEL },
});
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.change(
await screen.findByPlaceholderText("Policy description"),
{ target: { value: "A community-authored value." } },
);
fireEvent.click(screen.getByRole("button", { name: "Next" }));
expect(
await screen.findByText("Custom policy details"),
).toBeInTheDocument();
const wizard = screen.getByRole("dialog");
fireEvent.click(
within(wizard).getByRole("button", { name: "Add Custom Field" }),
);
fireEvent.click(within(wizard).getByRole("button", { name: "Text" }));
fireEvent.change(
await screen.findByPlaceholderText("Add your text block title"),
{ target: { value: "Policy title" } },
);
fireEvent.click(screen.getByRole("button", { name: "Add field" }));
expect(
await within(wizard).findByRole("button", { name: "Add Custom Field" }),
).toBeInTheDocument();
expect(
within(wizard).queryByRole("button", { name: "Text" }),
).not.toBeInTheDocument();
expect(
within(wizard).getByRole("button", { name: "Policy title" }),
).toBeInTheDocument();
});
it("opens the empty custom-policy wizard from add your own", async () => {
renderWithProviders();
fireEvent.click(screen.getByRole("button", { name: ADD_OWN }));
expect(
await screen.findByPlaceholderText("Policy name"),
).toHaveValue("");
expect(screen.queryByPlaceholderText("Type to add")).not.toBeInTheDocument();
});
it("leaves no chip when the add your own wizard is dismissed", async () => {
renderWithProviders();
fireEvent.click(screen.getByRole("button", { name: ADD_OWN }));
await screen.findByPlaceholderText("Policy name");
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => {
expect(
screen.queryByPlaceholderText("Policy name"),
).not.toBeInTheDocument();
});
expect(countCustomChips(CUSTOM_LABEL)).toBe(0);
expect(screen.queryByPlaceholderText("Type to add")).not.toBeInTheDocument();
});
it("adds a selected chip named from the wizard title after Finalize", async () => {
renderWithProviders();
await completeAddValueWizard(CUSTOM_LABEL);
await waitFor(() => {
expect(
screen.queryByPlaceholderText("Policy name"),
).not.toBeInTheDocument();
});
expect(countCustomChips(CUSTOM_LABEL)).toBe(1);
});
});
describe("six-value limit", () => {
async function addPresetValue(label: string) {
fireEvent.click(screen.getByText(label));
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: ADD_TO_RULE }));
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
}
it("shows a counter, disables remaining chips, and announces the limit", async () => {
renderWithProviders();
expect(screen.getByText("0 of 6 picked")).toBeInTheDocument();
await addPresetValue("Accessibility");
await addPresetValue("Accountability");
await addPresetValue("Adaptability");
await addPresetValue("Agency");
await addPresetValue("Anti-oppression");
await addPresetValue("Autonomy");
expect(screen.getByText("6 of 6 picked")).toBeInTheDocument();
expect(
screen.getByText(
"Six of six values selected. Remaining values are unavailable until one is removed.",
),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Select Capacity building" }),
).toBeDisabled();
expect(screen.getByRole("button", { name: ADD_OWN })).toBeDisabled();
});
});
});