Skip to content

Commit

Permalink
[expect] Protect against undefined matcher when extending
Browse files Browse the repository at this point in the history
  • Loading branch information
IanVS committed Feb 23, 2022
1 parent d472868 commit ec0cb2e
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 46 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
- `[jest-jasmine2, jest-types]` [**BREAKING**] Move all `jasmine` specific types from `@jest/types` to its own package ([#12125](https://github.com/facebook/jest/pull/12125))
- `[jest-matcher-utils]` Pass maxWidth to `pretty-format` to avoid printing every element in arrays by default ([#12402](https://github.com/facebook/jest/pull/12402))
- `[jest-mock]` Fix function overloads for `spyOn` to allow more correct type inference in complex object ([#12442](https://github.com/facebook/jest/pull/12442))
- `[expect]` Protect against undefined `matcher` when extending ([#12481](https://github.com/facebook/jest/pull/12481))

### Chore & Maintenance

Expand Down
8 changes: 8 additions & 0 deletions packages/expect/src/__tests__/extend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,11 @@ it('allows overriding existing extension', () => {

jestExpect('foo').toAllowOverridingExistingMatcher();
});

it('does not explode if matcher is undefined', () => {
expect(() =>
jestExpect.extend({
default: undefined,
}),
).not.toThrow();
});
98 changes: 52 additions & 46 deletions packages/expect/src/jestMatchersObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,57 +56,63 @@ export const setMatchers = (
): void => {
Object.keys(matchers).forEach(key => {
const matcher = matchers[key];
Object.defineProperty(matcher, INTERNAL_MATCHER_FLAG, {
value: isInternal,
});

if (!isInternal) {
// expect is defined

class CustomMatcher extends AsymmetricMatcher<
[unknown, ...Array<unknown>]
> {
constructor(inverse = false, ...sample: [unknown, ...Array<unknown>]) {
super(sample, inverse);
}

asymmetricMatch(other: unknown) {
const {pass} = matcher.call(
this.getMatcherContext(),
other,
...this.sample,
) as SyncExpectationResult;

return this.inverse ? !pass : pass;
}

toString() {
return `${this.inverse ? 'not.' : ''}${key}`;
}
if (matcher) {
Object.defineProperty(matcher, INTERNAL_MATCHER_FLAG, {
value: isInternal,
});

getExpectedType() {
return 'any';
if (!isInternal) {
// expect is defined

class CustomMatcher extends AsymmetricMatcher<
[unknown, ...Array<unknown>]
> {
constructor(
inverse = false,
...sample: [unknown, ...Array<unknown>]
) {
super(sample, inverse);
}

asymmetricMatch(other: unknown) {
const {pass} = matcher.call(
this.getMatcherContext(),
other,
...this.sample,
) as SyncExpectationResult;

return this.inverse ? !pass : pass;
}

toString() {
return `${this.inverse ? 'not.' : ''}${key}`;
}

getExpectedType() {
return 'any';
}

toAsymmetricMatcher() {
return `${this.toString()}<${this.sample.map(String).join(', ')}>`;
}
}

toAsymmetricMatcher() {
return `${this.toString()}<${this.sample.map(String).join(', ')}>`;
}
Object.defineProperty(expect, key, {
configurable: true,
enumerable: true,
value: (...sample: [unknown, ...Array<unknown>]) =>
new CustomMatcher(false, ...sample),
writable: true,
});
Object.defineProperty(expect.not, key, {
configurable: true,
enumerable: true,
value: (...sample: [unknown, ...Array<unknown>]) =>
new CustomMatcher(true, ...sample),
writable: true,
});
}

Object.defineProperty(expect, key, {
configurable: true,
enumerable: true,
value: (...sample: [unknown, ...Array<unknown>]) =>
new CustomMatcher(false, ...sample),
writable: true,
});
Object.defineProperty(expect.not, key, {
configurable: true,
enumerable: true,
value: (...sample: [unknown, ...Array<unknown>]) =>
new CustomMatcher(true, ...sample),
writable: true,
});
}
});

Expand Down

0 comments on commit ec0cb2e

Please sign in to comment.