mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 00:09:22 +00:00
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
574 lines
18 KiB
TypeScript
574 lines
18 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, afterEach } from "vitest";
|
|
import { nextTick } from "vue";
|
|
import { useStickyColumns } from "./useStickyColumns";
|
|
import { chartColor } from "@/utils/chartTheme";
|
|
|
|
describe("useStickyColumns", () => {
|
|
let mockProps: any;
|
|
let mockStore: any;
|
|
let composable: any;
|
|
|
|
beforeEach(() => {
|
|
// Clean up any existing style elements from previous tests
|
|
document.querySelectorAll('style[data-sticky-styles="true"]').forEach((el) => {
|
|
el.parentNode?.removeChild(el);
|
|
});
|
|
|
|
mockProps = {
|
|
columns: [],
|
|
};
|
|
|
|
mockStore = {
|
|
state: {
|
|
theme: "light",
|
|
},
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up style elements after each test
|
|
document.querySelectorAll('style[data-sticky-styles="true"]').forEach((el) => {
|
|
el.parentNode?.removeChild(el);
|
|
});
|
|
});
|
|
|
|
describe("Initialization", () => {
|
|
it("should initialize with empty sticky column offsets", () => {
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({});
|
|
});
|
|
|
|
it("should return getStickyColumnStyle function", () => {
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
expect(typeof composable.getStickyColumnStyle).toBe("function");
|
|
});
|
|
|
|
it("should return stickyColumnOffsets ref", () => {
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
expect(composable.stickyColumnOffsets).toBeDefined();
|
|
expect(typeof composable.stickyColumnOffsets.value).toBe("object");
|
|
});
|
|
});
|
|
|
|
describe("Column Offset Calculation", () => {
|
|
it("should calculate offsets for sticky columns", async () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: 100 },
|
|
{ name: "col2", sticky: true, width: 150 },
|
|
{ name: "col3", sticky: false },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
await nextTick();
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col2: 100,
|
|
});
|
|
});
|
|
|
|
it("should use default width of 100 when width is not specified", async () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true },
|
|
{ name: "col2", sticky: true },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
await nextTick();
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col2: 100,
|
|
});
|
|
});
|
|
|
|
it("should parse string width values", async () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: "120" },
|
|
{ name: "col2", sticky: true, width: "180" },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
await nextTick();
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col2: 120,
|
|
});
|
|
});
|
|
|
|
it("should calculate cumulative widths correctly", async () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: 100 },
|
|
{ name: "col2", sticky: true, width: 200 },
|
|
{ name: "col3", sticky: true, width: 150 },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
await nextTick();
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col2: 100,
|
|
col3: 300,
|
|
});
|
|
});
|
|
|
|
it("should only include sticky columns in offsets", async () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: 100 },
|
|
{ name: "col2", sticky: false, width: 150 },
|
|
{ name: "col3", sticky: true, width: 200 },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
await nextTick();
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col3: 100,
|
|
});
|
|
});
|
|
|
|
it("should add __colIndex to each column", async () => {
|
|
mockProps.columns = [{ name: "col1" }, { name: "col2" }, { name: "col3" }];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
await nextTick();
|
|
|
|
expect(mockProps.columns[0].__colIndex).toBe(0);
|
|
expect(mockProps.columns[1].__colIndex).toBe(1);
|
|
expect(mockProps.columns[2].__colIndex).toBe(2);
|
|
});
|
|
|
|
it("should handle empty columns array", async () => {
|
|
mockProps.columns = [];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
await nextTick();
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({});
|
|
});
|
|
|
|
it("should handle undefined columns", async () => {
|
|
mockProps.columns = undefined;
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
await nextTick();
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe("getStickyColumnStyle", () => {
|
|
it("should return empty object for non-sticky columns", () => {
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
const style = composable.getStickyColumnStyle({ sticky: false });
|
|
|
|
expect(style).toEqual({});
|
|
});
|
|
|
|
it("should return empty object for undefined column", () => {
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
const style = composable.getStickyColumnStyle(undefined);
|
|
|
|
expect(style).toEqual({});
|
|
});
|
|
|
|
it("should return empty object for null column", () => {
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
const style = composable.getStickyColumnStyle(null);
|
|
|
|
expect(style).toEqual({});
|
|
});
|
|
|
|
it("should return style object for sticky columns", async () => {
|
|
mockProps.columns = [{ name: "col1", sticky: true, width: 100 }];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
await nextTick();
|
|
|
|
const style = composable.getStickyColumnStyle({ sticky: true, name: "col1" });
|
|
|
|
expect(style).toEqual({
|
|
position: "sticky",
|
|
left: "0px",
|
|
"z-index": 2,
|
|
"background-color": chartColor("--color-surface-base"),
|
|
"box-shadow": "4px 0 8px rgba(0, 0, 0, 0.15)",
|
|
});
|
|
});
|
|
|
|
it("should use correct left offset from precomputed offsets", async () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: 100 },
|
|
{ name: "col2", sticky: true, width: 150 },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
await nextTick();
|
|
|
|
const style1 = composable.getStickyColumnStyle({ sticky: true, name: "col1" });
|
|
const style2 = composable.getStickyColumnStyle({ sticky: true, name: "col2" });
|
|
|
|
expect(style1.left).toBe("0px");
|
|
expect(style2.left).toBe("100px");
|
|
});
|
|
|
|
it("should default to 0 offset for unknown column names", () => {
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
const style = composable.getStickyColumnStyle({
|
|
sticky: true,
|
|
name: "unknown",
|
|
});
|
|
|
|
expect(style.left).toBe("0px");
|
|
});
|
|
|
|
it("should apply the surface-base token color in dark theme", () => {
|
|
mockStore.state.theme = "dark";
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
const style = composable.getStickyColumnStyle({ sticky: true, name: "col1" });
|
|
|
|
// Background is now the resolved --color-surface-base token (theme-independent in jsdom)
|
|
expect(style["background-color"]).toBe(chartColor("--color-surface-base"));
|
|
});
|
|
|
|
it("should apply the surface-base token color in light theme", () => {
|
|
mockStore.state.theme = "light";
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
const style = composable.getStickyColumnStyle({ sticky: true, name: "col1" });
|
|
|
|
expect(style["background-color"]).toBe(chartColor("--color-surface-base"));
|
|
});
|
|
});
|
|
|
|
describe("Style Generation Logic", () => {
|
|
it("should calculate offsets for style generation", () => {
|
|
mockProps.columns = [{ name: "col1", sticky: true, width: 100 }];
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
// Offsets are calculated which would be used for style generation
|
|
expect(composable.stickyColumnOffsets.value).toEqual({ col1: 0 });
|
|
});
|
|
|
|
it("should track multiple column offsets for styling", () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: 100 },
|
|
{ name: "col2", sticky: true, width: 150 },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
// Both offsets calculated for style application
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col2: 100,
|
|
});
|
|
});
|
|
|
|
it("should add column index for CSS targeting", () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true },
|
|
{ name: "col2", sticky: false },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
// __colIndex added for nth-child CSS selectors
|
|
expect(mockProps.columns[0].__colIndex).toBe(0);
|
|
expect(mockProps.columns[1].__colIndex).toBe(1);
|
|
});
|
|
|
|
it("should determine theme color for styles", () => {
|
|
mockStore.state.theme = "dark";
|
|
mockProps.columns = [{ name: "col1", sticky: true }];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
const style = composable.getStickyColumnStyle({ sticky: true, name: "col1" });
|
|
|
|
// Background color is driven by the --color-surface-base token
|
|
expect(style["background-color"]).toBe(chartColor("--color-surface-base"));
|
|
});
|
|
|
|
it("should skip offset calculation for non-sticky columns", () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: 100 },
|
|
{ name: "col2", sticky: false, width: 150 },
|
|
{ name: "col3", sticky: true, width: 200 },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
// col2 not included in offsets
|
|
expect(composable.stickyColumnOffsets.value).not.toHaveProperty("col2");
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col3: 100, // Only includes width of col1 (sticky), skips col2
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Dynamic Updates", () => {
|
|
it("should initialize offsets based on initial columns", () => {
|
|
mockProps.columns = [{ name: "col1", sticky: true, width: 100 }];
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
// Immediate watcher should calculate offsets on init
|
|
expect(composable.stickyColumnOffsets.value).toEqual({ col1: 0 });
|
|
});
|
|
|
|
it("should recalculate offsets with different initial columns", () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: 100 },
|
|
{ name: "col2", sticky: true, width: 150 },
|
|
];
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col2: 100,
|
|
});
|
|
});
|
|
|
|
it("should calculate all column indexes immediately", () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: 100 },
|
|
{ name: "col2", sticky: true, width: 200 },
|
|
{ name: "col3", sticky: false },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
// All columns get __colIndex regardless of sticky status
|
|
expect(mockProps.columns[0].__colIndex).toBe(0);
|
|
expect(mockProps.columns[1].__colIndex).toBe(1);
|
|
expect(mockProps.columns[2].__colIndex).toBe(2);
|
|
});
|
|
|
|
it("should return consistent offsets reference", () => {
|
|
mockProps.columns = [{ name: "col1", sticky: true }];
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
const ref1 = composable.stickyColumnOffsets;
|
|
const ref2 = composable.stickyColumnOffsets;
|
|
|
|
// Should be the same ref object
|
|
expect(ref1).toBe(ref2);
|
|
});
|
|
});
|
|
|
|
describe("Return Values", () => {
|
|
it("should return stickyColumnOffsets ref", () => {
|
|
mockProps.columns = [{ name: "col1", sticky: true }];
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
expect(composable).toHaveProperty("stickyColumnOffsets");
|
|
expect(composable.stickyColumnOffsets.value).toBeDefined();
|
|
});
|
|
|
|
it("should return getStickyColumnStyle function", () => {
|
|
mockProps.columns = [];
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
expect(composable).toHaveProperty("getStickyColumnStyle");
|
|
expect(typeof composable.getStickyColumnStyle).toBe("function");
|
|
});
|
|
|
|
it("should only return expected properties", () => {
|
|
mockProps.columns = [];
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
const keys = Object.keys(composable);
|
|
expect(keys).toHaveLength(3);
|
|
expect(keys).toContain("stickyColumnOffsets");
|
|
expect(keys).toContain("getStickyColumnStyle");
|
|
expect(keys).toContain("tableId");
|
|
});
|
|
});
|
|
|
|
describe("Edge Cases", () => {
|
|
it("should handle columns with zero width", () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: 0 },
|
|
{ name: "col2", sticky: true, width: 150 },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col2: 100, // Uses default width of 100 when width is 0
|
|
});
|
|
});
|
|
|
|
it("should handle very large column widths", () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: 10000 },
|
|
{ name: "col2", sticky: true, width: 20000 },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col2: 10000,
|
|
});
|
|
});
|
|
|
|
it("should handle columns with no name property", () => {
|
|
mockProps.columns = [{ sticky: true, width: 100 }];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
// Should not throw error
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
undefined: 0,
|
|
});
|
|
});
|
|
|
|
it("should handle mixed numeric and string widths", () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: 100 },
|
|
{ name: "col2", sticky: true, width: "150" },
|
|
{ name: "col3", sticky: true, width: 200 },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col2: 100,
|
|
col3: 250,
|
|
});
|
|
});
|
|
|
|
it("should handle floating point widths", () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: 100.5 },
|
|
{ name: "col2", sticky: true, width: "150.7" },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
// parseInt truncates decimal points
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col2: 100, // parseInt(100.5) = 100
|
|
});
|
|
});
|
|
|
|
it("should handle negative widths", () => {
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: -50 },
|
|
{ name: "col2", sticky: true, width: 100 },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col2: -50, // Cumulative calculation includes negative
|
|
});
|
|
});
|
|
|
|
it("should handle many sticky columns", () => {
|
|
mockProps.columns = Array.from({ length: 20 }, (_, i) => ({
|
|
name: `col${i}`,
|
|
sticky: true,
|
|
width: 100,
|
|
}));
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
// Check first, middle, and last
|
|
expect(composable.stickyColumnOffsets.value.col0).toBe(0);
|
|
expect(composable.stickyColumnOffsets.value.col10).toBe(1000);
|
|
expect(composable.stickyColumnOffsets.value.col19).toBe(1900);
|
|
});
|
|
});
|
|
|
|
describe("Reactivity", () => {
|
|
it("should calculate offsets on initialization", () => {
|
|
mockProps.columns = [{ name: "col1", sticky: true, width: 100 }];
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
// The watch runs immediately, so offsets should be calculated
|
|
expect(composable.stickyColumnOffsets.value).toEqual({ col1: 0 });
|
|
});
|
|
|
|
it("should handle columns being added", async () => {
|
|
mockProps.columns = [{ name: "col1", sticky: true, width: 100 }];
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
expect(composable.stickyColumnOffsets.value).toEqual({ col1: 0 });
|
|
|
|
// Add more columns by pushing (modifies array in place)
|
|
mockProps.columns.push({ name: "col2", sticky: true, width: 150 });
|
|
await nextTick();
|
|
await nextTick(); // Sometimes need extra tick for deep watch
|
|
|
|
// Note: The exact behavior depends on whether the watch triggers
|
|
// At minimum we should not have errors
|
|
expect(composable.stickyColumnOffsets.value).toBeDefined();
|
|
});
|
|
|
|
it("should return the token-driven color regardless of theme", () => {
|
|
mockProps.columns = [{ name: "col1", sticky: true }];
|
|
mockStore.state.theme = "light";
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
const style1 = composable.getStickyColumnStyle({ sticky: true, name: "col1" });
|
|
expect(style1["background-color"]).toBe(chartColor("--color-surface-base"));
|
|
|
|
// The color now comes from the --color-surface-base token, which resolves the
|
|
// same in jsdom for both themes (dark handled by CSS, not JS).
|
|
mockStore.state.theme = "dark";
|
|
|
|
const style2 = composable.getStickyColumnStyle({ sticky: true, name: "col1" });
|
|
expect(style2["background-color"]).toBe(chartColor("--color-surface-base"));
|
|
});
|
|
|
|
it("should handle immediate watcher execution", () => {
|
|
// The watch has immediate: true, so it runs on setup
|
|
mockProps.columns = [
|
|
{ name: "col1", sticky: true, width: 100 },
|
|
{ name: "col2", sticky: true, width: 150 },
|
|
];
|
|
|
|
composable = useStickyColumns(mockProps, mockStore);
|
|
|
|
// Offsets should be calculated immediately
|
|
expect(composable.stickyColumnOffsets.value).toEqual({
|
|
col1: 0,
|
|
col2: 100,
|
|
});
|
|
});
|
|
});
|
|
});
|