Skip to content

Commit

Permalink
Serialize and deserialize functions and special values
Browse files Browse the repository at this point in the history
  • Loading branch information
mantoni committed Jun 14, 2024
1 parent 183406d commit 58c0f28
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 35 deletions.
75 changes: 42 additions & 33 deletions client.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function copy(keys, from, to) {
function copyDecircular(keys, from, to) {
keys.forEach(function (key) {
if (hasOwnProperty.call(from, key)) {
to[key] = decircularCopy(from[key]);
to[key] = serializableCopy(from[key]);
}
});
}
Expand Down Expand Up @@ -157,7 +157,7 @@ mocha.mochify_run = function () {
['debug', 'log', 'info', 'warn', 'error'].forEach(function (name) {
if (console[name]) {
console[name] = function () {
write('console.' + name, slice.call(arguments).map(decircularCopy));
write('console.' + name, slice.call(arguments).map(serializableCopy));
};
}
});
Expand All @@ -178,40 +178,49 @@ window.onunhandledrejection = function (event) {
]);
};

function decircularCopy(value) {
if (value === null || typeof value !== 'object') {
return value;
}
return JSON.parse(JSON.stringify(decircular(value)));
}

// Shameless copy of https://github.com/sindresorhus/decircular
// TODO Use the package once codebase is migrated to ES modules
function decircular(object) {
const seenObjects = new WeakMap();

function internalDecircular(value, path = []) {
if (!(value !== null && typeof value === 'object')) {
return value;
}
function serializableCopy(object) {
const seen = new WeakMap();

const existingPath = seenObjects.get(value);
if (existingPath) {
return `[Circular *${existingPath.join('.')}]`;
function internal(value, path = []) {
if (value === null) {
return null;
}

seenObjects.set(value, path);

const newValue = Array.isArray(value) ? [] : {};

for (const [key2, value2] of Object.entries(value)) {
newValue[key2] = internalDecircular(value2, [...path, key2]);
switch (typeof value) {
case 'undefined':
return '[undefined]';
case 'number':
if (value === Infinity) {
return '[Infinity]';
}
if (value === -Infinity) {
return '[-Infinity]';
}
if (Number.isNaN(value)) {
return '[NaN]';
}
return value;
case 'function':
return `[Function: ${value.name || ''}]`;
case 'symbol':
return value.toString();
case 'object': {
const existing = seen.get(value);
if (existing) {
return `[Circular *${existing.join('.')}]`;
}
seen.set(value, path);

const new_value = Array.isArray(value) ? [] : {};
for (const [key2, value2] of Object.entries(value)) {
new_value[key2] = internal(value2, [...path, key2]);
}
seen.delete(value);
return new_value;
}
default:
return value;
}

seenObjects.delete(value);

return newValue;
}

return internalDecircular(object);
return internal(object);
}
3 changes: 2 additions & 1 deletion lib/mocha-event-adapter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const Mocha = require('mocha');
const { parseClientValue } = require('./parse-client-value');
const {
EVENT_RUN_BEGIN,
EVENT_RUN_END,
Expand Down Expand Up @@ -104,6 +105,6 @@ function processEnd(object) {
*/
function copy(from, to) {
for (const key of Object.keys(from)) {
to[key] = from[key];
to[key] = parseClientValue(from[key]);
}
}
46 changes: 46 additions & 0 deletions lib/parse-client-value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict';

exports.parseClientValue = parseClientValue;

function parseClientValue(value) {
if (value === null) {
return null;
}
switch (typeof value) {
case 'string':
if (value === '[undefined]') {
return undefined;
}
if (value === '[NaN]') {
return NaN;
}
if (value === '[Infinity]') {
return Infinity;
}
if (value === '[-Infinity]') {
return -Infinity;
}
if (value.startsWith('[Function: ')) {
return makeFunction(value.slice(11, -1));
}
if (value.startsWith('Symbol(')) {
return Symbol(value.slice(7, -1));
}
return value;
case 'object': {
const new_value = Array.isArray(value) ? [] : {};
for (const [key, value2] of Object.entries(value)) {
new_value[key] = parseClientValue(value2);
}
return new_value;
}
default:
return value;
}
}

function makeFunction(name) {
const fn = function () {};
Object.defineProperty(fn, 'name', { value: name });
return fn;
}
86 changes: 86 additions & 0 deletions lib/parse-client-value.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict';

const { assert } = require('@sinonjs/referee-sinon');
const { parseClientValue } = require('./parse-client-value');

describe('lib/parse-client-value', () => {
it('returns null for null', () => {
assert.isNull(parseClientValue(null));
});

it('returns true for true', () => {
assert.isTrue(parseClientValue(true));
});

it('returns false for false', () => {
assert.isFalse(parseClientValue(false));
});

it('returns number for number', () => {
assert.equals(parseClientValue(0), 0);
assert.equals(parseClientValue(-7), -7);
assert.equals(parseClientValue(42), 42);
});

it('returns string as is', () => {
assert.equals(parseClientValue(''), '');
assert.equals(parseClientValue('test'), 'test');
assert.equals(parseClientValue('[test]'), '[test]');
});

it('returns undefined for [undefined]', () => {
assert.isUndefined(parseClientValue('[undefined]'));
});

it('returns Infinity for [Infinity]', () => {
assert.isInfinity(parseClientValue('[Infinity]'));
});

it('returns -Infinity for [-Infinity]', () => {
assert.isNegativeInfinity(parseClientValue('[-Infinity]'));
});

it('returns NaN for [NaN]', () => {
assert.isNaN(parseClientValue('[NaN]'));
});

it('returns symbol for Symbol()', () => {
const value = parseClientValue('Symbol()');

assert.isSymbol(value);
assert.equals(value.toString(), 'Symbol()');
});

it('returns symbol for Symbol(test)', () => {
const value = parseClientValue('Symbol(test)');

assert.isSymbol(value);
assert.equals(value.toString(), 'Symbol(test)');
});

it('returns anonymous function for [Function: ]', () => {
const value = parseClientValue('[Function: ]');

assert.isFunction(value);
assert.equals(value.name, '');
});

it('returns named function for [Function: test]', () => {
const value = parseClientValue('[Function: test]');

assert.isFunction(value);
assert.equals(value.name, 'test');
});

it('returns object for object', () => {
assert.equals(parseClientValue({}), {});
assert.equals(parseClientValue({ test: 42 }), { test: 42 });
assert.equals(parseClientValue({ test: '[NaN]' }), { test: NaN });
});

it('returns array for array', () => {
assert.equals(parseClientValue([]), []);
assert.equals(parseClientValue([7, 42]), [7, 42]);
assert.equals(parseClientValue(['[NaN]']), [NaN]);
});
});
4 changes: 3 additions & 1 deletion lib/poll-events.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const { parseClientValue } = require('./parse-client-value.js');

/**
* @typedef {import('./driver').MochifyDriver} MochifyDriver
*/
Expand Down Expand Up @@ -28,7 +30,7 @@ function pollEvents(driver, emit) {
if (event === 'mochify.coverage') {
global.__coverage__ = data;
} else if (event.startsWith('console.')) {
console[event.substring(8)](...data);
console[event.substring(8)](...data.map(parseClientValue));
} else {
emit(event, data);
}
Expand Down

0 comments on commit 58c0f28

Please sign in to comment.