Skip to content

Commit

Permalink
Assert: New assert.propContains() for partial object comparison
Browse files Browse the repository at this point in the history
Closes #1668.
  • Loading branch information
izelnakri authored and Krinkle committed Feb 6, 2022
1 parent 0e105de commit 574fcec
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 10 deletions.
32 changes: 31 additions & 1 deletion src/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { internalStop, resetTestTimeout } from "./test";
import Logger from "./logger";

import config from "./core/config";
import { objectType, objectValues, errorString } from "./core/utilities";
import { objectType, objectValues, objectValuesSubset, errorString } from "./core/utilities";
import { sourceFromStacktrace } from "./core/stacktrace";
import { clearTimeout } from "./globals";

Expand Down Expand Up @@ -216,6 +216,36 @@ class Assert {
} );
}

propContains( actual, expected, message ) {
actual = objectValuesSubset( actual, expected );

// The expected parameter is usually a plain object, but clone it for
// consistency with propEqual(), and to make it easy to explain that
// inheritence is not considered (on either side), and to support
// recursively checking subsets of nested objects.
expected = objectValues( expected, false );

this.pushResult( {
result: equiv( actual, expected ),
actual,
expected,
message
} );
}

notPropContains( actual, expected, message ) {
actual = objectValuesSubset( actual, expected );
expected = objectValues( expected );

this.pushResult( {
result: !equiv( actual, expected ),
actual,
expected,
message,
negative: true
} );
}

deepEqual( actual, expected, message ) {
this.pushResult( {
result: equiv( actual, expected ),
Expand Down
45 changes: 38 additions & 7 deletions src/core/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,54 @@ export function inArray( elem, array ) {
}

/**
* Makes a clone of an object using only Array or Object as base,
* and copies over the own enumerable properties.
* Recursively clone an object into a plain array or object, taking only the
* own enumerable properties.
*
* @param {Object} obj
* @return {Object} New object with only the own properties (recursively).
* @param {any} obj
* @param {bool} [allowArray=true]
* @return {Object|Array}
*/
export function objectValues( obj ) {
const vals = is( "array", obj ) ? [] : {};
export function objectValues( obj, allowArray = true ) {
const vals = ( allowArray && is( "array", obj ) ) ? [] : {};
for ( const key in obj ) {
if ( hasOwn.call( obj, key ) ) {
const val = obj[ key ];
vals[ key ] = val === Object( val ) ? objectValues( val ) : val;
vals[ key ] = val === Object( val ) ? objectValues( val, allowArray ) : val;
}
}
return vals;
}

/**
* Recursively clone an object into a plain object, taking only the
* subset of own enumerable properties that exist a given model.
*
* @param {any} obj
* @param {any} model
* @return {Object}
*/
export function objectValuesSubset( obj, model ) {

// Return primitive values unchanged to avoid false positives or confusing
// results from assert.propContains().
// E.g. an actual null or false wrongly equaling an empty object,
// or an actual string being reported as object not matching a partial object.
if ( obj !== Object( obj ) ) {
return obj;
}

// Unlike objectValues(), subset arrays to a plain objects as well.
// This enables subsetting [20, 30] with {1: 30}.
const subset = {};

for ( const key in model ) {
if ( hasOwn.call( model, key ) && hasOwn.call( obj, key ) ) {
subset[ key ] = objectValuesSubset( obj[ key ], model[ key ] );
}
}
return subset;
}

export function extend( a, b, undefOnly ) {
for ( const prop in b ) {
if ( hasOwn.call( b, prop ) ) {
Expand Down
153 changes: 151 additions & 2 deletions test/main/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ QUnit.test( "propEqual", function( assert ) {
this.z = z;
}
Foo.prototype.doA = function() {};
Foo.prototype.doB = function() {};
Foo.prototype.bar = "prototype";
Foo.prototype.bar = "non-function";

function Bar() {
}
Expand Down Expand Up @@ -158,6 +157,156 @@ QUnit.test( "propEqual", function( assert ) {
);
} );

QUnit.test( "propContains", function( assert ) {
function Foo( x, y, z ) {
this.x = x;
this.y = y;
this.z = z;
}
Foo.prototype.doA = function() {};
Foo.prototype.bar = "non-function";

function Bar( x ) {
this.x = x;
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.constructor = Bar;

assert.propContains(
{ a: 0, b: "something", c: true },
{ a: 0, b: "something", c: true }
);
assert.propContains(
{ a: 0, b: "something", c: true },
{ a: 0, c: true },
"match object subset"
);
assert.propContains(
[ "a", "b" ],
{ 1: "b" },
"match array subset via plain object"
);
assert.propContains(
[],
{},
"empty array contains empty object"
);
assert.propContains(
{},
[],
"empty object contains empty array"
);
assert.propContains(
new Foo( 1, "2", [] ),
new Foo( 1, "2", [] ),
"deeply equal class instances"
);
assert.propContains(
new Foo( 1, "2", [] ),
{
x: 1,
y: "2",
z: []
},
"match different constructor via plain object"
);
assert.propContains(
new Foo( 1, "2", [] ),
{
x: 1
},
"match different constructor subset via plain object"
);
assert.propContains(
new Foo( 1, "2", [ "x" ] ),
new Foo( 1, "2", { 0: "x" } ),
"match nested array via plain object"
);
assert.propContains(
new Foo( 1, [ "a", "b" ], new Foo( [ "c", "d" ], new Bar(), null ) ),
{
x: 1,
y: [ "a", "b" ],
z: {
x: { 1: "d" }
}
},
"match nested array subset via plain object"
);
assert.propContains(
new Foo( 1, "2" ),
new Bar( 1 ),
"match subset via different constructor"
);
} );

QUnit.test( "notPropContains", function( assert ) {
function Foo( x, y, z ) {
this.x = x;
this.y = y;
this.z = z;
}
Foo.prototype.doA = function() {};
Foo.prototype.bar = "non-function";

function Bar( x ) {
this.x = x;
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.constructor = Bar;

assert.notPropContains(
{ a: 0, b: "something", c: true },
{ a: 0, b: "different", c: true }
);
assert.notPropContains(
{ a: 0, b: "something", c: true },
{ a: 0, c: false }
);
assert.notPropContains(
{ a: 0, b: "something", c: true },
{ e: "missing" }
);
assert.notPropContains(
new Foo( 1, "2", [] ),
{
x: 1,
y: "2",
z: [],
e: "missing"
},
"matching and missing properties"
);
assert.notPropContains(
new Foo( 1, "2", [] ),
{
e: "missing"
},
"missing property"
);
assert.notPropContains(
new Foo( 1, [], new Foo( [], new Bar(), "something" ) ),
new Foo( 1, [], new Foo( [], new Bar(), "different" ) ),
"difference in nested value"
);
assert.notPropContains(
new Foo( 1, "2", new Foo( [ 3 ], new Bar(), null ) ),
{
x: 1,
y: "2",
z: {
e: "missing"
}
},
"nested object with missing property"
);
assert.notPropContains(
new Foo( 1, "2" ),
new Bar( 2 ),
"different property value via different constructor"
);
} );

QUnit.test( "throws", function( assert ) {
function CustomError( message ) {
this.message = message;
Expand Down

0 comments on commit 574fcec

Please sign in to comment.