From c9e6e6fafacad3f54536dc9122555e939d13aa21 Mon Sep 17 00:00:00 2001 From: Vadim Demedes Date: Thu, 2 Feb 2017 06:51:39 +0200 Subject: [PATCH] Magic assert (#1154) --- lib/assert.js | 38 +++-- lib/cli.js | 4 +- lib/code-excerpt.js | 45 ++++++ lib/colors.js | 3 +- lib/enhance-assert.js | 92 +++++++----- lib/extract-stack.js | 10 ++ lib/format-assert-error.js | 72 +++++++++ lib/reporters/mini.js | 49 +++--- lib/reporters/verbose.js | 49 ++++-- lib/serialize-error.js | 53 ++++++- lib/test.js | 6 +- package.json | 13 +- test/api.js | 31 +--- test/assert.js | 46 ++++-- test/code-excerpt.js | 62 ++++++++ test/extract-stack.js | 36 +++++ test/format-assert-error.js | 95 ++++++++++++ test/reporters/mini.js | 289 ++++++++++++++++++++++++++++++++---- test/reporters/verbose.js | 249 +++++++++++++++++++++++++++++-- test/serialize-error.js | 95 ++++++++++++ 20 files changed, 1157 insertions(+), 180 deletions(-) create mode 100644 lib/code-excerpt.js create mode 100644 lib/extract-stack.js create mode 100644 lib/format-assert-error.js create mode 100644 test/code-excerpt.js create mode 100644 test/extract-stack.js create mode 100644 test/format-assert-error.js create mode 100644 test/serialize-error.js diff --git a/lib/assert.js b/lib/assert.js index 70cc9f147..1b5284ae0 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -17,7 +17,7 @@ function create(val, expected, operator, msg, fn) { return { actual: val, expected, - message: msg, + message: msg || ' ', operator, stackStartFunction: fn }; @@ -25,7 +25,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', 'throws', 'notThrows'].indexOf(err.operator) === -1; + throw err; } } @@ -109,7 +111,7 @@ x.throws = (fn, err, msg) => { return result; } catch (err) { - test(false, create(err.actual, err.expected, err.operator, err.message, x.throws)); + test(false, create(err.actual, err.expected, 'throws', err.message, x.throws)); } }; @@ -134,7 +136,7 @@ x.notThrows = (fn, msg) => { try { assert.doesNotThrow(fn, msg); } catch (err) { - test(false, create(err.actual, err.expected, err.operator, err.message, x.notThrows)); + test(false, create(err.actual, err.expected, 'notThrows', err.message, x.notThrows)); } }; @@ -163,21 +165,33 @@ x._snapshot = function (tree, optionalMessage, match, snapshotStateGetter) { snapshotState: state }; - const result = toMatchSnapshot.call(context, tree); + // symbols can't be serialized and saved in a snapshot, + // that's why tree is saved in `jsx` prop, so that JSX can be detected later + const serializedTree = tree.$$typeof === Symbol.for('react.test.json') ? {__ava_react_jsx: tree} : tree; // eslint-disable-line camelcase + const result = toMatchSnapshot.call(context, JSON.stringify(serializedTree)); - let message = 'Please check your code or --update-snapshots\n\n'; + let message = 'Please check your code or --update-snapshots'; if (optionalMessage) { - message += indentString(optionalMessage, 2); - } - - if (typeof result.message === 'function') { - message += indentString(result.message(), 2); + message += '\n\n' + indentString(optionalMessage, 2); } state.save(); - test(result.pass, create(result, false, 'snapshot', message, x.snap)); + let expected; + + if (result.expected) { + // JSON in a snapshot is surrounded with `"`, because jest-snapshot + // serializes snapshot values too, so it ends up double JSON encoded + expected = JSON.parse(result.expected.slice(1).slice(0, -1)); + // Define a `$$typeof` symbol, so that pretty-format detects it as React tree + if (expected.__ava_react_jsx) { // eslint-disable-line camelcase + expected = expected.__ava_react_jsx; // eslint-disable-line camelcase + Object.defineProperty(expected, '$$typeof', {value: Symbol.for('react.test.json')}); + } + } + + test(result.pass, create(tree, expected, 'snapshot', message, x.snapshot)); }; x.snapshot = function (tree, optionalMessage) { diff --git a/lib/cli.js b/lib/cli.js index 58192136b..6382231f4 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -129,9 +129,9 @@ exports.run = () => { if (cli.flags.tap && !cli.flags.watch) { reporter = tapReporter(); } else if (cli.flags.verbose || isCi) { - reporter = verboseReporter(); + reporter = verboseReporter({basePath: pkgDir}); } else { - reporter = miniReporter({watching: cli.flags.watch}); + reporter = miniReporter({watching: cli.flags.watch, basePath: pkgDir}); } reporter.api = api; diff --git a/lib/code-excerpt.js b/lib/code-excerpt.js new file mode 100644 index 000000000..6ccfd76f8 --- /dev/null +++ b/lib/code-excerpt.js @@ -0,0 +1,45 @@ +'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'); + +const formatLineNumber = (lineNumber, maxLineNumber) => { + return ' '.repeat(String(maxLineNumber).length - String(lineNumber).length) + lineNumber; +}; + +module.exports = (file, line, options) => { + options = options || {}; + + const maxWidth = options.maxWidth || 80; + const source = fs.readFileSync(file, 'utf8'); + const excerpt = codeExcerpt(source, line, {around: 1}); + if (!excerpt) { + return null; + } + + const lines = excerpt.map(item => ({ + line: item.line, + value: truncate(item.value, maxWidth - String(line).length - 5) + })); + + 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; + }) + .join('\n'); +}; diff --git a/lib/colors.js b/lib/colors.js index 1a922325e..74be14bb1 100644 --- a/lib/colors.js +++ b/lib/colors.js @@ -2,12 +2,13 @@ const chalk = require('chalk'); module.exports = { - title: chalk.white, + title: chalk.bold.white, error: chalk.red, skip: chalk.yellow, todo: chalk.blue, pass: chalk.green, duration: chalk.gray.dim, + errorSource: chalk.gray, errorStack: chalk.gray, stack: chalk.red, information: chalk.magenta diff --git a/lib/enhance-assert.js b/lib/enhance-assert.js index f88d530c8..e596280b3 100644 --- a/lib/enhance-assert.js +++ b/lib/enhance-assert.js @@ -1,67 +1,79 @@ 'use strict'; - -module.exports = enhanceAssert; -module.exports.formatter = formatter; +const dotProp = require('dot-prop'); // When adding patterns, don't forget to add to // https://github.com/avajs/babel-preset-transform-test-files/blob/master/espower-patterns.json // Then release a new version of that preset and bump the SemVer range here. -module.exports.PATTERNS = [ +const PATTERNS = [ 't.truthy(value, [message])', '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])' ]; -module.exports.NON_ENHANCED_PATTERNS = [ +const NON_ENHANCED_PATTERNS = [ 't.pass([message])', 't.fail([message])', '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])' ]; -function enhanceAssert(opts) { +const enhanceAssert = opts => { const empower = require('empower-core'); - - const enhanced = empower( - opts.assert, - { - destructive: false, - onError: opts.onError, - onSuccess: opts.onSuccess, - patterns: module.exports.PATTERNS, - wrapOnlyPatterns: module.exports.NON_ENHANCED_PATTERNS, - bindReceiver: false - } - ); + const enhanced = empower(opts.assert, { + destructive: false, + onError: opts.onError, + onSuccess: opts.onSuccess, + patterns: PATTERNS, + wrapOnlyPatterns: NON_ENHANCED_PATTERNS, + bindReceiver: false + }); return enhanced; -} +}; -function formatter() { - const createFormatter = require('power-assert-context-formatter'); - const SuccinctRenderer = require('power-assert-renderer-succinct'); - const AssertionRenderer = require('power-assert-renderer-assertion'); +const 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]); +}; - return createFormatter({ - renderers: [ - { - ctor: AssertionRenderer - }, - { - ctor: SuccinctRenderer, - options: { - maxDepth: 3 - } - } - ] - }); -} +const computeStatement = (tokens, range) => { + return tokens + .filter(token => isRangeMatch(token.range, range)) + .map(token => token.value === undefined ? token.type.label : token.value) + .join(''); +}; + +const getNode = (ast, path) => dotProp.get(ast, path.replace(/\//g, '.')); + +const formatter = () => { + return context => { + const ast = JSON.parse(context.source.ast); + const tokens = JSON.parse(context.source.tokens); + const args = context.args[0].events; + + return args + .map(arg => { + const range = getNode(ast, arg.espath).range; + + return [computeStatement(tokens, range), arg.value]; + }) + .reverse(); + }; +}; + +module.exports = enhanceAssert; +module.exports.PATTERNS = PATTERNS; +module.exports.NON_ENHANCED_PATTERNS = NON_ENHANCED_PATTERNS; +module.exports.formatter = formatter; diff --git a/lib/extract-stack.js b/lib/extract-stack.js new file mode 100644 index 000000000..64f63db1c --- /dev/null +++ b/lib/extract-stack.js @@ -0,0 +1,10 @@ +'use strict'; +const stackLineRegex = /^.+ \(.+:[0-9]+:[0-9]+\)$/; + +module.exports = stack => { + return stack + .split('\n') + .filter(line => stackLineRegex.test(line)) + .map(line => line.trim()) + .join('\n'); +}; diff --git a/lib/format-assert-error.js b/lib/format-assert-error.js new file mode 100644 index 000000000..5d8dd3f30 --- /dev/null +++ b/lib/format-assert-error.js @@ -0,0 +1,72 @@ +'use strict'; +const indentString = require('indent-string'); +const chalk = require('chalk'); +const diff = require('diff'); + +const 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') + .slice(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.bgGreen.black(part.value); + } + + if (part.removed) { + return chalk.bgRed.black(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 1c915c7da..cdf9ed5dc 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,6 +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 formatAssertError = require('../format-assert-error'); +var extractStack = require('../extract-stack'); +var codeExcerpt = require('../code-excerpt'); var colors = require('../colors'); chalk.enabled = true; @@ -169,28 +174,38 @@ 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; + var beforeSpacing = index === 0 ? '\n\n' : '\n\n\n\n'; - description = stripFirstLine(test.error.stack).trimRight(); + status += beforeSpacing + ' ' + colors.title(title) + '\n'; + if (test.error.source) { + status += ' ' + colors.errorSource(test.error.source.file + ':' + test.error.source.line) + '\n'; - if (isPowerAssert) { - description = stripFirstLine(description).replace(/ {3}/g, ' '); - } else { - description.replace(/ {3}/g, ' '); + var errorPath = path.join(this.options.basePath, test.error.source.file); + var excerpt = codeExcerpt(errorPath, test.error.source.line, {maxWidth: process.stdout.columns}); + if (excerpt) { + status += '\n' + indentString(excerpt, 2) + '\n'; + } } - status += '\n\n ' + colors.title(title) + '\n'; - status += colors.stack(errorTitle); - status += colors.errorStack(description); - }); + if (test.error.showOutput) { + status += '\n' + indentString(formatAssertError(test.error), 2); + } + + // .trim() is needed, because default err.message is ' ' (see lib/assert.js) + if (test.error.message.trim()) { + status += '\n' + indentString(test.error.message, 2) + '\n'; + } + + if (test.error.stack) { + status += '\n' + indentString(colors.errorStack(extractStack(test.error.stack)), 2); + } + }, this); } if (this.rejectionCount > 0 || this.exceptionCount > 0) { @@ -205,7 +220,7 @@ MiniReporter.prototype.finish = function (runStatus) { var title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception'; var description = err.stack ? err.stack.trimRight() : JSON.stringify(err); description = description.split('\n'); - var errorTitle = description[0]; + var errorTitle = err.name ? description[0] : 'Threw non-error: ' + description[0]; var errorStack = description.slice(1).join('\n'); status += '\n\n ' + colors.title(title) + '\n'; @@ -310,7 +325,3 @@ function eraseLines(count) { return clear; } - -function stripFirstLine(message) { - return message.replace(/^[^\n]*\n/, ''); -} diff --git a/lib/reporters/verbose.js b/lib/reporters/verbose.js index 5da3acd42..4d83492c3 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 formatAssertError = require('../format-assert-error'); +var extractStack = require('../extract-stack'); +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 = Object.assign({}, options); } module.exports = VerboseReporter; @@ -91,27 +98,43 @@ VerboseReporter.prototype.finish = function (runStatus) { output += lines.join('\n'); } - var i = 0; - if (runStatus.knownFailureCount > 0) { runStatus.knownFailures.forEach(function (test) { - i++; - output += '\n\n\n ' + colors.error(i + '.', test.title); + output += '\n\n\n ' + colors.error(test.title); }); } 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++; + var beforeSpacing = index === 0 ? '\n\n' : '\n\n\n\n'; + output += beforeSpacing + ' ' + colors.title(test.title) + '\n'; + if (test.error.source) { + output += ' ' + colors.errorSource(test.error.source.file + ':' + test.error.source.line) + '\n'; - output += '\n\n\n ' + colors.error(i + '.', test.title) + '\n'; - var stack = test.error.stack ? test.error.stack.trimRight() : ''; - output += ' ' + colors.stack(stack); - }); + var errorPath = path.join(this.options.basePath, test.error.source.file); + var excerpt = codeExcerpt(errorPath, test.error.source.line, {maxWidth: process.stdout.columns}); + if (excerpt) { + output += '\n' + indentString(excerpt, 2) + '\n'; + } + } + + if (test.error.showOutput) { + output += '\n' + indentString(formatAssertError(test.error), 2); + } + + // .trim() is needed, because default err.message is ' ' (see lib/assert.js) + if (test.error.message.trim()) { + output += '\n' + indentString(test.error.message, 2) + '\n'; + } + + if (test.error.stack) { + output += '\n' + indentString(colors.errorStack(extractStack(test.error.stack)), 2); + } + }, this); } if (runStatus.failFastEnabled === true && runStatus.remainingCount > 0 && runStatus.failCount > 0) { diff --git a/lib/serialize-error.js b/lib/serialize-error.js index 55fef22ea..e81e12256 100644 --- a/lib/serialize-error.js +++ b/lib/serialize-error.js @@ -1,6 +1,17 @@ 'use strict'; const cleanYamlObject = require('clean-yaml-object'); +const StackUtils = require('stack-utils'); +const prettyFormat = require('@ava/pretty-format'); +const reactTestPlugin = require('@ava/pretty-format/plugins/ReactTestComponent'); const beautifyStack = require('./beautify-stack'); +const extractStack = require('./extract-stack'); + +function serializeValue(value) { + return prettyFormat(value, { + plugins: [reactTestPlugin], + highlight: true + }); +} function filter(propertyName, isRoot, source, target) { if (!isRoot) { @@ -12,7 +23,47 @@ 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); + + if (err.stack) { + const firstStackLine = extractStack(err.stack).split('\n')[0]; + const source = stackUtils.parseLine(firstStackLine); + if (source) { + err.source = { + file: source.file.trim(), + line: source.line + }; + } + } + + return err; +}; diff --git a/lib/test.js b/lib/test.js index 58ca75912..377d5bdd6 100644 --- a/lib/test.js +++ b/lib/test.js @@ -62,10 +62,8 @@ class PublicApi { 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 364cc60f5..85d221b46 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,8 @@ ], "dependencies": { "@ava/babel-preset-stage-4": "^1.0.0", - "@ava/babel-preset-transform-test-files": "^1.0.0", + "@ava/babel-preset-transform-test-files": "^2.0.0", + "@ava/pretty-format": "^1.0.0", "arr-flatten": "^1.0.1", "array-union": "^1.0.1", "array-uniq": "^1.0.2", @@ -107,12 +108,16 @@ "cli-spinners": "^1.0.0", "cli-truncate": "^0.2.0", "co-with-promise": "^4.6.0", + "code-excerpt": "^2.1.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", @@ -126,7 +131,7 @@ "is-obj": "^1.0.0", "is-observable": "^0.2.0", "is-promise": "^2.1.0", - "jest-snapshot": "^17.0.3", + "jest-snapshot": "^18.1.0", "last-line-stream": "^1.0.0", "lodash.debounce": "^4.0.3", "lodash.difference": "^4.3.0", @@ -144,9 +149,6 @@ "package-hash": "^1.2.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-ms": "^2.0.0", "repeating": "^2.0.0", "require-precompiled": "^0.1.0", @@ -181,6 +183,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/api.js b/test/api.js index a96768361..4dc77b50a 100644 --- a/test/api.js +++ b/test/api.js @@ -303,14 +303,13 @@ function generateTests(prefix, apiCreator) { }); test(`${prefix} circular references on assertions do not break process.send`, t => { - t.plan(2); + t.plan(1); const api = apiCreator(); return api.run([path.join(__dirname, 'fixture/circular-reference-on-assertion.js')]) .then(result => { t.is(result.failCount, 1); - t.match(result.errors[0].error.message, /'c'.*?'d'/); }); }); @@ -692,34 +691,6 @@ function generateTests(prefix, apiCreator) { }, /^Could not resolve required module 'foo-bar'$/); }); - test(`${prefix} power-assert support`, t => { - t.plan(3); - - const api = apiCreator({ - babelConfig: { - presets: ['react', '@ava/stage-4'] - } - }); - - return api.run([path.join(__dirname, 'fixture/power-assert.js')]) - .then(result => { - t.match( - result.errors[0].error.message, - /t\.true\(a === 'bar'\)\s*\n\s+\|\s*\n\s+"foo"/m - ); - - t.match( - result.errors[1].error.message, - /with message\s+t\.true\(a === 'foo', 'with message'\)\s*\n\s+\|\s*\n\s+"bar"/m - ); - - t.match( - result.errors[2].error.message, - /t\.true\(
=== \)/m - ); - }); - }); - test(`${prefix} caching is enabled by default`, t => { t.plan(3); rimraf.sync(path.join(__dirname, 'fixture/caching/node_modules')); diff --git a/test/assert.js b/test/assert.js index 4b6cf5f1e..e0abdf307 100644 --- a/test/assert.js +++ b/test/assert.js @@ -331,13 +331,13 @@ test('.deepEqual()', t => { t.throws(() => { assert.deepEqual([['a', 'b'], 'c'], [['a', 'b'], 'd']); - }, / 'c' ].*? 'd' ]/); + }); t.throws(() => { const circular = ['a', 'b']; circular.push(circular); assert.deepEqual([circular, 'c'], [circular, 'd']); - }, / 'c' ].*? 'd' ]/); + }); t.end(); }); @@ -515,23 +515,47 @@ test('snapshot makes a snapshot using a library and global options', t => { t.end(); }); -test('if snapshot fails, prints a message', t => { +test('snapshot handles jsx tree', t => { const saveSpy = sinon.spy(); const state = {save: saveSpy}; const stateGetter = sinon.stub().returns(state); - const messageStub = sinon.stub().returns('message'); - const matchStub = sinon.stub().returns({ - pass: false, - message: messageStub + const matchStub = sinon.stub().returns({pass: true}); + + assert.title = 'Test name'; + + t.plan(5); + + t.doesNotThrow(() => { + const tree = { + type: 'h1', + children: ['Hello'], + props: {} + }; + + Object.defineProperty(tree, '$$typeof', {value: Symbol.for('react.test.json')}); + + assert._snapshot(tree, undefined, matchStub, stateGetter); }); - t.plan(2); + t.ok(stateGetter.called); + + const savedTree = JSON.parse(matchStub.firstCall.args[0]); + t.deepEqual(savedTree, { + __ava_react_jsx: { // eslint-disable-line camelcase + type: 'h1', + children: ['Hello'], + props: {} + } + }); - t.throws(() => { - assert._snapshot('tree', undefined, matchStub, stateGetter); + t.match(matchStub.firstCall.thisValue, { + currentTestName: 'Test name', + snapshotState: state }); - t.ok(messageStub.calledOnce); + t.ok(saveSpy.calledOnce); + + delete assert.title; t.end(); }); diff --git a/test/code-excerpt.js b/test/code-excerpt.js new file mode 100644 index 000000000..4608a9a39 --- /dev/null +++ b/test/code-excerpt.js @@ -0,0 +1,62 @@ +'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('truncate lines', t => { + const path = tempWrite.sync([ + 'function a() {', + '\talert();', + '}' + ].join('\n')); + + const excerpt = codeExcerpt(path, 2, {maxWidth: 14}); + const expected = [ + ` ${chalk.grey('1:')} functio…`, + 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/extract-stack.js b/test/extract-stack.js new file mode 100644 index 000000000..362e825fa --- /dev/null +++ b/test/extract-stack.js @@ -0,0 +1,36 @@ +'use strict'; +const test = require('tap').test; +const extractStack = require('../lib/extract-stack'); + +test('strip error message', t => { + const stack = [ + 'error message', + 'Test.t (test.js:1:1)' + ].join('\n'); + + t.is(extractStack(stack), 'Test.t (test.js:1:1)'); + t.end(); +}); + +test('strip multiline error message', t => { + const stack = [ + 'error message', + 'with multiple', + 'lines', + '', + 'Test.t (test.js:1:1)' + ].join('\n'); + + t.is(extractStack(stack), 'Test.t (test.js:1:1)'); + t.end(); +}); + +test('strip beginning whitespace from stack', t => { + const stack = [ + 'error message', + ' Test.t (test.js:1:1)' + ].join('\n'); + + t.is(extractStack(stack), 'Test.t (test.js:1:1)'); + t.end(); +}); diff --git a/test/format-assert-error.js b/test/format-assert-error.js new file mode 100644 index 000000000..e5a7d751a --- /dev/null +++ b/test/format-assert-error.js @@ -0,0 +1,95 @@ +'use strict'; +const indentString = require('indent-string'); +const prettyFormat = require('@ava/pretty-format'); +const chalk = require('chalk'); +const test = require('tap').test; +const format = require('../lib/format-assert-error'); + +chalk.enabled = true; + +test('render statements', t => { + const err = { + statements: JSON.stringify([ + ['actual.a[0]', prettyFormat(1)], + ['actual.a', prettyFormat([1])], + ['actual', prettyFormat({a: [1]})] + ]) + }; + + t.is(format(err), [ + `actual.a[0]\n${chalk.grey('=>')} ${prettyFormat(1)}`, + `actual.a\n${chalk.grey('=>')} ${prettyFormat([1])}`, + `actual\n${chalk.grey('=>')} ${prettyFormat({a: [1]})}` + ].join('\n\n') + '\n'); + t.end(); +}); + +test('diff objects', t => { + const err = { + actual: prettyFormat({a: 1}), + expected: prettyFormat({a: 2}), + actualType: 'object', + expectedType: 'object' + }; + + t.is(format(err), [ + 'Difference:\n', + ' Object {', + `${chalk.red('-')} a: 1,`, + `${chalk.green('+')} a: 2,`, + ' }', + ' ' + ].join('\n')); + t.end(); +}); + +test('diff arrays', t => { + const err = { + actual: prettyFormat([1]), + expected: prettyFormat([2]), + actualType: 'array', + expectedType: 'array' + }; + + t.is(format(err), [ + 'Difference:\n', + ' Array [', + `${chalk.red('-')} 1,`, + `${chalk.green('+')} 2,`, + ' ]', + ' ' + ].join('\n')); + t.end(); +}); + +test('diff strings', t => { + const err = { + actual: 'abc', + expected: 'abd', + actualType: 'string', + expectedType: 'string' + }; + + t.is(format(err), [ + 'Difference:\n', + `ab${chalk.bgRed.black('c')}${chalk.bgGreen.black('d')}\n` + ].join('\n')); + t.end(); +}); + +test('diff different types', t => { + const err = { + actual: prettyFormat([1, 2, 3]), + expected: prettyFormat({a: 1, b: 2, c: 3}), + actualType: 'array', + expectedType: 'object' + }; + + t.is(format(err), [ + 'Actual:\n', + `${indentString(err.actual, 2)}\n`, + 'Expected:\n', + `${indentString(err.expected, 2)}\n` + ].join('\n')); + t.end(); +}); diff --git a/test/reporters/mini.js b/test/reporters/mini.js index 90512cac5..202bd42bd 100644 --- a/test/reporters/mini.js +++ b/test/reporters/mini.js @@ -1,4 +1,8 @@ 'use strict'; +var path = require('path'); +var indentString = require('indent-string'); +var tempWrite = require('temp-write'); +var flatten = require('arr-flatten'); var chalk = require('chalk'); var sinon = require('sinon'); var test = require('tap').test; @@ -10,10 +14,13 @@ var _miniReporter = require('../../lib/reporters/mini'); var beautifyStack = require('../../lib/beautify-stack'); var colors = require('../../lib/colors'); var compareLineOutput = require('../helper/compare-line-output'); +var formatAssertError = require('../../lib/format-assert-error'); +var codeExcerpt = require('../../lib/code-excerpt'); chalk.enabled = true; var graySpinner = chalk.gray.dim(process.platform === 'win32' ? '-' : '⠋'); +var stackLineRegex = /.+ \(.+:[0-9]+:[0-9]+\)/; // Needed because tap doesn't emulate a tty environment and thus this is // undefined, making `cli-truncate` append '...' to test titles @@ -224,7 +231,7 @@ test('results with passing known failure tests', function (t) { '\n ' + chalk.green('1 passed'), ' ' + chalk.red('1 known failure'), '', - ' ' + chalk.white('known failure'), + ' ' + chalk.bold.white('known failure'), '\n' ].join('\n'); @@ -300,12 +307,12 @@ test('results with passing tests and rejections', function (t) { ' ' + chalk.green('1 passed'), ' ' + chalk.red('1 rejection'), '', - ' ' + chalk.white('Unhandled Rejection'), + ' ' + chalk.bold.white('Unhandled Rejection'), /Error: failure/, /test\/reporters\/mini\.js/, compareLineOutput.SKIP_UNTIL_EMPTY_LINE, '', - ' ' + chalk.white('Unhandled Rejection'), + ' ' + chalk.bold.white('Unhandled Rejection'), ' ' + colors.stack('stack line with trailing whitespace') ]); t.end(); @@ -333,7 +340,7 @@ test('results with passing tests and exceptions', function (t) { ' ' + chalk.green('1 passed'), ' ' + chalk.red('2 exceptions'), '', - ' ' + chalk.white('Uncaught Exception'), + ' ' + chalk.bold.white('Uncaught Exception'), /Error: failure/, /test\/reporters\/mini\.js/, compareLineOutput.SKIP_UNTIL_EMPTY_LINE, @@ -344,13 +351,92 @@ test('results with passing tests and exceptions', function (t) { }); test('results with errors', function (t) { - var reporter = miniReporter(); + var err1 = new Error('failure one'); + err1.stack = beautifyStack(err1.stack); + var err1Path = tempWrite.sync('a();'); + err1.source = {file: path.basename(err1Path), line: 1}; + err1.showOutput = true; + err1.actual = JSON.stringify('abc'); + err1.actualType = 'string'; + err1.expected = JSON.stringify('abd'); + err1.expectedType = 'string'; + + var err2 = new Error('failure two'); + err2.stack = 'error message\nTest.fn (test.js:1:1)\n'; + var err2Path = tempWrite.sync('b();'); + err2.source = {file: path.basename(err2Path), line: 1}; + err2.showOutput = true; + err2.actual = JSON.stringify([1]); + err2.actualType = 'array'; + err2.expected = JSON.stringify([2]); + err2.expectedType = 'array'; + + var reporter = miniReporter({basePath: path.dirname(err1Path)}); reporter.failCount = 1; + var runStatus = { + errors: [{ + title: 'failed one', + error: err1 + }, { + title: 'failed two', + error: err2 + }] + }; + + var output = reporter.finish(runStatus); + + compareLineOutput(t, output, flatten([ + '', + ' ' + chalk.red('1 failed'), + '', + ' ' + chalk.bold.white('failed one'), + ' ' + chalk.grey(`${err1.source.file}:${err1.source.line}`), + '', + indentString(codeExcerpt(err1Path, err1.source.line), 2).split('\n'), + '', + indentString(formatAssertError(err1), 2).split('\n'), + /failure one/, + '', + stackLineRegex, + compareLineOutput.SKIP_UNTIL_EMPTY_LINE, + '', + '', + '', + ' ' + chalk.bold.white('failed two'), + ' ' + chalk.grey(`${err2.source.file}:${err2.source.line}`), + '', + indentString(codeExcerpt(err2Path, err2.source.line), 2).split('\n'), + '', + indentString(formatAssertError(err2), 2).split('\n'), + /failure two/, + '', + stackLineRegex + ])); + t.end(); +}); + +test('results with errors and disabled code excerpts', function (t) { var err1 = new Error('failure one'); err1.stack = beautifyStack(err1.stack); + err1.showOutput = true; + err1.actual = JSON.stringify('abc'); + err1.actualType = 'string'; + err1.expected = JSON.stringify('abd'); + err1.expectedType = 'string'; + var err2 = new Error('failure two'); - err2.stack = 'first line is stripped\nstack line with trailing whitespace\t\n'; + err2.stack = 'error message\nTest.fn (test.js:1:1)\n'; + var err2Path = tempWrite.sync('b();'); + err2.source = {file: path.basename(err2Path), line: 1}; + err2.showOutput = true; + err2.actual = JSON.stringify([1]); + err2.actualType = 'array'; + err2.expected = JSON.stringify([2]); + err2.expectedType = 'array'; + + var reporter = miniReporter({basePath: path.dirname(err2Path)}); + reporter.failCount = 1; var runStatus = { errors: [{ @@ -364,22 +450,159 @@ test('results with errors', function (t) { var output = reporter.finish(runStatus); - var expectedStack = colors.error(' failure two\n') + colors.errorStack('stack line with trailing whitespace'); - expectedStack = expectedStack.split('\n'); + compareLineOutput(t, output, flatten([ + '', + ' ' + chalk.red('1 failed'), + '', + ' ' + chalk.bold.white('failed one'), + '', + indentString(formatAssertError(err1), 2).split('\n'), + /failure one/, + '', + stackLineRegex, + compareLineOutput.SKIP_UNTIL_EMPTY_LINE, + '', + '', + '', + ' ' + chalk.bold.white('failed two'), + ' ' + chalk.grey(`${err2.source.file}:${err2.source.line}`), + '', + indentString(codeExcerpt(err2Path, err2.source.line), 2).split('\n'), + '', + indentString(formatAssertError(err2), 2).split('\n'), + /failure two/, + '', + stackLineRegex + ])); + t.end(); +}); + +test('results with errors and broken code excerpts', function (t) { + var err1 = new Error('failure one'); + err1.stack = beautifyStack(err1.stack); + var err1Path = tempWrite.sync('a();'); + err1.source = {file: path.basename(err1Path), line: 10}; + err1.showOutput = true; + err1.actual = JSON.stringify('abc'); + err1.actualType = 'string'; + err1.expected = JSON.stringify('abd'); + err1.expectedType = 'string'; - compareLineOutput(t, output, [ + var err2 = new Error('failure two'); + err2.stack = 'error message\nTest.fn (test.js:1:1)\n'; + var err2Path = tempWrite.sync('b();'); + err2.source = {file: path.basename(err2Path), line: 1}; + err2.showOutput = true; + err2.actual = JSON.stringify([1]); + err2.actualType = 'array'; + err2.expected = JSON.stringify([2]); + err2.expectedType = 'array'; + + var reporter = miniReporter({basePath: path.dirname(err2Path)}); + reporter.failCount = 1; + + var runStatus = { + errors: [{ + title: 'failed one', + error: err1 + }, { + title: 'failed two', + error: err2 + }] + }; + + var output = reporter.finish(runStatus); + + compareLineOutput(t, output, flatten([ '', ' ' + chalk.red('1 failed'), '', - ' ' + chalk.white('failed one'), - /failure/, - /test\/reporters\/mini\.js/, + ' ' + chalk.bold.white('failed one'), + ' ' + chalk.grey(`${err1.source.file}:${err1.source.line}`), + '', + indentString(formatAssertError(err1), 2).split('\n'), + /failure one/, + '', + stackLineRegex, compareLineOutput.SKIP_UNTIL_EMPTY_LINE, '', - ' ' + chalk.white('failed two') - ].concat( - expectedStack - )); + '', + '', + ' ' + chalk.bold.white('failed two'), + ' ' + chalk.grey(`${err2.source.file}:${err2.source.line}`), + '', + indentString(codeExcerpt(err2Path, err2.source.line), 2).split('\n'), + '', + indentString(formatAssertError(err2), 2).split('\n'), + /failure two/, + '', + stackLineRegex + ])); + t.end(); +}); + +test('results with errors and disabled assert output', function (t) { + var err1 = new Error('failure one'); + err1.stack = beautifyStack(err1.stack); + var err1Path = tempWrite.sync('a();'); + err1.source = {file: path.basename(err1Path), line: 1}; + err1.showOutput = false; + err1.actual = JSON.stringify('abc'); + err1.actualType = 'string'; + err1.expected = JSON.stringify('abd'); + err1.expectedType = 'string'; + + var err2 = new Error('failure two'); + err2.stack = 'error message\nTest.fn (test.js:1:1)\n'; + var err2Path = tempWrite.sync('b();'); + err2.source = {file: path.basename(err2Path), line: 1}; + err2.showOutput = true; + err2.actual = JSON.stringify([1]); + err2.actualType = 'array'; + err2.expected = JSON.stringify([2]); + err2.expectedType = 'array'; + + var reporter = miniReporter({basePath: path.dirname(err1Path)}); + reporter.failCount = 1; + + var runStatus = { + errors: [{ + title: 'failed one', + error: err1 + }, { + title: 'failed two', + error: err2 + }] + }; + + var output = reporter.finish(runStatus); + + compareLineOutput(t, output, flatten([ + '', + ' ' + chalk.red('1 failed'), + '', + ' ' + chalk.bold.white('failed one'), + ' ' + chalk.grey(`${err1.source.file}:${err1.source.line}`), + '', + indentString(codeExcerpt(err1Path, err1.source.line), 2).split('\n'), + '', + /failure one/, + '', + stackLineRegex, + compareLineOutput.SKIP_UNTIL_EMPTY_LINE, + '', + '', + '', + ' ' + chalk.bold.white('failed two'), + ' ' + chalk.grey(`${err2.source.file}:${err2.source.line}`), + '', + indentString(codeExcerpt(err2Path, err2.source.line), 2).split('\n'), + '', + indentString(formatAssertError(err2), 2).split('\n'), + /failure two/, + '', + stackLineRegex + ])); t.end(); }); @@ -398,19 +621,16 @@ test('results with unhandled errors', function (t) { }; var output = reporter.finish(runStatus); - var expectedStack = colors.error(' failure two\n') + colors.errorStack('stack line with trailing whitespace'); - compareLineOutput(t, output, [ '', ' ' + chalk.red('2 failed'), '', - ' ' + chalk.white('failed one'), - /failure/, - /test\/reporters\/mini\.js/, - compareLineOutput.SKIP_UNTIL_EMPTY_LINE, + ' ' + chalk.bold.white('failed one'), + '', + /failure one/, '', - '' - ].concat(expectedStack.split('\n'))); + stackLineRegex + ]); t.end(); }); @@ -595,7 +815,25 @@ test('returns description based on error itself if no stack available', function var expectedOutput = [ '\n ' + colors.error('1 exception'), '\n ' + colors.title('Uncaught Exception'), - ' ' + colors.stack(JSON.stringify({error: err1})), + ' ' + colors.stack('Threw non-error: ' + JSON.stringify({error: err1})), + '\n\n' + ].join('\n'); + t.is(actualOutput, expectedOutput); + t.end(); +}); + +test('shows "non-error" hint for invalid throws', function (t) { + var reporter = miniReporter(); + reporter.exceptionCount = 1; + var err = {type: 'exception', message: 'function fooFn() {}', stack: 'function fooFn() {}'}; + var runStatus = { + errors: [err] + }; + var actualOutput = reporter.finish(runStatus); + var expectedOutput = [ + '\n ' + colors.error('1 exception'), + '\n ' + colors.title('Uncaught Exception'), + ' ' + colors.stack('Threw non-error: function fooFn() {}'), '\n\n' ].join('\n'); t.is(actualOutput, expectedOutput); @@ -670,4 +908,3 @@ test('results when hasExclusive is enabled, but there are multiple remaining tes t.is(actualOutput, expectedOutput); t.end(); }); - diff --git a/test/reporters/verbose.js b/test/reporters/verbose.js index e80999fc9..2ff53d2fc 100644 --- a/test/reporters/verbose.js +++ b/test/reporters/verbose.js @@ -1,4 +1,8 @@ 'use strict'; +var path = require('path'); +var indentString = require('indent-string'); +var flatten = require('arr-flatten'); +var tempWrite = require('temp-write'); var figures = require('figures'); var chalk = require('chalk'); var sinon = require('sinon'); @@ -9,18 +13,18 @@ var beautifyStack = require('../../lib/beautify-stack'); var colors = require('../../lib/colors'); var verboseReporter = require('../../lib/reporters/verbose'); var compareLineOutput = require('../helper/compare-line-output'); +var formatAssertError = require('../../lib/format-assert-error'); +var codeExcerpt = require('../../lib/code-excerpt'); chalk.enabled = true; -// Tap doesn't emulate a tty environment and thus process.stdout.columns is -// undefined. Expect an 80 character wide line to be rendered. -var fullWidthLine = chalk.gray.dim(repeating('\u2500', 80)); +var stackLineRegex = /.+ \(.+:[0-9]+:[0-9]+\)/; lolex.install(new Date(2014, 11, 19, 17, 19, 12, 200).getTime(), ['Date']); var time = ' ' + chalk.grey.dim('[17:19:12]'); -function createReporter() { - var reporter = verboseReporter(); +function createReporter(options) { + var reporter = verboseReporter(options); return reporter; } @@ -246,7 +250,7 @@ test('results with passing known failure tests', function (t) { ' ' + chalk.red('1 known failure'), '', '', - ' ' + chalk.red('1. known failure'), + ' ' + chalk.red('known failure'), '' ].join('\n'); @@ -349,10 +353,86 @@ test('results with passing tests, rejections and exceptions', function (t) { test('results with errors', function (t) { var error1 = new Error('error one message'); error1.stack = beautifyStack(error1.stack); + const err1Path = tempWrite.sync('a()'); + error1.source = {file: path.basename(err1Path), line: 1}; + error1.showOutput = true; + error1.actual = JSON.stringify('abc'); + error1.actualType = 'string'; + error1.expected = JSON.stringify('abd'); + error1.expectedType = 'string'; + var error2 = new Error('error two message'); - error2.stack = 'stack line with trailing whitespace\t\n'; + error2.stack = 'error message\nTest.fn (test.js:1:1)\n'; + const err2Path = tempWrite.sync('b()'); + error2.source = {file: path.basename(err2Path), line: 1}; + error2.showOutput = true; + error2.actual = JSON.stringify([1]); + error2.actualType = 'array'; + error2.expected = JSON.stringify([2]); + error2.expectedType = 'array'; + + var reporter = createReporter({basePath: path.dirname(err1Path)}); + var runStatus = createRunStatus(); + runStatus.failCount = 1; + runStatus.tests = [{ + title: 'fail one', + error: error1 + }, { + title: 'fail two', + error: error2 + }]; - var reporter = createReporter(); + var output = reporter.finish(runStatus); + compareLineOutput(t, output, flatten([ + '', + ' ' + chalk.red('1 test failed') + time, + '', + ' ' + chalk.bold.white('fail one'), + ' ' + chalk.grey(`${error1.source.file}:${error1.source.line}`), + '', + indentString(codeExcerpt(err1Path, error1.source.line), 2).split('\n'), + '', + indentString(formatAssertError(error1), 2).split('\n'), + /error one message/, + '', + stackLineRegex, + compareLineOutput.SKIP_UNTIL_EMPTY_LINE, + '', + '', + '', + ' ' + chalk.bold.white('fail two'), + ' ' + chalk.grey(`${error2.source.file}:${error2.source.line}`), + '', + indentString(codeExcerpt(err2Path, error2.source.line), 2).split('\n'), + '', + indentString(formatAssertError(error2), 2).split('\n'), + /error two message/, + '', + stackLineRegex + ])); + t.end(); +}); + +test('results with errors and disabled code excerpts', function (t) { + var error1 = new Error('error one message'); + error1.stack = beautifyStack(error1.stack); + error1.showOutput = true; + error1.actual = JSON.stringify('abc'); + error1.actualType = 'string'; + error1.expected = JSON.stringify('abd'); + error1.expectedType = 'string'; + + var error2 = new Error('error two message'); + error2.stack = 'error message\nTest.fn (test.js:1:1)\n'; + const err2Path = tempWrite.sync('b()'); + error2.source = {file: path.basename(err2Path), line: 1}; + error2.showOutput = true; + error2.actual = JSON.stringify([1]); + error2.actualType = 'array'; + error2.expected = JSON.stringify([2]); + error2.expectedType = 'array'; + + var reporter = createReporter({basePath: path.dirname(err2Path)}); var runStatus = createRunStatus(); runStatus.failCount = 1; runStatus.tests = [{ @@ -364,20 +444,153 @@ test('results with errors', function (t) { }]; var output = reporter.finish(runStatus); - compareLineOutput(t, output, [ + compareLineOutput(t, output, flatten([ '', ' ' + chalk.red('1 test failed') + time, '', + ' ' + chalk.bold.white('fail one'), + '', + indentString(formatAssertError(error1), 2).split('\n'), + /error one message/, '', - ' ' + chalk.red('1. fail one'), - /Error: error one message/, - /test\/reporters\/verbose\.js/, + stackLineRegex, compareLineOutput.SKIP_UNTIL_EMPTY_LINE, '', '', - ' ' + chalk.red('2. fail two'), - ' ' + colors.stack('stack line with trailing whitespace') - ]); + '', + ' ' + chalk.bold.white('fail two'), + ' ' + chalk.grey(`${error2.source.file}:${error2.source.line}`), + '', + indentString(codeExcerpt(err2Path, error2.source.line), 2).split('\n'), + '', + indentString(formatAssertError(error2), 2).split('\n'), + /error two message/, + '', + stackLineRegex + ])); + t.end(); +}); + +test('results with errors and disabled code excerpts', function (t) { + var error1 = new Error('error one message'); + error1.stack = beautifyStack(error1.stack); + var err1Path = tempWrite.sync('a();'); + error1.source = {file: path.basename(err1Path), line: 10}; + error1.showOutput = true; + error1.actual = JSON.stringify('abc'); + error1.actualType = 'string'; + error1.expected = JSON.stringify('abd'); + error1.expectedType = 'string'; + + var error2 = new Error('error two message'); + error2.stack = 'error message\nTest.fn (test.js:1:1)\n'; + const err2Path = tempWrite.sync('b()'); + error2.source = {file: path.basename(err2Path), line: 1}; + error2.showOutput = true; + error2.actual = JSON.stringify([1]); + error2.actualType = 'array'; + error2.expected = JSON.stringify([2]); + error2.expectedType = 'array'; + + var reporter = createReporter({basePath: path.dirname(err2Path)}); + var runStatus = createRunStatus(); + runStatus.failCount = 1; + runStatus.tests = [{ + title: 'fail one', + error: error1 + }, { + title: 'fail two', + error: error2 + }]; + + var output = reporter.finish(runStatus); + compareLineOutput(t, output, flatten([ + '', + ' ' + chalk.red('1 test failed') + time, + '', + ' ' + chalk.bold.white('fail one'), + ' ' + chalk.grey(`${error1.source.file}:${error1.source.line}`), + '', + indentString(formatAssertError(error1), 2).split('\n'), + /error one message/, + '', + stackLineRegex, + compareLineOutput.SKIP_UNTIL_EMPTY_LINE, + '', + '', + '', + ' ' + chalk.bold.white('fail two'), + ' ' + chalk.grey(`${error2.source.file}:${error2.source.line}`), + '', + indentString(codeExcerpt(err2Path, error2.source.line), 2).split('\n'), + '', + indentString(formatAssertError(error2), 2).split('\n'), + /error two message/, + '', + stackLineRegex + ])); + t.end(); +}); + +test('results with errors and disabled assert output', function (t) { + var error1 = new Error('error one message'); + error1.stack = beautifyStack(error1.stack); + const err1Path = tempWrite.sync('a();'); + error1.source = {file: path.basename(err1Path), line: 1}; + error1.showOutput = false; + error1.actual = JSON.stringify('abc'); + error1.actualType = 'string'; + error1.expected = JSON.stringify('abd'); + error1.expectedType = 'string'; + + var error2 = new Error('error two message'); + error2.stack = 'error message\nTest.fn (test.js:1:1)\n'; + const err2Path = tempWrite.sync('b();'); + error2.source = {file: path.basename(err2Path), line: 1}; + error2.showOutput = true; + error2.actual = JSON.stringify([1]); + error2.actualType = 'array'; + error2.expected = JSON.stringify([2]); + error2.expectedType = 'array'; + + var reporter = createReporter({basePath: path.dirname(err1Path)}); + var runStatus = createRunStatus(); + runStatus.failCount = 1; + runStatus.tests = [{ + title: 'fail one', + error: error1 + }, { + title: 'fail two', + error: error2 + }]; + + var output = reporter.finish(runStatus); + compareLineOutput(t, output, flatten([ + '', + ' ' + chalk.red('1 test failed') + time, + '', + ' ' + chalk.bold.white('fail one'), + ' ' + chalk.grey(`${error1.source.file}:${error1.source.line}`), + '', + indentString(codeExcerpt(err1Path, error1.source.line), 2).split('\n'), + '', + /error one message/, + '', + stackLineRegex, + compareLineOutput.SKIP_UNTIL_EMPTY_LINE, + '', + '', + '', + ' ' + chalk.bold.white('fail two'), + ' ' + chalk.grey(`${error2.source.file}:${error2.source.line}`), + '', + indentString(codeExcerpt(err2Path, error2.source.line), 2).split('\n'), + '', + indentString(formatAssertError(error2), 2).split('\n'), + /error two message/, + '', + stackLineRegex + ])); t.end(); }); @@ -484,8 +697,12 @@ test('results with 2 previous failures', function (t) { test('full-width line when sectioning', function (t) { var reporter = createReporter(); + const prevColumns = process.stdout.columns; + process.stdout.columns = 80; var output = reporter.section(); - t.is(output, fullWidthLine); + process.stdout.columns = prevColumns; + + t.is(output, chalk.gray.dim(repeating('\u2500', 80))); t.end(); }); diff --git a/test/serialize-error.js b/test/serialize-error.js new file mode 100644 index 000000000..b44b059e1 --- /dev/null +++ b/test/serialize-error.js @@ -0,0 +1,95 @@ +'use strict'; + +const prettyFormat = require('@ava/pretty-format'); +const reactTestPlugin = require('@ava/pretty-format/plugins/ReactTestComponent'); +const test = require('tap').test; +const beautifyStack = require('../lib/beautify-stack'); +const serialize = require('../lib/serialize-error'); + +function serializeValue(value) { + return prettyFormat(value, { + plugins: [reactTestPlugin], + 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(typeof serializedErr.source.file, 'string'); + 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(); +});