Loading...
Loading...
Use when working with WeChat mini-program automation (小程序自动化、自动化测试、E2E) via miniprogram-automator, especially for standalone Node scripts or Jest tests involving DevTools launch/connect, page navigation, waitFor, custom-component selectors, wx method mocking, console or exception listeners, screenshots, regression checks, or troubleshooting launch failures, connection timeouts, and element-not-found issues.
npx skill4agent add whinc/super-skills miniprogram-automationminiprogram-automatorautomator.launch({ projectPath })projectPathproject.config.jsonapp.jsondist/build/miniprogram//Applications/wechatwebdevtools.app/Contents/MacOS/cliC:\Program Files (x86)\Tencent\微信web开发者工具\cli.batcliPath0.12.00.5.xcli --auto <path>[error] { code: 31, message: "Error: Missing param 'project / appid'" }npm install miniprogram-automator@latest --save-dev0.12.xcli auto --project <path> --auto-port <port>automator.launch()/pages/home/indexreLaunchnavigateToswitchTabbeforeAll/afterAlllaunchconnect| 场景 | 推荐方式 |
|---|---|
| 全自动:脚本自己启动开发者工具 | |
| 开发者工具已开着,直接连 | CLI v2 + |
| 开发者工具已开着且自动化端口已就绪 | 直接 |
automator.launch()Failed to launch wechat web devToolstimeout120000miniProgram = await automator.launch({
cliPath: CLI_PATH,
projectPath: PROJECT_PATH,
timeout: 120000,
})automator.connect()connect()内部其实仍在使用旧版 CLI 格式(automator.launch()),在某些新版开发者工具上可能失败。如果遇到cli --auto <path>报错,改用这个方式通常更稳。launch()
const { spawnSync } = require('node:child_process')
const CLI_PATH = process.env.WECHAT_DEVTOOLS_CLI ||
'/Applications/wechatwebdevtools.app/Contents/MacOS/cli'
const PROJECT_PATH = process.env.MINIPROGRAM_PROJECT_PATH || '/path/to/project'
const AUTO_PORT = Number(process.env.WECHAT_AUTO_PORT || 9420)
// Step 1:用 CLI v2 开启自动化 ws 端口
// 若开发者工具 HTTP 服务端口不是默认值,需通过 --port 传入
function enableAutomation(httpPort) {
const args = ['auto', '--project', PROJECT_PATH, '--auto-port', String(AUTO_PORT)]
if (httpPort) args.push('--port', String(httpPort))
const result = spawnSync(CLI_PATH, args, { encoding: 'utf8', timeout: 20000 })
return { success: result.status === 0, output: (result.stdout || '') + (result.stderr || '') }
}
const { success, output } = enableAutomation()
if (!success) throw new Error(`CLI auto 命令失败:\n${output}`)
// Step 2:connect 到自动化 ws 端点
const miniProgram = await automator.connect({
wsEndpoint: `ws://127.0.0.1:${AUTO_PORT}`,
})--portautomator.connect()const miniProgram = await automator.connect({
wsEndpoint: 'ws://localhost:9420',
})connect()miniProgramclose()| 场景 | 建议 |
|---|---|
| 临时验证、单页面调试、做脚本工具 | 独立 Node.js 脚本 |
| 回归测试、多个 case、团队协作、CI | Jest |
await page.waitFor('稳定选择器')await page.waitFor(async () => 条件成立)await page.waitFor(数字毫秒)sleep(2000)waitForpage.waitFor()await page.waitFor('.page-title')
await page.waitFor(async () => (await page.$$('.loaded-item')).length > 0)
await page.waitFor(500)trueconst input = await page.$('form-panel input')const panel = await page.$('form-panel')
const input = await panel.$('input')page.$page.$$form-panel inputelement.$element.$$value()property('value')inputtextareaconst value = await input.value()
// 或
const value2 = await input.property('value')const data = await panel.data()data()mockWxMethodwx.requestwx.getLocationawait miniProgram.mockWxMethod('request', (options = {}) => {
const res = {
data: { code: 0, list: [{ id: 1, title: 'Mock Item A' }] },
statusCode: 200,
header: { 'content-type': 'application/json' },
cookies: [],
errMsg: 'request:ok',
}
Promise.resolve().then(() => {
if (typeof options.success === 'function') options.success(res)
if (typeof options.complete === 'function') options.complete(res)
})
return {
abort() {},
onHeadersReceived() {},
offHeadersReceived() {},
onChunkReceived() {},
offChunkReceived() {},
}
})await miniProgram.restoreWxMethod('request')mockWxMethodthis.originmockWxMethodevaluate()miniProgram.screenshot()await miniProgram.screenshot({ path: '/abs/path/to/file.png' }){ path }finallytry {
// 执行测试逻辑
} finally {
await miniProgram.restoreWxMethod('request').catch(() => {})
await miniProgram.close().catch(() => {})
}const automator = require('miniprogram-automator')
const path = require('node:path')
const fs = require('node:fs/promises')
const CLI_PATH = process.env.WECHAT_DEVTOOLS_CLI || '/Applications/wechatwebdevtools.app/Contents/MacOS/cli'
const PROJECT_PATH = process.env.MINIPROGRAM_PROJECT_PATH || '/absolute/path/to/devtools-project'
const TARGET_PAGE = '/pages/home/index' // 必须以 / 开头
const OUTPUT_DIR = path.resolve(process.cwd(), 'outputs')
async function main() {
let miniProgram
try {
await fs.mkdir(OUTPUT_DIR, { recursive: true })
// 前提:开发者工具已完全退出(Cmd+Q);首次运行需在界面确认信任项目
miniProgram = await automator.launch({
cliPath: CLI_PATH,
projectPath: PROJECT_PATH,
timeout: 120000, // 给首次启动 + 用户确认信任留足时间
})
const page = await miniProgram.reLaunch(TARGET_PAGE)
// 主同步手段优先用稳定选择器,固定等待只做短兜底
await page.waitFor('.page-title')
await page.waitFor(100)
const title = await page.$('.page-title')
const titleText = await title.text()
console.log('当前标题:', titleText)
await miniProgram.screenshot({
path: path.join(OUTPUT_DIR, 'current-page.png'),
})
console.log('截图完成')
} finally {
if (miniProgram) {
await miniProgram.close()
}
}
}
main().catch((error) => {
console.error(error)
process.exit(1)
})const automator = require('miniprogram-automator')
async function run() {
let miniProgram
try {
miniProgram = await automator.launch({
cliPath: process.env.WECHAT_DEVTOOLS_CLI || '/Applications/wechatwebdevtools.app/Contents/MacOS/cli',
projectPath: process.env.MINIPROGRAM_PROJECT_PATH || '/absolute/path/to/devtools-project',
})
const page = await miniProgram.reLaunch('/pages/form/index')
await page.waitFor(300)
const panel = await page.$('form-panel')
if (!panel) throw new Error('未找到 form-panel')
const input = await panel.$('input')
if (!input) throw new Error('未找到组件内 input')
await input.tap()
await input.input('13800138000')
await page.waitFor(100)
const value = await input.value()
if (value !== '13800138000') {
throw new Error(`输入框值不符合预期: ${value}`)
}
// panel 必须是自定义组件实例;原生组件或普通元素不适合用 data() 读取内部状态
const panelData = await panel.data().catch(() => null)
console.log('panel.data() =', panelData)
} finally {
if (miniProgram) await miniProgram.close()
}
}
run().catch((error) => {
console.error(error)
process.exit(1)
})page.$value()property('value')data()wx.requestconsole/exceptionloadingconsolepackageAPI/pages/network/request/requestconst automator = require('miniprogram-automator')
async function run() {
let miniProgram
const consoleEvents = []
const exceptionEvents = []
try {
miniProgram = await automator.launch({
cliPath: process.env.WECHAT_DEVTOOLS_CLI || '/Applications/wechatwebdevtools.app/Contents/MacOS/cli',
projectPath: process.env.MINIPROGRAM_PROJECT_PATH || '/absolute/path/to/devtools-project',
})
miniProgram.on('console', (payload) => {
consoleEvents.push(payload)
})
miniProgram.on('exception', (payload) => {
exceptionEvents.push(payload)
})
await miniProgram.mockWxMethod('request', (options = {}) => {
const res = {
data: {
code: 0,
list: [
{ id: 1, title: 'Mock Item A' },
{ id: 2, title: 'Mock Item B' },
],
},
statusCode: 200,
header: { 'content-type': 'application/json' },
cookies: [],
errMsg: 'request:ok',
}
Promise.resolve().then(() => {
if (typeof options.success === 'function') options.success(res)
if (typeof options.complete === 'function') options.complete(res)
})
return {
abort() {},
onHeadersReceived() {},
offHeadersReceived() {},
onChunkReceived() {},
offChunkReceived() {},
}
})
const page = await miniProgram.reLaunch('/packageAPI/pages/network/request/request')
await page.waitFor('button')
const button = await page.$('button')
if (!button) {
throw new Error('未找到 request 页面按钮')
}
await button.tap()
await page.waitFor(100)
const pageData = await page.data().catch(() => ({}))
if (pageData.loading !== false) {
throw new Error(`请求结束后 loading 状态异常: ${JSON.stringify(pageData)}`)
}
const successLogs = consoleEvents.filter((event) => {
if (!event || typeof event !== 'object' || !Array.isArray(event.args)) return false
return event.args.some((arg) => String(arg).includes('request success'))
})
if (!successLogs.length) {
throw new Error(`未观察到 request success 日志: ${JSON.stringify(consoleEvents)}`)
}
const consoleErrors = consoleEvents.filter((event) => {
return event && typeof event === 'object' && event.type === 'error'
})
if (consoleErrors.length) {
throw new Error(`存在 console.error: ${JSON.stringify(consoleErrors)}`)
}
if (exceptionEvents.length) {
throw new Error(`存在 exception: ${JSON.stringify(exceptionEvents)}`)
}
} finally {
if (miniProgram) {
await miniProgram.restoreWxMethod('request').catch(() => {})
await miniProgram.close().catch(() => {})
}
}
}
run().catch((error) => {
console.error(error)
process.exit(1)
})consoletypeargspayload.type === 'error'logconnect()const automator = require('miniprogram-automator')
const path = require('node:path')
const fs = require('node:fs/promises')
const { spawnSync } = require('node:child_process')
const CLI_PATH =
process.env.WECHAT_DEVTOOLS_CLI ||
'/Applications/wechatwebdevtools.app/Contents/MacOS/cli'
// projectPath 是开发者工具实际打开的目录(含 project.config.json)
const PROJECT_PATH =
process.env.MINIPROGRAM_PROJECT_PATH || '/absolute/path/to/project'
// 自动化 WebSocket 端口,默认 9420
const AUTO_PORT = Number(process.env.WECHAT_AUTO_PORT || 9420)
const TARGET_PAGE = '/pages/home/index'
const OUTPUT_DIR = path.resolve(process.cwd(), 'outputs')
/**
* 探测开发者工具当前 HTTP 服务端口。
* 原理:先用占位端口调用一次 CLI,从错误输出里解析实际端口。
* 如果工具还没启动,返回 null(CLI 会自行启动)。
*/
function detectHttpPort() {
if (process.env.WECHAT_DEVTOOLS_PORT) {
return Number(process.env.WECHAT_DEVTOOLS_PORT)
}
try {
const result = spawnSync(
CLI_PATH,
['auto', '--project', PROJECT_PATH, '--port', '9999'],
{ encoding: 'utf8', timeout: 8000 },
)
const output = (result.stdout || '') + (result.stderr || '')
const match = output.match(/started on http:\/\/127\.0\.0\.1:(\d+)/)
if (match) return Number(match[1])
} catch (_) {}
return null
}
/**
* 用 CLI v2 命令开启自动化 WebSocket 端口。
* 命令:cli auto --project <path> --auto-port <port> [--port <httpPort>]
*/
function enableAutomation(httpPort) {
const args = ['auto', '--project', PROJECT_PATH, '--auto-port', String(AUTO_PORT)]
if (httpPort) args.push('--port', String(httpPort))
const result = spawnSync(CLI_PATH, args, { encoding: 'utf8', timeout: 20000 })
return {
success: result.status === 0,
output: (result.stdout || '') + (result.stderr || ''),
}
}
async function main() {
let miniProgram
try {
await fs.mkdir(OUTPUT_DIR, { recursive: true })
// Step 1:探测 HTTP 端口,避免 CLI 因端口冲突报错
const httpPort = detectHttpPort()
console.log(httpPort ? `HTTP 端口: ${httpPort}` : '未检测到 HTTP 端口,由 CLI 启动')
// Step 2:用 CLI v2 开启自动化 ws 端口
const { success, output } = enableAutomation(httpPort)
if (!success) throw new Error(`CLI auto 命令失败:\n${output}`)
console.log('自动化端口已就绪')
// Step 3:connect 到 ws 端点
miniProgram = await automator.connect({
wsEndpoint: `ws://127.0.0.1:${AUTO_PORT}`,
})
console.log('已连接')
// Step 4:跳转页面并操作
const page = await miniProgram.reLaunch(TARGET_PAGE)
await page.waitFor('.page-title')
await page.waitFor(200)
await miniProgram.screenshot({ path: path.join(OUTPUT_DIR, 'page.png') })
console.log('截图完成')
} finally {
if (miniProgram) {
// connect 模式下 close() 只断开 ws 连接,不关闭开发者工具
await miniProgram.close().catch(() => {})
}
}
}
main().catch((error) => {
console.error(error)
process.exit(1)
})connect()close()launch()close()--auto-port--portdetectHttpPort()const PAGES = [
{ id: 'home', path: '/pages/home/index' },
{ id: 'list', path: '/pages/list/index' },
]
for (const item of PAGES) {
const page = await miniProgram.reLaunch(item.path)
await page.waitFor(500)
await miniProgram.screenshot({
path: path.join(OUTPUT_DIR, `${item.id}.png`),
})
}waitFor(500)index.jsonbeforeAllminiProgramafterAllminiProgramtest/it| 常见错误 | 正确做法 |
|---|---|
把 | 写成"开发者工具实际打开的目录",Taro 等场景可能是 |
| 只提醒安装依赖,不提醒安全设置 | 必须提醒开启服务端口 |
全靠固定 | 优先 |
用 | 先 |
用运行时 patch | 默认优先 |
| 忘记恢复 mock | 在 |
| 把截图当成真机能力 | 明确说明截图仅支持开发者工具模拟器 |
直接读 | 原生输入框优先 |
使用 | 升级到 |
| launch 前必须确保开发者工具完全退出(Cmd+Q) |
| 首次启动 + 用户确认信任需要时间,建议 |
页面路径不以 | 必须以 |
| 用选择器等待可能超时的页面(如首页列表)直接截图 | 首页等复杂页面用固定等待 |
开发者工具已开着时还用 | 改用 CLI v2 + |
| |
miniprogram-automator>= 0.12.0launch()projectPathproject.config.jsontimeout120000launch()connect()cli auto --project <path> --auto-port <port>automator.connect({ wsEndpoint: 'ws://127.0.0.1:<port>' })detectHttpPort()