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 } from "../utils/test-utils"; import { CoreValuesSelectScreen } from "../../app/(app)/create/screens/select/CoreValuesSelectScreen"; describe("CoreValuesSelectScreen", () => { beforeEach(() => { vi.clearAllMocks(); }); async function discardPendingValue() { fireEvent.click(await screen.findByRole("button", { name: "Discard" })); } async function editMeaningInOpenDialog(next: string) { const dialog = await screen.findByRole("dialog"); const fields = within(dialog).getAllByRole("textbox"); fireEvent.change(fields[0], { target: { value: next } }); } it("opens core value detail modal when a preset chip is clicked", async () => { renderWithProviders(); fireEvent.click(screen.getByText("Accessibility")); const dialog = await screen.findByRole("dialog"); expect( within(dialog).getByRole("button", { name: "Add Value" }), ).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("closes a pending value without confirm when fields are unchanged", async () => { renderWithProviders(); fireEvent.click(screen.getByText("Accessibility")); await screen.findByRole("dialog"); fireEvent.keyDown(document, { key: "Escape" }); await waitFor(() => { expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); expect( screen.queryByRole("button", { name: "Keep editing" }), ).not.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" }); expect( await screen.findByRole("button", { name: "Keep editing" }), ).toBeInTheDocument(); await discardPendingValue(); await waitFor(() => { expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); }); 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.click(await screen.findByRole("button", { name: "Keep editing" })); const dialog = await screen.findByRole("dialog"); expect( within(dialog).getByRole("button", { name: "Add Value" }), ).toBeInTheDocument(); }); it("saves details when Add Value is clicked", async () => { renderWithProviders(); fireEvent.click(screen.getByText("Accessibility")); const dialog = await screen.findByRole("dialog"); fireEvent.click( within(dialog).getByRole("button", { name: "Add Value" }), ); await waitFor(() => { expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); }); it("opens meaning and signals fields editable without Customize", async () => { renderWithProviders(); fireEvent.click(screen.getByText("Accessibility")); const dialog = await screen.findByRole("dialog"); const fields = within(dialog).getAllByRole("textbox"); expect(fields.length).toBeGreaterThanOrEqual(2); for (const field of fields) { expect(field).toBeEnabled(); } }); it("shows Remove only after the value has been added", async () => { renderWithProviders(); fireEvent.click(screen.getByText("Accessibility")); const pending = await screen.findByRole("dialog"); fireEvent.click(within(pending).getByRole("button", { name: "Add Value" })); await waitFor(() => { expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); fireEvent.click(screen.getByText("Accessibility")); const editing = await screen.findByRole("dialog"); fireEvent.click( within(editing).getByRole("button", { name: "More options" }), ); expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument(); expect( screen.getByRole("menuitem", { name: "Customize" }), ).toBeInTheDocument(); expect( screen.getByRole("menuitem", { name: "Duplicate" }), ).toBeInTheDocument(); }); it("Customize walks name and description before policy details", async () => { renderWithProviders(); fireEvent.click(screen.getByText("Accessibility")); 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")).toHaveValue( "Accessibility", ); expect(screen.queryByText("Custom policy details")).not.toBeInTheDocument(); 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: "What does this value mean to your group?", }), ).toBeInTheDocument(); }); it("Customize shows meaning saved from the value modal", async () => { renderWithProviders(); fireEvent.click(screen.getByText("Accessibility")); const pending = await screen.findByRole("dialog"); const fields = within(pending).getAllByRole("textbox"); fireEvent.change(fields[0], { target: { value: "Edited meaning from value modal" }, }); fireEvent.click(within(pending).getByRole("button", { name: "Add Value" })); await waitFor(() => { expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); 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" })); fireEvent.click(screen.getByRole("button", { name: "Next" })); fireEvent.click( await screen.findByRole("button", { name: "What does this value mean to your group?", }), ); expect( screen.getByDisplayValue("Edited meaning from value modal"), ).toBeInTheDocument(); }); it("closing Customize without Finalize returns to the value modal", async () => { renderWithProviders(); fireEvent.click(screen.getByText("Accessibility")); 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(); }); const valueDialog = screen.getByRole("dialog"); expect( within(valueDialog).getByRole("button", { name: "Add Value" }), ).toBeInTheDocument(); }); it("Customize Finalize shows reordered fields in the value modal", async () => { renderWithProviders(); fireEvent.click(screen.getByText("Accessibility")); const dialog = await screen.findByRole("dialog"); 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" })); 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/); expect(labels[1]).toMatch(/What does this value mean to your group/); }); // The "Add value" → custom-chip → modal flow uses a `customPending` // session: dismissing the modal must drop the brand-new chip entirely // (not just unselect it), because the user never confirmed it via // the modal's Add Value button. Clicking Add Value keeps the chip // as a selected entry. These two tests pin both halves of the // contract so the screens stay in sync with the create-flow draft. describe("custom chip — confirmed vs dismissed", () => { // Use a label guaranteed to NOT collide with any preset value // (we'd otherwise get two matching chips and false positives). const CUSTOM_LABEL = "ZZTopBespokeValue"; async function addCustomChipNamed(label: string) { fireEvent.click(screen.getByRole("button", { name: "Add value" })); const input = await screen.findByPlaceholderText("Type to add"); fireEvent.change(input, { target: { value: label } }); fireEvent.click(screen.getByRole("button", { name: "Confirm" })); return screen.findByRole("dialog"); } /** * The label can also appear in the modal header while the modal * is open, and as the chip's "Remove" button aria-label. Scope to * chip-row buttons by excluding both. */ function countCustomChips(label: string) { return screen .queryAllByRole("button", { name: label }) .filter( (el) => !el.closest('[role="dialog"]') && (el.getAttribute("aria-label") ?? "") !== `Remove ${label}`, ).length; } it("removes the custom chip when its modal is dismissed without Add Value", async () => { renderWithProviders(); await addCustomChipNamed(CUSTOM_LABEL); expect(countCustomChips(CUSTOM_LABEL)).toBe(1); fireEvent.keyDown(document, { key: "Escape" }); await waitFor(() => { expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); // Chip must be gone — not just unselected. If it were merely // unselected the chip button would still render. Wrap in waitFor // because the chip removal flushes through `updateState` → // `useEffect` → `setCoreValueOptions` and isn't synchronous with // the dialog close. await waitFor(() => { expect(countCustomChips(CUSTOM_LABEL)).toBe(0); }); }); it("keeps the custom chip selected when Add Value is clicked", async () => { renderWithProviders(); const dialog = await addCustomChipNamed(CUSTOM_LABEL); fireEvent.click( within(dialog).getByRole("button", { name: "Add Value" }), ); await waitFor(() => { expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); expect(countCustomChips(CUSTOM_LABEL)).toBe(1); }); }); });