fix: 兼容款式与套餐规格分组 (#166)

This commit is contained in:
chengma
2026-08-11 17:07:17 +08:00
parent f5d91cd707
commit 9c7e715489
5 changed files with 204 additions and 9 deletions
+141
View File
@@ -21,6 +21,31 @@ from src.pdd_device_service import PddDeviceService
FIXTURES = Path(__file__).parent / "fixtures"
def style_package_spec_xml() -> str:
"""模拟口罩商品的“款式 + 套餐”规格面板。"""
return """<hierarchy>
<node class="android.widget.FrameLayout" bounds="[0,0][1080,2340]">
<node class="android.widget.ScrollView" scrollable="true"
bounds="[0,600][1080,2200]">
<node text="券后 ¥1.68" bounds="[300,650][600,710]" />
<node text="已选:成人款 单个装" bounds="[260,720][1000,780]" />
<node text="款式" bounds="[36,800][180,860]" />
<node text="成人款" clickable="true" selected="true"
bounds="[36,900][300,1000]" />
<node text="儿童款" clickable="true" selected="false"
bounds="[330,900][594,1000]" />
<node text="套餐" bounds="[36,1100][180,1160]" />
<node text="单个装" clickable="true" selected="true"
bounds="[36,1200][300,1300]" />
<node text="两个装" clickable="true" selected="false"
bounds="[330,1200][594,1300]" />
<node text="确定" clickable="true" bounds="[0,2000][1080,2150]" />
</node>
</node>
</hierarchy>"""
@dataclass(frozen=True)
class FakeTask:
goods_url: str
@@ -428,6 +453,13 @@ class DefaultSizeDevice(SnakeColorDevice):
return xml_data
class DefaultPackageDevice(DefaultSizeDevice):
"""“套餐”作为第二规格时沿用安全的默认选择取消流程。"""
def _spec_xml(self):
return super()._spec_xml().replace('text="尺码"', 'text="套餐"')
class FakeClock:
"""测试用时钟:sleep 只推进虚拟时间,不真的等待。"""
@@ -538,6 +570,97 @@ class PddCollectParserTest(unittest.TestCase):
["黑色", "红色", "蓝色", "紫色", "浅粉", "本色"],
)
def test_style_and_package_are_separate_color_and_size_dimensions(self):
result = parse_spec_panel(style_package_spec_xml())
self.assertEqual(
[(item.key, item.name) for item in result.dimensions],
[("color", "款式"), ("size", "套餐")],
)
self.assertEqual(
[value.text for value in result.dimensions[0].values],
["成人款", "儿童款"],
)
self.assertEqual(
[value.text for value in result.dimensions[1].values],
["单个装", "两个装"],
)
def test_package_only_is_primary_dimension(self):
root = ET.fromstring(style_package_spec_xml())
for parent in root.iter():
for child in list(parent):
if child.get("text") in {"款式", "成人款", "儿童款"}:
parent.remove(child)
result = parse_spec_panel(ET.tostring(root, encoding="unicode"))
self.assertEqual(
[(item.key, item.name) for item in result.dimensions],
[("color", "套餐")],
)
def test_third_dimension_is_not_silently_merged(self):
xml_data = style_package_spec_xml().replace(
'<node text="确定" clickable="true" bounds="[0,2000][1080,2150]" />',
'<node text="尺码" bounds="[36,1400][180,1460]" />\n'
'<node text="均码" clickable="true" selected="false" '
'bounds="[36,1500][300,1600]" />\n'
'<node text="确定" clickable="true" bounds="[0,2000][1080,2150]" />',
)
parsed = parse_spec_panel(xml_data)
self.assertEqual(
[item.key for item in parsed.dimensions],
["color", "size", "size_2"],
)
device = FakeCollectDevice(self.home_xml, xml_data)
device.panel_open = True
device.opened_url = "https://mobile.yangkeduo.com/goods.html?goods_id=123"
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
"client-001",
sleeper=lambda _seconds: None,
max_spec_swipes=0,
)
with self.assertRaises(PddCollectError) as raised:
service._collect_size_dimension(device)
self.assertEqual(raised.exception.code, "PDD_DATA_SPEC_UNSUPPORTED")
def test_collect_preserves_style_and_package_names(self):
xml_data = style_package_spec_xml().replace(
'<node text="儿童款" clickable="true" selected="false"\n'
' bounds="[330,900][594,1000]" />',
"",
).replace(
'<node text="两个装" clickable="true" selected="false"\n'
' bounds="[330,1200][594,1300]" />',
"",
)
device = FakeCollectDevice(self.home_xml, xml_data)
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
"client-001",
sleeper=lambda _seconds: None,
max_spec_swipes=0,
)
result = service.collect(
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
)
self.assertEqual(
[(item.key, item.name) for item in result.dimensions],
[("color", "款式"), ("size", "套餐")],
)
self.assertEqual(
result.skus[0].options,
{"color": "成人款", "size": "单个装"},
)
def test_parse_non_scrollable_spec_panel(self):
result = parse_spec_panel(self.non_scrollable_spec_xml)
@@ -1030,6 +1153,24 @@ class PddCollectParserTest(unittest.TestCase):
self.assertEqual(1, device.size_clicks)
self.assertTrue(device.size_selected)
def test_default_package_is_cleared_and_keeps_original_heading(self):
device = DefaultPackageDevice(self.home_xml)
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
"client-001",
sleeper=lambda _seconds: None,
)
data = service.collect(
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
).to_pdd_data()
self.assertEqual(1, device.size_clicks)
self.assertFalse(device.size_selected)
self.assertEqual(data["dimensions"][1]["key"], "size")
self.assertEqual(data["dimensions"][1]["name"], "套餐")
def test_visible_click_is_fallback_when_page_exposes_no_selection_state(self):
xml_data = keep_only_one_sku(self.spec_xml)
root = ET.fromstring(xml_data)