executable-stories-showcase 0.0.0-stage → 0.0.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.
Files changed (67) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +17 -2
  3. package/dist/adapters/cookies.d.ts +11 -0
  4. package/dist/adapters/cookies.js +27 -0
  5. package/dist/adapters/cookies.js.map +1 -0
  6. package/dist/adapters/redux.d.ts +23 -0
  7. package/dist/adapters/redux.js +28 -0
  8. package/dist/adapters/redux.js.map +1 -0
  9. package/dist/adapters/tanstack-query.d.ts +17 -0
  10. package/dist/adapters/tanstack-query.js +43 -0
  11. package/dist/adapters/tanstack-query.js.map +1 -0
  12. package/dist/adapters/xstate.d.ts +33 -0
  13. package/dist/adapters/xstate.js +36 -0
  14. package/dist/adapters/xstate.js.map +1 -0
  15. package/dist/adapters/zustand.d.ts +17 -0
  16. package/dist/adapters/zustand.js +16 -0
  17. package/dist/adapters/zustand.js.map +1 -0
  18. package/dist/capture-GCESXDQD.js +10 -0
  19. package/dist/capture-GCESXDQD.js.map +1 -0
  20. package/dist/capture.d.ts +26 -0
  21. package/dist/catalog.d.ts +27 -0
  22. package/dist/chunk-7VEOUHJP.js +40 -0
  23. package/dist/chunk-7VEOUHJP.js.map +1 -0
  24. package/dist/chunk-BK4EEQXV.js +98 -0
  25. package/dist/chunk-BK4EEQXV.js.map +1 -0
  26. package/dist/chunk-MCC3CAWQ.js +208 -0
  27. package/dist/chunk-MCC3CAWQ.js.map +1 -0
  28. package/dist/chunk-PKGBYTBX.js +46 -0
  29. package/dist/chunk-PKGBYTBX.js.map +1 -0
  30. package/dist/chunk-QDLRQPTA.js +125 -0
  31. package/dist/chunk-QDLRQPTA.js.map +1 -0
  32. package/dist/chunk-XLNYICOI.js +234 -0
  33. package/dist/chunk-XLNYICOI.js.map +1 -0
  34. package/dist/chunk-Z664X6AQ.js +148 -0
  35. package/dist/chunk-Z664X6AQ.js.map +1 -0
  36. package/dist/index.d.ts +9 -0
  37. package/dist/index.js +531 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/jwt.d.ts +7 -0
  40. package/dist/link.d.ts +32 -0
  41. package/dist/network-4UFPO74C.js +7 -0
  42. package/dist/network-4UFPO74C.js.map +1 -0
  43. package/dist/network.d.ts +3 -0
  44. package/dist/next.d.ts +21 -0
  45. package/dist/next.js +30 -0
  46. package/dist/next.js.map +1 -0
  47. package/dist/no-msw-browser.d.ts +2 -0
  48. package/dist/no-msw-browser.js +8 -0
  49. package/dist/no-msw-browser.js.map +1 -0
  50. package/dist/panel.d.ts +30 -0
  51. package/dist/panel.js +608 -0
  52. package/dist/panel.js.map +1 -0
  53. package/dist/playwright.d.ts +31 -0
  54. package/dist/playwright.js +92 -0
  55. package/dist/playwright.js.map +1 -0
  56. package/dist/ready-4MENAMNI.js +13 -0
  57. package/dist/ready-4MENAMNI.js.map +1 -0
  58. package/dist/ready.d.ts +39 -0
  59. package/dist/record.d.ts +49 -0
  60. package/dist/redact.d.ts +19 -0
  61. package/dist/runtime.d.ts +191 -0
  62. package/dist/save.d.ts +27 -0
  63. package/dist/schema.d.ts +161 -0
  64. package/dist/vite.d.ts +11 -0
  65. package/dist/vite.js +26 -0
  66. package/dist/vite.js.map +1 -0
  67. package/package.json +118 -3
