weapp-ide-cli 6.1.9 → 6.3.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.md +74 -0
- package/dist/automator-BOe32upt.js +822 -0
- package/dist/automator-VSjgHujY.js +1 -0
- package/dist/automator-session-VOB03glx.js +186 -0
- package/dist/automatorProject-DjYLqMj6.js +881 -0
- package/dist/{cli-2sY1sAVq.js → cli-CFlqFpIw.js} +1552 -74
- package/dist/cli.js +3 -3
- package/dist/commands-DmirGBy4.js +1 -0
- package/dist/commands-mslf7-sP.js +529 -0
- package/dist/devtoolsProjectOwnership-BMfSAg4n.js +2022 -0
- package/dist/index.d.ts +420 -76
- package/dist/index.js +8 -5
- package/dist/managed-B04Xo9U_.js +264 -0
- package/dist/run-mcp-83-VKZVA.js +1 -0
- package/dist/{run-mcp-LqaWlQJ_.js → run-mcp-Ni139Z5C.js} +28 -9
- package/package.json +13 -12
- package/dist/automator-session-fbanL2eW.js +0 -1362
- package/dist/commands-DpMG1w04.js +0 -2
- package/dist/commands-__RJ552L.js +0 -1359
- package/dist/run-mcp-C2SQ_hRJ.js +0 -2
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as parse } from "./cli-
|
|
1
|
+
import { M as logger } from "./automatorProject-DjYLqMj6.js";
|
|
2
|
+
import { n as parse } from "./cli-CFlqFpIw.js";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
//#region src/cli.ts
|
|
5
5
|
const argv = process.argv.slice(2);
|
|
6
6
|
try {
|
|
7
7
|
await parse(argv);
|
|
8
8
|
} catch (err) {
|
|
9
|
-
|
|
9
|
+
logger.error(err);
|
|
10
10
|
if (typeof err?.exitCode === "number") process.exitCode = err.exitCode;
|
|
11
11
|
else if (typeof err?.code === "number") process.exitCode = err.code;
|
|
12
12
|
else process.exitCode = 1;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { h as takeScreenshot } from "./commands-mslf7-sP.js";
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { M as logger, j as colors } from "./automatorProject-DjYLqMj6.js";
|
|
2
|
+
import { M as i18nText } from "./automator-BOe32upt.js";
|
|
3
|
+
import { l as withMiniProgram } from "./automator-session-VOB03glx.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
import { PNG } from "pngjs";
|
|
8
|
+
//#region src/cli/fullPageScreenshot.ts
|
|
9
|
+
const MAX_FULL_PAGE_CAPTURES = 200;
|
|
10
|
+
const DEVTOOLS_INSPECTEE_REFERENCE_ERROR = /__inspectee__ is not defined|Can't find variable:\s*__inspectee__/i;
|
|
11
|
+
function decodeScreenshotBuffer(raw) {
|
|
12
|
+
if (raw === void 0) throw new Error("Failed to capture screenshot");
|
|
13
|
+
const buffer = typeof raw === "string" ? Buffer.from(raw, "base64") : Buffer.from(raw);
|
|
14
|
+
if (buffer.length === 0) throw new Error("Failed to capture screenshot");
|
|
15
|
+
return buffer;
|
|
16
|
+
}
|
|
17
|
+
function toPositiveNumber(value) {
|
|
18
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
19
|
+
}
|
|
20
|
+
function isDevtoolsInspecteeReferenceError(error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
return DEVTOOLS_INSPECTEE_REFERENCE_ERROR.test(message);
|
|
23
|
+
}
|
|
24
|
+
async function captureViewportScreenshotBuffer(miniProgram, timeoutMs, runWithTimeout, screenshotTimeoutMessage) {
|
|
25
|
+
return decodeScreenshotBuffer(await runWithTimeout(miniProgram.screenshot({ timeout: timeoutMs }), timeoutMs, screenshotTimeoutMessage, "DEVTOOLS_SCREENSHOT_TIMEOUT"));
|
|
26
|
+
}
|
|
27
|
+
function resolveScale(pageWidth, viewportHeight, png) {
|
|
28
|
+
if (pageWidth && pageWidth > 0) return png.width / pageWidth;
|
|
29
|
+
return png.height / viewportHeight;
|
|
30
|
+
}
|
|
31
|
+
function cropPngRows(source, startRow, rowCount) {
|
|
32
|
+
const cropped = new PNG({
|
|
33
|
+
width: source.width,
|
|
34
|
+
height: rowCount
|
|
35
|
+
});
|
|
36
|
+
const bytesPerRow = source.width * 4;
|
|
37
|
+
for (let row = 0; row < rowCount; row += 1) {
|
|
38
|
+
const sourceStart = (startRow + row) * bytesPerRow;
|
|
39
|
+
const sourceEnd = sourceStart + bytesPerRow;
|
|
40
|
+
cropped.data.set(source.data.subarray(sourceStart, sourceEnd), row * bytesPerRow);
|
|
41
|
+
}
|
|
42
|
+
return cropped;
|
|
43
|
+
}
|
|
44
|
+
function areRowsVisuallyEqual(first, second, firstRow, secondRow) {
|
|
45
|
+
const tolerance = 6;
|
|
46
|
+
const requiredEqualRatio = .98;
|
|
47
|
+
let equalPixels = 0;
|
|
48
|
+
for (let column = 0; column < first.width; column += 1) {
|
|
49
|
+
const firstIndex = (firstRow * first.width + column) * 4;
|
|
50
|
+
const secondIndex = (secondRow * second.width + column) * 4;
|
|
51
|
+
if (Math.abs(first.data[firstIndex] - second.data[secondIndex]) <= tolerance && Math.abs(first.data[firstIndex + 1] - second.data[secondIndex + 1]) <= tolerance && Math.abs(first.data[firstIndex + 2] - second.data[secondIndex + 2]) <= tolerance && Math.abs(first.data[firstIndex + 3] - second.data[secondIndex + 3]) <= tolerance) equalPixels += 1;
|
|
52
|
+
}
|
|
53
|
+
return equalPixels / first.width >= requiredEqualRatio;
|
|
54
|
+
}
|
|
55
|
+
function detectFixedBottomRows(first, second, scale) {
|
|
56
|
+
if (first.width !== second.width || first.height !== second.height) return 0;
|
|
57
|
+
const maxFixedRows = Math.floor(first.height * .45);
|
|
58
|
+
const minFixedRows = Math.max(8, Math.round(8 * scale));
|
|
59
|
+
let fixedRows = 0;
|
|
60
|
+
for (let offset = 0; offset < maxFixedRows; offset += 1) {
|
|
61
|
+
if (!areRowsVisuallyEqual(first, second, first.height - 1 - offset, second.height - 1 - offset)) break;
|
|
62
|
+
fixedRows += 1;
|
|
63
|
+
}
|
|
64
|
+
return fixedRows >= minFixedRows ? fixedRows : 0;
|
|
65
|
+
}
|
|
66
|
+
async function restoreScrollPosition(miniProgram, page, scrollTop) {
|
|
67
|
+
try {
|
|
68
|
+
await miniProgram.pageScrollTo(scrollTop);
|
|
69
|
+
await page.waitFor(150);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (!isDevtoolsInspecteeReferenceError(error)) throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function readRuntimeViewportMetrics(miniProgram) {
|
|
75
|
+
if (typeof miniProgram.evaluate !== "function") return;
|
|
76
|
+
const metrics = await miniProgram.evaluate(() => {
|
|
77
|
+
const wxLike = typeof wx === "undefined" ? void 0 : wx;
|
|
78
|
+
const systemInfo = wxLike?.getWindowInfo?.() ?? wxLike?.getSystemInfoSync?.();
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
if (!wxLike?.createSelectorQuery) {
|
|
81
|
+
resolve({
|
|
82
|
+
pageHeight: void 0,
|
|
83
|
+
pageWidth: systemInfo?.windowWidth,
|
|
84
|
+
scrollTop: void 0,
|
|
85
|
+
viewportHeight: systemInfo?.windowHeight
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
wxLike.createSelectorQuery().selectViewport().scrollOffset((viewport) => {
|
|
90
|
+
resolve({
|
|
91
|
+
pageHeight: viewport?.scrollHeight,
|
|
92
|
+
pageWidth: viewport?.scrollWidth ?? systemInfo?.windowWidth,
|
|
93
|
+
scrollTop: viewport?.scrollTop,
|
|
94
|
+
viewportHeight: systemInfo?.windowHeight
|
|
95
|
+
});
|
|
96
|
+
}).exec();
|
|
97
|
+
});
|
|
98
|
+
}).catch(() => void 0);
|
|
99
|
+
if (!metrics || typeof metrics !== "object") return;
|
|
100
|
+
const raw = metrics;
|
|
101
|
+
return {
|
|
102
|
+
pageHeight: toPositiveNumber(raw.pageHeight),
|
|
103
|
+
pageWidth: toPositiveNumber(raw.pageWidth),
|
|
104
|
+
scrollTop: typeof raw.scrollTop === "number" && Number.isFinite(raw.scrollTop) ? Math.max(raw.scrollTop, 0) : void 0,
|
|
105
|
+
viewportHeight: toPositiveNumber(raw.viewportHeight)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function readPageSize(page) {
|
|
109
|
+
return await page.size().catch(() => null);
|
|
110
|
+
}
|
|
111
|
+
async function readScrollTop(miniProgram, page) {
|
|
112
|
+
const runtimeMetrics = await readRuntimeViewportMetrics(miniProgram);
|
|
113
|
+
if (runtimeMetrics?.scrollTop !== void 0) return runtimeMetrics.scrollTop;
|
|
114
|
+
return typeof page.scrollTop === "function" ? toPositiveNumber(await page.scrollTop().catch(() => void 0)) ?? 0 : 0;
|
|
115
|
+
}
|
|
116
|
+
async function captureScrolledPng(miniProgram, page, scrollTop, timeoutMs, runWithTimeout, screenshotTimeoutMessage) {
|
|
117
|
+
await miniProgram.pageScrollTo(scrollTop);
|
|
118
|
+
await page.waitFor(150);
|
|
119
|
+
const rawScreenshot = await runWithTimeout(miniProgram.screenshot({ timeout: timeoutMs }), timeoutMs, screenshotTimeoutMessage, "DEVTOOLS_SCREENSHOT_TIMEOUT");
|
|
120
|
+
const runtimeMetrics = await readRuntimeViewportMetrics(miniProgram);
|
|
121
|
+
const actualScrollTop = runtimeMetrics?.scrollTop ?? (typeof page.scrollTop === "function" ? toPositiveNumber(await page.scrollTop().catch(() => void 0)) ?? scrollTop : scrollTop);
|
|
122
|
+
const pageSize = runtimeMetrics?.pageHeight ? void 0 : await readPageSize(page);
|
|
123
|
+
return {
|
|
124
|
+
pageHeight: runtimeMetrics?.pageHeight ?? toPositiveNumber(pageSize?.height),
|
|
125
|
+
png: PNG.sync.read(decodeScreenshotBuffer(rawScreenshot)),
|
|
126
|
+
scrollTop: actualScrollTop
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* @description 通过多次滚动和拼接生成整页长截图。
|
|
131
|
+
*/
|
|
132
|
+
async function captureFullPageScreenshotBuffer(options) {
|
|
133
|
+
const { miniProgram, timeoutMs, runWithTimeout, screenshotTimeoutMessage } = options;
|
|
134
|
+
const page = await miniProgram.currentPage();
|
|
135
|
+
const runtimeMetrics = await readRuntimeViewportMetrics(miniProgram);
|
|
136
|
+
const pageSize = runtimeMetrics?.pageHeight && runtimeMetrics.pageWidth ? void 0 : await readPageSize(page);
|
|
137
|
+
const systemInfo = runtimeMetrics?.viewportHeight ? void 0 : await miniProgram.systemInfo().catch((error) => {
|
|
138
|
+
if (isDevtoolsInspecteeReferenceError(error)) return;
|
|
139
|
+
throw error;
|
|
140
|
+
});
|
|
141
|
+
const initialScrollTop = runtimeMetrics?.scrollTop ?? await readScrollTop(miniProgram, page);
|
|
142
|
+
const pageWidth = runtimeMetrics?.pageWidth ?? toPositiveNumber(pageSize?.width);
|
|
143
|
+
const pageHeight = runtimeMetrics?.pageHeight ?? toPositiveNumber(pageSize?.height);
|
|
144
|
+
const viewportHeight = runtimeMetrics?.viewportHeight ?? toPositiveNumber(systemInfo?.windowHeight);
|
|
145
|
+
if (!pageHeight || !viewportHeight) return await captureViewportScreenshotBuffer(miniProgram, timeoutMs, runWithTimeout, screenshotTimeoutMessage);
|
|
146
|
+
const segments = [];
|
|
147
|
+
const capturesByRequestedScrollTop = /* @__PURE__ */ new Map();
|
|
148
|
+
let coveredUntil = 0;
|
|
149
|
+
let scale = 1;
|
|
150
|
+
let fixedBottomRows = 0;
|
|
151
|
+
let contentViewportHeight = viewportHeight;
|
|
152
|
+
let latestPageHeight = pageHeight;
|
|
153
|
+
try {
|
|
154
|
+
const firstCapture = await captureScrolledPng(miniProgram, page, 0, timeoutMs, runWithTimeout, screenshotTimeoutMessage);
|
|
155
|
+
const firstPng = firstCapture.png;
|
|
156
|
+
capturesByRequestedScrollTop.set(0, firstCapture);
|
|
157
|
+
scale = resolveScale(pageWidth, viewportHeight, firstPng);
|
|
158
|
+
latestPageHeight = firstCapture.pageHeight ?? latestPageHeight;
|
|
159
|
+
if (pageHeight > viewportHeight) {
|
|
160
|
+
const probeScrollTop = Math.min(viewportHeight, Math.max(pageHeight - viewportHeight, 1));
|
|
161
|
+
const probeCapture = await captureScrolledPng(miniProgram, page, probeScrollTop, timeoutMs, runWithTimeout, screenshotTimeoutMessage);
|
|
162
|
+
const probePng = probeCapture.png;
|
|
163
|
+
capturesByRequestedScrollTop.set(probeScrollTop, probeCapture);
|
|
164
|
+
latestPageHeight = probeCapture.pageHeight ?? latestPageHeight;
|
|
165
|
+
fixedBottomRows = detectFixedBottomRows(firstPng, probePng, scale);
|
|
166
|
+
if (fixedBottomRows > 0) {
|
|
167
|
+
const screenshotCssHeight = firstPng.height / scale;
|
|
168
|
+
const fixedBottomCss = fixedBottomRows / scale;
|
|
169
|
+
const fixedRowsOutsideViewportCss = Math.max(screenshotCssHeight - viewportHeight, 0);
|
|
170
|
+
const fixedRowsInsideViewportCss = Math.max(fixedBottomCss - fixedRowsOutsideViewportCss, 0);
|
|
171
|
+
contentViewportHeight = Math.max(1, viewportHeight - fixedRowsInsideViewportCss);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
let requestedScrollTop = 0;
|
|
175
|
+
for (let captureIndex = 0; captureIndex < MAX_FULL_PAGE_CAPTURES; captureIndex += 1) {
|
|
176
|
+
const capture = capturesByRequestedScrollTop.get(requestedScrollTop) ?? await captureScrolledPng(miniProgram, page, requestedScrollTop, timeoutMs, runWithTimeout, screenshotTimeoutMessage);
|
|
177
|
+
capturesByRequestedScrollTop.set(requestedScrollTop, capture);
|
|
178
|
+
const { png, scrollTop } = capture;
|
|
179
|
+
latestPageHeight = capture.pageHeight ?? latestPageHeight;
|
|
180
|
+
const visibleEnd = Math.min(scrollTop + contentViewportHeight, latestPageHeight);
|
|
181
|
+
const cropTopCss = Math.max(coveredUntil - scrollTop, 0);
|
|
182
|
+
const segmentHeightCss = Math.max(visibleEnd - scrollTop - cropTopCss, 0);
|
|
183
|
+
const isAtBottom = visibleEnd >= latestPageHeight - 1;
|
|
184
|
+
if (segmentHeightCss > 0) {
|
|
185
|
+
const contentRows = Math.max(png.height - fixedBottomRows, 1);
|
|
186
|
+
const cropTopRows = Math.min(Math.max(Math.round(cropTopCss * scale), 0), contentRows);
|
|
187
|
+
const segmentRows = Math.min(Math.max(Math.round(segmentHeightCss * scale), 1), contentRows - cropTopRows);
|
|
188
|
+
if (segmentRows > 0) segments.push(cropPngRows(png, cropTopRows, segmentRows));
|
|
189
|
+
}
|
|
190
|
+
coveredUntil = Math.max(coveredUntil, visibleEnd);
|
|
191
|
+
if (isAtBottom) {
|
|
192
|
+
if (fixedBottomRows > 0) segments.push(cropPngRows(png, png.height - fixedBottomRows, fixedBottomRows));
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
if (visibleEnd <= scrollTop || coveredUntil <= requestedScrollTop) {
|
|
196
|
+
if (fixedBottomRows > 0) segments.push(cropPngRows(png, png.height - fixedBottomRows, fixedBottomRows));
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
requestedScrollTop = coveredUntil;
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (!isDevtoolsInspecteeReferenceError(error)) throw error;
|
|
203
|
+
return await captureViewportScreenshotBuffer(miniProgram, timeoutMs, runWithTimeout, screenshotTimeoutMessage);
|
|
204
|
+
} finally {
|
|
205
|
+
await restoreScrollPosition(miniProgram, page, initialScrollTop);
|
|
206
|
+
}
|
|
207
|
+
if (segments.length === 0) throw new Error("Failed to capture screenshot");
|
|
208
|
+
const width = segments[0].width;
|
|
209
|
+
const height = segments.reduce((sum, segment) => sum + segment.height, 0);
|
|
210
|
+
const merged = new PNG({
|
|
211
|
+
width,
|
|
212
|
+
height
|
|
213
|
+
});
|
|
214
|
+
const bytesPerRow = width * 4;
|
|
215
|
+
let offsetY = 0;
|
|
216
|
+
for (const segment of segments) {
|
|
217
|
+
if (segment.width !== width) throw new Error("Full-page screenshots have inconsistent widths");
|
|
218
|
+
for (let row = 0; row < segment.height; row += 1) {
|
|
219
|
+
const sourceStart = row * bytesPerRow;
|
|
220
|
+
const sourceEnd = sourceStart + bytesPerRow;
|
|
221
|
+
merged.data.set(segment.data.subarray(sourceStart, sourceEnd), (offsetY + row) * bytesPerRow);
|
|
222
|
+
}
|
|
223
|
+
offsetY += segment.height;
|
|
224
|
+
}
|
|
225
|
+
return PNG.sync.write(merged);
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/cli/commands.ts
|
|
229
|
+
function createTimeoutError(message, code) {
|
|
230
|
+
const error = new Error(message);
|
|
231
|
+
error.code = code;
|
|
232
|
+
return error;
|
|
233
|
+
}
|
|
234
|
+
function normalizePagePath(page) {
|
|
235
|
+
return page.startsWith("/") ? page : `/${page}`;
|
|
236
|
+
}
|
|
237
|
+
function sleep(ms) {
|
|
238
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
239
|
+
}
|
|
240
|
+
function withCommandTimeout(task, timeoutMs, message, code) {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
const timeout = setTimeout(() => {
|
|
243
|
+
reject(createTimeoutError(message, code));
|
|
244
|
+
}, timeoutMs);
|
|
245
|
+
task.then((value) => {
|
|
246
|
+
clearTimeout(timeout);
|
|
247
|
+
resolve(value);
|
|
248
|
+
}).catch((error) => {
|
|
249
|
+
clearTimeout(timeout);
|
|
250
|
+
reject(error);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function createCurrentPageTimeoutMessage(commandTimeout) {
|
|
255
|
+
return i18nText(`当前页面请求在 ${commandTimeout}ms 内未收到 DevTools 回包,请确认当前打开的是目标项目;若之前跑过其他 e2e / screenshot 任务,关闭多余的微信开发者工具窗口,或结束残留的 \`wechatwebdevtools cli auto --project ...\` 进程后重试。`, `Current page request did not receive a DevTools response within ${commandTimeout}ms. Please confirm the current DevTools window is the target project. If you recently ran other e2e / screenshot tasks, close extra windows or stop stale \`wechatwebdevtools cli auto --project ...\` processes and retry.`);
|
|
256
|
+
}
|
|
257
|
+
function isDevtoolsProtocolTimeoutError(error) {
|
|
258
|
+
if (!(error instanceof Error)) return false;
|
|
259
|
+
return error.message === "DEVTOOLS_PROTOCOL_TIMEOUT" || error.message.includes("DevTools did not respond to protocol method") || Reflect.get(error, "code") === "DEVTOOLS_PROTOCOL_TIMEOUT";
|
|
260
|
+
}
|
|
261
|
+
function isPageStackStaleError(error) {
|
|
262
|
+
return error instanceof Error && error.message.includes("page is not on top of page stack");
|
|
263
|
+
}
|
|
264
|
+
function isScreenshotNavigationTimeoutError(error) {
|
|
265
|
+
return error instanceof Error && Reflect.get(error, "code") === "DEVTOOLS_SCREENSHOT_NAVIGATION_TIMEOUT";
|
|
266
|
+
}
|
|
267
|
+
function isScreenshotRequestTimeoutError(error) {
|
|
268
|
+
if (!(error instanceof Error)) return false;
|
|
269
|
+
return error.message === "DEVTOOLS_SCREENSHOT_TIMEOUT" || Reflect.get(error, "code") === "DEVTOOLS_SCREENSHOT_TIMEOUT";
|
|
270
|
+
}
|
|
271
|
+
function toScreenshotBuffer(screenshot) {
|
|
272
|
+
if (screenshot === void 0) return Buffer.alloc(0);
|
|
273
|
+
return typeof screenshot === "string" ? Buffer.from(screenshot, "base64") : Buffer.from(screenshot);
|
|
274
|
+
}
|
|
275
|
+
async function reLaunchForScreenshot(miniProgram, page) {
|
|
276
|
+
const routeOptions = { url: page };
|
|
277
|
+
if (typeof miniProgram.callWxMethod === "function") await miniProgram.callWxMethod("reLaunch", routeOptions);
|
|
278
|
+
else await miniProgram.reLaunch(page);
|
|
279
|
+
}
|
|
280
|
+
async function runRouteCommand(options, startMessage, successMessage, action) {
|
|
281
|
+
await withMiniProgram(options, async (miniProgram) => {
|
|
282
|
+
logger.info(startMessage);
|
|
283
|
+
await action(miniProgram);
|
|
284
|
+
logger.success(successMessage);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function toPageSnapshot(page) {
|
|
288
|
+
return {
|
|
289
|
+
path: page.path ?? "",
|
|
290
|
+
query: page.query
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function printStructuredResult(result, json, title) {
|
|
294
|
+
if (json) {
|
|
295
|
+
console.log(JSON.stringify(result, null, 2));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
logger.info(title);
|
|
299
|
+
console.log(JSON.stringify(result, null, 2));
|
|
300
|
+
}
|
|
301
|
+
async function requireElement(page, selector, timeoutMs = 5e3) {
|
|
302
|
+
const startedAt = Date.now();
|
|
303
|
+
let lastError;
|
|
304
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
305
|
+
try {
|
|
306
|
+
const element = await page.$(selector, { fallback: false });
|
|
307
|
+
if (element) return element;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
lastError = error;
|
|
310
|
+
}
|
|
311
|
+
await sleep(160);
|
|
312
|
+
}
|
|
313
|
+
const reason = lastError instanceof Error ? ` ${lastError.message}` : "";
|
|
314
|
+
throw new Error(i18nText(`未找到元素: ${selector}${reason}`, `Element not found: ${selector}${reason}`));
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* @description 执行保留栈页面跳转。
|
|
318
|
+
*/
|
|
319
|
+
async function navigateTo(options) {
|
|
320
|
+
await runRouteCommand(options, i18nText(`正在跳转到 ${colors.cyan(options.url)}...`, `Navigating to ${colors.cyan(options.url)}...`), i18nText(`已跳转到 ${colors.cyan(options.url)}`, `Navigated to ${colors.cyan(options.url)}`), (miniProgram) => miniProgram.navigateTo(options.url));
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* @description 执行关闭当前页的重定向。
|
|
324
|
+
*/
|
|
325
|
+
async function redirectTo(options) {
|
|
326
|
+
await runRouteCommand(options, i18nText(`正在重定向到 ${colors.cyan(options.url)}...`, `Redirecting to ${colors.cyan(options.url)}...`), i18nText(`已重定向到 ${colors.cyan(options.url)}`, `Redirected to ${colors.cyan(options.url)}`), (miniProgram) => miniProgram.redirectTo(options.url));
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* @description 返回上一页。
|
|
330
|
+
*/
|
|
331
|
+
async function navigateBack(options) {
|
|
332
|
+
await runRouteCommand(options, i18nText("正在返回上一页...", "Navigating back..."), i18nText("已返回上一页", "Navigated back"), (miniProgram) => miniProgram.navigateBack());
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* @description 重启到指定页面。
|
|
336
|
+
*/
|
|
337
|
+
async function reLaunch(options) {
|
|
338
|
+
await runRouteCommand(options, i18nText(`正在重启到 ${colors.cyan(options.url)}...`, `Relaunching to ${colors.cyan(options.url)}...`), i18nText(`已重启到 ${colors.cyan(options.url)}`, `Relaunched to ${colors.cyan(options.url)}`), (miniProgram) => miniProgram.reLaunch(options.url));
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* @description 切换到 tabBar 页面。
|
|
342
|
+
*/
|
|
343
|
+
async function switchTab(options) {
|
|
344
|
+
await runRouteCommand(options, i18nText(`正在切换 tab 到 ${colors.cyan(options.url)}...`, `Switching tab to ${colors.cyan(options.url)}...`), i18nText(`已切换 tab 到 ${colors.cyan(options.url)}`, `Switched tab to ${colors.cyan(options.url)}`), (miniProgram) => miniProgram.switchTab(options.url));
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* @description 获取页面栈。
|
|
348
|
+
*/
|
|
349
|
+
async function pageStack(options) {
|
|
350
|
+
return await withMiniProgram(options, async (miniProgram) => {
|
|
351
|
+
const result = (await miniProgram.pageStack()).map(toPageSnapshot);
|
|
352
|
+
printStructuredResult(result, options.json, i18nText("页面栈:", "Page stack:"));
|
|
353
|
+
return result;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* @description 获取当前页面信息。
|
|
358
|
+
*/
|
|
359
|
+
async function currentPage(options) {
|
|
360
|
+
return await withMiniProgram(options, async (miniProgram) => {
|
|
361
|
+
const commandTimeout = options.timeout ?? 3e4;
|
|
362
|
+
const result = toPageSnapshot(await withCommandTimeout(miniProgram.currentPage(), commandTimeout, createCurrentPageTimeoutMessage(commandTimeout), "DEVTOOLS_PROTOCOL_TIMEOUT"));
|
|
363
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
364
|
+
else logger.info(i18nText(`当前页面: ${result.path}${result.query ? ` ${JSON.stringify(result.query)}` : ""}`, `Current page: ${result.path}${result.query ? ` ${JSON.stringify(result.query)}` : ""}`));
|
|
365
|
+
return result;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* @description 获取系统信息。
|
|
370
|
+
*/
|
|
371
|
+
async function systemInfo(options) {
|
|
372
|
+
return await withMiniProgram(options, async (miniProgram) => {
|
|
373
|
+
const result = await miniProgram.systemInfo();
|
|
374
|
+
printStructuredResult(result, options.json, i18nText("系统信息:", "System info:"));
|
|
375
|
+
return result;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* @description 获取当前页面数据。
|
|
380
|
+
*/
|
|
381
|
+
async function pageData(options) {
|
|
382
|
+
return await withMiniProgram(options, async (miniProgram) => {
|
|
383
|
+
const result = await (await miniProgram.currentPage()).data(options.path);
|
|
384
|
+
printStructuredResult(result, options.json, i18nText("页面数据:", "Page data:"));
|
|
385
|
+
return result;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* @description 点击页面元素。
|
|
390
|
+
*/
|
|
391
|
+
async function tap(options) {
|
|
392
|
+
await withMiniProgram(options, async (miniProgram) => {
|
|
393
|
+
logger.info(i18nText(`正在点击元素 ${colors.cyan(options.selector)}...`, `Tapping element ${colors.cyan(options.selector)}...`));
|
|
394
|
+
const commandTimeout = options.timeout ?? 3e4;
|
|
395
|
+
const tapCurrentPageElement = async () => {
|
|
396
|
+
await (await requireElement(await miniProgram.currentPage(), options.selector, commandTimeout)).tap();
|
|
397
|
+
};
|
|
398
|
+
try {
|
|
399
|
+
await tapCurrentPageElement();
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (!isPageStackStaleError(error)) throw error;
|
|
402
|
+
logger.info(i18nText("页面栈已切换,正在重新获取顶部页面并重试点击...", "Page stack changed. Refreshing the top page and retrying the tap..."));
|
|
403
|
+
await tapCurrentPageElement();
|
|
404
|
+
}
|
|
405
|
+
logger.success(i18nText(`已点击元素 ${colors.cyan(options.selector)}`, `Tapped element ${colors.cyan(options.selector)}`));
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* @description 向页面元素输入文本。
|
|
410
|
+
*/
|
|
411
|
+
async function input(options) {
|
|
412
|
+
await withMiniProgram(options, async (miniProgram) => {
|
|
413
|
+
logger.info(i18nText(`正在向 ${colors.cyan(options.selector)} 输入 "${colors.cyan(options.value)}"...`, `Inputting "${colors.cyan(options.value)}" into ${colors.cyan(options.selector)}...`));
|
|
414
|
+
const commandTimeout = options.timeout ?? 3e4;
|
|
415
|
+
const element = await requireElement(await miniProgram.currentPage(), options.selector, commandTimeout);
|
|
416
|
+
if (typeof element.input !== "function") throw new TypeError(i18nText(`元素不支持输入: ${options.selector}`, `Element does not support input: ${options.selector}`));
|
|
417
|
+
await element.input(options.value);
|
|
418
|
+
logger.success(i18nText(`已向 ${colors.cyan(options.selector)} 输入 "${colors.cyan(options.value)}"`, `Inputted "${colors.cyan(options.value)}" into ${colors.cyan(options.selector)}`));
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* @description 滚动页面到指定位置。
|
|
423
|
+
*/
|
|
424
|
+
async function scrollTo(options) {
|
|
425
|
+
await withMiniProgram(options, async (miniProgram) => {
|
|
426
|
+
logger.info(i18nText(`正在滚动到位置 ${colors.cyan(String(options.scrollTop))}...`, `Scrolling to position ${colors.cyan(String(options.scrollTop))}...`));
|
|
427
|
+
await miniProgram.pageScrollTo(options.scrollTop);
|
|
428
|
+
logger.success(i18nText(`已滚动到位置 ${colors.cyan(String(options.scrollTop))}`, `Scrolled to position ${colors.cyan(String(options.scrollTop))}`));
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* @description 执行体验评分审计。
|
|
433
|
+
*/
|
|
434
|
+
async function audit(options) {
|
|
435
|
+
return await withMiniProgram(options, async (miniProgram) => {
|
|
436
|
+
logger.info(i18nText("正在执行体验审计...", "Running experience audit..."));
|
|
437
|
+
const result = await miniProgram.stopAudits();
|
|
438
|
+
if (options.outputPath) {
|
|
439
|
+
await fs.mkdir(path.dirname(options.outputPath), { recursive: true });
|
|
440
|
+
await fs.writeFile(options.outputPath, JSON.stringify(result, null, 2));
|
|
441
|
+
logger.success(i18nText(`审计报告已保存到 ${colors.cyan(options.outputPath)}`, `Audit report saved to ${colors.cyan(options.outputPath)}`));
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
console.log(JSON.stringify(result, null, 2));
|
|
445
|
+
return result;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* @description 捕获当前页面截图并返回二进制内容。
|
|
450
|
+
*/
|
|
451
|
+
async function captureScreenshotBuffer(options) {
|
|
452
|
+
return await withMiniProgram(options, async (miniProgram) => {
|
|
453
|
+
const commandTimeout = options.timeout ?? 3e4;
|
|
454
|
+
const screenshotTimeoutMessage = i18nText(`截图请求在 ${commandTimeout}ms 内未收到 DevTools 回包,请检查当前微信开发者工具是否仍停留在目标项目;若近期执行过其他 e2e / screenshot 任务,关闭多余窗口并清理残留 automator 会话后重试。`, `Screenshot request did not receive a DevTools response within ${commandTimeout}ms. Check that the current Wechat DevTools window is still the target project. If you recently ran other e2e or screenshot tasks, close extra windows and clean up stale automator sessions before retrying.`);
|
|
455
|
+
if (!options.miniProgram) logger.info(i18nText(`正在连接 DevTools:${colors.cyan(options.projectPath)}...`, `Connecting to DevTools at ${colors.cyan(options.projectPath)}...`));
|
|
456
|
+
if (options.page) {
|
|
457
|
+
const normalizedPage = normalizePagePath(options.page);
|
|
458
|
+
logger.info(i18nText(`正在跳转页面 ${colors.cyan(normalizedPage)}...`, `Navigating to page ${colors.cyan(normalizedPage)}...`));
|
|
459
|
+
try {
|
|
460
|
+
await reLaunchForScreenshot(miniProgram, normalizedPage);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
if (!isDevtoolsProtocolTimeoutError(error)) throw error;
|
|
463
|
+
logger.warn(i18nText(`截图前跳转页面 ${normalizedPage} 超时,将重建会话后截取当前页面。`, `Timed out navigating to ${normalizedPage} before screenshot. Rebuilding the session and capturing the current page.`));
|
|
464
|
+
throw createTimeoutError(i18nText(`截图前跳转页面 ${normalizedPage} 超时`, `Timed out navigating to ${normalizedPage} before screenshot`), "DEVTOOLS_SCREENSHOT_NAVIGATION_TIMEOUT");
|
|
465
|
+
}
|
|
466
|
+
if (options.fullPage) await sleep(1e3);
|
|
467
|
+
}
|
|
468
|
+
if (options.fullPage) {
|
|
469
|
+
logger.info(i18nText("正在生成整页长截图...", "Capturing full-page screenshot..."));
|
|
470
|
+
return await captureFullPageScreenshotBuffer({
|
|
471
|
+
miniProgram,
|
|
472
|
+
timeoutMs: commandTimeout,
|
|
473
|
+
runWithTimeout: withCommandTimeout,
|
|
474
|
+
screenshotTimeoutMessage
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
logger.info(i18nText("正在截图...", "Taking screenshot..."));
|
|
478
|
+
const buffer = toScreenshotBuffer(await withCommandTimeout(miniProgram.screenshot({ timeout: commandTimeout }), commandTimeout, screenshotTimeoutMessage, "DEVTOOLS_SCREENSHOT_TIMEOUT"));
|
|
479
|
+
if (buffer.length === 0) throw new Error(i18nText("截图失败", "Failed to capture screenshot"));
|
|
480
|
+
return buffer;
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* @description 获取当前小程序截图。
|
|
485
|
+
*/
|
|
486
|
+
async function takeScreenshot(options) {
|
|
487
|
+
let nextOptions = options;
|
|
488
|
+
let screenshotBuffer;
|
|
489
|
+
let hasRetriedWithFreshSession = false;
|
|
490
|
+
while (true) try {
|
|
491
|
+
screenshotBuffer = await captureScreenshotBuffer(nextOptions);
|
|
492
|
+
break;
|
|
493
|
+
} catch (error) {
|
|
494
|
+
const isProtocolTimeout = error instanceof Error && error.message === "DEVTOOLS_PROTOCOL_TIMEOUT";
|
|
495
|
+
const isNavigationTimeout = isScreenshotNavigationTimeoutError(error);
|
|
496
|
+
const isScreenshotTimeout = isScreenshotRequestTimeoutError(error);
|
|
497
|
+
if (!Boolean(nextOptions.retryWithFreshSession !== false && !nextOptions.miniProgram && (isProtocolTimeout || isNavigationTimeout || isScreenshotTimeout) && !hasRetriedWithFreshSession)) throw error;
|
|
498
|
+
hasRetriedWithFreshSession = true;
|
|
499
|
+
logger.warn(i18nText("当前 DevTools 会话截图超时,正在改用全新自动化会话重试一次...", "The current DevTools session timed out while capturing screenshot. Retrying once with a fresh automation session..."));
|
|
500
|
+
const { page: _page, ...optionsWithoutPage } = nextOptions;
|
|
501
|
+
nextOptions = {
|
|
502
|
+
...isNavigationTimeout ? optionsWithoutPage : nextOptions,
|
|
503
|
+
preferOpenedSession: false,
|
|
504
|
+
sharedSession: false
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
if (!screenshotBuffer) throw new Error(i18nText("截图失败", "Failed to capture screenshot"));
|
|
508
|
+
const base64 = screenshotBuffer.toString("base64");
|
|
509
|
+
if (options.outputPath) {
|
|
510
|
+
await fs.mkdir(path.dirname(options.outputPath), { recursive: true });
|
|
511
|
+
await fs.writeFile(options.outputPath, screenshotBuffer);
|
|
512
|
+
logger.success(i18nText(`截图已保存到 ${colors.cyan(options.outputPath)}`, `Screenshot saved to ${colors.cyan(options.outputPath)}`));
|
|
513
|
+
return { path: options.outputPath };
|
|
514
|
+
}
|
|
515
|
+
return { base64 };
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* @description 开关远程调试。
|
|
519
|
+
*/
|
|
520
|
+
async function remote(options) {
|
|
521
|
+
const enable = options.enable ?? true;
|
|
522
|
+
await withMiniProgram(options, async (miniProgram) => {
|
|
523
|
+
logger.info(enable ? i18nText("正在开启远程调试...", "Enabling remote debugging...") : i18nText("正在关闭远程调试...", "Disabling remote debugging..."));
|
|
524
|
+
await miniProgram.remote(enable);
|
|
525
|
+
logger.success(enable ? i18nText("远程调试已开启", "Remote debugging enabled") : i18nText("远程调试已关闭", "Remote debugging disabled"));
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
//#endregion
|
|
529
|
+
export { navigateBack as a, pageStack as c, remote as d, scrollTo as f, tap as g, takeScreenshot as h, input as i, reLaunch as l, systemInfo as m, captureScreenshotBuffer as n, navigateTo as o, switchTab as p, currentPage as r, pageData as s, audit as t, redirectTo as u };
|