@reticular/speakable 1.6.0 → 1.8.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 +4100 -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 +164 -21
- 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.d.ts +1 -1
- package/dist/index.js +168 -24
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +164 -21
- 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-CL3DJBD7.js → chunk-JSBRDJBE.js} +1 -11
- package/dist/storybook/dist/chunk-QVJRQZT5.js +16 -0
- package/dist/storybook/dist/index.js +6 -1
- package/dist/storybook/dist/manager.js +404 -5
- package/dist/storybook/dist/preview.js +2241 -49
- 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 +13 -1
|
@@ -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.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ interface ModelVersion {
|
|
|
16
16
|
* Accessible role following ARIA specification.
|
|
17
17
|
* V1 supports common interactive and structural roles.
|
|
18
18
|
*/
|
|
19
|
-
type AccessibleRole = 'button' | 'link' | 'heading' | 'textbox' | 'checkbox' | 'radio' | 'combobox' | 'listbox' | 'option' | 'list' | 'listitem' | 'article' | 'generic' | 'navigation' | 'main' | 'banner' | 'contentinfo' | 'region' | 'img' | 'complementary' | 'form' | 'search' | 'paragraph' | 'blockquote' | 'code' | 'staticText' | 'table' | 'row' | 'cell' | 'columnheader' | 'rowheader' | 'term' | 'definition' | 'figure' | 'caption' | 'group' | 'dialog' | 'meter' | 'progressbar' | 'status' | 'document' | 'application' | 'separator';
|
|
19
|
+
type AccessibleRole = 'button' | 'link' | 'heading' | 'textbox' | 'checkbox' | 'radio' | 'combobox' | 'listbox' | 'option' | 'list' | 'listitem' | 'article' | 'generic' | 'navigation' | 'main' | 'banner' | 'contentinfo' | 'region' | 'img' | 'complementary' | 'form' | 'search' | 'paragraph' | 'blockquote' | 'code' | 'staticText' | 'table' | 'row' | 'cell' | 'columnheader' | 'rowheader' | 'term' | 'definition' | 'figure' | 'caption' | 'group' | 'dialog' | 'meter' | 'progressbar' | 'status' | 'switch' | 'slider' | 'spinbutton' | 'tab' | 'tablist' | 'tabpanel' | 'document' | 'application' | 'separator';
|
|
20
20
|
/**
|
|
21
21
|
* Accessible states and properties.
|
|
22
22
|
* Only includes properties relevant to the element's role.
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,12 @@ var SUPPORTED_ROLES = [
|
|
|
48
48
|
"meter",
|
|
49
49
|
"progressbar",
|
|
50
50
|
"status",
|
|
51
|
+
"switch",
|
|
52
|
+
"slider",
|
|
53
|
+
"spinbutton",
|
|
54
|
+
"tab",
|
|
55
|
+
"tablist",
|
|
56
|
+
"tabpanel",
|
|
51
57
|
"document",
|
|
52
58
|
"application",
|
|
53
59
|
"separator"
|
|
@@ -968,8 +974,45 @@ function renderNodeVoiceOver(node, announcements, c) {
|
|
|
968
974
|
renderNodeVoiceOver(child, announcements, c);
|
|
969
975
|
}
|
|
970
976
|
}
|
|
977
|
+
function isValueFirstWidget(role) {
|
|
978
|
+
return role === "slider" || role === "spinbutton" || role === "combobox";
|
|
979
|
+
}
|
|
980
|
+
function isValueBeforeRoleWidget(role) {
|
|
981
|
+
return role === "progressbar" || role === "meter";
|
|
982
|
+
}
|
|
983
|
+
function isSilentContainerVoiceOver(node) {
|
|
984
|
+
if (node.role !== "list" && node.role !== "status") return false;
|
|
985
|
+
if (node.name) return false;
|
|
986
|
+
return node.children.length > 0;
|
|
987
|
+
}
|
|
971
988
|
function formatNodeVoiceOver(node, c) {
|
|
989
|
+
if (isSilentContainerVoiceOver(node)) {
|
|
990
|
+
return "";
|
|
991
|
+
}
|
|
972
992
|
const parts = [];
|
|
993
|
+
const valueFirst = isValueFirstWidget(node.role) && node.value !== void 0;
|
|
994
|
+
if (valueFirst) {
|
|
995
|
+
const valueText = node.value.text || String(node.value.current);
|
|
996
|
+
if (valueText) parts.push(valueText);
|
|
997
|
+
if (node.name) parts.push(c.elementName(node.name));
|
|
998
|
+
const roleText = formatRoleVoiceOver(node);
|
|
999
|
+
if (roleText) parts.push(c.roleName(roleText));
|
|
1000
|
+
const stateText2 = formatStatesVoiceOver(node);
|
|
1001
|
+
if (stateText2) parts.push(c.stateName(stateText2));
|
|
1002
|
+
if (node.description) parts.push(c.description(node.description));
|
|
1003
|
+
return parts.join(", ");
|
|
1004
|
+
}
|
|
1005
|
+
if (isValueBeforeRoleWidget(node.role) && node.value !== void 0) {
|
|
1006
|
+
if (node.name) parts.push(c.elementName(node.name));
|
|
1007
|
+
const valueText = node.value.text || String(node.value.current);
|
|
1008
|
+
if (valueText) parts.push(valueText);
|
|
1009
|
+
const roleText = formatRoleVoiceOver(node);
|
|
1010
|
+
if (roleText) parts.push(c.roleName(roleText));
|
|
1011
|
+
const stateText2 = formatStatesVoiceOver(node);
|
|
1012
|
+
if (stateText2) parts.push(c.stateName(stateText2));
|
|
1013
|
+
if (node.description) parts.push(c.description(node.description));
|
|
1014
|
+
return parts.join(", ");
|
|
1015
|
+
}
|
|
973
1016
|
const roleFirst = shouldAnnounceRoleFirst(node.role);
|
|
974
1017
|
if (roleFirst) {
|
|
975
1018
|
const roleText = formatRoleVoiceOver(node);
|
|
@@ -998,6 +1041,9 @@ function formatNodeVoiceOver(node, c) {
|
|
|
998
1041
|
parts.push(valueText);
|
|
999
1042
|
}
|
|
1000
1043
|
}
|
|
1044
|
+
if (node.state.posinset !== void 0 && node.state.setsize !== void 0) {
|
|
1045
|
+
parts.push(`${node.state.posinset} of ${node.state.setsize}`);
|
|
1046
|
+
}
|
|
1001
1047
|
if (node.description) {
|
|
1002
1048
|
parts.push(c.description(node.description));
|
|
1003
1049
|
}
|
|
@@ -1033,7 +1079,7 @@ function formatRoleVoiceOver(node) {
|
|
|
1033
1079
|
case "list":
|
|
1034
1080
|
return "list";
|
|
1035
1081
|
case "listitem":
|
|
1036
|
-
return "
|
|
1082
|
+
return "";
|
|
1037
1083
|
case "navigation":
|
|
1038
1084
|
return "navigation";
|
|
1039
1085
|
case "main":
|
|
@@ -1089,6 +1135,18 @@ function formatRoleVoiceOver(node) {
|
|
|
1089
1135
|
return "progress indicator";
|
|
1090
1136
|
case "status":
|
|
1091
1137
|
return "status";
|
|
1138
|
+
case "switch":
|
|
1139
|
+
return "switch";
|
|
1140
|
+
case "slider":
|
|
1141
|
+
return "slider";
|
|
1142
|
+
case "spinbutton":
|
|
1143
|
+
return "stepper";
|
|
1144
|
+
case "tab":
|
|
1145
|
+
return "tab";
|
|
1146
|
+
case "tablist":
|
|
1147
|
+
return node.name ? "tab group" : "tab group";
|
|
1148
|
+
case "tabpanel":
|
|
1149
|
+
return "tab panel";
|
|
1092
1150
|
case "group":
|
|
1093
1151
|
return node.name ? "group" : "";
|
|
1094
1152
|
case "document":
|
|
@@ -1109,6 +1167,10 @@ function formatStatesVoiceOver(node) {
|
|
|
1109
1167
|
if (node.state.checked !== void 0) {
|
|
1110
1168
|
if (node.state.checked === "mixed") {
|
|
1111
1169
|
states.push("mixed");
|
|
1170
|
+
} else if (node.role === "switch") {
|
|
1171
|
+
states.push(node.state.checked ? "on" : "off");
|
|
1172
|
+
} else if (node.role === "radio") {
|
|
1173
|
+
if (node.state.checked) states.push("selected");
|
|
1112
1174
|
} else {
|
|
1113
1175
|
states.push(node.state.checked ? "checked" : "unchecked");
|
|
1114
1176
|
}
|
|
@@ -1117,10 +1179,10 @@ function formatStatesVoiceOver(node) {
|
|
|
1117
1179
|
if (node.state.pressed === "mixed") {
|
|
1118
1180
|
states.push("mixed");
|
|
1119
1181
|
} else {
|
|
1120
|
-
states.push(node.state.pressed ? "
|
|
1182
|
+
states.push(node.state.pressed ? "selected" : "unselected");
|
|
1121
1183
|
}
|
|
1122
1184
|
}
|
|
1123
|
-
if (node.state.selected !== void 0) {
|
|
1185
|
+
if (node.state.selected !== void 0 && node.role !== "tab") {
|
|
1124
1186
|
states.push(node.state.selected ? "selected" : "unselected");
|
|
1125
1187
|
}
|
|
1126
1188
|
if (node.state.disabled) {
|
|
@@ -2069,7 +2131,10 @@ var INPUT_TYPE_ROLE_MAP = {
|
|
|
2069
2131
|
"search": "textbox",
|
|
2070
2132
|
"tel": "textbox",
|
|
2071
2133
|
"url": "textbox",
|
|
2072
|
-
"number"
|
|
2134
|
+
// Per ARIA, <input type="number"> has an implicit role of spinbutton
|
|
2135
|
+
// (VoiceOver announces it as "stepper").
|
|
2136
|
+
"number": "spinbutton",
|
|
2137
|
+
"range": "slider"
|
|
2073
2138
|
};
|
|
2074
2139
|
function computeRole(element) {
|
|
2075
2140
|
const warnings = [];
|
|
@@ -2102,6 +2167,11 @@ function computeImplicitRole(element, _warnings) {
|
|
|
2102
2167
|
if (tagName === "input") {
|
|
2103
2168
|
return computeInputRole(element);
|
|
2104
2169
|
}
|
|
2170
|
+
if (tagName === "select") {
|
|
2171
|
+
const multiple = element.hasAttribute("multiple");
|
|
2172
|
+
const size = parseInt(element.getAttribute("size") || "0", 10);
|
|
2173
|
+
return multiple || size > 1 ? "listbox" : "combobox";
|
|
2174
|
+
}
|
|
2105
2175
|
if (tagName === "a") {
|
|
2106
2176
|
return element.hasAttribute("href") ? "link" : null;
|
|
2107
2177
|
}
|
|
@@ -2511,6 +2581,7 @@ function processChildNode(node, children, warnings) {
|
|
|
2511
2581
|
if (node.nodeType === 1) {
|
|
2512
2582
|
const element = node;
|
|
2513
2583
|
if (isHidden(element)) return;
|
|
2584
|
+
if (isConsumedLabel(element)) return;
|
|
2514
2585
|
if (!isAccessible(element)) {
|
|
2515
2586
|
const grandchildren = collectChildrenFromRolelessElement(element, warnings);
|
|
2516
2587
|
children.push(...grandchildren);
|
|
@@ -2520,18 +2591,74 @@ function processChildNode(node, children, warnings) {
|
|
|
2520
2591
|
if (childNode) children.push(childNode);
|
|
2521
2592
|
}
|
|
2522
2593
|
}
|
|
2594
|
+
function isConsumedLabel(element) {
|
|
2595
|
+
if (element.tagName.toLowerCase() !== "label") return false;
|
|
2596
|
+
const forId = element.getAttribute("for");
|
|
2597
|
+
if (forId) {
|
|
2598
|
+
const doc = element.ownerDocument;
|
|
2599
|
+
const target = doc?.getElementById(forId);
|
|
2600
|
+
if (target && isLabelableControl(target)) return true;
|
|
2601
|
+
}
|
|
2602
|
+
const wrapped = element.querySelector("input, select, textarea, button, meter, progress");
|
|
2603
|
+
if (wrapped && isLabelableControl(wrapped)) return true;
|
|
2604
|
+
return false;
|
|
2605
|
+
}
|
|
2606
|
+
function isLabelableControl(element) {
|
|
2607
|
+
const tag = element.tagName.toLowerCase();
|
|
2608
|
+
if (tag === "input") {
|
|
2609
|
+
return (element.getAttribute("type") || "text").toLowerCase() !== "hidden";
|
|
2610
|
+
}
|
|
2611
|
+
return tag === "select" || tag === "textarea" || tag === "button" || tag === "meter" || tag === "progress";
|
|
2612
|
+
}
|
|
2523
2613
|
function collectChildrenFromRolelessElement(element, warnings) {
|
|
2614
|
+
return collectAccessibleChildren(element, warnings);
|
|
2615
|
+
}
|
|
2616
|
+
function collectAccessibleChildren(element, warnings) {
|
|
2524
2617
|
const children = [];
|
|
2525
2618
|
if (element.shadowRoot) {
|
|
2526
2619
|
for (const child of Array.from(element.shadowRoot.childNodes)) {
|
|
2527
|
-
|
|
2620
|
+
processShadowNode(child, children, warnings);
|
|
2528
2621
|
}
|
|
2622
|
+
return children;
|
|
2623
|
+
}
|
|
2624
|
+
if (element.localName && element.localName.includes("-") && element.childNodes.length === 0) {
|
|
2625
|
+
warnings.push({
|
|
2626
|
+
message: `Custom element <${element.localName}> has no accessible children and may have a closed shadow root; its content could not be analyzed.`,
|
|
2627
|
+
element
|
|
2628
|
+
});
|
|
2529
2629
|
}
|
|
2530
2630
|
for (const child of Array.from(element.childNodes)) {
|
|
2531
2631
|
processChildNode(child, children, warnings);
|
|
2532
2632
|
}
|
|
2533
2633
|
return children;
|
|
2534
2634
|
}
|
|
2635
|
+
function processShadowNode(node, children, warnings) {
|
|
2636
|
+
if (node.nodeType === 1) {
|
|
2637
|
+
const element = node;
|
|
2638
|
+
if (element.localName === "slot") {
|
|
2639
|
+
const slot = element;
|
|
2640
|
+
let assigned = [];
|
|
2641
|
+
if (typeof slot.assignedNodes === "function") {
|
|
2642
|
+
try {
|
|
2643
|
+
assigned = Array.from(slot.assignedNodes({ flatten: true }));
|
|
2644
|
+
} catch {
|
|
2645
|
+
assigned = [];
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
if (assigned.length > 0) {
|
|
2649
|
+
for (const assignedNode of assigned) {
|
|
2650
|
+
processChildNode(assignedNode, children, warnings);
|
|
2651
|
+
}
|
|
2652
|
+
} else {
|
|
2653
|
+
for (const child of Array.from(slot.childNodes)) {
|
|
2654
|
+
processShadowNode(child, children, warnings);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
processChildNode(node, children, warnings);
|
|
2661
|
+
}
|
|
2535
2662
|
function buildAccessibilityTree(rootElement, sourceHash) {
|
|
2536
2663
|
const warnings = [];
|
|
2537
2664
|
if (isHidden(rootElement)) {
|
|
@@ -2588,6 +2715,30 @@ function buildAccessibilityTreeWithSelector(rootElement, selector, sourceHash) {
|
|
|
2588
2715
|
}
|
|
2589
2716
|
return results;
|
|
2590
2717
|
}
|
|
2718
|
+
var SET_MEMBER_ROLES = /* @__PURE__ */ new Set([
|
|
2719
|
+
"radio",
|
|
2720
|
+
"option",
|
|
2721
|
+
"listitem",
|
|
2722
|
+
"tab"
|
|
2723
|
+
]);
|
|
2724
|
+
function assignImplicitSetPosition(children) {
|
|
2725
|
+
const totals = /* @__PURE__ */ new Map();
|
|
2726
|
+
for (const child of children) {
|
|
2727
|
+
if (SET_MEMBER_ROLES.has(child.role)) {
|
|
2728
|
+
totals.set(child.role, (totals.get(child.role) ?? 0) + 1);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2732
|
+
for (const child of children) {
|
|
2733
|
+
if (!SET_MEMBER_ROLES.has(child.role)) continue;
|
|
2734
|
+
const total = totals.get(child.role);
|
|
2735
|
+
if (total < 2) continue;
|
|
2736
|
+
const pos = (seen.get(child.role) ?? 0) + 1;
|
|
2737
|
+
seen.set(child.role, pos);
|
|
2738
|
+
if (child.state.posinset === void 0) child.state.posinset = pos;
|
|
2739
|
+
if (child.state.setsize === void 0) child.state.setsize = total;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2591
2742
|
function buildNodeRecursive(element, warnings) {
|
|
2592
2743
|
if (isHidden(element)) {
|
|
2593
2744
|
return null;
|
|
@@ -2624,15 +2775,15 @@ function buildNodeRecursive(element, warnings) {
|
|
|
2624
2775
|
element: w.element
|
|
2625
2776
|
})));
|
|
2626
2777
|
const focusInfo = extractFocusInfo(element);
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2778
|
+
let children = collectAccessibleChildren(element, warnings);
|
|
2779
|
+
const isNativeSelect = element.tagName.toLowerCase() === "select";
|
|
2780
|
+
if (isNativeSelect && roleResult.role === "combobox") {
|
|
2781
|
+
children = [];
|
|
2782
|
+
if (stateResult.state.expanded === void 0) {
|
|
2783
|
+
stateResult.state.expanded = false;
|
|
2631
2784
|
}
|
|
2632
2785
|
}
|
|
2633
|
-
|
|
2634
|
-
processChildNode(child, children, warnings);
|
|
2635
|
-
}
|
|
2786
|
+
assignImplicitSetPosition(children);
|
|
2636
2787
|
const node = {
|
|
2637
2788
|
role: roleResult.role,
|
|
2638
2789
|
name: nameResult.name,
|
|
@@ -2645,15 +2796,7 @@ function buildNodeRecursive(element, warnings) {
|
|
|
2645
2796
|
return node;
|
|
2646
2797
|
}
|
|
2647
2798
|
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
|
-
}
|
|
2799
|
+
const children = collectAccessibleChildren(rootElement, warnings);
|
|
2657
2800
|
return {
|
|
2658
2801
|
role: "generic",
|
|
2659
2802
|
name: "",
|
|
@@ -4365,8 +4508,10 @@ function createHeuristicAnalyzer(config) {
|
|
|
4365
4508
|
};
|
|
4366
4509
|
}
|
|
4367
4510
|
|
|
4511
|
+
// src/version.ts
|
|
4512
|
+
var SPEAKABLE_VERSION = "1.7.0" ? "1.7.0" : "dev";
|
|
4513
|
+
|
|
4368
4514
|
// src/runtime/timeline-generator.ts
|
|
4369
|
-
var SPEAKABLE_VERSION = "1.3.0";
|
|
4370
4515
|
var TIMELINE_FORMAT_VERSION = "1.0";
|
|
4371
4516
|
var DEFAULT_SETTLE_PERIOD = 500;
|
|
4372
4517
|
var DEFAULT_LOAD_TIMEOUT = 1e4;
|
|
@@ -5123,7 +5268,6 @@ function classifyModified(entry) {
|
|
|
5123
5268
|
// src/runtime/baseline-storage.ts
|
|
5124
5269
|
import { readFile, writeFile, mkdir, access } from "fs/promises";
|
|
5125
5270
|
import { join, dirname } from "path";
|
|
5126
|
-
var SPEAKABLE_VERSION2 = "1.3.0";
|
|
5127
5271
|
function createBaselineStorage(baseDir) {
|
|
5128
5272
|
function sanitize(name) {
|
|
5129
5273
|
return name.toLowerCase().replace(/[/\\]/g, "-").replace(/[^a-z0-9._-]/g, "-");
|
|
@@ -5143,7 +5287,7 @@ function createBaselineStorage(baseDir) {
|
|
|
5143
5287
|
const baselineFile = {
|
|
5144
5288
|
baseline: {
|
|
5145
5289
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5146
|
-
speakableVersion:
|
|
5290
|
+
speakableVersion: SPEAKABLE_VERSION,
|
|
5147
5291
|
storybookUrl: meta?.storybookUrl,
|
|
5148
5292
|
interactionSequence: timeline.interactionSequence
|
|
5149
5293
|
},
|