mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
1246 lines
37 KiB
TypeScript
1246 lines
37 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, shallowMount } from "@vue/test-utils";
|
|
import i18n from "@/locales";
|
|
import Header from "@/components/Header.vue";
|
|
import * as cookies from "@/utils/cookies";
|
|
import { chartColor } from "@/utils/chartTheme";
|
|
|
|
// Mock the cookies module
|
|
vi.mock("@/utils/cookies", () => ({
|
|
getLanguage: vi.fn(() => "en-us"),
|
|
setLanguage: vi.fn(),
|
|
getSidebarStatus: vi.fn(),
|
|
setSidebarStatus: vi.fn(),
|
|
getSize: vi.fn(),
|
|
setSize: vi.fn(),
|
|
getToken: vi.fn(),
|
|
setToken: vi.fn(),
|
|
removeToken: vi.fn(),
|
|
}));
|
|
|
|
describe("Header Component", () => {
|
|
let wrapper: any;
|
|
let mockStore: any;
|
|
let mockRouter: any;
|
|
let mockConfig: any;
|
|
let mockUser: any;
|
|
let defaultProps: any;
|
|
let defaultGlobalConfig: any;
|
|
|
|
const createWrapper = (
|
|
options: {
|
|
mountType?: "shallow" | "mount";
|
|
storeOverrides?: any;
|
|
configOverrides?: any;
|
|
propsOverrides?: any;
|
|
stubsOverrides?: any;
|
|
} = {},
|
|
) => {
|
|
const {
|
|
mountType = "shallow",
|
|
storeOverrides = {},
|
|
configOverrides = {},
|
|
propsOverrides = {},
|
|
stubsOverrides = {},
|
|
} = options;
|
|
|
|
const store = {
|
|
...mockStore,
|
|
state: {
|
|
...mockStore.state,
|
|
...storeOverrides.state,
|
|
zoConfig: {
|
|
...mockStore.state.zoConfig,
|
|
...storeOverrides.state?.zoConfig,
|
|
},
|
|
},
|
|
};
|
|
|
|
const config = {
|
|
...mockConfig,
|
|
...configOverrides,
|
|
};
|
|
|
|
const props = {
|
|
...defaultProps,
|
|
store,
|
|
config,
|
|
...propsOverrides,
|
|
};
|
|
|
|
const globalConfig = {
|
|
plugins: defaultGlobalConfig.plugins,
|
|
// Header renders useTheme(), whose useStore() resolves via vuex's `store`
|
|
// injection key — the store prop alone does not satisfy it. Provide the
|
|
// same per-test store so storeOverrides still drive theme-dependent logic.
|
|
provide: { store },
|
|
stubs: {
|
|
...defaultGlobalConfig.stubs,
|
|
...stubsOverrides,
|
|
},
|
|
};
|
|
|
|
if (mountType === "mount") {
|
|
return mount(Header, {
|
|
global: globalConfig,
|
|
props,
|
|
});
|
|
}
|
|
|
|
return shallowMount(Header, {
|
|
global: globalConfig,
|
|
props,
|
|
});
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockStore = {
|
|
state: {
|
|
theme: "light",
|
|
zoConfig: {
|
|
custom_logo_text: "",
|
|
custom_logo_img: null,
|
|
custom_logo_dark_img: null,
|
|
custom_hide_self_logo: false,
|
|
custom_hide_menus: "",
|
|
ai_enabled: false,
|
|
ingestion_quota_used: 0,
|
|
version: "2.0.0",
|
|
},
|
|
organizationData: {
|
|
quotaThresholdMsg: "",
|
|
},
|
|
isAiChatEnabled: false,
|
|
selectedOrganization: {
|
|
identifier: "test-org",
|
|
label: "Test Organization",
|
|
},
|
|
},
|
|
};
|
|
|
|
mockRouter = {
|
|
replace: vi.fn(),
|
|
push: vi.fn(),
|
|
};
|
|
|
|
mockConfig = {
|
|
isEnterprise: "false",
|
|
isCloud: "false",
|
|
};
|
|
|
|
mockUser = {
|
|
given_name: "John",
|
|
family_name: "Doe",
|
|
email: "john@test.com",
|
|
};
|
|
|
|
defaultProps = {
|
|
store: mockStore,
|
|
router: mockRouter,
|
|
config: mockConfig,
|
|
user: mockUser,
|
|
slackIcon: {},
|
|
zoBackendUrl: "http://localhost:5080",
|
|
langList: [
|
|
{ code: "en-us", label: "English" },
|
|
{ code: "fr", label: "Français" },
|
|
],
|
|
selectedLanguage: { code: "en-us", label: "English" },
|
|
selectedOrg: { identifier: "test-org", label: "Test Organization" },
|
|
userClickedOrg: { identifier: "test-org", label: "Test Organization" },
|
|
// Header.vue migration replaced filteredOrganizations/searchQuery/rowsPerPage
|
|
// with a single 'organizations' prop; the search/pagination state moved
|
|
// inside the component once it switched to OSelect.
|
|
organizations: [
|
|
{ identifier: "test-org", label: "Test Organization" },
|
|
{ identifier: "another-org", label: "Another Organization" },
|
|
],
|
|
isHovered: false,
|
|
getBtnLogo: "mock-logo.svg",
|
|
};
|
|
|
|
defaultGlobalConfig = {
|
|
plugins: [i18n],
|
|
stubs: {
|
|
ThemeSwitcher: true,
|
|
},
|
|
};
|
|
|
|
wrapper = createWrapper();
|
|
});
|
|
|
|
describe("Logo Rendering", () => {
|
|
it("should display OpenObserve logo by default", async () => {
|
|
const logoWrapper = createWrapper({
|
|
mountType: "mount",
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await logoWrapper.vm.$nextTick();
|
|
|
|
// Verify the OpenObserve logo is rendered
|
|
const openobserveLogo = logoWrapper.find('[data-test="header-openobserve-logo"]');
|
|
expect(openobserveLogo.exists()).toBe(true);
|
|
|
|
logoWrapper.unmount();
|
|
});
|
|
|
|
it("should display custom logo text when configured in enterprise mode", async () => {
|
|
const customWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
custom_logo_text: "Custom Company",
|
|
},
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
// Wait for component to render
|
|
await customWrapper.vm.$nextTick();
|
|
|
|
// Check if the custom logo text is rendered in the HTML
|
|
const html = customWrapper.html();
|
|
expect(html).toContain("Custom Company");
|
|
|
|
// Verify the text is also in the text content
|
|
const allText = customWrapper.text();
|
|
expect(allText).toContain("Custom Company");
|
|
|
|
customWrapper.unmount();
|
|
});
|
|
|
|
it("should emit goToHome when calling the method", () => {
|
|
wrapper.vm.goToHome();
|
|
expect(wrapper.emitted("goToHome")).toBeTruthy();
|
|
});
|
|
|
|
it("should use dark theme logo when theme is dark", async () => {
|
|
const darkWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
theme: "dark",
|
|
},
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await darkWrapper.vm.$nextTick();
|
|
|
|
// Verify the logo exists and check that dark theme is applied
|
|
const openobserveLogo = darkWrapper.find('[data-test="header-openobserve-logo"]');
|
|
expect(openobserveLogo.exists()).toBe(true);
|
|
|
|
// The logo src should contain 'dark' in the path
|
|
const logoSrc = openobserveLogo.attributes("src");
|
|
expect(logoSrc).toContain("dark");
|
|
|
|
darkWrapper.unmount();
|
|
});
|
|
|
|
it("should not display OpenObserve logo when custom_hide_self_logo is true", async () => {
|
|
const customWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
custom_logo_text: "Custom",
|
|
custom_hide_self_logo: true,
|
|
},
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await customWrapper.vm.$nextTick();
|
|
|
|
// Check that the OpenObserve logo is NOT in the HTML
|
|
const openobserveLogo = customWrapper.find('[data-test="header-openobserve-logo"]');
|
|
expect(openobserveLogo.exists()).toBe(false);
|
|
|
|
// Verify the custom logo text IS displayed
|
|
const html = customWrapper.html();
|
|
expect(html).toContain("Custom");
|
|
|
|
customWrapper.unmount();
|
|
});
|
|
|
|
it("should display OpenObserve logo when custom_hide_self_logo is false", async () => {
|
|
const customWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
custom_logo_text: "Custom",
|
|
custom_hide_self_logo: false,
|
|
},
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await customWrapper.vm.$nextTick();
|
|
|
|
// Check that the OpenObserve logo IS in the HTML
|
|
const openobserveLogo = customWrapper.find('[data-test="header-openobserve-logo"]');
|
|
expect(openobserveLogo.exists()).toBe(true);
|
|
|
|
// Verify the custom logo text is also displayed
|
|
const html = customWrapper.html();
|
|
expect(html).toContain("Custom");
|
|
|
|
customWrapper.unmount();
|
|
});
|
|
});
|
|
|
|
describe("Quota Warning", () => {
|
|
it("should display quota threshold message when present", async () => {
|
|
const quotaWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
organizationData: {
|
|
quotaThresholdMsg: "Quota exceeded",
|
|
},
|
|
},
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await quotaWrapper.vm.$nextTick();
|
|
|
|
// Verify the quota message is actually displayed in the DOM
|
|
const html = quotaWrapper.html();
|
|
expect(html).toContain("Quota exceeded");
|
|
|
|
// Also check that text is visible
|
|
const text = quotaWrapper.text();
|
|
expect(text).toContain("Quota exceeded");
|
|
|
|
quotaWrapper.unmount();
|
|
});
|
|
});
|
|
|
|
describe("Ingestion Quota Icon", () => {
|
|
it("should display ingestion quota icon when quota >= 85%", async () => {
|
|
const quotaWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ingestion_quota_used: 87.5,
|
|
},
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await quotaWrapper.vm.$nextTick();
|
|
|
|
// Verify the quota warning icon is displayed
|
|
const quotaWarning = quotaWrapper.find('[data-test="ingestion-quota-warning-icon"]');
|
|
expect(quotaWarning.exists()).toBe(true);
|
|
|
|
quotaWrapper.unmount();
|
|
});
|
|
|
|
it("should return warning token color when quota is 85-94%", () => {
|
|
const quotaWrapper = createWrapper({
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ingestion_quota_used: 90,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(quotaWrapper.vm.ingestionQuotaColor).toBe(chartColor("--color-status-warning-text"));
|
|
});
|
|
|
|
it("should return negative token color when quota is >= 95%", () => {
|
|
const quotaWrapper = createWrapper({
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ingestion_quota_used: 97,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(quotaWrapper.vm.ingestionQuotaColor).toBe(chartColor("--color-status-negative"));
|
|
});
|
|
|
|
it("should not display quota icon when quota < 85%", async () => {
|
|
const quotaWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ingestion_quota_used: 80,
|
|
},
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await quotaWrapper.vm.$nextTick();
|
|
|
|
// Verify the quota warning icon is NOT displayed
|
|
const quotaWarning = quotaWrapper.find('[data-test="ingestion-quota-warning-icon"]');
|
|
expect(quotaWarning.exists()).toBe(false);
|
|
|
|
quotaWrapper.unmount();
|
|
});
|
|
});
|
|
|
|
describe("AI Chat Button", () => {
|
|
it("should display AI chat button in enterprise mode with ai_enabled", async () => {
|
|
const aiWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ai_enabled: true,
|
|
},
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await aiWrapper.vm.$nextTick();
|
|
|
|
// Verify AI chat button is rendered
|
|
const aiButton = aiWrapper.find('[data-test="menu-link-ai-item"]');
|
|
expect(aiButton.exists()).toBe(true);
|
|
|
|
aiWrapper.unmount();
|
|
});
|
|
|
|
it("should not display AI button if not enterprise", async () => {
|
|
const aiWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ai_enabled: true,
|
|
},
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "false",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await aiWrapper.vm.$nextTick();
|
|
|
|
// Verify AI button is NOT rendered
|
|
const aiButton = aiWrapper.find('[data-test="menu-link-ai-item"]');
|
|
expect(aiButton.exists()).toBe(false);
|
|
|
|
aiWrapper.unmount();
|
|
});
|
|
|
|
it("should not display AI button if ai_enabled is false", async () => {
|
|
const aiWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ai_enabled: false,
|
|
},
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await aiWrapper.vm.$nextTick();
|
|
|
|
// Verify AI button is NOT rendered
|
|
const aiButton = aiWrapper.find('[data-test="menu-link-ai-item"]');
|
|
expect(aiButton.exists()).toBe(false);
|
|
|
|
aiWrapper.unmount();
|
|
});
|
|
|
|
it("should apply active class when AI chat is enabled", async () => {
|
|
const aiWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ai_enabled: true,
|
|
},
|
|
isAiChatEnabled: true,
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await aiWrapper.vm.$nextTick();
|
|
|
|
// Verify AI button has active class
|
|
const aiButton = aiWrapper.find('[data-test="menu-link-ai-item"]');
|
|
expect(aiButton.exists()).toBe(true);
|
|
expect(aiButton.classes()).toContain("ai-btn-active");
|
|
|
|
aiWrapper.unmount();
|
|
});
|
|
|
|
it("should not have active class when AI chat is disabled", async () => {
|
|
const aiWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ai_enabled: true,
|
|
},
|
|
isAiChatEnabled: false,
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await aiWrapper.vm.$nextTick();
|
|
|
|
// Verify AI button does NOT have active class
|
|
const aiButton = aiWrapper.find('[data-test="menu-link-ai-item"]');
|
|
expect(aiButton.exists()).toBe(true);
|
|
expect(aiButton.classes()).not.toContain("ai-btn-active");
|
|
|
|
aiWrapper.unmount();
|
|
});
|
|
|
|
it("should emit toggleAIChat when AI button is clicked", async () => {
|
|
const aiWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ai_enabled: true,
|
|
},
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await aiWrapper.vm.$nextTick();
|
|
|
|
// Click the AI button
|
|
const aiButton = aiWrapper.find('[data-test="menu-link-ai-item"]');
|
|
expect(aiButton.exists()).toBe(true);
|
|
|
|
await aiButton.trigger("click");
|
|
|
|
// Verify toggleAIChat event was emitted
|
|
expect(aiWrapper.emitted("toggleAIChat")).toBeTruthy();
|
|
expect(aiWrapper.emitted("toggleAIChat")).toHaveLength(1);
|
|
|
|
aiWrapper.unmount();
|
|
});
|
|
|
|
it("should emit update:isHovered true on mouse enter", async () => {
|
|
const aiWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ai_enabled: true,
|
|
},
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await aiWrapper.vm.$nextTick();
|
|
|
|
// Hover over the AI button
|
|
const aiButton = aiWrapper.find('[data-test="menu-link-ai-item"]');
|
|
expect(aiButton.exists()).toBe(true);
|
|
|
|
await aiButton.trigger("mouseenter");
|
|
|
|
// Verify update:isHovered event was emitted with true
|
|
expect(aiWrapper.emitted("update:isHovered")).toBeTruthy();
|
|
expect(aiWrapper.emitted("update:isHovered")[0]).toEqual([true]);
|
|
|
|
aiWrapper.unmount();
|
|
});
|
|
|
|
it("should emit update:isHovered false on mouse leave", async () => {
|
|
const aiWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ai_enabled: true,
|
|
},
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await aiWrapper.vm.$nextTick();
|
|
|
|
// Hover and then leave the AI button
|
|
const aiButton = aiWrapper.find('[data-test="menu-link-ai-item"]');
|
|
expect(aiButton.exists()).toBe(true);
|
|
|
|
await aiButton.trigger("mouseleave");
|
|
|
|
// Verify update:isHovered event was emitted with false
|
|
expect(aiWrapper.emitted("update:isHovered")).toBeTruthy();
|
|
expect(aiWrapper.emitted("update:isHovered")[0]).toEqual([false]);
|
|
|
|
aiWrapper.unmount();
|
|
});
|
|
|
|
it("should accept getBtnLogo prop for AI icon", () => {
|
|
const customLogoPath = "path/to/ai-logo.svg";
|
|
const aiWrapper = createWrapper({
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ai_enabled: true,
|
|
},
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
propsOverrides: {
|
|
getBtnLogo: customLogoPath,
|
|
},
|
|
});
|
|
|
|
// Verify the getBtnLogo prop is passed correctly
|
|
expect(aiWrapper.props("getBtnLogo")).toBe(customLogoPath);
|
|
});
|
|
});
|
|
|
|
describe("Organization Selector", () => {
|
|
it("should display selected organization", () => {
|
|
const orgButton = wrapper.find('[data-test="organization-select-button"]');
|
|
expect(orgButton.exists()).toBe(false); // Using stub, check implementation
|
|
});
|
|
|
|
it("should emit update:searchQuery when search input changes", async () => {
|
|
// After the OSelect migration, search/filter state is internal to the
|
|
// component. The test is reduced to verifying the manual emit pathway
|
|
// still works for backward-compat consumers that listen on the event.
|
|
await wrapper.vm.$emit("update:searchQuery", "test");
|
|
|
|
expect(wrapper.emitted("update:searchQuery")).toBeTruthy();
|
|
expect(wrapper.emitted("update:searchQuery")[0]).toEqual(["test"]);
|
|
});
|
|
|
|
it("should emit updateOrganization when organization is selected", async () => {
|
|
const newOrg = { identifier: "new-org", label: "New Organization" };
|
|
|
|
// handleOrgSelection takes the org object directly (matching the
|
|
// OTable row-click contract used by the org dropdown).
|
|
await wrapper.setProps({
|
|
organizations: [{ identifier: "test-org", label: "Test Organization" }, newOrg],
|
|
});
|
|
await wrapper.vm.handleOrgSelection(newOrg);
|
|
|
|
expect(wrapper.emitted("update:selectedOrg")).toBeTruthy();
|
|
expect(wrapper.emitted("update:selectedOrg")[0]).toEqual([newOrg]);
|
|
expect(wrapper.emitted("updateOrganization")).toBeTruthy();
|
|
});
|
|
|
|
it("should filter organizations based on search query", () => {
|
|
// The 'organizations' prop now drives the OSelect options directly;
|
|
// filtering happens inside the OSelect popup, not via props.
|
|
const orgs = [{ identifier: "test-org", label: "Test Organization" }];
|
|
|
|
wrapper = shallowMount(Header, {
|
|
global: { plugins: [i18n], provide: { store: mockStore } },
|
|
props: {
|
|
...wrapper.props(),
|
|
organizations: orgs,
|
|
},
|
|
});
|
|
|
|
expect(wrapper.props("organizations")).toHaveLength(1);
|
|
});
|
|
|
|
it("should display empty state when no organizations match search", () => {
|
|
wrapper = shallowMount(Header, {
|
|
global: { plugins: [i18n], provide: { store: mockStore } },
|
|
props: {
|
|
...wrapper.props(),
|
|
organizations: [],
|
|
},
|
|
});
|
|
|
|
expect(wrapper.props("organizations")).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("Navigation Methods", () => {
|
|
it("should emit goToHome when goToHome is called", () => {
|
|
wrapper.vm.goToHome();
|
|
|
|
expect(wrapper.emitted("goToHome")).toBeTruthy();
|
|
});
|
|
|
|
it("should emit toggleAIChat when toggleAIChat is called", () => {
|
|
wrapper.vm.toggleAIChat();
|
|
|
|
expect(wrapper.emitted("toggleAIChat")).toBeTruthy();
|
|
});
|
|
|
|
it("should emit openSlack when openSlack is called", () => {
|
|
wrapper.vm.openSlack();
|
|
|
|
expect(wrapper.emitted("openSlack")).toBeTruthy();
|
|
});
|
|
|
|
it("should emit navigateToOpenAPI when called", () => {
|
|
wrapper.vm.navigateToOpenAPI();
|
|
|
|
expect(wrapper.emitted("navigateToOpenAPI")).toBeTruthy();
|
|
// The method is called without parameters in the component
|
|
const emittedEvents = wrapper.emitted("navigateToOpenAPI");
|
|
expect(emittedEvents).toBeTruthy();
|
|
});
|
|
|
|
it("should emit navigateToDocs when navigateToDocs is called", () => {
|
|
wrapper.vm.navigateToDocs();
|
|
|
|
expect(wrapper.emitted("navigateToDocs")).toBeTruthy();
|
|
});
|
|
|
|
it("should emit changeLanguage with language object", () => {
|
|
const lang = { code: "fr", label: "Français" };
|
|
wrapper.vm.changeLanguage(lang);
|
|
|
|
expect(wrapper.emitted("changeLanguage")).toBeTruthy();
|
|
expect(wrapper.emitted("changeLanguage")[0]).toEqual([lang]);
|
|
});
|
|
|
|
it("should emit openPredefinedThemes when openPredefinedThemes is called", () => {
|
|
wrapper.vm.openPredefinedThemes();
|
|
|
|
expect(wrapper.emitted("openPredefinedThemes")).toBeTruthy();
|
|
});
|
|
|
|
it("should emit signout when signout is called", () => {
|
|
wrapper.vm.signout();
|
|
|
|
expect(wrapper.emitted("signout")).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("Help Menu", () => {
|
|
it("should navigate to settings when settings button is clicked", async () => {
|
|
const settingsBtn = wrapper.find('[data-test="menu-link-settings-item"]');
|
|
if (settingsBtn.exists()) {
|
|
await settingsBtn.trigger("click");
|
|
expect(mockRouter.push).toHaveBeenCalledWith({ name: "settings" });
|
|
}
|
|
});
|
|
|
|
it("should verify about route navigation properties", () => {
|
|
mockConfig.isEnterprise = "false";
|
|
|
|
wrapper = shallowMount(Header, {
|
|
global: { plugins: [i18n], provide: { store: mockStore } },
|
|
props: {
|
|
...wrapper.props(),
|
|
config: mockConfig,
|
|
},
|
|
});
|
|
|
|
// Verify that router and store are available for navigation
|
|
expect(wrapper.props("router")).toBeDefined();
|
|
expect(wrapper.props("store").state.selectedOrganization).toBeDefined();
|
|
expect(wrapper.props("store").state.selectedOrganization.identifier).toBe("test-org");
|
|
});
|
|
|
|
it("should not display OpenAPI link in cloud deployment", () => {
|
|
mockConfig.isCloud = "true";
|
|
|
|
wrapper = shallowMount(Header, {
|
|
global: { plugins: [i18n], provide: { store: mockStore } },
|
|
props: {
|
|
...wrapper.props(),
|
|
config: mockConfig,
|
|
},
|
|
});
|
|
|
|
expect(mockConfig.isCloud).toBe("true");
|
|
});
|
|
|
|
it("should hide OpenAPI link if in custom_hide_menus", () => {
|
|
mockStore.state.zoConfig.custom_hide_menus = "openapi,settings";
|
|
|
|
wrapper = shallowMount(Header, {
|
|
global: { plugins: [i18n], provide: { store: mockStore } },
|
|
props: {
|
|
...wrapper.props(),
|
|
store: mockStore,
|
|
},
|
|
});
|
|
|
|
expect(mockStore.state.zoConfig.custom_hide_menus).toContain("openapi");
|
|
});
|
|
});
|
|
|
|
describe("User Profile", () => {
|
|
it("should display user information", () => {
|
|
expect(wrapper.props("user").given_name).toBe("John");
|
|
expect(wrapper.props("user").family_name).toBe("Doe");
|
|
expect(wrapper.props("user").email).toBe("john@test.com");
|
|
});
|
|
|
|
it("should attach an OTooltip to the profile button", () => {
|
|
// (no class needed — OTooltip styles itself via Tailwind tokens).
|
|
// The test now verifies the OTooltip is co-located with the profile btn.
|
|
const tooltipWrapper = createWrapper({
|
|
mountType: "mount",
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
const profileBtn = tooltipWrapper.find('[data-test="header-my-account-profile-icon"]');
|
|
expect(profileBtn.exists()).toBe(true);
|
|
// OTooltip lives as a sibling/descendant of the button; verify present.
|
|
const tooltip = tooltipWrapper.findComponent({ name: "OTooltip" });
|
|
expect(tooltip.exists()).toBe(true);
|
|
|
|
tooltipWrapper.unmount();
|
|
});
|
|
|
|
it("should display language selection menu", () => {
|
|
expect(wrapper.props("langList")).toHaveLength(2);
|
|
expect(wrapper.props("langList")[0].code).toBe("en-us");
|
|
});
|
|
|
|
it("positions the language submenu with logical RTL-safe utilities", async () => {
|
|
const languageWrapper = createWrapper({
|
|
stubsOverrides: {
|
|
ODropdown: {
|
|
template: '<div><slot name="trigger" /><slot /></div>',
|
|
},
|
|
},
|
|
});
|
|
|
|
languageWrapper.vm.showLanguageSubmenu = true;
|
|
await languageWrapper.vm.$nextTick();
|
|
|
|
const submenu = languageWrapper.find('[data-test="language-dropdown-item"]');
|
|
expect(submenu.exists()).toBe(true);
|
|
expect(submenu.classes()).toContain("end-full");
|
|
expect(submenu.classes()).toContain("me-1");
|
|
expect(submenu.classes()).not.toContain("right-full");
|
|
expect(submenu.find("button").classes()).toContain("text-start");
|
|
|
|
languageWrapper.unmount();
|
|
});
|
|
|
|
it("should have access to current language from cookies", () => {
|
|
// Verify that getLanguage can be called (it's mocked to return "en-us")
|
|
const currentLang = cookies.getLanguage();
|
|
expect(currentLang).toBe("en-us");
|
|
|
|
// Verify the selected language prop matches the cookie language
|
|
expect(wrapper.props("selectedLanguage").code).toBe("en-us");
|
|
});
|
|
|
|
it("should emit changeLanguage event with correct language data", () => {
|
|
const newLang = { code: "fr", label: "Français" };
|
|
wrapper.vm.changeLanguage(newLang);
|
|
|
|
// Verify the event was emitted with the correct language object
|
|
expect(wrapper.emitted("changeLanguage")).toBeTruthy();
|
|
expect(wrapper.emitted("changeLanguage")[0]).toEqual([newLang]);
|
|
|
|
// Verify the emitted object has all required properties
|
|
const emittedLang = wrapper.emitted("changeLanguage")[0][0];
|
|
expect(emittedLang).toHaveProperty("code");
|
|
expect(emittedLang).toHaveProperty("label");
|
|
expect(emittedLang.code).toBe("fr");
|
|
expect(emittedLang.label).toBe("Français");
|
|
|
|
// Note: The actual cookie setting happens in MainLayout.vue
|
|
// Header.vue only emits the event with the language data
|
|
});
|
|
|
|
it("should emit changeLanguage for each language in langList", () => {
|
|
const languages = [
|
|
{ code: "en-us", label: "English" },
|
|
{ code: "fr", label: "Français" },
|
|
{ code: "de", label: "Deutsch" },
|
|
];
|
|
|
|
const langWrapper = createWrapper({
|
|
propsOverrides: {
|
|
langList: languages,
|
|
},
|
|
});
|
|
|
|
// Test changing to each language
|
|
languages.forEach((lang, index) => {
|
|
langWrapper.vm.changeLanguage(lang);
|
|
|
|
const emitted = langWrapper.emitted("changeLanguage");
|
|
expect(emitted).toBeTruthy();
|
|
expect(emitted[index]).toEqual([lang]);
|
|
});
|
|
});
|
|
|
|
it("should result in cookie being set after changing to French", () => {
|
|
// Clear previous mock calls
|
|
vi.clearAllMocks();
|
|
|
|
const frenchLang = { code: "fr", label: "Français" };
|
|
|
|
// User changes language in Header component
|
|
wrapper.vm.changeLanguage(frenchLang);
|
|
|
|
// Verify Header emitted the event with French language
|
|
expect(wrapper.emitted("changeLanguage")).toBeTruthy();
|
|
expect(wrapper.emitted("changeLanguage")[0][0].code).toBe("fr");
|
|
|
|
// Simulate what MainLayout does when it receives the event
|
|
// (This is the integration part - simulating parent component behavior)
|
|
const emittedLang = wrapper.emitted("changeLanguage")[0][0];
|
|
cookies.setLanguage(emittedLang.code);
|
|
|
|
// Verify the cookie was set with "fr"
|
|
expect(cookies.setLanguage).toHaveBeenCalledWith("fr");
|
|
expect(cookies.setLanguage).toHaveBeenCalledTimes(1);
|
|
|
|
// Now verify we can retrieve "fr" from cookies
|
|
// Update the mock to return "fr" after setting it
|
|
vi.mocked(cookies.getLanguage).mockReturnValueOnce("fr");
|
|
|
|
const retrievedLang = cookies.getLanguage();
|
|
expect(retrievedLang).toBe("fr");
|
|
});
|
|
|
|
it("should result in cookie being set after changing to German", () => {
|
|
// Clear previous mock calls
|
|
vi.clearAllMocks();
|
|
|
|
const germanLang = { code: "de", label: "Deutsch" };
|
|
|
|
// User changes language
|
|
wrapper.vm.changeLanguage(germanLang);
|
|
|
|
// Verify event emission
|
|
expect(wrapper.emitted("changeLanguage")).toBeTruthy();
|
|
const emittedLang = wrapper.emitted("changeLanguage")[0][0];
|
|
expect(emittedLang.code).toBe("de");
|
|
|
|
// Simulate MainLayout's response to the event
|
|
cookies.setLanguage(emittedLang.code);
|
|
|
|
// Verify cookie was set with "de"
|
|
expect(cookies.setLanguage).toHaveBeenCalledWith("de");
|
|
|
|
// Verify we can retrieve "de" from cookies
|
|
vi.mocked(cookies.getLanguage).mockReturnValueOnce("de");
|
|
expect(cookies.getLanguage()).toBe("de");
|
|
});
|
|
});
|
|
|
|
describe("Theme Management", () => {
|
|
it("should render with light theme logo", async () => {
|
|
const lightWrapper = createWrapper({
|
|
mountType: "mount",
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await lightWrapper.vm.$nextTick();
|
|
|
|
// Verify light theme logo is used
|
|
const logo = lightWrapper.find('[data-test="header-openobserve-logo"]');
|
|
expect(logo.exists()).toBe(true);
|
|
|
|
const logoSrc = logo.attributes("src");
|
|
expect(logoSrc).toContain("light");
|
|
|
|
lightWrapper.unmount();
|
|
});
|
|
|
|
it("should render with dark theme logo", async () => {
|
|
const darkWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
theme: "dark",
|
|
},
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await darkWrapper.vm.$nextTick();
|
|
|
|
// Verify dark theme logo is used
|
|
const logo = darkWrapper.find('[data-test="header-openobserve-logo"]');
|
|
expect(logo.exists()).toBe(true);
|
|
|
|
const logoSrc = logo.attributes("src");
|
|
expect(logoSrc).toContain("dark");
|
|
|
|
darkWrapper.unmount();
|
|
});
|
|
|
|
it("should emit openPredefinedThemes when theme management is clicked", () => {
|
|
wrapper.vm.openPredefinedThemes();
|
|
|
|
expect(wrapper.emitted("openPredefinedThemes")).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("Slack Integration", () => {
|
|
it("should emit openSlack when Slack button is clicked", () => {
|
|
wrapper.vm.openSlack();
|
|
|
|
expect(wrapper.emitted("openSlack")).toBeTruthy();
|
|
});
|
|
|
|
it("should display Slack icon", () => {
|
|
expect(wrapper.props("slackIcon")).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("Props Validation", () => {
|
|
it("should accept all required props", () => {
|
|
// Migration: filteredOrganizations/searchQuery/rowsPerPage merged into
|
|
// a single 'organizations' prop (OSelect owns search/pagination state).
|
|
expect(wrapper.props("store")).toBeDefined();
|
|
expect(wrapper.props("router")).toBeDefined();
|
|
expect(wrapper.props("config")).toBeDefined();
|
|
expect(wrapper.props("user")).toBeDefined();
|
|
expect(wrapper.props("zoBackendUrl")).toBe("http://localhost:5080");
|
|
expect(wrapper.props("langList")).toHaveLength(2);
|
|
expect(wrapper.props("selectedLanguage")).toBeDefined();
|
|
expect(wrapper.props("selectedOrg")).toBeDefined();
|
|
expect(wrapper.props("organizations")).toHaveLength(2);
|
|
expect(wrapper.props("isHovered")).toBe(false);
|
|
expect(wrapper.props("getBtnLogo")).toBe("mock-logo.svg");
|
|
});
|
|
});
|
|
|
|
describe("Edge Cases", () => {
|
|
it("should handle missing user name gracefully", () => {
|
|
const userWithoutName = {
|
|
email: "test@example.com",
|
|
};
|
|
|
|
wrapper = shallowMount(Header, {
|
|
global: { plugins: [i18n], provide: { store: mockStore } },
|
|
props: {
|
|
...wrapper.props(),
|
|
user: userWithoutName,
|
|
},
|
|
});
|
|
|
|
expect(wrapper.props("user").email).toBe("test@example.com");
|
|
});
|
|
|
|
it("should not show organization dropdown when organization list is empty", () => {
|
|
const wrapper = createWrapper({
|
|
mountType: "mount",
|
|
propsOverrides: {
|
|
organizations: [],
|
|
},
|
|
stubsOverrides: {
|
|
// the OSelect stub off so we can probe its options.
|
|
OSelect: false,
|
|
},
|
|
});
|
|
|
|
// Verify the organization selector doesn't show options when list is empty.
|
|
const orgSelect = wrapper.findComponent({ name: "OSelect" });
|
|
if (orgSelect.exists()) {
|
|
expect(orgSelect.props("options")).toHaveLength(0);
|
|
}
|
|
});
|
|
|
|
it("should render default OpenObserve logo when both custom logos are null", () => {
|
|
const wrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
custom_logo_img: null,
|
|
custom_logo_dark_img: null,
|
|
custom_logo_text: "",
|
|
},
|
|
},
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
const html = wrapper.html();
|
|
// Verify no custom logo images are rendered
|
|
expect(html).not.toContain("data:image; base64,");
|
|
|
|
// Verify default OpenObserve logo is rendered instead
|
|
expect(html).toContain("openobserve-logo");
|
|
const imgs = wrapper.findAll('[data-test="header-openobserve-logo"]');
|
|
expect(imgs.length).toBeGreaterThan(0);
|
|
expect(imgs[0].attributes("src")).toContain("openobserve");
|
|
});
|
|
|
|
it("should display red warning icon when quota is exactly 95%", () => {
|
|
const wrapper = createWrapper({
|
|
mountType: "shallow",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
ingestion_quota_used: 95.0,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Verify the computed property returns the negative token color
|
|
expect(wrapper.vm.ingestionQuotaColor).toBe(chartColor("--color-status-negative"));
|
|
|
|
// Verify the computed percentage
|
|
expect(wrapper.vm.ingestionQuotaPercentage).toBe(95.0);
|
|
});
|
|
|
|
it("should handle organization with very long name", () => {
|
|
const longName = "A".repeat(100);
|
|
const orgWithLongName = { identifier: "long", label: longName };
|
|
|
|
const wrapper = createWrapper({
|
|
mountType: "shallow",
|
|
propsOverrides: {
|
|
selectedOrg: orgWithLongName,
|
|
},
|
|
});
|
|
|
|
// Verify the component accepts and stores the long organization name
|
|
expect(wrapper.props("selectedOrg")).toEqual(orgWithLongName);
|
|
expect(wrapper.props("selectedOrg").label).toBe(longName);
|
|
expect(wrapper.props("selectedOrg").label.length).toBe(100);
|
|
});
|
|
});
|
|
|
|
describe("Conditional Rendering Logic", () => {
|
|
it("should show custom logo section only in enterprise mode with custom logo", async () => {
|
|
const customWrapper = createWrapper({
|
|
mountType: "mount",
|
|
storeOverrides: {
|
|
state: {
|
|
zoConfig: {
|
|
custom_logo_text: "Custom",
|
|
},
|
|
},
|
|
},
|
|
configOverrides: {
|
|
isEnterprise: "true",
|
|
},
|
|
stubsOverrides: {},
|
|
});
|
|
|
|
await customWrapper.vm.$nextTick();
|
|
|
|
// Verify custom logo text is displayed
|
|
const html = customWrapper.html();
|
|
expect(html).toContain("Custom");
|
|
|
|
customWrapper.unmount();
|
|
});
|
|
|
|
it("should apply active class to selected organization", () => {
|
|
const selectedOrg = { identifier: "test-org", label: "Test Organization" };
|
|
|
|
wrapper = shallowMount(Header, {
|
|
global: { plugins: [i18n], provide: { store: mockStore } },
|
|
props: {
|
|
...wrapper.props(),
|
|
userClickedOrg: selectedOrg,
|
|
selectedOrg: selectedOrg,
|
|
},
|
|
});
|
|
|
|
expect(wrapper.props("userClickedOrg").identifier).toBe(
|
|
wrapper.props("selectedOrg").identifier,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Integration Points", () => {
|
|
it("should use store for configuration", () => {
|
|
expect(wrapper.props("store")).toBeDefined();
|
|
expect(wrapper.props("store").state).toBeDefined();
|
|
expect(wrapper.props("store").state.zoConfig).toBeDefined();
|
|
});
|
|
|
|
it("should use router for navigation", () => {
|
|
wrapper.vm.$emit("goToHome");
|
|
|
|
expect(wrapper.emitted("goToHome")).toBeTruthy();
|
|
});
|
|
|
|
it("should use config for feature flags", () => {
|
|
expect(wrapper.props("config").isEnterprise).toBe("false");
|
|
expect(wrapper.props("config").isCloud).toBe("false");
|
|
});
|
|
});
|
|
});
|