slimjson 1.1.0 → 1.1.2

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
@@ -75,6 +75,145 @@ const src5 = [
75
75
  }
76
76
  ];
77
77
 
78
+ // 场景6:四层嵌套(组织→部门→团队→成员+技能标签)
79
+ const src6 = [
80
+ {
81
+ "org": "TechCorp",
82
+ "departments": [
83
+ {
84
+ "name": "Engineering",
85
+ "teams": [
86
+ {
87
+ "lead": "Alice",
88
+ "members": [
89
+ { "name": "Bob", "level": "L5", "skills": ["Go", "K8s"] },
90
+ { "name": "Carol", "level": "L4", "skills": ["Python", "ML"] }
91
+ ]
92
+ },
93
+ {
94
+ "lead": "Dave",
95
+ "members": [
96
+ { "name": "Eve", "level": "L6" }
97
+ ]
98
+ }
99
+ ]
100
+ },
101
+ {
102
+ "name": "Design",
103
+ "teams": [
104
+ {
105
+ "lead": "Frank",
106
+ "members": [
107
+ { "name": "Grace", "level": "L3", "skills": ["Figma", "CSS"] }
108
+ ]
109
+ }
110
+ ]
111
+ }
112
+ ]
113
+ }
114
+ ];
115
+
116
+ // 场景7:同层混合 — 对象同时包含嵌套对象和嵌套数组,且字段不对称
117
+ const src7 = [
118
+ {
119
+ "product": "Keyboard",
120
+ "spec": { "weight": "800g", "layout": "104-key", "switch": "Cherry MX" },
121
+ "reviews": [
122
+ { "user": "Alice", "rating": 5, "comment": "Great!" },
123
+ { "user": "Bob", "rating": 4 }
124
+ ],
125
+ "tags": ["mechanical", "RGB"]
126
+ },
127
+ {
128
+ "product": "Mouse",
129
+ "spec": { "weight": "60g", "dpi": 16000 },
130
+ "reviews": [
131
+ { "user": "Carol", "rating": 3, "comment": "Too light" },
132
+ { "user": "Dave", "rating": 5, "comment": "Perfect" },
133
+ { "user": "Eve", "rating": 4 }
134
+ ]
135
+ },
136
+ {
137
+ "product": "Monitor",
138
+ "spec": { "weight": "5kg", "size": "27in", "resolution": "4K", "refresh": 144 },
139
+ "reviews": []
140
+ }
141
+ ];
142
+
143
+ // 场景8:数组的数组内含对象(矩阵 + 内层对象不对称)
144
+ const src8 = [
145
+ {
146
+ "matrix": [
147
+ [{ "x": 1, "y": "a" }, { "x": 2, "y": "b", "z": 99 }],
148
+ [{ "x": 3, "y": "c" }]
149
+ ],
150
+ "label": "grid-A"
151
+ },
152
+ {
153
+ "matrix": [
154
+ [{ "x": 10, "y": "d", "z": 88 }],
155
+ [{ "x": 20, "y": "e" }, { "x": 30, "y": "f", "z": 77 }],
156
+ [{ "x": 40, "y": "g" }]
157
+ ],
158
+ "label": "grid-B"
159
+ }
160
+ ];
161
+
162
+ // 场景9:深层缺失 — 每层都有字段缺失
163
+ const src9 = [
164
+ {
165
+ "company": "Acme",
166
+ "address": { "city": "Beijing", "zip": "100000" },
167
+ "departments": [
168
+ {
169
+ "name": "Sales",
170
+ "head": { "name": "Tom", "age": 40 },
171
+ "staff": [
172
+ { "name": "Amy", "phone": "111" },
173
+ { "name": "Ben" }
174
+ ]
175
+ }
176
+ ]
177
+ },
178
+ {
179
+ "company": "Globex",
180
+ "departments": [
181
+ {
182
+ "name": "Tech",
183
+ "head": { "name": "Cat", "age": 35, "title": "VP" },
184
+ "staff": [
185
+ { "name": "Dan", "phone": "222", "email": "dan@x.com" },
186
+ { "name": "Eve", "email": "eve@x.com" }
187
+ ]
188
+ },
189
+ {
190
+ "name": "Ops",
191
+ "staff": [
192
+ { "name": "Fox" }
193
+ ]
194
+ }
195
+ ]
196
+ }
197
+ ];
198
+
199
+ // 场景10:数组内含不同结构的对象(同一数组内对象 key 完全不同)
200
+ const src10 = [
201
+ {
202
+ "events": [
203
+ { "type": "click", "target": "button", "timestamp": 1000 },
204
+ { "type": "scroll", "offset": 500, "direction": "down" },
205
+ { "type": "input", "field": "email", "value": "a@b.com", "valid": true }
206
+ ],
207
+ "session": "abc"
208
+ },
209
+ {
210
+ "events": [
211
+ { "type": "submit", "form": "login", "success": false, "error": "timeout" }
212
+ ],
213
+ "session": "xyz"
214
+ }
215
+ ];
216
+
78
217
  // decompress 始终返回规范化数据(补齐缺失 key 为 null)
79
218
  const src3Decompressed = [
80
219
  { "姓名": "张三", "年龄": 19, "家人": [{ "姓名": "张四", "年龄": 40 }], "伴侣": null },
@@ -122,6 +261,149 @@ const src5Decompressed = [
122
261
  }
123
262
  ];
124
263
 
