docs

Scenarios

+
UniversalWorks with Expo & React Native CLI

This tool works with both Expo and React Native CLI projects. Just install and go.

Save a repeatable test setup as a scenario. Its steps can configure a network override, write storage, select an impersonated user, and navigate to a screen through installed tools.

Review the step preview before running it. Deactivation removes supported active effects, but storage writes need explicit undo steps. Confirm the app's state after both activation and deactivation.

Installation

npm install @buoy-gg/scenarios

Auto-discovery finds the installed package and the SCENARIOS tool appears in your floating menu:

tsx
import { FloatingDevTools } from "@buoy-gg/core";

export default function App() {
  return (
    <>
      <YourApp />
      <FloatingDevTools />
    </>
  );
}
import { FloatingDevTools } from "@buoy-gg/core";

export default function App() {
  return (
    <>
      <YourApp />
      <FloatingDevTools />
    </>
  );
}

A scenario's steps are executed through the tools that already own that state, so install the ones your scenarios use — @buoy-gg/network for response overrides, @buoy-gg/storage for storage writes, @buoy-gg/impersonate for user sessions, @buoy-gg/route-events to navigate. A step whose tool isn't installed is refused before the run starts, and the preview tells you which one.


Ship the QA menu with your app

The authoring path that lasts is code. defineScenario() gives you a typed, reviewable, versioned definition that lives next to the code it bends:

tsx
// scenarios/qaMenu.ts
import { defineScenario } from "@buoy-gg/scenarios";

export const outOfStock = defineScenario({
  id: "oos-item",
  name: "Out-of-stock item at store {{storeId}}",
  description: "The double burger is sold out, one is already in the cart.",
  expectedOutcome:
    "Cart shows the item as sold out; checkout is blocked until it's removed.",
  author: "@sam",
  authoredAgainst: "5.4.0",
  folder: "Checkout",        // the flow it belongs to — see Folders below
  tags: ["cart", "menu"],
  vars: [
    { key: "storeId", label: "Store", type: "enum",
      options: ["100", "220", "415"], default: "100" },
  ],
  steps: [
    { tool: "network", action: "upsertOverrideRule",
      params: { rule: {
        urlPattern: "*/menu/{{storeId}}*", methods: ["GET"],
        kind: "respond", status: 200, body: '{"items":[{"id":"dbl","inStock":false}]}',
      } } },
    { tool: "storage", action: "async.setItem",
      params: { key: "@cart", value: '[{"item":"dbl","qty":1}]' } },
    { tool: "route-events", action: "navigate", params: { path: "/cart" } },
  ],
});
// scenarios/qaMenu.ts
import { defineScenario } from "@buoy-gg/scenarios";

export const outOfStock = defineScenario({
  id: "oos-item",
  name: "Out-of-stock item at store {{storeId}}",
  description: "The double burger is sold out, one is already in the cart.",
  expectedOutcome:
    "Cart shows the item as sold out; checkout is blocked until it's removed.",
  author: "@sam",
  authoredAgainst: "5.4.0",
  folder: "Checkout",        // the flow it belongs to — see Folders below
  tags: ["cart", "menu"],
  vars: [
    { key: "storeId", label: "Store", type: "enum",
      options: ["100", "220", "415"], default: "100" },
  ],
  steps: [
    { tool: "network", action: "upsertOverrideRule",
      params: { rule: {
        urlPattern: "*/menu/{{storeId}}*", methods: ["GET"],
        kind: "respond", status: 200, body: '{"items":[{"id":"dbl","inStock":false}]}',
      } } },
    { tool: "storage", action: "async.setItem",
      params: { key: "@cart", value: '[{"item":"dbl","qty":1}]' } },
    { tool: "route-events", action: "navigate", params: { path: "/cart" } },
  ],
});

Register them and they show up under Shipped with this app, read-only on the device:

tsx
import { createScenariosTool } from "@buoy-gg/scenarios";
import { outOfStock } from "./scenarios/qaMenu";

const scenarios = createScenariosTool({
  scenarios: [outOfStock],
  currentBuild: "5.4.0",   // drives the staleness badge
});

<FloatingDevTools apps={[scenarios]} />
import { createScenariosTool } from "@buoy-gg/scenarios";
import { outOfStock } from "./scenarios/qaMenu";

