state-scenarios 0.1.0 → 0.2.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/dist/adapters/cookies.d.ts +11 -0
- package/dist/adapters/cookies.js +27 -0
- package/dist/adapters/cookies.js.map +1 -0
- package/dist/adapters/redux.js +2 -2
- package/dist/adapters/tanstack-query.js +1 -1
- package/dist/adapters/xstate.js +2 -2
- package/dist/adapters/zustand.js +1 -1
- package/dist/{chunk-MLXYB7GV.js → chunk-6L55WKFS.js} +3 -1
- package/dist/chunk-6L55WKFS.js.map +1 -0
- package/dist/{chunk-JIHDABWO.js → chunk-KDH572NI.js} +2 -2
- package/dist/{chunk-M5Q4I3KT.js → chunk-KRCYO64S.js} +2 -1
- package/dist/chunk-KRCYO64S.js.map +1 -0
- package/dist/index.js +27 -15
- package/dist/index.js.map +1 -1
- package/dist/panel.d.ts +1 -1
- package/dist/panel.js +3 -2
- package/dist/panel.js.map +1 -1
- package/dist/playwright.d.ts +6 -0
- package/dist/playwright.js +32 -3
- package/dist/playwright.js.map +1 -1
- package/dist/runtime.d.ts +5 -0
- package/dist/schema.d.ts +2 -0
- package/package.json +7 -2
- package/dist/chunk-M5Q4I3KT.js.map +0 -1
- package/dist/chunk-MLXYB7GV.js.map +0 -1
- /package/dist/{chunk-JIHDABWO.js.map → chunk-KDH572NI.js.map} +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TypedAdapter } from '../runtime.js';
|
|
2
|
+
/**
|
|
3
|
+
* Connects `document.cookie` as `state.cookies` (or your key): `{ "authToken": "fake" }`.
|
|
4
|
+
* Applied before the app renders, so cookie-based auth gates let the scenario in.
|
|
5
|
+
* It overwrites real cookies with the same name: sign in again after leaving scenarios.
|
|
6
|
+
* Inline links can't set it; link to a catalog scenario by name instead.
|
|
7
|
+
*/
|
|
8
|
+
export declare const cookiesAdapter: ({ key, path }?: {
|
|
9
|
+
key?: string;
|
|
10
|
+
path?: string;
|
|
11
|
+
}) => TypedAdapter<Record<string, string>>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/adapters/cookies.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var CookiesSchema = z.record(z.string(), z.string());
|
|
4
|
+
var cookiesAdapter = ({ key = "cookies", path = "/" } = {}) => ({
|
|
5
|
+
key,
|
|
6
|
+
schema: CookiesSchema,
|
|
7
|
+
apply: (cookies) => {
|
|
8
|
+
for (const [name, value] of Object.entries(cookies)) {
|
|
9
|
+
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; path=${path}`;
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
read: () => Object.fromEntries(
|
|
13
|
+
document.cookie.split("; ").filter(Boolean).map((c) => {
|
|
14
|
+
const i = c.indexOf("=");
|
|
15
|
+
return [decodeURIComponent(c.slice(0, i)), decodeURIComponent(c.slice(i + 1))];
|
|
16
|
+
})
|
|
17
|
+
),
|
|
18
|
+
// Cookie libraries (react-cookie, js-cookie) read once at startup.
|
|
19
|
+
inPlace: false,
|
|
20
|
+
// A cookie outlives the scenario and reaches the real backend, so only catalog
|
|
21
|
+
// scenarios, which live in the repo, set it.
|
|
22
|
+
fromLinks: false
|
|
23
|
+
});
|
|
24
|
+
export {
|
|
25
|
+
cookiesAdapter
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=cookies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/cookies.ts"],"sourcesContent":["import { z } from 'zod';\nimport type { TypedAdapter } from '../runtime.js';\n\nconst CookiesSchema = z.record(z.string(), z.string());\n\n/**\n * Connects `document.cookie` as `state.cookies` (or your key): `{ \"authToken\": \"fake\" }`.\n * Applied before the app renders, so cookie-based auth gates let the scenario in.\n * It overwrites real cookies with the same name: sign in again after leaving scenarios.\n * Inline links can't set it; link to a catalog scenario by name instead.\n */\nexport const cookiesAdapter = (\n { key = 'cookies', path = '/' }: { key?: string; path?: string; } = {},\n): TypedAdapter<Record<string, string>> => ({\n key,\n schema: CookiesSchema,\n apply: (cookies) => {\n for (const [name, value] of Object.entries(cookies)) {\n document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; path=${path}`;\n }\n },\n read: () =>\n Object.fromEntries(\n document.cookie.split('; ').filter(Boolean).map((c) => {\n const i = c.indexOf('=');\n\n return [decodeURIComponent(c.slice(0, i)), decodeURIComponent(c.slice(i + 1))];\n }),\n ),\n // Cookie libraries (react-cookie, js-cookie) read once at startup.\n inPlace: false,\n // A cookie outlives the scenario and reaches the real backend, so only catalog\n // scenarios, which live in the repo, set it.\n fromLinks: false,\n});\n"],"mappings":";AAAA,SAAS,SAAS;AAGlB,IAAM,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC;AAQ9C,IAAM,iBAAiB,CAC5B,EAAE,MAAM,WAAW,OAAO,IAAI,IAAsC,CAAC,OAC3B;AAAA,EAC1C;AAAA,EACA,QAAQ;AAAA,EACR,OAAO,CAAC,YAAY;AAClB,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AACnD,eAAS,SAAS,GAAG,mBAAmB,IAAI,CAAC,IAAI,mBAAmB,KAAK,CAAC,UAAU,IAAI;AAAA,IAC1F;AAAA,EACF;AAAA,EACA,MAAM,MACJ,OAAO;AAAA,IACL,SAAS,OAAO,MAAM,IAAI,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,MAAM;AACrD,YAAM,IAAI,EAAE,QAAQ,GAAG;AAEvB,aAAO,CAAC,mBAAmB,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,mBAAmB,EAAE,MAAM,IAAI,CAAC,CAAC,CAAC;AAAA,IAC/E,CAAC;AAAA,EACH;AAAA;AAAA,EAEF,SAAS;AAAA;AAAA;AAAA,EAGT,WAAW;AACb;","names":[]}
|
package/dist/adapters/redux.js
CHANGED
package/dist/adapters/xstate.js
CHANGED
package/dist/adapters/zustand.js
CHANGED
|
@@ -38,6 +38,8 @@ var ScenarioSchema = z.object({
|
|
|
38
38
|
tags: z.array(z.string()).optional(),
|
|
39
39
|
/** Scenario name(s) to build on. Later parents override earlier ones. */
|
|
40
40
|
extends: z.union([z.string(), z.array(z.string())]).optional(),
|
|
41
|
+
/** App route the scenario opens at, e.g. "/orders/42". Used by links, the panel, `shoot` and `openInlineScenario`. */
|
|
42
|
+
path: z.string().startsWith("/").optional(),
|
|
41
43
|
/** Query params the app should start with. null removes an inherited param. */
|
|
42
44
|
url: z.record(z.string(), z.string().nullable()).optional(),
|
|
43
45
|
/**
|
|
@@ -84,4 +86,4 @@ export {
|
|
|
84
86
|
scenarioJsonSchema,
|
|
85
87
|
parseScenario
|
|
86
88
|
};
|
|
87
|
-
//# sourceMappingURL=chunk-
|
|
89
|
+
//# sourceMappingURL=chunk-6L55WKFS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema.ts"],"sourcesContent":["import { z } from 'zod';\n\n/** Scenarios live in JSON files and URLs, so everything they carry is JSON. */\nexport const JsonSchema = z.json();\n\nexport type Json = z.output<typeof JsonSchema>;\n\nexport type JsonObject = { [key: string]: Json; };\n\n/** Anything an app can hand over to be stored as JSON. */\nexport type Serializable = object | string | number | boolean | null;\n\n/**\n * Decode JSON text. Pair with `JSON.stringify` to copy app state into the format scenarios\n * are stored in: functions and undefined drop out, and the result is checked to be JSON.\n */\nexport const parseJson = (text: string): Json => JsonSchema.parse(JSON.parse(text));\n\nconst ResponseSchema = z\n .object({\n status: z.number().int().default(200),\n /** Sent as JSON. */\n body: JsonSchema.optional(),\n /** Sent as text, e.g. an HTML error page. */\n text: z.string().optional(),\n headers: z.record(z.string(), z.string()).optional(),\n /** Milliseconds, or \"infinite\" to hold the request open (loading states). */\n delay: z.union([z.number().nonnegative(), z.literal('infinite')]).optional(),\n })\n .refine((r) => r.body === undefined || r.text === undefined, {\n message: 'Provide \"body\" (JSON) or \"text\", not both',\n });\n\nconst NetworkEntrySchema = z\n .object({\n method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']).default('GET'),\n /** MSW path syntax: \"/api/users/:id\", absolute URLs and wildcards work. */\n path: z.string().min(1),\n /** Only match requests whose query string contains these values. */\n query: z.record(z.string(), z.string()).optional(),\n response: ResponseSchema.optional(),\n /** Successive responses for successive calls; the last one repeats. */\n sequence: z.array(ResponseSchema).min(1).optional(),\n /** Drop the inherited entry with the same method, path and query. */\n remove: z.literal(true).optional(),\n })\n .refine((e) => [e.response, e.sequence, e.remove].filter(Boolean).length === 1, {\n message: 'Provide exactly one of \"response\", \"sequence\" or \"remove\"',\n });\n\nexport const ScenarioSchema = z.object({\n $schema: z.string().optional(),\n // Kebab-case; a combined scenario is named by its parts, e.g. \"signed-in,server-error\".\n name: z.string().regex(/^[a-z0-9][a-z0-9-]*(,[a-z0-9][a-z0-9-]*)*$/, 'Use kebab-case, e.g. \"empty-search\"'),\n /** Not inherited. */\n description: z.string().optional(),\n /** Not inherited. */\n tags: z.array(z.string()).optional(),\n /** Scenario name(s) to build on. Later parents override earlier ones. */\n extends: z.union([z.string(), z.array(z.string())]).optional(),\n /** App route the scenario opens at, e.g. \"/orders/42\". Used by links, the panel, `shoot` and `openInlineScenario`. */\n path: z.string().startsWith('/').optional(),\n /** Query params the app should start with. null removes an inherited param. */\n url: z.record(z.string(), z.string().nullable()).optional(),\n /**\n * A child entry replaces the inherited entry with the same method, path and query.\n * Otherwise earlier entries win, and a scenario's own entries come before its parents'.\n */\n network: z.array(NetworkEntrySchema).optional(),\n /** Per-adapter state, keyed by adapter key. Objects deep-merge; null removes a key. */\n state: z.record(z.string(), JsonSchema).optional(),\n /**\n * Playwright selector that is visible once the UI shows this state,\n * e.g. \"text=Loading countries…\". Not inherited: each state looks different.\n */\n ready: z.string().optional(),\n});\n\nexport type Scenario = z.output<typeof ScenarioSchema>;\n\nexport type ScenarioInput = z.input<typeof ScenarioSchema>;\n\nexport type NetworkEntry = z.output<typeof NetworkEntrySchema>;\n\nexport type ScenarioResponse = z.output<typeof ResponseSchema>;\n\n/**\n * JSON Schema for editors, validators and AI generators.\n * Pass adapter state schemas so `state` is described too.\n */\nexport function scenarioJsonSchema({ state = {} }: { state?: Record<string, z.ZodType>; } = {}) {\n return z.toJSONSchema(\n ScenarioSchema.extend({ state: z.object(state).partial().catchall(JsonSchema).optional() }),\n {\n io: 'input',\n // Refinements aren't representable in JSON Schema; spell the exclusivity rules out.\n override: ({ zodSchema, jsonSchema }) => {\n if (zodSchema === NetworkEntrySchema) {\n jsonSchema.oneOf = ['response', 'sequence', 'remove'].map((key) => ({ required: [key] }));\n }\n\n if (zodSchema === ResponseSchema) {\n jsonSchema.not = { required: ['body', 'text'] };\n }\n },\n },\n );\n}\n\n/** Validate scenario input (from a file, URL or editor) into a Scenario, with readable errors. */\nexport function parseScenario(data: ScenarioInput, source = 'scenario'): Scenario {\n const result = ScenarioSchema.safeParse(data);\n\n if (result.success) return result.data;\n throw new Error(`Invalid scenario \"${data.name || source}\":\\n${z.prettifyError(result.error)}`);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
parseScenario
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-6L55WKFS.js";
|
|
4
4
|
|
|
5
5
|
// src/catalog.ts
|
|
6
6
|
var defineScenarioRules = (rules) => rules;
|
|
@@ -85,4 +85,4 @@ export {
|
|
|
85
85
|
createCatalog,
|
|
86
86
|
deepMerge
|
|
87
87
|
};
|
|
88
|
-
//# sourceMappingURL=chunk-
|
|
88
|
+
//# sourceMappingURL=chunk-KDH572NI.js.map
|
|
@@ -6,6 +6,7 @@ function scenarioHref(resolved, { base = location.href, inline = false, strict =
|
|
|
6
6
|
const url = new URL(base);
|
|
7
7
|
url.search = "";
|
|
8
8
|
url.hash = "";
|
|
9
|
+
if (resolved.path) url.pathname = resolved.path;
|
|
9
10
|
for (const [k, v] of Object.entries(resolved.url ?? {})) {
|
|
10
11
|
if (v !== null) url.searchParams.set(k, v);
|
|
11
12
|
}
|
|
@@ -27,4 +28,4 @@ export {
|
|
|
27
28
|
scenarioHref,
|
|
28
29
|
readInline
|
|
29
30
|
};
|
|
30
|
-
//# sourceMappingURL=chunk-
|
|
31
|
+
//# sourceMappingURL=chunk-KRCYO64S.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/link.ts"],"sourcesContent":["import type { Scenario } from './schema.js';\n\ndeclare global {\n /** Requests the active scenario didn't cover, for tests and agents (see `unhandledRequests`). */\n var stateScenariosUnhandled: string[] | undefined;\n}\n\nexport const PARAM = 'scenario';\n\n/** Fail unmatched requests instead of letting them reach the network. */\nexport const STRICT_PARAM = 'scenario-strict';\n\n/** Inline scenario JSON, kept in the URL hash so it never reaches a server. */\nexport const DATA_KEY = 'scenario-data';\n\n/**\n * A shareable URL that opens the app in a resolved scenario.\n * `inline` embeds the flattened scenario, so the link works without the catalog.\n */\nexport function scenarioHref(\n resolved: Scenario,\n { base = location.href, inline = false, strict = false } = {},\n): string {\n const url = new URL(base);\n url.search = '';\n url.hash = '';\n\n if (resolved.path) url.pathname = resolved.path;\n\n for (const [k, v] of Object.entries(resolved.url ?? {})) {\n if (v !== null) url.searchParams.set(k, v);\n }\n\n if (inline) {\n const { $schema: _schema, extends: _extends, ...snapshot } = resolved;\n url.hash = new URLSearchParams({ [DATA_KEY]: JSON.stringify(snapshot) }).toString();\n } else {\n url.searchParams.set(PARAM, resolved.name);\n }\n\n if (strict) url.searchParams.set(STRICT_PARAM, '');\n\n return url.toString();\n}\n\n/** Inline scenario JSON from a URL hash, if present. */\nexport const readInline = (hash: string) => new URLSearchParams(hash.slice(1)).get(DATA_KEY);\n"],"mappings":";AAOO,IAAM,QAAQ;AAGd,IAAM,eAAe;AAGrB,IAAM,WAAW;AAMjB,SAAS,aACd,UACA,EAAE,OAAO,SAAS,MAAM,SAAS,OAAO,SAAS,MAAM,IAAI,CAAC,GACpD;AACR,QAAM,MAAM,IAAI,IAAI,IAAI;AACxB,MAAI,SAAS;AACb,MAAI,OAAO;AAEX,MAAI,SAAS,KAAM,KAAI,WAAW,SAAS;AAE3C,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,SAAS,OAAO,CAAC,CAAC,GAAG;AACvD,QAAI,MAAM,KAAM,KAAI,aAAa,IAAI,GAAG,CAAC;AAAA,EAC3C;AAEA,MAAI,QAAQ;AACV,UAAM,EAAE,SAAS,SAAS,SAAS,UAAU,GAAG,SAAS,IAAI;AAC7D,QAAI,OAAO,IAAI,gBAAgB,EAAE,CAAC,QAAQ,GAAG,KAAK,UAAU,QAAQ,EAAE,CAAC,EAAE,SAAS;AAAA,EACpF,OAAO;AACL,QAAI,aAAa,IAAI,OAAO,SAAS,IAAI;AAAA,EAC3C;AAEA,MAAI,OAAQ,KAAI,aAAa,IAAI,cAAc,EAAE;AAEjD,SAAO,IAAI,SAAS;AACtB;AAGO,IAAM,aAAa,CAAC,SAAiB,IAAI,gBAAgB,KAAK,MAAM,CAAC,CAAC,EAAE,IAAI,QAAQ;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
toHandlers
|
|
3
|
+
} from "./chunk-U3I7ORTB.js";
|
|
1
4
|
import {
|
|
2
5
|
DATA_KEY,
|
|
3
6
|
PARAM,
|
|
4
7
|
STRICT_PARAM,
|
|
5
8
|
readInline,
|
|
6
9
|
scenarioHref
|
|
7
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-KRCYO64S.js";
|
|
8
11
|
import {
|
|
9
12
|
createCatalog,
|
|
10
13
|
deepMerge,
|
|
11
14
|
defineScenarioRules
|
|
12
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-KDH572NI.js";
|
|
13
16
|
import {
|
|
14
17
|
JsonSchema,
|
|
15
18
|
ScenarioSchema,
|
|
16
19
|
parseJson,
|
|
17
20
|
parseScenario,
|
|
18
21
|
scenarioJsonSchema
|
|
19
|
-
} from "./chunk-
|
|
20
|
-
import {
|
|
21
|
-
toHandlers
|
|
22
|
-
} from "./chunk-U3I7ORTB.js";
|
|
22
|
+
} from "./chunk-6L55WKFS.js";
|
|
23
23
|
|
|
24
24
|
// src/runtime.ts
|
|
25
25
|
var STORAGE_KEY = "state-scenarios:active";
|
|
@@ -29,6 +29,7 @@ function bind(adapter) {
|
|
|
29
29
|
read: () => adapter.read?.(),
|
|
30
30
|
subscribe: (onChange) => adapter.subscribe?.(onChange),
|
|
31
31
|
inPlace: adapter.inPlace !== false,
|
|
32
|
+
fromLinks: adapter.fromLinks !== false,
|
|
32
33
|
decode(state, scenario) {
|
|
33
34
|
if (adapter.schema === void 0) return () => adapter.apply(state);
|
|
34
35
|
const parsed = adapter.schema.safeParse(state);
|
|
@@ -63,9 +64,15 @@ async function startScenarios(options) {
|
|
|
63
64
|
let ownWorker = null;
|
|
64
65
|
const appHandlers = options.worker ? [...options.worker.listHandlers()] : [];
|
|
65
66
|
let destroyed = false;
|
|
66
|
-
|
|
67
|
+
let fromLink = false;
|
|
68
|
+
const prepareStates = (resolved2, adapters, link = fromLink) => adapters.flatMap((adapter) => {
|
|
67
69
|
const state = resolved2.state?.[adapter.key];
|
|
68
|
-
|
|
70
|
+
if (state === void 0) return [];
|
|
71
|
+
if (link && !adapter.fromLinks) {
|
|
72
|
+
console.warn(`[state-scenarios] ignored state.${adapter.key}: a link can't set it`);
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
return [adapter.decode(state, resolved2.name)];
|
|
69
76
|
});
|
|
70
77
|
const applyPrepared = async (prepared2) => {
|
|
71
78
|
for (const apply of prepared2) {
|
|
@@ -85,7 +92,7 @@ async function startScenarios(options) {
|
|
|
85
92
|
const allowed = allow.some(
|
|
86
93
|
(rule) => rule instanceof RegExp ? rule.test(request.url) : request.url.includes(rule)
|
|
87
94
|
);
|
|
88
|
-
if (isPage ||
|
|
95
|
+
if (isPage || isAsset(request.url) || pathname.startsWith("/@") || allowed) {
|
|
89
96
|
return msw.passthrough();
|
|
90
97
|
}
|
|
91
98
|
const label = `${request.method} ${request.url}`;
|
|
@@ -109,7 +116,8 @@ async function startScenarios(options) {
|
|
|
109
116
|
}
|
|
110
117
|
worker = ownWorker = started;
|
|
111
118
|
};
|
|
112
|
-
const activate = (active2, resolved2) => {
|
|
119
|
+
const activate = (active2, resolved2, link) => {
|
|
120
|
+
fromLink = link;
|
|
113
121
|
session.active = active2;
|
|
114
122
|
session.resolved = resolved2;
|
|
115
123
|
const html = document.documentElement;
|
|
@@ -122,12 +130,12 @@ async function startScenarios(options) {
|
|
|
122
130
|
let queue = Promise.resolve();
|
|
123
131
|
const switchTo = async (next, inline) => {
|
|
124
132
|
const resolved2 = catalog.resolve(next);
|
|
125
|
-
const prepared2 = prepareStates(resolved2, boundAdapters());
|
|
133
|
+
const prepared2 = prepareStates(resolved2, boundAdapters(), inline);
|
|
126
134
|
const prev = session.resolved;
|
|
127
135
|
const nextKeys = Object.keys(resolved2.state ?? {});
|
|
128
136
|
const mocksNetwork = worker !== null || !!resolved2.network?.length || session.strict;
|
|
129
|
-
const inPlace = prev !== null && JSON.stringify(appParams(location.search)) === JSON.stringify(sortedParams(resolved2.url)) && (!mocksNetwork || !!options.refresh) && Object.keys(prev.state ?? {}).every((k) => nextKeys.includes(k)) && nextKeys.every((k) => connected.get(k)?.adapter.inPlace === true);
|
|
130
137
|
const href = scenarioHref(resolved2, { inline, strict: session.strict });
|
|
138
|
+
const inPlace = prev !== null && new URL(href).pathname === location.pathname && JSON.stringify(appParams(location.search)) === JSON.stringify(sortedParams(resolved2.url)) && (!mocksNetwork || !!options.refresh) && Object.keys(prev.state ?? {}).every((k) => nextKeys.includes(k)) && nextKeys.every((k) => connected.get(k)?.adapter.inPlace === true);
|
|
131
139
|
if (!inPlace) {
|
|
132
140
|
navigate(href);
|
|
133
141
|
return;
|
|
@@ -140,7 +148,7 @@ async function startScenarios(options) {
|
|
|
140
148
|
await applyPrepared(prepared2);
|
|
141
149
|
if (destroyed) return;
|
|
142
150
|
unhandled.splice(0);
|
|
143
|
-
activate(next, resolved2);
|
|
151
|
+
activate(next, resolved2, inline);
|
|
144
152
|
const at = new URL(href);
|
|
145
153
|
history.replaceState(history.state, "", at);
|
|
146
154
|
const data = readInline(at.hash);
|
|
@@ -237,7 +245,7 @@ async function startScenarios(options) {
|
|
|
237
245
|
try {
|
|
238
246
|
const active2 = selection.data ? parseScenario(JSON.parse(selection.data), "inline scenario") : lookup(catalog, selection.name ?? "");
|
|
239
247
|
const resolved2 = catalog.resolve(active2);
|
|
240
|
-
return { active: active2, resolved: resolved2, prepared: prepareStates(resolved2, initial) };
|
|
248
|
+
return { active: active2, resolved: resolved2, prepared: prepareStates(resolved2, initial, !!selection.data) };
|
|
241
249
|
} catch (err) {
|
|
242
250
|
store(null);
|
|
243
251
|
throw err;
|
|
@@ -252,10 +260,14 @@ async function startScenarios(options) {
|
|
|
252
260
|
history.replaceState(history.state, "", url);
|
|
253
261
|
await applyPrepared(prepared);
|
|
254
262
|
if (destroyed) return session;
|
|
255
|
-
activate(active, resolved);
|
|
263
|
+
activate(active, resolved, !!selection.data);
|
|
256
264
|
emit();
|
|
257
265
|
return session;
|
|
258
266
|
}
|
|
267
|
+
var isAsset = (href) => {
|
|
268
|
+
const url = new URL(href);
|
|
269
|
+
return url.protocol === "file:" || url.hostname === "fonts.googleapis.com" || url.pathname.includes("node_modules") || url.pathname.includes("@vite") || /\.(s?css|less|m?jsx?|m?tsx?|html|ttf|otf|woff2?|eot|gif|jpe?g|png|avif|webp|svg|mp4|webm|ogg|mov|mp3|wav|flac|aac|pdf|txt|csv|json|xml|md|zip|tar|gz|rar|7z)$/i.test(url.pathname);
|
|
270
|
+
};
|
|
259
271
|
var byKey = ([a], [b]) => a.localeCompare(b);
|
|
260
272
|
var appParams = (search) => [...new URLSearchParams(search)].filter(([k]) => k !== PARAM && k !== STRICT_PARAM).toSorted(byKey);
|
|
261
273
|
var sortedParams = (url) => Object.entries(url ?? {}).filter(([, v]) => v !== null).toSorted(byKey);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["import type { SetupWorker } from 'msw/browser';\nimport type { z } from 'zod';\nimport { type Catalog, createCatalog, type ScenarioRule } from './catalog.js';\nimport { DATA_KEY, PARAM, readInline, scenarioHref, STRICT_PARAM } from './link.js';\nimport {\n type Json,\n parseJson,\n parseScenario,\n type Scenario,\n type ScenarioInput,\n type Serializable,\n} from './schema.js';\n\nconst STORAGE_KEY = 'state-scenarios:active';\n\ninterface AdapterBase {\n /** Key under `scenario.state` this adapter reads. */\n key: string;\n /** Current state as JSON, for the panel's live editor and \"Save as scenario\". */\n read?(): Json;\n /** Call `onChange` when the state changes; return an unsubscribe. */\n subscribe?(onChange: () => void): () => void;\n /** False when new state only takes effect on a reload, e.g. a snapshot an actor starts from. */\n inPlace?: boolean;\n}\n\n/** Decodes scenario JSON with `schema`, so `apply` receives typed state. */\nexport interface TypedAdapter<T> extends AdapterBase {\n /** Also feeds `scenarioJsonSchema`. */\n schema: z.ZodType<T>;\n apply(state: T): void | Promise<void>;\n}\n\n/** Receives the scenario's JSON as it is. */\nexport interface JsonAdapter extends AdapterBase {\n schema?: undefined;\n apply(state: Json): void | Promise<void>;\n}\n\n/** Connects one slice of app state (a store, a cache, a reducer) to scenarios and the panel. */\nexport type Adapter<T = Json> = TypedAdapter<T> | JsonAdapter;\n\nexport interface Connection<T extends Serializable> {\n /** Push the app's current state (for state the library can't `read`, e.g. useReducer). */\n update(state: T): void;\n disconnect(): void;\n}\n\nexport interface StartOptions {\n /** Scenario objects, e.g. `import.meta.glob<ScenarioInput>('./scenarios/*.json')`, or built in code. */\n scenarios: ScenarioInput[];\n /** Scenario to use when the URL names none. Omit to leave the app untouched. */\n defaultScenario?: string;\n /** Adapters available before render. Components can `connect` more later. */\n adapters?: Adapter<unknown>[];\n rules?: ScenarioRule[];\n /**\n * Answer unmatched requests with 501 instead of passing them to the real network.\n * Assets and page loads always pass. Also switched on by `?scenario-strict`.\n */\n strict?: boolean;\n /** Requests that may reach the network in strict mode (URL substring or RegExp). */\n allow?: (string | RegExp)[];\n /**\n * Called after an in-place switch changes network fixtures, so the app refetches,\n * e.g. `() => queryClient.resetQueries()`. Without it, such switches reload the page.\n */\n refresh?: () => void | Promise<void>;\n /**\n * The app's own MSW worker (or a `setupServer()` in tests), instead of starting one.\n * Its handlers stay active beneath the scenario's, and are restored by `destroy()`.\n * The app owns its lifecycle: start it before `startScenarios`.\n */\n worker?: MswWorker;\n /** Passed to `worker.start()` when state-scenarios starts its own worker. */\n workerOptions?: Parameters<SetupWorker['start']>[0];\n}\n\n/** The parts of an MSW worker or server state-scenarios uses. */\nexport type MswWorker = Pick<SetupWorker, 'resetHandlers' | 'listHandlers'>;\n\nexport interface ScenarioSession {\n catalog: Catalog;\n /** The scenario as authored (with `extends`), or null when none is active. */\n active: Scenario | null;\n /** The active scenario with `extends` flattened. */\n resolved: Scenario | null;\n strict: boolean;\n /** Non-asset requests no scenario entry matched (live list). */\n unhandled: string[];\n /** Keys of connected adapters. */\n keys(): string[];\n /** The connected adapter's current state, as JSON. */\n read(key: string): Json | undefined;\n /** Validate and apply state to a connected adapter now, without changing scenario. */\n applyState(key: string, state: Json): Promise<void>;\n /** Connect an adapter; the active scenario's state for its key is applied at once. */\n connect<T extends Serializable>(adapter: Adapter<T>): Connection<T>;\n /**\n * Switch to a catalog scenario: in place when only connected state (or network, with\n * `refresh`) changes, otherwise by reloading. Names combine: \"signed-in,server-error\".\n */\n open(names: string): Promise<void>;\n /** Switch to a scenario that isn't in the catalog, e.g. one edited in the panel. */\n openInline(scenario: Scenario): Promise<void>;\n onChange(listener: () => void): () => void;\n /**\n * Tear down: unsubscribe adapters, drop listeners and queued switches, and stop the\n * worker this session started (or restore the app's own worker's handlers).\n * For HMR and unmounting. The page keeps whatever state it shows.\n */\n destroy(): void;\n}\n\n/** An adapter with its type erased behind decode-then-apply. */\ninterface Bound {\n key: string;\n /** Validate now; the returned function applies. */\n decode(state: Json, scenario: string): () => void | Promise<void>;\n /** Undefined when the adapter can't read (state is pushed through `connection.update`). */\n read(): Json | undefined;\n subscribe(onChange: () => void): (() => void) | undefined;\n inPlace: boolean;\n}\n\nfunction bind<T>(adapter: Adapter<T>): Bound {\n return {\n key: adapter.key,\n read: () => adapter.read?.(),\n subscribe: (onChange) => adapter.subscribe?.(onChange),\n inPlace: adapter.inPlace !== false,\n decode(state, scenario) {\n if (adapter.schema === undefined) return () => adapter.apply(state);\n const parsed = adapter.schema.safeParse(state);\n\n if (!parsed.success) {\n throw new Error(`Scenario \"${scenario}\": invalid state[\"${adapter.key}\"]: ${parsed.error.message}`);\n }\n\n return () => adapter.apply(parsed.data);\n },\n };\n}\n\ninterface Connected {\n adapter: Bound;\n /** Last state pushed through `connection.update`; converted to JSON when read. */\n value?: Serializable;\n unsubscribe?: () => void;\n}\n\nlet current: ScenarioSession | null = null;\n\n/** The session whose scenario the <html> markers describe; only it may clear them. */\nlet markerOwner: ScenarioSession | null = null;\n\n/** The session started by `startScenarios`, if any (null in production builds that skip it). */\nexport const currentSession = () => current;\n\ninterface Selection {\n name?: string;\n data?: string;\n strict?: boolean;\n}\n\n/**\n * Read the scenario from the URL (or the tab's last one), mock the network if the\n * scenario needs it, apply URL and adapter state. Await this before rendering the app.\n */\nexport async function startScenarios(options: StartOptions): Promise<ScenarioSession> {\n const catalog = createCatalog(options.scenarios, { rules: options.rules });\n const url = new URL(location.href);\n const selection = select(url, options.defaultScenario);\n const unhandled: string[] = [];\n globalThis.stateScenariosUnhandled = unhandled;\n\n const connected = new Map<string, Connected>();\n const listeners = new Set<() => void>();\n\n // One failing listener (a panel, an app hook) must not stop the others or fail a switch.\n const emit = () => {\n for (const listener of listeners) {\n try {\n listener();\n } catch (err) {\n console.error('[state-scenarios] onChange listener failed', err);\n }\n }\n };\n\n // The app's worker, if given, else one this session starts on first need.\n let worker: MswWorker | null = options.worker ?? null;\n let ownWorker: SetupWorker | null = null;\n const appHandlers = options.worker ? [...options.worker.listHandlers()] : [];\n let destroyed = false;\n\n /** Validate every adapter's state up front, so nothing changes if any of it is invalid. */\n const prepareStates = (resolved: Scenario, adapters: Bound[]) =>\n adapters.flatMap((adapter) => {\n const state = resolved.state?.[adapter.key];\n\n return state === undefined ? [] : [adapter.decode(state, resolved.name)];\n });\n\n const applyPrepared = async (prepared: ReturnType<typeof prepareStates>) => {\n for (const apply of prepared) {\n // Destroyed while an earlier adapter was applying: leave the rest untouched.\n if (destroyed) return;\n await apply();\n }\n };\n\n const boundAdapters = () => [...connected.values()].map((entry) => entry.adapter);\n\n // MSW loads only when a scenario mocks the network or strict mode needs a catch-all.\n const setNetwork = async (resolved: Scenario) => {\n if (!resolved.network?.length && !session.strict && !worker) return;\n const [{ toHandlers }, msw] = await Promise.all([import('./network.js'), import('msw')]);\n\n if (destroyed) return;\n const allow = options.allow ?? [];\n\n const fallback = msw.http.all('*', ({ request }) => {\n const { pathname } = new URL(request.url);\n const isPage = request.mode === 'navigate' || !!request.headers.get('accept')?.includes('text/html');\n\n const allowed = allow.some((rule) =>\n rule instanceof RegExp ? rule.test(request.url) : request.url.includes(rule)\n );\n\n if (isPage || msw.isCommonAssetRequest(request) || pathname.startsWith('/@') || allowed) {\n return msw.passthrough();\n }\n\n const label = `${request.method} ${request.url}`;\n unhandled.push(label);\n emit();\n\n if (!session.strict) return msw.passthrough();\n console.error(`[state-scenarios] \"${resolved.name}\" has no handler for ${label}`);\n\n return msw.HttpResponse.json({ message: `No scenario handler for ${label}` }, { status: 501 });\n });\n\n // Scenario first, then the app's own handlers, then the catch-all.\n const handlers = [...toHandlers(resolved.network), ...appHandlers, fallback];\n\n if (worker) {\n worker.resetHandlers(...handlers);\n\n return;\n }\n\n const { setupWorker } = await import('msw/browser');\n const started = setupWorker(...handlers);\n await started.start({ quiet: true, ...options.workerOptions });\n\n // Destroyed while starting: don't leave a worker running.\n if (destroyed) {\n void started.stop();\n\n return;\n }\n\n worker = ownWorker = started;\n };\n\n const activate = (active: Scenario, resolved: Scenario) => {\n session.active = active;\n session.resolved = resolved;\n // Tests and agents wait on these: html[data-scenario=\"name\"], then the ready selector.\n const html = document.documentElement;\n markerOwner = session;\n html.dataset.scenario = resolved.name;\n\n if (resolved.ready) html.dataset.scenarioReady = resolved.ready;\n else delete html.dataset.scenarioReady;\n };\n\n let transition = 0;\n let queue: Promise<void> = Promise.resolve();\n\n const switchTo = async (next: Scenario, inline: boolean) => {\n const resolved = catalog.resolve(next);\n // Validate before choosing reload or in place: a rejected switch must leave the app as it was.\n const prepared = prepareStates(resolved, boundAdapters());\n\n const prev = session.resolved;\n const nextKeys = Object.keys(resolved.state ?? {});\n // Opening a scenario always restarts its network: fresh handler sequences, refetched data.\n const mocksNetwork = worker !== null || !!resolved.network?.length || session.strict;\n\n const inPlace = prev !== null\n // Compare with the app's actual URL: URL-driven UI only resets on a reload.\n && JSON.stringify(appParams(location.search)) === JSON.stringify(sortedParams(resolved.url))\n && (!mocksNetwork || !!options.refresh)\n // Can't un-apply state, and can't apply state nobody is connected to receive.\n && Object.keys(prev.state ?? {}).every((k) => nextKeys.includes(k))\n && nextKeys.every((k) => connected.get(k)?.adapter.inPlace === true);\n\n const href = scenarioHref(resolved, { inline, strict: session.strict });\n\n if (!inPlace) {\n navigate(href);\n\n return;\n }\n\n // After every await: a destroyed session must not touch the app, URL or storage.\n if (mocksNetwork) {\n await setNetwork(resolved);\n\n if (destroyed) return;\n // Kick off the refetch but don't wait: a scenario may hold requests open forever.\n Promise.resolve(options.refresh?.()).catch((err) => console.error('[state-scenarios]', err));\n }\n\n await applyPrepared(prepared);\n\n if (destroyed) return;\n unhandled.splice(0);\n activate(next, resolved);\n const at = new URL(href);\n history.replaceState(history.state, '', at);\n const data = readInline(at.hash);\n store(data ? { data, strict: session.strict } : { name: next.name, strict: session.strict });\n emit();\n };\n\n // Switches run one at a time, and one superseded while queued is skipped,\n // so a slow earlier switch can never land on top of a later one.\n const enqueue = (next: () => Scenario, inline: boolean) => {\n if (destroyed) return Promise.reject(new Error('This scenario session was destroyed'));\n const id = ++transition;\n const run = queue.then(() => (id === transition ? switchTo(next(), inline) : undefined));\n queue = run.catch(() => {});\n\n return run;\n };\n\n const attach = (adapter: Bound): Connection<Serializable> => {\n const entry: Connected = { adapter };\n connected.set(adapter.key, entry);\n entry.unsubscribe = adapter.subscribe?.(emit);\n emit();\n\n return {\n update(state) {\n entry.value = state;\n emit();\n },\n disconnect() {\n entry.unsubscribe?.();\n\n if (connected.get(adapter.key) === entry) connected.delete(adapter.key);\n emit();\n },\n };\n };\n\n const session: ScenarioSession = {\n catalog,\n active: null,\n resolved: null,\n strict: options.strict ?? !!selection?.strict,\n unhandled,\n keys: () => [...connected.keys()],\n read(key) {\n const entry = connected.get(key);\n const read = entry?.adapter.read();\n\n if (read !== undefined) return read;\n\n return entry?.value === undefined ? undefined : parseJson(JSON.stringify(entry.value));\n },\n async applyState(key, state) {\n const entry = connected.get(key);\n\n if (!entry) throw new Error(`No adapter connected for \"${key}\"`);\n await entry.adapter.decode(state, session.active?.name ?? 'live')();\n },\n connect(adapter) {\n const bound = bind(adapter);\n const noop = { update() {}, disconnect() {} };\n\n if (destroyed) return noop;\n const connection = attach(bound);\n\n if (session.resolved) {\n applyPrepared(prepareStates(session.resolved, [bound])).catch((err) =>\n console.error('[state-scenarios]', err)\n );\n }\n\n return connection;\n },\n open: (names) => enqueue(() => lookup(catalog, names), false),\n openInline: (scenario) => enqueue(() => scenario, true),\n onChange(listener) {\n listeners.add(listener);\n\n return () => listeners.delete(listener);\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n transition++; // drop queued switches\n\n for (const entry of connected.values()) entry.unsubscribe?.();\n connected.clear();\n listeners.clear();\n\n if (ownWorker) void ownWorker.stop();\n else worker?.resetHandlers(...appHandlers);\n worker = ownWorker = null;\n\n if (current === session) current = null;\n\n if (globalThis.stateScenariosUnhandled === unhandled) globalThis.stateScenariosUnhandled = undefined;\n\n if (markerOwner === session) {\n markerOwner = null;\n delete document.documentElement.dataset.scenario;\n delete document.documentElement.dataset.scenarioReady;\n }\n },\n };\n\n current = session;\n\n // Adapters connect whether or not a scenario is selected, so the plain app can be\n // inspected and captured too. Only applying scenario state is conditional.\n const initial = (options.adapters ?? []).map(bind);\n\n for (const adapter of initial) attach(adapter);\n\n if (!selection) return session;\n\n // A selection that doesn't load must not stick to the tab and break every later page load.\n const loaded = (() => {\n try {\n const active = selection.data\n ? parseScenario(JSON.parse(selection.data), 'inline scenario')\n : lookup(catalog, selection.name ?? '');\n\n const resolved = catalog.resolve(active);\n\n return { active, resolved, prepared: prepareStates(resolved, initial) };\n } catch (err) {\n store(null);\n throw err;\n }\n })();\n\n const { active, resolved, prepared } = loaded;\n await setNetwork(resolved);\n\n if (destroyed) return session;\n\n // Only fill params that are missing, so in-app navigation survives a reload.\n for (const [k, v] of Object.entries(resolved.url ?? {})) {\n if (v !== null && !url.searchParams.has(k)) url.searchParams.set(k, v);\n }\n\n history.replaceState(history.state, '', url);\n // Apply (awaiting async adapters) before the app renders.\n await applyPrepared(prepared);\n\n if (destroyed) return session;\n activate(active, resolved);\n emit();\n\n return session;\n}\n\nconst byKey = ([a]: [string, string | null], [b]: [string, string | null]) => a.localeCompare(b);\n\n/** The app's own query params (scenario params excluded), sorted for comparison. */\nconst appParams = (search: string) =>\n [...new URLSearchParams(search)].filter(([k]) => k !== PARAM && k !== STRICT_PARAM).toSorted(byKey);\n\nconst sortedParams = (url: Scenario['url']) =>\n Object.entries(url ?? {}).filter(([, v]) => v !== null).toSorted(byKey);\n\n/** Go to a scenario link, reloading even when only the hash changes. */\nexport function navigate(href: string) {\n const next = new URL(href, location.href);\n\n if (next.pathname + next.search === location.pathname + location.search) {\n history.replaceState(history.state, '', next);\n location.reload();\n } else {\n location.href = next.toString();\n }\n}\n\n/**\n * URL first (`?scenario=name` or `#scenario-data=...`), then the tab's last\n * selection, so navigation that drops the query keeps the scenario.\n * `?scenario=` (empty) resets to the default.\n */\nfunction select(url: URL, fallback?: string): Selection | null {\n const data = readInline(url.hash);\n const name = url.searchParams.get(PARAM);\n const strict = url.searchParams.has(STRICT_PARAM);\n\n let selection: Selection | null = null;\n\n if (data) selection = { data, strict };\n else if (name) selection = { name, strict };\n else if (name === '') {\n url.searchParams.delete(PARAM);\n store(null);\n } else {\n const stored = load();\n // An explicit ?scenario-strict in the current URL wins over the stored flag.\n selection = stored && { ...stored, strict: stored.strict || strict };\n\n // Restore the selection into the URL so it stays shareable.\n if (selection?.name) url.searchParams.set(PARAM, selection.name);\n\n if (selection?.data) url.hash = new URLSearchParams({ [DATA_KEY]: selection.data }).toString();\n\n if (selection?.strict) url.searchParams.set(STRICT_PARAM, '');\n }\n\n if (selection) store(selection);\n\n return selection ?? (fallback ? { name: fallback, strict } : null);\n}\n\n/** One name, or several joined with commas: later ones win, like `extends`. */\nfunction lookup(catalog: Catalog, names: string): Scenario {\n const list = names.split(',').map((n) => n.trim());\n\n for (const name of list) {\n if (catalog.get(name)) continue;\n const known = catalog.list.map((s) => s.name).join(', ');\n throw new Error(`Unknown scenario \"${name}\". Known scenarios: ${known}`);\n }\n\n const [only] = list;\n const single = list.length === 1 && only !== undefined ? catalog.get(only) : undefined;\n\n return single ?? { name: list.join(','), extends: list };\n}\n\nfunction store(selection: Selection | null) {\n try {\n if (selection) sessionStorage.setItem(STORAGE_KEY, JSON.stringify(selection));\n else sessionStorage.removeItem(STORAGE_KEY);\n } catch {}\n}\n\nfunction load(): Selection | null {\n try {\n return JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? 'null');\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":["resolved","prepared","toHandlers","active"]}
|
|
1
|
+
{"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["import type { SetupWorker } from 'msw/browser';\nimport type { z } from 'zod';\nimport { type Catalog, createCatalog, type ScenarioRule } from './catalog.js';\nimport { DATA_KEY, PARAM, readInline, scenarioHref, STRICT_PARAM } from './link.js';\nimport {\n type Json,\n parseJson,\n parseScenario,\n type Scenario,\n type ScenarioInput,\n type Serializable,\n} from './schema.js';\n\nconst STORAGE_KEY = 'state-scenarios:active';\n\ninterface AdapterBase {\n /** Key under `scenario.state` this adapter reads. */\n key: string;\n /** Current state as JSON, for the panel's live editor and \"Save as scenario\". */\n read?(): Json;\n /** Call `onChange` when the state changes; return an unsubscribe. */\n subscribe?(onChange: () => void): () => void;\n /** False when new state only takes effect on a reload, e.g. a snapshot an actor starts from. */\n inPlace?: boolean;\n /**\n * False to apply this state from catalog scenarios only, and skip it for inline links.\n * For state that outlives the scenario, such as a cookie the real backend reads.\n */\n fromLinks?: boolean;\n}\n\n/** Decodes scenario JSON with `schema`, so `apply` receives typed state. */\nexport interface TypedAdapter<T> extends AdapterBase {\n /** Also feeds `scenarioJsonSchema`. */\n schema: z.ZodType<T>;\n apply(state: T): void | Promise<void>;\n}\n\n/** Receives the scenario's JSON as it is. */\nexport interface JsonAdapter extends AdapterBase {\n schema?: undefined;\n apply(state: Json): void | Promise<void>;\n}\n\n/** Connects one slice of app state (a store, a cache, a reducer) to scenarios and the panel. */\nexport type Adapter<T = Json> = TypedAdapter<T> | JsonAdapter;\n\nexport interface Connection<T extends Serializable> {\n /** Push the app's current state (for state the library can't `read`, e.g. useReducer). */\n update(state: T): void;\n disconnect(): void;\n}\n\nexport interface StartOptions {\n /** Scenario objects, e.g. `import.meta.glob<ScenarioInput>('./scenarios/*.json')`, or built in code. */\n scenarios: ScenarioInput[];\n /** Scenario to use when the URL names none. Omit to leave the app untouched. */\n defaultScenario?: string;\n /** Adapters available before render. Components can `connect` more later. */\n adapters?: Adapter<unknown>[];\n rules?: ScenarioRule[];\n /**\n * Answer unmatched requests with 501 instead of passing them to the real network.\n * Assets and page loads always pass. Also switched on by `?scenario-strict`.\n */\n strict?: boolean;\n /** Requests that may reach the network in strict mode (URL substring or RegExp). */\n allow?: (string | RegExp)[];\n /**\n * Called after an in-place switch changes network fixtures, so the app refetches,\n * e.g. `() => queryClient.resetQueries()`. Without it, such switches reload the page.\n */\n refresh?: () => void | Promise<void>;\n /**\n * The app's own MSW worker (or a `setupServer()` in tests), instead of starting one.\n * Its handlers stay active beneath the scenario's, and are restored by `destroy()`.\n * The app owns its lifecycle: start it before `startScenarios`.\n */\n worker?: MswWorker;\n /** Passed to `worker.start()` when state-scenarios starts its own worker. */\n workerOptions?: Parameters<SetupWorker['start']>[0];\n}\n\n/** The parts of an MSW worker or server state-scenarios uses. */\nexport type MswWorker = Pick<SetupWorker, 'resetHandlers' | 'listHandlers'>;\n\nexport interface ScenarioSession {\n catalog: Catalog;\n /** The scenario as authored (with `extends`), or null when none is active. */\n active: Scenario | null;\n /** The active scenario with `extends` flattened. */\n resolved: Scenario | null;\n strict: boolean;\n /** Non-asset requests no scenario entry matched (live list). */\n unhandled: string[];\n /** Keys of connected adapters. */\n keys(): string[];\n /** The connected adapter's current state, as JSON. */\n read(key: string): Json | undefined;\n /** Validate and apply state to a connected adapter now, without changing scenario. */\n applyState(key: string, state: Json): Promise<void>;\n /** Connect an adapter; the active scenario's state for its key is applied at once. */\n connect<T extends Serializable>(adapter: Adapter<T>): Connection<T>;\n /**\n * Switch to a catalog scenario: in place when only connected state (or network, with\n * `refresh`) changes, otherwise by reloading. Names combine: \"signed-in,server-error\".\n */\n open(names: string): Promise<void>;\n /** Switch to a scenario that isn't in the catalog, e.g. one edited in the panel. */\n openInline(scenario: Scenario): Promise<void>;\n onChange(listener: () => void): () => void;\n /**\n * Tear down: unsubscribe adapters, drop listeners and queued switches, and stop the\n * worker this session started (or restore the app's own worker's handlers).\n * For HMR and unmounting. The page keeps whatever state it shows.\n */\n destroy(): void;\n}\n\n/** An adapter with its type erased behind decode-then-apply. */\ninterface Bound {\n key: string;\n /** Validate now; the returned function applies. */\n decode(state: Json, scenario: string): () => void | Promise<void>;\n /** Undefined when the adapter can't read (state is pushed through `connection.update`). */\n read(): Json | undefined;\n subscribe(onChange: () => void): (() => void) | undefined;\n inPlace: boolean;\n fromLinks: boolean;\n}\n\nfunction bind<T>(adapter: Adapter<T>): Bound {\n return {\n key: adapter.key,\n read: () => adapter.read?.(),\n subscribe: (onChange) => adapter.subscribe?.(onChange),\n inPlace: adapter.inPlace !== false,\n fromLinks: adapter.fromLinks !== false,\n decode(state, scenario) {\n if (adapter.schema === undefined) return () => adapter.apply(state);\n const parsed = adapter.schema.safeParse(state);\n\n if (!parsed.success) {\n throw new Error(`Scenario \"${scenario}\": invalid state[\"${adapter.key}\"]: ${parsed.error.message}`);\n }\n\n return () => adapter.apply(parsed.data);\n },\n };\n}\n\ninterface Connected {\n adapter: Bound;\n /** Last state pushed through `connection.update`; converted to JSON when read. */\n value?: Serializable;\n unsubscribe?: () => void;\n}\n\nlet current: ScenarioSession | null = null;\n\n/** The session whose scenario the <html> markers describe; only it may clear them. */\nlet markerOwner: ScenarioSession | null = null;\n\n/** The session started by `startScenarios`, if any (null in production builds that skip it). */\nexport const currentSession = () => current;\n\ninterface Selection {\n name?: string;\n data?: string;\n strict?: boolean;\n}\n\n/**\n * Read the scenario from the URL (or the tab's last one), mock the network if the\n * scenario needs it, apply URL and adapter state. Await this before rendering the app.\n */\nexport async function startScenarios(options: StartOptions): Promise<ScenarioSession> {\n const catalog = createCatalog(options.scenarios, { rules: options.rules });\n const url = new URL(location.href);\n const selection = select(url, options.defaultScenario);\n const unhandled: string[] = [];\n globalThis.stateScenariosUnhandled = unhandled;\n\n const connected = new Map<string, Connected>();\n const listeners = new Set<() => void>();\n\n // One failing listener (a panel, an app hook) must not stop the others or fail a switch.\n const emit = () => {\n for (const listener of listeners) {\n try {\n listener();\n } catch (err) {\n console.error('[state-scenarios] onChange listener failed', err);\n }\n }\n };\n\n // The app's worker, if given, else one this session starts on first need.\n let worker: MswWorker | null = options.worker ?? null;\n let ownWorker: SetupWorker | null = null;\n const appHandlers = options.worker ? [...options.worker.listHandlers()] : [];\n let destroyed = false;\n\n // Whether the active scenario came from an inline link.\n let fromLink = false;\n\n /** Validate every adapter's state up front, so nothing changes if any of it is invalid. */\n const prepareStates = (resolved: Scenario, adapters: Bound[], link = fromLink) =>\n adapters.flatMap((adapter) => {\n const state = resolved.state?.[adapter.key];\n\n if (state === undefined) return [];\n\n if (link && !adapter.fromLinks) {\n console.warn(`[state-scenarios] ignored state.${adapter.key}: a link can't set it`);\n\n return [];\n }\n\n return [adapter.decode(state, resolved.name)];\n });\n\n const applyPrepared = async (prepared: ReturnType<typeof prepareStates>) => {\n for (const apply of prepared) {\n // Destroyed while an earlier adapter was applying: leave the rest untouched.\n if (destroyed) return;\n await apply();\n }\n };\n\n const boundAdapters = () => [...connected.values()].map((entry) => entry.adapter);\n\n // MSW loads only when a scenario mocks the network or strict mode needs a catch-all.\n const setNetwork = async (resolved: Scenario) => {\n if (!resolved.network?.length && !session.strict && !worker) return;\n const [{ toHandlers }, msw] = await Promise.all([import('./network.js'), import('msw')]);\n\n if (destroyed) return;\n const allow = options.allow ?? [];\n\n const fallback = msw.http.all('*', ({ request }) => {\n const { pathname } = new URL(request.url);\n const isPage = request.mode === 'navigate' || !!request.headers.get('accept')?.includes('text/html');\n\n const allowed = allow.some((rule) =>\n rule instanceof RegExp ? rule.test(request.url) : request.url.includes(rule)\n );\n\n if (isPage || isAsset(request.url) || pathname.startsWith('/@') || allowed) {\n return msw.passthrough();\n }\n\n const label = `${request.method} ${request.url}`;\n unhandled.push(label);\n emit();\n\n if (!session.strict) return msw.passthrough();\n console.error(`[state-scenarios] \"${resolved.name}\" has no handler for ${label}`);\n\n return msw.HttpResponse.json({ message: `No scenario handler for ${label}` }, { status: 501 });\n });\n\n // Scenario first, then the app's own handlers, then the catch-all.\n const handlers = [...toHandlers(resolved.network), ...appHandlers, fallback];\n\n if (worker) {\n worker.resetHandlers(...handlers);\n\n return;\n }\n\n const { setupWorker } = await import('msw/browser');\n const started = setupWorker(...handlers);\n await started.start({ quiet: true, ...options.workerOptions });\n\n // Destroyed while starting: don't leave a worker running.\n if (destroyed) {\n void started.stop();\n\n return;\n }\n\n worker = ownWorker = started;\n };\n\n const activate = (active: Scenario, resolved: Scenario, link: boolean) => {\n fromLink = link;\n session.active = active;\n session.resolved = resolved;\n // Tests and agents wait on these: html[data-scenario=\"name\"], then the ready selector.\n const html = document.documentElement;\n markerOwner = session;\n html.dataset.scenario = resolved.name;\n\n if (resolved.ready) html.dataset.scenarioReady = resolved.ready;\n else delete html.dataset.scenarioReady;\n };\n\n let transition = 0;\n let queue: Promise<void> = Promise.resolve();\n\n const switchTo = async (next: Scenario, inline: boolean) => {\n const resolved = catalog.resolve(next);\n // Validate before choosing reload or in place: a rejected switch must leave the app as it was.\n const prepared = prepareStates(resolved, boundAdapters(), inline);\n\n const prev = session.resolved;\n const nextKeys = Object.keys(resolved.state ?? {});\n // Opening a scenario always restarts its network: fresh handler sequences, refetched data.\n const mocksNetwork = worker !== null || !!resolved.network?.length || session.strict;\n\n const href = scenarioHref(resolved, { inline, strict: session.strict });\n\n const inPlace = prev !== null\n // Compare with the app's actual URL: URL-driven UI only resets on a reload.\n && new URL(href).pathname === location.pathname\n && JSON.stringify(appParams(location.search)) === JSON.stringify(sortedParams(resolved.url))\n && (!mocksNetwork || !!options.refresh)\n // Can't un-apply state, and can't apply state nobody is connected to receive.\n && Object.keys(prev.state ?? {}).every((k) => nextKeys.includes(k))\n && nextKeys.every((k) => connected.get(k)?.adapter.inPlace === true);\n\n if (!inPlace) {\n navigate(href);\n\n return;\n }\n\n // After every await: a destroyed session must not touch the app, URL or storage.\n if (mocksNetwork) {\n await setNetwork(resolved);\n\n if (destroyed) return;\n // Kick off the refetch but don't wait: a scenario may hold requests open forever.\n Promise.resolve(options.refresh?.()).catch((err) => console.error('[state-scenarios]', err));\n }\n\n await applyPrepared(prepared);\n\n if (destroyed) return;\n unhandled.splice(0);\n activate(next, resolved, inline);\n const at = new URL(href);\n history.replaceState(history.state, '', at);\n const data = readInline(at.hash);\n store(data ? { data, strict: session.strict } : { name: next.name, strict: session.strict });\n emit();\n };\n\n // Switches run one at a time, and one superseded while queued is skipped,\n // so a slow earlier switch can never land on top of a later one.\n const enqueue = (next: () => Scenario, inline: boolean) => {\n if (destroyed) return Promise.reject(new Error('This scenario session was destroyed'));\n const id = ++transition;\n const run = queue.then(() => (id === transition ? switchTo(next(), inline) : undefined));\n queue = run.catch(() => {});\n\n return run;\n };\n\n const attach = (adapter: Bound): Connection<Serializable> => {\n const entry: Connected = { adapter };\n connected.set(adapter.key, entry);\n entry.unsubscribe = adapter.subscribe?.(emit);\n emit();\n\n return {\n update(state) {\n entry.value = state;\n emit();\n },\n disconnect() {\n entry.unsubscribe?.();\n\n if (connected.get(adapter.key) === entry) connected.delete(adapter.key);\n emit();\n },\n };\n };\n\n const session: ScenarioSession = {\n catalog,\n active: null,\n resolved: null,\n strict: options.strict ?? !!selection?.strict,\n unhandled,\n keys: () => [...connected.keys()],\n read(key) {\n const entry = connected.get(key);\n const read = entry?.adapter.read();\n\n if (read !== undefined) return read;\n\n return entry?.value === undefined ? undefined : parseJson(JSON.stringify(entry.value));\n },\n async applyState(key, state) {\n const entry = connected.get(key);\n\n if (!entry) throw new Error(`No adapter connected for \"${key}\"`);\n await entry.adapter.decode(state, session.active?.name ?? 'live')();\n },\n connect(adapter) {\n const bound = bind(adapter);\n const noop = { update() {}, disconnect() {} };\n\n if (destroyed) return noop;\n const connection = attach(bound);\n\n if (session.resolved) {\n applyPrepared(prepareStates(session.resolved, [bound])).catch((err) =>\n console.error('[state-scenarios]', err)\n );\n }\n\n return connection;\n },\n open: (names) => enqueue(() => lookup(catalog, names), false),\n openInline: (scenario) => enqueue(() => scenario, true),\n onChange(listener) {\n listeners.add(listener);\n\n return () => listeners.delete(listener);\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n transition++; // drop queued switches\n\n for (const entry of connected.values()) entry.unsubscribe?.();\n connected.clear();\n listeners.clear();\n\n if (ownWorker) void ownWorker.stop();\n else worker?.resetHandlers(...appHandlers);\n worker = ownWorker = null;\n\n if (current === session) current = null;\n\n if (globalThis.stateScenariosUnhandled === unhandled) globalThis.stateScenariosUnhandled = undefined;\n\n if (markerOwner === session) {\n markerOwner = null;\n delete document.documentElement.dataset.scenario;\n delete document.documentElement.dataset.scenarioReady;\n }\n },\n };\n\n current = session;\n\n // Adapters connect whether or not a scenario is selected, so the plain app can be\n // inspected and captured too. Only applying scenario state is conditional.\n const initial = (options.adapters ?? []).map(bind);\n\n for (const adapter of initial) attach(adapter);\n\n if (!selection) return session;\n\n // A selection that doesn't load must not stick to the tab and break every later page load.\n const loaded = (() => {\n try {\n const active = selection.data\n ? parseScenario(JSON.parse(selection.data), 'inline scenario')\n : lookup(catalog, selection.name ?? '');\n\n const resolved = catalog.resolve(active);\n\n return { active, resolved, prepared: prepareStates(resolved, initial, !!selection.data) };\n } catch (err) {\n store(null);\n throw err;\n }\n })();\n\n const { active, resolved, prepared } = loaded;\n await setNetwork(resolved);\n\n if (destroyed) return session;\n\n // Only fill params that are missing, so in-app navigation survives a reload.\n for (const [k, v] of Object.entries(resolved.url ?? {})) {\n if (v !== null && !url.searchParams.has(k)) url.searchParams.set(k, v);\n }\n\n history.replaceState(history.state, '', url);\n // Apply (awaiting async adapters) before the app renders.\n await applyPrepared(prepared);\n\n if (destroyed) return session;\n activate(active, resolved, !!selection.data);\n emit();\n\n return session;\n}\n\n/** msw's `isCommonAssetRequest`, inlined so msw 2.4 and later all work. */\nconst isAsset = (href: string) => {\n const url = new URL(href);\n\n return url.protocol === 'file:' || url.hostname === 'fonts.googleapis.com'\n || url.pathname.includes('node_modules') || url.pathname.includes('@vite')\n || /\\.(s?css|less|m?jsx?|m?tsx?|html|ttf|otf|woff2?|eot|gif|jpe?g|png|avif|webp|svg|mp4|webm|ogg|mov|mp3|wav|flac|aac|pdf|txt|csv|json|xml|md|zip|tar|gz|rar|7z)$/i\n .test(url.pathname);\n};\n\nconst byKey = ([a]: [string, string | null], [b]: [string, string | null]) => a.localeCompare(b);\n\n/** The app's own query params (scenario params excluded), sorted for comparison. */\nconst appParams = (search: string) =>\n [...new URLSearchParams(search)].filter(([k]) => k !== PARAM && k !== STRICT_PARAM).toSorted(byKey);\n\nconst sortedParams = (url: Scenario['url']) =>\n Object.entries(url ?? {}).filter(([, v]) => v !== null).toSorted(byKey);\n\n/** Go to a scenario link, reloading even when only the hash changes. */\nexport function navigate(href: string) {\n const next = new URL(href, location.href);\n\n if (next.pathname + next.search === location.pathname + location.search) {\n history.replaceState(history.state, '', next);\n location.reload();\n } else {\n location.href = next.toString();\n }\n}\n\n/**\n * URL first (`?scenario=name` or `#scenario-data=...`), then the tab's last\n * selection, so navigation that drops the query keeps the scenario.\n * `?scenario=` (empty) resets to the default.\n */\nfunction select(url: URL, fallback?: string): Selection | null {\n const data = readInline(url.hash);\n const name = url.searchParams.get(PARAM);\n const strict = url.searchParams.has(STRICT_PARAM);\n\n let selection: Selection | null = null;\n\n if (data) selection = { data, strict };\n else if (name) selection = { name, strict };\n else if (name === '') {\n url.searchParams.delete(PARAM);\n store(null);\n } else {\n const stored = load();\n // An explicit ?scenario-strict in the current URL wins over the stored flag.\n selection = stored && { ...stored, strict: stored.strict || strict };\n\n // Restore the selection into the URL so it stays shareable.\n if (selection?.name) url.searchParams.set(PARAM, selection.name);\n\n if (selection?.data) url.hash = new URLSearchParams({ [DATA_KEY]: selection.data }).toString();\n\n if (selection?.strict) url.searchParams.set(STRICT_PARAM, '');\n }\n\n if (selection) store(selection);\n\n return selection ?? (fallback ? { name: fallback, strict } : null);\n}\n\n/** One name, or several joined with commas: later ones win, like `extends`. */\nfunction lookup(catalog: Catalog, names: string): Scenario {\n const list = names.split(',').map((n) => n.trim());\n\n for (const name of list) {\n if (catalog.get(name)) continue;\n const known = catalog.list.map((s) => s.name).join(', ');\n throw new Error(`Unknown scenario \"${name}\". Known scenarios: ${known}`);\n }\n\n const [only] = list;\n const single = list.length === 1 && only !== undefined ? catalog.get(only) : undefined;\n\n return single ?? { name: list.join(','), extends: list };\n}\n\nfunction store(selection: Selection | null) {\n try {\n if (selection) sessionStorage.setItem(STORAGE_KEY, JSON.stringify(selection));\n else sessionStorage.removeItem(STORAGE_KEY);\n } catch {}\n}\n\nfunction load(): Selection | null {\n try {\n return JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? 'null');\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":["resolved","prepared","toHandlers","active"]}
|
package/dist/panel.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface PanelOptions {
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function mountPanel(session: ScenarioSession, options?: PanelOptions): ScenarioPanel;
|
|
16
16
|
/**
|
|
17
|
-
* Snapshot what the app shows now as a self-contained scenario: current URL params,
|
|
17
|
+
* Snapshot what the app shows now as a self-contained scenario: current route and URL params,
|
|
18
18
|
* the active network fixtures, and every connected adapter's live state.
|
|
19
19
|
*/
|
|
20
20
|
export declare function captureScenario(session: ScenarioSession, name: string, description?: string): Scenario;
|
package/dist/panel.js
CHANGED
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
STRICT_PARAM,
|
|
4
4
|
readInline,
|
|
5
5
|
scenarioHref
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-KRCYO64S.js";
|
|
7
7
|
import {
|
|
8
8
|
parseScenario
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-6L55WKFS.js";
|
|
10
10
|
|
|
11
11
|
// src/panel.ts
|
|
12
12
|
var OPEN_KEY = "scenario-panel:open";
|
|
@@ -39,6 +39,7 @@ function captureScenario(session, name, description) {
|
|
|
39
39
|
const draft = parseScenario({
|
|
40
40
|
name,
|
|
41
41
|
description: description || void 0,
|
|
42
|
+
path: location.pathname,
|
|
42
43
|
url,
|
|
43
44
|
network: session.resolved?.network,
|
|
44
45
|
state
|
package/dist/panel.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/panel.ts"],"sourcesContent":["import { PARAM, readInline, scenarioHref, STRICT_PARAM } from './link.js';\nimport type { ScenarioSession } from './runtime.js';\nimport { parseScenario, type Scenario } from './schema.js';\n\nconst OPEN_KEY = 'scenario-panel:open';\n\nconst SCENARIO_PARAMS = new Set([PARAM, STRICT_PARAM]);\n\nexport interface PanelOptions {\n /** Corner to dock in, e.g. to avoid a chat widget. Default \"bottom-right\". */\n position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n /** Default: the maximum, so the panel stays above app overlays. */\n zIndex?: number;\n /** \"auto\" follows the OS colour scheme. Default \"auto\". */\n theme?: 'auto' | 'light' | 'dark';\n}\n\n/**\n * Add the floating scenario panel to the page. Framework-agnostic.\n * Remove it with `panel.remove()`; it unsubscribes from the session.\n */\nexport function mountPanel(session: ScenarioSession, options: PanelOptions = {}) {\n if (!customElements.get('scenario-panel')) {\n customElements.define('scenario-panel', ScenarioPanel);\n }\n\n const el = new ScenarioPanel();\n el.session = session;\n const [v, h] = (options.position ?? 'bottom-right').split('-');\n el.dataset.v = v;\n el.dataset.h = h;\n\n if (options.zIndex !== undefined) el.style.zIndex = String(options.zIndex);\n\n if (options.theme && options.theme !== 'auto') el.dataset.theme = options.theme;\n document.body.append(el);\n\n return el;\n}\n\n/**\n * Snapshot what the app shows now as a self-contained scenario: current URL params,\n * the active network fixtures, and every connected adapter's live state.\n */\nexport function captureScenario(session: ScenarioSession, name: string, description?: string): Scenario {\n const url: Record<string, string> = {};\n new URLSearchParams(location.search).forEach((v, k) => {\n if (!SCENARIO_PARAMS.has(k)) url[k] = v;\n });\n\n const state = Object.fromEntries(\n session.keys().flatMap((key) => {\n const value = session.read(key);\n\n return value === undefined ? [] : [[key, value] as const];\n }),\n );\n\n const draft = parseScenario({\n name,\n description: description || undefined,\n url,\n network: session.resolved?.network,\n state,\n });\n\n return session.catalog.resolve(draft); // runs the rules\n}\n\nclass ScenarioPanel extends HTMLElement {\n session!: ScenarioSession;\n private render?: () => void;\n private unsubscribe?: () => void;\n\n disconnectedCallback() {\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n }\n\n connectedCallback() {\n // Re-attached (e.g. moved in the DOM): resubscribe and redraw.\n if (this.render) {\n this.unsubscribe = this.session.onChange(this.render);\n this.render();\n\n return;\n }\n\n const session = this.session;\n const { catalog } = session;\n const root = this.attachShadow({ mode: 'open' });\n\n root.innerHTML = `\n <style>${STYLES}</style>\n <button class=\"toggle\" aria-expanded=\"false\" aria-controls=\"drawer\"></button>\n <section id=\"drawer\" aria-label=\"Scenarios\" hidden>\n <header>\n <h2>Scenarios</h2>\n <button class=\"close\" aria-label=\"Close scenarios panel\">×</button>\n </header>\n <p class=\"error\" role=\"alert\"></p>\n <input type=\"search\" placeholder=\"Filter by name, description or tag\" aria-label=\"Filter scenarios\">\n <ul class=\"list\"></ul>\n <div class=\"unmocked\" role=\"status\"></div>\n <section class=\"live\" aria-label=\"Live state\"></section>\n <details class=\"save\">\n <summary>Save as scenario</summary>\n <p class=\"hint\">Current URL, network fixtures and live state, as one shareable scenario.</p>\n <label>Name <input name=\"save-name\" placeholder=\"e.g. checkout-card-declined\" aria-label=\"Scenario name\"></label>\n <label>Description <input name=\"save-description\" aria-label=\"Scenario description\"></label>\n <div class=\"row\">\n <button data-act=\"save-link\">Copy link</button>\n <button data-act=\"save-json\">Download JSON</button>\n </div>\n <input class=\"saved\" readonly aria-label=\"Saved scenario link\" hidden>\n </details>\n <details class=\"edit\">\n <summary>Edit current scenario</summary>\n <textarea spellcheck=\"false\" aria-label=\"Scenario JSON\"></textarea>\n <button data-act=\"apply\">Apply edited scenario</button>\n </details>\n <footer>\n <button data-act=\"copy\">Copy link</button>\n <a class=\"reset\">Reset</a>\n </footer>\n </section>`;\n\n const find = <T extends Element>(selector: string, type: new() => T): T => {\n const el = root.querySelector(selector);\n\n if (!(el instanceof type)) throw new Error(`state-scenarios panel: missing ${selector}`);\n\n return el;\n };\n\n const toggle = find('.toggle', HTMLButtonElement);\n const drawer = find('#drawer', HTMLElement);\n const error = find('.error', HTMLParagraphElement);\n\n const run = (fn: () => void | Promise<void>) => async () => {\n error.textContent = '';\n\n try {\n await fn();\n } catch (err) {\n error.textContent = err instanceof Error ? err.message : String(err);\n }\n };\n\n const href = (s: Scenario) => {\n try {\n return { href: scenarioHref(catalog.resolve(s), { strict: session.strict }), invalid: false };\n } catch {\n return { href: `?${PARAM}=${encodeURIComponent(s.name)}`, invalid: true };\n }\n };\n\n find('.list', HTMLUListElement).innerHTML = catalog.list\n .map((s) => ({ s, ...href(s) }))\n .map(\n ({ s, href, invalid }) => `\n <li data-name=\"${esc(s.name)}\" data-search=\"${\n esc([s.name, s.description, ...(s.tags ?? [])].join(' ').toLowerCase())\n }\">\n <a href=\"${esc(href)}\">\n <strong>${esc(s.name)}${invalid ? ' <em>(invalid, opens with error)</em>' : ''}</strong>\n ${s.description ? `<span>${esc(s.description)}</span>` : ''}\n ${s.tags?.length ? `<small>${s.tags.map(esc).join(' · ')}</small>` : ''}\n </a>\n <button class=\"add\" aria-label=\"Combine ${\n esc(s.name)\n } with the current scenario\" title=\"Combine with the current scenario\">+</button>\n </li>`,\n )\n .join('');\n root.querySelectorAll<HTMLLIElement>('li[data-name]').forEach((li) => {\n const name = li.dataset.name!;\n li.querySelector('a')!.onclick = (e) => {\n if (e.metaKey || e.ctrlKey || e.shiftKey) return; // let the browser open a new tab\n e.preventDefault();\n void run(() => session.open(name))();\n };\n\n li.querySelector<HTMLButtonElement>('.add')!.onclick = run(() => {\n const names = new Set((session.active?.name ?? '').split(',').filter(Boolean));\n names.add(name);\n\n return session.open([...names].join(','));\n });\n });\n const filter = find('input[type=search]', HTMLInputElement);\n filter.oninput = () => {\n const q = filter.value.toLowerCase().trim();\n root.querySelectorAll<HTMLLIElement>('li[data-search]').forEach((li) => {\n li.hidden = !li.dataset.search!.includes(q);\n });\n };\n\n const editors = new Map<string, { textarea: HTMLTextAreaElement; dirty: boolean; }>();\n\n const renderLive = () => {\n const keys = session.keys();\n\n for (const [key, ed] of editors) {\n if (!keys.includes(key)) {\n ed.textarea.closest('details')!.remove();\n editors.delete(key);\n }\n }\n\n for (const key of keys) {\n let ed = editors.get(key);\n\n if (!ed) {\n const details = document.createElement('details');\n details.innerHTML = `\n <summary>Live state: <code>${esc(key)}</code></summary>\n <textarea spellcheck=\"false\" aria-label=\"Live state ${esc(key)}\"></textarea>\n <div class=\"row\"><button data-act=\"live-apply\">Apply</button><button data-act=\"live-revert\">Revert</button></div>`;\n find('.live', HTMLElement).append(details);\n const entry = { textarea: details.querySelector('textarea')!, dirty: false };\n entry.textarea.oninput = () => (entry.dirty = true);\n details.ontoggle = () => renderLive();\n details.querySelector<HTMLButtonElement>('[data-act=live-apply]')!.onclick = run(async () => {\n await session.applyState(key, JSON.parse(entry.textarea.value));\n entry.dirty = false;\n renderLive();\n });\n details.querySelector<HTMLButtonElement>('[data-act=live-revert]')!.onclick = () => {\n entry.dirty = false;\n renderLive();\n };\n\n editors.set(key, ed = entry);\n }\n\n // Follow the app while open (state can be large), never overwriting what someone is typing.\n if (!ed.dirty && ed.textarea.closest('details')!.open) {\n ed.textarea.value = JSON.stringify(session.read(key) ?? null, null, 2);\n }\n }\n };\n\n const saveName = find('input[name=save-name]', HTMLInputElement);\n const saveDescription = find('input[name=save-description]', HTMLInputElement);\n const saved = find('.saved', HTMLInputElement);\n const capture = () => captureScenario(session, saveName.value.trim(), saveDescription.value.trim());\n find('[data-act=save-link]', HTMLButtonElement).onclick = run(async () => {\n const link = scenarioHref(capture(), { inline: true, strict: session.strict });\n saved.hidden = false;\n saved.value = link;\n saved.select();\n await navigator.clipboard?.writeText(link).catch(() => {});\n });\n find('[data-act=save-json]', HTMLButtonElement).onclick = run(() => {\n const scenario = capture();\n const a = document.createElement('a');\n a.href = URL.createObjectURL(\n new Blob([`${JSON.stringify(scenario, null, 2)}\\n`], { type: 'application/json' }),\n );\n a.download = `${scenario.name}.json`;\n a.click();\n URL.revokeObjectURL(a.href);\n });\n\n const editor = find('.edit textarea', HTMLTextAreaElement);\n\n if (readInline(location.hash)) find('.edit', HTMLDetailsElement).open = true;\n find('[data-act=apply]', HTMLButtonElement).onclick = run(() =>\n session.openInline(parseScenario(JSON.parse(editor.value)))\n );\n\n find('[data-act=copy]', HTMLButtonElement).onclick = run(async () => {\n try {\n await navigator.clipboard.writeText(location.href);\n } catch {\n prompt('Copy this link', location.href);\n }\n });\n // An absolute same-origin URL: a path starting with \"//\" must not read as another host.\n const reset = new URL(location.href);\n reset.search = '';\n reset.hash = '';\n reset.searchParams.set(PARAM, '');\n find('.reset', HTMLAnchorElement).href = reset.href;\n\n const setOpen = (open: boolean) => {\n drawer.hidden = !open;\n toggle.setAttribute('aria-expanded', String(open));\n\n try {\n sessionStorage.setItem(OPEN_KEY, open ? '1' : '');\n } catch {}\n };\n\n let stored: string | null = null;\n\n try {\n stored = sessionStorage.getItem(OPEN_KEY);\n } catch {}\n\n setOpen(!!stored);\n toggle.onclick = () => setOpen(!!drawer.hidden);\n find('.close', HTMLButtonElement).onclick = () => {\n setOpen(false);\n toggle.focus();\n };\n\n drawer.onkeydown = (e) => {\n if (e.key === 'Escape') {\n setOpen(false);\n toggle.focus();\n }\n };\n\n let shownScenario: string | undefined;\n\n const render = () => {\n const { active, strict, unhandled } = session;\n toggle.innerHTML = `Scenario: <b>${esc(active?.name ?? 'none')}</b>${strict ? ' (strict)' : ''}`;\n const names = (active?.name ?? '').split(',');\n root.querySelectorAll<HTMLLIElement>('li[data-name]').forEach((li) => {\n const a = li.querySelector('a')!;\n\n if (names.includes(li.dataset.name!)) a.setAttribute('aria-current', 'true');\n else a.removeAttribute('aria-current');\n });\n find('.unmocked', HTMLElement).innerHTML = unhandled.length\n ? `<p>${unhandled.length} request(s) not covered by this scenario${\n strict ? ' (answered 501)' : ''\n }:</p>\n <ul>${unhandled.map((u) => `<li><code>${esc(u)}</code></li>`).join('')}</ul>`\n : '';\n\n // Set as a property, not markup, so page text selectors never match the panel's JSON.\n if (active?.name !== shownScenario) {\n shownScenario = active?.name;\n editor.value = JSON.stringify(active ?? { name: 'custom', extends: catalog.list[0]?.name }, null, 2);\n }\n\n renderLive();\n };\n\n this.render = render;\n render();\n this.unsubscribe = session.onChange(render);\n }\n}\n\nconst esc = (s: string) => s.replace(/[&<>\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n\nconst STYLES = `\n :host {\n --bg: #ffffff; --fg: #1b1b1f; --muted: #5d5d66; --line: #dcdce2;\n --accent: #4747eb; --accent-fg: #ffffff; --hover: #f2f2f7; --danger: #b42318;\n all: initial; position: fixed; right: 16px; bottom: 16px; z-index: 2147483647;\n font: 14px/1.4 system-ui, sans-serif; color: var(--fg);\n }\n @media (prefers-color-scheme: dark) {\n :host(:not([data-theme=\"light\"])) {\n --bg: #1c1c21; --fg: #ececf1; --muted: #a3a3ad; --line: #34343c;\n --accent: #8c8cff; --accent-fg: #101014; --hover: #26262d; --danger: #ff8a80;\n }\n }\n :host([data-theme=\"dark\"]) {\n --bg: #1c1c21; --fg: #ececf1; --muted: #a3a3ad; --line: #34343c;\n --accent: #8c8cff; --accent-fg: #101014; --hover: #26262d; --danger: #ff8a80;\n }\n :host([data-v=\"top\"]) { bottom: auto; top: 16px; }\n :host([data-h=\"left\"]) { right: auto; left: 16px; }\n :host([data-v=\"top\"]) #drawer { bottom: auto; top: 48px; }\n :host([data-h=\"left\"]) #drawer { right: auto; left: 0; }\n :host([data-h=\"left\"]) .toggle { margin-left: 0; }\n * { box-sizing: border-box; font: inherit; color: inherit; }\n button, a { cursor: pointer; }\n :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n .toggle {\n display: block; margin-left: auto; padding: 8px 14px; border: 0; border-radius: 999px;\n background: var(--accent); color: var(--accent-fg); box-shadow: 0 4px 16px #0003;\n }\n .toggle b, strong { font-weight: 700; }\n #drawer {\n position: absolute; right: 0; bottom: 48px; width: min(400px, calc(100vw - 32px));\n max-height: min(680px, calc(100vh - 96px)); overflow: auto; padding: 12px;\n background: var(--bg); border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 12px 40px #0004;\n }\n #drawer[hidden] { display: none; }\n header, footer, .row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n .row { justify-content: flex-start; margin-top: 8px; }\n h2 { margin: 0; font-size: 16px; font-weight: 700; }\n .close { border: 0; background: none; font-size: 22px; line-height: 1; padding: 4px 8px; border-radius: 6px; }\n input, textarea {\n width: 100%; margin: 6px 0; padding: 8px 10px; border: 1px solid var(--line);\n border-radius: 8px; background: var(--bg);\n }\n label { display: block; margin-top: 6px; color: var(--muted); font-size: 12px; }\n label input { color: var(--fg); font-size: 14px; }\n ul { list-style: none; margin: 0; padding: 0; }\n li[hidden] { display: none; }\n .list li { display: flex; align-items: stretch; gap: 4px; }\n .list a {\n flex: 1; min-width: 0; overflow-wrap: anywhere; display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-radius: 8px;\n text-decoration: none; border-left: 3px solid transparent;\n }\n .list a:hover { background: var(--hover); }\n .list a[aria-current] { border-left-color: var(--accent); background: var(--hover); }\n .list span { color: var(--muted); }\n .list small { color: var(--muted); font-size: 12px; }\n .add { width: 32px; border: 0; border-radius: 8px; background: none; color: var(--muted); font-size: 18px; }\n .add:hover { background: var(--hover); color: var(--fg); }\n details { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }\n summary { cursor: pointer; font-weight: 600; }\n code { font: 12px ui-monospace, monospace; }\n textarea { height: 200px; font: 12px/1.45 ui-monospace, monospace; resize: vertical; }\n .hint { margin: 6px 0 0; color: var(--muted); font-size: 12px; }\n .error { color: var(--danger); white-space: pre-wrap; margin: 8px 0; font-size: 12px; }\n .error:empty, .unmocked:empty { display: none; }\n .unmocked { margin-top: 10px; padding: 8px 10px; border-radius: 8px; background: var(--hover); color: var(--danger); font-size: 12px; }\n .unmocked p { margin: 0 0 4px; }\n .unmocked ul { padding-left: 16px; list-style: disc; }\n .unmocked code { overflow-wrap: anywhere; }\n em { font-style: normal; color: var(--danger); font-weight: 400; }\n details button, footer button {\n padding: 6px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--hover);\n }\n footer { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }\n footer a { color: var(--accent); }\n`;\n"],"mappings":"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","names":["href"]}
|
|
1
|
+
{"version":3,"sources":["../src/panel.ts"],"sourcesContent":["import { PARAM, readInline, scenarioHref, STRICT_PARAM } from './link.js';\nimport type { ScenarioSession } from './runtime.js';\nimport { parseScenario, type Scenario } from './schema.js';\n\nconst OPEN_KEY = 'scenario-panel:open';\n\nconst SCENARIO_PARAMS = new Set([PARAM, STRICT_PARAM]);\n\nexport interface PanelOptions {\n /** Corner to dock in, e.g. to avoid a chat widget. Default \"bottom-right\". */\n position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n /** Default: the maximum, so the panel stays above app overlays. */\n zIndex?: number;\n /** \"auto\" follows the OS colour scheme. Default \"auto\". */\n theme?: 'auto' | 'light' | 'dark';\n}\n\n/**\n * Add the floating scenario panel to the page. Framework-agnostic.\n * Remove it with `panel.remove()`; it unsubscribes from the session.\n */\nexport function mountPanel(session: ScenarioSession, options: PanelOptions = {}) {\n if (!customElements.get('scenario-panel')) {\n customElements.define('scenario-panel', ScenarioPanel);\n }\n\n const el = new ScenarioPanel();\n el.session = session;\n const [v, h] = (options.position ?? 'bottom-right').split('-');\n el.dataset.v = v;\n el.dataset.h = h;\n\n if (options.zIndex !== undefined) el.style.zIndex = String(options.zIndex);\n\n if (options.theme && options.theme !== 'auto') el.dataset.theme = options.theme;\n document.body.append(el);\n\n return el;\n}\n\n/**\n * Snapshot what the app shows now as a self-contained scenario: current route and URL params,\n * the active network fixtures, and every connected adapter's live state.\n */\nexport function captureScenario(session: ScenarioSession, name: string, description?: string): Scenario {\n const url: Record<string, string> = {};\n new URLSearchParams(location.search).forEach((v, k) => {\n if (!SCENARIO_PARAMS.has(k)) url[k] = v;\n });\n\n const state = Object.fromEntries(\n session.keys().flatMap((key) => {\n const value = session.read(key);\n\n return value === undefined ? [] : [[key, value] as const];\n }),\n );\n\n const draft = parseScenario({\n name,\n description: description || undefined,\n path: location.pathname,\n url,\n network: session.resolved?.network,\n state,\n });\n\n return session.catalog.resolve(draft); // runs the rules\n}\n\nclass ScenarioPanel extends HTMLElement {\n session!: ScenarioSession;\n private render?: () => void;\n private unsubscribe?: () => void;\n\n disconnectedCallback() {\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n }\n\n connectedCallback() {\n // Re-attached (e.g. moved in the DOM): resubscribe and redraw.\n if (this.render) {\n this.unsubscribe = this.session.onChange(this.render);\n this.render();\n\n return;\n }\n\n const session = this.session;\n const { catalog } = session;\n const root = this.attachShadow({ mode: 'open' });\n\n root.innerHTML = `\n <style>${STYLES}</style>\n <button class=\"toggle\" aria-expanded=\"false\" aria-controls=\"drawer\"></button>\n <section id=\"drawer\" aria-label=\"Scenarios\" hidden>\n <header>\n <h2>Scenarios</h2>\n <button class=\"close\" aria-label=\"Close scenarios panel\">×</button>\n </header>\n <p class=\"error\" role=\"alert\"></p>\n <input type=\"search\" placeholder=\"Filter by name, description or tag\" aria-label=\"Filter scenarios\">\n <ul class=\"list\"></ul>\n <div class=\"unmocked\" role=\"status\"></div>\n <section class=\"live\" aria-label=\"Live state\"></section>\n <details class=\"save\">\n <summary>Save as scenario</summary>\n <p class=\"hint\">Current URL, network fixtures and live state, as one shareable scenario.</p>\n <label>Name <input name=\"save-name\" placeholder=\"e.g. checkout-card-declined\" aria-label=\"Scenario name\"></label>\n <label>Description <input name=\"save-description\" aria-label=\"Scenario description\"></label>\n <div class=\"row\">\n <button data-act=\"save-link\">Copy link</button>\n <button data-act=\"save-json\">Download JSON</button>\n </div>\n <input class=\"saved\" readonly aria-label=\"Saved scenario link\" hidden>\n </details>\n <details class=\"edit\">\n <summary>Edit current scenario</summary>\n <textarea spellcheck=\"false\" aria-label=\"Scenario JSON\"></textarea>\n <button data-act=\"apply\">Apply edited scenario</button>\n </details>\n <footer>\n <button data-act=\"copy\">Copy link</button>\n <a class=\"reset\">Reset</a>\n </footer>\n </section>`;\n\n const find = <T extends Element>(selector: string, type: new() => T): T => {\n const el = root.querySelector(selector);\n\n if (!(el instanceof type)) throw new Error(`state-scenarios panel: missing ${selector}`);\n\n return el;\n };\n\n const toggle = find('.toggle', HTMLButtonElement);\n const drawer = find('#drawer', HTMLElement);\n const error = find('.error', HTMLParagraphElement);\n\n const run = (fn: () => void | Promise<void>) => async () => {\n error.textContent = '';\n\n try {\n await fn();\n } catch (err) {\n error.textContent = err instanceof Error ? err.message : String(err);\n }\n };\n\n const href = (s: Scenario) => {\n try {\n return { href: scenarioHref(catalog.resolve(s), { strict: session.strict }), invalid: false };\n } catch {\n return { href: `?${PARAM}=${encodeURIComponent(s.name)}`, invalid: true };\n }\n };\n\n find('.list', HTMLUListElement).innerHTML = catalog.list\n .map((s) => ({ s, ...href(s) }))\n .map(\n ({ s, href, invalid }) => `\n <li data-name=\"${esc(s.name)}\" data-search=\"${\n esc([s.name, s.description, ...(s.tags ?? [])].join(' ').toLowerCase())\n }\">\n <a href=\"${esc(href)}\">\n <strong>${esc(s.name)}${invalid ? ' <em>(invalid, opens with error)</em>' : ''}</strong>\n ${s.description ? `<span>${esc(s.description)}</span>` : ''}\n ${s.tags?.length ? `<small>${s.tags.map(esc).join(' · ')}</small>` : ''}\n </a>\n <button class=\"add\" aria-label=\"Combine ${\n esc(s.name)\n } with the current scenario\" title=\"Combine with the current scenario\">+</button>\n </li>`,\n )\n .join('');\n root.querySelectorAll<HTMLLIElement>('li[data-name]').forEach((li) => {\n const name = li.dataset.name!;\n li.querySelector('a')!.onclick = (e) => {\n if (e.metaKey || e.ctrlKey || e.shiftKey) return; // let the browser open a new tab\n e.preventDefault();\n void run(() => session.open(name))();\n };\n\n li.querySelector<HTMLButtonElement>('.add')!.onclick = run(() => {\n const names = new Set((session.active?.name ?? '').split(',').filter(Boolean));\n names.add(name);\n\n return session.open([...names].join(','));\n });\n });\n const filter = find('input[type=search]', HTMLInputElement);\n filter.oninput = () => {\n const q = filter.value.toLowerCase().trim();\n root.querySelectorAll<HTMLLIElement>('li[data-search]').forEach((li) => {\n li.hidden = !li.dataset.search!.includes(q);\n });\n };\n\n const editors = new Map<string, { textarea: HTMLTextAreaElement; dirty: boolean; }>();\n\n const renderLive = () => {\n const keys = session.keys();\n\n for (const [key, ed] of editors) {\n if (!keys.includes(key)) {\n ed.textarea.closest('details')!.remove();\n editors.delete(key);\n }\n }\n\n for (const key of keys) {\n let ed = editors.get(key);\n\n if (!ed) {\n const details = document.createElement('details');\n details.innerHTML = `\n <summary>Live state: <code>${esc(key)}</code></summary>\n <textarea spellcheck=\"false\" aria-label=\"Live state ${esc(key)}\"></textarea>\n <div class=\"row\"><button data-act=\"live-apply\">Apply</button><button data-act=\"live-revert\">Revert</button></div>`;\n find('.live', HTMLElement).append(details);\n const entry = { textarea: details.querySelector('textarea')!, dirty: false };\n entry.textarea.oninput = () => (entry.dirty = true);\n details.ontoggle = () => renderLive();\n details.querySelector<HTMLButtonElement>('[data-act=live-apply]')!.onclick = run(async () => {\n await session.applyState(key, JSON.parse(entry.textarea.value));\n entry.dirty = false;\n renderLive();\n });\n details.querySelector<HTMLButtonElement>('[data-act=live-revert]')!.onclick = () => {\n entry.dirty = false;\n renderLive();\n };\n\n editors.set(key, ed = entry);\n }\n\n // Follow the app while open (state can be large), never overwriting what someone is typing.\n if (!ed.dirty && ed.textarea.closest('details')!.open) {\n ed.textarea.value = JSON.stringify(session.read(key) ?? null, null, 2);\n }\n }\n };\n\n const saveName = find('input[name=save-name]', HTMLInputElement);\n const saveDescription = find('input[name=save-description]', HTMLInputElement);\n const saved = find('.saved', HTMLInputElement);\n const capture = () => captureScenario(session, saveName.value.trim(), saveDescription.value.trim());\n find('[data-act=save-link]', HTMLButtonElement).onclick = run(async () => {\n const link = scenarioHref(capture(), { inline: true, strict: session.strict });\n saved.hidden = false;\n saved.value = link;\n saved.select();\n await navigator.clipboard?.writeText(link).catch(() => {});\n });\n find('[data-act=save-json]', HTMLButtonElement).onclick = run(() => {\n const scenario = capture();\n const a = document.createElement('a');\n a.href = URL.createObjectURL(\n new Blob([`${JSON.stringify(scenario, null, 2)}\\n`], { type: 'application/json' }),\n );\n a.download = `${scenario.name}.json`;\n a.click();\n URL.revokeObjectURL(a.href);\n });\n\n const editor = find('.edit textarea', HTMLTextAreaElement);\n\n if (readInline(location.hash)) find('.edit', HTMLDetailsElement).open = true;\n find('[data-act=apply]', HTMLButtonElement).onclick = run(() =>\n session.openInline(parseScenario(JSON.parse(editor.value)))\n );\n\n find('[data-act=copy]', HTMLButtonElement).onclick = run(async () => {\n try {\n await navigator.clipboard.writeText(location.href);\n } catch {\n prompt('Copy this link', location.href);\n }\n });\n // An absolute same-origin URL: a path starting with \"//\" must not read as another host.\n const reset = new URL(location.href);\n reset.search = '';\n reset.hash = '';\n reset.searchParams.set(PARAM, '');\n find('.reset', HTMLAnchorElement).href = reset.href;\n\n const setOpen = (open: boolean) => {\n drawer.hidden = !open;\n toggle.setAttribute('aria-expanded', String(open));\n\n try {\n sessionStorage.setItem(OPEN_KEY, open ? '1' : '');\n } catch {}\n };\n\n let stored: string | null = null;\n\n try {\n stored = sessionStorage.getItem(OPEN_KEY);\n } catch {}\n\n setOpen(!!stored);\n toggle.onclick = () => setOpen(!!drawer.hidden);\n find('.close', HTMLButtonElement).onclick = () => {\n setOpen(false);\n toggle.focus();\n };\n\n drawer.onkeydown = (e) => {\n if (e.key === 'Escape') {\n setOpen(false);\n toggle.focus();\n }\n };\n\n let shownScenario: string | undefined;\n\n const render = () => {\n const { active, strict, unhandled } = session;\n toggle.innerHTML = `Scenario: <b>${esc(active?.name ?? 'none')}</b>${strict ? ' (strict)' : ''}`;\n const names = (active?.name ?? '').split(',');\n root.querySelectorAll<HTMLLIElement>('li[data-name]').forEach((li) => {\n const a = li.querySelector('a')!;\n\n if (names.includes(li.dataset.name!)) a.setAttribute('aria-current', 'true');\n else a.removeAttribute('aria-current');\n });\n find('.unmocked', HTMLElement).innerHTML = unhandled.length\n ? `<p>${unhandled.length} request(s) not covered by this scenario${\n strict ? ' (answered 501)' : ''\n }:</p>\n <ul>${unhandled.map((u) => `<li><code>${esc(u)}</code></li>`).join('')}</ul>`\n : '';\n\n // Set as a property, not markup, so page text selectors never match the panel's JSON.\n if (active?.name !== shownScenario) {\n shownScenario = active?.name;\n editor.value = JSON.stringify(active ?? { name: 'custom', extends: catalog.list[0]?.name }, null, 2);\n }\n\n renderLive();\n };\n\n this.render = render;\n render();\n this.unsubscribe = session.onChange(render);\n }\n}\n\nconst esc = (s: string) => s.replace(/[&<>\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n\nconst STYLES = `\n :host {\n --bg: #ffffff; --fg: #1b1b1f; --muted: #5d5d66; --line: #dcdce2;\n --accent: #4747eb; --accent-fg: #ffffff; --hover: #f2f2f7; --danger: #b42318;\n all: initial; position: fixed; right: 16px; bottom: 16px; z-index: 2147483647;\n font: 14px/1.4 system-ui, sans-serif; color: var(--fg);\n }\n @media (prefers-color-scheme: dark) {\n :host(:not([data-theme=\"light\"])) {\n --bg: #1c1c21; --fg: #ececf1; --muted: #a3a3ad; --line: #34343c;\n --accent: #8c8cff; --accent-fg: #101014; --hover: #26262d; --danger: #ff8a80;\n }\n }\n :host([data-theme=\"dark\"]) {\n --bg: #1c1c21; --fg: #ececf1; --muted: #a3a3ad; --line: #34343c;\n --accent: #8c8cff; --accent-fg: #101014; --hover: #26262d; --danger: #ff8a80;\n }\n :host([data-v=\"top\"]) { bottom: auto; top: 16px; }\n :host([data-h=\"left\"]) { right: auto; left: 16px; }\n :host([data-v=\"top\"]) #drawer { bottom: auto; top: 48px; }\n :host([data-h=\"left\"]) #drawer { right: auto; left: 0; }\n :host([data-h=\"left\"]) .toggle { margin-left: 0; }\n * { box-sizing: border-box; font: inherit; color: inherit; }\n button, a { cursor: pointer; }\n :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n .toggle {\n display: block; margin-left: auto; padding: 8px 14px; border: 0; border-radius: 999px;\n background: var(--accent); color: var(--accent-fg); box-shadow: 0 4px 16px #0003;\n }\n .toggle b, strong { font-weight: 700; }\n #drawer {\n position: absolute; right: 0; bottom: 48px; width: min(400px, calc(100vw - 32px));\n max-height: min(680px, calc(100vh - 96px)); overflow: auto; padding: 12px;\n background: var(--bg); border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 12px 40px #0004;\n }\n #drawer[hidden] { display: none; }\n header, footer, .row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n .row { justify-content: flex-start; margin-top: 8px; }\n h2 { margin: 0; font-size: 16px; font-weight: 700; }\n .close { border: 0; background: none; font-size: 22px; line-height: 1; padding: 4px 8px; border-radius: 6px; }\n input, textarea {\n width: 100%; margin: 6px 0; padding: 8px 10px; border: 1px solid var(--line);\n border-radius: 8px; background: var(--bg);\n }\n label { display: block; margin-top: 6px; color: var(--muted); font-size: 12px; }\n label input { color: var(--fg); font-size: 14px; }\n ul { list-style: none; margin: 0; padding: 0; }\n li[hidden] { display: none; }\n .list li { display: flex; align-items: stretch; gap: 4px; }\n .list a {\n flex: 1; min-width: 0; overflow-wrap: anywhere; display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-radius: 8px;\n text-decoration: none; border-left: 3px solid transparent;\n }\n .list a:hover { background: var(--hover); }\n .list a[aria-current] { border-left-color: var(--accent); background: var(--hover); }\n .list span { color: var(--muted); }\n .list small { color: var(--muted); font-size: 12px; }\n .add { width: 32px; border: 0; border-radius: 8px; background: none; color: var(--muted); font-size: 18px; }\n .add:hover { background: var(--hover); color: var(--fg); }\n details { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }\n summary { cursor: pointer; font-weight: 600; }\n code { font: 12px ui-monospace, monospace; }\n textarea { height: 200px; font: 12px/1.45 ui-monospace, monospace; resize: vertical; }\n .hint { margin: 6px 0 0; color: var(--muted); font-size: 12px; }\n .error { color: var(--danger); white-space: pre-wrap; margin: 8px 0; font-size: 12px; }\n .error:empty, .unmocked:empty { display: none; }\n .unmocked { margin-top: 10px; padding: 8px 10px; border-radius: 8px; background: var(--hover); color: var(--danger); font-size: 12px; }\n .unmocked p { margin: 0 0 4px; }\n .unmocked ul { padding-left: 16px; list-style: disc; }\n .unmocked code { overflow-wrap: anywhere; }\n em { font-style: normal; color: var(--danger); font-weight: 400; }\n details button, footer button {\n padding: 6px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--hover);\n }\n footer { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }\n footer a { color: var(--accent); }\n`;\n"],"mappings":"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","names":["href"]}
|
package/dist/playwright.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface OpenOptions {
|
|
|
5
5
|
path?: string;
|
|
6
6
|
/** Fail unmocked requests instead of letting them reach a real backend. Default true. */
|
|
7
7
|
strict?: boolean;
|
|
8
|
+
/** Show the dev panel. False hides it, for screenshots that go into product docs. Default true. */
|
|
9
|
+
panel?: boolean;
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
12
|
* Open the app in a catalog scenario and wait until the UI shows it: the scenario is
|
|
@@ -13,5 +15,9 @@ export interface OpenOptions {
|
|
|
13
15
|
export declare function openScenario(page: Page, name: string, options?: OpenOptions): Promise<void>;
|
|
14
16
|
/** Like `openScenario`, for a scenario defined in the test rather than the catalog. */
|
|
15
17
|
export declare function openInlineScenario(page: Page, scenario: ScenarioInput, options?: OpenOptions): Promise<void>;
|
|
18
|
+
declare global {
|
|
19
|
+
/** Exposed by `openScenario({ panel: false })`: whether this page should hide the panel. */
|
|
20
|
+
var stateScenariosPanelHidden: (() => Promise<boolean>) | undefined;
|
|
21
|
+
}
|
|
16
22
|
/** Requests the active scenario didn't cover, e.g. to assert none in strict runs. */
|
|
17
23
|
export declare const unhandledRequests: (page: Page) => Promise<string[]>;
|
package/dist/playwright.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
DATA_KEY,
|
|
3
3
|
PARAM,
|
|
4
4
|
STRICT_PARAM
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-KRCYO64S.js";
|
|
6
6
|
|
|
7
7
|
// src/playwright.ts
|
|
8
8
|
async function openScenario(page, name, options = {}) {
|
|
@@ -10,10 +10,11 @@ async function openScenario(page, name, options = {}) {
|
|
|
10
10
|
}
|
|
11
11
|
async function openInlineScenario(page, scenario, options = {}) {
|
|
12
12
|
const hash = `#${new URLSearchParams({ [DATA_KEY]: JSON.stringify(scenario) })}`;
|
|
13
|
-
await open(page, scenario.name, new URLSearchParams(), hash, options);
|
|
13
|
+
await open(page, scenario.name, new URLSearchParams(), hash, { path: scenario.path, ...options });
|
|
14
14
|
}
|
|
15
|
-
async function open(page, name, search, hash, { path = "/", strict = true }) {
|
|
15
|
+
async function open(page, name, search, hash, { path = "/", strict = true, panel = true }) {
|
|
16
16
|
if (strict) search.set(STRICT_PARAM, "");
|
|
17
|
+
await setPanel(page, panel);
|
|
17
18
|
await page.goto("about:blank");
|
|
18
19
|
await page.goto(`${path}?${search}${hash}`);
|
|
19
20
|
const html = page.locator(`html[data-scenario="${name}"]`);
|
|
@@ -21,6 +22,34 @@ async function open(page, name, search, hash, { path = "/", strict = true }) {
|
|
|
21
22
|
const ready = await html.getAttribute("data-scenario-ready");
|
|
22
23
|
if (ready) await page.locator(ready).first().waitFor();
|
|
23
24
|
}
|
|
25
|
+
var panelHidden = /* @__PURE__ */ new WeakMap();
|
|
26
|
+
async function setPanel(page, show) {
|
|
27
|
+
const state = panelHidden.get(page);
|
|
28
|
+
if (state) {
|
|
29
|
+
state.hidden = !show;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (show) return;
|
|
33
|
+
const created = { hidden: true };
|
|
34
|
+
panelHidden.set(page, created);
|
|
35
|
+
await page.exposeFunction("stateScenariosPanelHidden", () => created.hidden);
|
|
36
|
+
await page.addInitScript(() => {
|
|
37
|
+
const style = document.createElement("style");
|
|
38
|
+
style.textContent = "scenario-panel { display: none !important; }";
|
|
39
|
+
const attach = () => document.documentElement.append(style);
|
|
40
|
+
if (document.documentElement) attach();
|
|
41
|
+
else {
|
|
42
|
+
new MutationObserver((_, observer) => {
|
|
43
|
+
if (!document.documentElement) return;
|
|
44
|
+
observer.disconnect();
|
|
45
|
+
attach();
|
|
46
|
+
}).observe(document, { childList: true });
|
|
47
|
+
}
|
|
48
|
+
void globalThis.stateScenariosPanelHidden?.().then((hidden) => {
|
|
49
|
+
if (!hidden) style.remove();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
24
53
|
var unhandledRequests = (page) => page.evaluate(() => globalThis.stateScenariosUnhandled ?? []);
|
|
25
54
|
export {
|
|
26
55
|
openInlineScenario,
|
package/dist/playwright.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/playwright.ts"],"sourcesContent":["import type { Page } from '@playwright/test';\nimport { DATA_KEY, PARAM, STRICT_PARAM } from './link.js';\nimport type { ScenarioInput } from './schema.js';\n\nexport interface OpenOptions {\n /** App path to open. Default \"/\". */\n path?: string;\n /** Fail unmocked requests instead of letting them reach a real backend. Default true. */\n strict?: boolean;\n}\n\n/**\n * Open the app in a catalog scenario and wait until the UI shows it: the scenario is\n * active and its `ready` selector, if any, is visible.\n */\nexport async function openScenario(page: Page, name: string, options: OpenOptions = {}) {\n await open(page, name, new URLSearchParams({ [PARAM]: name }), '', options);\n}\n\n/** Like `openScenario`, for a scenario defined in the test rather than the catalog. */\nexport async function openInlineScenario(page: Page, scenario: ScenarioInput, options: OpenOptions = {}) {\n const hash = `#${new URLSearchParams({ [DATA_KEY]: JSON.stringify(scenario) })}`;\n await open(page, scenario.name, new URLSearchParams(), hash, options);\n}\n\nasync function open(\n page: Page,\n name: string,\n search: URLSearchParams,\n hash: string,\n { path = '/', strict = true }: OpenOptions,\n) {\n if (strict) search.set(STRICT_PARAM, '');\n // Leave the page first: a hash-only change would not reload the app.\n await page.goto('about:blank');\n await page.goto(`${path}?${search}${hash}`);\n const html = page.locator(`html[data-scenario=\"${name}\"]`);\n await html.waitFor({ state: 'attached' });\n const ready = await html.getAttribute('data-scenario-ready');\n\n if (ready) await page.locator(ready).first().waitFor();\n}\n\n/** Requests the active scenario didn't cover, e.g. to assert none in strict runs. */\nexport const unhandledRequests = (page: Page): Promise<string[]> =>\n page.evaluate(() => globalThis.stateScenariosUnhandled ?? []);\n"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"sources":["../src/playwright.ts"],"sourcesContent":["import type { Page } from '@playwright/test';\nimport { DATA_KEY, PARAM, STRICT_PARAM } from './link.js';\nimport type { ScenarioInput } from './schema.js';\n\nexport interface OpenOptions {\n /** App path to open. Default \"/\". */\n path?: string;\n /** Fail unmocked requests instead of letting them reach a real backend. Default true. */\n strict?: boolean;\n /** Show the dev panel. False hides it, for screenshots that go into product docs. Default true. */\n panel?: boolean;\n}\n\n/**\n * Open the app in a catalog scenario and wait until the UI shows it: the scenario is\n * active and its `ready` selector, if any, is visible.\n */\nexport async function openScenario(page: Page, name: string, options: OpenOptions = {}) {\n await open(page, name, new URLSearchParams({ [PARAM]: name }), '', options);\n}\n\n/** Like `openScenario`, for a scenario defined in the test rather than the catalog. */\nexport async function openInlineScenario(page: Page, scenario: ScenarioInput, options: OpenOptions = {}) {\n const hash = `#${new URLSearchParams({ [DATA_KEY]: JSON.stringify(scenario) })}`;\n await open(page, scenario.name, new URLSearchParams(), hash, { path: scenario.path, ...options });\n}\n\nasync function open(\n page: Page,\n name: string,\n search: URLSearchParams,\n hash: string,\n { path = '/', strict = true, panel = true }: OpenOptions,\n) {\n if (strict) search.set(STRICT_PARAM, '');\n await setPanel(page, panel);\n // Leave the page first: a hash-only change would not reload the app.\n await page.goto('about:blank');\n await page.goto(`${path}?${search}${hash}`);\n const html = page.locator(`html[data-scenario=\"${name}\"]`);\n await html.waitFor({ state: 'attached' });\n const ready = await html.getAttribute('data-scenario-ready');\n\n if (ready) await page.locator(ready).first().waitFor();\n}\n\ndeclare global {\n /** Exposed by `openScenario({ panel: false })`: whether this page should hide the panel. */\n var stateScenariosPanelHidden: (() => Promise<boolean>) | undefined;\n}\n\n// Per page: whether the panel is hidden. The init script asks on every load, so a later\n// open can show the panel again on any Playwright version.\nconst panelHidden = new WeakMap<Page, { hidden: boolean; }>();\n\nasync function setPanel(page: Page, show: boolean) {\n const state = panelHidden.get(page);\n\n if (state) {\n state.hidden = !show;\n\n return;\n }\n\n if (show) return;\n const created = { hidden: true };\n panelHidden.set(page, created);\n await page.exposeFunction('stateScenariosPanelHidden', () => created.hidden);\n // Hide before the app's scripts run, so the panel never shows, not even in a video;\n // then show it again if this page no longer wants it hidden.\n await page.addInitScript(() => {\n const style = document.createElement('style');\n style.textContent = 'scenario-panel { display: none !important; }';\n // Init scripts can run before <html> exists: attach as soon as it does.\n const attach = () => document.documentElement.append(style);\n\n if (document.documentElement) attach();\n else {\n new MutationObserver((_, observer) => {\n if (!document.documentElement) return;\n observer.disconnect();\n attach();\n }).observe(document, { childList: true });\n }\n\n void globalThis.stateScenariosPanelHidden?.().then((hidden) => {\n if (!hidden) style.remove();\n });\n });\n}\n\n/** Requests the active scenario didn't cover, e.g. to assert none in strict runs. */\nexport const unhandledRequests = (page: Page): Promise<string[]> =>\n page.evaluate(() => globalThis.stateScenariosUnhandled ?? []);\n"],"mappings":";;;;;;;AAiBA,eAAsB,aAAa,MAAY,MAAc,UAAuB,CAAC,GAAG;AACtF,QAAM,KAAK,MAAM,MAAM,IAAI,gBAAgB,EAAE,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,IAAI,OAAO;AAC5E;AAGA,eAAsB,mBAAmB,MAAY,UAAyB,UAAuB,CAAC,GAAG;AACvG,QAAM,OAAO,IAAI,IAAI,gBAAgB,EAAE,CAAC,QAAQ,GAAG,KAAK,UAAU,QAAQ,EAAE,CAAC,CAAC;AAC9E,QAAM,KAAK,MAAM,SAAS,MAAM,IAAI,gBAAgB,GAAG,MAAM,EAAE,MAAM,SAAS,MAAM,GAAG,QAAQ,CAAC;AAClG;AAEA,eAAe,KACb,MACA,MACA,QACA,MACA,EAAE,OAAO,KAAK,SAAS,MAAM,QAAQ,KAAK,GAC1C;AACA,MAAI,OAAQ,QAAO,IAAI,cAAc,EAAE;AACvC,QAAM,SAAS,MAAM,KAAK;AAE1B,QAAM,KAAK,KAAK,aAAa;AAC7B,QAAM,KAAK,KAAK,GAAG,IAAI,IAAI,MAAM,GAAG,IAAI,EAAE;AAC1C,QAAM,OAAO,KAAK,QAAQ,uBAAuB,IAAI,IAAI;AACzD,QAAM,KAAK,QAAQ,EAAE,OAAO,WAAW,CAAC;AACxC,QAAM,QAAQ,MAAM,KAAK,aAAa,qBAAqB;AAE3D,MAAI,MAAO,OAAM,KAAK,QAAQ,KAAK,EAAE,MAAM,EAAE,QAAQ;AACvD;AASA,IAAM,cAAc,oBAAI,QAAoC;AAE5D,eAAe,SAAS,MAAY,MAAe;AACjD,QAAM,QAAQ,YAAY,IAAI,IAAI;AAElC,MAAI,OAAO;AACT,UAAM,SAAS,CAAC;AAEhB;AAAA,EACF;AAEA,MAAI,KAAM;AACV,QAAM,UAAU,EAAE,QAAQ,KAAK;AAC/B,cAAY,IAAI,MAAM,OAAO;AAC7B,QAAM,KAAK,eAAe,6BAA6B,MAAM,QAAQ,MAAM;AAG3E,QAAM,KAAK,cAAc,MAAM;AAC7B,UAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,UAAM,cAAc;AAEpB,UAAM,SAAS,MAAM,SAAS,gBAAgB,OAAO,KAAK;AAE1D,QAAI,SAAS,gBAAiB,QAAO;AAAA,SAChC;AACH,UAAI,iBAAiB,CAAC,GAAG,aAAa;AACpC,YAAI,CAAC,SAAS,gBAAiB;AAC/B,iBAAS,WAAW;AACpB,eAAO;AAAA,MACT,CAAC,EAAE,QAAQ,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,IAC1C;AAEA,SAAK,WAAW,4BAA4B,EAAE,KAAK,CAAC,WAAW;AAC7D,UAAI,CAAC,OAAQ,OAAM,OAAO;AAAA,IAC5B,CAAC;AAAA,EACH,CAAC;AACH;AAGO,IAAM,oBAAoB,CAAC,SAChC,KAAK,SAAS,MAAM,WAAW,2BAA2B,CAAC,CAAC;","names":[]}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -11,6 +11,11 @@ interface AdapterBase {
|
|
|
11
11
|
subscribe?(onChange: () => void): () => void;
|
|
12
12
|
/** False when new state only takes effect on a reload, e.g. a snapshot an actor starts from. */
|
|
13
13
|
inPlace?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* False to apply this state from catalog scenarios only, and skip it for inline links.
|
|
16
|
+
* For state that outlives the scenario, such as a cookie the real backend reads.
|
|
17
|
+
*/
|
|
18
|
+
fromLinks?: boolean;
|
|
14
19
|
}
|
|
15
20
|
/** Decodes scenario JSON with `schema`, so `apply` receives typed state. */
|
|
16
21
|
export interface TypedAdapter<T> extends AdapterBase {
|
package/dist/schema.d.ts
CHANGED
|
@@ -53,6 +53,7 @@ export declare const ScenarioSchema: z.ZodObject<{
|
|
|
53
53
|
description: z.ZodOptional<z.ZodString>;
|
|
54
54
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
55
55
|
extends: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
56
|
+
path: z.ZodOptional<z.ZodString>;
|
|
56
57
|
url: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
57
58
|
network: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
58
59
|
method: z.ZodDefault<z.ZodEnum<{
|
|
@@ -101,6 +102,7 @@ export declare function scenarioJsonSchema({ state }?: {
|
|
|
101
102
|
description: z.ZodOptional<z.ZodString>;
|
|
102
103
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
103
104
|
extends: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
105
|
+
path: z.ZodOptional<z.ZodString>;
|
|
104
106
|
url: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
105
107
|
network: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
106
108
|
method: z.ZodDefault<z.ZodEnum<{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "state-scenarios",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Put a running app into any state from a URL: shareable JSON scenarios for network, URL and app state, with a dev panel, Playwright helpers and adapters.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,13 +48,18 @@
|
|
|
48
48
|
"types": "./dist/adapters/xstate.d.ts",
|
|
49
49
|
"source": "./src/adapters/xstate.ts",
|
|
50
50
|
"import": "./dist/adapters/xstate.js"
|
|
51
|
+
},
|
|
52
|
+
"./cookies": {
|
|
53
|
+
"types": "./dist/adapters/cookies.d.ts",
|
|
54
|
+
"source": "./src/adapters/cookies.ts",
|
|
55
|
+
"import": "./dist/adapters/cookies.js"
|
|
51
56
|
}
|
|
52
57
|
},
|
|
53
58
|
"publishConfig": {
|
|
54
59
|
"access": "public"
|
|
55
60
|
},
|
|
56
61
|
"peerDependencies": {
|
|
57
|
-
"msw": "^3.0.0",
|
|
62
|
+
"msw": "^2.4.0 || ^3.0.0",
|
|
58
63
|
"zod": "^4.1.0",
|
|
59
64
|
"@playwright/test": ">=1.40",
|
|
60
65
|
"@tanstack/react-query": ">=5"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/link.ts"],"sourcesContent":["import type { Scenario } from './schema.js';\n\ndeclare global {\n /** Requests the active scenario didn't cover, for tests and agents (see `unhandledRequests`). */\n var stateScenariosUnhandled: string[] | undefined;\n}\n\nexport const PARAM = 'scenario';\n\n/** Fail unmatched requests instead of letting them reach the network. */\nexport const STRICT_PARAM = 'scenario-strict';\n\n/** Inline scenario JSON, kept in the URL hash so it never reaches a server. */\nexport const DATA_KEY = 'scenario-data';\n\n/**\n * A shareable URL that opens the app in a resolved scenario.\n * `inline` embeds the flattened scenario, so the link works without the catalog.\n */\nexport function scenarioHref(\n resolved: Scenario,\n { base = location.href, inline = false, strict = false } = {},\n): string {\n const url = new URL(base);\n url.search = '';\n url.hash = '';\n\n for (const [k, v] of Object.entries(resolved.url ?? {})) {\n if (v !== null) url.searchParams.set(k, v);\n }\n\n if (inline) {\n const { $schema: _schema, extends: _extends, ...snapshot } = resolved;\n url.hash = new URLSearchParams({ [DATA_KEY]: JSON.stringify(snapshot) }).toString();\n } else {\n url.searchParams.set(PARAM, resolved.name);\n }\n\n if (strict) url.searchParams.set(STRICT_PARAM, '');\n\n return url.toString();\n}\n\n/** Inline scenario JSON from a URL hash, if present. */\nexport const readInline = (hash: string) => new URLSearchParams(hash.slice(1)).get(DATA_KEY);\n"],"mappings":";AAOO,IAAM,QAAQ;AAGd,IAAM,eAAe;AAGrB,IAAM,WAAW;AAMjB,SAAS,aACd,UACA,EAAE,OAAO,SAAS,MAAM,SAAS,OAAO,SAAS,MAAM,IAAI,CAAC,GACpD;AACR,QAAM,MAAM,IAAI,IAAI,IAAI;AACxB,MAAI,SAAS;AACb,MAAI,OAAO;AAEX,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,SAAS,OAAO,CAAC,CAAC,GAAG;AACvD,QAAI,MAAM,KAAM,KAAI,aAAa,IAAI,GAAG,CAAC;AAAA,EAC3C;AAEA,MAAI,QAAQ;AACV,UAAM,EAAE,SAAS,SAAS,SAAS,UAAU,GAAG,SAAS,IAAI;AAC7D,QAAI,OAAO,IAAI,gBAAgB,EAAE,CAAC,QAAQ,GAAG,KAAK,UAAU,QAAQ,EAAE,CAAC,EAAE,SAAS;AAAA,EACpF,OAAO;AACL,QAAI,aAAa,IAAI,OAAO,SAAS,IAAI;AAAA,EAC3C;AAEA,MAAI,OAAQ,KAAI,aAAa,IAAI,cAAc,EAAE;AAEjD,SAAO,IAAI,SAAS;AACtB;AAGO,IAAM,aAAa,CAAC,SAAiB,IAAI,gBAAgB,KAAK,MAAM,CAAC,CAAC,EAAE,IAAI,QAAQ;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schema.ts"],"sourcesContent":["import { z } from 'zod';\n\n/** Scenarios live in JSON files and URLs, so everything they carry is JSON. */\nexport const JsonSchema = z.json();\n\nexport type Json = z.output<typeof JsonSchema>;\n\nexport type JsonObject = { [key: string]: Json; };\n\n/** Anything an app can hand over to be stored as JSON. */\nexport type Serializable = object | string | number | boolean | null;\n\n/**\n * Decode JSON text. Pair with `JSON.stringify` to copy app state into the format scenarios\n * are stored in: functions and undefined drop out, and the result is checked to be JSON.\n */\nexport const parseJson = (text: string): Json => JsonSchema.parse(JSON.parse(text));\n\nconst ResponseSchema = z\n .object({\n status: z.number().int().default(200),\n /** Sent as JSON. */\n body: JsonSchema.optional(),\n /** Sent as text, e.g. an HTML error page. */\n text: z.string().optional(),\n headers: z.record(z.string(), z.string()).optional(),\n /** Milliseconds, or \"infinite\" to hold the request open (loading states). */\n delay: z.union([z.number().nonnegative(), z.literal('infinite')]).optional(),\n })\n .refine((r) => r.body === undefined || r.text === undefined, {\n message: 'Provide \"body\" (JSON) or \"text\", not both',\n });\n\nconst NetworkEntrySchema = z\n .object({\n method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']).default('GET'),\n /** MSW path syntax: \"/api/users/:id\", absolute URLs and wildcards work. */\n path: z.string().min(1),\n /** Only match requests whose query string contains these values. */\n query: z.record(z.string(), z.string()).optional(),\n response: ResponseSchema.optional(),\n /** Successive responses for successive calls; the last one repeats. */\n sequence: z.array(ResponseSchema).min(1).optional(),\n /** Drop the inherited entry with the same method, path and query. */\n remove: z.literal(true).optional(),\n })\n .refine((e) => [e.response, e.sequence, e.remove].filter(Boolean).length === 1, {\n message: 'Provide exactly one of \"response\", \"sequence\" or \"remove\"',\n });\n\nexport const ScenarioSchema = z.object({\n $schema: z.string().optional(),\n // Kebab-case; a combined scenario is named by its parts, e.g. \"signed-in,server-error\".\n name: z.string().regex(/^[a-z0-9][a-z0-9-]*(,[a-z0-9][a-z0-9-]*)*$/, 'Use kebab-case, e.g. \"empty-search\"'),\n /** Not inherited. */\n description: z.string().optional(),\n /** Not inherited. */\n tags: z.array(z.string()).optional(),\n /** Scenario name(s) to build on. Later parents override earlier ones. */\n extends: z.union([z.string(), z.array(z.string())]).optional(),\n /** Query params the app should start with. null removes an inherited param. */\n url: z.record(z.string(), z.string().nullable()).optional(),\n /**\n * A child entry replaces the inherited entry with the same method, path and query.\n * Otherwise earlier entries win, and a scenario's own entries come before its parents'.\n */\n network: z.array(NetworkEntrySchema).optional(),\n /** Per-adapter state, keyed by adapter key. Objects deep-merge; null removes a key. */\n state: z.record(z.string(), JsonSchema).optional(),\n /**\n * Playwright selector that is visible once the UI shows this state,\n * e.g. \"text=Loading countries…\". Not inherited: each state looks different.\n */\n ready: z.string().optional(),\n});\n\nexport type Scenario = z.output<typeof ScenarioSchema>;\n\nexport type ScenarioInput = z.input<typeof ScenarioSchema>;\n\nexport type NetworkEntry = z.output<typeof NetworkEntrySchema>;\n\nexport type ScenarioResponse = z.output<typeof ResponseSchema>;\n\n/**\n * JSON Schema for editors, validators and AI generators.\n * Pass adapter state schemas so `state` is described too.\n */\nexport function scenarioJsonSchema({ state = {} }: { state?: Record<string, z.ZodType>; } = {}) {\n return z.toJSONSchema(\n ScenarioSchema.extend({ state: z.object(state).partial().catchall(JsonSchema).optional() }),\n {\n io: 'input',\n // Refinements aren't representable in JSON Schema; spell the exclusivity rules out.\n override: ({ zodSchema, jsonSchema }) => {\n if (zodSchema === NetworkEntrySchema) {\n jsonSchema.oneOf = ['response', 'sequence', 'remove'].map((key) => ({ required: [key] }));\n }\n\n if (zodSchema === ResponseSchema) {\n jsonSchema.not = { required: ['body', 'text'] };\n }\n },\n },\n );\n}\n\n/** Validate scenario input (from a file, URL or editor) into a Scenario, with readable errors. */\nexport function parseScenario(data: ScenarioInput, source = 'scenario'): Scenario {\n const result = ScenarioSchema.safeParse(data);\n\n if (result.success) return result.data;\n throw new Error(`Invalid scenario \"${data.name || source}\":\\n${z.prettifyError(result.error)}`);\n}\n"],"mappings":"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","names":[]}
|
|
File without changes
|