@reticlehq/browser 3.8.2 → 3.10.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.
Files changed (60) hide show
  1. package/dist/actions/action-report.d.ts +14 -0
  2. package/dist/actions/action-report.js +95 -22
  3. package/dist/actions/actions-dom.d.ts +17 -6
  4. package/dist/actions/actions-dom.js +98 -47
  5. package/dist/actions/actions-press.d.ts +10 -0
  6. package/dist/actions/actions-press.js +146 -2
  7. package/dist/actions/actions.js +64 -24
  8. package/dist/actions/insert-text.d.ts +3 -0
  9. package/dist/actions/insert-text.js +90 -0
  10. package/dist/actions/scroll.d.ts +1 -1
  11. package/dist/actions/scroll.js +25 -1
  12. package/dist/actions/value-input.d.ts +2 -0
  13. package/dist/actions/value-input.js +30 -9
  14. package/dist/commands/commands.js +85 -9
  15. package/dist/connect-options.d.ts +11 -1
  16. package/dist/connect-options.js +27 -1
  17. package/dist/dom/a11y.d.ts +1 -1
  18. package/dist/dom/a11y.js +11 -10
  19. package/dist/dom/chart-geometry.d.ts +7 -0
  20. package/dist/dom/chart-geometry.js +39 -0
  21. package/dist/dom/occlusion.js +21 -0
  22. package/dist/dom/query-finders.d.ts +1 -1
  23. package/dist/dom/query-finders.js +10 -7
  24. package/dist/dom/query.d.ts +6 -2
  25. package/dist/dom/query.js +165 -54
  26. package/dist/dom/scope-roots.d.ts +8 -0
  27. package/dist/dom/scope-roots.js +44 -0
  28. package/dist/dom/snapshot.d.ts +3 -0
  29. package/dist/dom/snapshot.js +9 -2
  30. package/dist/dom/text-match.js +6 -4
  31. package/dist/dom/theme.js +6 -7
  32. package/dist/dom/visibility.d.ts +3 -0
  33. package/dist/dom/visibility.js +125 -10
  34. package/dist/observers/canvas.d.ts +1 -0
  35. package/dist/observers/canvas.js +10 -1
  36. package/dist/observers/console.js +46 -35
  37. package/dist/observers/dom.js +28 -4
  38. package/dist/observers/net-detail/resource-timing.d.ts +12 -0
  39. package/dist/observers/net-detail/resource-timing.js +69 -0
  40. package/dist/observers/network.js +13 -44
  41. package/dist/observers/perf.js +15 -1
  42. package/dist/presenter/chrome/presenter-verbs.js +1 -0
  43. package/dist/presenter/presenter-config.d.ts +1 -0
  44. package/dist/presenter/presenter-config.js +1 -0
  45. package/dist/presenter/presenter-shell.d.ts +1 -0
  46. package/dist/presenter/presenter-shell.js +6 -3
  47. package/dist/presenter/tour/tour.js +4 -0
  48. package/dist/recorder/recorder.js +82 -39
  49. package/dist/reticle-inject.js +618 -618
  50. package/dist/reticle.d.ts +3 -19
  51. package/dist/reticle.js +76 -110
  52. package/dist/review/annotator.js +4 -0
  53. package/dist/security/serialization.js +35 -0
  54. package/dist/session-continuity.d.ts +1 -0
  55. package/dist/session-continuity.js +18 -0
  56. package/dist/storage-keys.js +2 -1
  57. package/dist/timers/native/native-timers.js +21 -4
  58. package/dist/transport/bridge-url.d.ts +8 -0
  59. package/dist/transport/bridge-url.js +52 -0
  60. package/package.json +10 -10
