objs-core 2.0.3 → 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/EXAMPLES.md +59 -58
- package/README.md +70 -27
- package/SKILL.md +53 -4
- package/objs.built.js +820 -223
- package/objs.built.min.js +47 -37
- package/objs.d.ts +58 -6
- package/objs.js +987 -305
- package/package.json +1 -1
package/objs.built.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
|
*/
|
|
@@ -13,6 +13,7 @@ const o = (query) => {
|
|
|
13
13
|
parented: {},
|
|
14
14
|
store: {},
|
|
15
15
|
refs: {},
|
|
16
|
+
_refsByIndex: [],
|
|
16
17
|
states: [],
|
|
17
18
|
isDebug: false,
|
|
18
19
|
currentState: "",
|
|
@@ -65,8 +66,40 @@ const o = (query) => {
|
|
|
65
66
|
result.states = [];
|
|
66
67
|
result.ie = {};
|
|
67
68
|
}
|
|
69
|
+
if (Array.isArray(result._refsByIndex)) {
|
|
70
|
+
const currentLen = result._refsByIndex.length;
|
|
71
|
+
if (currentLen > ln) {
|
|
72
|
+
cycleObj(result._refsByIndex, (k) => {
|
|
73
|
+
const idx = +k;
|
|
74
|
+
if (idx >= ln) {
|
|
75
|
+
delete result._refsByIndex[idx];
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
result._refsByIndex.length = ln;
|
|
79
|
+
} else if (currentLen < ln) {
|
|
80
|
+
for (let idx = currentLen; idx < ln; idx++) {
|
|
81
|
+
result._refsByIndex[idx] = {};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
68
85
|
};
|
|
69
86
|
result.reset = o;
|
|
87
|
+
const hydrateDataOInitIn = (containerEl) => {
|
|
88
|
+
if (ssr || !containerEl.querySelectorAll) return;
|
|
89
|
+
const nodes = containerEl.querySelectorAll("[data-o-init]");
|
|
90
|
+
const byId = {};
|
|
91
|
+
nodes.forEach((node) => {
|
|
92
|
+
const id = node.getAttribute("data-o-init");
|
|
93
|
+
if (id === null) return;
|
|
94
|
+
if (!byId[id]) byId[id] = [];
|
|
95
|
+
byId[id].push(node);
|
|
96
|
+
});
|
|
97
|
+
cycleObj(byId, (id) => {
|
|
98
|
+
const inst = o.inits[id];
|
|
99
|
+
if (!inst) return;
|
|
100
|
+
inst.getSSR(Number(id), byId[id]);
|
|
101
|
+
});
|
|
102
|
+
};
|
|
70
103
|
const transform = (el, state, props) => {
|
|
71
104
|
cycleObj(state, (s) => {
|
|
72
105
|
let value = state[s];
|
|
@@ -98,7 +131,7 @@ const o = (query) => {
|
|
|
98
131
|
"root",
|
|
99
132
|
"ref"
|
|
100
133
|
].includes(s)) {
|
|
101
|
-
["html", "innerHTML"].includes(s) ? el.innerHTML = value : (
|
|
134
|
+
["html", "innerHTML"].includes(s) ? (el.innerHTML = value, !ssr && hydrateDataOInitIn(el)) : (
|
|
102
135
|
// className alias
|
|
103
136
|
s === "className" ? el.setAttribute("class", value) : (
|
|
104
137
|
// attach dataset
|
|
@@ -198,21 +231,21 @@ const o = (query) => {
|
|
|
198
231
|
data["data-o-init"] = initN;
|
|
199
232
|
}
|
|
200
233
|
const newEl = (n, prop = {}) => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
newElem.firstElementChild.dataset.oInit = n;
|
|
211
|
-
return newElem.firstElementChild;
|
|
212
|
-
}
|
|
234
|
+
const resolved = type(data) === functionType ? data(prop) : data;
|
|
235
|
+
if (type(resolved) === objectType) {
|
|
236
|
+
return D.createElement(resolved.tag || resolved.tagName || "div");
|
|
237
|
+
}
|
|
238
|
+
const newElem = D.createElement("div");
|
|
239
|
+
newElem.innerHTML = resolved;
|
|
240
|
+
if (newElem.children.length > ONE || !newElem.firstElementChild) {
|
|
241
|
+
newElem.dataset.oInit = n;
|
|
242
|
+
return newElem;
|
|
213
243
|
}
|
|
244
|
+
newElem.firstElementChild.dataset.oInit = n;
|
|
245
|
+
return newElem.firstElementChild;
|
|
214
246
|
};
|
|
215
247
|
const rawData = props;
|
|
248
|
+
if (!Array.isArray(props)) props = [props];
|
|
216
249
|
!props.length ? props = [props] : props;
|
|
217
250
|
const creation = !els[0] && state === "render";
|
|
218
251
|
props = props.map((prop, i2) => {
|
|
@@ -247,19 +280,45 @@ const o = (query) => {
|
|
|
247
280
|
if (creation) {
|
|
248
281
|
buff["data-o-init"] = initN;
|
|
249
282
|
buff["data-o-init-i"] = i2;
|
|
283
|
+
if (buff.events) {
|
|
284
|
+
result._hydrateEvents = result._hydrateEvents || [];
|
|
285
|
+
result._hydrateEvents[i2] = buff.events;
|
|
286
|
+
}
|
|
250
287
|
}
|
|
251
288
|
transform(el, buff, props[j ? i2 : 0]);
|
|
252
289
|
}
|
|
253
290
|
});
|
|
254
291
|
if (creation) {
|
|
292
|
+
result._refsByIndex = [];
|
|
255
293
|
result.refs = {};
|
|
256
|
-
result.els.forEach((el) => {
|
|
294
|
+
result.els.forEach((el, idx) => {
|
|
257
295
|
if (!el.querySelectorAll) return;
|
|
296
|
+
const refsForEl = {};
|
|
258
297
|
el.querySelectorAll("[ref]").forEach((refEl) => {
|
|
259
|
-
|
|
260
|
-
refEl
|
|
298
|
+
const refName = refEl.getAttribute("ref");
|
|
299
|
+
const refInstance = o(refEl);
|
|
300
|
+
refsForEl[refName] = refInstance;
|
|
301
|
+
if (idx === 0) result.refs[refName] = refInstance;
|
|
261
302
|
});
|
|
303
|
+
result._refsByIndex[idx] = refsForEl;
|
|
262
304
|
});
|
|
305
|
+
if (!ssr && result._hydrateEvents) {
|
|
306
|
+
result._hydrateEvents.forEach((evts, idx) => {
|
|
307
|
+
if (!evts) return;
|
|
308
|
+
result.select(idx);
|
|
309
|
+
cycleObj(evts, (event) => {
|
|
310
|
+
const spec = evts[event];
|
|
311
|
+
if (type(spec) === objectType && spec.targetRef && type(spec.handler) === functionType) {
|
|
312
|
+
const refsForIdx = result._refsByIndex?.[idx] ?? result.refs;
|
|
313
|
+
const ref = refsForIdx?.[spec.targetRef];
|
|
314
|
+
if (ref) ref.on(event, spec.handler);
|
|
315
|
+
} else if (type(spec) === functionType) {
|
|
316
|
+
result.on(event, spec);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
result.all();
|
|
321
|
+
}
|
|
263
322
|
}
|
|
264
323
|
}
|
|
265
324
|
if (creation && type(data) === objectType && data.events) {
|
|
@@ -270,19 +329,47 @@ const o = (query) => {
|
|
|
270
329
|
});
|
|
271
330
|
});
|
|
272
331
|
const renderState = states.render || states;
|
|
273
|
-
|
|
332
|
+
const hasStateEvents = !ssr && type(renderState) === objectType && renderState.events;
|
|
333
|
+
const hasHydrateEvents = !ssr && result._hydrateEvents && result._hydrateEvents.length;
|
|
334
|
+
if (hasStateEvents || hasHydrateEvents) {
|
|
274
335
|
result.initSSRAfterGettingSSR = () => {
|
|
336
|
+
result._refsByIndex = [];
|
|
275
337
|
result.refs = {};
|
|
276
|
-
result.els.forEach((el) => {
|
|
338
|
+
result.els.forEach((el, idx) => {
|
|
277
339
|
if (!el.querySelectorAll) return;
|
|
340
|
+
const refsForEl = {};
|
|
278
341
|
el.querySelectorAll("[ref]").forEach((refEl) => {
|
|
279
|
-
|
|
342
|
+
const refName = refEl.getAttribute("ref");
|
|
343
|
+
const refInstance = o(refEl);
|
|
344
|
+
refsForEl[refName] = refInstance;
|
|
345
|
+
if (idx === 0) result.refs[refName] = refInstance;
|
|
280
346
|
refEl.removeAttribute("ref");
|
|
281
347
|
});
|
|
348
|
+
result._refsByIndex[idx] = refsForEl;
|
|
349
|
+
if (idx === 0) result.refs = refsForEl;
|
|
282
350
|
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
351
|
+
if (hasStateEvents) {
|
|
352
|
+
cycleObj(renderState.events, (event) => {
|
|
353
|
+
result.on(event, renderState.events[event]);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
if (result._hydrateEvents) {
|
|
357
|
+
result._hydrateEvents.forEach((evts, idx) => {
|
|
358
|
+
if (!evts) return;
|
|
359
|
+
result.select(idx);
|
|
360
|
+
cycleObj(evts, (event) => {
|
|
361
|
+
const spec = evts[event];
|
|
362
|
+
if (type(spec) === objectType && spec.targetRef && type(spec.handler) === functionType) {
|
|
363
|
+
const refsForIdx = result._refsByIndex?.[idx] ?? result.refs;
|
|
364
|
+
const ref = refsForIdx?.[spec.targetRef];
|
|
365
|
+
if (ref) ref.on(event, spec.handler);
|
|
366
|
+
} else if (type(spec) === functionType) {
|
|
367
|
+
result.on(event, spec);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
result.all();
|
|
372
|
+
}
|
|
286
373
|
};
|
|
287
374
|
}
|
|
288
375
|
}, "init");
|
|
@@ -294,21 +381,37 @@ const o = (query) => {
|
|
|
294
381
|
]);
|
|
295
382
|
loader.connect(self, state, fail);
|
|
296
383
|
}, "connect");
|
|
297
|
-
result.getSSR = returner((initId) => {
|
|
384
|
+
result.getSSR = returner((initId, fromEls) => {
|
|
298
385
|
typeVerify([[initId, [numberType, undefinedType]]]);
|
|
299
386
|
const effectiveId = initId !== void 0 ? initId : result.initID;
|
|
300
387
|
if (ssr || type(initId) === undefinedType && type(result.initID) === undefinedType) {
|
|
301
388
|
return;
|
|
302
389
|
}
|
|
303
|
-
const ssrEls = o.D.querySelectorAll(`[data-o-init="${effectiveId}"]`);
|
|
304
|
-
if (ssrEls.length
|
|
390
|
+
const ssrEls = fromEls && fromEls.length ? fromEls : o.D.querySelectorAll(`[data-o-init="${effectiveId}"]`);
|
|
391
|
+
if (ssrEls.length) {
|
|
305
392
|
result.els = Array.from(ssrEls);
|
|
306
|
-
|
|
307
|
-
|
|
393
|
+
if (initId !== void 0) {
|
|
394
|
+
result.initID = initId;
|
|
395
|
+
o.inits[initId] = result;
|
|
396
|
+
}
|
|
308
397
|
setResultVals(false);
|
|
309
398
|
if (type(result.initSSRAfterGettingSSR) === functionType) {
|
|
310
399
|
result.initSSRAfterGettingSSR();
|
|
311
|
-
|
|
400
|
+
} else if (fromEls && fromEls.length) {
|
|
401
|
+
result._refsByIndex = [];
|
|
402
|
+
result.refs = {};
|
|
403
|
+
result.els.forEach((el, idx) => {
|
|
404
|
+
if (!el.querySelectorAll) return;
|
|
405
|
+
const refsForEl = {};
|
|
406
|
+
el.querySelectorAll("[ref]").forEach((refEl) => {
|
|
407
|
+
const refName = refEl.getAttribute("ref");
|
|
408
|
+
refsForEl[refName] = o(refEl);
|
|
409
|
+
if (idx === 0) result.refs[refName] = refsForEl[refName];
|
|
410
|
+
refEl.removeAttribute("ref");
|
|
411
|
+
});
|
|
412
|
+
result._refsByIndex[idx] = refsForEl;
|
|
413
|
+
if (idx === 0) result.refs = refsForEl;
|
|
414
|
+
});
|
|
312
415
|
}
|
|
313
416
|
}
|
|
314
417
|
}, "getSSR");
|
|
@@ -424,20 +527,31 @@ const o = (query) => {
|
|
|
424
527
|
return { [state]: parseState(result.els[finish]) };
|
|
425
528
|
}, "sample");
|
|
426
529
|
result.select = returner((i2) => {
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
530
|
+
let idx = i2;
|
|
531
|
+
if (idx != null && type(idx) === objectType && idx.target && result.els.length) {
|
|
532
|
+
idx = result.els.findIndex((el) => el === idx.target || el.contains(idx.target));
|
|
533
|
+
if (idx < 0) idx = 0;
|
|
534
|
+
}
|
|
535
|
+
typeVerify([[idx, [numberType, undefinedType]]]);
|
|
536
|
+
if (idx === u) {
|
|
537
|
+
idx = result.length - ONE;
|
|
538
|
+
}
|
|
539
|
+
start = idx;
|
|
540
|
+
finish = idx;
|
|
541
|
+
result.el = result.els[idx];
|
|
434
542
|
select = ONE;
|
|
543
|
+
if (Array.isArray(result._refsByIndex) && result._refsByIndex[idx]) {
|
|
544
|
+
result.refs = result._refsByIndex[idx];
|
|
545
|
+
}
|
|
435
546
|
}, "select");
|
|
436
547
|
result.all = returner(() => {
|
|
437
548
|
start = result.length - ONE;
|
|
438
549
|
finish = 0;
|
|
439
550
|
result.el = result.els[0];
|
|
440
551
|
select = 0;
|
|
552
|
+
if (Array.isArray(result._refsByIndex) && result._refsByIndex.length) {
|
|
553
|
+
result.refs = result._refsByIndex[0] || {};
|
|
554
|
+
}
|
|
441
555
|
}, "all");
|
|
442
556
|
result.remove = returner((j2) => {
|
|
443
557
|
typeVerify([[j2, [numberType, undefinedType]]]);
|
|
@@ -464,7 +578,10 @@ const o = (query) => {
|
|
|
464
578
|
if (j2 === u) {
|
|
465
579
|
j2 = finish;
|
|
466
580
|
}
|
|
467
|
-
result.els.splice(
|
|
581
|
+
result.els.splice(j2, ONE);
|
|
582
|
+
if (Array.isArray(result._refsByIndex)) {
|
|
583
|
+
result._refsByIndex.splice(j2, ONE);
|
|
584
|
+
}
|
|
468
585
|
setResultVals();
|
|
469
586
|
}, "skip");
|
|
470
587
|
result.add = returner((el) => {
|
|
@@ -671,6 +788,14 @@ const o = (query) => {
|
|
|
671
788
|
return html;
|
|
672
789
|
}
|
|
673
790
|
}, "html");
|
|
791
|
+
result.toString = function() {
|
|
792
|
+
return result.html();
|
|
793
|
+
};
|
|
794
|
+
result[Symbol.toPrimitive] = function(hint) {
|
|
795
|
+
if (hint === "string" || hint === "default") return result.html();
|
|
796
|
+
if (hint === "number") return result.els?.length ?? 0;
|
|
797
|
+
return result.html();
|
|
798
|
+
};
|
|
674
799
|
result.val = returner((value) => {
|
|
675
800
|
if (value === void 0) return result.el?.value;
|
|
676
801
|
iterator(() => {
|
|
@@ -1583,6 +1708,7 @@ o.withReactContext = (React, Context, selector, component, state = "render") =>
|
|
|
1583
1708
|
if (__DEV__) {
|
|
1584
1709
|
o.debug = false;
|
|
1585
1710
|
}
|
|
1711
|
+
o.sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1586
1712
|
o.tLog = [];
|
|
1587
1713
|
o.tRes = [];
|
|
1588
1714
|
o.tStatus = [];
|
|
@@ -1591,6 +1717,8 @@ o.tShowOk = o.F;
|
|
|
1591
1717
|
o.tStyled = o.F;
|
|
1592
1718
|
o.tTime = 2e3;
|
|
1593
1719
|
o.tests = [];
|
|
1720
|
+
o.tExpectedSteps = {};
|
|
1721
|
+
o.tFinalized = {};
|
|
1594
1722
|
o.tAutolog = o.F;
|
|
1595
1723
|
o.tBeforeEach = void 0;
|
|
1596
1724
|
o.tAfterEach = void 0;
|
|
@@ -1691,10 +1819,15 @@ o.test = (title = "", ...tests) => {
|
|
|
1691
1819
|
}
|
|
1692
1820
|
}
|
|
1693
1821
|
};
|
|
1822
|
+
let opts = {};
|
|
1694
1823
|
if (typeof tests[num - 1] === "function") {
|
|
1695
1824
|
o.tFns[testN2] = tests[num - 1];
|
|
1696
1825
|
num--;
|
|
1697
1826
|
}
|
|
1827
|
+
if (num > 0 && typeof tests[num - 1] === "object" && !Array.isArray(tests[num - 1]) && (tests[num - 1].sync !== void 0 || tests[num - 1].confirmOnFailure !== void 0)) {
|
|
1828
|
+
opts = tests[num - 1];
|
|
1829
|
+
num--;
|
|
1830
|
+
}
|
|
1698
1831
|
if (testSession) {
|
|
1699
1832
|
o.tLog[testN2] = sessionStorage.getItem(`oTest-Log-${testN2}`) || "";
|
|
1700
1833
|
o.tRes[testN2] = sessionStorage.getItem(`oTest-Res-${testN2}`) || false;
|
|
@@ -1722,6 +1855,143 @@ o.test = (title = "", ...tests) => {
|
|
|
1722
1855
|
o.tRes[testN2] = o.F;
|
|
1723
1856
|
o.tStatus[testN2] = [];
|
|
1724
1857
|
}
|
|
1858
|
+
o.tExpectedSteps[testN2] = num;
|
|
1859
|
+
o.tFinalized[testN2] = false;
|
|
1860
|
+
const showConfirmOnFailureOverlay = (stepIdx, msg) => new Promise((resolve) => {
|
|
1861
|
+
const box = o.overlay({
|
|
1862
|
+
innerHTML: `<div style="display:flex;flex-direction:column;gap:8px;"><div style="cursor:grab;">Step ${stepIdx + 1} failed: ${msg || "error"}. Continue testing?</div><div style="display:flex;gap:8px;"><button class="o-cf-continue" style="padding:6px 12px;background:#2563eb;color:#fff;border:none;border-radius:6px;cursor:pointer;">Continue</button><button class="o-cf-stop" style="padding:6px 12px;background:#dc2626;color:#fff;border:none;border-radius:6px;cursor:pointer;">Stop</button></div></div>`,
|
|
1863
|
+
timeout: opts.confirmOnFailureTimeout || void 0,
|
|
1864
|
+
onClose: (r) => resolve(r || { continue: false }),
|
|
1865
|
+
excludeDragSelector: ".o-cf-continue, .o-cf-stop"
|
|
1866
|
+
});
|
|
1867
|
+
box.first(".o-cf-continue").on("click", () => {
|
|
1868
|
+
box._overlayCleanup();
|
|
1869
|
+
resolve({ continue: true });
|
|
1870
|
+
});
|
|
1871
|
+
box.first(".o-cf-stop").on("click", () => {
|
|
1872
|
+
box._overlayCleanup();
|
|
1873
|
+
resolve({ continue: false });
|
|
1874
|
+
});
|
|
1875
|
+
});
|
|
1876
|
+
const finalize = () => {
|
|
1877
|
+
if (o.tFinalized[testN2]) return;
|
|
1878
|
+
o.tFinalized[testN2] = true;
|
|
1879
|
+
const anyFailed = o.tStatus[testN2].some((s) => s === false);
|
|
1880
|
+
o.tRes[testN2] = !anyFailed && done === num;
|
|
1881
|
+
row = waits ? "\u251C " : "\u2558 ";
|
|
1882
|
+
row += "DONE " + done + "/" + num + (waits ? ", waiting: " + waits : "");
|
|
1883
|
+
log(row, done + waits !== num);
|
|
1884
|
+
if (!waits) {
|
|
1885
|
+
log();
|
|
1886
|
+
}
|
|
1887
|
+
if (o.tStyled) {
|
|
1888
|
+
o.tLog[testN2] += o.tPre + '<div style="color:' + (done + waits !== num ? "red" : "green") + ';"><b>DONE ' + done + "/" + num + (waits ? ", waiting: " + waits : "") + "</b>" + o.tDc + o.tDc;
|
|
1889
|
+
} else {
|
|
1890
|
+
o.tLog[testN2] += row + "\n";
|
|
1891
|
+
}
|
|
1892
|
+
if (testSession) {
|
|
1893
|
+
sessionStorage.setItem(`oTest-Log-${testN2}`, o.tLog[testN2]);
|
|
1894
|
+
sessionStorage.setItem(`oTest-Res-${testN2}`, o.tRes[testN2]);
|
|
1895
|
+
sessionStorage.setItem(`oTest-Status-${testN2}`, JSON.stringify(o.tStatus[testN2]));
|
|
1896
|
+
}
|
|
1897
|
+
if (!waits && typeof o.tFns[testN2] === "function") {
|
|
1898
|
+
o.tFns[testN2](testN2);
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
if (opts.sync || opts.confirmOnFailure) {
|
|
1902
|
+
(async () => {
|
|
1903
|
+
for (let i = o.tStatus[testN2].length; i < num; i++) {
|
|
1904
|
+
const testInfo = {
|
|
1905
|
+
n: testN2,
|
|
1906
|
+
i,
|
|
1907
|
+
title: tests[i][0],
|
|
1908
|
+
tShowOk: o.tShowOk,
|
|
1909
|
+
tStyled: o.tStyled
|
|
1910
|
+
};
|
|
1911
|
+
let res = tests[i][1];
|
|
1912
|
+
if (typeof res === "undefined") {
|
|
1913
|
+
if (o.tStyled) {
|
|
1914
|
+
o.tLog[testN2] += "<div>" + testInfo.title + "</div>";
|
|
1915
|
+
} else {
|
|
1916
|
+
o.tLog[testN2] += testInfo.title + "\n";
|
|
1917
|
+
}
|
|
1918
|
+
log("\u251C " + testInfo.title, false, true);
|
|
1919
|
+
o.tStatus[testN2][i] = true;
|
|
1920
|
+
done++;
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1923
|
+
if (typeof o.tBeforeEach === "function") {
|
|
1924
|
+
o.tBeforeEach(testInfo);
|
|
1925
|
+
}
|
|
1926
|
+
if (typeof res === "function") {
|
|
1927
|
+
try {
|
|
1928
|
+
res = res(testInfo);
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
res = error.message;
|
|
1931
|
+
if (o.onError) {
|
|
1932
|
+
o.onError(error);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (typeof o.tAfterEach === "function") {
|
|
1937
|
+
o.tAfterEach(testInfo, res);
|
|
1938
|
+
}
|
|
1939
|
+
if (res && typeof res.then === "function") {
|
|
1940
|
+
try {
|
|
1941
|
+
const value = await res;
|
|
1942
|
+
const ok = value === true || value == null || value && typeof value === "object" && value.ok === true;
|
|
1943
|
+
const msg = value && value.errors && value.errors.length ? value.errors.join("; ") : typeof value === "string" ? value : "";
|
|
1944
|
+
o.testUpdate(testInfo, ok, ok ? "" : msg ? ": " + msg : "");
|
|
1945
|
+
done++;
|
|
1946
|
+
if (!ok && opts.confirmOnFailure) {
|
|
1947
|
+
const choice = await showConfirmOnFailureOverlay(i, msg);
|
|
1948
|
+
if (!choice.continue) break;
|
|
1949
|
+
}
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
o.testUpdate(testInfo, false, err.message || "Promise rejected");
|
|
1952
|
+
if (opts.confirmOnFailure) {
|
|
1953
|
+
const choice = await showConfirmOnFailureOverlay(i, err.message || "Promise rejected");
|
|
1954
|
+
if (!choice.continue) break;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
if (typeof o.tStatus[testN2][i] === "undefined") {
|
|
1960
|
+
o.tStatus[testN2][i] = typeof res === "string" ? o.F : res;
|
|
1961
|
+
} else {
|
|
1962
|
+
sessionStorage.setItem(`oTest-Status-${testN2}`, JSON.stringify(o.tStatus[testN2]));
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
if (res === true) {
|
|
1966
|
+
done++;
|
|
1967
|
+
if (o.tShowOk) {
|
|
1968
|
+
o.tLog[testN2] += preOk + tests[i][0] + posOk;
|
|
1969
|
+
log("\u251C OK: " + tests[i][0]);
|
|
1970
|
+
}
|
|
1971
|
+
} else if (res !== o.U) {
|
|
1972
|
+
o.tLog[testN2] += preXx + tests[i][0] + (res !== o.F ? ": " + res : "") + posXx;
|
|
1973
|
+
log("\u251C \u2718 " + tests[i][0] + (res !== o.F ? ": " + res : ""), true);
|
|
1974
|
+
if (opts.confirmOnFailure) {
|
|
1975
|
+
const choice = await showConfirmOnFailureOverlay(i, typeof res === "string" ? res : "");
|
|
1976
|
+
if (!choice.continue) break;
|
|
1977
|
+
}
|
|
1978
|
+
} else {
|
|
1979
|
+
waits++;
|
|
1980
|
+
setTimeout(
|
|
1981
|
+
(info) => {
|
|
1982
|
+
info.title += " (timeout)";
|
|
1983
|
+
o.testUpdate(info);
|
|
1984
|
+
},
|
|
1985
|
+
o.tTime,
|
|
1986
|
+
testInfo
|
|
1987
|
+
);
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
finalize();
|
|
1992
|
+
})();
|
|
1993
|
+
return testN2;
|
|
1994
|
+
}
|
|
1725
1995
|
for (let i = o.tStatus[testN2].length; i < num; i++) {
|
|
1726
1996
|
const testInfo = {
|
|
1727
1997
|
n: testN2,
|
|
@@ -1802,26 +2072,7 @@ o.test = (title = "", ...tests) => {
|
|
|
1802
2072
|
);
|
|
1803
2073
|
}
|
|
1804
2074
|
}
|
|
1805
|
-
|
|
1806
|
-
row = waits ? "\u251C " : "\u2558 ";
|
|
1807
|
-
row += "DONE " + done + "/" + num + (waits ? ", waiting: " + waits : "");
|
|
1808
|
-
log(row, done + waits !== num);
|
|
1809
|
-
if (!waits) {
|
|
1810
|
-
log();
|
|
1811
|
-
}
|
|
1812
|
-
if (o.tStyled) {
|
|
1813
|
-
o.tLog[testN2] += o.tPre + '<div style="color:' + (done + waits !== num ? "red" : "green") + ';"><b>DONE ' + done + "/" + num + (waits ? ", waiting: " + waits : "") + "</b>" + o.tDc + o.tDc;
|
|
1814
|
-
} else {
|
|
1815
|
-
o.tLog[testN2] += row + "\n";
|
|
1816
|
-
}
|
|
1817
|
-
if (testSession) {
|
|
1818
|
-
sessionStorage.setItem(`oTest-Log-${testN2}`, o.tLog[testN2]);
|
|
1819
|
-
sessionStorage.setItem(`oTest-Res-${testN2}`, o.tRes[testN2]);
|
|
1820
|
-
sessionStorage.setItem(`oTest-Status-${testN2}`, JSON.stringify(o.tStatus[testN2]));
|
|
1821
|
-
}
|
|
1822
|
-
if (!waits && typeof o.tFns[testN2] === "function") {
|
|
1823
|
-
o.tFns[testN2](testN2);
|
|
1824
|
-
}
|
|
2075
|
+
finalize();
|
|
1825
2076
|
return testN2;
|
|
1826
2077
|
};
|
|
1827
2078
|
o.testUpdate = (info, res = o.F, suff = "") => {
|
|
@@ -1873,13 +2124,21 @@ o.testUpdate = (info, res = o.F, suff = "") => {
|
|
|
1873
2124
|
}
|
|
1874
2125
|
n++;
|
|
1875
2126
|
}
|
|
2127
|
+
const expectedSteps = o.tests[testN2]?.tests?.length ?? o.tExpectedSteps[testN2] ?? Number.MAX_SAFE_INTEGER;
|
|
2128
|
+
if (n < expectedSteps) {
|
|
2129
|
+
if (sessionStorage?.getItem("oTest-Run") === testN2) {
|
|
2130
|
+
sessionStorage.setItem(`oTest-Log-${testN2}`, o.tLog[testN2]);
|
|
2131
|
+
sessionStorage.setItem(`oTest-Res-${testN2}`, o.tRes[testN2]);
|
|
2132
|
+
sessionStorage.setItem(`oTest-Status-${testN2}`, JSON.stringify(o.tStatus[testN2]));
|
|
2133
|
+
}
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
if (o.tFinalized[testN2]) return;
|
|
2137
|
+
o.tFinalized[testN2] = true;
|
|
1876
2138
|
if (sessionStorage?.getItem("oTest-Run") === testN2) {
|
|
1877
2139
|
sessionStorage.setItem(`oTest-Log-${testN2}`, o.tLog[testN2]);
|
|
1878
2140
|
sessionStorage.setItem(`oTest-Res-${testN2}`, o.tRes[testN2]);
|
|
1879
2141
|
sessionStorage.setItem(`oTest-Status-${testN2}`, JSON.stringify(o.tStatus[testN2]));
|
|
1880
|
-
if (n < o.tests[testN2].tests.length) {
|
|
1881
|
-
return;
|
|
1882
|
-
}
|
|
1883
2142
|
}
|
|
1884
2143
|
o.tRes[testN2] = !fails;
|
|
1885
2144
|
row = fails ? "FAILED " + fails + "/" + n : "DONE " + n + "/" + n;
|
|
@@ -1984,6 +2243,7 @@ o.recorder = {
|
|
|
1984
2243
|
_listeners: [],
|
|
1985
2244
|
_observer: null
|
|
1986
2245
|
};
|
|
2246
|
+
o.recordingAssertionDebug = false;
|
|
1987
2247
|
o.startRecording = (observe, events, timeouts) => {
|
|
1988
2248
|
if (o.recorder.active) {
|
|
1989
2249
|
return;
|
|
@@ -2007,6 +2267,7 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2007
2267
|
rec.initialData = { url: window.location.href, timestamp: Date.now() };
|
|
2008
2268
|
rec.observeRoot = observe || null;
|
|
2009
2269
|
rec.assertions = [];
|
|
2270
|
+
rec.removedElements = [];
|
|
2010
2271
|
o.inits.forEach((inst, idx) => {
|
|
2011
2272
|
if (inst?.store) {
|
|
2012
2273
|
rec.initialData["init_" + idx] = JSON.parse(JSON.stringify(inst.store));
|
|
@@ -2087,6 +2348,16 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2087
2348
|
rec._observer = new MutationObserver((mutations) => {
|
|
2088
2349
|
const actionIdx = rec.actions.length - 1;
|
|
2089
2350
|
if (actionIdx < 0) return;
|
|
2351
|
+
const lastAction = rec.actions[actionIdx];
|
|
2352
|
+
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
2353
|
+
console.log("[recording] MutationObserver batch:", {
|
|
2354
|
+
actionIdx,
|
|
2355
|
+
lastAction: lastAction ? { type: lastAction.type, target: lastAction.target } : null,
|
|
2356
|
+
mutationTypes: mutations.map((x) => x.type),
|
|
2357
|
+
addedCount: mutations.reduce((n, x) => n + (x.addedNodes?.length || 0), 0),
|
|
2358
|
+
removedCount: mutations.reduce((n, x) => n + (x.removedNodes?.length || 0), 0)
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2090
2361
|
mutations.forEach((m) => {
|
|
2091
2362
|
const addAssertionIndex = (sel, node) => {
|
|
2092
2363
|
let listSelector;
|
|
@@ -2125,13 +2396,56 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2125
2396
|
(a2) => a2.actionIdx === actionIdx && a2.selector === sel && a2.type === "visible"
|
|
2126
2397
|
))
|
|
2127
2398
|
return;
|
|
2128
|
-
const
|
|
2129
|
-
const text = (textEl.textContent?.trim() || node.textContent?.trim() || "").slice(0, 80) || void 0;
|
|
2399
|
+
const text = (node.textContent?.trim() || "").slice(0, 80) || void 0;
|
|
2130
2400
|
const { listSelector: aListSel, index: aIdx } = addAssertionIndex(sel, node);
|
|
2131
2401
|
const a = { actionIdx, type: "visible", selector: sel, text };
|
|
2132
2402
|
if (aListSel != null) a.listSelector = aListSel;
|
|
2133
2403
|
if (aIdx != null) a.index = aIdx;
|
|
2134
2404
|
rec.assertions.push(a);
|
|
2405
|
+
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
2406
|
+
console.log("[recording] +visible assertion:", {
|
|
2407
|
+
actionIdx,
|
|
2408
|
+
lastAction: lastAction?.type + " " + lastAction?.target,
|
|
2409
|
+
selector: sel,
|
|
2410
|
+
text: (text || "").slice(0, 40),
|
|
2411
|
+
index: aIdx,
|
|
2412
|
+
listSelector: aListSel
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
});
|
|
2416
|
+
m.removedNodes.forEach((node) => {
|
|
2417
|
+
if (node.nodeType !== 1) return;
|
|
2418
|
+
const sel = buildSelector(node);
|
|
2419
|
+
if (!sel) return;
|
|
2420
|
+
const text = (node.textContent?.trim() || "").slice(0, 80) || void 0;
|
|
2421
|
+
const parent = m.target;
|
|
2422
|
+
let index;
|
|
2423
|
+
if (node.previousSibling) {
|
|
2424
|
+
index = Array.from(parent.children).indexOf(node.previousSibling) + 1;
|
|
2425
|
+
} else if (node.nextSibling) {
|
|
2426
|
+
index = Array.from(parent.children).indexOf(node.nextSibling);
|
|
2427
|
+
} else {
|
|
2428
|
+
index = 0;
|
|
2429
|
+
}
|
|
2430
|
+
let listSelector;
|
|
2431
|
+
if (o.autotag && node.dataset?.[o.autotag]) {
|
|
2432
|
+
const qaVal = node.dataset[o.autotag];
|
|
2433
|
+
listSelector = `[data-${o.autotag}="${qaVal}"]`;
|
|
2434
|
+
}
|
|
2435
|
+
const entry = { actionIdx, type: "removed", selector: sel, text };
|
|
2436
|
+
if (listSelector) entry.listSelector = listSelector;
|
|
2437
|
+
entry.index = index;
|
|
2438
|
+
rec.removedElements.push(entry);
|
|
2439
|
+
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
2440
|
+
console.log("[recording] +removed element:", {
|
|
2441
|
+
actionIdx,
|
|
2442
|
+
lastAction: lastAction?.type + " " + lastAction?.target,
|
|
2443
|
+
selector: sel,
|
|
2444
|
+
text: (text || "").slice(0, 40),
|
|
2445
|
+
index,
|
|
2446
|
+
listSelector
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2135
2449
|
});
|
|
2136
2450
|
}
|
|
2137
2451
|
if (m.type === "attributes") {
|
|
@@ -2151,6 +2465,16 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2151
2465
|
if (aListSel != null) a.listSelector = aListSel;
|
|
2152
2466
|
if (aIdx != null) a.index = aIdx;
|
|
2153
2467
|
rec.assertions.push(a);
|
|
2468
|
+
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
2469
|
+
console.log("[recording] +class assertion:", {
|
|
2470
|
+
actionIdx,
|
|
2471
|
+
lastAction: lastAction?.type + " " + lastAction?.target,
|
|
2472
|
+
selector: sel,
|
|
2473
|
+
className: m.target.className,
|
|
2474
|
+
index: aIdx,
|
|
2475
|
+
listSelector: aListSel
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2154
2478
|
}
|
|
2155
2479
|
});
|
|
2156
2480
|
});
|
|
@@ -2213,7 +2537,7 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2213
2537
|
const scrollY = ev === "scroll" ? window.scrollY : void 0;
|
|
2214
2538
|
const value = ev === "input" || ev === "change" ? target?.value : void 0;
|
|
2215
2539
|
const checked = ev === "change" && (target?.type === "checkbox" || target?.type === "radio") ? target?.checked : void 0;
|
|
2216
|
-
const delay = stepDelays[ev] !== void 0 ? stepDelays[ev] : captureDebounce[ev] ?? 0;
|
|
2540
|
+
const delay = ev === "click" || ev === "change" ? 0 : stepDelays[ev] !== void 0 ? stepDelays[ev] : captureDebounce[ev] ?? 0;
|
|
2217
2541
|
const pushAction = () => {
|
|
2218
2542
|
const action = { type: ev, target: selector, time: Date.now() };
|
|
2219
2543
|
if (targetType) action.targetType = targetType;
|
|
@@ -2256,6 +2580,7 @@ o.stopRecording = () => {
|
|
|
2256
2580
|
initialData: { ...rec.initialData },
|
|
2257
2581
|
stepDelays: { ...rec.stepDelays },
|
|
2258
2582
|
assertions: [...rec.assertions || []],
|
|
2583
|
+
removedElements: [...rec.removedElements || []],
|
|
2259
2584
|
observeRoot: rec.observeRoot || null
|
|
2260
2585
|
};
|
|
2261
2586
|
};
|
|
@@ -2271,34 +2596,218 @@ o.clearRecording = (id) => {
|
|
|
2271
2596
|
}
|
|
2272
2597
|
}
|
|
2273
2598
|
};
|
|
2274
|
-
o.
|
|
2275
|
-
const
|
|
2599
|
+
o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
|
|
2600
|
+
const preFiltered = opts && opts.assertions;
|
|
2601
|
+
const assertions = preFiltered != null ? preFiltered : (recording.assertions || []).filter(
|
|
2602
|
+
(a) => actionIdx == null || a.actionIdx === actionIdx
|
|
2603
|
+
);
|
|
2604
|
+
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
2605
|
+
console.log("[runRecordingAssertions] run:", {
|
|
2606
|
+
actionIdx,
|
|
2607
|
+
scope: actionIdx == null ? "teardown (all)" : "per-action",
|
|
2608
|
+
assertionsCount: assertions.length,
|
|
2609
|
+
assertions: assertions.map((a) => ({
|
|
2610
|
+
actionIdx: a.actionIdx,
|
|
2611
|
+
type: a.type,
|
|
2612
|
+
selector: a.selector,
|
|
2613
|
+
index: a.index,
|
|
2614
|
+
text: (a.text || "").slice(0, 40)
|
|
2615
|
+
}))
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2619
|
+
const deduped = assertions.filter((a) => {
|
|
2620
|
+
const key = `${a.selector}|${a.type}|${a.actionIdx}|${a.index ?? ""}`;
|
|
2621
|
+
if (seen.has(key)) return false;
|
|
2622
|
+
seen.add(key);
|
|
2623
|
+
return true;
|
|
2624
|
+
});
|
|
2625
|
+
const resolveRoot = () => {
|
|
2626
|
+
if (root != null) {
|
|
2627
|
+
return typeof root === "string" ? o.D.querySelector(root) || o.D.body : root;
|
|
2628
|
+
}
|
|
2629
|
+
const sel = recording.observeRoot;
|
|
2630
|
+
return sel ? o.D.querySelector(sel) || o.D.body : o.D.body;
|
|
2631
|
+
};
|
|
2632
|
+
const r = resolveRoot();
|
|
2633
|
+
const norm = (s) => (s || "").trim().replace(/\s+/g, " ");
|
|
2634
|
+
const getText = (el) => el ? norm(el.textContent || "") : "";
|
|
2635
|
+
const removedElements = opts?.removedElements || [];
|
|
2636
|
+
const isRemoved = (a) => {
|
|
2637
|
+
if (!removedElements.length || actionIdx == null) return false;
|
|
2638
|
+
const expText = norm(a.text || "");
|
|
2639
|
+
for (const r2 of removedElements) {
|
|
2640
|
+
if (r2.actionIdx > actionIdx) continue;
|
|
2641
|
+
if (norm(r2.text || "") !== expText) continue;
|
|
2642
|
+
if (r2.selector !== a.selector) continue;
|
|
2643
|
+
if (a.listSelector != null && r2.listSelector !== a.listSelector) continue;
|
|
2644
|
+
if (a.index != null && r2.index !== a.index) continue;
|
|
2645
|
+
return true;
|
|
2646
|
+
}
|
|
2647
|
+
return false;
|
|
2648
|
+
};
|
|
2649
|
+
let passed = 0;
|
|
2650
|
+
const failures = [];
|
|
2651
|
+
for (const a of deduped) {
|
|
2652
|
+
if (isRemoved(a)) {
|
|
2653
|
+
passed += 1;
|
|
2654
|
+
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
2655
|
+
console.log("[runRecordingAssertions] skip (explicit removed):", {
|
|
2656
|
+
actionIdx: a.actionIdx,
|
|
2657
|
+
selector: a.selector,
|
|
2658
|
+
text: (a.text || "").slice(0, 40)
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
let el = null;
|
|
2664
|
+
let indexOutOfBounds = false;
|
|
2665
|
+
if (a.listSelector != null && a.index != null) {
|
|
2666
|
+
const items = r.querySelectorAll(a.listSelector);
|
|
2667
|
+
const expectedText = norm(a.text || "");
|
|
2668
|
+
const tryItem = (idx) => {
|
|
2669
|
+
const it = items[idx];
|
|
2670
|
+
if (!it) return null;
|
|
2671
|
+
const e = a.selector !== a.listSelector ? it.querySelector(a.selector) : it;
|
|
2672
|
+
return e || (a.selector !== a.listSelector ? it : null);
|
|
2673
|
+
};
|
|
2674
|
+
let item = items[a.index];
|
|
2675
|
+
if (!item && a.index > 0) item = items[a.index - 1];
|
|
2676
|
+
if (item) {
|
|
2677
|
+
el = tryItem(a.index) || (a.index > 0 ? tryItem(a.index - 1) : null);
|
|
2678
|
+
if (!el && a.selector !== a.listSelector) el = item;
|
|
2679
|
+
if (a.type === "visible" && expectedText && el) {
|
|
2680
|
+
const actualText = getText(el);
|
|
2681
|
+
const textMismatch = actualText.indexOf(expectedText) === -1 && expectedText.indexOf(actualText) === -1;
|
|
2682
|
+
if (textMismatch) {
|
|
2683
|
+
for (let j = 0; j < items.length; j++) {
|
|
2684
|
+
const candEl = tryItem(j);
|
|
2685
|
+
if (candEl && getText(candEl).indexOf(expectedText) !== -1) {
|
|
2686
|
+
el = candEl;
|
|
2687
|
+
item = items[j];
|
|
2688
|
+
break;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
} else {
|
|
2694
|
+
indexOutOfBounds = true;
|
|
2695
|
+
}
|
|
2696
|
+
} else {
|
|
2697
|
+
const matches = r.querySelectorAll(a.selector);
|
|
2698
|
+
el = matches.length > 0 ? matches[0] : o.D.querySelector(a.selector);
|
|
2699
|
+
}
|
|
2700
|
+
if (a.type === "visible") {
|
|
2701
|
+
const visible = el && el.nodeType === 1 && (el.offsetParent !== null || el.getBoundingClientRect && el.getBoundingClientRect().width > 0);
|
|
2702
|
+
const expectedText = norm(a.text || "");
|
|
2703
|
+
const actualText = getText(el);
|
|
2704
|
+
const fullActual = actualText;
|
|
2705
|
+
const textOk = !expectedText || actualText.indexOf(expectedText) !== -1 || fullActual.indexOf(expectedText) !== -1 || expectedText.length > 0 && expectedText.indexOf(actualText) !== -1;
|
|
2706
|
+
if (visible && textOk) {
|
|
2707
|
+
passed += 1;
|
|
2708
|
+
} else {
|
|
2709
|
+
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";
|
|
2710
|
+
failures.push({ selector: a.selector, message });
|
|
2711
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
2712
|
+
console.warn("[runRecordingAssertions] visible failed:", {
|
|
2713
|
+
actionIdx: a.actionIdx,
|
|
2714
|
+
selector: a.selector,
|
|
2715
|
+
listSelector: a.listSelector,
|
|
2716
|
+
index: a.index,
|
|
2717
|
+
expectedText: a.text || "(any)",
|
|
2718
|
+
actualText: actualText.slice(0, 80),
|
|
2719
|
+
message
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
} else if (a.type === "class") {
|
|
2724
|
+
const tokens = (a.className || "").trim().split(/\s+/).filter(Boolean);
|
|
2725
|
+
const hasClass = el && (tokens.length === 0 || tokens.every((c) => el.classList?.contains(c)));
|
|
2726
|
+
if (hasClass) {
|
|
2727
|
+
passed += 1;
|
|
2728
|
+
} else {
|
|
2729
|
+
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}"`;
|
|
2730
|
+
failures.push({ selector: a.selector, message: msg });
|
|
2731
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
2732
|
+
console.warn("[runRecordingAssertions] failed:", {
|
|
2733
|
+
type: a.type,
|
|
2734
|
+
selector: a.selector,
|
|
2735
|
+
actionIdx: a.actionIdx,
|
|
2736
|
+
listSelector: a.listSelector,
|
|
2737
|
+
index: a.index,
|
|
2738
|
+
itemsInRoot: a.listSelector ? r.querySelectorAll(a.listSelector).length : "-",
|
|
2739
|
+
message: msg
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
return { passed, total: deduped.length, failures };
|
|
2746
|
+
};
|
|
2747
|
+
o.exportTest = (recording, options = {}) => {
|
|
2748
|
+
const delay = options.delay !== void 0 ? options.delay : 16;
|
|
2749
|
+
const recordingData = {
|
|
2750
|
+
actions: recording.actions,
|
|
2751
|
+
assertions: recording.assertions || [],
|
|
2752
|
+
observeRoot: recording.observeRoot || null
|
|
2753
|
+
};
|
|
2754
|
+
const rootVar = recording.observeRoot ? `(o.D.querySelector('${recording.observeRoot.replace(/'/g, "\\'")}') || o.D.body)` : "o.D.body";
|
|
2755
|
+
const getEl = (a) => {
|
|
2756
|
+
if (a.listSelector != null && a.targetIndex != null) {
|
|
2757
|
+
const listSel = JSON.stringify(a.listSelector);
|
|
2758
|
+
const useItem = a.target === a.listSelector;
|
|
2759
|
+
const targetSel = useItem ? listSel : JSON.stringify(a.target);
|
|
2760
|
+
return ` const items = o.D.querySelectorAll(${listSel});
|
|
2761
|
+
const item = items[${a.targetIndex}];
|
|
2762
|
+
let el = null;
|
|
2763
|
+
if (item) { el = ${useItem ? "item" : `item.querySelector(${targetSel}) || item`}; }`;
|
|
2764
|
+
}
|
|
2765
|
+
return ` const el = o.D.querySelector(${JSON.stringify(a.target)});`;
|
|
2766
|
+
};
|
|
2767
|
+
const endSuffix = delay > 0 ? `
|
|
2768
|
+
await o.sleep(${delay});
|
|
2769
|
+
return true;
|
|
2770
|
+
` : ` return true;
|
|
2771
|
+
`;
|
|
2772
|
+
const stepFn = delay > 0 ? "async () =>" : "() =>";
|
|
2773
|
+
const steps = [];
|
|
2774
|
+
for (let i = 0; i < recording.actions.length; i++) {
|
|
2775
|
+
const a = recording.actions[i];
|
|
2276
2776
|
let body;
|
|
2277
2777
|
if (a.type === "scroll") {
|
|
2278
|
-
body = ` window.scrollTo(0, ${a.scrollY || 0})
|
|
2279
|
-
`;
|
|
2778
|
+
body = ` window.scrollTo(0, ${a.scrollY || 0});${endSuffix}`;
|
|
2280
2779
|
} else if (a.type === "input" || a.type === "change") {
|
|
2281
2780
|
body = (a.value !== void 0 ? ` el.value = ${JSON.stringify(a.value)};
|
|
2282
2781
|
` : "") + (a.checked !== void 0 ? ` el.checked = ${a.checked};
|
|
2283
|
-
` : "") + ` el.dispatchEvent(new Event('${a.type}', {bubbles:true}))
|
|
2284
|
-
`;
|
|
2782
|
+
` : "") + ` el.dispatchEvent(new Event('${a.type}', {bubbles:true}));${endSuffix}`;
|
|
2285
2783
|
} else {
|
|
2286
2784
|
const useNativeClick = a.type === "click";
|
|
2287
|
-
body = useNativeClick ? ` el.click()
|
|
2288
|
-
` : ` el.dispatchEvent(new MouseEvent('${a.type}', {bubbles:true,cancelable:true})); return true;
|
|
2289
|
-
`;
|
|
2785
|
+
body = useNativeClick ? ` el.click();${endSuffix}` : ` el.dispatchEvent(new MouseEvent('${a.type}', {bubbles:true,cancelable:true}));${endSuffix}`;
|
|
2290
2786
|
}
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2787
|
+
steps.push(
|
|
2788
|
+
` ['${a.type} on ${a.target}', ${stepFn} {
|
|
2789
|
+
` + getEl(a) + `
|
|
2790
|
+
if (!el && '${a.type}' !== 'scroll') return 'element not found: ${a.target.replace(/'/g, "\\'")}';
|
|
2791
|
+
` + body + ` }]`
|
|
2792
|
+
);
|
|
2793
|
+
const assertsForAction = (recording.assertions || []).filter((x) => x.actionIdx === i);
|
|
2794
|
+
if (assertsForAction.length > 0) {
|
|
2795
|
+
steps.push(
|
|
2796
|
+
` ['assert after ${a.type}', () => {
|
|
2797
|
+
const r = o.runRecordingAssertions(recordingData, ${rootVar}, ${i});
|
|
2798
|
+
return r.passed === r.total ? true : r.failures.map(f => f.selector + ': ' + f.message).join('; ');
|
|
2799
|
+
}]`
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
const mocksStr = Object.keys(recording.mocks || {}).length ? JSON.stringify(recording.mocks, null, 2) : "{}";
|
|
2297
2804
|
return `// Auto-generated by o.exportTest() \u2014 review and anonymize mocks before committing
|
|
2298
2805
|
const recordingMocks = ${mocksStr};
|
|
2806
|
+
const recordingData = { actions: ${JSON.stringify(recording.actions)}, assertions: ${JSON.stringify(recording.assertions || [])}, observeRoot: ${JSON.stringify(recording.observeRoot || null)} };
|
|
2299
2807
|
|
|
2300
2808
|
o.addTest('Recorded test', [
|
|
2301
|
-
${
|
|
2809
|
+
${steps.join(",\n")}
|
|
2810
|
+
// Add manual checks: ['Manual: label', () => o.testConfirm('label', ['item1'])],
|
|
2302
2811
|
], () => {
|
|
2303
2812
|
// teardown
|
|
2304
2813
|
});
|
|
@@ -2390,60 +2899,151 @@ test(${JSON.stringify(testName)}, async ({ page }) => {
|
|
|
2390
2899
|
`) + `});
|
|
2391
2900
|
`;
|
|
2392
2901
|
};
|
|
2393
|
-
o.playRecording = (recording,
|
|
2902
|
+
o.playRecording = (recording, opts = {}) => {
|
|
2903
|
+
const isOptions = opts && typeof opts === "object" && (opts.runAssertions !== void 0 || opts.root !== void 0 || opts.manualChecks !== void 0 || opts.actionDelay !== void 0);
|
|
2904
|
+
const mockOverrides = isOptions ? opts.mockOverrides || {} : opts;
|
|
2905
|
+
const runAssertions = isOptions && opts.runAssertions;
|
|
2906
|
+
const rootOpt = isOptions ? opts.root : void 0;
|
|
2907
|
+
const manualChecks = isOptions && opts.manualChecks || [];
|
|
2908
|
+
const actionDelay = isOptions && opts.actionDelay !== void 0 ? opts.actionDelay : 16;
|
|
2394
2909
|
const allMocks = Object.assign({}, recording.mocks, mockOverrides);
|
|
2395
2910
|
const origFetch = window.fetch;
|
|
2396
|
-
window.fetch = (url,
|
|
2397
|
-
const method = (
|
|
2911
|
+
window.fetch = (url, opts2 = {}) => {
|
|
2912
|
+
const method = (opts2.method || "GET").toUpperCase();
|
|
2398
2913
|
const key = method + ":" + url;
|
|
2399
2914
|
if (allMocks[key]) {
|
|
2400
2915
|
const mock = allMocks[key];
|
|
2401
2916
|
const body = typeof mock.response === "string" ? mock.response : JSON.stringify(mock.response);
|
|
2402
2917
|
return Promise.resolve(new Response(body, { status: mock.status || 200 }));
|
|
2403
2918
|
}
|
|
2404
|
-
return origFetch(url,
|
|
2919
|
+
return origFetch(url, opts2);
|
|
2405
2920
|
};
|
|
2406
|
-
const
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2921
|
+
const resolveRoot = () => {
|
|
2922
|
+
if (rootOpt != null) {
|
|
2923
|
+
return typeof rootOpt === "string" ? o.D.querySelector(rootOpt) || o.D.body : rootOpt;
|
|
2924
|
+
}
|
|
2925
|
+
const sel = recording.observeRoot;
|
|
2926
|
+
return sel ? o.D.querySelector(sel) || o.D.body : o.D.body;
|
|
2927
|
+
};
|
|
2928
|
+
const rootEl = runAssertions ? resolveRoot() : null;
|
|
2929
|
+
const actionScope = rootOpt != null ? resolveRoot() : o.D;
|
|
2930
|
+
const actions = recording.actions;
|
|
2931
|
+
const assertions = recording.assertions || [];
|
|
2932
|
+
const assertionsByAction = {};
|
|
2933
|
+
for (const a of assertions) {
|
|
2934
|
+
const k = a.actionIdx;
|
|
2935
|
+
if (!assertionsByAction[k]) assertionsByAction[k] = [];
|
|
2936
|
+
assertionsByAction[k].push(a);
|
|
2937
|
+
}
|
|
2938
|
+
if (o.recordingAssertionDebug && runAssertions && typeof console !== "undefined" && console.log) {
|
|
2939
|
+
const summary = actions.map((act, i) => ({
|
|
2940
|
+
i,
|
|
2941
|
+
action: act.type + " " + (act.target || ""),
|
|
2942
|
+
assertions: (assertionsByAction[i] || []).length,
|
|
2943
|
+
assertionDetails: (assertionsByAction[i] || []).map((x) => ({
|
|
2944
|
+
type: x.type,
|
|
2945
|
+
index: x.index,
|
|
2946
|
+
text: (x.text || "").slice(0, 30)
|
|
2947
|
+
}))
|
|
2948
|
+
}));
|
|
2949
|
+
console.log("[playRecording] assertions by action:", summary);
|
|
2950
|
+
}
|
|
2951
|
+
const manualByAction = {};
|
|
2952
|
+
for (const mc of manualChecks) {
|
|
2953
|
+
const k = mc.afterAction;
|
|
2954
|
+
if (!manualByAction[k]) manualByAction[k] = [];
|
|
2955
|
+
manualByAction[k].push(mc);
|
|
2956
|
+
}
|
|
2957
|
+
const testCases = [];
|
|
2958
|
+
let assertionAccum = { passed: 0, total: 0, failures: [] };
|
|
2959
|
+
for (let i = 0; i < actions.length; i++) {
|
|
2960
|
+
const action = actions[i];
|
|
2961
|
+
testCases.push([
|
|
2962
|
+
`${action.type} on ${action.target}`,
|
|
2963
|
+
async () => {
|
|
2964
|
+
let el = null;
|
|
2965
|
+
const scope = actionScope;
|
|
2966
|
+
if (action.target) {
|
|
2967
|
+
if (action.listSelector != null && action.targetIndex != null) {
|
|
2968
|
+
const items = scope.querySelectorAll(action.listSelector);
|
|
2969
|
+
const item = items[action.targetIndex];
|
|
2970
|
+
if (item) {
|
|
2971
|
+
el = action.target !== action.listSelector ? item.querySelector(action.target) : item;
|
|
2972
|
+
if (!el && action.target !== action.listSelector) el = item;
|
|
2973
|
+
}
|
|
2974
|
+
} else {
|
|
2975
|
+
el = scope.querySelector(action.target);
|
|
2417
2976
|
}
|
|
2418
|
-
} else {
|
|
2419
|
-
el = o.D.querySelector(action.target);
|
|
2420
2977
|
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
el.dispatchEvent(new Event(action.type, { bubbles: true }));
|
|
2431
|
-
} else {
|
|
2432
|
-
if (action.type === "click") {
|
|
2433
|
-
el.click();
|
|
2978
|
+
if (!el && action.type !== "scroll") {
|
|
2979
|
+
return `element not found: ${action.target}`;
|
|
2980
|
+
}
|
|
2981
|
+
if (action.type === "scroll") {
|
|
2982
|
+
window.scrollTo(0, action.scrollY || 0);
|
|
2983
|
+
} else if (action.type === "input" || action.type === "change") {
|
|
2984
|
+
if (action.value !== void 0) el.value = action.value;
|
|
2985
|
+
if (action.checked !== void 0) el.checked = action.checked;
|
|
2986
|
+
el.dispatchEvent(new Event(action.type, { bubbles: true }));
|
|
2434
2987
|
} else {
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2988
|
+
if (action.type === "click") {
|
|
2989
|
+
el.click();
|
|
2990
|
+
} else {
|
|
2991
|
+
el.dispatchEvent(
|
|
2992
|
+
new MouseEvent(action.type, { bubbles: true, cancelable: true })
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2438
2995
|
}
|
|
2996
|
+
if (actionDelay > 0) await o.sleep(actionDelay);
|
|
2997
|
+
return true;
|
|
2439
2998
|
}
|
|
2440
|
-
|
|
2999
|
+
]);
|
|
3000
|
+
const asserted = assertionsByAction[i];
|
|
3001
|
+
if (runAssertions && asserted && asserted.length > 0) {
|
|
3002
|
+
testCases.push([
|
|
3003
|
+
`assert after ${action.type}`,
|
|
3004
|
+
() => new Promise((resolve) => {
|
|
3005
|
+
const run = () => {
|
|
3006
|
+
const r = o.runRecordingAssertions(recording, rootEl, i, {
|
|
3007
|
+
assertions: asserted,
|
|
3008
|
+
removedElements: recording.removedElements
|
|
3009
|
+
});
|
|
3010
|
+
assertionAccum.passed += r.passed;
|
|
3011
|
+
assertionAccum.total += r.total;
|
|
3012
|
+
assertionAccum.failures.push(...r.failures);
|
|
3013
|
+
resolve(
|
|
3014
|
+
r.passed === r.total ? true : r.failures.map((f) => f.selector + ": " + f.message).join("; ")
|
|
3015
|
+
);
|
|
3016
|
+
};
|
|
3017
|
+
requestAnimationFrame(() => requestAnimationFrame(run));
|
|
3018
|
+
})
|
|
3019
|
+
]);
|
|
2441
3020
|
}
|
|
2442
|
-
|
|
2443
|
-
|
|
3021
|
+
for (const mc of manualByAction[i] || []) {
|
|
3022
|
+
testCases.push([
|
|
3023
|
+
`Manual: ${mc.label}`,
|
|
3024
|
+
() => typeof o.testConfirm === "function" ? o.testConfirm(mc.label, mc.items || []) : { ok: true }
|
|
3025
|
+
]);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
for (const mc of manualByAction["end"] || []) {
|
|
3029
|
+
testCases.push([
|
|
3030
|
+
`Manual: ${mc.label}`,
|
|
3031
|
+
() => typeof o.testConfirm === "function" ? o.testConfirm(mc.label, mc.items || []) : { ok: true }
|
|
3032
|
+
]);
|
|
3033
|
+
}
|
|
3034
|
+
const onComplete = isOptions && opts.onComplete;
|
|
3035
|
+
const testId = o.test("Recorded playback", ...testCases, { sync: true }, (testId2) => {
|
|
2444
3036
|
window.fetch = origFetch;
|
|
3037
|
+
const assertionResult = runAssertions && assertions.length > 0 ? assertionAccum : void 0;
|
|
3038
|
+
if (assertionResult?.failures?.length > 0) {
|
|
3039
|
+
o.tRes[testId2] = false;
|
|
3040
|
+
const failLines = assertionResult.failures.map((f) => `${f.selector}: ${f.message}`).join("; ");
|
|
3041
|
+
const suffix = o.tStyled ? o.tPre + o.tXx + "Assertions failed: " + failLines + o.tDc : "\n\u2718 Assertions failed: " + failLines;
|
|
3042
|
+
o.tLog[testId2] = (o.tLog[testId2] || "") + suffix;
|
|
3043
|
+
}
|
|
3044
|
+
if (typeof onComplete === "function") onComplete(assertionResult);
|
|
2445
3045
|
});
|
|
2446
|
-
return testId;
|
|
3046
|
+
return runAssertions ? { testId } : testId;
|
|
2447
3047
|
};
|
|
2448
3048
|
o.testOverlay = () => {
|
|
2449
3049
|
const btnId = "o-test-overlay-btn";
|
|
@@ -2472,54 +3072,14 @@ o.testOverlay = () => {
|
|
|
2472
3072
|
a.click();
|
|
2473
3073
|
});
|
|
2474
3074
|
};
|
|
2475
|
-
const
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
transform: "translateX(-50%)",
|
|
2480
|
-
"z-index": "999999",
|
|
2481
|
-
width: "fit-content",
|
|
2482
|
-
"max-width": "min(90vw, 420px)",
|
|
2483
|
-
"font-family": "system-ui,sans-serif",
|
|
2484
|
-
cursor: "grab",
|
|
2485
|
-
"user-select": "text"
|
|
2486
|
-
};
|
|
2487
|
-
const box = o.initState({
|
|
2488
|
-
tag: "div",
|
|
2489
|
-
id: btnId,
|
|
3075
|
+
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>`;
|
|
3076
|
+
const box = o.overlay({
|
|
3077
|
+
innerHTML,
|
|
3078
|
+
removeExisting: false,
|
|
2490
3079
|
className: "o-test-overlay",
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
}).appendInside("body");
|
|
2494
|
-
const applyOverlayStyle = () => {
|
|
2495
|
-
box.css(overlayStyle);
|
|
2496
|
-
};
|
|
2497
|
-
let drag = null;
|
|
2498
|
-
const onMove = (e) => {
|
|
2499
|
-
if (!drag) return;
|
|
2500
|
-
overlayStyle.left = drag.left + (e.clientX - drag.startX) + "px";
|
|
2501
|
-
overlayStyle.top = drag.top + (e.clientY - drag.startY) + "px";
|
|
2502
|
-
delete overlayStyle.bottom;
|
|
2503
|
-
overlayStyle.transform = "none";
|
|
2504
|
-
applyOverlayStyle();
|
|
2505
|
-
};
|
|
2506
|
-
const onUp = () => {
|
|
2507
|
-
if (drag) {
|
|
2508
|
-
overlayStyle.cursor = "grab";
|
|
2509
|
-
applyOverlayStyle();
|
|
2510
|
-
}
|
|
2511
|
-
drag = null;
|
|
2512
|
-
};
|
|
2513
|
-
box.on("mousedown", (e) => {
|
|
2514
|
-
if (e.target.closest(".o-test-overlay-close") || e.target.closest(".o-test-overlay-toggle") || e.target.closest("#" + panelId))
|
|
2515
|
-
return;
|
|
2516
|
-
const r = box.el.getBoundingClientRect();
|
|
2517
|
-
drag = { startX: e.clientX, startY: e.clientY, left: r.left, top: r.top };
|
|
2518
|
-
overlayStyle.cursor = "grabbing";
|
|
2519
|
-
applyOverlayStyle();
|
|
3080
|
+
id: btnId,
|
|
3081
|
+
excludeDragSelector: ".o-test-overlay-close, .o-test-overlay-toggle, #" + panelId
|
|
2520
3082
|
});
|
|
2521
|
-
o.D.addEventListener("mousemove", onMove);
|
|
2522
|
-
o.D.addEventListener("mouseup", onUp);
|
|
2523
3083
|
const refreshSummary = () => {
|
|
2524
3084
|
const summary = o(".o-test-overlay-summary");
|
|
2525
3085
|
if (summary.els.length)
|
|
@@ -2533,9 +3093,7 @@ o.testOverlay = () => {
|
|
|
2533
3093
|
if (!isOpen) updatePanel();
|
|
2534
3094
|
});
|
|
2535
3095
|
box.first(".o-test-overlay-close").on("click", () => {
|
|
2536
|
-
|
|
2537
|
-
o.D.removeEventListener("mouseup", onUp);
|
|
2538
|
-
box.remove();
|
|
3096
|
+
box._overlayCleanup();
|
|
2539
3097
|
});
|
|
2540
3098
|
o.testOverlay.showPanel = () => {
|
|
2541
3099
|
const panel = o("#" + panelId);
|
|
@@ -2557,43 +3115,18 @@ o.testOverlay = () => {
|
|
|
2557
3115
|
return id;
|
|
2558
3116
|
};
|
|
2559
3117
|
};
|
|
2560
|
-
o.
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
className: "o-tc-overlay",
|
|
2573
|
-
style: "position:fixed;left:50%;bottom:50px;transform:translateX(-50%);z-index:999999;width:fit-content;max-width:min(90vw,400px);font-family:system-ui,sans-serif;cursor:grab;user-select:text;",
|
|
2574
|
-
html: `<div class="o-tc-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:280px;"><div style="display:flex;align-items:center;gap:12px;"><span class="o-tc-label" style="flex:1;">${label}: Paused</span><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></div>` + itemsHtml + `</div>`
|
|
2575
|
-
}).appendInside("body");
|
|
2576
|
-
const okBtnStyles = {
|
|
2577
|
-
padding: "6px 14px",
|
|
2578
|
-
background: hasCheckboxes ? "#dc2626" : "#2563eb",
|
|
2579
|
-
color: "#fff",
|
|
2580
|
-
border: "none",
|
|
2581
|
-
"border-radius": "6px",
|
|
2582
|
-
"font-weight": "600",
|
|
2583
|
-
cursor: "pointer",
|
|
2584
|
-
"font-size": "13px",
|
|
2585
|
-
"flex-shrink": "0"
|
|
2586
|
-
};
|
|
2587
|
-
if (hasCheckboxes) {
|
|
2588
|
-
const okBtn = box.first(".o-tc-ok");
|
|
2589
|
-
const cbs = o(".o-tc-overlay .o-tc-item-cb");
|
|
2590
|
-
const updateBtn = () => {
|
|
2591
|
-
const allChecked = cbs.length > 0 && cbs.els.every((el) => el.checked);
|
|
2592
|
-
okBtn.css({ ...okBtnStyles, background: allChecked ? "#16a34a" : "#dc2626" });
|
|
2593
|
-
};
|
|
2594
|
-
cbs.on("change", updateBtn);
|
|
2595
|
-
}
|
|
2596
|
-
let drag = null;
|
|
3118
|
+
o.overlay = (opts = {}) => {
|
|
3119
|
+
const {
|
|
3120
|
+
innerHTML,
|
|
3121
|
+
onClose,
|
|
3122
|
+
timeout,
|
|
3123
|
+
excludeDragSelector,
|
|
3124
|
+
removeExisting = true,
|
|
3125
|
+
className = "o-overlay-common",
|
|
3126
|
+
id
|
|
3127
|
+
} = opts;
|
|
3128
|
+
if (removeExisting) o("." + className).remove();
|
|
3129
|
+
else if (id && o("#" + id).el) return o("#" + id);
|
|
2597
3130
|
const overlayStyle = {
|
|
2598
3131
|
position: "fixed",
|
|
2599
3132
|
left: "50%",
|
|
@@ -2601,54 +3134,118 @@ o.testConfirm = (label, items = [], opts = {}) => new Promise((resolve) => {
|
|
|
2601
3134
|
transform: "translateX(-50%)",
|
|
2602
3135
|
"z-index": "999999",
|
|
2603
3136
|
width: "fit-content",
|
|
2604
|
-
"max-width": "min(90vw,
|
|
3137
|
+
"max-width": "min(90vw, 420px)",
|
|
2605
3138
|
"font-family": "system-ui,sans-serif",
|
|
2606
|
-
cursor: "grab",
|
|
2607
3139
|
"user-select": "text"
|
|
2608
3140
|
};
|
|
2609
|
-
const
|
|
2610
|
-
|
|
2611
|
-
|
|
3141
|
+
const countdownId = "o-overlay-countdown";
|
|
3142
|
+
const barHtml = `<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;">` + innerHTML + (timeout ? `<div id="${countdownId}" style="margin-top:6px;font-size:11px;color:#94a3b8;"></div>` : "") + "</div>";
|
|
3143
|
+
const box = o.initState({
|
|
3144
|
+
tag: "div",
|
|
3145
|
+
className,
|
|
3146
|
+
id: id || void 0,
|
|
3147
|
+
style: "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;",
|
|
3148
|
+
html: barHtml
|
|
3149
|
+
}).appendInside("body");
|
|
3150
|
+
const applyStyle = () => box.css(overlayStyle);
|
|
3151
|
+
let drag = null;
|
|
2612
3152
|
const onMove = (e) => {
|
|
2613
3153
|
if (!drag) return;
|
|
2614
3154
|
overlayStyle.left = drag.left + (e.clientX - drag.startX) + "px";
|
|
2615
3155
|
overlayStyle.top = drag.top + (e.clientY - drag.startY) + "px";
|
|
2616
3156
|
delete overlayStyle.bottom;
|
|
2617
3157
|
overlayStyle.transform = "none";
|
|
2618
|
-
|
|
3158
|
+
applyStyle();
|
|
2619
3159
|
};
|
|
2620
3160
|
const onUp = () => {
|
|
2621
3161
|
if (drag) {
|
|
2622
|
-
overlayStyle.cursor
|
|
2623
|
-
|
|
3162
|
+
delete overlayStyle.cursor;
|
|
3163
|
+
applyStyle();
|
|
2624
3164
|
}
|
|
2625
3165
|
drag = null;
|
|
2626
3166
|
};
|
|
2627
3167
|
box.on("mousedown", (e) => {
|
|
2628
|
-
if (e.target.closest(
|
|
3168
|
+
if (excludeDragSelector && e.target.closest(excludeDragSelector)) return;
|
|
2629
3169
|
const r = box.el.getBoundingClientRect();
|
|
2630
3170
|
drag = { startX: e.clientX, startY: e.clientY, left: r.left, top: r.top };
|
|
2631
3171
|
overlayStyle.cursor = "grabbing";
|
|
2632
|
-
|
|
3172
|
+
applyStyle();
|
|
2633
3173
|
});
|
|
2634
3174
|
o.D.addEventListener("mousemove", onMove);
|
|
2635
3175
|
o.D.addEventListener("mouseup", onUp);
|
|
2636
|
-
|
|
3176
|
+
let timerId;
|
|
3177
|
+
const cleanup = () => {
|
|
2637
3178
|
o.D.removeEventListener("mousemove", onMove);
|
|
2638
3179
|
o.D.removeEventListener("mouseup", onUp);
|
|
3180
|
+
if (timerId) clearInterval(timerId);
|
|
3181
|
+
box.remove();
|
|
3182
|
+
};
|
|
3183
|
+
if (timeout && timeout > 0) {
|
|
3184
|
+
let remaining = Math.ceil(timeout / 1e3);
|
|
3185
|
+
const cd = o("#" + countdownId);
|
|
3186
|
+
if (cd.el) cd.el.textContent = remaining ? `Continue in ${remaining}s` : "";
|
|
3187
|
+
timerId = setInterval(() => {
|
|
3188
|
+
remaining -= 1;
|
|
3189
|
+
if (cd.el) cd.el.textContent = remaining > 0 ? `Continue in ${remaining}s` : "";
|
|
3190
|
+
if (remaining <= 0) {
|
|
3191
|
+
clearInterval(timerId);
|
|
3192
|
+
timerId = null;
|
|
3193
|
+
cleanup();
|
|
3194
|
+
if (typeof onClose === "function") onClose({ ok: false, errors: ["timeout"] });
|
|
3195
|
+
}
|
|
3196
|
+
}, 1e3);
|
|
3197
|
+
}
|
|
3198
|
+
box._overlayCleanup = cleanup;
|
|
3199
|
+
box._overlayOnClose = onClose;
|
|
3200
|
+
return box;
|
|
3201
|
+
};
|
|
3202
|
+
o.testConfirm = (label, items = [], opts = {}) => new Promise((resolve) => {
|
|
3203
|
+
const btnLabel = opts.confirm || "Continue";
|
|
3204
|
+
const hasCheckboxes = items.length > 0;
|
|
3205
|
+
const btnBg = hasCheckboxes ? "#dc2626" : "#2563eb";
|
|
3206
|
+
const itemIds = items.map((_, idx) => "o-tc-cb-" + idx);
|
|
3207
|
+
const checkboxStyle = `.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;}.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;}`;
|
|
3208
|
+
const itemsHtml = hasCheckboxes ? `<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;">` + items.map(
|
|
3209
|
+
(i, idx) => `<li style="margin-bottom:4px;"><label for="${itemIds[idx]}" style="display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none;"><input type="checkbox" id="${itemIds[idx]}" class="o-tc-item-cb"> <span>${i}</span></label></li>`
|
|
3210
|
+
).join("") + "</ul>" : "";
|
|
3211
|
+
const innerHTML = `<div style="display:flex;align-items:center;gap:12px;"><span class="o-tc-label" style="flex:1;cursor:grab;">${label}: Paused</span><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></div>` + itemsHtml;
|
|
3212
|
+
const box = o.overlay({
|
|
3213
|
+
innerHTML,
|
|
3214
|
+
timeout: opts.timeout,
|
|
3215
|
+
excludeDragSelector: ".o-tc-ok",
|
|
3216
|
+
onClose: (r) => resolve(r || { ok: true })
|
|
3217
|
+
});
|
|
3218
|
+
const okBtnStyles = {
|
|
3219
|
+
padding: "6px 14px",
|
|
3220
|
+
background: hasCheckboxes ? "#dc2626" : "#2563eb",
|
|
3221
|
+
color: "#fff",
|
|
3222
|
+
border: "none",
|
|
3223
|
+
"border-radius": "6px",
|
|
3224
|
+
"font-weight": "600",
|
|
3225
|
+
cursor: "pointer",
|
|
3226
|
+
"font-size": "13px",
|
|
3227
|
+
"flex-shrink": "0"
|
|
3228
|
+
};
|
|
3229
|
+
if (hasCheckboxes) {
|
|
3230
|
+
const okBtn = box.first(".o-tc-ok");
|
|
3231
|
+
const cbs = o(".o-overlay-common .o-tc-item-cb");
|
|
3232
|
+
const updateBtn = () => {
|
|
3233
|
+
const allChecked = cbs.length > 0 && cbs.els.every((el) => el.checked);
|
|
3234
|
+
okBtn.css({ ...okBtnStyles, background: allChecked ? "#16a34a" : "#dc2626" });
|
|
3235
|
+
};
|
|
3236
|
+
cbs.on("change", updateBtn);
|
|
3237
|
+
}
|
|
3238
|
+
box.first(".o-tc-ok").on("click", () => {
|
|
2639
3239
|
let unchecked = [];
|
|
2640
3240
|
if (hasCheckboxes) {
|
|
2641
|
-
const cbsList = o(".o-
|
|
2642
|
-
cbsList.els.
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
box.remove();
|
|
2647
|
-
if (unchecked.length === 0) {
|
|
2648
|
-
resolve({ ok: true });
|
|
2649
|
-
} else {
|
|
2650
|
-
resolve({ ok: false, errors: unchecked });
|
|
3241
|
+
const cbsList = o(".o-overlay-common .o-tc-item-cb");
|
|
3242
|
+
if (cbsList.els.length)
|
|
3243
|
+
cbsList.els.forEach((el, idx) => {
|
|
3244
|
+
if (!el.checked && items[idx] !== void 0) unchecked.push(items[idx]);
|
|
3245
|
+
});
|
|
2651
3246
|
}
|
|
3247
|
+
box._overlayCleanup();
|
|
3248
|
+
resolve(unchecked.length === 0 ? { ok: true } : { ok: false, errors: unchecked });
|
|
2652
3249
|
});
|
|
2653
3250
|
});
|
|
2654
3251
|
|