fix(collect): recognize short Shopee product titles

This commit is contained in:
chengma
2026-07-14 16:31:35 +08:00
parent 2f6b9f7af4
commit 250c7e27eb
5 changed files with 378 additions and 70 deletions
+145 -2
View File
@@ -39,18 +39,23 @@ class FakeCDP:
class FakeProductCDP:
def __init__(self, ws, ready=True, toasts=None, rects=None):
def __init__(self, ws, ready=True, toasts=None, rects=None, url=""):
self.ws = ws
self.ready = ready
self.toasts = list(toasts or [])
self.rects = list(rects or [])
self.url = url
self.closed = False
self.sent = []
self.evaluated = []
self.toast_observer_installed = False
def val(self, expr):
self.evaluated.append(expr)
if expr == editor.JS_READY:
return self.ready
return json.dumps(self.ready) if isinstance(self.ready, dict) else self.ready
if expr == "location.href":
return self.url
if expr == editor.JS_INSTALL_TOAST_OBSERVER:
self.toast_observer_installed = True
return True
@@ -68,6 +73,32 @@ class FakeProductCDP:
self.closed = True
class FakeTitleCDP:
def __init__(self, title="短标题"):
self.title = title
self.write_expr = ""
def val(self, expr):
if expr == editor.JS_TITLE_STATE:
return json.dumps(
{
"found": True,
"candidate_count": 1,
"source": "business",
"field_exists": True,
"value": self.title,
"modelvalue": self.title,
}
)
self.write_expr = expr
marker = "s.call(el,"
if marker in expr:
encoded = expr.split(marker, 1)[1].split(");", 1)[0]
self.title = json.loads(encoded)
return self.title
return None
def cover_rects(count, prefix="old", start=0):
return [
{
@@ -84,6 +115,26 @@ def cover_rects(count, prefix="old", start=0):
]
def ready_snapshot(**overrides):
state = {
"ready": True,
"title_count": 1,
"title_source": "business",
"title_field_exists": True,
"title_ambiguous": False,
"image_field_exists": True,
"image_manager_exists": True,
"image_count": 5,
"cdn_count": 5,
"blob_count": 0,
"upload_input_exists": True,
"upload_input_count": 1,
"current_url": "https://seller.shopee.tw/portal/product/48363984966",
}
state.update(overrides)
return state
class FakeUpdateCDP:
def __init__(
self,
@@ -636,6 +687,98 @@ class EditorLoginTests(unittest.TestCase):
fake.sent,
)
def test_editor_business_selectors_replace_title_length_guess(self):
self.assertIn('data-product-edit-field-unique-id="name"', editor.TITLE_FIELD_SELECTOR)
self.assertIn("data-product-edit-field-unique-id='name'", editor.TITLE_XPATH)
self.assertNotIn("string-length", editor.TITLE_XPATH)
self.assertIn("string-length", editor.LEGACY_TITLE_XPATH)
self.assertIn('data-product-edit-field-unique-id="images"', editor.IMAGE_FIELD_SELECTOR)
self.assertIn("data-product-edit-field-unique-id='images'", editor.ITEMBOX_XPATH)
self.assertIn("source:'legacy'", editor.JS_EDITOR_FIELD_HELPERS)
def test_wait_ready_accepts_short_title_business_field_snapshot(self):
fake = FakeProductCDP("ws-new", ready=ready_snapshot())
with mock.patch("app.editor.time.sleep"):
result = editor._wait_ready(fake, timeout=0)
self.assertTrue(result)
self.assertIn(editor.JS_SCROLL_MAIN_IMAGE_MANAGER, fake.evaluated)
def test_wait_ready_reports_missing_title_without_hidden_logistics_toast(self):
state = ready_snapshot(ready=False, title_count=0)
fake = FakeProductCDP(
"ws-new",
ready=state,
url=state["current_url"],
toasts=[
{
"text": "此物流選項不支援較長備貨商品",
"url": state["current_url"],
"visible": False,
}
],
)
with self.assertRaises(editor.EditorError) as ctx:
editor._wait_ready(fake, timeout=0)
message = str(ctx.exception)
self.assertIn("未识别商品名称输入框", message)
self.assertIn("商品图片5张", message)
self.assertIn("主图上传入口正常", message)
self.assertNotIn("物流選項", message)
def test_wait_ready_reports_ambiguous_business_title_inputs(self):
fake = FakeProductCDP("ws-new", ready=ready_snapshot(ready=False, title_count=2))
with self.assertRaises(editor.EditorError) as ctx:
editor._wait_ready(fake, timeout=0)
self.assertIn("商品名称输入框匹配到2个,无法确定唯一字段", str(ctx.exception))
def test_ready_error_appends_only_visible_same_page_toast_as_hint(self):
url = "https://seller.shopee.tw/portal/product/48363984966"
fake = FakeProductCDP(
"ws-new",
url=url,
toasts=[{"text": "页面暂时繁忙", "url": url, "visible": True}],
)
message = editor._open_product_ready_error(fake, "商品编辑器未就绪", current_url=url)
self.assertEqual("商品编辑器未就绪;页面提示(可能无关):页面暂时繁忙", message)
self.assertIn("Object.assign({},item,{visible:false})", editor.JS_PAGE_TOASTS)
def test_read_and_change_title_share_business_field_locator(self):
fake = FakeTitleCDP(title="二十字以内的有效短标题")
self.assertEqual("二十字以内的有效短标题", editor.read_title(fake))
with mock.patch("app.editor.time.sleep"):
result = editor.change_title(fake, "更新后的短标题")
self.assertTrue(result["ok"])
self.assertEqual("business", result["source"])
self.assertIn(editor.JS_EDITOR_FIELD_HELPERS, fake.write_expr)
self.assertNotIn(editor.TITLE_XPATH, fake.write_expr)
def test_main_image_scripts_share_scoped_business_field_helpers(self):
scripts = (
editor.JS_READY,
editor.JS_RECTS,
editor.JS_UPLOAD_STATE,
editor.JS_CLICK_UPLOAD_TILE,
editor.JS_FIRST_COVER,
editor.JS_SCROLL_MAIN_IMAGE_MANAGER,
editor.JS_MAIN_UPLOAD_INPUT,
editor.JS_DISPATCH_MAIN_UPLOAD_INPUT_EVENTS,
editor.JS_CLICK_FIRST_DELETE,
)
for script in scripts:
with self.subTest(script=script[:40]):
self.assertIn(editor.JS_EDITOR_FIELD_HELPERS, script)
self.assertIn(json.dumps(editor.IMAGE_FIELD_SELECTOR), script)
def test_wait_ready_raises_product_unavailable_from_hidden_toast(self):
fake = FakeProductCDP(
"ws-new",