Skip to content

Commit 021bd18

Browse files
committed
Integrated chai-subset and added assert-based negation to containSubset
1 parent 6d8d727 commit 021bd18

File tree

3 files changed

+339
-1
lines changed

3 files changed

+339
-1
lines changed

lib/chai/core/assertions.js

+87
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import {Assertion} from '../assertion.js';
99
import {AssertionError} from 'assertion-error';
1010
import * as _ from '../utils/index.js';
11+
import {config} from '../config.js';
1112

1213
const {flag} = _;
1314

@@ -4061,3 +4062,89 @@ Assertion.addProperty('finite', function (msg) {
40614062
'expected #{this} to not be a finite number'
40624063
);
40634064
});
4065+
4066+
/**
4067+
* A subset-aware compare function
4068+
*
4069+
* @param {unknown} expected
4070+
* @param {unknown} actual
4071+
* @returns {void}
4072+
*/
4073+
function compareSubset(expected, actual) {
4074+
if (expected === actual) {
4075+
return true;
4076+
}
4077+
if (typeof actual !== typeof expected) {
4078+
return false;
4079+
}
4080+
if (typeof expected !== 'object' || expected === null) {
4081+
return expected === actual;
4082+
}
4083+
if (!!expected && !actual) {
4084+
return false;
4085+
}
4086+
4087+
if (Array.isArray(expected)) {
4088+
if (typeof actual.length !== 'number') {
4089+
return false;
4090+
}
4091+
var aa = Array.prototype.slice.call(actual);
4092+
return expected.every(function (exp) {
4093+
return aa.some(function (act) {
4094+
return compareSubset(exp, act);
4095+
});
4096+
});
4097+
}
4098+
4099+
if (expected instanceof Date) {
4100+
if (actual instanceof Date) {
4101+
return expected.getTime() === actual.getTime();
4102+
} else {
4103+
return false;
4104+
}
4105+
}
4106+
4107+
return Object.keys(expected).every(function (key) {
4108+
var eo = expected[key];
4109+
var ao = actual[key];
4110+
if (typeof eo === 'object' && eo !== null && ao !== null) {
4111+
return compareSubset(eo, ao);
4112+
}
4113+
if (typeof eo === 'function') {
4114+
return eo(ao);
4115+
}
4116+
return ao === eo;
4117+
});
4118+
}
4119+
4120+
/**
4121+
* ### .containSubset
4122+
*
4123+
* Asserts that the target primitive/object/array structure deeply contains all provided fields
4124+
* at the same key/depth as the provided structure.
4125+
*
4126+
* When comparing arrays, the target must contain the subset of at least one of each object/value in the subset array.
4127+
* Order does not matter.
4128+
*
4129+
* expect({name: {first: "John", last: "Smith"}}).to.containSubset({name: {first: "John"}});
4130+
*
4131+
* Add `.not` earlier in the chain to negate the assertion. This will cause the assertion to fail
4132+
* only if the target DOES contains the provided data at the expected keys/depths.
4133+
*
4134+
* @name containSubset
4135+
* @namespace BDD
4136+
* @public
4137+
*/
4138+
Assertion.addMethod('containSubset', function (expected) {
4139+
const actual = _.flag(this, 'object');
4140+
const showDiff = config.showDiff;
4141+
4142+
this.assert(
4143+
compareSubset(expected, actual),
4144+
'expected #{act} to contain subset #{exp}',
4145+
'expected #{act} to not contain subset #{exp}',
4146+
expected,
4147+
actual,
4148+
showDiff
4149+
);
4150+
});

lib/chai/interface/assert.js

+46-1
Original file line numberDiff line numberDiff line change
@@ -3157,6 +3157,48 @@ assert.isNotEmpty = function (val, msg) {
31573157
new Assertion(val, msg, assert.isNotEmpty, true).to.not.be.empty;
31583158
};
31593159

