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
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/03-assertions.md
Original file line number Diff line number Diff line change
@@ -141,6 +141,14 @@ 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()`.

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

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 `Map`s or `Set`s/arrays.

The size of `actual` and `expected` must be equal. For `Map`s, each key-value pair in `actual` must be in `expected`, and vice-versa. For `Set`s/arrays, each value in `actual` must be in `expected`.

Returns a boolean indicating whether the assertion passed.

### `.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.
65 changes: 65 additions & 0 deletions lib/assert.js
Original file line number Diff line number Diff line change
@@ -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};
@@ -796,5 +797,69 @@ export class Assertions {

return pass();
});

this.unorderedEqual = withSkip((actual, expected, message) => {
assertMessage(message, 't.unorderedEqual()');

const actualInfo = checkValueForUnorderedEqual(actual);
const expectedInfo = checkValueForUnorderedEqual(expected);

if (!actualInfo.isValid || !expectedInfo.isValid) {
throw fail(new AssertionError('`t.unorderedEqual()` only compares arrays, maps, and sets', {
assertion: 't.unorderedEqual()',
improperUsage: true,
values: [
!actualInfo.isValid && formatWithLabel('Called with:', actual),
!expectedInfo.isValid && formatWithLabel('Called with:', expected),
].filter(Boolean),
}));
}

if (
actualInfo.type !== expectedInfo.type
&& (actualInfo.type === 'map' || expectedInfo.type === 'map')
) {
throw fail(new AssertionError('types of actual and expected must be comparable', {
assertion: 't.unorderedEqual()',
improperUsage: true,
}));
}

if (actualInfo.size !== expectedInfo.size) {
throw fail(new AssertionError('size must be equal', {
assertion: 't.unorderedEqual()',
}));
}

if (actualInfo.type === 'map') {
// Keys are unique - if actual and expected are the same size,
// and expected has a value for every key in actual, then the two are equal.

for (const [key, value] of actual.entries()) {
const result = concordance.compare(value, expected.get(key), concordanceOptions);
if (!result.pass) {
// TODO: allow passing custom messages
throw fail(new AssertionError('all values must be equal - map', {
assertion: 't.unorderedEqual()',
}));
}
}

return pass();
}

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)) {
throw fail(new AssertionError('all values must be equal - array/set', {
assertion: 't.unorderedEqual()',
}));
}
}

return pass();
});
}
}
23 changes: 23 additions & 0 deletions lib/unordered-equal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const checkValueForUnorderedEqual = value => {
let type = 'invalid';

if (value instanceof Map) {
type = 'map';
} else if (value instanceof Set) {
type = 'set';
} else if (Array.isArray(value)) {
type = 'array';
}

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
Original file line number Diff line number Diff line change
@@ -1822,3 +1822,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
Original file line number Diff line number Diff line change
@@ -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);
});
});

@@ -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);
});
});

@@ -464,6 +466,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);
}),
1 change: 1 addition & 0 deletions test/assertions/fixtures/happy-path.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Binary file modified test/assertions/snapshots/test.js.snap
Binary file not shown.
42 changes: 42 additions & 0 deletions types/assertions.d.cts
Original file line number Diff line number Diff line change
@@ -141,6 +141,9 @@ export type Assertions = {
* Note: An `else` clause using this as a type guard will be subtly incorrect for `string` and `number` types and will not give `0` or `''` as a potential value in an `else` clause.
*/
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;
};

type FalsyValue = false | 0 | 0n | '' | null | undefined; // eslint-disable-line @typescript-eslint/ban-types
@@ -400,3 +403,42 @@ 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`. This is a variant of `.deepEqual()` that does not depend
* on the order of `actual`/`expected` and only compares instances of `Map`s or `Set`s/arrays.
*
* The size of `actual` and `expected` must be equal. For `Map`s, each key-value pair in `actual` must be in
* `expected`, and vice-versa. For `Set`s/arrays, each value in `actual` must be in `expected`.
*
* Returns `true` if the assertion passed and throws otherwise.
*/
<Actual, Expected extends Actual>(actual: Actual, expected: Expected, message?: string): actual is Expected;

/**
* 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 `Map`s or `Set`s/arrays.
*
* The size of `actual` and `expected` must be equal. For `Map`s, each key-value pair in `actual` must be in
* `expected`, and vice-versa. For `Set`s/arrays, each value in `actual` must be in `expected`.
*
* Returns `true` if the assertion passed and throws otherwise.
*/
<Actual extends Expected, Expected>(actual: Actual, expected: Expected, message?: string): expected is Actual;

/**
* 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 `Map`s or `Set`s/arrays.
*
* The size of `actual` and `expected` must be equal. For `Map`s, each key-value pair in `actual` must be in
* `expected`, and vice-versa. For `Set`s/arrays, each value in `actual` must be in `expected`.
*
* Returns `true` if the assertion passed and throws otherwise.
*/
<Actual, Expected>(actual: Actual, expected: Expected, message?: string): true;

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