@akinon/pz-theme 2.0.96-rc.0 → 2.0.97-beta.0
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 +15 -33
- package/package.json +4 -4
- package/src/__tests__/registry-parity.test.ts +11 -11
- package/src/blocks/__tests__/carousel-item-block.test.tsx +346 -0
- package/src/blocks/__tests__/container-block.test.tsx +149 -0
- package/src/blocks/__tests__/hover-showcase-item-block.test.tsx +315 -0
- package/src/blocks/__tests__/icon-button-block.test.tsx +231 -0
- package/src/blocks/__tests__/icon-group-block.test.tsx +186 -0
- package/src/blocks/__tests__/icon-source.test.tsx +226 -0
- package/src/blocks/__tests__/responsive-array.test.ts +77 -0
- package/src/blocks/__tests__/social-glyphs.test.tsx +108 -0
- package/src/blocks/__tests__/social-icons-list-block.test.tsx +243 -0
- package/src/blocks/__tests__/value-props-list-block.test.tsx +231 -0
- package/src/blocks/block-renderer-registry.tsx +21 -0
- package/src/blocks/carousel-item-block.tsx +168 -0
- package/src/blocks/container-block.tsx +80 -0
- package/src/blocks/hover-showcase-item-block.tsx +232 -0
- package/src/blocks/icon-block.tsx +82 -121
- package/src/blocks/icon-button-block.tsx +180 -0
- package/src/blocks/icon-group-block.tsx +119 -0
- package/src/blocks/icon-group-colored-block.tsx +47 -0
- package/src/blocks/icon-source.tsx +311 -0
- package/src/blocks/responsive-array.ts +54 -0
- package/src/blocks/social-glyphs.tsx +131 -0
- package/src/blocks/social-icons-list-block.tsx +159 -0
- package/src/blocks/social-links-block.tsx +1 -125
- package/src/blocks/value-props-list-block.tsx +152 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
// The recursive ThemeBlock is stubbed with a countable marker, the way
|
|
5
|
+
// group-block.test.tsx does it. `hover-showcase-section` renders its children
|
|
6
|
+
// through the same module, so one mock serves both halves of the parity test.
|
|
7
|
+
jest.mock('../../theme-block', () => ({
|
|
8
|
+
__esModule: true,
|
|
9
|
+
default: ({ block }: { block: { id: string } }) => (
|
|
10
|
+
<div data-testid="child" data-id={block.id} />
|
|
11
|
+
)
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
import HoverShowcaseItemBlock from '../hover-showcase-item-block';
|
|
15
|
+
import HoverShowcaseSection from '../../sections/hover-showcase-section';
|
|
16
|
+
|
|
17
|
+
/** The image the descriptor's `getDefaultValue` drops inside every new item. */
|
|
18
|
+
const showcaseImage = {
|
|
19
|
+
id: 'img-1',
|
|
20
|
+
type: 'image',
|
|
21
|
+
label: 'Showcase Image',
|
|
22
|
+
value: {
|
|
23
|
+
desktop: 'https://placehold.co/800x600',
|
|
24
|
+
mobile: 'https://placehold.co/800x600'
|
|
25
|
+
},
|
|
26
|
+
properties: { alt: { desktop: 'Showcase Image' } },
|
|
27
|
+
styles: { width: { desktop: '100%' } },
|
|
28
|
+
order: 0,
|
|
29
|
+
hidden: false
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A stored item exactly as `getDefaultHoverShowcaseItemBlockValue()` seeds it:
|
|
34
|
+
* localetext keys held as BREAKPOINT buckets, an empty `link`, one image child.
|
|
35
|
+
*/
|
|
36
|
+
const storedItem = (over: Record<string, unknown> = {}) => ({
|
|
37
|
+
id: 'item-1',
|
|
38
|
+
type: 'hover-showcase-item',
|
|
39
|
+
label: 'Hover Showcase Item',
|
|
40
|
+
value: '',
|
|
41
|
+
properties: {
|
|
42
|
+
title: { desktop: 'Autumn Knitwear', mobile: 'Autumn Knitwear' },
|
|
43
|
+
subtitle: {
|
|
44
|
+
desktop: 'Merino and lambswool, new in',
|
|
45
|
+
mobile: 'Merino and lambswool, new in'
|
|
46
|
+
},
|
|
47
|
+
link: ''
|
|
48
|
+
},
|
|
49
|
+
styles: {},
|
|
50
|
+
order: 0,
|
|
51
|
+
hidden: false,
|
|
52
|
+
blocks: [showcaseImage],
|
|
53
|
+
...over
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const props = (over: Record<string, unknown> = {}) =>
|
|
57
|
+
({
|
|
58
|
+
block: storedItem(),
|
|
59
|
+
placeholderId: 'header',
|
|
60
|
+
sectionId: 'sec-1',
|
|
61
|
+
isDesigner: false,
|
|
62
|
+
currentBreakpoint: 'desktop',
|
|
63
|
+
...over
|
|
64
|
+
} as never);
|
|
65
|
+
|
|
66
|
+
const title = (container: HTMLElement) =>
|
|
67
|
+
container.querySelector('[data-hover-showcase-title]');
|
|
68
|
+
const subtitle = (container: HTMLElement) =>
|
|
69
|
+
container.querySelector('[data-hover-showcase-subtitle]');
|
|
70
|
+
const link = (container: HTMLElement) =>
|
|
71
|
+
container.querySelector('[data-hover-showcase-link]');
|
|
72
|
+
|
|
73
|
+
describe('hover showcase item — a realistic stored block', () => {
|
|
74
|
+
it('draws the title, the subtitle and the media inside it', () => {
|
|
75
|
+
const { container } = render(<HoverShowcaseItemBlock {...props()} />);
|
|
76
|
+
|
|
77
|
+
expect(title(container)!.tagName).toBe('H3');
|
|
78
|
+
expect(title(container)!.textContent).toBe('Autumn Knitwear');
|
|
79
|
+
expect(subtitle(container)!.textContent).toBe(
|
|
80
|
+
'Merino and lambswool, new in'
|
|
81
|
+
);
|
|
82
|
+
expect(container.querySelector('[data-testid="child"]')).not.toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('stamps the parts a follow-up styles through', () => {
|
|
86
|
+
const { container } = render(
|
|
87
|
+
<HoverShowcaseItemBlock
|
|
88
|
+
{...props({ block: storedItem({ styles: { link: '/knitwear' } }) })}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(title(container)!.getAttribute('data-part')).toBe('title');
|
|
93
|
+
expect(subtitle(container)!.getAttribute('data-part')).toBe('subtitle');
|
|
94
|
+
expect(link(container)!.getAttribute('data-part')).toBe('link');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('adds no box of its own, so the authored layout has more than one child', () => {
|
|
98
|
+
const { container } = render(<HoverShowcaseItemBlock {...props()} />);
|
|
99
|
+
|
|
100
|
+
// title, subtitle, image — siblings. A wrapper here would make the
|
|
101
|
+
// merchant's display/gap on the item lay out exactly one flex item.
|
|
102
|
+
expect(Array.from(container.children).map((el) => el.tagName)).toEqual([
|
|
103
|
+
'H3',
|
|
104
|
+
'P',
|
|
105
|
+
'DIV'
|
|
106
|
+
]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('resolves a localetext value stored as a locale map', () => {
|
|
110
|
+
const { container } = render(
|
|
111
|
+
<HoverShowcaseItemBlock
|
|
112
|
+
{...props({
|
|
113
|
+
block: storedItem({
|
|
114
|
+
properties: {
|
|
115
|
+
title: { 'en-us': 'Autumn Knitwear', 'tr-tr': 'Sonbahar Triko' },
|
|
116
|
+
subtitle: '',
|
|
117
|
+
link: ''
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
})}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(title(container)!.textContent).toBe('Autumn Knitwear');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('hover showcase item — the two places one value is stored', () => {
|
|
129
|
+
/**
|
|
130
|
+
* The descriptor declares title/subtitle/link without `target: 'property'`,
|
|
131
|
+
* so the panel writes them into `styles` while `getDefaultValue` seeds them
|
|
132
|
+
* into `properties`. The seeded `link` is an EMPTY STRING, which is why the
|
|
133
|
+
* fallback cannot be a `??` chain: '' is not nullish and would shadow the URL
|
|
134
|
+
* the merchant actually typed.
|
|
135
|
+
*/
|
|
136
|
+
it('reads the URL the panel wrote into styles past the empty seeded property', () => {
|
|
137
|
+
const block = storedItem({ styles: { link: '/collections/knitwear' } });
|
|
138
|
+
expect(block.properties.link).toBe('');
|
|
139
|
+
|
|
140
|
+
const { container } = render(<HoverShowcaseItemBlock {...props({ block })} />);
|
|
141
|
+
|
|
142
|
+
expect(link(container)!.getAttribute('href')).toBe('/collections/knitwear');
|
|
143
|
+
// Only the title is linked, matching the section.
|
|
144
|
+
expect(link(container)!.querySelector('h3')).not.toBeNull();
|
|
145
|
+
expect(subtitle(container)!.closest('a')).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('lets a non-empty property win over a stale styles value', () => {
|
|
149
|
+
const { container } = render(
|
|
150
|
+
<HoverShowcaseItemBlock
|
|
151
|
+
{...props({
|
|
152
|
+
block: storedItem({ styles: { title: 'Typed before the fix' } })
|
|
153
|
+
})}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(title(container)!.textContent).toBe('Autumn Knitwear');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('does not follow the link in the designer', () => {
|
|
161
|
+
const { container } = render(
|
|
162
|
+
<HoverShowcaseItemBlock
|
|
163
|
+
{...props({
|
|
164
|
+
block: storedItem({ styles: { link: '/collections/knitwear' } }),
|
|
165
|
+
isDesigner: true
|
|
166
|
+
})}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const event = new MouseEvent('click', { bubbles: true, cancelable: true });
|
|
171
|
+
fireEvent(link(container)!, event);
|
|
172
|
+
expect(event.defaultPrevented).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('hover showcase item — the two font sizes that are not CSS', () => {
|
|
177
|
+
it('inlines an authored title/subtitle font size, which no stylesheet can', () => {
|
|
178
|
+
const { container } = render(
|
|
179
|
+
<HoverShowcaseItemBlock
|
|
180
|
+
{...props({
|
|
181
|
+
block: storedItem({
|
|
182
|
+
styles: {
|
|
183
|
+
'title-font-size': { desktop: '48px', mobile: '28px' },
|
|
184
|
+
'subtitle-font-size': '18px'
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
})}
|
|
188
|
+
/>
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect((title(container) as HTMLElement).style.fontSize).toBe('48px');
|
|
192
|
+
expect((subtitle(container) as HTMLElement).style.fontSize).toBe('18px');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('resolves them per breakpoint', () => {
|
|
196
|
+
const { container } = render(
|
|
197
|
+
<HoverShowcaseItemBlock
|
|
198
|
+
{...props({
|
|
199
|
+
block: storedItem({
|
|
200
|
+
styles: { 'title-font-size': { desktop: '48px', mobile: '28px' } }
|
|
201
|
+
}),
|
|
202
|
+
currentBreakpoint: 'mobile'
|
|
203
|
+
})}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect((title(container) as HTMLElement).style.fontSize).toBe('28px');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('inlines no size when neither was authored, so a real font-size still wins', () => {
|
|
211
|
+
const { container } = render(<HoverShowcaseItemBlock {...props()} />);
|
|
212
|
+
|
|
213
|
+
expect((title(container) as HTMLElement).style.fontSize).toBe('');
|
|
214
|
+
expect((subtitle(container) as HTMLElement).style.fontSize).toBe('');
|
|
215
|
+
// The only thing inlined on the heading is the user-agent margin reset.
|
|
216
|
+
expect((title(container) as HTMLElement).getAttribute('style')).toBe(
|
|
217
|
+
'margin: 0px;'
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('reads nothing else off styles — padding stays with the wrapper stylesheet', () => {
|
|
222
|
+
const { container } = render(
|
|
223
|
+
<HoverShowcaseItemBlock
|
|
224
|
+
{...props({
|
|
225
|
+
block: storedItem({
|
|
226
|
+
styles: { 'padding-top': 40, 'padding-bottom': 40, color: '#111' }
|
|
227
|
+
})
|
|
228
|
+
})}
|
|
229
|
+
/>
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const heading = title(container) as HTMLElement;
|
|
233
|
+
expect(heading.style.paddingTop).toBe('');
|
|
234
|
+
expect(heading.style.color).toBe('');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('hover showcase item — nothing to draw', () => {
|
|
239
|
+
const emptyBlock = storedItem({
|
|
240
|
+
properties: { title: '', subtitle: '', link: '' },
|
|
241
|
+
blocks: []
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('draws the labelled designer band instead of inventing a title', () => {
|
|
245
|
+
const { container } = render(
|
|
246
|
+
<HoverShowcaseItemBlock
|
|
247
|
+
{...props({ block: emptyBlock, isDesigner: true })}
|
|
248
|
+
/>
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
expect(container.textContent).toContain('Hover showcase item');
|
|
252
|
+
// The section's `Item ${index + 1}` needs a loop index a lone block has not
|
|
253
|
+
// got, and the layer-tree label must never ship as a shopper-facing heading.
|
|
254
|
+
expect(container.textContent).not.toContain('Item 1');
|
|
255
|
+
expect(container.textContent).not.toContain('Hover Showcase Item');
|
|
256
|
+
expect(
|
|
257
|
+
container.querySelector('[data-hover-showcase-item]')
|
|
258
|
+
).not.toBeNull();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('draws nothing at all on the storefront', () => {
|
|
262
|
+
const { container } = render(
|
|
263
|
+
<HoverShowcaseItemBlock {...props({ block: emptyBlock })} />
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
expect(container.innerHTML).toBe('');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('still draws when only the media is authored', () => {
|
|
270
|
+
const { container } = render(
|
|
271
|
+
<HoverShowcaseItemBlock
|
|
272
|
+
{...props({
|
|
273
|
+
block: storedItem({ properties: { title: '', subtitle: '', link: '' } })
|
|
274
|
+
})}
|
|
275
|
+
/>
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(title(container)).toBeNull();
|
|
279
|
+
expect(container.querySelector('[data-testid="child"]')).not.toBeNull();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('hover showcase item — parity with the section that draws it by hand', () => {
|
|
284
|
+
/**
|
|
285
|
+
* `hover-showcase-section.tsx` filters its children by type (:122) and draws
|
|
286
|
+
* them itself, never through ThemeBlock. The same stored block therefore has
|
|
287
|
+
* two renderers, and this pins that they read the same text out of it.
|
|
288
|
+
*/
|
|
289
|
+
const section = {
|
|
290
|
+
id: 'sec-1',
|
|
291
|
+
type: 'hover-showcase',
|
|
292
|
+
name: 'Hover Showcase',
|
|
293
|
+
label: 'Hover Showcase',
|
|
294
|
+
properties: {},
|
|
295
|
+
styles: {},
|
|
296
|
+
blocks: [storedItem()],
|
|
297
|
+
order: 0,
|
|
298
|
+
hidden: false
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
it('reads the same title and subtitle the section reads', () => {
|
|
302
|
+
const { container: standalone } = render(
|
|
303
|
+
<HoverShowcaseItemBlock {...props()} />
|
|
304
|
+
);
|
|
305
|
+
const { container: inSection } = render(
|
|
306
|
+
<HoverShowcaseSection section={section as never} />
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const headings = Array.from(inSection.querySelectorAll('h3')).map(
|
|
310
|
+
(el) => el.textContent
|
|
311
|
+
);
|
|
312
|
+
expect(headings).toContain(title(standalone)!.textContent);
|
|
313
|
+
expect(inSection.textContent).toContain(subtitle(standalone)!.textContent);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import IconButtonBlock from '../icon-button-block';
|
|
5
|
+
import { ThemeSettingsProvider } from '../../theme-settings-context';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A block as the editor actually stores it: the descriptor's `getDefaultValue`
|
|
9
|
+
* seeds every property AND every style as a per-breakpoint bucket, and the
|
|
10
|
+
* shared action properties are appended to the same `properties` object.
|
|
11
|
+
*/
|
|
12
|
+
const storedBlock = (over: Record<string, unknown> = {}) => ({
|
|
13
|
+
id: 'ib-1',
|
|
14
|
+
type: 'icon-button',
|
|
15
|
+
label: 'Icon Button',
|
|
16
|
+
properties: {
|
|
17
|
+
icon: { desktop: 'basket' },
|
|
18
|
+
iconSize: { desktop: '28' },
|
|
19
|
+
htmlId: '',
|
|
20
|
+
actionType: 'none',
|
|
21
|
+
...((over.properties as Record<string, unknown>) ?? {})
|
|
22
|
+
},
|
|
23
|
+
styles: {
|
|
24
|
+
color: { desktop: 'currentColor' },
|
|
25
|
+
'background-color': { desktop: 'transparent' },
|
|
26
|
+
'margin-top': { desktop: '0px' },
|
|
27
|
+
'margin-right': { desktop: '0px' },
|
|
28
|
+
'margin-bottom': { desktop: '0px' },
|
|
29
|
+
'margin-left': { desktop: '0px' }
|
|
30
|
+
},
|
|
31
|
+
order: 0,
|
|
32
|
+
hidden: false,
|
|
33
|
+
...over
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const props = (over: Record<string, unknown> = {}) =>
|
|
37
|
+
({
|
|
38
|
+
block: storedBlock(),
|
|
39
|
+
placeholderId: 'p',
|
|
40
|
+
sectionId: 's',
|
|
41
|
+
isDesigner: false,
|
|
42
|
+
...over
|
|
43
|
+
} as any);
|
|
44
|
+
|
|
45
|
+
describe('IconButtonBlock — drawing', () => {
|
|
46
|
+
it('draws the catalogue glyph at the authored size from a stored block', () => {
|
|
47
|
+
const { container } = render(<IconButtonBlock {...props()} />);
|
|
48
|
+
|
|
49
|
+
const svg = container.querySelector('svg[data-icon-button]');
|
|
50
|
+
expect(svg).not.toBeNull();
|
|
51
|
+
expect(svg!.getAttribute('width')).toBe('28');
|
|
52
|
+
expect(svg!.querySelector('path')).not.toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('reads icon and size from the breakpoint bucket the panel writes', () => {
|
|
56
|
+
const { container } = render(
|
|
57
|
+
<IconButtonBlock
|
|
58
|
+
{...props({
|
|
59
|
+
block: storedBlock({
|
|
60
|
+
properties: {
|
|
61
|
+
icon: { desktop: 'basket', mobile: 'search' },
|
|
62
|
+
iconSize: { desktop: '28', mobile: '16' }
|
|
63
|
+
}
|
|
64
|
+
}),
|
|
65
|
+
currentBreakpoint: 'mobile'
|
|
66
|
+
})}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const svg = container.querySelector('svg[data-icon-button]');
|
|
71
|
+
expect(svg!.getAttribute('width')).toBe('16');
|
|
72
|
+
// `search` is the only catalogue glyph drawn from a circle plus a line.
|
|
73
|
+
expect(svg!.querySelector('circle')).not.toBeNull();
|
|
74
|
+
expect(svg!.getAttribute('aria-label')).toBe('Search');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('draws an uploaded brand mark from theme settings instead of the glyph', () => {
|
|
78
|
+
const { container } = render(
|
|
79
|
+
<ThemeSettingsProvider
|
|
80
|
+
value={{ icons: { basket: '<svg data-brand="1"><rect /></svg>' } }}
|
|
81
|
+
>
|
|
82
|
+
<IconButtonBlock {...props()} />
|
|
83
|
+
</ThemeSettingsProvider>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(container.querySelector('svg[data-brand="1"]')).not.toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('sizes uploaded SVG markup through a rule scoped to this block', () => {
|
|
90
|
+
const { container } = render(
|
|
91
|
+
<IconButtonBlock
|
|
92
|
+
{...props({
|
|
93
|
+
block: storedBlock({
|
|
94
|
+
properties: {
|
|
95
|
+
icon: { desktop: '<svg viewBox="0 0 24 24"><rect /></svg>' },
|
|
96
|
+
iconSize: { desktop: '32' }
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
})}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(container.querySelector('svg rect')).not.toBeNull();
|
|
104
|
+
const css = container.querySelector('style')!.textContent!;
|
|
105
|
+
expect(css).toBe(
|
|
106
|
+
'[data-icon-button-id="ib-1"] svg{width:32px;height:32px;}'
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('draws a stored media path through the image component', () => {
|
|
111
|
+
const { container } = render(
|
|
112
|
+
<IconButtonBlock
|
|
113
|
+
{...props({
|
|
114
|
+
block: storedBlock({
|
|
115
|
+
label: 'Open basket',
|
|
116
|
+
properties: {
|
|
117
|
+
icon: { desktop: '/icons/basket.svg' },
|
|
118
|
+
iconSize: { desktop: '24' }
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
})}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const img = container.querySelector('img[data-icon-button]');
|
|
126
|
+
expect(img!.getAttribute('src')).toBe('/icons/basket.svg');
|
|
127
|
+
expect(img!.getAttribute('alt')).toBe('Open basket');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('IconButtonBlock — accessible name', () => {
|
|
132
|
+
it('uses the layer-tree name once the designer has renamed the block', () => {
|
|
133
|
+
const { container } = render(
|
|
134
|
+
<IconButtonBlock
|
|
135
|
+
{...props({ block: storedBlock({ label: 'Open basket' }) })}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const svg = container.querySelector('svg[data-icon-button]')!;
|
|
140
|
+
expect(svg.getAttribute('role')).toBe('img');
|
|
141
|
+
expect(svg.getAttribute('aria-label')).toBe('Open basket');
|
|
142
|
+
expect(svg.getAttribute('aria-hidden')).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('falls back to the picked icon key while the label is the descriptor default', () => {
|
|
146
|
+
const { container } = render(<IconButtonBlock {...props()} />);
|
|
147
|
+
|
|
148
|
+
expect(
|
|
149
|
+
container
|
|
150
|
+
.querySelector('svg[data-icon-button]')!
|
|
151
|
+
.getAttribute('aria-label')
|
|
152
|
+
).toBe('Basket');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('hides an unnamed icon from the accessibility tree rather than announcing nothing', () => {
|
|
156
|
+
const { container } = render(
|
|
157
|
+
<IconButtonBlock
|
|
158
|
+
{...props({
|
|
159
|
+
block: storedBlock({
|
|
160
|
+
label: 'Icon Button',
|
|
161
|
+
properties: { icon: { desktop: '<svg><rect /></svg>' } }
|
|
162
|
+
})
|
|
163
|
+
})}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const host = container.querySelector('[data-icon-button]')!;
|
|
168
|
+
expect(host.getAttribute('aria-hidden')).toBe('true');
|
|
169
|
+
expect(host.getAttribute('aria-label')).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('IconButtonBlock — empty state', () => {
|
|
174
|
+
it('shows a labelled band in the designer when no icon is picked', () => {
|
|
175
|
+
const { container } = render(
|
|
176
|
+
<IconButtonBlock
|
|
177
|
+
{...props({
|
|
178
|
+
block: storedBlock({ properties: { icon: { desktop: '' } } }),
|
|
179
|
+
isDesigner: true
|
|
180
|
+
})}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(container.textContent).toContain('Icon button (no icon)');
|
|
185
|
+
expect(
|
|
186
|
+
container.querySelector('[data-theme-block-placeholder]')
|
|
187
|
+
).not.toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('renders nothing for a shopper when no icon is picked', () => {
|
|
191
|
+
const { container } = render(
|
|
192
|
+
<IconButtonBlock
|
|
193
|
+
{...props({
|
|
194
|
+
block: storedBlock({ properties: { icon: { desktop: '' } } }),
|
|
195
|
+
isDesigner: false
|
|
196
|
+
})}
|
|
197
|
+
/>
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(container.firstChild).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('IconButtonBlock — the five centrally read properties', () => {
|
|
205
|
+
it('draws the same markup with an action authored: the wrapper owns the click', () => {
|
|
206
|
+
const plain = render(<IconButtonBlock {...props()} />).container.innerHTML;
|
|
207
|
+
|
|
208
|
+
const withAction = render(
|
|
209
|
+
<IconButtonBlock
|
|
210
|
+
{...props({
|
|
211
|
+
block: storedBlock({
|
|
212
|
+
properties: {
|
|
213
|
+
icon: { desktop: 'basket' },
|
|
214
|
+
iconSize: { desktop: '28' },
|
|
215
|
+
htmlId: 'basket-trigger',
|
|
216
|
+
actionType: 'open-modal',
|
|
217
|
+
actionTarget: 'mini-basket',
|
|
218
|
+
actionAudience: 'all'
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
})}
|
|
222
|
+
/>
|
|
223
|
+
).container.innerHTML;
|
|
224
|
+
|
|
225
|
+
expect(withAction).toBe(plain);
|
|
226
|
+
// No anchor and no button of its own, which is what keeps the type out of
|
|
227
|
+
// NATIVELY_INTERACTIVE_BLOCK_TYPES.
|
|
228
|
+
expect(withAction).not.toContain('<a ');
|
|
229
|
+
expect(withAction).not.toContain('<button');
|
|
230
|
+
});
|
|
231
|
+
});
|