feat(test runner): expect.extendImmutable

This commit is contained in:
Simon Knott 2024-08-29 10:16:54 +02:00
parent 6763d5ab6b
commit 2ca4a01af2
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
2 changed files with 89 additions and 20 deletions

View file

@ -61,6 +61,7 @@ import {
import { zones } from 'playwright-core/lib/utils';
import { TestInfoImpl } from '../worker/testInfo';
import { ExpectError, isExpectError } from './matcherHint';
import { randomUUID } from 'node:crypto';
// #region
// Mirrored from https://github.com/facebook/jest/blob/f13abff8df9a0e1148baf3584bcde6d1b479edc7/packages/expect/src/print.ts
@ -104,11 +105,30 @@ export const printReceivedStringContainExpectedResult = (
type ExpectMessage = string | { message?: string };
function createMatchers(actual: unknown, info: ExpectMetaInfo): any {
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info));
function createMatchers(actual: unknown, info: ExpectMetaInfo, prefix: string[]): any {
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info, prefix));
}
function createExpect(info: ExpectMetaInfo) {
function createExpect(info: ExpectMetaInfo, prefix: string[] = []) {
function extend(matchers: any, qualifier: string) {
const wrappedMatchers: any = {};
for (const [name, matcher] of Object.entries(matchers)) {
wrappedMatchers[qualifier + name] = function(...args: any[]) {
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);
};
}
expectLibrary.extend(wrappedMatchers);
}
const expectInstance: Expect<{}> = new Proxy(expectLibrary, {
apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) {
const [actual, messageOrOptions] = argumentsList;
@ -119,7 +139,7 @@ function createExpect(info: ExpectMetaInfo) {
throw new Error('`expect.poll()` accepts only function as a first argument');
newInfo.generator = actual as any;
}
return createMatchers(actual, newInfo);
return createMatchers(actual, newInfo, prefix);
},
get: function(target: any, property: string) {
@ -128,25 +148,20 @@ function createExpect(info: ExpectMetaInfo) {
if (property === 'extend') {
return (matchers: any) => {
const wrappedMatchers: any = {};
for (const [name, matcher] of Object.entries(matchers)) {
wrappedMatchers[name] = function(...args: any[]) {
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);
};
}
expectLibrary.extend(wrappedMatchers);
extend(matchers, '');
return expectInstance;
};
}
if (property === 'extendImmutable') {
return (matchers: any) => {
const key = randomUUID();
const qualifier = [...prefix, key];
extend(matchers, `${qualifier.join(':')}$`);
return createExpect(info, qualifier);
};
}
if (property === 'soft') {
return (actual: unknown, messageOrOptions?: ExpectMessage) => {
return configure({ soft: true })(actual, messageOrOptions) as any;
@ -241,13 +256,26 @@ type ExpectMetaInfo = {
class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
private _info: ExpectMetaInfo;
private _prefix: string[];
constructor(info: ExpectMetaInfo) {
constructor(info: ExpectMetaInfo, prefix: string[]) {
this._info = { ...info };
this._prefix = prefix;
}
get(target: Object, matcherName: string | symbol, receiver: any): any {
let matcher = Reflect.get(target, matcherName, receiver);
if (typeof matcherName === 'string') {
for (let i = this._prefix.length; i > 0; i--) {
const qualifiedName = `${this._prefix.slice(0, i).join(':')}$${matcherName}`;
if (Reflect.has(target, qualifiedName)) {
matcher = Reflect.get(target, qualifiedName, receiver);
break;
}
}
}
if (typeof matcherName !== 'string')
return matcher;
if (matcher === undefined)

View file

@ -1063,3 +1063,44 @@ test('should throw error when using .equals()', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('expect.extendImmutable should work', async ({ runInlineTest }) => {
const result = await runInlineTest({
'expect-test.spec.ts': `
import { test, expect } from '@playwright/test';
const expectFoo = expect.extendImmutable({
toFoo() {
console.log('%%foo');
return { pass: true };
}
});
const expectFoo2 = expect.extendImmutable({
toFoo() {
console.log('%%foo2');
return { pass: true };
}
});
const expectBar = expectFoo.extendImmutable({
toBar() {
console.log('%%bar');
return { pass: true };
}
});
test('logs', () => {
expect(expectFoo).not.toBe(expectFoo2);
expect(expectFoo).not.toBe(expectBar);
expectFoo().toFoo();
expectFoo2().toFoo();
expectBar().toFoo();
expectBar().toBar();
});
`
});
expect(result.outputLines).toEqual([
'foo',
'foo2',
'foo',
'bar',
]);
});