@veraxhq/verax 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +14 -36
- package/src/cli/commands/default.js +523 -0
- package/src/cli/commands/doctor.js +165 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +402 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +296 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +34 -0
- package/src/cli/util/expectation-extractor.js +378 -0
- package/src/cli/util/findings-writer.js +31 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +366 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +29 -0
- package/src/cli/util/project-discovery.js +277 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/summary-writer.js +32 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +101 -0
- package/src/verax/cli/wizard.js +98 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +403 -0
- package/src/verax/core/incremental-store.js +237 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +521 -0
- package/src/verax/detect/comparison.js +2 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +177 -0
- package/src/verax/detect/expectation-model.js +194 -172
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +44 -8
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +172 -286
- package/src/verax/detect/interactive-findings.js +613 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/verdict-engine.js +563 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/index.js +90 -14
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +579 -0
- package/src/verax/intel/vue-router-extractor.js +323 -0
- package/src/verax/learn/action-contract-extractor.js +335 -101
- package/src/verax/learn/ast-contract-extractor.js +95 -5
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/manifest-writer.js +97 -47
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +27 -96
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +112 -4
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +10 -5
- package/src/verax/observe/console-sensor.js +1 -17
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +512 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +643 -275
- package/src/verax/observe/index.js +908 -27
- package/src/verax/observe/index.js.backup +1 -0
- package/src/verax/observe/interaction-discovery.js +365 -14
- package/src/verax/observe/interaction-runner.js +563 -198
- package/src/verax/observe/loading-sensor.js +139 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +37 -17
- package/src/verax/observe/state-sensor.js +389 -0
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +61 -20
- package/src/verax/observe/ui-signal-sensor.js +136 -17
- package/src/verax/scan-summary-writer.js +77 -15
- package/src/verax/shared/artifact-manager.js +110 -8
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +170 -0
- package/src/verax/shared/dynamic-route-utils.js +218 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +14 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +65 -0
- package/src/verax/validate/context-validator.js +244 -0
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This is a backup of the broken version
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { generateSelector } from './selector-generator.js';
|
|
2
2
|
import { isExternalHref } from './domain-boundary.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
const MAX_INTERACTIONS_PER_PAGE = 30;
|
|
3
|
+
import { DEFAULT_SCAN_BUDGET } from '../shared/scan-budget.js';
|
|
6
4
|
|
|
7
5
|
function computePriority(candidate, viewportHeight) {
|
|
8
6
|
const hasAboveFold = candidate.boundingAvailable && typeof viewportHeight === 'number' && candidate.boundingY < viewportHeight;
|
|
9
7
|
const isFooter = candidate.boundingAvailable && typeof viewportHeight === 'number' && candidate.boundingY >= viewportHeight;
|
|
10
8
|
const isInternalLink = candidate.type === 'link' && candidate.href && candidate.href !== '#' && (!candidate.isExternal || candidate.href.startsWith('/'));
|
|
11
9
|
|
|
10
|
+
if (candidate.type === 'file_upload') return 2;
|
|
11
|
+
if (candidate.type === 'keyboard') return 2.5;
|
|
12
|
+
if (candidate.type === 'hover') return 3;
|
|
13
|
+
if (candidate.type === 'login') return 1;
|
|
14
|
+
if (candidate.type === 'logout') return 1.5;
|
|
15
|
+
if (candidate.type === 'auth_guard') return 1.8;
|
|
12
16
|
if (candidate.type === 'form') return 1;
|
|
13
17
|
if (candidate.type === 'link' && isFooter) return 6;
|
|
14
18
|
if (isInternalLink) return 2;
|
|
@@ -67,12 +71,7 @@ async function extractLabel(element) {
|
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
export async function discoverInteractions(page, baseOrigin) {
|
|
71
|
-
// Wave 2: Apply scrolling before discovery to reveal lazy-loaded elements
|
|
72
|
-
const driver = new HumanBehaviorDriver({ maxScrollSteps: 5 });
|
|
73
|
-
await driver.discoverInteractionsWithScroll(page);
|
|
74
|
-
|
|
75
|
-
// Now run the full discovery with all elements visible
|
|
74
|
+
export async function discoverInteractions(page, baseOrigin, scanBudget = DEFAULT_SCAN_BUDGET) {
|
|
76
75
|
const currentUrl = page.url();
|
|
77
76
|
const interactions = [];
|
|
78
77
|
const seenElements = new Set();
|
|
@@ -124,6 +123,8 @@ export async function discoverInteractions(page, baseOrigin) {
|
|
|
124
123
|
const text = await button.evaluate(el => el.textContent?.trim() || '');
|
|
125
124
|
const dataHref = await button.getAttribute('data-href');
|
|
126
125
|
const dataTestId = await button.getAttribute('data-testid');
|
|
126
|
+
const dataDanger = await button.getAttribute('data-danger');
|
|
127
|
+
const dataDestructive = await button.getAttribute('data-destructive');
|
|
127
128
|
|
|
128
129
|
allInteractions.push({
|
|
129
130
|
type: isLangToggle ? 'toggle' : 'button',
|
|
@@ -135,6 +136,8 @@ export async function discoverInteractions(page, baseOrigin) {
|
|
|
135
136
|
text: text,
|
|
136
137
|
dataHref: dataHref || '',
|
|
137
138
|
dataTestId: dataTestId || '',
|
|
139
|
+
dataDanger: dataDanger !== null,
|
|
140
|
+
dataDestructive: dataDestructive !== null,
|
|
138
141
|
isRoleButton: false
|
|
139
142
|
});
|
|
140
143
|
}
|
|
@@ -202,9 +205,17 @@ export async function discoverInteractions(page, baseOrigin) {
|
|
|
202
205
|
for (const form of forms) {
|
|
203
206
|
const submitButton = form.locator('button[type="submit"], input[type="submit"]').first();
|
|
204
207
|
if (await submitButton.count() > 0) {
|
|
208
|
+
const formAction = await form.getAttribute('action');
|
|
205
209
|
const selector = await generateSelector(submitButton);
|
|
206
210
|
const selectorKey = `form:${selector}`;
|
|
207
211
|
|
|
212
|
+
// Check if this is a login form (has password input)
|
|
213
|
+
const hasPasswordInput = await form.locator('input[type="password"]').count() > 0;
|
|
214
|
+
const formText = await form.evaluate(el => el.textContent?.toLowerCase() || '');
|
|
215
|
+
const isLoginForm = hasPasswordInput ||
|
|
216
|
+
/login|signin|sign.in|authenticate/i.test(formText) ||
|
|
217
|
+
/email|username|user/i.test(formText) && hasPasswordInput;
|
|
218
|
+
|
|
208
219
|
if (!seenElements.has(selectorKey)) {
|
|
209
220
|
seenElements.add(selectorKey);
|
|
210
221
|
const label = await extractLabel(submitButton);
|
|
@@ -213,7 +224,7 @@ export async function discoverInteractions(page, baseOrigin) {
|
|
|
213
224
|
const text = await submitButton.evaluate(el => el.textContent?.trim() || el.getAttribute('value') || '');
|
|
214
225
|
|
|
215
226
|
allInteractions.push({
|
|
216
|
-
type: 'form',
|
|
227
|
+
type: isLoginForm ? 'login' : 'form',
|
|
217
228
|
selector: selector,
|
|
218
229
|
label: label || text,
|
|
219
230
|
element: submitButton,
|
|
@@ -222,12 +233,185 @@ export async function discoverInteractions(page, baseOrigin) {
|
|
|
222
233
|
text: text,
|
|
223
234
|
dataHref: '',
|
|
224
235
|
dataTestId: '',
|
|
236
|
+
isRoleButton: false,
|
|
237
|
+
formAction: formAction || '',
|
|
238
|
+
hasPasswordInput: hasPasswordInput
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Detect logout actions (buttons/links with logout/signout patterns) - check existing buttons/links first
|
|
245
|
+
for (const item of allInteractions) {
|
|
246
|
+
if (item.type === 'button' || item.type === 'link') {
|
|
247
|
+
const text = (item.text || '').trim().toLowerCase();
|
|
248
|
+
const label = (item.label || '').trim().toLowerCase();
|
|
249
|
+
const combined = `${text} ${label}`;
|
|
250
|
+
|
|
251
|
+
const isLogout = /^(logout|sign\s*out|signout|log\s*out)$/i.test(text) ||
|
|
252
|
+
/^(logout|sign\s*out|signout|log\s*out)$/i.test(label) ||
|
|
253
|
+
(text.includes('logout') || text.includes('sign out') || text.includes('signout'));
|
|
254
|
+
|
|
255
|
+
if (isLogout) {
|
|
256
|
+
const selectorKey = `logout:${item.selector}`;
|
|
257
|
+
if (!seenElements.has(selectorKey)) {
|
|
258
|
+
seenElements.add(selectorKey);
|
|
259
|
+
allInteractions.push({
|
|
260
|
+
type: 'logout',
|
|
261
|
+
selector: item.selector,
|
|
262
|
+
label: item.label,
|
|
263
|
+
element: item.element,
|
|
264
|
+
tagName: item.tagName,
|
|
265
|
+
id: item.id,
|
|
266
|
+
text: item.text,
|
|
267
|
+
dataHref: item.dataHref || '',
|
|
268
|
+
dataTestId: item.dataTestId || '',
|
|
269
|
+
isRoleButton: item.isRoleButton || false
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Detect potential protected routes by looking for links/buttons with typical protected path patterns
|
|
277
|
+
const internalLinks = await page.locator('a[href]:not([href*="://"])').all();
|
|
278
|
+
for (const link of internalLinks) {
|
|
279
|
+
const href = await link.getAttribute('href');
|
|
280
|
+
const text = await link.evaluate(el => el.textContent?.trim() || '');
|
|
281
|
+
const id = await link.getAttribute('id') || '';
|
|
282
|
+
const combined = `${href} ${text} ${id}`.toLowerCase();
|
|
283
|
+
|
|
284
|
+
const isProtectedPath = /admin|dashboard|profile|account|settings|private|protected|secure/i.test(href || '') ||
|
|
285
|
+
/admin|dashboard|profile|account|settings/i.test(combined);
|
|
286
|
+
|
|
287
|
+
if (isProtectedPath && href && !href.startsWith('#')) {
|
|
288
|
+
const selector = await generateSelector(link);
|
|
289
|
+
const selectorKey = `auth_guard:${selector}`;
|
|
290
|
+
|
|
291
|
+
if (!seenElements.has(selectorKey)) {
|
|
292
|
+
seenElements.add(selectorKey);
|
|
293
|
+
const label = await extractLabel(link);
|
|
294
|
+
const tagName = await link.evaluate(el => el.tagName.toLowerCase());
|
|
295
|
+
|
|
296
|
+
allInteractions.push({
|
|
297
|
+
type: 'auth_guard',
|
|
298
|
+
selector: selector,
|
|
299
|
+
label: label || text,
|
|
300
|
+
element: link,
|
|
301
|
+
tagName: tagName,
|
|
302
|
+
id: id || '',
|
|
303
|
+
text: text,
|
|
304
|
+
href: href || '',
|
|
305
|
+
dataHref: '',
|
|
306
|
+
dataTestId: '',
|
|
225
307
|
isRoleButton: false
|
|
226
308
|
});
|
|
227
309
|
}
|
|
228
310
|
}
|
|
229
311
|
}
|
|
230
312
|
|
|
313
|
+
const fileInputs = await page.locator('input[type="file"]:not([disabled])').all();
|
|
314
|
+
for (const fileInput of fileInputs) {
|
|
315
|
+
const selector = await generateSelector(fileInput);
|
|
316
|
+
const selectorKey = `file:${selector}`;
|
|
317
|
+
|
|
318
|
+
if (!seenElements.has(selectorKey)) {
|
|
319
|
+
seenElements.add(selectorKey);
|
|
320
|
+
const label = await extractLabel(fileInput);
|
|
321
|
+
const tagName = await fileInput.evaluate(el => el.tagName.toLowerCase());
|
|
322
|
+
const id = await fileInput.getAttribute('id');
|
|
323
|
+
const accept = await fileInput.getAttribute('accept');
|
|
324
|
+
|
|
325
|
+
allInteractions.push({
|
|
326
|
+
type: 'file_upload',
|
|
327
|
+
selector,
|
|
328
|
+
label: label || 'File upload',
|
|
329
|
+
element: fileInput,
|
|
330
|
+
tagName,
|
|
331
|
+
id: id || '',
|
|
332
|
+
text: accept || '',
|
|
333
|
+
dataHref: '',
|
|
334
|
+
dataTestId: '',
|
|
335
|
+
isRoleButton: false
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const hoverableCandidates = await page.locator('[aria-haspopup], [data-hover], [role="menu"], [role="menuitem"]').all();
|
|
341
|
+
for (const hoverEl of hoverableCandidates) {
|
|
342
|
+
try {
|
|
343
|
+
const selector = await generateSelector(hoverEl);
|
|
344
|
+
const selectorKey = `hover:${selector}`;
|
|
345
|
+
|
|
346
|
+
if (!seenElements.has(selectorKey)) {
|
|
347
|
+
seenElements.add(selectorKey);
|
|
348
|
+
const label = await extractLabel(hoverEl);
|
|
349
|
+
const tagName = await hoverEl.evaluate(el => el.tagName.toLowerCase());
|
|
350
|
+
const id = await hoverEl.getAttribute('id');
|
|
351
|
+
const text = await hoverEl.evaluate(el => el.textContent?.trim() || '');
|
|
352
|
+
const ariaHasPopup = await hoverEl.getAttribute('aria-haspopup') || '';
|
|
353
|
+
const role = await hoverEl.getAttribute('role') || '';
|
|
354
|
+
const dataHover = await hoverEl.getAttribute('data-hover') || '';
|
|
355
|
+
|
|
356
|
+
const box = await hoverEl.boundingBox();
|
|
357
|
+
if (box && box.width > 0 && box.height > 0) {
|
|
358
|
+
allInteractions.push({
|
|
359
|
+
type: 'hover',
|
|
360
|
+
selector: selector,
|
|
361
|
+
label: label || text || role || 'hoverable',
|
|
362
|
+
element: hoverEl,
|
|
363
|
+
tagName: tagName,
|
|
364
|
+
id: id || '',
|
|
365
|
+
text: text,
|
|
366
|
+
ariaHasPopup: ariaHasPopup,
|
|
367
|
+
role: role,
|
|
368
|
+
dataHover: dataHover,
|
|
369
|
+
dataHref: '',
|
|
370
|
+
dataTestId: '',
|
|
371
|
+
isRoleButton: false
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
} catch (error) {
|
|
376
|
+
// Skip if element is invalid
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const keyboardFocusableElements = await page.locator('button, a[href], input[type="submit"], input[type="button"]').all();
|
|
381
|
+
for (const focusableEl of keyboardFocusableElements) {
|
|
382
|
+
try {
|
|
383
|
+
const selector = await generateSelector(focusableEl);
|
|
384
|
+
const selectorKey = `keyboard:${selector}`;
|
|
385
|
+
|
|
386
|
+
if (!seenElements.has(selectorKey)) {
|
|
387
|
+
seenElements.add(selectorKey);
|
|
388
|
+
const label = await extractLabel(focusableEl);
|
|
389
|
+
const tagName = await focusableEl.evaluate(el => el.tagName.toLowerCase());
|
|
390
|
+
const id = await focusableEl.getAttribute('id');
|
|
391
|
+
const text = await focusableEl.evaluate(el => el.textContent?.trim() || el.value || '');
|
|
392
|
+
|
|
393
|
+
const box = await focusableEl.boundingBox();
|
|
394
|
+
if (box && box.width > 0 && box.height > 0) {
|
|
395
|
+
allInteractions.push({
|
|
396
|
+
type: 'keyboard',
|
|
397
|
+
selector: selector,
|
|
398
|
+
label: label || text,
|
|
399
|
+
element: focusableEl,
|
|
400
|
+
tagName: tagName,
|
|
401
|
+
id: id || '',
|
|
402
|
+
text: text,
|
|
403
|
+
dataHref: '',
|
|
404
|
+
dataTestId: '',
|
|
405
|
+
isRoleButton: false
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
// Skip if element is invalid
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
|
|
231
415
|
const viewport = page.viewportSize();
|
|
232
416
|
const viewportHeight = viewport ? viewport.height : undefined;
|
|
233
417
|
|
|
@@ -245,13 +429,13 @@ export async function discoverInteractions(page, baseOrigin) {
|
|
|
245
429
|
}
|
|
246
430
|
|
|
247
431
|
const sorted = sortCandidates(allInteractions);
|
|
248
|
-
const capped = sorted.length >
|
|
249
|
-
const selected = sorted.slice(0,
|
|
432
|
+
const capped = sorted.length > scanBudget.maxInteractionsPerPage;
|
|
433
|
+
const selected = sorted.slice(0, scanBudget.maxInteractionsPerPage);
|
|
250
434
|
|
|
251
435
|
const coverage = {
|
|
252
436
|
candidatesDiscovered: sorted.length,
|
|
253
437
|
candidatesSelected: selected.length,
|
|
254
|
-
cap:
|
|
438
|
+
cap: scanBudget.maxInteractionsPerPage,
|
|
255
439
|
capped
|
|
256
440
|
};
|
|
257
441
|
|
|
@@ -261,9 +445,176 @@ export async function discoverInteractions(page, baseOrigin) {
|
|
|
261
445
|
selector: item.selector,
|
|
262
446
|
label: item.label,
|
|
263
447
|
element: item.element,
|
|
264
|
-
isExternal: item.isExternal || false
|
|
448
|
+
isExternal: item.isExternal || false,
|
|
449
|
+
href: item.href,
|
|
450
|
+
text: item.text
|
|
265
451
|
})),
|
|
266
452
|
coverage
|
|
267
453
|
};
|
|
268
454
|
}
|
|
269
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Discover ALL interactions on a page (no priority cap).
|
|
458
|
+
* Used for full-site coverage traversal.
|
|
459
|
+
*/
|
|
460
|
+
export async function discoverAllInteractions(page, baseOrigin, scanBudget = DEFAULT_SCAN_BUDGET) {
|
|
461
|
+
const currentUrl = page.url();
|
|
462
|
+
const seenElements = new Set();
|
|
463
|
+
const allInteractions = [];
|
|
464
|
+
|
|
465
|
+
const links = await page.locator('a[href]').all();
|
|
466
|
+
for (const link of links) {
|
|
467
|
+
const href = await link.getAttribute('href');
|
|
468
|
+
if (href && !href.startsWith('#') && !href.startsWith('javascript:')) {
|
|
469
|
+
const isExternal = isExternalHref(href, baseOrigin, currentUrl);
|
|
470
|
+
const selector = await generateSelector(link);
|
|
471
|
+
const selectorKey = `link:${selector}`;
|
|
472
|
+
|
|
473
|
+
if (!seenElements.has(selectorKey)) {
|
|
474
|
+
seenElements.add(selectorKey);
|
|
475
|
+
const label = await extractLabel(link);
|
|
476
|
+
const text = await link.evaluate(el => el.textContent?.trim() || '');
|
|
477
|
+
|
|
478
|
+
allInteractions.push({
|
|
479
|
+
type: 'link',
|
|
480
|
+
selector: selector,
|
|
481
|
+
label: label,
|
|
482
|
+
element: link,
|
|
483
|
+
isExternal: isExternal,
|
|
484
|
+
href: href,
|
|
485
|
+
text: text
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const buttons = await page.locator('button:not([disabled])').all();
|
|
492
|
+
for (const button of buttons) {
|
|
493
|
+
const selector = await generateSelector(button);
|
|
494
|
+
const selectorKey = `button:${selector}`;
|
|
495
|
+
|
|
496
|
+
if (!seenElements.has(selectorKey)) {
|
|
497
|
+
seenElements.add(selectorKey);
|
|
498
|
+
const label = await extractLabel(button);
|
|
499
|
+
const elementHandle = await button.elementHandle();
|
|
500
|
+
const isLangToggle = elementHandle ? await isLanguageToggle(elementHandle) : false;
|
|
501
|
+
const text = await button.evaluate(el => el.textContent?.trim() || '');
|
|
502
|
+
const dataHref = await button.getAttribute('data-href');
|
|
503
|
+
|
|
504
|
+
allInteractions.push({
|
|
505
|
+
type: isLangToggle ? 'toggle' : 'button',
|
|
506
|
+
selector: selector,
|
|
507
|
+
label: label,
|
|
508
|
+
element: button,
|
|
509
|
+
text: text,
|
|
510
|
+
dataHref: dataHref || ''
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const submitInputs = await page.locator('input[type="submit"]:not([disabled]), input[type="button"]:not([disabled])').all();
|
|
516
|
+
for (const input of submitInputs) {
|
|
517
|
+
const selector = await generateSelector(input);
|
|
518
|
+
const selectorKey = `input:${selector}`;
|
|
519
|
+
|
|
520
|
+
if (!seenElements.has(selectorKey)) {
|
|
521
|
+
seenElements.add(selectorKey);
|
|
522
|
+
const label = await extractLabel(input);
|
|
523
|
+
const text = await input.getAttribute('value') || '';
|
|
524
|
+
const dataHref = await input.getAttribute('data-href');
|
|
525
|
+
|
|
526
|
+
allInteractions.push({
|
|
527
|
+
type: 'button',
|
|
528
|
+
selector: selector,
|
|
529
|
+
label: label || text,
|
|
530
|
+
element: input,
|
|
531
|
+
text: text,
|
|
532
|
+
dataHref: dataHref || ''
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const roleButtons = await page.locator('[role="button"]:not([disabled])').all();
|
|
538
|
+
for (const roleButton of roleButtons) {
|
|
539
|
+
const selector = await generateSelector(roleButton);
|
|
540
|
+
const selectorKey = `role-button:${selector}`;
|
|
541
|
+
|
|
542
|
+
if (!seenElements.has(selectorKey)) {
|
|
543
|
+
seenElements.add(selectorKey);
|
|
544
|
+
const label = await extractLabel(roleButton);
|
|
545
|
+
const text = await roleButton.evaluate(el => el.textContent?.trim() || '');
|
|
546
|
+
const dataHref = await roleButton.getAttribute('data-href');
|
|
547
|
+
|
|
548
|
+
allInteractions.push({
|
|
549
|
+
type: 'button',
|
|
550
|
+
selector: selector,
|
|
551
|
+
label: label,
|
|
552
|
+
element: roleButton,
|
|
553
|
+
text: text,
|
|
554
|
+
dataHref: dataHref || ''
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const forms = await page.locator('form').all();
|
|
560
|
+
for (const form of forms) {
|
|
561
|
+
const submitButton = form.locator('button[type="submit"], input[type="submit"]').first();
|
|
562
|
+
if (await submitButton.count() > 0) {
|
|
563
|
+
const formAction = await form.getAttribute('action');
|
|
564
|
+
const selector = await generateSelector(submitButton);
|
|
565
|
+
const selectorKey = `form:${selector}`;
|
|
566
|
+
|
|
567
|
+
if (!seenElements.has(selectorKey)) {
|
|
568
|
+
seenElements.add(selectorKey);
|
|
569
|
+
const label = await extractLabel(submitButton);
|
|
570
|
+
const text = await submitButton.evaluate(el => el.textContent?.trim() || el.getAttribute('value') || '');
|
|
571
|
+
|
|
572
|
+
allInteractions.push({
|
|
573
|
+
type: 'form',
|
|
574
|
+
selector: selector,
|
|
575
|
+
label: label || text,
|
|
576
|
+
element: submitButton,
|
|
577
|
+
text: text,
|
|
578
|
+
formAction: formAction || ''
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Prioritize non-navigating interactions so navigation doesn't starve buttons/forms
|
|
585
|
+
const priority = {
|
|
586
|
+
form: 0,
|
|
587
|
+
button: 1,
|
|
588
|
+
toggle: 1,
|
|
589
|
+
link: 2
|
|
590
|
+
};
|
|
591
|
+
const ordered = allInteractions.sort((a, b) => {
|
|
592
|
+
const pa = priority[a.type] ?? 3;
|
|
593
|
+
const pb = priority[b.type] ?? 3;
|
|
594
|
+
if (pa !== pb) return pa - pb;
|
|
595
|
+
return (a.selector || '').localeCompare(b.selector || '');
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Return ALL interactions (no priority cap)
|
|
599
|
+
return {
|
|
600
|
+
interactions: ordered.map(item => ({
|
|
601
|
+
type: item.type,
|
|
602
|
+
selector: item.selector,
|
|
603
|
+
label: item.label,
|
|
604
|
+
element: item.element,
|
|
605
|
+
isExternal: item.isExternal || false,
|
|
606
|
+
href: item.href,
|
|
607
|
+
text: item.text,
|
|
608
|
+
dataHref: item.dataHref,
|
|
609
|
+
dataDanger: item.dataDanger || false,
|
|
610
|
+
dataDestructive: item.dataDestructive || false
|
|
611
|
+
})),
|
|
612
|
+
coverage: {
|
|
613
|
+
candidatesDiscovered: allInteractions.length,
|
|
614
|
+
candidatesSelected: allInteractions.length,
|
|
615
|
+
cap: Infinity,
|
|
616
|
+
capped: false
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|