const scenarios = createScenariosTool({
  scenarios: [outOfStock],
  currentBuild: "5.4.0",   // drives the staleness badge
});

<FloatingDevTools apps={[scenarios]} />

Keeping definitions in code lets you review the scenario alongside changes to the API or app state it uses.


What You Can Do

  • Run a saved state in one tap — pick the variables (real pickers, never free text for a URL), tap Run, and the configured steps run. On success the tool minimizes itself: the changed app is the confirmation.
  • See exactly what will happen before it happens — the preview lists every step in plain English (GET */menu/100* → respond 200, replaced body, Set storage @cart, Go to /cart) with a durability chip on each, plus the author's expected outcome. You can write a bug report against a scenario because you can read what it did.
  • Trust the boundary between fake and real — while a scenario is active, an amber ▲ SIMULATED banner shows on every screen, names the scenario, counts its changes, and carries the deactivate control. It survives app restarts alongside the overrides, and because it's a real view it lands in every screenshot and screen recording you take. There is no setting that hides it.
  • Deactivate, and be told the truth — one tap deletes the scenario's override rules and ends its impersonation, then reports what it could not reverse. Storage writes have no automatic inverse, so they're named rather than quietly left behind. Authors can supply undoSteps to close that gap.
  • Survive a restart — network overrides ride the shipped override-rule store, so the state holds through pull-to-refresh, a JS reload, and a full app relaunch. The active record is reconciled against the real rules on boot, so the banner can never claim state that isn't there.
  • Review overlapping scenarios — Review preflight and any Swap prompt before activation. Running a scenario that touches an endpoint the active one already owns offers a Swap instead of layering, and the overlap is named. Resolve overlapping rules before continuing the test.
  • Author from a conversation — set the state up interactively with your AI assistant, then ask it to save that as a scenario. It arrives on the device as a draft (see below).
  • Attach the state to the bug — Copy repro info puts the scenario name, version, variables, build and effect list on the clipboard, formatted for a ticket. "Are you sure a scenario wasn't active?" stops being a question anyone has to ask.

Folders: one flow at a time

A QA library grows past the point where one list is useful. Give a scenario a folder — the flow it belongs to — and the tool grows a chip bar across the top: All · Checkout · Login · Onboarding · Ungrouped. Tap one and the list is only that flow. Somebody testing checkout stops scrolling past login.

Three ways a scenario gets filed:

  • In code, with folder: in defineScenario(). The QA menu ships organized, and it looks the same on every device — which is the point. Shipped scenarios are read-only on the device, folder included.
  • On the device, from a scenario's detail screen: tap the Folder row, pick an existing folder or type a new one.
  • From your editor, with scenario_action setFolder, or a folder on the scenario you save.

A folder is a label on a scenario, not a record somewhere. That has consequences worth knowing:

  • It exists as long as something is in it. Move the last scenario out and the folder is gone. There is nothing to clean up, and no empty folders to wonder about.
  • Names converge instead of splitting. Typing checkout when Checkout already exists files it under Checkout. Names are trimmed, inner whitespace is collapsed, and they cap at 32 characters.
  • One folder per scenario. That's what makes it a folder rather than a tag — use tags for the many-to-many axis.
  • There is no rename or delete. A rename would silently skip the read-only code scenarios sharing the name, leaving half a folder behind. Change the folder: in the source, or re-file the device ones.

Search reads the folder too, so typing checkout finds the flow before you've noticed the chips.


Drafts: nothing writes to a device without a human

A scenario saved from chat or the desktop dashboard does not become runnable. It lands in a Drafts — awaiting review section and is inert: pre-flight refuses to run it. Someone on the device opens it, reads the plain-English steps, and taps Accept to library.

Device review lets the tester inspect the proposed changes before adding the scenario to the runnable library.


Durability, honestly

Different steps last for different lengths of time, and the UI never softens the difference.

StepDurable?What deactivate does
Override a request✅ survives refetch, JS reload and app restartDeletes the rule
Impersonate a user✅ survives restartEnds the session
Set / remove storage✅ it's the app's real storeNot auto-reversed — the key is named, or undoSteps restore it
Poke the query cache❌ dies on the next refetchNothing to remove; labelled transient throughout
Navigateone-shotn/a