@@ -0,0 +1,234 @@
1
+ // src/ready.ts
2
+ var SKIP = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "META", "LINK", "HEAD", "HTML"]);
3
+ var MAX_TEXT = 60;
4
+ var CHROME = "header, nav, aside, footer, [role=banner], [role=navigation], [role=complementary], [role=contentinfo]";
5
+ var NAME_FROM_CONTENT = /* @__PURE__ */ new Set([
6
+ "button",
7
+ "cell",
8
+ "checkbox",
9
+ "columnheader",
10
+ "gridcell",
11
+ "heading",
12
+ "link",
13
+ "menuitem",
14
+ "menuitemcheckbox",
15
+ "menuitemradio",
16
+ "option",
17
+ "radio",
18
+ "row",
19
+ "rowheader",
20
+ "switch",
21
+ "tab",
22
+ "tooltip",
23
+ "treeitem"
24
+ ]);
25
+ var IMPLICIT_ROLES = /* @__PURE__ */ new Set([
26
+ "button",
27
+ "link",
28
+ "heading",
29
+ "img",
30
+ "textbox",
31
+ "searchbox",
32
+ "checkbox",
33
+ "radio",
34
+ "combobox"
35
+ ]);
36
+ function selectorForElement(el) {
37
+ if (!(el instanceof Element) || SKIP.has(el.tagName) || el.closest("scenario-panel")) {
38
+ return void 0;
39
+ }
40
+ for (const attr of ["data-testid", "data-test-id"]) {
41
+ const testId = el.getAttribute(attr);
42
+ if (testId) return `[${attr}=${JSON.stringify(testId)}]`;
43
+ }
44
+ const role = roleOf(el);
45
+ const name = role && accessibleName(el, role);
46
+ if (role && name && name.length <= MAX_TEXT) {
47
+ return `role=${role}[name=${JSON.stringify(name)}]`;
48
+ }
49
+ const text = ownText(el);
50
+ if (text && text.length <= MAX_TEXT) {
51
+ const selector = `text=${JSON.stringify(text)}`;
52
+ const check = checkReadySelector(selector, el.ownerDocument);
53
+ if (check.status === "match" && check.count === 1) return selector;
54
+ }
55
+ if (role) return `role=${role}`;
56
+ return void 0;
57
+ }
58
+ function closestReadySelector(el) {
59
+ for (let node = el; node && node !== node.ownerDocument.body; node = node.parentElement) {
60
+ const selector = selectorForElement(node);
61
+ if (selector) return { element: node, selector };
62
+ }
63
+ return void 0;
64
+ }
65
+ function suggestReadySelectors(root, { limit = 3 } = {}) {
66
+ const target = root ?? globalThis.document?.body;
67
+ if (!target) return [];
68
+ const ranked = [];
69
+ const seen = /* @__PURE__ */ new Set();
70
+ const walk = (node) => {
71
+ if (SKIP.has(node.tagName) || node.localName === "scenario-panel" || !isVisible(node)) return;
72
+ const selector = selectorForElement(node);
73
+ const tooShort = selector?.startsWith("text=") && selector.length < 'text=""'.length + 3;
74
+ if (selector && !tooShort && !seen.has(selector)) {
75
+ seen.add(selector);
76
+ ranked.push({ score: scoreSelector(selector, node), selector });
77
+ }
78
+ for (const child of node.children) walk(child);
79
+ };
80
+ if (target instanceof Element) walk(target);
81
+ else for (const child of target.children) walk(child);
82
+ return ranked.toSorted((a, b) => b.score - a.score).slice(0, limit).map((r) => r.selector);
83
+ }
84
+ function checkReadySelector(selector, doc = document) {
85
+ const s = selector.trim();
86
+ if (!s || s.includes(">>")) return { status: "unchecked" };
87
+ const found = s.startsWith("text=") ? matchText(s.slice(5), doc) : s.startsWith("role=") ? matchRole(s.slice(5), doc) : s.startsWith("css=") || !/^[a-z-]+=/i.test(s) ? matchCss(s.replace(/^css=/, ""), doc) : void 0;
88
+ if (!found) return { status: "unchecked" };
89
+ if ("error" in found) return { status: "invalid", error: found.error };
90
+ const [first] = found;
91
+ return first ? { status: "match", count: found.length, first, visible: isShown(first) } : { status: "none" };
92
+ }
93
+ function matchText(body, doc) {
94
+ const quoted = unquote(body);
95
+ if (quoted !== void 0) {
96
+ return appElements(doc).filter(
97
+ (el) => [...el.childNodes].some(
98
+ (n) => n.nodeType === Node.TEXT_NODE && collapse(n.textContent ?? "") === quoted
99
+ )
100
+ );
101
+ }
102
+ if (body.startsWith("/")) return void 0;
103
+ const needle = collapse(body).toLowerCase();
104
+ if (!needle) return void 0;
105
+ const has = (el) => collapse(el.textContent ?? "").toLowerCase().includes(needle);
106
+ return appElements(doc).filter((el) => has(el) && ![...el.children].some(has));
107
+ }
108
+ function matchRole(body, doc) {
109
+ const m = /^([a-z]+)(?:\[name=(".*")\])?$/.exec(body);
110
+ if (!m) return void 0;
111
+ const [, role, rawName] = m;
112
+ const name = rawName === void 0 ? void 0 : unquote(rawName);
113
+ if (rawName !== void 0 && name === void 0) return void 0;
114
+ const found = appElements(doc).filter(
115
+ (el) => roleOf(el) === role && isShown(el) && !el.closest("[aria-hidden=true]") && (name === void 0 || accessibleName(el, role) === name)
116
+ );
117
+ if (!found.length && !IMPLICIT_ROLES.has(role)) return void 0;
118
+ return found;
119
+ }
120
+ function matchCss(css, doc) {
121
+ try {
122
+ return [...doc.querySelectorAll(css)].filter((el) => !el.closest("scenario-panel"));
123
+ } catch {
124
+ return { error: "Not a valid CSS selector" };
125
+ }
126
+ }
127
+ function unquote(s) {
128
+ if (!s.startsWith('"') || !s.endsWith('"') || s.length < 2) return void 0;
129
+ try {
130
+ return JSON.parse(s);
131
+ } catch {
132
+ return void 0;
133
+ }
134
+ }
135
+ function appElements(doc) {
136
+ const out = [];
137
+ const walk = (el) => {
138
+ if (SKIP.has(el.tagName) || el.localName === "scenario-panel") return;
139
+ out.push(el);
140
+ for (const child of el.children) walk(child);
141
+ };
142
+ if (doc.body) walk(doc.body);
143
+ return out;
144
+ }
145
+ function scoreSelector(selector, el) {
146
+ let score = 0;
147
+ if (selector.startsWith("[data-test")) score += 100;
148
+ else if (selector.startsWith("role=") && selector.includes("[name=")) score += 80;
149
+ else if (selector.startsWith("text=")) score += 60;
150
+ else if (selector.startsWith("role=")) score += 40;
151
+ if (el.children.length === 0) score += 10;
152
+ if (el.tagName === "H1" || el.tagName === "H2" || el.tagName === "H3") score += 15;
153
+ const role = el.getAttribute("role");
154
+ if (role === "alert" || role === "status") score += 20;
155
+ if (el.closest(CHROME)) score -= 50;
156
+ if (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT") score -= 50;
157
+ return score;
158
+ }
159
+ function roleOf(el) {
160
+ return el.getAttribute("role")?.trim().split(/\s+/)[0] || implicitRole(el);
161
+ }
162
+ function implicitRole(el) {
163
+ const tag = el.tagName;
164
+ if (tag === "BUTTON") return "button";
165
+ if (tag === "A" && el.hasAttribute("href")) return "link";
166
+ if (tag === "H1" || tag === "H2" || tag === "H3" || tag === "H4" || tag === "H5" || tag === "H6") {
167
+ return "heading";
168
+ }
169
+ if (tag === "IMG") return "img";
170
+ if (tag === "INPUT") {
171
+ const type = (el.getAttribute("type") ?? "text").toLowerCase();
172
+ if (type === "submit" || type === "button") return "button";
173
+ if (type === "checkbox") return "checkbox";
174
+ if (type === "radio") return "radio";
175
+ if (type === "search") return "searchbox";
176
+ return "textbox";
177
+ }
178
+ if (tag === "TEXTAREA") return "textbox";
179
+ if (tag === "SELECT") return "combobox";
180
+ return void 0;
181
+ }
182
+ function accessibleName(el, role) {
183
+ const labelledBy = el.getAttribute("aria-labelledby");
184
+ if (labelledBy) {
185
+ const text = collapse(
186
+ labelledBy.split(/\s+/).map((id) => el.ownerDocument.getElementById(id)?.textContent ?? "").join(" ")
187
+ );
188
+ if (text) return text;
189
+ }
190
+ const label = collapse(el.getAttribute("aria-label") ?? "");
191
+ if (label) return label;
192
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
193
+ const fromLabel = collapse([...el.labels ?? []].map((l) => l.textContent ?? "").join(" "));
194
+ if (fromLabel) return fromLabel;
195
+ if (!(el instanceof HTMLSelectElement) && el.placeholder) return collapse(el.placeholder);
196
+ }
197
+ if (el instanceof HTMLImageElement && el.alt) return collapse(el.alt);
198
+ if (NAME_FROM_CONTENT.has(role)) return collapse(el.textContent ?? "") || void 0;
199
+ return collapse(el.getAttribute("title") ?? "") || void 0;
200
+ }
201
+ function ownText(el) {
202
+ for (const n of el.childNodes) {
203
+ const text = n.nodeType === Node.TEXT_NODE ? collapse(n.textContent ?? "") : "";
204
+ if (text) return text;
205
+ }
206
+ return el.children.length === 1 ? ownText(el.children[0]) : "";
207
+ }
208
+ function collapse(s) {
209
+ return s.replace(/\s+/g, " ").trim();
210
+ }
211
+ function isVisible(el) {
212
+ if (!(el instanceof HTMLElement)) return true;
213
+ const style = el.ownerDocument.defaultView?.getComputedStyle(el);
214
+ if (style && (style.display === "none" || style.visibility === "hidden")) return false;
215
+ if (el.hidden || el.getAttribute("aria-hidden") === "true") return false;
216
+ return true;
217
+ }
218
+ function isShown(el) {
219
+ for (let node = el; node; node = node.parentElement) {
220
+ if (!(node instanceof HTMLElement)) continue;
221
+ if (node.hidden) return false;
222
+ const style = node.ownerDocument.defaultView?.getComputedStyle(node);
223
+ if (style && (style.display === "none" || style.visibility === "hidden")) return false;
224
+ }
225
+ return true;
226
+ }
227
+
228
+ export {
229
+ selectorForElement,
230
+ closestReadySelector,
231
+ suggestReadySelectors,
232
+ checkReadySelector
233
+ };
234
+ //# sourceMappingURL=chunk-XLNYICOI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ready.ts"],"sourcesContent":["const SKIP = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'META', 'LINK', 'HEAD', 'HTML']);\n\nconst MAX_TEXT = 60;\n\nconst CHROME =\n 'header, nav, aside, footer, [role=banner], [role=navigation], [role=complementary], [role=contentinfo]';\n\n// Roles whose accessible name comes from their text. Others (alert, status, region…) are\n// named only by aria-label/labelledby, so Playwright's [name=…] never matches their text.\nconst NAME_FROM_CONTENT = new Set([\n 'button',\n 'cell',\n 'checkbox',\n 'columnheader',\n 'gridcell',\n 'heading',\n 'link',\n 'menuitem',\n 'menuitemcheckbox',\n 'menuitemradio',\n 'option',\n 'radio',\n 'row',\n 'rowheader',\n 'switch',\n 'tab',\n 'tooltip',\n 'treeitem',\n]);\n\n// Implicit roles implicitRole knows; for any other role only an explicit role=\"…\" is checkable.\nconst IMPLICIT_ROLES = new Set([\n 'button',\n 'link',\n 'heading',\n 'img',\n 'textbox',\n 'searchbox',\n 'checkbox',\n 'radio',\n 'combobox',\n]);\n\n/** Playwright selector for an element the user marked as the scenario's ready signal. */\nexport function selectorForElement(el: Element): string | undefined {\n if (!(el instanceof Element) || SKIP.has(el.tagName) || el.closest('scenario-panel')) {\n return undefined;\n }\n\n for (const attr of ['data-testid', 'data-test-id']) {\n const testId = el.getAttribute(attr);\n\n if (testId) return `[${attr}=${JSON.stringify(testId)}]`;\n }\n\n const role = roleOf(el);\n const name = role && accessibleName(el, role);\n\n if (role && name && name.length <= MAX_TEXT) {\n return `role=${role}[name=${JSON.stringify(name)}]`;\n }\n\n const text = ownText(el);\n\n if (text && text.length <= MAX_TEXT) {\n const selector = `text=${JSON.stringify(text)}`;\n const check = checkReadySelector(selector, el.ownerDocument);\n\n if (check.status === 'match' && check.count === 1) return selector;\n }\n\n if (role) return `role=${role}`;\n\n return undefined;\n}\n\n/**\n * The nearest element at or above `el` that has a ready selector, so clicking an icon or a\n * wrapper still marks something.\n */\nexport function closestReadySelector(el: Element): { element: Element; selector: string; } | undefined {\n for (let node: Element | null = el; node && node !== node.ownerDocument.body; node = node.parentElement) {\n const selector = selectorForElement(node);\n\n if (selector) return { element: node, selector };\n }\n\n return undefined;\n}\n\n/**\n * Ranked Playwright selectors for visible, distinctive UI, offered when the user\n * hasn't marked ready. Prefers test ids, then named roles, then unique short text.\n */\nexport function suggestReadySelectors(\n root?: ParentNode | null,\n { limit = 3 }: { limit?: number; } = {},\n): string[] {\n const target = root ?? globalThis.document?.body;\n\n if (!target) return [];\n\n const ranked: { score: number; selector: string; }[] = [];\n const seen = new Set<string>();\n\n const walk = (node: Element) => {\n if (SKIP.has(node.tagName) || node.localName === 'scenario-panel' || !isVisible(node)) return;\n\n const selector = selectorForElement(node);\n // A count or a single character (\"0\" in \"0 countries\") is too generic to suggest.\n const tooShort = selector?.startsWith('text=') && selector.length < 'text=\"\"'.length + 3;\n\n if (selector && !tooShort && !seen.has(selector)) {\n seen.add(selector);\n ranked.push({ score: scoreSelector(selector, node), selector });\n }\n\n for (const child of node.children) walk(child);\n };\n\n if (target instanceof Element) walk(target);\n else for (const child of target.children) walk(child);\n\n return ranked.toSorted((a, b) => b.score - a.score).slice(0, limit).map((r) => r.selector);\n}\n\nexport type ReadyCheck =\n /** `first` is what `openScenario` waits on; `visible` says whether it's showing now. */\n | { status: 'match'; count: number; first: Element; visible: boolean; }\n | { status: 'none'; }\n /** A selector form this page can't evaluate (xpath, chains, regex…); Playwright still can. */\n | { status: 'unchecked'; }\n | { status: 'invalid'; error: string; };\n\n/**\n * Evaluate a ready selector against the live page the way Playwright would, for the forms\n * the panel generates and plain CSS. Anything else comes back `unchecked`, never a guess.\n */\nexport function checkReadySelector(selector: string, doc: Document = document): ReadyCheck {\n const s = selector.trim();\n\n if (!s || s.includes('>>')) return { status: 'unchecked' };\n\n const found = s.startsWith('text=')\n ? matchText(s.slice(5), doc)\n : s.startsWith('role=')\n ? matchRole(s.slice(5), doc)\n : s.startsWith('css=') || !/^[a-z-]+=/i.test(s)\n ? matchCss(s.replace(/^css=/, ''), doc)\n : undefined;\n\n if (!found) return { status: 'unchecked' };\n\n if ('error' in found) return { status: 'invalid', error: found.error };\n\n const [first] = found;\n\n return first\n ? { status: 'match', count: found.length, first, visible: isShown(first) }\n : { status: 'none' };\n}\n\ntype Found = Element[] | { error: string; } | undefined;\n\nfunction matchText(body: string, doc: Document): Found {\n const quoted = unquote(body);\n\n // Quoted: an element with a text node equal to it, case-sensitive.\n if (quoted !== undefined) {\n return appElements(doc).filter((el) =>\n [...el.childNodes].some((n) =>\n n.nodeType === Node.TEXT_NODE && collapse(n.textContent ?? '') === quoted\n )\n );\n }\n\n if (body.startsWith('/')) return undefined;\n\n // Unquoted: the smallest elements whose text contains it, ignoring case.\n const needle = collapse(body).toLowerCase();\n\n if (!needle) return undefined;\n\n const has = (el: Element) => collapse(el.textContent ?? '').toLowerCase().includes(needle);\n\n return appElements(doc).filter((el) => has(el) && ![...el.children].some(has));\n}\n\nfunction matchRole(body: string, doc: Document): Found {\n const m = /^([a-z]+)(?:\\[name=(\".*\")\\])?$/.exec(body);\n\n if (!m) return undefined;\n\n const [, role, rawName] = m;\n const name = rawName === undefined ? undefined : unquote(rawName);\n\n if (rawName !== undefined && name === undefined) return undefined;\n\n // Like Playwright's role engine, hidden elements don't count.\n const found = appElements(doc).filter((el) =>\n roleOf(el) === role && isShown(el) && !el.closest('[aria-hidden=true]')\n && (name === undefined || accessibleName(el, role) === name)\n );\n\n // We only know some implicit roles (<main> is \"main\", <p> is \"paragraph\"…), so a miss\n // on any other role isn't proof of absence.\n if (!found.length && !IMPLICIT_ROLES.has(role)) return undefined;\n\n return found;\n}\n\nfunction matchCss(css: string, doc: Document): Found {\n try {\n return [...doc.querySelectorAll(css)].filter((el) => !el.closest('scenario-panel'));\n } catch {\n return { error: 'Not a valid CSS selector' };\n }\n}\n\n/** A JSON-style \"quoted\" string, or undefined when `s` isn't one. */\nfunction unquote(s: string): string | undefined {\n if (!s.startsWith('\"') || !s.endsWith('\"') || s.length < 2) return undefined;\n\n try {\n return JSON.parse(s);\n } catch {\n return undefined;\n }\n}\n\nfunction appElements(doc: Document): Element[] {\n const out: Element[] = [];\n\n const walk = (el: Element) => {\n if (SKIP.has(el.tagName) || el.localName === 'scenario-panel') return;\n out.push(el);\n\n for (const child of el.children) walk(child);\n };\n\n if (doc.body) walk(doc.body);\n\n return out;\n}\n\nfunction scoreSelector(selector: string, el: Element): number {\n let score = 0;\n\n if (selector.startsWith('[data-test')) score += 100;\n else if (selector.startsWith('role=') && selector.includes('[name=')) score += 80;\n else if (selector.startsWith('text=')) score += 60;\n else if (selector.startsWith('role=')) score += 40;\n\n // Prefer leaf-ish content over giant containers.\n if (el.children.length === 0) score += 10;\n\n if (el.tagName === 'H1' || el.tagName === 'H2' || el.tagName === 'H3') score += 15;\n\n const role = el.getAttribute('role');\n\n if (role === 'alert' || role === 'status') score += 20;\n\n // Headers, navs, sidebars and inputs render in every state, so they make weak ready signals.\n if (el.closest(CHROME)) score -= 50;\n\n if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT') score -= 50;\n\n return score;\n}\n\nfunction roleOf(el: Element): string | undefined {\n return el.getAttribute('role')?.trim().split(/\\s+/)[0] || implicitRole(el);\n}\n\nfunction implicitRole(el: Element): string | undefined {\n const tag = el.tagName;\n\n if (tag === 'BUTTON') return 'button';\n\n if (tag === 'A' && el.hasAttribute('href')) return 'link';\n\n if (tag === 'H1' || tag === 'H2' || tag === 'H3' || tag === 'H4' || tag === 'H5' || tag === 'H6') {\n return 'heading';\n }\n\n if (tag === 'IMG') return 'img';\n\n if (tag === 'INPUT') {\n const type = (el.getAttribute('type') ?? 'text').toLowerCase();\n\n if (type === 'submit' || type === 'button') return 'button';\n\n if (type === 'checkbox') return 'checkbox';\n\n if (type === 'radio') return 'radio';\n\n if (type === 'search') return 'searchbox';\n\n return 'textbox';\n }\n\n if (tag === 'TEXTAREA') return 'textbox';\n\n if (tag === 'SELECT') return 'combobox';\n\n return undefined;\n}\n\n/** The accessible name Playwright's `[name=…]` compares against, for the common cases. */\nfunction accessibleName(el: Element, role: string): string | undefined {\n const labelledBy = el.getAttribute('aria-labelledby');\n\n if (labelledBy) {\n const text = collapse(\n labelledBy.split(/\\s+/).map((id) => el.ownerDocument.getElementById(id)?.textContent ?? '').join(' '),\n );\n\n if (text) return text;\n }\n\n const label = collapse(el.getAttribute('aria-label') ?? '');\n\n if (label) return label;\n\n if (\n el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement\n ) {\n const fromLabel = collapse([...(el.labels ?? [])].map((l) => l.textContent ?? '').join(' '));\n\n if (fromLabel) return fromLabel;\n\n if (!(el instanceof HTMLSelectElement) && el.placeholder) return collapse(el.placeholder);\n }\n\n if (el instanceof HTMLImageElement && el.alt) return collapse(el.alt);\n\n if (NAME_FROM_CONTENT.has(role)) return collapse(el.textContent ?? '') || undefined;\n\n return collapse(el.getAttribute('title') ?? '') || undefined;\n}\n\n/** The element's first own text node, which is what a quoted `text=` selector matches. */\nfunction ownText(el: Element): string {\n for (const n of el.childNodes) {\n const text = n.nodeType === Node.TEXT_NODE ? collapse(n.textContent ?? '') : '';\n\n if (text) return text;\n }\n\n // No own text: a lone descendant text still identifies this element's content.\n return el.children.length === 1 ? ownText(el.children[0]) : '';\n}\n\nfunction collapse(s: string): string {\n return s.replace(/\\s+/g, ' ').trim();\n}\n\nfunction isVisible(el: Element): boolean {\n if (!(el instanceof HTMLElement)) return true;\n const style = el.ownerDocument.defaultView?.getComputedStyle(el);\n\n if (style && (style.display === 'none' || style.visibility === 'hidden')) return false;\n\n if (el.hidden || el.getAttribute('aria-hidden') === 'true') return false;\n\n return true;\n}\n\n/** Rendered including ancestors: what Playwright's waitFor needs. */\nfunction isShown(el: Element): boolean {\n for (let node: Element | null = el; node; node = node.parentElement) {\n if (!(node instanceof HTMLElement)) continue;\n\n if (node.hidden) return false;\n const style = node.ownerDocument.defaultView?.getComputedStyle(node);\n\n if (style && (style.display === 'none' || style.visibility === 'hidden')) return false;\n }\n\n return true;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,148 @@
1
+ import {
2
+ SAVE_ENDPOINT
3
+ } from "./chunk-PKGBYTBX.js";
4
+ import {
5
+ JsonSchema,
6
+ ScenarioSchema
7
+ } from "./chunk-BK4EEQXV.js";
8
+
9
+ // src/save.ts
10
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
11
+ import { createServer } from "http";
12
+ import { join, relative } from "path";
13
+ import { z } from "zod";
14
+ var MAX_BODY = 20 * 1024 * 1024;
15
+ var SaveRequestSchema = z.object({ scenario: JsonSchema, overwrite: z.boolean().optional() });
16
+ async function handleSaveRequest(request, { dir, root, origins }) {
17
+ const display = (file2) => relative(root, file2).replaceAll("\\", "/");
18
+ const reply = (status, body2) => Response.json(body2, { status });
19
+ if (!allowedOrigin(request.headers, origins)) {
20
+ return reply(403, { error: "Save refused: only the app on localhost may write files" });
21
+ }
22
+ if (request.method === "GET") return reply(200, { dir: display(dir) });
23
+ if (request.method !== "POST") return reply(405, { error: "Use GET or POST" });
24
+ if (!request.headers.get("content-type")?.startsWith("application/json")) {
25
+ return reply(415, { error: "Send application/json" });
26
+ }
27
+ let body;
28
+ try {
29
+ body = SaveRequestSchema.parse(JSON.parse(await readCapped(request)));
30
+ } catch (err) {
31
+ return reply(400, { error: err instanceof z.ZodError ? z.prettifyError(err) : String(err) });
32
+ }
33
+ const parsed = ScenarioSchema.safeParse(body.scenario);
34
+ if (!parsed.success) return reply(400, { error: z.prettifyError(parsed.error) });
35
+ const { name } = parsed.data;
36
+ if (name.includes(",")) return reply(400, { error: "A saved scenario name can't contain a comma" });
37
+ const file = join(dir, `${name}.json`);
38
+ if (existsSync(file) && !body.overwrite) {
39
+ return reply(409, { error: `${display(file)} already exists`, path: display(file) });
40
+ }
41
+ mkdirSync(dir, { recursive: true });
42
+ writeFileSync(file, `${JSON.stringify(body.scenario, null, 2)}
43
+ `);
44
+ return reply(200, { path: display(file), file });
45
+ }
46
+ function createSaveHandler(target) {
47
+ return async (req, res) => {
48
+ const origin = req.headers.origin;
49
+ if (origin && target.origins?.includes(origin)) {
50
+ res.setHeader("access-control-allow-origin", origin);
51
+ res.setHeader("vary", "origin");
52
+ }
53
+ if (req.method === "OPTIONS") {
54
+ res.setHeader("access-control-allow-methods", "GET, POST");
55
+ res.setHeader("access-control-allow-headers", "content-type");
56
+ res.statusCode = 204;
57
+ res.end();
58
+ return;
59
+ }
60
+ let response;
61
+ try {
62
+ response = isLoopbackAddress(req.socket.remoteAddress) ? await handleSaveRequest(toRequest(req), target) : Response.json({ error: "Save refused: only this machine may write files" }, { status: 403 });
63
+ } catch (err) {
64
+ response = Response.json({ error: String(err) }, { status: 500 });
65
+ }
66
+ res.statusCode = response.status;
67
+ response.headers.forEach((value, key) => res.setHeader(key, value));
68
+ res.end(await response.text());
69
+ };
70
+ }
71
+ function startSaveServer(target) {
72
+ const handle = createSaveHandler(target);
73
+ const server = createServer((req, res) => void handle(req, res));
74
+ return new Promise((done, fail) => {
75
+ server.once("error", fail);
76
+ server.listen(0, "127.0.0.1", () => {
77
+ const { port } = z.object({ port: z.number() }).parse(server.address());
78
+ server.unref();
79
+ done(`http://127.0.0.1:${port}${SAVE_ENDPOINT}`);
80
+ });
81
+ });
82
+ }
83
+ function isLoopbackAddress(address) {
84
+ if (!address) return false;
85
+ const ip = address.replace(/^::ffff:/, "");
86
+ return ip === "::1" || /^127(\.\d{1,3}){3}$/.test(ip);
87
+ }
88
+ function allowedOrigin(headers, origins) {
89
+ const host = headers.get("host") ?? "";
90
+ if (!isLoopbackHost(host)) return false;
91
+ const origin = headers.get("origin");
92
+ if (!origin) return true;
93
+ if (origins) return origins.includes(origin);
94
+ try {
95
+ return new URL(origin).host === host;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+ function isLoopbackHost(host) {
101
+ let hostname;
102
+ try {
103
+ hostname = new URL(`http://${host}`).hostname;
104
+ } catch {
105
+ return false;
106
+ }
107
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "[::1]" || /^127(\.\d{1,3}){3}$/.test(hostname);
108
+ }
109
+ function toRequest(req) {
110
+ const headers = new Headers();
111
+ for (let i = 0; i < req.rawHeaders.length; i += 2) headers.append(req.rawHeaders[i], req.rawHeaders[i + 1]);
112
+ const method = req.method ?? "GET";
113
+ const body = method === "GET" || method === "HEAD" ? void 0 : new ReadableStream({
114
+ start(controller) {
115
+ req.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk)));
116
+ req.on("end", () => controller.close());
117
+ req.on("error", (err) => controller.error(err));
118
+ },
119
+ cancel() {
120
+ req.destroy();
121
+ }
122
+ });
123
+ const init = { method, headers, body, duplex: "half" };
124
+ return new Request(new URL(req.url ?? "/", `http://${headers.get("host") ?? "localhost"}`), init);
125
+ }
126
+ async function readCapped(request) {
127
+ const reader = request.body?.getReader();
128
+ if (!reader) return "";
129
+ const chunks = [];
130
+ let size = 0;
131
+ for (; ; ) {
132
+ const { done, value } = await reader.read();
133
+ if (done) break;
134
+ size += value.length;
135
+ if (size > MAX_BODY) {
136
+ await reader.cancel();
137
+ throw new Error("Scenario too large");
138
+ }
139
+ chunks.push(value);
140
+ }
141
+ return Buffer.concat(chunks).toString("utf8");
142
+ }
143
+
144
+ export {
145
+ createSaveHandler,
146
+ startSaveServer
147
+ };
148
+ //# sourceMappingURL=chunk-Z664X6AQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/save.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { createServer, type IncomingMessage, type ServerResponse } from 'node:http';\nimport { join, relative } from 'node:path';\nimport { z } from 'zod';\nimport { SAVE_ENDPOINT } from './link.js';\nimport { JsonSchema, ScenarioSchema } from './schema.js';\n\n// The panel's \"Save to project\" endpoint, shared by the Vite plugin and the Next.js route.\n\nconst MAX_BODY = 20 * 1024 * 1024;\n\nconst SaveRequestSchema = z.object({ scenario: JsonSchema, overwrite: z.boolean().optional() });\n\ninterface SaveReply {\n error?: string;\n /** Saved file, relative to the project root. */\n path?: string;\n /** Saved file, absolute. */\n file?: string;\n /** Scenarios folder, relative to the project root. */\n dir?: string;\n}\n\nexport interface SaveTarget {\n /** Absolute folder scenarios are written into. */\n dir: string;\n /** Absolute project root; replies show paths relative to it. */\n root: string;\n /** Page origins allowed to save cross-origin. Without it, only same-origin requests may. */\n origins?: string[];\n}\n\n/**\n * GET reports the folder; POST `{ scenario, overwrite? }` writes `<dir>/<name>.json`.\n * JSON from an allowed origin on a loopback host only, schema-checked, and never replaces a\n * file unless `overwrite` is set.\n */\nexport async function handleSaveRequest(\n request: Request,\n { dir, root, origins }: SaveTarget,\n): Promise<Response> {\n const display = (file: string) => relative(root, file).replaceAll('\\\\', '/');\n const reply = (status: number, body: SaveReply) => Response.json(body, { status });\n\n if (!allowedOrigin(request.headers, origins)) {\n return reply(403, { error: 'Save refused: only the app on localhost may write files' });\n }\n\n if (request.method === 'GET') return reply(200, { dir: display(dir) });\n\n if (request.method !== 'POST') return reply(405, { error: 'Use GET or POST' });\n\n // A JSON content type also forces a CORS preflight, which only allowed origins pass.\n if (!request.headers.get('content-type')?.startsWith('application/json')) {\n return reply(415, { error: 'Send application/json' });\n }\n\n let body: z.output<typeof SaveRequestSchema>;\n\n try {\n body = SaveRequestSchema.parse(JSON.parse(await readCapped(request)));\n } catch (err) {\n return reply(400, { error: err instanceof z.ZodError ? z.prettifyError(err) : String(err) });\n }\n\n // The schema's kebab-case name rule also keeps the file inside `dir`.\n const parsed = ScenarioSchema.safeParse(body.scenario);\n\n if (!parsed.success) return reply(400, { error: z.prettifyError(parsed.error) });\n\n const { name } = parsed.data;\n\n if (name.includes(',')) return reply(400, { error: \"A saved scenario name can't contain a comma\" });\n\n const file = join(dir, `${name}.json`);\n\n if (existsSync(file) && !body.overwrite) {\n return reply(409, { error: `${display(file)} already exists`, path: display(file) });\n }\n\n mkdirSync(dir, { recursive: true });\n // Write what the panel sent, not the parsed form, so defaults don't bloat the file.\n writeFileSync(file, `${JSON.stringify(body.scenario, null, 2)}\\n`);\n\n return reply(200, { path: display(file), file });\n}\n\n/**\n * The save endpoint as Node middleware. Headers can be forged by any client and the socket\n * can't, so only connections from this machine reach the handler.\n */\nexport function createSaveHandler(target: SaveTarget) {\n return async (req: IncomingMessage, res: ServerResponse) => {\n const origin = req.headers.origin;\n\n // Cross-origin saves (the Next.js save server) need CORS for the origins the target allows.\n if (origin && target.origins?.includes(origin)) {\n res.setHeader('access-control-allow-origin', origin);\n res.setHeader('vary', 'origin');\n }\n\n if (req.method === 'OPTIONS') {\n res.setHeader('access-control-allow-methods', 'GET, POST');\n res.setHeader('access-control-allow-headers', 'content-type');\n res.statusCode = 204;\n res.end();\n\n return;\n }\n\n let response: Response;\n\n try {\n response = isLoopbackAddress(req.socket.remoteAddress)\n ? await handleSaveRequest(toRequest(req), target)\n : Response.json({ error: 'Save refused: only this machine may write files' }, { status: 403 });\n } catch (err) {\n // A bad Host header or a dropped connection: answer rather than leave the request hanging.\n response = Response.json({ error: String(err) }, { status: 500 });\n }\n\n res.statusCode = response.status;\n response.headers.forEach((value, key) => res.setHeader(key, value));\n res.end(await response.text());\n };\n}\n\n/**\n * Start a save server on 127.0.0.1, unreachable from other machines whatever the app server\n * listens on. Resolves to its save URL.\n */\nexport function startSaveServer(target: SaveTarget): Promise<string> {\n const handle = createSaveHandler(target);\n const server = createServer((req, res) => void handle(req, res));\n\n return new Promise((done, fail) => {\n server.once('error', fail);\n server.listen(0, '127.0.0.1', () => {\n const { port } = z.object({ port: z.number() }).parse(server.address());\n server.unref(); // never keeps the dev process alive on its own\n done(`http://127.0.0.1:${port}${SAVE_ENDPOINT}`);\n });\n });\n}\n\n/** True for a loopback peer address, as Node reports it in `socket.remoteAddress`. */\nexport function isLoopbackAddress(address: string | undefined): boolean {\n if (!address) return false;\n const ip = address.replace(/^::ffff:/, '');\n\n return ip === '::1' || /^127(\\.\\d{1,3}){3}$/.test(ip);\n}\n\nfunction allowedOrigin(headers: Headers, origins: string[] | undefined): boolean {\n const host = headers.get('host') ?? '';\n\n // DNS rebinding points an attacker's domain at 127.0.0.1, making its requests \"same-origin\";\n // the Host header still names that domain, so only loopback hosts may write.\n if (!isLoopbackHost(host)) return false;\n\n const origin = headers.get('origin');\n\n // No Origin: same-origin GET, curl or a test. Browsers always send it on cross-origin requests.\n if (!origin) return true;\n\n if (origins) return origins.includes(origin);\n\n try {\n return new URL(origin).host === host;\n } catch {\n return false; // \"null\" from sandboxed frames and file:// pages\n }\n}\n\nfunction isLoopbackHost(host: string): boolean {\n let hostname: string;\n\n try {\n hostname = new URL(`http://${host}`).hostname;\n } catch {\n return false;\n }\n\n return hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '[::1]'\n || /^127(\\.\\d{1,3}){3}$/.test(hostname);\n}\n\nfunction toRequest(req: IncomingMessage): Request {\n const headers = new Headers();\n\n for (let i = 0; i < req.rawHeaders.length; i += 2) headers.append(req.rawHeaders[i], req.rawHeaders[i + 1]);\n\n const method = req.method ?? 'GET';\n\n // Streamed, so the handler can refuse a request before reading (or capping) its body.\n const body = method === 'GET' || method === 'HEAD' ? undefined : new ReadableStream<Uint8Array>({\n start(controller) {\n req.on('data', (chunk: Buffer) => controller.enqueue(new Uint8Array(chunk)));\n req.on('end', () => controller.close());\n req.on('error', (err) => controller.error(err));\n },\n cancel() {\n req.destroy();\n },\n });\n\n const init: RequestInit & { duplex: 'half'; } = { method, headers, body, duplex: 'half' };\n\n return new Request(new URL(req.url ?? '/', `http://${headers.get('host') ?? 'localhost'}`), init);\n}\n\nasync function readCapped(request: Request): Promise<string> {\n const reader = request.body?.getReader();\n\n if (!reader) return '';\n\n const chunks: Uint8Array[] = [];\n let size = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) break;\n size += value.length;\n\n if (size > MAX_BODY) {\n await reader.cancel();\n throw new Error('Scenario too large');\n }\n\n chunks.push(value);\n }\n\n return Buffer.concat(chunks).toString('utf8');\n}\n"],"mappings":"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","names":["file","body"]}
@@ -0,0 +1,9 @@
1
+ export { type CapturedScenario, type CaptureOptions, captureScenario } from './capture.js';
2
+ export { type Catalog, createCatalog, deepMerge, defineScenarioRules, diffScenario, type ScenarioRule, } from './catalog.js';
3
+ export { unsignedJwt } from './jwt.js';
4
+ export { redactSecrets } from './redact.js';
5
+ export { DATA_KEY, PARAM, readInline, RECORD_PARAM, scenarioHref, STRICT_PARAM } from './link.js';
6
+ export { toHandlers } from './network.js';
7
+ export { checkReadySelector, type ReadyCheck, selectorForElement, suggestReadySelectors } from './ready.js';
8
+ export { type Adapter, type Connection, currentSession, type JsonAdapter, type MswWorker, navigate, type PageHooks, type ReadyReport, type ScenarioSession, type StartOptions, startScenarios, type TypedAdapter, } from './runtime.js';
9
+ export { type Json, type JsonObject, JsonSchema, type NetworkEntry, parseJson, parseScenario, type Scenario, type ScenarioInput, scenarioJsonSchema, ScenarioSchema, type Serializable, } from './schema.js';