"""test_server_anchor_search.py — якорь заходит на origin ЧЕРЕЗ поиск yandex.ru (#3251). Контекст: до этой правки якорная вкладка (_ensure_anchor_page, #3244) открывала origin голым goto — без источника перехода вообще. Ручная сессия 29.08.2026 показала эталонный человеческий путь: yandex.ru → поиск → клик по результату → выдача Домклика (Referer "https://yandex.ru/"). _navigate_anchor_via_search воспроизводит именно этот первый шаг; переход дальше по карточке (referer на целевую навигацию) уже сделан в #3250 и здесь не трогается. camoufox НЕ запускается — context/page/link-элемент подделаны. Запуск (из tradein-mvp/browser/, venv со aiohttp — см. CLAUDE.md сайдкара):: python -m pytest test_server_anchor_search.py -q """ from __future__ import annotations import asyncio import importlib.util from pathlib import Path from typing import Any import pytest _SERVER_PATH = Path(__file__).resolve().parent / "server.py" _spec = importlib.util.spec_from_file_location("tradein_browser_server_anchor", _SERVER_PATH) assert _spec is not None and _spec.loader is not None server = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(server) _ORIGIN = "https://ekaterinburg.domclick.ru/pokupka/kvartiry/vtorichka" _HOSTNAME = "ekaterinburg.domclick.ru" @pytest.fixture(autouse=True) def _reset_state() -> None: server._contexts = {} server._anchor_pages = {} class _FakeLink: """Поддельный результат выдачи: get_attribute/text_content/click как ElementHandle. ``new_tab_host`` эмулирует ссылку с ``target="_blank"``: клик не трогает ``url`` исходной страницы, а открывает НОВУЮ поддельную page в том же context'е (как это делает настоящий Яндекс для части результатов выдачи). """ def __init__( self, page: "_FakeAnchorPage", host: str, land_host: str | None = None, new_tab_host: str | None = None, ) -> None: self._page = page self._host = host self._land_host = land_host or host self._new_tab_host = new_tab_host self.clicked = 0 async def get_attribute(self, name: str) -> str: return f"https://{self._host}/some/redir-wrapper" async def text_content(self) -> str: return self._host async def click(self, timeout: int | None = None) -> None: self.clicked += 1 if self._new_tab_host is not None: popup = _FakeAnchorPage() popup.url = f"https://{self._new_tab_host}/landed" popup.context = self._page.context self._page.context.pages.append(popup) self._page.opened_popup = popup return self._page.url = f"https://{self._land_host}/landed" class _FakeAnchorPage: """Поддельная page: фиксирует goto (url+referer), эмулирует выдачу/капчу/клик.""" def __init__( self, link_host: str | None = None, land_host: str | None = None, captcha: bool = False, fail_search_nav: bool = False, new_tab_host: str | None = None, ) -> None: self.goto_calls: list[dict[str, Any]] = [] self.url = "" self.closed = 0 self.route_calls = 0 self.context: "_FakeAnchorContext | None" = None self.opened_popup: "_FakeAnchorPage | None" = None self._link_host = link_host self._land_host = land_host self._captcha = captcha self._fail_search_nav = fail_search_nav self._new_tab_host = new_tab_host def is_closed(self) -> bool: return self.closed > 0 async def route(self, pattern: str, handler: Any) -> None: self.route_calls += 1 async def goto(self, url: str, **kwargs: Any) -> None: if self._fail_search_nav and "yandex.ru/search" in url: raise TimeoutError("yandex.ru недоступен") self.goto_calls.append({"url": url, "referer": kwargs.get("referer")}) self.url = url async def wait_for_timeout(self, ms: int) -> None: return None async def wait_for_load_state(self, state: str, timeout: int | None = None) -> None: return None async def content(self) -> str: if "yandex.ru/search" in self.url: if self._captcha: return "SmartCaptcha: подтвердите, что запросы отправляли вы" return "выдача Яндекса" return "карточка" async def query_selector_all(self, selector: str) -> list[_FakeLink]: if self._link_host is None: return [] return [_FakeLink(self, self._link_host, self._land_host, self._new_tab_host)] async def close(self) -> None: self.closed += 1 class _FakeAnchorContext: """Поддельный BrowserContext: new_page() отдаёт заранее сконструированную page. ``pages`` эмулирует ``BrowserContext.pages`` — список всех открытых вкладок контекста, растущий при появлении попапа (target="_blank"). """ def __init__(self, page: _FakeAnchorPage) -> None: self._page = page self.pages: list[_FakeAnchorPage] = [] async def new_page(self) -> _FakeAnchorPage: self.pages.append(self._page) self._page.context = self return self._page def _run_ensure_anchor(provider: str, origin: str, page: _FakeAnchorPage) -> bool: server._contexts[provider] = _FakeAnchorContext(page) return asyncio.run(server._ensure_anchor_page(provider, origin)) def test_anchor_via_search_visits_yandex_and_clicks_result() -> None: """domclick включён по умолчанию: заход на yandex.ru → клик по ссылке на хост.""" page = _FakeAnchorPage(link_host=_HOSTNAME) anchored = _run_ensure_anchor("domclick", _ORIGIN, page) assert anchored is True assert server._anchor_pages["domclick"] is page assert any("yandex.ru/search" in c["url"] for c in page.goto_calls), "должен зайти на yandex.ru" # Клик по ссылке сам довёл до хоста — повторного goto(origin) после него НЕ было. assert not any(c["url"] == _ORIGIN for c in page.goto_calls) assert page.url.startswith(f"https://{_HOSTNAME}/") def test_captcha_falls_back_to_plain_goto_without_referer() -> None: """Капча на выдаче → тихий откат на прежнее поведение, без исключения.""" page = _FakeAnchorPage(link_host=_HOSTNAME, captcha=True) anchored = _run_ensure_anchor("domclick", _ORIGIN, page) assert anchored is True assert server._anchor_pages["domclick"] is page assert any("yandex.ru/search" in c["url"] for c in page.goto_calls) origin_calls = [c for c in page.goto_calls if c["url"] == _ORIGIN] assert len(origin_calls) == 1 assert origin_calls[0]["referer"] is None, "прежнее поведение — без referer вообще" def test_no_link_in_serp_falls_back_without_referer() -> None: """Ссылки на хост в выдаче нет → перехода не было → goto(origin) БЕЗ referer. До #3258 этот случай подставлял referer=yandex.ru, хотя перехода с Яндекса на origin фактически не было — намеренная смена поведения. """ page = _FakeAnchorPage(link_host=None) anchored = _run_ensure_anchor("domclick", _ORIGIN, page) assert anchored is True origin_calls = [c for c in page.goto_calls if c["url"] == _ORIGIN] assert len(origin_calls) == 1 assert origin_calls[0]["referer"] is None def test_provider_not_in_allowlist_skips_search_entirely() -> None: """avito не в BROWSER_ANCHOR_VIA_SEARCH (дефолт только domclick) — поведение прежнее.""" origin = "https://www.avito.ru/ekaterinburg/kvartiry" page = _FakeAnchorPage(link_host="www.avito.ru") anchored = _run_ensure_anchor("avito", origin, page) assert anchored is True assert page.goto_calls == [{"url": origin, "referer": None}], "поиска быть не должно вовсе" def test_yandex_navigation_failure_falls_back_and_anchor_still_raised() -> None: """Таймаут захода на yandex.ru → откат на прямой goto(origin), якорь поднят.""" page = _FakeAnchorPage(link_host=_HOSTNAME, fail_search_nav=True) anchored = _run_ensure_anchor("domclick", _ORIGIN, page) assert anchored is True assert server._anchor_pages["domclick"] is page assert page.goto_calls == [{"url": _ORIGIN, "referer": None}] def test_click_opens_new_tab_on_target_host_becomes_anchor() -> None: """Ссылка в выдаче с target="_blank" открывает НОВУЮ вкладку на нужном хосте — она и становится якорной, исходная вкладка с Яндексом закрывается, _apply_resource_block применяется к новой вкладке (не унаследован). """ page = _FakeAnchorPage(link_host=_HOSTNAME, new_tab_host=_HOSTNAME) anchored = _run_ensure_anchor("domclick", _ORIGIN, page) assert anchored is True popup = server._anchor_pages["domclick"] assert popup is not page, "якорной должна стать НОВАЯ вкладка, а не исходная с Яндексом" assert popup.url == f"https://{_HOSTNAME}/landed" assert page.closed == 1, "исходная вкладка с Яндексом должна закрыться" assert popup.route_calls > 0, "_apply_resource_block должен примениться к новой вкладке" assert not any(c["url"] == _ORIGIN for c in popup.goto_calls), "был реальный клик, не goto" def test_new_tab_wrong_host_falls_back_without_referer() -> None: """Новая вкладка открылась НЕ на нужном хосте → None → goto(origin) без referer.""" page = _FakeAnchorPage(link_host=_HOSTNAME, new_tab_host="another.example.ru") anchored = _run_ensure_anchor("domclick", _ORIGIN, page) assert anchored is True assert server._anchor_pages["domclick"] is page, "якорь остаётся на исходной вкладке" origin_calls = [c for c in page.goto_calls if c["url"] == _ORIGIN] assert len(origin_calls) == 1 assert origin_calls[0]["referer"] is None assert page.opened_popup is not None assert page.opened_popup.closed == 1, "попап не на том хосте должен быть закрыт"