@vosjs/render-core 0.6.0 → 0.7.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 +418 -4
- package/dist/record/index.js +979 -159
- 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, Backdrop, RecordingArtifact, ProjectDoc } from '@vosjs/studio-core';
|
|
1
|
+
import { RecordingMeta, CursorEvent, RecordingSkip, RecordingOutput, Backdrop, RecordingArtifact, ProjectDoc } from '@vosjs/studio-core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The recorder's driver: what it needs from a browser, as a STRUCTURAL
|
|
@@ -21,13 +21,38 @@ interface RecorderRect {
|
|
|
21
21
|
}
|
|
22
22
|
interface RecorderLocator {
|
|
23
23
|
waitFor(opts: {
|
|
24
|
-
state: 'visible';
|
|
24
|
+
state: 'visible' | 'attached';
|
|
25
25
|
timeout: number;
|
|
26
26
|
}): Promise<unknown>;
|
|
27
27
|
scrollIntoViewIfNeeded(): Promise<unknown>;
|
|
28
28
|
boundingBox(): Promise<RecorderRect | null>;
|
|
29
29
|
click(): Promise<unknown>;
|
|
30
30
|
fill(value: string): Promise<unknown>;
|
|
31
|
+
/** what an `expect` and an `await` read, and the disabled-click report */
|
|
32
|
+
isVisible(): Promise<boolean>;
|
|
33
|
+
isEnabled(opts?: {
|
|
34
|
+
timeout?: number;
|
|
35
|
+
}): Promise<boolean>;
|
|
36
|
+
/** a `download` src reads the media element's source through the locator engine */
|
|
37
|
+
evaluate<R>(fn: (el: RecorderMediaElement) => R): Promise<R>;
|
|
38
|
+
}
|
|
39
|
+
/** What a `download` src reads off the element its selector matched. */
|
|
40
|
+
interface RecorderMediaElement {
|
|
41
|
+
currentSrc?: string;
|
|
42
|
+
src?: string;
|
|
43
|
+
getAttribute(name: string): string | null;
|
|
44
|
+
}
|
|
45
|
+
/** A browser download, as Playwright's `Download` answers it. */
|
|
46
|
+
interface RecorderDownload {
|
|
47
|
+
suggestedFilename(): string;
|
|
48
|
+
createReadStream(): Promise<AsyncIterable<unknown> | null>;
|
|
49
|
+
}
|
|
50
|
+
/** The page's own request context (its cookies), as Playwright's `APIResponse` answers. */
|
|
51
|
+
interface RecorderApiResponse {
|
|
52
|
+
ok(): boolean;
|
|
53
|
+
status(): number;
|
|
54
|
+
headers(): Record<string, string>;
|
|
55
|
+
body(): Promise<Uint8Array>;
|
|
31
56
|
}
|
|
32
57
|
interface RecorderLocatorRoot {
|
|
33
58
|
first(): RecorderLocator;
|
|
@@ -46,6 +71,21 @@ interface RecorderPage {
|
|
|
46
71
|
}): Promise<RecorderResponse | null>;
|
|
47
72
|
url(): string;
|
|
48
73
|
title(): Promise<string>;
|
|
74
|
+
/** An `await` with `reload: true`; absent, the recorder opens the page's URL again. */
|
|
75
|
+
reload?(opts: {
|
|
76
|
+
waitUntil: 'networkidle';
|
|
77
|
+
timeout: number;
|
|
78
|
+
}): Promise<RecorderResponse | null>;
|
|
79
|
+
/** A `download` step's click; absent, the recorder says the browser reports none. */
|
|
80
|
+
waitForEvent?(event: 'download', opts: {
|
|
81
|
+
timeout: number;
|
|
82
|
+
}): Promise<RecorderDownload>;
|
|
83
|
+
/** A `download` step's `src` fetch, with the page's cookies; absent, it fetches in the page. */
|
|
84
|
+
request?: {
|
|
85
|
+
get(url: string, opts?: {
|
|
86
|
+
timeout?: number;
|
|
87
|
+
}): Promise<RecorderApiResponse>;
|
|
88
|
+
};
|
|
49
89
|
evaluate<T>(pageFunction: string | (() => T)): Promise<T>;
|
|
50
90
|
on(event: 'console', cb: (m: RecorderConsoleMessage) => void): unknown;
|
|
51
91
|
locator(selector: string): RecorderLocatorRoot;
|
|
@@ -114,6 +154,14 @@ interface RecorderBrowser {
|
|
|
114
154
|
interface FrameSink {
|
|
115
155
|
frame(file: string, bytes: Uint8Array): Promise<void> | void;
|
|
116
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Where a `download` step's file goes: the CLI writes `outputs/<name>` in
|
|
159
|
+
* the take directory. A host that hands in none cannot keep a download,
|
|
160
|
+
* and the recorder refuses such a script before capturing anything.
|
|
161
|
+
*/
|
|
162
|
+
interface OutputSink {
|
|
163
|
+
save(name: string, bytes: Uint8Array, contentType?: string): Promise<void> | void;
|
|
164
|
+
}
|
|
117
165
|
/** The clock and the pauses, injectable so a rehearsal runs fast. */
|
|
118
166
|
interface RecorderClock {
|
|
119
167
|
now(): number;
|
|
@@ -332,6 +380,36 @@ declare function deadWarns(r: DeadReport): boolean;
|
|
|
332
380
|
/** The dead time in words, naming the steps and what to cut. */
|
|
333
381
|
declare function deadLine(r: DeadReport): string;
|
|
334
382
|
|
|
383
|
+
/**
|
|
384
|
+
* The page's own console errors, as the recorder reports them. A page
|
|
385
|
+
* under recording is someone else's, and its errors are context, never the
|
|
386
|
+
* story: a third party's Content-Security-Policy violation runs to a few
|
|
387
|
+
* kilobytes and repeats on every load, which buried the step lines. So the
|
|
388
|
+
* log gets one line per distinct error, cut short, and a repeat is counted
|
|
389
|
+
* instead of printed; the full text stays in the result (and a verbose
|
|
390
|
+
* host prints every one whole).
|
|
391
|
+
*/
|
|
392
|
+
/** How much of an error a log line keeps. */
|
|
393
|
+
declare const PAGE_ERROR_LINE_CHARS = 160;
|
|
394
|
+
interface PageError {
|
|
395
|
+
/** the full text, whitespace as the page wrote it */
|
|
396
|
+
text: string;
|
|
397
|
+
count: number;
|
|
398
|
+
}
|
|
399
|
+
/** One line of an error: whitespace collapsed, cut to the line's length. */
|
|
400
|
+
declare function pageErrorLine(text: string): string;
|
|
401
|
+
declare class PageErrorLog {
|
|
402
|
+
private readonly verbose;
|
|
403
|
+
private readonly seen;
|
|
404
|
+
constructor(verbose?: boolean);
|
|
405
|
+
/** The line to log for this error now, or null when it repeats one already said. */
|
|
406
|
+
add(text: string): string | null;
|
|
407
|
+
/** Every distinct error with how often it came, in the order first seen. */
|
|
408
|
+
entries(): PageError[];
|
|
409
|
+
/** The closing lines: each error that repeated, with its count. */
|
|
410
|
+
repeats(): string[];
|
|
411
|
+
}
|
|
412
|
+
|
|
335
413
|
/**
|
|
336
414
|
* What a signed-in take SHOWS. Getting past the login is half the problem;
|
|
337
415
|
* the other half is that the account's own data is now in the frame: an
|
|
@@ -383,7 +461,7 @@ declare const EXPOSURE_MATCHERS_SRC = "(() => {\n const EMAIL = /[A-Za-z0-9._%+
|
|
|
383
461
|
* Skips anything inside a masked element, password fields, and text the eye
|
|
384
462
|
* cannot see. Reports kind + a selector-ish name for the element + its rect.
|
|
385
463
|
*/
|
|
386
|
-
declare const EXPOSURE_PROBE = "(() => {\n const { kindsIn } = (() => {\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})()\n const vw = innerWidth, vh = innerHeight\n // A selector that reaches THIS element and no other, so it can be pasted\n // into \"mask\" as it is: climb until the path is unique, and say which\n // sibling wherever a tag-and-class alone would match several.\n const segment = (el) => {\n const tid = el.getAttribute('data-testid')\n if (tid) return
|
|
464
|
+
declare const EXPOSURE_PROBE = "(() => {\n const { kindsIn } = (() => {\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})()\n const vw = innerWidth, vh = innerHeight\n // A selector that reaches THIS element and no other, so it can be pasted\n // into \"mask\" as it is: climb until the path is unique, and say which\n // sibling wherever a tag-and-class alone would match several. What the\n // page wrote for people and tests comes first (an id, a test id, an\n // aria-label, a field's name or placeholder); utility classes are a\n // layout's, and they come last. A value that itself holds what the scan\n // looks for is never used: the selector would quote it.\n const attr = (v) => '\"' + v.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"') + '\"'\n const quiet = (v) => !!v && v.length <= 80 && !kindsIn(v).length\n const segment = (el) => {\n const tag = el.tagName.toLowerCase()\n if (el.id && /^[A-Za-z_][\\w-]*$/.test(el.id) && !/\\d{2,}/.test(el.id) && quiet(el.id)) return tag + '#' + el.id\n const tid = el.getAttribute('data-testid')\n if (quiet(tid)) return tag + '[data-testid=' + attr(tid) + ']'\n const label = el.getAttribute('aria-label')\n if (quiet(label)) return tag + '[aria-label=' + attr(label) + ']'\n if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)) {\n const nm = el.getAttribute('name')\n if (quiet(nm)) return tag + '[name=' + attr(nm) + ']'\n const ph = el.getAttribute('placeholder')\n if (quiet(ph)) return tag + '[placeholder=' + attr(ph) + ']'\n }\n const cls = [...el.classList].filter((c) => /^[A-Za-z_-][\\w-]*$/.test(c)).slice(0, 2)\n let seg = el.tagName.toLowerCase() + (cls.length ? '.' + cls.join('.') : '')\n const p = el.parentElement\n if (p && [...p.children].filter((c) => c.matches(seg)).length > 1)\n seg += ':nth-child(' + ([...p.children].indexOf(el) + 1) + ')'\n return seg\n }\n const name = (el) => {\n const parts = []\n for (let cur = el; cur && cur !== document.body && cur !== document.documentElement; cur = cur.parentElement) {\n parts.unshift(segment(cur))\n let n = 2\n try { n = document.querySelectorAll(parts.join(' > ')).length } catch {}\n // a bare tag is unique HERE and nowhere else: give it its parent too\n const bare = parts.length === 1 && !/[#.\\[]/.test(parts[0])\n if ((n === 1 && !bare) || parts.length >= 5) break\n }\n return parts.join(' > ')\n }\n const seen = (el) => {\n if (el.closest('[data-vos-masked]')) return null\n const r = el.getBoundingClientRect()\n if (r.width < 2 || r.height < 2) return null\n if (r.bottom <= 0 || r.right <= 0 || r.top >= vh || r.left >= vw) return null\n const s = getComputedStyle(el)\n if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity === 0) return null\n return r\n }\n const out = [], keys = new Set()\n const add = (el, text) => {\n const kinds = kindsIn(text)\n if (!kinds.length) return\n const r = seen(el)\n if (!r) return\n const selector = name(el)\n for (const kind of kinds) {\n const k = kind + '|' + selector\n if (keys.has(k) || out.length >= 40) continue\n keys.add(k)\n out.push({ kind, selector, rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } })\n }\n }\n const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT)\n for (let n; (n = walker.nextNode()); ) {\n const el = n.parentElement\n if (!el || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/.test(el.tagName)) continue\n add(el, n.nodeValue)\n }\n for (const el of document.querySelectorAll('input, textarea')) {\n if (/^(password|hidden|file|checkbox|radio)$/.test(el.type)) continue\n add(el, el.value)\n }\n const hits = {}\n for (const [sel, n] of Object.entries(window.__vosMaskHits || {})) hits[sel] = n\n return { exposures: out, maskHits: hits }\n})()";
|
|
387
465
|
/**
|
|
388
466
|
* The mask, as an init script: it runs at document start on EVERY navigation,
|
|
389
467
|
* so a masked value is hidden before the page's first paint, and a
|
|
@@ -611,7 +689,60 @@ type ActionStep = ({
|
|
|
611
689
|
tx: number;
|
|
612
690
|
ty: number;
|
|
613
691
|
ms?: number;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Check that the page is as the flow expects, with no cursor move: an
|
|
695
|
+
* element visible (`selector`; with `enabled`, enabled or disabled), some
|
|
696
|
+
* visible text containing `text`, or `gone` no longer visible. Exactly
|
|
697
|
+
* one of the three. Waits up to `timeout` seconds (default 5); a
|
|
698
|
+
* condition that never holds is a skip with its reason.
|
|
699
|
+
*/
|
|
700
|
+
| {
|
|
701
|
+
do: 'expect';
|
|
702
|
+
selector?: string;
|
|
703
|
+
text?: string;
|
|
704
|
+
gone?: string;
|
|
705
|
+
enabled?: boolean;
|
|
706
|
+
timeout?: number;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Wait OFF CAMERA for something slow (a render, a generated result, a
|
|
710
|
+
* deploy): the capture stops, the predicate is polled every `poll`
|
|
711
|
+
* seconds (default 2) for up to `timeout` (default 600), reloading the
|
|
712
|
+
* page between polls when `reload` is set, and the capture resumes when
|
|
713
|
+
* it holds. The footage has no gap and no frozen stretch; the cut lands
|
|
714
|
+
* in `meta.skips[]`. On timeout, `onTimeout: "fail"` (the default) stops
|
|
715
|
+
* the take and `"continue"` resumes it.
|
|
716
|
+
*/
|
|
717
|
+
| {
|
|
718
|
+
do: 'await';
|
|
719
|
+
until: AwaitUntil;
|
|
720
|
+
timeout?: number;
|
|
721
|
+
poll?: number;
|
|
722
|
+
reload?: boolean;
|
|
723
|
+
onTimeout?: 'fail' | 'continue';
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Keep a file the page makes: click `selector` and wait for the
|
|
727
|
+
* browser's download, or fetch the `currentSrc` of the video, audio or
|
|
728
|
+
* img element `src` names, with the page's own cookies. Saved under the
|
|
729
|
+
* take's `outputs/` as `as`, else the name the page suggested, and listed
|
|
730
|
+
* in `meta.outputs[]`.
|
|
731
|
+
*/
|
|
732
|
+
| {
|
|
733
|
+
do: 'download';
|
|
734
|
+
selector?: string;
|
|
735
|
+
src?: string;
|
|
736
|
+
as?: string;
|
|
614
737
|
}) & {
|
|
738
|
+
/**
|
|
739
|
+
* `"spend"`: the step costs something real (credits, a purchase, a
|
|
740
|
+
* message sent). Only a click, a press or a type carries it, and it
|
|
741
|
+
* needs an `id`. A rehearsal hovers it and never presses it; a recording
|
|
742
|
+
* presses it only when the command names it (`--confirm <id>`) and
|
|
743
|
+
* refuses before capturing anything otherwise.
|
|
744
|
+
*/
|
|
745
|
+
guard?: StepGuard;
|
|
615
746
|
/**
|
|
616
747
|
* Optional stable identity: anchors in doc.json name a step by this
|
|
617
748
|
* id (else by index), so a step can move or gain neighbours across script
|
|
@@ -634,11 +765,62 @@ type ActionStep = ({
|
|
|
634
765
|
*/
|
|
635
766
|
read?: string;
|
|
636
767
|
};
|
|
768
|
+
/** What an `await` waits for: exactly one of the three. */
|
|
769
|
+
interface AwaitUntil {
|
|
770
|
+
selector?: string;
|
|
771
|
+
text?: string;
|
|
772
|
+
gone?: string;
|
|
773
|
+
}
|
|
774
|
+
/** The guards a step may carry: one today, a step that spends. */
|
|
775
|
+
declare const STEP_GUARDS: readonly ["spend"];
|
|
776
|
+
type StepGuard = (typeof STEP_GUARDS)[number];
|
|
777
|
+
/** How long an `expect` waits by default, in seconds. */
|
|
778
|
+
declare const EXPECT_TIMEOUT_S = 5;
|
|
779
|
+
/** An `await`'s defaults, in seconds. */
|
|
780
|
+
declare const AWAIT_TIMEOUT_S = 600;
|
|
781
|
+
declare const AWAIT_POLL_S = 2;
|
|
782
|
+
/** How long a `download` click waits for the browser's download, in seconds. */
|
|
783
|
+
declare const DOWNLOAD_TIMEOUT_S = 30;
|
|
637
784
|
/** The keys a `press` step may name: navigation and control, never a character. */
|
|
638
785
|
declare const PRESS_KEYS: readonly ["Escape", "Enter", "Tab", "Backspace", "Delete", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown", "Space"];
|
|
639
786
|
type PressKey = (typeof PRESS_KEYS)[number];
|
|
640
787
|
/** Structural validation with actionable messages. Returns [] when valid. */
|
|
641
788
|
declare function validateActions(value: unknown): string[];
|
|
789
|
+
/**
|
|
790
|
+
* Why a name is not one a download may be saved under, or null. A plain
|
|
791
|
+
* file name only: the file lands in the take's `outputs/` and nowhere
|
|
792
|
+
* else, so no separator, no `..`, nothing hidden, nothing empty.
|
|
793
|
+
*/
|
|
794
|
+
declare function fileNameProblem(name: string): string | null;
|
|
795
|
+
/** A guarded step of a script: where it is, what it is, the id that confirms it. */
|
|
796
|
+
interface GuardedStep {
|
|
797
|
+
step: number;
|
|
798
|
+
id: string;
|
|
799
|
+
do: string;
|
|
800
|
+
guard: StepGuard;
|
|
801
|
+
}
|
|
802
|
+
/** The guarded steps of a script, in order. */
|
|
803
|
+
declare function guardedSteps(actions: Pick<ActionsFile, 'steps'>): GuardedStep[];
|
|
804
|
+
/**
|
|
805
|
+
* Whether a recording may run with the confirmations it was given: every
|
|
806
|
+
* guarded step named by `--confirm`, and every name a guarded step. A
|
|
807
|
+
* refusal is one sentence naming the steps and the flag, said before
|
|
808
|
+
* anything is captured.
|
|
809
|
+
*/
|
|
810
|
+
declare function guardVerdict(actions: Pick<ActionsFile, 'steps'>, confirm: readonly string[]): {
|
|
811
|
+
ok: true;
|
|
812
|
+
} | {
|
|
813
|
+
ok: false;
|
|
814
|
+
unconfirmed: string[];
|
|
815
|
+
unknown: string[];
|
|
816
|
+
message: string;
|
|
817
|
+
};
|
|
818
|
+
/**
|
|
819
|
+
* What a hosted recorder cannot do with a script, in words, or []: a
|
|
820
|
+
* guarded step has nobody to confirm it on a fleet, and a download has no
|
|
821
|
+
* take directory to land in there.
|
|
822
|
+
*/
|
|
823
|
+
declare function hostedRefusals(actions: Pick<ActionsFile, 'steps'>): string[];
|
|
642
824
|
|
|
643
825
|
/**
|
|
644
826
|
* The wall check: did the recorder land where it was asked, or in front of
|
|
@@ -724,6 +906,10 @@ declare const WALL_PROBE = "(() => {\n const shown = (el) => {\n const r = e
|
|
|
724
906
|
* same events, meta and frame index for the same script.
|
|
725
907
|
*/
|
|
726
908
|
|
|
909
|
+
/** How often a long `await` says it is still waiting, in ms. */
|
|
910
|
+
declare const AWAIT_HEARTBEAT_MS = 30000;
|
|
911
|
+
/** An `await` that held sooner than this cut nothing: no skip, no segment split. */
|
|
912
|
+
declare const MIN_SKIP_MS = 250;
|
|
727
913
|
/**
|
|
728
914
|
* The headers a request carries, or null to leave it alone: a job's
|
|
729
915
|
* headers (a preview's bypass token) go to the ORIGIN the take is of and
|
|
@@ -742,8 +928,27 @@ interface FrameRec {
|
|
|
742
928
|
interface SkippedStep {
|
|
743
929
|
/** index into actions.steps */
|
|
744
930
|
step: number;
|
|
931
|
+
id?: string;
|
|
745
932
|
do: string;
|
|
746
933
|
selector: string;
|
|
934
|
+
/** why, when it is not a selector that never appeared: an expect that never held, a download that never came */
|
|
935
|
+
reason?: string;
|
|
936
|
+
}
|
|
937
|
+
/** A click whose target was disabled when it was pressed: the press happened, and likely did nothing. */
|
|
938
|
+
interface DisabledClick {
|
|
939
|
+
step: number;
|
|
940
|
+
id?: string;
|
|
941
|
+
selector: string;
|
|
942
|
+
}
|
|
943
|
+
/** An `await` that timed out with `onTimeout: "fail"`: the take stopped there. */
|
|
944
|
+
interface StoppedTake {
|
|
945
|
+
step: number;
|
|
946
|
+
id?: string;
|
|
947
|
+
reason: string;
|
|
948
|
+
}
|
|
949
|
+
/** Thrown before anything is captured: a guarded step the command did not confirm, a download with nowhere to land. */
|
|
950
|
+
declare class RecordRefusedError extends Error {
|
|
951
|
+
constructor(message: string);
|
|
747
952
|
}
|
|
748
953
|
/** A stretch with no screencast frames — the page pixels did not change. */
|
|
749
954
|
interface FreezeSpan {
|
|
@@ -784,6 +989,18 @@ interface RecordResult {
|
|
|
784
989
|
dead: DeadReport;
|
|
785
990
|
/** The take reached --max-duration and stopped there; later steps did not run. */
|
|
786
991
|
capped: boolean;
|
|
992
|
+
/** Clicks pressed on a disabled target (reported; `--strict` fails on them). */
|
|
993
|
+
disabled: DisabledClick[];
|
|
994
|
+
/** The ids of the guarded steps this take pressed, because the command named them. */
|
|
995
|
+
confirmed: string[];
|
|
996
|
+
/** The cuts `await` steps made (also `meta.skips`). */
|
|
997
|
+
skips: RecordingSkip[];
|
|
998
|
+
/** The files `download` steps kept (also `meta.outputs`). */
|
|
999
|
+
outputs: RecordingOutput[];
|
|
1000
|
+
/** An `await` timed out and the take stopped there; null when it ran to the end. */
|
|
1001
|
+
stopped: StoppedTake | null;
|
|
1002
|
+
/** The page's own console errors, each distinct one with its full text and how often it came. */
|
|
1003
|
+
pageErrors: PageError[];
|
|
787
1004
|
/** The actions as recorded, `url` resolved: what a take directory writes back. */
|
|
788
1005
|
actions: ActionsFile;
|
|
789
1006
|
}
|
|
@@ -831,6 +1048,27 @@ interface RecordOpts {
|
|
|
831
1048
|
env?: Record<string, string | undefined>;
|
|
832
1049
|
/** The platform the take is recorded on, for `meta.platform`. */
|
|
833
1050
|
platform?: RecordingMeta['platform'];
|
|
1051
|
+
/**
|
|
1052
|
+
* The ids of the guarded steps this recording may press (`--confirm`).
|
|
1053
|
+
* A guarded step not named here refuses the take before anything is
|
|
1054
|
+
* captured; a rehearsal hovers every guarded step and presses none.
|
|
1055
|
+
*/
|
|
1056
|
+
confirm?: readonly string[];
|
|
1057
|
+
/**
|
|
1058
|
+
* Where a `download` step's file goes. A host that hands in none cannot
|
|
1059
|
+
* keep a download: such a script is refused before anything is captured.
|
|
1060
|
+
* A rehearsal fetches the file and keeps nothing.
|
|
1061
|
+
*/
|
|
1062
|
+
outputs?: OutputSink;
|
|
1063
|
+
/** Print every page error whole, each time it comes; by default one cut line per distinct error. */
|
|
1064
|
+
verbose?: boolean;
|
|
1065
|
+
/**
|
|
1066
|
+
* Structured progress for a host that reports it (the CLI's `--json`):
|
|
1067
|
+
* an `await`'s start, a heartbeat while it waits, and its end.
|
|
1068
|
+
*/
|
|
1069
|
+
onEvent?: (event: Record<string, unknown>) => void;
|
|
1070
|
+
/** How often a long `await` reports it is still waiting (default 30 s). */
|
|
1071
|
+
awaitHeartbeatMs?: number;
|
|
834
1072
|
}
|
|
835
1073
|
/**
|
|
836
1074
|
* The expression a scroll tick evaluates: move the first scrollable
|
|
@@ -847,6 +1085,11 @@ declare function jpegDims(buf: Uint8Array): {
|
|
|
847
1085
|
/** Base64 to bytes without Node's Buffer, so the fleet's isolate runs it too. */
|
|
848
1086
|
declare function base64Bytes(data: string): Uint8Array;
|
|
849
1087
|
declare function recordTake(browser: RecorderBrowser, url: string, actions: ActionsFile, sink: FrameSink, log: (msg: string) => void, opts?: RecordOpts): Promise<RecordResult>;
|
|
1088
|
+
declare function extForType(type: string | undefined): string;
|
|
1089
|
+
/** A download's type from its name's extension, as `{ contentType }`, or `{}` when the extension says nothing. */
|
|
1090
|
+
declare function typeOfName(name: string): {
|
|
1091
|
+
contentType?: string;
|
|
1092
|
+
};
|
|
850
1093
|
|
|
851
1094
|
/**
|
|
852
1095
|
* The recording cap as the recorder applies it, pure. WHAT the cap is
|
|
@@ -898,5 +1141,176 @@ interface FreshPlanOptions {
|
|
|
898
1141
|
activity?: readonly number[] | null;
|
|
899
1142
|
}
|
|
900
1143
|
declare function planFreshTake(artifact: RecordingArtifact, recordingName: string, opts?: FreshPlanOptions): ProjectDoc;
|
|
1144
|
+
/**
|
|
1145
|
+
* The footage's segments split at every cut an `await` made
|
|
1146
|
+
* (`meta.skips[].at`, source seconds), so each cut is a real segment
|
|
1147
|
+
* boundary: the timeline shows it, a transition can sit on it, a trim can
|
|
1148
|
+
* take either side. A cut at an edge or outside a segment splits nothing,
|
|
1149
|
+
* and a segment of another media is left alone. A split segment keeps its
|
|
1150
|
+
* arrival on its first piece and its leaving on its last.
|
|
1151
|
+
*/
|
|
1152
|
+
declare function splitSegmentsAtSkips(segments: ProjectDoc['segments'], skips: readonly {
|
|
1153
|
+
at: number;
|
|
1154
|
+
}[] | undefined): ProjectDoc['segments'];
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* What an `expect` and an `await` wait for: a condition on the page, read
|
|
1158
|
+
* without touching it (no cursor, no input), polled until it holds or the
|
|
1159
|
+
* time runs out. `expect` polls on camera for a few seconds; `await` polls
|
|
1160
|
+
* off camera for as long as a slow result takes. One reader for both, so
|
|
1161
|
+
* "appeared" means the same thing in either.
|
|
1162
|
+
*/
|
|
1163
|
+
|
|
1164
|
+
interface WaitCondition {
|
|
1165
|
+
/** an element matching this selector is visible */
|
|
1166
|
+
selector?: string;
|
|
1167
|
+
/** some visible text on the page contains this */
|
|
1168
|
+
text?: string;
|
|
1169
|
+
/** no element matching this selector is visible */
|
|
1170
|
+
gone?: string;
|
|
1171
|
+
/** with `selector`: the element is enabled (true) or disabled (false) */
|
|
1172
|
+
enabled?: boolean;
|
|
1173
|
+
}
|
|
1174
|
+
/** What the condition found the last time it looked: what a miss is said from. */
|
|
1175
|
+
interface ConditionState {
|
|
1176
|
+
holds: boolean;
|
|
1177
|
+
/** with `selector`: whether a match was visible */
|
|
1178
|
+
visible?: boolean;
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* The visible text of the page contains `text`: `innerText` is what the
|
|
1182
|
+
* page renders, so a hidden element's words never count.
|
|
1183
|
+
*/
|
|
1184
|
+
declare function visibleTextScript(text: string): string;
|
|
1185
|
+
/** Read the condition once. A page mid-navigation reads as "not yet". */
|
|
1186
|
+
declare function readCondition(page: RecorderPage, c: WaitCondition): Promise<ConditionState>;
|
|
1187
|
+
/** What a poll found: whether the condition held, after how long, and the last read. */
|
|
1188
|
+
interface PollResult {
|
|
1189
|
+
ok: boolean;
|
|
1190
|
+
ms: number;
|
|
1191
|
+
last: ConditionState;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Poll the condition every `everyMs` until it holds or `timeoutMs` pass.
|
|
1195
|
+
* `between` runs before each look but the first (an `await`'s reload).
|
|
1196
|
+
* With `confirm`, a hold counts only once it has held on that many reads
|
|
1197
|
+
* in a row, `confirm.gapMs` apart: a page that renders after network quiet
|
|
1198
|
+
* reads half-built for a moment, and a `gone` condition holds on a list
|
|
1199
|
+
* that has not been drawn yet. The clock is the wall's: a rehearsal waits
|
|
1200
|
+
* as long as a take would, because how long a result takes is what a
|
|
1201
|
+
* rehearsal is for.
|
|
1202
|
+
*/
|
|
1203
|
+
declare function pollCondition(page: RecorderPage, c: WaitCondition, opts: {
|
|
1204
|
+
timeoutMs: number;
|
|
1205
|
+
everyMs: number;
|
|
1206
|
+
sleep: (ms: number) => Promise<void>;
|
|
1207
|
+
now?: () => number;
|
|
1208
|
+
between?: () => Promise<void>;
|
|
1209
|
+
confirm?: {
|
|
1210
|
+
reads: number;
|
|
1211
|
+
gapMs: number;
|
|
1212
|
+
};
|
|
1213
|
+
}): Promise<PollResult>;
|
|
1214
|
+
/** The DOM counts as settled after this long with no change. */
|
|
1215
|
+
declare const DOM_QUIET_MS = 800;
|
|
1216
|
+
/** The longest a settle waits on a page that never stops changing. */
|
|
1217
|
+
declare const DOM_SETTLE_CAP_MS = 5000;
|
|
1218
|
+
/**
|
|
1219
|
+
* The page's own count of DOM changes: an observer installed once per
|
|
1220
|
+
* document, read back as a number. A string, the one evaluate shape every
|
|
1221
|
+
* driver shares.
|
|
1222
|
+
*/
|
|
1223
|
+
declare const DOM_CHANGE_PROBE = "(() => {\n if (!window.__vosDomChanges) {\n window.__vosDomChanges = { n: 0 };\n new MutationObserver((list) => { window.__vosDomChanges.n += list.length })\n .observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n }\n return window.__vosDomChanges.n;\n})()";
|
|
1224
|
+
/**
|
|
1225
|
+
* Wait until the DOM has not changed for `quietMs`, at most `capMs`: what
|
|
1226
|
+
* a page does after its network went quiet (a list rendered from data, a
|
|
1227
|
+
* hydration). The count is polled from here rather than timed in the
|
|
1228
|
+
* page, because a background tab throttles its own timers.
|
|
1229
|
+
*/
|
|
1230
|
+
declare function domSettled(page: RecorderPage, opts: {
|
|
1231
|
+
sleep: (ms: number) => Promise<void>;
|
|
1232
|
+
now?: () => number;
|
|
1233
|
+
quietMs?: number;
|
|
1234
|
+
capMs?: number;
|
|
1235
|
+
tickMs?: number;
|
|
1236
|
+
}): Promise<{
|
|
1237
|
+
settled: boolean;
|
|
1238
|
+
ms: number;
|
|
1239
|
+
}>;
|
|
1240
|
+
/** Seconds as the recorder says them: `5 s`, `2.5 s`, `3 min 20 s`. */
|
|
1241
|
+
declare function sayDuration(ms: number): string;
|
|
1242
|
+
/** What the condition waits for, in words: `'#result' to appear`. */
|
|
1243
|
+
declare function sayCondition(c: WaitCondition): string;
|
|
1244
|
+
/** Why the condition did not hold, in words: `'Generate' stayed disabled for 5 s`. */
|
|
1245
|
+
declare function conditionMiss(c: WaitCondition, last: ConditionState, ms: number): string;
|
|
1246
|
+
|
|
1247
|
+
/** The most a download keeps: a file the page made, never a disk's worth. */
|
|
1248
|
+
declare const OUTPUT_MAX_BYTES: number;
|
|
1249
|
+
/**
|
|
1250
|
+
* A name the page suggested, made plain: its last path segment, control
|
|
1251
|
+
* characters and leading dots removed, or `fallback` when nothing usable
|
|
1252
|
+
* is left. A page chooses this name, so it is cleaned, never trusted.
|
|
1253
|
+
*/
|
|
1254
|
+
declare function plainName(suggested: string, fallback: string): string;
|
|
1255
|
+
/** The file name a URL's path ends in, decoded, or ''. */
|
|
1256
|
+
declare function nameFromUrl(url: string): string;
|
|
1257
|
+
/**
|
|
1258
|
+
* The name a download is saved under: the step's `as` when it gave one,
|
|
1259
|
+
* else the page's suggestion made plain, made unique among the names this
|
|
1260
|
+
* take already used (`clip.mp4`, `clip-2.mp4`).
|
|
1261
|
+
*/
|
|
1262
|
+
declare function outputName(asked: string | undefined, suggested: string, fallback: string, used: ReadonlySet<string>): string;
|
|
1263
|
+
/** Read a stream of byte chunks whole, refusing past `max` bytes. */
|
|
1264
|
+
declare function readAllBytes(stream: AsyncIterable<unknown>, max?: number): Promise<Uint8Array>;
|
|
1265
|
+
/**
|
|
1266
|
+
* Read a media element's source, run in the page on the element the
|
|
1267
|
+
* step's selector matched through the locator engine: its `currentSrc`
|
|
1268
|
+
* (what it is actually playing), else its `src`, else the attribute; ''
|
|
1269
|
+
* while it has none yet. Self-contained, since it is serialized into the
|
|
1270
|
+
* page.
|
|
1271
|
+
*/
|
|
1272
|
+
declare const readMediaSource: (el: RecorderMediaElement) => string;
|
|
1273
|
+
/**
|
|
1274
|
+
* The expression that fetches a URL inside the page, for what the page
|
|
1275
|
+
* alone can read (a `blob:` or a `data:` source): base64 and the type, or
|
|
1276
|
+
* an error in words. Same-origin requests carry the page's cookies.
|
|
1277
|
+
*/
|
|
1278
|
+
declare function fetchInPageScript(url: string): string;
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* The take's clock: ONE path from a wall-clock instant to footage time,
|
|
1282
|
+
* for the cursor events and for every screencast frame alike.
|
|
1283
|
+
*
|
|
1284
|
+
* Footage time is wall time since `t0` minus every off-camera stretch (an
|
|
1285
|
+
* `await`) that began before the instant; an instant inside a stretch reads
|
|
1286
|
+
* as the stretch's start, which is where the cut is. A frame is placed by
|
|
1287
|
+
* the instant it was CAPTURED, never by when its callback ran: a frame in
|
|
1288
|
+
* flight when the capture paused lands after the resume and must still sit
|
|
1289
|
+
* before the cut.
|
|
1290
|
+
*
|
|
1291
|
+
* Frame times are strictly increasing. The screencast does not promise it:
|
|
1292
|
+
* a frame can arrive after a later one, and a frame without a capture
|
|
1293
|
+
* timestamp is stamped by its arrival, a second clock. A frame whose time
|
|
1294
|
+
* is not later than the last one kept holds older pixels than what the
|
|
1295
|
+
* footage already shows, so it is DROPPED (never re-stamped forward, which
|
|
1296
|
+
* would put a stale picture after a newer one).
|
|
1297
|
+
*/
|
|
1298
|
+
declare class TakeClock {
|
|
1299
|
+
readonly t0: number;
|
|
1300
|
+
private readonly pauses;
|
|
1301
|
+
private lastFrameMs;
|
|
1302
|
+
constructor(t0: number);
|
|
1303
|
+
/** Footage ms at a wall-clock instant (`Date.now()` space). */
|
|
1304
|
+
footageMs(wallMs: number): number;
|
|
1305
|
+
/** The camera stops at this instant; returns the cut's footage ms. */
|
|
1306
|
+
pause(wallMs: number): number;
|
|
1307
|
+
/** The camera rolls again; returns the wall ms the stretch lasted. */
|
|
1308
|
+
resume(wallMs: number): number;
|
|
1309
|
+
/**
|
|
1310
|
+
* A captured frame's footage ms, or null when it is not later than the
|
|
1311
|
+
* last frame kept (the frame is dropped; see the module comment).
|
|
1312
|
+
*/
|
|
1313
|
+
placeFrame(captureWallMs: number): number | null;
|
|
1314
|
+
}
|
|
901
1315
|
|
|
902
|
-
export { type ActionStep, type ActionsFile, type Arrival, DEAD_SHARE_WARN_PCT, DEAD_STRETCH_WARN_MS, type DeadReport, type DeadStep, EXPOSURE_ADVICE, EXPOSURE_MATCHERS_SRC, EXPOSURE_PROBE, type EncodePageOptions, type Exposure, type ExposureKind, ExposureLog, type FrameRec, type FrameSink, type FreezeSpan, type FreshPlanOptions, MOTION_TICK_MS, type MaskReport, type MaskRule, POINTER_MAX_MS, POINTER_MIN_MS, POINTER_MS_PER_PX, PRESS_HOLD_MS, PRESS_KEYS, PRESS_LEAD_MS, type PaceReport, type PressKey, QUIET_MS, READ_BEAT_MS, RESPONSE_MS, type RecordOpts, type RecordResult, type RecorderBrowser, type RecorderCdp, type RecorderClock, type RecorderConsoleMessage, type RecorderContext, type RecorderLocator, type RecorderLocatorRoot, 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, SetupEnvError, SetupError, type SetupResult, type SetupStep, type SetupText, type SkippedStep, type StepPace, TRAILING_HOLD_MS, WALL_PROBE, WallError, type WallKind, type WallVerdict, askedMs, base64Bytes, buildEncodePage, capReached, cappedLine, clampWait, clockMotion, clockTyping, deadLine, deadTime, deadWarns, easeInOutCubic, exposureLine, formatDurationCap, headersFor, holdLeftMs, isIdpHost, isSignInPath, jpegDims, maskInitScript, paceLine, paceReport, parseHeaders, planFreshTake, pointerTravelMs, publicUrl, realSleep, recordTake, redirectedAway, resolveSetupText, runSetup, scrollByScript, scrollTravelMs, settleMs, settleVerdict, setupEnvNames, urlSchemeProblem, validateActions, validateSetup, wallVerdict };
|
|
1316
|
+
export { AWAIT_HEARTBEAT_MS, AWAIT_POLL_S, AWAIT_TIMEOUT_S, type ActionStep, type ActionsFile, type Arrival, type AwaitUntil, type ConditionState, DEAD_SHARE_WARN_PCT, DEAD_STRETCH_WARN_MS, DOM_CHANGE_PROBE, DOM_QUIET_MS, DOM_SETTLE_CAP_MS, DOWNLOAD_TIMEOUT_S, type DeadReport, type DeadStep, type DisabledClick, 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, MIN_SKIP_MS, MOTION_TICK_MS, type MaskReport, type MaskRule, OUTPUT_MAX_BYTES, type OutputSink, PAGE_ERROR_LINE_CHARS, POINTER_MAX_MS, POINTER_MIN_MS, POINTER_MS_PER_PX, PRESS_HOLD_MS, PRESS_KEYS, PRESS_LEAD_MS, type PaceReport, type PageError, PageErrorLog, type PollResult, type PressKey, QUIET_MS, READ_BEAT_MS, RESPONSE_MS, type RecordOpts, RecordRefusedError, type RecordResult, type RecorderApiResponse, type RecorderBrowser, 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, STEP_GUARDS, SetupEnvError, SetupError, type SetupResult, type SetupStep, type SetupText, type SkippedStep, type StepGuard, type StepPace, type StoppedTake, TRAILING_HOLD_MS, TakeClock, WALL_PROBE, type WaitCondition, WallError, type WallKind, type WallVerdict, askedMs, base64Bytes, buildEncodePage, capReached, cappedLine, clampWait, clockMotion, clockTyping, conditionMiss, deadLine, deadTime, deadWarns, domSettled, easeInOutCubic, exposureLine, extForType, fetchInPageScript, fileNameProblem, formatDurationCap, guardVerdict, guardedSteps, headersFor, holdLeftMs, hostedRefusals, isIdpHost, isSignInPath, jpegDims, maskInitScript, nameFromUrl, outputName, paceLine, paceReport, pageErrorLine, parseHeaders, plainName, planFreshTake, pointerTravelMs, pollCondition, publicUrl, readAllBytes, readCondition, readMediaSource, realSleep, recordTake, redirectedAway, resolveSetupText, runSetup, sayCondition, sayDuration, scrollByScript, scrollTravelMs, settleMs, settleVerdict, setupEnvNames, splitSegmentsAtSkips, typeOfName, urlSchemeProblem, validateActions, validateSetup, visibleTextScript, wallVerdict };
|