@xbrowser/cli 1.9.4 → 1.9.6

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.
@@ -20,7 +20,7 @@ import {
20
20
  saveSessionDiskMeta,
21
21
  setActivePage,
22
22
  touchSession
23
- } from "./chunk-OOJ2H7IL.js";
23
+ } from "./chunk-37SHZSUR.js";
24
24
  import "./chunk-VJNMAWPZ.js";
25
25
  import "./chunk-TNEN6VQ2.js";
26
26
  import "./chunk-GDKLH7ZY.js";
@@ -20,7 +20,7 @@ import {
20
20
  saveSessionDiskMeta,
21
21
  setActivePage,
22
22
  touchSession
23
- } from "./chunk-Z7WC4P2M.js";
23
+ } from "./chunk-74S7UUI2.js";
24
24
  import "./chunk-TNEN6VQ2.js";
25
25
  import "./chunk-GDKLH7ZY.js";
26
26
  import "./chunk-KFQGP6VL.js";
@@ -20,7 +20,7 @@ import {
20
20
  saveSessionDiskMeta,
21
21
  setActivePage,
22
22
  touchSession
23
- } from "./chunk-MSQIPXKE.js";
23
+ } from "./chunk-5JSOGINQ.js";
24
24
  import "./chunk-VJNMAWPZ.js";
25
25
  import "./chunk-TNEN6VQ2.js";
26
26
  import "./chunk-GDKLH7ZY.js";
@@ -2021,7 +2021,7 @@ async function closeSessionByName(name) {
2021
2021
  } catch {
2022
2022
  }
2023
2023
  try {
2024
- const { SessionRecorder } = await import("./session-recorder-UTULJFTE.js");
2024
+ const { SessionRecorder } = await import("./session-recorder-7GXI42FO.js");
2025
2025
  SessionRecorder.cleanup(session.name);
2026
2026
  } catch {
2027
2027
  }
@@ -656,7 +656,7 @@ async function closeSessionByName(name) {
656
656
  } catch {
657
657
  }
658
658
  try {
659
- const { SessionRecorder } = await import("./session-recorder-KPU4YQAE.js");
659
+ const { SessionRecorder } = await import("./session-recorder-RFOPX4SF.js");
660
660
  SessionRecorder.cleanup(session.name);
661
661
  } catch {
662
662
  }
@@ -4965,7 +4965,7 @@ async function closeSessionByName(name) {
4965
4965
  } catch {
4966
4966
  }
4967
4967
  try {
4968
- const { SessionRecorder } = await import("./session-recorder-KPU4YQAE.js");
4968
+ const { SessionRecorder } = await import("./session-recorder-RFOPX4SF.js");
4969
4969
  SessionRecorder.cleanup(session.name);
4970
4970
  } catch {
4971
4971
  }
@@ -462,16 +462,28 @@ function getSelectorGeneratorScript() {
462
462
  if (isUnique(root, s)) return { selector: s, strategy: 'id+tag', confidence: 'high' };
463
463
  }
464
464
 
465
- // [data-testid]
466
- for (var si = 0; si < STABLE_ATTRS.length; si++) {
467
- var v = el.getAttribute(STABLE_ATTRS[si]);
468
- if (v) {
469
- var s = '[' + STABLE_ATTRS[si] + '="' + esc(v) + '"]';
470
- if (isUnique(root, s)) return { selector: s, strategy: 'testid', confidence: 'high' };
471
- s = tag + s;
472
- if (isUnique(root, s)) return { selector: s, strategy: 'testid+tag', confidence: 'high' };
473
- }
474
- }
465
+ // [data-testid]
466
+ for (var si = 0; si < STABLE_ATTRS.length; si++) {
467
+ var v = el.getAttribute(STABLE_ATTRS[si]);
468
+ if (v) {
469
+ var s = '[' + STABLE_ATTRS[si] + '="' + esc(v) + '"]';
470
+ if (isUnique(root, s)) return { selector: s, strategy: 'testid', confidence: 'high' };
471
+ s = tag + s;
472
+ if (isUnique(root, s)) return { selector: s, strategy: 'testid+tag', confidence: 'high' };
473
+ }
474
+ }
475
+
476
+ // [data-*] \u2014 other data attributes beyond known testids
477
+ for (var ai = 0; ai < el.attributes.length; ai++) {
478
+ var a = el.attributes[ai];
479
+ if (!a.name.startsWith('data-')) continue;
480
+ if (STABLE_ATTRS.indexOf(a.name) !== -1) continue;
481
+ if (!a.value || a.value.length > 60 || a.value.indexOf('http') === 0) continue;
482
+ var s = '[' + a.name + '="' + esc(a.value) + '"]';
483
+ if (isUnique(root, s)) return { selector: s, strategy: 'data-attr', confidence: 'high' };
484
+ s = tag + s;
485
+ if (isUnique(root, s)) return { selector: s, strategy: 'data-attr+tag', confidence: 'high' };
486
+ }
475
487
 
