@webreel/core 0.1.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.
Files changed (94) hide show
  1. package/README.md +188 -0
  2. package/assets/click-1.mp3 +0 -0
  3. package/assets/click-2.mp3 +0 -0
  4. package/assets/click-3.mp3 +0 -0
  5. package/assets/click-4.mp3 +0 -0
  6. package/assets/key-1.mp3 +0 -0
  7. package/assets/key-2.mp3 +0 -0
  8. package/assets/key-3.mp3 +0 -0
  9. package/assets/key-4.mp3 +0 -0
  10. package/dist/__tests__/actions.test.d.ts +2 -0
  11. package/dist/__tests__/actions.test.d.ts.map +1 -0
  12. package/dist/__tests__/actions.test.js +252 -0
  13. package/dist/__tests__/actions.test.js.map +1 -0
  14. package/dist/__tests__/chrome.test.d.ts +2 -0
  15. package/dist/__tests__/chrome.test.d.ts.map +1 -0
  16. package/dist/__tests__/chrome.test.js +29 -0
  17. package/dist/__tests__/chrome.test.js.map +1 -0
  18. package/dist/__tests__/cursor-motion.test.d.ts +2 -0
  19. package/dist/__tests__/cursor-motion.test.d.ts.map +1 -0
  20. package/dist/__tests__/cursor-motion.test.js +39 -0
  21. package/dist/__tests__/cursor-motion.test.js.map +1 -0
  22. package/dist/__tests__/ffmpeg.test.d.ts +2 -0
  23. package/dist/__tests__/ffmpeg.test.d.ts.map +1 -0
  24. package/dist/__tests__/ffmpeg.test.js +90 -0
  25. package/dist/__tests__/ffmpeg.test.js.map +1 -0
  26. package/dist/__tests__/media.test.d.ts +2 -0
  27. package/dist/__tests__/media.test.d.ts.map +1 -0
  28. package/dist/__tests__/media.test.js +98 -0
  29. package/dist/__tests__/media.test.js.map +1 -0
  30. package/dist/__tests__/overlays.test.d.ts +2 -0
  31. package/dist/__tests__/overlays.test.d.ts.map +1 -0
  32. package/dist/__tests__/overlays.test.js +109 -0
  33. package/dist/__tests__/overlays.test.js.map +1 -0
  34. package/dist/__tests__/recording-context.test.d.ts +2 -0
  35. package/dist/__tests__/recording-context.test.d.ts.map +1 -0
  36. package/dist/__tests__/recording-context.test.js +46 -0
  37. package/dist/__tests__/recording-context.test.js.map +1 -0
  38. package/dist/__tests__/timeline.test.d.ts +2 -0
  39. package/dist/__tests__/timeline.test.d.ts.map +1 -0
  40. package/dist/__tests__/timeline.test.js +88 -0
  41. package/dist/__tests__/timeline.test.js.map +1 -0
  42. package/dist/actions.d.ts +65 -0
  43. package/dist/actions.d.ts.map +1 -0
  44. package/dist/actions.js +729 -0
  45. package/dist/actions.js.map +1 -0
  46. package/dist/cdp.d.ts +3 -0
  47. package/dist/cdp.d.ts.map +1 -0
  48. package/dist/cdp.js +5 -0
  49. package/dist/cdp.js.map +1 -0
  50. package/dist/chrome.d.ts +12 -0
  51. package/dist/chrome.d.ts.map +1 -0
  52. package/dist/chrome.js +241 -0
  53. package/dist/chrome.js.map +1 -0
  54. package/dist/compositor.d.ts +8 -0
  55. package/dist/compositor.d.ts.map +1 -0
  56. package/dist/compositor.js +224 -0
  57. package/dist/compositor.js.map +1 -0
  58. package/dist/cursor-motion.d.ts +17 -0
  59. package/dist/cursor-motion.d.ts.map +1 -0
  60. package/dist/cursor-motion.js +138 -0
  61. package/dist/cursor-motion.js.map +1 -0
  62. package/dist/download.d.ts +6 -0
  63. package/dist/download.d.ts.map +1 -0
  64. package/dist/download.js +78 -0
  65. package/dist/download.js.map +1 -0
  66. package/dist/ffmpeg.d.ts +5 -0
  67. package/dist/ffmpeg.d.ts.map +1 -0
  68. package/dist/ffmpeg.js +106 -0
  69. package/dist/ffmpeg.js.map +1 -0
  70. package/dist/index.d.ts +12 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +11 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/media.d.ts +23 -0
  75. package/dist/media.d.ts.map +1 -0
  76. package/dist/media.js +155 -0
  77. package/dist/media.js.map +1 -0
  78. package/dist/overlays.d.ts +21 -0
  79. package/dist/overlays.d.ts.map +1 -0
  80. package/dist/overlays.js +97 -0
  81. package/dist/overlays.js.map +1 -0
  82. package/dist/recorder.d.ts +41 -0
  83. package/dist/recorder.d.ts.map +1 -0
  84. package/dist/recorder.js +223 -0
  85. package/dist/recorder.js.map +1 -0
  86. package/dist/timeline.d.ts +82 -0
  87. package/dist/timeline.d.ts.map +1 -0
  88. package/dist/timeline.js +140 -0
  89. package/dist/timeline.js.map +1 -0
  90. package/dist/types.d.ts +90 -0
  91. package/dist/types.d.ts.map +1 -0
  92. package/dist/types.js +16 -0
  93. package/dist/types.js.map +1 -0
  94. package/package.json +51 -0
