feat(gui): replace AI studio with product suite
This commit is contained in:
+3
-1
@@ -56,6 +56,7 @@ from app.gui import (
|
||||
ForcedUpdateDialog,
|
||||
ImageStudioTab,
|
||||
MainWindow,
|
||||
ProductSuiteTab,
|
||||
SettingsTab,
|
||||
TAB_STYLE,
|
||||
TAB_TITLES,
|
||||
@@ -491,7 +492,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
self.addCleanup(window.close)
|
||||
|
||||
self.assertEqual(gui.display_name(), window.windowTitle())
|
||||
self.assertEqual(5, window.tabs.count())
|
||||
self.assertEqual(6, window.tabs.count())
|
||||
self.assertEqual(
|
||||
TAB_TITLES,
|
||||
[window.tabs.tabText(index) for index in range(window.tabs.count())],
|
||||
@@ -511,6 +512,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
self.assertIsInstance(window.tabs.widget(2), ApplyTab)
|
||||
self.assertIsInstance(window.tabs.widget(3), AccountsTab)
|
||||
self.assertIsInstance(window.tabs.widget(4), SettingsTab)
|
||||
self.assertIsInstance(window.tabs.widget(5), ProductSuiteTab)
|
||||
self.assertFalse(
|
||||
any(
|
||||
isinstance(window.tabs.widget(index), ImageStudioTab)
|
||||
|
||||
@@ -59,6 +59,7 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
|
||||
"item_id",
|
||||
"target_main_count",
|
||||
"target_detail_count",
|
||||
"suite_settings_json",
|
||||
"deleted_at",
|
||||
}.issubset(projects_columns)
|
||||
)
|
||||
@@ -72,6 +73,50 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_init_db_migrates_legacy_projects_with_default_suite_settings(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "legacy-project.db")
|
||||
conn = db.connect(db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE image_studio_projects (
|
||||
id INTEGER PRIMARY KEY,
|
||||
account_alias TEXT NOT NULL,
|
||||
account_slug TEXT NOT NULL,
|
||||
account_name TEXT,
|
||||
item_id TEXT NOT NULL,
|
||||
target_main_count INTEGER NOT NULL DEFAULT 9,
|
||||
target_detail_count INTEGER NOT NULL DEFAULT 12,
|
||||
draft_prompt TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
deleted_at TEXT,
|
||||
deleted_reason TEXT,
|
||||
UNIQUE(account_alias, item_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO image_studio_projects
|
||||
(id, account_alias, account_slug, item_id, created_at, updated_at)
|
||||
VALUES (1, 'alias', 'alias_slug', '51100639510', '2026-07-14', '2026-07-14')
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
db.init_db(db_path)
|
||||
project = image_studio.get_project(1, path=db_path)
|
||||
|
||||
self.assertEqual("{}", project.suite_settings_json)
|
||||
self.assertEqual({}, image_studio.project_suite_settings(project))
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_project_crud_unique_per_account_and_image_dirs(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
@@ -110,6 +155,15 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
updated = image_studio.update_project_prompt(project.id, "二次提示词", path=db_path)
|
||||
self.assertEqual("二次提示词", updated.draft_prompt)
|
||||
suite_updated = image_studio.update_project_suite_settings(
|
||||
project.id,
|
||||
{"ratio": "3:4", "categories": {"白底图": 1}},
|
||||
path=db_path,
|
||||
)
|
||||
self.assertEqual(
|
||||
{"ratio": "3:4", "categories": {"白底图": 1}},
|
||||
image_studio.project_suite_settings(suite_updated),
|
||||
)
|
||||
|
||||
dirs = image_studio.project_image_dirs(os.path.join(temp_dir, "images"), project)
|
||||
self.assertEqual(
|
||||
@@ -215,6 +269,43 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_reorder_original_assets_requires_complete_project_order(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
db.init_db(db_path)
|
||||
project = image_studio.create_or_get_project(
|
||||
account_alias="alias",
|
||||
account_slug="alias_slug",
|
||||
item_id="51100639510",
|
||||
path=db_path,
|
||||
)
|
||||
first = image_studio.add_asset(
|
||||
project.id,
|
||||
image_studio.ASSET_KIND_ORIGINAL,
|
||||
local_path=os.path.join(temp_dir, "first.png"),
|
||||
source_order=1,
|
||||
path=db_path,
|
||||
)
|
||||
second = image_studio.add_asset(
|
||||
project.id,
|
||||
image_studio.ASSET_KIND_ORIGINAL,
|
||||
local_path=os.path.join(temp_dir, "second.png"),
|
||||
source_order=2,
|
||||
path=db_path,
|
||||
)
|
||||
|
||||
reordered = image_studio.reorder_original_assets(
|
||||
project.id,
|
||||
[second.id, first.id],
|
||||
path=db_path,
|
||||
)
|
||||
|
||||
self.assertEqual([second.id, first.id], [asset.id for asset in reordered])
|
||||
with self.assertRaisesRegex(db.DbError, "全部原图"):
|
||||
image_studio.reorder_original_assets(project.id, [first.id], path=db_path)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_remove_asset_only_when_not_referenced(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
@@ -287,6 +378,55 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_sync_original_asset_urls_preserves_local_upload_and_caps_active_assets(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
db.init_db(db_path)
|
||||
project = image_studio.create_or_get_project(
|
||||
account_alias="alias",
|
||||
account_slug="alias_slug",
|
||||
item_id="51100639510",
|
||||
path=db_path,
|
||||
)
|
||||
local = image_studio.add_asset(
|
||||
project.id,
|
||||
image_studio.ASSET_KIND_ORIGINAL,
|
||||
local_path=os.path.join(temp_dir, "local.png"),
|
||||
source_order=1,
|
||||
path=db_path,
|
||||
)
|
||||
|
||||
image_studio.sync_original_asset_urls(
|
||||
project.id,
|
||||
[
|
||||
{"index": index, "src": "https://susercontent.com/%d.jpg" % index}
|
||||
for index in range(1, 8)
|
||||
],
|
||||
max_assets=3,
|
||||
path=db_path,
|
||||
)
|
||||
active = image_studio.list_assets(
|
||||
project.id,
|
||||
kind=image_studio.ASSET_KIND_ORIGINAL,
|
||||
include_missing=False,
|
||||
path=db_path,
|
||||
)
|
||||
|
||||
self.assertEqual(3, len(active))
|
||||
self.assertIn(local.id, [asset.id for asset in active])
|
||||
self.assertEqual(
|
||||
image_studio.ASSET_STATUS_AVAILABLE,
|
||||
image_studio.get_asset(local.id, path=db_path).status,
|
||||
)
|
||||
|
||||
image_studio.sync_original_asset_urls(project.id, [], max_assets=3, path=db_path)
|
||||
self.assertEqual(
|
||||
image_studio.ASSET_STATUS_AVAILABLE,
|
||||
image_studio.get_asset(local.id, path=db_path).status,
|
||||
)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_job_lifecycle_and_resumable_query(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
@@ -376,6 +516,23 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
|
||||
[],
|
||||
image_studio.list_resumable_jobs(path=db_path, include_failed_downloads=True),
|
||||
)
|
||||
self.assertEqual(
|
||||
[terminal.id, job.id],
|
||||
[item.id for item in image_studio.list_jobs(project.id, path=db_path)],
|
||||
)
|
||||
self.assertEqual(
|
||||
[terminal.id],
|
||||
[
|
||||
item.id
|
||||
for item in image_studio.list_jobs(
|
||||
project.id,
|
||||
statuses=["failed"],
|
||||
path=db_path,
|
||||
)
|
||||
],
|
||||
)
|
||||
with self.assertRaisesRegex(db.DbError, "状态无效"):
|
||||
image_studio.list_jobs(project.id, statuses=["unknown"], path=db_path)
|
||||
|
||||
with self.assertRaises(db.DbError):
|
||||
image_studio.create_job(
|
||||
|
||||
@@ -146,6 +146,95 @@ class ImageStudioGenerationTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_generate_image_jobs_sends_selected_aspect_ratio(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg, project, source = self._project_source(temp_dir)
|
||||
submitted_payloads = []
|
||||
|
||||
def fake_submit(method, url, api_key, **kwargs):
|
||||
submitted_payloads.append(dict(kwargs["payload"]))
|
||||
return {"task_id": "cmhub-ratio", "status": "queued"}
|
||||
|
||||
with mock.patch("app.image_studio_generation._runtime", return_value=self._runtime()), \
|
||||
mock.patch(
|
||||
"app.image_studio_generation.ai._cmhub_call_with_retry",
|
||||
side_effect=fake_submit,
|
||||
), \
|
||||
mock.patch(
|
||||
"app.image_studio_generation.ai._cmhub_call_once",
|
||||
return_value={
|
||||
"task_id": "cmhub-ratio",
|
||||
"status": "succeeded",
|
||||
"result": {"image_url": "https://cdn.example.com/ratio.png"},
|
||||
},
|
||||
), \
|
||||
mock.patch(
|
||||
"app.image_studio_generation.ai._download_cmhub_image_with_retry",
|
||||
return_value=(self._png_bytes(), 0.1),
|
||||
):
|
||||
summary = image_studio_generation.generate_image_jobs(
|
||||
project.id,
|
||||
source.id,
|
||||
"比例测试",
|
||||
1,
|
||||
aspect_ratio="3:4",
|
||||
config=cfg,
|
||||
path=cfg["db_path"],
|
||||
)
|
||||
|
||||
self.assertEqual(1, summary["success"])
|
||||
self.assertEqual("3:4", submitted_payloads[0]["aspect_ratio"])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_stop_after_download_discards_temporary_result(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg, project, source = self._project_source(temp_dir)
|
||||
stopped = {"value": False}
|
||||
saved_paths = []
|
||||
|
||||
def fake_download(request_result, out_path, config, on_event, job_id):
|
||||
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
||||
with open(out_path, "wb") as fh:
|
||||
fh.write(self._png_bytes())
|
||||
saved_paths.append(out_path)
|
||||
stopped["value"] = True
|
||||
return out_path
|
||||
|
||||
with mock.patch("app.image_studio_generation._runtime", return_value=self._runtime()), \
|
||||
mock.patch(
|
||||
"app.image_studio_generation.ai._cmhub_call_with_retry",
|
||||
return_value={"task_id": "cmhub-stop", "status": "queued"},
|
||||
), \
|
||||
mock.patch(
|
||||
"app.image_studio_generation.ai._cmhub_call_once",
|
||||
return_value={
|
||||
"task_id": "cmhub-stop",
|
||||
"status": "succeeded",
|
||||
"result": {"image_url": "https://cdn.example.com/stop.png"},
|
||||
},
|
||||
), \
|
||||
mock.patch(
|
||||
"app.image_studio_generation._download_and_save_job_image",
|
||||
side_effect=fake_download,
|
||||
):
|
||||
summary = image_studio_generation.generate_image_jobs(
|
||||
project.id,
|
||||
source.id,
|
||||
"停止测试",
|
||||
1,
|
||||
config=cfg,
|
||||
path=cfg["db_path"],
|
||||
should_stop=lambda: stopped["value"],
|
||||
)
|
||||
|
||||
self.assertEqual(1, summary["cancelled"])
|
||||
self.assertEqual([], image_studio.list_assets(project.id, kind="generated_main", path=cfg["db_path"]))
|
||||
self.assertEqual(1, len(saved_paths))
|
||||
self.assertFalse(os.path.exists(saved_paths[0]))
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_resume_existing_job_polls_without_new_submit(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg, project, source = self._project_source(temp_dir)
|
||||
|
||||
@@ -246,6 +246,149 @@ class ImageStudioImageTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_import_original_files_copies_valid_images_deduplicates_and_limits(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
db.init_db(db_path)
|
||||
project = image_studio.create_or_get_project(
|
||||
account_alias="alias",
|
||||
account_slug="alias_slug",
|
||||
item_id="51100639510",
|
||||
path=db_path,
|
||||
)
|
||||
source = os.path.join(temp_dir, "商品图.png")
|
||||
with open(source, "wb") as fh:
|
||||
fh.write(self._png_bytes())
|
||||
config = {
|
||||
"data_dir": temp_dir,
|
||||
"db_path": db_path,
|
||||
"image_dir": os.path.join(temp_dir, "images"),
|
||||
}
|
||||
|
||||
first = image_studio_images.import_original_files(
|
||||
project.id,
|
||||
[source],
|
||||
path=db_path,
|
||||
config=config,
|
||||
)
|
||||
duplicate = image_studio_images.import_original_files(
|
||||
project.id,
|
||||
[source],
|
||||
path=db_path,
|
||||
config=config,
|
||||
)
|
||||
|
||||
self.assertEqual([], first["errors"])
|
||||
self.assertEqual(first["assets"][0].id, duplicate["assets"][0].id)
|
||||
self.assertNotEqual(os.path.abspath(source), first["assets"][0].local_path)
|
||||
self.assertTrue(os.path.isfile(first["assets"][0].local_path))
|
||||
with self.assertRaisesRegex(image_studio_images.ImageStudioImageError, "最多"):
|
||||
image_studio_images.import_original_bytes(
|
||||
project.id,
|
||||
self._png_bytes(color=(10, 20, 30, 255)),
|
||||
filename_hint="second.png",
|
||||
path=db_path,
|
||||
config=config,
|
||||
max_assets=1,
|
||||
)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_import_original_ignores_missing_history_when_enforcing_limit(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
db.init_db(db_path)
|
||||
project = image_studio.create_or_get_project(
|
||||
account_alias="alias",
|
||||
account_slug="alias_slug",
|
||||
item_id="51100639510",
|
||||
path=db_path,
|
||||
)
|
||||
config = {
|
||||
"data_dir": temp_dir,
|
||||
"db_path": db_path,
|
||||
"image_dir": os.path.join(temp_dir, "images"),
|
||||
}
|
||||
first = image_studio_images.import_original_bytes(
|
||||
project.id,
|
||||
self._png_bytes(),
|
||||
filename_hint="first.png",
|
||||
path=db_path,
|
||||
config=config,
|
||||
max_assets=1,
|
||||
)
|
||||
image_studio.mark_asset_status(
|
||||
first.id,
|
||||
image_studio.ASSET_STATUS_MISSING,
|
||||
path=db_path,
|
||||
)
|
||||
|
||||
second = image_studio_images.import_original_bytes(
|
||||
project.id,
|
||||
self._png_bytes(color=(10, 20, 30, 255)),
|
||||
filename_hint="second.png",
|
||||
path=db_path,
|
||||
config=config,
|
||||
max_assets=1,
|
||||
)
|
||||
|
||||
self.assertNotEqual(first.id, second.id)
|
||||
self.assertEqual(image_studio.ASSET_STATUS_AVAILABLE, second.status)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_generated_asset_trash_and_restore_keep_database_history(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
db.init_db(db_path)
|
||||
project = image_studio.create_or_get_project(
|
||||
account_alias="alias",
|
||||
account_slug="alias_slug",
|
||||
item_id="51100639510",
|
||||
path=db_path,
|
||||
)
|
||||
config = {
|
||||
"data_dir": temp_dir,
|
||||
"db_path": db_path,
|
||||
"image_dir": os.path.join(temp_dir, "images"),
|
||||
}
|
||||
generated_dir = image_studio.default_project_image_dirs(project, config=config)[
|
||||
"generated"
|
||||
]
|
||||
os.makedirs(generated_dir, exist_ok=True)
|
||||
generated_path = os.path.join(generated_dir, "result.png")
|
||||
with open(generated_path, "wb") as fh:
|
||||
fh.write(self._png_bytes())
|
||||
asset = image_studio.add_asset(
|
||||
project.id,
|
||||
"generated_main",
|
||||
local_path=generated_path,
|
||||
path=db_path,
|
||||
)
|
||||
|
||||
record = image_studio_images.trash_generated_asset(
|
||||
asset.id,
|
||||
path=db_path,
|
||||
config=config,
|
||||
)
|
||||
trashed = image_studio.get_asset(asset.id, path=db_path)
|
||||
|
||||
self.assertEqual(image_studio.ASSET_STATUS_MISSING, trashed.status)
|
||||
self.assertTrue(os.path.isfile(record["trash_path"]))
|
||||
self.assertFalse(os.path.exists(generated_path))
|
||||
|
||||
restored = image_studio_images.restore_trashed_asset(
|
||||
record,
|
||||
path=db_path,
|
||||
config=config,
|
||||
)
|
||||
|
||||
self.assertEqual(asset.id, restored.id)
|
||||
self.assertEqual(image_studio.ASSET_STATUS_AVAILABLE, restored.status)
|
||||
self.assertTrue(os.path.isfile(restored.local_path))
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from app import product_suite
|
||||
|
||||
|
||||
class ProductSuiteTests(unittest.TestCase):
|
||||
def test_defaults_and_per_image_total(self):
|
||||
settings = product_suite.default_suite_settings()
|
||||
|
||||
self.assertEqual(["白底图", "场景图", "卖点图"], product_suite.category_order(settings))
|
||||
self.assertEqual(5, product_suite.suite_total_count(settings, 3))
|
||||
|
||||
settings["per_image_primary"] = True
|
||||
|
||||
self.assertEqual(13, product_suite.suite_total_count(settings, 3))
|
||||
|
||||
def test_custom_category_validation_and_normalization(self):
|
||||
self.assertEqual("分类名称不能为空", product_suite.suite_name_error(""))
|
||||
self.assertEqual("分类名称不能包含空格", product_suite.suite_name_error("使用 场景"))
|
||||
self.assertEqual("分类名称不能超过10个字", product_suite.suite_name_error("一二三四五六七八九十甲"))
|
||||
self.assertEqual("分类名称已存在", product_suite.suite_name_error("场景图", {"场景图"}))
|
||||
|
||||
settings = product_suite.normalize_suite_settings(
|
||||
{
|
||||
"ratio": "3:4",
|
||||
"categories": {"白底图": 2, "场景图": 0, "卖点图": 1, "尺寸图": 2},
|
||||
"custom_category_order": ["尺寸图"],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual("3:4", settings["ratio"])
|
||||
self.assertEqual(["白底图", "场景图", "卖点图", "尺寸图"], product_suite.category_order(settings))
|
||||
self.assertEqual(5, product_suite.suite_total_count(settings, 1))
|
||||
|
||||
def test_job_specs_include_selected_context_and_source_assignment(self):
|
||||
settings = product_suite.default_suite_settings()
|
||||
settings.update(
|
||||
{
|
||||
"platform": "Shopee",
|
||||
"country": "中国台湾",
|
||||
"language": "繁体中文",
|
||||
"ratio": "4:3",
|
||||
"per_image_primary": True,
|
||||
"categories": {"白底图": 1, "场景图": 1, "卖点图": 0},
|
||||
}
|
||||
)
|
||||
assets = [SimpleNamespace(id=11), SimpleNamespace(id=12)]
|
||||
|
||||
specs = product_suite.build_job_specs(
|
||||
assets,
|
||||
"40小时续航,适合通勤",
|
||||
settings,
|
||||
"51100639510",
|
||||
)
|
||||
|
||||
self.assertEqual(3, len(specs))
|
||||
self.assertEqual([11, 11, 12], [spec["source_asset_id"] for spec in specs])
|
||||
self.assertEqual(["白底图", "场景图", "场景图"], [spec["job_type"] for spec in specs])
|
||||
for spec in specs:
|
||||
self.assertIn("平台:Shopee", spec["prompt"])
|
||||
self.assertIn("国家地区:中国台湾", spec["prompt"])
|
||||
self.assertIn("输出语言:繁体中文", spec["prompt"])
|
||||
self.assertIn("图片比例:4:3", spec["prompt"])
|
||||
self.assertIn("商品ID:51100639510", spec["prompt"])
|
||||
self.assertIn("40小时续航", spec["prompt"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,172 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from _helpers import TempDirMixin
|
||||
|
||||
from app import accounts, image_studio, image_studio_images
|
||||
from app import gui
|
||||
|
||||
if gui.QT_IMPORT_ERROR is not None:
|
||||
raise unittest.SkipTest("PySide6 未安装")
|
||||
|
||||
from PySide6.QtGui import QImage
|
||||
from PySide6.QtWidgets import QApplication, QLabel, QPushButton
|
||||
|
||||
from app.gui.tabs.product_suite import ProductSuiteTab, SuiteResultCard
|
||||
|
||||
|
||||
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))
|
||||
|
||||
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)
|
||||
|
||||
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())
|
||||
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"])
|
||||
self.assertEqual("尺寸图", tab._displayed_state.active_category)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,79 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from _helpers import TempDirMixin
|
||||
|
||||
from app import db, image_studio
|
||||
from app import gui
|
||||
|
||||
if gui.QT_IMPORT_ERROR is not None:
|
||||
raise unittest.SkipTest("PySide6 未安装")
|
||||
|
||||
from app.gui.workers import ProductSuiteGenerateWorker
|
||||
|
||||
|
||||
class ProductSuiteWorkerTests(TempDirMixin, unittest.TestCase):
|
||||
def test_generate_worker_creates_category_jobs_and_forwards_ratio(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
db.init_db(db_path)
|
||||
project = image_studio.create_or_get_project(
|
||||
account_alias="alias",
|
||||
account_slug="alias_slug",
|
||||
item_id="51100639510",
|
||||
path=db_path,
|
||||
)
|
||||
source = image_studio.add_asset(
|
||||
project.id,
|
||||
image_studio.ASSET_KIND_ORIGINAL,
|
||||
local_path=os.path.join(temp_dir, "source.png"),
|
||||
path=db_path,
|
||||
)
|
||||
worker = ProductSuiteGenerateWorker(
|
||||
project.id,
|
||||
[
|
||||
{
|
||||
"source_asset_id": source.id,
|
||||
"job_type": "白底图",
|
||||
"prompt": "白底商品图",
|
||||
},
|
||||
{
|
||||
"source_asset_id": source.id,
|
||||
"job_type": "场景图",
|
||||
"prompt": "通勤场景图",
|
||||
},
|
||||
],
|
||||
aspect_ratio="3:4",
|
||||
db_path=db_path,
|
||||
config={"db_path": db_path},
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"app.gui.workers.image_studio_generation.run_jobs",
|
||||
return_value={
|
||||
"total": 2,
|
||||
"success": 2,
|
||||
"failed": 0,
|
||||
"cancelled": 0,
|
||||
"jobs": [],
|
||||
},
|
||||
) as run_jobs:
|
||||
result = worker.execute()
|
||||
|
||||
jobs = image_studio.list_jobs(project.id, path=db_path)
|
||||
self.assertEqual(2, len(jobs))
|
||||
self.assertEqual({"白底图", "场景图"}, {job.job_type for job in jobs})
|
||||
self.assertEqual([job.id for job in reversed(jobs)], result["job_ids"])
|
||||
self.assertEqual("3:4", run_jobs.call_args.kwargs["aspect_ratio"])
|
||||
self.assertEqual(db_path, run_jobs.call_args.kwargs["path"])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user