fix(product): 千川任务中心账户选择页兜底与 jd_spus jsonb 查询修复
千川任务中心深链缺账户上下文时会被重定向到账户选择页,按 .account-list-item 卡片自动选中目标广告主后继续下载;jd_spus 列改 jsonb 后查询改用 jsonb_typeof/jsonb_array_length 防类型报错。
This commit is contained in:
1 parent
df11e195b7
commit
90282ae766
4 files changed
+248
-14
No files matched your search
@@ -4,6 +4,7 @@ from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
import collect_douyin_qianchuan_ads as collector
|
||||
import pytest
|
||||
|
||||
|
||||
def test_qianchuan_browser_uses_scrapling_stealth_and_persistent_state(
|
||||
@@ -216,3 +217,174 @@ def test_download_task_from_page_downloads_completed_matching_row() -> None:
|
||||
collector._matching_task_rows = original
|
||||
|
||||
assert actual is expected_download
|
||||
|
||||
|
||||
def test_wait_task_center_ready_selects_advertiser_after_redirect() -> None:
|
||||
clicked: list[str] = []
|
||||
state = {"on_selection": True}
|
||||
|
||||
class _AccountCard:
|
||||
def click(self) -> None:
|
||||
clicked.append("account")
|
||||
state["on_selection"] = False
|
||||
|
||||
class _FilteredLocator:
|
||||
first = _AccountCard()
|
||||
|
||||
def count(self) -> int:
|
||||
return 1
|
||||
|
||||
class _AccountListLocator:
|
||||
def count(self) -> int:
|
||||
return 1 if state["on_selection"] else 0
|
||||
|
||||
def filter(self, *, has_text: str):
|
||||
assert has_text == collector.ADVERTISER_ID
|
||||
return _FilteredLocator()
|
||||
|
||||
class _Page:
|
||||
def locator(self, selector: str) -> _AccountListLocator:
|
||||
assert selector == collector.ACCOUNT_ITEM_SELECTOR
|
||||
return _AccountListLocator()
|
||||
|
||||
def wait_for_timeout(self, ms: int) -> None:
|
||||
pass
|
||||
|
||||
def wait_for_function(self, expression: str, *, arg, timeout: int) -> None:
|
||||
assert not state["on_selection"]
|
||||
assert arg == collector.TASK_READY_MARKER
|
||||
|
||||
collector._wait_task_center_ready(_Page())
|
||||
|
||||
assert clicked == ["account"]
|
||||
|
||||
|
||||
def test_wait_task_center_ready_raises_when_selection_page_persists() -> None:
|
||||
class _AccountCard:
|
||||
def click(self) -> None:
|
||||
pass
|
||||
|
||||
class _FilteredLocator:
|
||||
first = _AccountCard()
|
||||
|
||||
def count(self) -> int:
|
||||
return 1
|
||||
|
||||
class _AccountListLocator:
|
||||
def count(self) -> int:
|
||||
return 1
|
||||
|
||||
def filter(self, *, has_text: str):
|
||||
assert has_text == collector.ADVERTISER_ID
|
||||
return _FilteredLocator()
|
||||
|
||||
class _Page:
|
||||
def locator(self, selector: str) -> _AccountListLocator:
|
||||
assert selector == collector.ACCOUNT_ITEM_SELECTOR
|
||||
return _AccountListLocator()
|
||||
|
||||
def wait_for_timeout(self, ms: int) -> None:
|
||||
pass
|
||||
|
||||
def wait_for_function(self, expression: str, *, arg, timeout: int) -> None:
|
||||
raise AssertionError("不应在账户选择页上等待任务中心标记")
|
||||
|
||||
with pytest.raises(
|
||||
collector.DouyinQianchuanDownloadError, match="仍停留在账户选择页"
|
||||
):
|
||||
collector._wait_task_center_ready(_Page())
|
||||
|
||||
|
||||
def test_wait_task_center_ready_raises_when_never_ready(monkeypatch) -> None:
|
||||
class _AccountListLocator:
|
||||
def count(self) -> int:
|
||||
return 0
|
||||
|
||||
class _Page:
|
||||
def locator(self, selector: str) -> _AccountListLocator:
|
||||
assert selector == collector.ACCOUNT_ITEM_SELECTOR
|
||||
return _AccountListLocator()
|
||||
|
||||
def wait_for_function(self, expression: str, *, arg, timeout: int) -> None:
|
||||
raise RuntimeError("Page.wait_for_function: Timeout 15000ms exceeded.")
|
||||
|
||||
def goto(self, url: str, **options: object) -> None:
|
||||
assert url == collector.TASK_CENTER_PAGE_URL
|
||||
|
||||
monkeypatch.setattr(collector, "is_browser_timeout_error", lambda exc: True)
|
||||
|
||||
with pytest.raises(
|
||||
collector.DouyinQianchuanDownloadError, match="未达到可用状态"
|
||||
):
|
||||
collector._wait_task_center_ready(_Page(), timeout_ms=50)
|
||||
|
||||
|
||||
def test_download_task_from_page_recovers_from_context_destroyed(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
expected_download = object()
|
||||
state = {"count_calls": 0}
|
||||
|
||||
class _DownloadInfo:
|
||||
value = expected_download
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, traceback) -> None:
|
||||
pass
|
||||
|
||||
class _DownloadButton:
|
||||
def click(self) -> None:
|
||||
pass
|
||||
|
||||
class _Row:
|
||||
def inner_text(self) -> str:
|
||||
return "全域推广数据-推商品\n生成完成\n下载"
|
||||
|
||||
def get_by_text(self, text: str, *, exact: bool) -> _DownloadButton:
|
||||
assert (text, exact) == ("下载", True)
|
||||
return _DownloadButton()
|
||||
|
||||
class _Rows:
|
||||
first = _Row()
|
||||
|
||||
def count(self) -> int:
|
||||
state["count_calls"] += 1
|
||||
if state["count_calls"] == 1:
|
||||
raise RuntimeError(
|
||||
"Locator.count: Execution context was destroyed, "
|
||||
"most likely because of a navigation."
|
||||
)
|
||||
return 1
|
||||
|
||||
class _Body:
|
||||
def inner_text(self, *, timeout: int) -> str:
|
||||
assert timeout == 10_000
|
||||
return collector.TASK_READY_MARKER
|
||||
|
||||
class _Page:
|
||||
url = "https://qianchuan.jinritemai.com/tools/task-center/download-task"
|
||||
|
||||
def expect_download(self, *, timeout: int) -> _DownloadInfo:
|
||||
assert timeout == 60_000
|
||||
return _DownloadInfo()
|
||||
|
||||
def locator(self, selector: str) -> _Body:
|
||||
assert selector == "body"
|
||||
return _Body()
|
||||
|
||||
monkeypatch.setattr(collector, "_matching_task_rows", lambda *args: _Rows())
|
||||
monkeypatch.setattr(
|
||||
collector, "_wait_task_center_ready", lambda page, **kwargs: None
|
||||
)
|
||||
|
||||
actual = collector._download_task_from_page(
|
||||
_Page(),
|
||||
date(2026, 9, 29),
|
||||
date(2026, 9, 29),
|
||||
wait_timeout_ms=0,
|
||||
)
|
||||
|
||||
assert actual is expected_download
|
||||
assert state["count_calls"] == 2
|
||||
Reference in new issue
Block a user