From 6cd97991bacd543df1dc2fb8ee2301d56f770814 Mon Sep 17 00:00:00 2001 From: vdemedes Date: Tue, 15 Nov 2016 22:42:50 +0200 Subject: [PATCH] magic assert --- lib/assert.js | 17 +++++- lib/cli.js | 8 +-- lib/code-excerpt.js | 43 +++++++++++++++ lib/colors.js | 2 +- lib/enhance-assert.js | 70 ++++++++++++++++--------- lib/format-assert-error.js | 73 ++++++++++++++++++++++++++ lib/reporters/mini.js | 37 +++++++------ lib/reporters/verbose.js | 40 +++++++++++--- lib/serialize-error.js | 57 +++++++++++++++++++- lib/test.js | 6 +-- package.json | 12 +++-- test/assert.js | 31 ++++++++++- test/code-excerpt.js | 80 ++++++++++++++++++++++++++++ test/serialize-error.js | 105 +++++++++++++++++++++++++++++++++++++ 14 files changed, 515 insertions(+), 66 deletions(-) create mode 100644 lib/code-excerpt.js create mode 100644 lib/format-assert-error.js create mode 100644 test/code-excerpt.js create mode 100644 test/serialize-error.js diff --git a/lib/assert.js b/lib/assert.js index 6e6804e98..d9faaf3e6 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -7,6 +7,7 @@ const indentString = require('indent-string'); const isObservable = require('is-observable'); const isPromise = require('is-promise'); const jestSnapshot = require('jest-snapshot'); +const reactElementToJSXString = require('react-element-to-jsx-string').default; const snapshotState = require('./snapshot-state'); const x = module.exports; @@ -18,7 +19,7 @@ function create(val, expected, operator, msg, fn) { return { actual: val, expected, - message: msg, + message: msg || ' ', operator, stackStartFunction: fn }; @@ -26,7 +27,9 @@ function create(val, expected, operator, msg, fn) { function test(ok, opts) { if (!ok) { - throw new assert.AssertionError(opts); + const err = new assert.AssertionError(opts); + err.showOutput = ['fail'].indexOf(err.operator) === -1; + throw err; } } @@ -151,6 +154,16 @@ x.ifError = (err, msg) => { test(!err, create(err, 'Error', '!==', msg, x.ifError)); }; +x.jsxEqual = (val, expected, msg) => { + const treeEquals = reactElementToJSXString(val) === reactElementToJSXString(expected); + test(treeEquals, create(val, expected, '===', msg, x.jsxEqual)); +}; + +x.jsxNotEqual = (val, expected, msg) => { + const treeEquals = reactElementToJSXString(val) === reactElementToJSXString(expected); + test(!treeEquals, create(val, expected, '!==', msg, x.jsxEqual)); +}; + x._snapshot = function (tree, optionalMessage, match, snapshotStateGetter) { // Set defaults - this allows tests to mock deps easily const toMatchSnapshot = match || jestSnapshot.toMatchSnapshot; diff --git a/lib/cli.js b/lib/cli.js index 7d0ab0407..cd3ec8e3b 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -103,6 +103,8 @@ exports.run = function () { throw new Error(colors.error(figures.cross) + ' The --require and -r flags are deprecated. Requirements should be configured in package.json - see documentation.'); } + var resolveTestsFrom = cli.input.length === 0 ? pkgDir : process.cwd(); + var api = new Api({ failFast: cli.flags.failFast, serial: cli.flags.serial, @@ -112,7 +114,7 @@ exports.run = function () { explicitTitles: cli.flags.watch, match: arrify(cli.flags.match), babelConfig: babelConfig.validate(conf.babel), - resolveTestsFrom: cli.input.length === 0 ? pkgDir : process.cwd(), + resolveTestsFrom, pkgDir: pkgDir, timeout: cli.flags.timeout, concurrency: cli.flags.concurrency ? parseInt(cli.flags.concurrency, 10) : 0, @@ -124,9 +126,9 @@ exports.run = function () { if (cli.flags.tap && !cli.flags.watch) { reporter = tapReporter(); } else if (cli.flags.verbose || isCi) { - reporter = verboseReporter(); + reporter = verboseReporter({basePath: resolveTestsFrom}); } else { - reporter = miniReporter({watching: cli.flags.watch}); + reporter = miniReporter({watching: cli.flags.watch, basePath: resolveTestsFrom}); } reporter.api = api; diff --git a/lib/code-excerpt.js b/lib/code-excerpt.js new file mode 100644 index 000000000..f8511a0c9 --- /dev/null +++ b/lib/code-excerpt.js @@ -0,0 +1,43 @@ +'use strict'; + +const fs = require('fs'); +const equalLength = require('equal-length'); +const codeExcerpt = require('code-excerpt'); +const truncate = require('cli-truncate'); +const chalk = require('chalk'); + +function formatLineNumber(line, maxLines) { + return ' '.repeat(String(maxLines).length - String(line).length) + line; +} + +module.exports = (file, line, options) => { + const maxWidth = (options || {}).maxWidth || 80; + const indent = (options || {}).indent || 0; + const source = fs.readFileSync(file, 'utf8'); + const excerpt = codeExcerpt(source, line, {around: 1}); + + const lines = excerpt.map(item => ({ + line: item.line, + value: truncate(item.value, maxWidth - String(line).length - 1) + })); + + const joinedLines = lines.map(line => line.value).join('\n'); + const extendedLines = equalLength(joinedLines).split('\n'); + + return lines + .map((item, index) => ({ + line: item.line, + value: extendedLines[index] + })) + .map(item => { + const isErrorSource = item.line === line; + + const lineNumber = formatLineNumber(item.line, line) + ':'; + const coloredLineNumber = isErrorSource ? lineNumber : chalk.grey(lineNumber); + const result = ` ${coloredLineNumber} ${item.value}`; + + return isErrorSource ? chalk.bgRed(result) : result; + }) + .map(line => ' '.repeat(indent) + line) + .join('\n'); +}; diff --git a/lib/colors.js b/lib/colors.js index 2d67c9808..f1cccbeb1 100644 --- a/lib/colors.js +++ b/lib/colors.js @@ -2,7 +2,7 @@ const chalk = require('chalk'); module.exports = { - title: chalk.white, + title: chalk.bold.white, error: chalk.red, skip: chalk.yellow, todo: chalk.blue, diff --git a/lib/enhance-assert.js b/lib/enhance-assert.js index 70fa8b95c..ed537cb6a 100644 --- a/lib/enhance-assert.js +++ b/lib/enhance-assert.js @@ -1,5 +1,7 @@ 'use strict'; +const dotProp = require('dot-prop'); + module.exports = enhanceAssert; module.exports.formatter = formatter; @@ -8,17 +10,9 @@ module.exports.PATTERNS = [ 't.falsy(value, [message])', 't.true(value, [message])', 't.false(value, [message])', - 't.is(value, expected, [message])', - 't.not(value, expected, [message])', - 't.deepEqual(value, expected, [message])', - 't.notDeepEqual(value, expected, [message])', - 't.regex(contents, regex, [message])', - 't.notRegex(contents, regex, [message])', // Deprecated apis 't.ok(value, [message])', - 't.notOk(value, [message])', - 't.same(value, expected, [message])', - 't.notSame(value, expected, [message])' + 't.notOk(value, [message])' ]; module.exports.NON_ENHANCED_PATTERNS = [ @@ -27,7 +21,15 @@ module.exports.NON_ENHANCED_PATTERNS = [ 't.throws(fn, [message])', 't.notThrows(fn, [message])', 't.ifError(error, [message])', - 't.snapshot(contents, [message])' + 't.snapshot(contents, [message])', + 't.is(value, expected, [message])', + 't.not(value, expected, [message])', + 't.deepEqual(value, expected, [message])', + 't.notDeepEqual(value, expected, [message])', + 't.regex(contents, regex, [message])', + 't.notRegex(contents, regex, [message])', + 't.same(value, expected, [message])', + 't.notSame(value, expected, [message])' ]; function enhanceAssert(opts) { @@ -48,22 +50,38 @@ function enhanceAssert(opts) { return enhanced; } +function isRangeMatch(a, b) { + return (a[0] === b[0] && a[1] === b[1]) || + (a[0] > b[0] && a[0] < b[1]) || + (a[1] > b[0] && a[1] < b[1]); +} + +function computeStatement(tokens, range) { + return tokens + .filter(token => isRangeMatch(token.range, range)) + .map(token => token.value === undefined ? token.type.label : token.value) + .join(''); +} + +function getNode(ast, path) { + return dotProp.get(ast, path.replace(/\//g, '.')); +} + function formatter() { - const createFormatter = require('power-assert-context-formatter'); - const SuccinctRenderer = require('power-assert-renderer-succinct'); - const AssertionRenderer = require('power-assert-renderer-assertion'); + return context => { + var ast = JSON.parse(context.source.ast); + var tokens = JSON.parse(context.source.tokens); + var args = context.args[0].events; + + return args + .map(arg => { + var range = getNode(ast, arg.espath).range; - return createFormatter({ - renderers: [ - { - ctor: AssertionRenderer - }, - { - ctor: SuccinctRenderer, - options: { - maxDepth: 3 - } - } - ] - }); + return [ + computeStatement(tokens, range), + arg.value + ]; + }) + .reverse(); + }; } diff --git a/lib/format-assert-error.js b/lib/format-assert-error.js new file mode 100644 index 000000000..e05f31685 --- /dev/null +++ b/lib/format-assert-error.js @@ -0,0 +1,73 @@ +'use strict'; + +const indentString = require('indent-string'); +const chalk = require('chalk'); +const diff = require('diff'); + +function cleanUp(line) { + if (line[0] === '+') { + return `${chalk.green('+')} ${line.slice(1)}`; + } + + if (line[0] === '-') { + return `${chalk.red('-')} ${line.slice(1)}`; + } + + if (line.match(/@@/)) { + return null; + } + + if (line.match(/\\ No newline/)) { + return null; + } + + return ` ${line}`; +} + +module.exports = err => { + if (err.statements) { + const statements = JSON.parse(err.statements); + + return statements + .map(statement => `${statement[0]}\n${chalk.grey('=>')} ${statement[1]}`) + .join('\n\n') + '\n'; + } + + if ((err.actualType === 'object' || err.actualType === 'array') && err.actualType === err.expectedType) { + const patch = diff.createPatch('string', err.actual, err.expected); + const msg = patch + .split('\n') + .splice(4) + .map(cleanUp) + .filter(Boolean) + .join('\n'); + + return `Difference:\n\n${msg}`; + } + + if (err.actualType === 'string' && err.expectedType === 'string') { + const patch = diff.diffChars(err.actual, err.expected); + const msg = patch + .map(part => { + if (part.added) { + return chalk.black.bgGreen(part.value); + } + + if (part.removed) { + return chalk.black.bgRed(part.value); + } + + return part.value; + }) + .join(''); + + return `Difference:\n\n${msg}\n`; + } + + return [ + 'Actual:\n', + `${indentString(err.actual, 2)}\n`, + 'Expected:\n', + `${indentString(err.expected, 2)}\n` + ].join('\n'); +}; diff --git a/lib/reporters/mini.js b/lib/reporters/mini.js index c78e758e4..2836172f4 100644 --- a/lib/reporters/mini.js +++ b/lib/reporters/mini.js @@ -1,5 +1,6 @@ 'use strict'; var StringDecoder = require('string_decoder').StringDecoder; +var path = require('path'); var cliCursor = require('cli-cursor'); var lastLineTracker = require('last-line-stream/tracker'); var plur = require('plur'); @@ -8,7 +9,10 @@ var chalk = require('chalk'); var cliTruncate = require('cli-truncate'); var cross = require('figures').cross; var repeating = require('repeating'); +var indentString = require('indent-string'); var objectAssign = require('object-assign'); +var formatAssertError = require('../format-assert-error'); +var codeExcerpt = require('../code-excerpt'); var colors = require('../colors'); chalk.enabled = true; @@ -170,28 +174,29 @@ MiniReporter.prototype.finish = function (runStatus) { } if (this.failCount > 0) { - runStatus.errors.forEach(function (test) { - if (!test.error || !test.error.message) { + runStatus.errors.forEach(function (test, index) { + if (!test.error) { return; } var title = test.error ? test.title : 'Unhandled Error'; - var description; - var errorTitle = ' ' + test.error.message + '\n'; - var isPowerAssert = test.error.message.split('\n').length > 1; - - description = stripFirstLine(test.error.stack).trimRight(); - - if (isPowerAssert) { - description = stripFirstLine(description).replace(/ {3}/g, ' '); - } else { - description.replace(/ {3}/g, ' '); + var description = stripFirstLine(test.error.stack + .split('\n') + .map(str => str.trim()) + .join('\n')); + + var beforeSpacing = index === 0 ? '\n\n' : '\n\n\n\n'; + var errorPath = path.relative(this.options.basePath, test.error.source.file) + ':' + test.error.source.line; + + status += beforeSpacing + ' ' + colors.title(title) + '\n'; + status += ' ' + colors.errorStack(errorPath) + '\n\n'; + status += codeExcerpt(test.error.source.file, test.error.source.line, {maxWidth: process.stdout.columns, indent: 2}) + '\n\n'; + if (test.error.showOutput) { + status += indentString(formatAssertError(test.error), 2) + '\n'; } - status += '\n\n ' + colors.title(title) + '\n'; - status += colors.stack(errorTitle); - status += colors.errorStack(description); - }); + status += indentString(colors.errorStack(description), 2); + }, this); } if (this.rejectionCount > 0 || this.exceptionCount > 0) { diff --git a/lib/reporters/verbose.js b/lib/reporters/verbose.js index a81860178..a2dbf6cb6 100644 --- a/lib/reporters/verbose.js +++ b/lib/reporters/verbose.js @@ -1,19 +1,26 @@ 'use strict'; +var path = require('path'); +var indentString = require('indent-string'); var prettyMs = require('pretty-ms'); var figures = require('figures'); var chalk = require('chalk'); var plur = require('plur'); var repeating = require('repeating'); +var objectAssign = require('object-assign'); +var formatAssertError = require('../format-assert-error'); +var codeExcerpt = require('../code-excerpt'); var colors = require('../colors'); Object.keys(colors).forEach(function (key) { colors[key].enabled = true; }); -function VerboseReporter() { +function VerboseReporter(options) { if (!(this instanceof VerboseReporter)) { - return new VerboseReporter(); + return new VerboseReporter(options); } + + this.options = objectAssign({}, options); } module.exports = VerboseReporter; @@ -101,17 +108,34 @@ VerboseReporter.prototype.finish = function (runStatus) { } if (runStatus.failCount > 0) { - runStatus.tests.forEach(function (test) { - if (!(test.error && test.error.message)) { + runStatus.tests.forEach(function (test, index) { + if (!test.error) { return; } i++; - output += '\n\n\n ' + colors.error(i + '.', test.title) + '\n'; - var stack = test.error.stack ? test.error.stack.trimRight() : ''; - output += ' ' + colors.stack(stack); - }); + var stack = ''; + + if (test.error.stack) { + stack = test.error.stack + .split('\n') + .map(str => str.trim()) + .join('\n'); + } + + var beforeSpacing = index === 0 ? '\n\n' : '\n\n\n\n'; + var errorPath = path.relative(this.options.basePath, test.error.source.file) + ':' + test.error.source.line; + + output += beforeSpacing + ' ' + colors.error(i + '.', test.title) + '\n'; + output += ' ' + colors.errorStack(errorPath) + '\n\n'; + output += codeExcerpt(test.error.source.file, test.error.source.line, {maxWidth: process.stdout.columns, indent: 2}) + '\n\n'; + if (test.error.showOutput) { + output += indentString(formatAssertError(test.error), 2) + '\n'; + } + + output += indentString(colors.stack(stack), 2); + }, this); } return output + '\n'; diff --git a/lib/serialize-error.js b/lib/serialize-error.js index 55fef22ea..fe10d5f6c 100644 --- a/lib/serialize-error.js +++ b/lib/serialize-error.js @@ -1,7 +1,27 @@ 'use strict'; const cleanYamlObject = require('clean-yaml-object'); +const StackUtils = require('stack-utils'); +const prettyFormat = require('pretty-format'); +const reactElementPlugin = require('pretty-format/plugins/ReactElement'); +const reactTestPlugin = require('pretty-format/plugins/ReactTestComponent'); +const renderer = require('react-test-renderer'); const beautifyStack = require('./beautify-stack'); +function isReactElement(obj) { + return obj.type && obj.ref !== undefined && obj.props; +} + +function serializeValue(value) { + if (isReactElement(value)) { + value = renderer.create(value).toJSON(); + } + + return prettyFormat(value, { + plugins: [reactTestPlugin, reactElementPlugin], + highlight: true + }); +} + function filter(propertyName, isRoot, source, target) { if (!isRoot) { return true; @@ -12,7 +32,42 @@ function filter(propertyName, isRoot, source, target) { return false; } + if (propertyName === 'statements') { + if (source.showOutput) { + target.statements = JSON.stringify(source[propertyName].map(statement => { + const path = statement[0]; + const value = serializeValue(statement[1]); + + return [path, value]; + })); + } + + return false; + } + + if (propertyName === 'actual' || propertyName === 'expected') { + if (source.showOutput) { + const value = source[propertyName]; + target[propertyName + 'Type'] = typeof value; + target[propertyName] = serializeValue(value); + } + + return false; + } + return true; } -module.exports = error => cleanYamlObject(error, filter); +const stackUtils = new StackUtils(); + +module.exports = error => { + const err = cleanYamlObject(error, filter); + + const source = stackUtils.parseLine(err.stack.split('\n')[1]); + err.source = { + file: source.file, + line: source.line + }; + + return err; +}; diff --git a/lib/test.js b/lib/test.js index b77edb35c..530ab9c73 100644 --- a/lib/test.js +++ b/lib/test.js @@ -300,10 +300,8 @@ function PublicApi(test) { function onAssertionEvent(event) { if (event.assertionThrew) { if (event.powerAssertContext) { - event.error.message = formatter(event.powerAssertContext); - if (event.originalMessage) { - event.error.message = event.originalMessage + ' ' + event.error.message; - } + event.error.statements = formatter(event.powerAssertContext); + event.error.message = event.originalMessage || ''; } this._test._setAssertError(event.error); this._test._assert(null); diff --git a/package.json b/package.json index 985ef41d1..61736083d 100644 --- a/package.json +++ b/package.json @@ -114,12 +114,16 @@ "cli-spinners": "^1.0.0", "cli-truncate": "^0.2.0", "co-with-promise": "^4.6.0", + "code-excerpt": "^2.0.0", "common-path-prefix": "^1.0.0", "convert-source-map": "^1.2.0", "core-assert": "^0.2.0", "currently-unhandled": "^0.4.1", "debug": "^2.2.0", + "diff": "^3.0.1", + "dot-prop": "^4.1.0", "empower-core": "^0.6.1", + "equal-length": "^1.0.0", "figures": "^2.0.0", "find-cache-dir": "^0.1.1", "fn-name": "^2.0.0", @@ -149,10 +153,11 @@ "package-hash": "^1.1.0", "pkg-conf": "^2.0.0", "plur": "^2.0.0", - "power-assert-context-formatter": "^1.0.4", - "power-assert-renderer-assertion": "^1.0.1", - "power-assert-renderer-succinct": "^1.0.1", + "pretty-format": "github:vadimdemedes/pretty-format#colors", "pretty-ms": "^2.0.0", + "react": "^15.4.0", + "react-element-to-jsx-string": "^5.0.2", + "react-test-renderer": "^15.4.0", "repeating": "^2.0.0", "require-precompiled": "^0.1.0", "resolve-cwd": "^1.0.0", @@ -186,6 +191,7 @@ "sinon": "^1.17.2", "source-map-fixtures": "^2.1.0", "tap": "^8.0.0", + "temp-write": "^2.1.0", "touch": "^1.0.0", "xo": "^0.17.0", "zen-observable": "^0.4.0" diff --git a/test/assert.js b/test/assert.js index 4b6cf5f1e..b4a64e8ee 100644 --- a/test/assert.js +++ b/test/assert.js @@ -1,5 +1,6 @@ 'use strict'; const test = require('tap').test; +const React = require('react'); const sinon = require('sinon'); const assert = require('../lib/assert'); @@ -331,13 +332,15 @@ test('.deepEqual()', t => { t.throws(() => { assert.deepEqual([['a', 'b'], 'c'], [['a', 'b'], 'd']); - }, / 'c' ].*? 'd' ]/); + }); + // }, / 'c' ].*? 'd' ]/); t.throws(() => { const circular = ['a', 'b']; circular.push(circular); assert.deepEqual([circular, 'c'], [circular, 'd']); - }, / 'c' ].*? 'd' ]/); + }); + // }, / 'c' ].*? 'd' ]/); t.end(); }); @@ -487,6 +490,30 @@ test('.deepEqual() should not mask RangeError from underlying assert', t => { t.end(); }); +test('.jsxEqual()', t => { + t.throws(() => { + assert.jsxEqual(React.createElement('b', null), React.createElement('i', null)); + }); + + t.doesNotThrow(() => { + assert.jsxEqual(React.createElement('b', null), React.createElement('b', null)); + }); + + t.end(); +}); + +test('.jsxNotEqual()', t => { + t.throws(() => { + assert.jsxNotEqual(React.createElement('b', null), React.createElement('b', null)); + }); + + t.doesNotThrow(() => { + assert.jsxNotEqual(React.createElement('b', null), React.createElement('i', null)); + }); + + t.end(); +}); + test('snapshot makes a snapshot using a library and global options', t => { const saveSpy = sinon.spy(); const state = {save: saveSpy}; diff --git a/test/code-excerpt.js b/test/code-excerpt.js new file mode 100644 index 000000000..f78f000f3 --- /dev/null +++ b/test/code-excerpt.js @@ -0,0 +1,80 @@ +'use strict'; +const tempWrite = require('temp-write'); +const chalk = require('chalk'); +const test = require('tap').test; +const codeExcerpt = require('../lib/code-excerpt'); + +chalk.enabled = true; + +test('read code excerpt', t => { + const path = tempWrite.sync([ + 'function a() {', + '\talert();', + '}' + ].join('\n')); + + const excerpt = codeExcerpt(path, 2); + const expected = [ + ` ${chalk.grey('1:')} function a() {`, + chalk.bgRed(` 2: alert(); `), + ` ${chalk.grey('3:')} } ` + ].join('\n'); + + t.is(excerpt, expected); + t.end(); +}); + +test('indent output', t => { + const path = tempWrite.sync([ + 'function a() {', + '\talert();', + '}' + ].join('\n')); + + const excerpt = codeExcerpt(path, 2, {indent: 2}); + const expected = [ + ` ${chalk.grey('1:')} function a() {`, + ` ${chalk.bgRed(` 2: alert(); `)}`, + ` ${chalk.grey('3:')} } ` + ].join('\n'); + + t.is(excerpt, expected); + t.end(); +}); + +test('truncate lines', t => { + const path = tempWrite.sync([ + 'function a() {', + '\talert();', + '}' + ].join('\n')); + + const excerpt = codeExcerpt(path, 2, {maxWidth: 14}); + const expected = [ + ` ${chalk.grey('1:')} function a(…`, + chalk.bgRed(` 2: alert(); `), + ` ${chalk.grey('3:')} } ` + ].join('\n'); + + t.is(excerpt, expected); + t.end(); +}); + +test('format line numbers', t => { + const path = tempWrite.sync([ + '', '', '', '', '', '', '', '', + 'function a() {', + '\talert();', + '}' + ].join('\n')); + + const excerpt = codeExcerpt(path, 10); + const expected = [ + ` ${chalk.grey(' 9:')} function a() {`, + chalk.bgRed(` 10: alert(); `), + ` ${chalk.grey('11:')} } ` + ].join('\n'); + + t.is(excerpt, expected); + t.end(); +}); diff --git a/test/serialize-error.js b/test/serialize-error.js new file mode 100644 index 000000000..e43e814c5 --- /dev/null +++ b/test/serialize-error.js @@ -0,0 +1,105 @@ +'use strict'; + +const prettyFormat = require('pretty-format'); +const reactElementPlugin = require('pretty-format/plugins/ReactElement'); +const reactTestPlugin = require('pretty-format/plugins/ReactTestComponent'); +const renderer = require('react-test-renderer'); +const test = require('tap').test; +const beautifyStack = require('../lib/beautify-stack'); +const serialize = require('../lib/serialize-error'); + +function isReactElement(obj) { + return obj.type && obj.ref !== undefined && obj.props; +} + +function serializeValue(value) { + if (isReactElement(value)) { + value = renderer.create(value).toJSON(); + } + + return prettyFormat(value, { + plugins: [reactTestPlugin, reactElementPlugin], + highlight: true + }); +} + +test('serialize standard props', t => { + const err = new Error('Hello'); + const serializedErr = serialize(err); + + t.is(Object.keys(serializedErr).length, 4); + t.is(serializedErr.name, 'Error'); + t.is(serializedErr.stack, beautifyStack(err.stack)); + t.is(serializedErr.message, 'Hello'); + t.is(serializedErr.source.file, 'test/serialize-error.js'); + t.is(typeof serializedErr.source.line, 'number'); + t.end(); +}); + +test('serialize statements', t => { + const err = new Error(); + err.showOutput = true; + err.statements = [ + ['actual.a[0]', 1], + ['actual.a', [1]], + ['actual', {a: [1]}] + ]; + + const serializedErr = serialize(err); + + t.true(serializedErr.showOutput); + t.deepEqual(serializedErr.statements, JSON.stringify([ + ['actual.a[0]', serializeValue(1)], + ['actual.a', serializeValue([1])], + ['actual', serializeValue({a: [1]})] + ])); + t.end(); +}); + +test('skip statements if output is off', t => { + const err = new Error(); + err.showOutput = false; + err.statements = [ + ['actual.a[0]', 1], + ['actual.a', [1]], + ['actual', {a: [1]}] + ]; + + const serializedErr = serialize(err); + + t.false(serializedErr.showOutput); + t.notOk(serializedErr.statements); + t.end(); +}); + +test('serialize actual and expected props', t => { + const err = new Error(); + err.showOutput = true; + err.actual = 1; + err.expected = 'a'; + + const serializedErr = serialize(err); + + t.true(serializedErr.showOutput); + t.is(serializedErr.actual, serializeValue(1)); + t.is(serializedErr.expected, serializeValue('a')); + t.is(serializedErr.actualType, 'number'); + t.is(serializedErr.expectedType, 'string'); + t.end(); +}); + +test('skip actual and expected if output is off', t => { + const err = new Error(); + err.showOutput = false; + err.actual = 1; + err.expected = 'a'; + + const serializedErr = serialize(err); + + t.false(serializedErr.showOutput); + t.notOk(serializedErr.actual); + t.notOk(serializedErr.expected); + t.notOk(serializedErr.actualType); + t.notOk(serializedErr.expectedType); + t.end(); +});