@akinon/pz-theme 2.0.71-beta.0 → 2.0.71
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/CHANGELOG.md +7 -4
- package/package.json +4 -4
- package/src/__tests__/custom-catalog-announce.test.ts +276 -0
- package/src/blocks/group-block.tsx +9 -57
- package/src/blocks/render-composed-children.tsx +104 -0
- package/src/custom/README.md +249 -0
- package/src/custom/__tests__/define.test.tsx +619 -0
- package/src/custom/__tests__/module-boundaries.test.ts +156 -0
- package/src/custom/__tests__/public-api.test.tsx +427 -0
- package/src/custom/__tests__/slot-node-wire.test.tsx +309 -0
- package/src/custom/__tests__/slot-nodes.test.ts +144 -0
- package/src/custom/__tests__/theme-editable.test.tsx +486 -0
- package/src/custom/__tests__/theme-slot.test.tsx +387 -0
- package/src/custom/__tests__/use-theme-setting.test.tsx +260 -0
- package/src/custom/block-context.ts +64 -0
- package/src/custom/block-host.tsx +162 -0
- package/src/custom/catalog.ts +97 -0
- package/src/custom/collect.ts +342 -0
- package/src/custom/define.tsx +172 -0
- package/src/custom/editable-tags.ts +120 -0
- package/src/custom/index.ts +105 -0
- package/src/custom/parts.ts +108 -0
- package/src/custom/property-types.ts +70 -0
- package/src/custom/register.ts +545 -0
- package/src/custom/slot-nodes.ts +119 -0
- package/src/custom/theme-editable.tsx +342 -0
- package/src/custom/theme-slot.tsx +210 -0
- package/src/custom/types.ts +251 -0
- package/src/custom/use-theme-setting.ts +225 -0
- package/src/index.ts +66 -0
- package/src/placeholder-registry.ts +168 -16
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
// The recursive renderer, stubbed with a countable marker so a slot's children
|
|
5
|
+
// are assertable without dragging every block renderer into the assertion. The
|
|
6
|
+
// same stand-in `public-api.test.tsx` uses, for the same reason.
|
|
7
|
+
jest.mock('../../theme-block', () => ({
|
|
8
|
+
__esModule: true,
|
|
9
|
+
default: ({ block }: { block: { id: string } }) => (
|
|
10
|
+
<div data-testid="slot-child" data-id={block.id} />
|
|
11
|
+
)
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
import blockRendererRegistry from '../../blocks/block-renderer-registry';
|
|
15
|
+
import type { BlockRendererProps } from '../../blocks/block-renderer-registry';
|
|
16
|
+
import type { Block } from '../../theme-block';
|
|
17
|
+
import { resetCustomCatalog } from '../catalog';
|
|
18
|
+
import { resetCustomCollection } from '../collect';
|
|
19
|
+
import { defineCustomBlock } from '../define';
|
|
20
|
+
import { registerCustomBlocks } from '../register';
|
|
21
|
+
import { ThemeSlot } from '../theme-slot';
|
|
22
|
+
import { isCustomThemeType } from '../types';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* THE OTHER HALF OF THE CROSS-REPO SEAM.
|
|
26
|
+
*
|
|
27
|
+
* The catalog travels one way — this repo describes the block and the editor
|
|
28
|
+
* reads it, and `custom-catalog-wire.test.ts` over there pins that direction
|
|
29
|
+
* with a literal capture of what `getCustomCatalog()` posts. Slot nodes travel
|
|
30
|
+
* the OTHER way: the editor writes them into the composition and this repo
|
|
31
|
+
* reads them back. Nothing in this package's own tests could notice if the two
|
|
32
|
+
* sides disagreed about the node, because every other slot test here builds
|
|
33
|
+
* its composition from this repo's own constants.
|
|
34
|
+
*
|
|
35
|
+
* `EDITOR_COMPOSITION` below is therefore not hand-written either. It is the
|
|
36
|
+
* literal output of the editor's `ensureCustomSlotNodes()` — the function its
|
|
37
|
+
* two insert reducers call — captured from pz-dynamic-widget and pasted here
|
|
38
|
+
* unedited, for a block declaring the same three slots the catalog capture
|
|
39
|
+
* over there declares.
|
|
40
|
+
*
|
|
41
|
+
* TO REGENERATE, in `pz-dynamic-widget`: ingest a catalog through
|
|
42
|
+
* `parseCustomCatalogPayload` + `setCustomCatalog`, call
|
|
43
|
+
* `ensureCustomSlotNodes()` on a freshly dropped block of that type, and log
|
|
44
|
+
* the block. The ids are the only values that differ per run.
|
|
45
|
+
*
|
|
46
|
+
* Read as DATA, not through this repo's constants. `CUSTOM_SLOT_BLOCK_TYPE`
|
|
47
|
+
* and `SLOT_NAME_PROPERTY` are exported precisely so the editor can mirror
|
|
48
|
+
* them, and a test that spelled the fixture through those exports would follow
|
|
49
|
+
* a rename here and stay green while the editor went on writing the old
|
|
50
|
+
* spelling — which is the one failure this file exists to catch.
|
|
51
|
+
*/
|
|
52
|
+
const EDITOR_COMPOSITION = {
|
|
53
|
+
id: 'block-menu',
|
|
54
|
+
type: 'custom:acme:mega-menu',
|
|
55
|
+
label: 'Mega menu',
|
|
56
|
+
properties: {},
|
|
57
|
+
styles: {},
|
|
58
|
+
order: 0,
|
|
59
|
+
hidden: false,
|
|
60
|
+
blocks: [
|
|
61
|
+
{
|
|
62
|
+
id: 'block-l6rl9',
|
|
63
|
+
type: 'custom-slot',
|
|
64
|
+
label: 'Promo',
|
|
65
|
+
properties: { slotName: 'promo' },
|
|
66
|
+
styles: {},
|
|
67
|
+
order: 0,
|
|
68
|
+
hidden: false,
|
|
69
|
+
blocks: [
|
|
70
|
+
{
|
|
71
|
+
id: 'promo-child',
|
|
72
|
+
type: 'image',
|
|
73
|
+
label: 'Image',
|
|
74
|
+
properties: {},
|
|
75
|
+
styles: {},
|
|
76
|
+
order: 0,
|
|
77
|
+
hidden: false
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'block-ud5p6',
|
|
83
|
+
type: 'custom-slot',
|
|
84
|
+
label: 'Links',
|
|
85
|
+
properties: { slotName: 'links' },
|
|
86
|
+
styles: {},
|
|
87
|
+
order: 1,
|
|
88
|
+
hidden: false,
|
|
89
|
+
blocks: [
|
|
90
|
+
{
|
|
91
|
+
id: 'links-child',
|
|
92
|
+
type: 'text',
|
|
93
|
+
label: 'Text',
|
|
94
|
+
properties: {},
|
|
95
|
+
styles: {},
|
|
96
|
+
order: 0,
|
|
97
|
+
hidden: false
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'block-g4b81',
|
|
103
|
+
type: 'custom-slot',
|
|
104
|
+
label: 'Footnote',
|
|
105
|
+
properties: { slotName: '' },
|
|
106
|
+
styles: {},
|
|
107
|
+
order: 2,
|
|
108
|
+
hidden: false,
|
|
109
|
+
blocks: [
|
|
110
|
+
{
|
|
111
|
+
id: 'footnote-child',
|
|
112
|
+
type: 'text',
|
|
113
|
+
label: 'Text',
|
|
114
|
+
properties: {},
|
|
115
|
+
styles: {},
|
|
116
|
+
order: 0,
|
|
117
|
+
hidden: false
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
} as unknown as Block;
|
|
123
|
+
|
|
124
|
+
const TYPE = 'custom:acme:mega-menu';
|
|
125
|
+
|
|
126
|
+
const rendererMap = (): Map<string, unknown> =>
|
|
127
|
+
(blockRendererRegistry as unknown as { renderers: Map<string, unknown> })
|
|
128
|
+
.renderers;
|
|
129
|
+
|
|
130
|
+
const clearCustomRenderers = (): void => {
|
|
131
|
+
const map = rendererMap();
|
|
132
|
+
Array.from(map.keys())
|
|
133
|
+
.filter(isCustomThemeType)
|
|
134
|
+
.forEach((type) => map.delete(type));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Declares its three slots exactly as the captured catalog frame does. */
|
|
138
|
+
function MegaMenu() {
|
|
139
|
+
return (
|
|
140
|
+
<nav>
|
|
141
|
+
<section data-testid="promo-region">
|
|
142
|
+
<ThemeSlot name="promo" />
|
|
143
|
+
</section>
|
|
144
|
+
<section data-testid="links-region">
|
|
145
|
+
<ThemeSlot name="links" />
|
|
146
|
+
</section>
|
|
147
|
+
<section data-testid="footnote-region">
|
|
148
|
+
{/* The unnamed slot, spelled the way every pre-named-slots block does. */}
|
|
149
|
+
<ThemeSlot />
|
|
150
|
+
</section>
|
|
151
|
+
</nav>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const registerMenu = () =>
|
|
156
|
+
registerCustomBlocks('acme', {
|
|
157
|
+
'mega-menu': defineCustomBlock(MegaMenu, {
|
|
158
|
+
label: 'Mega menu',
|
|
159
|
+
slots: [
|
|
160
|
+
{ name: 'promo', label: 'Promo', accepts: ['image', 'text'], max: 2 },
|
|
161
|
+
{ name: 'links', label: 'Links' },
|
|
162
|
+
{ label: 'Footnote' }
|
|
163
|
+
]
|
|
164
|
+
})
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const renderComposition = (block: Block, isDesigner = false) => {
|
|
168
|
+
const Renderer = blockRendererRegistry.getRenderer(TYPE);
|
|
169
|
+
if (!Renderer) throw new Error(`No renderer registered for ${TYPE}`);
|
|
170
|
+
|
|
171
|
+
const props: BlockRendererProps = {
|
|
172
|
+
block,
|
|
173
|
+
placeholderId: 'header',
|
|
174
|
+
sectionId: 's1',
|
|
175
|
+
isDesigner
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return render(<Renderer {...props} />);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const childIdsIn = (regionTestId: string): string[] =>
|
|
182
|
+
Array.from(
|
|
183
|
+
screen
|
|
184
|
+
.getByTestId(regionTestId)
|
|
185
|
+
.querySelectorAll('[data-testid="slot-child"]')
|
|
186
|
+
).map((element) => element.getAttribute('data-id') ?? '');
|
|
187
|
+
|
|
188
|
+
describe('the slot nodes the editor actually writes', () => {
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
resetCustomCatalog();
|
|
191
|
+
resetCustomCollection();
|
|
192
|
+
clearCustomRenderers();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('each render into the ThemeSlot that names them, and no other', () => {
|
|
196
|
+
registerMenu();
|
|
197
|
+
renderComposition(EDITOR_COMPOSITION);
|
|
198
|
+
|
|
199
|
+
// The whole of gap 1 in one assertion: three regions filled
|
|
200
|
+
// independently, from three nodes, in one block. Before slot nodes every
|
|
201
|
+
// `ThemeSlot` read the same `block.blocks` array, so the second was
|
|
202
|
+
// refused outright.
|
|
203
|
+
expect(childIdsIn('promo-region')).toEqual(['promo-child']);
|
|
204
|
+
expect(childIdsIn('links-region')).toEqual(['links-child']);
|
|
205
|
+
expect(childIdsIn('footnote-region')).toEqual(['footnote-child']);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('are matched on the property the editor writes, not on the label', () => {
|
|
209
|
+
registerMenu();
|
|
210
|
+
|
|
211
|
+
// The merchant renames a slot row from the layer tree (`RENAME_BLOCK`),
|
|
212
|
+
// which is the one field of a node they can change. If the name rode on
|
|
213
|
+
// the label, this rename would silently detach the region from its
|
|
214
|
+
// `<ThemeSlot name="promo" />` and nothing would say why it went empty.
|
|
215
|
+
const renamed = JSON.parse(
|
|
216
|
+
JSON.stringify(EDITOR_COMPOSITION)
|
|
217
|
+
) as typeof EDITOR_COMPOSITION;
|
|
218
|
+
(renamed.blocks as Array<{ label: string }>)[0].label = 'Seasonal banner';
|
|
219
|
+
|
|
220
|
+
renderComposition(renamed as unknown as Block);
|
|
221
|
+
|
|
222
|
+
expect(childIdsIn('promo-region')).toEqual(['promo-child']);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('leave a region the editor has never opened empty rather than wrong', () => {
|
|
226
|
+
registerMenu();
|
|
227
|
+
|
|
228
|
+
// The editor materializes nodes at insert, so a composition can hold some
|
|
229
|
+
// and not others — a block saved against an older build of the brand's
|
|
230
|
+
// component, or a slot added to the component after the merchant placed
|
|
231
|
+
// it. The missing region must draw nothing, and must NOT fall back to
|
|
232
|
+
// another slot's content.
|
|
233
|
+
const partial = JSON.parse(
|
|
234
|
+
JSON.stringify(EDITOR_COMPOSITION)
|
|
235
|
+
) as typeof EDITOR_COMPOSITION;
|
|
236
|
+
partial.blocks = [partial.blocks[0]];
|
|
237
|
+
|
|
238
|
+
renderComposition(partial as unknown as Block);
|
|
239
|
+
|
|
240
|
+
expect(childIdsIn('promo-region')).toEqual(['promo-child']);
|
|
241
|
+
expect(childIdsIn('links-region')).toEqual([]);
|
|
242
|
+
expect(childIdsIn('footnote-region')).toEqual([]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('do not leak into the unnamed slot of a half-migrated composition', () => {
|
|
246
|
+
registerMenu();
|
|
247
|
+
|
|
248
|
+
// Every composition saved before slots were nodes put the merchant's
|
|
249
|
+
// blocks straight into `block.blocks`, and the unnamed slot falls back to
|
|
250
|
+
// them — that fallback is what keeps published content rendering. It has
|
|
251
|
+
// to skip slot NODES, or a block carrying both shapes would show the named
|
|
252
|
+
// regions' content in the unnamed one as well.
|
|
253
|
+
const mixed = JSON.parse(
|
|
254
|
+
JSON.stringify(EDITOR_COMPOSITION)
|
|
255
|
+
) as typeof EDITOR_COMPOSITION;
|
|
256
|
+
mixed.blocks = [
|
|
257
|
+
mixed.blocks[0],
|
|
258
|
+
mixed.blocks[1],
|
|
259
|
+
{
|
|
260
|
+
id: 'legacy-child',
|
|
261
|
+
type: 'text',
|
|
262
|
+
label: 'Text',
|
|
263
|
+
properties: {},
|
|
264
|
+
styles: {},
|
|
265
|
+
order: 2,
|
|
266
|
+
hidden: false
|
|
267
|
+
} as unknown as typeof mixed.blocks[number]
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
renderComposition(mixed as unknown as Block);
|
|
271
|
+
|
|
272
|
+
expect(childIdsIn('promo-region')).toEqual(['promo-child']);
|
|
273
|
+
expect(childIdsIn('footnote-region')).toEqual(['legacy-child']);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('stop reading stray children the moment the unnamed NODE exists beside them', () => {
|
|
277
|
+
registerMenu();
|
|
278
|
+
|
|
279
|
+
// THE REASON THE EDITOR HAS TO MOVE THEM, pinned from the renderer's side.
|
|
280
|
+
//
|
|
281
|
+
// The legacy fallback above is a fallback: the node wins the lookup, so a
|
|
282
|
+
// block carrying BOTH shapes draws the node's children and nothing else.
|
|
283
|
+
// That is the right rule — two answers to one `<ThemeSlot/>` would render
|
|
284
|
+
// the region twice — but it means appending an unnamed node beside authored
|
|
285
|
+
// children silently takes them off the page while leaving them saved and
|
|
286
|
+
// listed in the layer tree. The editor's `ensureCustomSlotNodes` therefore
|
|
287
|
+
// adopts them into the node rather than appending beside them, and this is
|
|
288
|
+
// what says why.
|
|
289
|
+
const beside = JSON.parse(
|
|
290
|
+
JSON.stringify(EDITOR_COMPOSITION)
|
|
291
|
+
) as typeof EDITOR_COMPOSITION;
|
|
292
|
+
beside.blocks = [
|
|
293
|
+
{
|
|
294
|
+
id: 'legacy-child',
|
|
295
|
+
type: 'text',
|
|
296
|
+
label: 'Text',
|
|
297
|
+
properties: {},
|
|
298
|
+
styles: {},
|
|
299
|
+
order: 0,
|
|
300
|
+
hidden: false
|
|
301
|
+
} as unknown as typeof beside.blocks[number],
|
|
302
|
+
beside.blocks[2]
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
renderComposition(beside as unknown as Block);
|
|
306
|
+
|
|
307
|
+
expect(childIdsIn('footnote-region')).toEqual(['footnote-child']);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { Block } from '../../theme-block';
|
|
2
|
+
import {
|
|
3
|
+
isSlotNode,
|
|
4
|
+
resolveSlotChildren,
|
|
5
|
+
resolveUnnamedSlot,
|
|
6
|
+
slotNameError,
|
|
7
|
+
slotNodeName
|
|
8
|
+
} from '../slot-nodes';
|
|
9
|
+
import { CUSTOM_SLOT_BLOCK_TYPE, SLOT_NAME_PROPERTY } from '../types';
|
|
10
|
+
|
|
11
|
+
const block = (over: Partial<Block> = {}): Block => ({
|
|
12
|
+
id: 'b1',
|
|
13
|
+
type: 'text',
|
|
14
|
+
label: 'b1',
|
|
15
|
+
properties: {},
|
|
16
|
+
styles: {},
|
|
17
|
+
order: 0,
|
|
18
|
+
hidden: false,
|
|
19
|
+
...over
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const slotNode = (properties: Record<string, unknown>, blocks: Block[] = []) =>
|
|
23
|
+
block({
|
|
24
|
+
id: 'slot',
|
|
25
|
+
type: CUSTOM_SLOT_BLOCK_TYPE,
|
|
26
|
+
properties,
|
|
27
|
+
blocks
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('slotNodeName', () => {
|
|
31
|
+
it('reads the bare string the editor stamps', () => {
|
|
32
|
+
expect(slotNodeName(slotNode({ [SLOT_NAME_PROPERTY]: 'promo' }))).toBe(
|
|
33
|
+
'promo'
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('reads a per-breakpoint bucket as well', () => {
|
|
38
|
+
// Every other value in `block.properties` is per-breakpoint, and the panel
|
|
39
|
+
// re-buckets whatever it is given. A bare-string-only reader would detach
|
|
40
|
+
// every named slot in every published composition the day anything routed
|
|
41
|
+
// this key through a control.
|
|
42
|
+
expect(
|
|
43
|
+
slotNodeName(slotNode({ [SLOT_NAME_PROPERTY]: { desktop: 'promo' } }))
|
|
44
|
+
).toBe('promo');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('treats a node with no name as the unnamed slot', () => {
|
|
48
|
+
expect(slotNodeName(slotNode({}))).toBe('');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('resolveSlotChildren', () => {
|
|
53
|
+
const inPromo = block({ id: 'in-promo' });
|
|
54
|
+
const inAside = block({ id: 'in-aside' });
|
|
55
|
+
|
|
56
|
+
it('returns the children of the node with that name', () => {
|
|
57
|
+
const host = block({
|
|
58
|
+
blocks: [
|
|
59
|
+
slotNode({ [SLOT_NAME_PROPERTY]: 'promo' }, [inPromo]),
|
|
60
|
+
slotNode({ [SLOT_NAME_PROPERTY]: 'aside' }, [inAside])
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(resolveSlotChildren(host, 'promo')).toEqual([inPromo]);
|
|
65
|
+
expect(resolveSlotChildren(host, 'aside')).toEqual([inAside]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('separates an emptied slot from one that was never opened', () => {
|
|
69
|
+
const host = block({
|
|
70
|
+
blocks: [slotNode({ [SLOT_NAME_PROPERTY]: 'promo' }, [])]
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Both render nothing, so the distinction costs shoppers nothing — it
|
|
74
|
+
// exists so the designer can say which is which.
|
|
75
|
+
expect(resolveSlotChildren(host, 'promo')).toEqual([]);
|
|
76
|
+
expect(resolveSlotChildren(host, 'footer')).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('gives the unnamed slot the children of a composition with no nodes', () => {
|
|
80
|
+
const legacy = block({ blocks: [inPromo, inAside] });
|
|
81
|
+
|
|
82
|
+
expect(resolveSlotChildren(legacy, '')).toEqual([inPromo, inAside]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('keeps another slot node out of the unnamed slot', () => {
|
|
86
|
+
const halfMigrated = block({
|
|
87
|
+
blocks: [slotNode({ [SLOT_NAME_PROPERTY]: 'promo' }, [inPromo]), inAside]
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(resolveSlotChildren(halfMigrated, '')).toEqual([inAside]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('never falls back for a named slot', () => {
|
|
94
|
+
// A named slot cannot have legacy content to find: naming one was refused
|
|
95
|
+
// at define time before slots were nodes.
|
|
96
|
+
expect(resolveSlotChildren(block({ blocks: [inPromo] }), 'promo')).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('resolveUnnamedSlot', () => {
|
|
101
|
+
it('is the unnamed slot when the block declares nothing', () => {
|
|
102
|
+
expect(resolveUnnamedSlot([])).toEqual({ name: '' });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('is the single declared slot whatever it is called', () => {
|
|
106
|
+
expect(resolveUnnamedSlot(['promo'])).toEqual({ name: 'promo' });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('is the unnamed slot when one is declared beside named ones', () => {
|
|
110
|
+
expect(resolveUnnamedSlot(['', 'promo'])).toEqual({ name: '' });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('refuses several named slots and names them all', () => {
|
|
114
|
+
const resolved = resolveUnnamedSlot(['promo', 'aside']) as {
|
|
115
|
+
error: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
expect(resolved.error).toContain('<ThemeSlot name="promo" />');
|
|
119
|
+
expect(resolved.error).toContain('<ThemeSlot name="aside" />');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('slotNameError', () => {
|
|
124
|
+
it('accepts the empty name and a hyphenated one', () => {
|
|
125
|
+
expect(slotNameError('')).toBeNull();
|
|
126
|
+
expect(slotNameError('promo-content')).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('refuses anything that would not match a node literally', () => {
|
|
130
|
+
expect(slotNameError('Promo Content')).toContain('is not valid');
|
|
131
|
+
expect(slotNameError('1promo')).toContain('is not valid');
|
|
132
|
+
expect(slotNameError(undefined)).toContain('is not valid');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('isSlotNode', () => {
|
|
137
|
+
it('recognises the reserved type and nothing else', () => {
|
|
138
|
+
expect(isSlotNode(slotNode({}))).toBe(true);
|
|
139
|
+
expect(isSlotNode(block({ type: 'group' }))).toBe(false);
|
|
140
|
+
// The slot type deliberately sits outside the `custom:<vendor>:<name>`
|
|
141
|
+
// grammar: nothing renders it, so it is not a brand block type.
|
|
142
|
+
expect(isSlotNode(block({ type: 'custom:acme:promo-strip' }))).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
});
|