Skip to content
This repository was archived by the owner on Aug 6, 2025. It is now read-only.

Commit 3ca72c6

Browse files
committed
feat(fixture): add locatorFixtures that provide Locator-based queries
This will likely replace the fixtures that provided `ElementHandle`-based queries in a future major release, but for now the `Locator` queries are exported as `locatorFixtures`: ```ts import { test as baseTest } from '@playwright/test' import { locatorFixtures as fixtures, LocatorFixtures as TestingLibraryFixtures, within } from '@playwright-testing-library/test/fixture'; const test = baseTest.extend<TestingLibraryFixtures>(fixtures); const {expect} = test; test('my form', async ({queries: {getByTestId}}) => { // Queries now return `Locator` const formLocator = getByTestId('my-form'); // Locator-based `within` support const {getByLabelText} = within(formLocator); const emailInputLocator = getByLabelText('Email'); // Interact via `Locator` API 🥳 await emailInputLocator.fill('email@playwright.dev'); // Assert via `Locator` APIs 🎉 await expect(emailInputLocator).toHaveValue('email@playwright.dev'); }) ```
1 parent 1a3ed05 commit 3ca72c6

File tree

9 files changed

+407
-6
lines changed

9 files changed

+407
-6
lines changed

.eslintrc.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,12 @@ module.exports = {
2525
'jest/no-done-callback': 'off',
2626
},
2727
},
28+
{
29+
files: ['lib/fixture/**/*.+(js|ts)'],
30+
rules: {
31+
'no-empty-pattern': 'off',
32+
'no-underscore-dangle': ['error', {allow: ['__testingLibraryReviver']}],
33+
},
34+
},
2835
],
2936
}

.github/workflows/build.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,18 @@ jobs:
4848
npm install @playwright/test@${{ matrix.playwright }}
4949
5050
- name: Check types, run lint + tests
51+
if: ${{ matrix.playwright == 'latest' }}
5152
run: |
5253
npm why playwright
5354
npm why @playwright/test
54-
npm run validate
55+
npm run test
56+
57+
- name: Check types, run lint + tests
58+
if: ${{ matrix.playwright != 'latest' }}
59+
run: |
60+
npm why playwright
61+
npm why @playwright/test
62+
npm run test:legacy
5563
5664
# Only release on Node 14
5765

lib/fixture/helpers.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const replacer = (_: string, value: unknown) => {
2+
if (value instanceof RegExp) return `__REGEXP ${value.toString()}`
3+
4+
return value
5+
}
6+
7+
const reviver = (_: string, value: string) => {
8+
if (value.toString().includes('__REGEXP ')) {
9+
const match = /\/(.*)\/(.*)?/.exec(value.split('__REGEXP ')[1])
10+
11+
return new RegExp(match![1], match![2] || '')
12+
}
13+
14+
return value
15+
}
16+
17+
export {replacer, reviver}

lib/fixture/index.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,39 @@
11
import {Fixtures} from '@playwright/test'
2+
3+
import type {Queries as ElementHandleQueries} from './element-handle'
4+
import type {Queries as LocatorQueries} from './locator'
5+
6+
import {queriesFixture as elementHandleQueriesFixture} from './element-handle'
27
import {
3-
Queries as ElementHandleQueries,
4-
queriesFixture as elementHandleQueriesFixture,
5-
} from './element-handle'
8+
queriesFixture as locatorQueriesFixture,
9+
registerSelectorsFixture,
10+
installTestingLibraryFixture,
11+
within,
12+
} from './locator'
613

714
const elementHandleFixtures: Fixtures = {queries: elementHandleQueriesFixture}
15+
const locatorFixtures: Fixtures = {
16+
queries: locatorQueriesFixture,
17+
registerSelectors: registerSelectorsFixture,
18+
installTestingLibrary: installTestingLibraryFixture,
19+
}
820

921
interface ElementHandleFixtures {
1022
queries: ElementHandleQueries
1123
}
1224

