@akinon/pz-theme 2.0.96-beta.0 → 2.0.96

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 CHANGED
@@ -1,12 +1,12 @@
1
1
  # @akinon/pz-theme
2
2
 
3
- ## 2.0.96-beta.0
3
+ ## 2.0.96
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - Updated dependencies [cbbbfd757]
8
- - @akinon/next@2.0.96-beta.0
9
- - @akinon/theme-contract@2.0.96-beta.0
7
+ - 956fe4ea: ZERO-5007: Write renderers for the block types the picker offers but pz-theme cannot draw
8
+ - @akinon/next@2.0.96
9
+ - @akinon/theme-contract@2.0.96
10
10
 
11
11
  ## 2.0.95
12
12
 
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@akinon/pz-theme",
3
3
  "description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
4
- "version": "2.0.96-beta.0",
4
+ "version": "2.0.96",
5
5
  "license": "MIT",
6
6
  "main": "src/index.ts",
7
7
  "scripts": {
8
8
  "test": "jest"
9
9
  },
10
10
  "peerDependencies": {
11
- "@akinon/next": "2.0.96-beta.0",
11
+ "@akinon/next": "2.0.96",
12
12
  "react": "^18.0.0 || ^19.0.0",
13
13
  "react-dom": "^18.0.0 || ^19.0.0"
14
14
  },
15
15
  "dependencies": {
16
- "@akinon/theme-contract": "2.0.96-beta.0",
16
+ "@akinon/theme-contract": "2.0.96",
17
17
  "clsx": "2.1.1",
18
18
  "lottie-web": "^5.13.0",
19
19
  "react-multi-carousel": "2.8.4",
@@ -21,7 +21,7 @@
21
21
  "zod": "4.4.1"
22
22
  },
