Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add t.unorderedEqual() assertion #3234

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/03-assertions.md
Expand Up @@ -137,6 +137,10 @@ Assert that `actual` is deeply equal to `expected`. See [Concordance](https://gi

Assert that `actual` is not deeply equal to `expected`. The inverse of `.deepEqual()`. Returns a boolean indicating whether the assertion passed.

### `.unorderedEqual(actual, expected, message?)`

Assert that all values in `actual` are in `expected`, returning a boolean indicating whether the assertion passed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify the differences with deepEqual?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts?

Assert that all values in actual are in expected. This is a variant of .deepEqual() that does not depend
on the order of actual/expected and only compares instances of Maps or Sets/arrays.

The size of actual and expected must be equal. For Maps, each key-value pair in actual must be in
expected, and vice-versa. For Sets/arrays, each value in actual must be in expected.

Returns true if the assertion passed and throws otherwise.


### `.like(actual, selector, message?)`

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.
Expand Down
96 changes: 96 additions & 0 deletions lib/assert.js
Expand Up @@ -6,6 +6,7 @@ import isPromise from 'is-promise';
import concordanceOptions from './concordance-options.js';
import {CIRCULAR_SELECTOR, isLikeSelector, selectComparable} from './like-selector.js';
import {SnapshotError, VersionMismatchError} from './snapshot-manager.js';
import {checkValueForUnorderedEqual} from './unordered-equal.js';

function formatDescriptorDiff(actualDescriptor, expectedDescriptor, options) {
options = {...options, ...concordanceOptions};
Expand Down Expand Up @@ -958,5 +959,100 @@ export class Assertions {
pass();
return true;
});

this.unorderedEqual = withSkip((actual, expected, message) => {
if (!checkMessage('unorderedEqual', message)) {
return false;
}

const actualInfo = checkValueForUnorderedEqual(actual);

if (!actualInfo.isValid) {
fail(new AssertionError({
assertion: 'unorderedEqual',
improperUsage: true,
message: '`t.unorderedEqual` only compares Maps, Sets, and arrays',
tommy-mitchell marked this conversation as resolved.
Show resolved Hide resolved
values: [formatWithLabel('Called with:', actual)],
}));
return false;
}

const expectedInfo = checkValueForUnorderedEqual(expected);

if (!expectedInfo.isValid) {
fail(new AssertionError({
assertion: 'unorderedEqual',
improperUsage: true,
message: '`t.unorderedEqual` only compares Maps, Sets, and arrays',
tommy-mitchell marked this conversation as resolved.
Show resolved Hide resolved
values: [formatWithLabel('Called with:', expected)],
}));
return false;
}

if (actualInfo.size !== expectedInfo.size) {
fail(new AssertionError({
assertion: 'unorderedEqual',
message: 'size must be equal',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be the message argument. You can then do something like this:

ava/lib/assert.js

Lines 332 to 333 in a39dab0

raw: {actual, expected},
values: [formatDescriptorWithLabel('Values are deeply equal to each other, but they are not the same:', actualDescriptor)],

}));
return false;
}

if (actualInfo.type === 'map') {
if (expectedInfo.type !== 'map') {
fail(new AssertionError({
assertion: 'unorderedEqual',
message: 'both must be maps',
}));
return false;
}

const comparedKeysResult = concordance.compare(actual.keys, expected.keys, concordanceOptions);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean:

Suggested change
const comparedKeysResult = concordance.compare(actual.keys, expected.keys, concordanceOptions);
const comparedKeysResult = concordance.compare(actual.keys(), expected.keys(), concordanceOptions);

Of course that returns an iterator, which Concordance may walk over (can't recall), but the keys won't be in the same order.

If both values have the same size, then you probably don't need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agh - the joys of plain JavaScript.

If both values have the same size, then you probably don't need this?

Good catch, I think you're right as well.

if (!comparedKeysResult.pass) {
fail(new AssertionError({
assertion: 'unorderedEqual',
message: 'keys must be equal',
}));
return false;
}

for (const [key, value] of actual.entries()) {
const result = concordance.compare(value, expected.get(key), concordanceOptions);
if (!result.pass) {
fail(new AssertionError({
assertion: 'unorderedEqual',
message: 'all values must be equal - map',
}));
return false;
}
}

pass();
return true;
}

if (expectedInfo.type === 'map') {
fail(new AssertionError({
assertion: 'unorderedEqual',
message: 'both must be set-likes',
}));
return false;
}

const setActual = actualInfo.type === 'set' ? actual : new Set(actual);
const setExpected = expectedInfo.type === 'set' ? expected : new Set(expected);

for (const value of setActual) {
if (!setExpected.has(value)) {
fail(new AssertionError({
assertion: 'unorderedEqual',
message: 'all values must be equal - set',
}));
return false;
}
}

pass();
return true;
});
}
}
22 changes: 22 additions & 0 deletions lib/unordered-equal.js
@@ -0,0 +1,22 @@
export const checkValueForUnorderedEqual = value => {
/* eslint-disable indent, operator-linebreak, unicorn/no-nested-ternary */
const type = (
value instanceof Map ? 'map' :
value instanceof Set ? 'set' :
Array.isArray(value) ? 'array' :
'invalid'
);
/* eslint-enable indent, operator-linebreak, unicorn/no-nested-ternary */
tommy-mitchell marked this conversation as resolved.
Show resolved Hide resolved

if (type === 'invalid') {
return {isValid: false};
}

return {
isValid: true,
type,
size: type === 'array'
? value.length
: value.size,
};
};
95 changes: 95 additions & 0 deletions test-tap/assert.js
Expand Up @@ -1809,3 +1809,98 @@ test('.assert()', t => {

t.end();
});

