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