Skip to content

Commit 482f996

Browse files
fix(ui): ensure header updates when broadcast fails
- Add fallback to manually run sync_board_state when ui.broadcast() fails - Fix bug where header wouldn't update when game closed in newer NiceGUI versions - Add test to validate fix works when broadcast mechanism unavailable 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 41c30d8 commit 482f996

File tree

2 files changed

+88
-1
lines changed

2 files changed

+88
-1
lines changed

src/core/game_logic.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,11 @@ def close_game():
288288
# In newer versions of NiceGUI, broadcast might not be available
289289
# We rely on the timer-based sync instead
290290
logging.info("ui.broadcast not available, relying on timer-based sync")
291+
292+
# If broadcast isn't available, manually trigger sync on current view
293+
# This ensures immediate update even if broadcast fails
294+
from src.ui.sync import sync_board_state
295+
sync_board_state()
291296

292297
# Notify that game has been closed
293298
ui.notify("Game has been closed", color="red", duration=3)
@@ -344,5 +349,10 @@ def reopen_game():
344349
ui.broadcast() # Broadcast changes to all connected clients
345350
except AttributeError:
346351
# In newer versions of NiceGUI, broadcast might not be available
347-
# We rely on the timer-based sync instead
352+
# Run sync manually to ensure immediate update
348353
logging.info("ui.broadcast not available, relying on timer-based sync")
354+
355+
# If broadcast isn't available, manually trigger sync on current view
356+
# This ensures immediate update even if broadcast fails
357+
from src.ui.sync import sync_board_state
358+
sync_board_state()

tests/test_ui_functions.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,83 @@ def test_stream_header_update_when_game_closed(self, mock_broadcast):
468468
# Restore original state
469469
main.is_game_closed = original_is_game_closed
470470
main.header_label = original_header_label
471+
472+
@patch("src.core.game_logic.ui")
473+
def test_header_update_when_broadcast_fails(self, mock_ui):
474+
"""
475+
Test that the header is correctly updated when ui.broadcast() raises an AttributeError.
476+
This simulates the scenario where newer versions of NiceGUI don't support broadcast.
477+
"""
478+
from src.config.constants import CLOSED_HEADER_TEXT
479+
from src.core.game_logic import board_views, close_game, is_game_closed
480+
481+
# Mock board views
482+
mock_home_container = MagicMock()
483+
mock_stream_container = MagicMock()
484+
mock_buttons_home = {}
485+
mock_buttons_stream = {}
486+
487+
# Mock header labels
488+
mock_home_header = MagicMock()
489+
mock_stream_header = MagicMock()
490+
491+
# Save original state
492+
original_board_views = board_views.copy() if hasattr(board_views, "copy") else {}
493+
original_is_game_closed = is_game_closed
494+
495+
# Save and restore the header_label global variable
496+
import src.core.game_logic
497+
original_header_label = src.core.game_logic.header_label
498+
499+
try:
500+
# Set up board views dictionary
501+
board_views.clear()
502+
board_views.update({
503+
"home": (mock_home_container, mock_buttons_home),
504+
"stream": (mock_stream_container, mock_buttons_stream),
505+
})
506+
507+
# Set up initial state
508+
src.core.game_logic.is_game_closed = False
509+
src.core.game_logic.header_label = mock_home_header
510+
511+
# Make broadcast raise AttributeError to simulate newer NiceGUI versions
512+
mock_ui.broadcast.side_effect = AttributeError("'module' object has no attribute 'broadcast'")
513+
514+
# Set up controls_row mock
515+
src.core.game_logic.controls_row = MagicMock()
516+
517+
# Close the game from home view
518+
close_game()
519+
520+
# Check home header was updated
521+
mock_home_header.set_text.assert_called_with(CLOSED_HEADER_TEXT)
522+
mock_home_header.update.assert_called()
523+
524+
# Verify game is marked as closed
525+
self.assertTrue(src.core.game_logic.is_game_closed)
526+
527+
# Reset header mock and switch to stream view
528+
mock_home_header.reset_mock()
529+
src.core.game_logic.header_label = mock_stream_header
530+
531+
# Run sync_board_state to simulate a new client connecting
532+
from src.ui.sync import sync_board_state
533+
534+
# Mock ui in sync_board_state and make sure it sees the same is_game_closed state
535+
with patch("src.ui.sync.ui"), patch("src.ui.sync.is_game_closed", src.core.game_logic.is_game_closed), patch("src.ui.sync.header_label", mock_stream_header):
536+
sync_board_state()
537+
538+
# Check stream header was updated correctly even though broadcast failed
539+
mock_stream_header.set_text.assert_called_with(CLOSED_HEADER_TEXT)
540+
mock_stream_header.update.assert_called()
541+
542+
finally:
543+
# Restore original state
544+
board_views.clear()
545+
board_views.update(original_board_views)
546+
src.core.game_logic.is_game_closed = original_is_game_closed
547+
src.core.game_logic.header_label = original_header_label
471548

472549

473550
if __name__ == "__main__":

0 commit comments

Comments
 (0)