fix(tests): remove low-value source snapshots and define test standards

This commit is contained in:
t8y2
2026-09-11 11:51:24 +08:00
parent ead906477f
commit 8186c697d5
80 changed files with 14 additions and 2225 deletions
+7
View File
@@ -104,6 +104,13 @@ pnpm test
For frontend or package changes, run the relevant package tests under `packages/` or `packages/app-tests/`.
Test quality matters more than test count:
- Exercise production functions or mounted components and assert observable results, state changes, errors, or emitted events. Mock external boundaries, not the behavior under test.
- Do not copy the implementation into a test or use source-string matching to pin class names, local variable names, template fragments, or helper-call spelling. These checks break on harmless refactors without proving runtime behavior. Check layout in a browser rather than inferring it from CSS strings.
- Extend the existing behavior suite for a regression instead of adding a second source-wiring snapshot. Use table-driven cases when only the inputs and expected outputs differ.
- File-content checks are appropriate for shipped artifacts, permissions, compatibility rules, and cross-runtime contracts. Keep safety guards until equivalent behavior coverage exists; do not delete a test merely because it reads files, uses mocks, or runs slowly.
### Documentation
User-facing docs live in two places:
+7
View File
@@ -104,6 +104,13 @@ pnpm test
如果改的是前端或某个 package,再补跑对应目录下的测试。
测试质量比测试数量更重要:
- 调用生产函数或挂载真实组件,验证可观察的结果、状态变化、错误或事件。Mock 外部边界,不要 Mock 正在验证的行为。
- 不要在测试中复制实现,也不要用源码字符串匹配锁定 class、局部变量名、模板片段或辅助函数调用写法。这类检查会阻碍无害重构,却不能证明运行行为;布局应在浏览器中验证,而不是从 CSS 字符串推断。
- 回归用例优先补进已有行为测试,不再另加一份源码接线快照。只有输入和预期不同的场景优先使用参数化用例。
- 发布产物、权限、兼容性规则和跨运行时契约可以使用文件内容检查。安全门禁在有等价行为覆盖前保留,不要仅因测试读文件、使用 Mock 或运行较慢就删除。
### 文档
用户文档主要分两块:
@@ -1,14 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const queryChartSource = readFileSync(new URL("../QueryChart.vue", import.meta.url), "utf8");
describe("QueryChart Y-axis selection", () => {
it("binds the visible checkbox state to the selected Y columns", () => {
expect(queryChartSource).toContain(':model-value="yColumnIndexes.includes(col.index)"');
expect(queryChartSource).toContain('@update:model-value="setYColumn(col.index, $event)"');
expect(queryChartSource).not.toContain(':checked="yColumnIndexes.includes(col.index)"');
expect(queryChartSource).not.toContain('@click="toggleYColumn(col.index)"');
expect(queryChartSource).not.toContain("CheckIcon");
});
});
@@ -1,11 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../CodeSnapshotDialog.vue", import.meta.url), "utf8");
describe("CodeSnapshotDialog layout", () => {
it("keeps the exported snapshot at its intrinsic width inside the scrollable preview", () => {
// 截图节点不能被外层 flex 布局压缩,否则长代码导出时会被裁切或错误换行。
expect(dialogSource).toContain('v-html="snapshotHtml" class="flex w-max min-w-[320px] flex-none"');
});
});
@@ -1,12 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync(new URL("../DriverStoreDialog.vue", import.meta.url), "utf8");
const templateStart = source.indexOf("<template>");
const template = source.slice(templateStart, source.indexOf("\n<style", templateStart)).trim();
describe("DriverStoreDialog visibility", () => {
it("keeps the page and its auxiliary dialog under one root so component v-show works", () => {
expect(template).toMatch(/^<template>\s*<div class="driver-store-view[^"]*">[\s\S]*<AgentOfflineExportDialog[^>]*\/>\s*<\/div>\s*<\/template>$/);
});
});
@@ -1,13 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../SchemaDiagramDialog.vue", import.meta.url), "utf8");
describe("SchemaDiagramDialog keyboard boundaries", () => {
it("does not consume a space typed in an editable control for canvas panning", () => {
const keydownSource = dialogSource.slice(dialogSource.indexOf("function handleKeydown"), dialogSource.indexOf("function handleKeyup"));
expect(keydownSource).toContain('const typing = target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable);');
expect(keydownSource).toContain('if (!typing && (e.key === " " || e.key === "Spacebar"))');
});
});
@@ -1,29 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const panelSource = readFileSync(new URL("../SchemaDiffDdlPanel.vue", import.meta.url), "utf8");
describe("SchemaDiffDdlPanel diff highlighting", () => {
it("keeps modified rows height-stable and sticky DDL headers opaque", () => {
expect(panelSource).not.toContain("border-yellow-500/40");
expect(panelSource).not.toContain("sticky top-0 bg-muted/50");
expect(panelSource.match(/sticky top-0 bg-background/g)).toHaveLength(4);
});
it("never applies the focused-row outline to empty padding lines", () => {
expect(panelSource).toContain('if (line.isPadding || focusedLineNumber === null) return "";');
});
it("keeps rollback comparison based on the selected forward SQL", () => {
expect(panelSource).toContain("rollbackForwardSql?: string;");
expect(panelSource).toContain("props.rollbackForwardSql ?? props.deploySql");
});
it("does not expose selected-SQL execution from the focused script tab", () => {
const focusedScript = panelSource.slice(panelSource.indexOf("<!-- Deploy Script -->"), panelSource.indexOf("<!-- Deploy Script All -->"));
const selectedScript = panelSource.slice(panelSource.indexOf("<!-- Deploy Script All -->"));
expect(focusedScript).not.toContain("$emit('executeScript')");
expect(selectedScript).toContain("$emit('executeScript')");
});
});
@@ -1,10 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../DatabaseDocsDialog.vue", import.meta.url), "utf8");
describe("DatabaseDocsDialog layout", () => {
it("reserves header space for the absolute close button", () => {
expect(dialogSource).toMatch(/<DialogHeader class="[^"]*\bpr-12\b[^"]*">/);
});
});
@@ -1,18 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync(new URL("../AiAssistant.vue", import.meta.url), "utf8");
describe("AI assistant explain_query canvas containment", () => {
it("bounds the embedded ExplainPlanViewer to a fixed, clipped height", () => {
const match = source.match(/<div v-if="step\.toolName === 'explain_query' && step\.explainData[^>]*class="([^"]*)"/);
expect(match, "explain_query wrapper div not found").not.toBeNull();
const wrapperClass = match![1];
expect(wrapperClass).toContain("h-64");
expect(wrapperClass).toContain("overflow-hidden");
});
it("does not rely on an unresolvable max-height alone to contain the canvas", () => {
expect(source).not.toContain('<ExplainPlanViewer :plan="parseExplainFromData(step.explainData, connection.db_type)" class="max-h-64" />');
});
});
@@ -1,17 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync(new URL("../AiAssistant.vue", import.meta.url), "utf8");
describe("AI assistant maximize control", () => {
it("exposes a parent-controlled maximize toggle with accessible labels", () => {
expect(source).toContain("maximized?: boolean;");
expect(source).toContain("toggleMaximize: [];");
expect(source).toContain(":title=\"props.maximized ? t('ai.restore') : t('ai.maximize')\"");
expect(source).toContain(":aria-label=\"props.maximized ? t('ai.restore') : t('ai.maximize')\"");
expect(source).toContain(':aria-pressed="props.maximized"');
expect(source).toContain("@click=\"emit('toggleMaximize')\"");
expect(source).toContain('<Minimize2 v-if="props.maximized"');
expect(source).toContain("<Maximize2 v-else");
});
});
@@ -1,21 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync(new URL("../AiAssistant.vue", import.meta.url), "utf8");
describe("AI assistant message copy actions", () => {
it("exposes copy actions below user and assistant messages", () => {
expect(source).toContain('data-ai-message-copy="user"');
expect(source).toContain('data-ai-message-copy="assistant"');
});
it("routes message copies through the body-only copy contract", () => {
expect(source).toContain("resolveAiMessageCopyText(msg, isStreamingMessage(msg))");
expect(source).toContain("copyAiContent(text, `message:${index}`)");
});
it("keeps code-block and message feedback keys separate", () => {
expect(source).toContain("copyAiContent(seg.content, `code:${i}:${j}`)");
expect(source).toContain("messageCopyKey(i)");
});
});
@@ -1,120 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../EditorSettingsDialog.vue", import.meta.url), "utf8");
const templateSource = dialogSource.slice(dialogSource.indexOf("<template>"));
const styleSource = dialogSource.slice(dialogSource.indexOf("<style>"), dialogSource.indexOf("</style>"));
function sourceIndexForKey(key: string): number {
const index = templateSource.indexOf(`t("settings.${key}")`);
if (index < 0) throw new Error(`Missing settings key: ${key}`);
return index;
}
function tagEnd(source: string, start: number): number {
let quote: '"' | "'" | null = null;
for (let index = start; index < source.length; index += 1) {
const char = source[index];
if (quote) {
if (char === quote && source[index - 1] !== "\\") quote = null;
} else if (char === '"' || char === "'") {
quote = char;
} else if (char === ">") {
return index;
}
}
return -1;
}
function borderedSettingDivForKey(key: string): string {
const keyIndex = sourceIndexForKey(key);
let start = templateSource.lastIndexOf("<div", keyIndex);
while (start >= 0) {
const end = tagEnd(templateSource, start);
const tag = templateSource.slice(start, end + 1);
if (tag.includes("rounded") && tag.includes("border")) return tag;
start = templateSource.lastIndexOf("<div", start - 1);
}
throw new Error(`Missing bordered setting div for settings key: ${key}`);
}
function divTagForAttribute(attribute: string): string {
const index = templateSource.indexOf(attribute);
if (index < 0) throw new Error(`Missing template attribute: ${attribute}`);
let start = templateSource.lastIndexOf("<div", index);
while (start >= 0) {
const end = tagEnd(templateSource, start);
if (end >= index) return templateSource.slice(start, end + 1);
start = templateSource.lastIndexOf("<div", start - 1);
}
throw new Error(`Missing div for template attribute: ${attribute}`);
}
function settingItemDivForText(text: string): string {
const index = templateSource.indexOf(text);
if (index < 0) throw new Error(`Missing template text: ${text}`);
let start = templateSource.lastIndexOf("<div", index);
while (start >= 0) {
const end = tagEnd(templateSource, start);
const tag = templateSource.slice(start, end + 1);
if (tag.includes("settings-item")) return tag;
start = templateSource.lastIndexOf("<div", start - 1);
}
throw new Error(`Missing settings item for template text: ${text}`);
}
function borderedDivForText(text: string): string {
const index = templateSource.indexOf(text);
if (index < 0) throw new Error(`Missing template text: ${text}`);
let start = templateSource.lastIndexOf("<div", index);
while (start >= 0) {
const end = tagEnd(templateSource, start);
const tag = templateSource.slice(start, end + 1);
if (tag.includes("rounded") && tag.includes("border")) return tag;
start = templateSource.lastIndexOf("<div", start - 1);
}
throw new Error(`Missing bordered div for template text: ${text}`);
}
describe("EditorSettingsDialog standard item hover feedback", () => {
it("marks representative editor, navigation, data, toolbar, and update setting rows for the shared hover treatment", () => {
for (const key of ["showStatementRunButtons", "externalSqlEditorMaxMb", "dataGridTypeColorScheme", "exclusiveRightSidebarPanels", "updateDownloadSource"] as const) {
expect(borderedSettingDivForKey(key)).toContain("settings-item");
}
});
it("keeps hover feedback off rows while their controls are disabled", () => {
expect(settingItemDivForText('id="web-sql-file-upload-max-mb"')).toContain("'settings-item-disabled': !webSqlFileUploadMaxMbLoaded || webSqlFileUploadMaxMbLoading");
expect(settingItemDivForText('id="mcp-http-enabled"')).toContain("'settings-item-disabled': mcpHttpLoading || mcpHttpSaving");
expect(styleSource).toMatch(/\.settings-item:not\(\.settings-item-disabled\):not\(\.opacity-50\):hover/);
});
it("provides legacy color fallbacks before progressively enhancing hover colors", () => {
const fallbackStart = styleSource.indexOf(".settings-item:not(.settings-item-disabled):not(.opacity-50):hover");
const enhancementStart = styleSource.indexOf("@supports (background: color-mix(in oklab, black, white))");
expect(fallbackStart).toBeGreaterThan(-1);
expect(enhancementStart).toBeGreaterThan(fallbackStart);
expect(styleSource.slice(fallbackStart, enhancementStart)).toContain("background-color: var(--muted);");
expect(styleSource.slice(fallbackStart, enhancementStart)).toContain("border-color: var(--muted-foreground);");
expect(styleSource.slice(enhancementStart)).toContain("color-mix(in oklab");
});
it("limits shared hover feedback to independent rows instead of composite and informational containers", () => {
expect(divTagForAttribute('data-settings-search-id="data-grid-filter-view"')).not.toContain("settings-item");
expect(borderedSettingDivForKey("dataGridKeepFilterEditorExpanded")).toContain("settings-item");
for (const text of ['t("settings.mcpHttpWebServiceTitle")', 't("settings.mcpHttpUnsavedChangesHint")', "{{ mcpStatus.bin_path }}", 't("settings.mcpCursorConfigPath")', 't("settings.mcpQoderConfigPath")', 't("settings.supportInfoTitle")'] as const) {
expect(borderedDivForText(text)).not.toContain("settings-item");
}
});
it("uses direct selectors without styling nested descendants", () => {
expect(styleSource).not.toContain(".settings-item *");
expect(styleSource).not.toContain(".settings-item:hover *");
});
it("leaves choice cards and shortcut rows on their existing interaction styles", () => {
expect(borderedSettingDivForKey("showStatementRunButtons")).not.toContain("settings-choice-card");
expect(templateSource).toContain("settings-shortcut-row group");
});
});
@@ -1,23 +0,0 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
// reka-ui's SelectItem throws on value="" (SelectItem.js: "A <SelectItem /> must
// have a value prop that is not an empty string"). The throw happens while the
// select's dismissable layer is up, and the leftover layer then blocks every
// click in the app — dialogs can no longer be closed. These dialogs use
// sentinel values instead; keep it that way.
const multiDbDialogSource = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/editor/MultiDbExecuteDialog.vue"), "utf8");
const tagPanelSource = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/config/TagManagementPanel.vue"), "utf8");
describe("Select empty-value regression guard", () => {
it("keeps the multi-db target-group select free of empty SelectItem values", () => {
expect(multiDbDialogSource).not.toMatch(/<SelectItem\s+value=""/);
expect(multiDbDialogSource).toContain("NO_GROUP_VALUE");
});
it("keeps the tag environment select free of empty SelectItem values", () => {
expect(tagPanelSource).not.toMatch(/<SelectItem\s+value=""/);
expect(tagPanelSource).toContain("ALL_ENVIRONMENTS");
});
});
@@ -1,10 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../MultiDbExecuteDialog.vue", import.meta.url), "utf8");
describe("MultiDbExecuteDialog footer layout", () => {
it("keeps the target group manager close button aligned with the main dialog footer", () => {
expect(dialogSource).toContain('<DialogFooter class="mx-0 mb-0 shrink-0 border-t px-5 py-3">\n <Button variant="outline" @click="manageGroups = false">');
});
});
@@ -1,38 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const appSource = readFileSync(new URL("../../../App.vue", import.meta.url), "utf8");
const composableSource = readFileSync(new URL("../../../composables/useBackgroundImage.ts", import.meta.url), "utf8");
const globalsCss = readFileSync(new URL("../../../../src/styles/globals.css", import.meta.url), "utf8");
const queryEditorSource = readFileSync(new URL("../QueryEditor.vue", import.meta.url), "utf8");
const welcomeScreenSource = readFileSync(new URL("../../layout/WelcomeScreen.vue", import.meta.url), "utf8");
describe("global background image wiring", () => {
it("mounts a single app-level background layer behind all content", () => {
expect(appSource).toContain("data-app-background");
expect(appSource).toMatch(/pointer-events-none fixed inset-0 -z-10/);
expect(appSource).toContain("useBackgroundImage(settingsStore)");
});
it("toggles the wallpaper class on <html> and clears CodeMirror backgrounds app-wide", () => {
expect(composableSource).toContain('BACKGROUND_IMAGE_ACTIVE_CLASS = "dbx-bg-active"');
expect(globalsCss).toContain("html.dbx-bg-active .cm-editor");
expect(globalsCss).toContain("html.dbx-bg-active .cm-gutters");
});
it("keeps card and popover surfaces opaque (readability contract)", () => {
expect(composableSource).toContain("BACKGROUND_IMAGE_SURFACE_VARS");
const appBackground = readFileSync(new URL("../../../lib/app/appBackgroundImage.ts", import.meta.url), "utf8");
expect(appBackground).toContain('"--background"');
expect(appBackground).toContain('"--sidebar"');
expect(appBackground).not.toMatch(/"--card"|"--popover"/);
});
it("removed the scoped per-area mode (editor/welcome no longer render their own layers)", () => {
for (const source of [queryEditorSource, welcomeScreenSource]) {
expect(source).not.toContain("useBackgroundImage");
expect(source).not.toContain("data-query-editor-bg");
expect(source).not.toContain("data-welcome-bg");
}
});
});
@@ -1,64 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const viewerSource = readFileSync(new URL("../ExplainPlanViewer.vue", import.meta.url), "utf8");
describe("ExplainPlanViewer MySQL fallback", () => {
it("selects the table only after loading finishes without a visual plan", () => {
expect(viewerSource).toContain("const hasTableView = computed(() => !!props.tableResult || !!props.tableError);");
expect(viewerSource).toContain("[hasTableView, () => !!props.tableResult, () => !!props.plan, () => props.loading]");
expect(viewerSource).toContain('if (!loading && hasTableResult && !hasPlan) activeView.value = "table";');
expect(viewerSource).toContain("{ immediate: true },");
});
it("keeps the canvas default when the visual plan arrives", () => {
expect(viewerSource).toContain('if (!loading && hasTableResult && !hasPlan) activeView.value = "table";');
expect(viewerSource).toContain('if (!available && activeView.value === "table") activeView.value = "canvas";');
});
});
describe("ExplainPlanViewer canvas view", () => {
it("opens on the canvas and keeps the other views reachable", () => {
expect(viewerSource).toContain('const activeView = ref<"canvas" | "tree" | "summary" | "raw" | "table">("canvas");');
expect(viewerSource).toContain('<ExplainPlanDiagram :nodes="plan.nodes" />');
expect(viewerSource).toContain('import ExplainPlanDiagram from "./ExplainPlanDiagram.vue";');
for (const view of ["canvas", "tree", "summary", "raw", "table"]) {
expect(viewerSource, view).toContain(`activeView = '${view}'`);
}
});
it("derives the measured-rows chip from parsed nodes, not the raw plan text", () => {
expect(viewerSource).toContain('import { extractActualRows } from "@/lib/diagram/planCanvas";');
expect(viewerSource).toContain("const measuredRowsLabel = computed(() => {");
expect(viewerSource).toContain('if (databaseType !== "postgres" && databaseType !== "sqlserver") return undefined;');
expect(viewerSource).toContain("if (!flattenExplainPlanNodes(props.plan!.nodes).some((node) => extractActualRows(node) !== undefined)) return undefined;");
expect(viewerSource).toContain('return databaseType === "sqlserver" ? "ACTUAL" : "ANALYZE";');
expect(viewerSource).toContain('<span v-if="measuredRowsLabel"');
expect(viewerSource).toContain("{{ measuredRowsLabel }}</span>");
// The Dameng A-TRACE chip keeps its own raw-text condition.
expect(viewerSource).toContain("plan?.databaseType === 'dameng' && isRawString && rawContent.includes('->')");
});
it("puts the canvas tab first in the view switcher", () => {
const tabOrder = [...viewerSource.matchAll(/@click="activeView = '(\w+)'"/g)].map((match) => match[1]);
expect(tabOrder).toEqual(["canvas", "tree", "summary", "raw", "table"]);
});
});
describe("ExplainPlanViewer header at narrow widths", () => {
it("keeps the title badge and node-count label on one line instead of letting them wrap", () => {
const headerMatch = viewerSource.match(/<div class="h-9 shrink-0[^"]*"[^>]*>([\s\S]*?)<\/div>\s*\n\s*<div v-if="loading/);
expect(headerMatch, "explain plan header block not found").not.toBeNull();
const header = headerMatch![0];
expect(header).toContain("overflow-x-auto");
// Title badge and "MYSQL · N nodes" label must not be flex-shrinkable, or a
// narrow host (e.g. the AI chat panel) squeezes their text into a
// near-zero width and the browser wraps it one character per line.
expect(header).toMatch(/<span class="shrink-0 whitespace-nowrap inline-flex[^"]*">\s*<GitBranch/);
expect(header).toMatch(/<span v-if="plan \|\| hasTableView" class="shrink-0 whitespace-nowrap text-muted-foreground">/);
});
it("keeps the view-switcher button group from shrinking away", () => {
expect(viewerSource).toContain('<div v-if="plan || hasTableView" class="shrink-0 inline-flex rounded-md border bg-muted/40 p-0.5">');
});
});
@@ -1,27 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8");
describe("DataGrid native clipboard regions", () => {
it("keeps table info text selection out of grid copy shortcuts", () => {
expect(dataGridSource).toMatch(/<div\b(?=[^>]*\bv-if="showTableInfo")(?=[^>]*\bdata-native-clipboard)[^>]*>/);
});
it("keeps transposed field-name text selection out of grid copy shortcuts", () => {
expect(dataGridSource).toMatch(/<div\b(?=[^>]*\bdata-native-clipboard)(?=[^>]*class="sticky left-0)[^>]*>/);
});
it("opens read-only text selection from DOM, canvas, and transposed cells", () => {
expect(dataGridSource.match(/startReadonlyCellTextSelection\(/g)).toHaveLength(4);
expect(dataGridSource).not.toContain("showReadonlyCellDetailsOnDblClick");
expect(dataGridSource).toContain("if (!canEditCellItem(item, actualColIdx)) {");
expect(dataGridSource.match(/<DataGridReadonlyTextSelection/g)).toHaveLength(3);
});
it("uses a text cursor for read-only cells across grid render modes", () => {
expect(dataGridSource).toContain('hitItem && actualColIdx !== undefined ? "text" : "cell"');
expect(dataGridSource).toContain("'cursor-text': !isScrolling && !canEditCellItem(item, col.actualColIdx)");
expect(dataGridSource).toContain("'cursor-text': !isScrolling,");
});
});
@@ -1,20 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8");
describe("DataGrid column comments", () => {
it("uses per-ordinal multi-source comments, falling back to source metadata for single-source grids", () => {
expect(dataGridSource).toMatch(
/function resolvedColumnComment\(column: string, actualColIdx: number\)[\s\S]*?const ordinalComments = props\.resultColumnComments;[\s\S]*?if \(ordinalComments\) return ordinalComments\[actualColIdx\];[\s\S]*?dataGridColumnCommentFor\([\s\S]*?props\.sourceColumns\?\.\[actualColIdx\][\s\S]*?\);\s*\}/,
);
expect(dataGridSource).toContain(':column-comment="headerColumnComment(col.name, col.actualColIdx)"');
expect(dataGridSource).toContain(':tooltip-column-comment="resolvedColumnComment(col.name, col.actualColIdx)"');
expect(dataGridSource).toContain("(column, index) => headerColumnComment(column, index)");
});
it("routes per-ordinal comments to field filtering and go-to-column lookup", () => {
expect(dataGridSource).toMatch(/useDataGridColumnLayoutState\(\{[\s\S]*?columnComments: \(\) => props\.resultColumnComments,[\s\S]*?\}\);/);
expect(dataGridSource).toMatch(/const goToColumnItems = computed\(\(\) =>[\s\S]*?buildDataGridColumnLookupItems\(\{[\s\S]*?columnComments: props\.resultColumnComments,[\s\S]*?\}\),[\s\S]*?\);/);
});
});
@@ -1,32 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8");
const formatterSource = readFileSync(new URL("../../../composables/useDataGridColumnFormatter.ts", import.meta.url), "utf8");
function functionSource(source: string, name: string): string {
const start = source.indexOf(`function ${name}`);
expect(start).toBeGreaterThanOrEqual(0);
const end = source.indexOf("\n function ", start + 1);
return source.slice(start, end < 0 ? undefined : end);
}
describe("DataGrid column formatter keys", () => {
it("derives every key list from the shared canonical key builder", () => {
const keysSource = functionSource(formatterSource, "formatterKeysForColumn");
expect(keysSource).toContain("return columnFormatterKeys({");
expect(keysSource).toContain("databaseType: options.resolvedDatabaseType.value");
expect(keysSource).toContain("displaySource: props.queryDisplaySourceColumns?.[columnIndex]");
// Saves keep writing the single canonical keys[0] spelling.
const saveSource = functionSource(formatterSource, "saveColumnFormatter");
expect(saveSource).toContain("const key = formatterKeyForColumn(columnIndex);");
expect(saveSource).toContain("settingsStore.updateColumnFormatter(key, formatter);");
});
it("clears every candidate key so either surface leaves the column unformatted", () => {
const clearSource = functionSource(formatterSource, "clearColumnFormatter");
expect(clearSource).toContain("const keys = formatterKeysForColumn(columnIndex);");
expect(clearSource).toContain("if (!keys.length) return;");
expect(clearSource).toContain("for (const key of keys) settingsStore.updateColumnFormatter(key, undefined);");
});
});
@@ -1,14 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8");
describe("DataGrid loading elapsed timer", () => {
it("resets for a new load but resumes from the original start after KeepAlive activation", () => {
expect(dataGridSource).toContain("function startLoadingElapsedTimer(reset = false)");
expect(dataGridSource).toContain("if (reset || !_loadingStart)");
expect(dataGridSource).toContain("startLoadingElapsedTimer(true);");
expect(dataGridSource).toMatch(/function resumeCanvasGridWork\(\) \{\s*dataGridIsActive = true;\s*startLoadingElapsedTimer\(\);/);
expect(dataGridSource).toMatch(/function pauseCanvasGridWork\(\) \{\s*dataGridIsActive = false;\s*stopLoadingElapsedTimer\(\);/);
});
});
@@ -1,57 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8");
describe("DataGrid transpose presentation", () => {
it("left-aligns record headers with their transposed values", () => {
expect(dataGridSource).toContain('class="shrink-0 border-r border-border px-2 py-1.5 text-left tabular-nums relative"');
expect(dataGridSource).not.toContain('class="shrink-0 border-r border-border px-2 py-1.5 text-center tabular-nums relative"');
});
it("shows field metadata only when the transpose setting is enabled", () => {
expect(dataGridSource).toContain("settingsStore.editorSettings.dataGridShowTransposeFieldMetadata");
expect(dataGridSource).toContain("showTransposeFieldMetadata.value && showColumnTypesInHeader.value");
expect(dataGridSource).toContain("showTransposeFieldMetadata.value && showColumnCommentsInHeader.value");
expect(dataGridSource).toContain("30 + (transposeReserveTypeLine.value ? 14 : 0) + (transposeReserveCommentLine.value ? 14 : 0)");
expect(dataGridSource).toContain(':item-size="transposeRowHeight"');
expect(dataGridSource).toContain("data-grid-transpose-type-line");
expect(dataGridSource).toContain("data-grid-transpose-comment-line");
expect(dataGridSource).toContain("data-grid-transpose-index-indicator");
expect(dataGridSource).toContain("showColumnTypesInHeader && item.type");
expect(dataGridSource).toContain("showColumnCommentsInHeader && item.comment");
expect(dataGridSource).toContain("transposeColumnIndexKind(item.column)");
});
it("shows complete long metadata in a hoverable, bounded tooltip", () => {
expect(dataGridSource).toContain("data-grid-transpose-field-tooltip");
expect(dataGridSource).toContain(':text="transposeFieldTitle(item)"');
expect(dataGridSource).toContain('side="right"');
expect(dataGridSource).toContain("max-h-[min(20rem,calc(100vh-1rem))]");
expect(dataGridSource).toContain("w-[min(24rem,calc(100vw-1rem))]");
expect(dataGridSource).toContain("overflow-auto rounded-md");
expect(dataGridSource).toContain("whitespace-pre-wrap break-words select-text");
expect(dataGridSource).not.toContain("data-grid-transpose-field-popover");
});
it("stops clipping the cell while the long-text editor or readonly text selection is active", () => {
// Transposed rows are compact (30px by default) while the expanded editor
// grows well beyond that; the cell must switch to overflow-visible while the
// editor / readonly text selection is active, mirroring the normal grid cell.
expect(dataGridSource).toContain("'overflow-visible z-20 border-r-transparent': transposeCellEditorActive(cell.recordIndex, cell.valueIndex)");
expect(dataGridSource).toContain("'overflow-hidden text-ellipsis whitespace-nowrap': !transposeCellEditorActive(cell.recordIndex, cell.valueIndex)");
expect(dataGridSource).toContain("function transposeCellEditorActive");
expect(dataGridSource).toContain("readonlyTextCellMatches(rowId, valueIndex)");
// The transposed cell no longer unconditionally truncates (overflow: hidden),
// which would crop the expanded editor below the compact row bounds.
expect(dataGridSource).not.toContain('class="relative flex shrink-0 items-center border-r border-border/70 px-2 py-0 truncate"');
});
it("shares the normal grid cell's editor overlay contract while editing", () => {
// Normal grid cells switch to the same overflow/z/border contract while the
// cell editor or readonly text selection is active, so long-text editing
// looks identical across both views.
expect(dataGridSource).toContain("'overflow-visible z-20 border-r-transparent': (editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx) || readonlyTextCellMatches(item.id, col.actualColIdx),");
expect(dataGridSource).toContain("'overflow-hidden': !((editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx) || readonlyTextCellMatches(item.id, col.actualColIdx)),");
});
});
@@ -1,36 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8");
function sourceBetween(start: string, end: string): string {
const startIndex = dataGridSource.indexOf(start);
const endIndex = dataGridSource.indexOf(end, startIndex + start.length);
expect(startIndex).toBeGreaterThanOrEqual(0);
expect(endIndex).toBeGreaterThan(startIndex);
return dataGridSource.slice(startIndex, endIndex);
}
describe("data grid wheel bindings", () => {
it("routes Canvas and DOM wheel calculations through the shared helper", () => {
const canvasHandler = sourceBetween("function onCanvasWheel", "function onDomGridWheel");
const domHandler = sourceBetween("function onDomGridWheel", "function onCanvasMouseMove");
for (const handler of [canvasHandler, domHandler]) {
expect(handler).toContain("resolveDataGridWheelScroll({");
expect(handler).toContain("if (!wheelScroll.moved) return;");
expect(handler.indexOf("if (!wheelScroll.moved) return;")).toBeLessThan(handler.indexOf("event.preventDefault();"));
expect(handler).toContain("event.stopPropagation();");
}
});
it("accepts genuine pixel deltaX in the Canvas handler", () => {
const predicate = sourceBetween("function shouldAccelerateCanvasWheel", "function onCanvasWheel");
expect(predicate).toContain("if (event.deltaX !== 0) return true;");
});
it("keeps wheel handlers bound in both render modes", () => {
expect(dataGridSource).toContain('@wheel="onCanvasWheel"');
expect(dataGridSource.match(/@wheel="onDomGridWheel"/g)).toHaveLength(2);
});
});
@@ -1,44 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const browserSource = readFileSync(new URL("../KvKeyBrowser.vue", import.meta.url), "utf8");
const valueEditorSource = readFileSync(new URL("../KvValueEditor.vue", import.meta.url), "utf8");
describe("KvKeyBrowser edit dialog layout", () => {
it("keeps the value format label and selector together in the editor toolbar", () => {
expect(browserSource).toContain('class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"');
expect(browserSource).toContain('class="flex shrink-0 items-center gap-3"');
expect(browserSource).toContain('<SelectTrigger class="h-9 w-44">');
});
it("cancels the shared footer negative margin and preserves bottom safe spacing", () => {
expect(browserSource).toContain('<DialogFooter class="mx-0 mb-0 shrink-0 gap-3 border-t bg-muted/10 px-6 py-5">');
expect(browserSource).toContain('variant="outline" class="h-10 min-w-20"');
expect(browserSource).toContain('<Button class="h-10 min-w-20" :disabled="saving || !editKeyWritable"');
});
it("applies the same safe spacing to the history dialog footer", () => {
expect(browserSource).toContain('<DialogFooter class="mx-0 mb-0 shrink-0 border-t px-6 py-5">');
expect(browserSource).toContain('class="h-10 min-w-20" @click="showHistoryDialog = false"');
});
it("keeps an accessible copy action in the upper-right corner of the value panel", () => {
expect(browserSource).toContain('class="absolute right-2 top-2 z-10 h-8 w-8');
expect(browserSource).toContain('@click="copySelectedValue"');
expect(browserSource).toContain('<Check v-if="selectedValueCopied"');
expect(browserSource).toContain('<Copy v-else class="h-4 w-4" />');
expect(browserSource).toMatch(/<pre\s+data-native-clipboard/);
});
it("keeps the selected value preview word wrapping aligned with the global editor setting", () => {
expect(browserSource).toContain(":class=\"settingsStore.editorSettings.wordWrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'\"");
expect(browserSource).not.toContain("bg-muted/20 whitespace-pre-wrap break-words p-3");
});
it("keeps shared KV value editor word wrapping aligned with the global editor setting", () => {
expect(valueEditorSource).toContain("const wordWrapCompartment = new Compartment()");
expect(valueEditorSource).toContain("wordWrapCompartment.of(wordWrapExtension())");
expect(valueEditorSource).toContain("wordWrapCompartment.reconfigure(wordWrapExtension(wordWrap))");
expect(valueEditorSource).not.toContain("\n EditorView.lineWrapping,\n");
});
});
@@ -1,17 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const browserSource = readFileSync(new URL("../KvKeyBrowser.vue", import.meta.url), "utf8");
describe("KvKeyBrowser mutation errors", () => {
it("shows duplicate-key conflicts next to the Key field", () => {
expect(browserSource).toContain("classifyKvMutationError(error, isCreating.value");
expect(browserSource).toContain(`:aria-invalid="editErrorKind === 'keyAlreadyExists'"`);
expect(browserSource).toContain(`v-if="editError && editErrorKind === 'keyAlreadyExists'"`);
});
it("clears the duplicate warning after the Key name changes", () => {
expect(browserSource).toContain("watch(editKey, () =>");
expect(browserSource).toContain(`if (editErrorKind.value !== "keyAlreadyExists") return`);
});
});
@@ -1,26 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const browserSource = readFileSync(new URL("../KvKeyBrowser.vue", import.meta.url), "utf8");
describe("KvKeyBrowser Base64 UTF-8 preview", () => {
it("keeps the preview opt-in for backend-specific wrappers", () => {
expect(browserSource).toContain("enableBase64Utf8Preview?: boolean");
expect(browserSource).toContain("enableBase64Utf8Preview: false");
});
it("copies the selected derived view while preserving the raw value path", () => {
expect(browserSource).toContain("selectedValueClipboardText");
expect(browserSource).toContain("copyToClipboard(selectedValueClipboardText.value)");
expect(browserSource.match(/selectedBase64ViewMode\.value = "utf8"/g)).toHaveLength(2);
expect(browserSource).toContain("editValue.value = selectedTextValue.value");
expect(browserSource).toContain("value: selectedValue.value.value");
});
it("offers explicit UTF-8 and Base64 view controls with error feedback", () => {
expect(browserSource).toContain('data-testid="kv-base64-utf8-view"');
expect(browserSource).toContain('data-testid="kv-base64-raw-view"');
expect(browserSource).toContain("labels.utf8PreviewLossy");
expect(browserSource).toContain("labels.utf8PreviewUnavailable");
});
});
@@ -1,27 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const contentAreaSource = readFileSync(new URL("../ContentArea.vue", import.meta.url), "utf8");
describe("ContentArea data-mode header", () => {
it("lets name chips flex to available width and only truncate when the row overflows", () => {
const headerIndex = contentAreaSource.indexOf("data-data-header-connection");
expect(headerIndex).toBeGreaterThan(-1);
const chipsBlock = contentAreaSource.slice(headerIndex, contentAreaSource.indexOf('class="ml-auto"', headerIndex));
expect(chipsBlock.length).toBeGreaterThan(0);
// Fixed max-w-48/max-w-56 caps clipped table names even with plenty of free
// space in the header row (#7880); chips must rely on truncate so they only
// clip when the row itself runs out of space. The min-w-12 floor keeps
// scrollWidth reporting real overflow for the measured condensation tiers —
// below the floor the tiers condense the row instead of crushing chips.
expect(chipsBlock).not.toContain("max-w-48");
expect(chipsBlock).not.toContain("max-w-56");
expect(chipsBlock).toContain("inline-flex min-w-12 items-center truncate");
});
it("keeps the full table and schema names reachable via title tooltips", () => {
expect(contentAreaSource).toContain(':title="activeDataTabTableMeta?.tableName || activeTab.title"');
expect(contentAreaSource).toContain("[activeDataTabTableMeta?.schema, databaseDisplayNameForTab(activeTab.connectionId, activeTab.database, t)].filter(Boolean).join('@')");
});
});
@@ -1,21 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const contentAreaSource = readFileSync(new URL("../ContentArea.vue", import.meta.url), "utf8");
describe("ContentArea shared result pane collapse (#8233)", () => {
it("renders the results toolbar (and its hide-results chevron) even with no query output when resultOnly", () => {
// The shared result surface (QueryResultSurface -> ContentArea result-only)
// is mounted unconditionally by SqlEditorWorkspace regardless of whether the
// active tab has run a query. handleHideResultsPane's chevron lives inside
// this toolbar and is the only UI to collapse that shared pane, so gating
// the toolbar on hasQueryOutput alone left it permanently open with no way
// to close it until a query executed.
expect(contentAreaSource).not.toMatch(/v-if="hasQueryOutput"\s+class="flex h-10 shrink-0 items-center gap-1 border-b bg-muted\/20 px-2"/);
expect(contentAreaSource).toContain(`v-if="hasQueryOutput || resultOnly" class="flex h-10 shrink-0 items-center gap-1 border-b bg-muted/20 px-2"`);
});
it("keeps handleHideResultsPane bubbling a toggle event for the shared (resultOnly) surface", () => {
expect(contentAreaSource).toContain('if (props.resultOnly) {\n emit("toggleResultsPane");');
});
});
@@ -1,43 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const toolbarSource = readFileSync(new URL("../EditorToolbar.vue", import.meta.url), "utf8");
describe("EditorToolbar transaction mode control", () => {
it("shows an A or M badge next to Tx instead of relying on color alone", () => {
expect(toolbarSource).toContain('const transactionModeBadge = computed(() => (isManualTransactionMode.value ? "M" : "A"));');
expect(toolbarSource).toContain('<span class="text-[11px] font-bold">Tx:</span>');
expect(toolbarSource).toContain(">{{ transactionModeBadge }}</span>");
});
it("exposes the transaction mode to assistive technology", () => {
expect(toolbarSource).toContain('class="ml-1 flex items-center gap-0.5 border-l border-border/60 pl-1" role="group" :aria-label="transactionTooltip"');
expect(toolbarSource).toContain(':aria-label="transactionTooltip"');
expect(toolbarSource).toContain(':aria-pressed="isManualTransactionMode"');
expect(toolbarSource).toContain(":aria-label=\"t('toolbar.commit')\"");
expect(toolbarSource).toContain(":aria-label=\"t('toolbar.rollback')\"");
});
it("places the transaction controls at the end in mode, commit, rollback order", () => {
const multiExecuteIndex = toolbarSource.indexOf("@click=\"emit('multiExecute')\"");
const transactionToggleIndex = toolbarSource.indexOf("@click=\"emit('update:autoCommit', autoCommit === false)\"");
const commitIndex = toolbarSource.indexOf("@click=\"emit('commit')\"");
const rollbackIndex = toolbarSource.indexOf("@click=\"emit('rollback')\"");
const actionGroupEndIndex = toolbarSource.indexOf('<span class="flex-1 min-w-0" />');
expect(multiExecuteIndex).toBeGreaterThan(-1);
expect(transactionToggleIndex).toBeGreaterThan(multiExecuteIndex);
expect(commitIndex).toBeGreaterThan(transactionToggleIndex);
expect(rollbackIndex).toBeGreaterThan(commitIndex);
expect(actionGroupEndIndex).toBeGreaterThan(rollbackIndex);
});
it("hides Commit/Rollback for a clean Oracle manual session only", () => {
expect(toolbarSource).toContain("const showTxnActions = computed(() => {");
expect(toolbarSource).toContain("if (props.isOracleManualTransaction) return isTransactionActive.value && props.oracleTxnPossiblyDirty === true;");
expect(toolbarSource).toContain("return isTransactionActive.value;");
// Both buttons are driven by the combined condition.
expect(toolbarSource).toContain('<Tooltip v-if="showTxnActions">');
expect(toolbarSource.match(/<Tooltip v-if="showTxnActions">/g)).toHaveLength(2);
});
});
@@ -1,130 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const workspaceSource = readFileSync(new URL("../SqlEditorWorkspace.vue", import.meta.url), "utf8");
const groupSource = readFileSync(new URL("../EditorGroup.vue", import.meta.url), "utf8");
const groupTabBarSource = readFileSync(new URL("../EditorGroupTabBar.vue", import.meta.url), "utf8");
const tabMenuSource = readFileSync(new URL("../../../lib/tabs/tabMenu.ts", import.meta.url), "utf8");
const editorSurfaceSource = readFileSync(new URL("../QueryEditorSurface.vue", import.meta.url), "utf8");
const resultSurfaceSource = readFileSync(new URL("../QueryResultSurface.vue", import.meta.url), "utf8");
const contentAreaSource = readFileSync(new URL("../ContentArea.vue", import.meta.url), "utf8");
const surfaceContractSource = readFileSync(new URL("../querySurfaces.ts", import.meta.url), "utf8");
const surfaceEventsSource = readFileSync(new URL("../../../lib/tabs/contentSurfaceEvents.ts", import.meta.url), "utf8");
describe("SQL editor workspace single-group tracer bullet", () => {
it("composes splitpanes editor groups and one shared result surface", () => {
expect(workspaceSource).toContain("Splitpanes");
expect(workspaceSource).toContain("EditorGroup");
expect(workspaceSource).toContain("QueryResultSurface");
expect(groupSource).toContain("EditorGroupTabBar");
expect(groupSource).toContain("QueryEditorSurface");
expect(groupSource).toContain("ContentArea");
expect(groupSource).toContain("tabIds");
expect(groupSource).toContain("activeTabId");
});
it("routes query editor and shared result through dedicated surface components", () => {
expect(editorSurfaceSource).toContain("editor-only");
expect(resultSurfaceSource).toContain("result-only");
expect(contentAreaSource).toContain("editorOnly?: boolean");
expect(contentAreaSource).toContain("resultOnly?: boolean");
});
it("routes structure-tab search to the table editor instead of the sidebar", () => {
expect(contentAreaSource).toContain('if (props.activeTab.mode === "structure") return tableStructureEditorRef.value?.focusSearch() ?? false;');
expect(contentAreaSource).toContain("focusSearch: () => boolean");
});
it("keeps AppTabBar as the global special-page bar while regular tabs move to groups", () => {
const tabBarSource = readFileSync(new URL("../AppTabBar.vue", import.meta.url), "utf8");
expect(tabBarSource).not.toContain("hideRegularTabs");
expect(groupTabBarSource).toContain("app-tab-bar");
});
it("makes the shared result area vertically resizable through the workspace splitter", () => {
expect(workspaceSource).toContain("sql-editor-workspace-split");
expect(workspaceSource).toContain("onSharedResultResized");
expect(workspaceSource).toContain("dbx-shared-results-pane-size");
expect(workspaceSource).not.toContain("h-1/3");
});
it("routes shared-result statement navigation to the owning editor surface", () => {
expect(workspaceSource).toContain("@preview-statement");
expect(workspaceSource).toContain("@focus-statement");
expect(workspaceSource).toContain("handlePreviewStatement");
expect(workspaceSource).toContain("handleFocusStatement");
expect(resultSurfaceSource).toContain("previewStatement");
expect(resultSurfaceSource).toContain("focusStatement");
expect(resultSurfaceSource).toContain("previewStatementRange");
expect(resultSurfaceSource).toContain("focusStatementRange");
expect(editorSurfaceSource).toContain("previewStatementRange");
expect(editorSurfaceSource).toContain("focusStatementRange");
expect(groupSource).toContain("previewStatementRange");
expect(groupSource).toContain("focusStatementRange");
expect(surfaceContractSource).toContain("previewStatement: [tabId: string, range: StatementRange | null]");
expect(surfaceContractSource).toContain("focusStatement: [tabId: string, range: StatementRange | null]");
});
it("keeps group tabbar behavior compatible with the legacy tab bar", () => {
expect(workspaceSource).toContain("@toggle-zen-mode");
expect(groupSource).toContain("@toggle-zen-mode");
expect(groupTabBarSource).toContain('"toggle-zen-mode": []');
expect(groupTabBarSource).toContain('tab.mode === "data"');
expect(groupTabBarSource).toContain('emit("toggle-zen-mode")');
expect(tabMenuSource).toContain("visible: options.canRename");
expect(groupTabBarSource).toContain("createRenameDuplicateTabItems");
expect(groupTabBarSource).toContain("cleanupTabDrag");
expect(groupTabBarSource).toContain("onUnmounted(() => {");
expect(groupTabBarSource).toContain("cleanupTabDrag();");
expect(groupTabBarSource).toContain("pointercancel");
});
it("keeps event forwarding and per-group active tab wiring intact", () => {
expect(workspaceSource).toContain('v-bind="editorGroupBindings"');
expect(workspaceSource).toContain('v-bind="resultSurfaceBindings"');
expect(groupSource).toContain('v-bind="surfaceBindings"');
expect(groupSource).toContain("activeTab: activeTab.value!");
expect(editorSurfaceSource).toContain('v-bind="bindings"');
expect(resultSurfaceSource).toContain('v-bind="bindings"');
});
it("lets the mouse hide and re-show the shared query results pane", () => {
// Regression (#8076 refactor): the shared result surface's "hide results"
// chevron wrote the inert per-instance resultsPaneOpen ref, so query-tab
// results could no longer be collapsed by mouse — the pane was pinned
// open. The chevron must bubble a toggleResultsPane event up through the
// surface contract to the workspace, which owns the real collapse state.
expect(surfaceContractSource).toContain("toggleResultsPane: []");
expect(surfaceEventsSource).toContain('"toggleResultsPane",');
expect(contentAreaSource).toContain('emit("toggleResultsPane")');
expect(contentAreaSource).toContain('@click="handleHideResultsPane"');
expect(workspaceSource).toContain('@toggle-results-pane="toggleSharedResultsPane"');
expect(workspaceSource).toContain("showResultPane.value = !showResultPane.value");
// Collapsing unmounts the shared surface, so the mouse re-show affordance
// must live at the workspace level, not inside the collapsible pane.
expect(workspaceSource).toContain("editor.showResultsPane");
expect(workspaceSource).toContain("hasSharedOutput && !showResultPane");
// Running a query re-expands a collapsed pane (issue #6193 promise).
expect(workspaceSource).toContain("if (isExecuting || isExplaining) showResultPane.value = true;");
});
it("shows results without transitions while retaining new editor-group animation", () => {
const workspaceCssSource = readFileSync(new URL("../sqlEditorWorkspace.css", import.meta.url), "utf8");
expect(workspaceSource).toContain('import "./sqlEditorWorkspace.css"');
// Split-created group panes emerge from the divider side; hydration-gated
// so the restored layout never plays load choreography.
expect(workspaceSource).toContain("paneEnterClass");
expect(workspaceSource).toContain("workspace-pane-enter");
expect(workspaceCssSource).toContain("@keyframes dbx-workspace-pane-in");
// A completed response must not wait for a fade or pane-size transition.
expect(workspaceSource).toContain("resultPaneTargetSize");
expect(workspaceSource).not.toContain("<Transition");
expect(workspaceSource).toContain("result-pane-collapsed");
expect(workspaceCssSource).not.toContain(".result-surface-enter-active");
expect(workspaceCssSource).not.toContain(".result-surface-leave-active");
expect(workspaceCssSource).toMatch(/\.sql-editor-workspace-split\.splitpanes--ready\s*>\s*\.splitpanes__pane\s*\{\s*transition:\s*none !important;/);
expect(workspaceCssSource).toMatch(/\.sql-editor-workspace-split\s*>\s*\.splitpanes__splitter\s*\{\s*transition:\s*none;/);
// Group animation still respects reduced motion.
expect(workspaceCssSource).toContain("@media (prefers-reduced-motion: reduce)");
});
});
@@ -1,49 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const panelSource = readFileSync(new URL("../SqlLibraryPanel.vue", import.meta.url), "utf8");
describe("SqlLibraryPanel selection contrast", () => {
it("explains how SQL Library content differs from local SQL files", () => {
expect(panelSource).toContain('import HelpTooltip from "@/components/ui/tooltip/HelpTooltip.vue";');
expect(panelSource).toMatch(/<HelpTooltip :label="t\('sqlLibrary\.storageHelp'\)" side="bottom"[\s\S]*?\{\{ t\("sqlLibrary\.storageHelp"\) \}\}[\s\S]*?<\/HelpTooltip>/);
expect(panelSource).toContain('content-class="max-w-[320px] whitespace-pre-line"');
});
it("uses the accent foreground for selected rows and their metadata", () => {
expect(panelSource).toContain('return "bg-accent text-accent-foreground";');
expect(panelSource).toMatch(/function fileMetaClass[\s\S]*\? "text-accent-foreground" : "text-muted-foreground";/);
expect(panelSource).not.toContain('contextFile(fileId) ? "text-foreground/70"');
});
it("lets a plain click reliably exit batch selection", () => {
expect(panelSource).toMatch(/function handleDragMouseDown[\s\S]*if \(hasSelection\.value \|\| event\.shiftKey \|\| event\.metaKey \|\| event\.ctrlKey\) return;/);
expect(panelSource).toMatch(/function handleFileClick[\s\S]*else \{[\s\S]*clearSelection\(\);[\s\S]*lastClickedItemIndex\.value = currentIndex;[\s\S]*setActiveItem\(file\.id, "file"\);/);
expect(panelSource).toMatch(/function handleFolderClick[\s\S]*else \{[\s\S]*clearSelection\(\);[\s\S]*lastClickedItemIndex\.value = currentIndex;[\s\S]*setActiveItem\(folder\.id, "folder"\);/);
});
it("clears row selection when the blank panel area is clicked", () => {
expect(panelSource).toContain('@click.self="clearPanelSelection"');
expect(panelSource).toMatch(/function clearPanelSelection[\s\S]*clearSelection\(\);[\s\S]*activeItemId\.value = null;[\s\S]*activeItemType\.value = null;/);
});
it("does not use the first row as an implicit Shift selection anchor", () => {
expect(panelSource).toContain("const anchorIndex = rangeAnchorIndex();");
expect(panelSource).toMatch(/if \(anchorIndex === null\)[\s\S]*lastClickedItemIndex\.value = currentIndex;[\s\S]*return;/);
expect(panelSource).not.toContain("lastClickedItemIndex.value ?? 0");
});
it("shows a single collapse-all button and seeds a default-collapsed library", () => {
expect(panelSource).toContain("ChevronsDownUp,");
expect(panelSource).toContain("t('sqlLibrary.collapseAll')");
expect(panelSource).toMatch(/function collapseAllFolders\(\)[\s\S]*allFoldersTreeOrder[\s\S]*collapsedFolders\.value = new Set\(folderIds\);/);
expect(panelSource).toMatch(/const collapseDefaultsSeeded = ref\(false\);[\s\S]*collapsedFolders\.value = new Set\(folderIds\);/);
expect(panelSource).toMatch(/@click="collapseAllFolders"/);
expect(panelSource).toMatch(/:disabled="!hasAnyFolder\(\)"/);
});
it("auto-expands a search-matched branch so matched files remain visible when the library collapses by default", () => {
expect(panelSource).toMatch(/function isFolderExpanded\(folder: SavedSqlFolder\) \{[\s\S]*if \(searchQuery\.value && folderBranchMatchesQuery\(folder\)\) return true;[\s\S]*return !collapsedFolders\.value\.has\(folder\.id\);/);
expect(panelSource).toMatch(/if \(!isFolderExpanded\(folder\)\) return;/);
});
});
@@ -1,22 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const appSource = readFileSync(new URL("../../../App.vue", import.meta.url), "utf8");
const tabBarSource = readFileSync(new URL("../AppTabBar.vue", import.meta.url), "utf8");
// Special pages use the global tab-bar portal so their navigation remains
// separate from the editor groups in every tab-placement mode.
describe("special-page workspace portal layout", () => {
it("provides the portal from App and renders the special-page workspace", () => {
expect(appSource).toContain("provide(GROUP_TAB_BAR_PORTAL");
expect(tabBarSource).toContain('v-show="driverStoreActive || settingsPageActive"');
expect(tabBarSource).toContain("data-special-page-workspace");
});
it("keeps the special-page navigation and content panes independently sized", () => {
expect(tabBarSource).toContain("data-special-page-navigation");
expect(tabBarSource).toContain("data-special-page-content");
expect(tabBarSource).toContain(':class="layoutClass"');
expect(tabBarSource).toContain("flex-1 flex-col overflow-hidden");
});
});
@@ -1,44 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const contentAreaSource = readFileSync(new URL("../ContentArea.vue", import.meta.url), "utf8");
const databaseBrowserSource = readFileSync(new URL("../../objects/DatabaseBrowser.vue", import.meta.url), "utf8");
const objectBrowserSource = readFileSync(new URL("../../objects/ObjectBrowser.vue", import.meta.url), "utf8");
const overflowMenuSource = readFileSync(new URL("../../../components/ui/ToolbarOverflowMenu.vue", import.meta.url), "utf8");
describe("measured toolbar condensation wiring (non-SQL toolbars)", () => {
it("keeps the overflow trigger a single-layer dropdown (no Tooltip nesting)", () => {
expect(overflowMenuSource).toContain("DropdownMenuTrigger as-child");
expect(overflowMenuSource).not.toContain("TooltipTrigger");
expect(overflowMenuSource).not.toContain("LightTooltip");
});
it("condenses the data-mode header row through measured tiers", () => {
expect(contentAreaSource).toContain("useToolbarOverflow(dataToolbarRef");
// Chips keep a min-width floor so scrollWidth reports real overflow.
expect(contentAreaSource).toContain("inline-flex min-w-12 items-center truncate");
expect(contentAreaSource).toContain('ref="dataToolbarRef"');
expect(contentAreaSource).toContain("showDataColumnsChip");
expect(contentAreaSource).toContain("showDataTableInfoButton");
expect(contentAreaSource).toContain("dataToolbarCompact");
expect(contentAreaSource).toContain('v-if="showDataToolbarOverflow"');
});
it("condenses the database browser header row through measured tiers", () => {
expect(databaseBrowserSource).toContain("useToolbarOverflow(toolbarRef");
expect(databaseBrowserSource).toContain('ref="toolbarRef"');
expect(databaseBrowserSource).toContain("showInlineSortAndView");
expect(databaseBrowserSource).toContain("showConnectionChip");
expect(databaseBrowserSource).toContain('v-if="showToolbarOverflow"');
});
it("condenses the object browser header row through measured tiers", () => {
expect(objectBrowserSource).toContain("useToolbarOverflow(toolbarRef");
expect(objectBrowserSource).toContain('ref="toolbarRef"');
expect(objectBrowserSource).toContain("showInlineSortAndView");
expect(objectBrowserSource).toContain("showInlineCheckboxToggle");
expect(objectBrowserSource).toContain("showInlineObjectFilter");
expect(objectBrowserSource).toContain("showDatabaseChip");
expect(objectBrowserSource).toContain('v-if="showToolbarOverflow"');
});
});
@@ -1,21 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync(new URL("../MqttAdminConsole.vue", import.meta.url), "utf8");
describe("MqttAdminConsole i18n regression guards", () => {
it("does not append a hardcoded (No Local) suffix to the No Local label", () => {
// Issue #5858 follow-up: the subscription dialog used to render
// `t("connection.mqttNoLocal")(No Local)`. The CJK-bracket suffix was
// locale-invariant and leaked Chinese punctuation into every language.
expect(source).not.toContain(")No Local)");
expect(source).not.toContain("(No Local)");
});
it("renders the No Local label purely through vue-i18n", () => {
// The en locale value for connection.mqttNoLocal is already "No Local", so
// dropping the hardcoded suffix loses no information. Guard the intent.
expect(source).toContain('t("connection.mqttNoLocal")');
expect(source).not.toContain('t("connection.mqttNoLocal")(No Local)');
});
});
@@ -1,21 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync(new URL("../NacosConfigHistoryDialog.vue", import.meta.url), "utf8");
describe("NacosConfigHistoryDialog", () => {
it("keeps configuration identity values compact while allowing long values to wrap", () => {
expect(source).toContain('class="mt-1.5 flex min-w-0 flex-wrap items-baseline gap-x-3 gap-y-1 text-xs text-muted-foreground"');
expect(source).toContain('<span class="break-all text-foreground">{{ namespaceLabel }}</span>');
expect(source).toContain('<span class="break-all text-foreground">{{ dataIdLabel }}</span>');
expect(source).toContain('<span class="break-all text-foreground">{{ groupLabel }}</span>');
expect(source).toContain(':title="`namespace=${namespaceLabel}`"');
expect(source).toContain(':title="`dataId=${dataIdLabel}`"');
expect(source).toContain(':title="`group=${groupLabel}`"');
expect(source).not.toContain("grid-cols-1 gap-1.5 text-xs sm:grid-cols-2 xl:grid-cols-3");
expect(source).not.toContain("h-auto w-full");
expect(source).not.toContain('class="max-w-64 truncate font-mono"');
expect(source).not.toContain('class="max-w-72 truncate font-mono"');
expect(source).not.toContain('class="max-w-48 truncate font-mono"');
});
});
@@ -1,15 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync(new URL("../NacosNamespaceMultiSelect.vue", import.meta.url), "utf8");
describe("NacosNamespaceMultiSelect", () => {
it("renders selected namespaces as removable tags and keeps the dropdown open for multi-selection", () => {
expect(source).toContain("data-nacos-namespace-multiselect");
expect(source).toContain('v-for="value in modelValue"');
expect(source).toContain('@click.stop="remove(value)"');
expect(source).toContain('@click="toggle(option.value)"');
expect(source).toContain("selected.has(option.value)");
expect(source).toContain("nacos.namespaceSearchPlaceholder");
});
});
@@ -1,19 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync(new URL("../DdlViewDialog.vue", import.meta.url), "utf8");
describe("DdlViewDialog freshness", () => {
it("uses the persisted refresh preference when the dialog opens", () => {
expect(source).toMatch(/watch\(\s*\(\) => props\.open,[\s\S]*?await loadDdl\(settingsStore\.editorSettings\.refreshDdlOnOpen\);/);
});
it("refreshes the current DDL immediately when the preference is enabled", () => {
expect(source).toMatch(/function setRefreshDdlOnOpen\(value: boolean\)[\s\S]*?if \(value\) void loadDdl\(true\);/);
});
it("explains how the refresh preference affects cached DDL", () => {
expect(source).toContain('import { HelpTooltip } from "@/components/ui/tooltip";');
expect(source).toMatch(/<HelpTooltip :label="t\('contextMenu\.refreshDdlOnOpenHint'\)"[\s\S]*?t\("contextMenu\.refreshDdlOnOpenHint"\)[\s\S]*?<\/HelpTooltip>/);
});
});
@@ -1,59 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
function functionBody(name: string): string {
const signature = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\{`, "m").exec(source);
if (!signature) throw new Error(`Missing function ${name}`);
const bodyStart = signature.index + signature[0].length;
let depth = 1;
for (let index = bodyStart; index < source.length; index += 1) {
if (source[index] === "{") depth += 1;
else if (source[index] === "}") depth -= 1;
if (depth === 0) return source.slice(bodyStart, index);
}
throw new Error(`Unclosed function ${name}`);
}
describe("ObjectBrowser add to AI", () => {
it("declares the addToAi emit for table context hand-off", () => {
const emitDeclaration = source.slice(source.indexOf("const emit = defineEmits"), source.indexOf(">();", source.indexOf("const emit = defineEmits")));
expect(emitDeclaration).toContain("addToAi");
});
it("adds an 'Add to AI' menu item after 'New Query' in the table menu", () => {
const tableMenu = functionBody("getTableMenuItems");
const newQueryIndex = tableMenu.indexOf('t("contextMenu.newQuery")');
const addToAiIndex = tableMenu.indexOf("addToAiMenuItem(item)");
expect(newQueryIndex).toBeGreaterThan(-1);
expect(addToAiIndex).toBeGreaterThan(newQueryIndex);
});
it("guards the menu item behind supportsAiAssistantContext", () => {
const tableMenu = functionBody("getTableMenuItems");
expect(tableMenu).toContain("supportsAiAssistantContext(effectiveDatabaseType.value)");
expect(tableMenu).toContain("[addToAiMenuItem(item)]");
});
it("offers 'Add to AI' in the VictoriaMetrics early-return branch too", () => {
const tableMenu = functionBody("getTableMenuItems");
// The VM branch is the first block inside getTableMenuItems; verify it guards
// the same AI entry so ObjectBrowser stays consistent with the sidebar tree.
const vmBranchStart = tableMenu.indexOf("if (isVictoriaMetrics.value)");
const vmBranchEnd = tableMenu.indexOf("const useBatchActions", vmBranchStart);
const vmBranch = tableMenu.slice(vmBranchStart, vmBranchEnd);
expect(vmBranch).toContain("supportsAiAssistantContext(effectiveDatabaseType.value)");
expect(vmBranch).toContain("[addToAiMenuItem(item)]");
expect(vmBranch.indexOf("addToAiMenuItem(item)")).toBeGreaterThan(vmBranch.indexOf('t("contextMenu.newQuery")'));
});
it("emits selected tables (batch) or the single table with per-row schema", () => {
const addToAi = functionBody("addToAiMenuItem");
expect(addToAi).toContain("selectedTableRows.value.map((row) => ({ name: row.name, schema: row.schema }))");
expect(addToAi).toContain('emit("addToAi", targets)');
expect(addToAi).toContain('t("contextMenu.addToAiMultiple", { count })');
expect(addToAi).toContain('t("contextMenu.addToAi")');
});
});
@@ -1,14 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const objectSourceDialogSource = readFileSync(new URL("../ObjectSourceDialog.vue", import.meta.url), "utf8");
describe("ObjectSourceDialog edit draft wiring (issue #5057)", () => {
it("seeds the initial edit draft from the pretty-printed source, not the raw single-line definition", () => {
expect(objectSourceDialogSource).toMatch(/draft\.value = nextEditing && canEdit\.value \? resolveObjectSourceEditDraft\(props\.databaseType, resolvedType, formatted, editable\) : "";/);
});
it("seeds the draft opened via the Edit button from the pretty-printed source, not the raw single-line definition", () => {
expect(objectSourceDialogSource).toMatch(/function editSource\(\) \{[\s\S]*?draft\.value = resolveObjectSourceEditDraft\(props\.databaseType, resolvedObjectType\.value, content\.value, editableText\.value\);/);
});
});
@@ -1,41 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const objectBrowserSource = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
const databaseBrowserSource = readFileSync(new URL("../DatabaseBrowser.vue", import.meta.url), "utf8");
function functionBody(source: string, name: string): string {
const signature = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\{`, "m").exec(source);
if (!signature) throw new Error(`Missing function ${name}`);
const bodyStart = signature.index + signature[0].length;
let depth = 1;
for (let index = bodyStart; index < source.length; index += 1) {
if (source[index] === "{") depth += 1;
else if (source[index] === "}") depth -= 1;
if (depth === 0) return source.slice(bodyStart, index);
}
throw new Error(`Unclosed function ${name}`);
}
describe("database and object browser search clear controls", () => {
it("renders a clear button with input padding on both search surfaces", () => {
expect(objectBrowserSource).toContain('class="h-7 pl-8 pr-6 text-xs"');
expect(objectBrowserSource).toContain(':aria-label="t(\'common.clear\')" @click="clearObjectSearch"');
expect(objectBrowserSource).toContain('<button v-if="search" type="button"');
expect(databaseBrowserSource).toContain('class="h-7 pl-8 pr-6 text-xs"');
expect(databaseBrowserSource).toContain(':aria-label="t(\'common.clear\')" @click="clearDatabaseSearch"');
expect(databaseBrowserSource).toContain('<button v-if="search" type="button"');
});
it("clears and refocuses each search input", () => {
expect(functionBody(objectBrowserSource, "clearObjectSearch")).toContain('search.value = "";');
expect(functionBody(objectBrowserSource, "clearObjectSearch")).toContain("getSearchInput()?.focus();");
expect(functionBody(databaseBrowserSource, "clearDatabaseSearch")).toContain('search.value = "";');
expect(functionBody(databaseBrowserSource, "clearDatabaseSearch")).toContain("searchInput.value?.$el?.focus();");
});
it("keeps the ObjectBrowser keyboard clear path on the shared handler", () => {
expect(functionBody(objectBrowserSource, "onSearchKeydown")).toContain("clearObjectSearch();");
});
});
@@ -1,38 +0,0 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
function source(relative: string): string {
return readFileSync(fileURLToPath(new URL(relative, import.meta.url)), "utf8");
}
/**
* Slices the body of one sidebar menu builder out of SidebarTreeRuntimeHost.vue.
* Matching the builder rather than a formatted line keeps this guard indifferent
* to how oxfmt lays the menu pushes out.
*/
function sidebarMenuBuilder(name: string): string {
const text = source("../../sidebar/SidebarTreeRuntimeHost.vue");
const start = text.indexOf(`function ${name}(`);
expect(start, `${name} not found`).toBeGreaterThan(-1);
const next = text.indexOf("\nfunction ", start + 1);
return text.slice(start, next === -1 ? undefined : next);
}
describe("documentation menu placement", () => {
it("registers the entry on database and schema nodes", () => {
// The docs viewer documents a database or a schema, so it belongs on the
// tree nodes that name one. buildDatabaseSidebarMenu serves both types.
expect(sidebarMenuBuilder("buildDatabaseSidebarMenu")).toContain("docs.title");
});
it("keeps the entry off the sidebar's per-object menu", () => {
expect(sidebarMenuBuilder("buildObjectSidebarMenu")).not.toContain("docs.title");
});
it("keeps the entry off the object browser's menus", () => {
// openDocs never read the row's table: a table-level entry advertised a
// scope the viewer cannot render.
expect(source("../ObjectBrowser.vue")).not.toContain("docs.title");
});
});
@@ -1,13 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const quickOpenDialogSource = readFileSync(new URL("../QuickOpenDialog.vue", import.meta.url), "utf8");
describe("QuickOpenDialog theme styles", () => {
it("keeps the shared corner style and readable foreground text", () => {
expect(quickOpenDialogSource).toContain('class="max-w-2xl p-0 gap-0 rounded-lg overflow-hidden"');
expect(quickOpenDialogSource).toContain("background-color: var(--warning-bg) !important;");
expect(quickOpenDialogSource).not.toContain("border-radius: 0.75rem;");
expect(quickOpenDialogSource).not.toContain("color: var(--warning) !important;");
});
});
@@ -1,21 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../DatabaseSearchDialog.vue", import.meta.url), "utf8");
describe("DatabaseSearchDialog layout", () => {
it("keeps the header and footer visible within the dynamic viewport", () => {
expect(dialogSource).toContain('class="flex max-h-[calc(var(--dbx-viewport-height)-6rem)] min-h-0 max-w-4xl flex-col overflow-hidden gap-0 p-0"');
expect(dialogSource).toContain('<DialogHeader class="shrink-0 border-b px-5 py-4">');
expect(dialogSource).toContain('<DialogFooter class="shrink-0 border-t px-5 py-3">');
});
it("scrolls the middle region without removing result-list scrolling", () => {
expect(dialogSource).toContain('class="min-h-0 flex-1 space-y-4 overflow-y-auto px-5 py-4"');
expect(dialogSource).toContain('class="max-h-[360px] space-y-2 overflow-auto pr-1"');
});
it("preserves the footer close action", () => {
expect(dialogSource).toContain('@click="dialogOpen = false"');
});
});
@@ -1,18 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync(new URL("../TableStructureEditor.vue", import.meta.url), "utf8");
describe("TableStructureEditor constraints", () => {
it("renders complete check expressions instead of truncating them", () => {
expect(source).toContain('class="mt-1 whitespace-pre-wrap break-words font-mono text-muted-foreground">{{ constraint.definition }}');
expect(source).not.toContain('class="mt-1 truncate font-mono text-muted-foreground">{{ constraint.definition }}');
});
it("isolates secondary metadata failures to their owning tabs", () => {
expect(source).toContain("Promise.allSettled([indexesPromise, foreignKeysPromise, constraintsPromise, triggersPromise])");
expect(source).toContain("secondaryMetadataErrors.value[facet] = result.reason?.message || String(result.reason)");
expect(source).toContain('v-else-if="secondaryMetadataErrors.constraints"');
expect(source).not.toContain("errorMessage.value = reason?.message || String(reason)");
});
});
@@ -1,41 +0,0 @@
// Source-contract guard for the ErrorBanner "centered" variant (the query
// execution error panel rendered by DataGrid).
// Overview: the centered banner is the only scroll surface for execution
// errors — every ancestor up the results pane is overflow-hidden (DataGrid
// root and content cell, splitpanes pane), so the pane height must reach the
// message box through the flex chain (banner root `flex-1 min-h-0`). A fixed
// `max-h-*` cap on the message box breaks that chain: long error text gets
// clipped at the cap, and in a short pane the tiny internal scrollbar is
// clipped out of view entirely — see #7960 (错误信息面板无法展示完整错误信息).
//
// These guarantees are what stop that from re-happening:
// 1. The message box keeps `min-h-0` (the flex-shrink enabler) and
// `overflow-y-auto` (the internal scroller) with NO fixed max-height.
// 2. The message text keeps `whitespace-pre-wrap break-words`: backend
// errors are multi-line ("summary\n\ndetail"); `break-all` collapsed
// those newlines into a dense wall of text, and without wrap guards a
// long token could force horizontal overflow.
// If either is removed, the clipping regresses. Keep these assertions updated
// alongside the component whenever the class strings are touched.
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const errorBannerSource = readFileSync(new URL("../ErrorBanner.vue", import.meta.url), "utf8");
describe("ErrorBanner centered layout guard", () => {
it("bounds the centered message box by the flex chain, not a fixed max-height", () => {
// The box must stay shrinkable in both axes (`min-h-0 min-w-0`) and scroll internally
// (`overflow-y-auto`); a fixed `max-h-48` cap re-introduces #7960.
expect(errorBannerSource).toContain('class="min-h-0 min-w-0 max-w-lg overflow-y-auto space-y-1 select-text text-destructive"');
expect(errorBannerSource).not.toContain("max-h-48");
});
it("preserves backend error newlines and wraps long tokens", () => {
// `text-left` keeps multi-line errors scannable under the centered
// variant's inherited `text-center` (single-line messages are unaffected:
// the box hugs its content).
expect(errorBannerSource).toContain("text-xs whitespace-pre-wrap break-words text-left cursor-text");
});
});
@@ -1,43 +0,0 @@
// Source-contract guard for the shared dialog layout primitives.
// Overview: `DialogContent` renders a single-column CSS grid (no explicit
// `grid-template-columns`). By default a grid's implicit track sizes to the
// *max-content* of its children, so any child with a large min-content (e.g. a
// footer that lays out many `whitespace-nowrap` buttons on one line) can push
// the track wider than the dialog's `max-w-*`. The dialog's `overflow-hidden`
// then clips the right edge without any scrollbar or ellipsis — see #7325
// (Update dialog clips release notes and buttons on wide locales).
//
// These two guarantees are what stop that from re-happening:
// 1. `grid-cols-[minmax(0,1fr)]` pins the track to the dialog's available
// width and lets grid items shrink (equivalent to `min-width: 0`).
// 2. `sm:flex-wrap` on the footer lets a long row of buttons wrap instead of
// forcing the track wide.
// If either is removed, the clipping regresses. Keep these assertions updated
// alongside the components whenever the class strings are touched.
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogContentSource = readFileSync(new URL("../DialogContent.vue", import.meta.url), "utf8");
const dialogFooterSource = readFileSync(new URL("../DialogFooter.vue", import.meta.url), "utf8");
describe("dialog layout guard", () => {
it("pins the DialogContent grid track to the dialog width so children can shrink", () => {
// The base class (before `props.class`) must lock the implicit track to the
// available width. Without this, a wide grid item re-expands the track beyond
// `max-w-*` and the dialog's `overflow-hidden` clips content (issue #7325).
expect(dialogContentSource).toContain("relative grid grid-cols-[minmax(0,1fr)]");
// Keep it on the shared base class, not a per-dialog override, so every dialog
// that reuses DialogContent is protected.
expect(dialogContentSource).toMatch(/'[^']*relative grid grid-cols-\[minmax\(0,1fr\)\]/);
});
it("lets the dialog footer wrap so a row of buttons does not widen the track", () => {
// At the sm breakpoint the footer is a no-wrap row; a long label (e.g. the
// Spanish "Descargar e instalar") plus other actions then overflows. Wrapping
// keeps the footer's min-content down to one button, which in turn keeps the
// `minmax(0,1fr)` track inside the dialog.
expect(dialogFooterSource).toMatch(/flex flex-col-reverse gap-2 sm:flex-row sm:flex-wrap sm:justify-end/);
});
});
@@ -1,18 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
describe("BigQuery connection dialog", () => {
it("exposes and persists external JDBC driver configuration", () => {
const nativeAgentDriverSupport = dialogSource.match(/function supportsNativeAgentJdbcDriverConfigType\(dbType: DatabaseType\): boolean \{([\s\S]*?)\}/)?.[1] ?? "";
const jdbcBackedDatabaseTypes = dialogSource.match(/const jdbcBackedDatabaseTypes = new Set<DatabaseType>\(\[([\s\S]*?)\]\);/)?.[1] ?? "";
expect(nativeAgentDriverSupport).toContain('dbType === "bigquery"');
expect(jdbcBackedDatabaseTypes).toContain('"bigquery"');
expect(dialogSource).toContain("const supportsNativeAgentJdbcDriverConfig = computed(() => supportsNativeAgentJdbcDriverConfigType(form.value.db_type)");
expect(dialogSource).toContain('<template v-if="supportsNativeAgentJdbcDriverConfig">');
expect(dialogSource).toContain('if (profile.type === "bigquery") {');
expect(dialogSource).toContain("jdbcManualClasspathOpen.value = true;");
});
});
@@ -1,16 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
const tlsCapableDatabaseTypesSource = dialogSource.match(/const tlsCapableDatabaseTypes = new Set<DatabaseType>\(\[([\s\S]*?)\]\);/)?.[1] ?? "";
describe("Dameng SSL connection form", () => {
it("shows Dameng in the TLS tab with certificate directory and password fields", () => {
expect(tlsCapableDatabaseTypesSource).toContain('"dameng"');
expect(dialogSource).toContain("<template v-if=\"form.db_type === 'dameng'\">");
expect(dialogSource).toContain('v-model="damengSslFilesPath"');
expect(dialogSource).toContain('v-model="damengSslKeystorePassword"');
expect(dialogSource).toContain('v-model="damengSslProtocol"');
expect(dialogSource).toContain('t("connection.damengSslVerificationHint")');
});
});
@@ -1,22 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
describe("connection dialog accessibility", () => {
it("moves initial focus to an input inside the dialog", () => {
expect(dialogSource).toContain('<Input v-model="dbSearchQuery" v-connection-dialog-auto-focus');
expect(dialogSource).toContain('<Input v-model="form.name" v-connection-dialog-auto-focus');
expect(dialogSource).toContain("const vConnectionDialogAutoFocus: ObjectDirective<HTMLInputElement>");
expect(dialogSource).toContain("input.focus({ preventScroll: true })");
expect(dialogSource).not.toMatch(/<DialogContent[^>]*\stabindex="-1"/);
});
it("reserves focus ring space around the scrollable category navigation", () => {
const nav = dialogSource.match(/<nav data-connection-category-nav class="([^"]+)"/)?.[1] ?? "";
expect(nav).toContain("px-0.5");
expect(nav).toContain("pt-0.5");
expect(nav).toContain("sm:py-0.5");
});
});
@@ -1,11 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
describe("connection dialog corner style", () => {
it("uses the shared corner radius token for configuration controls", () => {
expect(dialogSource).toContain("border-radius: var(--dbx-radius-fixed-4, 4px);");
expect(dialogSource).not.toMatch(/\.connection-config-step[\s\S]*?border-radius:\s*4px;/);
});
});
@@ -1,30 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
describe("connection dialog scrolling", () => {
it("keeps every configuration tab inside a shrinkable form viewport", () => {
expect(dialogSource).toContain('connection-dialog-content--config${scrollableAdvancedNacos ? " connection-dialog-content--scrollable" : ""}');
expect(dialogSource.match(/<TabsContent[^>]*class="m-0 flex min-h-0 flex-1 flex-col overflow-hidden">/g)).toHaveLength(4);
expect(dialogSource.match(/class="connection-form-body grid min-h-0 flex-1 scroll-pb-6 gap-4 overflow-y-auto/g)).toHaveLength(4);
expect(dialogSource).toMatch(/\.connection-dialog-content--scrollable\s*\{[\s\S]*?height:\s*min\(720px, calc\(var\(--dbx-viewport-height\) - 2rem\)\);/);
expect(dialogSource).toMatch(/@media \(max-height: 720px\)[\s\S]*?height:\s*calc\(var\(--dbx-viewport-height\) - 2rem\);/);
expect(dialogSource).toMatch(/\.connection-dialog-content--config \.connection-form-body\s*\{[\s\S]*?align-content:\s*start;/);
});
it("hides proxy and HTTP tunnel controls for SQLite", () => {
expect(dialogSource).toContain('v-if="!sqliteSshOnlyTransport"');
expect(dialogSource).toContain('v-if="!selectedLayerProfileId && !sqliteSshOnlyTransport"');
});
it("keeps the transport form inside a shrinkable scroll viewport", () => {
expect(dialogSource).toContain('<TabsContent v-if="canUseTransportLayers" value="transport" class="m-0 flex min-h-0 flex-1 flex-col overflow-hidden">');
expect(dialogSource).toContain('class="connection-form-body grid min-h-0 flex-1 scroll-pb-6 gap-4 overflow-y-auto overflow-x-hidden pt-4 pr-2 pb-6"');
});
it("keeps Nacos cards at their content height on both form tabs", () => {
expect(dialogSource.match(/:class="\{ 'connection-form-body--nacos': form\.db_type === 'nacos' \}"/g)).toHaveLength(2);
expect(dialogSource).toMatch(/\.connection-form-body--nacos\s*\{[\s\S]*?grid-auto-rows:\s*max-content;/);
});
});
@@ -1,25 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
function section(startMarker: string, endMarker: string): string {
const start = dialogSource.indexOf(startMarker);
const end = dialogSource.indexOf(endMarker, start);
expect(start).toBeGreaterThanOrEqual(0);
expect(end).toBeGreaterThan(start);
return dialogSource.slice(start, end);
}
describe("Consul connection dialog", () => {
it("keeps Operator feature switches in the Consul form instead of the JDBC form", () => {
const jdbcForm = section("<!-- JDBC: optional external plugin -->", "<!-- Local database files: file path only -->");
const consulForm = section("<!-- Consul KV: HTTP endpoint, ACL token and scope -->", "<!-- etcd: endpoints, user, password, TLS -->");
const operatorModels = ["consulOperatorVisible", "consulOperatorSnapshotRestoreEnabled", "consulOperatorAutopilotWriteEnabled", "consulOperatorRaftWriteEnabled", "consulOperatorKeyringWriteEnabled", "consulOperatorLicenseWriteEnabled"];
for (const model of operatorModels) {
expect(jdbcForm).not.toContain(`v-model="${model}"`);
expect(consulForm).toContain(`v-model="${model}"`);
}
});
});
@@ -1,12 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
describe("GaussDB connection dialog persistence", () => {
it("preserves count-query DOP in submitted connection configs", () => {
const source = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
const submitConfig = source.slice(source.indexOf("function connectionConfigForSubmit"), source.indexOf("function connectionNameForSubmit"));
expect(submitConfig).toContain("const countQueryDop = gaussdbCountQueryDop(config);");
expect(submitConfig).toContain("setGaussdbCountQueryDop(config, countQueryDop);");
});
});
@@ -1,11 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
describe("MongoDB OIDC connection form", () => {
it("keeps the browser authentication hint inside a complete form row", () => {
expect(dialogSource).toContain(`<div v-if="mongoAuthMechanism === 'MONGODB-OIDC'" class="grid grid-cols-4 items-start gap-4">`);
expect(dialogSource).not.toContain(`<p v-if="mongoAuthMechanism === 'MONGODB-OIDC'" class="col-start-2 col-span-3`);
});
});
@@ -1,21 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
const mqttSectionStart = dialogSource.indexOf("<template v-else-if=\"form.db_type === 'mqtt'\">");
const mqttSectionEnd = dialogSource.indexOf("<template v-else-if=\"form.db_type === 'victoriametrics'\">", mqttSectionStart);
const mqttSection = dialogSource.slice(mqttSectionStart, mqttSectionEnd);
describe("MQTT connection dialog", () => {
it("binds TLS switches through the shared Switch model API", () => {
expect(mqttSection).toContain('<Switch v-model="mqttTls" />');
expect(mqttSection).toContain('<Switch v-model="mqttTlsSkipVerify" class="ml-4" />');
expect(mqttSection).not.toContain(":checked=");
expect(mqttSection).not.toContain("@update:checked=");
});
it("serializes both TLS choices into the MQTT external config", () => {
expect(dialogSource).toContain("tls: mqttTls.value");
expect(dialogSource).toContain("tlsSkipVerify: mqttTlsSkipVerify.value");
});
});
@@ -1,13 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
describe("SSH agent connection form", () => {
it("offers SSH agent for a new tunnel and exposes its custom socket path", () => {
expect(dialogSource).toContain('<SelectItem value="agent">{{ t("connection.sshUseAgent") }}</SelectItem>');
expect(dialogSource).toContain('<div v-if="selectedSshLayer.auth_method === \'agent\'" class="grid grid-cols-4 items-center gap-4">');
expect(dialogSource).toContain('v-model="selectedSshLayer.ssh_agent_sock_path"');
expect(dialogSource).not.toContain('v-if="isLegacySshAgentMethod(selectedSshLayer)" value="agent" disabled');
});
});
@@ -1,21 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const managerSource = readFileSync(new URL("../../../components/connection/TunnelProfileManager.vue", import.meta.url), "utf8");
describe("SSH agent tunnel profile form", () => {
it("offers the same SSH agent login method as the connection editor", () => {
expect(managerSource).toContain('<SelectItem value="agent">{{ t("connection.sshUseAgent") }}</SelectItem>');
});
it("exposes the custom agent socket path while keeping the shared auth mapping", () => {
expect(managerSource).toContain(`<div v-if="selectedSsh.auth_method === 'agent'" class="grid grid-cols-4 items-center gap-4">`);
expect(managerSource).toContain('v-model="selectedSsh.ssh_agent_sock_path"');
expect(managerSource).toContain('import { applySshAuthMethod } from "@/lib/connection/sshAuthMethod";');
expect(managerSource).toContain("applySshAuthMethod(profile, value);");
});
it("does not keep a second auth-method mapping that could drift from applySshAuthMethod", () => {
expect(managerSource).not.toContain('value === "key+password" ? "key+password"');
});
});
@@ -1,25 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
describe("ConnectionDialog SSH tunnel test", () => {
it("exposes a dedicated SSH tunnel test without running the database test", () => {
expect(dialogSource).toContain("async function testSshTunnel()");
expect(dialogSource).toContain("await api.testSshTunnel(config)");
expect(dialogSource).toContain('@click="testSshTunnel"');
expect(dialogSource).toContain('t("connection.sshTunnelTest")');
});
it("builds the tunnel probe config without database-specific validation", () => {
const start = dialogSource.indexOf("function connectionConfigForSshTunnelTest");
const end = dialogSource.indexOf("function connectionConfigForSubmit", start);
const helperSource = dialogSource.slice(start, end);
expect(start).toBeGreaterThan(-1);
expect(end).toBeGreaterThan(start);
expect(helperSource).toContain("normalizeTransportLayersForSubmit(config)");
expect(helperSource).not.toContain("kingbaseDatabaseRequired");
expect(helperSource).not.toContain("dynamodbCredentialsRequired");
});
});
@@ -1,32 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
const enSource = readFileSync(new URL("../../../i18n/locales/en.ts", import.meta.url), "utf8");
const zhCnSource = readFileSync(new URL("../../../i18n/locales/zh-CN.ts", import.meta.url), "utf8");
const fallbackLocaleSources = ["az", "es", "it", "ja", "ko", "pt-BR", "tr", "zh-TW"].map((locale) => readFileSync(new URL(`../../../i18n/locales/${locale}.ts`, import.meta.url), "utf8"));
describe("ZooKeeper connection dialog", () => {
it("binds the existing Select component to the ZooKeeper auth_scheme helper", () => {
expect(dialogSource).toContain("setZooKeeperAuthScheme");
expect(dialogSource).toContain("resolveZooKeeperAuthScheme");
expect(dialogSource).toContain("const zookeeperAuthScheme = computed<ZooKeeperAuthScheme>");
expect(dialogSource).toContain('<Select v-model="zookeeperAuthScheme">');
expect(dialogSource).toContain('<SelectItem value="digest">');
expect(dialogSource).toContain('<SelectItem value="sasl_digest">');
});
it("uses localized auth labels and tells users both cluster input locations", () => {
for (const source of [enSource, zhCnSource]) {
expect(source).toContain("zookeeperAuthMethod:");
expect(source).toContain("zookeeperAuthDigest:");
expect(source).toContain("zookeeperAuthSaslDigest:");
expect(source).toContain("zookeeperClusterInputHint:");
}
for (const source of fallbackLocaleSources) {
expect(source).toContain("withEnglishFallback");
}
expect(dialogSource).toContain('t("connection.zookeeperAuthMethod")');
expect(dialogSource).toContain('t("connection.zookeeperClusterInputHint")');
});
});
@@ -1,24 +0,0 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
const dataGridToolbarSource = readFileSync(new URL("../../../components/grid/DataGridToolbar.vue", import.meta.url), "utf8");
const dataGridSource = readFileSync(new URL("../../../components/grid/DataGrid.vue", import.meta.url), "utf8");
describe("data grid toolbar delete row", () => {
it("declares and renders the delete-row action button", () => {
expect(dataGridToolbarSource).toMatch(/deleteRow\?: DataGridToolbarActionCapability/);
expect(dataGridToolbarSource).toMatch(/isDataGridToolbarCapabilityVisible\(deleteRow\)/);
expect(dataGridToolbarSource).toMatch(/triggerDataGridToolbarAction\(deleteRow\)/);
});
it("wires the delete-row capability into the toolbar", () => {
expect(dataGridSource).toMatch(/:delete-row="deleteRowToolbarCapability"/);
});
it("keeps the delete-row capability tied to row deletion state", () => {
expect(dataGridSource).toMatch(/deleteRowToolbarTargetCount = computed\(\(\) => deletableRowIds\(selectedOrCurrentRowIds\(\)\)\.length\)/);
expect(dataGridSource).toMatch(/visible: deleteRowToolbarState\.value\.visible/);
expect(dataGridSource).toMatch(/disabled: deleteRowToolbarState\.value\.disabled/);
expect(dataGridSource).toMatch(/onTrigger: \(\) => \{\s*deleteCurrentRow\(\);/);
});
});
@@ -1,18 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const toolbarSource = readFileSync(new URL("../../../components/grid/DataGridToolbar.vue", import.meta.url), "utf8");
const dataGridSource = readFileSync(new URL("../../../components/grid/DataGrid.vue", import.meta.url), "utf8");
describe("DataGrid geometry layer preview toolbar entry", () => {
it("opens the registered geometry preview action from a visible map button", () => {
expect(toolbarSource).toMatch(/layerPreview\?: DataGridToolbarActionCapability/);
expect(toolbarSource).toMatch(/isDataGridToolbarCapabilityVisible\(layerPreview\)/);
expect(toolbarSource).toMatch(/triggerDataGridToolbarAction\(layerPreview\)/);
expect(toolbarSource).toMatch(/<Map class="data-grid-topbar-action-icon/);
expect(dataGridSource).toMatch(/candidate\.id === "geometry-map-preview"/);
expect(dataGridSource).toMatch(/:layer-preview="layerPreviewToolbarCapability"/);
expect(dataGridSource).toMatch(/executePreviewAction\(action\)/);
});
});
@@ -1,13 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
describe("QueryEditor column prefix cache", () => {
it("hydrates qualified completions from the persistent prefix cache", () => {
const prefixLookups = queryEditorSource.match(/lookupLocalCompletionColumnsByPrefix/g) ?? [];
expect(prefixLookups.length).toBeGreaterThanOrEqual(2);
expect(queryEditorSource).toContain("completionColumnRequestContext(reference)");
});
});
@@ -1,8 +0,0 @@
import { readFileSync } from "node:fs";
import { expect, it } from "vitest";
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
it("maps inline projected columns into local completion results", () => {
expect(queryEditorSource).toMatch(/if \(refTable\.columns\?\.length\) \{[\s\S]*?columnsByTable\.set\([\s\S]*?refTable\.columns\.map/);
});
@@ -1,45 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
// Regression test for issue #8096: clicking a long SQL statement in the
// execution-summary panel (previewStatementRange) used to only recolor the
// gutter line number, which is easy to miss and reads as "click does
// nothing" for statements that are already scrolled into view. The fix adds
// a visible text-highlight decoration alongside the existing gutter marker.
describe("QueryEditor result-source preview highlight", () => {
it("builds a highlight decoration field in addition to the gutter line-number field", () => {
const extensionStart = queryEditorSource.indexOf("buildResultSourceRangeExtension = () => {");
expect(extensionStart).toBeGreaterThan(-1);
const extensionEnd = queryEditorSource.indexOf("\n };", extensionStart);
const extensionBody = queryEditorSource.slice(extensionStart, extensionEnd);
expect(extensionBody).toContain("lineNumberMarkers.from(field)");
expect(extensionBody).toContain("const highlightField = StateField.define({");
expect(extensionBody).toContain('Decoration.mark({ class: "cm-db-result-source-highlight" })');
expect(extensionBody).toContain("EditorView.decorations.from(field)");
expect(extensionBody).toContain("return [field, highlightField];");
});
it("drives both the gutter marker and the highlight decoration from the same effect", () => {
const extensionStart = queryEditorSource.indexOf("buildResultSourceRangeExtension = () => {");
const extensionEnd = queryEditorSource.indexOf("\n };", extensionStart);
const extensionBody = queryEditorSource.slice(extensionStart, extensionEnd);
const effectUses = extensionBody.match(/effect\.is\(effectType\)/g) ?? [];
expect(effectUses.length).toBe(2);
});
it("defines a visible background style for the highlight class", () => {
expect(queryEditorSource).toMatch(/:deep\(\.cm-db-result-source-highlight\)\s*{\s*background:/);
});
it("keeps previewStatementRange wired to setResultSourceRangeEffect via setResultSourceRange", () => {
const fnStart = queryEditorSource.indexOf("function previewStatementRange(");
const fnEnd = queryEditorSource.indexOf("\n}", fnStart);
const fnBody = queryEditorSource.slice(fnStart, fnEnd);
expect(fnBody).toContain("setResultSourceRangeEffect.of({ from, to })");
});
});
@@ -1,12 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
describe("QueryEditor bottom scroll space", () => {
it("allows editable documents to scroll past the last line without changing read-only previews", () => {
expect(queryEditorSource).toMatch(/scrollPastEnd,\s*ViewPlugin/);
expect(queryEditorSource).toMatch(/layer,\s*RectangleMarker/);
expect(queryEditorSource).toContain("props.readOnly ? [] : scrollPastEnd()");
});
});
@@ -1,29 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
const appSource = readFileSync(new URL("../../../App.vue", import.meta.url), "utf8");
const globalStylesSource = readFileSync(new URL("../../../styles/globals.css", import.meta.url), "utf8");
describe("QueryEditor tooltip container", () => {
it("portals CodeMirror tooltips to a zero-sized app host", () => {
expect(queryEditorSource).toContain("const editorElement = editorRef.value;");
expect(queryEditorSource).toContain("if (!editorElement) return;");
expect(queryEditorSource).toContain('querySelector<HTMLElement>("#dbx-query-editor-tooltip-root") ?? editorElement');
expect(queryEditorSource).toContain("tooltips({ parent: tooltipParent })");
expect(queryEditorSource).toContain("new EditorView({ state, parent: editorElement })");
expect(queryEditorSource).not.toContain("tooltips({ parent: document.body })");
expect(appSource).toContain('id="dbx-query-editor-tooltip-root"');
expect(appSource).toContain('class="fixed left-0 top-0 z-[70] h-0 w-0 overflow-visible"');
});
it("keeps the app root viewport-sized without a transformed containing block", () => {
const rootRule = globalStylesSource.match(/html,\s*body,\s*#root\s*\{(?<declarations>[^}]*)\}/)?.groups?.declarations;
expect(rootRule).toBeDefined();
expect(rootRule).toMatch(/width:\s*100%/);
expect(rootRule).toMatch(/height:\s*100%/);
expect(rootRule).toMatch(/overflow:\s*hidden/);
expect(rootRule).not.toMatch(/(?:^|\s)(?:transform|contain)\s*:/);
});
});
@@ -1,19 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const appSource = readFileSync(new URL("../../../App.vue", import.meta.url), "utf8");
const useSqlExecutionSource = readFileSync(new URL("../../../composables/useSqlExecution.ts", import.meta.url), "utf8");
describe("toolbar explain routing", () => {
it("passes the tab id as the execution context, not as SQL", () => {
// Regression: the injected action once called tryExplain(tabId), whose
// first parameter is sqlOverride — the id silently typed as SQL and the
// EXPLAIN button sent the tab UUID to the backend.
expect(appSource).toContain("explain: (tabId: string) => tryExplain(undefined, { tabId }),");
expect(appSource).not.toContain("explain: (tabId: string) => tryExplain(tabId),");
});
it("keeps tryExplain's first parameter an SQL override", () => {
expect(useSqlExecutionSource).toContain("async function tryExplain(sqlOverride?: SqlExecutionOverride, options: SqlExecutionOptions = {})");
});
});
@@ -1,56 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const searchableSelectSource = readFileSync(new URL("../../../components/ui/searchable-select/SearchableSelect.vue", import.meta.url), "utf8");
const editorSettingsDialogSource = readFileSync(new URL("../../../components/editor/EditorSettingsDialog.vue", import.meta.url), "utf8");
const dataTransferDialogSource = readFileSync(new URL("../../../components/transfer/DataTransferDialog.vue", import.meta.url), "utf8");
const editorToolbarSource = readFileSync(new URL("../../../components/layout/EditorToolbar.vue", import.meta.url), "utf8");
const schemaDiffConfigStepSource = readFileSync(new URL("../../../components/diff/SchemaDiffConfigStep.vue", import.meta.url), "utf8");
const dataCompareDialogSource = readFileSync(new URL("../../../components/diff/DataCompareDialog.vue", import.meta.url), "utf8");
describe("SearchableSelect layout", () => {
it("keeps slotted option labels inside a shrinkable overflow boundary", () => {
const labelBoundaries = searchableSelectSource.match(/dbx-searchable-select-option-label min-w-0 flex-1 overflow-hidden/g) ?? [];
expect(labelBoundaries).toHaveLength(3);
});
it("can preserve custom option whitespace for exact database identifiers", () => {
expect(searchableSelectSource).toContain("trimCustom?: boolean");
expect(searchableSelectSource).toContain("trimCustom: true");
expect(searchableSelectSource).toContain("props.trimCustom ? searchText.value.trim() : searchText.value");
});
it("forwards an optional style to its portaled content for local preview isolation", () => {
expect(searchableSelectSource).toContain('contentStyle?: HTMLAttributes["style"]');
expect(searchableSelectSource).toContain(':style="contentStyle"');
expect(editorSettingsDialogSource).toContain(':content-style="{ fontFamily: editUiFontFamily || DEFAULT_UI_FONT_FAMILY }"');
expect(editorSettingsDialogSource).toContain('@option-hover="previewUiFontOption"');
expect(editorSettingsDialogSource).toContain('@option-highlight="previewUiFontOption"');
expect(editorSettingsDialogSource).toContain('@option-leave="restoreUiFontFamilyPreview"');
});
it("renders data transfer connection pickers as sidebar-like trees", () => {
const treeSelects = dataTransferDialogSource.match(/<ConnectionTreeSelect/g) ?? [];
expect(treeSelects).toHaveLength(2);
expect(dataTransferDialogSource).toContain(':layout="store.sidebarLayout"');
expect(dataTransferDialogSource).not.toContain("ConnectionGroupBadge");
});
it("renders the toolbar connection picker as a sidebar-like tree", () => {
expect(editorToolbarSource).toContain("<ConnectionTreeSelect");
expect(editorToolbarSource).toContain(':connections="connectionStore.connections"');
expect(editorToolbarSource).toContain(':layout="connectionStore.sidebarLayout"');
});
it.each([
["schema compare", schemaDiffConfigStepSource],
["data compare", dataCompareDialogSource],
])("renders %s connection pickers as sidebar-like trees", (_name, source) => {
expect(source.match(/<ConnectionTreeSelect/g) ?? []).toHaveLength(2);
expect(source).toContain(':connections="sqlConnections"');
expect(source).toContain(':layout="store.sidebarLayout"');
expect(source).not.toContain("ConnectionGroupBadge");
});
});
@@ -1,13 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const settingsDialogSource = readFileSync(new URL("../../../components/editor/EditorSettingsDialog.vue", import.meta.url), "utf8");
describe("openDataTabsNextToActive settings control", () => {
it("binds the navigation switch through apply and both reset paths", () => {
expect(settingsDialogSource).toContain("const editOpenDataTabsNextToActive = ref(settingsStore.editorSettings.openDataTabsNextToActive)");
expect(settingsDialogSource).toContain("openDataTabsNextToActive: editOpenDataTabsNextToActive.value");
expect(settingsDialogSource.match(/editOpenDataTabsNextToActive\.value = DEFAULT_EDITOR_SETTINGS\.openDataTabsNextToActive/g)).toHaveLength(2);
expect(settingsDialogSource).toContain('id="open-data-tabs-next-to-active" v-model="editOpenDataTabsNextToActive"');
});
});
@@ -1,18 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const appSource = readFileSync(new URL("../../../App.vue", import.meta.url), "utf8");
const settingsDialogSource = readFileSync(new URL("../../../components/editor/EditorSettingsDialog.vue", import.meta.url), "utf8");
const changelogPanelSource = readFileSync(new URL("../../../components/settings/ChangelogPanel.vue", import.meta.url), "utf8");
describe("about page update check", () => {
it("reuses the toolbar update check and pending state", () => {
expect(changelogPanelSource).toContain('"check-updates": []');
expect(changelogPanelSource).toContain("@click=\"emit('check-updates')\"");
expect(changelogPanelSource).toContain(':disabled="checkingUpdates"');
expect(settingsDialogSource).toContain(':checking-updates="props.checkingUpdates"');
expect(settingsDialogSource).toContain("@check-updates=\"emit('check-updates')\"");
expect(appSource).toContain(':checking-updates="checkingUpdates"');
expect(appSource).toContain('@check-updates="checkUpdates()"');
});
});
-50
View File
@@ -1,50 +0,0 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { test } from "vitest";
const aiAssistantPath = fileURLToPath(new URL("../../apps/desktop/src/components/editor/AiAssistant.vue", import.meta.url));
const aiAssistantSource = readFileSync(aiAssistantPath, "utf8");
test("AI mode initializes from the persisted default mode", () => {
// The fallback value is still Ask, but the panel syncs from the configured default.
assert.match(aiAssistantSource, /const assistantMode = ref<AiAssistantMode>\("ask"\)/);
assert.match(aiAssistantSource, /settings\.defaultAiMode/);
assert.doesNotMatch(aiAssistantSource, /"update:mode"/);
// The persisted setting is applied once after the asynchronous config load;
// later setting changes must not alter an active conversation.
assert.match(aiAssistantSource, /watch\([\s\S]*?settings\.isAiConfigLoaded[\s\S]*?defaultModeInitialized/);
assert.match(aiAssistantSource, /assistantMode\.value = settings\.defaultAiMode/);
});
test("startNewChat uses the configured default mode instead of a hardcoded Ask reset", () => {
const start = aiAssistantSource.indexOf("function startNewChat()");
const end = aiAssistantSource.indexOf("\nonMounted", start);
const startNewChat = aiAssistantSource.slice(start, end);
assert.notEqual(start, -1, "startNewChat should exist");
assert.match(startNewChat, /const mode = settings\.defaultAiMode/);
assert.match(startNewChat, /assistantMode\.value = mode/);
assert.match(startNewChat, /activeAction\.value = resolveDefaultAction\(mode\)/);
// The old hardcoded Ask reset must be gone.
assert.doesNotMatch(startNewChat, /assistantMode\.value = "ask"/);
});
test("switching mode in a session never touches the global default setting", () => {
const start = aiAssistantSource.indexOf('function switchModeActionTab(mode: "ask" | "agent")');
const end = aiAssistantSource.indexOf("\nfunction selectModeActionItem", start);
const switchModeActionTab = aiAssistantSource.slice(start, end);
assert.notEqual(start, -1, "switchModeActionTab should exist");
assert.notEqual(end, -1, "selectModeActionItem should follow switchModeActionTab");
// Session switch updates local state only — no store write.
assert.doesNotMatch(switchModeActionTab, /setDefaultAiMode|setAiAssistantDefaultMode/);
assert.doesNotMatch(switchModeActionTab, /if\s*\(\s*assistantMode\.value\s*===\s*mode\s*\)\s*(?:\{\s*)?return/);
const defaultActionReset = switchModeActionTab.indexOf("activeAction.value = resolveDefaultAction(mode)");
const modeChangeGuard = switchModeActionTab.indexOf("if (assistantMode.value !== mode)");
assert.notEqual(defaultActionReset, -1, "mode tab clicks should reset to the mode default action");
assert.notEqual(modeChangeGuard, -1, "mode assignment should still be guarded when unchanged");
assert.ok(defaultActionReset < modeChangeGuard, "default action reset should run before same-mode guard");
});
-67
View File
@@ -1,67 +0,0 @@
import { strict as assert } from "node:assert";
import { readFileSync } from "node:fs";
import { test } from "vitest";
const dataGridSource = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
const settingsDialogSource = readFileSync("apps/desktop/src/components/editor/EditorSettingsDialog.vue", "utf8");
const quickControlSource = readFileSync("apps/desktop/src/components/grid/DataGridFontFamilyControl.vue", "utf8");
test("applies the configured result grid font to DOM and canvas renderers", () => {
assert.match(dataGridSource, /const tableFontFamily = computed\(\(\) => settingsStore\.editorSettings\.tableFontFamily\)/);
assert.match(dataGridSource, /"--dbx-data-grid-font-family": tableFontFamily\.value/);
assert.match(dataGridSource, /class="canvas-grid-surface dbx-data-grid-font-family/);
assert.match(dataGridSource, /class="data-grid-scroller dbx-data-grid-font-family/);
});
test("invalidates grid measurements and canvas rendering when the result font changes", () => {
assert.match(dataGridSource, /columnHeaderMeasurementKey = computed\(\(\) => \[tableFontSize\.value, tableFontFamily\.value, dataGridFontReadyTick\.value, columnFormatterReadyTick\.value\]\)/);
assert.match(dataGridSource, /columnHeaderMeasureContext\.font = `600 \$\{tableFontSize\.value\}px \$\{tableFontFamily\.value\}`/);
assert.match(dataGridSource, /canvasRenderStyleKey = computed\(\(\) => `[^`]*\$\{tableFontFamily\.value\}:\$\{tableFontSize\.value\}:\$\{!!saveError\.value\}/);
});
test("invalidates canvas rendering when the data type color scheme changes", () => {
// The canvas caches its resolved paint theme by this key, so a recolor that is
// not represented here would leave the grid painted with the previous palette.
assert.match(dataGridSource, /canvasRenderStyleKey = computed\(\(\) => `[^`]*\$\{dataGridTypeColorKey\.value\}`\)/);
assert.match(dataGridSource, /resolveActiveDataGridTypeColors\(settings\.dataGridTypeColorSchemes, settings\.activeDataGridTypeColorSchemeId\)/);
});
test("waits only for the configured result font before remeasuring headers", () => {
assert.match(dataGridSource, /document\.fonts\.load\(`600 \$\{fontSize\}px \$\{fontFamily\}`\)/);
assert.doesNotMatch(dataGridSource, /document\.fonts\.addEventListener\("loadingdone"/);
});
test("places the result grid font beside the interface font in appearance settings", () => {
const editorSectionStart = settingsDialogSource.indexOf(`activeSettingsTab === 'editor'`);
const appearanceSectionStart = settingsDialogSource.indexOf(`activeSettingsTab === 'appearance'`);
const resultGridFontField = settingsDialogSource.indexOf(`<Label class="min-w-0 whitespace-normal leading-tight">{{ t("settings.dataGridFontFamily") }}</Label>`);
assert.ok(editorSectionStart >= 0);
assert.ok(appearanceSectionStart > editorSectionStart);
assert.ok(resultGridFontField > appearanceSectionStart);
assert.equal(settingsDialogSource.slice(editorSectionStart, appearanceSectionStart).includes(`settings.dataGridFontFamily`), false);
});
test("keeps appearance section spacing and help icons aligned without stacked margins", () => {
assert.doesNotMatch(settingsDialogSource, /\.settings-appearance-section > \* \+ \*/);
assert.match(settingsDialogSource, /activeSettingsTab === 'appearance'" class="settings-appearance-section flex flex-col gap-4 py-2"/);
assert.match(settingsDialogSource, /<div class="flex min-w-0 items-center gap-1">\s*<Label class="min-w-0 whitespace-normal leading-tight">\{\{ t\("settings\.dataGridFontFamily"\) \}\}<\/Label>/);
});
test("matches the result grid font selector styling with appearance form controls", () => {
const selectorStart = settingsDialogSource.indexOf(`:model-value="editTableFontFamily"`);
const selectorEnd = settingsDialogSource.indexOf(`</SearchableSelect>`, selectorStart);
const selectorSource = settingsDialogSource.slice(selectorStart, selectorEnd);
assert.ok(selectorStart >= 0);
assert.ok(selectorEnd > selectorStart);
// SearchableSelect defaults to outline; appearance fonts share the h-8 control chrome class.
assert.match(selectorSource, /:trigger-class="appearanceFontSearchTriggerClass"/);
assert.match(settingsDialogSource, /const fontSearchTriggerClass =\s*"h-8 w-full max-w-none justify-between/);
});
test("uses a table font label in view options without changing the settings label", () => {
assert.match(quickControlSource, /t\("grid\.tableFontFamily"\)/);
assert.doesNotMatch(quickControlSource, /t\("settings\.dataGridFontFamily"\)/);
assert.match(settingsDialogSource, /t\("settings\.dataGridFontFamily"\)/);
});
-12
View File
@@ -1,12 +0,0 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "vitest";
test("data grid selection colors stay on the classic blue palette", () => {
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
assert.match(source, /--data-grid-cell-selected-bg:\s*rgb\(239,\s*246,\s*255\)/);
assert.match(source, /--data-grid-cell-selected-border:\s*rgb\(59,\s*130,\s*246\)/);
assert.match(source, /"--data-grid-cell-selected-bg":\s*dark \? "rgb\(20, 40, 60\)" : "rgb\(239, 246, 255\)"/);
assert.doesNotMatch(source, /--data-grid-cell-selected-border:\s*color-mix\(in srgb, var\(--ring\)/);
assert.doesNotMatch(source, /--data-grid-cell-selected-bg:\s*color-mix\(in srgb, var\(--primary\)/);
});
-19
View File
@@ -1,19 +0,0 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { test } from "vitest";
function source(relativePath: string): string {
return readFileSync(path.resolve(relativePath), "utf8");
}
test("driver installation operation IDs support insecure HTTP contexts", () => {
const driverStore = source("apps/desktop/src/components/config/DriverStoreDialog.vue");
const connectionDialog = source("apps/desktop/src/components/connection/ConnectionDialog.vue");
assert.match(driverStore, /import \{ uuid \} from "@\/lib\/common\/utils"/);
assert.doesNotMatch(driverStore, /crypto\.randomUUID\(\)/);
assert.match(driverStore, /activeAgentOperationId\.value = uuid\(\)/);
assert.doesNotMatch(connectionDialog, /crypto\.randomUUID\(\)/);
assert.match(connectionDialog, /agentInstallOperationId\.value = uuid\(\)/);
});
-47
View File
@@ -1,47 +0,0 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { test } from "vitest";
function source(relativePath: string): string {
return readFileSync(path.resolve(relativePath), "utf8");
}
test("keeps JDBC outside the database picker and exposes a dedicated entry", () => {
const content = source("apps/desktop/src/components/connection/ConnectionDialog.vue");
const optionsStart = content.indexOf("const dbOptions: DbOption[] = [");
const optionsEnd = content.indexOf("const dbCategories", optionsStart);
const pickerToolbar = content.indexOf("sm:justify-between");
const searchInput = content.indexOf('v-model="dbSearchQuery"', pickerToolbar);
const jdbcEntry = content.indexOf("data-jdbc-connection-entry", pickerToolbar);
assert.notEqual(optionsStart, -1);
assert.notEqual(optionsEnd, -1);
assert.equal(content.slice(optionsStart, optionsEnd).includes('{ value: "jdbc"'), false);
assert.ok(pickerToolbar < searchInput && searchInput < jdbcEntry);
assert.match(content.slice(jdbcEntry, jdbcEntry + 400), /goToConnectionStep\('jdbc'\)/);
});
test("opens driver management on the JDBC tab from JDBC connection settings", () => {
const connectionDialog = source("apps/desktop/src/components/connection/ConnectionDialog.vue");
const appDialogs = source("apps/desktop/src/components/layout/AppDialogs.vue");
const app = source("apps/desktop/src/App.vue");
const driverStore = source("apps/desktop/src/components/config/DriverStoreDialog.vue");
assert.equal(connectionDialog.match(/emit\('openDriverStore', \{ target: 'tab', tab: 'jdbc' \}\)/g)?.length, 1);
assert.match(connectionDialog, /isJdbcProductConnection\.value \? agentDriverFocus\.value : \{ target: "tab", tab: "jdbc" \}/);
assert.match(appDialogs, /@open-driver-store="emit\('openDriverStore', \$event\)"/);
assert.match(app, /openDriverStorePage\(\$event\)/);
assert.match(app, /v-model:active-tab="driverStoreActiveTab"/);
assert.match(driverStore, /"update:activeTab": \[tab: "agent" \| "jdbc" \| "storage" \| "runtime"\]/);
});
test("resets the driver management tab after the page is closed", () => {
const app = source("apps/desktop/src/App.vue");
const closeStart = app.indexOf("function closeDriverStorePage() {");
const closeEnd = app.indexOf("\n}", closeStart);
assert.notEqual(closeStart, -1);
assert.notEqual(closeEnd, -1);
assert.match(app.slice(closeStart, closeEnd), /driverStoreActiveTab\.value = "agent"/);
});
-32
View File
@@ -1,32 +0,0 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { test } from "vitest";
const driverStoreSource = readFileSync(path.resolve("apps/desktop/src/components/config/DriverStoreDialog.vue"), "utf8");
function buttonOpeningTag(clickHandler: string): string {
const click = `@click="${clickHandler}"`;
const clickIndex = driverStoreSource.indexOf(click);
assert.notEqual(clickIndex, -1, `missing button for ${clickHandler}`);
const startIndex = driverStoreSource.lastIndexOf("<Button", clickIndex);
const endIndex = driverStoreSource.indexOf(">", clickIndex);
assert.notEqual(startIndex, -1, `missing button start for ${clickHandler}`);
assert.notEqual(endIndex, -1, `missing button end for ${clickHandler}`);
return driverStoreSource.slice(startIndex, endIndex + 1);
}
test("keeps Agent offline packages out of the JDBC tab", () => {
const agentOfflineButton = buttonOpeningTag("importOfflineZip");
assert.match(agentOfflineButton, /v-if="driverStoreTab === 'agent'"/);
assert.match(driverStoreSource, /api\.importAgentsFromZip\(selected, activeAgentOperationId\.value\)/);
});
test("keeps local JDBC plugin installation available after installation", () => {
const jdbcPluginLocalButton = buttonOpeningTag("installJdbcPluginLocal");
assert.doesNotMatch(jdbcPluginLocalButton, /v-if=/);
assert.match(jdbcPluginLocalButton, /:disabled="isInstallingJdbcPlugin \|\| isUninstallingJdbcPlugin"/);
assert.match(driverStoreSource, /api\.installJdbcPluginLocal\(selected\)/);
});
-10
View File
@@ -1,10 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync("apps/desktop/src/components/common/JsonTree.vue", "utf8");
describe("JSON tree layout", () => {
it("keeps a collapsed container closing bracket beside its summary", () => {
expect(source).toMatch(/\.json-tree-summary\s*\{\s*flex: 0 1 auto;\s*min-width: 0;\s*\}/);
});
});
-16
View File
@@ -1,16 +0,0 @@
import { strict as assert } from "node:assert";
import { readFileSync } from "node:fs";
import { test } from "vitest";
const repositoryRoot = new URL("../../", import.meta.url);
const runtimeHost = readFileSync(new URL("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", repositoryRoot), "utf8");
const connectionTree = readFileSync(new URL("apps/desktop/src/components/sidebar/ConnectionTree.vue", repositoryRoot), "utf8");
const treeItem = readFileSync(new URL("apps/desktop/src/components/sidebar/TreeItem.vue", repositoryRoot), "utf8");
test("connection F2 rename uses the rendered row while explicit edit stays unchanged", () => {
assert.match(runtimeHost, /const editTarget = selectedConnectionEditTarget[\s\S]*?emit\("request-connection-rename", editTarget\.connectionId\)/);
assert.match(runtimeHost, /function requestEditSelectedConnection\(\): boolean \{[\s\S]*?connectionStore\.startEditing\(editTarget\.connectionId\)/);
assert.match(connectionTree, /@request-connection-rename="startRenamingConnectionNode"/);
assert.match(treeItem, /activeNode\.value\.type === "connection"\) startRenameConnection\(\)/);
assert.match(treeItem, /async function finishRenameConnection\(\)[\s\S]*?connectionStore\.renameConnection\(connectionId, trimmed\)/);
});
-50
View File
@@ -1,50 +0,0 @@
import { strict as assert } from "node:assert";
import { readFileSync } from "node:fs";
import { test } from "vitest";
const connectionTreeSource = readFileSync("apps/desktop/src/components/sidebar/ConnectionTree.vue", "utf8");
const appSidebarSource = readFileSync("apps/desktop/src/components/layout/AppSidebar.vue", "utf8");
const activeConnectionFilterSource = readFileSync("apps/desktop/src/components/sidebar/ActiveConnectionFilterButton.vue", "utf8");
const locateButtonSource = readFileSync("apps/desktop/src/components/sidebar/SidebarLocateButton.vue", "utf8");
const regexButtonSource = readFileSync("apps/desktop/src/components/sidebar/SidebarRegexToggleButton.vue", "utf8");
const toolbarStart = connectionTreeSource.indexOf('<div class="connection-tree-search');
const toolbarEnd = connectionTreeSource.indexOf("<CustomContextMenu", toolbarStart);
const toolbarSource = connectionTreeSource.slice(toolbarStart, toolbarEnd);
const toolbarActionSource = `${toolbarSource}\n${activeConnectionFilterSource}\n${locateButtonSource}\n${regexButtonSource}`;
test("connection tree toolbar uses app tooltips for icon-only actions", () => {
for (const tooltip of [`<LightTooltip :text="t('sidebar.locateActiveTab')" side="top" :delay="300" nowrap>`, `<LightTooltip :text="sidebarListOptionsLabel" side="top" :delay="300" nowrap>`]) {
assert.ok(toolbarSource.includes(tooltip));
}
assert.ok(toolbarSource.includes("<ActiveConnectionFilterButton"));
assert.ok(toolbarSource.includes("<SidebarLocateButton"));
assert.ok(locateButtonSource.includes("<LocateFixed"));
assert.ok(activeConnectionFilterSource.includes(`<LightTooltip :text="t('sidebar.showActiveConnectionsOnly')" side="top" :delay="300" nowrap>`));
assert.doesNotMatch(toolbarActionSource, /:title="t\('sidebar\.(?:locateActiveTab|showActiveConnectionsOnly)'\)"/);
assert.doesNotMatch(toolbarSource, /:trigger-title=/);
});
test("connection tree toolbar groups low-frequency controls", () => {
assert.ok(toolbarSource.includes("pr-[4.75rem]"));
assert.ok(toolbarSource.includes("sidebar.regexSearchTooltip"));
assert.ok(toolbarSource.indexOf("sidebar.globalLocalSearchTooltip") < toolbarSource.indexOf("sidebar.locateActiveTab"));
assert.ok(locateButtonSource.includes("h-6 w-6 shrink-0"));
assert.ok(toolbarSource.includes("<SidebarRegexToggleButton"));
assert.ok(toolbarSource.includes(':items="sidebarListOptionItems"'));
assert.ok(connectionTreeSource.includes('groupLabel: index === 0 ? t("sidebar.sortConnections") : undefined'));
assert.ok(connectionTreeSource.includes('groupLabel: index === 0 ? t("sidebar.filterByType") : undefined'));
assert.ok(!toolbarSource.includes(':label="sidebarListOptionsLabel"'));
assert.equal((toolbarSource.match(/<LightDropdown/g) ?? []).length, 1);
assert.equal((toolbarSource.match(/border border-border/g) ?? []).length, 1);
});
test("connection sidebar exposes import and export actions", () => {
assert.ok(appSidebarSource.includes(':trigger-icon="ArrowDownUp"'));
assert.ok(appSidebarSource.includes('<ChevronsDownUp class="h-3 w-3" />'));
assert.ok(appSidebarSource.includes(':trigger-label="connectionTransferLabel"'));
assert.ok(appSidebarSource.includes('t("sidebar.importExport")'));
assert.ok(appSidebarSource.includes(':items="connectionTransferItems"'));
assert.ok(appSidebarSource.includes('@update:model-value="selectConnectionTransferAction"'));
assert.doesNotMatch(appSidebarSource, /:trigger-icon="Ellipsis"/);
});
-15
View File
@@ -1,15 +0,0 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const sqlPreviewSource = readFileSync("apps/desktop/src/components/editor/SqlPreviewPanel.vue", "utf8");
const sqlHighlighterSource = readFileSync("apps/desktop/src/lib/sql/sqlHighlighter.ts", "utf8");
describe("SQL preview wrapping", () => {
it("renders highlighted SQL inline inside a wrapping preformatted block", () => {
expect(sqlPreviewSource).toContain('themePreset: "preview"');
expect(sqlHighlighterSource).toContain('structure: "inline"');
expect(sqlPreviewSource).toContain('<pre v-else-if="highlightedHtml"');
expect(sqlPreviewSource).toContain("whitespace-pre-wrap break-words");
expect(sqlPreviewSource).toContain('v-html="highlightedHtml"');
});
});
-22
View File
@@ -1,22 +0,0 @@
import { readFileSync } from "node:fs";
import { strict as assert } from "node:assert";
import { test } from "vitest";
const source = readFileSync("apps/desktop/src/components/import/TableImportDialog.vue", "utf8");
test("positions the import data type picker from the focused input", () => {
assert.match(source, /function openDataTypePicker\(sourceColumn: string, input: HTMLInputElement\)/);
assert.match(source, /@focus="\(event\) => openDataTypePicker\(sourceColumn, event\.currentTarget as HTMLInputElement\)"/);
assert.doesNotMatch(source, /document\.querySelector<HTMLElement>\(`\[data-dt-input\]/);
});
test("dismisses and navigates the import data type picker with the keyboard", () => {
assert.match(source, /@blur="closeDataTypePicker"/);
assert.match(source, /@keydown="\(event\) => handleDataTypePickerKeydown\(event, sourceColumn, event\.currentTarget as HTMLInputElement\)"/);
assert.match(source, /!dataTypePickerOpen\.value && \(event\.key === "ArrowDown" \|\| event\.key === "ArrowUp"\)/);
assert.match(source, /event\.key === "Escape"/);
assert.match(source, /event\.key === "ArrowDown"/);
assert.match(source, /event\.key === "ArrowUp"/);
assert.match(source, /event\.key === "Enter"/);
assert.match(source, /if \(step !== "mapping"\) closeDataTypePicker\(\)/);
});