264
+ // 场景6 decompressed:四层嵌套,skills 缺失补 null
265
+ const src6Decompressed = [
266
+ {
267
+ "org": "TechCorp",
268
+ "departments": [
269
+ {
270
+ "name": "Engineering",
271
+ "teams": [
272
+ {
273
+ "lead": "Alice",
274
+ "members": [
275
+ { "name": "Bob", "level": "L5", "skills": ["Go", "K8s"] },
276
+ { "name": "Carol", "level": "L4", "skills": ["Python", "ML"] }
277
+ ]
278
+ },
279
+ {
280
+ "lead": "Dave",
281
+ "members": [
282
+ { "name": "Eve", "level": "L6", "skills": null }
283
+ ]
284
+ }
285
+ ]
286
+ },
287
+ {
288
+ "name": "Design",
289
+ "teams": [
290
+ {
291
+ "lead": "Frank",
292
+ "members": [
293
+ { "name": "Grace", "level": "L3", "skills": ["Figma", "CSS"] }
294
+ ]
295
+ }
296
+ ]
297
+ }
298
+ ]
299
+ }
300
+ ];
301
+
302
+ // 场景7 decompressed:同层混合,缺失字段补 null
303
+ const src7Decompressed = [
304
+ {
305
+ "product": "Keyboard",
306
+ "spec": { "weight": "800g", "layout": "104-key", "switch": "Cherry MX", "dpi": null, "size": null, "resolution": null, "refresh": null },
307
+ "reviews": [
308
+ { "user": "Alice", "rating": 5, "comment": "Great!" },
309
+ { "user": "Bob", "rating": 4, "comment": null }
310
+ ],
311
+ "tags": ["mechanical", "RGB"]
312
+ },
313
+ {
314
+ "product": "Mouse",
315
+ "spec": { "weight": "60g", "layout": null, "switch": null, "dpi": 16000, "size": null, "resolution": null, "refresh": null },
316
+ "reviews": [
317
+ { "user": "Carol", "rating": 3, "comment": "Too light" },
318
+ { "user": "Dave", "rating": 5, "comment": "Perfect" },
319
+ { "user": "Eve", "rating": 4, "comment": null }
320
+ ],
321
+ "tags": null
322
+ },
323
+ {
324
+ "product": "Monitor",
325
+ "spec": { "weight": "5kg", "layout": null, "switch": null, "dpi": null, "size": "27in", "resolution": "4K", "refresh": 144 },
326
+ "reviews": [],
327
+ "tags": null
328
+ }
329
+ ];
330
+
331
+ // 场景8 decompressed:数组的数组内含对象
332
+ const src8Decompressed = [
333
+ {
334
+ "matrix": [
335
+ [{ "x": 1, "y": "a", "z": null }, { "x": 2, "y": "b", "z": 99 }],
336
+ [{ "x": 3, "y": "c", "z": null }]
337
+ ],
338
+ "label": "grid-A"
339
+ },
340
+ {
341
+ "matrix": [
342
+ [{ "x": 10, "y": "d", "z": 88 }],
343
+ [{ "x": 20, "y": "e", "z": null }, { "x": 30, "y": "f", "z": 77 }],
344
+ [{ "x": 40, "y": "g", "z": null }]
345
+ ],
346
+ "label": "grid-B"
347
+ }
348
+ ];
349
+
350
+ // 场景9 decompressed:深层缺失
351
+ const src9Decompressed = [
352
+ {
353
+ "company": "Acme",
354
+ "address": { "city": "Beijing", "zip": "100000" },
355
+ "departments": [
356
+ {
357
+ "name": "Sales",
358
+ "head": { "name": "Tom", "age": 40, "title": null },
359
+ "staff": [
360
+ { "name": "Amy", "phone": "111", "email": null },
361
+ { "name": "Ben", "phone": null, "email": null }
362
+ ]
363
+ }
364
+ ]
365
+ },
366
+ {
367
+ "company": "Globex",
368
+ "address": null,
369
+ "departments": [
370
+ {
371
+ "name": "Tech",
372
+ "head": { "name": "Cat", "age": 35, "title": "VP" },
373
+ "staff": [
374
+ { "name": "Dan", "phone": "222", "email": "dan@x.com" },
375
+ { "name": "Eve", "phone": null, "email": "eve@x.com" }
376
+ ]
377
+ },
378
+ {
379
+ "name": "Ops",
380
+ "head": null,
381
+ "staff": [
382
+ { "name": "Fox", "phone": null, "email": null }
383
+ ]
384
+ }
385
+ ]
386
+ }
387
+ ];
388
+
389
+ // 场景10 decompressed:数组内含不同结构的对象
390
+ const src10Decompressed = [
391
+ {
392
+ "events": [
393
+ { "type": "click", "target": "button", "timestamp": 1000, "offset": null, "direction": null, "field": null, "value": null, "valid": null, "form": null, "success": null, "error": null },
394
+ { "type": "scroll", "target": null, "timestamp": null, "offset": 500, "direction": "down", "field": null, "value": null, "valid": null, "form": null, "success": null, "error": null },
395
+ { "type": "input", "target": null, "timestamp": null, "offset": null, "direction": null, "field": "email", "value": "a@b.com", "valid": true, "form": null, "success": null, "error": null }
396
+ ],
397
+ "session": "abc"
398
+ },
399
+ {
400
+ "events": [
401
+ { "type": "submit", "target": null, "timestamp": null, "offset": null, "direction": null, "field": null, "value": null, "valid": null, "form": "login", "success": false, "error": "timeout" }
402
+ ],
403
+ "session": "xyz"
404
+ }
405
+ ];
406
+
125
407
  // ============================================================
126
408
  // 测试
127
409
  // ============================================================
