225 lines
6.6 KiB
TypeScript
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();
|
|
});
|
|
});
|