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

Annotations

Four built-in annotations control how tests run: skip (don't run), fixme (known failure, don't run), fail (expect failure, do run), slow (triple the timeout). I use test.skip(condition) constantly — it's how I handle browser-specific bugs without deleting the test.

The four built-in annotations

test.fail is the unusual one — Playwright runs it and expects failure; if it passes unexpectedly, you get an error

Each one changes what Playwright does with the test in a different way.

ts
// test.skip — не запускати (тест пропускається, зеленого/червоного немає)
test.skip('search not yet implemented', async ({ page }) => {
  await page.goto('/search')
  await expect(page.getByRole('searchbox')).toBeVisible()
})

// test.fixme — відомий збій, не запускати
// Різниця з skip: fixme = "я знаю що це зламано, треба виправити"
test.fixme('order export crashes on large datasets', async ({ page }) => {
  await page.goto('/orders')
  await page.getByRole('button', { name: 'Export all' }).click()
  await expect(page.getByRole('link', { name: 'Download CSV' })).toBeVisible()
})

// test.fail — очікується що тест ВПАДЕ
// Playwright запустить його і перевірить що він справді падає
// Якщо пройде — Playwright скаржиться! Значить фіча виправлена і анотацію можна прибрати
test.fail('pagination broken in Firefox — JIRA-142', async ({ page }) => {
  await page.goto('/orders')
  await page.getByRole('button', { name: 'Next page' }).click()
  await expect(page.getByTestId('order-row')).toHaveCount(10)
})

// test.slow — потроїти дефолтний таймаут
// Якщо тест займає 40+ секунд і 30-секундного дефолту не вистачає
test.slow('full report generation', async ({ page }) => {
  await page.goto('/reports/generate')
  await page.getByRole('button', { name: 'Generate report' }).click()
  await expect(page.getByTestId('report-ready')).toBeVisible()
})

Conditional skip — browser or environment specific

The most useful pattern I know: skip a test only on a specific browser. The bug is real, the ticket is filed, but I don't want the whole test suite to be red just because of a Safari-specific issue.

ts
// Пропустити в конкретному браузері
test('drag and drop reorders items', async ({ page, browserName }) => {
  test.skip(browserName === 'firefox', 'JIRA-198: drag API behaves differently in Firefox')

  await page.goto('/orders')
  // ... drag and drop тест
})

// Пропустити на CI (наприклад, якщо тест потребує локального файлу)
test('import from CSV', async ({ page }) => {
  test.skip(!!process.env.CI, 'Requires local test fixture file')
  // ...
})

// Пропустити групу тестів умовно — через describe
test.describe('payment tests', () => {
  test.skip(({ browserName }) => browserName !== 'chromium', 'Payment widget — Chromium only')

  test('card payment flow', async ({ page }) => { /* ... */ })
  test('PayPal redirect', async ({ page }) => { /* ... */ })
})

test.only — focus on one test locally

When debugging, add test.only() to run just that test. The whole file becomes focused on it. Never commit `test.only` — use forbidOnly: !!process.env.CI in config to make CI fail if you accidentally leave it in.

ts
// Локально — запустити тільки цей тест
test.only('order status update', async ({ page }) => {
  await page.goto('/orders/42')
  await page.getByRole('combobox', { name: 'Status' }).selectOption('shipped')
  await expect(page.getByTestId('status-badge')).toHaveText('Shipped')
})

// В playwright.config.ts — заборонити test.only на CI
export default defineConfig({
  forbidOnly: !!process.env.CI, // якщо забудеш прибрати — CI завалиться
})

Tags — filter by @smoke, @slow, @critical

Tags let you run subsets of tests. I tag tests as @smoke for the quick sanity check suite that runs on every deploy, and @regression for the full suite that runs nightly.

ts
// Через властивість tag
test('login flow', { tag: '@smoke' }, async ({ page }) => {
  await page.goto('/login')
  await page.getByLabel('Email').fill('admin@example.com')
  await page.getByLabel('Password').fill(process.env.ADMIN_PASSWORD!)
  await page.getByRole('button', { name: 'Sign in' }).click()
  await page.waitForURL('/dashboard')
})

// Через @-токен у назві тесту
test('full report generation @regression @slow', async ({ page }) => {
  // ...
})

// Тег на групу
test.describe('checkout flow', { tag: '@smoke' }, () => {
  test('add to cart', async ({ page }) => { /* ... */ })
  test('proceed to payment', async ({ page }) => { /* ... */ })
})
bash
# Запустити тільки @smoke тести
npx playwright test --grep @smoke

# Запустити все КРІМ @slow
npx playwright test --grep-invert @slow

# @smoke АБО @critical
npx playwright test --grep "@smoke|@critical"

Annotations — link tests to issues

Annotations are metadata attached to a test — type and description. They appear in the HTML report. I use them to link tests to JIRA tickets or GitHub issues, so when a test fails I can immediately see which issue it relates to.

ts
// Прив'язати тест до тікету
test('order export to CSV', {
  annotation: {
    type: 'issue',
    description: 'https://github.com/org/repo/issues/142',
  },
}, async ({ page }) => {
  // ...
})

// Кілька анотацій — тікет + перфоманс-нотатка
test('full dashboard load', {
  annotation: [
    { type: 'issue', description: 'https://jira.company.com/PROJ-789' },
    { type: 'performance', description: 'Should load in < 3s, currently ~8s' },
  ],
}, async ({ page }) => {
  // ...
})

test.describe — group related tests

test.describe() groups tests under a shared name and scopes beforeEach/afterEach hooks to just that group. I use it when a feature has multiple related scenarios that share setup.

ts
test.describe('order filters', () => {
  // beforeEach запускається тільки для тестів у цьому describe
  test.beforeEach(async ({ page }) => {
    await page.goto('/orders')
  })

  test('filter by pending status', async ({ page }) => {
    await page.getByRole('combobox', { name: 'Status' }).selectOption('pending')
    await expect(page.getByTestId('order-row')).not.toHaveCount(0)
  })

  test('filter by date range', async ({ page }) => {
    await page.getByLabel('From date').fill('2024-01-01')
    await page.getByLabel('To date').fill('2024-01-31')
    await page.getByRole('button', { name: 'Apply filters' }).click()
    await expect(page.getByTestId('order-row')).toHaveCount(5)
  })
})