objs-core 2.3.0 → 2.4.1

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/objs.built.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Objs-core library
3
- * @version 2.2
3
+ * @version 2.4.1
4
4
  * @author Roman Torshin
5
5
  * @license Apache-2.0
6
6
  */
@@ -712,6 +712,44 @@ const o = (query) => {
712
712
  });
713
713
  result.style(val || null);
714
714
  }, "css");
715
+ result.cssMerge = returner((styles = {}) => {
716
+ if (styles === null) {
717
+ result.style(null);
718
+ return;
719
+ }
720
+ typeVerify([[styles, objectType]]);
721
+ const normKey = (k) => k.indexOf("-") !== -1 ? k : o.camelToKebab(k);
722
+ const parseStyleAttr = (s) => {
723
+ const out = {};
724
+ if (!s || typeof s !== stringType) return out;
725
+ const parts = s.split(";");
726
+ for (let p = 0; p < parts.length; p++) {
727
+ const part = parts[p];
728
+ const idx = part.indexOf(":");
729
+ if (idx === -1) continue;
730
+ const key = part.slice(0, idx).trim();
731
+ const val = part.slice(idx + 1).trim();
732
+ if (key) out[key] = val;
733
+ }
734
+ return out;
735
+ };
736
+ iterator(() => {
737
+ const el = result.els[i];
738
+ const merged = parseStyleAttr(el.getAttribute("style"));
739
+ cycleObj(styles, (style) => {
740
+ const k = normKey(style);
741
+ const v = styles[style];
742
+ if (v === null || v === u) delete merged[k];
743
+ else merged[k] = String(v).replace('"', "'");
744
+ });
745
+ let serialized = "";
746
+ cycleObj(merged, (k) => {
747
+ serialized += k + ":" + merged[k] + ";";
748
+ });
749
+ if (serialized) el.setAttribute("style", serialized);
750
+ else el.removeAttribute("style");
751
+ });
752
+ }, "cssMerge");
715
753
  result.setClass = returner((cl) => {
716
754
  typeVerify([[cl, stringType]]);
717
755
  iterator(() => {
@@ -2248,6 +2286,7 @@ o.recorder = {
2248
2286
  initialData: {},
2249
2287
  assertions: [],
2250
2288
  observeRoot: null,
2289
+ strictCapture: null,
2251
2290
  _originalFetch: null,
2252
2291
  _listeners: [],
2253
2292
  _observer: null
@@ -2257,6 +2296,28 @@ o.startRecording = (observe, events, timeouts) => {
2257
2296
  if (o.recorder.active) {
2258
2297
  return;
2259
2298
  }
2299
+ let observeSel;
2300
+ let eventsOpt;
2301
+ let timeoutsOpt;
2302
+ let strictCapture = null;
2303
+ const isStartBag = observe != null && typeof observe === "object" && !Array.isArray(observe) && (o.C(observe, "observe") || o.C(observe, "events") || o.C(observe, "timeouts") || o.C(observe, "strictCaptureAssertions") || o.C(observe, "strictCaptureNetwork") || o.C(observe, "strictCaptureWebSocket"));
2304
+ if (isStartBag) {
2305
+ const bag = observe;
2306
+ observeSel = bag.observe != null ? String(bag.observe) : void 0;
2307
+ eventsOpt = bag.events;
2308
+ timeoutsOpt = bag.timeouts;
2309
+ if (o.C(bag, "strictCaptureAssertions") || o.C(bag, "strictCaptureNetwork") || o.C(bag, "strictCaptureWebSocket")) {
2310
+ strictCapture = {
2311
+ assertions: !!bag.strictCaptureAssertions,
2312
+ network: !!bag.strictCaptureNetwork,
2313
+ websocket: !!bag.strictCaptureWebSocket
2314
+ };
2315
+ }
2316
+ } else {
2317
+ observeSel = typeof observe === "string" ? observe : void 0;
2318
+ eventsOpt = events;
2319
+ timeoutsOpt = timeouts;
2320
+ }
2260
2321
  const defaultEvents = [
2261
2322
  "click",
2262
2323
  "mouseover",
@@ -2279,8 +2340,8 @@ o.startRecording = (observe, events, timeouts) => {
2279
2340
  focus: 50,
2280
2341
  blur: 50
2281
2342
  };
2282
- const listenEvents = events || defaultEvents;
2283
- const stepDelays = Object.assign({}, defaultStepDelays, timeouts || {});
2343
+ const listenEvents = eventsOpt || defaultEvents;
2344
+ const stepDelays = Object.assign({}, defaultStepDelays, timeoutsOpt || {});
2284
2345
  const captureDebounce = {
2285
2346
  scroll: 30,
2286
2347
  mouseover: 50,
@@ -2294,7 +2355,8 @@ o.startRecording = (observe, events, timeouts) => {
2294
2355
  rec.mocks = {};
2295
2356
  rec.stepDelays = stepDelays;
2296
2357
  rec.initialData = { url: window.location.href, timestamp: Date.now() };
2297
- rec.observeRoot = observe || null;
2358
+ rec.strictCapture = strictCapture;
2359
+ rec.observeRoot = observeSel || null;
2298
2360
  rec.assertions = [];
2299
2361
  rec.removedElements = [];
2300
2362
  o.inits.forEach((inst, idx) => {
@@ -2434,7 +2496,7 @@ o.startRecording = (observe, events, timeouts) => {
2434
2496
  }
2435
2497
  return sel;
2436
2498
  };
2437
- const observeTarget = observe && o.D.querySelector(observe) || o.D.body;
2499
+ const observeTarget = observeSel && o.D.querySelector(observeSel) || o.D.body;
2438
2500
  rec._observer = new MutationObserver((mutations) => {
2439
2501
  const actionIdx = rec.actions.length - 1;
2440
2502
  if (actionIdx < 0) return;
@@ -2455,22 +2517,28 @@ o.startRecording = (observe, events, timeouts) => {
2455
2517
  if (sel && observeTarget) {
2456
2518
  const matches = observeTarget.querySelectorAll(sel);
2457
2519
  if (matches.length > 1) {
2458
- let n = node;
2459
- while (n && n !== observeTarget && n.nodeType === 1) {
2460
- const qaAttr = o.autotag && n.dataset && n.dataset[o.autotag];
2461
- if (qaAttr) {
2462
- const itemSel = `[data-${o.autotag}="${qaAttr}"]`;
2463
- const itemMatches = observeTarget.querySelectorAll(itemSel);
2464
- if (itemMatches.length > 1) {
2465
- const idx = [...itemMatches].indexOf(n);
2466
- if (idx !== -1) {
2467
- listSelector = itemSel;
2468
- index = idx;
2469
- break;
2520
+ const idxAmong = [...matches].indexOf(node);
2521
+ if (idxAmong !== -1) {
2522
+ listSelector = sel;
2523
+ index = idxAmong;
2524
+ } else {
2525
+ let n = node;
2526
+ while (n && n !== observeTarget && n.nodeType === 1) {
2527
+ const qaAttr = o.autotag && n.dataset && n.dataset[o.autotag];
2528
+ if (qaAttr) {
2529
+ const itemSel = `[data-${o.autotag}="${qaAttr}"]`;
2530
+ const itemMatches = observeTarget.querySelectorAll(itemSel);
2531
+ if (itemMatches.length > 1) {
2532
+ const idx = [...itemMatches].indexOf(n);
2533
+ if (idx !== -1) {
2534
+ listSelector = itemSel;
2535
+ index = idx;
2536
+ break;
2537
+ }
2470
2538
  }
2471
2539
  }
2540
+ n = n.parentElement;
2472
2541
  }
2473
- n = n.parentElement;
2474
2542
  }
2475
2543
  }
2476
2544
  }
@@ -2608,7 +2676,7 @@ o.startRecording = (observe, events, timeouts) => {
2608
2676
  listenEvents.forEach((ev) => {
2609
2677
  const handler = (e) => {
2610
2678
  const target = e.target;
2611
- if (observe && observeTarget && target?.nodeType === 1 && !observeTarget.contains(target)) {
2679
+ if (observeSel && observeTarget && target?.nodeType === 1 && !observeTarget.contains(target)) {
2612
2680
  return;
2613
2681
  }
2614
2682
  let selector = "";
@@ -2627,22 +2695,28 @@ o.startRecording = (observe, events, timeouts) => {
2627
2695
  if (selector && observeTarget) {
2628
2696
  const matches = observeTarget.querySelectorAll(selector);
2629
2697
  if (matches.length > 1) {
2630
- let node = target;
2631
- while (node && node !== observeTarget && node.nodeType === 1) {
2632
- const qaAttr = o.autotag && node.dataset && node.dataset[o.autotag];
2633
- if (qaAttr) {
2634
- const itemSel = `[data-${o.autotag}="${qaAttr}"]`;
2635
- const itemMatches = observeTarget.querySelectorAll(itemSel);
2636
- if (itemMatches.length > 1) {
2637
- const idx = [...itemMatches].indexOf(node);
2638
- if (idx !== -1) {
2639
- listSelector = itemSel;
2640
- targetIndex = idx;
2641
- break;
2698
+ const idxAmongMatches = [...matches].indexOf(target);
2699
+ if (idxAmongMatches !== -1) {
2700
+ listSelector = selector;
2701
+ targetIndex = idxAmongMatches;
2702
+ } else {
2703
+ let node = target;
2704
+ while (node && node !== observeTarget && node.nodeType === 1) {
2705
+ const qaAttr = o.autotag && node.dataset && node.dataset[o.autotag];
2706
+ if (qaAttr) {
2707
+ const itemSel = `[data-${o.autotag}="${qaAttr}"]`;
2708
+ const itemMatches = observeTarget.querySelectorAll(itemSel);
2709
+ if (itemMatches.length > 1) {
2710
+ const idx = [...itemMatches].indexOf(node);
2711
+ if (idx !== -1) {
2712
+ listSelector = itemSel;
2713
+ targetIndex = idx;
2714
+ break;
2715
+ }
2642
2716
  }
2643
2717
  }
2718
+ node = node.parentElement;
2644
2719
  }
2645
- node = node.parentElement;
2646
2720
  }
2647
2721
  }
2648
2722
  }
@@ -2714,7 +2788,7 @@ o.stopRecording = () => {
2714
2788
  rec._observer.disconnect();
2715
2789
  rec._observer = null;
2716
2790
  }
2717
- return {
2791
+ const out = {
2718
2792
  actions: [...rec.actions],
2719
2793
  mocks: { ...rec.mocks },
2720
2794
  initialData: { ...rec.initialData },
@@ -2724,6 +2798,10 @@ o.stopRecording = () => {
2724
2798
  observeRoot: rec.observeRoot || null,
2725
2799
  websocketEvents: [...rec.websocketEvents || []]
2726
2800
  };
2801
+ if (rec.strictCapture) {
2802
+ out.strictCapture = { ...rec.strictCapture };
2803
+ }
2804
+ return out;
2727
2805
  };
2728
2806
  o.clearRecording = (id) => {
2729
2807
  if (id !== void 0) {
@@ -2738,6 +2816,8 @@ o.clearRecording = (id) => {
2738
2816
  }
2739
2817
  };
2740
2818
  o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
2819
+ const strictAssertions = !!(opts && opts.strictAssertions);
2820
+ const strictRemoved = opts && opts.strictRemoved !== void 0 ? !!opts.strictRemoved : strictAssertions;
2741
2821
  const preFiltered = opts && opts.assertions;
2742
2822
  const assertions = preFiltered != null ? preFiltered : (recording.assertions || []).filter(
2743
2823
  (a) => actionIdx == null || a.actionIdx === actionIdx
@@ -2772,6 +2852,7 @@ o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
2772
2852
  };
2773
2853
  const r = resolveRoot();
2774
2854
  const norm = (s) => (s || "").trim().replace(/\s+/g, " ");
2855
+ const styleNorm = (s) => norm(String(s || "").replace(/\s*:\s*/g, ": ").replace(/\s*;\s*/g, "; "));
2775
2856
  const getText = (el) => el ? norm(el.textContent || "") : "";
2776
2857
  const removedElements = opts?.removedElements || [];
2777
2858
  const isRemoved = (a) => {
@@ -2791,20 +2872,67 @@ o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
2791
2872
  const failures = [];
2792
2873
  for (const a of deduped) {
2793
2874
  if (isRemoved(a)) {
2794
- passed += 1;
2795
- if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
2796
- console.log("[runRecordingAssertions] skip (explicit removed):", {
2797
- actionIdx: a.actionIdx,
2875
+ if (!strictRemoved) {
2876
+ passed += 1;
2877
+ if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
2878
+ console.log("[runRecordingAssertions] skip (explicit removed):", {
2879
+ actionIdx: a.actionIdx,
2880
+ selector: a.selector,
2881
+ text: (a.text || "").slice(0, 40)
2882
+ });
2883
+ }
2884
+ continue;
2885
+ }
2886
+ let ghost = null;
2887
+ const expText = norm(a.text || "");
2888
+ if (a.listSelector != null && a.index != null) {
2889
+ const items = r.querySelectorAll(a.listSelector);
2890
+ let item = items[a.index];
2891
+ if (!item && a.index > 0) item = items[a.index - 1];
2892
+ if (item) {
2893
+ ghost = a.selector !== a.listSelector ? item.querySelector(a.selector) || item : item;
2894
+ }
2895
+ if (!ghost && expText && a.type === "visible") {
2896
+ for (let j = 0; j < items.length; j++) {
2897
+ const it = items[j];
2898
+ const cand = a.selector !== a.listSelector ? it.querySelector(a.selector) || it : it;
2899
+ if (cand && getText(cand).indexOf(expText) !== -1) {
2900
+ ghost = cand;
2901
+ break;
2902
+ }
2903
+ }
2904
+ }
2905
+ } else {
2906
+ const matches = r.querySelectorAll(a.selector);
2907
+ ghost = matches.length > 0 ? matches[0] : o.D.querySelector(a.selector);
2908
+ }
2909
+ if (ghost && a.type === "visible") {
2910
+ const vis = ghost.nodeType === 1 && (ghost.offsetParent !== null || ghost.getBoundingClientRect && ghost.getBoundingClientRect().width > 0);
2911
+ const gtext = getText(ghost);
2912
+ const still = vis && (!expText || gtext.indexOf(expText) !== -1 || expText.indexOf(gtext) !== -1);
2913
+ if (still) {
2914
+ failures.push({
2915
+ selector: a.selector,
2916
+ message: "expected absent (recorded removed) but matching content still visible"
2917
+ });
2918
+ continue;
2919
+ }
2920
+ } else if (ghost && a.type !== "visible") {
2921
+ failures.push({
2798
2922
  selector: a.selector,
2799
- text: (a.text || "").slice(0, 40)
2923
+ message: "expected absent (recorded removed) but element still present"
2800
2924
  });
2925
+ continue;
2801
2926
  }
2927
+ passed += 1;
2802
2928
  continue;
2803
2929
  }
2804
2930
  let el = null;
2805
2931
  let indexOutOfBounds = false;
2932
+ let listItemsLength = -1;
2806
2933
  if (a.listSelector != null && a.index != null) {
2807
2934
  const items = r.querySelectorAll(a.listSelector);
2935
+ listItemsLength = items.length;
2808
2936
  const expectedText = norm(a.text || "");
2809
2937
  const tryItem = (idx) => {
2810
2938
  const it = items[idx];
@@ -2812,26 +2940,36 @@ o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
2812
2940
  const e = a.selector !== a.listSelector ? it.querySelector(a.selector) : it;
2813
2941
  return e || (a.selector !== a.listSelector ? it : null);
2814
2942
  };
2815
- let item = items[a.index];
2816
- if (!item && a.index > 0) item = items[a.index - 1];
2817
- if (item) {
2818
- el = tryItem(a.index) || (a.index > 0 ? tryItem(a.index - 1) : null);
2819
- if (!el && a.selector !== a.listSelector) el = item;
2820
- if (a.type === "visible" && expectedText && el) {
2821
- const actualText = getText(el);
2822
- const textMismatch = actualText.indexOf(expectedText) === -1 && expectedText.indexOf(actualText) === -1;
2823
- if (textMismatch) {
2824
- for (let j = 0; j < items.length; j++) {
2825
- const candEl = tryItem(j);
2826
- if (candEl && getText(candEl).indexOf(expectedText) !== -1) {
2827
- el = candEl;
2828
- item = items[j];
2829
- break;
2943
+ let item;
2944
+ if (strictAssertions) {
2945
+ item = items[a.index];
2946
+ if (item) {
2947
+ el = tryItem(a.index);
2948
+ if (!el && a.selector !== a.listSelector) el = item;
2949
+ }
2950
+ } else {
2951
+ item = items[a.index];
2952
+ if (!item && a.index > 0) item = items[a.index - 1];
2953
+ if (item) {
2954
+ el = tryItem(a.index) || (a.index > 0 ? tryItem(a.index - 1) : null);
2955
+ if (!el && a.selector !== a.listSelector) el = item;
2956
+ if (a.type === "visible" && expectedText && el) {
2957
+ const actualText = getText(el);
2958
+ const textMismatch = actualText.indexOf(expectedText) === -1 && expectedText.indexOf(actualText) === -1;
2959
+ if (textMismatch) {
2960
+ for (let j = 0; j < items.length; j++) {
2961
+ const candEl = tryItem(j);
2962
+ if (candEl && getText(candEl).indexOf(expectedText) !== -1) {
2963
+ el = candEl;
2964
+ item = items[j];
2965
+ break;
2966
+ }
2830
2967
  }
2831
2968
  }
2832
2969
  }
2833
2970
  }
2834
- } else {
2971
+ }
2972
+ if (!item) {
2835
2973
  indexOutOfBounds = true;
2836
2974
  }
2837
2975
  } else {
@@ -2842,12 +2980,12 @@ o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
2842
2980
  const visible = el && el.nodeType === 1 && (el.offsetParent !== null || el.getBoundingClientRect && el.getBoundingClientRect().width > 0);
2843
2981
  const expectedText = norm(a.text || "");
2844
2982
  const actualText = getText(el);
2845
- const fullActual = actualText;
2846
- const textOk = !expectedText || actualText.indexOf(expectedText) !== -1 || fullActual.indexOf(expectedText) !== -1 || expectedText.length > 0 && expectedText.indexOf(actualText) !== -1;
2983
+ const textOk = strictAssertions ? !expectedText || actualText === expectedText : !expectedText || actualText.indexOf(expectedText) !== -1 || expectedText.length > 0 && expectedText.indexOf(actualText) !== -1;
2847
2984
  if (visible && textOk) {
2848
2985
  passed += 1;
2849
2986
  } else {
2850
- const message = indexOutOfBounds ? `index out of bounds (list has ${r.querySelectorAll(a.listSelector || a.selector).length} items, assertion expected index ${a.index})` : !el ? "element not found" : !visible ? "not visible" : !textOk ? "text mismatch" : "fail";
2987
+ const listCount = listItemsLength >= 0 ? listItemsLength : r.querySelectorAll(a.listSelector || a.selector).length;
2988
+ const message = indexOutOfBounds ? `index out of bounds (list has ${listCount} items, assertion expected index ${a.index})` : !el ? "element not found" : !visible ? "not visible" : !textOk ? "text mismatch" : "fail";
2851
2989
  failures.push({ selector: a.selector, message });
2852
2990
  if (typeof console !== "undefined" && console.warn) {
2853
2991
  console.warn("[runRecordingAssertions] visible failed:", {
@@ -2864,10 +3002,11 @@ o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
2864
3002
  } else if (a.type === "class") {
2865
3003
  const tokens = (a.className || "").trim().split(/\s+/).filter(Boolean);
2866
3004
  const hasClass = el && (tokens.length === 0 || tokens.every((c) => el.classList?.contains(c)));
2867
- if (hasClass) {
3005
+ const classOrderOk = !strictAssertions || !a.className || norm((el?.className || "").trim()) === norm((a.className || "").trim());
3006
+ if (hasClass && classOrderOk) {
2868
3007
  passed += 1;
2869
3008
  } else {
2870
- const msg = indexOutOfBounds ? `index out of bounds (list has ${r.querySelectorAll(a.listSelector).length} items, expected index ${a.index})` : !el ? "element not found" : `expected class "${a.className}"`;
3009
+ const msg = indexOutOfBounds ? `index out of bounds (list has ${r.querySelectorAll(a.listSelector).length} items, expected index ${a.index})` : !el ? "element not found" : hasClass && !classOrderOk ? `expected exact className "${a.className}" (strict)` : `expected class "${a.className}"`;
2871
3010
  failures.push({ selector: a.selector, message: msg });
2872
3011
  if (typeof console !== "undefined" && console.warn) {
2873
3012
  console.warn("[runRecordingAssertions] failed:", {
@@ -2884,7 +3023,7 @@ o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
2884
3023
  } else if (a.type === "style") {
2885
3024
  const expected = (a.style || "").trim();
2886
3025
  const actual = (el?.style?.cssText || el?.getAttribute?.("style") || "").trim();
2887
- const ok = el && (!expected || actual.indexOf(expected) !== -1 || expected === actual);
3026
+ const ok = el && (!expected || (strictAssertions ? styleNorm(actual) === styleNorm(expected) : actual.indexOf(expected) !== -1 || expected === actual));
2888
3027
  if (ok) {
2889
3028
  passed += 1;
2890
3029
  } else {
@@ -2931,6 +3070,7 @@ o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
2931
3070
  };
2932
3071
  o.exportTest = (recording, options = {}) => {
2933
3072
  const delay = options.delay !== void 0 ? options.delay : 16;
3073
+ const extensionExport = options.extensionExport === true;
2934
3074
  const recordingData = {
2935
3075
  actions: recording.actions,
2936
3076
  assertions: recording.assertions || [],
@@ -2995,13 +3135,24 @@ o.exportTest = (recording, options = {}) => {
2995
3135
  }
2996
3136
  }
2997
3137
  const mocksStr = Object.keys(recording.mocks || {}).length ? JSON.stringify(recording.mocks, null, 2) : "{}";
2998
- return `// Auto-generated by o.exportTest() \u2014 review and anonymize mocks before committing
3138
+ const header = `// Auto-generated by o.exportTest() \u2014 review and anonymize mocks before committing
2999
3139
  const recordingMocks = ${mocksStr};
3000
3140
  const recordingData = { actions: ${JSON.stringify(recording.actions)}, assertions: ${JSON.stringify(recording.assertions || [])}, observeRoot: ${JSON.stringify(recording.observeRoot || null)} };
3001
3141
 
3002
- o.addTest('Recorded test', [
3142
+ `;
3143
+ const manualLine = ` // Add manual checks: ['Manual: label', () => o.testConfirm('label', ['item1'])],`;
3144
+ if (extensionExport) {
3145
+ return header + `const __objsExtensionTestRun = o.test('Recorded test',
3146
+ ${steps.join(",\n")},
3147
+ ${manualLine}
3148
+ { sync: true }, () => {
3149
+ // teardown
3150
+ });
3151
+ `;
3152
+ }
3153
+ return header + `o.addTest('Recorded test', [
3003
3154
  ${steps.join(",\n")}
3004
- // Add manual checks: ['Manual: label', () => o.testConfirm('label', ['item1'])],
3155
+ ${manualLine}
3005
3156
  ], () => {
3006
3157
  // teardown
3007
3158
  });
@@ -3166,50 +3317,227 @@ test(${JSON.stringify(testName)}, async ({ page }) => {
3166
3317
  `;
3167
3318
  };
3168
3319
  o.playRecording = (recording, opts = {}) => {
3169
- const isOptions = opts && typeof opts === "object" && (opts.runAssertions !== void 0 || opts.root !== void 0 || opts.manualChecks !== void 0 || opts.actionDelay !== void 0);
3320
+ const isOptions = opts && typeof opts === "object" && (opts.runAssertions !== void 0 || opts.root !== void 0 || opts.manualChecks !== void 0 || opts.actionDelay !== void 0 || opts.skipWebSocketMock !== void 0 || opts.skipNetworkMocks !== void 0 || opts.recordingAssertionDebug !== void 0 || opts.strictPlay !== void 0 || opts.strictAssertions !== void 0 || opts.strictNetwork !== void 0 || opts.strictWebSocket !== void 0 || opts.strictRemoved !== void 0 || opts.onComplete !== void 0);
3170
3321
  const mockOverrides = isOptions ? opts.mockOverrides || {} : opts;
3171
3322
  const runAssertions = isOptions && opts.runAssertions;
3172
3323
  const rootOpt = isOptions ? opts.root : void 0;
3173
3324
  const manualChecks = isOptions && opts.manualChecks || [];
3174
3325
  const actionDelay = isOptions && opts.actionDelay !== void 0 ? opts.actionDelay : 16;
3175
- const allMocks = Object.assign({}, recording.mocks, mockOverrides);
3176
- const origFetch = window.fetch;
3177
- window.fetch = (url, opts2 = {}) => {
3178
- const method = (opts2.method || "GET").toUpperCase();
3179
- const key = method + ":" + url;
3180
- if (allMocks[key]) {
3181
- const mock = allMocks[key];
3182
- const body = typeof mock.response === "string" ? mock.response : JSON.stringify(mock.response);
3183
- return Promise.resolve(new Response(body, { status: mock.status || 200 }));
3326
+ const skipWebSocketMock = isOptions && opts.skipWebSocketMock;
3327
+ const skipNetworkMocks = isOptions && opts.skipNetworkMocks;
3328
+ if (isOptions && opts.recordingAssertionDebug !== void 0) {
3329
+ o.recordingAssertionDebug = !!opts.recordingAssertionDebug;
3330
+ }
3331
+ const sc = recording.strictCapture || {};
3332
+ const strictPlay = isOptions && opts.strictPlay === true;
3333
+ const strictAssertions = isOptions && opts.strictAssertions !== void 0 ? !!opts.strictAssertions : strictPlay ? true : !!sc.assertions;
3334
+ const strictNetwork = isOptions && opts.strictNetwork !== void 0 ? !!opts.strictNetwork : strictPlay ? true : !!sc.network;
3335
+ const strictWebSocket = isOptions && opts.strictWebSocket !== void 0 ? !!opts.strictWebSocket : strictPlay ? true : !!sc.websocket;
3336
+ const strictRemoved = isOptions && opts.strictRemoved !== void 0 ? !!opts.strictRemoved : strictAssertions;
3337
+ const parseBodyLikeRecorder = (body) => {
3338
+ if (body == null || body === "") return void 0;
3339
+ if (typeof body === "string") {
3340
+ try {
3341
+ return JSON.parse(body);
3342
+ } catch (_e) {
3343
+ return body;
3344
+ }
3184
3345
  }
3185
- return origFetch(url, opts2);
3346
+ return body;
3347
+ };
3348
+ const mockRequestMatchesLive = (recordedReq, liveBody) => {
3349
+ const live = parseBodyLikeRecorder(liveBody);
3350
+ if (recordedReq === live) return true;
3351
+ if (recordedReq == null && live == null) return true;
3352
+ if (recordedReq == null || live == null) return false;
3353
+ if (typeof recordedReq === "object" && typeof live === "object")
3354
+ return JSON.stringify(recordedReq) === JSON.stringify(live);
3355
+ return String(recordedReq) === String(live);
3186
3356
  };
3357
+ const normWsData = (s) => String(s || "").trim().replace(/\s+/g, " ");
3358
+ const allMocks = Object.assign({}, recording.mocks, mockOverrides);
3359
+ const origFetch = window.fetch;
3187
3360
  const origXHROpen = XMLHttpRequest.prototype.open;
3188
3361
  const origXHRSend = XMLHttpRequest.prototype.send;
3189
- XMLHttpRequest.prototype.open = function(method, url) {
3190
- this._oMethod = (method || "GET").toUpperCase();
3191
- this._oUrl = url;
3192
- return origXHROpen.apply(this, arguments);
3193
- };
3194
- XMLHttpRequest.prototype.send = function(body) {
3195
- const xhr = this;
3196
- const key = (xhr._oMethod || "GET") + ":" + (xhr._oUrl || "");
3197
- const mock = allMocks[key];
3198
- if (mock) {
3199
- const respBody = typeof mock.response === "string" ? mock.response : JSON.stringify(mock.response);
3200
- setTimeout(() => {
3201
- xhr.readyState = 4;
3202
- xhr.status = mock.status || 200;
3203
- xhr.statusText = "OK";
3204
- xhr.responseText = respBody;
3205
- xhr.response = respBody;
3206
- xhr.dispatchEvent(new Event("readystatechange"));
3207
- xhr.dispatchEvent(new Event("load"));
3208
- }, 0);
3209
- return;
3210
- }
3211
- return origXHRSend.apply(this, arguments);
3212
- };
3362
+ if (!skipNetworkMocks) {
3363
+ window.fetch = (url, fetchOpts = {}) => {
3364
+ const method = (fetchOpts.method || "GET").toUpperCase();
3365
+ const key = method + ":" + url;
3366
+ if (allMocks[key]) {
3367
+ const mock = allMocks[key];
3368
+ if (strictNetwork && o.C(mock, "request") && !mockRequestMatchesLive(mock.request, fetchOpts.body)) {
3369
+ return Promise.reject(
3370
+ new Error(
3371
+ "[Objs playRecording] strictNetwork: request body does not match recording for " + key
3372
+ )
3373
+ );
3374
+ }
3375
+ const body = typeof mock.response === "string" ? mock.response : JSON.stringify(mock.response);
3376
+ return Promise.resolve(new Response(body, { status: mock.status || 200 }));
3377
+ }
3378
+ return origFetch(url, fetchOpts);
3379
+ };
3380
+ XMLHttpRequest.prototype.open = function(method, url) {
3381
+ this._oMethod = (method || "GET").toUpperCase();
3382
+ this._oUrl = url;
3383
+ return origXHROpen.apply(this, arguments);
3384
+ };
3385
+ XMLHttpRequest.prototype.send = function(body) {
3386
+ const xhr = this;
3387
+ const key = (xhr._oMethod || "GET") + ":" + (xhr._oUrl || "");
3388
+ const mock = allMocks[key];
3389
+ if (mock) {
3390
+ if (strictNetwork && o.C(mock, "request") && !mockRequestMatchesLive(mock.request, body)) {
3391
+ setTimeout(() => {
3392
+ xhr.readyState = 4;
3393
+ xhr.status = 0;
3394
+ xhr.statusText = "Objs strictNetwork mismatch";
3395
+ xhr.dispatchEvent(new Event("readystatechange"));
3396
+ xhr.dispatchEvent(new Event("error"));
3397
+ }, 0);
3398
+ return;
3399
+ }
3400
+ const respBody = typeof mock.response === "string" ? mock.response : JSON.stringify(mock.response);
3401
+ setTimeout(() => {
3402
+ xhr.readyState = 4;
3403
+ xhr.status = mock.status || 200;
3404
+ xhr.statusText = "OK";
3405
+ xhr.responseText = respBody;
3406
+ xhr.response = respBody;
3407
+ xhr.dispatchEvent(new Event("readystatechange"));
3408
+ xhr.dispatchEvent(new Event("load"));
3409
+ }, 0);
3410
+ return;
3411
+ }
3412
+ return origXHRSend.apply(this, arguments);
3413
+ };
3414
+ }
3415
+ let origWebSocket = null;
3416
+ const wsEvents = recording.websocketEvents || [];
3417
+ const useWsMock = !skipWebSocketMock && wsEvents.length > 0 && wsEvents.some((e) => e.messages && e.messages.length > 0);
3418
+ if (useWsMock && typeof window.WebSocket === "function") {
3419
+ origWebSocket = window.WebSocket;
3420
+ let wsConsumeIdx = 0;
3421
+ const normalizeWsUrl = (u) => {
3422
+ const s = typeof u === "string" ? u : String(u);
3423
+ try {
3424
+ return new URL(s, window.location.href).href;
3425
+ } catch (_e) {
3426
+ return s;
3427
+ }
3428
+ };
3429
+ const takeNextRecorded = (urlStr) => {
3430
+ const norm = normalizeWsUrl(urlStr);
3431
+ for (let i = wsConsumeIdx; i < wsEvents.length; i++) {
3432
+ if (normalizeWsUrl(wsEvents[i].url) === norm) {
3433
+ wsConsumeIdx = i + 1;
3434
+ return wsEvents[i];
3435
+ }
3436
+ }
3437
+ for (let i = wsConsumeIdx; i < wsEvents.length; i++) {
3438
+ if (String(wsEvents[i].url) === String(urlStr)) {
3439
+ wsConsumeIdx = i + 1;
3440
+ return wsEvents[i];
3441
+ }
3442
+ }
3443
+ return null;
3444
+ };
3445
+ const C = origWebSocket;
3446
+ class O_MockWebSocket extends EventTarget {
3447
+ constructor(url, protocols, recorded) {
3448
+ super();
3449
+ const urlStr = typeof url === "string" ? url : String(url);
3450
+ this.url = urlStr;
3451
+ this.readyState = C.CONNECTING;
3452
+ const p = protocols;
3453
+ this.protocol = Array.isArray(p) ? p[0] || "" : p ? String(p) : "";
3454
+ this.extensions = "";
3455
+ this.binaryType = "blob";
3456
+ this._messages = (recorded.messages || []).slice();
3457
+ this._pos = 0;
3458
+ const self = this;
3459
+ setTimeout(() => {
3460
+ if (self.readyState === C.CLOSED) return;
3461
+ self.readyState = C.OPEN;
3462
+ self._dispatchOpen();
3463
+ self._drainInbound();
3464
+ }, 0);
3465
+ }
3466
+ _dispatchOpen() {
3467
+ const ev = new Event("open");
3468
+ this.dispatchEvent(ev);
3469
+ if (typeof this.onopen === "function") this.onopen(ev);
3470
+ }
3471
+ _dispatchMessage(data) {
3472
+ const ev = new MessageEvent("message", { data });
3473
+ this.dispatchEvent(ev);
3474
+ if (typeof this.onmessage === "function") this.onmessage(ev);
3475
+ }
3476
+ _drainInbound() {
3477
+ while (this._pos < this._messages.length && this._messages[this._pos].dir === "in") {
3478
+ const m = this._messages[this._pos++];
3479
+ this._dispatchMessage(m.data);
3480
+ }
3481
+ }
3482
+ send(data) {
3483
+ if (this.readyState !== C.OPEN) {
3484
+ const err = typeof DOMException !== "undefined" ? new DOMException("Still in CONNECTING state.", "InvalidStateError") : new Error("InvalidStateError");
3485
+ throw err;
3486
+ }
3487
+ if (this._pos >= this._messages.length) {
3488
+ if (strictWebSocket) {
3489
+ throw new Error(
3490
+ "[Objs playRecording] strictWebSocket: unexpected send() after recorded frames exhausted"
3491
+ );
3492
+ }
3493
+ this._drainInbound();
3494
+ return;
3495
+ }
3496
+ const next = this._messages[this._pos];
3497
+ if (next.dir === "out") {
3498
+ if (strictWebSocket) {
3499
+ const got = typeof data === "string" ? data : String(data);
3500
+ const exp = String(next.data != null ? next.data : "");
3501
+ if (normWsData(got) !== normWsData(exp)) {
3502
+ throw new Error(
3503
+ "[Objs playRecording] strictWebSocket: outbound frame mismatch"
3504
+ );
3505
+ }
3506
+ }
3507
+ this._pos++;
3508
+ }
3509
+ this._drainInbound();
3510
+ }
3511
+ close(code, reason) {
3512
+ if (this.readyState === C.CLOSING || this.readyState === C.CLOSED) return;
3513
+ this.readyState = C.CLOSING;
3514
+ const self = this;
3515
+ setTimeout(() => {
3516
+ self.readyState = C.CLOSED;
3517
+ const ev = typeof CloseEvent !== "undefined" ? new CloseEvent("close", {
3518
+ code: code !== void 0 ? code : 1e3,
3519
+ reason: reason !== void 0 ? String(reason) : "",
3520
+ wasClean: true
3521
+ }) : new Event("close");
3522
+ self.dispatchEvent(ev);
3523
+ if (typeof self.onclose === "function") self.onclose(ev);
3524
+ }, 0);
3525
+ }
3526
+ }
3527
+ const MockWebSocketCtor = function MockWebSocketCtor2(url, protocols) {
3528
+ const urlStr = typeof url === "string" ? url : String(url);
3529
+ const rec = takeNextRecorded(urlStr);
3530
+ if (!rec || !rec.messages || rec.messages.length === 0) {
3531
+ return new origWebSocket(url, protocols);
3532
+ }
3533
+ return new O_MockWebSocket(url, protocols, rec);
3534
+ };
3535
+ MockWebSocketCtor.CONNECTING = C.CONNECTING;
3536
+ MockWebSocketCtor.OPEN = C.OPEN;
3537
+ MockWebSocketCtor.CLOSING = C.CLOSING;
3538
+ MockWebSocketCtor.CLOSED = C.CLOSED;
3539
+ window.WebSocket = MockWebSocketCtor;
3540
+ }
3213
3541
  const resolveRoot = () => {
3214
3542
  if (rootOpt != null) {
3215
3543
  return typeof rootOpt === "string" ? o.D.querySelector(rootOpt) || o.D.body : rootOpt;
@@ -3314,7 +3642,9 @@ o.playRecording = (recording, opts = {}) => {
3314
3642
  const run = () => {
3315
3643
  const r = o.runRecordingAssertions(recording, rootEl, i, {
3316
3644
  assertions: asserted,
3317
- removedElements: recording.removedElements
3645
+ removedElements: recording.removedElements,
3646
+ strictAssertions,
3647
+ strictRemoved
3318
3648
  });
3319
3649
  assertionAccum.passed += r.passed;
3320
3650
  assertionAccum.total += r.total;
@@ -3345,6 +3675,7 @@ o.playRecording = (recording, opts = {}) => {
3345
3675
  window.fetch = origFetch;
3346
3676
  XMLHttpRequest.prototype.open = origXHROpen;
3347
3677
  XMLHttpRequest.prototype.send = origXHRSend;
3678
+ if (origWebSocket) window.WebSocket = origWebSocket;
3348
3679
  const assertionResult = runAssertions && assertions.length > 0 ? assertionAccum : void 0;
3349
3680
  if (assertionResult?.failures?.length > 0) {
3350
3681
  o.tRes[testId2] = false;
@@ -3362,34 +3693,54 @@ o.testOverlay = () => {
3362
3693
  if (o("#" + btnId).el) {
3363
3694
  return;
3364
3695
  }
3696
+ const scrollId = "o-test-overlay-scroll";
3697
+ const exportBtnId = "o-test-export-objs";
3698
+ const copyBtnId = "o-test-copy-txt";
3699
+ const btnBarStyle = "padding:6px 10px;background:#334155;color:#e2e8f0;border:none;border-radius:6px;cursor:pointer;font-size:12px;";
3700
+ const buildListPlainText = () => o.tLog.map((log, i) => (log != null && log !== "" ? String(log) : "Test #" + i) + (o.tRes[i] ? " \u2713" : " \u2717")).join("\n\n");
3365
3701
  const updatePanel = () => {
3366
- const panel = o("#" + panelId);
3367
- if (!panel.el) return;
3368
- const total = o.tRes.length;
3369
- const passed = o.tRes.filter(Boolean).length;
3370
- let html = `<b>Tests: ${passed}/${total}</b><hr style="margin:4px 0">`;
3702
+ const scroll = o("#" + scrollId);
3703
+ if (!scroll.el) return;
3704
+ let html = "";
3371
3705
  o.tLog.forEach((log, i) => {
3372
3706
  const ok = o.tRes[i];
3373
- html += `<div style="margin:2px 0;padding:2px 4px;border-radius:3px;background:${ok ? "#d4edda" : "#f8d7da"};color:${ok ? "#155724" : "#721c24"};font-size:11px;white-space:pre-wrap">${log || "Test #" + i}</div>`;
3374
- });
3375
- html += `<button id="o-test-export" style="margin-top:6px;padding:4px 8px;font-size:11px;cursor:pointer">Export results</button>`;
3376
- panel.html(html);
3377
- o("#o-test-export").on("click", () => {
3378
- const data = JSON.stringify({ results: o.tRes, logs: o.tLog }, null, 2);
3379
- const blob = new Blob([data], { type: "application/json" });
3380
- const a = o.D.createElement("a");
3381
- a.href = URL.createObjectURL(blob);
3382
- a.download = "objs-test-results.json";
3383
- a.click();
3707
+ html += `<div style="margin:2px 0;padding:2px 4px;border-radius:3px;background:${ok ? "#14532d" : "#450a0a"};color:${ok ? "#86efac" : "#fca5a5"};font-size:11px;white-space:pre-wrap">${log || "Test #" + i}</div>`;
3384
3708
  });
3709
+ scroll.html(html);
3385
3710
  };
3386
- const innerHTML = `<div style="display:flex;align-items:center;gap:12px;"><span class="o-test-overlay-summary" style="flex:1;font-size:13px;cursor:grab;">Tests: 0/0</span><button type="button" class="o-test-overlay-toggle" style="padding:6px 10px;background:#334155;color:#e2e8f0;border:none;border-radius:6px;cursor:pointer;font-size:12px;">List</button><button type="button" class="o-test-overlay-close" style="padding:4px 8px;background:transparent;color:#94a3b8;border:none;border-radius:4px;cursor:pointer;font-size:16px;line-height:1;" title="Close">\xD7</button></div><div id="${panelId}" style="display:none;margin-top:4px;padding:8px;background:#fff;border:1px solid #334155;border-radius:6px;max-height:240px;overflow-y:auto;box-shadow:0 2px 8px rgba(0,0,0,.15);font-size:11px;user-select:text;cursor:text;"></div>`;
3711
+ const innerHTML = `<div class="o-test-overlay-root" style="display:flex;flex-direction:column;gap:4px;max-height:min(88vh,560px);overflow:hidden;"><div style="display:flex;align-items:center;gap:12px;flex-shrink:0;"><span class="o-test-overlay-summary" style="flex:1;font-size:13px;cursor:grab;">Tests: 0/0</span><button type="button" class="o-test-overlay-toggle" style="${btnBarStyle}">List</button><button type="button" class="o-test-overlay-close" style="padding:4px 8px;background:transparent;color:#94a3b8;border:none;border-radius:4px;cursor:pointer;font-size:16px;line-height:1;" title="Close">\xD7</button></div><div id="${panelId}" style="display:none;flex-direction:column;margin-top:4px;max-height:min(52vh,420px);background:#0a0f1e;border:1px solid #1e293b;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,.35);overflow:hidden;"><div id="${scrollId}" style="box-sizing:border-box;height:min(48vh,380px);overflow-y:scroll;padding:8px;font-size:11px;user-select:text;cursor:text;"></div><div id="o-test-overlay-footer" style="display:flex;flex-wrap:wrap;gap:8px;padding:8px;border-top:1px solid #1e293b;background:#0f172a;flex-shrink:0;"><button type="button" id="${exportBtnId}" class="o-test-overlay-export-btn" style="${btnBarStyle}">Export (objs)</button><button type="button" id="${copyBtnId}" class="o-test-overlay-export-btn" style="${btnBarStyle}">Copy (txt)</button></div></div></div>`;
3387
3712
  const box = o.overlay({
3388
3713
  innerHTML,
3389
3714
  removeExisting: false,
3390
3715
  className: "o-test-overlay",
3391
3716
  id: btnId,
3392
- excludeDragSelector: ".o-test-overlay-close, .o-test-overlay-toggle, #" + panelId
3717
+ excludeDragSelector: ".o-test-overlay-close, .o-test-overlay-toggle, #" + panelId + ", #" + scrollId + ", #o-test-overlay-footer, .o-test-overlay-export-btn"
3718
+ });
3719
+ o("#" + exportBtnId).on("click", () => {
3720
+ const data = JSON.stringify({ results: o.tRes, logs: o.tLog }, null, 2);
3721
+ const blob = new Blob([data], { type: "application/json" });
3722
+ const a = o.D.createElement("a");
3723
+ a.href = URL.createObjectURL(blob);
3724
+ a.download = "objs-test-results.json";
3725
+ a.click();
3726
+ });
3727
+ o("#" + copyBtnId).on("click", () => {
3728
+ const text = buildListPlainText();
3729
+ const write = () => {
3730
+ const ta = o.D.createElement("textarea");
3731
+ ta.value = text;
3732
+ ta.setAttribute("readonly", "");
3733
+ ta.style.cssText = "position:fixed;left:-9999px;top:0";
3734
+ o.D.body.appendChild(ta);
3735
+ ta.select();
3736
+ o.D.execCommand("copy");
3737
+ ta.remove();
3738
+ };
3739
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
3740
+ navigator.clipboard.writeText(text).catch(write);
3741
+ } else {
3742
+ write();
3743
+ }
3393
3744
  });
3394
3745
  const refreshSummary = () => {
3395
3746
  const summary = o(".o-test-overlay-summary");
@@ -3400,8 +3751,12 @@ o.testOverlay = () => {
3400
3751
  const panel = o("#" + panelId);
3401
3752
  if (!panel.el) return;
3402
3753
  const isOpen = panel.el.style.display !== "none";
3403
- panel.css({ display: isOpen ? "none" : "block" });
3404
- if (!isOpen) updatePanel();
3754
+ if (isOpen) {
3755
+ panel.el.style.display = "none";
3756
+ } else {
3757
+ panel.el.style.display = "flex";
3758
+ updatePanel();
3759
+ }
3405
3760
  });
3406
3761
  box.first(".o-test-overlay-close").on("click", () => {
3407
3762
  box._overlayCleanup();
@@ -3409,7 +3764,7 @@ o.testOverlay = () => {
3409
3764
  o.testOverlay.showPanel = () => {
3410
3765
  const panel = o("#" + panelId);
3411
3766
  if (!panel.el) return;
3412
- panel.css({ display: "block" });
3767
+ panel.el.style.display = "flex";
3413
3768
  updatePanel();
3414
3769
  refreshSummary();
3415
3770
  };