slimjson 1.0.2 → 1.0.4

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
@@ -31,10 +31,6 @@ const src3 = [
31
31
  { "姓名": "张三", "年龄": 19, "家人": [{ "姓名": "张四", "年龄": 40 }] },
32
32
  { "姓名": "李小花", "年龄": 28, "家人": [{ "姓名": "李大国", "年龄": 55 }], "伴侣": { "姓名": "赵明", "年龄": 30 } }
33
33
  ];
34
- const src3Normalized = [ // 所有对象补齐缺失 key 为 null
35
- { "姓名": "张三", "年龄": 19, "家人": [{ "姓名": "张四", "年龄": 40 }], "伴侣": null },
36
- { "姓名": "李小花", "年龄": 28, "家人": [{ "姓名": "李大国", "年龄": 55 }], "伴侣": { "姓名": "赵明", "年龄": 30 } }
37
- ];
38
34
 
39
35
  // 场景4:嵌套对象内部子 key 不全(第1个家人中缺少"关系")
40
36
  const src4 = [
@@ -46,15 +42,6 @@ const src4 = [
46
42
  ]
47
43
  }
48
44
  ];
49
- const src4Normalized = [
50
- {
51
- "姓名": "张三", "年龄": 19,
52
- "家人": [
53
- { "姓名": "张四", "年龄": 40, "关系": null },
54
- { "姓名": "李五", "年龄": 41, "关系": "母亲" }
55
- ]
56
- }
57
- ];
58
45
 
59
46
  // 场景5:复杂嵌套 — 年级,班级,班主任,其他老师,学生(学生含成绩子对象)
60
47
  const src5 = [
@@ -88,7 +75,23 @@ const src5 = [
88
75
  }
89
76
  ];
90
77
 