The durable path is the recommended path. setQueryData stays available because it's the right tool for a one-shot demo state, but it is always labelled until next refetch — a scenario that quietly expires is worse than one that never ran.


Variables

Declare them, and the run form builds itself: an enum renders as chips, a boolean as a switch, a number as a numeric field. Values are remembered per scenario, so a regression pass doesn't retype them.

Placeholders substitute by {{key}} and nothing else — no expressions, no templating language. Scenario definitions are data, but their actions can change real app state. Review imported definitions before accepting them. One rule worth knowing: a string that is exactly "{{code}}" keeps the variable's type, so status: "{{code}}" sends the number 500, not the string "500"; embedded placeholders substitute as text.


Failure is loud

Pre-flight runs before step one and collects every problem at once — an uninstalled tool, an action that doesn't exist, an undeclared variable, a plan limit already reached. Runtime failures can still leave partial changes; inspect the result before continuing.

If a step still fails at runtime, the run stops there, prints the error verbatim, marks the rest skipped, and makes you choose: Undo applied steps (attempting the available reversals) or Keep. There is no silent default, and a partially applied scenario is never recorded as active.


Drive it from your editor and your tests

Every action is available over MCP and from Buoy Desktop, so the same named state serves QA's finger, an E2E spec and an AI agent:

ActionWhat it does
listScenariosEvery scenario with its plain-English step lines
previewDry-run: what a run would do, with variables resolved
runRuns it; resolves once every step has been applied
getActiveThe active scenario — assert it at the top of a spec
saveSaves a scenario onto the device as a draft
setFolderFiles a saved scenario under a flow, or clears its folder
deactivateReverses it and returns the honest report

After run resolves, assert the expected app state before driving the flow. A completed step sequence does not establish that later requests or screen rendering succeeded.

Not to be confused with WireMock's "scenarios" (a stateful mock state-machine) or a Gherkin scenario (a test case). A Buoy scenario is closer to the Given half of a Gherkin scenario, made executable.


Safety

  • Development builds only. The runner refuses to execute when __DEV__ is false, with a release-lane test proving it. Scenario definitions in a shipped app are inert data.
  • The banner is not optional. It can be collapsed to a small amber pill; it cannot be turned off while a scenario is active.
  • A scenario can't run a scenario. Recursion is refused in pre-flight.
  • Your own traffic is protected. Override steps flow through the shipped engine, so its exclusions (license checks, the broker, Metro) and its OPTIONS skip apply automatically.
  • Size budgets — 64KB per step, 256KB per scenario, validated on every save. Prefer the smallest response body that triggers the state.

Plans

FreePro
Scenarios saved on a device3Unlimited
Active-scenario allowance1Up to 3, subject to preflight and overlap handling

Scenarios shipped in code with defineScenario() don't count toward the device limit.

Web support

Scenarios works in browser apps too. Add @buoy-gg/scenarios/web to the modules you pass to FloatingDevTools, under the key scenarios. You get the same panels and actions. Web installation shows how to set it up. It also lists what else to install and what a browser can't do.

Swift

The Swift SDK includes BuoyScenarios. Register shipped definitions with BuoyScenarios.define(...), using the same scenario fields and { tool, action, params } steps. The example app shares the Pokémon API error and delay definitions with RN.

The scenarios MCP adapter exposes the library, draft review, previews, execution, deactivation, folders, and JSON export. Remote saves always become drafts. Steps dispatch through the native tool registry, so an RN-only action fails preflight before any step runs. Native execution is disabled in production builds.

Storage writes need explicit undo steps. Deactivation removes recorded override rules and stops recorded impersonation sessions; read the result for changes left behind. A timed-out action blocks further runs until it finishes, and late successful effects remain available for cleanup. The native UI provides draft review, typed variables, a step preview, and a failed-run report showing completed, failed, and skipped steps.

Native recording captures supported host taps and text, then lets you review and select steps before saving. It excludes Buoy controls and secure text fields, and adds waits for observed navigation. It also records long presses, switches and sliders. It does not record scroll gestures or native state-store shortcuts. Capture and execution require a development build. MCP-driven capture and replay have passed simulator checks; physical-touch recording still needs device QA.

For Swift setup and what the native version covers, see the Swift tools guide.