Skip to content

Commit 4f85e86

Browse files
authored
test: add unit tests for fletx/core/routing/router
test: add unit tests for fletx/core/routing/router
2 parents 4dc51c3 + a7016df commit 4f85e86

1 file changed

Lines changed: 392 additions & 0 deletions

File tree

tests/test_routing_router.py

Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
1+
"""
2+
Unit tests for FletX Router System
3+
"""
4+
5+
import pytest
6+
import flet as ft
7+
from unittest.mock import Mock, AsyncMock, patch, MagicMock
8+
from typing import Dict, Any
9+
10+
from fletx.core.routing.router import FletXRouter
11+
from fletx.core.routing.models import (
12+
RouteInfo, NavigationIntent, RouterState,
13+
NavigationMode, NavigationResult
14+
)
15+
from fletx.core.routing.config import RouterConfig, RouteDefinition
16+
from fletx.core.routing.guards import RouteGuard
17+
from fletx.core.routing.middleware import RouteMiddleware
18+
from fletx.core.routing.transitions import RouteTransition, TransitionType
19+
from fletx.core.page import FletXPage
20+
21+
22+
class MockPage(Mock):
23+
"""Mock Flet Page for testing."""
24+
25+
def __init__(self):
26+
super().__init__(spec=ft.Page)
27+
self.route = "/"
28+
self.controls = []
29+
self.views = []
30+
self.on_route_change = None
31+
self.on_view_pop = None
32+
33+
def update(self):
34+
pass
35+
36+
def clean(self):
37+
self.controls = []
38+
39+
def add(self, *controls):
40+
self.controls.extend(controls)
41+
42+
43+
class MockComponent(FletXPage):
44+
"""Mock component for testing."""
45+
46+
def __init__(self):
47+
super().__init__()
48+
self.route_info = None
49+
self.did_mount_called = False
50+
51+
def did_mount(self):
52+
self.did_mount_called = True
53+
54+
def _build_page(self):
55+
pass
56+
57+
58+
class TestFletXRouterInitialization:
59+
"""Test FletXRouter initialization."""
60+
61+
def test_router_creation(self):
62+
"""Test basic router creation."""
63+
page = MockPage()
64+
config = RouterConfig()
65+
66+
router = FletXRouter(page, config)
67+
68+
assert router.page == page
69+
assert router.config == config
70+
assert isinstance(router.state, RouterState)
71+
assert router.state.navigation_mode == NavigationMode.HYBRID
72+
73+
def test_flet_integration_setup(self):
74+
"""Test Flet integration is set up correctly."""
75+
page = MockPage()
76+
router = FletXRouter(page)
77+
78+
assert page.on_route_change is not None
79+
assert page.on_view_pop is not None
80+
81+
@patch('fletx.core.routing.router.get_event_loop')
82+
def test_router_initialize_singleton(self, mock_loop):
83+
"""Test router singleton initialization."""
84+
mock_loop.return_value.create_task = Mock()
85+
page = MockPage()
86+
87+
router = FletXRouter.initialize(page, initial_route="/home")
88+
89+
assert FletXRouter._instance == router
90+
assert FletXRouter.get_instance() == router
91+
92+
def test_get_instance_before_init_raises_error(self):
93+
"""Test getting instance before initialization raises error."""
94+
FletXRouter._instance = None
95+
96+
with pytest.raises(RuntimeError, match="Router not initialized"):
97+
FletXRouter.get_instance()
98+
99+
100+
class TestRouterNavigation:
101+
"""Test router navigation functionality."""
102+
103+
def setup_method(self):
104+
"""Set up test fixtures."""
105+
self.page = MockPage()
106+
self.config = RouterConfig()
107+
self.router = FletXRouter(self.page, self.config)
108+
109+
# Add test route
110+
self.config.add_route("/test", MockComponent)
111+
112+
@pytest.mark.asyncio
113+
async def test_navigate_to_existing_route(self):
114+
"""Test navigation to an existing route."""
115+
result = await self.router.navigate("/test")
116+
117+
assert result == NavigationResult.SUCCESS
118+
assert self.router.state.current_route.path == "/test"
119+
120+
@pytest.mark.asyncio
121+
async def test_navigate_with_data(self):
122+
"""Test navigation with data."""
123+
data = {"user_id": 123}
124+
result = await self.router.navigate("/test", data=data)
125+
126+
assert result == NavigationResult.SUCCESS
127+
assert self.router.state.current_route.data == data
128+
129+
@pytest.mark.asyncio
130+
async def test_navigate_to_nonexistent_route(self):
131+
"""Test navigation to non-existent route."""
132+
result = await self.router.navigate("/nonexistent")
133+
134+
assert result == NavigationResult.ERROR
135+
136+
@pytest.mark.asyncio
137+
async def test_navigate_with_query_params(self):
138+
"""Test navigation with query parameters."""
139+
result = await self.router.navigate("/test?id=1&tab=main")
140+
141+
assert result == NavigationResult.SUCCESS
142+
assert self.router.state.current_route.query["id"] == "1"
143+
assert self.router.state.current_route.query["tab"] == "main"
144+
145+
@pytest.mark.asyncio
146+
async def test_navigate_with_replace(self):
147+
"""Test navigation with replace flag."""
148+
await self.router.navigate("/test")
149+
history_len = len(self.router.state.history)
150+
151+
await self.router.navigate("/test", replace=True)
152+
153+
assert len(self.router.state.history) == history_len
154+
155+
@pytest.mark.asyncio
156+
async def test_navigate_with_clear_history(self):
157+
"""Test navigation with clear history flag."""
158+
await self.router.navigate("/test")
159+
self.router.state.history.append(RouteInfo(path="/old"))
160+
161+
await self.router.navigate("/test", clear_history=True)
162+
163+
assert len(self.router.state.history) == 0
164+
165+
166+
class TestNavigationIntent:
167+
"""Test navigation with intents."""
168+
169+
def setup_method(self):
170+
"""Set up test fixtures."""
171+
self.page = MockPage()
172+
self.config = RouterConfig()
173+
self.router = FletXRouter(self.page, self.config)
174+
self.config.add_route("/test", MockComponent)
175+
176+
@pytest.mark.asyncio
177+
async def test_navigate_with_intent(self):
178+
"""Test navigation using NavigationIntent."""
179+
intent = NavigationIntent(
180+
route="/test",
181+
data={"key": "value"},
182+
replace=True
183+
)
184+
185+
result = await self.router.navigate_with_intent(intent)
186+
187+
assert result == NavigationResult.SUCCESS
188+
assert self.router.state.current_route.path == "/test"
189+
190+
191+
class TestHistoryManagement:
192+
"""Test navigation history management."""
193+
194+
def setup_method(self):
195+
"""Set up test fixtures."""
196+
self.page = MockPage()
197+
self.config = RouterConfig()
198+
self.router = FletXRouter(self.page, self.config)
199+
200+
self.config.add_route("/home", MockComponent)
201+
self.config.add_route("/about", MockComponent)
202+
203+
@pytest.mark.asyncio
204+
async def test_history_tracking(self):
205+
"""Test that navigation history is tracked."""
206+
await self.router.navigate("/home")
207+
await self.router.navigate("/about")
208+
209+
assert len(self.router.state.history) == 1
210+
assert self.router.state.history[0].path == "/home"
211+
212+
def test_can_go_back(self):
213+
"""Test can_go_back detection."""
214+
assert self.router.can_go_back() is False
215+
216+
self.router.state.history.append(RouteInfo(path="/home"))
217+
assert self.router.can_go_back() is True
218+
219+
def test_can_go_forward(self):
220+
"""Test can_go_forward detection."""
221+
assert self.router.can_go_forward() is False
222+
223+
self.router.state.forward_stack.append(RouteInfo(path="/next"))
224+
assert self.router.can_go_forward() is True
225+
226+
@patch('fletx.core.routing.router.run_async')
227+
def test_go_back(self, mock_run_async):
228+
"""Test go_back functionality."""
229+
self.router.state.history.append(RouteInfo(path="/previous"))
230+
231+
result = self.router.go_back()
232+
233+
assert result is True
234+
assert len(self.router.state.forward_stack) == 1
235+
236+
@patch('fletx.core.routing.router.run_async')
237+
def test_go_forward(self, mock_run_async):
238+
"""Test go_forward functionality."""
239+
self.router.state.forward_stack.append(RouteInfo(path="/next"))
240+
241+
result = self.router.go_forward()
242+
243+
assert result is True
244+
assert len(self.router.state.history) == 1
245+
246+
def test_get_history(self):
247+
"""Test getting navigation history."""
248+
self.router.state.history.append(RouteInfo(path="/home"))
249+
self.router.state.history.append(RouteInfo(path="/about"))
250+
251+
history = self.router.get_history()
252+
253+
assert len(history) == 2
254+
assert history[0].path == "/home"
255+
256+
257+
class TestGuardsAndMiddleware:
258+
"""Test route guards and middleware."""
259+
260+
def setup_method(self):
261+
"""Set up test fixtures."""
262+
self.page = MockPage()
263+
self.config = RouterConfig()
264+
self.router = FletXRouter(self.page, self.config)
265+
266+
@pytest.mark.asyncio
267+
async def test_add_global_guard(self):
268+
"""Test adding global guard."""
269+
guard = Mock(spec=RouteGuard)
270+
guard.can_activate = AsyncMock(return_value=True)
271+
guard.can_deactivate = AsyncMock(return_value=True)
272+
273+
self.router.add_global_guard(guard)
274+
275+
assert guard in self.router._global_guards
276+
277+
@pytest.mark.asyncio
278+
async def test_add_global_middleware(self):
279+
"""Test adding global middleware."""
280+
middleware = Mock(spec=RouteMiddleware)
281+
middleware.before_navigation = AsyncMock(return_value=None)
282+
283+
self.router.add_global_middleware(middleware)
284+
285+
assert middleware in self.router._global_middleware
286+
287+
@pytest.mark.asyncio
288+
async def test_guard_blocks_navigation(self):
289+
"""Test that guard can block navigation."""
290+
self.config.add_route("/protected", MockComponent)
291+
292+
guard = Mock(spec=RouteGuard)
293+
guard.can_activate = AsyncMock(return_value=False)
294+
guard.redirect_to = AsyncMock(return_value=None)
295+
guard.can_deactivate = AsyncMock(return_value=True)
296+
297+
self.router.add_global_guard(guard)
298+
299+
result = await self.router.navigate("/protected")
300+
301+
assert result == NavigationResult.BLOCKED_BY_GUARD
302+
303+
304+
class TestNavigationModes:
305+
"""Test different navigation modes."""
306+
307+
def setup_method(self):
308+
"""Set up test fixtures."""
309+
self.page = MockPage()
310+
self.config = RouterConfig()
311+
self.router = FletXRouter(self.page, self.config)
312+
self.config.add_route("/test", MockComponent)
313+
314+
def test_set_navigation_mode(self):
315+
"""Test setting navigation mode."""
316+
self.router.set_navigation_mode(NavigationMode.VIEWS)
317+
318+
assert self.router.state.navigation_mode == NavigationMode.VIEWS
319+
320+
@pytest.mark.asyncio
321+
async def test_views_mode_creates_views(self):
322+
"""Test that VIEWS mode creates Flet views."""
323+
self.router.set_navigation_mode(NavigationMode.VIEWS)
324+
325+
await self.router.navigate("/test")
326+
327+
assert len(self.page.views) > 0
328+
assert len(self.router.state.active_views) > 0
329+
330+
331+
class TestRouterUtilities:
332+
"""Test router utility methods."""
333+
334+
def setup_method(self):
335+
"""Set up test fixtures."""
336+
self.page = MockPage()
337+
self.config = RouterConfig()
338+
self.router = FletXRouter(self.page, self.config)
339+
340+
def test_get_current_route(self):
341+
"""Test getting current route."""
342+
self.router.state.current_route = RouteInfo(path="/test")
343+
344+
current = self.router.get_current_route()
345+
346+
assert current.path == "/test"
347+
348+
@pytest.mark.asyncio
349+
async def test_component_lifecycle_called(self):
350+
"""Test that component lifecycle methods are called."""
351+
component = MockComponent()
352+
route_info = RouteInfo(path="/test")
353+
354+
route_def = RouteDefinition(path="/test", component=lambda ri: component)
355+
356+
await self.router._apply_transition_and_update(
357+
component, route_info, None
358+
)
359+
360+
assert component.did_mount_called is True
361+
362+
363+
class TestFletIntegration:
364+
"""Test Flet native integration."""
365+
366+
def setup_method(self):
367+
"""Set up test fixtures."""
368+
self.page = MockPage()
369+
self.config = RouterConfig()
370+
self.router = FletXRouter(self.page, self.config)
371+
self.config.add_route("/test", MockComponent)
372+
373+
@pytest.mark.asyncio
374+
async def test_flet_route_sync(self):
375+
"""Test that Flet's page.route is synced."""
376+
await self.router.navigate("/test")
377+
378+
# In HYBRID or NATIVE mode, page.route should be updated
379+
if self.router.state.navigation_mode in [NavigationMode.HYBRID, NavigationMode.NATIVE]:
380+
assert self.page.route == "/test"
381+
382+
@patch('fletx.core.routing.router.get_event_loop')
383+
def test_flet_route_change_handler(self, mock_loop):
384+
"""Test Flet route change handler."""
385+
mock_loop.return_value.create_task = Mock()
386+
387+
event = Mock()
388+
event.route = "/new-route"
389+
390+
self.router._on_flet_route_change(event)
391+
392+
mock_loop.return_value.create_task.assert_called_once()

0 commit comments

Comments
 (0)