Skip to content

Commit c988e27

Browse files
Support Symbol keys and ignore non-enumerable properties in t.like()
Fixes #3208 Co-authored-by: Mark Wubben <[email protected]>
1 parent 568fe40 commit c988e27

File tree

3 files changed

+34
-14
lines changed

3 files changed

+34
-14
lines changed

docs/03-assertions.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ Assert that `actual` is not deeply equal to `expected`. The inverse of `.deepEqu
141141

142142
Assert that `actual` is like `selector`. This is a variant of `.deepEqual()`, however `selector` does not need to have the same enumerable properties as `actual` does.
143143

144-
Instead AVA derives a *comparable* value from `actual`, recursively based on the shape of `selector`. This value is then compared to `selector` using `.deepEqual()`.
144+
Instead AVA derives a *comparable* value from `actual`, recursively based on the enumerable shape of `selector`. This value is then compared to `selector` using `.deepEqual()`.
145145

146146
Any values in `selector` that are not arrays or regular objects should be deeply equal to the corresponding values in `actual`.
147147

@@ -165,7 +165,7 @@ t.like({
165165
You can also use arrays, but note that any indices in `actual` that are not in `selector` are ignored:
166166

167167
```js
168-
t.like([1, 2, 3], [1, 2])
168+
t.like([1, 2, 3, 4], [1, , 3])
169169
```
170170

171171
Finally, this returns a boolean indicating whether the assertion passed.

lib/like-selector.js

+18-11
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
1-
const isObject = selector => Reflect.getPrototypeOf(selector) === Object.prototype;
1+
const isPrimitive = value => value === null || typeof value !== 'object';
22

33
export function isLikeSelector(selector) {
4-
if (selector === null || typeof selector !== 'object') {
4+
// Require selector to be an array or plain object.
5+
if (
6+
isPrimitive(selector)
7+
|| (!Array.isArray(selector) && Reflect.getPrototypeOf(selector) !== Object.prototype)
8+
) {
59
return false;
610
}
711

8-
const keyCount = Reflect.ownKeys(selector).length;
9-
return (Array.isArray(selector) && keyCount > 1) || (isObject(selector) && keyCount > 0);
12+
// Also require at least one enumerable property.
13+
const descriptors = Object.getOwnPropertyDescriptors(selector);
14+
return Reflect.ownKeys(descriptors).some(key => descriptors[key].enumerable === true);
1015
}
1116

1217
export const CIRCULAR_SELECTOR = new Error('Encountered a circular selector');
1318

14-
export function selectComparable(lhs, selector, circular = new Set()) {
19+
export function selectComparable(actual, selector, circular = new Set()) {
1520
if (circular.has(selector)) {
1621
throw CIRCULAR_SELECTOR;
1722
}
1823

1924
circular.add(selector);
2025

21-
if (lhs === null || typeof lhs !== 'object') {
22-
return lhs;
26+
if (isPrimitive(actual)) {
27+
return actual;
2328
}
2429

2530
const comparable = Array.isArray(selector) ? [] : {};
26-
for (const [key, rhs] of Object.entries(selector)) {
27-
comparable[key] = isLikeSelector(rhs)
28-
? selectComparable(Reflect.get(lhs, key), rhs, circular)
29-
: Reflect.get(lhs, key);
31+
const enumerableKeys = Reflect.ownKeys(selector).filter(key => Reflect.getOwnPropertyDescriptor(selector, key).enumerable);
32+
for (const key of enumerableKeys) {
33+
const subselector = Reflect.get(selector, key);
34+
comparable[key] = isLikeSelector(subselector)
35+
? selectComparable(Reflect.get(actual, key), subselector, circular)
36+
: Reflect.get(actual, key);
3037
}
3138

3239
return comparable;

test-tap/assert.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ test('.like()', t => {
720720
return assertions.like({xc: [circular, 'c']}, {xc: [circular, 'd']});
721721
});
722722

723-
failsWith(t, () => assertions.like({a: 'a'}, {}), {
723+
failsWith(t, () => assertions.like({a: 'a'}, Object.defineProperties({}, {ignored: {}})), {
724724
assertion: 'like',
725725
message: '`t.like()` selector must be a non-empty object',
726726
values: [{label: 'Called with:', formatted: '{}'}],
@@ -732,6 +732,15 @@ test('.like()', t => {
732732
values: [{label: 'Called with:', formatted: '\'bar\''}],
733733
});
734734

735+
passes(t, () => {
736+
const specimen = {[Symbol.toStringTag]: 'Custom', extra: true};
737+
const selector = Object.defineProperties(
738+
{[Symbol.toStringTag]: 'Custom'},
739+
{ignored: {value: true}},
740+
);
741+
return assertions.like(specimen, selector);
742+
});
743+
735744
failsWith(t, () => {
736745
const likePattern = {
737746
a: 'a',
@@ -767,8 +776,12 @@ test('.like()', t => {
767776

768777
passes(t, () => assertions.like([1, 2, 3], [1, 2, 3]));
769778
passes(t, () => assertions.like([1, 2, 3], [1, 2]));
779+
// eslint-disable-next-line no-sparse-arrays
780+
passes(t, () => assertions.like([1, 2, 3], [1, , 3]));
770781

771782
fails(t, () => assertions.like([1, 2, 3], [3, 2, 1]));
783+
// eslint-disable-next-line no-sparse-arrays
784+
fails(t, () => assertions.like([1, 2, 3], [1, , 4]));
772785
fails(t, () => assertions.like([1, 2], [1, 2, 3]));
773786

774787
t.end();

0 commit comments

Comments
 (0)