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/README.md +34 -25
- package/README_EN.md +34 -25
- package/compress.js +44 -74
- package/coverage/clover.xml +275 -0
- package/coverage/coverage-final.json +2 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/compress.js.html +1630 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +116 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +636 -0
- package/data/data.json +96365 -0
- package/data/data.json.slim +1 -0
- package/esm.mjs +1 -0
- package/package.json +1 -1
- package/test.js +719 -214
- package/.claude/settings.local.json +0 -11
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('
|
|
142
|
-
expect(r1.schema).
|
|
143
|
-
|
|
423
|
+
test('schema 一致', () => {
|
|
424
|
+
expect(r1.schema).toEqual([[
|
|
425
|
+
"姓名", "年龄", { "家人": [["姓名", "年龄"]] }, { "伴侣": ["姓名", "年龄"] }
|
|
426
|
+
]]);
|
|
144
427
|
});
|
|
145
428
|
|
|
146
|
-
test('
|
|
147
|
-
|
|
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('
|
|
161
|
-
expect(r2.schema
|
|
457
|
+
test('schema 一致', () => {
|
|
458
|
+
expect(r2.schema).toEqual([["姓名", "班级"]]);
|
|
162
459
|
});
|
|
163
460
|
|
|
164
|
-
test('
|
|
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
|
|
482
|
+
test('schema 一致', () => {
|
|
483
|
+
expect(r3.schema).toEqual([["姓名", "年龄", { "家人": [["姓名", "年龄"]] }, { "伴侣": ["姓名", "年龄"] }]]);
|
|
181
484
|
});
|
|
182
485
|
|
|
183
|
-
test(
|
|
486
|
+
test('data 一致', () => {
|
|
184
487
|
if (trim) {
|
|
185
|
-
expect(r3.data
|
|
488
|
+
expect(r3.data).toEqual([
|
|
489
|
+
['张三', 19, [['张四', 40]]],
|
|
490
|
+
['李小花', 28, [['李大国', 55]], ['赵明', 30]]
|
|
491
|
+
]);
|
|
186
492
|
} else {
|
|
187
|
-
expect(r3.data
|
|
493
|
+
expect(r3.data).toEqual([
|
|
494
|
+
["张三", 19, [["张四", 40]], null],
|
|
495
|
+
["李小花", 28, [["李大国", 55]], ["赵明", 30]]
|
|
496
|
+
]);
|
|
188
497
|
}
|
|
189
498
|
});
|
|
190
499
|
|
|
191
|
-
test('
|
|
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('
|
|
200
|
-
|
|
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
|
|
514
|
+
test('schema 一致', () => {
|
|
515
|
+
expect(r4.schema).toEqual([["姓名", "年龄", { "家人": [["姓名", "年龄", "关系"]] }]]);
|
|
213
516
|
});
|
|
214
517
|
|
|
215
|
-
test(
|
|
518
|
+
test('data 一致', () => {
|
|
216
519
|
if (trim) {
|
|
217
|
-
expect(r4.data
|
|
520
|
+
expect(r4.data).toEqual([
|
|
521
|
+
["张三", 19, [["张四", 40], ["李五", 41, "母亲"]]]
|
|
522
|
+
]);
|
|
218
523
|
} else {
|
|
219
|
-
expect(r4.data
|
|
524
|
+
expect(r4.data).toEqual([
|
|
525
|
+
["张三", 19, [["张四", 40, null], ["李五", 41, "母亲"]]]
|
|
526
|
+
]);
|
|
220
527
|
}
|
|
221
528
|
});
|
|
222
529
|
|
|
223
|
-
test('
|
|
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('
|
|
232
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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('
|
|
276
|
-
expect(r5
|
|
568
|
+
test('还原一致', () => {
|
|
569
|
+
expect(decompress(r5)).toEqual(src5Decompressed);
|
|
277
570
|
});
|
|
278
571
|
|
|
279
|
-
test('
|
|
280
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
579
|
+
/* =========================================================
|
|
580
|
+
场景6-10:复杂嵌套对象和数组
|
|
581
|
+
========================================================= */
|
|
286
582
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
});
|
|
583
|
+
describe('场景6:四层嵌套(组织→部门→团队→成员+技能标签)', () => {
|
|
584
|
+
const r6 = compress(src6, copt);
|
|
290
585
|
|
|
291
|
-
test('
|
|
292
|
-
expect(
|
|
586
|
+
test('schema 一致', () => {
|
|
587
|
+
expect(r6.schema).toEqual([['org', { departments: [['name', { teams: [['lead', { members: [['name', 'level', 'skills']] }]] }]] }]]);
|
|
293
588
|
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
});
|
|
604
|
+
describe('场景7:同层混合 — 嵌套对象+嵌套数组+原始数组,字段不对称', () => {
|
|
605
|
+
const r7 = compress(src7, copt);
|
|
307
606
|
|
|
308
|
-
test('
|
|
309
|
-
|
|
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('
|
|
316
|
-
expect(
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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 单个对象
|
|
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
|
|
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
|
|
778
|
+
expect(r.data).toEqual([['a', 10], null, ['b']]);
|
|
371
779
|
} else {
|
|
372
|
-
expect(r.data
|
|
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
|
|
385
|
-
expect(r.data
|
|
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
|
-
|
|
393
|
-
expect(r.
|
|
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
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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(
|
|
832
|
+
expect(r.schema).toEqual([['a', 'b']]);
|
|
413
833
|
if (trim) {
|
|
414
|
-
expect(r.data
|
|
834
|
+
expect(r.data).toEqual([[1]]);
|
|
415
835
|
} else {
|
|
416
|
-
expect(r.data
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
expect(
|
|
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
|
|
447
|
-
expect(r.data
|
|
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
|
|
455
|
-
expect(r.data
|
|
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
|
|
463
|
-
|
|
464
|
-
expect(r
|
|
465
|
-
|
|
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
|
|
472
|
-
|
|
473
|
-
expect(r
|
|
474
|
-
|
|
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' }] },
|
|
480
|
-
{ items: null }
|
|
914
|
+
{ items: [{ name: 'a' }] },
|
|
915
|
+
{ items: null }
|
|
481
916
|
];
|
|
482
917
|
const r = compress(src, copt);
|
|
483
|
-
expect(r.schema
|
|
484
|
-
|
|
485
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
expect(decompress(
|
|
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('
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
expect(
|
|
530
|
-
expect(decompress(
|
|
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('
|
|
534
|
-
const
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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('
|
|
542
|
-
const
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
expect(
|
|
546
|
-
expect(decompress(
|
|
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('
|
|
550
|
-
const
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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('
|
|
558
|
-
const src = [
|
|
559
|
-
|
|
560
|
-
|
|
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(
|
|
567
|
-
expect(cmp.data[1].length).toBe(1);
|
|
1051
|
+
expect(r.data).toEqual([[[[[1]]]], []]);
|
|
568
1052
|
} else {
|
|
569
|
-
expect(
|
|
570
|
-
expect(cmp.data[1].length).toBe(2);
|
|
1053
|
+
expect(r.data).toEqual([[[[[1]]]], [null]]);
|
|
571
1054
|
}
|
|
572
|
-
|
|
573
|
-
const str = stringify(
|
|
574
|
-
expect(parse(str)).toEqual(
|
|
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 分组)
|