v9.5: 實作標籤完全不重疊算法

- 新增 _calculate_lane_conflicts_v2() 分開返回標籤重疊和線穿框分數
- 修改泳道選擇算法,優先選擇無標籤重疊的泳道
- 兩階段搜尋:優先側別無可用泳道則嘗試另一側
- 增強日誌輸出,顯示標籤範圍和詳細衝突分數

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
beabigegg
2025-11-06 11:35:29 +08:00
commit 2d37d23bcf
83 changed files with 22971 additions and 0 deletions

255
tests/unit/test_renderer.py Normal file
View File

@@ -0,0 +1,255 @@
"""
時間軸渲染模組單元測試
對應 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"])