v0.5.0 — Grid Interactivity, Home Polish, Deeper PostgreSQL (#8)
* chore: bump version to 0.5.0 (Task 1) * feat(store): v7 migration — favorites + recent_connections (Task 2) * feat(models): add favorite to Connection + Store CRUD (Task 3) * feat(types): ColumnInfo editability + IndexInfo/ConstraintInfo/RecentConnection (Task 4) * chore: bump version to 0.5.0 (Task 1) — lockfile * feat(commands): typed wrappers for favorites/recents/indexes/constraints (Task 5) * feat(db): PG indexes/constraints queries + matview UNION in tables (Task 6) * feat(db): get_table_data editability flags + ctid/rowid locator (Task 7) * feat(db): execute_change no-PK locator guard + affected-count check (Task 8) * feat(db): preserve bigint precision as string on PG read path (Task 9) * feat(db): get_indexes / get_constraints commands (Task 10) * feat(store): favorites + recents Store methods (Task 11) * feat(commands): favorites/recents IPC + register indexes/constraints (Task 12) * feat(store): connectionStore favorites/recents/move-selection (Task 13) * feat(lib): recent-connections pure helpers (Task 14) * feat(store): dbViewerStore indexes/constraints + stageCellEdit (Task 15) * feat(grid): pure editability + filter-operator + cell transform (Task 16) * feat(grid): pure keyboard-nav helper (Task 17) * feat(grid): CellEditor inline editor (Task 18) * feat(grid): CellContextMenu + RowDetailDrawer (Task 19) * feat(grid): focus model + keyboard nav + inline edit + copy + context menu (Task 20) * feat(db-viewer): FilterBuilder drag-and-drop + type-aware operators (Task 21) * feat(db-viewer): ObjectExplorer indexes/constraints/procedures + matview icon (Task 22) * feat(home): ConnectionCard favorite star + on-demand StatusDot (Task 23) * feat(home): move-to-folder + recents strip + status wiring (Task 24) * feat(db-viewer): grid wiring + matview read-only + post-commit refetch (Task 25) * docs: v0.5.0 status + roadmap updates (Task 26) * polish: empty/error/loading states for v0.5.0 surfaces (Task 28) * feat(home): duplicateConnection + useConnectionStatus hook, drop StatusDot (FEAT-A) * feat(home): connection card kebab menu — favorite/test/manage (FEAT-B) * fix(home): populate server_version/latency_ms in test_connection + clean online display * style(home): swap grab handle and kebab positions on connection card * style(home): nudge kebab menu to right-1 * style(home): nudge kebab menu to right-0.5 * feat(home): Escape clears + exits focused search * feat(grid): context-menu View/Select Row, outside-click close, Esc cancels edit, FK reference (GRID-A) * feat(grid): smart CellEditor — enum select, FK searchable dropdown, textarea height (GRID-B) * feat(grid): enums + FK options fed into CellEditor (GRID-C) * feat(grid): FK dropdown display-column labels + placeholder + empty state * fix(grid): FK dropdown renders as fixed overlay to avoid clipping * fix(grid): portal FK dropdown to body + FK reference icon instead of click-to-open * style(grid): move FK reference icon to the start of the cell * feat(grid): optimistic staged cell values + pending dot, cleared on refetch * fix(grid): queue is source of truth for staged values — value diff, Clear All clears dots, same-cell edits replace * fix(db-viewer): type getLocator for staged-value matching * test(db-viewer): unit-test deriveStagedValues; fix activeTab null guard * fix(db-viewer): pass table prop to VirtualDataGrid — staged edits now carry the table name * test(db-viewer): use index access instead of .at() for TS lib target * feat(grid): FK dropdown options as one-row column cells (FK-reference style, cap 5) * style(grid): FK dropdown — values only, fixed 360px width, FK-viewer surface styling * style(grid): harden FK dropdown minWidth to 360px * style(grid): cap FK dropdown cells at 3 * style(grid): cap FK dropdown cells at 4 * fix(grid): pending dot clears on commit — values stay until refetch * fix(db): deserialize pg_attribute char columns as i8 — no more panic on get_table_data * docs: reflect grid interactivity, smart editors, optimistic queue, FK reference, kebab status
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { ObjectExplorerPage } from "./ObjectExplorerPage";
|
||||
import { useDbViewerStore } from "../../stores/dbViewerStore";
|
||||
import * as commands from "../../lib/commands";
|
||||
|
||||
describe("ObjectExplorerPage", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
useDbViewerStore.getState().reset();
|
||||
useDbViewerStore.setState({
|
||||
schemas: ["public"],
|
||||
currentSchema: "public",
|
||||
});
|
||||
vi.spyOn(commands, "getFunctions").mockResolvedValue([]);
|
||||
vi.spyOn(commands, "getIndexes").mockResolvedValue([]);
|
||||
vi.spyOn(commands, "getConstraints").mockResolvedValue([]);
|
||||
vi.spyOn(commands, "getTriggers").mockResolvedValue([]);
|
||||
vi.spyOn(commands, "getSequences").mockResolvedValue([]);
|
||||
vi.spyOn(commands, "getEnums").mockResolvedValue([]);
|
||||
vi.spyOn(commands, "getExtensions").mockResolvedValue([]);
|
||||
});
|
||||
|
||||
|
||||
|
||||
it("renders functions by default", async () => {
|
||||
vi.spyOn(commands, "getFunctions").mockResolvedValue([
|
||||
{
|
||||
name: "add_one",
|
||||
schema: "public",
|
||||
return_type: "int",
|
||||
argument_types: ["int"],
|
||||
argument_names: ["x"],
|
||||
argument_modes: ["IN"],
|
||||
language: "sql",
|
||||
source: "SELECT $1 + 1",
|
||||
kind: "f",
|
||||
},
|
||||
]);
|
||||
render(<ObjectExplorerPage connectionId="c1" />);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("add_one(int)")).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it("functions type filters to kind === 'f'", async () => {
|
||||
vi.spyOn(commands, "getFunctions").mockResolvedValue([
|
||||
{
|
||||
name: "do_thing",
|
||||
schema: "public",
|
||||
return_type: "void",
|
||||
argument_types: [],
|
||||
argument_names: [],
|
||||
argument_modes: [],
|
||||
language: "plpgsql",
|
||||
source: "BEGIN END",
|
||||
kind: "p",
|
||||
},
|
||||
{
|
||||
name: "calc",
|
||||
schema: "public",
|
||||
return_type: "int",
|
||||
argument_types: [],
|
||||
argument_names: [],
|
||||
argument_modes: [],
|
||||
language: "sql",
|
||||
source: "SELECT 1",
|
||||
kind: "f",
|
||||
},
|
||||
]);
|
||||
render(<ObjectExplorerPage connectionId="c1" />);
|
||||
await waitFor(() => expect(screen.getByText("calc")).toBeInTheDocument());
|
||||
expect(screen.queryByText("do_thing")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("indexes type fetches getIndexes and renders the index name + detail", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getIndexes = vi.spyOn(commands, "getIndexes").mockResolvedValue([
|
||||
{
|
||||
name: "idx_users_email",
|
||||
schema: "public",
|
||||
table: "users",
|
||||
definition: "CREATE INDEX idx_users_email ON users USING btree (email);",
|
||||
is_unique: true,
|
||||
method: "btree",
|
||||
columns: ["email"],
|
||||
size_bytes: 8192,
|
||||
tablespace: null,
|
||||
},
|
||||
]);
|
||||
render(<ObjectExplorerPage connectionId="c1" />);
|
||||
await user.click(screen.getByLabelText("Object type"));
|
||||
await user.click(screen.getByText("Indexes"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("idx_users_email")).toBeInTheDocument(),
|
||||
);
|
||||
expect(getIndexes).toHaveBeenCalledWith("c1", "public");
|
||||
await user.click(screen.getByText("idx_users_email"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Index")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("btree").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText("8192")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("constraints type fetches getConstraints and renders the constraint name + detail", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getConstraints = vi.spyOn(commands, "getConstraints").mockResolvedValue([
|
||||
{
|
||||
name: "chk_users_positive",
|
||||
schema: "public",
|
||||
table: "users",
|
||||
contype: "CHECK",
|
||||
definition: "CHECK (age > 0)",
|
||||
deferrable: false,
|
||||
validated: true,
|
||||
columns: ["age"],
|
||||
},
|
||||
]);
|
||||
render(<ObjectExplorerPage connectionId="c1" />);
|
||||
await user.click(screen.getByLabelText("Object type"));
|
||||
await user.click(screen.getByText("Constraints"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("chk_users_positive")).toBeInTheDocument(),
|
||||
);
|
||||
expect(getConstraints).toHaveBeenCalledWith("c1", "public");
|
||||
await user.click(screen.getByText("chk_users_positive"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Constraint")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("CHECK").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText("Deferrable")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows 'No indexes found' when getIndexes returns []", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.spyOn(commands, "getIndexes").mockResolvedValue([]);
|
||||
render(<ObjectExplorerPage connectionId="c1" />);
|
||||
await user.click(screen.getByLabelText("Object type"));
|
||||
await user.click(screen.getByText("Indexes"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("No indexes found")).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows an error message when getIndexes rejects", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.spyOn(commands, "getIndexes").mockRejectedValue(new Error("boom"));
|
||||
render(<ObjectExplorerPage connectionId="c1" />);
|
||||
await user.click(screen.getByLabelText("Object type"));
|
||||
await user.click(screen.getByText("Indexes"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("boom")).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it("procedures type filters getFunctions to kind === 'p'", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.spyOn(commands, "getFunctions").mockResolvedValue([
|
||||
{
|
||||
name: "do_thing",
|
||||
schema: "public",
|
||||
return_type: "void",
|
||||
argument_types: [],
|
||||
argument_names: [],
|
||||
argument_modes: [],
|
||||
language: "plpgsql",
|
||||
source: "BEGIN END",
|
||||
kind: "p",
|
||||
},
|
||||
{
|
||||
name: "calc",
|
||||
schema: "public",
|
||||
return_type: "int",
|
||||
argument_types: [],
|
||||
argument_names: [],
|
||||
argument_modes: [],
|
||||
language: "sql",
|
||||
source: "SELECT 1",
|
||||
kind: "f",
|
||||
},
|
||||
]);
|
||||
render(<ObjectExplorerPage connectionId="c1" />);
|
||||
await user.click(screen.getByLabelText("Object type"));
|
||||
await user.click(screen.getByText("Procedures"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("do_thing")).toBeInTheDocument(),
|
||||
);
|
||||
expect(screen.queryByText("calc")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user