browser-debugger-cli 0.9.0 → 0.11.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 (139) hide show
  1. package/.claude/skills/bdg/SKILL.md +268 -0
  2. package/README.md +15 -1
  3. package/dist/commands/dom/a11y.js +2 -1
  4. package/dist/commands/dom/formInteraction.js +56 -25
  5. package/dist/commands/dom/helpers/keyAttributes.d.ts +20 -0
  6. package/dist/commands/dom/helpers/keyAttributes.js +54 -0
  7. package/dist/commands/dom/helpers/query.d.ts +1 -1
  8. package/dist/commands/dom/helpers/query.js +66 -19
  9. package/dist/commands/dom/helpers/runElementCommand.js +4 -3
  10. package/dist/commands/dom/helpers/screenshot.js +85 -12
  11. package/dist/commands/dom/index.d.ts +1 -0
  12. package/dist/commands/dom/index.js +8 -3
  13. package/dist/commands/dom/inspect.d.ts +15 -0
  14. package/dist/commands/dom/inspect.js +82 -0
  15. package/dist/commands/dom/layout.js +2 -2
  16. package/dist/commands/dom/listeners.js +2 -2
  17. package/dist/commands/dom/semanticUtils.d.ts +14 -1
  18. package/dist/commands/dom/semanticUtils.js +44 -3
  19. package/dist/commands/installSkill.d.ts +20 -0
  20. package/dist/commands/installSkill.js +87 -0
  21. package/dist/commands/network/list.js +13 -2
  22. package/dist/commands/optionBehaviors.js +48 -6
  23. package/dist/commands/page.d.ts +1 -1
  24. package/dist/commands/page.js +62 -3
  25. package/dist/commands/shared/commonOptions.d.ts +4 -0
  26. package/dist/commands/shared/commonOptions.js +9 -0
  27. package/dist/commands/shared/optionTypes.d.ts +21 -0
  28. package/dist/commands/shared/startHelpers.d.ts +66 -0
  29. package/dist/commands/shared/startHelpers.js +91 -10
  30. package/dist/commands/shared/validation.d.ts +11 -0
  31. package/dist/commands/shared/validation.js +16 -0
  32. package/dist/commands.js +3 -0
  33. package/dist/daemon/launcher.d.ts +8 -1
  34. package/dist/daemon/launcher.js +3 -1
  35. package/dist/daemon/session/Session.d.ts +7 -0
  36. package/dist/daemon/session/Session.js +23 -1
  37. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  38. package/dist/daemon/session/commandRegistry.js +65 -9
  39. package/dist/daemon/session/interactions.d.ts +18 -5
  40. package/dist/daemon/session/interactions.js +22 -12
  41. package/dist/daemon.js +3565 -329
  42. package/dist/errors/messages.d.ts +85 -0
  43. package/dist/errors/messages.js +128 -1
  44. package/dist/index.js +2151 -960
  45. package/dist/ipc/client.d.ts +9 -0
  46. package/dist/ipc/client.js +13 -0
  47. package/dist/ipc/protocol/commands.d.ts +56 -1
  48. package/dist/ipc/protocol/commands.js +2 -0
  49. package/dist/ipc/protocol/domTypes.d.ts +35 -2
  50. package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
  51. package/dist/ipc/protocol/inspectTypes.js +10 -0
  52. package/dist/runtime/dom/actionEffects.d.ts +94 -15
  53. package/dist/runtime/dom/actionEffects.js +173 -27
  54. package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -14
  55. package/dist/runtime/dom/actionEffectsScripts.js +224 -32
  56. package/dist/runtime/dom/elementInfo.d.ts +26 -0
  57. package/dist/runtime/dom/elementInfo.js +65 -0
  58. package/dist/runtime/dom/eventListeners.js +14 -4
  59. package/dist/runtime/dom/formFillHelpers/fill.d.ts +3 -4
  60. package/dist/runtime/dom/formFillHelpers/fill.js +77 -28
  61. package/dist/runtime/dom/frameSelection.d.ts +11 -0
  62. package/dist/runtime/dom/frameSelection.js +20 -1
  63. package/dist/runtime/dom/frames.d.ts +38 -5
  64. package/dist/runtime/dom/frames.js +136 -21
  65. package/dist/runtime/dom/inspect.d.ts +28 -0
  66. package/dist/runtime/dom/inspect.js +557 -0
  67. package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
  68. package/dist/runtime/dom/inspectAllStyles.js +385 -0
  69. package/dist/runtime/dom/inspectCascade.d.ts +94 -0
  70. package/dist/runtime/dom/inspectCascade.js +371 -0
  71. package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
  72. package/dist/runtime/dom/inspectCascadeModel.js +232 -0
  73. package/dist/runtime/dom/inspectHints.d.ts +62 -0
  74. package/dist/runtime/dom/inspectHints.js +305 -0
  75. package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
  76. package/dist/runtime/dom/inspectLayoutModel.js +346 -0
  77. package/dist/runtime/dom/inspectModel.d.ts +74 -0
  78. package/dist/runtime/dom/inspectModel.js +184 -0
  79. package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
  80. package/dist/runtime/dom/inspectPaintModel.js +461 -0
  81. package/dist/runtime/dom/inspectRules.d.ts +37 -0
  82. package/dist/runtime/dom/inspectRules.js +101 -0
  83. package/dist/runtime/dom/inspectScripts.d.ts +132 -0
  84. package/dist/runtime/dom/inspectScripts.js +263 -0
  85. package/dist/runtime/dom/inspectTree.d.ts +40 -0
  86. package/dist/runtime/dom/inspectTree.js +134 -0
  87. package/dist/runtime/dom/inspectVariables.d.ts +33 -0
  88. package/dist/runtime/dom/inspectVariables.js +94 -0
  89. package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
  90. package/dist/runtime/dom/inspectWhyModel.js +134 -0
  91. package/dist/runtime/dom/layout.d.ts +5 -1
  92. package/dist/runtime/dom/layout.js +10 -3
  93. package/dist/runtime/dom/listenerPageScripts.d.ts +11 -5
  94. package/dist/runtime/dom/listenerPageScripts.js +95 -9
  95. package/dist/runtime/dom/listenerSummary.d.ts +4 -0
  96. package/dist/runtime/dom/listenerSummary.js +26 -9
  97. package/dist/runtime/dom/reactEventHelpers.d.ts +5 -0
  98. package/dist/runtime/dom/reactEventHelpers.js +12 -4
  99. package/dist/runtime/page/emulation.d.ts +20 -0
  100. package/dist/runtime/page/emulation.js +37 -0
  101. package/dist/telemetry/a11y.d.ts +10 -0
  102. package/dist/telemetry/a11y.js +78 -1
  103. package/dist/telemetry/console.d.ts +1 -0
  104. package/dist/telemetry/console.js +100 -5
  105. package/dist/telemetry/network.js +3 -1
  106. package/dist/types.d.ts +40 -0
  107. package/dist/ui/formatters/details.d.ts +8 -0
  108. package/dist/ui/formatters/details.js +59 -3
  109. package/dist/ui/formatters/dom.d.ts +2 -1
  110. package/dist/ui/formatters/dom.js +25 -9
  111. package/dist/ui/formatters/inspect.d.ts +39 -0
  112. package/dist/ui/formatters/inspect.js +596 -0
  113. package/dist/ui/formatters/installSkill.d.ts +11 -0
  114. package/dist/ui/formatters/installSkill.js +31 -0
  115. package/dist/ui/formatters/keyAttributes.d.ts +19 -0
  116. package/dist/ui/formatters/keyAttributes.js +84 -0
  117. package/dist/ui/formatters/layout.js +2 -2
  118. package/dist/ui/formatters/networkHeaders.d.ts +13 -0
  119. package/dist/ui/formatters/networkHeaders.js +23 -3
  120. package/dist/ui/formatters/networkList.d.ts +29 -1
  121. package/dist/ui/formatters/networkList.js +86 -20
  122. package/dist/ui/formatters/status.js +1 -1
  123. package/dist/ui/formatting.d.ts +9 -0
  124. package/dist/ui/formatting.js +6 -3
  125. package/dist/ui/messages/commands.d.ts +123 -7
  126. package/dist/ui/messages/commands.js +181 -10
  127. package/dist/ui/messages/networkMessages.d.ts +14 -0
  128. package/dist/ui/messages/networkMessages.js +18 -0
  129. package/dist/ui/messages/session.d.ts +14 -0
  130. package/dist/ui/messages/session.js +20 -0
  131. package/dist/utils/async.d.ts +9 -0
  132. package/dist/utils/async.js +17 -0
  133. package/dist/utils/color.d.ts +84 -0
  134. package/dist/utils/color.js +376 -0
  135. package/dist/utils/cssValues.d.ts +109 -0
  136. package/dist/utils/cssValues.js +236 -0
  137. package/dist/utils/selectorFilters.d.ts +12 -0
  138. package/dist/utils/selectorFilters.js +29 -0
  139. package/package.json +2 -1
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Page scripts behind the "what changed" part of DOM action results: one
3
3
  * snapshot before the action ({@link EFFECTS_START_SCRIPT}) and one read after
