МЕРА v2: клик по «Предыдущие оценки» после отправки показывает выбранную оценку (#2425)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 13s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m19s

После отправки оценки freshResult оставался выставленным, и клик по строке
истории ничего не менял на экране: useEstimate получал null (восстановление
по id вообще не запускалось), а estimate и currentEstimateId брались из
только что посчитанного результата. onSelectEstimate теперь сбрасывает
freshResult так же, как это делает handleNew.

Тест рендерит страницу с заглушками хуков: submit, затем клик по другой
строке истории — в useEstimate уходит id строки, HeroBar и форма получают
её id и адрес. Без правки тест красный (useEstimate вызван с null).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
bot-backend 2026-09-17 12:23:47 +05:00
parent 34642e1dd5
commit f1f0d297cf
2 changed files with 126 additions and 0 deletions

View file

@ -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 }) => (
<button onClick={() => onNavigate(4)}>open-history</button>
),
}));
vi.mock("@/components/trade-in/v2/HeroBar", () => ({
default: ({ estimateId }: { estimateId: string | null }) => (
<div data-testid="hero-id">{estimateId ?? ""}</div>
),
}));
vi.mock("@/components/trade-in/v2/ParamsPanel", () => ({
default: ({
onSubmit,
initialValues,
}: {
onSubmit: (i: unknown) => void;
initialValues?: { address?: string };
}) => (
<div>
<div data-testid="form-address">{initialValues?.address ?? ""}</div>
<button onClick={() => onSubmit({})}>submit</button>
</div>
),
}));
vi.mock("@/components/trade-in/v2/SectionOverlay", () => ({
default: ({ onSelectEstimate }: { onSelectEstimate: (id: string) => void }) => (
<button onClick={() => onSelectEstimate("hist-1")}>row-hist-1</button>
),
}));
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(<TradeInV2Page />);
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");
});
});

View file

@ -1190,6 +1190,11 @@ export default function TradeInV2Page() {
// hands `urlId` the correct target id, mirroring how // hands `urlId` the correct target id, mirroring how
// `freshResult` already overrides this same race for the // `freshResult` already overrides this same race for the
// post-submit path (handleSubmit above). // 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); setPendingRestoreId(id);
router.replace(`/v2?id=${id}`, { scroll: false }); router.replace(`/v2?id=${id}`, { scroll: false });
setNav(0); setNav(0);