Skip to content

Commit

Permalink
Support Symbol keys and ignore non-enumerable properties in t.like()
Browse files Browse the repository at this point in the history
Fixes #3208

Co-authored-by: Mark Wubben <mark@novemberborn.net>
  • Loading branch information
gibson042 and novemberborn committed Jun 16, 2023
1 parent 568fe40 commit c988e27
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 14 deletions.
4 changes: 2 additions & 2 deletions docs/03-assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ Assert that `actual` is not deeply equal to `expected`. The inverse of `.deepEqu

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.

Instead AVA derives a *comparable* value from `actual`, recursively based on the shape of `selector`. This value is then compared to `selector` using `.deepEqual()`.
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()`.

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

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

```js
t.like([1, 2, 3], [1, 2])
t.like([1, 2, 3, 4], [1, , 3])
```

Finally, this returns a boolean indicating whether the assertion passed.
Expand Down
29 changes: 18 additions & 11 deletions lib/like-selector.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
const isObject = selector => Reflect.getPrototypeOf(selector) === Object.prototype;
const isPrimitive = value => value === null || typeof value !== 'object';

export function isLikeSelector(selector) {
if (selector === null || typeof selector !== 'object') {
// Require selector to be an array or plain object.
if (
isPrimitive(selector)
|| (!Array.isArray(selector) && Reflect.getPrototypeOf(selector) !== Object.prototype)
) {
return false;
}

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

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

export function selectComparable(lhs, selector, circular = new Set()) {
export function selectComparable(actual, selector, circular = new Set()) {
if (circular.has(selector)) {
throw CIRCULAR_SELECTOR;
}

circular.add(selector);

if (lhs === null || typeof lhs !== 'object') {
return lhs;
if (isPrimitive(actual)) {
return actual;
}

const comparable = Array.isArray(selector) ? [] : {};
for (const [key, rhs] of Object.entries(selector)) {
comparable[key] = isLikeSelector(rhs)
? selectComparable(Reflect.get(lhs, key), rhs, circular)
: Reflect.get(lhs, key);
const enumerableKeys = Reflect.ownKeys(selector).filter(key => Reflect.getOwnPropertyDescriptor(selector, key).enumerable);
for (const key of enumerableKeys) {
const subselector = Reflect.get(selector, key);
comparable[key] = isLikeSelector(subselector)
? selectComparable(Reflect.get(actual, key), subselector, circular)
: Reflect.get(actual, key);
}

return comparable;
Expand Down
15 changes: 14 additions & 1 deletion test-tap/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ test('.like()', t => {
return assertions.like({xc: [circular, 'c']}, {xc: [circular, 'd']});
});

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

passes(t, () => {
const specimen = {[Symbol.toStringTag]: 'Custom', extra: true};
const selector = Object.defineProperties(
{[Symbol.toStringTag]: 'Custom'},
{ignored: {value: true}},
);
return assertions.like(specimen, selector);
});

failsWith(t, () => {
const likePattern = {
a: 'a',
Expand Down Expand Up @@ -767,8 +776,12 @@ test('.like()', t => {

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

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

t.end();
Expand Down

0 comments on commit c988e27

Please sign in to comment.