1 parent
6b5b33540f
commit
492c9727ae
10 files changed
+189
-1
No files matched your search
@@ -77,6 +77,15 @@ function updateKind(name: string, kind: SqlParameterValueKind) {
|
||||
values.value[name] = { ...current, kind, value };
|
||||
}
|
||||
|
||||
function setAllParametersToRaw() {
|
||||
const next = { ...values.value };
|
||||
for (const parameter of props.parameters) {
|
||||
const current = next[parameter.key] ?? { kind: "string", value: "" };
|
||||
next[parameter.key] = { ...current, kind: "raw" };
|
||||
}
|
||||
values.value = next;
|
||||
}
|
||||
|
||||
function updateValue(name: string, value: string) {
|
||||
const matchedHistory = histories.value[name]?.find((entry) => entry.value === value);
|
||||
values.value[name] = { ...(values.value[name] ?? { kind: "string" }), ...(matchedHistory ? { kind: matchedHistory.kind } : {}), value };
|
||||
@@ -135,7 +144,12 @@ async function copyResolvedSql() {
|
||||
</DialogHeader>
|
||||
|
||||
<div class="grid max-h-[calc(86vh-8rem)] gap-4 overflow-y-auto pr-1">
|
||||
<p class="text-sm text-muted-foreground">{{ t("sqlParameters.description") }}</p>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<p class="min-w-0 flex-1 text-sm text-muted-foreground">{{ t("sqlParameters.description") }}</p>
|
||||
<Button type="button" size="sm" variant="outline" class="ml-auto shrink-0" data-testid="sql-parameters-use-raw-all" @click="setAllParametersToRaw">
|
||||
{{ t("sqlParameters.useRawForAll") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="relative z-20 max-h-[302px] overflow-auto rounded-md border bg-background">
|
||||
<div class="min-w-[680px]">
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, reactive, type App } from "vue";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "@/i18n";
|
||||
import SqlParameterDialog from "@/components/editor/SqlParameterDialog.vue";
|
||||
import type { SqlParameterDescriptor, SqlParameterInput } from "@/lib/sql/sqlParameters";
|
||||
|
||||
const historyMocks = vi.hoisted(() => ({
|
||||
load: vi.fn<(key: string) => SqlParameterInput[]>(),
|
||||
remember: vi.fn<(values: Record<string, SqlParameterInput>) => Record<string, SqlParameterInput[]>>(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/sql/sqlParameterHistory", () => ({
|
||||
loadSqlParameterHistory: historyMocks.load,
|
||||
rememberSqlParameterValues: historyMocks.remember,
|
||||
}));
|
||||
|
||||
vi.mock("@/composables/useSqlHighlighter", () => ({
|
||||
useSqlHighlighter: () => ({ highlight: (sql: string) => `<span>${sql}</span>` }),
|
||||
}));
|
||||
|
||||
const SQL = "SELECT ${text_value}, ${numeric_value}, ${null_value}, ${raw_value}, ${empty_value}, ${text_value}";
|
||||
const PARAMETERS: SqlParameterDescriptor[] = [
|
||||
{ key: "text_value", name: "text_value", syntax: "shell", token: "${text_value}" },
|
||||
{ key: "numeric_value", name: "numeric_value", syntax: "shell", token: "${numeric_value}" },
|
||||
{ key: "null_value", name: "null_value", syntax: "shell", token: "${null_value}" },
|
||||
{ key: "raw_value", name: "raw_value", syntax: "shell", token: "${raw_value}" },
|
||||
{ key: "empty_value", name: "empty_value", syntax: "shell", token: "${empty_value}" },
|
||||
];
|
||||
const MIXED_VALUES: Record<string, SqlParameterInput> = {
|
||||
text_value: { kind: "string", value: "alpha beta" },
|
||||
numeric_value: { kind: "number", value: " 42 " },
|
||||
null_value: { kind: "null", value: "NULL" },
|
||||
raw_value: { kind: "raw", value: "current_date" },
|
||||
empty_value: { kind: "boolean", value: "" },
|
||||
};
|
||||
const RAW_VALUES = Object.fromEntries(Object.entries(MIXED_VALUES).map(([key, input]) => [key, { ...input, kind: "raw" as const }]));
|
||||
const RAW_SQL = "SELECT alpha beta, 42, NULL, current_date, NULL, alpha beta";
|
||||
|
||||
const mountedApps: App[] = [];
|
||||
|
||||
async function mountDialog(values: Record<string, SqlParameterInput> = MIXED_VALUES) {
|
||||
historyMocks.load.mockImplementation((key) => (values[key] ? [{ ...values[key] }] : []));
|
||||
const state = reactive({ open: true });
|
||||
const onExecute = vi.fn();
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup: () => () =>
|
||||
h(SqlParameterDialog, {
|
||||
open: state.open,
|
||||
sql: SQL,
|
||||
parameters: PARAMETERS,
|
||||
databaseType: "mysql",
|
||||
enabledSyntaxes: ["shell"],
|
||||
onExecute,
|
||||
"onUpdate:open": (value: boolean) => {
|
||||
state.open = value;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
mountedApps.push(app);
|
||||
app.use(i18n);
|
||||
app.mount(container);
|
||||
await nextTick();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
return { state, onExecute };
|
||||
}
|
||||
|
||||
function actionButton(): HTMLButtonElement | null {
|
||||
return document.body.querySelector('[data-testid="sql-parameters-use-raw-all"]');
|
||||
}
|
||||
|
||||
function clickRawAction() {
|
||||
const button = actionButton();
|
||||
expect(button).not.toBeNull();
|
||||
button!.click();
|
||||
}
|
||||
|
||||
function buttonWithText(text: string): HTMLButtonElement | undefined {
|
||||
return Array.from(document.body.querySelectorAll("button")).find((button) => button.textContent?.trim() === text);
|
||||
}
|
||||
|
||||
function parameterInputs(): HTMLInputElement[] {
|
||||
return Array.from(document.body.querySelectorAll('input[data-form-type="other"]'));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
historyMocks.load.mockReset();
|
||||
historyMocks.remember.mockReset();
|
||||
historyMocks.remember.mockImplementation((values) => Object.fromEntries(Object.entries(values).map(([key, input]) => [key, [{ ...input }]])));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const app of mountedApps.splice(0)) app.unmount();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
describe("SqlParameterDialog raw parameter action", () => {
|
||||
it("sets every displayed parameter to raw while preserving each current value", async () => {
|
||||
const { state, onExecute } = await mountDialog();
|
||||
const beforeValues = parameterInputs().map((input) => input.value);
|
||||
expect(parameterInputs()[2]?.disabled).toBe(true);
|
||||
|
||||
clickRawAction();
|
||||
await nextTick();
|
||||
|
||||
const inputs = parameterInputs();
|
||||
expect(inputs.map((input) => input.value)).toEqual(beforeValues);
|
||||
expect(inputs[2]?.disabled).toBe(false);
|
||||
expect(Array.from(document.body.querySelectorAll('[role="combobox"]')).map((trigger) => trigger.textContent?.trim())).toEqual(Array(PARAMETERS.length).fill("Raw SQL"));
|
||||
expect(document.body.querySelector("pre")?.textContent).toBe(RAW_SQL);
|
||||
expect(state.open).toBe(true);
|
||||
expect(onExecute).not.toHaveBeenCalled();
|
||||
expect(historyMocks.remember).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the action idempotent when every displayed parameter is already raw", async () => {
|
||||
const { state, onExecute } = await mountDialog(RAW_VALUES);
|
||||
const beforeValues = parameterInputs().map((input) => input.value);
|
||||
const beforePreview = document.body.querySelector("pre")?.textContent;
|
||||
|
||||
clickRawAction();
|
||||
await nextTick();
|
||||
|
||||
expect(parameterInputs().map((input) => input.value)).toEqual(beforeValues);
|
||||
expect(document.body.querySelector("pre")?.textContent).toBe(beforePreview);
|
||||
expect(state.open).toBe(true);
|
||||
expect(onExecute).not.toHaveBeenCalled();
|
||||
expect(historyMocks.remember).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps cancel side-effect free after applying raw to all parameters", async () => {
|
||||
const { state, onExecute } = await mountDialog();
|
||||
|
||||
clickRawAction();
|
||||
await nextTick();
|
||||
buttonWithText("Cancel")?.click();
|
||||
await nextTick();
|
||||
|
||||
expect(state.open).toBe(false);
|
||||
expect(onExecute).not.toHaveBeenCalled();
|
||||
expect(historyMocks.remember).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("remembers and emits the resolved SQL only when Execute is clicked", async () => {
|
||||
const { state, onExecute } = await mountDialog();
|
||||
|
||||
clickRawAction();
|
||||
await nextTick();
|
||||
expect(onExecute).not.toHaveBeenCalled();
|
||||
expect(historyMocks.remember).not.toHaveBeenCalled();
|
||||
|
||||
buttonWithText("Execute")?.click();
|
||||
await nextTick();
|
||||
|
||||
expect(historyMocks.remember).toHaveBeenCalledOnce();
|
||||
expect(historyMocks.remember).toHaveBeenCalledWith(RAW_VALUES);
|
||||
expect(onExecute).toHaveBeenCalledOnce();
|
||||
expect(onExecute).toHaveBeenCalledWith(RAW_SQL);
|
||||
expect(state.open).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -5096,6 +5096,7 @@ export default {
|
||||
collectionValuePlaceholder: "Enter a JSON array or comma-separated values",
|
||||
preview: "SQL Preview",
|
||||
execute: "Execute",
|
||||
useRawForAll: "Use Raw SQL for All",
|
||||
kind: {
|
||||
string: "String",
|
||||
number: "Number",
|
||||
|
||||
@@ -4860,6 +4860,7 @@ export default withEnglishFallback({
|
||||
collectionValuePlaceholder: "Introduce un array JSON o valores separados por comas",
|
||||
preview: "Vista previa SQL",
|
||||
execute: "Ejecutar",
|
||||
useRawForAll: "Usar SQL sin procesar para todos",
|
||||
kind: {
|
||||
string: "Cadena",
|
||||
number: "Número",
|
||||
|
||||
@@ -4858,6 +4858,7 @@ export default withEnglishFallback({
|
||||
collectionValuePlaceholder: "Inserisci un array JSON o valori separati da virgole",
|
||||
preview: "Anteprima SQL",
|
||||
execute: "Esegui",
|
||||
useRawForAll: "Usa SQL non elaborato per tutti",
|
||||
kind: {
|
||||
string: "Stringa",
|
||||
number: "Numero",
|
||||
|
||||
@@ -4887,6 +4887,7 @@ export default withEnglishFallback({
|
||||
collectionValuePlaceholder: "JSON 配列またはカンマ区切りの値を入力",
|
||||
preview: "SQLプレビュー",
|
||||
execute: "実行",
|
||||
useRawForAll: "すべてを生の SQL に設定",
|
||||
kind: {
|
||||
string: "文字列",
|
||||
number: "数値",
|
||||
|
||||
@@ -4531,6 +4531,7 @@ export default withEnglishFallback({
|
||||
collectionValuePlaceholder: "JSON 배열 또는 쉼표로 구분된 값 입력",
|
||||
preview: "SQL 미리보기",
|
||||
execute: "실행",
|
||||
useRawForAll: "모두 원시 SQL로 설정",
|
||||
kind: {
|
||||
string: "문자열",
|
||||
number: "숫자",
|
||||
|
||||
@@ -4860,6 +4860,7 @@ export default withEnglishFallback({
|
||||
collectionValuePlaceholder: "Insira um array JSON ou valores separados por vírgula",
|
||||
preview: "Prévia SQL",
|
||||
execute: "Executar",
|
||||
useRawForAll: "Usar SQL bruto para todos",
|
||||
kind: {
|
||||
string: "Texto",
|
||||
number: "Número",
|
||||
|
||||
@@ -5081,6 +5081,7 @@ export default withEnglishFallback({
|
||||
collectionValuePlaceholder: "输入 JSON 数组或逗号分隔值",
|
||||
preview: "SQL 预览",
|
||||
execute: "执行",
|
||||
useRawForAll: "全部使用原始 SQL",
|
||||
kind: {
|
||||
string: "字符串",
|
||||
number: "数字",
|
||||
|
||||
@@ -4184,6 +4184,7 @@ export default withEnglishFallback({
|
||||
collectionValuePlaceholder: "輸入 JSON 陣列或逗號分隔值",
|
||||
preview: "SQL 預覽",
|
||||
execute: "執行",
|
||||
useRawForAll: "全部使用原始 SQL",
|
||||
kind: {
|
||||
string: "字串",
|
||||
number: "數字",
|
||||
|
||||
Reference in new issue
Block a user