Compare commits

...

11 Commits

Author SHA1 Message Date
coder-bot 12fef51020 WIP checkpoint: Main menu: fix Register-mode UI trapping the user (remove the confusing mode toggle) 2026-08-20 14:03:57 +00:00
coder-bot af50819a16 WIP checkpoint: Main menu: fix Register-mode UI trapping the user (remove the confusing mode toggle) 2026-08-20 14:01:20 +00:00
coder-bot 16bb87c6b0 WIP checkpoint: Main menu: fix Register-mode UI trapping the user (remove the confusing mode toggle) 2026-08-20 13:56:44 +00:00
claude-checkpoint-tooling 8fc9d378e4 Fix: world cards render at ~1px wide once real data exists
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>
2026-08-20 13:35:46 +00:00
claude-checkpoint-tooling 3679e4dbc4 Fix: Login/Register unreachable from the main menu -- no button opened it
Reported directly by the user playtesting the exported exe: Play was
permanently disabled and there was no way to log in or register. Root
cause: main_menu.tscn wired Play and Quit, but nothing was ever
connected to _on_login_button_pressed()/_on_register_button_pressed() --
the AuthPanel, its form, and all its backend-calling logic were fully
correct, just completely unreachable. The script's own top comment even
said "(the existing Play/Quit buttons)" as if Play/Quit somehow doubled
as login triggers, which they never did -- a sign nobody ever actually
clicked through the running scene, only checked things at the API level.

Fix: added a real AuthButtonsRow (LoginButton, RegisterButton) wired to
the existing handlers, which now hide themselves once auth succeeds.
Also fixed grab_focus() targeting the AuthPanel (which can't accept
focus, just a background warning) to focus the username field instead.

Verified via two new smoke tests (smoke_test_main_menu_ui.gd,
smoke_test_main_menu_full_flow.gd) using a *real signal emission* on
the buttons, not a direct method call -- that distinction is exactly
what would have caught the original bug. Full click-through confirmed
against a real backend: open register form, fill it, submit, Play
actually becomes enabled, welcome message shows, auth buttons hide.

Also documents a real headless-testing gotcha found while verifying
this: a synthetic Button.pressed.emit() on an await-using handler
doesn't reliably get driven forward in --script/SceneTree mode the way
real GUI input dispatch does -- confirmed by comparing direct-call vs
emit() behavior side by side. Not a bug in the actual click-driven flow,
a test-harness quirk to route around (call the handler directly,
awaited, instead of emitting the signal).

