@oxvo/ai-live-assist 7.3.4 → 7.4.23
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/cjs/AiLiveAssist.d.ts +588 -6
- package/cjs/AiLiveAssist.js +14892 -1240
- package/cjs/client.d.ts +71 -10
- package/cjs/client.js +316 -58
- package/cjs/context.d.ts +15 -5
- package/cjs/context.js +568 -73
- package/cjs/control.d.ts +72 -7
- package/cjs/control.js +594 -71
- package/cjs/dialogFocusScope.d.ts +37 -0
- package/cjs/dialogFocusScope.js +227 -0
- package/cjs/experienceState.js +1 -1
- package/cjs/index.d.ts +1 -1
- package/cjs/index.js +2 -2
- package/cjs/media.d.ts +14 -3
- package/cjs/media.js +128 -19
- package/cjs/messages.js +26 -3
- package/cjs/messengerLoader.d.ts +3 -0
- package/cjs/messengerLoader.js +156 -0
- package/cjs/pageAssistancePreference.d.ts +78 -0
- package/cjs/pageAssistancePreference.js +488 -0
- package/cjs/preview.d.ts +33 -0
- package/cjs/preview.js +567 -0
- package/cjs/tabLock.d.ts +54 -3
- package/cjs/tabLock.js +752 -72
- package/cjs/types.d.ts +152 -4
- package/cjs/ui.d.ts +200 -10
- package/cjs/ui.js +6946 -523
- package/cjs/version.d.ts +1 -1
- package/cjs/version.js +1 -1
- package/cjs/voicePresenceUi.d.ts +48 -9
- package/cjs/voicePresenceUi.js +863 -125
- package/lib/AiLiveAssist.d.ts +588 -6
- package/lib/AiLiveAssist.js +14897 -1248
- package/lib/client.d.ts +71 -10
- package/lib/client.js +313 -57
- package/lib/context.d.ts +15 -5
- package/lib/context.js +566 -72
- package/lib/control.d.ts +72 -7
- package/lib/control.js +594 -71
- package/lib/dialogFocusScope.d.ts +37 -0
- package/lib/dialogFocusScope.js +223 -0
- package/lib/experienceState.js +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +2 -2
- package/lib/media.d.ts +14 -3
- package/lib/media.js +128 -19
- package/lib/messages.js +26 -3
- package/lib/messengerLoader.d.ts +3 -0
- package/lib/messengerLoader.js +151 -0
- package/lib/pageAssistancePreference.d.ts +78 -0
- package/lib/pageAssistancePreference.js +482 -0
- package/lib/preview.d.ts +33 -0
- package/lib/preview.js +561 -0
- package/lib/tabLock.d.ts +54 -3
- package/lib/tabLock.js +752 -72
- package/lib/types.d.ts +152 -4
- package/lib/ui.d.ts +200 -10
- package/lib/ui.js +6947 -524
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/voicePresenceUi.d.ts +48 -9
- package/lib/voicePresenceUi.js +863 -125
- package/package.json +8 -3
- package/cjs/recordingPolicy.d.ts +0 -2
- package/cjs/recordingPolicy.js +0 -12
- package/lib/recordingPolicy.d.ts +0 -2
- package/lib/recordingPolicy.js +0 -8
package/lib/context.js
CHANGED
|
@@ -26,12 +26,63 @@ const SENSITIVE_PATTERNS = [
|
|
|
26
26
|
['authentication_secret', /auth.*secret|private.?key|access.?token/i],
|
|
27
27
|
['api_secret', /api.?key|client.?secret|secret.?key/i],
|
|
28
28
|
];
|
|
29
|
+
const EXPLICIT_SENSITIVITIES = new Set([
|
|
30
|
+
'private',
|
|
31
|
+
'password',
|
|
32
|
+
'otp',
|
|
33
|
+
'payment',
|
|
34
|
+
'banking',
|
|
35
|
+
'recovery',
|
|
36
|
+
'authentication_secret',
|
|
37
|
+
'api_secret',
|
|
38
|
+
]);
|
|
29
39
|
const id = (prefix) => `${prefix}_${crypto.randomUUID()}`;
|
|
30
40
|
const normalize = (value, maximum) => value
|
|
31
41
|
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '')
|
|
32
42
|
.replace(/\s+/g, ' ')
|
|
33
43
|
.trim()
|
|
34
44
|
.slice(0, maximum);
|
|
45
|
+
const canonicalJson = (value) => {
|
|
46
|
+
const sort = (item) => {
|
|
47
|
+
if (Array.isArray(item))
|
|
48
|
+
return item.map(sort);
|
|
49
|
+
if (item !== null && typeof item === 'object') {
|
|
50
|
+
return Object.fromEntries(Object.entries(item)
|
|
51
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
52
|
+
.map(([key, nested]) => [key, sort(nested)]));
|
|
53
|
+
}
|
|
54
|
+
return item;
|
|
55
|
+
};
|
|
56
|
+
return JSON.stringify(sort(value));
|
|
57
|
+
};
|
|
58
|
+
export const actionTargetFingerprint = async (target, subtle = globalThis.crypto?.subtle) => {
|
|
59
|
+
if (!subtle)
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
const canonical = canonicalJson({
|
|
63
|
+
targetId: target.targetId,
|
|
64
|
+
role: target.role,
|
|
65
|
+
name: target.name,
|
|
66
|
+
inputType: target.inputType ?? null,
|
|
67
|
+
autocomplete: target.autocomplete ?? null,
|
|
68
|
+
semanticRegion: target.semanticRegion ?? null,
|
|
69
|
+
destinationPath: target.destinationPath ?? null,
|
|
70
|
+
sensitivity: target.sensitivity,
|
|
71
|
+
protectedRegion: target.protectedRegion ?? null,
|
|
72
|
+
frameOrigin: target.frameOrigin ?? null,
|
|
73
|
+
consequence: target.consequence ?? null,
|
|
74
|
+
regionAssessment: target.regionAssessment ?? null,
|
|
75
|
+
options: target.options ?? null,
|
|
76
|
+
});
|
|
77
|
+
const digest = new Uint8Array(await subtle.digest('SHA-256', new TextEncoder().encode(canonical)));
|
|
78
|
+
if (digest.byteLength !== 32)
|
|
79
|
+
return null;
|
|
80
|
+
return Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
35
86
|
const isHTMLElement = (value) => value instanceof HTMLElement;
|
|
36
87
|
const isRendered = (element) => {
|
|
37
88
|
if (!isHTMLElement(element) || !element.isConnected || element.closest(PRIVATE_SELECTOR)) {
|
|
@@ -57,11 +108,22 @@ const isVisible = (element) => {
|
|
|
57
108
|
rect.top <= window.innerHeight &&
|
|
58
109
|
rect.left <= window.innerWidth);
|
|
59
110
|
};
|
|
60
|
-
const isCovered = (element) => {
|
|
111
|
+
const isCovered = (element, trustedGuideRoot = null) => {
|
|
61
112
|
const rect = element.getBoundingClientRect();
|
|
62
113
|
const x = Math.max(0, Math.min(window.innerWidth - 1, rect.left + rect.width / 2));
|
|
63
114
|
const y = Math.max(0, Math.min(window.innerHeight - 1, rect.top + rect.height / 2));
|
|
64
|
-
|
|
115
|
+
let top;
|
|
116
|
+
if (trustedGuideRoot?.isConnected &&
|
|
117
|
+
typeof document.elementsFromPoint === 'function') {
|
|
118
|
+
top =
|
|
119
|
+
document
|
|
120
|
+
.elementsFromPoint(x, y)
|
|
121
|
+
.find((candidate) => candidate !== trustedGuideRoot) ??
|
|
122
|
+
document.elementFromPoint(x, y);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
top = document.elementFromPoint(x, y);
|
|
126
|
+
}
|
|
65
127
|
return Boolean(top && top !== element && !element.contains(top) && !top.contains(element));
|
|
66
128
|
};
|
|
67
129
|
const safePageUrl = () => `${window.location.origin}${window.location.pathname}`;
|
|
@@ -94,17 +156,22 @@ const sensitivityFor = (element, name) => {
|
|
|
94
156
|
return 'otp';
|
|
95
157
|
if (/^cc-/.test(autocomplete))
|
|
96
158
|
return 'payment';
|
|
159
|
+
const explicitSensitivity = normalize(element.closest('[data-oxvo-sensitivity]')?.getAttribute('data-oxvo-sensitivity') ?? '', 64);
|
|
160
|
+
if (EXPLICIT_SENSITIVITIES.has(explicitSensitivity))
|
|
161
|
+
return explicitSensitivity;
|
|
97
162
|
const descriptors = [
|
|
98
163
|
name,
|
|
99
164
|
element.getAttribute('name') ?? '',
|
|
100
165
|
element.id,
|
|
101
166
|
element.getAttribute('autocomplete') ?? '',
|
|
102
|
-
element.getAttribute('data-oxvo-sensitivity') ?? '',
|
|
167
|
+
element.closest('[data-oxvo-sensitivity]')?.getAttribute('data-oxvo-sensitivity') ?? '',
|
|
103
168
|
].join(' ');
|
|
104
169
|
for (const [sensitivity, pattern] of SENSITIVE_PATTERNS) {
|
|
105
170
|
if (pattern.test(descriptors))
|
|
106
171
|
return sensitivity;
|
|
107
172
|
}
|
|
173
|
+
if (element.closest('[data-oxvo-sensitivity]'))
|
|
174
|
+
return 'private';
|
|
108
175
|
if (element.closest('[data-oxvo-private]'))
|
|
109
176
|
return 'private';
|
|
110
177
|
return 'none';
|
|
@@ -138,11 +205,6 @@ const roleFor = (element) => {
|
|
|
138
205
|
return tag.slice(0, 64);
|
|
139
206
|
};
|
|
140
207
|
const nameFor = (element) => {
|
|
141
|
-
const labelledBy = element.getAttribute('aria-labelledby');
|
|
142
|
-
const labelledText = labelledBy
|
|
143
|
-
?.split(/\s+/)
|
|
144
|
-
.map((value) => document.getElementById(value)?.textContent ?? '')
|
|
145
|
-
.join(' ');
|
|
146
208
|
const input = element instanceof HTMLInputElement ? element : null;
|
|
147
209
|
const formControl = element instanceof HTMLInputElement ||
|
|
148
210
|
element instanceof HTMLSelectElement ||
|
|
@@ -151,49 +213,208 @@ const nameFor = (element) => {
|
|
|
151
213
|
: null;
|
|
152
214
|
const associatedLabel = formControl
|
|
153
215
|
? Array.from(formControl.labels ?? [])
|
|
154
|
-
.
|
|
155
|
-
|
|
156
|
-
const text = [];
|
|
157
|
-
let node = walker.nextNode();
|
|
158
|
-
while (node) {
|
|
159
|
-
if (!node.parentElement?.closest('input,select,textarea,option,button')) {
|
|
160
|
-
text.push(node.textContent ?? '');
|
|
161
|
-
}
|
|
162
|
-
node = walker.nextNode();
|
|
163
|
-
}
|
|
164
|
-
return text;
|
|
165
|
-
})
|
|
216
|
+
.map((label) => safeAccessibleNameText(label, 300))
|
|
217
|
+
.filter(Boolean)
|
|
166
218
|
.join(' ')
|
|
167
219
|
: '';
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
element
|
|
171
|
-
|
|
172
|
-
|
|
220
|
+
const safeAttribute = (attribute, maximum) => {
|
|
221
|
+
const value = normalize(element.getAttribute(attribute) ?? '', maximum);
|
|
222
|
+
return value && sensitivityFor(element, value) === 'none' ? value : '';
|
|
223
|
+
};
|
|
224
|
+
const semanticName = safeAccessibleNameText(element, 300);
|
|
225
|
+
const attributeName = safeAttribute('alt', 300) ||
|
|
226
|
+
safeAttribute('title', 300) ||
|
|
227
|
+
safeAttribute('placeholder', 300);
|
|
173
228
|
if (element instanceof HTMLFormElement) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
229
|
+
const explicitSemanticName = element.hasAttribute('aria-label') || element.hasAttribute('aria-labelledby')
|
|
230
|
+
? semanticName
|
|
231
|
+
: '';
|
|
232
|
+
return normalize(explicitSemanticName ||
|
|
233
|
+
attributeName ||
|
|
234
|
+
safeAttribute('data-oxvo-ai-region', 120) ||
|
|
235
|
+
safeAttribute('name', 120) ||
|
|
177
236
|
'Form', 120);
|
|
178
237
|
}
|
|
179
|
-
|
|
180
|
-
(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
238
|
+
const buttonValue = input && ['button', 'submit', 'reset'].includes(input.type)
|
|
239
|
+
? normalize(input.value, 300)
|
|
240
|
+
: '';
|
|
241
|
+
return normalize(semanticName ||
|
|
242
|
+
associatedLabel ||
|
|
243
|
+
attributeName ||
|
|
244
|
+
(buttonValue && sensitivityFor(element, buttonValue) === 'none' ? buttonValue : ''), 300);
|
|
245
|
+
};
|
|
246
|
+
const safeDescendantText = (element, maximum, renderedOnly = false) => {
|
|
247
|
+
const parts = [];
|
|
248
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
249
|
+
let node = walker.nextNode();
|
|
250
|
+
while (node) {
|
|
251
|
+
const parent = node.parentElement;
|
|
252
|
+
const text = normalize(node.textContent ?? '', maximum);
|
|
253
|
+
if (parent &&
|
|
254
|
+
text &&
|
|
255
|
+
!parent.closest(PRIVATE_SELECTOR) &&
|
|
256
|
+
!parent.closest('script,style,noscript,template,svg,input,select,textarea,option') &&
|
|
257
|
+
(renderedOnly ? isRendered(parent) : isVisible(parent)) &&
|
|
258
|
+
sensitivityFor(parent, text) === 'none') {
|
|
259
|
+
parts.push(text);
|
|
260
|
+
}
|
|
261
|
+
node = walker.nextNode();
|
|
262
|
+
}
|
|
263
|
+
return normalize(parts.join(' '), maximum);
|
|
264
|
+
};
|
|
265
|
+
const safeSemanticText = (element, maximum) => {
|
|
266
|
+
if (!isVisible(element) || element.closest(PRIVATE_SELECTOR))
|
|
267
|
+
return '';
|
|
268
|
+
const explicit = normalize(element.getAttribute('aria-label') ?? '', maximum);
|
|
269
|
+
if (explicit) {
|
|
270
|
+
return sensitivityFor(element, explicit) === 'none' ? explicit : '';
|
|
271
|
+
}
|
|
272
|
+
const labelledBy = element.getAttribute('aria-labelledby');
|
|
273
|
+
const labelledParts = labelledBy
|
|
274
|
+
? labelledBy
|
|
275
|
+
.split(/\s+/)
|
|
276
|
+
.map((idValue) => document.getElementById(idValue))
|
|
277
|
+
.filter((label) => Boolean(label))
|
|
278
|
+
.flatMap((label) => {
|
|
279
|
+
if (!isVisible(label) || label.closest(PRIVATE_SELECTOR))
|
|
280
|
+
return [];
|
|
281
|
+
const text = safeDescendantText(label, maximum);
|
|
282
|
+
return text && sensitivityFor(label, text) === 'none' ? [text] : [];
|
|
283
|
+
})
|
|
284
|
+
: [];
|
|
285
|
+
if (labelledParts.length > 0) {
|
|
286
|
+
const text = normalize(labelledParts.join(' '), maximum);
|
|
287
|
+
return sensitivityFor(element, text) === 'none' ? text : '';
|
|
288
|
+
}
|
|
289
|
+
const text = safeDescendantText(element, maximum);
|
|
290
|
+
return text && sensitivityFor(element, text) === 'none' ? text : '';
|
|
291
|
+
};
|
|
292
|
+
const safeAccessibleNameText = (element, maximum) => {
|
|
293
|
+
if (!isRendered(element) || element.closest(PRIVATE_SELECTOR))
|
|
294
|
+
return '';
|
|
295
|
+
const explicit = normalize(element.getAttribute('aria-label') ?? '', maximum);
|
|
296
|
+
if (explicit) {
|
|
297
|
+
return sensitivityFor(element, explicit) === 'none' ? explicit : '';
|
|
298
|
+
}
|
|
299
|
+
const labelledBy = element.getAttribute('aria-labelledby');
|
|
300
|
+
const labelledParts = labelledBy
|
|
301
|
+
? labelledBy
|
|
302
|
+
.split(/\s+/)
|
|
303
|
+
.map((idValue) => document.getElementById(idValue))
|
|
304
|
+
.filter((label) => Boolean(label))
|
|
305
|
+
.flatMap((label) => {
|
|
306
|
+
if (!isRendered(label) || label.closest(PRIVATE_SELECTOR))
|
|
307
|
+
return [];
|
|
308
|
+
const text = safeDescendantText(label, maximum, true);
|
|
309
|
+
return text && sensitivityFor(label, text) === 'none' ? [text] : [];
|
|
310
|
+
})
|
|
311
|
+
: [];
|
|
312
|
+
if (labelledParts.length > 0) {
|
|
313
|
+
const text = normalize(labelledParts.join(' '), maximum);
|
|
314
|
+
return sensitivityFor(element, text) === 'none' ? text : '';
|
|
315
|
+
}
|
|
316
|
+
const text = safeDescendantText(element, maximum, true);
|
|
317
|
+
return text && sensitivityFor(element, text) === 'none' ? text : '';
|
|
318
|
+
};
|
|
319
|
+
const semanticSourceSensitivity = (source) => {
|
|
320
|
+
if (source.closest(PRIVATE_SELECTOR) || source.querySelector(PRIVATE_SELECTOR)) {
|
|
321
|
+
return 'private';
|
|
322
|
+
}
|
|
323
|
+
const markedSources = [
|
|
324
|
+
...(source.matches('[data-oxvo-sensitivity]') ? [source] : []),
|
|
325
|
+
...Array.from(source.querySelectorAll('[data-oxvo-sensitivity]')),
|
|
326
|
+
];
|
|
327
|
+
for (const marked of markedSources) {
|
|
328
|
+
const marker = normalize(marked.getAttribute('data-oxvo-sensitivity') ?? '', 64);
|
|
329
|
+
if (EXPLICIT_SENSITIVITIES.has(marker))
|
|
330
|
+
return marker;
|
|
331
|
+
const descriptor = normalize(`${marker} ${marked.textContent ?? ''}`, 500);
|
|
332
|
+
for (const [sensitivity, pattern] of SENSITIVE_PATTERNS) {
|
|
333
|
+
if (pattern.test(descriptor))
|
|
334
|
+
return sensitivity;
|
|
335
|
+
}
|
|
336
|
+
return 'private';
|
|
337
|
+
}
|
|
338
|
+
const text = normalize(`${source.getAttribute('label') ?? ''} ${source.textContent ?? ''}`, 500);
|
|
339
|
+
return sensitivityFor(source, text);
|
|
340
|
+
};
|
|
341
|
+
const accessibleNameSourceSensitivity = (element) => {
|
|
342
|
+
const labelledBy = element.getAttribute('aria-labelledby');
|
|
343
|
+
const referencedLabels = labelledBy
|
|
344
|
+
? labelledBy
|
|
345
|
+
.split(/\s+/)
|
|
346
|
+
.map((idValue) => document.getElementById(idValue))
|
|
347
|
+
.filter((label) => Boolean(label))
|
|
348
|
+
: [];
|
|
349
|
+
const formControl = element instanceof HTMLInputElement ||
|
|
350
|
+
element instanceof HTMLSelectElement ||
|
|
351
|
+
element instanceof HTMLTextAreaElement
|
|
352
|
+
? element
|
|
353
|
+
: null;
|
|
354
|
+
const sources = [
|
|
355
|
+
...referencedLabels,
|
|
356
|
+
...Array.from(formControl?.labels ?? []),
|
|
357
|
+
];
|
|
358
|
+
for (const source of sources) {
|
|
359
|
+
const sensitivity = semanticSourceSensitivity(source);
|
|
360
|
+
if (sensitivity !== 'none')
|
|
361
|
+
return sensitivity;
|
|
362
|
+
}
|
|
363
|
+
return 'none';
|
|
364
|
+
};
|
|
365
|
+
const safeOptionLabel = (option) => {
|
|
366
|
+
const label = normalize(option.label || option.textContent || '', 300);
|
|
367
|
+
if (!label || semanticSourceSensitivity(option) !== 'none')
|
|
368
|
+
return '';
|
|
369
|
+
const group = option.closest('optgroup');
|
|
370
|
+
if (group && semanticSourceSensitivity(group) !== 'none')
|
|
371
|
+
return '';
|
|
372
|
+
return sensitivityFor(option, label) === 'none' ? label : '';
|
|
184
373
|
};
|
|
185
374
|
const isActionTarget = (element) => element.matches('a[href],button,input,select,textarea,summary,form,[contenteditable="true"],[role="button"],[role="link"],[role="checkbox"],[role="radio"],[role="combobox"],[role="textbox"],[tabindex]');
|
|
375
|
+
const READONLY_INPUT_TYPES = new Set([
|
|
376
|
+
'date',
|
|
377
|
+
'datetime-local',
|
|
378
|
+
'email',
|
|
379
|
+
'month',
|
|
380
|
+
'number',
|
|
381
|
+
'password',
|
|
382
|
+
'search',
|
|
383
|
+
'tel',
|
|
384
|
+
'text',
|
|
385
|
+
'time',
|
|
386
|
+
'url',
|
|
387
|
+
'week',
|
|
388
|
+
]);
|
|
389
|
+
const isReadOnlyTextControl = (element) => (element instanceof HTMLInputElement &&
|
|
390
|
+
READONLY_INPUT_TYPES.has(element.type) &&
|
|
391
|
+
element.readOnly) ||
|
|
392
|
+
(element instanceof HTMLTextAreaElement && element.readOnly);
|
|
186
393
|
const enabled = (element) => {
|
|
187
|
-
if (element.getAttribute('aria-disabled') === 'true'
|
|
394
|
+
if (element.getAttribute('aria-disabled') === 'true' ||
|
|
395
|
+
element.getAttribute('aria-readonly') === 'true' ||
|
|
396
|
+
isReadOnlyTextControl(element)) {
|
|
188
397
|
return false;
|
|
398
|
+
}
|
|
189
399
|
if (element instanceof HTMLButtonElement ||
|
|
190
400
|
element instanceof HTMLInputElement ||
|
|
191
401
|
element instanceof HTMLSelectElement ||
|
|
192
402
|
element instanceof HTMLTextAreaElement) {
|
|
193
|
-
return !element.disabled;
|
|
403
|
+
return !element.matches(':disabled');
|
|
194
404
|
}
|
|
195
405
|
return true;
|
|
196
406
|
};
|
|
407
|
+
const optionDisabled = (option) => option.disabled ||
|
|
408
|
+
option.closest('optgroup')?.disabled === true ||
|
|
409
|
+
option.matches(':disabled');
|
|
410
|
+
const literalBooleanAttribute = (element, attribute) => {
|
|
411
|
+
const value = element.getAttribute(attribute);
|
|
412
|
+
if (value === 'true')
|
|
413
|
+
return true;
|
|
414
|
+
if (value === 'false')
|
|
415
|
+
return false;
|
|
416
|
+
return undefined;
|
|
417
|
+
};
|
|
197
418
|
const consequenceFor = (element, role, name) => {
|
|
198
419
|
const form = element instanceof HTMLFormElement ? element : element.closest('form');
|
|
199
420
|
const explicit = element
|
|
@@ -244,10 +465,12 @@ const consequenceFor = (element, role, name) => {
|
|
|
244
465
|
return role === 'button' || role === 'checkbox' || role === 'radio' ? 'state_change' : 'none';
|
|
245
466
|
};
|
|
246
467
|
export class PageContextCollector {
|
|
247
|
-
constructor(callbacks, selectorRegionRules = [], policyVersion = 1) {
|
|
468
|
+
constructor(callbacks, selectorRegionRules = [], policyVersion = 1, trustedGuideRoot = null, statefulButtonActionProof = false) {
|
|
248
469
|
this.callbacks = callbacks;
|
|
249
470
|
this.selectorRegionRules = selectorRegionRules;
|
|
250
471
|
this.policyVersion = policyVersion;
|
|
472
|
+
this.trustedGuideRoot = trustedGuideRoot;
|
|
473
|
+
this.statefulButtonActionProof = statefulButtonActionProof;
|
|
251
474
|
this.pageId = id('page');
|
|
252
475
|
this.revision = 0;
|
|
253
476
|
this.running = false;
|
|
@@ -264,10 +487,12 @@ export class PageContextCollector {
|
|
|
264
487
|
this.pendingNavigation = null;
|
|
265
488
|
this.pendingCapture = false;
|
|
266
489
|
this.deferredNavigation = null;
|
|
490
|
+
this.navigationSettlementGeneration = 0;
|
|
491
|
+
this.navigationSettlement = null;
|
|
267
492
|
this.recentSelection = null;
|
|
268
493
|
this.latestSemanticSignature = null;
|
|
269
494
|
this.scheduleCapture = () => {
|
|
270
|
-
if (!this.running)
|
|
495
|
+
if (!this.running || this.navigationSettlement)
|
|
271
496
|
return;
|
|
272
497
|
if (this.executingActionId) {
|
|
273
498
|
this.pendingCapture = true;
|
|
@@ -284,7 +509,7 @@ export class PageContextCollector {
|
|
|
284
509
|
}, CAPTURE_SETTLE_MS);
|
|
285
510
|
};
|
|
286
511
|
this.scheduleMutationCapture = () => {
|
|
287
|
-
if (!this.running)
|
|
512
|
+
if (!this.running || this.navigationSettlement)
|
|
288
513
|
return;
|
|
289
514
|
if (this.executingActionId) {
|
|
290
515
|
this.pendingCapture = true;
|
|
@@ -325,6 +550,14 @@ export class PageContextCollector {
|
|
|
325
550
|
this.handleNavigation = () => {
|
|
326
551
|
if (!this.running)
|
|
327
552
|
return;
|
|
553
|
+
const currentUrl = safePageUrl();
|
|
554
|
+
if (this.executingActionId &&
|
|
555
|
+
this.pendingNavigation?.causalActionId === this.executingActionId &&
|
|
556
|
+
this.pendingNavigation.url === currentUrl) {
|
|
557
|
+
this.pendingCapture = true;
|
|
558
|
+
this.reportCausalNavigationObservation();
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
328
561
|
this.pageId = id('page');
|
|
329
562
|
this.revision += 1;
|
|
330
563
|
this.targets.clear();
|
|
@@ -333,12 +566,27 @@ export class PageContextCollector {
|
|
|
333
566
|
this.pendingNavigation = {
|
|
334
567
|
pageId: this.pageId,
|
|
335
568
|
revision: this.revision,
|
|
336
|
-
url:
|
|
569
|
+
url: currentUrl,
|
|
570
|
+
causalActionId: this.executingActionId,
|
|
571
|
+
causalObservationReported: false,
|
|
337
572
|
};
|
|
338
573
|
this.pendingCapture = true;
|
|
574
|
+
this.reportCausalNavigationObservation();
|
|
339
575
|
return;
|
|
340
576
|
}
|
|
341
|
-
|
|
577
|
+
const settlement = this.beginNavigationSettlement(this.pageId, this.revision, currentUrl);
|
|
578
|
+
void Promise.resolve()
|
|
579
|
+
.then(() => this.callbacks.onNavigation(settlement.pageId, settlement.revision, settlement.url))
|
|
580
|
+
.then((navigationAcknowledged) => {
|
|
581
|
+
if (navigationAcknowledged === false ||
|
|
582
|
+
!this.running ||
|
|
583
|
+
this.navigationSettlement !== settlement) {
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
this.navigationSettlement = null;
|
|
587
|
+
return this.captureAndPublish();
|
|
588
|
+
})
|
|
589
|
+
.catch(() => undefined);
|
|
342
590
|
};
|
|
343
591
|
}
|
|
344
592
|
get currentRevision() {
|
|
@@ -385,6 +633,7 @@ export class PageContextCollector {
|
|
|
385
633
|
'aria-label',
|
|
386
634
|
'aria-disabled',
|
|
387
635
|
'aria-expanded',
|
|
636
|
+
'aria-pressed',
|
|
388
637
|
'aria-live',
|
|
389
638
|
'class',
|
|
390
639
|
'disabled',
|
|
@@ -439,14 +688,19 @@ export class PageContextCollector {
|
|
|
439
688
|
this.pendingNavigation = null;
|
|
440
689
|
this.pendingCapture = false;
|
|
441
690
|
this.deferredNavigation = null;
|
|
691
|
+
this.navigationSettlementGeneration += 1;
|
|
692
|
+
this.navigationSettlement = null;
|
|
442
693
|
this.recentSelection = null;
|
|
443
694
|
this.latestSemanticSignature = null;
|
|
444
695
|
this.callbacks.onHighlight(null, 'focus');
|
|
445
696
|
}
|
|
446
697
|
async captureAndPublish() {
|
|
698
|
+
if (this.navigationSettlement)
|
|
699
|
+
return null;
|
|
447
700
|
const snapshot = this.capture();
|
|
448
|
-
if (this.running)
|
|
701
|
+
if (this.running) {
|
|
449
702
|
await this.callbacks.onSnapshot(snapshot);
|
|
703
|
+
}
|
|
450
704
|
return snapshot;
|
|
451
705
|
}
|
|
452
706
|
capture() {
|
|
@@ -465,8 +719,17 @@ export class PageContextCollector {
|
|
|
465
719
|
}
|
|
466
720
|
if (!isActionTarget(element) || !isRendered(element))
|
|
467
721
|
continue;
|
|
722
|
+
const role = roleFor(element);
|
|
723
|
+
const hasStatefulButtonAttribute = role === 'button' &&
|
|
724
|
+
(element.hasAttribute('aria-expanded') || element.hasAttribute('aria-pressed'));
|
|
725
|
+
const hasUnsupportedMixedPressedState = role === 'button' &&
|
|
726
|
+
element.getAttribute('aria-pressed')?.trim().toLowerCase() === 'mixed';
|
|
727
|
+
if (hasUnsupportedMixedPressedState ||
|
|
728
|
+
(!this.statefulButtonActionProof && hasStatefulButtonAttribute)) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
468
731
|
const name = nameFor(element);
|
|
469
|
-
if (!name && !['form', 'textbox'].includes(
|
|
732
|
+
if (!name && !['form', 'textbox'].includes(role))
|
|
470
733
|
continue;
|
|
471
734
|
const targetId = this.targetIds.get(element) ?? id('target');
|
|
472
735
|
this.targetIds.set(element, targetId);
|
|
@@ -475,15 +738,18 @@ export class PageContextCollector {
|
|
|
475
738
|
if (element instanceof HTMLSelectElement) {
|
|
476
739
|
options = Array.from(element.options)
|
|
477
740
|
.slice(0, MAX_OPTIONS)
|
|
478
|
-
.
|
|
741
|
+
.flatMap((option) => {
|
|
742
|
+
const label = safeOptionLabel(option);
|
|
743
|
+
if (!label)
|
|
744
|
+
return [];
|
|
479
745
|
const optionId = this.optionIds.get(option) ?? id('option');
|
|
480
746
|
this.optionIds.set(option, optionId);
|
|
481
747
|
optionMap.set(optionId, option);
|
|
482
|
-
return {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
748
|
+
return [{
|
|
749
|
+
id: optionId,
|
|
750
|
+
label,
|
|
751
|
+
disabled: optionDisabled(option),
|
|
752
|
+
}];
|
|
487
753
|
});
|
|
488
754
|
}
|
|
489
755
|
const semanticRegion = normalize(element.closest('[data-oxvo-ai-region]')?.getAttribute('data-oxvo-ai-region') ?? '', 128);
|
|
@@ -503,10 +769,14 @@ export class PageContextCollector {
|
|
|
503
769
|
// Invalid tenant selectors remain unevaluated so server policy fails closed.
|
|
504
770
|
}
|
|
505
771
|
}
|
|
506
|
-
const
|
|
507
|
-
const sensitivity =
|
|
772
|
+
const directSensitivity = sensitivityFor(element, name);
|
|
773
|
+
const sensitivity = directSensitivity !== 'none'
|
|
774
|
+
? directSensitivity
|
|
775
|
+
: accessibleNameSourceSensitivity(element);
|
|
508
776
|
const visible = isVisible(element);
|
|
509
777
|
const destinationPath = safeDestinationPath(element);
|
|
778
|
+
const expanded = literalBooleanAttribute(element, 'aria-expanded');
|
|
779
|
+
const pressed = literalBooleanAttribute(element, 'aria-pressed');
|
|
510
780
|
const target = {
|
|
511
781
|
targetId,
|
|
512
782
|
revision,
|
|
@@ -532,10 +802,12 @@ export class PageContextCollector {
|
|
|
532
802
|
state: {
|
|
533
803
|
visible,
|
|
534
804
|
enabled: enabled(element),
|
|
535
|
-
covered: visible ? isCovered(element) : false,
|
|
805
|
+
covered: visible ? isCovered(element, this.trustedGuideRoot) : false,
|
|
536
806
|
...(element instanceof HTMLInputElement && ['checkbox', 'radio'].includes(element.type)
|
|
537
807
|
? { checked: element.checked }
|
|
538
808
|
: {}),
|
|
809
|
+
...(this.statefulButtonActionProof && expanded !== undefined ? { expanded } : {}),
|
|
810
|
+
...(this.statefulButtonActionProof && pressed !== undefined ? { pressed } : {}),
|
|
539
811
|
},
|
|
540
812
|
...(options ? { options } : {}),
|
|
541
813
|
};
|
|
@@ -568,11 +840,12 @@ export class PageContextCollector {
|
|
|
568
840
|
const landmarks = Array.from(document.querySelectorAll('main,nav,header,footer,aside,[role="main"],[role="navigation"]'))
|
|
569
841
|
.filter(isVisible)
|
|
570
842
|
.slice(0, 32)
|
|
571
|
-
.map((element) => ({ role: roleFor(element), label:
|
|
843
|
+
.map((element) => ({ role: roleFor(element), label: safeSemanticText(element, 300) }))
|
|
844
|
+
.filter(({ label }) => Boolean(label));
|
|
572
845
|
const alerts = Array.from(document.querySelectorAll('[role="alert"],[aria-live]'))
|
|
573
846
|
.filter(isVisible)
|
|
574
847
|
.slice(0, 16)
|
|
575
|
-
.map((element) =>
|
|
848
|
+
.map((element) => safeSemanticText(element, 500))
|
|
576
849
|
.filter(Boolean);
|
|
577
850
|
const activeSelection = this.readSafeSelection();
|
|
578
851
|
if (activeSelection)
|
|
@@ -609,6 +882,8 @@ export class PageContextCollector {
|
|
|
609
882
|
return snapshot;
|
|
610
883
|
}
|
|
611
884
|
captureVisualContext() {
|
|
885
|
+
if (this.navigationSettlement)
|
|
886
|
+
return null;
|
|
612
887
|
if (this.revision < 1 || this.targets.size === 0)
|
|
613
888
|
return null;
|
|
614
889
|
const viewportWidth = Math.max(1, Math.round(window.innerWidth));
|
|
@@ -647,14 +922,81 @@ export class PageContextCollector {
|
|
|
647
922
|
regions,
|
|
648
923
|
});
|
|
649
924
|
}
|
|
650
|
-
async execute(grant) {
|
|
651
|
-
const handle = this.targets.get(grant.targetId);
|
|
925
|
+
async execute(grant, authorityIsCurrent = () => true) {
|
|
652
926
|
const base = {
|
|
653
927
|
grant: grant.grant,
|
|
654
928
|
actionId: grant.actionId,
|
|
655
929
|
};
|
|
656
|
-
|
|
657
|
-
|
|
930
|
+
const authorityStale = () => ({
|
|
931
|
+
...base,
|
|
932
|
+
status: 'stale',
|
|
933
|
+
observed: {
|
|
934
|
+
url: safePageUrl(),
|
|
935
|
+
revision: this.revision,
|
|
936
|
+
targetState: 'unchanged',
|
|
937
|
+
safeSummary: 'Page assistance permission changed before execution.',
|
|
938
|
+
},
|
|
939
|
+
});
|
|
940
|
+
const expiresAt = Date.parse(grant.expiresAt);
|
|
941
|
+
const approvalExpired = () => ({
|
|
942
|
+
...base,
|
|
943
|
+
status: 'stale',
|
|
944
|
+
observed: {
|
|
945
|
+
url: safePageUrl(),
|
|
946
|
+
revision: this.revision,
|
|
947
|
+
targetState: 'unchanged',
|
|
948
|
+
safeSummary: 'The approved page action expired before execution.',
|
|
949
|
+
},
|
|
950
|
+
});
|
|
951
|
+
const executionStale = () => {
|
|
952
|
+
if (!authorityIsCurrent())
|
|
953
|
+
return authorityStale();
|
|
954
|
+
return expiresAt <= Date.now() ? approvalExpired() : null;
|
|
955
|
+
};
|
|
956
|
+
const initialStale = executionStale();
|
|
957
|
+
if (initialStale)
|
|
958
|
+
return initialStale;
|
|
959
|
+
this.captureSnapshot(false);
|
|
960
|
+
let handle = this.targets.get(grant.targetId);
|
|
961
|
+
const revisionMatchesOrStableNavigation = grant.domRevision === this.revision ||
|
|
962
|
+
(grant.domRevision < this.revision &&
|
|
963
|
+
grant.action === 'click' &&
|
|
964
|
+
handle?.element instanceof HTMLAnchorElement &&
|
|
965
|
+
handle.target.consequence === 'navigation' &&
|
|
966
|
+
typeof handle.target.destinationPath === 'string');
|
|
967
|
+
if (expiresAt <= Date.now() ||
|
|
968
|
+
grant.pageId !== this.pageId ||
|
|
969
|
+
grant.pageUrl !== safePageUrl() ||
|
|
970
|
+
grant.policyVersion !== this.policyVersion ||
|
|
971
|
+
!revisionMatchesOrStableNavigation ||
|
|
972
|
+
!handle ||
|
|
973
|
+
handle.revision !== this.revision ||
|
|
974
|
+
!handle.element.isConnected) {
|
|
975
|
+
return {
|
|
976
|
+
...base,
|
|
977
|
+
status: 'stale',
|
|
978
|
+
observed: {
|
|
979
|
+
url: safePageUrl(),
|
|
980
|
+
revision: this.revision,
|
|
981
|
+
targetState: 'detached',
|
|
982
|
+
safeSummary: 'The requested target changed before execution.',
|
|
983
|
+
},
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
const fingerprintedHandle = handle;
|
|
987
|
+
const fingerprint = await actionTargetFingerprint(handle.target);
|
|
988
|
+
const postFingerprintStale = executionStale();
|
|
989
|
+
if (postFingerprintStale)
|
|
990
|
+
return postFingerprintStale;
|
|
991
|
+
this.captureSnapshot(false);
|
|
992
|
+
handle = this.targets.get(grant.targetId);
|
|
993
|
+
if (!fingerprint ||
|
|
994
|
+
fingerprint !== grant.targetFingerprint ||
|
|
995
|
+
grant.pageId !== this.pageId ||
|
|
996
|
+
grant.pageUrl !== safePageUrl() ||
|
|
997
|
+
grant.policyVersion !== this.policyVersion ||
|
|
998
|
+
!revisionMatchesOrStableNavigation ||
|
|
999
|
+
handle !== fingerprintedHandle ||
|
|
658
1000
|
!handle ||
|
|
659
1001
|
handle.revision !== this.revision ||
|
|
660
1002
|
!handle.element.isConnected) {
|
|
@@ -680,7 +1022,9 @@ export class PageContextCollector {
|
|
|
680
1022
|
!isRendered(element) ||
|
|
681
1023
|
(grant.action !== 'scroll' && !isVisible(element) && !canRevealDesiredStateTarget) ||
|
|
682
1024
|
!enabled(element) ||
|
|
683
|
-
(grant.action !== 'scroll' &&
|
|
1025
|
+
(grant.action !== 'scroll' &&
|
|
1026
|
+
isVisible(element) &&
|
|
1027
|
+
isCovered(element, this.trustedGuideRoot))) {
|
|
684
1028
|
return {
|
|
685
1029
|
...base,
|
|
686
1030
|
status: 'blocked',
|
|
@@ -693,9 +1037,17 @@ export class PageContextCollector {
|
|
|
693
1037
|
};
|
|
694
1038
|
}
|
|
695
1039
|
if (!isVisible(element) && canRevealDesiredStateTarget) {
|
|
1040
|
+
const preRevealStale = executionStale();
|
|
1041
|
+
if (preRevealStale)
|
|
1042
|
+
return preRevealStale;
|
|
696
1043
|
element.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
697
1044
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
698
|
-
|
|
1045
|
+
const postRevealStale = executionStale();
|
|
1046
|
+
if (postRevealStale)
|
|
1047
|
+
return postRevealStale;
|
|
1048
|
+
if (!isRendered(element) ||
|
|
1049
|
+
!isVisible(element) ||
|
|
1050
|
+
isCovered(element, this.trustedGuideRoot)) {
|
|
699
1051
|
return {
|
|
700
1052
|
...base,
|
|
701
1053
|
status: 'blocked',
|
|
@@ -708,11 +1060,18 @@ export class PageContextCollector {
|
|
|
708
1060
|
};
|
|
709
1061
|
}
|
|
710
1062
|
}
|
|
1063
|
+
const preHighlightStale = executionStale();
|
|
1064
|
+
if (preHighlightStale)
|
|
1065
|
+
return preHighlightStale;
|
|
711
1066
|
const before = this.safeState(element);
|
|
1067
|
+
const beforeActionUrl = safePageUrl();
|
|
1068
|
+
this.callbacks.onHighlight(element, 'focus');
|
|
1069
|
+
const preMutationStale = executionStale();
|
|
1070
|
+
if (preMutationStale)
|
|
1071
|
+
return preMutationStale;
|
|
712
1072
|
this.executingActionId = grant.actionId;
|
|
713
1073
|
this.pendingCapture = false;
|
|
714
1074
|
this.deferredNavigation = null;
|
|
715
|
-
this.callbacks.onHighlight(element, 'focus');
|
|
716
1075
|
try {
|
|
717
1076
|
switch (grant.action) {
|
|
718
1077
|
case 'scroll':
|
|
@@ -729,7 +1088,7 @@ export class PageContextCollector {
|
|
|
729
1088
|
element.focus({ preventScroll: false });
|
|
730
1089
|
break;
|
|
731
1090
|
case 'click':
|
|
732
|
-
this.click(element, grant.actionId, grant.desiredChecked,
|
|
1091
|
+
this.click(element, grant.actionId, grant.desiredChecked, grant.desiredExpanded, grant.desiredPressed, grant.destinationPath);
|
|
733
1092
|
break;
|
|
734
1093
|
case 'type':
|
|
735
1094
|
this.type(element, grant.value ?? '');
|
|
@@ -741,13 +1100,29 @@ export class PageContextCollector {
|
|
|
741
1100
|
this.submit(element);
|
|
742
1101
|
break;
|
|
743
1102
|
}
|
|
1103
|
+
if (['click', 'type', 'select', 'submit'].includes(grant.action)) {
|
|
1104
|
+
this.callbacks.onActionMutationApplied?.(grant.actionId, this.revision, safePageUrl());
|
|
1105
|
+
}
|
|
1106
|
+
if (grant.action === 'click' &&
|
|
1107
|
+
typeof grant.destinationPath === 'string') {
|
|
1108
|
+
this.observeCausalNavigationAfterMutation(grant.actionId, grant.destinationPath, beforeActionUrl);
|
|
1109
|
+
}
|
|
744
1110
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
1111
|
+
const postActionStale = executionStale();
|
|
1112
|
+
if (postActionStale)
|
|
1113
|
+
return postActionStale;
|
|
745
1114
|
if (!this.actionApplied(grant, element, handle)) {
|
|
746
1115
|
throw new Error('The approved value was not applied.');
|
|
747
1116
|
}
|
|
748
1117
|
const snapshot = this.capture();
|
|
749
1118
|
const attached = element.isConnected;
|
|
750
1119
|
const after = attached ? this.safeState(element) : '';
|
|
1120
|
+
const expanded = attached
|
|
1121
|
+
? literalBooleanAttribute(element, 'aria-expanded')
|
|
1122
|
+
: undefined;
|
|
1123
|
+
const pressed = attached
|
|
1124
|
+
? literalBooleanAttribute(element, 'aria-pressed')
|
|
1125
|
+
: undefined;
|
|
751
1126
|
this.callbacks.onHighlight(attached ? element : null, 'success');
|
|
752
1127
|
return {
|
|
753
1128
|
...base,
|
|
@@ -766,10 +1141,14 @@ export class PageContextCollector {
|
|
|
766
1141
|
? 'The requested option was applied to the approved field.'
|
|
767
1142
|
: grant.desiredChecked !== undefined
|
|
768
1143
|
? 'The requested checkbox state was applied.'
|
|
769
|
-
:
|
|
1144
|
+
: grant.desiredExpanded !== undefined || grant.desiredPressed !== undefined
|
|
1145
|
+
? 'The requested button state was applied.'
|
|
1146
|
+
: `${grant.action} executed on the approved ${roleFor(element)} target.`,
|
|
770
1147
|
...(element instanceof HTMLInputElement && ['checkbox', 'radio'].includes(element.type)
|
|
771
1148
|
? { checked: element.checked }
|
|
772
1149
|
: {}),
|
|
1150
|
+
...(this.statefulButtonActionProof && expanded !== undefined ? { expanded } : {}),
|
|
1151
|
+
...(this.statefulButtonActionProof && pressed !== undefined ? { pressed } : {}),
|
|
773
1152
|
},
|
|
774
1153
|
};
|
|
775
1154
|
}
|
|
@@ -795,10 +1174,25 @@ export class PageContextCollector {
|
|
|
795
1174
|
this.pendingNavigation = null;
|
|
796
1175
|
const shouldCapture = this.pendingCapture || Boolean(navigation);
|
|
797
1176
|
this.pendingCapture = false;
|
|
1177
|
+
let navigationAcknowledged = true;
|
|
1178
|
+
const settlement = navigation && this.running
|
|
1179
|
+
? this.beginNavigationSettlement(navigation.pageId, Math.max(navigation.revision, this.revision), navigation.url)
|
|
1180
|
+
: null;
|
|
798
1181
|
if (navigation && this.running) {
|
|
799
|
-
|
|
1182
|
+
try {
|
|
1183
|
+
navigationAcknowledged =
|
|
1184
|
+
(await this.callbacks.onNavigation(navigation.pageId, Math.max(navigation.revision, this.revision), navigation.url, navigation.causalActionId)) !== false;
|
|
1185
|
+
}
|
|
1186
|
+
catch {
|
|
1187
|
+
navigationAcknowledged = false;
|
|
1188
|
+
}
|
|
800
1189
|
}
|
|
801
|
-
if (this.running &&
|
|
1190
|
+
if (this.running &&
|
|
1191
|
+
navigationAcknowledged &&
|
|
1192
|
+
(!settlement || this.navigationSettlement === settlement) &&
|
|
1193
|
+
(shouldCapture || !navigation)) {
|
|
1194
|
+
if (settlement)
|
|
1195
|
+
this.navigationSettlement = null;
|
|
802
1196
|
await this.captureAndPublish();
|
|
803
1197
|
}
|
|
804
1198
|
const deferred = this.deferredNavigation;
|
|
@@ -807,19 +1201,39 @@ export class PageContextCollector {
|
|
|
807
1201
|
? deferred.toUrl
|
|
808
1202
|
: null;
|
|
809
1203
|
}
|
|
810
|
-
click(element, actionId, desiredChecked, expectedDestinationPath) {
|
|
1204
|
+
click(element, actionId, desiredChecked, desiredExpanded, desiredPressed, expectedDestinationPath) {
|
|
811
1205
|
if (!(element instanceof HTMLElement))
|
|
812
1206
|
throw new Error('Target is not clickable.');
|
|
1207
|
+
if (desiredChecked !== undefined &&
|
|
1208
|
+
(desiredExpanded !== undefined || desiredPressed !== undefined)) {
|
|
1209
|
+
throw new Error('Conflicting desired-state contracts are not allowed.');
|
|
1210
|
+
}
|
|
1211
|
+
const desiredStates = [];
|
|
813
1212
|
if (desiredChecked !== undefined) {
|
|
814
1213
|
if (!(element instanceof HTMLInputElement) || !['checkbox', 'radio'].includes(element.type)) {
|
|
815
1214
|
throw new Error('Desired checked state requires a checkbox or radio target.');
|
|
816
1215
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
if (element.type === 'radio' && !desiredChecked) {
|
|
1216
|
+
desiredStates.push(element.checked === desiredChecked);
|
|
1217
|
+
if (element.type === 'radio' && !desiredChecked && element.checked !== desiredChecked) {
|
|
820
1218
|
throw new Error('A radio target cannot be cleared by clicking it.');
|
|
821
1219
|
}
|
|
822
1220
|
}
|
|
1221
|
+
if (desiredExpanded !== undefined) {
|
|
1222
|
+
const expanded = literalBooleanAttribute(element, 'aria-expanded');
|
|
1223
|
+
if (roleFor(element) !== 'button' || expanded === undefined) {
|
|
1224
|
+
throw new Error('Desired expanded state requires an expandable button target.');
|
|
1225
|
+
}
|
|
1226
|
+
desiredStates.push(expanded === desiredExpanded);
|
|
1227
|
+
}
|
|
1228
|
+
if (desiredPressed !== undefined) {
|
|
1229
|
+
const pressed = literalBooleanAttribute(element, 'aria-pressed');
|
|
1230
|
+
if (roleFor(element) !== 'button' || pressed === undefined) {
|
|
1231
|
+
throw new Error('Desired pressed state requires a pressed button target.');
|
|
1232
|
+
}
|
|
1233
|
+
desiredStates.push(pressed === desiredPressed);
|
|
1234
|
+
}
|
|
1235
|
+
if (desiredStates.length > 0 && desiredStates.every(Boolean))
|
|
1236
|
+
return;
|
|
823
1237
|
if (element instanceof HTMLAnchorElement) {
|
|
824
1238
|
const target = new URL(element.href, window.location.href);
|
|
825
1239
|
if (expectedDestinationPath !== undefined &&
|
|
@@ -859,6 +1273,8 @@ export class PageContextCollector {
|
|
|
859
1273
|
type(element, value) {
|
|
860
1274
|
if (value.length > 2000)
|
|
861
1275
|
throw new Error('Value is too long.');
|
|
1276
|
+
if (!enabled(element))
|
|
1277
|
+
throw new Error('Text target is not editable.');
|
|
862
1278
|
if (element instanceof HTMLInputElement) {
|
|
863
1279
|
if (!['text', 'search', 'email', 'tel', 'url', 'number'].includes(element.type)) {
|
|
864
1280
|
throw new Error('Input type is not supported.');
|
|
@@ -896,14 +1312,24 @@ export class PageContextCollector {
|
|
|
896
1312
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
897
1313
|
}
|
|
898
1314
|
select(element, handle, optionValueId) {
|
|
899
|
-
if (!(element instanceof HTMLSelectElement) || !optionValueId) {
|
|
1315
|
+
if (!(element instanceof HTMLSelectElement) || !optionValueId || !enabled(element)) {
|
|
900
1316
|
throw new Error('Select target is invalid.');
|
|
901
1317
|
}
|
|
902
1318
|
const option = handle.options.get(optionValueId);
|
|
903
|
-
if (!option ||
|
|
1319
|
+
if (!option ||
|
|
1320
|
+
optionDisabled(option) ||
|
|
1321
|
+
!option.isConnected ||
|
|
1322
|
+
option.closest('select') !== element) {
|
|
904
1323
|
throw new Error('Select option is unavailable.');
|
|
905
1324
|
}
|
|
906
|
-
|
|
1325
|
+
const selectedIndexSetter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'selectedIndex')?.set;
|
|
1326
|
+
if (!selectedIndexSetter) {
|
|
1327
|
+
throw new Error('Select option cannot be updated safely.');
|
|
1328
|
+
}
|
|
1329
|
+
selectedIndexSetter.call(element, option.index);
|
|
1330
|
+
if (element.options[element.selectedIndex] !== option || !option.selected) {
|
|
1331
|
+
throw new Error('Select option could not be applied.');
|
|
1332
|
+
}
|
|
907
1333
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
908
1334
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
909
1335
|
}
|
|
@@ -925,6 +1351,7 @@ export class PageContextCollector {
|
|
|
925
1351
|
connected: element.isConnected,
|
|
926
1352
|
disabled: !enabled(element),
|
|
927
1353
|
expanded: element.getAttribute('aria-expanded'),
|
|
1354
|
+
pressed: element.getAttribute('aria-pressed'),
|
|
928
1355
|
checked: element instanceof HTMLInputElement && ['checkbox', 'radio'].includes(element.type)
|
|
929
1356
|
? element.checked
|
|
930
1357
|
: undefined,
|
|
@@ -938,6 +1365,17 @@ export class PageContextCollector {
|
|
|
938
1365
|
['checkbox', 'radio'].includes(element.type) &&
|
|
939
1366
|
element.checked === grant.desiredChecked);
|
|
940
1367
|
}
|
|
1368
|
+
if (grant.desiredExpanded !== undefined &&
|
|
1369
|
+
literalBooleanAttribute(element, 'aria-expanded') !== grant.desiredExpanded) {
|
|
1370
|
+
return false;
|
|
1371
|
+
}
|
|
1372
|
+
if (grant.desiredPressed !== undefined &&
|
|
1373
|
+
literalBooleanAttribute(element, 'aria-pressed') !== grant.desiredPressed) {
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
if (grant.desiredExpanded !== undefined || grant.desiredPressed !== undefined) {
|
|
1377
|
+
return true;
|
|
1378
|
+
}
|
|
941
1379
|
if (grant.action === 'type') {
|
|
942
1380
|
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
943
1381
|
return element.value === (grant.value ?? '');
|
|
@@ -949,7 +1387,13 @@ export class PageContextCollector {
|
|
|
949
1387
|
}
|
|
950
1388
|
if (grant.action === 'select') {
|
|
951
1389
|
const option = grant.optionValueId ? handle.options.get(grant.optionValueId) : undefined;
|
|
952
|
-
|
|
1390
|
+
if (!(element instanceof HTMLSelectElement) || !option)
|
|
1391
|
+
return false;
|
|
1392
|
+
return (option.isConnected &&
|
|
1393
|
+
option.closest('select') === element &&
|
|
1394
|
+
!optionDisabled(option) &&
|
|
1395
|
+
element.options[element.selectedIndex] === option &&
|
|
1396
|
+
option.selected);
|
|
953
1397
|
}
|
|
954
1398
|
return true;
|
|
955
1399
|
}
|
|
@@ -970,6 +1414,8 @@ export class PageContextCollector {
|
|
|
970
1414
|
};
|
|
971
1415
|
}
|
|
972
1416
|
async captureAndPublishIfChanged() {
|
|
1417
|
+
if (this.navigationSettlement)
|
|
1418
|
+
return;
|
|
973
1419
|
const snapshot = this.captureSnapshot(false);
|
|
974
1420
|
if (snapshot && this.running)
|
|
975
1421
|
await this.callbacks.onSnapshot(snapshot);
|
|
@@ -1043,6 +1489,54 @@ export class PageContextCollector {
|
|
|
1043
1489
|
capturedAt: new Date().toISOString(),
|
|
1044
1490
|
};
|
|
1045
1491
|
}
|
|
1492
|
+
beginNavigationSettlement(pageId, revision, url) {
|
|
1493
|
+
const settlement = {
|
|
1494
|
+
generation: ++this.navigationSettlementGeneration,
|
|
1495
|
+
pageId,
|
|
1496
|
+
revision,
|
|
1497
|
+
url,
|
|
1498
|
+
};
|
|
1499
|
+
this.navigationSettlement = settlement;
|
|
1500
|
+
if (this.captureTimer)
|
|
1501
|
+
clearTimeout(this.captureTimer);
|
|
1502
|
+
this.captureTimer = null;
|
|
1503
|
+
if (this.mutationCaptureTimer)
|
|
1504
|
+
clearTimeout(this.mutationCaptureTimer);
|
|
1505
|
+
this.mutationCaptureTimer = null;
|
|
1506
|
+
return settlement;
|
|
1507
|
+
}
|
|
1508
|
+
observeCausalNavigationAfterMutation(actionId, destinationPath, beforeActionUrl) {
|
|
1509
|
+
const currentUrl = safePageUrl();
|
|
1510
|
+
if (currentUrl === beforeActionUrl ||
|
|
1511
|
+
window.location.search !== '' ||
|
|
1512
|
+
window.location.hash !== '' ||
|
|
1513
|
+
window.location.pathname !== destinationPath) {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
if (this.pendingNavigation?.causalActionId !== actionId ||
|
|
1517
|
+
this.pendingNavigation.url !== currentUrl) {
|
|
1518
|
+
this.pageId = id('page');
|
|
1519
|
+
this.revision += 1;
|
|
1520
|
+
this.targets.clear();
|
|
1521
|
+
this.recentSelection = null;
|
|
1522
|
+
this.pendingNavigation = {
|
|
1523
|
+
pageId: this.pageId,
|
|
1524
|
+
revision: this.revision,
|
|
1525
|
+
url: currentUrl,
|
|
1526
|
+
causalActionId: actionId,
|
|
1527
|
+
causalObservationReported: false,
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
this.pendingCapture = true;
|
|
1531
|
+
this.reportCausalNavigationObservation();
|
|
1532
|
+
}
|
|
1533
|
+
reportCausalNavigationObservation() {
|
|
1534
|
+
const navigation = this.pendingNavigation;
|
|
1535
|
+
if (!navigation || navigation.causalObservationReported)
|
|
1536
|
+
return;
|
|
1537
|
+
navigation.causalObservationReported = true;
|
|
1538
|
+
this.callbacks.onCausalNavigationObserved?.(navigation.pageId, navigation.revision, navigation.url, navigation.causalActionId);
|
|
1539
|
+
}
|
|
1046
1540
|
installNavigationHooks() {
|
|
1047
1541
|
this.originalPushState = history.pushState;
|
|
1048
1542
|
this.originalReplaceState = history.replaceState;
|