Dialogs (Alert, Confirm, Prompt)
automationplaywright
Dialogs là các hộp thoại native của browser: alert(), confirm(), prompt(), beforeunload. Chúng chặn toàn bộ page nên không query được bằng DOM — Playwright xử lý qua event page.on('dialog') với dialog.accept()/dialog.dismiss(), và mặc định auto-dismiss nếu không có listener.
Khái niệm & Bản chất
- Native dialog (
alert,confirm,prompt,beforeunload) render bởi browser, nằm ngoài DOM — không thểlocator()hay inspect bằng DevTools Elements. - Dialog chặn JS của page: mọi thao tác Playwright tới page đó sẽ chờ cho đến khi dialog được xử lý — không xử lý thì test treo.
- Playwright auto-dismiss dialog nếu không có listener
page.on('dialog')— nghĩa làconfirm()mặc định trả vềfalse,prompt()trả vềnull. - Đăng ký
page.on('dialog', handler)để kiểm soát:dialog.accept()(bấm OK),dialog.accept('text')(điền prompt rồi OK),dialog.dismiss()(Cancel/đóng). beforeunloadlà dialog 'rời trang mất dữ liệu?' khi điều hướng/đóng tab với form chưa lưu — cũng qua eventdialog,typelà'beforeunload'.
Cách hoạt động & API chính
page.on('dialog', dialog => dialog.accept())— listener chạy mỗi lần dialog xuất hiện; đăng ký một lần trước khi trigger, không cần Promise.all vì đây là event emitter không phải wait một lần.- Object
dialogcó:dialog.type()('alert'|'confirm'|'prompt'|'beforeunload'),dialog.message()(nội dung text),dialog.defaultValue()(giá trị mặc định của prompt). dialog.accept(promptText?): vớialert/confirm= bấm OK; vớiprompt= điền text rồi OK.dialog.dismiss()= Cancel/Escape; vớiconfirm()trảfalse,prompt()trảnull.page.once('dialog', ...)xử lý đúng 1 lần rồi tự gỡ — an toàn hơnonkhi chỉ expect 1 dialog, tránh listener sót ăn dialog của bước sau.
Ví dụ & Cú pháp thực tế
- Confirm xoá — kiểm tra đúng message rồi accept:
``
ts test('xoá user hiện confirm', async ({ page }) => { page.once('dialog', async dialog => { expect(dialog.type()).toBe('confirm'); expect(dialog.message()).toContain('Xoá vĩnh viễn'); await dialog.accept(); // bấm OK }); await page.getByRole('button', { name: 'Xoá' }).click(); await expect(page.getByText('Đã xoá thành công')).toBeVisible(); });`` - Prompt nhập tên:
``
ts page.once('dialog', dialog => dialog.accept('Team QA')); await page.getByRole('button', { name: 'Tạo nhóm' }).click(); await expect(page.getByText('Team QA')).toBeVisible();`` - Dismiss để verify flow huỷ:
``
ts page.once('dialog', dialog => dialog.dismiss()); await page.getByRole('button', { name: 'Xoá' }).click(); await expect(page.getByText('Nguyễn Văn A')).toBeVisible(); // không bị xoá`` beforeunloadkhi rời trang có form dở: ``ts await page.getByLabel('Tên').fill('abc'); page.once('dialog', dialog => { expect(dialog.type()).toBe('beforeunload'); dialog.accept(); // chấp nhận rời trang }); await page.goto('/other-page');``- Đếm số dialog bằng flag:
``
ts let dialogShown = false; page.on('dialog', d => { dialogShown = true; d.dismiss(); }); await page.getByText('Thanh toán').click(); expect(dialogShown).toBe(false); // verify KHÔNG có alert hiện``
Best practices & Khi nào dùng
- Đăng ký listener TRƯỚC khi trigger — dialog xuất hiện khi chưa có listener sẽ bị auto-dismiss, bạn mất cơ hội assert message.
- Dùng
page.oncekhi chỉ expect một dialog trong kịch bản — listeneronsót lại sẽ âm thầm xử lý dialog của các bước sau, gây bug khó lần. - Luôn assert
dialog.message()/dialog.type()— verify app hiện đúng nội dung cảnh báo chứ không chỉ 'có dialog'. - Test cả 2 nhánh: accept → hành động xảy ra; dismiss → không có gì đổi.
confirm()trả false là nhánh nghiệp vụ quan trọng. - Với
beforeunload: muốn điều hướng tiếp thìaccept(), muốn ở lại thìdismiss()— sau dismiss verify vẫn còn URL cũ.
Gotchas, So sánh & Lưu ý
- Auto-dismiss mặc định là con dao hai lưỡi: quên đăng ký listener →
confirm()luôn trảfalse→ nhánh 'huỷ' được test mà bạn tưởng là 'đồng ý'. Đây là bug im lặng khó phát hiện. - Listener gắn vào
pagechỉ bắt dialog của page đó — dialog trong popup/iframe của page vẫn đi qua eventdialogtrên page, nhưng popup (tab mới) cần gắn listener trên Page object của popup. - Không bọc
page.on('dialog')trong Promise.all nhưwaitForEvent—onđăng ký lâu dài; nếu chỉ cần 1 lần dùngoncehoặcpage.waitForEvent('dialog'). - Khác với modal HTML (div + backdrop): modal là DOM thường, dùng
getByRole('dialog')/locatorassert như element. Native dialog mới cầnpage.on('dialog')— đừng nhầm hai loại. alert()không có Cancel — gọiaccept()haydismiss()đều đóng được nhưng nên dùngaccept()cho đúng ngữ nghĩa.- Một số app gọi
alerttrongrequestAnimationFrame/sau async — listener vẫn bắt được vì event không phụ thuộc timing, miễn đã đăng ký.
