state-scenarios-cli 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/browser.d.ts +27 -0
- package/dist/chunk-GCYOLAFU.js +40 -0
- package/dist/chunk-GCYOLAFU.js.map +1 -0
- package/dist/chunk-O6QTNAAZ.js +67 -0
- package/dist/chunk-O6QTNAAZ.js.map +1 -0
- package/dist/chunk-YFNROT3R.js +115 -0
- package/dist/chunk-YFNROT3R.js.map +1 -0
- package/dist/cli.js +264 -44
- package/dist/cli.js.map +1 -1
- package/dist/doctor-URGIFA3W.js +200 -0
- package/dist/doctor-URGIFA3W.js.map +1 -0
- package/dist/doctor.d.ts +21 -0
- package/dist/errors-G7ODRBLT.js +11 -0
- package/dist/errors-G7ODRBLT.js.map +1 -0
- package/dist/errors.d.ts +33 -0
- package/dist/{init-VLZQEJRD.js → init-MRK4F5SV.js} +2 -1
- package/dist/init-MRK4F5SV.js.map +1 -0
- package/dist/init.d.ts +5 -1
- package/dist/inspect-LN4EACWA.js +13 -0
- package/dist/inspect-LN4EACWA.js.map +1 -0
- package/dist/inspect.d.ts +63 -0
- package/dist/mcp-HRQIWAIR.js +342 -0
- package/dist/mcp-HRQIWAIR.js.map +1 -0
- package/dist/mcp-protocol.d.ts +51 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/record-HXIYGMWA.js +65 -0
- package/dist/record-HXIYGMWA.js.map +1 -0
- package/dist/record.d.ts +45 -0
- package/dist/skills-NQRJHKHG.js +80 -0
- package/dist/skills-NQRJHKHG.js.map +1 -0
- package/dist/skills.d.ts +13 -0
- package/package.json +5 -4
- package/skills/create-scenario-map/SKILL.md +88 -0
- package/skills/state-scenarios/SKILL.md +91 -0
- package/skills/state-scenarios/references/auth.md +52 -0
- package/skills/state-scenarios/references/capture.md +45 -0
- package/skills/state-scenarios/references/debugging.md +52 -0
- package/skills/state-scenarios/references/mcp.md +39 -0
- package/skills/state-scenarios/references/media.md +60 -0
- package/skills/state-scenarios/references/network.md +67 -0
- package/skills/state-scenarios/references/setup.md +94 -0
- package/skills/state-scenarios/references/verify.md +47 -0
- package/skills/state-scenarios/references/writing-scenarios.md +55 -0
- package/dist/init-VLZQEJRD.js.map +0 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Setup
|
|
2
|
+
|
|
3
|
+
## Install and init
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i state-scenarios zod msw # msw only when scenarios mock the network
|
|
7
|
+
npm i -D state-scenarios-cli @playwright/test
|
|
8
|
+
npm i state-scenarios-react # useScenarioState, for component state (React, Preact)
|
|
9
|
+
|
|
10
|
+
npx state-scenarios init src/scenarios
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`init` writes `src/scenarios/default.json` and `src/scenario.schema.json`, prints a wiring snippet,
|
|
14
|
+
installs these skills into `.agents/skills/` (and `.claude/skills/`), and registers the MCP server
|
|
15
|
+
in `.mcp.json`. It never edits your source files. `--no-skills` skips the skills and the MCP server.
|
|
16
|
+
|
|
17
|
+
Vite: serve the MSW worker and let the panel save into the project.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// vite.config.ts
|
|
21
|
+
import { msw } from 'msw/vite';
|
|
22
|
+
import { stateScenarios } from 'state-scenarios/vite';
|
|
23
|
+
|
|
24
|
+
export default { plugins: [msw({ mode: 'worker-only' }), stateScenarios({ dir: 'src/scenarios' })] };
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Without the MSW Vite plugin: `npx msw init public`.
|
|
28
|
+
|
|
29
|
+
## Wire the runtime before render
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { type ScenarioInput, startScenarios } from 'state-scenarios';
|
|
33
|
+
import { mountPanel } from 'state-scenarios/panel';
|
|
34
|
+
|
|
35
|
+
if (import.meta.env.DEV) {
|
|
36
|
+
const session = await startScenarios({
|
|
37
|
+
scenarios: Object.values(
|
|
38
|
+
import.meta.glob<ScenarioInput>('/src/scenarios/*.json', { eager: true, import: 'default' }),
|
|
39
|
+
),
|
|
40
|
+
adapters: [], // see below
|
|
41
|
+
refresh: () => queryClient.resetQueries(), // when scenarios mock the network
|
|
42
|
+
});
|
|
43
|
+
mountPanel(session, { schemaPath: '../scenario.schema.json' });
|
|
44
|
+
}
|
|
45
|
+
// then create the router and render
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- `await` it before rendering. The first fetches must already hit the scenario's mocks.
|
|
49
|
+
- Routers read the URL once, when they're created. If a module creates the router as soon as it
|
|
50
|
+
is imported (`export const router = createBrowserRouter(...)`), a static import runs before the
|
|
51
|
+
session. Import it after:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// main.ts: start the session first, then import the router
|
|
55
|
+
import type { ScenarioInput } from 'state-scenarios';
|
|
56
|
+
|
|
57
|
+
if (import.meta.env.DEV) {
|
|
58
|
+
const { startScenarios } = await import('state-scenarios');
|
|
59
|
+
await startScenarios({
|
|
60
|
+
scenarios: Object.values(
|
|
61
|
+
import.meta.glob<ScenarioInput>('/src/scenarios/*.json', { eager: true, import: 'default' }),
|
|
62
|
+
),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { router } = await import('./routing/router'); // created now, with the scenario's URL
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- Next.js (App Router): start it from a `'use client'` wrapper in the root layout and render
|
|
70
|
+
children once it resolves. The package README has the component and the save route.
|
|
71
|
+
- Apps that already run MSW: start your worker and pass it as `worker`. See topic `network`.
|
|
72
|
+
|
|
73
|
+
## Connect app state
|
|
74
|
+
|
|
75
|
+
Mock the network first. Add adapters for state no response can produce, such as a panel left open
|
|
76
|
+
or cached data during a refetch.
|
|
77
|
+
|
|
78
|
+
| State lives in | Connect with |
|
|
79
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
80
|
+
| TanStack Query | `tanstackQuery(queryClient)` from `state-scenarios/tanstack-query` |
|
|
81
|
+
| Zustand | `zustandAdapter(store, { schema: StoreSchema.partial() })` from `state-scenarios/zustand` |
|
|
82
|
+
| Redux | `withScenarioState(rootReducer, StateSchema)`, then `reduxAdapter(store)` |
|
|
83
|
+
| XState | `xstateAdapter(machine, { context: ContextSchema })`, then `createActor(machine, { snapshot: xs.snapshot })` |
|
|
84
|
+
| Cookies | `cookiesAdapter()` from `state-scenarios/cookies` |
|
|
85
|
+
| A component | `useScenarioState('page', state, (s) => dispatch({ type: 'set', state: s }), Schema)` |
|
|
86
|
+
|
|
87
|
+
Pass a Zod schema wherever an adapter accepts one: invalid scenario state is then rejected before
|
|
88
|
+
anything changes. `scenarioJsonSchema({ state: { key: Schema } })` regenerates the editor schema
|
|
89
|
+
with typed `state`.
|
|
90
|
+
|
|
91
|
+
## Check
|
|
92
|
+
|
|
93
|
+
`npx state-scenarios doctor src/scenarios --base-url http://localhost:5173` passes. Opening
|
|
94
|
+
`/?scenario=default` sets `<html data-scenario="default">`.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Verify a UI change across every state
|
|
2
|
+
|
|
3
|
+
Start the dev server, then:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx state-scenarios shoot src/scenarios --base-url http://localhost:5173 --out .scenario-shots
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
For each scenario `shoot` opens the link in strict mode, waits for `ready`, saves `<name>.png`, and
|
|
10
|
+
checks for unmocked requests, uncaught exceptions and `console.error` calls. `--only a,b` limits
|
|
11
|
+
the run; `--json` prints the report for scripts.
|
|
12
|
+
|
|
13
|
+
## When a scenario fails
|
|
14
|
+
|
|
15
|
+
The terminal names a page for it: `❯ details .scenario-shots/<name>.md`. Read that page. It has:
|
|
16
|
+
|
|
17
|
+
- the codes (`READY_TIMEOUT`, `UNMOCKED_REQUEST`, `SESSION_NOT_STARTED`, `SCENARIO_NOT_IN_APP`, `PAGE_ERROR`,
|
|
18
|
+
`CONSOLE_ERROR`, `ELEMENT_NOT_FOUND`) and the link that reopens the state;
|
|
19
|
+
- for a ready timeout, whether the selector matched anything, plus distinctive selectors on the
|
|
20
|
+
page now;
|
|
21
|
+
- for unmocked requests, `network` entries ready to paste (fill in real bodies, or record them);
|
|
22
|
+
- uncaught exceptions and console errors.
|
|
23
|
+
|
|
24
|
+
Look at the screenshot before changing the scenario: a ready timeout can mean the selector is
|
|
25
|
+
wrong, or that the UI change broke the state. Fix the app when the screenshot shows a bug.
|
|
26
|
+
|
|
27
|
+
## Evidence
|
|
28
|
+
|
|
29
|
+
Look at the PNGs for the states the change touches, plus loading, empty and error. Attach the
|
|
30
|
+
relevant ones to the PR. Pages for passing scenarios are deleted on each run, so the `.md` files
|
|
31
|
+
in the output folder are always the current failures.
|
|
32
|
+
|
|
33
|
+
## In Playwright tests
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { openScenario, unhandledRequests } from 'state-scenarios/playwright';
|
|
37
|
+
|
|
38
|
+
await openScenario(page, 'orders-empty');
|
|
39
|
+
expect(await unhandledRequests(page)).toEqual([]);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Missing states
|
|
43
|
+
|
|
44
|
+
Ask which states lack scenarios: loading, empty, error, partial data, long content, permissions,
|
|
45
|
+
signed out. Capture them (topic `capture`). `npx state-scenarios suggest` asks a model to list
|
|
46
|
+
candidate states from the source and drafts them into `proposed/`; treat drafts as ideas, then
|
|
47
|
+
`promote` the reviewed ones.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Writing scenarios
|
|
2
|
+
|
|
3
|
+
| Field | Meaning | Through `extends` |
|
|
4
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
5
|
+
| `name` | kebab-case, unique | – |
|
|
6
|
+
| `extends` | Scenario name(s) to build on; later parents win | – |
|
|
7
|
+
| `path` | Route it opens at, e.g. `/orders/42` | inherited |
|
|
8
|
+
| `url` | Query params the app starts with | merged; `null` removes |
|
|
9
|
+
| `network[]` | `method`, `path` (MSW syntax: `/api/users/:id`, absolute URLs, `*`), optional `query`, then one of `response`, `sequence`, `remove: true` | a child entry replaces the parent's with the same method, path and query |
|
|
10
|
+
| `handlers` | Names of MSW handlers passed to `startScenarios({ handlers })` | combined |
|
|
11
|
+
| `state` | Per-adapter state, keyed by adapter key | deep-merged; `null` removes |
|
|
12
|
+
| `ready` | Playwright selector visible once this state shows | not inherited |
|
|
13
|
+
| `viewport` | `{ "width": 390, "height": 844 }` for `shoot` and `record` | inherited |
|
|
14
|
+
| `description`, `tags` | For people and the panel | not inherited |
|
|
15
|
+
|
|
16
|
+
Responses: `body` is sent as JSON, `text` as text (an HTML error page). `status` defaults to 200.
|
|
17
|
+
`headers` sets headers. `delay` is milliseconds, or `"infinite"` to hold the request open for a
|
|
18
|
+
loading state. `sequence` answers successive calls in order and repeats the last one, e.g. fail
|
|
19
|
+
then succeed on retry:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"name": "flaky-then-recovers",
|
|
24
|
+
"extends": "default",
|
|
25
|
+
"network": [{
|
|
26
|
+
"path": "/api/orders",
|
|
27
|
+
"sequence": [{ "status": 503, "body": { "error": "busy" } }, { "body": [{ "id": 1 }] }]
|
|
28
|
+
}],
|
|
29
|
+
"ready": "text=Order #1"
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Loading state: `"response": { "delay": "infinite" }` and `"ready": "text=Loading orders…"`.
|
|
34
|
+
|
|
35
|
+
## Ready selectors
|
|
36
|
+
|
|
37
|
+
`text=No orders yet`, `role=heading[name="Orders"]`, `[data-testid=empty-orders]`, or CSS. Pick
|
|
38
|
+
something that is visible only in this state: a count like "0" or a heading every state shares is
|
|
39
|
+
too generic. Check it against the running app (panel, or MCP `check_ready`) before saving.
|
|
40
|
+
|
|
41
|
+
## Combining
|
|
42
|
+
|
|
43
|
+
`?scenario=signed-in,server-error` combines scenarios like `extends`, later names winning.
|
|
44
|
+
|
|
45
|
+
## Rules and validation
|
|
46
|
+
|
|
47
|
+
`defineScenarioRules` checks business rules on every resolved scenario. Export `rules` from a module
|
|
48
|
+
and pass it to `startScenarios({ rules })` and `--rules` on the CLI.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx state-scenarios validate src/scenarios --rules src/scenario-rules.ts
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Schema shape, `extends` lookups and rules are checked. Whether production can actually reach a
|
|
55
|
+
state is your call: a valid scenario can describe a response the backend never sends.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/init.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport { scenarioJsonSchema } from 'state-scenarios';\n\n/** Create a scenarios dir, schema, default stub, and print a wiring snippet. */\nexport function initScenarios(scenariosDir: string, cwd = process.cwd()) {\n const dir = resolve(cwd, scenariosDir);\n mkdirSync(dir, { recursive: true });\n\n const schemaPath = join(dirname(dir), 'scenario.schema.json');\n const defaultPath = join(dir, 'default.json');\n const schemaRel = relative(dir, schemaPath).replaceAll('\\\\', '/');\n\n const schemaExisted = existsSync(schemaPath);\n const defaultExisted = existsSync(defaultPath);\n\n if (!schemaExisted) {\n writeFileSync(schemaPath, `${JSON.stringify(scenarioJsonSchema(), null, 2)}\\n`);\n }\n\n if (!defaultExisted) {\n writeFileSync(\n defaultPath,\n `${\n JSON.stringify(\n {\n $schema: schemaRel,\n name: 'default',\n description: 'Happy path: record the real network, then Save as scenario from the panel',\n },\n null,\n 2,\n )\n }\\n`,\n );\n }\n\n const stack = detectStack(cwd);\n const scenariosImport = relative(cwd, dir).replaceAll('\\\\', '/');\n\n console.log(`${defaultExisted ? 'Kept existing' : 'Created'} ${relative(cwd, defaultPath)}`);\n console.log(`${schemaExisted ? 'Kept existing' : 'Created'} ${relative(cwd, schemaPath)}`);\n console.log('');\n console.log('Wire the runtime (DEV only), then open the app with ?scenario-record,');\n console.log('click through the flow, Mark ready, and Download JSON into this folder.');\n console.log('');\n console.log(wiringSnippet(stack, scenariosImport));\n}\n\ntype Stack = 'vite' | 'next' | 'unknown';\n\nfunction detectStack(cwd: string): Stack {\n if (hasFile(cwd, ['next.config.ts', 'next.config.js', 'next.config.mjs', 'next.config.cjs'])) {\n return 'next';\n }\n\n if (hasFile(cwd, ['vite.config.ts', 'vite.config.js', 'vite.config.mjs', 'vite.config.cjs'])) {\n return 'vite';\n }\n\n return 'unknown';\n}\n\nfunction hasFile(cwd: string, names: string[]): boolean {\n return names.some((name) => existsSync(join(cwd, name)));\n}\n\nfunction wiringSnippet(stack: Stack, scenariosImport: string): string {\n // Root-relative: Vite resolves '/…' globs from the project root, wherever the snippet is pasted.\n const glob = `import.meta.glob<ScenarioInput>('${\n scenariosImport ? `/${scenariosImport}` : ''\n }/*.json', { eager: true, import: 'default' })`;\n\n if (stack === 'next') {\n return `// Next.js (App Router): wrap children in a client <Scenarios> gate (see README).\n// List scenario JSON imports explicitly (Next has no import.meta.glob).\n// Serve the MSW worker with: npx msw init public\n//\n// mountPanel(session, { schemaPath: '../scenario.schema.json' });\n//\n// Let the panel save into ${scenariosImport || '.'}: create app/%5F%5Fstate-scenarios/save/route.ts with\n// import { createSaveRoute } from 'state-scenarios/next';\n// export const { GET } = createSaveRoute({ dir: '${scenariosImport || '.'}' });`;\n }\n\n const plugins = `import { msw } from 'msw/vite';\n// import { stateScenarios } from 'state-scenarios/vite';\n// plugins: [msw({ mode: 'worker-only' }), stateScenarios({ dir: '${scenariosImport || '.'}' })]\n// stateScenarios lets the panel save scenarios straight into this folder.`;\n\n const viteNote = stack === 'vite'\n ? `// In vite.config.ts:\\n// ${plugins}`\n : `// Vite, in vite.config.ts:\\n// ${plugins}\n// Next: see README \"Next.js (App Router)\"`;\n\n return `${viteNote}\n\nimport { type ScenarioInput, startScenarios } from 'state-scenarios';\nimport { mountPanel } from 'state-scenarios/panel';\n// import { tanstackQuery } from 'state-scenarios/tanstack-query'; // if you use it\n\nif (import.meta.env.DEV) {\n const session = await startScenarios({\n scenarios: Object.values(${glob}),\n // adapters: [tanstackQuery(queryClient)],\n // refresh: () => queryClient.resetQueries(),\n });\n mountPanel(session, { schemaPath: '../scenario.schema.json' });\n}\n\n// Then: open /?scenario-record → use the app → panel → Mark ready → Download JSON`;\n}\n"],"mappings":";AAAA,SAAS,YAAY,WAAW,qBAAqB;AACrD,SAAS,SAAS,MAAM,UAAU,eAAe;AACjD,SAAS,0BAA0B;AAG5B,SAAS,cAAc,cAAsB,MAAM,QAAQ,IAAI,GAAG;AACvE,QAAM,MAAM,QAAQ,KAAK,YAAY;AACrC,YAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAElC,QAAM,aAAa,KAAK,QAAQ,GAAG,GAAG,sBAAsB;AAC5D,QAAM,cAAc,KAAK,KAAK,cAAc;AAC5C,QAAM,YAAY,SAAS,KAAK,UAAU,EAAE,WAAW,MAAM,GAAG;AAEhE,QAAM,gBAAgB,WAAW,UAAU;AAC3C,QAAM,iBAAiB,WAAW,WAAW;AAE7C,MAAI,CAAC,eAAe;AAClB,kBAAc,YAAY,GAAG,KAAK,UAAU,mBAAmB,GAAG,MAAM,CAAC,CAAC;AAAA,CAAI;AAAA,EAChF;AAEA,MAAI,CAAC,gBAAgB;AACnB;AAAA,MACE;AAAA,MACA,GACE,KAAK;AAAA,QACH;AAAA,UACE,SAAS;AAAA,UACT,MAAM;AAAA,UACN,aAAa;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,MACF,CACF;AAAA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QAAQ,YAAY,GAAG;AAC7B,QAAM,kBAAkB,SAAS,KAAK,GAAG,EAAE,WAAW,MAAM,GAAG;AAE/D,UAAQ,IAAI,GAAG,iBAAiB,kBAAkB,SAAS,IAAI,SAAS,KAAK,WAAW,CAAC,EAAE;AAC3F,UAAQ,IAAI,GAAG,gBAAgB,kBAAkB,SAAS,IAAI,SAAS,KAAK,UAAU,CAAC,EAAE;AACzF,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,uEAAuE;AACnF,UAAQ,IAAI,yEAAyE;AACrF,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,cAAc,OAAO,eAAe,CAAC;AACnD;AAIA,SAAS,YAAY,KAAoB;AACvC,MAAI,QAAQ,KAAK,CAAC,kBAAkB,kBAAkB,mBAAmB,iBAAiB,CAAC,GAAG;AAC5F,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,KAAK,CAAC,kBAAkB,kBAAkB,mBAAmB,iBAAiB,CAAC,GAAG;AAC5F,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,SAAS,QAAQ,KAAa,OAA0B;AACtD,SAAO,MAAM,KAAK,CAAC,SAAS,WAAW,KAAK,KAAK,IAAI,CAAC,CAAC;AACzD;AAEA,SAAS,cAAc,OAAc,iBAAiC;AAEpE,QAAM,OAAO,oCACX,kBAAkB,IAAI,eAAe,KAAK,EAC5C;AAEA,MAAI,UAAU,QAAQ;AACpB,WAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAMkB,mBAAmB,GAAG;AAAA;AAAA,sDAEG,mBAAmB,GAAG;AAAA,EAC1E;AAEA,QAAM,UAAU;AAAA;AAAA,sEAEoD,mBAAmB,GAAG;AAAA;AAG1F,QAAM,WAAW,UAAU,SACvB;AAAA,OAA+B,OAAO,KACtC;AAAA,OAAqC,OAAO;AAAA;AAGhD,SAAO,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+BAQW,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQnC;","names":[]}
|