feat: improve session polling logic in BoardOnboardingChat component
This commit is contained in:
122
frontend/src/components/BoardOnboardingChat.test.tsx
Normal file
122
frontend/src/components/BoardOnboardingChat.test.tsx
Normal file
@@ -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 }) => (
|
||||||
|
<div>{children}</div>
|
||||||
|
),
|
||||||
|
DialogFooter: ({ children }: { children?: ReactNode }) => (
|
||||||
|
<div>{children}</div>
|
||||||
|
),
|
||||||
|
DialogTitle: ({ children }: { children?: ReactNode }) => (
|
||||||
|
<h2>{children}</h2>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
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(
|
||||||
|
<BoardOnboardingChat boardId="board-1" onConfirmed={() => 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(
|
||||||
|
<BoardOnboardingChat boardId="board-1" onConfirmed={() => 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -247,12 +247,17 @@ export function BoardOnboardingChat({
|
|||||||
void startSession();
|
void startSession();
|
||||||
}, [startSession]);
|
}, [startSession]);
|
||||||
|
|
||||||
|
const shouldPollSession =
|
||||||
|
isPageActive && (loading || isAwaitingAgent || (!question && !draft));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPageActive) return;
|
if (!shouldPollSession) return;
|
||||||
void refreshSession();
|
void refreshSession();
|
||||||
const interval = setInterval(refreshSession, 2000);
|
const interval = setInterval(() => {
|
||||||
|
void refreshSession();
|
||||||
|
}, 2000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [isPageActive, refreshSession]);
|
}, [refreshSession, shouldPollSession]);
|
||||||
|
|
||||||
const handleAnswer = useCallback(
|
const handleAnswer = useCallback(
|
||||||
async (value: string, freeText?: string) => {
|
async (value: string, freeText?: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user