diff --git a/lib/nodejs/serializer.js b/lib/nodejs/serializer.js index 70640bc5b1..b25c493bf0 100644 --- a/lib/nodejs/serializer.js +++ b/lib/nodejs/serializer.js @@ -196,7 +196,8 @@ class SerializableEvent { parent[key] = Object.create(null); return; } - if (type(value) === 'error' || value instanceof Error) { + let _type = type(value); + if (_type === 'error') { // we need to reference the stack prop b/c it's lazily-loaded. // `__type` is necessary for deserialization to create an `Error` later. // `message` is apparently not enumerable, so we must handle it specifically. @@ -206,10 +207,11 @@ class SerializableEvent { __type: 'Error' }); parent[key] = value; - // after this, the result of type(value) will be `object`, and we'll throw + // after this, set the result of type(value) to be `object`, and we'll throw // whatever other junk is in the original error into the new `value`. + _type = 'object'; } - switch (type(value)) { + switch (_type) { case 'object': if (type(value.serialize) === 'function') { parent[key] = value.serialize(); diff --git a/lib/runner.js b/lib/runner.js index bc88876c39..d3349ca7a0 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -23,7 +23,7 @@ var dQuote = utils.dQuote; var sQuote = utils.sQuote; var stackFilter = utils.stackTraceFilter(); var stringify = utils.stringify; -var type = utils.type; + var errors = require('./errors'); var createInvalidExceptionError = errors.createInvalidExceptionError; var createUnsupportedError = errors.createUnsupportedError; @@ -1223,7 +1223,9 @@ function isError(err) { */ function thrown2Error(err) { return new Error( - 'the ' + type(err) + ' ' + stringify(err) + ' was thrown, throw an Error :)' + `the ${utils.canonicalType(err)} ${stringify( + err + )} was thrown, throw an Error :)` ); } diff --git a/lib/utils.js b/lib/utils.js index 5fca7aecb3..853fd5b108 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -129,19 +129,21 @@ function emptyRepresentation(value, typeHint) { * @param {*} value The value to test. * @returns {string} Computed type * @example - * type({}) // 'object' - * type([]) // 'array' - * type(1) // 'number' - * type(false) // 'boolean' - * type(Infinity) // 'number' - * type(null) // 'null' - * type(new Date()) // 'date' - * type(/foo/) // 'regexp' - * type('type') // 'string' - * type(global) // 'global' - * type(new String('foo') // 'object' + * canonicalType({}) // 'object' + * canonicalType([]) // 'array' + * canonicalType(1) // 'number' + * canonicalType(false) // 'boolean' + * canonicalType(Infinity) // 'number' + * canonicalType(null) // 'null' + * canonicalType(new Date()) // 'date' + * canonicalType(/foo/) // 'regexp' + * canonicalType('type') // 'string' + * canonicalType(global) // 'global' + * canonicalType(new String('foo') // 'object' + * canonicalType(async function() {}) // 'asyncfunction' + * canonicalType(await import(name)) // 'module' */ -var type = (exports.type = function type(value) { +var canonicalType = (exports.canonicalType = function canonicalType(value) { if (value === undefined) { return 'undefined'; } else if (value === null) { @@ -155,6 +157,47 @@ var type = (exports.type = function type(value) { .toLowerCase(); }); +/** + * + * Returns a general type or data structure of a variable + * @private + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures + * @param {*} value The value to test. + * @returns {string} One of undefined, boolean, number, string, bigint, symbol, object + * @example + * type({}) // 'object' + * type([]) // 'array' + * type(1) // 'number' + * type(false) // 'boolean' + * type(Infinity) // 'number' + * type(null) // 'null' + * type(new Date()) // 'object' + * type(/foo/) // 'object' + * type('type') // 'string' + * type(global) // 'object' + * type(new String('foo') // 'string' + */ +exports.type = function type(value) { + // Null is special + if (value === null) return 'null'; + const primitives = new Set([ + 'undefined', + 'boolean', + 'number', + 'string', + 'bigint', + 'symbol' + ]); + const _type = typeof value; + if (_type === 'function') return _type; + if (primitives.has(_type)) return _type; + if (value instanceof String) return 'string'; + if (value instanceof Error) return 'error'; + if (Array.isArray(value)) return 'array'; + + return _type; +}; + /** * Stringify `value`. Different behavior depending on type of value: * @@ -171,7 +214,7 @@ var type = (exports.type = function type(value) { * @return {string} */ exports.stringify = function(value) { - var typeHint = type(value); + var typeHint = canonicalType(value); if (!~['object', 'array', 'function'].indexOf(typeHint)) { if (typeHint === 'buffer') { @@ -237,7 +280,7 @@ function jsonStringify(object, spaces, depth) { } function _stringify(val) { - switch (type(val)) { + switch (canonicalType(val)) { case 'null': case 'undefined': val = '[' + val + ']'; @@ -318,7 +361,7 @@ exports.canonicalize = function canonicalize(value, stack, typeHint) { /* eslint-disable no-unused-vars */ var prop; /* eslint-enable no-unused-vars */ - typeHint = typeHint || type(value); + typeHint = typeHint || canonicalType(value); function withStack(value, fn) { stack.push(value); fn(); @@ -552,7 +595,7 @@ exports.createMap = function(obj) { * @throws {TypeError} if argument is not a non-empty object. */ exports.defineConstants = function(obj) { - if (type(obj) !== 'object' || !Object.keys(obj).length) { + if (canonicalType(obj) !== 'object' || !Object.keys(obj).length) { throw new TypeError('Invalid argument; expected a non-empty object'); } return Object.freeze(exports.createMap(obj)); diff --git a/test/node-unit/utils.spec.js b/test/node-unit/utils.spec.js index 720e93f6c3..a3eda635e7 100644 --- a/test/node-unit/utils.spec.js +++ b/test/node-unit/utils.spec.js @@ -18,10 +18,29 @@ describe('utils', function() { }); describe('type()', function() { - it('should return "asyncfunction" if the parameter is an async function', function() { + it('should return "function" if the parameter is an async function', function() { expect( utils.type(async () => {}), 'to be', + 'function' + ); + }); + it('should return "error" if the parameter is an Error', function() { + expect(utils.type(new Error('err')), 'to be', 'error'); + }); + }); + describe('canonicalType()', function() { + it('should return "buffer" if the parameter is a Buffer', function() { + expect( + utils.canonicalType(Buffer.from('ff', 'hex')), + 'to be', + 'buffer' + ); + }); + it('should return "asyncfunction" if the parameter is an async function', function() { + expect( + utils.canonicalType(async () => {}), + 'to be', 'asyncfunction' ); }); diff --git a/test/unit/utils.spec.js b/test/unit/utils.spec.js index a953ac51ae..c239627f55 100644 --- a/test/unit/utils.spec.js +++ b/test/unit/utils.spec.js @@ -536,6 +536,60 @@ describe('lib/utils', function() { }; }); + it('should recognize various types', function() { + expect(type({}), 'to be', 'object'); + expect(type([]), 'to be', 'array'); + expect(type(1), 'to be', 'number'); + expect(type(Infinity), 'to be', 'number'); + expect(type(null), 'to be', 'null'); + expect(type(undefined), 'to be', 'undefined'); + expect(type(new Date()), 'to be', 'object'); + expect(type(/foo/), 'to be', 'object'); + expect(type('type'), 'to be', 'string'); + expect(type(new Error()), 'to be', 'error'); + expect(type(global), 'to be', 'object'); + expect(type(true), 'to be', 'boolean'); + expect(type(Buffer.from('ff', 'hex')), 'to be', 'object'); + expect(type(Symbol.iterator), 'to be', 'symbol'); + expect(type(new Map()), 'to be', 'object'); + expect(type(new WeakMap()), 'to be', 'object'); + expect(type(new Set()), 'to be', 'object'); + expect(type(new WeakSet()), 'to be', 'object'); + expect( + type(async () => {}), + 'to be', + 'function' + ); + }); + + describe('when toString on null or undefined stringifies window', function() { + it('should recognize null and undefined', function() { + expect(type(null), 'to be', 'null'); + expect(type(undefined), 'to be', 'undefined'); + }); + }); + + afterEach(function() { + Object.prototype.toString = toString; + }); + }); + + describe('canonicalType()', function() { + /* eslint no-extend-native: off */ + + var type = utils.canonicalType; + var toString = Object.prototype.toString; + + beforeEach(function() { + // some JS engines such as PhantomJS 1.x exhibit this behavior + Object.prototype.toString = function() { + if (this === global) { + return '[object DOMWindow]'; + } + return toString.call(this); + }; + }); + it('should recognize various types', function() { expect(type({}), 'to be', 'object'); expect(type([]), 'to be', 'array');