GGua REFERENCE
GODOT / TESTING

Testing UI

Launch a Godot process and connect to its live semantic UI tree. Locate controls by role, text, and ID, then wait for observable results instead of clicking coordinates.

1. Install the required NuGet packages

External Godot tests require all three packages: Gua.Core, Gua.Testing, and Gua.Testing.Godot. Run these commands in the test project directory to resolve the latest compatible stable packages, then commit the versions written to the project.

PowerShellpowershell
dotnet add package Gua.Core
dotnet add package Gua.Testing
dotnet add package Gua.Testing.Godot

# When using NUnit
dotnet add package Microsoft.NET.Test.Sdk
dotnet add package NUnit
dotnet add package NUnit3TestAdapter

Align the game integration and related Gua package versions. See version management for recording and pinning the result.

What each command adds

CodeMeaning
dotnet add package Gua.CoreAdds shared Gua types and the native runtime boundary.
dotnet add package Gua.TestingAdds semantic locators, actions, waits, assertions, and diagnostics.
dotnet add package Gua.Testing.GodotAdds the process host that starts Godot and connects to its bridge.
dotnet add package Microsoft.NET.Test.SdkLets dotnet test discover and run a test framework.
dotnet add package NUnitAdds NUnit tests, fixtures, and assertions.
dotnet add package NUnit3TestAdapterConnects NUnit discovery to Microsoft.NET.Test.Sdk.

If Godot is not on PATH, set its executable explicitly.

PowerShellpowershell
$env:GODOT_EXECUTABLE = "C:\path\to\Godot_v4.7-stable_mono_win64_console.exe"
dotnet test YourGame.Tests.csproj

What the run commands mean

CodeMeaning
$env:GODOT_EXECUTABLE = "...console.exe"Selects the Godot executable inherited by the test process and its child.
dotnet test YourGame.Tests.csprojBuilds the named test project and runs its discovered NUnit tests.

2. Launch and operate a scene semantically

TitleScreenTests.cscsharp
using Gua.Testing;
using Gua.Testing.Godot;

using var host = GodotSceneTestHost.Load(
    "res://Main.tscn",
    new GodotSceneTestHostOptions {
        ProjectPath = projectPath,
        UseAvailableBridgePort = true,
        StartupResetPolicy = GuaResetPolicy.Strict,
        TeardownResetPolicy = GuaResetPolicy.Strict,
    });

GuaAssertions.GetByRole(
    host.Context, "button", "Start Game"
).ToBeVisible();

await GuaAssertions.GetById(
    host.Context, "start"
).ClickAsync();

await GuaAssertions.WaitForTextAsync(
    host.Context, "loading", "Loading..."
);

Read the test in execution order

CodeMeaning
using Gua.Testing;Imports locators, actions, waits, and assertions.
using Gua.Testing.Godot;Imports the Godot process host and its options.
using var host = GodotSceneTestHost.Load(...)Starts Main.tscn in a child Godot process and waits for the bridge; using disposes both.
"res://Main.tscn"Selects the scene resource inside the Godot project.
ProjectPath = projectPathPoints the host at the directory containing project.godot.
UseAvailableBridgePort = trueSelects a loopback port instead of sharing fixed port 8765.
StartupResetPolicy = GuaResetPolicy.StrictFails rather than silently deleting dirty state at startup.
TeardownResetPolicy = GuaResetPolicy.StrictReports leaked requests or events at the end of the test.
GetByRole(..., "button", "Start Game")Finds a button by semantic role and accessible name, not coordinates.
.ToBeVisible()Asserts that the matched node is visible in the current snapshot.
GetById(..., "start").ClickAsync()Sends a click and waits for the request-correlated host completion.
WaitForTextAsync(..., "loading", "Loading...")Refreshes snapshots until node loading exposes the expected text.

3. Operate forms

Correlated asynchronous actionscsharp
var name = GuaAssertions.Query(host.Context)
    .ByRole("textbox").Within("LoginForm").Get();

await name.SetValueAsync("alice");
await name.FocusAsync();
await GuaAssertions.PressKeyAsync(host.Context, "Enter");
await GuaAssertions.GetById(host.Context, "RememberMe")
    .SetCheckedAsync(true);
await name.WaitForValueAsync("alice");

What each form operation does

