@reticlehq/browser 3.5.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/action-report.d.ts +54 -0
- package/dist/actions/action-report.js +60 -0
- package/dist/actions/actions-dom.d.ts +1 -0
- package/dist/actions/actions-dom.js +56 -43
- package/dist/actions/actions-press.d.ts +11 -4
- package/dist/actions/actions-press.js +132 -17
- package/dist/actions/actions.d.ts +1 -35
- package/dist/actions/actions.js +32 -65
- package/dist/actions/appeared-text.js +3 -0
- package/dist/actions/danger-context.d.ts +2 -0
- package/dist/actions/danger-context.js +29 -3
- package/dist/actions/value-input.d.ts +1 -0
- package/dist/actions/value-input.js +11 -0
- package/dist/commands/commands.js +4 -0
- package/dist/connect-options.d.ts +1 -0
- package/dist/dom/a11y.js +264 -14
- package/dist/dom/addressing/coverage-markers.d.ts +6 -0
- package/dist/dom/addressing/coverage-markers.js +13 -0
- package/dist/dom/addressing/refs.js +2 -1
- package/dist/dom/addressing/testid-attr.d.ts +4 -0
- package/dist/dom/addressing/testid-attr.js +14 -0
- package/dist/dom/dom-ignore.d.ts +2 -0
- package/dist/dom/dom-ignore.js +15 -2
- package/dist/dom/query.d.ts +2 -1
- package/dist/dom/query.js +116 -27
- package/dist/dom/render-page.d.ts +12 -0
- package/dist/dom/render-page.js +135 -0
- package/dist/dom/snapshot.js +56 -7
- package/dist/observers/departure.js +51 -9
- package/dist/observers/dom.js +35 -18
- package/dist/observers/field.js +5 -21
- package/dist/observers/health.js +63 -2
- package/dist/observers/network-stream.d.ts +1 -1
- package/dist/observers/network-stream.js +3 -3
- package/dist/observers/network.js +57 -8
- package/dist/observers/perf.js +53 -1
- package/dist/observers/route.js +6 -5
- package/dist/observers/storage.js +6 -1
- package/dist/presenter/carousel/carousel.d.ts +5 -2
- package/dist/presenter/carousel/carousel.js +30 -8
- package/dist/presenter/carousel/panel-slides.d.ts +3 -4
- package/dist/presenter/carousel/panel-slides.js +48 -9
- package/dist/presenter/carousel/talk-card.d.ts +1 -1
- package/dist/presenter/chrome/log-kinds.d.ts +5 -0
- package/dist/presenter/chrome/log-kinds.js +1 -0
- package/dist/presenter/chrome/log-memory.d.ts +13 -0
- package/dist/presenter/chrome/log-memory.js +39 -0
- package/dist/presenter/chrome/presenter-hud-chrome.d.ts +1 -0
- package/dist/presenter/chrome/presenter-hud-chrome.js +7 -1
- package/dist/presenter/chrome/presenter-log.d.ts +12 -4
- package/dist/presenter/chrome/presenter-log.js +100 -10
- 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 +28 -5
- package/dist/presenter/chrome/presenter-report-copy.js +40 -5
- package/dist/presenter/hud-telemetry.js +83 -3
- package/dist/presenter/icons/presenter-heroicons-data.d.ts +1 -0
- package/dist/presenter/icons/presenter-heroicons-data.js +4 -3
- package/dist/presenter/icons/presenter-icons.d.ts +2 -0
- package/dist/presenter/icons/presenter-icons.js +2 -0
- package/dist/presenter/presenter-account-styles.js +3 -3
- package/dist/presenter/presenter-account.d.ts +31 -8
- package/dist/presenter/presenter-account.js +105 -15
- 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 +43 -0
- package/dist/presenter/presenter-chat-views.js +622 -0
- package/dist/presenter/presenter-config.d.ts +5 -2
- package/dist/presenter/presenter-config.js +5 -3
- package/dist/presenter/presenter-controls.d.ts +3 -2
- package/dist/presenter/presenter-controls.js +163 -61
- package/dist/presenter/presenter-design-tokens.d.ts +1 -0
- package/dist/presenter/presenter-design-tokens.js +34 -0
- package/dist/presenter/presenter-dock-layout.js +1 -1
- package/dist/presenter/presenter-frame.d.ts +12 -0
- package/dist/presenter/presenter-frame.js +187 -0
- package/dist/presenter/presenter-harness-row.d.ts +67 -0
- package/dist/presenter/presenter-harness-row.js +209 -0
- package/dist/presenter/presenter-personas.d.ts +35 -0
- package/dist/presenter/presenter-personas.js +95 -0
- package/dist/presenter/presenter-plan.d.ts +10 -0
- package/dist/presenter/presenter-plan.js +186 -0
- package/dist/presenter/presenter-report-styles.js +46 -21
- package/dist/presenter/presenter-report.d.ts +2 -1
- package/dist/presenter/presenter-report.js +123 -20
- package/dist/presenter/presenter-settings-styles.js +18 -4
- package/dist/presenter/presenter-settings.d.ts +8 -2
- package/dist/presenter/presenter-settings.js +134 -33
- package/dist/presenter/presenter-shell-styles.js +48 -42
- package/dist/presenter/presenter-shell.d.ts +17 -4
- package/dist/presenter/presenter-shell.js +139 -79
- package/dist/presenter/presenter-styles.js +9 -3
- package/dist/presenter/presenter-workspace.js +10 -3
- package/dist/presenter/presenter.d.ts +6 -4
- package/dist/presenter/presenter.js +171 -22
- package/dist/presenter/tour/tour.d.ts +4 -1
- package/dist/presenter/tour/tour.js +6 -4
- package/dist/recorder/recorder.js +2 -2
- package/dist/registry/auto-testids.js +3 -4
- package/dist/registry/declared-platform.js +4 -0
- package/dist/reticle-inject.js +938 -494
- package/dist/reticle.d.ts +10 -1
- package/dist/reticle.js +109 -11
- package/dist/review/annotator-styles.d.ts +1 -1
- package/dist/review/annotator.d.ts +7 -0
- package/dist/review/annotator.js +20 -2
- package/dist/review/mark-anchor.js +2 -2
- package/dist/session-continuity.js +2 -1
- package/dist/storage-keys.d.ts +15 -0
- package/dist/storage-keys.js +21 -0
- package/dist/transport/transport.js +5 -3
- package/dist/transport/unreachable-message.d.ts +1 -1
- package/dist/transport/unreachable-message.js +3 -5
- package/package.json +3 -2
- package/dist/presenter/carousel/offer-card.d.ts +0 -21
- package/dist/presenter/carousel/offer-card.js +0 -55
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CoverageMarker, SOURCE_MAPPING_CAPABILITY, DATA_RETICLE_SOURCE_ATTR, RETICLE_OVERLAY_SELECTOR, } from '@reticlehq/core';
|
|
2
|
+
import { testIdSelector } from './testid-attr.js';
|
|
3
|
+
const appCarries = (doc, selector) => Array.from(doc.querySelectorAll(selector)).some((el) => null === el.closest(RETICLE_OVERLAY_SELECTOR));
|
|
4
|
+
export function coverageMarkers(doc) {
|
|
5
|
+
return [
|
|
6
|
+
...(appCarries(doc, `[${DATA_RETICLE_SOURCE_ATTR}]`) ? [CoverageMarker.SOURCE] : []),
|
|
7
|
+
...(appCarries(doc, testIdSelector()) ? [CoverageMarker.TESTID] : []),
|
|
8
|
+
];
|
|
9
|
+
}
|
|
10
|
+
export function notApplicableFor(page) {
|
|
11
|
+
const couldStamp = page.buildPlugin || 0 < page.adapters.length || page.markers.includes(CoverageMarker.SOURCE);
|
|
12
|
+
return couldStamp ? [] : [SOURCE_MAPPING_CAPABILITY];
|
|
13
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TRANSPORT_LIMITS, asRef } from '@reticlehq/core';
|
|
2
|
+
import { ReticleStorageKey } from '../../storage-keys.js';
|
|
2
3
|
export const MAX_TRACKED_REFS = 10000;
|
|
3
4
|
const REF_ELISION = '…';
|
|
4
5
|
const REF_PREFIX = 'e';
|
|
@@ -7,7 +8,7 @@ export function echoRef(ref) {
|
|
|
7
8
|
return max >= ref.length ? ref : `${ref.slice(0, max)}${REF_ELISION}`;
|
|
8
9
|
}
|
|
9
10
|
const SWEEP_EVERY_MINTS = 1000;
|
|
10
|
-
const REF_BASE_KEY =
|
|
11
|
+
const REF_BASE_KEY = ReticleStorageKey.REF_BASE;
|
|
11
12
|
export const REF_BLOCK = 100;
|
|
12
13
|
function readBase(store) {
|
|
13
14
|
try {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DEFAULT_TESTID_ATTR, TESTID_ATTR_PATTERN } from '@reticlehq/core';
|
|
2
|
+
let testIdAttr = DEFAULT_TESTID_ATTR;
|
|
3
|
+
export function getTestIdAttr() {
|
|
4
|
+
return testIdAttr;
|
|
5
|
+
}
|
|
6
|
+
export function testIdSelector() {
|
|
7
|
+
return `[${testIdAttr}]`;
|
|
8
|
+
}
|
|
9
|
+
export function readTestId(el) {
|
|
10
|
+
return el.getAttribute(testIdAttr);
|
|
11
|
+
}
|
|
12
|
+
export function setTestIdAttr(name) {
|
|
13
|
+
testIdAttr = undefined !== name && TESTID_ATTR_PATTERN.test(name) ? name : DEFAULT_TESTID_ATTR;
|
|
14
|
+
}
|
package/dist/dom/dom-ignore.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export declare const RETICLE_OVERLAY: string;
|
|
2
2
|
export declare function setPresenterVisible(visible: boolean): void;
|
|
3
|
+
export declare function setPresenterViewer(harness: boolean): void;
|
|
3
4
|
export declare function isPresenterVisible(): boolean;
|
|
4
5
|
export declare function setIgnoreSelectors(selectors: string[]): void;
|
|
5
6
|
export declare function isReticleOverlay(el: Element): boolean;
|
|
7
|
+
export declare function isUnobserved(el: Element): boolean;
|
|
6
8
|
export declare function isReticleUi(node: Element | null): boolean;
|
|
7
9
|
export declare function isIgnored(el: Element): boolean;
|
|
8
10
|
export declare function appModalOpen(doc: Document): boolean;
|
package/dist/dom/dom-ignore.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import { RETICLE_OVERLAY_SELECTOR } from '@reticlehq/core';
|
|
2
|
+
export const RETICLE_OVERLAY = RETICLE_OVERLAY_SELECTOR;
|
|
2
3
|
const DEV_OVERLAYS = '[data-agentation],#__next-build-watcher,nextjs-portal,[data-nextjs-dialog],[data-nextjs-toast]';
|
|
3
4
|
let extraIgnore = '';
|
|
4
5
|
let presenterVisible = false;
|
|
5
6
|
export function setPresenterVisible(visible) {
|
|
6
7
|
presenterVisible = visible;
|
|
7
8
|
}
|
|
9
|
+
let harnessViewing = false;
|
|
10
|
+
export function setPresenterViewer(harness) {
|
|
11
|
+
harnessViewing = harness;
|
|
12
|
+
}
|
|
13
|
+
const exposed = () => presenterVisible && !harnessViewing;
|
|
8
14
|
export function isPresenterVisible() {
|
|
9
15
|
return presenterVisible;
|
|
10
16
|
}
|
|
@@ -14,11 +20,18 @@ export function setIgnoreSelectors(selectors) {
|
|
|
14
20
|
export function isReticleOverlay(el) {
|
|
15
21
|
return el.closest(RETICLE_OVERLAY) !== null;
|
|
16
22
|
}
|
|
23
|
+
const HUD_DOCK = '[data-reticle-dock]';
|
|
24
|
+
export function isUnobserved(el) {
|
|
25
|
+
if (!isReticleOverlay(el))
|
|
26
|
+
return false;
|
|
27
|
+
return !exposed() || null === el.closest(HUD_DOCK);
|
|
28
|
+
}
|
|
17
29
|
export function isReticleUi(node) {
|
|
18
30
|
return node !== null && node.closest(RETICLE_OVERLAY) !== null;
|
|
19
31
|
}
|
|
32
|
+
const HUD_NARRATION = '[data-reticle-log],[data-reticle-harness-log],[data-reticle-chat-pill-text],[data-reticle-agent-said],[data-reticle-agent-note]';
|
|
20
33
|
export function isIgnored(el) {
|
|
21
|
-
const ours =
|
|
34
|
+
const ours = exposed() ? HUD_NARRATION : RETICLE_OVERLAY;
|
|
22
35
|
const sel = [ours, DEV_OVERLAYS, extraIgnore].filter((part) => part.length > 0).join(',');
|
|
23
36
|
return el.closest(sel) !== null;
|
|
24
37
|
}
|
package/dist/dom/query.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ElementState, type ElementQuery, type MatchResult, type QueryResult } from '@reticlehq/core';
|
|
2
2
|
export declare const ATTR_VALUE_MAX = 512;
|
|
3
|
-
export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number): MatchResult;
|
|
3
|
+
export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number, wholePhrase?: boolean): MatchResult;
|
|
4
|
+
export declare function elementsMatching(query: ElementQuery): HTMLElement[];
|
|
4
5
|
export declare function runQuery(query: ElementQuery, limit?: number): QueryResult;
|
package/dist/dom/query.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DATA_RETICLE_SOURCE_ATTR, ElementState, QueryBy, REDACTED_VALUE, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
1
|
+
import { ALTERNATIVE_TESTID_ATTRS, DEFAULT_TESTID_ATTR, DATA_RETICLE_SOURCE_ATTR, ElementState, QueryBy, REDACTED_VALUE, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
2
2
|
import { isActionTarget, isElement, isFrame, isInput, isSelect, isTextArea } from './realm.js';
|
|
3
3
|
import { capturedRootOf } from './shadow-registry.js';
|
|
4
4
|
import { getAccessibleName, getRole, describe, getStates, isInViewport, isVisible, } from './a11y.js';
|
|
@@ -7,11 +7,11 @@ import { isSensitiveKey } from '../security/serialization.js';
|
|
|
7
7
|
import { declaredTestids } from '../registry/capabilities.js';
|
|
8
8
|
import { identifyComponent } from '../registry/stores/adapters.js';
|
|
9
9
|
import { refs } from './addressing/refs.js';
|
|
10
|
-
|
|
10
|
+
import { getTestIdAttr, readTestId, testIdSelector } from './addressing/testid-attr.js';
|
|
11
11
|
const SOURCE_ATTR = DATA_RETICLE_SOURCE_ATTR;
|
|
12
12
|
const MAX_PRESENT_TESTIDS = 12;
|
|
13
13
|
const MAX_COMPONENT_CANDIDATES = 2000;
|
|
14
|
-
const
|
|
14
|
+
const componentCandidateSelector = () => `[${SOURCE_ATTR}], ${testIdSelector()}, button, a, input, select, textarea, [role]`;
|
|
15
15
|
function elementsUnder(container) {
|
|
16
16
|
const self = Node.ELEMENT_NODE === container.nodeType ? [container] : [];
|
|
17
17
|
return [...self, ...Array.from(container.querySelectorAll('*'))];
|
|
@@ -24,6 +24,11 @@ function fuzzyVisibleText(actual, expected) {
|
|
|
24
24
|
.toLowerCase()
|
|
25
25
|
.includes(normaliseVisibleText(expected).toLowerCase());
|
|
26
26
|
}
|
|
27
|
+
function textMatches(actual, query, expected) {
|
|
28
|
+
if (true !== query.matchCase)
|
|
29
|
+
return fuzzyVisibleText(actual, expected);
|
|
30
|
+
return normaliseVisibleText(actual).includes(normaliseVisibleText(expected));
|
|
31
|
+
}
|
|
27
32
|
function exactVisibleText(actual, expected) {
|
|
28
33
|
return normaliseVisibleText(actual) === normaliseVisibleText(expected);
|
|
29
34
|
}
|
|
@@ -45,17 +50,29 @@ function directText(el) {
|
|
|
45
50
|
.join('');
|
|
46
51
|
}
|
|
47
52
|
function visibleTextOf(el, memo) {
|
|
48
|
-
if (isNonRendered(el) || isIgnored(el)
|
|
53
|
+
if (isNonRendered(el) || isIgnored(el))
|
|
49
54
|
return '';
|
|
55
|
+
const visible = isVisible(el, memo);
|
|
56
|
+
const apart = laysOutApart(el);
|
|
50
57
|
let text = '';
|
|
51
58
|
for (const node of Array.from(el.childNodes)) {
|
|
52
|
-
if (Node.TEXT_NODE === node.nodeType)
|
|
59
|
+
if (Node.TEXT_NODE === node.nodeType && visible)
|
|
53
60
|
text += node.textContent ?? '';
|
|
54
|
-
else if (isElement(node))
|
|
55
|
-
|
|
61
|
+
else if (isElement(node)) {
|
|
62
|
+
const own = visibleTextOf(node, memo);
|
|
63
|
+
text += apart || ownBox(node) ? ` ${own} ` : own;
|
|
64
|
+
}
|
|
56
65
|
}
|
|
57
66
|
return text;
|
|
58
67
|
}
|
|
68
|
+
function laysOutApart(el) {
|
|
69
|
+
const display = el.ownerDocument.defaultView?.getComputedStyle(el).display ?? '';
|
|
70
|
+
return display.includes('flex') || display.includes('grid');
|
|
71
|
+
}
|
|
72
|
+
function ownBox(el) {
|
|
73
|
+
const display = el.ownerDocument.defaultView?.getComputedStyle(el).display ?? 'inline';
|
|
74
|
+
return !display.startsWith('inline') && 'contents' !== display;
|
|
75
|
+
}
|
|
59
76
|
function semanticNameTarget(el) {
|
|
60
77
|
if (isInput(el) || isTextArea(el) || isSelect(el))
|
|
61
78
|
return true;
|
|
@@ -94,7 +111,7 @@ function findBySource(container, source) {
|
|
|
94
111
|
function findByComponentName(container, component) {
|
|
95
112
|
const out = [];
|
|
96
113
|
let scanned = 0;
|
|
97
|
-
for (const el of Array.from(container.querySelectorAll(
|
|
114
|
+
for (const el of Array.from(container.querySelectorAll(componentCandidateSelector()))) {
|
|
98
115
|
if (scanned >= MAX_COMPONENT_CANDIDATES)
|
|
99
116
|
break;
|
|
100
117
|
scanned += 1;
|
|
@@ -115,14 +132,33 @@ function findByComponent(container, query) {
|
|
|
115
132
|
}
|
|
116
133
|
return [];
|
|
117
134
|
}
|
|
135
|
+
function matchesRole(actual, queried) {
|
|
136
|
+
if (actual === queried)
|
|
137
|
+
return true;
|
|
138
|
+
if ('textbox' === queried && 'searchbox' === actual)
|
|
139
|
+
return true;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
118
142
|
function queryByRoleAndName(container, role, name) {
|
|
119
|
-
return elementsUnder(container).filter((el) => getRole(el)
|
|
143
|
+
return elementsUnder(container).filter((el) => matchesRole(getRole(el), role) &&
|
|
144
|
+
(name === undefined || exactVisibleText(getAccessibleName(el), name)));
|
|
120
145
|
}
|
|
121
|
-
function queryByText(container, value) {
|
|
122
|
-
return elementsUnder(container).filter((el) =>
|
|
146
|
+
function queryByText(container, value, query) {
|
|
147
|
+
return elementsUnder(container).filter((el) => textMatches(directText(el), query, value));
|
|
123
148
|
}
|
|
124
149
|
function queryByLabel(container, value) {
|
|
125
|
-
|
|
150
|
+
const expected = normaliseVisibleText(value).toLowerCase();
|
|
151
|
+
const exact = [];
|
|
152
|
+
const fuzzy = [];
|
|
153
|
+
for (const el of elementsUnder(container)) {
|
|
154
|
+
if (!semanticNameTarget(el))
|
|
155
|
+
continue;
|
|
156
|
+
const name = normaliseVisibleText(getAccessibleName(el)).toLowerCase();
|
|
157
|
+
if (!name.includes(expected))
|
|
158
|
+
continue;
|
|
159
|
+
(name === expected ? exact : fuzzy).push(el);
|
|
160
|
+
}
|
|
161
|
+
return exact.length > 0 ? exact : fuzzy;
|
|
126
162
|
}
|
|
127
163
|
function queryByPlaceholder(container, value) {
|
|
128
164
|
return elementsUnder(container).filter((el) => {
|
|
@@ -131,7 +167,7 @@ function queryByPlaceholder(container, value) {
|
|
|
131
167
|
});
|
|
132
168
|
}
|
|
133
169
|
function queryByTestId(container, value) {
|
|
134
|
-
return elementsUnder(container).filter((el) => el
|
|
170
|
+
return elementsUnder(container).filter((el) => readTestId(el) === value);
|
|
135
171
|
}
|
|
136
172
|
function queryByAlt(container, value) {
|
|
137
173
|
return elementsUnder(container).filter((el) => {
|
|
@@ -147,7 +183,7 @@ function findIn(container, query) {
|
|
|
147
183
|
case QueryBy.ROLE:
|
|
148
184
|
return queryByRoleAndName(container, value, query.name);
|
|
149
185
|
case QueryBy.TEXT:
|
|
150
|
-
return queryByText(container, value);
|
|
186
|
+
return queryByText(container, value, query);
|
|
151
187
|
case QueryBy.LABEL:
|
|
152
188
|
return queryByLabel(container, value);
|
|
153
189
|
case QueryBy.PLACEHOLDER:
|
|
@@ -169,7 +205,7 @@ function findIn(container, query) {
|
|
|
169
205
|
return queryByRoleAndName(container, query.role, query.name);
|
|
170
206
|
}
|
|
171
207
|
if (query.text !== undefined)
|
|
172
|
-
return queryByText(container, query.text);
|
|
208
|
+
return queryByText(container, query.text, query);
|
|
173
209
|
if (query.label !== undefined)
|
|
174
210
|
return queryByLabel(container, query.label);
|
|
175
211
|
if (query.placeholder !== undefined)
|
|
@@ -221,7 +257,7 @@ function findCandidates(query) {
|
|
|
221
257
|
if (query.scope === undefined)
|
|
222
258
|
return { candidates: [], scopeMissing };
|
|
223
259
|
if (query.text !== undefined &&
|
|
224
|
-
!
|
|
260
|
+
!textMatches(visibleTextOf(container, new Map()), query, query.text)) {
|
|
225
261
|
return { candidates: [], scopeMissing };
|
|
226
262
|
}
|
|
227
263
|
return { candidates: isIgnored(container) ? [] : [container], scopeMissing };
|
|
@@ -261,10 +297,13 @@ function inState(el, state, memo) {
|
|
|
261
297
|
return getStates(el, isVisible(el, memo)).includes(state);
|
|
262
298
|
}
|
|
263
299
|
const MAX_DESCRIBED = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS;
|
|
264
|
-
export function matchQuery(query, state, limit = MAX_DESCRIBED) {
|
|
300
|
+
export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = true) {
|
|
265
301
|
const found = findCandidates(query);
|
|
266
|
-
const elements = found.candidates;
|
|
267
302
|
const scopeMissing = found.scopeMissing;
|
|
303
|
+
const wanted = wholePhrase && textOnly(query) ? wantedTextOf(query) : undefined;
|
|
304
|
+
const scope = 0 === found.candidates.length ? resolveContainer(query.scope).container : null;
|
|
305
|
+
const owner = null === scope || wanted === undefined ? undefined : splitTextOwner(scope, wanted, query);
|
|
306
|
+
const elements = owner === undefined ? found.candidates : [owner];
|
|
268
307
|
const visMemo = new Map();
|
|
269
308
|
const filtered = state === undefined ? elements : elements.filter((el) => inState(el, state, visMemo));
|
|
270
309
|
const attrs = query.attrs;
|
|
@@ -329,7 +368,7 @@ function buildPresentRegions(query) {
|
|
|
329
368
|
continue;
|
|
330
369
|
const name = el.getAttribute('aria-label') ??
|
|
331
370
|
resolveLabelledBy(el) ??
|
|
332
|
-
el
|
|
371
|
+
readTestId(el) ??
|
|
333
372
|
undefined;
|
|
334
373
|
const children = el.querySelectorAll('[role]');
|
|
335
374
|
const sample = [];
|
|
@@ -338,7 +377,7 @@ function buildPresentRegions(query) {
|
|
|
338
377
|
break;
|
|
339
378
|
const childRole = child.getAttribute('role');
|
|
340
379
|
const childName = child.getAttribute('aria-label') ??
|
|
341
|
-
child
|
|
380
|
+
readTestId(child) ??
|
|
342
381
|
child.textContent?.trim().slice(0, 40) ??
|
|
343
382
|
'';
|
|
344
383
|
if (childRole !== null && childName.length > 0) {
|
|
@@ -355,14 +394,14 @@ function buildPresentRegions(query) {
|
|
|
355
394
|
}
|
|
356
395
|
return regions;
|
|
357
396
|
}
|
|
358
|
-
function splitTextOwner(container, wanted) {
|
|
397
|
+
function splitTextOwner(container, wanted, query) {
|
|
359
398
|
let best;
|
|
360
399
|
let bestDepth = -1;
|
|
361
400
|
const memo = new Map();
|
|
362
401
|
for (const el of elementsUnder(container)) {
|
|
363
402
|
if (isIgnored(el))
|
|
364
403
|
continue;
|
|
365
|
-
if (!
|
|
404
|
+
if (!textMatches(visibleTextOf(el, memo), query, wanted))
|
|
366
405
|
continue;
|
|
367
406
|
let depth = 0;
|
|
368
407
|
for (let parent = el.parentElement; parent !== null; parent = parent.parentElement)
|
|
@@ -374,6 +413,32 @@ function splitTextOwner(container, wanted) {
|
|
|
374
413
|
}
|
|
375
414
|
return best;
|
|
376
415
|
}
|
|
416
|
+
function caseOnlyMatch(container, wanted) {
|
|
417
|
+
const target = normaliseVisibleText(wanted).toLowerCase();
|
|
418
|
+
if (0 === target.length)
|
|
419
|
+
return undefined;
|
|
420
|
+
const memo = new Map();
|
|
421
|
+
for (const el of elementsUnder(container)) {
|
|
422
|
+
if (isIgnored(el))
|
|
423
|
+
continue;
|
|
424
|
+
const text = normaliseVisibleText(visibleTextOf(el, memo));
|
|
425
|
+
const at = text.toLowerCase().indexOf(target);
|
|
426
|
+
if (-1 !== at)
|
|
427
|
+
return text.slice(at, at + target.length);
|
|
428
|
+
}
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
const TEXT_ONLY_FIELDS = new Set([
|
|
432
|
+
'text',
|
|
433
|
+
'matchCase',
|
|
434
|
+
'scope',
|
|
435
|
+
'by',
|
|
436
|
+
'value',
|
|
437
|
+
]);
|
|
438
|
+
function textOnly(query) {
|
|
439
|
+
return ((query.by === undefined || QueryBy.TEXT === query.by) &&
|
|
440
|
+
Object.keys(query).every((key) => TEXT_ONLY_FIELDS.has(key)));
|
|
441
|
+
}
|
|
377
442
|
function wantedTextOf(query) {
|
|
378
443
|
if (query.text !== undefined)
|
|
379
444
|
return query.text;
|
|
@@ -381,7 +446,7 @@ function wantedTextOf(query) {
|
|
|
381
446
|
}
|
|
382
447
|
const MAX_NAME_NEAR_MISSES = 5;
|
|
383
448
|
function nameNearMisses(container, query) {
|
|
384
|
-
const role = QueryBy.ROLE === query.by ? query.value :
|
|
449
|
+
const role = QueryBy.ROLE === query.by ? query.value : query.role;
|
|
385
450
|
const wanted = query.name;
|
|
386
451
|
if (role === undefined || wanted === undefined || 0 === wanted.length)
|
|
387
452
|
return [];
|
|
@@ -405,15 +470,28 @@ function nameNearMisses(container, query) {
|
|
|
405
470
|
}
|
|
406
471
|
return out;
|
|
407
472
|
}
|
|
473
|
+
function testidFoundUnder(container, query) {
|
|
474
|
+
if (QueryBy.TESTID !== query.by || undefined === query.value)
|
|
475
|
+
return undefined;
|
|
476
|
+
const configured = getTestIdAttr();
|
|
477
|
+
for (const attr of [DEFAULT_TESTID_ATTR, ...ALTERNATIVE_TESTID_ATTRS]) {
|
|
478
|
+
if (attr === configured)
|
|
479
|
+
continue;
|
|
480
|
+
const found = elementsUnder(container).some((el) => el.getAttribute(attr) === query.value && !isIgnored(el));
|
|
481
|
+
if (found)
|
|
482
|
+
return attr;
|
|
483
|
+
}
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
408
486
|
function buildEmptyHint(query) {
|
|
409
487
|
const container = resolveContainer(query.scope).container ?? document.body;
|
|
410
|
-
const all = container.querySelectorAll(
|
|
488
|
+
const all = container.querySelectorAll(testIdSelector());
|
|
411
489
|
const present = [];
|
|
412
490
|
const seen = new Set();
|
|
413
491
|
for (const el of Array.from(all)) {
|
|
414
492
|
if (isIgnored(el))
|
|
415
493
|
continue;
|
|
416
|
-
const id = el
|
|
494
|
+
const id = readTestId(el);
|
|
417
495
|
if (null === id || 0 === id.length || seen.has(id))
|
|
418
496
|
continue;
|
|
419
497
|
seen.add(id);
|
|
@@ -432,17 +510,28 @@ function buildEmptyHint(query) {
|
|
|
432
510
|
};
|
|
433
511
|
const wanted = wantedTextOf(query);
|
|
434
512
|
if (wanted !== undefined) {
|
|
435
|
-
const owner = splitTextOwner(container, wanted);
|
|
513
|
+
const owner = splitTextOwner(container, wanted, query);
|
|
436
514
|
if (owner !== undefined)
|
|
437
515
|
hint.splitText = describe(owner);
|
|
516
|
+
else if (true === query.matchCase) {
|
|
517
|
+
const shown = caseOnlyMatch(container, wanted);
|
|
518
|
+
if (shown !== undefined)
|
|
519
|
+
hint.caseMiss = shown;
|
|
520
|
+
}
|
|
438
521
|
}
|
|
439
522
|
const near = nameNearMisses(container, query);
|
|
440
523
|
if (near.length > 0)
|
|
441
524
|
hint.nameNearMiss = near;
|
|
525
|
+
const elsewhere = testidFoundUnder(container, query);
|
|
526
|
+
if (elsewhere !== undefined)
|
|
527
|
+
hint.testidFoundUnder = elsewhere;
|
|
442
528
|
return hint;
|
|
443
529
|
}
|
|
530
|
+
export function elementsMatching(query) {
|
|
531
|
+
return findCandidates(query).candidates;
|
|
532
|
+
}
|
|
444
533
|
export function runQuery(query, limit) {
|
|
445
|
-
const result = matchQuery(query, undefined, limit);
|
|
534
|
+
const result = matchQuery(query, undefined, limit, false);
|
|
446
535
|
const scopeFields = true === result.scopeMissing ? { scopeMissing: true } : {};
|
|
447
536
|
if (0 === result.elements.length) {
|
|
448
537
|
return {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
2
|
+
export type RenderResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
chunks: string[];
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
reason: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function cloneForRender(doc: Document): HTMLElement;
|
|
12
|
+
export declare function renderPage(): Promise<RenderResult>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { isSensitiveKey, STEP_SCREENSHOT } from '@reticlehq/core';
|
|
2
|
+
import { RETICLE_OVERLAY } from './dom-ignore.js';
|
|
3
|
+
export const MASK = '••••••••';
|
|
4
|
+
const BUDGET_CHARS = 240_000;
|
|
5
|
+
const CHUNK_CHARS = 60_000;
|
|
6
|
+
const SHRINK = 0.75;
|
|
7
|
+
const MAX_ATTEMPTS = 4;
|
|
8
|
+
const LOAD_TIMEOUT_MS = 5_000;
|
|
9
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
10
|
+
const FALLBACK_BACKGROUND = '#ffffff';
|
|
11
|
+
const JPEG_MIME = 'image/jpeg';
|
|
12
|
+
const DATA_URL_PREFIX = /^data:[^,]*,/;
|
|
13
|
+
function isSecretField(el) {
|
|
14
|
+
if (el instanceof HTMLInputElement && 'password' === el.type)
|
|
15
|
+
return true;
|
|
16
|
+
return [el.name, el.id, el.getAttribute('autocomplete') ?? ''].some((key) => key.length > 0 && isSensitiveKey(key));
|
|
17
|
+
}
|
|
18
|
+
function carryState(from, to) {
|
|
19
|
+
if (from instanceof HTMLInputElement || from instanceof HTMLTextAreaElement) {
|
|
20
|
+
const value = isSecretField(from) && from.value.length > 0 ? MASK : from.value;
|
|
21
|
+
if (from instanceof HTMLTextAreaElement)
|
|
22
|
+
to.textContent = value;
|
|
23
|
+
else
|
|
24
|
+
to.setAttribute('value', value);
|
|
25
|
+
}
|
|
26
|
+
if (from instanceof HTMLInputElement) {
|
|
27
|
+
if (from.checked)
|
|
28
|
+
to.setAttribute('checked', '');
|
|
29
|
+
else
|
|
30
|
+
to.removeAttribute('checked');
|
|
31
|
+
}
|
|
32
|
+
if (from instanceof HTMLOptionElement) {
|
|
33
|
+
if (from.selected)
|
|
34
|
+
to.setAttribute('selected', '');
|
|
35
|
+
else
|
|
36
|
+
to.removeAttribute('selected');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function readableCss(doc) {
|
|
40
|
+
const sheets = [...Array.from(doc.styleSheets), ...(doc.adoptedStyleSheets ?? [])];
|
|
41
|
+
const parts = [];
|
|
42
|
+
for (const sheet of sheets) {
|
|
43
|
+
try {
|
|
44
|
+
for (const rule of Array.from(sheet.cssRules))
|
|
45
|
+
parts.push(rule.cssText);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return parts.join('\n');
|
|
51
|
+
}
|
|
52
|
+
export function cloneForRender(doc) {
|
|
53
|
+
const root = doc.documentElement;
|
|
54
|
+
const clone = root.cloneNode(true);
|
|
55
|
+
const live = root.querySelectorAll('*');
|
|
56
|
+
const copied = clone.querySelectorAll('*');
|
|
57
|
+
live.forEach((el, i) => {
|
|
58
|
+
const twin = copied[i];
|
|
59
|
+
if (twin !== undefined)
|
|
60
|
+
carryState(el, twin);
|
|
61
|
+
});
|
|
62
|
+
const dropped = [RETICLE_OVERLAY, 'script', 'noscript', 'link[rel~="stylesheet"]', 'style'];
|
|
63
|
+
clone.querySelectorAll(dropped.join(',')).forEach((el) => el.remove());
|
|
64
|
+
const style = doc.createElement('style');
|
|
65
|
+
style.textContent = readableCss(doc);
|
|
66
|
+
(clone.querySelector('head') ?? clone).append(style);
|
|
67
|
+
return clone;
|
|
68
|
+
}
|
|
69
|
+
function backgroundOf(doc) {
|
|
70
|
+
const view = doc.defaultView;
|
|
71
|
+
for (const el of [doc.body, doc.documentElement]) {
|
|
72
|
+
if (null === view || null === el)
|
|
73
|
+
continue;
|
|
74
|
+
const colour = view.getComputedStyle(el).backgroundColor;
|
|
75
|
+
if (colour.length > 0 && 'transparent' !== colour && 'rgba(0, 0, 0, 0)' !== colour)
|
|
76
|
+
return colour;
|
|
77
|
+
}
|
|
78
|
+
return FALLBACK_BACKGROUND;
|
|
79
|
+
}
|
|
80
|
+
function loadImage(url) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const img = new Image();
|
|
83
|
+
const timer = setTimeout(() => reject(new Error('the page did not render in time')), LOAD_TIMEOUT_MS);
|
|
84
|
+
img.onload = () => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
resolve(img);
|
|
87
|
+
};
|
|
88
|
+
img.onerror = () => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
reject(new Error('the page could not be drawn as an image'));
|
|
91
|
+
};
|
|
92
|
+
img.src = url;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function chunked(base64) {
|
|
96
|
+
const out = [];
|
|
97
|
+
for (let i = 0; i < base64.length; i += CHUNK_CHARS)
|
|
98
|
+
out.push(base64.slice(i, i + CHUNK_CHARS));
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
export async function renderPage() {
|
|
102
|
+
try {
|
|
103
|
+
const width = Math.max(1, window.innerWidth);
|
|
104
|
+
const height = Math.max(1, window.innerHeight);
|
|
105
|
+
const clone = cloneForRender(document);
|
|
106
|
+
clone.style.transform = `translate(${String(-window.scrollX)}px, ${String(-window.scrollY)}px)`;
|
|
107
|
+
const xhtml = new XMLSerializer().serializeToString(clone);
|
|
108
|
+
const svg = `<svg xmlns="${SVG_NS}" width="${String(width)}" height="${String(height)}">` +
|
|
109
|
+
`<foreignObject x="0" y="0" width="100%" height="100%">${xhtml}</foreignObject></svg>`;
|
|
110
|
+
const img = await loadImage(`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`);
|
|
111
|
+
const background = backgroundOf(document);
|
|
112
|
+
let scale = Math.min(1, STEP_SCREENSHOT.MAX_SIDE_PX / Math.max(width, height));
|
|
113
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1, scale *= SHRINK) {
|
|
114
|
+
const canvas = document.createElement('canvas');
|
|
115
|
+
canvas.width = Math.max(1, Math.round(width * scale));
|
|
116
|
+
canvas.height = Math.max(1, Math.round(height * scale));
|
|
117
|
+
const ctx = canvas.getContext('2d');
|
|
118
|
+
if (null === ctx)
|
|
119
|
+
return { ok: false, reason: 'this page has no 2D canvas to draw into' };
|
|
120
|
+
ctx.fillStyle = background;
|
|
121
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
122
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
123
|
+
const base64 = canvas
|
|
124
|
+
.toDataURL(JPEG_MIME, STEP_SCREENSHOT.JPEG_QUALITY)
|
|
125
|
+
.replace(DATA_URL_PREFIX, '');
|
|
126
|
+
if (base64.length <= BUDGET_CHARS) {
|
|
127
|
+
return { ok: true, width: canvas.width, height: canvas.height, chunks: chunked(base64) };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { ok: false, reason: 'the rendered page was too large to send, even scaled down' };
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
134
|
+
}
|
|
135
|
+
}
|
package/dist/dom/snapshot.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ElementState, SnapshotMode } from '@reticlehq/core';
|
|
2
2
|
import { capturedRootOf } from './shadow-registry.js';
|
|
3
|
-
import { isFrame } from './realm.js';
|
|
3
|
+
import { isElement, isFrame } from './realm.js';
|
|
4
4
|
import { getAccessibleName, getRole, getStates, getValue, isVisible } from './a11y.js';
|
|
5
5
|
import { refs } from './addressing/refs.js';
|
|
6
6
|
import { isIgnored, isReticleOverlay } from './dom-ignore.js';
|
|
@@ -8,6 +8,7 @@ const INTERACTIVE = new Set([
|
|
|
8
8
|
'button',
|
|
9
9
|
'link',
|
|
10
10
|
'textbox',
|
|
11
|
+
'searchbox',
|
|
11
12
|
'checkbox',
|
|
12
13
|
'radio',
|
|
13
14
|
'combobox',
|
|
@@ -29,13 +30,44 @@ function announces(el, role) {
|
|
|
29
30
|
}
|
|
30
31
|
const TEXT_MAX = 80;
|
|
31
32
|
function directText(el) {
|
|
32
|
-
|
|
33
|
+
const parts = readingParts(el);
|
|
34
|
+
const collapsed = (parts.own ? parts.text : '').replace(/\s+/g, ' ').trim();
|
|
35
|
+
return collapsed.length > TEXT_MAX ? `${collapsed.slice(0, TEXT_MAX)}…` : collapsed;
|
|
36
|
+
}
|
|
37
|
+
function readingParts(el) {
|
|
38
|
+
let text = '';
|
|
39
|
+
let own = false;
|
|
33
40
|
for (const node of el.childNodes) {
|
|
34
|
-
if (3 === node.nodeType)
|
|
35
|
-
|
|
41
|
+
if (3 === node.nodeType) {
|
|
42
|
+
text += node.textContent ?? '';
|
|
43
|
+
own ||= 0 < (node.textContent ?? '').trim().length;
|
|
44
|
+
}
|
|
45
|
+
else if (isElement(node) && foldsIntoParent(node)) {
|
|
46
|
+
const inner = readingParts(node);
|
|
47
|
+
text += inner.text;
|
|
48
|
+
own ||= inner.own;
|
|
49
|
+
}
|
|
50
|
+
else if (isElement(node) && isInlineIn(node) && INTERACTIVE.has(getRole(node))) {
|
|
51
|
+
text += getAccessibleName(node);
|
|
52
|
+
}
|
|
36
53
|
}
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
return { text, own };
|
|
55
|
+
}
|
|
56
|
+
function isInlineIn(el) {
|
|
57
|
+
const view = el.ownerDocument.defaultView;
|
|
58
|
+
const parent = el.parentElement;
|
|
59
|
+
if (null === view || null === parent)
|
|
60
|
+
return false;
|
|
61
|
+
const around = view.getComputedStyle(parent).display;
|
|
62
|
+
if (around.includes('flex') || around.includes('grid'))
|
|
63
|
+
return false;
|
|
64
|
+
return 'inline' === view.getComputedStyle(el).display;
|
|
65
|
+
}
|
|
66
|
+
function foldsIntoParent(el) {
|
|
67
|
+
if (skipEarly(el) || !isInlineIn(el))
|
|
68
|
+
return false;
|
|
69
|
+
const role = getRole(el);
|
|
70
|
+
return !INTERACTIVE.has(role) && !announces(el, role) && 0 === getAccessibleName(el).length;
|
|
39
71
|
}
|
|
40
72
|
export const MAX_UNREAD_BRANCHES = 50;
|
|
41
73
|
function hiddenByMarkup(el) {
|
|
@@ -82,6 +114,7 @@ function layoutSignature(style) {
|
|
|
82
114
|
}
|
|
83
115
|
return '';
|
|
84
116
|
}
|
|
117
|
+
const overlayStamped = (el) => el.hasAttribute('data-aria-hidden') && 'true' === el.getAttribute('aria-hidden');
|
|
85
118
|
function recordUnread(branches, ctx) {
|
|
86
119
|
for (const branch of branches) {
|
|
87
120
|
if (ctx.unread.length >= MAX_UNREAD_BRANCHES) {
|
|
@@ -108,7 +141,11 @@ function pierceChildren(parent) {
|
|
|
108
141
|
return out;
|
|
109
142
|
}
|
|
110
143
|
function visit(child, depth, ctx, inLive) {
|
|
111
|
-
|
|
144
|
+
const stampedOnly = true === ctx.throughOverlay &&
|
|
145
|
+
overlayStamped(child) &&
|
|
146
|
+
!isIgnored(child) &&
|
|
147
|
+
!(child instanceof HTMLElement && child.hidden);
|
|
148
|
+
if (!stampedOnly && skipEarly(child)) {
|
|
112
149
|
if (hiddenByMarkup(child))
|
|
113
150
|
ctx.hiddenSkipped += 1;
|
|
114
151
|
return;
|
|
@@ -119,6 +156,10 @@ function visit(child, depth, ctx, inLive) {
|
|
|
119
156
|
ctx.hiddenSkipped += 1;
|
|
120
157
|
return;
|
|
121
158
|
}
|
|
159
|
+
if (style !== null && 'inline' === style.display && !inLive && foldsIntoParent(child)) {
|
|
160
|
+
walk(child, depth, ctx, inLive);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
122
163
|
const role = getRole(child);
|
|
123
164
|
const name = getAccessibleName(child);
|
|
124
165
|
const interactive = INTERACTIVE.has(role);
|
|
@@ -173,6 +214,7 @@ function collectDialogs(root) {
|
|
|
173
214
|
}
|
|
174
215
|
return names;
|
|
175
216
|
}
|
|
217
|
+
const MODAL_SELECTOR = '[role="dialog"], [role="alertdialog"], dialog[open], [aria-modal="true"]';
|
|
176
218
|
function overlayHidingPage(root) {
|
|
177
219
|
if (root !== document.body)
|
|
178
220
|
return undefined;
|
|
@@ -241,6 +283,13 @@ export function buildSnapshot(options = {}) {
|
|
|
241
283
|
visit(root, 0, ctx, false);
|
|
242
284
|
else
|
|
243
285
|
walk(root, 0, ctx);
|
|
286
|
+
if (0 === ctx.nodes && root === document.body) {
|
|
287
|
+
ctx.throughOverlay = true;
|
|
288
|
+
for (const child of Array.from(root.children))
|
|
289
|
+
if (overlayStamped(child) && null !== child.querySelector(MODAL_SELECTOR))
|
|
290
|
+
visit(child, 0, ctx, false);
|
|
291
|
+
ctx.throughOverlay = false;
|
|
292
|
+
}
|
|
244
293
|
return {
|
|
245
294
|
tree: ctx.lines.join('\n'),
|
|
246
295
|
status,
|