// @vitest-environment jsdom import { i18n } from "@lingui/core"; import { I18nProvider } from "@lingui/react"; import type { ThreadMessage } from "@rakazo/contracts"; import { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ComposerTaskPill, extractActivePlanState, PlanCard } from "./message-cards"; // @ts-expect-error React act environment flag globalThis.IS_REACT_ACT_ENVIRONMENT = true; function buildSampleMessages(): ThreadMessage[] { return [ { id: "u1", threadId: "t1", seq: 1, role: "user", runId: "r1", createdAt: "2026-10-22T12:00:00Z", blocks: [{ kind: "text", text: "Build brand concept" }], }, { id: "b-plan", threadId: "t1", seq: 2, role: "bot", botId: "bot-1", runId: "r1", createdAt: "2026-10-22T12:00:05Z", blocks: [ { kind: "plan", planId: "p1", title: "Branding-Konzept finalisieren", status: "active", steps: [ { id: "s1", label: "Slogans erstellen", status: "completed" }, { id: "s2", label: "Brand Guide HTML-Seite erstellen", status: "in_progress", note: "Komplette Zusammenfassung aller Brand-Elemente", }, { id: "s3", label: "Alles auf Server kopieren", status: "pending" }, ], }, ], }, { id: "b-step-1", threadId: "t1", seq: 3, role: "bot", botId: "bot-1", runId: "r1", createdAt: "2026-10-22T12:00:10Z", blocks: [{ kind: "text", text: "running: pnpm build" }], }, { id: "b-step-2", threadId: "t1", seq: 4, role: "bot", botId: "bot-1", runId: "r1", createdAt: "2026-10-22T12:00:15Z", blocks: [{ kind: "text", text: "writing brand-guide.html" }], }, { id: "progress:r1", threadId: "t1", seq: 5, role: "bot", botId: "bot-1", runId: "r1", createdAt: "2026-10-22T12:00:20Z", blocks: [{ kind: "progress", text: "Working..." }], }, ]; } describe("ComposerTaskPill & PlanCard integration", () => { beforeEach(() => { i18n.load("en", {}); i18n.activate("en"); }); afterEach(() => { vi.useRealTimers(); }); it("extractActivePlanState collects the active plan and folded narration steps from the current turn", () => { const messages = buildSampleMessages(); const state = extractActivePlanState(messages, true, "2026-10-22T12:00:00Z"); expect(state).not.toBeNull(); expect(state?.block.planId).toBe("p1"); expect(state?.steps.map((s) => s.id)).toEqual(["b-step-1", "b-step-2"]); expect(state?.running).toBe(true); }); it("renders compact active step + progress badge and expands to reveal full checklist & intermediate steps", async () => { const state = extractActivePlanState(buildSampleMessages(), true, "2026-10-22T12:00:00Z")!; const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); await act(async () => { root.render( , ); }); const pill = container.querySelector('[data-testid="composer-task-pill"]'); expect(pill).not.toBeNull(); expect(pill?.getAttribute("data-expanded")).toBe("false"); expect(container.querySelector('[data-testid="composer-task-pill-label"]')?.textContent).toBe( "Brand Guide HTML-Seite erstellen", ); expect( container.querySelector('[data-testid="composer-task-pill-progress"]')?.textContent, ).toBe("1/3"); const toggle = container.querySelector( '[data-testid="composer-task-pill-toggle"]', ) as HTMLButtonElement; await act(async () => { toggle.click(); }); expect(pill?.getAttribute("data-expanded")).toBe("true"); const intermediate = container.querySelector( '[data-testid="composer-plan-intermediate-steps"]', ); expect(intermediate).not.toBeNull(); const stepToggle = intermediate?.querySelector("button") as HTMLButtonElement; await act(async () => { stepToggle.click(); }); expect(intermediate?.textContent).toContain("running: pnpm build"); expect(intermediate?.textContent).toContain("writing brand-guide.html"); await act(async () => { root.unmount(); }); container.remove(); }); it("auto-collapses and hides after all steps complete", async () => { vi.useFakeTimers(); const completedPlan = { block: { kind: "plan" as const, planId: "p-done", title: "Branding-Konzept finalisieren", status: "completed" as const, steps: [ { id: "s1", label: "Slogans erstellen", status: "completed" as const }, { id: "s2", label: "Brand Guide erstellen", status: "completed" as const }, ], }, steps: [], running: false, }; const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); await act(async () => { root.render( , ); }); expect(container.querySelector('[data-testid="composer-task-pill"]')).not.toBeNull(); expect( container.querySelector('[data-testid="composer-task-pill-progress"]')?.textContent, ).toBe("Done"); await act(async () => { vi.advanceTimersByTime(3600); }); expect(container.querySelector('[data-testid="composer-task-pill"]')).toBeNull(); await act(async () => { root.unmount(); }); container.remove(); }); it("PlanCard embeds intermediate steps under the active step", async () => { const state = extractActivePlanState(buildSampleMessages(), true, "2026-10-22T12:00:00Z")!; const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); await act(async () => { root.render( , ); }); const activeStepRow = container.querySelector('[data-testid="plan-step-s2"]'); expect(activeStepRow).not.toBeNull(); expect(activeStepRow?.querySelector('[data-testid="plan-intermediate-steps"]')).not.toBeNull(); await act(async () => { root.unmount(); }); container.remove(); }); });