CodeMeaning
GuaAssertions.Query(host.Context)Starts a semantic query against the connected UI tree.
.ByRole("textbox")Limits candidates to textbox nodes.
.Within("LoginForm")Searches only descendants of node LoginForm.
.Get()Requires exactly one match and reports zero or ambiguous results.
name.SetValueAsync("alice")Requests a value change and waits for host completion.
name.FocusAsync()Focuses the textbox so a global key action has a target.
PressKeyAsync(host.Context, "Enter")Sends an Enter key-down/key-up gesture to the focused Control.
GetById(..., "RememberMe").SetCheckedAsync(true)Finds the checkbox by stable ID and checks it.
name.WaitForValueAsync("alice")Observes the published value instead of treating action completion as state completion.

Four rules for reliable tests

Use an available port

Set UseAvailableBridgePort = true to avoid collisions in parallel runs.

Reset strictly

Detect leaked requests and events at startup and teardown.

Wait for state

Wait for nodes, values, or stable snapshots instead of sleeping for a fixed time.

Keep failure evidence

Capture the UI tree, logs, stdout/stderr, and optional PNG evidence.

1. Reserve a port per test

If every test uses 8765, only one Godot process can bind it during a parallel run. Let the host reserve a loopback port and pass it to the child as GUA_BRIDGE_PORT.

Launch on an available portcsharp
using var host = GodotSceneTestHost.Load(
    "res://Main.tscn",
    new GodotSceneTestHostOptions
    {
        ProjectPath = projectPath,
        UseAvailableBridgePort = true,
    });

// The reserved URL is assigned to the child and RemoteContext.

What the port options do

CodeMeaning
GodotSceneTestHost.Load("res://Main.tscn", ...)Starts the scene and returns a host connected to its Gua bridge.
ProjectPath = projectPathSelects the Godot project used to resolve the scene.
UseAvailableBridgePort = trueAsks the OS for an available port and passes it as GUA_BRIDGE_PORT.
using var hostCloses the connection and child process when the scope ends.

2. Reset strictly before and after the test

Do not carry nodes, requests, or events from one test into the next. A strict teardown reports unconsumed work instead of silently deleting it.

Strict isolationcsharp
using var host = GodotSceneTestHost.Load(
    "res://Main.tscn",
    new GodotSceneTestHostOptions
    {
        ProjectPath = projectPath,
        UseAvailableBridgePort = true,
        StartupResetPolicy = GuaResetPolicy.Strict,
        TeardownResetPolicy = GuaResetPolicy.Strict,
        CaptureDiagnosticsBeforeTeardown = true,
        CleanupAfterLeakReport = true,
    });

What strict isolation changes

CodeMeaning
StartupResetPolicy = GuaResetPolicy.StrictRejects dirty selected queues before the test starts.
TeardownResetPolicy = GuaResetPolicy.StrictReports unconsumed work as a test leak at teardown.
CaptureDiagnosticsBeforeTeardown = trueCaptures evidence before the process is destroyed when teardown fails.
CleanupAfterLeakReport = truePerforms cleanup only after preserving the leak report.

3. Wait for observed state, not elapsed time

Task.Delay(1000) wastes time on a fast machine and may still be too short on a slow one. Wait directly for the state that should follow the action.

State-based synchronizationcsharp
await GuaAssertions.GetById(host.Context, "start")
    .ClickAsync();

// Re-fetch snapshots until the node is visible.
await GuaAssertions.WaitForVisibleAsync(
    host.Context,
    "loading",
    timeout: TimeSpan.FromSeconds(3),
    pollInterval: TimeSpan.FromMilliseconds(20));

// Optionally require three unchanged rendered frames.
await GuaAssertions.WaitForStableSnapshotAsync(
    host.Context,
    stableFrames: 3);

What the waits guarantee

CodeMeaning
ClickAsync()Waits for the correlated click result, not the full screen transition.
WaitForVisibleAsync(..., "loading", ...)Polls fresh snapshots until loading is visible.
timeout: TimeSpan.FromSeconds(3)Fails if the condition is still false after three seconds.
pollInterval: TimeSpan.FromMilliseconds(20)Controls how often the state is checked.
WaitForStableSnapshotAsync(..., stableFrames: 3)Waits for three consecutive unchanged semantic frames.

4. Capture diagnostics when an assertion fails

Write the final UI tree, operation history, logs, and Godot stdout/stderr to artifacts/gua. In CI, upload that directory as a test artifact.

Enable failure diagnosticscsharp
using var diagnostics = host.CreateDiagnosticsSession(
    TestContext.CurrentContext.Test.FullName,
    outputDirectory: Path.Combine(
        TestContext.CurrentContext.WorkDirectory,
        "artifacts", "gua"));

