fix: 安全兼容采购规格繁简匹配 (#251)
This commit is contained in:
@@ -53,6 +53,7 @@ from .util.select_color_size import (
|
|||||||
color_selection_failure_reason,
|
color_selection_failure_reason,
|
||||||
select_color,
|
select_color,
|
||||||
select_size,
|
select_size,
|
||||||
|
size_selection_failure_reason,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1048,6 +1049,9 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
|
|||||||
diagnostics["artifacts"] = [artifact]
|
diagnostics["artifacts"] = [artifact]
|
||||||
messages = {
|
messages = {
|
||||||
"target_not_visible": f"没有找到目标颜色:{color}",
|
"target_not_visible": f"没有找到目标颜色:{color}",
|
||||||
|
"target_ambiguous": (
|
||||||
|
f"目标颜色存在多个繁简等价候选,已停止采购:{color}"
|
||||||
|
),
|
||||||
"safe_target_missing": (
|
"safe_target_missing": (
|
||||||
f"目标颜色没有完整可见的安全点击位置:{color}"
|
f"目标颜色没有完整可见的安全点击位置:{color}"
|
||||||
),
|
),
|
||||||
@@ -1074,10 +1078,27 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
|
|||||||
device, latest_xml, size, action_delay=0.2
|
device, latest_xml, size, action_delay=0.2
|
||||||
)
|
)
|
||||||
if not size_selected:
|
if not size_selected:
|
||||||
|
failed_xml = self._dump_hierarchy()
|
||||||
|
failure_reason = size_selection_failure_reason(
|
||||||
|
failed_xml, size
|
||||||
|
)
|
||||||
|
messages = {
|
||||||
|
"target_not_visible": f"没有找到目标尺码:{size}",
|
||||||
|
"target_ambiguous": (
|
||||||
|
f"目标尺码存在多个繁简等价候选,已停止采购:{size}"
|
||||||
|
),
|
||||||
|
"safe_target_missing": (
|
||||||
|
f"目标尺码没有可靠的安全点击位置:{size}"
|
||||||
|
),
|
||||||
|
"selection_unconfirmed": (
|
||||||
|
f"点击尺码后页面没有确认已选中:{size}"
|
||||||
|
),
|
||||||
|
}
|
||||||
raise PddPurchaseError(
|
raise PddPurchaseError(
|
||||||
"PURCHASE_OPTIONS_MISMATCH",
|
"PURCHASE_OPTIONS_MISMATCH",
|
||||||
f"没有精确选中尺码:{size}",
|
messages[failure_reason],
|
||||||
step="purchase_select_options",
|
step="purchase_select_options",
|
||||||
|
diagnostics={"selection_failure": failure_reason},
|
||||||
)
|
)
|
||||||
except PddPurchaseError:
|
except PddPurchaseError:
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import ctypes
|
||||||
|
from functools import lru_cache
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import unicodedata
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
from ctypes import wintypes
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
|
|
||||||
@@ -10,6 +15,7 @@ Bounds = tuple[int, int, int, int]
|
|||||||
_BOUNDS_PATTERN = re.compile(r"^\[(\d+),(\d+)\]\[(\d+),(\d+)\]$")
|
_BOUNDS_PATTERN = re.compile(r"^\[(\d+),(\d+)\]\[(\d+),(\d+)\]$")
|
||||||
_COLOR_HEADINGS = ("颜色分类", "颜色", "款式", "颜色款式", "花色")
|
_COLOR_HEADINGS = ("颜色分类", "颜色", "款式", "颜色款式", "花色")
|
||||||
_PACKAGE_HEADINGS = ("套餐",)
|
_PACKAGE_HEADINGS = ("套餐",)
|
||||||
|
_LCMAP_SIMPLIFIED_CHINESE = 0x02000000
|
||||||
|
|
||||||
|
|
||||||
def _parse_xml(xml_data: XmlData) -> ET.Element:
|
def _parse_xml(xml_data: XmlData) -> ET.Element:
|
||||||
@@ -41,21 +47,97 @@ def _node_labels(node: ET.Element) -> tuple[str, str]:
|
|||||||
return node.get("text", "").strip(), node.get("content-desc", "").strip()
|
return node.get("text", "").strip(), node.get("content-desc", "").strip()
|
||||||
|
|
||||||
|
|
||||||
def _matches_target(node: ET.Element, target: str) -> bool:
|
@lru_cache(maxsize=512)
|
||||||
target = target.strip()
|
def normalize_spec_text(value: str) -> str:
|
||||||
if not target:
|
"""生成采购规格比较键;Windows 下把繁体转换为简体。
|
||||||
return False
|
|
||||||
|
|
||||||
for label in _node_labels(node):
|
原始规格文字仍用于点击、显示和保存。本函数只生成比较键;Windows API
|
||||||
|
不可用时安全退化为 Unicode 规范化,不做模糊匹配。
|
||||||
|
"""
|
||||||
|
|
||||||
|
normalized = unicodedata.normalize("NFKC", str(value or "")).strip()
|
||||||
|
if not normalized or sys.platform != "win32":
|
||||||
|
return normalized.casefold()
|
||||||
|
|
||||||
|
try:
|
||||||
|
function = ctypes.windll.kernel32.LCMapStringEx
|
||||||
|
function.argtypes = [
|
||||||
|
wintypes.LPCWSTR,
|
||||||
|
wintypes.DWORD,
|
||||||
|
wintypes.LPCWSTR,
|
||||||
|
ctypes.c_int,
|
||||||
|
wintypes.LPWSTR,
|
||||||
|
ctypes.c_int,
|
||||||
|
ctypes.c_void_p,
|
||||||
|
ctypes.c_void_p,
|
||||||
|
wintypes.LPARAM,
|
||||||
|
]
|
||||||
|
function.restype = ctypes.c_int
|
||||||
|
required = function(
|
||||||
|
"zh-CN",
|
||||||
|
_LCMAP_SIMPLIFIED_CHINESE,
|
||||||
|
normalized,
|
||||||
|
-1,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if required <= 0:
|
||||||
|
return normalized.casefold()
|
||||||
|
buffer = ctypes.create_unicode_buffer(required)
|
||||||
|
written = function(
|
||||||
|
"zh-CN",
|
||||||
|
_LCMAP_SIMPLIFIED_CHINESE,
|
||||||
|
normalized,
|
||||||
|
-1,
|
||||||
|
buffer,
|
||||||
|
required,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if written <= 0:
|
||||||
|
return normalized.casefold()
|
||||||
|
return buffer.value.casefold()
|
||||||
|
except (AttributeError, OSError, TypeError, ValueError):
|
||||||
|
return normalized.casefold()
|
||||||
|
|
||||||
|
|
||||||
|
def _label_matches(label: str, target: str) -> bool:
|
||||||
if label.casefold() == target.casefold():
|
if label.casefold() == target.casefold():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 兼容“颜色 黑色,已选中”或“尺码: XL”等无障碍描述,
|
|
||||||
# 同时避免 XL 错误匹配到 2XL。
|
|
||||||
pattern = rf"(?:^|[\s,,::;;]){re.escape(target)}(?:$|[\s,,::;;])"
|
pattern = rf"(?:^|[\s,,::;;]){re.escape(target)}(?:$|[\s,,::;;])"
|
||||||
if re.search(pattern, label, flags=re.IGNORECASE):
|
return re.search(pattern, label, flags=re.IGNORECASE) is not None
|
||||||
return True
|
|
||||||
|
|
||||||
|
def _node_match_kind(node: ET.Element, target: str) -> int:
|
||||||
|
"""返回 2=原文精确匹配、1=繁简规范化匹配、0=不匹配。"""
|
||||||
|
|
||||||
|
checked_target = target.strip()
|
||||||
|
if not checked_target:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
labels = _node_labels(node)
|
||||||
|
if any(_label_matches(label, checked_target) for label in labels):
|
||||||
|
return 2
|
||||||
|
|
||||||
|
normalized_target = normalize_spec_text(checked_target)
|
||||||
|
if not normalized_target:
|
||||||
|
return 0
|
||||||
|
for label in labels:
|
||||||
|
normalized_label = normalize_spec_text(label)
|
||||||
|
if normalized_label and _label_matches(normalized_label, normalized_target):
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _matches_target(node: ET.Element, target: str) -> bool:
|
||||||
|
if not target.strip():
|
||||||
return False
|
return False
|
||||||
|
return _node_match_kind(node, target) > 0
|
||||||
|
|
||||||
|
|
||||||
def _parent_map(root: ET.Element) -> dict[ET.Element, ET.Element]:
|
def _parent_map(root: ET.Element) -> dict[ET.Element, ET.Element]:
|
||||||
@@ -328,17 +410,45 @@ def _safe_horizontal_target(bounds: Bounds, region: Bounds) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _target_click_bounds(
|
def _contains_bounds(outer: Bounds, inner: Bounds) -> bool:
|
||||||
|
return (
|
||||||
|
outer[0] <= inner[0]
|
||||||
|
and outer[1] <= inner[1]
|
||||||
|
and outer[2] >= inner[2]
|
||||||
|
and outer[3] >= inner[3]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _collapse_nested_bounds(bounds: list[Bounds]) -> list[Bounds]:
|
||||||
|
"""把同一规格卡片的外层和文字点击区域合并为外层卡片。"""
|
||||||
|
|
||||||
|
unique = sorted(
|
||||||
|
set(bounds),
|
||||||
|
key=lambda item: (item[2] - item[0]) * (item[3] - item[1]),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
result: list[Bounds] = []
|
||||||
|
for candidate in unique:
|
||||||
|
if any(_contains_bounds(existing, candidate) for existing in result):
|
||||||
|
continue
|
||||||
|
result.append(candidate)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _target_click_candidates(
|
||||||
root: ET.Element,
|
root: ET.Element,
|
||||||
target: str,
|
target: str,
|
||||||
region: Bounds,
|
region: Bounds,
|
||||||
require_horizontal_safe: bool = False,
|
require_horizontal_safe: bool = False,
|
||||||
) -> Optional[Bounds]:
|
) -> list[tuple[int, int, Bounds]]:
|
||||||
parents = _parent_map(root)
|
parents = _parent_map(root)
|
||||||
candidates: list[tuple[int, Bounds]] = []
|
candidates: list[tuple[int, int, Bounds]] = []
|
||||||
|
|
||||||
for node in root.iter("node"):
|
for node in root.iter("node"):
|
||||||
if not _is_available(node) or not _matches_target(node, target):
|
if not _is_available(node):
|
||||||
|
continue
|
||||||
|
match_kind = _node_match_kind(node, target)
|
||||||
|
if match_kind == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
bounds = _nearest_click_bounds(node, parents, region)
|
bounds = _nearest_click_bounds(node, parents, region)
|
||||||
@@ -348,11 +458,47 @@ def _target_click_bounds(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1])
|
area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1])
|
||||||
candidates.append((area, bounds))
|
candidates.append((match_kind, area, bounds))
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def _normalized_target_is_ambiguous(
|
||||||
|
root: ET.Element,
|
||||||
|
target: str,
|
||||||
|
region: Bounds,
|
||||||
|
require_horizontal_safe: bool = False,
|
||||||
|
) -> bool:
|
||||||
|
candidates = _target_click_candidates(
|
||||||
|
root,
|
||||||
|
target,
|
||||||
|
region,
|
||||||
|
require_horizontal_safe=require_horizontal_safe,
|
||||||
|
)
|
||||||
|
if not candidates or any(kind == 2 for kind, _area, _bounds in candidates):
|
||||||
|
return False
|
||||||
|
return len(_collapse_nested_bounds([item[2] for item in candidates])) > 1
|
||||||
|
|
||||||
|
|
||||||
|
def _target_click_bounds(
|
||||||
|
root: ET.Element,
|
||||||
|
target: str,
|
||||||
|
region: Bounds,
|
||||||
|
require_horizontal_safe: bool = False,
|
||||||
|
) -> Optional[Bounds]:
|
||||||
|
candidates = _target_click_candidates(
|
||||||
|
root,
|
||||||
|
target,
|
||||||
|
region,
|
||||||
|
require_horizontal_safe=require_horizontal_safe,
|
||||||
|
)
|
||||||
if not candidates:
|
if not candidates:
|
||||||
return None
|
return None
|
||||||
return max(candidates, key=lambda item: item[0])[1]
|
best_kind = max(item[0] for item in candidates)
|
||||||
|
best = [item for item in candidates if item[0] == best_kind]
|
||||||
|
if best_kind == 1:
|
||||||
|
bounds = _collapse_nested_bounds([item[2] for item in best])
|
||||||
|
return bounds[0] if len(bounds) == 1 else None
|
||||||
|
return max(best, key=lambda item: item[1])[2]
|
||||||
|
|
||||||
|
|
||||||
def _target_is_selected(root: ET.Element, target: str) -> bool:
|
def _target_is_selected(root: ET.Element, target: str) -> bool:
|
||||||
@@ -391,6 +537,13 @@ def color_selection_failure_reason(xml_data: XmlData, target: str) -> str:
|
|||||||
if not matching_nodes:
|
if not matching_nodes:
|
||||||
return "target_not_visible"
|
return "target_not_visible"
|
||||||
region = _horizontal_color_region(root, target)
|
region = _horizontal_color_region(root, target)
|
||||||
|
if region is not None and _normalized_target_is_ambiguous(
|
||||||
|
root,
|
||||||
|
target,
|
||||||
|
region,
|
||||||
|
require_horizontal_safe=True,
|
||||||
|
):
|
||||||
|
return "target_ambiguous"
|
||||||
if region is None or _target_click_bounds(
|
if region is None or _target_click_bounds(
|
||||||
root,
|
root,
|
||||||
target,
|
target,
|
||||||
@@ -401,6 +554,25 @@ def color_selection_failure_reason(xml_data: XmlData, target: str) -> str:
|
|||||||
return "selection_unconfirmed"
|
return "selection_unconfirmed"
|
||||||
|
|
||||||
|
|
||||||
|
def size_selection_failure_reason(xml_data: XmlData, target: str) -> str:
|
||||||
|
"""说明尺码选择失败阶段,繁简等价候选不唯一时明确拒绝。"""
|
||||||
|
|
||||||
|
root = _parse_xml(xml_data)
|
||||||
|
matching_nodes = [
|
||||||
|
node
|
||||||
|
for node in root.iter("node")
|
||||||
|
if _is_available(node) and _matches_target(node, target)
|
||||||
|
]
|
||||||
|
if not matching_nodes:
|
||||||
|
return "target_not_visible"
|
||||||
|
region = _vertical_panel_region(root)
|
||||||
|
if region is not None and _normalized_target_is_ambiguous(root, target, region):
|
||||||
|
return "target_ambiguous"
|
||||||
|
if region is None or _target_click_bounds(root, target, region) is None:
|
||||||
|
return "safe_target_missing"
|
||||||
|
return "selection_unconfirmed"
|
||||||
|
|
||||||
|
|
||||||
def _visible_signature(
|
def _visible_signature(
|
||||||
root: ET.Element,
|
root: ET.Element,
|
||||||
region: Bounds,
|
region: Bounds,
|
||||||
@@ -426,6 +598,13 @@ def _click_target_and_verify(
|
|||||||
action_delay: float,
|
action_delay: float,
|
||||||
require_horizontal_safe: bool = False,
|
require_horizontal_safe: bool = False,
|
||||||
) -> tuple[bool, ET.Element]:
|
) -> tuple[bool, ET.Element]:
|
||||||
|
if _normalized_target_is_ambiguous(
|
||||||
|
root,
|
||||||
|
target,
|
||||||
|
region,
|
||||||
|
require_horizontal_safe=require_horizontal_safe,
|
||||||
|
):
|
||||||
|
return False, root
|
||||||
if _target_is_selected(root, target):
|
if _target_is_selected(root, target):
|
||||||
return True, root
|
return True, root
|
||||||
|
|
||||||
@@ -444,6 +623,8 @@ def _click_target_and_verify(
|
|||||||
time.sleep(action_delay)
|
time.sleep(action_delay)
|
||||||
|
|
||||||
refreshed_root = _parse_xml(device.dump_hierarchy())
|
refreshed_root = _parse_xml(device.dump_hierarchy())
|
||||||
|
# 点击前已经完成唯一候选校验。点击后页面通常会额外出现“已选:目标”
|
||||||
|
# 摘要,不能把摘要和原卡片误判成两个可选候选。
|
||||||
return _target_is_selected(refreshed_root, target), refreshed_root
|
return _target_is_selected(refreshed_root, target), refreshed_root
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1618,6 +1618,35 @@ class U2PddPurchaseAdapterTest(unittest.TestCase):
|
|||||||
self.assertIn("purchase_select_size", operations)
|
self.assertIn("purchase_select_size", operations)
|
||||||
adapter.close()
|
adapter.close()
|
||||||
|
|
||||||
|
def test_failed_option_stage_is_recorded_as_failed(self):
|
||||||
|
cases = (
|
||||||
|
({"color": "不存在的颜色"}, "purchase_select_color"),
|
||||||
|
(
|
||||||
|
{"color": "黑色", "size": "不存在的尺码"},
|
||||||
|
"purchase_select_size",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for options, expected_operation in cases:
|
||||||
|
with self.subTest(operation=expected_operation):
|
||||||
|
device = FakeDevice()
|
||||||
|
adapter = self._adapter(device, [])
|
||||||
|
records = []
|
||||||
|
trace = TaskPerformanceTrace(sink=records.append)
|
||||||
|
trace.bind_task(f"PUR-{expected_operation}")
|
||||||
|
adapter.open_goods(GOODS_URL)
|
||||||
|
|
||||||
|
with trace.activate(), self.assertRaises(PddPurchaseError):
|
||||||
|
adapter.select_options(options)
|
||||||
|
|
||||||
|
matching = [
|
||||||
|
record
|
||||||
|
for record in records
|
||||||
|
if record["operation"] == expected_operation
|
||||||
|
]
|
||||||
|
self.assertEqual(len(matching), 1)
|
||||||
|
self.assertEqual(matching[0]["result"], "failed")
|
||||||
|
adapter.close()
|
||||||
|
|
||||||
def test_same_quantity_does_not_focus_editor(self):
|
def test_same_quantity_does_not_focus_editor(self):
|
||||||
device = FakeDevice()
|
device = FakeDevice()
|
||||||
adapter = self._adapter(device, [])
|
adapter = self._adapter(device, [])
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import xml.etree.ElementTree as ET
|
|||||||
|
|
||||||
from src.util.select_color_size import (
|
from src.util.select_color_size import (
|
||||||
color_selection_failure_reason,
|
color_selection_failure_reason,
|
||||||
|
normalize_spec_text,
|
||||||
select_color,
|
select_color,
|
||||||
|
select_size,
|
||||||
|
size_selection_failure_reason,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -16,9 +19,15 @@ FIXTURES = Path(__file__).parent / "fixtures"
|
|||||||
class StaticColorDevice:
|
class StaticColorDevice:
|
||||||
"""模拟没有滚动属性的图片颜色规格面板。"""
|
"""模拟没有滚动属性的图片颜色规格面板。"""
|
||||||
|
|
||||||
def __init__(self, xml_data: str, confirm_selection: bool = True) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
xml_data: str,
|
||||||
|
confirm_selection: bool = True,
|
||||||
|
selected_text: str = "测试黑色",
|
||||||
|
) -> None:
|
||||||
self.xml_data = xml_data
|
self.xml_data = xml_data
|
||||||
self.confirm_selection = confirm_selection
|
self.confirm_selection = confirm_selection
|
||||||
|
self.selected_text = selected_text
|
||||||
self.clicks = []
|
self.clicks = []
|
||||||
self.swipes = []
|
self.swipes = []
|
||||||
|
|
||||||
@@ -31,10 +40,10 @@ class StaticColorDevice:
|
|||||||
return
|
return
|
||||||
root = ET.fromstring(self.xml_data)
|
root = ET.fromstring(self.xml_data)
|
||||||
for node in root.iter("node"):
|
for node in root.iter("node"):
|
||||||
if node.get("text", "").strip() == "测试黑色":
|
if node.get("text", "").strip() == self.selected_text:
|
||||||
node.set("selected", "true")
|
node.set("selected", "true")
|
||||||
if node.get("text", "").strip().startswith("请选择"):
|
if node.get("text", "").strip().startswith("请选择"):
|
||||||
node.set("text", "已选: 测试黑色")
|
node.set("text", f"已选: {self.selected_text}")
|
||||||
self.xml_data = ET.tostring(root, encoding="unicode")
|
self.xml_data = ET.tostring(root, encoding="unicode")
|
||||||
|
|
||||||
def swipe(self, *args, **kwargs) -> None:
|
def swipe(self, *args, **kwargs) -> None:
|
||||||
@@ -122,6 +131,83 @@ class StaticColorSelectionTest(unittest.TestCase):
|
|||||||
self.assertEqual(device.clicks, [])
|
self.assertEqual(device.clicks, [])
|
||||||
self.assertEqual(device.swipes, [])
|
self.assertEqual(device.swipes, [])
|
||||||
|
|
||||||
|
def test_traditional_color_matches_unique_simplified_card(self):
|
||||||
|
simplified = "缥缈仙姬绾【黑色】"
|
||||||
|
traditional = "縹緲仙姬綰【黑色】"
|
||||||
|
panel_xml = self.panel_xml.replace("测试黑色", simplified)
|
||||||
|
device = StaticColorDevice(panel_xml, selected_text=simplified)
|
||||||
|
|
||||||
|
selected = select_color(
|
||||||
|
device,
|
||||||
|
panel_xml,
|
||||||
|
traditional,
|
||||||
|
action_delay=0.001,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(normalize_spec_text(traditional), simplified.casefold())
|
||||||
|
self.assertTrue(selected)
|
||||||
|
self.assertEqual(device.clicks, [(194, 1389)])
|
||||||
|
|
||||||
|
def test_traditional_size_matches_unique_simplified_option(self):
|
||||||
|
simplified = "均码【40.0-60.0公斤】"
|
||||||
|
traditional = "均碼【40.0-60.0公斤】"
|
||||||
|
panel_xml = self.panel_xml.replace('text="M"', f'text="{simplified}"')
|
||||||
|
device = StaticColorDevice(panel_xml, selected_text=simplified)
|
||||||
|
|
||||||
|
selected = select_size(
|
||||||
|
device,
|
||||||
|
panel_xml,
|
||||||
|
traditional,
|
||||||
|
action_delay=0.001,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(selected)
|
||||||
|
self.assertEqual(device.clicks, [(77, 1762)])
|
||||||
|
|
||||||
|
def test_normalized_duplicate_color_candidates_are_not_clicked(self):
|
||||||
|
simplified = "缥缈仙姬绾【黑色】"
|
||||||
|
traditional = "縹緲仙姬綰【黑色】"
|
||||||
|
panel_xml = self.panel_xml.replace("测试黑色", simplified).replace(
|
||||||
|
"测试红色", simplified
|
||||||
|
)
|
||||||
|
device = StaticColorDevice(panel_xml, selected_text=simplified)
|
||||||
|
|
||||||
|
selected = select_color(
|
||||||
|
device,
|
||||||
|
panel_xml,
|
||||||
|
traditional,
|
||||||
|
action_delay=0.001,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(selected)
|
||||||
|
self.assertEqual(device.clicks, [])
|
||||||
|
self.assertEqual(
|
||||||
|
color_selection_failure_reason(panel_xml, traditional),
|
||||||
|
"target_ambiguous",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_normalized_duplicate_size_candidates_are_not_clicked(self):
|
||||||
|
simplified = "均码【40.0-60.0公斤】"
|
||||||
|
traditional = "均碼【40.0-60.0公斤】"
|
||||||
|
panel_xml = self.panel_xml.replace('text="M"', f'text="{simplified}"').replace(
|
||||||
|
'text="L"', f'text="{simplified}"'
|
||||||
|
)
|
||||||
|
device = StaticColorDevice(panel_xml, selected_text=simplified)
|
||||||
|
|
||||||
|
selected = select_size(
|
||||||
|
device,
|
||||||
|
panel_xml,
|
||||||
|
traditional,
|
||||||
|
action_delay=0.001,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(selected)
|
||||||
|
self.assertEqual(device.clicks, [])
|
||||||
|
self.assertEqual(
|
||||||
|
size_selection_failure_reason(panel_xml, traditional),
|
||||||
|
"target_ambiguous",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user