state-scenarios-cli 0.2.0 → 0.4.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 +260 -43
- 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-MRK4F5SV.js +96 -0
- package/dist/init-MRK4F5SV.js.map +1 -0
- package/dist/init.d.ts +6 -0
- 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/promote-GSF5UXJL.js +78 -0
- package/dist/promote-GSF5UXJL.js.map +1 -0
- package/dist/promote.d.ts +12 -0
- package/dist/record-WEZRD2VT.js +59 -0
- package/dist/record-WEZRD2VT.js.map +1 -0
- package/dist/record.d.ts +41 -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 +50 -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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Network: strict mode, third parties and existing MSW
|
|
2
|
+
|
|
3
|
+
## Strict mode
|
|
4
|
+
|
|
5
|
+
`?scenario-strict` (or `startScenarios({ strict: true })`) answers every request the scenario
|
|
6
|
+
doesn't cover with a 501, so nothing reaches a real backend. Page loads and assets (any path with a
|
|
7
|
+
file extension) pass. `shoot`, `openScenario` and the MCP server run strict by default.
|
|
8
|
+
`unhandledRequests(page)`, the panel and `shoot` list what wasn't covered.
|
|
9
|
+
|
|
10
|
+
## Requests that should pass or be silenced
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { type ScenarioInput, startScenarios } from 'state-scenarios';
|
|
14
|
+
|
|
15
|
+
declare const scenarios: ScenarioInput[];
|
|
16
|
+
|
|
17
|
+
await startScenarios({
|
|
18
|
+
scenarios,
|
|
19
|
+
allow: ['images.example-cdn.com'], // reaches the real network, even in strict mode
|
|
20
|
+
ignore: [/posthog|segment|sentry|google-analytics|googletagmanager|recaptcha|hcaptcha|intercom/i],
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `allow`: requests the screen needs from the real network, such as CDN images or a maps API.
|
|
25
|
+
- `ignore`: analytics, error reporting, captcha and chat SDKs. Never reported as unhandled; in strict
|
|
26
|
+
mode they get an empty 204 instead of a 501, so most SDKs stay quiet and don't retry.
|
|
27
|
+
- An SDK that still logs errors (some expect a JSON body): don't start it in scenarios,
|
|
28
|
+
`if (!currentSession()?.active) analytics.init(...)`.
|
|
29
|
+
|
|
30
|
+
Patterns are URL substrings or regular expressions. Match the hosts the app actually calls.
|
|
31
|
+
|
|
32
|
+
## Apps that already use MSW
|
|
33
|
+
|
|
34
|
+
Start your worker and pass it in. Scenario entries answer first, then your handlers, then the strict
|
|
35
|
+
catch-all; `destroy()` restores your handlers.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
await worker.start();
|
|
39
|
+
await startScenarios({ scenarios, worker });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Reusing handlers from tests
|
|
43
|
+
|
|
44
|
+
Pass handlers by name, then name them in scenarios. They answer after the scenario's `network`.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { type ScenarioInput, startScenarios } from 'state-scenarios';
|
|
48
|
+
import { statementsEmpty } from './test/handlers'; // the handlers your unit tests use
|
|
49
|
+
|
|
50
|
+
const scenarios: ScenarioInput[] = [
|
|
51
|
+
{ name: 'no-statements', handlers: ['statementsEmpty'], ready: 'text=No statements yet' },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
await startScenarios({ scenarios, handlers: { statementsEmpty } });
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"name": "no-statements",
|
|
60
|
+
"extends": "signed-in",
|
|
61
|
+
"handlers": ["statementsEmpty"],
|
|
62
|
+
"ready": "text=No statements yet"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
An unknown name fails when the scenario opens and lists the known ones. `validate` can't see
|
|
67
|
+
handlers, so it only checks the JSON.
|
|
@@ -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.
|