using var assertionScope = GuaAssertionScope.Use(
    new GuaAssertionOptions
    {
        DiagnosticsSession = diagnostics,
    });

// A failure preserves the primary exception and writes diagnostics.
GuaAssertions.GetByRole(
    host.Context, "button", "Start Game"
).ToBeVisible();

What the diagnostics setup does

CodeMeaning
host.CreateDiagnosticsSession(...)Creates an evidence collector tied to this Godot host.
TestContext.CurrentContext.Test.FullNameUses the full NUnit test name to identify the artifact set.
Path.Combine(..., "artifacts", "gua")Builds a portable output path.
GuaAssertionScope.Use(...)Associates assertion failures in this scope with the diagnostics session.
DiagnosticsSession = diagnosticsSelects the UI tree, history, logs, and process evidence collector.
.ToBeVisible()Keeps the assertion as the primary failure even if evidence capture also fails.

With GitHub Actions, pass artifacts/gua/** to actions/upload-artifact after the test step so evidence remains available from a failed run.

Run tests with GitHub Actions

The example below installs the gua-v1.1.1 add-on. Use matching 1.1.1 NuGet packages in the test project. See CI setup and version selection.

The public gua-project/gua-tester/godot@v3.2 action installs the official Godot build matching its runner, links the selected released Gua add-on, sets GODOT_EXECUTABLE, and runs dotnet test.

.github/workflows/godot.ymlyaml
name: Godot Gua UI tests

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Gua UI tests
        uses: gua-project/gua-tester/godot@v3.2
        with:
          project-path: game
          test-project: tests/GuaTester.Tests.csproj
          godot-version: "4.7"
          gua-plugin-tag: gua-v1.1.1
          godot-status: stable

What each workflow section controls

CodeMeaning
on: pull_request / pushRuns the workflow for pull requests and pushes to main.
runs-on: windows-latestWindows is shown here; ubuntu-22.04 and macOS runners select their official Godot archive automatically.
actions/checkout@v4Checks out the game and test sources.
uses: gua-project/gua-tester/godot@v3.2Installs Godot, links the released add-on, and runs dotnet test.
project-path: gamePoints to the directory containing project.godot.
test-project: tests/GuaTester.Tests.csprojSelects the .NET test project to run.
godot-version / godot-statusPins the runner to Godot 4.7 stable.

Wait for actionable UI

Gua.Testing locators resolve a current strict single match when used. Actions wait for visible/enabled state and the advertised action, then correlated host completion. Multiple matches fail immediately. Resolution, enqueue, and completion share one timeout budget; rejected enqueue and host failure are not automatically retried.

Click, then wait for the next screencsharp
using Gua.Testing;

var play = GuaAssertions.Query(host.Context).ByRole("button", "Play");
await play.ClickAsync(timeout: TimeSpan.FromSeconds(5));
await GuaAssertions.Query(host.Context).ById("game-screen").ResolveAsync();

ResolveAsync waits for the next node to exist; add the state assertion your test requires. Locator API and timeout rules.

Compare semantic state without pixels

Add Gua.Testing.Snapshots to compare UI IDs, hierarchy, text, values, and state against reviewed JSON baselines. It works with host.Context, including a headless host. Add Gua.Testing.Visual separately when rendered pixels matter. Installation, masks, and baseline update example.

Add semantic comparisonpowershell
dotnet add package Gua.Testing.Snapshots

Save test evidence with Gua.Testing Trace

Trace belongs to Gua.Testing; no extra Trace package is needed. Enable it for a test scope, complete it with the test's actual outcome, and create an offline HTML report at a new output path.

Record and inspect a testcsharp
using Gua.Testing;

await using var trace = new GuaTraceSession(new() {
    OutputDirectory = "artifacts/traces",
    SavePolicy = GuaTraceSavePolicy.Always,
});
using var scope = GuaAssertionScope.Use(new() { Trace = trace });
try {
    await GuaAssertions.Query(host.Context).ById("play").ClickAsync();
    await trace.CompleteAsync(GuaTraceOutcome.Passed);
} catch {
    await trace.CompleteAsync(GuaTraceOutcome.Failed);
    throw;
}
var report = GuaTraceReport.WriteHtml(trace.ArtifactPath, "artifacts/report.html");
// Check report.Succeeded and trace.Status separately from the test result.

Trace records evidence; it does not replay the flow or change its result. Capture UI/World or Observe values explicitly when required, and handle sensitive data before recording. Trace capture APIs · Open a saved Trace in Inspector.