godot-e2e — E2E Testing for Godot
$ARGUMENTS
godot-e2e is a custom framework with zero LLM training data coverage.
Everything the model needs is in this skill (with deeper detail in
references/). Do not guess — follow these docs exactly.
Architecture
The godot-e2e CLI launches a Godot process and communicates over TCP
(localhost). Enabling the GodotE2E plugin in Project Settings
auto-registers an AutomationServer autoload that receives JSON
commands, executes them on the main thread, and sends back results.
The game runs unmodified — the server is dormant unless launched with
--e2e. Multiple instances can run in parallel (each auto-allocates a
unique port). The framework rests on three pillars: Locator for
semantic node queries, expect() for auto-retry assertions, and
engine log capture so every error carries the Godot logs that
preceded it.
Quick Start — conftest.py + Test File
# conftest.py (per test directory — explicit project path control;
# alternatively set GODOT_E2E_PROJECT_PATH env or pytest.ini
# `godot_e2e_project_path` and use the auto-registered `game` fixture).
# Replace "/root/Main" below with your project's entry-scene root —
# read it from `project.godot`'s `run/main_scene`.
import os
import pytest
from godot_e2e import GodotE2E
GODOT_PROJECT = os.path.join(os.path.dirname(__file__), "..")
GODOT_CONFIG = os.path.join(GODOT_PROJECT, ".claude", "godotmaker.yaml")
def _read_godot_path():
try:
with open(GODOT_CONFIG, "r", encoding="utf-8") as f:
for line in f:
line = line.split("#", 1)[0].strip()
if line.startswith("godot_path:"):
value = line.split(":", 1)[1].strip().strip("\"'")
return value or None
except OSError:
return None
return None
GODOT_PATH = _read_godot_path()
@pytest.fixture(scope="module")
def _game_process():
with GodotE2E.launch(
GODOT_PROJECT,
godot_path=GODOT_PATH,
timeout=15.0,
) as game:
game.wait_for_node("/root/Main", timeout=10.0)
yield game
@pytest.fixture(scope="function")
def game(_game_process):
_game_process.reload_scene()
_game_process.wait_for_node("/root/Main", timeout=5.0)
yield _game_process
# test_player.py
from godot_e2e import expect
def test_player_moves_right(game):
player = game.locator(group="player") # Locator query
initial_x = player.get_property("position:x")
game.input_action("ui_right", True)
game.wait_physics_frames(10)
game.input_action("ui_right", False)
expect(player).to_satisfy(
lambda l: l.get_property("position:x") > initial_x,
description="player moved right",
)
def test_button_starts_game(game):
game.get_by_button("Start").click() # auto-waits actionability
expect(game.locator(name="GameStatus")).to_have_text("Playing")
errors = [e for e in game.collected_logs if e.level == "error"]
assert not errors, f"errors during click: {errors}"
godot-e2e e2e/ -v
API Quick Reference
Launch / Lifecycle
| Method |
Description |
GodotE2E.launch(project_path, godot_path=None, port=0, timeout=10.0, extra_args=None, log_verbosity=None) |
Context manager. Launch Godot + connect. port=0 auto-allocates. log_verbosity ∈ "error"/"warning"/"info". |
GodotE2E.connect(host="127.0.0.1", port=6008, token="") |
Connect to already-running Godot. |
game.close() |
Kill Godot process and close connection. |
Locator — Semantic Queries
Locator is lazy: queries re-resolve on every action, so a Locator
created before reload_scene() still works after.
| Constructor |
Description |
game.locator(path=, name=, group=, text=, type=, script=) |
At least one kwarg required; AND-composed. name / text accept glob (*, ?). type matches via is X (descendants included, e.g. type="BaseButton" covers Button/CheckBox). |
game.get_by_text(text) |
Sugar for locator(text=text). |
game.get_by_button(text) |
Sugar for locator(type="BaseButton", text=text). |
| Refinement |
Returns |
Description |
loc.filter(**kwargs) |
Locator |
Add AND-composed predicates. |
loc.first() / loc.nth(i) |
Locator |
Pick first / i-th match. |
loc.all() |
list[Locator] |
Snapshot of all matches; [] if none (no raise). |
loc.locator(**kwargs) |
Locator |
Sub-query under this Locator's resolved node (parent resolved at action time). |
| Inspection (no raise on miss) |
Returns |
loc.exists() / loc.count() |
bool / int |
loc.is_visible() / loc.is_actionable() |
bool (raises on multi-match / missing) |
| Action (re-resolves; requires exactly one match) |
Notes |
loc.click(*, force=False, timeout=5.0) |
Auto-waits actionability for Control (visible + mouse_filter + in viewport); Node2D only checks visibility. force=True skips check. Raises NotActionableError on timeout. |
loc.hover() |
Inject InputEventMouseMotion at node's screen position. |
loc.get_property(prop) / loc.set_property(prop, value) / loc.call(method, args=None) |
Same as game.* versions, but path-pinned. |
loc.wait_visible(*, timeout=5.0) |
Block until target passes actionability. Raises NotActionableError with reasons + checks. |
loc.wait_for_signal(signal_name, timeout=5.0) |
Block until resolved node emits signal. |
expect() — Auto-Retry Assertions
expect(locator, *, timeout=5.0, poll_interval=0.05) → LocatorAssertions
re-resolves the Locator on each poll. Lookup errors during polling
(NodeNotFoundError, MultipleMatchesError, CommandError) are
caught; node may appear / disambiguate later.
| Matcher |
Passes When |
to_have_property(name, value) |
locator.get_property(name) == value |
to_have_text(text) |
Target's text property equals (sugar for property "text"). |
to_be_visible() |
Visible in scene tree (Control / Node2D). |
to_exist() |
Query resolves to ≥1 node. |
to_satisfy(predicate, *, description=None) |
predicate(locator) truthy. Use description= for readable failure messages. |
ExpectationFailedError dual-inherits AssertionError → pytest
renders it as a regular assertion failure, with attributes actual,
observation_captured, matcher, scene_tree, last_error.
Engine Log Capture
| Member |
Description |
game.last_logs / game.collected_logs |
List of LogEntry. last_logs cleared each command; collected_logs cleared per test by built-in fixtures. |
game.reset_collected_logs() |
Manual narrowing — reset window before a sub-assertion. |
game.set_log_verbosity(level) / game.set_log_buffer_size(size) |
Runtime tuning. Levels: "error" / "warning" (default) / "info". |
LogEntry fields |
level / message / function / file / line (last three populated for engine errors only). |
Every GodotE2EError carries .logs |
List of LogEntry from the failing command's response. Empty when capture inactive. |
Pytest auto-includes captured godot logs section on failure (built-in
plugin, no setup required). Buffer overflow synthesizes a "<N entries dropped>" warning.
Raw-Path Operations
Direct on game — used when you have a stable known path and a
Locator query would just add ceremony (typically root-level
singletons, autoloads, the Main entry node). Node ops
(game.get_property / set_property / call / find_by_group /
query_nodes / get_tree / batch), input (input_action /
input_key / input_mouse_* / press_action / press_key /
click / click_node), waits (wait_physics_frames /
wait_process_frames / wait_seconds / wait_for_node /
wait_for_signal / wait_for_property), scenes (get_scene /
change_scene / reload_scene / screenshot).
Types & Exceptions
from godot_e2e import (
Vector2, Vector2i, Vector3, Vector3i, Rect2, Rect2i,
Color, Transform2D, NodePath,
LogEntry, LogVerbosity, parse_log_entries,
expect, Locator, LocatorAssertions,
)
| Exception |
When |
NodeNotFoundError |
Node path doesn't exist. |
TimeoutError |
wait_for_* exceeded. Has .scene_tree. |
ConnectionLostError |
Godot crashed or TCP dropped. |
CommandError |
Server returned an error. |
MultipleMatchesError |
Locator action without .first()/.nth()/.filter() matched >1. Has .paths. |
NotActionableError |
Locator.click() / wait_visible() timed out waiting for actionability. Has .path, .reasons, .checks. |
ExpectationFailedError |
expect(...) matcher exceeded timeout. Dual-inherits AssertionError. |
All inherit from GodotE2EError (which carries .logs).
Critical Rules
| # |
Rule |
Detail |
| 1 |
Physics frames for movement |
After input, use wait_physics_frames for position/collision assertions. wait_process_frames does NOT advance physics. |
| 2 |
Hold input for movement |
press_action only taps (~4 frames). For sustained movement: input_action(act, True) → wait_physics_frames(N) → input_action(act, False). |
| 3 |
input_action needs 2 args |
input_action("jump", True) not input_action("jump"). For tap, use press_action("jump"). |
| 4 |
Prefer expect() over manual wait + assert |
expect(locator).to_have_property(...) retries with structured failure context (scene_tree + last_error) and renders as a normal pytest assertion. Use wait_for_property only when working with raw paths and no Locator is in scope. |
| 5 |
Assert direction, not exact values |
assert new_x > initial_x not assert pos_x == 450.0. Physics varies per machine. |
| 6 |
wait_for_signal timing |
Listener registers on arrival — signals emitted before are missed. Use expect().to_have_property for state assertions. |
| 7 |
Locators with semantic queries beat hardcoded paths |
game.get_by_button("Start") / game.locator(group="player") survives tree restructuring; /root/Main/UI/Menu/StartButton doesn't. Reserve raw paths for unique top-level nodes. |
| 8 |
Read .logs on every E2E failure |
Every GodotE2EError carries .logs — what Godot printed during the failing command. pytest -v auto-includes captured godot logs on failure. Ignoring it doubles diagnosis time. |
| 9 |
Default log verbosity is "warning" |
push_error and push_warning are captured; print() is NOT. Bump to "info" (via log_verbosity="info" at launch or game.set_log_verbosity("info") at runtime) only when debugging — at info verbosity log buffer fills 4-10× faster. |
| 10 |
Use wait_seconds for Timer-gated waits |
wait_process_frames(N) counts frames, not seconds. Under headless uncapped FPS, wait_process_frames(120) finishes in well under 2s. Use wait_seconds(t) or expect() for any wait gated by a Timer or wall-clock seconds. |
Fixture Strategies
| Strategy |
Scope |
Speed |
Isolation |
Use when |
reload_scene |
module process + function reload |
Fast |
Good |
Default. Most tests. |
game_fresh |
function process |
Slow |
Maximum |
Tests that modify global/autoload state. |
session |
session process |
Fastest |
None |
Read-only tests, careful ordering. |
The pytest plugin auto-registers game (reload-based) and
game_fresh (process-per-test) — both reset collected_logs at test
entry and capture screenshots on failure to test_output/.
Running & Debugging
godot-e2e e2e/ -v # all tests
godot-e2e e2e/test_player.py -v # single file
godot-e2e --godot-path /path/to/godot tests/ -v # specific binary
- Engine log verbosity at launch:
GodotE2E.launch(path, log_verbosity="info") or --e2e-log-verbosity=info flag
- Server-side wire log:
extra_args=["--e2e-log"] (separate from engine log capture — this logs request/response traffic on the Godot side)
- TimeoutError diagnosis: exception has
.scene_tree
- Locator actionability diagnosis:
NotActionableError.reasons lists failed checks ("not_visible_in_tree", "mouse_filter_ignore", "outside_viewport", "unclickable_node_type")
- expect() failure context:
ExpectationFailedError.actual (last observed value) + .scene_tree + .last_error (last swallowed CommandError)
E2E Test Quality Standards
Every E2E test MUST meet these minimum requirements. Tests that fail
these criteria are rejected.
Minimum Per-Test Requirements
- At least 1 user action —
input_action, press_action, click, Locator.click(), or call that triggers gameplay
- At least 1 state-change assertion — verify a property CHANGED (not just that a node exists). Prefer
expect(...) matchers over manual assert get_property(...) == X.
- No pure existence tests —
node_exists / Locator.exists() may serve as a precondition, but NEVER as the only assertion
Bad vs Good Examples
# BAD — only checks existence, proves nothing about gameplay:
def test_player(game):
assert game.locator(group="player").exists()
# GOOD — verifies actual gameplay behavior with auto-retry:
def test_player_moves_right(game):
player = game.locator(group="player")
initial_x = player.get_property("position:x")
game.input_action("move_right", True)
game.wait_physics_frames(10)
game.input_action("move_right", False)
expect(player).to_satisfy(
lambda l: l.get_property("position:x") > initial_x,
description="player moved right",
)
Fixture Sync Rules
- Entry scene change → update
conftest.py: change wait_for_node path, add change_scene if needed
- Entity naming change → if helpers used hardcoded paths, update; if they used Locators with
group= / type= / text=, often no change needed
- New game state (e.g., menu before gameplay) → create a
game_playing fixture that navigates past menus to gameplay state
- Private → public methods → E2E
game.call() / Locator.call() cannot call _private() methods; any method called by E2E must be public
- After ANY structural change → run
godot-e2e e2e/ -v to catch broken fixtures immediately
Extended References
For full API details (every Locator method, every matcher's polling
semantics, all wire commands, full exception attribute lists, type
serialization tags):
→ Grep references/api-reference.md
For testing patterns (Locator-based UI recipes, expect() idioms,
log-driven diagnosis, keep-alive, pause handling, CI config, flaky
test mitigation):
→ Grep references/testing-patterns.md
1---2name: godot-e2e3description: Write and run E2E (end-to-end) game tests using the godot-e2e framework. Python controls a live Godot game over TCP — Locator-based semantic queries, expect() auto-retry assertions, and engine log capture make failures self-diagnosing. Use this skill whenever you need to: - Test actual gameplay: player movement, collisions, scoring, scene transitions - Verify UI interactions: button clicks, label text, menu navigation - Write integration tests that run the real game (not mocked unit tests) - Debug E2E test failures or set up E2E test infrastructure Triggers: "E2E test", "end-to-end test", "gameplay test", "test the game running", "simulate input", "test player movement", "test UI clicks", "godot-e2e", "integration test for game", "test scene transitions".4---56# godot-e2e — E2E Testing for Godot78$ARGUMENTS910godot-e2e is a custom framework with **zero LLM training data coverage**.11Everything the model needs is in this skill (with deeper detail in12`references/`). Do not guess — follow these docs exactly.1314## Architecture1516The `godot-e2e` CLI launches a Godot process and communicates over TCP17(localhost). Enabling the GodotE2E plugin in Project Settings18auto-registers an `AutomationServer` autoload that receives JSON19commands, executes them on the main thread, and sends back results.20The game runs unmodified — the server is dormant unless launched with21`--e2e`. Multiple instances can run in parallel (each auto-allocates a22unique port). The framework rests on three pillars: `Locator` for23semantic node queries, `expect()` for auto-retry assertions, and24engine log capture so every error carries the Godot logs that25preceded it.2627## Quick Start — conftest.py + Test File2829```python30# conftest.py (per test directory — explicit project path control;31# alternatively set GODOT_E2E_PROJECT_PATH env or pytest.ini32# `godot_e2e_project_path` and use the auto-registered `game` fixture).33# Replace "/root/Main" below with your project's entry-scene root —34# read it from `project.godot`'s `run/main_scene`.35import os3637import pytest38from godot_e2e import GodotE2E3940GODOT_PROJECT = os.path.join(os.path.dirname(__file__), "..")41GODOT_CONFIG = os.path.join(GODOT_PROJECT, ".claude", "godotmaker.yaml")424344def _read_godot_path():45 try:46 with open(GODOT_CONFIG, "r", encoding="utf-8") as f:47 for line in f:48 line = line.split("#", 1)[0].strip()49 if line.startswith("godot_path:"):50 value = line.split(":", 1)[1].strip().strip("\"'")51 return value or None52 except OSError:53 return None54 return None555657GODOT_PATH = _read_godot_path()5859@pytest.fixture(scope="module")60def _game_process():61 with GodotE2E.launch(62 GODOT_PROJECT,63 godot_path=GODOT_PATH,64 timeout=15.0,65 ) as game:66 game.wait_for_node("/root/Main", timeout=10.0)67 yield game6869@pytest.fixture(scope="function")70def game(_game_process):71 _game_process.reload_scene()72 _game_process.wait_for_node("/root/Main", timeout=5.0)73 yield _game_process74```7576```python77# test_player.py78from godot_e2e import expect7980def test_player_moves_right(game):81 player = game.locator(group="player") # Locator query82 initial_x = player.get_property("position:x")8384 game.input_action("ui_right", True)85 game.wait_physics_frames(10)86 game.input_action("ui_right", False)8788 expect(player).to_satisfy(89 lambda l: l.get_property("position:x") > initial_x,90 description="player moved right",91 )9293def test_button_starts_game(game):94 game.get_by_button("Start").click() # auto-waits actionability95 expect(game.locator(name="GameStatus")).to_have_text("Playing")96 errors = [e for e in game.collected_logs if e.level == "error"]97 assert not errors, f"errors during click: {errors}"98```99100```bash101godot-e2e e2e/ -v102```103104## API Quick Reference105106### Launch / Lifecycle107108| Method | Description |109|---|---|110| `GodotE2E.launch(project_path, godot_path=None, port=0, timeout=10.0, extra_args=None, log_verbosity=None)` | Context manager. Launch Godot + connect. `port=0` auto-allocates. `log_verbosity` ∈ `"error"`/`"warning"`/`"info"`. |111| `GodotE2E.connect(host="127.0.0.1", port=6008, token="")` | Connect to already-running Godot. |112| `game.close()` | Kill Godot process and close connection. |113114### Locator — Semantic Queries115116`Locator` is lazy: queries re-resolve on every action, so a Locator117created before `reload_scene()` still works after.118119| Constructor | Description |120|---|---|121| `game.locator(path=, name=, group=, text=, type=, script=)` | At least one kwarg required; AND-composed. `name` / `text` accept glob (`*`, `?`). `type` matches via `is X` (descendants included, e.g. `type="BaseButton"` covers `Button`/`CheckBox`). |122| `game.get_by_text(text)` | Sugar for `locator(text=text)`. |123| `game.get_by_button(text)` | Sugar for `locator(type="BaseButton", text=text)`. |124125| Refinement | Returns | Description |126|---|---|---|127| `loc.filter(**kwargs)` | `Locator` | Add AND-composed predicates. |128| `loc.first()` / `loc.nth(i)` | `Locator` | Pick first / i-th match. |129| `loc.all()` | `list[Locator]` | Snapshot of all matches; `[]` if none (no raise). |130| `loc.locator(**kwargs)` | `Locator` | Sub-query under this Locator's resolved node (parent resolved at action time). |131132| Inspection (no raise on miss) | Returns |133|---|---|134| `loc.exists()` / `loc.count()` | `bool` / `int` |135| `loc.is_visible()` / `loc.is_actionable()` | `bool` (raises on multi-match / missing) |136137| Action (re-resolves; requires exactly one match) | Notes |138|---|---|139| `loc.click(*, force=False, timeout=5.0)` | Auto-waits actionability for `Control` (visible + mouse_filter + in viewport); `Node2D` only checks visibility. `force=True` skips check. Raises `NotActionableError` on timeout. |140| `loc.hover()` | Inject `InputEventMouseMotion` at node's screen position. |141| `loc.get_property(prop)` / `loc.set_property(prop, value)` / `loc.call(method, args=None)` | Same as `game.*` versions, but path-pinned. |142| `loc.wait_visible(*, timeout=5.0)` | Block until target passes actionability. Raises `NotActionableError` with `reasons` + `checks`. |143| `loc.wait_for_signal(signal_name, timeout=5.0)` | Block until resolved node emits signal. |144145### expect() — Auto-Retry Assertions146147`expect(locator, *, timeout=5.0, poll_interval=0.05) → LocatorAssertions`148re-resolves the Locator on each poll. Lookup errors during polling149(`NodeNotFoundError`, `MultipleMatchesError`, `CommandError`) are150caught; node may appear / disambiguate later.151152| Matcher | Passes When |153|---|---|154| `to_have_property(name, value)` | `locator.get_property(name) == value` |155| `to_have_text(text)` | Target's `text` property equals (sugar for property `"text"`). |156| `to_be_visible()` | Visible in scene tree (Control / Node2D). |157| `to_exist()` | Query resolves to ≥1 node. |158| `to_satisfy(predicate, *, description=None)` | `predicate(locator)` truthy. Use `description=` for readable failure messages. |159160`ExpectationFailedError` dual-inherits `AssertionError` → pytest161renders it as a regular assertion failure, with attributes `actual`,162`observation_captured`, `matcher`, `scene_tree`, `last_error`.163164### Engine Log Capture165166| Member | Description |167|---|---|168| `game.last_logs` / `game.collected_logs` | List of `LogEntry`. `last_logs` cleared each command; `collected_logs` cleared per test by built-in fixtures. |169| `game.reset_collected_logs()` | Manual narrowing — reset window before a sub-assertion. |170| `game.set_log_verbosity(level)` / `game.set_log_buffer_size(size)` | Runtime tuning. Levels: `"error"` / `"warning"` (default) / `"info"`. |171| `LogEntry` fields | `level` / `message` / `function` / `file` / `line` (last three populated for engine errors only). |172| **Every `GodotE2EError` carries `.logs`** | List of LogEntry from the failing command's response. Empty when capture inactive. |173174Pytest auto-includes `captured godot logs` section on failure (built-in175plugin, no setup required). Buffer overflow synthesizes a `"<N entries176dropped>"` warning.177178### Raw-Path Operations179180Direct on `game` — used when you have a stable known path and a181Locator query would just add ceremony (typically root-level182singletons, autoloads, the `Main` entry node). Node ops183(`game.get_property` / `set_property` / `call` / `find_by_group` /184`query_nodes` / `get_tree` / `batch`), input (`input_action` /185`input_key` / `input_mouse_*` / `press_action` / `press_key` /186`click` / `click_node`), waits (`wait_physics_frames` /187`wait_process_frames` / `wait_seconds` / `wait_for_node` /188`wait_for_signal` / `wait_for_property`), scenes (`get_scene` /189`change_scene` / `reload_scene` / `screenshot`).190191### Types & Exceptions192193```python194from godot_e2e import (195 Vector2, Vector2i, Vector3, Vector3i, Rect2, Rect2i,196 Color, Transform2D, NodePath,197 LogEntry, LogVerbosity, parse_log_entries,198 expect, Locator, LocatorAssertions,199)200```201202| Exception | When |203|---|---|204| `NodeNotFoundError` | Node path doesn't exist. |205| `TimeoutError` | `wait_for_*` exceeded. Has `.scene_tree`. |206| `ConnectionLostError` | Godot crashed or TCP dropped. |207| `CommandError` | Server returned an error. |208| `MultipleMatchesError` | Locator action without `.first()`/`.nth()`/`.filter()` matched >1. Has `.paths`. |209| `NotActionableError` | `Locator.click()` / `wait_visible()` timed out waiting for actionability. Has `.path`, `.reasons`, `.checks`. |210| `ExpectationFailedError` | `expect(...)` matcher exceeded timeout. Dual-inherits `AssertionError`. |211212All inherit from `GodotE2EError` (which carries `.logs`).213214## Critical Rules215216| # | Rule | Detail |217|---|------|--------|218| 1 | **Physics frames for movement** | After input, use `wait_physics_frames` for position/collision assertions. `wait_process_frames` does NOT advance physics. |219| 2 | **Hold input for movement** | `press_action` only taps (~4 frames). For sustained movement: `input_action(act, True)` → `wait_physics_frames(N)` → `input_action(act, False)`. |220| 3 | **`input_action` needs 2 args** | `input_action("jump", True)` not `input_action("jump")`. For tap, use `press_action("jump")`. |221| 4 | **Prefer `expect()` over manual wait + assert** | `expect(locator).to_have_property(...)` retries with structured failure context (scene_tree + last_error) and renders as a normal pytest assertion. Use `wait_for_property` only when working with raw paths and no Locator is in scope. |222| 5 | **Assert direction, not exact values** | `assert new_x > initial_x` not `assert pos_x == 450.0`. Physics varies per machine. |223| 6 | **`wait_for_signal` timing** | Listener registers on arrival — signals emitted before are missed. Use `expect().to_have_property` for state assertions. |224| 7 | **Locators with semantic queries beat hardcoded paths** | `game.get_by_button("Start")` / `game.locator(group="player")` survives tree restructuring; `/root/Main/UI/Menu/StartButton` doesn't. Reserve raw paths for unique top-level nodes. |225| 8 | **Read `.logs` on every E2E failure** | Every `GodotE2EError` carries `.logs` — what Godot printed during the failing command. `pytest -v` auto-includes `captured godot logs` on failure. Ignoring it doubles diagnosis time. |226| 9 | **Default log verbosity is `"warning"`** | `push_error` and `push_warning` are captured; `print()` is NOT. Bump to `"info"` (via `log_verbosity="info"` at launch or `game.set_log_verbosity("info")` at runtime) only when debugging — at info verbosity log buffer fills 4-10× faster. |227| 10 | **Use `wait_seconds` for Timer-gated waits** | `wait_process_frames(N)` counts frames, not seconds. Under headless uncapped FPS, `wait_process_frames(120)` finishes in well under 2s. Use `wait_seconds(t)` or `expect()` for any wait gated by a `Timer` or wall-clock seconds. |228229## Fixture Strategies230231| Strategy | Scope | Speed | Isolation | Use when |232|---|---|---|---|---|233| `reload_scene` | module process + function reload | Fast | Good | Default. Most tests. |234| `game_fresh` | function process | Slow | Maximum | Tests that modify global/autoload state. |235| `session` | session process | Fastest | None | Read-only tests, careful ordering. |236237The pytest plugin auto-registers `game` (reload-based) and238`game_fresh` (process-per-test) — both reset `collected_logs` at test239entry and capture screenshots on failure to `test_output/`.240241## Running & Debugging242243```bash244godot-e2e e2e/ -v # all tests245godot-e2e e2e/test_player.py -v # single file246godot-e2e --godot-path /path/to/godot tests/ -v # specific binary247```248249- **Engine log verbosity at launch**: `GodotE2E.launch(path, log_verbosity="info")` or `--e2e-log-verbosity=info` flag250- **Server-side wire log**: `extra_args=["--e2e-log"]` (separate from engine log capture — this logs request/response traffic on the Godot side)251- **TimeoutError diagnosis**: exception has `.scene_tree`252- **Locator actionability diagnosis**: `NotActionableError.reasons` lists failed checks (`"not_visible_in_tree"`, `"mouse_filter_ignore"`, `"outside_viewport"`, `"unclickable_node_type"`)253- **expect() failure context**: `ExpectationFailedError.actual` (last observed value) + `.scene_tree` + `.last_error` (last swallowed CommandError)254255## E2E Test Quality Standards256257Every E2E test MUST meet these minimum requirements. Tests that fail258these criteria are rejected.259260### Minimum Per-Test Requirements2612621. **At least 1 user action** — `input_action`, `press_action`, `click`, `Locator.click()`, or `call` that triggers gameplay2632. **At least 1 state-change assertion** — verify a property CHANGED (not just that a node exists). Prefer `expect(...)` matchers over manual `assert get_property(...) == X`.2643. **No pure existence tests** — `node_exists` / `Locator.exists()` may serve as a precondition, but NEVER as the only assertion265266### Bad vs Good Examples267268```python269# BAD — only checks existence, proves nothing about gameplay:270def test_player(game):271 assert game.locator(group="player").exists()272273# GOOD — verifies actual gameplay behavior with auto-retry:274def test_player_moves_right(game):275 player = game.locator(group="player")276 initial_x = player.get_property("position:x")277 game.input_action("move_right", True)278 game.wait_physics_frames(10)279 game.input_action("move_right", False)280 expect(player).to_satisfy(281 lambda l: l.get_property("position:x") > initial_x,282 description="player moved right",283 )284```285286## Fixture Sync Rules2872881. **Entry scene change** → update `conftest.py`: change `wait_for_node` path, add `change_scene` if needed2892. **Entity naming change** → if helpers used hardcoded paths, update; if they used Locators with `group=` / `type=` / `text=`, often no change needed2903. **New game state** (e.g., menu before gameplay) → create a `game_playing` fixture that navigates past menus to gameplay state2914. **Private → public methods** → E2E `game.call()` / `Locator.call()` cannot call `_private()` methods; any method called by E2E must be public2925. **After ANY structural change** → run `godot-e2e e2e/ -v` to catch broken fixtures immediately293294295## Extended References296297For full API details (every Locator method, every matcher's polling298semantics, all wire commands, full exception attribute lists, type299serialization tags):300→ Grep `references/api-reference.md`301302For testing patterns (Locator-based UI recipes, expect() idioms,303log-driven diagnosis, keep-alive, pause handling, CI config, flaky304test mitigation):305→ Grep `references/testing-patterns.md`