From 4e98a86fb318958e06e8086b492d987fc324344e Mon Sep 17 00:00:00 2001 From: chengma Date: Thu, 9 Jul 2026 12:19:29 +0800 Subject: [PATCH] =?UTF-8?q?T-569=20=E5=A2=9E=E5=8A=A0=E5=B0=81=E9=9D=A2?= =?UTF-8?q?=E7=94=BB=E5=BB=8A=E4=BB=BB=E5=8A=A1=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/gui/tabs/generate.py | 183 ++++++++++++++++++++++++--- docs/tasks/T-569.md | 20 ++- tests/test_gui.py | 262 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 447 insertions(+), 18 deletions(-) diff --git a/app/gui/tabs/generate.py b/app/gui/tabs/generate.py index 225b350..7f7ce9e 100644 --- a/app/gui/tabs/generate.py +++ b/app/gui/tabs/generate.py @@ -86,12 +86,19 @@ class CoverGalleryDialog(QDialog): bulk_running_checker=None, regenerate_running_callback=None, gallery_updated_callback=None, + visible_tasks=None, + task_index=None, + account_by_alias=None, ): super().__init__(parent) - self.task = task self.image_root = image_root self.db_path = db_path - self.account = account + self.visible_tasks = list(visible_tasks or [task]) + self.task_index = self._initial_task_index(task, task_index) + self.task = self.visible_tasks[self.task_index] if self.visible_tasks else task + self.account_by_alias = dict(account_by_alias or {}) + self.fallback_account = account + self.account = self._account_for_task(self.task) or account self.config = config or {} self.prompt_values = dict(prompt_values or {}) self.diagnostic_log_dir = diagnostic_log_dir @@ -106,8 +113,9 @@ class CoverGalleryDialog(QDialog): self.button_group.setExclusive(True) self.regenerate_worker = None self.regenerate_thread = None + self.regenerating_task_id = None self._regenerate_last_error = "" - self.candidates = image_paths.list_task_cover_candidates(image_root, task, account=account) + self.candidates = image_paths.list_task_cover_candidates(image_root, self.task, account=self.account) self.setWindowTitle(f"封面画廊:{getattr(task, 'item_id', '')}") layout = QVBoxLayout(self) @@ -117,6 +125,12 @@ class CoverGalleryDialog(QDialog): layout.addLayout(body_layout, 1) button_layout = QHBoxLayout() + self.previous_button = QPushButton("上一条") + self.next_button = QPushButton("下一条") + self.previous_button.clicked.connect(lambda checked=False: self.switch_task(-1)) + self.next_button.clicked.connect(lambda checked=False: self.switch_task(1)) + button_layout.addWidget(self.previous_button) + button_layout.addWidget(self.next_button) self.regenerate_button = QPushButton("重新生成") self.regenerate_button.clicked.connect(self.regenerate_cover) button_layout.addWidget(self.regenerate_button) @@ -131,6 +145,7 @@ class CoverGalleryDialog(QDialog): self._sync_initial_selection() self._update_status() + self._update_navigation_buttons() self._update_regenerate_button_state() self._fit_to_screen() @@ -138,17 +153,29 @@ class CoverGalleryDialog(QDialog): panel = QWidget() layout = QVBoxLayout(panel) layout.addWidget(QLabel("旧封面")) - old_path = getattr(self.task, "old_cover_path", None) - layout.addWidget( - _build_image_label( - old_path, - self.OLD_THUMBNAIL_SIZE, - empty_text="暂无旧封面", - ) + self.old_cover_label = _build_image_label( + getattr(self.task, "old_cover_path", None), + self.OLD_THUMBNAIL_SIZE, + empty_text="暂无旧封面", ) + layout.addWidget(self.old_cover_label) layout.addStretch(1) return panel + def _initial_task_index(self, task, task_index): + if self.visible_tasks: + if task_index is not None and 0 <= int(task_index) < len(self.visible_tasks): + return int(task_index) + task_id = getattr(task, "id", None) + for index, visible_task in enumerate(self.visible_tasks): + if getattr(visible_task, "id", None) == task_id: + return index + return 0 + + def _account_for_task(self, task): + alias = str(getattr(task, "alias", "") or "").strip() + return self.account_by_alias.get(alias) or self.fallback_account + def _candidate_panel(self): panel = QWidget() layout = QVBoxLayout(panel) @@ -207,6 +234,7 @@ class CoverGalleryDialog(QDialog): thumbnail.setMinimumSize(self.THUMBNAIL_SIZE, self.THUMBNAIL_SIZE) radio = QRadioButton(_candidate_label(candidate_path, self.current_path)) radio.setObjectName(f"coverCandidateRadio{index}") + radio.setFocusPolicy(Qt.ClickFocus) radio.toggled.connect( lambda checked, path=candidate_path: self._select_candidate(path) if checked else None ) @@ -235,6 +263,9 @@ class CoverGalleryDialog(QDialog): self._update_status() def _update_status(self): + if self._is_current_task_regenerating(): + self.status_label.setText("封面重新生成中,请稍候") + return if not self.candidates: self.status_label.setText("暂无生成封面图片") self.save_button.setEnabled(False) @@ -254,12 +285,16 @@ class CoverGalleryDialog(QDialog): self.status_label.setText("已选择新的生成封面,保存后用于后续更新蝦皮") def save_selection(self, checked=False): + return self._save_selected_cover(close_on_success=True) + + def _save_selected_cover(self, close_on_success=False): if not self.selected_path: QMessageBox.warning(self, "封面画廊", "请先选择一张生成封面") return False if _same_file(self.selected_path, self.current_path): self.changed = False - self.accept() + if close_on_success: + self.accept() return True if self._needs_committed_confirmation() and not self._confirm_committed_save(): return False @@ -268,9 +303,17 @@ class CoverGalleryDialog(QDialog): except Exception as exc: QMessageBox.warning(self, "封面画廊", f"保存当前封面失败:{exc}") return False + fresh_task = db.get_task(self.task.id, path=self.db_path) + if fresh_task is not None: + self.task = fresh_task + self.visible_tasks[self.task_index] = fresh_task self.current_path = _normalize_file_path(self.selected_path) self.changed = True - self.accept() + self._update_status() + if self.gallery_updated_callback is not None: + self.gallery_updated_callback(self.task) + if close_on_success: + self.accept() return True def regenerate_cover(self, checked=False): @@ -301,6 +344,7 @@ class CoverGalleryDialog(QDialog): QMessageBox.warning(self, "重新生成封面", "任务不存在或已删除") self.status_label.setText("重新生成封面失败:任务不存在或已删除") return False + self.regenerating_task_id = getattr(fresh_task, "id", None) self._reload_task_and_candidates(fresh_task) self.status_label.setText(f"开始重新生成商品 {getattr(fresh_task, 'item_id', '')} 的封面图片") worker = GenerateWorker( @@ -349,6 +393,96 @@ class CoverGalleryDialog(QDialog): box.exec() return box.clickedButton() is save_button + def switch_task(self, offset): + target_index = self.task_index + int(offset) + if target_index < 0 or target_index >= len(self.visible_tasks): + return False + if not self._confirm_unsaved_selection(): + return False + self._load_task_at_index(target_index) + return True + + def _load_task_at_index(self, target_index): + self.task_index = int(target_index) + target_task = self.visible_tasks[self.task_index] + fresh_task = db.get_task(getattr(target_task, "id", None), path=self.db_path) or target_task + self.visible_tasks[self.task_index] = fresh_task + self.task = fresh_task + self.account = self._account_for_task(fresh_task) + self.setWindowTitle(f"封面画廊:{getattr(fresh_task, 'item_id', '')}") + _set_image_label( + self.old_cover_label, + getattr(fresh_task, "old_cover_path", None), + self.OLD_THUMBNAIL_SIZE, + "暂无旧封面", + ) + self._reload_task_and_candidates(fresh_task) + self._update_navigation_buttons() + self._update_regenerate_button_state() + + def _update_navigation_buttons(self): + self.previous_button.setEnabled(self.task_index > 0) + self.next_button.setEnabled(self.task_index < len(self.visible_tasks) - 1) + + def _has_unsaved_selection(self): + if not self.selected_path: + return False + return not _same_file(self.selected_path, self.current_path) + + def _confirm_unsaved_selection(self): + if not self._has_unsaved_selection(): + return True + box = QMessageBox(self) + box.setWindowTitle("未保存封面选择") + box.setText("当前已选择另一张生成封面,但尚未保存。") + save_button = box.addButton("保存", QMessageBox.AcceptRole) + discard_button = box.addButton("不保存", QMessageBox.DestructiveRole) + box.addButton("取消", QMessageBox.RejectRole) + box.setDefaultButton(save_button) + box.exec() + clicked = box.clickedButton() + if clicked is save_button: + return self._save_selected_cover(close_on_success=False) + if clicked is discard_button: + return True + return False + + def reject(self): + if self._confirm_unsaved_selection(): + super().reject() + + def closeEvent(self, event): + if self._confirm_unsaved_selection(): + event.accept() + return + event.ignore() + + def keyPressEvent(self, event): + if event.key() == Qt.Key_Up and self._should_consume_navigation_key(): + self.switch_task(-1) + event.accept() + return + if event.key() == Qt.Key_Down and self._should_consume_navigation_key(): + self.switch_task(1) + event.accept() + return + super().keyPressEvent(event) + + def _should_consume_navigation_key(self): + focus = QApplication.focusWidget() + if focus is None or focus is self: + return True + return not isinstance( + focus, + ( + QPushButton, + QLineEdit, + QPlainTextEdit, + QComboBox, + QSpinBox, + ), + ) + def open_original_image(self, image_path): dialog = OriginalImageDialog(image_path, self) dialog.exec() @@ -356,6 +490,13 @@ class CoverGalleryDialog(QDialog): def _bulk_generate_running(self): return bool(self.bulk_running_checker and self.bulk_running_checker()) + def _is_current_task_regenerating(self): + return ( + self.regenerate_thread is not None + and self.regenerating_task_id is not None + and getattr(self.task, "id", None) == self.regenerating_task_id + ) + def _set_regenerate_running(self, running): self.save_button.setEnabled(not running and self.selected_path is not None) self.regenerate_button.setEnabled(not running and not self._bulk_generate_running()) @@ -379,6 +520,7 @@ class CoverGalleryDialog(QDialog): def _reload_task_and_candidates(self, task=None): if task is not None: self.task = task + self.account = self._account_for_task(task) self.current_path = _normalize_file_path(getattr(self.task, "new_cover_path", None)) self.selected_path = None self.candidates = image_paths.list_task_cover_candidates(self.image_root, self.task, account=self.account) @@ -431,6 +573,7 @@ class CoverGalleryDialog(QDialog): self._finish_regenerate(summary, cancelled=True) def _finish_regenerate(self, summary, cancelled=False): + self.regenerating_task_id = None fresh_task = db.get_task(self.task.id, path=self.db_path) if fresh_task is not None: self._reload_task_and_candidates(fresh_task) @@ -450,6 +593,7 @@ class CoverGalleryDialog(QDialog): if self.regenerate_thread is thread: self.regenerate_thread = None self.regenerate_worker = None + self.regenerating_task_id = None self._set_regenerate_running(False) def _fit_to_screen(self): @@ -488,10 +632,16 @@ def _build_image_label(image_path, size, empty_text): label = QLabel() label.setAlignment(Qt.AlignCenter) label.setMinimumSize(size, size) + _set_image_label(label, image_path, size, empty_text) + return label + + +def _set_image_label(label, image_path, size, empty_text): + label.clear() image = _load_image(image_path) if image_path else QImage() if image.isNull(): label.setText(empty_text) - return label + return label.setPixmap( QPixmap.fromImage( image.scaled( @@ -502,7 +652,6 @@ def _build_image_label(image_path, size, empty_text): ) ) ) - return label def _candidate_label(candidate_path, current_path): @@ -1416,7 +1565,8 @@ class GenerateTab(QWidget): def show_task_images(self, index): if index.isValid() and index.column() == 3: return - task = self.model.task_at(index.row()) if index.isValid() else self._selected_task() + row = index.row() if index.isValid() else self.task_table.currentIndex().row() + task = self.model.task_at(row) if row >= 0 else self._selected_task() if task is None: self._set_status("没有可预览的任务") return @@ -1436,6 +1586,9 @@ class GenerateTab(QWidget): bulk_running_checker=lambda: self.generate_thread is not None, regenerate_running_callback=self._set_cover_gallery_regenerate_running, gallery_updated_callback=self._on_cover_gallery_updated, + visible_tasks=list(self.model.tasks), + task_index=row if row >= 0 else 0, + account_by_alias=self.model.account_by_alias, ) if dialog.exec() == QDialog.Accepted: self.refresh_tasks() diff --git a/docs/tasks/T-569.md b/docs/tasks/T-569.md index 6ec9af4..c522b3c 100644 --- a/docs/tasks/T-569.md +++ b/docs/tasks/T-569.md @@ -3,7 +3,7 @@ id: T-569 title: ②封面画廊上下条切换与未保存选择拦截 phase: 7 deps: [T-567] -status: TODO +status: DONE created: 2026-07-09 --- @@ -66,4 +66,20 @@ T-567 的封面画廊只处理当前单条任务。运营批量检查时需要 ## 执行记录 -(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。) +- 2026-07-09 完成 T-569。 +- 修改 `app/gui/tabs/generate.py`: + - ②封面画廊新增「上一条」「下一条」按钮,目标任务来自打开画廊时②当前可见表格任务列表,按相邻行切换,首尾边界按钮自动置灰。 + - 切换任务时只刷新弹窗内部任务、旧封面和候选画廊内容,不重建整个窗口,窗口尺寸和位置保持不变;切换时会重新读取 DB 最新任务和候选文件。 + - 支持 `↑` / `↓` 键走同一套上一条/下一条切换逻辑;候选 `QRadioButton` 改为 `ClickFocus`,避免方向键在 radio 组内改候选。 + - 增加统一未保存选择拦截:按钮切换、方向键切换、取消/关闭弹窗前,如果当前选中候选不同于当前 DB 封面指针,会弹中文三态确认「保存 / 不保存 / 取消」。保存路径复用 `db.update_generated_cover()`,已提交线上任务仍先弹“本地换封面不会回滚蝦皮,重复更新会再次提交线上”确认;保存失败保留当前任务。 + - 与 T-568 兼容:单任务封面重生运行中切换回该任务时显示“封面重新生成中”,完成/失败后继续刷新 DB 和候选列表。 +- 修改 `tests/test_gui.py`: + - 覆盖上一条/下一条按当前可见列表切换、边界按钮状态、切换不改变窗口尺寸/位置。 + - 覆盖 `↑`/`↓` 键切换任务且不改变候选 radio 选择。 + - 覆盖未保存选择在切换和关闭时的「保存 / 不保存 / 取消」三种路径,以及已提交任务保存前的二次确认。 +- 验证通过: + - `py -3.10 -m unittest tests.test_gui` + - `python -m ruff check app tests main.py` + - `py -3.10 -m compileall app main.py` + - `py -3.10 -m unittest discover -s tests`(302 tests;PySide6 字体目录警告不影响结果) + - `git diff --check` diff --git a/tests/test_gui.py b/tests/test_gui.py index 99457cd..4d71894 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -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)