test: drop source-text assertion tests and merge locale checks
This commit is contained in:
1 parent
349d77d180
commit
8897e12714
257 files changed
+166
-12815
No files matched your search
@@ -1,47 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import objectBrowserSource from "./ObjectBrowser.vue?raw";
|
||||
|
||||
describe("ObjectBrowser table clipboard context menu", () => {
|
||||
it("resolves recycled row menus when they are opened", () => {
|
||||
const lazyMenuBindings = objectBrowserSource.match(/:items="\(\) => getObjectBrowserMenuItems\(item\)"/g);
|
||||
|
||||
expect(lazyMenuBindings).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps paste on the copied row and allows replacing it from another row", () => {
|
||||
expect(objectBrowserSource).toMatch(/function tableClipboardMenuItems\(item: ObjectBrowserRow\)[\s\S]*?objectBrowserTableClipboardMenuState\(item\)[\s\S]*?state === "copy"[\s\S]*?state === "paste" \? \[pasteItem\] : \[copyItem, pasteItem\]/);
|
||||
expect(objectBrowserSource).toContain("...tableClipboardMenuItems(item)");
|
||||
});
|
||||
|
||||
it("normalizes copied and target schemas before paste validation", () => {
|
||||
expect(objectBrowserSource).toMatch(/function normalizedObjectBrowserTableClipboardEntries\(\)[\s\S]*?normalizeObjectBrowserTableClipboardSchema\(entry\.schema, entry\.database, entry\.connectionId\)/);
|
||||
expect(objectBrowserSource).toMatch(/function canPasteTableClipboard\(\)[\s\S]*?tableClipboardMatchesTarget\(normalizedObjectBrowserTableClipboardEntries\(\), pasteTableTargetContext\(\)\)/);
|
||||
expect(objectBrowserSource).toMatch(/function pasteTableTargetContext\(\)[\s\S]*?normalizeObjectBrowserTableClipboardSchema\(selectedSchema\.value\)/);
|
||||
expect(objectBrowserSource).toMatch(/pasteTableEntries\.value = clipboard\.tables\.map[\s\S]*?normalizeObjectBrowserTableClipboardSchema\(entry\.schema, entry\.database, entry\.connectionId\)/);
|
||||
expect(objectBrowserSource).toMatch(/function normalizeObjectBrowserTableClipboardSchema[\s\S]*?connectionStore\.getConfig\(connectionId\) \?\? props\.connection[\s\S]*?!isSchemaAware\(connection\.db_type\)[\s\S]*?connection\.db_type !== "sqlite"[\s\S]*?return undefined/);
|
||||
});
|
||||
|
||||
it("consumes only the clipboard used by a fully successful paste", () => {
|
||||
expect(objectBrowserSource).toMatch(
|
||||
/async function confirmPasteTable\(\)[\s\S]*?const clipboardAtPasteStart = connectionStore\.treeClipboard[\s\S]*?const pasteFeedback = tablePasteFeedback\(successCount, pasteFailCount, firstPasteError\)[\s\S]*?if \(pasteFailCount === 0\)[\s\S]*?connectionStore\.treeClipboard === clipboardAtPasteStart[\s\S]*?connectionStore\.treeClipboard = null/,
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the first paste failure reason without concatenating every error", () => {
|
||||
expect(objectBrowserSource).toMatch(/pasteFailCount\+\+/);
|
||||
expect(objectBrowserSource).toMatch(/firstPasteError \?\?= e/);
|
||||
expect(objectBrowserSource).toMatch(/batchPastePartialFail[\s\S]*?tableOperationFailed[\s\S]*?translateBackendError\(t, pasteFeedback\.firstError\)/);
|
||||
});
|
||||
|
||||
it("refreshes created tables and retains the clipboard when a later paste step is cancelled", () => {
|
||||
expect(objectBrowserSource).toMatch(/let pasteCancelled = false[\s\S]*?let hasMutatedTable = false/);
|
||||
expect(objectBrowserSource).toMatch(/if \(!executed\) \{[\s\S]*?pasteCancelled = true;[\s\S]*?break;/);
|
||||
expect(objectBrowserSource).toMatch(/if \(pasteCancelled\) \{[\s\S]*?if \(hasMutatedTable\)[\s\S]*?await reload\(\)[\s\S]*?refreshObjectListTreeNode[\s\S]*?pasteTableCancelledAfterPartial[\s\S]*?return;/);
|
||||
});
|
||||
|
||||
it("carries table comments through local copy and paste", () => {
|
||||
expect(objectBrowserSource).toMatch(/tableName: row\.name,\s*tableComment: row\.comment/);
|
||||
expect(objectBrowserSource).toMatch(/targetName: `\$\{entry\.tableName\}_copy`,[\s\S]*?tableComment: entry\.tableComment/);
|
||||
expect(objectBrowserSource).toMatch(/buildDuplicateStructurePlan\(entry\.sourceName, targetName, schema, entry\.tableComment/);
|
||||
});
|
||||
});
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(new URL("../DatabaseBrowser.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("DatabaseBrowser metadata fallback", () => {
|
||||
it("falls back to basic database names when extended metadata fails", () => {
|
||||
expect(source).toMatch(/await api\.listDatabaseMetadata\(props\.connection\.id\)[\s\S]*?catch \(metadataError\)[\s\S]*?await api\.listDatabases\(props\.connection\.id\)/);
|
||||
expect(source).toMatch(/catch \{\s*throw metadataError;\s*\}/);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,10 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { createApp, defineComponent, h, nextTick, type App } from "vue";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import DdlScopeFixture from "./DdlDialog.legacyScope.fixture.vue";
|
||||
|
||||
const ddlViewDialogSource = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/objects/DdlViewDialog.vue"), "utf8");
|
||||
|
||||
const mountedApps: App[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
@@ -61,14 +57,4 @@ describe("DdlViewDialog legacy fallback selectors", () => {
|
||||
expect(content!.hasAttribute(scopeId!)).toBe(false);
|
||||
expect(footer!.hasAttribute(scopeId!)).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the DDL dialog free of per-dialog legacy fallback rules", () => {
|
||||
// The dialog content element is rendered through reka-ui's portal Teleport and
|
||||
// never carries this component's scoped data-v attribute, so per-dialog rules
|
||||
// are avoided entirely: width comes from the global sm:max-w-190 legacy entry
|
||||
// and the footer layout from the global dialog-footer rule.
|
||||
expect(ddlViewDialogSource).not.toContain("dbx-legacy-webview");
|
||||
expect(ddlViewDialogSource).not.toContain("@media");
|
||||
expect(ddlViewDialogSource).toContain('class="dbx-ddl-view-dialog sm:max-w-190"');
|
||||
});
|
||||
});
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const objectBrowserSource = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("ObjectBrowser cross-database table paste", () => {
|
||||
it("routes a foreign table clipboard to the shared data-transfer dialog", () => {
|
||||
expect(objectBrowserSource).toContain("function canTransferTableClipboard()");
|
||||
expect(objectBrowserSource).toContain("function openTransferFromTableClipboard()");
|
||||
expect(objectBrowserSource).toContain("targetConnectionId: target.connectionId");
|
||||
expect(objectBrowserSource).toContain("targetDatabase: target.database");
|
||||
expect(objectBrowserSource).toMatch(/function objectBrowserTableClipboardMenuState[\s\S]*?tableClipboardMenuState\([\s\S]*?canTransferTableClipboard\(\),\s*\)/);
|
||||
});
|
||||
|
||||
it("keeps the local duplicate-table paste path available", () => {
|
||||
expect(objectBrowserSource).toContain("if (canTransferTableClipboard()) {");
|
||||
expect(objectBrowserSource).toContain("if (!canPasteTableClipboard() && !canTransferTableClipboard()) return;");
|
||||
expect(objectBrowserSource).toContain("pasteTableMode.value = defaultPasteTableMode(effectiveDatabaseType.value);");
|
||||
expect(objectBrowserSource).toContain('normalizeNewTargetName: mode === "structure-and-data"');
|
||||
});
|
||||
});
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const objectBrowserSource = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
const contentAreaSource = readFileSync(new URL("../../layout/ContentArea.vue", import.meta.url), "utf8");
|
||||
|
||||
function functionBody(name: string, source: string): string {
|
||||
const signature = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\{`, "m").exec(source);
|
||||
if (!signature) throw new Error(`Missing function ${name}`);
|
||||
const bodyStart = signature.index + signature[0].length;
|
||||
let depth = 1;
|
||||
for (let index = bodyStart; index < source.length; index += 1) {
|
||||
if (source[index] === "{") depth += 1;
|
||||
else if (source[index] === "}") depth -= 1;
|
||||
if (depth === 0) return source.slice(bodyStart, index);
|
||||
}
|
||||
throw new Error(`Unclosed function ${name}`);
|
||||
}
|
||||
|
||||
describe("ObjectBrowser MySQL Event CREATE navigation", () => {
|
||||
it("accepts an explicit create request prop next to the edit props", () => {
|
||||
expect(objectBrowserSource).toContain("initialEventCreateRequestId?: number;");
|
||||
expect(objectBrowserSource).toMatch(/initialEventName\?: string;[\s\S]*?initialEventCreateRequestId\?: number;[\s\S]*?initialObjectFilter\?: "tables" \| "events";/);
|
||||
});
|
||||
|
||||
it("routes the initial event request through the shared resolver", () => {
|
||||
const openInitialEventIfNeeded = functionBody("openInitialEventIfNeeded", objectBrowserSource);
|
||||
expect(openInitialEventIfNeeded).toContain("resolveInitialEventEditorRequest({");
|
||||
expect(openInitialEventIfNeeded).toContain("eventCreateRequestId: props.initialEventCreateRequestId");
|
||||
expect(openInitialEventIfNeeded).toContain("openedRequestKey: openedInitialEvent.value");
|
||||
expect(openInitialEventIfNeeded).toContain("hasEventRow: rows.value.some");
|
||||
expect(openInitialEventIfNeeded).toContain("loadingObjects: loadingObjects.value");
|
||||
});
|
||||
|
||||
it("opens the CREATE editor without any existing EVENT row", () => {
|
||||
const openInitialEventIfNeeded = functionBody("openInitialEventIfNeeded", objectBrowserSource);
|
||||
expect(openInitialEventIfNeeded).toContain('if (decision.type === "create") {');
|
||||
expect(openInitialEventIfNeeded).toContain("sidePanelRow.value = null;");
|
||||
expect(openInitialEventIfNeeded).toContain('sidePanelMode.value = "event-editor";');
|
||||
expect(openInitialEventIfNeeded).toContain("openedInitialEvent.value = decision.requestKey;");
|
||||
// The editor must see an empty name so MySqlEventEditor renders CREATE mode
|
||||
expect(openInitialEventIfNeeded).not.toContain("sidePanelRow.value = row;");
|
||||
});
|
||||
|
||||
it("keeps the CREATE editor mounted when there is no EVENT row", () => {
|
||||
expect(objectBrowserSource).toContain('v-if="sidePanelRow || isEventEditor"');
|
||||
expect(objectBrowserSource).toContain('<MySqlEventEditor :key="eventEditorKey"');
|
||||
});
|
||||
|
||||
it("renders the editor with an empty CREATE name and request-scoped instance", () => {
|
||||
expect(objectBrowserSource).toContain('<MySqlEventEditor :key="eventEditorKey" :connection="props.connection" :database="props.database" :schema="sidePanelRow?.schema || selectedSchema || props.database" :name="sidePanelRow?.name"');
|
||||
expect(objectBrowserSource).toMatch(/const eventEditorKey = computed\(\(\) =>[\s\S]*?createRequestId: props\.initialEventCreateRequestId[\s\S]*?openRequestId: props\.initialEventOpenRequestId[\s\S]*?rowId: sidePanelRow\.value\?\.id/);
|
||||
});
|
||||
|
||||
it("re-triggers a create request when the request id changes on a reused tab", () => {
|
||||
expect(objectBrowserSource).toContain("() => props.initialEventCreateRequestId");
|
||||
expect(objectBrowserSource).toMatch(/watch\(\[\(\) => props\.initialEventName, \(\) => props\.initialEventOpenRequestId, \(\) => props\.initialEventCreateRequestId\][\s\S]*?openedInitialEvent\.value = "";[\s\S]*?openInitialEventIfNeeded\(\);/);
|
||||
expect(objectBrowserSource).toContain('if (name || createRequestId !== undefined) objectFilter.value = "events";');
|
||||
});
|
||||
|
||||
it("lists the events filter as the preferred view for a create request", () => {
|
||||
expect(objectBrowserSource).toMatch(/const preferredFilter = props\.initialEventName \|\| props\.initialEventCreateRequestId !== undefined \? "events" : \(props\.selectedObjectFilter \?\? props\.initialObjectFilter \?\? "tables"\);/);
|
||||
});
|
||||
|
||||
it("ContentArea passes the create request id into ObjectBrowser", () => {
|
||||
expect(contentAreaSource).toContain(':initial-event-name="activeTab.objectBrowser?.eventName"');
|
||||
expect(contentAreaSource).toContain(':initial-event-create-request-id="activeTab.objectBrowser?.eventCreateRequestId"');
|
||||
});
|
||||
});
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const objectBrowserSource = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("ObjectBrowser MongoDB collection statistics (#9705)", () => {
|
||||
it("requests table statistics for every engine, MongoDB included", () => {
|
||||
// The loader used to skip MongoDB wholesale, so the columns below stayed
|
||||
// empty even though the backend can now answer `collStats` per collection.
|
||||
expect(objectBrowserSource).not.toContain('if (props.connection.db_type !== "mongodb") void loadObjectStatistics');
|
||||
expect(objectBrowserSource).toContain("void loadObjectStatistics(request, cacheWriteToken, cachedAt);");
|
||||
});
|
||||
|
||||
it("renders the row/size columns and their sort keys for MongoDB", () => {
|
||||
expect(objectBrowserSource).toContain("const showObjectRowStats = computed(() => showTableStatistics.value);");
|
||||
expect(objectBrowserSource).toContain("const supportsObjectSizeStats = computed(() => !isVictoriaMetrics.value);");
|
||||
});
|
||||
|
||||
it("keeps the batch table toolbar SQL-only", () => {
|
||||
expect(objectBrowserSource).toContain("const supportsBatchTableActions = computed(() => !isVictoriaMetrics.value && !isMongodb.value);");
|
||||
for (const handler of ["openBatchDatabaseExport", "copySelectedTablesToClipboard", "requestBatchTruncateTables", "requestBatchEmptyTables", "requestBatchDropTables"]) {
|
||||
const button = new RegExp(`v-if="supportsBatchTableActions[^"]*"[^>]*@click="${handler}"`);
|
||||
expect(objectBrowserSource).toMatch(button);
|
||||
}
|
||||
// No toolbar action may key off the stats-column flag any more.
|
||||
expect(objectBrowserSource).not.toContain('v-if="supportsObjectSizeStats');
|
||||
});
|
||||
});
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const objectBrowserSource = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
|
||||
function functionBody(name: string, source: string): string {
|
||||
const signature = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\{`, "m").exec(source);
|
||||
if (!signature) throw new Error(`Missing function ${name}`);
|
||||
const bodyStart = signature.index + signature[0].length;
|
||||
let depth = 1;
|
||||
for (let index = bodyStart; index < source.length; index += 1) {
|
||||
if (source[index] === "{") depth += 1;
|
||||
else if (source[index] === "}") depth -= 1;
|
||||
if (depth === 0) return source.slice(bodyStart, index);
|
||||
}
|
||||
throw new Error(`Unclosed function ${name}`);
|
||||
}
|
||||
|
||||
describe("ObjectBrowser object-list schema fallback (#8301)", () => {
|
||||
it("resolves the listObjects schema through the shared Dameng-aware helper", () => {
|
||||
const loadObjects = functionBody("loadObjects", objectBrowserSource);
|
||||
expect(loadObjects).toContain("needsSchema.value ? objectListSchemaForConnection(props.connection, selectedSchema.value) : props.database");
|
||||
expect(objectBrowserSource).toContain('objectListSchemaForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"');
|
||||
});
|
||||
|
||||
it("routes the schema through the request scope used by listObjects", () => {
|
||||
const loadSqlObjectBrowserRows = functionBody("loadSqlObjectBrowserRows", objectBrowserSource);
|
||||
expect(loadSqlObjectBrowserRows).toContain("api.listObjects(request.scope.connectionId, request.scope.database, request.scope.schema");
|
||||
const loadObjects = functionBody("loadObjects", objectBrowserSource);
|
||||
expect(loadObjects).toContain("objectBrowserRowsCacheScope(schema)");
|
||||
});
|
||||
|
||||
it("keeps the reload chain alive when loadSchemas rejects", () => {
|
||||
const reload = functionBody("reload", objectBrowserSource);
|
||||
expect(reload).toMatch(/try \{\s*if \(!\(await loadSchemas\(epoch\)\)\) return;\s*\} catch/);
|
||||
const catchClause = reload.slice(reload.indexOf("} catch"));
|
||||
// The failure path must not clear the selection (props.schema / current value)
|
||||
// and must still load objects — Dameng then falls back to the username.
|
||||
expect(catchClause).not.toContain("selectedSchema.value =");
|
||||
expect(catchClause).toContain("console.warn");
|
||||
expect(reload.lastIndexOf("await loadObjects")).toBeGreaterThan(reload.indexOf("} catch"));
|
||||
});
|
||||
|
||||
it("leaves the selected schema untouched when listSchemas itself rejects", () => {
|
||||
const loadSchemas = functionBody("loadSchemas", objectBrowserSource);
|
||||
// Every selectedSchema assignment in the try block sits behind the awaited
|
||||
// listSchemas call, so a rejection keeps the current selection intact.
|
||||
const tryStart = loadSchemas.indexOf("try {");
|
||||
const apiCall = loadSchemas.indexOf("await api.listSchemas", tryStart);
|
||||
const firstAssignment = loadSchemas.indexOf("selectedSchema.value =", apiCall);
|
||||
expect(apiCall).toBeGreaterThan(tryStart);
|
||||
expect(firstAssignment).toBeGreaterThan(apiCall);
|
||||
});
|
||||
});
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
// Issue #9200: when the "Browse Objects" pane is narrow, the keyword search box
|
||||
// collapsed to a sliver and the "全部/表/视图" chips were painted on top of it,
|
||||
// instead of the toolbar condensing its controls into the overflow menu.
|
||||
//
|
||||
// useToolbarOverflow only condenses while the row reports real overflow, and its
|
||||
// contract requires "non-shrinking (shrink-0) or floored (min-w-*) children":
|
||||
// 1. the collapsed search wrapper was `min-w-0`, so the wrapper dropped to 0
|
||||
// width while the input inside still painted at its padding width (58px),
|
||||
// drawing on top of the neighbouring filter chips;
|
||||
// 2. the search + filter group carried `min-w-24`, which overrode the
|
||||
// content-based minimum and let the group shrink below its own content, so
|
||||
// its children spilled into the sort control instead of overflowing the row;
|
||||
// 3. the filter chips allowed their labels to wrap, so the group's min-content
|
||||
// (longest word) was far below the width the chips actually occupy.
|
||||
// Each of the three is load-bearing; removing any one of them brings the overlap
|
||||
// back at some pane width.
|
||||
|
||||
const objectBrowserSource = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("ObjectBrowser toolbar condenses instead of overlapping when narrow", () => {
|
||||
it("floors the search box and lets the search + filter group keep its content minimum", () => {
|
||||
// The floored wrapper keeps the input from painting over its neighbours, and
|
||||
// dropping `min-w-24` lets the group's content-based minimum stop the spill
|
||||
// into the sort control, so the row reports overflow instead.
|
||||
expect(objectBrowserSource).toMatch(/<div class="flex flex-1 items-center gap-2">\s*<div class="relative min-w-\[6rem\] flex-1">/);
|
||||
});
|
||||
|
||||
it("pins the filter chips to their single-line width so the row reports overflow", () => {
|
||||
expect(objectBrowserSource).toMatch(/<button\s+v-for="filter in objectFilters"[\s\S]*?class="h-6 shrink-0 whitespace-nowrap rounded-sm/);
|
||||
});
|
||||
});
|
||||
Vendored
-41
@@ -1,41 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
|
||||
// Regression test for #8885: when the object-browser columns are wider than the
|
||||
// panel, the vertical scrollbar must stay pinned to the visible right edge
|
||||
// instead of travelling with the content. The list scroller therefore owns the
|
||||
// horizontal scrolling (with the content width applied to its inner item
|
||||
// wrapper), while the header is clipped and aligned programmatically.
|
||||
describe("ObjectBrowser vertical scrollbar viewport lock", () => {
|
||||
it("keeps the outer table container from scrolling horizontally", () => {
|
||||
const tableMatch = /class="object-browser-table[^"]*"/.exec(source);
|
||||
expect(tableMatch).not.toBeNull();
|
||||
expect(tableMatch![0]).toContain("overflow-hidden");
|
||||
expect(tableMatch![0]).not.toContain("overflow-x-auto");
|
||||
});
|
||||
|
||||
it("moves the content min-width onto the scroller's inner wrapper", () => {
|
||||
const scrollerMatch = /<RecycleScroller[^>]*listScrollerRef[^>]*>/.exec(source);
|
||||
expect(scrollerMatch).not.toBeNull();
|
||||
expect(scrollerMatch![0]).toContain("--dbx-object-grid-min-width");
|
||||
expect(scrollerMatch![0]).not.toContain("minWidth:");
|
||||
expect(source).toContain(".object-browser-scroller {");
|
||||
expect(source).toContain("overflow-x: auto;");
|
||||
expect(source).toContain("min-width: var(--dbx-object-grid-min-width, 0px);");
|
||||
});
|
||||
|
||||
it("clips the header and syncs its scroll position with the scroller", () => {
|
||||
const headerMatch = /ref="objectListHeaderRef"[^>]*/.exec(source);
|
||||
expect(headerMatch).not.toBeNull();
|
||||
expect(headerMatch![0]).toContain("overflow-hidden");
|
||||
expect(source).toContain("function syncObjectListHeaderScroll(");
|
||||
expect(source).toContain("header.scrollLeft = el.scrollLeft");
|
||||
// The sync runs on every scroller scroll event, when the scroller (re)attaches,
|
||||
// and whenever the column widths change the content width.
|
||||
const scrollHandler = source.indexOf("function onObjectsScroll(");
|
||||
expect(source.slice(scrollHandler, scrollHandler + 300)).toContain("syncObjectListHeaderScroll()");
|
||||
expect(source).toContain("nextTick(() => syncObjectListHeaderScroll())");
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
|
||||
function functionBody(name: string): string {
|
||||
const signature = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\{`, "m").exec(source);
|
||||
if (!signature) throw new Error(`Missing function ${name}`);
|
||||
const bodyStart = signature.index + signature[0].length;
|
||||
let depth = 1;
|
||||
for (let index = bodyStart; index < source.length; index += 1) {
|
||||
if (source[index] === "{") depth += 1;
|
||||
else if (source[index] === "}") depth -= 1;
|
||||
if (depth === 0) return source.slice(bodyStart, index);
|
||||
}
|
||||
throw new Error(`Unclosed function ${name}`);
|
||||
}
|
||||
|
||||
describe("ObjectBrowser XLSX export", () => {
|
||||
it("asks for export options before opening the save dialog", () => {
|
||||
const dialog = functionBody("showObjectBrowserXlsxHeaderDialog");
|
||||
const exportDataXlsx = functionBody("exportDataXlsx");
|
||||
|
||||
expect(dialog).toContain("showHeaderOptions: hasComments");
|
||||
expect(exportDataXlsx).toContain("hasXlsxHeaderComments(columnInfos?.map((column) => column.comment))");
|
||||
expect(exportDataXlsx.indexOf("await showObjectBrowserXlsxHeaderDialog(")).toBeLessThan(exportDataXlsx.indexOf('await exportTableData(row, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter)'));
|
||||
});
|
||||
|
||||
it("asks for the SQL INSERT mode before opening the save dialog", () => {
|
||||
const exportData = functionBody("exportData");
|
||||
const exportTableData = functionBody("exportTableData");
|
||||
|
||||
expect(exportData).toContain("await showSqlInsertModeDialog({ allowSplit: true })");
|
||||
expect(exportData.indexOf("await showSqlInsertModeDialog({ allowSplit: true })")).toBeLessThan(exportData.indexOf("await exportTableData("));
|
||||
expect(exportTableData).toContain('...(format === "sql" ? { insertMode, splitMaxMb } : {})');
|
||||
});
|
||||
|
||||
it("falls back to field-name headers when column metadata is unavailable", () => {
|
||||
const exportDataXlsx = functionBody("exportDataXlsx");
|
||||
|
||||
expect(exportDataXlsx).toContain("catch {\n // Export still works with field-name headers when column metadata is unavailable.\n }");
|
||||
expect(exportDataXlsx).toContain('await exportTableData(row, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter)');
|
||||
});
|
||||
});
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
|
||||
function functionBody(name: string): string {
|
||||
const signature = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\{`, "m").exec(source);
|
||||
if (!signature) throw new Error(`Missing function ${name}`);
|
||||
const bodyStart = signature.index + signature[0].length;
|
||||
let depth = 1;
|
||||
for (let index = bodyStart; index < source.length; index += 1) {
|
||||
if (source[index] === "{") depth += 1;
|
||||
else if (source[index] === "}") depth -= 1;
|
||||
if (depth === 0) return source.slice(bodyStart, index);
|
||||
}
|
||||
throw new Error(`Unclosed function ${name}`);
|
||||
}
|
||||
|
||||
describe("ObjectBrowser table metadata refresh", () => {
|
||||
it("refreshes the object list and the open table-info tab from the toolbar", () => {
|
||||
const refresh = functionBody("refresh");
|
||||
|
||||
expect(refresh).toContain("void reload({ preserveExistingRows: true });");
|
||||
expect(refresh).toContain("void refreshActiveTableInfo();");
|
||||
expect(source).toContain('@click="refresh"');
|
||||
});
|
||||
|
||||
it("invalidates stale requests and reloads only the active metadata surface", () => {
|
||||
const refreshTableInfo = functionBody("refreshActiveTableInfo");
|
||||
|
||||
expect(refreshTableInfo).toContain('sidePanelMode.value !== "table-info" || !sidePanelRow.value');
|
||||
expect(refreshTableInfo).toContain("sidePanelGuard.bump();");
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "ddl"[\s\S]*?rawTableDdlContent\.value = "";[\s\S]*?await fetchTableDdl\(true\);/);
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "columns"[\s\S]*?tableColumns\.value = \[\];[\s\S]*?await fetchTableColumns\(true\);/);
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "indexes"[\s\S]*?tableIndexes\.value = \[\];[\s\S]*?await fetchTableIndexes\(true\);/);
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "foreignKeys"[\s\S]*?tableForeignKeys\.value = \[\];[\s\S]*?await fetchTableForeignKeys\(true\);/);
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "triggers"[\s\S]*?tableTriggers\.value = \[\];[\s\S]*?await fetchTableTriggers\(true\);/);
|
||||
});
|
||||
|
||||
it("routes table-info metadata through the object caches", () => {
|
||||
expect(source).toContain('from "@/lib/metadata/objectDdlCache"');
|
||||
expect(source).toContain('from "@/lib/metadata/objectMetadataCache"');
|
||||
expect(functionBody("fetchTableDdl")).toContain("loadObjectDdl(");
|
||||
for (const name of ["fetchTableColumns", "fetchTableIndexes", "fetchTableForeignKeys", "fetchTableTriggers"]) {
|
||||
expect(functionBody(name)).toContain("loadObjectMetadataFacet(");
|
||||
expect(functionBody(name)).not.toContain(".value.length > 0");
|
||||
}
|
||||
});
|
||||
|
||||
it("shares the DDL refresh preference and exposes an explicit refresh action", () => {
|
||||
expect(source).toContain("async function fetchTableDdl(force = settingsStore.editorSettings.refreshDdlOnOpen)");
|
||||
expect(source).toContain('@click="refreshActiveTableInfo"');
|
||||
expect(source).not.toContain("setTableInfoRefreshDdlOnOpen");
|
||||
});
|
||||
|
||||
it("keeps automatic object reloads free of extra metadata requests", () => {
|
||||
expect(functionBody("reload")).not.toContain("refreshActiveTableInfo");
|
||||
});
|
||||
|
||||
it("leaves a failed facet eligible for retry", () => {
|
||||
for (const name of ["fetchTableDdl", "fetchTableColumns", "fetchTableIndexes", "fetchTableForeignKeys", "fetchTableTriggers"]) {
|
||||
const body = functionBody(name);
|
||||
expect(body).toContain("let loadedSuccessfully = false;");
|
||||
expect(body).toMatch(/loadedSuccessfully = true;/);
|
||||
expect(body).toMatch(/Loaded\.value = loadedSuccessfully;/);
|
||||
}
|
||||
});
|
||||
});
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
|
||||
function functionBody(name: string): string {
|
||||
const signature = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\{`, "m").exec(source);
|
||||
if (!signature) throw new Error(`Missing function ${name}`);
|
||||
const bodyStart = signature.index + signature[0].length;
|
||||
let depth = 1;
|
||||
for (let index = bodyStart; index < source.length; index += 1) {
|
||||
if (source[index] === "{") depth += 1;
|
||||
else if (source[index] === "}") depth -= 1;
|
||||
if (depth === 0) return source.slice(bodyStart, index);
|
||||
}
|
||||
throw new Error(`Unclosed function ${name}`);
|
||||
}
|
||||
|
||||
describe("ObjectBrowser scaffold refresh race", () => {
|
||||
it("restores cached rows without revalidating on a tab remount", () => {
|
||||
const body = functionBody("loadObjects");
|
||||
|
||||
const cachedBranch = body.indexOf("const cached =");
|
||||
const cachedReturn = body.indexOf("finishOnce();\n return;", cachedBranch);
|
||||
|
||||
expect(cachedBranch).toBeGreaterThanOrEqual(0);
|
||||
expect(cachedReturn).toBeGreaterThan(cachedBranch);
|
||||
expect(body.slice(cachedBranch, cachedReturn)).not.toContain("cached.stale");
|
||||
expect(body.slice(cachedBranch, cachedReturn)).not.toContain("refreshingObjects.value = true;");
|
||||
});
|
||||
|
||||
it("exposes a non-blocking refresh flag distinct from the blocking full-area load", () => {
|
||||
expect(source).toContain("const refreshingObjects = ref(false);");
|
||||
expect(functionBody("loadObjects")).toContain("refreshingObjects.value = true;");
|
||||
expect(functionBody("loadObjects")).toContain("loadingObjects.value = true;");
|
||||
});
|
||||
|
||||
it("keeps visible rows on a background revalidate failure (scaffold-preserving error)", () => {
|
||||
expect(source).toContain('const scaffoldRefreshError = ref("");');
|
||||
const body = functionBody("loadObjects");
|
||||
// A scaffold/refresh revalidate failure must route to the non-blocking banner
|
||||
// (scaffoldRefreshError) and keep the visible rows, not replace them with the
|
||||
// blocking full-area error.
|
||||
expect(body).toContain("scaffoldRefresh = true;");
|
||||
expect(body).toContain("if (scaffoldRefresh) {");
|
||||
expect(body).toContain("scaffoldRefreshError.value = translateBackendError(t, e)");
|
||||
expect(source).toContain('v-if="scaffoldRefreshError"');
|
||||
});
|
||||
});
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
// Issues #8236 and #8285: switching away from the "Browse Objects" tab and
|
||||
// back reset the keyword search, including after opening and closing a table,
|
||||
// because ObjectBrowser has no <KeepAlive> around
|
||||
// it (ContentArea renders it inside a plain v-else-if chain) and its `search`
|
||||
// ref was always seeded to "". The fix threads the value through
|
||||
// activeTab.objectBrowser.searchQuery, mirroring the existing viewport
|
||||
// persistence path.
|
||||
|
||||
const objectBrowserSource = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
const contentAreaSource = readFileSync(new URL("../../layout/ContentArea.vue", import.meta.url), "utf8");
|
||||
const querySurfacesSource = readFileSync(new URL("../../layout/querySurfaces.ts", import.meta.url), "utf8");
|
||||
const contentSurfaceEventsSource = readFileSync(new URL("../../../lib/tabs/contentSurfaceEvents.ts", import.meta.url), "utf8");
|
||||
const queryStoreSource = readFileSync(new URL("../../../stores/queryStore.ts", import.meta.url), "utf8");
|
||||
const databaseTypesSource = readFileSync(new URL("../../../types/database.ts", import.meta.url), "utf8");
|
||||
const appSource = readFileSync(new URL("../../../App.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("ObjectBrowser keyword search survives tab switches", () => {
|
||||
it("seeds the local search ref from a persisted initial value instead of always starting empty", () => {
|
||||
expect(objectBrowserSource).toContain("initialSearchQuery?: string;");
|
||||
expect(objectBrowserSource).toContain('const search = ref(props.initialSearchQuery ?? "");');
|
||||
});
|
||||
|
||||
it("emits searchChange whenever the keyword changes, so the parent can persist it", () => {
|
||||
expect(objectBrowserSource).toContain("searchChange: [query: string];");
|
||||
expect(objectBrowserSource).toMatch(/watch\(search, \(value\) => \{\s*scrollObjectsToTop\(\);\s*emit\("searchChange", value\);\s*\}\);/);
|
||||
});
|
||||
|
||||
it("ContentArea binds the persisted search query as a prop and forwards the change event", () => {
|
||||
expect(contentAreaSource).toContain(':initial-search-query="activeTab.objectBrowser?.searchQuery"');
|
||||
expect(contentAreaSource).toContain("@search-change=\"emit('objectBrowserSearchChange', activeTab.id, $event)\"");
|
||||
});
|
||||
|
||||
it("keeps the original object tab as the owner when a table is opened", () => {
|
||||
expect(appSource).toMatch(/@open-object-table="\s*\n?\s*\(tabId: string, target: \{[\s\S]*?\) => \{[\s\S]*?const tab = queryStore\.tabs\.find\(\(candidate\) => candidate\.id === tabId\) \?\? activeTab;[\s\S]*?openObjectBrowserTableTarget\(/);
|
||||
expect(appSource).toContain('@object-browser-search-change="(tabId: string, query: string) => queryStore.updateObjectBrowserSearch(tabId, query)"');
|
||||
});
|
||||
|
||||
it("the surface event contract and forwarding list both know about objectBrowserSearchChange", () => {
|
||||
expect(querySurfacesSource).toContain("objectBrowserSearchChange: [tabId: string, query: string];");
|
||||
expect(contentSurfaceEventsSource).toContain('"objectBrowserSearchChange",');
|
||||
});
|
||||
|
||||
it("queryStore persists the search query onto the tab's objectBrowser state", () => {
|
||||
expect(queryStoreSource).toContain("function updateObjectBrowserSearch(id: string, query: string)");
|
||||
expect(queryStoreSource).toContain("tab.objectBrowser = { ...tab.objectBrowser, searchQuery: query };");
|
||||
expect(queryStoreSource).toContain("updateObjectBrowserSearch,");
|
||||
});
|
||||
|
||||
it("the tab type declares searchQuery on objectBrowser state", () => {
|
||||
expect(databaseTypesSource).toMatch(/objectBrowser\?:\s*\{[^}]*searchQuery\?:\s*string;/s);
|
||||
});
|
||||
});
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
|
||||
function functionBody(name: string): string {
|
||||
const signature = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\{`, "m").exec(source);
|
||||
if (!signature) throw new Error(`Missing function ${name}`);
|
||||
const bodyStart = signature.index + signature[0].length;
|
||||
let depth = 1;
|
||||
for (let index = bodyStart; index < source.length; index += 1) {
|
||||
if (source[index] === "{") depth += 1;
|
||||
else if (source[index] === "}") depth -= 1;
|
||||
if (depth === 0) return source.slice(bodyStart, index);
|
||||
}
|
||||
throw new Error(`Unclosed function ${name}`);
|
||||
}
|
||||
|
||||
describe("ObjectBrowser viewport persistence", () => {
|
||||
it("flushes the latest scroll position before generating SQL", () => {
|
||||
const body = functionBody("openNewQuery");
|
||||
const flushIndex = body.indexOf("flushObjectBrowserViewport();");
|
||||
const createTabIndex = body.indexOf("queryStore.createTab");
|
||||
|
||||
expect(flushIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(createTabIndex).toBeGreaterThan(flushIndex);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user