@vosjs/render-core 0.9.0 → 0.11.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 +199 -97
- package/dist/record/index.js +211 -49
- package/dist/record/index.js.map +1 -1
- package/package.json +2 -2
package/dist/record/index.js
CHANGED
|
@@ -689,7 +689,8 @@ function noticeAdvice(notices) {
|
|
|
689
689
|
}
|
|
690
690
|
|
|
691
691
|
// src/record/setup.ts
|
|
692
|
-
var VERBS = /* @__PURE__ */ new Set(["wait", "click", "type", "press", "goto"]);
|
|
692
|
+
var VERBS = /* @__PURE__ */ new Set(["wait", "click", "type", "press", "goto", "if"]);
|
|
693
|
+
var IF_LOOK_MS = 3e3;
|
|
693
694
|
function urlSchemeProblem(url) {
|
|
694
695
|
let u;
|
|
695
696
|
try {
|
|
@@ -701,11 +702,11 @@ function urlSchemeProblem(url) {
|
|
|
701
702
|
return `${url} is not an http(s) address, and the recorder opens only http and https pages`;
|
|
702
703
|
return null;
|
|
703
704
|
}
|
|
704
|
-
function validateSetup(value) {
|
|
705
|
+
function validateSetup(value, phase = "setup") {
|
|
705
706
|
const errors = [];
|
|
706
|
-
if (!Array.isArray(value)) return [
|
|
707
|
+
if (!Array.isArray(value)) return [`${phase} must be an array of steps`];
|
|
707
708
|
value.forEach((raw, i) => {
|
|
708
|
-
const at =
|
|
709
|
+
const at = `${phase}[${i}]`;
|
|
709
710
|
if (typeof raw !== "object" || raw === null) {
|
|
710
711
|
errors.push(`${at}: must be an object`);
|
|
711
712
|
return;
|
|
@@ -715,7 +716,7 @@ function validateSetup(value) {
|
|
|
715
716
|
errors.push(`${at}: "do" must be one of ${[...VERBS].join(", ")}`);
|
|
716
717
|
return;
|
|
717
718
|
}
|
|
718
|
-
if ((s.do === "click" || s.do === "type") && typeof s.selector !== "string")
|
|
719
|
+
if ((s.do === "click" || s.do === "type" || s.do === "if") && typeof s.selector !== "string")
|
|
719
720
|
errors.push(`${at}: ${s.do} needs a selector`);
|
|
720
721
|
if (s.do === "wait" && typeof s.ms !== "number")
|
|
721
722
|
errors.push(`${at}: wait needs ms`);
|
|
@@ -750,73 +751,158 @@ function setupEnvNames(steps) {
|
|
|
750
751
|
}
|
|
751
752
|
return [...out];
|
|
752
753
|
}
|
|
753
|
-
function
|
|
754
|
+
function scriptEnvNames(script) {
|
|
755
|
+
return [
|
|
756
|
+
.../* @__PURE__ */ new Set([
|
|
757
|
+
...setupEnvNames(script.setup),
|
|
758
|
+
...setupEnvNames(script.teardown)
|
|
759
|
+
])
|
|
760
|
+
];
|
|
761
|
+
}
|
|
762
|
+
function resolveSetupText(t, env, phase = "setup") {
|
|
754
763
|
if (typeof t === "string") return { value: t, secret: false };
|
|
755
764
|
const v = env[t.env];
|
|
756
765
|
if (v === void 0 || v === "")
|
|
757
766
|
throw new SetupEnvError(
|
|
758
|
-
|
|
767
|
+
`${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
768
|
);
|
|
760
769
|
return { value: v, secret: true };
|
|
761
770
|
}
|
|
771
|
+
function teardownEnvProblem(steps, env) {
|
|
772
|
+
for (const [i, s] of (steps ?? []).entries()) {
|
|
773
|
+
if (s.do !== "type" || typeof s.text !== "object") continue;
|
|
774
|
+
const v = env[s.text.env];
|
|
775
|
+
if (v === void 0 || v === "")
|
|
776
|
+
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.`;
|
|
777
|
+
}
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
var TEARDOWN_BOUND_MS = 6e4;
|
|
781
|
+
function teardownLine(report) {
|
|
782
|
+
const f = report.failed;
|
|
783
|
+
const k = report.skipped;
|
|
784
|
+
if (!f && k)
|
|
785
|
+
return `teardown: nothing to undo, ${k.selector} is not on the page (teardown[${k.step}] if); ${report.ran} step(s) ran, ${k.rest} skipped`;
|
|
786
|
+
if (!f) return `teardown: ${report.ran} step(s) ran`;
|
|
787
|
+
const reason = f.error.replace(/[.\s]+$/, "");
|
|
788
|
+
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.`;
|
|
789
|
+
}
|
|
762
790
|
async function runSetup(page, steps, opts) {
|
|
763
791
|
const env = opts.env ?? {};
|
|
792
|
+
const phase = opts.phase ?? "setup";
|
|
793
|
+
const now = opts.now ?? Date.now;
|
|
794
|
+
const deadline = opts.boundMs === void 0 ? void 0 : now() + opts.boundMs;
|
|
764
795
|
const lines = [];
|
|
765
|
-
|
|
796
|
+
let typed = "";
|
|
797
|
+
let gone = null;
|
|
798
|
+
const runStep = async (step, i) => {
|
|
766
799
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
}
|
|
800
|
+
switch (step.do) {
|
|
801
|
+
case "wait":
|
|
802
|
+
await opts.sleep(step.ms);
|
|
803
|
+
return `${phase} #${i} wait ${step.ms}ms`;
|
|
804
|
+
case "goto":
|
|
805
|
+
await (opts.goto ? opts.goto(step.url) : page.goto(step.url, {
|
|
806
|
+
waitUntil: "networkidle",
|
|
807
|
+
timeout: 45e3
|
|
808
|
+
})).catch(() => null);
|
|
809
|
+
return `${phase} #${i} goto ${step.url}`;
|
|
810
|
+
case "press":
|
|
811
|
+
await page.keyboard.press(step.key);
|
|
812
|
+
await opts.sleep(settle);
|
|
813
|
+
return `${phase} #${i} press ${step.key}`;
|
|
814
|
+
case "if": {
|
|
815
|
+
const present = await page.locator(step.selector).first().waitFor({ state: "visible", timeout: step.ms ?? IF_LOOK_MS }).then(() => true).catch(() => false);
|
|
816
|
+
if (present) return `${phase} #${i} if ${step.selector}: on the page`;
|
|
817
|
+
gone = { step: i, selector: step.selector };
|
|
818
|
+
return `${phase} #${i} if ${step.selector}: not on the page, the ${steps.length - i - 1} step(s) after it skipped`;
|
|
819
|
+
}
|
|
820
|
+
case "click": {
|
|
821
|
+
const loc = page.locator(step.selector).first();
|
|
822
|
+
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
823
|
+
await loc.click();
|
|
824
|
+
await opts.sleep(settle);
|
|
825
|
+
return `${phase} #${i} click ${step.selector}`;
|
|
804
826
|
}
|
|
827
|
+
case "type": {
|
|
828
|
+
const loc = page.locator(step.selector).first();
|
|
829
|
+
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
830
|
+
const { value, secret } = resolveSetupText(step.text, env, phase);
|
|
831
|
+
typed = value;
|
|
832
|
+
await loc.fill(value);
|
|
833
|
+
await opts.sleep(settle);
|
|
834
|
+
return `${phase} #${i} type ${secret ? `\${${step.text.env}}` : `${value.length} chars`} into ${step.selector}`;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
for (const [i, step] of steps.entries()) {
|
|
839
|
+
typed = "";
|
|
840
|
+
try {
|
|
841
|
+
lines.push(
|
|
842
|
+
await withinDeadline(
|
|
843
|
+
runStep(step, i),
|
|
844
|
+
deadline,
|
|
845
|
+
now,
|
|
846
|
+
`the ${phase} ran past its ${sayBound(opts.boundMs ?? 0)} bound`
|
|
847
|
+
)
|
|
848
|
+
);
|
|
805
849
|
} catch (e) {
|
|
806
|
-
if (e instanceof SetupEnvError) throw e;
|
|
850
|
+
if (e instanceof SetupEnvError && phase === "setup") throw e;
|
|
807
851
|
return {
|
|
808
852
|
ran: i,
|
|
809
853
|
lines,
|
|
810
854
|
failed: {
|
|
811
855
|
step: i,
|
|
812
856
|
do: step.do,
|
|
813
|
-
..."selector" in step ? { selector: step.selector } : {}
|
|
857
|
+
..."selector" in step ? { selector: step.selector } : {},
|
|
858
|
+
error: reasonWords(e, typed)
|
|
814
859
|
}
|
|
815
860
|
};
|
|
816
861
|
}
|
|
862
|
+
const ended = gone;
|
|
863
|
+
if (ended)
|
|
864
|
+
return {
|
|
865
|
+
ran: i + 1,
|
|
866
|
+
lines,
|
|
867
|
+
skipped: { ...ended, rest: steps.length - i - 1 }
|
|
868
|
+
};
|
|
817
869
|
}
|
|
818
870
|
return { ran: steps.length, lines };
|
|
819
871
|
}
|
|
872
|
+
function sayBound(ms) {
|
|
873
|
+
return ms >= 1e3 ? `${Math.round(ms / 1e3)} s` : `${ms} ms`;
|
|
874
|
+
}
|
|
875
|
+
function withinDeadline(p, deadline, now, message) {
|
|
876
|
+
if (deadline === void 0) return p;
|
|
877
|
+
const left = deadline - now();
|
|
878
|
+
return new Promise((resolve, reject) => {
|
|
879
|
+
const timer = setTimeout(
|
|
880
|
+
() => {
|
|
881
|
+
p.catch(() => {
|
|
882
|
+
});
|
|
883
|
+
reject(new Error(message));
|
|
884
|
+
},
|
|
885
|
+
Math.max(0, left)
|
|
886
|
+
);
|
|
887
|
+
p.then(
|
|
888
|
+
(v) => {
|
|
889
|
+
clearTimeout(timer);
|
|
890
|
+
resolve(v);
|
|
891
|
+
},
|
|
892
|
+
(e) => {
|
|
893
|
+
clearTimeout(timer);
|
|
894
|
+
reject(e);
|
|
895
|
+
}
|
|
896
|
+
);
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
var ANSI_COLOUR = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
900
|
+
function reasonWords(e, typed) {
|
|
901
|
+
const raw = e instanceof Error ? e.message : String(e);
|
|
902
|
+
const line = (raw.replace(ANSI_COLOUR, "").split("\n")[0] ?? "").trim();
|
|
903
|
+
if (typed && line.includes(typed)) return "the field could not be filled";
|
|
904
|
+
return line || "the step failed";
|
|
905
|
+
}
|
|
820
906
|
var SetupEnvError = class extends Error {
|
|
821
907
|
constructor(message) {
|
|
822
908
|
super(message);
|
|
@@ -2405,6 +2491,8 @@ function validateActions(value) {
|
|
|
2405
2491
|
}
|
|
2406
2492
|
}
|
|
2407
2493
|
if (obj.setup !== void 0) errors.push(...validateSetup(obj.setup));
|
|
2494
|
+
if (obj.teardown !== void 0)
|
|
2495
|
+
errors.push(...validateSetup(obj.teardown, "teardown"));
|
|
2408
2496
|
const ready = readyProblem(obj.ready);
|
|
2409
2497
|
if (ready) errors.push(ready);
|
|
2410
2498
|
if (obj.settle !== void 0 && typeof obj.settle !== "boolean")
|
|
@@ -3071,6 +3159,18 @@ function base64Bytes(data) {
|
|
|
3071
3159
|
return out;
|
|
3072
3160
|
}
|
|
3073
3161
|
async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
3162
|
+
const late = {};
|
|
3163
|
+
try {
|
|
3164
|
+
return await recordTakeSteps(browser, url, actions, sink, log, opts, late);
|
|
3165
|
+
} catch (e) {
|
|
3166
|
+
const teardown = late.teardown;
|
|
3167
|
+
late.teardown = void 0;
|
|
3168
|
+
if (teardown) await teardown().catch(() => {
|
|
3169
|
+
});
|
|
3170
|
+
throw e;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
async function recordTakeSteps(browser, url, actions, sink, log, opts, late) {
|
|
3074
3174
|
const dry = opts.dryRun === true;
|
|
3075
3175
|
const sleep = dry ? (ms) => realSleep(Math.min(ms, DRY_PAUSE_MS)) : realSleep;
|
|
3076
3176
|
const maxSeconds = dry ? Infinity : opts.maxDurationSeconds ?? Infinity;
|
|
@@ -3083,6 +3183,8 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3083
3183
|
`steps[${download}] is a download, and this recorder has nowhere to keep a file. Nothing was recorded.`
|
|
3084
3184
|
);
|
|
3085
3185
|
}
|
|
3186
|
+
const teardownEnv = teardownEnvProblem(actions.teardown, opts.env ?? {});
|
|
3187
|
+
if (teardownEnv) throw new SetupEnvError(teardownEnv);
|
|
3086
3188
|
const read = await loadScriptFiles(actions, opts.files);
|
|
3087
3189
|
if ("problem" in read) throw new RecordRefusedError(read.problem);
|
|
3088
3190
|
const files = read;
|
|
@@ -3126,8 +3228,9 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3126
3228
|
noticeLog.add(step, scan?.notices);
|
|
3127
3229
|
};
|
|
3128
3230
|
const pageErrors = new PageErrorLog(opts.verbose === true);
|
|
3231
|
+
let tearingDown = false;
|
|
3129
3232
|
page.on("console", (m) => {
|
|
3130
|
-
if (m.type() !== "error") return;
|
|
3233
|
+
if (m.type() !== "error" || tearingDown) return;
|
|
3131
3234
|
const line = pageErrors.add(m.text());
|
|
3132
3235
|
if (line) log(line);
|
|
3133
3236
|
});
|
|
@@ -3158,6 +3261,50 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3158
3261
|
log(`goto ${url} (after setup)`);
|
|
3159
3262
|
response = (await open(url)).response;
|
|
3160
3263
|
}
|
|
3264
|
+
const teardownSteps = actions.teardown ?? [];
|
|
3265
|
+
let teardown = null;
|
|
3266
|
+
let teardownRan = false;
|
|
3267
|
+
const runTeardown = async () => {
|
|
3268
|
+
if (!teardownSteps.length || teardownRan) return teardown;
|
|
3269
|
+
teardownRan = true;
|
|
3270
|
+
tearingDown = true;
|
|
3271
|
+
log(`teardown: ${teardownSteps.length} step(s), off camera`);
|
|
3272
|
+
try {
|
|
3273
|
+
const result = await runSetup(page, teardownSteps, {
|
|
3274
|
+
phase: "teardown",
|
|
3275
|
+
sleep: realSleep,
|
|
3276
|
+
env: opts.env ?? {},
|
|
3277
|
+
goto: (target) => open(target),
|
|
3278
|
+
boundMs: TEARDOWN_BOUND_MS
|
|
3279
|
+
});
|
|
3280
|
+
for (const line of result.lines) log(` ${line}`);
|
|
3281
|
+
teardown = {
|
|
3282
|
+
ran: result.ran,
|
|
3283
|
+
of: teardownSteps.length,
|
|
3284
|
+
...result.failed ? {
|
|
3285
|
+
failed: {
|
|
3286
|
+
step: result.failed.step,
|
|
3287
|
+
do: result.failed.do,
|
|
3288
|
+
...result.failed.selector !== void 0 ? { selector: result.failed.selector } : {},
|
|
3289
|
+
error: result.failed.error ?? "the step failed"
|
|
3290
|
+
}
|
|
3291
|
+
} : {},
|
|
3292
|
+
...result.skipped ? { skipped: result.skipped } : {}
|
|
3293
|
+
};
|
|
3294
|
+
} catch (e) {
|
|
3295
|
+
teardown = {
|
|
3296
|
+
ran: 0,
|
|
3297
|
+
of: teardownSteps.length,
|
|
3298
|
+
failed: {
|
|
3299
|
+
step: 0,
|
|
3300
|
+
do: teardownSteps[0].do,
|
|
3301
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3302
|
+
}
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
if (teardown.failed) log(` ${teardownLine(teardown)}`);
|
|
3306
|
+
return teardown;
|
|
3307
|
+
};
|
|
3161
3308
|
let wall = null;
|
|
3162
3309
|
if (opts.onArrival) {
|
|
3163
3310
|
const seen = await page.evaluate(WALL_PROBE).catch(() => null);
|
|
@@ -3468,6 +3615,13 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
3468
3615
|
})
|
|
3469
3616
|
);
|
|
3470
3617
|
signals.begin();
|
|
3618
|
+
late.teardown = async () => {
|
|
3619
|
+
if (!dry) await cdp.send("Page.stopScreencast").catch(() => {
|
|
3620
|
+
});
|
|
3621
|
+
pageBound.end();
|
|
3622
|
+
const report = await runTeardown();
|
|
3623
|
+
if (report && !report.failed) log(` ${teardownLine(report)}`);
|
|
3624
|
+
};
|
|
3471
3625
|
for (const [stepIdx, step] of actions.steps.entries()) {
|
|
3472
3626
|
if (capReached(now(), maxSeconds)) {
|
|
3473
3627
|
capped = true;
|
|
@@ -4179,6 +4333,8 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
4179
4333
|
const pageTitle = await page.title().catch(() => "") ?? "";
|
|
4180
4334
|
for (const line of pageErrors.repeats()) log(line);
|
|
4181
4335
|
pageBound.end();
|
|
4336
|
+
late.teardown = void 0;
|
|
4337
|
+
await runTeardown();
|
|
4182
4338
|
pageBound.start(CLOSE_BOUND_MS);
|
|
4183
4339
|
await pageBound.race(context.close().catch(() => {
|
|
4184
4340
|
}));
|
|
@@ -4265,7 +4421,8 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
4265
4421
|
// Self-contained: a path a file held is inline, so the take re-records
|
|
4266
4422
|
// from its own script with the file gone.
|
|
4267
4423
|
actions: { ...inlineScript(actions, files), url },
|
|
4268
|
-
peers: peerRuns
|
|
4424
|
+
peers: peerRuns,
|
|
4425
|
+
teardown
|
|
4269
4426
|
};
|
|
4270
4427
|
}
|
|
4271
4428
|
async function keepPageSound(heard, sink, fetchSource, log) {
|
|
@@ -4535,6 +4692,7 @@ export {
|
|
|
4535
4692
|
EXPOSURE_PROBE,
|
|
4536
4693
|
ExposureLog,
|
|
4537
4694
|
IDLE_CALL_MS,
|
|
4695
|
+
IF_LOOK_MS,
|
|
4538
4696
|
JS_SAID_MAX,
|
|
4539
4697
|
JS_VALUE_MAX,
|
|
4540
4698
|
KEY_ROUNDTRIP_MS,
|
|
@@ -4592,6 +4750,7 @@ export {
|
|
|
4592
4750
|
SetupError,
|
|
4593
4751
|
StepBound,
|
|
4594
4752
|
StepSignals,
|
|
4753
|
+
TEARDOWN_BOUND_MS,
|
|
4595
4754
|
TRAILING_HOLD_MS,
|
|
4596
4755
|
TakeClock,
|
|
4597
4756
|
WAIT_MARGIN_MS,
|
|
@@ -4681,6 +4840,7 @@ export {
|
|
|
4681
4840
|
sayJs,
|
|
4682
4841
|
sayValue,
|
|
4683
4842
|
schedulePath,
|
|
4843
|
+
scriptEnvNames,
|
|
4684
4844
|
scriptFiles,
|
|
4685
4845
|
scrollByScript,
|
|
4686
4846
|
scrollTravelMs,
|
|
@@ -4694,6 +4854,8 @@ export {
|
|
|
4694
4854
|
stageClipboardScript,
|
|
4695
4855
|
stepBoundMs,
|
|
4696
4856
|
syntheticPasteScript,
|
|
4857
|
+
teardownEnvProblem,
|
|
4858
|
+
teardownLine,
|
|
4697
4859
|
typeOfName,
|
|
4698
4860
|
untilReady,
|
|
4699
4861
|
urlSchemeProblem,
|