9709d5471b
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m56s
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 / 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 / Validate - Migration (alembic) (push) Successful in 6m37s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 6m30s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 6m55s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m56s
AI Code Review / AI Code Review (pull_request) Failing after 4m14s
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 / Frontend Unit Tests (push) Successful in 8m39s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 8m58s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 9m46s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m56s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Failing after 11m6s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (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) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 12m56s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m2s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 13m12s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m46s
CI/CD Pipeline / Unit Tests (push) Successful in 16m46s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 8m9s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 8m30s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 14m55s
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 / CI Gate (pull_request) Successful in 29s
280 lines
10 KiB
Python
280 lines
10 KiB
Python
"""标题渲染前后端一致性测试。
|
||
|
||
验证 build_ass_content 生成的 ASS 样式参数与前端 drawTitleOnCanvas.ts 一致:
|
||
- 字号不再设置上限,由前端/调用方控制
|
||
- 描边宽度 2px
|
||
- 阴影 blur=4, offset=2
|
||
- boolean stroke/shadow 自动转换
|
||
"""
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
# 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 ",49," in content, f"默认字号36应补偿为49(36*1.35),实际内容: {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 ",43," in content # 32*1.35=43
|
||
|
||
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 == 81, f"字号60应补偿为81(60*1.35), 实际={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] == "70", f"font_size=52 应补偿为70(52*1.35), 实际={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 ",32," in content # 24*1.35=32.4→32
|
||
|
||
|
||
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] == "Noto Sans SC"
|
||
# Fontsize = 28*1.35=37.8→38
|
||
assert fields[2] == "38"
|
||
# 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
|
||
|
||
|
||
class TestTitleSlashNewline:
|
||
"""用户输入 / 或 / 应触发标题换行。"""
|
||
|
||
def test_halfwidth_slash_in_title(self):
|
||
content = build_ass_content(
|
||
video_width=1080,
|
||
video_height=1920,
|
||
video_duration=10.0,
|
||
title_text="第一行/第二行",
|
||
title_config={"size": 48},
|
||
)
|
||
for line in content.splitlines():
|
||
if "Dialogue" in line and "TitleStyle" in line:
|
||
assert "\\N" in line, f"斜杠应转为换行: {line}"
|
||
assert "第一行" in line
|
||
assert "第二行" in line
|
||
break
|
||
else:
|
||
pytest.fail("未找到 TitleStyle Dialogue 行")
|
||
|
||
def test_fullwidth_slash_in_title(self):
|
||
content = build_ass_content(
|
||
video_width=1080,
|
||
video_height=1920,
|
||
video_duration=10.0,
|
||
title_text="第一行/第二行",
|
||
title_config={"size": 48},
|
||
)
|
||
for line in content.splitlines():
|
||
if "Dialogue" in line and "TitleStyle" in line:
|
||
assert "\\N" in line, f"全角斜杠应转为换行: {line}"
|
||
break
|
||
else:
|
||
pytest.fail("未找到 TitleStyle Dialogue 行")
|