Repository: unexpectedjs/unexpected
Pull request #576 (2019-01-09 21:37:05)

function type: Support inspection of classes and generators
I wasn't aware that functions created using the `class` syntax would reflect that in the output of the `toString` method:

```
console.log((class Foo {}).toString()); // 'class Foo {}'
```

Let's inspect them like that instead of `function Foo( /*...*/ ) { /*...*/ }` as we do now :sweat_smile:


Patch:
diff --git a/documentation/assertions/function/to-throw.md b/documentation/assertions/function/to-throw.md
index 6faf2076..df5c4a50 100644
--- a/documentation/assertions/function/to-throw.md
+++ b/documentation/assertions/function/to-throw.md
@@ -1,46 +1,27 @@
 Asserts that the function throws an error when called.
 
 ```js
-function somethingThatThrows() {
+function willThrow() {
   throw new Error('The error message');
 }
-
-expect(function () {
-  somethingThatThrows();
-}, 'to throw');
+expect(willThrow, 'to throw');
+expect(willThrow, 'to throw error');
+expect(willThrow, 'to throw exception');
 ```
 
 In case of a failing expectation you get the following output:
 
 ```js
-function willNotThrow() {
-  // ...
-}
-
-expect(function () {
-  willNotThrow();
-}, 'to throw');
+expect(function willNotThrow() {}, 'to throw');
 ```
 
 ```output
-expected function () { willNotThrow(); } to throw
+expected function willNotThrow() {} to throw
   did not throw
 ```
 
