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'
33import { queries } from '@testing-library/dom'
44
55import { replacer } from '../helpers'
@@ -9,14 +9,19 @@ import type {
99 FindQuery ,
1010 GetQuery ,
1111 LocatorQueries as Queries ,
12+ QueriesReturn ,
1213 Query ,
1314 QueryQuery ,
15+ QueryRoot ,
1416 Screen ,
1517 SynchronousQuery ,
18+ TestingLibraryLocator ,
1619} from '../types'
1720
1821import { includes , queryToSelector } from './helpers'
1922
23+ type SynchronousQueryParameters = Parameters < Queries [ SynchronousQuery ] >
24+
2025const isAllQuery = ( query : Query ) : query is AllQuery => query . includes ( 'All' )
2126
2227const isFindQuery = ( query : Query ) : query is FindQuery => query . startsWith ( 'find' )
@@ -29,60 +34,115 @@ const synchronousQueryNames = allQueryNames.filter(isNotFindQuery)
2934const findQueryToGetQuery = ( query : FindQuery ) => query . replace ( / ^ f i n d / , 'get' ) as GetQuery
3035const findQueryToQueryQuery = ( query : FindQuery ) => query . replace ( / ^ f i n d / , 'query' ) as QueryQuery
3136
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+ }
7549
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 >
8057
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+ }
8369
84- return locator
70+ within ( ) {
71+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
72+ return queriesFor ( this , this . config )
8573 }
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+ )
86146
87147/**
88148 * Given a `Page` or `Locator` instance, return an object of Testing Library
@@ -93,21 +153,26 @@ const createFindQuery =
93153 * should use the `locatorFixtures` with **@playwright/test** instead.
94154 * @see {@link locatorFixtures }
95155 *
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
97157 * @param config Testing Library configuration to apply to queries
98158 *
99159 * @returns object containing scoped Testing Library query methods
100160 */
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 > =>
102165 allQueryNames . reduce (
103166 ( rest , query ) => ( {
104167 ...rest ,
105168 [ 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 ) ,
109174 } ) ,
110- { } as Queries ,
175+ { } as QueriesReturn < Root > ,
111176 )
112177
113178const screenFor = ( page : Page , config : Partial < Config > ) =>
@@ -119,4 +184,12 @@ const screenFor = (page: Page, config: Partial<Config>) =>
119184 } ,
120185 } ) as { proxy : Screen ; revoke : ( ) => void }
121186
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