objs-core 2.2.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +561 -601
- package/objs-extension/README.md +32 -0
- package/objs-extension/background.js +110 -0
- package/objs-extension/bridge.js +193 -0
- package/objs-extension/icons/icon128.png +0 -0
- package/objs-extension/lib/objs-inject.js +5308 -0
- package/objs-extension/manifest.json +18 -0
- package/objs-extension/sidepanel.css +455 -0
- package/objs-extension/sidepanel.html +56 -0
- package/objs-extension/sidepanel.js +908 -0
- package/objs.built.js +793 -127
- package/objs.built.min.js +69 -44
- package/objs.d.ts +584 -507
- package/objs.js +954 -137
- package/package.json +71 -70
package/objs.built.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Objs-core library
|
|
3
|
-
* @version 2.
|
|
3
|
+
* @version 2.4.0
|
|
4
4
|
* @author Roman Torshin
|
|
5
5
|
* @license Apache-2.0
|
|
6
6
|
*/
|
|
@@ -712,6 +712,44 @@ const o = (query) => {
|
|
|
712
712
|
});
|
|
713
713
|
result.style(val || null);
|
|
714
714
|
}, "css");
|
|
715
|
+
result.cssMerge = returner((styles = {}) => {
|
|
716
|
+
if (styles === null) {
|
|
717
|
+
result.style(null);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
typeVerify([[styles, objectType]]);
|
|
721
|
+
const normKey = (k) => k.indexOf("-") !== -1 ? k : o.camelToKebab(k);
|
|
722
|
+
const parseStyleAttr = (s) => {
|
|
723
|
+
const out = {};
|
|
724
|
+
if (!s || typeof s !== stringType) return out;
|
|
725
|
+
const parts = s.split(";");
|
|
726
|
+
for (let p = 0; p < parts.length; p++) {
|
|
727
|
+
const part = parts[p];
|
|
728
|
+
const idx = part.indexOf(":");
|
|
729
|
+
if (idx === -1) continue;
|
|
730
|
+
const key = part.slice(0, idx).trim();
|
|
731
|
+
const val = part.slice(idx + 1).trim();
|
|
732
|
+
if (key) out[key] = val;
|
|
733
|
+
}
|
|
734
|
+
return out;
|
|
735
|
+
};
|
|
736
|
+
iterator(() => {
|
|
737
|
+
const el = result.els[i];
|
|
738
|
+
const merged = parseStyleAttr(el.getAttribute("style"));
|
|
739
|
+
cycleObj(styles, (style) => {
|
|
740
|
+
const k = normKey(style);
|
|
741
|
+
const v = styles[style];
|
|
742
|
+
if (v === null || v === u) delete merged[k];
|
|
743
|
+
else merged[k] = String(v).replace('"', "'");
|
|
744
|
+
});
|
|
745
|
+
let serialized = "";
|
|
746
|
+
cycleObj(merged, (k) => {
|
|
747
|
+
serialized += k + ":" + merged[k] + ";";
|
|
748
|
+
});
|
|
749
|
+
if (serialized) el.setAttribute("style", serialized);
|
|
750
|
+
else el.removeAttribute("style");
|
|
751
|
+
});
|
|
752
|
+
}, "cssMerge");
|
|
715
753
|
result.setClass = returner((cl) => {
|
|
716
754
|
typeVerify([[cl, stringType]]);
|
|
717
755
|
iterator(() => {
|
|
@@ -1875,17 +1913,26 @@ o.test = (title = "", ...tests) => {
|
|
|
1875
1913
|
});
|
|
1876
1914
|
const finalize = () => {
|
|
1877
1915
|
if (o.tFinalized[testN2]) return;
|
|
1916
|
+
if (waits > 0) {
|
|
1917
|
+
row = "\u251C ";
|
|
1918
|
+
row += "DONE " + done + "/" + num + ", waiting: " + waits;
|
|
1919
|
+
log(row, true);
|
|
1920
|
+
if (o.tStyled) {
|
|
1921
|
+
o.tLog[testN2] += o.tPre + '<div style="color:orange;"><b>DONE ' + done + "/" + num + ", waiting: " + waits + "</b>" + o.tDc + o.tDc;
|
|
1922
|
+
} else {
|
|
1923
|
+
o.tLog[testN2] += row + "\n";
|
|
1924
|
+
}
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1878
1927
|
o.tFinalized[testN2] = true;
|
|
1879
1928
|
const anyFailed = o.tStatus[testN2].some((s) => s === false);
|
|
1880
1929
|
o.tRes[testN2] = !anyFailed && done === num;
|
|
1881
|
-
row =
|
|
1882
|
-
row += "DONE " + done + "/" + num
|
|
1883
|
-
log(row, done
|
|
1884
|
-
|
|
1885
|
-
log();
|
|
1886
|
-
}
|
|
1930
|
+
row = "\u2558 ";
|
|
1931
|
+
row += "DONE " + done + "/" + num;
|
|
1932
|
+
log(row, done !== num);
|
|
1933
|
+
log();
|
|
1887
1934
|
if (o.tStyled) {
|
|
1888
|
-
o.tLog[testN2] += o.tPre + '<div style="color:' + (done
|
|
1935
|
+
o.tLog[testN2] += o.tPre + '<div style="color:' + (done !== num ? "red" : "green") + ';"><b>DONE ' + done + "/" + num + "</b>" + o.tDc + o.tDc;
|
|
1889
1936
|
} else {
|
|
1890
1937
|
o.tLog[testN2] += row + "\n";
|
|
1891
1938
|
}
|
|
@@ -1894,7 +1941,7 @@ o.test = (title = "", ...tests) => {
|
|
|
1894
1941
|
sessionStorage.setItem(`oTest-Res-${testN2}`, o.tRes[testN2]);
|
|
1895
1942
|
sessionStorage.setItem(`oTest-Status-${testN2}`, JSON.stringify(o.tStatus[testN2]));
|
|
1896
1943
|
}
|
|
1897
|
-
if (
|
|
1944
|
+
if (typeof o.tFns[testN2] === "function") {
|
|
1898
1945
|
o.tFns[testN2](testN2);
|
|
1899
1946
|
}
|
|
1900
1947
|
};
|
|
@@ -2239,6 +2286,7 @@ o.recorder = {
|
|
|
2239
2286
|
initialData: {},
|
|
2240
2287
|
assertions: [],
|
|
2241
2288
|
observeRoot: null,
|
|
2289
|
+
strictCapture: null,
|
|
2242
2290
|
_originalFetch: null,
|
|
2243
2291
|
_listeners: [],
|
|
2244
2292
|
_observer: null
|
|
@@ -2248,24 +2296,67 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2248
2296
|
if (o.recorder.active) {
|
|
2249
2297
|
return;
|
|
2250
2298
|
}
|
|
2251
|
-
|
|
2299
|
+
let observeSel;
|
|
2300
|
+
let eventsOpt;
|
|
2301
|
+
let timeoutsOpt;
|
|
2302
|
+
let strictCapture = null;
|
|
2303
|
+
const isStartBag = observe != null && typeof observe === "object" && !Array.isArray(observe) && (o.C(observe, "observe") || o.C(observe, "events") || o.C(observe, "timeouts") || o.C(observe, "strictCaptureAssertions") || o.C(observe, "strictCaptureNetwork") || o.C(observe, "strictCaptureWebSocket"));
|
|
2304
|
+
if (isStartBag) {
|
|
2305
|
+
const bag = observe;
|
|
2306
|
+
observeSel = bag.observe != null ? String(bag.observe) : void 0;
|
|
2307
|
+
eventsOpt = bag.events;
|
|
2308
|
+
timeoutsOpt = bag.timeouts;
|
|
2309
|
+
if (o.C(bag, "strictCaptureAssertions") || o.C(bag, "strictCaptureNetwork") || o.C(bag, "strictCaptureWebSocket")) {
|
|
2310
|
+
strictCapture = {
|
|
2311
|
+
assertions: !!bag.strictCaptureAssertions,
|
|
2312
|
+
network: !!bag.strictCaptureNetwork,
|
|
2313
|
+
websocket: !!bag.strictCaptureWebSocket
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
} else {
|
|
2317
|
+
observeSel = typeof observe === "string" ? observe : void 0;
|
|
2318
|
+
eventsOpt = events;
|
|
2319
|
+
timeoutsOpt = timeouts;
|
|
2320
|
+
}
|
|
2321
|
+
const defaultEvents = [
|
|
2322
|
+
"click",
|
|
2323
|
+
"mouseover",
|
|
2324
|
+
"scroll",
|
|
2325
|
+
"input",
|
|
2326
|
+
"change",
|
|
2327
|
+
"submit",
|
|
2328
|
+
"keydown",
|
|
2329
|
+
"focus",
|
|
2330
|
+
"blur"
|
|
2331
|
+
];
|
|
2252
2332
|
const defaultStepDelays = {
|
|
2253
2333
|
click: 100,
|
|
2254
2334
|
mouseover: 50,
|
|
2255
2335
|
scroll: 30,
|
|
2256
2336
|
input: 50,
|
|
2257
|
-
change: 50
|
|
2337
|
+
change: 50,
|
|
2338
|
+
submit: 100,
|
|
2339
|
+
keydown: 50,
|
|
2340
|
+
focus: 50,
|
|
2341
|
+
blur: 50
|
|
2342
|
+
};
|
|
2343
|
+
const listenEvents = eventsOpt || defaultEvents;
|
|
2344
|
+
const stepDelays = Object.assign({}, defaultStepDelays, timeoutsOpt || {});
|
|
2345
|
+
const captureDebounce = {
|
|
2346
|
+
scroll: 30,
|
|
2347
|
+
mouseover: 50,
|
|
2348
|
+
keydown: 50,
|
|
2349
|
+
focus: 50,
|
|
2350
|
+
blur: 50
|
|
2258
2351
|
};
|
|
2259
|
-
const listenEvents = events || defaultEvents;
|
|
2260
|
-
const stepDelays = Object.assign({}, defaultStepDelays, timeouts || {});
|
|
2261
|
-
const captureDebounce = { scroll: 30, mouseover: 50 };
|
|
2262
2352
|
const rec = o.recorder;
|
|
2263
2353
|
rec.active = true;
|
|
2264
2354
|
rec.actions = [];
|
|
2265
2355
|
rec.mocks = {};
|
|
2266
2356
|
rec.stepDelays = stepDelays;
|
|
2267
2357
|
rec.initialData = { url: window.location.href, timestamp: Date.now() };
|
|
2268
|
-
rec.
|
|
2358
|
+
rec.strictCapture = strictCapture;
|
|
2359
|
+
rec.observeRoot = observeSel || null;
|
|
2269
2360
|
rec.assertions = [];
|
|
2270
2361
|
rec.removedElements = [];
|
|
2271
2362
|
o.inits.forEach((inst, idx) => {
|
|
@@ -2300,6 +2391,67 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2300
2391
|
};
|
|
2301
2392
|
return response;
|
|
2302
2393
|
};
|
|
2394
|
+
rec._originalXHROpen = XMLHttpRequest.prototype.open;
|
|
2395
|
+
rec._originalXHRSend = XMLHttpRequest.prototype.send;
|
|
2396
|
+
XMLHttpRequest.prototype.open = function(method, url) {
|
|
2397
|
+
this._oMethod = (method || "GET").toUpperCase();
|
|
2398
|
+
this._oUrl = url;
|
|
2399
|
+
return rec._originalXHROpen.apply(this, arguments);
|
|
2400
|
+
};
|
|
2401
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
2402
|
+
const capture = () => {
|
|
2403
|
+
if (this.readyState !== 4) return;
|
|
2404
|
+
let reqBody;
|
|
2405
|
+
try {
|
|
2406
|
+
reqBody = body ? JSON.parse(body) : void 0;
|
|
2407
|
+
} catch (_e) {
|
|
2408
|
+
reqBody = body;
|
|
2409
|
+
}
|
|
2410
|
+
let respBody;
|
|
2411
|
+
try {
|
|
2412
|
+
const text = this.responseText;
|
|
2413
|
+
respBody = text ? JSON.parse(text) : null;
|
|
2414
|
+
} catch (_e) {
|
|
2415
|
+
respBody = this.responseText ?? null;
|
|
2416
|
+
}
|
|
2417
|
+
const key = (this._oMethod || "GET") + ":" + (this._oUrl || "");
|
|
2418
|
+
rec.mocks[key] = {
|
|
2419
|
+
url: this._oUrl,
|
|
2420
|
+
method: this._oMethod,
|
|
2421
|
+
request: reqBody,
|
|
2422
|
+
response: respBody,
|
|
2423
|
+
status: this.status
|
|
2424
|
+
};
|
|
2425
|
+
};
|
|
2426
|
+
this.addEventListener("readystatechange", capture);
|
|
2427
|
+
return rec._originalXHRSend.apply(this, arguments);
|
|
2428
|
+
};
|
|
2429
|
+
rec.websocketEvents = [];
|
|
2430
|
+
rec._originalWebSocket = window.WebSocket;
|
|
2431
|
+
window.WebSocket = function(url, protocols) {
|
|
2432
|
+
const ws = new rec._originalWebSocket(url, protocols);
|
|
2433
|
+
const id = rec.websocketEvents.length;
|
|
2434
|
+
rec.websocketEvents.push({
|
|
2435
|
+
url: typeof url === "string" ? url : String(url),
|
|
2436
|
+
protocol: Array.isArray(protocols) ? protocols[0] : protocols,
|
|
2437
|
+
open: true,
|
|
2438
|
+
messages: []
|
|
2439
|
+
});
|
|
2440
|
+
ws.addEventListener("message", (e) => {
|
|
2441
|
+
const data = typeof e.data === "string" ? e.data : String(e.data);
|
|
2442
|
+
rec.websocketEvents[id].messages.push({ dir: "in", data });
|
|
2443
|
+
});
|
|
2444
|
+
ws.addEventListener("close", () => {
|
|
2445
|
+
rec.websocketEvents[id].open = false;
|
|
2446
|
+
});
|
|
2447
|
+
const origSend = ws.send.bind(ws);
|
|
2448
|
+
ws.send = function(data) {
|
|
2449
|
+
const d = typeof data === "string" ? data : String(data);
|
|
2450
|
+
rec.websocketEvents[id].messages.push({ dir: "out", data: d });
|
|
2451
|
+
return origSend(data);
|
|
2452
|
+
};
|
|
2453
|
+
return ws;
|
|
2454
|
+
};
|
|
2303
2455
|
const unstableDataAttrs = { "data-o-init": 1, "data-o-init-i": 1, "data-o-state": 1 };
|
|
2304
2456
|
const qualify = (sel, fromNode) => {
|
|
2305
2457
|
if (o.D.querySelectorAll(sel).length <= 1) return sel;
|
|
@@ -2344,7 +2496,7 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2344
2496
|
}
|
|
2345
2497
|
return sel;
|
|
2346
2498
|
};
|
|
2347
|
-
const observeTarget =
|
|
2499
|
+
const observeTarget = observeSel && o.D.querySelector(observeSel) || o.D.body;
|
|
2348
2500
|
rec._observer = new MutationObserver((mutations) => {
|
|
2349
2501
|
const actionIdx = rec.actions.length - 1;
|
|
2350
2502
|
if (actionIdx < 0) return;
|
|
@@ -2365,22 +2517,28 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2365
2517
|
if (sel && observeTarget) {
|
|
2366
2518
|
const matches = observeTarget.querySelectorAll(sel);
|
|
2367
2519
|
if (matches.length > 1) {
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2520
|
+
const idxAmong = [...matches].indexOf(node);
|
|
2521
|
+
if (idxAmong !== -1) {
|
|
2522
|
+
listSelector = sel;
|
|
2523
|
+
index = idxAmong;
|
|
2524
|
+
} else {
|
|
2525
|
+
let n = node;
|
|
2526
|
+
while (n && n !== observeTarget && n.nodeType === 1) {
|
|
2527
|
+
const qaAttr = o.autotag && n.dataset && n.dataset[o.autotag];
|
|
2528
|
+
if (qaAttr) {
|
|
2529
|
+
const itemSel = `[data-${o.autotag}="${qaAttr}"]`;
|
|
2530
|
+
const itemMatches = observeTarget.querySelectorAll(itemSel);
|
|
2531
|
+
if (itemMatches.length > 1) {
|
|
2532
|
+
const idx = [...itemMatches].indexOf(n);
|
|
2533
|
+
if (idx !== -1) {
|
|
2534
|
+
listSelector = itemSel;
|
|
2535
|
+
index = idx;
|
|
2536
|
+
break;
|
|
2537
|
+
}
|
|
2380
2538
|
}
|
|
2381
2539
|
}
|
|
2540
|
+
n = n.parentElement;
|
|
2382
2541
|
}
|
|
2383
|
-
n = n.parentElement;
|
|
2384
2542
|
}
|
|
2385
2543
|
}
|
|
2386
2544
|
}
|
|
@@ -2449,28 +2607,51 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2449
2607
|
});
|
|
2450
2608
|
}
|
|
2451
2609
|
if (m.type === "attributes") {
|
|
2610
|
+
const attr = m.attributeName;
|
|
2611
|
+
if (!attr) return;
|
|
2452
2612
|
const sel = buildSelector(m.target);
|
|
2453
2613
|
if (!sel) return;
|
|
2614
|
+
const attrToType = {
|
|
2615
|
+
class: "class",
|
|
2616
|
+
style: "style",
|
|
2617
|
+
hidden: "hidden",
|
|
2618
|
+
disabled: "disabled",
|
|
2619
|
+
"aria-expanded": "aria-expanded",
|
|
2620
|
+
"aria-checked": "aria-checked"
|
|
2621
|
+
};
|
|
2622
|
+
const type = attrToType[attr];
|
|
2623
|
+
if (!type) return;
|
|
2454
2624
|
if (rec.assertions.some(
|
|
2455
|
-
(a2) => a2.actionIdx === actionIdx && a2.selector === sel && a2.type ===
|
|
2625
|
+
(a2) => a2.actionIdx === actionIdx && a2.selector === sel && a2.type === type
|
|
2456
2626
|
))
|
|
2457
2627
|
return;
|
|
2458
2628
|
const { listSelector: aListSel, index: aIdx } = addAssertionIndex(sel, m.target);
|
|
2459
|
-
const
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2629
|
+
const el = m.target;
|
|
2630
|
+
let value;
|
|
2631
|
+
if (type === "class") value = el.className;
|
|
2632
|
+
else if (type === "style") value = el.style?.cssText || el.getAttribute("style") || "";
|
|
2633
|
+
else if (type === "hidden") value = el.hidden;
|
|
2634
|
+
else if (type === "disabled") value = el.disabled === true;
|
|
2635
|
+
else if (type === "aria-expanded")
|
|
2636
|
+
value = el.getAttribute("aria-expanded");
|
|
2637
|
+
else if (type === "aria-checked") value = el.getAttribute("aria-checked");
|
|
2638
|
+
const a = { actionIdx, type, selector: sel };
|
|
2639
|
+
if (type === "class") a.className = value;
|
|
2640
|
+
else if (type === "style") a.style = value;
|
|
2641
|
+
else if (type === "hidden") a.hidden = value;
|
|
2642
|
+
else if (type === "disabled") a.disabled = value;
|
|
2643
|
+
else if (type === "aria-expanded") a.ariaExpanded = value;
|
|
2644
|
+
else if (type === "aria-checked") a.ariaChecked = value;
|
|
2465
2645
|
if (aListSel != null) a.listSelector = aListSel;
|
|
2466
2646
|
if (aIdx != null) a.index = aIdx;
|
|
2467
2647
|
rec.assertions.push(a);
|
|
2468
2648
|
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
2469
|
-
console.log("[recording] +
|
|
2649
|
+
console.log("[recording] +attr assertion:", {
|
|
2470
2650
|
actionIdx,
|
|
2471
2651
|
lastAction: lastAction?.type + " " + lastAction?.target,
|
|
2472
2652
|
selector: sel,
|
|
2473
|
-
|
|
2653
|
+
type,
|
|
2654
|
+
value,
|
|
2474
2655
|
index: aIdx,
|
|
2475
2656
|
listSelector: aListSel
|
|
2476
2657
|
});
|
|
@@ -2495,7 +2676,7 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2495
2676
|
listenEvents.forEach((ev) => {
|
|
2496
2677
|
const handler = (e) => {
|
|
2497
2678
|
const target = e.target;
|
|
2498
|
-
if (
|
|
2679
|
+
if (observeSel && observeTarget && target?.nodeType === 1 && !observeTarget.contains(target)) {
|
|
2499
2680
|
return;
|
|
2500
2681
|
}
|
|
2501
2682
|
let selector = "";
|
|
@@ -2514,22 +2695,28 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2514
2695
|
if (selector && observeTarget) {
|
|
2515
2696
|
const matches = observeTarget.querySelectorAll(selector);
|
|
2516
2697
|
if (matches.length > 1) {
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2698
|
+
const idxAmongMatches = [...matches].indexOf(target);
|
|
2699
|
+
if (idxAmongMatches !== -1) {
|
|
2700
|
+
listSelector = selector;
|
|
2701
|
+
targetIndex = idxAmongMatches;
|
|
2702
|
+
} else {
|
|
2703
|
+
let node = target;
|
|
2704
|
+
while (node && node !== observeTarget && node.nodeType === 1) {
|
|
2705
|
+
const qaAttr = o.autotag && node.dataset && node.dataset[o.autotag];
|
|
2706
|
+
if (qaAttr) {
|
|
2707
|
+
const itemSel = `[data-${o.autotag}="${qaAttr}"]`;
|
|
2708
|
+
const itemMatches = observeTarget.querySelectorAll(itemSel);
|
|
2709
|
+
if (itemMatches.length > 1) {
|
|
2710
|
+
const idx = [...itemMatches].indexOf(node);
|
|
2711
|
+
if (idx !== -1) {
|
|
2712
|
+
listSelector = itemSel;
|
|
2713
|
+
targetIndex = idx;
|
|
2714
|
+
break;
|
|
2715
|
+
}
|
|
2529
2716
|
}
|
|
2530
2717
|
}
|
|
2718
|
+
node = node.parentElement;
|
|
2531
2719
|
}
|
|
2532
|
-
node = node.parentElement;
|
|
2533
2720
|
}
|
|
2534
2721
|
}
|
|
2535
2722
|
}
|
|
@@ -2537,13 +2724,30 @@ o.startRecording = (observe, events, timeouts) => {
|
|
|
2537
2724
|
const scrollY = ev === "scroll" ? window.scrollY : void 0;
|
|
2538
2725
|
const value = ev === "input" || ev === "change" ? target?.value : void 0;
|
|
2539
2726
|
const checked = ev === "change" && (target?.type === "checkbox" || target?.type === "radio") ? target?.checked : void 0;
|
|
2540
|
-
const
|
|
2727
|
+
const key = ev === "keydown" ? target?.key : void 0;
|
|
2728
|
+
const code = ev === "keydown" ? target?.code : void 0;
|
|
2729
|
+
const delay = ev === "click" || ev === "change" || ev === "submit" ? 0 : stepDelays[ev] !== void 0 ? stepDelays[ev] : captureDebounce[ev] ?? 0;
|
|
2541
2730
|
const pushAction = () => {
|
|
2731
|
+
if ((ev === "blur" || ev === "focus") && selector) {
|
|
2732
|
+
const lastIdx = rec.actions.length - 1;
|
|
2733
|
+
const lastAction = lastIdx >= 0 ? rec.actions[lastIdx] : null;
|
|
2734
|
+
if (lastAction) {
|
|
2735
|
+
const sameTarget = lastAction.target === selector && lastAction.listSelector == null === (listSelector == null) && lastAction.targetIndex == null === (targetIndex == null) && (lastAction.targetIndex == null || lastAction.targetIndex === targetIndex);
|
|
2736
|
+
if (sameTarget) return;
|
|
2737
|
+
for (const r of rec.removedElements) {
|
|
2738
|
+
if (r.actionIdx !== lastIdx) continue;
|
|
2739
|
+
if (r.selector === selector || selector.startsWith(r.selector + " ") || selector.startsWith(r.selector + ">"))
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2542
2744
|
const action = { type: ev, target: selector, time: Date.now() };
|
|
2543
2745
|
if (targetType) action.targetType = targetType;
|
|
2544
2746
|
if (scrollY !== void 0) action.scrollY = scrollY;
|
|
2545
2747
|
if (value !== void 0) action.value = value;
|
|
2546
2748
|
if (checked !== void 0) action.checked = checked;
|
|
2749
|
+
if (key !== void 0) action.key = key;
|
|
2750
|
+
if (code !== void 0) action.code = code;
|
|
2547
2751
|
if (listSelector != null) action.listSelector = listSelector;
|
|
2548
2752
|
if (targetIndex != null) action.targetIndex = targetIndex;
|
|
2549
2753
|
rec.actions.push(action);
|
|
@@ -2566,6 +2770,16 @@ o.stopRecording = () => {
|
|
|
2566
2770
|
window.fetch = rec._originalFetch;
|
|
2567
2771
|
rec._originalFetch = null;
|
|
2568
2772
|
}
|
|
2773
|
+
if (rec._originalXHROpen) {
|
|
2774
|
+
XMLHttpRequest.prototype.open = rec._originalXHROpen;
|
|
2775
|
+
XMLHttpRequest.prototype.send = rec._originalXHRSend;
|
|
2776
|
+
rec._originalXHROpen = null;
|
|
2777
|
+
rec._originalXHRSend = null;
|
|
2778
|
+
}
|
|
2779
|
+
if (rec._originalWebSocket) {
|
|
2780
|
+
window.WebSocket = rec._originalWebSocket;
|
|
2781
|
+
rec._originalWebSocket = null;
|
|
2782
|
+
}
|
|
2569
2783
|
rec._listeners.forEach(({ ev, handler }) => {
|
|
2570
2784
|
o.D.removeEventListener(ev, handler, true);
|
|
2571
2785
|
});
|
|
@@ -2574,15 +2788,20 @@ o.stopRecording = () => {
|
|
|
2574
2788
|
rec._observer.disconnect();
|
|
2575
2789
|
rec._observer = null;
|
|
2576
2790
|
}
|
|
2577
|
-
|
|
2791
|
+
const out = {
|
|
2578
2792
|
actions: [...rec.actions],
|
|
2579
2793
|
mocks: { ...rec.mocks },
|
|
2580
2794
|
initialData: { ...rec.initialData },
|
|
2581
2795
|
stepDelays: { ...rec.stepDelays },
|
|
2582
2796
|
assertions: [...rec.assertions || []],
|
|
2583
2797
|
removedElements: [...rec.removedElements || []],
|
|
2584
|
-
observeRoot: rec.observeRoot || null
|
|
2798
|
+
observeRoot: rec.observeRoot || null,
|
|
2799
|
+
websocketEvents: [...rec.websocketEvents || []]
|
|
2585
2800
|
};
|
|
2801
|
+
if (rec.strictCapture) {
|
|
2802
|
+
out.strictCapture = { ...rec.strictCapture };
|
|
2803
|
+
}
|
|
2804
|
+
return out;
|
|
2586
2805
|
};
|
|
2587
2806
|
o.clearRecording = (id) => {
|
|
2588
2807
|
if (id !== void 0) {
|
|
@@ -2597,6 +2816,8 @@ o.clearRecording = (id) => {
|
|
|
2597
2816
|
}
|
|
2598
2817
|
};
|
|
2599
2818
|
o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
|
|
2819
|
+
const strictAssertions = !!(opts && opts.strictAssertions);
|
|
2820
|
+
const strictRemoved = opts && opts.strictRemoved !== void 0 ? !!opts.strictRemoved : strictAssertions;
|
|
2600
2821
|
const preFiltered = opts && opts.assertions;
|
|
2601
2822
|
const assertions = preFiltered != null ? preFiltered : (recording.assertions || []).filter(
|
|
2602
2823
|
(a) => actionIdx == null || a.actionIdx === actionIdx
|
|
@@ -2631,6 +2852,7 @@ o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
|
|
|
2631
2852
|
};
|
|
2632
2853
|
const r = resolveRoot();
|
|
2633
2854
|
const norm = (s) => (s || "").trim().replace(/\s+/g, " ");
|
|
2855
|
+
const styleNorm = (s) => norm(String(s || "").replace(/\s*:\s*/g, ": ").replace(/\s*;\s*/g, "; "));
|
|
2634
2856
|
const getText = (el) => el ? norm(el.textContent || "") : "";
|
|
2635
2857
|
const removedElements = opts?.removedElements || [];
|
|
2636
2858
|
const isRemoved = (a) => {
|
|
@@ -2650,20 +2872,67 @@ o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
|
|
|
2650
2872
|
const failures = [];
|
|
2651
2873
|
for (const a of deduped) {
|
|
2652
2874
|
if (isRemoved(a)) {
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2875
|
+
if (!strictRemoved) {
|
|
2876
|
+
passed += 1;
|
|
2877
|
+
if (o.recordingAssertionDebug && typeof console !== "undefined" && console.log) {
|
|
2878
|
+
console.log("[runRecordingAssertions] skip (explicit removed):", {
|
|
2879
|
+
actionIdx: a.actionIdx,
|
|
2880
|
+
selector: a.selector,
|
|
2881
|
+
text: (a.text || "").slice(0, 40)
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
continue;
|
|
2885
|
+
}
|
|
2886
|
+
let ghost = null;
|
|
2887
|
+
const expText = norm(a.text || "");
|
|
2888
|
+
if (a.listSelector != null && a.index != null) {
|
|
2889
|
+
const items = r.querySelectorAll(a.listSelector);
|
|
2890
|
+
let item = items[a.index];
|
|
2891
|
+
if (!item && a.index > 0) item = items[a.index - 1];
|
|
2892
|
+
if (item) {
|
|
2893
|
+
ghost = a.selector !== a.listSelector ? item.querySelector(a.selector) || item : item;
|
|
2894
|
+
}
|
|
2895
|
+
if (!ghost && expText && a.type === "visible") {
|
|
2896
|
+
for (let j = 0; j < items.length; j++) {
|
|
2897
|
+
const it = items[j];
|
|
2898
|
+
const cand = a.selector !== a.listSelector ? it.querySelector(a.selector) || it : it;
|
|
2899
|
+
if (cand && getText(cand).indexOf(expText) !== -1) {
|
|
2900
|
+
ghost = cand;
|
|
2901
|
+
break;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
} else {
|
|
2906
|
+
const matches = r.querySelectorAll(a.selector);
|
|
2907
|
+
ghost = matches.length > 0 ? matches[0] : o.D.querySelector(a.selector);
|
|
2908
|
+
}
|
|
2909
|
+
if (ghost && a.type === "visible") {
|
|
2910
|
+
const vis = ghost.nodeType === 1 && (ghost.offsetParent !== null || ghost.getBoundingClientRect && ghost.getBoundingClientRect().width > 0);
|
|
2911
|
+
const gtext = getText(ghost);
|
|
2912
|
+
const still = vis && (!expText || gtext.indexOf(expText) !== -1 || expText.indexOf(gtext) !== -1);
|
|
2913
|
+
if (still) {
|
|
2914
|
+
failures.push({
|
|
2915
|
+
selector: a.selector,
|
|
2916
|
+
message: "expected absent (recorded removed) but matching content still visible"
|
|
2917
|
+
});
|
|
2918
|
+
continue;
|
|
2919
|
+
}
|
|
2920
|
+
} else if (ghost && a.type !== "visible") {
|
|
2921
|
+
failures.push({
|
|
2657
2922
|
selector: a.selector,
|
|
2658
|
-
|
|
2923
|
+
message: "expected absent (recorded removed) but element still present"
|
|
2659
2924
|
});
|
|
2925
|
+
continue;
|
|
2660
2926
|
}
|
|
2927
|
+
passed += 1;
|
|
2661
2928
|
continue;
|
|
2662
2929
|
}
|
|
2663
2930
|
let el = null;
|
|
2664
2931
|
let indexOutOfBounds = false;
|
|
2932
|
+
let listItemsLength = -1;
|
|
2665
2933
|
if (a.listSelector != null && a.index != null) {
|
|
2666
2934
|
const items = r.querySelectorAll(a.listSelector);
|
|
2935
|
+
listItemsLength = items.length;
|
|
2667
2936
|
const expectedText = norm(a.text || "");
|
|
2668
2937
|
const tryItem = (idx) => {
|
|
2669
2938
|
const it = items[idx];
|
|
@@ -2671,26 +2940,36 @@ o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
|
|
|
2671
2940
|
const e = a.selector !== a.listSelector ? it.querySelector(a.selector) : it;
|
|
2672
2941
|
return e || (a.selector !== a.listSelector ? it : null);
|
|
2673
2942
|
};
|
|
2674
|
-
let item
|
|
2675
|
-
if (
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2943
|
+
let item;
|
|
2944
|
+
if (strictAssertions) {
|
|
2945
|
+
item = items[a.index];
|
|
2946
|
+
if (item) {
|
|
2947
|
+
el = tryItem(a.index);
|
|
2948
|
+
if (!el && a.selector !== a.listSelector) el = item;
|
|
2949
|
+
}
|
|
2950
|
+
} else {
|
|
2951
|
+
item = items[a.index];
|
|
2952
|
+
if (!item && a.index > 0) item = items[a.index - 1];
|
|
2953
|
+
if (item) {
|
|
2954
|
+
el = tryItem(a.index) || (a.index > 0 ? tryItem(a.index - 1) : null);
|
|
2955
|
+
if (!el && a.selector !== a.listSelector) el = item;
|
|
2956
|
+
if (a.type === "visible" && expectedText && el) {
|
|
2957
|
+
const actualText = getText(el);
|
|
2958
|
+
const textMismatch = actualText.indexOf(expectedText) === -1 && expectedText.indexOf(actualText) === -1;
|
|
2959
|
+
if (textMismatch) {
|
|
2960
|
+
for (let j = 0; j < items.length; j++) {
|
|
2961
|
+
const candEl = tryItem(j);
|
|
2962
|
+
if (candEl && getText(candEl).indexOf(expectedText) !== -1) {
|
|
2963
|
+
el = candEl;
|
|
2964
|
+
item = items[j];
|
|
2965
|
+
break;
|
|
2966
|
+
}
|
|
2689
2967
|
}
|
|
2690
2968
|
}
|
|
2691
2969
|
}
|
|
2692
2970
|
}
|
|
2693
|
-
}
|
|
2971
|
+
}
|
|
2972
|
+
if (!item) {
|
|
2694
2973
|
indexOutOfBounds = true;
|
|
2695
2974
|
}
|
|
2696
2975
|
} else {
|
|
@@ -2701,12 +2980,12 @@ o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
|
|
|
2701
2980
|
const visible = el && el.nodeType === 1 && (el.offsetParent !== null || el.getBoundingClientRect && el.getBoundingClientRect().width > 0);
|
|
2702
2981
|
const expectedText = norm(a.text || "");
|
|
2703
2982
|
const actualText = getText(el);
|
|
2704
|
-
const
|
|
2705
|
-
const textOk = !expectedText || actualText.indexOf(expectedText) !== -1 || fullActual.indexOf(expectedText) !== -1 || expectedText.length > 0 && expectedText.indexOf(actualText) !== -1;
|
|
2983
|
+
const textOk = strictAssertions ? !expectedText || actualText === expectedText : !expectedText || actualText.indexOf(expectedText) !== -1 || expectedText.length > 0 && expectedText.indexOf(actualText) !== -1;
|
|
2706
2984
|
if (visible && textOk) {
|
|
2707
2985
|
passed += 1;
|
|
2708
2986
|
} else {
|
|
2709
|
-
const
|
|
2987
|
+
const listCount = listItemsLength >= 0 ? listItemsLength : r.querySelectorAll(a.listSelector || a.selector).length;
|
|
2988
|
+
const message = indexOutOfBounds ? `index out of bounds (list has ${listCount} items, assertion expected index ${a.index})` : !el ? "element not found" : !visible ? "not visible" : !textOk ? "text mismatch" : "fail";
|
|
2710
2989
|
failures.push({ selector: a.selector, message });
|
|
2711
2990
|
if (typeof console !== "undefined" && console.warn) {
|
|
2712
2991
|
console.warn("[runRecordingAssertions] visible failed:", {
|
|
@@ -2723,10 +3002,11 @@ o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
|
|
|
2723
3002
|
} else if (a.type === "class") {
|
|
2724
3003
|
const tokens = (a.className || "").trim().split(/\s+/).filter(Boolean);
|
|
2725
3004
|
const hasClass = el && (tokens.length === 0 || tokens.every((c) => el.classList?.contains(c)));
|
|
2726
|
-
|
|
3005
|
+
const classOrderOk = !strictAssertions || !a.className || norm((el?.className || "").trim()) === norm((a.className || "").trim());
|
|
3006
|
+
if (hasClass && classOrderOk) {
|
|
2727
3007
|
passed += 1;
|
|
2728
3008
|
} 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}"`;
|
|
3009
|
+
const msg = indexOutOfBounds ? `index out of bounds (list has ${r.querySelectorAll(a.listSelector).length} items, expected index ${a.index})` : !el ? "element not found" : hasClass && !classOrderOk ? `expected exact className "${a.className}" (strict)` : `expected class "${a.className}"`;
|
|
2730
3010
|
failures.push({ selector: a.selector, message: msg });
|
|
2731
3011
|
if (typeof console !== "undefined" && console.warn) {
|
|
2732
3012
|
console.warn("[runRecordingAssertions] failed:", {
|
|
@@ -2740,12 +3020,57 @@ o.runRecordingAssertions = (recording, root, actionIdx, opts) => {
|
|
|
2740
3020
|
});
|
|
2741
3021
|
}
|
|
2742
3022
|
}
|
|
3023
|
+
} else if (a.type === "style") {
|
|
3024
|
+
const expected = (a.style || "").trim();
|
|
3025
|
+
const actual = (el?.style?.cssText || el?.getAttribute?.("style") || "").trim();
|
|
3026
|
+
const ok = el && (!expected || (strictAssertions ? styleNorm(actual) === styleNorm(expected) : actual.indexOf(expected) !== -1 || expected === actual));
|
|
3027
|
+
if (ok) {
|
|
3028
|
+
passed += 1;
|
|
3029
|
+
} else {
|
|
3030
|
+
const msg = !el ? "element not found" : `expected style "${expected.slice(0, 60)}..."`;
|
|
3031
|
+
failures.push({ selector: a.selector, message: msg });
|
|
3032
|
+
}
|
|
3033
|
+
} else if (a.type === "hidden") {
|
|
3034
|
+
const ok = el && el.hidden === a.hidden;
|
|
3035
|
+
if (ok) {
|
|
3036
|
+
passed += 1;
|
|
3037
|
+
} else {
|
|
3038
|
+
const msg = !el ? "element not found" : `expected hidden=${a.hidden}`;
|
|
3039
|
+
failures.push({ selector: a.selector, message: msg });
|
|
3040
|
+
}
|
|
3041
|
+
} else if (a.type === "disabled") {
|
|
3042
|
+
const ok = el && el.disabled === a.disabled;
|
|
3043
|
+
if (ok) {
|
|
3044
|
+
passed += 1;
|
|
3045
|
+
} else {
|
|
3046
|
+
const msg = !el ? "element not found" : `expected disabled=${a.disabled}`;
|
|
3047
|
+
failures.push({ selector: a.selector, message: msg });
|
|
3048
|
+
}
|
|
3049
|
+
} else if (a.type === "aria-expanded") {
|
|
3050
|
+
const actual = el?.getAttribute?.("aria-expanded");
|
|
3051
|
+
const ok = el && (a.ariaExpanded == null || String(actual) === String(a.ariaExpanded));
|
|
3052
|
+
if (ok) {
|
|
3053
|
+
passed += 1;
|
|
3054
|
+
} else {
|
|
3055
|
+
const msg = !el ? "element not found" : `expected aria-expanded="${a.ariaExpanded}"`;
|
|
3056
|
+
failures.push({ selector: a.selector, message: msg });
|
|
3057
|
+
}
|
|
3058
|
+
} else if (a.type === "aria-checked") {
|
|
3059
|
+
const actual = el?.getAttribute?.("aria-checked");
|
|
3060
|
+
const ok = el && (a.ariaChecked == null || String(actual) === String(a.ariaChecked));
|
|
3061
|
+
if (ok) {
|
|
3062
|
+
passed += 1;
|
|
3063
|
+
} else {
|
|
3064
|
+
const msg = !el ? "element not found" : `expected aria-checked="${a.ariaChecked}"`;
|
|
3065
|
+
failures.push({ selector: a.selector, message: msg });
|
|
3066
|
+
}
|
|
2743
3067
|
}
|
|
2744
3068
|
}
|
|
2745
3069
|
return { passed, total: deduped.length, failures };
|
|
2746
3070
|
};
|
|
2747
3071
|
o.exportTest = (recording, options = {}) => {
|
|
2748
3072
|
const delay = options.delay !== void 0 ? options.delay : 16;
|
|
3073
|
+
const extensionExport = options.extensionExport === true;
|
|
2749
3074
|
const recordingData = {
|
|
2750
3075
|
actions: recording.actions,
|
|
2751
3076
|
assertions: recording.assertions || [],
|
|
@@ -2780,14 +3105,23 @@ o.exportTest = (recording, options = {}) => {
|
|
|
2780
3105
|
body = (a.value !== void 0 ? ` el.value = ${JSON.stringify(a.value)};
|
|
2781
3106
|
` : "") + (a.checked !== void 0 ? ` el.checked = ${a.checked};
|
|
2782
3107
|
` : "") + ` el.dispatchEvent(new Event('${a.type}', {bubbles:true}));${endSuffix}`;
|
|
3108
|
+
} else if (a.type === "submit") {
|
|
3109
|
+
body = ` (el.requestSubmit && el.requestSubmit()) || el.submit();${endSuffix}`;
|
|
3110
|
+
} else if (a.type === "keydown") {
|
|
3111
|
+
body = ` el.dispatchEvent(new KeyboardEvent('keydown', {key:${JSON.stringify(a.key || "")}, code:${JSON.stringify(a.code || "")}, bubbles:true, cancelable:true}));${endSuffix}`;
|
|
3112
|
+
} else if (a.type === "focus") {
|
|
3113
|
+
body = ` el.focus();${endSuffix}`;
|
|
3114
|
+
} else if (a.type === "blur") {
|
|
3115
|
+
body = ` el.blur();${endSuffix}`;
|
|
2783
3116
|
} else {
|
|
2784
3117
|
const useNativeClick = a.type === "click";
|
|
2785
3118
|
body = useNativeClick ? ` el.click();${endSuffix}` : ` el.dispatchEvent(new MouseEvent('${a.type}', {bubbles:true,cancelable:true}));${endSuffix}`;
|
|
2786
3119
|
}
|
|
3120
|
+
const skipIfMissing = a.type === "blur" || a.type === "focus";
|
|
2787
3121
|
steps.push(
|
|
2788
3122
|
` ['${a.type} on ${a.target}', ${stepFn} {
|
|
2789
3123
|
` + getEl(a) + `
|
|
2790
|
-
if (!el && '${a.type}' !== 'scroll') return 'element not found: ${a.target.replace(/'/g, "\\'")}';
|
|
3124
|
+
if (!el && '${a.type}' !== 'scroll') { if (${skipIfMissing}) return true; return 'element not found: ${a.target.replace(/'/g, "\\'")}'; }
|
|
2791
3125
|
` + body + ` }]`
|
|
2792
3126
|
);
|
|
2793
3127
|
const assertsForAction = (recording.assertions || []).filter((x) => x.actionIdx === i);
|
|
@@ -2801,13 +3135,24 @@ o.exportTest = (recording, options = {}) => {
|
|
|
2801
3135
|
}
|
|
2802
3136
|
}
|
|
2803
3137
|
const mocksStr = Object.keys(recording.mocks || {}).length ? JSON.stringify(recording.mocks, null, 2) : "{}";
|
|
2804
|
-
|
|
3138
|
+
const header = `// Auto-generated by o.exportTest() \u2014 review and anonymize mocks before committing
|
|
2805
3139
|
const recordingMocks = ${mocksStr};
|
|
2806
3140
|
const recordingData = { actions: ${JSON.stringify(recording.actions)}, assertions: ${JSON.stringify(recording.assertions || [])}, observeRoot: ${JSON.stringify(recording.observeRoot || null)} };
|
|
2807
3141
|
|
|
2808
|
-
|
|
3142
|
+
`;
|
|
3143
|
+
const manualLine = ` // Add manual checks: ['Manual: label', () => o.testConfirm('label', ['item1'])],`;
|
|
3144
|
+
if (extensionExport) {
|
|
3145
|
+
return header + `const __objsExtensionTestRun = o.test('Recorded test',
|
|
3146
|
+
${steps.join(",\n")},
|
|
3147
|
+
${manualLine}
|
|
3148
|
+
{ sync: true }, () => {
|
|
3149
|
+
// teardown
|
|
3150
|
+
});
|
|
3151
|
+
`;
|
|
3152
|
+
}
|
|
3153
|
+
return header + `o.addTest('Recorded test', [
|
|
2809
3154
|
${steps.join(",\n")}
|
|
2810
|
-
|
|
3155
|
+
${manualLine}
|
|
2811
3156
|
], () => {
|
|
2812
3157
|
// teardown
|
|
2813
3158
|
});
|
|
@@ -2824,15 +3169,40 @@ o.exportPlaywrightTest = (recording, options = {}) => {
|
|
|
2824
3169
|
}
|
|
2825
3170
|
const baseUrl = options.baseUrl || path;
|
|
2826
3171
|
const routes = Object.values(recording.mocks).map((mock) => {
|
|
2827
|
-
|
|
2828
|
-
|
|
3172
|
+
let urlPath = mock.url;
|
|
3173
|
+
try {
|
|
3174
|
+
urlPath = new URL(mock.url).pathname || urlPath;
|
|
3175
|
+
} catch (_e) {
|
|
3176
|
+
}
|
|
3177
|
+
if (!urlPath.startsWith("/")) urlPath = "/" + urlPath;
|
|
3178
|
+
const respBody = JSON.stringify(mock.response);
|
|
3179
|
+
const reqBody = JSON.stringify(mock.request);
|
|
3180
|
+
const method = (mock.method || "GET").toUpperCase();
|
|
3181
|
+
let verify = ` if (route.request().method() !== ${JSON.stringify(method)}) { await route.continue(); return; }
|
|
3182
|
+
`;
|
|
3183
|
+
if (mock.request != null && (method === "POST" || method === "PUT" || method === "PATCH")) {
|
|
3184
|
+
verify += ` const postData = route.request().postData();
|
|
3185
|
+
const body = (() => { try { return JSON.parse(postData || '{}'); } catch { return {}; } })();
|
|
3186
|
+
expect(body).toEqual(${reqBody});
|
|
3187
|
+
`;
|
|
3188
|
+
}
|
|
2829
3189
|
return ` await page.route('**${urlPath}', async route => {
|
|
2830
|
-
await route.fulfill({ status: ${mock.status || 200}, contentType: 'application/json',
|
|
2831
|
-
body: JSON.stringify(${
|
|
3190
|
+
` + verify + ` await route.fulfill({ status: ${mock.status || 200}, contentType: 'application/json',
|
|
3191
|
+
body: JSON.stringify(${respBody}) });
|
|
2832
3192
|
});`;
|
|
2833
3193
|
}).join("\n");
|
|
2834
3194
|
const sd = Object.assign(
|
|
2835
|
-
{
|
|
3195
|
+
{
|
|
3196
|
+
click: 100,
|
|
3197
|
+
mouseover: 50,
|
|
3198
|
+
scroll: 30,
|
|
3199
|
+
input: 50,
|
|
3200
|
+
change: 50,
|
|
3201
|
+
submit: 100,
|
|
3202
|
+
keydown: 50,
|
|
3203
|
+
focus: 50,
|
|
3204
|
+
blur: 50
|
|
3205
|
+
},
|
|
2836
3206
|
recording.stepDelays || {}
|
|
2837
3207
|
);
|
|
2838
3208
|
const steps = recording.actions.map((action, i) => {
|
|
@@ -2857,6 +3227,15 @@ o.exportPlaywrightTest = (recording, options = {}) => {
|
|
|
2857
3227
|
} else {
|
|
2858
3228
|
step = ` await ${loc}.fill(${JSON.stringify(action.value || "")});`;
|
|
2859
3229
|
}
|
|
3230
|
+
} else if (action.type === "submit") {
|
|
3231
|
+
step = ` await ${loc}.evaluate((el) => el.requestSubmit?.() || el.submit());`;
|
|
3232
|
+
} else if (action.type === "keydown") {
|
|
3233
|
+
const key = action.key || "";
|
|
3234
|
+
step = key === "Enter" ? ` await ${loc}.press("Enter");` : key ? ` await ${loc}.press(${JSON.stringify(key)});` : ` await ${loc}.press(${JSON.stringify(action.code || "")});`;
|
|
3235
|
+
} else if (action.type === "focus") {
|
|
3236
|
+
step = ` if (await ${loc}.count() > 0) await ${loc}.focus();`;
|
|
3237
|
+
} else if (action.type === "blur") {
|
|
3238
|
+
step = ` if (await ${loc}.count() > 0) await ${loc}.blur();`;
|
|
2860
3239
|
} else {
|
|
2861
3240
|
step = ` await ${loc}.click();`;
|
|
2862
3241
|
}
|
|
@@ -2874,13 +3253,50 @@ o.exportPlaywrightTest = (recording, options = {}) => {
|
|
|
2874
3253
|
return s;
|
|
2875
3254
|
}
|
|
2876
3255
|
if (a.type === "class") {
|
|
2877
|
-
|
|
3256
|
+
const classes = (a.className || "").trim().split(/\s+/).filter(Boolean);
|
|
3257
|
+
if (classes.length > 0)
|
|
3258
|
+
return classes.map((c) => ` await expect(${aLoc}).toHaveClass(${JSON.stringify(c)});`).join("\n");
|
|
3259
|
+
return ` // class on ${a.selector} (no specific classes asserted)`;
|
|
3260
|
+
}
|
|
3261
|
+
if (a.type === "style") {
|
|
3262
|
+
const style = (a.style || "").trim();
|
|
3263
|
+
if (style) {
|
|
3264
|
+
const m = style.match(/(\w+)\s*:\s*([^;]+)/);
|
|
3265
|
+
if (m)
|
|
3266
|
+
return ` await expect(${aLoc}).toHaveCSS(${JSON.stringify(m[1])}, ${JSON.stringify(m[2].trim())});`;
|
|
3267
|
+
return ` await expect(${aLoc}).toHaveAttribute("style", ${JSON.stringify(style)});`;
|
|
3268
|
+
}
|
|
3269
|
+
return "";
|
|
3270
|
+
}
|
|
3271
|
+
if (a.type === "hidden") {
|
|
3272
|
+
return a.hidden ? ` await expect(${aLoc}).toBeHidden();` : ` await expect(${aLoc}).toBeVisible();`;
|
|
3273
|
+
}
|
|
3274
|
+
if (a.type === "disabled") {
|
|
3275
|
+
return a.disabled ? ` await expect(${aLoc}).toBeDisabled();` : ` await expect(${aLoc}).toBeEnabled();`;
|
|
3276
|
+
}
|
|
3277
|
+
if (a.type === "aria-expanded" && a.ariaExpanded != null) {
|
|
3278
|
+
return ` await expect(${aLoc}).toHaveAttribute("aria-expanded", ${JSON.stringify(String(a.ariaExpanded))});`;
|
|
3279
|
+
}
|
|
3280
|
+
if (a.type === "aria-checked" && a.ariaChecked != null) {
|
|
3281
|
+
return ` await expect(${aLoc}).toHaveAttribute("aria-checked", ${JSON.stringify(String(a.ariaChecked))});`;
|
|
2878
3282
|
}
|
|
2879
3283
|
return "";
|
|
2880
3284
|
}).filter(Boolean).join("\n");
|
|
2881
3285
|
return step + "\n" + wait + (asserts ? "\n" + asserts : "");
|
|
2882
3286
|
}).join("\n");
|
|
2883
3287
|
const hasAutoAssertions = (recording.assertions || []).length > 0;
|
|
3288
|
+
const wsEvents = recording.websocketEvents || [];
|
|
3289
|
+
const hasWsEvents = wsEvents.length > 0 && wsEvents.some((c) => c.messages?.length > 0);
|
|
3290
|
+
const wsSetup = hasWsEvents ? ` const wsCollected = [];
|
|
3291
|
+
page.on('websocket', ws => {
|
|
3292
|
+
ws.on('framereceived', ev => wsCollected.push({ dir: 'in', payload: typeof ev.payload === 'string' ? ev.payload : String(ev.payload) }));
|
|
3293
|
+
ws.on('framesent', ev => wsCollected.push({ dir: 'out', payload: typeof ev.payload === 'string' ? ev.payload : String(ev.payload) }));
|
|
3294
|
+
});
|
|
3295
|
+
|
|
3296
|
+
` : "";
|
|
3297
|
+
const wsAssertions = hasWsEvents ? wsEvents.flatMap((conn) => (conn.messages || []).map((msg) => ({ dir: msg.dir, data: msg.data }))).map(
|
|
3298
|
+
(msg) => ` expect(wsCollected).toContainEqual({ dir: ${JSON.stringify(msg.dir)}, payload: ${JSON.stringify(msg.data)} });`
|
|
3299
|
+
).join("\n") + "\n\n" : "";
|
|
2884
3300
|
return `// Auto-generated by o.exportPlaywrightTest() \u2014 review and anonymize mocks before committing
|
|
2885
3301
|
// Prerequisites: npm install @playwright/test && npx playwright install chromium
|
|
2886
3302
|
// Run: npx playwright test recorded.spec.ts
|
|
@@ -2888,36 +3304,240 @@ import { test, expect } from '@playwright/test';
|
|
|
2888
3304
|
|
|
2889
3305
|
test(${JSON.stringify(testName)}, async ({ page }) => {
|
|
2890
3306
|
` + (routes ? ` // Network mocks \u2014 edit/anonymize before committing
|
|
2891
|
-
` + routes + "\n\n" : "") + ` // Set baseURL in playwright.config.ts: { use: { baseURL: 'https://staging.example.com' } }
|
|
3307
|
+
` + routes + "\n\n" : "") + wsSetup + ` // Set baseURL in playwright.config.ts: { use: { baseURL: 'https://staging.example.com' } }
|
|
2892
3308
|
await page.goto(${JSON.stringify(baseUrl)});
|
|
2893
3309
|
|
|
2894
|
-
` + (steps ? steps + "\n\n" : "") + (
|
|
3310
|
+
` + (steps ? steps + "\n\n" : "") + (wsAssertions ? ` // WebSocket verifications
|
|
3311
|
+
` + wsAssertions : "") + (!hasAutoAssertions && !hasWsEvents ? ` // TODO: Add assertions before committing, e.g.:
|
|
2895
3312
|
// await expect(page.locator('[data-qa="success-panel"]')).toBeVisible();
|
|
2896
3313
|
// await expect(page).toHaveURL(/\\/confirmation/);
|
|
2897
3314
|
// await expect(page.locator('[data-qa="error-banner"]')).not.toBeVisible();
|
|
2898
|
-
` : ` // Auto-generated assertions above \u2014 review for correctness before committing
|
|
2899
|
-
`) + `});
|
|
3315
|
+
` : hasAutoAssertions || hasWsEvents ? ` // Auto-generated assertions above \u2014 review for correctness before committing
|
|
3316
|
+
` : "") + `});
|
|
2900
3317
|
`;
|
|
2901
3318
|
};
|
|
2902
3319
|
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);
|
|
3320
|
+
const isOptions = opts && typeof opts === "object" && (opts.runAssertions !== void 0 || opts.root !== void 0 || opts.manualChecks !== void 0 || opts.actionDelay !== void 0 || opts.skipWebSocketMock !== void 0 || opts.skipNetworkMocks !== void 0 || opts.recordingAssertionDebug !== void 0 || opts.strictPlay !== void 0 || opts.strictAssertions !== void 0 || opts.strictNetwork !== void 0 || opts.strictWebSocket !== void 0 || opts.strictRemoved !== void 0 || opts.onComplete !== void 0);
|
|
2904
3321
|
const mockOverrides = isOptions ? opts.mockOverrides || {} : opts;
|
|
2905
3322
|
const runAssertions = isOptions && opts.runAssertions;
|
|
2906
3323
|
const rootOpt = isOptions ? opts.root : void 0;
|
|
2907
3324
|
const manualChecks = isOptions && opts.manualChecks || [];
|
|
2908
3325
|
const actionDelay = isOptions && opts.actionDelay !== void 0 ? opts.actionDelay : 16;
|
|
3326
|
+
const skipWebSocketMock = isOptions && opts.skipWebSocketMock;
|
|
3327
|
+
const skipNetworkMocks = isOptions && opts.skipNetworkMocks;
|
|
3328
|
+
if (isOptions && opts.recordingAssertionDebug !== void 0) {
|
|
3329
|
+
o.recordingAssertionDebug = !!opts.recordingAssertionDebug;
|
|
3330
|
+
}
|
|
3331
|
+
const sc = recording.strictCapture || {};
|
|
3332
|
+
const strictPlay = isOptions && opts.strictPlay === true;
|
|
3333
|
+
const strictAssertions = isOptions && opts.strictAssertions !== void 0 ? !!opts.strictAssertions : strictPlay ? true : !!sc.assertions;
|
|
3334
|
+
const strictNetwork = isOptions && opts.strictNetwork !== void 0 ? !!opts.strictNetwork : strictPlay ? true : !!sc.network;
|
|
3335
|
+
const strictWebSocket = isOptions && opts.strictWebSocket !== void 0 ? !!opts.strictWebSocket : strictPlay ? true : !!sc.websocket;
|
|
3336
|
+
const strictRemoved = isOptions && opts.strictRemoved !== void 0 ? !!opts.strictRemoved : strictAssertions;
|
|
3337
|
+
const parseBodyLikeRecorder = (body) => {
|
|
3338
|
+
if (body == null || body === "") return void 0;
|
|
3339
|
+
if (typeof body === "string") {
|
|
3340
|
+
try {
|
|
3341
|
+
return JSON.parse(body);
|
|
3342
|
+
} catch (_e) {
|
|
3343
|
+
return body;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
return body;
|
|
3347
|
+
};
|
|
3348
|
+
const mockRequestMatchesLive = (recordedReq, liveBody) => {
|
|
3349
|
+
const live = parseBodyLikeRecorder(liveBody);
|
|
3350
|
+
if (recordedReq === live) return true;
|
|
3351
|
+
if (recordedReq == null && live == null) return true;
|
|
3352
|
+
if (recordedReq == null || live == null) return false;
|
|
3353
|
+
if (typeof recordedReq === "object" && typeof live === "object")
|
|
3354
|
+
return JSON.stringify(recordedReq) === JSON.stringify(live);
|
|
3355
|
+
return String(recordedReq) === String(live);
|
|
3356
|
+
};
|
|
3357
|
+
const normWsData = (s) => String(s || "").trim().replace(/\s+/g, " ");
|
|
2909
3358
|
const allMocks = Object.assign({}, recording.mocks, mockOverrides);
|
|
2910
3359
|
const origFetch = window.fetch;
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
3360
|
+
const origXHROpen = XMLHttpRequest.prototype.open;
|
|
3361
|
+
const origXHRSend = XMLHttpRequest.prototype.send;
|
|
3362
|
+
if (!skipNetworkMocks) {
|
|
3363
|
+
window.fetch = (url, fetchOpts = {}) => {
|
|
3364
|
+
const method = (fetchOpts.method || "GET").toUpperCase();
|
|
3365
|
+
const key = method + ":" + url;
|
|
3366
|
+
if (allMocks[key]) {
|
|
3367
|
+
const mock = allMocks[key];
|
|
3368
|
+
if (strictNetwork && o.C(mock, "request") && !mockRequestMatchesLive(mock.request, fetchOpts.body)) {
|
|
3369
|
+
return Promise.reject(
|
|
3370
|
+
new Error(
|
|
3371
|
+
"[Objs playRecording] strictNetwork: request body does not match recording for " + key
|
|
3372
|
+
)
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
const body = typeof mock.response === "string" ? mock.response : JSON.stringify(mock.response);
|
|
3376
|
+
return Promise.resolve(new Response(body, { status: mock.status || 200 }));
|
|
3377
|
+
}
|
|
3378
|
+
return origFetch(url, fetchOpts);
|
|
3379
|
+
};
|
|
3380
|
+
XMLHttpRequest.prototype.open = function(method, url) {
|
|
3381
|
+
this._oMethod = (method || "GET").toUpperCase();
|
|
3382
|
+
this._oUrl = url;
|
|
3383
|
+
return origXHROpen.apply(this, arguments);
|
|
3384
|
+
};
|
|
3385
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
3386
|
+
const xhr = this;
|
|
3387
|
+
const key = (xhr._oMethod || "GET") + ":" + (xhr._oUrl || "");
|
|
2915
3388
|
const mock = allMocks[key];
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
3389
|
+
if (mock) {
|
|
3390
|
+
if (strictNetwork && o.C(mock, "request") && !mockRequestMatchesLive(mock.request, body)) {
|
|
3391
|
+
setTimeout(() => {
|
|
3392
|
+
xhr.readyState = 4;
|
|
3393
|
+
xhr.status = 0;
|
|
3394
|
+
xhr.statusText = "Objs strictNetwork mismatch";
|
|
3395
|
+
xhr.dispatchEvent(new Event("readystatechange"));
|
|
3396
|
+
xhr.dispatchEvent(new Event("error"));
|
|
3397
|
+
}, 0);
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
const respBody = typeof mock.response === "string" ? mock.response : JSON.stringify(mock.response);
|
|
3401
|
+
setTimeout(() => {
|
|
3402
|
+
xhr.readyState = 4;
|
|
3403
|
+
xhr.status = mock.status || 200;
|
|
3404
|
+
xhr.statusText = "OK";
|
|
3405
|
+
xhr.responseText = respBody;
|
|
3406
|
+
xhr.response = respBody;
|
|
3407
|
+
xhr.dispatchEvent(new Event("readystatechange"));
|
|
3408
|
+
xhr.dispatchEvent(new Event("load"));
|
|
3409
|
+
}, 0);
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
return origXHRSend.apply(this, arguments);
|
|
3413
|
+
};
|
|
3414
|
+
}
|
|
3415
|
+
let origWebSocket = null;
|
|
3416
|
+
const wsEvents = recording.websocketEvents || [];
|
|
3417
|
+
const useWsMock = !skipWebSocketMock && wsEvents.length > 0 && wsEvents.some((e) => e.messages && e.messages.length > 0);
|
|
3418
|
+
if (useWsMock && typeof window.WebSocket === "function") {
|
|
3419
|
+
origWebSocket = window.WebSocket;
|
|
3420
|
+
let wsConsumeIdx = 0;
|
|
3421
|
+
const normalizeWsUrl = (u) => {
|
|
3422
|
+
const s = typeof u === "string" ? u : String(u);
|
|
3423
|
+
try {
|
|
3424
|
+
return new URL(s, window.location.href).href;
|
|
3425
|
+
} catch (_e) {
|
|
3426
|
+
return s;
|
|
3427
|
+
}
|
|
3428
|
+
};
|
|
3429
|
+
const takeNextRecorded = (urlStr) => {
|
|
3430
|
+
const norm = normalizeWsUrl(urlStr);
|
|
3431
|
+
for (let i = wsConsumeIdx; i < wsEvents.length; i++) {
|
|
3432
|
+
if (normalizeWsUrl(wsEvents[i].url) === norm) {
|
|
3433
|
+
wsConsumeIdx = i + 1;
|
|
3434
|
+
return wsEvents[i];
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
for (let i = wsConsumeIdx; i < wsEvents.length; i++) {
|
|
3438
|
+
if (String(wsEvents[i].url) === String(urlStr)) {
|
|
3439
|
+
wsConsumeIdx = i + 1;
|
|
3440
|
+
return wsEvents[i];
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
return null;
|
|
3444
|
+
};
|
|
3445
|
+
const C = origWebSocket;
|
|
3446
|
+
class O_MockWebSocket extends EventTarget {
|
|
3447
|
+
constructor(url, protocols, recorded) {
|
|
3448
|
+
super();
|
|
3449
|
+
const urlStr = typeof url === "string" ? url : String(url);
|
|
3450
|
+
this.url = urlStr;
|
|
3451
|
+
this.readyState = C.CONNECTING;
|
|
3452
|
+
const p = protocols;
|
|
3453
|
+
this.protocol = Array.isArray(p) ? p[0] || "" : p ? String(p) : "";
|
|
3454
|
+
this.extensions = "";
|
|
3455
|
+
this.binaryType = "blob";
|
|
3456
|
+
this._messages = (recorded.messages || []).slice();
|
|
3457
|
+
this._pos = 0;
|
|
3458
|
+
const self = this;
|
|
3459
|
+
setTimeout(() => {
|
|
3460
|
+
if (self.readyState === C.CLOSED) return;
|
|
3461
|
+
self.readyState = C.OPEN;
|
|
3462
|
+
self._dispatchOpen();
|
|
3463
|
+
self._drainInbound();
|
|
3464
|
+
}, 0);
|
|
3465
|
+
}
|
|
3466
|
+
_dispatchOpen() {
|
|
3467
|
+
const ev = new Event("open");
|
|
3468
|
+
this.dispatchEvent(ev);
|
|
3469
|
+
if (typeof this.onopen === "function") this.onopen(ev);
|
|
3470
|
+
}
|
|
3471
|
+
_dispatchMessage(data) {
|
|
3472
|
+
const ev = new MessageEvent("message", { data });
|
|
3473
|
+
this.dispatchEvent(ev);
|
|
3474
|
+
if (typeof this.onmessage === "function") this.onmessage(ev);
|
|
3475
|
+
}
|
|
3476
|
+
_drainInbound() {
|
|
3477
|
+
while (this._pos < this._messages.length && this._messages[this._pos].dir === "in") {
|
|
3478
|
+
const m = this._messages[this._pos++];
|
|
3479
|
+
this._dispatchMessage(m.data);
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
send(data) {
|
|
3483
|
+
if (this.readyState !== C.OPEN) {
|
|
3484
|
+
const err = typeof DOMException !== "undefined" ? new DOMException("Still in CONNECTING state.", "InvalidStateError") : new Error("InvalidStateError");
|
|
3485
|
+
throw err;
|
|
3486
|
+
}
|
|
3487
|
+
if (this._pos >= this._messages.length) {
|
|
3488
|
+
if (strictWebSocket) {
|
|
3489
|
+
throw new Error(
|
|
3490
|
+
"[Objs playRecording] strictWebSocket: unexpected send() after recorded frames exhausted"
|
|
3491
|
+
);
|
|
3492
|
+
}
|
|
3493
|
+
this._drainInbound();
|
|
3494
|
+
return;
|
|
3495
|
+
}
|
|
3496
|
+
const next = this._messages[this._pos];
|
|
3497
|
+
if (next.dir === "out") {
|
|
3498
|
+
if (strictWebSocket) {
|
|
3499
|
+
const got = typeof data === "string" ? data : String(data);
|
|
3500
|
+
const exp = String(next.data != null ? next.data : "");
|
|
3501
|
+
if (normWsData(got) !== normWsData(exp)) {
|
|
3502
|
+
throw new Error(
|
|
3503
|
+
"[Objs playRecording] strictWebSocket: outbound frame mismatch"
|
|
3504
|
+
);
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
this._pos++;
|
|
3508
|
+
}
|
|
3509
|
+
this._drainInbound();
|
|
3510
|
+
}
|
|
3511
|
+
close(code, reason) {
|
|
3512
|
+
if (this.readyState === C.CLOSING || this.readyState === C.CLOSED) return;
|
|
3513
|
+
this.readyState = C.CLOSING;
|
|
3514
|
+
const self = this;
|
|
3515
|
+
setTimeout(() => {
|
|
3516
|
+
self.readyState = C.CLOSED;
|
|
3517
|
+
const ev = typeof CloseEvent !== "undefined" ? new CloseEvent("close", {
|
|
3518
|
+
code: code !== void 0 ? code : 1e3,
|
|
3519
|
+
reason: reason !== void 0 ? String(reason) : "",
|
|
3520
|
+
wasClean: true
|
|
3521
|
+
}) : new Event("close");
|
|
3522
|
+
self.dispatchEvent(ev);
|
|
3523
|
+
if (typeof self.onclose === "function") self.onclose(ev);
|
|
3524
|
+
}, 0);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
const MockWebSocketCtor = function MockWebSocketCtor2(url, protocols) {
|
|
3528
|
+
const urlStr = typeof url === "string" ? url : String(url);
|
|
3529
|
+
const rec = takeNextRecorded(urlStr);
|
|
3530
|
+
if (!rec || !rec.messages || rec.messages.length === 0) {
|
|
3531
|
+
return new origWebSocket(url, protocols);
|
|
3532
|
+
}
|
|
3533
|
+
return new O_MockWebSocket(url, protocols, rec);
|
|
3534
|
+
};
|
|
3535
|
+
MockWebSocketCtor.CONNECTING = C.CONNECTING;
|
|
3536
|
+
MockWebSocketCtor.OPEN = C.OPEN;
|
|
3537
|
+
MockWebSocketCtor.CLOSING = C.CLOSING;
|
|
3538
|
+
MockWebSocketCtor.CLOSED = C.CLOSED;
|
|
3539
|
+
window.WebSocket = MockWebSocketCtor;
|
|
3540
|
+
}
|
|
2921
3541
|
const resolveRoot = () => {
|
|
2922
3542
|
if (rootOpt != null) {
|
|
2923
3543
|
return typeof rootOpt === "string" ? o.D.querySelector(rootOpt) || o.D.body : rootOpt;
|
|
@@ -2976,6 +3596,7 @@ o.playRecording = (recording, opts = {}) => {
|
|
|
2976
3596
|
}
|
|
2977
3597
|
}
|
|
2978
3598
|
if (!el && action.type !== "scroll") {
|
|
3599
|
+
if (action.type === "blur" || action.type === "focus") return true;
|
|
2979
3600
|
return `element not found: ${action.target}`;
|
|
2980
3601
|
}
|
|
2981
3602
|
if (action.type === "scroll") {
|
|
@@ -2984,6 +3605,22 @@ o.playRecording = (recording, opts = {}) => {
|
|
|
2984
3605
|
if (action.value !== void 0) el.value = action.value;
|
|
2985
3606
|
if (action.checked !== void 0) el.checked = action.checked;
|
|
2986
3607
|
el.dispatchEvent(new Event(action.type, { bubbles: true }));
|
|
3608
|
+
} else if (action.type === "submit") {
|
|
3609
|
+
if (typeof el.requestSubmit === "function") el.requestSubmit();
|
|
3610
|
+
else el.submit();
|
|
3611
|
+
} else if (action.type === "keydown") {
|
|
3612
|
+
el.dispatchEvent(
|
|
3613
|
+
new KeyboardEvent("keydown", {
|
|
3614
|
+
key: action.key || "",
|
|
3615
|
+
code: action.code || "",
|
|
3616
|
+
bubbles: true,
|
|
3617
|
+
cancelable: true
|
|
3618
|
+
})
|
|
3619
|
+
);
|
|
3620
|
+
} else if (action.type === "focus") {
|
|
3621
|
+
el.focus();
|
|
3622
|
+
} else if (action.type === "blur") {
|
|
3623
|
+
el.blur();
|
|
2987
3624
|
} else {
|
|
2988
3625
|
if (action.type === "click") {
|
|
2989
3626
|
el.click();
|
|
@@ -3005,7 +3642,9 @@ o.playRecording = (recording, opts = {}) => {
|
|
|
3005
3642
|
const run = () => {
|
|
3006
3643
|
const r = o.runRecordingAssertions(recording, rootEl, i, {
|
|
3007
3644
|
assertions: asserted,
|
|
3008
|
-
removedElements: recording.removedElements
|
|
3645
|
+
removedElements: recording.removedElements,
|
|
3646
|
+
strictAssertions,
|
|
3647
|
+
strictRemoved
|
|
3009
3648
|
});
|
|
3010
3649
|
assertionAccum.passed += r.passed;
|
|
3011
3650
|
assertionAccum.total += r.total;
|
|
@@ -3034,6 +3673,9 @@ o.playRecording = (recording, opts = {}) => {
|
|
|
3034
3673
|
const onComplete = isOptions && opts.onComplete;
|
|
3035
3674
|
const testId = o.test("Recorded playback", ...testCases, { sync: true }, (testId2) => {
|
|
3036
3675
|
window.fetch = origFetch;
|
|
3676
|
+
XMLHttpRequest.prototype.open = origXHROpen;
|
|
3677
|
+
XMLHttpRequest.prototype.send = origXHRSend;
|
|
3678
|
+
if (origWebSocket) window.WebSocket = origWebSocket;
|
|
3037
3679
|
const assertionResult = runAssertions && assertions.length > 0 ? assertionAccum : void 0;
|
|
3038
3680
|
if (assertionResult?.failures?.length > 0) {
|
|
3039
3681
|
o.tRes[testId2] = false;
|
|
@@ -3051,34 +3693,54 @@ o.testOverlay = () => {
|
|
|
3051
3693
|
if (o("#" + btnId).el) {
|
|
3052
3694
|
return;
|
|
3053
3695
|
}
|
|
3696
|
+
const scrollId = "o-test-overlay-scroll";
|
|
3697
|
+
const exportBtnId = "o-test-export-objs";
|
|
3698
|
+
const copyBtnId = "o-test-copy-txt";
|
|
3699
|
+
const btnBarStyle = "padding:6px 10px;background:#334155;color:#e2e8f0;border:none;border-radius:6px;cursor:pointer;font-size:12px;";
|
|
3700
|
+
const buildListPlainText = () => o.tLog.map((log, i) => (log != null && log !== "" ? String(log) : "Test #" + i) + (o.tRes[i] ? " \u2713" : " \u2717")).join("\n\n");
|
|
3054
3701
|
const updatePanel = () => {
|
|
3055
|
-
const
|
|
3056
|
-
if (!
|
|
3057
|
-
|
|
3058
|
-
const passed = o.tRes.filter(Boolean).length;
|
|
3059
|
-
let html = `<b>Tests: ${passed}/${total}</b><hr style="margin:4px 0">`;
|
|
3702
|
+
const scroll = o("#" + scrollId);
|
|
3703
|
+
if (!scroll.el) return;
|
|
3704
|
+
let html = "";
|
|
3060
3705
|
o.tLog.forEach((log, i) => {
|
|
3061
3706
|
const ok = o.tRes[i];
|
|
3062
|
-
html += `<div style="margin:2px 0;padding:2px 4px;border-radius:3px;background:${ok ? "#
|
|
3063
|
-
});
|
|
3064
|
-
html += `<button id="o-test-export" style="margin-top:6px;padding:4px 8px;font-size:11px;cursor:pointer">Export results</button>`;
|
|
3065
|
-
panel.html(html);
|
|
3066
|
-
o("#o-test-export").on("click", () => {
|
|
3067
|
-
const data = JSON.stringify({ results: o.tRes, logs: o.tLog }, null, 2);
|
|
3068
|
-
const blob = new Blob([data], { type: "application/json" });
|
|
3069
|
-
const a = o.D.createElement("a");
|
|
3070
|
-
a.href = URL.createObjectURL(blob);
|
|
3071
|
-
a.download = "objs-test-results.json";
|
|
3072
|
-
a.click();
|
|
3707
|
+
html += `<div style="margin:2px 0;padding:2px 4px;border-radius:3px;background:${ok ? "#14532d" : "#450a0a"};color:${ok ? "#86efac" : "#fca5a5"};font-size:11px;white-space:pre-wrap">${log || "Test #" + i}</div>`;
|
|
3073
3708
|
});
|
|
3709
|
+
scroll.html(html);
|
|
3074
3710
|
};
|
|
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="
|
|
3711
|
+
const innerHTML = `<div class="o-test-overlay-root" style="display:flex;flex-direction:column;gap:4px;max-height:min(88vh,560px);overflow:hidden;"><div style="display:flex;align-items:center;gap:12px;flex-shrink:0;"><span class="o-test-overlay-summary" style="flex:1;font-size:13px;cursor:grab;">Tests: 0/0</span><button type="button" class="o-test-overlay-toggle" style="${btnBarStyle}">List</button><button type="button" class="o-test-overlay-close" style="padding:4px 8px;background:transparent;color:#94a3b8;border:none;border-radius:4px;cursor:pointer;font-size:16px;line-height:1;" title="Close">\xD7</button></div><div id="${panelId}" style="display:none;flex-direction:column;margin-top:4px;max-height:min(52vh,420px);background:#0a0f1e;border:1px solid #1e293b;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,.35);overflow:hidden;"><div id="${scrollId}" style="box-sizing:border-box;height:min(48vh,380px);overflow-y:scroll;padding:8px;font-size:11px;user-select:text;cursor:text;"></div><div id="o-test-overlay-footer" style="display:flex;flex-wrap:wrap;gap:8px;padding:8px;border-top:1px solid #1e293b;background:#0f172a;flex-shrink:0;"><button type="button" id="${exportBtnId}" class="o-test-overlay-export-btn" style="${btnBarStyle}">Export (objs)</button><button type="button" id="${copyBtnId}" class="o-test-overlay-export-btn" style="${btnBarStyle}">Copy (txt)</button></div></div></div>`;
|
|
3076
3712
|
const box = o.overlay({
|
|
3077
3713
|
innerHTML,
|
|
3078
3714
|
removeExisting: false,
|
|
3079
3715
|
className: "o-test-overlay",
|
|
3080
3716
|
id: btnId,
|
|
3081
|
-
excludeDragSelector: ".o-test-overlay-close, .o-test-overlay-toggle, #" + panelId
|
|
3717
|
+
excludeDragSelector: ".o-test-overlay-close, .o-test-overlay-toggle, #" + panelId + ", #" + scrollId + ", #o-test-overlay-footer, .o-test-overlay-export-btn"
|
|
3718
|
+
});
|
|
3719
|
+
o("#" + exportBtnId).on("click", () => {
|
|
3720
|
+
const data = JSON.stringify({ results: o.tRes, logs: o.tLog }, null, 2);
|
|
3721
|
+
const blob = new Blob([data], { type: "application/json" });
|
|
3722
|
+
const a = o.D.createElement("a");
|
|
3723
|
+
a.href = URL.createObjectURL(blob);
|
|
3724
|
+
a.download = "objs-test-results.json";
|
|
3725
|
+
a.click();
|
|
3726
|
+
});
|
|
3727
|
+
o("#" + copyBtnId).on("click", () => {
|
|
3728
|
+
const text = buildListPlainText();
|
|
3729
|
+
const write = () => {
|
|
3730
|
+
const ta = o.D.createElement("textarea");
|
|
3731
|
+
ta.value = text;
|
|
3732
|
+
ta.setAttribute("readonly", "");
|
|
3733
|
+
ta.style.cssText = "position:fixed;left:-9999px;top:0";
|
|
3734
|
+
o.D.body.appendChild(ta);
|
|
3735
|
+
ta.select();
|
|
3736
|
+
o.D.execCommand("copy");
|
|
3737
|
+
ta.remove();
|
|
3738
|
+
};
|
|
3739
|
+
if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
|
|
3740
|
+
navigator.clipboard.writeText(text).catch(write);
|
|
3741
|
+
} else {
|
|
3742
|
+
write();
|
|
3743
|
+
}
|
|
3082
3744
|
});
|
|
3083
3745
|
const refreshSummary = () => {
|
|
3084
3746
|
const summary = o(".o-test-overlay-summary");
|
|
@@ -3089,8 +3751,12 @@ o.testOverlay = () => {
|
|
|
3089
3751
|
const panel = o("#" + panelId);
|
|
3090
3752
|
if (!panel.el) return;
|
|
3091
3753
|
const isOpen = panel.el.style.display !== "none";
|
|
3092
|
-
|
|
3093
|
-
|
|
3754
|
+
if (isOpen) {
|
|
3755
|
+
panel.el.style.display = "none";
|
|
3756
|
+
} else {
|
|
3757
|
+
panel.el.style.display = "flex";
|
|
3758
|
+
updatePanel();
|
|
3759
|
+
}
|
|
3094
3760
|
});
|
|
3095
3761
|
box.first(".o-test-overlay-close").on("click", () => {
|
|
3096
3762
|
box._overlayCleanup();
|
|
@@ -3098,7 +3764,7 @@ o.testOverlay = () => {
|
|
|
3098
3764
|
o.testOverlay.showPanel = () => {
|
|
3099
3765
|
const panel = o("#" + panelId);
|
|
3100
3766
|
if (!panel.el) return;
|
|
3101
|
-
panel.
|
|
3767
|
+
panel.el.style.display = "flex";
|
|
3102
3768
|
updatePanel();
|
|
3103
3769
|
refreshSummary();
|
|
3104
3770
|
};
|