Playwright tooling for Godot web exports.
This repository contains the Godot addon that emits test metadata from a web export. The gd-playwright-cli CLI and JavaScript helpers are separate companion repositories.
Use it when browser-based tests need stable names like start_button instead of hardcoded screen coordinates.
addon/: Godot addon that exposes browser events, element positions, viewport state, and test state.gd-playwright-cli: Rustgd-playwright-clihelper for querying exported games through CDP.gd-playwright-js: dependency-free Playwright helpers for live element resolution, confirmed actions, and retries.
gdam install @aviorstudio/gd-playwrightCopy addon/ into res://addons/@aviorstudio_gd-playwright/ and enable gd-playwright in Project Settings -> Plugins.
The plugin installs an autoload named PlaywrightService. You can also add autoload.gd manually as an autoload with that name.
const PlaywrightServiceModule = preload("res://addons/@aviorstudio_gd-playwright/src/playwright_service.gd")
func _ready() -> void:
PlaywrightService.configure(PlaywrightServiceModule.PlaywrightConfig.new(true, true, true, 1000))
PlaywrightService.emit_event("route_loaded", {"route": "home"})
PlaywrightService.set_test_state("menu", {"route": "home"})Add a visible PlaywrightTag child under any Control or Node2D that tests need to click or inspect:
StartButton
PlaywrightTag
tag_key = "start_button"
When the web export runs with gd-playwright enabled, the tag writes center-point positions to window.godotElements:
{
"start_button": { "x": 360, "y": 800, "w": 280, "h": 72, "visible": true }
}If your game wraps the addon service with its own autoload, set service_path on the helper nodes:
PlaywrightTag
tag_key = "start_button"
service_path = "/root/MyPlaywrightService"
Legacy metadata tags still work:
button.set_meta("playwright", "start_button")
PlaywrightService.scan_scene()The editor also includes tool menu actions:
gd-playwright: Scan Scene For Metadatagd-playwright: Convert Metadata To Tags
Use PlaywrightEventEmitter when a scene should author a named event in the Inspector:
HomeScreen
RouteLoadedEvent
script = PlaywrightEventEmitter
event_name = "route_loaded"
payload = { "route": "home" }
Game code can call the node when the event occurs:
$RouteLoadedEvent.emit_playwright_event({"screen": "HomeScreen"})Use PlaywrightStatePublisher for namespaced test-observable state:
GameScreen
GameStatePublisher
script = PlaywrightStatePublisher
state_namespace = "game"
$GameStatePublisher.publish({
"level": "level_01",
"solved": false
})An installable example scene is included at:
res://addons/@aviorstudio_gd-playwright/examples/app_shell/playwright_example_screen.tscn
For runtime-created objects, use the generic service APIs:
PlaywrightService.register_element("unit_0", center_pos, size, true)
PlaywrightService.unregister_element("unit_0")
PlaywrightService.emit_namespaced_event("combat", "turn_started", {"turn": 1})
PlaywrightService.set_state("combat", {"turn": 1})For dynamic objects, register positions manually:
var element_map = PlaywrightService.get_element_map()
if element_map:
element_map.register("unit_0", center_pos, size, true)Events are appended to window.godotEvents and dispatched as browser CustomEvent("godot-event") events:
PlaywrightService.emit_event("battle_started", {"round": 1})State is exposed at window.godotTestState.<namespace>:
PlaywrightService.set_test_state("puzzle", {"moves": 4, "solved": false})
PlaywrightService.clear_test_state("puzzle")Linux and macOS, x86_64 or arm64:
curl -fsSL https://raw.githubusercontent.com/aviorstudio/gd-playwright-cli/main/scripts/install.sh | shThe installer selects a published cli-v release, verifies the archive checksum and installs gd-playwright-cli into ~/.local/bin. Rust is not required to run the release binary. Use --version or --install-dir through sh -s -- for a specific release or destination.
Build the CLI from source in gd-playwright-cli, following that repository’s README.
# Open the game in a browser.
playwright-cli open http://localhost:3000 --headed
# See what the addon exposed. gd-playwright-cli discovers playwright-cli's CDP port.
gd-playwright-cli list --visible
# Get coordinates for an element.
gd-playwright-cli get start_button
# Click it via playwright-cli.
playwright-cli mousemove 360 640
playwright-cli mousedown
playwright-cli mouseup
# Wait for a new event.
gd-playwright-cli wait route_loadedMoving game objects should be resolved immediately before browser input. gd-playwright-cli remains read-only: these commands print one playwright-cli run-code command, and Playwright performs the input when the output is executed.
# Atomic browser-local click.
gd-playwright-cli get play_button --script | sh
# Retry a moving-target drag only when the expected fresh event is missing.
gd-playwright-cli script drag enemy_1 \
--to 1000,260 \
--expect-event enemy_released \
--filter id=1 \
--retries 3 | shEvery attempt re-reads window.godotElements, viewport scaling, and the canvas rectangle. Confirmation listeners are armed before input, and drag failures always attempt to release the mouse.
For sustained active-game tests, install the JavaScript helper package and import it in a Playwright test so decisions and input stay in one process:
import { dragElement } from "@aviorstudio/gd-playwright";
await dragElement(page, "enemy_1", {
to: { x: 1000, y: 260 },
confirm: { eventName: "enemy_released", filters: { id: 1 } },
retries: 3,
});Retries are not exactly-once delivery. Keep them disabled for non-idempotent actions unless the confirmation event uniquely identifies success.
| Command | Description |
|---|---|
get <key> [key2...] |
Get canvas-scaled center coordinates for elements. |
list |
List all registered element keys. |
status |
Check CDP connection and gd-playwright state. |
events |
Show recent game events from window.godotEvents. |
wait <event> |
Wait for a new event to appear. |
watch |
Stream events in real time. |
state |
Show aggregated state: test state, elements, viewport, and latest events by type. |
script click/drag |
Generate atomic Playwright actions with optional fresh-event confirmation and retries. |
gd-playwright-cli resolves its browser connection in this order:
--cdp ws://...--port <N>GDPW_CDPGDPW_PORT- Auto-discovery from default ports and local browser
--remote-debugging-portprocess arguments
Auto-discovery checks every page target and selects the one exposing gd-playwright globals. This avoids attaching to a stale non-game tab when multiple Playwright or Chrome targets exist. Explicit --cdp remains available when process inspection is unavailable or the browser is remote.
Settings use the gd_playwright/ prefix:
| Setting | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Force-enable event emission. |
test_mode |
bool | false |
Enable emission and element maps in non-debug builds. |
log_events |
bool | true |
Log emitted events in the browser console. |
event_buffer_max |
int | 1000 |
Max events in window.godotEvents; 0 means no limit. |
event_buffer_trim |
int | 500 |
Events kept after trimming; 0 means no trim. |
The Godot addon writes generic browser globals during enabled web runs:
window.godotElements: element keys mapped to{x, y, w, h, visible}in Godot viewport space.window.godotElementsViewport: viewport information used to scale coordinates to the canvas.window.godotEvents: buffered event records emitted by game code.window.godotTestState: namespaced test state dictionaries.
gd-playwright-cli reads those globals through CDP. It never mutates browser or game state and it never calls playwright-cli; it only provides data that another tool can use for input.
- Debug web builds retain the existing
enabled/test_modebehavior. Ordinary release exports never create gd-playwright browser globals, even when those settings are enabled. - Production diagnostics require a separate export preset with the custom feature
gd_playwright_diagnostics. ConfigurePlaywrightConfigwith aPlaywrightPayloadPolicy: exact element keys and prefixes, event-name to allowed-field rules, and state-namespace to allowed-field rules. Empty or missing rules deny publication. - Diagnostic release payloads must contain only JSON-safe values. Known credential-like keys (including
token,password,secret, cookies, session values, API keys, and private keys) are rejected recursively. This is a bounded guard, not a confidentiality or exhaustive secret-detection guarantee; games remain responsible for publishing only non-sensitive diagnostic data. - Validation walks each accepted payload once, handles primitive leaves without per-value frame dictionaries, and reuses repeated key classifications only for that publication. Element publication reuses internal wire views only while each entry's public fields remain unchanged; it still serializes a fresh full-map payload before publication. Enabled browser publication also reuses one fixed receiver per owner. These are internal safeguards only: publication remains synchronous, events retain order/detail, and element updates still replace the full map at the existing cadence.
- Calls are safe to leave in game code because disabled features no-op.
- Do not expose private player data through test state or event payloads.
- Game-specific knowledge belongs in game docs or skills, not in
gd-playwright-cli.
Example diagnostic release configuration:
var policy := PlaywrightServiceModule.PlaywrightPayloadPolicy.new(
PackedStringArray(["start_button"]),
PackedStringArray(["unit_"]),
{"route_loaded": PackedStringArray(["route"])},
{"fixture": PackedStringArray(["route", "ready"])}
)
PlaywrightService.configure(
PlaywrightServiceModule.PlaywrightConfig.new(true, true, false, 100, 50, policy)
)See LICENSE.
After cloning this repository, run the following with Python 3 before loading its skills, agents, or plugins:
python3 scripts/prepare-runtime.pyThis creates the native discovery files from the tracked functional inputs. Run it again after pulling an update; if a generated file has local edits, preserve those edits before replacing it.
Install mise, then prepare the pinned development helpers and run the complete native gate:
git clone https://github.com/nicodes/tools.git .ci-tools
git -C .ci-tools checkout --detach f7e243524ce2aa29f299e4c79db33001f72f4a30
mise install
mise run checkmise.toml owns tool versions and tasks. Engine and template downloads are checksum verified. mise run build packages the addon; mise run test runs its declared tests. The complete check also validates the installed artifact. Build and test receipts stay in .artifacts/verification.
Contributor tool versions and task entry points live in the native Mise configuration. Use mise run lint for source checks, mise run test for the applicable behavioral tests, and mise run check for the complete supported gate. mise run clean removes declared generated outputs while preserving source, credentials and retained data. This package has no separate Mise development service; use its native entry points directly.
Use a leading --json for finite queries, status, events, waiting and generated scripts, such as gd-playwright-cli --json status or gd-playwright-cli --json script click menuButton. Machine stdout contains one complete JSON document; diagnostics use stderr and failures exit nonzero. A single successful get keeps its coordinate object. Multi-key get returns one object containing elements, errors and ok; empty event results are [].
watch retains its event stream and rejects --json before connecting. Connection selection remains explicit --cdp/--port, environment values, then local discovery. Script generation remains offline and does not execute browser input.