"""transition_config 模块单测 — 纯逻辑,无 FFmpeg 依赖.""" from __future__ import annotations import pytest from packages.domain.transition_config import ( CUT_TRANSITION, DEFAULT_TRANSITION_DURATION, MAX_TRANSITION_DURATION, MIN_TRANSITION_DURATION, TransitionConfig, TransitionType, ) # ── 常量 ────────────────────────────────────────────────────────────────────── class TestConstants: def test_duration_bounds(self): assert MIN_TRANSITION_DURATION == 0.3 assert MAX_TRANSITION_DURATION == 2.0 assert DEFAULT_TRANSITION_DURATION == 0.5 assert MIN_TRANSITION_DURATION < DEFAULT_TRANSITION_DURATION < MAX_TRANSITION_DURATION def test_cut_transition(self): assert CUT_TRANSITION == "cut" # ── TransitionType 枚举 ────────────────────────────────────────────────────── class TestTransitionType: def test_all_supported_includes_all_except_cut(self): supported = TransitionType.all_supported() assert "cut" not in supported assert "fade" in supported assert "dissolve" in supported assert len(supported) >= 10 # 至少有10种转场 def test_all_supported_unique(self): supported = TransitionType.all_supported() assert len(supported) == len(set(supported)) def test_is_supported_exact_match(self): assert TransitionType.is_supported("fade") is True assert TransitionType.is_supported("dissolve") is True assert TransitionType.is_supported("slideleft") is True def test_is_supported_case_insensitive(self): assert TransitionType.is_supported("FADE") is True assert TransitionType.is_supported("Fade") is True assert TransitionType.is_supported("SlideLeft") is True def test_is_supported_with_underscores(self): assert TransitionType.is_supported("slide_left") is True assert TransitionType.is_supported("wipe_right") is True assert TransitionType.is_supported("circle_crop") is True def test_is_supported_with_hyphens(self): assert TransitionType.is_supported("slide-left") is True assert TransitionType.is_supported("wipe-down") is True def test_is_supported_aliases(self): assert TransitionType.is_supported("crossfade") is True assert TransitionType.is_supported("crossdissolve") is True assert TransitionType.is_supported("fadein") is True assert TransitionType.is_supported("fadeout") is True assert TransitionType.is_supported("slide") is True assert TransitionType.is_supported("wipe") is True assert TransitionType.is_supported("zoomin") is True assert TransitionType.is_supported("zoomout") is True assert TransitionType.is_supported("circle") is True assert TransitionType.is_supported("rect") is True def test_is_supported_unknown(self): assert TransitionType.is_supported("unknown_effect") is False assert TransitionType.is_supported("") is False assert TransitionType.is_supported("12345") is False def test_enum_values_match_ffmpeg(self): # 枚举值应该就是 ffmpeg xfade 的 transition 名 assert TransitionType.FADE.value == "fade" assert TransitionType.DISSOLVE.value == "dissolve" assert TransitionType.SLIDE_LEFT.value == "slideleft" assert TransitionType.CUT.value == "cut" # ── TransitionConfig 默认值 ────────────────────────────────────────────────── class TestTransitionConfigDefaults: def test_default_config(self): cfg = TransitionConfig() assert cfg.effect == CUT_TRANSITION assert cfg.duration == DEFAULT_TRANSITION_DURATION assert cfg.is_cut is True def test_is_cut_true(self): cfg = TransitionConfig(effect="cut") assert cfg.is_cut is True def test_is_cut_false(self): cfg = TransitionConfig(effect="fade") assert cfg.is_cut is False # ── TransitionConfig.parse ─────────────────────────────────────────────────── class TestTransitionConfigParse: def test_none_params_default(self): cfg = TransitionConfig.parse() assert cfg.effect == CUT_TRANSITION assert cfg.duration == DEFAULT_TRANSITION_DURATION def test_empty_effect_default(self): cfg = TransitionConfig.parse(effect="") assert cfg.effect == CUT_TRANSITION def test_whitespace_effect_default(self): cfg = TransitionConfig.parse(effect=" ") assert cfg.effect == CUT_TRANSITION def test_valid_effect_fade(self): cfg = TransitionConfig.parse(effect="fade") assert cfg.effect == "fade" assert cfg.is_cut is False def test_valid_effect_case_insensitive(self): cfg = TransitionConfig.parse(effect="FADE") assert cfg.effect == "fade" def test_valid_effect_with_underscores(self): cfg = TransitionConfig.parse(effect="slide_left") assert cfg.effect == "slideleft" def test_alias_effect(self): cfg = TransitionConfig.parse(effect="crossfade") assert cfg.effect == "dissolve" # 别名映射到 dissolve def test_unknown_effect_falls_back_to_cut(self): cfg = TransitionConfig.parse(effect="magic_sparkles") assert cfg.effect == CUT_TRANSITION assert cfg.is_cut is True def test_cut_effect_stays_cut(self): cfg = TransitionConfig.parse(effect="cut") assert cfg.effect == CUT_TRANSITION def test_cut_effect_case_insensitive(self): cfg = TransitionConfig.parse(effect="CUT") assert cfg.effect == CUT_TRANSITION def test_duration_default(self): cfg = TransitionConfig.parse(duration=None) assert cfg.duration == DEFAULT_TRANSITION_DURATION def test_duration_within_range(self): cfg = TransitionConfig.parse(duration=1.0) assert cfg.duration == 1.0 def test_duration_at_min(self): cfg = TransitionConfig.parse(duration=MIN_TRANSITION_DURATION) assert cfg.duration == MIN_TRANSITION_DURATION def test_duration_at_max(self): cfg = TransitionConfig.parse(duration=MAX_TRANSITION_DURATION) assert cfg.duration == MAX_TRANSITION_DURATION def test_duration_below_min_clamped(self): cfg = TransitionConfig.parse(duration=0.1) assert cfg.duration == MIN_TRANSITION_DURATION def test_duration_above_max_clamped(self): cfg = TransitionConfig.parse(duration=3.0) assert cfg.duration == MAX_TRANSITION_DURATION def test_duration_zero_clamped(self): cfg = TransitionConfig.parse(duration=0) assert cfg.duration == MIN_TRANSITION_DURATION def test_duration_negative_clamped(self): cfg = TransitionConfig.parse(duration=-1.0) assert cfg.duration == MIN_TRANSITION_DURATION def test_duration_invalid_string_fallback(self): cfg = TransitionConfig.parse(duration="bad") # type: ignore[arg-type] assert cfg.duration == DEFAULT_TRANSITION_DURATION def test_duration_numeric_string(self): cfg = TransitionConfig.parse(duration="1.5") # type: ignore[arg-type] assert cfg.duration == 1.5 def test_full_parse(self): cfg = TransitionConfig.parse(effect="wipe_up", duration=1.2) assert cfg.effect == "wipeup" assert cfg.duration == 1.2 assert cfg.is_cut is False # ── TransitionConfig.ffmpeg_transition ─────────────────────────────────────── class TestFfmpegTransition: def test_cut_returns_empty(self): cfg = TransitionConfig(effect="cut") assert cfg.ffmpeg_transition == "" def test_fade_matches(self): cfg = TransitionConfig(effect="fade") assert cfg.ffmpeg_transition == "fade" def test_dissolve_matches(self): cfg = TransitionConfig(effect="dissolve") assert cfg.ffmpeg_transition == "dissolve" def test_slide_left_matches(self): cfg = TransitionConfig(effect="slideleft") assert cfg.ffmpeg_transition == "slideleft" def test_wipe_down_matches(self): cfg = TransitionConfig(effect="wipedown") assert cfg.ffmpeg_transition == "wipedown" def test_zoom_matches_zoomin(self): cfg = TransitionConfig(effect="zoom") assert cfg.ffmpeg_transition == "zoomin" def test_circle_crop_matches(self): cfg = TransitionConfig(effect="circlecrop") assert cfg.ffmpeg_transition == "circlecrop" # ── TransitionConfig.validate ──────────────────────────────────────────────── class TestTransitionConfigValidate: def test_valid_cut(self): cfg = TransitionConfig(effect="cut", duration=0.5) ok, err = cfg.validate() assert ok is True assert err == "" def test_valid_fade(self): cfg = TransitionConfig(effect="fade", duration=1.0) ok, err = cfg.validate() assert ok is True def test_duration_below_min_invalid(self): cfg = TransitionConfig(effect="fade", duration=0.1) ok, err = cfg.validate() assert ok is False assert "duration" in err def test_duration_above_max_invalid(self): cfg = TransitionConfig(effect="fade", duration=3.0) ok, err = cfg.validate() assert ok is False assert "duration" in err def test_unsupported_effect_invalid(self): cfg = TransitionConfig(effect="unknown", duration=0.5) ok, err = cfg.validate() assert ok is False assert "不支持的转场" in err def test_min_duration_boundary_valid(self): cfg = TransitionConfig(effect="fade", duration=MIN_TRANSITION_DURATION) ok, _ = cfg.validate() assert ok is True def test_max_duration_boundary_valid(self): cfg = TransitionConfig(effect="fade", duration=MAX_TRANSITION_DURATION) ok, _ = cfg.validate() assert ok is True