feat: 完成T-502满9张封面替换

实现 replace_cover 满9张时先校验 old_cover_path 本地备份,备份缺失返回 OLD_COVER_BACKUP_MISSING 且不删除线上图片。

备份存在时点击第一张删除按钮并处理可见确认框,删除后继续上传新封面并拖到第一位;apply_task 会把任务 old_cover_path 传入 replace_cover。

补充 editor 单测覆盖备份缺失阻断、删除确认后上传拖首位、删除按钮缺失和 old_cover_path 传递;同步需求、架构、编码规则、任务看板、API、routes、current-state 与 progress。

已在测试商品 29671243750 上验证 9图删除和满9张替换的不提交流程;未点击更新保存线上。
This commit is contained in:
chengma
2026-06-29 09:49:53 +08:00
parent b05a856796
commit 82d390c25a
10 changed files with 345 additions and 24 deletions
+156
View File
@@ -1,4 +1,5 @@
import os
import json
import sys
import unittest
from unittest import mock
@@ -51,6 +52,83 @@ class FakeProductCDP:
self.closed = True
def cover_rects(count, prefix="old", start=0):
return [
{
"i": index,
"x": 100 + index * 10,
"y": 100,
"left": 60 + index * 10,
"top": 60,
"w": 80,
"h": 80,
"src": f"https://susercontent.com/{prefix}-{start + index}.jpg",
}
for index in range(count)
]
class FakeCoverCDP:
def __init__(self, count=9, delete_click=True, confirm_click=True):
self.initial = cover_rects(count)
self.after_delete = cover_rects(max(0, count - 1), prefix="kept", start=1)
self.new_rect = {
"i": max(0, count - 1),
"x": 300,
"y": 100,
"left": 260,
"top": 60,
"w": 80,
"h": 80,
"src": "https://susercontent.com/new-cover.jpg",
}
self.delete_click = delete_click
self.confirm_click = confirm_click
self.delete_clicked = False
self.confirm_clicked = False
self.uploaded = False
self.dragged = False
self.sent = []
self.drag_calls = []
def _rects(self):
if self.uploaded and self.dragged:
return [self.new_rect] + self.after_delete
if self.uploaded:
return self.after_delete + [self.new_rect]
if self.confirm_clicked:
return self.after_delete
return self.initial
def val(self, expr):
if expr == editor.JS_RECTS:
return json.dumps(self._rects())
if expr == editor.JS_CLICK_FIRST_DELETE:
if not self.delete_click:
return json.dumps({"clicked": False, "reason": "NO_DELETE_BUTTON"})
self.delete_clicked = True
return json.dumps({"clicked": True, "reason": None})
if expr == editor.JS_CLICK_DELETE_CONFIRM:
if not self.confirm_click:
return json.dumps({"clicked": False, "reason": "NO_CONFIRM_BUTTON"})
self.confirm_clicked = True
return json.dumps({"clicked": True, "reason": None, "text": "刪除"})
return True
def object_id(self, expr):
return "upload-input"
def send(self, method, params=None):
self.sent.append((method, params or {}))
if method == "DOM.setFileInputFiles":
self.uploaded = True
return {}
def drag(self, *args, **kwargs):
self.dragged = True
self.drag_calls.append((args, kwargs))
def cookie(name, domain=".shopee.tw"):
return {"name": name, "domain": domain}
@@ -293,6 +371,84 @@ class EditorLoginTests(unittest.TestCase):
self.assertTrue(cdp.closed)
close_tab.assert_not_called()
def test_apply_task_passes_old_cover_backup_to_replace_cover(self):
cdp = FakeProductCDP("ws-new")
with mock.patch("app.editor.open_product", return_value=cdp), mock.patch(
"app.editor.replace_cover",
return_value={"ok": True, "reason": None},
) as replace_cover, mock.patch(
"app.editor.click_update",
return_value={"clicked": True, "reason": None},
):
result = editor.apply_task(
{"debug_port": 9222},
{
"item_id": "51100639510",
"new_cover_path": "new.jpg",
"old_cover_path": "old.jpg",
},
)
self.assertTrue(result["committed"])
replace_cover.assert_called_once_with(
cdp,
"new.jpg",
old_cover_path="old.jpg",
)
def test_replace_cover_full_slots_requires_old_cover_backup(self):
cdp = FakeCoverCDP(count=9)
missing_backup = os.path.join(os.path.dirname(__file__), "missing-old-cover.jpg")
with mock.patch("app.editor.time.sleep"):
result = editor.replace_cover(
cdp,
__file__,
old_cover_path=missing_backup,
)
self.assertFalse(result["ok"])
self.assertEqual("OLD_COVER_BACKUP_MISSING", result["reason"])
self.assertFalse(cdp.delete_clicked)
self.assertFalse(cdp.uploaded)
def test_replace_cover_full_slots_deletes_first_then_uploads_and_drags(self):
cdp = FakeCoverCDP(count=9)
with mock.patch("app.editor.time.sleep"):
result = editor.replace_cover(
cdp,
__file__,
old_cover_path=__file__,
timeout=1,
)
self.assertTrue(result["ok"])
self.assertTrue(cdp.delete_clicked)
self.assertTrue(cdp.confirm_clicked)
self.assertTrue(cdp.uploaded)
self.assertTrue(cdp.dragged)
self.assertEqual(9, result["count_before"])
self.assertEqual(9, result["count_after"])
self.assertEqual(8, result["delete"]["count_after"])
self.assertEqual("https://susercontent.com/new-cover.jpg", result["new_src"])
def test_replace_cover_full_slots_reports_missing_delete_button(self):
cdp = FakeCoverCDP(count=9, delete_click=False)
with mock.patch("app.editor.time.sleep"):
result = editor.replace_cover(
cdp,
__file__,
old_cover_path=__file__,
timeout=1,
)
self.assertFalse(result["ok"])
self.assertEqual("NO_DELETE_BUTTON", result["reason"])
self.assertFalse(cdp.uploaded)
if __name__ == "__main__":
unittest.main()