fix: 兼容款式与套餐规格分组 (#166)
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user