Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Has been cancelled
70 lines
3.4 KiB
Python
70 lines
3.4 KiB
Python
"""#2669: Cian отдаёт просмотры фразой — разбор обязан доставать из неё числа.
|
||
|
||
Замер на проде до правки: `listings.views_total` пуст у ВСЕХ 21 799 cian-строк
|
||
(0 из 1 571 detail-обогащённых), при том что у avito 12 350/12 352, у domklik
|
||
6 296/6 296, у yandex 1 146/1 210 — потеря ровно одна и только у Cian.
|
||
|
||
Корень: `_parse_views` звала `int()` по всей фразе
|
||
`"146 просмотров, 8 за сегодня"` → ValueError → None (except в самой функции).
|
||
|
||
Тесты гоняют настоящий сохранённый ответ (fixtures/cian_flat_330982715.html)
|
||
через настоящий `fetch_detail` + перебирают формы фразы.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from unittest.mock import AsyncMock, MagicMock
|
||
|
||
import pytest
|
||
|
||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
||
|
||
from scraper_kit.providers.cian.detail import _parse_views, fetch_detail
|
||
|
||
_FIXTURE = os.path.join(os.path.dirname(__file__), "fixtures", "cian_flat_330982715.html")
|
||
|
||
# Дословно из фикстуры: "stats":{"totalViewsFormattedString":"146 просмотров, 8 за сегодня"}
|
||
_FIXTURE_PHRASE = "146 просмотров, 8 за сегодня"
|
||
|
||
|
||
def _fixture_html() -> str:
|
||
with open(_FIXTURE, encoding="utf-8") as fh:
|
||
return fh.read()
|
||
|
||
|
||
def test_fixture_still_carries_the_phrase() -> None:
|
||
"""Страховка: тесты ниже бессмысленны, если фикстура перестала содержать фразу."""
|
||
assert f'"totalViewsFormattedString":"{_FIXTURE_PHRASE}"' in _fixture_html()
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("phrase", "expected"),
|
||
[
|
||
(_FIXTURE_PHRASE, (146, 8)), # форма из сохранённого ответа
|
||
("1 просмотр", (1, None)), # единственное число, хвоста нет
|
||
("2 просмотра", (2, None)),
|
||
("0 просмотров", (0, None)), # ноль — это 0, а не «нет данных»
|
||
("1 234 просмотра", (1234, None)), # разделитель тысяч — обычный пробел
|
||
("1\xa0234 просмотра", (1234, None)), # ... и NBSP
|
||
("12 345 просмотров, 1\xa0234 за сегодня", (12345, 1234)), # narrow NBSP
|
||
("1 234", (1234, None)), # голое число (форма из старого докстринга)
|
||
("", (None, None)),
|
||
(None, (None, None)),
|
||
("просмотров нет", (None, None)), # без цифр — пусто, а не 0
|
||
],
|
||
)
|
||
def test_parse_views_forms(phrase: str | None, expected: tuple[int | None, int | None]) -> None:
|
||
assert _parse_views(phrase) == expected
|
||
|
||
|
||
async def test_fetch_detail_fills_views_from_real_saved_page() -> None:
|
||
"""Настоящая сохранённая страница → views_total/views_today непусты (#2669)."""
|
||
fetcher = MagicMock()
|
||
fetcher.fetch = AsyncMock(return_value=_fixture_html())
|
||
|
||
result = await fetch_detail("https://ekb.cian.ru/sale/flat/330982715/", browser_fetcher=fetcher)
|
||
|
||
assert result is not None
|
||
assert result.views_total == 146, "views_total снова теряется — колонка останется пустой"
|
||
assert result.views_today == 8
|