476
488
  // [name]
477
489
  var name = el.getAttribute('name');
@@ -480,14 +492,35 @@ function getSelectorGeneratorScript() {
480
492
  if (isUnique(root, s)) return { selector: s, strategy: 'name', confidence: 'high' };
481
493
  }
482
494
 
483
- // [aria-label]
484
- var aria = el.getAttribute('aria-label');
485
- if (aria) {
486
- var s = '[aria-label="' + esc(aria.substring(0, 60)) + '"]';
487
- if (isUnique(root, s)) return { selector: s, strategy: 'aria-label', confidence: 'high' };
488
- s = tag + s;
489
- if (isUnique(root, s)) return { selector: s, strategy: 'aria-label+tag', confidence: 'high' };
490
- }
495
+ // [aria-label]
496
+ var aria = el.getAttribute('aria-label');
497
+ if (aria) {
498
+ var s = '[aria-label="' + esc(aria.substring(0, 60)) + '"]';
499
+ if (isUnique(root, s)) return { selector: s, strategy: 'aria-label', confidence: 'high' };
500
+ s = tag + s;
501
+ if (isUnique(root, s)) return { selector: s, strategy: 'aria-label+tag', confidence: 'high' };
502
+ }
503
+
504
+ // [role][name] combo
505
+ var role = el.getAttribute('role');
506
+ if (role) {
507
+ var name2 = el.getAttribute('name');
508
+ if (name2) {
509
+ var s = '[role="' + role + '"][name="' + esc(name2) + '"]';
510
+ if (isUnique(root, s)) return { selector: s, strategy: 'role+name', confidence: 'high' };
511
+ }
512
+ // [role][aria-label] combo
513
+ if (aria) {
514
+ var s = '[role="' + role + '"][aria-label="' + esc(aria.substring(0, 60)) + '"]';
515
+ if (isUnique(root, s)) return { selector: s, strategy: 'role+aria-label', confidence: 'high' };
516
+ }
517
+ // [role] alone
518
+ var text2 = (el.textContent || '').trim();
519
+ if (text2) {
520
+ var s = '[role="' + role + '"]';
521
+ if (isUnique(root, s)) return { selector: s, strategy: 'role', confidence: 'high' };
522
+ }
523
+ }
491
524
 
492
525
  // [placeholder]
493
526
  var ph = el.getAttribute('placeholder');
@@ -510,11 +543,12 @@ function getSelectorGeneratorScript() {
510
543
  if (isUnique(root, s)) return { selector: s, strategy: 'title', confidence: 'high' };
511
544
  }
512
545
 
513
- // Unique attribute (skip URL-like, long)
514
- var skipAttr = {class:1,style:1,id:1,name:1,'aria-label':1,placeholder:1,alt:1,title:1,role:1,src:1,href:1,action:1,'data-src':1,'data-href':1};
515
- for (var ai = 0; ai < el.attributes.length; ai++) {
516
- var a = el.attributes[ai];
517
- if (skipAttr[a.name] || a.name.startsWith('data-') || a.name.startsWith('aria-')) continue;
546
+ // Unique attribute (skip URL-like, long, and common attrs handled above)
547
+ // Note: non-testid data-* attrs are already tried in [data-*] section above
548
+ var skipAttr = {class:1,style:1,id:1,name:1,'aria-label':1,placeholder:1,alt:1,title:1,role:1,src:1,href:1,action:1,'data-src':1,'data-href':1};
549
+ for (var ai = 0; ai < el.attributes.length; ai++) {
550
+ var a = el.attributes[ai];
551
+ if (skipAttr[a.name] || a.name.startsWith('data-') || a.name.startsWith('aria-')) continue;
518
552
  if (a.value && a.value.length > 2 && a.value.length <= 60) {
519
553
  var s = tag + '[' + a.name + '="' + esc(a.value) + '"]';
520
554
  if (isUnique(root, s)) return { selector: s, strategy: 'attribute', confidence: 'medium' };
@@ -551,10 +585,16 @@ function getSelectorGeneratorScript() {
551
585
  var s = tag + '.' + esc(cls[i]) + '.' + esc(cls[j]);
552
586
  if (isUnique(root, s)) return { selector: s, strategy: 'tag+class-combo', confidence: 'medium' };
553
587
  }
554
- }
555
- }
588
+ }
589
+ }
590
+
591
+ // tag[aria-label] fallback \u2014 aria-label exists but wasn't globally unique
592
+ if (aria) {
593
+ var s = tag + '[aria-label="' + esc(aria.substring(0, 60)) + '"]';
594
+ if (isUnique(root, s)) return { selector: s, strategy: 'tag+aria-label', confidence: 'medium' };
595
+ }
556
596
 
