Release / publish (, ubuntu-22.04) (push) Failing after 14m42s
Release / publish (, windows-latest) (push) Canceled after 0s
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
* [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)
159 lines
5.1 KiB
TypeScript
159 lines
5.1 KiB
TypeScript
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
import { ObjectFormTab } from "./ObjectFormTab";
|
|
import * as objectCrud from "../../../lib/objectCrud";
|
|
import {
|
|
useDbViewerStore,
|
|
type ViewerTab,
|
|
} from "../../../stores/dbViewerStore";
|
|
|
|
vi.mock("../../../lib/objectCrud", () => ({
|
|
buildObjectDdl: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../../editor/SqlEditorField", () => ({
|
|
SqlEditorField: ({ value, readOnly }: { value: string; readOnly?: boolean }) => (
|
|
<textarea data-testid="sql-editor" readOnly={readOnly} value={value} onChange={() => {}} />
|
|
),
|
|
}));
|
|
|
|
describe("ObjectFormTab", () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.clearAllMocks();
|
|
useDbViewerStore.getState().reset();
|
|
});
|
|
|
|
const connectionId = "c1";
|
|
const baseTab: ViewerTab = {
|
|
id: "form-tab-1",
|
|
schema: "public",
|
|
table: "Create sequence",
|
|
page: 1,
|
|
pageSize: 50,
|
|
loading: false,
|
|
error: null,
|
|
data: null,
|
|
filterRules: [],
|
|
sortRules: [],
|
|
hiddenColumns: [],
|
|
smartSortApplied: false,
|
|
tabType: "objectForm",
|
|
objectType: null,
|
|
form: {
|
|
kind: "sequence",
|
|
title: "Create sequence",
|
|
description: "Create my_seq",
|
|
mode: "create",
|
|
params: { schema: "public", name: "my_seq", action: { op: "create" } },
|
|
},
|
|
};
|
|
|
|
it("renders the form for a sequence kind in Visual view", () => {
|
|
vi.mocked(objectCrud.buildObjectDdl).mockResolvedValue([
|
|
"CREATE SEQUENCE \"public\".\"my_seq\" START WITH 1;",
|
|
]);
|
|
render(<ObjectFormTab connectionId={connectionId} tab={baseTab} />);
|
|
expect(screen.getByPlaceholderText("Sequence name")).toBeInTheDocument();
|
|
});
|
|
|
|
it("toggles to SQL and shows the generated SQL, then back to Visual", async () => {
|
|
vi.mocked(objectCrud.buildObjectDdl).mockResolvedValue([
|
|
"CREATE SEQUENCE \"public\".\"my_seq\" START WITH 1;",
|
|
]);
|
|
render(<ObjectFormTab connectionId={connectionId} tab={baseTab} />);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "SQL" }));
|
|
const ed = await screen.findByTestId("sql-editor");
|
|
expect(ed).toHaveValue('CREATE SEQUENCE "public"."my_seq" START WITH 1;');
|
|
expect(ed).toHaveProperty("readOnly", true);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Visual" }));
|
|
expect(screen.getByPlaceholderText("Sequence name")).toBeInTheDocument();
|
|
});
|
|
|
|
it("stages one ddl change and closes the tab", async () => {
|
|
vi.mocked(objectCrud.buildObjectDdl).mockResolvedValue([
|
|
"CREATE SEQUENCE \"public\".\"my_seq\" START WITH 1;",
|
|
]);
|
|
useDbViewerStore.setState({
|
|
tabs: [baseTab],
|
|
activeTabId: baseTab.id,
|
|
});
|
|
const addChange = vi.spyOn(useDbViewerStore.getState(), "addChange");
|
|
|
|
render(<ObjectFormTab connectionId={connectionId} tab={baseTab} />);
|
|
fireEvent.click(screen.getByRole("button", { name: /stage/i }));
|
|
|
|
await waitFor(() =>
|
|
expect(addChange).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: "ddl",
|
|
sql: 'CREATE SEQUENCE "public"."my_seq" START WITH 1;',
|
|
description: "Create my_seq",
|
|
}),
|
|
),
|
|
);
|
|
expect(useDbViewerStore.getState().tabs).toHaveLength(0);
|
|
});
|
|
|
|
it("stages multiple statements with (n/total) descriptions", async () => {
|
|
vi.mocked(objectCrud.buildObjectDdl).mockResolvedValue([
|
|
"DROP SEQUENCE \"public\".\"my_seq\";",
|
|
"CREATE SEQUENCE \"public\".\"my_seq\" START WITH 1;",
|
|
]);
|
|
useDbViewerStore.setState({
|
|
tabs: [baseTab],
|
|
activeTabId: baseTab.id,
|
|
});
|
|
const addChange = vi.spyOn(useDbViewerStore.getState(), "addChange");
|
|
|
|
render(<ObjectFormTab connectionId={connectionId} tab={baseTab} />);
|
|
fireEvent.click(screen.getByRole("button", { name: /stage/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(addChange).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({ description: "Create my_seq (1/2)" }),
|
|
);
|
|
expect(addChange).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({ description: "Create my_seq (2/2)" }),
|
|
);
|
|
});
|
|
expect(useDbViewerStore.getState().tabs).toHaveLength(0);
|
|
});
|
|
|
|
it("shows a builder error and disables Stage", async () => {
|
|
vi.mocked(objectCrud.buildObjectDdl).mockRejectedValueOnce(
|
|
"Name is required",
|
|
);
|
|
render(<ObjectFormTab connectionId={connectionId} tab={baseTab} />);
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getByText(/Name is required/)).toBeInTheDocument(),
|
|
);
|
|
expect(screen.getByRole("button", { name: /stage/i })).toBeDisabled();
|
|
});
|
|
|
|
it("renders only the uppercase kicker, not a duplicate title heading", () => {
|
|
vi.mocked(objectCrud.buildObjectDdl).mockResolvedValue([]);
|
|
render(
|
|
<ObjectFormTab
|
|
connectionId={connectionId}
|
|
tab={{
|
|
...baseTab,
|
|
table: "Create Function",
|
|
form: {
|
|
...baseTab.form!,
|
|
kind: "function",
|
|
title: "Create Function",
|
|
},
|
|
}}
|
|
/>,
|
|
);
|
|
// Kicker: lowercase DOM text uppercased by CSS
|
|
expect(screen.getByText("create function")).toBeInTheDocument();
|
|
expect(screen.queryByText("Create Function")).toBeNull();
|
|
});
|
|
}); |