@xbrowser/cli 1.9.8 → 1.9.9
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/{anti-bot-MCVM6IFB.js → anti-bot-DR56Y63V.js} +1 -1
- package/dist/{browser-GT4BMX6X.js → browser-FFYPFTBV.js} +1 -1
- package/dist/{browser-43YXUIML.js → browser-RHWUAMTB.js} +2 -2
- package/dist/{browser-SJWPJFN7.js → browser-YCO4GJMZ.js} +2 -2
- package/dist/{cdp-driver-WAFYSIIW.js → cdp-driver-2T4P4ZE2.js} +87 -1
- package/dist/{cdp-driver-PUZLDSQU.js → cdp-driver-J3YQ4LJV.js} +1 -1
- package/dist/chunk-3OD76SUE.js +20 -0
- package/dist/chunk-43TEMG4U.js +9 -0
- package/dist/{chunk-C3LIGO4V.js → chunk-4452SPFI.js} +906 -16
- package/dist/{chunk-VEWTHYDG.js → chunk-5UGR6MUK.js} +906 -16
- package/dist/{chunk-ENCEMM7Z.js → chunk-6OZWKXSW.js} +15 -16
- package/dist/{chunk-7ZDWM6T2.js → chunk-BNO7OKO4.js} +5 -6
- package/dist/{chunk-24SY6PE5.js → chunk-CG3D3CHY.js} +1 -8
- package/dist/{chunk-ZJHQPMCY.js → chunk-DWDXEGVK.js} +5 -6
- package/dist/{chunk-A4YHJW3Z.js → chunk-GUTBIYFW.js} +101 -16
- package/dist/{chunk-DPJNCMLC.js → chunk-INTQPBYF.js} +7 -0
- package/dist/{human-interaction-OU7LZ6OZ.js → chunk-MWFHZUIY.js} +6 -7
- package/dist/chunk-Q2DFJQGS.js +128 -0
- package/dist/{chunk-GM3HWQ5U.js → chunk-RRBXV7KE.js} +87 -1
- package/dist/{chunk-PBDID7SE.js → chunk-RZM5CNIY.js} +15 -16
- package/dist/{human-interaction-HJTXFVQS.js → chunk-SQHSZENE.js} +2 -2
- package/dist/chunk-TZPKFUBT.js +20 -0
- package/dist/{chunk-YAUG3NSI.js → chunk-YMUSHPU4.js} +16 -128
- package/dist/cli.js +41 -28
- package/dist/{daemon-client-4HJENICO.js → daemon-client-GKEPT4NY.js} +15 -3
- package/dist/{daemon-client-CMRAWXTN.js → daemon-client-O6BYVRXV.js} +4 -1
- package/dist/daemon-main.js +635 -57
- package/dist/human-interaction-5AO42MBA.js +8 -0
- package/dist/{human-interaction-2DZK4MW7.js → human-interaction-C5OEGXGO.js} +1 -1
- package/dist/human-interaction-ISXZTAYY.js +8 -0
- package/dist/index.d.ts +167 -2
- package/dist/index.js +173 -63
- package/dist/recovery-EF33LKRJ.js +77 -0
- package/dist/recovery-J2ISVGUL.js +77 -0
- package/dist/recovery-ZJVVHP7N.js +76 -0
- package/dist/{session-recorder-K3K63Q33.js → session-recorder-H3KEYU26.js} +1 -1
- package/dist/{session-recorder-AG6CH6EI.js → session-recorder-QRZMKFVL.js} +1 -1
- package/dist/{session-replayer-V4MP6M6I.js → session-replayer-LJUC4TI7.js} +24 -7
- package/package.json +1 -1
|
@@ -34,6 +34,22 @@ function getSelectorGeneratorScript() {
|
|
|
34
34
|
/^semi-[a-z]{4,}/
|
|
35
35
|
];
|
|
36
36
|
var STABLE_ATTRS = ['data-testid', 'data-test-id', 'data-cy', 'data-qa', 'data-el'];
|
|
37
|
+
// Dynamic attrs known to change per page load (skip even if unique)
|
|
38
|
+
var UNSTABLE_ATTRS = [
|
|
39
|
+
'data-spm-anchor-id', 'data-reactid', 'data-reactroot',
|
|
40
|
+
'data-n-head', 'data-server-rendered',
|
|
41
|
+
];
|
|
42
|
+
// Patterns catching other dynamic attrs by name shape
|
|
43
|
+
var UNSTABLE_ATTR_PATTERNS = [
|
|
44
|
+
/^data-spm/, /^data-track/, /^data-log/, /^data-pf/, /^data-key$/, /^data-rbd/,
|
|
45
|
+
];
|
|
46
|
+
function isUnstableAttr(name) {
|
|
47
|
+
if (UNSTABLE_ATTRS.indexOf(name) !== -1) return true;
|
|
48
|
+
for (var i = 0; i < UNSTABLE_ATTR_PATTERNS.length; i++) {
|
|
49
|
+
if (UNSTABLE_ATTR_PATTERNS[i].test(name)) return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
37
53
|
var MAX_DEPTH = 5;
|
|
38
54
|
|
|
39
55
|
function isStableClass(cls) {
|
|
@@ -81,6 +97,8 @@ function getSelectorGeneratorScript() {
|
|
|
81
97
|
var a = el.attributes[ai];
|
|
82
98
|
if (!a.name.startsWith('data-')) continue;
|
|
83
99
|
if (STABLE_ATTRS.indexOf(a.name) !== -1) continue;
|
|
100
|
+
// Skip dynamic attrs (e.g. data-spm-anchor-id changes per page load)
|
|
101
|
+
if (isUnstableAttr(a.name)) continue;
|
|
84
102
|
if (!a.value || a.value.length > 60 || a.value.indexOf('http') === 0) continue;
|
|
85
103
|
var s = '[' + a.name + '="' + esc(a.value) + '"]';
|
|
86
104
|
if (isUnique(root, s)) return { selector: s, strategy: 'data-attr', confidence: 'high' };
|
|
@@ -385,8 +403,11 @@ var ACTION_SIGNAL_SCRIPT = `
|
|
|
385
403
|
selector = uniqueSelector(el);
|
|
386
404
|
}
|
|
387
405
|
|
|
388
|
-
// For low-confidence selectors (nth-of-type)
|
|
389
|
-
//
|
|
406
|
+
// For low-confidence selectors (nth-of-type) OR selectors from dynamic
|
|
407
|
+
// attributes (which may not survive page reload), generate a text-based
|
|
408
|
+
// fallback when the element has short, unique text (e.g. menu items
|
|
409
|
+
// "\u5220\u9664", "\u786E\u8BA4", "\u6700\u65B0"). This lets replay fall back to text matching
|
|
410
|
+
// when the primary selector can't be found.
|
|
390
411
|
var textFallback;
|
|
391
412
|
var popupContext;
|
|
392
413
|
|
|
@@ -396,7 +417,18 @@ var ACTION_SIGNAL_SCRIPT = `
|
|
|
396
417
|
popupEl = el.closest('[role="menu"], [role="listbox"], [role="dialog"], [role="tooltip"], [role="list"], [class*="popover"], [class*="popup"], [class*="dropdown"], [class*="menu"], [class*="modal"], [id*="menu"], [id*="dropdown"], [id*="popup"], [id*="modal"]');
|
|
397
418
|
} catch(e) {}
|
|
398
419
|
|
|
399
|
-
if
|
|
420
|
+
// Determine if primary selector is "fragile" \u2014 won't survive page reload
|
|
421
|
+
// because it comes from a dynamic attribute (e.g. data-spm-anchor-id).
|
|
422
|
+
var selectorIsFragile = false;
|
|
423
|
+
if (selector && confidence === 'high' && strategy === 'data-attr') {
|
|
424
|
+
// Fragile if the selector references a known dynamic attribute
|
|
425
|
+
selectorIsFragile = selector.indexOf('[data-spm-anchor-id') !== -1
|
|
426
|
+
|| selector.indexOf('[data-reactid') !== -1
|
|
427
|
+
|| selector.indexOf('[data-track') !== -1
|
|
428
|
+
|| selector.indexOf('[data-log') !== -1;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (confidence === 'low' || selectorIsFragile) {
|
|
400
432
|
var rawText = (el.textContent || '').trim();
|
|
401
433
|
if (rawText && rawText.length >= 1 && rawText.length <= 30 && el.children.length === 0) {
|
|
402
434
|
try {
|
|
@@ -638,6 +670,10 @@ var ACTION_SIGNAL_SCRIPT = `
|
|
|
638
670
|
'.popover','.popup','.dropdown','.menu','.modal','.tooltip','.panel',
|
|
639
671
|
'[class*="popover"]','[class*="popup"]','[class*="dropdown"]','[class*="menu"]','[class*="tooltip"]',
|
|
640
672
|
'[class*="modal"]','[class*="panel"]','[class*="overlay"]','[class*="sheet"]',
|
|
673
|
+
// Modern SPA conventions: many libraries expose the open menu via class
|
|
674
|
+
// fragments like "items-container", "options", "list", "select-popup".
|
|
675
|
+
'[class*="items-container"]','[class*="options"]','[class*="select-popup"]',
|
|
676
|
+
'[class*="select-items"]','[class*="menu-items"]','[class*="list-box"]',
|
|
641
677
|
'[data-popup]','[data-dropdown]','[data-menu]','[data-popover]',
|
|
642
678
|
'.semi-dropdown','.semi-popover','.semi-modal','.semi-select-option',
|
|
643
679
|
'.ant-dropdown','.ant-popover','.ant-modal','.ant-select-dropdown',
|
|
@@ -679,6 +715,11 @@ var ACTION_SIGNAL_SCRIPT = `
|
|
|
679
715
|
if (child.getAttribute('aria-disabled') === 'true') childInfo.disabled = true;
|
|
680
716
|
if (child.tagName) childInfo.tag = child.tagName.toLowerCase();
|
|
681
717
|
if (child.href) childInfo.href = child.href.substring(0, 80);
|
|
718
|
+
// Attach a selector so replay / AI can target this item precisely.
|
|
719
|
+
try {
|
|
720
|
+
var childSel = uniqueSelector(child);
|
|
721
|
+
if (childSel) childInfo.selector = childSel;
|
|
722
|
+
} catch(e) {}
|
|
682
723
|
items.push(childInfo);
|
|
683
724
|
}
|
|
684
725
|
result.appeared.push({
|
|
@@ -740,6 +781,9 @@ var ACTION_SIGNAL_SCRIPT = `
|
|
|
740
781
|
|
|
741
782
|
pushAction('click', { element: describe(resolveMeaningful(e)), x: cx, y: cy });
|
|
742
783
|
|
|
784
|
+
// Mark this element as user-interacted in discovered_filters (proactive sensing)
|
|
785
|
+
try { __xb_markTriggerInteracted(e.target); } catch(_) {}
|
|
786
|
+
|
|
743
787
|
// After 200ms, capture what changed
|
|
744
788
|
setTimeout(function() {
|
|
745
789
|
try {
|
|
@@ -878,12 +922,102 @@ var ACTION_SIGNAL_SCRIPT = `
|
|
|
878
922
|
}, true);
|
|
879
923
|
|
|
880
924
|
// \u2500\u2500 Hover (throttled to 800ms) \u2500\u2500
|
|
925
|
+
// After a hover we asynchronously sample for popups/dropdowns that appear
|
|
926
|
+
// (200/500/1000ms) and run a short-lived MutationObserver. Any popup that
|
|
927
|
+
// surfaces is recorded on the hover action as hoverContext, mirroring the
|
|
928
|
+
// clickContext flow used after clicks.
|
|
881
929
|
var __xb_last_hover = 0;
|
|
930
|
+
var __xb_hover_observer = null; // current hover's MutationObserver
|
|
931
|
+
var __xb_hover_observer_timer = null; // auto-disconnect timer
|
|
932
|
+
var __xb_hover_action_ref = null; // ref to the hover action being enriched
|
|
933
|
+
var __xb_hover_seen_popups = {}; // dedup popup selectors for current hover
|
|
934
|
+
var __xb_hover_cx = 0, __xb_hover_cy = 0;
|
|
935
|
+
|
|
936
|
+
function __xb_merge_hover_context(ctx) {
|
|
937
|
+
if (!__xb_hover_action_ref || !ctx) return;
|
|
938
|
+
if (ctx.appeared.length === 0 && ctx.stateChanges.length === 0) return;
|
|
939
|
+
if (!__xb_hover_action_ref.hoverContext) {
|
|
940
|
+
__xb_hover_action_ref.hoverContext = { appeared: [], disappeared: [], stateChanges: [] };
|
|
941
|
+
}
|
|
942
|
+
var hc = __xb_hover_action_ref.hoverContext;
|
|
943
|
+
ctx.appeared.forEach(function(p) {
|
|
944
|
+
var key = p.selector || p.text;
|
|
945
|
+
if (!key || __xb_hover_seen_popups[key]) return;
|
|
946
|
+
__xb_hover_seen_popups[key] = true;
|
|
947
|
+
hc.appeared.push(p);
|
|
948
|
+
});
|
|
949
|
+
ctx.stateChanges.forEach(function(s) {
|
|
950
|
+
hc.stateChanges.push(s);
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function __xb_capture_hover_popups() {
|
|
955
|
+
if (!__xb_hover_action_ref) return;
|
|
956
|
+
try {
|
|
957
|
+
var ctx = captureVisibleContext(__xb_hover_cx, __xb_hover_cy);
|
|
958
|
+
__xb_merge_hover_context(ctx);
|
|
959
|
+
} catch(e) {}
|
|
960
|
+
}
|
|
961
|
+
// Expose for flush-time sweep (server-side flushPendingActions calls this
|
|
962
|
+
// before draining the pending queue so any in-flight hover enrichment lands).
|
|
963
|
+
window.__xb_capture_hover_popups = __xb_capture_hover_popups;
|
|
964
|
+
|
|
965
|
+
function __xb_start_hover_observer() {
|
|
966
|
+
if (!('MutationObserver' in window)) return;
|
|
967
|
+
if (__xb_hover_observer) {
|
|
968
|
+
__xb_hover_observer.disconnect();
|
|
969
|
+
__xb_hover_observer = null;
|
|
970
|
+
}
|
|
971
|
+
if (__xb_hover_observer_timer) {
|
|
972
|
+
clearTimeout(__xb_hover_observer_timer);
|
|
973
|
+
__xb_hover_observer_timer = null;
|
|
974
|
+
}
|
|
975
|
+
__xb_hover_observer = new MutationObserver(function(mutations) {
|
|
976
|
+
if (!__xb_hover_action_ref) return;
|
|
977
|
+
for (var i = 0; i < mutations.length; i++) {
|
|
978
|
+
var added = mutations[i].addedNodes;
|
|
979
|
+
for (var j = 0; j < added.length; j++) {
|
|
980
|
+
var node = added[j];
|
|
981
|
+
if (node.nodeType !== 1) continue;
|
|
982
|
+
var matches = false;
|
|
983
|
+
// Is the added node itself a popover?
|
|
984
|
+
for (var k = 0; k < POPOVER_SELECTORS.length; k++) {
|
|
985
|
+
try { if (node.matches && node.matches(POPOVER_SELECTORS[k])) { matches = true; break; } } catch(e) {}
|
|
986
|
+
}
|
|
987
|
+
// Or does it contain a popover?
|
|
988
|
+
if (!matches && node.querySelector) {
|
|
989
|
+
for (var k2 = 0; k2 < POPOVER_SELECTORS.length; k2++) {
|
|
990
|
+
try { if (node.querySelector(POPOVER_SELECTORS[k2])) { matches = true; break; } } catch(e) {}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (matches) {
|
|
994
|
+
__xb_capture_hover_popups();
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
try {
|
|
1001
|
+
__xb_hover_observer.observe(document.body, { childList: true, subtree: true });
|
|
1002
|
+
} catch(e) {}
|
|
1003
|
+
// Auto-disconnect after 1.2s \u2014 hover popups rarely appear later than this.
|
|
1004
|
+
__xb_hover_observer_timer = setTimeout(function() {
|
|
1005
|
+
if (__xb_hover_observer) {
|
|
1006
|
+
__xb_hover_observer.disconnect();
|
|
1007
|
+
__xb_hover_observer = null;
|
|
1008
|
+
}
|
|
1009
|
+
__xb_hover_observer_timer = null;
|
|
1010
|
+
}, 1200);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
882
1013
|
document.addEventListener('mouseover', function(e) {
|
|
883
1014
|
if (Date.now() - __xb_last_hover < 800) return;
|
|
884
|
-
__xb_last_hover = Date.now();
|
|
885
1015
|
var target = resolveMeaningful(e);
|
|
886
|
-
// Only record hover on interactive elements
|
|
1016
|
+
// Only record hover on interactive elements. Modern SPAs often implement
|
|
1017
|
+
// clickable items as <span>/<div> with onClick handlers and no semantic
|
|
1018
|
+
// role, so we also accept elements whose computed style suggests they are
|
|
1019
|
+
// meant to be interacted with (pointer cursor, or a class name that looks
|
|
1020
|
+
// like a UI control).
|
|
887
1021
|
var tag = target.tagName && target.tagName.toLowerCase();
|
|
888
1022
|
var isInteractive = tag === 'a' || tag === 'button' || tag === 'input'
|
|
889
1023
|
|| tag === 'select' || tag === 'textarea' || tag === 'summary'
|
|
@@ -893,12 +1027,69 @@ var ACTION_SIGNAL_SCRIPT = `
|
|
|
893
1027
|
|| target.getAttribute('role') === 'tab'
|
|
894
1028
|
|| target.getAttribute('role') === 'option'
|
|
895
1029
|
|| !!target.closest('[role="menu"], [role="menubar"], [role="tablist"], [role="listbox"], [role="tree"], nav, menu');
|
|
1030
|
+
// Heuristic fallback for span/div/li that look interactive (onClick,
|
|
1031
|
+
// pointer cursor, or class names hinting at a UI control like
|
|
1032
|
+
// "select", "trigger", "tab", "sort", "menu", "item").
|
|
1033
|
+
if (!isInteractive && (tag === 'span' || tag === 'div' || tag === 'li')) {
|
|
1034
|
+
try {
|
|
1035
|
+
if (target.onclick) isInteractive = true;
|
|
1036
|
+
} catch(e2) {}
|
|
1037
|
+
if (!isInteractive) {
|
|
1038
|
+
try {
|
|
1039
|
+
var cs = window.getComputedStyle(target);
|
|
1040
|
+
if (cs.cursor === 'pointer') isInteractive = true;
|
|
1041
|
+
} catch(e3) {}
|
|
1042
|
+
}
|
|
1043
|
+
if (!isInteractive) {
|
|
1044
|
+
try {
|
|
1045
|
+
var cls = (target.className && typeof target.className === 'string')
|
|
1046
|
+
? target.className.toLowerCase() : '';
|
|
1047
|
+
if (/(?:^|[-_])(select|trigger|tab|sort|menu|item|dropdown|popover|title|toggle|action|btn)(?:[-_]|$)/.test(cls)) {
|
|
1048
|
+
// But only if an ancestor looks like a toolbar/menu/sort bar \u2014
|
|
1049
|
+
// otherwise this would match almost every list item on the web.
|
|
1050
|
+
if (target.closest('[class*="select"], [class*="sort"], [class*="tab"], [class*="menu"], [class*="toolbar"], [class*="filter"], [class*="dropdown"]')) {
|
|
1051
|
+
isInteractive = true;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
} catch(e4) {}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
896
1057
|
if (isInteractive) {
|
|
1058
|
+
// Reset state for the new hover target
|
|
1059
|
+
if (__xb_hover_observer) {
|
|
1060
|
+
__xb_hover_observer.disconnect();
|
|
1061
|
+
__xb_hover_observer = null;
|
|
1062
|
+
}
|
|
1063
|
+
if (__xb_hover_observer_timer) {
|
|
1064
|
+
clearTimeout(__xb_hover_observer_timer);
|
|
1065
|
+
__xb_hover_observer_timer = null;
|
|
1066
|
+
}
|
|
1067
|
+
__xb_hover_seen_popups = {};
|
|
1068
|
+
|
|
1069
|
+
var cx = e.clientX, cy = e.clientY;
|
|
1070
|
+
__xb_hover_cx = cx;
|
|
1071
|
+
__xb_hover_cy = cy;
|
|
1072
|
+
|
|
897
1073
|
pushAction('hover', {
|
|
898
1074
|
element: describe(target),
|
|
899
|
-
x:
|
|
900
|
-
y:
|
|
1075
|
+
x: cx,
|
|
1076
|
+
y: cy,
|
|
901
1077
|
});
|
|
1078
|
+
// Mark this element as user-interacted in discovered_filters
|
|
1079
|
+
try { __xb_markTriggerInteracted(target); } catch(_) {}
|
|
1080
|
+
__xb_last_hover = Date.now();
|
|
1081
|
+
|
|
1082
|
+
// Grab a reference to the just-pushed action so we can enrich it async.
|
|
1083
|
+
var pending = window.__xb_pending_actions;
|
|
1084
|
+
__xb_hover_action_ref = pending.length > 0 ? pending[pending.length - 1] : null;
|
|
1085
|
+
|
|
1086
|
+
// 1. Async sampling: 200/500/1000ms covers CSS transitions + JS render delays.
|
|
1087
|
+
setTimeout(__xb_capture_hover_popups, 200);
|
|
1088
|
+
setTimeout(__xb_capture_hover_popups, 500);
|
|
1089
|
+
setTimeout(__xb_capture_hover_popups, 1000);
|
|
1090
|
+
|
|
1091
|
+
// 2. Short-lived MutationObserver for popups that appear on their own schedule.
|
|
1092
|
+
__xb_start_hover_observer();
|
|
902
1093
|
}
|
|
903
1094
|
}, true);
|
|
904
1095
|
|
|
@@ -1007,6 +1198,511 @@ var ACTION_SIGNAL_SCRIPT = `
|
|
|
1007
1198
|
visibility: { state: document.hidden ? 'hidden' : 'visible' },
|
|
1008
1199
|
});
|
|
1009
1200
|
}, true);
|
|
1201
|
+
|
|
1202
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1203
|
+
// PROACTIVE SENSING \u2014 discover interactive regions without user action.
|
|
1204
|
+
//
|
|
1205
|
+
// Three layers:
|
|
1206
|
+
// 1. __xb_scanFilters() \u2014 baseline scan, called on start/navigation
|
|
1207
|
+
// 2. Long-lived MutationObserver \u2014 watches popover visible transitions
|
|
1208
|
+
// 3. mousemove throttled diff \u2014 senses triggers near the cursor
|
|
1209
|
+
//
|
|
1210
|
+
// Results:
|
|
1211
|
+
// - 'discovered_filters' action \u2192 merged into RecordingData.discoveredFilters
|
|
1212
|
+
// - 'popup_appear' action \u2192 added to actions stream (with cause tag)
|
|
1213
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1214
|
+
|
|
1215
|
+
var FILTER_CONTAINER_SELECTORS = [
|
|
1216
|
+
'[role="tablist"]', '[role="menu"]', '[role="menubar"]', '[role="toolbar"]',
|
|
1217
|
+
'[role="navigation"]', '[role="listbox"]',
|
|
1218
|
+
'[class*="filter"]', '[class*="sort"]', '[class*="toolbar"]',
|
|
1219
|
+
'[class*="select-bar"]', '[class*="tabs"]', '[class*="nav-bar"]',
|
|
1220
|
+
'[class*="sidebar"]', '[class*="menu-bar"]', '[class*="select-container"]',
|
|
1221
|
+
'fieldset',
|
|
1222
|
+
];
|
|
1223
|
+
|
|
1224
|
+
var TRIGGER_SELECTORS = [
|
|
1225
|
+
'[role="tab"]', '[role="menuitem"]', '[role="menuitemcheckbox"]',
|
|
1226
|
+
'[role="menuitemradio"]', '[role="option"]', '[role="button"]',
|
|
1227
|
+
'[role="link"]', '[role="switch"]', '[role="checkbox"]',
|
|
1228
|
+
'button', 'a', 'select',
|
|
1229
|
+
// Modern SPA span/div triggers (cursor:pointer is a strong signal)
|
|
1230
|
+
'span[class*="title"]', 'div[class*="title"]',
|
|
1231
|
+
'div[class*="trigger"]', 'span[class*="trigger"]',
|
|
1232
|
+
'div[class*="item"]', 'li',
|
|
1233
|
+
];
|
|
1234
|
+
|
|
1235
|
+
/** Categorize a container/trigger by class name + role heuristics. */
|
|
1236
|
+
function categorizeElement(el) {
|
|
1237
|
+
try {
|
|
1238
|
+
var cls = ((el.className && typeof el.className === 'string') ? el.className : '').toLowerCase();
|
|
1239
|
+
var role = (el.getAttribute && el.getAttribute('role')) || '';
|
|
1240
|
+
if (role === 'tab' || /(^|[-_])tab(s|list)?([-_]|$)/.test(cls)) return 'tab';
|
|
1241
|
+
if (role === 'menuitem' || role === 'menu' || /menu/.test(cls)) return 'menu';
|
|
1242
|
+
if (/sort/.test(cls)) return 'sort';
|
|
1243
|
+
if (/filter/.test(cls)) return 'filter';
|
|
1244
|
+
if (role === 'navigation' || /nav([-_]|$)/.test(cls)) return 'navigation';
|
|
1245
|
+
// Common compound patterns
|
|
1246
|
+
if (/search-filter|search-select|filter-up|filter-select|filter-checkbox/.test(cls)) return 'filter';
|
|
1247
|
+
if (/sort-bar|sort-container|sort-trigger/.test(cls)) return 'sort';
|
|
1248
|
+
if (/checkbox-group|radio-group/.test(cls)) return 'filter';
|
|
1249
|
+
if (/sidebar/.test(cls)) return 'navigation';
|
|
1250
|
+
} catch(e) {}
|
|
1251
|
+
return 'unknown';
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/** Build a DiscoveredTrigger from an element. */
|
|
1255
|
+
function buildTrigger(el) {
|
|
1256
|
+
var text = (el.textContent || '').trim();
|
|
1257
|
+
var trigger = {
|
|
1258
|
+
selector: uniqueSelector(el),
|
|
1259
|
+
text: text.substring(0, 40),
|
|
1260
|
+
role: el.getAttribute ? (el.getAttribute('role') || undefined) : undefined,
|
|
1261
|
+
category: categorizeElement(el),
|
|
1262
|
+
hasPopup: false,
|
|
1263
|
+
userInteracted: false,
|
|
1264
|
+
explored: false,
|
|
1265
|
+
};
|
|
1266
|
+
return trigger;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/** All DiscoveredFilters found so far. Keyed by containerSelector for dedup. */
|
|
1270
|
+
var __xb_discovered_filters = {};
|
|
1271
|
+
/** True if a 'discovered_filters' push is pending (debounced). */
|
|
1272
|
+
var __xb_scan_scheduled = false;
|
|
1273
|
+
/** Track which DOM nodes we've already scanned (by identity, not selector). */
|
|
1274
|
+
var __xb_scanned_nodes = (typeof WeakSet !== 'undefined') ? new WeakSet() : null;
|
|
1275
|
+
|
|
1276
|
+
/** Scan the page for filter/sort/tab/menu containers + their triggers.
|
|
1277
|
+
* Dedup by DOM node identity (WeakSet) so the same element reached via
|
|
1278
|
+
* different selectors is recorded once. Nested containers are skipped \u2014
|
|
1279
|
+
* the outermost container wins. */
|
|
1280
|
+
function __xb_scanFilters() {
|
|
1281
|
+
try {
|
|
1282
|
+
// First pass: collect all candidate container nodes (dedup by identity)
|
|
1283
|
+
var candidates = [];
|
|
1284
|
+
var seenNodes = new Set(); // fallback if WeakSet unavailable
|
|
1285
|
+
for (var ci = 0; ci < FILTER_CONTAINER_SELECTORS.length; ci++) {
|
|
1286
|
+
var containers;
|
|
1287
|
+
try { containers = document.querySelectorAll(FILTER_CONTAINER_SELECTORS[ci]); } catch(e) { continue; }
|
|
1288
|
+
for (var cj = 0; cj < containers.length; cj++) {
|
|
1289
|
+
var c = containers[cj];
|
|
1290
|
+
// Dedup by identity
|
|
1291
|
+
if (__xb_scanned_nodes) {
|
|
1292
|
+
if (__xb_scanned_nodes.has(c)) continue;
|
|
1293
|
+
} else {
|
|
1294
|
+
if (seenNodes.has(c)) continue;
|
|
1295
|
+
seenNodes.add(c);
|
|
1296
|
+
}
|
|
1297
|
+
// Skip non-visible containers
|
|
1298
|
+
if (!c.offsetParent && c.tagName.toLowerCase() !== 'body') continue;
|
|
1299
|
+
// Skip if an ancestor is already a candidate (avoid nested dup)
|
|
1300
|
+
var isNested = false;
|
|
1301
|
+
for (var ck = 0; ck < candidates.length; ck++) {
|
|
1302
|
+
if (candidates[ck].contains(c)) { isNested = true; break; }
|
|
1303
|
+
}
|
|
1304
|
+
if (isNested) continue;
|
|
1305
|
+
// If this candidate contains existing ones, remove them
|
|
1306
|
+
candidates = candidates.filter(function(p) { return !c.contains(p); });
|
|
1307
|
+
candidates.push(c);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Second pass: for each candidate, mark as scanned + extract triggers
|
|
1312
|
+
for (var ck2 = 0; ck2 < candidates.length; ck2++) {
|
|
1313
|
+
var c2 = candidates[ck2];
|
|
1314
|
+
if (__xb_scanned_nodes) __xb_scanned_nodes.add(c2);
|
|
1315
|
+
|
|
1316
|
+
var containerSel = uniqueSelector(c2);
|
|
1317
|
+
// If we already have this in discovered map (by selector), skip
|
|
1318
|
+
if (__xb_discovered_filters[containerSel]) continue;
|
|
1319
|
+
|
|
1320
|
+
// Extract triggers: prefer leaf-ish elements with short text
|
|
1321
|
+
var triggers = [];
|
|
1322
|
+
var seenTrigSels = {};
|
|
1323
|
+
// Use a single querySelectorAll with comma-joined selectors for speed
|
|
1324
|
+
var allCandidates = c2.querySelectorAll(TRIGGER_SELECTORS.join(', '));
|
|
1325
|
+
for (var ti = 0; ti < allCandidates.length; ti++) {
|
|
1326
|
+
var el = allCandidates[ti];
|
|
1327
|
+
if (!el.offsetParent) continue;
|
|
1328
|
+
// Skip elements whose nearest ancestor in the container is also a
|
|
1329
|
+
// candidate (would create nested triggers).
|
|
1330
|
+
var skip = false;
|
|
1331
|
+
var p = el.parentElement;
|
|
1332
|
+
while (p && p !== c2) {
|
|
1333
|
+
if (seenTrigSels[uniqueSelector(p)]) { skip = true; break; }
|
|
1334
|
+
p = p.parentElement;
|
|
1335
|
+
}
|
|
1336
|
+
if (skip) continue;
|
|
1337
|
+
var txt = (el.textContent || '').trim();
|
|
1338
|
+
if (!txt || txt.length === 0 || txt.length > 40) continue;
|
|
1339
|
+
// Skip if this looks like a full item card (long combined text)
|
|
1340
|
+
if (el.children.length > 3) continue;
|
|
1341
|
+
var sel = uniqueSelector(el);
|
|
1342
|
+
if (seenTrigSels[sel]) continue;
|
|
1343
|
+
seenTrigSels[sel] = true;
|
|
1344
|
+
triggers.push(buildTrigger(el));
|
|
1345
|
+
if (triggers.length >= 30) break;
|
|
1346
|
+
}
|
|
1347
|
+
if (triggers.length === 0) continue;
|
|
1348
|
+
|
|
1349
|
+
__xb_discovered_filters[containerSel] = {
|
|
1350
|
+
containerSelector: containerSel,
|
|
1351
|
+
category: categorizeElement(c2),
|
|
1352
|
+
containerText: (c2.textContent || '').trim().substring(0, 60),
|
|
1353
|
+
triggers: triggers,
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
// Third pass: dedup containers whose trigger set is a subset of another.
|
|
1357
|
+
// This handles the case where two overlapping containers (e.g. outer
|
|
1358
|
+
// ".filter-up-container" and inner ".filter-select-container") share
|
|
1359
|
+
// triggers \u2014 keep the larger one, drop the redundant subset.
|
|
1360
|
+
var newFilters = [];
|
|
1361
|
+
var addedKeys = [];
|
|
1362
|
+
for (var key in __xb_discovered_filters) {
|
|
1363
|
+
var f = __xb_discovered_filters[key];
|
|
1364
|
+
var isSubset = false;
|
|
1365
|
+
for (var otherKey in __xb_discovered_filters) {
|
|
1366
|
+
if (otherKey === key) continue;
|
|
1367
|
+
var other = __xb_discovered_filters[otherKey];
|
|
1368
|
+
if (f.triggers.length > other.triggers.length) continue;
|
|
1369
|
+
// Check if every trigger in f is also in other (by selector)
|
|
1370
|
+
var allInOther = true;
|
|
1371
|
+
for (var ti2 = 0; ti2 < f.triggers.length; ti2++) {
|
|
1372
|
+
var found = false;
|
|
1373
|
+
for (var to2 = 0; to2 < other.triggers.length; to2++) {
|
|
1374
|
+
if (other.triggers[to2].selector === f.triggers[ti2].selector) { found = true; break; }
|
|
1375
|
+
}
|
|
1376
|
+
if (!found) { allInOther = false; break; }
|
|
1377
|
+
}
|
|
1378
|
+
if (allInOther && f.triggers.length < other.triggers.length) {
|
|
1379
|
+
isSubset = true;
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
// Same size + same triggers \u2014 keep the one with a more "container-y" selector
|
|
1383
|
+
if (allInOther && f.triggers.length === other.triggers.length && key > otherKey) {
|
|
1384
|
+
isSubset = true;
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (!isSubset) {
|
|
1389
|
+
newFilters.push(f);
|
|
1390
|
+
addedKeys.push(key);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
// Rebuild the discovered map with deduped entries
|
|
1394
|
+
__xb_discovered_filters = {};
|
|
1395
|
+
for (var ni = 0; ni < newFilters.length; ni++) {
|
|
1396
|
+
__xb_discovered_filters[addedKeys[ni]] = newFilters[ni];
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// Push a discovered_filters action so the server picks it up.
|
|
1400
|
+
__xb_pushDiscovered();
|
|
1401
|
+
} catch(e) { /* scan failed, don't crash recorder */ }
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/** Push current discovered_filters snapshot to pending actions (debounced). */
|
|
1405
|
+
function __xb_pushDiscovered() {
|
|
1406
|
+
if (__xb_scan_scheduled) return;
|
|
1407
|
+
__xb_scan_scheduled = true;
|
|
1408
|
+
setTimeout(function() {
|
|
1409
|
+
__xb_scan_scheduled = false;
|
|
1410
|
+
var list = Object.keys(__xb_discovered_filters).map(function(k) {
|
|
1411
|
+
return __xb_discovered_filters[k];
|
|
1412
|
+
});
|
|
1413
|
+
window.__xb_pending_actions.push({
|
|
1414
|
+
type: 'discovered_filters',
|
|
1415
|
+
ts: Date.now(),
|
|
1416
|
+
url: location.href,
|
|
1417
|
+
title: document.title,
|
|
1418
|
+
discoveredFilters: list,
|
|
1419
|
+
});
|
|
1420
|
+
}, 200);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/** Mark a trigger as user-interacted (called from click/hover handlers). */
|
|
1424
|
+
function __xb_markTriggerInteracted(target) {
|
|
1425
|
+
try {
|
|
1426
|
+
var sel = uniqueSelector(target);
|
|
1427
|
+
for (var k in __xb_discovered_filters) {
|
|
1428
|
+
var f = __xb_discovered_filters[k];
|
|
1429
|
+
for (var i = 0; i < f.triggers.length; i++) {
|
|
1430
|
+
if (f.triggers[i].selector === sel) {
|
|
1431
|
+
f.triggers[i].userInteracted = true;
|
|
1432
|
+
// Schedule a push so server gets the updated state
|
|
1433
|
+
__xb_pushDiscovered();
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
// Also accept ancestor match (target may be a child of the trigger)
|
|
1437
|
+
var triggerEl = document.querySelector(f.triggers[i].selector);
|
|
1438
|
+
if (triggerEl && triggerEl.contains(target)) {
|
|
1439
|
+
f.triggers[i].userInteracted = true;
|
|
1440
|
+
__xb_pushDiscovered();
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
} catch(e) {}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/** Expose scan + capture for server-side triggers (post-navigation, flush-time). */
|
|
1449
|
+
window.__xb_scanFilters = __xb_scanFilters;
|
|
1450
|
+
window.__xb_capture_discovered_filters = function() {
|
|
1451
|
+
return Object.keys(__xb_discovered_filters).map(function(k) {
|
|
1452
|
+
return __xb_discovered_filters[k];
|
|
1453
|
+
});
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
/** Find the nearest trigger element for a popup (looking up the DOM). */
|
|
1457
|
+
function findTriggerForPopup(popupEl) {
|
|
1458
|
+
try {
|
|
1459
|
+
// Walk up looking for known container types, then pick the trigger
|
|
1460
|
+
// nearest to the popup's top.
|
|
1461
|
+
var popupRect = popupEl.getBoundingClientRect();
|
|
1462
|
+
var ancestor = popupEl;
|
|
1463
|
+
for (var depth = 0; depth < 8 && ancestor; depth++) {
|
|
1464
|
+
ancestor = ancestor.parentElement;
|
|
1465
|
+
if (!ancestor) break;
|
|
1466
|
+
var triggersInAncestor = ancestor.querySelectorAll(
|
|
1467
|
+
'[role="tab"], [role="menuitem"], [role="option"], [role="button"], button, a, [class*="title"]'
|
|
1468
|
+
);
|
|
1469
|
+
// Pick the trigger whose x-center is closest to the popup's x-center
|
|
1470
|
+
// and is above the popup.
|
|
1471
|
+
var best = null, bestDist = Infinity;
|
|
1472
|
+
for (var i = 0; i < triggersInAncestor.length; i++) {
|
|
1473
|
+
var t = triggersInAncestor[i];
|
|
1474
|
+
if (!t.offsetParent) continue;
|
|
1475
|
+
var r = t.getBoundingClientRect();
|
|
1476
|
+
if (r.y + r.height > popupRect.y + 5) continue; // must be above popup
|
|
1477
|
+
var dist = Math.abs((r.x + r.width / 2) - (popupRect.x + popupRect.width / 2));
|
|
1478
|
+
if (dist < bestDist) {
|
|
1479
|
+
bestDist = dist;
|
|
1480
|
+
best = t;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (best) {
|
|
1484
|
+
// Mark the trigger as hasPopup in our discovered map
|
|
1485
|
+
var bestSel = uniqueSelector(best);
|
|
1486
|
+
for (var k in __xb_discovered_filters) {
|
|
1487
|
+
var f = __xb_discovered_filters[k];
|
|
1488
|
+
for (var j = 0; j < f.triggers.length; j++) {
|
|
1489
|
+
if (f.triggers[j].selector === bestSel) {
|
|
1490
|
+
f.triggers[j].hasPopup = true;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return {
|
|
1495
|
+
selector: bestSel,
|
|
1496
|
+
text: (best.textContent || '').trim().substring(0, 40),
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
} catch(e) {}
|
|
1501
|
+
return undefined;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/** Extract popup items (reuses the same item-extraction logic as captureVisibleContext). */
|
|
1505
|
+
function extractPopupItems(popupEl) {
|
|
1506
|
+
var items = [];
|
|
1507
|
+
try {
|
|
1508
|
+
var children = popupEl.querySelectorAll(
|
|
1509
|
+
'a,button,[role="menuitem"],[role="option"],[role="treeitem"],li,div[class*="item"],span[class*="item"]'
|
|
1510
|
+
);
|
|
1511
|
+
for (var k = 0; k < Math.min(children.length, 20); k++) {
|
|
1512
|
+
var child = children[k];
|
|
1513
|
+
var childText = (child.textContent || '').trim();
|
|
1514
|
+
if (!childText || childText.length > 60) continue;
|
|
1515
|
+
var info = { text: childText.substring(0, 60) };
|
|
1516
|
+
if (child.disabled) info.disabled = true;
|
|
1517
|
+
try { info.selector = uniqueSelector(child); } catch(e) {}
|
|
1518
|
+
items.push(info);
|
|
1519
|
+
}
|
|
1520
|
+
} catch(e) {}
|
|
1521
|
+
return items;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/** Check whether an element is currently visible (has layout box). */
|
|
1525
|
+
function isVisibleEl(el) {
|
|
1526
|
+
try {
|
|
1527
|
+
var r = el.getBoundingClientRect();
|
|
1528
|
+
if (r.width === 0 || r.height === 0) return false;
|
|
1529
|
+
var cs = window.getComputedStyle(el);
|
|
1530
|
+
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
|
|
1531
|
+
return true;
|
|
1532
|
+
} catch(e) { return false; }
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/** Push a 'popup_appear' sensing action. */
|
|
1536
|
+
function __xb_pushPopupAppear(popupEl, cause) {
|
|
1537
|
+
try {
|
|
1538
|
+
if (!popupEl || !isVisibleEl(popupEl)) return;
|
|
1539
|
+
var r = popupEl.getBoundingClientRect();
|
|
1540
|
+
// Heuristic: a real popup/menu should be relatively small (not a full
|
|
1541
|
+
// page banner) and have menu-like children. Page banners / hero sections
|
|
1542
|
+
// often match [class*="popup"] / [class*="modal"] but aren't real menus.
|
|
1543
|
+
var MAX_POPUP_AREA = 600 * 600; // 360,000 px\xB2
|
|
1544
|
+
if (r.width * r.height > MAX_POPUP_AREA) return;
|
|
1545
|
+
// Must have at least 1 child that looks like an item (link/button/li/div with class*="item")
|
|
1546
|
+
var itemChildren = popupEl.querySelectorAll(
|
|
1547
|
+
'a, button, [role="menuitem"], [role="option"], li, [class*="item"]'
|
|
1548
|
+
);
|
|
1549
|
+
if (itemChildren.length === 0) return;
|
|
1550
|
+
// Dedup against recent popup_appear actions (same selector within 3s)
|
|
1551
|
+
var popupSel = uniqueSelector(popupEl);
|
|
1552
|
+
var now = Date.now();
|
|
1553
|
+
var recent = window.__xb_pending_actions.filter(function(a) {
|
|
1554
|
+
return a.type === 'popup_appear' && (now - a.ts) < 3000;
|
|
1555
|
+
});
|
|
1556
|
+
for (var i = 0; i < recent.length; i++) {
|
|
1557
|
+
if (recent[i].popupAppear && recent[i].popupAppear.popup.selector === popupSel) return;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
var triggerInfo = findTriggerForPopup(popupEl);
|
|
1561
|
+
window.__xb_pending_actions.push({
|
|
1562
|
+
type: 'popup_appear',
|
|
1563
|
+
ts: now,
|
|
1564
|
+
url: location.href,
|
|
1565
|
+
title: document.title,
|
|
1566
|
+
popupAppear: {
|
|
1567
|
+
trigger: triggerInfo,
|
|
1568
|
+
popup: {
|
|
1569
|
+
selector: popupSel,
|
|
1570
|
+
text: (popupEl.textContent || '').trim().substring(0, 100),
|
|
1571
|
+
rect: {
|
|
1572
|
+
x: Math.round(r.x), y: Math.round(r.y),
|
|
1573
|
+
w: Math.round(r.width), h: Math.round(r.height),
|
|
1574
|
+
},
|
|
1575
|
+
items: extractPopupItems(popupEl),
|
|
1576
|
+
},
|
|
1577
|
+
cause: cause || 'auto',
|
|
1578
|
+
userTriggered: cause === 'user-hover' || cause === 'user-click',
|
|
1579
|
+
},
|
|
1580
|
+
});
|
|
1581
|
+
// Also schedule a discovered_filters push (hasPopup flags may have updated)
|
|
1582
|
+
__xb_pushDiscovered();
|
|
1583
|
+
} catch(e) {}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Expose for the hover handler to call when it observes a popup (avoid double-record)
|
|
1587
|
+
window.__xb_pushPopupAppear = __xb_pushPopupAppear;
|
|
1588
|
+
|
|
1589
|
+
// \u2500\u2500 Layer 2: long-lived MutationObserver for popover visible transitions \u2500\u2500
|
|
1590
|
+
var __xb_long_observer = null;
|
|
1591
|
+
var __xb_observer_debounce = null;
|
|
1592
|
+
// Set of selectors we've already recorded as popups (avoid re-pushing)
|
|
1593
|
+
var __xb_seen_popups = {};
|
|
1594
|
+
|
|
1595
|
+
function __xb_startLongObserver() {
|
|
1596
|
+
if (!('MutationObserver' in window)) return;
|
|
1597
|
+
if (__xb_long_observer) __xb_long_observer.disconnect();
|
|
1598
|
+
|
|
1599
|
+
__xb_long_observer = new MutationObserver(function(mutations) {
|
|
1600
|
+
// Debounce \u2014 collect then process once
|
|
1601
|
+
if (__xb_observer_debounce) clearTimeout(__xb_observer_debounce);
|
|
1602
|
+
__xb_observer_debounce = setTimeout(function() {
|
|
1603
|
+
__xb_observer_debounce = null;
|
|
1604
|
+
// Scan all known popover containers; any newly visible one \u2192 push
|
|
1605
|
+
for (var i = 0; i < POPOVER_SELECTORS.length; i++) {
|
|
1606
|
+
var els;
|
|
1607
|
+
try { els = document.querySelectorAll(POPOVER_SELECTORS[i]); } catch(e) { continue; }
|
|
1608
|
+
for (var j = 0; j < els.length; j++) {
|
|
1609
|
+
var el = els[j];
|
|
1610
|
+
var sel = uniqueSelector(el);
|
|
1611
|
+
if (__xb_seen_popups[sel]) continue;
|
|
1612
|
+
if (!isVisibleEl(el)) continue;
|
|
1613
|
+
__xb_seen_popups[sel] = Date.now();
|
|
1614
|
+
__xb_pushPopupAppear(el, 'auto');
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}, 100);
|
|
1618
|
+
});
|
|
1619
|
+
try {
|
|
1620
|
+
__xb_long_observer.observe(document.body, {
|
|
1621
|
+
childList: true,
|
|
1622
|
+
subtree: true,
|
|
1623
|
+
attributes: true,
|
|
1624
|
+
attributeFilter: ['class', 'style', 'hidden'],
|
|
1625
|
+
});
|
|
1626
|
+
} catch(e) {}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// Auto-cleanup seen_popups every 10s so transient popups can re-trigger
|
|
1630
|
+
setInterval(function() {
|
|
1631
|
+
var now = Date.now();
|
|
1632
|
+
for (var k in __xb_seen_popups) {
|
|
1633
|
+
if (now - __xb_seen_popups[k] > 10000) delete __xb_seen_popups[k];
|
|
1634
|
+
}
|
|
1635
|
+
}, 10000);
|
|
1636
|
+
|
|
1637
|
+
// \u2500\u2500 Layer 3: mousemove throttled diff (senses triggers near cursor) \u2500\u2500
|
|
1638
|
+
var __xb_last_scan_move = 0;
|
|
1639
|
+
var __xb_move_stop_timer = null;
|
|
1640
|
+
var __xb_last_pos = { x: 0, y: 0 };
|
|
1641
|
+
|
|
1642
|
+
document.addEventListener('mousemove', function(e) {
|
|
1643
|
+
__xb_last_pos = { x: e.clientX, y: e.clientY };
|
|
1644
|
+
var now = Date.now();
|
|
1645
|
+
if (now - __xb_last_scan_move < 300) return;
|
|
1646
|
+
__xb_last_scan_move = now;
|
|
1647
|
+
|
|
1648
|
+
// Reset the "stop" timer \u2014 when mouse stops moving for 800ms, we scan
|
|
1649
|
+
if (__xb_move_stop_timer) clearTimeout(__xb_move_stop_timer);
|
|
1650
|
+
__xb_move_stop_timer = setTimeout(function() {
|
|
1651
|
+
__xb_move_stop_timer = null;
|
|
1652
|
+
__xb_scanNearCursor(__xb_last_pos.x, __xb_last_pos.y, 'user-hover');
|
|
1653
|
+
}, 800);
|
|
1654
|
+
}, true);
|
|
1655
|
+
|
|
1656
|
+
/** Scan popups near the cursor; mark nearby triggers as explored. */
|
|
1657
|
+
function __xb_scanNearCursor(cx, cy, cause) {
|
|
1658
|
+
try {
|
|
1659
|
+
// 1. Check for any visible popover near cursor
|
|
1660
|
+
for (var i = 0; i < POPOVER_SELECTORS.length; i++) {
|
|
1661
|
+
var els;
|
|
1662
|
+
try { els = document.querySelectorAll(POPOVER_SELECTORS[i]); } catch(e) { continue; }
|
|
1663
|
+
for (var j = 0; j < els.length; j++) {
|
|
1664
|
+
var el = els[j];
|
|
1665
|
+
if (!isVisibleEl(el)) continue;
|
|
1666
|
+
var r = el.getBoundingClientRect();
|
|
1667
|
+
// Near = within 400px of cursor
|
|
1668
|
+
if (Math.abs((r.x + r.width / 2) - cx) > 400) continue;
|
|
1669
|
+
if (Math.abs((r.y + r.height / 2) - cy) > 400) continue;
|
|
1670
|
+
var sel = uniqueSelector(el);
|
|
1671
|
+
var now = Date.now();
|
|
1672
|
+
if (__xb_seen_popups[sel] && now - __xb_seen_popups[sel] < 2000) continue;
|
|
1673
|
+
__xb_seen_popups[sel] = now;
|
|
1674
|
+
__xb_pushPopupAppear(el, cause);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// 2. Mark nearby triggers as explored (mouse lingered without click)
|
|
1679
|
+
var near = document.elementsFromPoint(cx, cy);
|
|
1680
|
+
for (var k = 0; k < near.length && k < 5; k++) {
|
|
1681
|
+
var t = near[k];
|
|
1682
|
+
var tSel = uniqueSelector(t);
|
|
1683
|
+
for (var fk in __xb_discovered_filters) {
|
|
1684
|
+
var f = __xb_discovered_filters[fk];
|
|
1685
|
+
for (var ti = 0; ti < f.triggers.length; ti++) {
|
|
1686
|
+
if (f.triggers[ti].selector === tSel) {
|
|
1687
|
+
f.triggers[ti].explored = true;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
__xb_pushDiscovered();
|
|
1693
|
+
} catch(e) {}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// \u2500\u2500 Trigger initial baseline scan (after DOM is ready) \u2500\u2500
|
|
1697
|
+
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
|
1698
|
+
setTimeout(__xb_scanFilters, 500);
|
|
1699
|
+
setTimeout(__xb_startLongObserver, 800);
|
|
1700
|
+
} else {
|
|
1701
|
+
window.addEventListener('DOMContentLoaded', function() {
|
|
1702
|
+
setTimeout(__xb_scanFilters, 500);
|
|
1703
|
+
setTimeout(__xb_startLongObserver, 800);
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1010
1706
|
})();
|
|
1011
1707
|
`;
|
|
1012
1708
|
var CHECKPOINT_OVERLAY_SCRIPT = `
|
|
@@ -1254,6 +1950,9 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1254
1950
|
network = [];
|
|
1255
1951
|
contextChanges = [];
|
|
1256
1952
|
checkpoints = [];
|
|
1953
|
+
/** Proactive baseline scan of filter/sort/tab/menu regions. Keyed by
|
|
1954
|
+
* containerSelector so re-scans after navigation replace rather than dup. */
|
|
1955
|
+
discoveredFilters = /* @__PURE__ */ new Map();
|
|
1257
1956
|
actionCounter = 0;
|
|
1258
1957
|
networkCounter = 0;
|
|
1259
1958
|
contextCounter = 0;
|
|
@@ -1274,6 +1973,9 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1274
1973
|
_lastRequestTs = 0;
|
|
1275
1974
|
_isRecording = false;
|
|
1276
1975
|
streamMode = "clean";
|
|
1976
|
+
/** Snapshot throttle: prevents too many screenshots during rapid inputs.
|
|
1977
|
+
* Keyed by 'type|elementSelector', value = last snapshot timestamp. */
|
|
1978
|
+
snapshotThrottle = /* @__PURE__ */ new Map();
|
|
1277
1979
|
/** Ambient action types: filtered out in clean mode */
|
|
1278
1980
|
static AMBIENT_TYPES = /* @__PURE__ */ new Set([
|
|
1279
1981
|
"hover",
|
|
@@ -1323,6 +2025,8 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1323
2025
|
type: action.type,
|
|
1324
2026
|
element: actionToPush.element
|
|
1325
2027
|
});
|
|
2028
|
+
await this.captureAutoSnapshot(this.page, actionToPush, "after").catch(() => {
|
|
2029
|
+
});
|
|
1326
2030
|
this.lastActionTs = ts;
|
|
1327
2031
|
this.dedupMap.set(dedupFromAction, Date.now() + 2e3);
|
|
1328
2032
|
this.dedupActionCount++;
|
|
@@ -1369,6 +2073,10 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1369
2073
|
get recordingsDir() {
|
|
1370
2074
|
return _SessionRecorder.getRecordingsDir(this.sessionName);
|
|
1371
2075
|
}
|
|
2076
|
+
/** Directory for auto-captured snapshots (PNG screenshots, stored in .tmp for easy cleanup). */
|
|
2077
|
+
get snapshotsDir() {
|
|
2078
|
+
return join(this.recordingsDir, ".tmp", "snapshots");
|
|
2079
|
+
}
|
|
1372
2080
|
static getRecordingsDir(sessionName) {
|
|
1373
2081
|
return join(homedir(), ".xbrowser", "sessions", sessionName, "recordings");
|
|
1374
2082
|
}
|
|
@@ -1560,9 +2268,18 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1560
2268
|
}
|
|
1561
2269
|
}
|
|
1562
2270
|
// ==================== Private ====================
|
|
2271
|
+
/** Whether the initial action-signal script injection succeeded. Check this
|
|
2272
|
+
* after start() returns — if false, user actions will NOT be recorded and
|
|
2273
|
+
* the caller should surface a clear error to the user instead of silently
|
|
2274
|
+
* pretending recording is working. */
|
|
2275
|
+
_injectionFailed = false;
|
|
2276
|
+
get injectionFailed() {
|
|
2277
|
+
return this._injectionFailed;
|
|
2278
|
+
}
|
|
1563
2279
|
async injectActionScript(page) {
|
|
1564
2280
|
let success = false;
|
|
1565
2281
|
const maxAttempts = 3;
|
|
2282
|
+
let lastError = "";
|
|
1566
2283
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1567
2284
|
try {
|
|
1568
2285
|
await page.evaluate(getSelectorGeneratorScript());
|
|
@@ -1575,17 +2292,20 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1575
2292
|
success = true;
|
|
1576
2293
|
break;
|
|
1577
2294
|
}
|
|
2295
|
+
lastError = "verification failed (script did not set __xb_action_signal flag)";
|
|
1578
2296
|
console.error(`[recorder] Action signal injection verification failed (attempt ${attempt}/${maxAttempts})`);
|
|
1579
2297
|
} catch (e) {
|
|
1580
|
-
const
|
|
1581
|
-
|
|
2298
|
+
const err = e instanceof Error ? e : new Error(typeof e === "string" ? e : "unknown error");
|
|
2299
|
+
lastError = err.message;
|
|
2300
|
+
console.error(`[recorder] Action signal injection error (attempt ${attempt}/${maxAttempts}): ${err.message}`);
|
|
1582
2301
|
}
|
|
1583
2302
|
if (attempt < maxAttempts) {
|
|
1584
2303
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
1585
2304
|
}
|
|
1586
2305
|
}
|
|
1587
2306
|
if (!success) {
|
|
1588
|
-
|
|
2307
|
+
this._injectionFailed = true;
|
|
2308
|
+
console.error(`[recorder] WARNING: Action signal injection FAILED after ${maxAttempts} attempts. User actions will NOT be recorded. Last error: ${lastError}`);
|
|
1589
2309
|
}
|
|
1590
2310
|
try {
|
|
1591
2311
|
await page.evaluate(CHECKPOINT_OVERLAY_SCRIPT);
|
|
@@ -1593,6 +2313,16 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1593
2313
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1594
2314
|
console.error(`[recorder] Checkpoint overlay injection failed (non-fatal): ${msg}`);
|
|
1595
2315
|
}
|
|
2316
|
+
if (success) {
|
|
2317
|
+
try {
|
|
2318
|
+
await page.evaluate(`(function() {
|
|
2319
|
+
if (typeof window.__xb_scanFilters === 'function') {
|
|
2320
|
+
setTimeout(window.__xb_scanFilters, 100);
|
|
2321
|
+
}
|
|
2322
|
+
})()`);
|
|
2323
|
+
} catch {
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
1596
2326
|
try {
|
|
1597
2327
|
await page.evaluate(`
|
|
1598
2328
|
(function() {
|
|
@@ -1744,9 +2474,9 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1744
2474
|
return;
|
|
1745
2475
|
}
|
|
1746
2476
|
} catch (e) {
|
|
1747
|
-
const
|
|
2477
|
+
const err = e instanceof Error ? e : new Error(typeof e === "string" ? e : "unknown error");
|
|
1748
2478
|
if (attempt < 4) {
|
|
1749
|
-
console.error(`[recorder] injectActionScript retry ${attempt + 1}/5 failed: ${
|
|
2479
|
+
console.error(`[recorder] injectActionScript retry ${attempt + 1}/5 failed: ${err.message}`);
|
|
1750
2480
|
}
|
|
1751
2481
|
}
|
|
1752
2482
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
@@ -1768,8 +2498,8 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1768
2498
|
try {
|
|
1769
2499
|
await this.injectActionScript(page);
|
|
1770
2500
|
} catch (e) {
|
|
1771
|
-
const
|
|
1772
|
-
console.error(`[recorder] injectActionScript after navigation failed: ${
|
|
2501
|
+
const err = e instanceof Error ? e : new Error(typeof e === "string" ? e : "unknown error");
|
|
2502
|
+
console.error(`[recorder] injectActionScript after navigation failed: ${err.message}`);
|
|
1773
2503
|
}
|
|
1774
2504
|
}
|
|
1775
2505
|
});
|
|
@@ -1890,6 +2620,14 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1890
2620
|
try {
|
|
1891
2621
|
pending = await page.evaluate(`(function() {
|
|
1892
2622
|
var w = window;
|
|
2623
|
+
// Flush-time sweep: if there is a hover action still being enriched
|
|
2624
|
+
// by async samplers, force one last capture so its hoverContext lands
|
|
2625
|
+
// before the action is drained from the pending queue.
|
|
2626
|
+
try {
|
|
2627
|
+
if (typeof w.__xb_capture_hover_popups === 'function') {
|
|
2628
|
+
w.__xb_capture_hover_popups();
|
|
2629
|
+
}
|
|
2630
|
+
} catch(e) {}
|
|
1893
2631
|
var actions = w.__xb_pending_actions || [];
|
|
1894
2632
|
w.__xb_pending_actions = [];
|
|
1895
2633
|
|
|
@@ -1954,11 +2692,58 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1954
2692
|
});
|
|
1955
2693
|
}
|
|
1956
2694
|
this.lastKnownUrl = currentUrl;
|
|
2695
|
+
try {
|
|
2696
|
+
page.evaluate(`(function() {
|
|
2697
|
+
if (typeof window.__xb_scanFilters === 'function') {
|
|
2698
|
+
setTimeout(window.__xb_scanFilters, 500);
|
|
2699
|
+
}
|
|
2700
|
+
})()`).catch(() => {
|
|
2701
|
+
});
|
|
2702
|
+
} catch {
|
|
2703
|
+
}
|
|
1957
2704
|
}
|
|
1958
2705
|
} catch {
|
|
1959
2706
|
}
|
|
1960
2707
|
for (const raw of pending) {
|
|
1961
2708
|
if (raw.ts <= this.lastActionTs) continue;
|
|
2709
|
+
if (raw.type === "discovered_filters") {
|
|
2710
|
+
if (Array.isArray(raw.discoveredFilters)) {
|
|
2711
|
+
for (const f of raw.discoveredFilters) {
|
|
2712
|
+
if (f && f.containerSelector) {
|
|
2713
|
+
const existing = this.discoveredFilters.get(f.containerSelector);
|
|
2714
|
+
if (existing) {
|
|
2715
|
+
for (const newT of f.triggers) {
|
|
2716
|
+
const oldT = existing.triggers.find((t) => t.selector === newT.selector);
|
|
2717
|
+
if (oldT) {
|
|
2718
|
+
newT.userInteracted = newT.userInteracted || oldT.userInteracted;
|
|
2719
|
+
newT.explored = newT.explored || oldT.explored;
|
|
2720
|
+
newT.hasPopup = newT.hasPopup || oldT.hasPopup;
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
this.discoveredFilters.set(f.containerSelector, f);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
continue;
|
|
2729
|
+
}
|
|
2730
|
+
if (raw.type === "popup_appear") {
|
|
2731
|
+
if (!raw.popupAppear?.userTriggered) continue;
|
|
2732
|
+
this.actionCounter++;
|
|
2733
|
+
const pushed2 = {
|
|
2734
|
+
id: this.actionCounter,
|
|
2735
|
+
type: "popup_appear",
|
|
2736
|
+
timestamp: raw.ts,
|
|
2737
|
+
url: raw.url || page.url(),
|
|
2738
|
+
pageTitle: raw.title || "",
|
|
2739
|
+
popupAppear: raw.popupAppear
|
|
2740
|
+
};
|
|
2741
|
+
this.actions.push(pushed2);
|
|
2742
|
+
await this.captureAutoSnapshot(page, pushed2, "after").catch(() => {
|
|
2743
|
+
});
|
|
2744
|
+
this.lastActionTs = raw.ts;
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
1962
2747
|
const dedupFromSignal = this.dedupKey(raw.type, raw.element?.selector, raw.element?.tag, raw.value);
|
|
1963
2748
|
if (dedupFromSignal) {
|
|
1964
2749
|
const expires = this.dedupMap.get(dedupFromSignal);
|
|
@@ -1967,7 +2752,19 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1967
2752
|
}
|
|
1968
2753
|
}
|
|
1969
2754
|
if (this.streamMode === "clean" && _SessionRecorder.AMBIENT_TYPES.has(raw.type)) {
|
|
1970
|
-
|
|
2755
|
+
if (raw.type === "hover") {
|
|
2756
|
+
const hc = raw.hoverContext;
|
|
2757
|
+
if (!hc || hc.appeared.length === 0) continue;
|
|
2758
|
+
const hasSubstantialPopup = hc.appeared.some((p) => p.items && p.items.length > 0);
|
|
2759
|
+
if (!hasSubstantialPopup) continue;
|
|
2760
|
+
} else if (raw.type === "scroll") {
|
|
2761
|
+
const dx = raw.scrollX || 0;
|
|
2762
|
+
const dy = raw.scrollY || 0;
|
|
2763
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
2764
|
+
if (dist < 50) continue;
|
|
2765
|
+
} else {
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
1971
2768
|
}
|
|
1972
2769
|
this.actionCounter++;
|
|
1973
2770
|
let clickContext;
|
|
@@ -1988,10 +2785,17 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1988
2785
|
scrollX: raw.scrollX,
|
|
1989
2786
|
scrollY: raw.scrollY,
|
|
1990
2787
|
clickContext,
|
|
2788
|
+
// Browser-side hover enrichment writes hoverContext onto the pending
|
|
2789
|
+
// action asynchronously (200/500/1000ms after mouseover). Flush may
|
|
2790
|
+
// run before the last sample lands — passthrough keeps whatever has
|
|
2791
|
+
// been collected so far.
|
|
2792
|
+
hoverContext: raw.hoverContext,
|
|
1991
2793
|
files: raw.files
|
|
1992
2794
|
});
|
|
1993
2795
|
const pushed = this.actions[this.actions.length - 1];
|
|
1994
2796
|
pushed.elementScreenshot = await this.captureElementScreenshot(page, raw);
|
|
2797
|
+
await this.captureAutoSnapshot(page, pushed, "after").catch(() => {
|
|
2798
|
+
});
|
|
1995
2799
|
this.lastActionTs = raw.ts;
|
|
1996
2800
|
const dedupKey = this.dedupKey(raw.type, raw.element?.selector, raw.element?.tag, raw.value);
|
|
1997
2801
|
if (dedupKey) {
|
|
@@ -2055,6 +2859,85 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2055
2859
|
return;
|
|
2056
2860
|
}
|
|
2057
2861
|
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Action types that trigger auto-snapshot capture.
|
|
2864
|
+
* Hover is excluded — it's too noisy and rarely changes the page visually.
|
|
2865
|
+
* Popup_appear captures the popup state (after only).
|
|
2866
|
+
*/
|
|
2867
|
+
static SNAPSHOT_TYPES = /* @__PURE__ */ new Set([
|
|
2868
|
+
"click",
|
|
2869
|
+
"dblclick",
|
|
2870
|
+
"navigation",
|
|
2871
|
+
"goto",
|
|
2872
|
+
"submit",
|
|
2873
|
+
"cdp-click",
|
|
2874
|
+
"cdp-fill",
|
|
2875
|
+
"input",
|
|
2876
|
+
"change",
|
|
2877
|
+
"filechooser",
|
|
2878
|
+
"popup_appear"
|
|
2879
|
+
]);
|
|
2880
|
+
/**
|
|
2881
|
+
* Throttle key for snapshot capture to avoid too many during rapid inputs.
|
|
2882
|
+
* Returns true if this action should be throttled (skip snapshot).
|
|
2883
|
+
*/
|
|
2884
|
+
isSnapshotThrottled(type, selector) {
|
|
2885
|
+
if (type === "input" || type === "change" || type === "cdp-fill") {
|
|
2886
|
+
const key = `${type}|${selector || ""}`;
|
|
2887
|
+
const last = this.snapshotThrottle.get(key);
|
|
2888
|
+
const now = Date.now();
|
|
2889
|
+
if (last && now - last < 2e3) return true;
|
|
2890
|
+
this.snapshotThrottle.set(key, now);
|
|
2891
|
+
}
|
|
2892
|
+
return false;
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Capture a viewport screenshot + compact accessibility tree for a key action.
|
|
2896
|
+
* - PNG stored in .tmp/snapshots/ (temporary, easy to clean up)
|
|
2897
|
+
* - aria tree stored inline as compact text on the action (searchable via grep)
|
|
2898
|
+
*
|
|
2899
|
+
* Non-blocking (errors silently caught). Throttled for rapid inputs.
|
|
2900
|
+
*/
|
|
2901
|
+
async captureAutoSnapshot(page, action, suffix = "after") {
|
|
2902
|
+
if (!_SessionRecorder.SNAPSHOT_TYPES.has(action.type)) return;
|
|
2903
|
+
if (this.isSnapshotThrottled(action.type, action.element?.selector)) return;
|
|
2904
|
+
const snapDir = this.snapshotsDir;
|
|
2905
|
+
mkdirSync(snapDir, { recursive: true });
|
|
2906
|
+
const stepId = String(action.id).padStart(3, "0");
|
|
2907
|
+
const snapshot = {};
|
|
2908
|
+
try {
|
|
2909
|
+
const pngPath = join(snapDir, `step-${stepId}-${suffix}.png`);
|
|
2910
|
+
const buffer = await page.screenshot({ type: "png", timeout: 5e3 });
|
|
2911
|
+
writeFileSync(pngPath, buffer);
|
|
2912
|
+
snapshot.png = `.tmp/snapshots/step-${stepId}-${suffix}.png`;
|
|
2913
|
+
} catch {
|
|
2914
|
+
}
|
|
2915
|
+
try {
|
|
2916
|
+
snapshot.aria = await page.evaluate(`
|
|
2917
|
+
(function() {
|
|
2918
|
+
try {
|
|
2919
|
+
var parts = [];
|
|
2920
|
+
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false);
|
|
2921
|
+
var node;
|
|
2922
|
+
while (node = walker.nextNode()) {
|
|
2923
|
+
var tag = node.tagName.toLowerCase();
|
|
2924
|
+
var role = node.getAttribute('role') || '';
|
|
2925
|
+
var text = (node.textContent || '').trim().substring(0, 60);
|
|
2926
|
+
var ariaLabel = node.getAttribute('aria-label') || '';
|
|
2927
|
+
if (role || tag === 'button' || tag === 'a' || tag === 'input' || tag === 'select' || tag === 'textarea') {
|
|
2928
|
+
parts.push((role || tag) + ': ' + (ariaLabel || text).substring(0, 40));
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
return parts.slice(0, 100).join(' | ');
|
|
2932
|
+
} catch(e) { return ''; }
|
|
2933
|
+
})()
|
|
2934
|
+
`);
|
|
2935
|
+
} catch {
|
|
2936
|
+
}
|
|
2937
|
+
if (snapshot.png || snapshot.aria) {
|
|
2938
|
+
action.snapshots = snapshot;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2058
2941
|
/**
|
|
2059
2942
|
* After a click, wait 300ms then scan for popover/dropdown/menu elements
|
|
2060
2943
|
* near the click position. This runs server-side to avoid race conditions
|
|
@@ -2254,12 +3137,18 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2254
3137
|
JSON.stringify(data, null, 2),
|
|
2255
3138
|
"utf-8"
|
|
2256
3139
|
);
|
|
3140
|
+
const jsonlPath = join(this.recordingsDir, "recording.jsonl");
|
|
3141
|
+
const lines = data.actions.map((a) => JSON.stringify(a)).join("\n") + "\n";
|
|
3142
|
+
writeFileSync(jsonlPath, lines, "utf-8");
|
|
2257
3143
|
} catch {
|
|
2258
3144
|
}
|
|
2259
3145
|
}
|
|
2260
3146
|
writeFinalOutput(data, summary) {
|
|
2261
3147
|
mkdirSync(this.recordingsDir, { recursive: true });
|
|
2262
3148
|
writeFileSync(join(this.recordingsDir, "recording.json"), JSON.stringify(data, null, 2), "utf-8");
|
|
3149
|
+
const jsonlPath = join(this.recordingsDir, "recording.jsonl");
|
|
3150
|
+
const lines = data.actions.map((a) => JSON.stringify(a)).join("\n") + "\n";
|
|
3151
|
+
writeFileSync(jsonlPath, lines, "utf-8");
|
|
2263
3152
|
writeFileSync(join(this.recordingsDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
|
|
2264
3153
|
writeFileSync(join(this.recordingsDir, "summary.md"), this.buildMarkdownSummary(data, summary), "utf-8");
|
|
2265
3154
|
}
|
|
@@ -2271,7 +3160,8 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2271
3160
|
actions: [...this.actions],
|
|
2272
3161
|
network: [...this.network],
|
|
2273
3162
|
contextChanges: [...this.contextChanges],
|
|
2274
|
-
checkpoints: [...this.checkpoints]
|
|
3163
|
+
checkpoints: [...this.checkpoints],
|
|
3164
|
+
discoveredFilters: [...this.discoveredFilters.values()]
|
|
2275
3165
|
};
|
|
2276
3166
|
}
|
|
2277
3167
|
// ─── Summary builder with ref compression + input→network matching ──
|