Wiki

← Tất cả bài wiki

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).
  • beforeunload là 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 event dialog, type là '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 dialog có: 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ới alert/confirm = bấm OK; với prompt = điền text rồi OK.
  • dialog.dismiss() = Cancel/Escape; với confirm() trả false, prompt() trả null.
  • page.once('dialog', ...) xử lý đúng 1 lần rồi tự gỡ — an toàn hơn on khi 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á ``
  • beforeunload khi 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.once khi chỉ expect một dialog trong kịch bản — listener on só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 page chỉ bắt dialog của page đó — dialog trong popup/iframe của page vẫn đi qua event dialog trê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ùng once hoặc page.waitForEvent('dialog').
  • Khác với modal HTML (div + backdrop): modal là DOM thường, dùng getByRole('dialog')/locator assert như element. Native dialog mới cần page.on('dialog') — đừng nhầm hai loại.
  • alert() không có Cancel — gọi accept() hay dismiss() đều đóng được nhưng nên dùng accept() cho đúng ngữ nghĩa.
  • Một số app gọi alert trong requestAnimationFrame/sau async — listener vẫn bắt được vì event không phụ thuộc timing, miễn đã đăng ký.