/** * JSON 核心纯函数回归测试 * 覆盖 GitHub Issues + 已知 Bug:BigInt 精度、科学计数法、嵌套解析、编解码 等 */ import { describe, it, expect } from 'vitest'; import { htmlspecialchars, isUrl, isBigNumberLike, getType, rebuildBigNumberFromParts, getBigNumberDisplayString, parseWithBigInt, deepParseJSONStrings, uniEncode, uniDecode, safeStringify, formatDate, getStringBytes, createSafeToastHTML, } from '../apps/json-format/json-utils.js'; // ═══════════════════════════════════════════════════════ // 1. htmlspecialchars // ═══════════════════════════════════════════════════════ describe('htmlspecialchars', () => { it('转义 HTML 特殊字符', () => { expect(htmlspecialchars('')).toBe( '<script>alert("xss")</script>', ); }); it('转义 & 号', () => { expect(htmlspecialchars('a&b')).toBe('a&b'); }); it('转义单引号', () => { expect(htmlspecialchars("it's")).toBe('it's'); }); it('空字符串', () => { expect(htmlspecialchars('')).toBe(''); }); }); // ═══════════════════════════════════════════════════════ // 2. isUrl // ═══════════════════════════════════════════════════════ describe('isUrl', () => { it('http 协议', () => expect(isUrl('http://example.com')).toBe(true)); it('https 协议', () => expect(isUrl('https://example.com/path?a=1')).toBe(true)); it('ftp 协议', () => expect(isUrl('ftp://files.example.com')).toBe(true)); it('非 URL', () => expect(isUrl('not-a-url')).toBe(false)); it('null 输入', () => expect(isUrl(null)).toBe(false)); it('数字输入', () => expect(isUrl(123)).toBe(false)); }); // ═══════════════════════════════════════════════════════ // 3. isBigNumberLike / getType // ═══════════════════════════════════════════════════════ describe('isBigNumberLike', () => { it('识别 BigNumber duck-type', () => { expect(isBigNumberLike({ s: 1, e: 17, c: [99581589502011] })).toBe(true); }); it('排除普通对象', () => { expect(isBigNumberLike({ a: 1, b: 2 })).toBe(false); }); it('null 安全', () => { expect(isBigNumberLike(null)).toBe(false); }); }); describe('getType', () => { it('null', () => expect(getType(null)).toBe('null')); it('undefined', () => expect(getType(undefined)).toBe('undefined')); it('string', () => expect(getType('hello')).toBe('string')); it('number', () => expect(getType(42)).toBe('number')); it('boolean', () => expect(getType(true)).toBe('boolean')); it('array', () => expect(getType([1, 2])).toBe('array')); it('object', () => expect(getType({ a: 1 })).toBe('object')); it('bigint', () => expect(getType(BigInt('12345678901234567890'))).toBe('bigint')); it('BigNumber duck-type → bigint', () => { expect(getType({ s: 1, e: 5, c: [123456] })).toBe('bigint'); }); }); // ═══════════════════════════════════════════════════════ // 4. BigNumber 还原 // ═══════════════════════════════════════════════════════ describe('rebuildBigNumberFromParts', () => { it('还原正整数', () => { expect(rebuildBigNumberFromParts({ s: 1, e: 5, c: [123456] })).toBe('123456'); }); it('还原负整数', () => { expect(rebuildBigNumberFromParts({ s: -1, e: 5, c: [123456] })).toBe('-123456'); }); it('还原小数', () => { expect(rebuildBigNumberFromParts({ s: 1, e: 0, c: [1, 23000000000000] })).toBe('1.23'); }); it('还原纯小数 (0.xxx)', () => { const result = rebuildBigNumberFromParts({ s: 1, e: -1, c: [5] }); expect(result).toBe('0.5'); }); }); describe('getBigNumberDisplayString', () => { it('原生 BigInt', () => { expect(getBigNumberDisplayString(BigInt('995815895020119788889'))).toBe( '995815895020119788889', ); }); it('BigNumber duck-type 对象', () => { const bn = { s: 1, e: 2, c: [123] }; const result = getBigNumberDisplayString(bn); expect(result).toBe('123'); }); it('普通值 fallback', () => { expect(getBigNumberDisplayString(42)).toBe('42'); }); }); // ═══════════════════════════════════════════════════════ // 5. parseWithBigInt — GitHub Issue 核心回归 // ═══════════════════════════════════════════════════════ describe('parseWithBigInt', () => { it('Issue: 16 位及以上整数保持精度', () => { const json = '{"id": 995815895020119788889}'; const result = parseWithBigInt(json); expect(result.id).toBe(BigInt('995815895020119788889')); }); it('Issue: 负大整数', () => { const json = '{"id": -9958158950201197888}'; const result = parseWithBigInt(json); expect(result.id).toBe(BigInt('-9958158950201197888')); }); it('15 位及以下整数保持为 number', () => { const json = '{"id": 123456789012345}'; const result = parseWithBigInt(json); expect(typeof result.id).toBe('number'); expect(result.id).toBe(123456789012345); }); it('字符串内的大数字不被替换', () => { const json = '{"msg": "订单号:9958158950201197888"}'; const result = parseWithBigInt(json); expect(typeof result.msg).toBe('string'); expect(result.msg).toBe('订单号:9958158950201197888'); }); it('数组中的大整数', () => { const json = '[1234567890123456789, 42]'; const result = parseWithBigInt(json); expect(result[0]).toBe(BigInt('1234567890123456789')); expect(result[1]).toBe(42); }); it('嵌套对象中的大整数', () => { const json = '{"a": {"b": 1234567890123456789}}'; const result = parseWithBigInt(json); expect(result.a.b).toBe(BigInt('1234567890123456789')); }); it('宽松解析:单引号 key', () => { const json = "{'name': 'test', 'value': 123}"; const result = parseWithBigInt(json); expect(result.name).toBe('test'); }); it('宽松解析:未加引号 key', () => { const json = '{name: "test", value: 123}'; const result = parseWithBigInt(json); expect(result.name).toBe('test'); }); it('普通 JSON 解析(无大整数)', () => { const json = '{"a": 1, "b": "hello", "c": true, "d": null}'; const result = parseWithBigInt(json); expect(result).toEqual({ a: 1, b: 'hello', c: true, d: null }); }); it('Issue: 科学计数法数字不丢精度', () => { const json = '{"val": 1.5e2}'; const result = parseWithBigInt(json); expect(result.val).toBe(150); }); it('Issue: 带尾零的小数', () => { const json = '{"val": 1.50}'; const result = parseWithBigInt(json); expect(result.val).toBe(1.5); }); }); // ═══════════════════════════════════════════════════════ // 6. deepParseJSONStrings — 嵌套 JSON 解包 // ═══════════════════════════════════════════════════════ describe('deepParseJSONStrings', () => { it('解包字符串内的 JSON 对象', () => { const obj = { data: '{"name":"test"}' }; const result = deepParseJSONStrings(obj); expect(result.data).toEqual({ name: 'test' }); }); it('解包字符串内的 JSON 数组', () => { const obj = { list: '[1, 2, 3]' }; const result = deepParseJSONStrings(obj); expect(result.list).toEqual([1, 2, 3]); }); it('递归多层解包', () => { const inner = JSON.stringify({ x: 1 }); const outer = { data: JSON.stringify({ nested: inner }) }; const result = deepParseJSONStrings(outer); expect(result.data.nested).toEqual({ x: 1 }); }); it('非 JSON 字符串保持不变', () => { const obj = { msg: 'hello world' }; expect(deepParseJSONStrings(obj)).toEqual({ msg: 'hello world' }); }); it('BigNumber duck-type 不被误解包', () => { const obj = { val: '{"s":1,"e":5,"c":[123456]}' }; const result = deepParseJSONStrings(obj); expect(typeof result.val).toBe('string'); }); it('空字符串安全', () => { expect(deepParseJSONStrings({ a: '' })).toEqual({ a: '' }); }); it('数组内的嵌套 JSON', () => { const arr = ['{"k":"v"}', 'plain']; const result = deepParseJSONStrings(arr); expect(result[0]).toEqual({ k: 'v' }); expect(result[1]).toBe('plain'); }); it('null / 原始值安全', () => { expect(deepParseJSONStrings(null)).toBeNull(); expect(deepParseJSONStrings(42)).toBe(42); expect(deepParseJSONStrings('str')).toBe('str'); }); }); // ═══════════════════════════════════════════════════════ // 7. Unicode 编解码 // ═══════════════════════════════════════════════════════ describe('uniEncode / uniDecode', () => { it('中文编解码往返', () => { const str = '你好世界'; expect(uniDecode(uniEncode(str))).toBe(str); }); it('保留 JSON 结构字符', () => { const str = '{"key": "value"}'; const encoded = uniEncode(str); expect(encoded).toContain('{'); expect(encoded).toContain('}'); expect(encoded).toContain(':'); }); it('特殊空白字符', () => { const str = 'a\tb\nc'; const decoded = uniDecode(uniEncode(str)); expect(decoded).toBe(str); }); }); // ═══════════════════════════════════════════════════════ // 8. safeStringify — BigInt 保精度 // ═══════════════════════════════════════════════════════ describe('safeStringify', () => { it('BigInt 输出为裸数字', () => { const obj = { id: BigInt('9958158950201197888') }; const result = safeStringify(obj); expect(result).toBe('{"id":9958158950201197888}'); expect(result).not.toContain('"9958158950201197888"'); }); it('普通数字不受影响', () => { const obj = { val: 42 }; expect(safeStringify(obj)).toBe('{"val":42}'); }); it('支持 space 缩进', () => { const obj = { a: 1 }; const result = safeStringify(obj, 2); expect(result).toContain('\n'); }); it('嵌套 BigInt', () => { const obj = { data: { id: BigInt('1234567890123456789') } }; const result = safeStringify(obj); expect(result).toContain('1234567890123456789'); }); }); // ═══════════════════════════════════════════════════════ // 9. formatDate(替代 Date.prototype.format) // ═══════════════════════════════════════════════════════ describe('formatDate', () => { const date = new Date(2024, 0, 5, 9, 3, 7, 42); // 2024-01-05 09:03:07.042 it('yyyy-MM-dd HH:mm:ss', () => { expect(formatDate(date, 'yyyy-MM-dd HH:mm:ss')).toBe('2024-01-05 09:03:07'); }); it('yy/M/d', () => { expect(formatDate(date, 'yy/M/d')).toBe('24/1/5'); }); it('毫秒格式化 SSS', () => { expect(formatDate(date, 'HH:mm:ss.SSS')).toBe('09:03:07.042'); }); it('非字符串 pattern 回退', () => { expect(formatDate(date, null)).toBe(date.toString()); }); }); // ═══════════════════════════════════════════════════════ // 10. getStringBytes // ═══════════════════════════════════════════════════════ describe('getStringBytes', () => { it('纯 ASCII', () => { expect(getStringBytes('hello')).toBe(5); }); it('中文字符', () => { expect(getStringBytes('你好')).toBeGreaterThan(2); }); it('空字符串', () => { expect(getStringBytes('')).toBe(0); }); }); // ═══════════════════════════════════════════════════════ // 11. createSafeToastHTML(XSS 防护) // ═══════════════════════════════════════════════════════ describe('createSafeToastHTML', () => { it('转义 HTML 标签防止 XSS', () => { const html = createSafeToastHTML(''); expect(html).not.toContain(' { const html = createSafeToastHTML('操作成功'); expect(html).toContain('操作成功'); expect(html).toContain('fehelper_alertmsg'); }); }); // ═══════════════════════════════════════════════════════ // 12. 综合回归:端到端 JSON 解析 → 格式化 → 输出 // ═══════════════════════════════════════════════════════ describe('端到端回归', () => { it('包含 BigInt 的完整 JSON 往返', () => { const input = '{"orderId": 9958158950201197888, "amount": 99.5, "name": "test"}'; const parsed = parseWithBigInt(input); expect(parsed.orderId).toBe(BigInt('9958158950201197888')); expect(parsed.amount).toBe(99.5); expect(parsed.name).toBe('test'); const output = safeStringify(parsed); expect(output).toContain('9958158950201197888'); expect(output).not.toMatch(/\d+n[,\}]/); // 无 BigInt 后缀 "n" }); it('嵌套转义 JSON + BigInt', () => { // inner 本身包含大整数的 JSON 字符串——deepParse 会递归解包至对象 const inner = '{"id": 1234567890123456789}'; const outer = { data: JSON.stringify({ nested: inner }) }; const unpacked = deepParseJSONStrings(outer); // nested 被递归解包为对象 expect(typeof unpacked.data.nested).toBe('object'); expect(unpacked.data.nested.id).toBe(1234567890123456789); }); it('JSONP 格式预处理', () => { const jsonp = 'callback({"status": 200, "id": 1234567890123456789})'; const match = /^([\w.]+)\(\s*([\s\S]*)\s*\)$/gm.exec(jsonp); expect(match).not.toBeNull(); expect(match[1]).toBe('callback'); const parsed = parseWithBigInt(match[2]); expect(parsed.id).toBe(BigInt('1234567890123456789')); }); it('Issue: URL 编码的 JSON', () => { const encoded = '%7B%22name%22%3A%22test%22%7D'; const decoded = decodeURIComponent(encoded); expect(JSON.parse(decoded)).toEqual({ name: 'test' }); }); });