test('.unorderedEqual()', t => {
passes(t, () => assertions.unorderedEqual([1, 2, 3], [2, 3, 1]));

passes(t, () => assertions.unorderedEqual(new Set([1, 2, 3]), new Set([2, 3, 1])));

passes(t, () => assertions.unorderedEqual([1, 2, 3], new Set([2, 3, 1])));

passes(t, () => assertions.unorderedEqual(new Set([1, 2, 3]), [2, 3, 1]));

passes(t, () => assertions.unorderedEqual(
new Map([['a', 1], ['b', 2], ['c', 3]]),
new Map([['b', 2], ['c', 3], ['a', 1]]),
));

// Types must match

fails(t, () => assertions.unorderedEqual('foo', [1, 2, 3]));

fails(t, () => assertions.unorderedEqual([1, 2, 3], 'foo'));

// Sizes must match

fails(t, () => assertions.unorderedEqual([1, 2, 3], [1, 2, 3, 4]));

fails(t, () => assertions.unorderedEqual([1, 2, 3, 4], [1, 2, 3]));

fails(t, () => assertions.unorderedEqual(new Set([1, 2, 3]), new Set([1, 2, 3, 4])));

fails(t, () => assertions.unorderedEqual(new Set([1, 2, 3, 4]), new Set([1, 2, 3])));

fails(t, () => assertions.unorderedEqual(new Set([1, 2, 3]), [1, 2, 3, 4]));

fails(t, () => assertions.unorderedEqual(new Set([1, 2, 3, 4]), [1, 2, 3]));

fails(t, () => assertions.unorderedEqual([1, 2, 3], new Set([1, 2, 3, 4])));

fails(t, () => assertions.unorderedEqual([1, 2, 3, 4], new Set([1, 2, 3])));

fails(t, () => assertions.unorderedEqual(
new Map([['a', 1], ['b', 2], ['c', 3]]),
new Map([['a', 1], ['b', 2]])),
);

fails(t, () => assertions.unorderedEqual(
new Map([['a', 1], ['b', 2]]),
new Map([['a', 1], ['b', 2], ['c', 3]])),
);

// Keys must match - maps

fails(t, () => assertions.unorderedEqual(
new Map([['a', 1], ['b', 2], ['c', 3]]),
new Map([['a', 1], ['d', 2], ['c', 3]])),
);

fails(t, () => assertions.unorderedEqual(
new Map([['a', 1], ['d', 2], ['c', 3]]),
new Map([['a', 1], ['b', 2], ['c', 3]])),
);

// Values must match - maps

fails(t, () => assertions.unorderedEqual(
new Map([['a', 1], ['b', 2], ['c', 3]]),
new Map([['a', 1], ['b', 4], ['c', 3]])),
);

fails(t, () => assertions.unorderedEqual(
new Map([['a', 1], ['b', 4], ['c', 3]]),
new Map([['a', 1], ['b', 2], ['c', 3]])),
);

// Values must match - sets

fails(t, () => assertions.unorderedEqual([1, 2, 3], [1, 2, 4]));

fails(t, () => assertions.unorderedEqual([1, 2, 4], [1, 2, 3]));

fails(t, () => assertions.unorderedEqual(new Set([1, 2, 3]), new Set([1, 2, 4])));

fails(t, () => assertions.unorderedEqual(new Set([1, 2, 4]), new Set([1, 2, 3])));

fails(t, () => assertions.unorderedEqual(new Set([1, 2, 3]), [1, 2, 4]));

fails(t, () => assertions.unorderedEqual(new Set([1, 2, 4]), [1, 2, 3]));

fails(t, () => assertions.unorderedEqual([1, 2, 3], new Set([1, 2, 4])));

fails(t, () => assertions.unorderedEqual([1, 2, 4], new Set([1, 2, 3])));

// TODO: check error messages

t.end();
});
11 changes: 7 additions & 4 deletions test-tap/test.js
Expand Up @@ -270,11 +270,12 @@ test('skipped assertions count towards the plan', t => {
a.false.skip(false);
a.regex.skip('foo', /foo/);
a.notRegex.skip('bar', /foo/);
a.unorderedEqual.skip([1, 2, 3], [2, 3, 1]);
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.planCount, 16);
t.equal(instance.assertCount, 16);
t.equal(instance.planCount, 17);
t.equal(instance.assertCount, 17);
});
});

