* [P1-T1] Change::Ddl Rust variant + execute_change arm * [P1-T2] Frontend ddl change type + objectCrud capability * [P1-T3] use_keychain data model + migration v8 * [P2-T1] object_crud skeleton + validators + build_ddl dispatch * [P2-T2] Sequence builders * [P2-T3] Enum builders (no value removal) * [P2-T4] View / matview / extension builders * [P2-T5] Index + constraint builders * [P2-T6] Function / procedure + trigger builders * [P2-T7] build_object_ddl + get_available_extensions commands + wrappers * [P3-T1] conditional keychain + session passwords * [P3-T2] keychain-off password prompt on connect * [P3-T3] default-ON keychain opt-out + tooltip + modal conditional * [P3-T4] ddl queue card + after-commit refetch * [P4-T1] ObjectCrudDialog shell * [P4-T2] SequenceForm * [P4-T3] EnumForm with no-removal note * [P4-T4] ExtensionForm with available-extensions picker * [P4-T5] ViewForm (view + materialized view) * [P5-T1] IndexForm with column picker * [P5-T2] ConstraintForm (check/unique/pk/fk) + ColumnPicker * [P5-T3] FunctionForm (function + procedure) * [P5-T4] TriggerForm with trigger-function picker * [P5-T5] ObjectContextMenu + Explorer/TableOverflowMenu CRUD wiring * [P6-T1] object tab type + openObjectTab dedup * [P6-T2] extract ObjectDetail for object tabs * [P6-T3] Objects view two-pane sidebar + workspace * [P6-T4] object-tab content + per-type tab icons * [P7-T1] Version bump 0.7.5 -> 0.7.6 * [P7-T2] docs sync README/ROADMAP/AGENTS for v0.7.6 * [P7-T3] chore: Cargo.lock version sync 0.7.5 -> 0.7.6 * fix(ui): object tab icon stacks above name (preflight svg block) * fix(ui): optically center object tab icon with name * fix(ui): object tab icon matches query/table icon handling * [UI-POLISH-1] objectForm tab type + openFormTab store action * [UI-POLISH-2] ObjectFormTab + KindForm with Visual/SQL toggle * [UI-POLISH-3] route create/edit through form tabs; remove modal * docs: create/edit now open as form tabs (AGENTS sync) * [FB-1] follow app styling patterns + schema dropdown in forms * [FB-2] Monaco editor for function body + view definition * [FB-3] form tabs styled like viewers + in-cell editing * [FB-5] focus outline scoped to input area (label excluded) * [FB-6] no amber focus outline on Monaco body/definition rows * [FB-7] header dedupe + schema default + full edit prefill * [FB-8] SQL view in read-only Monaco editor * docs: roadmap — table create/edit + relationships (next) * docs: roadmap — Admin follow-up is 0.7.7 (next after 0.7.6)
120 lines
6.4 KiB
TypeScript
120 lines
6.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { TableOverflowMenu } from "./TableOverflowMenu";
|
|
import { useDbViewerStore } from "../../stores/dbViewerStore";
|
|
import { useUiStore } from "../../stores/uiStore";
|
|
import * as commands from "../../lib/commands";
|
|
import * as exportData from "../../lib/exportData";
|
|
|
|
describe("TableOverflowMenu", () => {
|
|
beforeEach(() => {
|
|
Object.defineProperty(navigator, "clipboard", {
|
|
value: { writeText: vi.fn() },
|
|
configurable: true,
|
|
writable: true,
|
|
});
|
|
useDbViewerStore.getState().reset();
|
|
useUiStore.setState({ activeConnectionId: "c1" });
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
it("offers Create Index… and Create Constraint…", () => {
|
|
render(<TableOverflowMenu schema="public" table="users" onOpenTab={() => "tab-1"} connectionId="c1" />);
|
|
fireEvent.click(screen.getByLabelText(/table options/i));
|
|
expect(screen.getByText("Create Index…")).toBeInTheDocument();
|
|
expect(screen.getByText("Create Constraint…")).toBeInTheDocument();
|
|
});
|
|
|
|
it("opens objectForm create tabs for Create Index… and Create Constraint…", async () => {
|
|
render(<TableOverflowMenu schema="public" table="users" onOpenTab={() => "tab-1"} connectionId="c1" />);
|
|
fireEvent.click(screen.getByLabelText(/table options/i));
|
|
fireEvent.click(screen.getByText("Create Index…"));
|
|
const st = useDbViewerStore.getState();
|
|
expect(st.tabs).toHaveLength(1);
|
|
expect(st.tabs[0].tabType).toBe("objectForm");
|
|
expect(st.tabs[0].form?.kind).toBe("index");
|
|
expect(st.tabs[0].form?.mode).toBe("create");
|
|
|
|
useDbViewerStore.getState().reset();
|
|
render(<TableOverflowMenu schema="public" table="users" onOpenTab={() => "tab-1"} connectionId="c1" />);
|
|
fireEvent.click(screen.getAllByLabelText(/table options/i)[1]);
|
|
fireEvent.click(screen.getByText("Create Constraint…"));
|
|
expect(useDbViewerStore.getState().tabs[0].tabType).toBe("objectForm");
|
|
expect(useDbViewerStore.getState().tabs[0].form?.kind).toBe("constraint");
|
|
});
|
|
|
|
it("renders menu trigger button", () => {
|
|
render(<TableOverflowMenu schema="public" table="users" onOpenTab={() => "tab-1"} />);
|
|
expect(screen.getByLabelText(/table options/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows menu options on click", async () => {
|
|
const user = userEvent.setup();
|
|
render(<TableOverflowMenu schema="public" table="users" onOpenTab={() => "tab-1"} />);
|
|
await user.click(screen.getByLabelText(/table options/i));
|
|
expect(screen.getByText("Open in new tab")).toBeInTheDocument();
|
|
expect(screen.getByText("Copy table schema")).toBeInTheDocument();
|
|
expect(screen.getByText("Export data (CSV)")).toBeInTheDocument();
|
|
});
|
|
|
|
it("fires onOpenTab when menu item clicked", async () => {
|
|
const user = userEvent.setup();
|
|
const onOpenTab = vi.fn().mockReturnValue("tab-1");
|
|
render(<TableOverflowMenu schema="public" table="users" onOpenTab={onOpenTab} />);
|
|
await user.click(screen.getByLabelText(/table options/i));
|
|
await user.click(screen.getByText("Open in new tab"));
|
|
expect(onOpenTab).toHaveBeenCalledWith("public", "users", true);
|
|
});
|
|
|
|
it("Copy table schema calls getTableDdl and writes clipboard", async () => {
|
|
vi.spyOn(commands, "getTableDdl").mockResolvedValue("CREATE TABLE t (id int)");
|
|
const writeText = (navigator.clipboard as any).writeText as ReturnType<typeof vi.fn>;
|
|
render(<TableOverflowMenu schema="public" table="t" onOpenTab={() => "tab-1"} />);
|
|
fireEvent.click(screen.getByLabelText(/table options/i));
|
|
fireEvent.click(screen.getByText(/copy table schema/i));
|
|
await waitFor(() => expect(writeText).toHaveBeenCalledWith("CREATE TABLE t (id int)"));
|
|
});
|
|
|
|
it("Empty Table opens confirm then stages an empty_table change", async () => {
|
|
render(<TableOverflowMenu schema="public" table="t" onOpenTab={() => "tab-1"} />);
|
|
fireEvent.click(screen.getByLabelText(/table options/i));
|
|
fireEvent.click(screen.getByText(/empty table/i));
|
|
fireEvent.click(screen.getByRole("button", { name: /empty table/i }));
|
|
await waitFor(() => {
|
|
const q = useDbViewerStore.getState().changesQueue;
|
|
expect(q[q.length - 1]).toEqual(expect.objectContaining({ type: "empty_table", schema: "public", table: "t" }));
|
|
});
|
|
});
|
|
|
|
it("Delete Table opens confirm then stages a drop_table change", async () => {
|
|
vi.spyOn(commands, "getObjectDependencies").mockResolvedValue([]);
|
|
render(<TableOverflowMenu schema="public" table="t" onOpenTab={() => "tab-1"} />);
|
|
fireEvent.click(screen.getByLabelText(/table options/i));
|
|
fireEvent.click(screen.getByText(/delete table/i));
|
|
await waitFor(() => expect(screen.queryByText(/open in new tab/i)).not.toBeInTheDocument());
|
|
fireEvent.click(screen.getByRole("button", { name: /delete table/i }));
|
|
await waitFor(() => {
|
|
const q = useDbViewerStore.getState().changesQueue;
|
|
expect(q[q.length - 1]).toEqual(expect.objectContaining({ type: "drop_table", schema: "public", table: "t" }));
|
|
});
|
|
});
|
|
|
|
it("Delete Table fetches dependencies and shows DependencyDialog before confirming", async () => {
|
|
vi.spyOn(commands, "getObjectDependencies").mockResolvedValue([{ deptype: "n", class: "pg_class", name: "v_orders" }]);
|
|
render(<TableOverflowMenu schema="public" table="orders" connectionId="c1" onOpenTab={() => "tab-1"} />);
|
|
fireEvent.click(screen.getByLabelText(/table options/i));
|
|
fireEvent.click(screen.getByText(/delete table/i));
|
|
await waitFor(() => expect(commands.getObjectDependencies).toHaveBeenCalledWith("c1", "public", "table", "orders"));
|
|
await waitFor(() => expect(screen.getByText("v_orders")).toBeInTheDocument());
|
|
});
|
|
|
|
it("Export data calls exportData when rows and columns are provided", async () => {
|
|
const spy = vi.spyOn(exportData, "exportData");
|
|
const columns = [{ name: "id", data_type: "integer", is_nullable: false, is_pk: true, is_fk: false, fk_ref: null, default_value: null, editable: true, is_generated: false }];
|
|
render(<TableOverflowMenu schema="public" table="t" onOpenTab={() => "tab-1"} columns={columns} rows={[[1]]} />);
|
|
fireEvent.click(screen.getByLabelText(/table options/i));
|
|
fireEvent.click(screen.getByText(/export data \(csv\)/i));
|
|
await waitFor(() => expect(spy).toHaveBeenCalledWith([[1]], columns, "csv", "public.t"));
|
|
});
|
|
}); |