clipwise 0.1.2 → 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/README.ko.md CHANGED
@@ -114,15 +114,57 @@ steps:
114
114
 
115
115
  ### 액션
116
116
 
117
- | 액션 | 파라미터 | 설명 |
118
- |------|---------|------|
119
- | `navigate` | `url`, `waitUntil?` | URL로 이동 |
120
- | `click` | `selector`, `delay?` | 요소 클릭 |
121
- | `type` | `selector`, `text`, `delay?` | 텍스트 입력 (한 글자씩) |
122
- | `hover` | `selector` | 요소에 마우스 올리기 |
123
- | `scroll` | `y?`, `x?`, `selector?`, `smooth?` | 스크롤 |
124
- | `wait` | `duration` | 대기 (ms) |
125
- | `screenshot` | `name?`, `fullPage?` | 캡처 마커 |
117
+ #### 기본 액션
118
+
119
+ | 액션 | 파라미터 | 기본값 | 설명 |
120
+ |------|---------|--------|------|
121
+ | `navigate` | `url`, `waitUntil?` | `waitUntil: "networkidle"` | URL로 이동 |
122
+ | `click` | `selector`, `delay?`, `timeout?` | | 요소 클릭 |
123
+ | `type` | `selector`, `text`, `delay?`, `timeout?` | `delay: 50` | 텍스트 입력 (한 글자씩) |
124
+ | `hover` | `selector`, `timeout?` | | 요소에 마우스 올리기 |
125
+ | `scroll` | `y?`, `x?`, `selector?`, `smooth?`, `timeout?` | `y: 0`, `x: 0`, `smooth: true` | 스크롤 |
126
+ | `wait` | `duration` | | 대기 (ms) |
127
+ | `screenshot` | `name?`, `fullPage?` | `fullPage: false` | 캡처 마커 |
128
+
129
+ #### 비동기 대기 액션
130
+
131
+ | 액션 | 파라미터 | 기본값 | 설명 |
132
+ |------|---------|--------|------|
133
+ | `waitForSelector` | `selector`, `state?`, `timeout?` | `state: "visible"`, `timeout: 15000` | 요소 상태 대기 |
134
+ | `waitForNavigation` | `waitUntil?`, `timeout?` | `waitUntil: "networkidle"`, `timeout: 15000` | 페이지 로드 대기 |
135
+ | `waitForURL` | `url`, `timeout?` | `timeout: 15000` | URL 매칭 대기 |
136
+ | `waitForFunction` | `expression`, `polling?`, `timeout?` | `polling: "raf"`, `timeout: 30000` | JS 표현식이 truthy가 될 때까지 대기 |
137
+ | `waitForResponse` | `url`, `status?`, `timeout?` | `timeout: 30000` | 네트워크 응답 대기 (URL 부분 문자열 매칭) |
138
+
139
+ **`waitUntil`** 옵션: `"load"`, `"domcontentloaded"`, `"networkidle"` (기본)
140
+ **`state`** 옵션: `"visible"` (기본), `"attached"`, `"hidden"`
141
+ **`polling`** 옵션: `"raf"` (requestAnimationFrame, 기본) 또는 밀리초 숫자 (예: `500`)
142
+
143
+ #### 비동기 대기 예시
144
+
145
+ ```yaml
146
+ # 요소가 나타날 때까지 대기
147
+ - action: waitForSelector
148
+ selector: ".result-panel"
149
+ state: visible
150
+ timeout: 20000
151
+
152
+ # AI 스트리밍 응답 완료 대기
153
+ - action: waitForFunction
154
+ expression: "document.querySelector('.ai-response')?.dataset.done === 'true'"
155
+ timeout: 60000
156
+
157
+ # API 응답 완료 대기
158
+ - action: waitForResponse
159
+ url: "/api/chat/completions"
160
+ status: 200
161
+ timeout: 60000
162
+
163
+ # 동적 콘텐츠 길이 대기
164
+ - action: waitForFunction
165
+ expression: "document.querySelector('.output')?.textContent?.length > 100"
166
+ polling: 500
167
+ ```
126
168
 
127
169
  ### 타이밍 팁
128
170
 
package/README.md CHANGED
@@ -114,17 +114,57 @@ steps:
114
114
 
115
115
  ### Actions
116
116
 
117
- | Action | Parameters | Description |
118
- |--------|-----------|-------------|
119
- | `navigate` | `url`, `waitUntil?` | Navigate to URL |
120
- | `click` | `selector`, `delay?` | Click an element |
121
- | `type` | `selector`, `text`, `delay?` | Type text (char-by-char) |
122
- | `hover` | `selector` | Hover over element |
123
- | `scroll` | `y?`, `x?`, `selector?`, `smooth?` | Scroll by offset |
124
- | `wait` | `duration` | Wait (ms) |
125
- | `screenshot` | `name?`, `fullPage?` | Capture marker |
117
+ #### Basic Actions
118
+
119
+ | Action | Parameters | Default | Description |
120
+ |--------|-----------|---------|-------------|
121
+ | `navigate` | `url`, `waitUntil?` | `waitUntil: "networkidle"` | Navigate to URL |
122
+ | `click` | `selector`, `delay?`, `timeout?` | | Click an element |
123
+ | `type` | `selector`, `text`, `delay?`, `timeout?` | `delay: 50` | Type text (char-by-char) |
124
+ | `hover` | `selector`, `timeout?` | | Hover over element |
125
+ | `scroll` | `y?`, `x?`, `selector?`, `smooth?`, `timeout?` | `y: 0`, `x: 0`, `smooth: true` | Scroll by offset |
126
+ | `wait` | `duration` | | Wait (ms) |
127
+ | `screenshot` | `name?`, `fullPage?` | `fullPage: false` | Capture marker |
128
+
129
+ #### Async Wait Actions
130
+
131
+ | Action | Parameters | Default | Description |
132
+ |--------|-----------|---------|-------------|
133
+ | `waitForSelector` | `selector`, `state?`, `timeout?` | `state: "visible"`, `timeout: 15000` | Wait for element state |
134
+ | `waitForNavigation` | `waitUntil?`, `timeout?` | `waitUntil: "networkidle"`, `timeout: 15000` | Wait for page load |
135
+ | `waitForURL` | `url`, `timeout?` | `timeout: 15000` | Wait for URL match |
136
+ | `waitForFunction` | `expression`, `polling?`, `timeout?` | `polling: "raf"`, `timeout: 30000` | Wait for JS expression to be truthy |
137
+ | `waitForResponse` | `url`, `status?`, `timeout?` | `timeout: 30000` | Wait for network response (URL substring match) |
126
138
 
127
139
  **`waitUntil`** options: `"load"`, `"domcontentloaded"`, `"networkidle"` (default)
140
+ **`state`** options: `"visible"` (default), `"attached"`, `"hidden"`
141
+ **`polling`** options: `"raf"` (requestAnimationFrame, default) or milliseconds (e.g. `500`)
142
+
143
+ #### Async Wait Examples
144
+
145
+ ```yaml
146
+ # Wait for element to appear
147
+ - action: waitForSelector
148
+ selector: ".result-panel"
149
+ state: visible
150
+ timeout: 20000
151
+
152
+ # Wait for AI streaming response to complete
153
+ - action: waitForFunction
154
+ expression: "document.querySelector('.ai-response')?.dataset.done === 'true'"
155
+ timeout: 60000
156
+
157
+ # Wait for API response
158
+ - action: waitForResponse
159
+ url: "/api/chat/completions"
160
+ status: 200
161
+ timeout: 60000
162
+
163
+ # Wait for dynamic content length
164
+ - action: waitForFunction
165
+ expression: "document.querySelector('.output')?.textContent?.length > 100"
166
+ polling: 500
167
+ ```
128
168
 
129
169
  ### Timing Tips
130
170
 
