
AAA Game Automation.
Modern Developer Experience.
Bring modern software testing practices directly into Unreal Engine. Specter bridges UE5 directly to Node via WebSockets, empowering your SDETs to build robust, code-driven test suites in TypeScript from day one.
import { test, expect } from '@specter/test'
test("Q - Charge should move Steel forward and apply damage", async ({
server: {
heroes: { steel },
minion,
},
steelActor,
}) => {
const enemySpawnLocations: Vector3[] = [
{ x: -412.950478, y: 441.416008, z: 160.0 },
{ x: -412.950478, y: 231.416008, z: 160.0 },
{ x: -582.950478, y: 301.416008, z: 160.0 },
];
const enemies = await test.step("Spawn enemies", async () => {
return await Promise.all(
enemySpawnLocations.map((location) =>
minion.spawn({
theme: "paragon",
type: "range",
spawnOptions: {
location,
properties: { InitialTeamId: 2 },
rotation: { pitch: 0, yaw: -90, roll: 0 },
},
}),
),
);
});
const startLocation = await steelActor.getLocation();
const forwardDirection = await steelActor.getForwardVector();
await steelActor.triggerAbilityWithTarget(
steel.abilityCharge,
VectorUtils.addOffset(startLocation, forwardDirection, 50),
);
await test.step("Verify hit enemies recieve damage and knockback", async () => {
await Promise.all([
...enemies.flatMap((enemy, index) => [
expect(enemy).toHaveGameplayTag("Status.Stunned"),
expect(enemy).toHaveAttributeChange("HealthSet", "Health", {
delta: -100,
}),
expect(enemy).toHaveMovedFurtherThan(enemySpawnLocations[index], 100),
]),
]);
});
await expect(steelActor).toHaveMovedFurtherThan(startLocation, 1000);
});See It In Action
Watch how perfectly our Playwright game-object model syncs with native Unreal Engine execution.
90% Less Boilerplate.
100% Native Execution.
Say goodbye to brittle, massive C++ test harnesses. Specter introduces the Game Object Model—giving your SDETs strongly-typed access to native actors and abilities directly from TypeScript.
The Native Reality
For contrast, look at the code on the right. This is the exact same test written natively in Unreal's Gauntlet framework using C++.
Manual cast fetching, brittle latent command queuing, zero IntelliSense for objects... this is what costs studios millions of dollars in SDET velocity.
import { test, expect } from '@specter/test'
test("Q - Charge should move Steel forward and apply damage", async ({
server: {
heroes: { steel },
minion,
},
steelActor,
}) => {
const enemySpawnLocations: Vector3[] = [
{ x: -412.950478, y: 441.416008, z: 160.0 },
{ x: -412.950478, y: 231.416008, z: 160.0 },
{ x: -582.950478, y: 301.416008, z: 160.0 },
];
const enemies = await test.step("Spawn enemies", async () => {
return await Promise.all(
enemySpawnLocations.map((location) =>
minion.spawn({
theme: "paragon",
type: "range",
spawnOptions: {
location,
properties: { InitialTeamId: 2 },
rotation: { pitch: 0, yaw: -90, roll: 0 },
},
}),
),
);
});
const startLocation = await steelActor.getLocation();
const forwardDirection = await steelActor.getForwardVector();
await steelActor.triggerAbilityWithTarget(
steel.abilityCharge,
VectorUtils.addOffset(startLocation, forwardDirection, 50),
);
await test.step("Verify hit enemies recieve damage and knockback", async () => {
await Promise.all([
...enemies.flatMap((enemy, index) => [
expect(enemy).toHaveGameplayTag("Status.Stunned"),
expect(enemy).toHaveAttributeChange("HealthSet", "Health", {
delta: -100,
}),
expect(enemy).toHaveMovedFurtherThan(enemySpawnLocations[index], 100),
]),
]);
});
await expect(steelActor).toHaveMovedFurtherThan(startLocation, 1000);
});In-Engine Test Explorer & Playwright UI
No context switching. Specter includes a native Unreal Editor Utility Widget that embeds the Playwright test runner directly inside UE5. Run test suites, stream live frame telemetry, and debug step failures without ever leaving the editor.
