@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.js
CHANGED
|
@@ -29,6 +29,7 @@ var SETTLE_MS = {
|
|
|
29
29
|
click: 150,
|
|
30
30
|
type: 150,
|
|
31
31
|
press: 150,
|
|
32
|
+
paste: 150,
|
|
32
33
|
scroll: 200,
|
|
33
34
|
drag: 80
|
|
34
35
|
};
|
|
@@ -112,6 +113,8 @@ function askedMs(step) {
|
|
|
112
113
|
return (step.text?.length ?? 0) * (step.delayMs ?? 40);
|
|
113
114
|
case "press":
|
|
114
115
|
return step.ms ?? SETTLE_MS.press;
|
|
116
|
+
case "paste":
|
|
117
|
+
return step.ms ?? SETTLE_MS.paste;
|
|
115
118
|
default:
|
|
116
119
|
return 0;
|
|
117
120
|
}
|
|
@@ -372,6 +375,113 @@ var EXPOSURE_MATCHERS_SRC = `(() => {
|
|
|
372
375
|
}
|
|
373
376
|
return { kindsIn }
|
|
374
377
|
})()`;
|
|
378
|
+
var NOTICE_SRC = String.raw`const notices = () => {
|
|
379
|
+
// A label made only of these words is the control's, never a person's:
|
|
380
|
+
// "Account menu", "Open user menu", "Profile settings".
|
|
381
|
+
const UI_WORDS = new Set(['account', 'accounts', 'profile', 'profiles', 'menu', 'user', 'users', 'settings', 'setting', 'avatar', 'open', 'close', 'toggle', 'show', 'hide', 'the', 'my', 'your', 'me', 'you', 'home', 'guest', 'admin', 'team', 'workspace', 'dashboard', 'sign', 'signed', 'in', 'out', 'log', 'logged', 'logout', 'login', 'help', 'search', 'notification', 'notifications', 'more', 'options', 'option', 'button', 'dropdown', 'picture', 'image', 'photo', 'main', 'navigation', 'nav', 'preferences', 'switch', 'manage'])
|
|
382
|
+
const nameLike = (v) => {
|
|
383
|
+
if (!v) return null
|
|
384
|
+
const t = v.replace(/\s+/g, ' ').trim().replace(/[!.,:]+$/, '')
|
|
385
|
+
if (t.length < 2 || t.length > 40 || /[@\d<>/|]/.test(t)) return null
|
|
386
|
+
if (!/^\p{Lu}[\p{L}'’.-]*(?: \p{L}[\p{L}'’.-]*){0,3}$/u.test(t)) return null
|
|
387
|
+
if (t.toLowerCase().split(' ').every((w) => UI_WORDS.has(w))) return null
|
|
388
|
+
return t
|
|
389
|
+
}
|
|
390
|
+
const GREETING = /^(?:signed in as|logged in as|welcome back,?|welcome,|hi,|hello,|hey,)\s+(.+)$/i
|
|
391
|
+
const LABELS = [
|
|
392
|
+
/^(?:account|user|profile)(?: menu)?(?: for|:|,| -)\s*(.+)$/i,
|
|
393
|
+
/^open (?:the )?(?:user|account|profile) menu(?: for|,|:)?\s*(.+)$/i,
|
|
394
|
+
/^(.+?)[’']s? (?:account|profile|avatar)$/i,
|
|
395
|
+
]
|
|
396
|
+
const avatarish = (el) => el.tagName === 'IMG' || !!el.querySelector('img, svg') || /avatar/i.test(String(el.className || '') + ' ' + (el.getAttribute('data-testid') || ''))
|
|
397
|
+
let sure = false
|
|
398
|
+
const texts = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT)
|
|
399
|
+
for (let n; !acct && (n = texts.nextNode()); ) {
|
|
400
|
+
const el = n.parentElement
|
|
401
|
+
if (!el || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/.test(el.tagName)) continue
|
|
402
|
+
const line = (el.textContent || '').replace(/\s+/g, ' ').trim()
|
|
403
|
+
if (line.length > 80) continue
|
|
404
|
+
const m = GREETING.exec(line)
|
|
405
|
+
const who = m && nameLike(m[1])
|
|
406
|
+
if (who && seen(el)) { acct = who; sure = true }
|
|
407
|
+
}
|
|
408
|
+
let maybe = null
|
|
409
|
+
if (!acct) {
|
|
410
|
+
for (const el of document.querySelectorAll('[aria-label], img[alt], [title]')) {
|
|
411
|
+
const label = el.getAttribute('aria-label') || el.getAttribute('alt') || el.getAttribute('title') || ''
|
|
412
|
+
const box = el.closest('button, a, [role="button"], [aria-haspopup]') || el
|
|
413
|
+
if (!seen(el) && !seen(box)) continue
|
|
414
|
+
let hit = null
|
|
415
|
+
for (const rx of LABELS) { const m = rx.exec(label.trim()); if (m && (hit = nameLike(m[1]))) break }
|
|
416
|
+
if (hit) { acct = hit; sure = true; break }
|
|
417
|
+
if (!maybe && (avatarish(el) || avatarish(box)) && box !== el.ownerDocument.body) maybe = nameLike(label)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// The account menu's TRIGGER, whose own words are the name: a control
|
|
421
|
+
// that opens a menu and holds the avatar, or one labelled as the
|
|
422
|
+
// account's. The name-like line among its words (an address there is
|
|
423
|
+
// an exposure already, and never a name).
|
|
424
|
+
if (!acct) {
|
|
425
|
+
const TRIGGER_LABEL = /(account|user|profile|avatar)/i
|
|
426
|
+
for (const el of document.querySelectorAll('button, a, [role="button"], [aria-haspopup]')) {
|
|
427
|
+
const label = (el.getAttribute('aria-label') || '') + ' ' + (el.getAttribute('title') || '')
|
|
428
|
+
const trigger = (el.hasAttribute('aria-haspopup') && avatarish(el)) || TRIGGER_LABEL.test(label)
|
|
429
|
+
if (!trigger || !seen(el)) continue
|
|
430
|
+
const words = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
|
|
431
|
+
for (let n; !acct && (n = words.nextNode()); ) {
|
|
432
|
+
const who = nameLike(n.nodeValue)
|
|
433
|
+
if (who && n.parentElement && seen(n.parentElement)) { acct = who; sure = true }
|
|
434
|
+
}
|
|
435
|
+
if (acct) break
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (!acct && maybe) acct = maybe
|
|
439
|
+
const found = []
|
|
440
|
+
if (acct) {
|
|
441
|
+
const places = [], at = new Set()
|
|
442
|
+
const walk = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT)
|
|
443
|
+
for (let n; (n = walk.nextNode()) && places.length < 20; ) {
|
|
444
|
+
const el = n.parentElement
|
|
445
|
+
if (!el || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/.test(el.tagName)) continue
|
|
446
|
+
if (!(n.nodeValue || '').includes(acct)) continue
|
|
447
|
+
const r = seen(el)
|
|
448
|
+
if (!r) continue
|
|
449
|
+
const selector = name(el)
|
|
450
|
+
if (at.has(selector)) continue
|
|
451
|
+
at.add(selector)
|
|
452
|
+
places.push({ selector, rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } })
|
|
453
|
+
}
|
|
454
|
+
if (places.length) found.push({ kind: 'account-name', sure, places })
|
|
455
|
+
}
|
|
456
|
+
const RECENT = /^(recents?|recently (?:viewed|opened|visited|edited)|history|recent (?:files|projects|documents|items|pages|boards|chats|conversations|searches))$/i
|
|
457
|
+
const ITEMS = 'a[href], [role="link"], [role="menuitem"], [role="treeitem"], [role="option"]'
|
|
458
|
+
const listed = new Set()
|
|
459
|
+
for (const el of document.querySelectorAll('h1, h2, h3, h4, h5, h6, [role="heading"], summary, div, span, p, li, label, strong, button, a')) {
|
|
460
|
+
if (el.children.length > 2) continue
|
|
461
|
+
const word = (el.textContent || '').replace(/\s+/g, ' ').trim()
|
|
462
|
+
if (word.length > 40 || !RECENT.test(word)) continue
|
|
463
|
+
// A nav link that happens to say History is a page, not a list.
|
|
464
|
+
if (/^(A|BUTTON)$/.test(el.tagName) && !el.hasAttribute('aria-expanded')) continue
|
|
465
|
+
if (!seen(el)) continue
|
|
466
|
+
for (const c of [el.nextElementSibling, el.parentElement && el.parentElement.nextElementSibling, el.parentElement]) {
|
|
467
|
+
if (!c || c === document.body) continue
|
|
468
|
+
const items = [...c.querySelectorAll(ITEMS)].filter((x) => x !== el && !x.contains(el) && seen(x))
|
|
469
|
+
if (items.length < 3) continue
|
|
470
|
+
const r = seen(c)
|
|
471
|
+
if (!r) continue
|
|
472
|
+
const selector = name(c)
|
|
473
|
+
if (!listed.has(selector)) {
|
|
474
|
+
listed.add(selector)
|
|
475
|
+
found.push({ kind: 'personal-list', heading: word, selector, items: items.length, rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } })
|
|
476
|
+
}
|
|
477
|
+
break
|
|
478
|
+
}
|
|
479
|
+
if (listed.size >= 5) break
|
|
480
|
+
}
|
|
481
|
+
return found
|
|
482
|
+
}
|
|
483
|
+
// Read before any selector is made, so none of them quotes the name.
|
|
484
|
+
const noticeList = notices()`;
|
|
375
485
|
var EXPOSURE_PROBE = `(() => {
|
|
376
486
|
const { kindsIn } = ${EXPOSURE_MATCHERS_SRC}
|
|
377
487
|
const vw = innerWidth, vh = innerHeight
|
|
@@ -383,7 +493,10 @@ var EXPOSURE_PROBE = `(() => {
|
|
|
383
493
|
// layout's, and they come last. A value that itself holds what the scan
|
|
384
494
|
// looks for is never used: the selector would quote it.
|
|
385
495
|
const attr = (v) => '"' + v.replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\\\"') + '"'
|
|
386
|
-
|
|
496
|
+
// The signed-in account's own name, once the notices have read it: a
|
|
497
|
+
// selector never quotes it either.
|
|
498
|
+
let acct = null
|
|
499
|
+
const quiet = (v) => !!v && v.length <= 80 && !kindsIn(v).length && !(acct && v.toLowerCase().includes(acct.toLowerCase()))
|
|
387
500
|
const segment = (el) => {
|
|
388
501
|
const tag = el.tagName.toLowerCase()
|
|
389
502
|
if (el.id && /^[A-Za-z_][\\w-]*$/.test(el.id) && !/\\d{2,}/.test(el.id) && quiet(el.id)) return tag + '#' + el.id
|
|
@@ -425,6 +538,7 @@ var EXPOSURE_PROBE = `(() => {
|
|
|
425
538
|
if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity === 0) return null
|
|
426
539
|
return r
|
|
427
540
|
}
|
|
541
|
+
${NOTICE_SRC}
|
|
428
542
|
const out = [], keys = new Set()
|
|
429
543
|
const add = (el, text) => {
|
|
430
544
|
const kinds = kindsIn(text)
|
|
@@ -451,7 +565,7 @@ var EXPOSURE_PROBE = `(() => {
|
|
|
451
565
|
}
|
|
452
566
|
const hits = {}
|
|
453
567
|
for (const [sel, n] of Object.entries(window.__vosMaskHits || {})) hits[sel] = n
|
|
454
|
-
return { exposures: out, maskHits: hits }
|
|
568
|
+
return { exposures: out, maskHits: hits, notices: noticeList }
|
|
455
569
|
})()`;
|
|
456
570
|
function maskInitScript(masks) {
|
|
457
571
|
const rules = masks.map((m) => ({
|
|
@@ -536,6 +650,43 @@ function exposureLine(e) {
|
|
|
536
650
|
return `${KIND_WORDS[e.kind]} in ${e.selector} (${when}, ${e.rect.x},${e.rect.y} ${e.rect.w}\xD7${e.rect.h})`;
|
|
537
651
|
}
|
|
538
652
|
var 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.';
|
|
653
|
+
var NoticeLog = class {
|
|
654
|
+
byKey = /* @__PURE__ */ new Map();
|
|
655
|
+
add(step, notices) {
|
|
656
|
+
for (const n of notices ?? []) {
|
|
657
|
+
const key = n.kind === "account-name" ? n.kind : `${n.kind}|${n.selector ?? ""}`;
|
|
658
|
+
const had = this.byKey.get(key);
|
|
659
|
+
if (!had) {
|
|
660
|
+
this.byKey.set(key, { step, ...n, seen: 1 });
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
had.seen += 1;
|
|
664
|
+
if (n.kind !== "account-name") continue;
|
|
665
|
+
if (n.sure) had.sure = true;
|
|
666
|
+
const places = had.places ?? [];
|
|
667
|
+
for (const p of n.places ?? [])
|
|
668
|
+
if (places.length < 20 && !places.some((q) => q.selector === p.selector))
|
|
669
|
+
places.push(p);
|
|
670
|
+
had.places = places;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
notices() {
|
|
674
|
+
return [...this.byKey.values()];
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
function noticeLine(n) {
|
|
678
|
+
const when = n.step < 0 ? "as the page opened" : `from step #${n.step}`;
|
|
679
|
+
if (n.kind === "account-name") {
|
|
680
|
+
const places = n.places ?? [];
|
|
681
|
+
return `${n.sure ? "" : "what may be "}the account's name (${places.length} ${places.length === 1 ? "place" : "places"}, ${when}): ${places.map((p) => p.selector).join("; ")}`;
|
|
682
|
+
}
|
|
683
|
+
const mask = JSON.stringify({ selector: n.selector, as: "blur" });
|
|
684
|
+
return `a "${n.heading}" list of the account's own items (${n.items} items, ${when}) in ${n.selector}; to hide it, add to "mask": ${mask}`;
|
|
685
|
+
}
|
|
686
|
+
function noticeAdvice(notices) {
|
|
687
|
+
const name = notices.some((n) => n.kind === "account-name");
|
|
688
|
+
return `These are the signed-in account's own: fine for a demo account, worth a mask on a real one.${name ? ' The name reads better swapped than blurred: { "selector": "\u2026", "as": "text", "text": "Jane Doe" }.' : ""}`;
|
|
689
|
+
}
|
|
539
690
|
|
|
540
691
|
// src/record/setup.ts
|
|
541
692
|
var VERBS = /* @__PURE__ */ new Set(["wait", "click", "type", "press", "goto"]);
|
|
@@ -620,7 +771,10 @@ async function runSetup(page, steps, opts) {
|
|
|
620
771
|
lines.push(`setup #${i} wait ${step.ms}ms`);
|
|
621
772
|
break;
|
|
622
773
|
case "goto":
|
|
623
|
-
await
|
|
774
|
+
await (opts.goto ? opts.goto(step.url) : page.goto(step.url, {
|
|
775
|
+
waitUntil: "networkidle",
|
|
776
|
+
timeout: 45e3
|
|
777
|
+
})).catch(() => null);
|
|
624
778
|
lines.push(`setup #${i} goto ${step.url}`);
|
|
625
779
|
break;
|
|
626
780
|
case "press":
|
|
@@ -689,6 +843,291 @@ function parseHeaders(given) {
|
|
|
689
843
|
return out;
|
|
690
844
|
}
|
|
691
845
|
|
|
846
|
+
// src/record/stepBound.ts
|
|
847
|
+
var STEP_MARGIN_MS = 3e4;
|
|
848
|
+
var WAIT_MARGIN_MS = 5e3;
|
|
849
|
+
var IDLE_CALL_MS = 6e4;
|
|
850
|
+
function stepBoundMs(step, plannedMs, defaults) {
|
|
851
|
+
if (step.do === "expect")
|
|
852
|
+
return (step.timeout ?? defaults.expectS) * 1e3 + WAIT_MARGIN_MS;
|
|
853
|
+
if (step.do === "await")
|
|
854
|
+
return (step.timeout ?? defaults.awaitS) * 1e3 + STEP_MARGIN_MS;
|
|
855
|
+
if (step.do === "download") return defaults.downloadS * 1e3 + STEP_MARGIN_MS;
|
|
856
|
+
return plannedMs + STEP_MARGIN_MS;
|
|
857
|
+
}
|
|
858
|
+
var StepBound = class {
|
|
859
|
+
constructor(now = Date.now, idleMs = IDLE_CALL_MS) {
|
|
860
|
+
this.now = now;
|
|
861
|
+
this.idleMs = idleMs;
|
|
862
|
+
}
|
|
863
|
+
now;
|
|
864
|
+
idleMs;
|
|
865
|
+
deadline = null;
|
|
866
|
+
started = 0;
|
|
867
|
+
hung = false;
|
|
868
|
+
/** A step begins: calls into the page answer within `boundMs` from now. */
|
|
869
|
+
start(boundMs) {
|
|
870
|
+
this.started = this.now();
|
|
871
|
+
this.deadline = this.started + boundMs;
|
|
872
|
+
this.hung = false;
|
|
873
|
+
}
|
|
874
|
+
/** The step ends: whether it ran into its bound, and how long it really took. */
|
|
875
|
+
end() {
|
|
876
|
+
const out = {
|
|
877
|
+
hung: this.hung,
|
|
878
|
+
ms: this.now() - this.started,
|
|
879
|
+
boundMs: (this.deadline ?? this.started) - this.started
|
|
880
|
+
};
|
|
881
|
+
this.deadline = null;
|
|
882
|
+
this.hung = false;
|
|
883
|
+
return out;
|
|
884
|
+
}
|
|
885
|
+
/** The pending call, or `undefined` once the bound has passed. */
|
|
886
|
+
race(p) {
|
|
887
|
+
const left = this.deadline === null ? this.idleMs : this.deadline - this.now();
|
|
888
|
+
if (left <= 0) {
|
|
889
|
+
this.hung = true;
|
|
890
|
+
p.catch(() => {
|
|
891
|
+
});
|
|
892
|
+
return Promise.resolve(void 0);
|
|
893
|
+
}
|
|
894
|
+
return new Promise((resolve, reject) => {
|
|
895
|
+
const timer = setTimeout(() => {
|
|
896
|
+
this.hung = true;
|
|
897
|
+
resolve(void 0);
|
|
898
|
+
}, left);
|
|
899
|
+
p.then(
|
|
900
|
+
(v) => {
|
|
901
|
+
clearTimeout(timer);
|
|
902
|
+
resolve(v);
|
|
903
|
+
},
|
|
904
|
+
(e) => {
|
|
905
|
+
clearTimeout(timer);
|
|
906
|
+
reject(e);
|
|
907
|
+
}
|
|
908
|
+
);
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
function bounded(target, bound) {
|
|
913
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
914
|
+
const wrap = (obj) => {
|
|
915
|
+
const had = cache.get(obj);
|
|
916
|
+
if (had) return had;
|
|
917
|
+
const proxy = new Proxy(obj, {
|
|
918
|
+
get(t, prop) {
|
|
919
|
+
const value = Reflect.get(t, prop, t);
|
|
920
|
+
if (typeof value === "function")
|
|
921
|
+
return (...args) => {
|
|
922
|
+
const out = value.apply(
|
|
923
|
+
t,
|
|
924
|
+
args
|
|
925
|
+
);
|
|
926
|
+
if (out && typeof out.then === "function")
|
|
927
|
+
return bound.race(out);
|
|
928
|
+
if (out && typeof out === "object" && out !== t) return wrap(out);
|
|
929
|
+
return out;
|
|
930
|
+
};
|
|
931
|
+
if (value && typeof value === "object") return wrap(value);
|
|
932
|
+
return value;
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
cache.set(obj, proxy);
|
|
936
|
+
return proxy;
|
|
937
|
+
};
|
|
938
|
+
return wrap(target);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/record/pageSound.ts
|
|
942
|
+
var MAX_ELEMENTS = 50;
|
|
943
|
+
var MAX_NOTES = 2e3;
|
|
944
|
+
var MIN_HEARD_S = 0.05;
|
|
945
|
+
var MEDIA_WATCH_SCRIPT = `(() => {
|
|
946
|
+
let host
|
|
947
|
+
try { host = window.top; void host.document.title } catch { return }
|
|
948
|
+
if (window.__vosMediaWatch) return
|
|
949
|
+
window.__vosMediaWatch = true
|
|
950
|
+
if (!host.__vosMedia) {
|
|
951
|
+
const M = { log: [], seq: 0, els: new Set(), doc: Math.random().toString(36).slice(2) }
|
|
952
|
+
M.fields = (el, kind) => ({
|
|
953
|
+
id: el.__vosMediaId,
|
|
954
|
+
tag: el.tagName === 'VIDEO' ? 'video' : 'audio',
|
|
955
|
+
src: el.currentSrc || el.src || '',
|
|
956
|
+
kind,
|
|
957
|
+
wall: Date.now(),
|
|
958
|
+
t: el.currentTime,
|
|
959
|
+
rate: el.playbackRate || 1,
|
|
960
|
+
paused: el.paused || el.ended,
|
|
961
|
+
audible: !el.muted && el.volume > 0,
|
|
962
|
+
volume: el.volume,
|
|
963
|
+
duration: isFinite(el.duration) ? el.duration : null,
|
|
964
|
+
ab: typeof el.webkitAudioDecodedByteCount === 'number' ? el.webkitAudioDecodedByteCount : null,
|
|
965
|
+
})
|
|
966
|
+
host.__vosMedia = M
|
|
967
|
+
}
|
|
968
|
+
const M = host.__vosMedia
|
|
969
|
+
const EVENTS = ['playing', 'pause', 'ended', 'seeking', 'seeked', 'volumechange', 'ratechange', 'emptied']
|
|
970
|
+
const note = (e) => {
|
|
971
|
+
const el = e.target
|
|
972
|
+
if (!el || typeof el.currentTime !== 'number' || !/^(VIDEO|AUDIO)$/.test(el.tagName || '')) return
|
|
973
|
+
if (e.__vosNoted) return
|
|
974
|
+
e.__vosNoted = true
|
|
975
|
+
if (!el.__vosMediaId) {
|
|
976
|
+
if (M.els.size >= ${MAX_ELEMENTS}) return
|
|
977
|
+
el.__vosMediaId = ++M.seq
|
|
978
|
+
M.els.add(el)
|
|
979
|
+
}
|
|
980
|
+
if (M.log.length < ${MAX_NOTES}) M.log.push(M.fields(el, e.type))
|
|
981
|
+
}
|
|
982
|
+
for (const type of EVENTS) document.addEventListener(type, note, true)
|
|
983
|
+
const proto = window.HTMLMediaElement && window.HTMLMediaElement.prototype
|
|
984
|
+
if (proto && proto.play) {
|
|
985
|
+
const play = proto.play
|
|
986
|
+
proto.play = function () {
|
|
987
|
+
if (!this.__vosListening && !this.isConnected) {
|
|
988
|
+
this.__vosListening = true
|
|
989
|
+
for (const type of EVENTS) this.addEventListener(type, note)
|
|
990
|
+
}
|
|
991
|
+
return play.apply(this, arguments)
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
})()`;
|
|
995
|
+
var MEDIA_DRAIN_PROBE = `(() => {
|
|
996
|
+
const M = window.__vosMedia
|
|
997
|
+
if (!M) return { now: Date.now(), doc: null, notes: [] }
|
|
998
|
+
const notes = M.log.splice(0)
|
|
999
|
+
M.els.forEach((el) => notes.push(M.fields(el, 'tick')))
|
|
1000
|
+
return { now: Date.now(), doc: M.doc, notes }
|
|
1001
|
+
})()`;
|
|
1002
|
+
function soundSrcForMeta(src) {
|
|
1003
|
+
if (/^blob:/i.test(src)) return "blob:";
|
|
1004
|
+
if (/^data:/i.test(src)) return src.slice(0, src.search(/[;,]/) + 1 || 5);
|
|
1005
|
+
try {
|
|
1006
|
+
const u = new URL(src);
|
|
1007
|
+
return `${u.origin}${u.pathname}`;
|
|
1008
|
+
} catch {
|
|
1009
|
+
return "";
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
var PageSoundLog = class {
|
|
1013
|
+
byKey = /* @__PURE__ */ new Map();
|
|
1014
|
+
openKeyOf = /* @__PURE__ */ new Map();
|
|
1015
|
+
doc = null;
|
|
1016
|
+
lastWall = 0;
|
|
1017
|
+
add(drain, nodeNow, step) {
|
|
1018
|
+
if (!drain) return;
|
|
1019
|
+
const skew = nodeNow - drain.now;
|
|
1020
|
+
if (drain.doc !== this.doc) {
|
|
1021
|
+
if (this.doc !== null) this.closeAll(this.lastWall);
|
|
1022
|
+
this.openKeyOf.clear();
|
|
1023
|
+
this.doc = drain.doc;
|
|
1024
|
+
}
|
|
1025
|
+
const notes = [...drain.notes].sort((a, b) => a.wall - b.wall);
|
|
1026
|
+
for (const n of notes) this.note(n, n.wall + skew, step);
|
|
1027
|
+
this.lastWall = nodeNow;
|
|
1028
|
+
}
|
|
1029
|
+
note(n, wall, step) {
|
|
1030
|
+
const key = `${this.doc}|${n.id}|${n.src}`;
|
|
1031
|
+
const prevKey = this.openKeyOf.get(n.id);
|
|
1032
|
+
if (prevKey && prevKey !== key) {
|
|
1033
|
+
const prev = this.byKey.get(prevKey);
|
|
1034
|
+
if (prev) this.close(prev, wall);
|
|
1035
|
+
}
|
|
1036
|
+
let f = this.byKey.get(key);
|
|
1037
|
+
if (!f) {
|
|
1038
|
+
if (!n.src) return;
|
|
1039
|
+
f = {
|
|
1040
|
+
tag: n.tag,
|
|
1041
|
+
src: n.src,
|
|
1042
|
+
played: false,
|
|
1043
|
+
audioBytes: null,
|
|
1044
|
+
open: null,
|
|
1045
|
+
stretches: []
|
|
1046
|
+
};
|
|
1047
|
+
this.byKey.set(key, f);
|
|
1048
|
+
}
|
|
1049
|
+
this.openKeyOf.set(n.id, key);
|
|
1050
|
+
if (n.duration !== null) f.duration = n.duration;
|
|
1051
|
+
if (n.ab !== null) f.audioBytes = Math.max(f.audioBytes ?? 0, n.ab);
|
|
1052
|
+
if (!n.paused) f.played = true;
|
|
1053
|
+
const stops = ["pause", "ended", "seeking", "emptied"].includes(n.kind);
|
|
1054
|
+
const heard = !n.paused && n.audible && !stops;
|
|
1055
|
+
const resync = ["playing", "seeked", "ratechange"].includes(n.kind);
|
|
1056
|
+
if (f.open && (!heard || resync))
|
|
1057
|
+
this.close(
|
|
1058
|
+
f,
|
|
1059
|
+
wall,
|
|
1060
|
+
n.kind === "ended" ? n.duration ?? void 0 : void 0
|
|
1061
|
+
);
|
|
1062
|
+
if (!f.open && heard) {
|
|
1063
|
+
f.open = { wall, t: n.t, rate: n.rate > 0 ? n.rate : 1 };
|
|
1064
|
+
f.volume = n.volume;
|
|
1065
|
+
if (f.step === void 0 && step !== void 0 && step >= 0) f.step = step;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
close(f, wall, endT) {
|
|
1069
|
+
if (!f.open) return;
|
|
1070
|
+
const { wall: from, t, rate } = f.open;
|
|
1071
|
+
f.open = null;
|
|
1072
|
+
if (wall <= from) return;
|
|
1073
|
+
const to = endT ?? t + (wall - from) / 1e3 * rate;
|
|
1074
|
+
f.stretches.push({
|
|
1075
|
+
fromWall: from,
|
|
1076
|
+
toWall: from + (to - t) / rate * 1e3,
|
|
1077
|
+
t0: t,
|
|
1078
|
+
rate
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
closeAll(wall) {
|
|
1082
|
+
for (const f of this.byKey.values()) this.close(f, wall);
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Every element the page played, each heard stretch as footage seconds
|
|
1086
|
+
* `at` with media seconds `from`..`to`, none past `endMs` of footage.
|
|
1087
|
+
* A video whose decoder never decoded a byte of sound played none.
|
|
1088
|
+
*/
|
|
1089
|
+
heard(clock, endWall, endMs) {
|
|
1090
|
+
this.closeAll(endWall);
|
|
1091
|
+
const out = [];
|
|
1092
|
+
for (const f of this.byKey.values()) {
|
|
1093
|
+
if (!f.played) continue;
|
|
1094
|
+
const silentVideo = f.tag === "video" && f.audioBytes === 0;
|
|
1095
|
+
const intervals = [];
|
|
1096
|
+
if (!silentVideo)
|
|
1097
|
+
for (const s of f.stretches)
|
|
1098
|
+
for (const c of clock.onCamera(s.fromWall, s.toWall)) {
|
|
1099
|
+
const at = c.atMs / 1e3;
|
|
1100
|
+
let from = s.t0 + (c.wallMs - s.fromWall) / 1e3 * s.rate;
|
|
1101
|
+
let to = from + c.ms / 1e3 * s.rate;
|
|
1102
|
+
if (f.duration !== void 0) to = Math.min(to, f.duration);
|
|
1103
|
+
const room = (endMs - c.atMs) / 1e3;
|
|
1104
|
+
if (room <= 0) continue;
|
|
1105
|
+
to = Math.min(to, from + room * s.rate);
|
|
1106
|
+
from = Math.max(0, from);
|
|
1107
|
+
if (to - from < MIN_HEARD_S) continue;
|
|
1108
|
+
intervals.push({
|
|
1109
|
+
at: +at.toFixed(3),
|
|
1110
|
+
from: +from.toFixed(3),
|
|
1111
|
+
to: +to.toFixed(3)
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
out.push({
|
|
1115
|
+
src: f.src,
|
|
1116
|
+
meta: {
|
|
1117
|
+
...f.step !== void 0 ? { step: f.step } : {},
|
|
1118
|
+
src: soundSrcForMeta(f.src),
|
|
1119
|
+
tag: f.tag,
|
|
1120
|
+
...f.duration !== void 0 ? { duration: +f.duration.toFixed(3) } : {},
|
|
1121
|
+
...intervals.length && f.volume !== void 0 ? { volume: +f.volume.toFixed(3) } : {},
|
|
1122
|
+
intervals,
|
|
1123
|
+
...intervals.length ? {} : { muted: true }
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
return out;
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
|
|
692
1131
|
// src/record/takeClock.ts
|
|
693
1132
|
var TakeClock = class {
|
|
694
1133
|
constructor(t0) {
|
|
@@ -719,6 +1158,32 @@ var TakeClock = class {
|
|
|
719
1158
|
open.to = Math.max(open.from, wallMs);
|
|
720
1159
|
return open.to - open.from;
|
|
721
1160
|
}
|
|
1161
|
+
/**
|
|
1162
|
+
* The parts of a wall-clock stretch the camera saw: each sub-stretch
|
|
1163
|
+
* outside every off-camera wait, as its wall start (`wallMs`), its
|
|
1164
|
+
* footage start (`atMs`) and its length. A stretch an `await` cut in two
|
|
1165
|
+
* comes back as two; one wholly off camera as none.
|
|
1166
|
+
*/
|
|
1167
|
+
onCamera(fromWall, toWall) {
|
|
1168
|
+
const out = [];
|
|
1169
|
+
let cur = Math.max(fromWall, this.t0);
|
|
1170
|
+
for (const p of this.pauses) {
|
|
1171
|
+
const pTo = p.to ?? Infinity;
|
|
1172
|
+
if (pTo <= cur) continue;
|
|
1173
|
+
if (p.from >= toWall) break;
|
|
1174
|
+
if (p.from > cur)
|
|
1175
|
+
out.push({
|
|
1176
|
+
wallMs: cur,
|
|
1177
|
+
atMs: this.footageMs(cur),
|
|
1178
|
+
ms: p.from - cur
|
|
1179
|
+
});
|
|
1180
|
+
cur = Math.max(cur, pTo);
|
|
1181
|
+
if (cur >= toWall) return out;
|
|
1182
|
+
}
|
|
1183
|
+
if (toWall > cur)
|
|
1184
|
+
out.push({ wallMs: cur, atMs: this.footageMs(cur), ms: toWall - cur });
|
|
1185
|
+
return out;
|
|
1186
|
+
}
|
|
722
1187
|
/**
|
|
723
1188
|
* A captured frame's footage ms, or null when it is not later than the
|
|
724
1189
|
* last frame kept (the frame is dropped; see the module comment).
|
|
@@ -764,6 +1229,591 @@ var PageErrorLog = class {
|
|
|
764
1229
|
}
|
|
765
1230
|
};
|
|
766
1231
|
|
|
1232
|
+
// src/record/surfaceFit.ts
|
|
1233
|
+
async function surfaceSize(cdp, dpr) {
|
|
1234
|
+
const shot = await cdp.send("Page.captureScreenshot", {
|
|
1235
|
+
format: "jpeg",
|
|
1236
|
+
quality: 1
|
|
1237
|
+
});
|
|
1238
|
+
const dims = shot.data ? jpegDims(base64Bytes(shot.data)) : null;
|
|
1239
|
+
if (!dims) return null;
|
|
1240
|
+
return { width: Math.round(dims.w / dpr), height: Math.round(dims.h / dpr) };
|
|
1241
|
+
}
|
|
1242
|
+
async function fitSurfaceToViewport(cdp, width, height) {
|
|
1243
|
+
try {
|
|
1244
|
+
const ratio = await cdp.send("Runtime.evaluate", {
|
|
1245
|
+
expression: "devicePixelRatio",
|
|
1246
|
+
returnByValue: true
|
|
1247
|
+
});
|
|
1248
|
+
const value = ratio.result?.value;
|
|
1249
|
+
const dpr = typeof value === "number" && value > 0 ? value : 1;
|
|
1250
|
+
const before = await surfaceSize(cdp, dpr);
|
|
1251
|
+
if (!before) return null;
|
|
1252
|
+
let after = before;
|
|
1253
|
+
for (let tries = 0; tries < 2; tries++) {
|
|
1254
|
+
if (after.width === width && after.height === height) break;
|
|
1255
|
+
const win = await cdp.send("Browser.getWindowForTarget");
|
|
1256
|
+
const { width: ww, height: wh } = win.bounds;
|
|
1257
|
+
if (ww === void 0 || wh === void 0) break;
|
|
1258
|
+
await cdp.send("Browser.setWindowBounds", {
|
|
1259
|
+
windowId: win.windowId,
|
|
1260
|
+
bounds: {
|
|
1261
|
+
width: ww + (width - after.width),
|
|
1262
|
+
height: wh + (height - after.height)
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
after = await surfaceSize(cdp, dpr) ?? after;
|
|
1266
|
+
}
|
|
1267
|
+
return { before, after };
|
|
1268
|
+
} catch {
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/record/waits.ts
|
|
1274
|
+
function visibleTextScript(text) {
|
|
1275
|
+
return `(() => { const b = document.body; return !!b && (b.innerText || '').includes(${JSON.stringify(text)}) })()`;
|
|
1276
|
+
}
|
|
1277
|
+
async function readCondition(page, c) {
|
|
1278
|
+
try {
|
|
1279
|
+
if (c.text !== void 0)
|
|
1280
|
+
return { holds: await page.evaluate(visibleTextScript(c.text)) };
|
|
1281
|
+
if (c.gone !== void 0)
|
|
1282
|
+
return { holds: !await page.locator(c.gone).first().isVisible() };
|
|
1283
|
+
if (c.selector !== void 0) {
|
|
1284
|
+
const loc = page.locator(c.selector).first();
|
|
1285
|
+
const visible = await loc.isVisible();
|
|
1286
|
+
if (!visible) return { holds: false, visible };
|
|
1287
|
+
if (c.enabled === void 0) return { holds: true, visible };
|
|
1288
|
+
const enabled = await loc.isEnabled({ timeout: 1e3 });
|
|
1289
|
+
return { holds: enabled === c.enabled, visible };
|
|
1290
|
+
}
|
|
1291
|
+
} catch {
|
|
1292
|
+
}
|
|
1293
|
+
return { holds: false };
|
|
1294
|
+
}
|
|
1295
|
+
async function pollCondition(page, c, opts) {
|
|
1296
|
+
const now = opts.now ?? Date.now;
|
|
1297
|
+
const start = now();
|
|
1298
|
+
const reads = Math.max(1, opts.confirm?.reads ?? 1);
|
|
1299
|
+
for (let first = true; ; first = false) {
|
|
1300
|
+
if (!first && opts.between) await opts.between();
|
|
1301
|
+
let last = await readCondition(page, c);
|
|
1302
|
+
for (let n = 1; last.holds && n < reads; n++) {
|
|
1303
|
+
await opts.sleep(opts.confirm?.gapMs ?? 0);
|
|
1304
|
+
last = await readCondition(page, c);
|
|
1305
|
+
}
|
|
1306
|
+
const ms = now() - start;
|
|
1307
|
+
if (last.holds) return { ok: true, ms, last };
|
|
1308
|
+
const left = opts.timeoutMs - ms;
|
|
1309
|
+
if (left <= 0) return { ok: false, ms, last };
|
|
1310
|
+
await opts.sleep(Math.min(opts.everyMs, left));
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
var DOM_QUIET_MS = 800;
|
|
1314
|
+
var DOM_SETTLE_CAP_MS = 5e3;
|
|
1315
|
+
var DOM_CHANGE_PROBE = `(() => {
|
|
1316
|
+
if (!window.__vosDomChanges) {
|
|
1317
|
+
window.__vosDomChanges = { n: 0 };
|
|
1318
|
+
new MutationObserver((list) => { window.__vosDomChanges.n += list.length })
|
|
1319
|
+
.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
1320
|
+
}
|
|
1321
|
+
return window.__vosDomChanges.n;
|
|
1322
|
+
})()`;
|
|
1323
|
+
async function domSettled(page, opts) {
|
|
1324
|
+
const now = opts.now ?? Date.now;
|
|
1325
|
+
const quiet = opts.quietMs ?? DOM_QUIET_MS;
|
|
1326
|
+
const cap = opts.capMs ?? DOM_SETTLE_CAP_MS;
|
|
1327
|
+
const start = now();
|
|
1328
|
+
let seen = await page.evaluate(DOM_CHANGE_PROBE).catch(() => -1);
|
|
1329
|
+
let since = start;
|
|
1330
|
+
for (; ; ) {
|
|
1331
|
+
const t = now();
|
|
1332
|
+
if (t - since >= quiet) return { settled: true, ms: t - start };
|
|
1333
|
+
if (t - start >= cap) return { settled: false, ms: t - start };
|
|
1334
|
+
await opts.sleep(opts.tickMs ?? 100);
|
|
1335
|
+
const n = await page.evaluate(DOM_CHANGE_PROBE).catch(() => -1);
|
|
1336
|
+
if (n !== seen) {
|
|
1337
|
+
seen = n;
|
|
1338
|
+
since = now();
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
function sayDuration(ms) {
|
|
1343
|
+
const s = Math.round(ms / 100) / 10;
|
|
1344
|
+
if (s < 60) return `${s} s`;
|
|
1345
|
+
const m = Math.floor(s / 60);
|
|
1346
|
+
const rest = Math.round(s - m * 60);
|
|
1347
|
+
return rest ? `${m} min ${rest} s` : `${m} min`;
|
|
1348
|
+
}
|
|
1349
|
+
function sayCondition(c) {
|
|
1350
|
+
if (c.text !== void 0) return `text "${c.text}" to show`;
|
|
1351
|
+
if (c.gone !== void 0) return `'${c.gone}' to go`;
|
|
1352
|
+
if (c.enabled === true) return `'${c.selector}' to be enabled`;
|
|
1353
|
+
if (c.enabled === false) return `'${c.selector}' to be disabled`;
|
|
1354
|
+
return `'${c.selector}' to appear`;
|
|
1355
|
+
}
|
|
1356
|
+
function conditionMiss(c, last, ms) {
|
|
1357
|
+
const t = sayDuration(ms);
|
|
1358
|
+
if (c.text !== void 0)
|
|
1359
|
+
return `no visible text contained "${c.text}" for ${t}`;
|
|
1360
|
+
if (c.gone !== void 0) return `'${c.gone}' was still there after ${t}`;
|
|
1361
|
+
if (last.visible && c.enabled === true)
|
|
1362
|
+
return `'${c.selector}' stayed disabled for ${t}`;
|
|
1363
|
+
if (last.visible && c.enabled === false)
|
|
1364
|
+
return `'${c.selector}' stayed enabled for ${t}`;
|
|
1365
|
+
return `'${c.selector}' never appeared in ${t}`;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/record/ready.ts
|
|
1369
|
+
var READY_MODES = ["dom", "networkidle"];
|
|
1370
|
+
var DEFAULT_READY = "dom";
|
|
1371
|
+
var NAV_TIMEOUT_MS = 45e3;
|
|
1372
|
+
var READY_SETTLE_CAP_MS = 8e3;
|
|
1373
|
+
var NETWORKIDLE_HYDRATION_MS = 800;
|
|
1374
|
+
function readyProblem(v) {
|
|
1375
|
+
if (v === void 0) return null;
|
|
1376
|
+
return typeof v === "string" && READY_MODES.includes(v) ? null : `ready must be ${READY_MODES.map((m) => `"${m}"`).join(" or ")}`;
|
|
1377
|
+
}
|
|
1378
|
+
async function untilReady(page, mode, navigate, opts) {
|
|
1379
|
+
const now = opts.now ?? Date.now;
|
|
1380
|
+
const start = now();
|
|
1381
|
+
const timeout = opts.timeoutMs ?? NAV_TIMEOUT_MS;
|
|
1382
|
+
if (mode === "networkidle") {
|
|
1383
|
+
let idle = true;
|
|
1384
|
+
const response2 = await navigate({
|
|
1385
|
+
waitUntil: "networkidle",
|
|
1386
|
+
timeout
|
|
1387
|
+
}).catch(() => {
|
|
1388
|
+
idle = false;
|
|
1389
|
+
return null;
|
|
1390
|
+
});
|
|
1391
|
+
await opts.sleep(NETWORKIDLE_HYDRATION_MS);
|
|
1392
|
+
return {
|
|
1393
|
+
response: response2,
|
|
1394
|
+
ready: { mode, ms: Math.round(now() - start), capped: !idle }
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
const response = await navigate({ waitUntil: "load", timeout }).catch(
|
|
1398
|
+
() => null
|
|
1399
|
+
);
|
|
1400
|
+
const settled = await domSettled(page, {
|
|
1401
|
+
sleep: opts.sleep,
|
|
1402
|
+
now,
|
|
1403
|
+
quietMs: DOM_QUIET_MS,
|
|
1404
|
+
capMs: opts.capMs ?? READY_SETTLE_CAP_MS
|
|
1405
|
+
});
|
|
1406
|
+
return {
|
|
1407
|
+
response,
|
|
1408
|
+
ready: { mode, ms: Math.round(now() - start), capped: !settled.settled }
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
async function reloadUntilReady(page, mode, timeoutMs, opts = {}) {
|
|
1412
|
+
const nav = {
|
|
1413
|
+
waitUntil: mode === "networkidle" ? "networkidle" : "load",
|
|
1414
|
+
timeout: Math.min(NAV_TIMEOUT_MS, timeoutMs)
|
|
1415
|
+
};
|
|
1416
|
+
await (page.reload ? page.reload(nav) : page.goto(page.url(), nav)).catch(
|
|
1417
|
+
() => null
|
|
1418
|
+
);
|
|
1419
|
+
await domSettled(page, { sleep: opts.sleep ?? realSleep });
|
|
1420
|
+
}
|
|
1421
|
+
function readyLine(r) {
|
|
1422
|
+
const s = (r.ms / 1e3).toFixed(1);
|
|
1423
|
+
if (r.mode === "networkidle")
|
|
1424
|
+
return r.capped ? `(networkidle timeout after ${s} s, continuing)` : `(network idle after ${s} s)`;
|
|
1425
|
+
return r.capped ? `(the page was still changing after ${s} s, starting anyway)` : `(the page settled after ${s} s)`;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/record/gestures.ts
|
|
1429
|
+
var MODIFIERS = [
|
|
1430
|
+
"Shift",
|
|
1431
|
+
"Control",
|
|
1432
|
+
"Alt",
|
|
1433
|
+
"Meta",
|
|
1434
|
+
"Mod",
|
|
1435
|
+
"ControlOrMeta"
|
|
1436
|
+
];
|
|
1437
|
+
var DRAG_MODIFIERS = [
|
|
1438
|
+
"Alt",
|
|
1439
|
+
"Shift",
|
|
1440
|
+
"Meta",
|
|
1441
|
+
"Control",
|
|
1442
|
+
"Mod"
|
|
1443
|
+
];
|
|
1444
|
+
var NAMED_KEYS = /* @__PURE__ */ new Set([
|
|
1445
|
+
"Escape",
|
|
1446
|
+
"Enter",
|
|
1447
|
+
"Tab",
|
|
1448
|
+
"Backspace",
|
|
1449
|
+
"Delete",
|
|
1450
|
+
"Insert",
|
|
1451
|
+
"ArrowUp",
|
|
1452
|
+
"ArrowDown",
|
|
1453
|
+
"ArrowLeft",
|
|
1454
|
+
"ArrowRight",
|
|
1455
|
+
"Home",
|
|
1456
|
+
"End",
|
|
1457
|
+
"PageUp",
|
|
1458
|
+
"PageDown",
|
|
1459
|
+
"Space",
|
|
1460
|
+
"CapsLock",
|
|
1461
|
+
"ContextMenu",
|
|
1462
|
+
"Backquote",
|
|
1463
|
+
"Minus",
|
|
1464
|
+
"Equal",
|
|
1465
|
+
"BracketLeft",
|
|
1466
|
+
"BracketRight",
|
|
1467
|
+
"Backslash",
|
|
1468
|
+
"Semicolon",
|
|
1469
|
+
"Quote",
|
|
1470
|
+
"Comma",
|
|
1471
|
+
"Period",
|
|
1472
|
+
"Slash",
|
|
1473
|
+
"NumpadAdd",
|
|
1474
|
+
"NumpadSubtract",
|
|
1475
|
+
"NumpadMultiply",
|
|
1476
|
+
"NumpadDivide",
|
|
1477
|
+
"NumpadDecimal",
|
|
1478
|
+
"NumpadEnter"
|
|
1479
|
+
]);
|
|
1480
|
+
var isModifier = (s) => MODIFIERS.includes(s);
|
|
1481
|
+
function isKeyName(k) {
|
|
1482
|
+
if ([...k].length === 1) return k.trim().length === 1 || k === " ";
|
|
1483
|
+
if (NAMED_KEYS.has(k)) return true;
|
|
1484
|
+
return /^Key[A-Z]$/.test(k) || /^Digit[0-9]$/.test(k) || /^Numpad[0-9]$/.test(k) || /^F([1-9]|1[0-9]|2[0-4])$/.test(k);
|
|
1485
|
+
}
|
|
1486
|
+
function splitChord(chord) {
|
|
1487
|
+
if (chord === "+") return { mods: [], key: "+" };
|
|
1488
|
+
const plusKey = chord.endsWith("++");
|
|
1489
|
+
const parts = (plusKey ? chord.slice(0, -2) : chord).split("+");
|
|
1490
|
+
if (plusKey) return { mods: parts, key: "+" };
|
|
1491
|
+
const key = parts.pop() ?? "";
|
|
1492
|
+
return { mods: parts, key };
|
|
1493
|
+
}
|
|
1494
|
+
function chordProblem(chord) {
|
|
1495
|
+
if (typeof chord !== "string" || !chord)
|
|
1496
|
+
return 'must be a key name or a chord like "Shift+D"';
|
|
1497
|
+
const { mods, key } = splitChord(chord);
|
|
1498
|
+
for (const m of mods) {
|
|
1499
|
+
if (!isModifier(m))
|
|
1500
|
+
return `"${chord}": "${m}" is not a modifier (one of ${MODIFIERS.join(", ")})`;
|
|
1501
|
+
}
|
|
1502
|
+
if (new Set(mods).size !== mods.length)
|
|
1503
|
+
return `"${chord}" names a modifier twice`;
|
|
1504
|
+
if (!key) return `"${chord}" ends without a key`;
|
|
1505
|
+
if (isModifier(key)) {
|
|
1506
|
+
if (mods.length) return `"${chord}" ends on a modifier, not a key`;
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
if (!isKeyName(key))
|
|
1510
|
+
return `"${chord}": "${key}" is not a key (a single character, a named key like Escape or ArrowLeft, F1 to F24, or a code like KeyA or Digit1)`;
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
function resolveChord(chord) {
|
|
1514
|
+
const { mods, key } = splitChord(chord);
|
|
1515
|
+
const k = key === "Mod" ? "ControlOrMeta" : key;
|
|
1516
|
+
return [...mods.map((m) => m === "Mod" ? "ControlOrMeta" : m), k].join("+");
|
|
1517
|
+
}
|
|
1518
|
+
function pressSequence(step) {
|
|
1519
|
+
if (step.keys !== void 0)
|
|
1520
|
+
return typeof step.keys === "string" ? [step.keys] : step.keys;
|
|
1521
|
+
return step.key ? [step.key] : [];
|
|
1522
|
+
}
|
|
1523
|
+
var KEY_SEQUENCE_GAP_MS = 150;
|
|
1524
|
+
var KEY_ROUNDTRIP_MS = 50;
|
|
1525
|
+
var keySequencePlanMs = (keys) => Math.max(0, keys - 1) * KEY_SEQUENCE_GAP_MS + keys * KEY_ROUNDTRIP_MS;
|
|
1526
|
+
var DRAG_HOLD_MS = 400;
|
|
1527
|
+
var MAX_KEYS = 50;
|
|
1528
|
+
function resolveModifier(m) {
|
|
1529
|
+
return m === "Mod" ? "ControlOrMeta" : m;
|
|
1530
|
+
}
|
|
1531
|
+
function modifierBits(mods, platform) {
|
|
1532
|
+
let bits = 0;
|
|
1533
|
+
for (const m of mods) {
|
|
1534
|
+
const k = m === "Mod" ? platform === "mac" ? "Meta" : "Control" : m;
|
|
1535
|
+
bits |= k === "Alt" ? 1 : k === "Control" ? 2 : k === "Meta" ? 4 : 8;
|
|
1536
|
+
}
|
|
1537
|
+
return bits;
|
|
1538
|
+
}
|
|
1539
|
+
var CLICK_BUTTONS = ["left", "right", "middle"];
|
|
1540
|
+
var buttonNumber = (b) => b === "right" ? 2 : b === "middle" ? 1 : 0;
|
|
1541
|
+
var MULTI_CLICK_GAP_MS = 90;
|
|
1542
|
+
var MAX_PATH_POINTS = 2e4;
|
|
1543
|
+
var PATH_DEFAULT_MS = 700;
|
|
1544
|
+
var finite = (v) => typeof v === "number" && Number.isFinite(v);
|
|
1545
|
+
function readPoint(raw) {
|
|
1546
|
+
if (Array.isArray(raw)) {
|
|
1547
|
+
if (raw.length < 2 || raw.length > 4)
|
|
1548
|
+
return "must be [x, y], [x, y, tMs] or [x, y, tMs, pressure]";
|
|
1549
|
+
const [x, y, t, pressure] = raw;
|
|
1550
|
+
if (!finite(x) || !finite(y)) return "x and y must be numbers (CSS px)";
|
|
1551
|
+
if (t !== void 0 && !finite(t)) return "its time must be a number of ms";
|
|
1552
|
+
if (pressure !== void 0 && !finite(pressure))
|
|
1553
|
+
return "its pressure must be a number from 0 to 1";
|
|
1554
|
+
return {
|
|
1555
|
+
x,
|
|
1556
|
+
y,
|
|
1557
|
+
...t !== void 0 ? { t } : {},
|
|
1558
|
+
...pressure !== void 0 ? { pressure } : {}
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
if (typeof raw === "object" && raw !== null) {
|
|
1562
|
+
const o = raw;
|
|
1563
|
+
const extra = Object.keys(o).filter(
|
|
1564
|
+
(k) => !["x", "y", "t", "pressure"].includes(k)
|
|
1565
|
+
);
|
|
1566
|
+
if (extra.length)
|
|
1567
|
+
return `takes x, y, t and pressure, not ${extra.join(", ")}`;
|
|
1568
|
+
if (!finite(o.x) || !finite(o.y)) return "x and y must be numbers (CSS px)";
|
|
1569
|
+
if (o.t !== void 0 && !finite(o.t)) return "its t must be a number of ms";
|
|
1570
|
+
if (o.pressure !== void 0 && !finite(o.pressure))
|
|
1571
|
+
return "its pressure must be a number from 0 to 1";
|
|
1572
|
+
return {
|
|
1573
|
+
x: o.x,
|
|
1574
|
+
y: o.y,
|
|
1575
|
+
...o.t !== void 0 ? { t: o.t } : {},
|
|
1576
|
+
...o.pressure !== void 0 ? { pressure: o.pressure } : {}
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
return "must be [x, y] or { x, y }";
|
|
1580
|
+
}
|
|
1581
|
+
function readPath(raw) {
|
|
1582
|
+
const list = typeof raw === "object" && raw !== null && !Array.isArray(raw) && Array.isArray(raw.points) ? raw.points : raw;
|
|
1583
|
+
if (!Array.isArray(list))
|
|
1584
|
+
return { problems: ['must be an array of points, or { "points": [...] }'] };
|
|
1585
|
+
if (list.length < 2) return { problems: ["needs at least two points"] };
|
|
1586
|
+
if (list.length > MAX_PATH_POINTS)
|
|
1587
|
+
return { problems: [`holds more than ${MAX_PATH_POINTS} points`] };
|
|
1588
|
+
const problems = [];
|
|
1589
|
+
const read = [];
|
|
1590
|
+
list.forEach((p, i) => {
|
|
1591
|
+
const r = readPoint(p);
|
|
1592
|
+
if (typeof r === "string") problems.push(`point ${i} ${r}`);
|
|
1593
|
+
else {
|
|
1594
|
+
if (r.x < 0 || r.y < 0)
|
|
1595
|
+
problems.push(`point ${i} is off the viewport (x and y are \u2265 0)`);
|
|
1596
|
+
if (r.pressure !== void 0 && (r.pressure < 0 || r.pressure > 1))
|
|
1597
|
+
problems.push(`point ${i} pressure must be from 0 to 1`);
|
|
1598
|
+
read.push(r);
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
if (problems.length) return { problems: problems.slice(0, 5) };
|
|
1602
|
+
const timedCount = read.filter((p) => p.t !== void 0).length;
|
|
1603
|
+
if (timedCount !== 0 && timedCount !== read.length)
|
|
1604
|
+
return {
|
|
1605
|
+
problems: [
|
|
1606
|
+
"every point carries a time or none does: a path is replayed by its own timing, or evenly over ms"
|
|
1607
|
+
]
|
|
1608
|
+
};
|
|
1609
|
+
const timed = timedCount > 0;
|
|
1610
|
+
if (timed) {
|
|
1611
|
+
for (let i = 1; i < read.length; i++)
|
|
1612
|
+
if (read[i].t < read[i - 1].t)
|
|
1613
|
+
return {
|
|
1614
|
+
problems: [`point ${i}'s time runs backwards (times never decrease)`]
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
const t0 = timed ? read[0].t : 0;
|
|
1618
|
+
return {
|
|
1619
|
+
timed,
|
|
1620
|
+
points: read.map((p) => ({
|
|
1621
|
+
x: p.x,
|
|
1622
|
+
y: p.y,
|
|
1623
|
+
at: timed ? p.t - t0 : 0,
|
|
1624
|
+
...p.pressure !== void 0 ? { pressure: p.pressure } : {}
|
|
1625
|
+
}))
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
function schedulePath(path, ms) {
|
|
1629
|
+
if (path.timed) return path.points;
|
|
1630
|
+
const dur = ms ?? PATH_DEFAULT_MS;
|
|
1631
|
+
const n = path.points.length;
|
|
1632
|
+
return path.points.map((p, i) => ({
|
|
1633
|
+
...p,
|
|
1634
|
+
at: n > 1 ? Math.round(dur * i / (n - 1)) : 0
|
|
1635
|
+
}));
|
|
1636
|
+
}
|
|
1637
|
+
var pathDurationMs = (s) => s.length ? s[s.length - 1].at : 0;
|
|
1638
|
+
var OVERRUN_SHARE = 0.2;
|
|
1639
|
+
var OVERRUN_MIN_PLANNED_MS = 100;
|
|
1640
|
+
var OVERRUN_MIN_MS = 100;
|
|
1641
|
+
function overrunOf(plannedMs, tookMs) {
|
|
1642
|
+
if (!(plannedMs >= OVERRUN_MIN_PLANNED_MS)) return null;
|
|
1643
|
+
if (tookMs <= plannedMs * (1 + OVERRUN_SHARE)) return null;
|
|
1644
|
+
if (tookMs - plannedMs < OVERRUN_MIN_MS) return null;
|
|
1645
|
+
return Math.round(tookMs - plannedMs);
|
|
1646
|
+
}
|
|
1647
|
+
function overrunLine(overruns) {
|
|
1648
|
+
if (!overruns.length) return null;
|
|
1649
|
+
const planned = overruns.reduce((a, o) => a + o.plannedMs, 0);
|
|
1650
|
+
const took = overruns.reduce((a, o) => a + o.tookMs, 0);
|
|
1651
|
+
const ratio = planned > 0 ? took / planned : 0;
|
|
1652
|
+
const n = overruns.length;
|
|
1653
|
+
const which = overruns.map((o) => `#${o.step}${o.id ? ` (${o.id})` : ""} ${o.do}`).join(", ");
|
|
1654
|
+
return `${n} step${n === 1 ? "" : "s"} ran ${ratio.toFixed(1)}\xD7 slower than scripted (${which}): the page could not keep up; add settle or raise ms`;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/record/paste.ts
|
|
1658
|
+
var PASTE_READ_WINDOW_MS = 300;
|
|
1659
|
+
var PASTE_UNREAD_MS = 3e3;
|
|
1660
|
+
var PASTE_MAX_BYTES = 10 * 1024 * 1024;
|
|
1661
|
+
var PASTE_TYPES = {
|
|
1662
|
+
".png": "image/png",
|
|
1663
|
+
".jpg": "image/jpeg",
|
|
1664
|
+
".jpeg": "image/jpeg",
|
|
1665
|
+
".webp": "image/webp",
|
|
1666
|
+
".gif": "image/gif",
|
|
1667
|
+
".svg": "image/svg+xml",
|
|
1668
|
+
".txt": "text/plain",
|
|
1669
|
+
".md": "text/plain",
|
|
1670
|
+
".html": "text/html",
|
|
1671
|
+
".htm": "text/html",
|
|
1672
|
+
".json": "text/plain",
|
|
1673
|
+
".csv": "text/plain"
|
|
1674
|
+
};
|
|
1675
|
+
function pasteTypeOf(name) {
|
|
1676
|
+
const dot = name.lastIndexOf(".");
|
|
1677
|
+
if (dot < 0) return null;
|
|
1678
|
+
return PASTE_TYPES[name.slice(dot).toLowerCase()] ?? null;
|
|
1679
|
+
}
|
|
1680
|
+
var BUILD = (item) => `
|
|
1681
|
+
const item = ${JSON.stringify(item)};
|
|
1682
|
+
const bytes = item.b64 ? Uint8Array.from(atob(item.b64), (c) => c.charCodeAt(0)) : null;
|
|
1683
|
+
const asText = bytes ? new TextDecoder().decode(bytes) : null;
|
|
1684
|
+
async function blobs() {
|
|
1685
|
+
if (item.text !== undefined) return { 'text/plain': new Blob([item.text], { type: 'text/plain' }) };
|
|
1686
|
+
const type = item.type;
|
|
1687
|
+
if (type === 'text/plain') return { 'text/plain': new Blob([asText], { type }) };
|
|
1688
|
+
if (type === 'text/html') return { 'text/html': new Blob([asText], { type }), 'text/plain': new Blob([asText], { type: 'text/plain' }) };
|
|
1689
|
+
if (type === 'image/svg+xml') return { 'image/svg+xml': new Blob([bytes], { type }), 'text/plain': new Blob([asText], { type: 'text/plain' }) };
|
|
1690
|
+
if (type === 'image/png') return { 'image/png': new Blob([bytes], { type }) };
|
|
1691
|
+
const bmp = await createImageBitmap(new Blob([bytes], { type }));
|
|
1692
|
+
const c = document.createElement('canvas');
|
|
1693
|
+
c.width = bmp.width; c.height = bmp.height;
|
|
1694
|
+
c.getContext('2d').drawImage(bmp, 0, 0);
|
|
1695
|
+
const png = await new Promise((r) => c.toBlob(r, 'image/png'));
|
|
1696
|
+
return { 'image/png': png };
|
|
1697
|
+
}
|
|
1698
|
+
`;
|
|
1699
|
+
function stageClipboardScript(item) {
|
|
1700
|
+
return `(async () => {
|
|
1701
|
+
${BUILD(item)}
|
|
1702
|
+
try {
|
|
1703
|
+
if (!navigator.clipboard || typeof ClipboardItem === 'undefined') return 'refused: this page has no clipboard';
|
|
1704
|
+
const b = await blobs();
|
|
1705
|
+
try {
|
|
1706
|
+
await navigator.clipboard.write([new ClipboardItem(b)]);
|
|
1707
|
+
} catch (e) {
|
|
1708
|
+
if (!b['text/plain'] || Object.keys(b).length === 1) throw e;
|
|
1709
|
+
await navigator.clipboard.write([new ClipboardItem({ 'text/plain': b['text/plain'] })]);
|
|
1710
|
+
}
|
|
1711
|
+
return 'ok';
|
|
1712
|
+
} catch (e) {
|
|
1713
|
+
return 'refused: ' + (e && e.message ? e.message : String(e));
|
|
1714
|
+
}
|
|
1715
|
+
})()`;
|
|
1716
|
+
}
|
|
1717
|
+
function syntheticPasteScript(item, opts = {}) {
|
|
1718
|
+
return `(async () => {
|
|
1719
|
+
${BUILD(item)}
|
|
1720
|
+
try {
|
|
1721
|
+
// With original, an image file is handed over as its own bytes and type
|
|
1722
|
+
// (an SVG also as its markup), never redrawn.
|
|
1723
|
+
const b = ${opts.original ? `item.type === 'image/svg+xml' ? { 'image/svg+xml': new Blob([bytes], { type: item.type }), 'text/plain': new Blob([asText], { type: 'text/plain' }) } : { [item.type]: new Blob([bytes], { type: item.type }) }` : "await blobs()"};
|
|
1724
|
+
const dt = new DataTransfer();
|
|
1725
|
+
let file = null;
|
|
1726
|
+
for (const [type, blob] of Object.entries(b)) {
|
|
1727
|
+
if (type.startsWith('image/')) {
|
|
1728
|
+
file = new File([blob], item.name || 'image.png', { type: blob.type || type });
|
|
1729
|
+
dt.items.add(file);
|
|
1730
|
+
} else dt.setData(type, await blob.text());
|
|
1731
|
+
}
|
|
1732
|
+
try {
|
|
1733
|
+
// The async reads answer the item for THIS paste only: shortly after
|
|
1734
|
+
// the first read (an app may read text and items in turn), or after a
|
|
1735
|
+
// few seconds when nothing reads, the browser's own reads come back,
|
|
1736
|
+
// so a later paste through the clipboard is the clipboard's.
|
|
1737
|
+
const staged = [new ClipboardItem(b)];
|
|
1738
|
+
const clip = navigator.clipboard;
|
|
1739
|
+
const own = { read: Object.prototype.hasOwnProperty.call(clip, 'read'), readText: Object.prototype.hasOwnProperty.call(clip, 'readText') };
|
|
1740
|
+
const was = { read: clip.read, readText: clip.readText };
|
|
1741
|
+
let restored = false;
|
|
1742
|
+
const restore = () => {
|
|
1743
|
+
if (restored) return;
|
|
1744
|
+
restored = true;
|
|
1745
|
+
for (const k of ['read', 'readText']) {
|
|
1746
|
+
if (own[k]) clip[k] = was[k];
|
|
1747
|
+
else delete clip[k];
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
let soon = null;
|
|
1751
|
+
const readOnce = () => { if (!soon) soon = setTimeout(restore, ${PASTE_READ_WINDOW_MS}) };
|
|
1752
|
+
clip.read = async () => { readOnce(); return staged };
|
|
1753
|
+
clip.readText = async () => { readOnce(); return b['text/plain'] ? b['text/plain'].text() : '' };
|
|
1754
|
+
setTimeout(restore, ${PASTE_UNREAD_MS});
|
|
1755
|
+
} catch {}
|
|
1756
|
+
const target = document.activeElement || document.body;
|
|
1757
|
+
const ev = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true, composed: true });
|
|
1758
|
+
const taken = !target.dispatchEvent(ev);
|
|
1759
|
+
const words = dt.getData('text/plain');
|
|
1760
|
+
if (!taken && words && (target.isContentEditable || /^(INPUT|TEXTAREA)$/.test(target.tagName)))
|
|
1761
|
+
document.execCommand('insertText', false, words);
|
|
1762
|
+
return 'ok';
|
|
1763
|
+
} catch (e) {
|
|
1764
|
+
return 'failed: ' + (e && e.message ? e.message : String(e));
|
|
1765
|
+
}
|
|
1766
|
+
})()`;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/record/settle.ts
|
|
1770
|
+
var APP_QUIET_MS = 300;
|
|
1771
|
+
var APP_SETTLE_CAP_MS = 4e3;
|
|
1772
|
+
var SETTLE_VERBS = /* @__PURE__ */ new Set([
|
|
1773
|
+
"hover",
|
|
1774
|
+
"click",
|
|
1775
|
+
"type",
|
|
1776
|
+
"press",
|
|
1777
|
+
"scroll",
|
|
1778
|
+
"move",
|
|
1779
|
+
"drag",
|
|
1780
|
+
"paste"
|
|
1781
|
+
]);
|
|
1782
|
+
var CHANGE_AGE_PROBE = `(() => {
|
|
1783
|
+
const w = window;
|
|
1784
|
+
if (!w.__vosChangeAt) {
|
|
1785
|
+
w.__vosChangeAt = { at: Date.now() };
|
|
1786
|
+
new MutationObserver(() => { w.__vosChangeAt.at = Date.now() })
|
|
1787
|
+
.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
1788
|
+
return -1;
|
|
1789
|
+
}
|
|
1790
|
+
return Date.now() - w.__vosChangeAt.at;
|
|
1791
|
+
})()`;
|
|
1792
|
+
function settleOn(step, scriptDefault) {
|
|
1793
|
+
if (!SETTLE_VERBS.has(step.do)) return false;
|
|
1794
|
+
return step.settle ?? scriptDefault ?? false;
|
|
1795
|
+
}
|
|
1796
|
+
async function appSettled(page, frames, opts) {
|
|
1797
|
+
const quiet = opts.quietMs ?? APP_QUIET_MS;
|
|
1798
|
+
const age = await page.evaluate(CHANGE_AGE_PROBE).catch(() => -1);
|
|
1799
|
+
const frameAge = opts.sinceFrame?.() ?? null;
|
|
1800
|
+
if (typeof age === "number" && age >= quiet && (frameAge === null || frameAge >= quiet))
|
|
1801
|
+
return { settled: true, ms: 0 };
|
|
1802
|
+
const counted = {
|
|
1803
|
+
evaluate: async (script) => {
|
|
1804
|
+
const n = await page.evaluate(script).catch(() => 0);
|
|
1805
|
+
return (typeof n === "number" && n > 0 ? n : 0) + frames();
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
return domSettled(counted, {
|
|
1809
|
+
sleep: opts.sleep,
|
|
1810
|
+
...opts.now ? { now: opts.now } : {},
|
|
1811
|
+
quietMs: quiet,
|
|
1812
|
+
capMs: opts.capMs ?? APP_SETTLE_CAP_MS,
|
|
1813
|
+
tickMs: 50
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
|
|
767
1817
|
// src/record/actions.ts
|
|
768
1818
|
var STEP_GUARDS = ["spend"];
|
|
769
1819
|
var GUARDABLE = /* @__PURE__ */ new Set(["click", "press", "type"]);
|
|
@@ -782,7 +1832,8 @@ var VERBS2 = /* @__PURE__ */ new Set([
|
|
|
782
1832
|
"drag",
|
|
783
1833
|
"expect",
|
|
784
1834
|
"await",
|
|
785
|
-
"download"
|
|
1835
|
+
"download",
|
|
1836
|
+
"paste"
|
|
786
1837
|
]);
|
|
787
1838
|
var PRESS_KEYS = [
|
|
788
1839
|
"Escape",
|
|
@@ -824,6 +1875,10 @@ function validateActions(value) {
|
|
|
824
1875
|
}
|
|
825
1876
|
}
|
|
826
1877
|
if (obj.setup !== void 0) errors.push(...validateSetup(obj.setup));
|
|
1878
|
+
const ready = readyProblem(obj.ready);
|
|
1879
|
+
if (ready) errors.push(ready);
|
|
1880
|
+
if (obj.settle !== void 0 && typeof obj.settle !== "boolean")
|
|
1881
|
+
errors.push("settle must be true or false");
|
|
827
1882
|
if (obj.mask !== void 0) {
|
|
828
1883
|
if (!Array.isArray(obj.mask)) {
|
|
829
1884
|
errors.push("mask must be an array of { selector, as?, text? }");
|
|
@@ -871,9 +1926,24 @@ function validateActions(value) {
|
|
|
871
1926
|
seenIds.add(s.id);
|
|
872
1927
|
}
|
|
873
1928
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1929
|
+
if (s.do === "type" && typeof s.selector !== "string")
|
|
1930
|
+
errors.push(`${at}: type needs a selector`);
|
|
1931
|
+
if (s.do === "hover" || s.do === "click") {
|
|
1932
|
+
const point = s.x !== void 0 || s.y !== void 0;
|
|
1933
|
+
if (point && s.selector !== void 0)
|
|
1934
|
+
errors.push(
|
|
1935
|
+
`${at}: ${s.do} takes a selector or x and y (a viewport point), not both`
|
|
1936
|
+
);
|
|
1937
|
+
else if (point) {
|
|
1938
|
+
if (!isCoord(s.x) || !isCoord(s.y))
|
|
1939
|
+
errors.push(
|
|
1940
|
+
`${at}: ${s.do} x and y must both be numbers of CSS px, 0 or more`
|
|
1941
|
+
);
|
|
1942
|
+
} else if (typeof s.selector !== "string")
|
|
1943
|
+
errors.push(
|
|
1944
|
+
`${at}: ${s.do} needs a selector, or x and y (a viewport point)`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
877
1947
|
if (s.do === "wait" && typeof s.ms !== "number")
|
|
878
1948
|
errors.push(`${at}: wait needs ms`);
|
|
879
1949
|
if (s.ms !== void 0 && (typeof s.ms !== "number" || !Number.isFinite(s.ms) || s.ms < 0))
|
|
@@ -887,13 +1957,64 @@ function validateActions(value) {
|
|
|
887
1957
|
if (s.do === "type" && s.at !== void 0 && s.at !== "end" && s.at !== "start")
|
|
888
1958
|
errors.push(`${at}: type at must be "end" or "start"`);
|
|
889
1959
|
if (s.do === "press") {
|
|
890
|
-
if (
|
|
1960
|
+
if (s.key !== void 0 && s.keys !== void 0)
|
|
1961
|
+
errors.push(
|
|
1962
|
+
`${at}: press takes key or keys, not both (keys says everything key does)`
|
|
1963
|
+
);
|
|
1964
|
+
else if (s.keys !== void 0) {
|
|
1965
|
+
const list = Array.isArray(s.keys) ? s.keys : [s.keys];
|
|
1966
|
+
if (!list.length)
|
|
1967
|
+
errors.push(`${at}: press keys must name at least one key`);
|
|
1968
|
+
else if (list.length > MAX_KEYS)
|
|
1969
|
+
errors.push(`${at}: press keys names more than ${MAX_KEYS} keys`);
|
|
1970
|
+
list.forEach((k, j) => {
|
|
1971
|
+
const problem = chordProblem(k);
|
|
1972
|
+
if (problem)
|
|
1973
|
+
errors.push(
|
|
1974
|
+
`${at}: press keys${Array.isArray(s.keys) ? `[${j}]` : ""} ${problem}`
|
|
1975
|
+
);
|
|
1976
|
+
});
|
|
1977
|
+
} else if (typeof s.key !== "string" || !PRESS_KEYS.includes(s.key))
|
|
891
1978
|
errors.push(
|
|
892
|
-
`${at}: press needs a key, one of ${PRESS_KEYS.join(", ")}; words are typed with type`
|
|
1979
|
+
`${at}: press needs a key, one of ${PRESS_KEYS.join(", ")}, or keys (a chord like "Shift+D", a plain key like "r", or a sequence); words are typed with type`
|
|
893
1980
|
);
|
|
894
1981
|
if (s.selector !== void 0 && typeof s.selector !== "string")
|
|
895
1982
|
errors.push(`${at}: press selector must be a string`);
|
|
896
1983
|
}
|
|
1984
|
+
if (s.do === "click") {
|
|
1985
|
+
errors.push(...modifiersProblem(at, "click", s.modifiers));
|
|
1986
|
+
if (s.count !== void 0 && s.count !== 1 && s.count !== 2 && s.count !== 3)
|
|
1987
|
+
errors.push(
|
|
1988
|
+
`${at}: click count must be 1, 2 (a double click) or 3 (a triple click)`
|
|
1989
|
+
);
|
|
1990
|
+
if (s.button !== void 0 && !CLICK_BUTTONS.includes(s.button))
|
|
1991
|
+
errors.push(`${at}: click button must be "left", "right" or "middle"`);
|
|
1992
|
+
}
|
|
1993
|
+
if (s.settle !== void 0) {
|
|
1994
|
+
if (typeof s.settle !== "boolean")
|
|
1995
|
+
errors.push(`${at}: settle must be true or false`);
|
|
1996
|
+
else if (!SETTLE_VERBS.has(s.do))
|
|
1997
|
+
errors.push(
|
|
1998
|
+
`${at}: settle follows a gesture (hover, click, type, press, scroll, move, drag, paste); a ${s.do} sends the page nothing to catch up on`
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
if (s.do === "paste") {
|
|
2002
|
+
const given = ["text", "file"].filter(
|
|
2003
|
+
(k) => s[k] !== void 0
|
|
2004
|
+
);
|
|
2005
|
+
if (given.length !== 1)
|
|
2006
|
+
errors.push(
|
|
2007
|
+
`${at}: paste needs exactly one of text (words) or file (an image, an SVG or a text file beside the actions file)`
|
|
2008
|
+
);
|
|
2009
|
+
if (s.text !== void 0 && typeof s.text !== "string")
|
|
2010
|
+
errors.push(`${at}: paste text must be a string`);
|
|
2011
|
+
if (s.file !== void 0) {
|
|
2012
|
+
const problem = typeof s.file === "string" ? fileNameProblem(s.file) ?? (pasteTypeOf(s.file) ? null : `"${s.file}" is not a type a paste takes (.png, .jpg, .webp, .gif, .svg, .txt, .md, .html, .json, .csv)`) : "is not a string";
|
|
2013
|
+
if (problem) errors.push(`${at}: paste file ${problem}`);
|
|
2014
|
+
}
|
|
2015
|
+
if (s.selector !== void 0 && !isFilled(s.selector))
|
|
2016
|
+
errors.push(`${at}: paste selector must be a non-empty string`);
|
|
2017
|
+
}
|
|
897
2018
|
if (s.do === "scroll" && typeof s.dy !== "number")
|
|
898
2019
|
errors.push(`${at}: scroll needs dy`);
|
|
899
2020
|
if (s.do === "move" && (typeof s.x !== "number" || typeof s.y !== "number")) {
|
|
@@ -985,18 +2106,51 @@ function validateActions(value) {
|
|
|
985
2106
|
}
|
|
986
2107
|
}
|
|
987
2108
|
if (s.do === "drag") {
|
|
988
|
-
if (
|
|
989
|
-
|
|
2109
|
+
if (s.path !== void 0) {
|
|
2110
|
+
const clash = ["selector", "x", "y", "tx", "ty"].filter(
|
|
2111
|
+
(k) => s[k] !== void 0
|
|
2112
|
+
);
|
|
2113
|
+
if (clash.length)
|
|
2114
|
+
errors.push(
|
|
2115
|
+
`${at}: a drag with a path starts at its first point and ends at its last, so it takes no ${clash.join(", ")}`
|
|
2116
|
+
);
|
|
2117
|
+
if (typeof s.path === "string") {
|
|
2118
|
+
const problem = fileNameProblem(s.path);
|
|
2119
|
+
if (problem)
|
|
2120
|
+
errors.push(
|
|
2121
|
+
`${at}: drag path ${problem} (a file beside the actions file)`
|
|
2122
|
+
);
|
|
2123
|
+
} else {
|
|
2124
|
+
const read = readPath(s.path);
|
|
2125
|
+
if ("problems" in read)
|
|
2126
|
+
for (const p of read.problems) errors.push(`${at}: drag path ${p}`);
|
|
2127
|
+
}
|
|
2128
|
+
} else {
|
|
2129
|
+
if (typeof s.tx !== "number" || typeof s.ty !== "number") {
|
|
2130
|
+
errors.push(`${at}: drag needs tx and ty (CSS px), or a path`);
|
|
2131
|
+
}
|
|
2132
|
+
const hasStart = typeof s.selector === "string" || typeof s.x === "number" && typeof s.y === "number";
|
|
2133
|
+
if (!hasStart)
|
|
2134
|
+
errors.push(
|
|
2135
|
+
`${at}: drag needs a selector OR x and y as the start point`
|
|
2136
|
+
);
|
|
990
2137
|
}
|
|
991
|
-
|
|
992
|
-
if (!hasStart)
|
|
993
|
-
errors.push(
|
|
994
|
-
`${at}: drag needs a selector OR x and y as the start point`
|
|
995
|
-
);
|
|
2138
|
+
errors.push(...modifiersProblem(at, "drag", s.modifiers));
|
|
996
2139
|
}
|
|
997
2140
|
});
|
|
998
2141
|
return errors;
|
|
999
2142
|
}
|
|
2143
|
+
var isCoord = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
2144
|
+
function modifiersProblem(at, verb, v) {
|
|
2145
|
+
if (v === void 0) return [];
|
|
2146
|
+
if (!Array.isArray(v) || v.some((m) => !DRAG_MODIFIERS.includes(m)))
|
|
2147
|
+
return [
|
|
2148
|
+
`${at}: ${verb} modifiers must be a list of ${DRAG_MODIFIERS.map((m) => `"${m}"`).join(", ")} (Mod is Meta on macOS, Control elsewhere)`
|
|
2149
|
+
];
|
|
2150
|
+
if (new Set(v).size !== v.length)
|
|
2151
|
+
return [`${at}: ${verb} modifiers names a modifier twice`];
|
|
2152
|
+
return [];
|
|
2153
|
+
}
|
|
1000
2154
|
var isFilled = (v) => typeof v === "string" && v.trim().length > 0;
|
|
1001
2155
|
function secondsProblem(at, what, v) {
|
|
1002
2156
|
if (v === void 0) return [];
|
|
@@ -1058,103 +2212,137 @@ function hostedRefusals(actions) {
|
|
|
1058
2212
|
out.push(
|
|
1059
2213
|
`steps[${i}] is a download, which a hosted take does not keep: record it on this machine, where the file lands in the take's outputs/`
|
|
1060
2214
|
);
|
|
2215
|
+
if (s.do === "paste")
|
|
2216
|
+
out.push(
|
|
2217
|
+
`steps[${i}] is a paste, and a hosted browser has no clipboard to paste from: record it on this machine`
|
|
2218
|
+
);
|
|
2219
|
+
if (s.do === "drag" && typeof s.path === "string")
|
|
2220
|
+
out.push(
|
|
2221
|
+
`steps[${i}] draws a path from the file "${s.path}", and a hosted take carries the script alone: write the points inline as path`
|
|
2222
|
+
);
|
|
1061
2223
|
});
|
|
1062
2224
|
return out;
|
|
1063
2225
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
2226
|
+
function scriptFiles(actions) {
|
|
2227
|
+
const out = [];
|
|
2228
|
+
actions.steps.forEach((s, i) => {
|
|
2229
|
+
if (s.do === "drag" && typeof s.path === "string")
|
|
2230
|
+
out.push({ step: i, name: s.path, as: "path" });
|
|
2231
|
+
if (s.do === "paste" && typeof s.file === "string")
|
|
2232
|
+
out.push({ step: i, name: s.file, as: "paste" });
|
|
2233
|
+
});
|
|
2234
|
+
return out;
|
|
1068
2235
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
const enabled = await loc.isEnabled({ timeout: 1e3 });
|
|
1081
|
-
return { holds: enabled === c.enabled, visible };
|
|
1082
|
-
}
|
|
1083
|
-
} catch {
|
|
2236
|
+
|
|
2237
|
+
// src/record/scriptFiles.ts
|
|
2238
|
+
async function loadScriptFiles(actions, source) {
|
|
2239
|
+
const loaded = { paths: /* @__PURE__ */ new Map(), pastes: /* @__PURE__ */ new Map() };
|
|
2240
|
+
const named = scriptFiles(actions);
|
|
2241
|
+
if (!named.length) return loaded;
|
|
2242
|
+
if (!source) {
|
|
2243
|
+
const f = named[0];
|
|
2244
|
+
return {
|
|
2245
|
+
problem: `steps[${f.step}] names the file "${f.name}", and this recorder has no actions directory to read it from${f.as === "path" ? ": write the points inline as path" : ""}. Nothing was recorded.`
|
|
2246
|
+
};
|
|
1084
2247
|
}
|
|
1085
|
-
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
2248
|
+
for (const f of named) {
|
|
2249
|
+
const at = `steps[${f.step}]`;
|
|
2250
|
+
const bad = fileNameProblem(f.name);
|
|
2251
|
+
if (bad) return { problem: `${at}: ${bad}. Nothing was recorded.` };
|
|
2252
|
+
let bytes;
|
|
2253
|
+
try {
|
|
2254
|
+
bytes = await source.read(f.name);
|
|
2255
|
+
} catch (e) {
|
|
2256
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
2257
|
+
return {
|
|
2258
|
+
problem: f.as === "paste" ? `${at} pastes the file "${f.name}", which could not be read beside the actions file (${why}): a file paste needs its file beside the script, so put ${f.name} next to it (a take fetched from vos.so carries its paths and its words, never a pasted file). Nothing was recorded.` : `${at} names "${f.name}", which could not be read beside the actions file (${why}). Nothing was recorded.`
|
|
2259
|
+
};
|
|
1097
2260
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
const t = now();
|
|
1124
|
-
if (t - since >= quiet) return { settled: true, ms: t - start };
|
|
1125
|
-
if (t - start >= cap) return { settled: false, ms: t - start };
|
|
1126
|
-
await opts.sleep(opts.tickMs ?? 100);
|
|
1127
|
-
const n = await page.evaluate(DOM_CHANGE_PROBE).catch(() => -1);
|
|
1128
|
-
if (n !== seen) {
|
|
1129
|
-
seen = n;
|
|
1130
|
-
since = now();
|
|
2261
|
+
if (f.as === "path") {
|
|
2262
|
+
let raw;
|
|
2263
|
+
try {
|
|
2264
|
+
raw = JSON.parse(new TextDecoder().decode(bytes));
|
|
2265
|
+
} catch {
|
|
2266
|
+
return {
|
|
2267
|
+
problem: `${at}: the path file "${f.name}" is not JSON (an array of [x, y] or [x, y, tMs] points). Nothing was recorded.`
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
const path = readPath(raw);
|
|
2271
|
+
if ("problems" in path)
|
|
2272
|
+
return {
|
|
2273
|
+
problem: `${at}: the path file "${f.name}" ${path.problems.join("; ")}. Nothing was recorded.`
|
|
2274
|
+
};
|
|
2275
|
+
loaded.paths.set(f.step, path);
|
|
2276
|
+
} else {
|
|
2277
|
+
if (bytes.length > PASTE_MAX_BYTES)
|
|
2278
|
+
return {
|
|
2279
|
+
problem: `${at}: "${f.name}" is larger than ${PASTE_MAX_BYTES / 1024 / 1024} MB, more than a paste carries. Nothing was recorded.`
|
|
2280
|
+
};
|
|
2281
|
+
loaded.pastes.set(f.step, {
|
|
2282
|
+
bytes,
|
|
2283
|
+
type: pasteTypeOf(f.name) ?? "application/octet-stream",
|
|
2284
|
+
name: f.name
|
|
2285
|
+
});
|
|
1131
2286
|
}
|
|
1132
2287
|
}
|
|
2288
|
+
return loaded;
|
|
1133
2289
|
}
|
|
1134
|
-
function
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
2290
|
+
function inlinePoints(path) {
|
|
2291
|
+
return path.points.map((p) => {
|
|
2292
|
+
if (path.timed)
|
|
2293
|
+
return p.pressure !== void 0 ? [p.x, p.y, p.at, p.pressure] : [p.x, p.y, p.at];
|
|
2294
|
+
return p.pressure !== void 0 ? { x: p.x, y: p.y, pressure: p.pressure } : [p.x, p.y];
|
|
2295
|
+
});
|
|
1140
2296
|
}
|
|
1141
|
-
function
|
|
1142
|
-
if (
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
2297
|
+
function inlineScript(actions, files) {
|
|
2298
|
+
if (!files.paths.size) return actions;
|
|
2299
|
+
return {
|
|
2300
|
+
...actions,
|
|
2301
|
+
steps: actions.steps.map((s, i) => {
|
|
2302
|
+
const read = files.paths.get(i);
|
|
2303
|
+
if (s.do !== "drag" || typeof s.path !== "string" || !read) return s;
|
|
2304
|
+
return { ...s, path: inlinePoints(read) };
|
|
2305
|
+
})
|
|
2306
|
+
};
|
|
1147
2307
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
2308
|
+
|
|
2309
|
+
// src/record/pasteStep.ts
|
|
2310
|
+
function toBase64(bytes) {
|
|
2311
|
+
let bin = "";
|
|
2312
|
+
for (let i = 0; i < bytes.length; i += 32768)
|
|
2313
|
+
bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
2314
|
+
return btoa(bin);
|
|
2315
|
+
}
|
|
2316
|
+
function pasteItemOf(step, stepIdx, files) {
|
|
2317
|
+
if (step.text !== void 0) return { text: step.text };
|
|
2318
|
+
const f = files.pastes.get(stepIdx);
|
|
2319
|
+
if (!f) return null;
|
|
2320
|
+
return { b64: toBase64(f.bytes), type: f.type, name: f.name };
|
|
2321
|
+
}
|
|
2322
|
+
async function runPaste(page, context, item, press) {
|
|
2323
|
+
if (item.b64 !== void 0 && item.type?.startsWith("image/")) {
|
|
2324
|
+
const done2 = await page.evaluate(syntheticPasteScript(item, { original: true })).catch((e) => `failed: ${e instanceof Error ? e.message : e}`);
|
|
2325
|
+
return done2 === "ok" ? { via: "file" } : { failed: `the page's paste failed (${done2.replace(/^failed: /, "")})` };
|
|
2326
|
+
}
|
|
2327
|
+
let origin;
|
|
2328
|
+
try {
|
|
2329
|
+
origin = new URL(page.url()).origin;
|
|
2330
|
+
} catch {
|
|
2331
|
+
origin = void 0;
|
|
2332
|
+
}
|
|
2333
|
+
if (context.grantPermissions && origin && origin !== "null")
|
|
2334
|
+
await context.grantPermissions(["clipboard-read", "clipboard-write"], { origin }).catch(() => void 0);
|
|
2335
|
+
const staged = await page.evaluate(stageClipboardScript(item)).catch((e) => `refused: ${e instanceof Error ? e.message : e}`);
|
|
2336
|
+
if (staged === "ok") {
|
|
2337
|
+
await press("ControlOrMeta+v");
|
|
2338
|
+
return { via: "clipboard" };
|
|
2339
|
+
}
|
|
2340
|
+
const why = staged.replace(/^refused: /, "");
|
|
2341
|
+
const done = await page.evaluate(syntheticPasteScript(item)).catch((e) => `failed: ${e instanceof Error ? e.message : e}`);
|
|
2342
|
+
if (done === "ok") return { via: "page", why };
|
|
2343
|
+
return {
|
|
2344
|
+
failed: `this browser has no clipboard to paste from (${why}), and the page's own paste failed too (${done.replace(/^failed: /, "")})`
|
|
2345
|
+
};
|
|
1158
2346
|
}
|
|
1159
2347
|
|
|
1160
2348
|
// src/record/outputs.ts
|
|
@@ -1318,6 +2506,9 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1318
2506
|
`steps[${download}] is a download, and this recorder has nowhere to keep a file. Nothing was recorded.`
|
|
1319
2507
|
);
|
|
1320
2508
|
}
|
|
2509
|
+
const read = await loadScriptFiles(actions, opts.files);
|
|
2510
|
+
if ("problem" in read) throw new RecordRefusedError(read.problem);
|
|
2511
|
+
const files = read;
|
|
1321
2512
|
const vw = actions.viewport?.width ?? 1280;
|
|
1322
2513
|
const vh = actions.viewport?.height ?? 720;
|
|
1323
2514
|
const context = opts.context ?? await browser.newContext({
|
|
@@ -1329,11 +2520,17 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1329
2520
|
await applyHeaders(context, opts.headers, url, log);
|
|
1330
2521
|
const masks = actions.mask ?? [];
|
|
1331
2522
|
if (masks.length) await context.addInitScript(maskInitScript(masks));
|
|
2523
|
+
await context.addInitScript(MEDIA_WATCH_SCRIPT);
|
|
2524
|
+
const soundLog = new PageSoundLog();
|
|
1332
2525
|
const exposureLog = new ExposureLog(masks);
|
|
1333
|
-
const
|
|
2526
|
+
const noticeLog = new NoticeLog();
|
|
2527
|
+
const rawPage = await context.newPage();
|
|
2528
|
+
const pageBound = new StepBound();
|
|
2529
|
+
const page = bounded(rawPage, pageBound);
|
|
1334
2530
|
const scanExposures = async (step) => {
|
|
1335
2531
|
const scan = await page.evaluate(EXPOSURE_PROBE).catch(() => null);
|
|
1336
2532
|
exposureLog.add(step, scan);
|
|
2533
|
+
noticeLog.add(step, scan?.notices);
|
|
1337
2534
|
};
|
|
1338
2535
|
const pageErrors = new PageErrorLog(opts.verbose === true);
|
|
1339
2536
|
page.on("console", (m) => {
|
|
@@ -1341,20 +2538,23 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1341
2538
|
const line = pageErrors.add(m.text());
|
|
1342
2539
|
if (line) log(line);
|
|
1343
2540
|
});
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
navTimeout = true;
|
|
1348
|
-
log(" (networkidle timeout \u2014 continuing)");
|
|
1349
|
-
return null;
|
|
2541
|
+
const readyMode = actions.ready ?? DEFAULT_READY;
|
|
2542
|
+
const open = (target) => untilReady(page, readyMode, (o) => page.goto(target, o), {
|
|
2543
|
+
sleep: realSleep
|
|
1350
2544
|
});
|
|
1351
|
-
|
|
2545
|
+
log(`goto ${url}`);
|
|
2546
|
+
const opened = await open(url);
|
|
2547
|
+
let response = opened.response;
|
|
2548
|
+
const ready = opened.ready;
|
|
2549
|
+
log(` ${readyLine(ready)}`);
|
|
2550
|
+
const navTimeout = ready.mode === "networkidle" && ready.capped;
|
|
1352
2551
|
const setup = actions.setup ?? [];
|
|
1353
2552
|
if (setup.length) {
|
|
1354
2553
|
log(`setup: ${setup.length} step(s), off camera`);
|
|
1355
2554
|
const result = await runSetup(page, setup, {
|
|
1356
2555
|
sleep: realSleep,
|
|
1357
|
-
env: opts.env ?? {}
|
|
2556
|
+
env: opts.env ?? {},
|
|
2557
|
+
goto: (target) => open(target)
|
|
1358
2558
|
});
|
|
1359
2559
|
for (const line of result.lines) log(` ${line}`);
|
|
1360
2560
|
if (result.failed) {
|
|
@@ -1363,8 +2563,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1363
2563
|
throw new SetupError(result.failed);
|
|
1364
2564
|
}
|
|
1365
2565
|
log(`goto ${url} (after setup)`);
|
|
1366
|
-
response = await
|
|
1367
|
-
await sleep(800);
|
|
2566
|
+
response = (await open(url)).response;
|
|
1368
2567
|
}
|
|
1369
2568
|
let wall = null;
|
|
1370
2569
|
if (opts.onArrival) {
|
|
@@ -1385,7 +2584,11 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1385
2584
|
}
|
|
1386
2585
|
}
|
|
1387
2586
|
await scanExposures(-1);
|
|
1388
|
-
|
|
2587
|
+
await page.evaluate(MEDIA_DRAIN_PROBE).catch(() => null);
|
|
2588
|
+
if (actions.steps.some((s) => settleOn(s, actions.settle)))
|
|
2589
|
+
await page.evaluate(CHANGE_AGE_PROBE).catch(() => void 0);
|
|
2590
|
+
const cdp = await context.newCDPSession(rawPage);
|
|
2591
|
+
if (!dry) await fitSurfaceToViewport(cdp, vw, vh);
|
|
1389
2592
|
const frames = [];
|
|
1390
2593
|
let frameIdx = 0;
|
|
1391
2594
|
let t0 = 0;
|
|
@@ -1430,6 +2633,10 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1430
2633
|
return { out, at, ms };
|
|
1431
2634
|
};
|
|
1432
2635
|
const emit = (e) => events.push({ t: now(), ...e });
|
|
2636
|
+
const drainSound = async (step) => {
|
|
2637
|
+
const drain = await page.evaluate(MEDIA_DRAIN_PROBE).catch(() => null);
|
|
2638
|
+
soundLog.add(drain, Date.now(), step);
|
|
2639
|
+
};
|
|
1433
2640
|
await page.mouse.move(cur.x, cur.y);
|
|
1434
2641
|
emit({ x: cur.x, y: cur.y, type: "move" });
|
|
1435
2642
|
const clock = { now: () => Date.now(), sleep };
|
|
@@ -1521,27 +2728,58 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1521
2728
|
...type ? { contentType: type } : {}
|
|
1522
2729
|
};
|
|
1523
2730
|
};
|
|
1524
|
-
const clickAt = async (
|
|
1525
|
-
|
|
1526
|
-
await
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
2731
|
+
const clickAt = async (at, how = {}) => {
|
|
2732
|
+
const rect = "w" in at ? at : void 0;
|
|
2733
|
+
await moveTo(
|
|
2734
|
+
rect ? rect.x + rect.w / 2 : at.x,
|
|
2735
|
+
rect ? rect.y + rect.h / 2 : at.y
|
|
2736
|
+
);
|
|
2737
|
+
const held = [];
|
|
2738
|
+
try {
|
|
2739
|
+
for (const m of how.modifiers ?? []) {
|
|
2740
|
+
const key = resolveModifier(m);
|
|
2741
|
+
await page.keyboard.down(key);
|
|
2742
|
+
held.push(key);
|
|
2743
|
+
}
|
|
2744
|
+
await sleep(PRESS_LEAD_MS);
|
|
2745
|
+
const count = how.count ?? 1;
|
|
2746
|
+
const button = buttonNumber(how.button);
|
|
2747
|
+
for (let n = 1; n <= count; n++) {
|
|
2748
|
+
if (n > 1) await sleep(MULTI_CLICK_GAP_MS);
|
|
2749
|
+
const press = {
|
|
2750
|
+
...how.button && how.button !== "left" ? { button: how.button } : {},
|
|
2751
|
+
...n > 1 ? { clickCount: n } : {}
|
|
2752
|
+
};
|
|
2753
|
+
const hasOpts = Object.keys(press).length > 0;
|
|
2754
|
+
emit({
|
|
2755
|
+
x: Math.round(cur.x),
|
|
2756
|
+
y: Math.round(cur.y),
|
|
2757
|
+
type: "down",
|
|
2758
|
+
button,
|
|
2759
|
+
...rect ? { rect } : {}
|
|
2760
|
+
});
|
|
2761
|
+
await (hasOpts ? page.mouse.down(press) : page.mouse.down());
|
|
2762
|
+
await sleep(n < count ? PRESS_HOLD_MS / 2 : PRESS_HOLD_MS);
|
|
2763
|
+
await (hasOpts ? page.mouse.up(press) : page.mouse.up());
|
|
2764
|
+
emit({
|
|
2765
|
+
x: Math.round(cur.x),
|
|
2766
|
+
y: Math.round(cur.y),
|
|
2767
|
+
type: "up",
|
|
2768
|
+
button,
|
|
2769
|
+
...rect ? { rect } : {}
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
} finally {
|
|
2773
|
+
for (const key of held.reverse())
|
|
2774
|
+
await page.keyboard.up(key).catch(() => void 0);
|
|
2775
|
+
}
|
|
1544
2776
|
};
|
|
2777
|
+
const keyPing = (rect) => emit({
|
|
2778
|
+
x: Math.round(rect ? rect.x + rect.w / 2 : cur.x),
|
|
2779
|
+
y: Math.round(rect ? rect.y + rect.h / 2 : cur.y),
|
|
2780
|
+
type: "key",
|
|
2781
|
+
...rect ? { rect } : {}
|
|
2782
|
+
});
|
|
1545
2783
|
const waitSettled = async () => {
|
|
1546
2784
|
if (dry) return null;
|
|
1547
2785
|
const pressAt = now();
|
|
@@ -1563,6 +2801,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1563
2801
|
const skips = [];
|
|
1564
2802
|
const outputs = [];
|
|
1565
2803
|
const outputNames = /* @__PURE__ */ new Set();
|
|
2804
|
+
const overruns = [];
|
|
1566
2805
|
let stopped = null;
|
|
1567
2806
|
let unpressedGuard = null;
|
|
1568
2807
|
let capped = false;
|
|
@@ -1580,6 +2819,16 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1580
2819
|
return out;
|
|
1581
2820
|
};
|
|
1582
2821
|
const skippedBefore = skipped.length;
|
|
2822
|
+
const paced = {
|
|
2823
|
+
v: null
|
|
2824
|
+
};
|
|
2825
|
+
const timed = async (plannedMs, fn) => {
|
|
2826
|
+
const t = Date.now();
|
|
2827
|
+
const out = await fn();
|
|
2828
|
+
paced.v = { plannedMs, tookMs: Date.now() - t };
|
|
2829
|
+
return out;
|
|
2830
|
+
};
|
|
2831
|
+
let askedOverride = null;
|
|
1583
2832
|
const urlBefore = page.url();
|
|
1584
2833
|
let stepRect = null;
|
|
1585
2834
|
const extra = {};
|
|
@@ -1595,7 +2844,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1595
2844
|
if (reason) extra.reason = reason;
|
|
1596
2845
|
};
|
|
1597
2846
|
if (step.guard) extra.guard = step.guard;
|
|
1598
|
-
const rehearseGuard = async (sel) => {
|
|
2847
|
+
const rehearseGuard = async (sel, point) => {
|
|
1599
2848
|
if (!step.guard || !dry) return false;
|
|
1600
2849
|
const rect = sel ? await boxOf(sel) : null;
|
|
1601
2850
|
stepRect = rect;
|
|
@@ -1603,8 +2852,9 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1603
2852
|
else {
|
|
1604
2853
|
if (rect)
|
|
1605
2854
|
await gesture(() => moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2));
|
|
2855
|
+
else if (point) await gesture(() => moveTo(point.x, point.y));
|
|
1606
2856
|
log(
|
|
1607
|
-
`${tag}: guarded (${step.guard}), ${rect ? "hovered" : "left alone"}, not pressed`
|
|
2857
|
+
`${tag}: guarded (${step.guard}), ${rect || point ? "hovered" : "left alone"}, not pressed`
|
|
1608
2858
|
);
|
|
1609
2859
|
}
|
|
1610
2860
|
unpressedGuard = stepIdx;
|
|
@@ -1618,14 +2868,40 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1618
2868
|
);
|
|
1619
2869
|
return true;
|
|
1620
2870
|
};
|
|
2871
|
+
const boundMs = stepBoundMs(
|
|
2872
|
+
step,
|
|
2873
|
+
askedMs(step),
|
|
2874
|
+
{
|
|
2875
|
+
expectS: EXPECT_TIMEOUT_S,
|
|
2876
|
+
awaitS: AWAIT_TIMEOUT_S,
|
|
2877
|
+
downloadS: DOWNLOAD_TIMEOUT_S
|
|
2878
|
+
}
|
|
2879
|
+
);
|
|
2880
|
+
opts.onEvent?.({
|
|
2881
|
+
event: "step",
|
|
2882
|
+
phase: "start",
|
|
2883
|
+
i: stepIdx,
|
|
2884
|
+
...step.id ? { id: step.id } : {},
|
|
2885
|
+
do: step.do
|
|
2886
|
+
});
|
|
2887
|
+
const stepWall = Date.now();
|
|
2888
|
+
pageBound.start(boundMs);
|
|
1621
2889
|
switch (step.do) {
|
|
1622
2890
|
case "wait":
|
|
1623
2891
|
await sleep(clampWait(step.ms, now(), maxSeconds));
|
|
1624
2892
|
break;
|
|
1625
2893
|
case "move":
|
|
1626
|
-
await moveTo(step.x, step.y);
|
|
2894
|
+
await gesture(() => moveTo(step.x, step.y));
|
|
1627
2895
|
break;
|
|
1628
2896
|
case "hover": {
|
|
2897
|
+
if (step.selector === void 0) {
|
|
2898
|
+
const px = step.x ?? cur.x;
|
|
2899
|
+
const py = step.y ?? cur.y;
|
|
2900
|
+
await gesture(() => moveTo(px, py));
|
|
2901
|
+
log(`hover ${Math.round(px)},${Math.round(py)}`);
|
|
2902
|
+
await sleep(step.ms ?? 700);
|
|
2903
|
+
break;
|
|
2904
|
+
}
|
|
1629
2905
|
const rect = await boxOf(step.selector);
|
|
1630
2906
|
stepRect = rect;
|
|
1631
2907
|
if (rect) {
|
|
@@ -1637,6 +2913,21 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1637
2913
|
break;
|
|
1638
2914
|
}
|
|
1639
2915
|
case "click": {
|
|
2916
|
+
const how = {
|
|
2917
|
+
count: step.count,
|
|
2918
|
+
button: step.button,
|
|
2919
|
+
modifiers: step.modifiers
|
|
2920
|
+
};
|
|
2921
|
+
const said = `${step.count === 3 ? "triple-click" : step.count === 2 ? "double-click" : "click"}${step.button && step.button !== "left" ? ` (${step.button})` : ""}${step.modifiers?.length ? ` holding ${step.modifiers.join("+")}` : ""}`;
|
|
2922
|
+
if (step.selector === void 0) {
|
|
2923
|
+
const point = { x: step.x ?? cur.x, y: step.y ?? cur.y };
|
|
2924
|
+
if (await rehearseGuard(void 0, point)) break;
|
|
2925
|
+
await gesture(() => clickAt(point, how));
|
|
2926
|
+
log(`${said} at ${Math.round(point.x)},${Math.round(point.y)}`);
|
|
2927
|
+
const sinceChange = await gesture(() => waitSettled());
|
|
2928
|
+
await sleep(holdLeftMs(settleMs(step), sinceChange));
|
|
2929
|
+
break;
|
|
2930
|
+
}
|
|
1640
2931
|
if (await rehearseGuard(step.selector)) break;
|
|
1641
2932
|
const rect = await boxOf(step.selector);
|
|
1642
2933
|
stepRect = rect;
|
|
@@ -1652,8 +2943,8 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1652
2943
|
` (${tag} click: ${step.selector} is disabled; pressed anyway)`
|
|
1653
2944
|
);
|
|
1654
2945
|
}
|
|
1655
|
-
await gesture(() => clickAt(rect));
|
|
1656
|
-
log(
|
|
2946
|
+
await gesture(() => clickAt(rect, how));
|
|
2947
|
+
log(`${said} ${step.selector}`);
|
|
1657
2948
|
const sinceChange = await gesture(() => waitSettled());
|
|
1658
2949
|
await sleep(holdLeftMs(settleMs(step), sinceChange));
|
|
1659
2950
|
} else
|
|
@@ -1677,17 +2968,21 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1677
2968
|
rect
|
|
1678
2969
|
});
|
|
1679
2970
|
let lastPing = -Infinity;
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
delay,
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
2971
|
+
const chars = [...step.text];
|
|
2972
|
+
await timed(
|
|
2973
|
+
Math.max(0, chars.length - 1) * delay,
|
|
2974
|
+
() => clockTyping(
|
|
2975
|
+
chars,
|
|
2976
|
+
delay,
|
|
2977
|
+
async (ch) => {
|
|
2978
|
+
if (now() - lastPing >= 350) {
|
|
2979
|
+
lastPing = now();
|
|
2980
|
+
ping();
|
|
2981
|
+
}
|
|
2982
|
+
await page.keyboard.type(ch);
|
|
2983
|
+
},
|
|
2984
|
+
clock
|
|
2985
|
+
)
|
|
1691
2986
|
);
|
|
1692
2987
|
ping();
|
|
1693
2988
|
log(`type ${step.text.length} chars into ${step.selector}`);
|
|
@@ -1711,14 +3006,18 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1711
3006
|
break;
|
|
1712
3007
|
}
|
|
1713
3008
|
}
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
3009
|
+
const seq = pressSequence(step);
|
|
3010
|
+
const chords = step.keys !== void 0 ? seq.map(resolveChord) : seq;
|
|
3011
|
+
const plan = keySequencePlanMs(chords.length);
|
|
3012
|
+
if (chords.length > 1) askedOverride = plan + settleMs(step);
|
|
3013
|
+
await timed(plan, async () => {
|
|
3014
|
+
for (const [i, chord] of chords.entries()) {
|
|
3015
|
+
if (i > 0) await sleep(KEY_SEQUENCE_GAP_MS);
|
|
3016
|
+
keyPing(rect);
|
|
3017
|
+
await page.keyboard.press(chord);
|
|
3018
|
+
}
|
|
1719
3019
|
});
|
|
1720
|
-
|
|
1721
|
-
log(`press ${step.key}`);
|
|
3020
|
+
log(`press ${seq.join(", ")}`);
|
|
1722
3021
|
const sinceChange = await gesture(() => waitSettled());
|
|
1723
3022
|
await sleep(holdLeftMs(settleMs(step), sinceChange));
|
|
1724
3023
|
break;
|
|
@@ -1749,9 +3048,19 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1749
3048
|
break;
|
|
1750
3049
|
}
|
|
1751
3050
|
case "drag": {
|
|
3051
|
+
let path = null;
|
|
3052
|
+
if (typeof step.path === "string") {
|
|
3053
|
+
const read2 = files.paths.get(stepIdx);
|
|
3054
|
+
if (read2) path = schedulePath(read2, step.ms);
|
|
3055
|
+
} else if (step.path !== void 0) {
|
|
3056
|
+
const read2 = readPath(step.path);
|
|
3057
|
+
if (!("problems" in read2)) path = schedulePath(read2, step.ms);
|
|
3058
|
+
}
|
|
1752
3059
|
let start = null;
|
|
1753
3060
|
let rect;
|
|
1754
|
-
if (
|
|
3061
|
+
if (path) {
|
|
3062
|
+
start = { x: path[0].x, y: path[0].y };
|
|
3063
|
+
} else if (step.selector) {
|
|
1755
3064
|
const box = await boxOf(step.selector);
|
|
1756
3065
|
stepRect = box;
|
|
1757
3066
|
if (box) {
|
|
@@ -1769,42 +3078,148 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1769
3078
|
start = { x: step.x, y: step.y };
|
|
1770
3079
|
}
|
|
1771
3080
|
if (!start) break;
|
|
1772
|
-
|
|
1773
|
-
await gesture(() =>
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
3081
|
+
const from0 = start;
|
|
3082
|
+
await gesture(() => moveTo(from0.x, from0.y));
|
|
3083
|
+
const mods = step.modifiers ?? [];
|
|
3084
|
+
const held = [];
|
|
3085
|
+
try {
|
|
3086
|
+
for (const m of mods) {
|
|
3087
|
+
const key = resolveModifier(m);
|
|
3088
|
+
await page.keyboard.down(key);
|
|
3089
|
+
held.push(key);
|
|
3090
|
+
}
|
|
3091
|
+
await gesture(() => sleep(PRESS_LEAD_MS));
|
|
3092
|
+
const bits = modifierBits(mods, opts.platform);
|
|
3093
|
+
const penAt = (type, p) => cdp.send("Input.dispatchMouseEvent", {
|
|
3094
|
+
type,
|
|
3095
|
+
x: p.x,
|
|
3096
|
+
y: p.y,
|
|
3097
|
+
button: "left",
|
|
3098
|
+
buttons: type === "mouseReleased" ? 0 : 1,
|
|
3099
|
+
...type === "mouseMoved" ? {} : { clickCount: 1 },
|
|
3100
|
+
pointerType: "pen",
|
|
3101
|
+
force: type === "mouseReleased" ? 0 : p.pressure ?? 0.5,
|
|
3102
|
+
modifiers: bits
|
|
3103
|
+
});
|
|
3104
|
+
let pen = path?.some((p) => p.pressure !== void 0) ?? false;
|
|
3105
|
+
if (pen && path) {
|
|
3106
|
+
try {
|
|
3107
|
+
await penAt("mousePressed", path[0]);
|
|
3108
|
+
} catch {
|
|
3109
|
+
pen = false;
|
|
3110
|
+
log(
|
|
3111
|
+
` (${tag} drag: this browser takes no pen input; the path is drawn with the mouse and its pressure dropped)`
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
emit({
|
|
3116
|
+
x: Math.round(cur.x),
|
|
3117
|
+
y: Math.round(cur.y),
|
|
3118
|
+
type: "down",
|
|
3119
|
+
button: 0,
|
|
3120
|
+
rect
|
|
3121
|
+
});
|
|
3122
|
+
if (!pen) await page.mouse.down();
|
|
3123
|
+
await gesture(() => sleep(PRESS_HOLD_MS));
|
|
3124
|
+
if (path) {
|
|
3125
|
+
const pts = path;
|
|
3126
|
+
const total = pathDurationMs(pts);
|
|
3127
|
+
askedOverride = total;
|
|
3128
|
+
await timed(total, async () => {
|
|
3129
|
+
const from = Date.now();
|
|
3130
|
+
for (let i = 1; i < pts.length; i++) {
|
|
3131
|
+
const p = pts[i];
|
|
3132
|
+
if (!dry) {
|
|
3133
|
+
const wait = from + p.at - Date.now();
|
|
3134
|
+
if (wait > 0) await realSleep(wait);
|
|
3135
|
+
}
|
|
3136
|
+
if (pen) await penAt("mouseMoved", p);
|
|
3137
|
+
else await page.mouse.move(p.x, p.y);
|
|
3138
|
+
cur.x = p.x;
|
|
3139
|
+
cur.y = p.y;
|
|
3140
|
+
emit({ x: Math.round(p.x), y: Math.round(p.y), type: "move" });
|
|
3141
|
+
}
|
|
3142
|
+
});
|
|
3143
|
+
} else {
|
|
3144
|
+
const dur = step.ms ?? 700;
|
|
3145
|
+
const from = { ...cur };
|
|
3146
|
+
const tx = step.tx ?? from.x;
|
|
3147
|
+
const ty = step.ty ?? from.y;
|
|
3148
|
+
await timed(
|
|
3149
|
+
dur,
|
|
3150
|
+
() => clockMotion(
|
|
3151
|
+
dur,
|
|
3152
|
+
async (u) => {
|
|
3153
|
+
cur.x = from.x + (tx - from.x) * u;
|
|
3154
|
+
cur.y = from.y + (ty - from.y) * u;
|
|
3155
|
+
await page.mouse.move(cur.x, cur.y);
|
|
3156
|
+
emit({
|
|
3157
|
+
x: Math.round(cur.x),
|
|
3158
|
+
y: Math.round(cur.y),
|
|
3159
|
+
type: "move"
|
|
3160
|
+
});
|
|
3161
|
+
},
|
|
3162
|
+
clock
|
|
3163
|
+
)
|
|
3164
|
+
);
|
|
3165
|
+
}
|
|
3166
|
+
await gesture(() => sleep(settleMs({ do: "drag" })));
|
|
3167
|
+
if (pen) await penAt("mouseReleased", { x: cur.x, y: cur.y });
|
|
3168
|
+
else await page.mouse.up();
|
|
3169
|
+
emit({
|
|
3170
|
+
x: Math.round(cur.x),
|
|
3171
|
+
y: Math.round(cur.y),
|
|
3172
|
+
type: "up",
|
|
3173
|
+
button: 0,
|
|
3174
|
+
rect
|
|
3175
|
+
});
|
|
3176
|
+
} finally {
|
|
3177
|
+
for (const key of held.reverse())
|
|
3178
|
+
await page.keyboard.up(key).catch(() => void 0);
|
|
3179
|
+
}
|
|
3180
|
+
const holding = mods.length ? ` holding ${mods.join("+")}` : "";
|
|
3181
|
+
log(
|
|
3182
|
+
path ? `drag a path of ${path.length} points from ${Math.round(start.x)},${Math.round(start.y)}${holding}` : `drag ${step.selector ?? `${start.x},${start.y}`} \u2192 ${step.tx},${step.ty}${holding}`
|
|
1794
3183
|
);
|
|
1795
|
-
await gesture(() => sleep(
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
3184
|
+
await gesture(() => sleep(DRAG_HOLD_MS));
|
|
3185
|
+
break;
|
|
3186
|
+
}
|
|
3187
|
+
case "paste": {
|
|
3188
|
+
const item = pasteItemOf(step, stepIdx, files);
|
|
3189
|
+
if (!item) {
|
|
3190
|
+
skip(step.file ?? "", `"${step.file}" was not read`);
|
|
3191
|
+
break;
|
|
3192
|
+
}
|
|
3193
|
+
let rect = null;
|
|
3194
|
+
if (step.selector) {
|
|
3195
|
+
rect = await boxOf(step.selector);
|
|
3196
|
+
stepRect = rect;
|
|
3197
|
+
if (!rect) {
|
|
3198
|
+
skip(step.selector);
|
|
3199
|
+
break;
|
|
3200
|
+
}
|
|
3201
|
+
const at = rect;
|
|
3202
|
+
await gesture(() => clickAt(at));
|
|
3203
|
+
}
|
|
3204
|
+
const ping = { done: false };
|
|
3205
|
+
const outcome = await runPaste(page, context, item, async (chord) => {
|
|
3206
|
+
keyPing(rect);
|
|
3207
|
+
ping.done = true;
|
|
3208
|
+
await page.keyboard.press(chord);
|
|
1803
3209
|
});
|
|
3210
|
+
if ("failed" in outcome) {
|
|
3211
|
+
skip(step.selector ?? step.file ?? "paste", outcome.failed);
|
|
3212
|
+
log(` (${tag} paste: ${outcome.failed})`);
|
|
3213
|
+
break;
|
|
3214
|
+
}
|
|
3215
|
+
if (!ping.done) keyPing(rect);
|
|
3216
|
+
const what = step.text !== void 0 ? `${[...step.text].length} chars` : `the file ${step.file}`;
|
|
3217
|
+
const into = step.selector ? ` into ${step.selector}` : "";
|
|
1804
3218
|
log(
|
|
1805
|
-
`
|
|
3219
|
+
outcome.via === "file" ? `paste ${what}${into}, dispatched in the page with the file's own bytes (the browser's clipboard re-encodes an image and drops what it carries, its density among it); its events are untrusted` : outcome.via === "page" ? `paste ${what}${into}, dispatched in the page (the browser's clipboard refused it: ${outcome.why}); its events are untrusted` : `paste ${what}${into}`
|
|
1806
3220
|
);
|
|
1807
|
-
await
|
|
3221
|
+
const sinceChange = await gesture(() => waitSettled());
|
|
3222
|
+
await sleep(holdLeftMs(settleMs(step), sinceChange));
|
|
1808
3223
|
break;
|
|
1809
3224
|
}
|
|
1810
3225
|
case "expect": {
|
|
@@ -1828,14 +3243,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1828
3243
|
if (notWaited()) break;
|
|
1829
3244
|
const cond = step.until;
|
|
1830
3245
|
const timeoutMs = (step.timeout ?? AWAIT_TIMEOUT_S) * 1e3;
|
|
1831
|
-
const reload = step.reload ?
|
|
1832
|
-
const opts2 = {
|
|
1833
|
-
waitUntil: "networkidle",
|
|
1834
|
-
timeout: Math.min(45e3, timeoutMs)
|
|
1835
|
-
};
|
|
1836
|
-
await (page.reload ? page.reload(opts2) : page.goto(page.url(), opts2)).catch(() => null);
|
|
1837
|
-
await domSettled(page, { sleep: realSleep });
|
|
1838
|
-
} : void 0;
|
|
3246
|
+
const reload = step.reload ? () => reloadUntilReady(page, readyMode, timeoutMs) : void 0;
|
|
1839
3247
|
log(`await ${sayCondition(cond)} (off camera)\u2026`);
|
|
1840
3248
|
const awaitEvent = {
|
|
1841
3249
|
event: "await",
|
|
@@ -1965,6 +3373,50 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1965
3373
|
break;
|
|
1966
3374
|
}
|
|
1967
3375
|
}
|
|
3376
|
+
if (settleOn(step, actions.settle) && skipped.length === skippedBefore && !stopped) {
|
|
3377
|
+
const res = await gesture(
|
|
3378
|
+
() => appSettled(page, () => frames.length, {
|
|
3379
|
+
sleep: realSleep,
|
|
3380
|
+
sinceFrame: () => frames.length ? now() - frames[frames.length - 1].tMs : null
|
|
3381
|
+
})
|
|
3382
|
+
);
|
|
3383
|
+
extra.settleMs = res.ms;
|
|
3384
|
+
if (!res.settled)
|
|
3385
|
+
log(
|
|
3386
|
+
` (${tag} settle: the page was still changing after ${sayDuration(res.ms)}; going on)`
|
|
3387
|
+
);
|
|
3388
|
+
}
|
|
3389
|
+
if (paced.v && !dry) {
|
|
3390
|
+
const over = overrunOf(paced.v.plannedMs, paced.v.tookMs);
|
|
3391
|
+
if (over !== null) {
|
|
3392
|
+
overruns.push({
|
|
3393
|
+
step: stepIdx,
|
|
3394
|
+
...step.id ? { id: step.id } : {},
|
|
3395
|
+
do: step.do,
|
|
3396
|
+
plannedMs: Math.round(paced.v.plannedMs),
|
|
3397
|
+
tookMs: Math.round(paced.v.tookMs),
|
|
3398
|
+
overrunMs: over
|
|
3399
|
+
});
|
|
3400
|
+
extra.overrunMs = over;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
const hang = pageBound.end();
|
|
3404
|
+
if (hang.hung) {
|
|
3405
|
+
const reason = hangReason(step.do, hang.ms, hang.boundMs);
|
|
3406
|
+
const said = skipped.length > skippedBefore ? skipped.at(-1) : void 0;
|
|
3407
|
+
if (said) {
|
|
3408
|
+
said.reason = reason;
|
|
3409
|
+
extra.reason = reason;
|
|
3410
|
+
} else skip(step.selector ?? "", reason);
|
|
3411
|
+
log(` (${tag} ${step.do}: ${reason})`);
|
|
3412
|
+
if (opts.stopOnHang && !stopped)
|
|
3413
|
+
stopped = {
|
|
3414
|
+
step: stepIdx,
|
|
3415
|
+
...step.id ? { id: step.id } : {},
|
|
3416
|
+
reason: `${tag} ${step.do}: ${reason}, so the take stopped there`
|
|
3417
|
+
};
|
|
3418
|
+
}
|
|
3419
|
+
pageBound.start(POST_STEP_READ_MS);
|
|
1968
3420
|
if (step.guard && !dry && skipped.length === skippedBefore) {
|
|
1969
3421
|
confirmed.push(step.id ?? String(stepIdx));
|
|
1970
3422
|
extra.confirmed = true;
|
|
@@ -1999,11 +3451,24 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
1999
3451
|
paces.push({
|
|
2000
3452
|
step: stepIdx,
|
|
2001
3453
|
do: step.do,
|
|
2002
|
-
askedMs: askedMs(step),
|
|
3454
|
+
askedMs: askedOverride ?? askedMs(step),
|
|
2003
3455
|
gestureMs,
|
|
2004
3456
|
wallMs: now() - stepStart
|
|
2005
3457
|
});
|
|
2006
3458
|
await scanExposures(stepIdx);
|
|
3459
|
+
await drainSound(stepIdx);
|
|
3460
|
+
pageBound.end();
|
|
3461
|
+
const stepSpan = stepSpans[stepSpans.length - 1];
|
|
3462
|
+
opts.onEvent?.({
|
|
3463
|
+
event: "step",
|
|
3464
|
+
phase: "end",
|
|
3465
|
+
i: stepIdx,
|
|
3466
|
+
...step.id ? { id: step.id } : {},
|
|
3467
|
+
do: step.do,
|
|
3468
|
+
ms: Date.now() - stepWall,
|
|
3469
|
+
...stepSpan?.skipped ? { skipped: true } : {},
|
|
3470
|
+
...hang.hung ? { hung: true } : {}
|
|
3471
|
+
});
|
|
2007
3472
|
if (stopped) break;
|
|
2008
3473
|
}
|
|
2009
3474
|
if (!capped && !stopped) await sleep(TRAILING_HOLD_MS);
|
|
@@ -2011,13 +3476,27 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2011
3476
|
capped = true;
|
|
2012
3477
|
log(` ${cappedLine(maxSeconds)}`);
|
|
2013
3478
|
}
|
|
3479
|
+
pageBound.start(POST_STEP_READ_MS * 2);
|
|
3480
|
+
await drainSound();
|
|
3481
|
+
const endWall = Date.now();
|
|
2014
3482
|
const durationMs = capped ? Math.min(now(), maxSeconds * 1e3) : now();
|
|
3483
|
+
const heard = soundLog.heard(takeClock, endWall, durationMs);
|
|
2015
3484
|
await cdp.send("Page.stopScreencast").catch(() => {
|
|
2016
3485
|
});
|
|
3486
|
+
const media = await keepPageSound(
|
|
3487
|
+
heard,
|
|
3488
|
+
dry ? void 0 : opts.sounds,
|
|
3489
|
+
fetchSource,
|
|
3490
|
+
log
|
|
3491
|
+
);
|
|
2017
3492
|
await sleep(200);
|
|
2018
|
-
const pageTitle = await page.title().catch(() => "");
|
|
3493
|
+
const pageTitle = await page.title().catch(() => "") ?? "";
|
|
2019
3494
|
for (const line of pageErrors.repeats()) log(line);
|
|
2020
|
-
|
|
3495
|
+
pageBound.end();
|
|
3496
|
+
pageBound.start(CLOSE_BOUND_MS);
|
|
3497
|
+
await pageBound.race(context.close().catch(() => {
|
|
3498
|
+
}));
|
|
3499
|
+
pageBound.end();
|
|
2021
3500
|
await sinkChain;
|
|
2022
3501
|
const meta = {
|
|
2023
3502
|
dpr: 1,
|
|
@@ -2040,9 +3519,11 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2040
3519
|
steps: stepSpans,
|
|
2041
3520
|
...wall ? { wall: { kind: wall.kind, asked: wall.asked, landed: wall.landed } } : {},
|
|
2042
3521
|
...exposureLog.exposures().length ? { exposures: exposureLog.exposures() } : {},
|
|
3522
|
+
...noticeLog.notices().length ? { notices: noticeLog.notices() } : {},
|
|
2043
3523
|
...masks.length ? { masks: exposureLog.masks(masks) } : {},
|
|
2044
3524
|
...skips.length ? { skips } : {},
|
|
2045
|
-
...outputs.length ? { outputs } : {}
|
|
3525
|
+
...outputs.length ? { outputs } : {},
|
|
3526
|
+
...media.length ? { media } : {}
|
|
2046
3527
|
};
|
|
2047
3528
|
const freezes = [];
|
|
2048
3529
|
for (let i = 1; i < frames.length; i++) {
|
|
@@ -2058,9 +3539,11 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2058
3539
|
const frozenMs = freezes.reduce((sum, f) => sum + f.ms, 0);
|
|
2059
3540
|
const freezePct = durationMs > 0 ? Math.round(frozenMs / durationMs * 100) : 0;
|
|
2060
3541
|
const dead = deadTime(stepSpans, frames, events, durationMs);
|
|
2061
|
-
if (deadWarns(dead)) log(` WARNING: ${deadLine(dead)}`);
|
|
3542
|
+
if (!dry && deadWarns(dead)) log(` WARNING: ${deadLine(dead)}`);
|
|
2062
3543
|
const pace = paceReport(paces);
|
|
2063
3544
|
if (!dry) log(` ${paceLine(pace)}`);
|
|
3545
|
+
const overrun = overrunLine(overruns);
|
|
3546
|
+
if (overrun) log(` WARNING: ${overrun}`);
|
|
2064
3547
|
return {
|
|
2065
3548
|
events,
|
|
2066
3549
|
frames,
|
|
@@ -2068,8 +3551,10 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2068
3551
|
pace,
|
|
2069
3552
|
skipped,
|
|
2070
3553
|
navTimeout,
|
|
3554
|
+
ready,
|
|
2071
3555
|
wall,
|
|
2072
3556
|
exposures: exposureLog.exposures(),
|
|
3557
|
+
notices: noticeLog.notices(),
|
|
2073
3558
|
masks: exposureLog.masks(masks),
|
|
2074
3559
|
freezes,
|
|
2075
3560
|
freezePct,
|
|
@@ -2079,11 +3564,50 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
|
2079
3564
|
confirmed,
|
|
2080
3565
|
skips,
|
|
2081
3566
|
outputs,
|
|
3567
|
+
media,
|
|
2082
3568
|
stopped,
|
|
2083
3569
|
pageErrors: pageErrors.entries(),
|
|
2084
|
-
|
|
3570
|
+
overruns,
|
|
3571
|
+
// Self-contained: a path a file held is inline, so the take re-records
|
|
3572
|
+
// from its own script with the file gone.
|
|
3573
|
+
actions: { ...inlineScript(actions, files), url }
|
|
2085
3574
|
};
|
|
2086
3575
|
}
|
|
3576
|
+
async function keepPageSound(heard, sink, fetchSource, log) {
|
|
3577
|
+
const kept = /* @__PURE__ */ new Map();
|
|
3578
|
+
const used = /* @__PURE__ */ new Set();
|
|
3579
|
+
for (const h of heard) {
|
|
3580
|
+
if (!h.meta.intervals.length) continue;
|
|
3581
|
+
const stretches = h.meta.intervals.length;
|
|
3582
|
+
log(
|
|
3583
|
+
` page sound: ${h.meta.src || "a media element"} heard ${stretches === 1 ? "once" : `${stretches} times`}`
|
|
3584
|
+
);
|
|
3585
|
+
if (!sink || kept.has(h.src)) continue;
|
|
3586
|
+
try {
|
|
3587
|
+
const got = await fetchSource(h.src);
|
|
3588
|
+
const name = outputName(
|
|
3589
|
+
void 0,
|
|
3590
|
+
got.suggested,
|
|
3591
|
+
`sound-${kept.size + 1}${extForType(got.contentType)}`,
|
|
3592
|
+
used
|
|
3593
|
+
);
|
|
3594
|
+
used.add(name);
|
|
3595
|
+
kept.set(h.src, {
|
|
3596
|
+
file: await sink.save(name, got.bytes, got.contentType)
|
|
3597
|
+
});
|
|
3598
|
+
} catch (e) {
|
|
3599
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
3600
|
+
kept.set(h.src, { fetchError: why });
|
|
3601
|
+
log(` (page sound not kept: ${why})`);
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
return heard.map((h) => ({ ...h.meta, ...kept.get(h.src) ?? {} }));
|
|
3605
|
+
}
|
|
3606
|
+
var POST_STEP_READ_MS = 5e3;
|
|
3607
|
+
var CLOSE_BOUND_MS = 1e4;
|
|
3608
|
+
function hangReason(verb, ms, boundMs) {
|
|
3609
|
+
return `the page stopped answering: this ${verb} ran ${sayDuration(ms)}, past its bound of ${sayDuration(boundMs)}`;
|
|
3610
|
+
}
|
|
2087
3611
|
var EXT_FOR_TYPE = {
|
|
2088
3612
|
"video/mp4": ".mp4",
|
|
2089
3613
|
"video/webm": ".webm",
|
|
@@ -2145,7 +3669,15 @@ try {
|
|
|
2145
3669
|
fi = next
|
|
2146
3670
|
const blob = await (await fetch(${JSON.stringify(o.framesBaseUrl)} + frames[fi].file + ${JSON.stringify(q)})).blob()
|
|
2147
3671
|
const bmp = await createImageBitmap(blob)
|
|
2148
|
-
|
|
3672
|
+
// A frame is drawn at its own size, never stretched to the take's: a
|
|
3673
|
+
// frame of another size means the surface moved mid-take, and a
|
|
3674
|
+
// picture scaled to fit would be a distorted take nobody was told of.
|
|
3675
|
+
if (bmp.width !== W || bmp.height !== H) {
|
|
3676
|
+
const size = bmp.width + 'x' + bmp.height
|
|
3677
|
+
bmp.close()
|
|
3678
|
+
throw new Error('frame ' + frames[fi].file + ' is ' + size + ', the take is ' + W + 'x' + H + ' (meta.captureWidth/captureHeight): frames are never resized, so the recorded surface changed size mid-take. Re-record it')
|
|
3679
|
+
}
|
|
3680
|
+
c2.drawImage(bmp, 0, 0)
|
|
2149
3681
|
bmp.close()
|
|
2150
3682
|
}
|
|
2151
3683
|
await src.add(t, 1 / FPS)
|
|
@@ -2169,9 +3701,12 @@ try {
|
|
|
2169
3701
|
// src/record/plan.ts
|
|
2170
3702
|
import {
|
|
2171
3703
|
DEFAULT_FRAME_STYLE,
|
|
3704
|
+
EXPORT_RESOLUTION_OPTIONS,
|
|
2172
3705
|
planAutoSpeed,
|
|
2173
3706
|
planAutoZoom,
|
|
2174
3707
|
projectFromArtifact,
|
|
3708
|
+
ratedSegments,
|
|
3709
|
+
recommendedExportResolution,
|
|
2175
3710
|
withBackdrop
|
|
2176
3711
|
} from "@vosjs/studio-core";
|
|
2177
3712
|
function planFreshTake(artifact, recordingName, opts = {}) {
|
|
@@ -2190,6 +3725,66 @@ function planFreshTake(artifact, recordingName, opts = {}) {
|
|
|
2190
3725
|
params: doc.speedParams,
|
|
2191
3726
|
activity: opts.activity
|
|
2192
3727
|
});
|
|
3728
|
+
return withFittingExport(withPageSound(doc, artifact.meta.media));
|
|
3729
|
+
}
|
|
3730
|
+
function withFittingExport(doc) {
|
|
3731
|
+
if (!doc.export) return doc;
|
|
3732
|
+
const fits = recommendedExportResolution(doc);
|
|
3733
|
+
const order = EXPORT_RESOLUTION_OPTIONS;
|
|
3734
|
+
if (order.indexOf(fits) < order.indexOf(doc.export.resolution))
|
|
3735
|
+
doc.export = { ...doc.export, resolution: fits };
|
|
3736
|
+
return doc;
|
|
3737
|
+
}
|
|
3738
|
+
function sourceToOutput(pieces, s) {
|
|
3739
|
+
let acc = 0;
|
|
3740
|
+
for (const p of pieces) {
|
|
3741
|
+
const rate = p.rate && p.rate > 0 ? p.rate : 1;
|
|
3742
|
+
if (p.media) {
|
|
3743
|
+
acc += Math.max(0, p.out - p.in) / rate;
|
|
3744
|
+
continue;
|
|
3745
|
+
}
|
|
3746
|
+
if (s < p.in) return acc;
|
|
3747
|
+
if (s <= p.out) return acc + (s - p.in) / rate;
|
|
3748
|
+
acc += Math.max(0, p.out - p.in) / rate;
|
|
3749
|
+
}
|
|
3750
|
+
return acc;
|
|
3751
|
+
}
|
|
3752
|
+
function withPageSound(doc, media) {
|
|
3753
|
+
const kept = (doc.audio ?? []).filter((a) => a.source !== "page");
|
|
3754
|
+
const heard = (media ?? []).filter((m) => m.file && m.intervals.length);
|
|
3755
|
+
if (!heard.length) {
|
|
3756
|
+
doc.audio = kept;
|
|
3757
|
+
return doc;
|
|
3758
|
+
}
|
|
3759
|
+
const spans = heard.flatMap(
|
|
3760
|
+
(m) => m.intervals.map((iv) => ({ in: iv.at, out: iv.at + (iv.to - iv.from) }))
|
|
3761
|
+
);
|
|
3762
|
+
doc.speed = (doc.speed ?? []).filter(
|
|
3763
|
+
(sp) => sp.source !== "auto" || !!sp.media || !spans.some((h) => sp.in < h.out && h.in < sp.out)
|
|
3764
|
+
);
|
|
3765
|
+
const pieces = ratedSegments(doc);
|
|
3766
|
+
const clips = [];
|
|
3767
|
+
heard.forEach((m, i) => {
|
|
3768
|
+
const file = m.file;
|
|
3769
|
+
const name = file.split("/").pop() || file;
|
|
3770
|
+
const length = Math.max(m.duration ?? 0, ...m.intervals.map((iv) => iv.to));
|
|
3771
|
+
m.intervals.forEach((iv, j) => {
|
|
3772
|
+
clips.push({
|
|
3773
|
+
id: `page-${i + 1}-${j + 1}`,
|
|
3774
|
+
key: file,
|
|
3775
|
+
name,
|
|
3776
|
+
start: +sourceToOutput(pieces, iv.at).toFixed(3),
|
|
3777
|
+
in: iv.from,
|
|
3778
|
+
out: iv.to,
|
|
3779
|
+
duration: +length.toFixed(3),
|
|
3780
|
+
gain: m.volume ?? 1,
|
|
3781
|
+
fadeIn: 0,
|
|
3782
|
+
fadeOut: 0,
|
|
3783
|
+
source: "page"
|
|
3784
|
+
});
|
|
3785
|
+
});
|
|
3786
|
+
});
|
|
3787
|
+
doc.audio = [...kept, ...clips];
|
|
2193
3788
|
return doc;
|
|
2194
3789
|
}
|
|
2195
3790
|
function splitSegmentsAtSkips(segments, skips) {
|
|
@@ -2223,24 +3818,52 @@ function splitSegmentsAtSkips(segments, skips) {
|
|
|
2223
3818
|
return out;
|
|
2224
3819
|
}
|
|
2225
3820
|
export {
|
|
3821
|
+
APP_QUIET_MS,
|
|
3822
|
+
APP_SETTLE_CAP_MS,
|
|
2226
3823
|
AWAIT_HEARTBEAT_MS,
|
|
2227
3824
|
AWAIT_POLL_S,
|
|
2228
3825
|
AWAIT_TIMEOUT_S,
|
|
3826
|
+
CHANGE_AGE_PROBE,
|
|
3827
|
+
CLICK_BUTTONS,
|
|
2229
3828
|
DEAD_SHARE_WARN_PCT,
|
|
2230
3829
|
DEAD_STRETCH_WARN_MS,
|
|
3830
|
+
DEFAULT_READY,
|
|
2231
3831
|
DOM_CHANGE_PROBE,
|
|
2232
3832
|
DOM_QUIET_MS,
|
|
2233
3833
|
DOM_SETTLE_CAP_MS,
|
|
2234
3834
|
DOWNLOAD_TIMEOUT_S,
|
|
3835
|
+
DRAG_HOLD_MS,
|
|
3836
|
+
DRAG_MODIFIERS,
|
|
2235
3837
|
EXPECT_TIMEOUT_S,
|
|
2236
3838
|
EXPOSURE_ADVICE,
|
|
2237
3839
|
EXPOSURE_MATCHERS_SRC,
|
|
2238
3840
|
EXPOSURE_PROBE,
|
|
2239
3841
|
ExposureLog,
|
|
3842
|
+
IDLE_CALL_MS,
|
|
3843
|
+
KEY_ROUNDTRIP_MS,
|
|
3844
|
+
KEY_SEQUENCE_GAP_MS,
|
|
3845
|
+
MAX_KEYS,
|
|
3846
|
+
MAX_PATH_POINTS,
|
|
3847
|
+
MEDIA_DRAIN_PROBE,
|
|
3848
|
+
MEDIA_WATCH_SCRIPT,
|
|
2240
3849
|
MIN_SKIP_MS,
|
|
3850
|
+
MODIFIERS,
|
|
2241
3851
|
MOTION_TICK_MS,
|
|
3852
|
+
MULTI_CLICK_GAP_MS,
|
|
3853
|
+
NAV_TIMEOUT_MS,
|
|
3854
|
+
NETWORKIDLE_HYDRATION_MS,
|
|
3855
|
+
NOTICE_SRC,
|
|
3856
|
+
NoticeLog,
|
|
2242
3857
|
OUTPUT_MAX_BYTES,
|
|
3858
|
+
OVERRUN_MIN_MS,
|
|
3859
|
+
OVERRUN_MIN_PLANNED_MS,
|
|
3860
|
+
OVERRUN_SHARE,
|
|
2243
3861
|
PAGE_ERROR_LINE_CHARS,
|
|
3862
|
+
PASTE_MAX_BYTES,
|
|
3863
|
+
PASTE_READ_WINDOW_MS,
|
|
3864
|
+
PASTE_TYPES,
|
|
3865
|
+
PASTE_UNREAD_MS,
|
|
3866
|
+
PATH_DEFAULT_MS,
|
|
2244
3867
|
POINTER_MAX_MS,
|
|
2245
3868
|
POINTER_MIN_MS,
|
|
2246
3869
|
POINTER_MS_PER_PX,
|
|
@@ -2248,7 +3871,10 @@ export {
|
|
|
2248
3871
|
PRESS_KEYS,
|
|
2249
3872
|
PRESS_LEAD_MS,
|
|
2250
3873
|
PageErrorLog,
|
|
3874
|
+
PageSoundLog,
|
|
2251
3875
|
QUIET_MS,
|
|
3876
|
+
READY_MODES,
|
|
3877
|
+
READY_SETTLE_CAP_MS,
|
|
2252
3878
|
READ_BEAT_MS,
|
|
2253
3879
|
RESPONSE_MS,
|
|
2254
3880
|
RecordRefusedError,
|
|
@@ -2258,18 +3884,26 @@ export {
|
|
|
2258
3884
|
SCROLL_SETTLE_MS,
|
|
2259
3885
|
SETTLE_CAP_MS,
|
|
2260
3886
|
SETTLE_MS,
|
|
3887
|
+
SETTLE_VERBS,
|
|
2261
3888
|
STEP_GUARDS,
|
|
3889
|
+
STEP_MARGIN_MS,
|
|
2262
3890
|
SetupEnvError,
|
|
2263
3891
|
SetupError,
|
|
3892
|
+
StepBound,
|
|
2264
3893
|
TRAILING_HOLD_MS,
|
|
2265
3894
|
TakeClock,
|
|
3895
|
+
WAIT_MARGIN_MS,
|
|
2266
3896
|
WALL_PROBE,
|
|
2267
3897
|
WallError,
|
|
3898
|
+
appSettled,
|
|
2268
3899
|
askedMs,
|
|
2269
3900
|
base64Bytes,
|
|
3901
|
+
bounded,
|
|
2270
3902
|
buildEncodePage,
|
|
3903
|
+
buttonNumber,
|
|
2271
3904
|
capReached,
|
|
2272
3905
|
cappedLine,
|
|
3906
|
+
chordProblem,
|
|
2273
3907
|
clampWait,
|
|
2274
3908
|
clockMotion,
|
|
2275
3909
|
clockTyping,
|
|
@@ -2283,48 +3917,80 @@ export {
|
|
|
2283
3917
|
extForType,
|
|
2284
3918
|
fetchInPageScript,
|
|
2285
3919
|
fileNameProblem,
|
|
3920
|
+
fitSurfaceToViewport,
|
|
2286
3921
|
formatDurationCap,
|
|
2287
3922
|
guardVerdict,
|
|
2288
3923
|
guardedSteps,
|
|
3924
|
+
hangReason,
|
|
2289
3925
|
headersFor,
|
|
2290
3926
|
holdLeftMs,
|
|
2291
3927
|
hostedRefusals,
|
|
3928
|
+
inlinePoints,
|
|
3929
|
+
inlineScript,
|
|
2292
3930
|
isIdpHost,
|
|
3931
|
+
isKeyName,
|
|
2293
3932
|
isSignInPath,
|
|
2294
3933
|
jpegDims,
|
|
3934
|
+
keySequencePlanMs,
|
|
3935
|
+
loadScriptFiles,
|
|
2295
3936
|
maskInitScript,
|
|
3937
|
+
modifierBits,
|
|
2296
3938
|
nameFromUrl,
|
|
3939
|
+
noticeAdvice,
|
|
3940
|
+
noticeLine,
|
|
2297
3941
|
outputName,
|
|
3942
|
+
overrunLine,
|
|
3943
|
+
overrunOf,
|
|
2298
3944
|
paceLine,
|
|
2299
3945
|
paceReport,
|
|
2300
3946
|
pageErrorLine,
|
|
2301
3947
|
parseHeaders,
|
|
3948
|
+
pasteTypeOf,
|
|
3949
|
+
pathDurationMs,
|
|
2302
3950
|
plainName,
|
|
2303
3951
|
planFreshTake,
|
|
2304
3952
|
pointerTravelMs,
|
|
2305
3953
|
pollCondition,
|
|
3954
|
+
pressSequence,
|
|
2306
3955
|
publicUrl,
|
|
2307
3956
|
readAllBytes,
|
|
2308
3957
|
readCondition,
|
|
2309
3958
|
readMediaSource,
|
|
3959
|
+
readPath,
|
|
3960
|
+
readyLine,
|
|
3961
|
+
readyProblem,
|
|
2310
3962
|
realSleep,
|
|
2311
3963
|
recordTake,
|
|
2312
3964
|
redirectedAway,
|
|
3965
|
+
reloadUntilReady,
|
|
3966
|
+
resolveChord,
|
|
3967
|
+
resolveModifier,
|
|
2313
3968
|
resolveSetupText,
|
|
2314
3969
|
runSetup,
|
|
2315
3970
|
sayCondition,
|
|
2316
3971
|
sayDuration,
|
|
3972
|
+
schedulePath,
|
|
3973
|
+
scriptFiles,
|
|
2317
3974
|
scrollByScript,
|
|
2318
3975
|
scrollTravelMs,
|
|
2319
3976
|
settleMs,
|
|
3977
|
+
settleOn,
|
|
2320
3978
|
settleVerdict,
|
|
2321
3979
|
setupEnvNames,
|
|
3980
|
+
soundSrcForMeta,
|
|
3981
|
+
splitChord,
|
|
2322
3982
|
splitSegmentsAtSkips,
|
|
3983
|
+
stageClipboardScript,
|
|
3984
|
+
stepBoundMs,
|
|
3985
|
+
syntheticPasteScript,
|
|
2323
3986
|
typeOfName,
|
|
3987
|
+
untilReady,
|
|
2324
3988
|
urlSchemeProblem,
|
|
2325
3989
|
validateActions,
|
|
2326
3990
|
validateSetup,
|
|
2327
3991
|
visibleTextScript,
|
|
2328
|
-
wallVerdict
|
|
3992
|
+
wallVerdict,
|
|
3993
|
+
withFittingExport,
|
|
3994
|
+
withPageSound
|
|
2329
3995
|
};
|
|
2330
3996
|
//# sourceMappingURL=index.js.map
|