Files
gridline/desktop/src/stores/dbViewerStore.test.ts
T
adrianbonpin 88e7e45818 feat(mysql): native SQL execution — fix #41 type decoding, in-process restore, .sql open-with (#42)
Fixes #41.

MySQL columns now decode by their declared type instead of a blind try_get chain,
so every non-string type (integers, DECIMAL, BIT, YEAR, DATE/TIME/DATETIME/TIMESTAMP)
renders its value instead of NULL. MySQL restore runs in-process over the sqlx driver
(no bundled MariaDB client, so MySQL 8 caching_sha2_password works) and the Clean toggle
is implemented. .sql files can be opened with Gridline from the OS file manager.

Verified: cargo test --lib 442 passed / 0 failed; bun run test 133 files / 1151 tests
exit 0; tsc clean; both live MySQL 8 integration tests pass with the bundled CLI hidden.
2026-09-29 08:52:47 +08:00

684 lines
28 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from "vitest";
import { useDbViewerStore } from "./dbViewerStore";
import type { QueryResult, TableInfo, RoleInfo } from "../lib/types";
import * as commands from "../lib/commands";
vi.mock("../lib/commands", () => ({
getDatabases: vi.fn(),
getSchemas: vi.fn(),
getTables: vi.fn(),
}));
beforeEach(() => {
useDbViewerStore.getState().reset();
});
describe("dbViewerStore", () => {
it("starts empty", () => {
const state = useDbViewerStore.getState();
expect(state.tabs).toEqual([]);
expect(state.activeTabId).toBeNull();
expect(state.changesQueue).toEqual([]);
expect(state.databases).toEqual([]);
expect(state.schemas).toEqual([]);
expect(state.tables).toEqual([]);
expect(state.currentDatabase).toBeNull();
expect(state.currentSchema).toBeNull();
expect(state.objectSearchOpen).toBe(false);
expect(state.selectedObjectType).toBeNull();
expect(state.schemaTreeLoading).toBe(false);
});
it("object search open + selected object type setters", () => {
const { setObjectSearchOpen, setSelectedObjectType } =
useDbViewerStore.getState();
setObjectSearchOpen(true);
expect(useDbViewerStore.getState().objectSearchOpen).toBe(true);
setSelectedObjectType("functions");
expect(useDbViewerStore.getState().selectedObjectType).toBe("functions");
setSelectedObjectType(null);
expect(useDbViewerStore.getState().selectedObjectType).toBeNull();
});
it("requestedView setter stores then clears", () => {
const { setRequestedView } = useDbViewerStore.getState();
setRequestedView("objects");
expect(useDbViewerStore.getState().requestedView).toBe("objects");
setRequestedView(null);
expect(useDbViewerStore.getState().requestedView).toBeNull();
});
it("openTab adds a new tab", () => {
const store = useDbViewerStore.getState();
store.openTab("public", "users");
const state = useDbViewerStore.getState();
expect(state.tabs).toHaveLength(1);
const tab = state.tabs[0];
expect(tab.schema).toBe("public");
expect(tab.table).toBe("users");
expect(tab.page).toBe(1);
expect(state.activeTabId).toBe(tab.id);
});
it("openTab does not duplicate", () => {
const store = useDbViewerStore.getState();
store.openTab("public", "users");
store.openTab("public", "users");
const state = useDbViewerStore.getState();
expect(state.tabs).toHaveLength(1);
});
it("openTab forceNew creates duplicate", () => {
const store = useDbViewerStore.getState();
store.openTab("public", "users");
store.openTab("public", "users");
const tabsAfterTwo = useDbViewerStore.getState().tabs;
expect(tabsAfterTwo).toHaveLength(1);
store.openTab("public", "users", true);
const state = useDbViewerStore.getState();
expect(state.tabs).toHaveLength(2);
});
it("closeTab removes tab and switches activeTabId", () => {
const store = useDbViewerStore.getState();
store.openTab("public", "users");
const tab1Id = useDbViewerStore.getState().tabs[0].id;
store.openTab("public", "posts", true);
store.closeTab(tab1Id);
const state = useDbViewerStore.getState();
expect(state.tabs).toHaveLength(1);
expect(state.activeTabId).toBe(state.tabs[0].id);
});
it("closeTabsForTable closes matching table tabs and keeps others", () => {
useDbViewerStore.getState().openTab("public", "users");
useDbViewerStore.getState().openTab("public", "posts", true);
useDbViewerStore.getState().closeTabsForTable("public", "users");
const tabs = useDbViewerStore.getState().tabs;
expect(tabs).toHaveLength(1);
expect(tabs[0].table).toBe("posts");
});
it("closeTabsForTable fixes the active tab when it is closed", () => {
useDbViewerStore.getState().openTab("public", "users");
const usersId = useDbViewerStore.getState().tabs[0].id;
useDbViewerStore.getState().openTab("public", "posts", true);
useDbViewerStore.getState().setActiveTab(usersId);
useDbViewerStore.getState().closeTabsForTable("public", "users");
const st = useDbViewerStore.getState();
expect(st.tabs).toHaveLength(1);
expect(st.activeTabId).toBe(st.tabs[0].id);
});
it("reorderTab moves a tab and keeps the active tab by id", () => {
useDbViewerStore.getState().openTab("public", "users");
useDbViewerStore.getState().openTab("public", "posts", true);
useDbViewerStore.getState().openTab("public", "comments", true);
const before = useDbViewerStore.getState().tabs;
const activeId = useDbViewerStore.getState().activeTabId;
const activeTable = before.find((t) => t.id === activeId)!.table;
useDbViewerStore.getState().reorderTab(0, 2);
const after = useDbViewerStore.getState().tabs;
expect(after.map((t) => t.table)).toEqual(["posts", "comments", "users"]);
// Active tab must follow the moved item (tracked by id, not index).
expect(useDbViewerStore.getState().activeTabId).toBe(activeId);
expect(after.find((t) => t.id === activeId)!.table).toBe(activeTable);
});
it("reorderTab ignores out-of-range and no-op moves", () => {
useDbViewerStore.getState().openTab("public", "users");
useDbViewerStore.getState().openTab("public", "posts", true);
const reorderTab = useDbViewerStore.getState().reorderTab;
reorderTab(0, 0);
expect(useDbViewerStore.getState().tabs.map((t) => t.table)).toEqual([
"users",
"posts",
]);
reorderTab(-1, 1);
reorderTab(0, 5);
expect(useDbViewerStore.getState().tabs.map((t) => t.table)).toEqual([
"users",
"posts",
]);
});
it("setPage updates pagination", () => {
const store = useDbViewerStore.getState();
store.openTab("public", "users");
const tabId = useDbViewerStore.getState().tabs[0].id;
store.setPage(tabId, 3);
const tab = useDbViewerStore.getState().tabs[0];
expect(tab.page).toBe(3);
});
it("setTabData updates tab data", () => {
const store = useDbViewerStore.getState();
store.openTab("public", "users");
const tabId = useDbViewerStore.getState().tabs[0].id;
// First set loading to true to verify it gets cleared
store.setTabLoading(tabId, true);
const mockData: QueryResult = {
columns: [{name:"id",data_type:"text",is_pk:false,is_fk:false,is_nullable:false,default_value:null,fk_ref:null,editable:true,is_generated:false},{name:"name",data_type:"text",is_pk:false,is_fk:false,is_nullable:false,default_value:null,fk_ref:null,editable:true,is_generated:false}],
rows: [[1, "Alice"]],
total_rows: 1, page: 1, page_size: 50,
};
store.setTabData(tabId, mockData);
const tab = useDbViewerStore.getState().tabs[0];
expect(tab.data).toEqual(mockData);
expect(tab.loading).toBe(false);
expect(tab.error).toBeNull();
});
it("setTabError sets error", () => {
const store = useDbViewerStore.getState();
store.openTab("public", "users");
const tabId = useDbViewerStore.getState().tabs[0].id;
store.setTabError(tabId, "Something went wrong");
const tab = useDbViewerStore.getState().tabs[0];
expect(tab.error).toBe("Something went wrong");
expect(tab.loading).toBe(false);
});
it("addChange appends to queue", () => {
const store = useDbViewerStore.getState();
store.addChange({ type: "insert", sql: "INSERT INTO users (id) VALUES (1)" });
const queue = useDbViewerStore.getState().changesQueue;
expect(queue).toHaveLength(1);
expect(queue[0].status).toBe("pending");
expect(queue[0].id).toBeTruthy();
expect(queue[0].createdAt).toBeGreaterThan(0);
});
it("addChange stages a bulk_insert with columns+rows", () => {
useDbViewerStore.getState().addChange({
type: "bulk_insert", schema: "public", table: "t",
columns: ["a", "b"], rows: [[1, 2]], description: "Import",
} as any);
const q = useDbViewerStore.getState().changesQueue;
expect(q[q.length - 1].type).toBe("bulk_insert");
expect((q[q.length - 1] as any).columns).toEqual(["a", "b"]);
expect((q[q.length - 1] as any).rows).toEqual([[1, 2]]);
});
it("cancelChange marks change as cancelled", () => {
const store = useDbViewerStore.getState();
store.addChange({ type: "insert", sql: "INSERT INTO users (id) VALUES (1)" });
const changeId = useDbViewerStore.getState().changesQueue[0].id;
store.cancelChange(changeId);
const change = useDbViewerStore.getState().changesQueue[0];
expect(change.status).toBe("cancelled");
});
it("markChangeCommitted updates status", () => {
const store = useDbViewerStore.getState();
store.addChange({ type: "insert", sql: "INSERT INTO users (id) VALUES (1)" });
const changeId = useDbViewerStore.getState().changesQueue[0].id;
store.markChangeCommitted(changeId);
const change = useDbViewerStore.getState().changesQueue[0];
expect(change.status).toBe("committed");
});
it("markChangeFailed updates status and records error", () => {
const store = useDbViewerStore.getState();
store.addChange({ type: "insert", sql: "INSERT INTO users (id) VALUES (1)" });
const changeId = useDbViewerStore.getState().changesQueue[0].id;
store.markChangeFailed(changeId, "Constraint violation");
const change = useDbViewerStore.getState().changesQueue[0];
expect(change.status).toBe("failed");
expect(change.error).toBe("Constraint violation");
});
it("removeChange drops the change from the queue", () => {
useDbViewerStore.getState().addChange({ type: "insert", schema: "public", table: "t", newData: { a: 1 }, description: "x" } as any);
const id = useDbViewerStore.getState().changesQueue[0].id;
useDbViewerStore.getState().removeChange(id);
expect(useDbViewerStore.getState().changesQueue).toHaveLength(0);
});
it("updateChange patches a single change in place", () => {
useDbViewerStore.getState().addChange({ type: "insert", schema: "public", table: "t", newData: { a: null }, description: "x" } as any);
const id = useDbViewerStore.getState().changesQueue[0].id;
useDbViewerStore.getState().updateChange(id, { newData: { a: "hello" } });
const item = useDbViewerStore.getState().changesQueue[0];
expect(item.newData).toEqual({ a: "hello" });
expect(item.type).toBe("insert");
expect(item.description).toBe("x");
});
it("clearChanges empties the queue", () => {
useDbViewerStore.getState().addChange({ type: "insert", schema: "public", table: "t", newData: { a: 1 }, description: "x" } as any);
useDbViewerStore.getState().addChange({ type: "delete", schema: "public", table: "t", primaryKey: { id: 1 }, description: "y" } as any);
useDbViewerStore.getState().clearChanges();
expect(useDbViewerStore.getState().changesQueue).toHaveLength(0);
});
it("setSchemaTreeLoading toggles the loading flag", () => {
const store = useDbViewerStore.getState();
expect(useDbViewerStore.getState().schemaTreeLoading).toBe(false);
store.setSchemaTreeLoading(true);
expect(useDbViewerStore.getState().schemaTreeLoading).toBe(true);
store.setSchemaTreeLoading(false);
expect(useDbViewerStore.getState().schemaTreeLoading).toBe(false);
});
it("reset clears all state", () => {
const store = useDbViewerStore.getState();
store.openTab("public", "users");
store.addChange({ type: "insert", sql: "INSERT INTO users (id) VALUES (1)" });
store.populate(["mydb"], ["public"], [{ name: "users", schema: "public", table_type: "TABLE" }]);
store.reset();
const state = useDbViewerStore.getState();
expect(state.tabs).toEqual([]);
expect(state.activeTabId).toBeNull();
expect(state.changesQueue).toEqual([]);
expect(state.databases).toEqual([]);
expect(state.schemas).toEqual([]);
expect(state.tables).toEqual([]);
expect(state.currentDatabase).toBeNull();
expect(state.currentSchema).toBeNull();
expect(state.schemaTreeLoading).toBe(false);
});
it("populate sets databases, schemas, tables", () => {
const tables: TableInfo[] = [
{ name: "users", schema: "public", table_type: "TABLE" },
{ name: "posts", schema: "public", table_type: "TABLE" },
];
const store = useDbViewerStore.getState();
store.populate(["mydb", "testdb"], ["public", "private"], tables);
const state = useDbViewerStore.getState();
expect(state.databases).toEqual(["mydb", "testdb"]);
expect(state.schemas).toEqual(["public", "private"]);
expect(state.tables).toEqual(tables);
});
it("stageCellEdit appends an update QueueItem with primaryKey + old/new data", () => {
const store = useDbViewerStore.getState();
store.openTab("public", "users");
const tabId = useDbViewerStore.getState().activeTabId!;
store.stageCellEdit({
tabId,
schema: "public",
table: "users",
primaryKey: { id: 1 },
oldData: { name: "Alice" },
newData: { name: "Alicia" },
description: "Edit users.name",
});
const q = useDbViewerStore.getState().changesQueue;
expect(q).toHaveLength(1);
expect(q[0].type).toBe("update");
expect(q[0].primaryKey).toEqual({ id: 1 });
expect(q[0].oldData).toEqual({ name: "Alice" });
expect(q[0].newData).toEqual({ name: "Alicia" });
});
it("re-staging the same cell replaces the pending entry (keeps original oldData)", () => {
const store = useDbViewerStore.getState();
store.openTab("public", "users");
const tabId = useDbViewerStore.getState().activeTabId!;
store.stageCellEdit({
tabId, schema: "public", table: "users", primaryKey: { id: 1 },
oldData: { name: "Alice" }, newData: { name: "Alicia" },
description: "Edit users.name",
});
store.stageCellEdit({
tabId, schema: "public", table: "users", primaryKey: { id: 1 },
oldData: { name: "Alice" }, newData: { name: "Alicia 2" },
description: "Edit users.name",
});
const q = useDbViewerStore.getState().changesQueue;
expect(q).toHaveLength(1);
expect(q[0].newData).toEqual({ name: "Alicia 2" });
expect(q[0].oldData).toEqual({ name: "Alice" }); // original DB value preserved
});
it("staging a different cell appends a second entry", () => {
const store = useDbViewerStore.getState();
store.openTab("public", "users");
const tabId = useDbViewerStore.getState().activeTabId!;
store.stageCellEdit({
tabId, schema: "public", table: "users", primaryKey: { id: 1 },
oldData: { name: "Alice" }, newData: { name: "Alicia" },
});
store.stageCellEdit({
tabId, schema: "public", table: "users", primaryKey: { id: 1 },
oldData: { age: 30 }, newData: { age: 31 },
});
expect(useDbViewerStore.getState().changesQueue).toHaveLength(2);
});
it("setIndexes / setConstraints update store slices", () => {
const store = useDbViewerStore.getState();
store.setIndexes([{ name: "idx", schema: "public", table: "t", definition: "", is_unique: true, method: "btree", columns: ["id"], size_bytes: 1, tablespace: null }]);
store.setConstraints([{ name: "ck", schema: "public", table: "t", contype: "CHECK", definition: "", deferrable: false, validated: true, columns: ["x"] }]);
expect(useDbViewerStore.getState().indexes).toHaveLength(1);
expect(useDbViewerStore.getState().constraints).toHaveLength(1);
});
it("reset clears indexes and constraints", () => {
useDbViewerStore.getState().setIndexes([{ name: "x", schema: "s", table: "t", definition: "", is_unique: false, method: "btree", columns: [], size_bytes: null, tablespace: null }]);
useDbViewerStore.getState().reset();
expect(useDbViewerStore.getState().indexes).toBeNull();
});
});
describe("refreshTree", () => {
it("fetches databases/schemas/tables and populates", async () => {
vi.mocked(commands.getDatabases).mockResolvedValue(["mydb"]);
vi.mocked(commands.getSchemas).mockResolvedValue(["public"]);
vi.mocked(commands.getTables).mockResolvedValue([
{ name: "users", schema: "public", table_type: "TABLE" },
] as any);
useDbViewerStore.setState({ currentSchema: "public" });
await useDbViewerStore.getState().refreshTree("c1");
expect(useDbViewerStore.getState().databases).toEqual(["mydb"]);
expect(useDbViewerStore.getState().schemas).toEqual(["public"]);
expect(useDbViewerStore.getState().tables).toHaveLength(1);
expect(commands.getTables).toHaveBeenCalledWith("c1", "public");
});
it("falls back to no schema when currentSchema is null", async () => {
vi.mocked(commands.getDatabases).mockResolvedValue([] as any);
vi.mocked(commands.getSchemas).mockResolvedValue([] as any);
vi.mocked(commands.getTables).mockResolvedValue([] as any);
useDbViewerStore.setState({ currentSchema: null });
await useDbViewerStore.getState().refreshTree("c1");
expect(commands.getTables).toHaveBeenCalledWith("c1", undefined);
});
it("swallows fetch errors", async () => {
vi.mocked(commands.getDatabases).mockResolvedValue([] as any);
vi.mocked(commands.getSchemas).mockRejectedValue(new Error("boom"));
await expect(
useDbViewerStore.getState().refreshTree("c1"),
).resolves.toBeUndefined();
});
});
describe("tabType discriminator", () => {
beforeEach(() => {
useDbViewerStore.getState().reset();
useDbViewerStore.setState({
schemas: ["public"],
currentSchema: "public",
currentDatabase: "mydb",
});
});
it("openQueryTab creates a query-type tab with empty query", () => {
useDbViewerStore.getState().openQueryTab();
const state = useDbViewerStore.getState();
expect(state.tabs).toHaveLength(1);
const tab = state.tabs[0];
expect(tab.tabType).toBe("query");
expect(tab.query).toBe("");
expect(tab.schema).toBe("public");
expect(tab.table).toBe("Query");
expect(state.activeTabId).toBe(tab.id);
});
it("creates sequential query tabs with unique IDs", () => {
const store = useDbViewerStore.getState();
store.openQueryTab();
store.openQueryTab();
store.openQueryTab();
const tabs = useDbViewerStore.getState().tabs;
expect(tabs).toHaveLength(3);
const ids = tabs.map((t) => t.id);
const uniqueIds = new Set(ids);
expect(uniqueIds.size).toBe(3);
tabs.forEach((t) => expect(t.tabType).toBe("query"));
});
it("openTab creates table-type tabs by default (backward compat)", () => {
useDbViewerStore.getState().openTab("public", "users");
const tab = useDbViewerStore.getState().tabs[0];
expect(tab.tabType).toBe("table");
});
});
describe("openObjectTab", () => {
beforeEach(() => useDbViewerStore.getState().reset());
it("opens an object tab carrying the per-type identity + item", () => {
useDbViewerStore.getState().openObjectTab("functions", "public", "add", { name: "add", schema: "public" });
const st = useDbViewerStore.getState();
expect(st.tabs).toHaveLength(1);
expect(st.tabs[0]).toMatchObject({ tabType: "object", objectType: "functions", schema: "public", table: "add" });
expect(st.tabs[0].objectItem).toEqual({ name: "add", schema: "public" });
expect(st.activeTabId).toBe(st.tabs[0].id);
});
it("dedups: re-clicking the same object focuses the existing tab", () => {
useDbViewerStore.getState().openObjectTab("enums", "public", "role", { name: "role" });
const first = useDbViewerStore.getState().tabs[0];
useDbViewerStore.getState().openObjectTab("enums", "public", "role", { name: "role" });
const st = useDbViewerStore.getState();
expect(st.tabs).toHaveLength(1);
expect(st.activeTabId).toBe(first.id);
});
it("does not collide with a table tab of the same name", () => {
useDbViewerStore.getState().openTab("public", "users");
useDbViewerStore.getState().openObjectTab("functions", "public", "users", { name: "users" });
const st = useDbViewerStore.getState();
expect(st.tabs).toHaveLength(2);
expect(st.tabs.find((t) => t.tabType === "object")).toBeDefined();
});
});
describe("openFormTab", () => {
beforeEach(() => useDbViewerStore.getState().reset());
const createOpts = (name: string, title = "Create sequence") => ({
kind: "sequence" as const,
schema: "public",
name,
title,
description: `Create ${name}`,
mode: "create" as const,
params: { schema: "public", name, action: { op: "create" } },
});
it("opens a create form tab carrying kind/params/title/description/mode", () => {
useDbViewerStore.getState().openFormTab(createOpts("my_seq"));
const st = useDbViewerStore.getState();
expect(st.tabs).toHaveLength(1);
const tab = st.tabs[0];
expect(tab.tabType).toBe("objectForm");
// table holds the display title, used as the TabBar label
expect(tab.table).toBe("Create sequence");
expect(tab.schema).toBe("public");
expect(tab.objectType).toBeNull();
expect(tab.form).toBeDefined();
expect(tab.form!.kind).toBe("sequence");
expect(tab.form!.mode).toBe("create");
expect(tab.form!.title).toBe("Create sequence");
expect(tab.form!.description).toBe("Create my_seq");
expect(tab.form!.params).toEqual({
schema: "public",
name: "my_seq",
action: { op: "create" },
});
expect(st.activeTabId).toBe(tab.id);
});
it("dedups create tabs per kind regardless of schema/name and does not clobber params", () => {
useDbViewerStore.getState().openFormTab(createOpts("first_seq"));
const first = useDbViewerStore.getState().tabs[0];
useDbViewerStore.getState().openFormTab(createOpts("second_seq"));
const st = useDbViewerStore.getState();
expect(st.tabs).toHaveLength(1);
expect(st.activeTabId).toBe(first.id);
// the existing tab keeps its original params (name not clobbered)
expect(st.tabs[0].form!.params.name).toBe("first_seq");
});
it("dedups edit tabs by kind+schema+name but a create tab of the same kind coexists", () => {
const editOpts = {
kind: "enum" as const,
schema: "public",
name: "role",
title: "Edit role",
description: "Edit public.role",
mode: "edit" as const,
params: { schema: "public", name: "role", action: { op: "update" } },
};
useDbViewerStore.getState().openFormTab(editOpts);
const first = useDbViewerStore.getState().tabs[0];
useDbViewerStore.getState().openFormTab(editOpts);
let st = useDbViewerStore.getState();
expect(st.tabs).toHaveLength(1);
expect(st.activeTabId).toBe(first.id);
// A create tab of the same kind is a distinct tab.
useDbViewerStore.getState().openFormTab(createOpts("new_enum", "Create enum"));
st = useDbViewerStore.getState();
expect(st.tabs).toHaveLength(2);
expect(st.tabs.every((t) => t.tabType === "objectForm")).toBe(true);
expect(st.tabs.some((t) => t.form!.mode === "edit")).toBe(true);
expect(st.tabs.some((t) => t.form!.mode === "create")).toBe(true);
});
it("does not collide with a table tab (distinct types)", () => {
useDbViewerStore.getState().openTab("public", "users");
useDbViewerStore.getState().openFormTab(createOpts("users_id_seq"));
const st = useDbViewerStore.getState();
expect(st.tabs).toHaveLength(2);
expect(st.tabs[0].tabType).toBe("table");
expect(st.tabs[1].tabType).toBe("objectForm");
});
it("resolves an empty schema to the current schema", () => {
useDbViewerStore.setState({ currentSchema: "utils" });
useDbViewerStore
.getState()
.openFormTab({ ...createOpts("my_seq"), schema: "" });
const tab = useDbViewerStore.getState().tabs[0];
expect(tab.schema).toBe("utils");
// params stay untouched — the form owns them
expect(tab.form!.params.schema).toBe("public");
});
it("falls back to public when no schema and no currentSchema", () => {
useDbViewerStore
.getState()
.openFormTab({ ...createOpts("my_seq"), schema: "" });
const tab = useDbViewerStore.getState().tabs[0];
expect(tab.schema).toBe("public");
});
it("keeps an explicit schema even when currentSchema is set", () => {
useDbViewerStore.setState({ currentSchema: "utils" });
useDbViewerStore
.getState()
.openFormTab({ ...createOpts("my_seq"), schema: "public" });
const tab = useDbViewerStore.getState().tabs[0];
expect(tab.schema).toBe("public");
});
it("updateFormTabParams updates params without touching title/description/mode/kind", () => {
useDbViewerStore.getState().openFormTab(createOpts("my_seq"));
const tabId = useDbViewerStore.getState().tabs[0].id;
const before = useDbViewerStore.getState().tabs[0].form;
expect(before).toBeDefined();
useDbViewerStore
.getState()
.updateFormTabParams(tabId, { schema: "other", name: "renamed", action: { op: "create" } });
const after = useDbViewerStore.getState().tabs[0].form;
expect(after?.params).toEqual({ schema: "other", name: "renamed", action: { op: "create" } });
expect(after?.title).toBe(before?.title);
expect(after?.description).toBe(before?.description);
expect(after?.mode).toBe(before?.mode);
expect(after?.kind).toBe(before?.kind);
});
});
describe("roles slice", () => {
it("setRoles stores roles", () => {
useDbViewerStore.getState().reset();
const r: RoleInfo = {
name: "app", superuser: false, inherit: true, create_db: false, create_role: false,
can_login: true, replication: false, bypass_rls: false, connection_limit: -1,
valid_until: null, memberships: [],
};
useDbViewerStore.getState().setRoles([r]);
expect(useDbViewerStore.getState().roles).toEqual([r]);
});
it("openFormTab accepts kind table and role", () => {
useDbViewerStore.getState().reset();
useDbViewerStore.getState().openFormTab({
kind: "table", schema: "public", name: "", title: "Create Table",
description: "Create Table", mode: "create", params: { schema: "public", name: "", action: { op: "create", columns: [] } },
});
const t = useDbViewerStore.getState().tabs[0];
expect(t.tabType).toBe("objectForm");
expect(t.form?.kind).toBe("table");
// dedup: second create of same kind focuses existing
useDbViewerStore.getState().openFormTab({
kind: "table", schema: "public", name: "", title: "Create Table",
description: "Create Table", mode: "create", params: { schema: "public", name: "", action: { op: "create", columns: [] } },
});
expect(useDbViewerStore.getState().tabs.filter((x) => x.form?.kind === "table").length).toBe(1);
});
});
describe("openQueryTab with an initial script", () => {
it("seeds the tab text and label from the payload", () => {
useDbViewerStore.setState({ tabs: [], activeTabId: null });
useDbViewerStore.getState().openQueryTab({
sql: "SELECT 1;",
label: "student_db.sql",
filePath: "/Users/me/student_db.sql",
});
const tab = useDbViewerStore.getState().tabs.slice(-1)[0]!;
expect(tab.tabType).toBe("query");
expect(tab.query).toBe("SELECT 1;");
expect(tab.table).toBe("student_db.sql");
expect(tab.filePath).toBe("/Users/me/student_db.sql");
});
it("focuses an existing tab for the same file instead of duplicating it", () => {
useDbViewerStore.setState({ tabs: [], activeTabId: null });
const payload = {
sql: "SELECT 1;",
label: "student_db.sql",
filePath: "/Users/me/student_db.sql",
};
useDbViewerStore.getState().openQueryTab(payload);
useDbViewerStore.getState().openQueryTab({ ...payload, sql: "SELECT 2;" });
const queryTabs = useDbViewerStore.getState().tabs.filter((t) => t.tabType === "query");
expect(queryTabs).toHaveLength(1);
// The already-open tab keeps its text: re-opening must not clobber edits.
expect(queryTabs[0].query).toBe("SELECT 1;");
});
it("still opens a plain empty tab when called with no payload", () => {
useDbViewerStore.setState({ tabs: [], activeTabId: null });
useDbViewerStore.getState().openQueryTab();
const tab = useDbViewerStore.getState().tabs.slice(-1)[0]!;
expect(tab.tabType).toBe("query");
expect(tab.query).toBe("");
expect(tab.filePath).toBeUndefined();
});
});