Repository: unexpectedjs/unexpected
Pull request #467 (2018-05-08 10:01:19)

add: negated assertions for 'to have an item satisfying' and 'to have…
… a value satisfying'

Closes https://github.com/unexpectedjs/unexpected/issues/430


Patch:
diff --git a/documentation/assertions/array-like/to-have-an-item-satisfying.md b/documentation/assertions/array-like/to-have-an-item-satisfying.md
index 14d4447b..f09e99d8 100644
--- a/documentation/assertions/array-like/to-have-an-item-satisfying.md
+++ b/documentation/assertions/array-like/to-have-an-item-satisfying.md
@@ -74,3 +74,27 @@ expected [ 0, -1, -2, -3, -4 ] to have an item satisfying
 expect.it('to be a number')
         .and('to be positive')
 ```
+
+This assertion can be negated using the `not` flag:
+
+```javascript
+expect(
+    [0, -1, -2, -3, -4],
+    'not to have an item satisfying',
+    expect.it('to be a number').and('to be negative')
+);
+```
+
+```output
+expected [ 0, -1, -2, -3, -4 ] not to have an item satisfying
+expect.it('to be a number')
+        .and('to be negative')
+
+[
+  0,
+  -1, // should be removed
+  -2, // should be removed
+  -3, // should be removed
+  -4 // should be removed
+]
+```
diff --git a/documentation/assertions/object/to-have-a-value-satisfying.md b/documentation/assertions/object/to-have-a-value-satisfying.md
index 3461b3be..ef0a570d 100644
--- a/documentation/assertions/object/to-have-a-value-satisfying.md
+++ b/documentation/assertions/object/to-have-a-value-satisfying.md
@@ -58,3 +58,17 @@ expected { foo: [ 10, 11, 12 ], bar: [ 14, 15, 16 ], baz: [ 17, 18, 19 ] } to ha
 to have items satisfying expect.it('to be a number')
         .and('to be below', 8)
 ```
