@vosjs/render-core 0.9.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.
@@ -416,6 +416,156 @@ declare function deadWarns(r: DeadReport): boolean;
416
416
  /** The dead time in words, naming the steps and what to cut. */
417
417
  declare function deadLine(r: DeadReport): string;
418
418
 
419
+ /**
420
+ * `setup`: the steps that run BEFORE the camera rolls. A sign-in form, a
421
+ * cookie banner, the "choose your editor" modal, an onboarding tour: things
422
+ * a take must get past and must not show. They run after the first
423
+ * navigation and before `Page.startScreencast`, with no cursor synthesis,
424
+ * no pace accounting and nothing written to `meta.steps`. Then the recorder
425
+ * navigates to `url` again and the take begins where the setup left it.
426
+ *
427
+ * A `type` step's `text` may be `{ "env": "DEMO_PASSWORD" }`: resolved at run
428
+ * time from the `env` the caller hands in (the shell's for the CLI, the
429
+ * job's own map for a hosted take), never logged, never stored. The
430
+ * guarantee is narrow and real: the secret is never in a file that travels
431
+ * (`actions.json` is committed and pushed with the take; `meta.json` is read
432
+ * by the digest) and never in the footage.
433
+ *
434
+ * `teardown`: the same steps, run AFTER the take, off camera too: what the
435
+ * take created in the product (an API key, a project, an invite) removed
436
+ * again, so a rehearsal or a re-record leaves the account as it found it.
437
+ * They run once the screencast has stopped, on the take's own page and
438
+ * context (a session's cookies apply), and also when the take stops early
439
+ * or throws once its first step has run. A teardown that fails never fails
440
+ * the take: the footage is fine, and what is left behind is reported.
441
+ */
442
+
443
+ type SetupText = string | {
444
+ env: string;
445
+ };
446
+ type SetupStep = ({
447
+ do: 'wait';
448
+ ms: number;
449
+ } | {
450
+ do: 'click';
451
+ selector: string;
452
+ ms?: number;
453
+ } | {
454
+ do: 'type';
455
+ selector: string;
456
+ text: SetupText;
457
+ ms?: number;
458
+ } | {
459
+ do: 'press';
460
+ key: string;
461
+ ms?: number;
462
+ } | {
463
+ do: 'goto';
464
+ url: string;
465
+ }) & {
466
+ id?: string;
467
+ };
468
+ /** Which list a step belongs to, as its messages and log lines name it. */
469
+ type StepPhase = 'setup' | 'teardown';
470
+ /**
471
+ * Why a page address is not one the recorder opens, or null. A script's
472
+ * `url` and a setup's `goto` open a browser, so only http and https are
473
+ * taken: a `file:` address would read this machine's disk into a take
474
+ * and a `javascript:` one would run in the page.
475
+ */
476
+ declare function urlSchemeProblem(url: string): string | null;
477
+ /**
478
+ * Structural validation, mirrored on `validateActions` for `steps`. The
479
+ * same verbs and rules hold for `teardown`; `phase` names the list.
480
+ */
481
+ declare function validateSetup(value: unknown, phase?: StepPhase): string[];
482
+ /** The names a setup reads from its env, so a host can say what it must supply. */
483
+ declare function setupEnvNames(steps: SetupStep[] | undefined): string[];
484
+ /**
485
+ * Every name a script reads from its env, its setup's and its teardown's,
486
+ * each once: what a host must supply before the take begins.
487
+ */
488
+ declare function scriptEnvNames(script: {
489
+ setup?: SetupStep[];
490
+ teardown?: SetupStep[];
491
+ }): string[];
492
+ /** A `{ env }` text resolved, or thrown in words. The value is never returned to a log. */
493
+ declare function resolveSetupText(t: SetupText, env: Record<string, string | undefined>, phase?: StepPhase): {
494
+ value: string;
495
+ secret: boolean;
496
+ };
497
+ /**
498
+ * The first `{ env }` a teardown reads that `env` does not hold, in words,
499
+ * or null. Asked BEFORE the take: a take that creates something its
500
+ * teardown cannot then remove is the one case teardown exists for.
501
+ */
502
+ declare function teardownEnvProblem(steps: SetupStep[] | undefined, env: Record<string, string | undefined>): string | null;
503
+ interface SetupResult {
504
+ ran: number;
505
+ /** what the log may say about each step: the verb and the selector, never a typed value */
506
+ lines: string[];
507
+ /**
508
+ * The step that failed: for a setup, a selector that never appeared (the
509
+ * take does not start); for a teardown, the step it stopped at. `error`
510
+ * is the driver's reason, one line, never a typed value.
511
+ */
512
+ failed?: {
513
+ step: number;
514
+ do: string;
515
+ selector?: string;
516
+ error?: string;
517
+ };
518
+ }
519
+ /** How long a whole teardown may take before it is stopped and reported. */
520
+ declare const TEARDOWN_BOUND_MS = 60000;
521
+ /**
522
+ * What a teardown did, on the take's result: how many of its steps ran,
523
+ * and where it stopped. Never a typed value.
524
+ */
525
+ interface TeardownReport {
526
+ ran: number;
527
+ of: number;
528
+ failed?: {
529
+ step: number;
530
+ do: string;
531
+ selector?: string;
532
+ error: string;
533
+ };
534
+ }
535
+ /** The teardown in one line, for the end of a take or a rehearsal. */
536
+ declare function teardownLine(report: TeardownReport): string;
537
+ /**
538
+ * Run the setup (or the teardown) on a page, with plain Playwright actions:
539
+ * no humanized pointer, no cursor events, no frames. A selector that never
540
+ * appears fails the steps there, because a take that begins at a
541
+ * half-finished sign-in is the wall by another name. With `boundMs`, the
542
+ * steps together run no longer than that: the step running when it passes
543
+ * fails, and the call it left pending is abandoned.
544
+ */
545
+ declare function runSetup(page: RecorderPage, steps: SetupStep[], opts: {
546
+ sleep: (ms: number) => Promise<void>;
547
+ /** Absent means no env at all: a `{ env }` text then fails in words. */
548
+ env?: Record<string, string | undefined>;
549
+ /** How a `goto` opens its page and waits for it (the take's ready mode); absent, network idle. */
550
+ goto?: (url: string) => Promise<unknown>;
551
+ /** Which list these are, for the log and the messages (default `setup`). */
552
+ phase?: StepPhase;
553
+ /** The longest the steps may take together, in ms. */
554
+ boundMs?: number;
555
+ now?: () => number;
556
+ }): Promise<SetupResult>;
557
+ /** A `{ env }` the shell did not provide: a usage error, not a page's. */
558
+ declare class SetupEnvError extends Error {
559
+ constructor(message: string);
560
+ }
561
+ /** A setup step whose selector never appeared: the take does not start. */
562
+ declare class SetupError extends Error {
563
+ failed: NonNullable<SetupResult['failed']>;
564
+ constructor(failed: NonNullable<SetupResult['failed']>);
565
+ }
566
+ /** `--header name=value`, repeatable: extra request headers on every request. */
567
+ declare function parseHeaders(given: string[] | undefined): Record<string, string>;
568
+
419
569
  /**
420
570
  * The page's own console errors, as the recorder reports them. A page
421
571
  * under recording is someone else's, and its errors are context, never the
@@ -564,99 +714,6 @@ declare function noticeLine(n: Notice): string;
564
714
  */