@@ -0,0 +1,729 @@
1
+ import { writeFileSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { OFFSCREEN_MARGIN, DEFAULT_VIEWPORT_SIZE } from "./types.js";
4
+ import { showKeys, hideKeys } from "./overlays.js";
5
+ import { animateMoveTo, computeEasedPath, computeDragTiming } from "./cursor-motion.js";
6
+ function getTimeline(ctx) {
7
+ const tl = ctx.timeline;
8
+ if (!tl)
9
+ throw new Error("Expected timeline to be set during recording");
10
+ return tl;
11
+ }
12
+ export class RecordingContext {
13
+ _mode = "preview";
14
+ _timeline = null;
15
+ _recorder = null;
16
+ _cursorX = -OFFSCREEN_MARGIN;
17
+ _cursorY = -OFFSCREEN_MARGIN;
18
+ _clickDwell;
19
+ get mode() {
20
+ return this._mode;
21
+ }
22
+ setMode(mode) {
23
+ this._mode = mode;
24
+ }
25
+ get timeline() {
26
+ return this._timeline;
27
+ }
28
+ setTimeline(timeline) {
29
+ this._timeline = timeline;
30
+ }
31
+ setRecorder(recorder) {
32
+ this._recorder = recorder;
33
+ }
34
+ get cursorX() {
35
+ return this._cursorX;
36
+ }
37
+ get cursorY() {
38
+ return this._cursorY;
39
+ }
40
+ setCursorPosition(x, y) {
41
+ this._cursorX = x;
42
+ this._cursorY = y;
43
+ }
44
+ get isRecording() {
45
+ return this._mode === "record" && this._timeline !== null;
46
+ }
47
+ resetCursorPosition(cssWidth, cssHeight) {
48
+ const w = cssWidth ?? DEFAULT_VIEWPORT_SIZE;
49
+ const h = cssHeight ?? DEFAULT_VIEWPORT_SIZE;
50
+ const edge = Math.floor(Math.random() * 4);
51
+ const along = 0.2 + Math.random() * 0.6;
52
+ switch (edge) {
53
+ case 0:
54
+ this._cursorX = along * w;
55
+ this._cursorY = -OFFSCREEN_MARGIN;
56
+ break;
57
+ case 1:
58
+ this._cursorX = w + OFFSCREEN_MARGIN;
59
+ this._cursorY = along * h;
60
+ break;
61
+ case 2:
62
+ this._cursorX = along * w;
63
+ this._cursorY = h + OFFSCREEN_MARGIN;
64
+ break;
65
+ case 3:
66
+ this._cursorX = -OFFSCREEN_MARGIN;
67
+ this._cursorY = along * h;
68
+ break;
69
+ }
70
+ }
71
+ setClickDwell(ms) {
72
+ this._clickDwell = ms;
73
+ }
74
+ getClickDwellMs() {
75
+ if (this._clickDwell !== undefined)
76
+ return this._clickDwell;
77
+ return 80 + Math.random() * 100;
78
+ }
79
+ getCursorPosition() {
80
+ return { x: this._cursorX, y: this._cursorY };
81
+ }
82
+ markEvent(type) {
83
+ if (this.isRecording) {
84
+ this._timeline?.addEvent(type);
85
+ }
86
+ this._recorder?.addEvent(type);
87
+ }
88
+ }
89
+ export function modKey() {
90
+ return process.platform === "darwin" ? "cmd" : "ctrl";
91
+ }
92
+ export async function pause(ms = 1200) {
93
+ await new Promise((r) => setTimeout(r, ms));
94
+ }
95
+ export async function navigate(client, url) {
96
+ const loadPromise = client.Page.loadEventFired();
97
+ await client.Page.navigate({ url });
98
+ await loadPromise;
99
+ }
100
+ export async function waitForSelector(client, selector, timeoutMs = 30000) {
101
+ const start = Date.now();
102
+ while (Date.now() - start < timeoutMs) {
103
+ const { result } = await client.Runtime.evaluate({
104
+ expression: `!!document.querySelector(${JSON.stringify(selector)})`,
105
+ returnByValue: true,
106
+ });
107
+ if (result.value === true)
108
+ return;
109
+ await pause(200);
110
+ }
111
+ throw new Error(`Timeout waiting for selector "${selector}" after ${timeoutMs}ms`);
112
+ }
113
+ export async function waitForText(client, text, within, timeoutMs = 30000) {
114
+ const start = Date.now();
115
+ while (Date.now() - start < timeoutMs) {
116
+ if (await findElementByText(client, text, within))
117
+ return;
118
+ await pause(200);
119
+ }
120
+ throw new Error(`Timeout waiting for text "${text}" after ${timeoutMs}ms`);
121
+ }
122
+ export async function findElementByText(client, text, within) {
123
+ const { result } = await client.Runtime.evaluate({
124
+ expression: `(() => {
125
+ const scope = ${within ? `document.querySelector(${JSON.stringify(within)})` : "document.body"};
126
+ if (!scope) return null;
127
+ const target = ${JSON.stringify(text)};
128
+ let best = null;
129
+ let bestArea = Infinity;
130
+ const walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT);
131
+ while (walker.nextNode()) {
132
+ const node = walker.currentNode;
133
+ if (!node.textContent || !node.textContent.includes(target)) continue;
134
+ let el = node.parentElement;
135
+ while (el && el !== scope) {
136
+ if (el.textContent && el.textContent.includes(target)) {
137
+ const r = el.getBoundingClientRect();
138
+ if (r.width > 0 && r.height > 0) {
139
+ const area = r.width * r.height;
140
+ if (area < bestArea) {
141
+ bestArea = area;
142
+ best = { x: r.x, y: r.y, width: r.width, height: r.height };
143
+ }
144
+ }
145
+ }
146
+ el = el.parentElement;
147
+ }
148
+ }
149
+ return best;
150
+ })()`,
151
+ returnByValue: true,
152
+ });
153
+ return result.value ?? null;
154
+ }
155
+ export async function findElementBySelector(client, selector, within) {
156
+ const { result } = await client.Runtime.evaluate({
157
+ expression: `(() => {
158
+ const scope = ${within ? `document.querySelector(${JSON.stringify(within)})` : "document"};
159
+ if (!scope) return null;
160
+ const el = scope.querySelector(${JSON.stringify(selector)});
161
+ if (!el) return null;
162
+ const r = el.getBoundingClientRect();
163
+ return { x: r.x, y: r.y, width: r.width, height: r.height };
164
+ })()`,
165
+ returnByValue: true,
166
+ });
167
+ return result.value ?? null;
168
+ }
169
+ export async function moveCursorTo(ctx, client, x, y) {
170
+ await animateMoveTo(ctx, client, ctx.cursorX, ctx.cursorY, x, y);
171
+ ctx.setCursorPosition(x, y);
172
+ await pause(40 + Math.random() * 30);
173
+ }
174
+ async function cursorDown(ctx, client) {
175
+ if (ctx.isRecording) {
176
+ getTimeline(ctx).setCursorScale(0.75);
177
+ return;
178
+ }
179
+ await client.Runtime.evaluate({
180
+ expression: `(() => {
181
+ const el = document.getElementById("__demo-cursor");
182
+ if (!el) return;
183
+ el.getAnimations().forEach(a => a.cancel());
184
+ const cx = el.dataset.cx || "0";
185
+ const cy = el.dataset.cy || "0";
186
+ el.style.transform = "translate(" + cx + "px," + cy + "px) scale(0.75)";
187
+ el.style.transition = "transform 0.1s ease";
188
+ })()`,
189
+ });
190
+ }
191
+ async function cursorUp(ctx, client) {
192
+ if (ctx.isRecording) {
193
+ getTimeline(ctx).setCursorScale(1);
194
+ return;
195
+ }
196
+ await client.Runtime.evaluate({
197
+ expression: `(() => {
198
+ const el = document.getElementById("__demo-cursor");
199
+ if (!el) return;
200
+ el.getAnimations().forEach(a => a.cancel());
201
+ const cx = el.dataset.cx || "0";
202
+ const cy = el.dataset.cy || "0";
203
+ el.style.transform = "translate(" + cx + "px," + cy + "px) scale(1)";
204
+ el.style.transition = "transform 0.1s ease";
205
+ setTimeout(() => { el.style.transition = ""; }, 120);
206
+ })()`,
207
+ });
208
+ }
209
+ export function resolveMod(mod) {
210
+ if (mod.toLowerCase() === "mod") {
211
+ return process.platform === "darwin" ? "cmd" : "ctrl";
212
+ }
213
+ return mod;
214
+ }
215
+ export function modifierFlag(mod) {
216
+ switch (resolveMod(mod).toLowerCase()) {
217
+ case "alt":
218
+ return 1;
219
+ case "ctrl":
220
+ case "control":
221
+ return 2;
222
+ case "cmd":
223
+ case "meta":
224
+ return 4;
225
+ case "shift":
226
+ return 8;
227
+ default:
228
+ return 0;
229
+ }
230
+ }
231
+ export function modifiersToFlag(mods) {
232
+ if (!mods)
233
+ return 0;
234
+ let flag = 0;
235
+ for (const m of mods)
236
+ flag |= modifierFlag(m.toLowerCase());
237
+ return flag;
238
+ }
239
+ export function modLabel(mod) {
240
+ const m = resolveMod(mod).toLowerCase();
241
+ if (m === "cmd" || m === "meta")
242
+ return "\u2318";
243
+ if (m === "ctrl" || m === "control")
244
+ return "Ctrl";
245
+ if (m === "shift")
246
+ return "\u21E7";
247
+ if (m === "alt")
248
+ return "\u2325";
249
+ return mod;
250
+ }
251
+ export function modKeyInfo(mod) {
252
+ switch (resolveMod(mod).toLowerCase()) {
253
+ case "cmd":
254
+ case "meta":
255
+ return { key: "Meta", code: "MetaLeft", keyCode: 91, location: 1 };
256
+ case "ctrl":
257
+ case "control":
258
+ return { key: "Control", code: "ControlLeft", keyCode: 17, location: 1 };
259
+ case "shift":
260
+ return { key: "Shift", code: "ShiftLeft", keyCode: 16, location: 1 };
261
+ case "alt":
262
+ return { key: "Alt", code: "AltLeft", keyCode: 18, location: 1 };
263
+ default:
264
+ return null;
265
+ }
266
+ }
267
+ export async function clickAt(ctx, client, x, y, modifiers) {
268
+ const flag = modifiersToFlag(modifiers);
269
+ const labels = [];
270
+ if (modifiers?.length) {
271
+ for (const m of modifiers)
272
+ labels.push(modLabel(m));
273
+ if (ctx.isRecording) {
274
+ getTimeline(ctx).showHud(labels);
275
+ }
276
+ else {
277
+ await showKeys(client, labels);
278
+ }
279
+ }
280
+ await moveCursorTo(ctx, client, x, y);
281
+ const dwellMs = ctx.getClickDwellMs();
282
+ if (dwellMs > 0)
283
+ await pause(dwellMs);
284
+ if (modifiers?.length) {
285
+ for (const mod of modifiers) {
286
+ const info = modKeyInfo(mod);
287
+ if (info) {
288
+ await client.Input.dispatchKeyEvent({
289
+ type: "keyDown",
290
+ key: info.key,
291
+ code: info.code,
292
+ windowsVirtualKeyCode: info.keyCode,
293
+ modifiers: flag,
294
+ });
295
+ }
296
+ }
297
+ await pause(30);
298
+ }
299
+ await client.Input.dispatchMouseEvent({
300
+ type: "mouseMoved",
301
+ x,
302
+ y,
303
+ modifiers: flag,
304
+ });
305
+ await cursorDown(ctx, client);
306
+ await pause(100);
307
+ ctx.markEvent("click");
308
+ const metaFlag = (flag & 4) !== 0;
309
+ const ctrlFlag = (flag & 2) !== 0;
310
+ const shiftFlag = (flag & 8) !== 0;
311
+ const altFlag = (flag & 1) !== 0;
312
+ // Prevent CDP-triggered mouse events from reaching app JS handlers, while
313
+ // preserving native browser behaviors (focus changes, selection clearing,
314
+ // caret placement). CDP's Input.dispatchMouseEvent is unreliable for JS
315
+ // event delivery and often drops modifier flags. We block propagation to
316
+ // app code but skip preventDefault so Chrome still performs default actions.
317
+ await client.Runtime.evaluate({
318
+ expression: `(() => {
319
+ var events = ['pointerdown','mousedown','pointerup','mouseup','click'];
320
+ events.forEach(function(evt) {
321
+ document.addEventListener(evt, function __wrBlock(e) {
322
+ if (e.__wrSynthetic) return;
323
+ e.stopImmediatePropagation();
324
+ document.removeEventListener(evt, __wrBlock, true);
325
+ }, true);
326
+ });
327
+ })()`,
328
+ });
329
+ // CDP mousePressed/mouseReleased for native focus management (the events
330
+ // themselves are suppressed above, but Chrome still updates internal focus)
331
+ await client.Input.dispatchMouseEvent({
332
+ type: "mousePressed",
333
+ x,
334
+ y,
335
+ button: "left",
336
+ clickCount: 1,
337
+ modifiers: flag,
338
+ });
339
+ await pause(50);
340
+ await client.Input.dispatchMouseEvent({
341
+ type: "mouseReleased",
342
+ x,
343
+ y,
344
+ button: "left",
345
+ clickCount: 1,
346
+ modifiers: flag,
347
+ });
348
+ await pause(30);
349
+ // Dispatch the full mouse event chain via JS with correct modifier flags
350
+ await client.Runtime.evaluate({
351
+ expression: `(() => {
352
+ var el = document.elementFromPoint(${x}, ${y});
353
+ if (!el) return;
354
+ if (!${shiftFlag}) {
355
+ var sel = window.getSelection();
356
+ if (sel && sel.rangeCount) {
357
+ var cp = document.caretPositionFromPoint(${x}, ${y});
358
+ if (cp) {
359
+ var cr = document.createRange();
360
+ cr.setStart(cp.offsetNode, cp.offset);
361
+ cr.collapse(true);
362
+ sel.removeAllRanges(); sel.addRange(cr); sel.collapseToStart();
363
+ } else {
364
+ sel.removeAllRanges();
365
+ }
366
+ }
367
+ }
368
+ var opts = {
369
+ bubbles: true, cancelable: true, clientX: ${x}, clientY: ${y},
370
+ metaKey: ${metaFlag}, ctrlKey: ${ctrlFlag}, shiftKey: ${shiftFlag}, altKey: ${altFlag},
371
+ button: 0, buttons: 1
372
+ };
373
+ function fire(Ctor, type) {
374
+ var ev = new Ctor(type, opts);
375
+ ev.__wrSynthetic = true;
376
+ el.dispatchEvent(ev);
377
+ }
378
+ fire(PointerEvent, 'pointerdown');
379
+ fire(MouseEvent, 'mousedown');
380
+ fire(PointerEvent, 'pointerup');
381
+ fire(MouseEvent, 'mouseup');
382
+ fire(MouseEvent, 'click');
383
+ })()`,
384
+ });
385
+ await pause(30);
386
+ await cursorUp(ctx, client);
387
+ if (modifiers?.length) {
388
+ for (const mod of modifiers) {
389
+ const info = modKeyInfo(mod);
390
+ if (info) {
391
+ await client.Input.dispatchKeyEvent({
392
+ type: "keyUp",
393
+ key: info.key,
394
+ code: info.code,
395
+ windowsVirtualKeyCode: info.keyCode,
396
+ });
397
+ }
398
+ }
399
+ await pause(400);
400
+ if (ctx.isRecording) {
401
+ getTimeline(ctx).hideHud();
402
+ }
403
+ else {
404
+ await hideKeys(client);
405
+ }
406
+ }
407
+ }
408
+ export const KEY_CODES = {
409
+ Delete: { code: "Delete", keyCode: 46 },
410
+ Backspace: { code: "Backspace", keyCode: 8 },
411
+ Escape: { code: "Escape", keyCode: 27 },
412
+ Enter: { code: "Enter", keyCode: 13 },
413
+ Tab: { code: "Tab", keyCode: 9 },
414
+ ArrowUp: { code: "ArrowUp", keyCode: 38 },
415
+ ArrowDown: { code: "ArrowDown", keyCode: 40 },
416
+ ArrowLeft: { code: "ArrowLeft", keyCode: 37 },
417
+ ArrowRight: { code: "ArrowRight", keyCode: 39 },
418
+ a: { code: "KeyA", keyCode: 65 },
419
+ b: { code: "KeyB", keyCode: 66 },
420
+ c: { code: "KeyC", keyCode: 67 },
421
+ d: { code: "KeyD", keyCode: 68 },
422
+ v: { code: "KeyV", keyCode: 86 },
423
+ x: { code: "KeyX", keyCode: 88 },
424
+ z: { code: "KeyZ", keyCode: 90 },
425
+ };
426
+ export const SHORTCUT_COMMANDS = {
427
+ "meta+a": ["selectAll"],
428
+ "meta+c": ["copy"],
429
+ "meta+x": ["cut"],
430
+ "meta+v": ["paste"],
431
+ "meta+z": ["undo"],
432
+ "meta+shift+z": ["redo"],
433
+ "ctrl+a": ["selectAll"],
434
+ "ctrl+c": ["copy"],
435
+ "ctrl+x": ["cut"],
436
+ "ctrl+v": ["paste"],
437
+ "ctrl+z": ["undo"],
438
+ "ctrl+shift+z": ["redo"],
439
+ };
440
+ export function resolveCommands(modifiers, mainKey) {
441
+ const parts = modifiers
442
+ .map((m) => {
443
+ const lower = resolveMod(m).toLowerCase();
444
+ if (lower === "cmd" || lower === "meta")
445
+ return "meta";
446
+ return lower;
447
+ })
448
+ .sort();
449
+ parts.push(mainKey.toLowerCase());
450
+ return SHORTCUT_COMMANDS[parts.join("+")];
451
+ }
452
+ export async function pressKey(ctx, client, key, label) {
453
+ const parts = key.split("+");
454
+ const modifiers = [];
455
+ let mainKey = "";
456
+ for (const p of parts) {
457
+ const lower = p.toLowerCase();
458
+ if (["cmd", "ctrl", "meta", "control", "shift", "alt", "mod"].includes(lower)) {
459
+ modifiers.push(p);
460
+ }
461
+ else {
462
+ mainKey = p;
463
+ }
464
+ }
465
+ if (!mainKey) {
466
+ throw new Error(`pressKey requires a non-modifier key, got "${key}" (modifiers only)`);
467
+ }
468
+ const displayParts = [];
469
+ for (const m of modifiers)
470
+ displayParts.push(modLabel(m));
471
+ displayParts.push(label ?? mainKey);
472
+ if (ctx.isRecording) {
473
+ getTimeline(ctx).showHud(displayParts);
474
+ }
475
+ else {
476
+ await showKeys(client, displayParts);
477
+ }
478
+ ctx.markEvent("key");
479
+ const flag = modifiersToFlag(modifiers);
480
+ const keyInfo = KEY_CODES[mainKey] ?? KEY_CODES[mainKey.toLowerCase()];
481
+ const code = keyInfo?.code ?? `Key${mainKey.toUpperCase()}`;
482
+ const keyCode = keyInfo?.keyCode ?? mainKey.toUpperCase().charCodeAt(0);
483
+ const commands = resolveCommands(modifiers, mainKey);
484
+ await client.Input.dispatchKeyEvent({
485
+ type: "keyDown",
486
+ key: mainKey,
487
+ code,
488
+ windowsVirtualKeyCode: keyCode,
489
+ modifiers: flag,
490
+ commands,
491
+ });
492
+ await client.Input.dispatchKeyEvent({
493
+ type: "keyUp",
494
+ key: mainKey,
495
+ code,
496
+ windowsVirtualKeyCode: keyCode,
497
+ modifiers: flag,
498
+ });
499
+ await pause(800);
500
+ if (ctx.isRecording) {
501
+ getTimeline(ctx).hideHud();
502
+ }
503
+ else {
504
+ await hideKeys(client);
505
+ }
506
+ }
507
+ export const CHAR_CODES = {
508
+ " ": { code: "Space", keyCode: 32 },
509
+ "0": { code: "Digit0", keyCode: 48 },
510
+ "1": { code: "Digit1", keyCode: 49 },
511
+ "2": { code: "Digit2", keyCode: 50 },
512
+ "3": { code: "Digit3", keyCode: 51 },
513
+ "4": { code: "Digit4", keyCode: 52 },
514
+ "5": { code: "Digit5", keyCode: 53 },
515
+ "6": { code: "Digit6", keyCode: 54 },
516
+ "7": { code: "Digit7", keyCode: 55 },
517
+ "8": { code: "Digit8", keyCode: 56 },
518
+ "9": { code: "Digit9", keyCode: 57 },
519
+ ";": { code: "Semicolon", keyCode: 186 },
520
+ "=": { code: "Equal", keyCode: 187 },
521
+ ",": { code: "Comma", keyCode: 188 },
522
+ "-": { code: "Minus", keyCode: 189 },
523
+ ".": { code: "Period", keyCode: 190 },
524
+ "/": { code: "Slash", keyCode: 191 },
525
+ "`": { code: "Backquote", keyCode: 192 },
526
+ "[": { code: "BracketLeft", keyCode: 219 },
527
+ "\\": { code: "Backslash", keyCode: 220 },
528
+ "]": { code: "BracketRight", keyCode: 221 },
529
+ "'": { code: "Quote", keyCode: 222 },
530
+ };
531
+ function humanDelay(base) {
532
+ const jitter = base * (0.6 + Math.random() * 0.9);
533
+ if (Math.random() < 0.12)
534
+ return jitter + base * 1.5 + Math.random() * base * 2;
535
+ return jitter;
536
+ }
537
+ export async function typeText(ctx, client, text, delayMs = 120) {
538
+ for (let i = 0; i < text.length; i++) {
539
+ const char = text[i];
540
+ const charInfo = CHAR_CODES[char];
541
+ const isLetter = /^[a-zA-Z]$/.test(char);
542
+ let code;
543
+ let keyCode;
544
+ if (charInfo) {
545
+ code = charInfo.code;
546
+ keyCode = charInfo.keyCode;
547
+ }
548
+ else if (isLetter) {
549
+ code = `Key${char.toUpperCase()}`;
550
+ keyCode = char.toUpperCase().charCodeAt(0);
551
+ }
552
+ else {
553
+ code = "";
554
+ keyCode = 0;
555
+ }
556
+ await client.Input.dispatchKeyEvent({
557
+ type: "rawKeyDown",
558
+ key: char,
559
+ code,
560
+ windowsVirtualKeyCode: keyCode,
561
+ });
562
+ await client.Input.dispatchKeyEvent({
563
+ type: "char",
564
+ key: char,
565
+ text: char,
566
+ });
567
+ await client.Input.dispatchKeyEvent({
568
+ type: "keyUp",
569
+ key: char,
570
+ code,
571
+ windowsVirtualKeyCode: keyCode,
572
+ });
573
+ ctx.markEvent("key");
574
+ if (ctx.isRecording) {
575
+ const waitStart = Date.now();
576
+ await getTimeline(ctx).waitForNextTick();
577
+ const tickElapsed = Date.now() - waitStart;
578
+ const delay = humanDelay(delayMs);
579
+ if (delay > tickElapsed) {
580
+ await pause(delay - tickElapsed);
581
+ }
582
+ }
583
+ else {
584
+ await pause(humanDelay(delayMs));
585
+ }
586
+ }
587
+ }
588
+ export async function dragFromTo(ctx, client, fromBox, toBox) {
589
+ const fx = fromBox.x + fromBox.width / 2;
590
+ const fy = fromBox.y + fromBox.height / 2;
591
+ const tx = toBox.x + toBox.width / 2;
592
+ const ty = toBox.y + toBox.height / 2;
593
+ const isRecording = ctx.isRecording;
594
+ await moveCursorTo(ctx, client, fx, fy);
595
+ await cursorDown(ctx, client);
596
+ ctx.markEvent("click");
597
+ await client.Runtime.evaluate({
598
+ expression: `(() => {
599
+ document.body.style.userSelect = "none";
600
+ document.body.style.webkitUserSelect = "none";
601
+ window.getSelection()?.removeAllRanges();
602
+ const el = document.elementFromPoint(${fx}, ${fy});
603
+ const src = el?.closest("[draggable]") || el;
604
+ if (src) {
605
+ const ghost = src.cloneNode(true);
606
+ ghost.id = "__demo-drag-ghost";
607
+ const rect = src.getBoundingClientRect();
608
+ ghost.style.cssText = [
609
+ "position:fixed",
610
+ "z-index:999998",
611
+ "pointer-events:none",
612
+ "width:" + rect.width + "px",
613
+ "height:" + rect.height + "px",
614
+ "left:" + rect.left + "px",
615
+ "top:" + rect.top + "px",
616
+ "opacity:0.85",
617
+ "transform:rotate(2deg) scale(1.03)",
618
+ "box-shadow:0 12px 32px rgba(0,0,0,0.4)",
619
+ "transition:opacity 0.15s ease",
620
+ ].join(";");
621
+ document.body.appendChild(ghost);
622
+ }
623
+ })()`,
624
+ });
625
+ await client.Input.dispatchMouseEvent({
626
+ type: "mousePressed",
627
+ x: fx,
628
+ y: fy,
629
+ button: "left",
630
+ clickCount: 1,
631
+ });
632
+ await client.Runtime.evaluate({
633
+ expression: `(() => {
634
+ const el = document.elementFromPoint(${fx}, ${fy});
635
+ if (el) el.dispatchEvent(new DragEvent("dragstart", {
636
+ bubbles: true, cancelable: true, clientX: ${fx}, clientY: ${fy},
637
+ dataTransfer: new DataTransfer(),
638
+ }));
639
+ })()`,
640
+ });
641
+ await pause(150);
642
+ const dist = Math.sqrt((tx - fx) * (tx - fx) + (ty - fy) * (ty - fy));
643
+ const { steps, delayMs } = computeDragTiming(dist);
644
+ const waypoints = computeEasedPath(fx, fy, tx, ty, steps);
645
+ for (const wp of waypoints) {
646
+ await client.Input.dispatchMouseEvent({
647
+ type: "mouseMoved",
648
+ x: wp.x,
649
+ y: wp.y,
650
+ button: "left",
651
+ buttons: 1,
652
+ });
653
+ await client.Runtime.evaluate({
654
+ expression: `(() => {
655
+ const el = document.elementFromPoint(${wp.x}, ${wp.y});
656
+ if (el) el.dispatchEvent(new DragEvent("dragover", {
657
+ bubbles: true, cancelable: true, clientX: ${wp.x}, clientY: ${wp.y},
658
+ dataTransfer: new DataTransfer(),
659
+ }));
660
+ const ghost = document.getElementById("__demo-drag-ghost");
661
+ if (ghost) {
662
+ ghost.style.left = "${wp.x - fromBox.width / 2}px";
663
+ ghost.style.top = "${wp.y - fromBox.height / 2}px";
664
+ }
665
+ ${isRecording
666
+ ? ""
667
+ : `
668
+ const cursor = document.getElementById("__demo-cursor");
669
+ if (cursor) {
670
+ cursor.getAnimations().forEach(a => a.cancel());
671
+ cursor.style.transform = "translate(${wp.x}px,${wp.y}px)";
672
+ cursor.dataset.cx = "${wp.x}";
673
+ cursor.dataset.cy = "${wp.y}";
674
+ }`}
675
+ })()`,
676
+ });
677
+ if (isRecording) {
678
+ getTimeline(ctx).setCursorPath([{ x: wp.x, y: wp.y }]);
679
+ }
680
+ await pause(delayMs + (Math.random() - 0.5) * 8);
681
+ }
682
+ await client.Runtime.evaluate({
683
+ expression: `(() => {
684
+ const el = document.elementFromPoint(${tx}, ${ty});
685
+ if (el) {
686
+ el.dispatchEvent(new DragEvent("dragover", {
687
+ bubbles: true, cancelable: true, clientX: ${tx}, clientY: ${ty},
688
+ dataTransfer: new DataTransfer(),
689
+ }));
690
+ el.dispatchEvent(new DragEvent("drop", {
691
+ bubbles: true, cancelable: true, clientX: ${tx}, clientY: ${ty},
692
+ dataTransfer: new DataTransfer(),
693
+ }));
694
+ }
695
+ })()`,
696
+ });
697
+ await pause(50);
698
+ await client.Runtime.evaluate({
699
+ expression: `(() => {
700
+ const el = document.elementFromPoint(${fx}, ${fy});
701
+ if (el) el.dispatchEvent(new DragEvent("dragend", {
702
+ bubbles: true, cancelable: true, clientX: ${tx}, clientY: ${ty},
703
+ dataTransfer: new DataTransfer(),
704
+ }));
705
+ const ghost = document.getElementById("__demo-drag-ghost");
706
+ if (ghost) {
707
+ ghost.style.opacity = "0";
708
+ setTimeout(() => ghost.remove(), 200);
709
+ }
710
+ document.body.style.userSelect = "";
711
+ document.body.style.webkitUserSelect = "";
712
+ })()`,
713
+ });
714
+ await client.Input.dispatchMouseEvent({
715
+ type: "mouseReleased",
716
+ x: tx,
717
+ y: ty,
718
+ button: "left",
719
+ clickCount: 1,
720
+ });
721
+ ctx.setCursorPosition(tx, ty);
722
+ await cursorUp(ctx, client);
723
+ }
724
+ export async function captureScreenshot(client, outputPath) {
725
+ const { data } = await client.Page.captureScreenshot({ format: "png" });
726
+ mkdirSync(dirname(outputPath), { recursive: true });
727
+ writeFileSync(outputPath, Buffer.from(data, "base64"));
728
+ }
729
+ //# sourceMappingURL=actions.js.map