Skip to content

Commit 7b4b43e

Browse files
committed
chore: use explicit matcher call context
1 parent 11913e6 commit 7b4b43e

File tree

1 file changed

+45
-30
lines changed

1 file changed

+45
-30
lines changed

packages/playwright/src/matchers/expect.ts

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,7 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
141141
const wrappedMatchers: any = {};
142142
const extendedMatchers: any = { ...customMatchers };
143143
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-
};
144+
wrappedMatchers[name] = wrapPlaywrightMatcherToPassNiceThis(matcher);
155145
const key = qualifiedMatcherName(qualifier, name);
156146
wrappedMatchers[key] = wrappedMatchers[name];
157147
Object.defineProperty(wrappedMatchers[key], 'name', { value: name });
@@ -203,6 +193,47 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
203193
return expectInstance;
204194
}
205195

196+
// Expect wraps matchers, so there is no way to pass this information to the raw Playwright matcher.
197+
// Rely on sync call sequence to seed each matcher call with the context.
198+
type MatcherCallContext = {
199+
expectInfo: ExpectMetaInfo;
200+
testInfo: TestInfoImpl | null;
201+
};
202+
203+
let matcherCallContext: MatcherCallContext | undefined;
204+
205+
function setMatcherCallContext(context: MatcherCallContext) {
206+
matcherCallContext = context;
207+
}
208+
209+
function getMatcherCallContext(): MatcherCallContext {
210+
// TODO: re-implement to allow for "take" semantics.
211+
return matcherCallContext!;
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 = getMatcherCallContext();
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+
206237
function 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> {
362392
async 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 () => {
@@ -379,6 +409,7 @@ async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, p
379409
let matchers = createMatchers(value, innerInfo, prefix);
380410
if (info.isNot)
381411
matchers = matchers.not;
412+
setMatcherCallContext({ expectInfo: info, testInfo });
382413
matchers[qualifiedMatcherName](...args);
383414
return { continuePolling: false, result: undefined };
384415
} catch (error) {
@@ -398,22 +429,6 @@ async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, p
398429
}
399430
}
400431

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-
417432
function computeArgsSuffix(matcherName: string, args: any[]) {
418433
let value = '';
419434
if (matcherName === 'toHaveScreenshot')

0 commit comments

Comments
 (0)