首页 / Playwright 入门教程 / 弹窗与对话框 Dialog 处理

Playwright 入门教程

弹窗与对话框 Dialog 处理

本教程共 59 篇 · 第 21 篇 · 更新于 2026-08-04 · 约 8 分钟阅读

PlaywrightDialogalertconfirmpromptbeforeunload事件处理

本节目标:能正确处理 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: truepage.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 这些进阶工具。