From f1f0d297cf85c23be31b7ced06fc528b6f1dd582 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 17 Sep 2026 12:23:47 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=95=D0=A0=D0=90=20v2:=20=D0=BA=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=20=D0=BF=D0=BE=20=C2=AB=D0=9F=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D1=8B=D0=B4=D1=83=D1=89=D0=B8=D0=B5=20=D0=BE=D1=86=D0=B5=D0=BD?= =?UTF-8?q?=D0=BA=D0=B8=C2=BB=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20=D0=B2=D1=8B=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=BD=D1=83=D1=8E=20=D0=BE=D1=86=D0=B5=D0=BD?= =?UTF-8?q?=D0=BA=D1=83=20(#2425)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit После отправки оценки freshResult оставался выставленным, и клик по строке истории ничего не менял на экране: useEstimate получал null (восстановление по id вообще не запускалось), а estimate и currentEstimateId брались из только что посчитанного результата. onSelectEstimate теперь сбрасывает freshResult так же, как это делает handleNew. Тест рендерит страницу с заглушками хуков: submit, затем клик по другой строке истории — в useEstimate уходит id строки, HeroBar и форма получают её id и адрес. Без правки тест красный (useEstimate вызван с null). Co-Authored-By: Claude Opus 5 --- .../historyClickAfterSubmit.test.tsx | 121 ++++++++++++++++++ tradein-mvp/frontend/src/app/v2/page.tsx | 5 + 2 files changed, 126 insertions(+) create mode 100644 tradein-mvp/frontend/src/app/v2/__tests__/historyClickAfterSubmit.test.tsx diff --git a/tradein-mvp/frontend/src/app/v2/__tests__/historyClickAfterSubmit.test.tsx b/tradein-mvp/frontend/src/app/v2/__tests__/historyClickAfterSubmit.test.tsx new file mode 100644 index 00000000..e7d36a7d --- /dev/null +++ b/tradein-mvp/frontend/src/app/v2/__tests__/historyClickAfterSubmit.test.tsx @@ -0,0 +1,121 @@ +/** + * #2425 — клик по строке «Предыдущие оценки» после отправки оценки в той же + * SPA-сессии должен показать ВЫБРАННУЮ запись, а не только что посчитанную. + * + * Раньше `freshResult` оставался выставленным: `useEstimate` получал null + * (восстановление по id не запускалось вовсе), а экран продолжал показывать + * свежий результат. Проверяется по значению: какой id ушёл в `useEstimate`, + * какой id получил HeroBar и чей адрес лёг в форму. + */ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { FIXTURE_ESTIMATE } from "@/app/ui-preview/estimate/fixture"; +import type { AggregatedEstimate } from "@/types/trade-in"; + +const FRESH: AggregatedEstimate = { + ...FIXTURE_ESTIMATE, + estimate_id: "fresh-1", + target_address: "Екатеринбург, ул. Свежая, 1", +}; +const HIST: AggregatedEstimate = { + ...FIXTURE_ESTIMATE, + estimate_id: "hist-1", + target_address: "Екатеринбург, ул. Историческая, 2", +}; + +const useEstimateMock = vi.hoisted(() => vi.fn()); +const router = vi.hoisted(() => ({ replace: vi.fn(), push: vi.fn() })); + +vi.mock("next/navigation", () => ({ useRouter: () => router })); +vi.mock("@tanstack/react-query", () => ({ + useQueryClient: () => ({ invalidateQueries: vi.fn() }), +})); + +vi.mock("@/lib/trade-in-api", () => { + const empty = () => ({ data: undefined }); + return { + useEstimate: useEstimateMock, + useEstimateMutation: () => ({ + mutate: (_input: unknown, opts: { onSuccess: (e: AggregatedEstimate) => void }) => + opts.onSuccess(FRESH), + isPending: false, + error: null, + }), + useEstimateHistory: empty, + useEstimateHouseAnalytics: empty, + useEstimatePlacementHistory: empty, + useEstimateSellTimeSensitivity: empty, + useLocationIndex: empty, + useSalesVsListings: empty, + useStreetDeals: empty, + }; +}); +vi.mock("@/lib/useQuota", () => ({ useQuota: () => ({ data: undefined }) })); +vi.mock("@/lib/useMe", () => ({ useMe: () => ({ data: undefined }) })); +vi.mock("@/lib/useLogout", () => ({ useLogout: () => ({ mutate: vi.fn() }) })); + +// Дочерние компоненты — заглушки: проверяется только, что страница им отдаёт. +vi.mock("@/components/trade-in/v2/TopNav", () => ({ + default: ({ onNavigate }: { onNavigate: (n: number) => void }) => ( + + ), +})); +vi.mock("@/components/trade-in/v2/HeroBar", () => ({ + default: ({ estimateId }: { estimateId: string | null }) => ( +
{estimateId ?? ""}
+ ), +})); +vi.mock("@/components/trade-in/v2/ParamsPanel", () => ({ + default: ({ + onSubmit, + initialValues, + }: { + onSubmit: (i: unknown) => void; + initialValues?: { address?: string }; + }) => ( +
+
{initialValues?.address ?? ""}
+ +
+ ), +})); +vi.mock("@/components/trade-in/v2/SectionOverlay", () => ({ + default: ({ onSelectEstimate }: { onSelectEstimate: (id: string) => void }) => ( + + ), +})); +vi.mock("@/components/trade-in/v2/ResultPanel", () => ({ default: () => null })); +vi.mock("@/components/trade-in/v2/LowConfidenceBanner", () => ({ + LowConfidenceBanner: () => null, +})); +vi.mock("@/components/trade-in/v2/ObjectSummary", () => ({ ObjectSummary: () => null })); +vi.mock("@/components/trade-in/v2/LeadForm", () => ({ LeadForm: () => null })); +vi.mock("@/components/trade-in/v2/Footer", () => ({ Footer: () => null })); +vi.mock("@/components/trade-in/v2/LocationDrawer", () => ({ LocationDrawer: () => null })); + +import TradeInV2Page from "../page"; + +describe("#2425 — строка истории после submit", () => { + it("клик по другой оценке показывает её, а не только что посчитанную", () => { + useEstimateMock.mockImplementation((id: string | null) => + id === "hist-1" + ? { data: HIST, isPending: false, isError: false, error: null } + : { data: undefined, isPending: id !== null, isError: false, error: null }, + ); + + render(); + + fireEvent.click(screen.getByText("submit")); + expect(screen.getByTestId("hero-id")).toHaveTextContent("fresh-1"); + expect(screen.getByTestId("form-address")).toHaveTextContent("ул. Свежая, 1"); + + fireEvent.click(screen.getByText("open-history")); + fireEvent.click(screen.getByText("row-hist-1")); + + expect(useEstimateMock).toHaveBeenLastCalledWith("hist-1"); + expect(router.replace).toHaveBeenLastCalledWith("/v2?id=hist-1", { scroll: false }); + expect(screen.getByTestId("hero-id")).toHaveTextContent("hist-1"); + expect(screen.getByTestId("form-address")).toHaveTextContent("ул. Историческая, 2"); + }); +}); diff --git a/tradein-mvp/frontend/src/app/v2/page.tsx b/tradein-mvp/frontend/src/app/v2/page.tsx index 4917d454..2c7a7b2a 100644 --- a/tradein-mvp/frontend/src/app/v2/page.tsx +++ b/tradein-mvp/frontend/src/app/v2/page.tsx @@ -1190,6 +1190,11 @@ export default function TradeInV2Page() { // hands `urlId` the correct target id, mirroring how // `freshResult` already overrides this same race for the // post-submit path (handleSubmit above). + // + // #2425: a fresh submit and restore-by-id are mutually exclusive + // both ways (as in handleNew) — a leftover freshResult would + // disable useEstimate(urlId) and keep shadowing the clicked row. + setFreshResult(null); setPendingRestoreId(id); router.replace(`/v2?id=${id}`, { scroll: false }); setNav(0); -- 2.45.3