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

This commit is contained in:
bot-backend 2026-08-30 10:33:34 +00:00
commit 65dfdee63d
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;