Skip to content

Commit 1b888cf

Browse files
committed
feat(fixture): add support for find* queries in locator fixture
1 parent f1e09ca commit 1b888cf

File tree

7 files changed

+236
-25
lines changed

7 files changed

+236
-25
lines changed

lib/fixture/index.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import {Fixtures} from '@playwright/test'
22

3-
import {Config} from '../common'
4-
53
import type {Queries as ElementHandleQueries} from './element-handle'
64
import {queriesFixture as elementHandleQueriesFixture} from './element-handle'
75
import type {Queries as LocatorQueries} from './locator'
@@ -10,12 +8,15 @@ import {
108
queriesFixture as locatorQueriesFixture,
119
options,
1210
registerSelectorsFixture,
13-
within,
11+
withinFixture,
1412
} from './locator'
13+
import type {Config} from './types'
14+
import {Within} from './types'
1515

1616
const elementHandleFixtures: Fixtures = {queries: elementHandleQueriesFixture}
1717
const locatorFixtures: Fixtures = {
1818
queries: locatorQueriesFixture,
19+
within: withinFixture,
1920
registerSelectors: registerSelectorsFixture,
2021
installTestingLibrary: installTestingLibraryFixture,
2122
...options,
@@ -27,6 +28,7 @@ interface ElementHandleFixtures {
2728

2829
interface LocatorFixtures extends Partial<Config> {
2930
queries: LocatorQueries
31+
within: Within
3032
registerSelectors: void
3133
installTestingLibrary: void
3234
}
@@ -38,4 +40,4 @@ export {elementHandleQueriesFixture as fixture}
3840
export {elementHandleFixtures as fixtures}
3941
export type {LocatorFixtures}
4042
export {locatorQueriesFixture}
41-
export {locatorFixtures, within}
43+
export {locatorFixtures}

lib/fixture/locator/fixtures.ts

+37-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import type {Locator, PlaywrightTestArgs, TestFixture} from '@playwright/test'
22
import {selectors} from '@playwright/test'
33

4-
import type {Config, LocatorQueries as Queries, SelectorEngine, SynchronousQuery} from '../types'
4+
import type {
5+
Config,
6+
LocatorQueries as Queries,
7+
SelectorEngine,
8+
SynchronousQuery,
9+
Within,
10+
} from '../types'
511

612
import {
713
buildTestingLibraryScript,
@@ -11,16 +17,30 @@ import {
1117
synchronousQueryNames,
1218
} from './helpers'
1319

14-
const defaultConfig: Config = {testIdAttribute: 'data-testid', asyncUtilTimeout: 1000}
20+
type TestArguments = PlaywrightTestArgs & Config
21+
22+
const defaultConfig: Config = {
23+
asyncUtilExpectedState: 'visible',
24+
asyncUtilTimeout: 1000,
25+
testIdAttribute: 'data-testid',
26+
}
1527

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

20-
const queriesFixture: TestFixture<Queries, PlaywrightTestArgs> = async ({page}, use) =>
21-
use(queriesFor(page))
32+
const queriesFixture: TestFixture<Queries, TestArguments> = async (
33+
{page, asyncUtilExpectedState, asyncUtilTimeout},
34+
use,
35+
) => use(queriesFor(page, {asyncUtilExpectedState, asyncUtilTimeout}))
2236

23-
const within = (locator: Locator): Queries => queriesFor(locator)
37+
const withinFixture: TestFixture<Within, TestArguments> = async (
38+
{asyncUtilExpectedState, asyncUtilTimeout},
39+
use,
40+
) =>
41+
use(
42+
(locator: Locator): Queries => queriesFor(locator, {asyncUtilExpectedState, asyncUtilTimeout}),
43+
)
2444

2545
declare const queryName: SynchronousQuery
2646

@@ -82,18 +102,26 @@ const registerSelectorsFixture: [
82102
]
83103

84104
const installTestingLibraryFixture: [
85-
TestFixture<void, PlaywrightTestArgs & Config>,
105+
TestFixture<void, TestArguments>,
86106
{scope: 'test'; auto?: boolean},
87107
] = [
88-
async ({context, asyncUtilTimeout, testIdAttribute}, use) => {
108+
async ({context, asyncUtilExpectedState, asyncUtilTimeout, testIdAttribute}, use) => {
89109
await context.addInitScript(
90-
await buildTestingLibraryScript({config: {asyncUtilTimeout, testIdAttribute}}),
110+
await buildTestingLibraryScript({
111+
config: {asyncUtilExpectedState, asyncUtilTimeout, testIdAttribute},
112+
}),
91113
)
92114

93115
await use()
94116
},
95117
{scope: 'test', auto: true},
96118
]
97119

98-
export {installTestingLibraryFixture, options, queriesFixture, registerSelectorsFixture, within}
120+
export {
121+
installTestingLibraryFixture,
122+
options,
123+
queriesFixture,
124+
registerSelectorsFixture,
125+
withinFixture,
126+
}
99127
export type {Queries}

lib/fixture/locator/helpers.ts

+57-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {promises as fs} from 'fs'
22

33
import type {Locator, Page} from '@playwright/test'
4+
import {errors} from '@playwright/test'
45
import {queries} from '@testing-library/dom'
56

67
import {configureTestingLibraryScript} from '../../common'
@@ -9,18 +10,25 @@ import type {
910
AllQuery,
1011
Config,
1112
FindQuery,
13+
GetQuery,
1214
LocatorQueries as Queries,
1315
Query,
16+
QueryQuery,
1417
Selector,
1518
SynchronousQuery,
1619
} from '../types'
1720

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

2023
const isAllQuery = (query: Query): query is AllQuery => query.includes('All')
24+
25+
const isFindQuery = (query: Query): query is FindQuery => query.startsWith('find')
2126
const isNotFindQuery = (query: Query): query is Exclude<Query, FindQuery> =>
2227
!query.startsWith('find')
2328

29+
const findQueryToGetQuery = (query: FindQuery) => query.replace(/^find/, 'get') as GetQuery
30+
const findQueryToQueryQuery = (query: FindQuery) => query.replace(/^find/, 'query') as QueryQuery
31+
2432
const queryToSelector = (query: SynchronousQuery) =>
2533
query.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() as Selector
2634

@@ -41,12 +49,57 @@ const buildTestingLibraryScript = async ({config}: {config: Config}) => {
4149

4250
const synchronousQueryNames = allQueryNames.filter(isNotFindQuery)
4351

44-
const queriesFor = (pageOrLocator: Page | Locator) =>
45-
synchronousQueryNames.reduce(
52+
const createFindQuery =
53+
(
54+
pageOrLocator: Page | Locator,
55+
query: FindQuery,
56+
{asyncUtilTimeout, asyncUtilExpectedState}: Partial<Config> = {},
57+
) =>
58+
async (...[id, options, waitForElementOptions]: Parameters<Queries[FindQuery]>) => {
59+
const synchronousOptions = ([id, options] as const).filter(Boolean)
60+
61+
const locator = pageOrLocator.locator(
62+
`${queryToSelector(findQueryToQueryQuery(query))}=${JSON.stringify(
63+
synchronousOptions,
64+
replacer,
65+
)}`,
66+
)
67+
68+
const {state = asyncUtilExpectedState, timeout = asyncUtilTimeout} = waitForElementOptions ?? {}
69+
70+
try {
71+
await locator.first().waitFor({state, timeout})
72+
} catch (error) {
73+
// In the case of a `waitFor` timeout from Playwright, we want to
74+
// surface the appropriate error from Testing Library, so run the
75+
// query one more time as `get*` knowing that it will fail with the
76+
// error that we want the user to see instead of the `TimeoutError`
77+
if (error instanceof errors.TimeoutError) {
78+
return pageOrLocator
79+
.locator(
80+
`${queryToSelector(findQueryToGetQuery(query))}=${JSON.stringify(
81+
synchronousOptions,
82+
replacer,
83+
)}`,
84+
)
85+
.first()
86+
.waitFor({state, timeout: 100})
87+
}
88+
89+
throw error
90+
}
91+
92+
return locator
93+
}
94+
95+
const queriesFor = (pageOrLocator: Page | Locator, config?: Partial<Config>) =>
96+
allQueryNames.reduce(
4697
(rest, query) => ({
4798
...rest,
48-
[query]: (...args: Parameters<Queries[keyof Queries]>) =>
49-
pageOrLocator.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`),
99+
[query]: isFindQuery(query)
100+
? createFindQuery(pageOrLocator, query, config)
101+
: (...args: Parameters<Queries[SynchronousQuery]>) =>
102+
pageOrLocator.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`),
50103
}),
51104
{} as Queries,
52105
)

lib/fixture/locator/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ export {
33
options,
44
queriesFixture,
55
registerSelectorsFixture,
6-
within,
6+
withinFixture,
77
} from './fixtures'
88
export type {Queries} from './fixtures'

lib/fixture/types.ts

+19-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {Locator} from '@playwright/test'
22
import type * as TestingLibraryDom from '@testing-library/dom'
33
import {queries} from '@testing-library/dom'
44

5-
import {Config} from '../common'
5+
import type {Config as CommonConfig} from '../common'
66

77
import {reviver} from './helpers'
88

@@ -23,13 +23,25 @@ export type SelectorEngine = {
2323
}
2424

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

27-
type StripNever<T> = {[P in keyof T as T[P] extends never ? never : P]: T[P]}
2829
type ConvertQuery<Query extends Queries[keyof Queries]> = Query extends (
2930
el: HTMLElement,
3031
...rest: infer Rest
3132
) => HTMLElement | (HTMLElement[] | null) | (HTMLElement | null)
3233
? (...args: Rest) => Locator
34+
: Query extends (
35+
el: HTMLElement,
36+
id: infer Id,
37+
options: infer Options,
38+
waitForOptions: infer WaitForOptions,
39+
) => Promise<any>
40+
? (
41+
id: Id,
42+
options?: Options,
43+
waitForOptions?: WaitForOptions & {state?: AsyncUtilExpectedState},
44+
) => Promise<Locator>
3345
: never
3446

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

41-
export type LocatorQueries = StripNever<{[K in keyof Queries]: ConvertQuery<Queries[K]>}>
53+
export type LocatorQueries = {[K in keyof Queries]: ConvertQuery<Queries[K]>}
4254
export type Within = (locator: Locator) => LocatorQueries
4355

4456
export type Query = keyof Queries
4557

4658
export type AllQuery = Extract<Query, `${string}All${string}`>
4759
export type FindQuery = Extract<Query, `find${string}`>
4860
export type GetQuery = Extract<Query, `get${string}`>
61+
export type QueryQuery = Extract<Query, `query${string}`>
4962
export type SynchronousQuery = Exclude<Query, FindQuery>
5063

5164
export type Selector = KebabCase<SynchronousQuery>
5265

53-
export type {Config}
66+
export interface Config extends CommonConfig {
67+
asyncUtilExpectedState: AsyncUtilExpectedState
68+
}
5469
export interface ConfigFn {
5570
(existingConfig: Config): Partial<Config>
5671
}

0 commit comments

Comments
 (0)