557
- // Parent scope
597
+ // Parent scope
558
598
  var parent = el.parentElement;
559
599
  if (parent && parent !== root) {
560
600
  if (parent.id) {
@@ -1621,12 +1661,28 @@ var SessionRecorder = class _SessionRecorder {
1621
1661
  activePages = /* @__PURE__ */ new Set();
1622
1662
  lastKnownUrl = "";
1623
1663
  // Track URL to detect real navigation changes
1624
- /** Dedup window: after a CDP command action, ignore matching action signals within this window */
1625
- cdpActionDedup = null;
1664
+ /** Dedup map: key = normalizedType|tag|value expiration timestamp.
1665
+ * Replaces old single-entry cdpActionDedup for bidirectional dedup. */
1666
+ dedupMap = /* @__PURE__ */ new Map();
1667
+ dedupActionCount = 0;
1668
+ // counter for periodic cleanup
1626
1669
  /** Network dedup: last request key for short-window dedup */
1627
1670
  _lastRequestKey = "";
1628
1671
  _lastRequestTs = 0;
1629
1672
  _isRecording = false;
1673
+ streamMode = "clean";
1674
+ /** Ambient action types: filtered out in clean mode */
1675
+ static AMBIENT_TYPES = /* @__PURE__ */ new Set([
1676
+ "hover",
1677
+ "focus",
1678
+ "scroll",
1679
+ "visibility",
1680
+ "resize",
1681
+ "clipboard",
1682
+ "touch",
1683
+ "contextmenu",
1684
+ "drag"
1685
+ ]);
1630
1686
  constructor(context, page, sessionName) {
1631
1687
  this.context = context;
1632
1688
  this.page = page;
@@ -1640,13 +1696,10 @@ var SessionRecorder = class _SessionRecorder {
1640
1696
  }
1641
1697
  /** Record an action triggered by a CDP command (e.g. xbrowser fill/click/goto) */
1642
1698
  async recordCommandAction(action) {
1643
- const normalizedType = action.type === "cdp-fill" ? "input" : action.type === "cdp-click" ? "click" : action.type;
1644
- const recent = this.actions[this.actions.length - 1];
1645
- if (recent && Date.now() - recent.timestamp < 1500) {
1646
- const typeMatch = recent.type === action.type || recent.type === normalizedType;
1647
- const valueMatch = !action.value || recent.value === action.value;
1648
- const selectorMatch = !action.selector || recent.element?.selector && (recent.element.selector === action.selector || recent.element.selector.endsWith(" " + action.selector) || action.selector.endsWith(" " + recent.element.selector));
1649
- if (typeMatch && valueMatch && selectorMatch) {
1699
+ const dedupFromAction = this.dedupKey(action.type, action.selector, action.element?.tag, action.value);
1700
+ if (dedupFromAction) {
1701
+ const expires = this.dedupMap.get(dedupFromAction);
1702
+ if (expires && Date.now() < expires) {
1650
1703
  return;
1651
1704
  }
1652
1705
  }
@@ -1668,18 +1721,26 @@ var SessionRecorder = class _SessionRecorder {
1668
1721
  element: actionToPush.element
1669
1722
  });
1670
1723
  this.lastActionTs = ts;
1671
- this.cdpActionDedup = {
1672
- type: normalizedType,
1673
- value: action.value,
1674
- selector: action.selector,
1675
- until: Date.now() + 1500
1676
- };
1724
+ this.dedupMap.set(dedupFromAction, Date.now() + 2e3);
1725
+ this.dedupActionCount++;
1726
+ if (this.dedupActionCount % 200 === 0) {
1727
+ const now = Date.now();
1728
+ for (const [k, v] of this.dedupMap) {
1729
+ if (now >= v) this.dedupMap.delete(k);
1730
+ }
1731
+ }
1677
1732
  if (action.url && action.url !== "about:blank") {
1678
1733
  this.lastKnownUrl = action.url;
1679
1734
  } else if (action.type === "goto" && action.value && action.value !== "about:blank") {
1680
1735
  this.lastKnownUrl = action.value;
1681
1736
  }
1682
1737
  }
1738
+ /** Generate dedup key for an action (normalized type + selector/tag + value). */
1739
+ dedupKey(type, selector, tag, value) {
1740
+ const normType = type === "cdp-click" ? "click" : type === "cdp-fill" ? "input" : type === "cdp-eval" ? "eval" : type;
1741
+ const selOrTag = selector || tag || "";
1742
+ return `${normType}|${selOrTag}|${value || ""}`;
1743
+ }
1683
1744
  get networkCount() {
1684
1745
  return this.network.length;
1685
1746
  }
@@ -1717,7 +1778,7 @@ var SessionRecorder = class _SessionRecorder {
1717
1778
  return join2(this.recordingsDir, ".stop");
1718
1779
  }
1719
1780
  // ─── Start ──────────────────────────────────────────────────────
1720
- async start(url) {
1781
+ async start(url, options) {
1721
1782
  if (this._isRecording) throw new Error("Already recording");
1722
1783
  this._isRecording = true;
1723
1784
  this.startedAt = Date.now();
@@ -1726,6 +1787,7 @@ var SessionRecorder = class _SessionRecorder {
1726
1787
  this.contextChanges = [];
1727
1788
  this.checkpoints = [];
1728
1789
  this.checkpointCounter = 0;
1790
+ this.streamMode = options?.stream ?? "clean";
1729
1791
  this.lastKnownUrl = this.page.url();
1730
1792
  await this.page.addInitScript(getSelectorGeneratorScript());
1731
1793
  await this.page.addInitScript(ACTION_SIGNAL_SCRIPT);
@@ -2294,15 +2356,16 @@ var SessionRecorder = class _SessionRecorder {
2294
2356
  }
2295
2357
  for (const raw of pending) {
2296
2358
  if (raw.ts <= this.lastActionTs) continue;
2297
- if (this.cdpActionDedup && Date.now() < this.cdpActionDedup.until) {
2298
- const dedup = this.cdpActionDedup;
2299
- const typeMatch = raw.type === dedup.type;
2300
- const valueMatch = !dedup.value || raw.value === dedup.value;
2301
- const selectorMatch = !dedup.selector || raw.element?.selector && (raw.element.selector === dedup.selector || raw.element.selector.endsWith(" " + dedup.selector) || dedup.selector.endsWith(" " + raw.element.selector));
2302
- if (typeMatch && valueMatch && selectorMatch) {
2359
+ const dedupFromSignal = this.dedupKey(raw.type, raw.element?.selector, raw.element?.tag, raw.value);
2360
+ if (dedupFromSignal) {
2361
+ const expires = this.dedupMap.get(dedupFromSignal);
2362
+ if (expires && Date.now() < expires) {
2303
2363
  continue;
2304
2364
  }
2305
2365
  }
2366
+ if (this.streamMode === "clean" && _SessionRecorder.AMBIENT_TYPES.has(raw.type)) {
2367
+ continue;
2368
+ }
2306
2369
  this.actionCounter++;
2307
2370
  let clickContext;
2308
2371
  if (raw.type === "click" && raw.x !== void 0 && raw.y !== void 0) {
@@ -2327,6 +2390,18 @@ var SessionRecorder = class _SessionRecorder {
2327
2390
  const pushed = this.actions[this.actions.length - 1];
2328
2391
  pushed.elementScreenshot = await this.captureElementScreenshot(page, raw);
2329
2392
  this.lastActionTs = raw.ts;
2393
+ const dedupKey = this.dedupKey(raw.type, raw.element?.selector, raw.element?.tag, raw.value);
2394
+ if (dedupKey) {
2395
+ this.dedupMap.set(dedupKey, Date.now() + 2e3);
2396
+ this.dedupActionCount++;
2397
+ }
2398
+ const verifyTypes = /* @__PURE__ */ new Set(["click", "submit", "input", "change", "cdp-click", "cdp-fill"]);
2399
+ if (verifyTypes.has(raw.type)) {
2400
+ const signals = await this.verifyAction(page, pushed).catch(() => []);
2401
+ if (signals.length > 0) {
2402
+ pushed.__signals = signals;
2403
+ }
2404
+ }
2330
2405
  if (raw.type === "click" || raw.type === "navigate" || raw.type === "submit") {
2331
2406
  const detected = await this.detectCheckpoints(page);
2332
2407
  for (const cp of detected) {
@@ -2509,6 +2584,44 @@ var SessionRecorder = class _SessionRecorder {
2509
2584
  return [];
2510
2585
  }
2511
2586
  }
2587
+ // ─── Verification: collect success signals after key actions ──────
2588
+ /**
2589
+ * After a key action (click/submit/input), check for success signals:
2590
+ * URL changes, recent successful network responses, dialog appearances.
2591
+ */
2592
+ async verifyAction(page, action) {
2593
+ const signals = [];
2594
+ try {
2595
+ const currentUrl = page.url();
2596
+ if (currentUrl && currentUrl !== "about:blank" && currentUrl !== action.url) {
2597
+ signals.push({ type: "url_change", value: currentUrl, label: `URL changed to ${currentUrl}` });
2598
+ }
2599
+ } catch {
2600
+ }
2601
+ try {
2602
+ const recentNetwork = this.network.filter(
2603
+ (n) => n.timestamp > action.timestamp - 100 && n.timestamp < action.timestamp + 5e3 && n.status >= 200 && n.status < 400
2604
+ );
2605
+ for (const net of recentNetwork.slice(0, 3)) {
2606
+ signals.push({
2607
+ type: "network_success",
2608
+ value: `${net.status}`,
2609
+ label: `${net.method} ${net.path} \u2192 ${net.status}`
2610
+ });
2611
+ }
2612
+ } catch {
2613
+ }
2614
+ try {
2615
+ const recentDialog = [...this.checkpoints].reverse().find(
2616
+ (cp) => cp.type === "dialog" && Math.abs(cp.timestamp - action.timestamp) < 3e3
2617
+ );
2618
+ if (recentDialog) {
2619
+ signals.push({ type: "dialog", value: recentDialog.type, label: recentDialog.hint });
2620
+ }
2621
+ } catch {
2622
+ }
2623
+ return signals;
2624
+ }
2512
2625
  // ─── Periodic disk flush ────────────────────────────────────────
2513
2626
  flushToDisk() {
2514
2627
  const data = this.buildData();
@@ -2579,7 +2692,7 @@ var SessionRecorder = class _SessionRecorder {
2579
2692
  return false;
2580
2693
  };
2581
2694
  const meaningfulNetwork = data.network.filter((n) => !isNoiseNetwork(n));
2582
- const filtered = data.actions.filter((a) => a.type !== "scroll");
2695
+ const filtered = data.actions;
2583
2696
  const groups = [];
2584
2697
  let current = null;
2585
2698
  for (const action of filtered) {
@@ -2606,6 +2719,7 @@ var SessionRecorder = class _SessionRecorder {
2606
2719
  );
2607
2720
  const matchedInputs = inputAction ? this.matchActionToNetwork(inputAction, nearbyNetwork) : [];
2608
2721
  const clickMatches = primary.type === "click" && primary.element?.text ? this.matchActionToNetwork(primary, nearbyNetwork) : [];
2722
+ const signals = primary.__signals || [];
2609
2723
  steps.push({
2610
2724
  step: steps.length + 1,
2611
2725
  ref: getRef(primary),
@@ -2615,7 +2729,8 @@ var SessionRecorder = class _SessionRecorder {
2615
2729
  responseBody: n.responseBody && JSON.stringify(n.responseBody).length > 1e3 ? "[truncated, " + JSON.stringify(n.responseBody).length + " bytes]" : n.responseBody
2616
2730
  })),
2617
2731
  contextChanges: nearbyContext,
2618
- matchedInputs: [...matchedInputs, ...clickMatches]
2732
+ matchedInputs: [...matchedInputs, ...clickMatches],
2733
+ signals
2619
2734
  });
2620
2735
  }
2621
2736
  return {