fast-replay 0.1.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/dist/api.d.ts +65 -0
  4. package/dist/api.js +78 -0
  5. package/dist/api.js.map +1 -0
  6. package/dist/browser.d.ts +67 -0
  7. package/dist/browser.js +77 -0
  8. package/dist/browser.js.map +1 -0
  9. package/dist/cli/format.d.ts +12 -0
  10. package/dist/cli/format.js +55 -0
  11. package/dist/cli/format.js.map +1 -0
  12. package/dist/cli/index.d.ts +2 -0
  13. package/dist/cli/index.js +199 -0
  14. package/dist/cli/index.js.map +1 -0
  15. package/dist/compiler/compile.d.ts +18 -0
  16. package/dist/compiler/compile.js +226 -0
  17. package/dist/compiler/compile.js.map +1 -0
  18. package/dist/compiler/normalize.d.ts +16 -0
  19. package/dist/compiler/normalize.js +75 -0
  20. package/dist/compiler/normalize.js.map +1 -0
  21. package/dist/compiler/waits.d.ts +31 -0
  22. package/dist/compiler/waits.js +69 -0
  23. package/dist/compiler/waits.js.map +1 -0
  24. package/dist/ir/io.d.ts +47 -0
  25. package/dist/ir/io.js +122 -0
  26. package/dist/ir/io.js.map +1 -0
  27. package/dist/ir/schema.d.ts +236 -0
  28. package/dist/ir/schema.js +153 -0
  29. package/dist/ir/schema.js.map +1 -0
  30. package/dist/mcp/index.d.ts +2 -0
  31. package/dist/mcp/index.js +22 -0
  32. package/dist/mcp/index.js.map +1 -0
  33. package/dist/mcp/server.d.ts +8 -0
  34. package/dist/mcp/server.js +268 -0
  35. package/dist/mcp/server.js.map +1 -0
  36. package/dist/noise.d.ts +20 -0
  37. package/dist/noise.js +70 -0
  38. package/dist/noise.js.map +1 -0
  39. package/dist/recorder/agent/capture.d.ts +15 -0
  40. package/dist/recorder/agent/capture.js +310 -0
  41. package/dist/recorder/agent/capture.js.map +1 -0
  42. package/dist/recorder/agent/config.d.ts +25 -0
  43. package/dist/recorder/agent/config.js +23 -0
  44. package/dist/recorder/agent/config.js.map +1 -0
  45. package/dist/recorder/agent/dom-reaction.d.ts +13 -0
  46. package/dist/recorder/agent/dom-reaction.js +166 -0
  47. package/dist/recorder/agent/dom-reaction.js.map +1 -0
  48. package/dist/recorder/agent/main.d.ts +1 -0
  49. package/dist/recorder/agent/main.js +14 -0
  50. package/dist/recorder/agent/main.js.map +1 -0
  51. package/dist/recorder/agent/page-agent.d.ts +12 -0
  52. package/dist/recorder/agent/page-agent.js +40 -0
  53. package/dist/recorder/agent/page-agent.js.map +1 -0
  54. package/dist/recorder/agent/reveal-tracker.d.ts +26 -0
  55. package/dist/recorder/agent/reveal-tracker.js +41 -0
  56. package/dist/recorder/agent/reveal-tracker.js.map +1 -0
  57. package/dist/recorder/agent/roles.d.ts +11 -0
  58. package/dist/recorder/agent/roles.js +148 -0
  59. package/dist/recorder/agent/roles.js.map +1 -0
  60. package/dist/recorder/agent/selectors.d.ts +26 -0
  61. package/dist/recorder/agent/selectors.js +256 -0
  62. package/dist/recorder/agent/selectors.js.map +1 -0
  63. package/dist/recorder/agent/text.d.ts +19 -0
  64. package/dist/recorder/agent/text.js +82 -0
  65. package/dist/recorder/agent/text.js.map +1 -0
  66. package/dist/recorder/agent/transport.d.ts +14 -0
  67. package/dist/recorder/agent/transport.js +43 -0
  68. package/dist/recorder/agent/transport.js.map +1 -0
  69. package/dist/recorder/agent/visibility.d.ts +8 -0
  70. package/dist/recorder/agent/visibility.js +20 -0
  71. package/dist/recorder/agent/visibility.js.map +1 -0
  72. package/dist/recorder/agent-bundle.generated.d.ts +2 -0
  73. package/dist/recorder/agent-bundle.generated.js +6 -0
  74. package/dist/recorder/agent-bundle.generated.js.map +1 -0
  75. package/dist/recorder/attach.d.ts +52 -0
  76. package/dist/recorder/attach.js +159 -0
  77. package/dist/recorder/attach.js.map +1 -0
  78. package/dist/recorder/instrument.d.ts +17 -0
  79. package/dist/recorder/instrument.js +24 -0
  80. package/dist/recorder/instrument.js.map +1 -0
  81. package/dist/recorder/launch.d.ts +38 -0
  82. package/dist/recorder/launch.js +72 -0
  83. package/dist/recorder/launch.js.map +1 -0
  84. package/dist/recorder/reaction.d.ts +13 -0
  85. package/dist/recorder/reaction.js +65 -0
  86. package/dist/recorder/reaction.js.map +1 -0
  87. package/dist/recorder/types.d.ts +62 -0
  88. package/dist/recorder/types.js +2 -0
  89. package/dist/recorder/types.js.map +1 -0
  90. package/dist/replayer/artifacts.d.ts +25 -0
  91. package/dist/replayer/artifacts.js +58 -0
  92. package/dist/replayer/artifacts.js.map +1 -0
  93. package/dist/replayer/invariants.d.ts +26 -0
  94. package/dist/replayer/invariants.js +109 -0
  95. package/dist/replayer/invariants.js.map +1 -0
  96. package/dist/replayer/resolve.d.ts +35 -0
  97. package/dist/replayer/resolve.js +50 -0
  98. package/dist/replayer/resolve.js.map +1 -0
  99. package/dist/replayer/run.d.ts +104 -0
  100. package/dist/replayer/run.js +467 -0
  101. package/dist/replayer/run.js.map +1 -0
  102. package/dist/replayer/values.d.ts +13 -0
  103. package/dist/replayer/values.js +76 -0
  104. package/dist/replayer/values.js.map +1 -0
  105. package/dist/replayer/waits.d.ts +25 -0
  106. package/dist/replayer/waits.js +90 -0
  107. package/dist/replayer/waits.js.map +1 -0
  108. package/dist/version.d.ts +1 -0
  109. package/dist/version.js +10 -0
  110. package/dist/version.js.map +1 -0
  111. package/package.json +77 -0
