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!", true) 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