385 lines
17 KiB
TypeScript
385 lines
17 KiB
TypeScript
import { test } from "vitest";
|
|
import assert from "node:assert/strict";
|
|
import { createPinia, setActivePinia } from "pinia";
|
|
import { DEFAULT_SQL_FORMATTER_SETTINGS } from "../../apps/desktop/src/lib/sqlFormatterConfig.ts";
|
|
import { AI_PROVIDER_PRESETS, DEFAULT_EDITOR_SETTINGS, normalizeAiConfig, normalizeEditorSettings, useSettingsStore } from "../../apps/desktop/src/stores/settingsStore.ts";
|
|
|
|
const OLD_FONT_SIZE_KEY = "dbx-query-editor-font-size";
|
|
|
|
function withMockLocalStorage(initial: Record<string, string>, run: () => void) {
|
|
const previousDescriptor = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
|
|
const values = new Map(Object.entries(initial));
|
|
const localStorageMock = {
|
|
getItem: (key: string) => values.get(key) ?? null,
|
|
setItem: (key: string, value: string) => {
|
|
values.set(key, value);
|
|
},
|
|
removeItem: (key: string) => {
|
|
values.delete(key);
|
|
},
|
|
clear: () => {
|
|
values.clear();
|
|
},
|
|
};
|
|
|
|
Object.defineProperty(globalThis, "localStorage", {
|
|
configurable: true,
|
|
value: localStorageMock,
|
|
});
|
|
|
|
try {
|
|
run();
|
|
} finally {
|
|
if (previousDescriptor) {
|
|
Object.defineProperty(globalThis, "localStorage", previousDescriptor);
|
|
} else {
|
|
delete (globalThis as any).localStorage;
|
|
}
|
|
}
|
|
}
|
|
|
|
test("defaults Redis scan page size to 1000 keys", () => {
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.redisScanPageSize, 1000);
|
|
assert.equal(normalizeEditorSettings({}).redisScanPageSize, 1000);
|
|
});
|
|
|
|
test("keeps a saved Redis scan page size", () => {
|
|
assert.equal(normalizeEditorSettings({ redisScanPageSize: 5000 }).redisScanPageSize, 5000);
|
|
});
|
|
|
|
test("normalizes saved query result page size", () => {
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.pageSize, 100);
|
|
assert.equal(normalizeEditorSettings({ pageSize: 5000 }).pageSize, 5000);
|
|
assert.equal(normalizeEditorSettings({ pageSize: 200000 }).pageSize, 100000);
|
|
assert.equal(normalizeEditorSettings({ pageSize: 0 }).pageSize, 100);
|
|
});
|
|
|
|
test("defaults export batch size to 10000 rows", () => {
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.exportBatchSize, 10000);
|
|
assert.equal(normalizeEditorSettings({}).exportBatchSize, 10000);
|
|
assert.equal(normalizeEditorSettings({ exportBatchSize: 2000 }).exportBatchSize, 2000);
|
|
});
|
|
|
|
test("normalizes editor theme settings", () => {
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.theme, "app");
|
|
assert.equal(normalizeEditorSettings({}).theme, "app");
|
|
assert.equal(normalizeEditorSettings({ theme: "app" }).theme, "app");
|
|
assert.equal(normalizeEditorSettings({ theme: "vscode-light" }).theme, "vscode-light");
|
|
assert.equal(normalizeEditorSettings({ theme: "invalid" as any }).theme, DEFAULT_EDITOR_SETTINGS.theme);
|
|
});
|
|
|
|
test("defaults dangerous SQL confirmation to enabled", () => {
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution, true);
|
|
assert.equal(normalizeEditorSettings({}).confirmDangerousSqlExecution, true);
|
|
assert.equal(normalizeEditorSettings({ confirmDangerousSqlExecution: false }).confirmDangerousSqlExecution, false);
|
|
});
|
|
|
|
test("defaults update notifications to enabled", () => {
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled, true);
|
|
assert.equal(normalizeEditorSettings({}).updateNotificationsEnabled, true);
|
|
assert.equal(normalizeEditorSettings({ updateNotificationsEnabled: false } as any).updateNotificationsEnabled, false);
|
|
});
|
|
|
|
test("defaults shortcut settings", () => {
|
|
const settings = normalizeEditorSettings({});
|
|
|
|
assert.equal(settings.shortcuts.executeSql, "Mod+Enter");
|
|
assert.equal(settings.shortcuts.saveSql, "Mod+S");
|
|
assert.equal(settings.shortcuts.copyCurrentRow, "Mod+D");
|
|
assert.equal(settings.shortcuts.deleteCurrentRow, "Delete");
|
|
assert.equal(settings.shortcuts.newQuery, "Mod+T");
|
|
assert.equal(settings.shortcuts.openSettings, "Mod+,");
|
|
assert.equal(settings.shortcuts.focusSearch, "Mod+F");
|
|
assert.equal(settings.shortcuts.zoomInUi, "Mod+=");
|
|
assert.equal(settings.shortcuts.zoomOutUi, "Mod+-");
|
|
assert.equal(settings.shortcuts.resetUiZoom, "Mod+0");
|
|
assert.equal(settings.shortcuts.refreshData, "F5");
|
|
assert.equal(settings.shortcuts.toggleTranspose, "Tab");
|
|
});
|
|
|
|
test("keeps saved shortcut overrides", () => {
|
|
const settings = normalizeEditorSettings({
|
|
shortcuts: {
|
|
executeSql: "Shift+Mod+Enter",
|
|
copyCurrentRow: "Alt+Shift+D",
|
|
deleteCurrentRow: "Backspace",
|
|
newQuery: "Shift+Mod+N",
|
|
openSettings: "Shift+Mod+P",
|
|
zoomInUi: "Alt+Mod+=",
|
|
} as any,
|
|
});
|
|
|
|
assert.equal(settings.shortcuts.executeSql, "Shift+Mod+Enter");
|
|
assert.equal(settings.shortcuts.copyCurrentRow, "Alt+Shift+D");
|
|
assert.equal(settings.shortcuts.deleteCurrentRow, "Backspace");
|
|
assert.equal(settings.shortcuts.newQuery, "Shift+Mod+N");
|
|
assert.equal(settings.shortcuts.openSettings, "Shift+Mod+P");
|
|
assert.equal(settings.shortcuts.zoomInUi, "Alt+Mod+=");
|
|
assert.equal(settings.shortcuts.saveSql, "Mod+S");
|
|
});
|
|
|
|
test("defaults sidebar activation to single click", () => {
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.sidebarActivation, "single");
|
|
assert.equal(normalizeEditorSettings({}).sidebarActivation, "single");
|
|
});
|
|
|
|
test("defaults active tab sidebar selection to off", () => {
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode, false);
|
|
assert.equal(normalizeEditorSettings({}).autoSelectActiveSidebarNode, false);
|
|
});
|
|
|
|
test("defaults sidebar horizontal scroll to off", () => {
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll, false);
|
|
assert.equal(normalizeEditorSettings({}).sidebarAllowHorizontalScroll, false);
|
|
});
|
|
|
|
test("defaults data grid header display settings", () => {
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader, false);
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions, true);
|
|
assert.equal(normalizeEditorSettings({}).showColumnCommentsInHeader, false);
|
|
assert.equal(normalizeEditorSettings({}).compactColumnHeaderActions, true);
|
|
});
|
|
|
|
test("keeps saved data grid header display settings", () => {
|
|
const settings = normalizeEditorSettings({
|
|
showColumnCommentsInHeader: true,
|
|
compactColumnHeaderActions: false,
|
|
} as any);
|
|
|
|
assert.equal(settings.showColumnCommentsInHeader, true);
|
|
assert.equal(settings.compactColumnHeaderActions, false);
|
|
});
|
|
|
|
test("normalizes data grid render mode", () => {
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.dataGridRenderMode, "canvas");
|
|
assert.equal(normalizeEditorSettings({}).dataGridRenderMode, "canvas");
|
|
assert.equal(normalizeEditorSettings({ dataGridRenderMode: "canvas" as any }).dataGridRenderMode, "canvas");
|
|
assert.equal(normalizeEditorSettings({ dataGridRenderMode: "unknown" as any }).dataGridRenderMode, "canvas");
|
|
});
|
|
|
|
test("normalizes table structure editor density", () => {
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.structureEditorDensity, "compact");
|
|
assert.equal(normalizeEditorSettings({}).structureEditorDensity, "compact");
|
|
assert.equal(normalizeEditorSettings({ structureEditorDensity: "standard" }).structureEditorDensity, "standard");
|
|
assert.equal(normalizeEditorSettings({ structureEditorDensity: "comfortable" }).structureEditorDensity, "comfortable");
|
|
assert.equal(normalizeEditorSettings({ structureEditorDensity: "invalid" as any }).structureEditorDensity, "compact");
|
|
});
|
|
|
|
test("normalizes grid drawer widths", () => {
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.tableInfoDrawerWidth, 320);
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.cellDetailDrawerWidth, 320);
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.cellDetailPanelLayout, "bottom");
|
|
assert.equal(normalizeEditorSettings({}).tableInfoDrawerWidth, 320);
|
|
assert.equal(normalizeEditorSettings({}).cellDetailDrawerWidth, 320);
|
|
assert.equal(normalizeEditorSettings({}).cellDetailPanelLayout, "bottom");
|
|
assert.equal(normalizeEditorSettings({ tableInfoDrawerWidth: 200 } as any).tableInfoDrawerWidth, 240);
|
|
assert.equal(normalizeEditorSettings({ cellDetailDrawerWidth: 200 } as any).cellDetailDrawerWidth, 260);
|
|
assert.equal(normalizeEditorSettings({ tableInfoDrawerWidth: 1000 } as any).tableInfoDrawerWidth, 900);
|
|
assert.equal(normalizeEditorSettings({ cellDetailDrawerWidth: 456.7 } as any).cellDetailDrawerWidth, 457);
|
|
assert.equal(normalizeEditorSettings({ cellDetailPanelLayout: "right" } as any).cellDetailPanelLayout, "right");
|
|
assert.equal(normalizeEditorSettings({ cellDetailPanelLayout: "invalid" } as any).cellDetailPanelLayout, "bottom");
|
|
});
|
|
|
|
test("keeps saved active tab sidebar selection", () => {
|
|
assert.equal(normalizeEditorSettings({ autoSelectActiveSidebarNode: true } as any).autoSelectActiveSidebarNode, true);
|
|
});
|
|
|
|
test("keeps saved sidebar horizontal scroll preference", () => {
|
|
assert.equal(normalizeEditorSettings({ sidebarAllowHorizontalScroll: true } as any).sidebarAllowHorizontalScroll, true);
|
|
});
|
|
|
|
test("keeps saved sidebar activation", () => {
|
|
assert.equal(normalizeEditorSettings({ sidebarActivation: "double" } as any).sidebarActivation, "double");
|
|
assert.equal(normalizeEditorSettings({ sidebarActivation: "invalid" } as any).sidebarActivation, "single");
|
|
});
|
|
|
|
test("normalizes saved sidebar hidden table prefixes", () => {
|
|
assert.deepEqual(DEFAULT_EDITOR_SETTINGS.sidebarHiddenTablePrefixes, []);
|
|
assert.deepEqual(normalizeEditorSettings({ sidebarHiddenTablePrefixes: [" app_", "app_", "", "ods."] } as any).sidebarHiddenTablePrefixes, ["app_", "ods."]);
|
|
});
|
|
|
|
test("defaults column formatters to an empty record", () => {
|
|
assert.deepEqual(DEFAULT_EDITOR_SETTINGS.columnFormatters, {});
|
|
assert.deepEqual(normalizeEditorSettings({}).columnFormatters, {});
|
|
});
|
|
|
|
test("keeps only valid saved column formatter configs", () => {
|
|
const settings = normalizeEditorSettings({
|
|
columnFormatters: {
|
|
"conn::db::public::users::created_at": { kind: "datetime", unit: "auto" },
|
|
"conn::db::public::users::bad_date": { kind: "datetime", unit: "bogus" },
|
|
"conn::db::public::users::name": { kind: "mask", prefix: 2, suffix: 2 },
|
|
"conn::db::public::users::payload": { kind: "json-path", path: "$.user.name" },
|
|
"conn::db::public::users::invalid_json": { kind: "json-path", path: "user.name" },
|
|
"conn::db::public::users::status": { kind: "custom-ref", formatterId: "fmt_1" },
|
|
},
|
|
customColumnFormatters: {
|
|
fmt_1: { id: "fmt_1", name: "Status label", template: "status:${value}" },
|
|
fmt_empty_name: { id: "fmt_empty_name", name: "", template: "x:${value}" },
|
|
fmt_empty_template: { id: "fmt_empty_template", name: "Broken", template: "" },
|
|
},
|
|
} as any);
|
|
|
|
assert.deepEqual(settings.columnFormatters, {
|
|
"conn::db::public::users::created_at": { kind: "datetime", unit: "auto" },
|
|
"conn::db::public::users::name": { kind: "mask", prefix: 2, suffix: 2 },
|
|
"conn::db::public::users::payload": { kind: "json-path", path: "$.user.name" },
|
|
"conn::db::public::users::status": { kind: "custom-ref", formatterId: "fmt_1" },
|
|
});
|
|
assert.deepEqual(settings.customColumnFormatters, {
|
|
fmt_1: { id: "fmt_1", name: "Status label", template: "status:${value}" },
|
|
});
|
|
});
|
|
|
|
test("AI provider presets include common hosted and local providers", () => {
|
|
assert.equal(AI_PROVIDER_PRESETS.gemini.endpoint, "https://generativelanguage.googleapis.com");
|
|
assert.equal(AI_PROVIDER_PRESETS.gemini.model, "gemini-1.5-pro");
|
|
assert.equal(AI_PROVIDER_PRESETS.deepseek.endpoint, "https://api.deepseek.com/v1");
|
|
assert.equal(AI_PROVIDER_PRESETS.deepseek.model, "deepseek-v4-flash");
|
|
assert.equal(AI_PROVIDER_PRESETS.qwen.endpoint, "https://dashscope.aliyuncs.com/compatible-mode/v1");
|
|
assert.equal(AI_PROVIDER_PRESETS.ollama.endpoint, "http://localhost:11434/v1");
|
|
assert.equal(AI_PROVIDER_PRESETS.ollama.requiresApiKey, false);
|
|
assert.equal(AI_PROVIDER_PRESETS.claude.authMethod, "api-key");
|
|
assert.equal(AI_PROVIDER_PRESETS.openai.authMethod, "bearer");
|
|
assert.equal(AI_PROVIDER_PRESETS.openai.iconSlug, "openai");
|
|
assert.equal(AI_PROVIDER_PRESETS.deepseek.iconSlug, "deepseek");
|
|
});
|
|
|
|
test("normalizes legacy AI config and fills provider defaults", () => {
|
|
const legacy = normalizeAiConfig({
|
|
provider: "openai",
|
|
apiKey: "key",
|
|
endpoint: "https://api.openai.com/v1/chat/completions",
|
|
model: "gpt-4o",
|
|
} as any);
|
|
|
|
assert.equal(legacy.apiStyle, "completions");
|
|
assert.equal(legacy.provider, "openai");
|
|
assert.equal(legacy.apiKey, "key");
|
|
assert.equal(legacy.authMethod, "bearer");
|
|
|
|
const ollama = normalizeAiConfig({ provider: "ollama" } as any);
|
|
assert.equal(ollama.endpoint, "http://localhost:11434/v1");
|
|
assert.equal(ollama.model, "llama3.1");
|
|
assert.equal(ollama.apiKey, "");
|
|
assert.equal(ollama.authMethod, "bearer");
|
|
|
|
const claudeToken = normalizeAiConfig({ provider: "claude", apiKey: "token", authMethod: "bearer" } as any);
|
|
assert.equal(claudeToken.authMethod, "bearer");
|
|
});
|
|
|
|
test("infers legacy AI provider from saved endpoint and model", () => {
|
|
const deepseek = normalizeAiConfig({
|
|
apiKey: "key",
|
|
endpoint: "https://api.deepseek.com/anthropic/v1/messages",
|
|
model: "deepseek-v4-pro",
|
|
} as any);
|
|
|
|
assert.equal(deepseek.provider, "deepseek");
|
|
assert.equal(deepseek.endpoint, "https://api.deepseek.com/anthropic/v1/messages");
|
|
assert.equal(deepseek.model, "deepseek-v4-pro");
|
|
});
|
|
|
|
test("normalizeEditorSettings falls back to the default UI scale", () => {
|
|
const settings = normalizeEditorSettings({});
|
|
|
|
assert.equal(settings.uiScale, DEFAULT_EDITOR_SETTINGS.uiScale);
|
|
});
|
|
|
|
test("normalizeEditorSettings clamps UI scale into the supported range", () => {
|
|
assert.equal(normalizeEditorSettings({ uiScale: 0.2 }).uiScale, 0.75);
|
|
assert.equal(normalizeEditorSettings({ uiScale: 2.8 }).uiScale, 2);
|
|
});
|
|
|
|
test("normalizeEditorSettings keeps valid UI scales with two-decimal precision", () => {
|
|
assert.equal(normalizeEditorSettings({ uiScale: 1.125 }).uiScale, 1.13);
|
|
});
|
|
|
|
test("defaults SQL formatter settings", () => {
|
|
assert.deepEqual(DEFAULT_EDITOR_SETTINGS.sqlFormatter, DEFAULT_SQL_FORMATTER_SETTINGS);
|
|
assert.deepEqual(normalizeEditorSettings({}).sqlFormatter, DEFAULT_EDITOR_SETTINGS.sqlFormatter);
|
|
});
|
|
|
|
test("normalizes saved SQL formatter settings", () => {
|
|
assert.deepEqual(
|
|
normalizeEditorSettings({
|
|
sqlFormatter: {
|
|
keywordCase: "lower",
|
|
functionCase: "upper",
|
|
dataTypeCase: "upper",
|
|
useTabs: true,
|
|
tabWidth: 4,
|
|
logicalOperatorNewline: "after",
|
|
expressionWidth: 120,
|
|
linesBetweenQueries: 2,
|
|
denseOperators: true,
|
|
newlineBeforeSemicolon: true,
|
|
},
|
|
} as any).sqlFormatter,
|
|
{
|
|
...DEFAULT_SQL_FORMATTER_SETTINGS,
|
|
keywordCase: "lower",
|
|
functionCase: "upper",
|
|
dataTypeCase: "upper",
|
|
useTabs: true,
|
|
tabWidth: 4,
|
|
logicalOperatorNewline: "after",
|
|
expressionWidth: 120,
|
|
linesBetweenQueries: 2,
|
|
denseOperators: true,
|
|
newlineBeforeSemicolon: true,
|
|
},
|
|
);
|
|
});
|
|
|
|
test("keeps SQL formatter default objects distinct", () => {
|
|
const normalized = normalizeEditorSettings({});
|
|
|
|
assert.notEqual(DEFAULT_EDITOR_SETTINGS.sqlFormatter, DEFAULT_SQL_FORMATTER_SETTINGS);
|
|
assert.notEqual(normalized.sqlFormatter, DEFAULT_EDITOR_SETTINGS.sqlFormatter);
|
|
assert.notEqual(normalized.sqlFormatter, DEFAULT_SQL_FORMATTER_SETTINGS);
|
|
});
|
|
|
|
test("does not leak default-loaded SQL formatter mutations into defaults", () => {
|
|
withMockLocalStorage({}, () => {
|
|
setActivePinia(createPinia());
|
|
const store = useSettingsStore();
|
|
const editorDefaultKeywordCase = DEFAULT_EDITOR_SETTINGS.sqlFormatter.keywordCase;
|
|
const formatterDefaultKeywordCase = DEFAULT_SQL_FORMATTER_SETTINGS.keywordCase;
|
|
|
|
try {
|
|
store.editorSettings.sqlFormatter.keywordCase = "lower";
|
|
|
|
assert.notEqual(store.editorSettings.sqlFormatter, DEFAULT_EDITOR_SETTINGS.sqlFormatter);
|
|
assert.notEqual(store.editorSettings.sqlFormatter, DEFAULT_SQL_FORMATTER_SETTINGS);
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.sqlFormatter.keywordCase, editorDefaultKeywordCase);
|
|
assert.equal(DEFAULT_SQL_FORMATTER_SETTINGS.keywordCase, formatterDefaultKeywordCase);
|
|
} finally {
|
|
DEFAULT_EDITOR_SETTINGS.sqlFormatter.keywordCase = editorDefaultKeywordCase;
|
|
DEFAULT_SQL_FORMATTER_SETTINGS.keywordCase = formatterDefaultKeywordCase;
|
|
}
|
|
});
|
|
});
|
|
|
|
test("does not leak migrated SQL formatter mutations into defaults", () => {
|
|
withMockLocalStorage({ [OLD_FONT_SIZE_KEY]: "18" }, () => {
|
|
setActivePinia(createPinia());
|
|
const store = useSettingsStore();
|
|
const editorDefaultKeywordCase = DEFAULT_EDITOR_SETTINGS.sqlFormatter.keywordCase;
|
|
const formatterDefaultKeywordCase = DEFAULT_SQL_FORMATTER_SETTINGS.keywordCase;
|
|
|
|
try {
|
|
assert.equal(store.editorSettings.fontSize, 18);
|
|
store.editorSettings.sqlFormatter.keywordCase = "lower";
|
|
|
|
assert.notEqual(store.editorSettings.sqlFormatter, DEFAULT_EDITOR_SETTINGS.sqlFormatter);
|
|
assert.notEqual(store.editorSettings.sqlFormatter, DEFAULT_SQL_FORMATTER_SETTINGS);
|
|
assert.equal(DEFAULT_EDITOR_SETTINGS.sqlFormatter.keywordCase, editorDefaultKeywordCase);
|
|
assert.equal(DEFAULT_SQL_FORMATTER_SETTINGS.keywordCase, formatterDefaultKeywordCase);
|
|
} finally {
|
|
DEFAULT_EDITOR_SETTINGS.sqlFormatter.keywordCase = editorDefaultKeywordCase;
|
|
DEFAULT_SQL_FORMATTER_SETTINGS.keywordCase = formatterDefaultKeywordCase;
|
|
}
|
|
});
|
|
});
|