Skip to content

Commit

Permalink
start implementing t.unorderedEqual()
Browse files Browse the repository at this point in the history
  • Loading branch information
tommy-mitchell committed Aug 15, 2023
1 parent e58f466 commit 4fc75ca
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 4 deletions.
4 changes: 4 additions & 0 deletions docs/03-assertions.md
Original file line number Diff line number Diff line change
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.

### `.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
Original file line number Diff line number Diff line change
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',
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',
values: [formatWithLabel('Called with:', expected)],
}));
return false;
}

if (actualInfo.size !== expectedInfo.size) {
fail(new AssertionError({
assertion: 'unorderedEqual',
message: 'size must be equal',
}));
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);
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
Original file line number Diff line number Diff line change
@@ -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 */

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
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.
15 changes: 15 additions & 0 deletions types/assertions.d.cts
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,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;
};

0 comments on commit 4fc75ca

Please sign in to comment.