"""分类树解析测试:基于真实页面状态样本""" import pytest from app.scraping.core import site from app.shared.errors import ScrapeParseError from app.scraping.parsers.genre import parse_genres from app.scraping.utils.urls import build_genre_url # ---- URL 构建 ---- def test_genre_url_uses_category_page_when_id_given(): assert build_genre_url("101205") == "https://www.rakuten.co.jp/category/101205/" def test_genre_url_falls_back_to_sentinel_search_for_top_level(): """顶层列表没有对应的分类页,改用查不到东西的哨兵关键词取干净的分类分面""" url = build_genre_url(None) assert url == f"https://search.rakuten.co.jp/search/mall/{site.GENRE_FACET_PROBE_KEYWORD}/" # ---- 顶层分类 ---- def test_root_returns_all_top_level_genres(genre_root_state): result = parse_genres(genre_root_state, genre_id=None) assert result.genre_id == "" assert result.ancestors == [] assert len(result.children) == 39 names = {child.name for child in result.children} assert "テレビゲーム" in names assert "レディースファッション" in names def test_root_children_expose_id_shortcut_and_url(genre_root_state): result = parse_genres(genre_root_state, genre_id=None) game = next(child for child in result.children if child.name == "テレビゲーム") assert game.genre_id == "101205" assert game.shortcut == "game" assert game.url == "https://www.rakuten.co.jp/category/101205/" def test_root_omits_item_count_because_it_is_query_scoped(genre_root_state): """站点给的是「当前查询在该分类下的命中数」,不是分类商品总量,不能透出""" result = parse_genres(genre_root_state, genre_id=None) assert all(child.item_count is None for child in result.children) # ---- 指定分类 ---- def test_genre_node_returns_itself_and_direct_children(genre_node_state): result = parse_genres(genre_node_state, genre_id="101205") assert result.genre_id == "101205" assert result.name == "テレビゲーム" assert result.is_leaf is False assert result.url == "https://www.rakuten.co.jp/category/101205/" assert len(result.children) == 27 child_names = {child.name for child in result.children} assert "Nintendo Switch" in child_names def test_genre_children_carry_item_count(genre_node_state): result = parse_genres(genre_node_state, genre_id="101205") switch = next(child for child in result.children if child.name == "Nintendo Switch") assert switch.genre_id == "565950" assert switch.item_count and switch.item_count > 0 def test_top_level_genre_has_no_ancestors_and_carries_genre_info(genre_node_state): result = parse_genres(genre_node_state, genre_id="101205") # 虚拟根 id=0 不是真实分类,不应出现在祖先里 assert result.ancestors == [] assert result.full_name == "テレビゲーム" assert result.description def test_nested_genre_reports_its_ancestor_chain(genre_node_state): """深层分类要能回报从顶层到父级的路径,方便上游拼面包屑""" root = genre_node_state["state"]["data"]["genreTree"]["parent_category"] game = root["children"][0] switch = next(child for child in game["children"] if child["id"] == 565950) # 模拟站点返回深层分类页时的链式结构:只保留通往目标的那条分支 game["children"] = [switch] switch["children"] = [{"id": 566404, "name": "ソフト", "count": 100, "leaf": True, "children": []}] result = parse_genres(genre_node_state, genre_id="565950") assert result.genre_id == "565950" assert [(a.genre_id, a.name) for a in result.ancestors] == [("101205", "テレビゲーム")] assert [c.genre_id for c in result.children] == ["566404"] def test_leaf_genre_reports_no_children(genre_node_state): root = genre_node_state["state"]["data"]["genreTree"]["parent_category"] root["children"] = [{"id": 999999, "name": "葉", "count": 5, "leaf": True, "children": []}] result = parse_genres(genre_node_state, genre_id="999999") assert result.is_leaf is True assert result.children == [] # ---- 错误处理 ---- def test_missing_genre_tree_is_a_parse_error(): with pytest.raises(ScrapeParseError): parse_genres({"state": {"data": {}}}, genre_id=None) def test_unknown_genre_id_is_a_parse_error(genre_node_state): with pytest.raises(ScrapeParseError): parse_genres(genre_node_state, genre_id="1")