import { describe, test, expect } from 'vitest'; import { stableJSONStringify, testStringOut, testStringSrc } from '.'; import { stableJSONStringify_fast } from './fast.js'; import { stableJSONStringify_portable } from './portable.js'; // an object with keys that are intentionally out of order const obj1 = { // some alphanumeric keys b: 0, a: 1, // some keys that could be interpreted as indices '1': 2, '10': 3, '7': 4, // some keys that could be interpreted as numbers but not as indices '2.0': 5, '10.0': 6, '+0': 7, '-0': 8, // a key that sorts before the numeric keys, but should appear after the indices $c: 9, // Symbol keys should be ignored [Symbol('f')]: 10 }; const expectedObj1String = '{"1":2,"7":4,"10":3,"$c":9,"+0":7,"-0":8,"10.0":6,"2.0":5,"a":1,"b":0}'; // a class with a toJSON method that returns an object class ClassWithToJSON { toJSON() { return obj1; } } // an object with a property that is an instance of a class with a toJSON method const obj2 = { a: new ClassWithToJSON() }; const stableJSONStringify_implementations: Record = { stableJSONStringify_fast, stableJSONStringify_portable }; // an object with keys that should be ignored because of their value type const obj3 = { a: undefined, b: () => { /* do nothing */ } }; const complexObj = { // deliberately out of order obj3, obj1, obj2, booleanKey: true }; describe('stableJSONStringify', () => { describe('object keys should be returned in canonical order', () => { test.each(Object.entries(stableJSONStringify_implementations))('stableJSONStringify implementation: %s', (_name, stringify) => { const str1 = stringify(obj1); expect(str1).toEqual(expectedObj1String); // verify that the canonicalization is applied recursively to objects returned by toJSON methods const str2 = stringify(obj2); expect(str2).toEqual('{"a":' + expectedObj1String + '}'); }); }); describe('some object properties should be skipped', () => { test.each(Object.entries(stableJSONStringify_implementations))('stableJSONStringify implementation: %s', (_name, stringify) => { expect(stringify(obj3)).toEqual('{}'); }); }); describe('skipped values are treated as null in arrays', () => { test.each(Object.entries(stableJSONStringify_implementations))('stableJSONStringify implementation: %s', (_name, stringify) => { const values = Object.values(obj3); const expected = '[' + new Array(values.length).fill('null').join(',') + ']'; expect(stringify(values)).toEqual(expected); }); }); describe('escaping of characters in strings is consistent', () => { test.each(Object.entries(stableJSONStringify_implementations))('stableJSONStringify implementation: %s', (_name, stringify) => { expect(stringify(testStringSrc)).toEqual(testStringOut); }); }); test('the two implementations agree on a complex object', () => { expect(stableJSONStringify_fast(complexObj)).toEqual(stableJSONStringify_portable(complexObj)); }); });