Skip to content

Commit da2e109

Browse files
feat: integrated chai-subset and added assert-based negation to containSubset (#1664)
Adds the features of `chai-subset` to core.
1 parent d044441 commit da2e109

File tree

3 files changed

+359
-1
lines changed

3 files changed

+359
-1
lines changed

lib/chai/core/assertions.js

+90
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,92 @@ 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 {boolean}
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 (!actual) {
4084+
return false;
4085+
}
4086+
4087+
if (Array.isArray(expected)) {
4088+
if (!Array.isArray(actual)) {
4089+
return false;
4090+
}
4091+
return expected.every(function (exp) {
4092+
return actual.some(function (act) {
4093+
return compareSubset(exp, act);
4094+
});
4095+
});
4096+
}
4097+
4098+
if (expected instanceof Date) {
4099+
if (actual instanceof Date) {
4100+
return expected.getTime() === actual.getTime();
4101+
} else {
4102+
return false;
4103+
}
4104+
}
4105+
4106+
return Object.keys(expected).every(function (key) {
4107+
var expectedValue = expected[key];
4108+
var actualValue = actual[key];
4109+
if (
4110+
typeof expectedValue === 'object' &&
4111+
expectedValue !== null &&
4112+
actualValue !== null
4113+
) {
4114+
return compareSubset(expectedValue, actualValue);
4115+
}
4116+
if (typeof expectedValue === 'function') {
4117+
return expectedValue(actualValue);
4118+
}
4119+
return actualValue === expectedValue;
4120+
});
4121+
}
4122+
4123+
/**
4124+
* ### .containSubset
4125+
*
4126+
* Asserts that the target primitive/object/array structure deeply contains all provided fields
4127+
* at the same key/depth as the provided structure.
4128+
*
4129+
* When comparing arrays, the target must contain the subset of at least one of each object/value in the subset array.
4130+
* Order does not matter.
4131+
*
4132+
* expect({name: {first: "John", last: "Smith"}}).to.containSubset({name: {first: "John"}});
4133+
*
4134+
* Add `.not` earlier in the chain to negate the assertion. This will cause the assertion to fail
4135+
* only if the target DOES contains the provided data at the expected keys/depths.
4136+
*
4137+
* @name containSubset
4138+
* @namespace BDD
4139+
* @public
4140+
*/
4141+
Assertion.addMethod('containSubset', function (expected) {
4142+
const actual = _.flag(this, 'object');
4143+
const showDiff = config.showDiff;
4144+
4145+
this.assert(
4146+
compareSubset(expected, actual),
4147+
'expected #{act} to contain subset #{exp}',
4148+
'expected #{act} to not contain subset #{exp}',
4149+
expected,
4150+
actual,
4151+
showDiff
4152+
);
4153+
});

lib/chai/interface/assert.js

+44-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
*
@@ -3178,7 +3220,8 @@ const aliases = [
31783220
['isEmpty', 'empty'],
31793221
['isNotEmpty', 'notEmpty'],
31803222
['isCallable', 'isFunction'],
3181-
['isNotCallable', 'isNotFunction']
3223+
['isNotCallable', 'isNotFunction'],
3224+
['containsSubset', 'containSubset']
31823225
];
31833226
for (const [name, as] of aliases) {
31843227
assert[as] = assert[name];

test/subset.js

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

0 commit comments

Comments
 (0)