- 新增 _calculate_lane_conflicts_v2() 分開返回標籤重疊和線穿框分數 - 修改泳道選擇算法,優先選擇無標籤重疊的泳道 - 兩階段搜尋:優先側別無可用泳道則嘗試另一側 - 增強日誌輸出,顯示標籤範圍和詳細衝突分數 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
256 lines
7.7 KiB
Python
256 lines
7.7 KiB
Python
"""
|
|
時間軸渲染模組單元測試
|
|
|
|
對應 TDD.md:
|
|
- UT-REN-01: 時間刻度演算法
|
|
- UT-REN-02: 節點避碰演算法
|
|
|
|
Version: 1.0.0
|
|
DocID: TDD-UT-REN-001
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime, timedelta
|
|
from backend.schemas import Event, TimelineConfig, RenderResult, EventType
|
|
from backend.renderer import (
|
|
TimeScaleCalculator, CollisionResolver, ThemeManager,
|
|
TimelineRenderer, TimeUnit
|
|
)
|
|
|
|
|
|
class TestTimeScaleCalculator:
|
|
"""時間刻度演算法測試"""
|
|
|
|
def test_calculate_time_range(self):
|
|
"""測試時間範圍計算"""
|
|
events = [
|
|
Event(id="1", title="E1", start=datetime(2024, 1, 1)),
|
|
Event(id="2", title="E2", start=datetime(2024, 1, 10))
|
|
]
|
|
|
|
start, end = TimeScaleCalculator.calculate_time_range(events)
|
|
|
|
assert start < datetime(2024, 1, 1)
|
|
assert end > datetime(2024, 1, 10)
|
|
|
|
def test_determine_time_unit_days(self):
|
|
"""測試天級別刻度判斷"""
|
|
start = datetime(2024, 1, 1)
|
|
end = datetime(2024, 1, 7)
|
|
|
|
unit = TimeScaleCalculator.determine_time_unit(start, end)
|
|
|
|
assert unit == TimeUnit.DAY
|
|
|
|
def test_determine_time_unit_weeks(self):
|
|
"""測試週級別刻度判斷"""
|
|
start = datetime(2024, 1, 1)
|
|
end = datetime(2024, 3, 1) # 約 2 個月
|
|
|
|
unit = TimeScaleCalculator.determine_time_unit(start, end)
|
|
|
|
assert unit == TimeUnit.WEEK
|
|
|
|
def test_determine_time_unit_months(self):
|
|
"""測試月級別刻度判斷"""
|
|
start = datetime(2024, 1, 1)
|
|
end = datetime(2024, 6, 1) # 6 個月
|
|
|
|
unit = TimeScaleCalculator.determine_time_unit(start, end)
|
|
|
|
assert unit == TimeUnit.MONTH
|
|
|
|
def test_generate_tick_values_days(self):
|
|
"""測試天級別刻度生成"""
|
|
start = datetime(2024, 1, 1)
|
|
end = datetime(2024, 1, 5)
|
|
|
|
ticks = TimeScaleCalculator.generate_tick_values(start, end, TimeUnit.DAY)
|
|
|
|
assert len(ticks) >= 5
|
|
assert all(isinstance(t, datetime) for t in ticks)
|
|
|
|
def test_generate_tick_values_months(self):
|
|
"""測試月級別刻度生成"""
|
|
start = datetime(2024, 1, 1)
|
|
end = datetime(2024, 6, 1)
|
|
|
|
ticks = TimeScaleCalculator.generate_tick_values(start, end, TimeUnit.MONTH)
|
|
|
|
assert len(ticks) >= 6
|
|
# 驗證是每月第一天
|
|
assert all(t.day == 1 for t in ticks)
|
|
|
|
|
|
class TestCollisionResolver:
|
|
"""節點避碰演算法測試"""
|
|
|
|
def test_no_overlapping_events(self):
|
|
"""測試無重疊事件"""
|
|
events = [
|
|
Event(id="1", title="E1", start=datetime(2024, 1, 1), end=datetime(2024, 1, 2)),
|
|
Event(id="2", title="E2", start=datetime(2024, 1, 3), end=datetime(2024, 1, 4))
|
|
]
|
|
|
|
resolver = CollisionResolver()
|
|
layers = resolver.resolve_collisions(events)
|
|
|
|
# 無重疊,都在第 0 層
|
|
assert layers["1"] == 0
|
|
assert layers["2"] == 0
|
|
|
|
def test_overlapping_events(self):
|
|
"""測試重疊事件分層"""
|
|
events = [
|
|
Event(id="1", title="E1", start=datetime(2024, 1, 1), end=datetime(2024, 1, 5)),
|
|
Event(id="2", title="E2", start=datetime(2024, 1, 3), end=datetime(2024, 1, 7))
|
|
]
|
|
|
|
resolver = CollisionResolver()
|
|
layers = resolver.resolve_collisions(events)
|
|
|
|
# 重疊,應該在不同層
|
|
assert layers["1"] != layers["2"]
|
|
|
|
def test_group_based_layout(self):
|
|
"""測試基於群組的排版"""
|
|
events = [
|
|
Event(id="1", title="E1", start=datetime(2024, 1, 1), group="A"),
|
|
Event(id="2", title="E2", start=datetime(2024, 1, 1), group="B")
|
|
]
|
|
|
|
resolver = CollisionResolver()
|
|
layers = resolver.group_based_layout(events)
|
|
|
|
# 不同群組,應該在不同層
|
|
assert layers["1"] != layers["2"]
|
|
|
|
def test_empty_events(self):
|
|
"""測試空事件列表"""
|
|
resolver = CollisionResolver()
|
|
layers = resolver.resolve_collisions([])
|
|
|
|
assert layers == {}
|
|
|
|
|
|
class TestThemeManager:
|
|
"""主題管理器測試"""
|
|
|
|
def test_get_modern_theme(self):
|
|
"""測試現代主題"""
|
|
from backend.schemas import ThemeStyle
|
|
theme = ThemeManager.get_theme(ThemeStyle.MODERN)
|
|
|
|
assert 'background' in theme
|
|
assert 'text' in theme
|
|
assert 'primary' in theme
|
|
|
|
def test_get_all_themes(self):
|
|
"""測試所有主題可用性"""
|
|
from backend.schemas import ThemeStyle
|
|
|
|
for style in ThemeStyle:
|
|
theme = ThemeManager.get_theme(style)
|
|
assert theme is not None
|
|
assert 'background' in theme
|
|
|
|
|
|
class TestTimelineRenderer:
|
|
"""時間軸渲染器測試"""
|
|
|
|
def test_render_basic_timeline(self):
|
|
"""測試基本時間軸渲染"""
|
|
events = [
|
|
Event(id="1", title="Event 1", start=datetime(2024, 1, 1)),
|
|
Event(id="2", title="Event 2", start=datetime(2024, 1, 5))
|
|
]
|
|
config = TimelineConfig()
|
|
|
|
renderer = TimelineRenderer()
|
|
result = renderer.render(events, config)
|
|
|
|
assert result.success is True
|
|
assert 'data' in result.data
|
|
assert result.layout is not None
|
|
|
|
def test_render_empty_timeline(self):
|
|
"""測試空白時間軸渲染"""
|
|
renderer = TimelineRenderer()
|
|
result = renderer.render([], TimelineConfig())
|
|
|
|
assert result.success is True
|
|
assert 'data' in result.data
|
|
|
|
def test_render_with_horizontal_direction(self):
|
|
"""測試水平方向渲染"""
|
|
events = [Event(id="1", title="E1", start=datetime(2024, 1, 1))]
|
|
config = TimelineConfig(direction='horizontal')
|
|
|
|
renderer = TimelineRenderer()
|
|
result = renderer.render(events, config)
|
|
|
|
assert result.success is True
|
|
|
|
def test_render_with_vertical_direction(self):
|
|
"""測試垂直方向渲染"""
|
|
events = [Event(id="1", title="E1", start=datetime(2024, 1, 1))]
|
|
config = TimelineConfig(direction='vertical')
|
|
|
|
renderer = TimelineRenderer()
|
|
result = renderer.render(events, config)
|
|
|
|
assert result.success is True
|
|
|
|
def test_render_with_different_themes(self):
|
|
"""測試不同主題渲染"""
|
|
from backend.schemas import ThemeStyle
|
|
events = [Event(id="1", title="E1", start=datetime(2024, 1, 1))]
|
|
|
|
renderer = TimelineRenderer()
|
|
|
|
for theme in [ThemeStyle.MODERN, ThemeStyle.CLASSIC]:
|
|
config = TimelineConfig(theme=theme)
|
|
result = renderer.render(events, config)
|
|
assert result.success is True
|
|
|
|
def test_render_with_grid(self):
|
|
"""測試顯示網格"""
|
|
events = [Event(id="1", title="E1", start=datetime(2024, 1, 1))]
|
|
config = TimelineConfig(show_grid=True)
|
|
|
|
renderer = TimelineRenderer()
|
|
result = renderer.render(events, config)
|
|
|
|
assert result.success is True
|
|
|
|
def test_render_single_event(self):
|
|
"""測試單一事件渲染"""
|
|
events = [Event(id="1", title="Single", start=datetime(2024, 1, 1))]
|
|
config = TimelineConfig()
|
|
|
|
renderer = TimelineRenderer()
|
|
result = renderer.render(events, config)
|
|
|
|
assert result.success is True
|
|
assert len(result.data['data']) == 1
|
|
|
|
def test_hover_text_generation(self):
|
|
"""測試提示訊息生成"""
|
|
event = Event(
|
|
id="1",
|
|
title="Test Event",
|
|
start=datetime(2024, 1, 1),
|
|
end=datetime(2024, 1, 2),
|
|
description="Test description"
|
|
)
|
|
|
|
renderer = TimelineRenderer()
|
|
hover_text = renderer._generate_hover_text(event)
|
|
|
|
assert "Test Event" in hover_text
|
|
assert "Test description" in hover_text
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|