@@ -138,30 +420,45 @@ describe('compress / decompress', () => {
138
420
  describe('样例1:基础嵌套', () => {
139
421
  const r1 = compress(src1, copt);
140
422
 
141
- test('compress 不出错', () => {
142
- expect(r1.schema).toBeDefined();
143
- expect(r1.data).toBeDefined();
423
+ test('schema 一致', () => {
424
+ expect(r1.schema).toEqual([[
425
+ "姓名", "年龄", { "家人": [["姓名", "年龄"]] }, { "伴侣": ["姓名", "年龄"] }
426
+ ]]);
144
427
  });
145
428
 
146
- test('与预期 schema 一致', () => {
147
- expect(r1.schema[0]).toEqual([
148
- "姓名", "年龄", { "家人": [["姓名", "年龄"]] }, { "伴侣": ["姓名", "年龄"] }
149
- ]);
429
+ test('data 一致', () => {
430
+ if (trim) {
431
+ expect(r1.data).toEqual([
432
+ ['张三', 19, [['张四', 40], ['李五', 41]], ['王六', 18]],
433
+ ['李小花', 28, [['李大国', 55], ['王淑芬', 53]], ['赵明', 30]]
434
+ ]);
435
+ } else {
436
+ expect(r1.data).toEqual([
437
+ ['张三', 19, [['张四', 40], ['李五', 41]], ['王六', 18]],
438
+ ['李小花', 28, [['李大国', 55], ['王淑芬', 53]], ['赵明', 30]]
439
+ ]);
440
+ }
150
441
  });
151
442
 
152
443
  test('还原一致', () => {
153
444
  expect(decompress(r1)).toEqual(src1);
154
445
  });
446
+
447
+ test('stringify/parse 往返', () => {
448
+ const str = stringify(r1);
449
+ expect(parse(str)).toEqual(r1);
450
+ expect(decompress(parse(str))).toEqual(src1);
451
+ });
155
452
  });
156
453
 
157
454
  describe('样例2:原始类型数组字段', () => {
158
455
  const r2 = compress(src2, copt);
159
456
 
160
- test('与预期 schema 一致', () => {
161
- expect(r2.schema[0]).toEqual(["姓名", "班级"]);
457
+ test('schema 一致', () => {
458
+ expect(r2.schema).toEqual([["姓名", "班级"]]);
162
459
  });
163
460
 
164
- test('与预期 data 一致', () => {
461
+ test('data 一致', () => {
165
462
  expect(r2.data).toEqual([
166
463
  [["张三", "李四", "王五"], "23班"],
167
464
  [["李小花", "张晓", "李旺", "张思"], "24班"]
@@ -171,166 +468,277 @@ describe('compress / decompress', () => {
171
468
  test('还原一致', () => {
172
469
  expect(decompress(r2)).toEqual(src2);
173
470
  });
471
+
472
+ test('stringify/parse 往返', () => {
473
+ const str = stringify(r2);
474
+ expect(parse(str)).toEqual(r2);
475
+ expect(decompress(parse(str))).toEqual(src2);
476
+ });
174
477
  });
175
478
 
176
479
  describe('场景3:顶层缺失字段(后端省略 null)', () => {
177
480
  const r3 = compress(src3, copt);
178
481
 
179
- test('schema 包含伴侣', () => {
180
- expect(r3.schema[0]).toEqual(["姓名", "年龄", { "家人": [["姓名", "年龄"]] }, { "伴侣": ["姓名", "年龄"] }]);
482
+ test('schema 一致', () => {
483
+ expect(r3.schema).toEqual([["姓名", "年龄", { "家人": [["姓名", "年龄"]] }, { "伴侣": ["姓名", "年龄"] }]]);
181
484
  });
182
485
 
183
- test(`row[0] 伴侣${trim ? ' trim' : '为 null'}`, () => {
486
+ test('data 一致', () => {
184
487
  if (trim) {
185
- expect(r3.data[0].length).toBe(3);
488
+ expect(r3.data).toEqual([
489
+ ['张三', 19, [['张四', 40]]],
490
+ ['李小花', 28, [['李大国', 55]], ['赵明', 30]]
491
+ ]);
186
492
  } else {
187
- expect(r3.data[0][3]).toBeNull();
493
+ expect(r3.data).toEqual([
494
+ ["张三", 19, [["张四", 40]], null],
495
+ ["李小花", 28, [["李大国", 55]], ["赵明", 30]]
496
+ ]);
188
497
  }
189
498
  });
190
499
 
191
- test('row[1] 伴侣正常', () => {
192
- expect(r3.data[1][3]).toEqual(["赵明", 30]);
193
- });
194
-
195
- test('还原(null 补齐)', () => {
500
+ test('还原一致', () => {
196
501
  expect(decompress(r3)).toEqual(src3Decompressed);
197
502
  });
198
503
 
199
- test('张三的伴侣为 null', () => {
200
- expect(decompress(r3)[0].伴侣).toBeNull();
201
- });
202
-
203
- test('李小花的伴侣正常', () => {
204
- expect(decompress(r3)[1].伴侣.姓名).toBe("赵明");
504
+ test('stringify/parse 往返', () => {
505
+ const str = stringify(r3);
506
+ expect(parse(str)).toEqual(r3);
507
+ expect(decompress(parse(str))).toEqual(src3Decompressed);
205
508
  });
206
509
  });
207
510
 
208
511
  describe('场景4:嵌套对象子 key 不全', () => {
209
512
  const r4 = compress(src4, copt);
210
513
 
211
- test('schema 中家人包含"关系"', () => {
212
- expect(r4.schema[0]).toEqual(["姓名", "年龄", { "家人": [["姓名", "年龄", "关系"]] }]);
514
+ test('schema 一致', () => {
515
+ expect(r4.schema).toEqual([["姓名", "年龄", { "家人": [["姓名", "年龄", "关系"]] }]]);
213
516
  });
214
517
 
215
- test(`家人[0] 关系${trim ? ' trim' : '为 null'}`, () => {
518
+ test('data 一致', () => {
216
519
  if (trim) {
217
- expect(r4.data[0][2][0].length).toBe(2);
520
+ expect(r4.data).toEqual([
521
+ ["张三", 19, [["张四", 40], ["李五", 41, "母亲"]]]
522
+ ]);
218
523
  } else {
219
- expect(r4.data[0][2][0][2]).toBeNull();
524
+ expect(r4.data).toEqual([
525
+ ["张三", 19, [["张四", 40, null], ["李五", 41, "母亲"]]]
526
+ ]);
220
527
  }
221
528
  });
222
529
 
223
- test('家人[1] 关系正常', () => {
224
- expect(r4.data[0][2][1][2]).toBe("母亲");
225
- });
226
-
227
- test('还原(null 补齐)', () => {
530
+ test('还原一致', () => {
228
531
  expect(decompress(r4)).toEqual(src4Decompressed);
229
532
  });
230
533
 
231
- test('第1个家人关系为 null', () => {
232
- expect(decompress(r4)[0].家人[0].关系).toBeNull();
233
- });
234
-
235
- test('第2个家人关系正常', () => {
236
- expect(decompress(r4)[0].家人[1].关系).toBe("母亲");
534
+ test('stringify/parse 往返', () => {
535
+ const str = stringify(r4);
536
+ expect(parse(str)).toEqual(r4);
537
+ expect(decompress(parse(str))).toEqual(src4Decompressed);
237
538
  });
238
539
  });
239
540
 
240
541
  describe('场景5:复杂嵌套(年级/班级/班主任/其他老师/学生+成绩)', () => {
241
542
  const r5 = compress(src5, copt);
242
543
 
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([
544
+ test('schema 一致', () => {
545
+ expect(r5.schema).toEqual([[
262
546
  "年级",
263
547
  "班级",
264
548
  { "班主任": ["姓名", "年龄", "科目"] },
265
549
  { "其他老师": [["姓名", "年龄", "科目"]] },
266
550
  { "学生": [["姓名", "年龄", "性别", { "成绩": ["语文", "数学", "英语"] }]] }
267
- ]);
551
+ ]]);
268
552
  });
269
553
 
270
- // —— data 数量 ——
271
- test('1班班主任正常', () => {
272
- expect(r5.data[0][2]).toEqual(["王老师", 35, "语文"]);
554
+ test('data 一致', () => {
555
+ if (trim) {
556
+ expect(r5.data).toEqual([
557
+ ['一年级', '1班', ['王老师', 35, '语文'], [['李老师', 28, '数学'], ['张老师', 40, '英语']], [['小明', 7, '男', [95, 88, 92]], ['小红', 7, '女', [90, 96, 89]]]],
558
+ ['一年级', '2班', ['赵老师', 42, '数学'], [['钱老师', 31, '语文'], ['孙老师', 33, '英语'], ['周老师', 26, '体育']], [['小刚', 7, '男', [78, 85]], ['小丽', 7, '女', [99, 100, 97]], ['小强', 8, '男', [null, 60]]]]
559
+ ]);
560
+ } else {
561
+ expect(r5.data).toEqual([
562
+ ['一年级', '1班', ['王老师', 35, '语文'], [['李老师', 28, '数学'], ['张老师', 40, '英语']], [['小明', 7, '男', [95, 88, 92]], ['小红', 7, '女', [90, 96, 89]]]],
563
+ ['一年级', '2班', ['赵老师', 42, '数学'], [['钱老师', 31, '语文'], ['孙老师', 33, '英语'], ['周老师', 26, '体育']], [['小刚', 7, '男', [78, 85, null]], ['小丽', 7, '女', [99, 100, 97]], ['小强', 8, '男', [null, 60, null]]]]
564
+ ]);
565
+ }
273
566
  });
274
567
 
275
- test('1班其他老师数量=2', () => {
276
- expect(r5.data[0][3].length).toBe(2);
568
+ test('还原一致', () => {
569
+ expect(decompress(r5)).toEqual(src5Decompressed);
277
570
  });
278
571
 
279
- test('1班学生数量=2', () => {
280
- expect(r5.data[0][4].length).toBe(2);
572
+ test('stringify/parse 往返', () => {
573
+ const str = stringify(r5);
574
+ expect(parse(str)).toEqual(r5);
575
+ expect(decompress(parse(str))).toEqual(src5Decompressed);
281
576
  });
577
+ });
282
578
 
283
- test('2班班主任正常', () => {
284
- expect(r5.data[1][2]).toEqual(["赵老师", 42, "数学"]);
285
- });
579
+ /* =========================================================
580
+ 场景6-10:复杂嵌套对象和数组
581
+ ========================================================= */
286
582
 
287
- test('2班其他老师数量=3', () => {
288
- expect(r5.data[1][3].length).toBe(3);
289
- });
583
+ describe('场景6:四层嵌套(组织→部门→团队→成员+技能标签)', () => {
584
+ const r6 = compress(src6, copt);
290
585
 
291
- test('2班学生数量=3', () => {
292
- expect(r5.data[1][4].length).toBe(3);
586
+ test('schema 一致', () => {
587
+ expect(r6.schema).toEqual([['org', { departments: [['name', { teams: [['lead', { members: [['name', 'level', 'skills']] }]] }]] }]]);
293
588
  });
294
-
295
- // —— 成绩子对象缺失 key 补 null ——
296
- test('小刚英语成绩为 null(缺英语字段)', () => {
297
- expect(decompress(r5)[1]["学生"][0]["成绩"]["英语"]).toBeNull();
589
+ test('data 一致', () => {
590
+ if (trim) {
591
+ expect(r6.data).toEqual([['TechCorp',[['Engineering',[['Alice',[['Bob','L5',['Go','K8s']],['Carol','L4',['Python','ML']]]],['Dave',[['Eve','L6']]]]],['Design',[['Frank',[['Grace','L3',['Figma','CSS']]]]]]]]]);
592
+ } else {
593
+ expect(r6.data).toEqual([['TechCorp',[['Engineering',[['Alice',[['Bob','L5',['Go','K8s']],['Carol','L4',['Python','ML']]]],['Dave',[['Eve','L6',null]]]]],['Design',[['Frank',[['Grace','L3',['Figma','CSS']]]]]]]]]);
594
+ }
298
595
  });
299
-
300
- test('小刚语文成绩正常', () => {
301
- expect(r5.data[1][4][0][3][0]).toBe(78);
596
+ test('还原一致', () => { expect(decompress(r6)).toEqual(src6Decompressed); });
597
+ test('stringify/parse 往返', () => {
598
+ const str = stringify(r6);
599
+ expect(parse(str)).toEqual(r6);
600
+ expect(decompress(parse(str))).toEqual(src6Decompressed);
302
601
  });
602
+ });
303
603
 
304
- test('小强语文成绩为 null(缺语文字段)', () => {
305
- expect(decompress(r5)[1]["学生"][2]["成绩"]["语文"]).toBeNull();
306
- });
604
+ describe('场景7:同层混合 — 嵌套对象+嵌套数组+原始数组,字段不对称', () => {
605
+ const r7 = compress(src7, copt);
307
606
 
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);
607
+ test('schema 一致', () => {
608
+ expect(r7.schema).toEqual([['product', { spec: ['weight', 'layout', 'switch', 'dpi', 'size', 'resolution', 'refresh'] }, { reviews: [['user', 'rating', 'comment']] }, 'tags']]);
313
609
  });
610
+ test('data 一致', () => {
611
+ if (trim) {
612
+ expect(r7.data).toEqual([
613
+ ['Keyboard', ['800g', '104-key', 'Cherry MX'], [['Alice', 5, 'Great!'], ['Bob', 4]], ['mechanical', 'RGB']],
614
+ ['Mouse', ['60g', null, null, 16000], [['Carol', 3, 'Too light'], ['Dave', 5, 'Perfect'], ['Eve', 4]]],
615
+ ['Monitor', ['5kg', null, null, null, '27in', '4K', 144], []]
616
+ ]);
617
+ } else {
618
+ expect(r7.data).toEqual([
619
+ ['Keyboard', ['800g', '104-key', 'Cherry MX', null, null, null, null], [['Alice', 5, 'Great!'], ['Bob', 4, null]], ['mechanical', 'RGB']],
620
+ ['Mouse', ['60g', null, null, 16000, null, null, null], [['Carol', 3, 'Too light'], ['Dave', 5, 'Perfect'], ['Eve', 4, null]], null],
621
+ ['Monitor', ['5kg', null, null, null, '27in', '4K', 144], [], null]
622
+ ]);
623
+ }
624
+ });
625
+ test('还原一致', () => { expect(decompress(r7)).toEqual(src7Decompressed); });
626
+ test('stringify/parse 往返', () => {
627
+ const str = stringify(r7);
628
+ expect(parse(str)).toEqual(r7);
629
+ expect(decompress(parse(str))).toEqual(src7Decompressed);
630
+ });
631
+ });
632
+
633
+ describe('场景8:数组的数组内含对象(矩阵,内层对象 key 不对称)', () => {
634
+ const r8 = compress(src8, copt);
314
635
 
315
- test('小强英语成绩为 null(缺英语字段)', () => {
316
- expect(decompress(r5)[1]["学生"][2]["成绩"]["英语"]).toBeNull();
636
+ test('schema 一致', () => {
637
+ expect(r8.schema).toEqual([[{ matrix: [[['x', 'y', 'z']]] }, 'label']]);
317
638
  });
639
+ test('data 一致', () => {
640
+ if (trim) {
641
+ expect(r8.data).toEqual([
642
+ [[[[1, 'a'], [2, 'b', 99]], [[3, 'c']]], 'grid-A'],
643
+ [[[[10, 'd', 88]], [[20, 'e'], [30, 'f', 77]], [[40, 'g']]], 'grid-B']
644
+ ]);
645
+ } else {
646
+ expect(r8.data).toEqual([
647
+ [[[[1, 'a', null], [2, 'b', 99]], [[3, 'c', null]]], 'grid-A'],
648
+ [[[[10, 'd', 88]], [[20, 'e', null], [30, 'f', 77]], [[40, 'g', null]]], 'grid-B']
649
+ ]);
650
+ }
651
+ });
652
+ test('还原一致', () => { expect(decompress(r8)).toEqual(src8Decompressed); });
653
+ test('stringify/parse 往返', () => {
654
+ const str = stringify(r8);
655
+ expect(parse(str)).toEqual(r8);
656
+ expect(decompress(parse(str))).toEqual(src8Decompressed);
657
+ });
658
+ });
318
659
 
319
- // —— 还原验证 ——
320
- test('还原后与 decompressed 一致', () => {
321
- expect(decompress(r5)).toEqual(src5Decompressed);
660
+ describe('场景9:深层缺失 每层都有字段缺失', () => {
661
+ const r9 = compress(src9, copt);
662
+
663
+ test('schema 一致', () => {
664
+ expect(r9.schema).toEqual([['company', { address: ['city', 'zip'] }, { departments: [['name', { head: ['name', 'age', 'title'] }, { staff: [['name', 'phone', 'email']] }]] }]]);
322
665
  });
666
+ test('data 一致', () => {
667
+ if (trim) {
668
+ expect(r9.data).toEqual([
669
+ ['Acme', ['Beijing', '100000'], [['Sales', ['Tom', 40], [['Amy', '111'], ['Ben']]]]],
670
+ ['Globex', null, [['Tech', ['Cat', 35, 'VP'], [['Dan', '222', 'dan@x.com'], ['Eve', null, 'eve@x.com']]], ['Ops', null, [['Fox']]]]]]);
671
+ } else {
672
+ expect(r9.data).toEqual([
673
+ ['Acme', ['Beijing', '100000'], [['Sales', ['Tom', 40, null], [['Amy', '111', null], ['Ben', null, null]]]]],
674
+ ['Globex', null, [['Tech', ['Cat', 35, 'VP'], [['Dan', '222', 'dan@x.com'], ['Eve', null, 'eve@x.com']]], ['Ops', null, [['Fox', null, null]]]]]]);
675
+ }
676
+ });
677
+ test('还原一致', () => { expect(decompress(r9)).toEqual(src9Decompressed); });
678
+ test('stringify/parse 往返', () => {
679
+ const str = stringify(r9);
680
+ expect(parse(str)).toEqual(r9);
681
+ expect(decompress(parse(str))).toEqual(src9Decompressed);
682
+ });
683
+ });
323
684
 
324
- test('1班年级正确', () => {
325
- expect(decompress(r5)[0]["年级"]).toBe("一年级");
685
+ describe('场景10:数组内含不同结构的对象(同数组 key 并集)', () => {
686
+ const r10 = compress(src10, copt);
687
+
688
+ test('schema 一致', () => {
689
+ expect(r10.schema).toEqual([[{ events: [['type', 'target', 'timestamp', 'offset', 'direction', 'field', 'value', 'valid', 'form', 'success', 'error']] }, 'session']]);
326
690
  });
691
+ test('data 一致', () => {
692
+ if (trim) {
693
+ expect(r10.data).toEqual([
694
+ [[['click', 'button', 1000], ['scroll', null, null, 500, 'down'], ['input', null, null, null, null, 'email', 'a@b.com', true]], 'abc'],
695
+ [[['submit', null, null, null, null, null, null, null, 'login', false, 'timeout']], 'xyz']
696
+ ]);
697
+ } else {
698
+ expect(r10.data).toEqual([
699
+ [[['click', 'button', 1000, null, null, null, null, null, null, null, null], ['scroll', null, null, 500, 'down', null, null, null, null, null, null], ['input', null, null, null, null, 'email', 'a@b.com', true, null, null, null]], 'abc'],
700
+ [[['submit', null, null, null, null, null, null, null, 'login', false, 'timeout']], 'xyz']
701
+ ]);
702
+ }
703
+ });
704
+ test('还原一致', () => { expect(decompress(r10)).toEqual(src10Decompressed); });
705
+ test('stringify/parse 往返', () => {
706
+ const str = stringify(r10);
707
+ expect(parse(str)).toEqual(r10);
708
+ expect(decompress(parse(str))).toEqual(src10Decompressed);
709
+ });
710
+ });
327
711
 
328
- test('2班第1个学生英语为 null', () => {
329
- expect(decompress(r5)[1]["学生"][0]["成绩"]["英语"]).toBeNull();
712
+ /* =========================================================
713
+ 覆盖率补充测试
714
+ ========================================================= */
715
+ describe('覆盖率补充', () => {
716
+ // line 35-36: mergeSchemas 对象 schema 合并不同 key(需经 array-of-arrays 路径)
717
+ test('数组的数组内含不同 key 的对象 → mergeSchemas 合并', () => {
718
+ const src = [[{ a: 1, b: 2 }], [{ a: 3, c: 4 }]];
719
+ const r = compress(src, copt);
720
+ expect(r.schema).toEqual([[['a', 'b', 'c']]]);
721
+ if (trim) {
722
+ expect(r.data).toEqual([[[1, 2]], [[3, null, 4]]]);
723
+ } else {
724
+ expect(r.data).toEqual([[[1, 2, null]], [[3, null, 4]]]);
725
+ }
726
+ expect(decompress(r)).toEqual([[{ a: 1, b: 2, c: null }], [{ a: 3, b: null, c: 4 }]]);
727
+ const str = stringify(r);
728
+ expect(parse(str)).toEqual(r);
729
+ expect(decompress(parse(str))).toEqual([[{ a: 1, b: 2, c: null }], [{ a: 3, b: null, c: 4 }]]);
330
730
  });
331
731
 
332
- test('2班第3个学生语文为 null', () => {
333
- expect(decompress(r5)[1]["学生"][2]["成绩"]["语文"]).toBeNull();
732
+ // line 79: inferSchema 原始值数组返回 undefined
733
+ test('原始值数组作为字段值 → 不拆解', () => {
734
+ const src = [{ name: 'x', nums: [1, 2, 3] }];
735
+ const r = compress(src, copt);
736
+ expect(r.schema).toEqual([['name', 'nums']]);
737
+ expect(r.data).toEqual([['x', [1, 2, 3]]]);
738
+ expect(decompress(r)).toEqual(src);
739
+ const str = stringify(r);
740
+ expect(parse(str)).toEqual(r);
741
+ expect(decompress(parse(str))).toEqual(src);
334
742
  });
335
743
  });
336
744
 
@@ -347,12 +755,15 @@ describe('compress / decompress', () => {
347
755
  expect(compress(null, copt)).toBeNull();
348
756
  });
349
757
 
350
- test('compress 单个对象 → schema 未包裹,data 平铺', () => {
758
+ test('compress 单个对象', () => {
351
759
  const obj = { name: '张三', age: 25 };
352
760
  const r = compress(obj, copt);
353
761
  expect(r.schema).toEqual(['name', 'age']);
354
762
  expect(r.data).toEqual(['张三', 25]);
355
763
  expect(decompress(r)).toEqual(obj);
764
+ const str = stringify(r);
765
+ expect(parse(str)).toEqual(r);
766
+ expect(decompress(parse(str))).toEqual(obj);
356
767
  });
357
768
 
358
769
  test('compress 非数组 → 返回原值', () => {
@@ -362,127 +773,164 @@ describe('compress / decompress', () => {
362
773
  test('源数组含 null 元素', () => {
363
774
  const src = [{ name: 'a', age: 10 }, null, { name: 'b' }];
364
775
  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]
776
+ expect(r.schema).toEqual([['name', 'age']]);
369
777
  if (trim) {
370
- expect(r.data[1]).toEqual(null);
778
+ expect(r.data).toEqual([['a', 10], null, ['b']]);
371
779
  } else {
372
- expect(r.data[1]).toEqual(null);
780
+ expect(r.data).toEqual([['a', 10], null, ['b', null]]);
373
781
  }
374
782
  expect(decompress(r)).toEqual([
375
783
  { name: 'a', age: 10 },
376
784
  null,
377
785
  { name: 'b', age: null }
378
786
  ]);
787
+ const str = stringify(r);
788
+ expect(parse(str)).toEqual(r);
789
+ expect(decompress(parse(str))).toEqual([
790
+ { name: 'a', age: 10 },
791
+ null,
792
+ { name: 'b', age: null }
793
+ ]);
379
794
  });
380
795
 
381
796
  test('空对象数组字段', () => {
382
797
  const src = [{ name: 'test', items: [] }];
383
798
  const r = compress(src, copt);
384
- expect(r.schema[0]).toEqual(['name', 'items']);
385
- expect(r.data[0]).toEqual(['test', []]);
799
+ expect(r.schema).toEqual([['name', 'items']]);
800
+ expect(r.data).toEqual([['test', []]]);
386
801
  expect(decompress(r)).toEqual(src);
802
+ const str = stringify(r);
803
+ expect(parse(str)).toEqual(r);
804
+ expect(decompress(parse(str))).toEqual(src);
387
805
  });
388
806
 
389
807
  test('嵌套对象数组中含 null 元素(首元 null→退化为 primitive-array)', () => {
390
808
  const src = [{ name: 'a', kids: [null, { name: 'child' }] }];
391
809
  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' }]]);
810
+ expect(r.schema).toEqual([['name', 'kids']]);
811
+ expect(r.data).toEqual([['a', [null, { name: 'child' }]]]);
395
812
  expect(decompress(r)).toEqual(src);
813
+ const str = stringify(r);
814
+ expect(parse(str)).toEqual(r);
815
+ expect(decompress(parse(str))).toEqual(src);
396
816
  });
397
817
 
398
818
  test('字段值为 undefined 转 null', () => {
399
819
  const src = [{ a: undefined, b: 1 }];
400
820
  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
- }
821
+ expect(r.schema).toEqual([['a', 'b']]);
822
+ expect(r.data).toEqual([[null, 1]]);
823
+ expect(decompress(r)).toEqual([{ a: null, b: 1 }]);
824
+ const str = stringify(r);
825
+ expect(parse(str)).toEqual(r);
826
+ expect(decompress(parse(str))).toEqual([{ a: null, b: 1 }]);
407
827
  });
408
828
 
409
829
  test('字段值为 null 在 decompress 中还原', () => {
410
830
  const src = [{ a: 1, b: null }];
411
831
  const r = compress(src, copt);
412
- expect(decompress(r)[0].b).toBeNull();
832
+ expect(r.schema).toEqual([['a', 'b']]);
413
833
  if (trim) {
414
- expect(r.data[0]).toEqual([1]); // b 的 null 被 trim
834
+ expect(r.data).toEqual([[1]]);
415
835
  } else {
416
- expect(r.data[0]).toEqual([1, null]);
836
+ expect(r.data).toEqual([[1, null]]);
417
837
  }
838
+ expect(decompress(r)).toEqual([{ a: 1, b: null }]);
839
+ const str = stringify(r);
840
+ expect(parse(str)).toEqual(r);
841
+ expect(decompress(parse(str))).toEqual([{ a: 1, b: null }]);
418
842
  });
419
843
 
420
844
  test('对象数组中含非对象元素(item typeof!=="object" 分支)', () => {
421
845
  const src = [{ name: 'x', kids: [{ name: 'kid' }, 'string', null] }];
422
846
  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]);
847
+ expect(r.schema).toEqual([['name', { kids: [['name']] }]]);
848
+ if (trim) {
849
+ expect(r.data).toEqual([['x', [['kid'], 'string']]]);
850
+ } else {
851
+ expect(r.data).toEqual([['x', [['kid'], 'string', null]]]);
852
+ }
853
+ const decompressed = decompress(r);
854
+ expect(decompressed[0].name).toBe('x');
855
+ expect(decompressed[0].kids[0]).toEqual({ name: 'kid' });
856
+ const str = stringify(r);
857
+ expect(parse(str)).toEqual(r);
429
858
  });
430
859
 
431
860
  test('原数组含数字元素(typeof obj!=="object" 各分支)', () => {
432
861
  const src = [42, { name: 'a' }, true, { name: 'b' }];
433
862
  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']);
863
+ expect(r.schema).toEqual([['name']]);
864
+ expect(r.data).toEqual([42, ['a'], true, ['b']]);
865
+ expect(decompress(r)).toEqual([42, { name: 'a' }, true, { name: 'b' }]);
866
+ const str = stringify(r);
867
+ expect(parse(str)).toEqual(r);
441
868
  });
442
869
 
443
870
  test('getValueKind:数组首元素也是数组 → primitive-array', () => {
444
871
  const src = [{ name: 'x', matrix: [[1, 2], [3, 4]] }];
445
872
  const r = compress(src, copt);
446
- expect(r.schema[0]).toEqual(['name', 'matrix']);
447
- expect(r.data[0]).toEqual(['x', [[1, 2], [3, 4]]]);
873
+ expect(r.schema).toEqual([['name', 'matrix']]);
874
+ expect(r.data).toEqual([['x', [[1, 2], [3, 4]]]]);
448
875
  expect(decompress(r)).toEqual(src);
876
+ const str = stringify(r);
877
+ expect(parse(str)).toEqual(r);
878
+ expect(decompress(parse(str))).toEqual(src);
449
879
  });
450
880
 
451
881
  test('普通原始类型数组 [1,2,3] 不拆解', () => {
452
882
  const src = [{ name: 'x', scores: [1, 2, 3] }];
453
883
  const r = compress(src, copt);
454
- expect(r.schema[0]).toEqual(['name', 'scores']);
455
- expect(r.data[0]).toEqual(['x', [1, 2, 3]]);
884
+ expect(r.schema).toEqual([['name', 'scores']]);
885
+ expect(r.data).toEqual([['x', [1, 2, 3]]]);
456
886
  expect(decompress(r)).toEqual(src);
887
+ const str = stringify(r);
888
+ expect(parse(str)).toEqual(r);
889
+ expect(decompress(parse(str))).toEqual(src);
457
890
  });
458
891
 
459
892
  test('非对象元素 + 嵌套 key(buildRow line 108 三元 false 分支)', () => {
460
893
  const src = [42, { name: 'a', detail: { x: 1 } }];
461
894
  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]]);
895
+ expect(r.schema).toEqual([['name', { detail: ['x'] }]]);
896
+ expect(r.data).toEqual([42, ['a', [1]]]);
897
+ expect(decompress(r)).toEqual([42, { name: 'a', detail: { x: 1 } }]);
898
+ const str = stringify(r);
899
+ expect(parse(str)).toEqual(r);
466
900
  });
467
901
 
468
902
  test('非对象元素 + 对象数组(buildKeys line 73 typeof false 分支)', () => {
469
903
  const src = [42, { items: [{ name: 'a' }] }];
470
904
  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']]]);
905
+ expect(r.schema).toEqual([[{ items: [['name']] }]]);
906
+ expect(r.data).toEqual([42, [[['a']]]]);
907
+ expect(decompress(r)).toEqual([42, { items: [{ name: 'a' }] }]);
908
+ const str = stringify(r);
909
+ expect(parse(str)).toEqual(r);
475
910
  });
476
911
 
477
912
  test('字段值为 null 且 repValue 为 object-array(line 75 Array.isArray false 分支)', () => {
478
913
  const src = [
479
- { items: [{ name: 'a' }] }, // items 是 object-array
480
- { items: null } // items 为 null → !Array.isArray
914
+ { items: [{ name: 'a' }] },
915
+ { items: null }
481
916
  ];
482
917
  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]);
918
+ expect(r.schema).toEqual([[{ items: [['name']] }]]);
919
+ if (trim) {
920
+ expect(r.data).toEqual([[[['a']]], []]);
921
+ } else {
922
+ expect(r.data).toEqual([[[['a']]], [null]]);
923
+ }
924
+ expect(decompress(r)).toEqual([
925
+ { items: [{ name: 'a' }] },
926
+ { items: null }
927
+ ]);
928
+ const str = stringify(r);
929
+ expect(parse(str)).toEqual(r);
930
+ expect(decompress(parse(str))).toEqual([
931
+ { items: [{ name: 'a' }] },
932
+ { items: null }
933
+ ]);
486
934
  });
487
935
 
488
936
  test('trimTrailingNulls 端到端', () => {
@@ -492,96 +940,153 @@ describe('compress / decompress', () => {
492
940
  { name: '王五' },
493
941
  ];
494
942
  const r = compress(src, copt);
943
+ expect(r.schema).toEqual([['name', 'age', { profile: ['avatar', 'bio', 'file'] }]]);
495
944
  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(['王五']);
945
+ expect(r.data).toEqual([['张三', 28, ['a.jpg', 'Hello']], ['李四', 35, ['b.jpg']], ['王五']]);
499
946
  } 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]);
947
+ expect(r.data).toEqual([['张三', 28, ['a.jpg', 'Hello', null]], ['李四', 35, ['b.jpg', null, null]], ['王五', null, null]]);
503
948
  }
504
949
  expect(decompress(r)).toEqual([
505
950
  { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello', file: null } },
506
951
  { name: '李四', age: 35, profile: { avatar: 'b.jpg', bio: null, file: null } },
507
952
  { name: '王五', age: null, profile: null },
508
953
  ]);
954
+ const str = stringify(r);
955
+ expect(parse(str)).toEqual(r);
956
+ expect(decompress(parse(str))).toEqual([
957
+ { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello', file: null } },
958
+ { name: '李四', age: 35, profile: { avatar: 'b.jpg', bio: null, file: null } },
959
+ { name: '王五', age: null, profile: null },
960
+ ]);
509
961
  });
510
962
 
511
- });
963
+ test('数组的数组含原始值子数组', () => {
964
+ const src = [[1, 2], [{ a: 1 }]];
965
+ const r = compress(src, copt);
966
+ expect(r.schema).toEqual([[['a']]]);
967
+ expect(r.data).toEqual([[1, 2], [[1]]]);
968
+ expect(decompress(r)).toEqual([[1, 2], [{ a: 1 }]]);
969
+ const str = stringify(r);
970
+ expect(parse(str)).toEqual(r);
971
+ expect(decompress(parse(str))).toEqual([[1, 2], [{ a: 1 }]]);
972
+ });
512
973
 
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);
974
+ test('数组的数组含不同 key 对象(mergeSchemas 不同 key 合并)', () => {
975
+ const src = [[{ a: 1, b: 2 }], [{ c: 3 }]];
976
+ const r = compress(src, copt);
977
+ expect(r.schema).toEqual([[['a', 'b', 'c']]]);
978
+ if (trim) {
979
+ expect(r.data).toEqual([[[1, 2]], [[null, null, 3]]]);
980
+ } else {
981
+ expect(r.data).toEqual([[[1, 2, null]], [[null, null, 3]]]);
982
+ }
983
+ expect(decompress(r)).toEqual([[{ a: 1, b: 2, c: null }], [{ a: null, b: null, c: 3 }]]);
984
+ const str = stringify(r);
985
+ expect(parse(str)).toEqual(r);
986
+ });
987
+
988
+ test('数组的数组混合含对象和原始值(mergeSchemas s2=undefined 分支)', () => {
989
+ const src = [{ matrix: [[{ a: 1 }]] }, { matrix: [[1, 2]] }];
990
+ const r = compress(src, copt);
991
+ expect(r.schema).toEqual([[{ matrix: [[['a']]] }]]);
992
+ expect(r.data).toEqual([[[[[1]]]], [[[1, 2]]]]);
993
+ expect(decompress(r)).toEqual([{ matrix: [[{ a: 1 }]] }, { matrix: [[1, 2]] }]);
994
+ const str = stringify(r);
995
+ expect(parse(str)).toEqual(r);
523
996
  });
524
997
 
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);
998
+ test('数组的数组含空子数组(inferSchema 空数组分支)', () => {
999
+ const src = [[{ a: 1 }], []];
1000
+ const r = compress(src, copt);
1001
+ expect(r.schema).toEqual([[['a']]]);
1002
+ expect(r.data).toEqual([[[1]], []]);
1003
+ expect(decompress(r)).toEqual([[{ a: 1 }], []]);
1004
+ const str = stringify(r);
1005
+ expect(parse(str)).toEqual(r);
531
1006
  });
532
1007
 
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);
1008
+ test('数组的数组含不同嵌套 key 对象(mergeSchemas 对象 fieldDef 合并)', () => {
1009
+ const src = [[{ a: { x: 1 } }], [{ b: { y: 2 } }]];
1010
+ const r = compress(src, copt);
1011
+ expect(r.schema).toEqual([[[{ a: ['x'] }, { b: ['y'] }]]]);
1012
+ if (trim) {
1013
+ expect(r.data).toEqual([[[[1]]], [[null, [2]]]]);
1014
+ } else {
1015
+ expect(r.data).toEqual([[[[1], null]], [[null, [2]]]]);
1016
+ }
1017
+ expect(decompress(r)).toEqual([[{ a: { x: 1 }, b: null }], [{ a: null, b: { y: 2 } }]]);
1018
+ const str = stringify(r);
1019
+ expect(parse(str)).toEqual(r);
539
1020
  });
540
1021
 
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);
1022
+ test('对象数组字段含空数组值(compressWithSchema 数组值分支)', () => {
1023
+ const src = [{ items: [{ name: 'a' }] }, { items: [] }];
1024
+ const r = compress(src, copt);
1025
+ expect(r.schema).toEqual([[{ items: [['name']] }]]);
1026
+ expect(r.data).toEqual([[[['a']]], [[]]]);
1027
+ expect(decompress(r)).toEqual([{ items: [{ name: 'a' }] }, { items: [] }]);
1028
+ const str = stringify(r);
1029
+ expect(parse(str)).toEqual(r);
547
1030
  });
548
1031
 
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);
1032
+ test('对象数组字段含非数组值(Array.isArray(v) false 分支)', () => {
1033
+ const src = [{ items: [{ name: 'a' }] }, { items: 'string' }];
1034
+ const r = compress(src, copt);
1035
+ expect(r.schema).toEqual([[{ items: [['name']] }]]);
1036
+ if (trim) {
1037
+ expect(r.data).toEqual([[[['a']]], []]);
1038
+ } else {
1039
+ expect(r.data).toEqual([[[['a']]], [null]]);
1040
+ }
1041
+ expect(decompress(r)).toEqual([{ items: [{ name: 'a' }] }, { items: null }]);
1042
+ const str = stringify(r);
1043
+ expect(parse(str)).toEqual(r);
555
1044
  });
556
1045
 
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
1046
+ test('数组的数组字段含非数组值(array-of-arrays Array.isArray(v) false 分支)', () => {
1047
+ const src = [{ matrix: [[{ a: 1 }]] }, { matrix: 'string' }];
1048
+ const r = compress(src, copt);
1049
+ expect(r.schema).toEqual([[{ matrix: [[['a']]] }]]);
565
1050
  if (trim) {
566
- expect(cmp.data[0].length).toBe(1);
567
- expect(cmp.data[1].length).toBe(1);
1051
+ expect(r.data).toEqual([[[[[1]]]], []]);
568
1052
  } else {
569
- expect(cmp.data[0].length).toBe(2);
570
- expect(cmp.data[1].length).toBe(2);
1053
+ expect(r.data).toEqual([[[[[1]]]], [null]]);
571
1054
  }
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
- ]);
1055
+ expect(decompress(r)).toEqual([{ matrix: [[{ a: 1 }]] }, { matrix: null }]);
1056
+ const str = stringify(r);
1057
+ expect(parse(str)).toEqual(r);
579
1058
  });
1059
+
580
1060
  });
581
1061
 
582
1062
  }); // end [label] describe
583
1063
  }); // end forEach
584
1064
 
1065
+ // line 285: decompressWithSchema 中 fieldDef 既非 string 也非 object → continue
1066
+ describe('decompress 特殊 schema', () => {
1067
+ test('schema 含非字符串非对象元素 → 跳过', () => {
1068
+ const r = decompress({ schema: [1, 'name'], data: [42, 'test'] });
1069
+ expect(r).toEqual({ name: 'test' });
1070
+ });
1071
+
1072
+ test('decompress(null) → null', () => {
1073
+ expect(decompress(null)).toBeNull();
1074
+ });
1075
+
1076
+ test('decompress([]) → []', () => {
1077
+ expect(decompress([])).toEqual([]);
1078
+ });
1079
+
1080
+ test('decompress 无 data 属性的对象 → 原样返回', () => {
1081
+ expect(decompress({ foo: 1 })).toEqual({ foo: 1 });
1082
+ });
1083
+
1084
+ test('decompress 非对象值 → 原样返回', () => {
1085
+ expect(decompress('hello')).toBe('hello');
1086
+ expect(decompress(42)).toBe(42);
1087
+ });
1088
+ });
1089
+
585
1090
  /* =========================================================
586
1091
  stringify / parse — 省略 null 文本化 与 还原
587
1092
  (以下测试不依赖 compress,不需要按 trim 分组)