objs-core 2.1.0 → 2.2.0
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/README.md +19 -0
- package/objs.built.js +667 -187
- package/objs.built.min.js +48 -38
- package/objs.d.ts +56 -5
- package/objs.js +818 -260
- package/package.json +1 -1
package/objs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Objs-core library
|
|
3
|
-
* @version 2.
|
|
3
|
+
* @version 2.2
|
|
4
4
|
* @author Roman Torshin
|
|
5
5
|
* @license Apache-2.0
|
|
6
6
|
*/
|
|
@@ -1157,6 +1157,15 @@ const __DEV__ = true;
|
|
|
1157
1157
|
}
|
|
1158
1158
|
}, "html");
|
|
1159
1159
|
|
|
1160
|
+
result.toString = function () {
|
|
1161
|
+
return result.html();
|
|
1162
|
+
};
|
|
1163
|
+
result[Symbol.toPrimitive] = function (hint) {
|
|
1164
|
+
if (hint === "string" || hint === "default") return result.html();
|
|
1165
|
+
if (hint === "number") return result.els?.length ?? 0;
|
|
1166
|
+
return result.html();
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1160
1169
|
/**
|
|
1161
1170
|
* Get or set the value property of form elements (input, textarea, select).
|
|
1162
1171
|
* @param {string} [value] - Value to set. Omit to get.
|
|
@@ -2506,6 +2515,7 @@ if (__DEV__) {
|
|
|
2506
2515
|
}
|
|
2507
2516
|
|
|
2508
2517
|
/* tests function parameters */
|
|
2518
|
+
o.sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2509
2519
|
o.tLog = []; // test sessions and results
|
|
2510
2520
|
o.tRes = []; // test results
|
|
2511
2521
|
o.tStatus = []; // test statuses
|
|
@@ -2514,6 +2524,8 @@ o.tShowOk = o.F; // show success tests or only errors
|
|
|
2514
2524
|
o.tStyled = o.F; // styled HTML results or plain style
|
|
2515
2525
|
o.tTime = 2000; // timeout for async tests
|
|
2516
2526
|
o.tests = []; // tests with storage
|
|
2527
|
+
o.tExpectedSteps = {}; // expected step count per test (for playRecording when o.tests not used)
|
|
2528
|
+
o.tFinalized = {}; // prevent duplicate finalization
|
|
2517
2529
|
o.tAutolog = o.F; // auto log to console
|
|
2518
2530
|
o.tBeforeEach = undefined; // called before each test case
|
|
2519
2531
|
o.tAfterEach = undefined; // called after each test case
|
|
@@ -2665,10 +2677,21 @@ o.test = (title = "", ...tests) => {
|
|
|
2665
2677
|
}
|
|
2666
2678
|
};
|
|
2667
2679
|
|
|
2680
|
+
// Extract callback and options
|
|
2681
|
+
let opts = {};
|
|
2668
2682
|
if (typeof tests[num - 1] === "function") {
|
|
2669
2683
|
o.tFns[testN] = tests[num - 1];
|
|
2670
2684
|
num--;
|
|
2671
2685
|
}
|
|
2686
|
+
if (
|
|
2687
|
+
num > 0 &&
|
|
2688
|
+
typeof tests[num - 1] === "object" &&
|
|
2689
|
+
!Array.isArray(tests[num - 1]) &&
|
|
2690
|
+
(tests[num - 1].sync !== undefined || tests[num - 1].confirmOnFailure !== undefined)
|
|
2691
|
+
) {
|
|
2692
|
+
opts = tests[num - 1];
|
|
2693
|
+
num--;
|
|
2694
|
+
}
|
|
2672
2695
|
|
|
2673
2696
|
// get tLog from sessionStorage
|
|
2674
2697
|
if (testSession) {
|
|
@@ -2702,6 +2725,172 @@ o.test = (title = "", ...tests) => {
|
|
|
2702
2725
|
o.tRes[testN] = o.F;
|
|
2703
2726
|
o.tStatus[testN] = [];
|
|
2704
2727
|
}
|
|
2728
|
+
o.tExpectedSteps[testN] = num;
|
|
2729
|
+
o.tFinalized[testN] = false;
|
|
2730
|
+
|
|
2731
|
+
const showConfirmOnFailureOverlay = (stepIdx, msg) =>
|
|
2732
|
+
new Promise((resolve) => {
|
|
2733
|
+
const box = o.overlay({
|
|
2734
|
+
innerHTML:
|
|
2735
|
+
`<div style="display:flex;flex-direction:column;gap:8px;">` +
|
|
2736
|
+
`<div style="cursor:grab;">Step ${stepIdx + 1} failed: ${msg || "error"}. Continue testing?</div>` +
|
|
2737
|
+
`<div style="display:flex;gap:8px;">` +
|
|
2738
|
+
`<button class="o-cf-continue" style="padding:6px 12px;background:#2563eb;color:#fff;border:none;border-radius:6px;cursor:pointer;">Continue</button>` +
|
|
2739
|
+
`<button class="o-cf-stop" style="padding:6px 12px;background:#dc2626;color:#fff;border:none;border-radius:6px;cursor:pointer;">Stop</button>` +
|
|
2740
|
+
`</div></div>`,
|
|
2741
|
+
timeout: opts.confirmOnFailureTimeout || undefined,
|
|
2742
|
+
onClose: (r) => resolve(r || { continue: false }),
|
|
2743
|
+
excludeDragSelector: ".o-cf-continue, .o-cf-stop",
|
|
2744
|
+
});
|
|
2745
|
+
box.first(".o-cf-continue").on("click", () => {
|
|
2746
|
+
box._overlayCleanup();
|
|
2747
|
+
resolve({ continue: true });
|
|
2748
|
+
});
|
|
2749
|
+
box.first(".o-cf-stop").on("click", () => {
|
|
2750
|
+
box._overlayCleanup();
|
|
2751
|
+
resolve({ continue: false });
|
|
2752
|
+
});
|
|
2753
|
+
});
|
|
2754
|
+
|
|
2755
|
+
const finalize = () => {
|
|
2756
|
+
if (o.tFinalized[testN]) return;
|
|
2757
|
+
o.tFinalized[testN] = true;
|
|
2758
|
+
const anyFailed = o.tStatus[testN].some((s) => s === false);
|
|
2759
|
+
o.tRes[testN] = !anyFailed && done === num;
|
|
2760
|
+
row = waits ? "├ " : "╘ ";
|
|
2761
|
+
row += "DONE " + done + "/" + num + (waits ? ", waiting: " + waits : "");
|
|
2762
|
+
log(row, done + waits !== num);
|
|
2763
|
+
if (!waits) {
|
|
2764
|
+
log();
|
|
2765
|
+
}
|
|
2766
|
+
if (o.tStyled) {
|
|
2767
|
+
o.tLog[testN] +=
|
|
2768
|
+
o.tPre +
|
|
2769
|
+
'<div style="color:' +
|
|
2770
|
+
(done + waits !== num ? "red" : "green") +
|
|
2771
|
+
';"><b>DONE ' +
|
|
2772
|
+
done +
|
|
2773
|
+
"/" +
|
|
2774
|
+
num +
|
|
2775
|
+
(waits ? ", waiting: " + waits : "") +
|
|
2776
|
+
"</b>" +
|
|
2777
|
+
o.tDc +
|
|
2778
|
+
o.tDc;
|
|
2779
|
+
} else {
|
|
2780
|
+
o.tLog[testN] += row + "\n";
|
|
2781
|
+
}
|
|
2782
|
+
if (testSession) {
|
|
2783
|
+
sessionStorage.setItem(`oTest-Log-${testN}`, o.tLog[testN]);
|
|
2784
|
+
sessionStorage.setItem(`oTest-Res-${testN}`, o.tRes[testN]);
|
|
2785
|
+
sessionStorage.setItem(`oTest-Status-${testN}`, JSON.stringify(o.tStatus[testN]));
|
|
2786
|
+
}
|
|
2787
|
+
if (!waits && typeof o.tFns[testN] === "function") {
|
|
2788
|
+
o.tFns[testN](testN);
|
|
2789
|
+
}
|
|
2790
|
+
};
|
|
2791
|
+
|
|
2792
|
+
if (opts.sync || opts.confirmOnFailure) {
|
|
2793
|
+
(async () => {
|
|
2794
|
+
for (let i = o.tStatus[testN].length; i < num; i++) {
|
|
2795
|
+
const testInfo = {
|
|
2796
|
+
n: testN,
|
|
2797
|
+
i,
|
|
2798
|
+
title: tests[i][0],
|
|
2799
|
+
tShowOk: o.tShowOk,
|
|
2800
|
+
tStyled: o.tStyled,
|
|
2801
|
+
};
|
|
2802
|
+
let res = tests[i][1];
|
|
2803
|
+
if (typeof res === "undefined") {
|
|
2804
|
+
if (o.tStyled) {
|
|
2805
|
+
o.tLog[testN] += "<div>" + testInfo.title + "</div>";
|
|
2806
|
+
} else {
|
|
2807
|
+
o.tLog[testN] += testInfo.title + "\n";
|
|
2808
|
+
}
|
|
2809
|
+
log("├ " + testInfo.title, false, true);
|
|
2810
|
+
o.tStatus[testN][i] = true;
|
|
2811
|
+
done++;
|
|
2812
|
+
continue;
|
|
2813
|
+
}
|
|
2814
|
+
if (typeof o.tBeforeEach === "function") {
|
|
2815
|
+
o.tBeforeEach(testInfo);
|
|
2816
|
+
}
|
|
2817
|
+
if (typeof res === "function") {
|
|
2818
|
+
try {
|
|
2819
|
+
res = res(testInfo);
|
|
2820
|
+
} catch (error) {
|
|
2821
|
+
res = error.message;
|
|
2822
|
+
if (o.onError) {
|
|
2823
|
+
o.onError(error);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
if (typeof o.tAfterEach === "function") {
|
|
2828
|
+
o.tAfterEach(testInfo, res);
|
|
2829
|
+
}
|
|
2830
|
+
if (res && typeof res.then === "function") {
|
|
2831
|
+
try {
|
|
2832
|
+
const value = await res;
|
|
2833
|
+
const ok =
|
|
2834
|
+
value === true ||
|
|
2835
|
+
value == null ||
|
|
2836
|
+
(value && typeof value === "object" && value.ok === true);
|
|
2837
|
+
const msg =
|
|
2838
|
+
value && value.errors && value.errors.length
|
|
2839
|
+
? value.errors.join("; ")
|
|
2840
|
+
: typeof value === "string"
|
|
2841
|
+
? value
|
|
2842
|
+
: "";
|
|
2843
|
+
o.testUpdate(testInfo, ok, ok ? "" : msg ? ": " + msg : "");
|
|
2844
|
+
done++;
|
|
2845
|
+
if (!ok && opts.confirmOnFailure) {
|
|
2846
|
+
const choice = await showConfirmOnFailureOverlay(i, msg);
|
|
2847
|
+
if (!choice.continue) break;
|
|
2848
|
+
}
|
|
2849
|
+
} catch (err) {
|
|
2850
|
+
o.testUpdate(testInfo, false, err.message || "Promise rejected");
|
|
2851
|
+
if (opts.confirmOnFailure) {
|
|
2852
|
+
const choice = await showConfirmOnFailureOverlay(i, err.message || "Promise rejected");
|
|
2853
|
+
if (!choice.continue) break;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
continue;
|
|
2857
|
+
}
|
|
2858
|
+
if (typeof o.tStatus[testN][i] === "undefined") {
|
|
2859
|
+
o.tStatus[testN][i] = typeof res === "string" ? o.F : res;
|
|
2860
|
+
} else {
|
|
2861
|
+
sessionStorage.setItem(`oTest-Status-${testN}`, JSON.stringify(o.tStatus[testN]));
|
|
2862
|
+
return;
|
|
2863
|
+
}
|
|
2864
|
+
if (res === true) {
|
|
2865
|
+
done++;
|
|
2866
|
+
if (o.tShowOk) {
|
|
2867
|
+
o.tLog[testN] += preOk + tests[i][0] + posOk;
|
|
2868
|
+
log("├ OK: " + tests[i][0]);
|
|
2869
|
+
}
|
|
2870
|
+
} else if (res !== o.U) {
|
|
2871
|
+
o.tLog[testN] += preXx + tests[i][0] + (res !== o.F ? ": " + res : "") + posXx;
|
|
2872
|
+
log("├ ✘ " + tests[i][0] + (res !== o.F ? ": " + res : ""), true);
|
|
2873
|
+
if (opts.confirmOnFailure) {
|
|
2874
|
+
const choice = await showConfirmOnFailureOverlay(i, typeof res === "string" ? res : "");
|
|
2875
|
+
if (!choice.continue) break;
|
|
2876
|
+
}
|
|
2877
|
+
} else {
|
|
2878
|
+
waits++;
|
|
2879
|
+
setTimeout(
|
|
2880
|
+
(info) => {
|
|
2881
|
+
info.title += " (timeout)";
|
|
2882
|
+
o.testUpdate(info);
|
|
2883
|
+
},
|
|
2884
|
+
o.tTime,
|
|
2885
|
+
testInfo,
|
|
2886
|
+
);
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
finalize();
|
|
2891
|
+
})();
|
|
2892
|
+
return testN;
|
|
2893
|
+
}
|
|
2705
2894
|
|
|
2706
2895
|
for (let i = o.tStatus[testN].length; i < num; i++) {
|
|
2707
2896
|
const testInfo = {
|
|
@@ -2801,42 +2990,7 @@ o.test = (title = "", ...tests) => {
|
|
|
2801
2990
|
}
|
|
2802
2991
|
}
|
|
2803
2992
|
|
|
2804
|
-
|
|
2805
|
-
row = waits ? "├ " : "╘ ";
|
|
2806
|
-
row += "DONE " + done + "/" + num + (waits ? ", waiting: " + waits : "");
|
|
2807
|
-
log(row, done + waits !== num);
|
|
2808
|
-
if (!waits) {
|
|
2809
|
-
log();
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
|
-
if (o.tStyled) {
|
|
2813
|
-
o.tLog[testN] +=
|
|
2814
|
-
o.tPre +
|
|
2815
|
-
'<div style="color:' +
|
|
2816
|
-
(done + waits !== num ? "red" : "green") +
|
|
2817
|
-
';"><b>DONE ' +
|
|
2818
|
-
done +
|
|
2819
|
-
"/" +
|
|
2820
|
-
num +
|
|
2821
|
-
(waits ? ", waiting: " + waits : "") +
|
|
2822
|
-
"</b>" +
|
|
2823
|
-
o.tDc +
|
|
2824
|
-
o.tDc;
|
|
2825
|
-
} else {
|
|
2826
|
-
o.tLog[testN] += row + "\n";
|
|
2827
|
-
}
|
|
2828
|
-
|
|
2829
|
-
// Save test results to sessionStorage
|
|
2830
|
-
if (testSession) {
|
|
2831
|
-
sessionStorage.setItem(`oTest-Log-${testN}`, o.tLog[testN]);
|
|
2832
|
-
sessionStorage.setItem(`oTest-Res-${testN}`, o.tRes[testN]);
|
|
2833
|
-
sessionStorage.setItem(`oTest-Status-${testN}`, JSON.stringify(o.tStatus[testN]));
|
|
2834
|
-
}
|
|
2835
|
-
|
|
2836
|
-
if (!waits && typeof o.tFns[testN] === "function") {
|
|
2837
|
-
o.tFns[testN](testN);
|
|
2838
|
-
}
|
|
2839
|
-
|
|
2993
|
+
finalize();
|
|
2840
2994
|
return testN;
|
|
2841
2995
|
};
|
|
2842
2996
|
|
|
@@ -2902,16 +3056,23 @@ o.testUpdate = (info, res = o.F, suff = "") => {
|
|
|
2902
3056
|
n++;
|
|
2903
3057
|
}
|
|
2904
3058
|
|
|
2905
|
-
|
|
3059
|
+
const expectedSteps =
|
|
3060
|
+
o.tests[testN]?.tests?.length ?? o.tExpectedSteps[testN] ?? Number.MAX_SAFE_INTEGER;
|
|
3061
|
+
if (n < expectedSteps) {
|
|
3062
|
+
if (sessionStorage?.getItem("oTest-Run") === testN) {
|
|
3063
|
+
sessionStorage.setItem(`oTest-Log-${testN}`, o.tLog[testN]);
|
|
3064
|
+
sessionStorage.setItem(`oTest-Res-${testN}`, o.tRes[testN]);
|
|
3065
|
+
sessionStorage.setItem(`oTest-Status-${testN}`, JSON.stringify(o.tStatus[testN]));
|
|
3066
|
+
}
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
if (o.tFinalized[testN]) return;
|
|
3071
|
+
o.tFinalized[testN] = true;
|
|
2906
3072
|
if (sessionStorage?.getItem("oTest-Run") === testN) {
|
|
2907
|
-
// save test results to sessionStorage
|
|
2908
3073
|
sessionStorage.setItem(`oTest-Log-${testN}`, o.tLog[testN]);
|
|
2909
3074
|
sessionStorage.setItem(`oTest-Res-${testN}`, o.tRes[testN]);
|
|
2910
3075
|
sessionStorage.setItem(`oTest-Status-${testN}`, JSON.stringify(o.tStatus[testN]));
|
|
2911
|
-
|
|
2912
|
-
if (n < o.tests[testN].tests.length) {
|
|
2913
|
-
return;
|
|
2914
|
-
}
|
|
2915
3076
|
}
|
|
2916
3077
|
|
|
2917
3078
|
o.tRes[testN] = !fails;
|
|
@@ -3067,6 +3228,8 @@ o.recorder = {
|
|
|
3067
3228
|
_listeners: [],
|
|
3068
3229
|
_observer: null,
|
|
3069
3230
|
};
|
|
3231
|
+
/** When true, log assertion flow (recording + playback) for debugging. */
|
|
3232
|
+
o.recordingAssertionDebug = false;
|
|
3070
3233
|
|
|
3071
3234
|
/**
|
|
3072
3235
|
* Start recording user interactions
|
|
@@ -3098,6 +3261,7 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
3098
3261
|
|
|
3099
3262
|
rec.observeRoot = observe || null;
|
|
3100
3263
|
rec.assertions = [];
|
|
3264
|
+
rec.removedElements = [];
|
|
3101
3265
|
|
|
3102
3266
|
// snapshot current o.inits data
|
|
3103
3267
|
o.inits.forEach((inst, idx) => {
|
|
@@ -3192,6 +3356,16 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
3192
3356
|
rec._observer = new MutationObserver((mutations) => {
|
|
3193
3357
|
const actionIdx = rec.actions.length - 1;
|
|
3194
3358
|
if (actionIdx < 0) return;
|
|
3359
|
+
const lastAction = rec.actions[actionIdx];
|
|
3360
|
+
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
3361
|
+
console.log("[recording] MutationObserver batch:", {
|
|
3362
|
+
actionIdx,
|
|
3363
|
+
lastAction: lastAction ? { type: lastAction.type, target: lastAction.target } : null,
|
|
3364
|
+
mutationTypes: mutations.map((x) => x.type),
|
|
3365
|
+
addedCount: mutations.reduce((n, x) => n + (x.addedNodes?.length || 0), 0),
|
|
3366
|
+
removedCount: mutations.reduce((n, x) => n + (x.removedNodes?.length || 0), 0),
|
|
3367
|
+
});
|
|
3368
|
+
}
|
|
3195
3369
|
mutations.forEach((m) => {
|
|
3196
3370
|
const addAssertionIndex = (sel, node) => {
|
|
3197
3371
|
let listSelector;
|
|
@@ -3233,16 +3407,57 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
3233
3407
|
)
|
|
3234
3408
|
)
|
|
3235
3409
|
return;
|
|
3236
|
-
// Prefer stable content (e.g. .task-text) so assertions survive reorder/restore
|
|
3237
|
-
const textEl = node.querySelector?.(".task-text") || node;
|
|
3238
3410
|
const text =
|
|
3239
|
-
(
|
|
3240
|
-
undefined;
|
|
3411
|
+
(node.textContent?.trim() || "").slice(0, 80) || undefined;
|
|
3241
3412
|
const { listSelector: aListSel, index: aIdx } = addAssertionIndex(sel, node);
|
|
3242
3413
|
const a = { actionIdx, type: "visible", selector: sel, text };
|
|
3243
3414
|
if (aListSel != null) a.listSelector = aListSel;
|
|
3244
3415
|
if (aIdx != null) a.index = aIdx;
|
|
3245
3416
|
rec.assertions.push(a);
|
|
3417
|
+
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
3418
|
+
console.log("[recording] +visible assertion:", {
|
|
3419
|
+
actionIdx,
|
|
3420
|
+
lastAction: lastAction?.type + " " + lastAction?.target,
|
|
3421
|
+
selector: sel,
|
|
3422
|
+
text: (text || "").slice(0, 40),
|
|
3423
|
+
index: aIdx,
|
|
3424
|
+
listSelector: aListSel,
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
3427
|
+
});
|
|
3428
|
+
m.removedNodes.forEach((node) => {
|
|
3429
|
+
if (node.nodeType !== 1) return;
|
|
3430
|
+
const sel = buildSelector(node);
|
|
3431
|
+
if (!sel) return;
|
|
3432
|
+
const text = (node.textContent?.trim() || "").slice(0, 80) || undefined;
|
|
3433
|
+
const parent = m.target;
|
|
3434
|
+
let index;
|
|
3435
|
+
if (node.previousSibling) {
|
|
3436
|
+
index = Array.from(parent.children).indexOf(node.previousSibling) + 1;
|
|
3437
|
+
} else if (node.nextSibling) {
|
|
3438
|
+
index = Array.from(parent.children).indexOf(node.nextSibling);
|
|
3439
|
+
} else {
|
|
3440
|
+
index = 0;
|
|
3441
|
+
}
|
|
3442
|
+
let listSelector;
|
|
3443
|
+
if (o.autotag && node.dataset?.[o.autotag]) {
|
|
3444
|
+
const qaVal = node.dataset[o.autotag];
|
|
3445
|
+
listSelector = `[data-${o.autotag}="${qaVal}"]`;
|
|
3446
|
+
}
|
|
3447
|
+
const entry = { actionIdx, type: "removed", selector: sel, text };
|
|
3448
|
+
if (listSelector) entry.listSelector = listSelector;
|
|
3449
|
+
entry.index = index;
|
|
3450
|
+
rec.removedElements.push(entry);
|
|
3451
|
+
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
3452
|
+
console.log("[recording] +removed element:", {
|
|
3453
|
+
actionIdx,
|
|
3454
|
+
lastAction: lastAction?.type + " " + lastAction?.target,
|
|
3455
|
+
selector: sel,
|
|
3456
|
+
text: (text || "").slice(0, 40),
|
|
3457
|
+
index,
|
|
3458
|
+
listSelector,
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3246
3461
|
});
|
|
3247
3462
|
}
|
|
3248
3463
|
if (m.type === "attributes") {
|
|
@@ -3264,6 +3479,16 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
3264
3479
|
if (aListSel != null) a.listSelector = aListSel;
|
|
3265
3480
|
if (aIdx != null) a.index = aIdx;
|
|
3266
3481
|
rec.assertions.push(a);
|
|
3482
|
+
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
3483
|
+
console.log("[recording] +class assertion:", {
|
|
3484
|
+
actionIdx,
|
|
3485
|
+
lastAction: lastAction?.type + " " + lastAction?.target,
|
|
3486
|
+
selector: sel,
|
|
3487
|
+
className: m.target.className,
|
|
3488
|
+
index: aIdx,
|
|
3489
|
+
listSelector: aListSel,
|
|
3490
|
+
});
|
|
3491
|
+
}
|
|
3267
3492
|
}
|
|
3268
3493
|
});
|
|
3269
3494
|
});
|
|
@@ -3348,8 +3573,14 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
3348
3573
|
? target?.checked
|
|
3349
3574
|
: undefined;
|
|
3350
3575
|
|
|
3576
|
+
// Push click/change immediately so MutationObserver sees correct actionIdx
|
|
3577
|
+
// (mutations fire sync after target handler; debounce would attach assertions to wrong action)
|
|
3351
3578
|
const delay =
|
|
3352
|
-
|
|
3579
|
+
ev === "click" || ev === "change"
|
|
3580
|
+
? 0
|
|
3581
|
+
: stepDelays[ev] !== undefined
|
|
3582
|
+
? stepDelays[ev]
|
|
3583
|
+
: captureDebounce[ev] ?? 0;
|
|
3353
3584
|
const pushAction = () => {
|
|
3354
3585
|
const action = { type: ev, target: selector, time: Date.now() };
|
|
3355
3586
|
if (targetType) action.targetType = targetType;
|
|
@@ -3397,6 +3628,7 @@ o.stopRecording = () => {
|
|
|
3397
3628
|
initialData: { ...rec.initialData },
|
|
3398
3629
|
stepDelays: { ...rec.stepDelays },
|
|
3399
3630
|
assertions: [...(rec.assertions || [])],
|
|
3631
|
+
removedElements: [...(rec.removedElements || [])],
|
|
3400
3632
|
observeRoot: rec.observeRoot || null,
|
|
3401
3633
|
};
|
|
3402
3634
|
};
|
|
@@ -3419,46 +3651,264 @@ o.clearRecording = (id) => {
|
|
|
3419
3651
|
};
|
|
3420
3652
|
|
|
3421
3653
|
/**
|
|
3422
|
-
*
|
|
3423
|
-
*
|
|
3424
|
-
* @param {
|
|
3425
|
-
* @
|
|
3654
|
+
* Run recording assertions in the current DOM.
|
|
3655
|
+
* @param {{actions: Array, assertions: Array, observeRoot?: string}} recording
|
|
3656
|
+
* @param {Element|string} [root] - Root element or selector; defaults to recording.observeRoot or document.body
|
|
3657
|
+
* @param {number} [actionIdx] - When provided, run only assertions for this action index
|
|
3658
|
+
* @returns {{ passed: number, total: number, failures: Array<{selector: string, message: string}> }}
|
|
3426
3659
|
*/
|
|
3427
|
-
o.
|
|
3428
|
-
const
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3660
|
+
o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
|
|
3661
|
+
const preFiltered = opts && opts.assertions;
|
|
3662
|
+
const assertions =
|
|
3663
|
+
preFiltered != null
|
|
3664
|
+
? preFiltered
|
|
3665
|
+
: (recording.assertions || []).filter(
|
|
3666
|
+
(a) => actionIdx == null || a.actionIdx === actionIdx,
|
|
3667
|
+
);
|
|
3668
|
+
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
3669
|
+
console.log("[runRecordingAssertions] run:", {
|
|
3670
|
+
actionIdx,
|
|
3671
|
+
scope: actionIdx == null ? "teardown (all)" : "per-action",
|
|
3672
|
+
assertionsCount: assertions.length,
|
|
3673
|
+
assertions: assertions.map((a) => ({
|
|
3674
|
+
actionIdx: a.actionIdx,
|
|
3675
|
+
type: a.type,
|
|
3676
|
+
selector: a.selector,
|
|
3677
|
+
index: a.index,
|
|
3678
|
+
text: (a.text || "").slice(0, 40),
|
|
3679
|
+
})),
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
const seen = new Set();
|
|
3683
|
+
const deduped = assertions.filter((a) => {
|
|
3684
|
+
const key = `${a.selector}|${a.type}|${a.actionIdx}|${a.index ?? ""}`;
|
|
3685
|
+
if (seen.has(key)) return false;
|
|
3686
|
+
seen.add(key);
|
|
3687
|
+
return true;
|
|
3688
|
+
});
|
|
3689
|
+
const resolveRoot = () => {
|
|
3690
|
+
if (root != null) {
|
|
3691
|
+
return typeof root === "string" ? o.D.querySelector(root) || o.D.body : root;
|
|
3692
|
+
}
|
|
3693
|
+
const sel = recording.observeRoot;
|
|
3694
|
+
return sel ? o.D.querySelector(sel) || o.D.body : o.D.body;
|
|
3695
|
+
};
|
|
3696
|
+
const r = resolveRoot();
|
|
3697
|
+
const norm = (s) => (s || "").trim().replace(/\s+/g, " ");
|
|
3698
|
+
const getText = (el) => (el ? norm(el.textContent || "") : "");
|
|
3699
|
+
const removedElements = opts?.removedElements || [];
|
|
3700
|
+
const isRemoved = (a) => {
|
|
3701
|
+
if (!removedElements.length || actionIdx == null) return false;
|
|
3702
|
+
const expText = norm(a.text || "");
|
|
3703
|
+
for (const r of removedElements) {
|
|
3704
|
+
if (r.actionIdx > actionIdx) continue;
|
|
3705
|
+
if (norm(r.text || "") !== expText) continue;
|
|
3706
|
+
if (r.selector !== a.selector) continue;
|
|
3707
|
+
if (a.listSelector != null && r.listSelector !== a.listSelector) continue;
|
|
3708
|
+
if (a.index != null && r.index !== a.index) continue;
|
|
3709
|
+
return true;
|
|
3710
|
+
}
|
|
3711
|
+
return false;
|
|
3712
|
+
};
|
|
3713
|
+
let passed = 0;
|
|
3714
|
+
const failures = [];
|
|
3715
|
+
for (const a of deduped) {
|
|
3716
|
+
if (isRemoved(a)) {
|
|
3717
|
+
passed += 1;
|
|
3718
|
+
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
3719
|
+
console.log("[runRecordingAssertions] skip (explicit removed):", {
|
|
3720
|
+
actionIdx: a.actionIdx,
|
|
3721
|
+
selector: a.selector,
|
|
3722
|
+
text: (a.text || "").slice(0, 40),
|
|
3723
|
+
});
|
|
3724
|
+
}
|
|
3725
|
+
continue;
|
|
3726
|
+
}
|
|
3727
|
+
let el = null;
|
|
3728
|
+
let indexOutOfBounds = false;
|
|
3729
|
+
if (a.listSelector != null && a.index != null) {
|
|
3730
|
+
const items = r.querySelectorAll(a.listSelector);
|
|
3731
|
+
const expectedText = norm(a.text || "");
|
|
3732
|
+
const tryItem = (idx) => {
|
|
3733
|
+
const it = items[idx];
|
|
3734
|
+
if (!it) return null;
|
|
3735
|
+
const e =
|
|
3736
|
+
a.selector !== a.listSelector ? it.querySelector(a.selector) : it;
|
|
3737
|
+
return (e || (a.selector !== a.listSelector ? it : null));
|
|
3738
|
+
};
|
|
3739
|
+
let item = items[a.index];
|
|
3740
|
+
if (!item && a.index > 0) item = items[a.index - 1];
|
|
3741
|
+
if (item) {
|
|
3742
|
+
el = tryItem(a.index) || (a.index > 0 ? tryItem(a.index - 1) : null);
|
|
3743
|
+
if (!el && a.selector !== a.listSelector) el = item;
|
|
3744
|
+
if (a.type === "visible" && expectedText && el) {
|
|
3745
|
+
const actualText = getText(el);
|
|
3746
|
+
const textMismatch =
|
|
3747
|
+
actualText.indexOf(expectedText) === -1 &&
|
|
3748
|
+
expectedText.indexOf(actualText) === -1;
|
|
3749
|
+
if (textMismatch) {
|
|
3750
|
+
for (let j = 0; j < items.length; j++) {
|
|
3751
|
+
const candEl = tryItem(j);
|
|
3752
|
+
if (candEl && getText(candEl).indexOf(expectedText) !== -1) {
|
|
3753
|
+
el = candEl;
|
|
3754
|
+
item = items[j];
|
|
3755
|
+
break;
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
} else {
|
|
3761
|
+
indexOutOfBounds = true;
|
|
3762
|
+
}
|
|
3763
|
+
} else {
|
|
3764
|
+
const matches = r.querySelectorAll(a.selector);
|
|
3765
|
+
el = matches.length > 0 ? matches[0] : o.D.querySelector(a.selector);
|
|
3766
|
+
}
|
|
3767
|
+
if (a.type === "visible") {
|
|
3768
|
+
const visible =
|
|
3769
|
+
el &&
|
|
3770
|
+
el.nodeType === 1 &&
|
|
3771
|
+
(el.offsetParent !== null ||
|
|
3772
|
+
(el.getBoundingClientRect && el.getBoundingClientRect().width > 0));
|
|
3773
|
+
const expectedText = norm(a.text || "");
|
|
3774
|
+
const actualText = getText(el);
|
|
3775
|
+
const fullActual = actualText;
|
|
3776
|
+
const textOk =
|
|
3777
|
+
!expectedText ||
|
|
3778
|
+
actualText.indexOf(expectedText) !== -1 ||
|
|
3779
|
+
fullActual.indexOf(expectedText) !== -1 ||
|
|
3780
|
+
(expectedText.length > 0 && expectedText.indexOf(actualText) !== -1);
|
|
3781
|
+
if (visible && textOk) {
|
|
3782
|
+
passed += 1;
|
|
3438
3783
|
} else {
|
|
3439
|
-
const
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3784
|
+
const message = indexOutOfBounds
|
|
3785
|
+
? `index out of bounds (list has ${r.querySelectorAll(a.listSelector || a.selector).length} items, assertion expected index ${a.index})`
|
|
3786
|
+
: !el
|
|
3787
|
+
? "element not found"
|
|
3788
|
+
: !visible
|
|
3789
|
+
? "not visible"
|
|
3790
|
+
: !textOk
|
|
3791
|
+
? "text mismatch"
|
|
3792
|
+
: "fail";
|
|
3793
|
+
failures.push({ selector: a.selector, message });
|
|
3794
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
3795
|
+
console.warn("[runRecordingAssertions] visible failed:", {
|
|
3796
|
+
actionIdx: a.actionIdx,
|
|
3797
|
+
selector: a.selector,
|
|
3798
|
+
listSelector: a.listSelector,
|
|
3799
|
+
index: a.index,
|
|
3800
|
+
expectedText: a.text || "(any)",
|
|
3801
|
+
actualText: actualText.slice(0, 80),
|
|
3802
|
+
message,
|
|
3803
|
+
});
|
|
3804
|
+
}
|
|
3443
3805
|
}
|
|
3806
|
+
} else if (a.type === "class") {
|
|
3807
|
+
const tokens = (a.className || "").trim().split(/\s+/).filter(Boolean);
|
|
3808
|
+
const hasClass =
|
|
3809
|
+
el && (tokens.length === 0 || tokens.every((c) => el.classList?.contains(c)));
|
|
3810
|
+
if (hasClass) {
|
|
3811
|
+
passed += 1;
|
|
3812
|
+
} else {
|
|
3813
|
+
const msg = indexOutOfBounds
|
|
3814
|
+
? `index out of bounds (list has ${r.querySelectorAll(a.listSelector).length} items, expected index ${a.index})`
|
|
3815
|
+
: !el
|
|
3816
|
+
? "element not found"
|
|
3817
|
+
: `expected class "${a.className}"`;
|
|
3818
|
+
failures.push({ selector: a.selector, message: msg });
|
|
3819
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
3820
|
+
console.warn("[runRecordingAssertions] failed:", {
|
|
3821
|
+
type: a.type,
|
|
3822
|
+
selector: a.selector,
|
|
3823
|
+
actionIdx: a.actionIdx,
|
|
3824
|
+
listSelector: a.listSelector,
|
|
3825
|
+
index: a.index,
|
|
3826
|
+
itemsInRoot: a.listSelector ? r.querySelectorAll(a.listSelector).length : "-",
|
|
3827
|
+
message: msg,
|
|
3828
|
+
});
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
return { passed, total: deduped.length, failures };
|
|
3834
|
+
};
|
|
3835
|
+
|
|
3836
|
+
/**
|
|
3837
|
+
* Export a recording as a ready-to-commit o.addTest() code string.
|
|
3838
|
+
* Includes assertions interleaved with actions (Playwright parity).
|
|
3839
|
+
* @param {{actions: Array, assertions: Array, mocks: Object, initialData: Object, observeRoot?: string}} recording
|
|
3840
|
+
* @param {{delay?: number}} [options] - delay in ms at end of each action (default 16 for recorded actions)
|
|
3841
|
+
* @returns {string}
|
|
3842
|
+
*/
|
|
3843
|
+
o.exportTest = (recording, options = {}) => {
|
|
3844
|
+
const delay = options.delay !== undefined ? options.delay : 16;
|
|
3845
|
+
const recordingData = {
|
|
3846
|
+
actions: recording.actions,
|
|
3847
|
+
assertions: recording.assertions || [],
|
|
3848
|
+
observeRoot: recording.observeRoot || null,
|
|
3849
|
+
};
|
|
3850
|
+
const rootVar = recording.observeRoot
|
|
3851
|
+
? `(o.D.querySelector('${recording.observeRoot.replace(/'/g, "\\'")}') || o.D.body)`
|
|
3852
|
+
: "o.D.body";
|
|
3853
|
+
const getEl = (a) => {
|
|
3854
|
+
if (a.listSelector != null && a.targetIndex != null) {
|
|
3855
|
+
const listSel = JSON.stringify(a.listSelector);
|
|
3856
|
+
const useItem = a.target === a.listSelector;
|
|
3857
|
+
const targetSel = useItem ? listSel : JSON.stringify(a.target);
|
|
3444
3858
|
return (
|
|
3445
|
-
`
|
|
3446
|
-
` const
|
|
3447
|
-
`
|
|
3859
|
+
` const items = o.D.querySelectorAll(${listSel});\n` +
|
|
3860
|
+
` const item = items[${a.targetIndex}];\n` +
|
|
3861
|
+
` let el = null;\n` +
|
|
3862
|
+
` if (item) { el = ${useItem ? "item" : `item.querySelector(${targetSel}) || item`}; }`
|
|
3863
|
+
);
|
|
3864
|
+
}
|
|
3865
|
+
return ` const el = o.D.querySelector(${JSON.stringify(a.target)});`;
|
|
3866
|
+
};
|
|
3867
|
+
const endSuffix = delay > 0 ? `\n await o.sleep(${delay});\n return true;\n` : ` return true;\n`;
|
|
3868
|
+
const stepFn = delay > 0 ? "async () =>" : "() =>";
|
|
3869
|
+
const steps = [];
|
|
3870
|
+
for (let i = 0; i < recording.actions.length; i++) {
|
|
3871
|
+
const a = recording.actions[i];
|
|
3872
|
+
let body;
|
|
3873
|
+
if (a.type === "scroll") {
|
|
3874
|
+
body = ` window.scrollTo(0, ${a.scrollY || 0});${endSuffix}`;
|
|
3875
|
+
} else if (a.type === "input" || a.type === "change") {
|
|
3876
|
+
body =
|
|
3877
|
+
(a.value !== undefined ? ` el.value = ${JSON.stringify(a.value)};\n` : "") +
|
|
3878
|
+
(a.checked !== undefined ? ` el.checked = ${a.checked};\n` : "") +
|
|
3879
|
+
` el.dispatchEvent(new Event('${a.type}', {bubbles:true}));${endSuffix}`;
|
|
3880
|
+
} else {
|
|
3881
|
+
const useNativeClick = a.type === "click";
|
|
3882
|
+
body = useNativeClick
|
|
3883
|
+
? ` el.click();${endSuffix}`
|
|
3884
|
+
: ` el.dispatchEvent(new MouseEvent('${a.type}', {bubbles:true,cancelable:true}));${endSuffix}`;
|
|
3885
|
+
}
|
|
3886
|
+
steps.push(
|
|
3887
|
+
` ['${a.type} on ${a.target}', ${stepFn} {\n` +
|
|
3888
|
+
getEl(a) +
|
|
3889
|
+
`\n if (!el && '${a.type}' !== 'scroll') return 'element not found: ${a.target.replace(/'/g, "\\'")}';\n` +
|
|
3448
3890
|
body +
|
|
3449
|
-
` }]
|
|
3891
|
+
` }]`,
|
|
3892
|
+
);
|
|
3893
|
+
const assertsForAction = (recording.assertions || []).filter((x) => x.actionIdx === i);
|
|
3894
|
+
if (assertsForAction.length > 0) {
|
|
3895
|
+
steps.push(
|
|
3896
|
+
` ['assert after ${a.type}', () => {\n` +
|
|
3897
|
+
` const r = o.runRecordingAssertions(recordingData, ${rootVar}, ${i});\n` +
|
|
3898
|
+
` return r.passed === r.total ? true : r.failures.map(f => f.selector + ': ' + f.message).join('; ');\n` +
|
|
3899
|
+
` }]`,
|
|
3450
3900
|
);
|
|
3451
|
-
}
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
const mocksStr = Object.keys(recording.mocks).length
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
const mocksStr = Object.keys(recording.mocks || {}).length
|
|
3455
3904
|
? JSON.stringify(recording.mocks, null, 2)
|
|
3456
3905
|
: "{}";
|
|
3457
3906
|
|
|
3458
3907
|
return (
|
|
3459
3908
|
`// Auto-generated by o.exportTest() — review and anonymize mocks before committing\n` +
|
|
3460
|
-
`const recordingMocks = ${mocksStr};\n
|
|
3461
|
-
`
|
|
3909
|
+
`const recordingMocks = ${mocksStr};\n` +
|
|
3910
|
+
`const recordingData = { actions: ${JSON.stringify(recording.actions)}, assertions: ${JSON.stringify(recording.assertions || [])}, observeRoot: ${JSON.stringify(recording.observeRoot || null)} };\n\n` +
|
|
3911
|
+
`o.addTest('Recorded test', [\n${steps.join(",\n")}\n // Add manual checks: ['Manual: label', () => o.testConfirm('label', ['item1'])],\n], () => {\n` +
|
|
3462
3912
|
` // teardown\n});\n`
|
|
3463
3913
|
);
|
|
3464
3914
|
};
|
|
@@ -3591,13 +4041,25 @@ o.exportPlaywrightTest = (recording, options = {}) => {
|
|
|
3591
4041
|
// Available in all builds so assessors can replay and see results (testOverlay) on staging.
|
|
3592
4042
|
/**
|
|
3593
4043
|
* Play back a recording as an automated test sequence
|
|
3594
|
-
* @param {{actions: Array, mocks: Object}} recording
|
|
3595
|
-
* @param {Object} [
|
|
3596
|
-
* @returns {number}
|
|
4044
|
+
* @param {{actions: Array, mocks: Object, assertions?: Array, observeRoot?: string}} recording
|
|
4045
|
+
* @param {Object} [opts] - mockOverrides or { runAssertions?, root?, manualChecks?, mockOverrides? }
|
|
4046
|
+
* @returns {number|{testId: number, assertionResult?: Object}}
|
|
3597
4047
|
*/
|
|
3598
|
-
o.playRecording = (recording,
|
|
4048
|
+
o.playRecording = (recording, opts = {}) => {
|
|
4049
|
+
const isOptions =
|
|
4050
|
+
opts &&
|
|
4051
|
+
typeof opts === "object" &&
|
|
4052
|
+
(opts.runAssertions !== undefined ||
|
|
4053
|
+
opts.root !== undefined ||
|
|
4054
|
+
opts.manualChecks !== undefined ||
|
|
4055
|
+
opts.actionDelay !== undefined);
|
|
4056
|
+
const mockOverrides = isOptions ? opts.mockOverrides || {} : opts;
|
|
4057
|
+
const runAssertions = isOptions && opts.runAssertions;
|
|
4058
|
+
const rootOpt = isOptions ? opts.root : undefined;
|
|
4059
|
+
const manualChecks = (isOptions && opts.manualChecks) || [];
|
|
4060
|
+
const actionDelay = isOptions && opts.actionDelay !== undefined ? opts.actionDelay : 16;
|
|
4061
|
+
|
|
3599
4062
|
const allMocks = Object.assign({}, recording.mocks, mockOverrides);
|
|
3600
|
-
// install mock fetch
|
|
3601
4063
|
const origFetch = window.fetch;
|
|
3602
4064
|
window.fetch = (url, opts = {}) => {
|
|
3603
4065
|
const method = (opts.method || "GET").toUpperCase();
|
|
@@ -3611,51 +4073,154 @@ o.playRecording = (recording, mockOverrides = {}) => {
|
|
|
3611
4073
|
return origFetch(url, opts);
|
|
3612
4074
|
};
|
|
3613
4075
|
|
|
3614
|
-
const
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
4076
|
+
const resolveRoot = () => {
|
|
4077
|
+
if (rootOpt != null) {
|
|
4078
|
+
return typeof rootOpt === "string" ? o.D.querySelector(rootOpt) || o.D.body : rootOpt;
|
|
4079
|
+
}
|
|
4080
|
+
const sel = recording.observeRoot;
|
|
4081
|
+
return sel ? o.D.querySelector(sel) || o.D.body : o.D.body;
|
|
4082
|
+
};
|
|
4083
|
+
const rootEl = runAssertions ? resolveRoot() : null;
|
|
4084
|
+
const actionScope = rootOpt != null ? resolveRoot() : o.D;
|
|
4085
|
+
|
|
4086
|
+
const actions = recording.actions;
|
|
4087
|
+
const assertions = recording.assertions || [];
|
|
4088
|
+
|
|
4089
|
+
const assertionsByAction = {};
|
|
4090
|
+
for (const a of assertions) {
|
|
4091
|
+
const k = a.actionIdx;
|
|
4092
|
+
if (!assertionsByAction[k]) assertionsByAction[k] = [];
|
|
4093
|
+
assertionsByAction[k].push(a);
|
|
4094
|
+
}
|
|
4095
|
+
if (o.recordingAssertionDebug && runAssertions && typeof console !== "undefined" && console.log) {
|
|
4096
|
+
const summary = actions.map((act, i) => ({
|
|
4097
|
+
i,
|
|
4098
|
+
action: act.type + " " + (act.target || ""),
|
|
4099
|
+
assertions: (assertionsByAction[i] || []).length,
|
|
4100
|
+
assertionDetails: (assertionsByAction[i] || []).map((x) => ({
|
|
4101
|
+
type: x.type,
|
|
4102
|
+
index: x.index,
|
|
4103
|
+
text: (x.text || "").slice(0, 30),
|
|
4104
|
+
})),
|
|
4105
|
+
}));
|
|
4106
|
+
console.log("[playRecording] assertions by action:", summary);
|
|
4107
|
+
}
|
|
4108
|
+
const manualByAction = {};
|
|
4109
|
+
for (const mc of manualChecks) {
|
|
4110
|
+
const k = mc.afterAction;
|
|
4111
|
+
if (!manualByAction[k]) manualByAction[k] = [];
|
|
4112
|
+
manualByAction[k].push(mc);
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
const testCases = [];
|
|
4116
|
+
let assertionAccum = { passed: 0, total: 0, failures: [] };
|
|
4117
|
+
|
|
4118
|
+
for (let i = 0; i < actions.length; i++) {
|
|
4119
|
+
const action = actions[i];
|
|
4120
|
+
testCases.push([
|
|
4121
|
+
`${action.type} on ${action.target}`,
|
|
4122
|
+
async () => {
|
|
4123
|
+
let el = null;
|
|
4124
|
+
const scope = actionScope;
|
|
4125
|
+
if (action.target) {
|
|
4126
|
+
if (action.listSelector != null && action.targetIndex != null) {
|
|
4127
|
+
const items = scope.querySelectorAll(action.listSelector);
|
|
4128
|
+
const item = items[action.targetIndex];
|
|
4129
|
+
if (item) {
|
|
4130
|
+
el =
|
|
4131
|
+
action.target !== action.listSelector
|
|
4132
|
+
? item.querySelector(action.target)
|
|
4133
|
+
: item;
|
|
4134
|
+
if (!el && action.target !== action.listSelector) el = item;
|
|
4135
|
+
}
|
|
4136
|
+
} else {
|
|
4137
|
+
el = scope.querySelector(action.target);
|
|
3628
4138
|
}
|
|
3629
|
-
} else {
|
|
3630
|
-
el = o.D.querySelector(action.target);
|
|
3631
4139
|
}
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
el.dispatchEvent(new Event(action.type, { bubbles: true }));
|
|
3642
|
-
} else {
|
|
3643
|
-
if (action.type === "click") {
|
|
3644
|
-
el.click();
|
|
4140
|
+
if (!el && action.type !== "scroll") {
|
|
4141
|
+
return `element not found: ${action.target}`;
|
|
4142
|
+
}
|
|
4143
|
+
if (action.type === "scroll") {
|
|
4144
|
+
window.scrollTo(0, action.scrollY || 0);
|
|
4145
|
+
} else if (action.type === "input" || action.type === "change") {
|
|
4146
|
+
if (action.value !== undefined) el.value = action.value;
|
|
4147
|
+
if (action.checked !== undefined) el.checked = action.checked;
|
|
4148
|
+
el.dispatchEvent(new Event(action.type, { bubbles: true }));
|
|
3645
4149
|
} else {
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
4150
|
+
if (action.type === "click") {
|
|
4151
|
+
el.click();
|
|
4152
|
+
} else {
|
|
4153
|
+
el.dispatchEvent(
|
|
4154
|
+
new MouseEvent(action.type, { bubbles: true, cancelable: true }),
|
|
4155
|
+
);
|
|
4156
|
+
}
|
|
3649
4157
|
}
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
4158
|
+
if (actionDelay > 0) await o.sleep(actionDelay);
|
|
4159
|
+
return true;
|
|
4160
|
+
},
|
|
4161
|
+
]);
|
|
4162
|
+
const asserted = assertionsByAction[i];
|
|
4163
|
+
if (runAssertions && asserted && asserted.length > 0) {
|
|
4164
|
+
testCases.push([
|
|
4165
|
+
`assert after ${action.type}`,
|
|
4166
|
+
() =>
|
|
4167
|
+
new Promise((resolve) => {
|
|
4168
|
+
const run = () => {
|
|
4169
|
+
const r = o.runRecordingAssertions(recording, rootEl, i, {
|
|
4170
|
+
assertions: asserted,
|
|
4171
|
+
removedElements: recording.removedElements,
|
|
4172
|
+
});
|
|
4173
|
+
assertionAccum.passed += r.passed;
|
|
4174
|
+
assertionAccum.total += r.total;
|
|
4175
|
+
assertionAccum.failures.push(...r.failures);
|
|
4176
|
+
resolve(
|
|
4177
|
+
r.passed === r.total
|
|
4178
|
+
? true
|
|
4179
|
+
: r.failures.map((f) => f.selector + ": " + f.message).join("; "),
|
|
4180
|
+
);
|
|
4181
|
+
};
|
|
4182
|
+
requestAnimationFrame(() => requestAnimationFrame(run));
|
|
4183
|
+
}),
|
|
4184
|
+
]);
|
|
4185
|
+
}
|
|
4186
|
+
for (const mc of manualByAction[i] || []) {
|
|
4187
|
+
testCases.push([
|
|
4188
|
+
`Manual: ${mc.label}`,
|
|
4189
|
+
() =>
|
|
4190
|
+
typeof o.testConfirm === "function"
|
|
4191
|
+
? o.testConfirm(mc.label, mc.items || [])
|
|
4192
|
+
: { ok: true },
|
|
4193
|
+
]);
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
for (const mc of manualByAction["end"] || []) {
|
|
4197
|
+
testCases.push([
|
|
4198
|
+
`Manual: ${mc.label}`,
|
|
4199
|
+
() =>
|
|
4200
|
+
typeof o.testConfirm === "function"
|
|
4201
|
+
? o.testConfirm(mc.label, mc.items || [])
|
|
4202
|
+
: { ok: true },
|
|
4203
|
+
]);
|
|
4204
|
+
}
|
|
3654
4205
|
|
|
3655
|
-
const
|
|
4206
|
+
const onComplete = isOptions && opts.onComplete;
|
|
4207
|
+
const testId = o.test("Recorded playback", ...testCases, { sync: true }, (testId) => {
|
|
3656
4208
|
window.fetch = origFetch;
|
|
4209
|
+
const assertionResult = runAssertions && assertions.length > 0 ? assertionAccum : undefined;
|
|
4210
|
+
if (assertionResult?.failures?.length > 0) {
|
|
4211
|
+
o.tRes[testId] = false;
|
|
4212
|
+
const failLines = assertionResult.failures
|
|
4213
|
+
.map((f) => `${f.selector}: ${f.message}`)
|
|
4214
|
+
.join("; ");
|
|
4215
|
+
const suffix = o.tStyled
|
|
4216
|
+
? o.tPre + o.tXx + "Assertions failed: " + failLines + o.tDc
|
|
4217
|
+
: "\n✘ Assertions failed: " + failLines;
|
|
4218
|
+
o.tLog[testId] = (o.tLog[testId] || "") + suffix;
|
|
4219
|
+
}
|
|
4220
|
+
if (typeof onComplete === "function") onComplete(assertionResult);
|
|
3657
4221
|
});
|
|
3658
|
-
|
|
4222
|
+
|
|
4223
|
+
return runAssertions ? { testId } : testId;
|
|
3659
4224
|
};
|
|
3660
4225
|
|
|
3661
4226
|
// ─── Test results overlay (all builds — for assessors to see auto + manual results) ───
|
|
@@ -3694,6 +4259,78 @@ o.testOverlay = () => {
|
|
|
3694
4259
|
});
|
|
3695
4260
|
};
|
|
3696
4261
|
|
|
4262
|
+
const innerHTML =
|
|
4263
|
+
`<div style="display:flex;align-items:center;gap:12px;">` +
|
|
4264
|
+
`<span class="o-test-overlay-summary" style="flex:1;font-size:13px;cursor:grab;">Tests: 0/0</span>` +
|
|
4265
|
+
`<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>` +
|
|
4266
|
+
`<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">×</button>` +
|
|
4267
|
+
`</div>` +
|
|
4268
|
+
`<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>`;
|
|
4269
|
+
const box = o.overlay({
|
|
4270
|
+
innerHTML,
|
|
4271
|
+
removeExisting: false,
|
|
4272
|
+
className: "o-test-overlay",
|
|
4273
|
+
id: btnId,
|
|
4274
|
+
excludeDragSelector: ".o-test-overlay-close, .o-test-overlay-toggle, #" + panelId,
|
|
4275
|
+
});
|
|
4276
|
+
|
|
4277
|
+
const refreshSummary = () => {
|
|
4278
|
+
const summary = o(".o-test-overlay-summary");
|
|
4279
|
+
if (summary.els.length)
|
|
4280
|
+
summary.innerText(`Tests: ${o.tRes.filter(Boolean).length}/${o.tRes.length}`);
|
|
4281
|
+
};
|
|
4282
|
+
|
|
4283
|
+
box.first(".o-test-overlay-toggle").on("click", () => {
|
|
4284
|
+
const panel = o("#" + panelId);
|
|
4285
|
+
if (!panel.el) return;
|
|
4286
|
+
const isOpen = panel.el.style.display !== "none";
|
|
4287
|
+
panel.css({ display: isOpen ? "none" : "block" });
|
|
4288
|
+
if (!isOpen) updatePanel();
|
|
4289
|
+
});
|
|
4290
|
+
|
|
4291
|
+
box.first(".o-test-overlay-close").on("click", () => {
|
|
4292
|
+
box._overlayCleanup();
|
|
4293
|
+
});
|
|
4294
|
+
|
|
4295
|
+
o.testOverlay.showPanel = () => {
|
|
4296
|
+
const panel = o("#" + panelId);
|
|
4297
|
+
if (!panel.el) return;
|
|
4298
|
+
panel.css({ display: "block" });
|
|
4299
|
+
updatePanel();
|
|
4300
|
+
refreshSummary();
|
|
4301
|
+
};
|
|
4302
|
+
|
|
4303
|
+
if (!o._testOverlayBase) o._testOverlayBase = o.test;
|
|
4304
|
+
o.test = (...args) => {
|
|
4305
|
+
const id = o._testOverlayBase(...args);
|
|
4306
|
+
const origFn = o.tFns[id];
|
|
4307
|
+
o.tFns[id] = (n) => {
|
|
4308
|
+
if (typeof origFn === "function") origFn(n);
|
|
4309
|
+
const panel = o("#" + panelId);
|
|
4310
|
+
if (panel.el && panel.el.style.display !== "none") updatePanel();
|
|
4311
|
+
refreshSummary();
|
|
4312
|
+
};
|
|
4313
|
+
return id;
|
|
4314
|
+
};
|
|
4315
|
+
};
|
|
4316
|
+
|
|
4317
|
+
/**
|
|
4318
|
+
* Common draggable overlay — shared by testConfirm, testOverlay, confirmOnFailure.
|
|
4319
|
+
* @param {{ innerHTML: string, onClose?: (result?: any) => void, timeout?: number, excludeDragSelector?: string }} opts
|
|
4320
|
+
* @returns {Object} box instance (Objs element)
|
|
4321
|
+
*/
|
|
4322
|
+
o.overlay = (opts = {}) => {
|
|
4323
|
+
const {
|
|
4324
|
+
innerHTML,
|
|
4325
|
+
onClose,
|
|
4326
|
+
timeout,
|
|
4327
|
+
excludeDragSelector,
|
|
4328
|
+
removeExisting = true,
|
|
4329
|
+
className = "o-overlay-common",
|
|
4330
|
+
id,
|
|
4331
|
+
} = opts;
|
|
4332
|
+
if (removeExisting) o("." + className).remove();
|
|
4333
|
+
else if (id && o("#" + id).el) return o("#" + id);
|
|
3697
4334
|
const overlayStyle = {
|
|
3698
4335
|
position: "fixed",
|
|
3699
4336
|
left: "50%",
|
|
@@ -3703,31 +4340,27 @@ o.testOverlay = () => {
|
|
|
3703
4340
|
width: "fit-content",
|
|
3704
4341
|
"max-width": "min(90vw, 420px)",
|
|
3705
4342
|
"font-family": "system-ui,sans-serif",
|
|
3706
|
-
cursor: "grab",
|
|
3707
4343
|
"user-select": "text",
|
|
3708
4344
|
};
|
|
3709
|
-
|
|
4345
|
+
const countdownId = "o-overlay-countdown";
|
|
4346
|
+
const barHtml =
|
|
4347
|
+
`<div class="o-overlay-bar" style="display:flex;flex-direction:column;align-items:stretch;padding:10px 14px;background:#1e293b;color:#e2e8f0;border:1px solid #334155;border-radius:8px;font-size:14px;min-width:200px;max-height:90vh;overflow-y:auto;">` +
|
|
4348
|
+
innerHTML +
|
|
4349
|
+
(timeout
|
|
4350
|
+
? `<div id="${countdownId}" style="margin-top:6px;font-size:11px;color:#94a3b8;"></div>`
|
|
4351
|
+
: "") +
|
|
4352
|
+
"</div>";
|
|
3710
4353
|
const box = o
|
|
3711
4354
|
.initState({
|
|
3712
4355
|
tag: "div",
|
|
3713
|
-
|
|
3714
|
-
|
|
4356
|
+
className,
|
|
4357
|
+
id: id || undefined,
|
|
3715
4358
|
style:
|
|
3716
|
-
"position:fixed;left:50%;bottom:50px;transform:translateX(-50%);z-index:999999;width:fit-content;max-width:min(90vw,420px);font-family:system-ui,sans-serif;
|
|
3717
|
-
html:
|
|
3718
|
-
`<div class="o-test-overlay-bar" style="display:flex;flex-direction:column;align-items:stretch;padding:10px 14px;background:#1e293b;color:#e2e8f0;border:1px solid #334155;border-radius:8px;font-size:14px;cursor:grab;min-width:200px;">` +
|
|
3719
|
-
`<div style="display:flex;align-items:center;gap:12px;">` +
|
|
3720
|
-
`<span class="o-test-overlay-summary" style="flex:1;font-size:13px;">Tests: 0/0</span>` +
|
|
3721
|
-
`<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>` +
|
|
3722
|
-
`<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">×</button>` +
|
|
3723
|
-
`</div></div>` +
|
|
3724
|
-
`<div id="${panelId}" style="display:none;margin-top:4px;padding:8px;background:#fff;border:1px solid #334155;border-radius:6px;max-height:60vh;overflow-y:auto;box-shadow:0 2px 8px rgba(0,0,0,.15);font-size:11px;user-select:text;cursor:text;"></div>`,
|
|
4359
|
+
"position:fixed;left:50%;bottom:50px;transform:translateX(-50%);z-index:999999;width:fit-content;max-width:min(90vw,420px);font-family:system-ui,sans-serif;user-select:text;",
|
|
4360
|
+
html: barHtml,
|
|
3725
4361
|
})
|
|
3726
4362
|
.appendInside("body");
|
|
3727
|
-
|
|
3728
|
-
const applyOverlayStyle = () => {
|
|
3729
|
-
box.css(overlayStyle);
|
|
3730
|
-
};
|
|
4363
|
+
const applyStyle = () => box.css(overlayStyle);
|
|
3731
4364
|
let drag = null;
|
|
3732
4365
|
const onMove = (e) => {
|
|
3733
4366
|
if (!drag) return;
|
|
@@ -3735,71 +4368,49 @@ o.testOverlay = () => {
|
|
|
3735
4368
|
overlayStyle.top = drag.top + (e.clientY - drag.startY) + "px";
|
|
3736
4369
|
delete overlayStyle.bottom;
|
|
3737
4370
|
overlayStyle.transform = "none";
|
|
3738
|
-
|
|
4371
|
+
applyStyle();
|
|
3739
4372
|
};
|
|
3740
4373
|
const onUp = () => {
|
|
3741
4374
|
if (drag) {
|
|
3742
|
-
overlayStyle.cursor
|
|
3743
|
-
|
|
4375
|
+
delete overlayStyle.cursor;
|
|
4376
|
+
applyStyle();
|
|
3744
4377
|
}
|
|
3745
4378
|
drag = null;
|
|
3746
4379
|
};
|
|
3747
4380
|
box.on("mousedown", (e) => {
|
|
3748
|
-
if (
|
|
3749
|
-
e.target.closest(".o-test-overlay-close") ||
|
|
3750
|
-
e.target.closest(".o-test-overlay-toggle") ||
|
|
3751
|
-
e.target.closest("#" + panelId)
|
|
3752
|
-
)
|
|
3753
|
-
return;
|
|
4381
|
+
if (excludeDragSelector && e.target.closest(excludeDragSelector)) return;
|
|
3754
4382
|
const r = box.el.getBoundingClientRect();
|
|
3755
4383
|
drag = { startX: e.clientX, startY: e.clientY, left: r.left, top: r.top };
|
|
3756
4384
|
overlayStyle.cursor = "grabbing";
|
|
3757
|
-
|
|
4385
|
+
applyStyle();
|
|
3758
4386
|
});
|
|
3759
4387
|
o.D.addEventListener("mousemove", onMove);
|
|
3760
4388
|
o.D.addEventListener("mouseup", onUp);
|
|
3761
|
-
|
|
3762
|
-
const
|
|
3763
|
-
const summary = o(".o-test-overlay-summary");
|
|
3764
|
-
if (summary.els.length)
|
|
3765
|
-
summary.innerText(`Tests: ${o.tRes.filter(Boolean).length}/${o.tRes.length}`);
|
|
3766
|
-
};
|
|
3767
|
-
|
|
3768
|
-
box.first(".o-test-overlay-toggle").on("click", () => {
|
|
3769
|
-
const panel = o("#" + panelId);
|
|
3770
|
-
if (!panel.el) return;
|
|
3771
|
-
const isOpen = panel.el.style.display !== "none";
|
|
3772
|
-
panel.css({ display: isOpen ? "none" : "block" });
|
|
3773
|
-
if (!isOpen) updatePanel();
|
|
3774
|
-
});
|
|
3775
|
-
|
|
3776
|
-
box.first(".o-test-overlay-close").on("click", () => {
|
|
4389
|
+
let timerId;
|
|
4390
|
+
const cleanup = () => {
|
|
3777
4391
|
o.D.removeEventListener("mousemove", onMove);
|
|
3778
4392
|
o.D.removeEventListener("mouseup", onUp);
|
|
4393
|
+
if (timerId) clearInterval(timerId);
|
|
3779
4394
|
box.remove();
|
|
3780
|
-
});
|
|
3781
|
-
|
|
3782
|
-
o.testOverlay.showPanel = () => {
|
|
3783
|
-
const panel = o("#" + panelId);
|
|
3784
|
-
if (!panel.el) return;
|
|
3785
|
-
panel.css({ display: "block" });
|
|
3786
|
-
updatePanel();
|
|
3787
|
-
refreshSummary();
|
|
3788
|
-
};
|
|
3789
|
-
|
|
3790
|
-
// Single patch of o.test to refresh panel when tests complete (use base so we don't stack)
|
|
3791
|
-
if (!o._testOverlayBase) o._testOverlayBase = o.test;
|
|
3792
|
-
o.test = (...args) => {
|
|
3793
|
-
const id = o._testOverlayBase(...args);
|
|
3794
|
-
const origFn = o.tFns[id];
|
|
3795
|
-
o.tFns[id] = (n) => {
|
|
3796
|
-
if (typeof origFn === "function") origFn(n);
|
|
3797
|
-
const panel = o("#" + panelId);
|
|
3798
|
-
if (panel.el && panel.el.style.display !== "none") updatePanel();
|
|
3799
|
-
refreshSummary();
|
|
3800
|
-
};
|
|
3801
|
-
return id;
|
|
3802
4395
|
};
|
|
4396
|
+
if (timeout && timeout > 0) {
|
|
4397
|
+
let remaining = Math.ceil(timeout / 1000);
|
|
4398
|
+
const cd = o("#" + countdownId);
|
|
4399
|
+
if (cd.el) cd.el.textContent = remaining ? `Continue in ${remaining}s` : "";
|
|
4400
|
+
timerId = setInterval(() => {
|
|
4401
|
+
remaining -= 1;
|
|
4402
|
+
if (cd.el) cd.el.textContent = remaining > 0 ? `Continue in ${remaining}s` : "";
|
|
4403
|
+
if (remaining <= 0) {
|
|
4404
|
+
clearInterval(timerId);
|
|
4405
|
+
timerId = null;
|
|
4406
|
+
cleanup();
|
|
4407
|
+
if (typeof onClose === "function") onClose({ ok: false, errors: ["timeout"] });
|
|
4408
|
+
}
|
|
4409
|
+
}, 1000);
|
|
4410
|
+
}
|
|
4411
|
+
box._overlayCleanup = cleanup;
|
|
4412
|
+
box._overlayOnClose = onClose;
|
|
4413
|
+
return box;
|
|
3803
4414
|
};
|
|
3804
4415
|
|
|
3805
4416
|
/**
|
|
@@ -3807,12 +4418,11 @@ o.testOverlay = () => {
|
|
|
3807
4418
|
* Only available in dev builds. NOT referenced in exportPlaywrightTest.
|
|
3808
4419
|
* @param {string} label - Test title (shown as "Test title: Paused")
|
|
3809
4420
|
* @param {string[]} [items] - Optional checklist for the operator (e.g. hover effects to verify); use labels so clicking text toggles checkbox
|
|
3810
|
-
* @param {{ confirm?: string }} [opts] - Continue button label (default "Continue")
|
|
4421
|
+
* @param {{ confirm?: string, timeout?: number }} [opts] - Continue button label (default "Continue"); timeout in ms for countdown
|
|
3811
4422
|
* @returns {Promise<{ ok: boolean, errors?: string[] }>} ok true if all items checked; errors = list of unchecked item texts when ok false
|
|
3812
4423
|
*/
|
|
3813
4424
|
o.testConfirm = (label, items = [], opts = {}) =>
|
|
3814
4425
|
new Promise((resolve) => {
|
|
3815
|
-
o(".o-tc-overlay").remove();
|
|
3816
4426
|
const btnLabel = opts.confirm || "Continue";
|
|
3817
4427
|
const hasCheckboxes = items.length > 0;
|
|
3818
4428
|
const btnBg = hasCheckboxes ? "#dc2626" : "#2563eb";
|
|
@@ -3821,7 +4431,7 @@ o.testConfirm = (label, items = [], opts = {}) =>
|
|
|
3821
4431
|
".o-tc-item-cb{appearance:none;-webkit-appearance:none;width:16px;height:16px;border:2px solid #ef4444;border-radius:3px;background:#fef2f2;flex-shrink:0;cursor:pointer;}" +
|
|
3822
4432
|
".o-tc-item-cb:checked{border-color:#22c55e;background:#22c55e;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E\");background-size:12px 12px;background-position:center;}";
|
|
3823
4433
|
const itemsHtml = hasCheckboxes
|
|
3824
|
-
? `<style>${checkboxStyle}</style><ul class="o-tc-list" style="margin:8px 0 0;padding:0;list-style:none;font-size:13px;color:#cbd5e1;">` +
|
|
4434
|
+
? `<style>${checkboxStyle}</style><ul class="o-tc-list" style="margin:8px 0 0;padding:0;list-style:none;font-size:13px;color:#cbd5e1;cursor:grab;">` +
|
|
3825
4435
|
items
|
|
3826
4436
|
.map(
|
|
3827
4437
|
(i, idx) =>
|
|
@@ -3830,23 +4440,18 @@ o.testConfirm = (label, items = [], opts = {}) =>
|
|
|
3830
4440
|
.join("") +
|
|
3831
4441
|
"</ul>"
|
|
3832
4442
|
: "";
|
|
3833
|
-
const
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
itemsHtml +
|
|
3846
|
-
`</div>`,
|
|
3847
|
-
})
|
|
3848
|
-
.appendInside("body");
|
|
3849
|
-
|
|
4443
|
+
const innerHTML =
|
|
4444
|
+
`<div style="display:flex;align-items:center;gap:12px;">` +
|
|
4445
|
+
`<span class="o-tc-label" style="flex:1;cursor:grab;">${label}: Paused</span>` +
|
|
4446
|
+
`<button type="button" class="o-tc-ok" style="padding:6px 14px;background:${btnBg};color:#fff;border:none;border-radius:6px;font-weight:600;cursor:pointer;font-size:13px;flex-shrink:0;">${btnLabel}</button>` +
|
|
4447
|
+
`</div>` +
|
|
4448
|
+
itemsHtml;
|
|
4449
|
+
const box = o.overlay({
|
|
4450
|
+
innerHTML,
|
|
4451
|
+
timeout: opts.timeout,
|
|
4452
|
+
excludeDragSelector: ".o-tc-ok",
|
|
4453
|
+
onClose: (r) => resolve(r || { ok: true }),
|
|
4454
|
+
});
|
|
3850
4455
|
const okBtnStyles = {
|
|
3851
4456
|
padding: "6px 14px",
|
|
3852
4457
|
background: hasCheckboxes ? "#dc2626" : "#2563eb",
|
|
@@ -3860,70 +4465,23 @@ o.testConfirm = (label, items = [], opts = {}) =>
|
|
|
3860
4465
|
};
|
|
3861
4466
|
if (hasCheckboxes) {
|
|
3862
4467
|
const okBtn = box.first(".o-tc-ok");
|
|
3863
|
-
const cbs = o(".o-
|
|
4468
|
+
const cbs = o(".o-overlay-common .o-tc-item-cb");
|
|
3864
4469
|
const updateBtn = () => {
|
|
3865
4470
|
const allChecked = cbs.length > 0 && cbs.els.every((el) => el.checked);
|
|
3866
4471
|
okBtn.css({ ...okBtnStyles, background: allChecked ? "#16a34a" : "#dc2626" });
|
|
3867
4472
|
};
|
|
3868
4473
|
cbs.on("change", updateBtn);
|
|
3869
4474
|
}
|
|
3870
|
-
|
|
3871
|
-
let drag = null;
|
|
3872
|
-
const overlayStyle = {
|
|
3873
|
-
position: "fixed",
|
|
3874
|
-
left: "50%",
|
|
3875
|
-
bottom: "50px",
|
|
3876
|
-
transform: "translateX(-50%)",
|
|
3877
|
-
"z-index": "999999",
|
|
3878
|
-
width: "fit-content",
|
|
3879
|
-
"max-width": "min(90vw, 400px)",
|
|
3880
|
-
"font-family": "system-ui,sans-serif",
|
|
3881
|
-
cursor: "grab",
|
|
3882
|
-
"user-select": "text",
|
|
3883
|
-
};
|
|
3884
|
-
const applyOverlayStyle = () => {
|
|
3885
|
-
box.css(overlayStyle);
|
|
3886
|
-
};
|
|
3887
|
-
const onMove = (e) => {
|
|
3888
|
-
if (!drag) return;
|
|
3889
|
-
overlayStyle.left = drag.left + (e.clientX - drag.startX) + "px";
|
|
3890
|
-
overlayStyle.top = drag.top + (e.clientY - drag.startY) + "px";
|
|
3891
|
-
delete overlayStyle.bottom;
|
|
3892
|
-
overlayStyle.transform = "none";
|
|
3893
|
-
applyOverlayStyle();
|
|
3894
|
-
};
|
|
3895
|
-
const onUp = () => {
|
|
3896
|
-
if (drag) {
|
|
3897
|
-
overlayStyle.cursor = "grab";
|
|
3898
|
-
applyOverlayStyle();
|
|
3899
|
-
}
|
|
3900
|
-
drag = null;
|
|
3901
|
-
};
|
|
3902
|
-
box.on("mousedown", (e) => {
|
|
3903
|
-
if (e.target.closest(".o-tc-ok")) return;
|
|
3904
|
-
const r = box.el.getBoundingClientRect();
|
|
3905
|
-
drag = { startX: e.clientX, startY: e.clientY, left: r.left, top: r.top };
|
|
3906
|
-
overlayStyle.cursor = "grabbing";
|
|
3907
|
-
applyOverlayStyle();
|
|
3908
|
-
});
|
|
3909
|
-
o.D.addEventListener("mousemove", onMove);
|
|
3910
|
-
o.D.addEventListener("mouseup", onUp);
|
|
3911
|
-
|
|
3912
4475
|
box.first(".o-tc-ok").on("click", () => {
|
|
3913
|
-
o.D.removeEventListener("mousemove", onMove);
|
|
3914
|
-
o.D.removeEventListener("mouseup", onUp);
|
|
3915
4476
|
let unchecked = [];
|
|
3916
4477
|
if (hasCheckboxes) {
|
|
3917
|
-
const cbsList = o(".o-
|
|
3918
|
-
cbsList.els.
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
box.remove();
|
|
3923
|
-
if (unchecked.length === 0) {
|
|
3924
|
-
resolve({ ok: true });
|
|
3925
|
-
} else {
|
|
3926
|
-
resolve({ ok: false, errors: unchecked });
|
|
4478
|
+
const cbsList = o(".o-overlay-common .o-tc-item-cb");
|
|
4479
|
+
if (cbsList.els.length)
|
|
4480
|
+
cbsList.els.forEach((el, idx) => {
|
|
4481
|
+
if (!el.checked && items[idx] !== undefined) unchecked.push(items[idx]);
|
|
4482
|
+
});
|
|
3927
4483
|
}
|
|
4484
|
+
box._overlayCleanup();
|
|
4485
|
+
resolve(unchecked.length === 0 ? { ok: true } : { ok: false, errors: unchecked });
|
|
3928
4486
|
});
|
|
3929
4487
|
});
|