diff --git a/docs/testing/test_inventory.md b/docs/testing/test_inventory.md index 339dbf3..a9f7f45 100644 --- a/docs/testing/test_inventory.md +++ b/docs/testing/test_inventory.md @@ -7,9 +7,13 @@ to know what "run the tests" actually checked. Run everything below in one shot with `scripts/run_checkpoint_tests.py`. -Last verified against `main`: 2026-08-20 (commit `927f9bd`) — full PC1 -checkpoint run, fresh clone, migrations + pytest + all 4 Godot smoke tests + -both platform exports, all clean. +Last verified against `main`: 2026-08-20 — full PC1 checkpoint run, +migrations + pytest + all 6 Godot smoke tests + both platform exports, all +clean. Two real bugs found and fixed via real playtesting this same day +(not caught by the checkpoint suite as it stood before today): the main +menu had no way to actually open the login/register form, and the world +list's cards rendered at ~1px wide once real data existed. Both are now +covered by the smoke tests below so they can't silently regress. ## Backend — `backend/tests/` (pytest) @@ -58,7 +62,7 @@ these yet): | `smoke_test_api.gd` | Exercises `ApiClient.health_check()` against a live backend. Verifies the real HTTP request path (`HTTPRequest` node lifecycle, JSON parse, result-dictionary contract). Exits 1 on failure. | | `smoke_test_auth.gd` | Tests the full auth flow via `ApiClient` autoload: register unique user (201), duplicate username (409), under-18 registration (400), correct login (200), wrong password (401), nonexistent user (401). | | `smoke_test_e2e.gd` | End-to-end flow: health check → register → login → worlds list. Verifies the full request chain with a fresh user, including token storage and `get_worlds()` with auth header. | -| `smoke_test_world_list.gd` | Instantiates `world_list.tscn` as a real scene, adds to tree (catches `@onready` node-path bugs), waits for `_refresh_worlds()` to complete, confirms the screen reaches a real end state (world cards, empty-list message, or error label — not stuck loading). | +| `smoke_test_world_list.gd` | Registers a user, **seeds a real world via `POST /api/worlds/`** (without this, `run_checkpoint_tests.py`'s always-fresh DB means the populated-card render path never actually runs in CI, only the empty-list path would), instantiates `world_list.tscn` as a real scene, waits for `_refresh_worlds()` to complete, confirms it reached the populated (not empty) state, and asserts **each rendered card's real computed width is >= 200px**. That last check is the one that matters: 2026-08-20, a real bug shipped where every card existed in the tree with a non-zero child count and no crash, but rendered at 1.0px wide (`ScrollContainer`'s `size_flags_horizontal`/`vertical` used the raw integer `4` = `Control.SIZE_SHRINK_CENTER` instead of `3` = `SIZE_EXPAND_FILL`) — invisible on screen, and a child-count-only check would never have caught it. Confirmed by deliberately reintroducing the bug: card width check correctly fails at 25px. | | `smoke_test_main_menu_ui.gd` | Instantiates `main_menu.tscn`, confirms the auth panel starts hidden, and confirms a **real signal emission** on LoginButton/RegisterButton (not calling the handler method directly) actually opens it. This is the check that would have caught 2026-08-20's real bug: the auth panel and its logic were fully correct, but nothing in the scene was ever wired to open it, so Login/Register were completely unreachable from the UI. | | `smoke_test_main_menu_full_flow.gd` | Full click-through: open the register form, fill it exactly as a user would, submit, confirm Play actually becomes enabled, the welcome message shows, and the auth buttons hide themselves. Calls the submit handler directly (awaited) rather than via `auth_button.pressed.emit()` — see the note at the top of the file: a synthetic `.emit()` on an `await`-using handler doesn't get driven forward reliably in `--script`/SceneTree headless mode the way real GUI input event dispatch does, confirmed by comparing direct-call vs `.emit()` behavior. Not a bug in the actual click-driven flow, just a headless-test-harness quirk to route around when writing tests. | diff --git a/scenes/world_list.tscn b/scenes/world_list.tscn index e2138e4..7b4885d 100644 --- a/scenes/world_list.tscn +++ b/scenes/world_list.tscn @@ -11,14 +11,17 @@ grow_vertical = 2 script = ExtResource("1_wl") [node name="VBoxContainer" type="VBoxContainer" parent="."] -layout_mode = 3 +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 theme_override_constants/separation = 8 -size_flags_horizontal = 4 -size_flags_vertical = 4 [node name="HeaderHBox" type="HBoxContainer" parent="VBoxContainer"] layout_mode = 3 -size_flags_horizontal = 4 +size_flags_horizontal = 3 theme_override_constants/separation = 16 [node name="BackButton" type="Button" parent="VBoxContainer/HeaderHBox"] @@ -30,7 +33,7 @@ size_flags_horizontal = 0 layout_mode = 3 text = "Browse Worlds" horizontal_alignment = 1 -size_flags_horizontal = 4 +size_flags_horizontal = 3 theme_override_font_sizes/font_size = 28 [node name="RefreshButton" type="Button" parent="VBoxContainer/HeaderHBox"] @@ -40,15 +43,14 @@ size_flags_horizontal = 0 [node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] layout_mode = 3 -size_flags_horizontal = 4 -size_flags_vertical = 4 +size_flags_horizontal = 3 +size_flags_vertical = 3 horizontal_mode = 2 vertical_mode = 2 [node name="WorldList" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"] layout_mode = 3 -size_flags_horizontal = 4 -size_flags_vertical = 4 +size_flags_horizontal = 3 theme_override_constants/separation = 8 [node name="LoadingLabel" type="Label" parent="VBoxContainer"] diff --git a/scripts/smoke_test_world_list.gd b/scripts/smoke_test_world_list.gd index 951d6f5..5cb6a16 100644 --- a/scripts/smoke_test_world_list.gd +++ b/scripts/smoke_test_world_list.gd @@ -16,15 +16,27 @@ extends SceneTree ## ## 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 +## 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. -## 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). +## 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: @@ -52,6 +64,19 @@ func _run() -> void: 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) @@ -99,10 +124,31 @@ func _run() -> void: 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") + # 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]) diff --git a/scripts/world_list.gd b/scripts/world_list.gd index 9f7bee6..44f2cb1 100644 --- a/scripts/world_list.gd +++ b/scripts/world_list.gd @@ -88,10 +88,26 @@ func _refresh_worlds() -> void: ## Create a visual card for a single world entry. func _create_world_card(world_dict: Dictionary) -> Control: + # PanelContainer for the background, not a ColorRect moved to child index + # 0 of a VBoxContainer -- a VBoxContainer lays out ALL its children in a + # vertical sequence, it has no concept of "float this one child behind + # the others as an overlay". That version rendered as an empty dark bar + # sitting above the text, not a background behind it. + var panel := PanelContainer.new() + panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL + panel.custom_minimum_size = Vector2(0, 80) + var style := StyleBoxFlat.new() + style.bg_color = Color(0.15, 0.15, 0.15, 1.0) + style.content_margin_left = 12 + style.content_margin_right = 12 + style.content_margin_top = 8 + style.content_margin_bottom = 8 + panel.add_theme_stylebox_override("panel", style) + var card := VBoxContainer.new() - card.size_flags_horizontal = Control.SIZE_FILL - card.custom_minimum_size = Vector2(0, 80) + card.size_flags_horizontal = Control.SIZE_EXPAND_FILL card.add_theme_constant_override("separation", 4) + panel.add_child(card) var name_label := Label.new() name_label.text = world_dict.get("name", "Unnamed World") @@ -123,13 +139,4 @@ func _create_world_card(world_dict: Dictionary) -> Control: card.add_child(desc_label) card.add_child(meta_label) - # Subtle background for the card - var bg := ColorRect.new() - bg.color = Color(0.15, 0.15, 0.15, 1.0) - bg.size_flags_horizontal = Control.SIZE_FILL - bg.size_flags_vertical = Control.SIZE_FILL - bg.custom_minimum_size = Vector2(0, 80) - card.add_child(bg) - card.move_child(bg, 0) - - return card + return panel