Files
xiaoxia-saas/tests/unit/test_title_render_consistency.py
T
xiaoxia 16767f675b
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Failing after 0s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 0s
CI/CD Pipeline / Unit Tests (push) Failing after 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 0s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m30s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m42s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m26s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 1m45s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
fix: 统一前后端标题渲染参数,修复描边/阴影样式丢失 (#1393)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-16 19:01:05 +08:00

216 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""标题渲染前后端一致性测试。
验证 build_ass_content 生成的 ASS 样式参数与前端 drawTitleOnCanvas.ts 一致:
- 字号上限 36px
- 描边宽度 2px
- 阴影 blur=4, offset=2
- boolean stroke/shadow 自动转换
"""
import sys
from pathlib import Path
# Ensure packages is importable
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "packages"))
from domain.ass_subtitle_builder import build_ass_content, build_ass_style
class TestFontSizeCap:
"""字号上限应与前端 Math.min(settings.size, 36) 一致。"""
def test_default_font_size_is_36(self):
"""无 size 字段时,默认字号应为 36。"""
config = {"text": "test"}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
assert ",36," in content, f"默认字号应为36,实际内容: {content}"
def test_size_32_preserved(self):
"""size=32 应原样使用。"""
config = {"size": 32}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
assert ",32," in content
def test_size_60_capped_at_36(self):
"""size=60 应被 cap 到 36。"""
config = {"size": 60}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
# 解析 Style 行的 Fontsize 字段(第3个字段,索引2)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
font_size = int(fields[2])
assert font_size == 36, f"字号60应被cap到36, 实际={font_size}"
def test_size_24_preserved(self):
"""size=24 应原样使用(小于36,不cap)。"""
config = {"size": 24}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
assert ",24," in content
class TestBooleanStrokeNormalization:
"""前端 stroke=true/false 应自动转换为标准 dict。"""
def test_stroke_true_enables_outline(self):
"""stroke=true 应生成 outline_width=2 的样式。"""
config = {"stroke": True}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
# 解析 Style 行的 Outline 字段(第17个字段,索引16)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
outline_width = float(fields[16])
assert outline_width == 2.0, f"stroke=true 应产生 outline_width=2, 实际={outline_width}"
def test_stroke_false_no_outline(self):
"""stroke=false 应生成 outline_width=0 的样式。"""
config = {"stroke": False}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
outline_width = float(fields[16])
assert outline_width == 0.0, f"stroke=false 应产生 outline_width=0, 实际={outline_width}"
def test_stroke_dict_still_works(self):
"""stroke={enabled:true, width:3} 仍应正常工作。"""
config = {"stroke": {"enabled": True, "width": 3, "color": "#FF0000"}}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
outline_width = float(fields[16])
assert outline_width == 3.0, f"自定义stroke width=3 应保留, 实际={outline_width}"
class TestBooleanShadowNormalization:
"""前端 shadow=true/false 应自动转换为标准 dict。"""
def test_shadow_true_enables_shadow(self):
"""shadow=true 应生成 shadow_depth=2 的样式。"""
config = {"shadow": True}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
# Shadow 字段是第18个(索引17
shadow_depth = int(fields[17])
assert shadow_depth == 2, f"shadow=true 应产生 shadow_depth=2, 实际={shadow_depth}"
def test_shadow_false_no_shadow(self):
"""shadow=false 应生成 shadow_depth=0 的样式。"""
config = {"shadow": False}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
shadow_depth = int(fields[17])
assert shadow_depth == 0, f"shadow=false 应产生 shadow_depth=0, 实际={shadow_depth}"
def test_shadow_dict_still_works(self):
"""shadow={enabled:true, blur:8} 仍应正常工作。"""
config = {"shadow": {"enabled": True, "blur": 8, "offset_x": 3, "offset_y": 3}}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
shadow_depth = int(fields[17])
assert shadow_depth == 3, f"自定义shadow offset_y=3 应保留, 实际={shadow_depth}"
class TestFullStyleConsistency:
"""完整样式参数一致性测试。"""
def test_frontend_default_style_matches_backend(self):
"""前端默认样式参数应在后端产生一致的 ASS 输出。
前端默认:font_size=24(或用户设置), bold=false, stroke=true, shadow=true, color=#FFFFFF
"""
config = {
"text": "标题文本",
"font": "思源黑体",
"size": 28,
"color": "#FFFFFF",
"bold": True,
"italic": False,
"stroke": True,
"shadow": True,
"position": "top",
}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="标题文本",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
# Fontname
assert fields[1] == "思源黑体"
# Fontsize = 28 (小于36,不cap)
assert fields[2] == "28"
# Bold = -1 (True)
assert fields[7] == "-1"
# Outline width = 2 (前端默认 stroke width)
assert float(fields[16]) == 2.0
# Shadow depth = 2 (offset_y)
assert int(fields[17]) == 2
# Alignment = 8 (top)
assert int(fields[18]) == 8