@olomonhelpdesk2/helpdesk 0.1.17 → 0.1.18
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/helpdesk-capture.mjs +110 -50
- package/package.json +1 -1
package/helpdesk-capture.mjs
CHANGED
|
@@ -248,6 +248,11 @@ function helpdeskCommand(command, args = []) {
|
|
|
248
248
|
|
|
249
249
|
// core/capture/classify.ts
|
|
250
250
|
var THIN_TEXT_CHARS = 25;
|
|
251
|
+
var ERROR_PAGE_PHRASES = [
|
|
252
|
+
/\bapplication error: a (server|client)-side exception has occurred\b/i,
|
|
253
|
+
/\bthis screen did not load\b/i,
|
|
254
|
+
/\bdigest\s*:?\s*[\w-]{6,}\s+copy reference\b/i
|
|
255
|
+
];
|
|
251
256
|
var BOUNDARY_PHRASES = [
|
|
252
257
|
/\bsomething went wrong\b/i,
|
|
253
258
|
/\bapplication error\b/i,
|
|
@@ -256,8 +261,12 @@ var BOUNDARY_PHRASES = [
|
|
|
256
261
|
/\binternal server error\b/i,
|
|
257
262
|
/\bthis page (could not|couldn'?t) be loaded\b/i,
|
|
258
263
|
/\bwe('| a)re sorry[ —,-]+something\b/i,
|
|
259
|
-
/\berror (id|digest)\b/i
|
|
264
|
+
/\berror (id|digest)\b/i,
|
|
265
|
+
...ERROR_PAGE_PHRASES
|
|
260
266
|
];
|
|
267
|
+
function showsErrorPage(text2) {
|
|
268
|
+
return ERROR_PAGE_PHRASES.some((phrase) => phrase.test(text2));
|
|
269
|
+
}
|
|
261
270
|
var NOT_FOUND_PHRASES = [
|
|
262
271
|
/\b(page|it) (was )?not found\b/i,
|
|
263
272
|
/\b404\b/,
|
|
@@ -1010,6 +1019,7 @@ async function readPaintedMarkup(page, regions) {
|
|
|
1010
1019
|
const style = window.getComputedStyle(element);
|
|
1011
1020
|
if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") return false;
|
|
1012
1021
|
if (style.display === "contents") return true;
|
|
1022
|
+
if (typeof element.checkVisibility === "function" && !element.checkVisibility()) return false;
|
|
1013
1023
|
for (const box of Array.from(element.getClientRects())) {
|
|
1014
1024
|
if (box.width > 0 && box.height > 0) return true;
|
|
1015
1025
|
}
|
|
@@ -1090,6 +1100,7 @@ async function readPageLayout(page, cover = []) {
|
|
|
1090
1100
|
};
|
|
1091
1101
|
const unseen = (element) => {
|
|
1092
1102
|
if (element.closest('[aria-hidden="true"], [inert]')) return true;
|
|
1103
|
+
if (typeof element.checkVisibility === "function" && !element.checkVisibility()) return true;
|
|
1093
1104
|
for (let node = element; node; node = node.parentElement) {
|
|
1094
1105
|
const style = window.getComputedStyle(node);
|
|
1095
1106
|
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return true;
|
|
@@ -1572,11 +1583,17 @@ function readStep(value, at) {
|
|
|
1572
1583
|
if (action !== "auto" && !typed && given !== void 0) return `step ${at} gives a value to ${action}, which takes none`;
|
|
1573
1584
|
if (action === "auto" && given !== void 0 && !valued) return `step ${at} has a value nothing can type`;
|
|
1574
1585
|
if (step["follows"] !== void 0 && step["follows"] !== true) return `step ${at} says neither that it follows the step before it nor that it does not`;
|
|
1586
|
+
const demo = step["demo"] === void 0 ? void 0 : cleanText(step["demo"]);
|
|
1587
|
+
const opens = step["opens"] === void 0 ? void 0 : cleanText(step["opens"]);
|
|
1588
|
+
if (demo !== void 0 && (!demo || demo.length > MAX_TEXT2 || action !== "auto" && action !== "fill")) return `step ${at} has a demo value nothing types`;
|
|
1589
|
+
if (opens !== void 0 && (!isPathLike(opens) || !fillsPattern(route, opens.split("?")[0]))) return `step ${at} opens at ${opens || "nowhere"}, which is not ${route}`;
|
|
1575
1590
|
return {
|
|
1576
1591
|
route,
|
|
1577
1592
|
control: { ...role === void 0 ? {} : { role }, name, ...scope ? { scope } : {} },
|
|
1578
1593
|
action,
|
|
1579
1594
|
...valued && (typed || action === "auto") ? { value: given } : {},
|
|
1595
|
+
...demo ? { demo } : {},
|
|
1596
|
+
...opens ? { opens } : {},
|
|
1580
1597
|
says,
|
|
1581
1598
|
...step["follows"] === true ? { follows: true } : {}
|
|
1582
1599
|
};
|
|
@@ -1677,7 +1694,7 @@ function inChrome(manifest, control) {
|
|
|
1677
1694
|
}
|
|
1678
1695
|
return false;
|
|
1679
1696
|
}
|
|
1680
|
-
function resolveStep(manifest, step) {
|
|
1697
|
+
function resolveStep(manifest, step, options = {}) {
|
|
1681
1698
|
const name = comparable(step.control.name);
|
|
1682
1699
|
const where = manifest.screen.path;
|
|
1683
1700
|
const role = step.control.role;
|
|
@@ -1691,16 +1708,18 @@ function resolveStep(manifest, step) {
|
|
|
1691
1708
|
const scoped = matches.filter((control) => within(manifest, control, regions));
|
|
1692
1709
|
if (scoped.length) matches = scoped;
|
|
1693
1710
|
}
|
|
1694
|
-
|
|
1695
|
-
const
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
if (matches.length
|
|
1711
|
+
const narrow = (keep) => {
|
|
1712
|
+
const kept = matches.filter(keep);
|
|
1713
|
+
if (kept.length === 1) matches = kept;
|
|
1714
|
+
};
|
|
1715
|
+
if (!role && matches.length > 1 && options.after) narrow((control) => within(manifest, control, /* @__PURE__ */ new Set([options.after])));
|
|
1716
|
+
if (!role && matches.length > 1) narrow((control) => inChrome(manifest, control) === IN_CHROME.test(step.says));
|
|
1717
|
+
if (!role && matches.length > 1) narrow((control) => !control.state?.disabled);
|
|
1718
|
+
if (matches.length === 0) return { ok: false, kind: "missing", because: `no ${what} "${step.control.name}" on ${where}` };
|
|
1700
1719
|
if (matches.length > 1) {
|
|
1701
|
-
return { ok: false, because: `${matches.length} of ${what} "${step.control.name}" on ${where}${scope ? ` in ${scope}` : ""}, so the step does not say which` };
|
|
1720
|
+
return { ok: false, kind: "ambiguous", because: `${matches.length} of ${what} "${step.control.name}" on ${where}${scope ? ` in ${scope}` : ""}, so the step does not say which` };
|
|
1702
1721
|
}
|
|
1703
|
-
if (matches[0].state?.disabled) return { ok: false, because: `${what} "${step.control.name}" on ${where} is disabled` };
|
|
1722
|
+
if (matches[0].state?.disabled) return { ok: false, kind: "disabled", because: `${what} "${step.control.name}" on ${where} is disabled` };
|
|
1704
1723
|
return { ok: true, control: matches[0] };
|
|
1705
1724
|
}
|
|
1706
1725
|
function onScreen(route, location2) {
|
|
@@ -1715,6 +1734,7 @@ function clipFingerprint(script, touched) {
|
|
|
1715
1734
|
role: step.control.role ?? null,
|
|
1716
1735
|
name: step.control.name,
|
|
1717
1736
|
...step.control.scope ? { scope: step.control.scope } : {},
|
|
1737
|
+
...step.demo ? { demo: step.demo } : {},
|
|
1718
1738
|
action: step.action,
|
|
1719
1739
|
value: step.value ?? null,
|
|
1720
1740
|
says: step.says
|
|
@@ -3195,7 +3215,14 @@ async function playwrightBrowser(options = {}) {
|
|
|
3195
3215
|
return;
|
|
3196
3216
|
}
|
|
3197
3217
|
await page.mouse.click(action.at.x, action.at.y);
|
|
3198
|
-
if (action.kind
|
|
3218
|
+
if (action.kind !== "focus") return;
|
|
3219
|
+
const field = await page.evaluate(() => {
|
|
3220
|
+
const focused = document.activeElement;
|
|
3221
|
+
const kind = focused instanceof HTMLInputElement ? focused.type : "";
|
|
3222
|
+
return Boolean(focused && (focused.isContentEditable || focused instanceof HTMLTextAreaElement || focused instanceof HTMLInputElement && !["button", "checkbox", "color", "file", "image", "radio", "range", "reset", "submit"].includes(kind)));
|
|
3223
|
+
});
|
|
3224
|
+
if (!field) throw new Error(`the press at (${action.at.x}, ${action.at.y}) focused no field, so nothing could be typed`);
|
|
3225
|
+
await page.keyboard.press("ControlOrMeta+A");
|
|
3199
3226
|
},
|
|
3200
3227
|
async type(text2) {
|
|
3201
3228
|
await page.keyboard.type(text2);
|
|
@@ -3476,20 +3503,22 @@ async function recordClip(options, script, touched) {
|
|
|
3476
3503
|
let last = new Uint8Array();
|
|
3477
3504
|
const judge = async (where) => {
|
|
3478
3505
|
const route = where.split("?")[0];
|
|
3479
|
-
const
|
|
3506
|
+
const text2 = await page.text();
|
|
3507
|
+
if (showsErrorPage(text2)) return fail(`${route} answered with its error page, not the screen`);
|
|
3508
|
+
const verdict = gateFrame(text2, {
|
|
3480
3509
|
route,
|
|
3481
3510
|
...options.brands ? { brands: options.brands } : {},
|
|
3482
3511
|
...options.clearance?.expectedBrands?.length ? { expected: options.clearance.expectedBrands } : {}
|
|
3483
3512
|
});
|
|
3484
|
-
if (!verdict.safe) return verdict.reason ?? `the gate refused ${route}
|
|
3513
|
+
if (!verdict.safe) return refuse(verdict.reason ?? `the gate refused ${route}`);
|
|
3485
3514
|
if (!options.clearance) return null;
|
|
3486
3515
|
const clearance = options.clearance;
|
|
3487
3516
|
const reading = await readScreen(page, clearance);
|
|
3488
|
-
if (!reading) return "this browser cannot read a screen against a clearance";
|
|
3489
|
-
if (reading.inventory.unaccounted.length) return `${reading.inventory.unaccounted.length} value(s) on ${route} are outside every region the clearance names
|
|
3517
|
+
if (!reading) return refuse("this browser cannot read a screen against a clearance");
|
|
3518
|
+
if (reading.inventory.unaccounted.length) return refuse(`${reading.inventory.unaccounted.length} value(s) on ${route} are outside every region the clearance names`);
|
|
3490
3519
|
const approved = new Set(clearance.approved);
|
|
3491
3520
|
const loose = reading.surfaces.filter((surface) => !surface.read && !clearance.covers.includes(surface.where) && !approved.has(surface.where));
|
|
3492
|
-
if (loose.length && !clearance.coverUnnamed) return `${loose.length} surface(s) on ${route} are neither approved nor covered
|
|
3521
|
+
if (loose.length && !clearance.coverUnnamed) return refuse(`${loose.length} surface(s) on ${route} are neither approved nor covered`);
|
|
3493
3522
|
cover = reading.surfaces.filter((surface) => !surface.read && !approved.has(surface.where)).map((surface) => surface.where);
|
|
3494
3523
|
return null;
|
|
3495
3524
|
};
|
|
@@ -3516,6 +3545,8 @@ async function recordClip(options, script, touched) {
|
|
|
3516
3545
|
return true;
|
|
3517
3546
|
};
|
|
3518
3547
|
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3548
|
+
let redirected;
|
|
3549
|
+
const isOn = (route, where) => onScreen(route, where) || redirected?.route === route && where.split("?")[0] === redirected.path;
|
|
3519
3550
|
const manifestOf = (raw, step, where) => buildLayoutManifest(raw, {
|
|
3520
3551
|
shotId: script.id,
|
|
3521
3552
|
route: step.route,
|
|
@@ -3534,12 +3565,13 @@ async function recordClip(options, script, touched) {
|
|
|
3534
3565
|
}
|
|
3535
3566
|
};
|
|
3536
3567
|
let resting = START;
|
|
3568
|
+
let lastRegion;
|
|
3537
3569
|
const find = async (step, where, filming) => {
|
|
3538
3570
|
const raw = await layout(cover);
|
|
3539
|
-
const shown = resolveStep(manifestOf(raw, step, where), step);
|
|
3571
|
+
const shown = resolveStep(manifestOf(raw, step, where), step, { after: lastRegion });
|
|
3540
3572
|
if (shown.ok) return shown;
|
|
3541
|
-
const whole = resolveStep(manifestOf(wholePage(raw), step, where), step);
|
|
3542
|
-
if (!whole.ok) return shown;
|
|
3573
|
+
const whole = resolveStep(manifestOf(wholePage(raw), step, where), step, { after: lastRegion });
|
|
3574
|
+
if (!whole.ok) return whole.kind === "missing" ? shown : whole;
|
|
3543
3575
|
const box = whole.control.box;
|
|
3544
3576
|
const by = Math.round(box.y - WHOLE_SHIFT + box.height / 2 - CLIP_VIEWPORT.height * SCROLL_TO);
|
|
3545
3577
|
const wheel = { x: Math.round(Math.min(Math.max(box.x + box.width / 2, 1), CLIP_VIEWPORT.width - 1)), y: Math.round(CLIP_VIEWPORT.height / 2) };
|
|
@@ -3560,15 +3592,20 @@ async function recordClip(options, script, touched) {
|
|
|
3560
3592
|
} else {
|
|
3561
3593
|
await steady();
|
|
3562
3594
|
}
|
|
3563
|
-
return resolveStep(manifestOf(await layout(cover), step, where), step);
|
|
3595
|
+
return resolveStep(manifestOf(await layout(cover), step, where), step, { after: lastRegion });
|
|
3564
3596
|
};
|
|
3565
|
-
const
|
|
3566
|
-
|
|
3567
|
-
|
|
3597
|
+
const open = async (route, opens, where) => {
|
|
3598
|
+
if (!opens || isOn(route, where)) return where;
|
|
3599
|
+
await page.goto(new URL(opens, options.baseUrl).toString());
|
|
3600
|
+
return location2();
|
|
3601
|
+
};
|
|
3602
|
+
const reach = async (route, opens) => {
|
|
3603
|
+
let where = await open(route, opens, await location2());
|
|
3604
|
+
for (let waited = 0; !isOn(route, where) && waited < STEP_WAIT_MS; waited += FRAME_MS) {
|
|
3568
3605
|
await pause(FRAME_MS);
|
|
3569
3606
|
where = await location2();
|
|
3570
3607
|
}
|
|
3571
|
-
if (
|
|
3608
|
+
if (isOn(route, where)) {
|
|
3572
3609
|
await page.quiet?.(SETTLE_MS);
|
|
3573
3610
|
await steady();
|
|
3574
3611
|
}
|
|
@@ -3579,22 +3616,29 @@ async function recordClip(options, script, touched) {
|
|
|
3579
3616
|
await steady();
|
|
3580
3617
|
return location2();
|
|
3581
3618
|
};
|
|
3582
|
-
const
|
|
3619
|
+
const focus = (at2) => pointer({ kind: "focus", at: at2 }).then(
|
|
3620
|
+
() => null,
|
|
3621
|
+
(error) => error instanceof Error ? error.message : String(error)
|
|
3622
|
+
);
|
|
3623
|
+
const performedOn = (step, where) => isOn(step.route, where) ? step.route : [...script.before ?? [], ...script.steps].map((each) => each.route).find((route) => isOn(route, where)) ?? where.split("?")[0];
|
|
3583
3624
|
const lead = async (step) => {
|
|
3584
|
-
const where = step.follows ? await settled() : await reach(step.route);
|
|
3585
|
-
if (!step.follows && !
|
|
3625
|
+
const where = step.follows ? await settled() : await reach(step.route, step.opens);
|
|
3626
|
+
if (!step.follows && !isOn(step.route, where)) return `the browser is on ${where}, not ${step.route}`;
|
|
3586
3627
|
let resolution = await find(step, where, false);
|
|
3587
3628
|
for (let waited = 0; !resolution.ok && waited < STEP_WAIT_MS; waited += FRAME_MS) {
|
|
3588
3629
|
await pause(FRAME_MS);
|
|
3589
3630
|
resolution = await find(step, where, false);
|
|
3590
3631
|
}
|
|
3591
3632
|
if (!resolution.ok) return resolution.because;
|
|
3633
|
+
lastRegion = resolution.control.region;
|
|
3592
3634
|
const target = centreOf(resolution.control.box);
|
|
3593
3635
|
const action = step.action === "auto" ? actionForRole(resolution.control.role, step.value) : step.action;
|
|
3636
|
+
const typed = step.value ?? step.demo;
|
|
3594
3637
|
if (action === "fill") {
|
|
3595
|
-
if (!
|
|
3596
|
-
|
|
3597
|
-
|
|
3638
|
+
if (!typed) return `it types into ${resolution.control.name}, and says nothing to type`;
|
|
3639
|
+
const unfocused = await focus(target);
|
|
3640
|
+
if (unfocused) return unfocused;
|
|
3641
|
+
await type(typed);
|
|
3598
3642
|
} else if (action === "select") {
|
|
3599
3643
|
await pointer({ kind: "select", at: target, value: step.value ?? "" });
|
|
3600
3644
|
} else {
|
|
@@ -3608,26 +3652,29 @@ async function recordClip(options, script, touched) {
|
|
|
3608
3652
|
if (await page.isSignIn()) return refuse("its first screen asked for a sign-in, so this session is not the demo tenant's");
|
|
3609
3653
|
const opened = await location2();
|
|
3610
3654
|
const performedFirst = script.before?.[0] ?? script.steps[0];
|
|
3611
|
-
if (!onScreen(performedFirst.route, opened))
|
|
3655
|
+
if (!onScreen(performedFirst.route, opened)) {
|
|
3656
|
+
if (!options.screens?.some((route) => onScreen(route, opened))) return fail(`it opened on ${opened}, not ${performedFirst.route}`);
|
|
3657
|
+
redirected = { route: performedFirst.route, path: opened.split("?")[0] };
|
|
3658
|
+
}
|
|
3612
3659
|
for (const [index, step] of (script.before ?? []).entries()) {
|
|
3613
3660
|
const problem = await lead(step);
|
|
3614
3661
|
if (problem) return fail(`before the recording, step ${index + 1}: ${problem}`);
|
|
3615
3662
|
}
|
|
3616
3663
|
const first = script.steps[0];
|
|
3617
|
-
const starting = !script.before?.length ? opened : first.follows ? await settled() : await reach(first.route);
|
|
3618
|
-
if (!first.follows && !
|
|
3664
|
+
const starting = !script.before?.length ? opened : first.follows ? await settled() : await reach(first.route, first.opens);
|
|
3665
|
+
if (!first.follows && !isOn(first.route, starting)) return fail(`once the steps before it were done, the browser is on ${starting}, not ${first.route}`);
|
|
3619
3666
|
const before = await judge(starting);
|
|
3620
|
-
if (before) return
|
|
3667
|
+
if (before) return before;
|
|
3621
3668
|
let poster;
|
|
3622
3669
|
const steps = [];
|
|
3623
|
-
const arrive = async (route, follows = false) => {
|
|
3624
|
-
let where = await location2();
|
|
3625
|
-
for (let waited = 0; !follows && !
|
|
3670
|
+
const arrive = async (route, follows = false, opens) => {
|
|
3671
|
+
let where = follows ? await location2() : await open(route, opens, await location2());
|
|
3672
|
+
for (let waited = 0; !follows && !isOn(route, where) && waited < STEP_WAIT_MS; waited += FRAME_MS) {
|
|
3626
3673
|
await cursor(resting);
|
|
3627
3674
|
await film();
|
|
3628
3675
|
where = await location2();
|
|
3629
3676
|
}
|
|
3630
|
-
if (!follows && !
|
|
3677
|
+
if (!follows && !isOn(route, where)) return where;
|
|
3631
3678
|
await page.quiet?.(SETTLE_MS);
|
|
3632
3679
|
await cursor(resting);
|
|
3633
3680
|
for (let waited = 0; waited < SETTLE_MS; waited += FRAME_MS) {
|
|
@@ -3638,10 +3685,10 @@ async function recordClip(options, script, touched) {
|
|
|
3638
3685
|
await cursor(resting);
|
|
3639
3686
|
checked.add(await shoot(OPENING_MS));
|
|
3640
3687
|
for (const [index, step] of script.steps.entries()) {
|
|
3641
|
-
const where = await arrive(step.route, step.follows === true);
|
|
3642
|
-
if (!step.follows && !
|
|
3688
|
+
const where = await arrive(step.route, step.follows === true, step.opens);
|
|
3689
|
+
if (!step.follows && !isOn(step.route, where)) return fail(`step ${index + 1}: the browser is on ${where}, not ${step.route}`);
|
|
3643
3690
|
const arrived = await judge(where);
|
|
3644
|
-
if (arrived) return
|
|
3691
|
+
if (arrived) return arrived;
|
|
3645
3692
|
let resolution = await find(step, where, true);
|
|
3646
3693
|
for (let waited = 0; !resolution.ok && waited < STEP_WAIT_MS; waited += FRAME_MS) {
|
|
3647
3694
|
await cursor(resting);
|
|
@@ -3649,10 +3696,12 @@ async function recordClip(options, script, touched) {
|
|
|
3649
3696
|
resolution = await find(step, where, true);
|
|
3650
3697
|
}
|
|
3651
3698
|
if (!resolution.ok) return fail(`step ${index + 1}: ${resolution.because}`);
|
|
3699
|
+
lastRegion = resolution.control.region;
|
|
3652
3700
|
const target = centreOf(resolution.control.box);
|
|
3653
3701
|
const startedAt = at;
|
|
3654
3702
|
const action = step.action === "auto" ? actionForRole(resolution.control.role, step.value) : step.action;
|
|
3655
|
-
|
|
3703
|
+
const typed = step.value ?? step.demo;
|
|
3704
|
+
if (action === "fill" && !typed) return fail(`step ${index + 1}: it types into ${resolution.control.name}, and says nothing to type`);
|
|
3656
3705
|
for (const point of cursorPath(resting, target, MOVE_FRAMES)) {
|
|
3657
3706
|
await cursor(point);
|
|
3658
3707
|
await shoot(FRAME_MS);
|
|
@@ -3663,8 +3712,9 @@ async function recordClip(options, script, touched) {
|
|
|
3663
3712
|
await cursor(target, true);
|
|
3664
3713
|
await shoot(PRESS_MS);
|
|
3665
3714
|
if (action === "fill") {
|
|
3666
|
-
const value =
|
|
3667
|
-
|
|
3715
|
+
const value = typed ?? "";
|
|
3716
|
+
const unfocused = await focus(target);
|
|
3717
|
+
if (unfocused) return fail(`step ${index + 1}: ${unfocused}`);
|
|
3668
3718
|
await cursor(target, false);
|
|
3669
3719
|
for (const chunk of chunksOf(value, Math.max(CHARS_PER_FRAME, Math.ceil(value.length / TYPING_FRAMES)))) {
|
|
3670
3720
|
await type(chunk);
|
|
@@ -3685,7 +3735,7 @@ async function recordClip(options, script, touched) {
|
|
|
3685
3735
|
checked.add(frames[frames.length - 1].file);
|
|
3686
3736
|
const now = await location2();
|
|
3687
3737
|
const after = await judge(now);
|
|
3688
|
-
if (after) return
|
|
3738
|
+
if (after) return after;
|
|
3689
3739
|
steps.push({
|
|
3690
3740
|
route: performedOn(step, where),
|
|
3691
3741
|
role: resolution.control.role,
|
|
@@ -3698,9 +3748,9 @@ async function recordClip(options, script, touched) {
|
|
|
3698
3748
|
}
|
|
3699
3749
|
if (script.lands) {
|
|
3700
3750
|
const landed = await arrive(script.lands);
|
|
3701
|
-
if (!
|
|
3751
|
+
if (!isOn(script.lands, landed)) return fail(`the last step left the browser on ${landed}, not ${script.lands}`);
|
|
3702
3752
|
const seen = await judge(landed);
|
|
3703
|
-
if (seen) return
|
|
3753
|
+
if (seen) return seen;
|
|
3704
3754
|
}
|
|
3705
3755
|
hold(CLOSING_MS);
|
|
3706
3756
|
checked.add(frames[frames.length - 1].file);
|
|
@@ -3761,7 +3811,7 @@ async function runClips(options) {
|
|
|
3761
3811
|
} else if (!options.force && options.held.get(script.id) === fingerprint) {
|
|
3762
3812
|
outcome = { ...base, status: "unchanged", reason: "neither its steps nor a screen they touch has changed" };
|
|
3763
3813
|
} else {
|
|
3764
|
-
const result = await recordClip(options, script, touched);
|
|
3814
|
+
const result = await recordClip({ ...options, screens: options.screens ?? [...options.frames.keys()] }, script, touched);
|
|
3765
3815
|
if (!result.ok) {
|
|
3766
3816
|
outcome = { ...base, status: result.status, reason: result.because };
|
|
3767
3817
|
} else {
|
|
@@ -4952,11 +5002,21 @@ function articleClipsToRecord(raw, examples) {
|
|
|
4952
5002
|
if (!Array.isArray(raw)) return readClipScripts(raw, CLIP_MAX_ARTICLE_SCRIPTS);
|
|
4953
5003
|
const named = examples ?? { paths: /* @__PURE__ */ new Map(), refused: [] };
|
|
4954
5004
|
const unopened = [];
|
|
5005
|
+
const recordOf3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
5006
|
+
const withOpens = (steps, from) => Array.isArray(steps) ? steps.map((value, index) => {
|
|
5007
|
+
const step = recordOf3(value);
|
|
5008
|
+
const later = from + index > 0 && step["follows"] !== true;
|
|
5009
|
+
const resolved = later && typeof step["route"] === "string" ? resolveVisit(step["route"], named) : void 0;
|
|
5010
|
+
return resolved && "visit" in resolved && resolved.visit.includes("?") ? { ...step, opens: resolved.visit } : value;
|
|
5011
|
+
}) : steps;
|
|
4955
5012
|
const opened = raw.map((value) => {
|
|
4956
|
-
const script =
|
|
5013
|
+
const script = recordOf3(value);
|
|
4957
5014
|
const visit = typeof script["visit"] === "string" ? script["visit"] : "";
|
|
4958
5015
|
const resolved = resolveVisit(visit, named);
|
|
4959
|
-
|
|
5016
|
+
const lead = Array.isArray(script["before"]) ? script["before"].length : 0;
|
|
5017
|
+
if (!("needs" in resolved)) {
|
|
5018
|
+
return { ...script, visit: resolved.visit, ...lead ? { before: withOpens(script["before"], 0) } : {}, steps: withOpens(script["steps"], lead) };
|
|
5019
|
+
}
|
|
4960
5020
|
unopened.push({ id: typeof script["id"] === "string" ? script["id"] : "a clip", because: describeMissingExample(visit, resolved.needs) });
|
|
4961
5021
|
return void 0;
|
|
4962
5022
|
});
|