fix: handle Shopee update confirmation and upload edge cases
This commit is contained in:
+348
-2
@@ -68,8 +68,108 @@ def cover_rects(count, prefix="old", start=0):
|
||||
]
|
||||
|
||||
|
||||
class FakeUpdateCDP:
|
||||
def __init__(
|
||||
self,
|
||||
confirm_present=True,
|
||||
confirm_click=True,
|
||||
require_content_selector=False,
|
||||
post_url=None,
|
||||
post_toasts=None,
|
||||
):
|
||||
self.confirm_present = confirm_present
|
||||
self.confirm_click = confirm_click
|
||||
self.require_content_selector = require_content_selector
|
||||
self.post_url = post_url
|
||||
self.post_toasts = post_toasts
|
||||
self.main_clicked = False
|
||||
self.confirm_clicked = False
|
||||
|
||||
def val(self, expr):
|
||||
if expr == editor.JS_FIND_UPDATE:
|
||||
return json.dumps({"found": True, "disabled": False})
|
||||
if expr == editor.JS_CLICK_UPDATE:
|
||||
self.main_clicked = True
|
||||
return "CLICKED"
|
||||
if expr == editor.JS_FIND_UPDATE_CONFIRM:
|
||||
if self.require_content_selector and ".eds-modal__content" not in expr:
|
||||
return json.dumps({"present": False})
|
||||
return json.dumps(
|
||||
{
|
||||
"present": self.confirm_present,
|
||||
"title": "確定您要更新商品嗎?" if self.confirm_present else "",
|
||||
"buttons": ["立即優化", "更新"] if self.confirm_present else [],
|
||||
}
|
||||
)
|
||||
if expr == editor.JS_CLICK_UPDATE_CONFIRM:
|
||||
if self.require_content_selector and ".eds-modal__content" not in expr:
|
||||
return json.dumps(
|
||||
{
|
||||
"present": False,
|
||||
"clicked": False,
|
||||
"reason": "NO_UPDATE_CONFIRM_MODAL",
|
||||
}
|
||||
)
|
||||
if not self.confirm_present:
|
||||
return json.dumps(
|
||||
{
|
||||
"present": False,
|
||||
"clicked": False,
|
||||
"reason": "NO_UPDATE_CONFIRM_MODAL",
|
||||
}
|
||||
)
|
||||
if not self.confirm_click:
|
||||
return json.dumps(
|
||||
{
|
||||
"present": True,
|
||||
"clicked": False,
|
||||
"reason": "NO_UPDATE_CONFIRM_BUTTON",
|
||||
"buttons": ["立即優化"],
|
||||
}
|
||||
)
|
||||
self.confirm_clicked = True
|
||||
self.confirm_present = False
|
||||
return json.dumps(
|
||||
{
|
||||
"present": True,
|
||||
"clicked": True,
|
||||
"reason": None,
|
||||
"text": "更新",
|
||||
"buttons": ["立即優化", "更新"],
|
||||
}
|
||||
)
|
||||
if expr == editor.JS_POST_UPDATE_STATE:
|
||||
if self.post_url is None and self.post_toasts is None:
|
||||
return None
|
||||
url = self.post_url or "https://seller.shopee.tw/portal/product/51100639510"
|
||||
toasts = self.post_toasts or []
|
||||
error_words = ("失敗", "失败", "錯誤", "错误", "不支援", "不支持", "無法", "无法", "error", "fail", "invalid")
|
||||
return json.dumps(
|
||||
{
|
||||
"url": url,
|
||||
"redirected_to_list": "/portal/product/list/" in url,
|
||||
"toasts": toasts,
|
||||
"error_toasts": [t for t in toasts if any(word in t for word in error_words)],
|
||||
"success_toasts": [t for t in toasts if "成功" in t or "success" in t.lower()],
|
||||
}
|
||||
)
|
||||
if expr == editor.JS_TOASTS:
|
||||
return json.dumps([])
|
||||
return None
|
||||
|
||||
|
||||
class FakeCoverCDP:
|
||||
def __init__(self, count=9, delete_click=True, confirm_click=True):
|
||||
def __init__(
|
||||
self,
|
||||
count=9,
|
||||
delete_click=True,
|
||||
confirm_click=True,
|
||||
upload_mode="success",
|
||||
post_delete_busy_polls=0,
|
||||
upload_input_ready=True,
|
||||
restore_upload_after_delete=False,
|
||||
page_error_toasts=None,
|
||||
):
|
||||
self.initial = cover_rects(count)
|
||||
self.after_delete = cover_rects(max(0, count - 1), prefix="kept", start=1)
|
||||
self.new_rect = {
|
||||
@@ -84,14 +184,26 @@ class FakeCoverCDP:
|
||||
}
|
||||
self.delete_click = delete_click
|
||||
self.confirm_click = confirm_click
|
||||
self.upload_mode = upload_mode
|
||||
self.post_delete_busy_polls = post_delete_busy_polls
|
||||
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.delete_clicked = False
|
||||
self.confirm_clicked = False
|
||||
self.uploaded = False
|
||||
self.uploaded_after_stable = None
|
||||
self.dragged = False
|
||||
self.sent = []
|
||||
self.drag_calls = []
|
||||
|
||||
def _rects(self):
|
||||
if self.uploaded and self.upload_mode == "processing":
|
||||
blob_rect = dict(self.new_rect)
|
||||
blob_rect["src"] = "blob:https://seller.shopee.tw/uploading"
|
||||
return self.after_delete + [blob_rect]
|
||||
if self.uploaded and self.upload_mode == "error":
|
||||
return self.after_delete
|
||||
if self.uploaded and self.dragged:
|
||||
return [self.new_rect] + self.after_delete
|
||||
if self.uploaded:
|
||||
@@ -100,9 +212,42 @@ class FakeCoverCDP:
|
||||
return self.after_delete
|
||||
return self.initial
|
||||
|
||||
def _upload_ready(self):
|
||||
return self.upload_input_ready or (self.restore_upload_after_delete and self.confirm_clicked)
|
||||
|
||||
def _state(self, busy_count=0, errors=None, crop_modal=False):
|
||||
rects = self._rects()
|
||||
srcs = [r["src"] for r in rects]
|
||||
upload_ready = self._upload_ready()
|
||||
return {
|
||||
"manager_exists": True,
|
||||
"count": len(rects),
|
||||
"srcs": srcs,
|
||||
"blob_count": len([src for src in srcs if src.startswith("blob:")]),
|
||||
"cdn_count": len([src for src in srcs if "susercontent" in src]),
|
||||
"upload_input_exists": upload_ready,
|
||||
"upload_input_disabled": (not upload_ready) or busy_count > 0,
|
||||
"upload_tile_visible": True,
|
||||
"busy_count": busy_count,
|
||||
"errors": errors or [],
|
||||
"crop_modal": crop_modal,
|
||||
"toasts": list(self.page_error_toasts),
|
||||
"error_toasts": [],
|
||||
"page_error_toasts": list(self.page_error_toasts),
|
||||
}
|
||||
|
||||
def val(self, expr):
|
||||
if expr == editor.JS_RECTS:
|
||||
return json.dumps(self._rects())
|
||||
if expr == editor.JS_UPLOAD_STATE:
|
||||
if self.uploaded and self.upload_mode == "processing":
|
||||
return json.dumps(self._state(busy_count=1))
|
||||
if self.uploaded and self.upload_mode == "error":
|
||||
return json.dumps(self._state(errors=["圖片格式錯誤"]))
|
||||
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))
|
||||
return json.dumps(self._state())
|
||||
if expr == editor.JS_CLICK_FIRST_DELETE:
|
||||
if not self.delete_click:
|
||||
return json.dumps({"clicked": False, "reason": "NO_DELETE_BUTTON"})
|
||||
@@ -116,11 +261,12 @@ class FakeCoverCDP:
|
||||
return True
|
||||
|
||||
def object_id(self, expr):
|
||||
return "upload-input"
|
||||
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.uploaded_after_stable = self.post_delete_busy_polls == 0
|
||||
self.uploaded = True
|
||||
return {}
|
||||
|
||||
@@ -322,6 +468,103 @@ class EditorLoginTests(unittest.TestCase):
|
||||
self.assertTrue(cdp.closed)
|
||||
close_tab.assert_not_called()
|
||||
|
||||
def test_click_update_confirms_shopee_update_modal(self):
|
||||
cdp = FakeUpdateCDP(confirm_present=True, confirm_click=True)
|
||||
|
||||
with mock.patch("app.editor.time.sleep"):
|
||||
result = editor.click_update(cdp, confirm_timeout=0.1)
|
||||
|
||||
self.assertTrue(result["clicked"])
|
||||
self.assertTrue(cdp.main_clicked)
|
||||
self.assertTrue(cdp.confirm_clicked)
|
||||
self.assertEqual("更新", result["confirm"]["text"])
|
||||
self.assertIn("立即優化", result["confirm"]["buttons"])
|
||||
|
||||
|
||||
def test_click_update_handles_real_eds_modal_content_confirm_dom(self):
|
||||
cdp = FakeUpdateCDP(
|
||||
confirm_present=True,
|
||||
confirm_click=True,
|
||||
require_content_selector=True,
|
||||
)
|
||||
|
||||
with mock.patch("app.editor.time.sleep"):
|
||||
result = editor.click_update(cdp, confirm_timeout=0.1)
|
||||
|
||||
self.assertTrue(result["clicked"])
|
||||
self.assertTrue(cdp.confirm_clicked)
|
||||
self.assertIn(".eds-modal__content", editor.JS_FIND_UPDATE_CONFIRM)
|
||||
self.assertIn(".eds-modal__content", editor.JS_CLICK_UPDATE_CONFIRM)
|
||||
self.assertIn("hasUpdateButton", editor.JS_CLICK_UPDATE_CONFIRM)
|
||||
self.assertEqual("更新", result["confirm"]["text"])
|
||||
|
||||
def test_click_update_records_product_list_redirect_after_confirm(self):
|
||||
cdp = FakeUpdateCDP(
|
||||
confirm_present=True,
|
||||
confirm_click=True,
|
||||
post_url="https://seller.shopee.tw/portal/product/list/all?operationSortBy=modified_time",
|
||||
)
|
||||
|
||||
with mock.patch("app.editor.time.sleep"):
|
||||
result = editor.click_update(cdp, confirm_timeout=0.1, post_timeout=0.1)
|
||||
|
||||
self.assertTrue(result["clicked"])
|
||||
self.assertTrue(result["post_update"]["redirected_to_list"])
|
||||
self.assertEqual(
|
||||
"https://seller.shopee.tw/portal/product/list/all?operationSortBy=modified_time",
|
||||
result["post_update"]["url"],
|
||||
)
|
||||
|
||||
def test_click_update_treats_list_redirect_as_success_even_with_stale_error_toast(self):
|
||||
cdp = FakeUpdateCDP(
|
||||
confirm_present=True,
|
||||
confirm_click=True,
|
||||
post_url="https://seller.shopee.tw/portal/product/list/all?operationSortBy=modified_time",
|
||||
post_toasts=["此物流選項不支援較長備貨商品"],
|
||||
)
|
||||
|
||||
with mock.patch("app.editor.time.sleep"):
|
||||
result = editor.click_update(cdp, confirm_timeout=0.1, post_timeout=0.1)
|
||||
|
||||
self.assertTrue(result["clicked"])
|
||||
self.assertTrue(result["post_update"]["redirected_to_list"])
|
||||
self.assertIn("此物流選項不支援較長備貨商品", result["post_update"]["error_toasts"])
|
||||
|
||||
def test_click_update_reports_logistics_validation_after_confirm(self):
|
||||
cdp = FakeUpdateCDP(
|
||||
confirm_present=True,
|
||||
confirm_click=True,
|
||||
post_toasts=["此物流選項不支援較長備貨商品"],
|
||||
)
|
||||
|
||||
with mock.patch("app.editor.time.sleep"):
|
||||
result = editor.click_update(cdp, confirm_timeout=0.1, post_timeout=0.1)
|
||||
|
||||
self.assertFalse(result["clicked"])
|
||||
self.assertEqual("POST_UPDATE_ERROR", result["reason"])
|
||||
self.assertIn("此物流選項不支援較長備貨商品", result["post_update"]["error_toasts"])
|
||||
|
||||
def test_click_update_fails_when_shopee_update_modal_is_not_confirmed(self):
|
||||
cdp = FakeUpdateCDP(confirm_present=True, confirm_click=False)
|
||||
|
||||
result = editor.click_update(cdp, confirm_timeout=0.01)
|
||||
|
||||
self.assertFalse(result["clicked"])
|
||||
self.assertTrue(cdp.main_clicked)
|
||||
self.assertFalse(cdp.confirm_clicked)
|
||||
self.assertEqual("NO_UPDATE_CONFIRM_BUTTON", result["reason"])
|
||||
self.assertIn("立即優化", result["confirm"]["buttons"])
|
||||
|
||||
def test_click_update_keeps_existing_success_when_no_shopee_confirm_modal(self):
|
||||
cdp = FakeUpdateCDP(confirm_present=False)
|
||||
|
||||
result = editor.click_update(cdp, confirm_timeout=0.01)
|
||||
|
||||
self.assertTrue(result["clicked"])
|
||||
self.assertTrue(cdp.main_clicked)
|
||||
self.assertFalse(cdp.confirm_clicked)
|
||||
self.assertFalse(result["confirm"]["present"])
|
||||
|
||||
def test_apply_task_closes_auto_created_product_tab_after_success_when_enabled(self):
|
||||
cdp = FakeProductCDP("ws-new")
|
||||
cdp.target_id = "target-new"
|
||||
@@ -434,6 +677,109 @@ class EditorLoginTests(unittest.TestCase):
|
||||
self.assertEqual(8, result["delete"]["count_after"])
|
||||
self.assertEqual("https://susercontent.com/new-cover.jpg", result["new_src"])
|
||||
|
||||
|
||||
def test_replace_cover_full_slots_deletes_before_upload_input_is_ready(self):
|
||||
cdp = FakeCoverCDP(
|
||||
count=9,
|
||||
upload_input_ready=False,
|
||||
restore_upload_after_delete=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.delete_clicked)
|
||||
self.assertTrue(cdp.confirm_clicked)
|
||||
self.assertTrue(cdp.uploaded)
|
||||
self.assertTrue(cdp.dragged)
|
||||
self.assertEqual(9, result["count_before"])
|
||||
self.assertEqual(8, result["delete"]["count_after"])
|
||||
|
||||
|
||||
def test_replace_cover_ignores_unrelated_logistics_page_error_toast(self):
|
||||
cdp = FakeCoverCDP(
|
||||
count=9,
|
||||
page_error_toasts=["此物流選項不支援較長備貨商品"],
|
||||
)
|
||||
|
||||
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.uploaded)
|
||||
self.assertTrue(cdp.dragged)
|
||||
|
||||
|
||||
def test_replace_cover_waits_for_deleted_images_to_settle_before_uploading(self):
|
||||
cdp = FakeCoverCDP(count=9, post_delete_busy_polls=2)
|
||||
|
||||
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.uploaded_after_stable)
|
||||
self.assertEqual(0, cdp.post_delete_busy_polls)
|
||||
|
||||
|
||||
def test_replace_cover_reports_upload_still_processing_with_state(self):
|
||||
cdp = FakeCoverCDP(count=8, upload_mode="processing")
|
||||
|
||||
with mock.patch("app.editor.time.sleep"):
|
||||
result = editor.replace_cover(cdp, __file__, timeout=0.01)
|
||||
|
||||
self.assertFalse(result["ok"])
|
||||
self.assertEqual("UPLOAD_STILL_PROCESSING", result["reason"])
|
||||
self.assertTrue(result["blob_seen"])
|
||||
self.assertEqual(1, result["upload_state"]["busy_count"])
|
||||
self.assertIn("blob:", "\n".join(result["upload_state"]["srcs"]))
|
||||
|
||||
def test_replace_cover_reports_page_upload_error_with_state(self):
|
||||
cdp = FakeCoverCDP(count=8, upload_mode="error")
|
||||
|
||||
with mock.patch("app.editor.time.sleep"):
|
||||
result = editor.replace_cover(cdp, __file__, timeout=1)
|
||||
|
||||
self.assertFalse(result["ok"])
|
||||
self.assertEqual("UPLOAD_PAGE_ERROR", result["reason"])
|
||||
self.assertIn("圖片格式錯誤", result["upload_state"]["errors"])
|
||||
|
||||
def test_apply_task_cover_failure_returns_readable_upload_error(self):
|
||||
cdp = FakeProductCDP("ws-new")
|
||||
|
||||
with mock.patch("app.editor.open_product", return_value=cdp), mock.patch(
|
||||
"app.editor.replace_cover",
|
||||
return_value={
|
||||
"ok": False,
|
||||
"reason": "UPLOAD_STILL_PROCESSING",
|
||||
"upload_state": {"busy_count": 1, "errors": [], "toasts": []},
|
||||
},
|
||||
), mock.patch("app.editor.click_update") as click_update:
|
||||
result = editor.apply_task(
|
||||
{"debug_port": 9222},
|
||||
{"item_id": "51100639510", "new_cover_path": "new.jpg"},
|
||||
)
|
||||
|
||||
self.assertFalse(result["committed"])
|
||||
self.assertIn("新封面上传仍在处理中", result["error"])
|
||||
click_update.assert_not_called()
|
||||
|
||||
def test_replace_cover_full_slots_reports_missing_delete_button(self):
|
||||
cdp = FakeCoverCDP(count=9, delete_click=False)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user