Files
dbx/apps/desktop/src/components/transfer/__tests__/DataTransferDialog.spec.ts
T

120 lines
6.8 KiB
TypeScript
Vendored

import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../DataTransferDialog.vue", import.meta.url), "utf8");
describe("DataTransferDialog layout", () => {
it("keeps only the main dialog resizable within safe viewport bounds", () => {
const dialogContentTags = dialogSource.match(/<DialogContent\b[^>]*>/g) ?? [];
const resizableDialogs = dialogContentTags.filter((tag) => tag.includes(" resize"));
expect(resizableDialogs).toHaveLength(1);
expect(resizableDialogs[0]).toContain('class="dbx-transfer-dialog sm:max-w-[1120px] max-h-[80vh] flex flex-col overflow-hidden resize"');
expect(resizableDialogs[0]).toContain(':style="transferDialogStyle"');
expect(dialogSource).toContain('width: "min(1120px, calc(100vw - 2rem))"');
expect(dialogSource).toContain('height: "min(80vh, calc(var(--dbx-viewport-height) - 2rem))"');
expect(dialogSource).toContain('minWidth: "min(780px, calc(100vw - 2rem))"');
expect(dialogSource).toContain('minHeight: "min(480px, calc(var(--dbx-viewport-height) - 2rem))"');
expect(dialogSource).toContain('maxWidth: "calc(100vw - 2rem)"');
expect(dialogSource).toContain('maxHeight: "calc(var(--dbx-viewport-height) - 2rem)"');
});
it("keeps source and target side by side while the dialog changes size", () => {
expect(dialogSource).toContain('class="grid grid-cols-[1fr_auto_1fr] gap-4 items-start"');
});
it("keeps the header and footer outside the shrinking content region", () => {
expect(dialogSource).toContain('<DialogHeader class="shrink-0">');
expect(dialogSource).toContain('<DialogFooter class="shrink-0">');
expect(dialogSource).toContain('class="min-h-0 flex-1 overflow-y-auto pl-4 pr-1 scrollbar-thin"');
});
it("allows layout scrolling on short viewports to prevent content clipping", () => {
expect(dialogSource).toContain('class="flex flex-col gap-5 py-3"');
expect(dialogSource).toContain('class="flex min-h-0 flex-col gap-2"');
});
it("keeps long table lists independently scrollable", () => {
expect(dialogSource).toContain('class="min-h-0 flex-1"');
});
});
describe("DataTransferDialog transfer prefill", () => {
it("accepts source, target, schema, and selected-table prefills", () => {
expect(dialogSource).toContain("prefillSchema?: string;");
expect(dialogSource).toContain("prefillTables?: string[];");
expect(dialogSource).toContain("prefillTargetConnectionId?: string;");
expect(dialogSource).toContain("prefillTargetDatabase?: string;");
expect(dialogSource).toContain("prefillTargetSchema?: string;");
});
it("keeps only copied tables selected after loading the source list", () => {
expect(dialogSource).toContain("function applyPendingTableSelection()");
expect(dialogSource).toContain("new Set(tables.filter((table) => pending.includes(table)))");
});
it("sends content and objects in the transfer request", () => {
expect(dialogSource).toContain("buildTransferObjectSelections(selectedObjects.value, treeDisabledGroups.value)");
expect(dialogSource).toContain('import { buildTransferObjectSelections } from "./transferSelections"');
expect(dialogSource).toContain('createTable: transferContent.value !== "dataOnly"');
expect(dialogSource).toContain('createTable: transferContent.value !== "dataOnly"');
});
it("loads non-table object groups per source database kind", () => {
expect(dialogSource).toContain("transferObjectKindsForDatabase");
expect(dialogSource).toContain("api.listObjects(connectionId, database, schema, [kind]");
expect(dialogSource).toContain("groups[kind] = objects.map((o) => o.name)");
});
it("routes table-only database kinds through the table-list request", () => {
expect(dialogSource).toContain("const kinds = transferObjectKindsForDatabase(config?.db_type)");
expect(dialogSource).toContain("for (const kind of kinds)");
expect(dialogSource).toContain('if (kind === "TABLE")');
expect(dialogSource).toContain("await api.listTables(connectionId, database, schema");
});
it("keeps MongoDB collection loading ahead of the generic object-kind path", () => {
const mongoCollectionBranch = dialogSource.indexOf("if (isMongoConnection(connectionId))");
const genericObjectKinds = dialogSource.indexOf("const kinds = transferObjectKindsForDatabase(config?.db_type)");
expect(mongoCollectionBranch).toBeGreaterThan(-1);
expect(dialogSource).toContain("await api.mongoListCollections(connectionId, database)");
expect(mongoCollectionBranch).toBeLessThan(genericObjectKinds);
});
it("disables non-table groups for data-only and cross-family transfers", () => {
expect(dialogSource).toContain("treeDisabledGroups");
expect(dialogSource).toContain('transferContent.value === "dataOnly"');
expect(dialogSource).toContain("crossFamilyTransferableKinds");
expect(dialogSource).toContain("objectDataOnlyDisabled");
});
it("allows a transfer between different schemas in the same database", () => {
expect(dialogSource).toContain("isSameTransferDatabase");
expect(dialogSource).toContain("const sameSourceAndTarget = sameCatalogAndDatabase && effectiveSourceSchema === effectiveTargetSchema");
});
it("keeps catalog filtering and completion refresh catalog-aware", () => {
expect(dialogSource).toContain("fetchCatalogNamespaceOptions(connectionId, catalog, config)");
expect(dialogSource).toContain("store.refreshObjectListTreeNode(request.targetConnectionId, request.targetDatabase, request.targetSchema, request.targetCatalog)");
});
it("keeps default tree-schema databases selectable without leaking sentinels", () => {
expect(dialogSource).toContain("const sourceDatabaseName = computed(() => decodedDatabase(sourceConnectionId.value, sourceDatabase.value))");
expect(dialogSource).toContain("const targetDatabaseName = computed(() => decodedDatabase(targetConnectionId.value, targetDatabase.value))");
expect(dialogSource).toContain("sourceDatabase: sourceDatabaseName.value");
expect(dialogSource).toContain("targetDatabase: targetDatabaseName.value");
expect(dialogSource).toContain(':display-name="(option) => databaseOptionLabel(sourceConnectionId, option)"');
expect(dialogSource).toContain(':display-name="(option) => databaseOptionLabel(targetConnectionId, option)"');
});
it("discards stale async results after the connection changes", () => {
// 竞态防护:切换连接后旧请求的回调必须丢弃,不能覆盖新选择的下拉选项
expect(dialogSource).toContain("if (isStale()) return;");
expect(dialogSource).toContain("sourceConnectionId.value !== connectionId");
expect(dialogSource).toContain("targetConnectionId.value !== connectionId");
expect(dialogSource).toContain("if (sourceConnectionId.value !== id) return;");
expect(dialogSource).toContain("if (targetConnectionId.value !== id) return;");
});
});