@reticular/speakable 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -2
- package/dist/browser/index.d.ts +58 -0
- package/dist/browser/index.js +3978 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/chunk-WYFXGXYU.js +87 -0
- package/dist/chunk-WYFXGXYU.js.map +1 -0
- package/dist/cli.js +40 -19
- package/dist/cli.js.map +1 -1
- package/dist/harness/index.d.ts +57 -0
- package/dist/harness/index.js +176 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/index.js +44 -22
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +40 -19
- package/dist/mcp.js.map +1 -1
- package/dist/speakable-browser.global.js +10 -0
- package/dist/speakable-browser.global.js.map +1 -0
- package/dist/storybook/dist/chunk-3SVI6IQE.js +86 -0
- package/dist/storybook/dist/chunk-JSBRDJBE.js +30 -0
- package/dist/storybook/dist/chunk-QVJRQZT5.js +16 -0
- package/dist/storybook/dist/index.js +17 -0
- package/dist/storybook/dist/manager.js +701 -0
- package/dist/storybook/dist/preset.js +38 -0
- package/dist/storybook/dist/preview.js +4199 -0
- package/dist/storybook/dist/upgrade-5GL42JH4.js +7 -0
- package/dist/types-Blh0EnZF.d.ts +213 -0
- package/dist/upgrade-53M5VYGH.js +7 -0
- package/dist/upgrade-53M5VYGH.js.map +1 -0
- package/package.json +28 -3
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { I as InteractionSequence, a as AnalysisResult, A as AccessibilityTimeline } from '../types-Blh0EnZF.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared message protocol for the iframe harness.
|
|
5
|
+
*
|
|
6
|
+
* The parent-page controller and the in-iframe agent exchange these
|
|
7
|
+
* request/response envelopes over `postMessage`. Every request carries a
|
|
8
|
+
* unique `id` so responses can be correlated.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Serializable subset of CaptureOptions safe to send over postMessage. */
|
|
12
|
+
interface SerializableCaptureOptions {
|
|
13
|
+
componentName: string;
|
|
14
|
+
storyName?: string;
|
|
15
|
+
sequence: InteractionSequence;
|
|
16
|
+
settlePeriod?: number;
|
|
17
|
+
awaitUpgrade?: boolean;
|
|
18
|
+
upgradeTimeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Iframe harness — parent-page controller.
|
|
23
|
+
*
|
|
24
|
+
* Runs in a parent page, mounts a component (URL or HTML string) into an
|
|
25
|
+
* iframe, injects the browser bundle, and drives analysis/capture over
|
|
26
|
+
* `postMessage`. This is the Storybook-independent runtime testing surface.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
interface HarnessOptions {
|
|
30
|
+
/** An existing iframe to drive, or a container to create one inside. */
|
|
31
|
+
target: HTMLIFrameElement | {
|
|
32
|
+
container: HTMLElement;
|
|
33
|
+
};
|
|
34
|
+
/** Load + inject + request timeout in ms (default: 10000). */
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
/** Origins allowed for postMessage. Defaults to same-origin only. */
|
|
37
|
+
allowedOrigins?: string[];
|
|
38
|
+
/**
|
|
39
|
+
* URL to the injectable IIFE bundle. When omitted, the harness assumes the
|
|
40
|
+
* bundle is already present in the iframe (e.g. bundled into the page).
|
|
41
|
+
*/
|
|
42
|
+
bundleUrl?: string;
|
|
43
|
+
}
|
|
44
|
+
type LoadSource = {
|
|
45
|
+
url: string;
|
|
46
|
+
} | {
|
|
47
|
+
html: string;
|
|
48
|
+
};
|
|
49
|
+
interface Harness {
|
|
50
|
+
load(source: LoadSource): Promise<void>;
|
|
51
|
+
analyze(selector?: string): Promise<AnalysisResult>;
|
|
52
|
+
captureTimeline(options: SerializableCaptureOptions): Promise<AccessibilityTimeline>;
|
|
53
|
+
destroy(): void;
|
|
54
|
+
}
|
|
55
|
+
declare function createHarness(options: HarnessOptions): Harness;
|
|
56
|
+
|
|
57
|
+
export { type Harness, type HarnessOptions, type LoadSource, type SerializableCaptureOptions, createHarness };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// src/harness/protocol.ts
|
|
2
|
+
var HARNESS_CHANNEL = "speakable-harness";
|
|
3
|
+
function isHarnessResponse(data) {
|
|
4
|
+
if (!data || typeof data !== "object") return false;
|
|
5
|
+
const d = data;
|
|
6
|
+
return d.channel === HARNESS_CHANNEL && typeof d.id === "string" && typeof d.ok === "boolean";
|
|
7
|
+
}
|
|
8
|
+
function isHarnessReady(data) {
|
|
9
|
+
if (!data || typeof data !== "object") return false;
|
|
10
|
+
const d = data;
|
|
11
|
+
return d.channel === HARNESS_CHANNEL && d.kind === "ready";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/harness/index.ts
|
|
15
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
16
|
+
var idCounter = 0;
|
|
17
|
+
function nextId() {
|
|
18
|
+
idCounter += 1;
|
|
19
|
+
return `req-${Date.now()}-${idCounter}`;
|
|
20
|
+
}
|
|
21
|
+
function isCrossOrigin(url) {
|
|
22
|
+
try {
|
|
23
|
+
const resolved = new URL(url, window.location.href);
|
|
24
|
+
return resolved.origin !== window.location.origin;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function createHarness(options) {
|
|
30
|
+
const { timeoutMs = DEFAULT_TIMEOUT, allowedOrigins, bundleUrl } = options;
|
|
31
|
+
let ownsIframe = false;
|
|
32
|
+
let iframe;
|
|
33
|
+
if (options.target instanceof HTMLIFrameElement) {
|
|
34
|
+
iframe = options.target;
|
|
35
|
+
} else {
|
|
36
|
+
iframe = document.createElement("iframe");
|
|
37
|
+
iframe.setAttribute("title", "speakable-harness");
|
|
38
|
+
options.target.container.appendChild(iframe);
|
|
39
|
+
ownsIframe = true;
|
|
40
|
+
}
|
|
41
|
+
const pending = /* @__PURE__ */ new Map();
|
|
42
|
+
function onMessage(event) {
|
|
43
|
+
if (event.source !== iframe.contentWindow) return;
|
|
44
|
+
if (allowedOrigins && allowedOrigins.length > 0 && !allowedOrigins.includes("*")) {
|
|
45
|
+
if (!allowedOrigins.includes(event.origin)) return;
|
|
46
|
+
}
|
|
47
|
+
const data = event.data;
|
|
48
|
+
if (!isHarnessResponse(data)) return;
|
|
49
|
+
const entry = pending.get(data.id);
|
|
50
|
+
if (!entry) return;
|
|
51
|
+
clearTimeout(entry.timer);
|
|
52
|
+
pending.delete(data.id);
|
|
53
|
+
entry.resolve(data);
|
|
54
|
+
}
|
|
55
|
+
window.addEventListener("message", onMessage);
|
|
56
|
+
function targetOrigin() {
|
|
57
|
+
if (allowedOrigins && allowedOrigins.includes("*")) return "*";
|
|
58
|
+
try {
|
|
59
|
+
return iframe.contentWindow ? new URL(iframe.src || window.location.href).origin : "*";
|
|
60
|
+
} catch {
|
|
61
|
+
return "*";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function request(payload) {
|
|
65
|
+
const id = nextId();
|
|
66
|
+
const win = iframe.contentWindow;
|
|
67
|
+
if (!win) {
|
|
68
|
+
return Promise.reject(new Error("Harness iframe has no content window; call load() first."));
|
|
69
|
+
}
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
pending.delete(id);
|
|
73
|
+
reject(new Error(`Harness request "${payload.kind}" timed out after ${timeoutMs}ms`));
|
|
74
|
+
}, timeoutMs);
|
|
75
|
+
pending.set(id, { resolve, reject, timer });
|
|
76
|
+
win.postMessage({ channel: HARNESS_CHANNEL, id, ...payload }, targetOrigin());
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function waitForReady() {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const timer = setTimeout(() => {
|
|
82
|
+
window.removeEventListener("message", onReady);
|
|
83
|
+
reject(new Error(`Harness bundle did not signal ready within ${timeoutMs}ms`));
|
|
84
|
+
}, timeoutMs);
|
|
85
|
+
function onReady(event) {
|
|
86
|
+
if (event.source !== iframe.contentWindow) return;
|
|
87
|
+
if (!isHarnessReady(event.data)) return;
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
window.removeEventListener("message", onReady);
|
|
90
|
+
resolve();
|
|
91
|
+
}
|
|
92
|
+
window.addEventListener("message", onReady);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function waitForLoad() {
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
iframe.removeEventListener("load", onLoad);
|
|
99
|
+
reject(new Error(`Harness iframe failed to load within ${timeoutMs}ms`));
|
|
100
|
+
}, timeoutMs);
|
|
101
|
+
function onLoad() {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
iframe.removeEventListener("load", onLoad);
|
|
104
|
+
resolve();
|
|
105
|
+
}
|
|
106
|
+
iframe.addEventListener("load", onLoad);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function injectBundle() {
|
|
110
|
+
const doc = iframe.contentDocument;
|
|
111
|
+
if (!doc) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"Cannot access iframe document for bundle injection. Cross-origin content is not supported; use { html } (srcdoc) or a same-origin URL."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const script = doc.createElement("script");
|
|
117
|
+
if (bundleUrl) {
|
|
118
|
+
script.src = bundleUrl;
|
|
119
|
+
} else {
|
|
120
|
+
const injected = iframe.contentWindow?.__SPEAKABLE__;
|
|
121
|
+
if (!injected) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"No bundleUrl provided and the iframe does not already include the Speakable bundle. Pass options.bundleUrl."
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
doc.head?.appendChild(script) ?? doc.documentElement.appendChild(script);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
async load(source) {
|
|
132
|
+
if ("url" in source && isCrossOrigin(source.url)) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Cannot inject into cross-origin URL "${source.url}". Use { html } (srcdoc) or a same-origin URL.`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const loaded = waitForLoad();
|
|
138
|
+
if ("url" in source) {
|
|
139
|
+
iframe.src = source.url;
|
|
140
|
+
} else {
|
|
141
|
+
iframe.srcdoc = source.html;
|
|
142
|
+
}
|
|
143
|
+
await loaded;
|
|
144
|
+
const ready = waitForReady();
|
|
145
|
+
injectBundle();
|
|
146
|
+
const alreadyPresent = iframe.contentWindow?.__SPEAKABLE__;
|
|
147
|
+
if (alreadyPresent) return;
|
|
148
|
+
await ready;
|
|
149
|
+
},
|
|
150
|
+
async analyze(selector) {
|
|
151
|
+
const res = await request({ kind: "analyze", ...selector && { selector } });
|
|
152
|
+
if (res.ok && res.kind === "analyze") return res.result;
|
|
153
|
+
throw new Error(res.ok ? "Unexpected response kind" : res.error);
|
|
154
|
+
},
|
|
155
|
+
async captureTimeline(opts) {
|
|
156
|
+
const res = await request({ kind: "capture", options: opts });
|
|
157
|
+
if (res.ok && res.kind === "capture") return res.result;
|
|
158
|
+
throw new Error(res.ok ? "Unexpected response kind" : res.error);
|
|
159
|
+
},
|
|
160
|
+
destroy() {
|
|
161
|
+
window.removeEventListener("message", onMessage);
|
|
162
|
+
for (const [, entry] of pending) {
|
|
163
|
+
clearTimeout(entry.timer);
|
|
164
|
+
entry.reject(new Error("Harness destroyed"));
|
|
165
|
+
}
|
|
166
|
+
pending.clear();
|
|
167
|
+
if (ownsIframe && iframe.parentNode) {
|
|
168
|
+
iframe.parentNode.removeChild(iframe);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export {
|
|
174
|
+
createHarness
|
|
175
|
+
};
|
|
176
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/harness/protocol.ts","../../src/harness/index.ts"],"sourcesContent":["/**\n * Shared message protocol for the iframe harness.\n *\n * The parent-page controller and the in-iframe agent exchange these\n * request/response envelopes over `postMessage`. Every request carries a\n * unique `id` so responses can be correlated.\n */\n\nimport type { AnalysisResult } from '../browser/analyze.js';\nimport type { AccessibilityTimeline, InteractionSequence } from '../runtime/types.js';\n\n/** Namespace tag stamped on every harness message to avoid collisions. */\nexport const HARNESS_CHANNEL = 'speakable-harness';\n\n/** Serializable subset of CaptureOptions safe to send over postMessage. */\nexport interface SerializableCaptureOptions {\n componentName: string;\n storyName?: string;\n sequence: InteractionSequence;\n settlePeriod?: number;\n awaitUpgrade?: boolean;\n upgradeTimeoutMs?: number;\n}\n\nexport type HarnessRequest =\n | { channel: typeof HARNESS_CHANNEL; id: string; kind: 'analyze'; selector?: string }\n | { channel: typeof HARNESS_CHANNEL; id: string; kind: 'capture'; options: SerializableCaptureOptions };\n\nexport type HarnessResponse =\n | { channel: typeof HARNESS_CHANNEL; id: string; ok: true; kind: 'analyze'; result: AnalysisResult }\n | { channel: typeof HARNESS_CHANNEL; id: string; ok: true; kind: 'capture'; result: AccessibilityTimeline }\n | { channel: typeof HARNESS_CHANNEL; id: string; ok: false; error: string };\n\n/** Sent by the agent once it has installed its message listener. */\nexport interface HarnessReadyMessage {\n channel: typeof HARNESS_CHANNEL;\n kind: 'ready';\n}\n\nexport function isHarnessRequest(data: unknown): data is HarnessRequest {\n if (!data || typeof data !== 'object') return false;\n const d = data as Record<string, unknown>;\n return d.channel === HARNESS_CHANNEL && typeof d.id === 'string' &&\n (d.kind === 'analyze' || d.kind === 'capture');\n}\n\nexport function isHarnessResponse(data: unknown): data is HarnessResponse {\n if (!data || typeof data !== 'object') return false;\n const d = data as Record<string, unknown>;\n return d.channel === HARNESS_CHANNEL && typeof d.id === 'string' && typeof d.ok === 'boolean';\n}\n\nexport function isHarnessReady(data: unknown): data is HarnessReadyMessage {\n if (!data || typeof data !== 'object') return false;\n const d = data as Record<string, unknown>;\n return d.channel === HARNESS_CHANNEL && d.kind === 'ready';\n}\n","/**\n * Iframe harness — parent-page controller.\n *\n * Runs in a parent page, mounts a component (URL or HTML string) into an\n * iframe, injects the browser bundle, and drives analysis/capture over\n * `postMessage`. This is the Storybook-independent runtime testing surface.\n */\n\nimport type { AnalysisResult } from '../browser/analyze.js';\nimport type { AccessibilityTimeline } from '../runtime/types.js';\nimport {\n HARNESS_CHANNEL,\n isHarnessReady,\n isHarnessResponse,\n type HarnessResponse,\n type SerializableCaptureOptions,\n} from './protocol.js';\n\nexport interface HarnessOptions {\n /** An existing iframe to drive, or a container to create one inside. */\n target: HTMLIFrameElement | { container: HTMLElement };\n /** Load + inject + request timeout in ms (default: 10000). */\n timeoutMs?: number;\n /** Origins allowed for postMessage. Defaults to same-origin only. */\n allowedOrigins?: string[];\n /**\n * URL to the injectable IIFE bundle. When omitted, the harness assumes the\n * bundle is already present in the iframe (e.g. bundled into the page).\n */\n bundleUrl?: string;\n}\n\nexport type LoadSource = { url: string } | { html: string };\n\nexport interface Harness {\n load(source: LoadSource): Promise<void>;\n analyze(selector?: string): Promise<AnalysisResult>;\n captureTimeline(options: SerializableCaptureOptions): Promise<AccessibilityTimeline>;\n destroy(): void;\n}\n\nconst DEFAULT_TIMEOUT = 10000;\n\nlet idCounter = 0;\nfunction nextId(): string {\n idCounter += 1;\n return `req-${Date.now()}-${idCounter}`;\n}\n\nfunction isCrossOrigin(url: string): boolean {\n try {\n const resolved = new URL(url, window.location.href);\n return resolved.origin !== window.location.origin;\n } catch {\n // Relative or malformed — treat as same-origin.\n return false;\n }\n}\n\nexport function createHarness(options: HarnessOptions): Harness {\n const { timeoutMs = DEFAULT_TIMEOUT, allowedOrigins, bundleUrl } = options;\n\n // Resolve or create the iframe.\n let ownsIframe = false;\n let iframe: HTMLIFrameElement;\n if (options.target instanceof HTMLIFrameElement) {\n iframe = options.target;\n } else {\n iframe = document.createElement('iframe');\n iframe.setAttribute('title', 'speakable-harness');\n options.target.container.appendChild(iframe);\n ownsIframe = true;\n }\n\n const pending = new Map<\n string,\n { resolve: (r: HarnessResponse) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }\n >();\n\n function onMessage(event: MessageEvent) {\n // Only accept messages originating from our iframe's window.\n if (event.source !== iframe.contentWindow) return;\n if (allowedOrigins && allowedOrigins.length > 0 && !allowedOrigins.includes('*')) {\n if (!allowedOrigins.includes(event.origin)) return;\n }\n const data = event.data;\n if (!isHarnessResponse(data)) return;\n const entry = pending.get(data.id);\n if (!entry) return;\n clearTimeout(entry.timer);\n pending.delete(data.id);\n entry.resolve(data);\n }\n\n window.addEventListener('message', onMessage);\n\n function targetOrigin(): string {\n if (allowedOrigins && allowedOrigins.includes('*')) return '*';\n try {\n return iframe.contentWindow ? new URL(iframe.src || window.location.href).origin : '*';\n } catch {\n return '*';\n }\n }\n\n function request(payload: Record<string, unknown>): Promise<HarnessResponse> {\n const id = nextId();\n const win = iframe.contentWindow;\n if (!win) {\n return Promise.reject(new Error('Harness iframe has no content window; call load() first.'));\n }\n return new Promise<HarnessResponse>((resolve, reject) => {\n const timer = setTimeout(() => {\n pending.delete(id);\n reject(new Error(`Harness request \"${payload.kind}\" timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n pending.set(id, { resolve, reject, timer });\n win.postMessage({ channel: HARNESS_CHANNEL, id, ...payload }, targetOrigin());\n });\n }\n\n function waitForReady(): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n window.removeEventListener('message', onReady);\n reject(new Error(`Harness bundle did not signal ready within ${timeoutMs}ms`));\n }, timeoutMs);\n function onReady(event: MessageEvent) {\n if (event.source !== iframe.contentWindow) return;\n if (!isHarnessReady(event.data)) return;\n clearTimeout(timer);\n window.removeEventListener('message', onReady);\n resolve();\n }\n window.addEventListener('message', onReady);\n });\n }\n\n function waitForLoad(): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n iframe.removeEventListener('load', onLoad);\n reject(new Error(`Harness iframe failed to load within ${timeoutMs}ms`));\n }, timeoutMs);\n function onLoad() {\n clearTimeout(timer);\n iframe.removeEventListener('load', onLoad);\n resolve();\n }\n iframe.addEventListener('load', onLoad);\n });\n }\n\n function injectBundle(): void {\n const doc = iframe.contentDocument;\n if (!doc) {\n throw new Error(\n 'Cannot access iframe document for bundle injection. Cross-origin content is not supported; use { html } (srcdoc) or a same-origin URL.'\n );\n }\n const script = doc.createElement('script');\n if (bundleUrl) {\n script.src = bundleUrl;\n } else {\n // No bundle URL provided and no bundle detected — surface a clear error\n // rather than hanging on waitForReady.\n const injected = (iframe.contentWindow as unknown as { __SPEAKABLE__?: unknown })?.__SPEAKABLE__;\n if (!injected) {\n throw new Error(\n 'No bundleUrl provided and the iframe does not already include the Speakable bundle. Pass options.bundleUrl.'\n );\n }\n return;\n }\n doc.head?.appendChild(script) ?? doc.documentElement.appendChild(script);\n }\n\n return {\n async load(source: LoadSource): Promise<void> {\n // Validate before starting any timers so we don't leak a pending\n // load-wait rejection when we bail out synchronously.\n if ('url' in source && isCrossOrigin(source.url)) {\n throw new Error(\n `Cannot inject into cross-origin URL \"${source.url}\". Use { html } (srcdoc) or a same-origin URL.`\n );\n }\n\n const loaded = waitForLoad();\n if ('url' in source) {\n iframe.src = source.url;\n } else {\n iframe.srcdoc = source.html;\n }\n await loaded;\n\n const ready = waitForReady();\n injectBundle();\n // If the bundle was already present, it may have signaled ready before we\n // attached the listener; fall back to a short probe.\n const alreadyPresent = (iframe.contentWindow as unknown as { __SPEAKABLE__?: unknown })?.__SPEAKABLE__;\n if (alreadyPresent) return;\n await ready;\n },\n\n async analyze(selector?: string): Promise<AnalysisResult> {\n const res = await request({ kind: 'analyze', ...(selector && { selector }) });\n if (res.ok && res.kind === 'analyze') return res.result;\n throw new Error(res.ok ? 'Unexpected response kind' : res.error);\n },\n\n async captureTimeline(opts: SerializableCaptureOptions): Promise<AccessibilityTimeline> {\n const res = await request({ kind: 'capture', options: opts });\n if (res.ok && res.kind === 'capture') return res.result;\n throw new Error(res.ok ? 'Unexpected response kind' : res.error);\n },\n\n destroy(): void {\n window.removeEventListener('message', onMessage);\n for (const [, entry] of pending) {\n clearTimeout(entry.timer);\n entry.reject(new Error('Harness destroyed'));\n }\n pending.clear();\n if (ownsIframe && iframe.parentNode) {\n iframe.parentNode.removeChild(iframe);\n }\n },\n };\n}\n\nexport type { SerializableCaptureOptions } from './protocol.js';\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -2521,17 +2521,54 @@ function processChildNode(node, children, warnings) {
|
|
|
2521
2521
|
}
|
|
2522
2522
|
}
|
|
2523
2523
|
function collectChildrenFromRolelessElement(element, warnings) {
|
|
2524
|
+
return collectAccessibleChildren(element, warnings);
|
|
2525
|
+
}
|
|
2526
|
+
function collectAccessibleChildren(element, warnings) {
|
|
2524
2527
|
const children = [];
|
|
2525
2528
|
if (element.shadowRoot) {
|
|
2526
2529
|
for (const child of Array.from(element.shadowRoot.childNodes)) {
|
|
2527
|
-
|
|
2530
|
+
processShadowNode(child, children, warnings);
|
|
2528
2531
|
}
|
|
2532
|
+
return children;
|
|
2533
|
+
}
|
|
2534
|
+
if (element.localName && element.localName.includes("-") && element.childNodes.length === 0) {
|
|
2535
|
+
warnings.push({
|
|
2536
|
+
message: `Custom element <${element.localName}> has no accessible children and may have a closed shadow root; its content could not be analyzed.`,
|
|
2537
|
+
element
|
|
2538
|
+
});
|
|
2529
2539
|
}
|
|
2530
2540
|
for (const child of Array.from(element.childNodes)) {
|
|
2531
2541
|
processChildNode(child, children, warnings);
|
|
2532
2542
|
}
|
|
2533
2543
|
return children;
|
|
2534
2544
|
}
|
|
2545
|
+
function processShadowNode(node, children, warnings) {
|
|
2546
|
+
if (node.nodeType === 1) {
|
|
2547
|
+
const element = node;
|
|
2548
|
+
if (element.localName === "slot") {
|
|
2549
|
+
const slot = element;
|
|
2550
|
+
let assigned = [];
|
|
2551
|
+
if (typeof slot.assignedNodes === "function") {
|
|
2552
|
+
try {
|
|
2553
|
+
assigned = Array.from(slot.assignedNodes({ flatten: true }));
|
|
2554
|
+
} catch {
|
|
2555
|
+
assigned = [];
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
if (assigned.length > 0) {
|
|
2559
|
+
for (const assignedNode of assigned) {
|
|
2560
|
+
processChildNode(assignedNode, children, warnings);
|
|
2561
|
+
}
|
|
2562
|
+
} else {
|
|
2563
|
+
for (const child of Array.from(slot.childNodes)) {
|
|
2564
|
+
processShadowNode(child, children, warnings);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
processChildNode(node, children, warnings);
|
|
2571
|
+
}
|
|
2535
2572
|
function buildAccessibilityTree(rootElement, sourceHash) {
|
|
2536
2573
|
const warnings = [];
|
|
2537
2574
|
if (isHidden(rootElement)) {
|
|
@@ -2624,15 +2661,7 @@ function buildNodeRecursive(element, warnings) {
|
|
|
2624
2661
|
element: w.element
|
|
2625
2662
|
})));
|
|
2626
2663
|
const focusInfo = extractFocusInfo(element);
|
|
2627
|
-
const children =
|
|
2628
|
-
if (element.shadowRoot) {
|
|
2629
|
-
for (const child of Array.from(element.shadowRoot.childNodes)) {
|
|
2630
|
-
processChildNode(child, children, warnings);
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
2633
|
-
for (const child of Array.from(element.childNodes)) {
|
|
2634
|
-
processChildNode(child, children, warnings);
|
|
2635
|
-
}
|
|
2664
|
+
const children = collectAccessibleChildren(element, warnings);
|
|
2636
2665
|
const node = {
|
|
2637
2666
|
role: roleResult.role,
|
|
2638
2667
|
name: nameResult.name,
|
|
@@ -2645,15 +2674,7 @@ function buildNodeRecursive(element, warnings) {
|
|
|
2645
2674
|
return node;
|
|
2646
2675
|
}
|
|
2647
2676
|
function createGenericContainer(rootElement, warnings) {
|
|
2648
|
-
const children =
|
|
2649
|
-
if (rootElement.shadowRoot) {
|
|
2650
|
-
for (const child of Array.from(rootElement.shadowRoot.childNodes)) {
|
|
2651
|
-
processChildNode(child, children, warnings);
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
for (const child of Array.from(rootElement.childNodes)) {
|
|
2655
|
-
processChildNode(child, children, warnings);
|
|
2656
|
-
}
|
|
2677
|
+
const children = collectAccessibleChildren(rootElement, warnings);
|
|
2657
2678
|
return {
|
|
2658
2679
|
role: "generic",
|
|
2659
2680
|
name: "",
|
|
@@ -4365,8 +4386,10 @@ function createHeuristicAnalyzer(config) {
|
|
|
4365
4386
|
};
|
|
4366
4387
|
}
|
|
4367
4388
|
|
|
4389
|
+
// src/version.ts
|
|
4390
|
+
var SPEAKABLE_VERSION = "1.7.0" ? "1.7.0" : "dev";
|
|
4391
|
+
|
|
4368
4392
|
// src/runtime/timeline-generator.ts
|
|
4369
|
-
var SPEAKABLE_VERSION = "1.3.0";
|
|
4370
4393
|
var TIMELINE_FORMAT_VERSION = "1.0";
|
|
4371
4394
|
var DEFAULT_SETTLE_PERIOD = 500;
|
|
4372
4395
|
var DEFAULT_LOAD_TIMEOUT = 1e4;
|
|
@@ -5123,7 +5146,6 @@ function classifyModified(entry) {
|
|
|
5123
5146
|
// src/runtime/baseline-storage.ts
|
|
5124
5147
|
import { readFile, writeFile, mkdir, access } from "fs/promises";
|
|
5125
5148
|
import { join, dirname } from "path";
|
|
5126
|
-
var SPEAKABLE_VERSION2 = "1.3.0";
|
|
5127
5149
|
function createBaselineStorage(baseDir) {
|
|
5128
5150
|
function sanitize(name) {
|
|
5129
5151
|
return name.toLowerCase().replace(/[/\\]/g, "-").replace(/[^a-z0-9._-]/g, "-");
|
|
@@ -5143,7 +5165,7 @@ function createBaselineStorage(baseDir) {
|
|
|
5143
5165
|
const baselineFile = {
|
|
5144
5166
|
baseline: {
|
|
5145
5167
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5146
|
-
speakableVersion:
|
|
5168
|
+
speakableVersion: SPEAKABLE_VERSION,
|
|
5147
5169
|
storybookUrl: meta?.storybookUrl,
|
|
5148
5170
|
interactionSequence: timeline.interactionSequence
|
|
5149
5171
|
},
|