- История цен в этом доме
+ {data.priceHistory.title}
{historyNote}
@@ -401,9 +401,9 @@ export default function AnalyticsView({
style={{ width: "100%", height: 260, marginTop: 12 }}
preserveAspectRatio="none"
role="img"
- aria-label={`График истории цен в этом доме: медиана ₽/м² по годам, серии ${AVITO_LABEL} и ${YANDEX_LABEL}`}
+ aria-label={`${data.priceHistory.title}: медиана ₽/м² по годам, серии ${AVITO_LABEL} и ${YANDEX_LABEL}`}
>
-
История цен в этом доме — медиана ₽/м² по годам
+
{`${data.priceHistory.title} — медиана ₽/м² по годам`}
{PH_GRID_Y.map((y) => (
diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/__tests__/priceHistoryScope.test.tsx b/tradein-mvp/frontend/src/components/trade-in/v2/__tests__/priceHistoryScope.test.tsx
new file mode 100644
index 00000000..fab3e36a
--- /dev/null
+++ b/tradein-mvp/frontend/src/components/trade-in/v2/__tests__/priceHistoryScope.test.tsx
@@ -0,0 +1,75 @@
+// M2 audit #2583 — подпись «История цен в этом доме · только этот дом» была
+// безусловной, хотя /house-analytics при <8 архивных лотах расширяет выборку до
+// соседних домов в радиусе 300 м (radius_m в ответе). На том же экране рядом
+// стояло «в радиусе 300 м» — две взаимоисключающие подписи.
+
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { PriceHistoryChart } from "../../PriceHistoryChart";
+import AnalyticsView from "../AnalyticsView";
+import { mapAnalytics } from "../mappers";
+import type {
+ HouseAnalyticsResponse,
+ PriceHistoryYearPoint,
+} from "@/types/trade-in";
+
+const points: PriceHistoryYearPoint[] = [
+ { year: 2024, source: "avito_imv", median_price_per_m2: 120_000, n_lots: 3, median_price_rub: 6_000_000 },
+ { year: 2025, source: "avito_imv", median_price_per_m2: 130_000, n_lots: 4, median_price_rub: 6_500_000 },
+];
+
+function analytics(radius_m: number): HouseAnalyticsResponse {
+ return {
+ house_ids: [1, 2, 3],
+ radius_m,
+ price_history: points,
+ recent_sold: [],
+ kpi: {
+ total_lots: 7,
+ sold_count: 0,
+ sold_rate_pct: 0,
+ median_exposure_days: null,
+ median_bargain_pct: null,
+ },
+ };
+}
+
+describe("v2 история цен: подпись следует radius_m", () => {
+ it("выборка расширена соседями — не «только этот дом»", () => {
+ const { priceHistory } = mapAnalytics(analytics(300), null, null);
+ expect(priceHistory.title).toBe("История цен: дом и соседние");
+ expect(priceHistory.note).toContain("дом и соседние в радиусе 300 м");
+ expect(priceHistory.note).not.toContain("только этот дом");
+ });
+
+ it("экран v2 печатает подпись из данных, а не константу", () => {
+ render();
+ expect(screen.getByText("История цен: дом и соседние")).toBeTruthy();
+ expect(screen.queryByText(/в этом доме/)).toBeNull();
+ });
+
+ it("выборка ровно по дому — прежняя подпись", () => {
+ const { priceHistory } = mapAnalytics(analytics(0), null, null);
+ expect(priceHistory.title).toBe("История цен в этом доме");
+ expect(priceHistory.note).toContain("только этот дом");
+ });
+});
+
+describe("старый экран: PriceHistoryChart", () => {
+ it("radiusM>0 — заголовок и подпись про соседей", () => {
+ // recharts ResponsiveContainer требует ResizeObserver, которого нет в jsdom.
+ vi.stubGlobal(
+ "ResizeObserver",
+ class {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ },
+ );
+ render();
+ expect(screen.getByRole("heading").textContent).toBe("История цен: дом и соседние");
+ expect(screen.queryByText(/только этот дом/)).toBeNull();
+ expect(screen.getByText(/дом и соседние в радиусе 300 м/)).toBeTruthy();
+ });
+});
diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/mappers.ts b/tradein-mvp/frontend/src/components/trade-in/v2/mappers.ts
index 5f33f713..c293fc51 100644
--- a/tradein-mvp/frontend/src/components/trade-in/v2/mappers.ts
+++ b/tradein-mvp/frontend/src/components/trade-in/v2/mappers.ts
@@ -1820,7 +1820,16 @@ export function phYearX(i: number, n: number): number {
}
function buildPriceHistory(a: HouseAnalyticsResponse | null): PriceHistory {
+ // M2 audit #2583: при <8 архивных лотах /house-analytics подмешивает до 30 соседних
+ // домов в радиусе radius_m — «только этот дом» тогда неправда (рядом на экране
+ // стоит «в радиусе 300 м»). radius_m=0 — выборка ровно по дому.
+ const radiusM = a?.radius_m ?? 0;
+ const title =
+ radiusM > 0 ? "История цен: дом и соседние" : "История цен в этом доме";
+ const scope =
+ radiusM > 0 ? `дом и соседние в радиусе ${radiusM} м` : "только этот дом";
const empty: PriceHistory = {
+ title,
note: "Нет данных по истории цен",
years: [],
avito: "",
@@ -1879,7 +1888,7 @@ function buildPriceHistory(a: HouseAnalyticsResponse | null): PriceHistory {
.join(" ");
const totalLots = ph.reduce((s, p) => s + (p.n_lots ?? 0), 0);
- const note = `Медиана ₽/м² по годам · только этот дом · ${sourcePublicLabel("avito_imv")} + ${sourcePublicLabel("yandex_valuation")} (${totalLots} ${pluralRu(
+ const note = `Медиана ₽/м² по годам · ${scope} · ${sourcePublicLabel("avito_imv")} + ${sourcePublicLabel("yandex_valuation")} (${totalLots} ${pluralRu(
totalLots,
["лот", "лота", "лотов"],
)})`;
@@ -1890,6 +1899,7 @@ function buildPriceHistory(a: HouseAnalyticsResponse | null): PriceHistory {
});
return {
+ title,
note,
years: yearsNum.map(String),
avito: series("avito_imv"),
diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/types.ts b/tradein-mvp/frontend/src/components/trade-in/v2/types.ts
index 61e4b8af..961c86d5 100644
--- a/tradein-mvp/frontend/src/components/trade-in/v2/types.ts
+++ b/tradein-mvp/frontend/src/components/trade-in/v2/types.ts
@@ -264,6 +264,8 @@ export interface SellTimeTier {
}
export interface PriceHistory {
+ /** Заголовок карточки: «в этом доме» только когда выборка не расширялась соседями. */
+ title: string;
note: string;
years: string[];
/** SVG polyline points string (viewBox 0 0 900 220). */