const roundTrip = (value) => parse(stringify(value));
test('the four scalar shapes survive the trip', () => {
assert.equal(roundTrip(42), 42);
assert.equal(roundTrip(-1.5), -1.5);
assert.equal(roundTrip('ada'), 'ada');
assert.equal(roundTrip(true), true);
assert.equal(roundTrip(false), false);
assert.equal(roundTrip(null), null);
});
test('undefined becomes null, so absent and empty agree', () => {
assert.equal(stringify(undefined), 'null');
assert.equal(encode(undefined), null);
assert.deepEqual(encode({ a: undefined }), { a: null });
});
test('a Map survives, which JSON.stringify alone does not', () => {
const original = new Map([
['ada', 1],
['grace', 2],
]);
assert.equal(JSON.stringify(original), '{}', 'the bug this file exists to fix');
assert.equal(stringify(original), '{"$map":[["ada",1],["grace",2]]}');
const back = roundTrip(original);
assert.ok(back instanceof Map, 'a map must come back a map');
assert.equal(back.get('ada'), 1);
assert.equal(back.get('grace'), 2);
assert.equal(back.size, 2);
});
test('an empty map is distinguishable from an empty record', () => {
assert.equal(stringify(new Map()), '{"$map":[]}');
assert.equal(stringify({}), '{}');
assert.ok(roundTrip(new Map()) instanceof Map);
assert.ok(!(roundTrip({}) instanceof Map));
});
test('a map whose own key is the marker is not confused for the marker', () => {
const original = new Map([['$map', 7]]);
assert.equal(stringify(original), '{"$map":[["$map",7]]}');
const back = roundTrip(original);
assert.ok(back instanceof Map);
assert.equal(back.get('$map'), 7);
assert.equal(back.size, 1);
});
test('maps nest, as keys and as values', () => {
const inner = new Map([['k', 1]]);
const original = new Map([['outer', inner]]);
const back = roundTrip(original);
assert.ok(back.get('outer') instanceof Map);
assert.equal(back.get('outer').get('k'), 1);
const listOfMaps = roundTrip([new Map([['a', 1]]), new Map([['b', 2]])]);
assert.equal(listOfMaps.length, 2);
assert.ok(listOfMaps[0] instanceof Map);
assert.equal(listOfMaps[1].get('b'), 2);
});
test('a record holding a map keeps both shapes', () => {
const back = roundTrip({ name: 'ada', scores: new Map([['maths', 9]]) });
assert.equal(back.name, 'ada');
assert.ok(back.scores instanceof Map);
assert.equal(back.scores.get('maths'), 9);
});
test('a choice rides as an ordinary object and recurses', () => {
const back = roundTrip({ tag: 'Ready', fields: [new Map([['a', 1]])] });
assert.equal(back.tag, 'Ready');
assert.ok(back.fields[0] instanceof Map);
});
class Chain {
constructor(base, item) {
this.base = base;
this.item = item;
this.flat = null;
}
toJSON() {
if (!this.flat) {
const base = this.base instanceof Chain ? this.base.toJSON() : this.base;
this.flat = [...base, this.item];
}
return this.flat;
}
}
test('a value with toJSON is encoded as the form it declares', () => {
const chain = new Chain(new Chain([], 1), 2);
assert.equal(stringify(chain), '[1,2]');
assert.deepEqual(encode(chain), [1, 2]);
assert.deepEqual(roundTrip(chain), [1, 2]);
});
test('a declared form is walked in turn, so a map inside one survives', () => {
const holder = { toJSON: () => ({ scores: new Map([['ada', 1]]) }) };
assert.equal(stringify(holder), '{"scores":{"$map":[["ada",1]]}}');
assert.ok(roundTrip(holder).scores instanceof Map);
});
test('a declared form is honoured at any depth', () => {
const bag = { tags: new Chain([], 1), size: 1 };
assert.equal(stringify(bag), '{"tags":[1],"size":1}');
assert.equal(stringify([new Chain([], 7)]), '[[7]]');
});
test('a toJSON that returns its receiver is walked instead of looping', () => {
class SelfReferring {
constructor() {
this.a = 1;
}
toJSON() {
return this;
}
}
assert.equal(stringify(new SelfReferring()), '{"a":1}');
assert.deepEqual(encode(new SelfReferring()), { a: 1 });
});
test('a map is unaffected, having no toJSON to consult', () => {
assert.equal(typeof new Map().toJSON, 'undefined', 'the reason this file exists');
assert.equal(stringify(new Map([['ada', 1]])), '{"$map":[["ada",1]]}');
});
test('a $map that is not an array is refused rather than emptied', () => {
for (const payload of [{ $map: 'junk' }, { $map: 7 }, { $map: null }, { $map: {} }]) {
let threw = false;
try {
decode(payload);
} catch (e) {
threw = true;
assert.ok(String(e.message).includes('array'), 'the message must say what was expected');
}
assert.ok(threw, 'refused: ' + JSON.stringify(payload));
}
});
test('a $map carrying sibling fields is refused rather than losing them', () => {
let threw = false;
try {
decode({ $map: [], name: 'ada' });
} catch (e) {
threw = true;
assert.ok(String(e.message).includes('name'), 'the message must name what would be lost');
}
assert.ok(threw, 'a record is not a map with extra fields, and neither is a map');
});
test('a $map entry that is not a pair is refused rather than skipped', () => {
for (const entries of [[['a']], [['a', 1, 2]], ['a'], [null]]) {
let threw = false;
try {
decode({ $map: entries });
} catch (e) {
threw = true;
}
assert.ok(threw, 'refused: ' + JSON.stringify(entries));
}
});
test('a payload shaped like the marker decodes as the map it claims to be', () => {
const back = decode({ $map: [['a', 1]] });
assert.ok(back instanceof Map, 'the marker means what it says');
assert.equal(back.get('a'), 1);
});
test('the marker is inert everywhere except as a field name', () => {
assert.equal(roundTrip('$map'), '$map');
assert.deepEqual(roundTrip(['$map']), ['$map']);
assert.deepEqual(roundTrip({ notmap: '$map' }), { notmap: '$map' });
});
test('the encoded-value assertion is in a dev build and not in a release one', () => {
assert.equal(typeof assertEncoded === 'function', !RELEASE, 'presence follows the build');
});
test('a Map that reached JSON.stringify would be caught in a dev build', () => {
if (RELEASE) {
assert.equal(typeof assertEncoded, 'undefined', 'a release build ships no assertion');
return;
}
assert.equal(JSON.stringify(new Map([['ada', 1]])), '{}', 'the silent failure itself');
let caught = '';
try {
assertEncoded({ scores: new Map([['ada', 1]]) }, '');
} catch (e) {
caught = String(e.message);
}
assert.ok(caught.includes('Map'), 'names the type: ' + caught);
assert.ok(caught.includes('scores'), 'names the path: ' + caught);
});
test('the assertion accepts everything encode is supposed to produce', () => {
if (RELEASE) {
assert.equal(typeof assertEncoded, 'undefined', 'a release build ships no assertion');
return;
}
assertEncoded(encode({ m: new Map([['a', [1, 2]]]), t: true, n: 1.5, s: 'x', e: null }), '');
assertEncoded(encode({ tag: 'Some', fields: [{ inner: [] }] }), '');
});