diff --git a/app/appconfig.py b/app/appconfig.py index 060ec6e..e1b451c 100644 --- a/app/appconfig.py +++ b/app/appconfig.py @@ -71,7 +71,7 @@ LEGACY_USER_DATA_PATHS = ( ) DEFAULT_CONFIG = { - "chrome_path": r"C:\Program Files\Google\Chrome\Application\chrome.exe", + "chrome_path": "", "user_data_root": "chrome_user_data_dir", "image_dir": "images", "db_path": "cmshopee.db", diff --git a/app/chrome.py b/app/chrome.py index e7800bd..fab46a5 100644 --- a/app/chrome.py +++ b/app/chrome.py @@ -5,6 +5,7 @@ from __future__ import annotations import json import os import re +import shutil import subprocess import time import urllib.error @@ -15,12 +16,166 @@ from . import config as account_config CDP_HOST = "127.0.0.1" +CHROME_APP_PATHS_KEY = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe" class ChromeLaunchError(RuntimeError): """Raised when Chrome launch configuration is invalid.""" +def _normalize_chrome_path(value) -> str: + """Return a filesystem-ready Chrome path, or an empty string.""" + + text = str(value or "").strip() + if len(text) >= 2 and text[0] == text[-1] and text[0] in {"\"", "'"}: + text = text[1:-1].strip() + if not text: + return "" + text = os.path.expandvars(os.path.expanduser(text)) + return os.path.abspath(text) + + +def _load_winreg(): + """Return winreg only on Windows so non-Windows callers stay harmless.""" + + if os.name != "nt": + return None + try: + import winreg + except ImportError: + return None + return winreg + + +def _registry_view_flags(winreg): + flags = [0] + for name in ("KEY_WOW64_64KEY", "KEY_WOW64_32KEY"): + value = getattr(winreg, name, 0) + if value and value not in flags: + flags.append(value) + return flags + + +def _close_registry_key(key): + close = getattr(key, "Close", None) or getattr(key, "close", None) + if callable(close): + try: + close() + except OSError: + pass + + +def _registry_chrome_candidates(): + """Yield Chrome App Paths values without exposing registry failures.""" + + winreg = _load_winreg() + if winreg is None: + return + + key_read = getattr(winreg, "KEY_READ", 0) + hives = ( + getattr(winreg, "HKEY_CURRENT_USER", None), + getattr(winreg, "HKEY_LOCAL_MACHINE", None), + ) + seen = set() + for hive in hives: + if hive is None: + continue + for view_flag in _registry_view_flags(winreg): + marker = (hive, view_flag) + if marker in seen: + continue + seen.add(marker) + key = None + try: + key = winreg.OpenKey( + hive, + CHROME_APP_PATHS_KEY, + 0, + key_read | view_flag, + ) + value, _value_type = winreg.QueryValueEx(key, "") + except (AttributeError, OSError): + continue + finally: + if key is not None: + _close_registry_key(key) + if value: + yield value + + +def _standard_chrome_candidates(): + """Yield the supported Windows Chrome install locations.""" + + suffix = ("Google", "Chrome", "Application", "chrome.exe") + for variable in ("ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"): + base = os.environ.get(variable, "").strip() + if base: + yield os.path.join(base, *suffix) + + +def is_configured_chrome_path_valid(value) -> bool: + """Return whether a configured Chrome executable can be launched safely.""" + + normalized = _normalize_chrome_path(value) + if normalized and os.path.isfile(normalized): + return True + text = str(value or "").strip().strip("\"") + return text.lower() == "chrome.exe" and bool(shutil.which(text)) + + +def detect_chrome_path() -> str: + """Find an installed Google Chrome executable without probing other browsers.""" + + for candidate in _registry_chrome_candidates(): + normalized = _normalize_chrome_path(candidate) + if normalized and os.path.isfile(normalized): + return normalized + for candidate in _standard_chrome_candidates(): + normalized = _normalize_chrome_path(candidate) + if normalized and os.path.isfile(normalized): + return normalized + path_candidate = shutil.which("chrome.exe") + normalized = _normalize_chrome_path(path_candidate) + if normalized and os.path.isfile(normalized): + return normalized + return "" + + +def ensure_configured_chrome_path(config=None, config_path=None) -> dict: + """Persist an auto-detected path only when the configured path is unusable.""" + + path = config_path or (config or {}).get("config_path") or appconfig.CONFIG_PATH + current = appconfig.load_config(path) if config is None else config + configured_path = appconfig.chrome_path(current) + if is_configured_chrome_path_valid(configured_path): + return { + "changed": False, + "config": current, + "path": configured_path, + "message": "", + } + + detected_path = detect_chrome_path() + if not detected_path: + return { + "changed": False, + "config": current, + "path": configured_path, + "message": "", + } + + updated = dict(current) + updated["chrome_path"] = detected_path + saved = appconfig.save_config(updated, path=path) + return { + "changed": True, + "config": saved, + "path": detected_path, + "message": f"已自动定位 Chrome:{detected_path}", + } + + def _account_value(account, field, default=None): if isinstance(account, dict): return account.get(field, default) diff --git a/app/gui/__init__.py b/app/gui/__init__.py index 52cb725..4b30526 100644 --- a/app/gui/__init__.py +++ b/app/gui/__init__.py @@ -6,7 +6,7 @@ import os import sys import webbrowser -from .. import appconfig, diagnostics, update_check +from .. import appconfig, chrome, diagnostics, update_check from ..version import APP_NAME, display_name from . import widgets as _widgets from .widgets import * @@ -136,6 +136,14 @@ def main() -> int: return 1 if not _run_startup_update_gate(): return 1 - window = MainWindow() + try: + startup = chrome.ensure_configured_chrome_path() + except (OSError, appconfig.ConfigError) as exc: + QMessageBox.critical(None, "启动配置错误", str(exc)) + return 1 + window = MainWindow( + config=startup["config"], + startup_status=startup["message"], + ) window.show() return app.exec() diff --git a/app/gui/main_window.py b/app/gui/main_window.py index 0889afa..315a953 100644 --- a/app/gui/main_window.py +++ b/app/gui/main_window.py @@ -74,7 +74,14 @@ def _fit_and_center_window( class MainWindow(QMainWindow): """Main application window with the fixed five-tab workflow.""" - def __init__(self, db_path=None, config=None, config_path=None, ai_models_path=None): + def __init__( + self, + db_path=None, + config=None, + config_path=None, + ai_models_path=None, + startup_status="", + ): super().__init__() self._initial_window_fit_applied_after_show = False self.config = appconfig.load_config(config_path or appconfig.CONFIG_PATH) if config is None else config @@ -104,6 +111,8 @@ class MainWindow(QMainWindow): self.tabs.setTabIcon(TAB_TITLES.index("③ 更新蝦皮"), _warning_dot_icon()) self.setCentralWidget(self.tabs) self.show_status("就绪", level="muted") + if startup_status: + self.show_status(startup_status, level="success") def showEvent(self, event): super().showEvent(event) diff --git a/app/gui/tabs/settings.py b/app/gui/tabs/settings.py index fa0369b..d78bd86 100644 --- a/app/gui/tabs/settings.py +++ b/app/gui/tabs/settings.py @@ -6,6 +6,7 @@ from contextlib import contextmanager import os from ... import ai as ai_module +from ... import chrome from ..widgets import * from ..workers import AIModelTestWorker as _RealAIModelTestWorker from ..workers import CMHubSettingsWorker as _RealCMHubSettingsWorker @@ -165,6 +166,8 @@ class SettingsTab(QWidget): self.chrome_path_edit.setObjectName("chromePathEdit") self.chrome_path_browse_button = QPushButton("选择...") self.chrome_path_browse_button.setObjectName("chromePathBrowseButton") + self.chrome_path_detect_button = QPushButton("自动检测") + self.chrome_path_detect_button.setObjectName("chromePathAutoDetectButton") self.chrome_path_widget = QWidget() self.chrome_path_widget.setObjectName("chromePathWidget") chrome_path_layout = QHBoxLayout(self.chrome_path_widget) @@ -172,6 +175,7 @@ class SettingsTab(QWidget): chrome_path_layout.setSpacing(8) chrome_path_layout.addWidget(self.chrome_path_edit, 1) chrome_path_layout.addWidget(self.chrome_path_browse_button) + chrome_path_layout.addWidget(self.chrome_path_detect_button) self.user_data_root_edit = QLineEdit() self.user_data_root_edit.setObjectName("userDataRootEdit") self.user_data_root_edit.setEnabled(False) @@ -393,6 +397,7 @@ class SettingsTab(QWidget): self._update_response_timeout_label ) self.chrome_path_browse_button.clicked.connect(self.browse_chrome_path) + self.chrome_path_detect_button.clicked.connect(self.detect_chrome_path) self.save_config_button.clicked.connect(self.save_app_settings) self._connect_dirty_signals() @@ -535,6 +540,17 @@ class SettingsTab(QWidget): return self.chrome_path_edit.setText(path) + def detect_chrome_path(self, checked=False): + path = chrome.detect_chrome_path() + if not path: + self._set_status( + "未找到 Chrome,请点『选择...』手动指定,或先安装 Chrome", + level="warning", + ) + return + self.chrome_path_edit.setText(path) + self._set_status(f"已自动定位 Chrome:{path}", level="success") + def discard_unsaved_changes(self): try: saved = appconfig.load_config(self.config_path) diff --git a/docs/04-architecture.md b/docs/04-architecture.md index d182d67..e5d80eb 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -445,6 +445,7 @@ data/images///__new. # AI 生成的新 - 无 Shopee tab 时打开卖家中心根地址 `https:///`(默认 `https://seller.shopee.tw/`),重定向到登录页或缺会话 Cookie(`SPC_ST`/`SPC_U`)→ 未登录;不自动登录,提示人工登录。 - ④「启动登录」只负责准备该账号独立 user-data-dir + CDP 端口的 Chrome,供用户人工登录;该入口必须幂等:若端口已响应,复用现有账号 Chrome 并打开/激活卖家中心登录 tab,不再新开 Chrome;若端口未响应,才启动 Chrome。「检测登录」只验证当前 Chrome/CDP/会话 Cookie 是否可用。① 采集的预检会复用同一幂等启动能力,自动确保本轮匹配账号 Chrome 就绪但不自动登录;③ 更新的预检只检测,不自动启动缺失浏览器。若商品详情页或卖家中心重定向到 `accounts.shopee.tw/seller/login`,必须按登录页处理,返回 `LOGIN_PAGE` 并在 GUI 显示未登录。 +- T-585 后新配置的 `chrome_path` 默认留空。程序在数据目录就绪、强制升级检查通过后、创建主窗口前,仅当当前路径为空或不可启动时,按当前用户/本机 Windows App Paths(含 64/32 位视图)、标准 Chrome 目录和 `PATH` 自动寻找 `chrome.exe`;命中才持久化归一化绝对路径,并在状态栏显示“已自动定位 Chrome”。有效的自定义/便携版路径和可从 PATH 启动的 `chrome.exe` 不覆盖;不扫描整盘、不检测 Edge 或 Chromium。⑤的“自动检测”只填入输入框并标记未保存,仍由用户点“保存设置”确认写入。 ## 七、CDP 已验证事实(务必遵守) diff --git a/docs/tasks/T-585.md b/docs/tasks/T-585.md index 672f3cf..e76b28e 100644 --- a/docs/tasks/T-585.md +++ b/docs/tasks/T-585.md @@ -3,7 +3,7 @@ id: T-585 title: Chrome 路径自动检测(注册表+标准目录,Chrome-only,仅空/失效才填)+ ⑤「自动检测」按钮 phase: 7 deps: [T-557] -status: TODO +status: DONE created: 2026-07-10 --- @@ -67,4 +67,7 @@ created: 2026-07-10 ## 执行记录 -(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。) +- 已完成:`appconfig.DEFAULT_CONFIG["chrome_path"]` 改为空;`app.chrome` 新增 Chrome-only 路径归一化、HKCU/HKLM App Paths(64/32 位视图)、标准目录和 PATH 检测,以及仅在当前配置无效时持久化自动定位结果的服务。 +- 已完成:GUI 启动在数据目录和强制升级检查通过后执行自动定位,并把同一份配置传给主窗口;命中后状态栏保留“已自动定位 Chrome”。⑤设置页新增“自动检测”,只填输入框并标记未保存,不直接写配置;未命中不清空原值。 +- 已同步:`docs/04-architecture.md` 记录路径来源与覆盖边界;`docs/troubleshooting.md` 增加 Chrome 路径排障步骤。 +- 验证:干净工作树执行 `py -3.10 -m unittest discover -s tests`(335 项通过)、`python -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check`,全部通过。新增测试覆盖注册表视图/优先级、带引号环境变量值、LOCALAPPDATA、Edge 排除、PATH 保护、空/失效/有效配置,以及设置页和启动状态栏行为。 diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 68e075e..18fe559 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -2,6 +2,22 @@ > 本文只记录可复用的本地排障步骤。T-538 后用户数据默认位于 `data/`,涉及 `data/config.json`、`data/config/ai_models.json`、`data/config/cmhub.json`、`data/cmshopee.db`、`data/chrome_user_data_dir/`、`data/images/` 时,默认它们是本机敏感或业务数据,必须保持 gitignore,不把真实密码、API Key、Cookie、token 写入文档、日志或提交。 +## ④启动登录提示 Chrome 路径无效或找不到 Chrome + +### 推荐修复 + +打开⑤“设置”,在“Chrome路径”右侧先点“自动检测”。检测到后会填入完整路径并提示结果,随后点“保存设置”。 + +如果没有检测到,不会清空当前输入内容。请点“选择...”手动选择本机的 `chrome.exe`;便携版、任意目录解压版或没有注册表记录的 Chrome 都需要这一步。不要选择 Edge、Chromium 或文件夹路径,本软件只使用 Google Chrome 保持各账号独立登录态。 + +### 检查顺序 + +1. 确认本机已安装 Google Chrome,并能手动打开。 +2. 在⑤先点“自动检测”,再保存设置。 +3. 仍失败时,点“选择...”定位 `chrome.exe`,再回④点击“启动登录”。 + +程序首次启动只会在当前路径为空或已失效时自动定位 Chrome;已经保存且有效的自定义路径不会被自动覆盖。 + ## 启动时报 “AI 模型 category 必须是 text 或 image” ### 现象 diff --git a/tests/test_appconfig.py b/tests/test_appconfig.py index 91aab54..446d416 100644 --- a/tests/test_appconfig.py +++ b/tests/test_appconfig.py @@ -18,6 +18,7 @@ class AppConfigTests(TempDirMixin, unittest.TestCase): config = appconfig.load_config(config_path) self.assertTrue(os.path.exists(config_path)) + self.assertEqual("", config["chrome_path"]) self.assertEqual(os.path.join(temp_dir, "images"), appconfig.image_dir(config)) self.assertEqual(os.path.join(temp_dir, "chrome_user_data_dir"), appconfig.user_data_root(config)) self.assertEqual(os.path.join(temp_dir, "cmshopee.db"), appconfig.db_path(config)) diff --git a/tests/test_chrome.py b/tests/test_chrome.py index 17f6b46..2f4b305 100644 --- a/tests/test_chrome.py +++ b/tests/test_chrome.py @@ -12,7 +12,7 @@ sys.path.insert(0, os.path.dirname(__file__)) from _helpers import TempDirMixin -from app import chrome +from app import appconfig, chrome from app import config as account_config @@ -51,6 +51,175 @@ class ChromeTests(TempDirMixin, unittest.TestCase): sock.close() return port + def make_executable(self, root, *parts): + path = os.path.join(root, *parts) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as fh: + fh.write("chrome") + return path + + def test_registry_candidates_prioritize_current_user_and_all_views(self): + class FakeKey: + def __init__(self, value): + self.value = value + + class FakeWinreg: + HKEY_CURRENT_USER = "HKCU" + HKEY_LOCAL_MACHINE = "HKLM" + KEY_READ = 1 + KEY_WOW64_64KEY = 16 + KEY_WOW64_32KEY = 32 + + def __init__(self): + self.calls = [] + self.values = { + (self.HKEY_CURRENT_USER, self.KEY_READ | self.KEY_WOW64_32KEY): "user-chrome.exe", + (self.HKEY_LOCAL_MACHINE, self.KEY_READ): "machine-chrome.exe", + } + + def OpenKey(self, hive, sub_key, _reserved, access): + self.calls.append((hive, sub_key, access)) + try: + return FakeKey(self.values[(hive, access)]) + except KeyError as exc: + raise FileNotFoundError() from exc + + @staticmethod + def QueryValueEx(key, name): + self.assertEqual("", name) + return key.value, 1 + + fake = FakeWinreg() + with mock.patch("app.chrome._load_winreg", return_value=fake): + candidates = list(chrome._registry_chrome_candidates()) + + self.assertEqual(["user-chrome.exe", "machine-chrome.exe"], candidates) + self.assertEqual("HKCU", fake.calls[0][0]) + self.assertEqual(chrome.CHROME_APP_PATHS_KEY, fake.calls[0][1]) + self.assertIn(("HKCU", chrome.CHROME_APP_PATHS_KEY, 1 | 16), fake.calls) + self.assertIn(("HKCU", chrome.CHROME_APP_PATHS_KEY, 1 | 32), fake.calls) + self.assertIn(("HKLM", chrome.CHROME_APP_PATHS_KEY, 1), fake.calls) + + def test_detect_chrome_path_normalizes_registry_value(self): + with self.make_temp_dir() as temp_dir: + executable = self.make_executable(temp_dir, "Chrome", "chrome.exe") + environment_name = "CMSHOPEE_TEST_CHROME" + registry_value = f'"%{environment_name}%"' + + with mock.patch.dict(os.environ, {environment_name: executable}, clear=False), \ + mock.patch( + "app.chrome._registry_chrome_candidates", + return_value=[registry_value], + ), \ + mock.patch("app.chrome._standard_chrome_candidates", return_value=[]), \ + mock.patch("app.chrome.shutil.which", return_value=None): + detected = chrome.detect_chrome_path() + + self.assertEqual(os.path.abspath(executable), detected) + + self.assert_removed(temp_dir) + + def test_detect_chrome_path_uses_local_app_data_without_registry(self): + with self.make_temp_dir() as temp_dir: + executable = self.make_executable( + temp_dir, + "Google", + "Chrome", + "Application", + "chrome.exe", + ) + with mock.patch.dict(os.environ, {"LOCALAPPDATA": temp_dir}, clear=True), \ + mock.patch("app.chrome._registry_chrome_candidates", return_value=[]), \ + mock.patch("app.chrome.shutil.which", return_value=None): + detected = chrome.detect_chrome_path() + + self.assertEqual(os.path.abspath(executable), detected) + + self.assert_removed(temp_dir) + + def test_detect_chrome_path_never_uses_edge_and_handles_missing_registry(self): + with self.make_temp_dir() as temp_dir: + self.make_executable( + temp_dir, + "Microsoft", + "Edge", + "Application", + "msedge.exe", + ) + with mock.patch("app.chrome._load_winreg", return_value=None), \ + mock.patch.dict(os.environ, {"LOCALAPPDATA": temp_dir}, clear=True), \ + mock.patch("app.chrome.shutil.which", return_value=None): + detected = chrome.detect_chrome_path() + + self.assertEqual("", detected) + + self.assert_removed(temp_dir) + + def test_ensure_configured_chrome_path_persists_only_when_invalid(self): + with self.make_temp_dir() as temp_dir: + config_path = os.path.join(temp_dir, "config.json") + detected = self.make_executable(temp_dir, "Chrome", "chrome.exe") + config = appconfig.load_config(config_path) + + with mock.patch("app.chrome.detect_chrome_path", return_value=detected): + first = chrome.ensure_configured_chrome_path(config) + + self.assertTrue(first["changed"]) + self.assertEqual(os.path.abspath(detected), first["config"]["chrome_path"]) + self.assertIn("已自动定位 Chrome", first["message"]) + self.assertEqual( + os.path.abspath(detected), + appconfig.load_config(config_path)["chrome_path"], + ) + + missing = os.path.join(temp_dir, "missing", "chrome.exe") + invalid = appconfig.save_config( + {"chrome_path": missing}, + path=config_path, + ) + with mock.patch("app.chrome.detect_chrome_path", return_value=detected): + repaired = chrome.ensure_configured_chrome_path(invalid) + self.assertTrue(repaired["changed"]) + self.assertEqual(os.path.abspath(detected), repaired["config"]["chrome_path"]) + + custom = self.make_executable(temp_dir, "Portable", "chrome.exe") + valid = appconfig.save_config( + {"chrome_path": custom}, + path=config_path, + ) + with mock.patch("app.chrome.detect_chrome_path") as detect: + unchanged = chrome.ensure_configured_chrome_path(valid) + self.assertFalse(unchanged["changed"]) + self.assertEqual(custom, unchanged["config"]["chrome_path"]) + detect.assert_not_called() + + self.assert_removed(temp_dir) + + def test_ensure_configured_chrome_path_preserves_path_command_and_no_match(self): + with self.make_temp_dir() as temp_dir: + config_path = os.path.join(temp_dir, "config.json") + config = appconfig.save_config( + {"chrome_path": "chrome.exe"}, + path=config_path, + ) + resolved_path = self.make_executable(temp_dir, "PATH", "chrome.exe") + + with mock.patch("app.chrome.shutil.which", return_value=resolved_path), \ + mock.patch("app.chrome.detect_chrome_path") as detect: + unchanged = chrome.ensure_configured_chrome_path(config) + self.assertFalse(unchanged["changed"]) + detect.assert_not_called() + + empty = appconfig.save_config({"chrome_path": ""}, path=config_path) + with mock.patch("app.chrome.detect_chrome_path", return_value=""), \ + mock.patch("app.chrome.appconfig.save_config") as save: + no_match = chrome.ensure_configured_chrome_path(empty) + self.assertFalse(no_match["changed"]) + self.assertEqual("", no_match["message"]) + save.assert_not_called() + + self.assert_removed(temp_dir) + def test_build_launch_args_includes_required_flags(self): with self.make_temp_dir() as temp_dir: cfg = { diff --git a/tests/test_gui.py b/tests/test_gui.py index 442447b..61d9987 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -889,6 +889,11 @@ class GuiTests(TempDirMixin, unittest.TestCase): ) self.assertEqual("选择...", tab.chrome_path_browse_button.text()) self.assertEqual("chromePathBrowseButton", tab.chrome_path_browse_button.objectName()) + self.assertEqual("自动检测", tab.chrome_path_detect_button.text()) + self.assertEqual( + "chromePathAutoDetectButton", + tab.chrome_path_detect_button.objectName(), + ) self.assertIs( tab.chrome_path_widget.layout().itemAt(0).widget(), tab.chrome_path_edit, @@ -897,6 +902,10 @@ class GuiTests(TempDirMixin, unittest.TestCase): tab.chrome_path_widget.layout().itemAt(1).widget(), tab.chrome_path_browse_button, ) + self.assertIs( + tab.chrome_path_widget.layout().itemAt(2).widget(), + tab.chrome_path_detect_button, + ) self.assert_removed(temp_dir) @@ -942,6 +951,53 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assert_removed(temp_dir) + def test_settings_tab_auto_detects_chrome_without_saving(self): + with self.make_temp_dir() as temp_dir: + cfg = self.make_config(temp_dir) + statuses = [] + tab = SettingsTab(config=cfg, status_callback=statuses.append) + self.addCleanup(tab.close) + detected = os.path.join(temp_dir, "Chrome", "chrome.exe") + + with mock.patch( + "app.gui.tabs.settings.chrome.detect_chrome_path", + return_value=detected, + ): + tab.chrome_path_detect_button.click() + + self.assertEqual(detected, tab.chrome_path_edit.text()) + self.assertTrue(tab.is_dirty()) + self.assertFalse(os.path.exists(cfg["config_path"])) + self.assertIn("已自动定位 Chrome", statuses[-1]) + + tab._set_dirty(False) + previous = tab.chrome_path_edit.text() + with mock.patch( + "app.gui.tabs.settings.chrome.detect_chrome_path", + return_value="", + ): + tab.chrome_path_detect_button.click() + + self.assertEqual(previous, tab.chrome_path_edit.text()) + self.assertFalse(tab.is_dirty()) + self.assertIn("未找到 Chrome", statuses[-1]) + + self.assert_removed(temp_dir) + + def test_main_window_keeps_startup_chrome_detection_status(self): + with self.make_temp_dir() as temp_dir: + message = "已自动定位 Chrome:C:\\Chrome\\chrome.exe" + window = MainWindow( + config=self.make_config(temp_dir), + startup_status=message, + ) + self.addCleanup(window.close) + + self.assertEqual(message, window.statusBar().currentMessage()) + self.assertIn(gui.COLOR_SUCCESS, window.statusBar().styleSheet()) + + self.assert_removed(temp_dir) + def test_settings_tab_adds_saves_and_deletes_model(self): with self.make_temp_dir() as temp_dir: cfg = self.make_config(temp_dir)