From df53ff5a5f631c3c82c2251e5886a73c7386fe43 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 30 Aug 2026 15:11:40 +0500 Subject: [PATCH] =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B4=D0=B8=D0=BD=D0=B3:=20?= =?UTF-8?q?=D1=8D=D1=82=D0=B0=D0=B6=20=D0=BF=D0=B5=D1=80=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BB=20=D1=82=D0=B5=D1=80=D1=8F=D1=82=D1=8C=D1=81=D1=8F?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=20=D0=BA=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Этаж — обязательное поле карточки: без него форма не отправляется. При этом saveDraft его не клал, а на целевой странице поля этажа нет вовсе. Комментарий объяснял это тем, что «повторно набирать одно короткое поле дешевле, чем хранить лишнее» — довод неверен: переспросить негде, и обязательное поле работало чистой помехой. Схема бэкенда этаж принимает (TradeInEstimateInput.floor), и на цену он влияет (первый и последний этаж). Хранится теперь по тому же доводу, что и состояние: выбросить уже полученный ответ и спросить второй раз хуже, чем донести. Тест — через САМУ ФОРМУ, и это не формальность. Первая попытка защитить правку звала saveDraft({floor}) напрямую: она проверяла round-trip хранилища и оставалась зелёной, когда из FreeCheckCard убирали передачу этажа, то есть ровно при возврате чинимого дефекта. Сторож, который не сторожит. Теперь форма заполняется и отправляется целиком; снятие проводки даёт «expected undefined to be 7/16» — красное по значению. Плюс три случая на само хранилище: старый черновик без поля переживает выкатку, нестроковое значение выпадает не унося остальное. --- .../__tests__/estimate-draft.test.ts | 35 +++++++++++ .../__tests__/free-check-card-floor.test.tsx | 60 +++++++++++++++++++ .../_components/v3/FreeCheckCard.tsx | 7 ++- .../src/app/mera-public/estimate-draft.ts | 12 ++++ 4 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 tradein-mvp/frontend/src/app/mera-public/__tests__/free-check-card-floor.test.tsx diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/estimate-draft.test.ts b/tradein-mvp/frontend/src/app/mera-public/__tests__/estimate-draft.test.ts index 6a081c4d..8449c935 100644 --- a/tradein-mvp/frontend/src/app/mera-public/__tests__/estimate-draft.test.ts +++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/estimate-draft.test.ts @@ -82,3 +82,38 @@ describe("черновик", () => { expect(draft?.area).toBeUndefined(); }); }); + +/** + * Этаж — обязательное поле карточки лендинга, и до 30.08.2026 оно терялось на + * переходе: `saveDraft` его не клал, а на целевой странице поля этажа нет + * вовсе. Комментарий в коде объяснял это тем, что «повторно набрать одно + * короткое поле дешевле» — переспросить было негде. + * + * Проверяем ОБА конца: и запись, и чтение. Поле, которое сохраняется, но не + * читается обратно, теряется ровно так же, только незаметнее. + */ +describe("этаж переживает переход на целевую страницу", () => { + it("сохранённый этаж читается обратно тем же значением", () => { + saveDraft({ address: "Ленина 5", rooms: "2", area: "54", floor: "7/16" }); + expect(takeDraft()?.floor).toBe("7/16"); + }); + + it("черновик без этажа читается, а не отбрасывается целиком", () => { + // Старые черновики в sessionStorage поля не имеют — они обязаны + // пережить выкатку, а не обнулить человеку всю форму. + saveDraft({ address: "Ленина 5", rooms: "2" }); + const d = takeDraft(); + expect(d?.address).toBe("Ленина 5"); + expect(d?.floor).toBeUndefined(); + }); + + it("этаж не строка — поле выпадает, остальной черновик цел", () => { + window.sessionStorage.setItem( + "mera:estimate-draft", + JSON.stringify({ address: "Ленина 5", floor: 7 }), + ); + const d = takeDraft(); + expect(d?.address).toBe("Ленина 5"); + expect(d?.floor).toBeUndefined(); + }); +}); diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/free-check-card-floor.test.tsx b/tradein-mvp/frontend/src/app/mera-public/__tests__/free-check-card-floor.test.tsx new file mode 100644 index 00000000..86ecb9fc --- /dev/null +++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/free-check-card-floor.test.tsx @@ -0,0 +1,60 @@ +/** + * Проводка этажа ЧЕРЕЗ САМУ ФОРМУ, а не через хранилище черновика. + * + * Первая попытка защитить эту правку была сторожем, который не сторожит: + * тест звал `saveDraft({ floor })` напрямую, поэтому проверял round-trip + * хранилища и оставался ЗЕЛЁНЫМ, когда из `FreeCheckCard` убирали передачу + * этажа — то есть ровно при возврате чинимого дефекта. Здесь форма + * заполняется и отправляется целиком, и утверждение проверяется на том пути, + * которым идёт человек. + */ +import { fireEvent, render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { FreeCheckCard } from "../_components/v3/FreeCheckCard"; +import { takeDraft } from "../estimate-draft"; + +/** + * Отправка формы делает `window.location.assign`. В jsdom переход не + * реализован и печатает «Not implemented: navigation» — глушим, чтобы шум не + * читался как отказ теста. + */ +beforeEach(() => { + window.sessionStorage.clear(); + Object.defineProperty(window, "location", { + configurable: true, + value: { ...window.location, assign: vi.fn() }, + }); +}); + +function fill(labelPart: string, value: string) { + const field = screen.getByLabelText(new RegExp(labelPart, "i")); + fireEvent.change(field, { target: { value } }); +} + +describe("FreeCheckCard: обязательные поля доезжают до следующего шага", () => { + it("этаж, набранный в форме, оказывается в черновике", () => { + render(); + + // Подписи ровно те, что видит человек: «Город», «Адрес», «М²», + // «Комнат», «Этаж». Искать по ним, а не по своим догадкам о названиях, — + // иначе тест разъедется с формой молча. + const city = screen.getByLabelText(/город/i); + fireEvent.change(city, { target: { value: "Екатеринбург" } }); + + fill("адрес", "Екатеринбург, Ленина 5"); + fill("М²", "54"); + fill("комнат", "2"); + fill("этаж", "7/16"); + + // Состояние — группа кнопок с aria-pressed, а не радиогруппа. + const conditions = screen.getByRole("group", { name: /состояние/i }); + fireEvent.click(conditions.querySelectorAll("button")[0]); + + fireEvent.click(screen.getByRole("button", { name: /мо[её]му дому/i })); + + const draft = takeDraft(); + expect(draft, "форма не сохранила черновик — проверь валидацию").not.toBeNull(); + expect(draft?.floor).toBe("7/16"); + }); +}); diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/FreeCheckCard.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/FreeCheckCard.tsx index 5b8a4dee..3afc400c 100644 --- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/FreeCheckCard.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/FreeCheckCard.tsx @@ -202,14 +202,17 @@ export function FreeCheckCard() { // Черновик кладём в sessionStorage, а НЕ в query-параметры: адрес в URL // попал бы в access-логи Caddy рядом с IP посетителя (см. estimate-draft.ts). // Этаж не передаём — его не использует ни проба покрытия, ни экран - // /estimate, и повторно набирать одно короткое поле дешевле, чем хранить - // лишнее. Состояние передаём: его спросили обязательным полем здесь. + // Всё, что человек ввёл обязательным полем, доезжает до следующего шага. + // Раньше этаж здесь терялся под доводом «повторно набрать дешевле, чем + // хранить лишнее» — довод был неверен: поля этажа на целевой странице нет, + // переспросить негде, и обязательное поле работало чистой помехой. saveDraft({ address: trimmedAddress, rooms: trimmedRooms, area: trimmedArea, city: resolvedCity, condition, + floor: trimmedFloor, }); window.location.assign(PUBLIC_ROUTES.estimate); } diff --git a/tradein-mvp/frontend/src/app/mera-public/estimate-draft.ts b/tradein-mvp/frontend/src/app/mera-public/estimate-draft.ts index e5d9b2cd..b24e8567 100644 --- a/tradein-mvp/frontend/src/app/mera-public/estimate-draft.ts +++ b/tradein-mvp/frontend/src/app/mera-public/estimate-draft.ts @@ -32,6 +32,17 @@ export interface EstimateDraft { * он ждёт платного шага; хранится ровно один переход, как и всё остальное. */ condition?: string; + /** + * Этаж, как его набрал человек («7/16» или «7»). Строкой — по тому же + * правилу, что комнаты и площадь: это ровно введённое, без домыслов о числе. + * + * Хранится по той же причине, что и `condition`, и она здесь ЖЁСТЧЕ. Раньше + * этаж не сохраняли, объясняя это тем, что «повторно набрать одно короткое + * поле дешевле, чем хранить лишнее». Утверждение было неверным: набирать + * негде — на целевой странице поля этажа нет вовсе. То есть лендинг требовал + * этаж обязательным полем, выбрасывал его и больше не спрашивал. + */ + floor?: string; } /** @@ -69,6 +80,7 @@ export function takeDraft(): EstimateDraft | null { city: typeof value.city === "string" ? value.city : undefined, condition: typeof value.condition === "string" ? value.condition : undefined, + floor: typeof value.floor === "string" ? value.floor : undefined, }; } catch { return null;