Skip to content

Commit 1a7df6b

Browse files
committed
feat(expect): add ArrayOf asymmetric matcher
1 parent 511ea93 commit 1a7df6b

File tree

8 files changed

+160
-0
lines changed

8 files changed

+160
-0
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### Features
44

5+
- `[expect]` Add `ArrayOf` asymmetric matcher for validating array elements. **TODO: ADD LINK!**
56
- `[babel-jest]` Add option `excludeJestPreset` to allow opting out of `babel-preset-jest` ([#15164](https://github.com/jestjs/jest/pull/15164))
67
- `[jest-circus, jest-cli, jest-config]` Add `waitNextEventLoopTurnForUnhandledRejectionEvents` flag to minimise performance impact of correct detection of unhandled promise rejections introduced in [#14315](https://github.com/jestjs/jest/pull/14315) ([#14681](https://github.com/jestjs/jest/pull/14681))
78
- `[jest-circus]` Add a `waitBeforeRetry` option to `jest.retryTimes` ([#14738](https://github.com/jestjs/jest/pull/14738))

Diff for: docs/ExpectAPI.md

+38
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,44 @@ describe('not.arrayContaining', () => {
958958
});
959959
```
960960

961+
### `expect.arrayOf(value)`
962+
963+
`expect.arrayOf(value)` matches a received array whose elements match the provided value. This is useful for asserting that every item in an array satisfies a particular condition or type.
964+
965+
**Example:**
966+
967+
```js
968+
test('all elements in array are strings', () => {
969+
expect(['apple', 'banana', 'cherry']).toEqual(expect.arrayOf(expect.any(String)));
970+
});
971+
```
972+
973+
This matcher is particularly useful for validating arrays containing complex structures:
974+
975+
```js
976+
test('array of objects with specific properties', () => {
977+
expect([
978+
{ id: 1, name: 'Alice' },
979+
{ id: 2, name: 'Bob' }
980+
]).toEqual(expect.arrayOf(expect.objectContaining({
981+
id: expect.any(Number),
982+
name: expect.any(String)
983+
})));
984+
});
985+
```
986+
987+
### `expect.not.arrayOf(matcher)`
988+
989+
`expect.not.arrayOf(matcher)` matches a received array where not all elements match the provided matcher.
990+
991+
**Example:**
992+
993+
```js
994+
test('not all elements in array are strings', () => {
995+
expect(['apple', 123, 'cherry']).toEqual(expect.not.arrayOf(expect.any(String)));
996+
});
997+
```
998+
961999
### `expect.closeTo(number, numDigits?)`
9621000

9631001
`expect.closeTo(number, numDigits?)` is useful when comparing floating point numbers in object properties or array item. If you need to compare a number, please use `.toBeCloseTo` instead.

Diff for: packages/expect/src/__tests__/asymmetricMatchers.test.ts

+61
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import {
1313
anything,
1414
arrayContaining,
1515
arrayNotContaining,
16+
arrayOf,
1617
closeTo,
18+
notArrayOf,
1719
notCloseTo,
1820
objectContaining,
1921
objectNotContaining,
@@ -514,3 +516,62 @@ describe('closeTo', () => {
514516
jestExpect(notCloseTo(1).asymmetricMatch('a')).toBe(false);
515517
});
516518
});
519+
520+
test('ArrayOf matches', () => {
521+
for (const test of [
522+
arrayOf(1).asymmetricMatch([1]),
523+
arrayOf(1).asymmetricMatch([1, 1, 1]),
524+
arrayOf({a: 1}).asymmetricMatch([{a: 1}, {a: 1}]),
525+
arrayOf(undefined).asymmetricMatch([undefined]),
526+
arrayOf(null).asymmetricMatch([null]),
527+
arrayOf([]).asymmetricMatch([[], []]),
528+
arrayOf(any(String)).asymmetricMatch(['a', 'b', 'c']),
529+
]) {
530+
jestExpect(test).toEqual(true);
531+
}
532+
});
533+
534+
test('ArrayOf does not match', () => {
535+
for (const test of [
536+
arrayOf(1).asymmetricMatch([2]),
537+
arrayOf(1).asymmetricMatch([1, 2]),
538+
arrayOf({a: 1}).asymmetricMatch([{a: 2}]),
539+
arrayOf(undefined).asymmetricMatch([null]),
540+
arrayOf(null).asymmetricMatch([undefined]),
541+
arrayOf([]).asymmetricMatch([{}]),
542+
arrayOf(1).asymmetricMatch(1),
543+
arrayOf(1).asymmetricMatch('not an array'),
544+
arrayOf(1).asymmetricMatch({}),
545+
arrayOf(any(String)).asymmetricMatch([1, 2]),
546+
]) {
547+
jestExpect(test).toEqual(false);
548+
}
549+
});
550+
551+
test('NotArrayOf matches', () => {
552+
for (const test of [
553+
notArrayOf(1).asymmetricMatch([2]),
554+
notArrayOf(1).asymmetricMatch([1, 2]),
555+
notArrayOf({a: 1}).asymmetricMatch([{a: 2}]),
556+
notArrayOf(1).asymmetricMatch(1),
557+
notArrayOf(1).asymmetricMatch('not an array'),
558+
notArrayOf(1).asymmetricMatch({}),
559+
notArrayOf(any(Number)).asymmetricMatch(['a', 'b']),
560+
]) {
561+
jestExpect(test).toEqual(true);
562+
}
563+
});
564+
565+
test('NotArrayOf does not match', () => {
566+
for (const test of [
567+
notArrayOf(1).asymmetricMatch([1]),
568+
notArrayOf(1).asymmetricMatch([1, 1, 1]),
569+
notArrayOf({a: 1}).asymmetricMatch([{a: 1}, {a: 1}]),
570+
notArrayOf(undefined).asymmetricMatch([undefined]),
571+
notArrayOf(null).asymmetricMatch([null]),
572+
notArrayOf([]).asymmetricMatch([[], []]),
573+
notArrayOf(any(String)).asymmetricMatch(['a', 'b', 'c']),
574+
]) {
575+
jestExpect(test).toEqual(false);
576+
}
577+
});

Diff for: packages/expect/src/asymmetricMatchers.ts

+24
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,27 @@ class ArrayContaining extends AsymmetricMatcher<Array<unknown>> {
225225
}
226226
}
227227

228+
class ArrayOf extends AsymmetricMatcher<unknown> {
229+
asymmetricMatch(other: unknown) {
230+
const matcherContext = this.getMatcherContext();
231+
const result =
232+
Array.isArray(other) &&
233+
other.every(item =>
234+
equals(this.sample, item, matcherContext.customTesters),
235+
);
236+
237+
return this.inverse ? !result : result;
238+
}
239+
240+
toString() {
241+
return `${this.inverse ? 'Not' : ''}ArrayOf`;
242+
}
243+
244+
override getExpectedType() {
245+
return 'array';
246+
}
247+
}
248+
228249
class ObjectContaining extends AsymmetricMatcher<
229250
Record<string | symbol, unknown>
230251
> {
@@ -383,6 +404,9 @@ export const arrayContaining = (sample: Array<unknown>): ArrayContaining =>
383404
new ArrayContaining(sample);
384405
export const arrayNotContaining = (sample: Array<unknown>): ArrayContaining =>
385406
new ArrayContaining(sample, true);
407+
export const arrayOf = (sample: unknown): ArrayOf => new ArrayOf(sample);
408+
export const notArrayOf = (sample: unknown): ArrayOf =>
409+
new ArrayOf(sample, true);
386410
export const objectContaining = (
387411
sample: Record<string, unknown>,
388412
): ObjectContaining => new ObjectContaining(sample);

Diff for: packages/expect/src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import {
1616
anything,
1717
arrayContaining,
1818
arrayNotContaining,
19+
arrayOf,
1920
closeTo,
21+
notArrayOf,
2022
notCloseTo,
2123
objectContaining,
2224
objectNotContaining,
@@ -397,13 +399,15 @@ expect.any = any;
397399

398400
expect.not = {
399401
arrayContaining: arrayNotContaining,
402+
arrayOf: notArrayOf,
400403
closeTo: notCloseTo,
401404
objectContaining: objectNotContaining,
402405
stringContaining: stringNotContaining,
403406
stringMatching: stringNotMatching,
404407
};
405408

406409
expect.arrayContaining = arrayContaining;
410+
expect.arrayOf = arrayOf;
407411
expect.closeTo = closeTo;
408412
expect.objectContaining = objectContaining;
409413
expect.stringContaining = stringContaining;

Diff for: packages/expect/src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export interface AsymmetricMatchers {
117117
any(sample: unknown): AsymmetricMatcher;
118118
anything(): AsymmetricMatcher;
119119
arrayContaining(sample: Array<unknown>): AsymmetricMatcher;
120+
arrayOf(sample: unknown): AsymmetricMatcher;
120121
closeTo(sample: number, precision?: number): AsymmetricMatcher;
121122
objectContaining(sample: Record<string, unknown>): AsymmetricMatcher;
122123
stringContaining(sample: string): AsymmetricMatcher;

Diff for: packages/pretty-format/src/__tests__/AsymmetricMatcher.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ test('arrayNotContaining()', () => {
7878
]`);
7979
});
8080

81+
test('arrayOf()', () => {
82+
const result = prettyFormat(expect.arrayOf(expect.any(String)), options);
83+
expect(result).toBe('ArrayOf Any<String>');
84+
});
85+
86+
test('notArrayOf()', () => {
87+
const result = prettyFormat(expect.not.arrayOf(expect.any(String)), options);
88+
expect(result).toBe('NotArrayOf Any<String>');
89+
});
90+
8191
test('objectContaining()', () => {
8292
const result = prettyFormat(expect.objectContaining({a: 'test'}), options);
8393
expect(result).toBe(`ObjectContaining {
@@ -183,6 +193,11 @@ test('supports multiple nested asymmetric matchers', () => {
183193
d: expect.stringContaining('jest'),
184194
e: expect.stringMatching('jest'),
185195
f: expect.objectContaining({test: 'case'}),
196+
g: expect.arrayOf(
197+
expect.objectContaining({
198+
nested: expect.any(Number),
199+
}),
200+
),
186201
}),
187202
},
188203
},
@@ -201,6 +216,9 @@ test('supports multiple nested asymmetric matchers', () => {
201216
"f": ObjectContaining {
202217
"test": "case",
203218
},
219+
"g": ArrayOf ObjectContaining {
220+
"nested": Any<Number>,
221+
},
204222
},
205223
},
206224
}`);

Diff for: packages/pretty-format/src/plugins/AsymmetricMatcher.ts

+13
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ export const serialize: NewPlugin['serialize'] = (
8080
);
8181
}
8282

83+
if (stringedValue === 'ArrayOf' || stringedValue === 'NotArrayOf') {
84+
if (++depth > config.maxDepth) {
85+
return `[${stringedValue}]`;
86+
}
87+
return `${stringedValue + SPACE}${printer(
88+
val.sample,
89+
config,
90+
indentation,
91+
depth,
92+
refs,
93+
)}`;
94+
}
95+
8396
if (typeof val.toAsymmetricMatcher !== 'function') {
8497
throw new TypeError(
8598
`Asymmetric matcher ${val.constructor.name} does not implement toAsymmetricMatcher()`,

0 commit comments

Comments
 (0)