objs-core 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/objs.built.js +667 -187
- package/objs.built.min.js +48 -38
- package/objs.d.ts +56 -5
- package/objs.js +818 -260
- package/package.json +1 -1
package/objs.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
|
*/
|
|
@@ -788,6 +788,14 @@ const o = (query) => {
|
|
|
788
788
|
return html;
|
|
789
789
|
}
|
|
790
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
|
+
};
|
|
791
799
|
result.val = returner((value) => {
|
|
792
800
|
if (value === void 0) return result.el?.value;
|
|
793
801
|
iterator(() => {
|
|
@@ -1700,6 +1708,7 @@ o.withReactContext = (React, Context, selector, component, state = "render") =>
|
|
|
1700
1708
|
if (__DEV__) {
|
|
1701
1709
|
o.debug = false;
|
|
1702
1710
|
}
|
|
1711
|
+
o.sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1703
1712
|
o.tLog = [];
|
|
1704
1713
|
o.tRes = [];
|
|
1705
1714
|
o.tStatus = [];
|
|
@@ -1708,6 +1717,8 @@ o.tShowOk = o.F;
|
|
|
1708
1717
|
o.tStyled = o.F;
|
|
1709
1718
|
o.tTime = 2e3;
|
|
1710
1719
|
o.tests = [];
|
|
1720
|
+
o.tExpectedSteps = {};
|
|
1721
|
+
o.tFinalized = {};
|
|
1711
1722
|
o.tAutolog = o.F;
|
|
1712
1723
|
o.tBeforeEach = void 0;
|
|
1713
1724
|
o.tAfterEach = void 0;
|
|
@@ -1808,10 +1819,15 @@ o.test = (title = "", ...tests) => {
|
|
|
1808
1819
|
}
|
|
1809
1820
|
}
|
|
1810
1821
|
};
|
|
1822
|
+
let opts = {};
|
|
1811
1823
|
if (typeof tests[num - 1] === "function") {
|
|
1812
1824
|
o.tFns[testN2] = tests[num - 1];
|
|
1813
1825
|
num--;
|
|
1814
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
|
+
}
|
|
1815
1831
|
if (testSession) {
|
|
1816
1832
|
o.tLog[testN2] = sessionStorage.getItem(`oTest-Log-${testN2}`) || "";
|
|
1817
1833
|
o.tRes[testN2] = sessionStorage.getItem(`oTest-Res-${testN2}`) || false;
|
|
@@ -1839,6 +1855,143 @@ o.test = (title = "", ...tests) => {
|
|
|
1839
1855
|
o.tRes[testN2] = o.F;
|
|
1840
1856
|
o.tStatus[testN2] = [];
|
|
1841
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
|
+
}
|
|
1842
1995
|
for (let i = o.tStatus[testN2].length; i < num; i++) {
|
|
1843
1996
|
const testInfo = {
|
|
1844
1997
|
n: testN2,
|
|
@@ -1919,26 +2072,7 @@ o.test = (title = "", ...tests) => {
|
|
|
1919
2072
|
);
|
|
1920
2073
|
}
|
|
1921
2074
|
}
|
|
1922
|
-
|
|
1923
|
-
row = waits ? "\u251C " : "\u2558 ";
|
|
1924
|
-
row += "DONE " + done + "/" + num + (waits ? ", waiting: " + waits : "");
|
|
1925
|
-
log(row, done + waits !== num);
|
|
1926
|
-
if (!waits) {
|
|
1927
|
-
log();
|
|
1928
|
-
}
|
|
1929
|
-
if (o.tStyled) {
|
|
1930
|
-
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;
|
|
1931
|
-
} else {
|
|
1932
|
-
o.tLog[testN2] += row + "\n";
|
|
1933
|
-
}
|
|
1934
|
-
if (testSession) {
|
|
1935
|
-
sessionStorage.setItem(`oTest-Log-${testN2}`, o.tLog[testN2]);
|
|
1936
|
-
sessionStorage.setItem(`oTest-Res-${testN2}`, o.tRes[testN2]);
|
|
1937
|
-
sessionStorage.setItem(`oTest-Status-${testN2}`, JSON.stringify(o.tStatus[testN2]));
|
|
1938
|
-
}
|
|
1939
|
-
if (!waits && typeof o.tFns[testN2] === "function") {
|
|
1940
|
-
o.tFns[testN2](testN2);
|
|
1941
|
-
}
|
|
2075
|
+
finalize();
|
|
1942
2076
|
return testN2;
|
|
1943
2077
|
};
|
|
1944
2078
|
o.testUpdate = (info, res = o.F, suff = "") => {
|
|
@@ -1990,13 +2124,21 @@ o.testUpdate = (info, res = o.F, suff = "") => {
|
|
|
1990
2124
|
}
|
|
1991
2125
|
n++;
|
|
1992
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;
|
|
1993
2138
|
if (sessionStorage?.getItem("oTest-Run") === testN2) {
|
|
1994
2139
|
sessionStorage.setItem(`oTest-Log-${testN2}`, o.tLog[testN2]);
|
|
1995
2140
|
sessionStorage.setItem(`oTest-Res-${testN2}`, o.tRes[testN2]);
|
|
1996
2141
|
sessionStorage.setItem(`oTest-Status-${testN2}`, JSON.stringify(o.tStatus[testN2]));
|
|
1997
|
-
if (n < o.tests[testN2].tests.length) {
|
|
1998
|
-
return;
|
|
1999
|
-
}
|
|
2000
2142
|
}
|
|
2001
2143
|
o.tRes[testN2] = !fails;
|
|
2002
2144
|
row = fails ? "FAILED " + fails + "/" + n : "DONE " + n + "/" + n;
|
|
@@ -2101,6 +2243,7 @@ o.recorder = {
|
|
|
2101
2243
|
_listeners: [],
|
|
2102
2244
|
_observer: null
|
|
2103
2245
|
};
|
|
2246
|
+
o.recordingAssertionDebug = false;
|
|
2104
2247
|
o.startRecording = (observe, events, timeouts) => {
|
|
2105
2248
|
if (o.recorder.active) {
|
|
2106
2249
|
return;
|
|
@@ -2124,6 +2267,7 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2124
2267
|
rec.initialData = { url: window.location.href, timestamp: Date.now() };
|
|
2125
2268
|
rec.observeRoot = observe || null;
|
|
2126
2269
|
rec.assertions = [];
|
|
2270
|
+
rec.removedElements = [];
|
|
2127
2271
|
o.inits.forEach((inst, idx) => {
|
|
2128
2272
|
if (inst?.store) {
|
|
2129
2273
|
rec.initialData["init_" + idx] = JSON.parse(JSON.stringify(inst.store));
|
|
@@ -2204,6 +2348,16 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2204
2348
|
rec._observer = new MutationObserver((mutations) => {
|
|
2205
2349
|
const actionIdx = rec.actions.length - 1;
|
|
2206
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
|
+
}
|
|
2207
2361
|
mutations.forEach((m) => {
|
|
2208
2362
|
const addAssertionIndex = (sel, node) => {
|
|
2209
2363
|
let listSelector;
|
|
@@ -2242,13 +2396,56 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2242
2396
|
(a2) => a2.actionIdx === actionIdx && a2.selector === sel && a2.type === "visible"
|
|
2243
2397
|
))
|
|
2244
2398
|
return;
|
|
2245
|
-
const
|
|
2246
|
-
const text = (textEl.textContent?.trim() || node.textContent?.trim() || "").slice(0, 80) || void 0;
|
|
2399
|
+
const text = (node.textContent?.trim() || "").slice(0, 80) || void 0;
|
|
2247
2400
|
const { listSelector: aListSel, index: aIdx } = addAssertionIndex(sel, node);
|
|
2248
2401
|
const a = { actionIdx, type: "visible", selector: sel, text };
|
|
2249
2402
|
if (aListSel != null) a.listSelector = aListSel;
|
|
2250
2403
|
if (aIdx != null) a.index = aIdx;
|
|
2251
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
|
+
}
|
|
2252
2449
|
});
|
|
2253
2450
|
}
|
|
2254
2451
|
if (m.type === "attributes") {
|
|
@@ -2268,6 +2465,16 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2268
2465
|
if (aListSel != null) a.listSelector = aListSel;
|
|
2269
2466
|
if (aIdx != null) a.index = aIdx;
|
|
2270
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
|
+
}
|
|
2271
2478
|
}
|
|
2272
2479
|
});
|
|
2273
2480
|
});
|
|
@@ -2330,7 +2537,7 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2330
2537
|
const scrollY = ev === "scroll" ? window.scrollY : void 0;
|
|
2331
2538
|
const value = ev === "input" || ev === "change" ? target?.value : void 0;
|
|
2332
2539
|
const checked = ev === "change" && (target?.type === "checkbox" || target?.type === "radio") ? target?.checked : void 0;
|
|
2333
|
-
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;
|
|
2334
2541
|
const pushAction = () => {
|
|
2335
2542
|
const action = { type: ev, target: selector, time: Date.now() };
|
|
2336
2543
|
if (targetType) action.targetType = targetType;
|
|
@@ -2373,6 +2580,7 @@ o.stopRecording = () => {
|
|
|
2373
2580
|
initialData: { ...rec.initialData },
|
|
2374
2581
|
stepDelays: { ...rec.stepDelays },
|
|
2375
2582
|
assertions: [...rec.assertions || []],
|
|
2583
|
+
removedElements: [...rec.removedElements || []],
|
|
2376
2584
|
observeRoot: rec.observeRoot || null
|
|
2377
2585
|
};
|
|
2378
2586
|
};
|
|
@@ -2388,34 +2596,218 @@ o.clearRecording = (id) => {
|
|
|
2388
2596
|
}
|
|
2389
2597
|
}
|
|
2390
2598
|
};
|
|
2391
|
-
o.
|
|
2392
|
-
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];
|
|
2393
2776
|
let body;
|
|
2394
2777
|
if (a.type === "scroll") {
|
|
2395
|
-
body = ` window.scrollTo(0, ${a.scrollY || 0})
|
|
2396
|
-
`;
|
|
2778
|
+
body = ` window.scrollTo(0, ${a.scrollY || 0});${endSuffix}`;
|
|
2397
2779
|
} else if (a.type === "input" || a.type === "change") {
|
|
2398
2780
|
body = (a.value !== void 0 ? ` el.value = ${JSON.stringify(a.value)};
|
|
2399
2781
|
` : "") + (a.checked !== void 0 ? ` el.checked = ${a.checked};
|
|
2400
|
-
` : "") + ` el.dispatchEvent(new Event('${a.type}', {bubbles:true}))
|
|
2401
|
-
`;
|
|
2782
|
+
` : "") + ` el.dispatchEvent(new Event('${a.type}', {bubbles:true}));${endSuffix}`;
|
|
2402
2783
|
} else {
|
|
2403
2784
|
const useNativeClick = a.type === "click";
|
|
2404
|
-
body = useNativeClick ? ` el.click()
|
|
2405
|
-
` : ` el.dispatchEvent(new MouseEvent('${a.type}', {bubbles:true,cancelable:true})); return true;
|
|
2406
|
-
`;
|
|
2785
|
+
body = useNativeClick ? ` el.click();${endSuffix}` : ` el.dispatchEvent(new MouseEvent('${a.type}', {bubbles:true,cancelable:true}));${endSuffix}`;
|
|
2407
2786
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
}
|
|
2413
|
-
|
|
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) : "{}";
|
|
2414
2804
|
return `// Auto-generated by o.exportTest() \u2014 review and anonymize mocks before committing
|
|
2415
2805
|
const recordingMocks = ${mocksStr};
|
|
2806
|
+
const recordingData = { actions: ${JSON.stringify(recording.actions)}, assertions: ${JSON.stringify(recording.assertions || [])}, observeRoot: ${JSON.stringify(recording.observeRoot || null)} };
|
|
2416
2807
|
|
|
2417
2808
|
o.addTest('Recorded test', [
|
|
2418
|
-
${
|
|
2809
|
+
${steps.join(",\n")}
|
|
2810
|
+
// Add manual checks: ['Manual: label', () => o.testConfirm('label', ['item1'])],
|
|
2419
2811
|
], () => {
|
|
2420
2812
|
// teardown
|
|
2421
2813
|
});
|
|
@@ -2507,60 +2899,151 @@ test(${JSON.stringify(testName)}, async ({ page }) => {
|
|
|
2507
2899
|
`) + `});
|
|
2508
2900
|
`;
|
|
2509
2901
|
};
|
|
2510
|
-
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;
|
|
2511
2909
|
const allMocks = Object.assign({}, recording.mocks, mockOverrides);
|
|
2512
2910
|
const origFetch = window.fetch;
|
|
2513
|
-
window.fetch = (url,
|
|
2514
|
-
const method = (
|
|
2911
|
+
window.fetch = (url, opts2 = {}) => {
|
|
2912
|
+
const method = (opts2.method || "GET").toUpperCase();
|
|
2515
2913
|
const key = method + ":" + url;
|
|
2516
2914
|
if (allMocks[key]) {
|
|
2517
2915
|
const mock = allMocks[key];
|
|
2518
2916
|
const body = typeof mock.response === "string" ? mock.response : JSON.stringify(mock.response);
|
|
2519
2917
|
return Promise.resolve(new Response(body, { status: mock.status || 200 }));
|
|
2520
2918
|
}
|
|
2521
|
-
return origFetch(url,
|
|
2919
|
+
return origFetch(url, opts2);
|
|
2522
2920
|
};
|
|
2523
|
-
const
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
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);
|
|
2534
2976
|
}
|
|
2535
|
-
} else {
|
|
2536
|
-
el = o.D.querySelector(action.target);
|
|
2537
2977
|
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
el.dispatchEvent(new Event(action.type, { bubbles: true }));
|
|
2548
|
-
} else {
|
|
2549
|
-
if (action.type === "click") {
|
|
2550
|
-
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 }));
|
|
2551
2987
|
} else {
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2988
|
+
if (action.type === "click") {
|
|
2989
|
+
el.click();
|
|
2990
|
+
} else {
|
|
2991
|
+
el.dispatchEvent(
|
|
2992
|
+
new MouseEvent(action.type, { bubbles: true, cancelable: true })
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2555
2995
|
}
|
|
2996
|
+
if (actionDelay > 0) await o.sleep(actionDelay);
|
|
2997
|
+
return true;
|
|
2556
2998
|
}
|
|
2557
|
-
|
|
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
|
+
]);
|
|
2558
3020
|
}
|
|
2559
|
-
|
|
2560
|
-
|
|
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) => {
|
|
2561
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);
|
|
2562
3045
|
});
|
|
2563
|
-
return testId;
|
|
3046
|
+
return runAssertions ? { testId } : testId;
|
|
2564
3047
|
};
|
|
2565
3048
|
o.testOverlay = () => {
|
|
2566
3049
|
const btnId = "o-test-overlay-btn";
|
|
@@ -2589,54 +3072,14 @@ o.testOverlay = () => {
|
|
|
2589
3072
|
a.click();
|
|
2590
3073
|
});
|
|
2591
3074
|
};
|
|
2592
|
-
const
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
transform: "translateX(-50%)",
|
|
2597
|
-
"z-index": "999999",
|
|
2598
|
-
width: "fit-content",
|
|
2599
|
-
"max-width": "min(90vw, 420px)",
|
|
2600
|
-
"font-family": "system-ui,sans-serif",
|
|
2601
|
-
cursor: "grab",
|
|
2602
|
-
"user-select": "text"
|
|
2603
|
-
};
|
|
2604
|
-
const box = o.initState({
|
|
2605
|
-
tag: "div",
|
|
2606
|
-
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,
|
|
2607
3079
|
className: "o-test-overlay",
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
}).appendInside("body");
|
|
2611
|
-
const applyOverlayStyle = () => {
|
|
2612
|
-
box.css(overlayStyle);
|
|
2613
|
-
};
|
|
2614
|
-
let drag = null;
|
|
2615
|
-
const onMove = (e) => {
|
|
2616
|
-
if (!drag) return;
|
|
2617
|
-
overlayStyle.left = drag.left + (e.clientX - drag.startX) + "px";
|
|
2618
|
-
overlayStyle.top = drag.top + (e.clientY - drag.startY) + "px";
|
|
2619
|
-
delete overlayStyle.bottom;
|
|
2620
|
-
overlayStyle.transform = "none";
|
|
2621
|
-
applyOverlayStyle();
|
|
2622
|
-
};
|
|
2623
|
-
const onUp = () => {
|
|
2624
|
-
if (drag) {
|
|
2625
|
-
overlayStyle.cursor = "grab";
|
|
2626
|
-
applyOverlayStyle();
|
|
2627
|
-
}
|
|
2628
|
-
drag = null;
|
|
2629
|
-
};
|
|
2630
|
-
box.on("mousedown", (e) => {
|
|
2631
|
-
if (e.target.closest(".o-test-overlay-close") || e.target.closest(".o-test-overlay-toggle") || e.target.closest("#" + panelId))
|
|
2632
|
-
return;
|
|
2633
|
-
const r = box.el.getBoundingClientRect();
|
|
2634
|
-
drag = { startX: e.clientX, startY: e.clientY, left: r.left, top: r.top };
|
|
2635
|
-
overlayStyle.cursor = "grabbing";
|
|
2636
|
-
applyOverlayStyle();
|
|
3080
|
+
id: btnId,
|
|
3081
|
+
excludeDragSelector: ".o-test-overlay-close, .o-test-overlay-toggle, #" + panelId
|
|
2637
3082
|
});
|
|
2638
|
-
o.D.addEventListener("mousemove", onMove);
|
|
2639
|
-
o.D.addEventListener("mouseup", onUp);
|
|
2640
3083
|
const refreshSummary = () => {
|
|
2641
3084
|
const summary = o(".o-test-overlay-summary");
|
|
2642
3085
|
if (summary.els.length)
|
|
@@ -2650,9 +3093,7 @@ o.testOverlay = () => {
|
|
|
2650
3093
|
if (!isOpen) updatePanel();
|
|
2651
3094
|
});
|
|
2652
3095
|
box.first(".o-test-overlay-close").on("click", () => {
|
|
2653
|
-
|
|
2654
|
-
o.D.removeEventListener("mouseup", onUp);
|
|
2655
|
-
box.remove();
|
|
3096
|
+
box._overlayCleanup();
|
|
2656
3097
|
});
|
|
2657
3098
|
o.testOverlay.showPanel = () => {
|
|
2658
3099
|
const panel = o("#" + panelId);
|
|
@@ -2674,43 +3115,18 @@ o.testOverlay = () => {
|
|
|
2674
3115
|
return id;
|
|
2675
3116
|
};
|
|
2676
3117
|
};
|
|
2677
|
-
o.
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
className: "o-tc-overlay",
|
|
2690
|
-
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;",
|
|
2691
|
-
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>`
|
|
2692
|
-
}).appendInside("body");
|
|
2693
|
-
const okBtnStyles = {
|
|
2694
|
-
padding: "6px 14px",
|
|
2695
|
-
background: hasCheckboxes ? "#dc2626" : "#2563eb",
|
|
2696
|
-
color: "#fff",
|
|
2697
|
-
border: "none",
|
|
2698
|
-
"border-radius": "6px",
|
|
2699
|
-
"font-weight": "600",
|
|
2700
|
-
cursor: "pointer",
|
|
2701
|
-
"font-size": "13px",
|
|
2702
|
-
"flex-shrink": "0"
|
|
2703
|
-
};
|
|
2704
|
-
if (hasCheckboxes) {
|
|
2705
|
-
const okBtn = box.first(".o-tc-ok");
|
|
2706
|
-
const cbs = o(".o-tc-overlay .o-tc-item-cb");
|
|
2707
|
-
const updateBtn = () => {
|
|
2708
|
-
const allChecked = cbs.length > 0 && cbs.els.every((el) => el.checked);
|
|
2709
|
-
okBtn.css({ ...okBtnStyles, background: allChecked ? "#16a34a" : "#dc2626" });
|
|
2710
|
-
};
|
|
2711
|
-
cbs.on("change", updateBtn);
|
|
2712
|
-
}
|
|
2713
|
-
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);
|
|
2714
3130
|
const overlayStyle = {
|
|
2715
3131
|
position: "fixed",
|
|
2716
3132
|
left: "50%",
|
|
@@ -2718,54 +3134,118 @@ o.testConfirm = (label, items = [], opts = {}) => new Promise((resolve) => {
|
|
|
2718
3134
|
transform: "translateX(-50%)",
|
|
2719
3135
|
"z-index": "999999",
|
|
2720
3136
|
width: "fit-content",
|
|
2721
|
-
"max-width": "min(90vw,
|
|
3137
|
+
"max-width": "min(90vw, 420px)",
|
|
2722
3138
|
"font-family": "system-ui,sans-serif",
|
|
2723
|
-
cursor: "grab",
|
|
2724
3139
|
"user-select": "text"
|
|
2725
3140
|
};
|
|
2726
|
-
const
|
|
2727
|
-
|
|
2728
|
-
|
|
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;
|
|
2729
3152
|
const onMove = (e) => {
|
|
2730
3153
|
if (!drag) return;
|
|
2731
3154
|
overlayStyle.left = drag.left + (e.clientX - drag.startX) + "px";
|
|
2732
3155
|
overlayStyle.top = drag.top + (e.clientY - drag.startY) + "px";
|
|
2733
3156
|
delete overlayStyle.bottom;
|
|
2734
3157
|
overlayStyle.transform = "none";
|
|
2735
|
-
|
|
3158
|
+
applyStyle();
|
|
2736
3159
|
};
|
|
2737
3160
|
const onUp = () => {
|
|
2738
3161
|
if (drag) {
|
|
2739
|
-
overlayStyle.cursor
|
|
2740
|
-
|
|
3162
|
+
delete overlayStyle.cursor;
|
|
3163
|
+
applyStyle();
|
|
2741
3164
|
}
|
|
2742
3165
|
drag = null;
|
|
2743
3166
|
};
|
|
2744
3167
|
box.on("mousedown", (e) => {
|
|
2745
|
-
if (e.target.closest(
|
|
3168
|
+
if (excludeDragSelector && e.target.closest(excludeDragSelector)) return;
|
|
2746
3169
|
const r = box.el.getBoundingClientRect();
|
|
2747
3170
|
drag = { startX: e.clientX, startY: e.clientY, left: r.left, top: r.top };
|
|
2748
3171
|
overlayStyle.cursor = "grabbing";
|
|
2749
|
-
|
|
3172
|
+
applyStyle();
|
|
2750
3173
|
});
|
|
2751
3174
|
o.D.addEventListener("mousemove", onMove);
|
|
2752
3175
|
o.D.addEventListener("mouseup", onUp);
|
|
2753
|
-
|
|
3176
|
+
let timerId;
|
|
3177
|
+
const cleanup = () => {
|
|
2754
3178
|
o.D.removeEventListener("mousemove", onMove);
|
|
2755
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", () => {
|
|
2756
3239
|
let unchecked = [];
|
|
2757
3240
|
if (hasCheckboxes) {
|
|
2758
|
-
const cbsList = o(".o-
|
|
2759
|
-
cbsList.els.
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
box.remove();
|
|
2764
|
-
if (unchecked.length === 0) {
|
|
2765
|
-
resolve({ ok: true });
|
|
2766
|
-
} else {
|
|
2767
|
-
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
|
+
});
|
|
2768
3246
|
}
|
|
3247
|
+
box._overlayCleanup();
|
|
3248
|
+
resolve(unchecked.length === 0 ? { ok: true } : { ok: false, errors: unchecked });
|
|
2769
3249
|
});
|
|
2770
3250
|
});
|
|
2771
3251
|
|