1
- import type { Locator , Page } from '@playwright/test'
2
- import { errors } from '@playwright/test'
1
+ import type { Page } from '@playwright/test'
2
+ import { Locator , errors } from '@playwright/test'
3
3
import { queries } from '@testing-library/dom'
4
4
5
5
import { replacer } from '../helpers'
@@ -9,14 +9,19 @@ import type {
9
9
FindQuery ,
10
10
GetQuery ,
11
11
LocatorQueries as Queries ,
12
+ QueriesReturn ,
12
13
Query ,
13
14
QueryQuery ,
15
+ QueryRoot ,
14
16
Screen ,
15
17
SynchronousQuery ,
18
+ TestingLibraryLocator ,
16
19
} from '../types'
17
20
18
21
import { includes , queryToSelector } from './helpers'
19
22
23
+ type SynchronousQueryParameters = Parameters < Queries [ SynchronousQuery ] >
24
+
20
25
const isAllQuery = ( query : Query ) : query is AllQuery => query . includes ( 'All' )
21
26
22
27
const isFindQuery = ( query : Query ) : query is FindQuery => query . startsWith ( 'find' )
@@ -29,60 +34,115 @@ const synchronousQueryNames = allQueryNames.filter(isNotFindQuery)
29
34
const findQueryToGetQuery = ( query : FindQuery ) => query . replace ( / ^ f i n d / , 'get' ) as GetQuery
30
35
const findQueryToQueryQuery = ( query : FindQuery ) => query . replace ( / ^ f i n d / , 'query' ) as QueryQuery
31
36
32
- const createFindQuery =
33
- (
34
- pageOrLocator : Page | Locator ,
35
- query : FindQuery ,
36
- { asyncUtilTimeout, asyncUtilExpectedState} : Partial < Config > = { } ,
37
- ) =>
38
- async ( ...[ id , options , waitForElementOptions ] : Parameters < Queries [ FindQuery ] > ) => {
39
- const synchronousOptions = ( [ id , options ] as const ) . filter ( Boolean )
40
-
41
- const locator = pageOrLocator . locator (
42
- `${ queryToSelector ( findQueryToQueryQuery ( query ) ) } =${ JSON . stringify (
43
- synchronousOptions ,
44
- replacer ,
45
- ) } `,
46
- )
47
-
48
- const { state : expectedState = asyncUtilExpectedState , timeout = asyncUtilTimeout } =
49
- waitForElementOptions ?? { }
50
-
51
- try {
52
- await locator . first ( ) . waitFor ( { state : expectedState , timeout} )
53
- } catch ( error ) {
54
- // In the case of a `waitFor` timeout from Playwright, we want to
55
- // surface the appropriate error from Testing Library, so run the
56
- // query one more time as `get*` knowing that it will fail with the
57
- // error that we want the user to see instead of the `TimeoutError`
58
- if ( error instanceof errors . TimeoutError ) {
59
- const timeoutLocator = pageOrLocator
60
- . locator (
61
- `${ queryToSelector ( findQueryToGetQuery ( query ) ) } =${ JSON . stringify (
62
- synchronousOptions ,
63
- replacer ,
64
- ) } `,
65
- )
66
- . first ( )
67
-
68
- // Handle case where element is attached, but hidden, and the expected
69
- // state is set to `visible`. In this case, dereferencing the
70
- // `Locator` instance won't throw a `get*` query error, so just
71
- // surface the original Playwright timeout error
72
- if ( expectedState === 'visible' && ! ( await timeoutLocator . isVisible ( ) ) ) {
73
- throw error
74
- }
37
+ class LocatorPromise extends Promise < Locator > {
38
+ /**
39
+ * Wrap an `async` function `Promise` return value in a `LocatorPromise`.
40
+ * This allows us to use `async/await` and still return a custom
41
+ * `LocatorPromise` instance instead of `Promise`.
42
+ *
43
+ * @param fn
44
+ * @returns
45
+ */
46
+ static wrap < A extends any [ ] > ( fn : ( ...args : A ) => Promise < Locator > , config : Partial < Config > ) {
47
+ return ( ...args : A ) => LocatorPromise . from ( fn ( ...args ) , config )
48
+ }
75
49
76
- // In all other cases, dereferencing the `Locator` instance here should
77
- // cause the above `get*` query to throw an error in Testing Library
78
- return timeoutLocator . waitFor ( { state : expectedState , timeout} )
79
- }
50
+ static from ( promise : Promise < Locator > , config : Partial < Config > ) {
51
+ return new LocatorPromise ( ( resolve , reject ) => {
52
+ promise . then ( resolve ) . catch ( reject )
53
+ } , config )
54
+ }
55
+
56
+ config : Partial < Config >
80
57
81
- throw error
82
- }
58
+ constructor (
59
+ executor : (
60
+ resolve : ( value : Locator | PromiseLike < Locator > ) => void ,
61
+ reject : ( reason ?: any ) => void ,
62
+ ) => void ,
63
+ config : Partial < Config > ,
64
+ ) {
65
+ super ( executor )
66
+
67
+ this . config = config
68
+ }
83
69
84
- return locator
70
+ within ( ) {
71
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
72
+ return queriesFor ( this , this . config )
85
73
}
74
+ }
75
+
76
+ const locatorFor = (
77
+ root : Exclude < QueryRoot , Promise < any > > ,
78
+ query : SynchronousQuery ,
79
+ options : SynchronousQueryParameters ,
80
+ ) => root . locator ( `${ queryToSelector ( query ) } =${ JSON . stringify ( options , replacer ) } ` )
81
+
82
+ const augmentedLocatorFor = (
83
+ root : Exclude < QueryRoot , Promise < any > > ,
84
+ query : SynchronousQuery ,
85
+ options : SynchronousQueryParameters ,
86
+ config : Partial < Config > ,
87
+ ) => {
88
+ const locator = locatorFor ( root , query , options )
89
+
90
+ return new Proxy ( locator , {
91
+ get ( target , property , receiver ) {
92
+ return property === 'within'
93
+ ? // eslint-disable-next-line @typescript-eslint/no-use-before-define
94
+ ( ) => queriesFor ( target , config )
95
+ : Reflect . get ( target , property , receiver )
96
+ } ,
97
+ } ) as TestingLibraryLocator
98
+ }
99
+
100
+ const createFindQuery = (
101
+ root : QueryRoot ,
102
+ query : FindQuery ,
103
+ { asyncUtilTimeout, asyncUtilExpectedState} : Partial < Config > = { } ,
104
+ ) =>
105
+ LocatorPromise . wrap (
106
+ async ( ...[ id , options , waitForElementOptions ] : Parameters < Queries [ FindQuery ] > ) => {
107
+ const settledRoot = root instanceof LocatorPromise ? await root : root
108
+ const synchronousOptions = ( options ? [ id , options ] : [ id ] ) as SynchronousQueryParameters
109
+
110
+ const locator = locatorFor ( settledRoot , findQueryToQueryQuery ( query ) , synchronousOptions )
111
+ const { state : expectedState = asyncUtilExpectedState , timeout = asyncUtilTimeout } =
112
+ waitForElementOptions ?? { }
113
+
114
+ try {
115
+ await locator . first ( ) . waitFor ( { state : expectedState , timeout} )
116
+ } catch ( error ) {
117
+ // In the case of a `waitFor` timeout from Playwright, we want to
118
+ // surface the appropriate error from Testing Library, so run the
119
+ // query one more time as `get*` knowing that it will fail with the
120
+ // error that we want the user to see instead of the `TimeoutError`
121
+ if ( error instanceof errors . TimeoutError ) {
122
+ const timeoutLocator = locatorFor (
123
+ settledRoot ,
124
+ findQueryToGetQuery ( query ) ,
125
+ synchronousOptions ,
126
+ ) . first ( )
127
+
128
+ // Handle case where element is attached, but hidden, and the expected
129
+ // state is set to `visible`. In this case, dereferencing the
130
+ // `Locator` instance won't throw a `get*` query error, so just
131
+ // surface the original Playwright timeout error
132
+ if ( expectedState === 'visible' && ! ( await timeoutLocator . isVisible ( ) ) ) {
133
+ throw error
134
+ }
135
+
136
+ // In all other cases, dereferencing the `Locator` instance here should
137
+ // cause the above `get*` query to throw an error in Testing Library
138
+ await timeoutLocator . waitFor ( { state : expectedState , timeout} )
139
+ }
140
+ }
141
+
142
+ return locator
143
+ } ,
144
+ { asyncUtilExpectedState, asyncUtilTimeout} ,
145
+ )
86
146
87
147
/**
88
148
* Given a `Page` or `Locator` instance, return an object of Testing Library
@@ -93,21 +153,26 @@ const createFindQuery =
93
153
* should use the `locatorFixtures` with **@playwright/test** instead.
94
154
* @see {@link locatorFixtures }
95
155
*
96
- * @param pageOrLocator `Page` or `Locator` instance to use as the query root
156
+ * @param root `Page` or `Locator` instance to use as the query root
97
157
* @param config Testing Library configuration to apply to queries
98
158
*
99
159
* @returns object containing scoped Testing Library query methods
100
160
*/
101
- const queriesFor = ( pageOrLocator : Page | Locator , config ?: Partial < Config > ) =>
161
+ const queriesFor = < Root extends QueryRoot > (
162
+ root : Root ,
163
+ config : Partial < Config > ,
164
+ ) : QueriesReturn < Root > =>
102
165
allQueryNames . reduce (
103
166
( rest , query ) => ( {
104
167
...rest ,
105
168
[ query ] : isFindQuery ( query )
106
- ? createFindQuery ( pageOrLocator , query , config )
107
- : ( ...args : Parameters < Queries [ SynchronousQuery ] > ) =>
108
- pageOrLocator . locator ( `${ queryToSelector ( query ) } =${ JSON . stringify ( args , replacer ) } ` ) ,
169
+ ? createFindQuery ( root , query , config )
170
+ : ( ...options : SynchronousQueryParameters ) =>
171
+ root instanceof LocatorPromise
172
+ ? root . then ( r => locatorFor ( r , query , options ) )
173
+ : augmentedLocatorFor ( root , query , options , config ) ,
109
174
} ) ,
110
- { } as Queries ,
175
+ { } as QueriesReturn < Root > ,
111
176
)
112
177
113
178
const screenFor = ( page : Page , config : Partial < Config > ) =>
@@ -119,4 +184,12 @@ const screenFor = (page: Page, config: Partial<Config>) =>
119
184
} ,
120
185
} ) as { proxy : Screen ; revoke : ( ) => void }
121
186
122
- export { allQueryNames , isAllQuery , isNotFindQuery , queriesFor , screenFor , synchronousQueryNames }
187
+ export {
188
+ LocatorPromise ,
189
+ allQueryNames ,
190
+ isAllQuery ,
191
+ isNotFindQuery ,
192
+ queriesFor ,
193
+ screenFor ,
194
+ synchronousQueryNames ,
195
+ }
0 commit comments