4
- * it ({@link EFFECTS_READ_SCRIPT}).
4
+ * it ({@link EFFECTS_READ_SCRIPT}), plus the snapshot a hover takes of what
5
+ * is hidden around its target ({@link REVEAL_SNAPSHOT_JS}).
5
6
  */
6
7
  import { ELEMENT_DESCRIPTION_JS } from './elementInfo.js';
7
8
  /** Class or id words that mark an element as a message (flash, toast, field error) */
@@ -23,14 +24,65 @@ const MESSAGE_CANDIDATES = [
23
24
  'output',
24
25
  ...MESSAGE_WORDS.flatMap((word) => [`[class*="${word}" i]`, `[id*="${word}" i]`]),
25
26
  ].join(', ');
27
+ /** Class or id words that mark an element as a loading indicator */
28
+ const LOADER_WORDS = ['loading', 'loader', 'spinner'];
29
+ /** Loading indicators by state or role, plus a cheap prefilter for the class and id words */
30
+ const LOADER_CANDIDATES = [
31
+ '[aria-busy="true"]',
32
+ '[role="progressbar"]',
33
+ ...['load', 'spinner'].flatMap((word) => [`[class*="${word}" i]`, `[id*="${word}" i]`]),
34
+ ].join(', ');
35
+ /** Elements anywhere on the page a hover may show (tooltips, menus, popovers) */
36
+ const REVEAL_GLOBAL_CANDIDATES = '[role="tooltip"], [role="menu"], [role="listbox"], [role="dialog"], [popover]';
37
+ /** Elements a hover snapshot looks at, at most */
38
+ const MAX_REVEAL_CANDIDATES = 1500;
39
+ /** Time a hover snapshot may spend before it stops looking (ms) */
40
+ const REVEAL_BUDGET_MS = 8;
41
+ /**
42
+ * Containers that hold a target and the results of its key presses (a form,
43
+ * a search box, a dialog); without one, the target's grandparent is used
44
+ */
45
+ const NEAR_CONTAINERS = 'form, [role="form"], [role="search"], dialog, [role="dialog"], [role="combobox"]';
46
+ /** Added elements reported wherever they are: popups and messages */
47
+ const POPUP_ROLES = /^(tooltip|menu|listbox|dialog|alert|alertdialog|status)$/;
26
48
  /** Messages kept per snapshot (after filtering) */
