Skip to content

Commit 823fd4d

Browse files
authored
feat(expect): add ArrayOf asymmetric matcher (#15567)
1 parent 6d7e796 commit 823fd4d

File tree

8 files changed

+168
-0
lines changed

8 files changed

+168
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
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. ([#15567](https://github.com/jestjs/jest/pull/15567))
56
- `[babel-jest]` Add option `excludeJestPreset` to allow opting out of `babel-preset-jest` ([#15164](https://github.com/jestjs/jest/pull/15164))
67
- `[expect]` Revert [#15038](https://github.com/jestjs/jest/pull/15038) to fix `expect(fn).toHaveBeenCalledWith(expect.objectContaining(...))` when there are multiple calls ([#15508](https://github.com/jestjs/jest/pull/15508))
78
- `[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))

docs/ExpectAPI.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,52 @@ 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(
970+
expect.arrayOf(expect.any(String)),
971+
);
972+
});
973+
```
974+
975+
This matcher is particularly useful for validating arrays containing complex structures:
976+
977+
```js
978+
test('array of objects with specific properties', () => {
979+
expect([
980+
{id: 1, name: 'Alice'},
981+
{id: 2, name: 'Bob'},
982+
]).toEqual(
983+
expect.arrayOf(
984+
expect.objectContaining({
985+
id: expect.any(Number),
986+
name: expect.any(String),
987+
}),
988+
),
989+
);
990+
});
991+
```
992+
993+
### `expect.not.arrayOf(value)`
994+
995+
`expect.not.arrayOf(value)` matches a received array where not all elements match the provided matcher.
996+
997+
**Example:**
998+
999+
```js
1000+
test('not all elements in array are strings', () => {
1001+
expect(['apple', 123, 'cherry']).toEqual(
1002+
expect.not.arrayOf(expect.any(String)),
1003+
);
1004+
});
1005+
```
1006+
9611007
### `expect.closeTo(number, numDigits?)`
9621008

9631009
`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.

packages/expect/src/__tests__/asymmetricMatchers.test.ts

Lines changed: 61 additions & 0 deletions
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,
@@ -523,3 +525,62 @@ describe('closeTo', () => {
523525
jestExpect(notCloseTo(1).asymmetricMatch('a')).toBe(false);
524526
});
525527
});
528+
529+
test('ArrayOf matches', () => {
530+
for (const test of [
531+
arrayOf(1).asymmetricMatch([1]),
532+
arrayOf(1).asymmetricMatch([1, 1, 1]),
533+
arrayOf({a: 1}).asymmetricMatch([{a: 1}, {a: 1}]),
534+
arrayOf(undefined).asymmetricMatch([undefined]),
535+
arrayOf(null).asymmetricMatch([null]),
536+
arrayOf([]).asymmetricMatch([[], []]),
537+
arrayOf(any(String)).asymmetricMatch(['a', 'b', 'c']),
538+
]) {
539+
jestExpect(test).toEqual(true);
540+
}
541+
});
542+
543+
test('ArrayOf does not match', () => {
544+
for (const test of [
545+
arrayOf(1).asymmetricMatch([2]),
546+
arrayOf(1).asymmetricMatch([1, 2]),
547+
arrayOf({a: 1}).asymmetricMatch([{a: 2}]),
548+
arrayOf(undefined).asymmetricMatch([null]),
549+
arrayOf(null).asymmetricMatch([undefined]),
550+
arrayOf([]).asymmetricMatch([{}]),
551+
arrayOf(1).asymmetricMatch(1),
552+
arrayOf(1).asymmetricMatch('not an array'),
553+
arrayOf(1).asymmetricMatch({}),
554+
arrayOf(any(String)).asymmetricMatch([1, 2]),
555+
]) {
556+
jestExpect(test).toEqual(false);
557+
}
558+
});
559+
560+
test('NotArrayOf matches', () => {
561+
for (const test of [
562+
notArrayOf(1).asymmetricMatch([2]),
563+
notArrayOf(1).asymmetricMatch([1, 2]),
564+
notArrayOf({a: 1}).asymmetricMatch([{a: 2}]),
565+
notArrayOf(1).asymmetricMatch(1),
566+
notArrayOf(1).asymmetricMatch('not an array'),
567+
notArrayOf(1).asymmetricMatch({}),
568+
notArrayOf(any(Number)).asymmetricMatch(['a', 'b']),
569+
]) {
570+
jestExpect(test).toEqual(true);
571+
}
572+
});
573+
574+
test('NotArrayOf does not match', () => {
575+
for (const test of [
576+
notArrayOf(1).asymmetricMatch([1]),
577+
notArrayOf(1).asymmetricMatch([1, 1, 1]),
578+
notArrayOf({a: 1}).asymmetricMatch([{a: 1}, {a: 1}]),
579+
notArrayOf(undefined).asymmetricMatch([undefined]),
580+
notArrayOf(null).asymmetricMatch([null]),
581+
notArrayOf([]).asymmetricMatch([[], []]),
582+
notArrayOf(any(String)).asymmetricMatch(['a', 'b', 'c']),
583+
]) {
584+
jestExpect(test).toEqual(false);
585+
}
586+
});

packages/expect/src/asymmetricMatchers.ts

Lines changed: 24 additions & 0 deletions
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
> {
@@ -384,6 +405,9 @@ export const arrayContaining = (sample: Array<unknown>): ArrayContaining =>
384405
new ArrayContaining(sample);
385406
export const arrayNotContaining = (sample: Array<unknown>): ArrayContaining =>
386407
new ArrayContaining(sample, true);
408+
export const arrayOf = (sample: unknown): ArrayOf => new ArrayOf(sample);
409+
export const notArrayOf = (sample: unknown): ArrayOf =>
410+
new ArrayOf(sample, true);
387411
export const objectContaining = (
388412
sample: Record<string, unknown>,
389413
): ObjectContaining => new ObjectContaining(sample);

packages/expect/src/index.ts

Lines changed: 4 additions & 0 deletions
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;

packages/expect/src/types.ts

Lines changed: 1 addition & 0 deletions
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;

packages/pretty-format/src/__tests__/AsymmetricMatcher.test.ts

Lines changed: 18 additions & 0 deletions
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
}`);

packages/pretty-format/src/plugins/AsymmetricMatcher.ts

Lines changed: 13 additions & 0 deletions
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)