2026-06-27 09:56:53 +08:00
|
|
|
import unittest
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
2026-06-27 10:38:21 +08:00
|
|
|
from unittest import mock
|
2026-06-27 09:56:53 +08:00
|
|
|
|
|
|
|
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
|
|
|
|
2026-06-27 10:30:45 +08:00
|
|
|
from _helpers import TempDirMixin
|
|
|
|
|
|
2026-06-27 09:56:53 +08:00
|
|
|
from app import gui
|
2026-06-27 11:29:48 +08:00
|
|
|
from app import accounts, db
|
2026-06-27 09:56:53 +08:00
|
|
|
|
|
|
|
|
if gui.QT_IMPORT_ERROR is not None:
|
|
|
|
|
raise unittest.SkipTest("PySide6 未安装")
|
|
|
|
|
|
2026-06-27 11:29:48 +08:00
|
|
|
from PySide6.QtWidgets import QApplication, QLineEdit, QTableView
|
2026-06-27 09:56:53 +08:00
|
|
|
|
2026-06-27 11:29:48 +08:00
|
|
|
from app.gui import (
|
|
|
|
|
AccountDialog,
|
|
|
|
|
AccountsTab,
|
|
|
|
|
CollectTab,
|
|
|
|
|
MainWindow,
|
|
|
|
|
TAB_STYLE,
|
|
|
|
|
TAB_TITLES,
|
|
|
|
|
)
|
2026-06-27 09:56:53 +08:00
|
|
|
|
|
|
|
|
|
2026-06-27 10:30:45 +08:00
|
|
|
class GuiTests(TempDirMixin, unittest.TestCase):
|
2026-06-27 09:56:53 +08:00
|
|
|
@classmethod
|
|
|
|
|
def setUpClass(cls):
|
|
|
|
|
cls.app = QApplication.instance() or QApplication([])
|
|
|
|
|
|
2026-06-27 10:30:45 +08:00
|
|
|
def make_config(self, temp_dir):
|
|
|
|
|
return {
|
|
|
|
|
"chrome_path": "chrome.exe",
|
|
|
|
|
"user_data_root": os.path.join(temp_dir, "chrome_user_data_dir"),
|
|
|
|
|
"db_path": os.path.join(temp_dir, "cmshopee.db"),
|
|
|
|
|
"debug_port_range": [9222, 9260],
|
|
|
|
|
}
|
2026-06-27 09:56:53 +08:00
|
|
|
|
2026-06-27 10:30:45 +08:00
|
|
|
def test_main_window_has_five_tabs_in_workflow_order(self):
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
window = MainWindow(config=self.make_config(temp_dir))
|
|
|
|
|
self.addCleanup(window.close)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(5, window.tabs.count())
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
TAB_TITLES,
|
|
|
|
|
[window.tabs.tabText(index) for index in range(window.tabs.count())],
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual("就绪", window.statusBar().currentMessage())
|
|
|
|
|
self.assertEqual(TAB_STYLE, window.tabs.styleSheet())
|
|
|
|
|
self.assertIn("min-width: 128px", window.tabs.styleSheet())
|
|
|
|
|
self.assertIn("padding: 8px 18px", window.tabs.styleSheet())
|
|
|
|
|
self.assertIn("margin-right: 8px", window.tabs.styleSheet())
|
2026-06-27 11:29:48 +08:00
|
|
|
self.assertIsInstance(window.tabs.widget(0), CollectTab)
|
2026-06-27 10:30:45 +08:00
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
2026-06-27 09:56:53 +08:00
|
|
|
|
|
|
|
|
def test_tab_switch_updates_status_bar(self):
|
2026-06-27 10:30:45 +08:00
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
window = MainWindow(config=self.make_config(temp_dir))
|
|
|
|
|
self.addCleanup(window.close)
|
2026-06-27 09:56:53 +08:00
|
|
|
|
2026-06-27 10:30:45 +08:00
|
|
|
window.tabs.setCurrentIndex(2)
|
2026-06-27 09:56:53 +08:00
|
|
|
|
2026-06-27 10:30:45 +08:00
|
|
|
self.assertEqual("③ 更新shopee", window.tabs.tabText(window.tabs.currentIndex()))
|
|
|
|
|
self.assertEqual("当前:③ 更新shopee", window.statusBar().currentMessage())
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
|
|
|
def test_accounts_tab_lists_accounts_without_showing_password(self):
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
cfg = self.make_config(temp_dir)
|
|
|
|
|
accounts.create_account(
|
|
|
|
|
"主店",
|
|
|
|
|
"alias",
|
|
|
|
|
"seller.shopee.tw",
|
|
|
|
|
9222,
|
|
|
|
|
password="secret",
|
|
|
|
|
config=cfg,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
window = MainWindow(config=cfg)
|
|
|
|
|
self.addCleanup(window.close)
|
|
|
|
|
tab = window.tabs.widget(TAB_TITLES.index("④ 账号管理"))
|
|
|
|
|
|
|
|
|
|
self.assertIsInstance(tab, AccountsTab)
|
|
|
|
|
self.assertEqual(1, tab.table.rowCount())
|
|
|
|
|
self.assertEqual("主店", tab.table.item(0, 0).text())
|
|
|
|
|
self.assertEqual("alias", tab.table.item(0, 1).text())
|
|
|
|
|
self.assertEqual("未知", tab.table.item(0, 4).text())
|
|
|
|
|
visible_values = [
|
|
|
|
|
tab.table.item(0, column).text()
|
|
|
|
|
for column in range(tab.table.columnCount())
|
|
|
|
|
if tab.table.item(0, column) is not None
|
|
|
|
|
]
|
|
|
|
|
self.assertNotIn("secret", visible_values)
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
|
|
|
def test_account_dialog_masks_password_field(self):
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
cfg = self.make_config(temp_dir)
|
|
|
|
|
account = accounts.create_account(
|
|
|
|
|
"主店",
|
|
|
|
|
"alias",
|
|
|
|
|
debug_port=9222,
|
|
|
|
|
password="secret",
|
|
|
|
|
config=cfg,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
dialog = AccountDialog(account=account, config=cfg)
|
|
|
|
|
self.addCleanup(dialog.close)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(QLineEdit.Password, dialog.password_edit.echoMode())
|
|
|
|
|
self.assertEqual("secret", dialog.password_edit.text())
|
|
|
|
|
self.assertIn(account.slug, dialog.user_data_dir_edit.text())
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
|
|
|
def test_accounts_tab_updates_login_status_cell(self):
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
cfg = self.make_config(temp_dir)
|
|
|
|
|
accounts.create_account("主店", "alias", debug_port=9222, config=cfg)
|
|
|
|
|
tab = AccountsTab(config=cfg)
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
|
|
|
|
tab._on_login_check_finished(
|
|
|
|
|
{"alias": "alias", "status": {"logged_in": True, "reason": None}}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual("已登录", tab.table.item(0, 4).text())
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
2026-06-27 09:56:53 +08:00
|
|
|
|
2026-06-27 10:38:21 +08:00
|
|
|
def test_accounts_tab_create_shortcut_for_selected_account(self):
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
cfg = self.make_config(temp_dir)
|
|
|
|
|
account = accounts.create_account("主店", "alias", debug_port=9222, config=cfg)
|
|
|
|
|
statuses = []
|
|
|
|
|
tab = AccountsTab(config=cfg, status_callback=statuses.append)
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
tab.table.selectRow(0)
|
|
|
|
|
shortcut_path = os.path.join(temp_dir, "Desktop", "主店.lnk")
|
|
|
|
|
|
|
|
|
|
with mock.patch(
|
|
|
|
|
"app.gui.accounts.create_shortcut",
|
|
|
|
|
return_value=shortcut_path,
|
|
|
|
|
) as create_shortcut, mock.patch("app.gui.QMessageBox.information") as info:
|
|
|
|
|
tab.create_shortcut()
|
|
|
|
|
|
|
|
|
|
create_shortcut.assert_called_once_with(account, config=cfg)
|
|
|
|
|
info.assert_called_once()
|
|
|
|
|
self.assertIn(shortcut_path, statuses[-1])
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
2026-06-27 11:29:48 +08:00
|
|
|
def test_collect_tab_lists_tasks_with_unmatched_alias_as_skipped(self):
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
cfg = self.make_config(temp_dir)
|
|
|
|
|
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
|
|
|
|
|
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
|
|
|
|
|
db.insert_tasks(
|
|
|
|
|
batch_id,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
|
|
|
|
|
"source_sheet": "商品",
|
|
|
|
|
"source_row": 2,
|
|
|
|
|
"account_name": "Excel主店",
|
|
|
|
|
"alias": "alias-a",
|
|
|
|
|
"item_id": "51100639510",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
|
|
|
|
|
"source_sheet": "商品",
|
|
|
|
|
"source_row": 3,
|
|
|
|
|
"account_name": "Excel副店",
|
|
|
|
|
"alias": "missing",
|
|
|
|
|
"item_id": "51100639511",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
path=cfg["db_path"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
tab = CollectTab(config=cfg)
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
|
|
|
|
self.assertIsInstance(tab.table, QTableView)
|
|
|
|
|
self.assertEqual(2, tab.model.rowCount())
|
|
|
|
|
self.assertEqual("主店", tab.model.index(0, 0).data())
|
|
|
|
|
self.assertEqual("alias-a", tab.model.index(0, 1).data())
|
|
|
|
|
self.assertEqual("51100639510", tab.model.index(0, 2).data())
|
|
|
|
|
self.assertEqual("待采集", tab.model.index(0, 3).data())
|
|
|
|
|
self.assertEqual("Excel副店", tab.model.index(1, 0).data())
|
|
|
|
|
self.assertEqual("missing", tab.model.index(1, 1).data())
|
|
|
|
|
self.assertEqual("略过", tab.model.index(1, 3).data())
|
2026-06-27 11:40:51 +08:00
|
|
|
self.assertIn("2 行", tab.summary_label.text())
|
|
|
|
|
self.assertIn("有效2/无效0", tab.summary_label.text())
|
|
|
|
|
self.assertIn("匹配1", tab.summary_label.text())
|
|
|
|
|
self.assertIn("未匹配1", tab.summary_label.text())
|
|
|
|
|
self.assertIn("主店1", tab.match_detail_label.text())
|
2026-06-27 11:29:48 +08:00
|
|
|
self.assertIn("未匹配", tab.empty_label.text())
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
2026-06-27 11:40:51 +08:00
|
|
|
def test_collect_tab_can_filter_unmatched_tasks_from_summary_bar(self):
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
cfg = self.make_config(temp_dir)
|
|
|
|
|
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
|
|
|
|
|
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
|
|
|
|
|
db.insert_tasks(
|
|
|
|
|
batch_id,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
|
|
|
|
|
"source_sheet": "商品",
|
|
|
|
|
"source_row": 2,
|
|
|
|
|
"account_name": "Excel主店",
|
|
|
|
|
"alias": "alias-a",
|
|
|
|
|
"item_id": "51100639510",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
|
|
|
|
|
"source_sheet": "商品",
|
|
|
|
|
"source_row": 3,
|
|
|
|
|
"account_name": "Excel副店",
|
|
|
|
|
"alias": "missing",
|
|
|
|
|
"item_id": "51100639511",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
path=cfg["db_path"],
|
|
|
|
|
)
|
|
|
|
|
tab = CollectTab(config=cfg)
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
|
|
|
|
tab.show_unmatched_tasks()
|
|
|
|
|
|
|
|
|
|
self.assertEqual(1, tab.model.rowCount())
|
|
|
|
|
self.assertEqual("missing", tab.model.index(0, 1).data())
|
|
|
|
|
self.assertEqual("略过", tab.model.index(0, 3).data())
|
|
|
|
|
|
|
|
|
|
tab.show_all_tasks()
|
|
|
|
|
|
|
|
|
|
self.assertEqual(2, tab.model.rowCount())
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
2026-06-27 11:29:48 +08:00
|
|
|
def test_collect_tab_import_button_imports_excel_and_refreshes_tasks(self):
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
cfg = self.make_config(temp_dir)
|
|
|
|
|
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
|
|
|
|
|
excel_path = os.path.join(temp_dir, "input.xlsx")
|
|
|
|
|
statuses = []
|
|
|
|
|
tab = CollectTab(config=cfg, status_callback=statuses.append)
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
|
|
|
|
def fake_import(file_paths, path=None):
|
|
|
|
|
batch_id = db.create_batch(file_paths, path=path)
|
|
|
|
|
db.insert_tasks(
|
|
|
|
|
batch_id,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
"source_file_abs": excel_path,
|
|
|
|
|
"source_sheet": "商品",
|
|
|
|
|
"source_row": 2,
|
|
|
|
|
"account_name": "Excel主店",
|
|
|
|
|
"alias": "alias-a",
|
|
|
|
|
"item_id": "51100639510",
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
path=path,
|
|
|
|
|
)
|
|
|
|
|
return {
|
|
|
|
|
"batch_id": batch_id,
|
|
|
|
|
"rows": [],
|
2026-06-27 11:40:51 +08:00
|
|
|
"stats": {
|
|
|
|
|
"files": 1,
|
|
|
|
|
"total": 2,
|
|
|
|
|
"valid": 1,
|
|
|
|
|
"invalid": 1,
|
|
|
|
|
"inserted": 1,
|
|
|
|
|
},
|
2026-06-27 11:29:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
with mock.patch.object(
|
|
|
|
|
tab,
|
|
|
|
|
"_choose_excel_files",
|
|
|
|
|
return_value=[excel_path],
|
|
|
|
|
), mock.patch("app.gui.excel.import_tasks", side_effect=fake_import) as import_tasks:
|
|
|
|
|
tab.import_excel()
|
|
|
|
|
|
|
|
|
|
import_tasks.assert_called_once_with([excel_path], path=cfg["db_path"])
|
|
|
|
|
self.assertEqual(1, tab.model.rowCount())
|
|
|
|
|
self.assertEqual("alias-a", tab.model.index(0, 1).data())
|
2026-06-27 11:40:51 +08:00
|
|
|
self.assertIn("1 文件", tab.summary_label.text())
|
|
|
|
|
self.assertIn("2 行", tab.summary_label.text())
|
|
|
|
|
self.assertIn("有效1/无效1", tab.summary_label.text())
|
|
|
|
|
self.assertIn("匹配1", tab.summary_label.text())
|
2026-06-27 11:29:48 +08:00
|
|
|
self.assertIn("入库1", statuses[-1])
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
2026-06-27 09:56:53 +08:00
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|