Concrete use cases
| Situation | Value |
|---|---|
| Login, configure, and save regression flow | Version a multi-step scenario as one document |
| Share a bug reproduction | Use stable IDs and roles instead of screen coordinates |
| Run a flow against Godot and Unity fixtures | Replay against the engine-independent IGuaContext |
| Enter a password | Store a secretKey, never plaintext |
Install
dotnet add package Gua.Testing.RecordingRecord a flow
using Gua.Testing.Recording;
var recorder = new GuaRecorder(host.Context);
await recorder.ClickAsync(new GuaRecordingTarget(Id: "open-login"));
var emailTarget = new GuaRecordingTarget(
Role: "textbox", Name: "Email", Scope: "login-dialog");
await recorder.SetValueAsync(
emailTarget,
"player@example.com",
waitCondition: GuaWaitConditions.Visible("email"));
var passwordTarget = new GuaRecordingTarget(Id: "password");
await recorder.SetValueAsync(
passwordTarget, password,
sensitive: true, secretKey: "login-password");
await recorder.FocusAsync(passwordTarget);
await recorder.PressKeyAsync("Enter");
GuaRecordingFile.Save("recordings/login.json", recorder.Recording);A step's waitCondition is evaluated before that step resolves its target and enqueues its action. Here the email step waits for the dialog's email node after the preceding click. SetValueAsync does not focus its target, so the password receives an explicit focus action before the untargeted PressKeyAsync sends Enter to the current focus. The recorder stores these actions, monotonic relative timing, revisions, and request IDs.
Choose stable targets
| Target | Use when | Rule |
|---|---|---|
Id | The game exposes a stable ID | Preferred; keep it stable through scene refactors |
Role + Name | An accessible name is unique | Add Scope when names repeat |
CurrentFocus | Sending a key to current focus | Valid only for press_key |
| Coordinate fallback | Importing legacy data | Rejected by default |
Prefer conditions to fixed delays
With the default PreferConditions mode, a step with a condition waits for semantic state instead of repeating its recorded delay. Conditions include visible, hidden, enabled, disabled, focused, unfocused, checked, unchecked, text, and value.
GuaWaitConditions.Visible("login-dialog")
GuaWaitConditions.Enabled("submit")
GuaWaitConditions.Text("status", "Ready")
GuaWaitConditions.Value("volume", "0.8")Replay with secret injection
var recording = GuaRecordingFile.Load("recordings/login.json");
var result = await GuaReplayer.ReplayAsync(
host.Context, recording, new GuaReplayOptions
{
TimingMode = GuaReplayTimingMode.PreferConditions,
SecretResolver = key => key == "login-password" ? password : null,
ActionTimeout = TimeSpan.FromSeconds(5),
PollInterval = TimeSpan.FromMilliseconds(50),
});A sensitive set_value stores only its key. Replay fails if SecretResolver cannot supply the value. Keep the resolver from writing secrets to logs or artifacts.
Replay safety boundaries
- Every semantic target is resolved against the current tree and must be unique.
- Every action waits for completion with the same request ID without consuming unrelated events.
- Coordinate fallback is rejected by default and requires an explicit caller-provided executor.
- Schema version, timing, revisions, action arguments, and secret leakage are validated on load.
Import retained diagnostics
GuaRecordingFile.ImportDiagnostics pairs enqueued operations with observed completion events by request ID. Its metadata reports paired requests, unpaired steps, and whether a legacy payload required synthetic timing.
var imported = GuaRecordingFile.ImportDiagnostics(context.GetDiagnosticsJson());
if (imported.UnpairedStepCount != 0)
throw new InvalidOperationException("Diagnostics contains unpaired operations.");
GuaRecordingFile.Save("recordings/imported.json", imported.Recording);