@a11y-oracle/core-engine 1.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.
- package/README.md +473 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/lib/a11y-orchestrator.d.ts +112 -0
- package/dist/lib/a11y-orchestrator.d.ts.map +1 -0
- package/dist/lib/a11y-orchestrator.js +170 -0
- package/dist/lib/role-map.d.ts +41 -0
- package/dist/lib/role-map.d.ts.map +1 -0
- package/dist/lib/role-map.js +128 -0
- package/dist/lib/speech-engine.d.ts +149 -0
- package/dist/lib/speech-engine.d.ts.map +1 -0
- package/dist/lib/speech-engine.js +219 -0
- package/dist/lib/state-map.d.ts +82 -0
- package/dist/lib/state-map.d.ts.map +1 -0
- package/dist/lib/state-map.js +86 -0
- package/dist/lib/types.d.ts +162 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +10 -0
- package/package.json +52 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module speech-engine
|
|
3
|
+
*
|
|
4
|
+
* The central class of A11y-Oracle. {@link SpeechEngine} connects to the
|
|
5
|
+
* browser's Accessibility Tree via the Chrome DevTools Protocol and generates
|
|
6
|
+
* standardized speech output following the format:
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* [Computed Name], [Role], [State/Properties]
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Chrome's CDP already computes accessible names per the W3C AccName spec,
|
|
13
|
+
* so the engine's job is to:
|
|
14
|
+
* 1. Fetch the AXTree via `Accessibility.getFullAXTree()`
|
|
15
|
+
* 2. Find the currently focused node
|
|
16
|
+
* 3. Map the node's role and properties to human-readable strings
|
|
17
|
+
* 4. Assemble the final speech string
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { SpeechEngine } from '@a11y-oracle/core-engine';
|
|
22
|
+
*
|
|
23
|
+
* const engine = new SpeechEngine(cdpSession);
|
|
24
|
+
* await engine.enable();
|
|
25
|
+
*
|
|
26
|
+
* const result = await engine.getSpeech();
|
|
27
|
+
* console.log(result?.speech); // "Products, button, collapsed"
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
import { ROLE_TO_SPEECH, LANDMARK_ROLES } from './role-map.js';
|
|
31
|
+
import { extractStates } from './state-map.js';
|
|
32
|
+
/**
|
|
33
|
+
* Generates standardized speech output from the browser's Accessibility Tree.
|
|
34
|
+
*
|
|
35
|
+
* The engine operates through a {@link CDPSessionLike} interface, making it
|
|
36
|
+
* framework-agnostic. It works with Playwright's CDP sessions, raw WebSocket
|
|
37
|
+
* connections, or any other CDP-compatible client.
|
|
38
|
+
*
|
|
39
|
+
* ## Speech Output Format
|
|
40
|
+
*
|
|
41
|
+
* Every element produces a string in this format:
|
|
42
|
+
* ```
|
|
43
|
+
* [Computed Name], [Role], [State/Properties]
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* Parts are omitted if they are empty. For example:
|
|
47
|
+
* - `"Products, button, collapsed"` — name + role + state
|
|
48
|
+
* - `"Main, navigation landmark"` — name + role (landmark)
|
|
49
|
+
* - `"Home, link"` — name + role (no states)
|
|
50
|
+
*
|
|
51
|
+
* ## Landmark Roles
|
|
52
|
+
*
|
|
53
|
+
* Landmark roles (navigation, main, banner, etc.) automatically append
|
|
54
|
+
* the word "landmark" to their role string unless
|
|
55
|
+
* {@link SpeechEngineOptions.includeLandmarks} is set to `false`.
|
|
56
|
+
*/
|
|
57
|
+
export class SpeechEngine {
|
|
58
|
+
cdp;
|
|
59
|
+
options;
|
|
60
|
+
/**
|
|
61
|
+
* Create a new SpeechEngine instance.
|
|
62
|
+
*
|
|
63
|
+
* @param cdp - A CDP session-like object for sending protocol commands.
|
|
64
|
+
* @param options - Optional configuration for speech output behavior.
|
|
65
|
+
*/
|
|
66
|
+
constructor(cdp, options = {}) {
|
|
67
|
+
this.cdp = cdp;
|
|
68
|
+
this.options = {
|
|
69
|
+
includeLandmarks: options.includeLandmarks ?? true,
|
|
70
|
+
includeDescription: options.includeDescription ?? false,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Enable the CDP Accessibility domain.
|
|
75
|
+
*
|
|
76
|
+
* Must be called before any other method. Enables the browser to
|
|
77
|
+
* start tracking and reporting accessibility tree data.
|
|
78
|
+
*/
|
|
79
|
+
async enable() {
|
|
80
|
+
await this.cdp.send('Accessibility.enable');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Disable the CDP Accessibility domain.
|
|
84
|
+
*
|
|
85
|
+
* Call this when you're done using the engine to free browser resources.
|
|
86
|
+
*/
|
|
87
|
+
async disable() {
|
|
88
|
+
await this.cdp.send('Accessibility.disable');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get the speech output for the currently focused element.
|
|
92
|
+
*
|
|
93
|
+
* Fetches the full AXTree, locates the node with `focused: true`,
|
|
94
|
+
* and computes its speech string.
|
|
95
|
+
*
|
|
96
|
+
* @returns The {@link SpeechResult} for the focused element, or `null`
|
|
97
|
+
* if no element has focus or the focused element is ignored.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* // After pressing Tab to focus a button
|
|
102
|
+
* const result = await engine.getSpeech();
|
|
103
|
+
* console.log(result?.speech); // "Products, button, collapsed"
|
|
104
|
+
* console.log(result?.name); // "Products"
|
|
105
|
+
* console.log(result?.role); // "button"
|
|
106
|
+
* console.log(result?.states); // ["collapsed"]
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
async getSpeech() {
|
|
110
|
+
const { nodes } = await this.cdp.send('Accessibility.getFullAXTree');
|
|
111
|
+
const focusedNode = this.findFocusedNode(nodes);
|
|
112
|
+
if (!focusedNode)
|
|
113
|
+
return null;
|
|
114
|
+
return this.computeSpeech(focusedNode);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get speech output for ALL non-ignored, non-silent nodes in the tree.
|
|
118
|
+
*
|
|
119
|
+
* Useful for asserting on landmarks, headings, or other structural
|
|
120
|
+
* elements that may not have focus.
|
|
121
|
+
*
|
|
122
|
+
* @returns An array of {@link SpeechResult} objects for every visible
|
|
123
|
+
* node that produces speech output.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* const all = await engine.getFullTreeSpeech();
|
|
128
|
+
* const nav = all.find(r => r.speech === 'Main, navigation landmark');
|
|
129
|
+
* expect(nav).toBeDefined();
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
async getFullTreeSpeech() {
|
|
133
|
+
const { nodes } = await this.cdp.send('Accessibility.getFullAXTree');
|
|
134
|
+
return nodes
|
|
135
|
+
.map((node) => this.computeSpeech(node))
|
|
136
|
+
.filter((result) => result !== null);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Find the most specific focused node in the flat AXTree node list.
|
|
140
|
+
*
|
|
141
|
+
* CDP returns the AXTree as a flat array with the document node first
|
|
142
|
+
* and more specific nodes later. Multiple nodes can report `focused: true`
|
|
143
|
+
* (e.g., both the RootWebArea and the actual focused element). This method
|
|
144
|
+
* returns the **last** focused node, which is the most specific (deepest)
|
|
145
|
+
* element that actually has user focus.
|
|
146
|
+
*
|
|
147
|
+
* @param nodes - The flat array of AXNodes from `getFullAXTree()`.
|
|
148
|
+
* @returns The most specific focused node, or `null` if no node is focused.
|
|
149
|
+
*/
|
|
150
|
+
findFocusedNode(nodes) {
|
|
151
|
+
let lastFocused = null;
|
|
152
|
+
for (const node of nodes) {
|
|
153
|
+
const focused = node.properties?.find((p) => p.name === 'focused');
|
|
154
|
+
if (focused?.value?.value === true) {
|
|
155
|
+
lastFocused = node;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return lastFocused;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Compute the speech string for a single AXNode.
|
|
162
|
+
*
|
|
163
|
+
* Follows the output format: `[Computed Name], [Role], [State/Properties]`.
|
|
164
|
+
* Parts are omitted when empty. Ignored nodes and nodes with no
|
|
165
|
+
* meaningful content return `null`.
|
|
166
|
+
*
|
|
167
|
+
* @param node - A CDP AXNode from the accessibility tree.
|
|
168
|
+
* @returns A {@link SpeechResult} with the computed speech, or `null`
|
|
169
|
+
* if the node should be silent (ignored, no role, no name).
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* const result = engine.computeSpeech(axNode);
|
|
174
|
+
* // { speech: "Products, button, collapsed", name: "Products", ... }
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
computeSpeech(node) {
|
|
178
|
+
const role = node.role?.value;
|
|
179
|
+
if (!role)
|
|
180
|
+
return null;
|
|
181
|
+
// Skip ignored/invisible nodes
|
|
182
|
+
if (node.ignored)
|
|
183
|
+
return null;
|
|
184
|
+
const name = node.name?.value ?? '';
|
|
185
|
+
let speechRole = ROLE_TO_SPEECH[role] ?? role;
|
|
186
|
+
// Skip nodes with no role output and no name (generic containers, etc.)
|
|
187
|
+
if (!speechRole && !name)
|
|
188
|
+
return null;
|
|
189
|
+
// Append "landmark" to landmark roles
|
|
190
|
+
if (this.options.includeLandmarks && LANDMARK_ROLES.has(role)) {
|
|
191
|
+
speechRole = `${speechRole} landmark`;
|
|
192
|
+
}
|
|
193
|
+
const states = extractStates(node.properties);
|
|
194
|
+
// Build the speech parts: [name], [role], [states...]
|
|
195
|
+
const parts = [];
|
|
196
|
+
if (name)
|
|
197
|
+
parts.push(name);
|
|
198
|
+
if (speechRole)
|
|
199
|
+
parts.push(speechRole);
|
|
200
|
+
parts.push(...states);
|
|
201
|
+
// Optionally include description
|
|
202
|
+
if (this.options.includeDescription) {
|
|
203
|
+
const description = node.description?.value ?? '';
|
|
204
|
+
if (description)
|
|
205
|
+
parts.push(description);
|
|
206
|
+
}
|
|
207
|
+
const speech = parts.join(', ');
|
|
208
|
+
// Don't return results with empty speech
|
|
209
|
+
if (!speech)
|
|
210
|
+
return null;
|
|
211
|
+
return {
|
|
212
|
+
speech,
|
|
213
|
+
name,
|
|
214
|
+
role: speechRole,
|
|
215
|
+
states,
|
|
216
|
+
rawNode: node,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module state-map
|
|
3
|
+
*
|
|
4
|
+
* Maps Chrome DevTools Protocol AXNode property values to human-readable
|
|
5
|
+
* state strings used in speech output.
|
|
6
|
+
*
|
|
7
|
+
* CDP provides node properties as an array of `{ name, value }` objects.
|
|
8
|
+
* This module translates those boolean/enumerated properties into the
|
|
9
|
+
* words a screen reader would announce (e.g., `expanded: false` → `"collapsed"`).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Defines how a single CDP AXNode property maps to spoken state strings.
|
|
13
|
+
*
|
|
14
|
+
* For boolean properties, `trueValue` is spoken when the property is `true`,
|
|
15
|
+
* and `falseValue` when `false`. An empty string means the state is not
|
|
16
|
+
* announced for that value.
|
|
17
|
+
*/
|
|
18
|
+
export interface StateMapping {
|
|
19
|
+
/** The CDP property name (e.g., `"expanded"`, `"checked"`). */
|
|
20
|
+
property: string;
|
|
21
|
+
/** The string to speak when the property value is `true`. */
|
|
22
|
+
trueValue: string;
|
|
23
|
+
/** The string to speak when the property value is `false`. Empty string means silent. */
|
|
24
|
+
falseValue: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Ordered list of CDP property → spoken state mappings.
|
|
28
|
+
*
|
|
29
|
+
* The order determines the sequence in which states appear in the speech
|
|
30
|
+
* output when multiple states are present. For example, a required invalid
|
|
31
|
+
* field produces `"..., required, invalid"`.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```
|
|
35
|
+
* aria-expanded="false" → "collapsed"
|
|
36
|
+
* aria-expanded="true" → "expanded"
|
|
37
|
+
* aria-checked="true" → "checked"
|
|
38
|
+
* aria-disabled="true" → "dimmed"
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare const STATE_MAPPINGS: StateMapping[];
|
|
42
|
+
/**
|
|
43
|
+
* Represents a single property from a CDP AXNode's `properties` array.
|
|
44
|
+
*
|
|
45
|
+
* CDP encodes property values as `{ type, value }` objects. For boolean
|
|
46
|
+
* properties, `type` is `"boolean"` or `"booleanOrUndefined"` and `value`
|
|
47
|
+
* is `true` or `false`.
|
|
48
|
+
*/
|
|
49
|
+
export interface AXNodeProperty {
|
|
50
|
+
name: string;
|
|
51
|
+
value: {
|
|
52
|
+
type: string;
|
|
53
|
+
value?: unknown;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Extract human-readable state strings from a CDP AXNode's properties array.
|
|
58
|
+
*
|
|
59
|
+
* Iterates through {@link STATE_MAPPINGS} and checks whether each mapped
|
|
60
|
+
* property is present in the node's properties. Also handles the special
|
|
61
|
+
* `level` property for headings.
|
|
62
|
+
*
|
|
63
|
+
* @param properties - The `properties` array from a CDP AXNode, or `undefined`.
|
|
64
|
+
* @returns An array of state strings (e.g., `["collapsed"]`, `["checked", "required"]`).
|
|
65
|
+
* Returns an empty array if no properties are present or none match.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* extractStates([
|
|
70
|
+
* { name: 'expanded', value: { type: 'boolean', value: false } },
|
|
71
|
+
* { name: 'required', value: { type: 'boolean', value: true } },
|
|
72
|
+
* ]);
|
|
73
|
+
* // → ['collapsed', 'required']
|
|
74
|
+
*
|
|
75
|
+
* extractStates([
|
|
76
|
+
* { name: 'level', value: { type: 'integer', value: 2 } },
|
|
77
|
+
* ]);
|
|
78
|
+
* // → ['level 2']
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function extractStates(properties: AXNodeProperty[] | undefined): string[];
|
|
82
|
+
//# sourceMappingURL=state-map.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-map.d.ts","sourceRoot":"","sources":["../../src/lib/state-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB,yFAAyF;IACzF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,EAAE,YAAY,EAUxC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,cAAc,EAAE,GAAG,SAAS,GACvC,MAAM,EAAE,CAwBV"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module state-map
|
|
3
|
+
*
|
|
4
|
+
* Maps Chrome DevTools Protocol AXNode property values to human-readable
|
|
5
|
+
* state strings used in speech output.
|
|
6
|
+
*
|
|
7
|
+
* CDP provides node properties as an array of `{ name, value }` objects.
|
|
8
|
+
* This module translates those boolean/enumerated properties into the
|
|
9
|
+
* words a screen reader would announce (e.g., `expanded: false` → `"collapsed"`).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Ordered list of CDP property → spoken state mappings.
|
|
13
|
+
*
|
|
14
|
+
* The order determines the sequence in which states appear in the speech
|
|
15
|
+
* output when multiple states are present. For example, a required invalid
|
|
16
|
+
* field produces `"..., required, invalid"`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```
|
|
20
|
+
* aria-expanded="false" → "collapsed"
|
|
21
|
+
* aria-expanded="true" → "expanded"
|
|
22
|
+
* aria-checked="true" → "checked"
|
|
23
|
+
* aria-disabled="true" → "dimmed"
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const STATE_MAPPINGS = [
|
|
27
|
+
{ property: 'expanded', trueValue: 'expanded', falseValue: 'collapsed' },
|
|
28
|
+
{ property: 'checked', trueValue: 'checked', falseValue: 'not checked' },
|
|
29
|
+
{ property: 'selected', trueValue: 'selected', falseValue: '' },
|
|
30
|
+
{ property: 'pressed', trueValue: 'pressed', falseValue: 'not pressed' },
|
|
31
|
+
{ property: 'disabled', trueValue: 'dimmed', falseValue: '' },
|
|
32
|
+
{ property: 'required', trueValue: 'required', falseValue: '' },
|
|
33
|
+
{ property: 'invalid', trueValue: 'invalid', falseValue: '' },
|
|
34
|
+
{ property: 'readonly', trueValue: 'read only', falseValue: '' },
|
|
35
|
+
{ property: 'multiselectable', trueValue: 'multi selectable', falseValue: '' },
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Extract human-readable state strings from a CDP AXNode's properties array.
|
|
39
|
+
*
|
|
40
|
+
* Iterates through {@link STATE_MAPPINGS} and checks whether each mapped
|
|
41
|
+
* property is present in the node's properties. Also handles the special
|
|
42
|
+
* `level` property for headings.
|
|
43
|
+
*
|
|
44
|
+
* @param properties - The `properties` array from a CDP AXNode, or `undefined`.
|
|
45
|
+
* @returns An array of state strings (e.g., `["collapsed"]`, `["checked", "required"]`).
|
|
46
|
+
* Returns an empty array if no properties are present or none match.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* extractStates([
|
|
51
|
+
* { name: 'expanded', value: { type: 'boolean', value: false } },
|
|
52
|
+
* { name: 'required', value: { type: 'boolean', value: true } },
|
|
53
|
+
* ]);
|
|
54
|
+
* // → ['collapsed', 'required']
|
|
55
|
+
*
|
|
56
|
+
* extractStates([
|
|
57
|
+
* { name: 'level', value: { type: 'integer', value: 2 } },
|
|
58
|
+
* ]);
|
|
59
|
+
* // → ['level 2']
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function extractStates(properties) {
|
|
63
|
+
if (!properties || properties.length === 0)
|
|
64
|
+
return [];
|
|
65
|
+
const states = [];
|
|
66
|
+
for (const mapping of STATE_MAPPINGS) {
|
|
67
|
+
const prop = properties.find((p) => p.name === mapping.property);
|
|
68
|
+
if (!prop)
|
|
69
|
+
continue;
|
|
70
|
+
const val = prop.value?.value;
|
|
71
|
+
if (val === true || val === 'true') {
|
|
72
|
+
if (mapping.trueValue)
|
|
73
|
+
states.push(mapping.trueValue);
|
|
74
|
+
}
|
|
75
|
+
else if (val === false || val === 'false') {
|
|
76
|
+
if (mapping.falseValue)
|
|
77
|
+
states.push(mapping.falseValue);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Handle heading level (special property, not boolean)
|
|
81
|
+
const level = properties.find((p) => p.name === 'level');
|
|
82
|
+
if (level?.value?.value !== undefined) {
|
|
83
|
+
states.push(`level ${level.value.value}`);
|
|
84
|
+
}
|
|
85
|
+
return states;
|
|
86
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module types
|
|
3
|
+
*
|
|
4
|
+
* Core type definitions for the A11y-Oracle speech engine.
|
|
5
|
+
*
|
|
6
|
+
* The key abstraction is {@link CDPSessionLike}, which decouples the engine
|
|
7
|
+
* from any specific test framework. Both Playwright's `CDPSession` and a raw
|
|
8
|
+
* WebSocket CDP client can satisfy this interface.
|
|
9
|
+
*/
|
|
10
|
+
import type { Protocol } from 'devtools-protocol';
|
|
11
|
+
import type { CDPSessionLike as BaseCDPSessionLike } from '@a11y-oracle/cdp-types';
|
|
12
|
+
/**
|
|
13
|
+
* Extended CDP session interface with typed Accessibility domain methods.
|
|
14
|
+
*
|
|
15
|
+
* Extends the base {@link BaseCDPSessionLike} from `@a11y-oracle/cdp-types`
|
|
16
|
+
* with specific overloads for the Accessibility CDP domain used by the
|
|
17
|
+
* speech engine and orchestrator.
|
|
18
|
+
*/
|
|
19
|
+
export interface CDPSessionLike extends BaseCDPSessionLike {
|
|
20
|
+
send(method: 'Accessibility.enable'): Promise<void>;
|
|
21
|
+
send(method: 'Accessibility.disable'): Promise<void>;
|
|
22
|
+
send(method: 'Accessibility.getFullAXTree', params?: {
|
|
23
|
+
depth?: number;
|
|
24
|
+
frameId?: string;
|
|
25
|
+
}): Promise<Protocol.Accessibility.GetFullAXTreeResponse>;
|
|
26
|
+
send(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The result of computing speech output for a single accessibility node.
|
|
30
|
+
*
|
|
31
|
+
* Contains both the final speech string and its constituent parts for
|
|
32
|
+
* granular assertions.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const result: SpeechResult = {
|
|
37
|
+
* speech: 'Products, button, collapsed',
|
|
38
|
+
* name: 'Products',
|
|
39
|
+
* role: 'button',
|
|
40
|
+
* states: ['collapsed'],
|
|
41
|
+
* rawNode: { ... } // CDP AXNode
|
|
42
|
+
* };
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export interface SpeechResult {
|
|
46
|
+
/** The full speech string, e.g. `"Products, button, collapsed"`. */
|
|
47
|
+
speech: string;
|
|
48
|
+
/** The computed accessible name. */
|
|
49
|
+
name: string;
|
|
50
|
+
/** The human-readable role string (already mapped from CDP role). */
|
|
51
|
+
role: string;
|
|
52
|
+
/** Array of state strings, e.g. `["collapsed"]`, `["checked", "required"]`. */
|
|
53
|
+
states: string[];
|
|
54
|
+
/** The raw CDP AXNode for advanced inspection. */
|
|
55
|
+
rawNode: Protocol.Accessibility.AXNode;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* DOM-level information about the currently focused element.
|
|
59
|
+
*
|
|
60
|
+
* This is the orchestrator's view of the focused element — a simpler,
|
|
61
|
+
* framework-agnostic shape compared to `FocusedElementInfo` from
|
|
62
|
+
* keyboard-engine (which is a raw CDP extraction type).
|
|
63
|
+
*/
|
|
64
|
+
export interface A11yFocusedElement {
|
|
65
|
+
/** Tag name, e.g. `'BUTTON'`. */
|
|
66
|
+
tag: string;
|
|
67
|
+
/** Element `id` attribute, or empty string. */
|
|
68
|
+
id: string;
|
|
69
|
+
/** Element `className` attribute, or empty string. */
|
|
70
|
+
className: string;
|
|
71
|
+
/** Trimmed text content of the element. */
|
|
72
|
+
textContent: string;
|
|
73
|
+
/** The element's `role` attribute, or empty string. */
|
|
74
|
+
role: string;
|
|
75
|
+
/** The element's `aria-label` attribute, or empty string. */
|
|
76
|
+
ariaLabel: string;
|
|
77
|
+
/** The element's `tabIndex` property. */
|
|
78
|
+
tabIndex: number;
|
|
79
|
+
/** Bounding rectangle in viewport coordinates. */
|
|
80
|
+
rect: {
|
|
81
|
+
x: number;
|
|
82
|
+
y: number;
|
|
83
|
+
width: number;
|
|
84
|
+
height: number;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Visual focus indicator analysis from CSS computed styles.
|
|
89
|
+
*
|
|
90
|
+
* A simplified projection of the full {@link FocusIndicator} from
|
|
91
|
+
* `@a11y-oracle/focus-analyzer`, carrying only the fields needed
|
|
92
|
+
* for unified state assertions.
|
|
93
|
+
*/
|
|
94
|
+
export interface A11yFocusIndicator {
|
|
95
|
+
/** Whether any visual focus indicator is detected. */
|
|
96
|
+
isVisible: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Contrast ratio of the focus indicator against the background.
|
|
99
|
+
* `null` if the colors could not be reliably parsed.
|
|
100
|
+
*/
|
|
101
|
+
contrastRatio: number | null;
|
|
102
|
+
/**
|
|
103
|
+
* Whether the indicator meets WCAG 2.4.12 AA
|
|
104
|
+
* (contrast ≥ 3.0 and indicator is visible).
|
|
105
|
+
*/
|
|
106
|
+
meetsWCAG_AA: boolean;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Unified accessibility state combining speech output, focus information,
|
|
110
|
+
* and visual indicator analysis.
|
|
111
|
+
*
|
|
112
|
+
* Returned by {@link A11yOrchestrator.pressKey} and
|
|
113
|
+
* {@link A11yOrchestrator.getState} to give a single snapshot of
|
|
114
|
+
* "what the screen reader says" + "where focus is" + "how focus looks".
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```typescript
|
|
118
|
+
* const state = await orchestrator.pressKey('Tab');
|
|
119
|
+
* console.log(state.speech); // "Products, button, collapsed"
|
|
120
|
+
* console.log(state.focusedElement?.tag); // "BUTTON"
|
|
121
|
+
* console.log(state.focusIndicator.meetsWCAG_AA); // true
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export interface A11yState {
|
|
125
|
+
/** The full speech string, e.g. `"Products, button, collapsed"`. */
|
|
126
|
+
speech: string;
|
|
127
|
+
/** The full speech result with raw AXNode data. `null` if no focused element. */
|
|
128
|
+
speechResult: SpeechResult | null;
|
|
129
|
+
/** DOM-level info about the focused element. `null` if no element has focus. */
|
|
130
|
+
focusedElement: A11yFocusedElement | null;
|
|
131
|
+
/** Visual focus indicator analysis. */
|
|
132
|
+
focusIndicator: A11yFocusIndicator;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Configuration options for the {@link A11yOrchestrator}.
|
|
136
|
+
*/
|
|
137
|
+
export interface A11yOrchestratorOptions extends SpeechEngineOptions {
|
|
138
|
+
/**
|
|
139
|
+
* Milliseconds to wait after a key press before reading state,
|
|
140
|
+
* allowing CSS transitions and focus events to settle.
|
|
141
|
+
* Defaults to `50`.
|
|
142
|
+
*/
|
|
143
|
+
focusSettleMs?: number;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Configuration options for the {@link SpeechEngine}.
|
|
147
|
+
*/
|
|
148
|
+
export interface SpeechEngineOptions {
|
|
149
|
+
/**
|
|
150
|
+
* Whether to include landmark roles in speech output.
|
|
151
|
+
* When `true` (default), landmarks like `<nav>` produce
|
|
152
|
+
* `"Main, navigation landmark"`.
|
|
153
|
+
*/
|
|
154
|
+
includeLandmarks?: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Whether to include the accessible description in speech output.
|
|
157
|
+
* When `true`, the description is appended after states.
|
|
158
|
+
* Defaults to `false`.
|
|
159
|
+
*/
|
|
160
|
+
includeDescription?: boolean;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,IAAI,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEnF;;;;;;GAMG;AACH,MAAM,WAAW,cAAe,SAAQ,kBAAkB;IACxD,IAAI,CACF,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,IAAI,CACF,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,IAAI,CACF,MAAM,EAAE,6BAA6B,EACrC,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC;IACzD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,YAAY;IAC3B,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,kDAAkD;IAClD,OAAO,EAAE,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC;CACxC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/D;AAED;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,SAAS,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;OAGG;IACH,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,SAAS;IACxB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,gFAAgF;IAChF,cAAc,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC1C,uCAAuC;IACvC,cAAc,EAAE,kBAAkB,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAwB,SAAQ,mBAAmB;IAClE;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module types
|
|
3
|
+
*
|
|
4
|
+
* Core type definitions for the A11y-Oracle speech engine.
|
|
5
|
+
*
|
|
6
|
+
* The key abstraction is {@link CDPSessionLike}, which decouples the engine
|
|
7
|
+
* from any specific test framework. Both Playwright's `CDPSession` and a raw
|
|
8
|
+
* WebSocket CDP client can satisfy this interface.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@a11y-oracle/core-engine",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Framework-agnostic accessibility speech engine and unified A11yOrchestrator via Chrome DevTools Protocol",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "a11y-oracle",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/a11y-oracle/a11y-oracle.git",
|
|
10
|
+
"directory": "libs/core-engine"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/a11y-oracle/a11y-oracle/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/a11y-oracle/a11y-oracle/tree/main/libs/core-engine",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"accessibility",
|
|
18
|
+
"a11y",
|
|
19
|
+
"screen-reader",
|
|
20
|
+
"speech",
|
|
21
|
+
"wcag",
|
|
22
|
+
"aria",
|
|
23
|
+
"cdp",
|
|
24
|
+
"testing"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
"./package.json": "./package.json",
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js",
|
|
38
|
+
"default": "./dist/index.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"!**/*.tsbuildinfo"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@a11y-oracle/cdp-types": "1.0.0",
|
|
47
|
+
"@a11y-oracle/keyboard-engine": "1.0.0",
|
|
48
|
+
"@a11y-oracle/focus-analyzer": "1.0.0",
|
|
49
|
+
"devtools-protocol": "^0.0.1591961",
|
|
50
|
+
"tslib": "^2.3.0"
|
|
51
|
+
}
|
|
52
|
+
}
|