"""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