// 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 . import { describe, expect, it, afterEach } from "vitest"; import { mount, VueWrapper } from "@vue/test-utils"; import QueryPlanTree from "./QueryPlanTree.vue"; import type { OperatorNode } from "@/utils/queryPlanParser"; function makeNode(name: string, children: OperatorNode[] = []): OperatorNode { return { name, fullText: name, depth: 0, metrics: {}, children, isRepartitionExec: false, }; } function makeRootNode(children: OperatorNode[]): OperatorNode { return { name: "Root", fullText: "", depth: -1, metrics: {}, children, isRepartitionExec: false, }; } function mountTree(tree: OperatorNode, props: Record = {}) { return mount(QueryPlanTree, { props: { tree, isAnalyze: false, ...props, }, }); } describe("QueryPlanTree", () => { let wrapper: VueWrapper; afterEach(() => { wrapper?.unmount(); }); describe("initial render", () => { it("should render without errors", () => { wrapper = mountTree(makeRootNode([])); expect(wrapper.exists()).toBe(true); }); it("should render the container with query-plan-tree class", () => { wrapper = mountTree(makeRootNode([])); expect(wrapper.find(".query-plan-tree").exists()).toBe(true); }); }); describe("empty tree", () => { it("should render no tree-node elements for empty root children", () => { wrapper = mountTree(makeRootNode([])); expect(wrapper.findAll(".tree-node")).toHaveLength(0); }); }); describe("single child", () => { it("should render one tree-node for a single child", () => { const tree = makeRootNode([makeNode("SortExec")]); wrapper = mountTree(tree); expect(wrapper.findAll(".tree-node")).toHaveLength(1); }); it("should render the operator name of the single child", () => { const tree = makeRootNode([makeNode("FilterExec")]); wrapper = mountTree(tree); expect(wrapper.find('[data-test="query-plan-node-operator-name"]').text()).toBe("FilterExec"); }); }); describe("multiple children", () => { it("should render correct number of tree-node elements", () => { const tree = makeRootNode([ makeNode("ProjectionExec"), makeNode("FilterExec"), makeNode("SortExec"), ]); wrapper = mountTree(tree); expect(wrapper.findAll(".tree-node")).toHaveLength(3); }); it("should render operator names for all children", () => { const tree = makeRootNode([makeNode("ProjectionExec"), makeNode("FilterExec")]); wrapper = mountTree(tree); const names = wrapper.findAll('[data-test="query-plan-node-operator-name"]'); expect(names[0].text()).toBe("ProjectionExec"); expect(names[1].text()).toBe("FilterExec"); }); it("should give isLast=true only to the last root child", () => { const tree = makeRootNode([ makeNode("FirstExec"), makeNode("MiddleExec"), makeNode("LastExec"), ]); wrapper = mountTree(tree); const connectors = wrapper.findAll('[data-test="query-plan-node-tree-connector"]'); // first two → ├─, last one → └─ expect(connectors[0].text()).toBe("├─"); expect(connectors[1].text()).toBe("├─"); expect(connectors[2].text()).toBe("└─"); }); }); describe("isAnalyze prop", () => { it("should pass isAnalyze=false to child nodes by default", () => { const nodeWithMetrics = makeNode("FilterExec"); nodeWithMetrics.metrics = { output_rows: 100, elapsed_compute: "1ms" }; const tree = makeRootNode([nodeWithMetrics]); wrapper = mountTree(tree, { isAnalyze: false }); // Metrics section should not show when isAnalyze=false expect(wrapper.find('[data-test="query-plan-node-metrics-inline"]').exists()).toBe(false); }); it("should pass isAnalyze=true to child nodes when set", () => { const nodeWithMetrics = makeNode("FilterExec"); nodeWithMetrics.metrics = { output_rows: 100, elapsed_compute: "1ms" }; const tree = makeRootNode([nodeWithMetrics]); wrapper = mountTree(tree, { isAnalyze: true }); // Metrics section should show when isAnalyze=true expect(wrapper.find('[data-test="query-plan-node-metrics-inline"]').exists()).toBe(true); }); }); describe("nested children propagation", () => { it("should render nested children via QueryPlanNode recursion", () => { const grandchild = makeNode("GrandchildExec"); const child = makeNode("ChildExec", [grandchild]); const tree = makeRootNode([child]); wrapper = mountTree(tree); // The grandchild should be rendered inside the child's children container expect( wrapper.find('.children .plan-node [data-test="query-plan-node-operator-name"]').text(), ).toBe("GrandchildExec"); }); }); describe("parentPrefix passed as empty string to root children", () => { it("should not render tree-indent for root-level nodes", () => { const tree = makeRootNode([makeNode("RootChild")]); wrapper = mountTree(tree); // Root children get parentPrefix="" so no tree-indent span should appear at top level expect( wrapper .find('.tree-node > .plan-node > .node-line > [data-test="query-plan-node-tree-indent"]') .exists(), ).toBe(false); }); }); });