+
+This assertion can be negated using the `not` flag:
+
+```javascript
+expect({ foo: { a: 1, b: 2 } }, 'not to have a value satisfying', { a: 1 });
+```
+
+```output
+expected { foo: { a: 1, b: 2 } } not to have a value satisfying { a: 1 }
+
+{
+  foo: { a: 1, b: 2 } // should be removed
+}
+```
diff --git a/lib/assertions.js b/lib/assertions.js
index 1e39de29..32cb2c1c 100644
--- a/lib/assertions.js
+++ b/lib/assertions.js
@@ -1140,8 +1140,8 @@ module.exports = expect => {
 
   expect.addAssertion(
     [
-      '<object> to have a value [exhaustively] satisfying <any>',
-      '<object> to have a value [exhaustively] satisfying <assertion>'
+      '<object> [not] to have a value [exhaustively] satisfying <any>',
+      '<object> [not] to have a value [exhaustively] satisfying <assertion>'
     ],
     (expect, subject, nextArg) => {
       expect.errorMode = 'nested';
@@ -1150,60 +1150,74 @@ module.exports = expect => {
 
       const subjectType = expect.findTypeOf(subject);
       const keys = subjectType.getKeys(subject);
-      return expect.promise
-        .any(
-          keys.map((key, index) => {
-            let expected;
-            if (typeof nextArg === 'string') {
-              expected = s => expect.shift(s);
-            } else if (typeof nextArg === 'function') {
-              expected = s => nextArg(s, index);
-            } else {
-              expected = nextArg;
-            }
-            return expect.promise(() =>
-              expect(
-                subjectType.valueForKey(subject, key),
-                'to [exhaustively] satisfy',
-                expected
-              )
-            );
-          })
-        )
-        .caught(err =>
-          expect.fail(output => {
-            output.append(
-              expect.standardErrorMessage(output.clone(), {
-                compact: err && err._isUnexpected && err.hasDiff()
+      const not = !!expect.flags.not;
+
+      const keyResults = new Array(keys.length);
+
+      expect.withError(
+        () =>
+          expect.promise[not ? 'all' : 'any'](
+            keys.map((key, index) => {
+              let expected;
+              if (typeof nextArg === 'string') {
+                expected = s => expect.shift(s);
+              } else if (typeof nextArg === 'function') {
+                expected = s => nextArg(s, index);
+              } else {
+                expected = nextArg;
+              }
+
+              keyResults[key] = expect.promise(() =>
+                expect(
+                  subjectType.valueForKey(subject, key),
+                  '[not] to [exhaustively] satisfy',
+                  expected
+                )
+              );
+              return keyResults[key];
+            })
+          ),
+        err => {
+          expect.fail({
+            message(output) {
+              output.append(
+                expect.standardErrorMessage(output.clone(), {
+                  compact: err && err._isUnexpected && err.hasDiff()
+                })
+              );
+            },
+            diff:
+              expect.flags.not &&
+              ((output, diff, inspect, equal) => {
+                const expectedObject = subjectType.is('array-like') ? [] : {};
+                keys.forEach(key => {
+                  if (keyResults[key].isFulfilled()) {
+                    expectedObject[key] = subjectType.valueForKey(subject, key);
+                  }
+                });
+
+                return diff(subject, expectedObject);
               })
-            );
-          })
-        );
+          });
+        }
+      );
     }
   );
 
   expect.addAssertion(
     [
-      '<array-like> to have an item [exhaustively] satisfying <any>',
-      '<array-like> to have an item [exhaustively] satisfying <assertion>'
+      '<array-like> [not] to have an item [exhaustively] satisfying <any>',
+      '<array-like> [not] to have an item [exhaustively] satisfying <assertion>'
     ],
     (expect, subject, ...rest) => {
       expect.errorMode = 'nested';
       expect(subject, 'not to be empty');
-      expect.errorMode = 'bubble';
+      expect.errorMode = 'default';
 
-      return expect.withError(
-        () =>
-          expect(subject, 'to have a value [exhaustively] satisfying', ...rest),
-        err => {
-          expect.fail(output => {
-            output.append(
-              expect.standardErrorMessage(output.clone(), {
-                compact: err && err._isUnexpected && err.hasDiff()
-              })
-            );
-          });
-        }
+      return expect(
+        subject,
+        '[not] to have a value [exhaustively] satisfying',
+        ...rest
       );
     }
   );


Test patch:
diff --git a/test/assertions/to-have-a-value-satisfying.spec.js b/test/assertions/to-have-a-value-satisfying.spec.js
index 8d39b165..0a5036b7 100644
--- a/test/assertions/to-have-a-value-satisfying.spec.js
+++ b/test/assertions/to-have-a-value-satisfying.spec.js
@@ -10,8 +10,8 @@ describe('to have a value satisfying assertion', function() {
         '  The assertion does not have a matching signature for:\n' +
         '    <array> to have a value satisfying\n' +
         '  did you mean:\n' +
-        '    <object> to have a value [exhaustively] satisfying <any>\n' +
-        '    <object> to have a value [exhaustively] satisfying <assertion>'
+        '    <object> [not] to have a value [exhaustively] satisfying <any>\n' +
+        '    <object> [not] to have a value [exhaustively] satisfying <assertion>'
     );
   });
 
@@ -37,8 +37,8 @@ describe('to have a value satisfying assertion', function() {
         '  The assertion does not have a matching signature for:\n' +
         '    <number> to have a value satisfying <function>\n' +
         '  did you mean:\n' +
-        '    <object> to have a value [exhaustively] satisfying <any>\n' +
-        '    <object> to have a value [exhaustively] satisfying <assertion>'
+        '    <object> [not] to have a value [exhaustively] satisfying <any>\n' +
+        '    <object> [not] to have a value [exhaustively] satisfying <assertion>'
     );
   });
 
@@ -89,6 +89,33 @@ describe('to have a value satisfying assertion', function() {
     );
   });
 
+  it('asserts that no values in the object satisfies the RHS expectation', function() {
+    expect(
+      { foo: 0, bar: 1, baz: 2, qux: 3 },
+      'not to have a value satisfying',
+      'to be a string'
+    );
+
+    expect({ foo: '0', bar: '1' }, 'not to have a value satisfying', function(
+      value
+    ) {
+      expect(value, 'to be a number');
+    });
+
+    expect(
+      { foo: 0, bar: 1 },
+      'not to have a value satisfying',
+      'not to be a number'
+    );
+
+    expect(
+      { foo: { foo: '0' }, bar: { bar: '1' } },
+      'not to have a value satisfying',
+      'to have a value satisfying',
+      'to be a number'
+    );
+  });
+
   it("throws the correct error if none of the subject's values match the RHS expectation", function() {
     expect(
       function() {
@@ -104,6 +131,25 @@ describe('to have a value satisfying assertion', function() {
     );
   });
 
+  it("throws the correct error, when negated, if any of the subject's values match the RHS expectation", function() {
+    expect(
+      function() {
+        expect(
+          { foo: 'foo', bar: 1 },
+          'not to have a value satisfying',
+          expect.it('to be a number')
+        );
+      },
+      'to throw',
+      "expected { foo: 'foo', bar: 1 }\n" +
+        "not to have a value satisfying expect.it('to be a number')\n\n" +
+        '{\n' +
+        "  foo: 'foo',\n" +
+        '  bar: 1 // should be removed\n' +
+        '}'
+    );
+  });
+
   describe('delegating to an async assertion', function() {
     var clonedExpect = expect
       .clone()
diff --git a/test/assertions/to-have-an-item-satisfying.spec.js b/test/assertions/to-have-an-item-satisfying.spec.js
index 17d13fcd..5fd2fbc1 100644
--- a/test/assertions/to-have-an-item-satisfying.spec.js
+++ b/test/assertions/to-have-an-item-satisfying.spec.js
@@ -10,8 +10,8 @@ describe('to have an item satisfying assertion', function() {
         '  The assertion does not have a matching signature for:\n' +
         '    <array> to have an item satisfying\n' +
         '  did you mean:\n' +
-        '    <array-like> to have an item [exhaustively] satisfying <any>\n' +
-        '    <array-like> to have an item [exhaustively] satisfying <assertion>'
+        '    <array-like> [not] to have an item [exhaustively] satisfying <any>\n' +
+        '    <array-like> [not] to have an item [exhaustively] satisfying <assertion>'
     );
   });
 
@@ -25,8 +25,8 @@ describe('to have an item satisfying assertion', function() {
         '  The assertion does not have a matching signature for:\n' +
         '    <number> to have an item satisfying <string>\n' +
         '  did you mean:\n' +
-        '    <array-like> to have an item [exhaustively] satisfying <any>\n' +
-        '    <array-like> to have an item [exhaustively] satisfying <assertion>'
+        '    <array-like> [not] to have an item [exhaustively] satisfying <any>\n' +
+        '    <array-like> [not] to have an item [exhaustively] satisfying <assertion>'
     );
   });
 
@@ -62,6 +62,27 @@ describe('to have an item satisfying assertion', function() {
     );
   });
 
+  it('asserts that no items in the array satisfies the RHS expectation', function() {
+    expect(['foo', 'bar'], 'not to have an item satisfying', 'to be a number');
+
+    expect(['foo', 'bar'], 'not to have an item satisfying', function(item) {
+      expect(item, 'to be a number');
+    });
+
+    expect(
+      [0, 1, 42, 2],
+      'not to have an item satisfying',
+      'not to be a number'
+    );
+
+    expect(
+      [['bar'], ['bar']],
+      'not to have an item satisfying',
+      'to have an item satisfying',
+      'to be a number'
+    );
+  });
+
   it("throws the correct error if none of the subject's values match the RHS expectation", function() {
     expect(
       function() {
@@ -76,6 +97,24 @@ describe('to have an item satisfying assertion', function() {
     );
   });
 
+  it("throws the correct error, when negated, if any of the subject's values match the RHS expectation", function() {
+    expect(
+      function() {
+        expect(
+          ['foo', 1],
+          'not to have an item satisfying',
+          expect.it('to be a number')
+        );
+      },
+      'to throw',
+      "expected [ 'foo', 1 ] not to have an item satisfying expect.it('to be a number')\n\n" +
+        '[\n' +
+        "  'foo',\n" +
+        '  1 // should be removed\n' +
+        ']'
+    );
+  });
+
   it('formats non-Unexpected errors correctly', function() {
     expect(
       function() {