* [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)
143 lines
4.3 KiB
TypeScript
143 lines
4.3 KiB
TypeScript
import { render, screen, fireEvent } from "@testing-library/react";
|
|
import { describe, it, expect, vi } from "vitest";
|
|
import { FunctionForm } from "./FunctionForm";
|
|
import type { DdlParams } from "../../../lib/objectCrud";
|
|
|
|
vi.mock("../../editor/SqlEditorField", () => ({
|
|
SqlEditorField: ({ value, onChange }: { value: string; onChange: (v: string) => void }) => (
|
|
<textarea data-testid="sql-editor" value={value} onChange={(e) => onChange(e.target.value)} />
|
|
),
|
|
}));
|
|
|
|
describe("FunctionForm", () => {
|
|
it("regression: Body row opts out of focus-within outline; normal rows keep it", async () => {
|
|
const params: DdlParams = {
|
|
schema: "public",
|
|
name: "add",
|
|
is_procedure: false,
|
|
action: {
|
|
op: "create_or_replace",
|
|
args: [],
|
|
return_type: "int",
|
|
language: "plpgsql",
|
|
body: "",
|
|
volatility: null,
|
|
strict: false,
|
|
},
|
|
};
|
|
render(<FunctionForm kind="function" params={params} onChange={() => {}} />);
|
|
|
|
// Monaco rows must NOT get the amber in-cell-editing outline.
|
|
const editor = await screen.findByTestId("sql-editor");
|
|
const bodyRow = editor.closest("div.flex.flex-row");
|
|
expect(bodyRow?.className ?? "").not.toContain("focus-within:outline");
|
|
|
|
// Normal rows still carry the outline — the opt-out must be scoped.
|
|
const operationRow = screen
|
|
.getByLabelText("Operation")
|
|
.closest("div.flex.flex-row");
|
|
expect(operationRow?.className ?? "").toContain("focus-within:outline");
|
|
});
|
|
|
|
it("function: renders args grid + return type; emits body", async () => {
|
|
const onChange = vi.fn();
|
|
const params: DdlParams = {
|
|
schema: "public",
|
|
name: "add",
|
|
is_procedure: false,
|
|
action: {
|
|
op: "create_or_replace",
|
|
args: [{ mode: "in", name: "a", type: "int" }],
|
|
return_type: "int",
|
|
language: "plpgsql",
|
|
body: "",
|
|
volatility: null,
|
|
strict: false,
|
|
},
|
|
};
|
|
render(<FunctionForm kind="function" params={params} onChange={onChange} />);
|
|
fireEvent.change(await screen.findByTestId("sql-editor"), {
|
|
target: { value: "BEGIN RETURN a; END" },
|
|
});
|
|
expect(onChange).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
action: expect.objectContaining({ body: "BEGIN RETURN a; END" }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("renders a schema dropdown when schemas are provided", () => {
|
|
const onChange = vi.fn();
|
|
const params: DdlParams = {
|
|
schema: "public",
|
|
name: "add",
|
|
is_procedure: false,
|
|
action: {
|
|
op: "create_or_replace",
|
|
args: [],
|
|
return_type: "int",
|
|
language: "plpgsql",
|
|
body: "",
|
|
volatility: null,
|
|
strict: false,
|
|
},
|
|
};
|
|
render(
|
|
<FunctionForm
|
|
kind="function"
|
|
params={params}
|
|
schemas={["public", "utils"]}
|
|
onChange={onChange}
|
|
/>,
|
|
);
|
|
|
|
const select = screen.getByLabelText("Schema");
|
|
expect(select).toBeInTheDocument();
|
|
expect(screen.getByRole("option", { name: "utils" })).toBeInTheDocument();
|
|
|
|
fireEvent.change(select, { target: { value: "utils" } });
|
|
expect(onChange).toHaveBeenCalledWith(
|
|
expect.objectContaining({ schema: "utils" }),
|
|
);
|
|
});
|
|
|
|
it("procedure: hides return type", () => {
|
|
const params: DdlParams = {
|
|
schema: "public",
|
|
name: "p",
|
|
is_procedure: true,
|
|
action: {
|
|
op: "create_or_replace",
|
|
args: [],
|
|
return_type: null,
|
|
language: "plpgsql",
|
|
body: "",
|
|
volatility: null,
|
|
strict: false,
|
|
},
|
|
};
|
|
render(
|
|
<FunctionForm kind="procedure" params={params} onChange={() => {}} />,
|
|
);
|
|
expect(screen.queryByPlaceholderText("Return type")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("drop op: renders arg_types list", () => {
|
|
const onChange = vi.fn();
|
|
const params: DdlParams = {
|
|
schema: "public",
|
|
name: "add",
|
|
is_procedure: false,
|
|
action: { op: "drop", arg_types: ["int"] },
|
|
};
|
|
render(<FunctionForm kind="function" params={params} onChange={onChange} />);
|
|
fireEvent.change(screen.getByPlaceholderText("Arg types (comma-separated)"), {
|
|
target: { value: "int, int" },
|
|
});
|
|
expect(onChange).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
action: expect.objectContaining({ arg_types: ["int", "int"] }),
|
|
}),
|
|
);
|
|
});
|
|
}); |