Skip to content

Commit 9ca0808

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 protected]'); // Assert via `Locator` APIs 🎉 await expect(emailInputLocator).toHaveValue('[email protected]'); }) ```
1 parent 7d1125c commit 9ca0808

File tree

9 files changed

+409
-6
lines changed

9 files changed

+409
-6
lines changed

.eslintrc.js

+7
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,12 @@ module.exports = {
2828
'jest/no-done-callback': 'off',
2929
},
3030
},
31+
{
32+
files: ['lib/fixture/**/*.+(js|ts)'],
33+
rules: {
34+
'no-empty-pattern': 'off',
35+
'no-underscore-dangle': ['error', {allow: ['__testingLibraryReviver']}],
36+
},
37+
},
3138
],
3239
}

.github/workflows/build.yml

+9-1
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,18 @@ jobs:
5151
run: npm run test:types
5252

5353
- name: Check types, run lint + tests
54+
if: ${{ matrix.playwright == 'latest' }}
5455
run: |
5556
npm why playwright
5657
npm why @playwright/test
57-
npm run validate
58+
npm run test
59+
60+
- name: Check types, run lint + tests
61+
if: ${{ matrix.playwright != 'latest' }}
62+
run: |
63+
npm why playwright
64+
npm why @playwright/test
65+
npm run test:legacy
5866
5967
# Only release on Node 14
6068

lib/fixture/helpers.ts

+17
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

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,38 @@
11
import {Fixtures} from '@playwright/test'
22

3+
import type {Queries as ElementHandleQueries} from './element-handle'
4+
import {queriesFixture as elementHandleQueriesFixture} from './element-handle'
5+
import type {Queries as LocatorQueries} from './locator'
36
import {
4-
Queries as ElementHandleQueries,
5-
queriesFixture as elementHandleQueriesFixture,
6-
} from './element-handle'
7+
installTestingLibraryFixture,
8+
queriesFixture as locatorQueriesFixture,
9+
registerSelectorsFixture,
10+
within,
11+
} from './locator'
712

813
const elementHandleFixtures: Fixtures = {queries: elementHandleQueriesFixture}
14+
const locatorFixtures: Fixtures = {
15+
queries: locatorQueriesFixture,
16+
registerSelectors: registerSelectorsFixture,
17+
installTestingLibrary: installTestingLibraryFixture,
18+
}
919

1020
interface ElementHandleFixtures {
1121
queries: ElementHandleQueries
1222
}
1323

24+
interface LocatorFixtures {
25+
queries: LocatorQueries
26+
registerSelectors: void
27+
installTestingLibrary: void
28+
}
29+
1430
export type {ElementHandleFixtures as TestingLibraryFixtures}
1531
export {elementHandleQueriesFixture as fixture}
1632
export {elementHandleFixtures as fixtures}
1733

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

lib/fixture/locator.ts

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

lib/fixture/types.ts

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

package.json

+3-2
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)