diff --git a/scripts/main_menu.gd b/scripts/main_menu.gd index 06b2d94..77d3226 100644 --- a/scripts/main_menu.gd +++ b/scripts/main_menu.gd @@ -6,6 +6,23 @@ extends Control ## The Login and Register buttons on the AuthButtonsRow directly set the panel's ## mode, so the user can switch between Login and Register at any time while ## the panel is open. +## +## On successful auth the token is stored via ApiClient, AuthButtonsRow hides, +## the welcome label shows the username, and the Play button becomes enabled. +## +## When returning from another scene (e.g. world_list), _ready() checks whether +## ApiClient already holds a valid token. If so, it restores the authenticated +## UI state (Play enabled, welcome label visible) instead of forcing the user +## back to the login form. + + +## Emitted once _ready()'s session check (has_token() + the awaited +## get_me() call) has actually resolved, whichever way -- lets a test (or +## anything else) know the UI state is trustworthy now, rather than +## guessing from a frame count or from UI properties that are ambiguous +## mid-await (e.g. AuthButtonsRow.visible is true both before the check +## resolves AND in a genuine logged-out end state). +signal auth_state_ready # ─── UI references ──────────────────────────────────────────────────────────── @@ -47,7 +64,40 @@ func _ready() -> void: _play_button = $CenterContainer/VBoxContainer/PlayButton _quit_button = $CenterContainer/VBoxContainer/QuitButton - # Initial state. + # Check whether a valid session already exists (e.g. user logged in, + # navigated to world_list, then clicked Back). If so, restore the + # authenticated UI state instead of forcing the login form again. + if ApiClient.has_token(): + var me_result: Dictionary = await ApiClient.get_me() + if me_result.ok and me_result.data is Dictionary: + var username: String = str((me_result.data as Dictionary).get("username", "")) + if username != "": + _show_authenticated(username) + auth_state_ready.emit() + return + + # Token missing or invalid — fall through to logged-out state. + _reset_to_logged_out() + auth_state_ready.emit() + + +## Restore the UI to the authenticated state (Play enabled, welcome label, +## auth buttons hidden). +func _show_authenticated(username: String) -> void: + _auth_panel.visible = false + _status_label.visible = false + _welcome_label.text = "Welcome, %s!" % username + _welcome_label.visible = true + _email_edit.visible = false + _dob_edit.visible = false + _play_button.disabled = false + _auth_buttons_row.visible = false + print("Restored authenticated session for: ", username) + + +## Reset the UI to the logged-out state (Play disabled, auth panel hidden, +## login/register buttons visible). +func _reset_to_logged_out() -> void: _auth_panel.visible = false _status_label.visible = false _welcome_label.visible = false diff --git a/scripts/smoke_test_main_menu_back.gd b/scripts/smoke_test_main_menu_back.gd new file mode 100644 index 0000000..621a31e --- /dev/null +++ b/scripts/smoke_test_main_menu_back.gd @@ -0,0 +1,150 @@ +extends SceneTree +## Regression test for the Play -> Back navigation bug. +## +## The reported bug: after logging in and clicking Play, the user lands on +## world_list.tscn; clicking Back returns to main_menu.tscn, but the menu +## reset to a fully logged-out state (Play disabled, login form showing) +## even though ApiClient still held a valid token. main_menu.gd's _ready() +## used to unconditionally reset the UI on every scene load. +## +## This test drives the real navigation flow (change_scene_to_file) against +## a live backend: +## 1. Register a unique user so ApiClient holds a valid token. +## 2. Load main_menu.tscn and assert it restores the authenticated state. +## 3. Click Play -> world_list.tscn. +## 4. Click Back -> main_menu.tscn. +## 5. Assert the menu is STILL authenticated (Play enabled, welcome label +## visible, login/register buttons hidden). +## +## Run with: +## godot --headless --path . --script res://scripts/smoke_test_main_menu_back.gd +## +## Requires the backend to be running (default base URL +## `application/agp/api_base_url` -> http://127.0.0.1:8000). + +const USERNAME := "backnav_%d" +const EMAIL := "backnav_%d@test.example.com" +const PASSWORD := "BackNav123!" +const DOB := "2000-01-01" + +var passed := 0 +var failed := 0 + + +func _initialize() -> void: + _run() + + +func _run() -> void: + await process_frame + + var api := root.get_node("ApiClient") + if api == null: + printerr("FAIL: ApiClient autoload not found") + quit(1) + return + + # 1. Establish a real, valid session exactly the way a user would. + randomize() + var unique_id: int = int(Time.get_unix_time_from_system() * 1000) + randi() % 100000 + var username := USERNAME % unique_id + var email := EMAIL % unique_id + print("\n=== Setup: register ", username, " ===") + var register_result: Dictionary = await api.register(username, email, PASSWORD, DOB) + if not _check("register succeeds", register_result.ok): + print(" detail: ", register_result.detail) + _finish() + return + _check("ApiClient stores token after register", api.has_token()) + + # 2. First visit to the main menu: should already show the authenticated + # state (no login form, Play enabled). + change_scene_to_file("res://scenes/main_menu.tscn") + # change_scene_to_file defers the actual swap -- a couple frames is + # plenty for the scene object itself to exist (unlike the real + # get_me() network round-trip afterward, which is what auth_state_ready + # actually waits for below). + for i in 5: + await process_frame + if current_scene is Control: + break + + var menu: Control = current_scene + if menu == null or not menu is Control: + printerr("FAIL: main_menu.tscn did not become current_scene") + _finish() + return + await menu.auth_state_ready + _check("first main menu visit restores authenticated state", _is_authenticated(menu)) + + # 3. Play -> world_list.tscn (real button press). + var play_button: Button = menu.get_node("CenterContainer/VBoxContainer/PlayButton") + play_button.pressed.emit() + await _wait_for_scene_change(menu) + + var world_list_scene := current_scene + if world_list_scene == null or world_list_scene == menu: + printerr("FAIL: world_list.tscn did not become current_scene after Play") + _finish() + return + + # 4. Back -> main_menu.tscn (real button press). + var back_button: Button = world_list_scene.get_node("VBoxContainer/HeaderHBox/BackButton") + back_button.pressed.emit() + await _wait_for_scene_change(world_list_scene) + + menu = current_scene + if menu == null or not menu is Control: + printerr("FAIL: main_menu.tscn did not become current_scene after Back") + _finish() + return + + # The new main_menu instance's own _ready() is *also* awaiting a real + # get_me() call -- _wait_for_scene_change only confirms the scene + # object exists, not that its authenticated-state fields have been + # set yet. Wait for the real signal, not a guess. + await menu.auth_state_ready + + # 5. THE regression assertion: still logged in after returning. + _check("main menu stays authenticated after Play -> Back", _is_authenticated(menu)) + + _finish() + + +func _is_authenticated(menu: Control) -> bool: + var play_button: Button = menu.get_node("CenterContainer/VBoxContainer/PlayButton") + var welcome_label: Label = menu.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel") + var auth_buttons_row: HBoxContainer = menu.get_node("CenterContainer/VBoxContainer/AuthButtonsRow") + var auth_panel: PanelContainer = menu.get_node("CenterContainer/VBoxContainer/AuthPanel") + + _check("Play enabled", not play_button.disabled) + _check("Welcome label visible", welcome_label.visible) + _check("Auth buttons row hidden", not auth_buttons_row.visible) + _check("Auth panel hidden", not auth_panel.visible) + + return (not play_button.disabled) \ + and welcome_label.visible \ + and not auth_buttons_row.visible \ + and not auth_panel.visible + + +func _wait_for_scene_change(previous_scene: Node) -> void: + for i in 20: + await process_frame + if current_scene != previous_scene and current_scene != null: + return + + +func _check(description: String, condition: bool) -> bool: + if condition: + print(" PASS: ", description) + passed += 1 + else: + printerr(" FAIL: ", description) + failed += 1 + return condition + + +func _finish() -> void: + print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed]) + quit(0 if failed == 0 else 1)