Reusable dossier primitives (classification strips, brackets, readouts, provenance chips, notice states) plus server-rendered /operations overview, ledger drill-down, and AAR with permission-gated sections and an authorized reconciliation panel. Realtime refresh is enhancement only. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
184 lines
7.6 KiB
TypeScript
184 lines
7.6 KiB
TypeScript
import { createElement as h } from "react";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { cleanup, render, screen, within } from "@testing-library/react";
|
|
import {
|
|
ClassificationStrip,
|
|
CornerBrackets,
|
|
LedgerRow,
|
|
NoticeState,
|
|
PageShell,
|
|
ProvenanceChip,
|
|
StatusReadout,
|
|
StrategicSummary,
|
|
STATUS_TONE_CLASSES,
|
|
type StatusTone,
|
|
} from "@/components/frontend/operations";
|
|
|
|
// Vitest globals are off, so @testing-library auto-cleanup never registers;
|
|
// without this, rendered DOM accumulates across tests and role queries match
|
|
// stale elements.
|
|
afterEach(cleanup);
|
|
|
|
/**
|
|
* Component tests documenting every operations-primitive variant:
|
|
* classification strip, corner brackets, status readout tones, ledger row,
|
|
* provenance chip (plain + link), notice states (empty/loading/error),
|
|
* the strategic summary composition, and the page shell.
|
|
*
|
|
* The file is .ts (the plan QA command names this exact path), so elements are
|
|
* built with createElement instead of JSX syntax.
|
|
*
|
|
* Accessibility contract: semantic roles, aria-hidden decoration, keyboard
|
|
* focus rings on interactive chips, contrast-compliant status text, and
|
|
* reduced-motion variants on every transition.
|
|
*/
|
|
|
|
describe("ClassificationStrip", () => {
|
|
it("renders both classification labels", () => {
|
|
render(h(ClassificationStrip, { left: "Operations // Unit Command", right: "REF 001" }));
|
|
expect(screen.getByText("Operations // Unit Command")).toBeDefined();
|
|
expect(screen.getByText("REF 001")).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("CornerBrackets", () => {
|
|
it("renders four decorative corner marks with reduced-motion variants", () => {
|
|
const { container } = render(h(CornerBrackets));
|
|
const slot = container.querySelector('[data-slot="corner-brackets"]');
|
|
expect(slot?.getAttribute("aria-hidden")).toBe("true");
|
|
expect(slot?.querySelectorAll("span").length).toBe(4);
|
|
expect(slot?.innerHTML).toContain("motion-reduce:transition-none");
|
|
});
|
|
});
|
|
|
|
describe("StatusReadout", () => {
|
|
const tones: StatusTone[] = ["neutral", "info", "success", "warning", "danger"];
|
|
|
|
it.each(tones)("renders the %s tone with contrast-compliant status text", (tone) => {
|
|
const { container } = render(h(StatusReadout, { label: `${tone} status`, tone }));
|
|
const pill = screen.getByRole("status");
|
|
expect(pill.textContent).toBe(`${tone} status`);
|
|
expect(pill.getAttribute("data-tone")).toBe(tone);
|
|
// Squared corners (rounded-sm), token-driven contrast classes present.
|
|
expect(pill.className).toContain("rounded-sm");
|
|
for (const cls of STATUS_TONE_CLASSES[tone].split(" ")) {
|
|
expect(pill.className).toContain(cls);
|
|
}
|
|
expect(container.firstElementChild).toBe(pill);
|
|
});
|
|
});
|
|
|
|
describe("LedgerRow", () => {
|
|
it("renders permanent readout cells with mono values and hairline separators", () => {
|
|
const cells = [
|
|
{ label: "Msns", value: "3", mono: true },
|
|
{ label: "Duration", value: "4h 0m", mono: true },
|
|
{ label: "State", value: "Contested" },
|
|
] as const;
|
|
const { container } = render(h(LedgerRow, { cells }));
|
|
const row = container.querySelector('[data-slot="ledger-row"]') as HTMLElement;
|
|
expect(row.tagName).toBe("FOOTER");
|
|
expect(row.className).toContain("border-t");
|
|
expect(row.className).toContain("bg-black/60");
|
|
|
|
const rendered = Array.from(row.querySelectorAll("span")).map((v) => v.textContent);
|
|
expect(rendered).toContain("3");
|
|
expect(rendered).toContain("4h 0m");
|
|
expect(rendered).toContain("Contested");
|
|
|
|
const hairlines = row.querySelectorAll(".border-r");
|
|
expect(hairlines.length).toBe(cells.length);
|
|
});
|
|
|
|
it("renders nothing when empty", () => {
|
|
const { container } = render(h(LedgerRow, { cells: [] }));
|
|
const row = container.querySelector('[data-slot="ledger-row"]') as HTMLElement;
|
|
expect(row.className).toContain("hidden");
|
|
});
|
|
});
|
|
|
|
describe("ProvenanceChip", () => {
|
|
it("renders a plain marker without href", () => {
|
|
const { container } = render(h(ProvenanceChip, { label: "OP 42" }));
|
|
const chip = container.querySelector('[data-slot="provenance-chip"]') as HTMLElement;
|
|
expect(chip.tagName).toBe("SPAN");
|
|
expect(chip.textContent).toBe("OP 42");
|
|
expect(chip.className).toContain("font-mono");
|
|
});
|
|
|
|
it("renders a keyboard-focusable link with href", () => {
|
|
const { container } = render(h(ProvenanceChip, { label: "OP 42", href: "/logistics/structures/1" }));
|
|
const chip = container.querySelector('[data-slot="provenance-chip"]') as HTMLElement;
|
|
expect(chip.tagName).toBe("A");
|
|
expect(chip.getAttribute("href")).toBe("/logistics/structures/1");
|
|
expect(chip.className).toContain("focus-visible:ring-2");
|
|
});
|
|
});
|
|
|
|
describe("NoticeState", () => {
|
|
it("renders empty and loading variants as polite status with aria-busy on loading", () => {
|
|
const { container } = render(
|
|
h("div", null,
|
|
h(NoticeState, { variant: "empty", message: "No operations yet", hint: "Run an op to populate the ledger." }),
|
|
h(NoticeState, { variant: "loading", message: "Loading operation ledger" }),
|
|
),
|
|
);
|
|
const notices = container.querySelectorAll('[data-slot="notice-state"]');
|
|
expect(notices.length).toBe(2);
|
|
const empty = notices[0] as HTMLElement;
|
|
const loading = notices[1] as HTMLElement;
|
|
expect(empty.getAttribute("data-variant")).toBe("empty");
|
|
expect(empty.getAttribute("role")).toBe("status");
|
|
expect(empty.className).not.toContain("rose");
|
|
expect(empty.textContent).toContain("No operations yet");
|
|
expect(loading.getAttribute("data-variant")).toBe("loading");
|
|
expect(loading.getAttribute("role")).toBe("status");
|
|
expect(loading.getAttribute("aria-busy")).toBe("true");
|
|
});
|
|
|
|
it("renders the error variant as an assertive alert", () => {
|
|
render(h(NoticeState, { variant: "error", message: "Failed effect", hint: "Replay from reconciliation." }));
|
|
const alert = screen.getByRole("alert");
|
|
expect(alert.getAttribute("data-variant")).toBe("error");
|
|
expect(alert.className).toContain("border-rose-500/30");
|
|
});
|
|
});
|
|
|
|
describe("StrategicSummary", () => {
|
|
it("composes strip, brackets, grid texture, content, and readout footer", () => {
|
|
const { container } = render(
|
|
h(StrategicSummary,
|
|
{
|
|
classificationLeft: "Official // Unit Command",
|
|
classificationRight: "REF 007",
|
|
readoutCells: [{ label: "Msns", value: "2", mono: true }],
|
|
children: h("p", null, "Summary body"),
|
|
},
|
|
),
|
|
);
|
|
const surface = container.querySelector('[data-slot="strategic-summary"]') as HTMLElement;
|
|
expect(surface.className).toContain("rounded-none");
|
|
|
|
within(surface).getByText("Official // Unit Command");
|
|
within(surface).getByText("REF 007");
|
|
expect(surface.querySelector('[data-slot="corner-brackets"]')).not.toBeNull();
|
|
expect(surface.querySelector('[data-slot="ledger-row"]')).not.toBeNull();
|
|
expect(surface.textContent).toContain("Summary body");
|
|
|
|
const texture = surface.querySelector('div[style*="linear-gradient"]') as HTMLElement;
|
|
expect(texture?.getAttribute("aria-hidden")).toBe("true");
|
|
expect(texture?.style.backgroundSize).toBe("28px 28px");
|
|
});
|
|
});
|
|
|
|
describe("PageShell", () => {
|
|
it("renders the title, meta strip, and content responsively", () => {
|
|
render(
|
|
h(PageShell, { title: "Operation Ledger", meta: "Command // Weekly", children: h("p", null, "Shell content") }),
|
|
);
|
|
const heading = screen.getByRole("heading", { level: 1 });
|
|
expect(heading.textContent).toContain("Operation Ledger");
|
|
expect(screen.getByText("Command // Weekly")).toBeDefined();
|
|
expect(screen.getByText("Shell content")).toBeDefined();
|
|
});
|
|
});
|