@@ -110,13 +110,13 @@ function createMatchers(actual: unknown, info: ExpectMetaInfo, prefix: string[])
110110 return new Proxy ( expectLibrary ( actual ) , new ExpectMetaInfoProxyHandler ( info , prefix ) ) ;
111111}
112112
113- const getCustomMatchersSymbol = Symbol ( 'get custom matchers ' ) ;
113+ const userMatchersSymbol = Symbol ( 'userMatchers ' ) ;
114114
115115function qualifiedMatcherName ( qualifier : string [ ] , matcherName : string ) {
116116 return qualifier . join ( ':' ) + '$' + matcherName ;
117117}
118118
119- function createExpect ( info : ExpectMetaInfo , prefix : string [ ] , customMatchers : Record < string , Function > ) {
119+ function createExpect ( info : ExpectMetaInfo , prefix : string [ ] , userMatchers : Record < string , Function > ) {
120120 const expectInstance : Expect < { } > = new Proxy ( expectLibrary , {
121121 apply : function ( target : any , thisArg : any , argumentsList : [ unknown , ExpectMessage ?] ) {
122122 const [ actual , messageOrOptions ] = argumentsList ;
@@ -130,7 +130,7 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
130130 return createMatchers ( actual , newInfo , prefix ) ;
131131 } ,
132132
133- get : function ( target : any , property : string | typeof getCustomMatchersSymbol ) {
133+ get : function ( target : any , property : string | typeof userMatchersSymbol ) {
134134 if ( property === 'configure' )
135135 return configure ;
136136
@@ -139,27 +139,14 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
139139 const qualifier = [ ...prefix , createGuid ( ) ] ;
140140
141141 const wrappedMatchers : any = { } ;
142- const extendedMatchers : any = { ...customMatchers } ;
143142 for ( const [ name , matcher ] of Object . entries ( matchers ) ) {
144- wrappedMatchers [ name ] = function ( ...args : any [ ] ) {
145- const { isNot, promise, utils } = this ;
146- const newThis : ExpectMatcherState = {
147- isNot,
148- promise,
149- utils,
150- timeout : currentExpectTimeout ( )
151- } ;
152- ( newThis as any ) . equals = throwUnsupportedExpectMatcherError ;
153- return ( matcher as any ) . call ( newThis , ...args ) ;
154- } ;
143+ wrappedMatchers [ name ] = wrapPlaywrightMatcherToPassNiceThis ( matcher ) ;
155144 const key = qualifiedMatcherName ( qualifier , name ) ;
156145 wrappedMatchers [ key ] = wrappedMatchers [ name ] ;
157146 Object . defineProperty ( wrappedMatchers [ key ] , 'name' , { value : name } ) ;
158- extendedMatchers [ name ] = wrappedMatchers [ key ] ;
159147 }
160148 expectLibrary . extend ( wrappedMatchers ) ;
161-
162- return createExpect ( info , qualifier , extendedMatchers ) ;
149+ return createExpect ( info , qualifier , { ...userMatchers , ...matchers } ) ;
163150 } ;
164151 }
165152
@@ -169,8 +156,8 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
169156 } ;
170157 }
171158
172- if ( property === getCustomMatchersSymbol )
173- return customMatchers ;
159+ if ( property === userMatchersSymbol )
160+ return userMatchers ;
174161
175162 if ( property === 'poll' ) {
176163 return ( actual : unknown , messageOrOptions ?: ExpectMessage & { timeout ?: number , intervals ?: number [ ] } ) => {
@@ -197,12 +184,56 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
197184 newInfo . poll ! . intervals = configuration . _poll . intervals ?? newInfo . poll ! . intervals ;
198185 }
199186 }
200- return createExpect ( newInfo , prefix , customMatchers ) ;
187+ return createExpect ( newInfo , prefix , userMatchers ) ;
201188 } ;
202189
203190 return expectInstance ;
204191}
205192
193+ // Expect wraps matchers, so there is no way to pass this information to the raw Playwright matcher.
194+ // Rely on sync call sequence to seed each matcher call with the context.
195+ type MatcherCallContext = {
196+ expectInfo : ExpectMetaInfo ;
197+ testInfo : TestInfoImpl | null ;
198+ } ;
199+
200+ let matcherCallContext : MatcherCallContext | undefined ;
201+
202+ function setMatcherCallContext ( context : MatcherCallContext ) {
203+ matcherCallContext = context ;
204+ }
205+
206+ function takeMatcherCallContext ( ) : MatcherCallContext {
207+ try {
208+ return matcherCallContext ! ;
209+ } finally {
210+ matcherCallContext = undefined ;
211+ }
212+ }
213+
214+ type ExpectMatcherStateInternal = ExpectMatcherState & {
215+ _context : MatcherCallContext | undefined ;
216+ } ;
217+
218+ const defaultExpectTimeout = 5000 ;
219+
220+ function wrapPlaywrightMatcherToPassNiceThis ( matcher : any ) {
221+ return function ( this : any , ...args : any [ ] ) {
222+ const { isNot, promise, utils } = this ;
223+ const context = takeMatcherCallContext ( ) ;
224+ const timeout = context . expectInfo . timeout ?? context . testInfo ?. _projectInternal ?. expect ?. timeout ?? defaultExpectTimeout ;
225+ const newThis : ExpectMatcherStateInternal = {
226+ isNot,
227+ promise,
228+ utils,
229+ timeout,
230+ _context : context ,
231+ } ;
232+ ( newThis as any ) . equals = throwUnsupportedExpectMatcherError ;
233+ return matcher . call ( newThis , ...args ) ;
234+ } ;
235+ }
236+
206237function throwUnsupportedExpectMatcherError ( ) {
207238 throw new Error ( 'It looks like you are using custom expect matchers that are not compatible with Playwright. See https://aka.ms/playwright/expect-compatibility' ) ;
208239}
@@ -299,8 +330,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
299330 }
300331 return ( ...args : any [ ] ) => {
301332 const testInfo = currentTestInfo ( ) ;
302- // We assume that the matcher will read the current expect timeout the first thing.
303- setCurrentExpectConfigureTimeout ( this . _info . timeout ) ;
333+ setMatcherCallContext ( { expectInfo : this . _info , testInfo } ) ;
304334 if ( ! testInfo )
305335 return matcher . call ( target , ...args ) ;
306336
@@ -362,7 +392,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
362392async function pollMatcher ( qualifiedMatcherName : string , info : ExpectMetaInfo , prefix : string [ ] , ...args : any [ ] ) {
363393 const testInfo = currentTestInfo ( ) ;
364394 const poll = info . poll ! ;
365- const timeout = poll . timeout ?? currentExpectTimeout ( ) ;
395+ const timeout = poll . timeout ?? info . timeout ?? testInfo ?. _projectInternal ?. expect ?. timeout ?? defaultExpectTimeout ;
366396 const { deadline, timeoutMessage } = testInfo ? testInfo . _deadlineForMatcher ( timeout ) : TestInfoImpl . _defaultDeadlineForMatcher ( timeout ) ;
367397
368398 const result = await pollAgainstDeadline < Error | undefined > ( async ( ) => {
@@ -398,22 +428,6 @@ async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, p
398428 }
399429}
400430
401- let currentExpectConfigureTimeout : number | undefined ;
402-
403- function setCurrentExpectConfigureTimeout ( timeout : number | undefined ) {
404- currentExpectConfigureTimeout = timeout ;
405- }
406-
407- function currentExpectTimeout ( ) {
408- if ( currentExpectConfigureTimeout !== undefined )
409- return currentExpectConfigureTimeout ;
410- const testInfo = currentTestInfo ( ) ;
411- let defaultExpectTimeout = testInfo ?. _projectInternal ?. expect ?. timeout ;
412- if ( typeof defaultExpectTimeout === 'undefined' )
413- defaultExpectTimeout = 5000 ;
414- return defaultExpectTimeout ;
415- }
416-
417431function computeArgsSuffix ( matcherName : string , args : any [ ] ) {
418432 let value = '' ;
419433 if ( matcherName === 'toHaveScreenshot' )
@@ -426,7 +440,7 @@ export const expect: Expect<{}> = createExpect({}, [], {}).extend(customMatchers
426440export function mergeExpects ( ...expects : any [ ] ) {
427441 let merged = expect ;
428442 for ( const e of expects ) {
429- const internals = e [ getCustomMatchersSymbol ] ;
443+ const internals = e [ userMatchersSymbol ] ;
430444 if ( ! internals ) // non-playwright expects mutate the global expect, so we don't need to do anything special
431445 continue ;
432446 merged = merged . extend ( internals ) ;
0 commit comments