T-569 增加封面画廊任务切换

This commit is contained in:
chengma
2026-07-09 12:19:29 +08:00
parent ae4daacbbf
commit 4e98a86fb3
3 changed files with 447 additions and 18 deletions
+261 -1
View File
@@ -18,7 +18,7 @@ if gui.QT_IMPORT_ERROR is not None:
raise unittest.SkipTest("PySide6 未安装")
from PySide6.QtCore import QItemSelectionModel, QRect
from PySide6.QtGui import QImage, QTextCursor
from PySide6.QtGui import QImage, QKeyEvent, QTextCursor
from PySide6.QtWidgets import QApplication, QCheckBox, QLineEdit, QPlainTextEdit, QProgressBar, QTableView
from app.gui import (
@@ -156,6 +156,40 @@ class GuiTests(TempDirMixin, unittest.TestCase):
db.set_generated(task.id, "新标题", canonical, path=cfg["db_path"])
return cfg, account, db.get_task(task.id, path=cfg["db_path"]), canonical
def _cover_gallery_task_set(self, temp_dir, count=3):
cfg = self.make_config(temp_dir)
cfg.setdefault("ai", {})["generate_cover"] = True
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
db.init_db(cfg["db_path"])
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
rows = []
for index in range(count):
rows.append(
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": index + 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": f"5110063951{index}",
}
)
db.insert_tasks(batch_id, rows, path=cfg["db_path"])
tasks = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])
canonicals = []
archives = []
for index, task in enumerate(tasks):
old_cover = self.write_test_image(os.path.join(temp_dir, f"old-{index}.jpg"))
canonical = image_paths.task_image_path(cfg["image_dir"], task, account, "new")
archive = os.path.splitext(canonical)[0] + "_20260709101000.jpg"
self.write_test_image(canonical)
self.write_test_image(archive)
db.set_collected(task.id, f"旧标题{index}", old_cover, path=cfg["db_path"])
db.set_generated(task.id, f"新标题{index}", canonical, path=cfg["db_path"])
canonicals.append(canonical)
archives.append(archive)
return cfg, account, db.list_tasks(batch_id=batch_id, path=cfg["db_path"]), canonicals, archives
def assert_foreground(self, model, row, column, color):
value = model.data(model.index(row, column), gui.Qt.ForegroundRole)
self.assertIsNotNone(value)
@@ -217,6 +251,53 @@ class GuiTests(TempDirMixin, unittest.TestCase):
return FakeMessageBox, boxes
def make_sequence_message_box(self, selected_labels):
boxes = []
labels = list(selected_labels)
class FakeMessageBox:
AcceptRole = object()
DestructiveRole = object()
RejectRole = object()
def __init__(self, parent=None):
self.parent = parent
self.title = ""
self.text = ""
self.buttons = {}
self.default_button = None
self.selected_label = labels.pop(0)
boxes.append(self)
def setWindowTitle(self, title):
self.title = title
def setText(self, text):
self.text = text
def addButton(self, label, role):
button = object()
self.buttons[label] = button
return button
def setDefaultButton(self, button):
self.default_button = button
def exec(self):
return 0
def clickedButton(self):
return self.buttons[self.selected_label]
@staticmethod
def warning(parent, title, text):
box = FakeMessageBox(parent)
box.setWindowTitle(title)
box.setText(text)
return 0
return FakeMessageBox, boxes
def test_collect_table_shows_product_unavailable_only_for_explicit_error(self):
account = SimpleNamespace(alias="papa", account_name="papa 店铺")
invalid_task = SimpleNamespace(
@@ -2277,6 +2358,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual(1, updated.committed)
self.assertIn("重复更新会再次提交线上", boxes[0].text)
self.assertFalse(dialog.changed)
dialog.selected_path = dialog.current_path
self.assert_removed(temp_dir)
@@ -2343,6 +2425,184 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_cover_gallery_previous_next_switch_visible_tasks_without_resizing(self):
with self.make_temp_dir() as temp_dir:
cfg, account, tasks, canonicals, _archives = self._cover_gallery_task_set(temp_dir, count=3)
dialog = CoverGalleryDialog(
tasks[1],
cfg["image_dir"],
cfg["db_path"],
account=account,
visible_tasks=tasks,
task_index=1,
account_by_alias={"alias-a": account},
)
self.addCleanup(dialog.close)
dialog.resize(820, 520)
dialog.move(25, 35)
size = dialog.size()
position = dialog.pos()
self.assertTrue(dialog.previous_button.isEnabled())
self.assertTrue(dialog.next_button.isEnabled())
self.assertTrue(dialog.switch_task(1))
self.assertEqual(tasks[2].id, dialog.task.id)
self.assertTrue(dialog.previous_button.isEnabled())
self.assertFalse(dialog.next_button.isEnabled())
self.assertEqual(canonicals[2], dialog.selected_path)
self.assertEqual(size, dialog.size())
self.assertEqual(position, dialog.pos())
self.assert_removed(temp_dir)
def test_cover_gallery_arrow_keys_switch_tasks_without_changing_radio_choice(self):
with self.make_temp_dir() as temp_dir:
cfg, account, tasks, canonicals, archives = self._cover_gallery_task_set(temp_dir, count=2)
dialog = CoverGalleryDialog(
tasks[0],
cfg["image_dir"],
cfg["db_path"],
account=account,
visible_tasks=tasks,
task_index=0,
account_by_alias={"alias-a": account},
)
self.addCleanup(dialog.close)
dialog.candidate_buttons[canonicals[0]].setFocus()
event = QKeyEvent(QKeyEvent.KeyPress, gui.Qt.Key_Down, gui.Qt.NoModifier)
dialog.keyPressEvent(event)
self.assertTrue(event.isAccepted())
self.assertEqual(tasks[1].id, dialog.task.id)
self.assertEqual(canonicals[1], dialog.selected_path)
self.assertFalse(dialog.candidate_buttons[archives[1]].isChecked())
self.assert_removed(temp_dir)
def test_cover_gallery_unsaved_switch_save_discard_and_cancel_paths(self):
with self.make_temp_dir() as temp_dir:
cfg, account, tasks, canonicals, archives = self._cover_gallery_task_set(temp_dir, count=2)
save_dialog = CoverGalleryDialog(
tasks[0],
cfg["image_dir"],
cfg["db_path"],
account=account,
visible_tasks=tasks,
task_index=0,
account_by_alias={"alias-a": account},
)
self.addCleanup(save_dialog.close)
save_dialog.candidate_buttons[archives[0]].setChecked(True)
message_box, _ = self.make_fake_message_box("保存")
with mock.patch("app.gui.tabs.generate.QMessageBox", message_box):
self.assertTrue(save_dialog.switch_task(1))
self.assertEqual(os.path.abspath(archives[0]), db.get_task(tasks[0].id, path=cfg["db_path"]).new_cover_path)
self.assertEqual(tasks[1].id, save_dialog.task.id)
db.update_generated_cover(tasks[0].id, canonicals[0], path=cfg["db_path"])
tasks = [db.get_task(task.id, path=cfg["db_path"]) for task in tasks]
discard_dialog = CoverGalleryDialog(
tasks[0],
cfg["image_dir"],
cfg["db_path"],
account=account,
visible_tasks=tasks,
task_index=0,
account_by_alias={"alias-a": account},
)
self.addCleanup(discard_dialog.close)
discard_dialog.candidate_buttons[archives[0]].setChecked(True)
message_box, _ = self.make_fake_message_box("不保存")
with mock.patch("app.gui.tabs.generate.QMessageBox", message_box):
self.assertTrue(discard_dialog.switch_task(1))
self.assertEqual(canonicals[0], db.get_task(tasks[0].id, path=cfg["db_path"]).new_cover_path)
self.assertEqual(tasks[1].id, discard_dialog.task.id)
cancel_dialog = CoverGalleryDialog(
tasks[0],
cfg["image_dir"],
cfg["db_path"],
account=account,
visible_tasks=tasks,
task_index=0,
account_by_alias={"alias-a": account},
)
self.addCleanup(cancel_dialog.close)
cancel_dialog.candidate_buttons[archives[0]].setChecked(True)
message_box, _ = self.make_fake_message_box("取消")
with mock.patch("app.gui.tabs.generate.QMessageBox", message_box):
self.assertFalse(cancel_dialog.switch_task(1))
self.assertEqual(canonicals[0], db.get_task(tasks[0].id, path=cfg["db_path"]).new_cover_path)
self.assertEqual(tasks[0].id, cancel_dialog.task.id)
cancel_dialog.selected_path = cancel_dialog.current_path
self.assert_removed(temp_dir)
def test_cover_gallery_unsaved_close_uses_same_three_choice_guard(self):
class FakeCloseEvent:
def __init__(self):
self.accepted = False
self.ignored = False
def accept(self):
self.accepted = True
def ignore(self):
self.ignored = True
with self.make_temp_dir() as temp_dir:
cfg, account, tasks, canonicals, archives = self._cover_gallery_task_set(temp_dir, count=1)
dialog = CoverGalleryDialog(tasks[0], cfg["image_dir"], cfg["db_path"], account=account)
self.addCleanup(dialog.close)
dialog.candidate_buttons[archives[0]].setChecked(True)
event = FakeCloseEvent()
message_box, _ = self.make_fake_message_box("取消")
with mock.patch("app.gui.tabs.generate.QMessageBox", message_box):
dialog.closeEvent(event)
self.assertTrue(event.ignored)
self.assertFalse(event.accepted)
self.assertEqual(canonicals[0], db.get_task(tasks[0].id, path=cfg["db_path"]).new_cover_path)
message_box, _ = self.make_fake_message_box("不保存")
event = FakeCloseEvent()
with mock.patch("app.gui.tabs.generate.QMessageBox", message_box):
dialog.closeEvent(event)
self.assertTrue(event.accepted)
dialog.selected_path = dialog.current_path
self.assert_removed(temp_dir)
def test_cover_gallery_unsaved_save_for_committed_task_keeps_warning(self):
with self.make_temp_dir() as temp_dir:
cfg, account, tasks, _canonicals, archives = self._cover_gallery_task_set(temp_dir, count=2)
db.set_applied(tasks[0].id, True, path=cfg["db_path"])
tasks = [db.get_task(task.id, path=cfg["db_path"]) for task in tasks]
dialog = CoverGalleryDialog(
tasks[0],
cfg["image_dir"],
cfg["db_path"],
account=account,
visible_tasks=tasks,
task_index=0,
account_by_alias={"alias-a": account},
)
self.addCleanup(dialog.close)
dialog.candidate_buttons[archives[0]].setChecked(True)
message_box, boxes = self.make_sequence_message_box(["保存", "确认保存"])
with mock.patch("app.gui.tabs.generate.QMessageBox", message_box):
self.assertTrue(dialog.switch_task(1))
self.assertEqual(os.path.abspath(archives[0]), db.get_task(tasks[0].id, path=cfg["db_path"]).new_cover_path)
self.assertIn("未保存", boxes[0].title)
self.assertIn("不会回滚蝦皮", boxes[1].text)
self.assertEqual(tasks[1].id, dialog.task.id)
self.assert_removed(temp_dir)
def test_cover_gallery_regenerate_cancel_has_no_side_effect(self):
with self.make_temp_dir() as temp_dir:
cfg, account, task, canonical = self._cover_gallery_task(temp_dir)