executable-stories-showcase 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.
@@ -24,8 +24,16 @@ declare global {
24
24
  }
25
25
  /** Requests the active scenario didn't cover, e.g. to assert none in strict runs. */
26
26
  export declare const unhandledRequests: (page: Page) => Promise<string[]>;
27
+ export interface CursorOptions {
28
+ /** `'arrow'` (default), `'dot'`, or your own SVG/HTML markup, drawn with its top-left corner on the mouse. */
29
+ pointer?: 'arrow' | 'dot' | (string & {});
30
+ /** Any CSS color for the pointer, its pulsing halo and the click ripple. Default `#ff3d71`. */
31
+ color?: string;
32
+ }
27
33
  /**
28
34
  * Draw a mouse pointer that follows Playwright's mouse, since recorded videos don't show one.
29
- * It glides between points and pulses on click. Call before the page loads, e.g. before `openScenario`.
35
+ * It pulses so the eye finds it, glides between points, follows focus (so `fill()` steps show where
36
+ * typing lands), and drops a ripple on each click. One pointer is drawn in the top frame and
37
+ * follows the mouse into iframes. Call before the page loads, e.g. before `openScenario`.
30
38
  */
31
- export declare function showCursor(page: Page): Promise<void>;
39
+ export declare function showCursor(page: Page, { pointer, color }?: CursorOptions): Promise<void>;
@@ -54,34 +54,86 @@ async function setPanel(page, show) {
54
54
  });
55
55
  }
56
56
  var unhandledRequests = (page) => page.evaluate(() => globalThis.executableStoriesShowcaseUnhandled ?? []);
