From f43006aec9bff54e1d71cd6f7513dd6352d1410c Mon Sep 17 00:00:00 2001 From: Ariel Mashraki Date: Tue, 13 Jan 2015 10:01:42 +0200 Subject: [PATCH] fix(utils.stringify): issue #1229, diff viewer --- lib/reporters/base.js | 3 +- lib/utils.js | 137 +++++++++++++++++++++++++++++-------- test/acceptance/utils.js | 144 ++++++++++++++++++++++++++++++++++----- 3 files changed, 236 insertions(+), 48 deletions(-) diff --git a/lib/reporters/base.js b/lib/reporters/base.js index 97b12ba5c5..b6a230588f 100644 --- a/lib/reporters/base.js +++ b/lib/reporters/base.js @@ -175,7 +175,6 @@ exports.list = function(failures){ if (err.uncaught) { msg = 'Uncaught ' + msg; } - // explicitly show diff if (err.showDiff && sameType(actual, expected)) { @@ -386,7 +385,7 @@ function unifiedDiff(err, escape) { function notBlank(line) { return line != null; } - msg = diff.createPatch('string', err.actual, err.expected); + var msg = diff.createPatch('string', err.actual, err.expected); var lines = msg.split('\n').splice(4); return '\n ' + colorLines('diff added', '+ expected') + ' ' diff --git a/lib/utils.js b/lib/utils.js index 7059b9de7f..f300cd9516 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -127,7 +127,7 @@ exports.filter = function(arr, fn){ exports.keys = Object.keys || function(obj) { var keys = [] - , has = Object.prototype.hasOwnProperty // for `window` on <=IE8 + , has = Object.prototype.hasOwnProperty; // for `window` on <=IE8 for (var key in obj) { if (has.call(obj, key)) { @@ -157,6 +157,26 @@ exports.watch = function(files, fn){ }); }; +/** + * Array.isArray (<=IE8) + * + * @param {Object} obj + * @return {Boolean} + * @api private + */ +var isArray = Array.isArray || function (obj) { + return '[object Array]' == {}.toString.call(obj); +}; + +/** + * @description + * Buffer.prototype.toJSON polyfill + * @type {Function} + */ +Buffer.prototype.toJSON = Buffer.prototype.toJSON || function () { + return Array.prototype.slice.call(this, 0); +}; + /** * Ignored files. */ @@ -179,15 +199,15 @@ exports.files = function(dir, ext, ret){ var re = new RegExp('\\.(' + ext.join('|') + ')$'); fs.readdirSync(dir) - .filter(ignored) - .forEach(function(path){ - path = join(dir, path); - if (fs.statSync(path).isDirectory()) { - exports.files(path, ext, ret); - } else if (path.match(re)) { - ret.push(path); - } - }); + .filter(ignored) + .forEach(function(path){ + path = join(dir, path); + if (fs.statSync(path).isDirectory()) { + exports.files(path, ext, ret); + } else if (path.match(re)) { + ret.push(path); + } + }); return ret; }; @@ -364,30 +384,94 @@ exports.type = function type(value) { */ exports.stringify = function(value) { - var prop, - type = exports.type(value); - - if (type === 'null' || type === 'undefined') { - return '[' + type + ']'; - } - - if (type === 'date') { - return '[Date: ' + value.toISOString() + ']'; - } + var type = exports.type(value); if (!~exports.indexOf(['object', 'array', 'function'], type)) { - return value.toString(); + if(type != 'buffer') { + return jsonStringify(value); + } + var json = value.toJSON(); + // Based on the toJSON result + return jsonStringify(json.data && json.type ? json.data : json, 2) + .replace(/,(\n|$)/g, '$1'); } - for (prop in value) { + for (var prop in value) { if (Object.prototype.hasOwnProperty.call(value, prop)) { - return JSON.stringify(exports.canonicalize(value), null, 2).replace(/,(\n|$)/g, '$1'); + return jsonStringify(exports.canonicalize(value), 2).replace(/,(\n|$)/g, '$1'); } } return emptyRepresentation(value, type); }; +/** + * @description + * like JSON.stringify but more sense. + * @param {Object} object + * @param {Number=} spaces + * @param {number=} depth + * @returns {*} + * @private + */ +function jsonStringify(object, spaces, depth) { + if(typeof spaces == 'undefined') return _stringify(object); // primitive types + + depth = depth || 1; + var space = spaces * depth + , str = isArray(object) ? '[' : '{' + , end = isArray(object) ? ']' : '}' + , length = object.length || exports.keys(object).length + , repeat = function(s, n) { return new Array(n).join(s); }; // `.repeat()` polyfill + + function _stringify(val) { + switch (exports.type(val)) { + case 'null': + case 'undefined': + val = '[' + val + ']'; + break; + case 'array': + case 'object': + val = jsonStringify(val, spaces, depth + 1); + break; + case 'boolean': + case 'regexp': + case 'number': + val = val === 0 && (1/val) === -Infinity // `-0` + ? '-0' + : val.toString(); + break; + case 'date': + val = '[Date: ' + val.toISOString() + ']'; + break; + case 'buffer': + var json = val.toJSON(); + // Based on the toJSON result + json = json.data && json.type ? json.data : json; + val = '[Buffer: ' + jsonStringify(json, 2, depth + 1) + ']'; + break; + default: + val = (val == '[Function]' || val == '[Circular]') + ? val + : '"' + val + '"'; //string + } + return val; + } + + for(var i in object) { + if(!object.hasOwnProperty(i)) continue; // not my business + --length; + str += '\n ' + repeat(' ', space) + + (isArray(object) ? '' : '"' + i + '": ') // key + + _stringify(object[i]) // value + + (length ? ',' : ''); // comma + } + + return str + (str.length != 1 // [], {} + ? '\n' + repeat(' ', --space) + end + : end); +} + /** * Return if obj is a Buffer * @param {Object} arg @@ -434,8 +518,6 @@ exports.canonicalize = function(value, stack) { switch(type) { case 'undefined': - canonicalizedObj = '[undefined]'; - break; case 'buffer': case 'null': canonicalizedObj = value; @@ -447,9 +529,6 @@ exports.canonicalize = function(value, stack) { }); }); break; - case 'date': - canonicalizedObj = '[Date: ' + value.toISOString() + ']'; - break; case 'function': for (prop in value) { canonicalizedObj = {}; @@ -468,7 +547,9 @@ exports.canonicalize = function(value, stack) { }); }); break; + case 'date': case 'number': + case 'regexp': case 'boolean': canonicalizedObj = value; break; diff --git a/test/acceptance/utils.js b/test/acceptance/utils.js index c268466818..6b373a45e3 100644 --- a/test/acceptance/utils.js +++ b/test/acceptance/utils.js @@ -35,8 +35,8 @@ describe('lib/utils', function () { it("should format functions saved in windows style - spaces", function () { var fn = [ "function (one) {" - , " do {", - , ' "nothing";', + , " do {" + , ' "nothing";' , " } while (false);" , ' }' ].join("\r\n"); @@ -75,6 +75,115 @@ describe('lib/utils', function () { var stringify = utils.stringify; + it('should return Buffer with .toJSON representation', function() { + stringify(new Buffer([0x01])).should.equal('[\n 1\n]'); + stringify(new Buffer([0x01, 0x02])).should.equal('[\n 1\n 2\n]'); + + stringify(new Buffer('ABCD')).should.equal('[\n 65\n 66\n 67\n 68\n]'); + }); + + it('should return Date object with .toISOString() + string prefix', function() { + stringify(new Date(0)).should.equal('[Date: ' + new Date(0).toISOString() + ']'); + + var date = new Date(); // now + stringify(date).should.equal('[Date: ' + date.toISOString() + ']'); + }); + + describe('#Number', function() { + it('should show the handle -0 situations', function() { + stringify(-0).should.eql('-0'); + stringify(0).should.eql('0'); + stringify('-0').should.eql('"-0"'); + }); + + it('should work well with `NaN` and `Infinity`', function() { + stringify(NaN).should.equal('NaN'); + stringify(Infinity).should.equal('Infinity'); + stringify(-Infinity).should.equal('-Infinity'); + }); + + it('floats and ints', function() { + stringify(1).should.equal('1'); + stringify(1.2).should.equal('1.2'); + stringify(1e9).should.equal('1000000000'); + }); + }); + + describe('canonicalize example', function() { + it('should represent the actual full result', function() { + var expected = { + str: 'string', + int: 90, + float: 9.99, + boolean: false, + nil: null, + undef: undefined, + regex: /^[a-z|A-Z]/, + date: new Date(0), + func: function() {}, + infi: Infinity, + nan: NaN, + zero: -0, + buffer: new Buffer([0x01, 0x02]), + array: [1,2,3], + empArr: [], + matrix: [[1], [2,3,4] ], + object: { a: 1, b: 2 }, + canObj: { a: { b: 1, c: 2 }, b: {} }, + empObj: {} + }; + expected.circular = expected; // Make `Circular` situation + var actual = ['{' + , ' "array": [' + , ' 1' + , ' 2' + , ' 3' + , ' ]' + , ' "boolean": false' + , ' "buffer": [Buffer: [' + , ' 1' + , ' 2' + , ' ]]' + , ' "canObj": {' + , ' "a": {' + , ' "b": 1' + , ' "c": 2' + , ' }' + , ' "b": {}' + , ' }' + , ' "circular": [Circular]' + , ' "date": [Date: 1970-01-01T00:00:00.000Z]' + , ' "empArr": []' + , ' "empObj": {}' + , ' "float": 9.99' + , ' "func": [Function]' + , ' "infi": Infinity' + , ' "int": 90' + , ' "matrix": [' + , ' [' + , ' 1' + , ' ]' + , ' [' + , ' 2' + , ' 3' + , ' 4' + , ' ]' + , ' ]' + , ' "nan": NaN' + , ' "nil": [null]' + , ' "object": {' + , ' "a": 1' + , ' "b": 2' + , ' }' + , ' "regex": /^[a-z|A-Z]/' + , ' "str": "string"' + , ' "undef": [undefined]' + , ' "zero": -0' + , '}'].join('\n'); + stringify(expected).should.equal(actual); + }); + }); + it('should canonicalize the object', function(){ var travis = { name: 'travis', age: 24 }; var travis2 = { age: 24, name: 'travis' }; @@ -86,21 +195,21 @@ describe('lib/utils', function () { var travis = { name: 'travis' }; travis.whoami = travis; - stringify(travis).should.equal('{\n "name": "travis"\n "whoami": "[Circular]"\n}'); + stringify(travis).should.equal('{\n "name": "travis"\n "whoami": [Circular]\n}'); }); it('should handle circular structures in arrays', function(){ var travis = ['travis']; travis.push(travis); - stringify(travis).should.equal('[\n "travis"\n "[Circular]"\n]'); + stringify(travis).should.equal('[\n "travis"\n [Circular]\n]'); }); it('should handle circular structures in functions', function(){ var travis = function () {}; travis.fn = travis; - stringify(travis).should.equal('{\n "fn": "[Circular]"\n}'); + stringify(travis).should.equal('{\n "fn": [Circular]\n}'); }); @@ -109,7 +218,7 @@ describe('lib/utils', function () { regExpObj = { regexp: regexp }, regexpString = '/(?:)/'; - stringify(regExpObj).should.equal('{\n "regexp": "' + regexpString + '"\n}'); + stringify(regExpObj).should.equal('{\n "regexp": ' + regexpString + '\n}'); stringify(regexp).should.equal(regexpString); var number = 1, @@ -130,15 +239,14 @@ describe('lib/utils', function () { stringObj = { string: string }; stringify(stringObj).should.equal('{\n "string": "' + string + '"\n}'); - stringify(string).should.equal(string); + stringify(string).should.equal(JSON.stringify(string)); var nullValue = null, nullObj = { 'null': null }, nullString = '[null]'; - stringify(nullObj).should.equal('{\n "null": null\n}'); + stringify(nullObj).should.equal('{\n "null": [null]\n}'); stringify(nullValue).should.equal(nullString); - }); it('should handle arrays', function () { @@ -157,7 +265,7 @@ describe('lib/utils', function () { fnObj = {fn: fn}, fnString = '[Function]'; - stringify(fnObj).should.equal('{\n "fn": "' + fnString + '"\n}'); + stringify(fnObj).should.equal('{\n "fn": ' + fnString + '\n}'); stringify(fn).should.equal('[Function]'); }); @@ -177,8 +285,8 @@ describe('lib/utils', function () { it('should handle empty functions (with no properties)', function () { stringify(function(){}).should.equal('[Function]'); - stringify({foo: function() {}}).should.equal('{\n "foo": "[Function]"\n}'); - stringify({foo: function() {}, bar: 'baz'}).should.equal('{\n "bar": "baz"\n "foo": "[Function]"\n}'); + stringify({foo: function() {}}).should.equal('{\n "foo": [Function]\n}'); + stringify({foo: function() {}, bar: 'baz'}).should.equal('{\n "bar": "baz"\n "foo": [Function]\n}'); }); it('should handle functions w/ properties', function () { @@ -189,28 +297,28 @@ describe('lib/utils', function () { }); it('should handle undefined values', function () { - stringify({foo: undefined}).should.equal('{\n "foo": "[undefined]"\n}'); - stringify({foo: 'bar', baz: undefined}).should.equal('{\n "baz": "[undefined]"\n "foo": "bar"\n}'); + stringify({foo: undefined}).should.equal('{\n "foo": [undefined]\n}'); + stringify({foo: 'bar', baz: undefined}).should.equal('{\n "baz": [undefined]\n "foo": "bar"\n}'); stringify().should.equal('[undefined]'); }); it('should recurse', function () { -stringify({foo: {bar: {baz: {quux: {herp: 'derp'}}}}}).should.equal('{\n "foo": {\n "bar": {\n "baz": {\n "quux": {\n "herp": "derp"\n }\n }\n }\n }\n}'); + stringify({foo: {bar: {baz: {quux: {herp: 'derp'}}}}}).should.equal('{\n "foo": {\n "bar": {\n "baz": {\n "quux": {\n "herp": "derp"\n }\n }\n }\n }\n}'); }); it('might get confusing', function () { - stringify(null).should.equal(stringify('[null]')); + stringify(null).should.equal('[null]'); }); it('should not freak out if it sees a primitive twice', function () { - stringify({foo: null, bar: null}).should.equal('{\n "bar": null\n "foo": null\n}'); + stringify({foo: null, bar: null}).should.equal('{\n "bar": [null]\n "foo": [null]\n}'); stringify({foo: 1, bar: 1}).should.equal('{\n "bar": 1\n "foo": 1\n}'); }); it('should stringify dates', function () { var date = new Date(0); stringify(date).should.equal('[Date: 1970-01-01T00:00:00.000Z]'); - stringify({date: date}).should.equal('{\n "date": "[Date: 1970-01-01T00:00:00.000Z]"\n}'); + stringify({date: date}).should.equal('{\n "date": [Date: 1970-01-01T00:00:00.000Z]\n}'); }); it('should handle object without an Object prototype', function () {