/* eslint-disable test/prefer-lowercase-title */ import type { EncodeReplacer } from '../src/index' import { describe, expect, it } from 'vitest' import { decode, encode } from '../src/index' describe('JavaScript-specific type normalization', () => { describe('BigInt normalization', () => { it('converts BigInt within safe integer range to number', () => { const result = encode(BigInt(123)) expect(result).toBe('123') }) it('converts BigInt at MAX_SAFE_INTEGER boundary to number', () => { const result = encode(BigInt(Number.MAX_SAFE_INTEGER)) expect(result).toBe('9007199254740991') }) it('converts BigInt beyond safe integer range to quoted string', () => { const result = encode(BigInt('9007199254740992')) expect(result).toBe('"9007199254740992"') }) it('converts large BigInt to quoted decimal string', () => { const result = encode(BigInt('12345678901234567890')) expect(result).toBe('"12345678901234567890"') }) }) describe('Date normalization', () => { it('converts Date to ISO 8601 quoted string', () => { const result = encode(new Date('2025-01-01T00:00:00.000Z')) expect(result).toBe('"2025-01-01T00:00:00.000Z"') }) it('converts Date with milliseconds to ISO quoted string', () => { const result = encode(new Date('2025-11-05T12:34:56.789Z')) expect(result).toBe('"2025-11-05T12:34:56.789Z"') }) }) describe('Set normalization', () => { it('converts Set to array', () => { const input = new Set(['a', 'b', 'c']) const encoded = encode(input) const decoded = decode(encoded) expect(decoded).toEqual(['a', 'b', 'c']) }) it('converts empty Set to empty array', () => { const result = encode(new Set()) expect(result).toBe('[]') }) }) describe('Map normalization', () => { it('converts Map to object', () => { const input = new Map([['key1', 'value1'], ['key2', 'value2']]) const encoded = encode(input) const decoded = decode(encoded) expect(decoded).toEqual({ key1: 'value1', key2: 'value2' }) }) it('converts empty Map to empty object', () => { const input = new Map() const result = encode(input) expect(result).toBe('') }) it('converts Map with numeric keys to object with quoted string keys', () => { const input = new Map([[1, 'one'], [2, 'two']]) const result = encode(input) expect(result).toBe('"1": one\n"2": two') }) }) describe('undefined, function, and Symbol normalization', () => { it('converts undefined to null', () => { const result = encode(undefined) expect(result).toBe('null') }) it('converts function to null', () => { const result = encode(() => {}) expect(result).toBe('null') }) it('converts Symbol to null', () => { const result = encode(Symbol('test')) expect(result).toBe('null') }) }) describe('NaN and Infinity normalization', () => { it('converts NaN to null', () => { const result = encode(Number.NaN) expect(result).toBe('null') }) it('converts Infinity to null', () => { const result = encode(Number.POSITIVE_INFINITY) expect(result).toBe('null') }) it('converts negative Infinity to null', () => { const result = encode(Number.NEGATIVE_INFINITY) expect(result).toBe('null') }) }) describe('negative zero normalization', () => { it('normalizes -0 to 0', () => { const result = encode(-0) expect(result).toBe('0') }) }) describe('canonical number form', () => { it('encodes zero as canonical decimal', () => { expect(encode(0)).toBe('0') expect(decode(encode(0))).toBe(0) }) it('encodes 1e-6 (lower boundary) as canonical decimal', () => { expect(encode(1e-6)).toBe('0.000001') expect(decode(encode(1e-6))).toBe(1e-6) }) it('encodes 1e-7 (just below lower boundary) as exponent form', () => { expect(encode(1e-7)).toBe('1e-7') expect(decode(encode(1e-7))).toBe(1e-7) }) it('encodes 9.999999e20 (near upper boundary) as canonical decimal', () => { expect(encode(9.999999e20)).toBe('999999900000000000000') expect(decode(encode(9.999999e20))).toBe(9.999999e20) }) it('encodes 1e21 (upper boundary) as exponent form', () => { expect(encode(1e21)).toBe('1e+21') expect(decode(encode(1e21))).toBe(1e21) }) it('encodes 1.5e20 as canonical decimal', () => { expect(encode(1.5e20)).toBe('150000000000000000000') expect(decode(encode(1.5e20))).toBe(1.5e20) }) it('encodes 5e-324 (smallest subnormal) as exponent form', () => { expect(encode(5e-324)).toBe('5e-324') expect(decode(encode(5e-324))).toBe(5e-324) }) }) describe('toJSON method support', () => { it('calls toJSON method when object has it', () => { const obj = { data: 'example', toJSON() { return { info: this.data } }, } const result = encode(obj) expect(result).toBe('info: example') }) it('calls toJSON returning a primitive', () => { const obj = { value: 42, toJSON() { return 'custom-string' }, } const result = encode(obj) expect(result).toBe('custom-string') }) it('calls toJSON returning an array', () => { const obj = { items: [1, 2, 3], toJSON() { return ['a', 'b', 'c'] }, } const result = encode(obj) expect(result).toBe('[3]: a,b,c') }) it('calls toJSON in nested object properties', () => { const nestedObj = { secret: 'hidden', toJSON() { return { public: 'visible' } }, } const obj = { nested: nestedObj, other: 'value', } const result = encode(obj) expect(result).toBe('nested:\n public: visible\nother: value') }) it('calls toJSON in array elements', () => { const obj1 = { data: 'first', toJSON() { return { transformed: 'first-transformed' } }, } const obj2 = { data: 'second', toJSON() { return { transformed: 'second-transformed' } }, } const arr = [obj1, obj2] const result = encode(arr) expect(result).toBe('[2]{transformed}:\n first-transformed\n second-transformed') }) it('toJSON takes precedence over Date normalization', () => { const customDate = { toJSON() { return { type: 'custom-date', value: '2025-01-01' } }, } // Make it look like a Date but with toJSON Object.setPrototypeOf(customDate, Date.prototype) const result = encode(customDate) expect(result).toBe('type: custom-date\nvalue: 2025-01-01') }) it('works with toJSON inherited from prototype', () => { class CustomClass { value: string constructor(value: string) { this.value = value } toJSON() { return { classValue: this.value } } } const instance = new CustomClass('test-value') const result = encode(instance) expect(result).toBe('classValue: test-value') }) it('handles toJSON returning undefined (normalizes to null)', () => { const obj = { data: 'test', toJSON() { return undefined }, } const result = encode(obj) expect(result).toBe('null') }) it('works with replacer function', () => { const obj = { id: 1, secret: 'hidden', toJSON() { return { id: this.id, public: 'visible' } }, } const replacer: EncodeReplacer = (key, value) => { // Replacer should see the toJSON result, not the original object if (typeof value === 'object' && value !== null && 'public' in value) { return { ...value, extra: 'added' } } return value } const result = encode(obj, { replacer }) const decoded = decode(result) expect(decoded).toEqual({ id: 1, public: 'visible', extra: 'added' }) expect(decoded).not.toHaveProperty('secret') }) it('toJSON result is normalized before replacer is applied', () => { const dateObj = { date: new Date('2025-01-01T00:00:00.000Z'), toJSON() { return { date: this.date } }, } const replacer: EncodeReplacer = (key, value) => { // The date should already be normalized to ISO string by the time replacer sees it if (key === 'date' && typeof value === 'string') { return value.replace('2025', 'YEAR') } return value } const result = encode(dateObj, { replacer }) const decoded = decode(result) expect(decoded).toEqual({ date: 'YEAR-01-01T00:00:00.000Z' }) }) }) })