import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { FieldEditor } from "../../src/components/FieldEditor"; import type { SchemaField } from "../../src/lib/api"; import { render } from "../utils/render.tsx"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const FIELD_TYPE_REGEXES = [ /Short Text/, /Long Text/, /Number Decimal number/, /Integer Whole number/, /Boolean True\/false toggle/, /Date & Time/, /^Select Single choice/, /Multi Select/, /Rich Text/, /Image/, /^File File from/, /Reference/, /JSON/, /Slug URL-friendly/, ]; const SHORT_TEXT_REGEX = /Short Text/; const LONG_TEXT_REGEX = /Long Text/; const BOOLEAN_REGEX = /Boolean/; const RICH_TEXT_REGEX = /Rich Text Rich text editor/; function makeField(overrides: Partial = {}): SchemaField { return { id: "field_01", collectionId: "col_01", slug: "title", label: "Title", type: "string", columnType: "TEXT", required: true, unique: false, searchable: true, sortOrder: 0, createdAt: new Date().toISOString(), ...overrides, }; } // The kumo Dialog renders a `data-base-ui-inert` overlay that blocks pointer // events inside the dialog in Playwright's actionability checks. Assertions // (toBeInTheDocument, toHaveValue, etc.) work fine; only click() is blocked. // // Strategy: // - Type selection step: assert type buttons exist (no clicking needed) // - Config step: use edit mode (pass field prop) to go directly to config // - onSave/callbacks: use edit mode fields to test form submission describe("FieldEditor", () => { const defaultProps = { open: true, onOpenChange: vi.fn(), onSave: vi.fn(), }; beforeEach(() => { vi.clearAllMocks(); }); describe("type selection step", () => { it("shows type selection grid when creating new field", async () => { const screen = await render(); await expect.element(screen.getByText("Add Field")).toBeInTheDocument(); await expect .element(screen.getByRole("button", { name: SHORT_TEXT_REGEX })) .toBeInTheDocument(); await expect .element(screen.getByRole("button", { name: LONG_TEXT_REGEX })) .toBeInTheDocument(); await expect.element(screen.getByRole("button", { name: BOOLEAN_REGEX })).toBeInTheDocument(); await expect .element(screen.getByRole("button", { name: RICH_TEXT_REGEX })) .toBeInTheDocument(); }); it("shows all 14 field types as buttons", async () => { const screen = await render(); // Each type renders as a button with label and description for (const name of FIELD_TYPE_REGEXES) { await expect.element(screen.getByRole("button", { name })).toBeInTheDocument(); } }); it("does not show config form on initial render", async () => { const screen = await render(); // Label and Slug inputs should NOT be present in type selection step expect(screen.getByLabelText("Label").query()).toBeNull(); expect(screen.getByLabelText("Slug").query()).toBeNull(); }); }); describe("config step (string field)", () => { // Use a minimal string field to go directly to config step const stringField = makeField({ slug: "", label: "", type: "string", required: false, unique: false, searchable: false, }); it("shows Configure Field title for string type", async () => { const screen = await render( , ); await expect.element(screen.getByText("Edit Field")).toBeInTheDocument(); }); it("shows label and slug inputs", async () => { const screen = await render(); await expect.element(screen.getByLabelText("Label")).toBeInTheDocument(); await expect.element(screen.getByLabelText("Slug")).toBeInTheDocument(); }); it("shows searchable checkbox for string type", async () => { const screen = await render(); await expect.element(screen.getByText("Searchable")).toBeInTheDocument(); }); it("shows min/max length validation for string type", async () => { const screen = await render(); await expect.element(screen.getByText("Validation")).toBeInTheDocument(); await expect.element(screen.getByLabelText("Min Length")).toBeInTheDocument(); await expect.element(screen.getByLabelText("Max Length")).toBeInTheDocument(); }); it("shows pattern input for string type", async () => { const screen = await render(); await expect.element(screen.getByLabelText("Pattern (Regex)")).toBeInTheDocument(); }); it("shows required and unique checkboxes", async () => { const screen = await render(); await expect.element(screen.getByText("Required")).toBeInTheDocument(); await expect.element(screen.getByText("Unique")).toBeInTheDocument(); }); }); describe("config step (number field)", () => { const numberField = makeField({ slug: "", label: "", type: "number", required: false, unique: false, searchable: false, }); it("shows min/max value for number type", async () => { const screen = await render(); await expect.element(screen.getByLabelText("Min Value")).toBeInTheDocument(); await expect.element(screen.getByLabelText("Max Value")).toBeInTheDocument(); }); it("does not show searchable for number type", async () => { const screen = await render(); expect(screen.getByText("Searchable").query()).toBeNull(); }); it("does not show pattern for number type", async () => { const screen = await render(); expect(screen.getByLabelText("Pattern (Regex)").query()).toBeNull(); }); it("does not show min/max length for number type", async () => { const screen = await render(); expect(screen.getByLabelText("Min Length").query()).toBeNull(); expect(screen.getByLabelText("Max Length").query()).toBeNull(); }); }); describe("config step (text field)", () => { const textField = makeField({ slug: "", label: "", type: "text", required: false, unique: false, searchable: false, }); it("shows min/max length but no pattern for text type", async () => { const screen = await render(); await expect.element(screen.getByLabelText("Min Length")).toBeInTheDocument(); await expect.element(screen.getByLabelText("Max Length")).toBeInTheDocument(); expect(screen.getByLabelText("Pattern (Regex)").query()).toBeNull(); }); it("shows searchable checkbox for text type", async () => { const screen = await render(); await expect.element(screen.getByText("Searchable")).toBeInTheDocument(); }); }); describe("config step (select field)", () => { const selectField = makeField({ slug: "", label: "", type: "select", required: false, unique: false, searchable: false, }); it("shows options textarea for select type", async () => { const screen = await render(); await expect.element(screen.getByText("Options (one per line)")).toBeInTheDocument(); // Textarea should have the placeholder await expect.element(screen.getByPlaceholder("Option 1")).toBeInTheDocument(); }); }); describe("config step (multiSelect field)", () => { const multiSelectField = makeField({ slug: "", label: "", type: "multiSelect", required: false, unique: false, searchable: false, }); it("shows options textarea for multi-select type", async () => { const screen = await render(); await expect.element(screen.getByText("Options (one per line)")).toBeInTheDocument(); await expect.element(screen.getByPlaceholder("Option 1")).toBeInTheDocument(); }); }); describe("edit mode", () => { const existingField = makeField({ validation: { maxLength: 200 }, }); it("skips type selection and shows config directly", async () => { const screen = await render(); await expect.element(screen.getByText("Edit Field")).toBeInTheDocument(); await expect.element(screen.getByLabelText("Label")).toHaveValue("Title"); }); it("disables slug input in edit mode", async () => { const screen = await render(); await expect.element(screen.getByLabelText("Slug")).toBeDisabled(); }); it("shows hint about slug immutability", async () => { const screen = await render(); await expect .element(screen.getByText("Field slugs cannot be changed after creation")) .toBeInTheDocument(); }); it("does not show Change button in edit mode", async () => { const screen = await render(); expect(screen.getByRole("button", { name: "Change" }).query()).toBeNull(); }); it("shows Update Field button instead of Add Field", async () => { const screen = await render(); await expect .element(screen.getByRole("button", { name: "Update Field" })) .toBeInTheDocument(); }); it("pre-populates validation values", async () => { const screen = await render(); await expect.element(screen.getByLabelText("Max Length")).toHaveValue(200); }); it("pre-populates slug value", async () => { const screen = await render(); await expect.element(screen.getByLabelText("Slug")).toHaveValue("title"); }); it("pre-populates required checkbox", async () => { const screen = await render(); // The Required checkbox text should be present (the field has required: true) await expect.element(screen.getByText("Required")).toBeInTheDocument(); }); it("does not auto-generate slug when editing label in edit mode", async () => { const screen = await render(); await screen.getByLabelText("Label").fill("New Label"); // Slug should remain "title", not change to "new_label" await expect.element(screen.getByLabelText("Slug")).toHaveValue("title"); }); it("shows type indicator with field type info", async () => { const screen = await render(); await expect.element(screen.getByText("Short Text")).toBeInTheDocument(); await expect.element(screen.getByText("Single line text input")).toBeInTheDocument(); }); }); describe("saving state", () => { it("shows Saving... when isSaving is true", async () => { const field = makeField(); const screen = await render(); await expect.element(screen.getByText("Saving...")).toBeInTheDocument(); }); it("disables cancel button when saving", async () => { const field = makeField(); const screen = await render(); await expect.element(screen.getByRole("button", { name: "Cancel" })).toBeDisabled(); }); it("disables update button when saving", async () => { const field = makeField(); const screen = await render(); await expect.element(screen.getByRole("button", { name: "Saving..." })).toBeDisabled(); }); }); describe("button state", () => { it("disables save button when label is empty", async () => { const field = makeField({ slug: "test", label: "" }); const screen = await render(); await expect.element(screen.getByRole("button", { name: "Update Field" })).toBeDisabled(); }); it("enables save button when label and slug are filled", async () => { const field = makeField({ slug: "test", label: "Test" }); const screen = await render(); await expect.element(screen.getByRole("button", { name: "Update Field" })).toBeEnabled(); }); }); describe("dialog closed", () => { it("renders nothing visible when open is false", async () => { const screen = await render(); expect(screen.getByText("Add Field").query()).toBeNull(); }); }); });