import { describe, expect, it, vi, beforeEach } from "vitest"; import { fireEvent, render, screen } from "@testing-library/react"; import type { AiSessionSummary } from "../../api"; import { SessionNotificationBanner, dismissedIds } from "../SessionNotificationBanner"; function buildSession(overrides: Partial): AiSessionSummary { return { id: overrides.id ?? "session-1", type: overrides.type ?? "planning", status: overrides.status ?? "awaiting_input", title: overrides.title ?? "Draft implementation plan", projectId: overrides.projectId ?? "proj-1", lockedByTab: overrides.lockedByTab ?? null, updatedAt: overrides.updatedAt ?? new Date().toISOString(), }; } describe("SessionNotificationBanner", () => { beforeEach(() => { dismissedIds.clear(); }); it("renders nothing when no sessions need input", () => { const { container } = render( , ); expect(container.firstChild).toBeNull(); }); it("renders banner with correct awaiting_input count", () => { render( , ); expect(screen.getByText("2 AI sessions need your input")).toBeInTheDocument(); expect(screen.getByText("First")).toBeInTheDocument(); expect(screen.getByText("Second")).toBeInTheDocument(); expect(screen.queryByText("Done")).not.toBeInTheDocument(); }); it("does not render sessions that are generating or complete", () => { render( , ); expect(screen.getByText("Planning Session")).toBeInTheDocument(); expect(screen.queryByText("Generating")).not.toBeInTheDocument(); expect(screen.queryByText("Complete")).not.toBeInTheDocument(); }); it("calls onResumeSession with the selected session", () => { const onResumeSession = vi.fn(); const planningSession = buildSession({ id: "planning-1", type: "planning", status: "awaiting_input", title: "Plan checkout flow", }); render( , ); fireEvent.click(screen.getByRole("button", { name: "Resume" })); expect(onResumeSession).toHaveBeenCalledWith(planningSession); }); it("calls onDismissSession with the session id", () => { const onDismissSession = vi.fn(); render( , ); fireEvent.click(screen.getByRole("button", { name: "Dismiss Dismiss me" })); expect(onDismissSession).toHaveBeenCalledWith("dismiss-1"); }); it("calls onDismissAll when clicking dismiss all", () => { const onDismissAll = vi.fn(); render( , ); fireEvent.click(screen.getByRole("button", { name: /dismiss all/i })); expect(onDismissAll).toHaveBeenCalledTimes(1); }); it("shows type labels and icons for planning, subtask, and mission interview", () => { const { container } = render( , ); expect(screen.getByText("Planning")).toBeInTheDocument(); expect(screen.getByText("Subtask Breakdown")).toBeInTheDocument(); expect(screen.getByText("Mission Interview")).toBeInTheDocument(); expect(container.querySelector(".lucide-lightbulb")).toBeTruthy(); expect(container.querySelector(".lucide-layers")).toBeTruthy(); expect(container.querySelector(".lucide-target")).toBeTruthy(); }); it("removes dismissed sessions from the banner", () => { render( , ); fireEvent.click(screen.getByRole("button", { name: "Dismiss First Session" })); expect(screen.queryByText("First Session")).not.toBeInTheDocument(); expect(screen.getByText("Second Session")).toBeInTheDocument(); }); it("renders error sessions in the banner", () => { render( , ); expect(screen.getByText("Failed Mission")).toBeInTheDocument(); expect(screen.getByText("Failed")).toBeInTheDocument(); }); it("shows 'Retry' button for error sessions instead of 'Resume'", () => { const errorSession = buildSession({ id: "error-1", type: "mission_interview", status: "error", title: "Error Session", }); render( , ); expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Resume" })).not.toBeInTheDocument(); }); it("calls onResumeSession when clicking Retry on error session", () => { const onResumeSession = vi.fn(); const errorSession = buildSession({ id: "error-retry", type: "mission_interview", status: "error", title: "Error to Retry", }); render( , ); fireEvent.click(screen.getByRole("button", { name: "Retry" })); expect(onResumeSession).toHaveBeenCalledWith(errorSession); }); it("shows combined header text for both awaiting_input and error sessions", () => { render( , ); expect(screen.getByText("1 AI session needs your input, 1 failed")).toBeInTheDocument(); }); it("shows error count only when no awaiting_input sessions", () => { render( , ); expect(screen.getByText("2 AI sessions failed")).toBeInTheDocument(); }); it("dismisses error sessions from the banner", () => { const onDismissSession = vi.fn(); render( , ); fireEvent.click(screen.getByRole("button", { name: "Dismiss Error to Dismiss" })); expect(onDismissSession).toHaveBeenCalledWith("error-dismiss"); }); it("preserves dismissed error sessions when session status changes", () => { const { rerender } = render( , ); // Dismiss the error session fireEvent.click(screen.getByRole("button", { name: "Dismiss Error Session" })); expect(screen.queryByText("Error Session")).not.toBeInTheDocument(); // Simulate session status changing to complete (should NOT reappear) rerender( , ); // Dismissed session should still be hidden even though status changed expect(screen.queryByText("Error Session")).not.toBeInTheDocument(); }); });