Expand All @@ -299,11 +300,12 @@ test('assertion.skip() is bound', t => {
(a.false.skip)(false);
(a.regex.skip)('foo', /foo/);
(a.notRegex.skip)('bar', /foo/);
(a.unorderedEqual.skip)([1, 2, 3], [2, 3, 1]);
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.planCount, 16);
t.equal(instance.assertCount, 16);
t.equal(instance.planCount, 17);
t.equal(instance.assertCount, 17);
});
});

Expand Down Expand Up @@ -488,6 +490,7 @@ test('assertions are bound', t =>
(a.false)(false);
(a.regex)('foo', /foo/);
(a.notRegex)('bar', /foo/);
(a.unorderedEquals)([1, 2, 3], [2, 3, 1]);
}).run().then(result => {
t.ok(result.passed);
}),
Expand Down
1 change: 1 addition & 0 deletions test/assertions/fixtures/happy-path.js
Expand Up @@ -43,3 +43,4 @@ test(passes, 'false', false);
test(passes, 'regex', 'foo', /foo/);
test(passes, 'notRegex', 'bar', /foo/);
test(passes, 'assert', 1);
test(passes, 'unorderedEqual', [1, 2, 3], [2, 3, 1]);
1 change: 1 addition & 0 deletions test/assertions/snapshots/test.js.md
Expand Up @@ -27,6 +27,7 @@ Generated by [AVA](https://avajs.dev).
't.throwsAsync() passes',
't.true(true) passes',
't.truthy(1) passes',
't.unorderedEqual([1,2,3], [2,3,1]) passes',
]

## throws requires native errors
Expand Down
Binary file modified test/assertions/snapshots/test.js.snap
Binary file not shown.
18 changes: 18 additions & 0 deletions types/assertions.d.cts
Expand Up @@ -123,6 +123,9 @@ export type Assertions = {
* indicating whether the assertion passed.
*/
truthy: TruthyAssertion;

/** Assert that all values in `actual` are in `expected`, returning a boolean indicating whether the assertion passed. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too let's clarify the differences with deepEqual.

unorderedEqual: UnorderedEqualAssertion;
};

export type AssertAssertion = {
Expand Down Expand Up @@ -342,3 +345,18 @@ export type TruthyAssertion = {
/** Skip this assertion. */
skip(actual: any, message?: string): void;
};

// TODO: limit to Map | Set | Array
export type UnorderedEqualAssertion = {
/** Assert that all values in `actual` are in `expected`, returning a boolean indicating whether the assertion passed. */
<Actual, Expected extends Actual>(actual: Actual, expected: Expected, message?: string): actual is Expected;

/** Assert that all values in `actual` are in `expected`, returning a boolean indicating whether the assertion passed. */
<Actual extends Expected, Expected>(actual: Actual, expected: Expected, message?: string): expected is Actual;

/** Assert that all values in `actual` are in `expected`, returning a boolean indicating whether the assertion passed. */
<Actual, Expected>(actual: Actual, expected: Expected, message?: string): boolean;

/** Skip this assertion. */
skip(actual: any, expected: any, message?: string): void;
};