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 { zones } from 'playwright-core/lib/utils';
import { TestInfoImpl } from '../worker/testInfo'; import { TestInfoImpl } from '../worker/testInfo';
import { ExpectError, isExpectError } from './matcherHint'; import { ExpectError, isExpectError } from './matcherHint';
import { randomUUID } from 'node:crypto';
// #region // #region
// Mirrored from https://github.com/facebook/jest/blob/f13abff8df9a0e1148baf3584bcde6d1b479edc7/packages/expect/src/print.ts // Mirrored from https://github.com/facebook/jest/blob/f13abff8df9a0e1148baf3584bcde6d1b479edc7/packages/expect/src/print.ts
@ -104,33 +105,16 @@ export const printReceivedStringContainExpectedResult = (
type ExpectMessage = string | { message?: string }; type ExpectMessage = string | { message?: string };
function createMatchers(actual: unknown, info: ExpectMetaInfo): any { function createMatchers(actual: unknown, info: ExpectMetaInfo, prefix: string[]): any {
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info)); return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info, prefix));
} }
function createExpect(info: ExpectMetaInfo) { function createExpect(info: ExpectMetaInfo, prefix: string[] = []) {
const expectInstance: Expect<{}> = new Proxy(expectLibrary, {
apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) {
const [actual, messageOrOptions] = argumentsList;
const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message;
const newInfo = { ...info, message };
if (newInfo.isPoll) {
if (typeof actual !== 'function')
throw new Error('`expect.poll()` accepts only function as a first argument');
newInfo.generator = actual as any;
}
return createMatchers(actual, newInfo);
},
get: function(target: any, property: string) { function extend(matchers: any, qualifier: string) {
if (property === 'configure')
return configure;
if (property === 'extend') {
return (matchers: any) => {
const wrappedMatchers: any = {}; const wrappedMatchers: any = {};
for (const [name, matcher] of Object.entries(matchers)) { for (const [name, matcher] of Object.entries(matchers)) {
wrappedMatchers[name] = function(...args: any[]) { wrappedMatchers[qualifier + name] = function(...args: any[]) {
const { isNot, promise, utils } = this; const { isNot, promise, utils } = this;
const newThis: ExpectMatcherState = { const newThis: ExpectMatcherState = {
isNot, isNot,
@ -143,10 +127,41 @@ function createExpect(info: ExpectMetaInfo) {
}; };
} }
expectLibrary.extend(wrappedMatchers); expectLibrary.extend(wrappedMatchers);
}
const expectInstance: Expect<{}> = new Proxy(expectLibrary, {
apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) {
const [actual, messageOrOptions] = argumentsList;
const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message;
const newInfo = { ...info, message };
if (newInfo.isPoll) {
if (typeof actual !== 'function')
throw new Error('`expect.poll()` accepts only function as a first argument');
newInfo.generator = actual as any;
}
return createMatchers(actual, newInfo, prefix);
},
get: function(target: any, property: string) {
if (property === 'configure')
return configure;
if (property === 'extend') {
return (matchers: any) => {
extend(matchers, '');
return expectInstance; 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') { if (property === 'soft') {
return (actual: unknown, messageOrOptions?: ExpectMessage) => { return (actual: unknown, messageOrOptions?: ExpectMessage) => {
return configure({ soft: true })(actual, messageOrOptions) as any; return configure({ soft: true })(actual, messageOrOptions) as any;
@ -241,13 +256,26 @@ type ExpectMetaInfo = {
class ExpectMetaInfoProxyHandler implements ProxyHandler<any> { class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
private _info: ExpectMetaInfo; private _info: ExpectMetaInfo;
private _prefix: string[];
constructor(info: ExpectMetaInfo) { constructor(info: ExpectMetaInfo, prefix: string[]) {
this._info = { ...info }; this._info = { ...info };
this._prefix = prefix;
} }
get(target: Object, matcherName: string | symbol, receiver: any): any { get(target: Object, matcherName: string | symbol, receiver: any): any {
let matcher = Reflect.get(target, matcherName, receiver); 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') if (typeof matcherName !== 'string')
return matcher; return matcher;
if (matcher === undefined) 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.exitCode).toBe(0);
expect(result.passed).toBe(1); 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',
]);
});