2d67fe8631
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
479 lines
14 KiB
Python
Executable File
479 lines
14 KiB
Python
Executable File
"""sticker_config 模块单测 — 纯逻辑."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from packages.domain.sticker_config import (
|
|
POSITION_PRESETS,
|
|
STICKER_CATEGORIES,
|
|
ImageStickerConfig,
|
|
StickerOverlayResult,
|
|
TextStickerConfig,
|
|
get_sticker_categories,
|
|
parse_stickers_from_config,
|
|
resolve_sticker_position,
|
|
)
|
|
|
|
# ── 常量测试 ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestConstants:
|
|
def test_position_presets_has_9_positions(self):
|
|
assert len(POSITION_PRESETS) == 9
|
|
|
|
def test_position_presets_normalized(self):
|
|
for name, (x, y) in POSITION_PRESETS.items():
|
|
assert 0.0 <= x <= 1.0
|
|
assert 0.0 <= y <= 1.0
|
|
|
|
def test_sticker_categories(self):
|
|
assert len(STICKER_CATEGORIES) >= 3
|
|
assert ("emoji", "表情包") in STICKER_CATEGORIES
|
|
assert ("text", "文字花字") in STICKER_CATEGORIES
|
|
|
|
|
|
# ── ImageStickerConfig 测试 ──────────────────────────────────────────────────
|
|
|
|
|
|
class TestImageStickerDefaults:
|
|
def test_default_values(self):
|
|
cfg = ImageStickerConfig()
|
|
assert cfg.enabled is False
|
|
assert cfg.type == "image"
|
|
assert cfg.position == "top_right"
|
|
assert cfg.x is None
|
|
assert cfg.y is None
|
|
assert cfg.scale == 1.0
|
|
assert cfg.opacity == 1.0
|
|
assert cfg.start_time == 0.0
|
|
assert cfg.duration == 0.0
|
|
assert cfg.fade_in == 0.0
|
|
assert cfg.fade_out == 0.0
|
|
assert cfg.z_index == 10
|
|
|
|
|
|
class TestImageStickerFromDict:
|
|
def test_none_returns_default(self):
|
|
cfg = ImageStickerConfig.from_dict(None)
|
|
assert cfg.position == "top_right"
|
|
assert cfg.scale == 1.0
|
|
|
|
def test_empty_dict_returns_default(self):
|
|
cfg = ImageStickerConfig.from_dict({})
|
|
assert cfg.enabled is False
|
|
|
|
def test_custom_values(self):
|
|
cfg = ImageStickerConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"position": "center",
|
|
"scale": 1.5,
|
|
"opacity": 0.8,
|
|
"start_time": 2.0,
|
|
"duration": 5.0,
|
|
"z_index": 20,
|
|
"image_url": "https://example.com/img.png",
|
|
}
|
|
)
|
|
assert cfg.enabled is True
|
|
assert cfg.position == "center"
|
|
assert cfg.scale == 1.5
|
|
assert cfg.opacity == 0.8
|
|
assert cfg.start_time == 2.0
|
|
assert cfg.duration == 5.0
|
|
assert cfg.z_index == 20
|
|
assert cfg.image_url == "https://example.com/img.png"
|
|
|
|
def test_custom_xy_pixel(self):
|
|
cfg = ImageStickerConfig.from_dict(
|
|
{
|
|
"x": 100,
|
|
"y": 200,
|
|
"x_unit": "pixel",
|
|
"y_unit": "pixel",
|
|
}
|
|
)
|
|
assert cfg.x == 100.0
|
|
assert cfg.y == 200.0
|
|
assert cfg.x_unit == "pixel"
|
|
assert cfg.y_unit == "pixel"
|
|
|
|
def test_opacity_clamped(self):
|
|
cfg = ImageStickerConfig.from_dict({"opacity": 1.5})
|
|
assert cfg.opacity == 1.0
|
|
cfg2 = ImageStickerConfig.from_dict({"opacity": -0.5})
|
|
assert cfg2.opacity == 0.0
|
|
|
|
def test_scale_minimum(self):
|
|
cfg = ImageStickerConfig.from_dict({"scale": 0.001})
|
|
assert cfg.scale == 0.01
|
|
|
|
def test_start_time_clamped(self):
|
|
cfg = ImageStickerConfig.from_dict({"start_time": -1})
|
|
assert cfg.start_time == 0.0
|
|
|
|
def test_duration_clamped(self):
|
|
cfg = ImageStickerConfig.from_dict({"duration": -5})
|
|
assert cfg.duration == 0.0
|
|
|
|
def test_invalid_x_returns_none(self):
|
|
cfg = ImageStickerConfig.from_dict({"x": "invalid"})
|
|
assert cfg.x is None
|
|
|
|
def test_width_height_int(self):
|
|
cfg = ImageStickerConfig.from_dict({"width": 200, "height": 100})
|
|
assert cfg.width == 200
|
|
assert cfg.height == 100
|
|
|
|
|
|
class TestImageStickerProperties:
|
|
def test_has_time_range_true(self):
|
|
cfg = ImageStickerConfig(duration=5.0)
|
|
assert cfg.has_time_range is True
|
|
|
|
def test_has_time_range_false(self):
|
|
cfg = ImageStickerConfig(duration=0.0)
|
|
assert cfg.has_time_range is False
|
|
|
|
def test_end_time(self):
|
|
cfg = ImageStickerConfig(start_time=2.0, duration=3.0)
|
|
assert cfg.end_time == 5.0
|
|
|
|
def test_end_time_zero_duration(self):
|
|
cfg = ImageStickerConfig(start_time=2.0, duration=0.0)
|
|
assert cfg.end_time == 2.0
|
|
|
|
|
|
# ── TextStickerConfig 测试 ───────────────────────────────────────────────────
|
|
|
|
|
|
class TestTextStickerDefaults:
|
|
def test_default_values(self):
|
|
cfg = TextStickerConfig()
|
|
assert cfg.enabled is False
|
|
assert cfg.type == "text"
|
|
assert cfg.text == ""
|
|
assert cfg.font_size == 36
|
|
assert cfg.font_color == "#FFFFFF"
|
|
assert cfg.stroke_width == 2
|
|
assert cfg.position == "center"
|
|
assert cfg.bg_color == ""
|
|
assert cfg.bg_padding == 8
|
|
assert cfg.bg_alpha == 0.8
|
|
assert cfg.z_index == 10
|
|
|
|
|
|
class TestTextStickerFromDict:
|
|
def test_none_returns_default(self):
|
|
cfg = TextStickerConfig.from_dict(None)
|
|
assert cfg.font_size == 36
|
|
|
|
def test_custom_text(self):
|
|
cfg = TextStickerConfig.from_dict({"text": "Hello World", "font_size": 48})
|
|
assert cfg.text == "Hello World"
|
|
assert cfg.font_size == 48
|
|
|
|
def test_font_color(self):
|
|
cfg = TextStickerConfig.from_dict({"font_color": "#FF0000"})
|
|
assert cfg.font_color == "#FF0000"
|
|
|
|
def test_stroke_config(self):
|
|
cfg = TextStickerConfig.from_dict(
|
|
{
|
|
"stroke_color": "#00FF00",
|
|
"stroke_width": 4,
|
|
}
|
|
)
|
|
assert cfg.stroke_color == "#00FF00"
|
|
assert cfg.stroke_width == 4
|
|
|
|
def test_shadow_config(self):
|
|
cfg = TextStickerConfig.from_dict(
|
|
{
|
|
"shadow_x": 4,
|
|
"shadow_y": 4,
|
|
"shadow_alpha": 0.7,
|
|
}
|
|
)
|
|
assert cfg.shadow_x == 4
|
|
assert cfg.shadow_y == 4
|
|
assert cfg.shadow_alpha == 0.7
|
|
|
|
def test_background_config(self):
|
|
cfg = TextStickerConfig.from_dict(
|
|
{
|
|
"bg_color": "#000000",
|
|
"bg_padding": 12,
|
|
"bg_alpha": 0.9,
|
|
"bg_corner_radius": 10,
|
|
}
|
|
)
|
|
assert cfg.bg_color == "#000000"
|
|
assert cfg.bg_padding == 12
|
|
assert cfg.bg_alpha == 0.9
|
|
assert cfg.bg_corner_radius == 10
|
|
|
|
def test_font_size_minimum(self):
|
|
cfg = TextStickerConfig.from_dict({"font_size": 0})
|
|
assert cfg.font_size == 1
|
|
|
|
def test_stroke_width_negative_clamped(self):
|
|
cfg = TextStickerConfig.from_dict({"stroke_width": -2})
|
|
assert cfg.stroke_width == 0
|
|
|
|
def test_shadow_alpha_clamped(self):
|
|
cfg = TextStickerConfig.from_dict({"shadow_alpha": 1.5})
|
|
assert cfg.shadow_alpha == 1.0
|
|
|
|
def test_bg_alpha_clamped(self):
|
|
cfg = TextStickerConfig.from_dict({"bg_alpha": -0.5})
|
|
assert cfg.bg_alpha == 0.0
|
|
|
|
def test_invalid_font_size_falls_back(self):
|
|
cfg = TextStickerConfig.from_dict({"font_size": "large"})
|
|
assert cfg.font_size == 36
|
|
|
|
|
|
class TestTextStickerProperties:
|
|
def test_has_background_true(self):
|
|
cfg = TextStickerConfig(bg_color="#000000")
|
|
assert cfg.has_background is True
|
|
|
|
def test_has_background_false(self):
|
|
cfg = TextStickerConfig(bg_color="")
|
|
assert cfg.has_background is False
|
|
|
|
def test_has_time_range_true(self):
|
|
cfg = TextStickerConfig(duration=3.0)
|
|
assert cfg.has_time_range is True
|
|
|
|
|
|
# ── StickerOverlayResult 测试 ────────────────────────────────────────────────
|
|
|
|
|
|
class TestStickerOverlayResult:
|
|
def test_basic(self):
|
|
result = StickerOverlayResult(filter_str="overlay", output_label="[out]")
|
|
assert result.filter_str == "overlay"
|
|
assert result.output_label == "[out]"
|
|
assert result.extra_inputs == []
|
|
|
|
def test_with_extra_inputs(self):
|
|
result = StickerOverlayResult(
|
|
filter_str="overlay",
|
|
output_label="[out]",
|
|
extra_inputs=["sticker.png"],
|
|
)
|
|
assert result.extra_inputs == ["sticker.png"]
|
|
|
|
|
|
# ── resolve_sticker_position 测试 ───────────────────────────────────────────
|
|
|
|
|
|
class TestResolvePositionPresets:
|
|
def test_top_left(self):
|
|
x, y = resolve_sticker_position(
|
|
"top_left",
|
|
None,
|
|
None,
|
|
"percent",
|
|
"percent",
|
|
canvas_w=1000,
|
|
canvas_h=500,
|
|
sticker_w=100,
|
|
sticker_h=50,
|
|
)
|
|
# 0.05 * 1000 - 50 = 0, 0.05 * 500 - 25 = 0
|
|
assert x == pytest.approx(0.0)
|
|
assert y == pytest.approx(0.0)
|
|
|
|
def test_center(self):
|
|
x, y = resolve_sticker_position(
|
|
"center",
|
|
None,
|
|
None,
|
|
"percent",
|
|
"percent",
|
|
canvas_w=1000,
|
|
canvas_h=500,
|
|
sticker_w=100,
|
|
sticker_h=50,
|
|
)
|
|
# 0.5 * 1000 - 50 = 450, 0.5 * 500 - 25 = 225
|
|
assert x == pytest.approx(450.0)
|
|
assert y == pytest.approx(225.0)
|
|
|
|
def test_bottom_right(self):
|
|
x, y = resolve_sticker_position(
|
|
"bottom_right",
|
|
None,
|
|
None,
|
|
"percent",
|
|
"percent",
|
|
canvas_w=1000,
|
|
canvas_h=500,
|
|
sticker_w=100,
|
|
sticker_h=50,
|
|
)
|
|
# 0.95 * 1000 - 50 = 900, 0.95 * 500 - 25 = 450
|
|
assert x == pytest.approx(900.0)
|
|
assert y == pytest.approx(450.0)
|
|
|
|
def test_invalid_position_defaults_center(self):
|
|
x, y = resolve_sticker_position(
|
|
"invalid_pos",
|
|
None,
|
|
None,
|
|
"percent",
|
|
"percent",
|
|
canvas_w=1000,
|
|
canvas_h=500,
|
|
sticker_w=100,
|
|
sticker_h=50,
|
|
)
|
|
assert x == pytest.approx(450.0)
|
|
assert y == pytest.approx(225.0)
|
|
|
|
|
|
class TestResolvePositionCustomPercent:
|
|
def test_custom_percent(self):
|
|
x, y = resolve_sticker_position(
|
|
"center",
|
|
25.0,
|
|
75.0,
|
|
"percent",
|
|
"percent",
|
|
canvas_w=1000,
|
|
canvas_h=500,
|
|
sticker_w=100,
|
|
sticker_h=50,
|
|
)
|
|
# 0.25 * 1000 - 50 = 200, 0.75 * 500 - 25 = 350
|
|
assert x == pytest.approx(200.0)
|
|
assert y == pytest.approx(350.0)
|
|
|
|
def test_percent_clamped_0_100(self):
|
|
x, y = resolve_sticker_position(
|
|
"center",
|
|
150.0,
|
|
-50.0,
|
|
"percent",
|
|
"percent",
|
|
canvas_w=1000,
|
|
canvas_h=500,
|
|
sticker_w=100,
|
|
sticker_h=50,
|
|
)
|
|
# x=100% → 1.0*1000-50=950, y=0% → 0*500-25=钳制到0
|
|
assert x == pytest.approx(900.0)
|
|
assert y == pytest.approx(0.0)
|
|
|
|
|
|
class TestResolvePositionCustomPixel:
|
|
def test_custom_pixel(self):
|
|
x, y = resolve_sticker_position(
|
|
"center",
|
|
200.0,
|
|
300.0,
|
|
"pixel",
|
|
"pixel",
|
|
canvas_w=1000,
|
|
canvas_h=500,
|
|
sticker_w=100,
|
|
sticker_h=50,
|
|
)
|
|
# 200/1000 = 0.2 → 0.2*1000-50=150, 300/500=0.6 → 0.6*500-25=275
|
|
assert x == pytest.approx(150.0)
|
|
assert y == pytest.approx(275.0)
|
|
|
|
|
|
class TestResolvePositionEdgeCases:
|
|
def test_zero_canvas(self):
|
|
x, y = resolve_sticker_position(
|
|
"center",
|
|
50.0,
|
|
50.0,
|
|
"pixel",
|
|
"pixel",
|
|
canvas_w=0,
|
|
canvas_h=0,
|
|
sticker_w=10,
|
|
sticker_h=10,
|
|
)
|
|
# canvas=0 时用默认 0.5, 0.5
|
|
assert x == pytest.approx(0.0)
|
|
assert y == pytest.approx(0.0)
|
|
|
|
def test_zero_sticker_size(self):
|
|
x, y = resolve_sticker_position(
|
|
"center",
|
|
None,
|
|
None,
|
|
"percent",
|
|
"percent",
|
|
canvas_w=1000,
|
|
canvas_h=500,
|
|
sticker_w=0,
|
|
sticker_h=0,
|
|
)
|
|
assert x == pytest.approx(500.0)
|
|
assert y == pytest.approx(250.0)
|
|
|
|
def test_clamped_when_sticker_larger_than_canvas(self):
|
|
# 贴纸比画布大时,钳制到0(x=0, y=0)
|
|
x, y = resolve_sticker_position(
|
|
"top_left",
|
|
None,
|
|
None,
|
|
"percent",
|
|
"percent",
|
|
canvas_w=100,
|
|
canvas_h=100,
|
|
sticker_w=200,
|
|
sticker_h=200,
|
|
)
|
|
# 位置为左上角0.05 → 钳制到 0
|
|
assert x == 0.0
|
|
assert y == 0.0
|
|
|
|
|
|
# ── parse_stickers_from_config 测试 ─────────────────────────────────────────
|
|
|
|
|
|
class TestParseStickersFromConfig:
|
|
def test_none_config(self):
|
|
assert parse_stickers_from_config(None) == []
|
|
|
|
def test_empty_dict(self):
|
|
assert parse_stickers_from_config({}) == []
|
|
|
|
def test_stickers_list(self):
|
|
cfg = {"stickers": [{"type": "image"}, {"type": "text"}]}
|
|
result = parse_stickers_from_config(cfg)
|
|
assert len(result) == 2
|
|
|
|
def test_stickers_not_list(self):
|
|
cfg = {"stickers": "not_a_list"}
|
|
assert parse_stickers_from_config(cfg) == []
|
|
|
|
def test_empty_stickers_list(self):
|
|
cfg = {"stickers": []}
|
|
assert parse_stickers_from_config(cfg) == []
|
|
|
|
|
|
# ── get_sticker_categories 测试 ─────────────────────────────────────────────
|
|
|
|
|
|
class TestGetStickerCategories:
|
|
def test_returns_list(self):
|
|
result = get_sticker_categories()
|
|
assert isinstance(result, list)
|
|
assert len(result) > 0
|
|
|
|
def test_returns_copy(self):
|
|
a = get_sticker_categories()
|
|
b = get_sticker_categories()
|
|
assert a is not b
|
|
assert a == b
|