@akinon/pz-theme 2.0.164-rc.1 → 2.0.164
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 +8 -58
- package/package.json +4 -4
- package/src/__tests__/editor-locale-bridge.test.tsx +1 -32
- package/src/__tests__/theme-settings-defaults.test.tsx +395 -0
- package/src/__tests__/theme-text-defaults-code-owned.test.tsx +340 -0
- package/src/blocks/__tests__/breadcrumb-theme-separator.test.tsx +148 -0
- package/src/blocks/__tests__/social-icons-theme-fallback.test.tsx +268 -0
- package/src/blocks/__tests__/text-block.test.tsx +6 -34
- package/src/blocks/accordion-block.tsx +10 -0
- package/src/blocks/hotspot-block.tsx +6 -1
- package/src/blocks/listing-breadcrumb-block.tsx +10 -1
- package/src/blocks/order-detail-block.tsx +95 -113
- package/src/blocks/product-breadcrumb-block.tsx +9 -1
- package/src/blocks/social-glyphs.tsx +31 -0
- package/src/blocks/social-icons-list-block.tsx +72 -8
- package/src/blocks/text-block.tsx +15 -6
- package/src/chrome/__tests__/theme-page-up.test.tsx +522 -0
- package/src/chrome/theme-page-up.tsx +303 -0
- package/src/chrome/use-editor-theme-settings.ts +104 -0
- package/src/components/with-designer-features.tsx +27 -0
- package/src/custom/README.md +219 -100
- package/src/custom/__tests__/use-theme-setting.test.tsx +0 -97
- package/src/custom/collect.ts +9 -4
- package/src/custom/property-types.ts +15 -15
- package/src/custom/use-theme-setting.ts +5 -21
- package/src/editor-locale-bridge.tsx +11 -39
- package/src/index.ts +15 -0
- package/src/overlays/theme-modals.tsx +10 -0
- package/src/sections/__tests__/section-wrapper.test.tsx +76 -0
- package/src/sections/coupon-banner-section.tsx +15 -1
- package/src/sections/section-wrapper.tsx +7 -0
- package/src/sections/tabs-section.tsx +13 -0
- package/src/theme-placeholder-wrapper.tsx +50 -3
- package/src/theme-section.tsx +15 -0
- package/src/utils/__tests__/svg-image-source.test.ts +123 -0
- package/src/utils/__tests__/theme-settings-css.test.ts +801 -0
- package/src/utils/__tests__/theme-settings-readers.test.ts +213 -0
- package/src/utils/__tests__/theme-settings-resolver.test.ts +232 -0
- package/src/utils/index.ts +36 -1
- package/src/utils/svg-image-source.ts +84 -0
- package/src/utils/theme-settings-css.ts +717 -0
- package/src/utils/theme-settings-readers.ts +205 -0
- package/src/blocks/__tests__/order-line-actions.test.tsx +0 -54
- package/src/blocks/order-line-actions.ts +0 -45
- package/src/utils/__tests__/sanitize-rich-text.test.ts +0 -80
- package/src/utils/sanitize-rich-text.ts +0 -215
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import CouponBannerSection from '../sections/coupon-banner-section';
|
|
5
|
+
import TabsSection from '../sections/tabs-section';
|
|
6
|
+
import AccordionBlock from '../blocks/accordion-block';
|
|
7
|
+
import HotspotBlock from '../blocks/hotspot-block';
|
|
8
|
+
import { ThemeSettingsProvider } from '../theme-settings-context';
|
|
9
|
+
import { buildThemeElementDefaultsCss } from '../utils/theme-settings-css';
|
|
10
|
+
import type { Section } from '../theme-section';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Boxes drawn by a renderer that paint the text of the blocks inside them —
|
|
14
|
+
* a colour from a section setting, a size from a class list, a part colour —
|
|
15
|
+
* must keep that text out of the theme's rich-text defaults, or the theme's
|
|
16
|
+
* Paragraf colour and style repaint it (a light Koyu text colour on the
|
|
17
|
+
* coupon's white code box is about 1.6:1).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The Koyu preset's text roles plus an edited Paragraf style. */
|
|
21
|
+
const THEMED = {
|
|
22
|
+
textColor: '#C8C8C8',
|
|
23
|
+
headingColor: '#FFFFFF',
|
|
24
|
+
linkColor: '#F2C35B',
|
|
25
|
+
typography: [
|
|
26
|
+
{
|
|
27
|
+
key: 'body',
|
|
28
|
+
fontFamily: 'Jost',
|
|
29
|
+
fontWeight: '400',
|
|
30
|
+
fontSize: 14,
|
|
31
|
+
lineHeight: 22,
|
|
32
|
+
letterSpacing: 0,
|
|
33
|
+
textTransform: 'none'
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Saved by the old settings page: no text default at all. */
|
|
39
|
+
const LEGACY = {
|
|
40
|
+
primaryColor: '#000000',
|
|
41
|
+
typography: [{ key: 'body', fontFamily: 'Jost', fontSize: 12 }]
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const rules = buildThemeElementDefaultsCss(THEMED)
|
|
45
|
+
.split('}')
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.map((rule) => {
|
|
48
|
+
const [selector, declarations] = rule.split('{');
|
|
49
|
+
return { selector, declarations };
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const selectorsSetting = (property: string) =>
|
|
53
|
+
rules
|
|
54
|
+
.filter(({ declarations }) => declarations.startsWith(`${property}:`))
|
|
55
|
+
.map(({ selector }) => selector);
|
|
56
|
+
|
|
57
|
+
const COLOR_SELECTORS = selectorsSetting('color');
|
|
58
|
+
const TYPE_SELECTORS = selectorsSetting('font-family');
|
|
59
|
+
|
|
60
|
+
/** Whether a theme colour default reaches `el`. */
|
|
61
|
+
const painted = (el: Element) =>
|
|
62
|
+
COLOR_SELECTORS.some((selector) => el.matches(selector));
|
|
63
|
+
/** Whether the theme's managed typography reaches `el`. */
|
|
64
|
+
const typeset = (el: Element) =>
|
|
65
|
+
TYPE_SELECTORS.some((selector) => el.matches(selector));
|
|
66
|
+
|
|
67
|
+
const text = (
|
|
68
|
+
id: string,
|
|
69
|
+
value: string,
|
|
70
|
+
extra: Record<string, unknown> = {}
|
|
71
|
+
) => ({
|
|
72
|
+
id,
|
|
73
|
+
type: 'text',
|
|
74
|
+
label: id,
|
|
75
|
+
order: 0,
|
|
76
|
+
hidden: false,
|
|
77
|
+
properties: {},
|
|
78
|
+
styles: {},
|
|
79
|
+
value,
|
|
80
|
+
blocks: [],
|
|
81
|
+
...extra
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const mount = (settings: Record<string, unknown>, node: React.ReactNode) =>
|
|
85
|
+
render(
|
|
86
|
+
<ThemeSettingsProvider value={settings}>
|
|
87
|
+
<div className="theme-placeholder">{node}</div>
|
|
88
|
+
</ThemeSettingsProvider>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
/** The paragraph of text block `id`. */
|
|
92
|
+
const paragraphOf = (container: HTMLElement, id: string) => {
|
|
93
|
+
const p = container.querySelector(`[data-block-id="${id}"] p`);
|
|
94
|
+
if (!p) throw new Error(`no paragraph in ${id}`);
|
|
95
|
+
return p;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
describe('the coupon banner code box', () => {
|
|
99
|
+
const section = (withBoxBlock: boolean) =>
|
|
100
|
+
({
|
|
101
|
+
id: 'coupon',
|
|
102
|
+
type: 'coupon-banner',
|
|
103
|
+
name: 'coupon',
|
|
104
|
+
label: 'Coupon',
|
|
105
|
+
order: 0,
|
|
106
|
+
hidden: false,
|
|
107
|
+
properties: { 'coupon-code': 'SAVE20' },
|
|
108
|
+
styles: {},
|
|
109
|
+
blocks: [
|
|
110
|
+
text('title', '<p>Weekend sale</p>'),
|
|
111
|
+
...(withBoxBlock
|
|
112
|
+
? [
|
|
113
|
+
{
|
|
114
|
+
...text('box', ''),
|
|
115
|
+
type: 'group',
|
|
116
|
+
order: 1,
|
|
117
|
+
properties: { couponBannerRole: 'coupon-code-box' }
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
: []),
|
|
121
|
+
text('code', '', {
|
|
122
|
+
order: 2,
|
|
123
|
+
properties: { couponBannerRole: 'coupon-code-text' }
|
|
124
|
+
})
|
|
125
|
+
]
|
|
126
|
+
} as unknown as Section);
|
|
127
|
+
|
|
128
|
+
it.each([
|
|
129
|
+
['a box block', true],
|
|
130
|
+
['the built-in box', false]
|
|
131
|
+
])('keeps its colour and type on the code inside %s', (_label, withBox) => {
|
|
132
|
+
const { container } = mount(
|
|
133
|
+
THEMED,
|
|
134
|
+
<CouponBannerSection
|
|
135
|
+
section={section(withBox)}
|
|
136
|
+
currentBreakpoint="desktop"
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
139
|
+
const code = paragraphOf(container, 'code');
|
|
140
|
+
expect(code).toHaveTextContent('SAVE20');
|
|
141
|
+
|
|
142
|
+
// The box paints the colour inline and the type with its classes.
|
|
143
|
+
const box = code.closest('[data-pz-own-type]');
|
|
144
|
+
expect(box).toHaveClass('uppercase', 'tracking-[0.12em]');
|
|
145
|
+
expect(box).toHaveAttribute('data-pz-own-color', '');
|
|
146
|
+
expect(painted(code)).toBe(false);
|
|
147
|
+
expect(typeset(code)).toBe(false);
|
|
148
|
+
|
|
149
|
+
// The banner's own copy still takes the theme defaults.
|
|
150
|
+
const title = paragraphOf(container, 'title');
|
|
151
|
+
expect(painted(title)).toBe(true);
|
|
152
|
+
expect(typeset(title)).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('adds no marker for a theme without text defaults', () => {
|
|
156
|
+
const { container } = mount(
|
|
157
|
+
LEGACY,
|
|
158
|
+
<CouponBannerSection
|
|
159
|
+
section={section(true)}
|
|
160
|
+
currentBreakpoint="desktop"
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
expect(container.innerHTML).not.toContain('data-pz-');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('the tabs section', () => {
|
|
168
|
+
const pane = (
|
|
169
|
+
id: string,
|
|
170
|
+
headerStyles: Record<string, unknown>,
|
|
171
|
+
contentStyles: Record<string, unknown>,
|
|
172
|
+
order: number
|
|
173
|
+
) => ({
|
|
174
|
+
id,
|
|
175
|
+
type: 'tab',
|
|
176
|
+
label: id,
|
|
177
|
+
order,
|
|
178
|
+
hidden: false,
|
|
179
|
+
properties: {},
|
|
180
|
+
styles: {},
|
|
181
|
+
blocks: [
|
|
182
|
+
{
|
|
183
|
+
...text(`${id}-header`, ''),
|
|
184
|
+
type: 'group',
|
|
185
|
+
label: 'Tab Header',
|
|
186
|
+
styles: headerStyles,
|
|
187
|
+
blocks: [text(`${id}-title`, `<p>${id}</p>`)]
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
...text(`${id}-content`, ''),
|
|
191
|
+
type: 'group',
|
|
192
|
+
label: 'Tab Content',
|
|
193
|
+
order: 1,
|
|
194
|
+
styles: contentStyles,
|
|
195
|
+
blocks: [text(`${id}-body`, '<p>Body</p>')]
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const section = {
|
|
201
|
+
id: 'tabs',
|
|
202
|
+
type: 'tabs',
|
|
203
|
+
name: 'tabs',
|
|
204
|
+
label: 'Tabs',
|
|
205
|
+
order: 0,
|
|
206
|
+
hidden: false,
|
|
207
|
+
properties: {},
|
|
208
|
+
styles: {},
|
|
209
|
+
blocks: [
|
|
210
|
+
pane(
|
|
211
|
+
'one',
|
|
212
|
+
{ 'font-size': { desktop: '18px' } },
|
|
213
|
+
{ color: { desktop: '#ff0000' } },
|
|
214
|
+
0
|
|
215
|
+
),
|
|
216
|
+
pane('two', {}, {}, 1)
|
|
217
|
+
]
|
|
218
|
+
} as unknown as Section;
|
|
219
|
+
|
|
220
|
+
it('lets the active colour and the header and content styles reach their text', () => {
|
|
221
|
+
const { container } = mount(
|
|
222
|
+
THEMED,
|
|
223
|
+
<TabsSection section={section} currentBreakpoint="desktop" />
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Active: the tab paints the title with its active colour, and the
|
|
227
|
+
// header block's size reaches it.
|
|
228
|
+
expect(painted(paragraphOf(container, 'one-title'))).toBe(false);
|
|
229
|
+
expect(typeset(paragraphOf(container, 'one-title'))).toBe(false);
|
|
230
|
+
// Inactive, with nothing authored: the theme's defaults.
|
|
231
|
+
expect(painted(paragraphOf(container, 'two-title'))).toBe(true);
|
|
232
|
+
expect(typeset(paragraphOf(container, 'two-title'))).toBe(true);
|
|
233
|
+
// The content block's own colour, painted on the pane.
|
|
234
|
+
expect(painted(paragraphOf(container, 'one-body'))).toBe(false);
|
|
235
|
+
expect(typeset(paragraphOf(container, 'one-body'))).toBe(true);
|
|
236
|
+
|
|
237
|
+
fireEvent.click(paragraphOf(container, 'two-title'));
|
|
238
|
+
|
|
239
|
+
expect(painted(paragraphOf(container, 'two-title'))).toBe(false);
|
|
240
|
+
expect(painted(paragraphOf(container, 'one-title'))).toBe(true);
|
|
241
|
+
expect(typeset(paragraphOf(container, 'one-title'))).toBe(false);
|
|
242
|
+
expect(painted(paragraphOf(container, 'two-body'))).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('the accordion parts', () => {
|
|
247
|
+
const accordion = (styles: Record<string, unknown>) => ({
|
|
248
|
+
id: 'acc',
|
|
249
|
+
type: 'accordion',
|
|
250
|
+
label: 'acc',
|
|
251
|
+
order: 0,
|
|
252
|
+
hidden: false,
|
|
253
|
+
properties: { defaultOpen: true },
|
|
254
|
+
styles,
|
|
255
|
+
blocks: [
|
|
256
|
+
{
|
|
257
|
+
...text('acc-heading', ''),
|
|
258
|
+
type: 'group',
|
|
259
|
+
label: 'Accordion Heading',
|
|
260
|
+
blocks: [text('question', '<p>Question</p>')]
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
...text('acc-content', ''),
|
|
264
|
+
type: 'group',
|
|
265
|
+
label: 'Accordion Content',
|
|
266
|
+
order: 1,
|
|
267
|
+
blocks: [text('answer', '<p>Answer</p>')]
|
|
268
|
+
}
|
|
269
|
+
]
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const mountAccordion = (styles: Record<string, unknown>) =>
|
|
273
|
+
mount(
|
|
274
|
+
THEMED,
|
|
275
|
+
<AccordionBlock
|
|
276
|
+
block={accordion(styles) as never}
|
|
277
|
+
placeholderId="p"
|
|
278
|
+
sectionId="s"
|
|
279
|
+
isDesigner={false}
|
|
280
|
+
currentBreakpoint="desktop"
|
|
281
|
+
/>
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
it('keep a part colour or type, at rest or open, on the blocks inside', () => {
|
|
285
|
+
const { container } = mountAccordion({
|
|
286
|
+
'header/open/color': { desktop: '#ff0000' },
|
|
287
|
+
'content/color': { desktop: '#00aa00' },
|
|
288
|
+
'content/font-size': { desktop: '18px' }
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(container.querySelector('[data-part="header"]')).toHaveAttribute(
|
|
292
|
+
'data-pz-own-color',
|
|
293
|
+
''
|
|
294
|
+
);
|
|
295
|
+
expect(painted(paragraphOf(container, 'question'))).toBe(false);
|
|
296
|
+
expect(typeset(paragraphOf(container, 'question'))).toBe(true);
|
|
297
|
+
expect(painted(paragraphOf(container, 'answer'))).toBe(false);
|
|
298
|
+
expect(typeset(paragraphOf(container, 'answer'))).toBe(false);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('mark no part without an authored part colour or type', () => {
|
|
302
|
+
const { container } = mountAccordion({
|
|
303
|
+
'header/background-color': { desktop: '#000000' }
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
expect(container.querySelector('[data-part="header"]')).not.toHaveAttribute(
|
|
307
|
+
'data-pz-own-color'
|
|
308
|
+
);
|
|
309
|
+
expect(painted(paragraphOf(container, 'question'))).toBe(true);
|
|
310
|
+
expect(painted(paragraphOf(container, 'answer'))).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('the hotspot tooltip', () => {
|
|
315
|
+
it('keeps its class-set size on the blocks inside', () => {
|
|
316
|
+
const { container } = mount(
|
|
317
|
+
THEMED,
|
|
318
|
+
<HotspotBlock
|
|
319
|
+
block={
|
|
320
|
+
{
|
|
321
|
+
...text('spot', ''),
|
|
322
|
+
type: 'hotspot',
|
|
323
|
+
properties: { title: 'Look' },
|
|
324
|
+
blocks: [text('tip', '<p>Tip</p>')]
|
|
325
|
+
} as never
|
|
326
|
+
}
|
|
327
|
+
placeholderId="p"
|
|
328
|
+
sectionId="s"
|
|
329
|
+
isDesigner={false}
|
|
330
|
+
currentBreakpoint="desktop"
|
|
331
|
+
/>
|
|
332
|
+
);
|
|
333
|
+
fireEvent.click(screen.getByRole('button', { name: 'Look' }));
|
|
334
|
+
|
|
335
|
+
const tip = paragraphOf(container, 'tip');
|
|
336
|
+
expect(typeset(tip)).toBe(false);
|
|
337
|
+
// Its text colour is not the tooltip's: the theme's still applies.
|
|
338
|
+
expect(painted(tip)).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import type { BlockRendererProps } from '../block-renderer-registry';
|
|
5
|
+
import ListingBreadcrumbBlock from '../listing-breadcrumb-block';
|
|
6
|
+
import ProductBreadcrumbBlock from '../product-breadcrumb-block';
|
|
7
|
+
import { ThemePageContextProvider } from '../../theme-page-context';
|
|
8
|
+
import { ThemeSettingsProvider } from '../../theme-settings-context';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The settings page's "Breadcrumb ayırıcı" (`breadcrumbSeparator`)
|
|
12
|
+
* is the theme-wide default for both breadcrumb blocks. It only fills a block
|
|
13
|
+
* whose own `separator` is absent — the editor stops persisting '/' for new
|
|
14
|
+
* blocks — so every block saved before keeps the separator it renders today.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const LISTING_PAGE = {
|
|
18
|
+
listing: {
|
|
19
|
+
products: [],
|
|
20
|
+
breadcrumbs: [
|
|
21
|
+
{ label: 'Women', url: '/women' },
|
|
22
|
+
{ label: 'Dresses', url: '/women/dresses' }
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const PRODUCT_PAGE = {
|
|
28
|
+
product: {
|
|
29
|
+
pk: 7,
|
|
30
|
+
name: 'Linen dress',
|
|
31
|
+
categories: [{ name: 'Women', url: '/women' }]
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const blockProps = (
|
|
36
|
+
type: string,
|
|
37
|
+
properties: Record<string, unknown>
|
|
38
|
+
): BlockRendererProps =>
|
|
39
|
+
({
|
|
40
|
+
block: {
|
|
41
|
+
id: `${type}-1`,
|
|
42
|
+
type,
|
|
43
|
+
label: 'Breadcrumb',
|
|
44
|
+
value: '',
|
|
45
|
+
properties,
|
|
46
|
+
styles: {},
|
|
47
|
+
order: 0,
|
|
48
|
+
hidden: false
|
|
49
|
+
},
|
|
50
|
+
placeholderId: 'p',
|
|
51
|
+
sectionId: 's',
|
|
52
|
+
isDesigner: false,
|
|
53
|
+
currentBreakpoint: 'desktop'
|
|
54
|
+
} as unknown as BlockRendererProps);
|
|
55
|
+
|
|
56
|
+
type Variant = {
|
|
57
|
+
name: string;
|
|
58
|
+
Block: React.ComponentType<BlockRendererProps>;
|
|
59
|
+
type: string;
|
|
60
|
+
page: Record<string, unknown>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const VARIANTS: Variant[] = [
|
|
64
|
+
{
|
|
65
|
+
name: 'listing breadcrumb',
|
|
66
|
+
Block: ListingBreadcrumbBlock,
|
|
67
|
+
type: 'listing-breadcrumb',
|
|
68
|
+
page: LISTING_PAGE
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'product breadcrumb',
|
|
72
|
+
Block: ProductBreadcrumbBlock,
|
|
73
|
+
type: 'product-breadcrumb',
|
|
74
|
+
page: PRODUCT_PAGE
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const separators = (
|
|
79
|
+
variant: Variant,
|
|
80
|
+
properties: Record<string, unknown>,
|
|
81
|
+
settings: Record<string, unknown> | null
|
|
82
|
+
) => {
|
|
83
|
+
const { Block, type, page } = variant;
|
|
84
|
+
const { container } = render(
|
|
85
|
+
<ThemeSettingsProvider value={settings}>
|
|
86
|
+
<ThemePageContextProvider value={page}>
|
|
87
|
+
<Block {...blockProps(type, properties)} />
|
|
88
|
+
</ThemePageContextProvider>
|
|
89
|
+
</ThemeSettingsProvider>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return Array.from(
|
|
93
|
+
container.querySelectorAll('[data-breadcrumb-separator]')
|
|
94
|
+
).map((node) => node.textContent);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
describe.each(VARIANTS)('$name — theme breadcrumb separator', (variant) => {
|
|
98
|
+
it('uses the theme separator when the block sets none', () => {
|
|
99
|
+
const seen = separators(variant, {}, { breadcrumbSeparator: '›' });
|
|
100
|
+
|
|
101
|
+
expect(seen.length).toBeGreaterThan(0);
|
|
102
|
+
expect(seen.every((text) => text === '›')).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('keeps a stored separator, including the old default "/"', () => {
|
|
106
|
+
const settings = { breadcrumbSeparator: '›' };
|
|
107
|
+
|
|
108
|
+
expect(
|
|
109
|
+
separators(variant, { separator: { desktop: '/' } }, settings)
|
|
110
|
+
).toEqual(expect.arrayContaining(['/']));
|
|
111
|
+
expect(
|
|
112
|
+
separators(variant, { separator: { desktop: '/' } }, settings).includes(
|
|
113
|
+
'›'
|
|
114
|
+
)
|
|
115
|
+
).toBe(false);
|
|
116
|
+
expect(separators(variant, { separator: '|' }, settings)[0]).toBe('|');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('keeps a stored empty separator empty', () => {
|
|
120
|
+
const seen = separators(
|
|
121
|
+
variant,
|
|
122
|
+
{ separator: { desktop: '' } },
|
|
123
|
+
{ breadcrumbSeparator: '›' }
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(seen.length).toBeGreaterThan(0);
|
|
127
|
+
expect(seen.every((text) => text === '')).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('falls back to "/" with no theme separator', () => {
|
|
131
|
+
expect(separators(variant, {}, null)[0]).toBe('/');
|
|
132
|
+
expect(separators(variant, {}, {})[0]).toBe('/');
|
|
133
|
+
expect(separators(variant, {}, { breadcrumbSeparator: '' })[0]).toBe('/');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('ignores a theme separator that is not a separator', () => {
|
|
137
|
+
// Too long, blank, or not a string: none of them may reach the trail.
|
|
138
|
+
for (const breadcrumbSeparator of ['a very long separator', ' ', 42]) {
|
|
139
|
+
expect(separators(variant, {}, { breadcrumbSeparator })[0]).toBe('/');
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('trims the theme separator', () => {
|
|
144
|
+
expect(separators(variant, {}, { breadcrumbSeparator: ' > ' })[0]).toBe(
|
|
145
|
+
'>'
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
});
|