clipwise 0.1.1 → 0.2.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/index.js CHANGED
@@ -31,12 +31,13 @@ function interpolatePath(from, to, steps) {
31
31
  }
32
32
 
33
33
  // src/core/screenshot.ts
34
- async function getElementCenter(page, selector) {
35
- if (!/^[\w\-#.\[\]="':\s,>+~*()@^$|]+$/.test(selector)) {
34
+ var DEFAULT_ELEMENT_TIMEOUT = 5e3;
35
+ async function getElementCenter(page, selector, timeout) {
36
+ if (/[\x00-\x1f\x7f;`\\{}]/.test(selector) || selector.length === 0) {
36
37
  throw new Error(`Invalid selector: ${selector}`);
37
38
  }
38
39
  const element = page.locator(selector).first();
39
- await element.waitFor({ state: "visible", timeout: 5e3 });
40
+ await element.waitFor({ state: "visible", timeout: timeout ?? DEFAULT_ELEMENT_TIMEOUT });
40
41
  const box = await element.boundingBox();
41
42
  if (!box) {
42
43
  throw new Error(
@@ -77,6 +78,7 @@ var ClipwiseRecorder = class {
77
78
  targetFps = 30;
78
79
  cursorSpeed = "fast";
79
80
  firstContentTimestamp = 0;
81
+ pendingResponsePromises = /* @__PURE__ */ new Map();
80
82
  /**
81
83
  * Launch the browser and create a page with the scenario viewport.
82
84
  */
@@ -161,8 +163,9 @@ var ClipwiseRecorder = class {
161
163
  for (let si = 0; si < scenario.steps.length; si++) {
162
164
  const step = scenario.steps[si];
163
165
  this.currentStepIndex = si;
164
- for (const action of step.actions) {
165
- await this.executeAction(action);
166
+ this.preRegisterResponseListeners(step.actions);
167
+ for (let ai = 0; ai < step.actions.length; ai++) {
168
+ await this.executeAction(step.actions[ai], ai);
166
169
  }
167
170
  if (step.captureDelay > 0) {
168
171
  await this.waitWithRepaints(step.captureDelay);
@@ -228,11 +231,33 @@ var ClipwiseRecorder = class {
228
231
  }
229
232
  }
230
233
  }
234
+ /**
235
+ * Pre-register waitForResponse listeners at the start of each step.
236
+ * This ensures the listener is active before any preceding action
237
+ * (e.g. click) triggers the request, preventing race conditions
238
+ * where the response arrives before the listener is set up.
239
+ */
240
+ preRegisterResponseListeners(actions) {
241
+ this.pendingResponsePromises.clear();
242
+ if (!this.page) return;
243
+ for (let i = 0; i < actions.length; i++) {
244
+ const action = actions[i];
245
+ if (action.action === "waitForResponse") {
246
+ this.pendingResponsePromises.set(
247
+ i,
248
+ this.page.waitForResponse(
249
+ (response) => response.url().includes(action.url) && (action.status === void 0 || response.status() === action.status),
250
+ { timeout: action.timeout }
251
+ )
252
+ );
253
+ }
254
+ }
255
+ }
231
256
  /**
232
257
  * Execute a single action. CDP screencast captures frames continuously
233
258
  * in the background while actions are performed.
234
259
  */
235
- async executeAction(action) {
260
+ async executeAction(action, actionIndex = 0) {
236
261
  if (!this.page) {
237
262
  throw new Error("Page not initialized. Call init() first.");
238
263
  }
@@ -250,7 +275,7 @@ var ClipwiseRecorder = class {
250
275
  break;
251
276
  }
252
277
  case "click": {
253
- const target = await getElementCenter(this.page, action.selector);
278
+ const target = await getElementCenter(this.page, action.selector, action.timeout);
254
279
  await this.moveCursorSmooth(target);
255
280
  this.clickTimeline.push({
256
281
  position: { ...target },
@@ -264,7 +289,8 @@ var ClipwiseRecorder = class {
264
289
  case "type": {
265
290
  const inputTarget = await getElementCenter(
266
291
  this.page,
267
- action.selector
292
+ action.selector,
293
+ action.timeout
268
294
  );
269
295
  await this.moveCursorSmooth(inputTarget);
270
296
  this.clickTimeline.push({
@@ -282,7 +308,7 @@ var ClipwiseRecorder = class {
282
308
  break;
283
309
  }
284
310
  case "scroll": {
285
- const scrollTarget = action.selector ? await getElementCenter(this.page, action.selector) : null;
311
+ const scrollTarget = action.selector ? await getElementCenter(this.page, action.selector, action.timeout) : null;
286
312
  await this.page.evaluate(
287
313
  ({ x, y, smooth, selector }) => {
288
314
  const target = selector ? document.querySelector(selector) : window;
@@ -326,7 +352,8 @@ var ClipwiseRecorder = class {
326
352
  case "hover": {
327
353
  const hoverTarget = await getElementCenter(
328
354
  this.page,
329
- action.selector
355
+ action.selector,
356
+ action.timeout
330
357
  );
331
358
  await this.moveCursorSmooth(hoverTarget);
332
359
  await this.page.hover(action.selector);
@@ -336,6 +363,33 @@ var ClipwiseRecorder = class {
336
363
  await this.waitWithRepaints(100);
337
364
  break;
338
365
  }
366
+ case "waitForSelector": {
367
+ const locator = this.page.locator(action.selector).first();
368
+ await locator.waitFor({ state: action.state, timeout: action.timeout });
369
+ break;
370
+ }
371
+ case "waitForNavigation": {
372
+ await this.page.waitForLoadState(action.waitUntil, { timeout: action.timeout });
373
+ break;
374
+ }
375
+ case "waitForURL": {
376
+ await this.page.waitForURL(action.url, { timeout: action.timeout });
377
+ break;
378
+ }
379
+ case "waitForFunction": {
380
+ await this.page.waitForFunction(action.expression, void 0, {
381
+ polling: action.polling,
382
+ timeout: action.timeout
383
+ });
384
+ break;
385
+ }
386
+ case "waitForResponse": {
387
+ const pending = this.pendingResponsePromises.get(actionIndex);
388
+ if (pending) {
389
+ await pending;
390
+ }
391
+ break;
392
+ }
339
393
  }
340
394
  await this.waitWithRepaints(ACTION_GAP_MS);
341
395
  }
@@ -1438,9 +1492,9 @@ import { readFile as readFile2 } from "fs/promises";
1438
1492
 
1439
1493
  // src/script/types.ts
1440
1494
  import { z } from "zod";
1441
- var SafeSelectorSchema = z.string().regex(
1442
- /^[a-zA-Z0-9\-_#.\[\]="':\s~^$|*,>+()@]+$/,
1443
- "Selector contains invalid characters"
1495
+ var SafeSelectorSchema = z.string().min(1, "Selector must not be empty").regex(
1496
+ /^[^\x00-\x1f\x7f;`\\{}]+$/,
1497
+ "Selector contains invalid characters (control chars, semicolons, backticks, or backslashes are not allowed)"
1444
1498
  );
1445
1499
  var NavigateActionSchema = z.object({
1446
1500
  action: z.literal("navigate"),
@@ -1450,20 +1504,23 @@ var NavigateActionSchema = z.object({
1450
1504
  var ClickActionSchema = z.object({
1451
1505
  action: z.literal("click"),
1452
1506
  selector: SafeSelectorSchema,
1453
- delay: z.number().optional()
1507
+ delay: z.number().optional(),
1508
+ timeout: z.number().min(0).optional()
1454
1509
  });
1455
1510
  var TypeActionSchema = z.object({
1456
1511
  action: z.literal("type"),
1457
1512
  selector: SafeSelectorSchema,
1458
1513
  text: z.string(),
1459
- delay: z.number().default(50)
1514
+ delay: z.number().default(50),
1515
+ timeout: z.number().min(0).optional()
1460
1516
  });
1461
1517
  var ScrollActionSchema = z.object({
1462
1518
  action: z.literal("scroll"),
1463
1519
  selector: SafeSelectorSchema.optional(),
1464
1520
  y: z.number().default(0),
1465
1521
  x: z.number().default(0),
1466
- smooth: z.boolean().default(true)
1522
+ smooth: z.boolean().default(true),
1523
+ timeout: z.number().min(0).optional()
1467
1524
  });
1468
1525
  var WaitActionSchema = z.object({
1469
1526
  action: z.literal("wait"),
@@ -1471,13 +1528,42 @@ var WaitActionSchema = z.object({
1471
1528
  });
1472
1529
  var HoverActionSchema = z.object({
1473
1530
  action: z.literal("hover"),
1474
- selector: SafeSelectorSchema
1531
+ selector: SafeSelectorSchema,
1532
+ timeout: z.number().min(0).optional()
1475
1533
  });
1476
1534
  var ScreenshotActionSchema = z.object({
1477
1535
  action: z.literal("screenshot"),
1478
1536
  name: z.string().optional(),
1479
1537
  fullPage: z.boolean().default(false)
1480
1538
  });
1539
+ var WaitForSelectorActionSchema = z.object({
1540
+ action: z.literal("waitForSelector"),
1541
+ selector: SafeSelectorSchema,
1542
+ state: z.enum(["visible", "attached", "hidden"]).default("visible"),
1543
+ timeout: z.number().min(0).default(15e3)
1544
+ });
1545
+ var WaitForNavigationActionSchema = z.object({
1546
+ action: z.literal("waitForNavigation"),
1547
+ waitUntil: z.enum(["load", "domcontentloaded", "networkidle"]).default("networkidle"),
1548
+ timeout: z.number().min(0).default(15e3)
1549
+ });
1550
+ var WaitForURLActionSchema = z.object({
1551
+ action: z.literal("waitForURL"),
1552
+ url: z.string().min(1),
1553
+ timeout: z.number().min(0).default(15e3)
1554
+ });
1555
+ var WaitForFunctionActionSchema = z.object({
1556
+ action: z.literal("waitForFunction"),
1557
+ expression: z.string().min(1),
1558
+ polling: z.union([z.literal("raf"), z.number().min(0)]).default("raf"),
1559
+ timeout: z.number().min(0).default(3e4)
1560
+ });
1561
+ var WaitForResponseActionSchema = z.object({
1562
+ action: z.literal("waitForResponse"),
1563
+ url: z.string().min(1),
1564
+ status: z.number().min(100).max(599).optional(),
1565
+ timeout: z.number().min(0).default(3e4)
1566
+ });
1481
1567
  var StepActionSchema = z.discriminatedUnion("action", [
1482
1568
  NavigateActionSchema,
1483
1569
  ClickActionSchema,
@@ -1485,7 +1571,12 @@ var StepActionSchema = z.discriminatedUnion("action", [
1485
1571
  ScrollActionSchema,
1486
1572
  WaitActionSchema,
1487
1573
  HoverActionSchema,
1488
- ScreenshotActionSchema
1574
+ ScreenshotActionSchema,
1575
+ WaitForSelectorActionSchema,
1576
+ WaitForNavigationActionSchema,
1577
+ WaitForURLActionSchema,
1578
+ WaitForFunctionActionSchema,
1579
+ WaitForResponseActionSchema
1489
1580
  ]);
1490
1581
  var AutoZoomConfigSchema = z.object({
1491
1582
  followCursor: z.boolean().default(true),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clipwise",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Scriptable cinematic screen recorder for product demos — YAML in, polished MP4 out",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -50,20 +50,20 @@
50
50
  "node": ">=18"
51
51
  },
52
52
  "dependencies": {
53
- "playwright": "^1.50.0",
54
- "yaml": "^2.7.0",
55
- "sharp": "^0.33.0",
53
+ "chalk": "^5.4.0",
56
54
  "commander": "^13.0.0",
55
+ "gifenc": "^1.0.3",
57
56
  "ora": "^8.0.0",
58
- "chalk": "^5.4.0",
59
- "zod": "^3.24.0",
60
- "gifenc": "^1.0.3"
57
+ "playwright": "^1.50.0",
58
+ "sharp": "^0.33.0",
59
+ "yaml": "^2.7.0",
60
+ "zod": "^3.24.0"
61
61
  },
62
62
  "devDependencies": {
63
- "typescript": "^5.7.0",
64
- "tsup": "^8.0.0",
65
- "vitest": "^3.0.0",
66
63
  "@types/node": "^22.0.0",
67
- "eslint": "^9.0.0"
64
+ "eslint": "^10.0.2",
65
+ "tsup": "^8.0.0",
66
+ "typescript": "^5.7.0",
67
+ "vitest": "^3.0.0"
68
68
  }
69
69
  }