feat(product-suite): support temporary item drafts
This commit is contained in:
@@ -651,6 +651,148 @@ class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_temporary_draft_allows_local_work_but_blocks_shopee_pull_and_recovers(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
config = self._config(temp_dir)
|
||||
accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
|
||||
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
||||
self.addCleanup(tab.close)
|
||||
state = tab._displayed_state
|
||||
|
||||
self.assertTrue(tab.add_images_button.isEnabled())
|
||||
self.assertFalse(tab.item_id_hint_label.isHidden())
|
||||
self.assertEqual("请输入正确的商品ID", tab.item_id_hint_label.text())
|
||||
with mock.patch(
|
||||
"app.gui.tabs.product_suite.QFileDialog.getOpenFileNames",
|
||||
return_value=([], ""),
|
||||
):
|
||||
tab.choose_images()
|
||||
self.assertEqual([], image_studio.list_projects(path=config["db_path"]))
|
||||
|
||||
source_path = os.path.join(temp_dir, "draft-source.png")
|
||||
self._write_image(source_path)
|
||||
with mock.patch.object(tab, "_start_thread", return_value=object()):
|
||||
tab._start_import(file_paths=[source_path])
|
||||
worker = state.import_worker
|
||||
draft = image_studio.get_project(state.project_id, path=config["db_path"])
|
||||
self.assertTrue(image_studio.is_draft_project(draft))
|
||||
self.assertEqual("", state.item_id)
|
||||
self.assertIn("临时草稿", tab.task_tabs.tabText(tab.task_tabs.currentIndex()))
|
||||
self.assertFalse(tab.item_id_hint_label.isHidden())
|
||||
self.assertTrue(tab.pull_button.isEnabled())
|
||||
self.assertEqual("需要先绑定正式商品ID", tab.pull_button.toolTip())
|
||||
tab._on_import_finished(state, worker.execute())
|
||||
self.assertEqual(1, len(image_studio.list_assets(draft.id, path=config["db_path"])))
|
||||
|
||||
captured = {}
|
||||
|
||||
class _Signal:
|
||||
def connect(self, callback):
|
||||
self.callback = callback
|
||||
|
||||
class _AiWriteWorker:
|
||||
def __init__(self, instruction, context, **kwargs):
|
||||
captured["instruction"] = instruction
|
||||
captured["context"] = context
|
||||
self.finished = _Signal()
|
||||
self.cancelled = _Signal()
|
||||
self.failed = _Signal()
|
||||
|
||||
def cancel(self):
|
||||
pass
|
||||
|
||||
with mock.patch(
|
||||
"app.gui.tabs.product_suite.ProductSuiteAiWriteWorker",
|
||||
_AiWriteWorker,
|
||||
), mock.patch.object(tab, "_start_thread", return_value=object()):
|
||||
tab.start_ai_write()
|
||||
self.assertIn("未绑定商品", captured["context"])
|
||||
self.assertNotIn("draft_", captured["context"])
|
||||
state.ai_worker = None
|
||||
state.ai_thread = None
|
||||
state.ai_started_at = None
|
||||
tab._apply_running_state(state)
|
||||
|
||||
pull_message = mock.Mock()
|
||||
with mock.patch.object(tab, "_message", pull_message):
|
||||
tab.pull_main_images()
|
||||
self.assertIsNone(state.pull_worker)
|
||||
self.assertEqual("无法拉取蝦皮主图", pull_message.call_args.args[0])
|
||||
self.assertIn("当前为临时项目", pull_message.call_args.args[1])
|
||||
|
||||
tab.item_id_edit.setText("51100639510")
|
||||
with mock.patch.object(tab, "_confirm", return_value=True):
|
||||
tab._on_item_finished()
|
||||
bound = image_studio.get_project(draft.id, path=config["db_path"])
|
||||
self.assertEqual("51100639510", bound.item_id)
|
||||
self.assertEqual(image_studio.PROJECT_BINDING_BOUND, bound.binding_state)
|
||||
self.assertTrue(tab.item_id_hint_label.isHidden())
|
||||
self.assertIn("套图任务", tab.task_tabs.tabText(tab.task_tabs.currentIndex()))
|
||||
|
||||
draft_state = tab.add_task(inherit=False)
|
||||
draft = tab._create_draft_project(draft_state)
|
||||
image_studio.add_asset(
|
||||
draft.id,
|
||||
image_studio.ASSET_KIND_ORIGINAL,
|
||||
local_path=source_path,
|
||||
path=config["db_path"],
|
||||
)
|
||||
draft_state.ai_worker = mock.Mock()
|
||||
with mock.patch.object(tab, "_draft_close_action", return_value="cancel"):
|
||||
tab.close_task(tab.task_tabs.currentIndex())
|
||||
draft_state.ai_worker.cancel.assert_not_called()
|
||||
self.assertIn(draft_state.key, tab._states)
|
||||
draft_state.ai_worker = None
|
||||
with mock.patch.object(tab, "_draft_close_action", return_value="keep"):
|
||||
tab.close_task(tab.task_tabs.currentIndex())
|
||||
self.assertIsNotNone(image_studio.get_project(draft.id, path=config["db_path"]))
|
||||
|
||||
tab.close()
|
||||
restored = ProductSuiteTab(config=config, db_path=config["db_path"])
|
||||
self.addCleanup(restored.close)
|
||||
restored_state = next(
|
||||
state for state in restored._states.values() if state.project_id == draft.id
|
||||
)
|
||||
self.assertEqual("", restored_state.item_id)
|
||||
self.assertTrue(restored._is_draft_state(restored_state))
|
||||
restored_index = next(
|
||||
index
|
||||
for index in range(restored.task_tabs.count())
|
||||
if restored.task_tabs.tabData(index) == restored_state.key
|
||||
)
|
||||
self.assertIn("临时草稿", restored.task_tabs.tabText(restored_index))
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_first_failed_import_discards_new_empty_temporary_draft(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
config = self._config(temp_dir)
|
||||
accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
|
||||
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
||||
self.addCleanup(tab.close)
|
||||
bad_path = os.path.join(temp_dir, "not-an-image.png")
|
||||
with open(bad_path, "wb") as handle:
|
||||
handle.write(b"not an image")
|
||||
|
||||
state = tab._displayed_state
|
||||
with mock.patch.object(tab, "_start_thread", return_value=object()):
|
||||
tab._start_import(file_paths=[bad_path])
|
||||
worker = state.import_worker
|
||||
draft_id = state.project_id
|
||||
with mock.patch.object(tab, "_message"):
|
||||
tab._on_import_finished(state, worker.execute())
|
||||
|
||||
discarded = image_studio.get_project(
|
||||
draft_id,
|
||||
path=config["db_path"],
|
||||
include_deleted=True,
|
||||
)
|
||||
self.assertIsNotNone(discarded.deleted_at)
|
||||
self.assertIsNone(state.project_id)
|
||||
self.assertEqual([], image_studio.list_recoverable_draft_projects(path=config["db_path"]))
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_original_checkbox_click_and_keyboard_delete_keep_actions_separate(self):
|
||||
original_list = ProductOriginalList()
|
||||
self.addCleanup(original_list.close)
|
||||
|
||||
Reference in New Issue
Block a user