---
name: pixi-vn-testing
description: Use when an AI agent (or any external script) needs to play-test a running Pixi'VN game end-to-end in a real browser — starting the game, advancing/branching the story, answering input prompts, going back, and reading/writing storage — via `Game.testing`, the opt-in devtools bridge exposed on `window`. Load this before wiring up automated/agent-driven playtesting, not for authoring story content itself (see `pixi-vn-narration`).
---

# Pixi'VN Testing module

## When to use this skill

Use this skill when the task is to **play the game from the outside** — driving it like a player
would, but from a script or an AI agent evaluating JavaScript in the page, instead of clicking UI. Typical
asks this covers: "start the game and play through label X", "verify choice B leads to ending Y",
"check that this bug repros after going back three steps", "seed storage with `gold: 100` and confirm the
shop unlocks", "run this game headless as a regression test".

This assumes the game's dev server (or a build served on a URL) and a browser are already
running/reachable — starting either one is outside this skill; it only covers the game-control API
itself. Do not use this skill for writing story content (`pixi-vn-narration`), save-file persistence
(`pixi-vn-saves`), or the go-back UI/internals (`pixi-vn-history`) — those are the modules this skill
drives, not what it teaches.

## 1. Why you can't just call `narration.continue({})` from the console

`narration.continue`, `.call`, `.jump`, `narration.choices.select`, and `stepHistory.back` all take a
`props` argument typed as `StepLabelProps` — the exact object a real game passes from its UI layer.
Most real projects augment `StepLabelProps` (via TS module augmentation, see `pixi-vn-getting-started`)
with app-specific fields: a router `navigate`, an i18n `t`, a `toast`, etc. (the official React
template does exactly this — see `pixi-vn-narration` section 9). If a step calls `props.navigate(...)`
or `props.t(...)`, passing `{}` from a console/script call throws immediately.

`Game.testing` solves this once: the app pushes its real, live props to it (via `setProps`, see below)
wherever it already builds them, and every action it exposes merges those in before delegating — so
agent-driven calls behave exactly like a real player action, with no need to reconstruct or fake the
app's props shape.

## 2. Enabling it

`Game.testing` is **opt-in and disabled by default**, and nothing about it depends on any particular
bundler (Vite, Webpack, or none at all). Two independent pieces make it work:

- **Turning the `window` bridge on/off** — `Game.testing.enable(options?)` / `.disable()`.
- **Keeping it supplied with live props** — `Game.testing.setProps(props)`.

### Turning it on/off

If the project uses `@drincs/pixi-vn/vite`'s `vitePluginPixivn`, this is already handled: its
`testing` option **defaults to `true`**, auto-enabling `Game.testing` for as long as `vite dev` keeps
running (by injecting a small module into `index.html`) — **never** during `vite build`, regardless of
the option. No app code needed for this part:

```ts
// vite.config.ts — this is already the default, shown here just to make it explicit
vitePluginPixivn({ testing: true }); // or { testing: { windowKey: "myGameTesting" } }, or false to opt out
```

Pass `testing: false` if you don't want this — e.g. a dev server shared with other people that you
don't want remote-controllable.

Without the Vite plugin (a different bundler, or no bundler-level integration at all), call it
yourself, gated behind your own dev-only check:

```ts
import { Game } from "@drincs/pixi-vn";

if (import.meta.env.DEV) {
  // or process.env.NODE_ENV !== "production", a Webpack DefinePlugin flag, etc.
  Game.testing.enable();
}
```

- `options.windowKey` (default `"pixiVN"`) — the property name the API is attached under on `window`.
- Returns the same API object it attaches to `window[windowKey]`, in case you want to keep a direct
  reference instead of going through `window`.
- Calling `enable` again (e.g. on hot reload) replaces the previous session instead of stacking.
- `Game.testing.disable()` tears it down (detaches from `window`, stops capturing errors).
- `Game.testing.isEnabled()` reports whether a session is currently active.

**Only enable this when you actually want the game remote-controllable.** It hands out full control
over game state (see §4) to anything that can run JavaScript in that page — treat it like any other
devtools/debug backdoor.

### Keeping it supplied with live props: `setProps`

Call `Game.testing.setProps(props)` **unconditionally**, wherever your app already builds its
`StepLabelProps` — it's a cheap assignment, safe to call whether or not testing happens to be enabled
right now, and every `Game.testing` action reads through whatever was passed here most recently:

```ts
import { Game, type StepLabelProps } from "@drincs/pixi-vn";

function useGameProps(): StepLabelProps {
  // ... build props the same way your app always has ...
  const props = { navigate, t, toast /* ... */ };
  Game.testing.setProps(props); // [!code focus]
  return props;
}
```

