Skip to content
Automation
Skill

/godot-e2e

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

From plugin
godotmaker
51141 skills7 agents14 hooks
Install
$ npx -y skills add RandallLiuXin/GodotMaker --skill godot-e2e --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/godot-e2e

Context preview

The summary Claude sees to decide when to auto-load this skill.

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

SKILL.md

godot-e2e.SKILL.md
name: godot-e2e
description: |
  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".

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)

Read more
Ships withgodotmaker

Autonomous text-to-game pipeline for Godot, powered by Claude Code,Codex,Opencode

Get the whole plugin