1+ """
2+ Tests that reproduce state persistence bugs during app restarts.
3+ These tests are expected to FAIL until we fix the underlying issues.
4+ """
5+
6+ import pytest
7+ import asyncio
8+ from unittest .mock import Mock , patch , MagicMock
9+ from nicegui import app , ui
10+
11+ from src .core .game_logic import (
12+ save_state_to_storage ,
13+ load_state_from_storage ,
14+ board , clicked_tiles , is_game_closed ,
15+ board_iteration , bingo_patterns , today_seed ,
16+ toggle_tile , generate_board
17+ )
18+ from src .utils .file_operations import read_phrases_file
19+
20+
21+ class TestStatePersistenceBugs :
22+ """Tests that demonstrate current bugs in state persistence."""
23+
24+ def setup_method (self ):
25+ """Set up test environment."""
26+ # Reset global state
27+ global board , clicked_tiles , is_game_closed , board_iteration
28+ board .clear ()
29+ clicked_tiles .clear ()
30+ is_game_closed = False
31+ board_iteration = 1
32+
33+ # Mock app.storage.general
34+ if not hasattr (app , 'storage' ):
35+ app .storage = Mock ()
36+ app .storage .general = {}
37+
38+ def test_state_not_restored_after_hot_reload (self ):
39+ """Test that state is lost when NiceGUI triggers a hot reload."""
40+ # Arrange: Set up game state
41+ phrases = read_phrases_file ()
42+ generate_board (1 , phrases )
43+ toggle_tile (0 , 0 )
44+ toggle_tile (1 , 1 )
45+ assert len (clicked_tiles ) == 2
46+
47+ # Save state
48+ assert save_state_to_storage () is True
49+ assert 'game_state' in app .storage .general
50+
51+ # Simulate hot reload by clearing globals (what actually happens)
52+ board .clear ()
53+ clicked_tiles .clear ()
54+
55+ # Act: Try to restore state
56+ restored = load_state_from_storage ()
57+
58+ # Assert: This currently FAILS because globals aren't properly restored
59+ assert restored is True
60+ assert len (clicked_tiles ) == 2 # This will fail!
61+ assert (0 , 0 ) in clicked_tiles
62+ assert (1 , 1 ) in clicked_tiles
63+
64+ def test_storage_persistence_across_app_restart (self ):
65+ """Test that storage actually persists when app fully restarts."""
66+ # Arrange: Save some state
67+ phrases = read_phrases_file ()
68+ generate_board (1 , phrases )
69+ toggle_tile (2 , 2 )
70+ save_state_to_storage ()
71+
72+ # Simulate app restart by creating new storage instance
73+ old_storage = app .storage .general .copy ()
74+ app .storage .general = {} # This simulates what happens on restart
75+
76+ # Act: Try to load state
77+ restored = load_state_from_storage ()
78+
79+ # Assert: This FAILS because storage is cleared on restart
80+ assert restored is True # This will fail!
81+ assert len (clicked_tiles ) == 1
82+
83+ @pytest .mark .asyncio
84+ async def test_concurrent_state_updates_cause_data_loss (self ):
85+ """Test that concurrent state updates don't cause data loss."""
86+ # Arrange: Initial state
87+ phrases = read_phrases_file ()
88+ generate_board (1 , phrases )
89+
90+ # Simulate concurrent updates from multiple users
91+ async def user1_clicks ():
92+ toggle_tile (0 , 0 )
93+ await asyncio .sleep (0.01 ) # Simulate network delay
94+ save_state_to_storage ()
95+
96+ async def user2_clicks ():
97+ toggle_tile (1 , 1 )
98+ await asyncio .sleep (0.01 ) # Simulate network delay
99+ save_state_to_storage ()
100+
101+ # Act: Run concurrent updates
102+ await asyncio .gather (user1_clicks (), user2_clicks ())
103+
104+ # Reload state
105+ clicked_tiles .clear ()
106+ load_state_from_storage ()
107+
108+ # Assert: Both clicks should be saved (this may fail due to race condition)
109+ assert len (clicked_tiles ) == 2
110+ assert (0 , 0 ) in clicked_tiles
111+ assert (1 , 1 ) in clicked_tiles
112+
113+ def test_state_corruption_on_partial_write (self ):
114+ """Test that partial writes don't corrupt state."""
115+ # Arrange: Set up state
116+ phrases = read_phrases_file ()
117+ generate_board (1 , phrases )
118+ toggle_tile (0 , 0 )
119+ save_state_to_storage ()
120+
121+ # Simulate partial write by corrupting storage
122+ app .storage .general ['game_state' ]['clicked_tiles' ] = "corrupted"
123+
124+ # Act: Try to load corrupted state
125+ clicked_tiles .clear ()
126+ restored = load_state_from_storage ()
127+
128+ # Assert: Should handle corruption gracefully (currently doesn't)
129+ assert restored is False # This might fail if error handling is poor
130+ assert len (clicked_tiles ) == 0 # Should be empty after failed load
131+
132+ def test_init_app_called_multiple_times (self ):
133+ """Test that calling init_app multiple times doesn't lose state."""
134+ from app import init_app
135+
136+ # Arrange: Set up initial state
137+ phrases = read_phrases_file ()
138+ generate_board (1 , phrases )
139+ toggle_tile (3 , 3 )
140+ save_state_to_storage ()
141+ initial_tiles = clicked_tiles .copy ()
142+
143+ # Act: Call init_app again (simulates hot reload scenario)
144+ with patch ('app.init_routes' ):
145+ init_app ()
146+
147+ # Assert: State should be preserved
148+ assert clicked_tiles == initial_tiles # This may fail!
149+
150+
151+ class TestBDDStyleStatePersistence :
152+ """BDD-style tests for state persistence scenarios."""
153+
154+ def setup_method (self ):
155+ """Given I have a bingo game in progress."""
156+ # Reset and initialize
157+ board .clear ()
158+ clicked_tiles .clear ()
159+ is_game_closed = False
160+
161+ if not hasattr (app , 'storage' ):
162+ app .storage = Mock ()
163+ app .storage .general = {}
164+
165+ # Set up game
166+ phrases = read_phrases_file ()
167+ generate_board (1 , phrases )
168+
169+ def test_scenario_graceful_restart (self ):
170+ """
171+ Scenario: State persists through graceful restart
172+ """
173+ # Given I have clicked tiles at positions (0,1), (2,3), and (4,4)
174+ positions = [(0 , 1 ), (2 , 3 ), (4 , 4 )]
175+ for row , col in positions :
176+ toggle_tile (row , col )
177+
178+ # When the app restarts gracefully
179+ save_state_to_storage ()
180+
181+ # Simulate restart
182+ clicked_tiles .clear ()
183+ board .clear ()
184+
185+ # Then the state should be restored
186+ assert load_state_from_storage () is True
187+
188+ # And the clicked tiles should remain
189+ for pos in positions :
190+ assert pos in clicked_tiles
191+
192+ # And the board should show the same phrases
193+ assert len (board ) == 5
194+ assert len (board [0 ]) == 5
195+
196+ def test_scenario_code_change_reload (self ):
197+ """
198+ Scenario: State persists when code changes trigger reload
199+ """
200+ # Given I have a game in progress
201+ toggle_tile (1 , 1 )
202+ toggle_tile (2 , 2 )
203+ original_tiles = clicked_tiles .copy ()
204+
205+ # When I modify a source file and NiceGUI triggers a hot reload
206+ save_state_to_storage ()
207+
208+ # Simulate what happens during hot reload
209+ import importlib
210+ import src .core .game_logic
211+
212+ # Clear module state (simulates reload)
213+ clicked_tiles .clear ()
214+ board .clear ()
215+
216+ # Reload module
217+ importlib .reload (src .core .game_logic )
218+
219+ # Then the game state should be preserved
220+ load_state_from_storage ()
221+ assert clicked_tiles == original_tiles # This likely fails!
222+
223+
224+ def test_nicegui_storage_behavior ():
225+ """Test to understand actual NiceGUI storage behavior."""
226+ # This test documents the actual behavior we need to work with
227+
228+ # 1. Storage is only available after ui.run() is called
229+ # 2. Storage uses browser localStorage by default
230+ # 3. Server restarts clear server-side state
231+ # 4. Hot reloads may or may not preserve storage
232+
233+ # We need to architect around these constraints
234+ pass
0 commit comments