Compare commits

..

3 commits

Author SHA1 Message Date
65dfdee63d Merge pull request 'лендинг: этаж перестал теряться на переходе к проверке' (#3279) from fix/mera-keep-floor-in-draft into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m9s
Deploy Trade-In / deploy (push) Successful in 58s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s
2026-08-30 10:33:34 +00:00
f3e05e1907 Merge branch 'main' into fix/mera-keep-floor-in-draft
All checks were successful
CI Trade-In / changes (pull_request) Successful in 13s
CI / changes (pull_request) Successful in 16s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 2m21s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
2026-08-30 10:23:10 +00:00
df53ff5a5f лендинг: этаж перестал теряться на переходе к проверке
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 12s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m5s
Этаж — обязательное поле карточки: без него форма не отправляется. При этом
saveDraft его не клал, а на целевой странице поля этажа нет вовсе. Комментарий
объяснял это тем, что «повторно набирать одно короткое поле дешевле, чем
хранить лишнее» — довод неверен: переспросить негде, и обязательное поле
работало чистой помехой.

Схема бэкенда этаж принимает (TradeInEstimateInput.floor), и на цену он влияет
(первый и последний этаж). Хранится теперь по тому же доводу, что и состояние:
выбросить уже полученный ответ и спросить второй раз хуже, чем донести.

Тест — через САМУ ФОРМУ, и это не формальность. Первая попытка защитить правку
звала saveDraft({floor}) напрямую: она проверяла round-trip хранилища и
оставалась зелёной, когда из FreeCheckCard убирали передачу этажа, то есть
ровно при возврате чинимого дефекта. Сторож, который не сторожит. Теперь форма
заполняется и отправляется целиком; снятие проводки даёт «expected undefined to
be 7/16» — красное по значению.

Плюс три случая на само хранилище: старый черновик без поля переживает выкатку,
нестроковое значение выпадает не унося остальное.
2026-08-30 15:11:40 +05:00
4 changed files with 112 additions and 2 deletions

View file

@ -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();
});
});

View file

@ -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(<FreeCheckCard />);
// Подписи ровно те, что видит человек: «Город», «Адрес», «М²»,
// «Комнат», «Этаж». Искать по ним, а не по своим догадкам о названиях, —
// иначе тест разъедется с формой молча.
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");
});
});

View file

@ -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);
}

View file

@ -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;