IKivan-kozenko -aqa
Try yourself as a QA tester
All topics·Beginner·7 / 16

Navigations

page.goto() waits for the page to load. For anything beyond that — buttons that redirect, URL changes after form submit — there are waitForURL and load state options.

goto() and load states

page.goto(url) navigates and waits for the load event by default — meaning all resources (scripts, styles, images) have loaded. For most tests this is the right default. For SPAs that load data after mount, load may complete before the data is visible — but that's fine because Playwright's locators wait for elements to appear anyway.

If load takes too long (heavy page with images/iframes), use waitUntil: 'domcontentloaded' to wait only for the HTML to parse. Or waitUntil: 'networkidle' if you need to wait for all async requests to complete (use sparingly — it's slow).

ts
// Звичайний перехід — чекає load event
await page.goto('/orders')

// Тільки HTML — без зображень і скриптів
await page.goto('/orders', { waitUntil: 'domcontentloaded' })

// Чекає поки мережа заспокоїться (немає запитів 500ms)
await page.goto('/dashboard', { waitUntil: 'networkidle' })

// Одразу — не чекає нічого (рідко потрібно)
await page.goto('/orders', { waitUntil: 'commit' })

Waiting for URL changes

After a click that triggers redirect, call waitForURL() to wait for the new URL before asserting page content

When a button click triggers a redirect, Playwright doesn't automatically wait for the navigation to complete before the next line runs. Use waitForURL to assert you've arrived at the expected URL — or to wait for the redirect before doing anything else.

I use waitForURL after form submissions and login flows — it's the most readable way to assert a successful redirect.

ts
test('login redirects to dashboard', async ({ page }) => {
  await page.goto('/login')
  await page.getByLabel('Email').fill('admin@example.com')
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!)
  await page.getByRole('button', { name: 'Sign in' }).click()

  // Чекаємо редиректу на dashboard
  await page.waitForURL('/dashboard')

  // Або з glob патерном
  await page.waitForURL('**/dashboard')
})

test('order submission redirects to confirmation', async ({ page }) => {
  await page.goto('/orders/new')
  await page.getByLabel('Item').fill('Laptop')
  await page.getByRole('button', { name: 'Submit' }).click()

  // URL може бути /orders/123/confirmation
  await page.waitForURL(//orders/d+/confirmation/)
  await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible()
})

The hydration trap

This is a subtle bug that trips people up: the page renders a button (SSR), Playwright clicks it immediately, but the React/Vue/Svelte code hasn't hydrated yet so the click handler doesn't exist. The click does nothing. The test fails with a mysterious "element not found" or just the wrong state.

The fix belongs to the app, not the test: disable interactive elements until hydration completes. In tests, you can work around it by waiting for a signal that hydration is done — like a specific element that only appears after client-side JS runs.

ts
// Якщо застосунок показує loader під час гідратації
test('dashboard loads after hydration', async ({ page }) => {
  await page.goto('/dashboard')

  // Чекаємо поки loader зникне — сигнал що JS виконався
  await page.getByTestId('loading-spinner').waitFor({ state: 'hidden' })

  // Тепер безпечно взаємодіяти
  await page.getByRole('button', { name: 'Create order' }).click()
})

Browser history navigation

For testing browser back/forward behavior — like "does clicking back restore the filter state" — Playwright has page.goBack() and page.goForward().

ts
test('back button restores order list filter', async ({ page }) => {
  await page.goto('/orders')
  await page.getByRole('combobox', { name: 'Status' }).selectOption('pending')

  // Перейшли до конкретного замовлення
  await page.getByRole('row').first().getByRole('link').click()
  await page.waitForURL(//orders/d+/)

  // Повертаємося назад
  await page.goBack()
  await page.waitForURL('/orders')

  // Фільтр має зберегтися
  await expect(page.getByRole('combobox', { name: 'Status' }))
    .toHaveValue('pending')
})