91
- const src5Normalized = [
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 = [
92
95
  {
93
96
  "年级": "一年级",
94
97
  "班级": "1班",
@@ -125,322 +128,463 @@ const src5Normalized = [
125
128
 
126
129
  describe('compress / decompress', () => {
127
130
 
128
- describe('样例1:基础嵌套', () => {
129
- const r1 = compress(src1);
130
-
131
- test('compress 不出错', () => {
132
- expect(r1.keys).toBeDefined();
133
- expect(r1.rows).toBeDefined();
134
- });
135
-
136
- test('与预期 keys 一致', () => {
137
- expect(r1.keys).toEqual([
138
- "姓名", "年龄", { "家人": ["姓名", "年龄"] }, { "伴侣": ["姓名", "年龄"] }
139
- ]);
140
- });
141
-
142
- test('还原一致', () => {
143
- expect(decompress(r1)).toEqual(src1);
144
- });
145
- });
146
-
147
- describe('样例2:原始类型数组字段', () => {
148
- const r2 = compress(src2);
149
-
150
- test('与预期 keys 一致', () => {
151
- expect(r2.keys).toEqual(["姓名", "班级"]);
152
- });
153
-
154
- test('与预期 rows 一致', () => {
155
- expect(r2.rows).toEqual([
156
- [["张三", "李四", "王五"], "23班"],
157
- [["李小花", "张晓", "李旺", "张思"], "24班"]
158
- ]);
159
- });
160
-
161
- test('还原一致', () => {
162
- expect(decompress(r2)).toEqual(src2);
163
- });
164
- });
165
-
166
- describe('场景3:顶层缺失字段(后端省略 null)', () => {
167
- const r3 = compress(src3);
168
-
169
- test('keys 包含伴侣', () => {
170
- expect(r3.keys).toEqual(["姓名", "年龄", { "家人": ["姓名", "年龄"] }, { "伴侣": ["姓名", "年龄"] }]);
171
- });
172
-
173
- test('row[0] 伴侣位置为 null', () => {
174
- expect(r3.rows[0][3]).toBeNull();
175
- });
176
-
177
- test('row[1] 伴侣正常', () => {
178
- expect(r3.rows[1][3]).toEqual(["赵明", 30]);
179
- });
180
-
181
- test('还原(null 补齐)', () => {
182
- expect(decompress(r3)).toEqual(src3Normalized);
183
- });
184
-
185
- test('张三的伴侣为 null', () => {
186
- expect(decompress(r3)[0].伴侣).toBeNull();
187
- });
188
-
189
- test('李小花的伴侣正常', () => {
190
- expect(decompress(r3)[1].伴侣.姓名).toBe("赵明");
191
- });
192
- });
193
-
194
- describe('场景4:嵌套对象子 key 不全', () => {
195
- const r4 = compress(src4);
196
-
197
- test('keys 中家人包含"关系"', () => {
198
- expect(r4.keys).toEqual(["姓名", "年龄", { "家人": ["姓名", "年龄", "关系"] }]);
199
- });
200
-
201
- test('家人[0] 关系为 null', () => {
202
- expect(r4.rows[0][2][0][2]).toBeNull();
203
- });
204
-
205
- test('家人[1] 关系正常', () => {
206
- expect(r4.rows[0][2][1][2]).toBe("母亲");
207
- });
208
-
209
- test('还原(null 补齐)', () => {
210
- expect(decompress(r4)).toEqual(src4Normalized);
211
- });
212
-
213
- test('第1个家人关系为 null', () => {
214
- expect(decompress(r4)[0].家人[0].关系).toBeNull();
215
- });
216
-
217
- test('第2个家人关系正常', () => {
218
- expect(decompress(r4)[0].家人[1].关系).toBe("母亲");
219
- });
220
- });
221
-
222
- describe('场景5:复杂嵌套(年级/班级/班主任/其他老师/学生+成绩)', () => {
223
- const r5 = compress(src5);
224
-
225
- // —— keys ——
226
- test('keys 顶层包含年级/班级', () => {
227
- expect(r5.keys.slice(0, 2)).toEqual(["年级", "班级"]);
228
- });
229
-
230
- test('keys 包含嵌套班主任', () => {
231
- expect(JSON.stringify(r5.keys[2])).toContain('班主任');
232
- });
233
-
234
- test('keys 包含嵌套其他老师', () => {
235
- expect(JSON.stringify(r5.keys[3])).toContain('其他老师');
236
- });
237
-
238
- test('keys 包含嵌套学生', () => {
239
- expect(JSON.stringify(r5.keys[4])).toContain('学生');
240
- });
241
-
242
- test('keys 完整结构', () => {
243
- expect(r5.keys).toEqual([
244
- "年级",
245
- "班级",
246
- { "班主任": ["姓名", "年龄", "科目"] },
247
- { "其他老师": ["姓名", "年龄", "科目"] },
248
- { "学生": ["姓名", "年龄", "性别", { "成绩": ["语文", "数学", "英语"] }] }
249
- ]);
250
- });
251
-
252
- // —— rows 数量 ——
253
- test('1班班主任正常', () => {
254
- expect(r5.rows[0][2]).toEqual(["王老师", 35, "语文"]);
255
- });
256
-
257
- test('1班其他老师数量=2', () => {
258
- expect(r5.rows[0][3].length).toBe(2);
259
- });
260
-
261
- test('1班学生数量=2', () => {
262
- expect(r5.rows[0][4].length).toBe(2);
263
- });
264
-
265
- test('2班班主任正常', () => {
266
- expect(r5.rows[1][2]).toEqual(["赵老师", 42, "数学"]);
267
- });
268
-
269
- test('2班其他老师数量=3', () => {
270
- expect(r5.rows[1][3].length).toBe(3);
271
- });
272
-
273
- test('2班学生数量=3', () => {
274
- expect(r5.rows[1][4].length).toBe(3);
275
- });
276
-
277
- // —— 成绩子对象缺失 key 补 null ——
278
- // 学生 keys: ["姓名", "年龄", "性别", { "成绩": ["语文", "数学", "英语"] }]
279
- // 学生 row: [姓名, 年龄, 性别, [语文, 数学, 英语]]
280
- test('小刚英语成绩为 null(缺英语字段)', () => {
281
- expect(r5.rows[1][4][0][3][2]).toBeNull();
282
- });
283
-
284
- test('小刚语文成绩正常', () => {
285
- expect(r5.rows[1][4][0][3][0]).toBe(78);
286
- });
287
-
288
- test('小强语文成绩为 null(缺语文字段)', () => {
289
- expect(r5.rows[1][4][2][3][0]).toBeNull();
290
- });
291
-
292
- test('小强数学成绩正常', () => {
293
- expect(r5.rows[1][4][2][3][1]).toBe(60);
294
- });
295
-
296
- test('小强英语成绩为 null(缺英语字段)', () => {
297
- expect(r5.rows[1][4][2][3][2]).toBeNull();
298
- });
299
-
300
- // —— 还原验证 ——
301
- test('还原后与 normalized 一致', () => {
302
- expect(decompress(r5)).toEqual(src5Normalized);
303
- });
304
-
305
- test('1班年级正确', () => {
306
- expect(decompress(r5)[0]["年级"]).toBe("一年级");
307
- });
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;
308
135
 
309
- test('2班第1个学生英语为 null', () => {
310
- expect(decompress(r5)[1]["学生"][0]["成绩"]["英语"]).toBeNull();
311
- });
136
+ describe(`[${label}]`, () => {
312
137
 
313
- test('2班第3个学生语文为 null', () => {
314
- expect(decompress(r5)[1]["学生"][2]["成绩"]["语文"]).toBeNull();
315
- });
316
- });
138
+ describe('样例1:基础嵌套', () => {
139
+ const r1 = compress(src1, copt);
317
140
 
318
- /* =========================================================
319
- 边界 / 异常 — 冲击 100% 覆盖率
320
- ========================================================= */
321
- describe('边界 / 异常', () => {
141
+ test('compress 不出错', () => {
142
+ expect(r1.keys).toBeDefined();
143
+ expect(r1.rows).toBeDefined();
144
+ });
322
145
 
323
- test('compress 空数组 → 返回原值', () => {
324
- expect(compress([])).toEqual([]);
325
- });
146
+ test('与预期 keys 一致', () => {
147
+ expect(r1.keys).toEqual([
148
+ "姓名", "年龄", { "家人": ["姓名", "年龄"] }, { "伴侣": ["姓名", "年龄"] }
149
+ ]);
150
+ });
326
151
 
327
- test('compress(null) → 返回原值', () => {
328
- expect(compress(null)).toBeNull();
329
- });
152
+ test('还原一致', () => {
153
+ expect(decompress(r1)).toEqual(src1);
154
+ });
155
+ });
330
156
 
331
- test('compress 非数组 → 返回原值', () => {
332
- expect(compress('hello')).toBe('hello');
333
- });
157
+ describe('样例2:原始类型数组字段', () => {
158
+ const r2 = compress(src2, copt);
334
159
 
335
- test('源数组含 null 元素', () => {
336
- const src = [{ name: 'a', age: 10 }, null, { name: 'b' }];
337
- const r = compress(src);
338
- expect(r.keys).toEqual(['name', 'age']);
339
- expect(r.rows[1]).toEqual([null, null]);
340
- expect(r.rows[0]).toEqual(['a', 10]);
341
- expect(r.rows[2]).toEqual(['b', null]);
342
- });
160
+ test('与预期 keys 一致', () => {
161
+ expect(r2.keys).toEqual(["姓名", "班级"]);
162
+ });
343
163
 
344
- test('空对象数组字段', () => {
345
- const src = [{ name: 'test', items: [] }];
346
- const r = compress(src);
347
- expect(r.keys).toEqual(['name', 'items']);
348
- expect(r.rows[0]).toEqual(['test', []]);
349
- expect(decompress(r)).toEqual(src);
350
- });
164
+ test('与预期 rows 一致', () => {
165
+ expect(r2.rows).toEqual([
166
+ [["张三", "李四", "王五"], "23班"],
167
+ [["李小花", "张晓", "李旺", "张思"], "24班"]
168
+ ]);
169
+ });
351
170
 
352
- test('嵌套对象数组中含 null 元素(首元 null→退化为 primitive-array)', () => {
353
- const src = [{ name: 'a', kids: [null, { name: 'child' }] }];
354
- const r = compress(src);
355
- // v[0]===null → 走 primitive-array 分支,不拆解子 key
356
- expect(r.keys).toEqual(['name', 'kids']);
357
- expect(r.rows[0]).toEqual(['a', [null, { name: 'child' }]]);
358
- expect(decompress(r)).toEqual(src);
359
- });
360
-
361
- test('字段值为 undefined 转 null', () => {
362
- const src = [{ a: undefined, b: 1 }];
363
- const r = compress(src);
364
- expect(r.keys).toEqual(['a', 'b']);
365
- expect(r.rows[0]).toEqual([null, 1]);
366
- });
171
+ test('还原一致', () => {
172
+ expect(decompress(r2)).toEqual(src2);
173
+ });
174
+ });
367
175
 
368
- test('字段值为 null 保留 null', () => {
369
- const src = [{ a: 1, b: null }];
370
- const r = compress(src);
371
- expect(r.rows[0][1]).toBeNull();
372
- expect(decompress(r)[0].b).toBeNull();
373
- });
176
+ describe('场景3:顶层缺失字段(后端省略 null', () => {
177
+ const r3 = compress(src3, copt);
374
178
 
375
- test('对象数组中含非对象元素(item typeof!=="object" 分支)', () => {
376
- const src = [{ name: 'x', kids: [{ name: 'kid' }, 'string', null] }];
377
- const r = compress(src);
378
- // 首元素是对象 → object-array;非对象元素在 buildKeys 收集时被跳过
379
- expect(r.keys).toEqual(['name', { kids: ['name'] }]);
380
- // 非对象元素 → buildRow 返回 [null],所以 null 元素也是 [null] 而非 null
381
- expect(r.rows[0][1]).toEqual([['kid'], [null], [null]]);
382
- });
179
+ test('keys 包含伴侣', () => {
180
+ expect(r3.keys).toEqual(["姓名", "年龄", { "家人": ["姓名", "年龄"] }, { "伴侣": ["姓名", "年龄"] }]);
181
+ });
383
182
 
384
- test('原数组含数字元素(typeof obj!=="object" 各分支)', () => {
385
- const src = [42, { name: 'a' }, true, { name: 'b' }];
386
- const r = compress(src);
387
- expect(r.keys).toEqual(['name']);
388
- // 数字/布尔不是对象 → buildRow 返回 [null]
389
- expect(r.rows[0]).toEqual([null]);
390
- expect(r.rows[1]).toEqual(['a']);
391
- expect(r.rows[2]).toEqual([null]);
392
- expect(r.rows[3]).toEqual(['b']);
393
- });
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
+ });
394
190
 
395
- test('getValueKind:数组首元素也是数组 → primitive-array', () => {
396
- const src = [{ name: 'x', matrix: [[1, 2], [3, 4]] }];
397
- const r = compress(src);
398
- expect(r.keys).toEqual(['name', 'matrix']);
399
- expect(r.rows[0]).toEqual(['x', [[1, 2], [3, 4]]]);
400
- expect(decompress(r)).toEqual(src);
401
- });
191
+ test('row[1] 伴侣正常', () => {
192
+ expect(r3.rows[1][3]).toEqual(["赵明", 30]);
193
+ });
402
194
 
403
- test('普通原始类型数组 [1,2,3] 不拆解', () => {
404
- const src = [{ name: 'x', scores: [1, 2, 3] }];
405
- const r = compress(src);
406
- expect(r.keys).toEqual(['name', 'scores']);
407
- expect(r.rows[0]).toEqual(['x', [1, 2, 3]]);
408
- expect(decompress(r)).toEqual(src);
409
- });
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
+ });
410
207
 
411
- test('非对象元素 + 嵌套 key(buildRow line 108 三元 false 分支)', () => {
412
- const src = [42, { name: 'a', detail: { x: 1 } }];
413
- const r = compress(src);
414
- expect(r.keys).toEqual(['name', { detail: ['x'] }]);
415
- // 42 不是对象 所有 key(包括嵌套)都取 undefined → null
416
- expect(r.rows[0]).toEqual([null, null]);
417
- expect(r.rows[1]).toEqual(['a', [1]]);
418
- });
419
-
420
- test('非对象元素 + 对象数组(buildKeys line 73 typeof false 分支)', () => {
421
- const src = [42, { items: [{ name: 'a' }] }];
422
- const r = compress(src);
423
- expect(r.keys).toEqual([{ items: ['name'] }]);
424
- // 42 不是对象 → buildRow 返回 [null]
425
- expect(r.rows[0]).toEqual([null]);
426
- expect(r.rows[1]).toEqual([[['a']]]);
427
- });
428
-
429
- test('字段值为 null 且 repValue 为 object-array(line 75 Array.isArray false 分支)', () => {
430
- const src = [
431
- { items: [{ name: 'a' }] }, // items 是 object-array
432
- { items: null } // items 为 null → !Array.isArray
433
- ];
434
- const r = compress(src);
435
- expect(r.keys).toEqual([{ items: ['name'] }]);
436
- expect(r.rows[0]).toEqual([[['a']]]);
437
- expect(r.rows[1]).toEqual([null]);
438
- });
439
-
440
- });
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
441
584
 
442
585
  /* =========================================================
443
586
  stringify / parse — 省略 null 文本化 与 还原
587
+ (以下测试不依赖 compress,不需要按 trim 分组)
444
588
  ========================================================= */
445
589
  describe('stringify / parse', () => {
446
590
 
@@ -562,59 +706,25 @@ test('原数组含数字元素(typeof obj!=="object" 各分支)', () => {
562
706
  });
563
707
  });
564
708
 
565
- // ---- 与 compress/decompress 联用 ----
709
+ // ---- 与 compress/decompress 联用(纯 stringify/parse,不按 trim 分组)----
566
710
  describe('与 compress/decompress 联用', () => {
567
- test('样例1 往返:compress → stringify → parse → decompress', () => {
568
- const cmp = compress(src1);
569
- const str = stringify(cmp);
570
- const restored = parse(str);
571
- expect(restored).toEqual(cmp);
572
- expect(decompress(restored)).toEqual(src1);
573
- });
574
-
575
- test('样例2 往返', () => {
576
- const cmp = compress(src2);
577
- const str = stringify(cmp);
578
- const restored = parse(str);
579
- expect(restored).toEqual(cmp);
580
- expect(decompress(restored)).toEqual(src2);
581
- });
582
-
583
- test('场景3(缺失字段→null)往返', () => {
584
- const cmp = compress(src3);
585
- const str = stringify(cmp);
586
- const restored = parse(str);
587
- expect(restored).toEqual(cmp);
588
- expect(decompress(restored)).toEqual(src3Normalized);
589
- });
590
-
591
- test('场景4(嵌套子 key 不全)往返', () => {
592
- const cmp = compress(src4);
593
- const str = stringify(cmp);
594
- const restored = parse(str);
595
- expect(restored).toEqual(cmp);
596
- expect(decompress(restored)).toEqual(src4Normalized);
597
- });
598
-
599
- test('场景5(复杂嵌套+缺失成绩)往返', () => {
600
- const cmp = compress(src5);
601
- const str = stringify(cmp);
602
- const restored = parse(str);
603
- expect(restored).toEqual(cmp);
604
- expect(decompress(restored)).toEqual(src5Normalized);
605
- });
606
-
607
- test('stringify 输出不含 null 关键字的实例(验证省略效果)', () => {
608
- // 构造一个有大量 null 的压缩结构,验证文本化后不含 "null"
609
- const src = [
610
- { name: 'a', extra: null },
611
- { name: 'b', extra: null }
711
+ test('compress → stringify → parse → decompress(顶层缺失字段)', () => {
712
+ const data = [
713
+ { name: '张三', age: 25, city: '北京' },
714
+ { name: '李四', age: 30 }
612
715
  ];
613
- const cmp = compress(src);
614
- const str = stringify(cmp);
615
- // rows 中的 null 应该被省略
616
- // rows: [["a",null],["b",null]] → [["a",],["b",]]
617
- expect(str).not.toContain('null');
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"]');
618
728
  });
619
729
  });
620
730
 
@@ -811,27 +921,6 @@ test('原数组含数字元素(typeof obj!=="object" 各分支)', () => {
811
921
  });
812
922
  });
813
923
 
814
- describe('与 compress/decompress 联用', () => {
815
- test('compress → stringify → parse → decompress', () => {
816
- const data = [
817
- { name: '张三', age: 25, city: '北京' },
818
- { name: '李四', age: 30 }
819
- ];
820
- const expected = [
821
- { name: '张三', age: 25, city: '北京' },
822
- { name: '李四', age: 30, city: null }
823
- ];
824
- const compressed = compress(data);
825
- const text = stringify(compressed);
826
- const parsed = parse(text);
827
- expect(decompress(parsed)).toEqual(expected);
828
- });
829
-
830
- test('含空格字符串保留引号', () => {
831
- expect(stringify(['hello world'])).toBe('["hello world"]');
832
- });
833
- });
834
-
835
924
  describe('parse 兼容有/无引号格式', () => {
836
925
  test('解析无引号字符串', () => {
837
926
  expect(parse('[hello]')).toEqual(['hello']);