diff --git a/Gruntfile.js b/Gruntfile.js index f773bee..4897d54 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -26,6 +26,9 @@ module.exports = function (grunt) { demo: { configFile: 'demo/karma.conf.js' }, + mocha: { + configFile: 'demo/karma.mocha.conf.js' + }, fast: { configFile: 'demo/karma.conf.js', browsers: ['PhantomJS'], @@ -196,6 +199,7 @@ module.exports = function (grunt) { grunt.registerTask('colors', ['copy:demo', 'karma:colors']); grunt.registerTask('duplicate', ['copy:demo', 'karma:duplicate']); grunt.registerTask('reload', ['copy:demo', 'karma:reload']); + grunt.registerTask('mocha', ['copy:demo', 'karma:mocha']); grunt.registerTask('demo', [ 'copy:demo', 'karma:singleBrowser', @@ -207,6 +211,7 @@ module.exports = function (grunt) { 'karma:noColors', 'karma:colors', 'karma:duplicate', + 'karma:mocha', 'karma:reload' ]); diff --git a/LICENSE b/LICENSE index ca26b54..601f996 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2013-2015 Litixsoft GmbH +Copyright (C) 2013-2016 Litixsoft GmbH Licensed under the MIT license. Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index 9976301..5f2625e 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,46 @@ module.exports = function(config) { }; ``` +### showDiff +**Type:** String | Boolean + +Shows a diff output. Is disabled by default. All credits to the contributors of [mocha](https://github.com/mochajs/mocha), since the diff logic is used from there and customized for this module. + +![screenshot](demo/diff.png) + +Currently only works with karma-mocha >= v0.2.2 Not supported for karma-jasmine since the additional properties needed to render the diff are not supported in jasmine yet. + +**Possible Values:** + +Value | Description +------ | ----------- +`true` | prints each diff in its own line, same as `'unified'` +`'unified'` | prints each diff in its own line +`'inline'` | prints diffs inline + +```js +// karma.conf.js +module.exports = function(config) { + config.set({ + frameworks: ['mocha', 'chai'], + + // reporters configuration + reporters: ['mocha'], + + // reporter options + mochaReporter: { + showDiff: true + }, + + plugins: [ + 'karma-chai', + 'karma-mocha', + 'karma-mocha-reporter' + ] + }); +}; +``` + ### divider **Type:** String @@ -164,7 +204,7 @@ In lieu of a formal styleguide take care to maintain the existing coding style. You can preview your changes by running: - $ grunt demo --force + $ npm run demo ## Release History ### v1.1.6 diff --git a/demo/diff.png b/demo/diff.png new file mode 100644 index 0000000..55400ab Binary files /dev/null and b/demo/diff.png differ diff --git a/demo/karma.mocha.conf.js b/demo/karma.mocha.conf.js new file mode 100644 index 0000000..43561b9 --- /dev/null +++ b/demo/karma.mocha.conf.js @@ -0,0 +1,73 @@ +module.exports = function (config) { + config.set({ + // base path, that will be used to resolve files and exclude + basePath: '../', + + frameworks: ['mocha', 'chai'], + + // list of files / patterns to load in the browser + files: [ + 'demo/mocha.spec.js' + ], + + mochaReporter: { + showDiff: true + }, + + // use dots reporter, as travis terminal does not support escaping sequences + // possible values: 'dots', 'progress' + // CLI --reporters progress + reporters: ['mocha'], + + // web server port + // CLI --port 9876 + port: 9876, + + // enable / disable colors in the output (reporters and logs) + // CLI --colors --no-colors + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + // CLI --log-level debug + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + // CLI --auto-watch --no-auto-watch + autoWatch: false, + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera + // - Safari (only Mac) + // - PhantomJS + // - IE (only Windows) + // CLI --browsers Chrome,Firefox,Safari + browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], + + // If browser does not capture in given timeout [ms], kill it + // CLI --capture-timeout 5000 + captureTimeout: 20000, + + // Auto run tests on start (when browsers are captured) and exit + // CLI --single-run --no-single-run + singleRun: true, + + // report which specs are slower than 500ms + // CLI --report-slower-than 500 + reportSlowerThan: 500, + + plugins: [ + 'karma-mocha', + 'karma-mocha-reporter', + 'karma-chrome-launcher', + 'karma-firefox-launcher', + 'karma-ie-launcher', + 'karma-safari-launcher', + 'karma-phantomjs-launcher', + 'karma-chai' + ] + }); +}; \ No newline at end of file diff --git a/demo/mocha.spec.js b/demo/mocha.spec.js new file mode 100644 index 0000000..3e46076 --- /dev/null +++ b/demo/mocha.spec.js @@ -0,0 +1,47 @@ +'use strict'; + +describe('A mocha test suite', function () { + + it('should work', function () { + var expected = 'foo'; + var actual = 'foo'; + expect(actual).to.equal(expected); + }); + + it('should show a string diff', function () { + var expected = 'foo'; + var actual = 'foo bar'; + expect(actual).to.equal(expected); + }); + + it('should show an array diff', function () { + var expected = [ + 'foo', + 'bar' + ]; + var actual = [ + 'bar', + 'baz' + ]; + expect(actual).to.deep.equal(expected); + }); + + it('should show an object diff', function () { + var expected = { + foo: 42, + bar: 1764, + baz: { + qux: 'bleep', + norf: 'bloop' + } + }; + var actual = { + bar: 1764, + baz: { + norf: 'bloop' + }, + random: true + }; + expect(actual).to.deep.equal(expected); + }); +}); \ No newline at end of file diff --git a/index.js b/index.js index 7d66d65..0a955e0 100644 --- a/index.js +++ b/index.js @@ -50,6 +50,9 @@ var MochaReporter = function (baseReporterDecorator, formatError, config) { // set color functions config.mochaReporter.colors = config.mochaReporter.colors || {}; + // set diff output + config.mochaReporter.showDiff = config.mochaReporter.showDiff || false; + var colors = { success: { symbol: symbols.success, @@ -69,10 +72,124 @@ var MochaReporter = function (baseReporterDecorator, formatError, config) { } }; + // check if mocha is installed when showDiff is enabled + if (config.mochaReporter.showDiff) { + try { + var mocha = require('mocha'); + var diff = require('diff'); + } catch (e) { + self.write(colors.error.print('Error loading module mocha!\nYou have enabled diff output. That only works with karma-mocha and mocha installed!\nRun the following command in your command line:\n npm install karma-mocha mocha\n')); + return; + } + } + function getLogSymbol (color) { return chalk.enabled ? color.print(color.symbol) : chalk.stripColor(color.symbol); } + /** + * Returns a unified diff between two strings. + * + * @param {Error} err with actual/expected + * @return {string} The diff. + */ + function unifiedDiff (err) { + var indent = ' '; + + function cleanUp (line) { + if (line[0] === '+') { + return indent + colors.success.print(line); + } + if (line[0] === '-') { + return indent + colors.error.print(line); + } + if (line.match(/\@\@/)) { + return null; + } + if (line.match(/\\ No newline/)) { + return null; + } + return indent + line; + } + + function notBlank (line) { + return line !== null; + } + + var msg = diff.createPatch('string', err.actual, err.expected); + var lines = msg.split('\n').splice(4); + return '\n ' + + colors.success.print('+ expected') + ' ' + + colors.error.print('- actual') + + '\n\n' + + lines.map(cleanUp).filter(notBlank).join('\n'); + } + + /** + * Return a character diff for `err`. + * + * @param {Error} err + * @param {string} type + * @return {string} + */ + function errorDiff (err, type) { + var actual = err.actual; + var expected = err.expected; + return diff['diff' + type](actual, expected).map(function (str) { + if (str.added) { + return colors.success.print(str.value); + } + if (str.removed) { + return colors.error.print(str.value); + } + return str.value; + }).join(''); + } + + /** + * Pad the given `str` to `len`. + * + * @param {string} str + * @param {string} len + * @return {string} + */ + function pad (str, len) { + str = String(str); + return Array(len - str.length + 1).join(' ') + str; + } + + /** + * Returns an inline diff between 2 strings with coloured ANSI output + * + * @param {Error} err with actual/expected + * @return {string} Diff + */ + function inlineDiff (err) { + var msg = errorDiff(err, 'WordsWithSpace'); + + // linenos + var lines = msg.split('\n'); + if (lines.length > 4) { + var width = String(lines.length).length; + msg = lines.map(function (str, i) { + return pad(++i, width) + ' |' + ' ' + str; + }).join('\n'); + } + + // legend + msg = '\n' + + colors.success.print('expected') + + ' ' + + colors.error.print('actual') + + '\n\n' + + msg + + '\n'; + + // indent + msg = msg.replace(/^/gm, ' '); + return msg; + } + /** * Returns a formatted time interval * @@ -181,9 +298,42 @@ var MochaReporter = function (baseReporterDecorator, formatError, config) { // add the error log in error color item.log = item.log || []; - item.log.forEach(function (err) { - line += colors.error.print(formatError(err, repeatString(' ', depth))); - }); + // print diff + if (config.mochaReporter.showDiff && item.assertionErrors) { + var log = item.log[0].split('\n'); + var errorMessage = log.splice(0, 1)[0]; + + // print error message before diff + line += colors.error.print(repeatString(' ', depth) + errorMessage + '\n'); + + var expected = item.assertionErrors[0].expected; + var actual = item.assertionErrors[0].actual; + var utils = mocha.utils; + var err = { + actual: actual, + expected: expected + }; + + // ensure that actual and expected are strings + if (!(utils.isString(actual) && utils.isString(expected))) { + err.actual = utils.stringify(actual); + err.expected = utils.stringify(expected); + } + + // create diff + var diff = config.mochaReporter.showDiff === 'inline' ? inlineDiff(err) : unifiedDiff(err); + + line += diff + '\n'; + + // print formatted stack trace after diff + log.forEach(function (err) { + line += colors.error.print(formatError(err)); + }); + } else { + item.log.forEach(function (err) { + line += colors.error.print(formatError(err, repeatString(' ', depth))); + }); + } } // use write method of baseReporter @@ -220,7 +370,6 @@ var MochaReporter = function (baseReporterDecorator, formatError, config) { * @param {!Number} testCount * @returns {String} */ - function getTestNounFor (testCount) { if (testCount === 1) { return 'test'; @@ -297,6 +446,9 @@ var MochaReporter = function (baseReporterDecorator, formatError, config) { // add error log item.log = result.log; + + // add assertion errors if available (currently in karma-mocha) + item.assertionErrors = result.assertionErrors; } if (config.reportSlowerThan && result.time > config.reportSlowerThan) { diff --git a/package.json b/package.json index 6df11f7..13cbf92 100644 --- a/package.json +++ b/package.json @@ -45,24 +45,29 @@ ], "main": "index.js", "scripts": { - "test": "grunt test" + "test": "grunt test", + "demo": "grunt demo --force" }, "devDependencies": { + "chai": "^3.5.0", "grunt": "^0.4.5", "grunt-contrib-copy": "^0.8.2", "grunt-contrib-jshint": "^1.0.0", "grunt-karma": "^0.12.1", "grunt-shell": "^1.1.2", - "jasmine-core": "2.4.1", + "jasmine-core": "^2.4.1", + "karma-chai": "^0.1.0", "karma-chrome-launcher": "*", "karma-detect-browsers": "^2.0.2", "karma-firefox-launcher": "*", "karma-ie-launcher": "*", "karma-jasmine": "^0.3.7", + "karma-mocha": "^0.2.2", "karma-opera-launcher": "*", "karma-phantomjs-launcher": "1.0.0", "karma-safari-launcher": "*", - "phantomjs-prebuilt": "2.1.4" + "mocha": "^2.4.5", + "phantomjs-prebuilt": "^2.1.4" }, "dependencies": { "chalk": "1.1.1",