25+
interface LocatorFixtures {
26+
queries: LocatorQueries
27+
registerSelectors: void
28+
installTestingLibrary: void
29+
}
30+
1331
export type {ElementHandleFixtures as TestingLibraryFixtures}
1432
export {elementHandleQueriesFixture as fixture}
1533
export {elementHandleFixtures as fixtures}
1634

35+
export type {LocatorFixtures}
36+
export {locatorQueriesFixture}
37+
export {locatorFixtures, within}
38+
1739
export {configure} from '..'

lib/fixture/locator.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import {promises as fs} from 'fs'
2+
3+
import type {Locator, PlaywrightTestArgs, TestFixture} from '@playwright/test'
4+
import {selectors} from '@playwright/test'
5+
6+
import {queryNames as allQueryNames} from '../common'
7+
import type {
8+
LocatorQueries as Queries,
9+
AllQuery,
10+
FindQuery,
11+
Query,
12+
Selector,
13+
SupportedQuery,
14+
SelectorEngine,
15+
} from './types'
16+
import {replacer, reviver} from './helpers'
17+
18+
const isAllQuery = (query: Query): query is AllQuery => query.includes('All')
19+
const isNotFindQuery = (query: Query): query is Exclude<Query, FindQuery> =>
20+
!query.startsWith('find')
21+
22+
const queryNames = allQueryNames.filter(isNotFindQuery)
23+
24+
const queryToSelector = (query: SupportedQuery) =>
25+
query.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() as Selector
26+
27+
const queriesFixture: TestFixture<Queries, PlaywrightTestArgs> = async ({page}, use) => {
28+
const queries = queryNames.reduce(
29+
(rest, query) => ({
30+
...rest,
31+
[query]: (...args: Parameters<Queries[keyof Queries]>) =>
32+
page.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`),
33+
}),
34+
{} as Queries,
35+
)
36+
37+
await use(queries)
38+
}
39+
40+
const within = (locator: Locator): Queries =>
41+
queryNames.reduce(
42+
(rest, query) => ({
43+
...rest,
44+
[query]: (...args: Parameters<Queries[keyof Queries]>) =>
45+
locator.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`),
46+
}),
47+
{} as Queries,
48+
)
49+
50+
declare const queryName: SupportedQuery
51+
52+
const engine: () => SelectorEngine = () => ({
53+
query(root, selector) {
54+
const args = JSON.parse(selector, window.__testingLibraryReviver) as unknown as Parameters<
55+
Queries[typeof queryName]
56+
>
57+
58+
if (isAllQuery(queryName))
59+
throw new Error(
60+
`PlaywrightTestingLibrary: the plural '${queryName}' was used to create this Locator`,
61+
)
62+
63+
// @ts-expect-error
64+
const result = window.TestingLibraryDom[queryName](root, ...args)
65+
66+
return result
67+
},
68+
queryAll(root, selector) {
69+
const testingLibrary = window.TestingLibraryDom
70+
const args = JSON.parse(selector, window.__testingLibraryReviver) as unknown as Parameters<
71+
Queries[typeof queryName]
72+
>
73+
74+
// @ts-expect-error
75+
const result = testingLibrary[queryName](root, ...args)
76+
77+
if (!result) return []
78+
79+
return Array.isArray(result) ? result : [result]
80+
},
81+
})
82+
83+
const registerSelectorsFixture: [
84+
TestFixture<void, PlaywrightTestArgs>,
85+
{scope: 'worker'; auto?: boolean},
86+
] = [
87+
async ({}, use) => {
88+
try {
89+
await Promise.all(
90+
queryNames.map(async name =>
91+
selectors.register(
92+
queryToSelector(name),
93+
`(${engine.toString().replace(/queryName/g, `"${name}"`)})()`,
94+
),
95+
),
96+
)
97+
} catch (error) {
98+
// eslint-disable-next-line no-console
99+
console.error(
100+
'PlaywrightTestingLibrary: failed to register Testing Library functions\n',
101+
error,
102+
)
103+
}
104+
await use()
105+
},
106+
{scope: 'worker', auto: true},
107+
]
108+
109+
const installTestingLibraryFixture: [
110+
TestFixture<void, PlaywrightTestArgs>,
111+
{scope: 'test'; auto?: boolean},
112+
] = [
113+
async ({context}, use) => {
114+
const testingLibraryDomUmdScript = await fs.readFile(
115+
require.resolve('@testing-library/dom/dist/@testing-library/dom.umd.js'),
116+
'utf8',
117+
)
118+
119+
await context.addInitScript(`
120+
${testingLibraryDomUmdScript}
121+
122+
window.__testingLibraryReviver = ${reviver.toString()};
123+
`)
124+
125+
await use()
126+
},
127+
{scope: 'test', auto: true},
128+
]
129+
130+
export {queriesFixture, registerSelectorsFixture, installTestingLibraryFixture, within}
131+
export type {Queries}

