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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
216 lines
8.1 KiB
Python
216 lines
8.1 KiB
Python
"""标题渲染前后端一致性测试。
|
||
|
||
验证 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
|