@vosjs/render-core 0.8.0 → 0.9.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 +322 -100
- package/dist/record/index.js +728 -16
- package/dist/record/index.js.map +1 -1
- package/package.json +2 -2
package/dist/record/index.js
CHANGED
|
@@ -1274,8 +1274,79 @@ async function fitSurfaceToViewport(cdp, width, height) {
|
|
|
1274
1274
|
function visibleTextScript(text) {
|
|
1275
1275
|
return `(() => { const b = document.body; return !!b && (b.innerText || '').includes(${JSON.stringify(text)}) })()`;
|
|
1276
1276
|
}
|
|
1277
|
+
var JS_SAID_MAX = 120;
|
|
1278
|
+
var JS_VALUE_MAX = 2048;
|
|
1279
|
+
function jsConditionScript(expr) {
|
|
1280
|
+
return `(async () => {
|
|
1281
|
+
let v;
|
|
1282
|
+
try { v = await (${expr}
|
|
1283
|
+
); } catch (e) { return JSON.stringify({ threw: String(e && e.message ? e.message : e) }) }
|
|
1284
|
+
const out = { truthy: !!v };
|
|
1285
|
+
try { const j = JSON.stringify(v); if (j !== undefined) out.json = j } catch (e) {}
|
|
1286
|
+
try { out.said = typeof v === 'string' ? JSON.stringify(v) : String(v) } catch (e) { out.said = typeof v }
|
|
1287
|
+
if (out.json !== undefined && typeof v !== 'string') out.said = out.json
|
|
1288
|
+
return JSON.stringify(out);
|
|
1289
|
+
})()`;
|
|
1290
|
+
}
|
|
1291
|
+
function sameJson(a, b) {
|
|
1292
|
+
if (a === b) return true;
|
|
1293
|
+
if (typeof a !== typeof b || a === null || b === null) return false;
|
|
1294
|
+
if (typeof a !== "object") return false;
|
|
1295
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
1296
|
+
if (Array.isArray(a)) {
|
|
1297
|
+
const bb = b;
|
|
1298
|
+
return a.length === bb.length && a.every((v, i) => sameJson(v, bb[i]));
|
|
1299
|
+
}
|
|
1300
|
+
const ao = a;
|
|
1301
|
+
const bo = b;
|
|
1302
|
+
const keys = Object.keys(ao);
|
|
1303
|
+
if (keys.length !== Object.keys(bo).length) return false;
|
|
1304
|
+
return keys.every(
|
|
1305
|
+
(k) => Object.prototype.hasOwnProperty.call(bo, k) && sameJson(ao[k], bo[k])
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
async function readJs(page, c) {
|
|
1309
|
+
let raw;
|
|
1310
|
+
try {
|
|
1311
|
+
raw = await page.evaluate(jsConditionScript(c.js));
|
|
1312
|
+
} catch (e) {
|
|
1313
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
1314
|
+
if (/SyntaxError/.test(why) || e instanceof Error && e.name === "SyntaxError")
|
|
1315
|
+
return { holds: false, threw: `it does not parse: ${why.split("\n")[0]}` };
|
|
1316
|
+
throw e;
|
|
1317
|
+
}
|
|
1318
|
+
let got;
|
|
1319
|
+
try {
|
|
1320
|
+
got = JSON.parse(raw);
|
|
1321
|
+
} catch {
|
|
1322
|
+
return { holds: false };
|
|
1323
|
+
}
|
|
1324
|
+
if (got.threw !== void 0) return { holds: false, threw: got.threw };
|
|
1325
|
+
let value;
|
|
1326
|
+
let hasValue = false;
|
|
1327
|
+
if (got.json !== void 0) {
|
|
1328
|
+
try {
|
|
1329
|
+
value = JSON.parse(got.json);
|
|
1330
|
+
hasValue = true;
|
|
1331
|
+
} catch {
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const holds = c.equals !== void 0 ? hasValue && sameJson(value, c.equals) : got.truthy === true;
|
|
1335
|
+
return {
|
|
1336
|
+
holds,
|
|
1337
|
+
...hasValue ? { value } : {},
|
|
1338
|
+
said: clip(got.said ?? "undefined", JS_SAID_MAX)
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
var clip = (s, max) => s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
1342
|
+
function keptValue(value) {
|
|
1343
|
+
const json = JSON.stringify(value);
|
|
1344
|
+
if (json === void 0) return void 0;
|
|
1345
|
+
return json.length <= JS_VALUE_MAX ? value : clip(json, JS_VALUE_MAX);
|
|
1346
|
+
}
|
|
1277
1347
|
async function readCondition(page, c) {
|
|
1278
1348
|
try {
|
|
1349
|
+
if (c.js !== void 0) return await readJs(page, { ...c, js: c.js });
|
|
1279
1350
|
if (c.text !== void 0)
|
|
1280
1351
|
return { holds: await page.evaluate(visibleTextScript(c.text)) };
|
|
1281
1352
|
if (c.gone !== void 0)
|
|
@@ -1347,6 +1418,8 @@ function sayDuration(ms) {
|
|
|
1347
1418
|
return rest ? `${m} min ${rest} s` : `${m} min`;
|
|
1348
1419
|
}
|
|
1349
1420
|
function sayCondition(c) {
|
|
1421
|
+
if (c.js !== void 0)
|
|
1422
|
+
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
1423
|
if (c.text !== void 0) return `text "${c.text}" to show`;
|
|
1351
1424
|
if (c.gone !== void 0) return `'${c.gone}' to go`;
|
|
1352
1425
|
if (c.enabled === true) return `'${c.selector}' to be enabled`;
|
|
@@ -1355,6 +1428,12 @@ function sayCondition(c) {
|
|
|
1355
1428
|
}
|
|
1356
1429
|
function conditionMiss(c, last, ms) {
|
|
1357
1430
|
const t = sayDuration(ms);
|
|
1431
|
+
if (c.js !== void 0) {
|
|
1432
|
+
if (last.threw !== void 0)
|
|
1433
|
+
return `${sayJs(c.js)} threw for ${t} (${clip(last.threw, JS_SAID_MAX)})`;
|
|
1434
|
+
const was = last.said ?? "never answered";
|
|
1435
|
+
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}`;
|
|
1436
|
+
}
|
|
1358
1437
|
if (c.text !== void 0)
|
|
1359
1438
|
return `no visible text contained "${c.text}" for ${t}`;
|
|
1360
1439
|
if (c.gone !== void 0) return `'${c.gone}' was still there after ${t}`;
|
|
@@ -1364,6 +1443,12 @@ function conditionMiss(c, last, ms) {
|
|
|
1364
1443
|
return `'${c.selector}' stayed enabled for ${t}`;
|
|
1365
1444
|
return `'${c.selector}' never appeared in ${t}`;
|
|
1366
1445
|
}
|
|
1446
|
+
function sayJs(expr) {
|
|
1447
|
+
return `js \`${clip(expr.replace(/\s+/g, " ").trim(), 80)}\``;
|
|
1448
|
+
}
|
|
1449
|
+
function sayValue(value) {
|
|
1450
|
+
return clip(JSON.stringify(value) ?? "undefined", JS_SAID_MAX);
|
|
1451
|
+
}
|
|
1367
1452
|
|
|
1368
1453
|
// src/record/ready.ts
|
|
1369
1454
|
var READY_MODES = ["dom", "networkidle"];
|
|
@@ -1814,10 +1899,455 @@ async function appSettled(page, frames, opts) {
|
|
|
1814
1899
|
});
|
|
1815
1900
|
}
|
|
1816
1901
|
|
|
1902
|
+
// src/record/peers.ts
|
|
1903
|
+
var PEER_VERBS = [
|
|
1904
|
+
"wait",
|
|
1905
|
+
"move",
|
|
1906
|
+
"hover",
|
|
1907
|
+
"click",
|
|
1908
|
+
"type",
|
|
1909
|
+
"press",
|
|
1910
|
+
"scroll",
|
|
1911
|
+
"drag",
|
|
1912
|
+
"expect"
|
|
1913
|
+
];
|
|
1914
|
+
var MAX_PEERS = 4;
|
|
1915
|
+
var MAX_PEER_STEPS = 200;
|
|
1916
|
+
var TAKE_ONLY = {
|
|
1917
|
+
guard: "a guard is pressed only on the filmed page, under --confirm",
|
|
1918
|
+
read: "read keeps words from the filmed page",
|
|
1919
|
+
caption: "a caption is a beat of the film, which is the take\u2019s",
|
|
1920
|
+
settle: "settle reads the filmed page\u2019s frames",
|
|
1921
|
+
zoom: "a zoom mark picks a moment on the cursor track, and a peer is never on it"
|
|
1922
|
+
};
|
|
1923
|
+
var PEER_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
1924
|
+
function validatePeers(value, takeSteps, validate) {
|
|
1925
|
+
if (!Array.isArray(value))
|
|
1926
|
+
return [
|
|
1927
|
+
"peers must be an array of { id, url?, viewport?, storageState?, steps }"
|
|
1928
|
+
];
|
|
1929
|
+
const errors = [];
|
|
1930
|
+
if (value.length > MAX_PEERS)
|
|
1931
|
+
errors.push(
|
|
1932
|
+
`peers names ${value.length} peers; a take holds at most ${MAX_PEERS} (each is a whole browser)`
|
|
1933
|
+
);
|
|
1934
|
+
const takeIds = new Set(
|
|
1935
|
+
(Array.isArray(takeSteps) ? takeSteps : []).map((s) => s?.id).filter((id) => typeof id === "string")
|
|
1936
|
+
);
|
|
1937
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1938
|
+
value.forEach((raw, i) => {
|
|
1939
|
+
const at = `peers[${i}]`;
|
|
1940
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
1941
|
+
errors.push(`${at}: must be an object`);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
const p = raw;
|
|
1945
|
+
if (typeof p.id !== "string" || !PEER_ID.test(p.id))
|
|
1946
|
+
errors.push(
|
|
1947
|
+
`${at}: id must be a short word (letters, digits, - and _, at most 32), which lines and meta.peers name the peer by`
|
|
1948
|
+
);
|
|
1949
|
+
else if (seen.has(p.id)) errors.push(`${at}: duplicate peer id "${p.id}"`);
|
|
1950
|
+
else seen.add(p.id);
|
|
1951
|
+
if (p.url !== void 0) {
|
|
1952
|
+
if (typeof p.url !== "string") errors.push(`${at}: url must be a string`);
|
|
1953
|
+
else {
|
|
1954
|
+
const problem = urlSchemeProblem(p.url);
|
|
1955
|
+
if (problem) errors.push(`${at}: url ${problem}`);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
if (p.viewport !== void 0) {
|
|
1959
|
+
const v = p.viewport;
|
|
1960
|
+
if (typeof v !== "object" || v === null || typeof v.width !== "number" || typeof v.height !== "number")
|
|
1961
|
+
errors.push(`${at}: viewport must be { width, height }`);
|
|
1962
|
+
}
|
|
1963
|
+
if (p.storageState !== void 0 && (typeof p.storageState !== "string" || !p.storageState.trim()))
|
|
1964
|
+
errors.push(
|
|
1965
|
+
`${at}: storageState must name a Playwright storage state file (the second person's sign-in)`
|
|
1966
|
+
);
|
|
1967
|
+
for (const k of Object.keys(p))
|
|
1968
|
+
if (!["id", "url", "viewport", "storageState", "steps"].includes(k))
|
|
1969
|
+
errors.push(
|
|
1970
|
+
`${at}: a peer takes id, url, viewport, storageState and steps, not ${k}`
|
|
1971
|
+
);
|
|
1972
|
+
if (!Array.isArray(p.steps) || p.steps.length === 0) {
|
|
1973
|
+
errors.push(`${at}: steps must be a non-empty array`);
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
if (p.steps.length > MAX_PEER_STEPS)
|
|
1977
|
+
errors.push(
|
|
1978
|
+
`${at}: steps holds ${p.steps.length} steps; a peer takes at most ${MAX_PEER_STEPS}`
|
|
1979
|
+
);
|
|
1980
|
+
const steps = p.steps;
|
|
1981
|
+
steps.forEach((rawStep, j) => {
|
|
1982
|
+
const sat = `${at}.steps[${j}]`;
|
|
1983
|
+
if (typeof rawStep !== "object" || rawStep === null) return;
|
|
1984
|
+
const s = rawStep;
|
|
1985
|
+
if (typeof s.do === "string" && !PEER_VERBS.includes(s.do))
|
|
1986
|
+
errors.push(
|
|
1987
|
+
`${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)" : ""}`
|
|
1988
|
+
);
|
|
1989
|
+
for (const [k, why] of Object.entries(TAKE_ONLY))
|
|
1990
|
+
if (s[k] !== void 0)
|
|
1991
|
+
errors.push(`${sat}: no ${k} on a peer step: ${why}`);
|
|
1992
|
+
if (s.do === "drag" && typeof s.path === "string")
|
|
1993
|
+
errors.push(
|
|
1994
|
+
`${sat}: a peer draws its path inline (an array of points), never from a file`
|
|
1995
|
+
);
|
|
1996
|
+
if (s.with !== void 0 && s.after !== void 0)
|
|
1997
|
+
errors.push(
|
|
1998
|
+
`${sat}: with or after, not both (with starts as the take's step starts, after once it ends)`
|
|
1999
|
+
);
|
|
2000
|
+
for (const k of ["with", "after"]) {
|
|
2001
|
+
const v = s[k];
|
|
2002
|
+
if (v === void 0) continue;
|
|
2003
|
+
if (typeof v !== "string" || !v.trim())
|
|
2004
|
+
errors.push(`${sat}: ${k} must name a take step by its id`);
|
|
2005
|
+
else if (!takeIds.has(v))
|
|
2006
|
+
errors.push(
|
|
2007
|
+
`${sat}: ${k} "${v}" names no take step (the take's steps with ids: ${takeIds.size ? [...takeIds].join(", ") : "none"}); give the take step an id`
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
});
|
|
2011
|
+
for (const e of validate(steps))
|
|
2012
|
+
errors.push(e.replace(/^steps\[/, `${at}.steps[`));
|
|
2013
|
+
});
|
|
2014
|
+
return errors;
|
|
2015
|
+
}
|
|
2016
|
+
function peersHostedRefusal(peers) {
|
|
2017
|
+
if (!Array.isArray(peers) || !peers.length) return null;
|
|
2018
|
+
return "the script has peers (more browsers on the page, off camera), and a hosted take runs one browser: record it on this machine";
|
|
2019
|
+
}
|
|
2020
|
+
var StepSignals = class {
|
|
2021
|
+
started = /* @__PURE__ */ new Map();
|
|
2022
|
+
ended = /* @__PURE__ */ new Map();
|
|
2023
|
+
began = deferred();
|
|
2024
|
+
closed = false;
|
|
2025
|
+
constructor(ids) {
|
|
2026
|
+
for (const id of ids) {
|
|
2027
|
+
this.started.set(id, deferred());
|
|
2028
|
+
this.ended.set(id, deferred());
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
begin() {
|
|
2032
|
+
this.began.resolve(true);
|
|
2033
|
+
}
|
|
2034
|
+
start(id) {
|
|
2035
|
+
if (id) this.started.get(id)?.resolve(true);
|
|
2036
|
+
}
|
|
2037
|
+
end(id) {
|
|
2038
|
+
if (id) this.ended.get(id)?.resolve(true);
|
|
2039
|
+
}
|
|
2040
|
+
close() {
|
|
2041
|
+
this.closed = true;
|
|
2042
|
+
this.began.resolve(false);
|
|
2043
|
+
for (const d of [...this.started.values(), ...this.ended.values()])
|
|
2044
|
+
d.resolve(false);
|
|
2045
|
+
}
|
|
2046
|
+
get isClosed() {
|
|
2047
|
+
return this.closed;
|
|
2048
|
+
}
|
|
2049
|
+
/** When a step with this trigger may start: true, or false for never. */
|
|
2050
|
+
when(trigger) {
|
|
2051
|
+
const d = trigger.with ? this.started.get(trigger.with) : trigger.after ? this.ended.get(trigger.after) : this.began;
|
|
2052
|
+
if (!d) return { ready: Promise.resolve(false), already: false };
|
|
2053
|
+
return { ready: d.promise, already: d.settled };
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
function deferred() {
|
|
2057
|
+
let resolve = () => {
|
|
2058
|
+
};
|
|
2059
|
+
const d = {
|
|
2060
|
+
promise: new Promise((r) => {
|
|
2061
|
+
resolve = r;
|
|
2062
|
+
}),
|
|
2063
|
+
resolve: (v) => {
|
|
2064
|
+
if (d.settled) return;
|
|
2065
|
+
d.settled = true;
|
|
2066
|
+
resolve(v);
|
|
2067
|
+
},
|
|
2068
|
+
settled: false
|
|
2069
|
+
};
|
|
2070
|
+
return d;
|
|
2071
|
+
}
|
|
2072
|
+
var PEER_VALUE_MAX = 2048;
|
|
2073
|
+
function peerValue(last) {
|
|
2074
|
+
if (!("value" in last)) return void 0;
|
|
2075
|
+
const value = last.value;
|
|
2076
|
+
const json = JSON.stringify(value);
|
|
2077
|
+
if (json === void 0) return void 0;
|
|
2078
|
+
return json.length <= PEER_VALUE_MAX ? value : `${json.slice(0, PEER_VALUE_MAX - 1)}\u2026`;
|
|
2079
|
+
}
|
|
2080
|
+
var EXPECT_TIMEOUT_S = 5;
|
|
2081
|
+
var EXPECT_EVERY_MS = 150;
|
|
2082
|
+
async function runPeer(peer, rawPage, signals, opts) {
|
|
2083
|
+
const bound = new StepBound();
|
|
2084
|
+
const page = bounded(rawPage, bound);
|
|
2085
|
+
const { clock, log } = opts;
|
|
2086
|
+
const run = { id: peer.id, steps: [], notRun: [] };
|
|
2087
|
+
const cur = { x: 24, y: 24 };
|
|
2088
|
+
const moveTo = async (tx, ty) => {
|
|
2089
|
+
const from = { ...cur };
|
|
2090
|
+
const dist = Math.hypot(tx - from.x, ty - from.y);
|
|
2091
|
+
await clockMotion(
|
|
2092
|
+
opts.dry ? 0 : pointerTravelMs(dist),
|
|
2093
|
+
async (u) => {
|
|
2094
|
+
cur.x = from.x + (tx - from.x) * u;
|
|
2095
|
+
cur.y = from.y + (ty - from.y) * u;
|
|
2096
|
+
await page.mouse.move(cur.x, cur.y);
|
|
2097
|
+
},
|
|
2098
|
+
clock
|
|
2099
|
+
);
|
|
2100
|
+
};
|
|
2101
|
+
const spotOf = async (s) => {
|
|
2102
|
+
if (s.selector === void 0) return { x: s.x ?? cur.x, y: s.y ?? cur.y };
|
|
2103
|
+
const loc = page.locator(s.selector).first();
|
|
2104
|
+
try {
|
|
2105
|
+
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
2106
|
+
await loc.scrollIntoViewIfNeeded();
|
|
2107
|
+
const bb = await loc.boundingBox();
|
|
2108
|
+
return bb ? { x: bb.x + bb.width / 2, y: bb.y + bb.height / 2 } : null;
|
|
2109
|
+
} catch {
|
|
2110
|
+
return null;
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
const press = async (how) => {
|
|
2114
|
+
const held = [];
|
|
2115
|
+
try {
|
|
2116
|
+
for (const m of how.modifiers ?? []) {
|
|
2117
|
+
const key = resolveModifier(m);
|
|
2118
|
+
await page.keyboard.down(key);
|
|
2119
|
+
held.push(key);
|
|
2120
|
+
}
|
|
2121
|
+
await clock.sleep(PRESS_LEAD_MS);
|
|
2122
|
+
const count = how.count ?? 1;
|
|
2123
|
+
for (let n = 1; n <= count; n++) {
|
|
2124
|
+
if (n > 1) await clock.sleep(MULTI_CLICK_GAP_MS);
|
|
2125
|
+
const o = {
|
|
2126
|
+
...how.button && how.button !== "left" ? { button: how.button } : {},
|
|
2127
|
+
...n > 1 ? { clickCount: n } : {}
|
|
2128
|
+
};
|
|
2129
|
+
const has = Object.keys(o).length > 0;
|
|
2130
|
+
await (has ? page.mouse.down(o) : page.mouse.down());
|
|
2131
|
+
await clock.sleep(PRESS_HOLD_MS);
|
|
2132
|
+
await (has ? page.mouse.up(o) : page.mouse.up());
|
|
2133
|
+
}
|
|
2134
|
+
} finally {
|
|
2135
|
+
for (const key of held.reverse())
|
|
2136
|
+
await page.keyboard.up(key).catch(() => void 0);
|
|
2137
|
+
}
|
|
2138
|
+
};
|
|
2139
|
+
for (const [i, step] of peer.steps.entries()) {
|
|
2140
|
+
const tag = `peer ${peer.id} #${i}${step.id ? ` ${step.id}` : ""}`;
|
|
2141
|
+
const trigger = signals.when(step);
|
|
2142
|
+
const go = await trigger.ready;
|
|
2143
|
+
if (!go) {
|
|
2144
|
+
run.notRun = peer.steps.slice(i).map((_, k) => i + k);
|
|
2145
|
+
log(
|
|
2146
|
+
` (${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"})`
|
|
2147
|
+
);
|
|
2148
|
+
break;
|
|
2149
|
+
}
|
|
2150
|
+
const late = (step.with !== void 0 || step.after !== void 0) && trigger.already;
|
|
2151
|
+
const tStart = opts.footageMs();
|
|
2152
|
+
let selector;
|
|
2153
|
+
let reason = null;
|
|
2154
|
+
let value;
|
|
2155
|
+
bound.start(
|
|
2156
|
+
stepBoundMs(
|
|
2157
|
+
step,
|
|
2158
|
+
askedMs(step),
|
|
2159
|
+
{ expectS: EXPECT_TIMEOUT_S, awaitS: 0, downloadS: 0 }
|
|
2160
|
+
)
|
|
2161
|
+
);
|
|
2162
|
+
try {
|
|
2163
|
+
switch (step.do) {
|
|
2164
|
+
case "wait":
|
|
2165
|
+
await clock.sleep(step.ms);
|
|
2166
|
+
break;
|
|
2167
|
+
case "move":
|
|
2168
|
+
await moveTo(step.x, step.y);
|
|
2169
|
+
break;
|
|
2170
|
+
case "hover": {
|
|
2171
|
+
selector = step.selector;
|
|
2172
|
+
const spot = await spotOf(step);
|
|
2173
|
+
if (!spot) reason = `'${step.selector}' never appeared`;
|
|
2174
|
+
else {
|
|
2175
|
+
await moveTo(spot.x, spot.y);
|
|
2176
|
+
await clock.sleep(step.ms ?? 700);
|
|
2177
|
+
}
|
|
2178
|
+
break;
|
|
2179
|
+
}
|
|
2180
|
+
case "click": {
|
|
2181
|
+
selector = step.selector;
|
|
2182
|
+
const spot = await spotOf(step);
|
|
2183
|
+
if (!spot) {
|
|
2184
|
+
reason = `'${step.selector}' never appeared`;
|
|
2185
|
+
break;
|
|
2186
|
+
}
|
|
2187
|
+
await moveTo(spot.x, spot.y);
|
|
2188
|
+
await press(step);
|
|
2189
|
+
await clock.sleep(step.ms ?? SETTLE_MS.click);
|
|
2190
|
+
break;
|
|
2191
|
+
}
|
|
2192
|
+
case "type": {
|
|
2193
|
+
selector = step.selector;
|
|
2194
|
+
if (step.focus !== false) {
|
|
2195
|
+
const spot = await spotOf(step);
|
|
2196
|
+
if (!spot) {
|
|
2197
|
+
reason = `'${step.selector}' never appeared`;
|
|
2198
|
+
break;
|
|
2199
|
+
}
|
|
2200
|
+
await moveTo(spot.x, spot.y);
|
|
2201
|
+
await press({});
|
|
2202
|
+
}
|
|
2203
|
+
if (step.at === "end") await page.keyboard.press("End");
|
|
2204
|
+
else if (step.at === "start") await page.keyboard.press("Home");
|
|
2205
|
+
if (step.replace) await page.keyboard.press("ControlOrMeta+a");
|
|
2206
|
+
await clockTyping(
|
|
2207
|
+
[...step.text],
|
|
2208
|
+
opts.dry ? 0 : step.delayMs ?? 40,
|
|
2209
|
+
(ch) => page.keyboard.type(ch),
|
|
2210
|
+
clock
|
|
2211
|
+
);
|
|
2212
|
+
await clock.sleep(step.ms ?? SETTLE_MS.type);
|
|
2213
|
+
break;
|
|
2214
|
+
}
|
|
2215
|
+
case "press": {
|
|
2216
|
+
selector = step.selector;
|
|
2217
|
+
if (step.selector !== void 0) {
|
|
2218
|
+
const spot = await spotOf(step);
|
|
2219
|
+
if (!spot) {
|
|
2220
|
+
reason = `'${step.selector}' never appeared`;
|
|
2221
|
+
break;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
const keys = pressSequence(step);
|
|
2225
|
+
for (const [k, key] of keys.entries()) {
|
|
2226
|
+
if (k > 0) await clock.sleep(KEY_SEQUENCE_GAP_MS);
|
|
2227
|
+
await page.keyboard.press(resolveChord(key));
|
|
2228
|
+
}
|
|
2229
|
+
await clock.sleep(step.ms ?? SETTLE_MS.press);
|
|
2230
|
+
break;
|
|
2231
|
+
}
|
|
2232
|
+
case "scroll":
|
|
2233
|
+
await page.mouse.wheel(0, step.dy);
|
|
2234
|
+
await clock.sleep(step.ms ?? 200);
|
|
2235
|
+
break;
|
|
2236
|
+
case "drag": {
|
|
2237
|
+
selector = step.selector;
|
|
2238
|
+
const read = step.path !== void 0 ? readPath(step.path) : null;
|
|
2239
|
+
if (read && "problems" in read) {
|
|
2240
|
+
reason = `its path ${read.problems.join("; ")}`;
|
|
2241
|
+
break;
|
|
2242
|
+
}
|
|
2243
|
+
const held = [];
|
|
2244
|
+
try {
|
|
2245
|
+
if (read) {
|
|
2246
|
+
const sched = schedulePath(read, step.ms);
|
|
2247
|
+
await moveTo(sched[0].x, sched[0].y);
|
|
2248
|
+
for (const m of step.modifiers ?? []) {
|
|
2249
|
+
const key = resolveModifier(m);
|
|
2250
|
+
await page.keyboard.down(key);
|
|
2251
|
+
held.push(key);
|
|
2252
|
+
}
|
|
2253
|
+
await page.mouse.down();
|
|
2254
|
+
const t0 = clock.now();
|
|
2255
|
+
for (const p of sched.slice(1)) {
|
|
2256
|
+
const wait = t0 + (opts.dry ? 0 : p.at) - clock.now();
|
|
2257
|
+
if (wait > 0) await clock.sleep(wait);
|
|
2258
|
+
cur.x = p.x;
|
|
2259
|
+
cur.y = p.y;
|
|
2260
|
+
await page.mouse.move(p.x, p.y);
|
|
2261
|
+
}
|
|
2262
|
+
} else {
|
|
2263
|
+
const start = await spotOf(step);
|
|
2264
|
+
if (!start) {
|
|
2265
|
+
reason = `'${step.selector}' never appeared`;
|
|
2266
|
+
break;
|
|
2267
|
+
}
|
|
2268
|
+
await moveTo(start.x, start.y);
|
|
2269
|
+
for (const m of step.modifiers ?? []) {
|
|
2270
|
+
const key = resolveModifier(m);
|
|
2271
|
+
await page.keyboard.down(key);
|
|
2272
|
+
held.push(key);
|
|
2273
|
+
}
|
|
2274
|
+
await page.mouse.down();
|
|
2275
|
+
const from = { ...cur };
|
|
2276
|
+
const tx = step.tx;
|
|
2277
|
+
const ty = step.ty;
|
|
2278
|
+
await clockMotion(
|
|
2279
|
+
opts.dry ? 0 : step.ms ?? 700,
|
|
2280
|
+
async (u) => {
|
|
2281
|
+
cur.x = from.x + (tx - from.x) * u;
|
|
2282
|
+
cur.y = from.y + (ty - from.y) * u;
|
|
2283
|
+
await page.mouse.move(cur.x, cur.y);
|
|
2284
|
+
},
|
|
2285
|
+
clock
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
await page.mouse.up();
|
|
2289
|
+
} finally {
|
|
2290
|
+
for (const key of held.reverse())
|
|
2291
|
+
await page.keyboard.up(key).catch(() => void 0);
|
|
2292
|
+
}
|
|
2293
|
+
await clock.sleep(DRAG_HOLD_MS);
|
|
2294
|
+
break;
|
|
2295
|
+
}
|
|
2296
|
+
case "expect": {
|
|
2297
|
+
const cond = step;
|
|
2298
|
+
selector = step.selector ?? step.gone;
|
|
2299
|
+
const timeoutMs = (step.timeout ?? EXPECT_TIMEOUT_S) * 1e3;
|
|
2300
|
+
const res = await pollCondition(page, cond, {
|
|
2301
|
+
timeoutMs,
|
|
2302
|
+
everyMs: EXPECT_EVERY_MS,
|
|
2303
|
+
sleep: clock.sleep
|
|
2304
|
+
});
|
|
2305
|
+
value = peerValue(res.last);
|
|
2306
|
+
if (!res.ok) reason = conditionMiss(cond, res.last, timeoutMs);
|
|
2307
|
+
else log(` ${tag} expect ${sayCondition(cond)}: holds`);
|
|
2308
|
+
break;
|
|
2309
|
+
}
|
|
2310
|
+
default:
|
|
2311
|
+
reason = `a peer does not ${step.do}`;
|
|
2312
|
+
}
|
|
2313
|
+
} catch (e) {
|
|
2314
|
+
reason = e instanceof Error ? e.message.split("\n")[0] : String(e);
|
|
2315
|
+
}
|
|
2316
|
+
const hang = bound.end();
|
|
2317
|
+
if (hang.hung)
|
|
2318
|
+
reason = `the page stopped answering: this ${step.do} ran past its bound`;
|
|
2319
|
+
if (reason) log(` (${tag} ${step.do}: ${reason})`);
|
|
2320
|
+
else if (step.do !== "expect")
|
|
2321
|
+
log(` ${tag} ${step.do}${selector ? ` ${selector}` : ""}`);
|
|
2322
|
+
run.steps.push({
|
|
2323
|
+
step: i,
|
|
2324
|
+
...step.id ? { id: step.id } : {},
|
|
2325
|
+
do: step.do,
|
|
2326
|
+
...step.with ? { with: step.with } : {},
|
|
2327
|
+
...step.after ? { after: step.after } : {},
|
|
2328
|
+
...selector ? { selector } : {},
|
|
2329
|
+
tStart: +(tStart / 1e3).toFixed(3),
|
|
2330
|
+
tEnd: +(opts.footageMs() / 1e3).toFixed(3),
|
|
2331
|
+
...reason ? { skipped: true, reason } : {},
|
|
2332
|
+
...late ? { late: true } : {},
|
|
2333
|
+
...value !== void 0 ? { value } : {}
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
return run;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
1817
2339
|
// src/record/actions.ts
|
|
1818
2340
|
var STEP_GUARDS = ["spend"];
|
|
2341
|
+
var ZOOM_MARK_VERBS = [
|
|
2342
|
+
"click",
|
|
2343
|
+
"drag",
|
|
2344
|
+
"type",
|
|
2345
|
+
"paste",
|
|
2346
|
+
"press",
|
|
2347
|
+
"hover"
|
|
2348
|
+
];
|
|
1819
2349
|
var GUARDABLE = /* @__PURE__ */ new Set(["click", "press", "type"]);
|
|
1820
|
-
var
|
|
2350
|
+
var EXPECT_TIMEOUT_S2 = 5;
|
|
1821
2351
|
var AWAIT_TIMEOUT_S = 600;
|
|
1822
2352
|
var AWAIT_POLL_S = 2;
|
|
1823
2353
|
var DOWNLOAD_TIMEOUT_S = 30;
|
|
@@ -1901,6 +2431,14 @@ function validateActions(value) {
|
|
|
1901
2431
|
errors.push("steps must be a non-empty array");
|
|
1902
2432
|
return errors;
|
|
1903
2433
|
}
|
|
2434
|
+
if (obj.peers !== void 0)
|
|
2435
|
+
errors.push(
|
|
2436
|
+
...validatePeers(
|
|
2437
|
+
obj.peers,
|
|
2438
|
+
obj.steps,
|
|
2439
|
+
(steps) => validateActions({ steps })
|
|
2440
|
+
)
|
|
2441
|
+
);
|
|
1904
2442
|
const seenIds = /* @__PURE__ */ new Set();
|
|
1905
2443
|
obj.steps.forEach((raw, i) => {
|
|
1906
2444
|
const at = `steps[${i}]`;
|
|
@@ -2020,6 +2558,14 @@ function validateActions(value) {
|
|
|
2020
2558
|
if (s.do === "move" && (typeof s.x !== "number" || typeof s.y !== "number")) {
|
|
2021
2559
|
errors.push(`${at}: move needs x and y (CSS px)`);
|
|
2022
2560
|
}
|
|
2561
|
+
if (s.zoom !== void 0) {
|
|
2562
|
+
if (typeof s.zoom !== "boolean")
|
|
2563
|
+
errors.push(`${at}: zoom must be true or false`);
|
|
2564
|
+
else if (!ZOOM_MARK_VERBS.includes(s.do))
|
|
2565
|
+
errors.push(
|
|
2566
|
+
`${at}: zoom marks a gesture (${ZOOM_MARK_VERBS.join(", ")}); a ${s.do} shows nothing to zoom on`
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2023
2569
|
if (s.guard !== void 0) {
|
|
2024
2570
|
if (!STEP_GUARDS.includes(s.guard))
|
|
2025
2571
|
errors.push(
|
|
@@ -2035,16 +2581,17 @@ function validateActions(value) {
|
|
|
2035
2581
|
);
|
|
2036
2582
|
}
|
|
2037
2583
|
if (s.do === "expect") {
|
|
2038
|
-
const given = ["selector", "text", "gone"].filter(
|
|
2584
|
+
const given = ["selector", "text", "gone", "js"].filter(
|
|
2039
2585
|
(k) => s[k] !== void 0
|
|
2040
2586
|
);
|
|
2041
2587
|
if (given.length !== 1)
|
|
2042
2588
|
errors.push(
|
|
2043
|
-
`${at}: expect needs exactly one of selector, text or
|
|
2589
|
+
`${at}: expect needs exactly one of selector, text, gone or js${given.length ? ` (it has ${given.join(" and ")})` : ""}`
|
|
2044
2590
|
);
|
|
2045
2591
|
for (const k of given)
|
|
2046
2592
|
if (!isFilled(s[k]))
|
|
2047
2593
|
errors.push(`${at}: expect ${k} must be a non-empty string`);
|
|
2594
|
+
errors.push(...equalsProblem(`${at}: expect`, s));
|
|
2048
2595
|
if (s.enabled !== void 0) {
|
|
2049
2596
|
if (typeof s.enabled !== "boolean")
|
|
2050
2597
|
errors.push(`${at}: expect enabled must be true or false`);
|
|
@@ -2059,28 +2606,29 @@ function validateActions(value) {
|
|
|
2059
2606
|
const u = s.until;
|
|
2060
2607
|
if (typeof u !== "object" || u === null || Array.isArray(u)) {
|
|
2061
2608
|
errors.push(
|
|
2062
|
-
`${at}: await needs until, one of { "selector" }, { "text" } or { "
|
|
2609
|
+
`${at}: await needs until, one of { "selector" }, { "text" }, { "gone" } or { "js" }`
|
|
2063
2610
|
);
|
|
2064
2611
|
} else {
|
|
2065
2612
|
const until = u;
|
|
2066
2613
|
const keys = Object.keys(until);
|
|
2067
2614
|
const known = keys.filter(
|
|
2068
|
-
(k) => k === "selector" || k === "text" || k === "gone"
|
|
2615
|
+
(k) => k === "selector" || k === "text" || k === "gone" || k === "js"
|
|
2069
2616
|
);
|
|
2070
|
-
const unknown = keys.filter((k) => !known.includes(k));
|
|
2617
|
+
const unknown = keys.filter((k) => !known.includes(k) && k !== "equals");
|
|
2071
2618
|
if (unknown.length)
|
|
2072
2619
|
errors.push(
|
|
2073
|
-
`${at}: await until takes selector, text or
|
|
2620
|
+
`${at}: await until takes selector, text, gone or js (with equals), not ${unknown.join(", ")}`
|
|
2074
2621
|
);
|
|
2075
2622
|
if (known.length !== 1)
|
|
2076
2623
|
errors.push(
|
|
2077
|
-
`${at}: await until needs exactly one of selector, text or
|
|
2624
|
+
`${at}: await until needs exactly one of selector, text, gone or js${known.length ? ` (it has ${known.join(" and ")})` : ""}`
|
|
2078
2625
|
);
|
|
2079
2626
|
for (const k of known) {
|
|
2080
2627
|
const v = until[k];
|
|
2081
2628
|
if (typeof v !== "string" || !v.trim())
|
|
2082
2629
|
errors.push(`${at}: await until.${k} must be a non-empty string`);
|
|
2083
2630
|
}
|
|
2631
|
+
errors.push(...equalsProblem(`${at}: await until`, until));
|
|
2084
2632
|
}
|
|
2085
2633
|
errors.push(...secondsProblem(at, "await timeout", s.timeout));
|
|
2086
2634
|
errors.push(...secondsProblem(at, "await poll", s.poll));
|
|
@@ -2151,6 +2699,29 @@ function modifiersProblem(at, verb, v) {
|
|
|
2151
2699
|
return [`${at}: ${verb} modifiers names a modifier twice`];
|
|
2152
2700
|
return [];
|
|
2153
2701
|
}
|
|
2702
|
+
function equalsProblem(at, s) {
|
|
2703
|
+
if (s.equals === void 0) return [];
|
|
2704
|
+
if (s.js === void 0)
|
|
2705
|
+
return [
|
|
2706
|
+
`${at} equals is the value a js expression must answer, so it needs js`
|
|
2707
|
+
];
|
|
2708
|
+
if (!isJsonValue(s.equals, 0))
|
|
2709
|
+
return [
|
|
2710
|
+
`${at} equals must be a JSON value (a number, a string, true, false, null, a list or an object of them)`
|
|
2711
|
+
];
|
|
2712
|
+
return [];
|
|
2713
|
+
}
|
|
2714
|
+
function isJsonValue(v, depth) {
|
|
2715
|
+
if (depth > 32) return false;
|
|
2716
|
+
if (v === null || typeof v === "string" || typeof v === "boolean") return true;
|
|
2717
|
+
if (typeof v === "number") return Number.isFinite(v);
|
|
2718
|
+
if (Array.isArray(v)) return v.every((x) => isJsonValue(x, depth + 1));
|
|
2719
|
+
if (typeof v === "object")
|
|
2720
|
+
return Object.values(v).every(
|
|
2721
|
+
(x) => isJsonValue(x, depth + 1)
|
|
2722
|
+
);
|
|
2723
|
+
return false;
|
|
2724
|
+
}
|
|
2154
2725
|
var isFilled = (v) => typeof v === "string" && v.trim().length > 0;
|
|
2155
2726
|
function secondsProblem(at, what, v) {
|
|
2156
2727
|
if (v === void 0) return [];
|
|
@@ -2203,6 +2774,8 @@ function guardVerdict(actions, confirm) {
|
|
|
2203
2774
|
}
|
|
2204
2775
|
function hostedRefusals(actions) {
|
|
2205
2776
|
const out = [];
|
|
2777
|
+
const peers = peersHostedRefusal(actions.peers);
|
|
2778
|
+
if (peers) out.push(peers);
|
|
2206
2779
|
for (const g of guardedSteps(actions))
|
|
2207
2780
|
out.push(
|
|
2208
2781
|
`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 +2789,10 @@ function hostedRefusals(actions) {
|
|
|
2216
2789
|
out.push(
|
|
2217
2790
|
`steps[${i}] is a paste, and a hosted browser has no clipboard to paste from: record it on this machine`
|
|
2218
2791
|
);
|
|
2792
|
+
if (s.do === "expect" && s.js !== void 0 || s.do === "await" && s.until.js !== void 0)
|
|
2793
|
+
out.push(
|
|
2794
|
+
`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)`
|
|
2795
|
+
);
|
|
2219
2796
|
if (s.do === "drag" && typeof s.path === "string")
|
|
2220
2797
|
out.push(
|
|
2221
2798
|
`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 +2832,7 @@ async function loadScriptFiles(actions, source) {
|
|
|
2255
2832
|
} catch (e) {
|
|
2256
2833
|
const why = e instanceof Error ? e.message : String(e);
|
|
2257
2834
|
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
|
|
2835
|
+
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
2836
|
};
|
|
2260
2837
|
}
|
|
2261
2838
|
if (f.as === "path") {
|
|
@@ -2416,7 +2993,7 @@ function fetchInPageScript(url) {
|
|
|
2416
2993
|
}
|
|
2417
2994
|
|
|
2418
2995
|
// src/record/recorder.ts
|
|
2419
|
-
var
|
|
2996
|
+
var EXPECT_EVERY_MS2 = 100;
|
|
2420
2997
|
var SRC_WAIT_MS = 8e3;
|
|
2421
2998
|
var AWAIT_HEARTBEAT_MS = 3e4;
|
|
2422
2999
|
var MIN_SKIP_MS = 250;
|
|
@@ -2509,6 +3086,22 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2509
3086
|
const read = await loadScriptFiles(actions, opts.files);
|
|
2510
3087
|
if ("problem" in read) throw new RecordRefusedError(read.problem);
|
|
2511
3088
|
const files = read;
|
|
3089
|
+
const peers = actions.peers ?? [];
|
|
3090
|
+
const peerStates = /* @__PURE__ */ new Map();
|
|
3091
|
+
for (const p of peers) {
|
|
3092
|
+
if (p.storageState === void 0) continue;
|
|
3093
|
+
if (!opts.peerStorageState)
|
|
3094
|
+
throw new RecordRefusedError(
|
|
3095
|
+
`peer ${p.id} signs in from the storage state "${p.storageState}", and this recorder has nowhere to read one from. Nothing was recorded.`
|
|
3096
|
+
);
|
|
3097
|
+
try {
|
|
3098
|
+
peerStates.set(p.id, await opts.peerStorageState(p.storageState));
|
|
3099
|
+
} catch (e) {
|
|
3100
|
+
throw new RecordRefusedError(
|
|
3101
|
+
`peer ${p.id}'s storage state "${p.storageState}" could not be read (${e instanceof Error ? e.message : String(e)}). Nothing was recorded.`
|
|
3102
|
+
);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
2512
3105
|
const vw = actions.viewport?.width ?? 1280;
|
|
2513
3106
|
const vh = actions.viewport?.height ?? 720;
|
|
2514
3107
|
const context = opts.context ?? await browser.newContext({
|
|
@@ -2587,6 +3180,59 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2587
3180
|
await page.evaluate(MEDIA_DRAIN_PROBE).catch(() => null);
|
|
2588
3181
|
if (actions.steps.some((s) => settleOn(s, actions.settle)))
|
|
2589
3182
|
await page.evaluate(CHANGE_AGE_PROBE).catch(() => void 0);
|
|
3183
|
+
const peerPages = [];
|
|
3184
|
+
const closePeers = async () => {
|
|
3185
|
+
const bound = new StepBound();
|
|
3186
|
+
bound.start(CLOSE_BOUND_MS);
|
|
3187
|
+
await Promise.all(
|
|
3188
|
+
peerPages.map((p) => bound.race(p.context.close().catch(() => {
|
|
3189
|
+
})))
|
|
3190
|
+
);
|
|
3191
|
+
bound.end();
|
|
3192
|
+
};
|
|
3193
|
+
for (const peer of peers) {
|
|
3194
|
+
const peerUrl = peer.url ?? url;
|
|
3195
|
+
const pctx = await browser.newContext({
|
|
3196
|
+
viewport: peer.viewport ?? { width: vw, height: vh },
|
|
3197
|
+
deviceScaleFactor: 1,
|
|
3198
|
+
...peerStates.has(peer.id) ? { storageState: peerStates.get(peer.id) } : {}
|
|
3199
|
+
});
|
|
3200
|
+
if (opts.headers && Object.keys(opts.headers).length)
|
|
3201
|
+
await applyHeaders(pctx, opts.headers, peerUrl, log);
|
|
3202
|
+
const ppage = await pctx.newPage();
|
|
3203
|
+
peerPages.push({ peer, context: pctx, page: ppage });
|
|
3204
|
+
const peerErrors = new PageErrorLog(opts.verbose === true);
|
|
3205
|
+
ppage.on("console", (m) => {
|
|
3206
|
+
if (m.type() !== "error") return;
|
|
3207
|
+
const line = peerErrors.add(m.text());
|
|
3208
|
+
if (line) log(` peer ${peer.id}: ${line.trim()}`);
|
|
3209
|
+
});
|
|
3210
|
+
log(`peer ${peer.id}: goto ${peerUrl} (off camera)`);
|
|
3211
|
+
const popened = await untilReady(
|
|
3212
|
+
ppage,
|
|
3213
|
+
readyMode,
|
|
3214
|
+
(o) => ppage.goto(peerUrl, o),
|
|
3215
|
+
{ sleep: realSleep }
|
|
3216
|
+
);
|
|
3217
|
+
const seen = await ppage.evaluate(WALL_PROBE).catch(() => null);
|
|
3218
|
+
const peerWall = wallVerdict({
|
|
3219
|
+
askedUrl: peerUrl,
|
|
3220
|
+
landedUrl: ppage.url(),
|
|
3221
|
+
...popened.response ? { status: popened.response.status() } : {},
|
|
3222
|
+
passwordFields: seen?.passwordFields ?? 0,
|
|
3223
|
+
newPasswordField: seen?.newPasswordField ?? false,
|
|
3224
|
+
oneTimeCodeField: seen?.oneTimeCodeField ?? false
|
|
3225
|
+
});
|
|
3226
|
+
if (peerWall?.level === "hard") {
|
|
3227
|
+
await closePeers();
|
|
3228
|
+
await context.close().catch(() => {
|
|
3229
|
+
});
|
|
3230
|
+
throw new RecordRefusedError(
|
|
3231
|
+
`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.`
|
|
3232
|
+
);
|
|
3233
|
+
}
|
|
3234
|
+
log(` peer ${peer.id}: ${readyLine(popened.ready)}`);
|
|
3235
|
+
}
|
|
2590
3236
|
const cdp = await context.newCDPSession(rawPage);
|
|
2591
3237
|
if (!dry) await fitSurfaceToViewport(cdp, vw, vh);
|
|
2592
3238
|
const frames = [];
|
|
@@ -2805,6 +3451,23 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2805
3451
|
let stopped = null;
|
|
2806
3452
|
let unpressedGuard = null;
|
|
2807
3453
|
let capped = false;
|
|
3454
|
+
const signals = new StepSignals(
|
|
3455
|
+
actions.steps.map((s) => s.id).filter((id) => !!id)
|
|
3456
|
+
);
|
|
3457
|
+
const lanes = peerPages.map(
|
|
3458
|
+
({ peer, page: ppage }) => runPeer(peer, ppage, signals, {
|
|
3459
|
+
clock: { now: () => Date.now(), sleep },
|
|
3460
|
+
footageMs: () => now(),
|
|
3461
|
+
log,
|
|
3462
|
+
dry
|
|
3463
|
+
}).catch((e) => {
|
|
3464
|
+
log(
|
|
3465
|
+
` (peer ${peer.id}: stopped: ${e instanceof Error ? e.message : String(e)})`
|
|
3466
|
+
);
|
|
3467
|
+
return { id: peer.id, steps: [], notRun: [] };
|
|
3468
|
+
})
|
|
3469
|
+
);
|
|
3470
|
+
signals.begin();
|
|
2808
3471
|
for (const [stepIdx, step] of actions.steps.entries()) {
|
|
2809
3472
|
if (capReached(now(), maxSeconds)) {
|
|
2810
3473
|
capped = true;
|
|
@@ -2844,6 +3507,12 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2844
3507
|
if (reason) extra.reason = reason;
|
|
2845
3508
|
};
|
|
2846
3509
|
if (step.guard) extra.guard = step.guard;
|
|
3510
|
+
if (typeof step.zoom === "boolean") extra.zoom = step.zoom;
|
|
3511
|
+
const keepValue = (last) => {
|
|
3512
|
+
if (!("value" in last)) return;
|
|
3513
|
+
const kept = keptValue(last.value);
|
|
3514
|
+
if (kept !== void 0) extra.value = kept;
|
|
3515
|
+
};
|
|
2847
3516
|
const rehearseGuard = async (sel, point) => {
|
|
2848
3517
|
if (!step.guard || !dry) return false;
|
|
2849
3518
|
const rect = sel ? await boxOf(sel) : null;
|
|
@@ -2872,7 +3541,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2872
3541
|
step,
|
|
2873
3542
|
askedMs(step),
|
|
2874
3543
|
{
|
|
2875
|
-
expectS:
|
|
3544
|
+
expectS: EXPECT_TIMEOUT_S2,
|
|
2876
3545
|
awaitS: AWAIT_TIMEOUT_S,
|
|
2877
3546
|
downloadS: DOWNLOAD_TIMEOUT_S
|
|
2878
3547
|
}
|
|
@@ -2884,6 +3553,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2884
3553
|
...step.id ? { id: step.id } : {},
|
|
2885
3554
|
do: step.do
|
|
2886
3555
|
});
|
|
3556
|
+
signals.start(step.id);
|
|
2887
3557
|
const stepWall = Date.now();
|
|
2888
3558
|
pageBound.start(boundMs);
|
|
2889
3559
|
switch (step.do) {
|
|
@@ -3225,12 +3895,13 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3225
3895
|
case "expect": {
|
|
3226
3896
|
if (notWaited()) break;
|
|
3227
3897
|
const cond = step;
|
|
3228
|
-
const timeoutMs = (step.timeout ??
|
|
3898
|
+
const timeoutMs = (step.timeout ?? EXPECT_TIMEOUT_S2) * 1e3;
|
|
3229
3899
|
const res = await pollCondition(page, cond, {
|
|
3230
3900
|
timeoutMs,
|
|
3231
|
-
everyMs:
|
|
3901
|
+
everyMs: EXPECT_EVERY_MS2,
|
|
3232
3902
|
sleep: realSleep
|
|
3233
3903
|
});
|
|
3904
|
+
keepValue(res.last);
|
|
3234
3905
|
if (res.ok) log(`expect ${sayCondition(cond)}: holds`);
|
|
3235
3906
|
else {
|
|
3236
3907
|
const reason = conditionMiss(cond, res.last, timeoutMs);
|
|
@@ -3275,6 +3946,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3275
3946
|
clearInterval(beat);
|
|
3276
3947
|
}
|
|
3277
3948
|
const { out, at, ms } = waited;
|
|
3949
|
+
keepValue(out.last);
|
|
3278
3950
|
opts.onEvent?.({ ...awaitEvent, phase: "end", ms, held: out.ok });
|
|
3279
3951
|
extra.offCameraMs = ms;
|
|
3280
3952
|
if (!out.ok) extra.timedOut = true;
|
|
@@ -3459,6 +4131,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3459
4131
|
await drainSound(stepIdx);
|
|
3460
4132
|
pageBound.end();
|
|
3461
4133
|
const stepSpan = stepSpans[stepSpans.length - 1];
|
|
4134
|
+
signals.end(step.id);
|
|
3462
4135
|
opts.onEvent?.({
|
|
3463
4136
|
event: "step",
|
|
3464
4137
|
phase: "end",
|
|
@@ -3471,6 +4144,19 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3471
4144
|
});
|
|
3472
4145
|
if (stopped) break;
|
|
3473
4146
|
}
|
|
4147
|
+
signals.close();
|
|
4148
|
+
const peerRuns = await Promise.all(lanes);
|
|
4149
|
+
for (const pr of peerRuns)
|
|
4150
|
+
for (const ps of pr.steps)
|
|
4151
|
+
if (ps.skipped)
|
|
4152
|
+
skipped.push({
|
|
4153
|
+
step: ps.step,
|
|
4154
|
+
...ps.id ? { id: ps.id } : {},
|
|
4155
|
+
do: ps.do,
|
|
4156
|
+
selector: ps.selector ?? "",
|
|
4157
|
+
...ps.reason ? { reason: ps.reason } : {},
|
|
4158
|
+
peer: pr.id
|
|
4159
|
+
});
|
|
3474
4160
|
if (!capped && !stopped) await sleep(TRAILING_HOLD_MS);
|
|
3475
4161
|
if (capped || capReached(now(), maxSeconds)) {
|
|
3476
4162
|
capped = true;
|
|
@@ -3497,6 +4183,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3497
4183
|
await pageBound.race(context.close().catch(() => {
|
|
3498
4184
|
}));
|
|
3499
4185
|
pageBound.end();
|
|
4186
|
+
await closePeers();
|
|
3500
4187
|
await sinkChain;
|
|
3501
4188
|
const meta = {
|
|
3502
4189
|
dpr: 1,
|
|
@@ -3523,7 +4210,14 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3523
4210
|
...masks.length ? { masks: exposureLog.masks(masks) } : {},
|
|
3524
4211
|
...skips.length ? { skips } : {},
|
|
3525
4212
|
...outputs.length ? { outputs } : {},
|
|
3526
|
-
...media.length ? { media } : {}
|
|
4213
|
+
...media.length ? { media } : {},
|
|
4214
|
+
...peerRuns.length ? {
|
|
4215
|
+
peers: peerRuns.map((pr) => ({
|
|
4216
|
+
id: pr.id,
|
|
4217
|
+
steps: pr.steps,
|
|
4218
|
+
...pr.notRun.length ? { notRun: pr.notRun } : {}
|
|
4219
|
+
}))
|
|
4220
|
+
} : {}
|
|
3527
4221
|
};
|
|
3528
4222
|
const freezes = [];
|
|
3529
4223
|
for (let i = 1; i < frames.length; i++) {
|
|
@@ -3570,7 +4264,8 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3570
4264
|
overruns,
|
|
3571
4265
|
// Self-contained: a path a file held is inline, so the take re-records
|
|
3572
4266
|
// from its own script with the file gone.
|
|
3573
|
-
actions: { ...inlineScript(actions, files), url }
|
|
4267
|
+
actions: { ...inlineScript(actions, files), url },
|
|
4268
|
+
peers: peerRuns
|
|
3574
4269
|
};
|
|
3575
4270
|
}
|
|
3576
4271
|
async function keepPageSound(heard, sink, fetchSource, log) {
|
|
@@ -3834,16 +4529,20 @@ export {
|
|
|
3834
4529
|
DOWNLOAD_TIMEOUT_S,
|
|
3835
4530
|
DRAG_HOLD_MS,
|
|
3836
4531
|
DRAG_MODIFIERS,
|
|
3837
|
-
EXPECT_TIMEOUT_S,
|
|
4532
|
+
EXPECT_TIMEOUT_S2 as EXPECT_TIMEOUT_S,
|
|
3838
4533
|
EXPOSURE_ADVICE,
|
|
3839
4534
|
EXPOSURE_MATCHERS_SRC,
|
|
3840
4535
|
EXPOSURE_PROBE,
|
|
3841
4536
|
ExposureLog,
|
|
3842
4537
|
IDLE_CALL_MS,
|
|
4538
|
+
JS_SAID_MAX,
|
|
4539
|
+
JS_VALUE_MAX,
|
|
3843
4540
|
KEY_ROUNDTRIP_MS,
|
|
3844
4541
|
KEY_SEQUENCE_GAP_MS,
|
|
3845
4542
|
MAX_KEYS,
|
|
3846
4543
|
MAX_PATH_POINTS,
|
|
4544
|
+
MAX_PEERS,
|
|
4545
|
+
MAX_PEER_STEPS,
|
|
3847
4546
|
MEDIA_DRAIN_PROBE,
|
|
3848
4547
|
MEDIA_WATCH_SCRIPT,
|
|
3849
4548
|
MIN_SKIP_MS,
|
|
@@ -3864,6 +4563,8 @@ export {
|
|
|
3864
4563
|
PASTE_TYPES,
|
|
3865
4564
|
PASTE_UNREAD_MS,
|
|
3866
4565
|
PATH_DEFAULT_MS,
|
|
4566
|
+
PEER_VALUE_MAX,
|
|
4567
|
+
PEER_VERBS,
|
|
3867
4568
|
POINTER_MAX_MS,
|
|
3868
4569
|
POINTER_MIN_MS,
|
|
3869
4570
|
POINTER_MS_PER_PX,
|
|
@@ -3890,11 +4591,13 @@ export {
|
|
|
3890
4591
|
SetupEnvError,
|
|
3891
4592
|
SetupError,
|
|
3892
4593
|
StepBound,
|
|
4594
|
+
StepSignals,
|
|
3893
4595
|
TRAILING_HOLD_MS,
|
|
3894
4596
|
TakeClock,
|
|
3895
4597
|
WAIT_MARGIN_MS,
|
|
3896
4598
|
WALL_PROBE,
|
|
3897
4599
|
WallError,
|
|
4600
|
+
ZOOM_MARK_VERBS,
|
|
3898
4601
|
appSettled,
|
|
3899
4602
|
askedMs,
|
|
3900
4603
|
base64Bytes,
|
|
@@ -3931,6 +4634,8 @@ export {
|
|
|
3931
4634
|
isKeyName,
|
|
3932
4635
|
isSignInPath,
|
|
3933
4636
|
jpegDims,
|
|
4637
|
+
jsConditionScript,
|
|
4638
|
+
keptValue,
|
|
3934
4639
|
keySequencePlanMs,
|
|
3935
4640
|
loadScriptFiles,
|
|
3936
4641
|
maskInitScript,
|
|
@@ -3947,6 +4652,8 @@ export {
|
|
|
3947
4652
|
parseHeaders,
|
|
3948
4653
|
pasteTypeOf,
|
|
3949
4654
|
pathDurationMs,
|
|
4655
|
+
peerValue,
|
|
4656
|
+
peersHostedRefusal,
|
|
3950
4657
|
plainName,
|
|
3951
4658
|
planFreshTake,
|
|
3952
4659
|
pointerTravelMs,
|
|
@@ -3966,9 +4673,13 @@ export {
|
|
|
3966
4673
|
resolveChord,
|
|
3967
4674
|
resolveModifier,
|
|
3968
4675
|
resolveSetupText,
|
|
4676
|
+
runPeer,
|
|
3969
4677
|
runSetup,
|
|
4678
|
+
sameJson,
|
|
3970
4679
|
sayCondition,
|
|
3971
4680
|
sayDuration,
|
|
4681
|
+
sayJs,
|
|
4682
|
+
sayValue,
|
|
3972
4683
|
schedulePath,
|
|
3973
4684
|
scriptFiles,
|
|
3974
4685
|
scrollByScript,
|
|
@@ -3987,6 +4698,7 @@ export {
|
|
|
3987
4698
|
untilReady,
|
|
3988
4699
|
urlSchemeProblem,
|
|
3989
4700
|
validateActions,
|
|
4701
|
+
validatePeers,
|
|
3990
4702
|
validateSetup,
|
|
3991
4703
|
visibleTextScript,
|
|
3992
4704
|
wallVerdict,
|