Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 51 additions & 25 deletions packages/expect/src/__tests__/asymmetricMatchers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ test('Any.toAsymmetricMatcher() with function name', () => {
});

test('Any throws when called with empty constructor', () => {
jestExpect(() => any()).toThrow();
// @ts-expect-error: Testing runtime error
jestExpect(() => any()).toThrow(
'any() expects to be passed a constructor function. Please pass one or use anything() to match any object.',
);
});

test('Anything matches any type', () => {
Expand Down Expand Up @@ -150,8 +153,9 @@ test('ArrayContaining does not match', () => {

test('ArrayContaining throws for non-arrays', () => {
jestExpect(() => {
// @ts-expect-error: Testing runtime error
arrayContaining('foo').asymmetricMatch([]);
}).toThrow();
}).toThrow("You must provide an array to ArrayContaining, not 'string'.");
});

test('ArrayNotContaining matches', () => {
Expand All @@ -171,8 +175,9 @@ test('ArrayNotContaining does not match', () => {

test('ArrayNotContaining throws for non-arrays', () => {
jestExpect(() => {
// @ts-expect-error: Testing runtime error
arrayNotContaining('foo').asymmetricMatch([]);
}).toThrow();
}).toThrow("You must provide an array to ArrayNotContaining, not 'string'.");
});

test('ObjectContaining matches', () => {
Expand Down Expand Up @@ -224,13 +229,16 @@ test('ObjectContaining matches prototype properties', () => {
function Foo() {}
Foo.prototype = prototypeObject;
Foo.prototype.constructor = Foo;
obj = new Foo();
obj = new (Foo as any)();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here TS complains about implicit any.

}
jestExpect(objectContaining({foo: 'bar'}).asymmetricMatch(obj)).toBe(true);
});

test('ObjectContaining throws for non-objects', () => {
jestExpect(() => objectContaining(1337).asymmetricMatch()).toThrow();
// @ts-expect-error: Testing runtime error
jestExpect(() => objectContaining(1337).asymmetricMatch()).toThrow(
"You must provide an object to ObjectContaining, not 'number'.",
);
});

test('ObjectContaining does not mutate the sample', () => {
Expand All @@ -243,6 +251,8 @@ test('ObjectContaining does not mutate the sample', () => {

test('ObjectNotContaining matches', () => {
[
objectContaining({}).asymmetricMatch(null),
objectContaining({}).asymmetricMatch(undefined),
Comment on lines +254 to +255
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added those simply for completeness.

objectNotContaining({foo: 'foo'}).asymmetricMatch({bar: 'bar'}),
objectNotContaining({foo: 'foo'}).asymmetricMatch({foo: 'foox'}),
objectNotContaining({foo: undefined}).asymmetricMatch({}),
Expand Down Expand Up @@ -278,32 +288,40 @@ test('ObjectNotContaining does not match', () => {
first: objectContaining({second: {}}),
}).asymmetricMatch({first: {second: {}}}),
objectNotContaining({}).asymmetricMatch(null),
objectNotContaining({}).asymmetricMatch(undefined),
objectNotContaining({}).asymmetricMatch({}),
].forEach(test => {
jestExpect(test).toEqual(false);
});
});

test('ObjectNotContaining inverts ObjectContaining', () => {
[
[{}, null],
[{foo: 'foo'}, {foo: 'foo', jest: 'jest'}],
[{foo: 'foo', jest: 'jest'}, {foo: 'foo'}],
[{foo: undefined}, {foo: undefined}],
[{foo: undefined}, {}],
[{first: {second: {}}}, {first: {second: {}}}],
[{first: objectContaining({second: {}})}, {first: {second: {}}}],
[{first: objectNotContaining({second: {}})}, {first: {second: {}}}],
[{}, {foo: undefined}],
].forEach(([sample, received]) => {
(
[
[{}, null],
[{foo: 'foo'}, {foo: 'foo', jest: 'jest'}],
[{foo: 'foo', jest: 'jest'}, {foo: 'foo'}],
[{foo: undefined}, {foo: undefined}],
[{foo: undefined}, {}],
[{first: {second: {}}}, {first: {second: {}}}],
[{first: objectContaining({second: {}})}, {first: {second: {}}}],
[{first: objectNotContaining({second: {}})}, {first: {second: {}}}],
[{}, {foo: undefined}],
] as const
).forEach(([sample, received]) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was considering to use test.each, but it was hard to invent a title for each case. Without unique titles it did not look good. So perhaps forEach is fine in this file?

jestExpect(objectNotContaining(sample).asymmetricMatch(received)).toEqual(
!objectContaining(sample).asymmetricMatch(received),
);
});
});

test('ObjectNotContaining throws for non-objects', () => {
jestExpect(() => objectNotContaining(1337).asymmetricMatch()).toThrow();
jestExpect(() => {
// @ts-expect-error: Testing runtime error
objectNotContaining(1337).asymmetricMatch();
}).toThrow(
"You must provide an object to ObjectNotContaining, not 'number'.",
);
});

test('StringContaining matches string against string', () => {
Expand All @@ -313,8 +331,9 @@ test('StringContaining matches string against string', () => {

test('StringContaining throws if expected value is not string', () => {
jestExpect(() => {
// @ts-expect-error: Testing runtime error
stringContaining([1]).asymmetricMatch('queen');
}).toThrow();
}).toThrow('Expected is not a string');
});

test('StringContaining returns false if received value is not string', () => {
Expand All @@ -328,8 +347,9 @@ test('StringNotContaining matches string against string', () => {

test('StringNotContaining throws if expected value is not string', () => {
jestExpect(() => {
// @ts-expect-error: Testing runtime error
stringNotContaining([1]).asymmetricMatch('queen');
}).toThrow();
}).toThrow('Expected is not a string');
});

test('StringNotContaining returns true if received value is not string', () => {
Expand All @@ -348,8 +368,9 @@ test('StringMatching matches string against string', () => {

test('StringMatching throws if expected value is neither string nor regexp', () => {
jestExpect(() => {
// @ts-expect-error: Testing runtime error
stringMatching([1]).asymmetricMatch('queen');
}).toThrow();
}).toThrow('Expected is not a String or a RegExp');
});

test('StringMatching returns false if received value is not string', () => {
Expand All @@ -372,8 +393,9 @@ test('StringNotMatching matches string against string', () => {

test('StringNotMatching throws if expected value is neither string nor regexp', () => {
jestExpect(() => {
// @ts-expect-error: Testing runtime error
stringNotMatching([1]).asymmetricMatch('queen');
}).toThrow();
}).toThrow('Expected is not a String or a RegExp');
});

test('StringNotMatching returns true if received value is not string', () => {
Expand Down Expand Up @@ -451,26 +473,30 @@ describe('closeTo', () => {

test('closeTo throw if expected is not number', () => {
jestExpect(() => {
// @ts-expect-error: Testing runtime error
closeTo('a');
}).toThrow();
}).toThrow('Expected is not a Number');
});

test('notCloseTo throw if expected is not number', () => {
jestExpect(() => {
// @ts-expect-error: Testing runtime error
notCloseTo('a');
}).toThrow();
}).toThrow('Expected is not a Number');
});

test('closeTo throw if precision is not number', () => {
jestExpect(() => {
// @ts-expect-error: Testing runtime error
closeTo(1, 'a');
}).toThrow();
}).toThrow('Precision is not a Number');
});

test('notCloseTo throw if precision is not number', () => {
jestExpect(() => {
// @ts-expect-error: Testing runtime error
notCloseTo(1, 'a');
}).toThrow();
}).toThrow('Precision is not a Number');
});

test('closeTo return false if received is not number', () => {
Expand Down
47 changes: 34 additions & 13 deletions packages/expect/src/__tests__/extend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ expect.addSnapshotSerializer(alignedAnsiStyleSerializer);
jestExpect.extend({
toBeDivisibleBy(actual: number, expected: number) {
const pass = actual % expected === 0;
const message = pass
const message: () => string = pass
? () =>
`expected ${this.utils.printReceived(
actual,
Expand Down Expand Up @@ -51,29 +51,47 @@ jestExpect.extend({
},
});

declare module '../types' {
interface AsymmetricMatchers {
toBeDivisibleBy(expected: number): void;
toBeSymbol(expected: symbol): void;
toBeWithinRange(floor: number, ceiling: number): void;
}
interface Matchers<R> {
toBeDivisibleBy(expected: number): R;
toBeSymbol(expected: symbol): R;
toBeWithinRange(floor: number, ceiling: number): R;

shouldNotError(): R;
toFailWithoutMessage(): R;
toBeOne(): R;
toAllowOverridingExistingMatcher(): R;
}
}

it('is available globally when matcher is unary', () => {
jestExpect(15).toBeDivisibleBy(5);
jestExpect(15).toBeDivisibleBy(3);
jestExpect(15).not.toBeDivisibleBy(6);

jestExpect(() =>
expect(() =>
jestExpect(15).toBeDivisibleBy(2),
).toThrowErrorMatchingSnapshot();
});

it('is available globally when matcher is variadic', () => {
jestExpect(15).toBeWithinRange(10, 20);
jestExpect(15).not.toBeWithinRange(6);
jestExpect(15).not.toBeWithinRange(6, 10);

jestExpect(() =>
expect(() =>
jestExpect(15).toBeWithinRange(1, 3),
).toThrowErrorMatchingSnapshot();
});

it('exposes matcherUtils in context', () => {
jestExpect.extend({
_shouldNotError(_actual: unknown, _expected: unknown) {
const pass = this.equals(
shouldNotError(_actual: unknown) {
const pass: boolean = this.equals(
this.utils,
Object.assign(matcherUtils, {
iterableEquality,
Expand All @@ -88,13 +106,13 @@ it('exposes matcherUtils in context', () => {
},
});

jestExpect()._shouldNotError();
jestExpect('test').shouldNotError();
});

it('is ok if there is no message specified', () => {
jestExpect.extend({
toFailWithoutMessage(_expected: unknown) {
return {pass: false};
return {message: () => '', pass: false};
},
});

Expand All @@ -107,13 +125,13 @@ it('exposes an equality function to custom matchers', () => {
// jestExpect and expect share the same global state
expect.assertions(3);
jestExpect.extend({
toBeOne() {
toBeOne(_expected: unknown) {
expect(this.equals).toBe(equals);
return {pass: !!this.equals(1, 1)};
return {message: () => '', pass: !!this.equals(1, 1)};
},
});

expect(() => jestExpect().toBeOne()).not.toThrow();
expect(() => jestExpect('test').toBeOne()).not.toThrow();
});

it('defines asymmetric unary matchers', () => {
Expand Down Expand Up @@ -170,15 +188,15 @@ it('prints the Symbol into the error message', () => {
it('allows overriding existing extension', () => {
jestExpect.extend({
toAllowOverridingExistingMatcher(_expected: unknown) {
return {pass: _expected === 'bar'};
return {message: () => '', pass: _expected === 'bar'};
},
});

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

jestExpect.extend({
toAllowOverridingExistingMatcher(_expected: unknown) {
return {pass: _expected === 'foo'};
return {message: () => '', pass: _expected === 'foo'};
},
});

Expand All @@ -188,20 +206,23 @@ it('allows overriding existing extension', () => {
it('throws descriptive errors for invalid matchers', () => {
expect(() =>
jestExpect.extend({
// @ts-expect-error: Testing runtime error
default: undefined,
}),
).toThrow(
'expect.extend: `default` is not a valid matcher. Must be a function, is "undefined"',
);
expect(() =>
jestExpect.extend({
// @ts-expect-error: Testing runtime error
default: 42,
}),
).toThrow(
'expect.extend: `default` is not a valid matcher. Must be a function, is "number"',
);
expect(() =>
jestExpect.extend({
// @ts-expect-error: Testing runtime error
default: 'foobar',
}),
).toThrow(
Expand Down
28 changes: 19 additions & 9 deletions packages/expect/src/__tests__/stacktrace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,42 @@
import jestExpect from '../';

jestExpect.extend({
toCustomMatch(callback: () => unknown, expectation: unknown) {
toCustomMatch(callback: () => unknown, expected: unknown) {
const actual = callback();

if (actual !== expectation) {
if (actual !== expected) {
return {
message: () => `Expected "${expectation}" but got "${actual}"`,
message: () => `Expected "${expected}" but got "${actual}"`,
pass: false,
};
}

return {pass: true};
return {
message: () => '',
pass: true,
};
},
toMatchPredicate(received: unknown, argument: (a: unknown) => void) {
argument(received);
toMatchPredicate(received: unknown, expected: (a: unknown) => void) {
expected(received);
return {
message: () => '',
pass: true,
};
},
});

declare module '../types' {
interface Matchers<R> {
toCustomMatch(expected: unknown): R;
toMatchPredicate(expected: (a: unknown) => void): R;
}
}

it('stack trace points to correct location when using matchers', () => {
try {
jestExpect(true).toBe(false);
} catch (error: any) {
expect(error.stack).toContain('stacktrace.test.ts:35');
expect(error.stack).toContain('stacktrace.test.ts:45:22');
}
});

Expand All @@ -44,7 +54,7 @@ it('stack trace points to correct location when using nested matchers', () => {
jestExpect(value).toBe(false);
});
} catch (error: any) {
expect(error.stack).toContain('stacktrace.test.ts:44');
expect(error.stack).toContain('stacktrace.test.ts:54:25');
}
});

Expand All @@ -60,6 +70,6 @@ it('stack trace points to correct location when throwing from a custom matcher',
foo();
}).toCustomMatch('bar');
} catch (error: any) {
expect(error.stack).toContain('stacktrace.test.ts:57');
expect(error.stack).toContain('stacktrace.test.ts:67:15');
}
});