evOo-local/apps/web/src/pages/shell/composer-task-pill.test.tsx

225 lines
6.6 KiB
TypeScript

// @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(
<I18nProvider i18n={i18n}>
<ComposerTaskPill activePlan={state} />
</I18nProvider>,
);
});
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(
<I18nProvider i18n={i18n}>
<ComposerTaskPill activePlan={completedPlan} />
</I18nProvider>,
);
});
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(
<I18nProvider i18n={i18n}>
<PlanCard block={state.block} steps={state.steps} />
</I18nProvider>,
);
});
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();
});
});