### React template convention

The official React template's `useGameProps()` (`src/lib/hooks/props-hooks.ts` — see
`pixi-vn-narration` section 9) already ends with exactly that `Game.testing.setProps(props)` call, and
`vite.config.ts`'s `vitePluginPixivn(...)` call needs no extra option — `testing` defaults to `true`.
Since `useGameProps()` already runs on every render of every component that needs game props, this is
the only line the template needs: no dedicated bridge component, no manual `enable()` call.

### Routing through the app's own narration functions: `setActions`

Most real apps don't call `narration.continue`/`stepHistory.back`/etc. straight from a "next" button —
they wrap them in their own functions that also handle UI-only concerns a test session should trigger
too: a loading indicator, refusing to advance while a menu/dialog is open, refreshing cached interface
data after the step resolves, and so on. By default `Game.testing`'s actions skip all of that and call
`narration`/`stepHistory` directly, so an agent driving the game through `window.pixiVN` can get out of
sync with what the real UI would have done (e.g. it never sees the loading state, or advances past a
guard the real "next" button respects).

`Game.testing.setActions(actions)` fixes this: register the app's own functions once, and
`continue()`/`goBack()`/`selectChoice()`/`start()`/`jump()`/`call()` call them instead. Any action left
out keeps calling `narration`/`stepHistory` directly, so this is opt-in per action:

```ts
import { Game } from "@drincs/pixi-vn";

Game.testing.setActions({
  continue: () => goNext(),
  back: () => goBack(),
  selectChoice: (item) => selectChoice(item),
  start: (label, props) => async () => {
    await props.navigate("/game");
    return await startNewGame(label, props);
  },
  jump: (label, props) => jump(label, props),
  call: (label, props) => call(label, props),
});
```

Mount that hook once near the app root (same idea as `useGameProps()` always calling `setProps`) and
`window.pixiVN.continue()` now runs the exact same code path as a player clicking "next".

A few things follow from `continue`/`back` taking no arguments themselves:

- The `extraProps`/`options` a caller passes to `window.pixiVN.continue(...)` /
  `window.pixiVN.goBack(...)` are **ignored** once an override is registered — the override manages its
  own props (typically via `setProps`) and usually has no notion of `{ steps, runNow }`. Drive those
  through `window.pixiVN.props`/`narration`/`stepHistory` directly if you need that level of control.
- `selectChoice`'s override receives the already-resolved choice item (the same shape as
  `getState().choices[i]`), not the raw `choiceIndex` — the lookup (and the "no open choice with that
  index" error) still happens before the override runs.
- `setActions` is safe to call whether or not testing is enabled yet, safe to call again to update the
  registered functions, and persists across `disable()`/`enable()` — same as `setProps`.

## 3. Driving the game

Every action below reads through `window.pixiVN` (or your custom `windowKey`), evaluated in the page —
e.g. via a browser automation tool's "evaluate script" capability, or by pasting into the devtools
console by hand. **If the Chrome DevTools MCP server is available, prefer it over any other browser
automation for this**: `evaluate_script` runs the snippets below directly in the page, `navigate_page`/
`click`/`take_snapshot` drive UI the game itself doesn't expose through `window.pixiVN` (e.g. a login
screen), `take_screenshot` lets you actually look at the canvas instead of only trusting `getState()`,
and `list_console_messages` surfaces errors/warnings a script-only check would miss — checking console
output after every meaningful step, not just at the end, has caught real bugs (e.g. a framework
deprecation warning) that a headless script relying only on `errors`/return values missed. Fall back to
a Playwright/`chromium-cli` script, or another MCP browser tool, only when Chrome DevTools MCP isn't
available. All of the actions below are `async` except `setInput`, `closeCurrentLabel`/
`closeAllLabels`, and `getState`.

```js
// Start the game from a label (id or Label object). Clears all game data first, like a real "New Game".
await window.pixiVN.start("start");

// Advance one step (same guard a "next" button uses — see canContinue in getState() below).
await window.pixiVN.continue();

// Call a sub-label (returns to the caller when it runs out of steps) / jump to another label (no return).
await window.pixiVN.call("some_label");
await window.pixiVN.jump("some_label");

// Pick an open choice by its choiceIndex (see getState().choices below for what's available).
await window.pixiVN.selectChoice(0);

// Answer a pending input prompt, then continue like the player pressing confirm would.
window.pixiVN.setInput("Liam");
await window.pixiVN.continue();

// Go back one (or more) steps.
await window.pixiVN.goBack();
await window.pixiVN.goBack({}, { steps: 3 });

// Close the current label / close every open label (can end the game — see pixi-vn-narration).
window.pixiVN.closeCurrentLabel();
window.pixiVN.closeAllLabels();
```