-Used with arrow functions this assertion is very elegant, and there
-are a couple of functionally equaivalent aliases provided to allow
-test suites using it be more explicit.
-
-```js
-expect(() => somethingThatThrows(), 'to throw');
-
-// aliases
-expect(() => somethingThatThrows(), 'to throw error');
-expect(() => somethingThatThrows(), 'to throw exception');
-```
-
-If you provide a string as the second parameter, it will be used to
-assert the thrown error has that message.
+You can assert the error message is a given string if you provide a
+string as the second parameter.
 
 ```js
 expect(
diff --git a/lib/styles.js b/lib/styles.js
index e58c8e7c..ff3dfdcd 100644
--- a/lib/styles.js
+++ b/lib/styles.js
@@ -206,24 +206,19 @@ module.exports = expect => {
     this.alt({
       text() {
         content.split(/(\n)/).forEach(function(fragment) {
-          this.block(function() {
-            this.text(fragment.replace(/\n/g, '\\n'))
-              .nl()
-              .text(fragment.replace(/[\s\S]/g, '^'));
-          });
           if (fragment === '\n') {
             this.nl();
+          } else {
+            this.block(function() {
+              this.text(fragment)
+                .nl()
+                .text(fragment.replace(/[\s\S]/g, '^'));
+            });
           }
         }, this);
       },
       fallback() {
-        content.split(/(\n)/).forEach(fragment => {
-          if (fragment === '\n') {
-            this.diffRemovedSpecialChar('\\n').nl();
-          } else {
-            this.diffRemovedHighlight(fragment);
-          }
-        });
+        this.diffRemovedHighlight(content);
       }
     });
   });
diff --git a/lib/types.js b/lib/types.js
index c38355d4..9f473938 100644
--- a/lib/types.js
+++ b/lib/types.js
@@ -935,11 +935,13 @@ module.exports = function(expect) {
       let body;
       let bodyIndent;
       const matchSource = source.match(
-        /^\s*((?:async )?\s*(?:\S+\s*=>|\([^)]*\)\s*=>|function\s?\w*\s*\([^)]*\)))([\s\S]*)$/
+        /^\s*((?:async )?\s*(?:\S+\s*=>|\([^)]*\)\s*=>|class|function\s?(?:\*\s*)?\w*\s*\([^)]*\)))([\s\S]*)$/
       );
       if (matchSource) {
-        // Normalize so there's always space after "function":
-        preamble = matchSource[1].replace(/function(\S)/, 'function $1');
+        // Normalize so there's always space after "function" and never after "*" for generator functions:
+        preamble = matchSource[1]
+          .replace(/function(\S)/, 'function $1')
+          .replace(/\* /, '*');
         if (preamble === 'function ()' && name) {
           // fn.bind() doesn't seem to include the name in the .toString() output:
           preamble = `function ${name}()`;


Test patch:
diff --git a/test/assertions/to-begin-with.spec.js b/test/assertions/to-begin-with.spec.js
index 6c2b751c..dce3adca 100644
--- a/test/assertions/to-begin-with.spec.js
+++ b/test/assertions/to-begin-with.spec.js
@@ -158,10 +158,10 @@ describe('to begin with assertion', () => {
           'to throw',
           "expected 'f\\no\\nobarquuxfoo' not to begin with 'f\\no\\no'\n" +
             '\n' +
-            'f\\n\n' +
-            '^^\n' +
-            'o\\n\n' +
-            '^^\n' +
+            'f\n' +
+            '^\n' +
+            'o\n' +
+            '^\n' +
             'obarquuxfoo\n' +
             '^'
         );
diff --git a/test/assertions/to-contain.spec.js b/test/assertions/to-contain.spec.js
index 3d5eda19..e2d10002 100644
--- a/test/assertions/to-contain.spec.js
+++ b/test/assertions/to-contain.spec.js
@@ -125,10 +125,10 @@ describe('to contain assertion', () => {
             'to have message',
             "expected 'blahfoo\\nbar\\nquux' not to contain 'foo\\nbar\\nq'\n" +
               '\n' +
-              'blahfoo\\n\n' +
-              '    ^^^^\n' +
-              'bar\\n\n' +
-              '^^^^\n' +
+              'blahfoo\n' +
+              '    ^^^\n' +
+              'bar\n' +
+              '^^^\n' +
               'quux\n' +
               '^'
           );
diff --git a/test/assertions/to-end-with.spec.js b/test/assertions/to-end-with.spec.js
index 7863f3c6..be6e56de 100644
--- a/test/assertions/to-end-with.spec.js
+++ b/test/assertions/to-end-with.spec.js
@@ -157,10 +157,10 @@ describe('to end with assertion', () => {
         'to throw',
         "expected 'foobarquuxf\\no\\no' not to end with 'f\\no\\no'\n" +
           '\n' +
-          'foobarquuxf\\n\n' +
-          '          ^^\n' +
-          'o\\n\n' +
-          '^^\n' +
+          'foobarquuxf\n' +
+          '          ^\n' +
+          'o\n' +
+          '^\n' +
           'o\n' +
           '^'
       );
diff --git a/test/assertions/to-match.spec.js b/test/assertions/to-match.spec.js
index 28df755a..d467e552 100644
--- a/test/assertions/to-match.spec.js
+++ b/test/assertions/to-match.spec.js
@@ -85,25 +85,11 @@ describe('to match assertion', () => {
         'to throw',
         "expected 'barfo\\noquuxfoobaz' not to match /fo\\no/\n" +
           '\n' +
-          'barfo\\n\n' +
-          '   ^^^\n' +
+          'barfo\n' +
+          '   ^^\n' +
           'oquuxfoobaz\n' +
           '^'
       );
     });
-
-    it('highlights a newline at the end of the match', function() {
-      expect(
-        function() {
-          expect('foobar\n', 'not to match', /\s+/);
-        },
-        'to throw',
-        "expected 'foobar\\n' not to match /\\s+/\n" +
-          '\n' +
-          'foobar\\n\n' +
-          '      ^\n' +
-          '\n'
-      );
-    });
   });
 });
diff --git a/test/styles/removedHighlight.spec.js b/test/styles/removedHighlight.spec.js
deleted file mode 100644
index efe86525..00000000
--- a/test/styles/removedHighlight.spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/*global expectWithUnexpectedMagicPen*/
-describe('removedHighlight', () => {
-  const expect = expectWithUnexpectedMagicPen;
-
-  var text = 'foo\nbar';
-
-  describe('in text mode', () => {
-    it('escapes the newlines', () => {
-      var pen = expect.createOutput('text').removedHighlight(text);
-      expect(pen.toString(), 'to equal', 'foo\\n\n^^^^\nbar\n^^^');
-
-      expect(
-        pen,
-        'to equal',
-        expect
-          .createOutput('text')
-          .block(function() {
-            this.text('foo')
-              .nl()
-              .text('^^^');
-          })
-          .block(function() {
-            this.diffRemovedHighlight('\\n')
-              .nl()
-              .text('^');
-          })
-          .nl()
-          .block(function() {
-            this.text('bar')
-              .nl()
-              .text('^^^');
-          })
-      );
-    });
-  });
-
-  describe('in ansi mode', () => {
-    it('does not output leading + and -', () => {
-      expect(
-        expect.createOutput('ansi').removedHighlight(text),
-        'to equal',
-        expect
-          .createOutput('ansi')
-          .diffRemovedHighlight('foo')
-          .diffRemovedSpecialChar('\\n')
-          .nl()
-          .diffRemovedHighlight('bar')
-      );
-    });
-  });
-});
diff --git a/test/types/function-type.spec.js b/test/types/function-type.spec.js
index 7b30b2d5..78ebc8a2 100644
--- a/test/types/function-type.spec.js
+++ b/test/types/function-type.spec.js
@@ -159,194 +159,168 @@ describe('function type', () => {
     // jscs:enable
   });
 
-  // We can't complete this test if the runtime doesn't support arrow functions:
-  var singleParamArrowFunction;
-  try {
-    // eslint-disable-next-line no-new-func
-    singleParamArrowFunction = new Function('return a => a + 1;')();
-  } catch (e) {}
-
-  if (singleParamArrowFunction) {
-    it('should render a single param arrow function', () => {
-      expect(singleParamArrowFunction, 'to inspect as', 'a => a + 1');
-    });
-  }
+  expect.addAssertion(
+    '<string> if supported by the runtime <assertion>',
+    (expect, subject) => {
+      let value;
+      try {
+        // eslint-disable-next-line no-new-func
+        value = new Function(`return ${subject};`)();
+      } catch (err) {
+        // Not supported, don't do anything
+        return;
+      }
+      return expect.shift(value);
+    }
+  );
+
+  it('should render a single param arrow function', () => {
+    expect(
+      'a => a + 1',
+      'if supported by the runtime to inspect as',
+      'a => a + 1'
+    );
+  });
 
-  // We can't complete this test if the runtime doesn't support arrow functions:
-  var implicitReturnMultilineArrowFunction;
-  try {
-    // eslint-disable-next-line no-new-func
-    implicitReturnMultilineArrowFunction = new Function(
-      'return a => \n    a + 1;'
-    )();
-  } catch (e) {}
-
-  if (implicitReturnMultilineArrowFunction) {
-    it('should render an implicit return multiline arrow function', () => {
-      expect(
-        implicitReturnMultilineArrowFunction,
-        'to inspect as',
-        'a => \n    a + 1'
-      );
-    });
-  }
+  it('should render an implicit return multiline arrow function', () => {
+    expect(
+      'a => \n    a + 1',
+      'if supported by the runtime to inspect as',
+      'a => \n    a + 1'
+    );
+  });
 
-  var implicitReturnArrowFunction;
-  try {
-    // eslint-disable-next-line no-new-func
-    implicitReturnArrowFunction = new Function('return a =>\n  a')();
-  } catch (e) {}
+  it('should render an implicit return arrow function a single line break after the arrow', () => {
+    expect(
+      'a =>\n  a',
+      'if supported by the runtime to inspect as',
+      `a =>\n  a`
+    );
+  });
 
-  if (implicitReturnArrowFunction) {
-    it('should render an implicit return arrow function a single line break after the arrow', () => {
-      expect(implicitReturnArrowFunction, 'to inspect as', `a =>\n  a`);
-    });
-  }
+  it('should reindent an implicit return arrow function with a single line break after the arrow', () => {
+    expect(
+      'a =>\n    a',
+      'if supported by the runtime to inspect as',
+      `a =>\n  a`
+    );
+  });
 
-  var implicitReturnArrowFunctionWith4SpaceIndent;
-  try {
-    // eslint-disable-next-line no-new-func
-    implicitReturnArrowFunctionWith4SpaceIndent = new Function(
-      'return a =>\n    a'
-    )();
-  } catch (e) {}
+  it('should render an implicit return multiline arrow function with an evil alternation', () => {
+    expect(
+      'a => \n    a || {}',
+      'if supported by the runtime to inspect as',
+      'a => \n    a || {}'
+    );
+  });
 
-  if (implicitReturnArrowFunctionWith4SpaceIndent) {
-    it('should reindent an implicit return arrow function with a single line break after the arrow', () => {
-      expect(
-        implicitReturnArrowFunctionWith4SpaceIndent,
-        'to inspect as',
-        `a =>\n  a`
-      );
-    });
-  }
+  it('should reindent an implicit return multiline arrow function with 1 space indent', () => {
+    expect(
+      '() =>\n foo(\n  1\n )',
+      'if supported by the runtime to inspect as',
+      '() =>\n  foo(\n    1\n  )'
+    );
+  });
 
-  // We can't complete this test if the runtime doesn't support arrow functions:
-  var evilImplicitReturnMultilineArrowFunction;
-  try {
-    // eslint-disable-next-line no-new-func
-    evilImplicitReturnMultilineArrowFunction = new Function(
-      'return a => \n    a || {};'
-    )();
-  } catch (e) {}
-
-  if (evilImplicitReturnMultilineArrowFunction) {
-    it('should render an implicit return multiline arrow function with an evil alternation', () => {
-      expect(
-        evilImplicitReturnMultilineArrowFunction,
-        'to inspect as',
-        'a => \n    a || {}'
-      );
-    });
-  }
+  it('should reindent an implicit return multiline arrow function with 2 space indent', () => {
+    expect(
+      '() =>\n        foo(\n          1\n        )',
+      'if supported by the runtime to inspect as',
+      '() =>\n  foo(\n    1\n  )'
+    );
+  });
 
-  // We can't complete this test if the runtime doesn't support arrow functions:
-  var arrowFunctionWith1SpaceIndentAndLeadingNewline;
-  try {
-    // eslint-disable-next-line no-new-func
-    arrowFunctionWith1SpaceIndentAndLeadingNewline = new Function(
-      'return () =>\n foo(\n  1\n )'
-    )();
-  } catch (e) {}
-
-  if (arrowFunctionWith1SpaceIndentAndLeadingNewline) {
-    it('should reindent an implicit return multiline arrow function with 1 space indent', () => {
-      expect(
-        arrowFunctionWith1SpaceIndentAndLeadingNewline,
-        'to inspect as',
-        '() =>\n  foo(\n    1\n  )'
-      );
-    });
-  }
+  it('should reindent an implicit return multiline arrow function with 4 space indent', () => {
+    expect(
+      '() =>\n      foo(\n         1\n      )',
+      'if supported by the runtime to inspect as',
+      '() =>\n  foo(\n    1\n  )'
+    );
+  });
 
-  // We can't complete this test if the runtime doesn't support arrow functions:
-  var arrowFunctionWith2SpaceIndentAndLeadingNewline;
-  try {
-    // eslint-disable-next-line no-new-func
-    arrowFunctionWith2SpaceIndentAndLeadingNewline = new Function(
-      'return () =>\n        foo(\n          1\n        )'
-    )();
-  } catch (e) {}
-
-  if (arrowFunctionWith2SpaceIndentAndLeadingNewline) {
-    it('should reindent an implicit return multiline arrow function with 2 space indent', () => {
-      expect(
-        arrowFunctionWith2SpaceIndentAndLeadingNewline,
-        'to inspect as',
-        '() =>\n  foo(\n    1\n  )'
-      );
-    });
-  }
+  it('should reindent an implicit return multiline arrow function with long leading indent', () => {
+    expect(
+      '() =>\n        foo(\n            1\n        )',
+      'if supported by the runtime to inspect as',
+      '() =>\n  foo(\n    1\n  )'
+    );
+  });
 
-  // We can't complete this test if the runtime doesn't support arrow functions:
-  var arrowFunctionWith3SpaceIndentAndLeadingNewline;
-  try {
-    // eslint-disable-next-line no-new-func
-    arrowFunctionWith3SpaceIndentAndLeadingNewline = new Function(
-      'return () =>\n      foo(\n         1\n      )'
-    )();
-  } catch (e) {}
-
-  if (arrowFunctionWith3SpaceIndentAndLeadingNewline) {
-    it('should reindent an implicit return multiline arrow function with 4 space indent', () => {
-      expect(
-        arrowFunctionWith3SpaceIndentAndLeadingNewline,
-        'to inspect as',
-        '() =>\n  foo(\n    1\n  )'
-      );
-    });
-  }
+  it('should render a multi param arrow function', () => {
+    expect(
+      '(a, b) => a + b;',
+      'if supported by the runtime to inspect as',
+      '(a, b) => a + b'
+    );
+  });
 
-  // We can't complete this test if the runtime doesn't support arrow functions:
-  var arrowFunctionWith4SpaceIndentAndLeadingNewline;
-  try {
-    // eslint-disable-next-line no-new-func
-    arrowFunctionWith4SpaceIndentAndLeadingNewline = new Function(
-      'return () =>\n        foo(\n            1\n        )'
-    )();
-  } catch (e) {}
-
-  if (arrowFunctionWith4SpaceIndentAndLeadingNewline) {
-    it('should reindent an implicit return multiline arrow function with long leading indent', () => {
-      expect(
-        arrowFunctionWith4SpaceIndentAndLeadingNewline,
-        'to inspect as',
-        '() =>\n  foo(\n    1\n  )'
-      );
-    });
-  }
+  it('should render "async" before an AsyncFunction instance', () => {
+    expect(
+      'async function foo(a) { return a + 1; }',
+      'if supported by the runtime to inspect as',
+      'async function foo(a) { return a + 1; }'
+    );
+  });
 
-  // We can't complete this test if the runtime doesn't support arrow functions:
-  var multiParamArrowFunction;
-  try {
-    // eslint-disable-next-line no-new-func
-    multiParamArrowFunction = new Function('return (a, b) => a + b;')();
-  } catch (e) {}
+  it('should inspect an anonymous generator', () => {
+    expect(
+      'function*(){}',
+      'if supported by the runtime to inspect as',
+      'function *(){}'
+    );
+  });
 
-  if (multiParamArrowFunction) {
-    it('should render a multi param arrow function', () => {
-      expect(multiParamArrowFunction, 'to inspect as', '(a, b) => a + b');
-    });
-  }
+  it('should inspect an anonymous generator with spaces around the *', () => {
+    expect(
+      'function * (){}',
+      'if supported by the runtime to inspect as',
+      'function *(){}'
+    );
+  });
 
-  // We can't complete this test if the runtime doesn't support the async keyword:
-  var asyncFunction;
-  try {
-    // eslint-disable-next-line no-new-func
-    asyncFunction = new Function(
-      'return async function foo(a) {return a + 1;}'
-    )();
-  } catch (e) {}
-
-  if (asyncFunction) {
-    it('should render "async" before an AsyncFunction instance', () => {
-      expect(
-        asyncFunction,
-        'to inspect as',
-        'async function foo(a) { return a + 1; }'
-      );
-    });
-  }
+  it('should inspect a named generator', () => {
+    expect(
+      'function*foo(){}',
+      'if supported by the runtime to inspect as',
+      'function *foo(){}'
+    );
+  });
+
+  it('should inspect a named generator with spaces around the *', () => {
+    expect(
+      'function * foo(){}',
+      'if supported by the runtime to inspect as',
+      'function *foo(){}'
+    );
+  });
+
+  it('should inspect an anonymous class', () => {
+    expect('class {}', 'if supported by the runtime to inspect as', 'class {}');
+  });
+
+  it('should inspect a class', () => {
+    expect(
+      'class Foo {}',
+      'if supported by the runtime to inspect as',
+      'class Foo {}'
+    );
+  });
+
+  it('should inspect and reindent a non-empty class', () => {
+    expect(
+      'class Foo {\n' +
+        ' constructor(bar) {\n' +
+        '  this.bar = bar;\n' +
+        ' }\n' +
+        '}',
+      'if supported by the runtime to inspect as',
+      'class Foo {\n' +
+        '  constructor(bar) {\n' +
+        '    this.bar = bar;\n' +
+        '  }\n' +
+        '}'
+    );
+  });
 
   describe('diff()', function() {
     function foo() {}