57
- async function showCursor(page) {
58
- await page.addInitScript(() => {
57
+ async function showCursor(page, { pointer = "arrow", color = "#ff3d71" } = {}) {
58
+ await page.addInitScript(({ pointer: pointer2, color: color2 }) => {
59
+ let show = (p) => parent.postMessage({ executableStoriesShowcaseCursor: p }, "*");
60
+ let clicking = false;
61
+ addEventListener("mousemove", (e) => show({ type: "move", x: e.clientX, y: e.clientY }), true);
62
+ addEventListener("mousedown", (e) => {
63
+ clicking = true;
64
+ show({ type: "down", x: e.clientX, y: e.clientY });
65
+ }, true);
66
+ addEventListener("mouseup", (e) => {
67
+ show({ type: "up", x: e.clientX, y: e.clientY });
68
+ setTimeout(() => clicking = false);
69
+ }, true);
70
+ addEventListener("keydown", () => clicking = false, true);
71
+ addEventListener("focusin", (e) => {
72
+ if (clicking || !(e.target instanceof Element) || e.target instanceof HTMLIFrameElement) return;
73
+ const r = e.target.getBoundingClientRect();
74
+ show({ type: "move", x: r.left + Math.min(24, r.width / 2), y: r.top + r.height / 2 });
75
+ }, true);
76
+ addEventListener("message", (e) => {
77
+ const p = e.data?.executableStoriesShowcaseCursor;
78
+ const frame = [...document.querySelectorAll("iframe")].find((f) => f.contentWindow === e.source);
79
+ if (!p || !frame) return;
80
+ const box = frame.getBoundingClientRect();
81
+ const style2 = getComputedStyle(frame);
82
+ const scaleX = box.width / frame.offsetWidth || 1;
83
+ const scaleY = box.height / frame.offsetHeight || 1;
84
+ show({
85
+ ...p,
86
+ x: box.left + (frame.clientLeft + parseFloat(style2.paddingLeft) + p.x) * scaleX,
87
+ y: box.top + (frame.clientTop + parseFloat(style2.paddingTop) + p.y) * scaleY
88
+ });
89
+ });
90
+ if (window !== window.top) return;
91
+ const style = document.createElement("style");
92
+ style.textContent = `
93
+ .es-cursor, .es-ripple { position: fixed; left: 0; top: 0; pointer-events: none; z-index: 2147483647; }
94
+ .es-cursor { transition: transform .35s cubic-bezier(.2,.7,.3,1); }
95
+ .es-cursor > * { position: absolute; left: 0; top: 0; }
96
+ .es-cursor > :last-child { transition: scale .1s; transform-origin: 0 0; }
97
+ .es-cursor[data-down] > :last-child { scale: .8; }
98
+ .es-halo {
99
+ left: -18px; top: -18px; width: 36px; height: 36px; border-radius: 50%; background: ${color2};
100
+ animation: es-cursor-halo 1.2s ease-out infinite;
101
+ }
102
+ .es-ripple {
103
+ width: 44px; height: 44px; margin: -22px 0 0 -22px; border-radius: 50%; box-sizing: border-box;
104
+ border: 4px solid ${color2}; animation: es-cursor-ripple .6s ease-out forwards;
105
+ }
106
+ @keyframes es-cursor-halo { from { opacity: .8; scale: .4 } to { opacity: 0; scale: 1.6 } }
107
+ @keyframes es-cursor-ripple { from { opacity: .9; scale: .3 } to { opacity: 0; scale: 2.6 } }`;
108
+ const shapes = {
109
+ arrow: `<svg width="24" height="28" viewBox="-1 -1 18 26" style="display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.5))"><path d="M0 0V20L5.5 15 9 23 12 21.5 8.6 14H16Z" fill="${color2}" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
110
+ dot: `<div style="width:22px;height:22px;margin:-11px 0 0 -11px;border-radius:50%;background:${color2};border:3px solid #fff;box-sizing:border-box"></div>`
111
+ };
59
112
  const cursor = document.createElement("div");
113
+ cursor.className = "es-cursor";
60
114
  cursor.setAttribute("aria-hidden", "true");
61
- cursor.style.cssText = [
62
- "position:fixed",
63
- "left:0",
64
- "top:0",
65
- "width:18px",
66
- "height:18px",
67
- "margin:-9px 0 0 -9px",
68
- "border-radius:50%",
69
- "background:rgba(20,20,20,.55)",
70
- "border:2px solid #fff",
71
- "box-shadow:0 1px 4px rgba(0,0,0,.4)",
72
- "pointer-events:none",
73
- "z-index:2147483647",
74
- "transform:translate(-40px,-40px)",
75
- "transition:transform .25s ease-out,scale .1s"
76
- ].join(";");
77
- const place = (e) => cursor.style.transform = `translate(${e.clientX}px,${e.clientY}px)`;
78
- addEventListener("mousemove", place, true);
79
- addEventListener("mousedown", () => cursor.style.scale = "0.7", true);
80
- addEventListener("mouseup", () => cursor.style.scale = "1", true);
81
- const attach = () => document.documentElement.append(cursor);
115
+ cursor.innerHTML = `<div class="es-halo"></div><div>${shapes[pointer2] ?? pointer2}</div>`;
116
+ cursor.style.transform = `translate(${innerWidth / 2}px,${innerHeight / 2}px)`;
117
+ show = ({ type, x, y }) => {
118
+ if (type === "up") {
119
+ delete cursor.dataset.down;
120
+ return;
121
+ }
122
+ cursor.style.transform = `translate(${x}px,${y}px)`;
123
+ if (type === "move") return;
124
+ cursor.dataset.down = "";
125
+ const ripple = document.createElement("div");
126
+ ripple.className = "es-ripple";
127
+ ripple.setAttribute("aria-hidden", "true");
128
+ ripple.style.left = `${x}px`;
129
+ ripple.style.top = `${y}px`;
130
+ ripple.addEventListener("animationend", () => ripple.remove());
131
+ document.documentElement.append(ripple);
132
+ };
133
+ const attach = () => document.documentElement.append(style, cursor);
82
134
  if (document.documentElement) attach();
83
135
  else addEventListener("DOMContentLoaded", attach);
84
- });
136
+ }, { pointer, color });
85
137
  }
86
138
  export {
87
139
  openInlineScenario,
@@ -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 /** 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/**\n * Like `openScenario`, for a scenario defined in the test rather than the catalog.\n * It opens as an inline link, so off localhost the app needs `startScenarios({ allowLinks: true })`.\n */\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 // Relative to the test's baseURL; `//host`, `/\\host` or a full URL would open another site.\n if (!/^\\/(?![/\\\\])/.test(path)) {\n throw new Error(`[executable-stories-showcase] path \"${path}\" must be a path on the app, such as /orders/42`);\n }\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 executableStoriesShowcasePanelHidden: (() => 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('executableStoriesShowcasePanelHidden', () => 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.executableStoriesShowcasePanelHidden?.().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.executableStoriesShowcaseUnhandled ?? []);\n\n/**\n * Draw a mouse pointer that follows Playwright's mouse, since recorded videos don't show one.\n * It glides between points and pulses on click. Call before the page loads, e.g. before `openScenario`.\n */\nexport async function showCursor(page: Page) {\n await page.addInitScript(() => {\n const cursor = document.createElement('div');\n cursor.setAttribute('aria-hidden', 'true');\n cursor.style.cssText = [\n 'position:fixed',\n 'left:0',\n 'top:0',\n 'width:18px',\n 'height:18px',\n 'margin:-9px 0 0 -9px',\n 'border-radius:50%',\n 'background:rgba(20,20,20,.55)',\n 'border:2px solid #fff',\n 'box-shadow:0 1px 4px rgba(0,0,0,.4)',\n 'pointer-events:none',\n 'z-index:2147483647',\n 'transform:translate(-40px,-40px)',\n 'transition:transform .25s ease-out,scale .1s',\n ].join(';');\n\n const place = (e: MouseEvent) => (cursor.style.transform = `translate(${e.clientX}px,${e.clientY}px)`);\n addEventListener('mousemove', place, true);\n addEventListener('mousedown', () => (cursor.style.scale = '0.7'), true);\n addEventListener('mouseup', () => (cursor.style.scale = '1'), true);\n\n const attach = () => document.documentElement.append(cursor);\n\n if (document.documentElement) attach();\n else addEventListener('DOMContentLoaded', attach);\n });\n}\n"],"mappings":"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","names":[]}
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/**\n * Like `openScenario`, for a scenario defined in the test rather than the catalog.\n * It opens as an inline link, so off localhost the app needs `startScenarios({ allowLinks: true })`.\n */\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 // Relative to the test's baseURL; `//host`, `/\\host` or a full URL would open another site.\n if (!/^\\/(?![/\\\\])/.test(path)) {\n throw new Error(`[executable-stories-showcase] path \"${path}\" must be a path on the app, such as /orders/42`);\n }\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 executableStoriesShowcasePanelHidden: (() => 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('executableStoriesShowcasePanelHidden', () => 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.executableStoriesShowcasePanelHidden?.().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.executableStoriesShowcaseUnhandled ?? []);\n\nexport interface CursorOptions {\n /** `'arrow'` (default), `'dot'`, or your own SVG/HTML markup, drawn with its top-left corner on the mouse. */\n pointer?: 'arrow' | 'dot' | (string & {});\n /** Any CSS color for the pointer, its pulsing halo and the click ripple. Default `#ff3d71`. */\n color?: string;\n}\n\n/**\n * Draw a mouse pointer that follows Playwright's mouse, since recorded videos don't show one.\n * It pulses so the eye finds it, glides between points, follows focus (so `fill()` steps show where\n * typing lands), and drops a ripple on each click. One pointer is drawn in the top frame and\n * follows the mouse into iframes. Call before the page loads, e.g. before `openScenario`.\n */\nexport async function showCursor(page: Page, { pointer = 'arrow', color = '#ff3d71' }: CursorOptions = {}) {\n await page.addInitScript(({ pointer, color }) => {\n type Pointer = { type: 'move' | 'down' | 'up'; x: number; y: number; };\n\n // Only the top frame draws. Other frames pass their pointer to their parent, which adds the\n // iframe's place, so a page with iframes shows one cursor.\n let show = (p: Pointer) => parent.postMessage({ executableStoriesShowcaseCursor: p }, '*');\n\n // A click focuses on mousedown, or for a <label> in the click after mouseup; the mouse is already there.\n let clicking = false;\n\n addEventListener('mousemove', (e) => show({ type: 'move', x: e.clientX, y: e.clientY }), true);\n addEventListener('mousedown', (e) => {\n clicking = true;\n show({ type: 'down', x: e.clientX, y: e.clientY });\n }, true);\n addEventListener('mouseup', (e) => {\n show({ type: 'up', x: e.clientX, y: e.clientY });\n setTimeout(() => (clicking = false));\n }, true);\n addEventListener('keydown', () => (clicking = false), true);\n // fill() and keyboard steps focus a field without moving the mouse. An iframe gets focus when\n // something inside it does; that frame reports where.\n addEventListener('focusin', (e) => {\n if (clicking || !(e.target instanceof Element) || e.target instanceof HTMLIFrameElement) return;\n const r = e.target.getBoundingClientRect();\n show({ type: 'move', x: r.left + Math.min(24, r.width / 2), y: r.top + r.height / 2 });\n }, true);\n addEventListener('message', (e) => {\n const p: Pointer | undefined = e.data?.executableStoriesShowcaseCursor;\n const frame = [...document.querySelectorAll('iframe')].find((f) => f.contentWindow === e.source);\n\n if (!p || !frame) return;\n\n // The measured box over the layout size gives the iframe's CSS scale, which applies to its\n // border, padding and the point itself.\n const box = frame.getBoundingClientRect();\n const style = getComputedStyle(frame);\n const scaleX = box.width / frame.offsetWidth || 1;\n const scaleY = box.height / frame.offsetHeight || 1;\n\n show({\n ...p,\n x: box.left + (frame.clientLeft + parseFloat(style.paddingLeft) + p.x) * scaleX,\n y: box.top + (frame.clientTop + parseFloat(style.paddingTop) + p.y) * scaleY,\n });\n });\n\n if (window !== window.top) return;\n\n const style = document.createElement('style');\n style.textContent = `\n .es-cursor, .es-ripple { position: fixed; left: 0; top: 0; pointer-events: none; z-index: 2147483647; }\n .es-cursor { transition: transform .35s cubic-bezier(.2,.7,.3,1); }\n .es-cursor > * { position: absolute; left: 0; top: 0; }\n .es-cursor > :last-child { transition: scale .1s; transform-origin: 0 0; }\n .es-cursor[data-down] > :last-child { scale: .8; }\n .es-halo {\n left: -18px; top: -18px; width: 36px; height: 36px; border-radius: 50%; background: ${color};\n animation: es-cursor-halo 1.2s ease-out infinite;\n }\n .es-ripple {\n width: 44px; height: 44px; margin: -22px 0 0 -22px; border-radius: 50%; box-sizing: border-box;\n border: 4px solid ${color}; animation: es-cursor-ripple .6s ease-out forwards;\n }\n @keyframes es-cursor-halo { from { opacity: .8; scale: .4 } to { opacity: 0; scale: 1.6 } }\n @keyframes es-cursor-ripple { from { opacity: .9; scale: .3 } to { opacity: 0; scale: 2.6 } }`;\n\n const shapes: Record<string, string> = {\n arrow: `<svg width=\"24\" height=\"28\" viewBox=\"-1 -1 18 26\" style=\"display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.5))\">`\n + `<path d=\"M0 0V20L5.5 15 9 23 12 21.5 8.6 14H16Z\" fill=\"${color}\" stroke=\"#fff\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/></svg>`,\n dot: `<div style=\"width:22px;height:22px;margin:-11px 0 0 -11px;border-radius:50%;background:${color};border:3px solid #fff;box-sizing:border-box\"></div>`,\n };\n\n // A zero-size box on the mouse: the halo centres on it, the pointer hangs from it.\n const cursor = document.createElement('div');\n cursor.className = 'es-cursor';\n cursor.setAttribute('aria-hidden', 'true');\n cursor.innerHTML = `<div class=\"es-halo\"></div><div>${shapes[pointer] ?? pointer}</div>`;\n // Start mid-screen so the first glide to a target shows in the video.\n cursor.style.transform = `translate(${innerWidth / 2}px,${innerHeight / 2}px)`;\n\n show = ({ type, x, y }) => {\n if (type === 'up') {\n delete cursor.dataset.down;\n\n return;\n }\n\n cursor.style.transform = `translate(${x}px,${y}px)`;\n\n if (type === 'move') return;\n\n cursor.dataset.down = '';\n const ripple = document.createElement('div');\n ripple.className = 'es-ripple';\n ripple.setAttribute('aria-hidden', 'true');\n ripple.style.left = `${x}px`;\n ripple.style.top = `${y}px`;\n ripple.addEventListener('animationend', () => ripple.remove());\n document.documentElement.append(ripple);\n };\n\n const attach = () => document.documentElement.append(style, cursor);\n\n if (document.documentElement) attach();\n else addEventListener('DOMContentLoaded', attach);\n }, { pointer, color });\n}\n"],"mappings":"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","names":["pointer","color","style"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executable-stories-showcase",
3
- "version": "0.1.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": "Apache-2.0",
6
6
  "repository": {