diff --git a/apps/web/src/components/layout/PageHead/PageHead.css b/apps/web/src/components/layout/PageHead/PageHead.css index dbbc3dbcf..9789a9183 100644 --- a/apps/web/src/components/layout/PageHead/PageHead.css +++ b/apps/web/src/components/layout/PageHead/PageHead.css @@ -159,4 +159,3 @@ flex-wrap: wrap; } } - diff --git a/tests/unit/test_template_clip_config.py b/tests/unit/test_template_clip_config.py new file mode 100755 index 000000000..d43dd315b --- /dev/null +++ b/tests/unit/test_template_clip_config.py @@ -0,0 +1,275 @@ +"""TemplateClipConfig domain entity unit tests (wave219).""" + +from datetime import datetime, timezone + +import pytest +from domain.template_clip_config import ( + ClipType, + TemplateClipConfig, + TransitionEffect, +) + +# --------------------------------------------------------------------------- +# ClipType enum +# --------------------------------------------------------------------------- + + +class TestClipType: + def test_all_members_exist(self): + assert ClipType.INTRO.value == "intro" + assert ClipType.MAIN.value == "main" + assert ClipType.TRANSITION.value == "transition" + assert ClipType.OUTRO.value == "outro" + assert ClipType.TITLE.value == "title" + assert ClipType.SUBTITLE.value == "subtitle" + + def test_is_str_enum(self): + # StrEnum members are instances of str + assert isinstance(ClipType.INTRO, str) + assert ClipType.INTRO == "intro" + + def test_from_string(self): + assert ClipType("intro") is ClipType.INTRO + assert ClipType("main") is ClipType.MAIN + + def test_invalid_value_raises(self): + with pytest.raises(ValueError): + ClipType("invalid") + + +# --------------------------------------------------------------------------- +# TransitionEffect enum +# --------------------------------------------------------------------------- + + +class TestTransitionEffect: + def test_all_members_exist(self): + assert TransitionEffect.CUT.value == "cut" + assert TransitionEffect.FADE.value == "fade" + assert TransitionEffect.SLIDE_LEFT.value == "slide_left" + assert TransitionEffect.SLIDE_RIGHT.value == "slide_right" + assert TransitionEffect.DISSOLVE.value == "dissolve" + assert TransitionEffect.WIPE.value == "wipe" + + def test_is_str_enum(self): + assert isinstance(TransitionEffect.CUT, str) + assert TransitionEffect.CUT == "cut" + + def test_from_string(self): + assert TransitionEffect("cut") is TransitionEffect.CUT + assert TransitionEffect("fade") is TransitionEffect.FADE + + def test_invalid_value_raises(self): + with pytest.raises(ValueError): + TransitionEffect("invalid_effect") + + +# --------------------------------------------------------------------------- +# TemplateClipConfig.create() +# --------------------------------------------------------------------------- + + +class TestTemplateClipConfigCreate: + def test_create_basic(self): + cfg = TemplateClipConfig.create( + template_id="tpl_001", + clip_type=ClipType.MAIN, + order=1, + ) + assert cfg.template_id == "tpl_001" + assert cfg.clip_type == ClipType.MAIN + assert cfg.order == 1 + assert cfg.id # auto-generated uuid hex + assert len(cfg.id) == 32 # uuid4 hex length + + def test_create_with_string_clip_type(self): + cfg = TemplateClipConfig.create( + template_id="tpl_001", + clip_type="intro", + order=0, + ) + assert cfg.clip_type == ClipType.INTRO + + def test_create_with_string_transition_effect(self): + cfg = TemplateClipConfig.create( + template_id="tpl_001", + clip_type=ClipType.MAIN, + order=1, + transition_effect="fade", + ) + assert cfg.transition_effect == TransitionEffect.FADE + + def test_create_empty_template_id_raises(self): + with pytest.raises(ValueError, match="template_id 不能为空"): + TemplateClipConfig.create( + template_id="", + clip_type=ClipType.MAIN, + order=1, + ) + + def test_create_whitespace_template_id_raises(self): + with pytest.raises(ValueError, match="template_id 不能为空"): + TemplateClipConfig.create( + template_id=" ", + clip_type=ClipType.MAIN, + order=1, + ) + + def test_create_template_id_stripped(self): + cfg = TemplateClipConfig.create( + template_id=" tpl_001 ", + clip_type=ClipType.MAIN, + order=1, + ) + assert cfg.template_id == "tpl_001" + + def test_create_negative_min_duration_raises(self): + with pytest.raises(ValueError, match="min_duration 不能为负数"): + TemplateClipConfig.create( + template_id="tpl_001", + clip_type=ClipType.MAIN, + order=1, + min_duration=-1.0, + ) + + def test_create_negative_max_duration_raises(self): + with pytest.raises(ValueError, match="max_duration 不能为负数"): + TemplateClipConfig.create( + template_id="tpl_001", + clip_type=ClipType.MAIN, + order=1, + max_duration=-5.0, + ) + + def test_create_min_greater_than_max_raises(self): + with pytest.raises(ValueError, match="min_duration 不能大于 max_duration"): + TemplateClipConfig.create( + template_id="tpl_001", + clip_type=ClipType.MAIN, + order=1, + min_duration=10.0, + max_duration=5.0, + ) + + def test_create_min_equals_max_ok(self): + cfg = TemplateClipConfig.create( + template_id="tpl_001", + clip_type=ClipType.MAIN, + order=1, + min_duration=5.0, + max_duration=5.0, + ) + assert cfg.min_duration == 5.0 + assert cfg.max_duration == 5.0 + + def test_create_default_values(self): + cfg = TemplateClipConfig.create( + template_id="tpl_001", + clip_type=ClipType.MAIN, + order=1, + ) + assert cfg.min_duration == 0.0 + assert cfg.max_duration == 0.0 + assert cfg.text_template == "" + assert cfg.material_requirements == {} + assert cfg.transition_effect == TransitionEffect.CUT + assert cfg.config == {} + + def test_create_material_requirements_none_becomes_empty_dict(self): + cfg = TemplateClipConfig.create( + template_id="tpl_001", + clip_type=ClipType.MAIN, + order=1, + material_requirements=None, + ) + assert cfg.material_requirements == {} + # should be a fresh dict, not shared + cfg.material_requirements["key"] = "val" + cfg2 = TemplateClipConfig.create( + template_id="tpl_002", + clip_type=ClipType.MAIN, + order=1, + ) + assert cfg2.material_requirements == {} + + def test_create_config_none_becomes_empty_dict(self): + cfg = TemplateClipConfig.create( + template_id="tpl_001", + clip_type=ClipType.MAIN, + order=1, + config=None, + ) + assert cfg.config == {} + + def test_create_text_template_stripped(self): + cfg = TemplateClipConfig.create( + template_id="tpl_001", + clip_type=ClipType.MAIN, + order=1, + text_template=" hello {{name}} ", + ) + assert cfg.text_template == "hello {{name}}" + + def test_create_generates_unique_ids(self): + cfg1 = TemplateClipConfig.create("tpl", ClipType.MAIN, 1) + cfg2 = TemplateClipConfig.create("tpl", ClipType.MAIN, 2) + assert cfg1.id != cfg2.id + + def test_create_timestamps_are_utc(self): + before = datetime.now(timezone.utc) + cfg = TemplateClipConfig.create("tpl", ClipType.MAIN, 1) + after = datetime.now(timezone.utc) + assert before <= cfg.created_at <= after + assert before <= cfg.updated_at <= after + assert cfg.created_at.tzinfo is timezone.utc + assert cfg.updated_at.tzinfo is timezone.utc + + +# --------------------------------------------------------------------------- +# has_duration_range property +# --------------------------------------------------------------------------- + + +class TestHasDurationRange: + def test_both_zero(self): + cfg = TemplateClipConfig.create("tpl", ClipType.MAIN, 1) + assert cfg.has_duration_range is False + + def test_only_min_positive(self): + cfg = TemplateClipConfig.create("tpl", ClipType.MAIN, 1, min_duration=2.0) + assert cfg.has_duration_range is True + + def test_only_max_positive(self): + cfg = TemplateClipConfig.create("tpl", ClipType.MAIN, 1, max_duration=5.0) + assert cfg.has_duration_range is True + + def test_both_positive(self): + cfg = TemplateClipConfig.create("tpl", ClipType.MAIN, 1, min_duration=2.0, max_duration=5.0) + assert cfg.has_duration_range is True + + +# --------------------------------------------------------------------------- +# default_duration property +# --------------------------------------------------------------------------- + + +class TestDefaultDuration: + def test_both_zero_returns_zero(self): + cfg = TemplateClipConfig.create("tpl", ClipType.MAIN, 1) + assert cfg.default_duration == 0.0 + + def test_only_min_returns_min(self): + cfg = TemplateClipConfig.create("tpl", ClipType.MAIN, 1, min_duration=3.0) + assert cfg.default_duration == 3.0 + + def test_only_max_returns_max(self): + cfg = TemplateClipConfig.create("tpl", ClipType.MAIN, 1, max_duration=5.0) + assert cfg.default_duration == 5.0 + + def test_both_set_returns_midpoint(self): + cfg = TemplateClipConfig.create("tpl", ClipType.MAIN, 1, min_duration=2.0, max_duration=6.0) + assert cfg.default_duration == 4.0 + + def test_min_equals_max_returns_same(self): + cfg = TemplateClipConfig.create("tpl", ClipType.MAIN, 1, min_duration=3.0, max_duration=3.0) + assert cfg.default_duration == 3.0