use growing customMatchers map

This commit is contained in:
Simon Knott 2024-09-02 09:09:27 +02:00
parent 1af4bc0b5d
commit e8945243e2
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
2 changed files with 37 additions and 36 deletions

View file

@ -105,17 +105,17 @@ export const printReceivedStringContainExpectedResult = (
type ExpectMessage = string | { message?: string };
function createMatchers(actual: unknown, info: ExpectMetaInfo, prefix: string[], parentPrefixes: string[][]): any {
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info, [prefix, ...parentPrefixes]));
function createMatchers(actual: unknown, info: ExpectMetaInfo, prefix: string[]): any {
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info, prefix));
}
const getPrefixSymbol = Symbol('get prefix');
const getCustomMatchersSymbol = Symbol('get prefix');
function qualifiedMatcherName(qualifier: string[], matcherName: string) {
return qualifier.join(':') + '$' + matcherName;
}
function createExpect(info: ExpectMetaInfo, prefix: string[] = [], parentPrefixes: string[][] = []) {
function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Record<string, Function>) {
const expectInstance: Expect<{}> = new Proxy(expectLibrary, {
apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) {
const [actual, messageOrOptions] = argumentsList;
@ -126,10 +126,10 @@ function createExpect(info: ExpectMetaInfo, prefix: string[] = [], parentPrefixe
throw new Error('`expect.poll()` accepts only function as a first argument');
newInfo.generator = actual as any;
}
return createMatchers(actual, newInfo, prefix, parentPrefixes);
return createMatchers(actual, newInfo, prefix);
},
get: function(target: any, property: string | typeof getPrefixSymbol) {
get: function(target: any, property: string | typeof getCustomMatchersSymbol) {
if (property === 'configure')
return configure;
@ -138,6 +138,7 @@ function createExpect(info: ExpectMetaInfo, prefix: string[] = [], parentPrefixe
const qualifier = [...prefix, createGuid()];
const wrappedMatchers: any = {};
const extendedMatchers: any = { ...customMatchers };
for (const [name, matcher] of Object.entries(matchers)) {
const key = qualifiedMatcherName(qualifier, name);
wrappedMatchers[key] = function(...args: any[]) {
@ -152,10 +153,11 @@ function createExpect(info: ExpectMetaInfo, prefix: string[] = [], parentPrefixe
return (matcher as any).call(newThis, ...args);
};
Object.defineProperty(wrappedMatchers[key], 'name', { value: name });
extendedMatchers[name] = wrappedMatchers[key];
}
expectLibrary.extend(wrappedMatchers);
return createExpect(info, qualifier, parentPrefixes);
return createExpect(info, qualifier, extendedMatchers);
};
}
@ -165,8 +167,8 @@ function createExpect(info: ExpectMetaInfo, prefix: string[] = [], parentPrefixe
};
}
if (property === getPrefixSymbol)
return { prefix, parentPrefixes };
if (property === getCustomMatchersSymbol)
return customMatchers;
if (property === 'poll') {
return (actual: unknown, messageOrOptions?: ExpectMessage & { timeout?: number, intervals?: number[] }) => {
@ -193,7 +195,7 @@ function createExpect(info: ExpectMetaInfo, prefix: string[] = [], parentPrefixe
newInfo.pollIntervals = configuration._poll.intervals;
}
}
return createExpect(newInfo, prefix, parentPrefixes);
return createExpect(newInfo, prefix, customMatchers);
};
return expectInstance;
@ -256,11 +258,11 @@ type ExpectMetaInfo = {
class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
private _info: ExpectMetaInfo;
private _prefixes: string[][];
private _prefix: string[];
constructor(info: ExpectMetaInfo, prefixes: string[][]) {
constructor(info: ExpectMetaInfo, prefix: string[]) {
this._info = { ...info };
this._prefixes = prefixes;
this._prefix = prefix;
}
get(target: Object, matcherName: string | symbol, receiver: any): any {
@ -268,15 +270,13 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
if (typeof matcherName !== 'string')
return matcher;
for (const prefix of this._prefixes) {
for (let i = prefix.length; i > 0; i--) {
const qualifiedName = qualifiedMatcherName(prefix.slice(0, i), matcherName);
for (let i = this._prefix.length; i > 0; i--) {
const qualifiedName = qualifiedMatcherName(this._prefix.slice(0, i), matcherName);
if (Reflect.has(target, qualifiedName)) {
matcher = Reflect.get(target, qualifiedName, receiver);
break;
}
}
}
if (matcher === undefined)
throw new Error(`expect: Property '${matcherName}' not found.`);
@ -403,14 +403,15 @@ function computeArgsSuffix(matcherName: string, args: any[]) {
return value ? `(${value})` : '';
}
export const expect: Expect<{}> = createExpect({}).extend(customMatchers);
export const expect: Expect<{}> = createExpect({}, [], {}).extend(customMatchers);
export function mergeExpects(...expects: any[]) {
const parentPrefixes = expects.flatMap(e => {
const internals = e[getPrefixSymbol];
let merged = expect;
for (const e of expects) {
const internals = e[getCustomMatchersSymbol];
if (!internals) // non-playwright expects mutate the global expect, so we don't need to do anything special
return [];
return [internals.prefix, ...internals.parentPrefixes];
});
return createExpect({}, undefined, parentPrefixes);
continue;
merged = merged.extend(internals);
}
return merged;
}

View file

@ -1008,8 +1008,8 @@ test('should expose timeout to custom matchers', async ({ runInlineTest, runTSC
test('should throw error when using .equals()', async ({ runInlineTest }) => {
const result = await runInlineTest({
'helper.ts': `
import { test as base, expect } from '@playwright/test';
expect.extend({
import { test as base, expect as baseExpect } from '@playwright/test';
export const expect = baseExpect.extend({
toBeWithinRange(received, floor, ceiling) {
this.equals(1, 2);
},
@ -1017,10 +1017,10 @@ test('should throw error when using .equals()', async ({ runInlineTest }) => {
export const test = base;
`,
'expect-test.spec.ts': `
import { test } from './helper';
import { test, expect } from './helper';
test('numeric ranges', () => {
test.expect(() => {
test.expect(100).toBeWithinRange(90, 110);
expect(() => {
expect(100).toBeWithinRange(90, 110);
}).toThrowError('It looks like you are using custom expect matchers that are not compatible with Playwright. See https://aka.ms/playwright/expect-compatibility');
});
`
@ -1029,23 +1029,23 @@ test('should throw error when using .equals()', async ({ runInlineTest }) => {
expect(result.passed).toBe(1);
});
test('expect.extendImmutable should work', async ({ runInlineTest }) => {
test('expect.extend should be immutable', async ({ runInlineTest }) => {
const result = await runInlineTest({
'expect-test.spec.ts': `
import { test, expect } from '@playwright/test';
const expectFoo = expect.extendImmutable({
const expectFoo = expect.extend({
toFoo() {
console.log('%%foo');
return { pass: true };
}
});
const expectFoo2 = expect.extendImmutable({
const expectFoo2 = expect.extend({
toFoo() {
console.log('%%foo2');
return { pass: true };
}
});
const expectBar = expectFoo.extendImmutable({
const expectBar = expectFoo.extend({
toBar() {
console.log('%%bar');
return { pass: true };