Files
openobserve/web/src/plugins/traces/SyntaxGuide.spec.ts
T
2026-05-06 15:00:37 +00:00

422 lines
15 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, afterEach } from "vitest";
import { mount, flushPromises } from "@vue/test-utils";
import { installQuasar } from "@/test/unit/helpers/install-quasar-plugin";
import { Dialog, Notify } from "quasar";
import SyntaxGuide from "@/plugins/traces/SyntaxGuide.vue";
import i18n from "@/locales";
import store from "@/test/unit/helpers/store";
installQuasar({
plugins: [Dialog, Notify],
});
const node = document.createElement("div");
node.setAttribute("id", "app");
node.style.height = "1024px";
document.body.appendChild(node);
describe("SyntaxGuide", () => {
let wrapper: any;
beforeEach(async () => {
wrapper = mount(SyntaxGuide, {
attachTo: "#app",
props: {
sqlmode: false,
},
global: {
provide: {
store: store,
},
plugins: [i18n],
},
});
await flushPromises();
});
afterEach(() => {
wrapper.unmount();
// Clean up any menus that might be attached to document body
const menus = document.querySelectorAll(".q-menu");
menus.forEach((menu) => menu.remove());
});
describe("Component Rendering", () => {
it("should render the syntax guide button", () => {
expect(wrapper.find('[data-cy="syntax-guide-button"]').exists()).toBe(
true,
);
});
it("should render button with help icon", () => {
const button = wrapper.find('[data-cy="syntax-guide-button"]');
// HelpCircle (lucide) renders as an SVG inside the button
expect(button.find("svg").exists()).toBe(true);
});
it("should have correct button classes", () => {
const button = wrapper.find('[data-cy="syntax-guide-button"]');
// traces SyntaxGuide uses OButton with only mode class (no syntax-guide-button class)
expect(button.classes()).toContain("normal-mode");
});
});
describe("Props", () => {
it("should apply normal-mode class when sqlmode is false", async () => {
await wrapper.setProps({ sqlmode: false });
const button = wrapper.find('[data-cy="syntax-guide-button"]');
expect(button.classes()).toContain("normal-mode");
expect(button.classes()).not.toContain("sql-mode");
});
it("should apply sql-mode class when sqlmode is true", async () => {
await wrapper.setProps({ sqlmode: true });
const button = wrapper.find('[data-cy="syntax-guide-button"]');
expect(button.classes()).toContain("sql-mode");
expect(button.classes()).not.toContain("normal-mode");
});
it("should have sqlmode prop with default value false", () => {
expect(wrapper.props("sqlmode")).toBe(false);
});
});
describe("Menu Content - Normal Mode", () => {
beforeEach(async () => {
await wrapper.setProps({ sqlmode: false });
await flushPromises();
});
it("should show normal mode title", async () => {
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
// Menu is attached to document body, so we need to search in document
const menu = document.querySelector(".q-menu");
expect(menu).toBeTruthy();
const title = document.querySelector(".syntax-guide-title .label");
expect(title?.textContent).toBe("Syntax Guide");
});
it("should display normal mode syntax examples", async () => {
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const guideList = document.querySelector(".guide-list");
expect(guideList).toBeTruthy();
const listItems = document.querySelectorAll(".guide-list li");
expect(listItems.length).toBeGreaterThan(0);
// Check for specific normal mode content
const text = document.body.textContent || "";
expect(text).toContain("match_all('error') in query editor");
expect(text).toContain("str_match(fieldname, 'error')");
expect(text).toContain("str_match_ignore_case(fieldname, 'Error')");
expect(text).toContain("code=200");
expect(text).toContain("stream='stderr'");
});
it("should contain link to documentation", async () => {
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const link = document.querySelector(
'a[href="https://openobserve.ai/docs/example-queries/"]',
);
expect(link).toBeTruthy();
expect(link?.getAttribute("target")).toBe("_blank");
});
});
describe("Menu Content - SQL Mode", () => {
beforeEach(async () => {
await wrapper.setProps({ sqlmode: true });
await flushPromises();
});
it("should show SQL mode title", async () => {
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const title = document.querySelector(".syntax-guide-title .label");
expect(title?.textContent).toBe("Syntax Guide: SQL Mode");
});
it("should display SQL mode syntax examples", async () => {
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const guideList = document.querySelector(".guide-list");
expect(guideList).toBeTruthy();
const listItems = document.querySelectorAll(".guide-list li");
expect(listItems.length).toBeGreaterThan(0);
// Check for specific SQL mode content
const text = document.body.textContent || "";
expect(text).toContain("SELECT * FROM stream WHERE match_all('error')");
expect(text).toContain(
"SELECT * FROM stream WHERE str_match(fieldname, 'error')",
);
expect(text).toContain("SELECT * FROM stream WHERE code=200");
expect(text).toContain("SELECT * FROM stream WHERE stream='stderr'");
expect(text).toContain(
"SELECT extract_ip(log) FROM stream WHERE code=200",
);
});
it("should contain link to documentation in SQL mode", async () => {
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const link = document.querySelector(
'a[href="https://openobserve.ai/docs/example-queries/"]',
);
expect(link).toBeTruthy();
expect(link?.getAttribute("target")).toBe("_blank");
});
});
describe("User Interactions", () => {
it("should open menu when button is clicked", async () => {
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const menu = document.querySelector(".q-menu");
expect(menu).toBeTruthy();
});
});
describe("Styling and Classes", () => {
it("should have correct button styling classes", () => {
const button = wrapper.find('[data-cy="syntax-guide-button"]');
// OButton uses Tailwind CSS classes; mode class is applied via :class binding
expect(button.exists()).toBe(true);
expect(button.classes()).toContain("normal-mode");
});
});
describe("Accessibility", () => {
it("should have data-cy attribute for testing", () => {
const button = wrapper.find('[data-cy="syntax-guide-button"]');
expect(button.exists()).toBe(true);
});
it("should have tooltip component for accessibility", () => {
const button = wrapper.find('[data-cy="syntax-guide-button"]');
// Check that QTooltip component is present as a child
const tooltipComponent = button.findComponent({ name: "QTooltip" });
expect(tooltipComponent.exists()).toBe(true);
});
});
describe("Component Integration", () => {
it("should work with i18n translations", () => {
const button = wrapper.find('[data-cy="syntax-guide-button"]');
// Check that QTooltip component is present
const tooltipComponent = button.findComponent({ name: "QTooltip" });
expect(tooltipComponent.exists()).toBe(true);
});
it("should integrate with Vuex store", () => {
expect(wrapper.vm.store).toBeDefined();
});
it("should use Vue I18n composable", () => {
expect(wrapper.vm.t).toBeDefined();
expect(typeof wrapper.vm.t).toBe("function");
});
});
describe("Edge Cases", () => {
it("should handle undefined sqlmode prop gracefully", async () => {
await wrapper.setProps({ sqlmode: undefined });
await flushPromises();
const button = wrapper.find('[data-cy="syntax-guide-button"]');
expect(button.classes()).toContain("normal-mode");
});
it("should handle null sqlmode prop gracefully", async () => {
await wrapper.setProps({ sqlmode: null });
await flushPromises();
const button = wrapper.find('[data-cy="syntax-guide-button"]');
expect(button.classes()).toContain("normal-mode");
});
});
// ─── New tests covering functionality added after Dec 29 2025 ───────────────
describe("Menu theme class", () => {
afterEach(() => {
store.state.theme = "dark"; // restore shared store default
const menus = document.querySelectorAll(".q-menu");
menus.forEach((m) => m.remove());
});
it("should apply theme-light class to menu when store theme is light", async () => {
store.state.theme = "light";
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const menu = document.querySelector('[data-test="syntax-guide-menu"]');
expect(menu?.classList.contains("theme-light")).toBe(true);
});
it("should apply theme-dark class to menu when store theme is dark", async () => {
const { createStore } = await import("vuex");
const darkStore = createStore({ state: { theme: "dark" } });
const darkWrapper = mount(SyntaxGuide, {
attachTo: "#app",
props: { sqlmode: false },
global: {
provide: { store: darkStore },
plugins: [i18n],
},
});
await flushPromises();
const button = darkWrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const menu = document.querySelector('[data-test="syntax-guide-menu"]');
expect(menu?.classList.contains("theme-dark")).toBe(true);
darkWrapper.unmount();
});
});
describe("str_match_ignore_case only in normal mode", () => {
it("should contain str_match_ignore_case example only in normal mode", async () => {
await wrapper.setProps({ sqlmode: false });
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const text = document.body.textContent || "";
expect(text).toContain("str_match_ignore_case");
});
it("should NOT contain str_match_ignore_case in SQL mode", async () => {
await wrapper.setProps({ sqlmode: true });
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const text = document.body.textContent || "";
expect(text).not.toContain("str_match_ignore_case");
});
});
describe("extract_ip query function example only in SQL mode", () => {
afterEach(() => {
const menus = document.querySelectorAll(".q-menu");
menus.forEach((m) => m.remove());
});
it("should contain extract_ip example in SQL mode", async () => {
await wrapper.setProps({ sqlmode: true });
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const text = document.body.textContent || "";
expect(text).toContain("extract_ip");
});
it("should NOT contain extract_ip example in normal mode", async () => {
await wrapper.setProps({ sqlmode: false });
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const text = document.body.textContent || "";
expect(text).not.toContain("extract_ip");
});
});
describe("sqlmode prop toggle between modes", () => {
afterEach(() => {
const menus = document.querySelectorAll(".q-menu");
menus.forEach((m) => m.remove());
});
it("should switch button class from normal-mode to sql-mode dynamically", async () => {
await wrapper.setProps({ sqlmode: false });
let button = wrapper.find('[data-cy="syntax-guide-button"]');
expect(button.classes()).toContain("normal-mode");
await wrapper.setProps({ sqlmode: true });
button = wrapper.find('[data-cy="syntax-guide-button"]');
expect(button.classes()).toContain("sql-mode");
});
it("should switch button class from sql-mode back to normal-mode dynamically", async () => {
await wrapper.setProps({ sqlmode: true });
await wrapper.setProps({ sqlmode: false });
const button = wrapper.find('[data-cy="syntax-guide-button"]');
expect(button.classes()).toContain("normal-mode");
expect(button.classes()).not.toContain("sql-mode");
});
});
describe("Documentation link present in both modes", () => {
afterEach(() => {
const menus = document.querySelectorAll(".q-menu");
menus.forEach((m) => m.remove());
});
it("normal mode: docs link opens in new tab", async () => {
await wrapper.setProps({ sqlmode: false });
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const link = document.querySelector(
'a[href="https://openobserve.ai/docs/example-queries/"]',
);
expect(link?.getAttribute("target")).toBe("_blank");
});
it("sql mode: docs link opens in new tab", async () => {
await wrapper.setProps({ sqlmode: true });
const button = wrapper.find('[data-cy="syntax-guide-button"]');
await button.trigger("click");
await flushPromises();
const link = document.querySelector(
'a[href="https://openobserve.ai/docs/example-queries/"]',
);
expect(link?.getAttribute("target")).toBe("_blank");
});
});
});