Unit testing with sdk-test-utils

MockBridge — drive the SDK from a test without a real platform.

@gamee/sdk-test-utils is a sister package that ships a MockBridge. Inject it in place of the real bridge and your tests can record outbound traffic, dictate platform responses, and emit platform → game events on demand. Works with Vitest, Jest, Mocha — anything that runs JS.

Install#

@gamee/sdk-test-utils ships from the same monorepo as @gamee/sdk and is not on npm yet. Build it locally and link it the same way you linked the SDK — see Install → option 1.

# Build alongside the SDK:
cd gamee-sdk
npm install
npm run build --workspace=@gamee/sdk
npm run build --workspace=@gamee/sdk-test-utils
// your-game/package.json
{
  "devDependencies": {
    "@gamee/sdk": "file:../gamee-sdk/packages/sdk",
    "@gamee/sdk-test-utils": "file:../gamee-sdk/packages/sdk-test-utils"
  }
}

The package has @gamee/sdk as a peer dependency — keep both at the same version. Once published, the install will be npm install --save-dev @gamee/sdk-test-utils.

Anatomy of a test#

import { describe, expect, test, beforeEach, afterEach } from 'vitest';
import { createSdk, type GameeSDK } from '@gamee/sdk';
import { MockBridge } from '@gamee/sdk-test-utils';

let mock: MockBridge;
let sdk: GameeSDK;

beforeEach(() => {
  // 1. New bridge + new SDK for every test — no shared state.
  mock = new MockBridge();
  sdk = createSdk({ bridge: mock });
});

afterEach(() => {
  sdk.dispose();
  mock.dispose();
});

test('ships score and ends the run', async () => {
  // 2. By default, every request is auto-acked with `null` data — perfect
  //    for fire-and-forget signals like updateScore / gameOver.
  await sdk.init({ capabilities: [] });

  sdk.updateScore({ score: 420, playTime: 12.3, checksum: 'sum' });
  sdk.gameOver({ saveStateData: { highScore: 420 } });

  // 3. Inspect the recorded traffic.
  const methods = mock.requests.map((r) => r.method);
  expect(methods).toEqual(['init', 'updateScore', 'gameOver']);

  const score = mock.requests.find((r) => r.method === 'updateScore');
  expect(score?.data).toMatchObject({ score: 420, gameChecksum: 'sum' });
});

Setting expectations#

Use expect(method).respondWith(...) to dictate the platform’s reply for a single RPC. Most useful for data-returning methods.

test('grants a reward when the ad plays', async () => {
  mock.expect('showRewardedVideo').respondWith({ videoPlayed: true });

  await sdk.init({ capabilities: ['rewardedAds'] });
  const result = await sdk.showRewardedVideo();

  expect(result.videoPlayed).toBe(true);
});

Other forms on the same builder:

mock.expect('purchaseItemWithGems').rejectWith({
  code: 'BRIDGE_ERROR',
  message: 'insufficient gems',
});

mock.expect('showRewardedVideo').leavePending(); // resolve later via respondTo()

Expectations are matched in FIFO order per method. They consume on first match; anything unmatched falls back to defaultResolution.

defaultResolution#

What to do with a request that has no matching expectation:

ModeEffect
'ack-null' (default)Auto-ack with data: null. Good for signals.
'leave-pending'Don’t respond at all. The Promise stays pending until you call respondTo() or hit the timeout.
'error'Respond with { code: 'NOT_EXPECTED', ... }. Use to enforce that every RPC has an expectation.
mock.defaultResolution = 'error'; // strict mode

Emitting platform → game events#

mock.emit(name, payload) reproduces the wire-level dispatch. The SDK auto-acks the message and then runs every on(name, ...) handler.

test('pauses the game loop on platform pause', async () => {
  await sdk.init({ capabilities: [] });

  let running = true;
  sdk.on('pause', () => {
    running = false;
  });

  mock.emit('pause', undefined);
  expect(running).toBe(false);
});

test('passes start payload through', async () => {
  await sdk.init({ capabilities: [] });

  let seed: string | undefined;
  sdk.on('start', (p) => {
    seed = p.gameSeed;
  });

  mock.emit('start', { gameSeed: 'abc-123' });
  expect(seed).toBe('abc-123');
});

Asserting validation / capability errors#

These throw synchronously on the SDK boundary — wrap in expect(...).toThrow, no await:

import { GameeError } from '@gamee/sdk';

test('rejects gameSaveState without saveState capability', async () => {
  await sdk.init({ capabilities: [] });

  expect(() => sdk.gameSaveState({ x: 1 })).toThrow(/CAPABILITY_MISSING|saveState/);
});

test('rejects oversized log event names', async () => {
  await sdk.init({ capabilities: ['logEvents'] });

  expect(() => sdk.logEvent({ name: 'x'.repeat(25), value: '' })).toThrow(GameeError);
});

For Promise-returning methods, use await expect(...).rejects.toThrow(...):

test('forwards platform errors as GameeError', async () => {
  mock.expect('showRewardedVideo').rejectWith({
    code: 'BRIDGE_ERROR',
    message: 'no fill',
  });

  await sdk.init({ capabilities: ['rewardedAds'] });
  await expect(sdk.showRewardedVideo()).rejects.toThrow('no fill');
});

Testing timeouts#

Combine leavePending with a small requestTimeoutMs:

test('rejects with BRIDGE_TIMEOUT when the platform is silent', async () => {
  sdk.dispose(); // throw away the default sdk
  sdk = createSdk({ bridge: mock, requestTimeoutMs: 50 });

  mock.expect('showRewardedVideo').leavePending();

  await sdk.init({ capabilities: ['rewardedAds'] });
  await expect(sdk.showRewardedVideo()).rejects.toMatchObject({
    code: 'BRIDGE_TIMEOUT',
  });
});

Late responses with respondTo#

For ad-hoc test choreography, settle a pending request manually:

test('drives the response from the test body', async () => {
  mock.expect('loadRewardedVideo').leavePending();

  await sdk.init({ capabilities: ['rewardedAds'] });
  const promise = sdk.loadRewardedVideo();

  // ...assert intermediate UI state here...

  mock.respondTo('loadRewardedVideo', { videoLoaded: true });
  await expect(promise).resolves.toEqual({ videoLoaded: true });
});

Other handy properties#

PropertyWhat it gives you
mock.requestsAll RPCs the SDK has sent (including init).
mock.ackedmessageIds of platform events the SDK acknowledged.
mock.platformThe platform the bridge advertises (default 'web').
mock.reset()Clear requests and any unconsumed expectations.
mock.dispose()Detach from the SDK. Safe to call repeatedly.

Constructor takes the platform string for tests that branch on it:

const iosMock = new MockBridge('ios');
expect(createSdk({ bridge: iosMock }).getPlatform()).toBe('ios');

Switch to a desktop

The GAMEE SDK emulator and docs are built for screens wider than 1280 px. Open this page on a laptop or desktop browser to get the full experience.

If you're already on a wide screen, drag your window wider and this banner will disappear.