Skip to content

Commit

Permalink
fix(assertions): object partiality is dropped passing through arrays (#…
Browse files Browse the repository at this point in the history
…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*
  • Loading branch information
rix0rrr committed Jan 19, 2022
1 parent 2041278 commit eb29e6f
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 4 deletions.
16 changes: 14 additions & 2 deletions packages/@aws-cdk/assertions/lib/match.ts
Expand Up @@ -115,7 +115,7 @@ class LiteralMatch extends Matcher {

public test(actual: any): MatchResult {
if (Array.isArray(this.pattern)) {
return new ArrayMatch(this.name, this.pattern, { subsequence: false }).test(actual);
return new ArrayMatch(this.name, this.pattern, { subsequence: false, partialObjects: this.partialObjects }).test(actual);
}

if (typeof this.pattern === 'object') {
Expand Down Expand Up @@ -155,13 +155,21 @@ interface ArrayMatchOptions {
* @default true
*/
readonly subsequence?: boolean;

/**
* Whether to continue matching objects inside the array partially
*
* @default false
*/
readonly partialObjects?: boolean;
}

/**
* Match class that matches arrays.
*/
class ArrayMatch extends Matcher {
private readonly subsequence: boolean;
private readonly partialObjects: boolean;

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

super();
this.subsequence = options.subsequence ?? true;
this.partialObjects = options.partialObjects ?? false;
}

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

const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement);
const matcher = Matcher.isMatcher(patternElement)
? patternElement
: new LiteralMatch(this.name, patternElement, { partialObjects: this.partialObjects });

const matcherName = matcher.name;
if (this.subsequence && (matcherName == 'absent' || matcherName == 'anyValue')) {
// array subsequence matcher is not compatible with anyValue() or absent() matcher. They don't make sense to be used together.
Expand Down
21 changes: 19 additions & 2 deletions packages/@aws-cdk/assertions/test/match.test.ts
Expand Up @@ -176,14 +176,31 @@ describe('Matchers', () => {
expectPass(matcher, { foo: 'bar', baz: { fred: 'waldo', wobble: 'flob' } });
});

test('nested with ArrayMatch', () => {
test('ArrayMatch nested inside ObjectMatch', () => {
matcher = Match.objectLike({
foo: Match.arrayWith(['bar']),
});
expectPass(matcher, { foo: ['bar', 'baz'], fred: 'waldo' });
expectFailure(matcher, { foo: ['baz'], fred: 'waldo' }, [/Missing element \[bar\] at pattern index 0 at \/foo/]);
});

test('Partiality is maintained throughout arrays', () => {
// Before this fix:
//
// - objectLike({ x: { LITERAL }) ==> LITERAL would be matched partially as well
// - objectLike({ xs: [ { LITERAL } ] }) ==> but here LITERAL would be matched fully
//
// That passing through an array resets the partial matching to full is a
// surprising inconsistency.
//
matcher = Match.objectLike({
foo: [{ bar: 'bar' }],
});
expectPass(matcher, { foo: [{ bar: 'bar' }] }); // Trivially true
expectPass(matcher, { boo: 'boo', foo: [{ bar: 'bar' }] }); // Additional members at top level okay
expectPass(matcher, { foo: [{ bar: 'bar', boo: 'boo' }] }); // Additional members at inner level okay
});

test('absent', () => {
matcher = Match.objectLike({ foo: Match.absent() });
expectPass(matcher, { bar: 'baz' });
Expand Down Expand Up @@ -389,7 +406,7 @@ describe('Matchers', () => {
function expectPass(matcher: Matcher, target: any): void {
const result = matcher.test(target);
if (result.hasFailed()) {
fail(result.toHumanStrings()); // eslint-disable-line jest/no-jasmine-globals
throw new Error(result.toHumanStrings().join('\n')); // eslint-disable-line jest/no-jasmine-globals
}
}

Expand Down

0 comments on commit eb29e6f

Please sign in to comment.