From 59b5d157526ec96a8a57d01496648f11930f5550 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Fri, 19 Jun 2026 21:28:09 +0300 Subject: [PATCH] =?UTF-8?q?feat(browser):=20block=20font/media=20+=20per-p?= =?UTF-8?q?rovider=20page=20pacing=20=E2=80=94=20=D1=81=D0=BD=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20proxy=20fan-out=20(#1812)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tradein-mvp/browser/server.py | 102 ++++++-- tradein-mvp/browser/test_server.py | 223 ++++++++++++++++++ .../browser/test_server_resource_block.py | 74 +++--- 3 files changed, 340 insertions(+), 59 deletions(-) diff --git a/tradein-mvp/browser/server.py b/tradein-mvp/browser/server.py index 93bdcd7b..73409e7d 100644 --- a/tradein-mvp/browser/server.py +++ b/tradein-mvp/browser/server.py @@ -34,15 +34,22 @@ Per-provider модель (#1793): BROWSER_RECYCLE_PAGES — страниц в одном сеансе браузера до перезапуска (default: 15) BROWSER_NAV_TIMEOUT_MS — таймаут page.goto в мс (default: 60000) BROWSER_WAIT_MS — ожидание гидрации listings после DOMContentLoaded, мс (default: 6000) - BROWSER_BLOCK_RESOURCES — блокировать тяжёлые ресурсы (картинки/медиа/шрифты) при - навигации, чтобы страница качалась быстрее через медленный - прокси (default: "true"; bandwidth — главный боттлнек). - Данные парсятся из page.content()/__initialData__, поэтому - картинки/шрифты не нужны. CSS/JS/XHR/document НЕ блокируем — - нужны для гидрации listings и не палят stealth. - BROWSER_BLOCKED_RESOURCE_TYPES — CSV типов ресурсов Playwright для блокировки - (default: "image,media,font"). Менять консервативно: - блокировка script/stylesheet/document/xhr сломает выдачу. + BROWSER_BLOCK_RESOURCE_TYPES — CSV типов ресурсов Playwright, которые abort'ить + через page.route при навигации (default: "font,media"). + Снижает число одновременных под-коннектов на страницу → + мобильный прокси не ловит NS_ERROR_PROXY_TOO_MANY_REQUESTS. + image уже глушит camoufox block_images. Пустая строка = + НЕ блокировать ничего. КРИТИЧНО: НЕ добавлять + document/script/stylesheet/xhr/fetch — данные парсятся из + HTML/JS-state (INITIAL_STATE/__preloadedState__), JS нужен + для гидрации, CSS проверяется anti-bot fingerprint. + (legacy alias BROWSER_BLOCKED_RESOURCE_TYPES всё ещё читается + как fallback, если новая переменная не задана.) + BROWSER_MIN_PAGE_INTERVAL_S — минимальный интервал (сек) между последовательными + page.goto ОДНОГО провайдера (default: 2.0). Даёт под- + коннектам предыдущей страницы дренироваться, прежде чем + открыть новую → меньше пиковый параллелизм на прокси. + 0 = без пейсинга. BROWSER_PROXY_AVITO — http-прокси для avito (fallback AVITO_PROXY_URL). BROWSER_PROXY_CIAN — http-прокси для cian (fallback CIAN_PROXY_URL). BROWSER_PROXY_YANDEX — http-прокси для yandex (fallback YANDEX_PROXY_URL). @@ -93,18 +100,37 @@ def _parse_bool(raw: str | None, default: bool) -> bool: return raw.strip().lower() in ("true", "1", "yes", "on") -# Блокировка тяжёлых ресурсов (картинки/медиа/шрифты) при навигации. avito SERP -# ~1.5-2 МБ, в основном медиа; нужные данные — в HTML/__initialData__ (парсятся из -# page.content()). Блокируя image/media/font, страница ~200 КБ (×10 меньше) → быстрее -# качается через медленный прокси (bandwidth — главный боттлнек). CSS/JS/XHR/document -# НЕ трогаем — нужны для гидрации listings, плюс «браузер без ресурсов» спалил бы stealth. -_BLOCK_RESOURCES: bool = _parse_bool(os.environ.get("BROWSER_BLOCK_RESOURCES"), True) -_BLOCKED_TYPES: frozenset[str] = frozenset( - t.strip().lower() - for t in os.environ.get("BROWSER_BLOCKED_RESOURCE_TYPES", "image,media,font").split(",") - if t.strip() +def _parse_block_types(raw: str | None) -> frozenset[str]: + """Парсит CSV типов ресурсов для блокировки → frozenset (lower/strip). + + None (env не задана) → дефолт "font,media". Пустая строка → пустой набор + (ничего не блокируем). Не задаём document/script/stylesheet/xhr/fetch здесь — + дефолт намеренно узкий (font+media), эти типы не нужны для extraction/fingerprint. + """ + if raw is None: + raw = "font,media" + return frozenset(t.strip().lower() for t in raw.split(",") if t.strip()) + + +# Типы под-ресурсов, которые abort'им через page.route на каждой странице. Каждый +# page.goto тянет десятки sub-requests (js/css/xhr/font/media) через прокси; мобильный +# прокси держит ~5 параллельных коннектов → 2-3 быстрых страницы подряд на одном +# аккаунте упираются в NS_ERROR_PROXY_TOO_MANY_REQUESTS/500. Глуша font+media (тяжёлые, +# не нужны ни для extraction, ни для fingerprint), снижаем пиковый fan-out. image уже +# глушит camoufox block_images. КРИТИЧНО: document/script/stylesheet/xhr/fetch НЕ +# блокируем — данные в HTML/JS-state (INITIAL_STATE/__preloadedState__), JS гидрирует +# listings, CSS проверяет anti-bot fingerprint. Legacy alias BROWSER_BLOCKED_RESOURCE_TYPES +# читается fallback'ом, если новая BROWSER_BLOCK_RESOURCE_TYPES не задана. +_BLOCKED_TYPES: frozenset[str] = _parse_block_types( + os.environ.get("BROWSER_BLOCK_RESOURCE_TYPES") + or os.environ.get("BROWSER_BLOCKED_RESOURCE_TYPES") ) +# Минимальный интервал (сек) между последовательными page.goto одного провайдера — +# даёт под-коннектам предыдущей страницы дренироваться (прокси освобождает слоты), +# прежде чем открыть новую страницу. 0 = без пейсинга. +BROWSER_MIN_PAGE_INTERVAL_S: float = float(os.environ.get("BROWSER_MIN_PAGE_INTERVAL_S", "2.0")) + def _provider_for_url(url: str) -> str: """Определяет поставщика по host URL. @@ -176,6 +202,7 @@ _browser_cms: dict[str, object] = {} # provider → AsyncCamoufox CM (для _page_counters: dict[str, int] = {} # provider → страниц с момента launch'а _locks: dict[str, asyncio.Lock] = {} # provider → Lock (весь доступ к инстансу) _retry_tasks: dict[str, asyncio.Task[None]] = {} # provider → фоновая retry-задача +_last_goto_at: dict[str, float] = {} # provider → loop-time последнего page.goto (пейсинг) # Guard на ленивое создание per-provider локов: setdefault на обычном dict из # разных корутин гонок не даёт (нет await между read-modify-write), но держим @@ -265,6 +292,7 @@ async def _close_browser(provider: str) -> None: _browsers.pop(provider, None) _browser_cms.pop(provider, None) _page_counters[provider] = 0 + _last_goto_at.pop(provider, None) async def _relaunch_browser(provider: str) -> None: @@ -487,18 +515,18 @@ async def fetch_handler(request: web.Request) -> web.Response: async def _apply_resource_block(page: object) -> None: - """Навешивает route-interception, блокирующую тяжёлые ресурсы (image/media/font). + """Навешивает route-interception, блокирующую тяжёлые под-ресурсы (font/media). - No-op при BROWSER_BLOCK_RESOURCES=false. Route живёт на странице и снимается - автоматически при page.close(). Блокируем ТОЛЬКО _BLOCKED_TYPES (по умолчанию - картинки/медиа/шрифты) — document/script/stylesheet/xhr/fetch проходят, чтобы + No-op при пустом _BLOCKED_TYPES (BROWSER_BLOCK_RESOURCE_TYPES=""). Route живёт на + странице и снимается автоматически при page.close(). Блокируем ТОЛЬКО _BLOCKED_TYPES + (по умолчанию font/media) — document/script/stylesheet/xhr/fetch проходят, чтобы гидрация listings работала и stealth не палился «браузером без ресурсов». Handler обёрнут в try/except: любой сбой abort/continue (например, route уже обработан гонкой) деградирует в continue_(), чтобы один битый ресурс не ронял навигацию. Сам continue_() тоже под try — если страница уже закрыта, молча выходим. """ - if not _BLOCK_RESOURCES: + if not _BLOCKED_TYPES: return async def _route_block(route: object) -> None: # type: ignore[no-untyped-def] @@ -519,6 +547,31 @@ async def _apply_resource_block(page: object) -> None: await page.route("**/*", _route_block) # type: ignore[attr-defined] +async def _pace_provider(provider: str) -> None: + """Притормаживает перед page.goto, если с прошлого goto этого провайдера прошло + меньше BROWSER_MIN_PAGE_INTERVAL_S — даёт под-коннектам прошлой страницы + дренироваться, снижая пиковый параллелизм на прокси. + + Caller держит _locks[provider] → timestamp per-provider читается/пишется без гонки. + Записывает момент ПОСЛЕ возможного sleep (т.е. фактический момент следующего goto), + чтобы интервал считался между реальными навигациями, а не между входами в функцию. + No-op при BROWSER_MIN_PAGE_INTERVAL_S <= 0. + """ + if BROWSER_MIN_PAGE_INTERVAL_S <= 0: + return + now = asyncio.get_event_loop().time() + last = _last_goto_at.get(provider) + if last is not None: + elapsed = now - last + remaining = BROWSER_MIN_PAGE_INTERVAL_S - elapsed + if remaining > 0: + logger.debug( + "tradein-browser[%s]: pacing — ждём %.2fс перед goto", provider, remaining + ) + await asyncio.sleep(remaining) + _last_goto_at[provider] = asyncio.get_event_loop().time() + + async def _do_fetch(provider: str, url: str) -> str: """Одна попытка навигации; при краше браузера — relaunch и один retry. @@ -554,6 +607,7 @@ async def _fetch_once(provider: str, url: str) -> str: page = await browser.new_page() # type: ignore[attr-defined] try: await _apply_resource_block(page) + await _pace_provider(provider) await page.goto(url, timeout=BROWSER_NAV_TIMEOUT_MS, wait_until="domcontentloaded") # type: ignore[attr-defined] if BROWSER_WAIT_MS > 0: await page.wait_for_timeout(BROWSER_WAIT_MS) # type: ignore[attr-defined] diff --git a/tradein-mvp/browser/test_server.py b/tradein-mvp/browser/test_server.py index 205b44ed..e8405774 100644 --- a/tradein-mvp/browser/test_server.py +++ b/tradein-mvp/browser/test_server.py @@ -47,6 +47,7 @@ def _reset_state(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(server, "_page_counters", {}) monkeypatch.setattr(server, "_locks", {}) monkeypatch.setattr(server, "_retry_tasks", {}) + monkeypatch.setattr(server, "_last_goto_at", {}) monkeypatch.setattr(server, "_locks_guard", asyncio.Lock()) @@ -290,5 +291,227 @@ def test_mixed_load_serializes_within_parallelizes_across( assert probe.global_peak >= 2, "avito и cian должны перекрываться во времени" +# ── _parse_block_types: env-парсинг ─────────────────────────────────────────────── + + +def test_parse_block_types_default() -> None: + """None (env не задана) → дефолт font,media.""" + assert server._parse_block_types(None) == frozenset({"font", "media"}) + + +def test_parse_block_types_empty_disables() -> None: + """Пустая строка → пустой набор (ничего не блокируем).""" + assert server._parse_block_types("") == frozenset() + assert server._parse_block_types(" ") == frozenset() + assert server._parse_block_types(" , , ") == frozenset() + + +def test_parse_block_types_custom_csv() -> None: + """CSV парсится с trim/lower, дубликаты схлопываются.""" + assert server._parse_block_types("font, Media , font") == frozenset({"font", "media"}) + assert server._parse_block_types("FONT,IMAGE") == frozenset({"font", "image"}) + + +# ── route-handler: блокировка font/media, пропуск document/script/css/xhr ────────── + + +class _FakeRequest: + def __init__(self, resource_type: str) -> None: + self.resource_type = resource_type + + +class _FakeRoute: + """Мок Playwright Route: фиксирует, был ли abort или continue_.""" + + def __init__(self, resource_type: str) -> None: + self.request = _FakeRequest(resource_type) + self.aborted = False + self.continued = False + + async def abort(self) -> None: + self.aborted = True + + async def continue_(self) -> None: + self.continued = True + + +class _CapturePage: + """Мок page: ловит handler, переданный в page.route, чтобы дёрнуть его напрямую.""" + + def __init__(self) -> None: + self.handler: Any = None + self.route_pattern: Any = None + + async def route(self, pattern: str, handler: Any) -> None: + self.route_pattern = pattern + self.handler = handler + + +def test_resource_block_aborts_font_and_media(monkeypatch: pytest.MonkeyPatch) -> None: + """Дефолтный набор font/media → abort; document/script/stylesheet/xhr/fetch → continue.""" + monkeypatch.setattr(server, "_BLOCKED_TYPES", frozenset({"font", "media"})) + page = _CapturePage() + + asyncio.run(server._apply_resource_block(page)) + assert page.handler is not None, "route-handler должен быть навешен" + assert page.route_pattern == "**/*" + + async def _check(resource_type: str) -> _FakeRoute: + route = _FakeRoute(resource_type) + await page.handler(route) + return route + + # Блокируемые типы → abort + for rt in ("font", "media"): + route = asyncio.run(_check(rt)) + assert route.aborted is True and route.continued is False, f"{rt} должен abort'иться" + + # Критичные для extraction/fingerprint типы → continue + for rt in ("document", "script", "stylesheet", "xhr", "fetch", "image"): + route = asyncio.run(_check(rt)) + assert route.continued is True and route.aborted is False, f"{rt} должен проходить" + + +def test_resource_block_noop_when_empty(monkeypatch: pytest.MonkeyPatch) -> None: + """Пустой _BLOCKED_TYPES → page.route не навешивается (no-op).""" + monkeypatch.setattr(server, "_BLOCKED_TYPES", frozenset()) + page = _CapturePage() + asyncio.run(server._apply_resource_block(page)) + assert page.handler is None, "при пустом наборе route навешивать не должны" + + +def test_resource_block_handler_degrades_to_continue(monkeypatch: pytest.MonkeyPatch) -> None: + """Сбой abort() деградирует в continue_() — битый ресурс не рвёт навигацию.""" + monkeypatch.setattr(server, "_BLOCKED_TYPES", frozenset({"font"})) + page = _CapturePage() + asyncio.run(server._apply_resource_block(page)) + + class _FlakyRoute(_FakeRoute): + async def abort(self) -> None: + raise RuntimeError("route already handled") + + route = _FlakyRoute("font") + asyncio.run(page.handler(route)) + assert route.continued is True, "при сбое abort должны fallback'нуть в continue_()" + + +# ── _pace_provider: межстраничный пейсинг одного провайдера ──────────────────────── + + +def test_pace_provider_first_call_no_sleep(monkeypatch: pytest.MonkeyPatch) -> None: + """Первый goto провайдера — без ожидания, но timestamp пишется.""" + monkeypatch.setattr(server, "BROWSER_MIN_PAGE_INTERVAL_S", 2.0) + slept: list[float] = [] + + async def _fake_sleep(d: float) -> None: + slept.append(d) + + monkeypatch.setattr(server.asyncio, "sleep", _fake_sleep) + asyncio.run(server._pace_provider("avito")) + assert slept == [], "первый вызов не должен спать" + assert "avito" in server._last_goto_at + + +def test_pace_provider_waits_remaining_interval(monkeypatch: pytest.MonkeyPatch) -> None: + """Второй goto в пределах интервала → sleep на остаток (interval - elapsed).""" + monkeypatch.setattr(server, "BROWSER_MIN_PAGE_INTERVAL_S", 2.0) + slept: list[float] = [] + + async def _fake_sleep(d: float) -> None: + slept.append(d) + + # _pace_provider читает time() дважды за вызов (now + запись last). + # 1-й вызов: now=100.0, last←100.0. 2-й: now=100.5 (elapsed=0.5→remaining=1.5), + # после sleep last←102.0. + clock = iter([100.0, 100.0, 100.5, 102.0]) + + class _FakeLoop: + def time(self) -> float: + return next(clock) + + monkeypatch.setattr(server.asyncio, "sleep", _fake_sleep) + monkeypatch.setattr(server.asyncio, "get_event_loop", lambda: _FakeLoop()) + + async def _run() -> None: + await server._pace_provider("cian") # первый — last=100.0 + await server._pace_provider("cian") # второй — elapsed=0.5, remaining=1.5 + + asyncio.run(_run()) + assert len(slept) == 1, "второй вызов должен один раз sleep'нуть" + assert slept[0] == pytest.approx(1.5), "ждём остаток интервала (2.0 - 0.5)" + + +def test_pace_provider_no_sleep_when_interval_elapsed(monkeypatch: pytest.MonkeyPatch) -> None: + """Если с прошлого goto прошло >= интервала → не спим.""" + monkeypatch.setattr(server, "BROWSER_MIN_PAGE_INTERVAL_S", 2.0) + slept: list[float] = [] + + async def _fake_sleep(d: float) -> None: + slept.append(d) + + # 2 чтения time() на вызов: [now1, last1, now2, last2]; elapsed=5.0 > 2.0 → no sleep. + clock = iter([100.0, 100.0, 105.0, 105.0]) + + class _FakeLoop: + def time(self) -> float: + return next(clock) + + monkeypatch.setattr(server.asyncio, "sleep", _fake_sleep) + monkeypatch.setattr(server.asyncio, "get_event_loop", lambda: _FakeLoop()) + + async def _run() -> None: + await server._pace_provider("yandex") + await server._pace_provider("yandex") # elapsed=5.0 > 2.0 → no sleep + + asyncio.run(_run()) + assert slept == [], "интервал уже прошёл — спать не нужно" + + +def test_pace_provider_disabled_when_zero(monkeypatch: pytest.MonkeyPatch) -> None: + """BROWSER_MIN_PAGE_INTERVAL_S <= 0 → пейсинг выключен, timestamp не пишется.""" + monkeypatch.setattr(server, "BROWSER_MIN_PAGE_INTERVAL_S", 0.0) + slept: list[float] = [] + + async def _fake_sleep(d: float) -> None: + slept.append(d) + + monkeypatch.setattr(server.asyncio, "sleep", _fake_sleep) + + async def _run() -> None: + await server._pace_provider("avito") + await server._pace_provider("avito") + + asyncio.run(_run()) + assert slept == [] + assert "avito" not in server._last_goto_at + + +def test_pace_provider_independent_per_provider(monkeypatch: pytest.MonkeyPatch) -> None: + """Timestamp независим по провайдерам — пейсинг avito не влияет на cian.""" + monkeypatch.setattr(server, "BROWSER_MIN_PAGE_INTERVAL_S", 2.0) + slept: list[float] = [] + + async def _fake_sleep(d: float) -> None: + slept.append(d) + + # 2 чтения time() на вызов: avito [100.0, 100.0]; cian первый [100.1, 100.1] + # (нет last для cian → no sleep). + clock = iter([100.0, 100.0, 100.1, 100.1]) + + class _FakeLoop: + def time(self) -> float: + return next(clock) + + monkeypatch.setattr(server.asyncio, "sleep", _fake_sleep) + monkeypatch.setattr(server.asyncio, "get_event_loop", lambda: _FakeLoop()) + + async def _run() -> None: + await server._pace_provider("avito") + await server._pace_provider("cian") + + asyncio.run(_run()) + assert slept == [], "разные провайдеры не пейсят друг друга" + + async def _coro(value: Any) -> Any: return value diff --git a/tradein-mvp/browser/test_server_resource_block.py b/tradein-mvp/browser/test_server_resource_block.py index 3bae85b7..443b9b5f 100644 --- a/tradein-mvp/browser/test_server_resource_block.py +++ b/tradein-mvp/browser/test_server_resource_block.py @@ -1,11 +1,12 @@ """test_server_resource_block.py — юниты для блокировки тяжёлых ресурсов (perf). Проверяет: - 1. _parse_bool — разбор BROWSER_BLOCK_RESOURCES в bool; - 2. _apply_resource_block навешивает route когда флаг включён и НЕ навешивает когда - выключен (no-op); - 3. сам route-handler аборт'ит image/media/font и continue'ит document/script/xhr; - 4. сбой abort() деградирует в continue_() (fallback), не роняя навигацию. + 1. _parse_bool — общий разбор булевых env-флагов; + 2. _parse_block_types — разбор BROWSER_BLOCK_RESOURCE_TYPES CSV (дефолт font,media); + 3. _apply_resource_block навешивает route когда _BLOCKED_TYPES непуст и НЕ навешивает + когда пуст (no-op); + 4. сам route-handler аборт'ит блокируемые типы и continue'ит document/script/xhr; + 5. сбой abort() деградирует в continue_() (fallback), не роняя навигацию. camoufox/Playwright НЕ поднимается: route + request мокаются заглушками. Хендлер вызывается напрямую через захваченный _apply_resource_block-callback. @@ -50,10 +51,19 @@ def test_parse_bool_none_uses_default() -> None: assert server._parse_bool(None, False) is False -def test_default_block_resources_on() -> None: - """Дефолт: блокировка включена, типы — image/media/font.""" - assert server._BLOCK_RESOURCES is True - assert server._BLOCKED_TYPES == frozenset({"image", "media", "font"}) +def test_parse_block_types_default_font_media() -> None: + """Дефолт BROWSER_BLOCK_RESOURCE_TYPES (env не задана) — font/media.""" + assert server._parse_block_types(None) == frozenset({"font", "media"}) + + +def test_parse_block_types_empty_string_disables() -> None: + """Пустая строка → пустой набор (блокировка выключена).""" + assert server._parse_block_types("") == frozenset() + + +def test_parse_block_types_csv_trim_lower() -> None: + """CSV парсится с trim/lower, дубликаты схлопываются.""" + assert server._parse_block_types(" Font , MEDIA ,font") == frozenset({"font", "media"}) # ── route-interception заглушки ────────────────────────────────────────────────── @@ -94,43 +104,37 @@ class _FakePage: self.route_handler = handler -def _capture_handler(monkeypatch: pytest.MonkeyPatch, *, enabled: bool, blocked: set[str]) -> Any: - """Включает флаг/типы, прогоняет _apply_resource_block, возвращает page (с handler).""" - monkeypatch.setattr(server, "_BLOCK_RESOURCES", enabled) +def _capture_handler(monkeypatch: pytest.MonkeyPatch, *, blocked: set[str]) -> Any: + """Ставит _BLOCKED_TYPES, прогоняет _apply_resource_block, возвращает page (с handler). + + Пустой blocked → блокировка выключена (no-op), route не навешивается. + """ monkeypatch.setattr(server, "_BLOCKED_TYPES", frozenset(blocked)) page = _FakePage() asyncio.run(server._apply_resource_block(page)) return page -def test_apply_resource_block_noop_when_disabled(monkeypatch: pytest.MonkeyPatch) -> None: - """BROWSER_BLOCK_RESOURCES=false → route НЕ навешивается.""" - page = _capture_handler(monkeypatch, enabled=False, blocked={"image"}) +def test_apply_resource_block_noop_when_empty(monkeypatch: pytest.MonkeyPatch) -> None: + """Пустой _BLOCKED_TYPES (BROWSER_BLOCK_RESOURCE_TYPES="") → route НЕ навешивается.""" + page = _capture_handler(monkeypatch, blocked=set()) assert page.route_handler is None assert page.route_pattern is None -def test_apply_resource_block_registers_route_when_enabled( +def test_apply_resource_block_registers_route_when_nonempty( monkeypatch: pytest.MonkeyPatch, ) -> None: - """Флаг включён → route на '**/*' с хендлером.""" - page = _capture_handler(monkeypatch, enabled=True, blocked={"image", "media", "font"}) + """Непустой набор → route на '**/*' с хендлером.""" + page = _capture_handler(monkeypatch, blocked={"font", "media"}) assert page.route_pattern == "**/*" assert callable(page.route_handler) -def test_handler_aborts_image(monkeypatch: pytest.MonkeyPatch) -> None: - """image (в _BLOCKED_TYPES) → abort, без continue.""" - page = _capture_handler(monkeypatch, enabled=True, blocked={"image", "media", "font"}) - route = _FakeRoute("image") - asyncio.run(page.route_handler(route)) - assert route.aborted == 1 - assert route.continued == 0 - - -def test_handler_aborts_media_and_font(monkeypatch: pytest.MonkeyPatch) -> None: - page = _capture_handler(monkeypatch, enabled=True, blocked={"image", "media", "font"}) - for rtype in ("media", "font"): +def test_handler_aborts_blocked_types(monkeypatch: pytest.MonkeyPatch) -> None: + """Дефолтные font/media (в _BLOCKED_TYPES) → abort, без continue.""" + page = _capture_handler(monkeypatch, blocked={"font", "media"}) + for rtype in ("font", "media"): route = _FakeRoute(rtype) asyncio.run(page.route_handler(route)) assert route.aborted == 1, rtype @@ -138,9 +142,9 @@ def test_handler_aborts_media_and_font(monkeypatch: pytest.MonkeyPatch) -> None: def test_handler_continues_document_and_script(monkeypatch: pytest.MonkeyPatch) -> None: - """document/script/stylesheet/xhr/fetch — НЕ блокируются (нужны для гидрации/stealth).""" - page = _capture_handler(monkeypatch, enabled=True, blocked={"image", "media", "font"}) - for rtype in ("document", "script", "stylesheet", "xhr", "fetch"): + """document/script/stylesheet/xhr/fetch/image — НЕ блокируются (гидрация/fingerprint).""" + page = _capture_handler(monkeypatch, blocked={"font", "media"}) + for rtype in ("document", "script", "stylesheet", "xhr", "fetch", "image"): route = _FakeRoute(rtype) asyncio.run(page.route_handler(route)) assert route.aborted == 0, rtype @@ -151,8 +155,8 @@ def test_handler_falls_back_to_continue_on_abort_error( monkeypatch: pytest.MonkeyPatch, ) -> None: """abort() кинул (route уже обработан) → fallback continue_(), навигация не падает.""" - page = _capture_handler(monkeypatch, enabled=True, blocked={"image"}) - route = _FakeRoute("image", fail_abort=True) + page = _capture_handler(monkeypatch, blocked={"font"}) + route = _FakeRoute("font", fail_abort=True) # Не должно бросить наружу. asyncio.run(page.route_handler(route)) assert route.continued == 1, "после сбоя abort должен быть fallback-continue" -- 2.45.3