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

Lines changed: 90 additions & 0 deletions
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

Lines changed: 44 additions & 1 deletion
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

Lines changed: 225 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy