diff --git a/frontend/src/components/BoardOnboardingChat.test.tsx b/frontend/src/components/BoardOnboardingChat.test.tsx new file mode 100644 index 00000000..b32591db --- /dev/null +++ b/frontend/src/components/BoardOnboardingChat.test.tsx @@ -0,0 +1,122 @@ +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { BoardOnboardingRead } from "@/api/generated/model"; +import { BoardOnboardingChat } from "./BoardOnboardingChat"; + +const startOnboardingMock = vi.fn(); +const getOnboardingMock = vi.fn(); +const answerOnboardingMock = vi.fn(); +const confirmOnboardingMock = vi.fn(); + +vi.mock("@/hooks/usePageActive", () => ({ + usePageActive: () => true, +})); + +vi.mock("@/components/ui/dialog", () => ({ + DialogHeader: ({ children }: { children?: ReactNode }) => ( +
{children}
+ ), + DialogFooter: ({ children }: { children?: ReactNode }) => ( +
{children}
+ ), + DialogTitle: ({ children }: { children?: ReactNode }) => ( +

{children}

+ ), +})); + +vi.mock("@/api/generated/board-onboarding/board-onboarding", () => ({ + startOnboardingApiV1BoardsBoardIdOnboardingStartPost: (...args: unknown[]) => + startOnboardingMock(...args), + getOnboardingApiV1BoardsBoardIdOnboardingGet: (...args: unknown[]) => + getOnboardingMock(...args), + answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost: (...args: unknown[]) => + answerOnboardingMock(...args), + confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost: (...args: unknown[]) => + confirmOnboardingMock(...args), +})); + +const buildQuestionSession = (question: string): BoardOnboardingRead => ({ + id: "session-1", + board_id: "board-1", + session_key: "session:key", + status: "active", + messages: [ + { + role: "assistant", + content: JSON.stringify({ + question, + options: ["Option A", "Option B"], + }), + timestamp: "2026-02-15T00:00:00Z", + }, + ], + draft_goal: null, + created_at: "2026-02-15T00:00:00Z", + updated_at: "2026-02-15T00:00:00Z", +}); + +describe("BoardOnboardingChat polling", () => { + beforeEach(() => { + vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] }); + startOnboardingMock.mockReset(); + getOnboardingMock.mockReset(); + answerOnboardingMock.mockReset(); + confirmOnboardingMock.mockReset(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("does not keep polling while waiting for user answer on a shown question", async () => { + const session = buildQuestionSession("What should we prioritize?"); + startOnboardingMock.mockResolvedValue({ status: 200, data: session }); + getOnboardingMock.mockResolvedValue({ status: 200, data: session }); + + render( + undefined} />, + ); + + await screen.findByText("What should we prioritize?"); + const callsBeforeWait = getOnboardingMock.mock.calls.length; + + await act(async () => { + vi.advanceTimersByTime(6500); + await Promise.resolve(); + }); + + expect(getOnboardingMock.mock.calls.length).toBe(callsBeforeWait); + }); + + it("continues polling after an answer is submitted and waiting for assistant", async () => { + const session = buildQuestionSession("Pick a style"); + startOnboardingMock.mockResolvedValue({ status: 200, data: session }); + getOnboardingMock.mockResolvedValue({ status: 200, data: session }); + answerOnboardingMock.mockResolvedValue({ status: 200, data: session }); + + render( + undefined} />, + ); + + await screen.findByText("Pick a style"); + + fireEvent.click(screen.getByRole("button", { name: "Option A" })); + fireEvent.click(screen.getByRole("button", { name: "Next" })); + + await waitFor(() => { + expect(answerOnboardingMock).toHaveBeenCalledTimes(1); + }); + + const callsBeforePoll = getOnboardingMock.mock.calls.length; + await act(async () => { + vi.advanceTimersByTime(2500); + await Promise.resolve(); + }); + + await waitFor(() => { + expect(getOnboardingMock.mock.calls.length).toBeGreaterThan(callsBeforePoll); + }); + }); +}); diff --git a/frontend/src/components/BoardOnboardingChat.tsx b/frontend/src/components/BoardOnboardingChat.tsx index ac5c95a9..66047dd0 100644 --- a/frontend/src/components/BoardOnboardingChat.tsx +++ b/frontend/src/components/BoardOnboardingChat.tsx @@ -247,12 +247,17 @@ export function BoardOnboardingChat({ void startSession(); }, [startSession]); + const shouldPollSession = + isPageActive && (loading || isAwaitingAgent || (!question && !draft)); + useEffect(() => { - if (!isPageActive) return; + if (!shouldPollSession) return; void refreshSession(); - const interval = setInterval(refreshSession, 2000); + const interval = setInterval(() => { + void refreshSession(); + }, 2000); return () => clearInterval(interval); - }, [isPageActive, refreshSession]); + }, [refreshSession, shouldPollSession]); const handleAnswer = useCallback( async (value: string, freeText?: string) => {