27
49
  const MAX_MESSAGES = 50;
28
50
  /** Time a message snapshot may spend before it stops looking (ms) */
29
51
  const MESSAGES_BUDGET_MS = 5;
52
+ /** Time the list of shown elements may spend before it stops looking (ms) */
53
+ const SHOWN_BUDGET_MS = 10;
54
+ /** Shown elements a read returns at most */
55
+ const MAX_SHOWN = 10;
56
+ /** Added and removed elements a watch keeps */
57
+ const MAX_TRACKED_NODES = 200;
30
58
  /** Longer texts are containers (a page, a form), not messages */
31
59
  const MAX_MESSAGE_TEXT = 300;
32
60
  /** How long a page keeps watching when no read or stop arrives (ms) */
33
61
  const MAX_WATCH_MS = 30000;
62
+ /** Bursts of DOM changes a watch keeps (their times) */
63
+ const MAX_BURSTS = 20;
64
+ /**
65
+ * Page-side test whether an element is shown: rendered, not aria-hidden,
66
+ * not `visibility: hidden` and not fully transparent.
67
+ */
68
+ const SHOWN_JS = `(el) => !el.closest('[aria-hidden="true"]') && el.getClientRects().length > 0 &&
69
+ (!el.checkVisibility || el.checkVisibility({ visibilityProperty: true, opacityProperty: true }))`;
70
+ /**
71
+ * Page-side visible text of an element, whitespace collapsed: text in
72
+ * hidden elements, scripts and styles left out, and parts for which `skip`
73
+ * says so. Stops a little over {@link MAX_MESSAGE_TEXT} characters.
74
+ */
75
+ const VISIBLE_TEXT_JS = `(el, skip) => {
76
+ let text = '';
77
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
78
+ while (text.length <= ${MAX_MESSAGE_TEXT} && walker.nextNode()) {
79
+ const parent = walker.currentNode.parentElement;
80
+ if (!parent || /^(script|style|template|noscript)$/.test(parent.localName) || skip(parent)) continue;
81
+ if (parent.checkVisibility && !parent.checkVisibility({ visibilityProperty: true, opacityProperty: true })) continue;
82
+ text += ' ' + walker.currentNode.data;
83
+ }
84
+ return text.replace(/\\s+/g, ' ').trim();
85
+ }`;
34
86
  /**
35
87
  * Page-side test whether an element is part of a message's chrome rather
36
88
  * than its text: aria-hidden parts, buttons, and elements whose class or
@@ -62,6 +114,8 @@ const MESSAGES_JS = `(ids) => {
62
114
  const deadline = performance.now() + ${MESSAGES_BUDGET_MS};
63
115
  const description = ${ELEMENT_DESCRIPTION_JS};
64
116
  const chrome = ${MESSAGE_CHROME_JS};
117
+ const shown = ${SHOWN_JS};
118
+ const visibleText = ${VISIBLE_TEXT_JS};
65
119
  const describe = (el) => {
66
120
  const text = description(el);
67
121
  const role = el.getAttribute('role');
@@ -71,28 +125,15 @@ const MESSAGES_JS = `(ids) => {
71
125
  const named = (el) => !/^(timer|marquee|progressbar)$/.test(el.getAttribute('role') || '') &&
72
126
  (el.matches('[role="alert"], [role="status"], [aria-live]:not([aria-live="off"]), output') ||
73
127
  word.test(el.getAttribute('class') || '') || word.test(el.id || ''));
74
- const shown = (el) => !el.closest('[aria-hidden="true"]') && el.getClientRects().length > 0 &&
75
- (!el.checkVisibility || el.checkVisibility({ visibilityProperty: true, opacityProperty: true }));
76
128
  const inChrome = (node, el) => {
77
129
  for (let n = node; n && n !== el; n = n.parentElement) if (chrome(n)) return true;
78
130
  return false;
79
131
  };
80
- const textOf = (el) => {
81
- let text = '';
82
- const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
83
- while (text.length <= ${MAX_MESSAGE_TEXT} && walker.nextNode()) {
84
- const parent = walker.currentNode.parentElement;
85
- if (!parent || inChrome(parent, el)) continue;
86
- if (parent.checkVisibility && !parent.checkVisibility({ visibilityProperty: true, opacityProperty: true })) continue;
87
- text += ' ' + walker.currentNode.data;
88
- }
89
- return text.replace(/\\s+/g, ' ').trim();
90
- };
91
132
  const found = [];
92
133
  for (const el of document.querySelectorAll(${JSON.stringify(MESSAGE_CANDIDATES)})) {
93
134
  if (found.length >= ${MAX_MESSAGES} || performance.now() > deadline) break;
94
135
  if (!named(el) || !shown(el)) continue;
95
- const text = textOf(el);
136
+ const text = visibleText(el, (node) => inChrome(node, el));
96
137
  if (text !== '' && text.length <= ${MAX_MESSAGE_TEXT}) found.push({ el: el, text: text });
97
138
  }
98
139
  return found
@@ -106,6 +147,102 @@ const MESSAGES_JS = `(ids) => {
106
147
  return { id: id, text: m.text, element: describe(m.el) };
107
148
  });
108
149
  }`;
150
+ /**
151
+ * Page-side list of the loading indicators shown: elements with
152
+ * `aria-busy="true"`, a progressbar role, or a class or id word from
153
+ * {@link LOADER_WORDS} (`is-loading`, `spinner`; not `lazyloading`).
154
+ */
155
+ const LOADERS_JS = `() => {
156
+ const shown = ${SHOWN_JS};
157
+ const word = /\\b(${LOADER_WORDS.join('|')})\\b/i;
158
+ const loader = (el) => el.getAttribute('aria-busy') === 'true' || el.getAttribute('role') === 'progressbar' ||
159
+ word.test(el.getAttribute('class') || '') || word.test(el.id || '');
160
+ return Array.from(document.querySelectorAll(${JSON.stringify(LOADER_CANDIDATES)})).filter((el) => loader(el) && shown(el));
161
+ }`;
162
+ /**
163
+ * Page-side snapshot a hover takes right before the mouse moves (called by
164
+ * the click script with the hovered element): the elements around it (its
165
+ * parent and everything in it) and the tooltips, menus, listboxes, dialogs
166
+ * and popovers anywhere on the page that are hidden, at most
167
+ * {@link MAX_REVEAL_CANDIDATES} looked at within {@link REVEAL_BUDGET_MS}.
168
+ * They are kept by identity in the action's watch, so its read can tell
169
+ * which of them the hover revealed, also through CSS `:hover` rules that
170
+ * change no DOM, and elements moving in the page can't pass for revealed
171
+ * ones. Does nothing without a running watch (another frame).
172
+ */
173
+ export const REVEAL_SNAPSHOT_JS = `(el) => {
174
+ const state = window.__bdgEffects;
175
+ if (!state || state.stopped) return;
176
+ const deadline = performance.now() + ${REVEAL_BUDGET_MS};
177
+ const shown = ${SHOWN_JS};
178
+ const parent = el.parentElement;
179
+ const scope = parent && !/^(body|html)$/.test(parent.localName) ? parent : el;
180
+ const hidden = [];
181
+ let looked = 0;
182
+ const room = () => looked < ${MAX_REVEAL_CANDIDATES} && performance.now() <= deadline;
183
+ const look = (candidate) => {
184
+ looked++;
185
+ if (!shown(candidate)) hidden.push(candidate);
186
+ };
187
+ for (const candidate of document.querySelectorAll(${JSON.stringify(REVEAL_GLOBAL_CANDIDATES)})) {
188
+ if (!room()) break;
189
+ look(candidate);
190
+ }
191
+ const walker = document.createTreeWalker(scope, NodeFilter.SHOW_ELEMENT);
192
+ for (let node = scope; node && room(); node = walker.nextNode()) look(node);
193
+ state.reveal = { target: el, hidden: hidden };
194
+ }`;
195
+ /**
196
+ * Page-side container whose added elements count as an action's result:
197
+ * the target's form, search box, dialog or combobox, else its grandparent
198
+ * (its parent when the grandparent is the body).
199
+ */
200
+ const NEAR_SCOPE_JS = `(target) => {
201
+ const container = target.closest(${JSON.stringify(NEAR_CONTAINERS)});
202
+ if (container) return container;
203
+ const parent = target.parentElement || target;
204
+ const grandparent = parent.parentElement;
205
+ return grandparent && !/^(body|html)$/.test(grandparent.localName) ? grandparent : parent;
206
+ }`;
207
+ /**
208
+ * Page-side list of the elements an action showed: elements added during
209
+ * the watch inside the target's container ({@link NEAR_SCOPE_JS}) or, anywhere,
210
+ * popups and messages (tooltip, menu, listbox, dialog, alert and status
211
+ * roles, `aria-live`, message-like classes), plus, after a hover, the
212
+ * elements hidden before it that are shown now ({@link REVEAL_SNAPSHOT_JS}).
213
+ * Background widgets elsewhere on the page do not count. Only shown ones
214
+ * with visible text count, the outermost of nested ones, and not those
215
+ * whose text a removed element had (a re-render). At most
216
+ * {@link MAX_SHOWN}, within {@link SHOWN_BUDGET_MS}.
217
+ */
218
+ export const SHOWN_ELEMENTS_JS = `(state) => {
219
+ const deadline = performance.now() + ${SHOWN_BUDGET_MS};
220
+ const describe = ${ELEMENT_DESCRIPTION_JS};
221
+ const shown = ${SHOWN_JS};
222
+ const visibleText = ${VISIBLE_TEXT_JS};
223
+ const word = /\\b(${MESSAGE_WORDS.join('|')})\\b/i;
224
+ const target = state.reveal ? state.reveal.target : state.keyTarget;
225
+ const scope = target && target.isConnected ? (${NEAR_SCOPE_JS})(target) : null;
226
+ const popup = (el) => ${POPUP_ROLES}.test(el.getAttribute('role') || '') || el.hasAttribute('popover') ||
227
+ (el.hasAttribute('aria-live') && el.getAttribute('aria-live') !== 'off') ||
228
+ word.test(el.getAttribute('class') || '') || word.test(el.id || '');
229
+ const near = (el) => (scope !== null && scope.contains(el)) || popup(el);
230
+ const contentOf = (node) => (node.textContent || '').replace(/\\s+/g, ' ').trim();
231
+ const removed = new Set(state.removed.map(contentOf));
232
+ const candidates = new Set(state.added.filter((node) => node.isConnected && near(node)));
233
+ if (state.reveal) for (const el of state.reveal.hidden) if (el.isConnected) candidates.add(el);
234
+ const found = [];
235
+ for (const el of candidates) {
236
+ if (performance.now() > deadline) break;
237
+ if (!shown(el) || removed.has(contentOf(el))) continue;
238
+ const text = visibleText(el, () => false);
239
+ if (text !== '') found.push({ el: el, text: text });
240
+ }
241
+ return found
242
+ .filter((m) => !found.some((other) => other !== m && other.el.contains(m.el)))
243
+ .slice(0, ${MAX_SHOWN})
244
+ .map((m) => ({ text: m.text, element: describe(m.el) }));
245
+ }`;
109
246
  /**
110
247
  * Page-side test whether a mutation is only focus/hover churn: a class
111
248
  * change on an element the action's events hit (`targets`) that only adds
@@ -119,6 +256,18 @@ export const CHURN_ONLY_JS = `(record, targets) => {
119
256
  const changed = [...before].filter((c) => !after.has(c)).concat([...after].filter((c) => !before.has(c)));
120
257
  return changed.every((c) => /focus|hover/i.test(c));
121
258
  }`;
259
+ /**
260
+ * Page-side test whether a mutation changes the page's structure or state
261
+ * rather than only animating it: elements added or removed, or an attribute
262
+ * other than `style` changed. Text-only changes (clocks, counters) and style
263
+ * changes (script-driven animations) do not count.
264
+ */
265
+ export const STRUCTURAL_CHANGE_JS = `(record) => {
266
+ if (record.type === 'attributes') return record.attributeName !== 'style';
267
+ if (record.type !== 'childList') return false;
268
+ const element = (node) => node.nodeType === 1;
269
+ return Array.from(record.addedNodes).some(element) || Array.from(record.removedNodes).some(element);
270
+ }`;
122
271
  /**
123
272
  * Page-side reason why "no effect" can't be claimed even without DOM
124
273
  * changes, or undefined: `clipboard` (a copy or cut happened), `no-event`
@@ -148,24 +297,61 @@ export const UNCERTAIN_JS = `(state, active) => {
148
297
  if (active && active !== state.focus && !plain(active)) return 'focus';
149
298
  return undefined;
150
299
  }`;
300
+ /**
301
+ * Page-side signs, at a read, that the page is still working on the
302
+ * action's result: how long ago each recent burst of DOM changes was (ms,
303
+ * newest last), and a loading indicator shown since the start (described).
304
+ */
305
+ const SETTLE_JS = `(state) => {
306
+ const now = performance.now();
307
+ const describe = ${ELEMENT_DESCRIPTION_JS};
308
+ const loader = (${LOADERS_JS})().find((el) => !state.loaders.has(el));
309
+ return {
310
+ burstAges: state.bursts.map((time) => Math.round(now - time)),
311
+ loading: loader ? describe(loader) : null
312
+ };
313
+ }`;
151
314
  /**
152
315
  * Snapshot before an action, left in `window.__bdgEffects`: the messages
153
- * shown, and a MutationObserver (on the document, its open shadow roots and
154
- * any shadow root attached while watching, which also counts as a change)
155
- * counting changes other than {@link CHURN_ONLY_JS}. Capture listeners
156
- * record which elements the action's events reached, copy/cut events, and
157
- * the scroll position at the first press (a click scrolls its target into
158
- * view first). The watch stops itself after {@link MAX_WATCH_MS}, so a
159
- * snapshot that ran late (after a navigation, with nobody reading it)
160
- * leaves nothing behind. Evaluates to the URL and the messages.
316
+ * and loading indicators shown, and a MutationObserver (on the document,
317
+ * its open shadow roots and any shadow root attached while watching, which
318
+ * also counts as a change) counting changes other than
319
+ * {@link CHURN_ONLY_JS}, keeping the elements added and removed and the
320
+ * times of {@link STRUCTURAL_CHANGE_JS} bursts. Capture listeners record
321
+ * which elements the action's events reached, the first key press's target,
322
+ * copy/cut events and the scroll position at the first press (a click
323
+ * scrolls its target into view first). The watch stops itself after
324
+ * {@link MAX_WATCH_MS}, so a snapshot that ran late (after a navigation,
325
+ * with nobody reading it) leaves nothing behind. Evaluates to the URL and
326
+ * the messages.
161
327
  */
