json-utils.test.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /**
  2. * JSON 核心纯函数回归测试
  3. * 覆盖 GitHub Issues + 已知 Bug:BigInt 精度、科学计数法、嵌套解析、编解码 等
  4. */
  5. import { describe, it, expect } from 'vitest';
  6. import {
  7. htmlspecialchars,
  8. isUrl,
  9. isBigNumberLike,
  10. getType,
  11. rebuildBigNumberFromParts,
  12. getBigNumberDisplayString,
  13. parseWithBigInt,
  14. deepParseJSONStrings,
  15. uniEncode,
  16. uniDecode,
  17. safeStringify,
  18. formatDate,
  19. getStringBytes,
  20. createSafeToastHTML,
  21. } from '../apps/json-format/json-utils.js';
  22. // ═══════════════════════════════════════════════════════
  23. // 1. htmlspecialchars
  24. // ═══════════════════════════════════════════════════════
  25. describe('htmlspecialchars', () => {
  26. it('转义 HTML 特殊字符', () => {
  27. expect(htmlspecialchars('<script>alert("xss")</script>')).toBe(
  28. '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;',
  29. );
  30. });
  31. it('转义 & 号', () => {
  32. expect(htmlspecialchars('a&b')).toBe('a&amp;b');
  33. });
  34. it('转义单引号', () => {
  35. expect(htmlspecialchars("it's")).toBe('it&#039;s');
  36. });
  37. it('空字符串', () => {
  38. expect(htmlspecialchars('')).toBe('');
  39. });
  40. });
  41. // ═══════════════════════════════════════════════════════
  42. // 2. isUrl
  43. // ═══════════════════════════════════════════════════════
  44. describe('isUrl', () => {
  45. it('http 协议', () => expect(isUrl('http://example.com')).toBe(true));
  46. it('https 协议', () => expect(isUrl('https://example.com/path?a=1')).toBe(true));
  47. it('ftp 协议', () => expect(isUrl('ftp://files.example.com')).toBe(true));
  48. it('非 URL', () => expect(isUrl('not-a-url')).toBe(false));
  49. it('null 输入', () => expect(isUrl(null)).toBe(false));
  50. it('数字输入', () => expect(isUrl(123)).toBe(false));
  51. });
  52. // ═══════════════════════════════════════════════════════
  53. // 3. isBigNumberLike / getType
  54. // ═══════════════════════════════════════════════════════
  55. describe('isBigNumberLike', () => {
  56. it('识别 BigNumber duck-type', () => {
  57. expect(isBigNumberLike({ s: 1, e: 17, c: [99581589502011] })).toBe(true);
  58. });
  59. it('排除普通对象', () => {
  60. expect(isBigNumberLike({ a: 1, b: 2 })).toBe(false);
  61. });
  62. it('null 安全', () => {
  63. expect(isBigNumberLike(null)).toBe(false);
  64. });
  65. });
  66. describe('getType', () => {
  67. it('null', () => expect(getType(null)).toBe('null'));
  68. it('undefined', () => expect(getType(undefined)).toBe('undefined'));
  69. it('string', () => expect(getType('hello')).toBe('string'));
  70. it('number', () => expect(getType(42)).toBe('number'));
  71. it('boolean', () => expect(getType(true)).toBe('boolean'));
  72. it('array', () => expect(getType([1, 2])).toBe('array'));
  73. it('object', () => expect(getType({ a: 1 })).toBe('object'));
  74. it('bigint', () => expect(getType(BigInt('12345678901234567890'))).toBe('bigint'));
  75. it('BigNumber duck-type → bigint', () => {
  76. expect(getType({ s: 1, e: 5, c: [123456] })).toBe('bigint');
  77. });
  78. });
  79. // ═══════════════════════════════════════════════════════
  80. // 4. BigNumber 还原
  81. // ═══════════════════════════════════════════════════════
  82. describe('rebuildBigNumberFromParts', () => {
  83. it('还原正整数', () => {
  84. expect(rebuildBigNumberFromParts({ s: 1, e: 5, c: [123456] })).toBe('123456');
  85. });
  86. it('还原负整数', () => {
  87. expect(rebuildBigNumberFromParts({ s: -1, e: 5, c: [123456] })).toBe('-123456');
  88. });
  89. it('还原小数', () => {
  90. expect(rebuildBigNumberFromParts({ s: 1, e: 0, c: [1, 23000000000000] })).toBe('1.23');
  91. });
  92. it('还原纯小数 (0.xxx)', () => {
  93. const result = rebuildBigNumberFromParts({ s: 1, e: -1, c: [5] });
  94. expect(result).toBe('0.5');
  95. });
  96. });
  97. describe('getBigNumberDisplayString', () => {
  98. it('原生 BigInt', () => {
  99. expect(getBigNumberDisplayString(BigInt('995815895020119788889'))).toBe(
  100. '995815895020119788889',
  101. );
  102. });
  103. it('BigNumber duck-type 对象', () => {
  104. const bn = { s: 1, e: 2, c: [123] };
  105. const result = getBigNumberDisplayString(bn);
  106. expect(result).toBe('123');
  107. });
  108. it('普通值 fallback', () => {
  109. expect(getBigNumberDisplayString(42)).toBe('42');
  110. });
  111. });
  112. // ═══════════════════════════════════════════════════════
  113. // 5. parseWithBigInt — GitHub Issue 核心回归
  114. // ═══════════════════════════════════════════════════════
  115. describe('parseWithBigInt', () => {
  116. it('Issue: 16 位及以上整数保持精度', () => {
  117. const json = '{"id": 995815895020119788889}';
  118. const result = parseWithBigInt(json);
  119. expect(result.id).toBe(BigInt('995815895020119788889'));
  120. });
  121. it('Issue: 负大整数', () => {
  122. const json = '{"id": -9958158950201197888}';
  123. const result = parseWithBigInt(json);
  124. expect(result.id).toBe(BigInt('-9958158950201197888'));
  125. });
  126. it('15 位及以下整数保持为 number', () => {
  127. const json = '{"id": 123456789012345}';
  128. const result = parseWithBigInt(json);
  129. expect(typeof result.id).toBe('number');
  130. expect(result.id).toBe(123456789012345);
  131. });
  132. it('字符串内的大数字不被替换', () => {
  133. const json = '{"msg": "订单号:9958158950201197888"}';
  134. const result = parseWithBigInt(json);
  135. expect(typeof result.msg).toBe('string');
  136. expect(result.msg).toBe('订单号:9958158950201197888');
  137. });
  138. it('数组中的大整数', () => {
  139. const json = '[1234567890123456789, 42]';
  140. const result = parseWithBigInt(json);
  141. expect(result[0]).toBe(BigInt('1234567890123456789'));
  142. expect(result[1]).toBe(42);
  143. });
  144. it('嵌套对象中的大整数', () => {
  145. const json = '{"a": {"b": 1234567890123456789}}';
  146. const result = parseWithBigInt(json);
  147. expect(result.a.b).toBe(BigInt('1234567890123456789'));
  148. });
  149. it('宽松解析:单引号 key', () => {
  150. const json = "{'name': 'test', 'value': 123}";
  151. const result = parseWithBigInt(json);
  152. expect(result.name).toBe('test');
  153. });
  154. it('宽松解析:未加引号 key', () => {
  155. const json = '{name: "test", value: 123}';
  156. const result = parseWithBigInt(json);
  157. expect(result.name).toBe('test');
  158. });
  159. it('普通 JSON 解析(无大整数)', () => {
  160. const json = '{"a": 1, "b": "hello", "c": true, "d": null}';
  161. const result = parseWithBigInt(json);
  162. expect(result).toEqual({ a: 1, b: 'hello', c: true, d: null });
  163. });
  164. it('Issue: 科学计数法数字不丢精度', () => {
  165. const json = '{"val": 1.5e2}';
  166. const result = parseWithBigInt(json);
  167. expect(result.val).toBe(150);
  168. });
  169. it('Issue: 带尾零的小数', () => {
  170. const json = '{"val": 1.50}';
  171. const result = parseWithBigInt(json);
  172. expect(result.val).toBe(1.5);
  173. });
  174. });
  175. // ═══════════════════════════════════════════════════════
  176. // 6. deepParseJSONStrings — 嵌套 JSON 解包
  177. // ═══════════════════════════════════════════════════════
  178. describe('deepParseJSONStrings', () => {
  179. it('解包字符串内的 JSON 对象', () => {
  180. const obj = { data: '{"name":"test"}' };
  181. const result = deepParseJSONStrings(obj);
  182. expect(result.data).toEqual({ name: 'test' });
  183. });
  184. it('解包字符串内的 JSON 数组', () => {
  185. const obj = { list: '[1, 2, 3]' };
  186. const result = deepParseJSONStrings(obj);
  187. expect(result.list).toEqual([1, 2, 3]);
  188. });
  189. it('递归多层解包', () => {
  190. const inner = JSON.stringify({ x: 1 });
  191. const outer = { data: JSON.stringify({ nested: inner }) };
  192. const result = deepParseJSONStrings(outer);
  193. expect(result.data.nested).toEqual({ x: 1 });
  194. });
  195. it('非 JSON 字符串保持不变', () => {
  196. const obj = { msg: 'hello world' };
  197. expect(deepParseJSONStrings(obj)).toEqual({ msg: 'hello world' });
  198. });
  199. it('BigNumber duck-type 不被误解包', () => {
  200. const obj = { val: '{"s":1,"e":5,"c":[123456]}' };
  201. const result = deepParseJSONStrings(obj);
  202. expect(typeof result.val).toBe('string');
  203. });
  204. it('空字符串安全', () => {
  205. expect(deepParseJSONStrings({ a: '' })).toEqual({ a: '' });
  206. });
  207. it('数组内的嵌套 JSON', () => {
  208. const arr = ['{"k":"v"}', 'plain'];
  209. const result = deepParseJSONStrings(arr);
  210. expect(result[0]).toEqual({ k: 'v' });
  211. expect(result[1]).toBe('plain');
  212. });
  213. it('null / 原始值安全', () => {
  214. expect(deepParseJSONStrings(null)).toBeNull();
  215. expect(deepParseJSONStrings(42)).toBe(42);
  216. expect(deepParseJSONStrings('str')).toBe('str');
  217. });
  218. });
  219. // ═══════════════════════════════════════════════════════
  220. // 7. Unicode 编解码
  221. // ═══════════════════════════════════════════════════════
  222. describe('uniEncode / uniDecode', () => {
  223. it('中文编解码往返', () => {
  224. const str = '你好世界';
  225. expect(uniDecode(uniEncode(str))).toBe(str);
  226. });
  227. it('保留 JSON 结构字符', () => {
  228. const str = '{"key": "value"}';
  229. const encoded = uniEncode(str);
  230. expect(encoded).toContain('{');
  231. expect(encoded).toContain('}');
  232. expect(encoded).toContain(':');
  233. });
  234. it('特殊空白字符', () => {
  235. const str = 'a\tb\nc';
  236. const decoded = uniDecode(uniEncode(str));
  237. expect(decoded).toBe(str);
  238. });
  239. });
  240. // ═══════════════════════════════════════════════════════
  241. // 8. safeStringify — BigInt 保精度
  242. // ═══════════════════════════════════════════════════════
  243. describe('safeStringify', () => {
  244. it('BigInt 输出为裸数字', () => {
  245. const obj = { id: BigInt('9958158950201197888') };
  246. const result = safeStringify(obj);
  247. expect(result).toBe('{"id":9958158950201197888}');
  248. expect(result).not.toContain('"9958158950201197888"');
  249. });
  250. it('普通数字不受影响', () => {
  251. const obj = { val: 42 };
  252. expect(safeStringify(obj)).toBe('{"val":42}');
  253. });
  254. it('支持 space 缩进', () => {
  255. const obj = { a: 1 };
  256. const result = safeStringify(obj, 2);
  257. expect(result).toContain('\n');
  258. });
  259. it('嵌套 BigInt', () => {
  260. const obj = { data: { id: BigInt('1234567890123456789') } };
  261. const result = safeStringify(obj);
  262. expect(result).toContain('1234567890123456789');
  263. });
  264. });
  265. // ═══════════════════════════════════════════════════════
  266. // 9. formatDate(替代 Date.prototype.format)
  267. // ═══════════════════════════════════════════════════════
  268. describe('formatDate', () => {
  269. const date = new Date(2024, 0, 5, 9, 3, 7, 42); // 2024-01-05 09:03:07.042
  270. it('yyyy-MM-dd HH:mm:ss', () => {
  271. expect(formatDate(date, 'yyyy-MM-dd HH:mm:ss')).toBe('2024-01-05 09:03:07');
  272. });
  273. it('yy/M/d', () => {
  274. expect(formatDate(date, 'yy/M/d')).toBe('24/1/5');
  275. });
  276. it('毫秒格式化 SSS', () => {
  277. expect(formatDate(date, 'HH:mm:ss.SSS')).toBe('09:03:07.042');
  278. });
  279. it('非字符串 pattern 回退', () => {
  280. expect(formatDate(date, null)).toBe(date.toString());
  281. });
  282. });
  283. // ═══════════════════════════════════════════════════════
  284. // 10. getStringBytes
  285. // ═══════════════════════════════════════════════════════
  286. describe('getStringBytes', () => {
  287. it('纯 ASCII', () => {
  288. expect(getStringBytes('hello')).toBe(5);
  289. });
  290. it('中文字符', () => {
  291. expect(getStringBytes('你好')).toBeGreaterThan(2);
  292. });
  293. it('空字符串', () => {
  294. expect(getStringBytes('')).toBe(0);
  295. });
  296. });
  297. // ═══════════════════════════════════════════════════════
  298. // 11. createSafeToastHTML(XSS 防护)
  299. // ═══════════════════════════════════════════════════════
  300. describe('createSafeToastHTML', () => {
  301. it('转义 HTML 标签防止 XSS', () => {
  302. const html = createSafeToastHTML('<img src=x onerror=alert(1)>');
  303. expect(html).not.toContain('<img');
  304. expect(html).toContain('&lt;img');
  305. });
  306. it('正常内容安全输出', () => {
  307. const html = createSafeToastHTML('操作成功');
  308. expect(html).toContain('操作成功');
  309. expect(html).toContain('fehelper_alertmsg');
  310. });
  311. });
  312. // ═══════════════════════════════════════════════════════
  313. // 12. 综合回归:端到端 JSON 解析 → 格式化 → 输出
  314. // ═══════════════════════════════════════════════════════
  315. describe('端到端回归', () => {
  316. it('包含 BigInt 的完整 JSON 往返', () => {
  317. const input = '{"orderId": 9958158950201197888, "amount": 99.5, "name": "test"}';
  318. const parsed = parseWithBigInt(input);
  319. expect(parsed.orderId).toBe(BigInt('9958158950201197888'));
  320. expect(parsed.amount).toBe(99.5);
  321. expect(parsed.name).toBe('test');
  322. const output = safeStringify(parsed);
  323. expect(output).toContain('9958158950201197888');
  324. expect(output).not.toMatch(/\d+n[,\}]/); // 无 BigInt 后缀 "n"
  325. });
  326. it('嵌套转义 JSON + BigInt', () => {
  327. // inner 本身包含大整数的 JSON 字符串——deepParse 会递归解包至对象
  328. const inner = '{"id": 1234567890123456789}';
  329. const outer = { data: JSON.stringify({ nested: inner }) };
  330. const unpacked = deepParseJSONStrings(outer);
  331. // nested 被递归解包为对象
  332. expect(typeof unpacked.data.nested).toBe('object');
  333. expect(unpacked.data.nested.id).toBe(1234567890123456789);
  334. });
  335. it('JSONP 格式预处理', () => {
  336. const jsonp = 'callback({"status": 200, "id": 1234567890123456789})';
  337. const match = /^([\w.]+)\(\s*([\s\S]*)\s*\)$/gm.exec(jsonp);
  338. expect(match).not.toBeNull();
  339. expect(match[1]).toBe('callback');
  340. const parsed = parseWithBigInt(match[2]);
  341. expect(parsed.id).toBe(BigInt('1234567890123456789'));
  342. });
  343. it('Issue: URL 编码的 JSON', () => {
  344. const encoded = '%7B%22name%22%3A%22test%22%7D';
  345. const decoded = decodeURIComponent(encoded);
  346. expect(JSON.parse(decoded)).toEqual({ name: 'test' });
  347. });
  348. });