// Copyright 2026 OpenObserve Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { mount, flushPromises } from "@vue/test-utils";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { ref } from "vue";
import TenstackTable from "./TenstackTable.vue";
// Mock CSS.supports which is not available in jsdom
Object.defineProperty(globalThis, "CSS", {
value: { supports: vi.fn(() => false) },
writable: true,
});
// ── Mock vue-i18n ──────────────────────────────────────────────────────────────
vi.mock("vue-i18n", () => ({
useI18n: () => ({ t: (key: string) => key }),
}));
// ── Mock Vuex ──────────────────────────────────────────────────────────────────
const mockStore = {
state: {
theme: "light",
zoConfig: {
timestamp_column: "_timestamp",
},
},
};
vi.mock("vuex", () => ({
useStore: () => mockStore,
}));
// ── Mock Quasar ────────────────────────────────────────────────────────────────
vi.mock("quasar", async () => {
const actual = await vi.importActual("quasar");
return {
...actual,
debounce: (fn: any) => fn,
};
});
// ── Mock @tanstack/vue-virtual ─────────────────────────────────────────────────
vi.mock("@tanstack/vue-virtual", () => ({
useVirtualizer: () =>
ref({
getVirtualItems: () => [],
getTotalSize: () => 0,
measureElement: vi.fn(),
}),
}));
// ── Mock @tanstack/vue-table ───────────────────────────────────────────────────
const mockTable = {
getCenterTotalSize: () => 800,
getTotalSize: () => 800,
getHeaderGroups: () => [{ id: "hg-0", headers: [] }],
getState: () => ({ columnOrder: [] }),
getRowModel: () => ({ rows: [] }),
getFlatHeaders: () => [],
setColumnOrder: vi.fn(),
resetColumnSizing: vi.fn(),
};
vi.mock("@tanstack/vue-table", () => ({
FlexRender: { template: "" },
useVueTable: () => mockTable,
getCoreRowModel: () => vi.fn(),
getSortedRowModel: () => vi.fn(),
}));
// ── Mock vue-draggable-next ────────────────────────────────────────────────────
vi.mock("vue-draggable-next", () => ({
VueDraggableNext: { template: "
" },
}));
// ── Mock composables ───────────────────────────────────────────────────────────
vi.mock("@/composables/useTextHighlighter", () => ({
useTextHighlighter: () => ({
isFTSColumn: vi.fn(() => false),
}),
}));
vi.mock("@/composables/useLogsHighlighter", () => ({
useLogsHighlighter: () => ({
processedResults: ref([]),
processHitsInChunks: vi.fn(),
}),
}));
// ── Mock utils ─────────────────────────────────────────────────────────────────
vi.mock("@/utils/logs/statusParser", () => ({
extractStatusFromLog: () => null,
}));
// ── Stubs ──────────────────────────────────────────────────────────────────────
const globalStubs = {
JsonPreview: { template: "" },
CellActions: { template: "" },
O2AIContextAddBtn: { template: "" },
FlexRender: { template: "" },
};
const defaultProps = {
rows: [],
columns: [],
};
const mountComponent = (propsOverrides = {}) =>
mount(TenstackTable, {
global: {
stubs: globalStubs,
},
props: {
...defaultProps,
...propsOverrides,
},
});
describe("TenstackTable", () => {
afterEach(() => {
vi.clearAllMocks();
});
// ── Component Initialization ─────────────────────────────────────────────────
describe("Component Initialization", () => {
it("mounts successfully with minimal props", () => {
const wrapper = mountComponent();
expect(wrapper.exists()).toBe(true);
});
it("initializes sorting as empty array", () => {
const wrapper = mountComponent();
expect(wrapper.vm.sorting).toEqual([]);
});
it("initializes columnOrder as empty array", () => {
const wrapper = mountComponent();
expect(wrapper.vm.columnOrder).toEqual([]);
});
it("initializes isFunctionErrorOpen as false", () => {
const wrapper = mountComponent();
expect(wrapper.vm.isFunctionErrorOpen).toBe(false);
});
it("initializes activeCellActionId as empty string", () => {
const wrapper = mountComponent();
expect(wrapper.vm.activeCellActionId).toBe("");
});
it("initializes expandedRowIndices as empty Set", () => {
const wrapper = mountComponent();
expect(wrapper.vm.expandedRowIndices.size).toBe(0);
});
it("initializes isResizingHeader as false", () => {
const wrapper = mountComponent();
expect(wrapper.vm.isResizingHeader).toBe(false);
});
});
// ── Props ────────────────────────────────────────────────────────────────────
describe("Props", () => {
it("accepts rows prop", () => {
const rows = [{ _timestamp: 1000, log: "test" }];
const wrapper = mountComponent({ rows });
expect(wrapper.props("rows")).toEqual(rows);
});
it("accepts columns prop", () => {
const columns = [{ id: "_timestamp", header: "Timestamp" }];
const wrapper = mountComponent({ columns });
expect(wrapper.props("columns")).toEqual(columns);
});
it("defaults wrap to false", () => {
const wrapper = mountComponent();
expect(wrapper.props("wrap")).toBe(false);
});
it("defaults loading to false", () => {
const wrapper = mountComponent();
expect(wrapper.props("loading")).toBe(false);
});
it("defaults defaultColumns to true", () => {
const wrapper = mountComponent();
expect(wrapper.props("defaultColumns")).toBe(true);
});
it("defaults errMsg to empty string", () => {
const wrapper = mountComponent();
expect(wrapper.props("errMsg")).toBe("");
});
it("defaults highlightTimestamp to -1", () => {
const wrapper = mountComponent();
expect(wrapper.props("highlightTimestamp")).toBe(-1);
});
it("defaults hideSearchTermActions to false", () => {
const wrapper = mountComponent();
expect(wrapper.props("hideSearchTermActions")).toBe(false);
});
it("defaults hideViewRelatedButton to false", () => {
const wrapper = mountComponent();
expect(wrapper.props("hideViewRelatedButton")).toBe(false);
});
it("accepts wrap=true prop", () => {
const wrapper = mountComponent({ wrap: true });
expect(wrapper.props("wrap")).toBe(true);
});
});
// ── Emits via copyLogToClipboard ─────────────────────────────────────────────
describe("copyLogToClipboard", () => {
it("emits 'copy' event with value and copyAsJson=true by default", () => {
const wrapper = mountComponent();
const logData = { _timestamp: 1000, message: "test log" };
wrapper.vm.copyLogToClipboard(logData);
const emitted = wrapper.emitted("copy");
expect(emitted).toBeTruthy();
expect(emitted![0]).toEqual([logData, true]);
});
it("emits 'copy' event with copyAsJson=false when specified", () => {
const wrapper = mountComponent();
const logData = { _timestamp: 1000 };
wrapper.vm.copyLogToClipboard(logData, false);
const emitted = wrapper.emitted("copy");
expect(emitted![0]).toEqual([logData, false]);
});
});
// ── Emits via addSearchTerm ──────────────────────────────────────────────────
describe("addSearchTerm", () => {
it("emits 'addSearchTerm' with field, value, action", () => {
const wrapper = mountComponent();
wrapper.vm.addSearchTerm("status", "200", "include");
const emitted = wrapper.emitted("addSearchTerm");
expect(emitted).toBeTruthy();
expect(emitted![0]).toEqual(["status", "200", "include"]);
});
it("emits 'addSearchTerm' with numeric value", () => {
const wrapper = mountComponent();
wrapper.vm.addSearchTerm("latency", 42, "exclude");
expect(wrapper.emitted("addSearchTerm")![0]).toEqual(["latency", 42, "exclude"]);
});
});
// ── Emits via addFieldToTable ─────────────────────────────────────────────────
describe("addFieldToTable", () => {
it("emits 'addFieldToTable' with field name", () => {
const wrapper = mountComponent();
wrapper.vm.addFieldToTable("kubernetes.pod");
const emitted = wrapper.emitted("addFieldToTable");
expect(emitted).toBeTruthy();
expect(emitted![0]).toEqual(["kubernetes.pod"]);
});
});
// ── Emits via closeColumn ─────────────────────────────────────────────────────
describe("closeColumn", () => {
it("emits 'closeColumn' with column data", () => {
const wrapper = mountComponent();
const colData = { id: "status", name: "Status" };
wrapper.vm.closeColumn(colData);
const emitted = wrapper.emitted("closeColumn");
expect(emitted).toBeTruthy();
expect(emitted![0]).toEqual([colData]);
});
});
// ── handleDragStart ───────────────────────────────────────────────────────────
describe("handleDragStart", () => {
it("sets isResizingHeader=true when dragging timestamp column", () => {
const wrapper = mountComponent();
wrapper.vm.columnOrder = ["_timestamp", "status", "log"];
wrapper.vm.handleDragStart({ oldIndex: 0 });
expect(wrapper.vm.isResizingHeader).toBe(true);
});
it("sets isResizingHeader=false when dragging non-timestamp column", () => {
const wrapper = mountComponent();
wrapper.vm.columnOrder = ["_timestamp", "status", "log"];
wrapper.vm.handleDragStart({ oldIndex: 1 });
expect(wrapper.vm.isResizingHeader).toBe(false);
});
});
// ── handleDragEnd ─────────────────────────────────────────────────────────────
describe("handleDragEnd", () => {
it("moves timestamp column back to front if it was reordered away", async () => {
const wrapper = mountComponent();
wrapper.vm.columnOrder = ["status", "log", "_timestamp"];
await wrapper.vm.handleDragEnd();
expect(wrapper.vm.columnOrder[0]).toBe("_timestamp");
});
it("does not modify order when timestamp is already first", async () => {
const wrapper = mountComponent();
wrapper.vm.columnOrder = ["_timestamp", "status", "log"];
await wrapper.vm.handleDragEnd();
expect(wrapper.vm.columnOrder[0]).toBe("_timestamp");
});
it("does nothing when timestamp column is not in order", async () => {
const wrapper = mountComponent();
wrapper.vm.columnOrder = ["status", "log"];
const originalOrder = [...wrapper.vm.columnOrder];
await wrapper.vm.handleDragEnd();
expect(wrapper.vm.columnOrder).toEqual(originalOrder);
});
});
// ── expandRow ────────────────────────────────────────────────────────────────
describe("expandRow", () => {
it("adds index to expandedRowIndices on expand", async () => {
const rows = [
{ _timestamp: 1, log: "first" },
{ _timestamp: 2, log: "second" },
];
const wrapper = mountComponent({ rows });
await wrapper.vm.expandRow(0);
expect(wrapper.vm.expandedRowIndices.has(0)).toBe(true);
});
it("removes index from expandedRowIndices on collapse", async () => {
const rows = [
{ _timestamp: 1, log: "first" },
{ _timestamp: 2, log: "second" },
];
const wrapper = mountComponent({ rows });
// Expand first
await wrapper.vm.expandRow(0);
expect(wrapper.vm.expandedRowIndices.has(0)).toBe(true);
// Collapse
await wrapper.vm.expandRow(0);
expect(wrapper.vm.expandedRowIndices.has(0)).toBe(false);
});
});
// ── setSorting ────────────────────────────────────────────────────────────────
describe("setSorting", () => {
it("updates sorting state via updater function", () => {
const wrapper = mountComponent();
const newSorting = [{ id: "_timestamp", desc: true }];
wrapper.vm.setSorting(() => newSorting);
expect(wrapper.vm.sorting).toEqual(newSorting);
});
it("clears sorting when updater returns empty array", () => {
const wrapper = mountComponent();
wrapper.vm.setSorting(() => [{ id: "status", desc: false }]);
wrapper.vm.setSorting(() => []);
expect(wrapper.vm.sorting).toEqual([]);
});
});
// ── selectedStreamFtsKeys computed ────────────────────────────────────────────
describe("selectedStreamFtsKeys computed", () => {
it("returns empty array when prop is not provided", () => {
const wrapper = mountComponent();
expect(wrapper.vm.selectedStreamFtsKeys).toEqual([]);
});
it("returns the prop value when provided", () => {
const ftsKeys = [
{ name: "message", isSchemaField: true },
{ name: "log", isSchemaField: false },
];
const wrapper = mountComponent({ selectedStreamFtsKeys: ftsKeys });
expect(wrapper.vm.selectedStreamFtsKeys).toEqual(ftsKeys);
});
});
// ── columnResizeMode ──────────────────────────────────────────────────────────
describe("columnResizeMode", () => {
it("is set to 'onChange'", () => {
const wrapper = mountComponent();
expect(wrapper.vm.columnResizeMode).toBe("onChange");
});
});
// ── isFunctionErrorOpen ───────────────────────────────────────────────────────
describe("isFunctionErrorOpen", () => {
it("can be toggled", async () => {
const wrapper = mountComponent();
expect(wrapper.vm.isFunctionErrorOpen).toBe(false);
wrapper.vm.isFunctionErrorOpen = true;
await wrapper.vm.$nextTick();
expect(wrapper.vm.isFunctionErrorOpen).toBe(true);
});
});
});