Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

assert.propContains API Implementation & Tests #1668

Merged
merged 2 commits into from
Feb 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
79 changes: 79 additions & 0 deletions docs/assert/notPropContains.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
layout: page-api
title: assert.notPropContains()
excerpt: Check that an object does not contain certain properties.
groups:
- assert
version_added: "unreleased"
---

`notPropContains( actual, expected, message = "" )`

Check that an object does not contain certain properties.

| name | description |
|------|-------------|
| `actual` | Expression being tested |
| `expected` | Known comparison value |
| `message` (string) | Short description of the actual value |


The `notPropContains` assertion compares the subset of properties in the expected object, and tests that these keys are either absent or hold a value that is different according to a strict equality comparison.

This method is recursive and allows partial comparison of nested objects as well.

## See also

* Use [`assert.propContains()`](./propContains.md) to test for the presence and equality of properties instead.

## Examples

```js
QUnit.test( "example", assert => {

const result = {
foo: 0,
vehicle: {
timeCircuits: "on",
fluxCapacitor: "fluxing",
engine: "running"
},
quux: 1
};

// succeeds, property "timeCircuits" is actually "on"
assert.notPropContains( result, {
vehicle: {
timeCircuits: "off"
}
} );

// succeeds, property "wings" is not in the object
assert.notPropContains( result, {
vehicle: {
wings: "flapping"
}
} );

function Point( x, y ) {
this.x = x;
this.y = y;
}

assert.notPropContains(
new Point( 10, 20 ),
{ z: 30 }
);

const nested = {
north: [ /* ... */ ],
east: new Point( 10, 20 ),
south: [ /* ... */ ],
west: [ /* ... */ ]
};

assert.notPropContains( nested, { east: new Point( 88, 42 ) } );
assert.notPropContains( nested, { east: { x: 88 } } );

});
```
15 changes: 8 additions & 7 deletions docs/assert/notPropEqual.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: page-api
title: assert.notPropEqual()
excerpt: A strict comparison of an object's own properties, checking for inequality.
excerpt: Compare an object's own properties for inequality.
groups:
- assert
redirect_from:
Expand All @@ -11,21 +11,22 @@ version_added: "1.11.0"

`notPropEqual( actual, expected, message = "" )`

A strict comparison of an object's own properties, checking for inequality.
Compare an object's own properties using a strict inequality comparison.

| name | description |
|------|-------------|
| `actual` | Expression being tested |
| `expected` | Known comparison value |
| `message` (string) | A short description of the assertion |

The `notPropEqual` assertion uses the strict inverted comparison operator (`!==`) to compare the actual and expected arguments as Objects regarding only their properties but not their constructors.
The `notPropEqual` assertion compares only an object's own properties, using the strict inquality operator (`!==`).

When they aren't equal, the assertion passes; otherwise, it fails. When it fails, both actual and expected values are displayed in the test result, in addition to a given message.
The test passes if there are properties with different values, or extra properties, or missing properties.

[`assert.equal()`](./equal.md) can be used to test equality.
## See also

[`assert.propEqual()`](./propEqual.md) can be used to test strict equality of an Object properties.
* Use [`assert.notPropContains()`](./notPropContains.md) to only check for the absence or inequality of some properties.
* Use [`assert.propEqual()`](./propEqual.md) to test for equality of properties instead.

## Examples

Expand All @@ -49,6 +50,6 @@ QUnit.test( "example", assert => {
assert.notPropEqual( foo, {
x: 1,
y: 2
} );
});
});
```
78 changes: 78 additions & 0 deletions docs/assert/propContains.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
layout: page-api
title: assert.propContains()
excerpt: Check that an object contains certain properties.
groups:
- assert
version_added: "unreleased"
---

`propContains( actual, expected, message = "" )`

Check that an object contains certain properties.

| name | description |
|------|-------------|
| `actual` | Expression being tested |
| `expected` | Known comparison value |
| `message` (string) | Short description of the actual value |


The `propContains` assertion compares only the **subset** of properties in the expected object,
and tests that these keys exist as own properties with strictly equal values.

This method is recursive and allows partial comparison of nested objects as well.

## See also

* Use [`assert.propEqual()`](./propEqual.md) to compare all properties, considering extra properties as unexpected.
* Use [`assert.notPropContains()`](./notPropContains.md) to test for the absence or inequality of certain properties.

## Examples

```js
QUnit.test( "example", assert => {

const result = {
foo: 0,
vehicle: {
timeCircuits: "on",
fluxCapacitor: "fluxing",
engine: "running"
},
quux: 1
};

assert.propContains( result, {
foo: 0,
vehicle: { fluxCapacitor: "fluxing" }
} );

function Point( x, y ) {
this.x = x;
this.y = y;
}

assert.propContains(
new Point( 10, 20 ),
{ y: 20 }
);

assert.propContains(
[ "a", "b" ],
{ 1: "b" }
);

const nested = {
north: [ /* ... */ ],
east: new Point( 10, 20 ),
south: [ /* ... */ ],
west: [ /* ... */ ]
};

assert.propContains( nested, { east: new Point( 10, 20 ) } );
assert.propContains( nested, { east: { x: 10, y: 20 } } );
assert.propContains( nested, { east: { x: 10 } } );

});
```
25 changes: 15 additions & 10 deletions docs/assert/propEqual.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: page-api
title: assert.propEqual()
excerpt: A strict type and value comparison of an object's own properties.
excerpt: Compare an object's own properties.
groups:
- assert
redirect_from:
Expand All @@ -11,23 +11,28 @@ version_added: "1.11.0"

`propEqual( actual, expected, message = "" )`

A strict type and value comparison of an object's own properties.
Compare an object's own properties using a strict comparison.

| name | description |
|------|-------------|
| `actual` | Expression being tested |
| `expected` | Known comparison value |
| `message` (string) | A short description of the assertion |

The `propEqual` assertion provides strictly (`===`) comparison of Object properties. Unlike [`assert.deepEqual()`](./deepEqual.md), this assertion can be used to compare two objects made with different constructors or prototypes.
The `propEqual` assertion compares only an object's own properties. This means the expected value does not need to be an instance of the same class or otherwise inherit the same prototype, unlike with [`assert.deepEqual()`](./deepEqual.md).

[`assert.strictEqual()`](./strictEqual.md) can be used to test strict equality.
The assertion fails if values differ, if there are additional properties, or if some properties are missing.

[`assert.notPropEqual()`](./notPropEqual.md) can be used to explicitly test strict inequality of Object properties.
This method is recursive and can compare any nested or complex object via a plain object.

## See also

* Use [`assert.propContains()`](./propContains.md) to only check a subset of properties.
* Use [`assert.notPropEqual()`](./notPropEqual.md) to test for the inequality of object properties instead.

## Examples

Compare the properties values of two objects.
Compare the property values of two objects.

```js
QUnit.test( "example", assert => {
Expand All @@ -43,11 +48,11 @@ QUnit.test( "example", assert => {
const foo = new Foo();

// succeeds, own properties are strictly equal,
// and inherited properties (such as which object constructor) are ignored.
// and inherited properties (such as which constructor) are ignored.
assert.propEqual( foo, {
x: 1,
y: 2
} );
});
});
```

Expand All @@ -63,12 +68,12 @@ QUnit.test( "example", function ( assert ) {
Foo.prototype.run = function () {};

var foo = new Foo();

// succeeds, own properties are strictly equal.
var expected = {
x: 1,
y: 2
};

// succeeds, own properties are strictly equal.
assert.propEqual( foo, expected );
});
```
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