All but React Native are in beta.
All but React Native are in beta.

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.
npm install @buoy-gg/scenariosAuto-discovery finds the installed package and the SCENARIOS tool appears in your floating menu:
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.
The authoring path that lasts is code. defineScenario() gives you a typed, reviewable, versioned definition that lives next to the code it bends:
// 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:
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.
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.▲ 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.undoSteps to close that gap.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:
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.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:
checkout when Checkout already exists files it under Checkout. Names are trimmed, inner whitespace is collapsed, and they cap at 32 characters.tags for the many-to-many axis.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.
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.
Different steps last for different lengths of time, and the UI never softens the difference.
| Step | Durable? | What deactivate does |
|---|---|---|
| Override a request | ✅ survives refetch, JS reload and app restart | Deletes the rule |
| Impersonate a user | ✅ survives restart | Ends the session |
| Set / remove storage | ✅ it's the app's real store | Not auto-reversed — the key is named, or undoSteps restore it |
| Poke the query cache | ❌ dies on the next refetch | Nothing to remove; labelled transient throughout |
| Navigate | one-shot | n/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.
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.
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.
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:
| Action | What it does |
|---|---|
listScenarios | Every scenario with its plain-English step lines |
preview | Dry-run: what a run would do, with variables resolved |
run | Runs it; resolves once every step has been applied |
getActive | The active scenario — assert it at the top of a spec |
save | Saves a scenario onto the device as a draft |
setFolder | Files a saved scenario under a flow, or clears its folder |
deactivate | Reverses 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.
__DEV__ is false, with a release-lane test proving it. Scenario definitions in a shipped app are inert data.OPTIONS skip apply automatically.| Free | Pro | |
|---|---|---|
| Scenarios saved on a device | 3 | Unlimited |
| Active-scenario allowance | 1 | Up to 3, subject to preflight and overlap handling |
Scenarios shipped in code with defineScenario() don't count toward the device limit.
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.
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.