😲 Heads up — Playwright introduced native Testing Library queries in version 1.27.
💬 #558 ← We're discussing what this means for Playwright Testing Library in this issue. You can find a more detailed comparison of the new Playwright API and this library here. Please ask any questions you may have or share thoughts and suggestions!
All of your favorite user-centric querying functions from @testing-library/react and @testing-library/dom available from within Playwright!
- Test fixture for @playwright/test via @playwright-testing-library/test
- Standalone queries for playwright via playwright-testing-library
ElementHandle
queries (getDocument
+queries
) ↓- Asynchronous
waitFor
assertion helper (via wait-for-expect)
# For use with Playwright Test (@playwright/test)
npm install --save-dev @playwright-testing-library/test
# For use with Playwright (playwright)
npm install --save-dev playwright-testing-library
There are currently a few different ways to use Playwright Testing Library, depending on how you use Playwright. However, the recommended approach is to use the Locator
queries fixture with Playwright Test (@playwright/test).
⚠️ TheElementHandle
query APIs were created before Playwright introduced itsLocator
API and will be replaced in the next major version of Playwright Testing Library. If you can't use @playwright/test at the moment, you'll need to use theElementHandle
query API, but a migration path will be provided when we switch to the newLocator
APIs.
🔖 Added in 4.4.0
Using the Locator
Playwright Test (@playwright/test) fixture with @playwright-testing-library/test.
import {test as base} from '@playwright/test'
import {
locatorFixtures as fixtures,
LocatorFixtures as TestingLibraryFixtures,
} from '@playwright-testing-library/test/fixture'
const test = base.extend<TestingLibraryFixtures>(fixtures)
const {expect} = test
test('my form', async ({screen, within}) => {
// Screen provides `Locator` queries scoped to current Playwright `Page`
const formLocator = screen.getByTestId('my-form')
// Scope queries to `Locator` with `within`
// (note that this is a fixture from `test`, not the `within` import)
const emailInputLocator = within(formLocator).getByLabelText('Email')
// Interact via `Locator` API 🥳
await emailInputLocator.fill('[email protected]')
await emailInputLocator.press('Enter')
// Screen also provides Playwright's `Page` API
screen.goto('/account')
const emailLocator = screen.getByRole('heading', {level: 2})
// Assert via `Locator` APIs 🎉
await expect(emailLocator).toHaveText('[email protected]')
})
The findBy
queries work the same way as they do in Testing Library core in that they return Promise<Locator>
and are intended to be used to defer test execution until an element appears on the page.
test('my modal', async ({screen, within}) => {
// Here we wait for a modal to appear asynchronously before continuing
// Note: the timeout for `findBy` queries is configured with `asyncUtilTimeout`
const modalLocator = await screen.findByRole('dialog')
// Once the modal is visible, we can interact with its contents and assert
await expect(modalLocator).toHaveText(/My Modal/)
await within(modalLocator).getByRole('button', {name: 'Okay'}).click()
// We can also use `queryBy` methods to take advantage of Playwright's `Locator` auto-waiting
// See: https://playwright.dev/docs/actionability
// Note: this will use Playwright's timeout, not `asyncUtilTimeout`
await expect(screen.queryByRole('dialog')).toBeHidden()
})
🔖 Added in 4.5.0
As an alternative to the within(locator: Locator)
function you're familiar with from Testing Library, Playwright Testing Library also supports chaining queries together.
All synchronous queries (get*
+ query*
) return Locator
instances augmented with a .within()
method (TestingLibraryLocator
). All asynchronous queries (find*
) return a special LocatorPromise
that also supports .within()
. This makes it possible to chain queries, including chaining get*
, query*
and find*
interchangeably.
⚠️ Note that including anyfind*
query in the chain will make the entire chain asynchronous
test('chaining synchronous queries', async ({screen}) => {
const locator = screen.getByRole('figure').within().findByRole('img')
expect(await locator.getAttribute('alt')).toEqual('Some image')
})
test('chaining synchronous queries + asynchronous queries', ({screen}) => {
// ↓↓↓↓↓ including any `find*` queries makes the whole chain asynchronous
const locator = await screen
.getByTestId('modal-container') // Get "modal container" or throw (sync)
.within()
.findByRole('dialog') // Wait for modal to appear (async, until `asyncUtilTimeout`)
.within()
.getByRole('button', {name: 'Close'}) // Get close button within modal (sync)
expect(await locator.textContent()).toEqual('Close')
})
The Locator
query API is configured using Playwright's use
API. See Playwright's documentation for global, project, and test.
Configuring Testing Library globally in playwright.config.ts
import type {PlaywrightTestConfig} from '@playwright/test'
const config: PlaywrightTestConfig = {
use: {
// These are the defaults
testIdAttribute: 'data-testid',
asyncUtilTimeout: 1000,
asyncUtilExpectedState: 'visible',
},
}
export default config
Scoping Testing Library configuration to test suites or describe
blocks
import {test as base} from '@playwright/test'
import {
locatorFixtures as fixtures,
LocatorFixtures as TestingLibraryFixtures,
} from '@playwright-testing-library/test/fixture'
const test = base.extend<TestingLibraryFixtures>(fixtures)
const {describe, expect, use} = test
// Entire test suite
use({testIdAttribute: 'data-custom-test-id'})
describe(() => {
// Specific block
use({
testIdAttribute: 'some-other-test-id',
asyncUtilsTimeout: 5000,
asyncUtilExpectedState: 'attached',
})
test('my form', async ({screen}) => {
// ...
})
})
Using the ElementHandle
Playwright Test (@playwright/test) fixture with @playwright-testing-library/test.
⚠️ See note in Usage as you should be using theLocator
fixture if possible
import {test as base} from '@playwright/test'
import {fixtures, within, TestingLibraryFixtures} from '@playwright-testing-library/test/fixture'
const test = base.extend<TestingLibraryFixtures>(fixtures)
const {expect} = test
test('my form', async ({page, queries}) => {
// Query methods are available in `test` blocks
const formHandle = await queries.getByTestId('my-form')
// Scope queries to an `ElementHandle` with `within`
const emailInputHandle = await within(formHandle).getByLabelText('Email')
// Interact via `ElementHandle` API
await emailInputHandle.fill('[email protected]')
await emailInputHandle.press('Enter')
page.goto('/account')
const emailHandle = queries.getByRole('heading', {level: 2})
// Assert via `ElementHandle` APIs
expect(await emailHandle.textContent()).toEqual('[email protected]')
})
import {test as base} from '@playwright/test'
import {
configure,
fixtures,
within,
TestingLibraryFixtures,
} from '@playwright-testing-library/test/fixture'
const test = base.extend<TestingLibraryFixtures>(fixtures)
const {beforeEach, describe, expect} = test
// Global (these are the defaults)
configure({asyncUtilTimeout: 1000, testIdAttribute: 'data-testid'})
// Specific block
describe('my page', () => {
beforeEach(() => configure({asyncUtilTimeout: 5000, testIdAttribute: 'data-custom-test-id'}))
afterEach(() => configure({}))
test('my form', async ({page, queries}) => {
// ...
})
})
Using the ElementHandle
queries with Playwright (playwright) and playwright-testing-library.
⚠️ See note in Usage as you should be using @playwright/test with theLocator
fixture if possible. TheLocator
queries will be made available for standalone playwright in the next major release.
import {beforeAll, expect, jest, test} from '@jest/globals'
import {webkit} from 'playwright' // or 'firefox' or 'chromium'
import {getDocument, queries, within} from 'playwright-testing-library'
let browser: playwright.Browser
let page: playwright.Page
beforeAll(() => {
const browser = await webkit.launch()
const page = await browser.newPage()
})
test('my form', () => {
// Get `ElementHandle` for document from `Page`
const documentHandle = await getDocument(page)
// Global query methods take document handle as the first parameter
const formHandle = await queries.getByTestId(documentHandle, 'my-form')
// Scope queries to an `ElementHandle` with `within`
const emailInputHandle = await within(formHandle).getByLabelText('Email')
// Interact via `ElementHandle` API
await emailInputHandle.fill('[email protected]')
await emailInputHandle.press('Enter')
page.goto('/account')
const accountHandle = getDocument(page)
const emailHandle = queries.getByRole(accountHandle, 'heading', {level: 2})
// Assert via `ElementHandle` APIs
expect(await emailHandle.textContent()).toEqual('[email protected]')
})
import {beforeEach, afterEach, expect, jest, test} from '@jest/globals'
import {configure, getDocument, queries, within} from 'playwright-testing-library'
// Global (these are the defaults)
configure({asyncUtilTimeout: 1000, testIdAttribute: 'data-testid'})
// Specific block
describe('my page', () => {
beforeEach(() => configure({asyncUtilTimeout: 5000, testIdAttribute: 'data-custom-test-id'}))
afterEach(() => configure({}))
test('my form', async ({page, queries}) => {
// ...
})
})
All queries from @testing-library/dom are supported.
📝 The
find*
queries for theLocator
queries returnPromise<Locator>
which resolves when the element is found before the timeout specified viaasyncUtilTimeout
Unique methods, not part of @testing-library/dom
⚠️ These only apply to theElementHandle
queries
-
Get an
ElementHandle
for the documentgetDocument(page: playwright.Page): ElementHandle
-
Wait for an assertion (wrapper around wait-for-expect)
waitFor( expectation: () => void | Promise<void>, timeout?: number, interval?: number ): Promise<{}>
- Only
testIdAttribute
andasyncUtilTimeout
are supported as configuration options - Async utilities
waitForElement
,waitForElementToBeRemoved
andwaitForDomChange
are not exposed. Consider using afind*
query or a Playwright built-in likeLocator.waitFor()
. - The
fireEvent
method is not exposed, use Playwright's built-ins instead. - Assertion extensions from jest-dom are not compatible, use Playwright Test if possible.
-
The
getNodeText()
function is currently unsupported. -
When using a function for
TextMatch
, the function cannot reference its closure scope// ✅ This is supported screen.getByText(content => content.startsWith('Foo')) // ❌ This is not supported const startsWithFoo = (content: string) => content.startsWith('Foo') screen.getByText(content => startsWithFoo(content))
MIT
This project is actively maintained by engineers at @hoverinc 😀.