import { describe, it, expect, vi } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { CellEditor } from "./CellEditor"; describe("CellEditor", () => { it("renders the initial value and commits on Enter", () => { const onCommit = vi.fn(); const onCancel = vi.fn(); render(); const input = screen.getByRole("textbox"); expect(input).toHaveValue("Alice"); fireEvent.change(input, { target: { value: "Alicia" } }); fireEvent.keyDown(input, { key: "Enter" }); expect(onCommit).toHaveBeenCalledWith("Alicia"); }); it("renders a native date input for date columns", () => { render(); const input = screen.getByDisplayValue("2024-01-15"); expect(input).toHaveAttribute("type", "date"); }); it("renders a datetime-local input for timestamp columns", () => { render(); const input = screen.getByDisplayValue("2024-01-15T10:30"); expect(input).toHaveAttribute("type", "datetime-local"); }); it("commits a datetime-local value in canonical DB format (space separator + seconds)", () => { const onCommit = vi.fn(); render(); const input = screen.getByDisplayValue("2024-01-15T10:30"); fireEvent.change(input, { target: { value: "2024-01-15T11:45" } }); fireEvent.keyDown(input, { key: "Enter" }); expect(onCommit).toHaveBeenCalledWith("2024-01-15 11:45:00"); }); it("renders a boolean select with true/false options", () => { render(); const select = screen.getByRole("combobox"); expect(select).toHaveValue("true"); expect(screen.getByRole("option", { name: "true" })).toBeInTheDocument(); expect(screen.getByRole("option", { name: "false" })).toBeInTheDocument(); }); it("renders a number input for integer columns", () => { render(); const input = screen.getByRole("spinbutton"); expect(input).toHaveAttribute("type", "number"); expect(input).toHaveAttribute("step", "1"); }); it("commits null when a nullable cell is emptied", () => { const onCommit = vi.fn(); render(); const input = screen.getByRole("textbox"); fireEvent.change(input, { target: { value: "" } }); fireEvent.keyDown(input, { key: "Enter" }); expect(onCommit).toHaveBeenCalledWith(null); }); it("leaves an empty nullable cell alone when it was already empty (NULL stays NULL)", () => { const onCommit = vi.fn(); render(); fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter" }); expect(onCommit).toHaveBeenCalledWith(null); }); it("cancels on Escape", () => { const onCancel = vi.fn(); render(); fireEvent.keyDown(screen.getByRole("textbox"), { key: "Escape" }); expect(onCancel).toHaveBeenCalled(); }); it("uses textarea for large/JSON columns", () => { render(); expect(screen.getByRole("textbox").tagName).toBe("TEXTAREA"); }); it("blocks empty commits on non-nullable non-text columns and shows an error", () => { const onCommit = vi.fn(); render(); const input = screen.getByRole("spinbutton"); fireEvent.change(input, { target: { value: "" } }); fireEvent.keyDown(input, { key: "Enter" }); expect(onCommit).not.toHaveBeenCalled(); expect(screen.getByTestId("cell-editor-error")).toBeInTheDocument(); }); it("commits an empty string on non-nullable text columns", () => { const onCommit = vi.fn(); render(); const input = screen.getByRole("textbox"); fireEvent.change(input, { target: { value: "" } }); fireEvent.keyDown(input, { key: "Enter" }); expect(onCommit).toHaveBeenCalledWith(""); }); it("clears the validation error as soon as the user types again", () => { const onCommit = vi.fn(); render(); const input = screen.getByRole("spinbutton"); fireEvent.change(input, { target: { value: "" } }); fireEvent.keyDown(input, { key: "Enter" }); expect(screen.getByTestId("cell-editor-error")).toBeInTheDocument(); fireEvent.change(input, { target: { value: "5" } }); expect(screen.queryByTestId("cell-editor-error")).toBeNull(); }); it("does not offer a NULL row in the FK dropdown for non-nullable FK columns", () => { const onCommit = vi.fn(); render( ); const buttons = screen.getAllByRole("button"); expect(buttons.some((b) => b.textContent === "NULL")).toBe(false); }); it("offers a NULL row in the FK dropdown for nullable FK columns", () => { const onCommit = vi.fn(); render( ); const nullRow = screen.getAllByRole("button").find((b) => b.textContent === "NULL"); expect(nullRow).toBeDefined(); fireEvent.click(nullRow!); expect(onCommit).toHaveBeenCalledWith(null); }); it("commits null when the NULL option is selected in enum mode", () => { const onCommit = vi.fn(); render( ); fireEvent.change(screen.getByRole("combobox"), { target: { value: "" } }); expect(onCommit).toHaveBeenCalledWith(null); }); it("renders a combobox with enum values and commits on change", () => { const onCommit = vi.fn(); render( ); const select = screen.getByRole("combobox"); expect(select).toBeInTheDocument(); const labels = screen.getAllByRole("option").map((o) => o.textContent); expect(labels).toEqual(expect.arrayContaining(["active", "inactive", "pending"])); fireEvent.change(select, { target: { value: "pending" } }); expect(onCommit).toHaveBeenCalledWith("pending"); }); it("filters FK options by query and commits the clicked value", () => { const onCommit = vi.fn(); render( ); const search = screen.getByLabelText(/search foreign key/i); fireEvent.change(search, { target: { value: "bo" } }); const buttons = screen.getAllByRole("button"); expect(buttons).toHaveLength(1); expect(buttons[0]).toHaveTextContent("2 — Bob"); fireEvent.click(buttons[0]); expect(onCommit).toHaveBeenCalledWith("2"); }); it("shows all FK options when the query is empty", () => { render( ); expect(screen.getAllByRole("button")).toHaveLength(2); }); it("renders text columns as a single-line scrolling textarea", () => { render(); const input = screen.getByRole("textbox"); expect(input.tagName).toBe("TEXTAREA"); expect(input.className).toContain("overflow-y-auto"); }); it("renders the FK placeholder and a No matches empty state", () => { const onCommit = vi.fn(); render(); const search = screen.getByLabelText(/search foreign key/i); expect(search).toHaveAttribute("placeholder", "Search users…"); fireEvent.change(search, { target: { value: "zzz" } }); expect(screen.getByText("No matches")).toBeInTheDocument(); }); it("renders the FK option list as a fixed-position overlay so it is never clipped", () => { const onCommit = vi.fn(); render(); const list = screen.getByTestId("fk-options"); expect(list.style.position).toBe("fixed"); expect(document.body.contains(list)).toBe(true); expect(list).toHaveTextContent("1 — Alice"); expect(list).toHaveTextContent("2 — Bob"); }); it("renders FK options as one-row values only (FK-reference style, cap 4, fixed width)", () => { const onCommit = vi.fn(); const cells = Array.from({ length: 6 }, (_, i) => ({ name: `col${i}`, value: `v${i}`, })); render(); const list = screen.getByTestId("fk-options"); // first 3 values shown, column names NOT shown, 4th+ capped for (let i = 0; i < 4; i++) { expect(list).toHaveTextContent(`v${i}`); } expect(list).not.toHaveTextContent("col0"); expect(list).not.toHaveTextContent("v4"); // fixed 360px width, FK-viewer surface styling expect(list.style.width).toBe("360px"); expect(list.className).toContain("bg-surface"); expect(list.className).toContain("rounded-lg"); expect(list.className).toContain("shadow-xl"); // clicking the row commits the value fireEvent.click(screen.getByRole("button")); expect(onCommit).toHaveBeenCalledWith("1"); }); });