Merge pull request 'fix(tradein/browser): переход с Яндекса на площадку должен быть настоящим, а не заявленным' (#3260) from feat/browser-real-search-clickthrough into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / build-frontend (push) Successful in 2m14s
Deploy Trade-In / build-browser (push) Successful in 2m16s
Deploy Trade-In / test (push) Successful in 4m12s
Deploy Trade-In / build-backend (push) Successful in 34s
Deploy Trade-In / deploy (push) Successful in 2m27s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s
All checks were successful
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / build-frontend (push) Successful in 2m14s
Deploy Trade-In / build-browser (push) Successful in 2m16s
Deploy Trade-In / test (push) Successful in 4m12s
Deploy Trade-In / build-backend (push) Successful in 34s
Deploy Trade-In / deploy (push) Successful in 2m27s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s
This commit is contained in:
commit
6cf987c55b
2 changed files with 187 additions and 72 deletions
|
|
@ -278,11 +278,11 @@ _ANCHOR_SEARCH_QUERY_DEFAULT_BY_PROVIDER: dict[str, str] = {
|
||||||
"domclick": "домклик екатеринбург квартиры вторичка",
|
"domclick": "домклик екатеринбург квартиры вторичка",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Базовый URL поиска и Referer "визита без клика" — если ссылку в выдаче найти не
|
# Базовый URL поиска. Константы «Referer визита без клика» здесь СОЗНАТЕЛЬНО нет:
|
||||||
# удалось, заход на yandex.ru всё равно РЕАЛЬНО был, поэтому Referer честно ставим
|
# подставлять https://yandex.ru/ по факту одного лишь захода на поиск — это заявить
|
||||||
# на сам yandex.ru, а не молчим об источнике вовсе (#3251).
|
# переход, которого не было. Не нашли ссылку / клик увёл не туда → идём на origin
|
||||||
|
# вообще без Referer (#3251).
|
||||||
_YANDEX_SEARCH_URL: str = "https://yandex.ru/search/"
|
_YANDEX_SEARCH_URL: str = "https://yandex.ru/search/"
|
||||||
_YANDEX_REFERER: str = "https://yandex.ru/"
|
|
||||||
|
|
||||||
# Маркеры SmartCaptcha Яндекса. Мобильные прокси иногда ловят капчу на выдаче —
|
# Маркеры SmartCaptcha Яндекса. Мобильные прокси иногда ловят капчу на выдаче —
|
||||||
# наблюдалось вживую 29.08.2026. Решать капчу нечем и незачем: детектируем и тихо
|
# наблюдалось вживую 29.08.2026. Решать капчу нечем и незачем: детектируем и тихо
|
||||||
|
|
@ -346,31 +346,43 @@ async def _find_serp_result_link(page: object, hostname: str) -> object | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def _navigate_anchor_via_search(page: object, provider: str, origin: str) -> bool:
|
async def _navigate_anchor_via_search(page: object, provider: str, origin: str) -> object | None:
|
||||||
"""Пытается открыть ``origin`` переходом со страницы выдачи yandex.ru вместо
|
"""Пытается открыть ``origin`` переходом со страницы выдачи yandex.ru вместо
|
||||||
голого goto — так это делает реальный пользователь (#3251).
|
голого goto — так это делает реальный пользователь (#3251).
|
||||||
|
|
||||||
Эталон — ручная сессия 29.08.2026: yandex.ru → клик по результату → выдача
|
ПРИНЦИП (не оптимизировать обратно на "поставить правдоподобный Referer"):
|
||||||
Домклика с Referer "https://yandex.ru/" → клик по карточке → Referer = URL
|
либо переход с Яндекса на origin случился НАСТОЯЩИЙ, либо мы честно об этом
|
||||||
выдачи. Второй шаг уже воспроизведён передачей referer на целевую навигацию
|
молчим. До этой правки (#3258) два фолбэка — "ссылка не найдена" и "клик
|
||||||
(#3250), здесь — первый: заход на origin НЕ напрямую, а через поиск.
|
увёл не туда" — подставляли Referer (yandex.ru / URL выдачи) страницам, на
|
||||||
|
которые фактически НЕ переходили с Яндекса. Заявлять переход, которого не
|
||||||
|
было, нельзя. Теперь оба случая возвращают ``None`` без единого referer, и
|
||||||
|
вызывающий делает обычный ``goto(origin)`` без referer — ровно как до
|
||||||
|
#3251, когда захода через поиск не существовало вовсе.
|
||||||
|
|
||||||
Возвращает True, если ``origin`` уже открыт — успешно через клик по
|
Возвращает страницу, на которой origin реально открыт после клика:
|
||||||
результату, либо через один из контролируемых фолбэков с честным referer
|
- ``page`` (тот же объект) — клик по результату выдачи навигировал текущую
|
||||||
(ссылка не найдена / клик увёл не туда). Вызывающему в этом случае ничего
|
вкладку;
|
||||||
дополнительно делать не нужно.
|
- НОВУЮ страницу — клик открыл origin в отдельной вкладке (частый случай:
|
||||||
|
ссылки в выдаче Яндекса нередко имеют ``target="_blank"``). В этом случае
|
||||||
|
именно новая страница несёт на себе результат клика, поэтому она
|
||||||
|
становится якорной, а исходная вкладка с Яндексом закрывается (держать
|
||||||
|
две вкладки на один провайдер незачем и дороже по памяти). Вызывающий
|
||||||
|
обязан подменить свою ссылку на ``page`` возвращённой — и применить к
|
||||||
|
ней ``_apply_resource_block`` заново, т.к. новая вкладка ничего не
|
||||||
|
наследует от исходной.
|
||||||
|
|
||||||
Возвращает False, если заход через поиск не делается вовсе (провайдер не в
|
Возвращает ``None``, если настоящего перехода на origin не случилось:
|
||||||
BROWSER_ANCHOR_VIA_SEARCH, запроса для него нет) либо не удался целиком —
|
поиск выключен для провайдера / нет запроса, капча на выдаче, упавшая
|
||||||
капча на выдаче или упавшая навигация на yandex.ru. В обоих случаях
|
навигация на yandex.ru, ссылки в выдаче нет, клик не удался, либо клик
|
||||||
прогон НЕ должен падать: вызывающий откатывается на прежний прямой
|
(в текущей вкладке или в новой) увёл не на тот хост. Прогон в любом из
|
||||||
goto(origin) без referer, ровно как до #3251.
|
этих случаев НЕ должен падать — вызывающий откатывается на прямой
|
||||||
|
goto(origin) без referer.
|
||||||
"""
|
"""
|
||||||
if provider not in _ANCHOR_VIA_SEARCH_PROVIDERS:
|
if provider not in _ANCHOR_VIA_SEARCH_PROVIDERS:
|
||||||
return False
|
return None
|
||||||
query = _anchor_search_query(provider)
|
query = _anchor_search_query(provider)
|
||||||
if query is None:
|
if query is None:
|
||||||
return False
|
return None
|
||||||
|
|
||||||
hostname = urlparse(origin).hostname or ""
|
hostname = urlparse(origin).hostname or ""
|
||||||
search_url = f"{_YANDEX_SEARCH_URL}?text={quote(query)}"
|
search_url = f"{_YANDEX_SEARCH_URL}?text={quote(query)}"
|
||||||
|
|
@ -382,37 +394,39 @@ async def _navigate_anchor_via_search(page: object, provider: str, origin: str)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"tradein-browser[%s]: заход на yandex.ru для якоря не удался (%s) — "
|
"tradein-browser[%s]: заход на yandex.ru для якоря не удался (%s) — "
|
||||||
"откат на прямой goto(origin)",
|
"откат на прямой goto(origin) без referer",
|
||||||
provider,
|
provider,
|
||||||
type(exc).__name__,
|
type(exc).__name__,
|
||||||
)
|
)
|
||||||
return False
|
return None
|
||||||
|
|
||||||
html = await page.content() # type: ignore[attr-defined]
|
html = await page.content() # type: ignore[attr-defined]
|
||||||
serp_url = str(getattr(page, "url", search_url))
|
serp_url = str(getattr(page, "url", search_url))
|
||||||
if _looks_like_yandex_captcha(html, serp_url):
|
if _looks_like_yandex_captcha(html, serp_url):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"tradein-browser[%s]: капча на выдаче Яндекса — откат на прямой goto(origin)",
|
"tradein-browser[%s]: капча на выдаче Яндекса — откат на прямой "
|
||||||
|
"goto(origin) без referer",
|
||||||
provider,
|
provider,
|
||||||
)
|
)
|
||||||
return False
|
return None
|
||||||
|
|
||||||
link = await _find_serp_result_link(page, hostname)
|
link = await _find_serp_result_link(page, hostname)
|
||||||
if link is None:
|
if link is None:
|
||||||
logger.info(
|
logger.info(
|
||||||
"tradein-browser[%s]: ссылка на %s не найдена в выдаче yandex.ru — "
|
"tradein-browser[%s]: ссылка на %s не найдена в выдаче yandex.ru — "
|
||||||
"goto(origin) с referer=%s",
|
"перехода не было, откат на прямой goto(origin) без referer",
|
||||||
provider,
|
provider,
|
||||||
hostname,
|
hostname,
|
||||||
_YANDEX_REFERER,
|
|
||||||
)
|
)
|
||||||
await page.goto( # type: ignore[attr-defined]
|
return None
|
||||||
origin,
|
|
||||||
timeout=BROWSER_NAV_TIMEOUT_MS,
|
# Снимок открытых вкладок ДО клика — так после клика можно отличить "клик
|
||||||
wait_until="domcontentloaded",
|
# открыл новую вкладку" (target="_blank", частый случай в живой выдаче
|
||||||
referer=_YANDEX_REFERER,
|
# Яндекса) от "клик навигировал текущую". Без этого попап на другой вкладке
|
||||||
)
|
# остался бы незамеченным: текущая page осталась бы на yandex.ru, проверка
|
||||||
return True
|
# хоста ниже не прошла бы, и мы ушли бы в фолбэк вместо настоящего перехода.
|
||||||
|
context = getattr(page, "context", None)
|
||||||
|
pages_before = list(context.pages) if context is not None else [] # type: ignore[attr-defined]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await link.click(timeout=BROWSER_NAV_TIMEOUT_MS) # type: ignore[attr-defined]
|
await link.click(timeout=BROWSER_NAV_TIMEOUT_MS) # type: ignore[attr-defined]
|
||||||
|
|
@ -420,38 +434,64 @@ async def _navigate_anchor_via_search(page: object, provider: str, origin: str)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"tradein-browser[%s]: клик по ссылке в выдаче не удался (%s) — "
|
"tradein-browser[%s]: клик по ссылке в выдаче не удался (%s) — "
|
||||||
"goto(origin) с referer=%s",
|
"откат на прямой goto(origin) без referer",
|
||||||
provider,
|
provider,
|
||||||
type(exc).__name__,
|
type(exc).__name__,
|
||||||
_YANDEX_REFERER,
|
|
||||||
)
|
)
|
||||||
await page.goto( # type: ignore[attr-defined]
|
return None
|
||||||
origin,
|
|
||||||
timeout=BROWSER_NAV_TIMEOUT_MS,
|
|
||||||
wait_until="domcontentloaded",
|
|
||||||
referer=_YANDEX_REFERER,
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
landed_host = urlparse(str(getattr(page, "url", ""))).hostname or ""
|
popup: object | None = None
|
||||||
if landed_host != hostname:
|
if context is not None:
|
||||||
# Клик состоялся, но увёл не туда (редирект-обёртка Яндекса подвела) — визит
|
for candidate in context.pages: # type: ignore[attr-defined]
|
||||||
# на саму выдачу РЕАЛЬНО был, поэтому referer = точный URL выдачи, а не общий
|
if candidate not in pages_before and candidate is not page:
|
||||||
# yandex.ru (в отличие от ветки "ссылка не найдена" выше).
|
popup = candidate
|
||||||
|
break
|
||||||
|
|
||||||
|
if popup is not None:
|
||||||
|
try:
|
||||||
|
await popup.wait_for_load_state( # type: ignore[attr-defined]
|
||||||
|
"domcontentloaded", timeout=BROWSER_NAV_TIMEOUT_MS
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
popup_host = urlparse(str(getattr(popup, "url", ""))).hostname or ""
|
||||||
|
if popup_host == hostname:
|
||||||
|
logger.info(
|
||||||
|
"tradein-browser[%s]: клик по ссылке в выдаче открыл НОВУЮ вкладку "
|
||||||
|
"на %s — она становится якорной, исходная с Яндексом закрывается",
|
||||||
|
provider,
|
||||||
|
hostname,
|
||||||
|
)
|
||||||
|
await _apply_resource_block(popup)
|
||||||
|
try:
|
||||||
|
await page.close() # type: ignore[attr-defined]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return popup
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"tradein-browser[%s]: клик увёл на %s вместо %s — goto(origin) с "
|
"tradein-browser[%s]: клик открыл новую вкладку на %s вместо %s — "
|
||||||
"referer=URL выдачи",
|
"перехода на origin не было, откат на прямой goto(origin) без referer",
|
||||||
provider,
|
provider,
|
||||||
landed_host,
|
popup_host,
|
||||||
hostname,
|
hostname,
|
||||||
)
|
)
|
||||||
await page.goto( # type: ignore[attr-defined]
|
try:
|
||||||
origin,
|
await popup.close() # type: ignore[attr-defined]
|
||||||
timeout=BROWSER_NAV_TIMEOUT_MS,
|
except Exception:
|
||||||
wait_until="domcontentloaded",
|
pass
|
||||||
referer=serp_url,
|
return None
|
||||||
)
|
|
||||||
return True
|
landed_host = urlparse(str(getattr(page, "url", ""))).hostname or ""
|
||||||
|
if landed_host == hostname:
|
||||||
|
return page
|
||||||
|
logger.warning(
|
||||||
|
"tradein-browser[%s]: клик увёл текущую вкладку на %s вместо %s — "
|
||||||
|
"перехода на origin не было, откат на прямой goto(origin) без referer",
|
||||||
|
provider,
|
||||||
|
landed_host,
|
||||||
|
hostname,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _resolve_min_interval(
|
def _resolve_min_interval(
|
||||||
|
|
@ -836,13 +876,18 @@ async def _ensure_anchor_page(provider: str, origin: str) -> bool:
|
||||||
try:
|
try:
|
||||||
page = await ctx.new_page() # type: ignore[attr-defined]
|
page = await ctx.new_page() # type: ignore[attr-defined]
|
||||||
await _apply_resource_block(page)
|
await _apply_resource_block(page)
|
||||||
# #3251: за origin для включённых провайдеров идём через реальный поиск
|
# #3251/#3258: за origin для включённых провайдеров идём через реальный
|
||||||
# yandex.ru, а не голым goto — так это делает человек. landed_via_search
|
# поиск yandex.ru, а не голым goto — так это делает человек. anchored_page
|
||||||
# уже покрывает и успех, и контролируемые фолбэки со своим referer; False
|
# не None только если переход на origin был НАСТОЯЩИЙ (см. докстринг
|
||||||
# означает "поиск не делается или не удался целиком" — старое поведение
|
# _navigate_anchor_via_search) — тогда это либо та же page, либо новая
|
||||||
# ниже не меняется ни на байт.
|
# вкладка (клик открыл её через target="_blank"), и её нужно взять как
|
||||||
landed_via_search = await _navigate_anchor_via_search(page, provider, origin)
|
# якорную вместо исходной. None — переход не случился ни в каком виде,
|
||||||
if not landed_via_search:
|
# откатываемся на прежний прямой goto(origin) БЕЗ referer, ровно как до
|
||||||
|
# #3251: заявлять переход, которого не было, нельзя.
|
||||||
|
anchored_page = await _navigate_anchor_via_search(page, provider, origin)
|
||||||
|
if anchored_page is not None:
|
||||||
|
page = anchored_page # type: ignore[assignment]
|
||||||
|
else:
|
||||||
await page.goto( # type: ignore[attr-defined]
|
await page.goto( # type: ignore[attr-defined]
|
||||||
origin, timeout=BROWSER_NAV_TIMEOUT_MS, wait_until="domcontentloaded"
|
origin, timeout=BROWSER_NAV_TIMEOUT_MS, wait_until="domcontentloaded"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,24 @@ def _reset_state() -> None:
|
||||||
|
|
||||||
|
|
||||||
class _FakeLink:
|
class _FakeLink:
|
||||||
"""Поддельный результат выдачи: get_attribute/text_content/click как ElementHandle."""
|
"""Поддельный результат выдачи: get_attribute/text_content/click как ElementHandle.
|
||||||
|
|
||||||
def __init__(self, page: "_FakeAnchorPage", host: str, land_host: str | None = None) -> None:
|
``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._page = page
|
||||||
self._host = host
|
self._host = host
|
||||||
self._land_host = land_host or host
|
self._land_host = land_host or host
|
||||||
|
self._new_tab_host = new_tab_host
|
||||||
self.clicked = 0
|
self.clicked = 0
|
||||||
|
|
||||||
async def get_attribute(self, name: str) -> str:
|
async def get_attribute(self, name: str) -> str:
|
||||||
|
|
@ -56,6 +68,13 @@ class _FakeLink:
|
||||||
|
|
||||||
async def click(self, timeout: int | None = None) -> None:
|
async def click(self, timeout: int | None = None) -> None:
|
||||||
self.clicked += 1
|
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"
|
self._page.url = f"https://{self._land_host}/landed"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -68,20 +87,25 @@ class _FakeAnchorPage:
|
||||||
land_host: str | None = None,
|
land_host: str | None = None,
|
||||||
captcha: bool = False,
|
captcha: bool = False,
|
||||||
fail_search_nav: bool = False,
|
fail_search_nav: bool = False,
|
||||||
|
new_tab_host: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.goto_calls: list[dict[str, Any]] = []
|
self.goto_calls: list[dict[str, Any]] = []
|
||||||
self.url = ""
|
self.url = ""
|
||||||
self.closed = 0
|
self.closed = 0
|
||||||
|
self.route_calls = 0
|
||||||
|
self.context: "_FakeAnchorContext | None" = None
|
||||||
|
self.opened_popup: "_FakeAnchorPage | None" = None
|
||||||
self._link_host = link_host
|
self._link_host = link_host
|
||||||
self._land_host = land_host
|
self._land_host = land_host
|
||||||
self._captcha = captcha
|
self._captcha = captcha
|
||||||
self._fail_search_nav = fail_search_nav
|
self._fail_search_nav = fail_search_nav
|
||||||
|
self._new_tab_host = new_tab_host
|
||||||
|
|
||||||
def is_closed(self) -> bool:
|
def is_closed(self) -> bool:
|
||||||
return self.closed > 0
|
return self.closed > 0
|
||||||
|
|
||||||
async def route(self, pattern: str, handler: Any) -> None:
|
async def route(self, pattern: str, handler: Any) -> None:
|
||||||
return None
|
self.route_calls += 1
|
||||||
|
|
||||||
async def goto(self, url: str, **kwargs: Any) -> None:
|
async def goto(self, url: str, **kwargs: Any) -> None:
|
||||||
if self._fail_search_nav and "yandex.ru/search" in url:
|
if self._fail_search_nav and "yandex.ru/search" in url:
|
||||||
|
|
@ -92,6 +116,9 @@ class _FakeAnchorPage:
|
||||||
async def wait_for_timeout(self, ms: int) -> None:
|
async def wait_for_timeout(self, ms: int) -> None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def wait_for_load_state(self, state: str, timeout: int | None = None) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
async def content(self) -> str:
|
async def content(self) -> str:
|
||||||
if "yandex.ru/search" in self.url:
|
if "yandex.ru/search" in self.url:
|
||||||
if self._captcha:
|
if self._captcha:
|
||||||
|
|
@ -102,14 +129,18 @@ class _FakeAnchorPage:
|
||||||
async def query_selector_all(self, selector: str) -> list[_FakeLink]:
|
async def query_selector_all(self, selector: str) -> list[_FakeLink]:
|
||||||
if self._link_host is None:
|
if self._link_host is None:
|
||||||
return []
|
return []
|
||||||
return [_FakeLink(self, self._link_host, self._land_host)]
|
return [_FakeLink(self, self._link_host, self._land_host, self._new_tab_host)]
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
self.closed += 1
|
self.closed += 1
|
||||||
|
|
||||||
|
|
||||||
class _FakeAnchorContext:
|
class _FakeAnchorContext:
|
||||||
"""Поддельный BrowserContext: new_page() отдаёт заранее сконструированную page."""
|
"""Поддельный BrowserContext: new_page() отдаёт заранее сконструированную page.
|
||||||
|
|
||||||
|
``pages`` эмулирует ``BrowserContext.pages`` — список всех открытых вкладок
|
||||||
|
контекста, растущий при появлении попапа (target="_blank").
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, page: _FakeAnchorPage) -> None:
|
def __init__(self, page: _FakeAnchorPage) -> None:
|
||||||
self._page = page
|
self._page = page
|
||||||
|
|
@ -117,6 +148,7 @@ class _FakeAnchorContext:
|
||||||
|
|
||||||
async def new_page(self) -> _FakeAnchorPage:
|
async def new_page(self) -> _FakeAnchorPage:
|
||||||
self.pages.append(self._page)
|
self.pages.append(self._page)
|
||||||
|
self._page.context = self
|
||||||
return self._page
|
return self._page
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -153,8 +185,12 @@ def test_captcha_falls_back_to_plain_goto_without_referer() -> None:
|
||||||
assert origin_calls[0]["referer"] is None, "прежнее поведение — без referer вообще"
|
assert origin_calls[0]["referer"] is None, "прежнее поведение — без referer вообще"
|
||||||
|
|
||||||
|
|
||||||
def test_no_link_in_serp_falls_back_with_yandex_referer() -> None:
|
def test_no_link_in_serp_falls_back_without_referer() -> None:
|
||||||
"""Ссылки на хост в выдаче нет → goto(origin, referer=yandex.ru)."""
|
"""Ссылки на хост в выдаче нет → перехода не было → goto(origin) БЕЗ referer.
|
||||||
|
|
||||||
|
До #3258 этот случай подставлял referer=yandex.ru, хотя перехода с
|
||||||
|
Яндекса на origin фактически не было — намеренная смена поведения.
|
||||||
|
"""
|
||||||
page = _FakeAnchorPage(link_host=None)
|
page = _FakeAnchorPage(link_host=None)
|
||||||
|
|
||||||
anchored = _run_ensure_anchor("domclick", _ORIGIN, page)
|
anchored = _run_ensure_anchor("domclick", _ORIGIN, page)
|
||||||
|
|
@ -162,7 +198,7 @@ def test_no_link_in_serp_falls_back_with_yandex_referer() -> None:
|
||||||
assert anchored is True
|
assert anchored is True
|
||||||
origin_calls = [c for c in page.goto_calls if c["url"] == _ORIGIN]
|
origin_calls = [c for c in page.goto_calls if c["url"] == _ORIGIN]
|
||||||
assert len(origin_calls) == 1
|
assert len(origin_calls) == 1
|
||||||
assert origin_calls[0]["referer"] == "https://yandex.ru/"
|
assert origin_calls[0]["referer"] is None
|
||||||
|
|
||||||
|
|
||||||
def test_provider_not_in_allowlist_skips_search_entirely() -> None:
|
def test_provider_not_in_allowlist_skips_search_entirely() -> None:
|
||||||
|
|
@ -185,3 +221,37 @@ def test_yandex_navigation_failure_falls_back_and_anchor_still_raised() -> None:
|
||||||
assert anchored is True
|
assert anchored is True
|
||||||
assert server._anchor_pages["domclick"] is page
|
assert server._anchor_pages["domclick"] is page
|
||||||
assert page.goto_calls == [{"url": _ORIGIN, "referer": None}]
|
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, "попап не на том хосте должен быть закрыт"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue