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 +51 -9
- package/README.md +49 -9
- package/dist/cli/index.js +110 -19
- package/dist/index.d.ts +413 -0
- package/dist/index.js +109 -18
- package/package.json +11 -11
package/README.ko.md
CHANGED
|
@@ -114,15 +114,57 @@ steps:
|
|
|
114
114
|
|
|
115
115
|
### 액션
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
|
120
|
-
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
|
120
|
-
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
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
|
-
/^[
|
|
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
|
-
|
|
332
|
-
|
|
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:
|
|
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
|
-
|
|
462
|
-
|
|
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
|
}
|