2026-07-14 09:53:13 +08:00
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import unittest
|
2026-07-16 09:22:33 +08:00
|
|
|
|
from unittest import mock
|
2026-07-14 09:53:13 +08:00
|
|
|
|
|
|
|
|
|
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
|
|
|
|
|
|
|
|
|
|
from _helpers import TempDirMixin
|
|
|
|
|
|
|
2026-07-14 15:16:09 +08:00
|
|
|
|
from app import accounts, appconfig, image_studio, image_studio_images
|
2026-07-14 09:53:13 +08:00
|
|
|
|
from app import gui
|
|
|
|
|
|
|
|
|
|
|
|
if gui.QT_IMPORT_ERROR is not None:
|
|
|
|
|
|
raise unittest.SkipTest("PySide6 未安装")
|
|
|
|
|
|
|
2026-07-16 09:34:10 +08:00
|
|
|
|
from PySide6.QtCore import QMimeData, Qt, QUrl
|
2026-07-14 15:35:59 +08:00
|
|
|
|
from PySide6.QtGui import QIcon, QImage, QPixmap
|
|
|
|
|
|
from PySide6.QtTest import QTest
|
|
|
|
|
|
from PySide6.QtWidgets import QApplication, QLabel, QListWidgetItem, QPushButton
|
2026-07-14 09:53:13 +08:00
|
|
|
|
|
2026-07-14 15:35:59 +08:00
|
|
|
|
from app.gui.tabs.product_suite import (
|
|
|
|
|
|
ORIGINAL_CHECK_STATE_ROLE,
|
|
|
|
|
|
ProductOriginalDelegate,
|
|
|
|
|
|
ProductOriginalList,
|
|
|
|
|
|
ProductSuiteTab,
|
|
|
|
|
|
SuiteResultCard,
|
|
|
|
|
|
)
|
2026-07-14 09:53:13 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase):
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def setUpClass(cls):
|
|
|
|
|
|
cls.app = QApplication.instance() or QApplication([])
|
|
|
|
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
|
|
for widget in QApplication.topLevelWidgets():
|
|
|
|
|
|
widget.close()
|
|
|
|
|
|
widget.deleteLater()
|
|
|
|
|
|
self.app.processEvents()
|
|
|
|
|
|
|
|
|
|
|
|
def _config(self, temp_dir):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"chrome_path": "chrome.exe",
|
|
|
|
|
|
"user_data_root": os.path.join(temp_dir, "chrome_user_data_dir"),
|
|
|
|
|
|
"image_dir": os.path.join(temp_dir, "images"),
|
|
|
|
|
|
"db_path": os.path.join(temp_dir, "cmshopee.db"),
|
|
|
|
|
|
"debug_port_range": [9222, 9260],
|
|
|
|
|
|
"config_path": os.path.join(temp_dir, "config.json"),
|
|
|
|
|
|
"cmhub_config_path": os.path.join(temp_dir, "cmhub.json"),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def _write_image(self, path):
|
|
|
|
|
|
image = QImage(40, 30, QImage.Format_RGB32)
|
|
|
|
|
|
image.fill(0xFF336699)
|
|
|
|
|
|
self.assertTrue(image.save(path))
|
|
|
|
|
|
|
2026-07-14 15:35:59 +08:00
|
|
|
|
def _create_project_with_assets(self, temp_dir, config, count, item_id="51100639510"):
|
|
|
|
|
|
account = accounts.create_account(
|
|
|
|
|
|
"主店",
|
|
|
|
|
|
"alias-a",
|
|
|
|
|
|
debug_port=9222,
|
|
|
|
|
|
config=config,
|
|
|
|
|
|
)
|
|
|
|
|
|
project = image_studio.create_or_get_project(
|
|
|
|
|
|
account,
|
|
|
|
|
|
item_id=item_id,
|
|
|
|
|
|
path=config["db_path"],
|
|
|
|
|
|
)
|
|
|
|
|
|
assets = []
|
|
|
|
|
|
for index in range(count):
|
|
|
|
|
|
source_path = os.path.join(temp_dir, "%s-%02d.png" % (item_id, index + 1))
|
|
|
|
|
|
self._write_image(source_path)
|
|
|
|
|
|
assets.append(
|
|
|
|
|
|
image_studio.add_asset(
|
|
|
|
|
|
project.id,
|
|
|
|
|
|
image_studio.ASSET_KIND_ORIGINAL,
|
|
|
|
|
|
local_path=source_path,
|
|
|
|
|
|
source_order=index + 1,
|
|
|
|
|
|
path=config["db_path"],
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return project, assets
|
|
|
|
|
|
|
2026-07-14 09:53:13 +08:00
|
|
|
|
def test_tab_builds_suite_controls_without_old_detail_workspace(self):
|
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
|
config = self._config(temp_dir)
|
|
|
|
|
|
accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
|
|
|
|
|
|
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
|
|
|
|
|
self.addCleanup(tab.close)
|
2026-07-14 15:16:09 +08:00
|
|
|
|
tab.resize(1180, 760)
|
|
|
|
|
|
tab.show()
|
|
|
|
|
|
self.app.processEvents()
|
2026-07-14 09:53:13 +08:00
|
|
|
|
|
|
|
|
|
|
self.assertEqual("productSuiteTab", tab.objectName())
|
|
|
|
|
|
self.assertEqual(1, tab.task_tabs.count())
|
|
|
|
|
|
self.assertEqual("套图任务 1", tab.task_tabs.tabText(0))
|
|
|
|
|
|
self.assertEqual("alias-a", tab.account_combo.currentData())
|
|
|
|
|
|
self.assertEqual("Shopee", tab.platform_combo.currentData())
|
|
|
|
|
|
self.assertEqual("中国台湾", tab.country_combo.currentData())
|
|
|
|
|
|
self.assertEqual("繁体中文", tab.language_combo.currentData())
|
|
|
|
|
|
self.assertEqual("1:1", tab.ratio_combo.currentData())
|
2026-07-14 15:16:09 +08:00
|
|
|
|
self.assertEqual("Shopee", tab.platform_combo.currentText())
|
|
|
|
|
|
self.assertEqual("中国台湾", tab.country_combo.currentText())
|
|
|
|
|
|
self.assertEqual("繁体中文", tab.language_combo.currentText())
|
|
|
|
|
|
self.assertEqual("1:1", tab.ratio_combo.currentText())
|
|
|
|
|
|
self.assertEqual("平台", tab.platform_label.text())
|
|
|
|
|
|
self.assertEqual("站点", tab.country_label.text())
|
|
|
|
|
|
self.assertEqual("语言", tab.language_label.text())
|
|
|
|
|
|
self.assertEqual("比例", tab.ratio_label.text())
|
|
|
|
|
|
for column, combo in enumerate(
|
|
|
|
|
|
(
|
|
|
|
|
|
tab.platform_combo,
|
|
|
|
|
|
tab.country_combo,
|
|
|
|
|
|
tab.language_combo,
|
|
|
|
|
|
tab.ratio_combo,
|
|
|
|
|
|
)
|
|
|
|
|
|
):
|
|
|
|
|
|
self.assertIs(combo, tab.settings_grid.itemAtPosition(1, column).widget())
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
1,
|
|
|
|
|
|
len(
|
|
|
|
|
|
{
|
|
|
|
|
|
tab.platform_combo.width(),
|
|
|
|
|
|
tab.country_combo.width(),
|
|
|
|
|
|
tab.language_combo.width(),
|
|
|
|
|
|
tab.ratio_combo.width(),
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertLessEqual(tab.task_tabs.maximumHeight(), 36)
|
|
|
|
|
|
self.assertEqual(120, tab.account_combo.minimumWidth())
|
|
|
|
|
|
self.assertEqual(160, tab.account_combo.maximumWidth())
|
|
|
|
|
|
self.assertGreaterEqual(tab.item_id_edit.minimumWidth(), 120)
|
|
|
|
|
|
self.assertLessEqual(tab.item_id_edit.maximumWidth(), 140)
|
|
|
|
|
|
self.assertLess(
|
|
|
|
|
|
tab.context_bar_layout.indexOf(tab.history_button),
|
|
|
|
|
|
tab.context_bar_layout.indexOf(tab.account_combo),
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertLess(
|
|
|
|
|
|
tab.context_bar_layout.indexOf(tab.open_folder_button),
|
|
|
|
|
|
tab.context_bar_layout.indexOf(tab.account_combo),
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertLess(
|
|
|
|
|
|
tab.context_bar_layout.indexOf(tab.add_images_button),
|
|
|
|
|
|
tab.context_bar_layout.indexOf(tab.account_combo),
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(-1, tab.results_toolbar_layout.indexOf(tab.history_button))
|
|
|
|
|
|
self.assertEqual(-1, tab.results_toolbar_layout.indexOf(tab.open_folder_button))
|
|
|
|
|
|
self.assertEqual("打开结果文件夹", tab.open_folder_button.text())
|
2026-07-14 09:53:13 +08:00
|
|
|
|
self.assertEqual("合计 5 张", tab.category_total_label.text())
|
|
|
|
|
|
self.assertEqual("生成套图(5)", tab.generate_button.text())
|
|
|
|
|
|
|
|
|
|
|
|
visible_text = " ".join(
|
|
|
|
|
|
[widget.text() for widget in tab.findChildren(QLabel)]
|
|
|
|
|
|
+ [widget.text() for widget in tab.findChildren(QPushButton)]
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertNotIn("详情图", visible_text)
|
|
|
|
|
|
self.assertNotIn("AI工场", visible_text)
|
|
|
|
|
|
self.assertIn("白底图", visible_text)
|
|
|
|
|
|
self.assertIn("场景图", visible_text)
|
|
|
|
|
|
self.assertIn("卖点图", visible_text)
|
|
|
|
|
|
|
|
|
|
|
|
tab.add_custom_category()
|
|
|
|
|
|
self.assertFalse(tab.custom_category_edit.isHidden())
|
|
|
|
|
|
tab.custom_category_edit.setText("尺寸图")
|
|
|
|
|
|
tab._commit_custom_category()
|
|
|
|
|
|
self.assertTrue(tab.custom_category_edit.isHidden())
|
|
|
|
|
|
self.assertIn("尺寸图", tab._displayed_state.settings["categories"])
|
2026-07-16 09:22:33 +08:00
|
|
|
|
self.assertEqual(1, tab.category_rows["尺寸图"].count())
|
|
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
|
|
|
|
|
def test_category_rows_are_vertical_with_helpers_and_independent_counters(self):
|
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
|
config = self._config(temp_dir)
|
|
|
|
|
|
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
tab.resize(1180, 760)
|
|
|
|
|
|
tab.show()
|
|
|
|
|
|
self.app.processEvents()
|
|
|
|
|
|
|
|
|
|
|
|
expected = {
|
|
|
|
|
|
"白底图": (1, "白底主图,多角度呈现商品细节"),
|
|
|
|
|
|
"场景图": (2, "生活化场景展示商品使用方式"),
|
|
|
|
|
|
"卖点图": (2, "突出核心卖点和差异化优势"),
|
|
|
|
|
|
}
|
|
|
|
|
|
self.assertEqual(list(expected), list(tab.category_rows))
|
|
|
|
|
|
for row_index, (name, (count, helper)) in enumerate(expected.items()):
|
|
|
|
|
|
row = tab.category_rows[name]
|
|
|
|
|
|
layout_index = tab.category_grid.indexOf(row)
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
(row_index, 0, 1, 1),
|
|
|
|
|
|
tab.category_grid.getItemPosition(layout_index),
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(name, row.name_label.text())
|
|
|
|
|
|
self.assertEqual(count, row.count())
|
|
|
|
|
|
self.assertEqual(helper, row.helper_label.text())
|
|
|
|
|
|
self.assertIsNone(row.rename_button)
|
|
|
|
|
|
self.assertIsNone(row.delete_button)
|
|
|
|
|
|
|
|
|
|
|
|
scene_row = tab.category_rows["场景图"]
|
|
|
|
|
|
scene_row.plus_button.click()
|
|
|
|
|
|
self.assertIs(scene_row, tab.category_rows["场景图"])
|
|
|
|
|
|
self.assertEqual(3, scene_row.count())
|
|
|
|
|
|
self.assertEqual(1, tab.category_rows["白底图"].count())
|
|
|
|
|
|
self.assertEqual(2, tab.category_rows["卖点图"].count())
|
|
|
|
|
|
self.assertEqual("合计 6 张", tab.category_total_label.text())
|
|
|
|
|
|
self.assertEqual("生成套图(6)", tab.generate_button.text())
|
|
|
|
|
|
|
|
|
|
|
|
white_row = tab.category_rows["白底图"]
|
|
|
|
|
|
white_row.minus_button.click()
|
|
|
|
|
|
self.assertEqual(0, white_row.count())
|
|
|
|
|
|
self.assertFalse(white_row.minus_button.isEnabled())
|
|
|
|
|
|
white_row.minus_button.click()
|
|
|
|
|
|
self.assertEqual(0, tab._displayed_state.settings["categories"]["白底图"])
|
|
|
|
|
|
self.assertEqual("合计 5 张", tab.category_total_label.text())
|
|
|
|
|
|
|
|
|
|
|
|
tab._displayed_state.worker = object()
|
|
|
|
|
|
tab._apply_running_state(tab._displayed_state)
|
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
|
all(
|
|
|
|
|
|
not row.plus_button.isEnabled()
|
|
|
|
|
|
for row in tab.category_rows.values()
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
tab._displayed_state.worker = None
|
|
|
|
|
|
tab._apply_running_state(tab._displayed_state)
|
|
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
|
|
|
|
|
def test_custom_category_row_preserves_count_and_order_when_renamed(self):
|
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
|
config = self._config(temp_dir)
|
|
|
|
|
|
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
|
|
|
|
|
|
fixed_counts = {
|
|
|
|
|
|
name: tab.category_rows[name].count()
|
|
|
|
|
|
for name in ("白底图", "场景图", "卖点图")
|
|
|
|
|
|
}
|
|
|
|
|
|
tab.add_custom_category()
|
|
|
|
|
|
tab.custom_category_edit.setText("尺寸图")
|
|
|
|
|
|
tab._commit_custom_category()
|
|
|
|
|
|
custom_row = tab.category_rows["尺寸图"]
|
|
|
|
|
|
self.assertEqual(1, custom_row.count())
|
|
|
|
|
|
self.assertIsNone(custom_row.helper_label)
|
|
|
|
|
|
self.assertIsNotNone(custom_row.rename_button)
|
|
|
|
|
|
self.assertIsNotNone(custom_row.delete_button)
|
|
|
|
|
|
|
|
|
|
|
|
custom_row.plus_button.click()
|
|
|
|
|
|
self.assertEqual(2, custom_row.count())
|
|
|
|
|
|
with mock.patch(
|
|
|
|
|
|
"app.gui.tabs.product_suite.QInputDialog.getText",
|
|
|
|
|
|
return_value=("规格图", True),
|
|
|
|
|
|
):
|
|
|
|
|
|
custom_row.rename_button.click()
|
|
|
|
|
|
self.assertNotIn("尺寸图", tab.category_rows)
|
|
|
|
|
|
self.assertEqual(2, tab.category_rows["规格图"].count())
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
["规格图"],
|
|
|
|
|
|
tab._displayed_state.settings["custom_category_order"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
tab.category_rows["规格图"].delete_button.click()
|
|
|
|
|
|
self.assertNotIn("规格图", tab.category_rows)
|
|
|
|
|
|
self.assertEqual([], tab._displayed_state.settings["custom_category_order"])
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
fixed_counts,
|
|
|
|
|
|
{
|
|
|
|
|
|
name: tab.category_rows[name].count()
|
|
|
|
|
|
for name in ("白底图", "场景图", "卖点图")
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
|
|
|
|
|
def test_category_counts_restore_after_task_switch_and_project_reload(self):
|
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
|
config = self._config(temp_dir)
|
|
|
|
|
|
account = accounts.create_account(
|
|
|
|
|
|
"主店",
|
|
|
|
|
|
"alias-a",
|
|
|
|
|
|
debug_port=9222,
|
|
|
|
|
|
config=config,
|
|
|
|
|
|
)
|
|
|
|
|
|
project = image_studio.create_or_get_project(
|
|
|
|
|
|
account,
|
|
|
|
|
|
item_id="51100639510",
|
|
|
|
|
|
path=config["db_path"],
|
|
|
|
|
|
)
|
|
|
|
|
|
first = ProductSuiteTab(config=config, db_path=config["db_path"])
|
|
|
|
|
|
state = first._displayed_state
|
|
|
|
|
|
state.account_alias = "alias-a"
|
|
|
|
|
|
state.item_id = "51100639510"
|
|
|
|
|
|
state.project_id = project.id
|
|
|
|
|
|
first.change_category_count("场景图", 1)
|
|
|
|
|
|
first.add_custom_category()
|
|
|
|
|
|
first.custom_category_edit.setText("细节图")
|
|
|
|
|
|
first._commit_custom_category()
|
|
|
|
|
|
first.category_rows["细节图"].plus_button.click()
|
|
|
|
|
|
|
|
|
|
|
|
first.add_task(inherit=False)
|
|
|
|
|
|
self.assertEqual(2, first.category_rows["场景图"].count())
|
|
|
|
|
|
first.task_tabs.setCurrentIndex(0)
|
|
|
|
|
|
self.app.processEvents()
|
|
|
|
|
|
self.assertEqual(3, first.category_rows["场景图"].count())
|
|
|
|
|
|
self.assertEqual(2, first.category_rows["细节图"].count())
|
|
|
|
|
|
first.close()
|
|
|
|
|
|
first.deleteLater()
|
|
|
|
|
|
self.app.processEvents()
|
|
|
|
|
|
|
|
|
|
|
|
second = ProductSuiteTab(config=config, db_path=config["db_path"])
|
|
|
|
|
|
self.addCleanup(second.close)
|
|
|
|
|
|
reloaded = second._displayed_state
|
|
|
|
|
|
reloaded.account_alias = "alias-a"
|
|
|
|
|
|
reloaded.item_id = "51100639510"
|
|
|
|
|
|
second._bind_project(reloaded, load_existing=True)
|
|
|
|
|
|
self.assertEqual(3, second.category_rows["场景图"].count())
|
|
|
|
|
|
self.assertEqual(2, second.category_rows["细节图"].count())
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
["细节图"],
|
|
|
|
|
|
reloaded.settings["custom_category_order"],
|
|
|
|
|
|
)
|
2026-07-14 09:53:13 +08:00
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
2026-07-14 15:16:09 +08:00
|
|
|
|
def test_recent_dropdown_settings_restore_after_restart_and_project_wins(self):
|
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
|
config_path = os.path.join(temp_dir, "config.json")
|
|
|
|
|
|
config = appconfig.save_config(self._config(temp_dir), path=config_path)
|
|
|
|
|
|
account = accounts.create_account(
|
|
|
|
|
|
"主店",
|
|
|
|
|
|
"alias-a",
|
|
|
|
|
|
debug_port=9222,
|
|
|
|
|
|
config=config,
|
|
|
|
|
|
)
|
|
|
|
|
|
project = image_studio.create_or_get_project(
|
|
|
|
|
|
account,
|
|
|
|
|
|
item_id="51100639510",
|
|
|
|
|
|
path=config["db_path"],
|
|
|
|
|
|
)
|
|
|
|
|
|
project_settings = image_studio.project_suite_settings(project)
|
|
|
|
|
|
project_settings["ratio"] = "4:3"
|
|
|
|
|
|
image_studio.update_project_suite_settings(
|
|
|
|
|
|
project.id,
|
|
|
|
|
|
project_settings,
|
|
|
|
|
|
path=config["db_path"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
first = ProductSuiteTab(
|
|
|
|
|
|
config=config,
|
|
|
|
|
|
config_path=config_path,
|
|
|
|
|
|
db_path=config["db_path"],
|
|
|
|
|
|
)
|
|
|
|
|
|
first.ratio_combo.setCurrentIndex(first.ratio_combo.findData("16:9"))
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
"16:9",
|
|
|
|
|
|
appconfig.product_suite_last_settings(
|
|
|
|
|
|
appconfig.load_config(config_path)
|
|
|
|
|
|
)["ratio"],
|
|
|
|
|
|
)
|
|
|
|
|
|
first.close()
|
|
|
|
|
|
first.deleteLater()
|
|
|
|
|
|
self.app.processEvents()
|
|
|
|
|
|
|
|
|
|
|
|
reloaded = appconfig.load_config(config_path)
|
|
|
|
|
|
second = ProductSuiteTab(
|
|
|
|
|
|
config=reloaded,
|
|
|
|
|
|
config_path=config_path,
|
|
|
|
|
|
db_path=reloaded["db_path"],
|
|
|
|
|
|
)
|
|
|
|
|
|
self.addCleanup(second.close)
|
|
|
|
|
|
self.assertEqual("16:9", second.ratio_combo.currentData())
|
|
|
|
|
|
|
|
|
|
|
|
state = second._displayed_state
|
|
|
|
|
|
state.account_alias = "alias-a"
|
|
|
|
|
|
state.item_id = "51100639510"
|
|
|
|
|
|
second._bind_project(state, load_existing=True)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual("4:3", second.ratio_combo.currentData())
|
|
|
|
|
|
self.assertEqual("4:3", state.settings["ratio"])
|
|
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
2026-07-14 09:53:13 +08:00
|
|
|
|
def test_task_tabs_keep_independent_prompt_and_settings(self):
|
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
|
config = self._config(temp_dir)
|
|
|
|
|
|
accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
|
|
|
|
|
|
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
|
|
|
|
|
|
tab.prompt_edit.setPlainText("任务一卖点")
|
|
|
|
|
|
first_state = tab._displayed_state
|
|
|
|
|
|
tab.ratio_combo.setCurrentIndex(tab.ratio_combo.findData("3:4"))
|
|
|
|
|
|
second_state = tab.add_task(inherit=True)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(2, tab.task_tabs.count())
|
|
|
|
|
|
self.assertEqual("任务一卖点", second_state.prompt)
|
|
|
|
|
|
self.assertEqual("3:4", second_state.settings["ratio"])
|
|
|
|
|
|
tab.prompt_edit.setPlainText("任务二卖点")
|
|
|
|
|
|
tab.ratio_combo.setCurrentIndex(tab.ratio_combo.findData("16:9"))
|
|
|
|
|
|
tab.task_tabs.setCurrentIndex(0)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertIs(first_state, tab._displayed_state)
|
|
|
|
|
|
self.assertEqual("任务一卖点", tab.prompt_edit.toPlainText())
|
|
|
|
|
|
self.assertEqual("3:4", tab.ratio_combo.currentData())
|
|
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
|
|
|
|
|
def test_project_settings_and_result_history_use_existing_backend(self):
|
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
|
config = self._config(temp_dir)
|
|
|
|
|
|
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
|
|
|
|
|
|
project = image_studio.create_or_get_project(
|
|
|
|
|
|
account,
|
|
|
|
|
|
item_id="51100639510",
|
|
|
|
|
|
path=config["db_path"],
|
|
|
|
|
|
)
|
|
|
|
|
|
source_path = os.path.join(temp_dir, "source.png")
|
|
|
|
|
|
self._write_image(source_path)
|
|
|
|
|
|
source = image_studio_images.import_original_files(
|
|
|
|
|
|
project.id,
|
|
|
|
|
|
[source_path],
|
|
|
|
|
|
path=config["db_path"],
|
|
|
|
|
|
config=config,
|
|
|
|
|
|
)["assets"][0]
|
|
|
|
|
|
job = image_studio.create_job(
|
|
|
|
|
|
project.id,
|
|
|
|
|
|
source_asset_id=source.id,
|
|
|
|
|
|
job_type="场景图",
|
|
|
|
|
|
prompt="场景卖点",
|
|
|
|
|
|
path=config["db_path"],
|
|
|
|
|
|
)
|
|
|
|
|
|
image_studio.update_job_status(
|
|
|
|
|
|
job.id,
|
|
|
|
|
|
"failed",
|
|
|
|
|
|
error="上游超时 https://example.invalid/private",
|
|
|
|
|
|
path=config["db_path"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
state = tab._displayed_state
|
|
|
|
|
|
state.account_alias = "alias-a"
|
|
|
|
|
|
state.item_id = "51100639510"
|
|
|
|
|
|
state.project_id = project.id
|
|
|
|
|
|
state.current_job_ids = [job.id]
|
|
|
|
|
|
tab._load_state(state)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual([source.id], tab.original_list.asset_ids())
|
|
|
|
|
|
self.assertEqual("共 1 张 · 成功 0 张", tab.result_summary_label.text())
|
|
|
|
|
|
cards = tab.findChildren(SuiteResultCard)
|
|
|
|
|
|
self.assertEqual(1, len(cards))
|
|
|
|
|
|
self.assertNotIn(
|
|
|
|
|
|
"https://",
|
|
|
|
|
|
" ".join(label.text() for label in cards[0].findChildren(QLabel)),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
state.settings["ratio"] = "4:3"
|
|
|
|
|
|
state.prompt = "持久化卖点"
|
|
|
|
|
|
tab._persist_state(state)
|
|
|
|
|
|
stored = image_studio.get_project(project.id, path=config["db_path"])
|
|
|
|
|
|
self.assertEqual("持久化卖点", stored.draft_prompt)
|
|
|
|
|
|
self.assertEqual("4:3", image_studio.project_suite_settings(stored)["ratio"])
|
|
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
2026-07-14 15:35:59 +08:00
|
|
|
|
def test_original_list_expands_without_internal_scrollbars(self):
|
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
|
config = self._config(temp_dir)
|
|
|
|
|
|
project, assets = self._create_project_with_assets(temp_dir, config, 0)
|
|
|
|
|
|
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
state = tab._displayed_state
|
|
|
|
|
|
state.account_alias = "alias-a"
|
|
|
|
|
|
state.item_id = "51100639510"
|
|
|
|
|
|
state.project_id = project.id
|
|
|
|
|
|
tab.resize(1180, 760)
|
|
|
|
|
|
tab.show()
|
|
|
|
|
|
tab._load_state(state)
|
|
|
|
|
|
self.app.processEvents()
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(Qt.ScrollBarAlwaysOff, tab.original_list.horizontalScrollBarPolicy())
|
|
|
|
|
|
self.assertEqual(Qt.ScrollBarAlwaysOff, tab.original_list.verticalScrollBarPolicy())
|
2026-07-16 09:34:10 +08:00
|
|
|
|
self.assertEqual(0, tab.original_list.count())
|
|
|
|
|
|
self.assertEqual([], tab.original_list.asset_ids())
|
|
|
|
|
|
self.assertEqual("0/16", tab.original_count_label.text())
|
|
|
|
|
|
self.assertEqual("已选 0 张", tab.original_selected_label.text())
|
|
|
|
|
|
self.assertFalse(tab.select_all_originals_button.isEnabled())
|
|
|
|
|
|
self.assertFalse(tab.invert_originals_button.isEnabled())
|
|
|
|
|
|
self.assertEqual(1, tab.original_list.content_row_count())
|
2026-07-14 15:35:59 +08:00
|
|
|
|
empty_height = tab.original_list.height()
|
|
|
|
|
|
|
2026-07-16 09:34:10 +08:00
|
|
|
|
for target_count in (1, 2, 5, 6, 7, 16):
|
2026-07-14 15:35:59 +08:00
|
|
|
|
for index in range(len(assets), target_count):
|
|
|
|
|
|
source_path = os.path.join(temp_dir, "added-%02d.png" % (index + 1))
|
|
|
|
|
|
self._write_image(source_path)
|
|
|
|
|
|
assets.append(
|
|
|
|
|
|
image_studio.add_asset(
|
|
|
|
|
|
project.id,
|
|
|
|
|
|
image_studio.ASSET_KIND_ORIGINAL,
|
|
|
|
|
|
local_path=source_path,
|
|
|
|
|
|
source_order=index + 1,
|
|
|
|
|
|
path=config["db_path"],
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
tab._refresh_originals(state)
|
|
|
|
|
|
self.app.processEvents()
|
2026-07-16 09:34:10 +08:00
|
|
|
|
self.assertEqual(target_count, tab.original_list.count())
|
2026-07-14 15:35:59 +08:00
|
|
|
|
self.assertEqual(target_count, len(tab.original_list.asset_ids()))
|
2026-07-16 09:34:10 +08:00
|
|
|
|
columns = tab.original_list.content_column_count()
|
|
|
|
|
|
expected_rows = (target_count + columns - 1) // columns
|
2026-07-14 15:35:59 +08:00
|
|
|
|
self.assertEqual(expected_rows, tab.original_list.content_row_count())
|
2026-07-16 09:34:10 +08:00
|
|
|
|
self.assertEqual("%d/16" % target_count, tab.original_count_label.text())
|
|
|
|
|
|
if target_count == 1:
|
2026-07-14 15:35:59 +08:00
|
|
|
|
self.assertEqual(empty_height, tab.original_list.height())
|
2026-07-16 09:34:10 +08:00
|
|
|
|
self.assertTrue(tab.add_images_button.isEnabled())
|
2026-07-14 15:35:59 +08:00
|
|
|
|
|
|
|
|
|
|
wide_height = tab.original_list.height()
|
|
|
|
|
|
tab.original_list.setFixedWidth(250)
|
|
|
|
|
|
self.app.processEvents()
|
|
|
|
|
|
self.assertEqual(2, tab.original_list.content_column_count())
|
|
|
|
|
|
self.assertEqual(8, tab.original_list.content_row_count())
|
|
|
|
|
|
self.assertGreater(tab.original_list.height(), wide_height)
|
2026-07-16 09:34:10 +08:00
|
|
|
|
self.assertFalse(tab.add_images_button.isEnabled())
|
|
|
|
|
|
self.assertEqual("已达到16张商品原图上限", tab.add_images_button.toolTip())
|
2026-07-14 15:35:59 +08:00
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
2026-07-16 09:34:10 +08:00
|
|
|
|
def test_empty_original_list_accepts_file_drop_and_clipboard_image(self):
|
|
|
|
|
|
original_list = ProductOriginalList()
|
|
|
|
|
|
self.addCleanup(original_list.close)
|
|
|
|
|
|
original_list.setFixedSize(360, 110)
|
|
|
|
|
|
original_list.show()
|
|
|
|
|
|
self.app.processEvents()
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(0, original_list.count())
|
|
|
|
|
|
self.assertEqual("暂无商品原图", original_list.EMPTY_STATE_TEXT)
|
|
|
|
|
|
self.assertEqual(1, original_list.content_row_count())
|
|
|
|
|
|
|
|
|
|
|
|
dropped = []
|
|
|
|
|
|
pasted = []
|
|
|
|
|
|
original_list.filesDropped.connect(dropped.append)
|
|
|
|
|
|
original_list.clipboardImage.connect(pasted.append)
|
|
|
|
|
|
|
|
|
|
|
|
class DropEvent:
|
|
|
|
|
|
def __init__(self, mime_data):
|
|
|
|
|
|
self._mime_data = mime_data
|
|
|
|
|
|
self.accepted = False
|
|
|
|
|
|
|
|
|
|
|
|
def mimeData(self):
|
|
|
|
|
|
return self._mime_data
|
|
|
|
|
|
|
|
|
|
|
|
def acceptProposedAction(self):
|
|
|
|
|
|
self.accepted = True
|
|
|
|
|
|
|
|
|
|
|
|
mime_data = QMimeData()
|
|
|
|
|
|
expected_path = os.path.abspath("待导入图片.png")
|
|
|
|
|
|
mime_data.setUrls([QUrl.fromLocalFile(expected_path)])
|
|
|
|
|
|
drop_event = DropEvent(mime_data)
|
|
|
|
|
|
original_list.dropEvent(drop_event)
|
|
|
|
|
|
self.assertTrue(drop_event.accepted)
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
os.path.normcase(os.path.normpath(expected_path)),
|
|
|
|
|
|
os.path.normcase(os.path.normpath(dropped[0][0])),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
clipboard_image = QImage(24, 18, QImage.Format_RGB32)
|
|
|
|
|
|
clipboard_image.fill(0xFF336699)
|
|
|
|
|
|
QApplication.clipboard().setImage(clipboard_image)
|
|
|
|
|
|
original_list.setFocus()
|
|
|
|
|
|
QTest.keyClick(original_list, Qt.Key_V, Qt.ControlModifier)
|
|
|
|
|
|
self.app.processEvents()
|
|
|
|
|
|
QApplication.clipboard().clear()
|
|
|
|
|
|
self.assertEqual(1, len(pasted))
|
|
|
|
|
|
self.assertTrue(pasted[0].startswith(b"\x89PNG\r\n\x1a\n"))
|
|
|
|
|
|
|
|
|
|
|
|
def test_add_images_button_tracks_capacity_import_and_generation(self):
|
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
|
config = self._config(temp_dir)
|
|
|
|
|
|
project, _ = self._create_project_with_assets(temp_dir, config, 1)
|
|
|
|
|
|
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
state = tab._displayed_state
|
|
|
|
|
|
state.account_alias = "alias-a"
|
|
|
|
|
|
state.item_id = "51100639510"
|
|
|
|
|
|
state.project_id = project.id
|
|
|
|
|
|
tab._load_state(state)
|
|
|
|
|
|
|
|
|
|
|
|
style = tab.add_images_button.styleSheet()
|
|
|
|
|
|
for color in ("#0969da", "#eef4ff", "#dbeafe", "#c7ddff", "#f6f8fa"):
|
|
|
|
|
|
self.assertIn(color, style)
|
|
|
|
|
|
self.assertTrue(tab.add_images_button.isEnabled())
|
|
|
|
|
|
self.assertEqual("添加本地商品原图", tab.add_images_button.toolTip())
|
|
|
|
|
|
|
|
|
|
|
|
state.import_worker = object()
|
|
|
|
|
|
tab._refresh_add_images_action(state)
|
|
|
|
|
|
self.assertFalse(tab.add_images_button.isEnabled())
|
|
|
|
|
|
self.assertEqual("正在添加商品原图", tab.add_images_button.toolTip())
|
|
|
|
|
|
state.import_thread = object()
|
|
|
|
|
|
tab._on_import_failed(state, "测试导入失败")
|
|
|
|
|
|
self.assertIsNone(state.import_worker)
|
|
|
|
|
|
self.assertIsNone(state.import_thread)
|
|
|
|
|
|
self.assertTrue(tab.add_images_button.isEnabled())
|
|
|
|
|
|
|
|
|
|
|
|
state.worker = object()
|
|
|
|
|
|
tab._refresh_add_images_action(state)
|
|
|
|
|
|
self.assertFalse(tab.add_images_button.isEnabled())
|
|
|
|
|
|
self.assertEqual("生成中不能添加商品原图", tab.add_images_button.toolTip())
|
|
|
|
|
|
state.worker = None
|
|
|
|
|
|
tab._refresh_add_images_action(state)
|
|
|
|
|
|
self.assertTrue(tab.add_images_button.isEnabled())
|
|
|
|
|
|
|
2026-07-14 15:35:59 +08:00
|
|
|
|
def test_original_checks_preserve_on_refresh_and_clear_on_task_switch(self):
|
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
|
config = self._config(temp_dir)
|
|
|
|
|
|
project, assets = self._create_project_with_assets(temp_dir, config, 2)
|
|
|
|
|
|
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
state = tab._displayed_state
|
|
|
|
|
|
state.account_alias = "alias-a"
|
|
|
|
|
|
state.item_id = "51100639510"
|
|
|
|
|
|
state.project_id = project.id
|
|
|
|
|
|
tab._load_state(state)
|
|
|
|
|
|
|
2026-07-16 09:34:10 +08:00
|
|
|
|
self.assertEqual(2, tab.original_list.count())
|
2026-07-14 15:35:59 +08:00
|
|
|
|
self.assertTrue(tab.select_all_originals_button.isEnabled())
|
|
|
|
|
|
self.assertTrue(tab.invert_originals_button.isEnabled())
|
2026-07-16 09:34:10 +08:00
|
|
|
|
for row in range(tab.original_list.count()):
|
2026-07-14 15:35:59 +08:00
|
|
|
|
self.assertIsNotNone(tab.original_list.item(row).data(ORIGINAL_CHECK_STATE_ROLE))
|
|
|
|
|
|
|
|
|
|
|
|
tab.select_all_originals_button.click()
|
|
|
|
|
|
self.assertEqual([asset.id for asset in assets], tab.original_list.checked_asset_ids())
|
|
|
|
|
|
self.assertEqual("已选 2 张", tab.original_selected_label.text())
|
|
|
|
|
|
tab.invert_originals_button.click()
|
|
|
|
|
|
self.assertEqual([], tab.original_list.checked_asset_ids())
|
|
|
|
|
|
|
|
|
|
|
|
tab.original_list.set_checked_asset_ids([assets[1].id])
|
|
|
|
|
|
tab._refresh_originals(state)
|
|
|
|
|
|
self.assertEqual([assets[1].id], tab.original_list.checked_asset_ids())
|
|
|
|
|
|
tab.reorder_originals([assets[1].id, assets[0].id])
|
|
|
|
|
|
self.assertEqual([assets[1].id], tab.original_list.checked_asset_ids())
|
|
|
|
|
|
|
|
|
|
|
|
tab.add_task(inherit=True)
|
|
|
|
|
|
self.assertEqual([], tab.original_list.checked_asset_ids())
|
|
|
|
|
|
self.assertFalse(tab.select_all_originals_button.isEnabled())
|
|
|
|
|
|
tab.task_tabs.setCurrentIndex(0)
|
|
|
|
|
|
self.assertEqual([], tab.original_list.checked_asset_ids())
|
|
|
|
|
|
self.assertEqual("已选 0 张", tab.original_selected_label.text())
|
|
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
2026-07-16 11:10:51 +08:00
|
|
|
|
def test_temporary_draft_allows_local_work_but_blocks_shopee_pull_and_recovers(self):
|
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
|
config = self._config(temp_dir)
|
|
|
|
|
|
accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
|
|
|
|
|
|
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
state = tab._displayed_state
|
|
|
|
|
|
|
|
|
|
|
|
self.assertTrue(tab.add_images_button.isEnabled())
|
|
|
|
|
|
self.assertFalse(tab.item_id_hint_label.isHidden())
|
|
|
|
|
|
self.assertEqual("请输入正确的商品ID", tab.item_id_hint_label.text())
|
|
|
|
|
|
with mock.patch(
|
|
|
|
|
|
"app.gui.tabs.product_suite.QFileDialog.getOpenFileNames",
|
|
|
|
|
|
return_value=([], ""),
|
|
|
|
|
|
):
|
|
|
|
|
|
tab.choose_images()
|
|
|
|
|
|
self.assertEqual([], image_studio.list_projects(path=config["db_path"]))
|
|
|
|
|
|
|
|
|
|
|
|
source_path = os.path.join(temp_dir, "draft-source.png")
|
|
|
|
|
|
self._write_image(source_path)
|
|
|
|
|
|
with mock.patch.object(tab, "_start_thread", return_value=object()):
|
|
|
|
|
|
tab._start_import(file_paths=[source_path])
|
|
|
|
|
|
worker = state.import_worker
|
|
|
|
|
|
draft = image_studio.get_project(state.project_id, path=config["db_path"])
|
|
|
|
|
|
self.assertTrue(image_studio.is_draft_project(draft))
|
|
|
|
|
|
self.assertEqual("", state.item_id)
|
|
|
|
|
|
self.assertIn("临时草稿", tab.task_tabs.tabText(tab.task_tabs.currentIndex()))
|
|
|
|
|
|
self.assertFalse(tab.item_id_hint_label.isHidden())
|
|
|
|
|
|
self.assertTrue(tab.pull_button.isEnabled())
|
|
|
|
|
|
self.assertEqual("需要先绑定正式商品ID", tab.pull_button.toolTip())
|
|
|
|
|
|
tab._on_import_finished(state, worker.execute())
|
|
|
|
|
|
self.assertEqual(1, len(image_studio.list_assets(draft.id, path=config["db_path"])))
|
|
|
|
|
|
|
|
|
|
|
|
captured = {}
|
|
|
|
|
|
|
|
|
|
|
|
class _Signal:
|
|
|
|
|
|
def connect(self, callback):
|
|
|
|
|
|
self.callback = callback
|
|
|
|
|
|
|
|
|
|
|
|
class _AiWriteWorker:
|
|
|
|
|
|
def __init__(self, instruction, context, **kwargs):
|
|
|
|
|
|
captured["instruction"] = instruction
|
|
|
|
|
|
captured["context"] = context
|
|
|
|
|
|
self.finished = _Signal()
|
|
|
|
|
|
self.cancelled = _Signal()
|
|
|
|
|
|
self.failed = _Signal()
|
|
|
|
|
|
|
|
|
|
|
|
def cancel(self):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
with mock.patch(
|
|
|
|
|
|
"app.gui.tabs.product_suite.ProductSuiteAiWriteWorker",
|
|
|
|
|
|
_AiWriteWorker,
|
|
|
|
|
|
), mock.patch.object(tab, "_start_thread", return_value=object()):
|
|
|
|
|
|
tab.start_ai_write()
|
|
|
|
|
|
self.assertIn("未绑定商品", captured["context"])
|
|
|
|
|
|
self.assertNotIn("draft_", captured["context"])
|
|
|
|
|
|
state.ai_worker = None
|
|
|
|
|
|
state.ai_thread = None
|
|
|
|
|
|
state.ai_started_at = None
|
|
|
|
|
|
tab._apply_running_state(state)
|
|
|
|
|
|
|
|
|
|
|
|
pull_message = mock.Mock()
|
|
|
|
|
|
with mock.patch.object(tab, "_message", pull_message):
|
|
|
|
|
|
tab.pull_main_images()
|
|
|
|
|
|
self.assertIsNone(state.pull_worker)
|
|
|
|
|
|
self.assertEqual("无法拉取蝦皮主图", pull_message.call_args.args[0])
|
|
|
|
|
|
self.assertIn("当前为临时项目", pull_message.call_args.args[1])
|
|
|
|
|
|
|
|
|
|
|
|
tab.item_id_edit.setText("51100639510")
|
|
|
|
|
|
with mock.patch.object(tab, "_confirm", return_value=True):
|
|
|
|
|
|
tab._on_item_finished()
|
|
|
|
|
|
bound = image_studio.get_project(draft.id, path=config["db_path"])
|
|
|
|
|
|
self.assertEqual("51100639510", bound.item_id)
|
|
|
|
|
|
self.assertEqual(image_studio.PROJECT_BINDING_BOUND, bound.binding_state)
|
|
|
|
|
|
self.assertTrue(tab.item_id_hint_label.isHidden())
|
|
|
|
|
|
self.assertIn("套图任务", tab.task_tabs.tabText(tab.task_tabs.currentIndex()))
|
|
|
|
|
|
|
|
|
|
|
|
draft_state = tab.add_task(inherit=False)
|
|
|
|
|
|
draft = tab._create_draft_project(draft_state)
|
|
|
|
|
|
image_studio.add_asset(
|
|
|
|
|
|
draft.id,
|
|
|
|
|
|
image_studio.ASSET_KIND_ORIGINAL,
|
|
|
|
|
|
local_path=source_path,
|
|
|
|
|
|
path=config["db_path"],
|
|
|
|
|
|
)
|
|
|
|
|
|
draft_state.ai_worker = mock.Mock()
|
|
|
|
|
|
with mock.patch.object(tab, "_draft_close_action", return_value="cancel"):
|
|
|
|
|
|
tab.close_task(tab.task_tabs.currentIndex())
|
|
|
|
|
|
draft_state.ai_worker.cancel.assert_not_called()
|
|
|
|
|
|
self.assertIn(draft_state.key, tab._states)
|
|
|
|
|
|
draft_state.ai_worker = None
|
|
|
|
|
|
with mock.patch.object(tab, "_draft_close_action", return_value="keep"):
|
|
|
|
|
|
tab.close_task(tab.task_tabs.currentIndex())
|
|
|
|
|
|
self.assertIsNotNone(image_studio.get_project(draft.id, path=config["db_path"]))
|
|
|
|
|
|
|
|
|
|
|
|
tab.close()
|
|
|
|
|
|
restored = ProductSuiteTab(config=config, db_path=config["db_path"])
|
|
|
|
|
|
self.addCleanup(restored.close)
|
|
|
|
|
|
restored_state = next(
|
|
|
|
|
|
state for state in restored._states.values() if state.project_id == draft.id
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual("", restored_state.item_id)
|
|
|
|
|
|
self.assertTrue(restored._is_draft_state(restored_state))
|
|
|
|
|
|
restored_index = next(
|
|
|
|
|
|
index
|
|
|
|
|
|
for index in range(restored.task_tabs.count())
|
|
|
|
|
|
if restored.task_tabs.tabData(index) == restored_state.key
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertIn("临时草稿", restored.task_tabs.tabText(restored_index))
|
|
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
|
|
|
|
|
def test_first_failed_import_discards_new_empty_temporary_draft(self):
|
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
|
config = self._config(temp_dir)
|
|
|
|
|
|
accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
|
|
|
|
|
|
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
bad_path = os.path.join(temp_dir, "not-an-image.png")
|
|
|
|
|
|
with open(bad_path, "wb") as handle:
|
|
|
|
|
|
handle.write(b"not an image")
|
|
|
|
|
|
|
|
|
|
|
|
state = tab._displayed_state
|
|
|
|
|
|
with mock.patch.object(tab, "_start_thread", return_value=object()):
|
|
|
|
|
|
tab._start_import(file_paths=[bad_path])
|
|
|
|
|
|
worker = state.import_worker
|
|
|
|
|
|
draft_id = state.project_id
|
|
|
|
|
|
with mock.patch.object(tab, "_message"):
|
|
|
|
|
|
tab._on_import_finished(state, worker.execute())
|
|
|
|
|
|
|
|
|
|
|
|
discarded = image_studio.get_project(
|
|
|
|
|
|
draft_id,
|
|
|
|
|
|
path=config["db_path"],
|
|
|
|
|
|
include_deleted=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertIsNotNone(discarded.deleted_at)
|
|
|
|
|
|
self.assertIsNone(state.project_id)
|
|
|
|
|
|
self.assertEqual([], image_studio.list_recoverable_draft_projects(path=config["db_path"]))
|
|
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
2026-07-14 15:35:59 +08:00
|
|
|
|
def test_original_checkbox_click_and_keyboard_delete_keep_actions_separate(self):
|
|
|
|
|
|
original_list = ProductOriginalList()
|
|
|
|
|
|
self.addCleanup(original_list.close)
|
|
|
|
|
|
original_list.setFixedSize(240, 210)
|
|
|
|
|
|
for asset_id in (11, 12):
|
|
|
|
|
|
item = QListWidgetItem("原图%d" % asset_id)
|
|
|
|
|
|
item.setData(Qt.UserRole, asset_id)
|
|
|
|
|
|
item.setData(ORIGINAL_CHECK_STATE_ROLE, Qt.Unchecked)
|
|
|
|
|
|
pixmap = QPixmap(82, 64)
|
|
|
|
|
|
pixmap.fill(0xFF336699)
|
|
|
|
|
|
item.setIcon(QIcon(pixmap))
|
|
|
|
|
|
original_list.addItem(item)
|
|
|
|
|
|
original_list.show()
|
|
|
|
|
|
self.app.processEvents()
|
|
|
|
|
|
|
|
|
|
|
|
clicked = []
|
|
|
|
|
|
double_clicked = []
|
|
|
|
|
|
batch_deleted = []
|
|
|
|
|
|
single_deleted = []
|
|
|
|
|
|
original_list.itemClicked.connect(lambda item: clicked.append(item.data(Qt.UserRole)))
|
|
|
|
|
|
original_list.itemDoubleClicked.connect(
|
|
|
|
|
|
lambda item: double_clicked.append(item.data(Qt.UserRole))
|
|
|
|
|
|
)
|
|
|
|
|
|
original_list.deleteAssetsRequested.connect(lambda ids: batch_deleted.append(ids))
|
|
|
|
|
|
original_list.deleteRequested.connect(lambda asset_id: single_deleted.append(asset_id))
|
|
|
|
|
|
|
|
|
|
|
|
first_rect = original_list.visualItemRect(original_list.item(0))
|
|
|
|
|
|
check_point = ProductOriginalDelegate.checkbox_hit_rect(first_rect).center()
|
|
|
|
|
|
QTest.mouseClick(original_list.viewport(), Qt.LeftButton, pos=check_point)
|
|
|
|
|
|
self.assertEqual([11], original_list.checked_asset_ids())
|
|
|
|
|
|
self.assertEqual([], clicked)
|
|
|
|
|
|
QTest.mouseDClick(original_list.viewport(), Qt.LeftButton, pos=check_point)
|
|
|
|
|
|
self.assertEqual([], double_clicked)
|
|
|
|
|
|
|
|
|
|
|
|
QTest.mouseClick(original_list.viewport(), Qt.LeftButton, pos=first_rect.center())
|
|
|
|
|
|
self.assertEqual([11], clicked)
|
|
|
|
|
|
original_list.set_checked_asset_ids([11, 12])
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
[("删除选中的2张图片…", [11, 12])],
|
|
|
|
|
|
original_list.context_delete_options(11),
|
|
|
|
|
|
)
|
|
|
|
|
|
original_list.set_checked_asset_ids([12])
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
[("删除这张图片…", [11]), ("删除选中图片…", [12])],
|
|
|
|
|
|
original_list.context_delete_options(11),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
original_list.setFocus()
|
|
|
|
|
|
original_list.set_checked_asset_ids([11, 12])
|
|
|
|
|
|
QTest.keyClick(original_list, Qt.Key_Delete)
|
|
|
|
|
|
self.assertEqual([[11, 12]], batch_deleted)
|
|
|
|
|
|
original_list.clear_checks()
|
|
|
|
|
|
original_list.setCurrentRow(1)
|
|
|
|
|
|
QTest.keyClick(original_list, Qt.Key_Backspace)
|
|
|
|
|
|
self.assertEqual([12], single_deleted)
|
|
|
|
|
|
|
|
|
|
|
|
def test_batch_delete_confirms_main_image_and_blocks_running_or_downloading(self):
|
|
|
|
|
|
with self.make_temp_dir() as temp_dir:
|
|
|
|
|
|
config = self._config(temp_dir)
|
|
|
|
|
|
project, assets = self._create_project_with_assets(temp_dir, config, 3)
|
|
|
|
|
|
statuses = []
|
|
|
|
|
|
tab = ProductSuiteTab(
|
|
|
|
|
|
config=config,
|
|
|
|
|
|
db_path=config["db_path"],
|
|
|
|
|
|
status_callback=lambda message, level=None: statuses.append((message, level)),
|
|
|
|
|
|
)
|
|
|
|
|
|
self.addCleanup(tab.close)
|
|
|
|
|
|
state = tab._displayed_state
|
|
|
|
|
|
state.account_alias = "alias-a"
|
|
|
|
|
|
state.item_id = "51100639510"
|
|
|
|
|
|
state.project_id = project.id
|
|
|
|
|
|
tab._load_state(state)
|
|
|
|
|
|
|
|
|
|
|
|
confirmations = []
|
|
|
|
|
|
tab._confirm = lambda title, message, **kwargs: confirmations.append(
|
|
|
|
|
|
(title, message, kwargs)
|
|
|
|
|
|
) or True
|
|
|
|
|
|
tab.original_list.set_checked_asset_ids([assets[0].id, assets[1].id])
|
|
|
|
|
|
tab.delete_originals(tab.original_list.checked_asset_ids())
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual([assets[2].id], tab.original_list.asset_ids())
|
|
|
|
|
|
self.assertEqual([], tab.original_list.checked_asset_ids())
|
|
|
|
|
|
self.assertIn("选中的2张", confirmations[0][1])
|
|
|
|
|
|
self.assertIn("下一张图片将成为主图", confirmations[0][1])
|
|
|
|
|
|
self.assertIn("不会删除蝦皮线上图片", confirmations[0][1])
|
|
|
|
|
|
self.assertEqual(("已移除2张商品原图", "success"), statuses[-1])
|
|
|
|
|
|
|
|
|
|
|
|
messages = []
|
|
|
|
|
|
tab._message = lambda title, message, **kwargs: messages.append((title, message))
|
|
|
|
|
|
state.download_queue = [assets[2].id]
|
|
|
|
|
|
tab.delete_originals([assets[2].id])
|
|
|
|
|
|
self.assertIn("仍在后台下载", messages[-1][1])
|
|
|
|
|
|
self.assertIsNotNone(image_studio.get_asset(assets[2].id, path=config["db_path"]))
|
|
|
|
|
|
|
|
|
|
|
|
class RunningWorker:
|
|
|
|
|
|
def cancel(self):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
state.download_queue = []
|
|
|
|
|
|
state.worker = RunningWorker()
|
|
|
|
|
|
tab.delete_originals([assets[2].id])
|
|
|
|
|
|
self.assertEqual(("生成中不能删除当前任务的商品原图", "warning"), statuses[-1])
|
|
|
|
|
|
self.assertIsNotNone(image_studio.get_asset(assets[2].id, path=config["db_path"]))
|
|
|
|
|
|
state.worker = None
|
|
|
|
|
|
|
|
|
|
|
|
self.assert_removed(temp_dir)
|
|
|
|
|
|
|
2026-07-14 09:53:13 +08:00
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
unittest.main()
|