chore: use explicit matcher call context (#34620)
This commit is contained in:
parent
25ef2f1344
commit
4b64c47a25
|
@ -110,13 +110,13 @@ function createMatchers(actual: unknown, info: ExpectMetaInfo, prefix: string[])
|
||||||
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info, prefix));
|
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info, prefix));
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomMatchersSymbol = Symbol('get custom matchers');
|
const userMatchersSymbol = Symbol('userMatchers');
|
||||||
|
|
||||||
function qualifiedMatcherName(qualifier: string[], matcherName: string) {
|
function qualifiedMatcherName(qualifier: string[], matcherName: string) {
|
||||||
return qualifier.join(':') + '$' + matcherName;
|
return qualifier.join(':') + '$' + matcherName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Record<string, Function>) {
|
function createExpect(info: ExpectMetaInfo, prefix: string[], userMatchers: Record<string, Function>) {
|
||||||
const expectInstance: Expect<{}> = new Proxy(expectLibrary, {
|
const expectInstance: Expect<{}> = new Proxy(expectLibrary, {
|
||||||
apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) {
|
apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) {
|
||||||
const [actual, messageOrOptions] = argumentsList;
|
const [actual, messageOrOptions] = argumentsList;
|
||||||
|
@ -130,7 +130,7 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
||||||
return createMatchers(actual, newInfo, prefix);
|
return createMatchers(actual, newInfo, prefix);
|
||||||
},
|
},
|
||||||
|
|
||||||
get: function(target: any, property: string | typeof getCustomMatchersSymbol) {
|
get: function(target: any, property: string | typeof userMatchersSymbol) {
|
||||||
if (property === 'configure')
|
if (property === 'configure')
|
||||||
return configure;
|
return configure;
|
||||||
|
|
||||||
|
@ -139,27 +139,14 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
||||||
const qualifier = [...prefix, createGuid()];
|
const qualifier = [...prefix, createGuid()];
|
||||||
|
|
||||||
const wrappedMatchers: any = {};
|
const wrappedMatchers: any = {};
|
||||||
const extendedMatchers: any = { ...customMatchers };
|
|
||||||
for (const [name, matcher] of Object.entries(matchers)) {
|
for (const [name, matcher] of Object.entries(matchers)) {
|
||||||
wrappedMatchers[name] = function(...args: any[]) {
|
wrappedMatchers[name] = wrapPlaywrightMatcherToPassNiceThis(matcher);
|
||||||
const { isNot, promise, utils } = this;
|
|
||||||
const newThis: ExpectMatcherState = {
|
|
||||||
isNot,
|
|
||||||
promise,
|
|
||||||
utils,
|
|
||||||
timeout: currentExpectTimeout()
|
|
||||||
};
|
|
||||||
(newThis as any).equals = throwUnsupportedExpectMatcherError;
|
|
||||||
return (matcher as any).call(newThis, ...args);
|
|
||||||
};
|
|
||||||
const key = qualifiedMatcherName(qualifier, name);
|
const key = qualifiedMatcherName(qualifier, name);
|
||||||
wrappedMatchers[key] = wrappedMatchers[name];
|
wrappedMatchers[key] = wrappedMatchers[name];
|
||||||
Object.defineProperty(wrappedMatchers[key], 'name', { value: name });
|
Object.defineProperty(wrappedMatchers[key], 'name', { value: name });
|
||||||
extendedMatchers[name] = wrappedMatchers[key];
|
|
||||||
}
|
}
|
||||||
expectLibrary.extend(wrappedMatchers);
|
expectLibrary.extend(wrappedMatchers);
|
||||||
|
return createExpect(info, qualifier, { ...userMatchers, ...matchers });
|
||||||
return createExpect(info, qualifier, extendedMatchers);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,8 +156,8 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (property === getCustomMatchersSymbol)
|
if (property === userMatchersSymbol)
|
||||||
return customMatchers;
|
return userMatchers;
|
||||||
|
|
||||||
if (property === 'poll') {
|
if (property === 'poll') {
|
||||||
return (actual: unknown, messageOrOptions?: ExpectMessage & { timeout?: number, intervals?: number[] }) => {
|
return (actual: unknown, messageOrOptions?: ExpectMessage & { timeout?: number, intervals?: number[] }) => {
|
||||||
|
@ -197,12 +184,56 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
||||||
newInfo.poll!.intervals = configuration._poll.intervals ?? newInfo.poll!.intervals;
|
newInfo.poll!.intervals = configuration._poll.intervals ?? newInfo.poll!.intervals;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return createExpect(newInfo, prefix, customMatchers);
|
return createExpect(newInfo, prefix, userMatchers);
|
||||||
};
|
};
|
||||||
|
|
||||||
return expectInstance;
|
return expectInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expect wraps matchers, so there is no way to pass this information to the raw Playwright matcher.
|
||||||
|
// Rely on sync call sequence to seed each matcher call with the context.
|
||||||
|
type MatcherCallContext = {
|
||||||
|
expectInfo: ExpectMetaInfo;
|
||||||
|
testInfo: TestInfoImpl | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let matcherCallContext: MatcherCallContext | undefined;
|
||||||
|
|
||||||
|
function setMatcherCallContext(context: MatcherCallContext) {
|
||||||
|
matcherCallContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeMatcherCallContext(): MatcherCallContext {
|
||||||
|
try {
|
||||||
|
return matcherCallContext!;
|
||||||
|
} finally {
|
||||||
|
matcherCallContext = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpectMatcherStateInternal = ExpectMatcherState & {
|
||||||
|
_context: MatcherCallContext | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultExpectTimeout = 5000;
|
||||||
|
|
||||||
|
function wrapPlaywrightMatcherToPassNiceThis(matcher: any) {
|
||||||
|
return function(this: any, ...args: any[]) {
|
||||||
|
const { isNot, promise, utils } = this;
|
||||||
|
const context = takeMatcherCallContext();
|
||||||
|
const timeout = context.expectInfo.timeout ?? context.testInfo?._projectInternal?.expect?.timeout ?? defaultExpectTimeout;
|
||||||
|
const newThis: ExpectMatcherStateInternal = {
|
||||||
|
isNot,
|
||||||
|
promise,
|
||||||
|
utils,
|
||||||
|
timeout,
|
||||||
|
_context: context,
|
||||||
|
};
|
||||||
|
(newThis as any).equals = throwUnsupportedExpectMatcherError;
|
||||||
|
return matcher.call(newThis, ...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function throwUnsupportedExpectMatcherError() {
|
function throwUnsupportedExpectMatcherError() {
|
||||||
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');
|
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');
|
||||||
}
|
}
|
||||||
|
@ -299,8 +330,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
}
|
}
|
||||||
return (...args: any[]) => {
|
return (...args: any[]) => {
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
// We assume that the matcher will read the current expect timeout the first thing.
|
setMatcherCallContext({ expectInfo: this._info, testInfo });
|
||||||
setCurrentExpectConfigureTimeout(this._info.timeout);
|
|
||||||
if (!testInfo)
|
if (!testInfo)
|
||||||
return matcher.call(target, ...args);
|
return matcher.call(target, ...args);
|
||||||
|
|
||||||
|
@ -362,7 +392,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, prefix: string[], ...args: any[]) {
|
async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, prefix: string[], ...args: any[]) {
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
const poll = info.poll!;
|
const poll = info.poll!;
|
||||||
const timeout = poll.timeout ?? currentExpectTimeout();
|
const timeout = poll.timeout ?? info.timeout ?? testInfo?._projectInternal?.expect?.timeout ?? defaultExpectTimeout;
|
||||||
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
|
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
|
||||||
|
|
||||||
const result = await pollAgainstDeadline<Error|undefined>(async () => {
|
const result = await pollAgainstDeadline<Error|undefined>(async () => {
|
||||||
|
@ -398,22 +428,6 @@ async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentExpectConfigureTimeout: number | undefined;
|
|
||||||
|
|
||||||
function setCurrentExpectConfigureTimeout(timeout: number | undefined) {
|
|
||||||
currentExpectConfigureTimeout = timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
function currentExpectTimeout() {
|
|
||||||
if (currentExpectConfigureTimeout !== undefined)
|
|
||||||
return currentExpectConfigureTimeout;
|
|
||||||
const testInfo = currentTestInfo();
|
|
||||||
let defaultExpectTimeout = testInfo?._projectInternal?.expect?.timeout;
|
|
||||||
if (typeof defaultExpectTimeout === 'undefined')
|
|
||||||
defaultExpectTimeout = 5000;
|
|
||||||
return defaultExpectTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeArgsSuffix(matcherName: string, args: any[]) {
|
function computeArgsSuffix(matcherName: string, args: any[]) {
|
||||||
let value = '';
|
let value = '';
|
||||||
if (matcherName === 'toHaveScreenshot')
|
if (matcherName === 'toHaveScreenshot')
|
||||||
|
@ -426,7 +440,7 @@ export const expect: Expect<{}> = createExpect({}, [], {}).extend(customMatchers
|
||||||
export function mergeExpects(...expects: any[]) {
|
export function mergeExpects(...expects: any[]) {
|
||||||
let merged = expect;
|
let merged = expect;
|
||||||
for (const e of expects) {
|
for (const e of expects) {
|
||||||
const internals = e[getCustomMatchersSymbol];
|
const internals = e[userMatchersSymbol];
|
||||||
if (!internals) // non-playwright expects mutate the global expect, so we don't need to do anything special
|
if (!internals) // non-playwright expects mutate the global expect, so we don't need to do anything special
|
||||||
continue;
|
continue;
|
||||||
merged = merged.extend(internals);
|
merged = merged.extend(internals);
|
||||||
|
|
Loading…
Reference in New Issue