aeccc78143
_show_authenticated() (the session-restore path, used when returning to main_menu.tscn -- e.g. after clicking Back from world_list) only hid EmailEdit/Over18Check, unlike _on_auth_complete() (the fresh-login path) which hides the full form: UsernameEdit, PasswordEdit, EmailEdit, Over18Check, AuthButton, AuthModeLabel. Play was correctly enabled and the welcome message was correctly visible, so a check that stopped at those two passed clean -- but the actual login fields were still on screen, which is what a real user sees as "it's making me log in again." Confirmed via live PC1 playtest today. Reported by: real playtest, 2026-08-20 (yesterday's automated regression test for this exact Play -> Back flow only asserted Play/welcome/auth buttons row -- strengthened smoke_test_main_menu_back.gd to also check the individual form fields, and confirmed it fails exactly this way with the bug reintroduced before restoring the fix). Verified against a live backend: 61/61 pytest (unaffected), 8/8 Godot smoke tests (including 20/20 on the strengthened back-nav test). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M6rUEynmv2QmweocAQYARN
171 lines
6.7 KiB
GDScript
171 lines
6.7 KiB
GDScript
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
|
|
## actually visible in the tree, 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!"
|
|
|
|
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, true)
|
|
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")
|
|
var username_edit: LineEdit = menu.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit")
|
|
var password_edit: LineEdit = menu.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit")
|
|
var auth_button: Button = menu.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton")
|
|
var auth_mode_label: Label = menu.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel")
|
|
|
|
_check("Play enabled", not play_button.disabled)
|
|
# WelcomeLabel lives inside AuthPanel, so `.visible` alone is a
|
|
# false-positive: the label can have visible=true while its parent is
|
|
# hidden and it renders as nothing. `is_visible_in_tree()` is the
|
|
# check that proves the user can actually see the welcome message.
|
|
_check("Welcome label visible in tree", welcome_label.is_visible_in_tree())
|
|
_check("Auth buttons row hidden", not auth_buttons_row.visible)
|
|
_check("Auth panel visible (it contains the welcome label)", auth_panel.visible)
|
|
# Real bug, 2026-08-20: the session-restore path (this one, after
|
|
# Play -> Back) hid EmailEdit/Over18Check but never the rest of the
|
|
# login form -- Play enabled + welcome label visible were both still
|
|
# true, so a check that stopped there passed while the user was
|
|
# looking at the login fields again on top of the welcome message.
|
|
_check("Username field hidden", not username_edit.visible)
|
|
_check("Password field hidden", not password_edit.visible)
|
|
_check("Auth button hidden", not auth_button.visible)
|
|
_check("Auth mode label hidden", not auth_mode_label.visible)
|
|
|
|
return (not play_button.disabled) \
|
|
and welcome_label.is_visible_in_tree() \
|
|
and not auth_buttons_row.visible \
|
|
and auth_panel.visible \
|
|
and not username_edit.visible \
|
|
and not password_edit.visible \
|
|
and not auth_button.visible \
|
|
and not auth_mode_label.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)
|