fix: stabilize Shopee cover replacement

This commit is contained in:
chengma
2026-07-01 08:35:46 +08:00
parent f7ea9ec0fa
commit 483bec427c
18 changed files with 782 additions and 330 deletions
+113 -6
View File
@@ -169,6 +169,8 @@ class FakeCoverCDP:
upload_input_ready=True,
restore_upload_after_delete=False,
page_error_toasts=None,
upload_tile_click=True,
require_upload_tile_click=False,
):
self.initial = cover_rects(count)
self.after_delete = cover_rects(max(0, count - 1), prefix="kept", start=1)
@@ -189,10 +191,14 @@ class FakeCoverCDP:
self.upload_input_ready = upload_input_ready
self.restore_upload_after_delete = restore_upload_after_delete
self.page_error_toasts = page_error_toasts or []
self.upload_tile_click = upload_tile_click
self.require_upload_tile_click = require_upload_tile_click
self.delete_clicked = False
self.confirm_clicked = False
self.upload_tile_clicked = False
self.uploaded = False
self.uploaded_after_stable = None
self.upload_tile_clicked_before_file_set = None
self.dragged = False
self.sent = []
self.drag_calls = []
@@ -244,6 +250,12 @@ class FakeCoverCDP:
return json.dumps(self._state(busy_count=1))
if self.uploaded and self.upload_mode == "error":
return json.dumps(self._state(errors=["圖片格式錯誤"]))
if self.uploaded and self.upload_mode == "duplicate":
state = self._state()
state["toasts"] = ["有1張重複的圖片"]
state["error_toasts"] = ["有1張重複的圖片"]
state["page_error_toasts"] = ["有1張重複的圖片"]
return json.dumps(state)
if self.confirm_clicked and not self.uploaded and self.post_delete_busy_polls > 0:
self.post_delete_busy_polls -= 1
return json.dumps(self._state(busy_count=1))
@@ -258,14 +270,22 @@ class FakeCoverCDP:
return json.dumps({"clicked": False, "reason": "NO_CONFIRM_BUTTON"})
self.confirm_clicked = True
return json.dumps({"clicked": True, "reason": None, "text": "刪除"})
if expr == editor.JS_CLICK_UPLOAD_TILE:
if not self.upload_tile_click:
return json.dumps({"clicked": False, "reason": "NO_UPLOAD_TILE"})
self.upload_tile_clicked = True
return json.dumps({"clicked": True, "reason": None})
return True
def object_id(self, expr):
if self.require_upload_tile_click and not self.upload_tile_clicked:
return None
return "upload-input" if self._upload_ready() else None
def send(self, method, params=None):
self.sent.append((method, params or {}))
if method == "DOM.setFileInputFiles":
self.upload_tile_clicked_before_file_set = self.upload_tile_clicked
self.uploaded_after_stable = self.post_delete_busy_polls == 0
self.uploaded = True
return {}
@@ -577,7 +597,9 @@ class EditorLoginTests(unittest.TestCase):
) as change_title, mock.patch("app.editor.replace_cover") as replace_cover, mock.patch(
"app.editor.click_update",
return_value={"clicked": True, "reason": None},
), mock.patch("app.editor.close_tab", return_value=True) as close_tab:
), mock.patch("app.editor.close_tab", return_value=True) as close_tab, mock.patch(
"app.editor.time.sleep"
) as sleep:
result = editor.apply_task(
{"debug_port": 9222},
{"item_id": "51100639510", "new_title": "新标题"},
@@ -588,6 +610,7 @@ class EditorLoginTests(unittest.TestCase):
change_title.assert_called_once_with(cdp, "新标题")
replace_cover.assert_not_called()
self.assertTrue(cdp.closed)
sleep.assert_called_once_with(2)
close_tab.assert_called_once_with("target-new", host="127.0.0.1:9222")
def test_apply_task_keeps_product_tab_after_failed_update(self):
@@ -602,7 +625,9 @@ class EditorLoginTests(unittest.TestCase):
), mock.patch(
"app.editor.click_update",
return_value={"clicked": False, "reason": "UPDATE_DISABLED"},
), mock.patch("app.editor.close_tab") as close_tab:
), mock.patch("app.editor.close_tab") as close_tab, mock.patch(
"app.editor.time.sleep"
) as sleep:
result = editor.apply_task(
{"debug_port": 9222},
{"item_id": "51100639510", "new_title": "新标题"},
@@ -613,6 +638,7 @@ class EditorLoginTests(unittest.TestCase):
self.assertEqual("UPDATE_DISABLED", result["error"])
self.assertTrue(cdp.closed)
close_tab.assert_not_called()
sleep.assert_not_called()
def test_apply_task_passes_old_cover_backup_to_replace_cover(self):
cdp = FakeProductCDP("ws-new")
@@ -640,8 +666,8 @@ class EditorLoginTests(unittest.TestCase):
old_cover_path="old.jpg",
)
def test_replace_cover_full_slots_requires_old_cover_backup(self):
cdp = FakeCoverCDP(count=9)
def test_replace_cover_requires_old_cover_backup_before_delete(self):
cdp = FakeCoverCDP(count=8)
missing_backup = os.path.join(os.path.dirname(__file__), "missing-old-cover.jpg")
with mock.patch("app.editor.time.sleep"):
@@ -678,6 +704,61 @@ class EditorLoginTests(unittest.TestCase):
self.assertEqual("https://susercontent.com/new-cover.jpg", result["new_src"])
def test_replace_cover_deletes_first_for_non_full_image_slots(self):
cdp = FakeCoverCDP(count=8)
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(8, result["count_before"])
self.assertEqual(8, result["count_after"])
self.assertEqual(7, result["delete"]["count_after"])
def test_replace_cover_clicks_upload_tile_before_setting_files(self):
cdp = FakeCoverCDP(count=9, require_upload_tile_click=True)
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.upload_tile_clicked)
self.assertTrue(cdp.uploaded)
self.assertIn("DOM.setFileInputFiles", [method for method, _params in cdp.sent])
self.assertTrue(cdp.upload_tile_clicked_before_file_set)
self.assertEqual({"clicked": True, "reason": None}, result["upload_click"])
def test_replace_cover_reports_upload_tile_not_ready(self):
cdp = FakeCoverCDP(count=8, upload_tile_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("UPLOAD_TILE_NOT_READY", result["reason"])
self.assertFalse(cdp.uploaded)
self.assertEqual("NO_UPLOAD_TILE", result["upload_click"]["reason"])
def test_replace_cover_full_slots_deletes_before_upload_input_is_ready(self):
cdp = FakeCoverCDP(
count=9,
@@ -742,7 +823,12 @@ class EditorLoginTests(unittest.TestCase):
cdp = FakeCoverCDP(count=8, upload_mode="processing")
with mock.patch("app.editor.time.sleep"):
result = editor.replace_cover(cdp, __file__, timeout=0.01)
result = editor.replace_cover(
cdp,
__file__,
old_cover_path=__file__,
timeout=0.01,
)
self.assertFalse(result["ok"])
self.assertEqual("UPLOAD_STILL_PROCESSING", result["reason"])
@@ -754,12 +840,33 @@ class EditorLoginTests(unittest.TestCase):
cdp = FakeCoverCDP(count=8, upload_mode="error")
with mock.patch("app.editor.time.sleep"):
result = editor.replace_cover(cdp, __file__, timeout=1)
result = editor.replace_cover(
cdp,
__file__,
old_cover_path=__file__,
timeout=1,
)
self.assertFalse(result["ok"])
self.assertEqual("UPLOAD_PAGE_ERROR", result["reason"])
self.assertIn("圖片格式錯誤", result["upload_state"]["errors"])
def test_replace_cover_reports_duplicate_upload_toast(self):
cdp = FakeCoverCDP(count=8, upload_mode="duplicate")
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("UPLOAD_DUPLICATE_IMAGE", result["reason"])
self.assertIn("有1張重複的圖片", result["upload_state"]["error_toasts"])
self.assertIn("新封面与现有商品图片重复", editor._cover_upload_error_message(result))
def test_apply_task_cover_failure_returns_readable_upload_error(self):
cdp = FakeProductCDP("ws-new")