Every action accepts an optional second `extraProps` argument (first argument for `selectChoice`'s
props is its second argument, `continue`/`goBack`'s is first) that's shallow-merged **on top of** the
live props for that one call — useful to override just one field (e.g. a fake `navigate` to observe
where the game tried to go) without touching the app's real wiring:

```js
await window.pixiVN.continue({
  navigate: (to) => console.log("would navigate to", to),
});
```

### Reading state before deciding the next action

Don't act blindly — read `getState()` first to see what's actually on screen and what actions are
valid right now:

```js
window.pixiVN.getState();
// {
//   dialogue: { text: "What do you choose?", character: {...} } | undefined,
//   dialogueGlue: false,
//   choices: [{ text: "A", choiceIndex: 0, ... }, { text: "Close", choiceIndex: 1, ... }] | undefined,
//   input: { isRequired: false, type: undefined, value: undefined },
//   canContinue: true,
//   canGoBack: true,
//   labelsOpened: [...],       // narration.labels.opened
//   currentLabelId: "choice_label",
//   stepCounter: 12,
// }
```

- If `choices` is set, call `selectChoice(choiceIndex)` — don't call `continue()`, it won't advance
  past an open choice menu.
- If `input.isRequired` is `true`, call `setInput(value)` before the next `continue()`.
- If `canContinue` is `false` for any other reason, a step is mid-flight (e.g. an async transition) —
  wait and re-check rather than firing another action immediately.

### Driving the app's UI directly, not just narration

`window.pixiVN.props` is the same live object `setProps` was last called with — whatever your
app's `StepLabelProps` augmentation defines. Most projects put a `navigate` function there (see
`pixi-vn-getting-started` and the wiki's "Navigate/switch between UI screens" page) so labels can
switch screens; that same function is reachable here for full control over what's on screen, not just
the story — e.g. opening a settings screen, jumping to a main menu, or checking where a bugged label
tried to send the player, all without going through a choice/label:

```js
window.pixiVN.props.navigate("/settings");
```

Anything else the app's `StepLabelProps` happens to expose (a `toast`, a `t` translator, ...) is
reachable the same way. If your project doesn't augment `StepLabelProps` with a navigation function at
all, this simply isn't available — driving screen navigation is then whatever mechanism the app itself
uses outside of Pixi'VN (e.g. calling the router directly from the evaluated script).

## 4. Full state control

Beyond the guided actions above, the API also exposes the underlying singletons directly, for anything
not covered by a dedicated method — most commonly **storage**, to set up a scenario without replaying
the whole story to reach it, or to assert on state a dialogue doesn't surface:

```js
// Arrange: seed state before starting/continuing.
window.pixiVN.storage.set("gold", 100);
window.pixiVN.storage.flags.set("met_liam", true);

// Assert: read state after driving the story forward.
window.pixiVN.storage.get("gold"); // 100

// Reset between test scenarios.
window.pixiVN.storage.clear(); // back to storage.default
window.pixiVN.Game.clear(); // wipes storage + narration + history + canvas + sound

// Snapshot / restore full game state instead of replaying steps to get back to a scenario.
const snapshot = window.pixiVN.Game.exportGameState();
// ... drive the game further, try something risky ...
await window.pixiVN.Game.restoreGameState(snapshot);
```

`window.pixiVN.narration` and `window.pixiVN.stepHistory` are the same `narration`/`stepHistory`
singletons documented in `pixi-vn-narration` and `pixi-vn-history` — anything those skills document
(`narration.dialogue`, `narration.labels.current`, `stepHistory.narrativeHistory`, ...) is reachable
the same way here, read-only or not.

## 5. Catching errors during an automated run

An automated/agent-driven session can't "notice" a red error toast the way a human tester would.
`Game.testing.enable` registers its own `Game.addOnError` handler for as long as it's active, so
errors raised anywhere in the game (including inside steps) are captured instead of only surfacing in
the UI:

```js
window.pixiVN.errors;
// [{ error: Error("..."), timestamp: 1730000000000 }, ...]

window.pixiVN.clearErrors(); // e.g. between test cases, so failures don't bleed across scenarios
```

This is in addition to, not a replacement for, whatever `Game.addOnError` handlers the app itself
registers (e.g. `drawCanvasErrorHandler()`) — both run.

## Related skills

pixi-vn-getting-started, pixi-vn-narration, pixi-vn-storage, pixi-vn-history, pixi-vn-saves