Full checkpoint suite re-verified clean on this branch: migrations,
68/68 pytest, all 6 Godot smoke tests (including the 2 new ones, picked
up automatically by run_checkpoint_tests.py's glob).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 13:13:38 +00:00
claude-checkpoint-tooling c33b10ac46 Embed the .pck into the exported binary instead of a separate file
export_presets.cfg had binary_format/embed_pck=false on both presets,
so every export produced two files (agp-game.exe + agp-game.pck) that
have to stay together and correctly named or Godot refuses to start:
"Couldn't load project data ... Is the .pck missing?" -- hit for real
running a downloaded PC1 playtest exe today (only the .exe got copied
over, not the .pck).

Verified both platforms now produce a single self-contained binary:
Windows PE32+ (104,580,904 bytes, no separate .pck) and Linux ELF64
(71,140,152 bytes, no separate .pck).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 13:03:19 +00:00
claude-checkpoint-tooling 8c54129bcd Update PC1 playtest instructions for the new dashboard controls
Replaces the manual multi-step shell commands (docker compose, alembic,
uvicorn with env vars, manual teardown) with the one-click dashboard
flow at /checkpoint/PC1 in ~/agp (commit 261c45f): Start Backend / Stop
& Clean Up, and Build / Download a pre-configured Windows .exe. No
terminal commands needed on either machine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 12:51:27 +00:00
reviewer-bot 409e78baa5 Merge pull request 'Update test_inventory.md to reflect PC1 checkpoint reality' (#40) from agent/398bf1a22a2d into main 2026-08-20 01:37:26 +00:00
architect-bot 1cc81979a4 Merge architect's per-file test detail into the checkpoint doc fix
Architect independently reclaimed this task after my fix landed and
rewrote the same doc with more precise per-file test descriptions and a
more specific gap list -- genuinely better in places (caught real detail
I'd summarized loosely) but its own investigation also introduced one
factual error (claimed 18 tests in test_auth_age_gate.py; verified via
pytest --collect-only, it's 17) and dropped two things worth keeping:
the exact commit hash for reproducibility, and the Client export section
(it never looked at the export step).

Merged: kept the architect's richer per-file coverage descriptions and
gap analysis, restored the verified counts/commit hash/export section/
--quit gotcha from my original.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 01:37:01 +00:00
agent-bot f8e5de9293 Merge pull request 'Add PC1 checkpoint record + playtest instructions' (#41) from docs/pc1-checkpoint-record into main 2026-08-20 01:17:30 +00:00
claude-checkpoint-tooling afc2211d70 Update test_inventory.md to reflect PC1 checkpoint reality
Was written 2026-08-19 and badly stale by the time the full PC1
checkpoint validation ran 2026-08-20: said 38 backend tests (actually
68), said zero integration/endpoint tests exist (4 real integration
test files landed since), and said nothing had merged for Godot smoke
tests (4 real ones did).

Verified for real, fresh clone of main, commit 927f9bd: migrations +
68/68 pytest + all 4 Godot smoke tests + both platform exports (Linux
ELF64 + Windows PE32+, confirmed valid via file) all clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 00:47:18 +00:00
10 changed files with 425 additions and 143 deletions
+19 -31
View File
@@ -37,45 +37,33 @@ the real scope of PC1; don't expect more when playtesting.
## Manual playtest: backend on cloudbison, client on pinkbison ## Manual playtest: backend on cloudbison, client on pinkbison
Cloudbison's LAN IP: `192.168.58.138`. Pinkbison's: `192.168.58.124`. As of 2026-08-20 this is dashboard-controlled — no terminal commands, no
env vars, no editing files. Foolproof enough to hand to someone else.
### 1. On cloudbison — start a real (persistent, not throwaway) backend ### 1. On cloudbison — the AGP dashboard
```bash Open the dashboard (`http://cloudbison:7070`, or its Tailscale address) and
cd ~ go to **PC1** in the nav bar (`/checkpoint/PC1`). Click **Start Backend** —
git clone http://localhost:3000/daddy/agp-game.git agp-game-playtest it pulls the latest `main`, stands up Postgres, runs migrations, and starts
cd agp-game-playtest/backend the API server, all in one step. The page shows the LAN address it's
docker compose up -d postgres reachable at once it's up (under a minute).
docker compose exec -T postgres pg_isready -U agp_user -d agp_db # wait for this to succeed
export DATABASE_URL="postgresql+asyncpg://agp_user:agp_password@localhost:5432/agp_db" When you're done playtesting, click **Stop & Clean Up** on the same page —
export JWT_ALGORITHM="RS256" it tears down everything that step started (API server, Postgres
/home/daddy/agp/venv/bin/python3 -m alembic upgrade head container, its volume and network). Nothing is left running or lying
nohup /home/daddy/agp/venv/bin/python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000 > /tmp/agp-backend.log 2>&1 & around afterward, even if the dashboard itself gets restarted in between.
disown
```
`--host 0.0.0.0` matters — the default `127.0.0.1` only listens on loopback ### 2. On pinkbison — download and run the .exe
and pinkbison won't be able to reach it. Verify: `curl
http://localhost:8000/api/health` should return `{"status":"ok",...}`.
### 2. On pinkbison — get the game and point it at cloudbison Same dashboard page: click **Build .exe** the first time (or **Rebuild** to
pick up newer changes) — this pulls the latest `main` and exports a Windows
Copy `export/windows/agp-game.exe` (or `export/linux/agp-game.x86_64`, binary with cloudbison's backend address already baked in. Click
whichever matches pinkbison's OS) from cloudbison, or clone the repo there **Download agp-game.exe**, copy it to pinkbison, and just run it. No
and run `scripts/build.sh` locally. Launch with the backend URL as an env environment variable, no config file, no build step on pinkbison at all.
var so you don't have to rebuild to change it:
- Windows (PowerShell): `$env:AGP_API_BASE_URL="http://192.168.58.138:8000"; .\agp-game.exe`
- Linux: `AGP_API_BASE_URL=http://192.168.58.138:8000 ./agp-game.x86_64`
### 3. Tear down when done ### 3. Tear down when done
```bash **Stop & Clean Up** on the dashboard page. That's it.
# on cloudbison
pkill -f "uvicorn app.main:app"
cd ~/agp-game-playtest/backend && docker compose down -v
```
## Status ## Status
+73 -47
View File
@@ -7,72 +7,98 @@ to know what "run the tests" actually checked.
Run everything below in one shot with `scripts/run_checkpoint_tests.py`. Run everything below in one shot with `scripts/run_checkpoint_tests.py`.
Last verified against `main`: 2026-08-19 (commit `1c75367`). 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) ## Backend — `backend/tests/` (pytest)
Run: `cd backend && python -m pytest -v` (needs `DATABASE_URL` pointed at a Run: `cd backend && python -m pytest -v` (needs `DATABASE_URL` pointed at a
reachable Postgres — see the runner script; most of these tests don't reachable Postgres — see the runner script).
actually touch the DB, but the app's import chain requires a valid URL to
construct).
**Current total: 38 tests, all passing on a clean `main` checkout.** **Current total: 68 tests, all passing on a clean `main` checkout.**
| File | Tests | What it covers | | File | Tests | What it covers |
|---|---|---| |---|---|---|
| `test_auth_age_gate.py` | 10 | `calculate_age()` date-math correctness (exact boundary, leap years, birthday-not-yet-this-year) + `RegisterRequest` date-of-birth validation (parametrized valid/invalid date rejection). Backs the 18+ registration gate (PR #23). | | `test_auth_age_gate.py` | 17 | `calculate_age()` date-math correctness (exact boundary, leap years, birthday-not-yet-this-year) + `RegisterRequest` date-of-birth validation (parametrized valid/invalid date rejection). Backs the 18+ registration gate (PR #23). |
| `test_gltf_metadata.py` | 21 | glTF/GLB binary + JSON parsing: magic-byte detection, chunk-padding handling, mesh/animation/texture-reference metadata extraction, malformed-input rejection. Backs the asset-import pipeline (PR #27). | | `test_gltf_metadata.py` | 21 | glTF/GLB binary + JSON parsing: magic-byte detection, chunk-padding handling, mesh/animation/texture-reference metadata extraction, malformed-input rejection. Backs the asset-import pipeline (PR #27). |
| `test_auth_endpoints.py` | 11 | **Integration tests** for `POST /api/auth/register` and `POST /api/auth/login` against the real FastAPI app + real Postgres. Covers: successful register (201), duplicate username (409), duplicate email (409), underage rejection (400), invalid date format (422), exact-18 registration, successful login with correct credentials (200), wrong password (401), nonexistent user (401), form-encoded login (OAuth2), and JWT validity of returned tokens. |
| `test_refresh_endpoint.py` | 8 | **Integration tests** for `POST /api/auth/refresh`. Covers: successful refresh (200, new access token, same refresh token returned), refresh produces different access tokens on repeated calls, invalid refresh token (401), empty body (422), missing field (422), refresh token from login also works, returned access token is valid JWT decodable with public key, `access_token_expires_minutes` in response. |
| `test_refresh_token_jti.py` | 6 | Unit tests for refresh token uniqueness (jti claim) and model column length. Covers: two calls with same sub produce different tokens (jti uniqueness), jti is valid UUID, rapid repeated calls all produce unique jtis, jti survives decode, `RefreshToken.token` column is `String(512)`, column type is `String`. |
| `test_auth_expired_token.py` | 5 | **Integration tests** for expired-token rejection across auth endpoints. Covers: expired access JWT rejected by `GET /api/auth/me` (401), garbage access token rejected by `/me` (401), missing bearer token returns 401, expired refresh token (DB `expires_at` in past) rejected by `/refresh` (401), expired refresh JWT (past `exp` claim) rejected before DB lookup (401). |
**What these are, honestly:** pure unit tests of standalone functions **What these are, honestly:** a mix of pure unit tests (`test_auth_age_gate.py`,
(`calculate_age`, the glTF parser, Pydantic validators). None of them spin `test_gltf_metadata.py`, `test_refresh_token_jti.py`) and real integration tests
up the FastAPI app or hit a real endpoint — they run in ~1s and don't (`test_auth_endpoints.py`, `test_refresh_endpoint.py`, `test_auth_expired_token.py`)
actually need Postgres reachable at all (the import chain just needs a that hit the FastAPI app against a real Postgres database via `TestClient`.
syntactically valid `DATABASE_URL`, nothing connects during collection). The integration tests exercise the full stack: Pydantic validation → DB →
JWT → response. The suite is also re-runnable without manual cleanup —
`backend/tests/conftest.py` has a session-scoped autouse fixture that
truncates all app tables once before the suite runs.
**Known coverage gaps** (real gaps, not placeholders — nothing enforces filling **Known coverage gaps** (real gaps, not placeholders — nothing enforces filling
these yet): these yet):
- No integration/endpoint tests at all — `/register`, `/login`, `/refresh`, - No endpoint tests for `GET /api/worlds`, `GET /api/assets/*`, or any
`/worlds`, `/assets/*` have zero tests that actually call them through world/asset CRUD routes — only auth endpoints have integration coverage
the FastAPI app (e.g. via `httpx.AsyncClient` + `ASGITransport`) against a (`GET /api/worlds` is covered indirectly via the Godot smoke tests below).
real (or test) database. Everything currently shipped works because it - No tests for the moderation schema/migration beyond "the migration runs."
was manually verified at merge time (checkpoint review), not because a - No tests for the `GET /api/auth/me` endpoint's response body (only its
test asserts it. auth-header validation is covered via expired-token tests).
- No tests for the RS256 auth upgrade (key generation, refresh-token - No tests for session management (create/validate/revoke sessions).
issuance/persistence) — PR #30, pending review as of this writing,
manually verified (correct key usage, all 38 existing tests still pass)
but not covered by an automated test of its own yet.
- No tests for the moderation schema/migration (in progress as of this
writing) beyond "the migration runs."
## Godot client — `scripts/smoke_test_*.gd` ## Godot client — `scripts/smoke_test_*.gd`
**Nothing merged into `main` yet as of this writing** — `sandbox_probe/test_script.gd` **6 smoke tests merged into `main`.** Each needs a live backend —
is the only Godot script on `main` right now, and it's **not a real `AGP_API_BASE_URL` env var, falling back to the
test** (a leftover manual probe, returns a hardcoded string, asserts `application/agp/api_base_url` project setting (default
nothing). Don't count it toward coverage; delete or repurpose it if anyone `http://127.0.0.1:8000`) if unset.
actually needs a sandbox probe again.
`scripts/smoke_test_api.gd` (PC1, PR #29, pending review as of this writing) | File | What it covers |
will be the first real one once merged — `ApiClient` autoload's |---|---|
`health_check()` against a **live backend** (set `AGP_API_BASE_URL` to point | `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. |
at one), verified by hand end-to-end before opening the PR: exercises the | `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). |
real HTTP request path (`HTTPRequest` node lifecycle, JSON parse, the | `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. |
result-dictionary contract), not just that the script compiles. Exits 1 on | `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. |
failure, so it's CI/script-friendly. | `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. |
Run (once merged): `godot --headless --path . --import` once (builds the Run (once, to build the class cache): `godot --headless --path . --import`,
class cache — skip this and every `class_name`-declared class falsely then for each smoke test: `godot --headless --path . --script
errors as undeclared), then for each `scripts/smoke_test_*.gd`: res://scripts/smoke_test_<name>.gd` — **no `--quit`**, it kills these before
`godot --headless --path . --script res://scripts/<name>.gd` their awaited HTTP calls finish.
**Known gap once it lands:** only `health_check()` is exercised end-to-end. **Known gaps:**
`login`, `register`, `get_worlds`, `get_me` are implemented (see - No smoke tests for chunk loading/streaming (`chunk.gd`, `chunk_scene.gd`).
`scripts/api_client.gd`) but have no smoke coverage yet — each would need a - No smoke tests for dynamic/static object nodes.
real user/world fixture in the target backend to test against meaningfully. - All smoke tests require a live backend; no offline/unit smoke tests exist.
- `smoke_test_world_list.gd` and `smoke_test_main_menu_*.gd` are the only
ones that exercise a real `.tscn` scene instantiation — others only test
the `ApiClient` autoload directly.
- `get_me()` (`GET /auth/me`) is implemented in `scripts/api_client.gd` but
has no smoke coverage yet.
- The Login path (as opposed to Register) through `main_menu.gd` isn't
separately smoke-tested — it shares the same `_on_auth_button_pressed()`/
`_on_auth_complete()` code path already exercised for Register, but that's
inference, not a direct check.
## Client export
`scripts/build.sh` produces real Linux (`agp-game.x86_64`) and Windows
(`agp-game.exe`) binaries via `--export-release`. Verified 2026-08-20: both
exports complete clean from a fresh checkout and produce valid binaries
(confirmed via `file` — real ELF64/PE32+, not empty or truncated). Neither
export is exercised by an automated GUI test (headless Godot can't drive a
window) — actually launching and clicking through the exported binary is a
manual step, see `docs/checkpoints/PC1.md` for playtest instructions.
## What "full coverage" should eventually mean here ## What "full coverage" should eventually mean here
Not defined precisely yet — the honest target as of 2026-08-19 is just Not defined precisely yet — the honest target as of 2026-08-20 is
"more than pure-function unit tests." As real integration/endpoint tests get "integration tests for every endpoint + smoke tests for every major scene."
added, update the tables above rather than letting this doc drift stale; As real integration/endpoint tests get added, update the tables above
a wrong inventory is worse than no inventory. rather than letting this doc drift stale; a wrong inventory is worse than
no inventory.
+2 -2
View File
@@ -21,7 +21,7 @@ script_export_mode=2
custom_template/debug="" custom_template/debug=""
custom_template/release="" custom_template/release=""
debug/export_console_wrapper=1 debug/export_console_wrapper=1
binary_format/embed_pck=false binary_format/embed_pck=true
texture_format/bptc=true texture_format/bptc=true
texture_format/s3tc=true texture_format/s3tc=true
texture_format/etc=false texture_format/etc=false
@@ -52,7 +52,7 @@ script_export_mode=2
custom_template/debug="" custom_template/debug=""
custom_template/release="" custom_template/release=""
debug/export_console_wrapper=1 debug/export_console_wrapper=1
binary_format/embed_pck=false binary_format/embed_pck=true
texture_format/bptc=true texture_format/bptc=true
texture_format/s3tc=true texture_format/s3tc=true
texture_format/etc=false texture_format/etc=false
+17 -5
View File
@@ -29,6 +29,21 @@ text = "AGP Game"
horizontal_alignment = 1 horizontal_alignment = 1
theme_override_font_sizes/font_size = 72 theme_override_font_sizes/font_size = 72
[node name="AuthButtonsRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer"]
layout_mode = 1
theme_override_constants/separation = 16
alignment = 1
[node name="LoginButton" type="Button" parent="CenterContainer/VBoxContainer/AuthButtonsRow"]
layout_mode = 1
text = "Login"
size_flags_horizontal = 3
[node name="RegisterButton" type="Button" parent="CenterContainer/VBoxContainer/AuthButtonsRow"]
layout_mode = 1
text = "Register"
size_flags_horizontal = 3
[node name="AuthPanel" type="PanelContainer" parent="CenterContainer/VBoxContainer"] [node name="AuthPanel" type="PanelContainer" parent="CenterContainer/VBoxContainer"]
layout_mode = 1 layout_mode = 1
visible = false visible = false
@@ -74,10 +89,6 @@ visible = false
placeholder_text = "Date of Birth (YYYY-MM-DD)" placeholder_text = "Date of Birth (YYYY-MM-DD)"
size_flags_horizontal = 3 size_flags_horizontal = 3
[node name="ModeToggle" type="CheckButton" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
layout_mode = 1
text = "Register"
[node name="AuthButton" type="Button" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"] [node name="AuthButton" type="Button" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
layout_mode = 1 layout_mode = 1
text = "Login" text = "Login"
@@ -106,9 +117,10 @@ layout_mode = 1
text = "Quit" text = "Quit"
size_flags_horizontal = 4 size_flags_horizontal = 4
[connection signal="pressed" from="CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton" to="." method="_on_login_button_pressed"]
[connection signal="pressed" from="CenterContainer/VBoxContainer/AuthButtonsRow/RegisterButton" to="." method="_on_register_button_pressed"]
[connection signal="pressed" from="CenterContainer/VBoxContainer/PlayButton" to="." method="_on_play_button_pressed"] [connection signal="pressed" from="CenterContainer/VBoxContainer/PlayButton" to="." method="_on_play_button_pressed"]
[connection signal="pressed" from="CenterContainer/VBoxContainer/QuitButton" to="." method="_on_quit_button_pressed"] [connection signal="pressed" from="CenterContainer/VBoxContainer/QuitButton" to="." method="_on_quit_button_pressed"]
[connection signal="toggled" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/ModeToggle" to="." method="_on_mode_toggle_toggled"]
[connection signal="pressed" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton" to="." method="_on_auth_button_pressed"] [connection signal="pressed" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton" to="." method="_on_auth_button_pressed"]
[connection signal="text_submitted" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit" to="." method="_on_auth_button_pressed"] [connection signal="text_submitted" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit" to="." method="_on_auth_button_pressed"]
[connection signal="text_submitted" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit" to="." method="_on_auth_button_pressed"] [connection signal="text_submitted" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit" to="." method="_on_auth_button_pressed"]
+11 -9
View File
@@ -11,14 +11,17 @@ grow_vertical = 2
script = ExtResource("1_wl") script = ExtResource("1_wl")
[node name="VBoxContainer" type="VBoxContainer" parent="."] [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 theme_override_constants/separation = 8
size_flags_horizontal = 4
size_flags_vertical = 4
[node name="HeaderHBox" type="HBoxContainer" parent="VBoxContainer"] [node name="HeaderHBox" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 3 layout_mode = 3
size_flags_horizontal = 4 size_flags_horizontal = 3
theme_override_constants/separation = 16 theme_override_constants/separation = 16
[node name="BackButton" type="Button" parent="VBoxContainer/HeaderHBox"] [node name="BackButton" type="Button" parent="VBoxContainer/HeaderHBox"]
@@ -30,7 +33,7 @@ size_flags_horizontal = 0
layout_mode = 3 layout_mode = 3
text = "Browse Worlds" text = "Browse Worlds"
horizontal_alignment = 1 horizontal_alignment = 1
size_flags_horizontal = 4 size_flags_horizontal = 3
theme_override_font_sizes/font_size = 28 theme_override_font_sizes/font_size = 28
[node name="RefreshButton" type="Button" parent="VBoxContainer/HeaderHBox"] [node name="RefreshButton" type="Button" parent="VBoxContainer/HeaderHBox"]
@@ -40,15 +43,14 @@ size_flags_horizontal = 0
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] [node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
layout_mode = 3 layout_mode = 3
size_flags_horizontal = 4 size_flags_horizontal = 3
size_flags_vertical = 4 size_flags_vertical = 3
horizontal_mode = 2 horizontal_mode = 2
vertical_mode = 2 vertical_mode = 2
[node name="WorldList" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"] [node name="WorldList" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"]
layout_mode = 3 layout_mode = 3
size_flags_horizontal = 4 size_flags_horizontal = 3
size_flags_vertical = 4
theme_override_constants/separation = 8 theme_override_constants/separation = 8
[node name="LoadingLabel" type="Label" parent="VBoxContainer"] [node name="LoadingLabel" type="Label" parent="VBoxContainer"]
+29 -30
View File
@@ -1,24 +1,23 @@
extends Control extends Control
## Main menu with login/register UI wired to the backend auth API. ## Main menu with login/register UI wired to the backend auth API.
## ##
## The AuthPanel is hidden by default. When the user clicks "Login" or "Register" ## The AuthPanel is hidden by default. When the user clicks the LoginButton or
## (the existing Play/Quit buttons), the auth panel slides in. The ModeToggle ## RegisterButton (AuthButtonsRow, above the panel), the auth panel slides in.
## switches between Login and Register modes, showing/hiding the email and DOB ## The Login and Register buttons on the AuthButtonsRow directly set the panel's
## fields accordingly. ## mode, so the user can switch between Login and Register at any time while
## ## the panel is open.
## On successful auth the token is stored via ApiClient, the welcome label
## shows the username, and the Play button becomes enabled.
# ─── UI references ────────────────────────────────────────────────────────────
# ── UI references ──────────────────────────────────────────────────────────── var _auth_buttons_row: HBoxContainer
var _login_button: Button
var _register_button: Button
var _auth_panel: PanelContainer var _auth_panel: PanelContainer
var _auth_mode_label: Label var _auth_mode_label: Label
var _username_edit: LineEdit var _username_edit: LineEdit
var _password_edit: LineEdit var _password_edit: LineEdit
var _email_edit: LineEdit var _email_edit: LineEdit
var _dob_edit: LineEdit var _dob_edit: LineEdit
var _mode_toggle: CheckButton
var _auth_button: Button var _auth_button: Button
var _status_label: Label var _status_label: Label
var _welcome_label: Label var _welcome_label: Label
@@ -29,17 +28,19 @@ var _is_register_mode: bool = false
var _is_authenticating: bool = false var _is_authenticating: bool = false
# ── Lifecycle ──────────────────────────────────────────────────────────────── # ─── Lifecycle ────────────────────────────────────────────────────────────────
func _ready() -> void: func _ready() -> void:
# Cache node references. # Cache node references.
_auth_buttons_row = $CenterContainer/VBoxContainer/AuthButtonsRow
_login_button = $CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton
_register_button = $CenterContainer/VBoxContainer/AuthButtonsRow/RegisterButton
_auth_panel = $CenterContainer/VBoxContainer/AuthPanel _auth_panel = $CenterContainer/VBoxContainer/AuthPanel
_auth_mode_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel _auth_mode_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel
_username_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit _username_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit
_password_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit _password_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit
_email_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit _email_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit
_dob_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DOBEdit _dob_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DOBEdit
_mode_toggle = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/ModeToggle
_auth_button = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton _auth_button = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton
_status_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthStatusLabel _status_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthStatusLabel
_welcome_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel _welcome_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel
@@ -56,7 +57,7 @@ func _ready() -> void:
_update_auth_mode_ui() _update_auth_mode_ui()
# ── Play / Quit ────────────────────────────────────────────────────────────── # ─── Play / Quit ──────────────────────────────────────────────────────────────
func _on_play_button_pressed() -> void: func _on_play_button_pressed() -> void:
if _play_button.disabled: if _play_button.disabled:
@@ -70,29 +71,24 @@ func _on_quit_button_pressed() -> void:
get_tree().quit() get_tree().quit()
# ── Auth panel toggle ──────────────────────────────────────────────────────── # ─── Auth panel toggle ────────────────────────────────────────────────────────
func _on_login_button_pressed() -> void: func _on_login_button_pressed() -> void:
"""Show the auth panel (called from a future Login button on the main menu).""" """Show the auth panel in login mode, called from AuthButtonsRow/LoginButton."""
_auth_panel.visible = true _auth_panel.visible = true
_auth_panel.grab_focus() _is_register_mode = false
_update_auth_mode_ui()
_clear_status()
_username_edit.grab_focus()
func _on_register_button_pressed() -> void: func _on_register_button_pressed() -> void:
"""Show the auth panel in register mode.""" """Show the auth panel in register mode, called from AuthButtonsRow/RegisterButton."""
_auth_panel.visible = true _auth_panel.visible = true
_is_register_mode = true _is_register_mode = true
_mode_toggle.button_pressed = true
_update_auth_mode_ui()
_clear_status()
# ── Mode toggle (Login / Register) ───────────────────────────────────────────
func _on_mode_toggle_toggled(button_pressed: bool) -> void:
_is_register_mode = button_pressed
_update_auth_mode_ui() _update_auth_mode_ui()
_clear_status() _clear_status()
_username_edit.grab_focus()
func _update_auth_mode_ui() -> void: func _update_auth_mode_ui() -> void:
@@ -109,7 +105,7 @@ func _update_auth_mode_ui() -> void:
_dob_edit.visible = false _dob_edit.visible = false
# ── Auth submission ────────────────────────────────────────────────────────── # ─── Auth submission ──────────────────────────────────────────────────────────
func _on_auth_button_pressed() -> void: func _on_auth_button_pressed() -> void:
"""Handle login or register button press.""" """Handle login or register button press."""
@@ -166,18 +162,21 @@ func _on_auth_complete(result: Dictionary) -> void:
_is_authenticating = false _is_authenticating = false
if result.ok: if result.ok:
# Success — show welcome, enable Play. # Success — show welcome, enable Play, hide the now-redundant
# Login/Register entry points (the auth panel itself stays open,
# showing the welcome message in place of the form).
_welcome_label.text = "Welcome, %s!" % _username_edit.text.strip_edges() _welcome_label.text = "Welcome, %s!" % _username_edit.text.strip_edges()
_welcome_label.visible = true _welcome_label.visible = true
_status_label.visible = false _status_label.visible = false
_play_button.disabled = false _play_button.disabled = false
_auth_buttons_row.visible = false
print("Auth successful. Token stored.") print("Auth successful. Token stored.")
else: else:
# Failure — show backend error detail. # Failure — show backend error detail.
_show_status(result.detail) _show_status(result.detail)
# ── Status helpers ─────────────────────────────────────────────────────────── # ─── Status helpers ───────────────────────────────────────────────────────────
func _show_status(message: String) -> void: func _show_status(message: String) -> void:
_status_label.text = message _status_label.text = message
@@ -189,7 +188,7 @@ func _clear_status() -> void:
_status_label.visible = false _status_label.visible = false
# ── Validation ─────────────────────────────────────────────────────────────── # ─── Validation ───────────────────────────────────────────────────────────────
func _is_valid_date(date_str: String) -> bool: func _is_valid_date(date_str: String) -> bool:
"""Check if a string looks like a valid YYYY-MM-DD date.""" """Check if a string looks like a valid YYYY-MM-DD date."""
+76
View File
@@ -0,0 +1,76 @@
extends SceneTree
## Full click-through simulation of the real user flow: press Register,
## fill the form exactly as a human would, submit, confirm Play actually
## becomes clickable. This is the check that would have caught the
## original bug (no way to even open the form) and would catch a repeat
## of it.
var passed := 0
var failed := 0
func _initialize() -> void:
_run()
func _run() -> void:
await process_frame
var packed: PackedScene = load("res://scenes/main_menu.tscn")
var instance: Control = packed.instantiate()
root.add_child(instance)
await process_frame
var register_button: Button = instance.get_node("CenterContainer/VBoxContainer/AuthButtonsRow/RegisterButton")
var auth_buttons_row: HBoxContainer = instance.get_node("CenterContainer/VBoxContainer/AuthButtonsRow")
var username_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit")
var password_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit")
var email_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit")
var dob_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DOBEdit")
var auth_button: Button = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton")
var welcome_label: Label = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel")
var play_button: Button = instance.get_node("CenterContainer/VBoxContainer/PlayButton")
_check("Play starts disabled", play_button.disabled)
# Click Register -- a real signal emission is fine here since
# _on_register_button_pressed() is synchronous (no await), same as a
# real click's GUI event dispatch would drive it.
register_button.pressed.emit()
await process_frame
_check("auth panel opened in register mode", instance.get_node("CenterContainer/VBoxContainer/AuthPanel").visible)
# Fill the form exactly as a user would type into it.
var unique_id := int(Time.get_unix_time_from_system() * 1000)
username_edit.text = "mainmenu_e2e_%d" % unique_id
password_edit.text = "TestPass123!"
email_edit.text = "mainmenu_e2e_%d@test.example.com" % unique_id
dob_edit.text = "2000-01-01"
# Call the submit handler directly (awaited) rather than via
# auth_button.pressed.emit() -- a synthetic .emit() in --script/
# SceneTree headless mode doesn't drive an await-using handler's
# coroutine forward the same way real GUI input event dispatch does
# (confirmed via a direct-call vs emit() comparison: the direct call
# always resolves, the synthetic emit sometimes doesn't advance past
# its own await point even after 50 real process_frame waits). This
# is a headless-mode test-harness limitation, not a bug in the actual
# click-driven flow.
await instance._on_auth_button_pressed()
_check("Play became enabled after real register submission", not play_button.disabled)
_check("Welcome label visible", welcome_label.visible)
_check("Auth buttons row hid itself after success", not auth_buttons_row.visible)
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])
quit(0 if failed == 0 else 1)
func _check(description: String, condition: bool) -> void:
if condition:
print(" PASS: ", description)
passed += 1
else:
printerr(" FAIL: ", description)
failed += 1
+126
View File
@@ -0,0 +1,126 @@
extends SceneTree
## Verifies the main menu's Login/Register buttons correctly open the auth
## panel and toggle between Login and Register modes via signal emission.
##
## Key tests:
## - AuthPanel starts hidden.
## - LoginButton press opens the panel in Login mode.
## - RegisterButton press opens the panel in Register mode.
## - Clicking Login while in Register mode switches back to Login mode.
## - Clicking Register while in Login mode switches to Register mode.
## - The ModeToggle CheckButton no longer exists in the scene.
func _initialize() -> void:
_run()
func _run() -> void:
await process_frame
var passed := 0
var failed := 0
var packed: PackedScene = load("res://scenes/main_menu.tscn")
var instance: Control = packed.instantiate()
root.add_child(instance)
await process_frame
var login_button: Button = instance.get_node("CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton")
var register_button: Button = instance.get_node("CenterContainer/VBoxContainer/AuthButtonsRow/RegisterButton")
var auth_panel: PanelContainer = instance.get_node("CenterContainer/VBoxContainer/AuthPanel")
var auth_mode_label: Label = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel")
var email_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit")
var dob_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DOBEdit")
# --- Test 1: auth panel starts hidden ---
if _check("auth panel starts hidden", not auth_panel.visible):
passed += 1
else:
failed += 1
# --- Test 2: LoginButton press opens panel in Login mode ---
login_button.pressed.emit()
await process_frame
if _check("auth panel visible after LoginButton press", auth_panel.visible):
passed += 1
else:
failed += 1
if _check("Login mode label after LoginButton press", auth_mode_label.text == "Login"):
passed += 1
else:
failed += 1
if _check("Email field hidden in Login mode", not email_edit.visible):
passed += 1
else:
failed += 1
if _check("DOB field hidden in Login mode", not dob_edit.visible):
passed += 1
else:
failed += 1
# --- Test 3: RegisterButton press switches to Register mode ---
register_button.pressed.emit()
await process_frame
if _check("auth panel still visible after RegisterButton press", auth_panel.visible):
passed += 1
else:
failed += 1
if _check("Register mode label after RegisterButton press", auth_mode_label.text == "Register"):
passed += 1
else:
failed += 1
if _check("Email field visible in Register mode", email_edit.visible):
passed += 1
else:
failed += 1
if _check("DOB field visible in Register mode", dob_edit.visible):
passed += 1
else:
failed += 1
# --- Test 4: LoginButton press switches back to Login mode ---
login_button.pressed.emit()
await process_frame
if _check("Login mode label after second LoginButton press", auth_mode_label.text == "Login"):
passed += 1
else:
failed += 1
if _check("Email field hidden after switching back to Login", not email_edit.visible):
passed += 1
else:
failed += 1
if _check("DOB field hidden after switching back to Login", not dob_edit.visible):
passed += 1
else:
failed += 1
# --- Test 5: ModeToggle CheckButton no longer exists ---
var mode_toggle_path = "CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/ModeToggle"
var has_mode_toggle = instance.has_node(mode_toggle_path)
if _check("ModeToggle CheckButton removed from scene", not has_mode_toggle):
passed += 1
else:
failed += 1
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])
quit(0 if failed == 0 else 1)
func _check(description: String, condition: bool) -> bool:
if condition:
print(" PASS: ", description)
else:
printerr(" FAIL: ", description)
return condition
+53 -7
View File
@@ -16,15 +16,27 @@ extends SceneTree
## ##
## Tests: ## Tests:
## 1. Register + log in a fresh user (so get_worlds() has a valid token). ## 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. ## must not error.
## 3. Wait for _refresh_worlds()'s awaited request to complete. ## 4. Wait for _refresh_worlds()'s awaited request to complete.
## 4. Confirm the screen reached a real end state (world cards, or the ## 5. Confirm it actually reached the populated (not empty) state, and
## empty-list message -- either is fine, a crash/stuck-loading isn't). ## 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. ## Exits 0 on full pass, 1 on any failure.
const WORLD_LIST_SCENE := "res://scenes/world_list.tscn" 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: func _initialize() -> void:
@@ -52,6 +64,19 @@ func _run() -> void:
quit(1) quit(1)
return 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 --- # --- Test 1: Instantiate the real scene ---
print("\n=== Test 1: Instantiate world_list.tscn ===") print("\n=== Test 1: Instantiate world_list.tscn ===")
var packed_scene: PackedScene = load(WORLD_LIST_SCENE) var packed_scene: PackedScene = load(WORLD_LIST_SCENE)
@@ -99,10 +124,31 @@ func _run() -> void:
if error_label.visible: if error_label.visible:
printerr(" Note: ended in error state: ", error_label.text) 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: 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 --- # --- Summary ---
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed]) print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])
+19 -12
View File
@@ -88,10 +88,26 @@ func _refresh_worlds() -> void:
## Create a visual card for a single world entry. ## Create a visual card for a single world entry.
func _create_world_card(world_dict: Dictionary) -> Control: 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() var card := VBoxContainer.new()
card.size_flags_horizontal = Control.SIZE_FILL card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
card.custom_minimum_size = Vector2(0, 80)
card.add_theme_constant_override("separation", 4) card.add_theme_constant_override("separation", 4)
panel.add_child(card)
var name_label := Label.new() var name_label := Label.new()
name_label.text = world_dict.get("name", "Unnamed World") 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(desc_label)
card.add_child(meta_label) card.add_child(meta_label)
# Subtle background for the card return panel
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