lib/fixture/types.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type * as TestingLibraryDom from '@testing-library/dom'
2+
import {Locator} from '@playwright/test'
3+
import {queries} from '@testing-library/dom'
4+
import {reviver} from './helpers'
5+
6+
/**
7+
* This type was copied across from Playwright
8+
*
9+
* @see {@link https://github.com/microsoft/playwright/blob/82ff85b106e31ffd7b3702aef260c9c460cfb10c/packages/playwright-core/src/client/types.ts#L108-L117}
10+
*/
11+
export type SelectorEngine = {
12+
/**
13+
* Returns the first element matching given selector in the root's subtree.
14+
*/
15+
query(root: HTMLElement, selector: string): HTMLElement | null
16+
/**
17+
* Returns all elements matching given selector in the root's subtree.
18+
*/
19+
queryAll(root: HTMLElement, selector: string): HTMLElement[]
20+
}
21+
22+
type Queries = typeof queries
23+
24+
type StripNever<T> = {[P in keyof T as T[P] extends never ? never : P]: T[P]}
25+
type ConvertQuery<Query extends Queries[keyof Queries]> = Query extends (
26+
el: HTMLElement,
27+
...rest: infer Rest
28+
) => HTMLElement | (HTMLElement[] | null) | (HTMLElement | null)
29+
? (...args: Rest) => Locator
30+
: never
31+
32+
type KebabCase<S> = S extends `${infer C}${infer T}`
33+
? T extends Uncapitalize<T>
34+
? `${Uncapitalize<C>}${KebabCase<T>}`
35+
: `${Uncapitalize<C>}-${KebabCase<T>}`
36+
: S
37+
38+
export type LocatorQueries = StripNever<{[K in keyof Queries]: ConvertQuery<Queries[K]>}>
39+
40+
export type Query = keyof Queries
41+
42+
export type AllQuery = Extract<Query, `${string}All${string}`>
43+
export type FindQuery = Extract<Query, `find${string}`>
44+
export type SupportedQuery = Exclude<Query, FindQuery>
45+
46+
export type Selector = KebabCase<SupportedQuery>
47+
48+
declare global {
49+
interface Window {
50+
TestingLibraryDom: typeof TestingLibraryDom
51+
__testingLibraryReviver: typeof reviver
52+
}
53+
}

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@
2020
"prepublishOnly": "npm run build",
2121
"start:standalone": "hover-scripts test",
2222
"test": "run-s build:testing-library test:*",
23+
"test:legacy": "run-s build:testing-library test:standalone test:fixture:legacy",
2324
"test:fixture": "playwright test",
25+
"test:fixture:legacy": "playwright test test/fixture/element-handles.test.ts",
2426
"test:standalone": "hover-scripts test --no-watch",
25-
"test:types": "tsc --noEmit",
26-
"validate": "run-s test"
27+
"test:types": "tsc --noEmit"
2728
},
2829
"repository": {
2930
"type": "git",

0 commit comments

Comments
 (0)