Events & Telemetry (waitForVariable)

world.waitForVariable() is a push-based, event-driven synchronization method in the Specter TypeScript SDK. Instead of repeatedly polling the engine over WebSocket, it pauses test execution until Unreal Engine pushes an instant telemetry event confirming that a tracked variable has transitioned to a specific target value.

What is world.waitForVariable()?

Traditional game test frameworks poll the game state every 100ms over RPCs or WebSockets. This introduces network overhead, creates flakiness, and misses microsecond-level state transitions.

With Specter's event-driven telemetry, the engine notifies your test runner the exact millisecond a state change occurs on the game thread.

Opt-In Performance Model

Unreal Engine has tens of thousands of UPROPERTY variables in memory at any given time. Broadcasting every single variable change by default would destroy CPU frame rates. Instead, you tell Specter which variable to monitor using actor.trackProperty() (or actor.trackAttribute() for GAS).

The Complete Workflow

A standard event-driven test workflow follows four clean steps:

import { test, expect } from '@specter-automation/test';

test('Wave Survival Match Flow', async ({ world }) => {
// 1. Tell Specter to monitor 'MatchState' on the GameState
const gameState = world.actor('GameStateBase');
const matchStateVar = await gameState.trackProperty('MatchState');

// 2. Also track the wave counter on the Spawner actor
const spawner = world.actor('BP_WaveSpawner');
const waveVar = await spawner.trackProperty('CurrentWave');

// 3. Trigger game flow
const menu = world.widget('WBP_MainMenu');
await menu.getByRole('button', { name: 'Start Match' }).click();

// 4. Now waitForVariable flawlessly catches the exact actor's event!
await world.waitForVariable(matchStateVar, 'InProgress');
await world.waitForVariable(waveVar, '5', 10000); // 10s timeout
});

Summary of Tracking Methods

Specter provides specialized tracking registration methods depending on what engine system you are monitoring:

MethodWhat It Monitors
actor.trackProperty('PropertyName')Any Blueprint / C++ UPROPERTY (integers, strings, enums, booleans).
actor.trackAttribute('Set', 'Attr')Any GAS Gameplay Attribute (e.g. Health, Mana, Shield on an AttributeSet).
world.waitForVariable('Name', 'Value', timeout?)Waits for any tracked property or attribute to transition to Value.

How trackProperty Connects to waitForVariable

Step 1: Test Clientactor.trackProperty('MatchState')Registers property to monitor
Step 2: C++ EngineUSpecterSubsystemListens on Game Thread & broadcasts FSpecterTimelineEvent
Step 3: Test Clientworld.waitForVariable(...)Promise resolves instantly with event metadata

1. Engine-Side Monitoring (USpecterSubsystem in C++)

  • Unreal Engine monitors only registered UPROPERTY variables.
  • When a variable value changes on the game thread, the engine immediately constructs an FSpecterTimelineEvent containing:
    eventType: "StandardVariable"
    eventName: "MatchState"
    oldValue: "WaitingToStart"
    newValue: "InProgress"
    timestamp: 14.285 // game world seconds
  • The subsystem broadcasts this event over WebSocket to all connected test clients.

2. Client-Side Event Listener (@specter-automation/test)

  • Calling await world.waitForVariable('MatchState', 'InProgress') registers a one-shot event listener on the WebSocket transport (GLOBAL_timeline-event).
  • The exact millisecond the engine broadcasts the change, the Promise resolves with the full SpecterTimelineEvent metadata.
  • If the variable does not reach the expected value before the timeout (default 5,000ms), it throws a descriptive timeout error.

Inspecting Timestamp Telemetry

waitForVariable returns the resolved event object, allowing you to extract precise engine timestamps for performance benchmarking and analytics:

const boss = world.actor('BP_Boss');
const bossDefeatedVar = await boss.trackProperty('bBossDefeated');

// Trigger combat damage
await player.triggerAbilityByTag('Ability.Attack.Ultimate');

const event = await world.waitForVariable(bossDefeatedVar, 'true', 30000);

console.log(`Boss defeated at engine timestamp: ${event.timestamp}s`);
console.log(`Event type: ${event.eventType}`); // "StandardVariable"

Event Telemetry vs Assertion Matchers

Many of Specter's expect(actor) matchers already use event-driven C++ delegates under the hood (for example, toHaveGameplayTag hooks directly into UAbilitySystemComponent tag delegates, and actor.waitForEvent listens to native Blueprint multicast delegates).

The primary distinction between world.waitForVariable() and expect() matchers lies in timeline telemetry vs test validation:

Capabilityworld.waitForVariable()expect(actor).toHave...()
Primary PurposeTimeline Synchronization & Telemetry (pauses until engine event broadcasts)Test Validation & Assertion (verifies state with rich diffs, tolerances, and soft assertions)
Under the HoodRegisters tracked property with USpecterSubsystemHybrid engine delegates (GAS, delegates) & auto-retrying fast-path queries
Output & Return ValueReturns full SpecterTimelineEvent with exact game timestampsResolves to void on pass; outputs detailed assertion failure diff on fail
Negation SupportN/A (waits for specific target value)Full expect().not... negation support
Best Used ForGlobal match phases, level milestones, wave counters, benchmark timestampsActor/component state assertions, transforms, health thresholds, visual checks