diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/__tests__/SourcesView.test.tsx b/tradein-mvp/frontend/src/components/trade-in/v2/__tests__/SourcesView.test.tsx new file mode 100644 index 00000000..3ab3a05d --- /dev/null +++ b/tradein-mvp/frontend/src/components/trade-in/v2/__tests__/SourcesView.test.tsx @@ -0,0 +1,94 @@ +// #2755 — колонка ссылки в таблице аналогов «05 РЫНОК». +// +// Здесь до этого жила третья ветка: когда `url` приходил `undefined`, рисовался +// НЕкликабельный , стилизованный под кнопку-ссылку «Росреестр ↗». Ветка +// была недостижима (mapSources всегда ставит `string | null`), но выглядела как +// рабочий элемент интерфейса. Ветку убрали, поле сделали обязательным — этот +// тест держит обе половины: ссылка есть только там, где есть URL, а строка без +// URL получает прочерк, а не кнопку-обманку. + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import SourcesView from "../SourcesView"; +import { mapSources } from "../mappers"; +import { FIXTURE_ESTIMATE } from "@/app/ui-preview/estimate/fixture"; +import type { AggregatedEstimate, AnalogLot } from "@/types/trade-in"; + +function lot(over: Partial): AnalogLot { + return { + address: "ул. Репина, 73", + area_m2: 54, + rooms: 2, + floor: 7, + total_floors: 16, + price_rub: 9_700_000, + price_per_m2: 179_629, + listing_date: "2026-05-12", + days_on_market: 18, + photo_url: null, + source: "avito", + source_url: null, + distance_m: 120, + tier: null, + lat: 56.8401, + lon: 60.5702, + ...over, + }; +} + +// Оффлайн-фикстура оценки репозитория с подменёнными аналогами: остальные поля +// (sources_used, коридоры, KPI) настоящие, меняем ровно то, что проверяем. +function estimateWith(analogs: AnalogLot[]): AggregatedEstimate { + return { + ...FIXTURE_ESTIMATE, + analogs, + n_analogs: analogs.length, + actual_deals: [], + }; +} + +describe("SourcesView — колонка ссылки на оригинал", () => { + it("есть URL — кликабельная ссылка на него", () => { + const estimate = estimateWith([ + lot({ + address: "ул. Репина, 73", + source_url: "https://www.avito.ru/ekaterinburg/kvartiry/1", + }), + ]); + + render(); + + const link = screen.getByRole("link", { name: /Источник/ }); + expect(link).toHaveAttribute( + "href", + "https://www.avito.ru/ekaterinburg/kvartiry/1", + ); + }); + + it("URL нет — прочерк, а не кнопка-обманка «Росреестр ↗»", () => { + const estimate = estimateWith([ + lot({ address: "ул. Викулова, 33", source_url: null }), + ]); + + render(); + + expect(screen.queryByText(/Росреестр ↗/)).toBeNull(); + expect(screen.queryByRole("link", { name: /Источник/ })).toBeNull(); + // Прочерк — единственное содержимое ячейки ссылки в строке аналога. + const row = screen.getByText("ул. Викулова, 33").closest("div"); + expect(row?.textContent).toContain("—"); + }); + + it("javascript:-схема отбрасывается safeUrl — прочерк, а не ссылка", () => { + const estimate = estimateWith([ + lot({ address: "ул. Кирова, 28", source_url: "javascript:alert(1)" }), + ]); + + render(); + + expect(screen.queryByRole("link", { name: /Источник/ })).toBeNull(); + const row = screen.getByText("ул. Кирова, 28").closest("div"); + expect(row?.textContent).toContain("—"); + }); +});