@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.
- package/dist/custom-elements.json +193 -428
- package/dist/vscode.html-custom-data.json +1 -1
- package/dist/web-types.json +3 -3
- package/dist/zn.d.ts +113 -224
- package/dist/zn.min.js +343 -384
- package/docs/pages/components/flow-builder.md +17 -1
- package/docs/pages/components/page-builder.md +21 -92
- package/package.json +1 -1
- package/src/components/data-table/data-table.component.ts +13 -8
- package/src/components/data-table/data-table.scss +1 -1
- package/src/components/flow-builder/flow-builder.component.ts +16 -1
- package/src/components/flow-builder/flow-builder.test.ts +206 -1
- package/src/components/flow-builder/flow-geometry.test.ts +102 -0
- package/src/components/flow-builder/flow.types.ts +82 -15
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +163 -108
- package/src/components/flow-builder/modules/flow-step/flow-step.component.ts +2 -0
- package/src/components/page-builder/page-builder.component.ts +229 -429
- package/src/components/page-builder/page-builder.scss +10 -64
- package/src/components/page-builder/page-builder.test.ts +162 -614
- package/src/components/page-builder/page.types.ts +7 -75
- package/src/components/remarkd-editor/remarkd-editor.component.ts +37 -11
- package/src/components/remarkd-editor/remarkd-editor.test.ts +79 -0
- package/src/components/page-builder/page-tree.test.ts +0 -483
- package/src/components/page-builder/page-tree.ts +0 -329
|
@@ -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
|
-
});
|