@reticlehq/browser 3.6.0 → 3.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/dist/actions/action-report.d.ts +64 -0
- package/dist/actions/action-report.js +87 -0
- package/dist/actions/actions-dom.d.ts +5 -0
- package/dist/actions/actions-dom.js +32 -4
- package/dist/actions/actions-press.d.ts +1 -0
- package/dist/actions/actions-press.js +25 -1
- package/dist/actions/actions.d.ts +2 -37
- package/dist/actions/actions.js +56 -80
- package/dist/actions/appeared-text.js +5 -2
- package/dist/actions/click-target.d.ts +1 -0
- package/dist/actions/click-target.js +14 -0
- package/dist/actions/danger-context.d.ts +4 -1
- package/dist/actions/danger-context.js +45 -4
- package/dist/actions/value-input.d.ts +1 -0
- package/dist/actions/value-input.js +11 -0
- package/dist/activate.d.ts +14 -0
- package/dist/activate.js +43 -0
- package/dist/busy.d.ts +3 -0
- package/dist/busy.js +42 -0
- package/dist/commands/commands.js +49 -17
- package/dist/connect-options.d.ts +12 -1
- package/dist/connect-options.js +18 -1
- package/dist/dom/a11y.d.ts +2 -0
- package/dist/dom/a11y.js +353 -80
- package/dist/dom/addressing/coverage-markers.d.ts +6 -0
- package/dist/dom/addressing/coverage-markers.js +13 -0
- package/dist/dom/chart.d.ts +3 -2
- package/dist/dom/chart.js +3 -2
- package/dist/dom/dom-ignore.d.ts +8 -0
- package/dist/dom/dom-ignore.js +57 -2
- package/dist/dom/layout-audit.d.ts +15 -0
- package/dist/dom/layout-audit.js +208 -0
- package/dist/dom/paint-context.d.ts +15 -0
- package/dist/dom/paint-context.js +78 -0
- package/dist/dom/query.d.ts +2 -1
- package/dist/dom/query.js +163 -38
- package/dist/dom/render-page.d.ts +12 -0
- package/dist/dom/render-page.js +135 -0
- package/dist/dom/shadow-registry.d.ts +1 -0
- package/dist/dom/shadow-registry.js +27 -0
- package/dist/dom/snapshot.d.ts +5 -0
- package/dist/dom/snapshot.js +135 -36
- package/dist/dom/stable-selector.d.ts +1 -0
- package/dist/dom/stable-selector.js +19 -0
- package/dist/dom/text-match.d.ts +12 -0
- package/dist/dom/text-match.js +115 -0
- package/dist/dom/theme.d.ts +1 -0
- package/dist/dom/theme.js +21 -9
- package/dist/dom/virtualized.js +5 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -0
- package/dist/observers/blind-spots.js +3 -2
- package/dist/observers/canvas.d.ts +18 -0
- package/dist/observers/canvas.js +76 -0
- package/dist/observers/dom.js +96 -32
- package/dist/observers/field.d.ts +11 -0
- package/dist/observers/field.js +116 -30
- package/dist/observers/install-all.js +4 -0
- package/dist/observers/media.d.ts +2 -0
- package/dist/observers/media.js +32 -0
- package/dist/observers/net-detail/network-body.d.ts +2 -0
- package/dist/observers/net-detail/network-body.js +8 -1
- package/dist/observers/network.js +82 -13
- package/dist/observers/perf.js +53 -1
- package/dist/observers/route.d.ts +2 -0
- package/dist/observers/route.js +39 -9
- package/dist/presenter/carousel/carousel.js +2 -7
- package/dist/presenter/carousel/panel-slides.d.ts +2 -4
- package/dist/presenter/carousel/panel-slides.js +17 -15
- package/dist/presenter/chrome/log-memory.d.ts +1 -0
- package/dist/presenter/chrome/presenter-log.d.ts +7 -0
- package/dist/presenter/chrome/presenter-log.js +48 -8
- package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
- package/dist/presenter/chrome/presenter-morph.js +34 -0
- package/dist/presenter/chrome/presenter-report-copy.d.ts +12 -4
- package/dist/presenter/chrome/presenter-report-copy.js +12 -4
- package/dist/presenter/hud-telemetry.js +55 -4
- package/dist/presenter/presenter-account-styles.js +3 -3
- package/dist/presenter/presenter-account.d.ts +27 -5
- package/dist/presenter/presenter-account.js +83 -6
- package/dist/presenter/presenter-agent-link.d.ts +9 -0
- package/dist/presenter/presenter-agent-link.js +62 -0
- package/dist/presenter/presenter-chat-views.d.ts +25 -4
- package/dist/presenter/presenter-chat-views.js +339 -72
- package/dist/presenter/presenter-config.d.ts +3 -0
- package/dist/presenter/presenter-config.js +2 -0
- package/dist/presenter/presenter-controls.d.ts +0 -1
- package/dist/presenter/presenter-controls.js +13 -91
- package/dist/presenter/presenter-design-tokens.js +1 -1
- package/dist/presenter/presenter-frame.d.ts +3 -2
- package/dist/presenter/presenter-frame.js +31 -24
- package/dist/presenter/presenter-harness-row.d.ts +56 -0
- package/dist/presenter/presenter-harness-row.js +170 -0
- package/dist/presenter/presenter-personas.d.ts +35 -0
- package/dist/presenter/presenter-personas.js +95 -0
- package/dist/presenter/presenter-plan.js +25 -24
- package/dist/presenter/presenter-report-styles.js +23 -22
- package/dist/presenter/presenter-report.js +29 -13
- package/dist/presenter/presenter-run-state.js +2 -1
- package/dist/presenter/presenter-settings-styles.js +8 -4
- package/dist/presenter/presenter-settings.d.ts +4 -4
- package/dist/presenter/presenter-settings.js +66 -28
- package/dist/presenter/presenter-shell-styles.js +12 -3
- package/dist/presenter/presenter-shell.d.ts +11 -3
- package/dist/presenter/presenter-shell.js +60 -22
- package/dist/presenter/presenter-styles.js +0 -2
- package/dist/presenter/presenter-workspace.js +10 -3
- package/dist/presenter/presenter.d.ts +3 -2
- package/dist/presenter/presenter.js +107 -25
- package/dist/registry/declared-platform.js +2 -0
- package/dist/registry/store-adapters.d.ts +5 -2
- package/dist/registry/store-adapters.js +40 -10
- package/dist/reticle-inject.js +771 -703
- package/dist/reticle.d.ts +4 -2
- package/dist/reticle.js +72 -8
- package/dist/security/page-location.d.ts +2 -0
- package/dist/security/page-location.js +7 -0
- package/dist/security/serialization.js +1 -1
- package/dist/storage-keys.d.ts +4 -2
- package/dist/storage-keys.js +8 -3
- package/dist/timers/native/native-timers.d.ts +1 -0
- package/dist/timers/native/native-timers.js +5 -0
- package/dist/transport/transport.d.ts +1 -0
- package/dist/transport/transport.js +23 -3
- package/dist/transport/unreachable-message.d.ts +1 -1
- package/dist/transport/unreachable-message.js +3 -5
- package/package.json +6 -2
- package/dist/presenter/carousel/offer-card.d.ts +0 -21
- package/dist/presenter/carousel/offer-card.js +0 -56
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { CanvasTrigger, EventType } from '@reticlehq/core';
|
|
2
|
+
export { CanvasTrigger };
|
|
3
|
+
import { refs } from '../dom/addressing/refs.js';
|
|
4
|
+
import { nativeSetInterval } from '../timers/native/native-timers.js';
|
|
5
|
+
import { observeSafely, observeValue } from './types.js';
|
|
6
|
+
const SIDE = 16;
|
|
7
|
+
const POLL_MS = 500;
|
|
8
|
+
const MAX_CANVASES = 4;
|
|
9
|
+
const FNV_OFFSET = 0x811c9dc5;
|
|
10
|
+
const FNV_PRIME = 0x01000193;
|
|
11
|
+
const QUANTUM = 8;
|
|
12
|
+
export function digestPixels(data) {
|
|
13
|
+
let hash = FNV_OFFSET;
|
|
14
|
+
const sum = [0, 0, 0];
|
|
15
|
+
const pixels = Math.max(1, data.length / 4);
|
|
16
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
17
|
+
for (let c = 0; c < 3; c += 1) {
|
|
18
|
+
const value = data[i + c] ?? 0;
|
|
19
|
+
sum[c] = (sum[c] ?? 0) + value;
|
|
20
|
+
hash = Math.imul(hash ^ Math.floor(value / QUANTUM), FNV_PRIME) >>> 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const mean = sum.map((s) => Math.round(s / pixels));
|
|
24
|
+
return { digest: hash.toString(16).padStart(8, '0'), mean };
|
|
25
|
+
}
|
|
26
|
+
function sampleCanvas(canvas) {
|
|
27
|
+
if (0 === canvas.width || 0 === canvas.height) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const scratch = document.createElement('canvas');
|
|
31
|
+
scratch.width = SIDE;
|
|
32
|
+
scratch.height = SIDE;
|
|
33
|
+
const ctx = scratch.getContext('2d');
|
|
34
|
+
if (null === ctx) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const data = observeValue(() => {
|
|
38
|
+
ctx.drawImage(canvas, 0, 0, SIDE, SIDE);
|
|
39
|
+
return ctx.getImageData(0, 0, SIDE, SIDE).data;
|
|
40
|
+
});
|
|
41
|
+
return data === undefined ? undefined : { data, width: canvas.width, height: canvas.height };
|
|
42
|
+
}
|
|
43
|
+
let sampleAll;
|
|
44
|
+
export function canvasAfterAct() {
|
|
45
|
+
sampleAll?.(CanvasTrigger.ACT);
|
|
46
|
+
}
|
|
47
|
+
export function installCanvas(emit, opts = {}) {
|
|
48
|
+
const sample = opts.sample ?? sampleCanvas;
|
|
49
|
+
const every = opts.every ?? nativeSetInterval;
|
|
50
|
+
const last = new WeakMap();
|
|
51
|
+
const look = (trigger) => {
|
|
52
|
+
observeSafely(() => {
|
|
53
|
+
const canvases = Array.from(document.querySelectorAll('canvas')).slice(0, MAX_CANVASES);
|
|
54
|
+
for (const canvas of canvases) {
|
|
55
|
+
const read = sample(canvas);
|
|
56
|
+
if (read === undefined) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const { digest, mean } = digestPixels(read.data);
|
|
60
|
+
if (CanvasTrigger.POLL === trigger && last.get(canvas) === digest) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
last.set(canvas, digest);
|
|
64
|
+
emit(EventType.CANVAS_DIGEST, { digest, width: read.width, height: read.height, mean, trigger }, refs.refFor(canvas));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
sampleAll = look;
|
|
69
|
+
const stop = every(() => look(CanvasTrigger.POLL), POLL_MS);
|
|
70
|
+
return () => {
|
|
71
|
+
stop();
|
|
72
|
+
if (sampleAll === look) {
|
|
73
|
+
sampleAll = undefined;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
package/dist/observers/dom.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { readTestId, testIdSelector } from '../dom/addressing/testid-attr.js';
|
|
2
|
-
import { EventType, TruncationChannel } from '@reticlehq/core';
|
|
3
|
-
import { getAccessibleName, getRole, isVisible } from '../dom/a11y.js';
|
|
2
|
+
import { EventType, REDACTED_VALUE, TruncationChannel } from '@reticlehq/core';
|
|
3
|
+
import { getAccessibleName, getRole, isSensitiveField, isVisible } from '../dom/a11y.js';
|
|
4
4
|
import { refs } from '../dom/addressing/refs.js';
|
|
5
5
|
function regionKeyOf(target) {
|
|
6
6
|
const el = isElement(target) ? target : null;
|
|
7
|
-
if (null === el)
|
|
7
|
+
if (null === el) {
|
|
8
8
|
return undefined;
|
|
9
|
+
}
|
|
9
10
|
const labelled = el.closest(testIdSelector());
|
|
10
11
|
return (labelled ? readTestId(labelled) : null) ?? refs.refFor(el);
|
|
11
12
|
}
|
|
12
|
-
import {
|
|
13
|
+
import { isUnobserved } from '../dom/dom-ignore.js';
|
|
13
14
|
import { capturedRoots, installShadowRegistry, onShadowRoot } from '../dom/shadow-registry.js';
|
|
14
15
|
import { isElement } from '../dom/realm.js';
|
|
15
16
|
import { observeSameOriginFrames } from './frames.js';
|
|
@@ -30,8 +31,9 @@ const WATCHED_ATTRS = [
|
|
|
30
31
|
];
|
|
31
32
|
const MAX_ATTR_VALUE_LEN = 120;
|
|
32
33
|
function capValue(value) {
|
|
33
|
-
if (null === value)
|
|
34
|
+
if (null === value) {
|
|
34
35
|
return undefined;
|
|
36
|
+
}
|
|
35
37
|
return value.length > MAX_ATTR_VALUE_LEN ? `${value.slice(0, MAX_ATTR_VALUE_LEN)}…` : value;
|
|
36
38
|
}
|
|
37
39
|
const DIALOG_ROLES = new Set(['dialog', 'alertdialog']);
|
|
@@ -44,19 +46,23 @@ const MOUNT_SCAN_LIMIT = 40;
|
|
|
44
46
|
function describeMount(node) {
|
|
45
47
|
const role = getRole(node);
|
|
46
48
|
const name = getAccessibleName(node);
|
|
47
|
-
if (isMeaningful(role, name))
|
|
49
|
+
if (isMeaningful(role, name)) {
|
|
48
50
|
return { role, name, el: node };
|
|
51
|
+
}
|
|
49
52
|
let scanned = 0;
|
|
50
53
|
for (const child of node.querySelectorAll('*')) {
|
|
51
|
-
if (scanned >= MOUNT_SCAN_LIMIT)
|
|
54
|
+
if (scanned >= MOUNT_SCAN_LIMIT) {
|
|
52
55
|
return undefined;
|
|
56
|
+
}
|
|
53
57
|
scanned += 1;
|
|
54
|
-
if (
|
|
58
|
+
if (isUnobserved(child)) {
|
|
55
59
|
continue;
|
|
60
|
+
}
|
|
56
61
|
const childRole = getRole(child);
|
|
57
62
|
const childName = getAccessibleName(child);
|
|
58
|
-
if (isMeaningful(childRole, childName))
|
|
63
|
+
if (isMeaningful(childRole, childName)) {
|
|
59
64
|
return { role: childRole, name: childName, el: child };
|
|
65
|
+
}
|
|
60
66
|
}
|
|
61
67
|
return undefined;
|
|
62
68
|
}
|
|
@@ -64,31 +70,63 @@ const TEXT_NODE = 3;
|
|
|
64
70
|
function textOf(nodes) {
|
|
65
71
|
let out = '';
|
|
66
72
|
for (const node of nodes) {
|
|
67
|
-
if (node.nodeType !== TEXT_NODE)
|
|
73
|
+
if (node.nodeType !== TEXT_NODE) {
|
|
68
74
|
continue;
|
|
75
|
+
}
|
|
69
76
|
out += node.nodeValue ?? '';
|
|
70
77
|
}
|
|
71
78
|
const trimmed = out.trim();
|
|
72
79
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
73
80
|
}
|
|
81
|
+
const SAFE_ON_SECRET = new Set([
|
|
82
|
+
'class',
|
|
83
|
+
'style',
|
|
84
|
+
'type',
|
|
85
|
+
'disabled',
|
|
86
|
+
'readonly',
|
|
87
|
+
'required',
|
|
88
|
+
'hidden',
|
|
89
|
+
]);
|
|
90
|
+
function secretAttr(el, attr) {
|
|
91
|
+
return !SAFE_ON_SECRET.has(attr) && !attr.startsWith('aria-') && isSensitiveField(el);
|
|
92
|
+
}
|
|
93
|
+
const TESTID_DEPTH = 3;
|
|
94
|
+
const MAX_INITIAL_TEXTS = 50;
|
|
95
|
+
function testidNear(el) {
|
|
96
|
+
let at = el;
|
|
97
|
+
for (let depth = 0; at !== null && depth <= TESTID_DEPTH; depth += 1) {
|
|
98
|
+
const id = readTestId(at);
|
|
99
|
+
if (id !== null && id.length > 0) {
|
|
100
|
+
return id;
|
|
101
|
+
}
|
|
102
|
+
at = at.parentElement;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const named = (el) => {
|
|
107
|
+
const testid = testidNear(el);
|
|
108
|
+
return testid === undefined ? {} : { testid };
|
|
109
|
+
};
|
|
74
110
|
export function installDom(emit) {
|
|
75
111
|
const stopRegistry = installShadowRegistry();
|
|
76
112
|
const observer = new MutationObserver((records) => {
|
|
77
113
|
let added = 0;
|
|
78
114
|
let removed = 0;
|
|
79
115
|
let changed = 0;
|
|
116
|
+
let initials = 0;
|
|
80
117
|
let dropped = 0;
|
|
81
118
|
for (const record of records) {
|
|
82
119
|
if ('attributes' === record.type) {
|
|
83
120
|
const target = record.target;
|
|
84
|
-
if (isElement(target) && record.attributeName !== null && !
|
|
121
|
+
if (isElement(target) && record.attributeName !== null && !isUnobserved(target)) {
|
|
85
122
|
if (changed >= MAX_PER_BATCH) {
|
|
86
123
|
dropped += 1;
|
|
87
124
|
continue;
|
|
88
125
|
}
|
|
89
126
|
changed += 1;
|
|
90
|
-
const
|
|
91
|
-
const
|
|
127
|
+
const hide = secretAttr(target, record.attributeName);
|
|
128
|
+
const value = hide ? REDACTED_VALUE : capValue(target.getAttribute(record.attributeName));
|
|
129
|
+
const old = hide && null !== record.oldValue ? REDACTED_VALUE : capValue(record.oldValue);
|
|
92
130
|
emit(EventType.DOM_ATTR, {
|
|
93
131
|
attr: record.attributeName,
|
|
94
132
|
...(value === undefined ? {} : { value }),
|
|
@@ -99,43 +137,65 @@ export function installDom(emit) {
|
|
|
99
137
|
}
|
|
100
138
|
if ('characterData' === record.type) {
|
|
101
139
|
const parent = record.target.parentElement;
|
|
102
|
-
if (parent !== null && !
|
|
140
|
+
if (parent !== null && !isUnobserved(parent)) {
|
|
103
141
|
if (changed >= MAX_PER_BATCH) {
|
|
104
142
|
dropped += 1;
|
|
105
143
|
continue;
|
|
106
144
|
}
|
|
107
145
|
changed += 1;
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
146
|
+
const hide = isSensitiveField(parent);
|
|
147
|
+
const text = hide
|
|
148
|
+
? REDACTED_VALUE
|
|
149
|
+
: (record.target.textContent ?? '').trim().slice(0, 80);
|
|
150
|
+
const old = hide ? undefined : capValue(record.oldValue?.trim() ?? null);
|
|
151
|
+
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent) }, refs.refFor(parent));
|
|
111
152
|
}
|
|
112
153
|
continue;
|
|
113
154
|
}
|
|
114
155
|
if (textOf(record.addedNodes) !== undefined || textOf(record.removedNodes) !== undefined) {
|
|
115
156
|
const parent = isElement(record.target) ? record.target : null;
|
|
116
|
-
if (parent !== null && !
|
|
117
|
-
if (changed >= MAX_PER_BATCH)
|
|
157
|
+
if (parent !== null && !isUnobserved(parent)) {
|
|
158
|
+
if (changed >= MAX_PER_BATCH) {
|
|
118
159
|
dropped += 1;
|
|
160
|
+
}
|
|
119
161
|
else {
|
|
120
162
|
changed += 1;
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
163
|
+
const hide = isSensitiveField(parent);
|
|
164
|
+
const text = hide ? REDACTED_VALUE : (parent.textContent ?? '').trim().slice(0, 80);
|
|
165
|
+
const old = hide ? undefined : capValue(textOf(record.removedNodes) ?? null);
|
|
166
|
+
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent) }, refs.refFor(parent));
|
|
124
167
|
}
|
|
125
168
|
}
|
|
126
169
|
}
|
|
127
170
|
for (const node of record.addedNodes) {
|
|
128
|
-
if (!isElement(node))
|
|
171
|
+
if (!isElement(node)) {
|
|
129
172
|
continue;
|
|
173
|
+
}
|
|
130
174
|
if (added >= MAX_PER_BATCH) {
|
|
131
175
|
dropped += 1;
|
|
132
176
|
continue;
|
|
133
177
|
}
|
|
134
|
-
if (
|
|
178
|
+
if (isUnobserved(node)) {
|
|
135
179
|
continue;
|
|
180
|
+
}
|
|
181
|
+
const testids = [node, ...Array.from(node.querySelectorAll(testIdSelector()))].filter((el) => readTestId(el) !== null);
|
|
182
|
+
for (const el of testids) {
|
|
183
|
+
const text = (el.textContent ?? '').trim();
|
|
184
|
+
if (0 === text.length) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (initials >= MAX_INITIAL_TEXTS) {
|
|
188
|
+
dropped += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
initials += 1;
|
|
192
|
+
const shown = isSensitiveField(el) ? REDACTED_VALUE : text.slice(0, 80);
|
|
193
|
+
emit(EventType.DOM_TEXT, { text: shown, testid: readTestId(el) ?? '', initial: true }, refs.refFor(el));
|
|
194
|
+
}
|
|
136
195
|
const mounted = describeMount(node);
|
|
137
|
-
if (mounted === undefined)
|
|
196
|
+
if (mounted === undefined) {
|
|
138
197
|
continue;
|
|
198
|
+
}
|
|
139
199
|
const { role, name } = mounted;
|
|
140
200
|
added += 1;
|
|
141
201
|
const ref = refs.refFor(mounted.el);
|
|
@@ -143,31 +203,34 @@ export function installDom(emit) {
|
|
|
143
203
|
if (DIALOG_ROLES.has(role) ||
|
|
144
204
|
LIVE_ROLES.has(role) ||
|
|
145
205
|
'true' === node.getAttribute('aria-modal')) {
|
|
146
|
-
if (isVisible(node))
|
|
206
|
+
if (isVisible(node)) {
|
|
147
207
|
emit(EventType.VISIBLE_SHOWN, { role, name }, ref);
|
|
208
|
+
}
|
|
148
209
|
}
|
|
149
210
|
}
|
|
150
211
|
for (const node of record.removedNodes) {
|
|
151
|
-
if (!isElement(node))
|
|
212
|
+
if (!isElement(node)) {
|
|
152
213
|
continue;
|
|
214
|
+
}
|
|
153
215
|
if (removed >= MAX_PER_BATCH) {
|
|
154
216
|
dropped += 1;
|
|
155
217
|
continue;
|
|
156
218
|
}
|
|
157
|
-
if (
|
|
158
|
-
(isElement(record.target) && isReticleOverlay(record.target))) {
|
|
219
|
+
if (isUnobserved(node) || (isElement(record.target) && isUnobserved(record.target))) {
|
|
159
220
|
continue;
|
|
160
221
|
}
|
|
161
222
|
const gone = describeMount(node);
|
|
162
|
-
if (gone === undefined)
|
|
223
|
+
if (gone === undefined) {
|
|
163
224
|
continue;
|
|
225
|
+
}
|
|
164
226
|
const { role, name } = gone;
|
|
165
227
|
removed += 1;
|
|
166
228
|
emit(EventType.DOM_REMOVED, { role, name, region: regionKeyOf(record.target) });
|
|
167
229
|
}
|
|
168
230
|
}
|
|
169
|
-
if (dropped > 0)
|
|
231
|
+
if (dropped > 0) {
|
|
170
232
|
emit(EventType.TRUNCATED, { channel: TruncationChannel.DOM, dropped });
|
|
233
|
+
}
|
|
171
234
|
});
|
|
172
235
|
const options = {
|
|
173
236
|
subtree: true,
|
|
@@ -179,8 +242,9 @@ export function installDom(emit) {
|
|
|
179
242
|
characterDataOldValue: true,
|
|
180
243
|
};
|
|
181
244
|
observer.observe(document.documentElement, options);
|
|
182
|
-
for (const root of capturedRoots())
|
|
245
|
+
for (const root of capturedRoots()) {
|
|
183
246
|
observer.observe(root, options);
|
|
247
|
+
}
|
|
184
248
|
const unsubscribe = onShadowRoot((root) => observer.observe(root, options));
|
|
185
249
|
const stopFrames = observeSameOriginFrames((body) => observer.observe(body, options));
|
|
186
250
|
return () => {
|
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
import type { Emit, Teardown } from './types.js';
|
|
2
|
+
export interface FieldReading {
|
|
3
|
+
value: string;
|
|
4
|
+
checked?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface ActedField {
|
|
7
|
+
before: FieldReading;
|
|
8
|
+
stop: () => void;
|
|
9
|
+
written: () => boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function watchActedField(el: Element): ActedField | undefined;
|
|
12
|
+
export declare function publishSettledField(el: Element, acted: ActedField | undefined): void;
|
|
2
13
|
export declare function installField(emit: Emit): Teardown;
|
package/dist/observers/field.js
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
import { readTestId } from '../dom/addressing/testid-attr.js';
|
|
2
|
-
import {
|
|
3
|
-
import { getAccessibleName } from '../dom/a11y.js';
|
|
2
|
+
import { EventType, FieldChangeField, FieldChangeKind } from '@reticlehq/core';
|
|
3
|
+
import { getAccessibleName, isSensitiveField } from '../dom/a11y.js';
|
|
4
4
|
const MAX_VALUE_LEN = 160;
|
|
5
5
|
const BURST_MS = 150;
|
|
6
|
-
const
|
|
6
|
+
const CHECKBOX_TYPE = 'checkbox';
|
|
7
|
+
const RADIO_TYPE = 'radio';
|
|
8
|
+
let settledEmit;
|
|
7
9
|
function isField(target) {
|
|
8
10
|
return (target instanceof HTMLInputElement ||
|
|
9
11
|
target instanceof HTMLTextAreaElement ||
|
|
10
12
|
target instanceof HTMLSelectElement);
|
|
11
13
|
}
|
|
14
|
+
function isCheckable(element) {
|
|
15
|
+
return (element instanceof HTMLInputElement &&
|
|
16
|
+
(CHECKBOX_TYPE === element.type || RADIO_TYPE === element.type));
|
|
17
|
+
}
|
|
18
|
+
function readingOf(element) {
|
|
19
|
+
if (isCheckable(element))
|
|
20
|
+
return { value: element.value, checked: element.checked };
|
|
21
|
+
return { value: element.value };
|
|
22
|
+
}
|
|
23
|
+
function readingMoved(before, after) {
|
|
24
|
+
if (before.checked !== undefined || after.checked !== undefined) {
|
|
25
|
+
return before.checked !== after.checked;
|
|
26
|
+
}
|
|
27
|
+
return before.value !== after.value;
|
|
28
|
+
}
|
|
29
|
+
function clip(value) {
|
|
30
|
+
return value.length > MAX_VALUE_LEN ? `${value.slice(0, MAX_VALUE_LEN)}…` : value;
|
|
31
|
+
}
|
|
12
32
|
function fieldName(element) {
|
|
13
33
|
const testid = readTestId(element);
|
|
14
34
|
if (null !== testid && testid.length > 0)
|
|
@@ -21,38 +41,102 @@ function fieldName(element) {
|
|
|
21
41
|
const id = element.getAttribute('id');
|
|
22
42
|
return null !== id && id.length > 0 ? id : element.tagName.toLowerCase();
|
|
23
43
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
function fieldPayload(element, kind, after, before) {
|
|
45
|
+
const secret = isSensitiveField(element);
|
|
46
|
+
const payload = {
|
|
47
|
+
[FieldChangeField.FIELD]: fieldName(element),
|
|
48
|
+
[FieldChangeField.KIND]: kind,
|
|
49
|
+
[FieldChangeField.LENGTH]: after.value.length,
|
|
50
|
+
};
|
|
51
|
+
if (after.checked !== undefined)
|
|
52
|
+
payload[FieldChangeField.CHECKED] = after.checked;
|
|
53
|
+
if (before?.checked !== undefined)
|
|
54
|
+
payload[FieldChangeField.PREVIOUS_CHECKED] = before.checked;
|
|
55
|
+
if (secret) {
|
|
56
|
+
payload[FieldChangeField.REDACTED] = true;
|
|
57
|
+
return payload;
|
|
38
58
|
}
|
|
39
|
-
|
|
59
|
+
payload[FieldChangeField.VALUE] = clip(after.value);
|
|
60
|
+
if (before !== undefined && before.checked === undefined) {
|
|
61
|
+
payload[FieldChangeField.PREVIOUS] = clip(before.value);
|
|
62
|
+
}
|
|
63
|
+
return payload;
|
|
64
|
+
}
|
|
65
|
+
function settledProperty(el) {
|
|
66
|
+
return isCheckable(el) ? 'checked' : 'value';
|
|
67
|
+
}
|
|
68
|
+
function inheritedDescriptor(el, prop) {
|
|
69
|
+
for (let proto = Object.getPrototypeOf(el); proto !== null;) {
|
|
70
|
+
const found = Object.getOwnPropertyDescriptor(proto, prop);
|
|
71
|
+
if (found !== undefined)
|
|
72
|
+
return found;
|
|
73
|
+
proto = Object.getPrototypeOf(proto);
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
function watchWrites(el, prop) {
|
|
78
|
+
const own = Object.getOwnPropertyDescriptor(el, prop);
|
|
79
|
+
const target = own ?? inheritedDescriptor(el, prop);
|
|
80
|
+
const get = target === undefined ? undefined : Reflect.get(target, 'get');
|
|
81
|
+
const set = target === undefined ? undefined : Reflect.get(target, 'set');
|
|
82
|
+
let written = false;
|
|
83
|
+
const usable = 'function' === typeof get && 'function' === typeof set;
|
|
84
|
+
if (!usable || false === own?.configurable) {
|
|
85
|
+
return { stop: () => { }, written: () => written };
|
|
86
|
+
}
|
|
87
|
+
Object.defineProperty(el, prop, {
|
|
88
|
+
configurable: true,
|
|
89
|
+
enumerable: target?.enumerable ?? false,
|
|
90
|
+
get() {
|
|
91
|
+
return Reflect.apply(get, this, []);
|
|
92
|
+
},
|
|
93
|
+
set(next) {
|
|
94
|
+
written = true;
|
|
95
|
+
Reflect.apply(set, this, [next]);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
let stopped = false;
|
|
99
|
+
return {
|
|
100
|
+
stop: () => {
|
|
101
|
+
if (stopped)
|
|
102
|
+
return;
|
|
103
|
+
stopped = true;
|
|
104
|
+
if (own === undefined)
|
|
105
|
+
Reflect.deleteProperty(el, prop);
|
|
106
|
+
else
|
|
107
|
+
Object.defineProperty(el, prop, own);
|
|
108
|
+
},
|
|
109
|
+
written: () => written,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function watchActedField(el) {
|
|
113
|
+
if (!isField(el) || el instanceof HTMLSelectElement)
|
|
114
|
+
return undefined;
|
|
115
|
+
return { before: readingOf(el), ...watchWrites(el, settledProperty(el)) };
|
|
116
|
+
}
|
|
117
|
+
export function publishSettledField(el, acted) {
|
|
118
|
+
const emit = settledEmit;
|
|
119
|
+
if (acted === undefined)
|
|
120
|
+
return;
|
|
121
|
+
acted.stop();
|
|
122
|
+
if (emit === undefined || !isField(el) || !el.isConnected || !acted.written())
|
|
123
|
+
return;
|
|
124
|
+
const before = acted.before;
|
|
125
|
+
if (isSensitiveField(el) && before.checked === undefined)
|
|
126
|
+
return;
|
|
127
|
+
const after = readingOf(el);
|
|
128
|
+
if (!readingMoved(before, after))
|
|
129
|
+
return;
|
|
130
|
+
emit(EventType.FIELD_CHANGE, fieldPayload(el, FieldChangeKind.SETTLED, after, before));
|
|
40
131
|
}
|
|
41
132
|
export function installField(emit) {
|
|
42
133
|
const ac = new AbortController();
|
|
43
134
|
const { signal } = ac;
|
|
135
|
+
const previousEmit = settledEmit;
|
|
136
|
+
settledEmit = emit;
|
|
44
137
|
const pending = new Map();
|
|
45
138
|
const report = (element, kind) => {
|
|
46
|
-
|
|
47
|
-
const secret = isSecret(element);
|
|
48
|
-
emit(EventType.FIELD_CHANGE, {
|
|
49
|
-
field: fieldName(element),
|
|
50
|
-
kind,
|
|
51
|
-
length: value.length,
|
|
52
|
-
...(secret
|
|
53
|
-
? { redacted: true }
|
|
54
|
-
: { value: value.length > MAX_VALUE_LEN ? `${value.slice(0, MAX_VALUE_LEN)}…` : value }),
|
|
55
|
-
});
|
|
139
|
+
emit(EventType.FIELD_CHANGE, fieldPayload(element, kind, readingOf(element)));
|
|
56
140
|
};
|
|
57
141
|
const clear = (element) => {
|
|
58
142
|
const timer = pending.get(element);
|
|
@@ -67,7 +151,7 @@ export function installField(emit) {
|
|
|
67
151
|
clear(element);
|
|
68
152
|
pending.set(element, setTimeout(() => {
|
|
69
153
|
pending.delete(element);
|
|
70
|
-
report(element,
|
|
154
|
+
report(element, FieldChangeKind.INPUT);
|
|
71
155
|
}, BURST_MS));
|
|
72
156
|
};
|
|
73
157
|
const onChange = (event) => {
|
|
@@ -75,7 +159,7 @@ export function installField(emit) {
|
|
|
75
159
|
if (!isField(element))
|
|
76
160
|
return;
|
|
77
161
|
clear(element);
|
|
78
|
-
report(element,
|
|
162
|
+
report(element, FieldChangeKind.CHANGE);
|
|
79
163
|
};
|
|
80
164
|
document.addEventListener('input', onInput, { signal, capture: true });
|
|
81
165
|
document.addEventListener('change', onChange, { signal, capture: true });
|
|
@@ -83,6 +167,8 @@ export function installField(emit) {
|
|
|
83
167
|
for (const timer of pending.values())
|
|
84
168
|
clearTimeout(timer);
|
|
85
169
|
pending.clear();
|
|
170
|
+
if (settledEmit === emit)
|
|
171
|
+
settledEmit = previousEmit;
|
|
86
172
|
ac.abort();
|
|
87
173
|
};
|
|
88
174
|
}
|
|
@@ -17,6 +17,8 @@ import { installDialogs } from './dialogs.js';
|
|
|
17
17
|
import { installAnimation } from './animation.js';
|
|
18
18
|
import { installScroll } from './scroll.js';
|
|
19
19
|
import { installHealth } from './health.js';
|
|
20
|
+
import { installMedia } from './media.js';
|
|
21
|
+
import { installCanvas } from './canvas.js';
|
|
20
22
|
import { reportSdkFailure, SdkSite } from './sdk-failure.js';
|
|
21
23
|
function guard(emit, site, install) {
|
|
22
24
|
try {
|
|
@@ -65,5 +67,7 @@ export function installAllObservers(emit, options) {
|
|
|
65
67
|
guard(emit, SdkSite.DOM_OBSERVER, () => installBlindSpots(emit)),
|
|
66
68
|
guard(emit, SdkSite.HEALTH_OBSERVER, () => installHealth(emit)),
|
|
67
69
|
guard(emit, SdkSite.DOM_OBSERVER, () => installContextOpen(emit)),
|
|
70
|
+
guard(emit, SdkSite.DOM_OBSERVER, () => installMedia(emit)),
|
|
71
|
+
guard(emit, SdkSite.DOM_OBSERVER, () => installCanvas(emit)),
|
|
68
72
|
];
|
|
69
73
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EventType, MediaEvent } from '@reticlehq/core';
|
|
2
|
+
import { refs } from '../dom/addressing/refs.js';
|
|
3
|
+
import { observeSafely } from './types.js';
|
|
4
|
+
const TAGS = { VIDEO: 'video', AUDIO: 'audio' };
|
|
5
|
+
export function installMedia(emit) {
|
|
6
|
+
const ac = new AbortController();
|
|
7
|
+
const onMedia = (event) => {
|
|
8
|
+
observeSafely(() => {
|
|
9
|
+
const el = event.target;
|
|
10
|
+
if (!(el instanceof HTMLMediaElement)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const tag = TAGS[el.tagName];
|
|
14
|
+
if (tag === undefined) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const duration = Number.isFinite(el.duration) ? el.duration : undefined;
|
|
18
|
+
emit(EventType.MEDIA_STATE, {
|
|
19
|
+
event: event.type,
|
|
20
|
+
tag,
|
|
21
|
+
currentTime: el.currentTime,
|
|
22
|
+
...(duration === undefined ? {} : { duration }),
|
|
23
|
+
readyState: el.readyState,
|
|
24
|
+
paused: el.paused,
|
|
25
|
+
}, refs.refFor(el));
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
for (const type of Object.values(MediaEvent)) {
|
|
29
|
+
document.addEventListener(type, onMedia, { capture: true, signal: ac.signal });
|
|
30
|
+
}
|
|
31
|
+
return () => ac.abort();
|
|
32
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { ResponseBodyReason } from '@reticlehq/core';
|
|
1
2
|
export declare function isStreamingBody(contentType: string | null, contentLength: string | null): boolean;
|
|
2
3
|
export declare function withBodyDeadline(read: Promise<string>): Promise<string | undefined>;
|
|
4
|
+
export declare function skippedBodyReason(contentType: string | null): ResponseBodyReason | undefined;
|
|
3
5
|
export declare function isCapturableType(contentType: string | null): boolean;
|
|
4
6
|
export declare function setNetworkBodyMaxChars(chars: number): void;
|
|
5
7
|
export declare function networkBodyMaxChars(): number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { REDACTED_VALUE } from '@reticlehq/core';
|
|
1
|
+
import { REDACTED_VALUE, ResponseBodyReason } from '@reticlehq/core';
|
|
2
2
|
import { isSensitiveKey, safeStringify, scrubKnownSecrets } from '../../security/serialization.js';
|
|
3
3
|
import { nativeClearTimeout, nativeSetTimeout } from '../../timers/native/native-timers.js';
|
|
4
4
|
const CAPTURABLE_CONTENT = /application\/json|text\/|application\/xml|x-www-form-urlencoded|graphql/i;
|
|
@@ -25,6 +25,13 @@ export async function withBodyDeadline(read) {
|
|
|
25
25
|
nativeClearTimeout(timer);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
export function skippedBodyReason(contentType) {
|
|
29
|
+
if (null === contentType)
|
|
30
|
+
return undefined;
|
|
31
|
+
return STREAMING_CONTENT.test(contentType)
|
|
32
|
+
? ResponseBodyReason.STREAMING
|
|
33
|
+
: ResponseBodyReason.NOT_TEXT;
|
|
34
|
+
}
|
|
28
35
|
export function isCapturableType(contentType) {
|
|
29
36
|
if (null === contentType)
|
|
30
37
|
return false;
|