Files
openobserve/web/src/components/alertLibrary/LibraryRail.spec.ts
T
Omkar Kesarkhaneandgithub-actions[bot] 305e0c2fc1 fix(alerts): faster alert library cards and a first-run path to the library (#16087)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-11-02 12:38:25 +00:00

304 lines
14 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 } from "vitest";
import { mount } from "@vue/test-utils";
import LibraryRail from "./LibraryRail.vue";
import type { LibraryCategoryFacet, LibraryFacet } from "./libraryFacets";
import i18n from "@/locales";
import { raw } from "@/types/i18n";
const categories: LibraryCategoryFacet[] = [
{ id: "kafka", label: raw("Kafka"), count: 12, pack: "message-brokers" },
{ id: "clickhouse", label: raw("Clickhouse"), count: 23, pack: "databases" },
// id-vs-label discriminator: "cert-m" is in the id, not in "Cert Manager".
{ id: "cert-manager", label: raw("Cert Manager"), count: 8, pack: "k8s" },
// The dead end.
{ id: "retired", label: raw("Retired"), count: 0, pack: "databases" },
];
const severities: LibraryFacet[] = [
{ id: "all", label: raw("All"), count: 86 },
{ id: "critical", label: raw("Critical"), count: 20 },
{ id: "warning", label: raw("Warning"), count: 60 },
{ id: "info", label: raw("Info"), count: 6 },
];
const mountRail = (props: Record<string, unknown> = {}) =>
mount(LibraryRail, {
props: {
categories,
selectedCategories: [],
severities,
severity: "all",
search: "",
...props,
},
global: { plugins: [i18n] },
});
type Rail = ReturnType<typeof mountRail>;
/** Rows are checkboxes; the click target is the box, not the label. */
const toggle = (wrapper: Rail, id: string) =>
wrapper
.find(`[data-test="alert-library-rail-category-${id}"]`)
.find('[role="checkbox"]')
.trigger("click");
// OInput sets inheritAttrs:false and re-derives child selectors, so the field
// itself is `<data-test>-field`.
const field = (wrapper: Rail) =>
wrapper.find<HTMLInputElement>('[data-test="alert-library-rail-search-categories-field"]');
/**
* The page owns the term (so its "Clear filters" can reset it), so typing only
* EMITS — a real parent feeds the value back down. These tests play that parent.
*/
const type = async (wrapper: Rail, term: string) => {
await field(wrapper).setValue(term);
await wrapper.setProps({ search: term });
};
describe("LibraryRail", () => {
it("lists one axis only — categories — with no pack switcher", () => {
// A pack is a coarse bucket; the category is what people come looking for.
// Two axes over one catalogue put the useful one behind a tab.
const wrapper = mountRail();
expect(wrapper.find('[data-test="alert-library-rail-axis"]').exists()).toBe(false);
expect(wrapper.text()).toContain("Category");
expect(wrapper.text()).not.toContain("Packs");
});
it("lists every category with its alert count", () => {
const wrapper = mountRail();
expect(wrapper.find('[data-test="alert-library-rail-category-kafka"]').text()).toContain(
"Kafka",
);
expect(wrapper.find('[data-test="alert-library-rail-count-kafka"]').text()).toBe("12");
});
// ── pack headings ────────────────────────────────────────────────────────
it("lists each technology under its pack heading, packs in label order", () => {
const wrapper = mountRail();
const headings = wrapper.findAll('[data-test^="alert-library-rail-pack-heading-"]');
expect(headings.map((heading) => heading.text())).toEqual([
"Databases",
"Kubernetes",
"Message brokers",
]);
const group = wrapper.find('[data-test="alert-library-rail-pack-message-brokers"]');
expect(group.find('[data-test="alert-library-rail-category-kafka"]').exists()).toBe(true);
expect(group.find('[data-test="alert-library-rail-category-clickhouse"]').exists()).toBe(false);
// The group is named by its heading for assistive tech.
expect(group.attributes("role")).toBe("group");
expect(group.attributes("aria-labelledby")).toBe(
"alert-library-rail-pack-heading-message-brokers",
);
});
it("makes the pack heading a label, not a select-all", async () => {
const wrapper = mountRail();
const heading = wrapper.find('[data-test="alert-library-rail-pack-heading-databases"]');
expect(heading.element.tagName).toBe("H3");
expect(heading.find("button, [role='checkbox']").exists()).toBe(false);
await heading.trigger("click");
expect(wrapper.emitted("update:selectedCategories")).toBeUndefined();
});
it("drops a pack heading once none of its technologies are listed", async () => {
const wrapper = mountRail();
await type(wrapper, "kafka");
expect(wrapper.find('[data-test="alert-library-rail-pack-heading-databases"]').exists()).toBe(
false,
);
expect(
wrapper.find('[data-test="alert-library-rail-pack-heading-message-brokers"]').exists(),
).toBe(true);
});
it("shows every technology of a pack whose name matches the search", async () => {
const wrapper = mountRail();
await type(wrapper, "message");
expect(wrapper.find('[data-test="alert-library-rail-category-kafka"]').exists()).toBe(true);
expect(wrapper.find('[data-test="alert-library-rail-category-clickhouse"]').exists()).toBe(
false,
);
});
it("still hides a dead-end row when its pack name matches", async () => {
const wrapper = mountRail();
await type(wrapper, "databases");
expect(wrapper.find('[data-test="alert-library-rail-category-clickhouse"]').exists()).toBe(
true,
);
expect(wrapper.find('[data-test="alert-library-rail-category-retired"]').exists()).toBe(false);
});
// ── severity ─────────────────────────────────────────────────────────────
it("puts severity above the category list, outside the scrolling area", () => {
// Below the list it collided with the rows and scrolled out of reach after
// a hundred categories. It must sit in the pinned block, not the scroller.
const wrapper = mountRail();
const severityGroup = wrapper.find('[data-test="alert-library-rail-severity"]').element;
const firstRow = wrapper.find('[data-test="alert-library-rail-category-kafka"]').element;
expect(severityGroup.compareDocumentPosition(firstRow)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
const scroller = wrapper.find(".overflow-y-auto").element;
expect(scroller.contains(firstRow)).toBe(true);
expect(scroller.contains(severityGroup)).toBe(false);
});
it("emits the severity the user picked", async () => {
const wrapper = mountRail();
await wrapper.find('[data-test="alert-library-rail-severity-critical"]').trigger("click");
expect(wrapper.emitted("update:severity")).toEqual([["critical"]]);
});
it("keeps the severity selected when its own chip is re-clicked", async () => {
// You widen it with the explicit All chip, never by accidentally clearing
// the one you just chose.
const wrapper = mountRail({ severity: "critical" });
await wrapper.find('[data-test="alert-library-rail-severity-critical"]').trigger("click");
expect(wrapper.emitted("update:severity")).toBeUndefined();
});
// ── selection ────────────────────────────────────────────────────────────
it("adds a category to the selection rather than replacing it", async () => {
const wrapper = mountRail({ selectedCategories: ["kafka"] });
await toggle(wrapper, "clickhouse");
expect(wrapper.emitted("update:selectedCategories")).toEqual([[["kafka", "clickhouse"]]]);
});
it("removes a category when its own box is unticked", async () => {
const wrapper = mountRail({ selectedCategories: ["kafka", "clickhouse"] });
await toggle(wrapper, "kafka");
expect(wrapper.emitted("update:selectedCategories")).toEqual([[["clickhouse"]]]);
});
it("shows the tick on exactly the selected rows", () => {
const wrapper = mountRail({ selectedCategories: ["clickhouse"] });
const checked = (id: string) =>
wrapper
.find(`[data-test="alert-library-rail-category-${id}"]`)
.find('[role="checkbox"]')
.attributes("aria-checked");
expect(checked("clickhouse")).toBe("true");
expect(checked("kafka")).toBe("false");
});
// ── search ───────────────────────────────────────────────────────────────
it("filters the listed rows as you type, case-insensitively", async () => {
const wrapper = mountRail();
await type(wrapper, "KAF");
expect(wrapper.find('[data-test="alert-library-rail-category-kafka"]').exists()).toBe(true);
expect(wrapper.find('[data-test="alert-library-rail-category-clickhouse"]').exists()).toBe(
false,
);
});
it("searches the label people read, not the id", async () => {
// "cert-manager" is the id; "Cert Manager" is what the row says.
const wrapper = mountRail();
await type(wrapper, "cert-m");
expect(wrapper.find('[data-test="alert-library-rail-category-cert-manager"]').exists()).toBe(
false,
);
});
it("filters rows without touching the selection", async () => {
// A search is a view over the list. Typing must never silently deselect
// the category whose alerts are on screen.
const wrapper = mountRail({ selectedCategories: ["clickhouse"] });
await type(wrapper, "kafka");
expect(wrapper.emitted("update:selectedCategories")).toBeUndefined();
});
it("says so when nothing matches, instead of showing a blank rail", async () => {
const wrapper = mountRail();
await type(wrapper, "zzzz");
expect(wrapper.find('[data-test="alert-library-rail-empty-categories"]').exists()).toBe(true);
});
it("hides a dead-end row, unless it is the one you have selected", async () => {
// A zero-count row filters to nothing, so offering it wastes a click — but
// hiding a SELECTED one would strand the user with no way to untick it.
const wrapper = mountRail();
expect(wrapper.find('[data-test="alert-library-rail-category-retired"]').exists()).toBe(false);
await wrapper.setProps({ selectedCategories: ["retired"] });
expect(wrapper.find('[data-test="alert-library-rail-category-retired"]').exists()).toBe(true);
});
// ── clearing ─────────────────────────────────────────────────────────────
it("counts the selection and clears it", async () => {
const wrapper = mountRail({ selectedCategories: ["kafka", "clickhouse"] });
expect(wrapper.find('[data-test="alert-library-rail-selected-categories"]').text()).toContain(
"2",
);
await wrapper.find('[data-test="alert-library-rail-clear-categories"]').trigger("click");
expect(wrapper.emitted("update:selectedCategories")).toEqual([[[]]]);
// Severity is a separate question and is left alone.
expect(wrapper.emitted("update:severity")).toBeUndefined();
});
it("disables clear when there is nothing to clear", () => {
const wrapper = mountRail();
expect(
wrapper.find('[data-test="alert-library-rail-clear-categories"]').attributes("disabled"),
).toBeDefined();
});
it("carries no availability facet — the rail navigates, the strip filters", () => {
// Needles read from the catalogue, not typed as literals: a copy change
// would otherwise leave this passing while testing nothing.
const ready = i18n.global.t("alert_library.statReady");
const needsData = i18n.global.t("alert_library.statNeedsData");
expect(ready).not.toContain("alert_library.");
expect(needsData).not.toContain("alert_library.");
const text = mountRail().text();
expect(text).not.toContain(ready);
expect(text).not.toContain(needsData);
});
it("never hides a row you have ticked, whatever you type", () => {
// The grid stays filtered to it, so hiding the row leaves a filter whose
// effect you can see and whose control you cannot reach.
const wrapper = mountRail({ selectedCategories: ["kafka"], search: "redis" });
expect(wrapper.find('[data-test="alert-library-rail-category-kafka"]').exists()).toBe(true);
expect(wrapper.find('[data-test="alert-library-rail-category-clickhouse"]').exists()).toBe(
false,
);
});
it("emits the term rather than holding it, so the page can clear it", async () => {
const wrapper = mountRail();
await field(wrapper).setValue("kafka");
expect(wrapper.emitted("update:search")).toEqual([["kafka"]]);
});
it("cuts a long category name with a cut-only tooltip instead of an always-on title", () => {
const label = mountRail().find(
'[data-test="alert-library-rail-category-kafka"] [data-test="o-truncated-text"]',
);
expect(label.text()).toBe("Kafka");
expect(label.attributes("title")).toBeUndefined();
expect(label.attributes("data-o-tooltip-trigger")).toBe("overflow");
});
});