// Copyright 2023 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 . import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { mount, flushPromises } from "@vue/test-utils"; import { reactive, nextTick } from "vue"; import { createI18n } from "vue-i18n"; import { Quasar, Dialog, Notify } from "quasar"; import BuildQueryPage from "./BuildQueryPage.vue"; // Mock vuex store const mockStore = { state: { timezone: "UTC", theme: "light", selectedOrganization: { identifier: "test-org", }, organizationData: { isDataIngested: true, }, zoConfig: { timestamp_column: "_timestamp", default_functions: [], }, savedViewDialog: { show: false, }, }, commit: vi.fn(), dispatch: vi.fn().mockResolvedValue({}), getters: {}, }; vi.mock("vuex", () => ({ useStore: () => mockStore, })); // Mock vue-router const mockRouter = { push: vi.fn(), currentRoute: { value: { name: "logs", query: {}, // Empty query - no build_data in URL }, }, }; vi.mock("vue-router", () => ({ useRouter: () => mockRouter, })); // Mock logsVisualization vi.mock("@/composables/useLogs/logsVisualization", () => ({ decodeBuildConfig: vi.fn().mockReturnValue(null), })); // Mock SQL query parser vi.mock("@/utils/query/sqlQueryParser", () => ({ parseSQL: vi.fn().mockResolvedValue({ stream: "test_stream", streamType: "logs", xFields: [], yFields: [], breakdownFields: [], filters: [], groupBy: [], customQuery: false, rawQuery: "", }), shouldUseCustomMode: vi.fn().mockReturnValue(false), parsedQueryToPanelFields: vi.fn().mockReturnValue({ stream: "test_stream", stream_type: "logs", x: [], y: [], breakdown: [], filter: [], }), })); // Mock useDashboardPanelData const mockDashboardPanelData = reactive({ data: { description: "", type: "line", config: { table_dynamic_columns: false, }, queries: [ { query: "", customQuery: false, fields: { stream: "", stream_type: "logs", x: [], y: [], z: [], breakdown: [], filter: [], }, }, ], }, layout: { splitter: 20, showFieldList: true, showQueryBar: true, querySplitter: 50, currentQueryIndex: 0, isConfigPanelOpen: false, }, meta: { dateTime: { start_time: new Date("2024-01-01"), end_time: new Date("2024-01-02"), }, stream: { customQueryFields: [], vrlFunctionFieldList: [], }, streamFields: { groupedFields: [], }, }, }); const mockResetDashboardPanelData = vi.fn(); const mockMakeAutoSQLQuery = vi.fn(); const mockUpdateGroupedFields = vi.fn().mockResolvedValue(undefined); vi.mock("@/composables/useDashboardPanel", () => ({ default: () => ({ dashboardPanelData: mockDashboardPanelData, resetAggregationFunction: vi.fn(), resetDashboardPanelData: mockResetDashboardPanelData, makeAutoSQLQuery: mockMakeAutoSQLQuery, updateGroupedFields: mockUpdateGroupedFields, }), })); // Mock PanelEditor component vi.mock("@/components/dashboards/PanelEditor/PanelEditor.vue", () => ({ default: { name: "PanelEditor", template: '
', props: ["pageType", "editMode", "selectedDateTime", "showAddToDashboardButton"], emits: ["addToDashboard", "chartApiError", "queryGenerated", "customQueryModeChanged"], methods: { runQuery: vi.fn(), }, }, })); // Mock AddToDashboard component vi.mock("@/plugins/metrics/AddToDashboard.vue", () => ({ default: { name: "AddToDashboard", template: '
AddToDashboard
', props: ["dashboardPanelData"], emits: ["save"], }, })); // Mock QueryTypeSelector component vi.mock("@/components/dashboards/addPanel/QueryTypeSelector.vue", () => ({ default: { name: "QueryTypeSelector", template: '
QueryTypeSelector
', }, })); // Create i18n instance const i18n = createI18n({ legacy: false, locale: "en", messages: { en: { common: { cancel: "Cancel", apply: "Apply", save: "Save", }, search: { buildQuery: "Build Query", }, panel: { addToDashboard: "Add to Dashboard", }, }, }, }); // Create a mount helper function createWrapper(props = {}) { return mount(BuildQueryPage, { props: { searchQuery: "", selectedStream: "", selectedDateTime: undefined, ...props, }, global: { plugins: [i18n, [Quasar, { plugins: { Dialog, Notify } }]], provide: { store: mockStore, dashboardPanelDataPageKey: "build", }, stubs: { PanelEditor: { template: '
', methods: { runQuery: vi.fn(), }, }, AddToDashboard: true, QueryTypeSelector: true, "q-dialog": true, }, }, }); } describe("BuildQueryPage Component", () => { let wrapper: any; beforeEach(() => { vi.clearAllMocks(); // Reset mock data mockDashboardPanelData.data.queries[0].query = ""; mockDashboardPanelData.data.queries[0].customQuery = false; mockDashboardPanelData.data.queries[0].fields.stream = ""; mockDashboardPanelData.data.type = "line"; mockDashboardPanelData.data.config.table_dynamic_columns = false; }); afterEach(() => { if (wrapper) { wrapper.unmount(); } }); describe("Rendering", () => { it("should render the build query page container", async () => { wrapper = createWrapper(); await flushPromises(); expect(wrapper.find(".build-query-page").exists()).toBe(true); }); it("should render the PanelEditor component", async () => { wrapper = createWrapper(); await flushPromises(); expect(wrapper.find('[data-test="panel-editor"]').exists()).toBe(true); }); it("should have PanelEditor stub with correct test attribute", async () => { wrapper = createWrapper(); await flushPromises(); // The PanelEditor stub renders with data-test attribute const panelEditorStub = wrapper.find('[data-test="panel-editor"]'); expect(panelEditorStub.exists()).toBe(true); expect(panelEditorStub.classes()).toContain("panel-editor-mock"); }); }); describe("Props Handling", () => { it("should accept searchQuery prop", async () => { const testQuery = 'SELECT * FROM "test_stream"'; wrapper = createWrapper({ searchQuery: testQuery }); await flushPromises(); expect(wrapper.props("searchQuery")).toBe(testQuery); }); it("should accept selectedStream prop", async () => { const testStream = "my_logs_stream"; wrapper = createWrapper({ selectedStream: testStream }); await flushPromises(); expect(wrapper.props("selectedStream")).toBe(testStream); }); it("should accept selectedDateTime prop", async () => { const testDateTime = { start_time: new Date("2024-01-01"), end_time: new Date("2024-01-02"), valueType: "absolute", }; wrapper = createWrapper({ selectedDateTime: testDateTime }); await flushPromises(); expect(wrapper.props("selectedDateTime")).toEqual(testDateTime); }); }); describe("Initialization", () => { it("should call resetDashboardPanelData on mount", async () => { wrapper = createWrapper(); await flushPromises(); expect(mockResetDashboardPanelData).toHaveBeenCalled(); }); it("should initialize with builder mode for empty query", async () => { wrapper = createWrapper({ searchQuery: "", selectedStream: "test_stream", }); await flushPromises(); expect(mockDashboardPanelData.data.queries[0].customQuery).toBe(false); }); it("should set stream from selectedStream prop when no query", async () => { wrapper = createWrapper({ searchQuery: "", selectedStream: "my_logs_stream", }); await flushPromises(); expect(mockDashboardPanelData.data.queries[0].fields.stream).toBe("my_logs_stream"); expect(mockDashboardPanelData.data.queries[0].fields.stream_type).toBe("logs"); }); it("should call updateGroupedFields when stream is set", async () => { wrapper = createWrapper({ searchQuery: "", selectedStream: "test_stream", }); await flushPromises(); expect(mockUpdateGroupedFields).toHaveBeenCalled(); }); }); describe("Query Parsing", () => { it("should try to parse SQL query when provided", async () => { const { parseSQL } = await import("@/utils/query/sqlQueryParser"); wrapper = createWrapper({ searchQuery: 'SELECT * FROM "test_stream"', selectedStream: "test_stream", }); await flushPromises(); expect(parseSQL).toHaveBeenCalled(); }); it("should use custom mode for complex queries", async () => { const { shouldUseCustomMode } = await import("@/utils/query/sqlQueryParser"); (shouldUseCustomMode as any).mockReturnValueOnce(true); wrapper = createWrapper({ searchQuery: 'SELECT * FROM (SELECT * FROM "test_stream")', selectedStream: "test_stream", }); await flushPromises(); expect(mockDashboardPanelData.data.queries[0].customQuery).toBe(true); }); }); describe("Events", () => { it("should forward queryGenerated event from PanelEditor", async () => { wrapper = createWrapper({ searchQuery: "", selectedStream: "logs", }); await flushPromises(); // Find PanelEditor mock by data-test attribute and trigger event const panelEditorMock = wrapper.find('[data-test="panel-editor"]'); expect(panelEditorMock.exists()).toBe(true); // Trigger the event using Vue's event system await wrapper.vm.$options.components?.PanelEditor?.methods?.runQuery?.(); // Directly call the onQueryGenerated handler to simulate PanelEditor emitting the event // This tests that the forwarding logic works const testQuery = "SELECT * FROM logs"; // Access the internal handler via the component's setup wrapper.vm.onQueryGenerated?.(testQuery) || wrapper.vm.$emit("queryGenerated", testQuery); // Fallback await flushPromises(); // Check if queryGenerated was forwarded by BuildQueryPage const emitted = wrapper.emitted("queryGenerated"); expect(emitted).toBeTruthy(); }); it("should forward customQueryModeChanged event from PanelEditor", async () => { wrapper = createWrapper({ searchQuery: "", selectedStream: "logs", }); await flushPromises(); // Directly call the onCustomQueryModeChanged handler to simulate PanelEditor emitting the event if (wrapper.vm.onCustomQueryModeChanged) { wrapper.vm.onCustomQueryModeChanged(true); } await flushPromises(); // Check that BuildQueryPage forwards the event const emitted = wrapper.emitted("customQueryModeChanged"); expect(emitted).toBeTruthy(); // Find the emitted event with value true const hasTrue = emitted!.some((args: any[]) => args[0] === true); expect(hasTrue).toBe(true); }); }); describe("Exposed Methods", () => { it("should expose runQuery method", async () => { wrapper = createWrapper(); await flushPromises(); expect(wrapper.vm.runQuery).toBeDefined(); expect(typeof wrapper.vm.runQuery).toBe("function"); }); it("should expose panelEditorRef", async () => { wrapper = createWrapper(); await flushPromises(); expect(wrapper.vm.panelEditorRef).toBeDefined(); }); it("should expose dashboardPanelData", async () => { wrapper = createWrapper(); await flushPromises(); expect(wrapper.vm.dashboardPanelData).toBeDefined(); }); }); describe("Add to Dashboard Dialog", () => { it("should show add to dashboard dialog when triggered", async () => { wrapper = createWrapper(); await flushPromises(); // Check initial state expect(wrapper.vm.showAddToDashboardDialog).toBe(false); // Trigger the dialog wrapper.vm.showAddToDashboardDialog = true; await nextTick(); expect(wrapper.vm.showAddToDashboardDialog).toBe(true); }); it("should close add to dashboard dialog on addPanelToDashboard", async () => { wrapper = createWrapper(); await flushPromises(); wrapper.vm.showAddToDashboardDialog = true; await nextTick(); // Call the handler wrapper.vm.showAddToDashboardDialog = false; await nextTick(); expect(wrapper.vm.showAddToDashboardDialog).toBe(false); }); }); describe("DateTime Handling", () => { it("should sync datetime from props to dashboardPanelData", async () => { const testDateTime = { start_time: new Date("2024-06-01").getTime(), end_time: new Date("2024-06-02").getTime(), valueType: "absolute" as const, }; wrapper = createWrapper({ selectedDateTime: testDateTime, }); await flushPromises(); expect(mockDashboardPanelData.meta.dateTime).toEqual(testDateTime); }); }); describe("Error Handling", () => { it("should handle chart API errors gracefully", async () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); wrapper = createWrapper(); await flushPromises(); // Simulate chart API error wrapper.vm.handleChartApiError("Test error"); expect(consoleErrorSpy).toHaveBeenCalledWith("Chart API error:", "Test error"); consoleErrorSpy.mockRestore(); }); it("should fallback to custom mode on parse error", async () => { const { parseSQL } = await import("@/utils/query/sqlQueryParser"); (parseSQL as any).mockRejectedValueOnce(new Error("Parse error")); const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); wrapper = createWrapper({ searchQuery: "INVALID SQL", selectedStream: "test_stream", }); await flushPromises(); expect(mockDashboardPanelData.data.queries[0].customQuery).toBe(true); consoleErrorSpy.mockRestore(); }); }); describe("Query Mode", () => { it("should set customQuery to false for builder mode", async () => { const { parseSQL } = await import("@/utils/query/sqlQueryParser"); (parseSQL as any).mockResolvedValueOnce({ customQuery: false, stream: "test_stream", }); wrapper = createWrapper({ searchQuery: 'SELECT * FROM "test_stream"', selectedStream: "test_stream", }); await flushPromises(); expect(mockDashboardPanelData.data.queries[0].customQuery).toBe(false); }); it("should set table type with dynamic columns for custom mode", async () => { const { shouldUseCustomMode } = await import("@/utils/query/sqlQueryParser"); (shouldUseCustomMode as any).mockReturnValueOnce(true); wrapper = createWrapper({ searchQuery: "complex query", selectedStream: "test_stream", }); await flushPromises(); expect(mockDashboardPanelData.data.type).toBe("table"); expect(mockDashboardPanelData.data.config.table_dynamic_columns).toBe(true); }); it("should auto-select metric chart type when only Y-axis fields are present", async () => { const { parsedQueryToPanelFields, shouldUseCustomMode, parseSQL } = await import("@/utils/query/sqlQueryParser"); // Mock parseSQL to return a valid parsed result (parseSQL as any).mockResolvedValueOnce({ stream: "test_stream", streamType: "logs", xFields: [], yFields: [{ column: "_timestamp", alias: "y_axis_1", aggregationFunction: "count" }], breakdownFields: [], filters: { filterType: "group", logicalOperator: "AND", conditions: [] }, customQuery: false, rawQuery: 'SELECT count(_timestamp) as "y_axis_1" FROM "test_stream"', }); // Mock parsedQueryToPanelFields to return only Y-axis field (no X, no breakdown) (parsedQueryToPanelFields as any).mockReturnValueOnce({ stream: "test_stream", stream_type: "logs", x: [], // No X-axis fields y: [{ column: "_timestamp", alias: "y_axis_1", functionName: "count" }], // Has Y-axis field breakdown: [], // No breakdown fields filter: { filterType: "group", logicalOperator: "AND", conditions: [] }, }); (shouldUseCustomMode as any).mockReturnValueOnce(false); wrapper = createWrapper({ searchQuery: 'SELECT count(_timestamp) as "y_axis_1" FROM "test_stream"', selectedStream: "test_stream", }); await flushPromises(); // Should auto-select "metric" chart type for Y-axis only queries expect(mockDashboardPanelData.data.type).toBe("metric"); }); it("should auto-select table chart type when zero Y-axis fields are present", async () => { const { parsedQueryToPanelFields, shouldUseCustomMode, parseSQL } = await import("@/utils/query/sqlQueryParser"); // Mock parseSQL to return a valid parsed result with no Y-axis fields (parseSQL as any).mockResolvedValueOnce({ stream: "test_stream", streamType: "logs", xFields: [{ column: "method", alias: "x_axis_1", aggregationFunction: null }], yFields: [], breakdownFields: [], filters: { filterType: "group", logicalOperator: "AND", conditions: [] }, customQuery: false, rawQuery: 'SELECT method FROM "test_stream"', }); // Mock parsedQueryToPanelFields to return only X-axis field (no Y, no breakdown) (parsedQueryToPanelFields as any).mockReturnValueOnce({ stream: "test_stream", stream_type: "logs", x: [{ column: "method", alias: "x_axis_1", functionName: null }], y: [], // No Y-axis fields breakdown: [], filter: { filterType: "group", logicalOperator: "AND", conditions: [] }, }); (shouldUseCustomMode as any).mockReturnValueOnce(false); wrapper = createWrapper({ searchQuery: 'SELECT method FROM "test_stream"', selectedStream: "test_stream", }); await flushPromises(); // Should auto-select "table" chart type when no Y-axis fields expect(mockDashboardPanelData.data.type).toBe("table"); }); }); describe("Run Query", () => { it("should call PanelEditor runQuery for builder mode", async () => { mockDashboardPanelData.data.queries[0].customQuery = false; wrapper = createWrapper({ selectedStream: "test_stream", }); await flushPromises(); // Mock PanelEditor's runQuery method const mockRunQuery = vi.fn(); wrapper.vm.panelEditorRef = { runQuery: mockRunQuery }; await wrapper.vm.runQuery(); await flushPromises(); // PanelEditor's runQuery should be called (BuildQueryPage delegates to PanelEditor) expect(mockRunQuery).toHaveBeenCalled(); }); it("should call PanelEditor runQuery for custom mode", async () => { mockDashboardPanelData.data.queries[0].customQuery = true; wrapper = createWrapper({ selectedStream: "test_stream", }); await flushPromises(); // Get PanelEditor ref and mock its runQuery const mockRunQuery = vi.fn(); wrapper.vm.panelEditorRef = { runQuery: mockRunQuery }; await wrapper.vm.runQuery(); await flushPromises(); // PanelEditor's runQuery should be called for custom queries too expect(mockRunQuery).toHaveBeenCalled(); }); it("should update stream fields before running query in builder mode", async () => { mockDashboardPanelData.data.queries[0].customQuery = false; mockDashboardPanelData.meta.streamFields = { groupedFields: [] }; mockUpdateGroupedFields.mockClear(); wrapper = createWrapper({ selectedStream: "test_stream", }); await flushPromises(); // Mock panelEditorRef wrapper.vm.panelEditorRef = { runQuery: vi.fn() }; await wrapper.vm.runQuery(); await flushPromises(); expect(mockUpdateGroupedFields).toHaveBeenCalled(); }); }); }); describe("BuildQueryPage Component - Integration Tests", () => { let wrapper: any; beforeEach(() => { vi.clearAllMocks(); mockDashboardPanelData.data.queries[0].query = ""; mockDashboardPanelData.data.queries[0].customQuery = false; mockDashboardPanelData.data.queries[0].fields.stream = ""; }); afterEach(() => { if (wrapper) { wrapper.unmount(); } }); it("should handle full workflow: select stream, build query, apply", async () => { const { parseSQL, parsedQueryToPanelFields } = await import("@/utils/query/sqlQueryParser"); // Mock successful parse (parseSQL as any).mockResolvedValueOnce({ customQuery: false, stream: "logs", }); (parsedQueryToPanelFields as any).mockReturnValueOnce({ stream: "logs", stream_type: "logs", x: [{ column: "_timestamp" }], y: [{ column: "count" }], breakdown: [], filter: [], }); wrapper = createWrapper({ searchQuery: "", selectedStream: "logs", }); await flushPromises(); // Verify initialization expect(mockDashboardPanelData.data.queries[0].fields.stream).toBe("logs"); // Mock PanelEditor's runQuery method const mockRunQuery = vi.fn(); wrapper.vm.panelEditorRef = { runQuery: mockRunQuery }; // Run query (this delegates to PanelEditor) await wrapper.vm.runQuery(); await flushPromises(); // Verify PanelEditor's runQuery was called expect(mockRunQuery).toHaveBeenCalled(); // Simulate PanelEditor emitting queryGenerated via the onQueryGenerated handler const generatedQuery = 'SELECT histogram(_timestamp) FROM "logs"'; if (wrapper.vm.onQueryGenerated) { wrapper.vm.onQueryGenerated(generatedQuery); } await flushPromises(); // Verify query event was forwarded by BuildQueryPage const emitted = wrapper.emitted("queryGenerated"); expect(emitted).toBeTruthy(); expect(emitted![emitted!.length - 1]).toEqual([generatedQuery]); }); });