diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/deal-map.test.tsx b/tradein-mvp/frontend/src/app/mera-public/__tests__/deal-map.test.tsx index 987bb353..da8b6541 100644 --- a/tradein-mvp/frontend/src/app/mera-public/__tests__/deal-map.test.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/deal-map.test.tsx @@ -216,12 +216,12 @@ describe("карточка игры выбирает карту по налич }); it("схема есть — показана улица", () => { - const { container } = render(); + const { container } = render(); expect(container.textContent).toContain("ДОМ НЕ ИЗВЕСТЕН"); }); it("схемы нет — остаётся район, а не пустое место", () => { - const { container } = render(); + const { container } = render(); expect(container.textContent).toContain("ЛЕНИНСКИЙ"); expect(container.textContent).not.toContain("ДОМ НЕ ИЗВЕСТЕН"); }); diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-v3-render.test.tsx b/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-v3-render.test.tsx index b1405fdf..3598d5e8 100644 --- a/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-v3-render.test.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-v3-render.test.tsx @@ -6,11 +6,13 @@ * ИСЧЕЗАЕТ, а не показывает ноль, прочерк или подпись без числа. Отказ ручки * `/stats` в проде выглядит именно так: пустой объект. */ -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { AccuracyV3 } from "../_components/v3/AccuracyV3"; import { DealsTickerV3 } from "../_components/v3/DealsTickerV3"; +import { GuessGameV3 } from "../_components/v3/GuessGameV3"; +import { toGameRound } from "../_components/v3/deal-view"; import { CostOfErrorV3 } from "../_components/v3/CostOfErrorV3"; import { HeroV3 } from "../_components/v3/HeroV3"; import { BACKTEST } from "../landing-facts"; @@ -315,3 +317,57 @@ describe("лента сделок: контекст разброса", () => { expect(note.textContent).toContain("75,7 %"); }); }); + +/** + * ИТОГ ИГРЫ НАЗЫВАЕТ ОТБОР (#2903). + * + * Раунды — строки витрины, а витрина с 12.09.2026 отобрана по ошибке МЕРЫ + * (полоса −5 %..+20 %). Плитка «ошибка МЕРЫ на этих же квартирах» без + * оговорки читается как точность расчёта, хотя это лучший отобранный кусок: + * на проде 17.09 медиана |ошибки| двадцати строк 6,5 %, по всей сверке + * кратно больше. Проверяется по тексту итога, до которого доходим кликами. + */ +describe("игра: итог называет отбор раундов", () => { + const rounds = [0.58, -3.2, 17.9].map((err_pct, i) => + toGameRound( + { + ...SHOWCASE.deals[0], + rooms: i, + err_pct, + predicted_rub: Math.round(SHOWCASE.deals[0].fact_rub * (1 + err_pct / 100)), + }, + i, + ), + ); + + const finish = () => { + for (let i = 0; i < rounds.length; i += 1) { + fireEvent.click(screen.getByRole("button", { name: "Ответить" })); + fireEvent.click( + screen.getByRole("button", { + name: i === rounds.length - 1 ? "Показать итог" : "Следующая квартира →", + }), + ); + } + return screen.getByText(/Медиана расхождения МЕРЫ по всей сверке/u); + }; + + it("строки витрины в полосе → итог говорит, что квартиры из отобранной полосы, и печатает медиану сверки", () => { + render(); + const note = finish(); + expect(screen.getByText("ошибка МЕРЫ на этих же квартирах")).toBeTruthy(); + expect( + note.textContent, + "итог сравнивает игрока с МЕРОЙ на отобранных по её ошибке квартирах и молчит об отборе", + ).toMatch(/отобранной полосы.*от -5 % до \+20 %.*не вся сверка/u); + expect(note.textContent).toContain(BACKTEST.priceError.text); + expect(note.textContent).toContain(String(BACKTEST.priceError.sampleN)); + }); + + it("строки витрины вне полосы → про полосу молчит, медиана сверки остаётся", () => { + render(); + const note = finish(); + expect(note.textContent).not.toMatch(/полос/u); + expect(note.textContent).toContain(BACKTEST.priceError.text); + }); +}); diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/GuessGameV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/GuessGameV3.tsx index e59f0ffc..564663fd 100644 --- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/GuessGameV3.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/GuessGameV3.tsx @@ -27,12 +27,22 @@ * — той же формулой, что и ошибка МЕРЫ на этих же сделках (`err_pct` витрины). * Обе величины меряют одно и то же на одной выборке, поэтому их и можно * ставить рядом. + * + * НО ВЫБОРКА ОТОБРАНА ПО ОШИБКЕ МЕРЫ (#2903). С 12.09.2026 на витрину, а + * значит и в раунды, попадают только сделки с расхождением от −5 % до +20 % + * (`select_rows` в app/tasks/landing_showcase_deals.py). «Ошибка МЕРЫ на этих + * же квартирах» без оговорки читается как точность расчёта, а это лучший + * отобранный кусок. Поэтому итог называет полосу и держит рядом медиану по + * всей сверке — как подписи ленты и таблицы. Про полосу говорим, только если + * её подтверждают сами строки витрины (`bandSelected` = `allWithinBand`, + * считает родитель: раунды — лишь три из этих строк). */ import { useState } from "react"; +import { BACKTEST } from "../../landing-facts"; import styles from "../../landing-v3.module.css"; -import type { GameRound } from "./deal-view"; +import { BAND_MAX_PCT, BAND_MIN_PCT, count, errPct, type GameRound } from "./deal-view"; import { DealMapV3 } from "./DealMapV3"; import { StreetMapV3 } from "./StreetMapV3"; @@ -49,7 +59,14 @@ function median(xs: number[]): number { return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2; } -export function GuessGameV3({ rounds }: { rounds: readonly GameRound[] }) { +export function GuessGameV3({ + rounds, + bandSelected, +}: { + rounds: readonly GameRound[]; + /** Все строки витрины лежат в полосе — значит раунды взяты из отобранной полосы. */ + bandSelected: boolean; +}) { const [idx, setIdx] = useState(0); const [phase, setPhase] = useState("playing"); const [guess, setGuess] = useState(rounds[0].start); @@ -188,6 +205,13 @@ export function GuessGameV3({ rounds }: { rounds: readonly GameRound[] }) { +

+ {`${ + bandSelected + ? `Квартиры для игры взяты из отобранной полосы, где МЕРА разошлась с ценой ДКП от ${errPct(BAND_MIN_PCT)} до ${errPct(BAND_MAX_PCT)}: это не вся сверка. ` + : "" + }Медиана расхождения МЕРЫ по всей сверке — ${BACKTEST.priceError.text} (${count(BACKTEST.priceError.sampleN)} сделок).`} +

Рынок сложно угадать на глаз — для этого и существует расчёт по данным. diff --git a/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css b/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css index fa5f4963..d3d02b07 100644 --- a/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css +++ b/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css @@ -1920,6 +1920,11 @@ .gameFinal { text-align: center; } +/* Оговорка итога игры (#2903): отбор раундов и медиана всей сверки. */ +.gameFinalNote { + composes: accFootnote; + margin: 0 0 18px; +} .gameFinalGrid { display: grid; grid-template-columns: 1fr 1fr; diff --git a/tradein-mvp/frontend/src/app/mera-public/page.tsx b/tradein-mvp/frontend/src/app/mera-public/page.tsx index 662aba96..8bf611fc 100644 --- a/tradein-mvp/frontend/src/app/mera-public/page.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/page.tsx @@ -12,7 +12,7 @@ import { StepsV3 } from "./_components/v3/StepsV3"; import { StickyCtaV3 } from "./_components/v3/StickyCtaV3"; import { TwoPathsV3 } from "./_components/v3/TwoPathsV3"; import { WhatYouGetV3 } from "./_components/v3/WhatYouGetV3"; -import { pickVariedDeals, toGameRound } from "./_components/v3/deal-view"; +import { allWithinBand, pickVariedDeals, toGameRound } from "./_components/v3/deal-view"; import { serviceNode } from "./_lib/schema"; import { fetchLandingStats, fetchShowcase } from "./public-api"; import styles from "./landing.module.css"; @@ -120,7 +120,9 @@ export default async function MeraPublicPage() { - {rounds.length >= MIN_ROUNDS && } + {rounds.length >= MIN_ROUNDS && ( + + )}