Skip to content
This repository was archived by the owner on Aug 6, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Unique methods, not part of **@testing-library/dom**

---

The **[@testing-library/dom](https://github.com/testing-library/dom-testing-library#usage)** — All **`get*`** and **`query*`** methods are supported.
The **[@testing-library/dom](https://github.com/testing-library/dom-testing-library#usage)** — All **`get*`**, **`query*`**, and **`find*`** methods are supported.

- `getQueriesForElement(handle: ElementHandle): ElementHandle & QueryUtils` - extend the input object with the query API and return it
- `getNodeText(handle: ElementHandle): Promise<string>` - get the text content of the element
Expand Down
23 changes: 14 additions & 9 deletions lib/fixture/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import {Fixtures} from '@playwright/test'

import {Config} from '../common'

import type {Queries as ElementHandleQueries} from './element-handle'
import {queriesFixture as elementHandleQueriesFixture} from './element-handle'
import type {Queries as LocatorQueries} from './locator'
import {
installTestingLibraryFixture,
queriesFixture as locatorQueriesFixture,
options,
queriesFor,
registerSelectorsFixture,
within,
withinFixture,
} from './locator'
import type {Config} from './types'
import {Within} from './types'

const elementHandleFixtures: Fixtures = {queries: elementHandleQueriesFixture}
const locatorFixtures: Fixtures = {
queries: locatorQueriesFixture,
within: withinFixture,
registerSelectors: registerSelectorsFixture,
installTestingLibrary: installTestingLibraryFixture,
...options,
Expand All @@ -27,15 +29,18 @@ interface ElementHandleFixtures {

interface LocatorFixtures extends Partial<Config> {
queries: LocatorQueries
within: Within
registerSelectors: void
installTestingLibrary: void
}

export {configure} from '..'

export type {ElementHandleFixtures as TestingLibraryFixtures}
export {elementHandleQueriesFixture as fixture}
export {elementHandleFixtures as fixtures}
export type {LocatorFixtures}
export {locatorQueriesFixture}
export {locatorFixtures, within}
export type {ElementHandleFixtures as TestingLibraryFixtures, LocatorFixtures}
export {
locatorFixtures,
locatorQueriesFixture,
elementHandleQueriesFixture as fixture,
elementHandleFixtures as fixtures,
queriesFor,
}
46 changes: 37 additions & 9 deletions lib/fixture/locator/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type {Locator, PlaywrightTestArgs, TestFixture} from '@playwright/test'
import {selectors} from '@playwright/test'

import type {Config, LocatorQueries as Queries, SelectorEngine, SynchronousQuery} from '../types'
import type {
Config,
LocatorQueries as Queries,
SelectorEngine,
SynchronousQuery,
Within,
} from '../types'

import {
buildTestingLibraryScript,
Expand All @@ -11,16 +17,30 @@ import {
synchronousQueryNames,
} from './helpers'

const defaultConfig: Config = {testIdAttribute: 'data-testid', asyncUtilTimeout: 1000}
type TestArguments = PlaywrightTestArgs & Config

const defaultConfig: Config = {
asyncUtilExpectedState: 'visible',
asyncUtilTimeout: 1000,
testIdAttribute: 'data-testid',
}

const options = Object.fromEntries(
Object.entries(defaultConfig).map(([key, value]) => [key, [value, {option: true}] as const]),
)

const queriesFixture: TestFixture<Queries, PlaywrightTestArgs> = async ({page}, use) =>
use(queriesFor(page))
const queriesFixture: TestFixture<Queries, TestArguments> = async (
{page, asyncUtilExpectedState, asyncUtilTimeout},
use,
) => use(queriesFor(page, {asyncUtilExpectedState, asyncUtilTimeout}))

const within = (locator: Locator): Queries => queriesFor(locator)
const withinFixture: TestFixture<Within, TestArguments> = async (
{asyncUtilExpectedState, asyncUtilTimeout},
use,
) =>
use(
(locator: Locator): Queries => queriesFor(locator, {asyncUtilExpectedState, asyncUtilTimeout}),
)

declare const queryName: SynchronousQuery

Expand Down Expand Up @@ -82,18 +102,26 @@ const registerSelectorsFixture: [
]

const installTestingLibraryFixture: [
TestFixture<void, PlaywrightTestArgs & Config>,
TestFixture<void, TestArguments>,
{scope: 'test'; auto?: boolean},
] = [
async ({context, asyncUtilTimeout, testIdAttribute}, use) => {
async ({context, asyncUtilExpectedState, asyncUtilTimeout, testIdAttribute}, use) => {
await context.addInitScript(
await buildTestingLibraryScript({config: {asyncUtilTimeout, testIdAttribute}}),
await buildTestingLibraryScript({
config: {asyncUtilExpectedState, asyncUtilTimeout, testIdAttribute},
}),
)

await use()
},
{scope: 'test', auto: true},
]

export {installTestingLibraryFixture, options, queriesFixture, registerSelectorsFixture, within}
export {
installTestingLibraryFixture,
options,
queriesFixture,
registerSelectorsFixture,
withinFixture,
}
export type {Queries}
75 changes: 71 additions & 4 deletions lib/fixture/locator/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {promises as fs} from 'fs'

import type {Locator, Page} from '@playwright/test'
import {errors} from '@playwright/test'
import {queries} from '@testing-library/dom'

import {configureTestingLibraryScript} from '../../common'
Expand All @@ -9,18 +10,25 @@ import type {
AllQuery,
Config,
FindQuery,
GetQuery,
LocatorQueries as Queries,
Query,
QueryQuery,
Selector,
SynchronousQuery,
} from '../types'

const allQueryNames = Object.keys(queries) as Query[]

const isAllQuery = (query: Query): query is AllQuery => query.includes('All')

const isFindQuery = (query: Query): query is FindQuery => query.startsWith('find')
const isNotFindQuery = (query: Query): query is Exclude<Query, FindQuery> =>
!query.startsWith('find')

const findQueryToGetQuery = (query: FindQuery) => query.replace(/^find/, 'get') as GetQuery
const findQueryToQueryQuery = (query: FindQuery) => query.replace(/^find/, 'query') as QueryQuery

const queryToSelector = (query: SynchronousQuery) =>
query.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() as Selector

Expand All @@ -41,12 +49,71 @@ const buildTestingLibraryScript = async ({config}: {config: Config}) => {

const synchronousQueryNames = allQueryNames.filter(isNotFindQuery)

const queriesFor = (pageOrLocator: Page | Locator) =>
synchronousQueryNames.reduce(
const createFindQuery =
(
pageOrLocator: Page | Locator,
query: FindQuery,
{asyncUtilTimeout, asyncUtilExpectedState}: Partial<Config> = {},
) =>
async (...[id, options, waitForElementOptions]: Parameters<Queries[FindQuery]>) => {
const synchronousOptions = ([id, options] as const).filter(Boolean)

const locator = pageOrLocator.locator(
`${queryToSelector(findQueryToQueryQuery(query))}=${JSON.stringify(
synchronousOptions,
replacer,
)}`,
)

const {state = asyncUtilExpectedState, timeout = asyncUtilTimeout} = waitForElementOptions ?? {}

try {
await locator.first().waitFor({state, timeout})
} catch (error) {
// In the case of a `waitFor` timeout from Playwright, we want to
// surface the appropriate error from Testing Library, so run the
// query one more time as `get*` knowing that it will fail with the
// error that we want the user to see instead of the `TimeoutError`
if (error instanceof errors.TimeoutError) {
return pageOrLocator
.locator(
`${queryToSelector(findQueryToGetQuery(query))}=${JSON.stringify(
synchronousOptions,
replacer,
)}`,
)
.first()
.waitFor({state, timeout: 100})
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bleh, this hard-coded timeout is to handle the hidden element case I mentioned where getBy* doesn't throw 😑.

}

throw error
}

return locator
}

/**
* Given a `Page` or `Locator` instance, return an object of Testing Library
* query methods that return a `Locator` instance for the queried element
*
* @internal this API is not currently intended for public usage and may be
* removed or changed outside of semantic release versioning. If possible, you
* should use the `locatorFixtures` with **@playwright/test** instead.
* @see {@link locatorFixtures}
*
* @param pageOrLocator `Page` or `Locator` instance to use as the query root
* @param config Testing Library configuration to apply to queries
*
* @returns object containing scoped Testing Library query methods
*/
const queriesFor = (pageOrLocator: Page | Locator, config?: Partial<Config>) =>
allQueryNames.reduce(
(rest, query) => ({
...rest,
[query]: (...args: Parameters<Queries[keyof Queries]>) =>
pageOrLocator.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`),
[query]: isFindQuery(query)
? createFindQuery(pageOrLocator, query, config)
: (...args: Parameters<Queries[SynchronousQuery]>) =>
pageOrLocator.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`),
}),
{} as Queries,
)
Expand Down
3 changes: 2 additions & 1 deletion lib/fixture/locator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export {
options,
queriesFixture,
registerSelectorsFixture,
within,
withinFixture,
} from './fixtures'
export type {Queries} from './fixtures'
export {queriesFor} from './helpers'
23 changes: 19 additions & 4 deletions lib/fixture/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {Locator} from '@playwright/test'
import type * as TestingLibraryDom from '@testing-library/dom'
import {queries} from '@testing-library/dom'

import {Config} from '../common'
import type {Config as CommonConfig} from '../common'

import {reviver} from './helpers'

Expand All @@ -23,13 +23,25 @@ export type SelectorEngine = {
}

type Queries = typeof queries
type WaitForState = Exclude<Parameters<Locator['waitFor']>[0], undefined>['state']
type AsyncUtilExpectedState = Extract<WaitForState, 'visible' | 'attached'>

type StripNever<T> = {[P in keyof T as T[P] extends never ? never : P]: T[P]}
type ConvertQuery<Query extends Queries[keyof Queries]> = Query extends (
el: HTMLElement,
...rest: infer Rest
) => HTMLElement | (HTMLElement[] | null) | (HTMLElement | null)
? (...args: Rest) => Locator
: Query extends (
el: HTMLElement,
id: infer Id,
options: infer Options,
waitForOptions: infer WaitForOptions,
) => Promise<any>
? (
id: Id,
options?: Options,
waitForOptions?: WaitForOptions & {state?: AsyncUtilExpectedState},
) => Promise<Locator>
: never

type KebabCase<S> = S extends `${infer C}${infer T}`
Expand All @@ -38,19 +50,22 @@ type KebabCase<S> = S extends `${infer C}${infer T}`
: `${Uncapitalize<C>}-${KebabCase<T>}`
: S

export type LocatorQueries = StripNever<{[K in keyof Queries]: ConvertQuery<Queries[K]>}>
export type LocatorQueries = {[K in keyof Queries]: ConvertQuery<Queries[K]>}
export type Within = (locator: Locator) => LocatorQueries

export type Query = keyof Queries

export type AllQuery = Extract<Query, `${string}All${string}`>
export type FindQuery = Extract<Query, `find${string}`>
export type GetQuery = Extract<Query, `get${string}`>
export type QueryQuery = Extract<Query, `query${string}`>
export type SynchronousQuery = Exclude<Query, FindQuery>

export type Selector = KebabCase<SynchronousQuery>

export type {Config}
export interface Config extends CommonConfig {
asyncUtilExpectedState: AsyncUtilExpectedState
}
export interface ConfigFn {
(existingConfig: Config): Partial<Config>
}
Expand Down
13 changes: 13 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,19 @@ export function wait(

export const waitFor = wait

/**
* Configuration API for legacy queries that return `ElementHandle` instances.
* Only `testIdAttribute` and `asyncUtilTimeout` are currently supported.

* @see {@link https://testing-library.com/docs/dom-testing-library/api-configuration}
*
* ⚠️ This API has no effect on the queries that return `Locator` instances. Use
* `test.use` instead to configure the `Locator` queries.
*
* @see {@link https://github.com/testing-library/playwright-testing-library/releases/tag/v4.4.0-beta.2}
*
* @param config
*/
export function configure(config: Partial<Config>): void {
if (!config) {
return
Expand Down
Loading