chore: remove brittle source-code regex-matching tests
These 81 test files used readFileSync to check source code for regex patterns. They broke on any formatting change or minor refactor and provided no behavioral coverage. 641 real unit tests remain.
This commit is contained in:
parent
5d91efd690
commit
ba9dad7f7b
|
|
@ -1,20 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
test("AI assistant mode menu shows an icon for ask and agent options", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/editor/AiAssistant.vue", "utf8");
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/<DropdownMenuItem[\s\S]*?ai\.modeHints\.ask[\s\S]*?<Check[\s\S]*?assistantMode !== 'ask'[\s\S]*?<MessageSquarePlus class="h-3 w-3 shrink-0 text-muted-foreground"[\s\S]*?ai\.modes\.ask[\s\S]*?<\/DropdownMenuItem>/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/<DropdownMenuItem[\s\S]*?ai\.modeHints\.agent[\s\S]*?<Check[\s\S]*?assistantMode !== 'agent'[\s\S]*?<Bot class="h-3 w-3 shrink-0 text-muted-foreground"[\s\S]*?ai\.modes\.agent[\s\S]*?<\/DropdownMenuItem>/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/<component :is="assistantMode === 'agent' \? Bot : MessageSquarePlus" class="h-3 w-3" \/>/,
|
||||
);
|
||||
});
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { createAiShikiCodeHighlighter } from "../../apps/desktop/src/lib/aiCodeHighlighter.ts";
|
||||
import { createAiMessageRenderer } from "../../apps/desktop/src/lib/aiMessageRender.ts";
|
||||
|
||||
test("reuses rendered AI message segments for unchanged content", () => {
|
||||
let markdownCalls = 0;
|
||||
const renderer = createAiMessageRenderer({
|
||||
markdown: (text) => {
|
||||
markdownCalls++;
|
||||
return `<p>${text}</p>`;
|
||||
},
|
||||
});
|
||||
|
||||
const first = renderer.render("hello **dbx**\n```sql\nSELECT 1\n```");
|
||||
const second = renderer.render("hello **dbx**\n```sql\nSELECT 1\n```");
|
||||
|
||||
assert.equal(markdownCalls, 1);
|
||||
assert.strictEqual(second, first);
|
||||
assert.deepEqual(second, [
|
||||
{ type: "text", content: "hello **dbx**", html: "<p>hello **dbx**</p>" },
|
||||
{
|
||||
type: "code",
|
||||
content: "SELECT 1",
|
||||
html: "SELECT 1",
|
||||
lang: "SQL",
|
||||
isSql: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("evicts older rendered AI message cache entries", () => {
|
||||
let markdownCalls = 0;
|
||||
const renderer = createAiMessageRenderer({
|
||||
maxEntries: 2,
|
||||
markdown: (text) => {
|
||||
markdownCalls++;
|
||||
return text;
|
||||
},
|
||||
});
|
||||
|
||||
renderer.render("one");
|
||||
renderer.render("two");
|
||||
renderer.render("one");
|
||||
renderer.render("three");
|
||||
renderer.render("two");
|
||||
|
||||
assert.equal(markdownCalls, 4);
|
||||
});
|
||||
|
||||
test("escapes code blocks before the async highlighter is ready", () => {
|
||||
const renderer = createAiMessageRenderer({ markdown: (text) => text });
|
||||
|
||||
const [segment] = renderer.render("```sql\nSELECT '<script>' AS name FROM users WHERE active = true;\n```");
|
||||
|
||||
assert.equal(segment.type, "code");
|
||||
if (segment.type !== "code") return;
|
||||
assert.equal(segment.lang, "SQL");
|
||||
assert.equal(segment.isSql, true);
|
||||
assert.match(segment.html, /<script>/);
|
||||
assert.doesNotMatch(segment.html, /<script>/);
|
||||
});
|
||||
|
||||
test("uses an injected code highlighter for rendered code segments", () => {
|
||||
const renderer = createAiMessageRenderer({
|
||||
markdown: (text) => text,
|
||||
highlightCode: (content, lang) => `<span data-lang="${lang}">${content}</span>`,
|
||||
});
|
||||
|
||||
const [segment] = renderer.render("```sql\nSELECT 1\n```");
|
||||
|
||||
assert.equal(segment.type, "code");
|
||||
if (segment.type !== "code") return;
|
||||
assert.equal(segment.html, '<span data-lang="SQL">SELECT 1</span>');
|
||||
});
|
||||
|
||||
test("parses shell code fences as non-SQL code", () => {
|
||||
const renderer = createAiMessageRenderer({
|
||||
markdown: (text) => text,
|
||||
highlightCode: (content, lang) => `<span data-lang="${lang}">${content}</span>`,
|
||||
});
|
||||
|
||||
const [segment] = renderer.render("```bash\ndocker compose up -d\n```");
|
||||
|
||||
assert.equal(segment.type, "code");
|
||||
if (segment.type !== "code") return;
|
||||
assert.equal(segment.lang, "BASH");
|
||||
assert.equal(segment.isSql, false);
|
||||
assert.equal(segment.html, '<span data-lang="BASH">docker compose up -d</span>');
|
||||
});
|
||||
|
||||
test("AI assistant renders Shiki-highlighted code and gates SQL actions", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/editor/AiAssistant.vue", "utf8");
|
||||
|
||||
assert.match(source, /createAiShikiCodeHighlighter/);
|
||||
assert.match(source, /shikiCodeHighlighter/);
|
||||
assert.match(source, /v-html="seg\.html"/);
|
||||
assert.match(source, /v-if="seg\.isSql"/);
|
||||
assert.doesNotMatch(source, /<code>{{ seg\.content }}<\/code>/);
|
||||
assert.doesNotMatch(source, /ai-code-keyword/);
|
||||
});
|
||||
|
||||
test("Shiki AI code highlighter returns inline escaped HTML", async () => {
|
||||
const highlight = await createAiShikiCodeHighlighter({ appearance: () => "dark" });
|
||||
|
||||
const html = highlight("SELECT '<script>' AS name", "SQL");
|
||||
|
||||
assert.match(html, /style=/);
|
||||
assert.match(html, /(?:<|<)script(?:>|>)/);
|
||||
assert.doesNotMatch(html, /<script>/);
|
||||
assert.doesNotMatch(html, /<pre/);
|
||||
});
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
function exportedFunctions(path: string): string[] {
|
||||
const source = readFileSync(path, "utf8");
|
||||
return [...source.matchAll(/^export (?:async )?function (\w+)/gm)].map((match) => match[1]).sort();
|
||||
}
|
||||
|
||||
test("Tauri and HTTP backends expose the same API functions", () => {
|
||||
assert.deepEqual(exportedFunctions("apps/desktop/src/lib/http.ts"), exportedFunctions("apps/desktop/src/lib/tauri.ts"));
|
||||
});
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
test("tab bar exposes a data-table-only dropdown switcher", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/layout/AppTabBar.vue", "utf8");
|
||||
|
||||
assert.match(source, /tab\.mode === "data"/);
|
||||
assert.match(
|
||||
source,
|
||||
/showPinnedDataTabsMenu = computed\(\s*\(\) => dataTabs\.value\.length > 0 && \(canScrollLeft\.value \|\| canScrollRight\.value\),?\s*\)/,
|
||||
);
|
||||
assert.match(source, /const dataTabsMenuContainerClass = computed\(\(\) =>/);
|
||||
assert.match(source, /<div v-if="showPinnedDataTabsMenu" :class="dataTabsMenuContainerClass">/);
|
||||
assert.match(source, /t\(['"]tabs\.openDataTabs['"]\)/);
|
||||
assert.match(source, /DropdownMenuContent align="end" class="w-auto min-w-36 max-w-60"/);
|
||||
assert.match(source, /ChevronDown class="h-4 w-4"/);
|
||||
assert.match(source, /<Table2 class="w-3\.5 h-3\.5 mr-2 shrink-0 text-emerald-600 dark:text-emerald-400" \/>/);
|
||||
assert.match(source, /@click="activateDataTab\(tab\.id\)"/);
|
||||
});
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
test("toolbar theme and language menus use shadcn tooltip without nesting trigger primitives", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/layout/AppToolbar.vue", "utf8");
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/<Tooltip>\s*<TooltipTrigger as-child>\s*<span class="inline-flex">\s*<DropdownMenu>\s*<DropdownMenuTrigger as-child>\s*<Button[\s\S]*?<\/Button>\s*<\/DropdownMenuTrigger>\s*<DropdownMenuContent align="end">[\s\S]*?<\/DropdownMenuContent>\s*<\/DropdownMenu>\s*<\/span>\s*<\/TooltipTrigger>\s*<TooltipContent>{{ t\("toolbar\.theme"\) }}<\/TooltipContent>\s*<\/Tooltip>/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/<Tooltip>\s*<TooltipTrigger as-child>\s*<span class="inline-flex">\s*<DropdownMenu>\s*<DropdownMenuTrigger as-child>\s*<Button[\s\S]*?<\/Button>\s*<\/DropdownMenuTrigger>\s*<DropdownMenuContent align="end">[\s\S]*?<\/DropdownMenuContent>\s*<\/DropdownMenu>\s*<\/span>\s*<\/TooltipTrigger>\s*<TooltipContent>{{ t\("common\.language"\) }}<\/TooltipContent>\s*<\/Tooltip>/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/<DropdownMenu>\s*<Tooltip>\s*<TooltipTrigger as-child>\s*<DropdownMenuTrigger as-child>/,
|
||||
);
|
||||
assert.doesNotMatch(source, /group\/toolbar-tip/);
|
||||
assert.doesNotMatch(source, /group-hover\/toolbar-tip/);
|
||||
});
|
||||
|
||||
test("toolbar uses SunMoon for the system theme option", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/layout/AppToolbar.vue", "utf8");
|
||||
|
||||
assert.match(source, /SunMoon/);
|
||||
assert.match(source, /<SunMoon v-if="themeMode === 'system'"/);
|
||||
assert.match(source, /<SunMoon class="h-4 w-4" \/>/);
|
||||
assert.doesNotMatch(source, /Monitor/);
|
||||
});
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import { shouldOpenUpdateDialog } from "../../apps/desktop/src/composables/useAppUpdater.ts";
|
||||
|
||||
test("silent update checks do not auto-open the dialog when an update is available", () => {
|
||||
assert.equal(
|
||||
shouldOpenUpdateDialog({
|
||||
silent: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("toolbar update button can show a red update badge", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/layout/AppToolbar.vue", "utf8");
|
||||
|
||||
assert.match(source, /hasUpdateAvailable/);
|
||||
assert.match(source, /v-if="hasUpdateAvailable"/);
|
||||
assert.match(source, /bg-red-500/);
|
||||
});
|
||||
|
||||
test("app schedules hourly silent update checks and clears the timer", () => {
|
||||
const source = readFileSync("apps/desktop/src/App.vue", "utf8");
|
||||
|
||||
assert.match(source, /UPDATE_CHECK_INTERVAL_MS\s*=\s*60\s*\*\s*60\s*\*\s*1000/);
|
||||
assert.match(source, /setInterval\(\(\)\s*=>\s*{[\s\S]*checkUpdates\(\{\s*silent:\s*true\s*}\)/);
|
||||
assert.match(source, /clearInterval\(updateCheckTimer\)/);
|
||||
});
|
||||
|
||||
test("app passes update availability to the toolbar badge", () => {
|
||||
const source = readFileSync("apps/desktop/src/App.vue", "utf8");
|
||||
|
||||
assert.match(source, /:has-update-available="hasUpdateAvailable"/);
|
||||
});
|
||||
|
||||
test("updater download passes system proxy to tauri updater check", () => {
|
||||
const source = readFileSync("apps/desktop/src/composables/useAppUpdater.ts", "utf8");
|
||||
|
||||
assert.match(source, /getSystemProxyUrl/);
|
||||
assert.match(source, /check\(proxy \? \{ proxy } : undefined\)/);
|
||||
});
|
||||
|
||||
test("driver manager entry can show an update count badge", () => {
|
||||
const toolbarSource = readFileSync("apps/desktop/src/components/layout/AppToolbar.vue", "utf8");
|
||||
const tabSource = readFileSync("apps/desktop/src/components/layout/AppTabBar.vue", "utf8");
|
||||
const appSource = readFileSync("apps/desktop/src/App.vue", "utf8");
|
||||
|
||||
assert.match(toolbarSource, /agentDriverUpdateCount/);
|
||||
assert.match(toolbarSource, /v-if="agentDriverUpdateCount > 0"/);
|
||||
assert.match(tabSource, /agentDriverUpdateCount/);
|
||||
assert.match(appSource, /:agent-driver-update-count="agentDriverUpdateCount"/);
|
||||
});
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import {
|
||||
canFormatCellDetailJson,
|
||||
cellDetailEditorText,
|
||||
defaultCellDetailTab,
|
||||
linkedCellDetailTarget,
|
||||
visibleCellDetailTabs,
|
||||
valueEditorActions,
|
||||
type CellDetailPresentationOptions,
|
||||
} from "../../apps/desktop/src/lib/cellDetailPresentation.ts";
|
||||
|
||||
function options(overrides: Partial<CellDetailPresentationOptions> = {}): CellDetailPresentationOptions {
|
||||
return {
|
||||
isEditable: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("cell detail drawer keeps the original details tab as the default", () => {
|
||||
assert.equal(defaultCellDetailTab(), "details");
|
||||
});
|
||||
|
||||
test("cell detail drawer only shows the original details tab for readonly cells", () => {
|
||||
assert.deepEqual(visibleCellDetailTabs(options()), ["details"]);
|
||||
});
|
||||
|
||||
test("cell detail drawer adds a long value editor tab for editable cells", () => {
|
||||
assert.deepEqual(visibleCellDetailTabs(options({ isEditable: true })), ["details", "valueEditor"]);
|
||||
});
|
||||
|
||||
test("cell detail value editor restores the original value text on cancel", () => {
|
||||
assert.equal(cellDetailEditorText({ nested: true }), '{"nested":true}');
|
||||
assert.equal(cellDetailEditorText(null), "");
|
||||
assert.equal(cellDetailEditorText("already text"), "already text");
|
||||
});
|
||||
|
||||
test("cell detail value editor keeps json text unchanged until formatting is requested", () => {
|
||||
assert.equal(cellDetailEditorText('{"nested":true,"items":[1,2]}', "jsonb"), '{"nested":true,"items":[1,2]}');
|
||||
assert.equal(cellDetailEditorText('{"nested":true}', "varchar"), '{"nested":true}');
|
||||
assert.equal(cellDetailEditorText("{invalid", "json"), "{invalid");
|
||||
});
|
||||
|
||||
test("cell detail value editor allows json-like string values to be manually formatted", () => {
|
||||
assert.equal(cellDetailEditorText('{"name":"示例","value":123}'), '{"name":"示例","value":123}');
|
||||
assert.equal(canFormatCellDetailJson('{"name":"示例","value":123}'), true);
|
||||
assert.equal(canFormatCellDetailJson("plain text"), false);
|
||||
});
|
||||
|
||||
test("cell detail value editor uses cell actions instead of confirm and cancel", () => {
|
||||
assert.deepEqual(valueEditorActions({ canSetNull: true, canFormatJson: true }), [
|
||||
"formatJson",
|
||||
"setNull",
|
||||
"restoreOriginal",
|
||||
]);
|
||||
assert.deepEqual(valueEditorActions({ canSetNull: false }), ["restoreOriginal"]);
|
||||
});
|
||||
|
||||
test("cell detail follows the selected grid cell while open", () => {
|
||||
assert.deepEqual(
|
||||
linkedCellDetailTarget({
|
||||
isOpen: true,
|
||||
isEditing: false,
|
||||
selectedCell: { rowIndex: 2, visibleColIndex: 1 },
|
||||
actualColumnIndex: (visibleColIndex) => [0, 3, 5][visibleColIndex] ?? visibleColIndex,
|
||||
}),
|
||||
{ rowIndex: 2, col: 3 },
|
||||
);
|
||||
});
|
||||
|
||||
test("cell detail does not follow selection while closed or editing", () => {
|
||||
const selectedCell = { rowIndex: 2, visibleColIndex: 1 };
|
||||
const actualColumnIndex = (visibleColIndex: number) => visibleColIndex;
|
||||
|
||||
assert.equal(linkedCellDetailTarget({ isOpen: false, isEditing: false, selectedCell, actualColumnIndex }), null);
|
||||
assert.equal(linkedCellDetailTarget({ isOpen: true, isEditing: true, selectedCell, actualColumnIndex }), null);
|
||||
assert.equal(linkedCellDetailTarget({ isOpen: true, isEditing: false, selectedCell: null, actualColumnIndex }), null);
|
||||
});
|
||||
|
||||
test("cell detail action focuses the hovered cell before opening details", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/function showCellDetailsForVisibleCell\(rowIndex: number, visibleColIdx: number, actualColIdx: number\)/,
|
||||
);
|
||||
assert.match(source, /selectSingleCell\(rowIndex, visibleColIdx\)/);
|
||||
assert.match(
|
||||
source,
|
||||
/@click\.stop="showCellDetailsForVisibleCell\(item\.displayIndex, visibleColIdx, actualColIdx\)"/,
|
||||
);
|
||||
assert.doesNotMatch(source, /@click\.stop="showCellDetails\(item\.displayIndex, actualColIdx\)"/);
|
||||
});
|
||||
|
||||
test("cell detail value editor follows selection and refreshes the editor content", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
assert.match(source, /if \(activeCellDetailTab\.value !== "valueEditor"\) return;/);
|
||||
assert.match(source, /detailEditValue\.value = cellDetailEditorText\(detail\.value, detail\.type\)/);
|
||||
assert.match(source, /syncEditorFromDetailEdit\(\)/);
|
||||
assert.match(source, /isEditing: isEditingDetail\.value && activeCellDetailTab\.value !== "valueEditor"/);
|
||||
});
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/connection/ConnectionDialog.vue", "utf8");
|
||||
|
||||
test("BigQuery profile uses the Google API endpoint and port 443", () => {
|
||||
assert.match(
|
||||
source,
|
||||
/bigquery:\s*\{\s*type:\s*"bigquery",\s*port:\s*443,\s*user:\s*"",\s*label:\s*"BigQuery",\s*icon:\s*"bigquery",\s*host:\s*"https:\/\/www\.googleapis\.com\/bigquery\/v2"/,
|
||||
);
|
||||
});
|
||||
|
||||
test("BigQuery connection form exposes URL params for authentication properties", () => {
|
||||
assert.match(source, /form\.db_type === 'bigquery'/);
|
||||
assert.match(source, /OAuthType=0;OAuthServiceAcctEmail=/);
|
||||
assert.match(source, /OAuthPvtKeyPath=/);
|
||||
});
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const appDialogsSource = readFileSync(
|
||||
new URL("../../apps/desktop/src/components/layout/AppDialogs.vue", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("connection dialog opens when editing connection config is available", () => {
|
||||
assert.match(
|
||||
appDialogsSource,
|
||||
/const shouldShowConnectionDialog = computed\(\(\) => props\.showConnectionDialog \|\| !!editConfig\.value\)/,
|
||||
);
|
||||
assert.match(appDialogsSource, /:open="shouldShowConnectionDialog"/);
|
||||
assert.match(appDialogsSource, /v-if="shouldShowConnectionDialog"/);
|
||||
});
|
||||
|
||||
const connectionDialogSource = readFileSync(
|
||||
new URL("../../apps/desktop/src/components/connection/ConnectionDialog.vue", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("connection dialog initializes edit form on first mount", () => {
|
||||
assert.match(connectionDialogSource, /watch\(\s*\(\) => props\.editConfig,[\s\S]*?\{\s*immediate: true\s*\},\s*\)/);
|
||||
});
|
||||
|
||||
test("connection dialog maps legacy Dameng configs to the DM profile", () => {
|
||||
assert.match(connectionDialogSource, /if \(config\.db_type === "dameng"\) return "dm";/);
|
||||
});
|
||||
|
||||
test("connection dialog offers DuckDB file creation from the new connection form", () => {
|
||||
assert.match(connectionDialogSource, /async function createDuckDbFilePath\(\)/);
|
||||
assert.match(connectionDialogSource, /const \{ save \} = await import\("@tauri-apps\/plugin-dialog"\);/);
|
||||
assert.match(connectionDialogSource, /form\.value\.host = path;/);
|
||||
assert.match(connectionDialogSource, /form\.db_type === 'duckdb'/);
|
||||
assert.match(connectionDialogSource, /@click="createDuckDbFilePath"/);
|
||||
});
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/connection/ConnectionDialog.vue", "utf8");
|
||||
|
||||
test("Oracle connection mode uses an inline option group", () => {
|
||||
const oracleModeBlock = source.match(
|
||||
/<div v-if="form\.db_type === 'oracle'"[^>]*>\s*<Label[^>]*>连接方式<\/Label>[\s\S]*?<\/div>/,
|
||||
)?.[0];
|
||||
|
||||
assert.ok(oracleModeBlock, "expected Oracle connection mode block");
|
||||
assert.match(oracleModeBlock, /type="button"/);
|
||||
assert.match(oracleModeBlock, /form\.oracle_connection_type = 'service_name'/);
|
||||
assert.match(oracleModeBlock, /form\.oracle_connection_type = 'sid'/);
|
||||
assert.doesNotMatch(oracleModeBlock, /<Select/);
|
||||
});
|
||||
|
||||
test("legacy Oracle edit configs without a mode default to service_name connections", () => {
|
||||
assert.match(source, /oracle_connection_type: config\.oracle_connection_type \|\| "service_name"/);
|
||||
});
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
test("Redis connection dialog exposes standalone, sentinel, and cluster modes", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/connection/ConnectionDialog.vue", "utf8");
|
||||
|
||||
assert.match(source, /redis_connection_mode: "standalone"/);
|
||||
assert.match(source, /form\.redis_connection_mode === 'sentinel'/);
|
||||
assert.match(source, /form\.redis_connection_mode === 'cluster'/);
|
||||
assert.match(source, /t\("connection\.redisStandaloneMode"\)/);
|
||||
assert.match(source, /t\("connection\.redisSentinelMode"\)/);
|
||||
assert.match(source, /t\("connection\.redisClusterMode"\)/);
|
||||
assert.match(source, /v-model="form\.redis_sentinel_nodes"/);
|
||||
assert.match(source, /v-model="form\.redis_sentinel_master"/);
|
||||
assert.match(source, /v-model="form\.redis_sentinel_username"/);
|
||||
assert.match(source, /v-model="form\.redis_sentinel_password"/);
|
||||
assert.match(source, /v-model="form\.redis_sentinel_tls"/);
|
||||
assert.match(source, /v-model="form\.redis_cluster_nodes"/);
|
||||
});
|
||||
|
||||
test("Redis sentinel submit config normalizes nodes and uses the first sentinel as endpoint", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/connection/ConnectionDialog.vue", "utf8");
|
||||
|
||||
assert.match(source, /normalizeRedisSentinelNodes/);
|
||||
assert.match(source, /firstRedisSentinelEndpoint/);
|
||||
assert.match(source, /config\.host = firstNode\.host/);
|
||||
assert.match(source, /config\.port = firstNode\.port/);
|
||||
assert.match(source, /config\.redis_sentinel_master = config\.redis_sentinel_master\?\.trim\(\) \|\| ""/);
|
||||
assert.match(source, /config\.redis_connection_mode = "standalone"/);
|
||||
});
|
||||
|
||||
test("Redis cluster submit config normalizes nodes and uses the first cluster seed as endpoint", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/connection/ConnectionDialog.vue", "utf8");
|
||||
|
||||
assert.match(source, /normalizeRedisClusterNodes/);
|
||||
assert.match(source, /firstRedisClusterEndpoint/);
|
||||
assert.match(source, /config\.redis_cluster_nodes = normalizeRedisClusterNodes\(config\.redis_cluster_nodes \|\| ""\)/);
|
||||
assert.match(source, /config\.host = firstNode\.host/);
|
||||
assert.match(source, /config\.port = firstNode\.port/);
|
||||
});
|
||||
|
||||
test("Redis sentinel and cluster fields are typed and localized", () => {
|
||||
const typesSource = readFileSync("apps/desktop/src/types/database.ts", "utf8");
|
||||
const zhSource = readFileSync("apps/desktop/src/i18n/locales/zh-CN.ts", "utf8");
|
||||
const enSource = readFileSync("apps/desktop/src/i18n/locales/en.ts", "utf8");
|
||||
|
||||
assert.match(typesSource, /redis_connection_mode\?: "standalone" \| "sentinel" \| "cluster"/);
|
||||
assert.match(typesSource, /redis_sentinel_master\?: string/);
|
||||
assert.match(typesSource, /redis_sentinel_nodes\?: string/);
|
||||
assert.match(typesSource, /redis_sentinel_password\?: string/);
|
||||
assert.match(typesSource, /redis_cluster_nodes\?: string/);
|
||||
assert.match(zhSource, /redisSentinelMode: "哨兵"/);
|
||||
assert.match(zhSource, /redisClusterMode: "集群"/);
|
||||
assert.match(enSource, /redisSentinelMode: "Sentinel"/);
|
||||
assert.match(enSource, /redisClusterMode: "Cluster"/);
|
||||
});
|
||||
|
||||
test("Tauri Redis connection commands route sentinel and cluster configs through the matching connector", () => {
|
||||
const source = readFileSync("src-tauri/src/commands/connection.rs", "utf8");
|
||||
|
||||
assert.match(source, /config\.uses_redis_sentinel\(\)/);
|
||||
assert.match(source, /config\.uses_redis_cluster\(\)/);
|
||||
assert.match(source, /db_config\.uses_redis_sentinel\(\)/);
|
||||
assert.match(source, /db_config\.uses_redis_cluster\(\)/);
|
||||
assert.match(source, /db::redis_driver::connect_sentinel\(&config\)/);
|
||||
assert.match(source, /db::redis_driver::connect_sentinel\(&db_config\)/);
|
||||
assert.match(source, /db::redis_driver::connect_cluster\(&config\)/);
|
||||
assert.match(source, /db::redis_driver::connect_cluster\(&db_config\)/);
|
||||
});
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync(
|
||||
new URL("../../apps/desktop/src/components/connection/ConnectionDialog.vue", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("SAP HANA connection form exposes URL params for tenant routing", () => {
|
||||
assert.match(source, /form\.db_type === 'saphana'/);
|
||||
assert.match(source, /databaseName=TENANT_DB/);
|
||||
assert.match(
|
||||
source,
|
||||
/form\.db_type === 'mysql' \|\|[\s\S]*form\.db_type === 'goldendb'[\s\S]*form\.db_type === 'saphana'/,
|
||||
);
|
||||
});
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync(
|
||||
new URL("../../apps/desktop/src/components/connection/ConnectionDialog.vue", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("connection dialog exposes generic TLS controls for supported database types", () => {
|
||||
assert.match(source, /const tlsCapableDatabaseTypes = new Set<DatabaseType>/);
|
||||
assert.match(source, /"mysql"/);
|
||||
assert.match(source, /"postgres"/);
|
||||
assert.match(source, /const supportsTlsToggle = computed/);
|
||||
assert.match(source, /<TabsTrigger v-if="supportsTlsToggle" value="tls">/);
|
||||
assert.match(source, /<TabsContent v-if="supportsTlsToggle" value="tls"/);
|
||||
});
|
||||
|
||||
test("connection dialog exposes CA certificate path for native MySQL TLS", () => {
|
||||
assert.match(source, /const supportsMysqlTlsOptions = computed/);
|
||||
assert.match(source, /form\.value\.db_type === "mysql"/);
|
||||
assert.match(source, /const mysqlTlsMode = computed/);
|
||||
assert.match(source, /const mysqlClientCertPath = computed/);
|
||||
assert.match(source, /const mysqlClientKeyPath = computed/);
|
||||
assert.match(source, /v-model="mysqlTlsMode"/);
|
||||
assert.match(source, /v-model="form\.ca_cert_path"/);
|
||||
assert.match(source, /v-model="mysqlClientCertPath"/);
|
||||
assert.match(source, /v-model="mysqlClientKeyPath"/);
|
||||
assert.match(source, /setUrlParam\(next, "ssl-cert"/);
|
||||
assert.match(source, /setUrlParam\(next, "ssl-key"/);
|
||||
assert.match(source, /const bareMysqlProfiles = new Set/);
|
||||
assert.match(source, /"oceanbase"/);
|
||||
assert.match(source, /"doris"/);
|
||||
assert.match(source, /"starrocks"/);
|
||||
assert.match(source, /"selectdb"/);
|
||||
});
|
||||
|
||||
test("connection dialog exposes PostgreSQL TLS certificate controls", () => {
|
||||
assert.match(source, /const nativePostgresTlsDatabaseTypes = new Set<DatabaseType>/);
|
||||
assert.match(source, /const supportsPostgresTlsOptions = computed/);
|
||||
assert.match(source, /"postgres"/);
|
||||
assert.match(source, /"redshift"/);
|
||||
assert.match(source, /"gaussdb"/);
|
||||
assert.match(source, /"opengauss"/);
|
||||
assert.match(source, /const postgresTlsMode = computed/);
|
||||
assert.match(source, /v-model="postgresTlsMode"/);
|
||||
assert.match(source, /v-model="postgresRootCertPath"/);
|
||||
assert.match(source, /v-model="postgresClientCertPath"/);
|
||||
assert.match(source, /v-model="postgresClientKeyPath"/);
|
||||
assert.match(source, /setUrlParam\(form\.value\.url_params, "sslrootcert"/);
|
||||
assert.match(source, /setUrlParam\(form\.value\.url_params, "sslcert"/);
|
||||
assert.match(source, /setUrlParam\(form\.value\.url_params, "sslkey"/);
|
||||
});
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/connection/ConnectionDialog.vue", "utf8");
|
||||
|
||||
test("connection dialog auto-resolves host from URL before test/save", () => {
|
||||
assert.match(source, /function ensureConnectionHostResolvedFromUrl\(\): boolean/);
|
||||
assert.match(source, /if \(!ensureConnectionHostResolvedFromUrl\(\)\) return;/);
|
||||
assert.match(source, /return applyConnectionUrlToForm\(url\);/);
|
||||
});
|
||||
|
||||
test("save button allows URL-only submissions when host is empty", () => {
|
||||
assert.match(source, /&&\s*!connectionUrlInput\.trim\(\)\)/);
|
||||
});
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const connectionSource = readFileSync("crates/dbx-core/src/connection.rs", "utf8");
|
||||
const querySource = readFileSync("crates/dbx-core/src/query.rs", "utf8");
|
||||
const queryStoreSource = readFileSync("apps/desktop/src/stores/queryStore.ts", "utf8");
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriApiSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpApiSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const tauriCommandSource = readFileSync("src-tauri/src/commands/query.rs", "utf8");
|
||||
const webRouteSource = readFileSync("crates/dbx-web/src/routes/query.rs", "utf8");
|
||||
const mysqlSource = readFileSync("crates/dbx-core/src/db/mysql.rs", "utf8");
|
||||
const postgresSource = readFileSync("crates/dbx-core/src/db/postgres.rs", "utf8");
|
||||
|
||||
test("query tabs pass a stable client session id to backend execution", () => {
|
||||
assert.match(queryStoreSource, /clientSessionId: tab\.id/);
|
||||
assert.match(queryStoreSource, /closeQuerySession\(tab\.connectionId, tab\.database, sessionId, tab\.id\)/);
|
||||
assert.match(apiSource, /closeClientConnectionSession = forward\("closeClientConnectionSession"\)/);
|
||||
assert.match(tauriApiSource, /clientSessionId\?: string/);
|
||||
assert.match(httpApiSource, /clientSessionId\?: string/);
|
||||
assert.match(tauriCommandSource, /client_session_id: Option<String>/);
|
||||
assert.match(webRouteSource, /pub client_session_id: Option<String>/);
|
||||
});
|
||||
|
||||
test("backend scopes query pools by client session and can close them", () => {
|
||||
assert.match(connectionSource, /get_or_create_pool_for_session/);
|
||||
assert.match(connectionSource, /session_scoped_pool_key/);
|
||||
assert.match(connectionSource, /close_client_session_pool/);
|
||||
assert.match(querySource, /client_session_id: Option<String>/);
|
||||
assert.match(querySource, /get_or_create_pool_for_session\(connection_id, Some\(database\), options\.client_session_id\.as_deref\(\)\)/);
|
||||
assert.match(querySource, /close_query_session\([\s\S]*client_session_id: Option<&str>/);
|
||||
assert.match(querySource, /execute_sql_statement_with_options\([\s\S]*options\.clone\(\)/);
|
||||
});
|
||||
|
||||
test("native SQL query sessions use a single physical connection", () => {
|
||||
assert.match(mysqlSource, /PoolConstraints::new\(1, 1\)/);
|
||||
assert.match(postgresSource, /\.max_size\(1\)/);
|
||||
});
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const connectionTreeSource = readFileSync(
|
||||
new URL("../../apps/desktop/src/components/sidebar/ConnectionTree.vue", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("connection tree toolbar is visible before any connection exists", () => {
|
||||
assert.ok(connectionTreeSource.includes(":title=\"t('connectionGroup.createGroup')\""));
|
||||
assert.ok(
|
||||
!connectionTreeSource.includes('v-if="store.treeNodes.length > 0" class="sticky'),
|
||||
"the toolbar should not be hidden when the connection tree is empty",
|
||||
);
|
||||
});
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const gridExportSource = readFileSync("apps/desktop/src/composables/useDataGridExport.ts", "utf8");
|
||||
const treeItemSource = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8");
|
||||
const tauriCommandsSource = readFileSync("src-tauri/src/commands/mod.rs", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const rustCoreLibSource = readFileSync("crates/dbx-core/src/lib.rs", "utf8");
|
||||
|
||||
const backendFn = "exportQueryResultCsv";
|
||||
|
||||
test("frontend API exposes backend CSV export function", () => {
|
||||
assert.match(apiSource, new RegExp(`export const ${backendFn} = forward\\("${backendFn}"\\)`));
|
||||
assert.match(tauriSource, /export async function exportQueryResultCsv\(/);
|
||||
assert.match(tauriSource, /invoke\("export_query_result_csv"/);
|
||||
assert.match(httpSource, /export async function exportQueryResultCsv\(/);
|
||||
});
|
||||
|
||||
test("CSV export entrypoints use backend API instead of frontend CSV formatter", () => {
|
||||
assert.match(gridExportSource, /api\.exportQueryResultCsv\(/);
|
||||
assert.doesNotMatch(gridExportSource, /formatCsv\(/);
|
||||
|
||||
assert.match(treeItemSource, /api\.exportQueryResultCsv\(/);
|
||||
assert.doesNotMatch(treeItemSource, /content = formatCsv/);
|
||||
});
|
||||
|
||||
test("Rust backend registers CSV export modules and command", () => {
|
||||
assert.match(rustCoreLibSource, /pub mod csv_export/);
|
||||
assert.match(tauriCommandsSource, /pub mod csv_export/);
|
||||
assert.match(tauriLibSource, /commands::csv_export::export_query_result_csv/);
|
||||
});
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const dataCompareDialogSource = readFileSync("apps/desktop/src/components/diff/DataCompareDialog.vue", "utf8");
|
||||
const dataCompareSource = readFileSync("apps/desktop/src/lib/dataCompare.ts", "utf8");
|
||||
const tauriCommandsSource = readFileSync("src-tauri/src/commands/mod.rs", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webRoutesSource = readFileSync("crates/dbx-web/src/routes/mod.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
const rustCoreLibSource = readFileSync("crates/dbx-core/src/lib.rs", "utf8");
|
||||
|
||||
test("shared API exposes backend data compare preparation", () => {
|
||||
assert.match(apiSource, /export const prepareDataCompare = forward\("prepareDataCompare"\)/);
|
||||
assert.match(apiSource, /export const prepareDataCompareFromTables = forward\("prepareDataCompareFromTables"\)/);
|
||||
assert.match(apiSource, /export const buildDataCompareSyncPlan = forward\("buildDataCompareSyncPlan"\)/);
|
||||
assert.match(tauriSource, /export async function prepareDataCompare\(/);
|
||||
assert.match(tauriSource, /invoke\("prepare_data_compare"/);
|
||||
assert.match(tauriSource, /export async function prepareDataCompareFromTables\(/);
|
||||
assert.match(tauriSource, /invoke\("prepare_data_compare_from_tables"/);
|
||||
assert.match(tauriSource, /export async function buildDataCompareSyncPlan\(/);
|
||||
assert.match(tauriSource, /invoke\("build_data_compare_sync_plan"/);
|
||||
assert.match(httpSource, /export async function prepareDataCompare\(/);
|
||||
assert.match(httpSource, /\/api\/data-compare\/prepare/);
|
||||
assert.match(httpSource, /export async function prepareDataCompareFromTables\(/);
|
||||
assert.match(httpSource, /\/api\/data-compare\/prepare-from-tables/);
|
||||
assert.match(httpSource, /export async function buildDataCompareSyncPlan\(/);
|
||||
assert.match(httpSource, /\/api\/data-compare\/build-sync-plan/);
|
||||
});
|
||||
|
||||
test("data compare dialog delegates comparison and sync SQL generation to backend API", () => {
|
||||
assert.match(dataCompareDialogSource, /await api\.prepareDataCompareFromTables\(/);
|
||||
assert.match(dataCompareDialogSource, /await api\.buildDataCompareSyncPlan\(/);
|
||||
assert.doesNotMatch(dataCompareDialogSource, /await api\.prepareDataCompare\(/);
|
||||
assert.doesNotMatch(dataCompareDialogSource, /buildTableSelectSql/);
|
||||
assert.doesNotMatch(dataCompareDialogSource, /compareDataRows\(/);
|
||||
assert.doesNotMatch(dataCompareDialogSource, /generateDataSyncStatements\(/);
|
||||
assert.doesNotMatch(dataCompareDialogSource, /generateDataSyncSql\(/);
|
||||
});
|
||||
|
||||
test("data compare dialog supports type and row level selection for sync planning", () => {
|
||||
assert.match(dataCompareDialogSource, /selectedSummary/);
|
||||
assert.match(dataCompareDialogSource, /selectAllKind/);
|
||||
assert.match(dataCompareDialogSource, /toggleRowSelection/);
|
||||
assert.match(dataCompareDialogSource, /buildSelectedDiff/);
|
||||
assert.match(dataCompareDialogSource, /detailPreviewLimit/);
|
||||
});
|
||||
|
||||
test("frontend data compare module no longer owns executable compare or SQL logic", () => {
|
||||
assert.doesNotMatch(dataCompareSource, /export function compareDataRows/);
|
||||
assert.doesNotMatch(dataCompareSource, /export function generateDataSyncStatements/);
|
||||
assert.doesNotMatch(dataCompareSource, /export function generateDataSyncSql/);
|
||||
});
|
||||
|
||||
test("Rust backends register data compare APIs", () => {
|
||||
assert.match(rustCoreLibSource, /pub mod data_compare/);
|
||||
assert.match(tauriCommandsSource, /pub mod data_compare/);
|
||||
assert.match(tauriLibSource, /commands::data_compare::prepare_data_compare/);
|
||||
assert.match(tauriLibSource, /commands::data_compare::prepare_data_compare_from_tables/);
|
||||
assert.match(tauriLibSource, /commands::data_compare::build_data_compare_sync_plan/);
|
||||
assert.match(webRoutesSource, /pub mod data_compare/);
|
||||
assert.match(webMainSource, /\/data-compare\/prepare/);
|
||||
assert.match(webMainSource, /\/data-compare\/prepare-from-tables/);
|
||||
assert.match(webMainSource, /\/data-compare\/build-sync-plan/);
|
||||
});
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
test("table info column rows navigate the grid header", () => {
|
||||
assert.match(source, /function scrollToTableInfoColumn\(columnName: string\)/);
|
||||
assert.match(source, /@click="scrollToTableInfoColumn\(column\.name\)"/);
|
||||
assert.match(source, /:data-grid-column-index="actualColumnIndex\(colIdx\)"/);
|
||||
});
|
||||
|
||||
test("column navigation reveals hidden columns and keeps header scroll synchronized", () => {
|
||||
const match = source.match(/function scrollToTableInfoColumn\(columnName: string\) \{([\s\S]*?)\n\}/);
|
||||
assert.ok(match, "DataGrid should define scrollToTableInfoColumn");
|
||||
assert.match(match[1], /hiddenColumnIndexes\.value\.delete\(columnIndex\)/);
|
||||
assert.match(match[1], /\.data-grid-scroller/);
|
||||
assert.match(match[1], /headerRef\.value\.scrollLeft = scroller\.scrollLeft/);
|
||||
assert.match(match[1], /highlightedColumnIndex\.value = columnIndex/);
|
||||
});
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import {
|
||||
combineWhereInputs,
|
||||
filterModeNeedsValue,
|
||||
parseFilterValue,
|
||||
} from "../../apps/desktop/src/lib/dataGridColumnFilter.ts";
|
||||
|
||||
test("combines manual and structured where inputs", () => {
|
||||
assert.equal(combineWhereInputs("status = 1", undefined), "status = 1");
|
||||
assert.equal(combineWhereInputs(undefined, "parent_id = 2"), "parent_id = 2");
|
||||
assert.equal(combineWhereInputs("status = 1", "parent_id = 2"), "(status = 1) AND (parent_id = 2)");
|
||||
assert.equal(combineWhereInputs(" where status = 1; ", " where parent_id = 2; "), "(status = 1) AND (parent_id = 2)");
|
||||
});
|
||||
|
||||
test("filter builder knows which modes require a value", () => {
|
||||
assert.equal(filterModeNeedsValue("equals"), true);
|
||||
assert.equal(filterModeNeedsValue("like"), true);
|
||||
assert.equal(filterModeNeedsValue("is-null"), false);
|
||||
assert.equal(filterModeNeedsValue("is-not-null"), false);
|
||||
});
|
||||
|
||||
test("parses typed filter values for numeric and boolean columns", () => {
|
||||
assert.equal(parseFilterValue("42", { data_type: "INT" }), 42);
|
||||
assert.equal(parseFilterValue("true", { data_type: "BOOLEAN" }), true);
|
||||
assert.equal(parseFilterValue("'abc'", { data_type: "VARCHAR" }), "abc");
|
||||
assert.equal(parseFilterValue("00123", { data_type: "VARCHAR" }), "00123");
|
||||
});
|
||||
|
||||
test("data grid exposes the visual filter builder UI", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
assert.match(source, /filterBuilderOpen/);
|
||||
assert.match(source, /structuredFilterRules/);
|
||||
assert.match(source, /applyStructuredFilters/);
|
||||
assert.match(source, /class="w-\[380px\] max-w-\[calc\(100vw-24px\)\] gap-3 p-3"/);
|
||||
assert.match(source, /combineWhereInputs\(whereFilterInput\.value, appliedStructuredWhereInput\.value\)/);
|
||||
});
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
test("toolbar refresh preserves header sort order", () => {
|
||||
const match = source.match(/async function onToolbarRefresh\(\) \{([\s\S]*?)\n\}/);
|
||||
assert.ok(match, "DataGrid should define onToolbarRefresh");
|
||||
assert.match(match[1], /currentOrderBy\(\)/);
|
||||
assert.doesNotMatch(match[1], /orderByInput\.value\.trim\(\) \|\| undefined/);
|
||||
});
|
||||
|
||||
test("header sort starts from first page and top row", () => {
|
||||
const match = source.match(/function toggleSort\(colName: string, colIdx: number\) \{([\s\S]*?)\n\}/);
|
||||
assert.ok(match, "DataGrid should define toggleSort");
|
||||
assert.match(match[1], /currentPage\.value = 1/);
|
||||
assert.match(match[1], /resetGridVerticalScroll\(true\)/);
|
||||
assert.match(match[1], /syncOrderByInputWithSort\(colName, "asc"\)/);
|
||||
assert.match(match[1], /syncOrderByInputWithSort\(colName, "desc"\)/);
|
||||
});
|
||||
|
||||
test("header sort mirrors the active sort into ORDER BY input", () => {
|
||||
assert.match(
|
||||
source,
|
||||
/function syncOrderByInputWithSort\(column: string \| null, direction: "asc" \| "desc" \| null\)/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/orderByInput\.value = column && direction \? `\$\{queryColumnRef\(column\)\} \$\{direction\.toUpperCase\(\)\}` : ""/,
|
||||
);
|
||||
});
|
||||
|
||||
test("visible row numbers use display index after sorting", () => {
|
||||
assert.match(source, /displayIndex: number/);
|
||||
assert.match(source, /\.map\(\(item, displayIndex\) => \(\{ \.\.\.item, displayIndex \}\)\)/);
|
||||
assert.match(source, /<template #default="\{ item \}">/);
|
||||
assert.match(source, /\{\{ item\.displayIndex \+ 1 \}\}/);
|
||||
});
|
||||
|
||||
test("rollback refresh preserves header sort order", () => {
|
||||
const match = source.match(/function onToolbarRollback\(\) \{([\s\S]*?)\n\}/);
|
||||
assert.ok(match, "DataGrid should define onToolbarRollback");
|
||||
assert.match(match[1], /currentOrderBy\(\)/);
|
||||
assert.doesNotMatch(match[1], /orderByInput\.value\.trim\(\) \|\| undefined/);
|
||||
});
|
||||
|
||||
test("data result refresh preserves local column filters", () => {
|
||||
assert.doesNotMatch(source, /watch\(\s*\(\)\s*=>\s*props\.result,[\s\S]*?localColumnFilters\.value\s*=\s*\{\}/);
|
||||
assert.match(source, /localFilterScopeKey/);
|
||||
});
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
test("data grid wires copy and delete row shortcuts", () => {
|
||||
assert.match(source, /isCopyCurrentRowShortcut/);
|
||||
assert.match(source, /isDeleteCurrentRowShortcut/);
|
||||
assert.match(source, /copyCurrentRow\(\)/);
|
||||
assert.match(source, /deleteCurrentRow\(\)/);
|
||||
});
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const editorSource = readFileSync("apps/desktop/src/composables/useDataGridEditor.ts", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
const rustCoreLibSource = readFileSync("crates/dbx-core/src/lib.rs", "utf8");
|
||||
|
||||
test("shared API exposes backend data grid save preparation", () => {
|
||||
assert.match(apiSource, /export const prepareDataGridSave = forward\("prepareDataGridSave"\)/);
|
||||
assert.match(apiSource, /export const buildDataGridCopyUpdateStatements = forward\("buildDataGridCopyUpdateStatements"\)/);
|
||||
assert.match(apiSource, /export const buildDataGridContextFilterCondition = forward\("buildDataGridContextFilterCondition"\)/);
|
||||
assert.match(apiSource, /export const buildDataGridCountSql = forward\("buildDataGridCountSql"\)/);
|
||||
assert.match(apiSource, /export const buildHiveTablePropertiesSql = forward\("buildHiveTablePropertiesSql"\)/);
|
||||
assert.match(tauriSource, /export async function prepareDataGridSave\(/);
|
||||
assert.match(tauriSource, /invoke\("prepare_data_grid_save"/);
|
||||
assert.match(tauriSource, /invoke\("build_data_grid_copy_update_statements"/);
|
||||
assert.match(tauriSource, /invoke<string \| null>\("build_data_grid_context_filter_condition"/);
|
||||
assert.match(tauriSource, /invoke\("build_data_grid_count_sql"/);
|
||||
assert.match(tauriSource, /invoke\("build_hive_table_properties_sql"/);
|
||||
assert.match(httpSource, /export async function prepareDataGridSave\(/);
|
||||
assert.match(httpSource, /\/api\/query\/prepare-data-grid-save/);
|
||||
assert.match(httpSource, /\/api\/query\/build-data-grid-copy-update-statements/);
|
||||
assert.match(httpSource, /\/api\/query\/build-data-grid-context-filter-condition/);
|
||||
assert.match(httpSource, /\/api\/query\/build-data-grid-count-sql/);
|
||||
assert.match(httpSource, /\/api\/query\/build-hive-table-properties-sql/);
|
||||
});
|
||||
|
||||
test("data grid save flow uses backend save preparation", () => {
|
||||
assert.match(editorSource, /await api\.prepareDataGridSave\(stmtOptions\)/);
|
||||
assert.doesNotMatch(editorSource, /buildDataGridSaveStatements\(/);
|
||||
assert.doesNotMatch(editorSource, /buildDataGridRollbackStatements\(/);
|
||||
assert.doesNotMatch(editorSource, /validateDataGridSave\(/);
|
||||
});
|
||||
|
||||
test("Rust backends register data grid save preparation", () => {
|
||||
assert.match(rustCoreLibSource, /pub mod data_grid_sql/);
|
||||
assert.match(tauriLibSource, /commands::query::prepare_data_grid_save/);
|
||||
assert.match(tauriLibSource, /commands::query::build_data_grid_copy_update_statements/);
|
||||
assert.match(tauriLibSource, /commands::query::build_data_grid_context_filter_condition/);
|
||||
assert.match(tauriLibSource, /commands::query::build_data_grid_count_sql/);
|
||||
assert.match(tauriLibSource, /commands::query::build_hive_table_properties_sql/);
|
||||
assert.match(webMainSource, /\/query\/prepare-data-grid-save/);
|
||||
assert.match(webMainSource, /\/query\/build-data-grid-copy-update-statements/);
|
||||
assert.match(webMainSource, /\/query\/build-data-grid-context-filter-condition/);
|
||||
assert.match(webMainSource, /\/query\/build-data-grid-count-sql/);
|
||||
assert.match(webMainSource, /\/query\/build-hive-table-properties-sql/);
|
||||
});
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync(new URL("../../apps/desktop/src/components/grid/DataGrid.vue", import.meta.url), "utf8");
|
||||
const selectionSource = readFileSync(
|
||||
new URL("../../apps/desktop/src/composables/useDataGridSelection.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("data grid wires whole table, row, and column selection gestures", () => {
|
||||
assert.match(source, /@click="selectAllCells"/);
|
||||
assert.match(source, /@click="selectColumn\(colIdx, \$event\)"/);
|
||||
assert.match(source, /columnIsSelected\(colIdx\)/);
|
||||
assert.match(selectionSource, /function selectAllCells\(\)/);
|
||||
assert.match(selectionSource, /function selectColumn\(colIndex: number, event\?: MouseEvent\)/);
|
||||
assert.match(selectionSource, /lastClickedColumnIndex/);
|
||||
assert.match(selectionSource, /selectedColumnIndexes/);
|
||||
assert.match(selectionSource, /event\?\.metaKey \|\| event\?\.ctrlKey/);
|
||||
assert.match(selectionSource, /next\.has\(colIndex\)/);
|
||||
assert.match(selectionSource, /clearCellSelection\(\);\s+selectSingleCell\(rowIndex, colIndex\);/);
|
||||
assert.match(selectionSource, /if \(hasColumnSelection\.value\) clearCellSelection\(\);/);
|
||||
});
|
||||
|
||||
test("data grid intercepts copy and select-all shortcuts for grid selections", () => {
|
||||
assert.match(source, /clipboardShortcut\(event, "a"\)/);
|
||||
assert.match(source, /selectAllCells\(\)/);
|
||||
assert.match(source, /isTransposeMode\.value && hasRowSelection\.value/);
|
||||
assert.match(source, /copyRow\(\);/);
|
||||
assert.match(source, /if \(hasCellSelection\.value\) \{\s+copySelectionTsv\(\);/);
|
||||
assert.match(source, /copySelectedRowsTsv\(\)/);
|
||||
});
|
||||
|
||||
test("row number multi-selection reuses cell selection visuals", () => {
|
||||
assert.match(source, /function rowCellsUseSelectionVisual\(rowId: number\): boolean/);
|
||||
assert.match(source, /hasRowSelection\.value && isRowSelected\(rowId\) && !hasCellSelection\.value/);
|
||||
assert.match(source, /'row-cell-selected':/);
|
||||
assert.match(source, /'row-cell-selected-dirty':/);
|
||||
assert.match(source, /\.row-cell-selected \{/);
|
||||
assert.match(source, /\.row-cell-selected-dirty \{/);
|
||||
});
|
||||
|
||||
test("transpose cells reuse grid cell selection and details", () => {
|
||||
assert.match(source, /function selectTransposeCell\(rowIndex: number, actualColIdx: number, event: MouseEvent\)/);
|
||||
assert.match(source, /transposeCellIsSelected\(cell\.recordIndex, cell\.valueIndex\)/);
|
||||
assert.match(source, /@click="selectTransposeCell\(cell\.recordIndex, cell\.valueIndex, \$event\)"/);
|
||||
assert.match(source, /@contextmenu="onTransposeCellContext\(cell\.recordIndex, cell\.valueIndex, \$event\)"/);
|
||||
assert.match(source, /showCellDetails\(cell\.recordIndex, cell\.valueIndex\)/);
|
||||
});
|
||||
|
||||
test("transpose record headers copy selected records as rows", () => {
|
||||
assert.match(source, /function selectTransposeRecord\(rowIndex: number, event\?: MouseEvent\)/);
|
||||
assert.match(source, /function transposeRecordUsesSelectionVisual\(rowIndex: number\): boolean/);
|
||||
assert.match(source, /function transposeRecordUsesActiveHighlight\(rowIndex: number\): boolean/);
|
||||
assert.match(source, /function transposeRecordUsesFramedHeader\(rowIndex: number\): boolean/);
|
||||
assert.match(source, /transposeRecordUsesSelectionVisual\(/);
|
||||
assert.match(source, /transposeRecordUsesActiveHighlight\(/);
|
||||
assert.match(source, /transposeRecordUsesFramedHeader\(/);
|
||||
assert.match(source, /'transpose-record-header-selected text-primary font-semibold':/);
|
||||
assert.match(source, /'transpose-record-header-active text-primary':/);
|
||||
assert.match(source, /\.transpose-record-header-selected \{/);
|
||||
assert.match(source, /\.transpose-record-header-active \{/);
|
||||
assert.match(source, /'row-cell-selected':/);
|
||||
assert.match(source, /'row-cell-selected-dirty':/);
|
||||
assert.match(source, /handleRowClick\(rowIndex, item\.id, event\)/);
|
||||
assert.match(source, /@click="selectTransposeRecord\(recordIndex, \$event\)"/);
|
||||
assert.match(source, /@contextmenu="selectTransposeRecord\(recordIndex, \$event\)"/);
|
||||
assert.match(source, /function copyRowLabels\(\)/);
|
||||
assert.match(source, /const labels = copyRowLabels\(\)/);
|
||||
assert.match(source, /items\.push\(\{ label: labels\.row, action: copyRow \}\)/);
|
||||
assert.match(source, /t\("grid\.copyRows", \{ count \}\)/);
|
||||
assert.match(source, /t\("grid\.copyRow"\)/);
|
||||
});
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import { normalizeDataGridSaveError } from "../../apps/desktop/src/lib/dataGridSql.ts";
|
||||
|
||||
const dataGridSqlSource = readFileSync("apps/desktop/src/lib/dataGridSql.ts", "utf8");
|
||||
const dataGridExportSource = readFileSync("apps/desktop/src/composables/useDataGridExport.ts", "utf8");
|
||||
const columnFilterSource = readFileSync("apps/desktop/src/lib/dataGridColumnFilter.ts", "utf8");
|
||||
const gridSource = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
const rustSource = readFileSync("crates/dbx-core/src/data_grid_sql.rs", "utf8");
|
||||
|
||||
test("frontend data grid SQL helpers delegate executable SQL generation to backend APIs", () => {
|
||||
assert.match(dataGridSqlSource, /return api\.buildDataGridCopyUpdateStatements\(options\)/);
|
||||
assert.match(dataGridSqlSource, /return api\.buildDataGridCopyInsertStatement\(options\)/);
|
||||
assert.match(dataGridSqlSource, /return api\.buildDataGridContextFilterCondition\(options\)/);
|
||||
assert.match(dataGridSqlSource, /return api\.buildDataGridColumnValueFilterCondition\(options\)/);
|
||||
assert.match(dataGridSqlSource, /return api\.buildDataGridCountSql\(options\)/);
|
||||
assert.match(dataGridSqlSource, /return api\.buildHiveTablePropertiesSql\(options\)/);
|
||||
assert.doesNotMatch(dataGridSqlSource, /INSERT INTO|UPDATE .* SET|DELETE FROM|formatGridSqlLiteral|quoteTableIdentifier/);
|
||||
});
|
||||
|
||||
test("data grid copy and filter callers await backend SQL helpers", () => {
|
||||
assert.match(dataGridExportSource, /await buildDataGridCopyInsertStatement\(/);
|
||||
assert.match(dataGridExportSource, /await buildDataGridCopyUpdateStatements\(/);
|
||||
assert.match(columnFilterSource, /return buildDataGridColumnValueFilterCondition\(/);
|
||||
assert.match(gridSource, /await buildDataGridContextFilterCondition\(/);
|
||||
assert.match(gridSource, /await buildHiveTablePropertiesSql\(/);
|
||||
assert.match(gridSource, /await buildDataGridCountSql\(/);
|
||||
assert.doesNotMatch(gridSource, /formatGridSqlLiteral|SHOW TBLPROPERTIES|SELECT COUNT\(\*\) AS cnt/);
|
||||
});
|
||||
|
||||
test("Rust data grid SQL exposes copy and filter builders", () => {
|
||||
assert.match(rustSource, /pub fn build_data_grid_copy_update_statements/);
|
||||
assert.match(rustSource, /pub fn build_data_grid_copy_insert_statement/);
|
||||
assert.match(rustSource, /pub fn build_data_grid_context_filter_condition/);
|
||||
assert.match(rustSource, /pub fn build_data_grid_column_value_filter_condition/);
|
||||
assert.match(rustSource, /pub fn build_data_grid_count_sql/);
|
||||
assert.match(rustSource, /pub fn build_hive_table_properties_sql/);
|
||||
});
|
||||
|
||||
test("normalizes Hive ACID update and delete errors", () => {
|
||||
const error = normalizeDataGridSaveError(
|
||||
"hive",
|
||||
"Statement 1 failed: Agent RPC error (-1): Error while compiling statement: FAILED: SemanticException [Error 10294]: Attempt to do update or delete using transaction manager that does not support these operations.. Previous 0 statement(s) may have been committed.",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
error,
|
||||
"Hive UPDATE/DELETE are not enabled for this table or server. Add rows with INSERT, or enable ACID transactional tables in Hive before editing/deleting existing rows.",
|
||||
);
|
||||
});
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
test("data grid toolbar keeps a minimum content width", () => {
|
||||
assert.match(source, /class="data-grid-topbar-scroll shrink-0 overflow-x-auto/);
|
||||
assert.match(source, /class="data-grid-topbar flex items-stretch/);
|
||||
assert.match(source, /\.data-grid-topbar\s*\{\s*min-width: 760px;/s);
|
||||
assert.match(source, /\.data-grid-topbar-scroll\s*\{\s*scrollbar-width: thin;/s);
|
||||
});
|
||||
|
||||
test("data grid toolbar suggestions render outside the scroll container", () => {
|
||||
assert.match(source, /<Teleport to="body">/);
|
||||
assert.match(source, /class="fixed z-50 min-w-\[180px\] rounded-md border bg-popover/);
|
||||
assert.match(source, /:style="whereSuggestionStyle"/);
|
||||
assert.match(source, /:style="orderBySuggestionStyle"/);
|
||||
});
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
test("data grid uses lazy transpose rows and Tab keyboard toggle", () => {
|
||||
assert.match(source, /buildVisibleTransposeRows/);
|
||||
assert.match(source, /nextKeyboardTransposeState/);
|
||||
assert.match(source, /isToggleTransposeShortcut/);
|
||||
assert.match(source, /settingsStore\.editorSettings\.shortcuts/);
|
||||
});
|
||||
|
||||
test("transpose cells reuse inline editing controls", () => {
|
||||
assert.match(source, /canEditCellItem\(displayItems\[cell\.recordIndex\], cell\.valueIndex\)/);
|
||||
assert.match(source, /startEdit\(displayItems\[cell\.recordIndex\]\.id, cell\.valueIndex\)/);
|
||||
assert.match(source, /editingCell\?\.rowId === displayItems\[cell\.recordIndex\]\?\.id/);
|
||||
});
|
||||
|
||||
test("transpose mode follows appended rows and survives rollback refresh", () => {
|
||||
assert.match(source, /addRow:\s*addEditorRow/);
|
||||
assert.match(source, /function addRow\(\)[\s\S]*?focusAppendedTransposeRecord\(\)/);
|
||||
assert.match(source, /preserveTransposeOnNextResult/);
|
||||
assert.match(
|
||||
source,
|
||||
/function onToolbarRollback\(\)[\s\S]*?preserveTransposeOnNextResult\.value = showTranspose\.value/,
|
||||
);
|
||||
assert.match(source, /nextTransposeStateForRecordCount/);
|
||||
});
|
||||
|
||||
test("closing transpose scrolls the normal grid to the active record", () => {
|
||||
assert.match(source, /function currentTransposeViewportRowIndex\(\)/);
|
||||
assert.match(source, /transposeRowIndex\.value \?\? transposeRecordWindow\.value\.start/);
|
||||
assert.match(source, /function scrollGridRowIntoView\(rowIndex: number\)/);
|
||||
assert.match(source, /scrollToItem\?\.\(target\)/);
|
||||
assert.match(source, /function closeTranspose\(scrollToCurrentRecord = true\)/);
|
||||
assert.match(source, /closeTranspose\(false\)/);
|
||||
});
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
test("data grid does not memoize virtual rows", () => {
|
||||
assert.doesNotMatch(source, /v-memo=/);
|
||||
assert.doesNotMatch(source, /function rowRenderMemoDeps/);
|
||||
assert.match(source, /\{\{ item\.displayIndex \+ 1 \}\}/);
|
||||
});
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import { generateDatabaseExportId } from "../../apps/desktop/src/lib/databaseExport.ts";
|
||||
|
||||
const databaseExportSource = readFileSync("apps/desktop/src/lib/databaseExport.ts", "utf8");
|
||||
const exportFormatsSource = readFileSync("apps/desktop/src/lib/exportFormats.ts", "utf8");
|
||||
const treeItemSource = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8");
|
||||
const objectBrowserSource = readFileSync("apps/desktop/src/components/objects/ObjectBrowser.vue", "utf8");
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const rustSource = readFileSync("crates/dbx-core/src/database_export.rs", "utf8");
|
||||
|
||||
test("generates export ids when crypto.randomUUID is unavailable", () => {
|
||||
const originalCrypto = globalThis.crypto;
|
||||
|
||||
try {
|
||||
Object.defineProperty(globalThis, "crypto", {
|
||||
configurable: true,
|
||||
value: {},
|
||||
});
|
||||
|
||||
assert.match(generateDatabaseExportId(), /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, "crypto", {
|
||||
configurable: true,
|
||||
value: originalCrypto,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("frontend database SQL export helpers delegate executable SQL generation to backend APIs", () => {
|
||||
assert.match(databaseExportSource, /return api\.buildExportInsertStatements\(options\)/);
|
||||
assert.match(databaseExportSource, /return api\.buildDatabaseSqlExport\(/);
|
||||
assert.match(exportFormatsSource, /return api\.buildExportSqlInsert\(/);
|
||||
assert.doesNotMatch(databaseExportSource, /INSERT INTO|formatSqlLiteral|replace\(\s*\/'/);
|
||||
assert.doesNotMatch(exportFormatsSource, /INSERT INTO|VALUES|quoteIdent|replace\(\s*\/'/);
|
||||
});
|
||||
|
||||
test("SQL export callers await backend INSERT builders", () => {
|
||||
assert.match(treeItemSource, /await formatSqlInsert\(/);
|
||||
assert.match(objectBrowserSource, /await formatSqlInsert\(/);
|
||||
});
|
||||
|
||||
test("shared API exposes backend database export SQL builders", () => {
|
||||
assert.match(apiSource, /export const buildExportInsertStatements = forward\("buildExportInsertStatements"\)/);
|
||||
assert.match(apiSource, /export const buildExportSqlInsert = forward\("buildExportSqlInsert"\)/);
|
||||
assert.match(apiSource, /export const buildDatabaseSqlExport = forward\("buildDatabaseSqlExport"\)/);
|
||||
assert.match(tauriSource, /invoke\("build_export_insert_statements"/);
|
||||
assert.match(tauriSource, /invoke\("build_export_sql_insert"/);
|
||||
assert.match(tauriSource, /invoke\("build_database_sql_export"/);
|
||||
assert.match(httpSource, /\/api\/query\/build-export-insert-statements/);
|
||||
assert.match(httpSource, /\/api\/query\/build-export-sql-insert/);
|
||||
assert.match(httpSource, /\/api\/query\/build-database-sql-export/);
|
||||
});
|
||||
|
||||
test("Rust database export SQL exposes INSERT and full export builders", () => {
|
||||
assert.match(rustSource, /pub fn format_export_sql_literal/);
|
||||
assert.match(rustSource, /pub fn build_export_insert_statements/);
|
||||
assert.match(rustSource, /pub fn build_export_sql_insert/);
|
||||
assert.match(rustSource, /pub fn build_database_sql_export/);
|
||||
});
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const connectionDialogSource = readFileSync("apps/desktop/src/components/connection/ConnectionDialog.vue", "utf8");
|
||||
const databaseIconSource = readFileSync("apps/desktop/src/components/icons/DatabaseIcon.vue", "utf8");
|
||||
|
||||
const expectedIconNames = [
|
||||
"databricks",
|
||||
"saphana",
|
||||
"teradata",
|
||||
"vertica",
|
||||
"firebird",
|
||||
"exasol",
|
||||
"gbase",
|
||||
"tdsql",
|
||||
"polardb",
|
||||
"greatsql",
|
||||
] as const;
|
||||
|
||||
test("new database profiles use dedicated icon identities", () => {
|
||||
for (const iconName of expectedIconNames) {
|
||||
assert.match(connectionDialogSource, new RegExp(`${iconName}: \\{[^}]*icon: "${iconName}"`, "s"));
|
||||
assert.match(databaseIconSource, new RegExp(`${iconName}: "${iconName}\\.webp"`));
|
||||
}
|
||||
});
|
||||
|
||||
test("new database favicon assets exist", () => {
|
||||
for (const iconName of expectedIconNames) {
|
||||
const iconPath = join("apps/desktop/public/icons/database", `${iconName}.webp`);
|
||||
assert.equal(existsSync(iconPath), true, `${iconPath} should exist`);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const searchSource = readFileSync("apps/desktop/src/lib/databaseSearch.ts", "utf8");
|
||||
const dialogSource = readFileSync("apps/desktop/src/components/search/DatabaseSearchDialog.vue", "utf8");
|
||||
const rustSearchSource = readFileSync("crates/dbx-core/src/database_search_sql.rs", "utf8");
|
||||
const rustCoreLibSource = readFileSync("crates/dbx-core/src/lib.rs", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
|
||||
test("shared API exposes backend database search SQL builders", () => {
|
||||
assert.match(apiSource, /export const buildDatabaseSearchSql = forward\("buildDatabaseSearchSql"\)/);
|
||||
assert.match(apiSource, /export const buildSearchResultWhere = forward\("buildSearchResultWhere"\)/);
|
||||
assert.match(tauriSource, /invoke\("build_database_search_sql"/);
|
||||
assert.match(tauriSource, /invoke\("build_search_result_where"/);
|
||||
assert.match(httpSource, /\/api\/query\/build-database-search-sql/);
|
||||
assert.match(httpSource, /\/api\/query\/build-search-result-where/);
|
||||
});
|
||||
|
||||
test("database search frontend delegates executable SQL generation to backend APIs", () => {
|
||||
assert.match(searchSource, /return api\.buildDatabaseSearchSql\(options\)/);
|
||||
assert.match(searchSource, /return api\.buildSearchResultWhere\(options\)/);
|
||||
assert.match(dialogSource, /await buildDatabaseSearchSql\(/);
|
||||
assert.match(dialogSource, /await buildSearchResultWhere\(/);
|
||||
assert.doesNotMatch(searchSource, /function textCastExpression/);
|
||||
assert.doesNotMatch(searchSource, /function sqlValueLiteral/);
|
||||
assert.doesNotMatch(searchSource, /SELECT \* FROM/);
|
||||
});
|
||||
|
||||
test("Rust backends register database search SQL builders", () => {
|
||||
assert.match(rustCoreLibSource, /pub mod database_search_sql/);
|
||||
assert.match(rustSearchSource, /pub fn build_database_search_sql/);
|
||||
assert.match(rustSearchSource, /pub fn build_search_result_where/);
|
||||
assert.match(tauriLibSource, /commands::query::build_database_search_sql/);
|
||||
assert.match(tauriLibSource, /commands::query::build_search_result_where/);
|
||||
assert.match(webMainSource, /\/query\/build-database-search-sql/);
|
||||
assert.match(webMainSource, /\/query\/build-search-result-where/);
|
||||
});
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const objectRenameSource = readFileSync("apps/desktop/src/lib/objectRenameSql.ts", "utf8");
|
||||
const createDatabaseSource = readFileSync("apps/desktop/src/lib/createDatabaseSql.ts", "utf8");
|
||||
const dbAdminSource = readFileSync("apps/desktop/src/lib/dbAdminSql.ts", "utf8");
|
||||
const objectBrowserSource = readFileSync("apps/desktop/src/components/objects/ObjectBrowser.vue", "utf8");
|
||||
const treeItemSource = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8");
|
||||
const rustAdminSource = readFileSync("crates/dbx-core/src/db_admin_sql.rs", "utf8");
|
||||
const rustCoreLibSource = readFileSync("crates/dbx-core/src/lib.rs", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
|
||||
test("shared API exposes backend admin SQL builders", () => {
|
||||
assert.match(apiSource, /export const buildRenameObjectSql = forward\("buildRenameObjectSql"\)/);
|
||||
assert.match(apiSource, /export const buildCreateDatabaseSql = forward\("buildCreateDatabaseSql"\)/);
|
||||
assert.match(apiSource, /export const buildDuckDbAttachDatabaseSql = forward\("buildDuckDbAttachDatabaseSql"\)/);
|
||||
assert.match(apiSource, /export const buildDropObjectSql = forward\("buildDropObjectSql"\)/);
|
||||
assert.match(apiSource, /export const buildDuplicateTableStructureSql = forward\("buildDuplicateTableStructureSql"\)/);
|
||||
assert.match(tauriSource, /invoke\("build_rename_object_sql"/);
|
||||
assert.match(tauriSource, /invoke\("build_create_database_sql"/);
|
||||
assert.match(tauriSource, /invoke\("build_duckdb_attach_database_sql"/);
|
||||
assert.match(tauriSource, /invoke\("build_drop_object_sql"/);
|
||||
assert.match(tauriSource, /invoke\("build_duplicate_table_structure_sql"/);
|
||||
assert.match(httpSource, /\/api\/query\/build-rename-object-sql/);
|
||||
assert.match(httpSource, /\/api\/query\/build-create-database-sql/);
|
||||
assert.match(httpSource, /\/api\/query\/build-duckdb-attach-database-sql/);
|
||||
assert.match(httpSource, /\/api\/query\/build-drop-object-sql/);
|
||||
assert.match(httpSource, /\/api\/query\/build-duplicate-table-structure-sql/);
|
||||
});
|
||||
|
||||
test("frontend admin SQL helpers delegate executable SQL generation to backend APIs", () => {
|
||||
assert.match(objectRenameSource, /return api\.buildRenameObjectSql\(options\)/);
|
||||
assert.match(createDatabaseSource, /return api\.buildCreateDatabaseSql\(options\)/);
|
||||
assert.match(createDatabaseSource, /return api\.buildDuckDbAttachDatabaseSql\(path, name\)/);
|
||||
assert.match(dbAdminSource, /return api\.buildDropObjectSql\(options\)/);
|
||||
assert.match(dbAdminSource, /return api\.buildDuplicateTableStructureSql\(options\)/);
|
||||
assert.doesNotMatch(objectRenameSource, /EXEC sp_rename/);
|
||||
assert.doesNotMatch(objectRenameSource, /RENAME TABLE/);
|
||||
assert.doesNotMatch(createDatabaseSource, /CREATE DATABASE/);
|
||||
assert.doesNotMatch(createDatabaseSource, /ATTACH .* AS/);
|
||||
assert.doesNotMatch(dbAdminSource, /DROP |TRUNCATE |DELETE FROM|CREATE TABLE|CREATE SCHEMA/);
|
||||
});
|
||||
|
||||
test("admin SQL callers await backend builders and keep async previews out of templates", () => {
|
||||
assert.match(treeItemSource, /const sql = await buildRenameObjectSql\(/);
|
||||
assert.match(treeItemSource, /await buildDuckDbAttachDatabaseSql\(/);
|
||||
assert.match(treeItemSource, /const sql = await buildCreateDatabaseSql\(/);
|
||||
assert.match(treeItemSource, /await buildDuplicateTableStructureSql\(/);
|
||||
assert.match(treeItemSource, /dropTablePreviewSql/);
|
||||
assert.match(treeItemSource, /renameObjectPreviewSql/);
|
||||
assert.doesNotMatch(treeItemSource, /buildRenameObjectPreviewSql\(\)/);
|
||||
assert.doesNotMatch(treeItemSource, /CREATE TABLE .*LIKE|TRUNCATE TABLE|DROP SCHEMA|DROP DATABASE|CREATE SCHEMA/);
|
||||
assert.match(objectBrowserSource, /const sql = await buildRenameObjectSql\(/);
|
||||
assert.match(objectBrowserSource, /await buildDuplicateTableStructureSql\(/);
|
||||
assert.match(objectBrowserSource, /truncatePreviewSql/);
|
||||
assert.match(objectBrowserSource, /renamePreviewSqlText/);
|
||||
assert.doesNotMatch(objectBrowserSource, /renamePreviewSql\(\)/);
|
||||
assert.doesNotMatch(objectBrowserSource, /CREATE TABLE .*LIKE|TRUNCATE TABLE|DELETE FROM \$|DROP \$/);
|
||||
});
|
||||
|
||||
test("Rust backends register admin SQL builders", () => {
|
||||
assert.match(rustCoreLibSource, /pub mod db_admin_sql/);
|
||||
assert.match(rustAdminSource, /pub fn build_rename_object_sql/);
|
||||
assert.match(rustAdminSource, /pub fn build_create_database_sql/);
|
||||
assert.match(rustAdminSource, /pub fn build_duckdb_attach_database_sql/);
|
||||
assert.match(rustAdminSource, /pub fn build_drop_object_sql/);
|
||||
assert.match(rustAdminSource, /pub fn build_duplicate_table_structure_sql/);
|
||||
assert.match(tauriLibSource, /commands::query::build_rename_object_sql/);
|
||||
assert.match(tauriLibSource, /commands::query::build_create_database_sql/);
|
||||
assert.match(tauriLibSource, /commands::query::build_duckdb_attach_database_sql/);
|
||||
assert.match(tauriLibSource, /commands::query::build_drop_object_sql/);
|
||||
assert.match(tauriLibSource, /commands::query::build_duplicate_table_structure_sql/);
|
||||
assert.match(webMainSource, /\/query\/build-rename-object-sql/);
|
||||
assert.match(webMainSource, /\/query\/build-create-database-sql/);
|
||||
assert.match(webMainSource, /\/query\/build-duckdb-attach-database-sql/);
|
||||
assert.match(webMainSource, /\/query\/build-drop-object-sql/);
|
||||
assert.match(webMainSource, /\/query\/build-duplicate-table-structure-sql/);
|
||||
});
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
test("desktop settings fall back to the legacy run_in_background preference during upgrades", () => {
|
||||
const source = readFileSync("crates/dbx-core/src/storage.rs", "utf8");
|
||||
|
||||
assert.match(source, /\.or_else\(\|\| settings\.get\("run_in_background"\)\.and_then\(\|value\| value\.as_bool\(\)\)\)/);
|
||||
});
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
test("app startup eagerly loads desktop settings so tray preference is not reset in the UI", () => {
|
||||
const source = readFileSync("apps/desktop/src/App.vue", "utf8");
|
||||
|
||||
assert.match(source, /settingsStore\.initDesktopSettings\(\)\.catch\(\(\) => \{\}\);/);
|
||||
});
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
test("desktop capability allows setting the webview zoom", () => {
|
||||
const capability = JSON.parse(readFileSync("src-tauri/capabilities/default.json", "utf8")) as {
|
||||
permissions: string[];
|
||||
};
|
||||
|
||||
assert.equal(capability.permissions.includes("core:webview:allow-set-webview-zoom"), true);
|
||||
});
|
||||
|
||||
test("app logs webview zoom failures instead of swallowing them silently", () => {
|
||||
const source = readFileSync("apps/desktop/src/App.vue", "utf8");
|
||||
|
||||
assert.match(source, /getCurrentWebview\(\)\.setZoom\(scale\)/);
|
||||
assert.match(source, /console\.warn\("\[DBX\] Failed to apply UI scale"/);
|
||||
assert.match(source, /applyUiScale\(settingsStore\.editorSettings\.uiScale\)/);
|
||||
});
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const mountedOpenDialogs = [
|
||||
"apps/desktop/src/components/connection/ConnectionDialog.vue",
|
||||
"apps/desktop/src/components/transfer/DataTransferDialog.vue",
|
||||
"apps/desktop/src/components/diff/SchemaDiffDialog.vue",
|
||||
"apps/desktop/src/components/diff/DataCompareDialog.vue",
|
||||
"apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue",
|
||||
"apps/desktop/src/components/diagram/SchemaDiagramDialog.vue",
|
||||
"apps/desktop/src/components/import/TableImportDialog.vue",
|
||||
"apps/desktop/src/components/lineage/FieldLineageDialog.vue",
|
||||
"apps/desktop/src/components/search/DatabaseSearchDialog.vue",
|
||||
"apps/desktop/src/components/export/DatabaseExportDialog.vue",
|
||||
"apps/desktop/src/components/config/ConfigPassphraseDialog.vue",
|
||||
] as const;
|
||||
|
||||
test("dialogs initialized through v-if run their open watcher on mount", () => {
|
||||
for (const filePath of mountedOpenDialogs) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
assert.match(
|
||||
source,
|
||||
/watch\(\s*(open|dialogOpen),[\s\S]*?\{\s*immediate:\s*true\s*\},?\s*\)/,
|
||||
`${filePath} should use an immediate open watcher`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
test("dialog overlays scope backdrop filters to the open state", () => {
|
||||
const overlaySource = readFileSync("apps/desktop/src/components/ui/dialog/DialogOverlay.vue", "utf8");
|
||||
const scrollContentSource = readFileSync("apps/desktop/src/components/ui/dialog/DialogScrollContent.vue", "utf8");
|
||||
|
||||
assert.match(overlaySource, /data-open:supports-backdrop-filter:backdrop-blur-xs/);
|
||||
assert.match(scrollContentSource, /data-\[state=open\]:supports-backdrop-filter:backdrop-blur-xs/);
|
||||
assert.doesNotMatch(overlaySource, /\sbg-black\/10 duration-100 supports-backdrop-filter:backdrop-blur-xs/);
|
||||
assert.doesNotMatch(scrollContentSource, /\sbg-black\/10 supports-backdrop-filter:backdrop-blur-xs/);
|
||||
});
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import appPackage from "../../package.json" with { type: "json" };
|
||||
import downloadLinks from "../../docs/lib/downloadLinks.ts";
|
||||
|
||||
const { createInstallOptions } = downloadLinks;
|
||||
|
||||
test("docs install links are generated from the app package version", () => {
|
||||
const options = createInstallOptions("en", appPackage.version);
|
||||
const hrefs = options.map((option) => option.href);
|
||||
|
||||
assert.equal(hrefs.length, 5);
|
||||
assert.ok(hrefs.every((href) => href.includes(`/DBX_${appPackage.version}_`)));
|
||||
assert.equal(hrefs.some((href) => href.includes("0.5.9")), false);
|
||||
});
|
||||
|
||||
test("docs landing page reads latest release data instead of hard-coding the old latest update", () => {
|
||||
const source = readFileSync("docs/app/[lang]/page.tsx", "utf8");
|
||||
|
||||
assert.equal(source.includes("version: 'v0.5.4'"), false);
|
||||
assert.match(source, /LandingLatestUpdates/);
|
||||
assert.match(source, /fetchLatestReleaseInfo/);
|
||||
assert.match(source, /initialLatestRelease/);
|
||||
});
|
||||
|
||||
test("docs install widget reads the latest release version from latest.json at runtime", () => {
|
||||
const source = readFileSync("docs/components/landing/InstallTabs.tsx", "utf8");
|
||||
const latestRelease = readFileSync("docs/lib/latestRelease.ts", "utf8");
|
||||
|
||||
assert.equal(source.includes("fetchChangelog"), false);
|
||||
assert.match(source, /fetchLatestReleaseInfo/);
|
||||
assert.match(latestRelease, /latest\.json/);
|
||||
});
|
||||
|
||||
test("homepage update badge keeps latest.json version ahead of changelog tags", () => {
|
||||
const source = readFileSync("docs/components/landing/LandingLatestUpdates.tsx", "utf8");
|
||||
|
||||
assert.match(source, /Promise\.all\(\[fetchLatestReleaseInfo\(\), fetchChangelog\(lang\)\]\)/);
|
||||
assert.match(source, /releaseInfo \?\? initialLatestRelease/);
|
||||
});
|
||||
|
||||
test("docs changelog page loads releases in the browser from R2", () => {
|
||||
const source = readFileSync("docs/app/[lang]/changelog/page.tsx", "utf8");
|
||||
|
||||
assert.match(source, /initialData/);
|
||||
assert.match(source, /ChangelogRuntime/);
|
||||
});
|
||||
|
||||
test("legacy docs changelog pages do not keep stale release entries", () => {
|
||||
const en = readFileSync("docs/content/docs/changelog.mdx", "utf8");
|
||||
const cn = readFileSync("docs/content/docs/changelog.cn.mdx", "utf8");
|
||||
|
||||
assert.equal(en.includes("## v0.5.4"), false);
|
||||
assert.equal(cn.includes("## v0.5.4"), false);
|
||||
assert.match(en, /\/en\/changelog/);
|
||||
assert.match(cn, /\/cn\/changelog/);
|
||||
});
|
||||
|
||||
test("docs deploy reruns after package publishing refreshes latest release data", () => {
|
||||
const source = readFileSync(".github/workflows/docs.yml", "utf8");
|
||||
|
||||
assert.match(source, /workflow_run:/);
|
||||
assert.match(source, /workflows: \['Publish Packages'\]/);
|
||||
assert.match(source, /github\.event\.workflow_run\.conclusion == 'success'/);
|
||||
});
|
||||
|
||||
test("changelog sync does not configure bucket-level R2 CORS during upload", () => {
|
||||
const workflow = readFileSync(".github/workflows/sync-changelog.yml", "utf8");
|
||||
|
||||
assert.equal(existsSync(".github/r2-cors.json"), false);
|
||||
assert.equal(workflow.includes("put-bucket-cors"), false);
|
||||
assert.equal(workflow.includes("r2-cors.json"), false);
|
||||
});
|
||||
|
||||
test("changelog sync runs after package publishing promotes the release", () => {
|
||||
const workflow = readFileSync(".github/workflows/sync-changelog.yml", "utf8");
|
||||
|
||||
assert.match(workflow, /workflow_run:/);
|
||||
assert.match(workflow, /workflows: \['Publish Packages'\]/);
|
||||
assert.match(workflow, /github\.event\.workflow_run\.conclusion == 'success'/);
|
||||
assert.equal(workflow.includes("types: [published]"), false);
|
||||
});
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const dataGridSource = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
test("data grid mounts the cell detail action only for the active cell", () => {
|
||||
assert.match(dataGridSource, /cellDetailButtonVisible/);
|
||||
assert.match(dataGridSource, /v-if="cellDetailButtonVisible\(item\.displayIndex, actualColIdx\)"/);
|
||||
assert.match(dataGridSource, /@mouseenter="onCellMouseenter\(item\.displayIndex, visibleColIdx, actualColIdx\)"/);
|
||||
assert.doesNotMatch(dataGridSource, /group-hover\/cell:flex/);
|
||||
});
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/config/DriverStoreDialog.vue", "utf8");
|
||||
|
||||
test("driver store keeps the v0.5.12 grouped-list appearance", () => {
|
||||
assert.match(source, /max-w-4xl mx-auto px-6 py-6/);
|
||||
assert.match(source, /rounded-xl border bg-muted\/20 p-4/);
|
||||
assert.match(source, /rounded-md border divide-y/);
|
||||
assert.match(source, /flex items-center gap-3 px-4 py-2\.5 transition hover:bg-muted\/30/);
|
||||
});
|
||||
|
||||
test("driver store only keeps oval button styling from the later appearance pass", () => {
|
||||
assert.match(source, /rounded-full/);
|
||||
for (const className of [
|
||||
"driver-store-page",
|
||||
"driver-store-panel",
|
||||
"driver-store-list",
|
||||
"driver-store-row",
|
||||
"driver-store-icon",
|
||||
"driver-store-badge",
|
||||
"driver-store-action-primary",
|
||||
"driver-store-action-secondary",
|
||||
]) {
|
||||
assert.doesNotMatch(source, new RegExp(className));
|
||||
}
|
||||
assert.doesNotMatch(source, /<style scoped>/);
|
||||
});
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import {
|
||||
buildEditorFontThemeRules,
|
||||
buildSqlCompletionThemeRules,
|
||||
EDITOR_FONT_FAMILY_CSS_VAR,
|
||||
EDITOR_FONT_SIZE_CSS_VAR,
|
||||
} from "../../apps/desktop/src/lib/editorThemes.ts";
|
||||
|
||||
test("sql completion theme styles the autocomplete popup", () => {
|
||||
const rules = buildSqlCompletionThemeRules();
|
||||
|
||||
assert.deepEqual(rules[".cm-tooltip.cm-tooltip-autocomplete"], {
|
||||
background: "var(--popover)",
|
||||
border: "1px solid color-mix(in oklch, var(--border) 82%, var(--foreground) 18%)",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 8px 18px rgb(0 0 0 / 0.14)",
|
||||
color: "var(--popover-foreground)",
|
||||
fontFamily: `var(${EDITOR_FONT_FAMILY_CSS_VAR}, var(--font-mono, monospace))`,
|
||||
maxWidth: "min(520px, calc(100vw - 24px))",
|
||||
minWidth: "min(280px, calc(100vw - 24px))",
|
||||
overflow: "hidden",
|
||||
padding: "4px 0",
|
||||
});
|
||||
assert.deepEqual(rules[".cm-completionIcon"], {
|
||||
alignItems: "center",
|
||||
display: "inline-flex",
|
||||
flex: "0 0 15px",
|
||||
height: "15px",
|
||||
justifyContent: "center",
|
||||
marginRight: "0.65em",
|
||||
opacity: "1",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
width: "15px",
|
||||
});
|
||||
assert.equal(rules[".cm-completionIcon:before"]?.backgroundColor, "currentColor");
|
||||
assert.equal(rules[".cm-completionIcon:before"]?.content, "''");
|
||||
assert.equal(rules[".cm-completionIcon:before"]?.WebkitMaskSize, "14px 14px");
|
||||
assert.equal(rules[".cm-completionIcon:after"]?.display, "none");
|
||||
assert.equal(
|
||||
rules[".cm-completionIcon-table"]?.color,
|
||||
"color-mix(in oklch, var(--primary) 92%, var(--popover-foreground))",
|
||||
);
|
||||
assert.equal(
|
||||
rules[".cm-completionIcon-column"]?.color,
|
||||
"color-mix(in oklch, var(--blue-500, #3b82f6) 92%, var(--popover-foreground))",
|
||||
);
|
||||
assert.equal(
|
||||
rules[".cm-completionIcon-keyword"]?.color,
|
||||
"color-mix(in oklch, var(--orange-500, #f97316) 92%, var(--popover-foreground))",
|
||||
);
|
||||
assert.equal(
|
||||
rules[".cm-completionIcon-keyword"]?.["--dbx-completion-icon-mask"]?.includes("m16%2018%206-6-6-6"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
rules[".cm-completionIcon-snippet"]?.color,
|
||||
"color-mix(in oklch, var(--violet-500, #8b5cf6) 92%, var(--popover-foreground))",
|
||||
);
|
||||
assert.deepEqual(rules[".cm-completionLabel"], {
|
||||
color: "inherit",
|
||||
fontFamily: `var(${EDITOR_FONT_FAMILY_CSS_VAR}, var(--font-mono, monospace))`,
|
||||
fontSize: `clamp(12px, var(${EDITOR_FONT_SIZE_CSS_VAR}, 13px), 14px)`,
|
||||
fontWeight: "520",
|
||||
letterSpacing: "0",
|
||||
});
|
||||
assert.equal(rules[".cm-completionMatchedText"]?.color, "oklch(0.62 0.19 255)");
|
||||
assert.equal(
|
||||
rules[".cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected]"]?.background,
|
||||
"color-mix(in oklch, var(--primary) 14%, var(--popover)) !important",
|
||||
);
|
||||
});
|
||||
|
||||
test("query editor portals CodeMirror tooltips outside clipped editor panes", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/editor/QueryEditor.vue", "utf8");
|
||||
|
||||
assert.match(source, /tooltips/);
|
||||
assert.match(source, /tooltips\(\{\s*parent:\s*document\.body\s*\}\)/s);
|
||||
});
|
||||
|
||||
test("editor font theme reads size and family from CSS variables", () => {
|
||||
const rules = buildEditorFontThemeRules({ fixedHeight: true, scrollable: true });
|
||||
|
||||
assert.equal(rules["&"]?.height, "100%");
|
||||
assert.equal(rules["&"]?.fontSize, `var(${EDITOR_FONT_SIZE_CSS_VAR}, 13px)`);
|
||||
assert.deepEqual(rules[".cm-content"], {
|
||||
fontFamily: `var(${EDITOR_FONT_FAMILY_CSS_VAR}, monospace)`,
|
||||
lineHeight: "1.6",
|
||||
padding: "0",
|
||||
});
|
||||
assert.equal(rules[".cm-gutters"]?.fontSize, `var(${EDITOR_FONT_SIZE_CSS_VAR}, 13px)`);
|
||||
assert.deepEqual(rules[".cm-scroller"], { overflow: "auto" });
|
||||
});
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
test("frontend API exposes system font loading", () => {
|
||||
const api = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauri = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const http = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
|
||||
assert.match(api, /export const listSystemFonts = forward\("listSystemFonts"\)/);
|
||||
assert.match(tauri, /invoke\("list_system_fonts"\)/);
|
||||
assert.match(http, /export async function listSystemFonts\(\): Promise<string\[\]>/);
|
||||
});
|
||||
|
||||
test("settings editor font picker loads system fonts and accepts custom names", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/editor/EditorSettingsDialog.vue", "utf8");
|
||||
|
||||
assert.match(source, /listSystemFonts/);
|
||||
assert.match(source, /systemFontOptions/);
|
||||
assert.match(source, /allow-custom/);
|
||||
assert.match(source, /normalizeCustomFontFamilyInput/);
|
||||
assert.match(source, /settings\.useCustomFont/);
|
||||
assert.doesNotMatch(source, /settings\.customFontFamily/);
|
||||
});
|
||||
|
||||
test("Tauri registers the system font command", () => {
|
||||
const commands = readFileSync("src-tauri/src/commands/mod.rs", "utf8");
|
||||
const lib = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
|
||||
assert.match(commands, /pub mod system_fonts/);
|
||||
assert.match(lib, /commands::system_fonts::list_system_fonts/);
|
||||
});
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import {
|
||||
flattenExplainPlanNodes,
|
||||
parseExplainResult,
|
||||
supportsExplainPlan,
|
||||
} from "../../apps/desktop/src/lib/explainPlan.ts";
|
||||
|
||||
const explainPlanSource = readFileSync("apps/desktop/src/lib/explainPlan.ts", "utf8");
|
||||
const queryStoreSource = readFileSync("apps/desktop/src/stores/queryStore.ts", "utf8");
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
const rustSource = readFileSync("crates/dbx-core/src/query_execution_sql.rs", "utf8");
|
||||
|
||||
test("frontend explain SQL builder delegates executable SQL wrapping to backend API", () => {
|
||||
assert.match(explainPlanSource, /return api\.buildExplainSql\(\{ databaseType, sql \}\)/);
|
||||
assert.doesNotMatch(explainPlanSource, /EXPLAIN \(FORMAT JSON\)|EXPLAIN FORMAT=JSON|SAFE_EXPLAIN_RE|stripSqlComments/);
|
||||
assert.match(queryStoreSource, /await buildExplainSql\(databaseType, sql\)/);
|
||||
});
|
||||
|
||||
test("reports explain support by database type", () => {
|
||||
assert.equal(supportsExplainPlan("postgres"), true);
|
||||
assert.equal(supportsExplainPlan("mysql"), true);
|
||||
assert.equal(supportsExplainPlan("sqlite"), false);
|
||||
});
|
||||
|
||||
test("shared API exposes backend explain SQL builder", () => {
|
||||
assert.match(apiSource, /export const buildExplainSql = forward\("buildExplainSql"\)/);
|
||||
assert.match(tauriSource, /invoke\("build_explain_sql"/);
|
||||
assert.match(httpSource, /\/api\/query\/build-explain-sql/);
|
||||
assert.match(tauriLibSource, /commands::query::build_explain_sql/);
|
||||
assert.match(webMainSource, /\/query\/build-explain-sql/);
|
||||
});
|
||||
|
||||
test("Rust query execution SQL exposes explain builder", () => {
|
||||
assert.match(rustSource, /pub fn build_explain_sql/);
|
||||
assert.match(rustSource, /EXPLAIN \(FORMAT JSON\)/);
|
||||
assert.match(rustSource, /EXPLAIN FORMAT=JSON/);
|
||||
});
|
||||
|
||||
test("parses PostgreSQL FORMAT JSON output into plan nodes", () => {
|
||||
const plan = parseExplainResult("postgres", {
|
||||
columns: ["QUERY PLAN"],
|
||||
rows: [[[
|
||||
{
|
||||
Plan: {
|
||||
"Node Type": "Nested Loop",
|
||||
"Startup Cost": 0.42,
|
||||
"Total Cost": 42.9,
|
||||
"Plan Rows": 12,
|
||||
Plans: [
|
||||
{
|
||||
"Node Type": "Index Scan",
|
||||
"Relation Name": "users",
|
||||
"Index Name": "users_pkey",
|
||||
"Startup Cost": 0.28,
|
||||
"Total Cost": 8.3,
|
||||
"Plan Rows": 1,
|
||||
},
|
||||
{
|
||||
"Node Type": "Seq Scan",
|
||||
"Relation Name": "orders",
|
||||
"Filter": "(user_id = users.id)",
|
||||
"Total Cost": 31.2,
|
||||
"Plan Rows": 20,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 3,
|
||||
});
|
||||
|
||||
assert.equal(plan.nodes[0].title, "Nested Loop");
|
||||
assert.equal(plan.nodes[0].cost, "0.42..42.9");
|
||||
assert.equal(plan.nodes[0].rows, "12");
|
||||
assert.equal(plan.nodes[0].children[0].relation, "users");
|
||||
assert.equal(plan.nodes[0].children[0].index, "users_pkey");
|
||||
assert.equal(flattenExplainPlanNodes(plan.nodes).map((node) => node.nodeType).join(","), "Nested Loop,Index Scan,Seq Scan");
|
||||
});
|
||||
|
||||
test("parses MySQL FORMAT=JSON output into plan nodes", () => {
|
||||
const plan = parseExplainResult("mysql", {
|
||||
columns: ["EXPLAIN"],
|
||||
rows: [[JSON.stringify({
|
||||
query_block: {
|
||||
select_id: 1,
|
||||
nested_loop: [
|
||||
{
|
||||
table: {
|
||||
table_name: "users",
|
||||
access_type: "ref",
|
||||
key: "idx_users_email",
|
||||
rows_examined_per_scan: 3,
|
||||
cost_info: { query_cost: "1.20" },
|
||||
attached_condition: "users.email is not null",
|
||||
},
|
||||
},
|
||||
{
|
||||
table: {
|
||||
table_name: "orders",
|
||||
access_type: "ALL",
|
||||
rows_examined_per_scan: 200,
|
||||
cost_info: { read_cost: "18.00" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 2,
|
||||
});
|
||||
|
||||
const flat = flattenExplainPlanNodes(plan.nodes);
|
||||
assert.equal(flat[0].nodeType, "query_block");
|
||||
assert.equal(flat[1].title, "ref on users");
|
||||
assert.equal(flat[1].index, "idx_users_email");
|
||||
assert.equal(flat[1].cost, "1.20");
|
||||
assert.equal(flat[2].rows, "200");
|
||||
});
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const fileDropSource = readFileSync("apps/desktop/src/composables/useFileDrop.ts", "utf8");
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
const rustSource = readFileSync("crates/dbx-core/src/query_execution_sql.rs", "utf8");
|
||||
|
||||
test("file drop data preview delegates executable DuckDB SQL generation to backend API", () => {
|
||||
assert.match(fileDropSource, /return api\.buildDroppedFilePreviewSql\(\{ path \}\)/);
|
||||
assert.match(fileDropSource, /const dataQuery = await getDataFileQuery\(path\)/);
|
||||
assert.doesNotMatch(fileDropSource, /read_parquet|read_csv|read_json|SELECT \* FROM/);
|
||||
});
|
||||
|
||||
test("shared API exposes backend dropped file preview SQL builder", () => {
|
||||
assert.match(apiSource, /export const buildDroppedFilePreviewSql = forward\("buildDroppedFilePreviewSql"\)/);
|
||||
assert.match(tauriSource, /invoke<string \| null>\("build_dropped_file_preview_sql"/);
|
||||
assert.match(httpSource, /\/api\/query\/build-dropped-file-preview-sql/);
|
||||
assert.match(tauriLibSource, /commands::query::build_dropped_file_preview_sql/);
|
||||
assert.match(webMainSource, /\/query\/build-dropped-file-preview-sql/);
|
||||
});
|
||||
|
||||
test("Rust query execution SQL exposes dropped file preview builder", () => {
|
||||
assert.match(rustSource, /pub fn build_dropped_file_preview_sql/);
|
||||
assert.match(rustSource, /read_parquet/);
|
||||
assert.match(rustSource, /read_csv/);
|
||||
assert.match(rustSource, /read_json/);
|
||||
});
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const tauriConfig = JSON.parse(readFileSync("src-tauri/tauri.conf.json", "utf8")) as {
|
||||
bundle?: { macOS?: { entitlements?: string } };
|
||||
};
|
||||
|
||||
test("macOS bundle disables library validation for DuckDB extensions", () => {
|
||||
const entitlementsPath = tauriConfig.bundle?.macOS?.entitlements;
|
||||
|
||||
assert.equal(entitlementsPath, "Entitlements.plist");
|
||||
assert.equal(existsSync(`src-tauri/${entitlementsPath}`), true);
|
||||
|
||||
const entitlements = readFileSync(`src-tauri/${entitlementsPath}`, "utf8");
|
||||
assert.match(entitlements, /com\.apple\.security\.cs\.disable-library-validation/);
|
||||
assert.match(entitlements, /<true\/>/);
|
||||
});
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const objectSourceEditorSource = readFileSync("apps/desktop/src/lib/objectSourceEditor.ts", "utf8");
|
||||
const appSource = readFileSync("apps/desktop/src/App.vue", "utf8");
|
||||
const objectBrowserSource = readFileSync("apps/desktop/src/components/objects/ObjectBrowser.vue", "utf8");
|
||||
const treeItemSource = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8");
|
||||
const rustObjectSource = readFileSync("crates/dbx-core/src/object_source_sql.rs", "utf8");
|
||||
const rustCoreLibSource = readFileSync("crates/dbx-core/src/lib.rs", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
|
||||
test("shared API exposes backend object source SQL builders", () => {
|
||||
assert.match(apiSource, /export const buildExecutableObjectSourceStatements = forward\("buildExecutableObjectSourceStatements"\)/);
|
||||
assert.match(apiSource, /export const buildRoutineRenameObjectSourceStatements = forward\("buildRoutineRenameObjectSourceStatements"\)/);
|
||||
assert.match(tauriSource, /invoke\("build_executable_object_source_statements"/);
|
||||
assert.match(tauriSource, /invoke\("build_routine_rename_object_source_statements"/);
|
||||
assert.match(httpSource, /\/api\/query\/build-executable-object-source-statements/);
|
||||
assert.match(httpSource, /\/api\/query\/build-routine-rename-object-source-statements/);
|
||||
});
|
||||
|
||||
test("frontend object source editor delegates executable SQL generation to backend APIs", () => {
|
||||
assert.match(objectSourceEditorSource, /return api\.buildExecutableObjectSourceStatements\(input\)/);
|
||||
assert.match(objectSourceEditorSource, /return api\.buildRoutineRenameObjectSourceStatements\(input\)/);
|
||||
assert.doesNotMatch(objectSourceEditorSource, /function routineDeclaration/);
|
||||
assert.doesNotMatch(objectSourceEditorSource, /function replaceSqlRoutineDeclarationName/);
|
||||
assert.doesNotMatch(objectSourceEditorSource, /DROP .* IF EXISTS/);
|
||||
assert.match(appSource, /await buildExecutableObjectSourceStatements\(/);
|
||||
assert.match(objectBrowserSource, /await buildExecutableObjectSourceStatements\(/);
|
||||
assert.match(treeItemSource, /await buildRoutineRenameObjectSourceStatements\(/);
|
||||
});
|
||||
|
||||
test("Rust backends register object source SQL builders", () => {
|
||||
assert.match(rustCoreLibSource, /pub mod object_source_sql/);
|
||||
assert.match(rustObjectSource, /pub fn build_executable_object_source_statements/);
|
||||
assert.match(rustObjectSource, /pub fn build_routine_rename_object_source_statements/);
|
||||
assert.match(tauriLibSource, /commands::query::build_executable_object_source_statements/);
|
||||
assert.match(tauriLibSource, /commands::query::build_routine_rename_object_source_statements/);
|
||||
assert.match(webMainSource, /\/query\/build-executable-object-source-statements/);
|
||||
assert.match(webMainSource, /\/query\/build-routine-rename-object-source-statements/);
|
||||
});
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const coreConnectionSource = readFileSync(new URL("../../crates/dbx-core/src/connection.rs", import.meta.url), "utf8");
|
||||
const tauriConnectionSource = readFileSync(
|
||||
new URL("../../src-tauri/src/commands/connection.rs", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("Oracle agent fallback helper is shared with Tauri connection commands", () => {
|
||||
assert.match(coreConnectionSource, /pub fn should_retry_oracle_with_10g_driver/);
|
||||
assert.match(tauriConnectionSource, /should_retry_oracle_with_10g_driver/);
|
||||
});
|
||||
|
||||
test("connection test retries Oracle listener errors with the 10g profile", () => {
|
||||
assert.match(tauriConnectionSource, /async fn test_agent_connection/);
|
||||
assert.match(
|
||||
tauriConnectionSource,
|
||||
/call_daemon_method::<serde_json::Value>\([\s\S]*?AgentMethod::TestConnection[\s\S]*?should_retry_oracle_with_10g_driver/,
|
||||
);
|
||||
assert.match(tauriConnectionSource, /Some\("oracle-10g"\)/);
|
||||
});
|
||||
|
||||
test("initial connect retries Oracle listener errors with the 10g profile", () => {
|
||||
assert.match(tauriConnectionSource, /async fn connect_agent_pool/);
|
||||
assert.match(
|
||||
tauriConnectionSource,
|
||||
/call_method::<serde_json::Value>\([\s\S]*?AgentMethod::Connect[\s\S]*?should_retry_oracle_with_10g_driver/,
|
||||
);
|
||||
});
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
RESULT_PAGE_SIZE_OPTIONS,
|
||||
normalizeResultPageSize,
|
||||
resultPageSizeMenuOptions,
|
||||
} from "../../apps/desktop/src/lib/paginationPageSize.ts";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
test("normalizes query result page sizes into a safe range", () => {
|
||||
assert.equal(normalizeResultPageSize(undefined), 100);
|
||||
assert.equal(normalizeResultPageSize(0), 100);
|
||||
assert.equal(normalizeResultPageSize(-5), 100);
|
||||
assert.equal(normalizeResultPageSize(42.8), 42);
|
||||
assert.equal(normalizeResultPageSize(200000), 100000);
|
||||
});
|
||||
|
||||
test("query result page size menu includes the current custom value", () => {
|
||||
assert.deepEqual(RESULT_PAGE_SIZE_OPTIONS, [50, 100, 500, 1000]);
|
||||
assert.deepEqual(resultPageSizeMenuOptions(5000), [50, 100, 500, 1000, 5000]);
|
||||
assert.deepEqual(resultPageSizeMenuOptions(100), [50, 100, 500, 1000]);
|
||||
});
|
||||
|
||||
test("data grid page size menu exposes a custom input", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
assert.match(source, /customPageSizeInput/);
|
||||
assert.match(source, /settingsStore\.updateEditorSettings\(\{ pageSize: normalizedSize \}\)/);
|
||||
assert.match(source, /t\("grid\.customRowsPerPage"\)/);
|
||||
});
|
||||
|
||||
test("data grid page size follows the global editor setting", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
assert.match(source, /\(\) => settingsStore\.editorSettings\.pageSize/);
|
||||
assert.match(source, /pageSize\.value = normalizeResultPageSize\(value, pageSize\.value\)/);
|
||||
});
|
||||
|
||||
test("truncated result copy uses the active page size", () => {
|
||||
const gridSource = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
const zhSource = readFileSync("apps/desktop/src/i18n/locales/zh-CN.ts", "utf8");
|
||||
const enSource = readFileSync("apps/desktop/src/i18n/locales/en.ts", "utf8");
|
||||
|
||||
assert.match(gridSource, /const showTruncationWarning = computed/);
|
||||
assert.match(gridSource, /v-if="showTruncationWarning"/);
|
||||
assert.match(gridSource, /t\("grid\.truncatedHint", \{ count: pageSize \}\)/);
|
||||
assert.match(zhSource, /结果已截断,仅显示前 \{count\} 行/);
|
||||
assert.match(enSource, /Results truncated to \{count\} rows/);
|
||||
assert.doesNotMatch(zhSource, /仅显示前 10,000 行/);
|
||||
assert.doesNotMatch(enSource, /truncated to 10,000 rows/);
|
||||
});
|
||||
|
||||
test("query execution sends the selected page size to agent drivers", () => {
|
||||
const source = readFileSync("apps/desktop/src/stores/queryStore.ts", "utf8");
|
||||
|
||||
assert.match(source, /if \(tab\.mode === "data"\) \{/);
|
||||
assert.match(source, /pageLimit = settingsStore\.editorSettings\.pageSize/);
|
||||
assert.match(source, /maxRows: pageLimit/);
|
||||
assert.match(source, /fetchSize: pageLimit/);
|
||||
assert.match(source, /pageSize: pageLimit/);
|
||||
assert.match(source, /resultSessionId: options\?\.pagination\?\.sessionId/);
|
||||
assert.match(source, /clientSessionId: tab\.id/);
|
||||
assert.match(source, /const queryTimeoutSecs =/);
|
||||
assert.match(source, /conn\?\.query_timeout_secs/);
|
||||
assert.match(source, /timeoutSecs: queryTimeoutSecs/);
|
||||
assert.doesNotMatch(source, /maxRows: 10000,\s*fetchSize: pageLimit,\s*pageSize: pageLimit/s);
|
||||
});
|
||||
|
||||
test("native sql drivers receive the selected row limit", () => {
|
||||
const querySource = readFileSync("crates/dbx-core/src/query.rs", "utf8");
|
||||
const postgresSource = readFileSync("crates/dbx-core/src/db/postgres.rs", "utf8");
|
||||
const mysqlSource = readFileSync("crates/dbx-core/src/db/mysql.rs", "utf8");
|
||||
const sqliteSource = readFileSync("crates/dbx-core/src/db/sqlite.rs", "utf8");
|
||||
const sqlserverSource = readFileSync("crates/dbx-core/src/db/sqlserver.rs", "utf8");
|
||||
const clickhouseSource = readFileSync("crates/dbx-core/src/db/clickhouse_driver.rs", "utf8");
|
||||
|
||||
assert.match(querySource, /let max_rows = options\.max_rows/);
|
||||
assert.match(querySource, /db::postgres::execute_query_with_max_rows\(&p, sql, max_rows\)/);
|
||||
assert.match(querySource, /db::mysql::execute_query_with_max_rows\(&p, sql, bare, max_rows\)/);
|
||||
assert.match(querySource, /db::sqlite::execute_query_with_max_rows\(&p, sql, max_rows\)/);
|
||||
assert.match(querySource, /db::clickhouse_driver::execute_query_with_max_rows\(&client, &database, sql, max_rows\)/);
|
||||
assert.match(querySource, /db::sqlserver::execute_query_with_max_rows\(&mut client, sql, max_rows\)/);
|
||||
assert.match(querySource, /truncate_result_with_max_rows\(result, max_rows\)/);
|
||||
assert.match(postgresSource, /let row_limit = query_result_row_limit\(max_rows\)/);
|
||||
assert.match(mysqlSource, /let row_limit = query_result_row_limit\(max_rows\)/);
|
||||
assert.match(sqliteSource, /let row_limit = query_result_row_limit\(max_rows\)/);
|
||||
assert.match(sqlserverSource, /let row_limit = query_result_row_limit\(max_rows\)/);
|
||||
assert.match(clickhouseSource, /let row_limit = query_result_row_limit\(max_rows\)/);
|
||||
});
|
||||
|
||||
test("table data grid receives pagination context", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/layout/ContentArea.vue", "utf8");
|
||||
|
||||
assert.match(source, /:page-offset="activeTab\.resultPageOffset"/);
|
||||
assert.match(source, /:page-limit="activeTab\.resultPageLimit"/);
|
||||
});
|
||||
|
||||
test("data grid page size menu keeps the custom control compact", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
assert.match(source, /DropdownMenuContent align="end" class="w-36"/);
|
||||
assert.match(source, /class="h-7 w-24 text-xs tabular-nums/);
|
||||
assert.match(source, /:aria-label="t\('grid\.applyPageSize'\)"/);
|
||||
assert.doesNotMatch(source, /<Check class="h-3 w-3" \/>\s*\{\{ t\("grid\.applyPageSize"\) \}\}/);
|
||||
});
|
||||
|
||||
test("editor settings dialog does not duplicate result page size controls", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/editor/EditorSettingsDialog.vue", "utf8");
|
||||
|
||||
assert.doesNotMatch(source, /editPageSize/);
|
||||
assert.doesNotMatch(source, /settings\.resultPageSize/);
|
||||
assert.doesNotMatch(source, /pageSize: normalizeResultPageSize/);
|
||||
assert.doesNotMatch(source, /query-timeout-secs/);
|
||||
assert.doesNotMatch(source, /settings\.queryTimeoutSecs/);
|
||||
});
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("crates/dbx-core/src/db/postgres.rs", "utf8");
|
||||
|
||||
test("postgres table list SQL has no trailing comma before FROM", () => {
|
||||
assert.doesNotMatch(source, /AS table_comment,\s*\\\s*FROM pg_catalog\.pg_class/);
|
||||
assert.match(source, /obj_description\(c\.oid\) AS table_comment\s*\\\s*FROM pg_catalog\.pg_class/);
|
||||
});
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/editor/QueryEditor.vue", "utf8");
|
||||
const searchPanelSource = readFileSync("apps/desktop/src/components/editor/EditorSearchPanel.vue", "utf8");
|
||||
const appSource = readFileSync("apps/desktop/src/App.vue", "utf8");
|
||||
const cellDetailEditorSource = readFileSync("apps/desktop/src/composables/useCellDetailEditor.ts", "utf8");
|
||||
const contentAreaSource = readFileSync("apps/desktop/src/components/layout/ContentArea.vue", "utf8");
|
||||
const dataGridSource = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
const editorThemeSource = readFileSync("apps/desktop/src/lib/editorThemes.ts", "utf8");
|
||||
|
||||
test("query editor opens search and replace with configurable shortcuts", () => {
|
||||
assert.match(source, /shortcutToCodeMirrorKey\(shortcuts\.find\)/);
|
||||
assert.match(source, /shortcutToCodeMirrorKey\(shortcuts\.replace\)/);
|
||||
assert.match(source, /run:\s*openReplace/);
|
||||
assert.match(source, /defineExpose\(\{\s*openSearch,\s*openReplace,\s*scrollCursorIntoView\s*\}\)/);
|
||||
assert.match(searchPanelSource, /showReplace\.value\s*=\s*true/);
|
||||
assert.match(searchPanelSource, /replaceInputRef\.value\?\.focus\(\)/);
|
||||
assert.match(searchPanelSource, /defineExpose\(\{\s*openSearch,\s*openReplace,\s*closeSearch\s*\}\)/);
|
||||
});
|
||||
|
||||
test("query editor localizes the replace all button", () => {
|
||||
assert.match(searchPanelSource, /t\("editor\.search\.replaceAll"\)/);
|
||||
assert.doesNotMatch(searchPanelSource, />\s*全部\s*</);
|
||||
});
|
||||
|
||||
test("query editor no longer binds keyboard shortcuts for editor font zoom", () => {
|
||||
assert.doesNotMatch(source, /key:\s*"Mod-="/);
|
||||
assert.doesNotMatch(source, /key:\s*"Mod-\+"/);
|
||||
assert.doesNotMatch(source, /key:\s*"Mod--"/);
|
||||
assert.doesNotMatch(source, /key:\s*"Mod-0"/);
|
||||
});
|
||||
|
||||
test("query editor exposes a context menu for executing selected SQL", () => {
|
||||
assert.match(source, /CustomContextMenu/);
|
||||
assert.match(source, /v-slot="\{ onContextMenu \}"/);
|
||||
assert.match(source, /syncContextMenuState\(update\.view\)/);
|
||||
assert.match(source, /executeSelection/);
|
||||
assert.match(source, /copySelection/);
|
||||
assert.match(source, /selectAllSqlFromContextMenu/);
|
||||
assert.match(appSource, /\[data-context-menu\]/);
|
||||
});
|
||||
|
||||
test("query editor does not apply custom search match highlight styles", () => {
|
||||
assert.doesNotMatch(editorThemeSource, /"\.cm-searchMatch"/);
|
||||
assert.doesNotMatch(editorThemeSource, /"\.cm-searchMatch-selected"/);
|
||||
});
|
||||
|
||||
test("cell detail editor uses custom search panel with configurable shortcuts", () => {
|
||||
assert.match(cellDetailEditorSource, /shortcutToCodeMirrorKey\(shortcuts\.find\)/);
|
||||
assert.match(cellDetailEditorSource, /shortcutToCodeMirrorKey\(shortcuts\.replace\)/);
|
||||
assert.match(cellDetailEditorSource, /openSearch:\s*\(\)\s*=>\s*boolean/);
|
||||
assert.match(cellDetailEditorSource, /openReplace:\s*\(\)\s*=>\s*boolean/);
|
||||
assert.match(cellDetailEditorSource, /createApp\(EditorSearchPanel/);
|
||||
assert.match(cellDetailEditorSource, /searchApp\.use\(i18n\)/);
|
||||
assert.match(dataGridSource, /openCellDetailSearch/);
|
||||
});
|
||||
|
||||
test("data grid uses Mod-R for refresh instead of editor replace", () => {
|
||||
assert.match(dataGridSource, /data-grid-root/);
|
||||
assert.match(dataGridSource, /data-cell-detail-editor-root/);
|
||||
assert.match(dataGridSource, /if \(event\.defaultPrevented\) return/);
|
||||
assert.match(dataGridSource, /isModRShortcut\(event\)/);
|
||||
assert.match(dataGridSource, /await onToolbarRefresh\(\)/);
|
||||
});
|
||||
|
||||
test("app keydown routes Mod-R directly before browser reload handling", () => {
|
||||
assert.match(appSource, /if \(e\.defaultPrevented\) return/);
|
||||
assert.match(appSource, /isModRShortcut\(e\)/);
|
||||
assert.match(appSource, /contentAreaRef\.value\?\.handleModRTarget\(e\.target\)/);
|
||||
assert.match(contentAreaSource, /function handleModRTarget\(target: Element\): boolean/);
|
||||
assert.match(contentAreaSource, /queryEditorRef\.value\?\.openReplace\(\)/);
|
||||
assert.match(contentAreaSource, /dataGridRef\.value\?\.openCellDetailSearch\(\)/);
|
||||
assert.match(contentAreaSource, /if \(target\.closest\("\[data-grid-root\]"\)\) return refreshData\(\)/);
|
||||
});
|
||||
|
||||
test("app routes global UI zoom shortcuts across editor surfaces", () => {
|
||||
assert.match(appSource, /const shortcuts = settingsStore\.editorSettings\.shortcuts;/);
|
||||
assert.match(appSource, /isZoomInShortcut\(e, shortcuts\)/);
|
||||
assert.match(appSource, /isZoomOutShortcut\(e, shortcuts\)/);
|
||||
assert.match(appSource, /isResetZoomShortcut\(e, shortcuts\)/);
|
||||
assert.match(appSource, /isGlobalUiZoomTarget\(e\.target\)/);
|
||||
assert.match(appSource, /settingsStore\.updateEditorSettings\(\{\s*uiScale:\s*scale\s*\}\)/);
|
||||
assert.match(appSource, /\[data-query-editor-root\], \[data-cell-detail-editor-root\], \[data-object-source-editor\]/);
|
||||
});
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/editor/QueryEditor.vue", "utf8");
|
||||
|
||||
test("query editor reconfigures autocompletion when snippets change", () => {
|
||||
assert.match(source, /let completionComp: import\("@codemirror\/state"\)\.Compartment \| null = null;/);
|
||||
assert.match(
|
||||
source,
|
||||
/let buildSqlCompletionExtension: \(\(\) => import\("@codemirror\/state"\)\.Extension\) \| null = null;/,
|
||||
);
|
||||
assert.match(source, /completionComp = new Compartment\(\);/);
|
||||
assert.match(source, /completionComp\.of\(buildSqlCompletionExtension\(\)\)/);
|
||||
assert.match(source, /\(\) => settingsStore\.editorSettings\.snippets/);
|
||||
assert.match(source, /completionComp\.reconfigure\(buildSqlCompletionExtension\(\)\)/);
|
||||
assert.match(source, /codeMirrorStartCompletion\?\.\(view\.value\)/);
|
||||
});
|
||||
|
||||
test("query editor disables CodeMirror label re-filtering for custom SQL completions", () => {
|
||||
assert.match(source, /from: position - prefixLength,\s*filter: false,\s*options:/s);
|
||||
});
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/editor/QueryEditor.vue", "utf8");
|
||||
|
||||
test("query editor requests a fresh CodeMirror measure after live zoom updates", () => {
|
||||
assert.match(source, /syncEditorFontCssVars/);
|
||||
});
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/layout/ContentArea.vue", "utf8");
|
||||
|
||||
test("query result pane is hidden by default and can be reopened after output exists", () => {
|
||||
assert.match(source, /const resultsPaneOpen = ref\(false\)/);
|
||||
assert.match(source, /const hasQueryOutput = computed\(/);
|
||||
assert.match(source, /watch\(\s*hasQueryOutput,[\s\S]*?resultsPaneOpen\.value = true/);
|
||||
assert.match(source, /<Pane[\s\S]*:size="resultsPaneOpen \? 40 : 100"[\s\S]*>/);
|
||||
assert.match(source, /<Pane v-if="resultsPaneOpen"[\s\S]*:size="60"[\s\S]*>/);
|
||||
assert.match(source, /resultsPaneOpen = false/);
|
||||
assert.match(source, /v-if="hasQueryOutput && !resultsPaneOpen"/);
|
||||
assert.match(source, /resultsPaneOpen = true/);
|
||||
});
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const queryStoreSource = readFileSync("apps/desktop/src/stores/queryStore.ts", "utf8");
|
||||
const dataGridActionsSource = readFileSync("apps/desktop/src/composables/useDataGridActions.ts", "utf8");
|
||||
const rustCoreLibSource = readFileSync("crates/dbx-core/src/lib.rs", "utf8");
|
||||
const rustQueryResultSqlSource = readFileSync("crates/dbx-core/src/query_result_sql.rs", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
|
||||
test("shared API exposes backend query result SQL builders", () => {
|
||||
assert.match(
|
||||
apiSource,
|
||||
/export const prepareQueryPaginationExecutionPlan = forward\("prepareQueryPaginationExecutionPlan"\)/,
|
||||
);
|
||||
assert.match(apiSource, /export const buildSortedQuerySql = forward\("buildSortedQuerySql"\)/);
|
||||
|
||||
assert.match(tauriSource, /invoke\("prepare_query_pagination_execution_plan"/);
|
||||
assert.match(tauriSource, /invoke\("build_sorted_query_sql"/);
|
||||
|
||||
assert.match(httpSource, /\/api\/query\/prepare-pagination-plan/);
|
||||
assert.match(httpSource, /\/api\/query\/build-sorted-sql/);
|
||||
});
|
||||
|
||||
test("frontend query result pagination and sorting delegate to backend APIs", () => {
|
||||
assert.match(queryStoreSource, /await api\.prepareQueryPaginationExecutionPlan\(/);
|
||||
assert.match(dataGridActionsSource, /await api\.buildSortedQuerySql\(/);
|
||||
|
||||
assert.doesNotMatch(queryStoreSource, /queryResultPagination/);
|
||||
assert.doesNotMatch(dataGridActionsSource, /queryResultSort/);
|
||||
});
|
||||
|
||||
test("Rust backends register query result SQL builders", () => {
|
||||
assert.match(rustCoreLibSource, /pub mod query_result_sql/);
|
||||
assert.match(rustQueryResultSqlSource, /pub fn build_query_pagination_execution_plan/);
|
||||
assert.match(rustQueryResultSqlSource, /pub fn build_sorted_query_sql/);
|
||||
assert.match(tauriLibSource, /commands::query::prepare_query_pagination_execution_plan/);
|
||||
assert.match(tauriLibSource, /commands::query::build_sorted_query_sql/);
|
||||
assert.match(webMainSource, /\/query\/prepare-pagination-plan/);
|
||||
assert.match(webMainSource, /\/query\/build-sorted-sql/);
|
||||
});
|
||||
|
||||
test("SQL Server pagination keeps user-provided TOP clauses", () => {
|
||||
assert.match(rustQueryResultSqlSource, /fn add_sql_server_top/);
|
||||
assert.match(rustQueryResultSqlSource, /if has_top_level_select_top\(sql\)/);
|
||||
assert.match(rustQueryResultSqlSource, /SELECT TOP 1000 \* FROM TicketInfo/);
|
||||
});
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { createRedisShikiJsonHighlighter } from "../../apps/desktop/src/lib/redisJsonHighlighter.ts";
|
||||
|
||||
const redisViewerSource = readFileSync(
|
||||
join(process.cwd(), "apps/desktop/src/components/redis/RedisValueViewer.vue"),
|
||||
"utf8",
|
||||
);
|
||||
const jsonTreeSource = readFileSync(join(process.cwd(), "apps/desktop/src/components/redis/RedisJsonTree.vue"), "utf8");
|
||||
|
||||
test("Redis string values expose JSON and raw content views", () => {
|
||||
assert.match(redisViewerSource, /stringValueDetail/);
|
||||
assert.match(redisViewerSource, /stringValueView === 'json'/);
|
||||
assert.match(redisViewerSource, /RedisJsonTree/);
|
||||
assert.match(redisViewerSource, /redis\.jsonView/);
|
||||
assert.match(redisViewerSource, /redis\.rawContent/);
|
||||
});
|
||||
|
||||
test("Redis JSON tree supports folding and word wrap", () => {
|
||||
assert.match(jsonTreeSource, /collapsedPaths/);
|
||||
assert.match(jsonTreeSource, /toggleCollapsed/);
|
||||
assert.match(jsonTreeSource, /wordWrap/);
|
||||
assert.match(jsonTreeSource, /ChevronRight/);
|
||||
assert.match(jsonTreeSource, /ChevronDown/);
|
||||
});
|
||||
|
||||
test("Redis JSON raw content uses Shiki highlighting safely", async () => {
|
||||
assert.match(redisViewerSource, /createRedisShikiJsonHighlighter/);
|
||||
assert.match(redisViewerSource, /:highlight-json="highlightRedisJson"/);
|
||||
assert.match(redisViewerSource, /v-html="memberRawJsonHtml"/);
|
||||
|
||||
const highlight = await createRedisShikiJsonHighlighter({ appearance: () => "dark" });
|
||||
const html = highlight('{"name":"<script>","active":true}');
|
||||
|
||||
assert.match(html, /style=/);
|
||||
assert.match(html, /(?:<|<)script(?:>|>)/);
|
||||
assert.doesNotMatch(html, /<script>/);
|
||||
assert.doesNotMatch(html, /<pre/);
|
||||
});
|
||||
|
||||
test("Redis collection and stream values are virtualized", () => {
|
||||
assert.match(redisViewerSource, /RecycleScroller/);
|
||||
assert.match(redisViewerSource, /DynamicScroller/);
|
||||
assert.match(redisViewerSource, /:items="collectionRows"/);
|
||||
assert.match(redisViewerSource, /:items="streamRows"/);
|
||||
assert.doesNotMatch(redisViewerSource, /v-for="\(item, idx\) in collectionItems"/);
|
||||
assert.doesNotMatch(redisViewerSource, /v-for="entry in data\.value"/);
|
||||
});
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
test("Redis key browser exposes a create key dialog for common value types", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
|
||||
assert.match(source, /<Plus class="h-3 w-3" \/>/);
|
||||
assert.match(source, /v-model:open="showCreateKeyDialog"/);
|
||||
assert.match(source, /createKeyTypeOptions/);
|
||||
assert.match(source, /value: "string"/);
|
||||
assert.match(source, /value: "hash"/);
|
||||
assert.match(source, /value: "list"/);
|
||||
assert.match(source, /value: "set"/);
|
||||
assert.match(source, /value: "zset"/);
|
||||
});
|
||||
|
||||
test("Redis command line opens in the right workspace instead of taking permanent row space", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
|
||||
assert.match(source, /activeSidePanel/);
|
||||
assert.match(source, /@click="openCommandPanel"/);
|
||||
assert.match(source, /<Tabs v-model="activeSidePanel"/);
|
||||
assert.match(source, /<TabsContent value="command"/);
|
||||
assert.match(source, /t\("redis\.keyDetail"\)/);
|
||||
assert.match(source, /t\("redis\.commandLine"\)/);
|
||||
assert.match(source, /data-redis-command-input/);
|
||||
assert.doesNotMatch(source, /<Terminal class=/);
|
||||
assert.doesNotMatch(source, /absolute inset-x-0 bottom-0/);
|
||||
assert.doesNotMatch(source, /<div class="min-h-9 flex items-center gap-1 px-2 border-b bg-muted\/20 shrink-0">/);
|
||||
});
|
||||
|
||||
test("Redis key list keeps metadata out of the browsing rows until a key is selected", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
|
||||
assert.doesNotMatch(source, /t\("redis\.columnValue"\)/);
|
||||
assert.doesNotMatch(source, /t\("redis\.columnSize"\)/);
|
||||
assert.doesNotMatch(source, /t\("redis\.columnTTL"\)/);
|
||||
assert.match(source, /:metadata="selectedKey"/);
|
||||
});
|
||||
|
||||
test("Redis key list uses readable leaf rows with stable key icons", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
|
||||
assert.match(source, /:item-size="30"/);
|
||||
assert.match(source, /:style="\{ height: '30px' \}"/);
|
||||
assert.match(source, /text-\[13px\]/);
|
||||
assert.match(source, /class="h-3\.5 w-3\.5 text-muted-foreground\/70 transition-opacity group-hover:opacity-0"/);
|
||||
assert.match(source, /class="relative flex h-4 w-4 shrink-0 items-center justify-center"/);
|
||||
assert.match(source, /group-hover:opacity-0/);
|
||||
assert.match(source, /group-hover:opacity-100/);
|
||||
});
|
||||
|
||||
test("Redis key browser uses side-by-side panes for key list and value details", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
|
||||
assert.match(source, /<Splitpanes class="redis-workspace-splitpanes h-full">/);
|
||||
assert.doesNotMatch(source, /<Splitpanes class="h-full" horizontal>/);
|
||||
assert.match(source, /<Pane :size="36" :min-size="24">/);
|
||||
assert.match(source, /<Pane :size="64" :min-size="36">/);
|
||||
assert.doesNotMatch(source, /v-if="showSidePanel"/);
|
||||
});
|
||||
|
||||
test("Redis workspace tab bar aligns with the key browser toolbar height", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
|
||||
assert.match(source, /class="h-9 shrink-0 border-b bg-background px-3 flex items-center"/);
|
||||
assert.doesNotMatch(source, /class="h-10 shrink-0 border-b bg-muted\/20 px-3 flex items-center"/);
|
||||
});
|
||||
|
||||
test("Redis workspace tabs use compact shadcn tabs with icons and no underline", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
|
||||
assert.match(source, /<TabsList class="h-7 gap-1 p-0\.5"/);
|
||||
assert.doesNotMatch(source, /<TabsList variant="line"/);
|
||||
assert.doesNotMatch(source, /data-active:after:/);
|
||||
assert.doesNotMatch(source, /data-active:text-destructive/);
|
||||
assert.match(source, /<KeyRound class="size-3\.5"/);
|
||||
assert.match(source, /<TerminalSquare class="size-3\.5"/);
|
||||
assert.doesNotMatch(source, /grid h-7 w-52 grid-cols-2 p-0\.5/);
|
||||
});
|
||||
|
||||
test("Redis command workspace uses a terminal-like surface", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
|
||||
assert.match(source, /type RedisCommandHistoryEntry/);
|
||||
assert.match(source, /commandHistory/);
|
||||
assert.match(source, /isRedisClearScreenCommand/);
|
||||
assert.match(source, /commandHistory\.value = \[\]/);
|
||||
assert.match(source, /t\("redis\.commandWelcome"\)/);
|
||||
assert.match(source, /v-for="entry in commandHistory"/);
|
||||
assert.match(source, /{{ commandPrompt }}/);
|
||||
assert.match(source, /{{ entry\.prompt }}/);
|
||||
assert.match(source, /{{ entry\.command }}/);
|
||||
assert.match(
|
||||
source,
|
||||
/class="dbx-editor-font-family relative flex min-h-0 flex-1 flex-col bg-\[#090c10\] text-\[13px\] leading-5 text-slate-100"/,
|
||||
);
|
||||
assert.match(source, /class="flex shrink-0 items-center gap-2 border-t border-white\/10 bg-\[#090c10\] px-4 py-2"/);
|
||||
assert.match(
|
||||
source,
|
||||
/class="dbx-editor-font-family min-w-0 flex-1 border-0 bg-transparent p-0 text-\[13px\] text-slate-100 caret-\[#d7ba7d\] outline-none/,
|
||||
);
|
||||
assert.doesNotMatch(source, /class="border-b border-slate-800 bg-slate-950 px-4 py-3 text-slate-100"/);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/class="min-h-full whitespace-pre-wrap break-words rounded-md border border-slate-800 bg-slate-950/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Redis text surfaces inherit the configured editor font family without changing sizing classes", () => {
|
||||
const keyBrowserSource = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
const valueViewerSource = readFileSync("apps/desktop/src/components/redis/RedisValueViewer.vue", "utf8");
|
||||
const fontComposableSource = readFileSync("apps/desktop/src/composables/useEditorFontFamilyStyle.ts", "utf8");
|
||||
const globalStylesSource = readFileSync("apps/desktop/src/styles/globals.css", "utf8");
|
||||
|
||||
assert.match(fontComposableSource, /EDITOR_FONT_FAMILY_CSS_VAR/);
|
||||
assert.match(fontComposableSource, /\[EDITOR_FONT_FAMILY_CSS_VAR\]: settingsStore\.editorSettings\.fontFamily/);
|
||||
assert.match(globalStylesSource, /\.dbx-editor-font-family/);
|
||||
assert.match(globalStylesSource, /font-family: var\(--dbx-editor-font-family, var\(--font-mono, monospace\)\)/);
|
||||
|
||||
assert.match(keyBrowserSource, /const editorFontFamilyStyle = useEditorFontFamilyStyle\(\)/);
|
||||
assert.match(keyBrowserSource, /<div ref="rootRef" class="h-full" :style="editorFontFamilyStyle">/);
|
||||
assert.match(keyBrowserSource, /<DialogContent class="sm:max-w-md" :style="editorFontFamilyStyle">/);
|
||||
assert.match(keyBrowserSource, /class="dbx-editor-font-family truncate"/);
|
||||
assert.match(keyBrowserSource, /class="dbx-editor-font-family h-8 text-xs"/);
|
||||
assert.match(keyBrowserSource, /class="dbx-editor-font-family .* text-\[13px\] leading-5/);
|
||||
|
||||
assert.match(valueViewerSource, /const editorFontFamilyStyle = useEditorFontFamilyStyle\(\)/);
|
||||
assert.match(valueViewerSource, /<div class="h-full flex flex-col overflow-hidden" :style="editorFontFamilyStyle">/);
|
||||
assert.match(
|
||||
valueViewerSource,
|
||||
/:style="\[editorFontFamilyStyle, \{ width: `\$\{memberDetailSheetWidth\}px`, maxWidth: 'calc\(100vw - 2rem\)' \}\]"/,
|
||||
);
|
||||
assert.match(
|
||||
valueViewerSource,
|
||||
/class="dbx-editor-font-family min-h-0 flex-1 overflow-auto bg-background p-4 text-sm leading-6"/,
|
||||
);
|
||||
assert.match(
|
||||
valueViewerSource,
|
||||
/class="dbx-editor-font-family min-h-0 flex-1 resize-none bg-background p-5 text-\[13px\] leading-6 outline-none"/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Redis browser uses a single thin shared splitter between panes", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
|
||||
assert.match(source, /<Splitpanes class="redis-workspace-splitpanes h-full">/);
|
||||
assert.doesNotMatch(source, /class="h-full min-w-0 border-l bg-background flex flex-col overflow-hidden"/);
|
||||
assert.match(source, /\.redis-workspace-splitpanes :deep\(\.splitpanes--vertical > \.splitpanes__splitter\)/);
|
||||
assert.match(source, /width: 1px !important;/);
|
||||
});
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
test("Redis DB flush is exposed from the sidebar DB context menu", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8");
|
||||
|
||||
assert.match(source, /const showFlushRedisDbConfirm = ref\(false\)/);
|
||||
assert.match(source, /function flushRedisDb\(\)/);
|
||||
assert.match(source, /async function confirmFlushRedisDb\(\)/);
|
||||
assert.match(source, /await api\.redisFlushDb\(node\.connectionId, Number\(node\.database\)\)/);
|
||||
assert.match(source, /connectionStore\.updateRedisDbKeyStats\(node\.connectionId, Number\(node\.database\), \{ loaded: 0, total: 0 \}\)/);
|
||||
assert.match(source, /if \(node\.type === "redis-db" \|\| node\.type === "mongo-db"\) \{/);
|
||||
assert.match(source, /if \(node\.type === "redis-db"\) \{/);
|
||||
assert.match(source, /items\.push\(\{ label: t\("redis\.flushDb"\), action: flushRedisDb, icon: Eraser, variant: "destructive" as const \}\)/);
|
||||
assert.match(source, /t\("redis\.flushDb"\)/);
|
||||
assert.match(source, /v-model:open="showFlushRedisDbConfirm"/);
|
||||
assert.match(source, /:message="t\('redis\.flushDbMessage'\)"/);
|
||||
assert.match(source, /:details="t\('redis\.flushDbDetails', \{ db: node\.database \}\)"/);
|
||||
assert.match(source, /:confirm-label="t\('redis\.flushDbConfirm'\)"/);
|
||||
assert.match(source, /@confirm="confirmFlushRedisDb"/);
|
||||
});
|
||||
|
||||
test("Redis command panel no longer shows a flush DB button", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
|
||||
assert.doesNotMatch(source, /requestFlushDb/);
|
||||
assert.doesNotMatch(source, /redisFlushDb/);
|
||||
assert.doesNotMatch(source, /DatabaseZap/);
|
||||
assert.doesNotMatch(source, /redis\.flushDb/);
|
||||
});
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
test("Redis browser exposes key/value search modes", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
|
||||
assert.match(source, /type RedisSearchMode = "key" \| "value"/);
|
||||
assert.match(source, /searchMode\s*=\s*ref<RedisSearchMode>\("key"\)/);
|
||||
assert.match(source, /redisScanValues/);
|
||||
assert.match(source, /redis\.searchByKey/);
|
||||
assert.match(source, /redis\.searchByValue/);
|
||||
});
|
||||
|
||||
test("Redis key search input starts blank while scanning all keys internally", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
|
||||
assert.match(source, /searchPattern\s*=\s*ref\(""\)/);
|
||||
assert.match(source, /searchPattern\.value\.trim\(\) \|\| "\*"/);
|
||||
assert.doesNotMatch(source, /searchPattern\.value = "\*"/);
|
||||
});
|
||||
|
||||
test("Redis command input is visually distinct from search", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
|
||||
assert.match(source, /data-redis-command-input/);
|
||||
assert.match(source, /t\("redis\.commandWelcome"\)/);
|
||||
assert.match(source, /{{ commandPrompt }}/);
|
||||
assert.match(source, /ref="commandTerminalRef"/);
|
||||
assert.match(source, /@submit\.prevent="executeCommand"/);
|
||||
assert.match(source, /@keydown\.enter\.prevent="executeCommand"/);
|
||||
assert.match(source, /caret-\[#d7ba7d\]/);
|
||||
assert.doesNotMatch(source, /redis\.commandPrefix/);
|
||||
});
|
||||
|
||||
test("Redis value search streams incremental scan pages from the browser", () => {
|
||||
const browserSource = readFileSync("apps/desktop/src/components/redis/RedisKeyBrowser.vue", "utf8");
|
||||
const driverSource = readFileSync("crates/dbx-core/src/db/redis_driver.rs", "utf8");
|
||||
|
||||
assert.match(browserSource, /async function streamValueSearch/);
|
||||
assert.match(browserSource, /async function fillInitialKeyBatch/);
|
||||
assert.match(browserSource, /searchRequestId/);
|
||||
assert.match(browserSource, /redis\.searchingValues/);
|
||||
assert.match(browserSource, /flatKeys\.value\.length < targetCount/);
|
||||
assert.match(browserSource, /await fillInitialKeyBatch\(requestId\)/);
|
||||
assert.doesNotMatch(driverSource, /while\s+result\.len\(\)\s*<\s*target_count/);
|
||||
});
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
test("Redis value viewer uses a clearer two-line side panel header", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisValueViewer.vue", "utf8");
|
||||
|
||||
assert.match(source, /class="flex h-9 items-center gap-2 px-4"/);
|
||||
assert.match(source, /class="flex min-h-7 flex-wrap items-center gap-2 px-4 pb-1"/);
|
||||
assert.doesNotMatch(source, /class="h-9 flex items-center gap-2 px-4 border-b bg-muted\/30 shrink-0"/);
|
||||
});
|
||||
|
||||
test("Redis collection headers avoid unclear raw loaded over total suffixes", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/redis/RedisValueViewer.vue", "utf8");
|
||||
|
||||
assert.match(source, /function collectionCountLabel/);
|
||||
assert.doesNotMatch(source, /` \/ \$\{data\.total\}`/);
|
||||
assert.match(source, /collectionCountLabel\("items", collectionItems\.length, data\.total\)/);
|
||||
assert.match(source, /collectionCountLabel\("fields", collectionItems\.length, data\.total\)/);
|
||||
assert.match(source, /collectionCountLabel\("members", collectionItems\.length, data\.total\)/);
|
||||
});
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
test("settings about panel uses the app version prop instead of a hard-coded version", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/editor/EditorSettingsDialog.vue", "utf8");
|
||||
|
||||
assert.equal(source.includes("v0.5.0"), false);
|
||||
assert.match(source, /appVersion/);
|
||||
});
|
||||
|
||||
test("release workflow does not publish the default Tauri release body", () => {
|
||||
const source = readFileSync(".github/workflows/release.yml", "utf8");
|
||||
|
||||
assert.equal(source.includes("See the assets below to download and install."), false);
|
||||
});
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
test("release Linux runners install xdg-utils for AppImage bundling", () => {
|
||||
const workflow = readFileSync(".github/workflows/release.yml", "utf8");
|
||||
|
||||
assert.match(workflow, /sudo apt-get install -y [^\n]*\bxdg-utils\b/);
|
||||
});
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const schemaDiffDialogSource = readFileSync("apps/desktop/src/components/diff/SchemaDiffDialog.vue", "utf8");
|
||||
const schemaDiffSource = readFileSync("apps/desktop/src/lib/schemaDiff.ts", "utf8");
|
||||
const tauriCommandsSource = readFileSync("src-tauri/src/commands/mod.rs", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webRoutesSource = readFileSync("crates/dbx-web/src/routes/mod.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
const rustCoreLibSource = readFileSync("crates/dbx-core/src/lib.rs", "utf8");
|
||||
|
||||
test("shared API exposes backend schema diff preparation and SQL generation", () => {
|
||||
assert.match(apiSource, /export const prepareSchemaDiff = forward\("prepareSchemaDiff"\)/);
|
||||
assert.match(apiSource, /export const generateSchemaSyncSql = forward\("generateSchemaSyncSql"\)/);
|
||||
assert.match(tauriSource, /export async function prepareSchemaDiff\(/);
|
||||
assert.match(tauriSource, /invoke\("prepare_schema_diff"/);
|
||||
assert.match(tauriSource, /export async function generateSchemaSyncSql\(/);
|
||||
assert.match(tauriSource, /invoke\("generate_schema_sync_sql"/);
|
||||
assert.match(httpSource, /export async function prepareSchemaDiff\(/);
|
||||
assert.match(httpSource, /\/api\/schema-diff\/prepare/);
|
||||
assert.match(httpSource, /export async function generateSchemaSyncSql\(/);
|
||||
assert.match(httpSource, /\/api\/schema-diff\/generate-sync-sql/);
|
||||
});
|
||||
|
||||
test("schema diff dialog delegates diff and SQL generation to backend APIs", () => {
|
||||
assert.match(schemaDiffDialogSource, /await api\.prepareSchemaDiff\(/);
|
||||
assert.match(schemaDiffDialogSource, /await api\.generateSchemaSyncSql\(/);
|
||||
assert.doesNotMatch(schemaDiffDialogSource, /diffColumns\(/);
|
||||
assert.doesNotMatch(schemaDiffDialogSource, /diffIndexes\(/);
|
||||
assert.doesNotMatch(schemaDiffDialogSource, /diffForeignKeys\(/);
|
||||
assert.doesNotMatch(schemaDiffDialogSource, /diffTriggers\(/);
|
||||
assert.doesNotMatch(schemaDiffDialogSource, /generateSyncSql\(/);
|
||||
});
|
||||
|
||||
test("frontend schema diff module no longer owns executable diff logic", () => {
|
||||
assert.doesNotMatch(schemaDiffSource, /export function diffColumns/);
|
||||
assert.doesNotMatch(schemaDiffSource, /export function generateSyncSql/);
|
||||
});
|
||||
|
||||
test("Rust backends register schema diff APIs", () => {
|
||||
assert.match(rustCoreLibSource, /pub mod schema_diff/);
|
||||
assert.match(tauriCommandsSource, /pub mod schema_diff/);
|
||||
assert.match(tauriLibSource, /commands::schema_diff::prepare_schema_diff/);
|
||||
assert.match(tauriLibSource, /commands::schema_diff::generate_schema_sync_sql/);
|
||||
assert.match(webRoutesSource, /pub mod schema_diff/);
|
||||
assert.match(webMainSource, /\/schema-diff\/prepare/);
|
||||
assert.match(webMainSource, /\/schema-diff\/generate-sync-sql/);
|
||||
});
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
test("select content preserves Reka outside pointer-event lock by default", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/ui/select/SelectContent.vue", "utf8");
|
||||
|
||||
assert.match(source, /disableOutsidePointerEvents\?: boolean/);
|
||||
assert.match(source, /disableOutsidePointerEvents:\s*true/);
|
||||
});
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/editor/EditorSettingsDialog.vue", "utf8");
|
||||
const shortcutSource = readFileSync("apps/desktop/src/lib/shortcutRegistry.ts", "utf8");
|
||||
|
||||
test("settings dialog uses a side category navigation", () => {
|
||||
assert.match(source, /settingsCategoryNav/);
|
||||
assert.match(source, /settingsCategoryButton/);
|
||||
});
|
||||
|
||||
test("Redis scan size lives in its own settings category", () => {
|
||||
const redisTab = source.indexOf('value: "redis"');
|
||||
const redisContent = source.search(/activeSettingsTab === ['"]redis['"]/);
|
||||
const redisScanSetting = source.indexOf('t("settings.redisScanPageSize")');
|
||||
const editorContent = source.search(/activeSettingsTab === ['"]editor['"]/);
|
||||
|
||||
assert.ok(redisTab > -1);
|
||||
assert.ok(redisContent > -1);
|
||||
assert.ok(redisScanSetting > redisContent);
|
||||
assert.ok(redisScanSetting > editorContent);
|
||||
});
|
||||
|
||||
test("settings action footer stays at the bottom of the content pane", () => {
|
||||
assert.match(source, /class="[^"]*overflow-hidden[^"]*flex-col[^"]*"/);
|
||||
assert.match(source, /class="[^"]*overflow-y-auto[^"]*"/);
|
||||
assert.match(source, /<DialogFooter[\s\S]*class="[^"]*shrink-0[^"]*"/);
|
||||
assert.match(source, /<DialogFooter[\s\S]*class="[^"]*bg-transparent[^"]*"/);
|
||||
assert.doesNotMatch(source, /<DialogFooter[\s\S]*sticky/);
|
||||
assert.doesNotMatch(source, /<DialogFooter[\s\S]*bg-background/);
|
||||
});
|
||||
|
||||
test("settings dialog has a shortcuts category", () => {
|
||||
assert.match(source, /value: "shortcuts"/);
|
||||
assert.match(source, /activeSettingsTab === ['"]shortcuts['"]/);
|
||||
assert.match(source, /SHORTCUT_DEFINITIONS/);
|
||||
assert.match(shortcutSource, /settings\.shortcutToggleTranspose/);
|
||||
assert.match(shortcutSource, /settings\.shortcutCopyCurrentRow/);
|
||||
assert.match(shortcutSource, /settings\.shortcutDeleteCurrentRow/);
|
||||
});
|
||||
|
||||
test("settings editor theme preview can follow app appearance", () => {
|
||||
assert.match(source, /useTheme/);
|
||||
assert.match(source, /appAppearance: isDark\.value \? "dark" : "light"/);
|
||||
assert.match(source, /loadEditorTheme\(ss\.theme, ss\.appAppearance\)/);
|
||||
});
|
||||
|
||||
test("shortcut settings capture custom keydown input instead of fixed select options", () => {
|
||||
assert.match(source, /onShortcutKeydown/);
|
||||
assert.match(source, /@keydown="\(event: KeyboardEvent\) => onShortcutKeydown/);
|
||||
assert.doesNotMatch(source, /definition\.options/);
|
||||
});
|
||||
|
||||
test("shortcut conflicts only block applying changed shortcut settings", () => {
|
||||
assert.match(source, /const shortcutsChanged = computed/);
|
||||
assert.match(source, /const hasBlockingShortcutConflicts = computed/);
|
||||
assert.match(source, /shortcutsChanged\.value && hasShortcutConflicts\.value/);
|
||||
assert.match(source, /if \(hasBlockingShortcutConflicts\.value\) return/);
|
||||
assert.match(source, /:disabled="!hasChanges\(\) \|\| hasBlockingShortcutConflicts"/);
|
||||
});
|
||||
|
||||
test("settings dialog exposes separate apply and apply-and-close actions", () => {
|
||||
assert.match(source, /async function persistSettings\(\)/);
|
||||
assert.match(source, /async function applySettings\(\)/);
|
||||
assert.match(source, /async function applySettingsAndClose\(\)/);
|
||||
assert.match(source, /await persistSettings\(\);[\s\S]*emit\("update:open", false\)/);
|
||||
assert.match(source, /t\("settings\.applyAndClose"\)/);
|
||||
});
|
||||
|
||||
test("settings dialog exposes sidebar activation in navigation settings", () => {
|
||||
assert.match(source, /value: "navigation"/);
|
||||
assert.match(source, /activeSettingsTab === ['"]navigation['"]/);
|
||||
assert.match(source, /settings\.sidebarActivation/);
|
||||
assert.match(source, /settings\.autoSelectActiveSidebarNode/);
|
||||
assert.match(source, /editAutoSelectActiveSidebarNode/);
|
||||
assert.match(source, /<Switch id="auto-select-active-sidebar-node" v-model="editAutoSelectActiveSidebarNode"/);
|
||||
assert.match(source, /<Switch id="editor-word-wrap" v-model="editWordWrap"/);
|
||||
assert.doesNotMatch(source, /v-model:checked/);
|
||||
assert.match(source, /settings\.sidebarHiddenTablePrefixes/);
|
||||
assert.match(source, /editSidebarHiddenTablePrefixes/);
|
||||
assert.match(source, /focus-visible:ring-inset/);
|
||||
});
|
||||
|
||||
test("AI settings can browse provider model names while keeping manual input", () => {
|
||||
assert.match(source, /aiListModels/);
|
||||
assert.match(source, /<SearchableSelect[\s\S]*:options="aiModelOptionIds"/);
|
||||
assert.match(source, /v-model="aiEditModel"/);
|
||||
assert.match(source, /aiRefreshModels/);
|
||||
});
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const treeItemSource = readFileSync(join(process.cwd(), "apps/desktop/src/components/sidebar/TreeItem.vue"), "utf8");
|
||||
|
||||
function extractRefreshTableListBody(): string {
|
||||
const match = treeItemSource.match(/async function refreshTableList\(node: TreeNode\) \{([\s\S]*?)\n\}/);
|
||||
assert.ok(match, "TreeItem.vue should define refreshTableList");
|
||||
return match[1]!;
|
||||
}
|
||||
|
||||
test("object mutations refresh through the expansion-preserving object list path", () => {
|
||||
const body = extractRefreshTableListBody();
|
||||
|
||||
assert.match(body, /refreshObjectListTreeNode/);
|
||||
assert.doesNotMatch(body, /loadTables\(/);
|
||||
assert.doesNotMatch(body, /loadSqlServerDatabaseObjects\(/);
|
||||
});
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const treeItemSource = readFileSync(
|
||||
new URL("../../apps/desktop/src/components/sidebar/TreeItem.vue", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("connection group rename input keeps the normal sidebar text size", () => {
|
||||
const inputMatch = treeItemSource.match(/<input\s+[\s\S]*?ref="renameInputRef"[\s\S]*?class="([^"]+)"/);
|
||||
|
||||
assert.ok(inputMatch, "expected connection group rename input to have a class attribute");
|
||||
assert.ok(!inputMatch[1].split(/\s+/).includes("text-xs"), "rename input should not be smaller than the row label");
|
||||
});
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/sidebar/ConnectionTree.vue", "utf8");
|
||||
|
||||
test("connection tree exposes node search scopes instead of driver profile filters", () => {
|
||||
assert.match(source, /type SearchScope = "connection" \| "database" \| "schema" \| "table" \| "view"/);
|
||||
assert.match(source, /const selectedSearchScopes = ref<SearchScope\[]>\(\[\]\)/);
|
||||
assert.match(source, /filterSidebarTree\(nodes, q, searchCollapsedIds\.value, searchableNodeTypes\.value\)/);
|
||||
});
|
||||
|
||||
test("connection tree filter menu uses sidebar search scope i18n labels", () => {
|
||||
assert.match(source, /t\("sidebar\.searchScopeConnection"\)/);
|
||||
assert.match(source, /t\("sidebar\.searchScopeDatabase"\)/);
|
||||
assert.match(source, /t\("sidebar\.searchScopeTable"\)/);
|
||||
});
|
||||
|
||||
test("connection tree can select visible sidebar nodes for the active tab when enabled", () => {
|
||||
assert.match(source, /autoSelectActiveSidebarNode/);
|
||||
assert.match(source, /findSidebarNodeForActiveTab\(activeTab\.value, flatNodes\.value\)/);
|
||||
assert.match(source, /store\.selectedTreeNodeId = match\.id/);
|
||||
assert.match(source, /scrollTopForSidebarNode/);
|
||||
});
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8");
|
||||
|
||||
test("sidebar table prefix hiding is display-only", () => {
|
||||
assert.match(source, /sidebarDisplayTableName/);
|
||||
assert.match(source, /sidebarHiddenTablePrefixes/);
|
||||
assert.match(source, /node\.type === "table" \|\| node\.type === "view" \|\| node\.type === "mongo-collection"/);
|
||||
assert.match(source, /{{ visibleLabel\(node\) }}/);
|
||||
assert.match(source, /{{ displayLabel\(node\) }}/);
|
||||
});
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const queryStoreSource = readFileSync("apps/desktop/src/stores/queryStore.ts", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
const rustCoreLibSource = readFileSync("crates/dbx-core/src/lib.rs", "utf8");
|
||||
|
||||
test("shared API exposes backend SQL editability analysis", () => {
|
||||
assert.match(apiSource, /export const analyzeEditableQueryEditability = forward\("analyzeEditableQueryEditability"\)/);
|
||||
assert.match(tauriSource, /export async function analyzeEditableQueryEditability\(/);
|
||||
assert.match(tauriSource, /invoke\("analyze_editable_query_editability"/);
|
||||
assert.match(httpSource, /export async function analyzeEditableQueryEditability\(/);
|
||||
assert.match(httpSource, /\/api\/query\/analyze-editability/);
|
||||
});
|
||||
|
||||
test("query metadata analysis uses backend SQL editability analysis", () => {
|
||||
assert.match(queryStoreSource, /await api\.analyzeEditableQueryEditability\(sql\)/);
|
||||
assert.doesNotMatch(queryStoreSource, /analyzeEditableQueryEditability,\n\s+sourceColumnsForResult/);
|
||||
});
|
||||
|
||||
test("Rust backends register SQL editability analysis", () => {
|
||||
assert.match(rustCoreLibSource, /pub mod sql_editability/);
|
||||
assert.match(tauriLibSource, /commands::query::analyze_editable_query_editability/);
|
||||
assert.match(webMainSource, /\/query\/analyze-editability/);
|
||||
});
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const executionTargetSource = readFileSync("apps/desktop/src/lib/sqlExecutionTarget.ts", "utf8");
|
||||
const sqlSource = readFileSync("crates/dbx-core/src/sql.rs", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
|
||||
test("shared API exposes backend current statement resolution", () => {
|
||||
assert.match(apiSource, /export const findStatementAtCursor = forward\("findStatementAtCursor"\)/);
|
||||
assert.match(tauriSource, /export async function findStatementAtCursor\(/);
|
||||
assert.match(tauriSource, /invoke\("find_statement_at_cursor"/);
|
||||
assert.match(httpSource, /export async function findStatementAtCursor\(/);
|
||||
assert.match(httpSource, /\/api\/query\/find-statement-at-cursor/);
|
||||
});
|
||||
|
||||
test("execution target prefers backend current statement resolution", () => {
|
||||
assert.match(executionTargetSource, /api\.findStatementAtCursor\(fullSql, options\.cursorPos, options\.databaseType\)/);
|
||||
assert.doesNotMatch(executionTargetSource, /sqlStatementSplit/);
|
||||
assert.doesNotMatch(executionTargetSource, /return findStatementAtCursor\(fullSql, options\.cursorPos\)/);
|
||||
});
|
||||
|
||||
test("Rust backends register current statement resolution", () => {
|
||||
assert.match(sqlSource, /pub fn find_statement_at_cursor/);
|
||||
assert.match(tauriLibSource, /commands::query::find_statement_at_cursor/);
|
||||
assert.match(webMainSource, /\/query\/find-statement-at-cursor/);
|
||||
});
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const appSource = readFileSync("apps/desktop/src/App.vue", "utf8");
|
||||
const appDialogsSource = readFileSync("apps/desktop/src/components/layout/AppDialogs.vue", "utf8");
|
||||
const contentAreaSource = readFileSync("apps/desktop/src/components/layout/ContentArea.vue", "utf8");
|
||||
const i18nSource = readFileSync("apps/desktop/src/i18n/index.ts", "utf8");
|
||||
const mainSource = readFileSync("apps/desktop/src/main.ts", "utf8");
|
||||
|
||||
test("app defers cold-start side panels and modal pages behind async components", () => {
|
||||
assert.match(appSource, /defineAsyncComponent/);
|
||||
for (const component of ["AiAssistant", "QueryHistory", "DriverStorePage", "UpdateDialog", "LoginPage"]) {
|
||||
assert.doesNotMatch(appSource, new RegExp(`import ${component} from`));
|
||||
assert.match(appSource, new RegExp(`const ${component} = defineAsyncComponent`));
|
||||
}
|
||||
});
|
||||
|
||||
test("AI assistant panel stays closed on first launch to preserve startup memory", () => {
|
||||
assert.match(appSource, /const showAiPanel = ref\(safeLocalStorageGet\("dbx-ai-panel-open"\) === "true"\)/);
|
||||
});
|
||||
|
||||
test("dock side panels layer above the editor content", () => {
|
||||
assert.match(appSource, /\? 'flex-1 min-w-0 overflow-hidden'/);
|
||||
assert.match(appSource, /\? 'h-full shrink-0 relative z-30 isolate bg-background'/);
|
||||
assert.match(appSource, /: 'h-full shrink-0 relative z-30 isolate rounded-md border border-border\/80 bg-background'/);
|
||||
});
|
||||
|
||||
test("app dialogs keep non-primary dialogs out of the startup chunk", () => {
|
||||
for (const component of ["ConnectionDialog", "EditorSettingsDialog", "DangerConfirmDialog"]) {
|
||||
assert.doesNotMatch(appDialogsSource, new RegExp(`import ${component} from`));
|
||||
assert.match(appDialogsSource, new RegExp(`const ${component} = defineAsyncComponent`));
|
||||
}
|
||||
});
|
||||
|
||||
test("app dialogs only render async dialogs when their open state needs them", () => {
|
||||
assert.match(
|
||||
appDialogsSource,
|
||||
/const shouldShowConnectionDialog = computed\(\(\) => props\.showConnectionDialog \|\| !!editConfig\.value\)/,
|
||||
);
|
||||
assert.match(appDialogsSource, /<ConnectionDialog\s+v-if="shouldShowConnectionDialog"/);
|
||||
assert.match(appDialogsSource, /<EditorSettingsDialog\s+v-if="showSettingsDialog"/);
|
||||
assert.match(appDialogsSource, /<DangerConfirmDialog\s+v-if="showDangerDialog"/);
|
||||
assert.match(appDialogsSource, /<DataTransferDialog\s+v-if="dialogs\.showTransferDialog\.value"/);
|
||||
assert.match(appDialogsSource, /<SchemaDiagramDialog\s+v-if="dialogs\.showDiagramDialog\.value"/);
|
||||
assert.match(appDialogsSource, /<DatabaseExportDialog\s+v-if="dialogs\.showDatabaseExportDialog\.value"/);
|
||||
});
|
||||
|
||||
test("content area defers database-specific browsers behind async components", () => {
|
||||
assert.match(contentAreaSource, /defineAsyncComponent/);
|
||||
for (const component of ["DataGrid", "RedisKeyBrowser", "MongoDocBrowser", "ObjectBrowser"]) {
|
||||
assert.doesNotMatch(contentAreaSource, new RegExp(`import ${component} from`));
|
||||
assert.match(contentAreaSource, new RegExp(`const ${component} = defineAsyncComponent`));
|
||||
}
|
||||
});
|
||||
|
||||
test("i18n defers non-default locale payloads out of the startup chunk", () => {
|
||||
assert.doesNotMatch(i18nSource, /import en from "\.\/locales\/en"/);
|
||||
assert.doesNotMatch(i18nSource, /import es from "\.\/locales\/es"/);
|
||||
assert.match(i18nSource, /const localeLoaders/);
|
||||
assert.match(i18nSource, /en:\s*\(\)\s*=>\s*import\("\.\/locales\/en"\)/);
|
||||
assert.match(i18nSource, /es:\s*\(\)\s*=>\s*import\("\.\/locales\/es"\)/);
|
||||
assert.match(mainSource, /loadSavedLocale\(\)/);
|
||||
assert.match(mainSource, /app\.mount\("#root"\)/);
|
||||
});
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const tableSelectSource = readFileSync("apps/desktop/src/lib/tableSelectSql.ts", "utf8");
|
||||
const dataGridActionsSource = readFileSync("apps/desktop/src/composables/useDataGridActions.ts", "utf8");
|
||||
const navigationTargetsSource = readFileSync("apps/desktop/src/composables/useNavigationTargets.ts", "utf8");
|
||||
const dataGridSource = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
const rustSqlDialectSource = readFileSync("crates/dbx-core/src/sql_dialect.rs", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
|
||||
test("shared API exposes backend table select SQL builder", () => {
|
||||
assert.match(apiSource, /export const buildTableSelectSql = forward\("buildTableSelectSql"\)/);
|
||||
assert.match(tauriSource, /invoke\("build_table_select_sql"/);
|
||||
assert.match(httpSource, /\/api\/query\/build-table-select-sql/);
|
||||
});
|
||||
|
||||
test("frontend table select builder delegates to backend API", () => {
|
||||
assert.match(tableSelectSource, /return api\.buildTableSelectSql\(options\)/);
|
||||
assert.doesNotMatch(tableSelectSource, /function buildSqlServerTableSelectSql/);
|
||||
assert.doesNotMatch(tableSelectSource, /function buildNeo4jTableSelectSql/);
|
||||
assert.doesNotMatch(tableSelectSource, /ROW_NUMBER\(\) OVER/);
|
||||
});
|
||||
|
||||
test("table data callers await backend table select SQL", () => {
|
||||
assert.match(dataGridActionsSource, /await buildTableSql\(tab/);
|
||||
assert.match(navigationTargetsSource, /await buildTableSelectSql\(/);
|
||||
assert.match(dataGridSource, /await buildTableSelectSql\(/);
|
||||
});
|
||||
|
||||
test("Rust backends register table select SQL builder", () => {
|
||||
assert.match(rustSqlDialectSource, /pub fn build_table_data_select_sql/);
|
||||
assert.match(tauriLibSource, /commands::query::build_table_select_sql/);
|
||||
assert.match(webMainSource, /\/query\/build-table-select-sql/);
|
||||
});
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/structure/TableStructureEditor.vue", "utf8");
|
||||
const clickhouseSource = readFileSync("crates/dbx-core/src/db/clickhouse_driver.rs", "utf8");
|
||||
|
||||
test("column comments can be expanded into a multiline editor", () => {
|
||||
assert.match(source, /PopoverContent/);
|
||||
assert.match(source, /v-model="column\.comment"/);
|
||||
assert.match(source, /<textarea[\s\S]*v-model="column\.comment"/);
|
||||
assert.match(source, /t\("structureEditor\.editComment"\)/);
|
||||
});
|
||||
|
||||
test("structure editor keeps columns when optional metadata fails", () => {
|
||||
assert.match(source, /const nextColumns = await api\.getColumns/);
|
||||
assert.match(source, /api\.listIndexes[\s\S]*?\.catch\(\(\) => \[\]\)/);
|
||||
assert.match(source, /api\.listForeignKeys[\s\S]*?\.catch\(\(\) => \[\]\)/);
|
||||
assert.match(source, /api\.listTriggers[\s\S]*?\.catch\(\(\) => \[\]\)/);
|
||||
});
|
||||
|
||||
test("ClickHouse column metadata preserves comments for structure editing", () => {
|
||||
assert.match(clickhouseSource, /SELECT name, type, default_kind, default_expression, is_in_primary_key, comment/);
|
||||
assert.match(clickhouseSource, /comment:\s*row\.get\(5\)/);
|
||||
});
|
||||
|
||||
test("structure editor loads table metadata on mount", () => {
|
||||
assert.match(source, /async function loadStructure/);
|
||||
assert.match(source, /api\.getColumns/);
|
||||
assert.match(source, /api\.listIndexes/);
|
||||
assert.match(source, /api\.listForeignKeys/);
|
||||
assert.match(source, /api\.listTriggers/);
|
||||
});
|
||||
|
||||
test("structure editor gates controls through table structure capabilities", () => {
|
||||
assert.match(source, /getTableStructureCapabilities/);
|
||||
assert.match(source, /const structureCapabilities = computed/);
|
||||
assert.match(source, /function isColumnNameDisabled/);
|
||||
assert.match(source, /function isColumnTypeDisabled/);
|
||||
assert.match(source, /function isColumnDefaultDisabled/);
|
||||
assert.match(source, /function isColumnCommentDisabled/);
|
||||
assert.match(source, /function canDropColumn/);
|
||||
assert.match(source, /function canEditIndexDraft/);
|
||||
assert.match(source, /structureCapabilities\.value\.createIndex/);
|
||||
assert.match(source, /structureCapabilities\.value\.dropIndex/);
|
||||
assert.match(source, /structureCapabilities\.value\.indexInclude/);
|
||||
assert.match(source, /structureCapabilities\.value\.indexFilter/);
|
||||
});
|
||||
|
||||
test("structure editor exposes column order controls", () => {
|
||||
assert.match(source, /function moveColumn/);
|
||||
assert.match(source, /@click="moveColumn\(index, -1\)"/);
|
||||
assert.match(source, /@click="moveColumn\(index, 1\)"/);
|
||||
assert.match(source, /t\(['"]structureEditor\.moveColumnUp['"]\)/);
|
||||
assert.match(source, /t\(['"]structureEditor\.moveColumnDown['"]\)/);
|
||||
});
|
||||
|
||||
test("structure editor uses a dense wide layout for large tables", () => {
|
||||
assert.match(source, /grid-cols-\[minmax\(0,1fr\)_300px\]/);
|
||||
assert.match(source, /data-structure-density="compact"/);
|
||||
assert.match(source, /class="h-6 min-w-28 text-\[11px\]"/);
|
||||
assert.match(source, /w-36/);
|
||||
assert.match(source, /w-24/);
|
||||
});
|
||||
|
||||
test("structure editor keeps the table body vertically scrollable", () => {
|
||||
assert.match(source, /class="flex h-full min-h-0 flex-col gap-2 overflow-hidden p-3 text-\[11px\]"/);
|
||||
assert.match(source, /class="grid min-h-0 flex-1 grid-cols-\[minmax\(0,1fr\)_300px\] gap-2 overflow-hidden"/);
|
||||
assert.match(source, /class="min-h-0 min-w-0 overflow-hidden rounded-md border"/);
|
||||
assert.match(source, /class="flex h-full min-h-0 flex-col"/);
|
||||
assert.match(source, /<TabsContent value="columns" class="m-0 min-h-0 flex-1 overflow-auto p-0">/);
|
||||
});
|
||||
|
||||
test("new column input is focused after adding a field", () => {
|
||||
assert.match(source, /import \{ computed, nextTick, onMounted, ref, watch \} from "vue"/);
|
||||
assert.match(source, /async function addColumn/);
|
||||
assert.match(source, /activeTab\.value = "columns"/);
|
||||
assert.match(source, /await nextTick\(\)/);
|
||||
assert.match(source, /data-new-column-row="true"/);
|
||||
assert.match(source, /data-column-name-input/);
|
||||
assert.match(source, /input\?\.focus\(\)/);
|
||||
});
|
||||
|
||||
test("table comment input is disabled and shows tooltip when database does not support comments", () => {
|
||||
assert.match(source, /isTableCommentDisabled/);
|
||||
assert.match(source, /:disabled="isTableCommentDisabled"/);
|
||||
assert.match(source, /v-if="isTableCommentDisabled"/);
|
||||
assert.match(source, /Tooltip/);
|
||||
assert.match(source, /tableCommentUnsupported/);
|
||||
});
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const dialogSource = readFileSync("apps/desktop/src/components/structure/TableStructureEditor.vue", "utf8");
|
||||
const tableStructureTypesSource = readFileSync("apps/desktop/src/lib/tableStructureEditorSql.ts", "utf8");
|
||||
const rustCoreLibSource = readFileSync("crates/dbx-core/src/lib.rs", "utf8");
|
||||
const rustTableStructureSqlSource = readFileSync("crates/dbx-core/src/table_structure_sql.rs", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
|
||||
test("shared API exposes backend table structure SQL builders", () => {
|
||||
assert.match(apiSource, /export const buildTableStructureChangeSql = forward\("buildTableStructureChangeSql"\)/);
|
||||
assert.match(apiSource, /export const buildCreateTableSql = forward\("buildCreateTableSql"\)/);
|
||||
|
||||
assert.match(tauriSource, /invoke\("build_table_structure_change_sql"/);
|
||||
assert.match(tauriSource, /invoke\("build_create_table_sql"/);
|
||||
|
||||
assert.match(httpSource, /\/api\/query\/build-table-structure-change-sql/);
|
||||
assert.match(httpSource, /\/api\/query\/build-create-table-sql/);
|
||||
});
|
||||
|
||||
test("table structure editor delegates SQL preview generation to backend APIs", () => {
|
||||
assert.match(dialogSource, /await api\.buildCreateTableSql\(options\)/);
|
||||
assert.match(dialogSource, /await api\.buildTableStructureChangeSql\(options\)/);
|
||||
assert.doesNotMatch(dialogSource, /buildCreateTableSql,\s*buildTableStructureChangeSql/);
|
||||
});
|
||||
|
||||
test("frontend keeps table structure SQL file as types only", () => {
|
||||
assert.match(tableStructureTypesSource, /export interface EditableStructureColumn/);
|
||||
assert.match(tableStructureTypesSource, /export interface BuildTableStructureChangeSqlOptions/);
|
||||
assert.doesNotMatch(tableStructureTypesSource, /export function buildTableStructureChangeSql/);
|
||||
assert.doesNotMatch(tableStructureTypesSource, /export function buildCreateTableSql/);
|
||||
assert.doesNotMatch(tableStructureTypesSource, /function quoteIdent/);
|
||||
assert.doesNotMatch(tableStructureTypesSource, /function columnDefinition/);
|
||||
});
|
||||
|
||||
test("Rust backends register table structure SQL builders", () => {
|
||||
assert.match(rustCoreLibSource, /pub mod table_structure_sql/);
|
||||
assert.match(rustTableStructureSqlSource, /pub fn build_table_structure_change_sql/);
|
||||
assert.match(rustTableStructureSqlSource, /pub fn build_create_table_sql/);
|
||||
assert.match(tauriLibSource, /commands::query::build_table_structure_change_sql/);
|
||||
assert.match(tauriLibSource, /commands::query::build_create_table_sql/);
|
||||
assert.match(webMainSource, /\/query\/build-table-structure-change-sql/);
|
||||
assert.match(webMainSource, /\/query\/build-create-table-sql/);
|
||||
});
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import {
|
||||
buildGroupedObjectTreeNodes,
|
||||
buildTableTreeNodes,
|
||||
expandCachedObjectBrowserNodes,
|
||||
objectGroupRefreshParentId,
|
||||
} from "../../apps/desktop/src/lib/tableTree.ts";
|
||||
import type { ObjectInfo, TableInfo } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
const treeItemSource = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8");
|
||||
const connectionStoreSource = readFileSync("apps/desktop/src/stores/connectionStore.ts", "utf8");
|
||||
|
||||
function table(name: string, tableType: "TABLE" | "VIEW" = "TABLE"): TableInfo {
|
||||
return { name, table_type: tableType };
|
||||
}
|
||||
|
||||
function obj(name: string, objectType = "TABLE", schema = "public"): ObjectInfo {
|
||||
return {
|
||||
name,
|
||||
object_type: objectType,
|
||||
schema,
|
||||
};
|
||||
}
|
||||
|
||||
test("keeps every table as a sidebar node instead of truncating to object browser", () => {
|
||||
const tables: TableInfo[] = Array.from({ length: 16 }, (_, index) => table(`table_${index + 1}`));
|
||||
|
||||
const nodes = buildTableTreeNodes({
|
||||
nodeId: "conn:db",
|
||||
connectionId: "conn",
|
||||
database: "db",
|
||||
tables,
|
||||
});
|
||||
|
||||
assert.equal(nodes.length, 16);
|
||||
assert.equal(nodes.at(-1)?.label, "table_16");
|
||||
assert.equal(
|
||||
nodes.some((node) => node.type === "object-browser"),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("preserves table and view node types", () => {
|
||||
const nodes = buildTableTreeNodes({
|
||||
nodeId: "conn:db:public",
|
||||
connectionId: "conn",
|
||||
database: "db",
|
||||
schema: "public",
|
||||
tables: [table("users"), table("user_view", "VIEW")],
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
nodes.map((node) => [node.label, node.type, node.schema]),
|
||||
[
|
||||
["users", "table", "public"],
|
||||
["user_view", "view", "public"],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes padded table names from database drivers", () => {
|
||||
const nodes = buildTableTreeNodes({
|
||||
nodeId: "conn:db:public",
|
||||
connectionId: "conn",
|
||||
database: "db",
|
||||
schema: "public",
|
||||
tables: [table(" users "), table("\norders\t"), table(" ")],
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
nodes.map((node) => [node.id, node.label]),
|
||||
[
|
||||
["conn:db:public:users", "users"],
|
||||
["conn:db:public:orders", "orders"],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("object tree groups count unique objects when metadata returns duplicates", () => {
|
||||
const nodes = buildGroupedObjectTreeNodes({
|
||||
nodeId: "conn:app:public",
|
||||
connectionId: "conn",
|
||||
database: "app",
|
||||
schema: "public",
|
||||
objects: [
|
||||
obj("orders"),
|
||||
obj("orders"),
|
||||
obj("customers"),
|
||||
obj("active_orders", "VIEW"),
|
||||
obj("active_orders", "VIEW"),
|
||||
],
|
||||
});
|
||||
|
||||
const tableGroup = nodes.find((node) => node.type === "group-tables");
|
||||
assert.equal(tableGroup?.objectCount, 2);
|
||||
assert.deepEqual(
|
||||
tableGroup?.children?.map((child) => child.label),
|
||||
["orders", "customers"],
|
||||
);
|
||||
|
||||
const viewGroup = nodes.find((node) => node.type === "group-views");
|
||||
assert.equal(viewGroup?.objectCount, 1);
|
||||
assert.deepEqual(
|
||||
viewGroup?.children?.map((child) => child.label),
|
||||
["active_orders"],
|
||||
);
|
||||
});
|
||||
|
||||
test("object tree table nodes keep the schema returned by metadata", () => {
|
||||
const nodes = buildGroupedObjectTreeNodes({
|
||||
nodeId: "conn:app",
|
||||
connectionId: "conn",
|
||||
database: "app",
|
||||
schema: "app",
|
||||
objects: [obj("orders", "TABLE", "sales")],
|
||||
});
|
||||
|
||||
const tableNode = nodes.find((node) => node.type === "group-tables")?.children?.[0];
|
||||
|
||||
assert.equal(tableNode?.schema, "sales");
|
||||
});
|
||||
|
||||
test("expands cached object-browser nodes back into regular table nodes", () => {
|
||||
const nodes = expandCachedObjectBrowserNodes([
|
||||
{
|
||||
id: "conn:db:table_1",
|
||||
label: "table_1",
|
||||
type: "table",
|
||||
connectionId: "conn",
|
||||
database: "db",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "conn:db:__object_browser",
|
||||
label: "tree.objectBrowser",
|
||||
type: "object-browser",
|
||||
connectionId: "conn",
|
||||
database: "db",
|
||||
hiddenChildren: [
|
||||
{
|
||||
id: "conn:db:table_16",
|
||||
label: "table_16",
|
||||
type: "table",
|
||||
connectionId: "conn",
|
||||
database: "db",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
nodes.map((node) => [node.label, node.type]),
|
||||
[
|
||||
["table_1", "table"],
|
||||
["table_16", "table"],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("resolves grouped object refreshes to the parent schema node", () => {
|
||||
assert.equal(
|
||||
objectGroupRefreshParentId({
|
||||
id: "conn:db:public:__tables",
|
||||
label: "tree.tables",
|
||||
type: "group-tables",
|
||||
connectionId: "conn",
|
||||
database: "db",
|
||||
schema: "public",
|
||||
}),
|
||||
"conn:db:public",
|
||||
);
|
||||
});
|
||||
|
||||
test("table expander loads groups by the actual tree node id", () => {
|
||||
assert.match(
|
||||
treeItemSource,
|
||||
/loadTableGroups\(node\.connectionId,\s*node\.database,\s*node\.label,\s*node\.schema,\s*node\.id\)/,
|
||||
);
|
||||
});
|
||||
|
||||
test("view metadata groups only expose columns", () => {
|
||||
assert.match(
|
||||
connectionStoreSource,
|
||||
/if \(node\.type === "table"\) \{[\s\S]*type: "group-indexes"[\s\S]*type: "group-fkeys"[\s\S]*type: "group-triggers"/,
|
||||
);
|
||||
});
|
||||
|
||||
test("table metadata group expanders load by their actual tree node ids", () => {
|
||||
for (const [type, loader] of [
|
||||
["group-columns", "loadColumns"],
|
||||
["group-indexes", "loadIndexes"],
|
||||
["group-fkeys", "loadForeignKeys"],
|
||||
["group-triggers", "loadTriggers"],
|
||||
]) {
|
||||
assert.match(
|
||||
treeItemSource,
|
||||
new RegExp(
|
||||
`node\\.type === "${type}"[\\s\\S]*connectionStore\\.${loader}\\(node\\.connectionId,\\s*node\\.database,\\s*node\\.tableName,\\s*node\\.schema,\\s*node\\.id\\)`,
|
||||
),
|
||||
);
|
||||
assert.match(
|
||||
connectionStoreSource,
|
||||
new RegExp(
|
||||
`node\\.type === "${type}"[\\s\\S]*await ${loader}\\(node\\.connectionId,\\s*node\\.database,\\s*node\\.tableName,\\s*node\\.schema,\\s*node\\.id\\)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const gridExportSource = readFileSync("apps/desktop/src/composables/useDataGridExport.ts", "utf8");
|
||||
const tauriCommandsSource = readFileSync("src-tauri/src/commands/mod.rs", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webRoutesSource = readFileSync("crates/dbx-web/src/routes/mod.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
const rustCoreLibSource = readFileSync("crates/dbx-core/src/lib.rs", "utf8");
|
||||
|
||||
test("frontend API exposes backend JSON and Markdown export functions", () => {
|
||||
assert.match(apiSource, /export const exportQueryResultJson = forward\("exportQueryResultJson"\)/);
|
||||
assert.match(apiSource, /export const exportQueryResultMarkdown = forward\("exportQueryResultMarkdown"\)/);
|
||||
|
||||
assert.match(tauriSource, /export async function exportQueryResultJson\(/);
|
||||
assert.match(tauriSource, /invoke\("export_query_result_json"/);
|
||||
assert.match(tauriSource, /export async function exportQueryResultMarkdown\(/);
|
||||
assert.match(tauriSource, /invoke\("export_query_result_markdown"/);
|
||||
|
||||
assert.match(httpSource, /export async function exportQueryResultJson\(/);
|
||||
assert.match(httpSource, /\/api\/export\/query-result-json/);
|
||||
assert.match(httpSource, /export async function exportQueryResultMarkdown\(/);
|
||||
assert.match(httpSource, /\/api\/export\/query-result-markdown/);
|
||||
});
|
||||
|
||||
test("data grid JSON and Markdown exports use backend APIs", () => {
|
||||
assert.match(gridExportSource, /api\.exportQueryResultJson\(/);
|
||||
assert.match(gridExportSource, /api\.exportQueryResultMarkdown\(/);
|
||||
assert.doesNotMatch(gridExportSource, /formatJson\(/);
|
||||
assert.doesNotMatch(gridExportSource, /formatMarkdownTable/);
|
||||
});
|
||||
|
||||
test("Rust backends register JSON and Markdown export modules", () => {
|
||||
assert.match(rustCoreLibSource, /pub mod text_export/);
|
||||
assert.match(tauriCommandsSource, /pub mod text_export/);
|
||||
assert.match(tauriLibSource, /commands::text_export::export_query_result_json/);
|
||||
assert.match(tauriLibSource, /commands::text_export::export_query_result_markdown/);
|
||||
assert.match(webRoutesSource, /pub mod text_export/);
|
||||
assert.match(webMainSource, /\/export\/query-result-json/);
|
||||
assert.match(webMainSource, /\/export\/query-result-markdown/);
|
||||
});
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const viewDdlSource = readFileSync("apps/desktop/src/lib/viewDdl.ts", "utf8");
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
const rustSource = readFileSync("crates/dbx-core/src/object_source_sql.rs", "utf8");
|
||||
|
||||
test("frontend view DDL helper delegates executable DDL generation to backend API", () => {
|
||||
assert.match(viewDdlSource, /return api\.buildViewDdlSql\(input\)/);
|
||||
assert.doesNotMatch(viewDdlSource, /CREATE OR REPLACE VIEW|CREATE VIEW|ensureSemicolon|quotePostgresIdentifier/);
|
||||
});
|
||||
|
||||
test("sidebar view context menu exposes a separate DDL action", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8");
|
||||
|
||||
assert.match(source, /function viewObjectDdl/);
|
||||
assert.match(source, /await buildViewDdl\(/);
|
||||
assert.match(source, /contextMenu\.viewDdl/);
|
||||
});
|
||||
|
||||
test("object browser view context menu exposes a separate DDL action", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/objects/ObjectBrowser.vue", "utf8");
|
||||
|
||||
assert.match(source, /function openViewDdl/);
|
||||
assert.match(source, /await buildViewDdl\(/);
|
||||
assert.match(source, /contextMenu\.viewDdl/);
|
||||
});
|
||||
|
||||
test("shared API exposes backend view DDL builder", () => {
|
||||
assert.match(apiSource, /export const buildViewDdlSql = forward\("buildViewDdlSql"\)/);
|
||||
assert.match(tauriSource, /invoke\("build_view_ddl_sql"/);
|
||||
assert.match(httpSource, /\/api\/query\/build-view-ddl-sql/);
|
||||
assert.match(tauriLibSource, /commands::query::build_view_ddl_sql/);
|
||||
assert.match(webMainSource, /\/query\/build-view-ddl-sql/);
|
||||
});
|
||||
|
||||
test("Rust object source SQL exposes view DDL builder", () => {
|
||||
assert.match(rustSource, /pub struct BuildViewDdlInput/);
|
||||
assert.match(rustSource, /pub fn build_view_ddl_sql/);
|
||||
assert.match(rustSource, /CREATE OR REPLACE VIEW/);
|
||||
});
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const driverStoreSource = readFileSync("apps/desktop/src/components/config/DriverStoreDialog.vue", "utf8");
|
||||
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
|
||||
const webRoutesSource = readFileSync("crates/dbx-web/src/routes/mod.rs", "utf8");
|
||||
|
||||
const agentFunctions = [
|
||||
"listInstalledAgentsLocal",
|
||||
"listInstalledAgents",
|
||||
"installAgent",
|
||||
"upgradeAllAgents",
|
||||
"uninstallAgent",
|
||||
"getAgentJavaRuntimeConfig",
|
||||
"setAgentJavaRuntimeConfig",
|
||||
"invalidateAgentRegistryCache",
|
||||
"reinstallJre",
|
||||
"uninstallJre",
|
||||
"listenAgentInstallProgress",
|
||||
];
|
||||
|
||||
test("shared frontend API exposes agent driver management functions", () => {
|
||||
for (const name of agentFunctions) {
|
||||
assert.match(apiSource, new RegExp(`export const ${name} = forward\\("${name}"\\)`));
|
||||
assert.match(httpSource, new RegExp(`export async function ${name}\\b`));
|
||||
assert.match(tauriSource, new RegExp(`export async function ${name}\\b`));
|
||||
}
|
||||
assert.match(apiSource, /export const aiListModels = forward\("aiListModels"\)/);
|
||||
assert.match(httpSource, /export async function aiListModels\b/);
|
||||
assert.match(tauriSource, /export async function aiListModels\b/);
|
||||
});
|
||||
|
||||
test("web backend exposes agent driver management routes", () => {
|
||||
assert.match(webRoutesSource, /pub mod agents;/);
|
||||
assert.match(webMainSource, /\/agents\/installed-local/);
|
||||
assert.match(webMainSource, /\/agents\/install/);
|
||||
assert.match(webMainSource, /\/agents\/progress\/\{operationId\}/);
|
||||
assert.match(webMainSource, /\/agents\/java-runtime/);
|
||||
assert.match(webMainSource, /\/ai\/models/);
|
||||
});
|
||||
|
||||
test("web runtime supports importing an offline agent driver zip", () => {
|
||||
assert.match(httpSource, /export async function importAgentsFromZip\(fileOrPath: string \| File\)/);
|
||||
assert.match(httpSource, /FormData/);
|
||||
assert.match(httpSource, /\/api\/agents\/import-offline/);
|
||||
assert.doesNotMatch(httpSource, /Offline ZIP import is only available in the desktop app/);
|
||||
assert.match(webMainSource, /\/agents\/import-offline/);
|
||||
assert.match(driverStoreSource, /chooseWebOfflineZip/);
|
||||
assert.match(driverStoreSource, /accept = "\.zip"/);
|
||||
assert.doesNotMatch(driverStoreSource, /if \(isWeb \|\| importingZip\.value\) return;/);
|
||||
});
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const appSource = readFileSync("apps/desktop/src/App.vue", "utf8");
|
||||
const connectionDialogSource = readFileSync("apps/desktop/src/components/connection/ConnectionDialog.vue", "utf8");
|
||||
const driverStoreSource = readFileSync("apps/desktop/src/components/config/DriverStoreDialog.vue", "utf8");
|
||||
|
||||
function appSourceFiles(dir: string): string[] {
|
||||
return readdirSync(dir).flatMap((entry) => {
|
||||
const path = `${dir}/${entry}`;
|
||||
if (statSync(path).isDirectory()) return appSourceFiles(path);
|
||||
return /\.(ts|vue)$/.test(entry) ? [path] : [];
|
||||
});
|
||||
}
|
||||
|
||||
test("web runtime handles driver store open events", () => {
|
||||
assert.match(appSource, /showDriverStore\.value = true;/);
|
||||
assert.doesNotMatch(appSource, /if \(!isDesktop\) return;\s+showDriverStore\.value = true;/);
|
||||
});
|
||||
|
||||
test("web runtime can show driver install hints", () => {
|
||||
assert.match(connectionDialogSource, /showAgentDriverInstallHint\(form\.value\.db_type, agentDrivers\.value, selectedType\.value\)/);
|
||||
assert.doesNotMatch(connectionDialogSource, /isDesktop &&\s+showAgentDriverInstallHint/);
|
||||
});
|
||||
|
||||
test("driver store uses the shared API instead of direct Tauri calls", () => {
|
||||
assert.doesNotMatch(driverStoreSource, /@tauri-apps\/api\/core/);
|
||||
assert.doesNotMatch(driverStoreSource, /@tauri-apps\/api\/event/);
|
||||
assert.match(driverStoreSource, /api\.listInstalledAgents/);
|
||||
assert.match(driverStoreSource, /api\.listenAgentInstallProgress/);
|
||||
});
|
||||
|
||||
test("web runtime uses the shared uuid helper instead of direct randomUUID calls", () => {
|
||||
const directRandomUuidCalls = appSourceFiles("apps/desktop/src")
|
||||
.filter((path) => path !== "apps/desktop/src/lib/utils.ts")
|
||||
.filter((path) => readFileSync(path, "utf8").includes("crypto.randomUUID("));
|
||||
|
||||
assert.deepEqual(directRandomUuidCalls, []);
|
||||
});
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const tauriConfig = JSON.parse(readFileSync("src-tauri/tauri.conf.json", "utf8")) as {
|
||||
bundle?: { windows?: { nsis?: { installMode?: string; template?: string } } };
|
||||
};
|
||||
const nsisTemplate = readFileSync("src-tauri/windows/nsis/installer.nsi", "utf8");
|
||||
|
||||
test("Windows NSIS installer template is tracked by config", () => {
|
||||
const nsis = tauriConfig.bundle?.windows?.nsis;
|
||||
|
||||
assert.equal(nsis?.installMode, "currentUser");
|
||||
assert.equal(nsis?.template, "windows/nsis/installer.nsi");
|
||||
assert.equal(existsSync(join("src-tauri", nsis.template)), true);
|
||||
});
|
||||
|
||||
test("Windows upgrades preserve user data when reinstalling", () => {
|
||||
assert.match(nsisTemplate, /\$\{OrIf\} \$R0 = 1/);
|
||||
assert.match(nsisTemplate, /StrCpy \$R1 "\$R1 \/UPDATE"/);
|
||||
assert.match(nsisTemplate, /\$\{AndIf\} \$UpdateMode <> 1/);
|
||||
});
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
|
||||
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
|
||||
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
|
||||
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
|
||||
const gridExportSource = readFileSync("apps/desktop/src/composables/useDataGridExport.ts", "utf8");
|
||||
const treeItemSource = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8");
|
||||
const tauriCommandsSource = readFileSync("src-tauri/src/commands/mod.rs", "utf8");
|
||||
const tauriLibSource = readFileSync("src-tauri/src/lib.rs", "utf8");
|
||||
|
||||
const backendFn = "exportQueryResultXlsx";
|
||||
|
||||
test("frontend API exposes backend XLSX export function", () => {
|
||||
assert.match(apiSource, new RegExp(`export const ${backendFn} = forward\\("${backendFn}"\\)`));
|
||||
assert.match(tauriSource, /export async function exportQueryResultXlsx\(/);
|
||||
assert.match(tauriSource, /invoke\("export_query_result_xlsx"/);
|
||||
assert.match(httpSource, /export async function exportQueryResultXlsx\(/);
|
||||
});
|
||||
|
||||
test("UI uses backend XLSX export instead of frontend workbook builder", () => {
|
||||
assert.match(gridExportSource, /api\.exportQueryResultXlsx\(/);
|
||||
assert.doesNotMatch(gridExportSource, /buildXlsxWorkbook/);
|
||||
|
||||
assert.match(treeItemSource, /api\.exportQueryResultXlsx\(/);
|
||||
assert.doesNotMatch(treeItemSource, /buildXlsxWorkbook/);
|
||||
});
|
||||
|
||||
test("Tauri registers backend XLSX export command", () => {
|
||||
assert.match(tauriCommandsSource, /pub mod xlsx_export/);
|
||||
assert.match(tauriLibSource, /commands::xlsx_export::export_query_result_xlsx/);
|
||||
});
|
||||
Loading…
Reference in New Issue