@@ -0,0 +1,26 @@
1
+ import type { AgentConfig } from './config.js';
2
+ /**
3
+ * Decides when a hover deserves to become a step of its own.
4
+ *
5
+ * Almost every hover is incidental mouse travel and must be dropped, or the IR
6
+ * would be unreadable. The exception is the hover-to-open menu: if hovering
7
+ * revealed the content we then acted on, replay must reproduce that hover or it
8
+ * will click an element that is not there yet.
9
+ *
10
+ * This owns the three pieces of state that decision needs, which is why it is a
11
+ * unit rather than loose variables shared between the observer and the capture
12
+ * listeners.
13
+ */
14
+ export interface RevealTracker {
15
+ noteHover(el: Element): void;
16
+ /** Called for every emitted action, to date the reveal cooldown. */
17
+ noteAction(at: number): void;
18
+ /** Called by the DOM observer with nodes that just became visible. */
19
+ noteRevealed(nodes: Element[], at: number): void;
20
+ /**
21
+ * Consumes the pending reveal. Returns the hovered element only if it
22
+ * revealed the thing being acted on now; null otherwise.
23
+ */
24
+ takeLoadBearingHover(actionTarget: Element | null): Element | null;
25
+ }
26
+ export declare function createRevealTracker(config: AgentConfig): RevealTracker;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * DOM landing right after an action is that action's consequence, not a
3
+ * hover's. Without this, clicking "Add" and then clicking inside the new row
4
+ * would invent a bogus hover step on the Add button.
5
+ */
6
+ const ACTION_COOLDOWN_MS = 600;
7
+ export function createRevealTracker(config) {
8
+ let pending = null;
9
+ let lastHovered = null;
10
+ let lastActionAt = 0;
11
+ return {
12
+ noteHover(el) {
13
+ lastHovered = el;
14
+ },
15
+ noteAction(at) {
16
+ lastActionAt = at;
17
+ },
18
+ noteRevealed(nodes, at) {
19
+ if (!nodes.length || !lastHovered)
20
+ return;
21
+ if (at - lastActionAt <= ACTION_COOLDOWN_MS)
22
+ return;
23
+ pending = { el: lastHovered, nodes, at };
24
+ },
25
+ takeLoadBearingHover(actionTarget) {
26
+ const reveal = pending;
27
+ pending = null;
28
+ if (!reveal || !actionTarget)
29
+ return null;
30
+ if (Date.now() - reveal.at > config.hoverRevealWindowMs)
31
+ return null;
32
+ if (!reveal.el.isConnected)
33
+ return null;
34
+ // Load-bearing means: the thing we are about to act on lives inside what
35
+ // the hover revealed.
36
+ const inside = reveal.nodes.some((n) => n === actionTarget || n.contains(actionTarget));
37
+ return inside ? reveal.el : null;
38
+ },
39
+ };
40
+ }
41
+ //# sourceMappingURL=reveal-tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reveal-tracker.js","sourceRoot":"","sources":["../../../src/recorder/agent/reveal-tracker.ts"],"names":[],"mappings":"AA2BA;;;;GAIG;AACH,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAE/B,MAAM,UAAU,mBAAmB,CAAC,MAAmB;IACrD,IAAI,OAAO,GAAyD,IAAI,CAAC;IACzE,IAAI,WAAW,GAAmB,IAAI,CAAC;IACvC,IAAI,YAAY,GAAG,CAAC,CAAC;IAErB,OAAO;QACL,SAAS,CAAC,EAAE;YACV,WAAW,GAAG,EAAE,CAAC;QACnB,CAAC;QAED,UAAU,CAAC,EAAE;YACX,YAAY,GAAG,EAAE,CAAC;QACpB,CAAC;QAED,YAAY,CAAC,KAAK,EAAE,EAAE;YACpB,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,WAAW;gBAAE,OAAO;YAC1C,IAAI,EAAE,GAAG,YAAY,IAAI,kBAAkB;gBAAE,OAAO;YACpD,OAAO,GAAG,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QAC3C,CAAC;QAED,oBAAoB,CAAC,YAAY;YAC/B,MAAM,MAAM,GAAG,OAAO,CAAC;YACvB,OAAO,GAAG,IAAI,CAAC;YACf,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY;gBAAE,OAAO,IAAI,CAAC;YAC1C,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,EAAE,GAAG,MAAM,CAAC,mBAAmB;gBAAE,OAAO,IAAI,CAAC;YACrE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,WAAW;gBAAE,OAAO,IAAI,CAAC;YACxC,yEAAyE;YACzE,sBAAsB;YACtB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,YAAY,IAAI,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;YACxF,OAAO,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACnC,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ export declare function getRole(el: Element): string | null;
2
+ /** Elements whose value is typed rather than toggled — the `fill` targets. */
3
+ export declare function isEditable(el: Element | null): el is HTMLElement;
4
+ /**
5
+ * A pragmatic subset of the accname algorithm — enough to make role+name
6
+ * selectors useful, without shipping the full 600-line spec. Order follows
7
+ * accname's precedence for the sources we do implement.
8
+ */
9
+ export declare function accessibleName(el: Element): string;
10
+ /** Text of the element itself, used for the `text=` selector candidate. */
11
+ export declare function ownText(el: Element): string;
@@ -0,0 +1,148 @@
1
+ import { clean, escAttr } from './text.js';
2
+ /** ARIA roles and accessible names, computed from a live element. */
3
+ const TAG_ROLES = {
4
+ button: 'button',
5
+ select: 'combobox',
6
+ textarea: 'textbox',
7
+ nav: 'navigation',
8
+ main: 'main',
9
+ header: 'banner',
10
+ footer: 'contentinfo',
11
+ aside: 'complementary',
12
+ form: 'form',
13
+ table: 'table',
14
+ tr: 'row',
15
+ td: 'cell',
16
+ th: 'columnheader',
17
+ ul: 'list',
18
+ ol: 'list',
19
+ li: 'listitem',
20
+ dialog: 'dialog',
21
+ option: 'option',
22
+ h1: 'heading',
23
+ h2: 'heading',
24
+ h3: 'heading',
25
+ h4: 'heading',
26
+ h5: 'heading',
27
+ h6: 'heading',
28
+ };
29
+ const INPUT_ROLES = {
30
+ button: 'button',
31
+ submit: 'button',
32
+ reset: 'button',
33
+ image: 'button',
34
+ checkbox: 'checkbox',
35
+ radio: 'radio',
36
+ range: 'slider',
37
+ number: 'spinbutton',
38
+ search: 'searchbox',
39
+ email: 'textbox',
40
+ tel: 'textbox',
41
+ text: 'textbox',
42
+ url: 'textbox',
43
+ };
44
+ /** Roles whose accessible name comes from their own text content. */
45
+ const NAME_FROM_CONTENT = [
46
+ 'button',
47
+ 'link',
48
+ 'heading',
49
+ 'cell',
50
+ 'columnheader',
51
+ 'rowheader',
52
+ 'listitem',
53
+ 'option',
54
+ 'tab',
55
+ 'menuitem',
56
+ 'menuitemcheckbox',
57
+ 'menuitemradio',
58
+ 'treeitem',
59
+ 'switch',
60
+ 'checkbox',
61
+ 'radio',
62
+ ];
63
+ const NON_EDITABLE_INPUTS = ['checkbox', 'radio', 'button', 'submit', 'reset', 'file', 'image'];
64
+ export function getRole(el) {
65
+ const explicit = el.getAttribute('role');
66
+ if (explicit)
67
+ return explicit.trim().split(/\s+/)[0] || null;
68
+ const tag = el.tagName.toLowerCase();
69
+ if (tag === 'a')
70
+ return el.hasAttribute('href') ? 'link' : null;
71
+ if (tag === 'img')
72
+ return el.getAttribute('alt') === '' ? null : 'img';
73
+ if (tag === 'input') {
74
+ const type = (el.getAttribute('type') || 'text').toLowerCase();
75
+ return INPUT_ROLES[type] ?? null;
76
+ }
77
+ return TAG_ROLES[tag] ?? null;
78
+ }
79
+ /** Elements whose value is typed rather than toggled — the `fill` targets. */
80
+ export function isEditable(el) {
81
+ if (!el || el.nodeType !== 1)
82
+ return false;
83
+ const tag = el.tagName.toLowerCase();
84
+ if (tag === 'textarea')
85
+ return true;
86
+ if (el.isContentEditable)
87
+ return true;
88
+ if (tag !== 'input')
89
+ return false;
90
+ const type = (el.getAttribute('type') || 'text').toLowerCase();
91
+ return !NON_EDITABLE_INPUTS.includes(type);
92
+ }
93
+ /**
94
+ * A pragmatic subset of the accname algorithm — enough to make role+name
95
+ * selectors useful, without shipping the full 600-line spec. Order follows
96
+ * accname's precedence for the sources we do implement.
97
+ */
98
+ export function accessibleName(el) {
99
+ const aria = el.getAttribute('aria-label');
100
+ if (aria && aria.trim())
101
+ return clean(aria);
102
+ const labelledby = el.getAttribute('aria-labelledby');
103
+ if (labelledby) {
104
+ const parts = labelledby
105
+ .split(/\s+/)
106
+ .map((id) => document.getElementById(id))
107
+ .filter((n) => !!n)
108
+ .map((n) => clean(n.textContent));
109
+ const joined = clean(parts.filter(Boolean).join(' '));
110
+ if (joined)
111
+ return joined;
112
+ }
113
+ const tag = el.tagName.toLowerCase();
114
+ if (['input', 'select', 'textarea'].includes(tag)) {
115
+ const id = el.getAttribute('id');
116
+ if (id) {
117
+ const forLabel = document.querySelector(`label[for="${escAttr(id)}"]`);
118
+ if (forLabel) {
119
+ const t = clean(forLabel.textContent);
120
+ if (t)
121
+ return t;
122
+ }
123
+ }
124
+ const wrapping = el.closest('label');
125
+ if (wrapping) {
126
+ const t = clean(wrapping.textContent);
127
+ if (t)
128
+ return t;
129
+ }
130
+ }
131
+ for (const attr of ['alt', 'title', 'placeholder']) {
132
+ const v = el.getAttribute(attr);
133
+ if (v && v.trim())
134
+ return clean(v);
135
+ }
136
+ const role = getRole(el);
137
+ if (role && NAME_FROM_CONTENT.includes(role)) {
138
+ const t = clean(el.textContent);
139
+ if (t)
140
+ return t;
141
+ }
142
+ return '';
143
+ }
144
+ /** Text of the element itself, used for the `text=` selector candidate. */
145
+ export function ownText(el) {
146
+ return clean(el.textContent, 60);
147
+ }
148
+ //# sourceMappingURL=roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.js","sourceRoot":"","sources":["../../../src/recorder/agent/roles.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { CapturedTarget } from '../types.js';
2
+ export declare function testIdSelector(el: Element): string | null;
3
+ export declare function idSelector(el: Element): string | null;
4
+ export declare function roleNameSelector(el: Element): string | null;
5
+ /**
6
+ * Selectors durable enough to wait on. `appeared` tolerates role+name because
7
+ * waiting for *a* matching element is the right semantic; `gone` does not,
8
+ * because a wait for count===0 would never settle while siblings remain.
9
+ */
10
+ export declare function appearedSelector(el: Element): string | null;
11
+ export declare function goneSelector(el: Element): string | null;
12
+ export declare function buildCssPath(el: Element): string | null;
13
+ /**
14
+ * Candidates in priority order:
15
+ * test id / stable id → name attribute → role + accessible name
16
+ * → stable CSS path → text anchor
17
+ */
18
+ export declare function buildCandidates(el: Element): string[];
19
+ /**
20
+ * Plain-language description of an element, from role + accessible name +
21
+ * nearest row/section label. Rule-based, no LLM. Surfaces in failure messages
22
+ * and is the re-grounding prompt for Phase 1 self-heal.
23
+ */
24
+ export declare function semanticOf(el: Element): string;
25
+ /** Null when the element has no addressable selector at all. */
26
+ export declare function describe(el: Element): CapturedTarget | null;
@@ -0,0 +1,256 @@
1
+ import { accessibleName, getRole, ownText } from './roles.js';
2
+ import { clean, escAttr, escId, isStableClass, isStableToken, renderedText } from './text.js';
3
+ /**
4
+ * Selector-candidate generation.
5
+ *
6
+ * Everything here runs while the element is still live and attached, which is
7
+ * the only moment these can be computed reliably — and the only way `domGone`
8
+ * can name a node that is already detached by the time we report it.
9
+ */
10
+ /**
11
+ * Attributes an app deliberately puts on an element to identify it. Ordered by
12
+ * how conventional they are.
13
+ *
14
+ * `data-sentry-label` is here because real apps label elements for telemetry
15
+ * far more consistently than they add test ids — Expensify labels nearly every
16
+ * interactive element that way, and those labels are as stable as a test id.
17
+ */
18
+ const TEST_ID_ATTRS = [
19
+ 'data-testid',
20
+ 'data-test',
21
+ 'data-test-id',
22
+ 'data-cy',
23
+ 'data-sentry-label',
24
+ 'data-testid-label',
25
+ ];
26
+ const MAX_CANDIDATES = 5;
27
+ const MAX_CSS_PATH_DEPTH = 6;
28
+ export function testIdSelector(el) {
29
+ for (const attr of TEST_ID_ATTRS) {
30
+ const v = el.getAttribute(attr);
31
+ if (v)
32
+ return `[${attr}="${escAttr(v)}"]`;
33
+ }
34
+ return null;
35
+ }
36
+ export function idSelector(el) {
37
+ const id = el.getAttribute('id');
38
+ return id && isStableToken(id) ? `#${escId(id)}` : null;
39
+ }
40
+ export function roleNameSelector(el) {
41
+ const role = getRole(el);
42
+ if (!role)
43
+ return null;
44
+ const name = accessibleName(el);
45
+ if (!name)
46
+ return null;
47
+ return `role=${role}[name="${escAttr(name)}"]`;
48
+ }
49
+ /**
50
+ * Selectors durable enough to wait on. `appeared` tolerates role+name because
51
+ * waiting for *a* matching element is the right semantic; `gone` does not,
52
+ * because a wait for count===0 would never settle while siblings remain.
53
+ */
54
+ export function appearedSelector(el) {
55
+ return testIdSelector(el) ?? idSelector(el) ?? roleNameSelector(el);
56
+ }
57
+ export function goneSelector(el) {
58
+ return testIdSelector(el) ?? idSelector(el);
59
+ }
60
+ function allElements() {
61
+ return Array.from(document.querySelectorAll('*'));
62
+ }
63
+ /** Append `>> nth=N` only when the base selector is genuinely ambiguous. */
64
+ function withNth(base, el, matches) {
65
+ if (matches.length <= 1)
66
+ return base;
67
+ const idx = matches.indexOf(el);
68
+ return idx < 0 ? base : `${base} >> nth=${idx}`;
69
+ }
70
+ export function buildCssPath(el) {
71
+ const segs = [];
72
+ let cur = el;
73
+ let depth = 0;
74
+ while (cur && cur.nodeType === 1 && cur !== document.documentElement && depth < MAX_CSS_PATH_DEPTH) {
75
+ const node = cur;
76
+ // Anchoring on a testid/id ancestor beats a long fragile tag chain.
77
+ if (depth > 0) {
78
+ const anchor = testIdSelector(node) ?? idSelector(node);
79
+ if (anchor) {
80
+ segs.unshift(anchor);
81
+ break;
82
+ }
83
+ }
84
+ let seg = node.tagName.toLowerCase();
85
+ const classes = Array.from(node.classList).filter(isStableClass).slice(0, 2);
86
+ if (classes.length)
87
+ seg += `.${classes.map(escId).join('.')}`;
88
+ const parent = node.parentElement;
89
+ if (parent) {
90
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === node.tagName);
91
+ if (sameTag.length > 1)
92
+ seg += `:nth-of-type(${sameTag.indexOf(node) + 1})`;
93
+ }
94
+ segs.unshift(seg);
95
+ const partial = segs.join(' > ');
96
+ try {
97
+ if (document.querySelectorAll(partial).length === 1)
98
+ return partial;
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ cur = parent;
104
+ depth++;
105
+ }
106
+ if (!segs.length)
107
+ return null;
108
+ const sel = segs.join(' > ');
109
+ try {
110
+ return document.querySelectorAll(sel).length ? sel : null;
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ /**
117
+ * Candidates in priority order:
118
+ * test id / stable id → name attribute → role + accessible name
119
+ * → stable CSS path → text anchor
120
+ */
121
+ export function buildCandidates(el) {
122
+ const out = [];
123
+ const push = (s) => {
124
+ if (s && out.indexOf(s) === -1)
125
+ out.push(s);
126
+ };
127
+ push(testIdSelector(el));
128
+ push(idSelector(el));
129
+ const nameAttr = el.getAttribute('name');
130
+ if (nameAttr && isStableToken(nameAttr)) {
131
+ push(`${el.tagName.toLowerCase()}[name="${escAttr(nameAttr)}"]`);
132
+ }
133
+ const roleName = roleNameSelector(el);
134
+ if (roleName) {
135
+ const role = getRole(el);
136
+ const name = accessibleName(el);
137
+ const matches = allElements().filter((c) => getRole(c) === role && accessibleName(c) === name);
138
+ push(withNth(roleName, el, matches));
139
+ }
140
+ // Ranked above the CSS path on purpose: when a click lands on an inner node
141
+ // of a labelled control, `[data-testid="x"]` is strictly more durable than
142
+ // `[data-testid="x"] > span` or a chain of :nth-of-type positions.
143
+ push(labelledAncestorSelector(el));
144
+ const cssPath = buildCssPath(el);
145
+ const text = ownText(el);
146
+ const textSelector = text && isSmallestWithText(el, text)
147
+ ? withNth(`text="${escAttr(text)}"`, el, allElements().filter((c) => isSmallestWithText(c, text)))
148
+ : null;
149
+ // A path that is unique only by sibling position breaks the moment anything
150
+ // is inserted, reordered or conditionally rendered above it — which on a list
151
+ // or a nav is routine. Visible text is not durable either, but it survives a
152
+ // reshuffle, so it goes first when the path has nothing but position to
153
+ // offer. A path anchored on an attribute or a stable class still wins.
154
+ if (isPositionalOnly(cssPath)) {
155
+ push(textSelector);
156
+ push(cssPath);
157
+ }
158
+ else {
159
+ push(cssPath);
160
+ push(textSelector);
161
+ }
162
+ return out.slice(0, MAX_CANDIDATES);
163
+ }
164
+ /** True when the selector's uniqueness rests entirely on :nth-of-type. */
165
+ function isPositionalOnly(selector) {
166
+ if (!selector)
167
+ return false;
168
+ if (!selector.includes(':nth-of-type('))
169
+ return false;
170
+ // An attribute anchor or a surviving class name carries real identity.
171
+ return !selector.includes('[') && !selector.includes('.');
172
+ }
173
+ /**
174
+ * The nearest identifiable ancestor, offered as a late fallback.
175
+ *
176
+ * When a click lands on an inner node of a labelled control, the CSS path
177
+ * descends from the label into style hashes and there is nothing else to fall
178
+ * back to. The labelled ancestor is usually clickable and always more durable,
179
+ * so it is worth having behind the more precise candidates.
180
+ *
181
+ * Only offered when the element has no identity of its own, and only for
182
+ * ancestors close enough to be the same control.
183
+ */
184
+ function labelledAncestorSelector(el) {
185
+ if (testIdSelector(el) ?? idSelector(el))
186
+ return null;
187
+ let cur = el.parentElement;
188
+ for (let depth = 0; cur && depth < 3; depth++) {
189
+ const anchor = testIdSelector(cur);
190
+ if (anchor) {
191
+ try {
192
+ // Ambiguous ancestors are worse than no fallback at all.
193
+ return document.querySelectorAll(anchor).length === 1 ? anchor : null;
194
+ }
195
+ catch {
196
+ return null;
197
+ }
198
+ }
199
+ cur = cur.parentElement;
200
+ }
201
+ return null;
202
+ }
203
+ /**
204
+ * Playwright's `text=` engine matches the *smallest* element containing the
205
+ * text, so an ancestor whose only content is that text does not match.
206
+ *
207
+ * Counting ancestors here would shift every index: a lone `<ul><li><button>Go`
208
+ * would record `text="Go" >> nth=2` when replay sees exactly one match, and the
209
+ * selector would resolve to nothing.
210
+ */
211
+ function isSmallestWithText(el, text) {
212
+ if (ownText(el) !== text)
213
+ return false;
214
+ return !Array.from(el.children).some((child) => ownText(child) === text);
215
+ }
216
+ function contextLabel(el) {
217
+ const row = el.closest('tr, [role="row"], li, [role="listitem"], [data-testid*="row"]');
218
+ if (row && row !== el) {
219
+ // Strip the element's own label so the context names its surroundings,
220
+ // not the thing we already named: "the row containing Sensor 2", not
221
+ // "the row containing Sensor 2 Delete".
222
+ const own = renderedText(el, 60);
223
+ const full = renderedText(row, 120);
224
+ const t = clean(own ? full.split(own).join(' ') : full, 60);
225
+ if (t)
226
+ return ` in the row containing "${t}"`;
227
+ }
228
+ const section = el.closest('form, section, dialog, [role="dialog"], nav, main, aside');
229
+ if (section && section !== el) {
230
+ const label = section.getAttribute('aria-label') ||
231
+ clean(section.querySelector('h1, h2, h3, h4, legend')?.textContent) ||
232
+ '';
233
+ if (label)
234
+ return ` in the ${section.tagName.toLowerCase()} labelled "${clean(label, 40)}"`;
235
+ }
236
+ return '';
237
+ }
238
+ /**
239
+ * Plain-language description of an element, from role + accessible name +
240
+ * nearest row/section label. Rule-based, no LLM. Surfaces in failure messages
241
+ * and is the re-grounding prompt for Phase 1 self-heal.
242
+ */
243
+ export function semanticOf(el) {
244
+ const role = getRole(el) || el.tagName.toLowerCase();
245
+ const name = accessibleName(el) || ownText(el);
246
+ const head = name ? `${name} ${role}` : role;
247
+ return head + contextLabel(el);
248
+ }
249
+ /** Null when the element has no addressable selector at all. */
250
+ export function describe(el) {
251
+ const candidates = buildCandidates(el);
252
+ if (!candidates.length)
253
+ return null;
254
+ return { candidates, semantic: semanticOf(el) };
255
+ }
256
+ //# sourceMappingURL=selectors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selectors.js","sourceRoot":"","sources":["../../../src/recorder/agent/selectors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9F;;;;;;GAMG;AAEH;;;;;;;GAOG;AACH,MAAM,aAAa,GAAG;IACpB,aAAa;IACb,WAAW;IACX,cAAc;IACd,SAAS;IACT,mBAAmB;IACnB,mBAAmB;CACpB,CAAC;AACF,MAAM,cAAc,GAAG,CAAC,CAAC;AACzB,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAE7B,MAAM,UAAU,cAAc,CAAC,EAAW;IACxC,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;QACjC,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC;YAAE,OAAO,IAAI,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,EAAW;IACpC,MAAM,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,EAAE,IAAI,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAAW;IAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;IACzB,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,IAAI,GAAG,cAAc,CAAC,EAAE,CAAC,CAAC;IAChC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,OAAO,QAAQ,IAAI,UAAU,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAW;IAC1C,OAAO,cAAc,CAAC,EAAE,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,IAAI,gBAAgB,CAAC,EAAE,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,EAAW;IACtC,OAAO,cAAc,CAAC,EAAE,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,WAAW;IAClB,OAAO,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,4EAA4E;AAC5E,SAAS,OAAO,CAAC,IAAY,EAAE,EAAW,EAAE,OAAkB;IAC5D,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAChC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,WAAW,GAAG,EAAE,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,EAAW;IACtC,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,IAAI,GAAG,GAAmB,EAAE,CAAC;IAC7B,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,OAAO,GAAG,IAAI,GAAG,CAAC,QAAQ,KAAK,CAAC,IAAI,GAAG,KAAK,QAAQ,CAAC,eAAe,IAAI,KAAK,GAAG,kBAAkB,EAAE,CAAC;QACnG,MAAM,IAAI,GAAY,GAAG,CAAC;QAE1B,oEAAoE;QACpE,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;YACxD,IAAI,MAAM,EAAE,CAAC;gBACX,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;gBACrB,MAAM;YACR,CAAC;QACH,CAAC;QAED,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC7E,IAAI,OAAO,CAAC,MAAM;YAAE,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAE9D,MAAM,MAAM,GAAmB,IAAI,CAAC,aAAa,CAAC;QAClD,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO,CAAC,CAAC;YACtF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;gBAAE,GAAG,IAAI,gBAAgB,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QAC9E,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAElB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,IAAI,CAAC;YACH,IAAI,QAAQ,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,OAAO,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;QAED,GAAG,GAAG,MAAM,CAAC;QACb,KAAK,EAAE,CAAC;IACV,CAAC;IAED,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7B,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,EAAW;IACzC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,IAAI,GAAG,CAAC,CAAgB,EAAQ,EAAE;QACtC,IAAI,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC,CAAC;IAEF,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC;IACzB,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;IAErB,MAAM,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IACzC,IAAI,QAAQ,IAAI,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,UAAU,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,QAAQ,GAAG,gBAAgB,CAAC,EAAE,CAAC,CAAC;IACtC,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,IAAI,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;QACzB,MAAM,IAAI,GAAG,cAAc,CAAC,EAAE,CAAC,CAAC;QAChC,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;QAC/F,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;IACvC,CAAC;IAED,4EAA4E;IAC5E,2EAA2E;IAC3E,mEAAmE;IACnE,IAAI,CAAC,wBAAwB,CAAC,EAAE,CAAC,CAAC,CAAC;IAEnC,MAAM,OAAO,GAAG,YAAY,CAAC,EAAE,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;IACzB,MAAM,YAAY,GAChB,IAAI,IAAI,kBAAkB,CAAC,EAAE,EAAE,IAAI,CAAC;QAClC,CAAC,CAAC,OAAO,CAAC,SAAS,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;QAClG,CAAC,CAAC,IAAI,CAAC;IAEX,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,wEAAwE;IACxE,uEAAuE;IACvE,IAAI,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9B,IAAI,CAAC,YAAY,CAAC,CAAC;QACnB,IAAI,CAAC,OAAO,CAAC,CAAC;IAChB,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,OAAO,CAAC,CAAC;QACd,IAAI,CAAC,YAAY,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;AACtC,CAAC;AAED,0EAA0E;AAC1E,SAAS,gBAAgB,CAAC,QAAuB;IAC/C,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5B,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,eAAe,CAAC;QAAE,OAAO,KAAK,CAAC;IACtD,uEAAuE;IACvE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,wBAAwB,CAAC,EAAW;IAC3C,IAAI,cAAc,CAAC,EAAE,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IAEtD,IAAI,GAAG,GAAG,EAAE,CAAC,aAAa,CAAC;IAC3B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,GAAG,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QAC9C,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,CAAC;gBACH,yDAAyD;gBACzD,OAAO,QAAQ,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;YACxE,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,GAAG,GAAG,GAAG,CAAC,aAAa,CAAC;IAC1B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,kBAAkB,CAAC,EAAW,EAAE,IAAY;IACnD,IAAI,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACvC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;AAC3E,CAAC;AAED,SAAS,YAAY,CAAC,EAAW;IAC/B,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,+DAA+D,CAAC,CAAC;IACxF,IAAI,GAAG,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;QACtB,uEAAuE;QACvE,qEAAqE;QACrE,wCAAwC;QACxC,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACjC,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACpC,MAAM,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC;YAAE,OAAO,2BAA2B,CAAC,GAAG,CAAC;IAChD,CAAC;IACD,MAAM,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,0DAA0D,CAAC,CAAC;IACvF,IAAI,OAAO,IAAI,OAAO,KAAK,EAAE,EAAE,CAAC;QAC9B,MAAM,KAAK,GACT,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC;YAClC,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,wBAAwB,CAAC,EAAE,WAAW,CAAC;YACnE,EAAE,CAAC;QACL,IAAI,KAAK;YAAE,OAAO,WAAW,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,cAAc,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC;IAC9F,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,EAAW;IACpC,MAAM,IAAI,GAAG,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;IACrD,MAAM,IAAI,GAAG,cAAc,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,EAAE,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7C,OAAO,IAAI,GAAG,YAAY,CAAC,EAAE,CAAC,CAAC;AACjC,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,QAAQ,CAAC,EAAW;IAClC,MAAM,UAAU,GAAG,eAAe,CAAC,EAAE,CAAC,CAAC;IACvC,IAAI,CAAC,UAAU,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC;AAClD,CAAC"}
@@ -0,0 +1,19 @@
1
+ /** String and token helpers shared across the in-page agent. */
2
+ /** Collapse whitespace, trim, and cap length with an ellipsis. */
3
+ export declare function clean(s: string | null | undefined, max?: number): string;
4
+ /** Escape a value for use inside a double-quoted selector attribute. */
5
+ export declare function escAttr(v: string): string;
6
+ /** Escape an identifier for use in a CSS selector. */
7
+ export declare function escId(v: string): string;
8
+ /**
9
+ * Reject build-generated tokens. The goal is stability across rebuilds, so
10
+ * anything carrying a content hash or a CSS-in-JS prefix is out.
11
+ */
12
+ export declare function isStableToken(t: string): boolean;
13
+ export declare function isStableClass(c: string): boolean;
14
+ /**
15
+ * Text as rendered. `textContent` concatenates across element boundaries —
16
+ * a row reads "Sensor 2Delete" — whereas `innerText` respects layout and
17
+ * separates them.
18
+ */
19
+ export declare function renderedText(el: Element, max?: number): string;
@@ -0,0 +1,82 @@
1
+ /** String and token helpers shared across the in-page agent. */
2
+ /** Collapse whitespace, trim, and cap length with an ellipsis. */
3
+ export function clean(s, max = 80) {
4
+ if (!s)
5
+ return '';
6
+ const t = s.replace(/\s+/g, ' ').trim();
7
+ return t.length > max ? `${t.slice(0, max - 1)}…` : t;
8
+ }
9
+ /** Escape a value for use inside a double-quoted selector attribute. */
10
+ export function escAttr(v) {
11
+ return v.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
12
+ }
13
+ /** Escape an identifier for use in a CSS selector. */
14
+ export function escId(v) {
15
+ return typeof window.CSS?.escape === 'function'
16
+ ? window.CSS.escape(v)
17
+ : v.replace(/([^\w-])/g, '\\$1');
18
+ }
19
+ /**
20
+ * Reject build-generated tokens. The goal is stability across rebuilds, so
21
+ * anything carrying a content hash or a CSS-in-JS prefix is out.
22
+ */
23
+ export function isStableToken(t) {
24
+ if (!t || t.length > 40)
25
+ return false;
26
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(t))
27
+ return false;
28
+ if (/^(css|sc|emotion|styled|jsx|glamor|makeStyles)-/i.test(t))
29
+ return false;
30
+ if (/[0-9a-f]{6,}/i.test(t))
31
+ return false; // embedded hash
32
+ if ((t.match(/\d/g) || []).length >= 3)
33
+ return false; // numeric noise
34
+ if (/[A-Z]/.test(t) && /\d/.test(t))
35
+ return false; // e.g. Button_root__2Xy4z
36
+ // Atomic-CSS hashes: a 1-2 char namespace then an opaque blob. React Native
37
+ // Web emits these (r-1awozwy, r-1mdbw0j) and they change whenever styling
38
+ // does, so a CSS path built from them breaks on the next restyle.
39
+ if (/^[a-z]{1,2}-[a-z0-9]{5,}$/i.test(t) && /\d/.test(t))
40
+ return false;
41
+ // Per-session ids from a11y announcers and portal libraries (#zb5bjyh-aria).
42
+ // They are regenerated on every page load, so they can never match on a
43
+ // later run — a wait on one is guaranteed to time out.
44
+ if (t.split(/[-_]/).some(looksGenerated))
45
+ return false;
46
+ // Live-region announcers (react-aria and friends) mint ids per page load and
47
+ // suffix them by role: `fvbiask-aria`, `rouspeg-diff`. The prefix is random
48
+ // but not always vowel-starved, so the suffix is the reliable tell.
49
+ if (/-(aria|diff|live|announcer|portal)$/i.test(t))
50
+ return false;
51
+ return true;
52
+ }
53
+ /**
54
+ * A token that reads like a nanoid rather than a word: long enough to be
55
+ * opaque and starved of vowels.
56
+ *
57
+ * Two shapes. With a digit, one vowel is already suspicious (`zb5bjyh`).
58
+ * Without one, it takes a total absence of vowels — which is what catches
59
+ * React Native Web's digit-free atomic hashes (`dnmrzs` in `div.r-dnmrzs`).
60
+ * `y` counts as a vowel so ordinary words like `rhythm` survive.
61
+ */
62
+ function looksGenerated(segment) {
63
+ if (segment.length < 6)
64
+ return false;
65
+ if (!/^[a-z0-9]+$/i.test(segment))
66
+ return false;
67
+ const vowels = (segment.match(/[aeiouy]/gi) || []).length;
68
+ return /\d/.test(segment) ? vowels <= 1 : vowels === 0;
69
+ }
70
+ export function isStableClass(c) {
71
+ return isStableToken(c) && !/^_/.test(c);
72
+ }
73
+ /**
74
+ * Text as rendered. `textContent` concatenates across element boundaries —
75
+ * a row reads "Sensor 2Delete" — whereas `innerText` respects layout and
76
+ * separates them.
77
+ */
78
+ export function renderedText(el, max = 80) {
79
+ const inner = el.innerText;
80
+ return clean(typeof inner === 'string' ? inner : el.textContent, max);
81
+ }
82
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../../../src/recorder/agent/text.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { PageEvent } from '../types.js';
2
+ import type { AgentConfig } from './config.js';
3
+ export interface Transport {
4
+ emit(ev: PageEvent): void;
5
+ /** Look up a Playwright-exposed binding, or null if it is not installed yet. */
6
+ binding(name: string): ((arg: unknown) => unknown) | null;
7
+ }
8
+ /**
9
+ * Carries events from the page to Node over a Playwright exposed binding.
10
+ *
11
+ * Bindings are not guaranteed to exist when the first events fire, so events
12
+ * queue until one appears rather than being dropped on the floor.
13
+ */
14
+ export declare function createTransport(config: AgentConfig): Transport;