8fc9d378e4
Reported directly by the user after seeding a test world through the browse-worlds screen: "screen is blank now". Two compounding layout bugs, both real: 1. world_list.tscn's root VBoxContainer had no anchors at all, only size_flags -- but its parent (the scene's root node) is a plain Control, not a Container, so size_flags on a Control's direct child are meaningless there. Without anchors it never actually filled the screen, so it (and everything under it) collapsed toward zero size. 2. size_flags_horizontal/vertical used the raw integer 4 everywhere expand-fill was clearly intended (ScrollContainer, its WorldList child, HeaderHBox, TitleLabel) -- but 4 is Control.SIZE_SHRINK_CENTER in Godot 4, not SIZE_EXPAND_FILL (which is 3). ScrollContainer with SHRINK_CENTER takes only its own minimum size instead of expanding, cascading a ~1px width down through every card and every label. Confirmed the actual enum values by asking Godot directly rather than assuming. Both are exactly the kind of bug a headless smoke test checking only "did cards get added to the tree" can't catch -- child_count was correctly non-zero, nothing crashed, the content was just rendered invisibly thin. Verified the fix is real by deliberately reintroducing bug #2 and confirming the test now correctly fails (card width 25px, below the 200px sanity threshold) before restoring it. Also replaced the card background: it was a ColorRect manually moved to child index 0 of a VBoxContainer, which doesn't work as an overlay -- a VBoxContainer lays out ALL its children in sequence, so the "moved to front" ColorRect rendered as an empty dark bar ABOVE the text, not behind it. Now a proper PanelContainer with a StyleBoxFlat background, the idiomatic way to get a colored panel behind auto-laid-out content in Godot. smoke_test_world_list.gd strengthened to actually catch this class of bug going forward: seeds a real world via POST /api/worlds/ first (the checkpoint runner's fresh DB meant the populated-card path never ran in CI at all before this, only empty-list did), and asserts each card's real computed width, not just that cards exist. Full checkpoint suite re-verified clean on this branch: migrations, 68/68 pytest, all 6 Godot smoke tests, both platform exports. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
169 lines
6.3 KiB
GDScript
169 lines
6.3 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. POST a real world via the API so the populated-card path actually
|
|
## gets exercised -- run_checkpoint_tests.py always starts from a
|
|
## genuinely empty DB, so without this the card-rendering code below
|
|
## would never run in CI at all, only the empty-list path would.
|
|
## 3. Instantiate world_list.tscn, add to tree -- @onready resolution
|
|
## must not error.
|
|
## 4. Wait for _refresh_worlds()'s awaited request to complete.
|
|
## 5. Confirm it actually reached the populated (not empty) state, and
|
|
## that the rendered card has a real width -- not just non-zero
|
|
## child_count. 2026-08-20: a real bug shipped where every card
|
|
## technically existed in the tree (child_count > 0, no error, no
|
|
## crash) but rendered at 1.0px wide because size_flags_horizontal/
|
|
## vertical used the raw integer 4 (Control.SIZE_SHRINK_CENTER)
|
|
## instead of 3 (Control.SIZE_EXPAND_FILL) on ScrollContainer -- a
|
|
## child-count check alone would never catch that, only a real size
|
|
## assertion does.
|
|
##
|
|
## Exits 0 on full pass, 1 on any failure.
|
|
|
|
const WORLD_LIST_SCENE := "res://scenes/world_list.tscn"
|
|
const MIN_SANE_CARD_WIDTH := 200.0 # a real screen is at minimum a few hundred px wide
|
|
|
|
|
|
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
|
|
|
|
# --- Seed a real world so the populated-card path is actually exercised ---
|
|
print("\n=== Setup: seed a real world ===")
|
|
var world_body := JSON.stringify({
|
|
"name": "Smoke Test World %d" % unique_id,
|
|
"description": "Seeded by smoke_test_world_list.gd to exercise the populated-card render path.",
|
|
"visibility": "public",
|
|
})
|
|
var create_result: Dictionary = await api.request(HTTPClient.METHOD_POST, "/api/worlds/", world_body, "application/json", true)
|
|
if not _check("setup: world created", create_result.ok):
|
|
print(" detail: ", create_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 empty_label.visible:
|
|
# We just seeded a world -- ending up empty means GET /api/worlds
|
|
# isn't actually seeing what POST /api/worlds/ just created.
|
|
_check("ended in populated state, not empty (a world was just seeded)", false)
|
|
failed += 1
|
|
elif world_list_node.visible:
|
|
var card_count := world_list_node.get_child_count()
|
|
print(" Note: rendered %d world card(s)" % card_count)
|
|
|
|
if _check("at least one card rendered", card_count > 0):
|
|
passed += 1
|
|
else:
|
|
failed += 1
|
|
|
|
# The actual bug this test exists to catch: a card can be present
|
|
# in the tree, not crash, and still be visually nothing because
|
|
# it rendered at ~1px wide. Check every card's real computed size,
|
|
# not just that it exists.
|
|
for i in range(card_count):
|
|
var card: Control = world_list_node.get_child(i)
|
|
var wide_enough := card.size.x >= MIN_SANE_CARD_WIDTH
|
|
if _check("card %d has a real width (%.0fpx >= %.0fpx)" % [i, card.size.x, MIN_SANE_CARD_WIDTH], wide_enough):
|
|
passed += 1
|
|
else:
|
|
failed += 1
|
|
|
|
# --- 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
|