Files
xiaoxia-saas/tests/unit/test_title_render_consistency.py
T
xiaoxia 35f4ae8b3f
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m24s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m49s
AI Code Review / AI Code Review (pull_request) Successful in 2m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m25s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 20s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m44s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 4m11s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 6m14s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m3s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 13m9s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m4s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 55s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m8s
fix(ass): normalize font_size/font_color field aliases and remove font size cap
- Add field name normalization in build_ass_content: frontend sends
  font_size/font_color, internal uses size/color. Normalize before
  stroke/shadow boolean conversion.
- Remove min(size, 36) cap from both build_ass_style font_size and
  _wrap_title_text font_size, allowing larger title fonts.
- Update tests: rename TestFontSizeCap to TestFontSize, change capped
  assertions to preserve-actual, add alias normalization tests.
2026-08-25 21:15:08 +08:00

242 lines
9.0 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 一致:
- 字号不再设置上限,由前端/调用方控制
- 描边宽度 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 TestFontSize:
"""字号处理:默认值与保留逻辑,不再做上限截断。"""
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_preserved(self):
"""size=60 应原样保留(字号上限已移除)。"""
config = {"size": 60}
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(",")]
font_size = int(fields[2])
assert font_size == 60, f"字号60应原样保留, 实际={font_size}"
def test_font_size_alias_normalized(self):
"""前端传 font_size 应归一化为 size。"""
config = {"font_size": 52}
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(",")]
assert fields[2] == "52", f"font_size=52 应归一化, 实际={fields[2]}"
def test_font_color_alias_normalized(self):
"""前端传 font_color 应归一化为 color。"""
config = {"font_color": "#00FF00"}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
# 绿色 → &H00FF00
assert "&H00FF00" in content
def test_size_24_preserved(self):
"""size=24 应原样使用。"""
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
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