Repository: unexpectedjs/unexpected
Pull request #555 (2019-01-01 16:01:55)

expect.it: Forward flags to assertions further down the parameter list
https://github.com/unexpectedjs/unexpected-knex/pull/28#discussion_r244633170

Not too sure about the implementation. Maybe this is something that belongs in `expect.context`?

papandreou: Talked about this on gitter and found that it doesn't work for regular `expect` either: https://gitter.im/unexpectedjs/unexpected?at=5c2e7de9babbc178b2220706
alexjeffburke: The tests make it clear this keeps the two cases consistent and they look exhaustive to my eye. 👍 
papandreou: The full version of this would be easier to implement on top of https://github.com/unexpectedjs/unexpected/pull/565

Patch:
diff --git a/lib/createTopLevelExpect.js b/lib/createTopLevelExpect.js
index 3c7e819c..99fa507b 100644
--- a/lib/createTopLevelExpect.js
+++ b/lib/createTopLevelExpect.js
@@ -84,7 +84,7 @@ function getOrGroups(expectations) {
   return orGroups;
 }
 
-function evaluateGroup(unexpected, context, subject, orGroup) {
+function evaluateGroup(unexpected, context, subject, orGroup, forwardedFlags) {
   return orGroup.map(expectation => {
     const args = Array.prototype.slice.call(expectation);
     args.unshift(subject);
@@ -101,7 +101,7 @@ function evaluateGroup(unexpected, context, subject, orGroup) {
             return args[1](args[0]);
           }
         } else {
-          return unexpected._expect(context.child(), args);
+          return unexpected._expect(context.child(), args, forwardedFlags);
         }
       })
     };
@@ -171,7 +171,7 @@ function writeGroupEvaluationsToOutput(output, groupEvaluations) {
   });
 }
 
