slimjson 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/test.js CHANGED
@@ -1,975 +1,975 @@
1
- /**
2
- * 测试:compress / decompress v2 —— Jest 版
3
- * 运行:npm test
4
- */
5
- const { compress, decompress, stringify, parse } = require('./compress');
6
-
7
- // ---------- 测试数据 ----------
8
-
9
- // 样例1
10
- const src1 = [
11
- {
12
- "姓名": "张三", "年龄": 19,
13
- "家人": [{ "姓名": "张四", "年龄": 40 }, { "姓名": "李五", "年龄": 41 }],
14
- "伴侣": { "姓名": "王六", "年龄": 18 }
15
- },
16
- {
17
- "姓名": "李小花", "年龄": 28,
18
- "家人": [{ "姓名": "李大国", "年龄": 55 }, { "姓名": "王淑芬", "年龄": 53 }],
19
- "伴侣": { "姓名": "赵明", "年龄": 30 }
20
- }
21
- ];
22
-
23
- // 样例2
24
- const src2 = [
25
- { "姓名": ["张三", "李四", "王五"], "班级": "23班" },
26
- { "姓名": ["李小花", "张晓", "李旺", "张思"], "班级": "24班" }
27
- ];
28
-
29
- // 场景3:第1个元素缺少"伴侣"字段(后端省略 null)
30
- const src3 = [
31
- { "姓名": "张三", "年龄": 19, "家人": [{ "姓名": "张四", "年龄": 40 }] },
32
- { "姓名": "李小花", "年龄": 28, "家人": [{ "姓名": "李大国", "年龄": 55 }], "伴侣": { "姓名": "赵明", "年龄": 30 } }
33
- ];
34
-
35
- // 场景4:嵌套对象内部子 key 不全(第1个家人中缺少"关系")
36
- const src4 = [
37
- {
38
- "姓名": "张三", "年龄": 19,
39
- "家人": [
40
- { "姓名": "张四", "年龄": 40 },
41
- { "姓名": "李五", "年龄": 41, "关系": "母亲" }
42
- ]
43
- }
44
- ];
45
-
46
- // 场景5:复杂嵌套 — 年级,班级,班主任,其他老师,学生(学生含成绩子对象)
47
- const src5 = [
48
- {
49
- "年级": "一年级",
50
- "班级": "1班",
51
- "班主任": { "姓名": "王老师", "年龄": 35, "科目": "语文" },
52
- "其他老师": [
53
- { "姓名": "李老师", "年龄": 28, "科目": "数学" },
54
- { "姓名": "张老师", "年龄": 40, "科目": "英语" }
55
- ],
56
- "学生": [
57
- { "姓名": "小明", "年龄": 7, "性别": "男", "成绩": { "语文": 95, "数学": 88, "英语": 92 } },
58
- { "姓名": "小红", "年龄": 7, "性别": "女", "成绩": { "语文": 90, "数学": 96, "英语": 89 } }
59
- ]
60
- },
61
- {
62
- "年级": "一年级",
63
- "班级": "2班",
64
- "班主任": { "姓名": "赵老师", "年龄": 42, "科目": "数学" },
65
- "其他老师": [
66
- { "姓名": "钱老师", "年龄": 31, "科目": "语文" },
67
- { "姓名": "孙老师", "年龄": 33, "科目": "英语" },
68
- { "姓名": "周老师", "年龄": 26, "科目": "体育" }
69
- ],
70
- "学生": [
71
- { "姓名": "小刚", "年龄": 7, "性别": "男", "成绩": { "语文": 78, "数学": 85 } },
72
- { "姓名": "小丽", "年龄": 7, "性别": "女", "成绩": { "语文": 99, "数学": 100, "英语": 97 } },
73
- { "姓名": "小强", "年龄": 8, "性别": "男", "成绩": { "数学": 60 } }
74
- ]
75
- }
76
- ];
77
-
78
- // decompress 始终返回规范化数据(补齐缺失 key 为 null)
79
- const src3Decompressed = [
80
- { "姓名": "张三", "年龄": 19, "家人": [{ "姓名": "张四", "年龄": 40 }], "伴侣": null },
81
- { "姓名": "李小花", "年龄": 28, "家人": [{ "姓名": "李大国", "年龄": 55 }], "伴侣": { "姓名": "赵明", "年龄": 30 } }
82
- ];
83
-
84
- const src4Decompressed = [
85
- {
86
- "姓名": "张三", "年龄": 19,
87
- "家人": [
88
- { "姓名": "张四", "年龄": 40, "关系": null },
89
- { "姓名": "李五", "年龄": 41, "关系": "母亲" }
90
- ]
91
- }
92
- ];
93
-
94
- const src5Decompressed = [
95
- {
96
- "年级": "一年级",
97
- "班级": "1班",
98
- "班主任": { "姓名": "王老师", "年龄": 35, "科目": "语文" },
99
- "其他老师": [
100
- { "姓名": "李老师", "年龄": 28, "科目": "数学" },
101
- { "姓名": "张老师", "年龄": 40, "科目": "英语" }
102
- ],
103
- "学生": [
104
- { "姓名": "小明", "年龄": 7, "性别": "男", "成绩": { "语文": 95, "数学": 88, "英语": 92 } },
105
- { "姓名": "小红", "年龄": 7, "性别": "女", "成绩": { "语文": 90, "数学": 96, "英语": 89 } }
106
- ]
107
- },
108
- {
109
- "年级": "一年级",
110
- "班级": "2班",
111
- "班主任": { "姓名": "赵老师", "年龄": 42, "科目": "数学" },
112
- "其他老师": [
113
- { "姓名": "钱老师", "年龄": 31, "科目": "语文" },
114
- { "姓名": "孙老师", "年龄": 33, "科目": "英语" },
115
- { "姓名": "周老师", "年龄": 26, "科目": "体育" }
116
- ],
117
- "学生": [
118
- { "姓名": "小刚", "年龄": 7, "性别": "男", "成绩": { "语文": 78, "数学": 85, "英语": null } },
119
- { "姓名": "小丽", "年龄": 7, "性别": "女", "成绩": { "语文": 99, "数学": 100, "英语": 97 } },
120
- { "姓名": "小强", "年龄": 8, "性别": "男", "成绩": { "语文": null, "数学": 60, "英语": null } }
121
- ]
122
- }
123
- ];
124
-
125
- // ============================================================
126
- // 测试
127
- // ============================================================
128
-
129
- describe('compress / decompress', () => {
130
-
131
- // —— compress / decompress / roundtrip 测试:trim=false 和 trim=true 两套 ——
132
- [false, true].forEach(trim => {
133
- const label = trim ? 'trimTrailingNulls' : '默认';
134
- const copt = trim ? { trimTrailingNulls: true } : undefined;
135
-
136
- describe(`[${label}]`, () => {
137
-
138
- describe('样例1:基础嵌套', () => {
139
- const r1 = compress(src1, copt);
140
-
141
- test('compress 不出错', () => {
142
- expect(r1.keys).toBeDefined();
143
- expect(r1.rows).toBeDefined();
144
- });
145
-
146
- test('与预期 keys 一致', () => {
147
- expect(r1.keys).toEqual([
148
- "姓名", "年龄", { "家人": ["姓名", "年龄"] }, { "伴侣": ["姓名", "年龄"] }
149
- ]);
150
- });
151
-
152
- test('还原一致', () => {
153
- expect(decompress(r1)).toEqual(src1);
154
- });
155
- });
156
-
157
- describe('样例2:原始类型数组字段', () => {
158
- const r2 = compress(src2, copt);
159
-
160
- test('与预期 keys 一致', () => {
161
- expect(r2.keys).toEqual(["姓名", "班级"]);
162
- });
163
-
164
- test('与预期 rows 一致', () => {
165
- expect(r2.rows).toEqual([
166
- [["张三", "李四", "王五"], "23班"],
167
- [["李小花", "张晓", "李旺", "张思"], "24班"]
168
- ]);
169
- });
170
-
171
- test('还原一致', () => {
172
- expect(decompress(r2)).toEqual(src2);
173
- });
174
- });
175
-
176
- describe('场景3:顶层缺失字段(后端省略 null)', () => {
177
- const r3 = compress(src3, copt);
178
-
179
- test('keys 包含伴侣', () => {
180
- expect(r3.keys).toEqual(["姓名", "年龄", { "家人": ["姓名", "年龄"] }, { "伴侣": ["姓名", "年龄"] }]);
181
- });
182
-
183
- test(`row[0] 伴侣${trim ? '被 trim' : '为 null'}`, () => {
184
- if (trim) {
185
- expect(r3.rows[0].length).toBe(3);
186
- } else {
187
- expect(r3.rows[0][3]).toBeNull();
188
- }
189
- });
190
-
191
- test('row[1] 伴侣正常', () => {
192
- expect(r3.rows[1][3]).toEqual(["赵明", 30]);
193
- });
194
-
195
- test('还原(null 补齐)', () => {
196
- expect(decompress(r3)).toEqual(src3Decompressed);
197
- });
198
-
199
- test('张三的伴侣为 null', () => {
200
- expect(decompress(r3)[0].伴侣).toBeNull();
201
- });
202
-
203
- test('李小花的伴侣正常', () => {
204
- expect(decompress(r3)[1].伴侣.姓名).toBe("赵明");
205
- });
206
- });
207
-
208
- describe('场景4:嵌套对象子 key 不全', () => {
209
- const r4 = compress(src4, copt);
210
-
211
- test('keys 中家人包含"关系"', () => {
212
- expect(r4.keys).toEqual(["姓名", "年龄", { "家人": ["姓名", "年龄", "关系"] }]);
213
- });
214
-
215
- test(`家人[0] 关系${trim ? '被 trim' : '为 null'}`, () => {
216
- if (trim) {
217
- expect(r4.rows[0][2][0].length).toBe(2);
218
- } else {
219
- expect(r4.rows[0][2][0][2]).toBeNull();
220
- }
221
- });
222
-
223
- test('家人[1] 关系正常', () => {
224
- expect(r4.rows[0][2][1][2]).toBe("母亲");
225
- });
226
-
227
- test('还原(null 补齐)', () => {
228
- expect(decompress(r4)).toEqual(src4Decompressed);
229
- });
230
-
231
- test('第1个家人关系为 null', () => {
232
- expect(decompress(r4)[0].家人[0].关系).toBeNull();
233
- });
234
-
235
- test('第2个家人关系正常', () => {
236
- expect(decompress(r4)[0].家人[1].关系).toBe("母亲");
237
- });
238
- });
239
-
240
- describe('场景5:复杂嵌套(年级/班级/班主任/其他老师/学生+成绩)', () => {
241
- const r5 = compress(src5, copt);
242
-
243
- // —— keys ——
244
- test('keys 顶层包含年级/班级', () => {
245
- expect(r5.keys.slice(0, 2)).toEqual(["年级", "班级"]);
246
- });
247
-
248
- test('keys 包含嵌套班主任', () => {
249
- expect(JSON.stringify(r5.keys[2])).toContain('班主任');
250
- });
251
-
252
- test('keys 包含嵌套其他老师', () => {
253
- expect(JSON.stringify(r5.keys[3])).toContain('其他老师');
254
- });
255
-
256
- test('keys 包含嵌套学生', () => {
257
- expect(JSON.stringify(r5.keys[4])).toContain('学生');
258
- });
259
-
260
- test('keys 完整结构', () => {
261
- expect(r5.keys).toEqual([
262
- "年级",
263
- "班级",
264
- { "班主任": ["姓名", "年龄", "科目"] },
265
- { "其他老师": ["姓名", "年龄", "科目"] },
266
- { "学生": ["姓名", "年龄", "性别", { "成绩": ["语文", "数学", "英语"] }] }
267
- ]);
268
- });
269
-
270
- // —— rows 数量 ——
271
- test('1班班主任正常', () => {
272
- expect(r5.rows[0][2]).toEqual(["王老师", 35, "语文"]);
273
- });
274
-
275
- test('1班其他老师数量=2', () => {
276
- expect(r5.rows[0][3].length).toBe(2);
277
- });
278
-
279
- test('1班学生数量=2', () => {
280
- expect(r5.rows[0][4].length).toBe(2);
281
- });
282
-
283
- test('2班班主任正常', () => {
284
- expect(r5.rows[1][2]).toEqual(["赵老师", 42, "数学"]);
285
- });
286
-
287
- test('2班其他老师数量=3', () => {
288
- expect(r5.rows[1][3].length).toBe(3);
289
- });
290
-
291
- test('2班学生数量=3', () => {
292
- expect(r5.rows[1][4].length).toBe(3);
293
- });
294
-
295
- // —— 成绩子对象缺失 key 补 null ——
296
- test('小刚英语成绩为 null(缺英语字段)', () => {
297
- expect(decompress(r5)[1]["学生"][0]["成绩"]["英语"]).toBeNull();
298
- });
299
-
300
- test('小刚语文成绩正常', () => {
301
- expect(r5.rows[1][4][0][3][0]).toBe(78);
302
- });
303
-
304
- test('小强语文成绩为 null(缺语文字段)', () => {
305
- expect(decompress(r5)[1]["学生"][2]["成绩"]["语文"]).toBeNull();
306
- });
307
-
308
- test('小强数学成绩正常', () => {
309
- const scores = r5.rows[1][4][2][3];
310
- // trim=true → [null, 60],数学在 index 1
311
- // trim=false → [null, 60, null],数学在 index 1
312
- expect(scores[1]).toBe(60);
313
- });
314
-
315
- test('小强英语成绩为 null(缺英语字段)', () => {
316
- expect(decompress(r5)[1]["学生"][2]["成绩"]["英语"]).toBeNull();
317
- });
318
-
319
- // —— 还原验证 ——
320
- test('还原后与 decompressed 一致', () => {
321
- expect(decompress(r5)).toEqual(src5Decompressed);
322
- });
323
-
324
- test('1班年级正确', () => {
325
- expect(decompress(r5)[0]["年级"]).toBe("一年级");
326
- });
327
-
328
- test('2班第1个学生英语为 null', () => {
329
- expect(decompress(r5)[1]["学生"][0]["成绩"]["英语"]).toBeNull();
330
- });
331
-
332
- test('2班第3个学生语文为 null', () => {
333
- expect(decompress(r5)[1]["学生"][2]["成绩"]["语文"]).toBeNull();
334
- });
335
- });
336
-
337
- /* =========================================================
338
- 边界 / 异常
339
- ========================================================= */
340
- describe('边界 / 异常', () => {
341
-
342
- test('compress 空数组 → 返回原值', () => {
343
- expect(compress([], copt)).toEqual([]);
344
- });
345
-
346
- test('compress(null) → 返回原值', () => {
347
- expect(compress(null, copt)).toBeNull();
348
- });
349
-
350
- test('compress 单个对象 → 自动包裹为数组', () => {
351
- const obj = { name: '张三', age: 25 };
352
- const r = compress(obj, copt);
353
- expect(r.keys).toEqual(['name', 'age']);
354
- expect(r.rows).toEqual([['张三', 25]]);
355
- expect(decompress(r)).toEqual([obj]);
356
- });
357
-
358
- test('compress 非数组 → 返回原值', () => {
359
- expect(compress('hello', copt)).toBe('hello');
360
- });
361
-
362
- test('源数组含 null 元素', () => {
363
- const src = [{ name: 'a', age: 10 }, null, { name: 'b' }];
364
- const r = compress(src, copt);
365
- expect(r.keys).toEqual(['name', 'age']);
366
- expect(r.rows[0]).toEqual(['a', 10]);
367
- expect(r.rows[2]).toEqual(trim ? ['b'] : ['b', null]);
368
- // null 元素行: [null, null] → trim 后 [],不 trim 仍是 [null, null]
369
- if (trim) {
370
- expect(r.rows[1]).toEqual([]);
371
- } else {
372
- expect(r.rows[1]).toEqual([null, null]);
373
- }
374
- expect(decompress(r)).toEqual([
375
- { name: 'a', age: 10 },
376
- { name: null, age: null },
377
- { name: 'b', age: null }
378
- ]);
379
- });
380
-
381
- test('空对象数组字段', () => {
382
- const src = [{ name: 'test', items: [] }];
383
- const r = compress(src, copt);
384
- expect(r.keys).toEqual(['name', 'items']);
385
- expect(r.rows[0]).toEqual(['test', []]);
386
- expect(decompress(r)).toEqual(src);
387
- });
388
-
389
- test('嵌套对象数组中含 null 元素(首元 null→退化为 primitive-array)', () => {
390
- const src = [{ name: 'a', kids: [null, { name: 'child' }] }];
391
- const r = compress(src, copt);
392
- // v[0]===null → 走 primitive-array 分支,不拆解子 key
393
- expect(r.keys).toEqual(['name', 'kids']);
394
- expect(r.rows[0]).toEqual(['a', [null, { name: 'child' }]]);
395
- expect(decompress(r)).toEqual(src);
396
- });
397
-
398
- test('字段值为 undefined 转 null', () => {
399
- const src = [{ a: undefined, b: 1 }];
400
- const r = compress(src, copt);
401
- expect(r.keys).toEqual(['a', 'b']);
402
- if (trim) {
403
- expect(r.rows[0]).toEqual([null, 1]); // a 的 null 被 trim
404
- } else {
405
- expect(r.rows[0]).toEqual([null, 1]);
406
- }
407
- });
408
-
409
- test('字段值为 null 在 decompress 中还原', () => {
410
- const src = [{ a: 1, b: null }];
411
- const r = compress(src, copt);
412
- expect(decompress(r)[0].b).toBeNull();
413
- if (trim) {
414
- expect(r.rows[0]).toEqual([1]); // b 的 null 被 trim
415
- } else {
416
- expect(r.rows[0]).toEqual([1, null]);
417
- }
418
- });
419
-
420
- test('对象数组中含非对象元素(item typeof!=="object" 分支)', () => {
421
- const src = [{ name: 'x', kids: [{ name: 'kid' }, 'string', null] }];
422
- const r = compress(src, copt);
423
- // 首元素是对象 → object-array;非对象元素在 buildKeys 收集时被跳过
424
- expect(r.keys).toEqual(['name', { kids: ['name'] }]);
425
- // 非对象元素 → buildRow 返回 [null]
426
- // trim 后 [null]→[],不 trim 保持 [null]
427
- const nonObj = trim ? [] : [null];
428
- expect(r.rows[0][1]).toEqual([['kid'], nonObj, nonObj]);
429
- });
430
-
431
- test('原数组含数字元素(typeof obj!=="object" 各分支)', () => {
432
- const src = [42, { name: 'a' }, true, { name: 'b' }];
433
- const r = compress(src, copt);
434
- expect(r.keys).toEqual(['name']);
435
- // 数字/布尔不是对象 → buildRow 返回 [null]
436
- const nonObj = trim ? [] : [null];
437
- expect(r.rows[0]).toEqual(nonObj);
438
- expect(r.rows[1]).toEqual(['a']);
439
- expect(r.rows[2]).toEqual(nonObj);
440
- expect(r.rows[3]).toEqual(['b']);
441
- });
442
-
443
- test('getValueKind:数组首元素也是数组 → primitive-array', () => {
444
- const src = [{ name: 'x', matrix: [[1, 2], [3, 4]] }];
445
- const r = compress(src, copt);
446
- expect(r.keys).toEqual(['name', 'matrix']);
447
- expect(r.rows[0]).toEqual(['x', [[1, 2], [3, 4]]]);
448
- expect(decompress(r)).toEqual(src);
449
- });
450
-
451
- test('普通原始类型数组 [1,2,3] 不拆解', () => {
452
- const src = [{ name: 'x', scores: [1, 2, 3] }];
453
- const r = compress(src, copt);
454
- expect(r.keys).toEqual(['name', 'scores']);
455
- expect(r.rows[0]).toEqual(['x', [1, 2, 3]]);
456
- expect(decompress(r)).toEqual(src);
457
- });
458
-
459
- test('非对象元素 + 嵌套 key(buildRow line 108 三元 false 分支)', () => {
460
- const src = [42, { name: 'a', detail: { x: 1 } }];
461
- const r = compress(src, copt);
462
- expect(r.keys).toEqual(['name', { detail: ['x'] }]);
463
- // 42 不是对象 → 全 null
464
- expect(r.rows[0]).toEqual(trim ? [] : [null, null]);
465
- expect(r.rows[1]).toEqual(['a', [1]]);
466
- });
467
-
468
- test('非对象元素 + 对象数组(buildKeys line 73 typeof false 分支)', () => {
469
- const src = [42, { items: [{ name: 'a' }] }];
470
- const r = compress(src, copt);
471
- expect(r.keys).toEqual([{ items: ['name'] }]);
472
- // 42 不是对象 → buildRow 返回 [null]
473
- expect(r.rows[0]).toEqual(trim ? [] : [null]);
474
- expect(r.rows[1]).toEqual([[['a']]]);
475
- });
476
-
477
- test('字段值为 null 且 repValue 为 object-array(line 75 Array.isArray false 分支)', () => {
478
- const src = [
479
- { items: [{ name: 'a' }] }, // items 是 object-array
480
- { items: null } // items 为 null → !Array.isArray
481
- ];
482
- const r = compress(src, copt);
483
- expect(r.keys).toEqual([{ items: ['name'] }]);
484
- expect(r.rows[0]).toEqual([[['a']]]);
485
- expect(r.rows[1]).toEqual(trim ? [] : [null]);
486
- });
487
-
488
- test('trimTrailingNulls 端到端', () => {
489
- const src = [
490
- { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
491
- { name: '李四', age: 35, profile: { avatar: 'b.jpg', file: null } },
492
- { name: '王五' },
493
- ];
494
- const r = compress(src, copt);
495
- if (trim) {
496
- expect(r.rows[0]).toEqual(['张三', 28, ['a.jpg', 'Hello']]);
497
- expect(r.rows[1]).toEqual(['李四', 35, ['b.jpg']]);
498
- expect(r.rows[2]).toEqual(['王五']);
499
- } else {
500
- expect(r.rows[0]).toEqual(['张三', 28, ['a.jpg', 'Hello', null]]);
501
- expect(r.rows[1]).toEqual(['李四', 35, ['b.jpg', null, null]]);
502
- expect(r.rows[2]).toEqual(['王五', null, null]);
503
- }
504
- expect(decompress(r)).toEqual([
505
- { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello', file: null } },
506
- { name: '李四', age: 35, profile: { avatar: 'b.jpg', bio: null, file: null } },
507
- { name: '王五', age: null, profile: null },
508
- ]);
509
- });
510
-
511
- });
512
-
513
- /* =========================================================
514
- stringify / parse 联用 roundtrip
515
- ========================================================= */
516
- describe('stringify / parse 联用 roundtrip', () => {
517
- test('样例1 往返:compress → stringify → parse → decompress', () => {
518
- const cmp = compress(src1, copt);
519
- const str = stringify(cmp);
520
- const restored = parse(str);
521
- expect(restored).toEqual(cmp);
522
- expect(decompress(restored)).toEqual(src1);
523
- });
524
-
525
- test('样例2 往返', () => {
526
- const cmp = compress(src2, copt);
527
- const str = stringify(cmp);
528
- const restored = parse(str);
529
- expect(restored).toEqual(cmp);
530
- expect(decompress(restored)).toEqual(src2);
531
- });
532
-
533
- test('场景3(缺失字段→null)往返', () => {
534
- const cmp = compress(src3, copt);
535
- const str = stringify(cmp);
536
- const restored = parse(str);
537
- expect(restored).toEqual(cmp);
538
- expect(decompress(restored)).toEqual(src3Decompressed);
539
- });
540
-
541
- test('场景4(嵌套子 key 不全)往返', () => {
542
- const cmp = compress(src4, copt);
543
- const str = stringify(cmp);
544
- const restored = parse(str);
545
- expect(restored).toEqual(cmp);
546
- expect(decompress(restored)).toEqual(src4Decompressed);
547
- });
548
-
549
- test('场景5(复杂嵌套+缺失成绩)往返', () => {
550
- const cmp = compress(src5, copt);
551
- const str = stringify(cmp);
552
- const restored = parse(str);
553
- expect(restored).toEqual(cmp);
554
- expect(decompress(restored)).toEqual(src5Decompressed);
555
- });
556
-
557
- test('trim 对 rows 长度的影响', () => {
558
- const src = [
559
- { name: 'a', extra: null },
560
- { name: 'b', extra: null }
561
- ];
562
- const cmp = compress(src, copt);
563
- // trim=true: 尾部 null 被移除,每行长度=1
564
- // trim=false: 尾部 null 保留,每行长度=2
565
- if (trim) {
566
- expect(cmp.rows[0].length).toBe(1);
567
- expect(cmp.rows[1].length).toBe(1);
568
- } else {
569
- expect(cmp.rows[0].length).toBe(2);
570
- expect(cmp.rows[1].length).toBe(2);
571
- }
572
- // stringify 往返正确
573
- const str = stringify(cmp);
574
- expect(parse(str)).toEqual(cmp);
575
- expect(decompress(parse(str))).toEqual([
576
- { name: 'a', extra: null },
577
- { name: 'b', extra: null }
578
- ]);
579
- });
580
- });
581
-
582
- }); // end [label] describe
583
- }); // end forEach
584
-
585
- /* =========================================================
586
- stringify / parse — 省略 null 文本化 与 还原
587
- (以下测试不依赖 compress,不需要按 trim 分组)
588
- ========================================================= */
589
- describe('stringify / parse', () => {
590
-
591
- // ---- 基础数组序列化 ----
592
- describe('基础数组规则', () => {
593
- test('全 null 数组 → [,,]', () => {
594
- expect(stringify([null, null, null])).toBe('[,,]');
595
- });
596
-
597
- test('["null", null, null] → ["null",,]', () => {
598
- expect(stringify(['null', null, null])).toBe('["null",,]');
599
- });
600
-
601
- test('[null, 1, null] → [,1,]', () => {
602
- expect(stringify([null, 1, null])).toBe('[,1,]');
603
- });
604
-
605
- test('[null, "a", null, 2] → [,a,,2]', () => {
606
- expect(stringify([null, 'a', null, 2])).toBe('[,a,,2]');
607
- });
608
-
609
- test('无 null → 正常', () => {
610
- expect(stringify([1, 2, 3])).toBe('[1,2,3]');
611
- expect(stringify(['a', 'b', 'c'])).toBe('[a,b,c]');
612
- });
613
-
614
- test('空数组 → []', () => {
615
- expect(stringify([])).toBe('[]');
616
- });
617
-
618
- test('单元素 null → [null](无法用逗号表示)', () => {
619
- expect(stringify([null])).toBe('[null]');
620
- });
621
- });
622
-
623
- // ---- 基础数组解析 ----
624
- describe('基础数组解析', () => {
625
- test('[,,] → [null, null, null]', () => {
626
- expect(parse('[,,]')).toEqual([null, null, null]);
627
- });
628
-
629
- test('["null",,] → ["null", null, null]', () => {
630
- expect(parse('["null",,]')).toEqual(['null', null, null]);
631
- });
632
-
633
- test('[,1,] → [null, 1, null]', () => {
634
- expect(parse('[,1,]')).toEqual([null, 1, null]);
635
- });
636
-
637
- test('[,"a",,2] → [null, "a", null, 2]', () => {
638
- expect(parse('[,"a",,2]')).toEqual([null, 'a', null, 2]);
639
- });
640
-
641
- test('无 null → 原样', () => {
642
- expect(parse('[1,2,3]')).toEqual([1, 2, 3]);
643
- expect(parse('["a","b","c"]')).toEqual(['a', 'b', 'c']);
644
- });
645
-
646
- test('空数组', () => {
647
- expect(parse('[]')).toEqual([]);
648
- });
649
-
650
- test('[,] → [null, null](两 null)', () => {
651
- expect(parse('[,]')).toEqual([null, null]);
652
- });
653
-
654
- test('[null] → [null](单 null 保留文字)', () => {
655
- expect(parse('[null]')).toEqual([null]);
656
- });
657
- });
658
-
659
- // ---- 嵌套数组 ----
660
- describe('嵌套数组', () => {
661
- test('[[1,2],,] → [[1,2], null, null]', () => {
662
- expect(stringify([[1, 2], null, null])).toBe('[[1,2],,]');
663
- expect(parse('[[1,2],,]')).toEqual([[1, 2], null, null]);
664
- });
665
-
666
- test('[[null,"a"],["b",null]] 往返', () => {
667
- const arr = [[null, 'a'], ['b', null]];
668
- expect(parse(stringify(arr))).toEqual(arr);
669
- });
670
-
671
- test('[[null,null,null]] → [[,,]]', () => {
672
- expect(stringify([[null, null, null]])).toBe('[[,,]]');
673
- expect(parse('[[,,]]')).toEqual([[null, null, null]]);
674
- });
675
-
676
- test('三层嵌套 [,[,],] 往返', () => {
677
- const arr = [null, [null], null];
678
- expect(parse(stringify(arr))).toEqual(arr);
679
- });
680
- });
681
-
682
- // ---- 对象序列化 ----
683
- describe('对象', () => {
684
- test('简单对象', () => {
685
- expect(stringify({ a: 1, b: null, c: 'x' }))
686
- .toBe('{a:1,b:null,c:x}');
687
- expect(parse('{a:1,b:null,c:x}')).toEqual({ a: 1, b: null, c: 'x' });
688
- });
689
-
690
- test('对象中数组字段含 null → 省略', () => {
691
- const obj = { name: 'test', items: [1, null, 3] };
692
- const str = stringify(obj);
693
- expect(str).toBe('{name:test,items:[1,,3]}');
694
- expect(parse(str)).toEqual(obj);
695
- });
696
-
697
- test('对象中嵌套数组全部 null(2个)', () => {
698
- const obj = { a: [null, null] };
699
- expect(stringify(obj)).toBe('{a:[,]}');
700
- expect(parse(stringify(obj))).toEqual(obj);
701
- });
702
-
703
- test('空对象', () => {
704
- expect(stringify({})).toBe('{}');
705
- expect(parse('{}')).toEqual({});
706
- });
707
- });
708
-
709
- // ---- 与 compress/decompress 联用(纯 stringify/parse,不按 trim 分组)----
710
- describe('与 compress/decompress 联用', () => {
711
- test('compress → stringify → parse → decompress(顶层缺失字段)', () => {
712
- const data = [
713
- { name: '张三', age: 25, city: '北京' },
714
- { name: '李四', age: 30 }
715
- ];
716
- const expected = [
717
- { name: '张三', age: 25, city: '北京' },
718
- { name: '李四', age: 30, city: null }
719
- ];
720
- const compressed = compress(data);
721
- const text = stringify(compressed);
722
- const parsed = parse(text);
723
- expect(decompress(parsed)).toEqual(expected);
724
- });
725
-
726
- test('含空格字符串保留引号', () => {
727
- expect(stringify(['hello world'])).toBe('["hello world"]');
728
- });
729
- });
730
-
731
- // ---- 边界 / 特殊值 ----
732
- describe('边界 / 特殊值', () => {
733
- test('含有布尔值', () => {
734
- const obj = { a: true, b: false, c: [true, null, false] };
735
- expect(parse(stringify(obj))).toEqual(obj);
736
- });
737
-
738
- test('含有数字(含负数、小数、科学计数法)', () => {
739
- const obj = { nums: [-1, 0, 3.14, 1e10, null] };
740
- expect(parse(stringify(obj))).toEqual(obj);
741
- });
742
-
743
- test('含有转义字符串', () => {
744
- const obj = { msg: 'hello "world"\n\t\\' };
745
- expect(parse(stringify(obj))).toEqual(obj);
746
- });
747
-
748
- test('字符串 "null" 与真正的 null 能区分', () => {
749
- const arr = ['null', null, 'null'];
750
- const str = stringify(arr);
751
- expect(str).toBe('["null",,"null"]');
752
- expect(parse(str)).toEqual(['null', null, 'null']);
753
- });
754
-
755
- test('NaN → null', () => {
756
- const str = stringify([NaN]);
757
- expect(str).toBe('[null]');
758
- expect(parse(str)).toEqual([null]);
759
- });
760
-
761
- test('undefined 视为 null', () => {
762
- const str = stringify([undefined, 1]);
763
- expect(str).toBe('[,1]');
764
- expect(parse(str)).toEqual([null, 1]);
765
- });
766
-
767
- test('全部 null 的嵌套结构', () => {
768
- const arr = [null, [null, null, null], null];
769
- const str = stringify(arr);
770
- expect(parse(str)).toEqual(arr);
771
- });
772
-
773
- test('空行 / 空白容忍', () => {
774
- expect(parse(' [ 1 , , 3 ] ')).toEqual([1, null, 3]);
775
- });
776
-
777
- test('parse 对非法输入抛错', () => {
778
- expect(() => parse('not json')).toThrow();
779
- expect(() => parse('[1,,')).toThrow();
780
- });
781
-
782
- // ---- 覆盖率补齐 ----
783
- test('非常规类型(Symbol)→ null', () => {
784
- expect(stringify([Symbol('x')])).toBe('[null]');
785
- });
786
-
787
- test('转义字符 \\/ \\b \\f \\r', () => {
788
- // 这些转义只在 parse 直接输入时出现(stringify 用 JSON.stringify 会直接输出字符)
789
- expect(parse('["a\\/b"]')).toEqual(['a/b']);
790
- expect(parse('["a\\bb"]')).toEqual(['a\bb']);
791
- expect(parse('["a\\fb"]')).toEqual(['a\fb']);
792
- expect(parse('["a\\rb"]')).toEqual(['a\rb']);
793
- });
794
-
795
- test('unicode 转义 \\uXXXX', () => {
796
- expect(parse('["\\u0041"]')).toEqual(['A']);
797
- expect(parse('["\\u4e2d"]')).toEqual(['中']);
798
- });
799
-
800
- test('未闭合引号报错', () => {
801
- expect(() => parse('"hello')).toThrow();
802
- });
803
-
804
- test('数组中非法字符报错', () => {
805
- expect(() => parse('[1x]')).toThrow();
806
- });
807
-
808
- test('对象中非法分隔符报错', () => {
809
- expect(() => parse('{key:1x}')).toThrow();
810
- });
811
-
812
- test('转义字符 default 分支(非标准转义)', () => {
813
- // \q 不是标准 JSON 转义,走 default 分支
814
- expect(parse('["a\\qb"]')).toEqual(['aqb']);
815
- });
816
-
817
- test('科学计数法 e+ e-', () => {
818
- expect(parse('[1e+10,1.5E-3]')).toEqual([1e+10, 1.5e-3]);
819
- });
820
-
821
- test('科学计数法 e-(单独测 - 分支)', () => {
822
- expect(parse('[1e-3]')).toEqual([0.001]);
823
- });
824
-
825
- test('过大数字 → null', () => {
826
- expect(parse('[1e1000]')).toEqual([null]);
827
- });
828
-
829
- test('对象 key 为 null/true/false(bare string 防御分支)', () => {
830
- expect(parse('{null:1,true:2,false:3}'))
831
- .toEqual({ null: 1, true: 2, false: 3 });
832
- });
833
-
834
- test('对象缺冒号报错', () => {
835
- expect(() => parse('{key}')).toThrow();
836
- });
837
-
838
- test('对象 key 为空报错', () => {
839
- expect(() => parse('{:1}')).toThrow();
840
- });
841
- });
842
-
843
- });
844
-
845
- /* =========================================================
846
- stringify 省略引号测试
847
- ========================================================= */
848
- describe('stringify 省略引号', () => {
849
-
850
- describe('基础:省略引号', () => {
851
- test('安全字符串省略引号', () => {
852
- expect(stringify(['hello'])).toBe('[hello]');
853
- });
854
-
855
- test('含空格的字符串保留引号', () => {
856
- expect(stringify(['hello world'])).toBe('["hello world"]');
857
- });
858
-
859
- test('含特殊字符的字符串保留引号', () => {
860
- expect(stringify(['a,b'])).toBe('["a,b"]');
861
- expect(stringify(['a:b'])).toBe('["a:b"]');
862
- expect(stringify(['[a]'])).toBe('["[a]"]');
863
- });
864
-
865
- test('空字符串保留引号', () => {
866
- expect(stringify([''])).toBe('[""]');
867
- });
868
-
869
- test('null/true/false 保留引号', () => {
870
- expect(stringify(['null'])).toBe('["null"]');
871
- expect(stringify(['true'])).toBe('["true"]');
872
- expect(stringify(['false'])).toBe('["false"]');
873
- });
874
-
875
- test('数字字符串保留引号', () => {
876
- expect(stringify(['123'])).toBe('["123"]');
877
- expect(stringify(['-5'])).toBe('["-5"]');
878
- expect(stringify(['3.14'])).toBe('["3.14"]');
879
- });
880
-
881
- test('数字开头的中文字符串保留引号', () => {
882
- // "23班" 以数字开头,parseValue 会误入 parseNumber 分支 → 必须保留引号
883
- expect(stringify(['23班'])).toBe('["23班"]');
884
- });
885
-
886
- test('中文字符串省略引号', () => {
887
- expect(stringify(['你好'])).toBe('[你好]');
888
- expect(stringify(['张三'])).toBe('[张三]');
889
- });
890
- });
891
-
892
- describe('数组中的省略引号', () => {
893
- test('数组元素全部省略引号', () => {
894
- expect(stringify(['a', 'b', 'c'])).toBe('[a,b,c]');
895
- });
896
-
897
- test('混合:安全的省略,不安全的保留', () => {
898
- expect(stringify(['hello', 'hello world', 'a,b']))
899
- .toBe('[hello,"hello world","a,b"]');
900
- });
901
-
902
- test('null 省略规则仍然生效', () => {
903
- expect(stringify(['a', null, 'b'])).toBe('[a,,b]');
904
- });
905
- });
906
-
907
- describe('对象中的省略引号', () => {
908
- test('对象 key 和值都省略引号', () => {
909
- expect(stringify({ name: 'alice', age: 25 }))
910
- .toBe('{name:alice,age:25}');
911
- });
912
-
913
- test('仅安全 key 省略,不安全 key 保留', () => {
914
- expect(stringify({ 'hello world': 1 }))
915
- .toBe('{"hello world":1}');
916
- });
917
-
918
- test('不安全值保留引号、安全 key 省略', () => {
919
- expect(stringify({ name: 'hello world' }))
920
- .toBe('{name:"hello world"}');
921
- });
922
- });
923
-
924
- describe('parse 兼容有/无引号格式', () => {
925
- test('解析无引号字符串', () => {
926
- expect(parse('[hello]')).toEqual(['hello']);
927
- expect(parse('[hello,world]')).toEqual(['hello', 'world']);
928
- });
929
-
930
- test('解析混合格式', () => {
931
- expect(parse('[hello,"world",123]')).toEqual(['hello', 'world', 123]);
932
- });
933
-
934
- test('解析含中文字符串', () => {
935
- expect(parse('[你好,张三]')).toEqual(['你好', '张三']);
936
- });
937
-
938
- test('关键字后无边界符时视为裸字符串', () => {
939
- expect(parse('[nullx]')).toEqual(['nullx']);
940
- expect(parse('[truex]')).toEqual(['truex']);
941
- });
942
-
943
- test('空槽仍为 null', () => {
944
- expect(parse('[,1,]')).toEqual([null, 1, null]);
945
- });
946
-
947
- test('解析对象 bare key', () => {
948
- expect(parse('{name:alice,age:25}')).toEqual({ name: 'alice', age: 25 });
949
- });
950
-
951
- test('解析对象混合 key', () => {
952
- expect(parse('{name:alice,"full name":"hello world"}'))
953
- .toEqual({ name: 'alice', 'full name': 'hello world' });
954
- });
955
-
956
- test('数字 key 保留引号 → 解析为字符串 key', () => {
957
- expect(parse('{"123":hello}')).toEqual({ '123': 'hello' });
958
- });
959
-
960
- test('对象 bare key 往返', () => {
961
- const obj = {
962
- name: '张三',
963
- age: 25,
964
- items: [1, null, 3],
965
- profile: { avatar: 'a.jpg', bio: 'hello' }
966
- };
967
- const str = stringify(obj);
968
- const parsed = parse(str);
969
- expect(parsed).toEqual(obj);
970
- });
971
- });
972
-
973
- });
974
-
975
- });
1
+ /**
2
+ * 测试:compress / decompress v2 —— Jest 版
3
+ * 运行:npm test
4
+ */
5
+ const { compress, decompress, stringify, parse } = require('./compress');
6
+
7
+ // ---------- 测试数据 ----------
8
+
9
+ // 样例1
10
+ const src1 = [
11
+ {
12
+ "姓名": "张三", "年龄": 19,
13
+ "家人": [{ "姓名": "张四", "年龄": 40 }, { "姓名": "李五", "年龄": 41 }],
14
+ "伴侣": { "姓名": "王六", "年龄": 18 }
15
+ },
16
+ {
17
+ "姓名": "李小花", "年龄": 28,
18
+ "家人": [{ "姓名": "李大国", "年龄": 55 }, { "姓名": "王淑芬", "年龄": 53 }],
19
+ "伴侣": { "姓名": "赵明", "年龄": 30 }
20
+ }
21
+ ];
22
+
23
+ // 样例2
24
+ const src2 = [
25
+ { "姓名": ["张三", "李四", "王五"], "班级": "23班" },
26
+ { "姓名": ["李小花", "张晓", "李旺", "张思"], "班级": "24班" }
27
+ ];
28
+
29
+ // 场景3:第1个元素缺少"伴侣"字段(后端省略 null)
30
+ const src3 = [
31
+ { "姓名": "张三", "年龄": 19, "家人": [{ "姓名": "张四", "年龄": 40 }] },
32
+ { "姓名": "李小花", "年龄": 28, "家人": [{ "姓名": "李大国", "年龄": 55 }], "伴侣": { "姓名": "赵明", "年龄": 30 } }
33
+ ];
34
+
35
+ // 场景4:嵌套对象内部子 key 不全(第1个家人中缺少"关系")
36
+ const src4 = [
37
+ {
38
+ "姓名": "张三", "年龄": 19,
39
+ "家人": [
40
+ { "姓名": "张四", "年龄": 40 },
41
+ { "姓名": "李五", "年龄": 41, "关系": "母亲" }
42
+ ]
43
+ }
44
+ ];
45
+
46
+ // 场景5:复杂嵌套 — 年级,班级,班主任,其他老师,学生(学生含成绩子对象)
47
+ const src5 = [
48
+ {
49
+ "年级": "一年级",
50
+ "班级": "1班",
51
+ "班主任": { "姓名": "王老师", "年龄": 35, "科目": "语文" },
52
+ "其他老师": [
53
+ { "姓名": "李老师", "年龄": 28, "科目": "数学" },
54
+ { "姓名": "张老师", "年龄": 40, "科目": "英语" }
55
+ ],
56
+ "学生": [
57
+ { "姓名": "小明", "年龄": 7, "性别": "男", "成绩": { "语文": 95, "数学": 88, "英语": 92 } },
58
+ { "姓名": "小红", "年龄": 7, "性别": "女", "成绩": { "语文": 90, "数学": 96, "英语": 89 } }
59
+ ]
60
+ },
61
+ {
62
+ "年级": "一年级",
63
+ "班级": "2班",
64
+ "班主任": { "姓名": "赵老师", "年龄": 42, "科目": "数学" },
65
+ "其他老师": [
66
+ { "姓名": "钱老师", "年龄": 31, "科目": "语文" },
67
+ { "姓名": "孙老师", "年龄": 33, "科目": "英语" },
68
+ { "姓名": "周老师", "年龄": 26, "科目": "体育" }
69
+ ],
70
+ "学生": [
71
+ { "姓名": "小刚", "年龄": 7, "性别": "男", "成绩": { "语文": 78, "数学": 85 } },
72
+ { "姓名": "小丽", "年龄": 7, "性别": "女", "成绩": { "语文": 99, "数学": 100, "英语": 97 } },
73
+ { "姓名": "小强", "年龄": 8, "性别": "男", "成绩": { "数学": 60 } }
74
+ ]
75
+ }
76
+ ];
77
+
78
+ // decompress 始终返回规范化数据(补齐缺失 key 为 null)
79
+ const src3Decompressed = [
80
+ { "姓名": "张三", "年龄": 19, "家人": [{ "姓名": "张四", "年龄": 40 }], "伴侣": null },
81
+ { "姓名": "李小花", "年龄": 28, "家人": [{ "姓名": "李大国", "年龄": 55 }], "伴侣": { "姓名": "赵明", "年龄": 30 } }
82
+ ];
83
+
84
+ const src4Decompressed = [
85
+ {
86
+ "姓名": "张三", "年龄": 19,
87
+ "家人": [
88
+ { "姓名": "张四", "年龄": 40, "关系": null },
89
+ { "姓名": "李五", "年龄": 41, "关系": "母亲" }
90
+ ]
91
+ }
92
+ ];
93
+
94
+ const src5Decompressed = [
95
+ {
96
+ "年级": "一年级",
97
+ "班级": "1班",
98
+ "班主任": { "姓名": "王老师", "年龄": 35, "科目": "语文" },
99
+ "其他老师": [
100
+ { "姓名": "李老师", "年龄": 28, "科目": "数学" },
101
+ { "姓名": "张老师", "年龄": 40, "科目": "英语" }
102
+ ],
103
+ "学生": [
104
+ { "姓名": "小明", "年龄": 7, "性别": "男", "成绩": { "语文": 95, "数学": 88, "英语": 92 } },
105
+ { "姓名": "小红", "年龄": 7, "性别": "女", "成绩": { "语文": 90, "数学": 96, "英语": 89 } }
106
+ ]
107
+ },
108
+ {
109
+ "年级": "一年级",
110
+ "班级": "2班",
111
+ "班主任": { "姓名": "赵老师", "年龄": 42, "科目": "数学" },
112
+ "其他老师": [
113
+ { "姓名": "钱老师", "年龄": 31, "科目": "语文" },
114
+ { "姓名": "孙老师", "年龄": 33, "科目": "英语" },
115
+ { "姓名": "周老师", "年龄": 26, "科目": "体育" }
116
+ ],
117
+ "学生": [
118
+ { "姓名": "小刚", "年龄": 7, "性别": "男", "成绩": { "语文": 78, "数学": 85, "英语": null } },
119
+ { "姓名": "小丽", "年龄": 7, "性别": "女", "成绩": { "语文": 99, "数学": 100, "英语": 97 } },
120
+ { "姓名": "小强", "年龄": 8, "性别": "男", "成绩": { "语文": null, "数学": 60, "英语": null } }
121
+ ]
122
+ }
123
+ ];
124
+
125
+ // ============================================================
126
+ // 测试
127
+ // ============================================================
128
+
129
+ describe('compress / decompress', () => {
130
+
131
+ // —— compress / decompress / roundtrip 测试:trim=false 和 trim=true 两套 ——
132
+ [false, true].forEach(trim => {
133
+ const label = trim ? 'trimTrailingNulls' : '默认';
134
+ const copt = trim ? { trimTrailingNulls: true } : undefined;
135
+
136
+ describe(`[${label}]`, () => {
137
+
138
+ describe('样例1:基础嵌套', () => {
139
+ const r1 = compress(src1, copt);
140
+
141
+ test('compress 不出错', () => {
142
+ expect(r1.schema).toBeDefined();
143
+ expect(r1.data).toBeDefined();
144
+ });
145
+
146
+ test('与预期 schema 一致', () => {
147
+ expect(r1.schema[0]).toEqual([
148
+ "姓名", "年龄", { "家人": [["姓名", "年龄"]] }, { "伴侣": ["姓名", "年龄"] }
149
+ ]);
150
+ });
151
+
152
+ test('还原一致', () => {
153
+ expect(decompress(r1)).toEqual(src1);
154
+ });
155
+ });
156
+
157
+ describe('样例2:原始类型数组字段', () => {
158
+ const r2 = compress(src2, copt);
159
+
160
+ test('与预期 schema 一致', () => {
161
+ expect(r2.schema[0]).toEqual(["姓名", "班级"]);
162
+ });
163
+
164
+ test('与预期 data 一致', () => {
165
+ expect(r2.data).toEqual([
166
+ [["张三", "李四", "王五"], "23班"],
167
+ [["李小花", "张晓", "李旺", "张思"], "24班"]
168
+ ]);
169
+ });
170
+
171
+ test('还原一致', () => {
172
+ expect(decompress(r2)).toEqual(src2);
173
+ });
174
+ });
175
+
176
+ describe('场景3:顶层缺失字段(后端省略 null)', () => {
177
+ const r3 = compress(src3, copt);
178
+
179
+ test('schema 包含伴侣', () => {
180
+ expect(r3.schema[0]).toEqual(["姓名", "年龄", { "家人": [["姓名", "年龄"]] }, { "伴侣": ["姓名", "年龄"] }]);
181
+ });
182
+
183
+ test(`row[0] 伴侣${trim ? '被 trim' : '为 null'}`, () => {
184
+ if (trim) {
185
+ expect(r3.data[0].length).toBe(3);
186
+ } else {
187
+ expect(r3.data[0][3]).toBeNull();
188
+ }
189
+ });
190
+
191
+ test('row[1] 伴侣正常', () => {
192
+ expect(r3.data[1][3]).toEqual(["赵明", 30]);
193
+ });
194
+
195
+ test('还原(null 补齐)', () => {
196
+ expect(decompress(r3)).toEqual(src3Decompressed);
197
+ });
198
+
199
+ test('张三的伴侣为 null', () => {
200
+ expect(decompress(r3)[0].伴侣).toBeNull();
201
+ });
202
+
203
+ test('李小花的伴侣正常', () => {
204
+ expect(decompress(r3)[1].伴侣.姓名).toBe("赵明");
205
+ });
206
+ });
207
+
208
+ describe('场景4:嵌套对象子 key 不全', () => {
209
+ const r4 = compress(src4, copt);
210
+
211
+ test('schema 中家人包含"关系"', () => {
212
+ expect(r4.schema[0]).toEqual(["姓名", "年龄", { "家人": [["姓名", "年龄", "关系"]] }]);
213
+ });
214
+
215
+ test(`家人[0] 关系${trim ? '被 trim' : '为 null'}`, () => {
216
+ if (trim) {
217
+ expect(r4.data[0][2][0].length).toBe(2);
218
+ } else {
219
+ expect(r4.data[0][2][0][2]).toBeNull();
220
+ }
221
+ });
222
+
223
+ test('家人[1] 关系正常', () => {
224
+ expect(r4.data[0][2][1][2]).toBe("母亲");
225
+ });
226
+
227
+ test('还原(null 补齐)', () => {
228
+ expect(decompress(r4)).toEqual(src4Decompressed);
229
+ });
230
+
231
+ test('第1个家人关系为 null', () => {
232
+ expect(decompress(r4)[0].家人[0].关系).toBeNull();
233
+ });
234
+
235
+ test('第2个家人关系正常', () => {
236
+ expect(decompress(r4)[0].家人[1].关系).toBe("母亲");
237
+ });
238
+ });
239
+
240
+ describe('场景5:复杂嵌套(年级/班级/班主任/其他老师/学生+成绩)', () => {
241
+ const r5 = compress(src5, copt);
242
+
243
+ // —— schema ——
244
+ test('schema 顶层包含年级/班级', () => {
245
+ expect(r5.schema[0].slice(0, 2)).toEqual(["年级", "班级"]);
246
+ });
247
+
248
+ test('schema 包含嵌套班主任', () => {
249
+ expect(JSON.stringify(r5.schema[0][2])).toContain('班主任');
250
+ });
251
+
252
+ test('schema 包含嵌套其他老师', () => {
253
+ expect(JSON.stringify(r5.schema[0][3])).toContain('其他老师');
254
+ });
255
+
256
+ test('schema 包含嵌套学生', () => {
257
+ expect(JSON.stringify(r5.schema[0][4])).toContain('学生');
258
+ });
259
+
260
+ test('schema 完整结构', () => {
261
+ expect(r5.schema[0]).toEqual([
262
+ "年级",
263
+ "班级",
264
+ { "班主任": ["姓名", "年龄", "科目"] },
265
+ { "其他老师": [["姓名", "年龄", "科目"]] },
266
+ { "学生": [["姓名", "年龄", "性别", { "成绩": ["语文", "数学", "英语"] }]] }
267
+ ]);
268
+ });
269
+
270
+ // —— data 数量 ——
271
+ test('1班班主任正常', () => {
272
+ expect(r5.data[0][2]).toEqual(["王老师", 35, "语文"]);
273
+ });
274
+
275
+ test('1班其他老师数量=2', () => {
276
+ expect(r5.data[0][3].length).toBe(2);
277
+ });
278
+
279
+ test('1班学生数量=2', () => {
280
+ expect(r5.data[0][4].length).toBe(2);
281
+ });
282
+
283
+ test('2班班主任正常', () => {
284
+ expect(r5.data[1][2]).toEqual(["赵老师", 42, "数学"]);
285
+ });
286
+
287
+ test('2班其他老师数量=3', () => {
288
+ expect(r5.data[1][3].length).toBe(3);
289
+ });
290
+
291
+ test('2班学生数量=3', () => {
292
+ expect(r5.data[1][4].length).toBe(3);
293
+ });
294
+
295
+ // —— 成绩子对象缺失 key 补 null ——
296
+ test('小刚英语成绩为 null(缺英语字段)', () => {
297
+ expect(decompress(r5)[1]["学生"][0]["成绩"]["英语"]).toBeNull();
298
+ });
299
+
300
+ test('小刚语文成绩正常', () => {
301
+ expect(r5.data[1][4][0][3][0]).toBe(78);
302
+ });
303
+
304
+ test('小强语文成绩为 null(缺语文字段)', () => {
305
+ expect(decompress(r5)[1]["学生"][2]["成绩"]["语文"]).toBeNull();
306
+ });
307
+
308
+ test('小强数学成绩正常', () => {
309
+ const scores = r5.data[1][4][2][3];
310
+ // trim=true → [null, 60],数学在 index 1
311
+ // trim=false → [null, 60, null],数学在 index 1
312
+ expect(scores[1]).toBe(60);
313
+ });
314
+
315
+ test('小强英语成绩为 null(缺英语字段)', () => {
316
+ expect(decompress(r5)[1]["学生"][2]["成绩"]["英语"]).toBeNull();
317
+ });
318
+
319
+ // —— 还原验证 ——
320
+ test('还原后与 decompressed 一致', () => {
321
+ expect(decompress(r5)).toEqual(src5Decompressed);
322
+ });
323
+
324
+ test('1班年级正确', () => {
325
+ expect(decompress(r5)[0]["年级"]).toBe("一年级");
326
+ });
327
+
328
+ test('2班第1个学生英语为 null', () => {
329
+ expect(decompress(r5)[1]["学生"][0]["成绩"]["英语"]).toBeNull();
330
+ });
331
+
332
+ test('2班第3个学生语文为 null', () => {
333
+ expect(decompress(r5)[1]["学生"][2]["成绩"]["语文"]).toBeNull();
334
+ });
335
+ });
336
+
337
+ /* =========================================================
338
+ 边界 / 异常
339
+ ========================================================= */
340
+ describe('边界 / 异常', () => {
341
+
342
+ test('compress 空数组 → 返回原值', () => {
343
+ expect(compress([], copt)).toEqual([]);
344
+ });
345
+
346
+ test('compress(null) → 返回原值', () => {
347
+ expect(compress(null, copt)).toBeNull();
348
+ });
349
+
350
+ test('compress 单个对象 → schema 未包裹,data 平铺', () => {
351
+ const obj = { name: '张三', age: 25 };
352
+ const r = compress(obj, copt);
353
+ expect(r.schema).toEqual(['name', 'age']);
354
+ expect(r.data).toEqual(['张三', 25]);
355
+ expect(decompress(r)).toEqual(obj);
356
+ });
357
+
358
+ test('compress 非数组 → 返回原值', () => {
359
+ expect(compress('hello', copt)).toBe('hello');
360
+ });
361
+
362
+ test('源数组含 null 元素', () => {
363
+ const src = [{ name: 'a', age: 10 }, null, { name: 'b' }];
364
+ const r = compress(src, copt);
365
+ expect(r.schema[0]).toEqual(['name', 'age']);
366
+ expect(r.data[0]).toEqual(['a', 10]);
367
+ expect(r.data[2]).toEqual(trim ? ['b'] : ['b', null]);
368
+ // null 元素行: [null, null] → trim 后 [],不 trim 仍是 [null, null]
369
+ if (trim) {
370
+ expect(r.data[1]).toEqual(null);
371
+ } else {
372
+ expect(r.data[1]).toEqual(null);
373
+ }
374
+ expect(decompress(r)).toEqual([
375
+ { name: 'a', age: 10 },
376
+ null,
377
+ { name: 'b', age: null }
378
+ ]);
379
+ });
380
+
381
+ test('空对象数组字段', () => {
382
+ const src = [{ name: 'test', items: [] }];
383
+ const r = compress(src, copt);
384
+ expect(r.schema[0]).toEqual(['name', 'items']);
385
+ expect(r.data[0]).toEqual(['test', []]);
386
+ expect(decompress(r)).toEqual(src);
387
+ });
388
+
389
+ test('嵌套对象数组中含 null 元素(首元 null→退化为 primitive-array)', () => {
390
+ const src = [{ name: 'a', kids: [null, { name: 'child' }] }];
391
+ const r = compress(src, copt);
392
+ // v[0]===null → 走 primitive-array 分支,不拆解子 key
393
+ expect(r.schema[0]).toEqual(['name', 'kids']);
394
+ expect(r.data[0]).toEqual(['a', [null, { name: 'child' }]]);
395
+ expect(decompress(r)).toEqual(src);
396
+ });
397
+
398
+ test('字段值为 undefined 转 null', () => {
399
+ const src = [{ a: undefined, b: 1 }];
400
+ const r = compress(src, copt);
401
+ expect(r.schema[0]).toEqual(['a', 'b']);
402
+ if (trim) {
403
+ expect(r.data[0]).toEqual([null, 1]); // a 的 null 被 trim
404
+ } else {
405
+ expect(r.data[0]).toEqual([null, 1]);
406
+ }
407
+ });
408
+
409
+ test('字段值为 null 在 decompress 中还原', () => {
410
+ const src = [{ a: 1, b: null }];
411
+ const r = compress(src, copt);
412
+ expect(decompress(r)[0].b).toBeNull();
413
+ if (trim) {
414
+ expect(r.data[0]).toEqual([1]); // b 的 null 被 trim
415
+ } else {
416
+ expect(r.data[0]).toEqual([1, null]);
417
+ }
418
+ });
419
+
420
+ test('对象数组中含非对象元素(item typeof!=="object" 分支)', () => {
421
+ const src = [{ name: 'x', kids: [{ name: 'kid' }, 'string', null] }];
422
+ const r = compress(src, copt);
423
+ // 首元素是对象 → object-array;非对象元素在 buildKeys 收集时被跳过
424
+ expect(r.schema[0]).toEqual(['name', { kids: [['name']] }]);
425
+ // 非对象元素 → buildRow 返回 [null]
426
+ // trim 后 [null]→[],不 trim 保持 [null]
427
+ const nonObj = trim ? [] : [null];
428
+ expect(r.data[0][1]).toEqual([['kid'], 'string', ...nonObj]);
429
+ });
430
+
431
+ test('原数组含数字元素(typeof obj!=="object" 各分支)', () => {
432
+ const src = [42, { name: 'a' }, true, { name: 'b' }];
433
+ const r = compress(src, copt);
434
+ expect(r.schema[0]).toEqual(['name']);
435
+ // 数字/布尔不是对象 → buildRow 返回 [null]
436
+ const nonObj = trim ? [] : [null];
437
+ expect(r.data[0]).toEqual(42);
438
+ expect(r.data[1]).toEqual(['a']);
439
+ expect(r.data[2]).toEqual(true);
440
+ expect(r.data[3]).toEqual(['b']);
441
+ });
442
+
443
+ test('getValueKind:数组首元素也是数组 → primitive-array', () => {
444
+ const src = [{ name: 'x', matrix: [[1, 2], [3, 4]] }];
445
+ const r = compress(src, copt);
446
+ expect(r.schema[0]).toEqual(['name', 'matrix']);
447
+ expect(r.data[0]).toEqual(['x', [[1, 2], [3, 4]]]);
448
+ expect(decompress(r)).toEqual(src);
449
+ });
450
+
451
+ test('普通原始类型数组 [1,2,3] 不拆解', () => {
452
+ const src = [{ name: 'x', scores: [1, 2, 3] }];
453
+ const r = compress(src, copt);
454
+ expect(r.schema[0]).toEqual(['name', 'scores']);
455
+ expect(r.data[0]).toEqual(['x', [1, 2, 3]]);
456
+ expect(decompress(r)).toEqual(src);
457
+ });
458
+
459
+ test('非对象元素 + 嵌套 key(buildRow line 108 三元 false 分支)', () => {
460
+ const src = [42, { name: 'a', detail: { x: 1 } }];
461
+ const r = compress(src, copt);
462
+ expect(r.schema[0]).toEqual(['name', { detail: ['x'] }]);
463
+ // 42 不是对象 → 全 null
464
+ expect(r.data[0]).toEqual(42);
465
+ expect(r.data[1]).toEqual(['a', [1]]);
466
+ });
467
+
468
+ test('非对象元素 + 对象数组(buildKeys line 73 typeof false 分支)', () => {
469
+ const src = [42, { items: [{ name: 'a' }] }];
470
+ const r = compress(src, copt);
471
+ expect(r.schema[0]).toEqual([{ items: [['name']] }]);
472
+ // 42 不是对象 → buildRow 返回 [null]
473
+ expect(r.data[0]).toEqual(42);
474
+ expect(r.data[1]).toEqual([[['a']]]);
475
+ });
476
+
477
+ test('字段值为 null 且 repValue 为 object-array(line 75 Array.isArray false 分支)', () => {
478
+ const src = [
479
+ { items: [{ name: 'a' }] }, // items 是 object-array
480
+ { items: null } // items 为 null → !Array.isArray
481
+ ];
482
+ const r = compress(src, copt);
483
+ expect(r.schema[0]).toEqual([{ items: [['name']] }]);
484
+ expect(r.data[0]).toEqual([[['a']]]);
485
+ expect(r.data[1]).toEqual(trim ? [] : [null]);
486
+ });
487
+
488
+ test('trimTrailingNulls 端到端', () => {
489
+ const src = [
490
+ { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
491
+ { name: '李四', age: 35, profile: { avatar: 'b.jpg', file: null } },
492
+ { name: '王五' },
493
+ ];
494
+ const r = compress(src, copt);
495
+ if (trim) {
496
+ expect(r.data[0]).toEqual(['张三', 28, ['a.jpg', 'Hello']]);
497
+ expect(r.data[1]).toEqual(['李四', 35, ['b.jpg']]);
498
+ expect(r.data[2]).toEqual(['王五']);
499
+ } else {
500
+ expect(r.data[0]).toEqual(['张三', 28, ['a.jpg', 'Hello', null]]);
501
+ expect(r.data[1]).toEqual(['李四', 35, ['b.jpg', null, null]]);
502
+ expect(r.data[2]).toEqual(['王五', null, null]);
503
+ }
504
+ expect(decompress(r)).toEqual([
505
+ { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello', file: null } },
506
+ { name: '李四', age: 35, profile: { avatar: 'b.jpg', bio: null, file: null } },
507
+ { name: '王五', age: null, profile: null },
508
+ ]);
509
+ });
510
+
511
+ });
512
+
513
+ /* =========================================================
514
+ stringify / parse 联用 roundtrip
515
+ ========================================================= */
516
+ describe('stringify / parse 联用 roundtrip', () => {
517
+ test('样例1 往返:compress → stringify → parse → decompress', () => {
518
+ const cmp = compress(src1, copt);
519
+ const str = stringify(cmp);
520
+ const restored = parse(str);
521
+ expect(restored).toEqual(cmp);
522
+ expect(decompress(restored)).toEqual(src1);
523
+ });
524
+
525
+ test('样例2 往返', () => {
526
+ const cmp = compress(src2, copt);
527
+ const str = stringify(cmp);
528
+ const restored = parse(str);
529
+ expect(restored).toEqual(cmp);
530
+ expect(decompress(restored)).toEqual(src2);
531
+ });
532
+
533
+ test('场景3(缺失字段→null)往返', () => {
534
+ const cmp = compress(src3, copt);
535
+ const str = stringify(cmp);
536
+ const restored = parse(str);
537
+ expect(restored).toEqual(cmp);
538
+ expect(decompress(restored)).toEqual(src3Decompressed);
539
+ });
540
+
541
+ test('场景4(嵌套子 key 不全)往返', () => {
542
+ const cmp = compress(src4, copt);
543
+ const str = stringify(cmp);
544
+ const restored = parse(str);
545
+ expect(restored).toEqual(cmp);
546
+ expect(decompress(restored)).toEqual(src4Decompressed);
547
+ });
548
+
549
+ test('场景5(复杂嵌套+缺失成绩)往返', () => {
550
+ const cmp = compress(src5, copt);
551
+ const str = stringify(cmp);
552
+ const restored = parse(str);
553
+ expect(restored).toEqual(cmp);
554
+ expect(decompress(restored)).toEqual(src5Decompressed);
555
+ });
556
+
557
+ test('trim 对 data 长度的影响', () => {
558
+ const src = [
559
+ { name: 'a', extra: null },
560
+ { name: 'b', extra: null }
561
+ ];
562
+ const cmp = compress(src, copt);
563
+ // trim=true: 尾部 null 被移除,每行长度=1
564
+ // trim=false: 尾部 null 保留,每行长度=2
565
+ if (trim) {
566
+ expect(cmp.data[0].length).toBe(1);
567
+ expect(cmp.data[1].length).toBe(1);
568
+ } else {
569
+ expect(cmp.data[0].length).toBe(2);
570
+ expect(cmp.data[1].length).toBe(2);
571
+ }
572
+ // stringify 往返正确
573
+ const str = stringify(cmp);
574
+ expect(parse(str)).toEqual(cmp);
575
+ expect(decompress(parse(str))).toEqual([
576
+ { name: 'a', extra: null },
577
+ { name: 'b', extra: null }
578
+ ]);
579
+ });
580
+ });
581
+
582
+ }); // end [label] describe
583
+ }); // end forEach
584
+
585
+ /* =========================================================
586
+ stringify / parse — 省略 null 文本化 与 还原
587
+ (以下测试不依赖 compress,不需要按 trim 分组)
588
+ ========================================================= */
589
+ describe('stringify / parse', () => {
590
+
591
+ // ---- 基础数组序列化 ----
592
+ describe('基础数组规则', () => {
593
+ test('全 null 数组 → [,,]', () => {
594
+ expect(stringify([null, null, null])).toBe('[,,]');
595
+ });
596
+
597
+ test('["null", null, null] → ["null",,]', () => {
598
+ expect(stringify(['null', null, null])).toBe('["null",,]');
599
+ });
600
+
601
+ test('[null, 1, null] → [,1,]', () => {
602
+ expect(stringify([null, 1, null])).toBe('[,1,]');
603
+ });
604
+
605
+ test('[null, "a", null, 2] → [,a,,2]', () => {
606
+ expect(stringify([null, 'a', null, 2])).toBe('[,a,,2]');
607
+ });
608
+
609
+ test('无 null → 正常', () => {
610
+ expect(stringify([1, 2, 3])).toBe('[1,2,3]');
611
+ expect(stringify(['a', 'b', 'c'])).toBe('[a,b,c]');
612
+ });
613
+
614
+ test('空数组 → []', () => {
615
+ expect(stringify([])).toBe('[]');
616
+ });
617
+
618
+ test('单元素 null → [null](无法用逗号表示)', () => {
619
+ expect(stringify([null])).toBe('[null]');
620
+ });
621
+ });
622
+
623
+ // ---- 基础数组解析 ----
624
+ describe('基础数组解析', () => {
625
+ test('[,,] → [null, null, null]', () => {
626
+ expect(parse('[,,]')).toEqual([null, null, null]);
627
+ });
628
+
629
+ test('["null",,] → ["null", null, null]', () => {
630
+ expect(parse('["null",,]')).toEqual(['null', null, null]);
631
+ });
632
+
633
+ test('[,1,] → [null, 1, null]', () => {
634
+ expect(parse('[,1,]')).toEqual([null, 1, null]);
635
+ });
636
+
637
+ test('[,"a",,2] → [null, "a", null, 2]', () => {
638
+ expect(parse('[,"a",,2]')).toEqual([null, 'a', null, 2]);
639
+ });
640
+
641
+ test('无 null → 原样', () => {
642
+ expect(parse('[1,2,3]')).toEqual([1, 2, 3]);
643
+ expect(parse('["a","b","c"]')).toEqual(['a', 'b', 'c']);
644
+ });
645
+
646
+ test('空数组', () => {
647
+ expect(parse('[]')).toEqual([]);
648
+ });
649
+
650
+ test('[,] → [null, null](两 null)', () => {
651
+ expect(parse('[,]')).toEqual([null, null]);
652
+ });
653
+
654
+ test('[null] → [null](单 null 保留文字)', () => {
655
+ expect(parse('[null]')).toEqual([null]);
656
+ });
657
+ });
658
+
659
+ // ---- 嵌套数组 ----
660
+ describe('嵌套数组', () => {
661
+ test('[[1,2],,] → [[1,2], null, null]', () => {
662
+ expect(stringify([[1, 2], null, null])).toBe('[[1,2],,]');
663
+ expect(parse('[[1,2],,]')).toEqual([[1, 2], null, null]);
664
+ });
665
+
666
+ test('[[null,"a"],["b",null]] 往返', () => {
667
+ const arr = [[null, 'a'], ['b', null]];
668
+ expect(parse(stringify(arr))).toEqual(arr);
669
+ });
670
+
671
+ test('[[null,null,null]] → [[,,]]', () => {
672
+ expect(stringify([[null, null, null]])).toBe('[[,,]]');
673
+ expect(parse('[[,,]]')).toEqual([[null, null, null]]);
674
+ });
675
+
676
+ test('三层嵌套 [,[,],] 往返', () => {
677
+ const arr = [null, [null], null];
678
+ expect(parse(stringify(arr))).toEqual(arr);
679
+ });
680
+ });
681
+
682
+ // ---- 对象序列化 ----
683
+ describe('对象', () => {
684
+ test('简单对象', () => {
685
+ expect(stringify({ a: 1, b: null, c: 'x' }))
686
+ .toBe('{a:1,b:null,c:x}');
687
+ expect(parse('{a:1,b:null,c:x}')).toEqual({ a: 1, b: null, c: 'x' });
688
+ });
689
+
690
+ test('对象中数组字段含 null → 省略', () => {
691
+ const obj = { name: 'test', items: [1, null, 3] };
692
+ const str = stringify(obj);
693
+ expect(str).toBe('{name:test,items:[1,,3]}');
694
+ expect(parse(str)).toEqual(obj);
695
+ });
696
+
697
+ test('对象中嵌套数组全部 null(2个)', () => {
698
+ const obj = { a: [null, null] };
699
+ expect(stringify(obj)).toBe('{a:[,]}');
700
+ expect(parse(stringify(obj))).toEqual(obj);
701
+ });
702
+
703
+ test('空对象', () => {
704
+ expect(stringify({})).toBe('{}');
705
+ expect(parse('{}')).toEqual({});
706
+ });
707
+ });
708
+
709
+ // ---- 与 compress/decompress 联用(纯 stringify/parse,不按 trim 分组)----
710
+ describe('与 compress/decompress 联用', () => {
711
+ test('compress → stringify → parse → decompress(顶层缺失字段)', () => {
712
+ const data = [
713
+ { name: '张三', age: 25, city: '北京' },
714
+ { name: '李四', age: 30 }
715
+ ];
716
+ const expected = [
717
+ { name: '张三', age: 25, city: '北京' },
718
+ { name: '李四', age: 30, city: null }
719
+ ];
720
+ const compressed = compress(data);
721
+ const text = stringify(compressed);
722
+ const parsed = parse(text);
723
+ expect(decompress(parsed)).toEqual(expected);
724
+ });
725
+
726
+ test('含空格字符串保留引号', () => {
727
+ expect(stringify(['hello world'])).toBe('["hello world"]');
728
+ });
729
+ });
730
+
731
+ // ---- 边界 / 特殊值 ----
732
+ describe('边界 / 特殊值', () => {
733
+ test('含有布尔值', () => {
734
+ const obj = { a: true, b: false, c: [true, null, false] };
735
+ expect(parse(stringify(obj))).toEqual(obj);
736
+ });
737
+
738
+ test('含有数字(含负数、小数、科学计数法)', () => {
739
+ const obj = { nums: [-1, 0, 3.14, 1e10, null] };
740
+ expect(parse(stringify(obj))).toEqual(obj);
741
+ });
742
+
743
+ test('含有转义字符串', () => {
744
+ const obj = { msg: 'hello "world"\n\t\\' };
745
+ expect(parse(stringify(obj))).toEqual(obj);
746
+ });
747
+
748
+ test('字符串 "null" 与真正的 null 能区分', () => {
749
+ const arr = ['null', null, 'null'];
750
+ const str = stringify(arr);
751
+ expect(str).toBe('["null",,"null"]');
752
+ expect(parse(str)).toEqual(['null', null, 'null']);
753
+ });
754
+
755
+ test('NaN → null', () => {
756
+ const str = stringify([NaN]);
757
+ expect(str).toBe('[null]');
758
+ expect(parse(str)).toEqual([null]);
759
+ });
760
+
761
+ test('undefined 视为 null', () => {
762
+ const str = stringify([undefined, 1]);
763
+ expect(str).toBe('[,1]');
764
+ expect(parse(str)).toEqual([null, 1]);
765
+ });
766
+
767
+ test('全部 null 的嵌套结构', () => {
768
+ const arr = [null, [null, null, null], null];
769
+ const str = stringify(arr);
770
+ expect(parse(str)).toEqual(arr);
771
+ });
772
+
773
+ test('空行 / 空白容忍', () => {
774
+ expect(parse(' [ 1 , , 3 ] ')).toEqual([1, null, 3]);
775
+ });
776
+
777
+ test('parse 对非法输入抛错', () => {
778
+ expect(() => parse('not json')).toThrow();
779
+ expect(() => parse('[1,,')).toThrow();
780
+ });
781
+
782
+ // ---- 覆盖率补齐 ----
783
+ test('非常规类型(Symbol)→ null', () => {
784
+ expect(stringify([Symbol('x')])).toBe('[null]');
785
+ });
786
+
787
+ test('转义字符 \\/ \\b \\f \\r', () => {
788
+ // 这些转义只在 parse 直接输入时出现(stringify 用 JSON.stringify 会直接输出字符)
789
+ expect(parse('["a\\/b"]')).toEqual(['a/b']);
790
+ expect(parse('["a\\bb"]')).toEqual(['a\bb']);
791
+ expect(parse('["a\\fb"]')).toEqual(['a\fb']);
792
+ expect(parse('["a\\rb"]')).toEqual(['a\rb']);
793
+ });
794
+
795
+ test('unicode 转义 \\uXXXX', () => {
796
+ expect(parse('["\\u0041"]')).toEqual(['A']);
797
+ expect(parse('["\\u4e2d"]')).toEqual(['中']);
798
+ });
799
+
800
+ test('未闭合引号报错', () => {
801
+ expect(() => parse('"hello')).toThrow();
802
+ });
803
+
804
+ test('数组中非法字符报错', () => {
805
+ expect(() => parse('[1x]')).toThrow();
806
+ });
807
+
808
+ test('对象中非法分隔符报错', () => {
809
+ expect(() => parse('{key:1x}')).toThrow();
810
+ });
811
+
812
+ test('转义字符 default 分支(非标准转义)', () => {
813
+ // \q 不是标准 JSON 转义,走 default 分支
814
+ expect(parse('["a\\qb"]')).toEqual(['aqb']);
815
+ });
816
+
817
+ test('科学计数法 e+ e-', () => {
818
+ expect(parse('[1e+10,1.5E-3]')).toEqual([1e+10, 1.5e-3]);
819
+ });
820
+
821
+ test('科学计数法 e-(单独测 - 分支)', () => {
822
+ expect(parse('[1e-3]')).toEqual([0.001]);
823
+ });
824
+
825
+ test('过大数字 → null', () => {
826
+ expect(parse('[1e1000]')).toEqual([null]);
827
+ });
828
+
829
+ test('对象 key 为 null/true/false(bare string 防御分支)', () => {
830
+ expect(parse('{null:1,true:2,false:3}'))
831
+ .toEqual({ null: 1, true: 2, false: 3 });
832
+ });
833
+
834
+ test('对象缺冒号报错', () => {
835
+ expect(() => parse('{key}')).toThrow();
836
+ });
837
+
838
+ test('对象 key 为空报错', () => {
839
+ expect(() => parse('{:1}')).toThrow();
840
+ });
841
+ });
842
+
843
+ });
844
+
845
+ /* =========================================================
846
+ stringify 省略引号测试
847
+ ========================================================= */
848
+ describe('stringify 省略引号', () => {
849
+
850
+ describe('基础:省略引号', () => {
851
+ test('安全字符串省略引号', () => {
852
+ expect(stringify(['hello'])).toBe('[hello]');
853
+ });
854
+
855
+ test('含空格的字符串保留引号', () => {
856
+ expect(stringify(['hello world'])).toBe('["hello world"]');
857
+ });
858
+
859
+ test('含特殊字符的字符串保留引号', () => {
860
+ expect(stringify(['a,b'])).toBe('["a,b"]');
861
+ expect(stringify(['a:b'])).toBe('["a:b"]');
862
+ expect(stringify(['[a]'])).toBe('["[a]"]');
863
+ });
864
+
865
+ test('空字符串保留引号', () => {
866
+ expect(stringify([''])).toBe('[""]');
867
+ });
868
+
869
+ test('null/true/false 保留引号', () => {
870
+ expect(stringify(['null'])).toBe('["null"]');
871
+ expect(stringify(['true'])).toBe('["true"]');
872
+ expect(stringify(['false'])).toBe('["false"]');
873
+ });
874
+
875
+ test('数字字符串保留引号', () => {
876
+ expect(stringify(['123'])).toBe('["123"]');
877
+ expect(stringify(['-5'])).toBe('["-5"]');
878
+ expect(stringify(['3.14'])).toBe('["3.14"]');
879
+ });
880
+
881
+ test('数字开头的中文字符串保留引号', () => {
882
+ // "23班" 以数字开头,parseValue 会误入 parseNumber 分支 → 必须保留引号
883
+ expect(stringify(['23班'])).toBe('["23班"]');
884
+ });
885
+
886
+ test('中文字符串省略引号', () => {
887
+ expect(stringify(['你好'])).toBe('[你好]');
888
+ expect(stringify(['张三'])).toBe('[张三]');
889
+ });
890
+ });
891
+
892
+ describe('数组中的省略引号', () => {
893
+ test('数组元素全部省略引号', () => {
894
+ expect(stringify(['a', 'b', 'c'])).toBe('[a,b,c]');
895
+ });
896
+
897
+ test('混合:安全的省略,不安全的保留', () => {
898
+ expect(stringify(['hello', 'hello world', 'a,b']))
899
+ .toBe('[hello,"hello world","a,b"]');
900
+ });
901
+
902
+ test('null 省略规则仍然生效', () => {
903
+ expect(stringify(['a', null, 'b'])).toBe('[a,,b]');
904
+ });
905
+ });
906
+
907
+ describe('对象中的省略引号', () => {
908
+ test('对象 key 和值都省略引号', () => {
909
+ expect(stringify({ name: 'alice', age: 25 }))
910
+ .toBe('{name:alice,age:25}');
911
+ });
912
+
913
+ test('仅安全 key 省略,不安全 key 保留', () => {
914
+ expect(stringify({ 'hello world': 1 }))
915
+ .toBe('{"hello world":1}');
916
+ });
917
+
918
+ test('不安全值保留引号、安全 key 省略', () => {
919
+ expect(stringify({ name: 'hello world' }))
920
+ .toBe('{name:"hello world"}');
921
+ });
922
+ });
923
+
924
+ describe('parse 兼容有/无引号格式', () => {
925
+ test('解析无引号字符串', () => {
926
+ expect(parse('[hello]')).toEqual(['hello']);
927
+ expect(parse('[hello,world]')).toEqual(['hello', 'world']);
928
+ });
929
+
930
+ test('解析混合格式', () => {
931
+ expect(parse('[hello,"world",123]')).toEqual(['hello', 'world', 123]);
932
+ });
933
+
934
+ test('解析含中文字符串', () => {
935
+ expect(parse('[你好,张三]')).toEqual(['你好', '张三']);
936
+ });
937
+
938
+ test('关键字后无边界符时视为裸字符串', () => {
939
+ expect(parse('[nullx]')).toEqual(['nullx']);
940
+ expect(parse('[truex]')).toEqual(['truex']);
941
+ });
942
+
943
+ test('空槽仍为 null', () => {
944
+ expect(parse('[,1,]')).toEqual([null, 1, null]);
945
+ });
946
+
947
+ test('解析对象 bare key', () => {
948
+ expect(parse('{name:alice,age:25}')).toEqual({ name: 'alice', age: 25 });
949
+ });
950
+
951
+ test('解析对象混合 key', () => {
952
+ expect(parse('{name:alice,"full name":"hello world"}'))
953
+ .toEqual({ name: 'alice', 'full name': 'hello world' });
954
+ });
955
+
956
+ test('数字 key 保留引号 → 解析为字符串 key', () => {
957
+ expect(parse('{"123":hello}')).toEqual({ '123': 'hello' });
958
+ });
959
+
960
+ test('对象 bare key 往返', () => {
961
+ const obj = {
962
+ name: '张三',
963
+ age: 25,
964
+ items: [1, null, 3],
965
+ profile: { avatar: 'a.jpg', bio: 'hello' }
966
+ };
967
+ const str = stringify(obj);
968
+ const parsed = parse(str);
969
+ expect(parsed).toEqual(obj);
970
+ });
971
+ });
972
+
973
+ });
974
+
975
+ });