565
715
  declare function noticeAdvice(notices: readonly Pick<Notice, 'kind'>[]): string;
566
716
 
567
- /**
568
- * `setup`: the steps that run BEFORE the camera rolls. A sign-in form, a
569
- * cookie banner, the "choose your editor" modal, an onboarding tour: things
570
- * a take must get past and must not show. They run after the first
571
- * navigation and before `Page.startScreencast`, with no cursor synthesis,
572
- * no pace accounting and nothing written to `meta.steps`. Then the recorder
573
- * navigates to `url` again and the take begins where the setup left it.
574
- *
575
- * A `type` step's `text` may be `{ "env": "DEMO_PASSWORD" }`: resolved at run
576
- * time from the `env` the caller hands in (the shell's for the CLI, the
577
- * job's own map for a hosted take), never logged, never stored. The
578
- * guarantee is narrow and real: the secret is never in a file that travels
579
- * (`actions.json` is committed and pushed with the take; `meta.json` is read
580
- * by the digest) and never in the footage.
581
- */
582
-
583
- type SetupText = string | {
584
- env: string;
585
- };
586
- type SetupStep = ({
587
- do: 'wait';
588
- ms: number;
589
- } | {
590
- do: 'click';
591
- selector: string;
592
- ms?: number;
593
- } | {
594
- do: 'type';
595
- selector: string;
596
- text: SetupText;
597
- ms?: number;
598
- } | {
599
- do: 'press';
600
- key: string;
601
- ms?: number;
602
- } | {
603
- do: 'goto';
604
- url: string;
605
- }) & {
606
- id?: string;
607
- };
608
- /**
609
- * Why a page address is not one the recorder opens, or null. A script's
610
- * `url` and a setup's `goto` open a browser, so only http and https are
611
- * taken: a `file:` address would read this machine's disk into a take
612
- * and a `javascript:` one would run in the page.
613
- */
614
- declare function urlSchemeProblem(url: string): string | null;
615
- /** Structural validation, mirrored on `validateActions` for `steps`. */
616
- declare function validateSetup(value: unknown): string[];
617
- /** The names a setup reads from its env, so a host can say what it must supply. */
618
- declare function setupEnvNames(steps: SetupStep[] | undefined): string[];
619
- /** A `{ env }` text resolved, or thrown in words. The value is never returned to a log. */
620
- declare function resolveSetupText(t: SetupText, env: Record<string, string | undefined>): {
621
- value: string;
622
- secret: boolean;
623
- };
624
- interface SetupResult {
625
- ran: number;
626
- /** what the log may say about each step: the verb and the selector, never a typed value */
627
- lines: string[];
628
- /** a selector that never appeared; the take does not start */
629
- failed?: {
630
- step: number;
631
- do: string;
632
- selector?: string;
633
- };
634
- }
635
- /**
636
- * Run the setup on a page, with plain Playwright actions: no humanized
637
- * pointer, no cursor events, no frames. A selector that never appears
638
- * fails the setup, because a take that begins at a half-finished sign-in is
639
- * the wall by another name.
640
- */
641
- declare function runSetup(page: RecorderPage, steps: SetupStep[], opts: {
642
- sleep: (ms: number) => Promise<void>;
643
- /** Absent means no env at all: a `{ env }` text then fails in words. */
644
- env?: Record<string, string | undefined>;
645
- /** How a `goto` opens its page and waits for it (the take's ready mode); absent, network idle. */
646
- goto?: (url: string) => Promise<unknown>;
647
- }): Promise<SetupResult>;
648
- /** A `{ env }` the shell did not provide: a usage error, not a page's. */
649
- declare class SetupEnvError extends Error {
650
- constructor(message: string);
651
- }
652
- /** A setup step whose selector never appeared: the take does not start. */
653
- declare class SetupError extends Error {
654
- failed: NonNullable<SetupResult['failed']>;
655
- constructor(failed: NonNullable<SetupResult['failed']>);
656
- }
657
- /** `--header name=value`, repeatable: extra request headers on every request. */
658
- declare function parseHeaders(given: string[] | undefined): Record<string, string>;
659
-
660
717
  /**
661
718
  * When a page counts as ready for the take to begin. The default is the
662
719
  * DOM: after `load`, the page must make no DOM change for a beat (counted
@@ -1176,6 +1233,15 @@ interface ActionsFile {
1176
1233
  * or stored.
1177
1234
  */