-function createExpectIt(expect, expectations) {
+function createExpectIt(expect, expectations, forwardedFlags) {
   const orGroups = getOrGroups(expectations);
 
   function expectIt(subject, context) {
@@ -193,7 +193,13 @@ function createExpectIt(expect, expectations) {
     const groupEvaluations = [];
     const promises = [];
     orGroups.forEach(orGroup => {
-      const evaluations = evaluateGroup(expect, context, subject, orGroup);
+      const evaluations = evaluateGroup(
+        expect,
+        context,
+        subject,
+        orGroup,
+        forwardedFlags
+      );
       evaluations.forEach(({ promise }) => {
         promises.push(promise);
       });
@@ -231,12 +237,12 @@ function createExpectIt(expect, expectations) {
   expectIt.and = function(...args) {
     const copiedExpectations = expectations.slice();
     copiedExpectations.push(args);
-    return createExpectIt(expect, copiedExpectations);
+    return createExpectIt(expect, copiedExpectations, forwardedFlags);
   };
   expectIt.or = function(...args) {
     const copiedExpectations = expectations.slice();
     copiedExpectations.push(OR, args);
-    return createExpectIt(expect, copiedExpectations);
+    return createExpectIt(expect, copiedExpectations, forwardedFlags);
   };
   return expectIt;
 }
@@ -249,10 +255,7 @@ const expectPrototype = {
 utils.setPrototypeOfOrExtend(expectPrototype, Function.prototype);
 
 expectPrototype.it = function(...args) {
-  if (this.flags && typeof args[0] === 'string') {
-    args[0] = utils.forwardFlags(args[0], this.flags);
-  }
-  return createExpectIt(this._topLevelExpect, [args]);
+  return createExpectIt(this._topLevelExpect, [args], this.flags);
 };
 
 expectPrototype.equal = function(actual, expected, depth, seen) {
@@ -1308,9 +1311,10 @@ expectPrototype._createWrappedExpect = function(
   subject,
   args,
   testDescriptionString,
-  context
+  context,
+  forwardedFlags
 ) {
-  const flags = extend({}, assertionRule.flags);
+  const flags = extend({}, forwardedFlags, assertionRule.flags);
   const parentExpect = this;
 
   function wrappedExpect(subject, testDescriptionString) {
@@ -1342,7 +1346,8 @@ expectPrototype._createWrappedExpect = function(
         context.child(),
         subject,
         testDescriptionString,
-        args
+        args,
+        wrappedExpect.flags
       )
     );
   }
@@ -1368,7 +1373,7 @@ expectPrototype._createWrappedExpect = function(
       ((argRule && argRule.type.is('assertion')) ||
         wrappedExpect._getAssertionIndices().indexOf(i) >= 0)
     ) {
-      return new AssertionString(arg);
+      return new AssertionString(utils.forwardFlags(arg, flags));
     }
 
     return output => {
@@ -1383,8 +1388,15 @@ expectPrototype._executeExpect = function(
   context,
   subject,
   testDescriptionString,
-  args
+  args,
+  forwardedFlags
 ) {
+  if (forwardedFlags) {
+    testDescriptionString = utils.forwardFlags(
+      testDescriptionString,
+      forwardedFlags
+    );
+  }
   let assertionRule = this.lookupAssertionRule(
     subject,
     testDescriptionString,
@@ -1438,13 +1450,14 @@ expectPrototype._executeExpect = function(
     subject,
     args,
     testDescriptionString,
-    context
+    context,
+    forwardedFlags
   );
 
   return oathbreaker(assertionRule.handler(wrappedExpect, subject, ...args));
 };
 
-expectPrototype._expect = function expect(context, args) {
+expectPrototype._expect = function expect(context, args, forwardedFlags) {
   const subject = args[0];
   const testDescriptionString = args[1];
 
@@ -1464,7 +1477,8 @@ expectPrototype._expect = function expect(context, args) {
       context,
       subject,
       testDescriptionString,
-      Array.prototype.slice.call(args, 2)
+      Array.prototype.slice.call(args, 2),
+      forwardedFlags
     );
     if (utils.isPromise(result)) {
       result = wrapPromiseIfNecessary(result);


Test patch:
diff --git a/test/api/expect.spec.js b/test/api/expect.spec.js
new file mode 100644
index 00000000..eb2b5020
--- /dev/null
+++ b/test/api/expect.spec.js
@@ -0,0 +1,44 @@
+/* global expect */
+describe('expect', function() {
+  it('should forward flags to assertion strings that are processed by the next assertion', function() {
+    var clonedExpect = expect.clone();
+
+    clonedExpect.addAssertion(
+      '<number> [not] to have an absolute value of <number>',
+      function(expect, subject, value) {
+        expect.errorMode = 'nested';
+        expect(
+          subject,
+          'when passed as parameter to',
+          Math.abs,
+          '[not] to equal',
+          value
+        );
+      }
+    );
+
+    clonedExpect(-124, 'not to have an absolute value of', 0);
+  });
+
+  it('should forward flags to assertion strings that are processed by the next next assertion', function() {
+    var clonedExpect = expect.clone();
+
+    clonedExpect.addAssertion(
+      '<number> [not] to have a floored absolute value of <number>',
+      function(expect, subject, value) {
+        expect.errorMode = 'nested';
+        expect(
+          subject,
+          'when passed as parameter to',
+          Math.abs,
+          'when passed as parameter to',
+          Math.floor,
+          '[not] to equal',
+          value
+        );
+      }
+    );
+
+    clonedExpect(-124, 'not to have a floored absolute value of', 0);
+  });
+});
diff --git a/test/api/it.spec.js b/test/api/it.spec.js
index 351ee7ed..b0abfba0 100644
--- a/test/api/it.spec.js
+++ b/test/api/it.spec.js
@@ -314,55 +314,112 @@ describe('expect.it', () => {
   });
 
   describe('with forwarding of flags', () => {
-    var clonedExpect = expect
-      .clone()
-      .addAssertion('<object> [not] to have a foo property of bar', function(
-        expect,
-        subject
-      ) {
-        return expect(subject, 'to satisfy', {
-          foo: expect.it('[not] to equal', 'bar')
+    describe('directly', () => {
+      var clonedExpect = expect
+        .clone()
+        .addAssertion('<object> [not] to have a foo property of bar', function(
+          expect,
+          subject
+        ) {
+          return expect(subject, 'to satisfy', {
+            foo: expect.it('[not] to equal', 'bar')
+          });
         });
-      });
 
-    describe('when the flag is not being forwarded', () => {
-      it('should succeed', () => {
-        clonedExpect({ foo: 'bar' }, 'to have a foo property of bar');
+      describe('when the flag is not being forwarded', () => {
+        it('should succeed', () => {
+          clonedExpect({ foo: 'bar' }, 'to have a foo property of bar');
+        });
+
+        it('should fail with a diff', () => {
+          return expect(
+            function() {
+              clonedExpect({ quux: 123 }, 'to have a foo property of bar');
+            },
+            'to throw',
+            'expected { quux: 123 } to have a foo property of bar\n' +
+              '\n' +
+              '{\n' +
+              '  quux: 123\n' +
+              "  // missing: foo: should equal 'bar'\n" +
+              '}'
+          );
+        });
       });
 
-      it('should fail with a diff', () => {
-        return expect(
-          function() {
-            clonedExpect({ quux: 123 }, 'to have a foo property of bar');
-          },
-          'to throw',
-          'expected { quux: 123 } to have a foo property of bar\n' +
-            '\n' +
-            '{\n' +
-            '  quux: 123\n' +
-            "  // missing: foo: should equal 'bar'\n" +
-            '}'
-        );
+      describe('when the flag is being forwarded', () => {
+        it('should succeed', () => {
+          clonedExpect({ quux: 123 }, 'not to have a foo property of bar');
+        });
+
+        it('should fail with a diff', () => {
+          return expect(
+            function() {
+              clonedExpect({ foo: 'bar' }, 'not to have a foo property of bar');
+            },
+            'to throw',
+            "expected { foo: 'bar' } not to have a foo property of bar\n" +
+              '\n' +
+              '{\n' +
+              "  foo: 'bar' // should not equal 'bar'\n" +
+              '}'
+          );
+        });
       });
     });
 
-    describe('when the flag is being forwarded', () => {
-      it('should succeed', () => {
-        clonedExpect({ quux: 123 }, 'not to have a foo property of bar');
+    describe('through an <assertion> being shifted to', () => {
+      var clonedExpect = expect
+        .clone()
+        .addAssertion('<object> [not] to have a foo property of bar', function(
+          expect,
+          subject
+        ) {
+          return expect(subject, 'to satisfy', {
+            foo: expect.it('noop', '[not] to equal', 'bar')
+          });
+        })
+        .addAssertion('<any> noop <assertion>', expect => expect.shift());
+
+      describe('when the flag is not being forwarded', () => {
+        it('should succeed', () => {
+          clonedExpect({ foo: 'bar' }, 'to have a foo property of bar');
+        });
+
+        it('should fail with a diff', () => {
+          return expect(
+            function() {
+              clonedExpect({ quux: 123 }, 'to have a foo property of bar');
+            },
+            'to throw',
+            'expected { quux: 123 } to have a foo property of bar\n' +
+              '\n' +
+              '{\n' +
+              '  quux: 123\n' +
+              "  // missing: foo: expected: noop to equal 'bar'\n" +
+              '}'
+          );
+        });
       });
 
-      it('should fail with a diff', () => {
-        return expect(
-          function() {
-            clonedExpect({ foo: 'bar' }, 'not to have a foo property of bar');
-          },
-          'to throw',
-          "expected { foo: 'bar' } not to have a foo property of bar\n" +
-            '\n' +
-            '{\n' +
-            "  foo: 'bar' // should not equal 'bar'\n" +
-            '}'
-        );
+      describe('when the flag is being forwarded', () => {
+        it('should succeed', () => {
+          clonedExpect({ quux: 123 }, 'not to have a foo property of bar');
+        });
+
+        it('should fail with a diff', () => {
+          return expect(
+            function() {
+              clonedExpect({ foo: 'bar' }, 'not to have a foo property of bar');
+            },
+            'to throw',
+            "expected { foo: 'bar' } not to have a foo property of bar\n" +
+              '\n' +
+              '{\n' +
+              "  foo: 'bar' // expected: noop not to equal 'bar'\n" +
+              '}'
+          );
+        });
       });
     });
   });