Custom Matchers
Specter provides a robust suite of built-in assertions, but as your project grows, you may want to create domain-specific matchers. By extending Playwright's expect object, you can encapsulate complex engine polling logic into clean, readable assertions tailored exactly to your game's mechanics.
The Goal
Suppose we are building a MOBA (like Heroes of the Storm) and we want to assert the current phase of the match. Instead of writing raw variable polling logic in every test:
// Repetitive and verbose
await world.waitForVariable("GameState.MatchPhase", "ObjectivesActive", 10000);We want to create a clean, custom matcher that reads naturally:
// Clean and domain-specific
await expect(world).toBeInGamePhase('ObjectivesActive');1. Extending the TypeScript Interface
First, we need to tell TypeScript about our new matcher using module augmentation. Create a matchers.d.ts file in your test directory:
import { SpecterWorld } from '@specter/test';
// Define the custom phases for our MOBA
type GamePhase = 'Drafting' | 'PreGame' | 'Laning' | 'ObjectivesActive' | 'CoreExposed' | 'GameOver';
declare global {
namespace PlaywrightTest {
interface Matchers<R> {
/**
* Polls the UE5 GameState until the specified match phase is reached.
*/
toBeInGamePhase(expectedPhase: GamePhase): Promise<R>;
}
}
}2. Implementing the Matcher Logic
Next, we write the actual implementation using expect.extend. We can leverage Specter's built-in polling tools like waitForVariable inside the matcher. Create a setup.ts file:
import { expect, SpecterWorld } from '@specter/test';
import type { GamePhase } from './matchers';
expect.extend({
async toBeInGamePhase(world: SpecterWorld, expectedPhase: GamePhase) {
try {
// Use Specter to poll the engine state
await world.waitForVariable("GameState.MatchPhase", expectedPhase, 15000);
return {
message: () => `expected GamePhase not to be '${expectedPhase}'`,
pass: true,
};
} catch (error) {
return {
message: () => `expected GamePhase to become '${expectedPhase}' within timeout, but it failed.`,
pass: false,
};
}
},
});3. Using Your Custom Matcher
Ensure your setup.ts is included in your Playwright config's setup files. Now, you can use your domain-specific assertion in any test!
import { test, expect } from '@specter/test';
test('Objective spawns correctly during ObjectivesActive phase', async ({ world }) => {
// Wait for the match to transition out of Laning
await expect(world).toBeInGamePhase('ObjectivesActive');
// Now verify the objective actor spawned
const tribute = world.actor('BP_RavenTribute');
await expect(tribute).toBeVisible();
});