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(); }); }); });