Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: avajs/ava
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v5.3.0
Choose a base ref
...
head repository: avajs/ava
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v5.3.1
Choose a head ref
  • 3 commits
  • 5 files changed
  • 2 contributors

Commits on Jun 16, 2023

  1. Support Symbol keys and ignore non-enumerable properties in t.like()

    Fixes #3208
    
    Co-authored-by: Mark Wubben <mark@novemberborn.net>
    gibson042 and novemberborn authored Jun 16, 2023
    Copy the full SHA
    c988e27 View commit details
  2. Fix circular selector detection in t.like()

    The previous implementation tracked each object, even if not circular. Update to use a stack.
    
    Fixes #3205.
    novemberborn authored Jun 16, 2023
    Copy the full SHA
    6398772 View commit details
  3. 5.3.1

    novemberborn committed Jun 16, 2023
    Copy the full SHA
    306e37c View commit details
Showing with 50 additions and 23 deletions.
  1. +2 −2 docs/03-assertions.md
  2. +26 −17 lib/like-selector.js
  3. +2 −2 package-lock.json
  4. +1 −1 package.json
  5. +19 −1 test-tap/assert.js
4 changes: 2 additions & 2 deletions docs/03-assertions.md
Original file line number Diff line number Diff line change
@@ -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`.

@@ -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.
43 changes: 26 additions & 17 deletions lib/like-selector.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
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()) {
if (circular.has(selector)) {
throw CIRCULAR_SELECTOR;
}

circular.add(selector);

if (lhs === null || typeof lhs !== 'object') {
return lhs;
export function selectComparable(actual, selector, circular = [selector]) {
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);
if (isLikeSelector(subselector)) {
if (circular.includes(subselector)) {
throw CIRCULAR_SELECTOR;
}

circular.push(subselector);
comparable[key] = selectComparable(Reflect.get(actual, key), subselector, circular);
circular.pop();
} else {
comparable[key] = Reflect.get(actual, key);
}
}

return comparable;
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ava",
"version": "5.3.0",
"version": "5.3.1",
"description": "Node.js test runner that lets you develop with confidence.",
"license": "MIT",
"repository": "avajs/ava",
20 changes: 19 additions & 1 deletion test-tap/assert.js
Original file line number Diff line number Diff line change
@@ -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: '{}'}],
@@ -732,6 +732,20 @@ 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);
});

passes(t, () => {
const array = ['c1', 'c2'];
return assertions.like({a: 'a', b: 'b', c: ['c1', 'c2'], d: ['c1', 'c2']}, {b: 'b', d: array, c: array});
});

failsWith(t, () => {
const likePattern = {
a: 'a',
@@ -767,8 +781,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();