Skip to content
Permalink
Browse files
fix(core): plain JavaScript/JSON object are now pretty-printed to mak…
…e them easier to read

Closes #509
  • Loading branch information
jan-molak committed May 2, 2020
1 parent be23c6e commit c63d64de689ed7194b8fce9c65aa1a896d1728de
Showing 3 changed files with 69 additions and 9 deletions.
@@ -36,7 +36,7 @@ describe('Ensure', () => {
person: {
name: 'Jan',
},
})).toString()).to.equal(`#actor ensures that { person: { name: 'Jan' } } does equal { person: { name: 'Jan' } }`);
})).toString()).to.equal(`#actor ensures that { "person": { "name": "Jan" } } does equal { "person": { "name": "Jan" } }`);
});

given<Answerable<number>>(
@@ -99,7 +99,7 @@ describe('Ensure', () => {
description: 'list',
expected: [{ name: 'Bob' }, { name: 'Alice' }],
actual: [{ name: 'Alice' }],
artifact: { expected: `[\n { name: 'Bob' },\n { name: 'Alice' }\n]`, actual: `[\n { name: 'Alice' }\n]` },
artifact: { expected: '[\n {\n "name": "Bob"\n},\n {\n "name": "Alice"\n}\n]', actual: `[\n {\n "name": "Alice"\n}\n]` },
}, {
description: 'promise',
expected: Promise.resolve(true),
@@ -22,16 +22,16 @@ describe ('`formatted` tag function', () => {
{ description: 'an undefined parameter', actual: formatted `param: ${ undefined }`, expected: 'param: undefined' },
{ description: 'a number parameter', actual: formatted `Answer: ${ 42 }`, expected: 'Answer: 42' },
{ description: 'a string parameter', actual: formatted `Hello ${ 'World' }!`, expected: "Hello 'World'!" },
{ description: 'an object parameter', actual: formatted `${ { twitter: '@JanMolak'} }`, expected: "{ twitter: '@JanMolak' }" },
{ description: 'an object parameter', actual: formatted `${ { twitter: '@JanMolak'} }`, expected: '{ "twitter": "@JanMolak" }' },
{ description: 'an empty array', actual: formatted `${ [] }`, expected: '[ ]' },
{ description: 'an array parameter', actual: formatted `${ [1, 2, '3'] }`, expected: "[ 1, 2, '3' ]" },
{ description: 'an array of params', actual: formatted `${ [ Promise.resolve(1), q('2') ] }`, expected: '[ a Promise, the meaning of life ]' },
{ description: 'an object array parameter', actual: formatted `${ [{ name: 'Jan'}] }`, expected: "[ { name: 'Jan' } ]" },
{ description: 'a Date parameter', actual: formatted `${ new Date(818035920000) }`, expected: '1995-12-04T00:12:00.000Z' },
{ description: 'a promised parameter', actual: formatted `${ p('something') }`, expected: 'a Promise' },
{ description: 'a question', actual: formatted `${ q('value') }`, expected: 'the meaning of life' },
{ description: 'an inspectable object', actual: formatted `${ i('result') }`, expected: 'result' },
{ description: 'an "toStringable" object', actual: formatted `${ ts('result') }`, expected: 'result' },
{ description: 'an object array parameter', actual: formatted `${ [{ name: 'Jan'}] }`, expected: '[ { "name": "Jan" } ]' },
{ description: 'a Date parameter', actual: formatted `${ new Date(818035920000) }`, expected: '1995-12-04T00:12:00.000Z' },
{ description: 'a promised parameter', actual: formatted `${ p('something') }`, expected: 'a Promise' },
{ description: 'a question', actual: formatted `${ q('value') }`, expected: 'the meaning of life' },
{ description: 'an inspectable object', actual: formatted `${ i('result') }`, expected: 'result' },
{ description: 'an "toStringable" object', actual: formatted `${ ts('result') }`, expected: 'result' },
{ description: 'a function parameter', actual: formatted `${ SomeAttribute }`, expected: 'SomeAttribute property' },
).
it('produces a human-readable description when given a template with', ({ actual, expected }) => {
@@ -48,6 +48,10 @@ export function inspected(value: Answerable<any>): string {
return `${ value.name } property`;
}

if (! hasCustomInspectionFunction(value) && isPlainObject(value) && isSerialisableAsJSON(value)) {
return JSON.stringify(value, null, 4);
}

return inspect(value, { breakLength: Infinity, compact: true, sorted: false });
}

@@ -120,6 +124,62 @@ function isANamedFunction<T>(v: any): v is { name: string } {
return {}.toString.call(v) === '[object Function]' && (v as any).name !== '';
}

/**
* @desc
* Checks if the value defines its own `inspect` method
* See: https://nodejs.org/api/util.html#util_util_inspect_custom
*
* @private
* @param {Answerable<any>} v
*/
function hasCustomInspectionFunction(v: Answerable<any>): v is object {
return v && v[Symbol.for('nodejs.util.inspect.custom')];
}

/**
* @desc
* Checks if the value has a good chance of being a plain JavaScript object
*
* @private
* @param {Answerable<any>} v
*/
function isPlainObject(v: Answerable<any>): v is object {

// Basic check for Type object that's not null
if (typeof v === 'object' && v !== null) {

// If Object.getPrototypeOf supported, use it
if (typeof Object.getPrototypeOf === 'function') {
const proto = Object.getPrototypeOf(v);
return proto === Object.prototype || proto === null;
}

// Otherwise, use internal class
// This should be reliable as if getPrototypeOf not supported, is pre-ES5
return Object.prototype.toString.call(v) === '[object Object]';
}

// Not an object
return false;
}

/**
* @desc
* Checks if the value is a JSON object that can be stringified
*
* @private
* @param {Answerable<any>} v
*/
function isSerialisableAsJSON<T>(v: any): v is object {
try {
JSON.stringify(v);

return true;
} catch (e) {
return false;
}
}

/**
* https://davidwalsh.name/detect-native-function
* @param {any} v

0 comments on commit c63d64d

Please sign in to comment.