@weapp-vite/miniprogram-automator 1.0.1 → 1.0.3
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.md +95 -4
- package/dist/index.d.mts +6 -2
- package/dist/index.mjs +119 -25
- package/dist/{launch-IFPMxQYb.mjs → launch-Didv0lMX.mjs} +1880 -277
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,98 @@
|
|
|
1
1
|
# @weapp-vite/miniprogram-automator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 1. 简介
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
`@weapp-vite/miniprogram-automator` 是面向 `weapp-vite` 生态维护的 `miniprogram-automator` 兼容实现,目标是在保留官方心智模型的前提下,提供更现代的 ESM 导出、类型支持与 headless 调试能力。
|
|
6
|
+
|
|
7
|
+
它同时服务于:
|
|
8
|
+
|
|
9
|
+
- `weapp-ide-cli` 的自动化能力
|
|
10
|
+
- 仓库内 WeChat DevTools 相关 e2e 测试
|
|
11
|
+
- 需要直接连接微信开发者工具 WebSocket 协议的 Node 工具
|
|
12
|
+
|
|
13
|
+
## 2. 特性
|
|
14
|
+
|
|
15
|
+
- 对齐官方常见对象模型:`MiniProgram`、`Page`、`Element`、`Native`
|
|
16
|
+
- 提供 `Launcher` 用于启动、连接和复用 DevTools 会话
|
|
17
|
+
- 支持截图、输入、滚动、点击、页面跳转等自动化操作
|
|
18
|
+
- 默认输出现代 ESM 与完整类型声明
|
|
19
|
+
- 内置二维码打印与解码辅助能力,便于配合登录、连接流程调试
|
|
20
|
+
- 支持 headless 自动化启动入口
|
|
21
|
+
|
|
22
|
+
## 3. 安装
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm add -D @weapp-vite/miniprogram-automator
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> **注意**:运行前仍需要本机安装微信开发者工具,并开启服务端口。
|
|
29
|
+
|
|
30
|
+
## 4. 快速开始
|
|
31
|
+
|
|
32
|
+
### 4.1 连接现有会话
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { Launcher } from '@weapp-vite/miniprogram-automator'
|
|
36
|
+
|
|
37
|
+
const launcher = new Launcher()
|
|
38
|
+
const miniProgram = await launcher.connect({
|
|
39
|
+
wsEndpoint: 'ws://127.0.0.1:9420',
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const page = await miniProgram.currentPage()
|
|
43
|
+
console.log(await page.data())
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 4.2 启动并打开项目
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { Launcher } from '@weapp-vite/miniprogram-automator'
|
|
50
|
+
|
|
51
|
+
const launcher = new Launcher()
|
|
52
|
+
const miniProgram = await launcher.launch({
|
|
53
|
+
projectPath: './dist/build/mp-weixin',
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
await miniProgram.reLaunch('/pages/index/index')
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 4.3 使用页面与元素对象
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { Launcher } from '@weapp-vite/miniprogram-automator'
|
|
63
|
+
|
|
64
|
+
const launcher = new Launcher()
|
|
65
|
+
const miniProgram = await launcher.launch({
|
|
66
|
+
projectPath: './dist/build/mp-weixin',
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const page = await miniProgram.currentPage()
|
|
70
|
+
const button = await page.$('.submit')
|
|
71
|
+
|
|
72
|
+
await button?.tap()
|
|
73
|
+
await page.waitFor(500)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 5. 主要导出
|
|
77
|
+
|
|
78
|
+
| 导出 | 说明 |
|
|
79
|
+
| ------------------------- | ---------------------------------------- |
|
|
80
|
+
| `Launcher` | 启动 DevTools、连接 WebSocket、创建会话 |
|
|
81
|
+
| `MiniProgram` | 小程序级操作入口,如页面跳转、获取当前页 |
|
|
82
|
+
| `Page` | 页面级查询与操作入口 |
|
|
83
|
+
| `Element` | 通用节点对象,支持点击、输入、事件触发 |
|
|
84
|
+
| `Native` | 原生能力桥接 |
|
|
85
|
+
| `launchHeadlessAutomator` | headless 启动辅助函数 |
|
|
86
|
+
|
|
87
|
+
## 6. 本地开发
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pnpm --filter @weapp-vite/miniprogram-automator build
|
|
91
|
+
pnpm --filter @weapp-vite/miniprogram-automator test
|
|
92
|
+
pnpm --filter @weapp-vite/miniprogram-automator typecheck
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 7. 相关链接
|
|
96
|
+
|
|
97
|
+
- 仓库:https://github.com/weapp-vite/weapp-vite
|
|
98
|
+
- `weapp-ide-cli`:[../weapp-ide-cli/README.md](../weapp-ide-cli/README.md)
|
package/dist/index.d.mts
CHANGED
|
@@ -247,7 +247,6 @@ declare class MiniProgram extends EventEmitter {
|
|
|
247
247
|
}
|
|
248
248
|
//#endregion
|
|
249
249
|
//#region src/Launcher.d.ts
|
|
250
|
-
/** IConnectOptions 的类型定义。 */
|
|
251
250
|
interface IConnectOptions {
|
|
252
251
|
wsEndpoint: string;
|
|
253
252
|
}
|
|
@@ -265,6 +264,11 @@ interface ILaunchOptions {
|
|
|
265
264
|
cwd?: string;
|
|
266
265
|
runtimeProvider?: 'devtools' | 'headless';
|
|
267
266
|
}
|
|
267
|
+
interface ILauncherSessionMetadata {
|
|
268
|
+
port: number;
|
|
269
|
+
projectPath: string;
|
|
270
|
+
wsEndpoint: string;
|
|
271
|
+
}
|
|
268
272
|
/** Launcher 的实现。 */
|
|
269
273
|
declare class Launcher {
|
|
270
274
|
launch(options: ILaunchOptions): Promise<any>;
|
|
@@ -301,4 +305,4 @@ declare function isPluginPath(p: string): boolean;
|
|
|
301
305
|
/** extractPluginId 的方法封装。 */
|
|
302
306
|
declare function extractPluginId(p: string): string;
|
|
303
307
|
//#endregion
|
|
304
|
-
export { Automator, Connection, ContextElement, CustomElement, Element, IConnectOptions, ILaunchOptions, InputElement, Launcher, MiniProgram, MovableViewElement, Native, Page, ScrollViewElement, SliderElement, SwiperElement, SwitchElement, TextareaElement, Transport, decodeQrCode, extractPluginId, isPluginPath, printQrCode };
|
|
308
|
+
export { Automator, Connection, ContextElement, CustomElement, Element, IConnectOptions, ILaunchOptions, ILauncherSessionMetadata, InputElement, Launcher, MiniProgram, MovableViewElement, Native, Page, ScrollViewElement, SliderElement, SwiperElement, SwitchElement, TextareaElement, Transport, decodeQrCode, extractPluginId, isPluginPath, printQrCode };
|
package/dist/index.mjs
CHANGED
|
@@ -497,6 +497,7 @@ var Transport = class extends EventEmitter {
|
|
|
497
497
|
*/
|
|
498
498
|
const debugProtocol = debug("automator:protocol");
|
|
499
499
|
const closeErrTip = "Connection closed, check if wechat web devTools is still running";
|
|
500
|
+
const REQUEST_TIMEOUT = 3e4;
|
|
500
501
|
/** Connection 的实现。 */
|
|
501
502
|
var Connection = class Connection extends EventEmitter {
|
|
502
503
|
callbacks = /* @__PURE__ */ new Map();
|
|
@@ -515,13 +516,22 @@ var Connection = class Connection extends EventEmitter {
|
|
|
515
516
|
});
|
|
516
517
|
debugProtocol(`${dateFormat("yyyy-mm-dd HH:MM:ss:l")} SEND ► ${payload}`);
|
|
517
518
|
return new Promise((resolve, reject) => {
|
|
519
|
+
const timeout = setTimeout(() => {
|
|
520
|
+
this.callbacks.delete(id);
|
|
521
|
+
const error = /* @__PURE__ */ new Error(`DevTools did not respond to protocol method ${method} within ${REQUEST_TIMEOUT}ms`);
|
|
522
|
+
error.code = "DEVTOOLS_PROTOCOL_TIMEOUT";
|
|
523
|
+
error.method = method;
|
|
524
|
+
reject(error);
|
|
525
|
+
}, REQUEST_TIMEOUT);
|
|
518
526
|
this.callbacks.set(id, {
|
|
519
527
|
resolve,
|
|
520
|
-
reject
|
|
528
|
+
reject,
|
|
529
|
+
timeout
|
|
521
530
|
});
|
|
522
531
|
try {
|
|
523
532
|
this.transport.send(payload);
|
|
524
533
|
} catch {
|
|
534
|
+
clearTimeout(timeout);
|
|
525
535
|
this.callbacks.delete(id);
|
|
526
536
|
reject(new Error(closeErrTip));
|
|
527
537
|
}
|
|
@@ -540,6 +550,7 @@ var Connection = class Connection extends EventEmitter {
|
|
|
540
550
|
const callback = this.callbacks.get(id);
|
|
541
551
|
if (!callback) return;
|
|
542
552
|
this.callbacks.delete(id);
|
|
553
|
+
clearTimeout(callback.timeout);
|
|
543
554
|
if (error) {
|
|
544
555
|
callback.reject(new Error(error.message || closeErrTip));
|
|
545
556
|
return;
|
|
@@ -547,7 +558,10 @@ var Connection = class Connection extends EventEmitter {
|
|
|
547
558
|
callback.resolve(result);
|
|
548
559
|
};
|
|
549
560
|
onClose = () => {
|
|
550
|
-
for (const callback of this.callbacks.values())
|
|
561
|
+
for (const callback of this.callbacks.values()) {
|
|
562
|
+
clearTimeout(callback.timeout);
|
|
563
|
+
callback.reject(new Error(closeErrTip));
|
|
564
|
+
}
|
|
551
565
|
this.callbacks.clear();
|
|
552
566
|
};
|
|
553
567
|
static create(url) {
|
|
@@ -563,11 +577,11 @@ var Connection = class Connection extends EventEmitter {
|
|
|
563
577
|
//#endregion
|
|
564
578
|
//#region src/headless.ts
|
|
565
579
|
async function launchHeadlessAutomator(options) {
|
|
566
|
-
return await (await import("./launch-
|
|
580
|
+
return await (await import("./launch-Didv0lMX.mjs")).launch({ projectPath: options.projectPath });
|
|
567
581
|
}
|
|
568
582
|
//#endregion
|
|
569
583
|
//#region package.json
|
|
570
|
-
var version = "1.0.
|
|
584
|
+
var version = "1.0.3";
|
|
571
585
|
//#endregion
|
|
572
586
|
//#region src/Native.ts
|
|
573
587
|
/** Native 的实现。 */
|
|
@@ -755,14 +769,34 @@ function extractPluginId(p) {
|
|
|
755
769
|
}
|
|
756
770
|
//#endregion
|
|
757
771
|
//#region src/MiniProgram.ts
|
|
772
|
+
const CLOSE_STEP_TIMEOUT = 2e3;
|
|
773
|
+
const CURRENT_PAGE_RETRIES = 3;
|
|
774
|
+
const CURRENT_PAGE_RETRY_DELAY = 400;
|
|
758
775
|
function sleep(ms) {
|
|
759
776
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
760
777
|
}
|
|
778
|
+
function withTimeout(task, timeoutMs) {
|
|
779
|
+
return new Promise((resolve, reject) => {
|
|
780
|
+
const timeout = setTimeout(() => {
|
|
781
|
+
reject(/* @__PURE__ */ new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
782
|
+
}, timeoutMs);
|
|
783
|
+
task.then((value) => {
|
|
784
|
+
clearTimeout(timeout);
|
|
785
|
+
resolve(value);
|
|
786
|
+
}).catch((error) => {
|
|
787
|
+
clearTimeout(timeout);
|
|
788
|
+
reject(error);
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
}
|
|
761
792
|
function isFnStr(value) {
|
|
762
793
|
if (!isStr(value)) return false;
|
|
763
794
|
const trimmed = trim(value);
|
|
764
795
|
return startWith(trimmed, "function") || startWith(trimmed, "() =>");
|
|
765
796
|
}
|
|
797
|
+
function isCurrentPageProtocolTimeout(error) {
|
|
798
|
+
return error instanceof Error && "code" in error && error.code === "DEVTOOLS_PROTOCOL_TIMEOUT" && "method" in error && error.method === "App.getCurrentPage";
|
|
799
|
+
}
|
|
766
800
|
/** MiniProgram 的实现。 */
|
|
767
801
|
var MiniProgram = class extends EventEmitter {
|
|
768
802
|
appBindings = /* @__PURE__ */ new Map();
|
|
@@ -801,12 +835,20 @@ var MiniProgram = class extends EventEmitter {
|
|
|
801
835
|
return await this.changeRoute("switchTab", url);
|
|
802
836
|
}
|
|
803
837
|
async currentPage() {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
838
|
+
let lastError;
|
|
839
|
+
for (let attempt = 1; attempt <= CURRENT_PAGE_RETRIES; attempt += 1) try {
|
|
840
|
+
const { pageId, path, query } = await this.send("App.getCurrentPage");
|
|
841
|
+
return Page.create(this.connection, {
|
|
842
|
+
id: pageId,
|
|
843
|
+
path,
|
|
844
|
+
query
|
|
845
|
+
}, this.pageMap);
|
|
846
|
+
} catch (error) {
|
|
847
|
+
lastError = error;
|
|
848
|
+
if (!isCurrentPageProtocolTimeout(error) || attempt >= CURRENT_PAGE_RETRIES) throw error;
|
|
849
|
+
await sleep(CURRENT_PAGE_RETRY_DELAY);
|
|
850
|
+
}
|
|
851
|
+
throw lastError;
|
|
810
852
|
}
|
|
811
853
|
async systemInfo() {
|
|
812
854
|
return await this.callWxMethod("getSystemInfoSync");
|
|
@@ -878,11 +920,14 @@ var MiniProgram = class extends EventEmitter {
|
|
|
878
920
|
}
|
|
879
921
|
async close() {
|
|
880
922
|
try {
|
|
881
|
-
await this.send("App.exit");
|
|
923
|
+
await withTimeout(this.send("App.exit"), CLOSE_STEP_TIMEOUT);
|
|
882
924
|
} catch {}
|
|
883
925
|
await sleep(1e3);
|
|
884
|
-
|
|
885
|
-
|
|
926
|
+
try {
|
|
927
|
+
await withTimeout(this.send("Tool.close"), CLOSE_STEP_TIMEOUT);
|
|
928
|
+
} catch {} finally {
|
|
929
|
+
this.disconnect();
|
|
930
|
+
}
|
|
886
931
|
}
|
|
887
932
|
async remote(auto = false) {
|
|
888
933
|
const { qrCode } = await this.send("Tool.enableRemoteDebug", { auto });
|
|
@@ -972,7 +1017,12 @@ const DEFAULT_PORT = 9420;
|
|
|
972
1017
|
const DEFAULT_TIMEOUT = 3e4;
|
|
973
1018
|
const DEFAULT_RUNTIME_PROVIDER_ENV = "WEAPP_VITE_AUTOMATOR_RUNTIME_PROVIDER";
|
|
974
1019
|
const LEGACY_RUNTIME_PROVIDER_ENV = "WEAPP_VITE_E2E_RUNTIME_PROVIDER";
|
|
1020
|
+
const EXTENSION_CONTEXT_INVALIDATED_RE = /Extension context invalidated/i;
|
|
1021
|
+
const WINDOWS_BATCH_CLI_RE = /\.(?:bat|cmd)$/i;
|
|
975
1022
|
let localhostListenPatched = false;
|
|
1023
|
+
function isExtensionContextInvalidatedError(error) {
|
|
1024
|
+
return error instanceof Error && EXTENSION_CONTEXT_INVALIDATED_RE.test(error.message);
|
|
1025
|
+
}
|
|
976
1026
|
function patchNetListenToLoopback() {
|
|
977
1027
|
if (localhostListenPatched) return;
|
|
978
1028
|
localhostListenPatched = true;
|
|
@@ -990,6 +1040,25 @@ function patchNetListenToLoopback() {
|
|
|
990
1040
|
return rawListen.apply(this, args);
|
|
991
1041
|
};
|
|
992
1042
|
}
|
|
1043
|
+
/** IConnectOptions 的类型定义。 */
|
|
1044
|
+
function shouldUseWindowsCommandShell(cliPath) {
|
|
1045
|
+
return isWindows && WINDOWS_BATCH_CLI_RE.test(cliPath);
|
|
1046
|
+
}
|
|
1047
|
+
function escapeWindowsCmdArg(arg) {
|
|
1048
|
+
const escaped = arg.replace(/"/g, "\"\"").replace(/%/g, "%%");
|
|
1049
|
+
return /[\s"&<>^|()]/.test(arg) ? `"${escaped}"` : escaped;
|
|
1050
|
+
}
|
|
1051
|
+
function resolveWindowsBatchSpawn(cliPath, args) {
|
|
1052
|
+
return {
|
|
1053
|
+
file: process.env.ComSpec || "cmd.exe",
|
|
1054
|
+
args: [
|
|
1055
|
+
"/d",
|
|
1056
|
+
"/s",
|
|
1057
|
+
"/c",
|
|
1058
|
+
`"${[cliPath, ...args].map(escapeWindowsCmdArg).join(" ")}"`
|
|
1059
|
+
]
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
993
1062
|
function resolveRuntimeProvider(options) {
|
|
994
1063
|
return options.runtimeProvider || process.env[DEFAULT_RUNTIME_PROVIDER_ENV] || process.env[LEGACY_RUNTIME_PROVIDER_ENV] || "devtools";
|
|
995
1064
|
}
|
|
@@ -1009,7 +1078,8 @@ var Launcher = class {
|
|
|
1009
1078
|
if (!await import("node:fs/promises").then((fs) => fs.access(projectPath).then(() => true).catch(() => false))) throw new Error(`Project path ${projectPath} doesn't exist`);
|
|
1010
1079
|
if (!isEmpty(projectConfig)) await this.extendProjectConfig(projectConfig, projectPath);
|
|
1011
1080
|
let processError = null;
|
|
1012
|
-
let
|
|
1081
|
+
let processExitCode = null;
|
|
1082
|
+
let processSignal = null;
|
|
1013
1083
|
args = [
|
|
1014
1084
|
...args,
|
|
1015
1085
|
"auto",
|
|
@@ -1022,39 +1092,63 @@ var Launcher = class {
|
|
|
1022
1092
|
else if (ticket) args.push("--ticket", ticket);
|
|
1023
1093
|
if (trustProject) args.push("--trust-project");
|
|
1024
1094
|
try {
|
|
1025
|
-
const
|
|
1095
|
+
const spawnTarget = shouldUseWindowsCommandShell(cliPath) ? resolveWindowsBatchSpawn(cliPath, args) : {
|
|
1096
|
+
file: cliPath,
|
|
1097
|
+
args
|
|
1098
|
+
};
|
|
1099
|
+
const child = spawn(spawnTarget.file, spawnTarget.args, {
|
|
1026
1100
|
stdio: "ignore",
|
|
1027
|
-
cwd: cwd || void 0
|
|
1101
|
+
cwd: cwd || void 0,
|
|
1102
|
+
...shouldUseWindowsCommandShell(cliPath) ? {
|
|
1103
|
+
windowsHide: true,
|
|
1104
|
+
windowsVerbatimArguments: true
|
|
1105
|
+
} : {}
|
|
1028
1106
|
});
|
|
1029
1107
|
child.on("error", (error) => {
|
|
1030
1108
|
processError = error;
|
|
1031
1109
|
});
|
|
1032
|
-
child.on("exit", () => {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
}
|
|
1110
|
+
child.on("exit", (code, signal) => {
|
|
1111
|
+
processExitCode = code;
|
|
1112
|
+
processSignal = signal;
|
|
1113
|
+
if (code !== 0 || signal) processError = /* @__PURE__ */ new Error(`DevTools cli exited unexpectedly with code ${code ?? "null"}${signal ? ` and signal ${signal}` : ""}`);
|
|
1036
1114
|
});
|
|
1037
1115
|
child.unref();
|
|
1038
1116
|
} catch (error) {
|
|
1039
1117
|
processError = error;
|
|
1040
1118
|
}
|
|
1041
1119
|
let miniProgram = null;
|
|
1120
|
+
let lastConnectError = null;
|
|
1042
1121
|
await waitUntil(async () => {
|
|
1043
1122
|
try {
|
|
1044
|
-
if (processError
|
|
1045
|
-
|
|
1123
|
+
if (processError) return true;
|
|
1124
|
+
const candidate = await this.connectTool({ wsEndpoint: `ws://127.0.0.1:${port}` });
|
|
1125
|
+
try {
|
|
1126
|
+
await candidate.checkVersion();
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
candidate.disconnect();
|
|
1129
|
+
lastConnectError = error;
|
|
1130
|
+
if (isExtensionContextInvalidatedError(error)) return false;
|
|
1131
|
+
throw error;
|
|
1132
|
+
}
|
|
1133
|
+
miniProgram = candidate;
|
|
1046
1134
|
return true;
|
|
1047
|
-
} catch {
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
lastConnectError = error;
|
|
1048
1137
|
return false;
|
|
1049
1138
|
}
|
|
1050
1139
|
}, timeout, 1e3);
|
|
1051
1140
|
if (!miniProgram) {
|
|
1052
1141
|
if (processError) throw new Error("Failed to launch wechat web devTools, please make sure cliPath is correctly specified");
|
|
1053
|
-
if (
|
|
1142
|
+
if (lastConnectError) throw lastConnectError;
|
|
1143
|
+
if (processExitCode !== null || processSignal) throw new Error("Failed to launch wechat web devTools, please make sure http port is open");
|
|
1054
1144
|
throw new Error("Failed connecting to devtools websocket endpoint");
|
|
1055
1145
|
}
|
|
1056
1146
|
const resolvedMiniProgram = miniProgram;
|
|
1057
|
-
|
|
1147
|
+
Reflect.set(resolvedMiniProgram, "__WEAPP_VITE_SESSION_METADATA", {
|
|
1148
|
+
port,
|
|
1149
|
+
projectPath,
|
|
1150
|
+
wsEndpoint: `ws://127.0.0.1:${port}`
|
|
1151
|
+
});
|
|
1058
1152
|
await sleep$1(5e3);
|
|
1059
1153
|
return resolvedMiniProgram;
|
|
1060
1154
|
}
|