import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ChatPanel } from "../ChatPanel"; import type { ChatAskRequest, ChatAskResponse } from "@/lib/site-finder-api"; // Mock the API client so the mutation resolves with a controlled response and we // can assert the request payload (intent / cad_num). const postChatAsk = vi.fn<(req: ChatAskRequest) => Promise>(); vi.mock("@/lib/site-finder-api", () => ({ postChatAsk: (req: ChatAskRequest) => postChatAsk(req), })); function renderPanel(cadNum = "66:41:0000000:1") { const client = new QueryClient({ defaultOptions: { mutations: { retry: false } }, }); return render( , ); } const READY_RESPONSE: ChatAskResponse = { answer: "Краткая сводка по участку.", grounded_in: { run_id: 42, schema_version: "1.0", sections: ["6.1", "6.4"] }, llm_used: true, fallback_reason: null, advisory: true, report_status: "ready", }; const PENDING_RESPONSE: ChatAskResponse = { answer: "Сначала запустите анализ участка.", grounded_in: null, llm_used: false, fallback_reason: "report_pending", advisory: false, report_status: "pending", }; beforeEach(() => { postChatAsk.mockReset(); }); afterEach(() => { vi.clearAllMocks(); }); describe("ChatPanel", () => { it("renders the heading and the five preset-intent buttons", () => { renderPanel(); expect( screen.getByRole("heading", { name: "Чат по участку" }), ).toBeInTheDocument(); for (const label of [ "Сводка", "Что строить", "Почему такой прогноз", "Риски", "Сценарии", ]) { expect(screen.getByRole("button", { name: label })).toBeInTheDocument(); } }); it("sends with the matching intent + cad_num when a preset is clicked", async () => { postChatAsk.mockResolvedValue(READY_RESPONSE); renderPanel("66:41:0000000:1"); await userEvent.click(screen.getByRole("button", { name: "Риски" })); await waitFor(() => expect(postChatAsk).toHaveBeenCalledTimes(1)); const req = postChatAsk.mock.calls[0][0]; expect(req.intent).toBe("risks"); expect(req.cad_num).toBe("66:41:0000000:1"); expect(req.message.length).toBeGreaterThan(0); }); it("renders the assistant answer, advisory badge and provenance line", async () => { postChatAsk.mockResolvedValue(READY_RESPONSE); renderPanel(); await userEvent.click(screen.getByRole("button", { name: "Сводка" })); expect( await screen.findByText("Краткая сводка по участку."), ).toBeInTheDocument(); expect(screen.getByText("Advisory")).toBeInTheDocument(); // grounded_in → "источник: разделы 6.1, 6.4 · run #42" expect(screen.getByText(/источник:/)).toHaveTextContent("разделы 6.1, 6.4"); expect(screen.getByText(/источник:/)).toHaveTextContent("run #42"); }); it("renders the pending state as a neutral message (not an error)", async () => { postChatAsk.mockResolvedValue(PENDING_RESPONSE); renderPanel(); await userEvent.click(screen.getByRole("button", { name: "Сводка" })); expect( await screen.findByText("Сначала запустите анализ участка."), ).toBeInTheDocument(); expect(screen.getByText("Прогноз не рассчитан")).toBeInTheDocument(); expect(screen.getByText("Детерминированный ответ")).toBeInTheDocument(); // pending is NOT an error → no alert / retry button expect(screen.queryByRole("alert")).not.toBeInTheDocument(); expect( screen.queryByRole("button", { name: "Повторить" }), ).not.toBeInTheDocument(); }); it("shows a retry affordance on network error", async () => { postChatAsk.mockRejectedValueOnce(new Error("network down")); renderPanel(); await userEvent.click(screen.getByRole("button", { name: "Сводка" })); expect(await screen.findByRole("alert")).toBeInTheDocument(); const retry = screen.getByRole("button", { name: "Повторить" }); expect(retry).toBeInTheDocument(); // Retry re-fires the same request. postChatAsk.mockResolvedValueOnce(READY_RESPONSE); await userEvent.click(retry); await waitFor(() => expect(postChatAsk).toHaveBeenCalledTimes(2)); }); it("submits the free-text input and clears it", async () => { postChatAsk.mockResolvedValue(READY_RESPONSE); renderPanel(); const input = screen.getByPlaceholderText("Спросите про участок…"); await userEvent.type(input, "Какова плотность застройки?"); await userEvent.click( screen.getByRole("button", { name: "Отправить сообщение" }), ); await waitFor(() => expect(postChatAsk).toHaveBeenCalledTimes(1)); expect(postChatAsk.mock.calls[0][0].message).toBe( "Какова плотность застройки?", ); expect(input).toHaveValue(""); }); });