feat: 接入采购演练真机适配器 (#75)
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
"""uiautomator2 采购演练 Adapter 测试;不连接真实手机。"""
|
||||
|
||||
import unittest
|
||||
|
||||
from src.pdd_device_service import PddDeviceService
|
||||
from src.pdd_purchase_adapter import PddPurchaseError
|
||||
from src.pdd_u2_purchase_adapter import U2PddPurchaseAdapter
|
||||
|
||||
|
||||
GOODS_URL = "https://mobile.yangkeduo.com/goods.html?goods_id=753136429979"
|
||||
|
||||
|
||||
def home_xml() -> str:
|
||||
return """<hierarchy>
|
||||
<node package="com.xunmeng.pinduoduo" bounds="[0,0][1080,2376]">
|
||||
<node clickable="true" enabled="true" visible-to-user="true"
|
||||
bounds="[500,2100][1080,2328]">
|
||||
<node text="立即购买 ¥5.03" enabled="true" visible-to-user="true"
|
||||
bounds="[600,2150][1000,2280]"/>
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>"""
|
||||
|
||||
|
||||
def panel_xml(quantity: int = 1) -> str:
|
||||
return f"""<hierarchy>
|
||||
<node package="com.xunmeng.pinduoduo" bounds="[0,0][1080,2376]">
|
||||
<node text="已选: 黑色 3XL【140-165斤】" bounds="[300,500][1000,560]"/>
|
||||
<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,400][600,470]"/>
|
||||
<node class="android.widget.EditText" text="{quantity}"
|
||||
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" bounds="[0,2181][1080,2328]">
|
||||
<node text="提交订单 ¥5.03" bounds="[380,2220][700,2290]"/>
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>"""
|
||||
|
||||
|
||||
class FakeEditor:
|
||||
def __init__(self, device) -> None:
|
||||
self.device = device
|
||||
self.count = 1
|
||||
|
||||
def set_text(self, value: str) -> None:
|
||||
self.device.quantity = int(value)
|
||||
|
||||
|
||||
class FakeDevice:
|
||||
def __init__(self, initial_xml: str = "") -> None:
|
||||
self.mode = "special" if initial_xml else "home"
|
||||
self.special_xml = initial_xml
|
||||
self.quantity = 1
|
||||
self.clicks = []
|
||||
self.opened_urls = []
|
||||
|
||||
def app_current(self):
|
||||
return {"package": "com.xunmeng.pinduoduo"}
|
||||
|
||||
def app_start(self, _package):
|
||||
pass
|
||||
|
||||
def app_wait(self, _package, timeout=10):
|
||||
return True
|
||||
|
||||
def open_url(self, url):
|
||||
self.opened_urls.append(url)
|
||||
|
||||
def dump_hierarchy(self):
|
||||
if self.mode == "special":
|
||||
return self.special_xml
|
||||
return home_xml() if self.mode == "home" else panel_xml(self.quantity)
|
||||
|
||||
def click(self, x, y):
|
||||
self.clicks.append((x, y))
|
||||
self.mode = "panel"
|
||||
|
||||
def __call__(self, **selector):
|
||||
if selector == {"className": "android.widget.EditText"}:
|
||||
return FakeEditor(self)
|
||||
raise AssertionError(f"未预期的 selector: {selector}")
|
||||
|
||||
|
||||
class U2PddPurchaseAdapterTest(unittest.TestCase):
|
||||
def _adapter(self, device, calls):
|
||||
def select_color_fn(_device, _xml, target, **_kwargs):
|
||||
calls.append(("color", target))
|
||||
return target == "黑色"
|
||||
|
||||
def select_size_fn(_device, _xml, target, **_kwargs):
|
||||
calls.append(("size", target))
|
||||
return target == "3XL【140-165斤】"
|
||||
|
||||
return U2PddPurchaseAdapter(
|
||||
"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,
|
||||
)
|
||||
|
||||
def test_dry_run_reaches_confirmation_without_clicking_submit(self):
|
||||
device = FakeDevice()
|
||||
calls = []
|
||||
adapter = self._adapter(device, calls)
|
||||
|
||||
adapter.open_goods(GOODS_URL)
|
||||
goods = adapter.read_state()
|
||||
adapter.select_options(
|
||||
{"color": "黑色", "size": "3XL【140-165斤】"}
|
||||
)
|
||||
adapter.set_quantity(2)
|
||||
confirmation = adapter.read_state()
|
||||
adapter.enter_confirmation()
|
||||
adapter.stop_before_submit()
|
||||
adapter.close()
|
||||
|
||||
self.assertEqual(goods.page_kind, "goods")
|
||||
self.assertEqual(goods.goods_id, "753136429979")
|
||||
self.assertEqual(
|
||||
confirmation.selected_options,
|
||||
{"color": "黑色", "size": "3XL【140-165斤】"},
|
||||
)
|
||||
self.assertEqual(confirmation.quantity, 2)
|
||||
self.assertEqual(confirmation.price_cent, 503)
|
||||
self.assertEqual(confirmation.page_kind, "order_confirmation")
|
||||
self.assertEqual(calls, [("color", "黑色"), ("size", "3XL【140-165斤】")])
|
||||
# 只点击一次商品页采购入口;最终“提交订单”从不点击。
|
||||
self.assertEqual(len(device.clicks), 1)
|
||||
|
||||
def test_unsupported_dynamic_option_stops_before_click(self):
|
||||
device = FakeDevice()
|
||||
adapter = self._adapter(device, [])
|
||||
adapter.open_goods(GOODS_URL)
|
||||
|
||||
with self.assertRaises(PddPurchaseError) as raised:
|
||||
adapter.select_options({"bundle": "标准版"})
|
||||
|
||||
self.assertEqual(raised.exception.code, "PURCHASE_OPTIONS_UNSUPPORTED")
|
||||
self.assertEqual(device.clicks, [])
|
||||
adapter.close()
|
||||
|
||||
def test_captcha_stops_without_waiting_or_clicking(self):
|
||||
device = FakeDevice(
|
||||
'<hierarchy><node package="com.xunmeng.pinduoduo" '
|
||||
'text="请完成验证" bounds="[0,0][1080,2376]"/></hierarchy>'
|
||||
)
|
||||
adapter = self._adapter(device, [])
|
||||
|
||||
with self.assertRaises(PddPurchaseError) as raised:
|
||||
adapter.open_goods(GOODS_URL)
|
||||
|
||||
self.assertEqual(raised.exception.code, "PDD_PAGE_CAPTCHA")
|
||||
self.assertEqual(device.clicks, [])
|
||||
adapter.close()
|
||||
|
||||
def test_invalid_non_pdd_url_is_rejected_before_connect(self):
|
||||
device = FakeDevice()
|
||||
adapter = self._adapter(device, [])
|
||||
|
||||
with self.assertRaises(PddPurchaseError) as raised:
|
||||
adapter.open_goods("https://example.test/goods?goods_id=1")
|
||||
|
||||
self.assertEqual(raised.exception.code, "PURCHASE_GOODS_URL_INVALID")
|
||||
self.assertEqual(device.opened_urls, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -21,6 +21,7 @@ from src.pdd_ui_event import (
|
||||
)
|
||||
from src.mock_admin_gateway import MockAdminGateway
|
||||
from src.pdd_purchase_adapter import PddPurchaseAdapter, PurchasePageState
|
||||
from src.pdd_u2_purchase_adapter import create_u2_purchase_adapter
|
||||
from src.settings_repository import SettingsRepository
|
||||
from src.task_models import NewClaimedTask, TaskStatus, TaskSummary, TaskType
|
||||
from src.task_repository import TaskRepository
|
||||
@@ -461,6 +462,10 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertIsInstance(window.pddTaskPageEvent, PDDTaskPageEvent)
|
||||
self.assertIs(
|
||||
window.pddTaskPageEvent._purchase_adapter_factory,
|
||||
create_u2_purchase_adapter,
|
||||
)
|
||||
self.assertEqual(window.pddTaskPage.taskModel.data_row_count(), 0)
|
||||
window.close()
|
||||
window.deleteLater()
|
||||
|
||||
Reference in New Issue
Block a user