Release / publish (, ubuntu-22.04) (push) Failing after 14m39s
Release / publish (--target aarch64-apple-darwin, macos-latest) (push) Canceled after 0s
Release / publish (--target x86_64-apple-darwin, macos-15-intel) (push) Canceled after 0s
Release / publish (, windows-latest) (push) Canceled after 0s
* feat(models): Change::RebuildTable + role/privilege/readiness/maintenance structs (Task 1.1) * feat(introspection): role/privilege/tablespace/rebuild-readiness query builders (Task 1.2) * feat(lib): rebuild_table payload, table/role object kinds, v0.7.7 capabilities + types (Task 1.3) * feat(object_crud): table_ddl builder (create/edit-diff/options/drop) + build_ddl table arm (Task 2.1) * feat(object_crud): role_ddl builder (create/edit/drop/grant/revoke) + build_ddl role arm (Task 2.2) * feat(object_crud): rebuild_script assembler + fk/grant/sequence introspection queries (Task 2.3) * feat(commands): transactional RebuildTable arm + build_rebuild_script + get_table_columns commands (Task 2.4) * feat(commands): get_roles/privileges/readiness/tablespaces + run_maintenance (Task 2.5) * feat(commands): role/privilege/rebuild/maintenance typed wrappers (Task 3.1) * feat(store): roles slice + openFormTab table/role dedup (Task 3.2) * feat(ui): TableForm (create/edit-diff/rebuild) + cross-schema FK with ON DELETE/UPDATE (Task 4.1) * feat(ui): RoleForm + RoleDetail + RoleGrantsEditor (Task 4.2) * feat(ui): wire roles into object registry + explorer (Task 4.3) * feat(ui): Edit Table + maintenance menu + Create Table toolbar + rebuild badge (Task 4.4) * chore: bump version to 0.7.7 (Task 5.1) * docs: sync README/ROADMAP/AGENTS to v0.7.7 (Task 5.2) * test: v0.7.7 manual smoke checklist (Task 5.3) * chore: sync Cargo.lock to v0.7.7 * style(ui): match TableForm visual layout to shared object-form rows (remove stray padding) * feat(ui): table creator grid redesign + per-column FK slide-in panel with relationship controls * style(ui): table-creator columns grid — bordered cells like the data table view * feat(ui): table creator — type dropdown, PK/Nullable rules, drag-to-reorder columns (vertical), tidy add-column row * feat(ui): table creator — single-PK exclusivity, name on top, fixed column widths + contained h-scroll, no-wrap constraints * style(ui): table creator — constraints column sized to content (no clip), overscroll containment * style(ui): table creator — overscrollBehavior none on both scroll containers, add-column row py-2 * feat(ui): table creator — centered add-column button, schema dropdown auto-selected to current schema * style(ui): table creator — columns grid fills container width, scrolls only when too narrow * feat(ui): table creator — shorthand PG types in dropdown, Auto-Increment only for int-family * feat(ui): FK panel — animate in/out from the right, flush create-table-style rows * fix(ui): FK panel — smooth slide (willChange + eased), local column options from the form grid * feat(ui): FK composer (multi-column, type preview, gated sections) + composite PK + constraints cog in actions col + auto-named FKs * fix(ui): FK panel blocks unnamed table with hint; Foreign keys section lists staged + DB FKs * style(ui): FK panel — transparent full-width selects, title↔select borders, 2-col column picker * style(ui): FK panel — vertical divider between local/ref columns * style(ui): FK panel — section title padding py-2 * style(ui): FK panel — remove row margins + select/text padding in column picker * style(ui): FK panel — flush column picker (no section padding), border-r + py-1 cells * style(ui): FK panel — cell padding + row borders on column picker (user polish) * feat: FK inline into CREATE TABLE (single staged change in create mode); touch up class * style(ui): hide FK icon on PK columns; fix grid header/row column alignment * feat(ui): SQL preview in table creator uses read-only Monaco editor * feat(ui): Foreign keys section — relation rows with icon + Edit/Remove actions * style(ui): FK relation row — label, icon, table on one line * style(ui): FormRow selects transparent like FK panel; test matches FK relation label * style(ui): form-row focus outline amber-400/20 (subtle) * style(ui): SQL view — remove px-4 py-3 wrapper padding * feat(ui): Create Table button label; auto tree refresh after schema-modifying commits; table name on staged ddl card * fix(roles): cast rolconnlimit::int8 to fix panic deserializing int4 into i64 * fix(privileges): cast privilege_type::text to avoid domain-array panic; aclexplode for sequences/schemas (role_sequence_grants removed in PG15, schema_privileges never existed) * feat(ui): role privileges sections collapsible (collapsed by default) with 5-entry preview + fade + Show more * feat(ui): role privileges — collapsed state shows first 5 with fade; expand reveals all * chore(test): drop unused fireEvent import in RoleDetail test * style(ui): hide chevron when section has 5 or fewer entries * docs: README/ROADMAP/AGENTS — v0.7.7 shipped details (roles/grants, table editor, FK composer, privilege explorer); ROADMAP adds v0.7.8 Next up (quick wins) * docs(readme): collapse Recent changes and Key features into details blocks * docs(readme): move hero caption under image; align download + comparison table columns * docs(readme): collapse only body content — keep Recent Changes / Key Features headings visible * docs(readme): per-subsection collapsibles with meaningful summary labels
256 lines
9.8 KiB
TypeScript
256 lines
9.8 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { ChangesQueuePanel } from "./ChangesQueuePanel";
|
|
import { useDbViewerStore } from "../../stores/dbViewerStore";
|
|
import { useUiStore } from "../../stores/uiStore";
|
|
import * as commands from "../../lib/commands";
|
|
|
|
describe("ChangesQueuePanel", () => {
|
|
beforeEach(() => {
|
|
useDbViewerStore.getState().reset();
|
|
useUiStore.setState({ activeConnectionId: "c1" });
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
it("shows nothing when queue is empty", () => {
|
|
const { container } = render(<ChangesQueuePanel />);
|
|
expect(container.textContent).toBe("");
|
|
});
|
|
|
|
it("shows the pending-changes header and a change card", () => {
|
|
useDbViewerStore.getState().addChange({
|
|
type: "update",
|
|
schema: "public",
|
|
table: "users",
|
|
primaryKey: { id: 1 },
|
|
oldData: { name: "Bob" },
|
|
newData: { name: "Alice" },
|
|
description: "Update row in users",
|
|
} as any);
|
|
render(<ChangesQueuePanel />);
|
|
expect(screen.getByText(/pending changes/i)).toBeInTheDocument();
|
|
expect(screen.getByText(/update/i)).toBeInTheDocument();
|
|
expect(screen.getByText(/public.users/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows the old → new value diff on update cards", () => {
|
|
useDbViewerStore.getState().addChange({
|
|
type: "update",
|
|
schema: "public",
|
|
table: "users",
|
|
primaryKey: { id: 1 },
|
|
oldData: { name: "Bob" },
|
|
newData: { name: "Alice" },
|
|
description: "Update row in users",
|
|
} as any);
|
|
render(<ChangesQueuePanel />);
|
|
// the diff renders old (struck) → new (accent) as separate spans
|
|
expect(screen.getByText(/name: Bob/)).toBeInTheDocument();
|
|
expect(screen.getByText("Alice")).toBeInTheDocument();
|
|
});
|
|
|
|
it("revert removes the change from the queue", async () => {
|
|
const user = userEvent.setup();
|
|
useDbViewerStore.getState().addChange({
|
|
type: "update",
|
|
schema: "public",
|
|
table: "users",
|
|
primaryKey: { id: 1 },
|
|
oldData: { name: "Bob" },
|
|
newData: { name: "Alice" },
|
|
description: "Update row in users",
|
|
} as any);
|
|
render(<ChangesQueuePanel />);
|
|
await user.click(screen.getByRole("button", { name: /revert/i }));
|
|
expect(useDbViewerStore.getState().changesQueue).toHaveLength(0);
|
|
});
|
|
|
|
it("labels bulk_insert / empty_table / drop_table cards", () => {
|
|
useDbViewerStore.getState().addChange({
|
|
type: "bulk_insert",
|
|
schema: "public",
|
|
table: "t",
|
|
columns: ["a"],
|
|
rows: [[1]],
|
|
description: "Import 2 rows into public.t",
|
|
} as any);
|
|
useDbViewerStore.getState().addChange({
|
|
type: "empty_table",
|
|
schema: "public",
|
|
table: "t",
|
|
description: "Empty Table: public.t",
|
|
} as any);
|
|
useDbViewerStore.getState().addChange({
|
|
type: "drop_table",
|
|
schema: "public",
|
|
table: "t",
|
|
description: "Drop Table: public.t",
|
|
} as any);
|
|
render(<ChangesQueuePanel />);
|
|
expect(screen.getByText(/import 2 rows into public.t/i)).toBeInTheDocument();
|
|
expect(screen.getByText(/empty table: public.t/i)).toBeInTheDocument();
|
|
expect(screen.getByText(/drop table: public.t/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders a ddl change with a DDL badge + description (visual) and SQL preview (sql view)", async () => {
|
|
const user = userEvent.setup();
|
|
useDbViewerStore.getState().addChange({
|
|
type: "ddl",
|
|
sql: "DROP INDEX public.i",
|
|
description: "Drop index i",
|
|
} as any);
|
|
render(<ChangesQueuePanel />);
|
|
expect(screen.getByText("DDL")).toBeInTheDocument();
|
|
expect(screen.getByText("Drop index i")).toBeInTheDocument();
|
|
await user.click(screen.getByRole("button", { name: /sql/i }));
|
|
expect(screen.getByText(/DROP INDEX public\.i/)).toBeInTheDocument();
|
|
});
|
|
|
|
it("commit calls executeChange with buildChangePayload output for insert", async () => {
|
|
const exec = vi.spyOn(commands, "executeChange").mockResolvedValue(undefined);
|
|
useDbViewerStore.getState().addChange({
|
|
type: "insert",
|
|
schema: "public",
|
|
table: "t",
|
|
newData: { id: 1, name: "Alice" },
|
|
description: "Insert row into t",
|
|
});
|
|
render(<ChangesQueuePanel />);
|
|
fireEvent.click(screen.getByRole("button", { name: /commit all/i }));
|
|
await waitFor(() => expect(exec).toHaveBeenCalled());
|
|
expect(exec.mock.calls[0][0]).toBe("c1");
|
|
expect(exec.mock.calls[0][1]).toEqual(expect.objectContaining({
|
|
type: "insert",
|
|
schema: "public",
|
|
table: "t",
|
|
data: expect.any(String),
|
|
}));
|
|
});
|
|
|
|
it("refreshes the schema tree after committing a drop_table change", async () => {
|
|
vi.spyOn(commands, "executeChange").mockResolvedValue(undefined);
|
|
const getSchemas = vi.spyOn(commands, "getSchemas").mockResolvedValue(["public"]);
|
|
vi.spyOn(commands, "getDatabases").mockResolvedValue(["mydb"]);
|
|
vi.spyOn(commands, "getTables").mockResolvedValue([] as any);
|
|
useDbViewerStore.getState().addChange({
|
|
type: "drop_table",
|
|
schema: "public",
|
|
table: "t",
|
|
description: "Drop Table: public.t",
|
|
});
|
|
render(<ChangesQueuePanel />);
|
|
fireEvent.click(screen.getByRole("button", { name: /commit all/i }));
|
|
await waitFor(() => expect(getSchemas).toHaveBeenCalledWith("c1"));
|
|
});
|
|
|
|
it("refreshes the schema tree after committing a schema-modifying ddl change", async () => {
|
|
vi.spyOn(commands, "executeChange").mockResolvedValue(undefined);
|
|
const getSchemas = vi.spyOn(commands, "getSchemas").mockResolvedValue(["public"]);
|
|
vi.spyOn(commands, "getDatabases").mockResolvedValue(["mydb"]);
|
|
vi.spyOn(commands, "getTables").mockResolvedValue([] as any);
|
|
useDbViewerStore.getState().addChange({
|
|
type: "ddl",
|
|
schema: "public",
|
|
table: "products",
|
|
sql: 'CREATE TABLE "public"."products" ("id" integer NOT NULL)',
|
|
description: "Create Table",
|
|
});
|
|
render(<ChangesQueuePanel />);
|
|
fireEvent.click(screen.getByRole("button", { name: /commit all/i }));
|
|
await waitFor(() => expect(getSchemas).toHaveBeenCalledWith("c1"));
|
|
});
|
|
|
|
it("SQL toggle shows the generated SQL", async () => {
|
|
const user = userEvent.setup();
|
|
useDbViewerStore.getState().addChange({
|
|
type: "insert",
|
|
schema: "public",
|
|
table: "users",
|
|
newData: { name: "Alice" },
|
|
description: "Insert row into users",
|
|
} as any);
|
|
render(<ChangesQueuePanel />);
|
|
await user.click(screen.getByRole("button", { name: /sql/i }));
|
|
expect(screen.getByText(/insert into "public"."users"/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it("Cmd+S commits all pending changes", async () => {
|
|
const exec = vi.spyOn(commands, "executeChange").mockResolvedValue(undefined);
|
|
useDbViewerStore.getState().addChange({
|
|
type: "insert",
|
|
schema: "public",
|
|
table: "t",
|
|
newData: { a: 1 },
|
|
description: "Insert row into t",
|
|
} as any);
|
|
render(<ChangesQueuePanel />);
|
|
fireEvent.keyDown(document, { key: "s", metaKey: true });
|
|
await waitFor(() => expect(exec).toHaveBeenCalled());
|
|
});
|
|
|
|
it("Clear All empties the queue", async () => {
|
|
const user = userEvent.setup();
|
|
useDbViewerStore.getState().addChange({
|
|
type: "insert",
|
|
schema: "public",
|
|
table: "t",
|
|
newData: { a: 1 },
|
|
description: "Insert row into t",
|
|
} as any);
|
|
render(<ChangesQueuePanel />);
|
|
await user.click(screen.getByRole("button", { name: /clear all/i }));
|
|
expect(useDbViewerStore.getState().changesQueue).toHaveLength(0);
|
|
});
|
|
|
|
it("calls onCommitted after a successful commit cycle", async () => {
|
|
const onCommitted = vi.fn();
|
|
vi.spyOn(commands, "executeChange").mockResolvedValue(undefined);
|
|
useDbViewerStore.getState().addChange({
|
|
type: "insert",
|
|
schema: "public",
|
|
table: "t",
|
|
newData: { a: 1 },
|
|
description: "Insert row into t",
|
|
} as any);
|
|
render(<ChangesQueuePanel onCommitted={onCommitted} />);
|
|
fireEvent.click(screen.getByRole("button", { name: /commit all/i }));
|
|
await waitFor(() => expect(onCommitted).toHaveBeenCalledTimes(1));
|
|
});
|
|
|
|
it("shows a green check on committed changes after Commit All", async () => {
|
|
vi.spyOn(commands, "executeChange").mockResolvedValue(undefined);
|
|
useDbViewerStore.getState().addChange({ type: "insert", schema: "public", table: "t", newData: { a: 1 }, description: "Insert row into t" } as any);
|
|
render(<ChangesQueuePanel />);
|
|
fireEvent.click(screen.getByRole("button", { name: /commit all/i }));
|
|
await waitFor(() => expect(screen.getByTitle("Committed")).toBeInTheDocument());
|
|
});
|
|
|
|
it("auto-closes tabs for a table dropped via Commit All", async () => {
|
|
vi.spyOn(commands, "executeChange").mockResolvedValue(undefined);
|
|
useDbViewerStore.getState().openTab("public", "users");
|
|
useDbViewerStore.getState().openTab("public", "posts", true);
|
|
useDbViewerStore.getState().addChange({ type: "drop_table", schema: "public", table: "users", description: "Drop Table: public.users" } as any);
|
|
render(<ChangesQueuePanel />);
|
|
fireEvent.click(screen.getByRole("button", { name: /commit all/i }));
|
|
await waitFor(() => {
|
|
const tabs = useDbViewerStore.getState().tabs;
|
|
expect(tabs.some((t) => t.table === "users")).toBe(false);
|
|
expect(tabs.some((t) => t.table === "posts")).toBe(true);
|
|
});
|
|
});
|
|
|
|
it("renders a rebuild_table change with an amber REBUILD badge and SQL preview", () => {
|
|
useDbViewerStore.getState().addChange({
|
|
type: "rebuild_table",
|
|
schema: "public",
|
|
table: "users",
|
|
sql: "BEGIN; ALTER TABLE \"public\".\"users\" ...; COMMIT;",
|
|
description: "Rebuild public.users",
|
|
} as any);
|
|
render(<ChangesQueuePanel />);
|
|
expect(screen.getByText("REBUILD")).toBeInTheDocument();
|
|
expect(screen.getByText(/rebuild public\.users/i)).toBeInTheDocument();
|
|
expect(screen.getByText(/BEGIN;/)).toBeInTheDocument();
|
|
});
|
|
}); |