@vosjs/render-core 0.8.0 → 0.10.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/dist/record/index.d.ts +490 -196
- package/dist/record/index.js +916 -62
- package/dist/record/index.js.map +1 -1
- package/package.json +2 -2
package/dist/record/index.js
CHANGED
|
@@ -701,11 +701,11 @@ function urlSchemeProblem(url) {
|
|
|
701
701
|
return `${url} is not an http(s) address, and the recorder opens only http and https pages`;
|
|
702
702
|
return null;
|
|
703
703
|
}
|
|
704
|
-
function validateSetup(value) {
|
|
704
|
+
function validateSetup(value, phase = "setup") {
|
|
705
705
|
const errors = [];
|
|
706
|
-
if (!Array.isArray(value)) return [
|
|
706
|
+
if (!Array.isArray(value)) return [`${phase} must be an array of steps`];
|
|
707
707
|
value.forEach((raw, i) => {
|
|
708
|
-
const at =
|
|
708
|
+
const at = `${phase}[${i}]`;
|
|
709
709
|
if (typeof raw !== "object" || raw === null) {
|
|
710
710
|
errors.push(`${at}: must be an object`);
|
|
711
711
|
return;
|
|
@@ -750,73 +750,141 @@ function setupEnvNames(steps) {
|
|
|
750
750
|
}
|
|
751
751
|
return [...out];
|
|
752
752
|
}
|
|
753
|
-
function
|
|
753
|
+
function scriptEnvNames(script) {
|
|
754
|
+
return [
|
|
755
|
+
.../* @__PURE__ */ new Set([
|
|
756
|
+
...setupEnvNames(script.setup),
|
|
757
|
+
...setupEnvNames(script.teardown)
|
|
758
|
+
])
|
|
759
|
+
];
|
|
760
|
+
}
|
|
761
|
+
function resolveSetupText(t, env, phase = "setup") {
|
|
754
762
|
if (typeof t === "string") return { value: t, secret: false };
|
|
755
763
|
const v = env[t.env];
|
|
756
764
|
if (v === void 0 || v === "")
|
|
757
765
|
throw new SetupEnvError(
|
|
758
|
-
|
|
766
|
+
`${phase}: the environment variable ${t.env} is not set, and a type step reads it. Export it in the shell that runs vos record; it is never written to a file`
|
|
759
767
|
);
|
|
760
768
|
return { value: v, secret: true };
|
|
761
769
|
}
|
|
770
|
+
function teardownEnvProblem(steps, env) {
|
|
771
|
+
for (const [i, s] of (steps ?? []).entries()) {
|
|
772
|
+
if (s.do !== "type" || typeof s.text !== "object") continue;
|
|
773
|
+
const v = env[s.text.env];
|
|
774
|
+
if (v === void 0 || v === "")
|
|
775
|
+
return `teardown[${i}] reads the environment variable ${s.text.env}, which is not set, so the take could leave behind what its teardown removes. Export it in the shell that runs vos record; it is never written to a file. Nothing was recorded.`;
|
|
776
|
+
}
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
var TEARDOWN_BOUND_MS = 6e4;
|
|
780
|
+
function teardownLine(report) {
|
|
781
|
+
const f = report.failed;
|
|
782
|
+
if (!f) return `teardown: ${report.ran} step(s) ran`;
|
|
783
|
+
const reason = f.error.replace(/[.\s]+$/, "");
|
|
784
|
+
return `TEARDOWN FAILED at teardown[${f.step}] (${f.do}${f.selector ? ` ${f.selector}` : ""}): ${reason}. What the take created may still be in the product; remove it by hand.`;
|
|
785
|
+
}
|
|
762
786
|
async function runSetup(page, steps, opts) {
|
|
763
787
|
const env = opts.env ?? {};
|
|
788
|
+
const phase = opts.phase ?? "setup";
|
|
789
|
+
const now = opts.now ?? Date.now;
|
|
790
|
+
const deadline = opts.boundMs === void 0 ? void 0 : now() + opts.boundMs;
|
|
764
791
|
const lines = [];
|
|
765
|
-
|
|
792
|
+
let typed = "";
|
|
793
|
+
const runStep = async (step, i) => {
|
|
766
794
|
const settle = "ms" in step && typeof step.ms === "number" ? step.ms : 150;
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
788
|
-
await loc.click();
|
|
789
|
-
await opts.sleep(settle);
|
|
790
|
-
lines.push(`setup #${i} click ${step.selector}`);
|
|
791
|
-
break;
|
|
792
|
-
}
|
|
793
|
-
case "type": {
|
|
794
|
-
const loc = page.locator(step.selector).first();
|
|
795
|
-
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
796
|
-
const { value, secret } = resolveSetupText(step.text, env);
|
|
797
|
-
await loc.fill(value);
|
|
798
|
-
await opts.sleep(settle);
|
|
799
|
-
lines.push(
|
|
800
|
-
`setup #${i} type ${secret ? `\${${step.text.env}}` : `${value.length} chars`} into ${step.selector}`
|
|
801
|
-
);
|
|
802
|
-
break;
|
|
803
|
-
}
|
|
795
|
+
switch (step.do) {
|
|
796
|
+
case "wait":
|
|
797
|
+
await opts.sleep(step.ms);
|
|
798
|
+
return `${phase} #${i} wait ${step.ms}ms`;
|
|
799
|
+
case "goto":
|
|
800
|
+
await (opts.goto ? opts.goto(step.url) : page.goto(step.url, {
|
|
801
|
+
waitUntil: "networkidle",
|
|
802
|
+
timeout: 45e3
|
|
803
|
+
})).catch(() => null);
|
|
804
|
+
return `${phase} #${i} goto ${step.url}`;
|
|
805
|
+
case "press":
|
|
806
|
+
await page.keyboard.press(step.key);
|
|
807
|
+
await opts.sleep(settle);
|
|
808
|
+
return `${phase} #${i} press ${step.key}`;
|
|
809
|
+
case "click": {
|
|
810
|
+
const loc = page.locator(step.selector).first();
|
|
811
|
+
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
812
|
+
await loc.click();
|
|
813
|
+
await opts.sleep(settle);
|
|
814
|
+
return `${phase} #${i} click ${step.selector}`;
|
|
804
815
|
}
|
|
816
|
+
case "type": {
|
|
817
|
+
const loc = page.locator(step.selector).first();
|
|
818
|
+
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
819
|
+
const { value, secret } = resolveSetupText(step.text, env, phase);
|
|
820
|
+
typed = value;
|
|
821
|
+
await loc.fill(value);
|
|
822
|
+
await opts.sleep(settle);
|
|
823
|
+
return `${phase} #${i} type ${secret ? `\${${step.text.env}}` : `${value.length} chars`} into ${step.selector}`;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
for (const [i, step] of steps.entries()) {
|
|
828
|
+
typed = "";
|
|
829
|
+
try {
|
|
830
|
+
lines.push(
|
|
831
|
+
await withinDeadline(
|
|
832
|
+
runStep(step, i),
|
|
833
|
+
deadline,
|
|
834
|
+
now,
|
|
835
|
+
`the ${phase} ran past its ${sayBound(opts.boundMs ?? 0)} bound`
|
|
836
|
+
)
|
|
837
|
+
);
|
|
805
838
|
} catch (e) {
|
|
806
|
-
if (e instanceof SetupEnvError) throw e;
|
|
839
|
+
if (e instanceof SetupEnvError && phase === "setup") throw e;
|
|
807
840
|
return {
|
|
808
841
|
ran: i,
|
|
809
842
|
lines,
|
|
810
843
|
failed: {
|
|
811
844
|
step: i,
|
|
812
845
|
do: step.do,
|
|
813
|
-
..."selector" in step ? { selector: step.selector } : {}
|
|
846
|
+
..."selector" in step ? { selector: step.selector } : {},
|
|
847
|
+
error: reasonWords(e, typed)
|
|
814
848
|
}
|
|
815
849
|
};
|
|
816
850
|
}
|
|
817
851
|
}
|
|
818
852
|
return { ran: steps.length, lines };
|
|
819
853
|
}
|
|
854
|
+
function sayBound(ms) {
|
|
855
|
+
return ms >= 1e3 ? `${Math.round(ms / 1e3)} s` : `${ms} ms`;
|
|
856
|
+
}
|
|
857
|
+
function withinDeadline(p, deadline, now, message) {
|
|
858
|
+
if (deadline === void 0) return p;
|
|
859
|
+
const left = deadline - now();
|
|
860
|
+
return new Promise((resolve, reject) => {
|
|
861
|
+
const timer = setTimeout(
|
|
862
|
+
() => {
|
|
863
|
+
p.catch(() => {
|
|
864
|
+
});
|
|
865
|
+
reject(new Error(message));
|
|
866
|
+
},
|
|
867
|
+
Math.max(0, left)
|
|
868
|
+
);
|
|
869
|
+
p.then(
|
|
870
|
+
(v) => {
|
|
871
|
+
clearTimeout(timer);
|
|
872
|
+
resolve(v);
|
|
873
|
+
},
|
|
874
|
+
(e) => {
|
|
875
|
+
clearTimeout(timer);
|
|
876
|
+
reject(e);
|
|
877
|
+
}
|
|
878
|
+
);
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
var ANSI_COLOUR = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
882
|
+
function reasonWords(e, typed) {
|
|
883
|
+
const raw = e instanceof Error ? e.message : String(e);
|
|
884
|
+
const line = (raw.replace(ANSI_COLOUR, "").split("\n")[0] ?? "").trim();
|
|
885
|
+
if (typed && line.includes(typed)) return "the field could not be filled";
|
|
886
|
+
return line || "the step failed";
|
|
887
|
+
}
|
|
820
888
|
var SetupEnvError = class extends Error {
|
|
821
889
|
constructor(message) {
|
|
822
890
|
super(message);
|
|
@@ -1274,8 +1342,79 @@ async function fitSurfaceToViewport(cdp, width, height) {
|
|
|
1274
1342
|
function visibleTextScript(text) {
|
|
1275
1343
|
return `(() => { const b = document.body; return !!b && (b.innerText || '').includes(${JSON.stringify(text)}) })()`;
|
|
1276
1344
|
}
|
|
1345
|
+
var JS_SAID_MAX = 120;
|
|
1346
|
+
var JS_VALUE_MAX = 2048;
|
|
1347
|
+
function jsConditionScript(expr) {
|
|
1348
|
+
return `(async () => {
|
|
1349
|
+
let v;
|
|
1350
|
+
try { v = await (${expr}
|
|
1351
|
+
); } catch (e) { return JSON.stringify({ threw: String(e && e.message ? e.message : e) }) }
|
|
1352
|
+
const out = { truthy: !!v };
|
|
1353
|
+
try { const j = JSON.stringify(v); if (j !== undefined) out.json = j } catch (e) {}
|
|
1354
|
+
try { out.said = typeof v === 'string' ? JSON.stringify(v) : String(v) } catch (e) { out.said = typeof v }
|
|
1355
|
+
if (out.json !== undefined && typeof v !== 'string') out.said = out.json
|
|
1356
|
+
return JSON.stringify(out);
|
|
1357
|
+
})()`;
|
|
1358
|
+
}
|
|
1359
|
+
function sameJson(a, b) {
|
|
1360
|
+
if (a === b) return true;
|
|
1361
|
+
if (typeof a !== typeof b || a === null || b === null) return false;
|
|
1362
|
+
if (typeof a !== "object") return false;
|
|
1363
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
1364
|
+
if (Array.isArray(a)) {
|
|
1365
|
+
const bb = b;
|
|
1366
|
+
return a.length === bb.length && a.every((v, i) => sameJson(v, bb[i]));
|
|
1367
|
+
}
|
|
1368
|
+
const ao = a;
|
|
1369
|
+
const bo = b;
|
|
1370
|
+
const keys = Object.keys(ao);
|
|
1371
|
+
if (keys.length !== Object.keys(bo).length) return false;
|
|
1372
|
+
return keys.every(
|
|
1373
|
+
(k) => Object.prototype.hasOwnProperty.call(bo, k) && sameJson(ao[k], bo[k])
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
async function readJs(page, c) {
|
|
1377
|
+
let raw;
|
|
1378
|
+
try {
|
|
1379
|
+
raw = await page.evaluate(jsConditionScript(c.js));
|
|
1380
|
+
} catch (e) {
|
|
1381
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
1382
|
+
if (/SyntaxError/.test(why) || e instanceof Error && e.name === "SyntaxError")
|
|
1383
|
+
return { holds: false, threw: `it does not parse: ${why.split("\n")[0]}` };
|
|
1384
|
+
throw e;
|
|
1385
|
+
}
|
|
1386
|
+
let got;
|
|
1387
|
+
try {
|
|
1388
|
+
got = JSON.parse(raw);
|
|
1389
|
+
} catch {
|
|
1390
|
+
return { holds: false };
|
|
1391
|
+
}
|
|
1392
|
+
if (got.threw !== void 0) return { holds: false, threw: got.threw };
|
|
1393
|
+
let value;
|
|
1394
|
+
let hasValue = false;
|
|
1395
|
+
if (got.json !== void 0) {
|
|
1396
|
+
try {
|
|
1397
|
+
value = JSON.parse(got.json);
|
|
1398
|
+
hasValue = true;
|
|
1399
|
+
} catch {
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
const holds = c.equals !== void 0 ? hasValue && sameJson(value, c.equals) : got.truthy === true;
|
|
1403
|
+
return {
|
|
1404
|
+
holds,
|
|
1405
|
+
...hasValue ? { value } : {},
|
|
1406
|
+
said: clip(got.said ?? "undefined", JS_SAID_MAX)
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
var clip = (s, max) => s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
1410
|
+
function keptValue(value) {
|
|
1411
|
+
const json = JSON.stringify(value);
|
|
1412
|
+
if (json === void 0) return void 0;
|
|
1413
|
+
return json.length <= JS_VALUE_MAX ? value : clip(json, JS_VALUE_MAX);
|
|
1414
|
+
}
|
|
1277
1415
|
async function readCondition(page, c) {
|
|
1278
1416
|
try {
|
|
1417
|
+
if (c.js !== void 0) return await readJs(page, { ...c, js: c.js });
|
|
1279
1418
|
if (c.text !== void 0)
|
|
1280
1419
|
return { holds: await page.evaluate(visibleTextScript(c.text)) };
|
|
1281
1420
|
if (c.gone !== void 0)
|
|
@@ -1347,6 +1486,8 @@ function sayDuration(ms) {
|
|
|
1347
1486
|
return rest ? `${m} min ${rest} s` : `${m} min`;
|
|
1348
1487
|
}
|
|
1349
1488
|
function sayCondition(c) {
|
|
1489
|
+
if (c.js !== void 0)
|
|
1490
|
+
return c.equals !== void 0 ? `${sayJs(c.js)} to equal ${clip(JSON.stringify(c.equals) ?? "undefined", JS_SAID_MAX)}` : `${sayJs(c.js)} to hold`;
|
|
1350
1491
|
if (c.text !== void 0) return `text "${c.text}" to show`;
|
|
1351
1492
|
if (c.gone !== void 0) return `'${c.gone}' to go`;
|
|
1352
1493
|
if (c.enabled === true) return `'${c.selector}' to be enabled`;
|
|
@@ -1355,6 +1496,12 @@ function sayCondition(c) {
|
|
|
1355
1496
|
}
|
|
1356
1497
|
function conditionMiss(c, last, ms) {
|
|
1357
1498
|
const t = sayDuration(ms);
|
|
1499
|
+
if (c.js !== void 0) {
|
|
1500
|
+
if (last.threw !== void 0)
|
|
1501
|
+
return `${sayJs(c.js)} threw for ${t} (${clip(last.threw, JS_SAID_MAX)})`;
|
|
1502
|
+
const was = last.said ?? "never answered";
|
|
1503
|
+
return c.equals !== void 0 ? `${sayJs(c.js)} was ${was}, not ${clip(JSON.stringify(c.equals) ?? "undefined", JS_SAID_MAX)}, for ${t}` : `${sayJs(c.js)} was ${was} for ${t}`;
|
|
1504
|
+
}
|
|
1358
1505
|
if (c.text !== void 0)
|
|
1359
1506
|
return `no visible text contained "${c.text}" for ${t}`;
|
|
1360
1507
|
if (c.gone !== void 0) return `'${c.gone}' was still there after ${t}`;
|
|
@@ -1364,6 +1511,12 @@ function conditionMiss(c, last, ms) {
|
|
|
1364
1511
|
return `'${c.selector}' stayed enabled for ${t}`;
|
|
1365
1512
|
return `'${c.selector}' never appeared in ${t}`;
|
|
1366
1513
|
}
|
|
1514
|
+
function sayJs(expr) {
|
|
1515
|
+
return `js \`${clip(expr.replace(/\s+/g, " ").trim(), 80)}\``;
|
|
1516
|
+
}
|
|
1517
|
+
function sayValue(value) {
|
|
1518
|
+
return clip(JSON.stringify(value) ?? "undefined", JS_SAID_MAX);
|
|
1519
|
+
}
|
|
1367
1520
|
|
|
1368
1521
|
// src/record/ready.ts
|
|
1369
1522
|
var READY_MODES = ["dom", "networkidle"];
|
|
@@ -1814,10 +1967,455 @@ async function appSettled(page, frames, opts) {
|
|
|
1814
1967
|
});
|
|
1815
1968
|
}
|
|
1816
1969
|
|
|
1970
|
+
// src/record/peers.ts
|
|
1971
|
+
var PEER_VERBS = [
|
|
1972
|
+
"wait",
|
|
1973
|
+
"move",
|
|
1974
|
+
"hover",
|
|
1975
|
+
"click",
|
|
1976
|
+
"type",
|
|
1977
|
+
"press",
|
|
1978
|
+
"scroll",
|
|
1979
|
+
"drag",
|
|
1980
|
+
"expect"
|
|
1981
|
+
];
|
|
1982
|
+
var MAX_PEERS = 4;
|
|
1983
|
+
var MAX_PEER_STEPS = 200;
|
|
1984
|
+
var TAKE_ONLY = {
|
|
1985
|
+
guard: "a guard is pressed only on the filmed page, under --confirm",
|
|
1986
|
+
read: "read keeps words from the filmed page",
|
|
1987
|
+
caption: "a caption is a beat of the film, which is the take\u2019s",
|
|
1988
|
+
settle: "settle reads the filmed page\u2019s frames",
|
|
1989
|
+
zoom: "a zoom mark picks a moment on the cursor track, and a peer is never on it"
|
|
1990
|
+
};
|
|
1991
|
+
var PEER_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
1992
|
+
function validatePeers(value, takeSteps, validate) {
|
|
1993
|
+
if (!Array.isArray(value))
|
|
1994
|
+
return [
|
|
1995
|
+
"peers must be an array of { id, url?, viewport?, storageState?, steps }"
|
|
1996
|
+
];
|
|
1997
|
+
const errors = [];
|
|
1998
|
+
if (value.length > MAX_PEERS)
|
|
1999
|
+
errors.push(
|
|
2000
|
+
`peers names ${value.length} peers; a take holds at most ${MAX_PEERS} (each is a whole browser)`
|
|
2001
|
+
);
|
|
2002
|
+
const takeIds = new Set(
|
|
2003
|
+
(Array.isArray(takeSteps) ? takeSteps : []).map((s) => s?.id).filter((id) => typeof id === "string")
|
|
2004
|
+
);
|
|
2005
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2006
|
+
value.forEach((raw, i) => {
|
|
2007
|
+
const at = `peers[${i}]`;
|
|
2008
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
2009
|
+
errors.push(`${at}: must be an object`);
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
const p = raw;
|
|
2013
|
+
if (typeof p.id !== "string" || !PEER_ID.test(p.id))
|
|
2014
|
+
errors.push(
|
|
2015
|
+
`${at}: id must be a short word (letters, digits, - and _, at most 32), which lines and meta.peers name the peer by`
|
|
2016
|
+
);
|
|
2017
|
+
else if (seen.has(p.id)) errors.push(`${at}: duplicate peer id "${p.id}"`);
|
|
2018
|
+
else seen.add(p.id);
|
|
2019
|
+
if (p.url !== void 0) {
|
|
2020
|
+
if (typeof p.url !== "string") errors.push(`${at}: url must be a string`);
|
|
2021
|
+
else {
|
|
2022
|
+
const problem = urlSchemeProblem(p.url);
|
|
2023
|
+
if (problem) errors.push(`${at}: url ${problem}`);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
if (p.viewport !== void 0) {
|
|
2027
|
+
const v = p.viewport;
|
|
2028
|
+
if (typeof v !== "object" || v === null || typeof v.width !== "number" || typeof v.height !== "number")
|
|
2029
|
+
errors.push(`${at}: viewport must be { width, height }`);
|
|
2030
|
+
}
|
|
2031
|
+
if (p.storageState !== void 0 && (typeof p.storageState !== "string" || !p.storageState.trim()))
|
|
2032
|
+
errors.push(
|
|
2033
|
+
`${at}: storageState must name a Playwright storage state file (the second person's sign-in)`
|
|
2034
|
+
);
|
|
2035
|
+
for (const k of Object.keys(p))
|
|
2036
|
+
if (!["id", "url", "viewport", "storageState", "steps"].includes(k))
|
|
2037
|
+
errors.push(
|
|
2038
|
+
`${at}: a peer takes id, url, viewport, storageState and steps, not ${k}`
|
|
2039
|
+
);
|
|
2040
|
+
if (!Array.isArray(p.steps) || p.steps.length === 0) {
|
|
2041
|
+
errors.push(`${at}: steps must be a non-empty array`);
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
if (p.steps.length > MAX_PEER_STEPS)
|
|
2045
|
+
errors.push(
|
|
2046
|
+
`${at}: steps holds ${p.steps.length} steps; a peer takes at most ${MAX_PEER_STEPS}`
|
|
2047
|
+
);
|
|
2048
|
+
const steps = p.steps;
|
|
2049
|
+
steps.forEach((rawStep, j) => {
|
|
2050
|
+
const sat = `${at}.steps[${j}]`;
|
|
2051
|
+
if (typeof rawStep !== "object" || rawStep === null) return;
|
|
2052
|
+
const s = rawStep;
|
|
2053
|
+
if (typeof s.do === "string" && !PEER_VERBS.includes(s.do))
|
|
2054
|
+
errors.push(
|
|
2055
|
+
`${sat}: a peer step is one of ${PEER_VERBS.join(", ")}; ${/^[aeiou]/.test(s.do) ? "an" : "a"} ${s.do} belongs to the filmed page${s.do === "await" ? " (wait for what the peer did with the take\u2019s own await or expect)" : ""}`
|
|
2056
|
+
);
|
|
2057
|
+
for (const [k, why] of Object.entries(TAKE_ONLY))
|
|
2058
|
+
if (s[k] !== void 0)
|
|
2059
|
+
errors.push(`${sat}: no ${k} on a peer step: ${why}`);
|
|
2060
|
+
if (s.do === "drag" && typeof s.path === "string")
|
|
2061
|
+
errors.push(
|
|
2062
|
+
`${sat}: a peer draws its path inline (an array of points), never from a file`
|
|
2063
|
+
);
|
|
2064
|
+
if (s.with !== void 0 && s.after !== void 0)
|
|
2065
|
+
errors.push(
|
|
2066
|
+
`${sat}: with or after, not both (with starts as the take's step starts, after once it ends)`
|
|
2067
|
+
);
|
|
2068
|
+
for (const k of ["with", "after"]) {
|
|
2069
|
+
const v = s[k];
|
|
2070
|
+
if (v === void 0) continue;
|
|
2071
|
+
if (typeof v !== "string" || !v.trim())
|
|
2072
|
+
errors.push(`${sat}: ${k} must name a take step by its id`);
|
|
2073
|
+
else if (!takeIds.has(v))
|
|
2074
|
+
errors.push(
|
|
2075
|
+
`${sat}: ${k} "${v}" names no take step (the take's steps with ids: ${takeIds.size ? [...takeIds].join(", ") : "none"}); give the take step an id`
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
for (const e of validate(steps))
|
|
2080
|
+
errors.push(e.replace(/^steps\[/, `${at}.steps[`));
|
|
2081
|
+
});
|
|
2082
|
+
return errors;
|
|
2083
|
+
}
|
|
2084
|
+
function peersHostedRefusal(peers) {
|
|
2085
|
+
if (!Array.isArray(peers) || !peers.length) return null;
|
|
2086
|
+
return "the script has peers (more browsers on the page, off camera), and a hosted take runs one browser: record it on this machine";
|
|
2087
|
+
}
|
|
2088
|
+
var StepSignals = class {
|
|
2089
|
+
started = /* @__PURE__ */ new Map();
|
|
2090
|
+
ended = /* @__PURE__ */ new Map();
|
|
2091
|
+
began = deferred();
|
|
2092
|
+
closed = false;
|
|
2093
|
+
constructor(ids) {
|
|
2094
|
+
for (const id of ids) {
|
|
2095
|
+
this.started.set(id, deferred());
|
|
2096
|
+
this.ended.set(id, deferred());
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
begin() {
|
|
2100
|
+
this.began.resolve(true);
|
|
2101
|
+
}
|
|
2102
|
+
start(id) {
|
|
2103
|
+
if (id) this.started.get(id)?.resolve(true);
|
|
2104
|
+
}
|
|
2105
|
+
end(id) {
|
|
2106
|
+
if (id) this.ended.get(id)?.resolve(true);
|
|
2107
|
+
}
|
|
2108
|
+
close() {
|
|
2109
|
+
this.closed = true;
|
|
2110
|
+
this.began.resolve(false);
|
|
2111
|
+
for (const d of [...this.started.values(), ...this.ended.values()])
|
|
2112
|
+
d.resolve(false);
|
|
2113
|
+
}
|
|
2114
|
+
get isClosed() {
|
|
2115
|
+
return this.closed;
|
|
2116
|
+
}
|
|
2117
|
+
/** When a step with this trigger may start: true, or false for never. */
|
|
2118
|
+
when(trigger) {
|
|
2119
|
+
const d = trigger.with ? this.started.get(trigger.with) : trigger.after ? this.ended.get(trigger.after) : this.began;
|
|
2120
|
+
if (!d) return { ready: Promise.resolve(false), already: false };
|
|
2121
|
+
return { ready: d.promise, already: d.settled };
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
function deferred() {
|
|
2125
|
+
let resolve = () => {
|
|
2126
|
+
};
|
|
2127
|
+
const d = {
|
|
2128
|
+
promise: new Promise((r) => {
|
|
2129
|
+
resolve = r;
|
|
2130
|
+
}),
|
|
2131
|
+
resolve: (v) => {
|
|
2132
|
+
if (d.settled) return;
|
|
2133
|
+
d.settled = true;
|
|
2134
|
+
resolve(v);
|
|
2135
|
+
},
|
|
2136
|
+
settled: false
|
|
2137
|
+
};
|
|
2138
|
+
return d;
|
|
2139
|
+
}
|
|
2140
|
+
var PEER_VALUE_MAX = 2048;
|
|
2141
|
+
function peerValue(last) {
|
|
2142
|
+
if (!("value" in last)) return void 0;
|
|
2143
|
+
const value = last.value;
|
|
2144
|
+
const json = JSON.stringify(value);
|
|
2145
|
+
if (json === void 0) return void 0;
|
|
2146
|
+
return json.length <= PEER_VALUE_MAX ? value : `${json.slice(0, PEER_VALUE_MAX - 1)}\u2026`;
|
|
2147
|
+
}
|
|
2148
|
+
var EXPECT_TIMEOUT_S = 5;
|
|
2149
|
+
var EXPECT_EVERY_MS = 150;
|
|
2150
|
+
async function runPeer(peer, rawPage, signals, opts) {
|
|
2151
|
+
const bound = new StepBound();
|
|
2152
|
+
const page = bounded(rawPage, bound);
|
|
2153
|
+
const { clock, log } = opts;
|
|
2154
|
+
const run = { id: peer.id, steps: [], notRun: [] };
|
|
2155
|
+
const cur = { x: 24, y: 24 };
|
|
2156
|
+
const moveTo = async (tx, ty) => {
|
|
2157
|
+
const from = { ...cur };
|
|
2158
|
+
const dist = Math.hypot(tx - from.x, ty - from.y);
|
|
2159
|
+
await clockMotion(
|
|
2160
|
+
opts.dry ? 0 : pointerTravelMs(dist),
|
|
2161
|
+
async (u) => {
|
|
2162
|
+
cur.x = from.x + (tx - from.x) * u;
|
|
2163
|
+
cur.y = from.y + (ty - from.y) * u;
|
|
2164
|
+
await page.mouse.move(cur.x, cur.y);
|
|
2165
|
+
},
|
|
2166
|
+
clock
|
|
2167
|
+
);
|
|
2168
|
+
};
|
|
2169
|
+
const spotOf = async (s) => {
|
|
2170
|
+
if (s.selector === void 0) return { x: s.x ?? cur.x, y: s.y ?? cur.y };
|
|
2171
|
+
const loc = page.locator(s.selector).first();
|
|
2172
|
+
try {
|
|
2173
|
+
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
2174
|
+
await loc.scrollIntoViewIfNeeded();
|
|
2175
|
+
const bb = await loc.boundingBox();
|
|
2176
|
+
return bb ? { x: bb.x + bb.width / 2, y: bb.y + bb.height / 2 } : null;
|
|
2177
|
+
} catch {
|
|
2178
|
+
return null;
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
const press = async (how) => {
|
|
2182
|
+
const held = [];
|
|
2183
|
+
try {
|
|
2184
|
+
for (const m of how.modifiers ?? []) {
|
|
2185
|
+
const key = resolveModifier(m);
|
|
2186
|
+
await page.keyboard.down(key);
|
|
2187
|
+
held.push(key);
|
|
2188
|
+
}
|
|
2189
|
+
await clock.sleep(PRESS_LEAD_MS);
|
|
2190
|
+
const count = how.count ?? 1;
|
|
2191
|
+
for (let n = 1; n <= count; n++) {
|
|
2192
|
+
if (n > 1) await clock.sleep(MULTI_CLICK_GAP_MS);
|
|
2193
|
+
const o = {
|
|
2194
|
+
...how.button && how.button !== "left" ? { button: how.button } : {},
|
|
2195
|
+
...n > 1 ? { clickCount: n } : {}
|
|
2196
|
+
};
|
|
2197
|
+
const has = Object.keys(o).length > 0;
|
|
2198
|
+
await (has ? page.mouse.down(o) : page.mouse.down());
|
|
2199
|
+
await clock.sleep(PRESS_HOLD_MS);
|
|
2200
|
+
await (has ? page.mouse.up(o) : page.mouse.up());
|
|
2201
|
+
}
|
|
2202
|
+
} finally {
|
|
2203
|
+
for (const key of held.reverse())
|
|
2204
|
+
await page.keyboard.up(key).catch(() => void 0);
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
for (const [i, step] of peer.steps.entries()) {
|
|
2208
|
+
const tag = `peer ${peer.id} #${i}${step.id ? ` ${step.id}` : ""}`;
|
|
2209
|
+
const trigger = signals.when(step);
|
|
2210
|
+
const go = await trigger.ready;
|
|
2211
|
+
if (!go) {
|
|
2212
|
+
run.notRun = peer.steps.slice(i).map((_, k) => i + k);
|
|
2213
|
+
log(
|
|
2214
|
+
` (${tag} ${step.do}: not run: ${step.with ? `the take's step ${step.with} never started` : step.after ? `the take's step ${step.after} never ended` : "the take ended first"})`
|
|
2215
|
+
);
|
|
2216
|
+
break;
|
|
2217
|
+
}
|
|
2218
|
+
const late = (step.with !== void 0 || step.after !== void 0) && trigger.already;
|
|
2219
|
+
const tStart = opts.footageMs();
|
|
2220
|
+
let selector;
|
|
2221
|
+
let reason = null;
|
|
2222
|
+
let value;
|
|
2223
|
+
bound.start(
|
|
2224
|
+
stepBoundMs(
|
|
2225
|
+
step,
|
|
2226
|
+
askedMs(step),
|
|
2227
|
+
{ expectS: EXPECT_TIMEOUT_S, awaitS: 0, downloadS: 0 }
|
|
2228
|
+
)
|
|
2229
|
+
);
|
|
2230
|
+
try {
|
|
2231
|
+
switch (step.do) {
|
|
2232
|
+
case "wait":
|
|
2233
|
+
await clock.sleep(step.ms);
|
|
2234
|
+
break;
|
|
2235
|
+
case "move":
|
|
2236
|
+
await moveTo(step.x, step.y);
|
|
2237
|
+
break;
|
|
2238
|
+
case "hover": {
|
|
2239
|
+
selector = step.selector;
|
|
2240
|
+
const spot = await spotOf(step);
|
|
2241
|
+
if (!spot) reason = `'${step.selector}' never appeared`;
|
|
2242
|
+
else {
|
|
2243
|
+
await moveTo(spot.x, spot.y);
|
|
2244
|
+
await clock.sleep(step.ms ?? 700);
|
|
2245
|
+
}
|
|
2246
|
+
break;
|
|
2247
|
+
}
|
|
2248
|
+
case "click": {
|
|
2249
|
+
selector = step.selector;
|
|
2250
|
+
const spot = await spotOf(step);
|
|
2251
|
+
if (!spot) {
|
|
2252
|
+
reason = `'${step.selector}' never appeared`;
|
|
2253
|
+
break;
|
|
2254
|
+
}
|
|
2255
|
+
await moveTo(spot.x, spot.y);
|
|
2256
|
+
await press(step);
|
|
2257
|
+
await clock.sleep(step.ms ?? SETTLE_MS.click);
|
|
2258
|
+
break;
|
|
2259
|
+
}
|
|
2260
|
+
case "type": {
|
|
2261
|
+
selector = step.selector;
|
|
2262
|
+
if (step.focus !== false) {
|
|
2263
|
+
const spot = await spotOf(step);
|
|
2264
|
+
if (!spot) {
|
|
2265
|
+
reason = `'${step.selector}' never appeared`;
|
|
2266
|
+
break;
|
|
2267
|
+
}
|
|
2268
|
+
await moveTo(spot.x, spot.y);
|
|
2269
|
+
await press({});
|
|
2270
|
+
}
|
|
2271
|
+
if (step.at === "end") await page.keyboard.press("End");
|
|
2272
|
+
else if (step.at === "start") await page.keyboard.press("Home");
|
|
2273
|
+
if (step.replace) await page.keyboard.press("ControlOrMeta+a");
|
|
2274
|
+
await clockTyping(
|
|
2275
|
+
[...step.text],
|
|
2276
|
+
opts.dry ? 0 : step.delayMs ?? 40,
|
|
2277
|
+
(ch) => page.keyboard.type(ch),
|
|
2278
|
+
clock
|
|
2279
|
+
);
|
|
2280
|
+
await clock.sleep(step.ms ?? SETTLE_MS.type);
|
|
2281
|
+
break;
|
|
2282
|
+
}
|
|
2283
|
+
case "press": {
|
|
2284
|
+
selector = step.selector;
|
|
2285
|
+
if (step.selector !== void 0) {
|
|
2286
|
+
const spot = await spotOf(step);
|
|
2287
|
+
if (!spot) {
|
|
2288
|
+
reason = `'${step.selector}' never appeared`;
|
|
2289
|
+
break;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
const keys = pressSequence(step);
|
|
2293
|
+
for (const [k, key] of keys.entries()) {
|
|
2294
|
+
if (k > 0) await clock.sleep(KEY_SEQUENCE_GAP_MS);
|
|
2295
|
+
await page.keyboard.press(resolveChord(key));
|
|
2296
|
+
}
|
|
2297
|
+
await clock.sleep(step.ms ?? SETTLE_MS.press);
|
|
2298
|
+
break;
|
|
2299
|
+
}
|
|
2300
|
+
case "scroll":
|
|
2301
|
+
await page.mouse.wheel(0, step.dy);
|
|
2302
|
+
await clock.sleep(step.ms ?? 200);
|
|
2303
|
+
break;
|
|
2304
|
+
case "drag": {
|
|
2305
|
+
selector = step.selector;
|
|
2306
|
+
const read = step.path !== void 0 ? readPath(step.path) : null;
|
|
2307
|
+
if (read && "problems" in read) {
|
|
2308
|
+
reason = `its path ${read.problems.join("; ")}`;
|
|
2309
|
+
break;
|
|
2310
|
+
}
|
|
2311
|
+
const held = [];
|
|
2312
|
+
try {
|
|
2313
|
+
if (read) {
|
|
2314
|
+
const sched = schedulePath(read, step.ms);
|
|
2315
|
+
await moveTo(sched[0].x, sched[0].y);
|
|
2316
|
+
for (const m of step.modifiers ?? []) {
|
|
2317
|
+
const key = resolveModifier(m);
|
|
2318
|
+
await page.keyboard.down(key);
|
|
2319
|
+
held.push(key);
|
|
2320
|
+
}
|
|
2321
|
+
await page.mouse.down();
|
|
2322
|
+
const t0 = clock.now();
|
|
2323
|
+
for (const p of sched.slice(1)) {
|
|
2324
|
+
const wait = t0 + (opts.dry ? 0 : p.at) - clock.now();
|
|
2325
|
+
if (wait > 0) await clock.sleep(wait);
|
|
2326
|
+
cur.x = p.x;
|
|
2327
|
+
cur.y = p.y;
|
|
2328
|
+
await page.mouse.move(p.x, p.y);
|
|
2329
|
+
}
|
|
2330
|
+
} else {
|
|
2331
|
+
const start = await spotOf(step);
|
|
2332
|
+
if (!start) {
|
|
2333
|
+
reason = `'${step.selector}' never appeared`;
|
|
2334
|
+
break;
|
|
2335
|
+
}
|
|
2336
|
+
await moveTo(start.x, start.y);
|
|
2337
|
+
for (const m of step.modifiers ?? []) {
|
|
2338
|
+
const key = resolveModifier(m);
|
|
2339
|
+
await page.keyboard.down(key);
|
|
2340
|
+
held.push(key);
|
|
2341
|
+
}
|
|
2342
|
+
await page.mouse.down();
|
|
2343
|
+
const from = { ...cur };
|
|
2344
|
+
const tx = step.tx;
|
|
2345
|
+
const ty = step.ty;
|
|
2346
|
+
await clockMotion(
|
|
2347
|
+
opts.dry ? 0 : step.ms ?? 700,
|
|
2348
|
+
async (u) => {
|
|
2349
|
+
cur.x = from.x + (tx - from.x) * u;
|
|
2350
|
+
cur.y = from.y + (ty - from.y) * u;
|
|
2351
|
+
await page.mouse.move(cur.x, cur.y);
|
|
2352
|
+
},
|
|
2353
|
+
clock
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
await page.mouse.up();
|
|
2357
|
+
} finally {
|
|
2358
|
+
for (const key of held.reverse())
|
|
2359
|
+
await page.keyboard.up(key).catch(() => void 0);
|
|
2360
|
+
}
|
|
2361
|
+
await clock.sleep(DRAG_HOLD_MS);
|
|
2362
|
+
break;
|
|
2363
|
+
}
|
|
2364
|
+
case "expect": {
|
|
2365
|
+
const cond = step;
|
|
2366
|
+
selector = step.selector ?? step.gone;
|
|
2367
|
+
const timeoutMs = (step.timeout ?? EXPECT_TIMEOUT_S) * 1e3;
|
|
2368
|
+
const res = await pollCondition(page, cond, {
|
|
2369
|
+
timeoutMs,
|
|
2370
|
+
everyMs: EXPECT_EVERY_MS,
|
|
2371
|
+
sleep: clock.sleep
|
|
2372
|
+
});
|
|
2373
|
+
value = peerValue(res.last);
|
|
2374
|
+
if (!res.ok) reason = conditionMiss(cond, res.last, timeoutMs);
|
|
2375
|
+
else log(` ${tag} expect ${sayCondition(cond)}: holds`);
|
|
2376
|
+
break;
|
|
2377
|
+
}
|
|
2378
|
+
default:
|
|
2379
|
+
reason = `a peer does not ${step.do}`;
|
|
2380
|
+
}
|
|
2381
|
+
} catch (e) {
|
|
2382
|
+
reason = e instanceof Error ? e.message.split("\n")[0] : String(e);
|
|
2383
|
+
}
|
|
2384
|
+
const hang = bound.end();
|
|
2385
|
+
if (hang.hung)
|
|
2386
|
+
reason = `the page stopped answering: this ${step.do} ran past its bound`;
|
|
2387
|
+
if (reason) log(` (${tag} ${step.do}: ${reason})`);
|
|
2388
|
+
else if (step.do !== "expect")
|
|
2389
|
+
log(` ${tag} ${step.do}${selector ? ` ${selector}` : ""}`);
|
|
2390
|
+
run.steps.push({
|
|
2391
|
+
step: i,
|
|
2392
|
+
...step.id ? { id: step.id } : {},
|
|
2393
|
+
do: step.do,
|
|
2394
|
+
...step.with ? { with: step.with } : {},
|
|
2395
|
+
...step.after ? { after: step.after } : {},
|
|
2396
|
+
...selector ? { selector } : {},
|
|
2397
|
+
tStart: +(tStart / 1e3).toFixed(3),
|
|
2398
|
+
tEnd: +(opts.footageMs() / 1e3).toFixed(3),
|
|
2399
|
+
...reason ? { skipped: true, reason } : {},
|
|
2400
|
+
...late ? { late: true } : {},
|
|
2401
|
+
...value !== void 0 ? { value } : {}
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
return run;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
1817
2407
|
// src/record/actions.ts
|
|
1818
2408
|
var STEP_GUARDS = ["spend"];
|
|
2409
|
+
var ZOOM_MARK_VERBS = [
|
|
2410
|
+
"click",
|
|
2411
|
+
"drag",
|
|
2412
|
+
"type",
|
|
2413
|
+
"paste",
|
|
2414
|
+
"press",
|
|
2415
|
+
"hover"
|
|
2416
|
+
];
|
|
1819
2417
|
var GUARDABLE = /* @__PURE__ */ new Set(["click", "press", "type"]);
|
|
1820
|
-
var
|
|
2418
|
+
var EXPECT_TIMEOUT_S2 = 5;
|
|
1821
2419
|
var AWAIT_TIMEOUT_S = 600;
|
|
1822
2420
|
var AWAIT_POLL_S = 2;
|
|
1823
2421
|
var DOWNLOAD_TIMEOUT_S = 30;
|
|
@@ -1875,6 +2473,8 @@ function validateActions(value) {
|
|
|
1875
2473
|
}
|
|
1876
2474
|
}
|
|
1877
2475
|
if (obj.setup !== void 0) errors.push(...validateSetup(obj.setup));
|
|
2476
|
+
if (obj.teardown !== void 0)
|
|
2477
|
+
errors.push(...validateSetup(obj.teardown, "teardown"));
|
|
1878
2478
|
const ready = readyProblem(obj.ready);
|
|
1879
2479
|
if (ready) errors.push(ready);
|
|
1880
2480
|
if (obj.settle !== void 0 && typeof obj.settle !== "boolean")
|
|
@@ -1901,6 +2501,14 @@ function validateActions(value) {
|
|
|
1901
2501
|
errors.push("steps must be a non-empty array");
|
|
1902
2502
|
return errors;
|
|
1903
2503
|
}
|
|
2504
|
+
if (obj.peers !== void 0)
|
|
2505
|
+
errors.push(
|
|
2506
|
+
...validatePeers(
|
|
2507
|
+
obj.peers,
|
|
2508
|
+
obj.steps,
|
|
2509
|
+
(steps) => validateActions({ steps })
|
|
2510
|
+
)
|
|
2511
|
+
);
|
|
1904
2512
|
const seenIds = /* @__PURE__ */ new Set();
|
|
1905
2513
|
obj.steps.forEach((raw, i) => {
|
|
1906
2514
|
const at = `steps[${i}]`;
|
|
@@ -2020,6 +2628,14 @@ function validateActions(value) {
|
|
|
2020
2628
|
if (s.do === "move" && (typeof s.x !== "number" || typeof s.y !== "number")) {
|
|
2021
2629
|
errors.push(`${at}: move needs x and y (CSS px)`);
|
|
2022
2630
|
}
|
|
2631
|
+
if (s.zoom !== void 0) {
|
|
2632
|
+
if (typeof s.zoom !== "boolean")
|
|
2633
|
+
errors.push(`${at}: zoom must be true or false`);
|
|
2634
|
+
else if (!ZOOM_MARK_VERBS.includes(s.do))
|
|
2635
|
+
errors.push(
|
|
2636
|
+
`${at}: zoom marks a gesture (${ZOOM_MARK_VERBS.join(", ")}); a ${s.do} shows nothing to zoom on`
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2023
2639
|
if (s.guard !== void 0) {
|
|
2024
2640
|
if (!STEP_GUARDS.includes(s.guard))
|
|
2025
2641
|
errors.push(
|
|
@@ -2035,16 +2651,17 @@ function validateActions(value) {
|
|
|
2035
2651
|
);
|
|
2036
2652
|
}
|
|
2037
2653
|
if (s.do === "expect") {
|
|
2038
|
-
const given = ["selector", "text", "gone"].filter(
|
|
2654
|
+
const given = ["selector", "text", "gone", "js"].filter(
|
|
2039
2655
|
(k) => s[k] !== void 0
|
|
2040
2656
|
);
|
|
2041
2657
|
if (given.length !== 1)
|
|
2042
2658
|
errors.push(
|
|
2043
|
-
`${at}: expect needs exactly one of selector, text or
|
|
2659
|
+
`${at}: expect needs exactly one of selector, text, gone or js${given.length ? ` (it has ${given.join(" and ")})` : ""}`
|
|
2044
2660
|
);
|
|
2045
2661
|
for (const k of given)
|
|
2046
2662
|
if (!isFilled(s[k]))
|
|
2047
2663
|
errors.push(`${at}: expect ${k} must be a non-empty string`);
|
|
2664
|
+
errors.push(...equalsProblem(`${at}: expect`, s));
|
|
2048
2665
|
if (s.enabled !== void 0) {
|
|
2049
2666
|
if (typeof s.enabled !== "boolean")
|
|
2050
2667
|
errors.push(`${at}: expect enabled must be true or false`);
|
|
@@ -2059,28 +2676,29 @@ function validateActions(value) {
|
|
|
2059
2676
|
const u = s.until;
|
|
2060
2677
|
if (typeof u !== "object" || u === null || Array.isArray(u)) {
|
|
2061
2678
|
errors.push(
|
|
2062
|
-
`${at}: await needs until, one of { "selector" }, { "text" } or { "
|
|
2679
|
+
`${at}: await needs until, one of { "selector" }, { "text" }, { "gone" } or { "js" }`
|
|
2063
2680
|
);
|
|
2064
2681
|
} else {
|
|
2065
2682
|
const until = u;
|
|
2066
2683
|
const keys = Object.keys(until);
|
|
2067
2684
|
const known = keys.filter(
|
|
2068
|
-
(k) => k === "selector" || k === "text" || k === "gone"
|
|
2685
|
+
(k) => k === "selector" || k === "text" || k === "gone" || k === "js"
|
|
2069
2686
|
);
|
|
2070
|
-
const unknown = keys.filter((k) => !known.includes(k));
|
|
2687
|
+
const unknown = keys.filter((k) => !known.includes(k) && k !== "equals");
|
|
2071
2688
|
if (unknown.length)
|
|
2072
2689
|
errors.push(
|
|
2073
|
-
`${at}: await until takes selector, text or
|
|
2690
|
+
`${at}: await until takes selector, text, gone or js (with equals), not ${unknown.join(", ")}`
|
|
2074
2691
|
);
|
|
2075
2692
|
if (known.length !== 1)
|
|
2076
2693
|
errors.push(
|
|
2077
|
-
`${at}: await until needs exactly one of selector, text or
|
|
2694
|
+
`${at}: await until needs exactly one of selector, text, gone or js${known.length ? ` (it has ${known.join(" and ")})` : ""}`
|
|
2078
2695
|
);
|
|
2079
2696
|
for (const k of known) {
|
|
2080
2697
|
const v = until[k];
|
|
2081
2698
|
if (typeof v !== "string" || !v.trim())
|
|
2082
2699
|
errors.push(`${at}: await until.${k} must be a non-empty string`);
|
|
2083
2700
|
}
|
|
2701
|
+
errors.push(...equalsProblem(`${at}: await until`, until));
|
|
2084
2702
|
}
|
|
2085
2703
|
errors.push(...secondsProblem(at, "await timeout", s.timeout));
|
|
2086
2704
|
errors.push(...secondsProblem(at, "await poll", s.poll));
|
|
@@ -2151,6 +2769,29 @@ function modifiersProblem(at, verb, v) {
|
|
|
2151
2769
|
return [`${at}: ${verb} modifiers names a modifier twice`];
|
|
2152
2770
|
return [];
|
|
2153
2771
|
}
|
|
2772
|
+
function equalsProblem(at, s) {
|
|
2773
|
+
if (s.equals === void 0) return [];
|
|
2774
|
+
if (s.js === void 0)
|
|
2775
|
+
return [
|
|
2776
|
+
`${at} equals is the value a js expression must answer, so it needs js`
|
|
2777
|
+
];
|
|
2778
|
+
if (!isJsonValue(s.equals, 0))
|
|
2779
|
+
return [
|
|
2780
|
+
`${at} equals must be a JSON value (a number, a string, true, false, null, a list or an object of them)`
|
|
2781
|
+
];
|
|
2782
|
+
return [];
|
|
2783
|
+
}
|
|
2784
|
+
function isJsonValue(v, depth) {
|
|
2785
|
+
if (depth > 32) return false;
|
|
2786
|
+
if (v === null || typeof v === "string" || typeof v === "boolean") return true;
|
|
2787
|
+
if (typeof v === "number") return Number.isFinite(v);
|
|
2788
|
+
if (Array.isArray(v)) return v.every((x) => isJsonValue(x, depth + 1));
|
|
2789
|
+
if (typeof v === "object")
|
|
2790
|
+
return Object.values(v).every(
|
|
2791
|
+
(x) => isJsonValue(x, depth + 1)
|
|
2792
|
+
);
|
|
2793
|
+
return false;
|
|
2794
|
+
}
|
|
2154
2795
|
var isFilled = (v) => typeof v === "string" && v.trim().length > 0;
|
|
2155
2796
|
function secondsProblem(at, what, v) {
|
|
2156
2797
|
if (v === void 0) return [];
|
|
@@ -2203,6 +2844,8 @@ function guardVerdict(actions, confirm) {
|
|
|
2203
2844
|
}
|
|
2204
2845
|
function hostedRefusals(actions) {
|
|
2205
2846
|
const out = [];
|
|
2847
|
+
const peers = peersHostedRefusal(actions.peers);
|
|
2848
|
+
if (peers) out.push(peers);
|
|
2206
2849
|
for (const g of guardedSteps(actions))
|
|
2207
2850
|
out.push(
|
|
2208
2851
|
`steps[${g.step}] (${g.id}) is guarded (${g.guard}), and a hosted take has no way to confirm it: record it on this machine with --confirm ${g.id}`
|
|
@@ -2216,6 +2859,10 @@ function hostedRefusals(actions) {
|
|
|
2216
2859
|
out.push(
|
|
2217
2860
|
`steps[${i}] is a paste, and a hosted browser has no clipboard to paste from: record it on this machine`
|
|
2218
2861
|
);
|
|
2862
|
+
if (s.do === "expect" && s.js !== void 0 || s.do === "await" && s.until.js !== void 0)
|
|
2863
|
+
out.push(
|
|
2864
|
+
`steps[${i}] ${s.do === "expect" ? "checks" : "waits on"} a js expression, which runs as code in the page, and a hosted browser runs no script it was handed: record it on this machine, or check what the page shows (selector, text or gone)`
|
|
2865
|
+
);
|
|
2219
2866
|
if (s.do === "drag" && typeof s.path === "string")
|
|
2220
2867
|
out.push(
|
|
2221
2868
|
`steps[${i}] draws a path from the file "${s.path}", and a hosted take carries the script alone: write the points inline as path`
|
|
@@ -2255,7 +2902,7 @@ async function loadScriptFiles(actions, source) {
|
|
|
2255
2902
|
} catch (e) {
|
|
2256
2903
|
const why = e instanceof Error ? e.message : String(e);
|
|
2257
2904
|
return {
|
|
2258
|
-
problem: f.as === "paste" ? `${at} pastes the file "${f.name}", which could not be read beside the actions file (${why}): a file paste needs its file beside the script, so put ${f.name} next to it (a take fetched
|
|
2905
|
+
problem: f.as === "paste" ? `${at} pastes the file "${f.name}", which could not be read beside the actions file (${why}): a file paste needs its file beside the script, so put ${f.name} next to it (a take fetched with its media brings its pasted files home beside its script; one pushed before they travelled with it does not). Nothing was recorded.` : `${at} names "${f.name}", which could not be read beside the actions file (${why}). Nothing was recorded.`
|
|
2259
2906
|
};
|
|
2260
2907
|
}
|
|
2261
2908
|
if (f.as === "path") {
|
|
@@ -2416,7 +3063,7 @@ function fetchInPageScript(url) {
|
|
|
2416
3063
|
}
|
|
2417
3064
|
|
|
2418
3065
|
// src/record/recorder.ts
|
|
2419
|
-
var
|
|
3066
|
+
var EXPECT_EVERY_MS2 = 100;
|
|
2420
3067
|
var SRC_WAIT_MS = 8e3;
|
|
2421
3068
|
var AWAIT_HEARTBEAT_MS = 3e4;
|
|
2422
3069
|
var MIN_SKIP_MS = 250;
|
|
@@ -2494,6 +3141,18 @@ function base64Bytes(data) {
|
|
|
2494
3141
|
return out;
|
|
2495
3142
|
}
|
|
2496
3143
|
async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
3144
|
+
const late = {};
|
|
3145
|
+
try {
|
|
3146
|
+
return await recordTakeSteps(browser, url, actions, sink, log, opts, late);
|
|
3147
|
+
} catch (e) {
|
|
3148
|
+
const teardown = late.teardown;
|
|
3149
|
+
late.teardown = void 0;
|
|
3150
|
+
if (teardown) await teardown().catch(() => {
|
|
3151
|
+
});
|
|
3152
|
+
throw e;
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
async function recordTakeSteps(browser, url, actions, sink, log, opts, late) {
|
|
2497
3156
|
const dry = opts.dryRun === true;
|
|
2498
3157
|
const sleep = dry ? (ms) => realSleep(Math.min(ms, DRY_PAUSE_MS)) : realSleep;
|
|
2499
3158
|
const maxSeconds = dry ? Infinity : opts.maxDurationSeconds ?? Infinity;
|
|
@@ -2506,9 +3165,27 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2506
3165
|
`steps[${download}] is a download, and this recorder has nowhere to keep a file. Nothing was recorded.`
|
|
2507
3166
|
);
|
|
2508
3167
|
}
|
|
3168
|
+
const teardownEnv = teardownEnvProblem(actions.teardown, opts.env ?? {});
|
|
3169
|
+
if (teardownEnv) throw new SetupEnvError(teardownEnv);
|
|
2509
3170
|
const read = await loadScriptFiles(actions, opts.files);
|
|
2510
3171
|
if ("problem" in read) throw new RecordRefusedError(read.problem);
|
|
2511
3172
|
const files = read;
|
|
3173
|
+
const peers = actions.peers ?? [];
|
|
3174
|
+
const peerStates = /* @__PURE__ */ new Map();
|
|
3175
|
+
for (const p of peers) {
|
|
3176
|
+
if (p.storageState === void 0) continue;
|
|
3177
|
+
if (!opts.peerStorageState)
|
|
3178
|
+
throw new RecordRefusedError(
|
|
3179
|
+
`peer ${p.id} signs in from the storage state "${p.storageState}", and this recorder has nowhere to read one from. Nothing was recorded.`
|
|
3180
|
+
);
|
|
3181
|
+
try {
|
|
3182
|
+
peerStates.set(p.id, await opts.peerStorageState(p.storageState));
|
|
3183
|
+
} catch (e) {
|
|
3184
|
+
throw new RecordRefusedError(
|
|
3185
|
+
`peer ${p.id}'s storage state "${p.storageState}" could not be read (${e instanceof Error ? e.message : String(e)}). Nothing was recorded.`
|
|
3186
|
+
);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
2512
3189
|
const vw = actions.viewport?.width ?? 1280;
|
|
2513
3190
|
const vh = actions.viewport?.height ?? 720;
|
|
2514
3191
|
const context = opts.context ?? await browser.newContext({
|
|
@@ -2533,8 +3210,9 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2533
3210
|
noticeLog.add(step, scan?.notices);
|
|
2534
3211
|
};
|
|
2535
3212
|
const pageErrors = new PageErrorLog(opts.verbose === true);
|
|
3213
|
+
let tearingDown = false;
|
|
2536
3214
|
page.on("console", (m) => {
|
|
2537
|
-
if (m.type() !== "error") return;
|
|
3215
|
+
if (m.type() !== "error" || tearingDown) return;
|
|
2538
3216
|
const line = pageErrors.add(m.text());
|
|
2539
3217
|
if (line) log(line);
|
|
2540
3218
|
});
|
|
@@ -2565,6 +3243,49 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2565
3243
|
log(`goto ${url} (after setup)`);
|
|
2566
3244
|
response = (await open(url)).response;
|
|
2567
3245
|
}
|
|
3246
|
+
const teardownSteps = actions.teardown ?? [];
|
|
3247
|
+
let teardown = null;
|
|
3248
|
+
let teardownRan = false;
|
|
3249
|
+
const runTeardown = async () => {
|
|
3250
|
+
if (!teardownSteps.length || teardownRan) return teardown;
|
|
3251
|
+
teardownRan = true;
|
|
3252
|
+
tearingDown = true;
|
|
3253
|
+
log(`teardown: ${teardownSteps.length} step(s), off camera`);
|
|
3254
|
+
try {
|
|
3255
|
+
const result = await runSetup(page, teardownSteps, {
|
|
3256
|
+
phase: "teardown",
|
|
3257
|
+
sleep: realSleep,
|
|
3258
|
+
env: opts.env ?? {},
|
|
3259
|
+
goto: (target) => open(target),
|
|
3260
|
+
boundMs: TEARDOWN_BOUND_MS
|
|
3261
|
+
});
|
|
3262
|
+
for (const line of result.lines) log(` ${line}`);
|
|
3263
|
+
teardown = {
|
|
3264
|
+
ran: result.ran,
|
|
3265
|
+
of: teardownSteps.length,
|
|
3266
|
+
...result.failed ? {
|
|
3267
|
+
failed: {
|
|
3268
|
+
step: result.failed.step,
|
|
3269
|
+
do: result.failed.do,
|
|
3270
|
+
...result.failed.selector !== void 0 ? { selector: result.failed.selector } : {},
|
|
3271
|
+
error: result.failed.error ?? "the step failed"
|
|
3272
|
+
}
|
|
3273
|
+
} : {}
|
|
3274
|
+
};
|
|
3275
|
+
} catch (e) {
|
|
3276
|
+
teardown = {
|
|
3277
|
+
ran: 0,
|
|
3278
|
+
of: teardownSteps.length,
|
|
3279
|
+
failed: {
|
|
3280
|
+
step: 0,
|
|
3281
|
+
do: teardownSteps[0].do,
|
|
3282
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3283
|
+
}
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
if (teardown.failed) log(` ${teardownLine(teardown)}`);
|
|
3287
|
+
return teardown;
|
|
3288
|
+
};
|
|
2568
3289
|
let wall = null;
|
|
2569
3290
|
if (opts.onArrival) {
|
|
2570
3291
|
const seen = await page.evaluate(WALL_PROBE).catch(() => null);
|
|
@@ -2587,6 +3308,59 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2587
3308
|
await page.evaluate(MEDIA_DRAIN_PROBE).catch(() => null);
|
|
2588
3309
|
if (actions.steps.some((s) => settleOn(s, actions.settle)))
|
|
2589
3310
|
await page.evaluate(CHANGE_AGE_PROBE).catch(() => void 0);
|
|
3311
|
+
const peerPages = [];
|
|
3312
|
+
const closePeers = async () => {
|
|
3313
|
+
const bound = new StepBound();
|
|
3314
|
+
bound.start(CLOSE_BOUND_MS);
|
|
3315
|
+
await Promise.all(
|
|
3316
|
+
peerPages.map((p) => bound.race(p.context.close().catch(() => {
|
|
3317
|
+
})))
|
|
3318
|
+
);
|
|
3319
|
+
bound.end();
|
|
3320
|
+
};
|
|
3321
|
+
for (const peer of peers) {
|
|
3322
|
+
const peerUrl = peer.url ?? url;
|
|
3323
|
+
const pctx = await browser.newContext({
|
|
3324
|
+
viewport: peer.viewport ?? { width: vw, height: vh },
|
|
3325
|
+
deviceScaleFactor: 1,
|
|
3326
|
+
...peerStates.has(peer.id) ? { storageState: peerStates.get(peer.id) } : {}
|
|
3327
|
+
});
|
|
3328
|
+
if (opts.headers && Object.keys(opts.headers).length)
|
|
3329
|
+
await applyHeaders(pctx, opts.headers, peerUrl, log);
|
|
3330
|
+
const ppage = await pctx.newPage();
|
|
3331
|
+
peerPages.push({ peer, context: pctx, page: ppage });
|
|
3332
|
+
const peerErrors = new PageErrorLog(opts.verbose === true);
|
|
3333
|
+
ppage.on("console", (m) => {
|
|
3334
|
+
if (m.type() !== "error") return;
|
|
3335
|
+
const line = peerErrors.add(m.text());
|
|
3336
|
+
if (line) log(` peer ${peer.id}: ${line.trim()}`);
|
|
3337
|
+
});
|
|
3338
|
+
log(`peer ${peer.id}: goto ${peerUrl} (off camera)`);
|
|
3339
|
+
const popened = await untilReady(
|
|
3340
|
+
ppage,
|
|
3341
|
+
readyMode,
|
|
3342
|
+
(o) => ppage.goto(peerUrl, o),
|
|
3343
|
+
{ sleep: realSleep }
|
|
3344
|
+
);
|
|
3345
|
+
const seen = await ppage.evaluate(WALL_PROBE).catch(() => null);
|
|
3346
|
+
const peerWall = wallVerdict({
|
|
3347
|
+
askedUrl: peerUrl,
|
|
3348
|
+
landedUrl: ppage.url(),
|
|
3349
|
+
...popened.response ? { status: popened.response.status() } : {},
|
|
3350
|
+
passwordFields: seen?.passwordFields ?? 0,
|
|
3351
|
+
newPasswordField: seen?.newPasswordField ?? false,
|
|
3352
|
+
oneTimeCodeField: seen?.oneTimeCodeField ?? false
|
|
3353
|
+
});
|
|
3354
|
+
if (peerWall?.level === "hard") {
|
|
3355
|
+
await closePeers();
|
|
3356
|
+
await context.close().catch(() => {
|
|
3357
|
+
});
|
|
3358
|
+
throw new RecordRefusedError(
|
|
3359
|
+
`peer ${peer.id} landed on ${peerWall.landed}, not ${peerWall.asked} (${peerWall.kind}): a peer acts as someone signed in, so give it a storageState of the second person's sign-in (beside actions.json). Nothing was recorded.`
|
|
3360
|
+
);
|
|
3361
|
+
}
|
|
3362
|
+
log(` peer ${peer.id}: ${readyLine(popened.ready)}`);
|
|
3363
|
+
}
|
|
2590
3364
|
const cdp = await context.newCDPSession(rawPage);
|
|
2591
3365
|
if (!dry) await fitSurfaceToViewport(cdp, vw, vh);
|
|
2592
3366
|
const frames = [];
|
|
@@ -2805,6 +3579,30 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2805
3579
|
let stopped = null;
|
|
2806
3580
|
let unpressedGuard = null;
|
|
2807
3581
|
let capped = false;
|
|
3582
|
+
const signals = new StepSignals(
|
|
3583
|
+
actions.steps.map((s) => s.id).filter((id) => !!id)
|
|
3584
|
+
);
|
|
3585
|
+
const lanes = peerPages.map(
|
|
3586
|
+
({ peer, page: ppage }) => runPeer(peer, ppage, signals, {
|
|
3587
|
+
clock: { now: () => Date.now(), sleep },
|
|
3588
|
+
footageMs: () => now(),
|
|
3589
|
+
log,
|
|
3590
|
+
dry
|
|
3591
|
+
}).catch((e) => {
|
|
3592
|
+
log(
|
|
3593
|
+
` (peer ${peer.id}: stopped: ${e instanceof Error ? e.message : String(e)})`
|
|
3594
|
+
);
|
|
3595
|
+
return { id: peer.id, steps: [], notRun: [] };
|
|
3596
|
+
})
|
|
3597
|
+
);
|
|
3598
|
+
signals.begin();
|
|
3599
|
+
late.teardown = async () => {
|
|
3600
|
+
if (!dry) await cdp.send("Page.stopScreencast").catch(() => {
|
|
3601
|
+
});
|
|
3602
|
+
pageBound.end();
|
|
3603
|
+
const report = await runTeardown();
|
|
3604
|
+
if (report && !report.failed) log(` ${teardownLine(report)}`);
|
|
3605
|
+
};
|
|
2808
3606
|
for (const [stepIdx, step] of actions.steps.entries()) {
|
|
2809
3607
|
if (capReached(now(), maxSeconds)) {
|
|
2810
3608
|
capped = true;
|
|
@@ -2844,6 +3642,12 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2844
3642
|
if (reason) extra.reason = reason;
|
|
2845
3643
|
};
|
|
2846
3644
|
if (step.guard) extra.guard = step.guard;
|
|
3645
|
+
if (typeof step.zoom === "boolean") extra.zoom = step.zoom;
|
|
3646
|
+
const keepValue = (last) => {
|
|
3647
|
+
if (!("value" in last)) return;
|
|
3648
|
+
const kept = keptValue(last.value);
|
|
3649
|
+
if (kept !== void 0) extra.value = kept;
|
|
3650
|
+
};
|
|
2847
3651
|
const rehearseGuard = async (sel, point) => {
|
|
2848
3652
|
if (!step.guard || !dry) return false;
|
|
2849
3653
|
const rect = sel ? await boxOf(sel) : null;
|
|
@@ -2872,7 +3676,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2872
3676
|
step,
|
|
2873
3677
|
askedMs(step),
|
|
2874
3678
|
{
|
|
2875
|
-
expectS:
|
|
3679
|
+
expectS: EXPECT_TIMEOUT_S2,
|
|
2876
3680
|
awaitS: AWAIT_TIMEOUT_S,
|
|
2877
3681
|
downloadS: DOWNLOAD_TIMEOUT_S
|
|
2878
3682
|
}
|
|
@@ -2884,6 +3688,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2884
3688
|
...step.id ? { id: step.id } : {},
|
|
2885
3689
|
do: step.do
|
|
2886
3690
|
});
|
|
3691
|
+
signals.start(step.id);
|
|
2887
3692
|
const stepWall = Date.now();
|
|
2888
3693
|
pageBound.start(boundMs);
|
|
2889
3694
|
switch (step.do) {
|
|
@@ -3225,12 +4030,13 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3225
4030
|
case "expect": {
|
|
3226
4031
|
if (notWaited()) break;
|
|
3227
4032
|
const cond = step;
|
|
3228
|
-
const timeoutMs = (step.timeout ??
|
|
4033
|
+
const timeoutMs = (step.timeout ?? EXPECT_TIMEOUT_S2) * 1e3;
|
|
3229
4034
|
const res = await pollCondition(page, cond, {
|
|
3230
4035
|
timeoutMs,
|
|
3231
|
-
everyMs:
|
|
4036
|
+
everyMs: EXPECT_EVERY_MS2,
|
|
3232
4037
|
sleep: realSleep
|
|
3233
4038
|
});
|
|
4039
|
+
keepValue(res.last);
|
|
3234
4040
|
if (res.ok) log(`expect ${sayCondition(cond)}: holds`);
|
|
3235
4041
|
else {
|
|
3236
4042
|
const reason = conditionMiss(cond, res.last, timeoutMs);
|
|
@@ -3275,6 +4081,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3275
4081
|
clearInterval(beat);
|
|
3276
4082
|
}
|
|
3277
4083
|
const { out, at, ms } = waited;
|
|
4084
|
+
keepValue(out.last);
|
|
3278
4085
|
opts.onEvent?.({ ...awaitEvent, phase: "end", ms, held: out.ok });
|
|
3279
4086
|
extra.offCameraMs = ms;
|
|
3280
4087
|
if (!out.ok) extra.timedOut = true;
|
|
@@ -3459,6 +4266,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3459
4266
|
await drainSound(stepIdx);
|
|
3460
4267
|
pageBound.end();
|
|
3461
4268
|
const stepSpan = stepSpans[stepSpans.length - 1];
|
|
4269
|
+
signals.end(step.id);
|
|
3462
4270
|
opts.onEvent?.({
|
|
3463
4271
|
event: "step",
|
|
3464
4272
|
phase: "end",
|
|
@@ -3471,6 +4279,19 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3471
4279
|
});
|
|
3472
4280
|
if (stopped) break;
|
|
3473
4281
|
}
|
|
4282
|
+
signals.close();
|
|
4283
|
+
const peerRuns = await Promise.all(lanes);
|
|
4284
|
+
for (const pr of peerRuns)
|
|
4285
|
+
for (const ps of pr.steps)
|
|
4286
|
+
if (ps.skipped)
|
|
4287
|
+
skipped.push({
|
|
4288
|
+
step: ps.step,
|
|
4289
|
+
...ps.id ? { id: ps.id } : {},
|
|
4290
|
+
do: ps.do,
|
|
4291
|
+
selector: ps.selector ?? "",
|
|
4292
|
+
...ps.reason ? { reason: ps.reason } : {},
|
|
4293
|
+
peer: pr.id
|
|
4294
|
+
});
|
|
3474
4295
|
if (!capped && !stopped) await sleep(TRAILING_HOLD_MS);
|
|
3475
4296
|
if (capped || capReached(now(), maxSeconds)) {
|
|
3476
4297
|
capped = true;
|
|
@@ -3493,10 +4314,13 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3493
4314
|
const pageTitle = await page.title().catch(() => "") ?? "";
|
|
3494
4315
|
for (const line of pageErrors.repeats()) log(line);
|
|
3495
4316
|
pageBound.end();
|
|
4317
|
+
late.teardown = void 0;
|
|
4318
|
+
await runTeardown();
|
|
3496
4319
|
pageBound.start(CLOSE_BOUND_MS);
|
|
3497
4320
|
await pageBound.race(context.close().catch(() => {
|
|
3498
4321
|
}));
|
|
3499
4322
|
pageBound.end();
|
|
4323
|
+
await closePeers();
|
|
3500
4324
|
await sinkChain;
|
|
3501
4325
|
const meta = {
|
|
3502
4326
|
dpr: 1,
|
|
@@ -3523,7 +4347,14 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3523
4347
|
...masks.length ? { masks: exposureLog.masks(masks) } : {},
|
|
3524
4348
|
...skips.length ? { skips } : {},
|
|
3525
4349
|
...outputs.length ? { outputs } : {},
|
|
3526
|
-
...media.length ? { media } : {}
|
|
4350
|
+
...media.length ? { media } : {},
|
|
4351
|
+
...peerRuns.length ? {
|
|
4352
|
+
peers: peerRuns.map((pr) => ({
|
|
4353
|
+
id: pr.id,
|
|
4354
|
+
steps: pr.steps,
|
|
4355
|
+
...pr.notRun.length ? { notRun: pr.notRun } : {}
|
|
4356
|
+
}))
|
|
4357
|
+
} : {}
|
|
3527
4358
|
};
|
|
3528
4359
|
const freezes = [];
|
|
3529
4360
|
for (let i = 1; i < frames.length; i++) {
|
|
@@ -3570,7 +4401,9 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3570
4401
|
overruns,
|
|
3571
4402
|
// Self-contained: a path a file held is inline, so the take re-records
|
|
3572
4403
|
// from its own script with the file gone.
|
|
3573
|
-
actions: { ...inlineScript(actions, files), url }
|
|
4404
|
+
actions: { ...inlineScript(actions, files), url },
|
|
4405
|
+
peers: peerRuns,
|
|
4406
|
+
teardown
|
|
3574
4407
|
};
|
|
3575
4408
|
}
|
|
3576
4409
|
async function keepPageSound(heard, sink, fetchSource, log) {
|
|
@@ -3834,16 +4667,20 @@ export {
|
|
|
3834
4667
|
DOWNLOAD_TIMEOUT_S,
|
|
3835
4668
|
DRAG_HOLD_MS,
|
|
3836
4669
|
DRAG_MODIFIERS,
|
|
3837
|
-
EXPECT_TIMEOUT_S,
|
|
4670
|
+
EXPECT_TIMEOUT_S2 as EXPECT_TIMEOUT_S,
|
|
3838
4671
|
EXPOSURE_ADVICE,
|
|
3839
4672
|
EXPOSURE_MATCHERS_SRC,
|
|
3840
4673
|
EXPOSURE_PROBE,
|
|
3841
4674
|
ExposureLog,
|
|
3842
4675
|
IDLE_CALL_MS,
|
|
4676
|
+
JS_SAID_MAX,
|
|
4677
|
+
JS_VALUE_MAX,
|
|
3843
4678
|
KEY_ROUNDTRIP_MS,
|
|
3844
4679
|
KEY_SEQUENCE_GAP_MS,
|
|
3845
4680
|
MAX_KEYS,
|
|
3846
4681
|
MAX_PATH_POINTS,
|
|
4682
|
+
MAX_PEERS,
|
|
4683
|
+
MAX_PEER_STEPS,
|
|
3847
4684
|
MEDIA_DRAIN_PROBE,
|
|
3848
4685
|
MEDIA_WATCH_SCRIPT,
|
|
3849
4686
|
MIN_SKIP_MS,
|
|
@@ -3864,6 +4701,8 @@ export {
|
|
|
3864
4701
|
PASTE_TYPES,
|
|
3865
4702
|
PASTE_UNREAD_MS,
|
|
3866
4703
|
PATH_DEFAULT_MS,
|
|
4704
|
+
PEER_VALUE_MAX,
|
|
4705
|
+
PEER_VERBS,
|
|
3867
4706
|
POINTER_MAX_MS,
|
|
3868
4707
|
POINTER_MIN_MS,
|
|
3869
4708
|
POINTER_MS_PER_PX,
|
|
@@ -3890,11 +4729,14 @@ export {
|
|
|
3890
4729
|
SetupEnvError,
|
|
3891
4730
|
SetupError,
|
|
3892
4731
|
StepBound,
|
|
4732
|
+
StepSignals,
|
|
4733
|
+
TEARDOWN_BOUND_MS,
|
|
3893
4734
|
TRAILING_HOLD_MS,
|
|
3894
4735
|
TakeClock,
|
|
3895
4736
|
WAIT_MARGIN_MS,
|
|
3896
4737
|
WALL_PROBE,
|
|
3897
4738
|
WallError,
|
|
4739
|
+
ZOOM_MARK_VERBS,
|
|
3898
4740
|
appSettled,
|
|
3899
4741
|
askedMs,
|
|
3900
4742
|
base64Bytes,
|
|
@@ -3931,6 +4773,8 @@ export {
|
|
|
3931
4773
|
isKeyName,
|
|
3932
4774
|
isSignInPath,
|
|
3933
4775
|
jpegDims,
|
|
4776
|
+
jsConditionScript,
|
|
4777
|
+
keptValue,
|
|
3934
4778
|
keySequencePlanMs,
|
|
3935
4779
|
loadScriptFiles,
|
|
3936
4780
|
maskInitScript,
|
|
@@ -3947,6 +4791,8 @@ export {
|
|
|
3947
4791
|
parseHeaders,
|
|
3948
4792
|
pasteTypeOf,
|
|
3949
4793
|
pathDurationMs,
|
|
4794
|
+
peerValue,
|
|
4795
|
+
peersHostedRefusal,
|
|
3950
4796
|
plainName,
|
|
3951
4797
|
planFreshTake,
|
|
3952
4798
|
pointerTravelMs,
|
|
@@ -3966,10 +4812,15 @@ export {
|
|
|
3966
4812
|
resolveChord,
|
|
3967
4813
|
resolveModifier,
|
|
3968
4814
|
resolveSetupText,
|
|
4815
|
+
runPeer,
|
|
3969
4816
|
runSetup,
|
|
4817
|
+
sameJson,
|
|
3970
4818
|
sayCondition,
|
|
3971
4819
|
sayDuration,
|
|
4820
|
+
sayJs,
|
|
4821
|
+
sayValue,
|
|
3972
4822
|
schedulePath,
|
|
4823
|
+
scriptEnvNames,
|
|
3973
4824
|
scriptFiles,
|
|
3974
4825
|
scrollByScript,
|
|
3975
4826
|
scrollTravelMs,
|
|
@@ -3983,10 +4834,13 @@ export {
|
|
|
3983
4834
|
stageClipboardScript,
|
|
3984
4835
|
stepBoundMs,
|
|
3985
4836
|
syntheticPasteScript,
|
|
4837
|
+
teardownEnvProblem,
|
|
4838
|
+
teardownLine,
|
|
3986
4839
|
typeOfName,
|
|
3987
4840
|
untilReady,
|
|
3988
4841
|
urlSchemeProblem,
|
|
3989
4842
|
validateActions,
|
|
4843
|
+
validatePeers,
|
|
3990
4844
|
validateSetup,
|
|
3991
4845
|
visibleTextScript,
|
|
3992
4846
|
wallVerdict,
|