gendesign/tradein-mvp/backend/tests/test_avito_detail_fields_3047.py
bot-backend f34edeb512
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / backend-tests (pull_request) Successful in 4m48s
chore(tradein/avito): ссылки на реальный issue #3047 вместо выдуманного номера
Воркер сослался на #3086, которого не существует — тест с таким именем
остался бы загадкой для следующего читателя. Заведён настоящий issue #3047
с замерами покрытия и разбором дефекта; все ссылки и имя файла приведены
к нему.

Refs #3047
2026-08-21 23:40:22 +03:00

138 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Тесты на реальной разметке для #3047 (5 полей detail-парсера Avito).
Фикстуры — две живые detail-карточки, снятые через прод-браузер 2026-08-21:
- avito_detail_card_owner_66m2.html — item_id 8322506679, продавец "Частное лицо"
- avito_detail_card_agency_95m2.html — item_id 8343481647, продавец "Агентство"
Разные значения seller-info/label в двух фикстурах — намеренно: это единственный
способ проверить и True, и False ветку is_homeowner на реальной разметке, а не
придуманным HTML.
Что покрыто (см. докстринг detail.py для полного разбора по каждому полю):
- sale_type: способ чтения таблицы параметров (item-view/item-params — два блока
с ОДНИМ маркером, не один блок с двумя <ul>)
- metro_stations: структурная разметка (#item-view-address) вместо текста описания
- is_homeowner: [data-marker='seller-info/label']
- days_on_market: производное от исправленного publish_date
([data-marker='item-view/item-date'], а не текст item-id-блока)
- cadastral_number: подтверждение ОТСУТСТВИЯ на этих карточках (regression guard —
если Avito когда-то начнёт отдавать значение, эти тесты не должны молча зелёнеть
на None, если код начнёт что-то парсить без проверки)
"""
from __future__ import annotations
from datetime import date
from pathlib import Path
from scraper_kit.providers.avito.detail import parse_detail_html
FIXTURES = Path(__file__).parent / "fixtures"
def _load(name: str) -> str:
return (FIXTURES / name).read_text(encoding="utf-8")
CARD_OWNER = "avito_detail_card_owner_66m2.html"
CARD_AGENCY = "avito_detail_card_agency_95m2.html"
class TestSaleTypeParamsReading:
"""item-view/item-params: два <div> с ОДНИМ маркером ("О квартире"/"О доме"),
каждый со своим <ul> — раньше css_first брал только первый."""
def test_sale_type_owner_card(self) -> None:
e = parse_detail_html(_load(CARD_OWNER), "https://example.com/1")
assert e.sale_type == "free"
def test_sale_type_agency_card(self) -> None:
e = parse_detail_html(_load(CARD_AGENCY), "https://example.com/2")
assert e.sale_type == "free"
def test_house_params_from_second_block_not_dropped(self) -> None:
"""Regression guard: house_type/total_floors_house из ВТОРОГО
item-view/item-params блока больше не теряются молча."""
e = parse_detail_html(_load(CARD_OWNER), "https://example.com/1")
assert e.house_type == "monolith_brick"
assert e.total_floors_house == 16
def test_house_params_second_card(self) -> None:
e = parse_detail_html(_load(CARD_AGENCY), "https://example.com/2")
assert e.house_type == "brick"
assert e.total_floors_house == 17
class TestMetroStructured:
"""#item-view-address: имя станции + время читаются из структурной разметки,
а не из regex по тексту описания."""
def test_metro_owner_card(self) -> None:
e = parse_detail_html(_load(CARD_OWNER), "https://example.com/1")
names = [s["name"] for s in e.metro_stations]
# "Машиностроителей" и "Уралмаш" не подходят под старый METRO_RE
# (регекс требовал суффикс -ская/-инская и т.п.) — структурный путь их не
# теряет.
assert names == ["Уральская", "Машиностроителей", "Уралмаш"]
assert e.metro_stations[0] == {
"name": "Уральская",
"min_to": 20,
"min_from": 16,
"mode": "walk",
}
assert e.metro_stations[2] == {
"name": "Уралмаш",
"min_to": 30,
"min_from": 21,
"mode": "walk",
}
def test_metro_agency_card_open_ended_time(self) -> None:
"""'от 31 мин.' -> min_from=None, min_to=31 (открытый диапазон)."""
e = parse_detail_html(_load(CARD_AGENCY), "https://example.com/2")
names = [s["name"] for s in e.metro_stations]
assert names == ["Площадь 1905 года", "Геологическая", "Чкаловская"]
for station in e.metro_stations:
assert station["min_from"] is None
assert station["min_to"] == 31
assert station["mode"] == "walk"
class TestIsHomeowner:
"""[data-marker='seller-info/label']: 'Частное лицо' -> True, 'Агентство' ->
False. Обе ветки подтверждены на разных эталонах (не выдумано)."""
def test_private_seller_is_homeowner_true(self) -> None:
e = parse_detail_html(_load(CARD_OWNER), "https://example.com/1")
assert e.is_homeowner is True
def test_agency_seller_is_homeowner_false(self) -> None:
e = parse_detail_html(_load(CARD_AGENCY), "https://example.com/2")
assert e.is_homeowner is False
class TestDaysOnMarketFromPublishDate:
"""publish_date раньше искался ВНУТРИ item-id-блока — на этой вёрстке дата лежит
в соседнем [data-marker='item-view/item-date'] и всегда уходила None.
days_on_market — честно выведенное производное (обе карточки: 'сегодня')."""
def test_publish_date_today_owner_card(self) -> None:
e = parse_detail_html(_load(CARD_OWNER), "https://example.com/1")
assert e.publish_date == date.today()
assert e.days_on_market == 0
def test_publish_date_today_agency_card(self) -> None:
e = parse_detail_html(_load(CARD_AGENCY), "https://example.com/2")
assert e.publish_date == date.today()
assert e.days_on_market == 0
class TestCadastralNumberNotAvailable:
"""Regression guard: на этих карточках кадастрового номера нет НИГДЕ —
ни в тексте, ни в структурной разметке (domotekaReportTeaser.cadastralNumber
пустая строка). DetailEnrichment не имеет поля cadastral_number вовсе — код
не должен его придумывать."""
def test_no_cadastral_field_on_dataclass(self) -> None:
e = parse_detail_html(_load(CARD_OWNER), "https://example.com/1")
assert not hasattr(e, "cadastral_number")