fix: 多地址面板选择默认地址 (#187)
This commit is contained in:
@@ -202,6 +202,68 @@ def _clickable_text_targets(root: ET.Element, exact_text: str) -> list[Bounds]:
|
||||
return sorted(targets)
|
||||
|
||||
|
||||
def _node_package(
|
||||
node: ET.Element, parents: Mapping[ET.Element, ET.Element]
|
||||
) -> str:
|
||||
"""返回节点自身或最近祖先声明的包名。"""
|
||||
|
||||
current: Optional[ET.Element] = node
|
||||
while current is not None:
|
||||
if package_name := current.get("package", "").strip():
|
||||
return package_name
|
||||
current = parents.get(current)
|
||||
return ""
|
||||
|
||||
|
||||
def _pdd_text_targets(root: ET.Element, exact_text: str) -> list[Bounds]:
|
||||
"""只返回 PDD 页面中指定文字的最近点击容器。"""
|
||||
|
||||
parents = _parent_nodes(root)
|
||||
targets = {
|
||||
bounds
|
||||
for node in root.iter("node")
|
||||
if exact_text
|
||||
in {
|
||||
node.get("text", "").strip(),
|
||||
node.get("content-desc", "").strip(),
|
||||
}
|
||||
if _node_package(node, parents) == PDD_PACKAGE_NAME
|
||||
if (bounds := _nearest_clickable_bounds(node, parents)) is not None
|
||||
}
|
||||
return sorted(targets)
|
||||
|
||||
|
||||
def _address_modify_targets(root: ET.Element) -> list[Bounds]:
|
||||
"""多地址时只返回唯一“已设默认”卡片内的修改按钮。"""
|
||||
|
||||
address_cards = [
|
||||
card
|
||||
for recycler in root.iter("node")
|
||||
if recycler.get("class") == "androidx.recyclerview.widget.RecyclerView"
|
||||
for card in list(recycler)
|
||||
if _pdd_text_targets(card, "修改")
|
||||
]
|
||||
if len(address_cards) <= 1:
|
||||
return _pdd_text_targets(root, "修改")
|
||||
|
||||
default_cards = [
|
||||
card for card in address_cards if "已设默认" in _labels(card)
|
||||
]
|
||||
if len(default_cards) != 1:
|
||||
return []
|
||||
return _pdd_text_targets(default_cards[0], "修改")
|
||||
|
||||
|
||||
def _pdd_title_back_targets(root: ET.Element) -> list[Bounds]:
|
||||
"""只返回 PDD 地址页标题栏左侧的返回目标。"""
|
||||
|
||||
return [
|
||||
bounds
|
||||
for bounds in _pdd_text_targets(root, "返回")
|
||||
if bounds[0] <= 240 and bounds[1] <= 320
|
||||
]
|
||||
|
||||
|
||||
def _address_entry_targets(root: ET.Element) -> list[Bounds]:
|
||||
"""通过脱敏手机号结构定位规格面板顶部的收货地址卡片。"""
|
||||
|
||||
@@ -266,9 +328,9 @@ def _is_reliable_address_panel(
|
||||
labels = _labels(root)
|
||||
if "收货地址" not in labels:
|
||||
return False
|
||||
if len(_clickable_text_targets(root, "修改")) != 1:
|
||||
if len(_address_modify_targets(root)) != 1:
|
||||
return False
|
||||
if len(_clickable_text_targets(root, "返回")) != 1:
|
||||
if len(_pdd_title_back_targets(root)) != 1:
|
||||
return False
|
||||
if expected_address and any(expected_address in label for label in labels):
|
||||
return True
|
||||
@@ -1344,7 +1406,7 @@ class U2PddLivePurchaseAdapter(U2PddPurchaseAdapter, PddLivePurchaseAdapter):
|
||||
saved_root, saved_page = self._wait_after_address_save(new_address)
|
||||
if saved_page == "confirmation":
|
||||
return
|
||||
back_targets = _clickable_text_targets(saved_root, "返回")
|
||||
back_targets = _pdd_title_back_targets(saved_root)
|
||||
self._click_unique_address_target(
|
||||
back_targets,
|
||||
"PURCHASE_ADDRESS_BACK_AMBIGUOUS",
|
||||
@@ -1394,7 +1456,7 @@ class U2PddLivePurchaseAdapter(U2PddPurchaseAdapter, PddLivePurchaseAdapter):
|
||||
return root
|
||||
|
||||
if observed_page == "panel":
|
||||
targets = _clickable_text_targets(root, "修改")
|
||||
targets = _address_modify_targets(root)
|
||||
candidate_count = len(targets)
|
||||
if candidate_count != 1:
|
||||
raise PddPurchaseError(
|
||||
|
||||
@@ -294,6 +294,56 @@ def address_panel_xml(address: str) -> str:
|
||||
</hierarchy>"""
|
||||
|
||||
|
||||
def multiple_address_panel_xml(
|
||||
address: str, *, default_indexes=(0,)
|
||||
) -> str:
|
||||
"""按真机多地址结构构造不含个人数据的最小控件树。"""
|
||||
|
||||
cards = []
|
||||
for index, (top, bottom) in enumerate(((226, 654), (654, 1083))):
|
||||
modify_top = 544 if index == 0 else 973
|
||||
marker = "已设默认" if index in default_indexes else "使用"
|
||||
marker_top = 547 if index == 0 else 817
|
||||
address_text = address if index in default_indexes else "备用测试地址"
|
||||
cards.append(f"""
|
||||
<node package="com.xunmeng.pinduoduo"
|
||||
class="android.view.ViewGroup" bounds="[0,{top}][1020,{bottom}]">
|
||||
<node package="com.xunmeng.pinduoduo" text="{address_text}"
|
||||
class="android.widget.TextView" bounds="[32,{top + 120}][825,{top + 240}]"/>
|
||||
<node package="com.xunmeng.pinduoduo" clickable="true"
|
||||
bounds="[32,{marker_top - 20}][249,{marker_top + 75}]">
|
||||
<node package="com.xunmeng.pinduoduo" text="{marker}"
|
||||
class="android.widget.TextView"
|
||||
bounds="[89,{marker_top}][249,{marker_top + 48}]"/>
|
||||
</node>
|
||||
<node package="com.xunmeng.pinduoduo" clickable="true"
|
||||
bounds="[908,{modify_top}][988,{modify_top + 54}]">
|
||||
<node package="com.xunmeng.pinduoduo" text="修改"
|
||||
class="android.widget.TextView"
|
||||
bounds="[908,{modify_top}][988,{modify_top + 54}]"/>
|
||||
</node>
|
||||
</node>""")
|
||||
return f"""<hierarchy>
|
||||
<node package="com.xunmeng.pinduoduo" bounds="[0,0][1080,2376]">
|
||||
<node package="com.xunmeng.pinduoduo" clickable="true"
|
||||
bounds="[0,102][217,226]">
|
||||
<node package="com.xunmeng.pinduoduo" text="返回"
|
||||
class="android.widget.TextView" bounds="[38,132][98,195]"/>
|
||||
</node>
|
||||
<node package="com.xunmeng.pinduoduo" text="收货地址"
|
||||
class="android.widget.TextView" bounds="[418,102][602,226]"/>
|
||||
<node package="com.xunmeng.pinduoduo"
|
||||
class="androidx.recyclerview.widget.RecyclerView"
|
||||
bounds="[0,226][1020,2140]">
|
||||
{''.join(cards)}
|
||||
</node>
|
||||
</node>
|
||||
<node package="com.android.systemui" content-desc="返回"
|
||||
class="android.widget.ImageView" clickable="true"
|
||||
bounds="[640,2140][802,2250]"/>
|
||||
</hierarchy>"""
|
||||
|
||||
|
||||
def address_edit_xml(address: str) -> str:
|
||||
return f"""<hierarchy>
|
||||
<node package="com.xunmeng.pinduoduo" bounds="[0,0][1080,2376]">
|
||||
@@ -377,6 +427,75 @@ class AddressFlowDevice(FakeDevice):
|
||||
return super().__call__(**selector)
|
||||
|
||||
|
||||
class MultipleAddressFlowDevice(AddressFlowDevice):
|
||||
"""模拟多地址面板,并记录是否点击默认卡片和 PDD 标题栏返回。"""
|
||||
|
||||
def __init__(self, default_indexes=(0,)) -> None:
|
||||
super().__init__()
|
||||
self.default_indexes = default_indexes
|
||||
self.address_modify_clicks = []
|
||||
self.address_back_clicks = []
|
||||
|
||||
def dump_hierarchy(self):
|
||||
if self.mode in {"address_panel", "address_saved"}:
|
||||
return multiple_address_panel_xml(
|
||||
self.address, default_indexes=self.default_indexes
|
||||
)
|
||||
return super().dump_hierarchy()
|
||||
|
||||
def click(self, x, y):
|
||||
if self.mode == "address_panel":
|
||||
self.clicks.append((x, y))
|
||||
self.address_modify_clicks.append((x, y))
|
||||
expected_y = 571 if self.default_indexes == (0,) else 1000
|
||||
if (x, y) == (948, expected_y):
|
||||
self.mode = "address_edit"
|
||||
return
|
||||
if self.mode == "address_saved":
|
||||
self.clicks.append((x, y))
|
||||
self.address_back_clicks.append((x, y))
|
||||
if x <= 240 and y <= 320:
|
||||
self.mode = "panel"
|
||||
return
|
||||
super().click(x, y)
|
||||
|
||||
|
||||
class AmbiguousDefaultAddressDevice(MultipleAddressFlowDevice):
|
||||
"""模拟默认地址卡片内部出现两个修改目标。"""
|
||||
|
||||
def dump_hierarchy(self):
|
||||
xml_data = super().dump_hierarchy()
|
||||
if self.mode != "address_panel":
|
||||
return xml_data
|
||||
root = ET.fromstring(xml_data)
|
||||
recycler = next(
|
||||
node
|
||||
for node in root.iter("node")
|
||||
if node.get("class")
|
||||
== "androidx.recyclerview.widget.RecyclerView"
|
||||
)
|
||||
default_card = list(recycler)[0]
|
||||
extra = ET.SubElement(
|
||||
default_card,
|
||||
"node",
|
||||
{
|
||||
"package": "com.xunmeng.pinduoduo",
|
||||
"clickable": "true",
|
||||
"bounds": "[800,544][880,598]",
|
||||
},
|
||||
)
|
||||
ET.SubElement(
|
||||
extra,
|
||||
"node",
|
||||
{
|
||||
"package": "com.xunmeng.pinduoduo",
|
||||
"text": "修改",
|
||||
"bounds": "[800,544][880,598]",
|
||||
},
|
||||
)
|
||||
return ET.tostring(root, encoding="unicode")
|
||||
|
||||
|
||||
class AddressSaveOverlayDevice(AddressFlowDevice):
|
||||
"""模拟保存后出现 ColorOS 密码本浮层。"""
|
||||
|
||||
@@ -730,6 +849,77 @@ class U2PddPurchaseAdapterTest(unittest.TestCase):
|
||||
self.assertEqual(state.price_cent, 503)
|
||||
adapter.close()
|
||||
|
||||
def test_multiple_addresses_modify_only_default_card(self):
|
||||
device = MultipleAddressFlowDevice(default_indexes=(0,))
|
||||
adapter = self._live_adapter(device, [])
|
||||
adapter.open_goods(GOODS_URL)
|
||||
adapter.select_options(
|
||||
{"color": "黑色", "size": "3XL【140-165斤】"}
|
||||
)
|
||||
|
||||
adapter.update_shipping_address("cg8")
|
||||
|
||||
self.assertEqual(device.address_modify_clicks, [(948, 571)])
|
||||
self.assertEqual(device.address_back_clicks, [(108, 164)])
|
||||
self.assertEqual(device.mode, "panel")
|
||||
adapter.close()
|
||||
|
||||
def test_default_address_can_be_second_card(self):
|
||||
device = MultipleAddressFlowDevice(default_indexes=(1,))
|
||||
adapter = self._live_adapter(device, [])
|
||||
adapter.open_goods(GOODS_URL)
|
||||
adapter.select_options(
|
||||
{"color": "黑色", "size": "3XL【140-165斤】"}
|
||||
)
|
||||
|
||||
adapter.update_shipping_address("cg8")
|
||||
|
||||
self.assertEqual(device.address_modify_clicks, [(948, 1000)])
|
||||
self.assertEqual(device.address_back_clicks, [(108, 164)])
|
||||
self.assertEqual(device.mode, "panel")
|
||||
adapter.close()
|
||||
|
||||
def test_multiple_addresses_without_unique_default_stop_safely(self):
|
||||
for default_indexes in ((), (0, 1)):
|
||||
with self.subTest(default_indexes=default_indexes):
|
||||
device = MultipleAddressFlowDevice(
|
||||
default_indexes=default_indexes
|
||||
)
|
||||
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("cg8")
|
||||
|
||||
self.assertEqual(
|
||||
raised.exception.code,
|
||||
"PURCHASE_ADDRESS_EDIT_AMBIGUOUS",
|
||||
)
|
||||
self.assertEqual(device.address_modify_clicks, [])
|
||||
self.assertEqual(device.address_back_clicks, [])
|
||||
adapter.close()
|
||||
|
||||
def test_default_address_with_multiple_modify_targets_stops_safely(self):
|
||||
device = AmbiguousDefaultAddressDevice()
|
||||
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("cg8")
|
||||
|
||||
self.assertEqual(
|
||||
raised.exception.code, "PURCHASE_ADDRESS_EDIT_AMBIGUOUS"
|
||||
)
|
||||
self.assertEqual(raised.exception.diagnostics["candidate_count"], 2)
|
||||
self.assertEqual(device.address_modify_clicks, [])
|
||||
adapter.close()
|
||||
|
||||
def test_saved_address_panel_can_hide_full_address(self):
|
||||
device = HiddenSavedAddressDevice()
|
||||
adapter = self._live_adapter(device, [])
|
||||
|
||||
Reference in New Issue
Block a user