b3227f5b78
- main_menu.gd: Play button now navigates to world_list.tscn
- world_list.gd: fix invalid theme_override_constants property-path syntax
(use add_theme_constant_override), fix @onready node paths missing
/HeaderHBox/, cast Variant dict values to String to avoid type-inference
errors when cards actually render
- world_list.tscn: wire up Back/Refresh button signal connections (scene
had zero [connection] blocks, buttons were dead)
- backend: fix real bug where World.visibility enum sent member names
("PUBLIC") to Postgres instead of lowercase values, breaking both GET
and POST /api/worlds/ for any real query
- backend: fix WorldResponse.created_at typed as str instead of datetime,
causing a second 500 on GET once the enum bug was fixed
- add smoke_test_world_list.gd: headless scene-level smoke test that
registers a user, instantiates the real scene, and confirms world cards
render from live backend data (not just an API-level check)
123 lines
4.1 KiB
GDScript
123 lines
4.1 KiB
GDScript
extends SceneTree
|
|
## Headless smoke test for the world list screen.
|
|
##
|
|
## Run with:
|
|
## godot --headless --path . --script res://scripts/smoke_test_world_list.gd
|
|
##
|
|
## Requires the backend to be running (default base URL
|
|
## `application/agp/api_base_url` -> http://127.0.0.1:8000, override with
|
|
## AGP_API_BASE_URL).
|
|
##
|
|
## Unlike a pure API-level test, this actually instantiates scenes/
|
|
## world_list.tscn as a real scene and adds it to the tree -- catches
|
|
## @onready node-path bugs (a node reference pointing at the wrong path
|
|
## errors right here, not just "looks right" from reading the script) that
|
|
## an API-only test would miss entirely.
|
|
##
|
|
## Tests:
|
|
## 1. Register + log in a fresh user (so get_worlds() has a valid token).
|
|
## 2. Instantiate world_list.tscn, add to tree -- @onready resolution
|
|
## must not error.
|
|
## 3. Wait for _refresh_worlds()'s awaited request to complete.
|
|
## 4. Confirm the screen reached a real end state (world cards, or the
|
|
## empty-list message -- either is fine, a crash/stuck-loading isn't).
|
|
##
|
|
## Exits 0 on full pass, 1 on any failure.
|
|
|
|
const WORLD_LIST_SCENE := "res://scenes/world_list.tscn"
|
|
|
|
|
|
func _initialize() -> void:
|
|
_run()
|
|
|
|
|
|
func _run() -> void:
|
|
await process_frame
|
|
|
|
var api := root.get_node("ApiClient")
|
|
assert(api != null, "ApiClient autoload not found")
|
|
|
|
var passed: int = 0
|
|
var failed: int = 0
|
|
|
|
# --- Setup: register + log in so the world list has a valid token ---
|
|
print("\n=== Setup: register + login ===")
|
|
randomize()
|
|
var unique_id: int = int(Time.get_unix_time_from_system() * 1000) + randi() % 100000
|
|
var username := "worldlist_smoke_%d" % unique_id
|
|
var email := "worldlist_smoke_%d@test.example.com" % unique_id
|
|
var reg_result: Dictionary = await api.register(username, email, "SmokeTest123!", "2000-01-01")
|
|
if not _check("setup: register succeeded", reg_result.ok):
|
|
print(" detail: ", reg_result.detail)
|
|
quit(1)
|
|
return
|
|
|
|
# --- Test 1: Instantiate the real scene ---
|
|
print("\n=== Test 1: Instantiate world_list.tscn ===")
|
|
var packed_scene: PackedScene = load(WORLD_LIST_SCENE)
|
|
if not _check("scene loads", packed_scene != null):
|
|
quit(1)
|
|
return
|
|
|
|
var instance: Control = packed_scene.instantiate()
|
|
if not _check("scene instantiates", instance != null):
|
|
quit(1)
|
|
return
|
|
|
|
root.add_child(instance)
|
|
# One frame for _ready() (and its @onready resolution) to run. A wrong
|
|
# node path in an @onready var errors during this, not silently.
|
|
await process_frame
|
|
|
|
if _check("scene added to tree without error", is_instance_valid(instance)):
|
|
passed += 1
|
|
else:
|
|
failed += 1
|
|
|
|
# --- Test 2: Wait for the world fetch to resolve, check a real end state ---
|
|
print("\n=== Test 2: World fetch reaches a real end state ===")
|
|
# _refresh_worlds() runs from _ready(); give its awaited HTTP call time
|
|
# to complete rather than a fixed sleep -- poll for either end state.
|
|
var world_list_node: VBoxContainer = instance.get_node("VBoxContainer/ScrollContainer/WorldList")
|
|
var empty_label: Label = instance.get_node("VBoxContainer/EmptyLabel")
|
|
var error_label: Label = instance.get_node("VBoxContainer/ErrorLabel")
|
|
|
|
var settled := false
|
|
for i in range(50): # up to ~5s at 100ms/frame-ish via process_frame waits
|
|
await process_frame
|
|
if world_list_node.visible or empty_label.visible or error_label.visible:
|
|
settled = true
|
|
break
|
|
|
|
if _check("reached a real end state (cards, empty, or error) not stuck loading", settled):
|
|
passed += 1
|
|
else:
|
|
failed += 1
|
|
print(" world_list.visible=%s empty_label.visible=%s error_label.visible=%s" % [
|
|
world_list_node.visible, empty_label.visible, error_label.visible
|
|
])
|
|
|
|
if error_label.visible:
|
|
printerr(" Note: ended in error state: ", error_label.text)
|
|
elif world_list_node.visible:
|
|
print(" Note: rendered %d world card(s)" % world_list_node.get_child_count())
|
|
elif empty_label.visible:
|
|
print(" Note: ended in empty-list state")
|
|
|
|
# --- Summary ---
|
|
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])
|
|
if failed > 0:
|
|
quit(1)
|
|
else:
|
|
print("SMOKE RESULT: ALL PASS")
|
|
quit(0)
|
|
|
|
|
|
func _check(description: String, condition: bool) -> bool:
|
|
if condition:
|
|
print(" PASS: ", description)
|
|
return true
|
|
else:
|
|
printerr(" FAIL: ", description)
|
|
return false
|