diff --git a/app/gui/tabs/collect.py b/app/gui/tabs/collect.py index d74a80b..5ae1d95 100644 --- a/app/gui/tabs/collect.py +++ b/app/gui/tabs/collect.py @@ -194,8 +194,9 @@ class CollectTab(QWidget): self._append_collect_log(safe_message) def _show_error(self, message): - QMessageBox.warning(self, "导入采集", str(message)) - self._set_status(str(message)) + text = str(message) + self._set_status(text, level="danger") + QTimer.singleShot(0, lambda: QMessageBox.warning(self, "导入采集", text)) def _show_account_guide(self, message): full_message = ( diff --git a/docs/tasks/T-582.md b/docs/tasks/T-582.md index 8909a71..e0ed020 100644 --- a/docs/tasks/T-582.md +++ b/docs/tasks/T-582.md @@ -3,7 +3,7 @@ id: T-582 title: 修复①导入 Excel 失败通知弹窗偶发只显示一半(延迟一拍弹窗) phase: 7 deps: [] -status: TODO +status: DONE created: 2026-07-10 --- @@ -55,4 +55,12 @@ created: 2026-07-10 ## 执行记录 -(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。) +- 2026-07-10:已实现 `_show_error()` 延迟弹出导入失败通知:状态栏仍同步显示错误,`QMessageBox.warning(self, "导入采集", text)` 改为经 `QTimer.singleShot(0, ...)` 下一拍执行,降低刷新/绘制同栈内立即弹模态框导致首帧显示不完整的概率。 +- 2026-07-10:未改导入成功路径;成功导入仍只刷新列表、更新 summary、状态栏显示“导入完成...”,不新增成功弹窗。未改 `_show_account_guide()`、删除批次确认框、HiDPI 全局设置、导入解析入库逻辑。 +- 2026-07-10:补 `tests/test_gui.py`:成功导入断言不会调用 `QTimer.singleShot` 和弹窗;导入异常时断言 `QTimer.singleShot(0, callback)` 被调用、回调执行前不直接弹窗、run log/脱敏错误/弹窗标题正文不回归。 +- 当前工作区验证通过: + - `py -3.10 -m unittest tests.test_gui.GuiTests.test_collect_tab_import_button_imports_excel_and_refreshes_tasks tests.test_gui.GuiTests.test_collect_tab_import_error_dialog_is_delayed_one_event_loop tests.test_gui.GuiTests.test_collect_tab_import_excel_writes_diagnostic_run_log` + - `python -m ruff check app tests main.py` + - `py -3.10 -m compileall app main.py` + - `git diff --check`(仅输出当前工作区既有 CRLF 提示,无空白错误) +- 当前工作区直接运行 `py -3.10 -m unittest tests.test_gui` 失败 2 项,原因是本任务开始前已有未提交改动把默认封面提示词从 `papa1` 改成「默认」,导致两个旧断言仍期望 `papa1`;该提示词改名不属于 T-582,本次未处理、未提交。提交后需用干净 worktree 验证本次提交本身。 diff --git a/tests/test_gui.py b/tests/test_gui.py index d3389f8..c8e9503 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -5907,10 +5907,14 @@ class GuiTests(TempDirMixin, unittest.TestCase): tab, "_choose_excel_files", return_value=[excel_path], - ), mock.patch("app.gui.excel.import_tasks", side_effect=fake_import) as import_tasks: + ), mock.patch("app.gui.excel.import_tasks", side_effect=fake_import) as import_tasks, mock.patch( + "app.gui.tabs.collect.QTimer.singleShot" + ) as single_shot, mock.patch("app.gui.QMessageBox") as message_box: tab.import_excel() import_tasks.assert_called_once_with([excel_path], path=cfg["db_path"]) + single_shot.assert_not_called() + message_box.warning.assert_not_called() self.assertEqual(1, tab.model.rowCount()) self.assertEqual("alias-a", tab.model.index(0, 1).data()) self.assertIn("1 文件", tab.summary_label.text()) @@ -5925,6 +5929,44 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assert_removed(temp_dir) + def test_collect_tab_import_error_dialog_is_delayed_one_event_loop(self): + with self.make_temp_dir() as temp_dir: + cfg = self.make_config(temp_dir) + excel_path = os.path.join(temp_dir, "input.xlsx") + statuses = [] + tab = CollectTab(config=cfg, status_callback=statuses.append) + self.addCleanup(tab.close) + + with mock.patch.object(tab, "_choose_excel_files", return_value=[excel_path]), mock.patch( + "app.gui.excel.import_tasks", + side_effect=RuntimeError("Excel导入失败 token=SECRET"), + ), mock.patch("app.gui.QMessageBox") as message_box, mock.patch( + "app.gui.tabs.collect.QTimer.singleShot" + ) as single_shot: + tab.import_excel() + + single_shot.assert_called_once() + self.assertEqual(0, single_shot.call_args[0][0]) + delayed_callback = single_shot.call_args[0][1] + message_box.warning.assert_not_called() + self.assertIn("token=***", statuses[-1]) + + run_log = db.list_run_logs(limit=1, run_type="import", path=cfg["db_path"])[0] + self.assertEqual("failed", run_log.status) + self.assertEqual(1, run_log.failed_count) + events = db.list_run_log_events(run_log.id, path=cfg["db_path"]) + messages = "\n".join(event.message for event in events) + self.assertIn("step=import result=failed", messages) + self.assertNotIn("SECRET", messages) + + delayed_callback() + message_box.warning.assert_called_once() + self.assertEqual("导入采集", message_box.warning.call_args[0][1]) + self.assertIn("Excel导入失败", message_box.warning.call_args[0][2]) + self.assertIn("token=***", message_box.warning.call_args[0][2]) + + self.assert_removed(temp_dir) + def test_collect_worker_collects_success_and_skips_unmatched_or_logged_out(self): with self.make_temp_dir() as temp_dir: cfg = self.make_config(temp_dir)