在 Playwright 中测试
用 Playwright 写小程序的 E2E 测试:在浏览器里打开小程序的页面,点击按钮,检查页面上的文字、页面的 data、小程序发出的请求。 CI 里无头运行,Linux 上不需要开发者工具。
fixture 是什么
fixture 是 Playwright 的概念:测试框架在每个用例开始前准备好、作为参数传进来的对象。Playwright 自带的 page(浏览器页面)就是一个 fixture。
@open-wxa/wxa-runner-playwright 提供了一个叫 miniProgram 的 fixture。用了它,就不用自己启动 wxa-runner、打开浏览器、等小程序启动, 用例里直接写要测的东西:
test('点击「详情」进入详情页', async ({ miniProgram }) => {
await miniProgram.open('pages/index/index');
await miniProgram.current.locator('#detail').click();
await expect(miniProgram).toHavePath('pages/detail/detail');
});不想多装一个包,或者用的不是 Playwright,可以直接调用浏览器里的测试 API。
快速上手
1. 安装
pnpm add -D @playwright/test @open-wxa/wxa-runner-playwright用例在本机安装的 Google Chrome 里运行。没装 Chrome 时,运行 npx playwright install chrome。
2. 新建 playwright.config.ts
- 原生小程序:放在
project.config.json所在目录 - uni-app、Taro 等框架:放在源码根目录,用
wxaProject指向编译产物目录。跑测试前先构建出这个目录
// playwright.config.ts
import { defineConfig } from '@playwright/test';
import type { WxaOptions } from '@open-wxa/wxa-runner-playwright';
export default defineConfig<WxaOptions>({
testDir: 'e2e',
use: {
channel: 'chrome',
// uni-app、Taro:编译产物目录,相对这个配置文件
// wxaProject: './dist/dev/mp-weixin',
// setup 文件,相对这个配置文件
// wxaServerOptions: { setup: './wxa.setup.ts' },
}
});3. 写第一个用例
// e2e/index.spec.ts
import { test, expect } from '@open-wxa/wxa-runner-playwright';
test('首页能打开', async ({ miniProgram }) => {
await miniProgram.open();
// 换成你的首页路径
await expect(miniProgram).toHavePath('pages/index/index');
});test 和 expect 要从 @open-wxa/wxa-runner-playwright 导入,不是 @playwright/test:这样用例里才有 miniProgram, expect 才有 toHavePath 等断言。
4. 运行
npx playwright test # 无头运行全部用例
npx playwright test --headed # 打开浏览器窗口,看着它运行
npx playwright test --ui # 图形界面,可以一步步回看写用例
下面是一个完整的例子:下单前 mock 登录、拦截下单接口,点击提交后检查跳转、页面 data 和发出的请求。各部分的说明见后面几节。
import { test, expect } from '@open-wxa/wxa-runner-playwright';
test('提交订单后跳转到结果页', async ({ miniProgram }) => {
// 打开页面前声明,App.onLaunch 里就生效
await miniProgram.mock('login', { code: 'test_code' });
await miniProgram.route('POST **/api/orders', {
status: 201,
body: { id: 'o-1' }
});
await miniProgram.open('pages/cart/cart');
// current 是当前页,跳转后自动指向新页面
await miniProgram.current.locator('#submit').click();
// 跳转需要时间,这两个断言会自动重试,不用手动等待
await expect(miniProgram).toHavePath('pages/result/result');
await expect(miniProgram).toHavePageData({ orderId: 'o-1' });
const req = await miniProgram.waitForRequest(
'POST **/api/orders'
);
expect(req.body).toEqual({ skuId: 1, count: 2 });
});打开页面
| 写法 | 作用 |
|---|---|
await miniProgram.open() | 打开首页 |
await miniProgram.open('pages/detail/detail?id=1') | 打开指定页面,? 后面是页面参数 |
- 页面不在
app.json里时直接报错,不会停在首页上继续执行 - 同一个用例里再次
open()会整页重新加载,之前的mock、route仍然生效
操作页面
miniProgram.current 是当前页(页面栈顶),用法和 Playwright 的 page.frameLocator() 一样。跳转后它自动指向新页面,不用重新获取:
const page = miniProgram.current;
await page.locator('#add').click();
await page.getByText('去结算').click();
await expect(page.locator('#count')).toHaveText('1');断言
除了 Playwright 自带的断言,还可以直接断言小程序的状态,不满足时自动重试,直到超时:
| 写法 | 检查 |
|---|---|
expect(miniProgram).toHavePath('pages/result/result') | 当前页是这个页面 |
expect(miniProgram).toHavePageData({ status: 'ok' }) | 当前页的 data 包含这些字段和值,没列出的字段不比较 |
expect(miniProgram).toHavePageData(data, { path }) | 页面栈里指定页面的 data |
别忘了 await。两者都可以加 .not,也可以传 { timeout } 改超时时间(毫秒)。
mock 原生 API、拦截请求
在 open() 之前调用,App.onLaunch 里就生效;打开之后调用,从下一次调用开始生效:
await miniProgram.mock('getLocation', {
latitude: 31.23,
longitude: 121.47
});
await miniProgram.route('GET **/api/feed', {
body: { list: [] }
});
await miniProgram.open();- 写法和 setup 的
mock、network.routes一样,会覆盖 setup 里的同名配置 unmock(api?)、unroute(pattern?)撤销,不传参数时撤销全部,包括 setup 里声明的
也可以传函数,按调用参数或请求内容返回结果。函数在 Node 里执行,能用用例里的变量,也能在里面写断言:
await miniProgram.route('POST **/api/orders', request => {
expect(request.body).toEqual({ skuId: 1, count: 2 });
return { status: 201, body: { id: 'o-1' } };
});- 函数抛错时这次调用失败,用例结束时也会失败,写在函数里的断言失败了不会被忽略
- 函数的结果是异步返回的。基础库同步调用的 API 只能传对象:以
Sync结尾的,以及getSystemInfo、getMenuButtonBoundingClientRect(wx.getWindowInfo等都经过getSystemInfo) - 参数和返回值要能序列化。
route的函数拿到的request.body已经按 Content-Type 解析好(JSON、表单)
检查发出的请求
await miniProgram.clearRequests();
await miniProgram.current.locator('#save').click();
const req = await miniProgram.waitForRequest(
'POST **/api/profile'
);
expect(req.body).toEqual({ name: '张三' });waitForRequest(pattern):等到小程序发出匹配的请求,返回最近的一个。之前已经发出的也算,只关心某个操作之后的请求时,操作前先clearRequests()requests(pattern?):已经发出的全部请求,按发出顺序。被拦截的和真正发出的都有,只保留最近 1000 个- 规则的写法同
route
每个请求有:
| 字段 | 说明 |
|---|---|
method | 大写,如 POST |
url | 完整地址 |
header | 请求头 |
body | 按 Content-Type 解析好的请求体:JSON 是对象,表单是 { 字段: 值 } |
data | 没解析的原始请求体 |
routedBy | 拦截它的规则;没被拦截、真正发出去的为 null |
读取状态、直接跳转
| 写法 | 作用 |
|---|---|
pageData(path?) | 页面的 data。不传时读当前页 |
currentPage() / pageStack() | 当前页 / 整个页面栈,每页有 path、query |
waitForPage(path) | 等到这个页面成为当前页 |
callPageMethod(name, ...args) | 调用当前页的方法,返回方法的返回值 |
navigateTo(url)、navigateBack()、redirectTo(url)、reLaunch(url)、switchTab(url) | 直接跳转,同 wx 上的同名 API |
getStorage(key) / clearStorage() | 读取 / 清空小程序的存储 |
都是 async 的,要 await。每个成员在编辑器里悬停都能看到详细说明。
选择图片
wx.chooseImage、wx.chooseMedia 会打开浏览器的文件选择框。用 Playwright 的 filechooser 事件直接给文件,不会弹出系统窗口。 和真机一样,相册和拍摄都可选时(默认)会先弹出「拍摄 / 从手机相册选择」菜单,要先点菜单项:
test('上传头像', async ({ miniProgram, page }) => {
await miniProgram.open('pages/profile/profile');
await miniProgram.current.locator('#avatar').click();
const [chooser] = await Promise.all([
page.waitForEvent('filechooser'),
page.getByRole('button', { name: '从手机相册选择' }).click()
]);
await chooser.setFiles('fixtures/avatar.png');
});- 菜单、图片预览这些弹层在
page上,不在miniProgram.current里。预览层用page.getByRole('dialog', { name: '图片预览' })找到 chooser.setFiles([])等于用户关掉选择框,页面收到cancel
检查运行时错误
每个用例结束时,fixture 会检查小程序有没有出错,有就让用例失败:
- 逻辑层、渲染层的运行时错误
- 调用了 wxa-runner 还没有实现的
wxAPI。这些 API 会返回成功,以免打断业务代码,但测试里要 mock 掉
用 miniProgram.errors() 可以看到具体内容。有意触发错误的用例,用 test.use({ wxaCheckErrors: false }) 关掉检查。
配置项
在 playwright.config.ts 的 use 里设置:
| 选项 | 默认值 | 作用 |
|---|---|---|
wxaProject | '.' | 小程序目录,相对配置文件 |
wxaServerOptions | {} | 开发服务器的其他参数,如 setup、baseLibVersion、realLogin。路径相对配置文件 |
wxaCheckErrors | true | 用例结束时检查运行时错误 |
- 每个 worker(Playwright 并行运行用例的进程)启动一个开发服务器,小程序只编译一次;每个用例用一个新的浏览器上下文,存储、页面栈互不影响
- 测试时不监听文件变化、不热更新,运行环境和真机一致
wxaProject、wxaServerOptions只能写在配置文件里,或用例文件顶层的test.use里,不能写在describe里
注意事项
- 跳转后的选择器:新旧页面有同一个选择器时,先
await expect(miniProgram).toHavePath(...)等跳转完成,免得断言落在上一个页面上 - text 组件的文字:基础库 3.3.x 及更早的版本里,
textContent(Playwright 的toHaveText默认用它)会把 text 组件的文字读到两遍。 用toHaveText(text, { useInnerText: true })或innerText()

