Files
gridline/desktop/src/components/db-viewer/DbViewerScreen.test.tsx
T
adrianbonpin 5607773299
Release / windows (push) Successful in 17m5s
Release / linux (push) Successful in 24m10s
0.8.0: MariaDB + PlanetScale, TimescaleDB, multi-result sets, streaming export, schema diff (#27)
2026-09-11 17:20:41 +00:00

1011 lines
36 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
import {
DbViewerScreen,
deriveStagedValues,
derivePendingCellKeys,
pickDisplayColumn,
} from "./DbViewerScreen";
import { useDbViewerStore } from "../../stores/dbViewerStore";
import { useUiStore } from "../../stores/uiStore";
import { useConnectionStore } from "../../stores/connectionStore";
import * as commands from "../../lib/commands";
import type { Connection } from "../../lib/types";
vi.mock("../../hooks/useDbConnection", () => ({
useDbConnection: (_connectionId: string) => ({
connectionError: null,
connect: vi.fn(),
passwordPromptOpen: false,
submitPassword: vi.fn(),
cancelPassword: vi.fn(),
}),
}));
vi.mock("@tanstack/react-virtual", () => ({
useVirtualizer: ({ count }: any) => ({
getVirtualItems: () =>
count > 0
? Array.from({ length: count }, (_, i) => ({
key: i,
index: i,
start: i * 36,
size: 36,
}))
: [],
getTotalSize: () => count * 36,
measureElement: () => {},
}),
}));
const { registeredActions } = vi.hoisted(() => ({
registeredActions: [] as Array<{ run: () => void }>,
}));
// monaco-editor's global font re-measure — stub so jsdom stays light
vi.mock("monaco-editor", () => ({
editor: { remeasureFonts: vi.fn() },
}));
vi.mock("@monaco-editor/react", async () => {
const { useEffect } = await import("react");
return {
default: ({ value, onChange, onMount }: any) => {
useEffect(() => {
onMount?.({
addAction: (action: any) => registeredActions.push(action),
getValue: () => value,
setValue: (v: string) => onChange?.(v),
focus: () => {},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div data-testid="monaco-editor">
<textarea
data-testid="monaco-textarea"
value={value}
onChange={(e) => onChange?.(e.target.value)}
/>
</div>
);
},
};
});
const mockQueryResult = {
columns: [
{
name: "id",
data_type: "integer",
is_nullable: false,
is_pk: true,
is_fk: false,
fk_ref: null,
default_value: null,
editable: true,
is_generated: false,
},
],
rows: [[1]],
total_rows: 1,
page: 1,
page_size: 50,
execution_time_ms: 42,
};
describe("DbViewerScreen", () => {
beforeEach(() => {
useDbViewerStore.getState().reset();
useConnectionStore.setState({ connections: [] });
useDbViewerStore.setState({
databases: ["mydb"],
schemas: ["public"],
tables: [
{ name: "users", schema: "public", table_type: "TABLE" },
],
currentDatabase: "mydb",
currentSchema: "public",
});
vi.resetAllMocks();
});
it("renders the sidebar", () => {
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
expect(screen.getByLabelText(/home/i)).toBeInTheDocument();
});
it("full flow: editing a cell shows the staged value + pending dot in the grid", async () => {
const store = useDbViewerStore.getState();
store.openTab("public", "users");
const tabId = useDbViewerStore.getState().activeTabId!;
store.setTabData(tabId, {
columns: [
{ name: "id", data_type: "integer", is_nullable: false, is_pk: true, is_fk: false, fk_ref: null, default_value: null, editable: false, is_generated: false },
{ name: "name", data_type: "text", is_nullable: true, is_pk: false, is_fk: false, fk_ref: null, default_value: null, editable: true, is_generated: false },
],
rows: [[1, "Alice"]],
total_rows: 1,
page: 1,
page_size: 50,
} as any);
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
const cell = await waitFor(() => screen.getByText("Alice"));
fireEvent.click(cell);
fireEvent.keyDown(cell, { key: "Enter" });
// the editor's textarea is the last textbox (toolbar filter input is first)
const textboxes = screen.getAllByRole("textbox");
const input = textboxes[textboxes.length - 1]!;
fireEvent.change(input, { target: { value: "Alicia" } });
fireEvent.keyDown(input, { key: "Enter" });
// staged change carries the correct table (was the root-cause bug)
const staged = useDbViewerStore.getState().changesQueue[0];
expect(staged?.table).toBe("users");
expect(staged?.schema).toBe("public");
// grid cell shows the optimistic value + the pending outline (2nd match is the queue panel diff)
await waitFor(() => {
expect(screen.getAllByText("Alicia").length).toBeGreaterThanOrEqual(2);
});
expect(screen.getByTestId("pending-cell")).toBeInTheDocument();
// committing the change clears the pending outline but keeps the value until refetch
act(() => {
useDbViewerStore
.getState()
.markChangeCommitted(
useDbViewerStore.getState().changesQueue[0].id,
);
});
await waitFor(() => {
expect(screen.queryByTestId("pending-cell")).toBeNull();
});
expect(screen.getAllByText("Alicia").length).toBeGreaterThanOrEqual(2);
// clearing the queue clears the optimistic display
act(() => {
useDbViewerStore.getState().clearChanges();
});
await waitFor(() => {
expect(screen.getByText("Alice")).toBeInTheDocument();
});
expect(screen.queryByText("Alicia")).toBeNull();
});
it("deriveStagedValues maps queue updates to optimistic cell values", () => {
const queue = [
{
id: "ch-1", type: "update" as const, sql: "", schema: "public", table: "users",
primaryKey: { id: 1 }, oldData: { name: "Alice" }, newData: { name: "Alicia" },
status: "pending" as const, createdAt: 0,
},
];
const rows: unknown[][] = [[1, "Alice"], [2, "Bob"]];
const loc = (r: unknown[]) => ({ id: r[0] });
expect(deriveStagedValues(queue as any, "public", "users", rows, loc)).toEqual({
"0:name": "Alicia",
});
});
it("deriveStagedValues ignores failed/other-table changes and handles NULL", () => {
const queue = [
{
id: "ch-1", type: "update" as const, sql: "", schema: "public", table: "users",
primaryKey: { id: 1 }, oldData: { name: "Alice" }, newData: { name: null },
status: "pending" as const, createdAt: 0,
},
{
id: "ch-2", type: "update" as const, sql: "", schema: "public", table: "orders",
primaryKey: { id: 1 }, oldData: { x: 1 }, newData: { x: 2 },
status: "pending" as const, createdAt: 0,
},
{
id: "ch-3", type: "update" as const, sql: "", schema: "public", table: "users",
primaryKey: { id: 1 }, oldData: { name: "Alice" }, newData: { name: "X" },
status: "failed" as const, error: "boom", createdAt: 0,
},
];
const rows: unknown[][] = [[1, "Alice"]];
const loc = (r: unknown[]) => ({ id: r[0] });
expect(deriveStagedValues(queue as any, "public", "users", rows, loc)).toEqual({
"0:name": null,
});
});
it("derivePendingCellKeys only includes pending updates (dot clears on commit)", () => {
const queue = [
{
id: "ch-1", type: "update" as const, sql: "", schema: "public", table: "users",
primaryKey: { id: 1 }, oldData: { name: "Alice" }, newData: { name: "Alicia" },
status: "pending" as const, createdAt: 0,
},
{
id: "ch-2", type: "update" as const, sql: "", schema: "public", table: "users",
primaryKey: { id: 2 }, oldData: { name: "Bob" }, newData: { name: "Bobby" },
status: "committed" as const, createdAt: 0,
},
];
const rows: unknown[][] = [[1, "Alice"], [2, "Bob"]];
const loc = (r: unknown[]) => ({ id: r[0] });
expect(derivePendingCellKeys(queue as any, "public", "users", rows, loc)).toEqual({
"0:name": true,
});
});
it("renders the New Query button", () => {
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
expect(screen.getByRole("button", { name: /new query/i })).toBeInTheDocument();
});
it("opens a query tab when New Query is clicked", () => {
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /new query/i }));
expect(screen.getByRole("tab", { name: "Query" })).toBeInTheDocument();
});
it("renders the query editor inside a query tab", async () => {
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /new query/i }));
await waitFor(() => {
expect(screen.getByTestId("monaco-editor")).toBeInTheDocument();
});
expect(screen.getByRole("button", { name: /run query/i })).toBeInTheDocument();
});
it("executes a non-destructive query when Run is clicked", async () => {
const executeQuery = vi
.spyOn(commands, "executeQueryMulti")
.mockResolvedValue({ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 } as any);
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /new query/i }));
const textarea = await waitFor(() =>
screen.getByTestId("monaco-textarea"),
);
fireEvent.change(textarea, { target: { value: "SELECT 1" } });
fireEvent.click(screen.getByRole("button", { name: /run query/i }));
await waitFor(() =>
expect(executeQuery).toHaveBeenCalledWith("c1", "SELECT 1", 1, 50),
);
// query variant toolbar shows the execution time from the result
await waitFor(() => expect(screen.getByText("42.00ms")).toBeInTheDocument());
expect(screen.getByLabelText(/execution time/i)).toBeInTheDocument();
});
it("shows a destructive-query confirmation dialog and executes on confirm", async () => {
const executeQuery = vi
.spyOn(commands, "executeQueryMulti")
.mockResolvedValue({
result_sets: [{ columns: [], rows: [], total_rows: 0, page: 1, page_size: 50 }],
notices: [],
execution_time_ms: 42,
} as any);
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /new query/i }));
const textarea = await waitFor(() =>
screen.getByTestId("monaco-textarea"),
);
fireEvent.change(textarea, {
target: { value: "DELETE FROM users" },
});
fireEvent.click(screen.getByRole("button", { name: /run query/i }));
await waitFor(() => {
expect(screen.getByText("Destructive Query")).toBeInTheDocument();
});
expect(executeQuery).not.toHaveBeenCalled();
fireEvent.click(screen.getByText("Execute"));
await waitFor(() =>
expect(executeQuery).toHaveBeenCalledWith(
"c1",
"DELETE FROM users",
1,
50,
),
);
});
it("refreshes the schema tree after a DDL query runs", async () => {
vi.spyOn(commands, "executeQueryMulti").mockResolvedValue(
{ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 } as any,
);
const getSchemas = vi
.spyOn(commands, "getSchemas")
.mockResolvedValue(["public"]);
vi.spyOn(commands, "getDatabases").mockResolvedValue(["mydb"]);
vi.spyOn(commands, "getTables").mockResolvedValue([] as any);
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /new query/i }));
const textarea = await waitFor(() =>
screen.getByTestId("monaco-textarea"),
);
fireEvent.change(textarea, {
target: { value: "CREATE TABLE users_new (id INTEGER)" },
});
fireEvent.click(screen.getByRole("button", { name: /run query/i }));
// CREATE is destructive -> confirm dialog appears -> click Execute
await waitFor(() =>
screen.getByRole("button", { name: /execute/i }),
);
fireEvent.click(screen.getByRole("button", { name: /execute/i }));
await waitFor(() => expect(getSchemas).toHaveBeenCalledWith("c1"));
});
it("does not refresh the schema tree after a SELECT", async () => {
vi.spyOn(commands, "executeQueryMulti").mockResolvedValue(
{ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 } as any,
);
const getSchemas = vi
.spyOn(commands, "getSchemas")
.mockResolvedValue(["public"]);
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /new query/i }));
const textarea = await waitFor(() =>
screen.getByTestId("monaco-textarea"),
);
fireEvent.change(textarea, { target: { value: "SELECT 1" } });
fireEvent.click(screen.getByRole("button", { name: /run query/i }));
await waitFor(() => expect(commands.executeQueryMulti).toHaveBeenCalled());
expect(getSchemas).not.toHaveBeenCalled();
});
it("formats the query SQL when Auto format is clicked", async () => {
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /new query/i }));
const textarea = await waitFor(() =>
screen.getByTestId("monaco-textarea"),
);
fireEvent.change(textarea, {
target: { value: "select * from users where id = 1" },
});
fireEvent.click(screen.getByRole("button", { name: /auto format/i }));
await waitFor(() => {
expect((textarea as HTMLTextAreaElement).value).toMatch(/\n/);
});
});
it("runs the current query when the Cmd+Enter action fires", async () => {
const executeQuery = vi
.spyOn(commands, "executeQueryMulti")
.mockResolvedValue({ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 } as any);
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
registeredActions.length = 0;
fireEvent.click(screen.getByRole("button", { name: /new query/i }));
const textarea = await waitFor(() =>
screen.getByTestId("monaco-textarea"),
);
fireEvent.change(textarea, {
target: { value: "SELECT 42" },
});
expect(registeredActions).toHaveLength(1);
registeredActions[0].run();
await waitFor(() =>
expect(executeQuery).toHaveBeenCalledWith("c1", "SELECT 42", 1, 50),
);
});
it("shows the pulse while a query is running and hides it after", async () => {
let resolveRun!: (v: unknown) => void;
const pending = new Promise<unknown>((r) => {
resolveRun = r;
});
vi.spyOn(commands, "executeQueryMulti").mockReturnValue(pending as any);
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /new query/i }));
const textarea = await waitFor(() =>
screen.getByTestId("monaco-textarea"),
);
fireEvent.change(textarea, { target: { value: "SELECT 1" } });
fireEvent.click(screen.getByRole("button", { name: /run query/i }));
await waitFor(() =>
expect(screen.getByTestId("query-run-pulse")).toBeInTheDocument(),
);
resolveRun({ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 });
await waitFor(() =>
expect(screen.queryByTestId("query-run-pulse")).toBeNull(),
);
});
it("collapses and re-expands the query results via the caret", async () => {
const executeQuery = vi
.spyOn(commands, "executeQueryMulti")
.mockResolvedValue({ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 } as any);
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /new query/i }));
const textarea = await waitFor(() =>
screen.getByTestId("monaco-textarea"),
);
fireEvent.change(textarea, { target: { value: "SELECT 1" } });
fireEvent.click(screen.getByRole("button", { name: /run query/i }));
await waitFor(() => expect(screen.getByText("42.00ms")).toBeInTheDocument());
expect(screen.getByTestId("query-results")).toBeInTheDocument();
fireEvent.click(screen.getByLabelText(/hide results/i));
expect(screen.queryByTestId("query-results")).toBeNull();
expect(screen.queryByText("42.00ms")).toBeNull();
fireEvent.click(screen.getByLabelText(/show results/i));
expect(screen.getByTestId("query-results")).toBeInTheDocument();
expect(executeQuery).toHaveBeenCalledTimes(1);
});
it("resizes the results panel with a drag handle, clamped to min/max", async () => {
vi.spyOn(commands, "executeQueryMulti").mockResolvedValue(
{ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 } as any,
);
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /new query/i }));
const textarea = await waitFor(() =>
screen.getByTestId("monaco-textarea"),
);
fireEvent.change(textarea, { target: { value: "SELECT 1" } });
fireEvent.click(screen.getByRole("button", { name: /run query/i }));
await waitFor(() => expect(screen.getByText("42.00ms")).toBeInTheDocument());
const results = screen.getByTestId("query-results");
const initial = parseFloat(results.style.height);
const handle = screen.getByTestId("query-results-resize");
// Drag up: results grow
fireEvent.mouseDown(handle, { clientY: 200 });
fireEvent.mouseMove(document, { clientY: 100 });
fireEvent.mouseUp(document);
await waitFor(() =>
expect(parseFloat(results.style.height)).toBeGreaterThan(initial),
);
// Drag far down: clamps to the 120px minimum
fireEvent.mouseDown(handle, { clientY: 200 });
fireEvent.mouseMove(document, { clientY: 5000 });
fireEvent.mouseUp(document);
await waitFor(() => expect(parseFloat(results.style.height)).toBe(120));
});
it("re-fetches the active table and shows the refresh indicator when refresh is clicked", async () => {
let resolveFetch!: (v: unknown) => void;
const pendingFetch = new Promise<unknown>((r) => {
resolveFetch = r;
});
const getTableData = vi
.spyOn(commands, "getTableData")
.mockReturnValue(pendingFetch as any);
useDbViewerStore.setState({
tabs: [
{
id: "tab-1",
schema: "public",
table: "users",
page: 1,
pageSize: 50,
loading: false,
error: null,
data: mockQueryResult,
filterRules: [],
sortRules: [],
hiddenColumns: [],
smartSortApplied: true,
tabType: "table",
},
],
activeTabId: "tab-1",
});
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
// Tab already has data and is not loading → no fetch on mount
expect(getTableData).not.toHaveBeenCalled();
fireEvent.click(screen.getByLabelText(/refresh table/i));
// Refetch triggered for the active tab
await waitFor(() => expect(getTableData).toHaveBeenCalledTimes(1));
// Indicator visible while the fetch is in flight
await waitFor(() =>
expect(screen.getByTestId("refresh-pulse")).toBeInTheDocument(),
);
act(() => {
resolveFetch({ ...mockQueryResult });
});
await waitFor(() =>
expect(
screen.queryByTestId("refresh-pulse"),
).not.toBeInTheDocument(),
);
});
it("shows the table toolbar while a table tab is still loading its first data", () => {
useDbViewerStore.setState({
tabs: [
{
id: "tab-loading",
schema: "public",
table: "users",
page: 1,
pageSize: 50,
loading: true,
error: null,
data: null, // first fetch still in flight
filterRules: [],
sortRules: [],
hiddenColumns: [],
smartSortApplied: false,
tabType: "table",
},
],
activeTabId: "tab-loading",
});
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
// Toolbar must be visible immediately while data is still loading,
// so the user sees the loading state instead of an empty pane.
expect(
screen.getByLabelText(/refresh table/i),
).toBeInTheDocument();
expect(
screen.getByLabelText(/column filters/i),
).toBeInTheDocument();
});
it("disables Insert Row for a materialized-view tab", async () => {
useDbViewerStore.setState({
tables: [
{ name: "mat_users", schema: "public", table_type: "MATERIALIZED VIEW" },
],
tabs: [
{
id: "tab-mv",
schema: "public",
table: "mat_users",
page: 1,
pageSize: 50,
loading: false,
error: null,
data: mockQueryResult,
filterRules: [],
sortRules: [],
hiddenColumns: [],
smartSortApplied: true,
tabType: "table",
},
],
activeTabId: "tab-mv",
});
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
await waitFor(() =>
expect(screen.queryByLabelText(/insert row/i)).toBeNull(),
);
});
it("refetches the active tab after a successful Commit All", async () => {
useUiStore.setState({ activeConnectionId: "c1" });
vi.spyOn(commands, "executeChange").mockResolvedValue(undefined);
const getTableData = vi
.spyOn(commands, "getTableData")
.mockResolvedValue({
columns: mockQueryResult.columns,
rows: [[2]],
total_rows: 1,
page: 1,
page_size: 50,
} as any);
useDbViewerStore.setState({
tabs: [
{
id: "tab-1",
schema: "public",
table: "users",
page: 1,
pageSize: 50,
loading: false,
error: null,
data: mockQueryResult,
filterRules: [],
sortRules: [],
hiddenColumns: [],
smartSortApplied: true,
tabType: "table",
},
],
activeTabId: "tab-1",
changesQueue: [],
changesPanelExpanded: true,
});
useDbViewerStore.getState().addChange({
type: "insert",
schema: "public",
table: "users",
newData: { id: 2, name: "Alice" },
description: "Insert row into users",
});
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /commit all/i }));
await waitFor(() => expect(getTableData).toHaveBeenCalledTimes(1));
});
it("fetches enum labels and FK reference rows for the active table tab", async () => {
const getEnums = vi
.spyOn(commands, "getEnums")
.mockResolvedValue([
{
name: "user_role",
schema: "public",
labels: ["admin", "user"],
},
]);
const getTableData = vi
.spyOn(commands, "getTableData")
.mockResolvedValue({
columns: [
{
name: "id",
data_type: "integer",
is_nullable: false,
is_pk: true,
is_fk: false,
fk_ref: null,
default_value: null,
editable: false,
is_generated: false,
},
],
rows: [[1], [2]],
total_rows: 2,
page: 1,
page_size: 50,
} as any);
useDbViewerStore.setState({
tabs: [
{
id: "tab-1",
schema: "public",
table: "users",
page: 1,
pageSize: 50,
loading: false,
error: null,
data: {
columns: [
{
name: "id",
data_type: "integer",
is_nullable: false,
is_pk: true,
is_fk: false,
fk_ref: null,
default_value: null,
editable: false,
is_generated: false,
},
{
name: "user_id",
data_type: "integer",
is_nullable: true,
is_pk: false,
is_fk: true,
fk_ref: ["users", "id"],
default_value: null,
editable: true,
is_generated: false,
},
{
name: "role",
data_type: "user_role",
is_nullable: true,
is_pk: false,
is_fk: false,
fk_ref: null,
default_value: null,
editable: true,
is_generated: false,
},
],
rows: [[1, 2, "admin"]],
total_rows: 1,
page: 1,
page_size: 50,
} as any,
filterRules: [],
sortRules: [],
hiddenColumns: [],
smartSortApplied: true,
tabType: "table",
},
],
activeTabId: "tab-1",
});
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
// Enum labels are fetched for the tab's schema (cached per schema).
await waitFor(() =>
expect(getEnums).toHaveBeenCalledWith("c1", "public"),
);
// FK reference rows are fetched from the referenced table (page 1, 50).
await waitFor(() =>
expect(getTableData).toHaveBeenCalledWith(
"c1",
"public",
"users",
1,
50,
),
);
});
it("pickDisplayColumn prefers name-like columns over the ref column", () => {
const cols = [
{ name: "id", data_type: "integer" },
{ name: "email", data_type: "text" },
{ name: "name", data_type: "text" },
];
expect(pickDisplayColumn(cols, "id")).toBe("name");
expect(pickDisplayColumn(cols, "id", "email")).toBe("email");
});
it("pickDisplayColumn falls back to the ref column when nothing is name-like", () => {
const cols = [{ name: "id", data_type: "integer" }];
expect(pickDisplayColumn(cols, "id")).toBe("id");
});
it("shows a Redis unsupported state when a redis connection is active", () => {
useConnectionStore.setState({
connections: [
{
id: "redis-1",
name: "Redis",
db_type: "redis",
host: "localhost",
port: 6379,
username: null,
folder_id: null,
keychain_ref: null,
tag_ids: [],
favorite: false,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
} as Connection,
],
});
render(
<DbViewerScreen
connectionId="redis-1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
expect(
screen.getByText(/redis browsing isn't supported/i),
).toBeInTheDocument();
});
it("objects view renders the sidebar + the tabbed workspace (New query + Changes)", () => {
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /objects/i }));
expect(
screen.getByRole("button", { name: /new query/i }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Changes queue" }),
).toBeInTheDocument();
});
it("objects view renders ObjectDetail for an open object tab", () => {
useDbViewerStore.getState().openObjectTab("enums", "public", "role", { name: "role", schema: "public", labels: ["admin"] });
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /objects/i }));
expect(screen.getByText("admin")).toBeInTheDocument();
});
it("renders an objectForm tab with the Visual/SQL toggle in the objects view", async () => {
vi.spyOn(commands, "executeQueryMulti").mockResolvedValue({ result_sets: [{ columns: [], rows: [], total_rows: 0, page: 1, page_size: 50 }], notices: [], execution_time_ms: 42 } as any);
vi.spyOn(commands, "getSchemas").mockResolvedValue(["public"]);
vi.spyOn(commands, "getDatabases").mockResolvedValue(["mydb"]);
vi.spyOn(commands, "getTables").mockResolvedValue([] as any);
useDbViewerStore.getState().openFormTab({
kind: "sequence",
schema: "public",
name: "",
title: "Create sequence",
description: "Create sequence",
mode: "create",
params: { schema: "public", name: "", action: { op: "create" } },
});
render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /objects/i }));
await waitFor(() => {
expect(screen.getByPlaceholderText("Sequence name")).toBeInTheDocument();
});
expect(screen.getByRole("button", { name: "Visual" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "SQL" })).toBeInTheDocument();
});
it("guards the Objects view for MySQL (capability false)", () => {
useConnectionStore.setState({
connections: [
{
id: "c1",
name: "Postgres",
db_type: "postgresql",
host: "localhost",
port: 5432,
username: "user",
folder_id: null,
keychain_ref: null,
tag_ids: [],
favorite: false,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
} as Connection,
{
id: "mysql-1",
name: "MySQL",
db_type: "mysql",
host: "localhost",
port: 3306,
username: "user",
folder_id: null,
keychain_ref: null,
tag_ids: [],
favorite: false,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
} as Connection,
],
});
const { rerender } = render(
<DbViewerScreen
connectionId="c1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
fireEvent.click(screen.getByLabelText(/objects/i));
rerender(
<DbViewerScreen
connectionId="mysql-1"
onHome={() => {}}
onSettings={() => {}}
/>,
);
expect(
screen.getByText(/objects is unsupported for mysql/i),
).toBeInTheDocument();
});
});