@xbrowser/cli 1.9.5 → 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.
- package/dist/{browser-6WJHQDPV.js → browser-ALPLJNH2.js} +1 -1
- package/dist/{browser-4KRHALJ3.js → browser-JXIJF4VL.js} +1 -1
- package/dist/{browser-C63PFGPT.js → browser-ZPQP6VW7.js} +1 -1
- package/dist/{chunk-OOJ2H7IL.js → chunk-37SHZSUR.js} +1 -1
- package/dist/{chunk-MSQIPXKE.js → chunk-5JSOGINQ.js} +1 -1
- package/dist/{chunk-Z7WC4P2M.js → chunk-74S7UUI2.js} +1 -1
- package/dist/{chunk-TTZNR3QP.js → chunk-C4O73DQL.js} +165 -50
- package/dist/{chunk-YSCY52UJ.js → chunk-CH5ZQV64.js} +165 -50
- package/dist/{chunk-53LRNYPG.js → chunk-YAUG3NSI.js} +2 -2
- package/dist/cli.js +22 -13
- package/dist/{daemon-client-PZX2KOLQ.js → daemon-client-4HJENICO.js} +2 -2
- package/dist/{daemon-client-TOUDMIY5.js → daemon-client-CMRAWXTN.js} +1 -1
- package/dist/daemon-main.js +12 -8
- package/dist/index.d.ts +24 -3
- package/dist/index.js +21 -12
- package/dist/{session-recorder-UTULJFTE.js → session-recorder-7GXI42FO.js} +1 -1
- package/dist/{session-recorder-KPU4YQAE.js → session-recorder-RFOPX4SF.js} +1 -1
- package/dist/{session-replayer-RWFOWH3F.js → session-replayer-3HEIJBJ2.js} +65 -33
- package/package.json +1 -1
|
@@ -2021,7 +2021,7 @@ async function closeSessionByName(name) {
|
|
|
2021
2021
|
} catch {
|
|
2022
2022
|
}
|
|
2023
2023
|
try {
|
|
2024
|
-
const { SessionRecorder } = await import("./session-recorder-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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
|
|
1625
|
-
cdpActionDedup
|
|
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
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
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.
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
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
|
-
|
|
2298
|
-
|
|
2299
|
-
const
|
|
2300
|
-
|
|
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
|
|
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 {
|