@@ -1,7 +1,7 @@
1
- import { ComponentStateReason, ElementQuerySchema, INSPECT_IN_VIEWPORT_FIELD, INSPECT_REVEAL_ARG, ReticleCommand, SnapshotMode, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
1
+ import { ANIMATION_TARGET_ARG, ComponentStateReason, ElementQuerySchema, INSPECT_IN_VIEWPORT_FIELD, INSPECT_REVEAL_ARG, MatchArg, ReticleCommand, SnapshotMode, SESSION_REF, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
2
2
  import { buildSnapshot } from '../dom/snapshot.js';
3
3
  import { paintContextOf } from '../dom/paint-context.js';
4
- import { matchQuery, runQuery } from '../dom/query.js';
4
+ import { embeddedRootsUnder, matchQuery, runQuery } from '../dom/query.js';
5
5
  import { executeAction, executeSequence } from '../actions/actions.js';
6
6
  import { describe } from '../dom/a11y.js';
7
7
  import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/addressing/source.js';
@@ -19,7 +19,7 @@ import { sanitizeWithReport } from '../security/serialization.js';
19
19
  import { getCapabilities } from '../registry/capabilities.js';
20
20
  import { freezeClock, advanceClock, resetClock, isClockFrozen } from '../timers/clock.js';
21
21
  import { scrollContainer } from '../actions/scroll.js';
22
- import { isOffViewport } from '../actions/actions-dom.js';
22
+ import { isOffViewport, scrollIntoViewNow } from '../actions/actions-dom.js';
23
23
  export const RELOAD_CACHE_BUST_PARAM = '_reticle_reload';
24
24
  function str(value) {
25
25
  return 'string' === typeof value ? value : undefined;
@@ -69,7 +69,7 @@ function inspect(ref, reveal = false) {
69
69
  'function' === typeof el.scrollIntoView &&
70
70
  isOffViewport(el, el.getBoundingClientRect());
71
71
  if (scrolledIntoView) {
72
- el.scrollIntoView({ block: 'center', inline: 'center' });
72
+ scrollIntoViewNow(el, { block: 'center', inline: 'center' });
73
73
  }
74
74
  const rect = el.getBoundingClientRect();
75
75
  const component = identifyComponent(el);
@@ -173,23 +173,98 @@ function readState(ref, store, path, depth) {
173
173
  }
174
174
  return result;
175
175
  }
176
- function listAnimations() {
176
+ function animationName(animation) {
177
+ const fields = animation;
178
+ const name = str(fields.animationName);
179
+ if (name !== undefined && name.length > 0) {
180
+ return name;
181
+ }
182
+ const transition = str(fields.transitionProperty);
183
+ return transition !== undefined && transition.length > 0 ? transition : undefined;
184
+ }
185
+ function resolveAnimationTarget(target) {
186
+ if (target === undefined) {
187
+ return undefined;
188
+ }
189
+ const byRef = refs.resolve(target);
190
+ if (byRef !== null) {
191
+ const ref = refs.refFor(byRef);
192
+ return {
193
+ report: { target, matched: true, refs: [ref], count: 1 },
194
+ matches: (animationTarget) => animationTarget !== null && refs.refFor(animationTarget) === ref,
195
+ };
196
+ }
197
+ if (SESSION_REF.test(target)) {
198
+ return {
199
+ report: { target, matched: false, refs: [], count: 0 },
200
+ matches: () => false,
201
+ };
202
+ }
203
+ let elements;
204
+ try {
205
+ const roots = [document];
206
+ const container = document.body ?? document.documentElement;
207
+ if (container !== null) {
208
+ roots.push(...embeddedRootsUnder(container));
209
+ }
210
+ const seen = new Set();
211
+ elements = [];
212
+ for (const root of roots) {
213
+ for (const el of root.querySelectorAll(target)) {
214
+ if (seen.has(el)) {
215
+ continue;
216
+ }
217
+ seen.add(el);
218
+ elements.push(el);
219
+ }
220
+ }
221
+ }
222
+ catch {
223
+ return {
224
+ report: { target, matched: false, refs: [], count: 0, invalid: true },
225
+ matches: () => false,
226
+ };
227
+ }
228
+ const allRefs = new Set(elements.map((el) => refs.refFor(el)));
229
+ const refsInReport = Array.from(allRefs).slice(0, TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS);
230
+ return {
231
+ report: {
232
+ target,
233
+ matched: elements.length > 0,
234
+ refs: refsInReport,
235
+ count: elements.length,
236
+ ...(allRefs.size > refsInReport.length ? { truncated: true } : {}),
237
+ },
238
+ matches: (animationTarget) => animationTarget !== null && allRefs.has(refs.refFor(animationTarget)),
239
+ };
240
+ }
241
+ function listAnimations(args = {}) {
177
242
  const doc = document;
243
+ const targetFilter = resolveAnimationTarget(str(args[ANIMATION_TARGET_ARG]));
178
244
  if (typeof doc.getAnimations !== 'function') {
179
- return { animations: [] };
245
+ return {
246
+ animations: [],
247
+ ...(targetFilter === undefined ? {} : { target: targetFilter.report }),
248
+ };
180
249
  }
181
250
  const animations = doc.getAnimations().map((a) => {
182
251
  const effect = a.effect;
183
252
  const timing = effect?.getTiming();
184
253
  const target = effect?.target ?? null;
254
+ const name = animationName(a);
185
255
  return {
256
+ ...(name === undefined ? {} : { name }),
186
257
  playState: a.playState,
187
258
  currentTime: a.currentTime,
188
259
  duration: timing?.duration,
189
260
  target: null === target ? null : describe(target),
261
+ ...(targetFilter === undefined ? {} : { targetMatches: targetFilter.matches(target) }),
190
262
  };
191
263
  });
192
- return { animations };
264
+ return {
265
+ animations,
266
+ ...(targetFilter === undefined ? {} : { target: targetFilter.report }),
267
+ };
193
268
  }
194
269
  export function resolveNavigationUrl(rawUrl, baseUrl) {
195
270
  if (0 === rawUrl.length || rawUrl.length > TRANSPORT_LIMITS.MAX_URL_LENGTH) {
@@ -211,12 +286,13 @@ export function createCommandRegistry() {
211
286
  includeRoot: true === args['includeRoot'],
212
287
  ids: true === args['ids'],
213
288
  audit: true === args['audit'],
289
+ charts: true === args['charts'],
214
290
  }));
215
291
  reg.set(ReticleCommand.QUERY, (args) => {
216
292
  const limit = args['limit'];
217
293
  return runQuery(queryFromArgs(args), 'number' === typeof limit ? limit : undefined);
218
294
  });
219
- reg.set(ReticleCommand.MATCH, (args) => matchQuery(ElementQuerySchema.parse(record(args['query'])), str(args['state']), undefined, undefined, true === args['diagnose']));
295
+ reg.set(ReticleCommand.MATCH, (args) => matchQuery(ElementQuerySchema.parse(record(args['query'])), str(args['state']), undefined, undefined, true === args['diagnose'], true === args[MatchArg.FULL_TEXT]));
220
296
  reg.set(ReticleCommand.ACT, async (args) => {
221
297
  const action = str(args['action']) ?? '';
222
298
  const result = await executeAction(str(args['ref']) ?? '', action, record(args['args']));
@@ -229,7 +305,7 @@ export function createCommandRegistry() {
229
305
  return result;
230
306
  });
231
307
  reg.set(ReticleCommand.INSPECT, (args) => inspect(str(args['ref']) ?? '', true === args[INSPECT_REVEAL_ARG]));
232
- reg.set(ReticleCommand.ANIMATIONS, () => listAnimations());
308
+ reg.set(ReticleCommand.ANIMATIONS, (args) => listAnimations(args));
233
309
  reg.set(ReticleCommand.CLOCK, (args) => {
234
310
  if (true === args['reset']) {
235
311
  resetClock();
@@ -1,4 +1,4 @@
1
- import { HelloInstrumentation, type HelloEnv, type HelloScope, type RedactionConfig } from '@reticlehq/core';
1
+ import { HelloInstrumentation, HudVisibility, type HelloEnv, type HelloScope, type RedactionConfig } from '@reticlehq/core';
2
2
  export interface ReticleConnectOptions {
3
3
  url?: string;
4
4
  session?: string;
@@ -37,3 +37,13 @@ export declare function helloMode(options: Pick<ReticleConnectOptions, 'env' | '
37
37
  };
38
38
  export declare function shouldBlockProduction(nodeEnv: string | undefined, allowInProduction: boolean): boolean;
39
39
  export declare function leaseToken(options: ReticleConnectOptions, current: string | undefined): string | undefined;
40
+ export declare function oneOf<T extends Record<string, string>>(values: T, raw: unknown): T[keyof T] | undefined;
41
+ export declare function reticleParamsFromSearch(search: string): {
42
+ session?: string;
43
+ projectId?: string;
44
+ hud?: HudVisibility;
45
+ };
46
+ export declare function hudWithoutPanel(hud: HudVisibility | undefined): {
47
+ remember?: HudVisibility;
48
+ placed: HudVisibility[];
49
+ };
@@ -1,4 +1,4 @@
1
- import { HelloInstrumentation, } from '@reticlehq/core';
1
+ import { HelloInstrumentation, HudVisibility, RETICLE_URL_PARAM, } from '@reticlehq/core';
2
2
  export function helloMode(options) {
3
3
  return {
4
4
  ...(options.env === undefined ? {} : { env: options.env }),
@@ -16,3 +16,29 @@ export function leaseToken(options, current) {
16
16
  }
17
17
  return token;
18
18
  }
19
+ export function oneOf(values, raw) {
20
+ return Object.values(values).includes(raw) ? raw : undefined;
21
+ }
22
+ export function reticleParamsFromSearch(search) {
23
+ const params = new URLSearchParams(search);
24
+ const out = {};
25
+ const session = params.get(RETICLE_URL_PARAM.SESSION);
26
+ const projectId = params.get(RETICLE_URL_PARAM.PROJECT);
27
+ const hud = oneOf(HudVisibility, params.get(RETICLE_URL_PARAM.HUD));
28
+ if (session !== null && session.length > 0) {
29
+ out.session = session;
30
+ }
31
+ if (projectId !== null && projectId.length > 0) {
32
+ out.projectId = projectId;
33
+ }
34
+ if (hud !== undefined) {
35
+ out.hud = hud;
36
+ }
37
+ return out;
38
+ }
39
+ export function hudWithoutPanel(hud) {
40
+ if (hud === undefined) {
41
+ return { placed: [] };
42
+ }
43
+ return { remember: hud, placed: HudVisibility.SHOWN === hud ? [] : [hud] };
44
+ }
@@ -4,4 +4,4 @@ export declare function getAccessibleName(el: Element): string;
4
4
  export declare function getStates(el: Element, visible?: boolean): ElementState[];
5
5
  export declare function isSensitiveField(el: Element): boolean;
6
6
  export declare function getValue(el: Element): string | undefined;
7
- export declare function describe(el: Element, memo?: Map<Element, boolean>): ElementDescriptor;
7
+ export declare function describe(el: Element, memo?: Map<Element, boolean>, fullText?: boolean): ElementDescriptor;
package/dist/dom/a11y.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { readTestId } from './addressing/testid-attr.js';
2
- import { DEFAULT_TESTID_ATTR, ElementState, REDACTED_VALUE, } from '@reticlehq/core';
2
+ import { DEFAULT_TESTID_ATTR, ElementState, REDACTED_VALUE, TRANSPORT_LIMITS, } from '@reticlehq/core';
3
3
  import { isButton, isImage, isInput, isMeter, isOutput, isProgress, isSelect, isTextArea, } from './realm.js';
4
4
  import { refs } from './addressing/refs.js';
5
5
  import { inspectChart } from './chart.js';
6
6
  import { isSensitiveKey } from '../security/serialization.js';
7
7
  import { formatSource, sourceFromDom } from './addressing/source.js';
8
- import { HTML_DETAILS_OPEN_ATTRIBUTE, HTML_DETAILS_TAG, HTML_SUMMARY_TAG, isVisible, } from './visibility.js';
8
+ import { HTML_DETAILS_OPEN_ATTRIBUTE, HTML_DETAILS_TAG, HTML_SUMMARY_TAG, isNonRendered, isVisible, } from './visibility.js';
9
9
  const NAME_FROM_CONTENT = new Set([
10
10
  'button',
11
11
  'link',
@@ -30,6 +30,7 @@ const NAME_FROM_CONTENT = new Set([
30
30
  'alert',
31
31
  ]);
32
32
  const INPUT_TEXT_TYPES = new Set(['text', 'email', 'tel', 'url', 'password', '']);
33
+ const INPUT_BUTTON_TYPES = new Set(['submit', 'button', 'reset', 'file', 'image']);
33
34
  function inputRole(input) {
34
35
  const type = input.type.toLowerCase();
35
36
  if ('search' === type) {
@@ -50,7 +51,7 @@ function inputRole(input) {
50
51
  if ('number' === type) {
51
52
  return 'spinbutton';
52
53
  }
53
- if ('submit' === type || 'button' === type || 'reset' === type) {
54
+ if (INPUT_BUTTON_TYPES.has(type)) {
54
55
  return 'button';
55
56
  }
56
57
  return 'textbox';
@@ -182,7 +183,7 @@ function textWithoutHidden(node) {
182
183
  return '';
183
184
  }
184
185
  const el = node;
185
- if ('true' === el.getAttribute('aria-hidden')) {
186
+ if ('true' === el.getAttribute('aria-hidden') || isNonRendered(el)) {
186
187
  return '';
187
188
  }
188
189
  if (isImage(el)) {
@@ -360,14 +361,14 @@ export function getValue(el) {
360
361
  const valueNow = el.getAttribute('aria-valuenow');
361
362
  return valueNow ?? undefined;
362
363
  }
363
- const MAX_TEXT = 80;
364
- function getVisibleText(el) {
364
+ function getVisibleText(el, fullText) {
365
365
  const text = collapse(el.textContent ?? '');
366
- return text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT)}…` : text;
366
+ const max = fullText ? TRANSPORT_LIMITS.MAX_FULL_TEXT : TRANSPORT_LIMITS.MAX_DESCRIBED_TEXT;
367
+ return text.length > max ? `${text.slice(0, max)}…` : text;
367
368
  }
368
- export function describe(el, memo) {
369
+ export function describe(el, memo, fullText = false) {
369
370
  const value = getValue(el);
370
- const text = getVisibleText(el);
371
+ const text = getVisibleText(el, fullText);
371
372
  const name = getAccessibleName(el);
372
373
  const visible = isVisible(el, memo);
373
374
  const base = {
@@ -380,7 +381,7 @@ export function describe(el, memo) {
380
381
  if (value !== undefined && value.length > 0) {
381
382
  base.value = value;
382
383
  }
383
- if (text.length > 0 && text !== name) {
384
+ if (text.length > 0 && (fullText || text !== name)) {
384
385
  base.text = text;
385
386
  }
386
387
  const source = formatSource(sourceFromDom(el));
@@ -0,0 +1,7 @@
1
+ export declare const CHART_SHAPE_CAP = 120;
2
+ export interface ChartReading {
3
+ label: string;
4
+ shapes: string[];
5
+ cut?: number;
6
+ }
7
+ export declare function readCharts(root: Element): ChartReading[];
@@ -0,0 +1,39 @@
1
+ export const CHART_SHAPE_CAP = 120;
2
+ const MIN_CHART_WIDTH = 120;
3
+ const MIN_CHART_HEIGHT = 60;
4
+ const SHAPE_TAGS = ['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path'];
5
+ const NOT_DRAWN = 'defs, clipPath, mask, pattern, marker, symbol';
6
+ const pct = (v, of) => {
7
+ const n = Math.round((v / of) * 1000) / 10;
8
+ return Object.is(n, -0) ? '0' : String(n);
9
+ };
10
+ export function readCharts(root) {
11
+ const out = [];
12
+ const svgs = Array.from(root.querySelectorAll('svg')).filter((s) => null === s.parentElement?.closest('svg'));
13
+ for (const svg of svgs) {
14
+ const frame = svg.getBoundingClientRect();
15
+ if (frame.width < MIN_CHART_WIDTH || frame.height < MIN_CHART_HEIGHT) {
16
+ continue;
17
+ }
18
+ const marks = Array.from(svg.querySelectorAll(SHAPE_TAGS.join(','))).filter((el) => null === el.closest(NOT_DRAWN));
19
+ const shapes = [];
20
+ for (const el of marks) {
21
+ const b = el.getBoundingClientRect();
22
+ if (0 === b.width && 0 === b.height) {
23
+ continue;
24
+ }
25
+ const x = pct(b.left - frame.left, frame.width);
26
+ const y = pct(b.top - frame.top, frame.height);
27
+ shapes.push(`${el.tagName.toLowerCase()} ${x} ${y} ${pct(b.width, frame.width)} ${pct(b.height, frame.height)}`);
28
+ }
29
+ if (0 === shapes.length) {
30
+ continue;
31
+ }
32
+ const label = svg.getAttribute('aria-label') ?? `chart ${String(out.length + 1)}`;
33
+ const kept = shapes.slice(0, CHART_SHAPE_CAP);
34
+ out.push(shapes.length > CHART_SHAPE_CAP
35
+ ? { label, shapes: kept, cut: shapes.length - CHART_SHAPE_CAP }
36
+ : { label, shapes: kept });
37
+ }
38
+ return out;
39
+ }
@@ -13,6 +13,10 @@ export function hitTestOccluder(el, rect) {
13
13
  const doc = el.ownerDocument;
14
14
  if (typeof doc.elementFromPoint !== 'function')
15
15
  return null;
16
+ const passThrough = pointerPassesThrough(el, rect);
17
+ if (passThrough !== undefined) {
18
+ return passThrough;
19
+ }
16
20
  const withStack = doc;
17
21
  const isOursButNotTheTarget = (candidate) => isReticleUi(candidate) &&
18
22
  candidate !== el &&
@@ -41,3 +45,20 @@ export function hitTestOccluder(el, rect) {
41
45
  }
42
46
  return worstCount >= BLOCKED_MAJORITY ? worst : null;
43
47
  }
48
+ function pointerPassesThrough(el, rect) {
49
+ const view = el.ownerDocument.defaultView;
50
+ if (null === view || 'none' !== view.getComputedStyle(el).pointerEvents) {
51
+ return undefined;
52
+ }
53
+ const top = el.ownerDocument.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
54
+ if (null === top || el.contains(top) || isReticleUi(top)) {
55
+ return null;
56
+ }
57
+ const labels = 'labels' in el ? el.labels : null;
58
+ for (const label of labels ?? []) {
59
+ if (label === top || label.contains(top)) {
60
+ return null;
61
+ }
62
+ }
63
+ return top;
64
+ }
@@ -1,6 +1,6 @@
1
1
  import { type ElementQuery } from '@reticlehq/core';
2
2
  export declare function elementsUnder(container: HTMLElement): HTMLElement[];
3
- export declare function isNonRendered(el: Element): boolean;
3
+ export declare function ownWords(el: Element): string;
4
4
  export declare function isHiddenNativeOption(el: HTMLElement): boolean;
5
5
  export declare function queryByRoleAndName(container: HTMLElement, role: string, name: string | undefined): HTMLElement[];
6
6
  export declare function findIn(container: HTMLElement, query: ElementQuery): HTMLElement[];
@@ -1,7 +1,7 @@
1
1
  import { DATA_RETICLE_SOURCE_ATTR, QueryBy } from '@reticlehq/core';
2
2
  import { isInput, isSelect, isTextArea } from './realm.js';
3
3
  import { getAccessibleName, getRole } from './a11y.js';
4
- import { isVisible } from './visibility.js';
4
+ import { isNonRendered, isVisible } from './visibility.js';
5
5
  import { identifyComponent } from '../registry/stores/adapters.js';
6
6
  import { readTestId, testIdSelector } from './addressing/testid-attr.js';
7
7
  import { exactVisibleText, fuzzyVisibleText, comparableText, paintedCase, textMatches, } from './text-match.js';
@@ -12,10 +12,6 @@ export function elementsUnder(container) {
12
12
  const self = Node.ELEMENT_NODE === container.nodeType ? [container] : [];
13
13
  return [...self, ...Array.from(container.querySelectorAll('*'))];
14
14
  }
15
- const NON_RENDERED_TAGS = ['script', 'style', 'template', 'noscript'];
16
- export function isNonRendered(el) {
17
- return NON_RENDERED_TAGS.includes(el.tagName.toLowerCase());
18
- }
19
15
  function directText(el) {
20
16
  if (isInput(el)) {
21
17
  const type = el.type.toLowerCase();
@@ -67,15 +63,22 @@ function findByComponentName(container, component) {
67
63
  }
68
64
  return out;
69
65
  }
66
+ export function ownWords(el) {
67
+ return (el.textContent ?? '').replace(/\s+/g, ' ').trim();
68
+ }
70
69
  function findByComponent(container, query) {
70
+ const words = query.text === undefined ? undefined : query.text.replace(/\s+/g, ' ').trim();
71
+ const role = query.role;
72
+ const keep = (els) => els.filter((el) => (words === undefined || ownWords(el) === words) &&
73
+ (role === undefined || matchesRole(getRole(el), role)));
71
74
  if (query.source !== undefined) {
72
- const bySource = findBySource(container, query.source);
75
+ const bySource = keep(findBySource(container, query.source));
73
76
  if (bySource.length > 0) {
74
77
  return bySource;
75
78
  }
76
79
  }
77
80
  if (query.component !== undefined && query.component.length > 0) {
78
- return findByComponentName(container, query.component);
81
+ return keep(findByComponentName(container, query.component));
79
82
  }
80
83
  return [];
81
84
  }
@@ -1,5 +1,9 @@
1
- import { ElementState, type ElementQuery, type MatchResult, type QueryResult } from '@reticlehq/core';
1
+ import { ElementState, type ElementQuery, type MatchResult, type NearestTarget, type QueryResult } from '@reticlehq/core';
2
+ export declare function embeddedRootsUnder(root: HTMLElement): HTMLElement[];
2
3
  export declare const ATTR_VALUE_MAX = 512;
3
- export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number, wholePhrase?: boolean, diagnose?: boolean): MatchResult;
4
+ export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number, wholePhrase?: boolean, diagnose?: boolean, fullText?: boolean): MatchResult;
5
+ export declare const MAX_NEAREST_NAME = 60;
6
+ export declare function nameSimilarity(a: string, b: string): number;
7
+ export declare function nearestTargets(container: HTMLElement, query: ElementQuery): NearestTarget[];
4
8
  export declare function elementsMatching(query: ElementQuery): HTMLElement[];
5
9
  export declare function runQuery(query: ElementQuery, limit?: number): QueryResult;