3160+
/**
3161+
* ### .containsSubset(target, subset)
3162+
*
3163+
* Asserts that the target primitive/object/array structure deeply contains all provided fields
3164+
* at the same key/depth as the provided structure.
3165+
*
3166+
* When comparing arrays, the target must contain the subset of at least one of each object/value in the subset array.
3167+
* Order does not matter.
3168+
*
3169+
* assert.containsSubset(
3170+
* [{name: {first: "John", last: "Smith"}}, {name: {first: "Jane", last: "Doe"}}],
3171+
* [{name: {first: "Jane"}}]
3172+
* );
3173+
*
3174+
* @name containsSubset
3175+
* @alias containSubset
3176+
* @param {unknown} val
3177+
* @param {unknown} exp
3178+
* @param {string} msg _optional_
3179+
* @namespace Assert
3180+
* @public
3181+
*/
3182+
assert.containsSubset = function (val, exp, msg) {
3183+
new Assertion(val, msg).to.containSubset(exp);
3184+
};
3185+
3186+
/**
3187+
* ### .doesNotContainSubset(target, subset)
3188+
*
3189+
* The negation of assert.containsSubset.
3190+
*
3191+
* @name doesNotContainSubset
3192+
* @param {unknown} val
3193+
* @param {unknown} exp
3194+
* @param {string} msg _optional_
3195+
* @namespace Assert
3196+
* @public
3197+
*/
3198+
assert.doesNotContainSubset = function (val, exp, msg) {
3199+
new Assertion(val, msg).to.not.containSubset(exp);
3200+
};
3201+
31603202
/**
31613203
* Aliases.
31623204
*
@@ -3176,4 +3218,7 @@ assert.isNotEmpty = function (val, msg) {
31763218
)('isFrozen', 'frozen')('isNotFrozen', 'notFrozen')('isEmpty', 'empty')(
31773219
'isNotEmpty',
31783220
'notEmpty'
3179-
)('isCallable', 'isFunction')('isNotCallable', 'isNotFunction');
3221+
)('isCallable', 'isFunction')('isNotCallable', 'isNotFunction')(
3222+
'containsSubset',
3223+
'containSubset'
3224+
);

test/subset.js

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import {assert, expect} from '../index.js';
2+
3+
describe('plain object', function () {
4+
var testedObject = {
5+
a: 'b',
6+
c: 'd'
7+
};
8+
9+
it('should pass for smaller object', function () {
10+
expect(testedObject).to.containSubset({
11+
a: 'b'
12+
});
13+
});
14+
15+
it('should pass for same object', function () {
16+
expect(testedObject).to.containSubset({
17+
a: 'b',
18+
c: 'd'
19+
});
20+
});
21+
22+
it('should pass for similar, but not the same object', function () {
23+
expect(testedObject).to.not.containSubset({
24+
a: 'notB',
25+
c: 'd'
26+
});
27+
});
28+
});
29+
30+
describe('complex object', function () {
31+
var testedObject = {
32+
a: 'b',
33+
c: 'd',
34+
e: {
35+
foo: 'bar',
36+
baz: {
37+
qux: 'quux'
38+
}
39+
}
40+
};
41+
42+
it('should pass for smaller object', function () {
43+
expect(testedObject).to.containSubset({
44+
a: 'b',
45+
e: {
46+
foo: 'bar'
47+
}
48+
});
49+
});
50+
51+
it('should pass for smaller object', function () {
52+
expect(testedObject).to.containSubset({
53+
e: {
54+
foo: 'bar',
55+
baz: {
56+
qux: 'quux'
57+
}
58+
}
59+
});
60+
});
61+
62+
it('should pass for same object', function () {
63+
expect(testedObject).to.containSubset({
64+
a: 'b',
65+
c: 'd',
66+
e: {
67+
foo: 'bar',
68+
baz: {
69+
qux: 'quux'
70+
}
71+
}
72+
});
73+
});
74+
75+
it('should pass for similar, but not the same object', function () {
76+
expect(testedObject).to.not.containSubset({
77+
e: {
78+
foo: 'bar',
79+
baz: {
80+
qux: 'notAQuux'
81+
}
82+
}
83+
});
84+
});
85+
86+
it('should fail if comparing when comparing objects to dates', function () {
87+
expect(testedObject).to.not.containSubset({
88+
e: new Date()
89+
});
90+
});
91+
});
92+
93+
describe('circular objects', function () {
94+
var object = {};
95+
96+
before(function () {
97+
object.arr = [object, object];
98+
object.arr.push(object.arr);
99+
object.obj = object;
100+
});
101+
102+
it('should contain subdocument', function () {
103+
expect(object).to.containSubset({
104+
arr: [{arr: []}, {arr: []}, [{arr: []}, {arr: []}]]
105+
});
106+
});
107+
108+
it('should not contain similar object', function () {
109+
expect(object).to.not.containSubset({
110+
arr: [{arr: ['just random field']}, {arr: []}, [{arr: []}, {arr: []}]]
111+
});
112+
});
113+
});
114+
115+
describe('object with compare function', function () {
116+
it('should pass when function returns true', function () {
117+
expect({a: 5}).to.containSubset({a: (a) => a});
118+
});
119+
120+
it('should fail when function returns false', function () {
121+
expect({a: 5}).to.not.containSubset({a: (a) => !a});
122+
});
123+
124+
it('should pass for function with no arguments', function () {
125+
expect({a: 5}).to.containSubset({a: () => true});
126+
});
127+
});
128+
129+
describe('comparison of non objects', function () {
130+
it('should fail if actual subset is null', function () {
131+
expect(null).to.not.containSubset({a: 1});
132+
});
133+
134+
it('should fail if expected subset is not a object', function () {
135+
expect({a: 1}).to.not.containSubset(null);
136+
});
137+
138+
it('should not fail for same non-object (string) variables', function () {
139+
expect('string').to.containSubset('string');
140+
});
141+
});
142+
143+
describe('assert style of test', function () {
144+
it('should find subset', function () {
145+
assert.containsSubset({a: 1, b: 2}, {a: 1});
146+
assert.containSubset({a: 1, b: 2}, {a: 1});
147+
});
148+
149+
it('negated assert style should function', function () {
150+
assert.doesNotContainSubset({a: 1, b: 2}, {a: 3});
151+
});
152+
});
153+
154+
describe('comparison of dates', function () {
155+
it('should pass for the same date', function () {
156+
expect(new Date('2015-11-30')).to.containSubset(new Date('2015-11-30'));
157+
});
158+
159+
it('should pass for the same date if nested', function () {
160+
expect({a: new Date('2015-11-30')}).to.containSubset({
161+
a: new Date('2015-11-30')
162+
});
163+
});
164+
165+
it('should fail for a different date', function () {
166+
expect(new Date('2015-11-30')).to.not.containSubset(new Date('2012-02-22'));
167+
});
168+
169+
it('should fail for a different date if nested', function () {
170+
expect({a: new Date('2015-11-30')}).to.not.containSubset({
171+
a: new Date('2012-02-22')
172+
});
173+
});
174+
175+
it('should fail for invalid expected date', function () {
176+
expect(new Date('2015-11-30')).to.not.containSubset(
177+
new Date('not valid date')
178+
);
179+
});
180+
181+
it('should fail for invalid actual date', function () {
182+
expect(new Date('not valid actual date')).to.not.containSubset(
183+
new Date('not valid expected date')
184+
);
185+
});
186+
});
187+
188+
describe('cyclic objects', () => {
189+
it('should pass', () => {
190+
const child = {};
191+
const parent = {
192+
children: [child]
193+
};
194+
child.parent = parent;
195+
196+
const myObject = {
197+
a: 1,
198+
b: 'two',
199+
c: parent
200+
};
201+
expect(myObject).to.containSubset({
202+
a: 1,
203+
c: parent
204+
});
205+
});
206+
});

0 commit comments

Comments
 (0)