@ulpi/browse 0.7.2 → 0.7.4
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/package.json +1 -1
- package/skill/SKILL.md +2 -1
- package/src/cli.ts +3 -2
- package/src/snapshot.ts +94 -6
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -261,7 +261,8 @@ browse accessibility Accessibility tree snapshot (ARIA)
|
|
|
261
261
|
```
|
|
262
262
|
browse snapshot Full accessibility tree with @refs
|
|
263
263
|
browse snapshot -i Interactive elements only — terse flat list (minimal tokens)
|
|
264
|
-
browse snapshot -i -
|
|
264
|
+
browse snapshot -i -f Interactive elements — full indented tree with props
|
|
265
|
+
browse snapshot -i -V Interactive elements — viewport only (skip below-fold)
|
|
265
266
|
browse snapshot -c Compact (no empty structural elements)
|
|
266
267
|
browse snapshot -C Cursor-interactive (detect divs with cursor:pointer/onclick/tabindex)
|
|
267
268
|
browse snapshot -d <N> Limit depth to N levels
|
package/src/cli.ts
CHANGED
|
@@ -612,7 +612,7 @@ Inspection: js <expr> | eval <file> | css <sel> <prop> | attrs <sel>
|
|
|
612
612
|
cookies | storage [set <k> <v>] | perf
|
|
613
613
|
value <sel> | count <sel> | clipboard [write <text>]
|
|
614
614
|
Visual: screenshot [path] | pdf [path] | responsive [prefix]
|
|
615
|
-
Snapshot: snapshot [-i] [-
|
|
615
|
+
Snapshot: snapshot [-i] [-f] [-V] [-c] [-C] [-d N] [-s sel]
|
|
616
616
|
Find: find role|text|label|placeholder|testid <query> [name]
|
|
617
617
|
Compare: diff <url1> <url2> | screenshot-diff <baseline> [current]
|
|
618
618
|
Multi-step: chain (reads JSON from stdin)
|
|
@@ -639,7 +639,8 @@ Options:
|
|
|
639
639
|
|
|
640
640
|
Snapshot flags:
|
|
641
641
|
-i Interactive elements only (terse flat list by default)
|
|
642
|
-
-
|
|
642
|
+
-f Full — indented tree with props and children (use with -i)
|
|
643
|
+
-V Viewport — only elements visible in current viewport
|
|
643
644
|
-c Compact — remove empty structural elements
|
|
644
645
|
-C Cursor-interactive — detect divs with cursor:pointer,
|
|
645
646
|
onclick, tabindex, data-action (missed by ARIA tree)
|
package/src/snapshot.ts
CHANGED
|
@@ -31,8 +31,9 @@ const INTERACTIVE_ROLES = new Set([
|
|
|
31
31
|
|
|
32
32
|
interface SnapshotOptions {
|
|
33
33
|
interactive?: boolean; // -i: only interactive elements (terse flat list by default)
|
|
34
|
-
|
|
34
|
+
full?: boolean; // -f: full indented ARIA tree with props/children (overrides -i terse default)
|
|
35
35
|
compact?: boolean; // -c: remove empty structural elements
|
|
36
|
+
viewport?: boolean; // -V: only elements visible in current viewport
|
|
36
37
|
depth?: number; // -d N: limit tree depth
|
|
37
38
|
selector?: string; // -s SEL: scope to CSS selector
|
|
38
39
|
cursor?: boolean; // -C: detect cursor-interactive elements (divs with cursor:pointer, onclick, tabindex)
|
|
@@ -73,9 +74,13 @@ export function parseSnapshotArgs(args: string[]): SnapshotOptions {
|
|
|
73
74
|
case '--compact':
|
|
74
75
|
opts.compact = true;
|
|
75
76
|
break;
|
|
76
|
-
case '-
|
|
77
|
-
case '--
|
|
78
|
-
opts.
|
|
77
|
+
case '-f':
|
|
78
|
+
case '--full':
|
|
79
|
+
opts.full = true;
|
|
80
|
+
break;
|
|
81
|
+
case '-V':
|
|
82
|
+
case '--viewport':
|
|
83
|
+
opts.viewport = true;
|
|
79
84
|
break;
|
|
80
85
|
case '-C':
|
|
81
86
|
case '--cursor':
|
|
@@ -422,8 +427,8 @@ export async function handleSnapshot(
|
|
|
422
427
|
refMap.set(ref, locator);
|
|
423
428
|
|
|
424
429
|
// Format output line
|
|
425
|
-
// -i without -
|
|
426
|
-
const terse = opts.interactive && !opts.
|
|
430
|
+
// -i without -f: terse flat list (no indent, no props, no children)
|
|
431
|
+
const terse = opts.interactive && !opts.full;
|
|
427
432
|
let outputLine: string;
|
|
428
433
|
if (terse) {
|
|
429
434
|
outputLine = `@${ref} [${node.role}]`;
|
|
@@ -441,6 +446,89 @@ export async function handleSnapshot(
|
|
|
441
446
|
output.push(outputLine);
|
|
442
447
|
}
|
|
443
448
|
|
|
449
|
+
// Viewport filter: remove elements outside the visible viewport
|
|
450
|
+
// Uses a single page.evaluate() for speed — checking 189 locators individually is slow
|
|
451
|
+
if (opts.viewport) {
|
|
452
|
+
const vp = page.viewportSize();
|
|
453
|
+
if (vp) {
|
|
454
|
+
// Build a list of {ref, role, name} to check in the DOM
|
|
455
|
+
const checks = Array.from(refMap.keys()).map(ref => {
|
|
456
|
+
const line = output.find(l => l.includes(`@${ref} `));
|
|
457
|
+
const roleMatch = line?.match(/\[(\w+)\]/);
|
|
458
|
+
const nameMatch = line?.match(/"([^"]*)"/);
|
|
459
|
+
return { ref, role: roleMatch?.[1] || '', name: nameMatch?.[1] || '' };
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const visibleRefs = await evalCtx.evaluate(
|
|
463
|
+
({ checks, vpHeight }) => {
|
|
464
|
+
const ROLE_TO_SELECTOR: Record<string, string> = {
|
|
465
|
+
link: 'a,[role="link"]',
|
|
466
|
+
button: 'button,[role="button"],input[type="button"],input[type="submit"]',
|
|
467
|
+
textbox: 'input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="hidden"]),textarea,[role="textbox"]',
|
|
468
|
+
checkbox: 'input[type="checkbox"],[role="checkbox"]',
|
|
469
|
+
radio: 'input[type="radio"],[role="radio"]',
|
|
470
|
+
combobox: 'select,[role="combobox"]',
|
|
471
|
+
searchbox: 'input[type="search"],[role="searchbox"]',
|
|
472
|
+
tab: '[role="tab"]',
|
|
473
|
+
switch: '[role="switch"]',
|
|
474
|
+
slider: 'input[type="range"],[role="slider"]',
|
|
475
|
+
menuitem: '[role="menuitem"]',
|
|
476
|
+
option: 'option,[role="option"]',
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const visible = new Set<string>();
|
|
480
|
+
// Track which elements we've already matched per role+name
|
|
481
|
+
const roleCounts = new Map<string, number>();
|
|
482
|
+
|
|
483
|
+
for (const { ref, role, name } of checks) {
|
|
484
|
+
const selector = ROLE_TO_SELECTOR[role] || `[role="${role}"]`;
|
|
485
|
+
const all = document.querySelectorAll(selector);
|
|
486
|
+
const key = `${role}:${name}`;
|
|
487
|
+
const skip = roleCounts.get(key) || 0;
|
|
488
|
+
|
|
489
|
+
let matched = 0;
|
|
490
|
+
for (let i = 0; i < all.length; i++) {
|
|
491
|
+
const el = all[i] as HTMLElement;
|
|
492
|
+
// Match by accessible name (textContent or aria-label)
|
|
493
|
+
const accName = (el.getAttribute('aria-label') || el.textContent || '').trim();
|
|
494
|
+
// For terse mode, name may be truncated — check startsWith
|
|
495
|
+
const nameMatches = !name || accName === name ||
|
|
496
|
+
(name.endsWith('...') && accName.startsWith(name.slice(0, -3)));
|
|
497
|
+
if (!nameMatches) continue;
|
|
498
|
+
|
|
499
|
+
if (matched < skip) { matched++; continue; }
|
|
500
|
+
|
|
501
|
+
const rect = el.getBoundingClientRect();
|
|
502
|
+
if (rect.y + rect.height > 0 && rect.y < vpHeight) {
|
|
503
|
+
visible.add(ref);
|
|
504
|
+
}
|
|
505
|
+
matched++;
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
roleCounts.set(key, skip + 1);
|
|
509
|
+
}
|
|
510
|
+
return [...visible];
|
|
511
|
+
},
|
|
512
|
+
{ checks, vpHeight: vp.height }
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
const visibleSet = new Set(visibleRefs);
|
|
516
|
+
const toRemove = new Set<string>();
|
|
517
|
+
for (const ref of refMap.keys()) {
|
|
518
|
+
if (!visibleSet.has(ref)) toRemove.add(ref);
|
|
519
|
+
}
|
|
520
|
+
for (const ref of toRemove) {
|
|
521
|
+
refMap.delete(ref);
|
|
522
|
+
}
|
|
523
|
+
for (let i = output.length - 1; i >= 0; i--) {
|
|
524
|
+
const match = output[i].match(/@(e\d+)/);
|
|
525
|
+
if (match && toRemove.has(match[1])) {
|
|
526
|
+
output.splice(i, 1);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
444
532
|
// Cursor-interactive detection: supplement ARIA tree with DOM-level scan
|
|
445
533
|
if (opts.cursor) {
|
|
446
534
|
const result = await appendCursorElements(evalCtx, locatorRoot, opts, output, refMap, refCounter, bm);
|