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"); }); });