662 lines
24 KiB
TypeScript
662 lines
24 KiB
TypeScript
// Copyright 2026 OpenObserve Inc.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import { describe, expect, it, beforeEach, vi } from "vitest";
|
|
import { mount, flushPromises } from "@vue/test-utils";
|
|
import i18n from "@/locales";
|
|
import store from "@/test/unit/helpers/store";
|
|
|
|
vi.mock("@/services/search", () => ({
|
|
default: {
|
|
search: vi.fn().mockResolvedValue({ data: { hits: [] } }),
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/composables/useQuery", () => ({
|
|
default: () => ({
|
|
buildQueryPayload: vi.fn().mockReturnValue({
|
|
query: { sql: "", size: 10, sql_mode: true, query_fn: null, per_query_response: true },
|
|
}),
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/composables/useParser", () => ({
|
|
default: () => ({
|
|
sqlParser: vi.fn().mockResolvedValue({}),
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/utils/alerts/alertSqlUtils", () => ({
|
|
getParser: vi.fn().mockReturnValue(null),
|
|
}));
|
|
|
|
vi.mock("@/aws-exports", () => ({
|
|
default: { isEnterprise: "false", isCloud: "false", isAiEnabled: "false" },
|
|
}));
|
|
|
|
import QueryEditorDialog from "@/components/alerts/QueryEditorDialog.vue";
|
|
|
|
const childEditorStub = {
|
|
template: '<div class="stub-editor"></div>',
|
|
props: ["query", "editorId", "language", "readOnly"],
|
|
emits: ["update:query", "blur"],
|
|
};
|
|
|
|
const unifiedEditorStub = {
|
|
template: '<div class="stub-unified-editor"></div>',
|
|
props: ["query", "editorId", "streamName", "streamType"],
|
|
emits: ["update:query", "blur"],
|
|
};
|
|
|
|
async function mountComp(props: Record<string, any> = {}) {
|
|
return mount(QueryEditorDialog, {
|
|
props: {
|
|
modelValue: true,
|
|
tab: "sql",
|
|
sqlQuery: "",
|
|
promqlQuery: "",
|
|
vrlFunction: "",
|
|
streamName: "my-stream",
|
|
streamType: "logs",
|
|
columns: [
|
|
{ label: "host", value: "host" },
|
|
{ label: "level", value: "level" },
|
|
],
|
|
period: 10,
|
|
multiTimeRange: [],
|
|
savedFunctions: [],
|
|
sqlQueryErrorMsg: "",
|
|
...props,
|
|
},
|
|
global: {
|
|
plugins: [i18n, store],
|
|
stubs: {
|
|
CodeQueryEditor: childEditorStub,
|
|
QueryEditor: childEditorStub,
|
|
UnifiedQueryEditor: unifiedEditorStub,
|
|
FullViewContainer: {
|
|
template: '<div><slot /><slot name="right" /></div>',
|
|
props: ["name", "label", "isExpanded"],
|
|
emits: ["update:isExpanded"],
|
|
},
|
|
O2AIChat: {
|
|
template: '<div class="stub-ai-chat"></div>',
|
|
props: ["headerHeight", "isOpen"],
|
|
emits: ["close"],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
describe("QueryEditorDialog - rendering", () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it("renders without errors", async () => {
|
|
const w = await mountComp();
|
|
await flushPromises();
|
|
expect(w.exists()).toBe(true);
|
|
});
|
|
|
|
it("renders when modelValue is true", async () => {
|
|
const w = await mountComp({ modelValue: true });
|
|
await flushPromises();
|
|
expect(w.html()).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("QueryEditorDialog - initial state", () => {
|
|
it("localTab defaults to sql", async () => {
|
|
const w = await mountComp({ tab: "sql" });
|
|
await flushPromises();
|
|
expect((w.vm as any).localTab).toBe("sql");
|
|
});
|
|
|
|
it("localTab uses provided tab prop", async () => {
|
|
const w = await mountComp({ tab: "promql" });
|
|
await flushPromises();
|
|
expect((w.vm as any).localTab).toBe("promql");
|
|
});
|
|
|
|
it("localSqlQuery initialized from props.sqlQuery", async () => {
|
|
const w = await mountComp({ sqlQuery: "SELECT * FROM stream" });
|
|
await flushPromises();
|
|
expect((w.vm as any).localSqlQuery).toBe("SELECT * FROM stream");
|
|
});
|
|
|
|
it("localPromqlQuery initialized from props.promqlQuery", async () => {
|
|
const w = await mountComp({ promqlQuery: "rate(metric[5m])" });
|
|
await flushPromises();
|
|
expect((w.vm as any).localPromqlQuery).toBe("rate(metric[5m])");
|
|
});
|
|
|
|
it("vrlFunctionContent initialized from props.vrlFunction", async () => {
|
|
const w = await mountComp({ vrlFunction: ".level = upcase(.level)" });
|
|
await flushPromises();
|
|
expect((w.vm as any).vrlFunctionContent).toBe(".level = upcase(.level)");
|
|
});
|
|
|
|
it("functionOptions initialized from props.savedFunctions", async () => {
|
|
const w = await mountComp({ savedFunctions: [{ name: "fn1" }, { name: "fn2" }] });
|
|
await flushPromises();
|
|
expect((w.vm as any).functionOptions).toHaveLength(2);
|
|
});
|
|
|
|
it("expandSqlOutput defaults to true", async () => {
|
|
const w = await mountComp();
|
|
expect((w.vm as any).expandSqlOutput).toBe(true);
|
|
});
|
|
|
|
it("expandCombinedOutput defaults to true", async () => {
|
|
const w = await mountComp();
|
|
expect((w.vm as any).expandCombinedOutput).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("QueryEditorDialog - closeDialog", () => {
|
|
it("emits update:modelValue with false", async () => {
|
|
const w = await mountComp();
|
|
await flushPromises();
|
|
(w.vm as any).closeDialog();
|
|
expect(w.emitted("update:modelValue")).toBeTruthy();
|
|
expect((w.emitted("update:modelValue") as any[][])[0][0]).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("QueryEditorDialog - updateSqlQuery", () => {
|
|
it("updates localSqlQuery", async () => {
|
|
const w = await mountComp();
|
|
(w.vm as any).updateSqlQuery("SELECT count(*) FROM logs");
|
|
expect((w.vm as any).localSqlQuery).toBe("SELECT count(*) FROM logs");
|
|
});
|
|
|
|
it("emits update:sqlQuery with new value", async () => {
|
|
const w = await mountComp();
|
|
(w.vm as any).updateSqlQuery("SELECT count(*) FROM logs");
|
|
expect(w.emitted("update:sqlQuery")).toBeTruthy();
|
|
expect((w.emitted("update:sqlQuery") as any[][])[0][0]).toBe("SELECT count(*) FROM logs");
|
|
});
|
|
});
|
|
|
|
describe("QueryEditorDialog - updatePromqlQuery", () => {
|
|
it("updates localPromqlQuery", async () => {
|
|
const w = await mountComp();
|
|
(w.vm as any).updatePromqlQuery("rate(http_requests[5m])");
|
|
expect((w.vm as any).localPromqlQuery).toBe("rate(http_requests[5m])");
|
|
});
|
|
|
|
it("emits update:promqlQuery", async () => {
|
|
const w = await mountComp();
|
|
(w.vm as any).updatePromqlQuery("rate(http_requests[5m])");
|
|
expect(w.emitted("update:promqlQuery")).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("QueryEditorDialog - updateVrlFunction", () => {
|
|
it("updates vrlFunctionContent", async () => {
|
|
const w = await mountComp();
|
|
(w.vm as any).updateVrlFunction(".level = upcase(.level)");
|
|
expect((w.vm as any).vrlFunctionContent).toBe(".level = upcase(.level)");
|
|
});
|
|
|
|
it("emits update:vrlFunction", async () => {
|
|
const w = await mountComp();
|
|
(w.vm as any).updateVrlFunction(".level = upcase(.level)");
|
|
expect(w.emitted("update:vrlFunction")).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
// The saved-functions OSelect is configured with valueKey="name", so it emits
|
|
// the function NAME, never the option object. These tests must pass a name —
|
|
// passing an object here is what let the "select does nothing" bug through.
|
|
describe("QueryEditorDialog - onFunctionSelect", () => {
|
|
const FUNCS = [
|
|
{ name: "myFn", function: ".level = upcase(.level)" },
|
|
{ name: "otherFn", function: ".a = 1" },
|
|
];
|
|
|
|
it("sets vrlFunctionContent from the selected function's body", async () => {
|
|
const w = await mountComp({ savedFunctions: FUNCS });
|
|
(w.vm as any).onFunctionSelect("myFn");
|
|
expect((w.vm as any).vrlFunctionContent).toBe(".level = upcase(.level)");
|
|
});
|
|
|
|
it("emits update:vrlFunction so the parent form keeps the selection", async () => {
|
|
const w = await mountComp({ savedFunctions: FUNCS });
|
|
(w.vm as any).onFunctionSelect("myFn");
|
|
expect(w.emitted("update:vrlFunction")?.at(-1)).toEqual([".level = upcase(.level)"]);
|
|
});
|
|
|
|
it("replaces the body when switching to a different function", async () => {
|
|
const w = await mountComp({ savedFunctions: FUNCS });
|
|
(w.vm as any).onFunctionSelect("myFn");
|
|
(w.vm as any).onFunctionSelect("otherFn");
|
|
expect((w.vm as any).vrlFunctionContent).toBe(".a = 1");
|
|
});
|
|
|
|
it("falls back to `body` when the function has no `function` field", async () => {
|
|
const w = await mountComp({ savedFunctions: [{ name: "bodyFn", body: ".b = 2" }] });
|
|
(w.vm as any).onFunctionSelect("bodyFn");
|
|
expect((w.vm as any).vrlFunctionContent).toBe(".b = 2");
|
|
});
|
|
|
|
it("does nothing when the name matches no saved function", async () => {
|
|
const w = await mountComp({ vrlFunction: "existing", savedFunctions: FUNCS });
|
|
(w.vm as any).onFunctionSelect("nope");
|
|
expect((w.vm as any).vrlFunctionContent).toBe("existing");
|
|
});
|
|
});
|
|
|
|
describe("QueryEditorDialog - onFunctionClear", () => {
|
|
it("clears vrlFunctionContent", async () => {
|
|
const w = await mountComp({ vrlFunction: ".level = upcase(.level)" });
|
|
(w.vm as any).onFunctionClear();
|
|
expect((w.vm as any).vrlFunctionContent).toBe("");
|
|
});
|
|
|
|
it("clears selectedFunction", async () => {
|
|
const w = await mountComp();
|
|
(w.vm as any).selectedFunction = { name: "myFn" };
|
|
(w.vm as any).onFunctionClear();
|
|
expect((w.vm as any).selectedFunction).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("QueryEditorDialog - buildMultiWindowQuery", () => {
|
|
it("returns empty array when multiTimeRange is empty", async () => {
|
|
const w = await mountComp({ multiTimeRange: [] });
|
|
const result = (w.vm as any).buildMultiWindowQuery("SELECT * FROM logs", false, 600000000);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("builds queries for each multiTimeRange entry", async () => {
|
|
const w = await mountComp({
|
|
multiTimeRange: [{ offSet: "1h" }, { offSet: "24h" }],
|
|
});
|
|
const result = (w.vm as any).buildMultiWindowQuery("SELECT * FROM logs", false, 600000000);
|
|
expect(result).toHaveLength(2);
|
|
});
|
|
|
|
it("includes sql in each query", async () => {
|
|
const w = await mountComp({
|
|
multiTimeRange: [{ offSet: "1h" }],
|
|
});
|
|
const result = (w.vm as any).buildMultiWindowQuery(
|
|
"SELECT count(*) FROM logs",
|
|
false,
|
|
600000000,
|
|
);
|
|
expect(result[0].sql).toBe("SELECT count(*) FROM logs");
|
|
});
|
|
|
|
it("ignores entries with invalid offset format", async () => {
|
|
const w = await mountComp({
|
|
multiTimeRange: [{ offSet: "invalid-format" }],
|
|
});
|
|
const result = (w.vm as any).buildMultiWindowQuery("SELECT * FROM logs", false, 600000000);
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("QueryEditorDialog - prop watchers", () => {
|
|
it("updates localTab when tab prop changes", async () => {
|
|
const w = await mountComp({ tab: "sql" });
|
|
await flushPromises();
|
|
await w.setProps({ tab: "promql" });
|
|
await flushPromises();
|
|
expect((w.vm as any).localTab).toBe("promql");
|
|
});
|
|
|
|
it("updates localSqlQuery when sqlQuery prop changes", async () => {
|
|
const w = await mountComp({ sqlQuery: "SELECT 1" });
|
|
await flushPromises();
|
|
await w.setProps({ sqlQuery: "SELECT count(*) FROM logs" });
|
|
await flushPromises();
|
|
expect((w.vm as any).localSqlQuery).toBe("SELECT count(*) FROM logs");
|
|
});
|
|
|
|
it("updates localPromqlQuery when promqlQuery prop changes", async () => {
|
|
const w = await mountComp({ promqlQuery: "" });
|
|
await flushPromises();
|
|
await w.setProps({ promqlQuery: "rate(http[5m])" });
|
|
await flushPromises();
|
|
expect((w.vm as any).localPromqlQuery).toBe("rate(http[5m])");
|
|
});
|
|
|
|
it("updates vrlFunctionContent when vrlFunction prop changes", async () => {
|
|
const w = await mountComp({ vrlFunction: "" });
|
|
await flushPromises();
|
|
await w.setProps({ vrlFunction: ".level = downcase(.level)" });
|
|
await flushPromises();
|
|
expect((w.vm as any).vrlFunctionContent).toBe(".level = downcase(.level)");
|
|
});
|
|
|
|
it("updates functionOptions when savedFunctions prop changes", async () => {
|
|
const w = await mountComp({ savedFunctions: [] });
|
|
await flushPromises();
|
|
await w.setProps({ savedFunctions: [{ name: "newFn" }] });
|
|
await flushPromises();
|
|
expect((w.vm as any).functionOptions).toHaveLength(1);
|
|
expect((w.vm as any).functionOptions[0].name).toBe("newFn");
|
|
});
|
|
});
|
|
|
|
describe("QueryEditorDialog - onBlurFunctionEditor", () => {
|
|
it("sets functionEditorPlaceholderFlag to true when vrl is empty", async () => {
|
|
const w = await mountComp({ vrlFunction: "" });
|
|
(w.vm as any).onBlurFunctionEditor();
|
|
expect((w.vm as any).functionEditorPlaceholderFlag).toBe(true);
|
|
});
|
|
|
|
it("sets functionEditorPlaceholderFlag to false when vrl is not empty", async () => {
|
|
const w = await mountComp({ vrlFunction: ".level = upcase(.level)" });
|
|
(w.vm as any).onBlurFunctionEditor();
|
|
expect((w.vm as any).functionEditorPlaceholderFlag).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Coverage for the dialog → ODrawer migration. The template now renders
|
|
// <ODrawer v-model:open="isOpen" size="full" :show-close="false"> where
|
|
// isOpen is a computed get/set bound to the modelValue prop.
|
|
describe("QueryEditorDialog - ODrawer Migration", () => {
|
|
// Lightweight stub mirroring ODrawer's migrated prop surface so we can
|
|
// assert the open/size/showClose contract without pulling in reka-ui.
|
|
const ODrawerStub = {
|
|
name: "ODrawer",
|
|
props: {
|
|
open: { type: Boolean, default: false },
|
|
size: { type: String, default: undefined },
|
|
showClose: { type: Boolean, default: true },
|
|
title: { type: String, default: undefined },
|
|
persistent: { type: Boolean, default: false },
|
|
width: { type: [String, Number], default: undefined },
|
|
},
|
|
emits: ["update:open"],
|
|
template: `
|
|
<div
|
|
data-test="o-drawer-stub"
|
|
:data-open="String(open)"
|
|
:data-size="size"
|
|
:data-show-close="String(!!showClose)"
|
|
>
|
|
<slot name="header" />
|
|
<slot name="header-left" />
|
|
<slot name="header-right" />
|
|
<slot />
|
|
<slot name="footer" />
|
|
<button
|
|
data-test="o-drawer-stub-close"
|
|
@click="$emit('update:open', false)"
|
|
>close</button>
|
|
</div>
|
|
`,
|
|
};
|
|
|
|
async function mountWithDrawerStub(props: Record<string, any> = {}) {
|
|
return mount(QueryEditorDialog, {
|
|
props: {
|
|
modelValue: true,
|
|
tab: "sql",
|
|
sqlQuery: "",
|
|
promqlQuery: "",
|
|
vrlFunction: "",
|
|
streamName: "my-stream",
|
|
streamType: "logs",
|
|
columns: [],
|
|
period: 10,
|
|
multiTimeRange: [],
|
|
savedFunctions: [],
|
|
sqlQueryErrorMsg: "",
|
|
...props,
|
|
},
|
|
global: {
|
|
plugins: [i18n, store],
|
|
stubs: {
|
|
ODrawer: ODrawerStub,
|
|
CodeQueryEditor: childEditorStub,
|
|
QueryEditor: childEditorStub,
|
|
UnifiedQueryEditor: unifiedEditorStub,
|
|
FullViewContainer: {
|
|
template: '<div><slot /><slot name="right" /></div>',
|
|
props: ["name", "label", "isExpanded"],
|
|
emits: ["update:isExpanded"],
|
|
},
|
|
O2AIChat: {
|
|
template: '<div class="stub-ai-chat"></div>',
|
|
props: ["headerHeight", "isOpen"],
|
|
emits: ["close"],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it("renders exactly one ODrawer at the root of the template", async () => {
|
|
const w = await mountWithDrawerStub();
|
|
await flushPromises();
|
|
const drawers = w.findAllComponents(ODrawerStub);
|
|
expect(drawers.length).toBe(1);
|
|
});
|
|
|
|
it("forwards modelValue=true to ODrawer's open prop", async () => {
|
|
const w = await mountWithDrawerStub({ modelValue: true });
|
|
await flushPromises();
|
|
const drawer = w.findComponent(ODrawerStub);
|
|
expect(drawer.props("open")).toBe(true);
|
|
});
|
|
|
|
it("forwards modelValue=false to ODrawer's open prop", async () => {
|
|
const w = await mountWithDrawerStub({ modelValue: false });
|
|
await flushPromises();
|
|
const drawer = w.findComponent(ODrawerStub);
|
|
expect(drawer.props("open")).toBe(false);
|
|
});
|
|
|
|
it("reflects modelValue prop changes onto ODrawer's open prop", async () => {
|
|
const w = await mountWithDrawerStub({ modelValue: true });
|
|
await flushPromises();
|
|
let drawer = w.findComponent(ODrawerStub);
|
|
expect(drawer.props("open")).toBe(true);
|
|
|
|
await w.setProps({ modelValue: false });
|
|
await flushPromises();
|
|
drawer = w.findComponent(ODrawerStub);
|
|
expect(drawer.props("open")).toBe(false);
|
|
});
|
|
|
|
it('renders ODrawer with size="full"', async () => {
|
|
const w = await mountWithDrawerStub();
|
|
await flushPromises();
|
|
const drawer = w.findComponent(ODrawerStub);
|
|
expect(drawer.props("size")).toBe("full");
|
|
});
|
|
|
|
it('renders ODrawer with :show-close="false"', async () => {
|
|
const w = await mountWithDrawerStub();
|
|
await flushPromises();
|
|
const drawer = w.findComponent(ODrawerStub);
|
|
expect(drawer.props("showClose")).toBe(false);
|
|
});
|
|
|
|
it("emits update:modelValue=false when ODrawer emits update:open=false", async () => {
|
|
const w = await mountWithDrawerStub({ modelValue: true });
|
|
await flushPromises();
|
|
const drawer = w.findComponent(ODrawerStub);
|
|
await drawer.vm.$emit("update:open", false);
|
|
await flushPromises();
|
|
const emitted = w.emitted("update:modelValue");
|
|
expect(emitted).toBeTruthy();
|
|
expect(emitted![emitted!.length - 1]).toEqual([false]);
|
|
});
|
|
|
|
it("emits update:modelValue=true when ODrawer emits update:open=true", async () => {
|
|
const w = await mountWithDrawerStub({ modelValue: false });
|
|
await flushPromises();
|
|
const drawer = w.findComponent(ODrawerStub);
|
|
await drawer.vm.$emit("update:open", true);
|
|
await flushPromises();
|
|
const emitted = w.emitted("update:modelValue");
|
|
expect(emitted).toBeTruthy();
|
|
expect(emitted![emitted!.length - 1]).toEqual([true]);
|
|
});
|
|
|
|
it("emits update:modelValue=false when the back button inside the drawer is clicked (closeDialog path)", async () => {
|
|
const w = await mountWithDrawerStub({ modelValue: true });
|
|
await flushPromises();
|
|
const backBtn = w.find('[data-test="add-alert-back-btn"]');
|
|
expect(backBtn.exists()).toBe(true);
|
|
await backBtn.trigger("click");
|
|
await flushPromises();
|
|
const emitted = w.emitted("update:modelValue");
|
|
expect(emitted).toBeTruthy();
|
|
expect(emitted![emitted!.length - 1]).toEqual([false]);
|
|
});
|
|
|
|
it("renders the dialog body content (drawer default slot) inside the ODrawer", async () => {
|
|
const w = await mountWithDrawerStub();
|
|
await flushPromises();
|
|
const drawer = w.findComponent(ODrawerStub);
|
|
// the dialog card lives inside the drawer's default slot
|
|
expect(drawer.find('[data-test="query-editor-dialog-card"]').exists()).toBe(true);
|
|
});
|
|
|
|
it("renders ODrawer as the migration wrapper (migration completed)", async () => {
|
|
const w = await mountWithDrawerStub();
|
|
await flushPromises();
|
|
expect(w.findComponent(ODrawerStub).exists()).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Phase 1 (N1) / Phase 3 (C4) — autocomplete reaches the editor ───────────
|
|
// These two tests used to drive a VALUE context and assert that values arrived
|
|
// through `keywords` while `suggestions` went blank. That was the only way to
|
|
// tell effectiveKeywords from autoCompleteKeywords, which are identical unless
|
|
// a context is active — and it was a fair test until the value round trip was
|
|
// removed: the completion PROVIDER now resolves values itself, so nothing
|
|
// pushes them through the props any more.
|
|
//
|
|
// What replaces each half:
|
|
// - the N1 invariant (bind the context-aware list, never the base one) is now
|
|
// enforced for EVERY editor host in utils/query/editorWiring.spec.ts, which
|
|
// needs no context to make the distinction visible;
|
|
// - what this file can still prove is its own wiring — that the resolver the
|
|
// provider awaits arrives, and resolves against the stream context the
|
|
// dialog sets.
|
|
|
|
describe("QueryEditorDialog - autocomplete wiring reaches the editor", () => {
|
|
const editorStubDef = {
|
|
name: "UnifiedQueryEditor",
|
|
template: '<div class="stub-kw-editor" />',
|
|
props: ["query", "keywords", "suggestions", "fieldValueResolver"],
|
|
emits: ["update:query", "blur", "focus", "language-change", "ask-ai", "run-query"],
|
|
methods: {
|
|
// handleQueryUpdate reads these off queryEditorRef.
|
|
getCursorIndex() {
|
|
return 9999; // past end-of-query => analyse the whole string
|
|
},
|
|
triggerAutoComplete() {},
|
|
},
|
|
};
|
|
|
|
const mountWithStub = () =>
|
|
mount(QueryEditorDialog, {
|
|
props: {
|
|
modelValue: true,
|
|
tab: "sql",
|
|
sqlQuery: "",
|
|
promqlQuery: "",
|
|
vrlFunction: "",
|
|
streamName: "my-stream",
|
|
streamType: "logs",
|
|
columns: [
|
|
{ label: "host", value: "host" },
|
|
{ label: "level", value: "level" },
|
|
],
|
|
period: 10,
|
|
multiTimeRange: [],
|
|
savedFunctions: [],
|
|
sqlQueryErrorMsg: "",
|
|
},
|
|
global: {
|
|
plugins: [i18n, store],
|
|
stubs: {
|
|
UnifiedQueryEditor: editorStubDef,
|
|
// ODrawer is the dialog root; without a slot-rendering stub none of
|
|
// its content (including the editor) mounts.
|
|
ODrawer: {
|
|
name: "ODrawer",
|
|
props: ["open", "size", "showClose", "bleed", "persistent", "title", "width"],
|
|
emits: ["update:open"],
|
|
template: "<div><slot name='header-left' /><slot name='header-right' /><slot /></div>",
|
|
},
|
|
FullViewContainer: {
|
|
template: "<div><slot /><slot name='right' /></div>",
|
|
props: ["name", "label", "isExpanded"],
|
|
emits: ["update:isExpanded"],
|
|
},
|
|
O2AIChat: { template: "<div />", props: ["headerHeight", "isOpen"], emits: ["close"] },
|
|
},
|
|
},
|
|
});
|
|
|
|
it("renders the unified editor", () => {
|
|
const wrapper = mountWithStub();
|
|
expect(wrapper.findComponent({ name: "UnifiedQueryEditor" }).exists()).toBe(true);
|
|
});
|
|
|
|
it("hands the editor a resolver that produces the stored values", async () => {
|
|
const wrapper = mountWithStub();
|
|
const editor = wrapper.findComponent({ name: "UnifiedQueryEditor" });
|
|
// Drives handleQueryUpdate, which is where the dialog sets the org/stream
|
|
// context the lookup is keyed on. Without that the resolver returns [] and
|
|
// this fails — which is the wiring worth guarding.
|
|
await editor.vm.$emit("update:query", "level = ");
|
|
await flushPromises();
|
|
|
|
const resolve = editor.props("fieldValueResolver") as (f: string) => Promise<string[]>;
|
|
expect(typeof resolve, "no resolver reached the editor").toBe("function");
|
|
await expect(resolve("level")).resolves.toEqual(expect.arrayContaining(["error", "warn"]));
|
|
});
|
|
|
|
it("leaves the catalog suggestions alone in a value position", async () => {
|
|
// The provider swaps the whole list out for values itself. The parent
|
|
// blanking the catalog was part of the removed round trip, and doing it
|
|
// here would now take the functions away for no one's benefit.
|
|
const wrapper = mountWithStub();
|
|
const editor = wrapper.findComponent({ name: "UnifiedQueryEditor" });
|
|
await editor.vm.$emit("update:query", "level = ");
|
|
await flushPromises();
|
|
|
|
const suggestions = (editor.props("suggestions") ?? []) as any[];
|
|
expect(suggestions.some((s: any) => s.name === "match_all")).toBe(true);
|
|
});
|
|
});
|
|
|
|
// Stored field values for the resolver test above. Only useSuggestions
|
|
// consumes this module, so mocking it does not affect the rest of the suite.
|
|
vi.mock("@/composables/fieldValueStore", () => ({
|
|
getFieldValuesForSuggestion: vi.fn().mockResolvedValue(["error", "warn"]),
|
|
}));
|
|
|
|
// getSuggestions now awaits a lazy fetch of the server function catalog. Stub it
|
|
// so this suite makes no HTTP call and the awaited chain settles inside
|
|
// flushPromises(); the fetch itself is covered in
|
|
// useSuggestions.serverCatalog.spec.ts.
|
|
vi.mock("@/services/query_functions", () => ({
|
|
default: { list: vi.fn().mockResolvedValue({ data: { list: [] } }) },
|
|
}));
|