feat: track settings changes and enforce Chinese UI copy

This commit is contained in:
chengma
2026-07-06 11:32:02 +08:00
parent 26e59c8cea
commit 19eaa794cf
10 changed files with 430 additions and 19 deletions
+230
View File
@@ -84,6 +84,44 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertIsNotNone(value)
self.assertEqual(color, value.name())
def make_fake_message_box(self, selected_label):
boxes = []
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
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[selected_label]
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(
@@ -936,6 +974,198 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_settings_tab_tracks_dirty_state_and_programmatic_cmhub_refresh_is_clean(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
cfg["ai"] = appconfig.default_config()["ai"]
cfg["ai"]["backend"] = "cmhub"
cfg["ai"]["cmhub"] = {
"base_url": "https://cmhub.old",
"title_alias": "title-old",
"image_alias": "image-old",
"connect_timeout": 9,
"check_balance_before_batch": False,
}
cfg["cmhub_config_path"] = os.path.join(temp_dir, "config", "cmhub.json")
appconfig.save_cmhub_config({"api_key": "sk-old-secret"}, path=cfg["cmhub_config_path"])
tab = SettingsTab(config=cfg, config_path=cfg["config_path"], ai_models_path=cfg["ai_models_path"])
self.addCleanup(tab.close)
self.assertFalse(tab.is_dirty())
self.assertTrue(tab.unsaved_changes_label.isHidden())
tab._on_cmhub_finished(
{
"ok": True,
"models": [
{
"alias": "title-fresh",
"operation_type": "title",
"pricing_status": "priced",
"prices": [{"resolution": "1K", "points_cost": 1}],
},
{
"alias": "image-fresh",
"operation_type": "image",
"pricing_status": "priced",
"requires_image": True,
"prices": [{"resolution": "1K", "points_cost": 5}],
},
],
}
)
self.assertFalse(tab.is_dirty())
self.assertIn("记得点『保存设置』", tab.cmhub_result_label.text())
tab.cmhub_base_url_edit.setText("https://cmhub.example.com/api/v1/")
self.assertTrue(tab.is_dirty())
self.assertFalse(tab.unsaved_changes_label.isHidden())
with mock.patch("app.gui.QMessageBox.information") as info:
self.assertTrue(tab.save_app_settings())
info.assert_called_once_with(tab, "保存设置", "设置已保存")
self.assertFalse(tab.is_dirty())
self.assertTrue(tab.unsaved_changes_label.isHidden())
self.assert_removed(temp_dir)
def test_settings_tab_discard_unsaved_changes_reloads_saved_config_and_key(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
cfg["ai"] = appconfig.default_config()["ai"]
cfg["ai"]["backend"] = "cmhub"
cfg["ai"]["cmhub"] = {
"base_url": "https://cmhub.saved",
"title_alias": "title-saved",
"image_alias": "image-saved",
"connect_timeout": 11,
"check_balance_before_batch": False,
}
cfg["cmhub_config_path"] = os.path.join(temp_dir, "config", "cmhub.json")
appconfig.save_config(
{key: value for key, value in cfg.items() if key not in {"config_path", "ai_models_path", "cmhub_config_path"}},
path=cfg["config_path"],
)
appconfig.save_cmhub_config({"api_key": "sk-saved-secret"}, path=cfg["cmhub_config_path"])
tab = SettingsTab(config=cfg, config_path=cfg["config_path"], ai_models_path=cfg["ai_models_path"])
self.addCleanup(tab.close)
tab.cmhub_base_url_edit.setText("https://cmhub.changed")
tab.cmhub_api_key_edit.setText("sk-changed-secret")
self.assertTrue(tab.is_dirty())
self.assertTrue(tab.discard_unsaved_changes())
self.assertFalse(tab.is_dirty())
self.assertEqual("https://cmhub.saved", tab.cmhub_base_url_edit.text())
self.assertEqual("sk-saved-secret", tab.cmhub_api_key_edit.text())
self.assertTrue(tab.unsaved_changes_label.isHidden())
self.assert_removed(temp_dir)
def test_main_window_cancelled_tab_change_keeps_user_on_dirty_settings(self):
with self.make_temp_dir() as temp_dir:
window = MainWindow(config=self.make_config(temp_dir))
self.addCleanup(window.close)
settings_index = TAB_TITLES.index("⑤ 设置")
window.tabs.setCurrentIndex(settings_index)
settings_tab = window.tabs.widget(settings_index)
settings_tab.cmhub_base_url_edit.setText("https://cmhub.changed")
message_box, boxes = self.make_fake_message_box("取消")
with mock.patch("app.gui.main_window.QMessageBox", message_box):
window.tabs.setCurrentIndex(0)
self.assertEqual(1, len(boxes))
self.assertEqual("未保存更改", boxes[0].title)
self.assertEqual(["保存", "放弃", "取消"], list(boxes[0].buttons))
self.assertEqual(settings_index, window.tabs.currentIndex())
self.assertTrue(settings_tab.is_dirty())
settings_tab._set_dirty(False)
self.assert_removed(temp_dir)
def test_main_window_save_failure_keeps_user_on_dirty_settings(self):
with self.make_temp_dir() as temp_dir:
window = MainWindow(config=self.make_config(temp_dir))
self.addCleanup(window.close)
settings_index = TAB_TITLES.index("⑤ 设置")
window.tabs.setCurrentIndex(settings_index)
settings_tab = window.tabs.widget(settings_index)
settings_tab.cmhub_base_url_edit.setText("https://cmhub.changed")
message_box, _ = self.make_fake_message_box("保存")
with mock.patch("app.gui.main_window.QMessageBox", message_box), \
mock.patch.object(settings_tab, "save_app_settings", return_value=False) as save_settings:
window.tabs.setCurrentIndex(0)
save_settings.assert_called_once_with()
self.assertEqual(settings_index, window.tabs.currentIndex())
self.assertTrue(settings_tab.is_dirty())
settings_tab._set_dirty(False)
self.assert_removed(temp_dir)
def test_main_window_discard_tab_change_restores_saved_settings(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
cfg["ai"] = appconfig.default_config()["ai"]
cfg["ai"]["backend"] = "cmhub"
cfg["ai"]["cmhub"] = {
"base_url": "https://cmhub.saved",
"title_alias": "title-saved",
"image_alias": "image-saved",
"connect_timeout": 10,
"check_balance_before_batch": False,
}
cfg["cmhub_config_path"] = os.path.join(temp_dir, "config", "cmhub.json")
appconfig.save_config(
{key: value for key, value in cfg.items() if key not in {"config_path", "ai_models_path", "cmhub_config_path"}},
path=cfg["config_path"],
)
appconfig.save_cmhub_config({"api_key": "sk-saved-secret"}, path=cfg["cmhub_config_path"])
window = MainWindow(config=cfg, config_path=cfg["config_path"], ai_models_path=cfg["ai_models_path"])
self.addCleanup(window.close)
settings_index = TAB_TITLES.index("⑤ 设置")
window.tabs.setCurrentIndex(settings_index)
settings_tab = window.tabs.widget(settings_index)
settings_tab.cmhub_base_url_edit.setText("https://cmhub.changed")
settings_tab.cmhub_api_key_edit.setText("sk-changed-secret")
message_box, _ = self.make_fake_message_box("放弃")
with mock.patch("app.gui.main_window.QMessageBox", message_box):
window.tabs.setCurrentIndex(0)
self.assertEqual(0, window.tabs.currentIndex())
self.assertFalse(settings_tab.is_dirty())
self.assertEqual("https://cmhub.saved", settings_tab.cmhub_base_url_edit.text())
self.assertEqual("sk-saved-secret", settings_tab.cmhub_api_key_edit.text())
self.assert_removed(temp_dir)
def test_main_window_close_cancel_ignores_dirty_settings_close(self):
with self.make_temp_dir() as temp_dir:
window = MainWindow(config=self.make_config(temp_dir))
self.addCleanup(window.close)
settings_index = TAB_TITLES.index("⑤ 设置")
window.tabs.setCurrentIndex(settings_index)
settings_tab = window.tabs.widget(settings_index)
settings_tab.cmhub_base_url_edit.setText("https://cmhub.changed")
event = SimpleNamespace(accepted=False, ignored=False)
event.accept = lambda: setattr(event, "accepted", True)
event.ignore = lambda: setattr(event, "ignored", True)
message_box, _ = self.make_fake_message_box("取消")
with mock.patch("app.gui.main_window.QMessageBox", message_box):
window.closeEvent(event)
self.assertFalse(event.accepted)
self.assertTrue(event.ignored)
settings_tab._set_dirty(False)
self.assert_removed(temp_dir)
def test_generate_tab_has_prompt_editors_and_task_table(self):
with self.make_temp_dir() as temp_dir:
title_prompt_path = os.path.join(temp_dir, "title_prompt.txt")