fix: 稳定进入收货地址编辑页 (#179)
This commit is contained in:
@@ -363,6 +363,88 @@ class AddressFlowDevice(FakeDevice):
|
||||
return super().__call__(**selector)
|
||||
|
||||
|
||||
class LostFirstModifyClickAddressDevice(AddressFlowDevice):
|
||||
"""模拟第一次“修改”点击被页面吞掉。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.modify_clicks = 0
|
||||
|
||||
def click(self, x, y):
|
||||
if self.mode != "address_panel":
|
||||
return super().click(x, y)
|
||||
self.clicks.append((x, y))
|
||||
self.modify_clicks += 1
|
||||
if self.modify_clicks >= 2:
|
||||
self.mode = "address_edit"
|
||||
|
||||
|
||||
class NeverOpenAddressEditDevice(AddressFlowDevice):
|
||||
"""模拟两次“修改”点击都未打开编辑页。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.modify_clicks = 0
|
||||
|
||||
def click(self, x, y):
|
||||
if self.mode != "address_panel":
|
||||
return super().click(x, y)
|
||||
self.clicks.append((x, y))
|
||||
self.modify_clicks += 1
|
||||
|
||||
|
||||
class UnknownAfterModifyAddressDevice(AddressFlowDevice):
|
||||
"""模拟点击“修改”后进入无法识别的中间页面。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.modify_clicks = 0
|
||||
|
||||
def dump_hierarchy(self):
|
||||
if self.mode == "address_unknown":
|
||||
return """<hierarchy>
|
||||
<node package="com.xunmeng.pinduoduo" text="其他页面"
|
||||
bounds="[0,0][1080,2376]"/>
|
||||
</hierarchy>"""
|
||||
return super().dump_hierarchy()
|
||||
|
||||
def click(self, x, y):
|
||||
if self.mode != "address_panel":
|
||||
return super().click(x, y)
|
||||
self.clicks.append((x, y))
|
||||
self.modify_clicks += 1
|
||||
self.mode = "address_unknown"
|
||||
|
||||
|
||||
class AmbiguousAfterModifyAddressDevice(AddressFlowDevice):
|
||||
"""模拟点击后地址面板同时出现两个“修改”按钮。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.modify_clicks = 0
|
||||
|
||||
def dump_hierarchy(self):
|
||||
if self.mode == "address_ambiguous":
|
||||
xml_data = address_panel_xml(self.address)
|
||||
return xml_data.replace(
|
||||
"</node>\n </node>\n </hierarchy>",
|
||||
"""</node>
|
||||
<node clickable="true" enabled="true" bounds="[700,600][850,700]">
|
||||
<node text="修改" bounds="[730,620][820,680]"/>
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>""",
|
||||
)
|
||||
return super().dump_hierarchy()
|
||||
|
||||
def click(self, x, y):
|
||||
if self.mode != "address_panel":
|
||||
return super().click(x, y)
|
||||
self.clicks.append((x, y))
|
||||
self.modify_clicks += 1
|
||||
self.mode = "address_ambiguous"
|
||||
|
||||
|
||||
class U2PddPurchaseAdapterTest(unittest.TestCase):
|
||||
def _adapter(self, device, calls):
|
||||
def select_color_fn(_device, _xml, target, **_kwargs):
|
||||
@@ -381,7 +463,7 @@ class U2PddPurchaseAdapterTest(unittest.TestCase):
|
||||
select_size_fn=select_size_fn,
|
||||
)
|
||||
|
||||
def _live_adapter(self, device, calls):
|
||||
def _live_adapter(self, device, calls, **adapter_options):
|
||||
def select_color_fn(_device, _xml, target, **_kwargs):
|
||||
calls.append(("color", target))
|
||||
return target == "黑色"
|
||||
@@ -390,12 +472,16 @@ class U2PddPurchaseAdapterTest(unittest.TestCase):
|
||||
calls.append(("size", target))
|
||||
return target == "3XL【140-165斤】"
|
||||
|
||||
options = {
|
||||
"sleeper": lambda _seconds: None,
|
||||
"select_color_fn": select_color_fn,
|
||||
"select_size_fn": select_size_fn,
|
||||
}
|
||||
options.update(adapter_options)
|
||||
return U2PddLivePurchaseAdapter(
|
||||
"USB-001",
|
||||
device_service=PddDeviceService(connector=lambda _serial: device),
|
||||
sleeper=lambda _seconds: None,
|
||||
select_color_fn=select_color_fn,
|
||||
select_size_fn=select_size_fn,
|
||||
**options,
|
||||
)
|
||||
|
||||
def test_dry_run_reaches_confirmation_without_clicking_submit(self):
|
||||
@@ -548,6 +634,100 @@ class U2PddPurchaseAdapterTest(unittest.TestCase):
|
||||
self.assertEqual(state.price_cent, 503)
|
||||
adapter.close()
|
||||
|
||||
def test_address_edit_retries_once_when_first_modify_click_is_lost(self):
|
||||
device = LostFirstModifyClickAddressDevice()
|
||||
adapter = self._live_adapter(device, [])
|
||||
adapter.open_goods(GOODS_URL)
|
||||
adapter.select_options(
|
||||
{"color": "黑色", "size": "3XL【140-165斤】"}
|
||||
)
|
||||
|
||||
adapter.update_shipping_address("cg2")
|
||||
|
||||
self.assertEqual(device.modify_clicks, 2)
|
||||
self.assertEqual(device.address, "测试省测试市测试区测试路1号_cg2")
|
||||
self.assertEqual(device.mode, "panel")
|
||||
adapter.close()
|
||||
|
||||
def test_address_edit_stops_after_two_lost_modify_clicks(self):
|
||||
clock = FakeClock()
|
||||
device = NeverOpenAddressEditDevice()
|
||||
original_address = device.address
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
adapter = self._live_adapter(
|
||||
device,
|
||||
[],
|
||||
sleeper=clock.sleep,
|
||||
monotonic=clock.monotonic,
|
||||
panel_timeout=1.0,
|
||||
artifact_directory=Path(directory),
|
||||
)
|
||||
adapter.open_goods(GOODS_URL)
|
||||
adapter.select_options(
|
||||
{"color": "黑色", "size": "3XL【140-165斤】"}
|
||||
)
|
||||
|
||||
with self.assertRaises(PddPurchaseError) as raised:
|
||||
adapter.update_shipping_address("cg2")
|
||||
|
||||
self.assertEqual(
|
||||
raised.exception.code, "PURCHASE_ADDRESS_PAGE_TIMEOUT"
|
||||
)
|
||||
diagnostics = raised.exception.diagnostics
|
||||
self.assertEqual(diagnostics["expected_page"], "edit")
|
||||
self.assertEqual(diagnostics["observed_page"], "panel")
|
||||
self.assertEqual(diagnostics["click_attempts"], 2)
|
||||
self.assertEqual(diagnostics["candidate_count"], 1)
|
||||
self.assertEqual(device.modify_clicks, 2)
|
||||
self.assertEqual(device.address, original_address)
|
||||
artifact = diagnostics["artifacts"][0]
|
||||
content = Path(artifact["path"]).read_text(encoding="utf-8")
|
||||
self.assertNotIn(original_address, content)
|
||||
adapter.close()
|
||||
|
||||
def test_address_edit_does_not_retry_on_unknown_page(self):
|
||||
clock = FakeClock()
|
||||
device = UnknownAfterModifyAddressDevice()
|
||||
adapter = self._live_adapter(
|
||||
device,
|
||||
[],
|
||||
sleeper=clock.sleep,
|
||||
monotonic=clock.monotonic,
|
||||
panel_timeout=1.0,
|
||||
)
|
||||
adapter.open_goods(GOODS_URL)
|
||||
adapter.select_options(
|
||||
{"color": "黑色", "size": "3XL【140-165斤】"}
|
||||
)
|
||||
|
||||
with self.assertRaises(PddPurchaseError) as raised:
|
||||
adapter.update_shipping_address("cg2")
|
||||
|
||||
self.assertEqual(raised.exception.code, "PURCHASE_ADDRESS_PAGE_TIMEOUT")
|
||||
self.assertEqual(raised.exception.diagnostics["observed_page"], "unknown")
|
||||
self.assertEqual(raised.exception.diagnostics["click_attempts"], 1)
|
||||
self.assertEqual(device.modify_clicks, 1)
|
||||
adapter.close()
|
||||
|
||||
def test_address_edit_does_not_retry_when_modify_target_is_ambiguous(self):
|
||||
device = AmbiguousAfterModifyAddressDevice()
|
||||
adapter = self._live_adapter(device, [])
|
||||
adapter.open_goods(GOODS_URL)
|
||||
adapter.select_options(
|
||||
{"color": "黑色", "size": "3XL【140-165斤】"}
|
||||
)
|
||||
|
||||
with self.assertRaises(PddPurchaseError) as raised:
|
||||
adapter.update_shipping_address("cg2")
|
||||
|
||||
self.assertEqual(
|
||||
raised.exception.code, "PURCHASE_ADDRESS_EDIT_AMBIGUOUS"
|
||||
)
|
||||
self.assertEqual(raised.exception.diagnostics["candidate_count"], 2)
|
||||
self.assertEqual(raised.exception.diagnostics["click_attempts"], 1)
|
||||
self.assertEqual(device.modify_clicks, 1)
|
||||
adapter.close()
|
||||
|
||||
def test_reliable_pdd_tree_skips_repeated_app_current(self):
|
||||
device = FakeDevice()
|
||||
adapter = self._adapter(device, [])
|
||||
|
||||
Reference in New Issue
Block a user