Skip to content

Commit eb29e6f

Browse files
authored
fix(assertions): object partiality is dropped passing through arrays (#18525)
`objectLike()` imposes partial object matching. That means that we don't need to fully specify all properties of an object to match it, but just the properties we care about (all other properties can have any value). Partial object matching is inherited. That means that in nested objects, the partiality is maintained: ```ts objectLike({ x: 'x', inner: { // Matches any object that has AT LEAST an 'y' property y: 'y', } }) ``` However, the partiality is dropped when passing through arrays: ```ts objectLike({ x: 'x', inner: [ { // Matches any object that has ONLY an 'y' property y: 'y', } ], }) ``` This is both unintuitive and different from past behavior, which makes migrating tests unnecessarily hard. Fix the discrepancy. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 2041278 commit eb29e6f

File tree

2 files changed

+33
-4
lines changed

2 files changed

+33
-4
lines changed

packages/@aws-cdk/assertions/lib/match.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class LiteralMatch extends Matcher {
115115

116116
public test(actual: any): MatchResult {
117117
if (Array.isArray(this.pattern)) {
118-
return new ArrayMatch(this.name, this.pattern, { subsequence: false }).test(actual);
118+
return new ArrayMatch(this.name, this.pattern, { subsequence: false, partialObjects: this.partialObjects }).test(actual);
119119
}
120120

121121
if (typeof this.pattern === 'object') {
@@ -155,13 +155,21 @@ interface ArrayMatchOptions {
155155
* @default true
156156
*/
157157
readonly subsequence?: boolean;
158+
159+
/**
160+
* Whether to continue matching objects inside the array partially
161+
*
162+
* @default false
163+
*/
164+
readonly partialObjects?: boolean;
158165
}
159166

160167
/**
161168
* Match class that matches arrays.
162169
*/
163170
class ArrayMatch extends Matcher {
164171
private readonly subsequence: boolean;
172+
private readonly partialObjects: boolean;
165173

166174
constructor(
167175
public readonly name: string,
@@ -170,6 +178,7 @@ class ArrayMatch extends Matcher {
170178

171179
super();
172180
this.subsequence = options.subsequence ?? true;
181+
this.partialObjects = options.partialObjects ?? false;
173182
}
174183

175184
public test(actual: any): MatchResult {
@@ -195,7 +204,10 @@ class ArrayMatch extends Matcher {
195204
while (patternIdx < this.pattern.length && actualIdx < actual.length) {
196205
const patternElement = this.pattern[patternIdx];
197206

198-
const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement);
207+
const matcher = Matcher.isMatcher(patternElement)
208+
? patternElement
209+
: new LiteralMatch(this.name, patternElement, { partialObjects: this.partialObjects });
210+
199211
const matcherName = matcher.name;
200212
if (this.subsequence && (matcherName == 'absent' || matcherName == 'anyValue')) {
201213
// array subsequence matcher is not compatible with anyValue() or absent() matcher. They don't make sense to be used together.

packages/@aws-cdk/assertions/test/match.test.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,31 @@ describe('Matchers', () => {
176176
expectPass(matcher, { foo: 'bar', baz: { fred: 'waldo', wobble: 'flob' } });
177177
});
178178

179-
test('nested with ArrayMatch', () => {
179+
test('ArrayMatch nested inside ObjectMatch', () => {
180180
matcher = Match.objectLike({
181181
foo: Match.arrayWith(['bar']),
182182
});
183183
expectPass(matcher, { foo: ['bar', 'baz'], fred: 'waldo' });
184184
expectFailure(matcher, { foo: ['baz'], fred: 'waldo' }, [/Missing element \[bar\] at pattern index 0 at \/foo/]);
185185
});
186186

187+
test('Partiality is maintained throughout arrays', () => {
188+
// Before this fix:
189+
//
190+
// - objectLike({ x: { LITERAL }) ==> LITERAL would be matched partially as well
191+
// - objectLike({ xs: [ { LITERAL } ] }) ==> but here LITERAL would be matched fully
192+
//
193+
// That passing through an array resets the partial matching to full is a
194+
// surprising inconsistency.
195+
//
196+
matcher = Match.objectLike({
197+
foo: [{ bar: 'bar' }],
198+
});
199+
expectPass(matcher, { foo: [{ bar: 'bar' }] }); // Trivially true
200+
expectPass(matcher, { boo: 'boo', foo: [{ bar: 'bar' }] }); // Additional members at top level okay
201+
expectPass(matcher, { foo: [{ bar: 'bar', boo: 'boo' }] }); // Additional members at inner level okay
202+
});
203+
187204
test('absent', () => {
188205
matcher = Match.objectLike({ foo: Match.absent() });
189206
expectPass(matcher, { bar: 'baz' });
@@ -389,7 +406,7 @@ describe('Matchers', () => {
389406
function expectPass(matcher: Matcher, target: any): void {
390407
const result = matcher.test(target);
391408
if (result.hasFailed()) {
392-
fail(result.toHumanStrings()); // eslint-disable-line jest/no-jasmine-globals
409+
throw new Error(result.toHumanStrings().join('\n')); // eslint-disable-line jest/no-jasmine-globals
393410
}
394411
}
395412

0 commit comments

Comments
 (0)