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.RecordingAlign the game integration and related Gua package versions. See version management for recording and pinning the result.
Record 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);Replay a finite game-input segment
Ordinary ReplayAsync waits for each completion before the next step. For finite input offsets, explicitly use GuaReplayer.ReplayTimedSegmentAsync in Gua.Testing.Recording with an IGuaTimedSegmentHost. The supplied GuaRuntimeSegmentHost belongs to that same package and connects to a local Gua.Runtime with an initialized in-order game-input pump.
using Gua.Runtime;
using Gua.Testing.Recording;
// runtime is local; its initialized adapter applies game input through a FIFO pump.
var segmentHost = new GuaRuntimeSegmentHost(runtime, adapterAppliesInOrder: true);
var segment = new GuaTimedSegment(
SchemaVersion: 1, DurationMilliseconds: 300, MaxLatenessMilliseconds: 50,
ExecutionTimeoutMilliseconds: 1000, CleanupTimeoutMilliseconds: 500,
Inputs: [
new(0, GuaGameInputKind.Keyboard, GuaGameInputOperation.Down,
"KeyW", LeaseMilliseconds: 5000),
new(100, GuaGameInputKind.Keyboard, GuaGameInputOperation.Press, "Space"),
new(300, GuaGameInputKind.Keyboard, GuaGameInputOperation.Up, "KeyW"),
]);
var result = await GuaReplayer.ReplayTimedSegmentAsync(segmentHost, segment);
if (result.Outcome != GuaSegmentOutcome.Succeeded || !result.NeutralConfirmed)
throw new InvalidOperationException("Timed input or cleanup failed.");Offsets are relative to segment start. Set execution and cleanup budgets, close holds with explicit releases, and inspect both the outcome and neutral confirmation. The supplied host supports realtime FIFO only; it does not prove exact application times or same-tick application. Keep UI actions and condition waits outside the segment. Built-in MCP/Inspector replay stays sequential and does not offer Timed Segment execution. Plan save/load, import, and timing contract.
Attach a recording to Trace
GuaRecordingTrace.Attach explicitly attaches a validated recording to a Gua.Testing.GuaTraceSession step. AttachTimedResult adds value-free timing results. These are evidence attachments, not a guarantee of replayability; Trace does not replay them. Create a Trace · View saved evidence.