23
23
  "devDependencies": {
24
- "@akinon/next": "2.0.96-beta.0",
24
+ "@akinon/next": "2.0.96",
25
25
  "@testing-library/jest-dom": "5.16.4",
26
26
  "@testing-library/react": "16.3.2",
27
27
  "@testing-library/user-event": "14.1.1",
@@ -34,28 +34,24 @@ const EXPECTED_BLOCK_TYPES = [
34
34
  'applied-filters',
35
35
  'auth-consent',
36
36
  'auth-error',
37
- 'auth-flow',
38
37
  'auth-field',
38
+ 'auth-flow',
39
39
  'auth-form',
40
40
  'auth-step',
41
41
  'auth-step-title',
42
- // The confirmation as a placeable block, the success twin of 'auth-error':
43
- // the two forms that have a success state publish their message and this
44
- // draws it where the design put it. See `auth/form-success-context.tsx`.
45
42
  'auth-success',
46
43
  'basket-items',
47
44
  'basket-summary',
48
45
  'button',
49
- // The security check commerce escalates to, placed inside an auth form.
50
46
  'captcha',
51
- // The screen the password-reset mail lands on.
52
- 'reset-password',
47
+ 'carousel-item',
53
48
  'change-password-form',
54
49
  'checkout-flow-outlet',
55
50
  'checkout-steps',
56
51
  'checkout-steps-outlet',
57
52
  'checkout-summary',
58
53
  'checkout-summary-outlet',
54
+ 'container',
59
55
  'countdown',
60
56
  'counter',
61
57
  'coupon-list',
@@ -68,14 +64,15 @@ const EXPECTED_BLOCK_TYPES = [
68
64
  'favorite-button',
69
65
  'favourites-grid',
70
66
  'forgot-password',
71
- // One screen of a composed form — the confirmation, the refusal, the busy
72
- // moment — as a container the form draws when it is on that screen. See
73
- // `auth/form-state-context.tsx`.
74
67
  'form-state',
75
68
  'group',
76
69
  'hamburger-menu',
77
70
  'hotspot',
71
+ 'hover-showcase-item',
78
72
  'icon',
73
+ 'icon-button',
74
+ 'icon-group',
75
+ 'icon-group-colored',
79
76
  'image',
80
77
  'image-gallery',
81
78
  'input',
@@ -111,19 +108,22 @@ const EXPECTED_BLOCK_TYPES = [
111
108
  'product-title',
112
109
  'profile-form',
113
110
  'register-form',
111
+ 'reset-password',
114
112
  'search-box',
115
113
  'share',
116
114
  'size-guide',
117
115
  'slider',
116
+ 'social-icons-list',
118
117
  'social-links',
119
118
  'sort-select',
120
119
  'stock-status',
121
120
  'store-locator',
122
121
  'tab',
123
122
  'text',
123
+ 'value-props-list',
124
124
  'variant-selector',
125
125
  'video',
126
- 'wishlist-button'
126
+ 'wishlist-button',
127
127
  ];
128
128
 
129
129
  const EXPECTED_SECTION_TYPES = [
@@ -0,0 +1,346 @@
1
+ import React from 'react';
2
+ import { fireEvent, render } from '@testing-library/react';
3
+
4
+ // Stub the recursive child renderer so the slide's children are countable
5
+ // without pulling in every block type.
6
+ jest.mock('../../theme-block', () => ({
7
+ __esModule: true,
8
+ default: ({ block }: { block: { id: string } }) => (
9
+ <div data-testid="tb" data-id={block.id} />
10
+ )
11
+ }));
12
+
13
+ import CarouselItemBlock from '../carousel-item-block';
14
+
15
+ /**
16
+ * The block exactly as `getDefaultCarouselItemBlockValue()` seeds it in
17
+ * pz-dynamic-widget: both properties bucketed per breakpoint, `url` seeded to
18
+ * `#`, and one image child.
19
+ */
20
+ const seededSlide = (over: Record<string, unknown> = {}) => ({
21
+ id: 'slide',
22
+ type: 'carousel-item',
23
+ label: 'Carousel Item',
24
+ value: '',
25
+ properties: {
26
+ url: { desktop: '#', mobile: '#' },
27
+ target: { desktop: '_self', mobile: '_self' }
28
+ },
29
+ styles: {},
30
+ blocks: [
31
+ {
32
+ id: 'img',
33
+ type: 'image',
34
+ label: 'Carousel Image',
35
+ value: '',
36
+ properties: {
37
+ alt: { desktop: 'Carousel Image', mobile: 'Carousel Image' },
38
+ loadingStrategy: 'eager',
39
+ priority: 'high'
40
+ },
41
+ styles: {
42
+ width: { desktop: '100%', mobile: '100%' },
43
+ height: { desktop: '400px', mobile: '300px' },
44
+ 'object-fit': { desktop: 'cover', mobile: 'cover' }
45
+ }
46
+ }
47
+ ],
48
+ ...over
49
+ });
50
+
51
+ const props = (over: Record<string, unknown> = {}) =>
52
+ ({
53
+ block: seededSlide(),
54
+ placeholderId: 'p',
55
+ sectionId: 's',
56
+ isDesigner: false,
57
+ currentBreakpoint: 'desktop',
58
+ ...over
59
+ } as any);
60
+
61
+ const anchor = (container: HTMLElement) =>
62
+ container.querySelector('a') as HTMLAnchorElement | null;
63
+
64
+ const markers = (container: HTMLElement) =>
65
+ Array.from(container.querySelectorAll('[data-testid="tb"]'));
66
+
67
+ describe('CarouselItemBlock — the stored slide', () => {
68
+ it('draws the seeded image child', () => {
69
+ const { container } = render(<CarouselItemBlock {...props()} />);
70
+ expect(markers(container)).toHaveLength(1);
71
+ expect(markers(container)[0]).toHaveAttribute('data-id', 'img');
72
+ });
73
+
74
+ it('draws NO anchor for the seeded "#" url — a fresh slide is not a link', () => {
75
+ const { container } = render(<CarouselItemBlock {...props()} />);
76
+ expect(anchor(container)).toBeNull();
77
+ });
78
+
79
+ it('draws no anchor for an empty or whitespace url either', () => {
80
+ const block = seededSlide({ properties: { url: ' ' } });
81
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
82
+ expect(anchor(container)).toBeNull();
83
+ expect(markers(container)).toHaveLength(1);
84
+ });
85
+
86
+ it('wraps the slide in an anchor once a real url is authored', () => {
87
+ const block = seededSlide({
88
+ properties: { url: '/campaigns/summer-sale', target: '_self' }
89
+ });
90
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
91
+ expect(anchor(container)).toHaveAttribute('href', '/campaigns/summer-sale');
92
+ expect(markers(container)).toHaveLength(1);
93
+ });
94
+
95
+ it('treats a real in-page target as a destination (only the bare hash is the seed)', () => {
96
+ const block = seededSlide({ properties: { url: '#size-guide' } });
97
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
98
+ expect(anchor(container)).toHaveAttribute('href', '#size-guide');
99
+ });
100
+ });
101
+
102
+ describe('CarouselItemBlock — per-breakpoint properties', () => {
103
+ it('reads the mobile url from the bucketed value the descriptor seeds', () => {
104
+ const block = seededSlide({
105
+ properties: {
106
+ url: { desktop: '/desktop-campaign', mobile: '/mobile-campaign' }
107
+ }
108
+ });
109
+ const { container } = render(
110
+ <CarouselItemBlock {...props({ block, currentBreakpoint: 'mobile' })} />
111
+ );
112
+ expect(anchor(container)).toHaveAttribute('href', '/mobile-campaign');
113
+ });
114
+
115
+ it('reads the desktop url on the desktop breakpoint', () => {
116
+ const block = seededSlide({
117
+ properties: {
118
+ url: { desktop: '/desktop-campaign', mobile: '/mobile-campaign' }
119
+ }
120
+ });
121
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
122
+ expect(anchor(container)).toHaveAttribute('href', '/desktop-campaign');
123
+ });
124
+
125
+ it('opens a bucketed mobile-only "_blank" in a new tab', () => {
126
+ const block = seededSlide({
127
+ properties: {
128
+ url: '/campaigns/lookbook',
129
+ target: { desktop: '_self', mobile: '_blank' }
130
+ }
131
+ });
132
+ const { container } = render(
133
+ <CarouselItemBlock {...props({ block, currentBreakpoint: 'mobile' })} />
134
+ );
135
+ expect(anchor(container)).toHaveAttribute('target', '_blank');
136
+ });
137
+ });
138
+
139
+ describe('CarouselItemBlock — rel', () => {
140
+ it('adds the security pair on _blank', () => {
141
+ const block = seededSlide({
142
+ properties: { url: 'https://example.com/lookbook', target: '_blank' }
143
+ });
144
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
145
+ expect(anchor(container)).toHaveAttribute('rel', 'noopener noreferrer');
146
+ });
147
+
148
+ it('emits no rel at all on _self', () => {
149
+ const block = seededSlide({
150
+ properties: { url: '/campaigns/summer-sale', target: '_self' }
151
+ });
152
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
153
+ expect(anchor(container)?.hasAttribute('rel')).toBe(false);
154
+ });
155
+
156
+ /**
157
+ * The descriptor declares `url` and `target` and nothing else, so a `rel`
158
+ * key can only be junk left by a hand-edited composition. Reading it (the way
159
+ * the link block reads its own declared one) would let that junk reach the
160
+ * anchor.
161
+ */
162
+ it('ignores a rel property, which this block type does not declare', () => {
163
+ const block = seededSlide({
164
+ properties: {
165
+ url: '/campaigns/summer-sale',
166
+ target: '_self',
167
+ rel: 'me nofollow'
168
+ }
169
+ });
170
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
171
+ expect(anchor(container)?.hasAttribute('rel')).toBe(false);
172
+ });
173
+
174
+ it('falls back to _self when the stored target is not one of the two options', () => {
175
+ const block = seededSlide({
176
+ properties: { url: '/campaigns/summer-sale', target: 'new-window' }
177
+ });
178
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
179
+ expect(anchor(container)).toHaveAttribute('target', '_self');
180
+ expect(anchor(container)?.hasAttribute('rel')).toBe(false);
181
+ });
182
+ });
183
+
184
+ describe('CarouselItemBlock — merchant input on its way to href', () => {
185
+ it.each([
186
+ ['javascript:alert(1)'],
187
+ ['JavaScript:alert(1)'],
188
+ ['java\tscript:alert(1)'],
189
+ ['vbscript:msgbox(1)'],
190
+ ['data:text/html,<script>alert(1)</script>']
191
+ ])('never turns %s into a link', (url) => {
192
+ const block = seededSlide({ properties: { url } });
193
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
194
+ expect(anchor(container)).toBeNull();
195
+ // The slide itself is still drawn — a bad url costs the link, not the art.
196
+ expect(markers(container)).toHaveLength(1);
197
+ });
198
+ });
199
+
200
+ describe('CarouselItemBlock — who owns the click', () => {
201
+ /**
202
+ * The wrapper fires an authored action and cancels this navigation on the
203
+ * way up (`with-designer-features.tsx` + `use-block-action.ts`), which only
204
+ * works while the renderer leaves the live click untouched. A
205
+ * `preventDefault` or `stopPropagation` here would either kill every slide
206
+ * link or hide the click from the central action wiring.
207
+ */
208
+ it('leaves a live click for the wrapper: not prevented, still bubbling', () => {
209
+ const seen: MouseEvent[] = [];
210
+ const block = seededSlide({
211
+ properties: { url: '/campaigns/summer-sale' }
212
+ });
213
+ const { container } = render(
214
+ <div onClick={(event) => seen.push(event.nativeEvent)}>
215
+ <CarouselItemBlock {...props({ block })} />
216
+ </div>
217
+ );
218
+
219
+ fireEvent.click(anchor(container) as HTMLAnchorElement);
220
+
221
+ expect(seen).toHaveLength(1);
222
+ expect(seen[0].defaultPrevented).toBe(false);
223
+ });
224
+
225
+ it('neutralises the link on the canvas but still lets the click reach the wrapper', () => {
226
+ const seen: MouseEvent[] = [];
227
+ const block = seededSlide({
228
+ properties: { url: '/campaigns/summer-sale', target: '_blank' }
229
+ });
230
+ const { container } = render(
231
+ <div onClick={(event) => seen.push(event.nativeEvent)}>
232
+ <CarouselItemBlock {...props({ block, isDesigner: true })} />
233
+ </div>
234
+ );
235
+
236
+ const link = anchor(container) as HTMLAnchorElement;
237
+ expect(link).toHaveAttribute('href', '#');
238
+ // A _blank slide would otherwise open the campaign in a second tab from
239
+ // inside the editor.
240
+ expect(link.hasAttribute('target')).toBe(false);
241
+
242
+ fireEvent.click(link);
243
+
244
+ expect(seen).toHaveLength(1);
245
+ expect(seen[0].defaultPrevented).toBe(true);
246
+ });
247
+ });
248
+
249
+ describe('CarouselItemBlock — layout ownership', () => {
250
+ /**
251
+ * `collectBlockCSS` emits the merchant's styles onto the `[data-block-id]`
252
+ * wrapper. Inlining them here would apply them twice and outrank the media
253
+ * queries the same sheet emits.
254
+ */
255
+ it('inlines none of the authored styles', () => {
256
+ const block = seededSlide({
257
+ properties: { url: '/campaigns/summer-sale' },
258
+ styles: {
259
+ display: { desktop: 'grid', mobile: 'block' },
260
+ gap: { desktop: '24px' },
261
+ 'background-color': { desktop: '#111111' }
262
+ }
263
+ });
264
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
265
+ expect(anchor(container)?.hasAttribute('style')).toBe(false);
266
+ });
267
+
268
+ it('gives the anchor display:contents so the wrapper layout reaches the children', () => {
269
+ const block = seededSlide({
270
+ properties: { url: '/campaigns/summer-sale' }
271
+ });
272
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
273
+ expect(anchor(container)).toHaveClass('contents');
274
+ });
275
+
276
+ it('stamps no part attributes — the descriptor declares none', () => {
277
+ const block = seededSlide({
278
+ properties: { url: '/campaigns/summer-sale' }
279
+ });
280
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
281
+ expect(container.querySelector('[data-part]')).toBeNull();
282
+ });
283
+ });
284
+
285
+ describe('CarouselItemBlock — children', () => {
286
+ it('drops hidden children on the storefront and keeps them on the canvas', () => {
287
+ const block = seededSlide({
288
+ properties: { url: '/campaigns/summer-sale' },
289
+ blocks: [
290
+ { id: 'img', type: 'image', label: 'img', order: 1 },
291
+ {
292
+ id: 'caption',
293
+ type: 'text',
294
+ label: 'caption',
295
+ order: 0,
296
+ hidden: true
297
+ }
298
+ ]
299
+ });
300
+
301
+ const live = render(<CarouselItemBlock {...props({ block })} />);
302
+ expect(
303
+ markers(live.container).map((m) => m.getAttribute('data-id'))
304
+ ).toEqual(['img']);
305
+
306
+ const canvas = render(
307
+ <CarouselItemBlock {...props({ block, isDesigner: true })} />
308
+ );
309
+ expect(
310
+ markers(canvas.container).map((m) => m.getAttribute('data-id'))
311
+ ).toEqual(['caption', 'img']);
312
+ });
313
+ });
314
+
315
+ describe('CarouselItemBlock — empty', () => {
316
+ it('shows the designer placeholder when the slide has no blocks', () => {
317
+ const block = seededSlide({ blocks: [] });
318
+ const { container } = render(
319
+ <CarouselItemBlock {...props({ block, isDesigner: true })} />
320
+ );
321
+ expect(container.textContent).toContain('Carousel item (drop blocks here)');
322
+ expect(
323
+ container.querySelector('[data-carousel-item-placeholder]')
324
+ ).toBeInTheDocument();
325
+ });
326
+
327
+ it('renders nothing at all on the storefront when the slide is empty', () => {
328
+ const block = seededSlide({ blocks: [] });
329
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
330
+ expect(container).toBeEmptyDOMElement();
331
+ });
332
+
333
+ /**
334
+ * A slide whose only child is hidden is empty on the storefront. Drawing the
335
+ * anchor anyway would leave an empty link in the carousel track — a focus
336
+ * stop on nothing, still counted by the dots.
337
+ */
338
+ it('draws no empty anchor when every child is hidden', () => {
339
+ const block = seededSlide({
340
+ properties: { url: '/campaigns/summer-sale' },
341
+ blocks: [{ id: 'img', type: 'image', label: 'img', hidden: true }]
342
+ });
343
+ const { container } = render(<CarouselItemBlock {...props({ block })} />);
344
+ expect(container).toBeEmptyDOMElement();
345
+ });
346
+ });
@@ -0,0 +1,149 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+
4
+ // Stub the recursive ThemeBlock (default export) with a countable marker, the
5
+ // way group-block.test.tsx does — a container's whole job is which children it
6
+ // hands to ThemeBlock, not what those children draw.
7
+ jest.mock('../../theme-block', () => ({
8
+ __esModule: true,
9
+ default: ({ block }: { block: { id: string } }) => (
10
+ <div data-testid="tb" data-id={block.id} />
11
+ )
12
+ }));
13
+
14
+ import ContainerBlock from '../container-block';
15
+
16
+ const child = (id: string, order = 0, hidden = false) => ({
17
+ id,
18
+ type: 'text',
19
+ label: id,
20
+ value: '',
21
+ properties: {},
22
+ styles: {},
23
+ order,
24
+ hidden
25
+ });
26
+
27
+ /**
28
+ * A container as the editor actually stores one: no properties at all, and the
29
+ * curated box styles the descriptor seeds through `getDefaultValue`, bucketed
30
+ * per breakpoint with a couple of authored values on top.
31
+ */
32
+ const storedBlock = (over: Record<string, unknown> = {}) => ({
33
+ id: 'blk-top-row',
34
+ type: 'container',
35
+ label: 'Top row',
36
+ value: '',
37
+ properties: {},
38
+ styles: {
39
+ 'background-color': { desktop: '#111827', mobile: '#000000' },
40
+ 'padding-top': { desktop: '16px' },
41
+ 'padding-bottom': { desktop: '16px' },
42
+ 'padding-left': { desktop: '24px' },
43
+ 'padding-right': { desktop: '24px' },
44
+ 'border-width': { desktop: '1px' },
45
+ 'border-color': { desktop: '#e5e7eb' },
46
+ 'border-radius': { desktop: '8px' }
47
+ },
48
+ order: 0,
49
+ hidden: false,
50
+ blocks: [child('logo', 0), child('search', 1), child('basket', 2)],
51
+ ...over
52
+ });
53
+
54
+ const props = (over: Record<string, unknown> = {}) =>
55
+ ({
56
+ block: storedBlock(),
57
+ placeholderId: 'header',
58
+ sectionId: 'sec-1',
59
+ isDesigner: false,
60
+ ...over
61
+ } as any);
62
+
63
+ const markers = (host: HTMLElement) =>
64
+ Array.from(host.querySelectorAll('[data-testid="tb"]'));
65
+
66
+ describe('ContainerBlock — children', () => {
67
+ it('draws the children of a realistic stored container, in order', () => {
68
+ const { container } = render(<ContainerBlock {...props()} />);
69
+ expect(markers(container).map((m) => m.getAttribute('data-id'))).toEqual([
70
+ 'logo',
71
+ 'search',
72
+ 'basket'
73
+ ]);
74
+ });
75
+
76
+ it('drops hidden children on the storefront and keeps them on the canvas', () => {
77
+ const block = storedBlock({
78
+ blocks: [child('logo', 0), child('promo', 1, true)]
79
+ });
80
+
81
+ const live = render(<ContainerBlock {...props({ block })} />);
82
+ expect(markers(live.container)).toHaveLength(1);
83
+
84
+ const canvas = render(
85
+ <ContainerBlock {...props({ block, isDesigner: true })} />
86
+ );
87
+ expect(markers(canvas.container)).toHaveLength(2);
88
+ });
89
+ });
90
+
91
+ describe('ContainerBlock — empty', () => {
92
+ it('shows the designer placeholder when there is nothing inside', () => {
93
+ const block = storedBlock({ blocks: [] });
94
+ const { container } = render(
95
+ <ContainerBlock {...props({ block, isDesigner: true })} />
96
+ );
97
+ expect(container.textContent).toContain('Container (drop blocks here)');
98
+ expect(
99
+ container.querySelector('[data-container-placeholder]')
100
+ ).toBeInTheDocument();
101
+ });
102
+
103
+ it('shows nothing to a shopper when there is nothing inside', () => {
104
+ const block = storedBlock({ blocks: [] });
105
+ const { container } = render(<ContainerBlock {...props({ block })} />);
106
+ expect(container).toBeEmptyDOMElement();
107
+ });
108
+
109
+ it('shows nothing to a shopper when every child is hidden', () => {
110
+ const block = storedBlock({ blocks: [child('promo', 0, true)] });
111
+ const { container } = render(<ContainerBlock {...props({ block })} />);
112
+ expect(container).toBeEmptyDOMElement();
113
+ });
114
+
115
+ it('survives a container stored without a blocks array at all', () => {
116
+ const block = storedBlock({ blocks: undefined });
117
+ const { container } = render(<ContainerBlock {...props({ block })} />);
118
+ expect(container).toBeEmptyDOMElement();
119
+ });
120
+ });
121
+
122
+ describe('ContainerBlock — contract: it is not a box', () => {
123
+ /*
124
+ * The reason the type exists is that the merchant's background and padding
125
+ * paint on the `[data-block-id]` wrapper. A box added here would be a second
126
+ * box inside that one and would swallow an authored `display: flex` — every
127
+ * child would become one flex item. These two pin that it draws none.
128
+ */
129
+ it('adds no element of its own — the children are the wrapper content', () => {
130
+ const { container } = render(<ContainerBlock {...props()} />);
131
+ expect(Array.from(container.children)).toEqual(markers(container));
132
+ });
133
+
134
+ it('inlines nothing from block.styles, authored keys and junk keys alike', () => {
135
+ const block = storedBlock({
136
+ styles: {
137
+ ...storedBlock().styles,
138
+ display: { desktop: 'flex' },
139
+ gap: { desktop: '32px' },
140
+ // A descriptor's made-up key: the CSS generator has no whitelist, so a
141
+ // renderer reaching into styles would happily inline this too.
142
+ 'container-title-font-size': { desktop: '18px' }
143
+ }
144
+ });
145
+ const { container } = render(<ContainerBlock {...props({ block })} />);
146
+ expect(container.querySelector('[style]')).toBeNull();
147
+ expect(container.querySelector('[class]')).toBeNull();
148
+ });
149
+ });