162
328
  export const EFFECTS_START_SCRIPT = `(() => {
163
329
  if (window.__bdgEffects) window.__bdgEffects.stop();
164
330
  const churnOnly = ${CHURN_ONLY_JS};
331
+ const structural = ${STRUCTURAL_CHANGE_JS};
165
332
  const ids = { map: new WeakMap(), next: 1 };
166
- const state = { ids: ids, changes: 0, targets: new Set(), path: new Set(), focus: document.activeElement, pressScroll: null, copied: false, stopped: false };
333
+ const state = {
334
+ ids: ids, changes: 0, targets: new Set(), path: new Set(), focus: document.activeElement,
335
+ pressScroll: null, copied: false, stopped: false, added: [], removed: [], bursts: [],
336
+ keyTarget: null, loaders: new Set((${LOADERS_JS})())
337
+ };
338
+ const keep = (list, nodes) => {
339
+ for (const node of nodes) if (node.nodeType === 1 && list.length < ${MAX_TRACKED_NODES}) list.push(node);
340
+ };
167
341
  const count = (records) => {
168
- for (const record of records) if (!churnOnly(record, state.targets)) state.changes++;
342
+ let burst = false;
343
+ for (const record of records) {
344
+ if (churnOnly(record, state.targets)) continue;
345
+ state.changes++;
346
+ burst = burst || structural(record);
347
+ if (record.type === 'childList') {
348
+ keep(state.added, record.addedNodes);
349
+ keep(state.removed, record.removedNodes);
350
+ }
351
+ }
352
+ if (!burst) return;
353
+ state.bursts.push(performance.now());
354
+ if (state.bursts.length > ${MAX_BURSTS}) state.bursts.shift();
169
355
  };
170
356
  const observer = new MutationObserver(count);
171
357
  const options = { subtree: true, childList: true, characterData: true, attributes: true, attributeOldValue: true };
@@ -188,6 +374,7 @@ export const EFFECTS_START_SCRIPT = `(() => {
188
374
  if (event.type === 'copy' || event.type === 'cut') state.copied = true;
189
375
  const path = event.composedPath().filter((node) => node.nodeType === 1);
190
376
  if (path[0]) state.targets.add(path[0]);
377
+ if (path[0] && !state.keyTarget && event.type === 'keydown') state.keyTarget = path[0];
191
378
  path.forEach((node) => state.path.add(node));
192
379
  if (!state.pressScroll && pressTypes.includes(event.type)) state.pressScroll = [scrollX, scrollY];
193
380
  };
@@ -206,13 +393,16 @@ export const EFFECTS_START_SCRIPT = `(() => {
206
393
  return { href: location.href, messages: (${MESSAGES_JS})(ids) };
207
394
  })()`;
208
395
  /**
209
- * Read after an action (call with `true` to also stop watching): the URL,
210
- * the messages shown and, when the snapshot is still there (same document),
211
- * the number of changes counted (plus one when the page scrolled after the
212
- * press) and why "no effect" could not be claimed ({@link UNCERTAIN_JS}).
213
- * `fresh` means a new document (everything shown is new).
396
+ * Read after an action, called with `(stop, shown)`: `stop` also stops
397
+ * watching, `shown` lists the elements the action showed
398
+ * ({@link SHOWN_ELEMENTS_JS}). Returns the URL, the messages shown and,
399
+ * when the snapshot is still there (same document), the number of changes
400
+ * counted (plus one when the page scrolled after the press), why "no
401
+ * effect" could not be claimed ({@link UNCERTAIN_JS}) and whether the page
402
+ * is still working ({@link SETTLE_JS}). `fresh` means a new document
403
+ * (everything shown is new).
214
404
  */
215
- export const EFFECTS_READ_SCRIPT = `((stop) => {
405
+ export const EFFECTS_READ_SCRIPT = `((stop, shown) => {
216
406
  const state = window.__bdgEffects;
217
407
  if (!state) return { href: location.href, fresh: true, messages: (${MESSAGES_JS})({ map: new WeakMap(), next: 1 }) };
218
408
  if (!state.stopped) state.flush();
@@ -226,7 +416,9 @@ export const EFFECTS_READ_SCRIPT = `((stop) => {
226
416
  fresh: false,
227
417
  changes: state.changes + (scrolled ? 1 : 0),
228
418
  uncertain: (${UNCERTAIN_JS})(state, document.activeElement),
229
- messages: (${MESSAGES_JS})(state.ids)
419
+ messages: (${MESSAGES_JS})(state.ids),
420
+ settle: (${SETTLE_JS})(state),
421
+ shown: shown ? (${SHOWN_ELEMENTS_JS})(state) : undefined
230
422
  };
231
423
  })`;
232
424
  /** Stops the watch {@link EFFECTS_START_SCRIPT} left (when no read stopped it) */
@@ -26,6 +26,32 @@ export declare const WITHOUT_DECORATIONS_JS = "(el, text) => {\n if (!text || t
26
26
  * unless `full` is set. Decorations are left out ({@link WITHOUT_DECORATIONS_JS}).
27
27
  */
28
28
  export declare const ELEMENT_TEXT_JS = "(el, full) => {\n const withoutDecorations = (el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n};\n const all = el.textContent || '';\n if (el.tagName === 'OPTION') return el.label;\n if (typeof el.innerText !== 'string') return full ? all : all.slice(0, 2000);\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n if (!rendered(el)) {\n const boxless = el.ownerDocument.defaultView.getComputedStyle(el).display === 'contents';\n return boxless ? withoutDecorations(el, full ? all : all.slice(0, 2000)) : '';\n }\n if (full || all.length <= 2000) return withoutDecorations(el, el.innerText);\n const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);\n let start = '';\n while (start.length < 1000 && walker.nextNode()) {\n const parent = walker.currentNode.parentElement;\n if (!parent || rendered(parent)) start += walker.currentNode.data;\n }\n return withoutDecorations(el, start);\n}";
29
+ /** Shown instead of a secret field value (the same for every length) */
30
+ export declare const MASKED_VALUE = "\u2022\u2022\u2022\u2022";
31
+ /**
32
+ * Page-side check whether a form control holds a secret whose value must
33
+ * never leave the page: a password field (live type or type attribute), a
34
+ * field shown masked by CSS (`-webkit-text-security` other than `none`),
35
+ * one whose `autocomplete` names a password, a payment card (`cc-*`) or a
36
+ * one-time code, or a field named like a password, one-time code or card code
37
+ * (`password`, `passwd`, `pwd`, `passcode`, `otp`, `cvv`, `cvc`), which
38
+ * covers a password field switched to text by a "show password" button.
39
+ */
40
+ export declare const SENSITIVE_FIELD_JS = "(el) => {\n const autocomplete = el.getAttribute('autocomplete') || '';\n if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;\n if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;\n const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');\n if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;\n try {\n const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');\n return Boolean(security) && security !== 'none';\n } catch (e) {\n return false;\n }\n}";
41
+ /**
42
+ * Page-side live state of a form control, which its attributes do not show:
43
+ * the type and current value of an `<input>` (for checkboxes and radios
44
+ * `checked` and their `value` attribute, not the default "on"), the value of
45
+ * a `<textarea>`, the labels of a `<select>`'s selected options and the type
46
+ * of a `<button>` in a form (`submit` when it has none; outside a form only
47
+ * a type attribute is shown). Empty for other elements.
48
+ *
49
+ * Secrets never leave the page: a hidden input's value is left out, and the
50
+ * value (or selected option) of a sensitive field ({@link SENSITIVE_FIELD_JS})
51
+ * is replaced by {@link MASKED_VALUE}, whatever its length, with
52
+ * `sensitive: true`.
53
+ */
54
+ export declare const ELEMENT_STATE_JS = "(el) => {\n const isSensitive = (el) => {\n const autocomplete = el.getAttribute('autocomplete') || '';\n if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;\n if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;\n const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');\n if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;\n try {\n const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');\n return Boolean(security) && security !== 'none';\n } catch (e) {\n return false;\n }\n};\n const mask = (value) => (value ? '\u2022\u2022\u2022\u2022' : '');\n const guarded = (state) => {\n if (!isSensitive(el)) return state;\n const result = { ...state, sensitive: true };\n if ('value' in result) result.value = mask(result.value);\n if ('selected' in result) result.selected = mask(result.selected);\n return result;\n };\n switch (el.localName) {\n case 'input':\n if (el.type === 'hidden') return { type: 'hidden' };\n return guarded(\n /^(checkbox|radio)$/.test(el.type)\n ? { type: el.type, checked: el.checked, value: el.getAttribute('value') || '' }\n : { type: el.type, value: el.value }\n );\n case 'textarea':\n return guarded({ value: el.value });\n case 'select':\n return guarded({ selected: Array.from(el.selectedOptions || [], (option) => option.label).join(', ') });\n case 'button':\n return el.form ? { type: el.type } : {};\n default:\n return {};\n }\n}";
29
55
  /**
30
56
  * Page-side short description of an element: tag, id and up to two classes,
31
57
  * e.g. `button#save.primary.large`.
@@ -66,6 +66,71 @@ export const ELEMENT_TEXT_JS = `(el, full) => {
66
66
  }
67
67
  return withoutDecorations(el, start);
68
68
  }`;
69
+ /** Shown instead of a secret field value (the same for every length) */
70
+ export const MASKED_VALUE = '••••';
71
+ /**
72
+ * Page-side check whether a form control holds a secret whose value must
73
+ * never leave the page: a password field (live type or type attribute), a
74
+ * field shown masked by CSS (`-webkit-text-security` other than `none`),
75
+ * one whose `autocomplete` names a password, a payment card (`cc-*`) or a
76
+ * one-time code, or a field named like a password, one-time code or card code
77
+ * (`password`, `passwd`, `pwd`, `passcode`, `otp`, `cvv`, `cvc`), which
78
+ * covers a password field switched to text by a "show password" button.
79
+ */
80
+ export const SENSITIVE_FIELD_JS = `(el) => {
81
+ const autocomplete = el.getAttribute('autocomplete') || '';
82
+ if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;
83
+ if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;
84
+ const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');
85
+ if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;
86
+ try {
87
+ const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');
88
+ return Boolean(security) && security !== 'none';
89
+ } catch (e) {
90
+ return false;
91
+ }
92
+ }`;
93
+ /**
94
+ * Page-side live state of a form control, which its attributes do not show:
95
+ * the type and current value of an `<input>` (for checkboxes and radios
96
+ * `checked` and their `value` attribute, not the default "on"), the value of
97
+ * a `<textarea>`, the labels of a `<select>`'s selected options and the type
98
+ * of a `<button>` in a form (`submit` when it has none; outside a form only
99
+ * a type attribute is shown). Empty for other elements.
100
+ *
101
+ * Secrets never leave the page: a hidden input's value is left out, and the
102
+ * value (or selected option) of a sensitive field ({@link SENSITIVE_FIELD_JS})
103
+ * is replaced by {@link MASKED_VALUE}, whatever its length, with
104
+ * `sensitive: true`.
105
+ */
106
+ export const ELEMENT_STATE_JS = `(el) => {
107
+ const isSensitive = ${SENSITIVE_FIELD_JS};
108
+ const mask = (value) => (value ? '${MASKED_VALUE}' : '');
109
+ const guarded = (state) => {
110
+ if (!isSensitive(el)) return state;
111
+ const result = { ...state, sensitive: true };
112
+ if ('value' in result) result.value = mask(result.value);
113
+ if ('selected' in result) result.selected = mask(result.selected);
114
+ return result;
115
+ };
116
+ switch (el.localName) {
117
+ case 'input':
118
+ if (el.type === 'hidden') return { type: 'hidden' };
119
+ return guarded(
120
+ /^(checkbox|radio)$/.test(el.type)
121
+ ? { type: el.type, checked: el.checked, value: el.getAttribute('value') || '' }
122
+ : { type: el.type, value: el.value }
123
+ );
124
+ case 'textarea':
125
+ return guarded({ value: el.value });
126
+ case 'select':
127
+ return guarded({ selected: Array.from(el.selectedOptions || [], (option) => option.label).join(', ') });
128
+ case 'button':
129
+ return el.form ? { type: el.type } : {};
130
+ default:
131
+ return {};
132
+ }
133
+ }`;
69
134
  /**
70
135
  * Page-side short description of an element: tag, id and up to two classes,
71
136
  * e.g. `button#save.primary.large`.
@@ -249,7 +249,13 @@ async function pageDetails(cdp, chain, found, objectGroup, types) {
249
249
  objectId: chain[0]?.objectId,
250
250
  functionDeclaration: ELEMENT_INFO_JS,
251
251
  arguments: [
252
- { value: listeners.map(({ position, listener }) => ({ position, type: listener.type })) },
252
+ {
253
+ value: listeners.map(({ position, listener }) => ({
254
+ position,
255
+ type: listener.type,
256
+ capture: listener.useCapture,
257
+ })),
258
+ },
253
259
  { value: types ?? null },
254
260
  ...chain.map((entry) => ({ objectId: entry.objectId })),
255
261
  ...handlers.map(objectArgument),
@@ -346,13 +352,13 @@ const UNKNOWN_SOURCE = { scriptId: '0', lineNumber: 0, columnNumber: 0 };
346
352
  * Combine the page's report with the framework handlers' sources.
347
353
  *
348
354
  * @param info - Page report
349
- * @param sources - Source and location of each jQuery handler, then each
350
- * React prop handler, in report order
355
+ * @param sources - Source and location of each jQuery or Preact handler,
356
+ * then each React prop handler, in report order
351
357
  * @returns Details per listener and the React prop handlers
352
358
  */
353
359
  function toPageDetails(info, sources) {
354
360
  let next = 0;
355
- const details = info.listeners.map(({ name, identity, targetName, jquery }) => ({
361
+ const details = info.listeners.map(({ name, identity, targetName, jquery, preact }) => ({
356
362
  name: name ?? undefined,
357
363
  identity: identity ?? undefined,
358
364
  targetName: targetName ?? undefined,
@@ -362,6 +368,10 @@ function toPageDetails(info, sources) {
362
368
  name: handler.name,
363
369
  ...(sources[next++] ?? UNKNOWN_SOURCE),
364
370
  })),
371
+ preact: preact && {
372
+ name: preact.name,
373
+ ...(sources[next++] ?? UNKNOWN_SOURCE),
374
+ },
365
375
  }));
366
376
  const react = info.react.map((handler) => ({
367
377
  ...handler,
@@ -20,10 +20,8 @@ export type PointerAction = 'click' | 'double' | 'right' | 'hover';
20
20
  */
21
21
  export declare function mouseEvents(action: PointerAction, x: number, y: number): Array<Record<string, unknown>>;
22
22
  /**
23
- * Whether the mouse press just dispatched reached the target element.
24
- *
25
- * Any doubt (no probe, evaluation failure) counts as reached, so only a
26
- * press that the page provably never saw is reported.
23
+ * Whether the mouse press just dispatched reached the target element
24
+ * (see {@link readPressProbe}).
27
25
  *
28
26
  * @param cdp - CDP connection
29
27
  * @returns False only when the target received no pointerdown/mousedown
@@ -35,5 +33,6 @@ export declare function pressReachedTarget(cdp: CDPConnection): Promise<boolean>
35
33
  export declare function clickElement(cdp: CDPConnection, selector: string, options?: {
36
34
  index?: number;
37
35
  action?: PointerAction;
36
+ strict?: boolean;
38
37
  }): Promise<ClickResult>;
39
38
  //# sourceMappingURL=fill.d.ts.map