feat: 正式采购下单前更新地址标记 (#173)
This commit is contained in:
@@ -13,6 +13,7 @@ from src.pdd_u2_purchase_adapter import (
|
||||
U2PddPurchaseAdapter,
|
||||
_final_submit_targets,
|
||||
_page_kind,
|
||||
_tagged_shipping_address,
|
||||
)
|
||||
|
||||
|
||||
@@ -239,6 +240,114 @@ class ContextualConfirmPanelDevice(FakeDevice):
|
||||
self.mode = "panel"
|
||||
|
||||
|
||||
def address_confirmation_xml(address: str) -> str:
|
||||
return f"""<hierarchy>
|
||||
<node package="com.xunmeng.pinduoduo" bounds="[0,0][1080,2376]">
|
||||
<node clickable="true" enabled="true" visible-to-user="true"
|
||||
bounds="[0,300][1080,520]">
|
||||
<node text="测试用户,000****0000" bounds="[300,330][800,380]"/>
|
||||
<node text="{address}" bounds="[120,390][1000,470]"/>
|
||||
</node>
|
||||
<node text="已选: 黑色 3XL【140-165斤】" bounds="[300,550][1000,610]"/>
|
||||
<node content-desc="黑色" selected="true" bounds="[30,900][330,1000]"/>
|
||||
<node text="3XL【140-165斤】" selected="true" bounds="[40,1200][500,1300]"/>
|
||||
<node text="折后¥5.03" bounds="[300,530][600,590]"/>
|
||||
<node class="android.widget.EditText" text="1" bounds="[480,700][560,780]"/>
|
||||
<node content-desc="减少数量" clickable="true" bounds="[390,700][470,780]"/>
|
||||
<node content-desc="增加数量" clickable="true" bounds="[570,700][650,780]"/>
|
||||
<node clickable="true" enabled="true" visible-to-user="true"
|
||||
bounds="[0,2181][1080,2328]">
|
||||
<node text="提交订单 ¥5.03" enabled="true" visible-to-user="true"
|
||||
bounds="[380,2220][700,2290]"/>
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>"""
|
||||
|
||||
|
||||
def address_panel_xml(address: str) -> str:
|
||||
return f"""<hierarchy>
|
||||
<node package="com.xunmeng.pinduoduo" bounds="[0,0][1080,2376]">
|
||||
<node clickable="true" enabled="true" bounds="[0,120][240,258]">
|
||||
<node text="返回" bounds="[42,154][108,223]"/>
|
||||
</node>
|
||||
<node text="收货地址" bounds="[438,120][642,258]"/>
|
||||
<node text="{address}" bounds="[36,400][900,590]"/>
|
||||
<node clickable="true" enabled="true" bounds="[900,600][1040,700]">
|
||||
<node text="修改" bounds="[930,620][1020,680]"/>
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>"""
|
||||
|
||||
|
||||
def address_edit_xml(address: str) -> str:
|
||||
return f"""<hierarchy>
|
||||
<node package="com.xunmeng.pinduoduo" bounds="[0,0][1080,2376]">
|
||||
<node text="修改收货地址" bounds="[380,350][700,450]"/>
|
||||
<node text="收货人" bounds="[120,550][260,610]"/>
|
||||
<node class="android.widget.EditText" text="测试用户"
|
||||
enabled="true" visible-to-user="true" bounds="[327,510][906,654]"/>
|
||||
<node text="手机号码" bounds="[120,700][260,760]"/>
|
||||
<node class="android.widget.EditText" text="00000000000"
|
||||
enabled="true" visible-to-user="true" bounds="[327,654][906,798]"/>
|
||||
<node text="详细地址" bounds="[120,990][300,1043]"/>
|
||||
<node class="android.widget.EditText" text="{address}"
|
||||
enabled="true" visible-to-user="true" bounds="[327,990][813,1116]"/>
|
||||
<node clickable="true" enabled="true" bounds="[120,1194][960,1329]">
|
||||
<node text="保存" bounds="[489,1232][591,1291]"/>
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>"""
|
||||
|
||||
|
||||
class FakeAddressEditor:
|
||||
def __init__(self, device, matched: bool) -> None:
|
||||
self.device = device
|
||||
self.count = 1 if matched else 0
|
||||
|
||||
def set_text(self, value: str) -> None:
|
||||
self.device.address = value
|
||||
|
||||
|
||||
class AddressFlowDevice(FakeDevice):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.address = "测试省测试市测试区测试路1号-1dd212"
|
||||
|
||||
def dump_hierarchy(self):
|
||||
if not self.has_opened:
|
||||
return '<hierarchy><node package="com.xunmeng.pinduoduo" /></hierarchy>'
|
||||
if self.mode == "home":
|
||||
return home_xml()
|
||||
if self.mode == "panel":
|
||||
return address_confirmation_xml(self.address)
|
||||
if self.mode in {"address_panel", "address_saved"}:
|
||||
return address_panel_xml(self.address)
|
||||
if self.mode == "address_edit":
|
||||
return address_edit_xml(self.address)
|
||||
raise AssertionError(f"未知测试页面:{self.mode}")
|
||||
|
||||
def click(self, x, y):
|
||||
self.clicks.append((x, y))
|
||||
if self.mode == "home":
|
||||
self.mode = "panel"
|
||||
elif self.mode == "panel" and y < 600:
|
||||
self.mode = "address_panel"
|
||||
elif self.mode == "address_panel":
|
||||
self.mode = "address_edit"
|
||||
elif self.mode == "address_edit":
|
||||
self.mode = "address_saved"
|
||||
elif self.mode == "address_saved":
|
||||
self.mode = "panel"
|
||||
|
||||
def __call__(self, **selector):
|
||||
if (
|
||||
selector.get("className") == "android.widget.EditText"
|
||||
and "text" in selector
|
||||
):
|
||||
return FakeAddressEditor(self, selector["text"] == self.address)
|
||||
return super().__call__(**selector)
|
||||
|
||||
|
||||
class U2PddPurchaseAdapterTest(unittest.TestCase):
|
||||
def _adapter(self, device, calls):
|
||||
def select_color_fn(_device, _xml, target, **_kwargs):
|
||||
@@ -383,6 +492,47 @@ class U2PddPurchaseAdapterTest(unittest.TestCase):
|
||||
self.assertEqual(device.clicks[-1], (540, 2140))
|
||||
adapter.close()
|
||||
|
||||
def test_program_address_suffix_is_replaced_without_cutting_body(self):
|
||||
self.assertEqual(
|
||||
_tagged_shipping_address(
|
||||
"测试省测试市测试区测试路1号-1dd212", "cg2"
|
||||
),
|
||||
"测试省测试市测试区测试路1号_cg2",
|
||||
)
|
||||
self.assertEqual(
|
||||
_tagged_shipping_address(
|
||||
"测试省测试市测试区测试路1号_cg2", "cg3"
|
||||
),
|
||||
"测试省测试市测试区测试路1号_cg3",
|
||||
)
|
||||
self.assertEqual(
|
||||
_tagged_shipping_address(
|
||||
"测试省-开发区测试路1号", "cg4"
|
||||
),
|
||||
"测试省-开发区测试路1号_cg4",
|
||||
)
|
||||
|
||||
def test_live_adapter_updates_address_and_returns_to_confirmation(self):
|
||||
device = AddressFlowDevice()
|
||||
adapter = self._live_adapter(device, [])
|
||||
adapter.open_goods(GOODS_URL)
|
||||
adapter.select_options(
|
||||
{"color": "黑色", "size": "3XL【140-165斤】"}
|
||||
)
|
||||
adapter.set_quantity(1)
|
||||
|
||||
adapter.update_shipping_address("cg2")
|
||||
state = adapter.read_state()
|
||||
|
||||
self.assertEqual(
|
||||
device.address, "测试省测试市测试区测试路1号_cg2"
|
||||
)
|
||||
self.assertEqual(device.mode, "panel")
|
||||
self.assertEqual(state.page_kind, "order_confirmation")
|
||||
self.assertEqual(state.quantity, 1)
|
||||
self.assertEqual(state.price_cent, 503)
|
||||
adapter.close()
|
||||
|
||||
def test_reliable_pdd_tree_skips_repeated_app_current(self):
|
||||
device = FakeDevice()
|
||||
adapter = self._adapter(device, [])
|
||||
|
||||
@@ -92,11 +92,27 @@ class RecordingDryRunAdapter(PddPurchaseAdapter):
|
||||
class RecordingLiveAdapter(RecordingDryRunAdapter, PddLivePurchaseAdapter):
|
||||
"""只记录一次提交调用的 live 测试 Adapter。"""
|
||||
|
||||
def __init__(self, *, submit_error: bool = False, **kwargs) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
submit_error: bool = False,
|
||||
address_error: bool = False,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.submit_error = submit_error
|
||||
self.address_error = address_error
|
||||
self.submit_count = 0
|
||||
|
||||
def update_shipping_address(self, purchase_number: str) -> None:
|
||||
self.calls.append(("update_shipping_address", purchase_number))
|
||||
if self.address_error:
|
||||
raise PddPurchaseError(
|
||||
"PURCHASE_ADDRESS_PAGE_TIMEOUT",
|
||||
"地址页面切换超时",
|
||||
step="purchase_update_shipping_address",
|
||||
)
|
||||
|
||||
def submit_order_once(self) -> None:
|
||||
self.submit_count += 1
|
||||
self.calls.append(("submit_order_once",))
|
||||
@@ -324,6 +340,7 @@ class PurchaseTaskServiceTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(outcome.kind, "reconcile_pending")
|
||||
self.assertEqual(adapter.submit_count, 1)
|
||||
self.assertIn(("update_shipping_address", "PUR-001"), adapter.calls)
|
||||
detail = self.repository.get_task("PUR-001")
|
||||
run = self.repository.latest_task_run("PUR-001")
|
||||
self.assertEqual(detail.status, TaskStatus.MANUAL_REVIEW)
|
||||
@@ -344,6 +361,21 @@ class PurchaseTaskServiceTest(unittest.TestCase):
|
||||
run = self.repository.latest_task_run("PUR-001")
|
||||
self.assertIsNone(run.irreversible_action_at)
|
||||
|
||||
def test_live_address_failure_stops_before_irreversible(self):
|
||||
self._prepare_task(execution_mode="live")
|
||||
adapter = RecordingLiveAdapter(address_error=True)
|
||||
|
||||
outcome = self._service(adapter).execute_one_local()
|
||||
|
||||
self.assertEqual(outcome.kind, "task_failed")
|
||||
self.assertEqual(adapter.submit_count, 0)
|
||||
run = self.repository.latest_task_run("PUR-001")
|
||||
self.assertIsNone(run.irreversible_action_at)
|
||||
detail = self.repository.get_task("PUR-001")
|
||||
self.assertEqual(
|
||||
detail.last_error_code, "PURCHASE_ADDRESS_PAGE_TIMEOUT"
|
||||
)
|
||||
|
||||
def test_live_submit_error_never_retries_and_still_enters_reconcile(self):
|
||||
self._prepare_task(execution_mode="live")
|
||||
adapter = RecordingLiveAdapter(submit_error=True)
|
||||
|
||||
@@ -76,6 +76,9 @@ class ReadyPurchaseAdapter(PddPurchaseAdapter):
|
||||
|
||||
|
||||
class ReadyLivePurchaseAdapter(ReadyPurchaseAdapter, PddLivePurchaseAdapter):
|
||||
def update_shipping_address(self, purchase_number):
|
||||
self.calls.append(("purchase", "address", purchase_number))
|
||||
|
||||
def read_state(self):
|
||||
state = super().read_state()
|
||||
return PurchasePageState(
|
||||
|
||||
Reference in New Issue
Block a user