package/dist/cli/index.js CHANGED
@@ -11,13 +11,13 @@ var __export = (target, all) => {
11
11
 
12
12
  // src/script/types.ts
13
13
  import { z } from "zod";
14
- var SafeSelectorSchema, NavigateActionSchema, ClickActionSchema, TypeActionSchema, ScrollActionSchema, WaitActionSchema, HoverActionSchema, ScreenshotActionSchema, StepActionSchema, AutoZoomConfigSchema, ZoomEffectSchema, CursorEffectSchema, BackgroundSchema, DeviceFrameSchema, SpeedRampConfigSchema, KeystrokeConfigSchema, WatermarkConfigSchema, EffectsConfigSchema, OutputConfigSchema, StepSchema, ScenarioSchema;
14
+ var SafeSelectorSchema, NavigateActionSchema, ClickActionSchema, TypeActionSchema, ScrollActionSchema, WaitActionSchema, HoverActionSchema, ScreenshotActionSchema, WaitForSelectorActionSchema, WaitForNavigationActionSchema, WaitForURLActionSchema, WaitForFunctionActionSchema, WaitForResponseActionSchema, StepActionSchema, AutoZoomConfigSchema, ZoomEffectSchema, CursorEffectSchema, BackgroundSchema, DeviceFrameSchema, SpeedRampConfigSchema, KeystrokeConfigSchema, WatermarkConfigSchema, EffectsConfigSchema, OutputConfigSchema, StepSchema, ScenarioSchema;
15
15
  var init_types = __esm({
16
16
  "src/script/types.ts"() {
17
17
  "use strict";
18
- SafeSelectorSchema = z.string().regex(
19
- /^[a-zA-Z0-9\-_#.\[\]="':\s~^$|*,>+()@]+$/,
20
- "Selector contains invalid characters"
18
+ SafeSelectorSchema = z.string().min(1, "Selector must not be empty").regex(
19
+ /^[^\x00-\x1f\x7f;`\\{}]+$/,
20
+ "Selector contains invalid characters (control chars, semicolons, backticks, or backslashes are not allowed)"
21
21
  );
22
22
  NavigateActionSchema = z.object({
23
23
  action: z.literal("navigate"),
@@ -27,20 +27,23 @@ var init_types = __esm({
27
27
  ClickActionSchema = z.object({
28
28
  action: z.literal("click"),
29
29
  selector: SafeSelectorSchema,
30
- delay: z.number().optional()
30
+ delay: z.number().optional(),
31
+ timeout: z.number().min(0).optional()
31
32
  });
32
33
  TypeActionSchema = z.object({
33
34
  action: z.literal("type"),
34
35
  selector: SafeSelectorSchema,
35
36
  text: z.string(),
36
- delay: z.number().default(50)
37
+ delay: z.number().default(50),
38
+ timeout: z.number().min(0).optional()
37
39
  });
38
40
  ScrollActionSchema = z.object({
39
41
  action: z.literal("scroll"),
40
42
  selector: SafeSelectorSchema.optional(),
41
43
  y: z.number().default(0),
42
44
  x: z.number().default(0),
43
- smooth: z.boolean().default(true)
45
+ smooth: z.boolean().default(true),
46
+ timeout: z.number().min(0).optional()
44
47
  });
45
48
  WaitActionSchema = z.object({
46
49
  action: z.literal("wait"),
@@ -48,13 +51,42 @@ var init_types = __esm({
48
51
  });
49
52
  HoverActionSchema = z.object({
50
53
  action: z.literal("hover"),
51
- selector: SafeSelectorSchema
54
+ selector: SafeSelectorSchema,
55
+ timeout: z.number().min(0).optional()
52
56
  });
53
57
  ScreenshotActionSchema = z.object({
54
58
  action: z.literal("screenshot"),
55
59
  name: z.string().optional(),
56
60
  fullPage: z.boolean().default(false)
57
61
  });
62
+ WaitForSelectorActionSchema = z.object({
63
+ action: z.literal("waitForSelector"),
64
+ selector: SafeSelectorSchema,
65
+ state: z.enum(["visible", "attached", "hidden"]).default("visible"),
66
+ timeout: z.number().min(0).default(15e3)
67
+ });
68
+ WaitForNavigationActionSchema = z.object({
69
+ action: z.literal("waitForNavigation"),
70
+ waitUntil: z.enum(["load", "domcontentloaded", "networkidle"]).default("networkidle"),
71
+ timeout: z.number().min(0).default(15e3)
72
+ });
73
+ WaitForURLActionSchema = z.object({
74
+ action: z.literal("waitForURL"),
75
+ url: z.string().min(1),
76
+ timeout: z.number().min(0).default(15e3)
77
+ });
78
+ WaitForFunctionActionSchema = z.object({
79
+ action: z.literal("waitForFunction"),
80
+ expression: z.string().min(1),
81
+ polling: z.union([z.literal("raf"), z.number().min(0)]).default("raf"),
82
+ timeout: z.number().min(0).default(3e4)
83
+ });
84
+ WaitForResponseActionSchema = z.object({
85
+ action: z.literal("waitForResponse"),
86
+ url: z.string().min(1),
87
+ status: z.number().min(100).max(599).optional(),
88
+ timeout: z.number().min(0).default(3e4)
89
+ });
58
90
  StepActionSchema = z.discriminatedUnion("action", [
59
91
  NavigateActionSchema,
60
92
  ClickActionSchema,
@@ -62,7 +94,12 @@ var init_types = __esm({
62
94
  ScrollActionSchema,
63
95
  WaitActionSchema,
64
96
  HoverActionSchema,
65
- ScreenshotActionSchema
97
+ ScreenshotActionSchema,
98
+ WaitForSelectorActionSchema,
99
+ WaitForNavigationActionSchema,
100
+ WaitForURLActionSchema,
101
+ WaitForFunctionActionSchema,
102
+ WaitForResponseActionSchema
66
103
  ]);
67
104
  AutoZoomConfigSchema = z.object({
68
105
  followCursor: z.boolean().default(true),
@@ -328,12 +365,13 @@ function interpolatePath(from, to, steps) {
328
365
  }
329
366
 
330
367
  // src/core/screenshot.ts
331
- async function getElementCenter(page, selector) {
332
- if (!/^[\w\-#.\[\]="':\s,>+~*()@^$|]+$/.test(selector)) {
368
+ var DEFAULT_ELEMENT_TIMEOUT = 5e3;
369
+ async function getElementCenter(page, selector, timeout) {
370
+ if (/[\x00-\x1f\x7f;`\\{}]/.test(selector) || selector.length === 0) {
333
371
  throw new Error(`Invalid selector: ${selector}`);
334
372
  }
335
373
  const element = page.locator(selector).first();
336
- await element.waitFor({ state: "visible", timeout: 5e3 });
374
+ await element.waitFor({ state: "visible", timeout: timeout ?? DEFAULT_ELEMENT_TIMEOUT });
337
375
  const box = await element.boundingBox();
338
376
  if (!box) {
339
377
  throw new Error(
@@ -374,6 +412,7 @@ var ClipwiseRecorder = class {
374
412
  targetFps = 30;
375
413
  cursorSpeed = "fast";
376
414
  firstContentTimestamp = 0;
415
+ pendingResponsePromises = /* @__PURE__ */ new Map();
377
416
  /**
378
417
  * Launch the browser and create a page with the scenario viewport.
379
418
  */
@@ -458,8 +497,9 @@ var ClipwiseRecorder = class {
458
497
  for (let si = 0; si < scenario.steps.length; si++) {
459
498
  const step = scenario.steps[si];
460
499
  this.currentStepIndex = si;
461
- for (const action of step.actions) {
462
- await this.executeAction(action);
500
+ this.preRegisterResponseListeners(step.actions);
501
+ for (let ai = 0; ai < step.actions.length; ai++) {
502
+ await this.executeAction(step.actions[ai], ai);
463
503
  }
464
504
  if (step.captureDelay > 0) {
465
505
  await this.waitWithRepaints(step.captureDelay);
@@ -525,11 +565,33 @@ var ClipwiseRecorder = class {
525
565
  }
526
566
  }
527
567
  }
568
+ /**
569
+ * Pre-register waitForResponse listeners at the start of each step.
570
+ * This ensures the listener is active before any preceding action
571
+ * (e.g. click) triggers the request, preventing race conditions
572
+ * where the response arrives before the listener is set up.
573
+ */
574
+ preRegisterResponseListeners(actions) {
575
+ this.pendingResponsePromises.clear();
576
+ if (!this.page) return;
577
+ for (let i = 0; i < actions.length; i++) {
578
+ const action = actions[i];
579
+ if (action.action === "waitForResponse") {
580
+ this.pendingResponsePromises.set(
581
+ i,
582
+ this.page.waitForResponse(
583
+ (response) => response.url().includes(action.url) && (action.status === void 0 || response.status() === action.status),
584
+ { timeout: action.timeout }
585
+ )
586
+ );
587
+ }
588
+ }
589
+ }
528
590
  /**
529
591
  * Execute a single action. CDP screencast captures frames continuously
530
592
  * in the background while actions are performed.
531
593
  */
532
- async executeAction(action) {
594
+ async executeAction(action, actionIndex = 0) {
533
595
  if (!this.page) {
534
596
  throw new Error("Page not initialized. Call init() first.");
535
597
  }
@@ -547,7 +609,7 @@ var ClipwiseRecorder = class {
547
609
  break;
548
610
  }
549
611
  case "click": {
550
- const target = await getElementCenter(this.page, action.selector);
612
+ const target = await getElementCenter(this.page, action.selector, action.timeout);
551
613
  await this.moveCursorSmooth(target);
552
614
  this.clickTimeline.push({
553
615
  position: { ...target },
@@ -561,7 +623,8 @@ var ClipwiseRecorder = class {
561
623
  case "type": {
562
624
  const inputTarget = await getElementCenter(
563
625
  this.page,
564
- action.selector
626
+ action.selector,
627
+ action.timeout
565
628
  );
566
629
  await this.moveCursorSmooth(inputTarget);
567
630
  this.clickTimeline.push({
@@ -579,7 +642,7 @@ var ClipwiseRecorder = class {
579
642
  break;
580
643
  }
581
644
  case "scroll": {
582
- const scrollTarget = action.selector ? await getElementCenter(this.page, action.selector) : null;
645
+ const scrollTarget = action.selector ? await getElementCenter(this.page, action.selector, action.timeout) : null;
583
646
  await this.page.evaluate(
584
647
  ({ x, y, smooth, selector }) => {
585
648
  const target = selector ? document.querySelector(selector) : window;
@@ -623,7 +686,8 @@ var ClipwiseRecorder = class {
623
686
  case "hover": {
624
687
  const hoverTarget = await getElementCenter(
625
688
  this.page,
626
- action.selector
689
+ action.selector,
690
+ action.timeout
627
691
  );
628
692
  await this.moveCursorSmooth(hoverTarget);
629
693
  await this.page.hover(action.selector);
@@ -633,6 +697,33 @@ var ClipwiseRecorder = class {
633
697
  await this.waitWithRepaints(100);
634
698
  break;
635
699
  }
700
+ case "waitForSelector": {
701
+ const locator = this.page.locator(action.selector).first();
702
+ await locator.waitFor({ state: action.state, timeout: action.timeout });
703
+ break;
704
+ }
705
+ case "waitForNavigation": {
706
+ await this.page.waitForLoadState(action.waitUntil, { timeout: action.timeout });
707
+ break;
708
+ }
709
+ case "waitForURL": {
710
+ await this.page.waitForURL(action.url, { timeout: action.timeout });
711
+ break;
712
+ }
713
+ case "waitForFunction": {
714
+ await this.page.waitForFunction(action.expression, void 0, {
715
+ polling: action.polling,
716
+ timeout: action.timeout
717
+ });
718
+ break;
719
+ }
720
+ case "waitForResponse": {
721
+ const pending = this.pendingResponsePromises.get(actionIndex);
722
+ if (pending) {
723
+ await pending;
724
+ }
725
+ break;
726
+ }
636
727
  }
637
728
  await this.waitWithRepaints(ACTION_GAP_MS);
638
729
  }