mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 00:09:35 +00:00
Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
474 lines
22 KiB
TypeScript
474 lines
22 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 LibraryCard from "./LibraryCard.vue";
|
|
import i18n from "@/locales";
|
|
import type { AlertLibraryEntry } from "@/types/alertLibrary";
|
|
|
|
const entry: AlertLibraryEntry = {
|
|
id: "k8s/pod-oom-killed",
|
|
name: "pod-oom-killed",
|
|
pack: "k8s",
|
|
category: "pod",
|
|
title: "Pod OOM Killed",
|
|
severity: "critical",
|
|
description: "A container in the pod was terminated by the kernel OOM killer.",
|
|
stream: "kube_pod_container_status_last_terminated_reason",
|
|
stream_type: "metrics",
|
|
query_type: "promql",
|
|
required_streams: ["kube_pod_container_status_last_terminated_reason"],
|
|
path: "packs/k8s/alerts/pod/pod-oom-killed.json",
|
|
content_hash: "abc123",
|
|
};
|
|
|
|
const mountCard = (props: Record<string, unknown> = {}) =>
|
|
mount(LibraryCard, {
|
|
props: { entry, ready: true, ...props },
|
|
global: { plugins: [i18n] },
|
|
});
|
|
|
|
describe("LibraryCard", () => {
|
|
it("shows the title, the description and the stream it reads", () => {
|
|
const text = mountCard().text();
|
|
expect(text).toContain("Pod OOM Killed");
|
|
expect(text).toContain("terminated by the kernel OOM killer");
|
|
expect(text).toContain("kube_pod_container_status_last_terminated_reason");
|
|
});
|
|
|
|
it("labels the severity", () => {
|
|
expect(mountCard().find('[data-test="alert-library-card-severity"]').text()).toBe("Critical");
|
|
});
|
|
|
|
it("shows warning severity in the badge group's amber, matching its P3 install", () => {
|
|
const wrapper = mountCard({ entry: { ...entry, severity: "warning" } });
|
|
const badge = wrapper.find('[data-test="alert-library-card-severity"]');
|
|
expect(badge.text()).toBe("Warning");
|
|
expect(badge.html()).toContain("amber");
|
|
});
|
|
|
|
it("shows the query language as a quiet neutral chip", () => {
|
|
expect(mountCard().find('[data-test="alert-library-card-query-type"]').text()).toBe("PromQL");
|
|
});
|
|
|
|
it("carries NO ready label — it would describe almost every card and signal nothing", () => {
|
|
expect(mountCard().find('[data-test="alert-library-card-needs-data"]').exists()).toBe(false);
|
|
expect(mountCard().text()).not.toContain("Ready");
|
|
});
|
|
|
|
it("names the missing stream, so the card says WHY it differs", () => {
|
|
// The whole point of the treatment: a reader must not have to infer the
|
|
// cause from a border style. The stream that is absent is stated on the
|
|
// card, and the tooltip explains the consequence.
|
|
const wrapper = mountCard({ ready: false });
|
|
const note = wrapper.find('[data-test="alert-library-card-needs-data"]');
|
|
expect(note.exists()).toBe(true);
|
|
expect(note.text()).toBe("Not ingested");
|
|
expect(note.attributes("title")).toContain("kube_pod_container_status_last_terminated_reason");
|
|
expect(note.attributes("title")).toContain("would never fire");
|
|
});
|
|
|
|
it("marks the state in a tone that each theme can wear", () => {
|
|
// Warning and not error: an alert with no data is not broken, it is waiting.
|
|
//
|
|
// The chip uses warning-QUIET, whose tokens differ by theme, because 959 of
|
|
// 1242 alerts are not ingested on a typical org. Light wears the amber wash
|
|
// at a dozen per screen; dark, where the same ramp is full-chroma against
|
|
// near-black, turns the chip neutral and lets the icon carry the colour.
|
|
const wrapper = mountCard({ ready: false });
|
|
const note = wrapper.find('[data-test="alert-library-card-needs-data"]');
|
|
const mark = wrapper.find('[data-test="alert-library-card-needs-data-mark"]');
|
|
|
|
expect(note.classes().join(" ")).toMatch(/badge-warning-quiet/);
|
|
// Never the filled variant: that one is identical in both themes.
|
|
expect(note.classes().join(" ")).not.toMatch(/badge-warning-soft-bg/);
|
|
expect(note.html()).not.toMatch(/badge-error/);
|
|
// The icon stays amber in BOTH themes, so dark keeps a colour cue.
|
|
expect(mark.classes().join(" ")).toMatch(/badge-warning/);
|
|
});
|
|
|
|
it("shapes the note like the query-language chip beside it", () => {
|
|
// The user asked for the two to read as the same kind of thing — one chip
|
|
// says what language it speaks, the other says what it is missing — so the
|
|
// note must not drift into its own bespoke treatment.
|
|
const wrapper = mountCard({ ready: false });
|
|
const shapeOf = (test: string) =>
|
|
wrapper
|
|
.find(`[data-test="${test}"]`)
|
|
.classes()
|
|
.filter((c) => c.startsWith("rounded-") || c.startsWith("px-") || c.startsWith("text-3xs"))
|
|
.sort();
|
|
const shape = shapeOf("alert-library-card-needs-data");
|
|
expect(shape.length).toBeGreaterThan(0); // else the comparison is vacuous
|
|
expect(shape).toEqual(shapeOf("alert-library-card-query-type"));
|
|
});
|
|
|
|
it("drops the dashed border — it only meant anything next to a solid one", () => {
|
|
// The border was the original complaint: dashed vs solid is invisible until
|
|
// you happen to see both, so a pack where every stream is missing looked
|
|
// arbitrarily different from one where none were. The chip carries it now.
|
|
const dashed = (ready: boolean) =>
|
|
mountCard({ ready })
|
|
.find('[data-test="alert-library-card-k8s/pod-oom-killed"]')
|
|
.classes()
|
|
.join(" ");
|
|
expect(dashed(false)).not.toMatch(/border-dashed/);
|
|
expect(dashed(false)).toBe(dashed(true));
|
|
});
|
|
|
|
it("puts the note at the end of the stream line, beside the stream it names", () => {
|
|
const wrapper = mountCard({ ready: false });
|
|
const line = wrapper.find('[data-test="alert-library-card-stream"]');
|
|
expect(line.find('[data-test="alert-library-card-needs-data"]').exists()).toBe(true);
|
|
expect(line.text()).toContain("kube_pod_container_status_last_terminated_reason");
|
|
expect(line.element.lastElementChild?.getAttribute("data-test")).toBe(
|
|
"alert-library-card-needs-data",
|
|
);
|
|
});
|
|
|
|
it("shows the stream on its own line with a database icon, under the description", () => {
|
|
const wrapper = mountCard();
|
|
const line = wrapper.find('[data-test="alert-library-card-stream"]');
|
|
expect(line.exists()).toBe(true);
|
|
expect(line.text()).toBe("kube_pod_container_status_last_terminated_reason");
|
|
expect(line.findComponent({ name: "OIcon" }).props("name")).toBe("database");
|
|
});
|
|
|
|
it("keeps the stream name out of the chip row", () => {
|
|
const tag = mountCard().find('[data-test="alert-library-card-query-type"]').element;
|
|
expect(tag.parentElement?.textContent).not.toContain(
|
|
"kube_pod_container_status_last_terminated_reason",
|
|
);
|
|
});
|
|
|
|
it("renders no stream line for an entry without a stream", () => {
|
|
const wrapper = mountCard({ entry: { ...entry, stream: "" } });
|
|
expect(wrapper.find('[data-test="alert-library-card-stream"]').exists()).toBe(false);
|
|
});
|
|
|
|
it("still flags a not-ingested entry that has no stream", () => {
|
|
const wrapper = mountCard({ entry: { ...entry, stream: "" }, ready: false });
|
|
const tag = wrapper.find('[data-test="alert-library-card-needs-data"]');
|
|
expect(tag.exists()).toBe(true);
|
|
expect(tag.attributes("title")).toBeUndefined();
|
|
});
|
|
|
|
it("does not dim the card, so its description stays readable", () => {
|
|
// The old opacity-65 faded the description people need in order to judge
|
|
// the alert, and overstated the state — an unavailable alert can still be
|
|
// read, previewed and installed.
|
|
const root = mountCard({ ready: false }).find(
|
|
'[data-test="alert-library-card-k8s/pod-oom-killed"]',
|
|
);
|
|
expect(root.classes().join(" ")).not.toMatch(/opacity-/);
|
|
});
|
|
|
|
it("shows the stream as plain provenance when it is available", () => {
|
|
const wrapper = mountCard({ ready: true });
|
|
expect(wrapper.find('[data-test="alert-library-card-needs-data"]').exists()).toBe(false);
|
|
expect(wrapper.text()).toContain("kube_pod_container_status_last_terminated_reason");
|
|
});
|
|
|
|
it("keeps a stable data-test keyed on the library id", () => {
|
|
expect(mountCard().find('[data-test="alert-library-card-k8s/pod-oom-killed"]').exists()).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("opens the drawer when the card body is clicked", () => {
|
|
// Kept as a mouse convenience once the card stopped being a button itself:
|
|
// everything it offers is also reachable from a real control inside it.
|
|
const wrapper = mountCard();
|
|
wrapper.find('[data-test="alert-library-card-k8s/pod-oom-killed"]').trigger("click");
|
|
expect(wrapper.emitted("open")).toHaveLength(1);
|
|
});
|
|
|
|
it("opens the drawer from the title button — once, not twice", () => {
|
|
// The title sits inside the card's own click handler. Without stopping
|
|
// propagation a single click opens the drawer twice.
|
|
const wrapper = mountCard();
|
|
wrapper.find('[data-test="alert-library-card-title-k8s/pod-oom-killed"]').trigger("click");
|
|
expect(wrapper.emitted("open")).toHaveLength(1);
|
|
});
|
|
|
|
it("makes the TITLE the control, so a checkbox can live beside it", () => {
|
|
// The card used to be the affordance. A card with two actions cannot be:
|
|
// a control nested inside role="button" is invalid for keyboard and AT.
|
|
const wrapper = mountCard();
|
|
const root = wrapper.find('[data-test="alert-library-card-k8s/pod-oom-killed"]');
|
|
expect(root.attributes("role")).toBeUndefined();
|
|
expect(root.attributes("tabindex")).toBeUndefined();
|
|
expect(root.attributes("aria-label")).toBeUndefined();
|
|
|
|
const title = wrapper.find('[data-test="alert-library-card-title-k8s/pod-oom-killed"]');
|
|
expect(title.element.tagName).toBe("BUTTON");
|
|
expect(title.text()).toContain("Pod OOM Killed");
|
|
});
|
|
|
|
it("keeps the pointer cursor on the card, which is still clickable", () => {
|
|
const root = mountCard().find('[data-test="alert-library-card-k8s/pod-oom-killed"]');
|
|
expect(root.classes()).toContain("cursor-pointer");
|
|
});
|
|
|
|
it("moves the focus ring to the title, where focus can actually land", () => {
|
|
const wrapper = mountCard();
|
|
const ringed = (test: string) =>
|
|
wrapper
|
|
.find(`[data-test="${test}"]`)
|
|
.classes()
|
|
.some((c) => c.startsWith("focus-visible:ring"));
|
|
expect(ringed("alert-library-card-title-k8s/pod-oom-killed")).toBe(true);
|
|
expect(ringed("alert-library-card-k8s/pod-oom-killed")).toBe(false);
|
|
});
|
|
|
|
// ── selection ────────────────────────────────────────────────────────────
|
|
|
|
it("renders a selection checkbox, always — not on hover", () => {
|
|
// Hover-reveal leaves a touch user with no way to START a selection from a
|
|
// card, and a hidden control cannot take the focus that would reveal it.
|
|
// Both mechanisms §7.2a rejects are ruled out: display and opacity.
|
|
const wrapper = mountCard();
|
|
const box = wrapper.find('[data-test="alert-library-select-k8s/pod-oom-killed"]');
|
|
expect(box.exists()).toBe(true);
|
|
expect(box.attributes("hidden")).toBeUndefined();
|
|
// Read off the CARD, not the checkbox's own root: the reveal would live on
|
|
// a wrapper the checkbox knows nothing about.
|
|
const html = wrapper.html();
|
|
expect(html).not.toMatch(/opacity-0/);
|
|
expect(html).not.toMatch(/group-hover:/);
|
|
});
|
|
|
|
it("no longer opens the drawer from the card's own key handlers", () => {
|
|
// The root is not a control any more. A leftover handler there would put an
|
|
// action on a non-interactive element, which nothing in web/ lints for.
|
|
const wrapper = mountCard();
|
|
const root = wrapper.find('[data-test="alert-library-card-k8s/pod-oom-killed"]');
|
|
root.trigger("keydown.enter");
|
|
root.trigger("keydown.space");
|
|
expect(wrapper.emitted("open")).toBeUndefined();
|
|
});
|
|
|
|
it("opens from the title on Enter and Space, because it is a real button", () => {
|
|
// Native button semantics: asserted through the element, not a handler, so
|
|
// this stays true however the click is wired.
|
|
const title = mountCard().find('[data-test="alert-library-card-title-k8s/pod-oom-killed"]');
|
|
expect(title.element.tagName).toBe("BUTTON");
|
|
expect((title.element as HTMLButtonElement).type).toBe("button");
|
|
expect(title.attributes("disabled")).toBeUndefined();
|
|
});
|
|
|
|
it("puts the checkbox before the title, so tab order reads left to right", () => {
|
|
const wrapper = mountCard();
|
|
const stops = wrapper
|
|
.findAll('[data-test^="alert-library-card-title-"], [role="checkbox"]')
|
|
.map((node) => (node.attributes("role") === "checkbox" ? "checkbox" : "title"));
|
|
expect(stops).toEqual(["checkbox", "title"]);
|
|
});
|
|
|
|
it("names the checkbox, which has no visible label of its own", () => {
|
|
const wrapper = mountCard();
|
|
const box = wrapper.find('[data-test="alert-library-select-k8s/pod-oom-killed"]');
|
|
expect(box.find('[role="checkbox"]').attributes("aria-label")).toContain("Pod OOM Killed");
|
|
});
|
|
|
|
it("emits update:selected from the checkbox and does NOT open the drawer", () => {
|
|
const wrapper = mountCard();
|
|
wrapper
|
|
.find('[data-test="alert-library-select-k8s/pod-oom-killed"]')
|
|
.find('[role="checkbox"]')
|
|
.trigger("click");
|
|
expect(wrapper.emitted("update:selected")).toEqual([[true]]);
|
|
expect(wrapper.emitted("open")).toBeUndefined();
|
|
});
|
|
|
|
it("unticks from the checkbox when it is already selected", () => {
|
|
const wrapper = mountCard({ selected: true });
|
|
wrapper
|
|
.find('[data-test="alert-library-select-k8s/pod-oom-killed"]')
|
|
.find('[role="checkbox"]')
|
|
.trigger("click");
|
|
expect(wrapper.emitted("update:selected")).toEqual([[false]]);
|
|
});
|
|
|
|
it("marks a selected card, so the ring is not the only cue", () => {
|
|
const test = '[data-test="alert-library-card-k8s/pod-oom-killed"]';
|
|
expect(mountCard({ selected: true }).find(test).attributes("data-selected")).toBe("true");
|
|
expect(mountCard().find(test).attributes("data-selected")).toBe("false");
|
|
});
|
|
|
|
it("drops the default border rules when selected, rather than layering over them", () => {
|
|
// `hover:border-border-strong` outranks a plain `border-accent`, so leaving
|
|
// the resting rules in place makes a selected card lose its accent on hover.
|
|
const classes = (selected: boolean) =>
|
|
mountCard({ selected }).find('[data-test="alert-library-card-k8s/pod-oom-killed"]').classes();
|
|
|
|
expect(classes(true)).toContain("border-accent");
|
|
expect(classes(true)).not.toContain("border-border-default");
|
|
expect(classes(true)).not.toContain("hover:border-border-strong");
|
|
expect(classes(false)).toContain("border-border-default");
|
|
expect(classes(false)).toContain("hover:border-border-strong");
|
|
});
|
|
|
|
it("names the title button by its own text, so the heading is not renamed", () => {
|
|
// An aria-label on the only child of the <h3> becomes the HEADING's name,
|
|
// so screen-reader heading navigation reads "Open Pod OOM Killed".
|
|
const title = mountCard().find('[data-test="alert-library-card-title-k8s/pod-oom-killed"]');
|
|
expect(title.attributes("aria-label")).toBeUndefined();
|
|
expect(title.text()).toBe("Pod OOM Killed");
|
|
});
|
|
|
|
it("defaults to unselected, so the gallery owns the state", () => {
|
|
const box = mountCard().find('[data-test="alert-library-select-k8s/pod-oom-killed"]');
|
|
expect(box.find('[role="checkbox"]').attributes("aria-checked")).toBe("false");
|
|
});
|
|
|
|
it("survives an entry with no description", () => {
|
|
const wrapper = mountCard({ entry: { ...entry, description: "" } });
|
|
expect(wrapper.text()).toContain("Pod OOM Killed");
|
|
});
|
|
|
|
describe("anomaly alerts", () => {
|
|
const anomaly: AlertLibraryEntry = {
|
|
...entry,
|
|
id: "apm/traces_server_error_ratio_anomaly",
|
|
query_type: "sql",
|
|
alert_type: "anomaly_detection",
|
|
};
|
|
|
|
it("badges an anomaly alert", () => {
|
|
const badge = mountCard({ entry: anomaly }).find('[data-test="alert-library-card-anomaly"]');
|
|
expect(badge.exists()).toBe(true);
|
|
expect(badge.text()).toBe("Anomaly");
|
|
});
|
|
|
|
it("does not badge a scheduled alert, nor one from a manifest without alert_type", () => {
|
|
expect(mountCard().find('[data-test="alert-library-card-anomaly"]').exists()).toBe(false);
|
|
const scheduled = mountCard({ entry: { ...entry, alert_type: "scheduled" } });
|
|
expect(scheduled.find('[data-test="alert-library-card-anomaly"]').exists()).toBe(false);
|
|
});
|
|
|
|
it("disables the card, its checkbox and its title button when the license locks it", async () => {
|
|
const wrapper = mountCard({ entry: anomaly, locked: true, licenseLockedReason: "Locked" });
|
|
const card = wrapper.find(`[data-test="alert-library-card-${anomaly.id}"]`);
|
|
expect(card.attributes("aria-disabled")).toBe("true");
|
|
expect(card.classes()).toContain("cursor-not-allowed");
|
|
expect(
|
|
wrapper.find(`[data-test="alert-library-card-title-${anomaly.id}"]`).attributes("disabled"),
|
|
).toBeDefined();
|
|
await card.trigger("click");
|
|
expect(wrapper.emitted("open")).toBeUndefined();
|
|
});
|
|
|
|
it("shows the Enterprise badge only when locked", () => {
|
|
const locked = mountCard({ entry: anomaly, locked: true });
|
|
expect(locked.find('[data-test="alert-library-card-enterprise"]').text()).toBe("Enterprise");
|
|
const open = mountCard({ entry: anomaly });
|
|
expect(open.find('[data-test="alert-library-card-enterprise"]').exists()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("actions", () => {
|
|
const ADD_ID = `alert-library-card-add-${entry.id}`;
|
|
const CUSTOMIZE_ID = `alert-library-card-customize-${entry.id}`;
|
|
const ADD = `[data-test="${ADD_ID}"]`;
|
|
const CUSTOMIZE = `[data-test="${CUSTOMIZE_ID}"]`;
|
|
const anomaly: AlertLibraryEntry = {
|
|
...entry,
|
|
query_type: "sql",
|
|
alert_type: "anomaly_detection",
|
|
};
|
|
|
|
it("offers a short Add label and an icon-only Customize on a scheduled card", () => {
|
|
const wrapper = mountCard();
|
|
expect(wrapper.find(ADD).text()).toBe("Add");
|
|
const customize = wrapper.find(CUSTOMIZE);
|
|
expect(customize.text()).toBe("");
|
|
expect(customize.attributes("aria-label")).toBe("Customize in editor");
|
|
const button = wrapper
|
|
.findAllComponents({ name: "OButton" })
|
|
.find((b) => b.attributes("data-test") === CUSTOMIZE_ID);
|
|
expect(button?.props("variant")).toBe("outline");
|
|
expect(button?.props("size")).toBe("icon-sm");
|
|
expect(button?.props("iconLeft")).toBe("edit");
|
|
});
|
|
|
|
it("explains Customize in a tooltip and spells out Add on hover", () => {
|
|
const wrapper = mountCard();
|
|
expect(wrapper.find(ADD).attributes("title")).toBe("Add to alerts");
|
|
const tip = wrapper.find(CUSTOMIZE).findComponent({ name: "OTooltip" });
|
|
expect(tip.props("content")).toBe("Opens the alert editor prefilled");
|
|
});
|
|
|
|
it("puts Customize before Add, at the end of the chip row", () => {
|
|
const wrapper = mountCard();
|
|
const row = wrapper.find('[data-test="alert-library-card-query-type"]').element.parentElement;
|
|
const ids = Array.from(row?.querySelectorAll("[data-test]") ?? []).map((n) =>
|
|
n.getAttribute("data-test"),
|
|
);
|
|
expect(ids.indexOf(CUSTOMIZE_ID)).toBeLessThan(ids.indexOf(ADD_ID));
|
|
expect(ids.indexOf(CUSTOMIZE_ID)).toBeGreaterThan(-1);
|
|
});
|
|
|
|
it("offers Customize on a SQL card as well as a PromQL one", () => {
|
|
const wrapper = mountCard({ entry: { ...entry, query_type: "sql" } });
|
|
expect(wrapper.find(CUSTOMIZE).exists()).toBe(true);
|
|
});
|
|
|
|
it("emits install from Add without opening the card", async () => {
|
|
const wrapper = mountCard();
|
|
await wrapper.find(ADD).trigger("click");
|
|
expect(wrapper.emitted("install")).toHaveLength(1);
|
|
expect(wrapper.emitted("open")).toBeUndefined();
|
|
});
|
|
|
|
it("emits customize from Customize without opening the card", async () => {
|
|
const wrapper = mountCard();
|
|
await wrapper.find(CUSTOMIZE).trigger("click");
|
|
expect(wrapper.emitted("customize")).toHaveLength(1);
|
|
expect(wrapper.emitted("open")).toBeUndefined();
|
|
});
|
|
|
|
it("offers Add but not Customize on an anomaly card, which has no editor prefill", () => {
|
|
const wrapper = mountCard({ entry: anomaly });
|
|
expect(wrapper.find(ADD).exists()).toBe(true);
|
|
expect(wrapper.find(CUSTOMIZE).exists()).toBe(false);
|
|
});
|
|
|
|
it("ties both buttons to their own card's title, since every card repeats the labels", () => {
|
|
const wrapper = mountCard();
|
|
const title = wrapper.find(`[data-test="alert-library-card-title-${entry.id}"]`);
|
|
const titleId = title.attributes("id");
|
|
expect(titleId).toBeTruthy();
|
|
expect(title.text()).toBe("Pod OOM Killed");
|
|
expect(wrapper.find(ADD).attributes("aria-describedby")).toBe(titleId);
|
|
expect(wrapper.find(CUSTOMIZE).attributes("aria-describedby")).toBe(titleId);
|
|
|
|
const other = mountCard({ entry: { ...entry, id: "k8s/node-disk-pressure" } });
|
|
const otherTitle = other.find(
|
|
'[data-test="alert-library-card-title-k8s/node-disk-pressure"]',
|
|
);
|
|
expect(otherTitle.attributes("id")).not.toBe(titleId);
|
|
});
|
|
|
|
it("offers neither on a locked card, whose click is the upsell", () => {
|
|
const wrapper = mountCard({ entry: anomaly, locked: true });
|
|
expect(wrapper.find(ADD).exists()).toBe(false);
|
|
expect(wrapper.find(CUSTOMIZE).exists()).toBe(false);
|
|
});
|
|
});
|
|
});
|