ectrol 0.0.2 → 0.0.4
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.d.mts +80 -1
- package/dist/index.mjs +240 -13
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -88,9 +88,16 @@ declare class ElementHandle {
|
|
|
88
88
|
* 填充输入框内容。
|
|
89
89
|
* - 适用于 `<input>`、`<textarea>` 或 `contenteditable` 元素。
|
|
90
90
|
* - 会触发 `input` 事件(冒泡)。
|
|
91
|
+
* - 支持自定义输入间隔, 'auto' 表示模拟自然输入节奏
|
|
92
|
+
* @example
|
|
93
|
+
* ```
|
|
94
|
+
* await element.fill('Hello, World!', { interval: 100 });
|
|
95
|
+
* ```
|
|
91
96
|
*/
|
|
92
97
|
fill(value: string, options?: {
|
|
93
98
|
timeout?: number;
|
|
99
|
+
interval?: number | 'auto';
|
|
100
|
+
append?: boolean;
|
|
94
101
|
}): Promise<void>;
|
|
95
102
|
/**
|
|
96
103
|
* 在元素上发送按键事件。
|
|
@@ -119,6 +126,74 @@ declare class ElementHandle {
|
|
|
119
126
|
* - 返回扩展的 DOMRect 信息并包含元素中心点坐标。
|
|
120
127
|
*/
|
|
121
128
|
getBoundingClientRect(timeout?: number): Promise<IBoundingClientRect | null>;
|
|
129
|
+
/**
|
|
130
|
+
* 选择下拉框选项。
|
|
131
|
+
* @param value 选项的值
|
|
132
|
+
* @returns
|
|
133
|
+
*/
|
|
134
|
+
selectOption(value: string): Promise<any>;
|
|
135
|
+
/**
|
|
136
|
+
* 获取元素的 HTML 内容。
|
|
137
|
+
*/
|
|
138
|
+
innerHTML(): Promise<any>;
|
|
139
|
+
/**
|
|
140
|
+
* 获取元素的文本内容。
|
|
141
|
+
*/
|
|
142
|
+
innerText(): Promise<any>;
|
|
143
|
+
/**
|
|
144
|
+
* 获取元素的文本内容。
|
|
145
|
+
* @returns 元素的文本内容
|
|
146
|
+
*/
|
|
147
|
+
textContent(): Promise<any>;
|
|
148
|
+
/**
|
|
149
|
+
* 获取元素的输入值。
|
|
150
|
+
* @returns 元素的输入值
|
|
151
|
+
*/
|
|
152
|
+
inputValue(): Promise<any>;
|
|
153
|
+
/**
|
|
154
|
+
* 获取元素的选中状态。
|
|
155
|
+
* @returns 元素的选中状态
|
|
156
|
+
*/
|
|
157
|
+
isChecked(): Promise<any>;
|
|
158
|
+
/**
|
|
159
|
+
* 获取元素的禁用状态。
|
|
160
|
+
* @returns 元素的禁用状态
|
|
161
|
+
*/
|
|
162
|
+
isDisabled(): Promise<any>;
|
|
163
|
+
/**
|
|
164
|
+
* 获取元素的可见状态。
|
|
165
|
+
* @returns 元素的可见状态
|
|
166
|
+
*/
|
|
167
|
+
isVisible(): Promise<any>;
|
|
168
|
+
/**
|
|
169
|
+
* 获取元素的可用状态。
|
|
170
|
+
* @returns 元素的可用状态
|
|
171
|
+
*/
|
|
172
|
+
isEnabled(): Promise<any>;
|
|
173
|
+
/**
|
|
174
|
+
* 获取元素的可编辑状态。
|
|
175
|
+
* @returns 元素的可编辑状态
|
|
176
|
+
*/
|
|
177
|
+
isEditable(): Promise<any>;
|
|
178
|
+
/**
|
|
179
|
+
* 获取元素的隐藏状态。
|
|
180
|
+
* @returns 元素的隐藏状态
|
|
181
|
+
*/
|
|
182
|
+
isHidden(): Promise<any>;
|
|
183
|
+
/**
|
|
184
|
+
* 选中元素的文本内容。
|
|
185
|
+
*/
|
|
186
|
+
selectText(options: {
|
|
187
|
+
timeout?: number;
|
|
188
|
+
}): Promise<any>;
|
|
189
|
+
/**
|
|
190
|
+
* 取消选中复选框或单选框。
|
|
191
|
+
* @param options 选项
|
|
192
|
+
* @returns
|
|
193
|
+
*/
|
|
194
|
+
uncheck(options?: {
|
|
195
|
+
timeout?: number;
|
|
196
|
+
}): Promise<any>;
|
|
122
197
|
}
|
|
123
198
|
//#endregion
|
|
124
199
|
//#region src/localStorage.d.ts
|
|
@@ -169,6 +244,10 @@ declare class SessionStorage {
|
|
|
169
244
|
removeItem(key: string): Promise<void>;
|
|
170
245
|
}
|
|
171
246
|
//#endregion
|
|
247
|
+
//#region src/utils.d.ts
|
|
248
|
+
declare function getHumanTypingDelay(index: number, text: string): number;
|
|
249
|
+
declare function sleep(ms: number): Promise<unknown>;
|
|
250
|
+
//#endregion
|
|
172
251
|
//#region src/index.d.ts
|
|
173
252
|
/**
|
|
174
253
|
* Ectrol
|
|
@@ -193,4 +272,4 @@ declare class Ectrol {
|
|
|
193
272
|
$(selector: string): ElementHandle;
|
|
194
273
|
}
|
|
195
274
|
//#endregion
|
|
196
|
-
export { Ectrol, Ectrol as default, ElementHandle, IBoundingClientRect, ISize, IVector2D, LocalStorage, SessionStorage };
|
|
275
|
+
export { Ectrol, Ectrol as default, ElementHandle, IBoundingClientRect, ISize, IVector2D, LocalStorage, SessionStorage, getHumanTypingDelay, sleep };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
//#region src/utils.ts
|
|
2
|
+
function getHumanTypingDelay(index, text) {
|
|
3
|
+
const char = text[index];
|
|
4
|
+
let delay = 90 + Math.random() * 60;
|
|
5
|
+
const progress = index / text.length;
|
|
6
|
+
if (progress < .15) delay *= 1.3;
|
|
7
|
+
else if (progress > .85) delay *= 1.25;
|
|
8
|
+
if (char === " ") delay += 40;
|
|
9
|
+
if (/[,.!?;:]/.test(char)) delay += 200 + Math.random() * 150;
|
|
10
|
+
if (Math.random() < .03) delay += 300 + Math.random() * 500;
|
|
11
|
+
return Math.max(40, Math.round(delay));
|
|
12
|
+
}
|
|
13
|
+
async function sleep(ms) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
1
18
|
//#region src/element.ts
|
|
2
19
|
/**
|
|
3
20
|
* ElementHandle
|
|
@@ -147,7 +164,7 @@ var ElementHandle = class {
|
|
|
147
164
|
button: options?.button || "left",
|
|
148
165
|
clickCount: 1
|
|
149
166
|
});
|
|
150
|
-
await
|
|
167
|
+
await sleep(options?.delay || 10);
|
|
151
168
|
this.contents.sendInputEvent({
|
|
152
169
|
type: "mouseUp",
|
|
153
170
|
x: rect.centerX,
|
|
@@ -198,22 +215,32 @@ var ElementHandle = class {
|
|
|
198
215
|
* 填充输入框内容。
|
|
199
216
|
* - 适用于 `<input>`、`<textarea>` 或 `contenteditable` 元素。
|
|
200
217
|
* - 会触发 `input` 事件(冒泡)。
|
|
218
|
+
* - 支持自定义输入间隔, 'auto' 表示模拟自然输入节奏
|
|
219
|
+
* @example
|
|
220
|
+
* ```
|
|
221
|
+
* await element.fill('Hello, World!', { interval: 100 });
|
|
222
|
+
* ```
|
|
201
223
|
*/
|
|
202
224
|
async fill(value, options) {
|
|
203
|
-
await this.
|
|
225
|
+
await this.focus({ timeout: options?.timeout });
|
|
226
|
+
let interval;
|
|
227
|
+
if (typeof options?.interval === "number") interval = options?.interval;
|
|
228
|
+
if (!options?.append) await this.contents.executeJavaScript(`
|
|
204
229
|
(function() {
|
|
205
230
|
const target = ${this._getElement(options?.timeout)};
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
target.element.focus();
|
|
212
|
-
|
|
213
|
-
target.element.value = ${JSON.stringify(value)};
|
|
214
|
-
target.element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
231
|
+
if (target && target.element) {
|
|
232
|
+
if (target.element.tagName !== 'INPUT' && target.element.tagName !== 'TEXTAREA' && !target.element.isContentEditable) return null;
|
|
233
|
+
target.element.value = "";
|
|
234
|
+
}
|
|
215
235
|
})()
|
|
216
236
|
`);
|
|
237
|
+
for (let index = 0; index < value.length; index++) {
|
|
238
|
+
await sleep(interval || getHumanTypingDelay(index, value));
|
|
239
|
+
this.contents.sendInputEvent({
|
|
240
|
+
type: "char",
|
|
241
|
+
keyCode: value[index]
|
|
242
|
+
});
|
|
243
|
+
}
|
|
217
244
|
}
|
|
218
245
|
/**
|
|
219
246
|
* 在元素上发送按键事件。
|
|
@@ -230,7 +257,7 @@ var ElementHandle = class {
|
|
|
230
257
|
type: "keyDown",
|
|
231
258
|
keyCode: key$1
|
|
232
259
|
});
|
|
233
|
-
await
|
|
260
|
+
await sleep(options?.delay || 10);
|
|
234
261
|
for (const key$1 of keys) this.contents.sendInputEvent({
|
|
235
262
|
type: "keyUp",
|
|
236
263
|
keyCode: key$1
|
|
@@ -286,6 +313,206 @@ var ElementHandle = class {
|
|
|
286
313
|
})()
|
|
287
314
|
`);
|
|
288
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* 选择下拉框选项。
|
|
318
|
+
* @param value 选项的值
|
|
319
|
+
* @returns
|
|
320
|
+
*/
|
|
321
|
+
async selectOption(value) {
|
|
322
|
+
return this.contents.executeJavaScript(`
|
|
323
|
+
(function() {
|
|
324
|
+
const target = ${this._getElement()};
|
|
325
|
+
if (!target) return null;
|
|
326
|
+
|
|
327
|
+
// 验证元素是否为选择框
|
|
328
|
+
if (target.element.tagName !== 'SELECT') return null;
|
|
329
|
+
|
|
330
|
+
target.element.value = ${JSON.stringify(value)};
|
|
331
|
+
target.element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
332
|
+
})()
|
|
333
|
+
`);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* 获取元素的 HTML 内容。
|
|
337
|
+
*/
|
|
338
|
+
async innerHTML() {
|
|
339
|
+
return this.contents.executeJavaScript(`
|
|
340
|
+
(function() {
|
|
341
|
+
const target = ${this._getElement()};
|
|
342
|
+
if (!target) return null;
|
|
343
|
+
|
|
344
|
+
return target.element.innerHTML;
|
|
345
|
+
})()
|
|
346
|
+
`);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* 获取元素的文本内容。
|
|
350
|
+
*/
|
|
351
|
+
async innerText() {
|
|
352
|
+
return this.contents.executeJavaScript(`
|
|
353
|
+
(function() {
|
|
354
|
+
const target = ${this._getElement()};
|
|
355
|
+
if (!target) return null;
|
|
356
|
+
|
|
357
|
+
return target.element.innerText;
|
|
358
|
+
})()
|
|
359
|
+
`);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* 获取元素的文本内容。
|
|
363
|
+
* @returns 元素的文本内容
|
|
364
|
+
*/
|
|
365
|
+
async textContent() {
|
|
366
|
+
return this.contents.executeJavaScript(`
|
|
367
|
+
(function() {
|
|
368
|
+
const target = ${this._getElement()};
|
|
369
|
+
if (!target) return null;
|
|
370
|
+
|
|
371
|
+
return target.element.textContent;
|
|
372
|
+
})()
|
|
373
|
+
`);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* 获取元素的输入值。
|
|
377
|
+
* @returns 元素的输入值
|
|
378
|
+
*/
|
|
379
|
+
async inputValue() {
|
|
380
|
+
return this.contents.executeJavaScript(`
|
|
381
|
+
(function() {
|
|
382
|
+
const target = ${this._getElement()};
|
|
383
|
+
if (!target) return null;
|
|
384
|
+
|
|
385
|
+
// 验证输入框是否是文本类型
|
|
386
|
+
if (target.element.tagName !== 'INPUT' && target.element.tagName !== 'TEXTAREA' && !target.element.isContentEditable) return null;
|
|
387
|
+
|
|
388
|
+
return target.element.value;
|
|
389
|
+
})()
|
|
390
|
+
`);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 获取元素的选中状态。
|
|
394
|
+
* @returns 元素的选中状态
|
|
395
|
+
*/
|
|
396
|
+
async isChecked() {
|
|
397
|
+
return this.contents.executeJavaScript(`
|
|
398
|
+
(function() {
|
|
399
|
+
const target = ${this._getElement()};
|
|
400
|
+
if (!target) return null;
|
|
401
|
+
|
|
402
|
+
// 验证输入框是否是复选框或单选框
|
|
403
|
+
if (target.element.tagName !== 'INPUT' || target.element.type !== 'checkbox' && target.element.type !== 'radio') return null;
|
|
404
|
+
|
|
405
|
+
return target.element.checked;
|
|
406
|
+
})()
|
|
407
|
+
`);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* 获取元素的禁用状态。
|
|
411
|
+
* @returns 元素的禁用状态
|
|
412
|
+
*/
|
|
413
|
+
async isDisabled() {
|
|
414
|
+
return this.contents.executeJavaScript(`
|
|
415
|
+
(function() {
|
|
416
|
+
const target = ${this._getElement()};
|
|
417
|
+
if (!target) return null;
|
|
418
|
+
|
|
419
|
+
return target.element.disabled;
|
|
420
|
+
})()
|
|
421
|
+
`);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* 获取元素的可见状态。
|
|
425
|
+
* @returns 元素的可见状态
|
|
426
|
+
*/
|
|
427
|
+
async isVisible() {
|
|
428
|
+
return this.contents.executeJavaScript(`
|
|
429
|
+
(function() {
|
|
430
|
+
const target = ${this._getElement()};
|
|
431
|
+
if (!target) return null;
|
|
432
|
+
|
|
433
|
+
return target.element.offsetParent !== null;
|
|
434
|
+
})()
|
|
435
|
+
`);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* 获取元素的可用状态。
|
|
439
|
+
* @returns 元素的可用状态
|
|
440
|
+
*/
|
|
441
|
+
async isEnabled() {
|
|
442
|
+
return this.contents.executeJavaScript(`
|
|
443
|
+
(function() {
|
|
444
|
+
const target = ${this._getElement()};
|
|
445
|
+
if (!target) return null;
|
|
446
|
+
|
|
447
|
+
return !target.element.disabled;
|
|
448
|
+
})()
|
|
449
|
+
`);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* 获取元素的可编辑状态。
|
|
453
|
+
* @returns 元素的可编辑状态
|
|
454
|
+
*/
|
|
455
|
+
async isEditable() {
|
|
456
|
+
return this.contents.executeJavaScript(`
|
|
457
|
+
(function() {
|
|
458
|
+
const target = ${this._getElement()};
|
|
459
|
+
if (!target) return null;
|
|
460
|
+
|
|
461
|
+
return target.element.isContentEditable;
|
|
462
|
+
})()
|
|
463
|
+
`);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* 获取元素的隐藏状态。
|
|
467
|
+
* @returns 元素的隐藏状态
|
|
468
|
+
*/
|
|
469
|
+
async isHidden() {
|
|
470
|
+
return this.contents.executeJavaScript(`
|
|
471
|
+
(function() {
|
|
472
|
+
const target = ${this._getElement()};
|
|
473
|
+
if (!target) return null;
|
|
474
|
+
|
|
475
|
+
return target.element.offsetParent === null;
|
|
476
|
+
})()
|
|
477
|
+
`);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* 选中元素的文本内容。
|
|
481
|
+
*/
|
|
482
|
+
async selectText(options) {
|
|
483
|
+
return this.contents.executeJavaScript(`
|
|
484
|
+
(function() {
|
|
485
|
+
const target = ${this._getElement(options.timeout)};
|
|
486
|
+
if (!target) return null;
|
|
487
|
+
|
|
488
|
+
// 选中元素的文本内容
|
|
489
|
+
const selection = window.getSelection();
|
|
490
|
+
const range = document.createRange();
|
|
491
|
+
range.selectNodeContents(target.element);
|
|
492
|
+
selection.removeAllRanges();
|
|
493
|
+
selection.addRange(range);
|
|
494
|
+
})()
|
|
495
|
+
`);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* 取消选中复选框或单选框。
|
|
499
|
+
* @param options 选项
|
|
500
|
+
* @returns
|
|
501
|
+
*/
|
|
502
|
+
async uncheck(options) {
|
|
503
|
+
return this.contents.executeJavaScript(`
|
|
504
|
+
(function() {
|
|
505
|
+
const target = ${this._getElement(options?.timeout)};
|
|
506
|
+
if (!target) return null;
|
|
507
|
+
|
|
508
|
+
// 验证输入框是否是复选框或单选框
|
|
509
|
+
if (target.element.tagName !== 'INPUT' || target.element.type !== 'checkbox' && target.element.type !== 'radio') return null;
|
|
510
|
+
|
|
511
|
+
target.element.checked = false;
|
|
512
|
+
target.element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
513
|
+
})()
|
|
514
|
+
`);
|
|
515
|
+
}
|
|
289
516
|
};
|
|
290
517
|
var element_default = ElementHandle;
|
|
291
518
|
|
|
@@ -387,4 +614,4 @@ var Ectrol = class {
|
|
|
387
614
|
var src_default = Ectrol;
|
|
388
615
|
|
|
389
616
|
//#endregion
|
|
390
|
-
export { Ectrol, ElementHandle, LocalStorage, SessionStorage, src_default as default };
|
|
617
|
+
export { Ectrol, ElementHandle, LocalStorage, SessionStorage, src_default as default, getHumanTypingDelay, sleep };
|
package/package.json
CHANGED