IKivan-kozenko -aqa
Try yourself as a QA tester
All topics·Intermediate·20 / 27

Dialogs

Native browser dialogs — alert, confirm, prompt — are invisible to locators. You handle them through page events, not clicks. Get this wrong and the test hangs forever.

Why native dialogs are different

When JavaScript calls window.alert(), window.confirm(), or window.prompt(), the browser opens a native OS dialog — not a DOM element. Playwright can't find it with getByRole or any locator because it's not in the page's HTML at all.

By default, Playwright auto-dismisses all dialogs. For alert that means clicking OK. For confirm it means clicking Cancel (returns false). For prompt it means clicking Cancel (returns null). If your code checks the return value of confirm() and does something on true, the default behavior will break that flow.

Handling alert, confirm, prompt

Register a dialog event handler on the page. The handler fires when any dialog opens. The critical detail: register it BEFORE the action that triggers the dialog. If you register after, the dialog might appear and auto-close before your handler is attached.

ts
test('delete order with confirmation', async ({ page }) => {
  await page.goto('/orders')

  // Реєструємо обробник ДО кліку — прийняти confirm
  page.on('dialog', dialog => dialog.accept())

  await page.getByRole('row').filter({ hasText: 'ORDER-042' })
    .getByRole('button', { name: 'Delete' }).click()

  // Order видалений
  await expect(page.getByText('ORDER-042')).not.toBeVisible()
})

test('cancel deletes nothing', async ({ page }) => {
  await page.goto('/orders')

  // Відхилити confirm — order лишається
  page.on('dialog', dialog => dialog.dismiss())

  await page.getByRole('row').filter({ hasText: 'ORDER-042' })
    .getByRole('button', { name: 'Delete' }).click()

  await expect(page.getByText('ORDER-042')).toBeVisible()
})
ts
test('rename order via prompt', async ({ page }) => {
  await page.goto('/orders')

  // Для prompt: acceptText встановлює що ввів користувач
  page.on('dialog', async dialog => {
    expect(dialog.type()).toBe('prompt')
    expect(dialog.message()).toBe('Enter new order name:')
    await dialog.accept('Laptop Stand Order')
  })

  await page.getByRole('button', { name: 'Rename' }).click()
  await expect(page.getByText('Laptop Stand Order')).toBeVisible()
})
ts
test('alert shows correct message', async ({ page }) => {
  await page.goto('/orders')

  let alertMessage = ''

  page.on('dialog', async dialog => {
    alertMessage = dialog.message()
    await dialog.accept()
  })

  // Дія що викликає alert
  await page.getByRole('button', { name: 'Show summary' }).click()

  // Перевіряємо текст після обробки
  expect(alertMessage).toContain('5 orders pending')
})

beforeunload — unsaved changes warning

The beforeunload event fires when the user tries to leave the page with unsaved changes. The browser shows "Are you sure you want to leave?" — a dialog you can't suppress with CSS or locators. Handle it the same way: register before the action that navigates away.

ts
test('warns before leaving with unsaved edits', async ({ page }) => {
  await page.goto('/orders/42/edit')

  // Зміни без збереження
  await page.getByLabel('Item name').fill('Modified item')

  let dialogShown = false
  page.on('dialog', async dialog => {
    dialogShown = true
    expect(dialog.type()).toBe('beforeunload')
    await dialog.dismiss() // Залишитися на сторінці
  })

  await page.close({ runBeforeUnload: true })
  expect(dialogShown).toBe(true)
})