From c45e837dc9781754816f0fe659d64af195c9aa90 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Mon, 13 Mar 2023 16:38:12 -0500 Subject: [PATCH 1/7] tests(`t.like`): add case from #2627 --- test-tap/assert.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test-tap/assert.js b/test-tap/assert.js index 48ab73d2b..674cb0eb1 100644 --- a/test-tap/assert.js +++ b/test-tap/assert.js @@ -761,6 +761,8 @@ test('.like()', t => { values: [{label: 'Difference (- actual, + expected):', formatted: /{\n-\s*a: 'foo',\n\+\s*a: 'bar',\n\s*}/}], }); + passes(t, () => assertions.like({a: [{a: 1, b: 2}]}, {a: [{a: 1}]})); + t.end(); }); From c9a7a218980eb4ff8b3f6dae570a59b99049a468 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Mon, 13 Mar 2023 16:41:56 -0500 Subject: [PATCH 2/7] chore: merge changes from #3023 --- lib/like-selector.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/like-selector.js b/lib/like-selector.js index 09675a9fe..ea8068977 100644 --- a/lib/like-selector.js +++ b/lib/like-selector.js @@ -1,7 +1,8 @@ export function isLikeSelector(selector) { + const prototype = Reflect.getPrototypeOf(selector); return selector !== null && typeof selector === 'object' - && Reflect.getPrototypeOf(selector) === Object.prototype + && (prototype === Object.prototype || prototype === Array.prototype) && Reflect.ownKeys(selector).length > 0; } @@ -18,7 +19,7 @@ export function selectComparable(lhs, selector, circular = new Set()) { return lhs; } - const comparable = {}; + const comparable = Array.isArray(selector) ? [] : {}; for (const [key, rhs] of Object.entries(selector)) { if (isLikeSelector(rhs)) { comparable[key] = selectComparable(Reflect.get(lhs, key), rhs, circular); From a50b1c8f22612c6bfe545c1dfcd4ba030f27ec37 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Mon, 13 Mar 2023 16:54:07 -0500 Subject: [PATCH 3/7] feat: update `isLikeSelector` for `Reflect.ownKeys().length` --- lib/like-selector.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/like-selector.js b/lib/like-selector.js index ea8068977..ca27aaa95 100644 --- a/lib/like-selector.js +++ b/lib/like-selector.js @@ -1,9 +1,14 @@ export function isLikeSelector(selector) { - const prototype = Reflect.getPrototypeOf(selector); - return selector !== null - && typeof selector === 'object' - && (prototype === Object.prototype || prototype === Array.prototype) - && Reflect.ownKeys(selector).length > 0; + const isArrayOrObject = selector !== null && typeof selector === 'object'; + + if (!isArrayOrObject) { + return false; + } + + const isArray = Array.isArray(selector); + const minimumKeysRequired = isArray ? 1 : 0; + + return Reflect.ownKeys(selector).length > minimumKeysRequired; } export const CIRCULAR_SELECTOR = new Error('Encountered a circular selector'); From 162ecdb599db61a496bbc995e194094520ae5527 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Tue, 11 Apr 2023 20:49:28 -0500 Subject: [PATCH 4/7] refactor: simpler `isLikeSelector` solution --- lib/like-selector.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/like-selector.js b/lib/like-selector.js index ca27aaa95..5ccd77bbc 100644 --- a/lib/like-selector.js +++ b/lib/like-selector.js @@ -1,14 +1,12 @@ -export function isLikeSelector(selector) { - const isArrayOrObject = selector !== null && typeof selector === 'object'; +const isObject = selector => Reflect.getPrototypeOf(selector) === Object.prototype; - if (!isArrayOrObject) { +export function isLikeSelector(selector) { + if (selector === null || typeof selector !== 'object') { return false; } - const isArray = Array.isArray(selector); - const minimumKeysRequired = isArray ? 1 : 0; - - return Reflect.ownKeys(selector).length > minimumKeysRequired; + const keyCount = Reflect.ownKeys(selector).length; + return (Array.isArray(selector) && keyCount > 1) || (isObject(selector) && keyCount > 0); } export const CIRCULAR_SELECTOR = new Error('Encountered a circular selector'); From 7627404b23f62738bb393b460c40789dcecdc619 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Tue, 11 Apr 2023 20:54:10 -0500 Subject: [PATCH 5/7] tests: add array cases --- test-tap/assert.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test-tap/assert.js b/test-tap/assert.js index 674cb0eb1..5048b3281 100644 --- a/test-tap/assert.js +++ b/test-tap/assert.js @@ -763,6 +763,10 @@ test('.like()', t => { passes(t, () => assertions.like({a: [{a: 1, b: 2}]}, {a: [{a: 1}]})); + passes(t, () => assertions.like([1, 2, 3], [1, 2, 3])); + + fails(t, () => assertions.like([1, 2, 3], [3, 2, 1])); + t.end(); }); From 13f44c1b512c99328c44b7c00bc2259685ec0981 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sun, 21 May 2023 16:11:36 +0200 Subject: [PATCH 6/7] Additional test cases --- test-tap/assert.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test-tap/assert.js b/test-tap/assert.js index 5048b3281..46d2deac6 100644 --- a/test-tap/assert.js +++ b/test-tap/assert.js @@ -762,10 +762,14 @@ test('.like()', t => { }); passes(t, () => assertions.like({a: [{a: 1, b: 2}]}, {a: [{a: 1}]})); + passes(t, () => assertions.like([{a: 1, b: 2}], [{a: 1}])); + passes(t, () => assertions.like([{a: 1, b: 2}, {c: 3}], [{a: 1}])); passes(t, () => assertions.like([1, 2, 3], [1, 2, 3])); + passes(t, () => assertions.like([1, 2, 3], [1, 2])); fails(t, () => assertions.like([1, 2, 3], [3, 2, 1])); + fails(t, () => assertions.like([1, 2], [1, 2, 3])); t.end(); }); From dc31ce31fa9ea39c0941527acfd335d43aaf1edd Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sun, 21 May 2023 16:15:50 +0200 Subject: [PATCH 7/7] Update documentation for t.like() --- docs/03-assertions.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/03-assertions.md b/docs/03-assertions.md index 933e9d2c9..02461b5a2 100644 --- a/docs/03-assertions.md +++ b/docs/03-assertions.md @@ -141,9 +141,9 @@ 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* object from `actual`, based on the deeply-nested properties of `selector`. This object is then compared to `selector` using `.deepEqual()`. +Instead AVA derives a *comparable* value from `actual`, recursively based on the shape of `selector`. This value is then compared to `selector` using `.deepEqual()`. -Any values in `selector` that are not regular objects should be deeply equal to the corresponding values in `actual`. +Any values in `selector` that are not arrays or regular objects should be deeply equal to the corresponding values in `actual`. In the following example, the `map` property of `actual` must be deeply equal to that of `selector`. However `nested.qux` is ignored, because it's not in `selector`. @@ -162,6 +162,12 @@ 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]) +``` + Finally, this returns a boolean indicating whether the assertion passed. ### `.throws(fn, expectation?, message?)` @@ -172,7 +178,7 @@ Assert that an error is thrown. `fn` must be a function which should throw. The * `instanceOf`: a constructor, the thrown error must be an instance of * `is`: the thrown error must be strictly equal to `expectation.is` -* `message`: the following types are valid: +* `message`: the following types are valid: * *string* - it is compared against the thrown error's message * *regular expression* - it is matched against this message * *function* - it is passed the thrown error message and must return a boolean for whether the assertion passed @@ -207,7 +213,7 @@ The thrown value *must* be an error. It is returned so you can run more assertio * `instanceOf`: a constructor, the thrown error must be an instance of * `is`: the thrown error must be strictly equal to `expectation.is` -* `message`: the following types are valid: +* `message`: the following types are valid: * *string* - it is compared against the thrown error's message * *regular expression* - it is matched against this message * *function* - it is passed the thrown error message and must return a boolean for whether the assertion passed