IKivan-kozenko -aqa
Try yourself as a QA tester
All topics·Advanced·24 / 24

Migrating from Testing Library

If you know React Testing Library, the mental model carries over almost directly — getByRole, getByLabel, getByText, getByTestId all exist in Playwright too. The main differences: no more getBy/findBy/queryBy split (Playwright locators auto-wait), render() becomes mount(), screen becomes page or the component locator, and waitFor is usually replaced by a Playwright assertion.

The mental model shift

In Testing Library there are three query variants: - getBy* — synchronous, throws if not found - findBy* — async, waits and retries - queryBy* — synchronous, returns null if not found In Playwright all locators are lazy — they don't do anything until you call an action or assertion. When I call .click() or expect(...).toBeVisible(), Playwright auto-waits for the element to appear and be actionable. I never need to choose between the three variants.

Quick reference

| Testing Library | Playwright | |---|---| | screen | page (e2e) або component (CT) | | getBy*, findBy*, queryBy* | page.getBy*() (всі однакові — auto-wait) | | render(<Component />) | await mount(<Component />) | | const { unmount } = render(...) | const { unmount } = await mount(...) | | const { rerender } = render(...) | const { update } = await mount(...) | | within(element) | locator.locator(...) (nested) | | waitFor(() => expect(...)) | await expect(...).toBeVisible() | | waitForElementToBeRemoved(...) | await expect(...).toBeHidden() | | user.click(el) | await locator.click() | | user.type(el, 'text') | await locator.fill('text') | | expect(el).toBeInTheDocument() | await expect(locator).toBeVisible() |

Side by side example

A sign-in test migrated from React Testing Library to Playwright Component Testing:

ts
// React Testing Library (до міграції)
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('sign in', async () => {
  const user = userEvent.setup()
  render(<SignInForm />)

  await user.type(screen.getByLabelText('Username'), 'John')
  await user.type(screen.getByLabelText('Password'), 'secret')
  await user.click(screen.getByRole('button', { name: 'Sign in' }))

  expect(await screen.findByText('Welcome, John')).toBeInTheDocument()
})
ts
// Playwright Component Testing (після міграції)
import { test, expect } from '@playwright/experimental-ct-react'

test('sign in', async ({ mount }) => {
  // render() → mount() (тепер async)
  const component = await mount(<SignInForm />)

  // screen.getByLabelText() → component.getByLabel()
  await component.getByLabel('Username').fill('John')
  await component.getByLabel('Password').fill('secret')
  await component.getByRole('button', { name: 'Sign in' }).click()

  // findByText() + toBeInTheDocument() → getByText() + toBeVisible()
  // Playwright auto-wait — немає потреби у findBy vs getBy
  await expect(component.getByText('Welcome, John')).toBeVisible()
})

Replacing waitFor and waitForElementToBeRemoved

In Testing Library I often need waitFor to wait for async state changes. In Playwright, assertions auto-wait — so await expect(locator).toBeVisible() already waits for up to the configured timeout.

When there's no suitable built-in assertion, I use expect.poll() for custom conditions.

ts
// Testing Library
await waitFor(() => {
  expect(getByText('Order created')).toBeInTheDocument()
})
await waitForElementToBeRemoved(() => queryByText('Loading...'))

// Playwright — просто assertions
await expect(page.getByText('Order created')).toBeVisible()
await expect(page.getByText('Loading...')).toBeHidden()

// Кастомна умова без built-in assertion
await expect.poll(async () => {
  return await page.evaluate(() => window.appState.loaded)
}).toBe(true)

Replacing within()

within(element) in Testing Library scopes queries to inside a specific element. In Playwright, I chain locators — every locator method called on a locator searches within that locator's scope.

ts
// Testing Library
const orderRow = screen.getByTestId('order-row-1042')
const cancelButton = within(orderRow).getByRole('button', { name: 'Cancel' })

// Playwright — вкладені локатори
const orderRow = page.getByTestId('order-row-1042')
const cancelButton = orderRow.getByRole('button', { name: 'Cancel' })
await cancelButton.click()

What you gain by switching

Moving from RTL to Playwright Component Testing gives: - Tests run in a real browser — real CSS, real layout, real hover states - All standard Playwright tools work: traces, screenshots, HTML reports, UI mode - The same test can run in Chrome, Firefox, and WebKit - Visual snapshot testing built in - No JSDOM quirks or limitations