mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
@@ -3519,7 +3519,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<div v-else-if="error" class="flex flex-1 items-center justify-center px-6 text-center text-sm text-destructive">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-else-if="filteredRows.length === 0" class="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
||||
<div v-else-if="filteredRows.length === 0 && !isEventEditor" class="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
||||
{{ t("objects.empty") }}
|
||||
</div>
|
||||
<div v-else class="flex min-h-0 min-w-0 flex-1" :class="{ 'event-editor-layout': isEventEditor }">
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, reactive, ref, type App } from "vue";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import ObjectBrowser from "@/components/objects/ObjectBrowser.vue";
|
||||
import { invalidateObjectBrowserRowsCache } from "@/lib/table/objectBrowserRowsCache";
|
||||
import type { ConnectionConfig, ObjectInfo } from "@/types/database";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
listObjects: vi.fn(),
|
||||
ensureConnected: vi.fn(),
|
||||
editorInstances: 0,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({
|
||||
listObjects: (...args: unknown[]) => mocks.listObjects(...args),
|
||||
listSchemas: vi.fn().mockResolvedValue([]),
|
||||
listObjectStatistics: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
vi.mock("@/stores/connectionStore", () => ({
|
||||
useConnectionStore: () => ({
|
||||
ensureConnected: mocks.ensureConnected,
|
||||
getConfig: () => connection,
|
||||
orderByPinnedTreeNodes: (rows: unknown[]) => rows,
|
||||
}),
|
||||
}));
|
||||
vi.mock("@/stores/queryStore", () => ({ useQueryStore: () => ({}) }));
|
||||
vi.mock("@/stores/settingsStore", () => ({
|
||||
useSettingsStore: () => ({
|
||||
editorSettings: {
|
||||
shortcuts: { refreshData: "F5" },
|
||||
objectBrowserViewMode: "grid",
|
||||
objectBrowserShowCheckbox: false,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key, locale: ref("en-US") }) }));
|
||||
vi.mock("@/i18n", () => ({ default: { install: () => undefined } }));
|
||||
vi.mock("@/composables/useSqlHighlighter", () => ({ useSqlHighlighter: () => ({ highlight: (sql: string) => sql }) }));
|
||||
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) }));
|
||||
vi.mock("vue-virtual-scroller", () => ({ RecycleScroller: { render: () => null } }));
|
||||
vi.mock("@/components/ui/searchable-select", () => ({ SearchableSelect: { render: () => null } }));
|
||||
vi.mock("@/components/ui/ToolbarOverflowMenu.vue", () => ({ default: { render: () => null } }));
|
||||
vi.mock("@/components/ui/CustomContextMenu.vue", () => ({ default: { render: () => null } }));
|
||||
vi.mock("@/components/editor/QueryEditor.vue", () => ({ default: { render: () => null } }));
|
||||
vi.mock("@/components/editor/DangerConfirmDialog.vue", () => ({ default: { render: () => null } }));
|
||||
vi.mock("@/components/objects/ProcedureExecutionDialog.vue", () => ({ default: { render: () => null } }));
|
||||
vi.mock("@/components/objects/CustomTypeInfoPanel.vue", () => ({ default: { render: () => null } }));
|
||||
vi.mock("@/components/export/XlsxHeaderDialog.vue", () => ({ default: { render: () => null } }));
|
||||
vi.mock("@/components/objects/MySqlEventEditor.vue", () => ({
|
||||
default: defineComponent({
|
||||
props: { name: String },
|
||||
emits: ["close", "saved"],
|
||||
setup(props, { emit }) {
|
||||
const draft = ref("");
|
||||
const instance = ++mocks.editorInstances;
|
||||
return () =>
|
||||
h("section", { "data-event-editor": props.name ?? "create", "data-instance": instance }, [
|
||||
h("input", {
|
||||
"data-event-draft": "",
|
||||
value: draft.value,
|
||||
onInput: (event: Event) => (draft.value = (event.target as HTMLInputElement).value),
|
||||
}),
|
||||
h("button", { "data-close-event": "", onClick: () => emit("close") }, "close"),
|
||||
]);
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const connection = {
|
||||
id: "mysql-events",
|
||||
name: "MySQL",
|
||||
db_type: "mysql",
|
||||
database: "app",
|
||||
driver_profile: null,
|
||||
url_params: null,
|
||||
transport_layers: [],
|
||||
} as unknown as ConnectionConfig;
|
||||
const mountedApps: Array<{ app: App; host: HTMLElement }> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.listObjects.mockResolvedValue([]);
|
||||
mocks.ensureConnected.mockResolvedValue(undefined);
|
||||
mocks.editorInstances = 0;
|
||||
invalidateObjectBrowserRowsCache({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const { app, host } of mountedApps.splice(0)) {
|
||||
app.unmount();
|
||||
host.remove();
|
||||
}
|
||||
invalidateObjectBrowserRowsCache({});
|
||||
});
|
||||
|
||||
async function mountBrowser(overrides: Partial<InstanceType<typeof ObjectBrowser>["$props"]> = {}) {
|
||||
const props = reactive({ connection, database: "app", initialEventCreateRequestId: 1 as number | undefined, ...overrides });
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const app = createApp({ setup: () => () => h(ObjectBrowser, props) });
|
||||
mountedApps.push({ app, host });
|
||||
app.mount(host);
|
||||
await vi.waitFor(() => expect(mocks.listObjects).toHaveBeenCalledOnce());
|
||||
await nextTick();
|
||||
return { host, props };
|
||||
}
|
||||
|
||||
describe("ObjectBrowser event editor rendering", () => {
|
||||
it("renders the first event CREATE editor when the database has no objects", async () => {
|
||||
const { host } = await mountBrowser();
|
||||
|
||||
expect(host.querySelector('[data-event-editor="create"]')).not.toBeNull();
|
||||
expect(host.textContent).not.toContain("objects.empty");
|
||||
});
|
||||
|
||||
it("keeps the ordinary empty state when no editor was requested", async () => {
|
||||
const { host } = await mountBrowser({ initialEventCreateRequestId: undefined });
|
||||
|
||||
expect(host.textContent).toContain("objects.empty");
|
||||
expect(host.querySelector("[data-event-editor]")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns to the empty state after closing the CREATE editor", async () => {
|
||||
const { host } = await mountBrowser();
|
||||
const closeButton = host.querySelector<HTMLButtonElement>("[data-close-event]");
|
||||
expect(closeButton).not.toBeNull();
|
||||
closeButton!.click();
|
||||
await nextTick();
|
||||
|
||||
expect(host.querySelector("[data-event-editor]")).toBeNull();
|
||||
expect(host.textContent).toContain("objects.empty");
|
||||
});
|
||||
|
||||
it("remounts a fresh CREATE editor for another request on the empty tab", async () => {
|
||||
const { host, props } = await mountBrowser();
|
||||
const draft = host.querySelector<HTMLInputElement>("[data-event-draft]");
|
||||
expect(draft).not.toBeNull();
|
||||
draft!.value = "unsaved event";
|
||||
draft!.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
await nextTick();
|
||||
const firstInstance = host.querySelector("[data-event-editor]")?.getAttribute("data-instance");
|
||||
|
||||
props.initialEventCreateRequestId = 2;
|
||||
await nextTick();
|
||||
|
||||
expect(host.querySelector("[data-event-editor]")?.getAttribute("data-instance")).not.toBe(firstInstance);
|
||||
expect(host.querySelector<HTMLInputElement>("[data-event-draft]")?.value).toBe("");
|
||||
expect(mocks.listObjects).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not hide CREATE when an existing event is filtered out", async () => {
|
||||
mocks.listObjects.mockResolvedValue([{ name: "existing_event", schema: "app", object_type: "EVENT" }]);
|
||||
const { host } = await mountBrowser({ initialSearchQuery: "no-match" });
|
||||
|
||||
expect(host.querySelector('[data-event-editor="create"]')).not.toBeNull();
|
||||
expect(host.textContent).not.toContain("objects.empty");
|
||||
});
|
||||
|
||||
it("still opens an existing event in edit mode", async () => {
|
||||
mocks.listObjects.mockResolvedValue([{ name: "existing_event", schema: "app", object_type: "EVENT" }]);
|
||||
const { host } = await mountBrowser({ initialEventCreateRequestId: undefined, initialEventName: "existing_event", initialEventOpenRequestId: 1 });
|
||||
|
||||
expect(host.querySelector('[data-event-editor="existing_event"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it("keeps loading visible until the initial object request completes", async () => {
|
||||
let resolveObjects!: (objects: ObjectInfo[]) => void;
|
||||
mocks.listObjects.mockImplementation(() => new Promise<ObjectInfo[]>((resolve) => (resolveObjects = resolve)));
|
||||
const { host } = await mountBrowser();
|
||||
|
||||
expect(host.textContent).toContain("objects.loading");
|
||||
expect(host.querySelector("[data-event-editor]")).toBeNull();
|
||||
|
||||
resolveObjects([]);
|
||||
await vi.waitFor(() => expect(host.querySelector('[data-event-editor="create"]')).not.toBeNull());
|
||||
});
|
||||
|
||||
it("preserves an object-load failure instead of hiding it with the editor", async () => {
|
||||
mocks.listObjects.mockRejectedValue(new Error("objects unavailable"));
|
||||
const { host } = await mountBrowser();
|
||||
|
||||
expect(host.textContent).toContain("objects unavailable");
|
||||
expect(host.querySelector("[data-event-editor]")).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user