explorbot 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -2
- package/bin/explorbot-cli.ts +21 -21
- package/dist/bin/explorbot-cli.js +3 -3
- package/dist/package.json +4 -3
- package/dist/rules/researcher/container-rules.md +2 -0
- package/dist/src/action-result.js +2 -1
- package/dist/src/action.js +5 -10
- package/dist/src/ai/captain.js +0 -2
- package/dist/src/ai/driller.js +1108 -0
- package/dist/src/ai/historian/codeceptjs.js +2 -2
- package/dist/src/ai/historian/experience.js +1 -0
- package/dist/src/ai/historian/playwright.js +4 -4
- package/dist/src/ai/historian/screencast.js +121 -0
- package/dist/src/ai/historian.js +5 -3
- package/dist/src/ai/pilot.js +31 -22
- package/dist/src/ai/rules.js +3 -5
- package/dist/src/ai/session-analyst.js +117 -0
- package/dist/src/ai/tester.js +13 -2
- package/dist/src/commands/base-command.js +6 -6
- package/dist/src/commands/drill-command.js +3 -2
- package/dist/src/commands/exit-command.js +1 -0
- package/dist/src/commands/explore-command.js +20 -3
- package/dist/src/components/AddRule.js +1 -1
- package/dist/src/explorbot.js +52 -9
- package/dist/src/explorer.js +11 -9
- package/dist/src/reporter.js +68 -4
- package/dist/src/state-manager.js +4 -3
- package/dist/src/stats.js +5 -0
- package/dist/src/utils/aria.js +354 -529
- package/dist/src/utils/hooks-runner.js +2 -8
- package/dist/src/utils/html.js +371 -0
- package/dist/src/utils/strings.js +15 -0
- package/dist/src/utils/unique-names.js +12 -1
- package/dist/src/utils/url-matcher.js +6 -1
- package/dist/src/utils/web-element.js +27 -24
- package/dist/src/utils/xpath.js +1 -1
- package/package.json +4 -3
- package/rules/researcher/container-rules.md +2 -0
- package/src/action-result.ts +2 -1
- package/src/action.ts +5 -12
- package/src/ai/captain.ts +0 -2
- package/src/ai/driller.ts +1194 -0
- package/src/ai/historian/codeceptjs.ts +2 -2
- package/src/ai/historian/experience.ts +3 -2
- package/src/ai/historian/playwright.ts +5 -5
- package/src/ai/historian/screencast.ts +133 -0
- package/src/ai/historian.ts +7 -5
- package/src/ai/pilot.ts +31 -21
- package/src/ai/rules.ts +3 -5
- package/src/ai/session-analyst.ts +133 -0
- package/src/ai/tester.ts +15 -2
- package/src/commands/base-command.ts +6 -6
- package/src/commands/drill-command.ts +3 -2
- package/src/commands/exit-command.ts +1 -0
- package/src/commands/explore-command.ts +22 -3
- package/src/components/AddRule.tsx +1 -1
- package/src/config.ts +10 -0
- package/src/explorbot.ts +59 -11
- package/src/explorer.ts +11 -9
- package/src/reporter.ts +68 -4
- package/src/state-manager.ts +4 -3
- package/src/stats.ts +7 -0
- package/src/utils/aria.ts +367 -537
- package/src/utils/hooks-runner.ts +2 -6
- package/src/utils/html.ts +381 -0
- package/src/utils/strings.ts +17 -0
- package/src/utils/unique-names.ts +13 -0
- package/src/utils/url-matcher.ts +5 -1
- package/src/utils/web-element.ts +31 -28
- package/src/utils/xpath.ts +1 -1
- package/dist/src/ai/bosun.js +0 -456
- package/src/ai/bosun.ts +0 -571
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createDebug } from "./logger.js";
|
|
2
|
+
import { extractStatePath } from "./url-matcher.js";
|
|
2
3
|
import { matchesUrl } from "./url-matcher.js";
|
|
3
4
|
const debugLog = createDebug('explorbot:hooks');
|
|
4
5
|
export class HooksRunner {
|
|
@@ -64,13 +65,6 @@ export class HooksRunner {
|
|
|
64
65
|
return 'type' in config && 'hook' in config;
|
|
65
66
|
}
|
|
66
67
|
extractPath(url) {
|
|
67
|
-
|
|
68
|
-
return url;
|
|
69
|
-
try {
|
|
70
|
-
return new URL(url).pathname;
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
return url;
|
|
74
|
-
}
|
|
68
|
+
return extractStatePath(url);
|
|
75
69
|
}
|
|
76
70
|
}
|
package/dist/src/utils/html.js
CHANGED
|
@@ -65,6 +65,377 @@ const INTERACTIVE_TAGS = new Set(['a', 'button', 'details', 'input', 'option', '
|
|
|
65
65
|
const INTERACTIVE_ROLES = new Set(['button', 'checkbox', 'combobox', 'link', 'listbox', 'radio', 'search', 'switch', 'tab', 'textbox']);
|
|
66
66
|
const INTERACTIVE_EVENT_ATTRIBUTES = new Set(['onclick', 'onchange', 'onblur', 'onfocus', 'onmousedown', 'onmouseup']);
|
|
67
67
|
const HIDDEN_CLASSES = new Set(['hidden', 'invisible', 'd-none', 'hide', 'dn', 'u-hidden', 'is-hidden', 'visually-hidden', 'sr-only', 'screen-reader-only', 'visuallyhidden', 'opacity-0']);
|
|
68
|
+
export const EXPLORBOT_ATTRS = {
|
|
69
|
+
area: 'data-explorbot-area',
|
|
70
|
+
context: 'data-explorbot-context',
|
|
71
|
+
eidx: 'data-explorbot-eidx',
|
|
72
|
+
variant: 'data-explorbot-variant',
|
|
73
|
+
};
|
|
74
|
+
export const HTML_SELECTORS = {
|
|
75
|
+
headingLabel: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"]',
|
|
76
|
+
interactiveContent: 'button, a[href], input, select, textarea, [role="button"], [role="link"], [role="option"], [role="menuitem"], [role="switch"], [role="checkbox"], [role="radio"], [aria-label], [tabindex]',
|
|
77
|
+
interactiveControl: 'button, a[href], input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"], [role="menuitem"]',
|
|
78
|
+
labelLike: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"], [class*="title"], [class*="label"], [class*="header"], [class*="name"]',
|
|
79
|
+
semanticContextContainer: 'section, article, form, fieldset, li, tr, td, th, [role="group"], [role="tabpanel"], [role="region"], [class*="card"], [class*="panel"], [class*="item"], [class*="usage"], [class*="group"]',
|
|
80
|
+
semanticOverlays: ['[role="dialog"]', '[role="listbox"]', '[role="menu"]', '[role="tooltip"]:not([style*="display: none"]):not([style*="visibility: hidden"])'],
|
|
81
|
+
};
|
|
82
|
+
export const HTML_VISIBILITY_LIMITS = {
|
|
83
|
+
maxViewportOverlayRatio: 0.95,
|
|
84
|
+
minOpacity: 0.1,
|
|
85
|
+
minOverlayHeight: 40,
|
|
86
|
+
minOverlayWidth: 80,
|
|
87
|
+
};
|
|
88
|
+
export const HTML_EXTRACTION_LIMITS = {
|
|
89
|
+
componentScopeHtmlLength: 8000,
|
|
90
|
+
maxOverlayCount: 3,
|
|
91
|
+
maxScopeInteractiveCount: 16,
|
|
92
|
+
overlayHtmlLength: 6000,
|
|
93
|
+
};
|
|
94
|
+
export const CODE_EDITOR_MARKERS = ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'];
|
|
95
|
+
export const HTML_INTERACTIVE_ROLES = new Set(['button', 'link', 'checkbox', 'radio', 'switch', 'tab', 'combobox', 'iframe', 'code-editor', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox', 'treeitem']);
|
|
96
|
+
export const HTML_FORM_CONTROL_ROLES = new Set(['checkbox', 'radio', 'switch', 'combobox', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox']);
|
|
97
|
+
export const HTML_COMPOSITE_TARGET_ROLES = new Set(['tab', 'option', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'treeitem']);
|
|
98
|
+
export const HTML_COMPOSITE_AREA_HINTS = new Set(['role:tab', 'role:option', 'role:menuitem', 'role:menuitemcheckbox', 'role:menuitemradio', 'role:treeitem']);
|
|
99
|
+
export const HTML_FORM_CONTROL_TAGS = new Set(['input', 'select', 'textarea']);
|
|
100
|
+
export function inferHtmlRole(data) {
|
|
101
|
+
if (data.tag === 'iframe' && data.variantHints?.includes('code-editor'))
|
|
102
|
+
return 'code-editor';
|
|
103
|
+
if (data.role)
|
|
104
|
+
return data.role.toLowerCase();
|
|
105
|
+
const explicitRole = data.attrs.role;
|
|
106
|
+
if (explicitRole)
|
|
107
|
+
return explicitRole.toLowerCase();
|
|
108
|
+
if (data.tag === 'a' && data.attrs.href)
|
|
109
|
+
return 'link';
|
|
110
|
+
if (data.tag === 'button')
|
|
111
|
+
return 'button';
|
|
112
|
+
if (data.tag === 'iframe')
|
|
113
|
+
return 'iframe';
|
|
114
|
+
if (data.tag === 'select')
|
|
115
|
+
return 'combobox';
|
|
116
|
+
if (data.tag === 'textarea')
|
|
117
|
+
return 'textbox';
|
|
118
|
+
if (data.tag === 'input') {
|
|
119
|
+
const type = (data.attrs.type || 'text').toLowerCase();
|
|
120
|
+
if (type === 'checkbox')
|
|
121
|
+
return 'checkbox';
|
|
122
|
+
if (type === 'radio')
|
|
123
|
+
return 'radio';
|
|
124
|
+
return 'textbox';
|
|
125
|
+
}
|
|
126
|
+
return data.tag;
|
|
127
|
+
}
|
|
128
|
+
export const ELEMENT_EXTRACTION_CONFIG = {
|
|
129
|
+
attrs: EXPLORBOT_ATTRS,
|
|
130
|
+
codeEditorMarkers: CODE_EDITOR_MARKERS,
|
|
131
|
+
maxAreaDepth: 5,
|
|
132
|
+
maxContextLength: 120,
|
|
133
|
+
maxOuterHTMLLength: 2000,
|
|
134
|
+
maxTextLength: 80,
|
|
135
|
+
minOpacity: HTML_VISIBILITY_LIMITS.minOpacity,
|
|
136
|
+
selectors: {
|
|
137
|
+
headingLabel: HTML_SELECTORS.headingLabel,
|
|
138
|
+
labelLike: HTML_SELECTORS.labelLike,
|
|
139
|
+
semanticContextContainer: HTML_SELECTORS.semanticContextContainer,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
export function extractElementData(el, config) {
|
|
143
|
+
const cfg = config ||
|
|
144
|
+
{
|
|
145
|
+
attrs: {
|
|
146
|
+
area: 'data-explorbot-area',
|
|
147
|
+
context: 'data-explorbot-context',
|
|
148
|
+
eidx: 'data-explorbot-eidx',
|
|
149
|
+
variant: 'data-explorbot-variant',
|
|
150
|
+
},
|
|
151
|
+
codeEditorMarkers: ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'],
|
|
152
|
+
maxAreaDepth: 5,
|
|
153
|
+
maxContextLength: 120,
|
|
154
|
+
maxOuterHTMLLength: 2000,
|
|
155
|
+
maxTextLength: 80,
|
|
156
|
+
minOpacity: 0.1,
|
|
157
|
+
selectors: {
|
|
158
|
+
headingLabel: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"]',
|
|
159
|
+
labelLike: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"], [class*="title"], [class*="label"], [class*="header"], [class*="name"]',
|
|
160
|
+
semanticContextContainer: 'section, article, form, fieldset, li, tr, td, th, [role="group"], [role="tabpanel"], [role="region"], [class*="card"], [class*="panel"], [class*="item"], [class*="usage"], [class*="group"]',
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
function normalizeText(value) {
|
|
164
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
165
|
+
}
|
|
166
|
+
function readText(node) {
|
|
167
|
+
if (!node)
|
|
168
|
+
return '';
|
|
169
|
+
return normalizeText(node.textContent || '').slice(0, cfg.maxContextLength);
|
|
170
|
+
}
|
|
171
|
+
function getLabelLikeText(node) {
|
|
172
|
+
if (!node)
|
|
173
|
+
return '';
|
|
174
|
+
const direct = readText(node);
|
|
175
|
+
if (direct)
|
|
176
|
+
return direct;
|
|
177
|
+
const labelLike = node.querySelector(cfg.selectors.labelLike);
|
|
178
|
+
return readText(labelLike);
|
|
179
|
+
}
|
|
180
|
+
function collectVariantHints(target) {
|
|
181
|
+
const tokens = new Set();
|
|
182
|
+
const className = target.getAttribute('class') || '';
|
|
183
|
+
const tagName = target.tagName.toLowerCase();
|
|
184
|
+
for (const cls of className.split(/\s+/).filter(Boolean)) {
|
|
185
|
+
const lower = cls.toLowerCase();
|
|
186
|
+
if (/^(xs|sm|md|lg|xl|xxl)$/.test(lower))
|
|
187
|
+
tokens.add(lower);
|
|
188
|
+
if (/^(mini|small|medium|large|xlarge|xl|compact|dense)$/.test(lower))
|
|
189
|
+
tokens.add(lower);
|
|
190
|
+
if (/(^|[-_])(xs|sm|md|lg|xl|xxl|mini|small|medium|large|compact|dense)([-_]|$)/.test(lower))
|
|
191
|
+
tokens.add(lower);
|
|
192
|
+
if (/(selected|disabled|primary|secondary|tertiary|danger|success|warning|outline|ghost|icon|dropdown)/.test(lower))
|
|
193
|
+
tokens.add(lower);
|
|
194
|
+
}
|
|
195
|
+
const type = (target.getAttribute('type') || '').toLowerCase();
|
|
196
|
+
if (type)
|
|
197
|
+
tokens.add(type);
|
|
198
|
+
if (target.hasAttribute('disabled') || target.getAttribute('aria-disabled') === 'true')
|
|
199
|
+
tokens.add('disabled');
|
|
200
|
+
if (className.toLowerCase().includes('selected') || target.getAttribute('aria-pressed') === 'true')
|
|
201
|
+
tokens.add('selected');
|
|
202
|
+
if (tagName === 'iframe')
|
|
203
|
+
tokens.add('iframe');
|
|
204
|
+
if (tagName === 'iframe' && isEmbeddedCodeEditorFrame(target))
|
|
205
|
+
tokens.add('code-editor');
|
|
206
|
+
const svgCount = target.querySelectorAll('svg').length;
|
|
207
|
+
if (svgCount > 0)
|
|
208
|
+
tokens.add('has-icon');
|
|
209
|
+
if (svgCount > 1)
|
|
210
|
+
tokens.add('double-icon');
|
|
211
|
+
const normalizedText = normalizeText(target.textContent || '');
|
|
212
|
+
if (!normalizedText && svgCount > 0)
|
|
213
|
+
tokens.add('icon-only');
|
|
214
|
+
if (normalizedText && svgCount > 0) {
|
|
215
|
+
const first = target.firstElementChild?.tagName.toLowerCase();
|
|
216
|
+
const last = target.lastElementChild?.tagName.toLowerCase();
|
|
217
|
+
if (first === 'svg')
|
|
218
|
+
tokens.add('leading-icon');
|
|
219
|
+
if (last === 'svg')
|
|
220
|
+
tokens.add('trailing-icon');
|
|
221
|
+
}
|
|
222
|
+
if (tagName === 'a' && target.getAttribute('href'))
|
|
223
|
+
tokens.add('navigates');
|
|
224
|
+
return Array.from(tokens).slice(0, 8);
|
|
225
|
+
}
|
|
226
|
+
function isEmbeddedCodeEditorFrame(target) {
|
|
227
|
+
const src = (target.getAttribute('src') || '').toLowerCase();
|
|
228
|
+
const markerSelector = cfg.codeEditorMarkers.map((marker) => `[class*="${marker}"]`).join(', ');
|
|
229
|
+
const ancestorClasses = (target.closest(markerSelector)?.getAttribute('class') || '').toLowerCase();
|
|
230
|
+
return cfg.codeEditorMarkers.some((marker) => src.includes(marker) || ancestorClasses.includes(marker));
|
|
231
|
+
}
|
|
232
|
+
function findContextLabel(target) {
|
|
233
|
+
const labelledby = target.getAttribute('aria-labelledby');
|
|
234
|
+
const candidates = [];
|
|
235
|
+
if (labelledby) {
|
|
236
|
+
for (const id of labelledby.split(/\s+/).filter(Boolean)) {
|
|
237
|
+
const ref = document.getElementById(id);
|
|
238
|
+
const text = readText(ref);
|
|
239
|
+
if (text)
|
|
240
|
+
candidates.push(text);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const semanticContainer = target.closest(cfg.selectors.semanticContextContainer);
|
|
244
|
+
if (semanticContainer) {
|
|
245
|
+
const ownHeading = semanticContainer.querySelector(cfg.selectors.headingLabel);
|
|
246
|
+
const ownHeadingText = readText(ownHeading);
|
|
247
|
+
if (ownHeadingText)
|
|
248
|
+
candidates.push(ownHeadingText);
|
|
249
|
+
let previous = semanticContainer.previousElementSibling;
|
|
250
|
+
let hops = 0;
|
|
251
|
+
while (previous && hops < 3) {
|
|
252
|
+
const previousText = getLabelLikeText(previous);
|
|
253
|
+
if (previousText) {
|
|
254
|
+
candidates.push(previousText);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
previous = previous.previousElementSibling;
|
|
258
|
+
hops++;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
let parent = target.parentElement;
|
|
262
|
+
let depth = 0;
|
|
263
|
+
while (parent && depth < 4) {
|
|
264
|
+
let sibling = parent.previousElementSibling;
|
|
265
|
+
let hops = 0;
|
|
266
|
+
while (sibling && hops < 2) {
|
|
267
|
+
const siblingText = getLabelLikeText(sibling);
|
|
268
|
+
if (siblingText) {
|
|
269
|
+
candidates.push(siblingText);
|
|
270
|
+
sibling = null;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
sibling = sibling.previousElementSibling;
|
|
274
|
+
hops++;
|
|
275
|
+
}
|
|
276
|
+
parent = parent.parentElement;
|
|
277
|
+
depth++;
|
|
278
|
+
}
|
|
279
|
+
const ownText = normalizeText(target.textContent || '');
|
|
280
|
+
for (const candidate of candidates) {
|
|
281
|
+
if (!candidate)
|
|
282
|
+
continue;
|
|
283
|
+
if (candidate === ownText)
|
|
284
|
+
continue;
|
|
285
|
+
if (candidate.toLowerCase().includes('title should not be empty'))
|
|
286
|
+
continue;
|
|
287
|
+
return candidate.slice(0, cfg.maxContextLength);
|
|
288
|
+
}
|
|
289
|
+
return '';
|
|
290
|
+
}
|
|
291
|
+
const rect = el.getBoundingClientRect();
|
|
292
|
+
if (rect.width === 0 && rect.height === 0)
|
|
293
|
+
return null;
|
|
294
|
+
const style = window.getComputedStyle(el);
|
|
295
|
+
if (style.display === 'none' || style.visibility === 'hidden')
|
|
296
|
+
return null;
|
|
297
|
+
if (Number.parseFloat(style.opacity || '1') < cfg.minOpacity)
|
|
298
|
+
return null;
|
|
299
|
+
if (el.getAttribute('aria-hidden') === 'true' || el.hasAttribute('hidden'))
|
|
300
|
+
return null;
|
|
301
|
+
if (el.offsetParent === null && style.position !== 'fixed')
|
|
302
|
+
return null;
|
|
303
|
+
const allAttrs = {};
|
|
304
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
305
|
+
const attr = el.attributes[i];
|
|
306
|
+
allAttrs[attr.name] = attr.value;
|
|
307
|
+
}
|
|
308
|
+
const areaHints = [];
|
|
309
|
+
let current = el;
|
|
310
|
+
let depth = 0;
|
|
311
|
+
while (current && depth < cfg.maxAreaDepth) {
|
|
312
|
+
const tag = current.tagName.toLowerCase();
|
|
313
|
+
areaHints.push(tag);
|
|
314
|
+
const role = current.getAttribute('role');
|
|
315
|
+
if (role)
|
|
316
|
+
areaHints.push(`role:${role.toLowerCase()}`);
|
|
317
|
+
const id = current.getAttribute('id');
|
|
318
|
+
if (id)
|
|
319
|
+
areaHints.push(`id:${id.toLowerCase()}`);
|
|
320
|
+
const className = current.getAttribute('class');
|
|
321
|
+
if (className) {
|
|
322
|
+
for (const cls of className.split(/\s+/).filter(Boolean)) {
|
|
323
|
+
areaHints.push(`class:${cls.toLowerCase()}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
current = current.parentElement;
|
|
327
|
+
depth++;
|
|
328
|
+
}
|
|
329
|
+
allAttrs[cfg.attrs.area] = areaHints.join('|');
|
|
330
|
+
allAttrs[cfg.attrs.context] = findContextLabel(el);
|
|
331
|
+
allAttrs[cfg.attrs.variant] = collectVariantHints(el).join('|');
|
|
332
|
+
return {
|
|
333
|
+
tag: el.tagName.toLowerCase(),
|
|
334
|
+
text: normalizeText(el.textContent || '').slice(0, cfg.maxTextLength),
|
|
335
|
+
allAttrs,
|
|
336
|
+
outerHTML: el.outerHTML.slice(0, cfg.maxOuterHTMLLength),
|
|
337
|
+
x: Math.round(rect.x + rect.width / 2),
|
|
338
|
+
y: Math.round(rect.y + rect.height / 2),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
export function getElementDataExtractorSource() {
|
|
342
|
+
return extractElementData.toString();
|
|
343
|
+
}
|
|
344
|
+
export function extractVisibleOverlayHtml(config) {
|
|
345
|
+
function isVisible(element) {
|
|
346
|
+
const html = element;
|
|
347
|
+
const style = window.getComputedStyle(html);
|
|
348
|
+
const rect = html.getBoundingClientRect();
|
|
349
|
+
if (rect.width === 0 && rect.height === 0)
|
|
350
|
+
return false;
|
|
351
|
+
if (style.display === 'none' || style.visibility === 'hidden')
|
|
352
|
+
return false;
|
|
353
|
+
if (Number.parseFloat(style.opacity || '1') < config.visibilityLimits.minOpacity)
|
|
354
|
+
return false;
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
function getUsefulContent(element) {
|
|
358
|
+
const text = (element.textContent || '').replace(/\s+/g, ' ').trim();
|
|
359
|
+
const interactiveCount = element.querySelectorAll(config.interactiveContentSelector).length;
|
|
360
|
+
return { interactiveCount, text };
|
|
361
|
+
}
|
|
362
|
+
function isLikelyFloatingOverlay(element) {
|
|
363
|
+
const html = element;
|
|
364
|
+
const style = window.getComputedStyle(html);
|
|
365
|
+
const rect = html.getBoundingClientRect();
|
|
366
|
+
const zIndex = Number.parseInt(style.zIndex || '0', 10);
|
|
367
|
+
const isFloating = style.position === 'fixed' || style.position === 'absolute' || style.position === 'sticky' || zIndex > 0;
|
|
368
|
+
if (!isFloating)
|
|
369
|
+
return false;
|
|
370
|
+
if (rect.width < config.visibilityLimits.minOverlayWidth || rect.height < config.visibilityLimits.minOverlayHeight)
|
|
371
|
+
return false;
|
|
372
|
+
if (rect.bottom < 0 || rect.right < 0 || rect.top > window.innerHeight || rect.left > window.innerWidth)
|
|
373
|
+
return false;
|
|
374
|
+
if (rect.width >= window.innerWidth * config.visibilityLimits.maxViewportOverlayRatio && rect.height >= window.innerHeight * config.visibilityLimits.maxViewportOverlayRatio)
|
|
375
|
+
return false;
|
|
376
|
+
const { interactiveCount, text } = getUsefulContent(element);
|
|
377
|
+
return interactiveCount > 0 || text.length > 0;
|
|
378
|
+
}
|
|
379
|
+
const overlays = [];
|
|
380
|
+
const seen = new Set();
|
|
381
|
+
for (const selector of config.overlaySelectors) {
|
|
382
|
+
for (const element of Array.from(document.querySelectorAll(selector))) {
|
|
383
|
+
if (seen.has(element))
|
|
384
|
+
continue;
|
|
385
|
+
seen.add(element);
|
|
386
|
+
if (!isVisible(element))
|
|
387
|
+
continue;
|
|
388
|
+
const { interactiveCount, text } = getUsefulContent(element);
|
|
389
|
+
if (interactiveCount === 0 && text.length === 0)
|
|
390
|
+
continue;
|
|
391
|
+
overlays.push(element.outerHTML.slice(0, config.limits.overlayHtmlLength));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (overlays.length === 0) {
|
|
395
|
+
const floatingCandidates = Array.from(document.body.querySelectorAll('*'))
|
|
396
|
+
.filter((element) => !seen.has(element) && isVisible(element) && isLikelyFloatingOverlay(element))
|
|
397
|
+
.sort((left, right) => {
|
|
398
|
+
const leftStyle = window.getComputedStyle(left);
|
|
399
|
+
const rightStyle = window.getComputedStyle(right);
|
|
400
|
+
const leftZ = Number.parseInt(leftStyle.zIndex || '0', 10) || 0;
|
|
401
|
+
const rightZ = Number.parseInt(rightStyle.zIndex || '0', 10) || 0;
|
|
402
|
+
if (leftZ !== rightZ)
|
|
403
|
+
return rightZ - leftZ;
|
|
404
|
+
const leftRect = left.getBoundingClientRect();
|
|
405
|
+
const rightRect = right.getBoundingClientRect();
|
|
406
|
+
return leftRect.width * leftRect.height - rightRect.width * rightRect.height;
|
|
407
|
+
});
|
|
408
|
+
for (const element of floatingCandidates.slice(0, config.limits.maxOverlayCount)) {
|
|
409
|
+
overlays.push(element.outerHTML.slice(0, config.limits.overlayHtmlLength));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return overlays.slice(0, config.limits.maxOverlayCount).join('\n\n--- overlay ---\n\n');
|
|
413
|
+
}
|
|
414
|
+
export function extractComponentScopeHtml(eidx, config) {
|
|
415
|
+
const element = document.querySelector(`[${config.eidxAttr}="${eidx}"]`);
|
|
416
|
+
if (!element)
|
|
417
|
+
return '';
|
|
418
|
+
function countInteractive(node) {
|
|
419
|
+
return node.querySelectorAll(config.interactiveControlSelector).length;
|
|
420
|
+
}
|
|
421
|
+
let current = element.parentElement;
|
|
422
|
+
while (current) {
|
|
423
|
+
const count = countInteractive(current);
|
|
424
|
+
if (count > 0 && count <= config.limits.maxScopeInteractiveCount) {
|
|
425
|
+
return current.outerHTML.slice(0, config.limits.componentScopeHtmlLength);
|
|
426
|
+
}
|
|
427
|
+
current = current.parentElement;
|
|
428
|
+
}
|
|
429
|
+
if (element instanceof HTMLElement)
|
|
430
|
+
return element.outerHTML.slice(0, config.limits.componentScopeHtmlLength);
|
|
431
|
+
return '';
|
|
432
|
+
}
|
|
433
|
+
export function getVisibleOverlayHtmlExtractorSource() {
|
|
434
|
+
return extractVisibleOverlayHtml.toString();
|
|
435
|
+
}
|
|
436
|
+
export function getComponentScopeHtmlExtractorSource() {
|
|
437
|
+
return extractComponentScopeHtml.toString();
|
|
438
|
+
}
|
|
68
439
|
export const TRASH_HTML_CLASSES = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/;
|
|
69
440
|
export const TAILWIND_CLASS_PATTERNS = [
|
|
70
441
|
/^m[trblxy]?-/i,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
export function truncateJson(input) {
|
|
2
3
|
if (!input)
|
|
3
4
|
return '';
|
|
@@ -11,3 +12,17 @@ export function sanitizeFilename(name) {
|
|
|
11
12
|
.replace(/^_+|_+$/g, '')
|
|
12
13
|
.slice(0, 50);
|
|
13
14
|
}
|
|
15
|
+
export function safeFilename(name, ext = '', maxBytes = 240) {
|
|
16
|
+
const sanitized = name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
|
|
17
|
+
const extBytes = Buffer.byteLength(ext, 'utf8');
|
|
18
|
+
const budget = maxBytes - extBytes;
|
|
19
|
+
if (Buffer.byteLength(sanitized, 'utf8') <= budget)
|
|
20
|
+
return sanitized + ext;
|
|
21
|
+
const hash = createHash('sha1').update(name).digest('hex').slice(0, 8);
|
|
22
|
+
const suffix = `_${hash}`;
|
|
23
|
+
let truncated = sanitized;
|
|
24
|
+
while (Buffer.byteLength(truncated + suffix, 'utf8') > budget && truncated.length > 0) {
|
|
25
|
+
truncated = truncated.slice(0, -1);
|
|
26
|
+
}
|
|
27
|
+
return truncated + suffix + ext;
|
|
28
|
+
}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import { adjectives, colors, uniqueNamesGenerator } from 'unique-names-generator';
|
|
1
|
+
import { adjectives, animals, colors, uniqueNamesGenerator } from 'unique-names-generator';
|
|
2
2
|
const nameConfig = {
|
|
3
3
|
dictionaries: [adjectives, adjectives, colors],
|
|
4
4
|
separator: '',
|
|
5
5
|
length: 3,
|
|
6
6
|
style: 'capital',
|
|
7
7
|
};
|
|
8
|
+
const explorationConfig = {
|
|
9
|
+
dictionaries: [adjectives, animals],
|
|
10
|
+
separator: '',
|
|
11
|
+
length: 2,
|
|
12
|
+
style: 'capital',
|
|
13
|
+
};
|
|
8
14
|
export function uniqSessionName() {
|
|
9
15
|
const name = uniqueNamesGenerator(nameConfig);
|
|
10
16
|
const randomNum = Math.floor(Math.random() * 999);
|
|
11
17
|
return `${name}${randomNum}`;
|
|
12
18
|
}
|
|
19
|
+
export function uniqExplorationName() {
|
|
20
|
+
const name = uniqueNamesGenerator(explorationConfig);
|
|
21
|
+
const randomNum = Math.floor(Math.random() * 999);
|
|
22
|
+
return `${name}${randomNum}`;
|
|
23
|
+
}
|
|
@@ -52,6 +52,11 @@ export function generalizeUrl(url) {
|
|
|
52
52
|
export function matchesUrl(pattern, path) {
|
|
53
53
|
if (pattern === '*')
|
|
54
54
|
return true;
|
|
55
|
+
if (!pattern.includes('?')) {
|
|
56
|
+
const queryIndex = path.indexOf('?');
|
|
57
|
+
if (queryIndex >= 0)
|
|
58
|
+
path = path.slice(0, queryIndex);
|
|
59
|
+
}
|
|
55
60
|
const norm = (s) => s?.replace(/\/+$/, '').toLowerCase();
|
|
56
61
|
if (norm(pattern) === norm(path))
|
|
57
62
|
return true;
|
|
@@ -89,7 +94,7 @@ export function extractStatePath(url) {
|
|
|
89
94
|
return url;
|
|
90
95
|
try {
|
|
91
96
|
const urlObj = new URL(url);
|
|
92
|
-
return urlObj.pathname
|
|
97
|
+
return `${urlObj.pathname}${urlObj.search}${urlObj.hash}`;
|
|
93
98
|
}
|
|
94
99
|
catch {
|
|
95
100
|
return url;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { ELEMENT_EXTRACTION_CONFIG, EXPLORBOT_ATTRS, extractElementData, getElementDataExtractorSource } from "./html.js";
|
|
1
2
|
import { buildClickableXPath, evaluateXPath, isDynamicId, isGenericClass } from "./xpath.js";
|
|
3
|
+
export { extractElementData } from "./html.js";
|
|
2
4
|
const KEY_DISPLAY_ATTRS = ['role', 'id', 'class', 'aria-label'];
|
|
3
5
|
const KEY_ATTRS = ['role', 'aria-label', 'id', 'name', 'type', 'href'];
|
|
4
6
|
export class WebElement {
|
|
@@ -35,7 +37,7 @@ export class WebElement {
|
|
|
35
37
|
return `(${this.x}, ${this.y})`;
|
|
36
38
|
}
|
|
37
39
|
get eidx() {
|
|
38
|
-
return this.attrs[
|
|
40
|
+
return this.attrs[EXPLORBOT_ATTRS.eidx] || this.attrs.eidx || null;
|
|
39
41
|
}
|
|
40
42
|
get isNavigationLink() {
|
|
41
43
|
if (this.tag !== 'a')
|
|
@@ -47,6 +49,23 @@ export class WebElement {
|
|
|
47
49
|
const cls = this.attrs.class || '';
|
|
48
50
|
return cls.split(/\s+/).filter((c) => c.length > 2 && !isDynamicId(c) && !isGenericClass(c));
|
|
49
51
|
}
|
|
52
|
+
get areaHints() {
|
|
53
|
+
const raw = this.attrs[EXPLORBOT_ATTRS.area] || '';
|
|
54
|
+
return raw
|
|
55
|
+
.split('|')
|
|
56
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
get contextLabel() {
|
|
60
|
+
return (this.attrs[EXPLORBOT_ATTRS.context] || '').trim();
|
|
61
|
+
}
|
|
62
|
+
get variantHints() {
|
|
63
|
+
const raw = this.attrs[EXPLORBOT_ATTRS.variant] || '';
|
|
64
|
+
return raw
|
|
65
|
+
.split('|')
|
|
66
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
}
|
|
50
69
|
static fromRawData(d, role) {
|
|
51
70
|
return new WebElement({
|
|
52
71
|
tag: d.tag,
|
|
@@ -55,6 +74,7 @@ export class WebElement {
|
|
|
55
74
|
clickXPath: buildClickableXPath({ tag: d.tag, allAttrs: d.allAttrs, text: d.text }),
|
|
56
75
|
attrs: d.allAttrs,
|
|
57
76
|
text: d.text,
|
|
77
|
+
outerHTML: d.outerHTML,
|
|
58
78
|
x: d.x,
|
|
59
79
|
y: d.y,
|
|
60
80
|
});
|
|
@@ -76,7 +96,7 @@ export class WebElement {
|
|
|
76
96
|
const count = await locator.count();
|
|
77
97
|
if (count === 0)
|
|
78
98
|
return null;
|
|
79
|
-
const data = await locator.first().evaluate(extractElementData);
|
|
99
|
+
const data = await locator.first().evaluate(extractElementData, ELEMENT_EXTRACTION_CONFIG);
|
|
80
100
|
if (!data)
|
|
81
101
|
return null;
|
|
82
102
|
return WebElement.fromRawData(data);
|
|
@@ -86,24 +106,24 @@ export class WebElement {
|
|
|
86
106
|
}
|
|
87
107
|
}
|
|
88
108
|
static async fromEidx(page, eidx) {
|
|
89
|
-
return WebElement.fromPlaywrightLocator(page.locator(`[
|
|
109
|
+
return WebElement.fromPlaywrightLocator(page.locator(`[${EXPLORBOT_ATTRS.eidx}="${eidx}"]`));
|
|
90
110
|
}
|
|
91
111
|
static async fromEidxList(page, eidxList) {
|
|
92
112
|
if (eidxList.length === 0)
|
|
93
113
|
return [];
|
|
94
|
-
const rawList = await page.evaluate(([list, extractFnStr]) => {
|
|
114
|
+
const rawList = await page.evaluate(([list, extractFnStr, config]) => {
|
|
95
115
|
const extract = new Function(`return ${extractFnStr}`)();
|
|
96
116
|
const results = [];
|
|
97
117
|
for (const eidx of list) {
|
|
98
|
-
const el = document.querySelector(`[
|
|
118
|
+
const el = document.querySelector(`[${config.attrs.eidx}="${eidx}"]`);
|
|
99
119
|
if (!el)
|
|
100
120
|
continue;
|
|
101
|
-
const data = extract(el);
|
|
121
|
+
const data = extract(el, config);
|
|
102
122
|
if (data)
|
|
103
123
|
results.push(data);
|
|
104
124
|
}
|
|
105
125
|
return results;
|
|
106
|
-
}, [eidxList,
|
|
126
|
+
}, [eidxList, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG]);
|
|
107
127
|
return rawList.map((d) => WebElement.fromRawData(d));
|
|
108
128
|
}
|
|
109
129
|
static async findByXPath(html, xpath) {
|
|
@@ -113,20 +133,3 @@ export class WebElement {
|
|
|
113
133
|
return { totalFound: result.totalFound, elements: result.matches.map((m) => WebElement.fromXPathMatch(m)) };
|
|
114
134
|
}
|
|
115
135
|
}
|
|
116
|
-
export function extractElementData(el) {
|
|
117
|
-
const rect = el.getBoundingClientRect();
|
|
118
|
-
if (rect.width === 0 && rect.height === 0)
|
|
119
|
-
return null;
|
|
120
|
-
const allAttrs = {};
|
|
121
|
-
for (let i = 0; i < el.attributes.length; i++) {
|
|
122
|
-
const attr = el.attributes[i];
|
|
123
|
-
allAttrs[attr.name] = attr.value;
|
|
124
|
-
}
|
|
125
|
-
return {
|
|
126
|
-
tag: el.tagName.toLowerCase(),
|
|
127
|
-
text: (el.textContent || '').trim().slice(0, 80),
|
|
128
|
-
allAttrs,
|
|
129
|
-
x: Math.round(rect.x + rect.width / 2),
|
|
130
|
-
y: Math.round(rect.y + rect.height / 2),
|
|
131
|
-
};
|
|
132
|
-
}
|
package/dist/src/utils/xpath.js
CHANGED
|
@@ -33,7 +33,7 @@ function getAbsoluteXPath(el) {
|
|
|
33
33
|
return `//${parts.join('/')}`;
|
|
34
34
|
}
|
|
35
35
|
export const isDynamicId = (id) => /^(ember|react|__next)\d|^\d+$/.test(id);
|
|
36
|
-
export const isGenericClass = (cls) => /^ember-view$|^ember\d|^react-|^__next/.test(cls);
|
|
36
|
+
export const isGenericClass = (cls) => /^ember-view$|^ember\d|^ember-|^react-|^__next/.test(cls);
|
|
37
37
|
export function buildClickableXPath(el) {
|
|
38
38
|
const a = el.allAttrs;
|
|
39
39
|
if (a.id && !isDynamicId(a.id))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"@opentelemetry/sdk-trace-base": "^2.2.0",
|
|
79
79
|
"@opentelemetry/semantic-conventions": "^1.38.0",
|
|
80
80
|
"@scalar/openapi-parser": "^0.25.6",
|
|
81
|
-
"@testomatio/reporter": "^2.7.
|
|
81
|
+
"@testomatio/reporter": "^2.7.9-beta.2-markdown",
|
|
82
82
|
"ai": "^6.0.6",
|
|
83
83
|
"axe-core": "^4.11.1",
|
|
84
84
|
"bash-tool": "^1.3.15",
|
|
@@ -104,11 +104,12 @@
|
|
|
104
104
|
"micromatch": "^4.0.8",
|
|
105
105
|
"ora-classic": "^5.4.2",
|
|
106
106
|
"parse5": "^8.0.0",
|
|
107
|
-
"playwright": "^1.
|
|
107
|
+
"playwright": "^1.59.0",
|
|
108
108
|
"react": "^19.1.1",
|
|
109
109
|
"strip-ansi": "^7.1.2",
|
|
110
110
|
"turndown": "^7.2.1",
|
|
111
111
|
"unique-names-generator": "^4.7.1",
|
|
112
|
+
"yaml": "^2.8.3",
|
|
112
113
|
"yargs": "^17.7.2",
|
|
113
114
|
"zod": "^4.1.8"
|
|
114
115
|
},
|
|
@@ -5,6 +5,8 @@ Container CSS must be a SINGLE semantic selector — one class, one id, or one a
|
|
|
5
5
|
- VALID: semantic class names that describe what the section IS (`.product-list`, `.sidebar-menu`, `.user-profile`, `.search-results`), semantic roles (`[role="dialog"]`), semantic ids (`#main-content`)
|
|
6
6
|
|
|
7
7
|
The container must uniquely identify a semantic wrapper, not a path through the DOM.
|
|
8
|
+
|
|
9
|
+
Iframes are not sections — list each iframe as a single row of type `iframe` inside the section that contains it. Never use an iframe selector as a section's `> Container:`.
|
|
8
10
|
</container_rules>
|
|
9
11
|
|
|
10
12
|
<css_selector_rules>
|
package/src/action-result.ts
CHANGED
|
@@ -457,8 +457,9 @@ export class ActionResult implements ActionResultData {
|
|
|
457
457
|
try {
|
|
458
458
|
const urlObj = new URL(this.url);
|
|
459
459
|
const path = urlObj.pathname.replace(/\/$/, '') || '/';
|
|
460
|
+
const search = urlObj.search || '';
|
|
460
461
|
const hash = urlObj.hash || '';
|
|
461
|
-
return path + hash;
|
|
462
|
+
return path + search + hash;
|
|
462
463
|
} catch {
|
|
463
464
|
// If URL parsing fails, assume it's already a relative URL
|
|
464
465
|
return this.url;
|