@kubex/zinc 1.1.95 → 1.1.97

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.
@@ -1,483 +0,0 @@
1
- import {
2
- cellRows,
3
- cloneWithNewIds,
4
- containerCells,
5
- containerColumns,
6
- containerDepth,
7
- containerHeight,
8
- extractSection,
9
- findSection,
10
- insertIntoCell,
11
- migrateSection,
12
- normaliseCells,
13
- normaliseGrowth,
14
- normaliseSections,
15
- padCells,
16
- patchSection,
17
- recolumnCells,
18
- trimTrailingEmptyCells,
19
- type TypeLookup,
20
- } from './page-tree';
21
- import {
22
- DEFAULT_WIDTHS,
23
- defaultLayout,
24
- isContainer,
25
- MAX_COLUMNS,
26
- MAX_SECTIONS,
27
- type PageSection,
28
- type PageSectionType,
29
- sectionSummary,
30
- } from './page.types';
31
- import {expect} from '@open-wc/testing';
32
-
33
- const containerType = (over: Partial<PageSectionType> = {}): PageSectionType =>
34
- ({type: 'row', label: 'Row', container: true, ...over});
35
-
36
- describe('page.types container helpers', () => {
37
- it('treats a container flag and the deprecated slots count as containers', () => {
38
- expect(isContainer(containerType())).to.be.true;
39
- expect(isContainer({type: 'grid', label: 'Grid', slots: 6})).to.be.true;
40
- expect(isContainer({type: 'hero', label: 'Hero'})).to.be.false;
41
- expect(isContainer(undefined)).to.be.false;
42
- });
43
-
44
- it('seeds a bare container with three equal columns', () => {
45
- expect(defaultLayout(containerType())).to.deep.equal({widths: [...DEFAULT_WIDTHS], grow: false});
46
- });
47
-
48
- it('seeds from defaultWidths and defaultGrow when declared', () => {
49
- expect(defaultLayout(containerType({defaultWidths: [1, 2, 1], defaultGrow: true})))
50
- .to.deep.equal({widths: [1, 2, 1], grow: true});
51
- });
52
-
53
- it('clamps a declared column count to MAX_COLUMNS', () => {
54
- const widths = Array.from({length: MAX_COLUMNS + 3}, () => 1);
55
- expect(defaultLayout(containerType({defaultWidths: widths})).widths).to.have.lengthOf(MAX_COLUMNS);
56
- });
57
-
58
- it('summarises a container by its shape rather than its data', () => {
59
- const section: PageSection = {
60
- id: 'c1',
61
- type: 'row',
62
- data: {title: 'ignored'},
63
- layout: {widths: [1, 2, 1], grow: false},
64
- cells: [[{id: 'a', type: 'hero', data: {}}], [{id: 'b', type: 'hero', data: {}}], []],
65
- };
66
- expect(sectionSummary(section, containerType())).to.equal('3 columns · 2 sections');
67
- });
68
-
69
- it('singularises a one-column, one-section container', () => {
70
- const section: PageSection = {
71
- id: 'c1',
72
- type: 'row',
73
- data: {},
74
- layout: {widths: [1], grow: false},
75
- cells: [[{id: 'a', type: 'hero', data: {}}]],
76
- };
77
- expect(sectionSummary(section, containerType())).to.equal('1 column · 1 section');
78
- });
79
-
80
- it('still summarises a non-container from its data', () => {
81
- const section: PageSection = {id: 'h1', type: 'hero', data: {title: 'Help Centre'}};
82
- expect(sectionSummary(section, {type: 'hero', label: 'Hero'})).to.equal('Help Centre');
83
- });
84
- });
85
-
86
- const s = (id: string): PageSection => ({id, type: 'hero', data: {}});
87
-
88
- describe('page-tree cell normalisation', () => {
89
- it('drops only trailing empty cells', () => {
90
- expect(trimTrailingEmptyCells([[s('a')], [], [s('b')], [], []]))
91
- .to.deep.equal([[s('a')], [], [s('b')]]);
92
- });
93
-
94
- it('pads to a whole number of rows', () => {
95
- expect(padCells([[s('a')], [s('b')]], 3)).to.deep.equal([[s('a')], [s('b')], []]);
96
- });
97
-
98
- it('keeps at least one row for an empty container', () => {
99
- expect(padCells([], 3)).to.deep.equal([[], [], []]);
100
- expect(normaliseCells([], 2, true)).to.deep.equal([[], []]);
101
- });
102
-
103
- it('keeps a fixed container trailing empty row', () => {
104
- const cells = [[s('a')], [], [], [], [], []];
105
- expect(normaliseCells(cells, 3, false)).to.have.lengthOf(6);
106
- expect(normaliseCells(cells, 3, false)).to.deep.equal(cells);
107
- });
108
-
109
- it('trims a growable container trailing empty row', () => {
110
- const cells = [[s('a')], [], [], [], [], []];
111
- expect(normaliseCells(cells, 3, true)).to.deep.equal([[s('a')], [], []]);
112
- });
113
-
114
- it('re-chunks losslessly when the column count changes', () => {
115
- const cells = [[s('a')], [s('b')], [s('c')], [s('d')]];
116
- const widened = recolumnCells(cells, 4);
117
- expect(widened).to.deep.equal([[s('a')], [s('b')], [s('c')], [s('d')]]);
118
-
119
- const narrowed = recolumnCells(cells, 2);
120
- expect(narrowed).to.deep.equal([[s('a')], [s('b')], [s('c')], [s('d')]]);
121
- expect(cellRows(narrowed, 2)).to.deep.equal([
122
- [[s('a')], [s('b')]],
123
- [[s('c')], [s('d')]],
124
- ]);
125
- });
126
-
127
- it('never loses a section when narrowing columns', () => {
128
- const cells = [[s('a'), s('b')], [], [s('c')], [], [], []];
129
- const narrowed = recolumnCells(cells, 1);
130
- const ids = narrowed.flat().map(x => x.id);
131
- expect(ids).to.deep.equal(['a', 'b', 'c']);
132
- });
133
-
134
- it('chunks cells into rows row-major', () => {
135
- expect(cellRows([[s('a')], [s('b')], [s('c')], [s('d')]], 3)).to.deep.equal([
136
- [[s('a')], [s('b')], [s('c')]],
137
- [[s('d')]],
138
- ]);
139
- });
140
-
141
- it('reads a container instance layout, falling back to the type then the default', () => {
142
- const type: PageSectionType = {type: 'row', label: 'Row', container: true, defaultWidths: [2, 1]};
143
- const withLayout: PageSection = {id: 'c', type: 'row', data: {}, layout: {widths: [1, 2, 1], grow: true}};
144
- const bare: PageSection = {id: 'c', type: 'row', data: {}};
145
- expect(containerColumns(withLayout, type)).to.equal(3);
146
- expect(containerColumns(bare, type)).to.equal(2);
147
- expect(containerColumns(bare, {type: 'row', label: 'Row', container: true})).to.equal(3);
148
- });
149
-
150
- it('returns no cells for a non-container', () => {
151
- expect(containerCells({id: 'h', type: 'hero', data: {}}, {type: 'hero', label: 'Hero'}))
152
- .to.deep.equal([]);
153
- });
154
- });
155
-
156
- describe('page-tree recursive operations', () => {
157
- const nested = (): PageSection[] => [
158
- {id: 'top', type: 'hero', data: {}},
159
- {
160
- id: 'outer',
161
- type: 'row',
162
- data: {},
163
- layout: {widths: [1, 1], grow: false},
164
- cells: [
165
- [{id: 'a', type: 'hero', data: {}}],
166
- [
167
- {
168
- id: 'inner',
169
- type: 'row',
170
- data: {},
171
- layout: {widths: [1, 1], grow: false},
172
- cells: [[{id: 'deep', type: 'hero', data: {}}], []],
173
- },
174
- ],
175
- ],
176
- },
177
- ];
178
-
179
- it('finds sections at every level', () => {
180
- const sections = nested();
181
- expect(findSection(sections, 'top')?.id).to.equal('top');
182
- expect(findSection(sections, 'a')?.id).to.equal('a');
183
- expect(findSection(sections, 'deep')?.id).to.equal('deep');
184
- expect(findSection(sections, 'nope')).to.be.undefined;
185
- expect(findSection(sections, null)).to.be.undefined;
186
- });
187
-
188
- it('patches a section nested two levels down', () => {
189
- const patched = patchSection(nested(), 'deep', sec => ({...sec, label: 'renamed'}));
190
- expect(findSection(patched, 'deep')?.label).to.equal('renamed');
191
- expect(findSection(patched, 'a')?.label).to.be.undefined;
192
- });
193
-
194
- it('extracts from a nested cell and leaves the cell empty', () => {
195
- const [removed, rest] = extractSection(nested(), 'deep');
196
- expect(removed?.id).to.equal('deep');
197
- expect(findSection(rest, 'deep')).to.be.undefined;
198
- expect(findSection(rest, 'inner')?.cells?.[0]).to.deep.equal([]);
199
- });
200
-
201
- it('extracts a whole container with its contents', () => {
202
- const [removed, rest] = extractSection(nested(), 'inner');
203
- expect(removed?.id).to.equal('inner');
204
- expect(findSection(removed ? [removed] : [], 'deep')?.id).to.equal('deep');
205
- expect(findSection(rest, 'deep')).to.be.undefined;
206
- expect(findSection(rest, 'inner')).to.be.undefined;
207
- });
208
-
209
- it('inserts into a cell at an index', () => {
210
- const sections = insertIntoCell(nested(), 'outer', 0, 0, {id: 'new', type: 'hero', data: {}}, 2);
211
- expect(findSection(sections, 'outer')?.cells?.[0].map(x => x.id)).to.deep.equal(['new', 'a']);
212
- });
213
-
214
- it('appends when the insert index is past the end of the stack', () => {
215
- const sections = insertIntoCell(nested(), 'outer', 0, 99, {id: 'new', type: 'hero', data: {}}, 2);
216
- expect(findSection(sections, 'outer')?.cells?.[0].map(x => x.id)).to.deep.equal(['a', 'new']);
217
- });
218
-
219
- it('extends the cell list when inserting past the last row', () => {
220
- const sections = insertIntoCell(nested(), 'outer', 3, 0, {id: 'new', type: 'hero', data: {}}, 2);
221
- const cells = findSection(sections, 'outer')?.cells ?? [];
222
- expect(cells).to.have.lengthOf(4);
223
- expect(cells[3].map(x => x.id)).to.deep.equal(['new']);
224
- });
225
-
226
- it('clamps a negative, fractional, or non-finite cell index into cell 0', () => {
227
- const negative = insertIntoCell(nested(), 'outer', -1, 0, {id: 'new', type: 'hero', data: {}}, 2);
228
- expect(findSection(negative, 'outer')?.cells?.[0].map(x => x.id)).to.deep.equal(['new', 'a']);
229
-
230
- const fractional = insertIntoCell(nested(), 'outer', 0.5, 0, {id: 'new2', type: 'hero', data: {}}, 2);
231
- expect(findSection(fractional, 'outer')?.cells?.[0].map(x => x.id)).to.deep.equal(['new2', 'a']);
232
-
233
- const infinite = insertIntoCell(nested(), 'outer', Infinity, 0, {id: 'new3', type: 'hero', data: {}}, 2);
234
- expect(findSection(infinite, 'outer')?.cells?.[0].map(x => x.id)).to.deep.equal(['new3', 'a']);
235
- });
236
-
237
- it('reports container nesting depth', () => {
238
- const sections = nested();
239
- expect(containerDepth(sections, 'outer')).to.equal(1);
240
- expect(containerDepth(sections, 'inner')).to.equal(2);
241
- expect(containerDepth(sections, 'missing')).to.equal(0);
242
- });
243
-
244
- it('clones a container giving every descendant a fresh id', () => {
245
- const original = findSection(nested(), 'outer')!;
246
- const copy = cloneWithNewIds(original);
247
- expect(copy.id).to.not.equal(original.id);
248
- expect(copy.cells?.[0][0].id).to.not.equal('a');
249
- expect(copy.cells?.[1][0].cells?.[0][0].id).to.not.equal('deep');
250
- expect(findSection([original], 'deep')?.id).to.equal('deep');
251
- });
252
-
253
- const containerAt = (id: string, cells: PageSection[][]): PageSection =>
254
- ({id, type: 'row', data: {}, layout: {widths: [1], grow: false}, cells});
255
-
256
- it('heights a flat container (no nested containers) at 1', () => {
257
- const flat = containerAt('flat', [[{id: 'a', type: 'hero', data: {}}], []]);
258
- expect(containerHeight(flat)).to.equal(1);
259
- });
260
-
261
- it('heights a container with one nested level at 2', () => {
262
- const oneDeep = containerAt('l1', [[containerAt('l2', [[{id: 'leaf', type: 'hero', data: {}}]])]]);
263
- expect(containerHeight(oneDeep)).to.equal(2);
264
- });
265
-
266
- it('heights a container with two nested levels at 3', () => {
267
- const twoDeep = containerAt('l1', [[containerAt('l2', [[containerAt('l3', [[{id: 'leaf', type: 'hero', data: {}}]])]])]]);
268
- expect(containerHeight(twoDeep)).to.equal(3);
269
- });
270
-
271
- it('heights a non-container at 0', () => {
272
- expect(containerHeight({id: 'h', type: 'hero', data: {}})).to.equal(0);
273
- });
274
- });
275
-
276
- describe('page-tree commit-time growth normalisation', () => {
277
- const lookup: TypeLookup = key => ({
278
- row: {type: 'row', label: 'Row', container: true},
279
- hero: {type: 'hero', label: 'Hero'},
280
- })[key];
281
-
282
- it('trims a growable container trailing empty row across the whole tree', () => {
283
- const sections: PageSection[] = [{
284
- id: 'outer', type: 'row', data: {}, layout: {widths: [1, 1], grow: true},
285
- cells: [[s('a')], [s('b')], [], []],
286
- }];
287
- const normalised = normaliseGrowth(sections, lookup);
288
- expect(normalised[0].cells).to.deep.equal([[s('a')], [s('b')]]);
289
- });
290
-
291
- it('leaves a fixed container trailing empty row untouched', () => {
292
- const sections: PageSection[] = [{
293
- id: 'outer', type: 'row', data: {}, layout: {widths: [1, 1], grow: false},
294
- cells: [[s('a')], [s('b')], [], []],
295
- }];
296
- expect(normaliseGrowth(sections, lookup)).to.deep.equal(sections);
297
- });
298
-
299
- it('is idempotent', () => {
300
- const sections: PageSection[] = [{
301
- id: 'outer', type: 'row', data: {}, layout: {widths: [1, 1], grow: true},
302
- cells: [[s('a')], [], [], []],
303
- }];
304
- const once = normaliseGrowth(sections, lookup);
305
- expect(normaliseGrowth(once, lookup)).to.deep.equal(once);
306
- });
307
-
308
- it('leaves cells untouched on a section whose type is not a registered container', () => {
309
- const sections: PageSection[] = [{id: 'ghost', type: 'not-registered', data: {}, cells: [[s('a')], []]}];
310
- expect(normaliseGrowth(sections, lookup)).to.deep.equal(sections);
311
- });
312
- });
313
-
314
- describe('page-tree migration and normalisation', () => {
315
- const lookup: TypeLookup = key => ({
316
- hero: {type: 'hero', label: 'Hero'},
317
- grid: {type: 'grid', label: 'Grid', slots: 6},
318
- row: {type: 'row', label: 'Row', container: true},
319
- })[key];
320
-
321
- it('migrates a flat null-padded children array to cells', () => {
322
- const migrated = migrateSection(
323
- {
324
- id: 'g1',
325
- type: 'grid',
326
- data: {},
327
- children: [{id: 'a', type: 'hero', data: {}}, null, {id: 'b', type: 'hero', data: {}}, null, null, null],
328
- },
329
- lookup('grid')
330
- );
331
- expect(migrated.children).to.be.undefined;
332
- expect(migrated.layout).to.deep.equal({widths: [1, 1, 1], grow: false});
333
- expect(migrated.cells).to.have.lengthOf(6);
334
- expect(migrated.cells?.[0].map(x => x.id)).to.deep.equal(['a']);
335
- expect(migrated.cells?.[1]).to.deep.equal([]);
336
- expect(migrated.cells?.[2].map(x => x.id)).to.deep.equal(['b']);
337
- });
338
-
339
- it('leaves a section without children untouched', () => {
340
- const section: PageSection = {id: 'h', type: 'hero', data: {}};
341
- expect(migrateSection(section, lookup('hero'))).to.deep.equal(section);
342
- });
343
-
344
- it('normalises an old-shape page and reports no warnings', () => {
345
- const {sections, warnings} = normaliseSections(
346
- [{id: 'g1', type: 'grid', data: {}, children: [{id: 'a', type: 'hero', data: {}}, null, null]}],
347
- lookup
348
- );
349
- expect(warnings).to.deep.equal([]);
350
- expect(sections[0].cells?.[0].map(x => x.id)).to.deep.equal(['a']);
351
- expect(sections[0].children).to.be.undefined;
352
- });
353
-
354
- it('regenerates duplicate and missing ids across nesting levels', () => {
355
- const {sections} = normaliseSections(
356
- [
357
- {id: 'dup', type: 'row', data: {}, cells: [[{id: 'dup', type: 'hero', data: {}}]]},
358
- {type: 'hero', data: {}},
359
- ],
360
- lookup
361
- );
362
- const ids = [sections[0].id, sections[0].cells![0][0].id, sections[1].id];
363
- expect(new Set(ids).size).to.equal(3);
364
- expect(ids.every(Boolean)).to.be.true;
365
- });
366
-
367
- it('drops entries that are not sections', () => {
368
- const {sections} = normaliseSections(
369
- [{id: 'a', type: 'hero', data: {}}, null, {id: 'b'}, {type: 42}, 'nope'],
370
- lookup
371
- );
372
- expect(sections.map(x => x.id)).to.deep.equal(['a']);
373
- });
374
-
375
- it('empties the cells of a container nested past the level cap but keeps the section', () => {
376
- const {sections, warnings} = normaliseSections(
377
- [
378
- {
379
- id: 'l1',
380
- type: 'row',
381
- data: {},
382
- cells: [[{id: 'l2', type: 'row', data: {}, cells: [[{id: 'l3', type: 'row', data: {}, cells: [[{id: 'x', type: 'hero', data: {}}]]}]]}]],
383
- },
384
- ],
385
- lookup
386
- );
387
- expect(findSection(sections, 'l3')?.id).to.equal('l3');
388
- expect(findSection(sections, 'l3')?.cells).to.deep.equal([]);
389
- expect(findSection(sections, 'x')).to.be.undefined;
390
- expect(warnings.some(w => w.includes('nesting'))).to.be.true;
391
- });
392
-
393
- it('falls back to default widths for an unusable layout and warns', () => {
394
- const {sections, warnings} = normaliseSections(
395
- [{id: 'r', type: 'row', data: {}, layout: {widths: [], grow: false}, cells: [[]]}],
396
- lookup
397
- );
398
- expect(sections[0].layout?.widths).to.deep.equal([1, 1, 1]);
399
- expect(warnings.some(w => w.includes('widths'))).to.be.true;
400
- });
401
-
402
- it('warns and corrects non-numeric width entries', () => {
403
- const {sections, warnings} = normaliseSections(
404
- [{id: 'r', type: 'row', data: {}, layout: {widths: ['a', 'b', 'c'], grow: false}, cells: [[]]}],
405
- lookup
406
- );
407
- expect(sections[0].layout?.widths).to.deep.equal([1, 1, 1]);
408
- expect(warnings.some(w => w.includes('widths'))).to.be.true;
409
- });
410
-
411
- it('warns and corrects out-of-range width entries', () => {
412
- const {sections, warnings} = normaliseSections(
413
- [{id: 'r', type: 'row', data: {}, layout: {widths: [-5, 0, 999], grow: false}, cells: [[]]}],
414
- lookup
415
- );
416
- expect(sections[0].layout?.widths).to.deep.equal([1, 1, 12]);
417
- expect(warnings.some(w => w.includes('widths'))).to.be.true;
418
- });
419
-
420
- it('warns and truncates more than MAX_COLUMNS width entries', () => {
421
- const {sections, warnings} = normaliseSections(
422
- [{id: 'r', type: 'row', data: {}, layout: {widths: [1, 1, 1, 1, 1, 1, 1], grow: false}, cells: [[]]}],
423
- lookup
424
- );
425
- expect(sections[0].layout?.widths).to.deep.equal([1, 1, 1, 1, 1, 1]);
426
- expect(warnings.some(w => w.includes('widths'))).to.be.true;
427
- });
428
-
429
- it('does not warn when widths are already valid', () => {
430
- const {sections, warnings} = normaliseSections(
431
- [{id: 'r', type: 'row', data: {}, layout: {widths: [1, 2, 1], grow: false}, cells: [[]]}],
432
- lookup
433
- );
434
- expect(sections[0].layout?.widths).to.deep.equal([1, 2, 1]);
435
- expect(warnings.some(w => w.includes('widths'))).to.be.false;
436
- });
437
-
438
- it('clamps an absurd cells length and warns', () => {
439
- const cells = Array.from({length: MAX_SECTIONS + 10}, () => [] as PageSection[]);
440
- const {sections, warnings} = normaliseSections(
441
- [{id: 'r', type: 'row', data: {}, layout: {widths: [1, 1, 1], grow: false}, cells}],
442
- lookup
443
- );
444
- expect(sections[0].cells!.length).to.be.at.most(MAX_SECTIONS);
445
- expect(warnings.some(w => w.includes('cells'))).to.be.true;
446
- });
447
-
448
- it('clamps the total section count and warns', () => {
449
- const many = Array.from({length: MAX_SECTIONS + 5}, (_, i) => ({id: `s${i}`, type: 'hero', data: {}}));
450
- const {sections, warnings} = normaliseSections(many, lookup);
451
- expect(sections).to.have.lengthOf(MAX_SECTIONS);
452
- expect(warnings.some(w => w.includes('sections'))).to.be.true;
453
- });
454
-
455
- it('bounds total sections across nested containers, not just the top level', () => {
456
- const heroesPerInner = 300;
457
- const heroes = (prefix: string) =>
458
- Array.from({length: heroesPerInner}, (_, i) => [{id: `${prefix}-h${i}`, type: 'hero', data: {}}]);
459
- const inner = (id: string): PageSection =>
460
- ({id, type: 'row', data: {}, layout: {widths: [1], grow: false}, cells: heroes(id)});
461
- // One level-1 container holding three level-2 containers, each with 300 heroes —
462
- // 904 sections in total, none of which trips the per-container cells clamp
463
- // (300 < 500), only the whole-tree budget.
464
- const outer: PageSection = {
465
- id: 'outer', type: 'row', data: {}, layout: {widths: [1], grow: false},
466
- cells: [[inner('a')], [inner('b')], [inner('c')]],
467
- };
468
- const {sections, warnings} = normaliseSections([outer], lookup);
469
-
470
- const countAll = (list: PageSection[]): number =>
471
- list.reduce((n, section) => n + 1 + (section.cells ?? []).reduce((m, cell) => m + countAll(cell), 0), 0);
472
- expect(countAll(sections)).to.be.at.most(MAX_SECTIONS);
473
- expect(warnings.some(w => w.includes('sections'))).to.be.true;
474
- });
475
-
476
- it('preserves cells on an unregistered type', () => {
477
- const {sections} = normaliseSections(
478
- [{id: 'ghost', type: 'not-registered', data: {}, cells: [[{id: 'kept', type: 'hero', data: {}}]]}],
479
- lookup
480
- );
481
- expect(findSection(sections, 'kept')?.id).to.equal('kept');
482
- });
483
- });