Skip to content

Commit

Permalink
Added function name to formatting of expect.satisfies
Browse files Browse the repository at this point in the history
  • Loading branch information
johnw42 committed May 16, 2023
1 parent 19f528d commit e194155
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 23 deletions.
23 changes: 14 additions & 9 deletions docs/ExpectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -1037,32 +1037,37 @@ describe('not.objectContaining', () => {
});
```

### `expect.satisfies(predicate)`
### `expect.satisfies([description,] predicate)`

`expect.satisfies(predicate)` matches the received value if an abitrary predicate returns a truthy value when passed the received value as an argument.

```js
describe('satisfies', () => {
it('matches if the received value satisfies a predicate', () => {
it('matches if the received value is even', () => {
const isEven = (n) => n % 2 === 0;
// Expected value is printed as 'Satisfies isEven'
expect(42).toEqual(expect.satisfies(isEven));
})
})
```

### `expect.not.satisfies(predicate)`

`expect.not.satisfies(predicate)` matches the received value if an abitrary predicate returns a falsy value when passed the received value as an argument.
If a description is given, it is used to format test failure messages (otherwise Jest attempts to find a name for the predicate).

```js
describe('not.satisfies', () => {
it('matches if the received value does not satisfy a predicate', () => {
const isEven = n => n % 2 === 0;
expect(41).toEqual(expect.not.satisfies(isEven));
describe('satisfies', () => {
it('matches if the received value is even', () => {
// Expected value is printed as 'Satisfies "number is even"'
expect(42).toEqual(expect.satisfies('number is even', n => n % 2 === 0));
})
})
```

### `expect.not.satisfies([description,] predicate)`

`expect.not.satisfies(predicate)` matches the received value if an abitrary predicate returns a falsy value when passed the received value as an argument.

It is the inverse of `expect.satisfies`.

### `expect.stringContaining(string)`

`expect.stringContaining(string)` matches the received value if it is a string that contains the exact expected string.
Expand Down
23 changes: 23 additions & 0 deletions packages/expect/src/__tests__/asymmetricMatchers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,12 +332,18 @@ test('Satisfies matches when predicate returns a truthy value', () => {
jestExpect(expectSatisfies(() => value).asymmetricMatch(null)).toBe(
Boolean(value),
);
jestExpect(
expectSatisfies('description', () => value).asymmetricMatch(null),
).toBe(Boolean(value));
});
});

test('NotSatisfies matches when predicate returns a falsy value', () => {
[true, 'a', 1, false, null, undefined, '', 0].forEach(value => {
jestExpect(notSatisfies(() => value).asymmetricMatch(null)).toBe(!value);
jestExpect(
notSatisfies('description', () => value).asymmetricMatch(null),
).toBe(!value);
});
});

Expand All @@ -352,7 +358,13 @@ test('Satisfies and NotSatisfies pass the received value to the predicate', () =
jestExpect(() =>
matcher(assertIsSentinel).asymmetricMatch(sentinel),
).not.toThrow();
jestExpect(() =>
matcher('description', assertIsSentinel).asymmetricMatch(sentinel),
).not.toThrow();
jestExpect(() => matcher(assertIsSentinel).asymmetricMatch(null)).toThrow();
jestExpect(() =>
matcher('description', assertIsSentinel).asymmetricMatch(null),
).toThrow();
});
});

Expand All @@ -361,6 +373,17 @@ test('Satisfies throws if the predicate is not a function', () => {
// @ts-expect-error: Testing runtime error
expectSatisfies(42);
}).toThrow('Predicate is not a function');
jestExpect(() => {
// @ts-expect-error: Testing runtime error
expectSatisfies('description', 42);
}).toThrow('Predicate is not a function');
});

test('Satisfies throws if the description is not a string', () => {
jestExpect(() => {
// @ts-expect-error: Testing runtime error
expectSatisfies(42, () => 1);
}).toThrow('Description is not a string');
});

test('StringContaining matches string against string', () => {
Expand Down
67 changes: 58 additions & 9 deletions packages/expect/src/asymmetricMatchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,23 @@ import type {

const functionToString = Function.prototype.toString;

function fnNameFor(func: () => unknown) {
function maybeFnNameFor<A extends Array<unknown>>(
func: (...args: A) => unknown,
): string | undefined {
if (func.name) {
return func.name;
}

const matches = functionToString
.call(func)
.match(/^(?:async)?\s*function\s*\*?\s*([\w$]+)\s*\(/);
return matches ? matches[1] : '<anonymous>';
return matches?.[1];
}

function fnNameFor<A extends Array<unknown>>(
func: (...args: A) => unknown,
): string {
return maybeFnNameFor(func) ?? '<anonymous>';
}

const utils = Object.freeze({
Expand Down Expand Up @@ -259,9 +267,28 @@ class ObjectContaining extends AsymmetricMatcher<Record<string, unknown>> {
}

class Satisfies<T> extends AsymmetricMatcher<void> {
constructor(private predicate: (sample: T) => unknown, inverse = false) {
private predicate: (sample: T) => unknown;
private description: string | undefined;

constructor(
predicateOrDescription: string | ((sample: T) => unknown),
maybePredicate: undefined | ((sample: T) => unknown),
inverse = false,
) {
super(void 0, inverse);
if (typeof predicate !== 'function') {
if (typeof maybePredicate === 'function') {
if (typeof predicateOrDescription !== 'string') {
throw new Error('Description is not a string');
}
this.predicate = maybePredicate;
this.description = predicateOrDescription;
} else if (
typeof predicateOrDescription === 'function' &&
maybePredicate === undefined
) {
this.predicate = predicateOrDescription;
this.description = undefined;
} else {
throw new Error('Predicate is not a function');
}
}
Expand All @@ -275,7 +302,10 @@ class Satisfies<T> extends AsymmetricMatcher<void> {
}

override toAsymmetricMatcher(): string {
return this.toString();
const fnName = this.description
? `"${this.description}"`
: maybeFnNameFor(this.predicate);
return fnName ? `${this.toString()} ${fnName}` : this.toString();
}
}

Expand Down Expand Up @@ -381,17 +411,36 @@ export const arrayContaining = (sample: Array<unknown>): ArrayContaining =>
new ArrayContaining(sample);
export const arrayNotContaining = (sample: Array<unknown>): ArrayContaining =>
new ArrayContaining(sample, true);
export const notSatisfies = <T>(
export function notSatisfies<T>(
predicate: (sample: T) => unknown,
): Satisfies<T> => new Satisfies<T>(predicate, true);
): Satisfies<T>;
export function notSatisfies<T>(
description: string,
predicate: (sample: T) => unknown,
): Satisfies<T>;
export function notSatisfies<T>(
predicateOrDescription: string | ((sample: T) => unknown),
maybePredicate?: (sample: T) => unknown,
): Satisfies<T> {
return new Satisfies<T>(predicateOrDescription, maybePredicate, true);
}
export const objectContaining = (
sample: Record<string, unknown>,
): ObjectContaining => new ObjectContaining(sample);
export const objectNotContaining = (
sample: Record<string, unknown>,
): ObjectContaining => new ObjectContaining(sample, true);
export const satisfies = <T>(predicate: (sample: T) => unknown): Satisfies<T> =>
new Satisfies<T>(predicate);
export function satisfies<T>(predicate: (sample: T) => unknown): Satisfies<T>;
export function satisfies<T>(
description: string,
predicate: (sample: T) => unknown,
): Satisfies<T>;
export function satisfies<T>(
predicateOrDescription: string | ((sample: T) => unknown),
maybePredicate?: (sample: T) => unknown,
): Satisfies<T> {
return new Satisfies<T>(predicateOrDescription, maybePredicate);
}
export const stringContaining = (expected: string): StringContaining =>
new StringContaining(expected);
export const stringNotContaining = (expected: string): StringContaining =>
Expand Down
4 changes: 4 additions & 0 deletions packages/expect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export interface AsymmetricMatchers {
closeTo(sample: number, precision?: number): AsymmetricMatcher;
objectContaining(sample: Record<string, unknown>): AsymmetricMatcher;
satisfies<T>(predicate: (sample: T) => unknown): AsymmetricMatcher;
satisfies<T>(
description: string,
predicate: (sample: T) => unknown,
): AsymmetricMatcher;
stringContaining(sample: string): AsymmetricMatcher;
stringMatching(sample: string | RegExp): AsymmetricMatcher;
}
Expand Down
32 changes: 32 additions & 0 deletions packages/jest-types/__typetests__/expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,44 @@ expectError(expect({a: 1}).toEqual(expect.not.objectContaining()));
expectType<void>(
expect('x').toEqual(expect.satisfies((_sample: string): unknown => 1)),
);
expectType<void>(
expect('x').toEqual(
expect.satisfies('description', (_sample: string): unknown => 1),
),
);
expectError(expect('x').toEqual(expect.satisfies('not a function')));
expectError(
expect('x').toEqual(expect.satisfies('description', 'not a function')),
);
expectError(
expect('x').toEqual(
expect.satisfies(
() => 42,
() => 1,
),
),
);
expectError(expect('x').toEqual(expect.satisfies()));
expectType<void>(
expect('x').toEqual(expect.not.satisfies((_sample: string): unknown => 1)),
);
expectType<void>(
expect('x').toEqual(
expect.not.satisfies('description', (_sample: string): unknown => 1),
),
);
expectError(expect('x').toEqual(expect.not.satisfies('not a function')));
expectError(
expect('x').toEqual(expect.not.satisfies('description', 'not a function')),
);
expectError(
expect('x').toEqual(
expect.not.satisfies(
() => 42,
() => 1,
),
),
);
expectError(expect('x').toEqual(expect.not.satisfies()));

expectType<void>(expect('one').toEqual(expect.stringContaining('n')));
Expand Down
50 changes: 45 additions & 5 deletions packages/pretty-format/src/__tests__/AsymmetricMatcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,22 +95,62 @@ test('objectNotContaining()', () => {
}`);
});

test('satisfies(predicate)', () => {
test('satisfies(description, predicate)', () => {
const result = prettyFormat(
expect.satisfies(_sample => true),
expect.satisfies('xyzzy', () => true),
options,
);
expect(result).toBe('Satisfies "xyzzy"');
});

test('satisfies(anonymous predicate)', () => {
const result = prettyFormat(
expect.satisfies(() => true),
options,
);
expect(result).toBe('Satisfies');
});

test('not.satisfies(predicate)', () => {
test('satisfies(named predicate)', () => {
function myPredicate() {}
const result = prettyFormat(expect.satisfies(myPredicate), options);
expect(result).toBe('Satisfies myPredicate');
});

test('satisfies(named const predicate)', () => {
const myPredicate = () => true;
const result = prettyFormat(expect.satisfies(myPredicate), options);
expect(result).toBe('Satisfies myPredicate');
});

test('not.satisfies(description, predicate)', () => {
const result = prettyFormat(
expect.not.satisfies(_sample => true),
expect.not.satisfies('xyzzy', () => true),
options,
);
expect(result).toBe('NotSatisfies "xyzzy"');
});

test('not.satisfies(anonymous predicate)', () => {
const result = prettyFormat(
expect.not.satisfies(() => true),
options,
);
expect(result).toBe('NotSatisfies');
});

test('not.satisfies(named predicate)', () => {
function myPredicate() {}
const result = prettyFormat(expect.not.satisfies(myPredicate), options);
expect(result).toBe('NotSatisfies myPredicate');
});

test('not.satisfies(named const predicate)', () => {
const myPredicate = () => true;
const result = prettyFormat(expect.not.satisfies(myPredicate), options);
expect(result).toBe('NotSatisfies myPredicate');
});

test('stringContaining(string)', () => {
const result = prettyFormat(expect.stringContaining('jest'), options);
expect(result).toBe('StringContaining "jest"');
Expand Down Expand Up @@ -215,7 +255,7 @@ test('supports multiple nested asymmetric matchers', () => {
"f": ObjectContaining {
"test": "case",
},
"g": Satisfies
"g": Satisfies,
},
},
}`);
Expand Down

0 comments on commit e194155

Please sign in to comment.