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/objs.built.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Objs-core library
3
- * @version 2.0
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
- if (type(data) === objectType) {
202
- return D.createElement(data.tag || data.tagName || "div");
203
- } else {
204
- const newElem = D.createElement("div");
205
- newElem.innerHTML = type(data) === functionType ? data(prop) : data;
206
- if (newElem.children.length > ONE || !newElem.firstElementChild) {
207
- newElem.dataset.oInit = n;
208
- return newElem;
209
- } else {
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
- result.refs[refEl.getAttribute("ref")] = o(refEl);
260
- refEl.removeAttribute("ref");
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
- if (!ssr && type(renderState) === objectType && renderState.events && renderState.ssr) {
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
- result.refs[refEl.getAttribute("ref")] = o(refEl);
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
- cycleObj(renderState.events, (event) => {
284
- result.on(event, renderState.events[event]);
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 && !result.els.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
- result.initID = initId;
307
- o.inits[initId] = result;
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
- delete result.initSSRAfterGettingSSR;
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
- typeVerify([[i2, [numberType, undefinedType]]]);
428
- if (i2 === u) {
429
- i2 = result.length - ONE;
430
- }
431
- start = i2;
432
- finish = i2;
433
- result.el = result.els[i2];
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(i, ONE);
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
- o.tRes[testN2] = done === num;
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 textEl = node.querySelector?.(".task-text") || node;
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.exportTest = (recording) => {
2275
- const cases = recording.actions.map((a) => {
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}); return true;
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})); return 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(); return true;
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
- return ` ['${a.type} on ${a.target}', () => {
2292
- const el = document.querySelector('${a.target}');
2293
- if (!el) return 'element not found';
2294
- ` + body + ` }],`;
2295
- }).join("\n");
2296
- const mocksStr = Object.keys(recording.mocks).length ? JSON.stringify(recording.mocks, null, 2) : "{}";
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
- ${cases}
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, mockOverrides = {}) => {
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, opts = {}) => {
2397
- const method = (opts.method || "GET").toUpperCase();
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, opts);
2919
+ return origFetch(url, opts2);
2405
2920
  };
2406
- const testCases = recording.actions.map((action) => [
2407
- `${action.type} on ${action.target}`,
2408
- () => {
2409
- let el = null;
2410
- if (action.target) {
2411
- if (action.listSelector != null && action.targetIndex != null) {
2412
- const items = o.D.querySelectorAll(action.listSelector);
2413
- const item = items[action.targetIndex];
2414
- if (item) {
2415
- el = action.target !== action.listSelector ? item.querySelector(action.target) : item;
2416
- if (!el && action.target !== action.listSelector) el = item;
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
- if (!el && action.type !== "scroll") {
2423
- return `element not found: ${action.target}`;
2424
- }
2425
- if (action.type === "scroll") {
2426
- window.scrollTo(0, action.scrollY || 0);
2427
- } else if (action.type === "input" || action.type === "change") {
2428
- if (action.value !== void 0) el.value = action.value;
2429
- if (action.checked !== void 0) el.checked = action.checked;
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
- el.dispatchEvent(
2436
- new MouseEvent(action.type, { bubbles: true, cancelable: true })
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
- return true;
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
- const testId = o.test("Recorded playback", ...testCases, () => {
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 overlayStyle = {
2476
- position: "fixed",
2477
- left: "50%",
2478
- bottom: "50px",
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
- 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;cursor:grab;user-select:text;",
2492
- html: `<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;"><div style="display:flex;align-items:center;gap:12px;"><span class="o-test-overlay-summary" style="flex:1;font-size:13px;">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><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>`
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
- o.D.removeEventListener("mousemove", onMove);
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.testConfirm = (label, items = [], opts = {}) => new Promise((resolve) => {
2561
- o(".o-tc-overlay").remove();
2562
- const btnLabel = opts.confirm || "Continue";
2563
- const hasCheckboxes = items.length > 0;
2564
- const btnBg = hasCheckboxes ? "#dc2626" : "#2563eb";
2565
- const itemIds = items.map((_, idx) => "o-tc-cb-" + idx);
2566
- 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;}`;
2567
- 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;">` + items.map(
2568
- (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>`
2569
- ).join("") + "</ul>" : "";
2570
- const box = o.initState({
2571
- tag: "div",
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, 400px)",
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 applyOverlayStyle = () => {
2610
- box.css(overlayStyle);
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
- applyOverlayStyle();
3158
+ applyStyle();
2619
3159
  };
2620
3160
  const onUp = () => {
2621
3161
  if (drag) {
2622
- overlayStyle.cursor = "grab";
2623
- applyOverlayStyle();
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(".o-tc-ok")) return;
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
- applyOverlayStyle();
3172
+ applyStyle();
2633
3173
  });
2634
3174
  o.D.addEventListener("mousemove", onMove);
2635
3175
  o.D.addEventListener("mouseup", onUp);
2636
- box.first(".o-tc-ok").on("click", () => {
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-tc-overlay .o-tc-item-cb");
2642
- cbsList.els.forEach((el, idx) => {
2643
- if (!el.checked && items[idx] !== void 0) unchecked.push(items[idx]);
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