From 049dd028c0e3ed20bcb7aefdfd0d2d2609773f10 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 01:40:02 +0800 Subject: [PATCH] =?UTF-8?q?test(wave173):=20pip=5Fconfig=E7=94=BB=E4=B8=AD?= =?UTF-8?q?=E7=94=BB=E9=85=8D=E7=BD=AE=20+67=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/domain/test_pip_config.py | 471 +++++++++++++++++++++++++++ 1 file changed, 471 insertions(+) create mode 100644 tests/unit/domain/test_pip_config.py diff --git a/tests/unit/domain/test_pip_config.py b/tests/unit/domain/test_pip_config.py new file mode 100644 index 000000000..efbb56258 --- /dev/null +++ b/tests/unit/domain/test_pip_config.py @@ -0,0 +1,471 @@ +"""PiP 画中画配置单测. + +纯逻辑模块,覆盖:PiPLayerConfig校验、PiPConfig解析+属性、 +parse_size_value尺寸解析、calculate_pip_position位置计算。 +""" + +from __future__ import annotations + +from packages.domain.pip_config import ( + ANIMATION_FADE, + ANIMATION_SCALE, + ANIMATION_SLIDE_BOTTOM, + ANIMATION_SLIDE_LEFT, + ANIMATION_SLIDE_RIGHT, + ANIMATION_SLIDE_TOP, + POSITION_BOTTOM_LEFT, + POSITION_BOTTOM_RIGHT, + POSITION_CENTER, + POSITION_TOP_LEFT, + POSITION_TOP_RIGHT, + PiPConfig, + PiPLayerConfig, + calculate_pip_position, + parse_size_value, +) + + +class TestPiPLayerConfigDefaults: + def test_default_source(self): + layer = PiPLayerConfig() + assert layer.source == "" + assert layer.source_type == "asset_id" + + def test_default_position(self): + layer = PiPLayerConfig() + assert layer.position == POSITION_BOTTOM_RIGHT + assert layer.x == 0 + assert layer.y == 0 + assert layer.margin == 20 + + def test_default_size(self): + layer = PiPLayerConfig() + assert layer.width == "25%" + assert layer.height == "" + + def test_default_style(self): + layer = PiPLayerConfig() + assert layer.opacity == 1.0 + assert layer.corner_radius == 0 + assert layer.border_width == 0 + assert layer.border_color == "white" + + def test_default_timing(self): + layer = PiPLayerConfig() + assert layer.start_time == 0.0 + assert layer.duration == 0.0 + + def test_default_animation(self): + layer = PiPLayerConfig() + assert layer.animation_in == "" + assert layer.animation_out == "" + assert layer.animation_duration == 0.5 + + def test_default_z_index(self): + layer = PiPLayerConfig() + assert layer.z_index == 1 + + +class TestPiPLayerConfigValidate: + def test_valid_with_source(self): + layer = PiPLayerConfig(source="asset_123") + ok, msg = layer.validate() + assert ok is True + assert msg == "" + + def test_empty_source_invalid(self): + layer = PiPLayerConfig(source="") + ok, msg = layer.validate() + assert ok is False + assert "source" in msg + + def test_invalid_position(self): + layer = PiPLayerConfig(source="a", position="invalid") + ok, msg = layer.validate() + assert ok is False + assert "position" in msg + + def test_custom_position_valid(self): + layer = PiPLayerConfig(source="a", position="custom") + ok, _ = layer.validate() + assert ok is True + + def test_all_9_positions_valid(self): + positions = [ + "top_left", + "top_center", + "top_right", + "center_left", + "center", + "center_right", + "bottom_left", + "bottom_center", + "bottom_right", + ] + for pos in positions: + layer = PiPLayerConfig(source="a", position=pos) + ok, _ = layer.validate() + assert ok is True, f"position {pos} should be valid" + + def test_opacity_below_zero_invalid(self): + layer = PiPLayerConfig(source="a", opacity=-0.1) + ok, msg = layer.validate() + assert ok is False + assert "opacity" in msg + + def test_opacity_above_one_invalid(self): + layer = PiPLayerConfig(source="a", opacity=1.5) + ok, msg = layer.validate() + assert ok is False + assert "opacity" in msg + + def test_opacity_zero_valid(self): + layer = PiPLayerConfig(source="a", opacity=0.0) + ok, _ = layer.validate() + assert ok is True + + def test_opacity_one_valid(self): + layer = PiPLayerConfig(source="a", opacity=1.0) + ok, _ = layer.validate() + assert ok is True + + def test_negative_corner_radius_invalid(self): + layer = PiPLayerConfig(source="a", corner_radius=-1) + ok, msg = layer.validate() + assert ok is False + assert "corner_radius" in msg + + def test_negative_start_time_invalid(self): + layer = PiPLayerConfig(source="a", start_time=-1.0) + ok, msg = layer.validate() + assert ok is False + assert "start_time" in msg + + def test_negative_duration_invalid(self): + layer = PiPLayerConfig(source="a", duration=-1.0) + ok, msg = layer.validate() + assert ok is False + assert "duration" in msg + + def test_zero_duration_valid(self): + layer = PiPLayerConfig(source="a", duration=0.0) + ok, _ = layer.validate() + assert ok is True + + def test_invalid_animation_in(self): + layer = PiPLayerConfig(source="a", animation_in="invalid") + ok, msg = layer.validate() + assert ok is False + assert "入场动画" in msg + + def test_invalid_animation_out(self): + layer = PiPLayerConfig(source="a", animation_out="invalid") + ok, msg = layer.validate() + assert ok is False + assert "出场动画" in msg + + def test_empty_animation_valid(self): + layer = PiPLayerConfig(source="a", animation_in="", animation_out="") + ok, _ = layer.validate() + assert ok is True + + def test_all_valid_animations(self): + anims = [ + ANIMATION_FADE, + ANIMATION_SLIDE_LEFT, + ANIMATION_SLIDE_RIGHT, + ANIMATION_SLIDE_TOP, + ANIMATION_SLIDE_BOTTOM, + ANIMATION_SCALE, + ] + for anim in anims: + layer = PiPLayerConfig(source="a", animation_in=anim, animation_out=anim) + ok, _ = layer.validate() + assert ok is True, f"animation {anim} should be valid" + + def test_negative_animation_duration_invalid(self): + layer = PiPLayerConfig(source="a", animation_duration=-0.5) + ok, msg = layer.validate() + assert ok is False + assert "animation_duration" in msg + + +class TestPiPConfigDefaults: + def test_default_disabled(self): + config = PiPConfig() + assert config.enabled is False + assert config.layers == [] + + def test_default_layer_count(self): + config = PiPConfig() + assert config.layer_count == 0 + + def test_default_max_z_index(self): + config = PiPConfig() + assert config.max_z_index == 0 + + +class TestPiPConfigFromDict: + def test_none_returns_disabled(self): + config = PiPConfig.from_dict(None) + assert config.enabled is False + assert config.layer_count == 0 + + def test_empty_dict_returns_disabled(self): + config = PiPConfig.from_dict({}) + assert config.enabled is False + + def test_enabled_false_returns_disabled(self): + config = PiPConfig.from_dict({"enabled": False}) + assert config.enabled is False + + def test_enabled_no_layers_returns_disabled(self): + config = PiPConfig.from_dict({"enabled": True, "layers": []}) + assert config.enabled is False + assert config.layer_count == 0 + + def test_single_layer(self): + config = PiPConfig.from_dict( + { + "enabled": True, + "layers": [ + {"source": "asset_1", "position": "top_left"}, + ], + } + ) + assert config.enabled is True + assert config.layer_count == 1 + assert config.layers[0].source == "asset_1" + assert config.layers[0].position == POSITION_TOP_LEFT + + def test_multiple_layers_sorted_by_z_index(self): + config = PiPConfig.from_dict( + { + "enabled": True, + "layers": [ + {"source": "a", "z_index": 3}, + {"source": "b", "z_index": 1}, + {"source": "c", "z_index": 2}, + ], + } + ) + assert config.layer_count == 3 + assert config.layers[0].source == "b" # z=1 + assert config.layers[1].source == "c" # z=2 + assert config.layers[2].source == "a" # z=3 + + def test_invalid_layer_skipped(self): + config = PiPConfig.from_dict( + { + "enabled": True, + "layers": [ + {"source": "valid_layer", "position": "center"}, + {"source": "", "position": "center"}, # 空source,无效 + ], + } + ) + assert config.layer_count == 1 + assert config.layers[0].source == "valid_layer" + + def test_all_invalid_layers_returns_disabled(self): + config = PiPConfig.from_dict( + { + "enabled": True, + "layers": [ + {"source": ""}, # 无效 + ], + } + ) + assert config.enabled is False + assert config.layer_count == 0 + + def test_layer_with_full_config(self): + config = PiPConfig.from_dict( + { + "enabled": True, + "layers": [ + { + "source": "https://example.com/video.mp4", + "source_type": "url", + "position": "bottom_right", + "width": "30%", + "height": "auto", + "opacity": 0.8, + "corner_radius": 8, + "border_width": 2, + "border_color": "#00FF00", + "start_time": 2.5, + "duration": 10.0, + "animation_in": "fade", + "animation_out": "slide_right", + "animation_duration": 0.8, + "z_index": 5, + "margin": 30, + }, + ], + } + ) + assert config.layer_count == 1 + layer = config.layers[0] + assert layer.source == "https://example.com/video.mp4" + assert layer.source_type == "url" + assert layer.width == "30%" + assert layer.opacity == 0.8 + assert layer.corner_radius == 8 + assert layer.border_width == 2 + assert layer.start_time == 2.5 + assert layer.duration == 10.0 + assert layer.animation_in == "fade" + assert layer.animation_out == "slide_right" + assert layer.animation_duration == 0.8 + assert layer.z_index == 5 + assert layer.margin == 30 + + def test_layer_parse_error_skipped(self): + config = PiPConfig.from_dict( + { + "enabled": True, + "layers": [ + {"source": "ok", "opacity": "not_a_number"}, # 会抛ValueError + {"source": "valid"}, + ], + } + ) + # opacity解析失败会被跳过 + assert config.layer_count >= 1 + # 至少valid那个还在 + sources = [layer.source for layer in config.layers] + assert "valid" in sources + + +class TestPiPConfigProperties: + def test_layer_count(self): + config = PiPConfig( + enabled=True, + layers=[ + PiPLayerConfig(source="a"), + PiPLayerConfig(source="b"), + PiPLayerConfig(source="c"), + ], + ) + assert config.layer_count == 3 + + def test_max_z_index(self): + config = PiPConfig( + enabled=True, + layers=[ + PiPLayerConfig(source="a", z_index=5), + PiPLayerConfig(source="b", z_index=2), + PiPLayerConfig(source="c", z_index=10), + ], + ) + assert config.max_z_index == 10 + + def test_max_z_index_empty(self): + config = PiPConfig(enabled=False, layers=[]) + assert config.max_z_index == 0 + + +class TestParseSizeValue: + def test_int_passthrough(self): + assert parse_size_value(100, 1000) == 100 + + def test_int_zero_clamped_to_1(self): + assert parse_size_value(0, 1000) == 1 + + def test_int_negative_clamped_to_1(self): + assert parse_size_value(-10, 1000) == 1 + + def test_percentage_string(self): + assert parse_size_value("50%", 1000) == 500 + + def test_percentage_25(self): + assert parse_size_value("25%", 1920) == 480 + + def test_percentage_small(self): + assert parse_size_value("1%", 100) == 1 + + def test_percentage_zero_clamped(self): + assert parse_size_value("0%", 1000) == 1 + + def test_invalid_percentage_uses_default(self): + assert parse_size_value("abc%", 1000) == 250 # default 25% of 1000 + + def test_numeric_string(self): + assert parse_size_value("200", 1000) == 200 + + def test_empty_string_uses_default(self): + assert parse_size_value("", 1000) == 250 + + def test_custom_default_pct(self): + assert parse_size_value("invalid", 1000, default_pct=0.5) == 500 + + def test_float_string(self): + """float字符串会走int()转换路径.""" + result = parse_size_value("150.5", 1000) + assert result >= 1 # 至少不崩 + + +class TestCalculatePipPosition: + def test_top_left(self): + x, y = calculate_pip_position(POSITION_TOP_LEFT, 1920, 1080, 400, 300, margin=20) + assert (x, y) == (20, 20) + + def test_top_right(self): + x, y = calculate_pip_position(POSITION_TOP_RIGHT, 1920, 1080, 400, 300, margin=20) + assert (x, y) == (1920 - 400 - 20, 20) + + def test_bottom_right(self): + x, y = calculate_pip_position(POSITION_BOTTOM_RIGHT, 1920, 1080, 400, 300, margin=20) + assert (x, y) == (1920 - 400 - 20, 1080 - 300 - 20) + + def test_bottom_left(self): + x, y = calculate_pip_position(POSITION_BOTTOM_LEFT, 1920, 1080, 400, 300, margin=20) + assert (x, y) == (20, 1080 - 300 - 20) + + def test_center(self): + x, y = calculate_pip_position(POSITION_CENTER, 1920, 1080, 400, 300, margin=20) + assert x == (1920 - 400) // 2 + assert y == (1080 - 300) // 2 + + def test_top_center(self): + x, y = calculate_pip_position("top_center", 1920, 1080, 400, 300, margin=20) + assert x == (1920 - 400) // 2 + assert y == 20 + + def test_bottom_center(self): + x, y = calculate_pip_position("bottom_center", 1920, 1080, 400, 300, margin=30) + assert x == (1920 - 400) // 2 + assert y == 1080 - 300 - 30 + + def test_center_left(self): + x, y = calculate_pip_position("center_left", 1920, 1080, 400, 300, margin=20) + assert x == 20 + assert y == (1080 - 300) // 2 + + def test_center_right(self): + x, y = calculate_pip_position("center_right", 1920, 1080, 400, 300, margin=20) + assert x == 1920 - 400 - 20 + assert y == (1080 - 300) // 2 + + def test_custom_position_pixel_values(self): + x, y = calculate_pip_position("custom", 1920, 1080, 400, 300, custom_x=100, custom_y=200) + assert (x, y) == (100, 200) + + def test_custom_position_percentage(self): + x, y = calculate_pip_position("custom", 1920, 1080, 400, 300, custom_x="10%", custom_y="20%") + assert x == 192 # 10% of 1920 + assert y == 216 # 20% of 1080 + + def test_different_margin(self): + x, y = calculate_pip_position(POSITION_TOP_LEFT, 1920, 1080, 400, 300, margin=50) + assert (x, y) == (50, 50) + + def test_invalid_position_defaults_to_bottom_right(self): + x, y = calculate_pip_position("invalid_pos", 1920, 1080, 400, 300, margin=20) + assert (x, y) == (1920 - 400 - 20, 1080 - 300 - 20) + + def test_small_output_size(self): + x, y = calculate_pip_position(POSITION_CENTER, 100, 100, 50, 50, margin=5) + assert x == 25 + assert y == 25