1178
1235
  setup?: SetupStep[];
1236
+ /**
1237
+ * Run AFTER the take, off camera, with the same verbs as `setup`: what
1238
+ * the take created in the product (an API key, a project, an invite)
1239
+ * removed again. They run once the screencast has stopped, on the take's
1240
+ * own page, after a rehearsal too, and when the take stops early or
1241
+ * throws after its first step. A failing teardown never fails the take;
1242
+ * it stops at the step that failed and is reported.
1243
+ */
1244
+ teardown?: SetupStep[];
1179
1245
  /**
1180
1246
  * When the first page counts as ready for step 1: `dom` (default),
1181
1247
  * once its DOM has made no change for a beat after `load`, at most 8 s;
@@ -1683,6 +1749,11 @@ interface RecordResult {
1683
1749
  actions: ActionsFile;
1684
1750
  /** What each peer did, off camera (also `meta.peers`); [] with none. */
1685
1751
  peers: PeerRun[];
1752
+ /**
1753
+ * What the script's `teardown` did after the take, off camera; null when
1754
+ * it has none. A failure here is reported and never fails the take.
1755
+ */
1756
+ teardown: TeardownReport | null;
1686
1757
  }
1687
1758
  interface RecordOpts {
1688
1759
  /** Stop the capture at this many seconds (the hosted cap). */
@@ -1721,9 +1792,10 @@ interface RecordOpts {
1721
1792
  */
1722
1793
  context?: RecorderContext;
1723
1794
  /**
1724
- * The environment a setup's `{ env }` texts resolve from: the shell's
1725
- * for the CLI, the job's own map on the fleet. Absent means no env at
1726
- * all, so a setup that reads one fails in words.
1795
+ * The environment a setup's and a teardown's `{ env }` texts resolve
1796
+ * from: the shell's for the CLI, the job's own map on the fleet. Absent
1797
+ * means no env at all, so a setup that reads one fails in words, and a
1798
+ * teardown that reads one refuses the take before it begins.
1727
1799
  */
1728
1800
  env?: Record<string, string | undefined>;
1729
1801
  /** The platform the take is recorded on, for `meta.platform`. */
@@ -2267,4 +2339,4 @@ declare function inlinePoints(path: {
2267
2339
  */
2268
2340
  declare function inlineScript<T extends Pick<ActionsFile, 'steps'>>(actions: T, files: LoadedFiles): T;
2269
2341
 
2270
- export { APP_QUIET_MS, APP_SETTLE_CAP_MS, AWAIT_HEARTBEAT_MS, AWAIT_POLL_S, AWAIT_TIMEOUT_S, type ActionStep, type ActionsFile, type Arrival, type AwaitUntil, CHANGE_AGE_PROBE, CLICK_BUTTONS, type ClickButton, type ConditionState, DEAD_SHARE_WARN_PCT, DEAD_STRETCH_WARN_MS, DEFAULT_READY, DOM_CHANGE_PROBE, DOM_QUIET_MS, DOM_SETTLE_CAP_MS, DOWNLOAD_TIMEOUT_S, DRAG_HOLD_MS, DRAG_MODIFIERS, type DeadReport, type DeadStep, type DisabledClick, type DragModifier, EXPECT_TIMEOUT_S, EXPOSURE_ADVICE, EXPOSURE_MATCHERS_SRC, EXPOSURE_PROBE, type EncodePageOptions, type Exposure, type ExposureKind, ExposureLog, type FrameRec, type FrameSink, type FreezeSpan, type FreshPlanOptions, type GuardedStep, type HeardSound, IDLE_CALL_MS, JS_SAID_MAX, JS_VALUE_MAX, KEY_ROUNDTRIP_MS, KEY_SEQUENCE_GAP_MS, type LoadedFiles, MAX_KEYS, MAX_PATH_POINTS, MAX_PEERS, MAX_PEER_STEPS, MEDIA_DRAIN_PROBE, MEDIA_WATCH_SCRIPT, MIN_SKIP_MS, MODIFIERS, MOTION_TICK_MS, MULTI_CLICK_GAP_MS, type MaskReport, type MaskRule, type MediaDrain, type MediaNote, type Modifier, NAV_TIMEOUT_MS, NETWORKIDLE_HYDRATION_MS, NOTICE_SRC, type Notice, NoticeLog, OUTPUT_MAX_BYTES, OVERRUN_MIN_MS, OVERRUN_MIN_PLANNED_MS, OVERRUN_SHARE, type OutputSink, PAGE_ERROR_LINE_CHARS, PASTE_MAX_BYTES, PASTE_READ_WINDOW_MS, PASTE_TYPES, PASTE_UNREAD_MS, PATH_DEFAULT_MS, PEER_VALUE_MAX, PEER_VERBS, POINTER_MAX_MS, POINTER_MIN_MS, POINTER_MS_PER_PX, PRESS_HOLD_MS, PRESS_KEYS, PRESS_LEAD_MS, type PaceReport, type PageError, PageErrorLog, PageSoundLog, type PasteItem, type PathPoint, type PathSample, type Peer, type PeerRun, type PeerStep, type PollResult, type PressKey, QUIET_MS, READY_MODES, READY_SETTLE_CAP_MS, READ_BEAT_MS, RESPONSE_MS, type ReadyMode, type ReadyOpts, type ReadyReport, type RecordOpts, RecordRefusedError, type RecordResult, type RecorderApiResponse, type RecorderBrowser, type RecorderButtonOpts, type RecorderCdp, type RecorderClock, type RecorderConsoleMessage, type RecorderContext, type RecorderDownload, type RecorderLocator, type RecorderLocatorRoot, type RecorderMediaElement, type RecorderPage, type RecorderRect, type RecorderResponse, type RecorderRoute, SCROLL_MAX_MS, SCROLL_MIN_MS, SCROLL_MS_PER_PX, SCROLL_SETTLE_MS, SETTLE_CAP_MS, SETTLE_MS, SETTLE_VERBS, STEP_GUARDS, STEP_MARGIN_MS, type ScannedNotice, type ScriptFiles, SetupEnvError, SetupError, type SetupResult, type SetupStep, type SetupText, type SkippedStep, type SoundSink, StepBound, type StepGuard, type StepOverrun, type StepPace, StepSignals, type StoppedTake, type SurfaceFit, type SurfaceSize, TRAILING_HOLD_MS, TakeClock, WAIT_MARGIN_MS, WALL_PROBE, type WaitCondition, WallError, type WallKind, type WallVerdict, ZOOM_MARK_VERBS, appSettled, askedMs, base64Bytes, bounded, buildEncodePage, buttonNumber, capReached, cappedLine, chordProblem, clampWait, clockMotion, clockTyping, conditionMiss, deadLine, deadTime, deadWarns, domSettled, easeInOutCubic, exposureLine, extForType, fetchInPageScript, fileNameProblem, fitSurfaceToViewport, formatDurationCap, guardVerdict, guardedSteps, hangReason, headersFor, holdLeftMs, hostedRefusals, inlinePoints, inlineScript, isIdpHost, isKeyName, isSignInPath, jpegDims, jsConditionScript, keptValue, keySequencePlanMs, loadScriptFiles, maskInitScript, modifierBits, nameFromUrl, noticeAdvice, noticeLine, outputName, overrunLine, overrunOf, paceLine, paceReport, pageErrorLine, parseHeaders, pasteTypeOf, pathDurationMs, peerValue, peersHostedRefusal, plainName, planFreshTake, pointerTravelMs, pollCondition, pressSequence, publicUrl, readAllBytes, readCondition, readMediaSource, readPath, readyLine, readyProblem, realSleep, recordTake, redirectedAway, reloadUntilReady, resolveChord, resolveModifier, resolveSetupText, runPeer, runSetup, sameJson, sayCondition, sayDuration, sayJs, sayValue, schedulePath, scriptFiles, scrollByScript, scrollTravelMs, settleMs, settleOn, settleVerdict, setupEnvNames, soundSrcForMeta, splitChord, splitSegmentsAtSkips, stageClipboardScript, stepBoundMs, syntheticPasteScript, typeOfName, untilReady, urlSchemeProblem, validateActions, validatePeers, validateSetup, visibleTextScript, wallVerdict, withFittingExport, withPageSound };
2342
+ export { APP_QUIET_MS, APP_SETTLE_CAP_MS, AWAIT_HEARTBEAT_MS, AWAIT_POLL_S, AWAIT_TIMEOUT_S, type ActionStep, type ActionsFile, type Arrival, type AwaitUntil, CHANGE_AGE_PROBE, CLICK_BUTTONS, type ClickButton, type ConditionState, DEAD_SHARE_WARN_PCT, DEAD_STRETCH_WARN_MS, DEFAULT_READY, DOM_CHANGE_PROBE, DOM_QUIET_MS, DOM_SETTLE_CAP_MS, DOWNLOAD_TIMEOUT_S, DRAG_HOLD_MS, DRAG_MODIFIERS, type DeadReport, type DeadStep, type DisabledClick, type DragModifier, EXPECT_TIMEOUT_S, EXPOSURE_ADVICE, EXPOSURE_MATCHERS_SRC, EXPOSURE_PROBE, type EncodePageOptions, type Exposure, type ExposureKind, ExposureLog, type FrameRec, type FrameSink, type FreezeSpan, type FreshPlanOptions, type GuardedStep, type HeardSound, IDLE_CALL_MS, JS_SAID_MAX, JS_VALUE_MAX, KEY_ROUNDTRIP_MS, KEY_SEQUENCE_GAP_MS, type LoadedFiles, MAX_KEYS, MAX_PATH_POINTS, MAX_PEERS, MAX_PEER_STEPS, MEDIA_DRAIN_PROBE, MEDIA_WATCH_SCRIPT, MIN_SKIP_MS, MODIFIERS, MOTION_TICK_MS, MULTI_CLICK_GAP_MS, type MaskReport, type MaskRule, type MediaDrain, type MediaNote, type Modifier, NAV_TIMEOUT_MS, NETWORKIDLE_HYDRATION_MS, NOTICE_SRC, type Notice, NoticeLog, OUTPUT_MAX_BYTES, OVERRUN_MIN_MS, OVERRUN_MIN_PLANNED_MS, OVERRUN_SHARE, type OutputSink, PAGE_ERROR_LINE_CHARS, PASTE_MAX_BYTES, PASTE_READ_WINDOW_MS, PASTE_TYPES, PASTE_UNREAD_MS, PATH_DEFAULT_MS, PEER_VALUE_MAX, PEER_VERBS, POINTER_MAX_MS, POINTER_MIN_MS, POINTER_MS_PER_PX, PRESS_HOLD_MS, PRESS_KEYS, PRESS_LEAD_MS, type PaceReport, type PageError, PageErrorLog, PageSoundLog, type PasteItem, type PathPoint, type PathSample, type Peer, type PeerRun, type PeerStep, type PollResult, type PressKey, QUIET_MS, READY_MODES, READY_SETTLE_CAP_MS, READ_BEAT_MS, RESPONSE_MS, type ReadyMode, type ReadyOpts, type ReadyReport, type RecordOpts, RecordRefusedError, type RecordResult, type RecorderApiResponse, type RecorderBrowser, type RecorderButtonOpts, type RecorderCdp, type RecorderClock, type RecorderConsoleMessage, type RecorderContext, type RecorderDownload, type RecorderLocator, type RecorderLocatorRoot, type RecorderMediaElement, type RecorderPage, type RecorderRect, type RecorderResponse, type RecorderRoute, SCROLL_MAX_MS, SCROLL_MIN_MS, SCROLL_MS_PER_PX, SCROLL_SETTLE_MS, SETTLE_CAP_MS, SETTLE_MS, SETTLE_VERBS, STEP_GUARDS, STEP_MARGIN_MS, type ScannedNotice, type ScriptFiles, SetupEnvError, SetupError, type SetupResult, type SetupStep, type SetupText, type SkippedStep, type SoundSink, StepBound, type StepGuard, type StepOverrun, type StepPace, type StepPhase, StepSignals, type StoppedTake, type SurfaceFit, type SurfaceSize, TEARDOWN_BOUND_MS, TRAILING_HOLD_MS, TakeClock, type TeardownReport, WAIT_MARGIN_MS, WALL_PROBE, type WaitCondition, WallError, type WallKind, type WallVerdict, ZOOM_MARK_VERBS, appSettled, askedMs, base64Bytes, bounded, buildEncodePage, buttonNumber, capReached, cappedLine, chordProblem, clampWait, clockMotion, clockTyping, conditionMiss, deadLine, deadTime, deadWarns, domSettled, easeInOutCubic, exposureLine, extForType, fetchInPageScript, fileNameProblem, fitSurfaceToViewport, formatDurationCap, guardVerdict, guardedSteps, hangReason, headersFor, holdLeftMs, hostedRefusals, inlinePoints, inlineScript, isIdpHost, isKeyName, isSignInPath, jpegDims, jsConditionScript, keptValue, keySequencePlanMs, loadScriptFiles, maskInitScript, modifierBits, nameFromUrl, noticeAdvice, noticeLine, outputName, overrunLine, overrunOf, paceLine, paceReport, pageErrorLine, parseHeaders, pasteTypeOf, pathDurationMs, peerValue, peersHostedRefusal, plainName, planFreshTake, pointerTravelMs, pollCondition, pressSequence, publicUrl, readAllBytes, readCondition, readMediaSource, readPath, readyLine, readyProblem, realSleep, recordTake, redirectedAway, reloadUntilReady, resolveChord, resolveModifier, resolveSetupText, runPeer, runSetup, sameJson, sayCondition, sayDuration, sayJs, sayValue, schedulePath, scriptEnvNames, scriptFiles, scrollByScript, scrollTravelMs, settleMs, settleOn, settleVerdict, setupEnvNames, soundSrcForMeta, splitChord, splitSegmentsAtSkips, stageClipboardScript, stepBoundMs, syntheticPasteScript, teardownEnvProblem, teardownLine, typeOfName, untilReady, urlSchemeProblem, validateActions, validatePeers, validateSetup, visibleTextScript, wallVerdict, withFittingExport, withPageSound };
@@ -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 ["setup must be an array of steps"];
706
+ if (!Array.isArray(value)) return [`${phase} must be an array of steps`];
707
707
  value.forEach((raw, i) => {
708
- const at = `setup[${i}]`;
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 resolveSetupText(t, env) {
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
- `setup: 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`
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
- for (const [i, step] of steps.entries()) {
792
+ let typed = "";
793
+ const runStep = async (step, i) => {
766
794
  const settle = "ms" in step && typeof step.ms === "number" ? step.ms : 150;
767
- try {
768
- switch (step.do) {
769
- case "wait":
770
- await opts.sleep(step.ms);
771
- lines.push(`setup #${i} wait ${step.ms}ms`);
772
- break;
773
- case "goto":
774
- await (opts.goto ? opts.goto(step.url) : page.goto(step.url, {
775
- waitUntil: "networkidle",
776
- timeout: 45e3
777
- })).catch(() => null);
778
- lines.push(`setup #${i} goto ${step.url}`);
779
- break;
780
- case "press":
781
- await page.keyboard.press(step.key);
782
- await opts.sleep(settle);
783
- lines.push(`setup #${i} press ${step.key}`);
784
- break;
785
- case "click": {
786
- const loc = page.locator(step.selector).first();
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);
@@ -2405,6 +2473,8 @@ function validateActions(value) {
2405
2473
  }
2406
2474
  }
2407
2475
  if (obj.setup !== void 0) errors.push(...validateSetup(obj.setup));
2476
+ if (obj.teardown !== void 0)
2477
+ errors.push(...validateSetup(obj.teardown, "teardown"));
2408
2478
  const ready = readyProblem(obj.ready);
2409
2479
  if (ready) errors.push(ready);
2410
2480
  if (obj.settle !== void 0 && typeof obj.settle !== "boolean")
@@ -3071,6 +3141,18 @@ function base64Bytes(data) {
3071
3141
  return out;
3072
3142
  }
3073
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) {
3074
3156
  const dry = opts.dryRun === true;
3075
3157
  const sleep = dry ? (ms) => realSleep(Math.min(ms, DRY_PAUSE_MS)) : realSleep;
3076
3158
  const maxSeconds = dry ? Infinity : opts.maxDurationSeconds ?? Infinity;
@@ -3083,6 +3165,8 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
3083
3165
  `steps[${download}] is a download, and this recorder has nowhere to keep a file. Nothing was recorded.`
3084
3166
  );
3085
3167
  }
3168
+ const teardownEnv = teardownEnvProblem(actions.teardown, opts.env ?? {});
3169
+ if (teardownEnv) throw new SetupEnvError(teardownEnv);
3086
3170
  const read = await loadScriptFiles(actions, opts.files);
3087
3171
  if ("problem" in read) throw new RecordRefusedError(read.problem);
3088
3172
  const files = read;
@@ -3126,8 +3210,9 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
3126
3210
  noticeLog.add(step, scan?.notices);
3127
3211
  };
3128
3212
  const pageErrors = new PageErrorLog(opts.verbose === true);
3213
+ let tearingDown = false;
3129
3214
  page.on("console", (m) => {
3130
- if (m.type() !== "error") return;
3215
+ if (m.type() !== "error" || tearingDown) return;
3131
3216
  const line = pageErrors.add(m.text());
3132
3217
  if (line) log(line);
3133
3218
  });
@@ -3158,6 +3243,49 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
3158
3243
  log(`goto ${url} (after setup)`);
3159
3244
  response = (await open(url)).response;
3160
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
+ };
3161
3289
  let wall = null;
3162
3290
  if (opts.onArrival) {
3163
3291
  const seen = await page.evaluate(WALL_PROBE).catch(() => null);
@@ -3468,6 +3596,13 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
3468
3596
  })
3469
3597
  );
3470
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
+ };
3471
3606
  for (const [stepIdx, step] of actions.steps.entries()) {
3472
3607
  if (capReached(now(), maxSeconds)) {
3473
3608
  capped = true;
@@ -4179,6 +4314,8 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
4179
4314
  const pageTitle = await page.title().catch(() => "") ?? "";
4180
4315
  for (const line of pageErrors.repeats()) log(line);
4181
4316
  pageBound.end();
4317
+ late.teardown = void 0;
4318
+ await runTeardown();
4182
4319
  pageBound.start(CLOSE_BOUND_MS);
4183
4320
  await pageBound.race(context.close().catch(() => {
4184
4321
  }));
@@ -4265,7 +4402,8 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
4265
4402
  // Self-contained: a path a file held is inline, so the take re-records
4266
4403
  // from its own script with the file gone.
4267
4404
  actions: { ...inlineScript(actions, files), url },
4268
- peers: peerRuns
4405
+ peers: peerRuns,
4406
+ teardown
4269
4407
  };
4270
4408
  }
4271
4409
  async function keepPageSound(heard, sink, fetchSource, log) {
@@ -4592,6 +4730,7 @@ export {
4592
4730
  SetupError,
4593
4731
  StepBound,
4594
4732
  StepSignals,
4733
+ TEARDOWN_BOUND_MS,
4595
4734
  TRAILING_HOLD_MS,
4596
4735
  TakeClock,
4597
4736
  WAIT_MARGIN_MS,
@@ -4681,6 +4820,7 @@ export {
4681
4820
  sayJs,
4682
4821
  sayValue,
4683
4822
  schedulePath,
4823
+ scriptEnvNames,
4684
4824
  scriptFiles,
4685
4825
  scrollByScript,
4686
4826
  scrollTravelMs,
@@ -4694,6 +4834,8 @@ export {
4694
4834
  stageClipboardScript,
4695
4835
  stepBoundMs,
4696
4836
  syntheticPasteScript,
4837
+ teardownEnvProblem,
4838
+ teardownLine,
4697
4839
  typeOfName,
4698
4840
  untilReady,
4699
4841
  urlSchemeProblem,