弹窗与对话框 Dialog 处理
本教程共 59 篇 · 第 21 篇 · 更新于 2026-08-04 · 约 8 分钟阅读
本节目标:能正确处理 alert / confirm / prompt / beforeunload 四类原生对话框,并避开「脚本挂死」这个大坑。
这里说的对话框,是浏览器画的那种系统级弹窗。就是 alert('保存成功') 弹出来的灰色小框,不是页面上用 div 拼的 Modal。
区别很重要。div 拼的弹窗就是普通元素,用定位器点就行。原生对话框在页面之外,DOM 里找不到它,必须走事件。
默认行为:自动关闭
先说个好消息。Playwright 默认会自动关闭所有对话框,等同于点了「取消」。
也就是说,如果你的测试只是碰巧遇到一个 alert,什么都不用写,脚本会继续跑。
// 页面弹了 alert,Playwright 自动 dismiss,这行照常执行完
await page.getByRole('button', { name: '删除' }).click();
这个默认值挺合理。大多数时候对话框不是测试重点,自动关掉最省事。
但当对话框本身就是你要测的东西时,就得接管。
注册处理器:page.on(‘dialog’)
// 先注册处理器
page.on('dialog', dialog => dialog.accept());
// 再触发对话框
await page.getByRole('button', { name: '删除' }).click();
两行的顺序不能反。处理器必须在触发动作之前挂好,否则对话框弹出来时没人管,就会退回默认的自动关闭。
dialog 对象上有这些东西:
page.on('dialog', async dialog => {
console.log(dialog.type()); // 'alert' | 'confirm' | 'prompt' | 'beforeunload'
console.log(dialog.message()); // 对话框里显示的文字
console.log(dialog.defaultValue()); // prompt 的默认值,其他类型是空串
await dialog.accept(); // 点「确定」
// 或
await dialog.dismiss(); // 点「取消」
});
三类对话框怎么处理
alert 只有一个确定按钮。accept() 和 dismiss() 效果一样,都是关掉:
page.on('dialog', async dialog => {
expect(dialog.message()).toBe('保存成功');
await dialog.accept();
});
await page.getByRole('button', { name: '保存' }).click();
confirm 有确定和取消,返回布尔值给页面:
// 确认删除
page.on('dialog', dialog => dialog.accept());
await page.getByRole('button', { name: '删除' }).click();
await expect(page.getByText('已删除')).toBeVisible();
prompt 需要用户输入文本,把内容作为 accept() 的参数传进去:
page.on('dialog', dialog => dialog.accept('码上学'));
await page.getByRole('button', { name: '重命名' }).click();
await expect(page.getByText('码上学')).toBeVisible();
不传参数就是提交空字符串;调 dismiss() 则让页面收到 null。
最大的坑:监听了但不处理
这段代码看着人畜无害,实际会永久卡死:
// ❌ 错误示范
page.on('dialog', dialog => console.log(dialog.message()));
await page.getByRole('button').click(); // 卡在这一行,永远不返回
为什么?
网页对话框是模态的。它弹出来之后,页面上所有 JavaScript 执行都被冻结,直到有人关掉它。
Playwright 的规则是:一旦你注册了 dialog 监听器,自动关闭机制就关闭了,处理对话框的责任转移给你。 你的回调只打了个日志,没调 accept() 也没调 dismiss(),对话框就一直挂着,click() 自然永远等不到结束。
Warning记死这条:注册了
page.on('dialog'),回调里就必须调accept()或dismiss()。 想只看内容不做处理,是不成立的。
正确写法是又看又关:
// ✅ 正确
page.on('dialog', async dialog => {
console.log(dialog.message());
await dialog.dismiss();
});
只想处理一次:page.once
page.on() 注册的是长期监听,测试里后续所有对话框都会走它。
如果只想接管某一次,用 page.once():
page.once('dialog', dialog => dialog.accept('2026'));
await page.evaluate("prompt('请输入年份:')");
回调执行一次后自动摘掉,后面的对话框回到默认的自动关闭。
需要中途手动摘掉监听器,用 page.off()。注意 Dialog(对话框)类型要单独导入:
import type { Dialog } from '@playwright/test';
const handler = (dialog: Dialog) => dialog.accept();
page.on('dialog', handler);
await page.getByRole('button', { name: '删除' }).click();
page.off('dialog', handler); // 摘掉后回到默认自动关闭
断言对话框内容的两种姿势
想验证对话框文字对不对,可以在回调里直接断言:
page.once('dialog', async dialog => {
expect(dialog.message()).toContain('确定要删除吗');
await dialog.accept();
});
await page.getByRole('button', { name: '删除' }).click();
回调里断言失败的错误有时不好定位。另一种写法是把信息存出来,在主流程断言:
let dialogMessage = '';
page.once('dialog', async dialog => {
dialogMessage = dialog.message();
await dialog.accept();
});
await page.getByRole('button', { name: '删除' }).click();
expect(dialogMessage).toContain('确定要删除吗');
我更常用第二种。断言留在主流程里,失败信息和调用栈都更清楚。
beforeunload:离开页面的确认框
用户有未保存内容时关标签页,浏览器会问一句「确定要离开吗」。这就是 beforeunload 对话框。
在 Playwright 里,它基本只有一个触发口子:调用 page.close() 时显式打开 runBeforeUnload。
page.on('dialog', async dialog => {
expect(dialog.type()).toBe('beforeunload');
await dialog.dismiss(); // 取消关闭,留在页面
});
await page.close({ runBeforeUnload: true });
Note带
runBeforeUnload: true的page.close()是唯一不等待页面真正关闭的情况。因为对话框可能被 dismiss,页面最终还开着。
日常 page.close() 不带参数,直接关,不跑 unload 处理器,也就不会有这个对话框。
打印对话框怎么测
window.print() 弹出的打印预览不走 Dialog 事件,page.on('dialog') 接不到。想验证它被调用过,只能换个思路——页面加载后把 window.print 换成一个能被观察的函数:
await page.goto('https://example.com');
// 把 window.print 换成一个会 resolve Promise 的函数
await page.evaluate(`(() => {
window.waitForPrintDialog = new Promise(resolve => { window.print = resolve; });
})()`);
await page.getByText('打印').click();
// 那个 Promise 被 resolve,就说明 print 确实被调用了
await page.waitForFunction('window.waitForPrintDialog');
顺序是关键:替换脚本必须在页面加载之后、点击之前执行。这里 page.evaluate 传的是字符串,为的是绕开 TypeScript 对 window 上自定义属性的类型检查。
小结
- 原生对话框在 DOM 之外,只能用
page.on('dialog')处理,定位器找不到它。 - 不注册监听器时,Playwright 自动关闭所有对话框,很多场景可以不管。
- 一旦注册监听器,回调里必须调
accept()或dismiss(),否则脚本挂死。 prompt的输入值通过dialog.accept('文本')传入。- 处理器要在触发动作之前注册,只处理一次用
page.once()。 - 断言内容时,把消息存到变量再在主流程断言,报错更清楚。
beforeunload只有page.close({ runBeforeUnload: true })才会触发。
下一章预告:Web 优先断言 Assertions 全解——自动重试断言的机制、匹配器速查,以及软断言、poll、toPass 这些进阶工具。