import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import {
PRODUCT_STATUS_KEYS,
PRODUCT_STATUSES,
resolveStatus,
StatusBadge,
StatusBadgeSkeleton,
} from "./status-badge";
import { StatusDot } from "./status-dot";
describe("status vocabulary", () => {
test("labels are sentence case and never raw enums", () => {
for (const key of PRODUCT_STATUS_KEYS) {
const { label } = PRODUCT_STATUSES[key];
expect(label).not.toContain("_");
expect(label[0]).toBe(label[0]!.toUpperCase());
// Every word after the first is lower case: "Needs reconnect", not "Needs Reconnect".
for (const word of label.split(" ").slice(1)) {
expect(word).toBe(word.toLowerCase());
}
}
});
test("one tone per meaning", () => {
expect(PRODUCT_STATUSES.connected.tone).toBe("success");
expect(PRODUCT_STATUSES.needs_reconnect.tone).toBe("attention");
expect(PRODUCT_STATUSES.running.tone).toBe("progress");
expect(PRODUCT_STATUSES.failed.tone).toBe("danger");
expect(PRODUCT_STATUSES.expired.tone).toBe("danger");
for (const key of ["paused", "revoked", "suspended", "unavailable", "off"] as const) {
expect(PRODUCT_STATUSES[key].tone).toBe("neutral");
}
});
test("an explicit tone wins, and a custom label defaults to neutral", () => {
expect(resolveStatus("paused", "attention").tone).toBe("attention");
expect(resolveStatus(undefined, undefined)).toEqual({
label: null,
tone: "neutral",
meta: null,
});
});
});
describe("StatusBadge", () => {
test("renders a dot and the label, never color alone", () => {
const html = renderToStaticMarkup();
expect(html).toContain("Needs reconnect");
expect(html).toContain('data-tone="attention"');
expect(html).toContain("bg-status-waiting");
expect(html).toContain('data-variant="outline"');
});
test("children override the label but keep the status tone", () => {
const html = renderToStaticMarkup(
Invited · expires in 5 days,
);
expect(html).toContain("Invited · expires in 5 days");
expect(html).toContain('data-tone="neutral"');
});
test("a reason makes the badge focusable and readable", () => {
const reason = "GitHub isn't available on this Opengeni server yet.";
const html = renderToStaticMarkup();
expect(html).toContain('tabindex="0"');
expect(html).toContain('class="sr-only"');
expect(html).toContain(reason.replace("'", "'"));
});
test("without a reason it is not a tab stop", () => {
expect(renderToStaticMarkup()).not.toContain("tabindex");
});
test("each look has its own chrome", () => {
expect(renderToStaticMarkup()).not.toContain(
"rounded-full border",
);
const tinted = renderToStaticMarkup();
expect(tinted).toContain("bg-danger/10");
expect(tinted).toContain("text-danger");
});
test("live statuses pulse, others don't", () => {
expect(renderToStaticMarkup()).toContain("animate-pulse");
expect(renderToStaticMarkup()).not.toContain("animate-pulse");
});
test("the skeleton announces loading", () => {
expect(renderToStaticMarkup()).toContain('aria-label="Loading status"');
});
});
describe("StatusDot", () => {
test("keeps lifecycle tones and adds semantic ones", () => {
expect(renderToStaticMarkup()).toContain("bg-status-idle");
expect(renderToStaticMarkup()).toContain("bg-danger");
expect(renderToStaticMarkup()).toContain("size-1.5");
expect(renderToStaticMarkup()).toContain("size-2");
});
});