@ulpi/browse 0.7.3 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/snapshot.ts +65 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulpi/browse",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ulpi-io/browse"
package/src/snapshot.ts CHANGED
@@ -446,27 +446,80 @@ export async function handleSnapshot(
446
446
  output.push(outputLine);
447
447
  }
448
448
 
449
- // Viewport filter: remove elements below the visible viewport
449
+ // Viewport filter: remove elements outside the visible viewport
450
+ // Uses a single page.evaluate() for speed — checking 189 locators individually is slow
450
451
  if (opts.viewport) {
451
452
  const vp = page.viewportSize();
452
453
  if (vp) {
453
- const toRemove = new Set<string>();
454
- await Promise.all(
455
- Array.from(refMap.entries()).map(async ([ref, loc]) => {
456
- try {
457
- const box = await loc.boundingBox({ timeout: 500 });
458
- if (!box || box.y >= vp.height || box.y + box.height <= 0) {
459
- toRemove.add(ref);
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;
460
507
  }
461
- } catch {
462
- toRemove.add(ref);
508
+ roleCounts.set(key, skip + 1);
463
509
  }
464
- })
510
+ return [...visible];
511
+ },
512
+ { checks, vpHeight: vp.height }
465
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
+ }
466
520
  for (const ref of toRemove) {
467
521
  refMap.delete(ref);
468
522
  }
469
- // Remove output lines for filtered refs
470
523
  for (let i = output.length - 1; i >= 0; i--) {
471
524
  const match = output[i].match(/@(e\d+)/);
472
525
  if (match && toRemove.has(match[1])) {