@vosjs/render-core 0.7.0 → 0.8.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 +751 -19
- package/dist/record/index.js +1872 -206
- package/dist/record/index.js.map +1 -1
- package/package.json +2 -2
package/dist/record/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RecordingMeta, CursorEvent, RecordingSkip, RecordingOutput, Backdrop, RecordingArtifact, ProjectDoc } from '@vosjs/studio-core';
|
|
1
|
+
import { RecordingMeta, CursorEvent, RecordingSkip, RecordingOutput, Backdrop, RecordingArtifact, ProjectDoc, RecordingPageMedia } from '@vosjs/studio-core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The recorder's driver: what it needs from a browser, as a STRUCTURAL
|
|
@@ -54,6 +54,11 @@ interface RecorderApiResponse {
|
|
|
54
54
|
headers(): Record<string, string>;
|
|
55
55
|
body(): Promise<Uint8Array>;
|
|
56
56
|
}
|
|
57
|
+
/** Which button a press is, and which press of a multi-click. */
|
|
58
|
+
interface RecorderButtonOpts {
|
|
59
|
+
button?: 'left' | 'right' | 'middle';
|
|
60
|
+
clickCount?: number;
|
|
61
|
+
}
|
|
57
62
|
interface RecorderLocatorRoot {
|
|
58
63
|
first(): RecorderLocator;
|
|
59
64
|
}
|
|
@@ -66,14 +71,14 @@ interface RecorderConsoleMessage {
|
|
|
66
71
|
}
|
|
67
72
|
interface RecorderPage {
|
|
68
73
|
goto(url: string, opts: {
|
|
69
|
-
waitUntil: 'networkidle';
|
|
74
|
+
waitUntil: 'networkidle' | 'load';
|
|
70
75
|
timeout: number;
|
|
71
76
|
}): Promise<RecorderResponse | null>;
|
|
72
77
|
url(): string;
|
|
73
78
|
title(): Promise<string>;
|
|
74
79
|
/** An `await` with `reload: true`; absent, the recorder opens the page's URL again. */
|
|
75
80
|
reload?(opts: {
|
|
76
|
-
waitUntil: 'networkidle';
|
|
81
|
+
waitUntil: 'networkidle' | 'load';
|
|
77
82
|
timeout: number;
|
|
78
83
|
}): Promise<RecorderResponse | null>;
|
|
79
84
|
/** A `download` step's click; absent, the recorder says the browser reports none. */
|
|
@@ -91,13 +96,16 @@ interface RecorderPage {
|
|
|
91
96
|
locator(selector: string): RecorderLocatorRoot;
|
|
92
97
|
mouse: {
|
|
93
98
|
move(x: number, y: number): Promise<void>;
|
|
94
|
-
down(): Promise<void>;
|
|
95
|
-
up(): Promise<void>;
|
|
99
|
+
down(opts?: RecorderButtonOpts): Promise<void>;
|
|
100
|
+
up(opts?: RecorderButtonOpts): Promise<void>;
|
|
96
101
|
wheel(dx: number, dy: number): Promise<void>;
|
|
97
102
|
};
|
|
98
103
|
keyboard: {
|
|
99
104
|
type(text: string): Promise<void>;
|
|
100
105
|
press(key: string): Promise<void>;
|
|
106
|
+
/** A drag's modifiers, held from the press to the release. */
|
|
107
|
+
down(key: string): Promise<void>;
|
|
108
|
+
up(key: string): Promise<void>;
|
|
101
109
|
};
|
|
102
110
|
}
|
|
103
111
|
interface RecorderCdp {
|
|
@@ -131,6 +139,13 @@ interface RecorderContext {
|
|
|
131
139
|
*/
|
|
132
140
|
route?(pattern: string, handler: (route: RecorderRoute) => Promise<unknown> | unknown): Promise<unknown>;
|
|
133
141
|
newCDPSession(page: unknown): Promise<RecorderCdp>;
|
|
142
|
+
/**
|
|
143
|
+
* A `paste` step's clipboard access, when the driver grants it; absent,
|
|
144
|
+
* the paste is dispatched in the page and the recorder says so.
|
|
145
|
+
*/
|
|
146
|
+
grantPermissions?(permissions: string[], opts?: {
|
|
147
|
+
origin?: string;
|
|
148
|
+
}): Promise<unknown>;
|
|
134
149
|
close(): Promise<void>;
|
|
135
150
|
}
|
|
136
151
|
interface RecorderBrowser {
|
|
@@ -162,6 +177,25 @@ interface FrameSink {
|
|
|
162
177
|
interface OutputSink {
|
|
163
178
|
save(name: string, bytes: Uint8Array, contentType?: string): Promise<void> | void;
|
|
164
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Where the sound a page played during the take is kept: the CLI writes
|
|
182
|
+
* `page-sound/<name>` in the take directory and answers that take-relative
|
|
183
|
+
* key, which the plan's audio clips name. A host that hands in none (the
|
|
184
|
+
* fleet) still records `meta.media`; the plan then places nothing.
|
|
185
|
+
*/
|
|
186
|
+
interface SoundSink {
|
|
187
|
+
save(name: string, bytes: Uint8Array, contentType?: string): Promise<string> | string;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Where the files a script names beside itself come from: a drag's path
|
|
191
|
+
* file, a paste's file, by a plain name. The CLI reads the actions file's
|
|
192
|
+
* directory. A host that hands in none (a hosted recorder carries the
|
|
193
|
+
* script alone) cannot run such a step, and the recorder refuses it before
|
|
194
|
+
* capturing anything.
|
|
195
|
+
*/
|
|
196
|
+
interface ScriptFiles {
|
|
197
|
+
read(name: string): Promise<Uint8Array>;
|
|
198
|
+
}
|
|
165
199
|
/** The clock and the pauses, injectable so a rehearsal runs fast. */
|
|
166
200
|
interface RecorderClock {
|
|
167
201
|
now(): number;
|
|
@@ -200,6 +234,7 @@ declare const SETTLE_MS: {
|
|
|
200
234
|
readonly click: 150;
|
|
201
235
|
readonly type: 150;
|
|
202
236
|
readonly press: 150;
|
|
237
|
+
readonly paste: 150;
|
|
203
238
|
readonly scroll: 200;
|
|
204
239
|
readonly drag: 80;
|
|
205
240
|
};
|
|
@@ -437,6 +472,9 @@ declare class PageErrorLog {
|
|
|
437
472
|
type ExposureKind = 'email' | 'key' | 'card' | 'card-tail';
|
|
438
473
|
type Exposure = NonNullable<RecordingMeta['exposures']>[number];
|
|
439
474
|
type MaskReport = NonNullable<RecordingMeta['masks']>[number];
|
|
475
|
+
type Notice = NonNullable<RecordingMeta['notices']>[number];
|
|
476
|
+
/** A notice as one scan reports it. */
|
|
477
|
+
type ScannedNotice = Omit<Notice, 'step' | 'seen'>;
|
|
440
478
|
interface MaskRule {
|
|
441
479
|
selector: string;
|
|
442
480
|
/** `blur` (default) blurs the element; `text` swaps its words for `text`. */
|
|
@@ -456,12 +494,28 @@ interface MaskRule {
|
|
|
456
494
|
* - card-tail: a masked number's visible tail (•••• 4242).
|
|
457
495
|
*/
|
|
458
496
|
declare const EXPOSURE_MATCHERS_SRC = "(() => {\n const EMAIL = /[A-Za-z0-9._%+-]+@((?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,})/g\n const SAFE_HOST = /(^|\\.)example\\.(com|org|net)$|\\.(test|example|invalid|localhost)$/i\n const KEY = new RegExp([\n '\\\\b[srp]k_(?:live|test)_[A-Za-z0-9]{8,}',\n '\\\\bgh[pousr]_[A-Za-z0-9]{20,}',\n '\\\\bgithub_pat_[A-Za-z0-9_]{20,}',\n '\\\\bxox[abprs]-[A-Za-z0-9-]{10,}',\n '\\\\bAKIA[0-9A-Z]{16}\\\\b',\n '\\\\bAIza[0-9A-Za-z_-]{30,}',\n '\\\\bvos_(?:sk|rg|ho)_[A-Za-z0-9]{8,}',\n '\\\\beyJ[A-Za-z0-9_-]{8,}\\\\.eyJ[A-Za-z0-9_-]{8,}\\\\.[A-Za-z0-9_-]{8,}',\n ].join('|'))\n const CARD = /(?:^|[^0-9])((?:[0-9][ -]?){12,18}[0-9])(?![0-9])/g\n const TAIL = /[\\u2022\\u00b7*xX]{2,}[ -]?[0-9]{4}(?![0-9])/\n const luhn = (digits) => {\n let sum = 0, alt = false\n for (let i = digits.length - 1; i >= 0; i--) {\n let d = digits.charCodeAt(i) - 48\n if (alt) { d *= 2; if (d > 9) d -= 9 }\n sum += d; alt = !alt\n }\n return sum % 10 === 0\n }\n const kindsIn = (text) => {\n const kinds = []\n if (!text || text.length < 6) return kinds\n EMAIL.lastIndex = 0\n for (let m; (m = EMAIL.exec(text)); ) {\n if (!SAFE_HOST.test(m[1])) { kinds.push('email'); break }\n }\n if (KEY.test(text)) kinds.push('key')\n CARD.lastIndex = 0\n for (let m; (m = CARD.exec(text)); ) {\n const digits = m[1].replace(/[ -]/g, '')\n if (digits.length >= 13 && digits.length <= 19 && !/^0+$/.test(digits) && luhn(digits)) { kinds.push('card'); break }\n }\n if (TAIL.test(text)) kinds.push('card-tail')\n return kinds\n }\n return { kindsIn }\n})()";
|
|
497
|
+
/**
|
|
498
|
+
* The INFORMATION the scan reports beside the exposures (never a failure):
|
|
499
|
+
* the signed-in account's own display name, wherever the frame shows it,
|
|
500
|
+
* and a list of the account's recent items. Runs inside EXPOSURE_PROBE,
|
|
501
|
+
* after `seen` and before any selector is made: it sets `acct`, which
|
|
502
|
+
* `quiet` then keeps out of every selector. Never `$` + `{` in here: it is
|
|
503
|
+
* spliced into a template.
|
|
504
|
+
*
|
|
505
|
+
* The name is read conservatively, from where apps say whose account it
|
|
506
|
+
* is: a line that greets or names the account ("Signed in as …",
|
|
507
|
+
* "Welcome back, …"), the label of an account menu ("Account menu for …"),
|
|
508
|
+
* or the words on the account menu's own trigger (the button that opens
|
|
509
|
+
* a menu and holds the avatar), which are SURE; or a name-like label on an avatar (an image's alt, a
|
|
510
|
+
* button holding the picture), which MAY be the name.
|
|
511
|
+
*/
|
|
512
|
+
declare const NOTICE_SRC: string;
|
|
459
513
|
/**
|
|
460
514
|
* The scan: visible text in the viewport, plus the values of text inputs.
|
|
461
515
|
* Skips anything inside a masked element, password fields, and text the eye
|
|
462
516
|
* cannot see. Reports kind + a selector-ish name for the element + its rect.
|
|
463
517
|
*/
|
|
464
|
-
declare const EXPOSURE_PROBE
|
|
518
|
+
declare const EXPOSURE_PROBE: string;
|
|
465
519
|
/**
|
|
466
520
|
* The mask, as an init script: it runs at document start on EVERY navigation,
|
|
467
521
|
* so a masked value is hidden before the page's first paint, and a
|
|
@@ -488,6 +542,26 @@ declare class ExposureLog {
|
|
|
488
542
|
/** One exposure, in words. `step` -1 is the page as it opened. */
|
|
489
543
|
declare function exposureLine(e: Exposure): string;
|
|
490
544
|
declare const EXPOSURE_ADVICE = "The recording shows these. Record from a demo account, or hide them before the camera rolls with \"mask\" in actions.json: { \"selector\": \"\u2026\", \"as\": \"text\", \"text\": \"demo@acme.test\" } swaps the words, \"as\": \"blur\" blurs. Then re-record.";
|
|
545
|
+
/**
|
|
546
|
+
* Accumulates the notices across a take: the account's name is ONE notice,
|
|
547
|
+
* its places gathered from every scan; a recent-items list is one notice
|
|
548
|
+
* per list. Each keeps the first step it was seen at.
|
|
549
|
+
*/
|
|
550
|
+
declare class NoticeLog {
|
|
551
|
+
private byKey;
|
|
552
|
+
add(step: number, notices: ScannedNotice[] | undefined | null): void;
|
|
553
|
+
notices(): Notice[];
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* One notice, in words. The account's name is never printed: the line says
|
|
557
|
+
* where it showed, so each place can be masked.
|
|
558
|
+
*/
|
|
559
|
+
declare function noticeLine(n: Notice): string;
|
|
560
|
+
/**
|
|
561
|
+
* What to do about the notices, in one line: the swap hint is about the
|
|
562
|
+
* account's name, so it is said only when the name was noticed.
|
|
563
|
+
*/
|
|
564
|
+
declare function noticeAdvice(notices: readonly Pick<Notice, 'kind'>[]): string;
|
|
491
565
|
|
|
492
566
|
/**
|
|
493
567
|
* `setup`: the steps that run BEFORE the camera rolls. A sign-in form, a
|
|
@@ -567,6 +641,8 @@ declare function runSetup(page: RecorderPage, steps: SetupStep[], opts: {
|
|
|
567
641
|
sleep: (ms: number) => Promise<void>;
|
|
568
642
|
/** Absent means no env at all: a `{ env }` text then fails in words. */
|
|
569
643
|
env?: Record<string, string | undefined>;
|
|
644
|
+
/** How a `goto` opens its page and waits for it (the take's ready mode); absent, network idle. */
|
|
645
|
+
goto?: (url: string) => Promise<unknown>;
|
|
570
646
|
}): Promise<SetupResult>;
|
|
571
647
|
/** A `{ env }` the shell did not provide: a usage error, not a page's. */
|
|
572
648
|
declare class SetupEnvError extends Error {
|
|
@@ -580,6 +656,222 @@ declare class SetupError extends Error {
|
|
|
580
656
|
/** `--header name=value`, repeatable: extra request headers on every request. */
|
|
581
657
|
declare function parseHeaders(given: string[] | undefined): Record<string, string>;
|
|
582
658
|
|
|
659
|
+
/**
|
|
660
|
+
* When a page counts as ready for the take to begin. The default is the
|
|
661
|
+
* DOM: after `load`, the page must make no DOM change for a beat (counted
|
|
662
|
+
* from outside, the way an `await` reload's settle is), at most a few
|
|
663
|
+
* seconds. Network idle is the opt-in: plenty of real apps hold a socket
|
|
664
|
+
* open or keep an analytics beacon going and never go network-idle, so a
|
|
665
|
+
* take that waited for it started after its whole timeout, every time.
|
|
666
|
+
*/
|
|
667
|
+
|
|
668
|
+
/** How a page counts as ready: its DOM settled (default), or its network went idle. */
|
|
669
|
+
type ReadyMode = 'networkidle' | 'dom';
|
|
670
|
+
declare const READY_MODES: readonly ReadyMode[];
|
|
671
|
+
declare const DEFAULT_READY: ReadyMode;
|
|
672
|
+
/** The longest a navigation waits for `load` or network idle. */
|
|
673
|
+
declare const NAV_TIMEOUT_MS = 45000;
|
|
674
|
+
/** The longest the settle waits on a page that never stops changing. */
|
|
675
|
+
declare const READY_SETTLE_CAP_MS = 8000;
|
|
676
|
+
/** After network idle, the beat a hydration takes (what the recorder always gave it). */
|
|
677
|
+
declare const NETWORKIDLE_HYDRATION_MS = 800;
|
|
678
|
+
/** How the first load settled: the done event's `ready`. */
|
|
679
|
+
interface ReadyReport {
|
|
680
|
+
mode: ReadyMode;
|
|
681
|
+
/** ms from the navigation's start until the page counted as ready */
|
|
682
|
+
ms: number;
|
|
683
|
+
/**
|
|
684
|
+
* The wait gave up instead of the page settling: the DOM never stopped
|
|
685
|
+
* changing within the cap (dom), or network idle never came within
|
|
686
|
+
* the navigation timeout (networkidle). Recorded anyway either way.
|
|
687
|
+
*/
|
|
688
|
+
capped: boolean;
|
|
689
|
+
}
|
|
690
|
+
/** Why a ready mode is not one, or null. */
|
|
691
|
+
declare function readyProblem(v: unknown): string | null;
|
|
692
|
+
interface ReadyOpts {
|
|
693
|
+
sleep: (ms: number) => Promise<void>;
|
|
694
|
+
now?: () => number;
|
|
695
|
+
/** the settle's cap (default `READY_SETTLE_CAP_MS`) */
|
|
696
|
+
capMs?: number;
|
|
697
|
+
/** the navigation's own timeout (default `NAV_TIMEOUT_MS`) */
|
|
698
|
+
timeoutMs?: number;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Wait for a page that is navigating (or has just navigated) to be ready:
|
|
702
|
+
* `navigate` starts the navigation with the `waitUntil` the mode asks for;
|
|
703
|
+
* in `dom` mode the DOM settle follows `load`. A navigation that times
|
|
704
|
+
* out is not fatal: the take goes on from whatever the page shows, and the
|
|
705
|
+
* report says it was capped.
|
|
706
|
+
*/
|
|
707
|
+
declare function untilReady(page: RecorderPage, mode: ReadyMode, navigate: (opts: {
|
|
708
|
+
waitUntil: 'networkidle' | 'load';
|
|
709
|
+
timeout: number;
|
|
710
|
+
}) => Promise<RecorderResponse | null>, opts: ReadyOpts): Promise<{
|
|
711
|
+
response: RecorderResponse | null;
|
|
712
|
+
ready: ReadyReport;
|
|
713
|
+
}>;
|
|
714
|
+
/**
|
|
715
|
+
* An `await`'s reload between polls, under the take's ready mode: the
|
|
716
|
+
* reload waits for `load` (dom) or network idle (networkidle), at most
|
|
717
|
+
* the await's own timeout, and then the DOM settles either way, because
|
|
718
|
+
* network quiet is not a drawn page (a list rendered from its data lands
|
|
719
|
+
* after it). A page that never goes network-idle no longer costs a whole
|
|
720
|
+
* navigation timeout per poll.
|
|
721
|
+
*/
|
|
722
|
+
declare function reloadUntilReady(page: RecorderPage, mode: ReadyMode, timeoutMs: number, opts?: {
|
|
723
|
+
sleep?: (ms: number) => Promise<void>;
|
|
724
|
+
}): Promise<void>;
|
|
725
|
+
/** The ready report in words, for the take's log. */
|
|
726
|
+
declare function readyLine(r: ReadyReport): string;
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* The canvas gestures, the pure parts: key chords and sequences, the
|
|
730
|
+
* modifiers a drag holds, a drawn path and its timing, a click's count and
|
|
731
|
+
* button, and the overrun report. What a whiteboard, a design tool or a
|
|
732
|
+
* timeline is recorded with, said as data so a script can carry it and
|
|
733
|
+
* both hosts validate it in the same words.
|
|
734
|
+
*
|
|
735
|
+
* None of it reaches the cursor track as key identity: a key is a typing
|
|
736
|
+
* ping (when and where), a modifier is nothing at all, and a path is its
|
|
737
|
+
* points.
|
|
738
|
+
*/
|
|
739
|
+
/** The modifiers a chord or a drag may hold. `Mod` is Meta on macOS, Control elsewhere. */
|
|
740
|
+
declare const MODIFIERS: readonly ["Shift", "Control", "Alt", "Meta", "Mod", "ControlOrMeta"];
|
|
741
|
+
type Modifier = (typeof MODIFIERS)[number];
|
|
742
|
+
/** The modifiers a drag names (the platform-neutral one is `Mod`). */
|
|
743
|
+
declare const DRAG_MODIFIERS: readonly ["Alt", "Shift", "Meta", "Control", "Mod"];
|
|
744
|
+
type DragModifier = (typeof DRAG_MODIFIERS)[number];
|
|
745
|
+
/** A key name the browser's keyboard answers: a character, a code, a named key. */
|
|
746
|
+
declare function isKeyName(k: string): boolean;
|
|
747
|
+
/**
|
|
748
|
+
* A chord split into its modifiers and its key: `Shift+D` is
|
|
749
|
+
* `{ mods: ['Shift'], key: 'D' }`, a bare `r` has no modifiers, and `+`
|
|
750
|
+
* names the plus key when it ends the chord (`Shift++`).
|
|
751
|
+
*/
|
|
752
|
+
declare function splitChord(chord: string): {
|
|
753
|
+
mods: string[];
|
|
754
|
+
key: string;
|
|
755
|
+
};
|
|
756
|
+
/** Why a chord is not one the recorder can press, or null. */
|
|
757
|
+
declare function chordProblem(chord: unknown): string | null;
|
|
758
|
+
/**
|
|
759
|
+
* A chord as the browser's keyboard takes it: `Mod` becomes
|
|
760
|
+
* `ControlOrMeta`, which the browser resolves to Meta on macOS and to
|
|
761
|
+
* Control elsewhere, where the browser runs.
|
|
762
|
+
*/
|
|
763
|
+
declare function resolveChord(chord: string): string;
|
|
764
|
+
/** A `press` step's keys as a list: one chord or a sequence. */
|
|
765
|
+
declare function pressSequence(step: {
|
|
766
|
+
key?: string;
|
|
767
|
+
keys?: string | string[];
|
|
768
|
+
}): string[];
|
|
769
|
+
/** The pause between two keys of a sequence, a person's hand: ms. */
|
|
770
|
+
declare const KEY_SEQUENCE_GAP_MS = 150;
|
|
771
|
+
/**
|
|
772
|
+
* What one key costs beyond the script's gaps: the browser's round trip for
|
|
773
|
+
* the press (a keydown and a keyup the page answers). Part of the plan, so a
|
|
774
|
+
* sequence on an idle page is never "slower than scripted".
|
|
775
|
+
*/
|
|
776
|
+
declare const KEY_ROUNDTRIP_MS = 50;
|
|
777
|
+
/** A key sequence's plan, ms: its gaps, and each key's round trip. */
|
|
778
|
+
declare const keySequencePlanMs: (keys: number) => number;
|
|
779
|
+
/** The hold after a drag's release, ms: the recorder's, part of the gesture. */
|
|
780
|
+
declare const DRAG_HOLD_MS = 400;
|
|
781
|
+
/** The most keys one press step may name. */
|
|
782
|
+
declare const MAX_KEYS = 50;
|
|
783
|
+
/** A drag modifier as the keyboard holds it. */
|
|
784
|
+
declare function resolveModifier(m: DragModifier): string;
|
|
785
|
+
/**
|
|
786
|
+
* The modifier bits of a raw pointer event (the pen path): Alt 1, Control
|
|
787
|
+
* 2, Meta 4, Shift 8, with `Mod` read for the platform the browser runs on.
|
|
788
|
+
*/
|
|
789
|
+
declare function modifierBits(mods: readonly DragModifier[], platform: 'mac' | 'linux' | 'windows' | undefined): number;
|
|
790
|
+
/** A click's button, and the pointer button number the cursor track records. */
|
|
791
|
+
declare const CLICK_BUTTONS: readonly ["left", "right", "middle"];
|
|
792
|
+
type ClickButton = (typeof CLICK_BUTTONS)[number];
|
|
793
|
+
declare const buttonNumber: (b: ClickButton | undefined) => 0 | 1 | 2;
|
|
794
|
+
/**
|
|
795
|
+
* The pause between the presses of a double or triple click, well inside
|
|
796
|
+
* every platform's double-click interval (500 ms on macOS and Windows by
|
|
797
|
+
* default), so the page reads one gesture.
|
|
798
|
+
*/
|
|
799
|
+
declare const MULTI_CLICK_GAP_MS = 90;
|
|
800
|
+
/**
|
|
801
|
+
* One point of a drawn path: viewport CSS px, with an optional time in ms
|
|
802
|
+
* (from the path's start) and an optional pressure in [0, 1]. Written as a
|
|
803
|
+
* tuple `[x, y]`, `[x, y, t]`, `[x, y, t, pressure]`, or an object.
|
|
804
|
+
*/
|
|
805
|
+
type PathPoint = [number, number] | [number, number, number] | [number, number, number, number] | {
|
|
806
|
+
x: number;
|
|
807
|
+
y: number;
|
|
808
|
+
t?: number;
|
|
809
|
+
pressure?: number;
|
|
810
|
+
};
|
|
811
|
+
/** A path point as the recorder replays it. */
|
|
812
|
+
interface PathSample {
|
|
813
|
+
x: number;
|
|
814
|
+
y: number;
|
|
815
|
+
/** ms from the path's first point */
|
|
816
|
+
at: number;
|
|
817
|
+
pressure?: number;
|
|
818
|
+
}
|
|
819
|
+
/** The most points one path may hold. */
|
|
820
|
+
declare const MAX_PATH_POINTS = 20000;
|
|
821
|
+
/** An untimed path's travel when the step names no ms. */
|
|
822
|
+
declare const PATH_DEFAULT_MS = 700;
|
|
823
|
+
/**
|
|
824
|
+
* A path's points read and checked, or the problems in words. The same
|
|
825
|
+
* reader takes an inline `path` and the contents of a path file (an array
|
|
826
|
+
* of points, or `{ "points": [...] }`).
|
|
827
|
+
*/
|
|
828
|
+
declare function readPath(raw: unknown): {
|
|
829
|
+
points: PathSample[];
|
|
830
|
+
timed: boolean;
|
|
831
|
+
} | {
|
|
832
|
+
problems: string[];
|
|
833
|
+
};
|
|
834
|
+
/**
|
|
835
|
+
* The path's schedule: a timed path keeps its own times (from its first
|
|
836
|
+
* point), an untimed one spreads its points evenly over `ms` (default
|
|
837
|
+
* 700). The last point is at the path's whole duration.
|
|
838
|
+
*/
|
|
839
|
+
declare function schedulePath(path: {
|
|
840
|
+
points: PathSample[];
|
|
841
|
+
timed: boolean;
|
|
842
|
+
}, ms: number | undefined): PathSample[];
|
|
843
|
+
/** How long a scheduled path takes, ms. */
|
|
844
|
+
declare const pathDurationMs: (s: readonly PathSample[]) => number;
|
|
845
|
+
/**
|
|
846
|
+
* A paced gesture that ran past its script: the wall time it took against
|
|
847
|
+
* the time the script planned for it (a drag's travel, a path's timing,
|
|
848
|
+
* typing's characters × delay, a key sequence's gaps). Reported when the
|
|
849
|
+
* gesture took more than a fifth longer, the sign of a page that could not
|
|
850
|
+
* keep up with what it was sent.
|
|
851
|
+
*/
|
|
852
|
+
interface StepOverrun {
|
|
853
|
+
step: number;
|
|
854
|
+
id?: string;
|
|
855
|
+
do: string;
|
|
856
|
+
plannedMs: number;
|
|
857
|
+
tookMs: number;
|
|
858
|
+
overrunMs: number;
|
|
859
|
+
}
|
|
860
|
+
/** The share past the plan that counts as an overrun. */
|
|
861
|
+
declare const OVERRUN_SHARE = 0.2;
|
|
862
|
+
/** A plan shorter than this is not measured: its noise is the round trip. */
|
|
863
|
+
declare const OVERRUN_MIN_PLANNED_MS = 100;
|
|
864
|
+
/**
|
|
865
|
+
* The least a gesture must run over to count, ms: below it the difference
|
|
866
|
+
* is one slow sample or round trip on a busy machine, never a page that
|
|
867
|
+
* fell behind, however short the gesture.
|
|
868
|
+
*/
|
|
869
|
+
declare const OVERRUN_MIN_MS = 100;
|
|
870
|
+
/** The overrun, ms, or null when the gesture kept to its plan. */
|
|
871
|
+
declare function overrunOf(plannedMs: number, tookMs: number): number | null;
|
|
872
|
+
/** The overruns in one line for the log and the done event, or null when none. */
|
|
873
|
+
declare function overrunLine(overruns: readonly StepOverrun[]): string | null;
|
|
874
|
+
|
|
583
875
|
/**
|
|
584
876
|
* The action script — the declarative recipe an agent (or human) writes to
|
|
585
877
|
* drive a take. Small on purpose: selectors + a handful of verbs. The recorder
|
|
@@ -611,21 +903,51 @@ interface ActionsFile {
|
|
|
611
903
|
* or stored.
|
|
612
904
|
*/
|
|
613
905
|
setup?: SetupStep[];
|
|
906
|
+
/**
|
|
907
|
+
* When the first page counts as ready for step 1: `dom` (default),
|
|
908
|
+
* once its DOM has made no change for a beat after `load`, at most 8 s;
|
|
909
|
+
* `networkidle`, once the network has gone quiet, at most 45 s, which a
|
|
910
|
+
* page holding a socket open never does. `--ready` overrides.
|
|
911
|
+
*/
|
|
912
|
+
ready?: ReadyMode;
|
|
913
|
+
/**
|
|
914
|
+
* Every gesture waits on the app after it (`settle` on a step overrides):
|
|
915
|
+
* a canvas that falls behind under load finishes the last stroke before
|
|
916
|
+
* the next one starts.
|
|
917
|
+
*/
|
|
918
|
+
settle?: boolean;
|
|
614
919
|
steps: ActionStep[];
|
|
615
920
|
}
|
|
616
921
|
type ActionStep = ({
|
|
617
922
|
do: 'wait';
|
|
618
923
|
ms: number;
|
|
619
|
-
}
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Rest the pointer on `selector`'s centre, or on the viewport point
|
|
927
|
+
* (`x`, `y`) when no selector names the spot (a place on a canvas).
|
|
928
|
+
*/
|
|
929
|
+
| {
|
|
620
930
|
do: 'hover';
|
|
621
|
-
selector
|
|
931
|
+
selector?: string;
|
|
932
|
+
x?: number;
|
|
933
|
+
y?: number;
|
|
622
934
|
ms?: number;
|
|
623
935
|
}
|
|
624
|
-
/**
|
|
936
|
+
/**
|
|
937
|
+
* Press `selector`'s centre, or the viewport point (`x`, `y`). `ms` is
|
|
938
|
+
* the settle after the press (default 150). `count` 2 or 3 is a real
|
|
939
|
+
* double or triple click; `button` presses the right or middle one;
|
|
940
|
+
* `modifiers` are held for the press (a Shift-click) and released after.
|
|
941
|
+
*/
|
|
625
942
|
| {
|
|
626
943
|
do: 'click';
|
|
627
|
-
selector
|
|
944
|
+
selector?: string;
|
|
945
|
+
x?: number;
|
|
946
|
+
y?: number;
|
|
628
947
|
ms?: number;
|
|
948
|
+
count?: 1 | 2 | 3;
|
|
949
|
+
button?: ClickButton;
|
|
950
|
+
modifiers?: DragModifier[];
|
|
629
951
|
}
|
|
630
952
|
/**
|
|
631
953
|
* Type into `selector`. The recorder clicks the field first, which is what
|
|
@@ -655,14 +977,18 @@ type ActionStep = ({
|
|
|
655
977
|
ms?: number;
|
|
656
978
|
}
|
|
657
979
|
/**
|
|
658
|
-
*
|
|
659
|
-
* `selector` when given, with no click)
|
|
660
|
-
*
|
|
661
|
-
*
|
|
980
|
+
* A key, a chord or a sequence, each recorded as a typing ping where the
|
|
981
|
+
* pointer is (or on `selector` when given, with no click), never as the
|
|
982
|
+
* key itself. `key` names one control key (Escape closes what a click
|
|
983
|
+
* opened, Enter submits what was typed); `keys` names a chord
|
|
984
|
+
* (`"Shift+D"`, `"Mod+Z"`, with `Mod` Meta on macOS and Control
|
|
985
|
+
* elsewhere), a plain key (`"r"`, a tool's shortcut) or a sequence
|
|
986
|
+
* (`["v", "Escape"]`). Words are typed with `type`.
|
|
662
987
|
*/
|
|
663
988
|
| {
|
|
664
989
|
do: 'press';
|
|
665
|
-
key
|
|
990
|
+
key?: PressKey;
|
|
991
|
+
keys?: string | string[];
|
|
666
992
|
selector?: string;
|
|
667
993
|
ms?: number;
|
|
668
994
|
}
|
|
@@ -680,14 +1006,32 @@ type ActionStep = ({
|
|
|
680
1006
|
* Press-move-release — real edits (drag an element on the stage canvas,
|
|
681
1007
|
* slide a range input, move a timeline clip). Start = the selector's center
|
|
682
1008
|
* when given, else (x, y); end = (tx, ty); eased over ms (default 700).
|
|
1009
|
+
* `path` draws instead: viewport points (inline, or a file beside the
|
|
1010
|
+
* actions file), the press at the first and the release at the last,
|
|
1011
|
+
* replayed by the points' own times or evenly over ms. `modifiers` are
|
|
1012
|
+
* held for the whole drag.
|
|
683
1013
|
*/
|
|
684
1014
|
| {
|
|
685
1015
|
do: 'drag';
|
|
686
1016
|
selector?: string;
|
|
687
1017
|
x?: number;
|
|
688
1018
|
y?: number;
|
|
689
|
-
tx
|
|
690
|
-
ty
|
|
1019
|
+
tx?: number;
|
|
1020
|
+
ty?: number;
|
|
1021
|
+
path?: PathPoint[] | string;
|
|
1022
|
+
modifiers?: DragModifier[];
|
|
1023
|
+
ms?: number;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Put words or a file (an image, an SVG) on the clipboard and paste it
|
|
1027
|
+
* the way a person's ⌘V does, into `selector` (clicked first) or where
|
|
1028
|
+
* the focus is. A file is read beside the actions file, by a plain name.
|
|
1029
|
+
*/
|
|
1030
|
+
| {
|
|
1031
|
+
do: 'paste';
|
|
1032
|
+
selector?: string;
|
|
1033
|
+
text?: string;
|
|
1034
|
+
file?: string;
|
|
691
1035
|
ms?: number;
|
|
692
1036
|
}
|
|
693
1037
|
/**
|
|
@@ -764,6 +1108,13 @@ type ActionStep = ({
|
|
|
764
1108
|
* what is read. Only what the flow names, never a secret it did not.
|
|
765
1109
|
*/
|
|
766
1110
|
read?: string;
|
|
1111
|
+
/**
|
|
1112
|
+
* After the gesture, wait until the page has made no DOM change and
|
|
1113
|
+
* painted no new frame for a beat (at most a few seconds): an app that
|
|
1114
|
+
* falls behind catches up before the next step. The script's top-level
|
|
1115
|
+
* `settle` is the default.
|
|
1116
|
+
*/
|
|
1117
|
+
settle?: boolean;
|
|
767
1118
|
};
|
|
768
1119
|
/** What an `await` waits for: exactly one of the three. */
|
|
769
1120
|
interface AwaitUntil {
|
|
@@ -821,6 +1172,15 @@ declare function guardVerdict(actions: Pick<ActionsFile, 'steps'>, confirm: read
|
|
|
821
1172
|
* take directory to land in there.
|
|
822
1173
|
*/
|
|
823
1174
|
declare function hostedRefusals(actions: Pick<ActionsFile, 'steps'>): string[];
|
|
1175
|
+
/**
|
|
1176
|
+
* The files a script names beside itself, in order: a drag's path file
|
|
1177
|
+
* and a paste's file. A host reads them before anything is captured.
|
|
1178
|
+
*/
|
|
1179
|
+
declare function scriptFiles(actions: Pick<ActionsFile, 'steps'>): {
|
|
1180
|
+
step: number;
|
|
1181
|
+
name: string;
|
|
1182
|
+
as: 'path' | 'paste';
|
|
1183
|
+
}[];
|
|
824
1184
|
|
|
825
1185
|
/**
|
|
826
1186
|
* The wall check: did the recorder land where it was asked, or in front of
|
|
@@ -965,12 +1325,19 @@ interface RecordResult {
|
|
|
965
1325
|
pace: PaceReport;
|
|
966
1326
|
/** steps whose selector never became visible — the take continued without them. */
|
|
967
1327
|
skipped: SkippedStep[];
|
|
968
|
-
/** the
|
|
1328
|
+
/** `ready: "networkidle"` was asked for and the first load never reached it (recording proceeded anyway). */
|
|
969
1329
|
navTimeout: boolean;
|
|
1330
|
+
/** How the first load settled before step 1. */
|
|
1331
|
+
ready: ReadyReport;
|
|
970
1332
|
/** A wall the caller let through; null when the recorder landed where it was asked. */
|
|
971
1333
|
wall: WallVerdict | null;
|
|
972
1334
|
/** Sensitive-looking text seen in the frame (kind + place, never the string). */
|
|
973
1335
|
exposures: NonNullable<RecordingMeta['exposures']>;
|
|
1336
|
+
/**
|
|
1337
|
+
* INFORMATION, never a failure and never in `exposures`: the signed-in
|
|
1338
|
+
* account's own name and its recent-items lists (also `meta.notices`).
|
|
1339
|
+
*/
|
|
1340
|
+
notices: NonNullable<RecordingMeta['notices']>;
|
|
974
1341
|
/** The script's masks with how many elements each reached; hits 0 hid nothing. */
|
|
975
1342
|
masks: NonNullable<RecordingMeta['masks']>;
|
|
976
1343
|
/**
|
|
@@ -997,10 +1364,18 @@ interface RecordResult {
|
|
|
997
1364
|
skips: RecordingSkip[];
|
|
998
1365
|
/** The files `download` steps kept (also `meta.outputs`). */
|
|
999
1366
|
outputs: RecordingOutput[];
|
|
1367
|
+
/** The sound the page played, each heard stretch in footage seconds (also `meta.media`). */
|
|
1368
|
+
media: NonNullable<RecordingMeta['media']>;
|
|
1000
1369
|
/** An `await` timed out and the take stopped there; null when it ran to the end. */
|
|
1001
1370
|
stopped: StoppedTake | null;
|
|
1002
1371
|
/** The page's own console errors, each distinct one with its full text and how often it came. */
|
|
1003
1372
|
pageErrors: PageError[];
|
|
1373
|
+
/**
|
|
1374
|
+
* Paced gestures that ran more than a fifth past their script (a drag's
|
|
1375
|
+
* travel, a path's timing, typing, a key sequence): the page could not
|
|
1376
|
+
* keep up with what it was sent. Also on `meta.steps[].overrunMs`.
|
|
1377
|
+
*/
|
|
1378
|
+
overruns: StepOverrun[];
|
|
1004
1379
|
/** The actions as recorded, `url` resolved: what a take directory writes back. */
|
|
1005
1380
|
actions: ActionsFile;
|
|
1006
1381
|
}
|
|
@@ -1060,6 +1435,18 @@ interface RecordOpts {
|
|
|
1060
1435
|
* A rehearsal fetches the file and keeps nothing.
|
|
1061
1436
|
*/
|
|
1062
1437
|
outputs?: OutputSink;
|
|
1438
|
+
/**
|
|
1439
|
+
* Where the sound the page played is kept, fetched with the page's own
|
|
1440
|
+
* cookies at the end of the take. A host that hands in none still
|
|
1441
|
+
* records `meta.media`, with no file; a rehearsal fetches nothing.
|
|
1442
|
+
*/
|
|
1443
|
+
sounds?: SoundSink;
|
|
1444
|
+
/**
|
|
1445
|
+
* The files the script names beside itself (a drag's path file, a
|
|
1446
|
+
* paste's file). A host that hands in none cannot run such a step: the
|
|
1447
|
+
* script is refused before anything is captured.
|
|
1448
|
+
*/
|
|
1449
|
+
files?: ScriptFiles;
|
|
1063
1450
|
/** Print every page error whole, each time it comes; by default one cut line per distinct error. */
|
|
1064
1451
|
verbose?: boolean;
|
|
1065
1452
|
/**
|
|
@@ -1067,6 +1454,12 @@ interface RecordOpts {
|
|
|
1067
1454
|
* an `await`'s start, a heartbeat while it waits, and its end.
|
|
1068
1455
|
*/
|
|
1069
1456
|
onEvent?: (event: Record<string, unknown>) => void;
|
|
1457
|
+
/**
|
|
1458
|
+
* A step the page stopped answering in (it ran into its bound) stops the
|
|
1459
|
+
* take there, as an `await` that timed out does (`--strict`). Without
|
|
1460
|
+
* it the step is a skip and the take goes on.
|
|
1461
|
+
*/
|
|
1462
|
+
stopOnHang?: boolean;
|
|
1070
1463
|
/** How often a long `await` reports it is still waiting (default 30 s). */
|
|
1071
1464
|
awaitHeartbeatMs?: number;
|
|
1072
1465
|
}
|
|
@@ -1085,6 +1478,8 @@ declare function jpegDims(buf: Uint8Array): {
|
|
|
1085
1478
|
/** Base64 to bytes without Node's Buffer, so the fleet's isolate runs it too. */
|
|
1086
1479
|
declare function base64Bytes(data: string): Uint8Array;
|
|
1087
1480
|
declare function recordTake(browser: RecorderBrowser, url: string, actions: ActionsFile, sink: FrameSink, log: (msg: string) => void, opts?: RecordOpts): Promise<RecordResult>;
|
|
1481
|
+
/** Why a step ended at its bound, with the real time it took. */
|
|
1482
|
+
declare function hangReason(verb: string, ms: number, boundMs: number): string;
|
|
1088
1483
|
declare function extForType(type: string | undefined): string;
|
|
1089
1484
|
/** A download's type from its name's extension, as `{ contentType }`, or `{}` when the extension says nothing. */
|
|
1090
1485
|
declare function typeOfName(name: string): {
|
|
@@ -1141,6 +1536,25 @@ interface FreshPlanOptions {
|
|
|
1141
1536
|
activity?: readonly number[] | null;
|
|
1142
1537
|
}
|
|
1143
1538
|
declare function planFreshTake(artifact: RecordingArtifact, recordingName: string, opts?: FreshPlanOptions): ProjectDoc;
|
|
1539
|
+
/**
|
|
1540
|
+
* The export a FRESH document opens on, sized to what the footage holds:
|
|
1541
|
+
* the ingest's default resolution, or the largest the recording supports
|
|
1542
|
+
* without upscaling when that is less (a 1280×720 take exports 720p, not a
|
|
1543
|
+
* 1080p that `vos validate` then warns upscales). Never raised. In place,
|
|
1544
|
+
* and returned.
|
|
1545
|
+
*/
|
|
1546
|
+
declare function withFittingExport(doc: ProjectDoc): ProjectDoc;
|
|
1547
|
+
/**
|
|
1548
|
+
* The sound the page played (`meta.media`) as the document's audio: one
|
|
1549
|
+
* clip per heard stretch, keyed to the fetched file, placed at the OUTPUT
|
|
1550
|
+
* time of the footage moment it was heard (through the segments, the speed
|
|
1551
|
+
* spans and the freezes), stamped `source: 'page'`. A re-plan replaces only
|
|
1552
|
+
* those clips, so a clip someone added stays. An auto speed span over a
|
|
1553
|
+
* heard stretch is dropped first, or the footage would race ahead of what
|
|
1554
|
+
* the page was saying. A stretch with no fetched file (a hosted take, a
|
|
1555
|
+
* fetch that failed) places nothing. In place, and returned.
|
|
1556
|
+
*/
|
|
1557
|
+
declare function withPageSound(doc: ProjectDoc, media: readonly RecordingPageMedia[] | undefined): ProjectDoc;
|
|
1144
1558
|
/**
|
|
1145
1559
|
* The footage's segments split at every cut an `await` made
|
|
1146
1560
|
* (`meta.skips[].at`, source seconds), so each cut is a real segment
|
|
@@ -1306,6 +1720,17 @@ declare class TakeClock {
|
|
|
1306
1720
|
pause(wallMs: number): number;
|
|
1307
1721
|
/** The camera rolls again; returns the wall ms the stretch lasted. */
|
|
1308
1722
|
resume(wallMs: number): number;
|
|
1723
|
+
/**
|
|
1724
|
+
* The parts of a wall-clock stretch the camera saw: each sub-stretch
|
|
1725
|
+
* outside every off-camera wait, as its wall start (`wallMs`), its
|
|
1726
|
+
* footage start (`atMs`) and its length. A stretch an `await` cut in two
|
|
1727
|
+
* comes back as two; one wholly off camera as none.
|
|
1728
|
+
*/
|
|
1729
|
+
onCamera(fromWall: number, toWall: number): {
|
|
1730
|
+
wallMs: number;
|
|
1731
|
+
atMs: number;
|
|
1732
|
+
ms: number;
|
|
1733
|
+
}[];
|
|
1309
1734
|
/**
|
|
1310
1735
|
* A captured frame's footage ms, or null when it is not later than the
|
|
1311
1736
|
* last frame kept (the frame is dropped; see the module comment).
|
|
@@ -1313,4 +1738,311 @@ declare class TakeClock {
|
|
|
1313
1738
|
placeFrame(captureWallMs: number): number | null;
|
|
1314
1739
|
}
|
|
1315
1740
|
|
|
1316
|
-
|
|
1741
|
+
interface SurfaceSize {
|
|
1742
|
+
width: number;
|
|
1743
|
+
height: number;
|
|
1744
|
+
}
|
|
1745
|
+
interface SurfaceFit {
|
|
1746
|
+
before: SurfaceSize;
|
|
1747
|
+
after: SurfaceSize;
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Make the surface the viewport. Returns what it measured, or null when
|
|
1751
|
+
* nothing could be measured; never throws, because a take made as before
|
|
1752
|
+
* is better than no take.
|
|
1753
|
+
*/
|
|
1754
|
+
declare function fitSurfaceToViewport(cdp: RecorderCdp, width: number, height: number): Promise<SurfaceFit | null>;
|
|
1755
|
+
|
|
1756
|
+
/**
|
|
1757
|
+
* The sound a page plays during a take. The recorder captures frames, and
|
|
1758
|
+
* a product's sound (a voice preview, a video playing in the page) would
|
|
1759
|
+
* be lost with nothing to say it was there. Most of it is a media element,
|
|
1760
|
+
* so the recorder watches every `<video>` and `<audio>` the page plays, in
|
|
1761
|
+
* the page and in its same-origin frames, and keeps WHEN each was heard
|
|
1762
|
+
* and WHAT media time it played, so a clip of the same file can be placed
|
|
1763
|
+
* exactly under the footage (`meta.media`, then the plan's audio clips).
|
|
1764
|
+
*
|
|
1765
|
+
* Two pieces run IN the page, both plain JavaScript strings (a function
|
|
1766
|
+
* serialized out of a bundle picks up the bundler's helpers and dies in the
|
|
1767
|
+
* page): an init script that notes every play, pause, seek, end and volume
|
|
1768
|
+
* change into a log on the top window, and a drain that hands the log over
|
|
1769
|
+
* with the page's clock. The rest is here, in the recorder's clock: the
|
|
1770
|
+
* page's instants become wall instants, and the take clock turns those
|
|
1771
|
+
* into footage stretches, split wherever an `await` took the camera away.
|
|
1772
|
+
*/
|
|
1773
|
+
|
|
1774
|
+
/**
|
|
1775
|
+
* The init script: runs at document start in every frame. A frame that
|
|
1776
|
+
* cannot reach the top window (another origin) notes nothing. A media
|
|
1777
|
+
* element out of the document (`new Audio(url).play()`) is reached by
|
|
1778
|
+
* the play it is started with.
|
|
1779
|
+
*/
|
|
1780
|
+
declare const MEDIA_WATCH_SCRIPT = "(() => {\n let host\n try { host = window.top; void host.document.title } catch { return }\n if (window.__vosMediaWatch) return\n window.__vosMediaWatch = true\n if (!host.__vosMedia) {\n const M = { log: [], seq: 0, els: new Set(), doc: Math.random().toString(36).slice(2) }\n M.fields = (el, kind) => ({\n id: el.__vosMediaId,\n tag: el.tagName === 'VIDEO' ? 'video' : 'audio',\n src: el.currentSrc || el.src || '',\n kind,\n wall: Date.now(),\n t: el.currentTime,\n rate: el.playbackRate || 1,\n paused: el.paused || el.ended,\n audible: !el.muted && el.volume > 0,\n volume: el.volume,\n duration: isFinite(el.duration) ? el.duration : null,\n ab: typeof el.webkitAudioDecodedByteCount === 'number' ? el.webkitAudioDecodedByteCount : null,\n })\n host.__vosMedia = M\n }\n const M = host.__vosMedia\n const EVENTS = ['playing', 'pause', 'ended', 'seeking', 'seeked', 'volumechange', 'ratechange', 'emptied']\n const note = (e) => {\n const el = e.target\n if (!el || typeof el.currentTime !== 'number' || !/^(VIDEO|AUDIO)$/.test(el.tagName || '')) return\n if (e.__vosNoted) return\n e.__vosNoted = true\n if (!el.__vosMediaId) {\n if (M.els.size >= 50) return\n el.__vosMediaId = ++M.seq\n M.els.add(el)\n }\n if (M.log.length < 2000) M.log.push(M.fields(el, e.type))\n }\n for (const type of EVENTS) document.addEventListener(type, note, true)\n const proto = window.HTMLMediaElement && window.HTMLMediaElement.prototype\n if (proto && proto.play) {\n const play = proto.play\n proto.play = function () {\n if (!this.__vosListening && !this.isConnected) {\n this.__vosListening = true\n for (const type of EVENTS) this.addEventListener(type, note)\n }\n return play.apply(this, arguments)\n }\n }\n})()";
|
|
1781
|
+
/**
|
|
1782
|
+
* The drain: the notes since the last drain, a `tick` for every element
|
|
1783
|
+
* followed (what it is doing now, which catches a note that never came),
|
|
1784
|
+
* the page's clock, and which document the log belongs to (a navigation
|
|
1785
|
+
* starts a new one).
|
|
1786
|
+
*/
|
|
1787
|
+
declare const MEDIA_DRAIN_PROBE = "(() => {\n const M = window.__vosMedia\n if (!M) return { now: Date.now(), doc: null, notes: [] }\n const notes = M.log.splice(0)\n M.els.forEach((el) => notes.push(M.fields(el, 'tick')))\n return { now: Date.now(), doc: M.doc, notes }\n})()";
|
|
1788
|
+
/** One note from the page. */
|
|
1789
|
+
interface MediaNote {
|
|
1790
|
+
id: number;
|
|
1791
|
+
tag: 'video' | 'audio';
|
|
1792
|
+
src: string;
|
|
1793
|
+
kind: string;
|
|
1794
|
+
wall: number;
|
|
1795
|
+
t: number;
|
|
1796
|
+
rate: number;
|
|
1797
|
+
paused: boolean;
|
|
1798
|
+
audible: boolean;
|
|
1799
|
+
volume: number;
|
|
1800
|
+
duration: number | null;
|
|
1801
|
+
ab: number | null;
|
|
1802
|
+
}
|
|
1803
|
+
interface MediaDrain {
|
|
1804
|
+
now: number;
|
|
1805
|
+
doc: string | null;
|
|
1806
|
+
notes: MediaNote[];
|
|
1807
|
+
}
|
|
1808
|
+
/** A page sound with its full source, for the fetch; `meta` is what the take keeps. */
|
|
1809
|
+
interface HeardSound {
|
|
1810
|
+
meta: RecordingPageMedia;
|
|
1811
|
+
/** the address as the page played it, query and all: fetched, never stored */
|
|
1812
|
+
src: string;
|
|
1813
|
+
}
|
|
1814
|
+
/** What `meta.media[].src` keeps of an address: origin and path; a `blob:` or `data:` by its scheme. */
|
|
1815
|
+
declare function soundSrcForMeta(src: string): string;
|
|
1816
|
+
/**
|
|
1817
|
+
* Accumulates the page's notes across a take, in the recorder's clock.
|
|
1818
|
+
* `add` takes each drain with the recorder's own `Date.now()` at the
|
|
1819
|
+
* moment it came back; `heard` closes what is still playing at the end
|
|
1820
|
+
* and maps every stretch to footage time.
|
|
1821
|
+
*/
|
|
1822
|
+
declare class PageSoundLog {
|
|
1823
|
+
private byKey;
|
|
1824
|
+
private openKeyOf;
|
|
1825
|
+
private doc;
|
|
1826
|
+
private lastWall;
|
|
1827
|
+
add(drain: MediaDrain | null, nodeNow: number, step?: number): void;
|
|
1828
|
+
private note;
|
|
1829
|
+
private close;
|
|
1830
|
+
private closeAll;
|
|
1831
|
+
/**
|
|
1832
|
+
* Every element the page played, each heard stretch as footage seconds
|
|
1833
|
+
* `at` with media seconds `from`..`to`, none past `endMs` of footage.
|
|
1834
|
+
* A video whose decoder never decoded a byte of sound played none.
|
|
1835
|
+
*/
|
|
1836
|
+
heard(clock: TakeClock, endWall: number, endMs: number): HeardSound[];
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
/**
|
|
1840
|
+
* A step never runs past its own bound, however the page behaves. A page
|
|
1841
|
+
* that stops answering (a busy loop, a renderer stuck after a reload the
|
|
1842
|
+
* page made itself) leaves every driver call pending for good, and a poll
|
|
1843
|
+
* that reads it waits on the read, not on its clock: an `expect` that said
|
|
1844
|
+
* "for 5 s" ran for fifteen minutes. So the page the recorder drives is
|
|
1845
|
+
* BOUNDED: while a step runs, every call into it is raced against the
|
|
1846
|
+
* step's deadline, and once the deadline has passed a pending call answers
|
|
1847
|
+
* `undefined` (what an absent element answers) instead of waiting. The
|
|
1848
|
+
* step then ends as a skip with the real time it took; the call it left
|
|
1849
|
+
* pending is abandoned, never awaited.
|
|
1850
|
+
*
|
|
1851
|
+
* Between steps the same race holds with a looser bound, so the take's
|
|
1852
|
+
* own finish (its last reads, the page's title) cannot hang either.
|
|
1853
|
+
*/
|
|
1854
|
+
/** A gesture's bound past what it planned to take. */
|
|
1855
|
+
declare const STEP_MARGIN_MS = 30000;
|
|
1856
|
+
/** A check's (`expect`, `await`) bound past its own timeout. */
|
|
1857
|
+
declare const WAIT_MARGIN_MS = 5000;
|
|
1858
|
+
/** One call's bound while no step runs (a navigation has its own 45 s timeout). */
|
|
1859
|
+
declare const IDLE_CALL_MS = 60000;
|
|
1860
|
+
/** The bound of one step, in ms: its own timeout for a check, its plan plus a margin for a gesture. */
|
|
1861
|
+
declare function stepBoundMs(step: {
|
|
1862
|
+
do: string;
|
|
1863
|
+
timeout?: number;
|
|
1864
|
+
until?: unknown;
|
|
1865
|
+
reload?: boolean;
|
|
1866
|
+
}, plannedMs: number, defaults: {
|
|
1867
|
+
expectS: number;
|
|
1868
|
+
awaitS: number;
|
|
1869
|
+
downloadS: number;
|
|
1870
|
+
}): number;
|
|
1871
|
+
declare class StepBound {
|
|
1872
|
+
private readonly now;
|
|
1873
|
+
private readonly idleMs;
|
|
1874
|
+
private deadline;
|
|
1875
|
+
private started;
|
|
1876
|
+
private hung;
|
|
1877
|
+
constructor(now?: () => number, idleMs?: number);
|
|
1878
|
+
/** A step begins: calls into the page answer within `boundMs` from now. */
|
|
1879
|
+
start(boundMs: number): void;
|
|
1880
|
+
/** The step ends: whether it ran into its bound, and how long it really took. */
|
|
1881
|
+
end(): {
|
|
1882
|
+
hung: boolean;
|
|
1883
|
+
ms: number;
|
|
1884
|
+
boundMs: number;
|
|
1885
|
+
};
|
|
1886
|
+
/** The pending call, or `undefined` once the bound has passed. */
|
|
1887
|
+
race<T>(p: Promise<T>): Promise<T>;
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* `target` with every method that answers a promise raced through the
|
|
1891
|
+
* guard, and every object it hands back (`page.mouse`, `locator()`,
|
|
1892
|
+
* `.first()`) guarded the same way. A call is made on the real object, so
|
|
1893
|
+
* nothing private to the driver is touched through the proxy.
|
|
1894
|
+
*/
|
|
1895
|
+
declare function bounded<T extends object>(target: T, bound: StepBound): T;
|
|
1896
|
+
|
|
1897
|
+
/**
|
|
1898
|
+
* The `paste` step, the pure parts: what a file is pasted as, and the two
|
|
1899
|
+
* page scripts that put an item where a person's ⌘V finds it.
|
|
1900
|
+
*
|
|
1901
|
+
* The first rung is the browser's own clipboard: the item is written with
|
|
1902
|
+
* `navigator.clipboard.write` (the context is granted clipboard access
|
|
1903
|
+
* first), and the recorder then presses the platform's paste chord, so the
|
|
1904
|
+
* page receives a TRUSTED keydown and a trusted `paste` event carrying the
|
|
1905
|
+
* item, and a page that reads `navigator.clipboard.read()` itself sees it
|
|
1906
|
+
* there too. A headless browser keeps its own clipboard, so nothing on the
|
|
1907
|
+
* machine's clipboard is touched.
|
|
1908
|
+
*
|
|
1909
|
+
* When the browser refuses the write (no clipboard access on this driver,
|
|
1910
|
+
* a type it will not hold), the second rung dispatches the paste in the
|
|
1911
|
+
* page: a `paste` event with a `DataTransfer` holding the item on the
|
|
1912
|
+
* focused element, and `navigator.clipboard.read`/`readText` answering the
|
|
1913
|
+
* item for the rest of the take. Those events are untrusted, which the
|
|
1914
|
+
* recorder says.
|
|
1915
|
+
*
|
|
1916
|
+
* Both scripts are strings, the one evaluate shape every driver shares,
|
|
1917
|
+
* and neither carries anything the cursor track keeps: a paste is a typing
|
|
1918
|
+
* ping, never its contents.
|
|
1919
|
+
*/
|
|
1920
|
+
/** How long after a dispatched paste is first read its item stays on the async reads, ms. */
|
|
1921
|
+
declare const PASTE_READ_WINDOW_MS = 300;
|
|
1922
|
+
/** How long a dispatched paste's item stays on the async reads when nothing reads it, ms. */
|
|
1923
|
+
declare const PASTE_UNREAD_MS = 3000;
|
|
1924
|
+
/** The most bytes a pasted file may hold. */
|
|
1925
|
+
declare const PASTE_MAX_BYTES: number;
|
|
1926
|
+
/** The file types a paste takes, by extension. */
|
|
1927
|
+
declare const PASTE_TYPES: Record<string, string>;
|
|
1928
|
+
/** A pasted file's type from its name, or null when a paste does not take it. */
|
|
1929
|
+
declare function pasteTypeOf(name: string): string | null;
|
|
1930
|
+
/** What a paste puts on the clipboard: words, or a file's bytes (base64) with its type and name. */
|
|
1931
|
+
interface PasteItem {
|
|
1932
|
+
text?: string;
|
|
1933
|
+
b64?: string;
|
|
1934
|
+
type?: string;
|
|
1935
|
+
name?: string;
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Rung one: write the item to the browser's clipboard. Answers 'ok', or
|
|
1939
|
+
* 'refused: <why>'. An SVG the clipboard will not hold as an image is
|
|
1940
|
+
* written as its markup alone.
|
|
1941
|
+
*/
|
|
1942
|
+
declare function stageClipboardScript(item: PasteItem): string;
|
|
1943
|
+
/**
|
|
1944
|
+
* Rung two: the paste dispatched in the page, on the focused element (or
|
|
1945
|
+
* the body), with the item in its `clipboardData`; and the async clipboard
|
|
1946
|
+
* reads answer the item from here on. A plain field the page did not take
|
|
1947
|
+
* the paste for gets the words inserted, as the browser's own paste does.
|
|
1948
|
+
* Answers 'ok', or 'failed: <why>'.
|
|
1949
|
+
*/
|
|
1950
|
+
declare function syntheticPasteScript(item: PasteItem, opts?: {
|
|
1951
|
+
original?: boolean;
|
|
1952
|
+
}): string;
|
|
1953
|
+
|
|
1954
|
+
/**
|
|
1955
|
+
* A gesture that waits on the app (`settle`): after the gesture, the
|
|
1956
|
+
* recorder waits until the page has made no DOM change and painted no new
|
|
1957
|
+
* frame for a beat, at most a few seconds. A heavy canvas falls behind
|
|
1958
|
+
* under load, and a script paced by the wall clock then outruns it: the
|
|
1959
|
+
* next stroke is drawn before the app has processed the last one.
|
|
1960
|
+
*
|
|
1961
|
+
* The DOM half is the recorder's one DOM-settle reader (`domSettled`, the
|
|
1962
|
+
* same an `await`'s reload lets the page finish with). The paint half is
|
|
1963
|
+
* the screencast: it emits a frame only when the compositor painted
|
|
1964
|
+
* something new, a canvas redrawn by `requestAnimationFrame` included, and
|
|
1965
|
+
* it is read from here rather than by an animation frame in the page,
|
|
1966
|
+
* which a background tab never fires. The two are one count: the page's
|
|
1967
|
+
* DOM changes plus the frames captured so far. A rehearsal captures no
|
|
1968
|
+
* frames, so there it waits on the DOM alone.
|
|
1969
|
+
*/
|
|
1970
|
+
|
|
1971
|
+
/** The quiet a settle waits for: no DOM change and no new frame, ms. */
|
|
1972
|
+
declare const APP_QUIET_MS = 300;
|
|
1973
|
+
/** The longest a settle waits on a page that never stops changing, ms. */
|
|
1974
|
+
declare const APP_SETTLE_CAP_MS = 4000;
|
|
1975
|
+
/** The verbs a settle follows: the ones that send the page input. */
|
|
1976
|
+
declare const SETTLE_VERBS: Set<string>;
|
|
1977
|
+
/**
|
|
1978
|
+
* How long ago the page last changed its DOM, in ms by the page's own clock,
|
|
1979
|
+
* or -1 when this is the first look at this document (the reader is
|
|
1980
|
+
* installed now and knows no history). A string, the one evaluate shape
|
|
1981
|
+
* every driver shares.
|
|
1982
|
+
*/
|
|
1983
|
+
declare const CHANGE_AGE_PROBE = "(() => {\n const w = window;\n if (!w.__vosChangeAt) {\n w.__vosChangeAt = { at: Date.now() };\n new MutationObserver(() => { w.__vosChangeAt.at = Date.now() })\n .observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n return -1;\n }\n return Date.now() - w.__vosChangeAt.at;\n})()";
|
|
1984
|
+
/** Whether a step waits on the app after its gesture: its own `settle`, else the script's. */
|
|
1985
|
+
declare function settleOn(step: {
|
|
1986
|
+
do: string;
|
|
1987
|
+
settle?: boolean;
|
|
1988
|
+
}, scriptDefault: boolean | undefined): boolean;
|
|
1989
|
+
/**
|
|
1990
|
+
* Wait for the app: no DOM change and no new captured frame for `quietMs`,
|
|
1991
|
+
* at most `capMs`. `frames` reads how many frames the capture has kept.
|
|
1992
|
+
*/
|
|
1993
|
+
declare function appSettled(page: Pick<RecorderPage, 'evaluate'>, frames: () => number, opts: {
|
|
1994
|
+
sleep: (ms: number) => Promise<void>;
|
|
1995
|
+
now?: () => number;
|
|
1996
|
+
quietMs?: number;
|
|
1997
|
+
capMs?: number;
|
|
1998
|
+
/** ms since the last captured frame, or null when none was captured */
|
|
1999
|
+
sinceFrame?: () => number | null;
|
|
2000
|
+
}): Promise<{
|
|
2001
|
+
settled: boolean;
|
|
2002
|
+
ms: number;
|
|
2003
|
+
}>;
|
|
2004
|
+
|
|
2005
|
+
/**
|
|
2006
|
+
* The files a script names beside itself, read before the take: a drag's
|
|
2007
|
+
* path file (points as JSON) and a paste's file (its bytes). The host
|
|
2008
|
+
* hands in where they come from (`ScriptFiles`); the names are plain file
|
|
2009
|
+
* names, checked by `validateActions`, so a script can only name what sits
|
|
2010
|
+
* next to it.
|
|
2011
|
+
*/
|
|
2012
|
+
|
|
2013
|
+
interface LoadedFiles {
|
|
2014
|
+
/** a drag's path, read from its file, by step index */
|
|
2015
|
+
paths: Map<number, {
|
|
2016
|
+
points: PathSample[];
|
|
2017
|
+
timed: boolean;
|
|
2018
|
+
}>;
|
|
2019
|
+
/** a paste's file, by step index */
|
|
2020
|
+
pastes: Map<number, {
|
|
2021
|
+
bytes: Uint8Array;
|
|
2022
|
+
type: string;
|
|
2023
|
+
name: string;
|
|
2024
|
+
}>;
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Read every file the script names. Answers the files, or the problem in
|
|
2028
|
+
* words (the step, the file, what is wrong) for the caller to refuse the
|
|
2029
|
+
* take with.
|
|
2030
|
+
*/
|
|
2031
|
+
declare function loadScriptFiles(actions: Pick<ActionsFile, 'steps'>, source: ScriptFiles | undefined): Promise<LoadedFiles | {
|
|
2032
|
+
problem: string;
|
|
2033
|
+
}>;
|
|
2034
|
+
/** A read path as inline points: its own times when it had them, its pressure when it carried one. */
|
|
2035
|
+
declare function inlinePoints(path: {
|
|
2036
|
+
points: PathSample[];
|
|
2037
|
+
timed: boolean;
|
|
2038
|
+
}): PathPoint[];
|
|
2039
|
+
/**
|
|
2040
|
+
* The script as it ran, self-contained: every path a file held is written
|
|
2041
|
+
* inline, so the script a take keeps (and the one a push embeds, a fetch
|
|
2042
|
+
* writes back and a hosted recorder runs) re-records the same strokes with
|
|
2043
|
+
* the file gone. A paste's file stays a name: it is bytes, kept beside the
|
|
2044
|
+
* script by the host.
|
|
2045
|
+
*/
|
|
2046
|
+
declare function inlineScript<T extends Pick<ActionsFile, 'steps'>>(actions: T, files: LoadedFiles): T;
|
|
2047
|
+
|
|
2048
|
+
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, KEY_ROUNDTRIP_MS, KEY_SEQUENCE_GAP_MS, type LoadedFiles, MAX_KEYS, MAX_PATH_POINTS, 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, 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 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 StoppedTake, type SurfaceFit, type SurfaceSize, TRAILING_HOLD_MS, TakeClock, WAIT_MARGIN_MS, WALL_PROBE, type WaitCondition, WallError, type WallKind, type WallVerdict, 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, keySequencePlanMs, loadScriptFiles, maskInitScript, modifierBits, nameFromUrl, noticeAdvice, noticeLine, outputName, overrunLine, overrunOf, paceLine, paceReport, pageErrorLine, parseHeaders, pasteTypeOf, pathDurationMs, plainName, planFreshTake, pointerTravelMs, pollCondition, pressSequence, publicUrl, readAllBytes, readCondition, readMediaSource, readPath, readyLine, readyProblem, realSleep, recordTake, redirectedAway, reloadUntilReady, resolveChord, resolveModifier, resolveSetupText, runSetup, sayCondition, sayDuration, schedulePath, scriptFiles, scrollByScript, scrollTravelMs, settleMs, settleOn, settleVerdict, setupEnvNames, soundSrcForMeta, splitChord, splitSegmentsAtSkips, stageClipboardScript, stepBoundMs, syntheticPasteScript, typeOfName, untilReady, urlSchemeProblem, validateActions, validateSetup, visibleTextScript, wallVerdict, withFittingExport, withPageSound };
|