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
Воркер сослался на #3086, которого не существует — тест с таким именем остался бы загадкой для следующего читателя. Заведён настоящий issue #3047 с замерами покрытия и разбором дефекта; все ссылки и имя файла приведены к нему. Refs #3047
138 lines
6.9 KiB
Python
138 lines
6.9 KiB
Python
"""Тесты на реальной разметке для #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")
|