Test Fixtures
In the previous guide on the Game Object Model (GOM), we successfully encapsulated game logic into reusable classes like our Steel object. However, instantiating those classes and spawning actors at the top of every single test quickly becomes repetitive. Fixtures are Playwright's dependency injection system, allowing you to automatically initialize and teardown your Game Objects.
The Problem with GOM
Take a look at the GOM test we wrote previously. Notice how much boilerplate is required just to set up the objects and spawn the actors before the test logic actually begins:
import { test, expect } from '@specter/test';
import { Steel } from '../objects/Steel';
import { KeepObject } from '../objects/KeepObject';
test('Hero destroys enemy keep', async ({ world }) => {
// We have to instantiate these in EVERY test...
const steel = new Steel(world);
const enemyKeep = new KeepObject(world, 2);
const steelActor = await steel.spawn({
location: { x: 497, y: 351, z: 100 },
rotation: { pitch: 0, yaw: 180, roll: 0 },
});
await steel.moveTo(steelActor, enemyKeep.locator);
await steelActor.triggerAbilityByTag(steel.abilityShieldBash);
await expect(enemyKeep.locator).toBeDestroyed();
});Creating Fixtures
Instead of relying on the base test object, we can extend it to automatically provide steelActor and enemyKeep to any test that requests them. We can even group our GOM classes into a global server object! Create a fixtures.ts file:
import { test as base, ActorLocator, SpecterWorld } from '@specter/test';
import { Steel } from '../objects/Steel';
import { KeepObject } from '../objects/KeepObject';
// 1. Declare the types of your fixtures
type MyFixtures = {
// A global object that holds all our game systems/factories
server: {
heroes: { steel: Steel }
};
steelActor: ActorLocator;
enemyKeep: KeepObject;
};
// 2. Extend the base test object with your custom fixtures
export const test = base.extend<MyFixtures>({
// Set up the 'server' systems fixture
server: async ({ world }, use) => {
await use({
heroes: { steel: new Steel(world) }
});
},
// Set up the steel actor using the factory method!
steelActor: async ({ server }, use) => {
const steelActor = await server.heroes.steel.spawn({
location: { x: 497.049522, y: 351.416008, z: 100.0 },
rotation: { pitch: 0, yaw: 180, roll: 0 },
});
// Yield the spawned actor to the test
await use(steelActor);
// Teardown phase (optional): Clean up after the test completes
// e.g. await steelActor.callFunction('Destroy');
},
// Define how 'enemyKeep' is created
enemyKeep: async ({ world }, use) => {
const keep = new KeepObject(world, 2);
await use(keep);
}
});
// Re-export expect so you only need to import from this file
export { expect } from '@specter/test';The Magic Result
Now, whenever you write a test, you import test from your new fixtures.ts file. If you ask for steelActor in the test parameters, Playwright automatically runs the setup logic, spawns the actor at the exact coordinates, and injects it!
// Import 'test' from our custom fixture file, NOT '@specter/test'!
import { test, expect } from '../fixtures';
test('Hero destroys enemy keep', async ({ server, steelActor, enemyKeep }) => {
// Notice we don't instantiate or spawn anything. Playwright handles it automatically!
// Move toward enemy keep using our GOM helper method
await server.heroes.steel.moveTo(steelActor, enemyKeep.locator);
// Use properties from our server GOM fixture, and the actor from our steelActor fixture!
await steelActor.triggerAbilityByTag(server.heroes.steel.abilityShieldBash);
await expect(enemyKeep.locator).toBeDestroyed();
});The test is now laser-focused purely on the behavior being tested, with zero boilerplate. Fixtures are lazy-loaded, meaning if a test doesn't request steelActor, the spawning logic won't run, saving execution time.