@akinon/pz-theme 2.0.37-rc.0 → 2.0.37
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 -31
- package/package.json +4 -4
- package/src/__tests__/__snapshots__/facet-filters-block.test.tsx.snap +11 -0
- package/src/__tests__/custom-css.test.ts +181 -0
- package/src/__tests__/designer-chrome-clipping.test.tsx +497 -0
- package/src/__tests__/facet-filters-block.test.tsx +707 -0
- package/src/__tests__/item-template.test.tsx +184 -0
- package/src/__tests__/listing-controls.test.tsx +961 -0
- package/src/__tests__/part-style-scope.test.tsx +229 -0
- package/src/__tests__/part-styles.test.ts +311 -0
- package/src/__tests__/registry-parity.test.ts +42 -0
- package/src/__tests__/row-visibility.test.ts +672 -0
- package/src/__tests__/sort-select-page-cursor.test.tsx +336 -0
- package/src/__tests__/theme-block-html-id.test.tsx +167 -0
- package/src/blocks/__tests__/__snapshots__/product-gallery-block.test.tsx.snap +15 -0
- package/src/blocks/__tests__/accordion-block-state.test.tsx +201 -0
- package/src/blocks/__tests__/facet-filters-block.test.tsx +190 -0
- package/src/blocks/__tests__/listing-context-card-model.test.tsx +230 -0
- package/src/blocks/__tests__/listing-lookup-hardening.test.ts +80 -0
- package/src/blocks/__tests__/product-gallery-block.test.tsx +974 -0
- package/src/blocks/__tests__/product-grid-composition.test.tsx +570 -0
- package/src/blocks/__tests__/product-grid-lookups.test.tsx +230 -0
- package/src/blocks/__tests__/product-price-block.test.tsx +248 -0
- package/src/blocks/__tests__/product-recommendations-block.test.tsx +702 -0
- package/src/blocks/__tests__/share-block.test.tsx +392 -0
- package/src/blocks/__tests__/store-locator-block.test.tsx +691 -0
- package/src/blocks/__tests__/variant-selector-block.test.tsx +817 -0
- package/src/blocks/__tests__/variant-selector-golden.json +20 -0
- package/src/blocks/account-menu-block.tsx +264 -0
- package/src/blocks/add-to-cart-block.tsx +417 -0
- package/src/blocks/address-list-block.tsx +348 -0
- package/src/blocks/applied-filters-block.tsx +288 -0
- package/src/blocks/auth-form-block.tsx +295 -0
- package/src/blocks/basket-items-block.tsx +457 -0
- package/src/blocks/basket-summary-block.tsx +404 -0
- package/src/blocks/block-renderer-registry.tsx +94 -0
- package/src/blocks/change-password-form-block.tsx +240 -0
- package/src/blocks/checkout-steps-block.tsx +288 -0
- package/src/blocks/checkout-summary-block.tsx +449 -0
- package/src/blocks/countdown-block.tsx +182 -0
- package/src/blocks/coupon-list-block.tsx +292 -0
- package/src/blocks/facet-filters-block.tsx +1686 -0
- package/src/blocks/faq-accordion-block.tsx +358 -0
- package/src/blocks/favorite-button-block.tsx +252 -0
- package/src/blocks/favourites-grid-block.tsx +510 -0
- package/src/blocks/forgot-password-block.tsx +155 -0
- package/src/blocks/item-template-utils.ts +131 -0
- package/src/blocks/item-template.tsx +164 -0
- package/src/blocks/listing-breadcrumb-block.tsx +175 -0
- package/src/blocks/listing-context.ts +850 -0
- package/src/blocks/listing-pagination-block.tsx +923 -0
- package/src/blocks/listing-result-count-block.tsx +126 -0
- package/src/blocks/listing-title-block.tsx +115 -0
- package/src/blocks/oauth-buttons-block.tsx +205 -0
- package/src/blocks/order-detail-block.tsx +474 -0
- package/src/blocks/order-list-block.tsx +274 -0
- package/src/blocks/order-success-block.tsx +641 -0
- package/src/blocks/order-tracking-block.tsx +533 -0
- package/src/blocks/product-attributes-block.tsx +307 -0
- package/src/blocks/product-block-utils.tsx +639 -0
- package/src/blocks/product-breadcrumb-block.tsx +215 -0
- package/src/blocks/product-description-block.tsx +169 -0
- package/src/blocks/product-gallery-block.tsx +1523 -0
- package/src/blocks/product-grid-block.tsx +966 -0
- package/src/blocks/product-price-block.tsx +373 -0
- package/src/blocks/product-recommendations-block.tsx +1433 -0
- package/src/blocks/product-title-block.tsx +129 -0
- package/src/blocks/profile-form-block.tsx +297 -0
- package/src/blocks/recently-viewed-store.ts +201 -0
- package/src/blocks/register-form-block.tsx +362 -0
- package/src/blocks/share-block.tsx +667 -0
- package/src/blocks/size-guide-block.tsx +313 -0
- package/src/blocks/sort-select-block.tsx +636 -0
- package/src/blocks/stock-status-block.tsx +168 -0
- package/src/blocks/store-locator-block.tsx +1184 -0
- package/src/blocks/variant-selector-block.tsx +1110 -0
- package/src/sections/__tests__/tablet-raw-bucket-readers.test.tsx +186 -0
- package/src/utils/__tests__/part-ownership-guard.test.tsx +29 -4
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
const mockPush = jest.fn();
|
|
5
|
+
let mockQuery = '';
|
|
6
|
+
|
|
7
|
+
jest.mock('next/navigation', () => ({
|
|
8
|
+
useRouter: () => ({ push: mockPush }),
|
|
9
|
+
usePathname: () => '/list',
|
|
10
|
+
useSearchParams: () => new URLSearchParams(mockQuery)
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import FacetFiltersBlock from '../blocks/facet-filters-block';
|
|
14
|
+
import SortSelectBlock from '../blocks/sort-select-block';
|
|
15
|
+
import { ThemePageContextProvider } from '../theme-page-context';
|
|
16
|
+
|
|
17
|
+
type Choice = { label: string; value: string; selected?: boolean };
|
|
18
|
+
|
|
19
|
+
const facet = (
|
|
20
|
+
key: string,
|
|
21
|
+
name: string,
|
|
22
|
+
choices: Choice[],
|
|
23
|
+
searchKey = `attributes_${key}`
|
|
24
|
+
) => ({
|
|
25
|
+
key,
|
|
26
|
+
name,
|
|
27
|
+
search_key: searchKey,
|
|
28
|
+
widget_type: 'multiselect',
|
|
29
|
+
data: {
|
|
30
|
+
choices: choices.map((choice) => ({
|
|
31
|
+
label: choice.label,
|
|
32
|
+
value: choice.value,
|
|
33
|
+
quantity: 5,
|
|
34
|
+
is_selected: choice.selected ?? false
|
|
35
|
+
}))
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const COLOR = facet('color', 'Renk', [
|
|
40
|
+
{ label: 'Siyah', value: 'black' },
|
|
41
|
+
{ label: 'Beyaz', value: 'white' }
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const SIZE = facet('size', 'Beden', [
|
|
45
|
+
{ label: 'S', value: 's' },
|
|
46
|
+
{ label: 'M', value: 'm' }
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const SORTERS = [
|
|
50
|
+
{ label: 'Newest', value: 'newest', is_selected: false },
|
|
51
|
+
{ label: 'Price low to high', value: 'price_asc', is_selected: true },
|
|
52
|
+
{ label: 'Price high to low', value: 'price_desc', is_selected: false }
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const pageContext = (facets: unknown[] = [COLOR, SIZE]) => ({
|
|
56
|
+
listing: {
|
|
57
|
+
title: 'Shoes',
|
|
58
|
+
path: '/list',
|
|
59
|
+
query: '',
|
|
60
|
+
products: [],
|
|
61
|
+
facets,
|
|
62
|
+
sorters: SORTERS,
|
|
63
|
+
pagination: {},
|
|
64
|
+
breadcrumbs: []
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const blockProps = (
|
|
69
|
+
type: string,
|
|
70
|
+
properties: Record<string, unknown>,
|
|
71
|
+
isDesigner = false
|
|
72
|
+
) =>
|
|
73
|
+
({
|
|
74
|
+
block: {
|
|
75
|
+
id: `${type}-1`,
|
|
76
|
+
type,
|
|
77
|
+
label: type,
|
|
78
|
+
order: 0,
|
|
79
|
+
hidden: false,
|
|
80
|
+
value: '',
|
|
81
|
+
properties,
|
|
82
|
+
styles: {},
|
|
83
|
+
blocks: []
|
|
84
|
+
},
|
|
85
|
+
placeholderId: 'ph',
|
|
86
|
+
sectionId: 'sec',
|
|
87
|
+
isDesigner
|
|
88
|
+
} as never);
|
|
89
|
+
|
|
90
|
+
const renderSort = (
|
|
91
|
+
properties: Record<string, unknown> = {},
|
|
92
|
+
facets: unknown[] = [COLOR, SIZE]
|
|
93
|
+
) =>
|
|
94
|
+
render(
|
|
95
|
+
<ThemePageContextProvider value={pageContext(facets)}>
|
|
96
|
+
<SortSelectBlock {...blockProps('sort-select', properties)} />
|
|
97
|
+
</ThemePageContextProvider>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
/** Changes the native `<select>`, which is the shipped control. */
|
|
101
|
+
const chooseSort = (container: HTMLElement, value: string) =>
|
|
102
|
+
fireEvent.change(container.querySelector('select') as HTMLSelectElement, {
|
|
103
|
+
target: { value }
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
mockPush.mockClear();
|
|
108
|
+
mockQuery = '';
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('sort — the page cursor it drops', () => {
|
|
112
|
+
it('drops a real cursor when the sort changes', () => {
|
|
113
|
+
mockQuery = 'page=4';
|
|
114
|
+
const { container } = renderSort();
|
|
115
|
+
|
|
116
|
+
chooseSort(container, 'price_desc');
|
|
117
|
+
|
|
118
|
+
expect(mockPush).toHaveBeenCalledWith('/list?sorter=price_desc');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `pageQueryParam` is free text, so a merchant can type a key one of this
|
|
123
|
+
* listing's facets already owns. Honouring that turns every "drop the
|
|
124
|
+
* cursor" into "drop that filter": the shopper reorders the results and
|
|
125
|
+
* silently loses the colour they were filtering by.
|
|
126
|
+
*/
|
|
127
|
+
describe('a page cursor a facet already owns', () => {
|
|
128
|
+
it('keeps the filter on the URL when the sort changes', () => {
|
|
129
|
+
mockQuery = 'attributes_color=black';
|
|
130
|
+
const { container } = renderSort({
|
|
131
|
+
pageQueryParam: 'attributes_color'
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
chooseSort(container, 'price_desc');
|
|
135
|
+
|
|
136
|
+
expect(mockPush).toHaveBeenCalledWith(
|
|
137
|
+
'/list?attributes_color=black&sorter=price_desc'
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('still drops the real cursor it fell back to', () => {
|
|
142
|
+
mockQuery = 'page=4&attributes_color=black';
|
|
143
|
+
const { container } = renderSort({
|
|
144
|
+
pageQueryParam: 'attributes_color'
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
chooseSort(container, 'price_desc');
|
|
148
|
+
|
|
149
|
+
expect(mockPush).toHaveBeenCalledWith(
|
|
150
|
+
'/list?attributes_color=black&sorter=price_desc'
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('drops nothing when a facet owns the fallback key too', () => {
|
|
155
|
+
// A listing whose own facet is called "page": there is no cursor left to
|
|
156
|
+
// fall back to, and leaving a stale cursor beats erasing a filter.
|
|
157
|
+
mockQuery = 'page=4&attributes_color=black';
|
|
158
|
+
const { container } = renderSort({ pageQueryParam: 'attributes_color' }, [
|
|
159
|
+
COLOR,
|
|
160
|
+
facet('page', 'Sayfa', [{ label: 'Dört', value: '4' }], 'page')
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
chooseSort(container, 'price_desc');
|
|
164
|
+
|
|
165
|
+
expect(mockPush).toHaveBeenCalledWith(
|
|
166
|
+
'/list?page=4&attributes_color=black&sorter=price_desc'
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('resolves the same key in the option panel', () => {
|
|
171
|
+
mockQuery = 'attributes_color=black';
|
|
172
|
+
renderSort({ variant: 'panel', pageQueryParam: 'attributes_color' });
|
|
173
|
+
|
|
174
|
+
fireEvent.click(screen.getByRole('button'));
|
|
175
|
+
fireEvent.click(screen.getByText('Newest'));
|
|
176
|
+
|
|
177
|
+
expect(mockPush).toHaveBeenCalledWith(
|
|
178
|
+
'/list?attributes_color=black&sorter=newest'
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Two controls on one page must answer "which key is the page cursor" the
|
|
185
|
+
* same way — a filter that survives the sort but not the next facet click
|
|
186
|
+
* is the same bug wearing the other block's clothes.
|
|
187
|
+
*/
|
|
188
|
+
it('agrees with the facet block about which key is the cursor', () => {
|
|
189
|
+
mockQuery = 'attributes_color=black';
|
|
190
|
+
const properties = { pageQueryParam: 'attributes_color' };
|
|
191
|
+
|
|
192
|
+
render(
|
|
193
|
+
<ThemePageContextProvider
|
|
194
|
+
value={pageContext([
|
|
195
|
+
facet('color', 'Renk', [
|
|
196
|
+
{ label: 'Siyah', value: 'black', selected: true },
|
|
197
|
+
{ label: 'Beyaz', value: 'white' }
|
|
198
|
+
]),
|
|
199
|
+
SIZE
|
|
200
|
+
])}
|
|
201
|
+
>
|
|
202
|
+
<SortSelectBlock {...blockProps('sort-select', properties)} />
|
|
203
|
+
<FacetFiltersBlock
|
|
204
|
+
{...blockProps('facet-filters', {
|
|
205
|
+
...properties,
|
|
206
|
+
layout: 'sidebar'
|
|
207
|
+
})}
|
|
208
|
+
/>
|
|
209
|
+
</ThemePageContextProvider>
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
fireEvent.click(screen.getByRole('button', { name: /^M/ }));
|
|
213
|
+
expect(mockPush).toHaveBeenLastCalledWith(
|
|
214
|
+
'/list?attributes_color=black&attributes_size=m'
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
chooseSort(document.body as HTMLElement, 'price_desc');
|
|
218
|
+
expect(mockPush).toHaveBeenLastCalledWith(
|
|
219
|
+
'/list?attributes_color=black&sorter=price_desc'
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('keeps every value of a multi-valued filter it refused to drop', () => {
|
|
224
|
+
mockQuery = 'attributes_color=black&attributes_color=white&page=3';
|
|
225
|
+
const { container } = renderSort({ pageQueryParam: 'attributes_color' });
|
|
226
|
+
|
|
227
|
+
chooseSort(container, 'newest');
|
|
228
|
+
|
|
229
|
+
const params = new URLSearchParams(
|
|
230
|
+
String(mockPush.mock.calls[0][0]).split('?')[1]
|
|
231
|
+
);
|
|
232
|
+
expect(params.getAll('attributes_color')).toEqual(['black', 'white']);
|
|
233
|
+
expect(params.get('page')).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('protects a facet that has no search_key of its own', () => {
|
|
237
|
+
// The listing normaliser falls back search_key -> key, so this facet's URL
|
|
238
|
+
// key is the bare "color" — the refusal has to see it under that name too.
|
|
239
|
+
mockQuery = 'color=black';
|
|
240
|
+
const { container } = renderSort({ pageQueryParam: 'color' }, [
|
|
241
|
+
{
|
|
242
|
+
key: 'color',
|
|
243
|
+
name: 'Renk',
|
|
244
|
+
widget_type: 'multiselect',
|
|
245
|
+
data: {
|
|
246
|
+
choices: [
|
|
247
|
+
{ label: 'Siyah', value: 'black', quantity: 5, is_selected: true }
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
chooseSort(container, 'price_desc');
|
|
254
|
+
|
|
255
|
+
expect(mockPush).toHaveBeenCalledWith('/list?color=black&sorter=price_desc');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('resolves per breakpoint rather than once for the block', () => {
|
|
259
|
+
mockQuery = 'attributes_color=black&page=2';
|
|
260
|
+
const { container } = render(
|
|
261
|
+
<ThemePageContextProvider value={pageContext()}>
|
|
262
|
+
<SortSelectBlock
|
|
263
|
+
{...({
|
|
264
|
+
...(blockProps('sort-select', {
|
|
265
|
+
pageQueryParam: { mobile: 'attributes_color', desktop: 'page' }
|
|
266
|
+
}) as object),
|
|
267
|
+
currentBreakpoint: 'mobile'
|
|
268
|
+
} as never)}
|
|
269
|
+
/>
|
|
270
|
+
</ThemePageContextProvider>
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
chooseSort(container, 'price_desc');
|
|
274
|
+
|
|
275
|
+
// Mobile named a facet, so mobile falls back to "page" — the colour has to
|
|
276
|
+
// survive there even though the desktop value would have been honoured.
|
|
277
|
+
expect(mockPush).toHaveBeenCalledWith(
|
|
278
|
+
'/list?attributes_color=black&sorter=price_desc'
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* `queryParam` is free text as well, and the sort is WRITTEN into it rather
|
|
285
|
+
* than deleted: a merchant who types a facet's own `search_key` here turns
|
|
286
|
+
* every sort change into `?attributes_color=price_desc` — the shopper's colour
|
|
287
|
+
* replaced by a sorter value commerce reads as a colour, so the grid empties.
|
|
288
|
+
*/
|
|
289
|
+
describe('sort — the key it writes into', () => {
|
|
290
|
+
it('writes into the shipped key when a facet owns the typed one', () => {
|
|
291
|
+
mockQuery = 'attributes_color=black&page=2';
|
|
292
|
+
const { container } = renderSort({ queryParam: 'attributes_color' });
|
|
293
|
+
|
|
294
|
+
chooseSort(container, 'price_desc');
|
|
295
|
+
|
|
296
|
+
expect(mockPush).toHaveBeenCalledWith(
|
|
297
|
+
'/list?attributes_color=black&sorter=price_desc'
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('honours a custom sort key no facet owns', () => {
|
|
302
|
+
mockQuery = 'attributes_color=black';
|
|
303
|
+
const { container } = renderSort({ queryParam: 'order_by' });
|
|
304
|
+
|
|
305
|
+
chooseSort(container, 'newest');
|
|
306
|
+
|
|
307
|
+
expect(mockPush).toHaveBeenCalledWith(
|
|
308
|
+
'/list?attributes_color=black&order_by=newest'
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('leaves the typed key alone on a listing that facets on "sorter"', () => {
|
|
313
|
+
// No "write nothing" option exists for the sort, so when the fallback is
|
|
314
|
+
// claimed too the merchant's key stands rather than being sent elsewhere.
|
|
315
|
+
mockQuery = '';
|
|
316
|
+
const { container } = renderSort({ queryParam: 'sorter' }, [
|
|
317
|
+
COLOR,
|
|
318
|
+
facet('sorter', 'Sıralama', [{ label: 'Newest', value: 'newest' }], 'sorter')
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
chooseSort(container, 'newest');
|
|
322
|
+
|
|
323
|
+
expect(mockPush).toHaveBeenCalledWith('/list?sorter=newest');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('never pushes a nameless parameter when a breakpoint is cleared', () => {
|
|
327
|
+
// A bare '' falls back inside getResponsiveValue, but a per-breakpoint ''
|
|
328
|
+
// reaches the block verbatim — and `params.set('', …)` writes "?=newest".
|
|
329
|
+
mockQuery = '';
|
|
330
|
+
const { container } = renderSort({ queryParam: { desktop: '' } });
|
|
331
|
+
|
|
332
|
+
chooseSort(container, 'newest');
|
|
333
|
+
|
|
334
|
+
expect(mockPush).toHaveBeenCalledWith('/list?sorter=newest');
|
|
335
|
+
});
|
|
336
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
// A trivial renderer for every type, so the test observes the wrapper ThemeBlock
|
|
5
|
+
// builds rather than any particular block's markup.
|
|
6
|
+
jest.mock('../blocks/block-renderer-registry', () => ({
|
|
7
|
+
__esModule: true,
|
|
8
|
+
default: {
|
|
9
|
+
getRenderer: () => () => <p data-testid="rendered">R</p>,
|
|
10
|
+
hasRenderer: () => true
|
|
11
|
+
}
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
import ThemeBlock, { sanitizeHtmlId } from '../theme-block';
|
|
15
|
+
import { WithDesignerFeatures } from '../components/with-designer-features';
|
|
16
|
+
|
|
17
|
+
const blockOf = (properties: Record<string, unknown> = {}) => ({
|
|
18
|
+
id: 'blk-1',
|
|
19
|
+
type: 'group',
|
|
20
|
+
label: 'Card',
|
|
21
|
+
properties,
|
|
22
|
+
styles: {},
|
|
23
|
+
order: 0,
|
|
24
|
+
hidden: false
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const props = (over: Record<string, unknown> = {}) =>
|
|
28
|
+
({
|
|
29
|
+
block: blockOf(),
|
|
30
|
+
placeholderId: 'p',
|
|
31
|
+
sectionId: 's',
|
|
32
|
+
isDesigner: false,
|
|
33
|
+
...over
|
|
34
|
+
} as any);
|
|
35
|
+
|
|
36
|
+
describe('ThemeBlock — html id is absent by default', () => {
|
|
37
|
+
/**
|
|
38
|
+
* The proof that nothing rendering today changes: the control below is
|
|
39
|
+
* literally the render tree ThemeBlock had before the id existed — the
|
|
40
|
+
* designer wrapper with the block renderer as its only child.
|
|
41
|
+
*/
|
|
42
|
+
it('renders byte-identically to the pre-change tree when no id is set', () => {
|
|
43
|
+
const { container } = render(<ThemeBlock {...props()} />);
|
|
44
|
+
|
|
45
|
+
const control = render(
|
|
46
|
+
<WithDesignerFeatures
|
|
47
|
+
block={blockOf()}
|
|
48
|
+
placeholderId="p"
|
|
49
|
+
sectionId="s"
|
|
50
|
+
isDesigner={false}
|
|
51
|
+
isSelected={false}
|
|
52
|
+
currentBreakpoint="desktop"
|
|
53
|
+
>
|
|
54
|
+
<p data-testid="rendered">R</p>
|
|
55
|
+
</WithDesignerFeatures>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(container.innerHTML).toBe(control.container.innerHTML);
|
|
59
|
+
expect(container.innerHTML).toBe(
|
|
60
|
+
'<div data-block-id="blk-1" class="relative"><p data-testid="rendered">R</p></div>'
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('emits no anchor and no id attribute at all', () => {
|
|
65
|
+
const { container } = render(<ThemeBlock {...props()} />);
|
|
66
|
+
|
|
67
|
+
expect(container.querySelector('[data-theme-anchor]')).toBeNull();
|
|
68
|
+
expect(container.querySelector('[id]')).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('emits nothing when the id sanitises down to nothing', () => {
|
|
72
|
+
const { container } = render(
|
|
73
|
+
<ThemeBlock {...props({ block: blockOf({ htmlId: '123' }) })} />
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(container.querySelector('[data-theme-anchor]')).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('ThemeBlock — html id when authored', () => {
|
|
81
|
+
it('gives a jump link something to point at', () => {
|
|
82
|
+
const { container } = render(
|
|
83
|
+
<ThemeBlock {...props({ block: blockOf({ htmlId: 'reviews' }) })} />
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const anchor = container.querySelector('#reviews') as HTMLElement;
|
|
87
|
+
expect(anchor).toBeInTheDocument();
|
|
88
|
+
expect(anchor).toHaveAttribute('data-theme-anchor');
|
|
89
|
+
expect(anchor).toHaveAttribute('aria-hidden', 'true');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('keeps the anchor out of flow so the block does not move', () => {
|
|
93
|
+
const { container } = render(
|
|
94
|
+
<ThemeBlock {...props({ block: blockOf({ htmlId: 'reviews' }) })} />
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const anchor = container.querySelector('#reviews') as HTMLElement;
|
|
98
|
+
expect(anchor.style.position).toBe('absolute');
|
|
99
|
+
expect(anchor.style.width).toBe('0px');
|
|
100
|
+
expect(anchor.style.height).toBe('0px');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('leaves the block markup itself untouched', () => {
|
|
104
|
+
const withId = render(
|
|
105
|
+
<ThemeBlock {...props({ block: blockOf({ htmlId: 'reviews' }) })} />
|
|
106
|
+
);
|
|
107
|
+
const rendered = withId.container.querySelector('[data-testid="rendered"]');
|
|
108
|
+
|
|
109
|
+
expect(rendered).toBeInTheDocument();
|
|
110
|
+
expect(rendered?.outerHTML).toBe('<p data-testid="rendered">R</p>');
|
|
111
|
+
// The anchor is a sibling of the block, never a wrapper around it.
|
|
112
|
+
expect(rendered?.parentElement).toHaveAttribute('data-block-id', 'blk-1');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('reads the id per breakpoint like every other property', () => {
|
|
116
|
+
const { container } = render(
|
|
117
|
+
<ThemeBlock
|
|
118
|
+
{...props({
|
|
119
|
+
block: blockOf({ htmlId: { desktop: 'reviews', mobile: 'r-m' } }),
|
|
120
|
+
currentBreakpoint: 'mobile'
|
|
121
|
+
})}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(container.querySelector('#r-m')).toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('renders the same anchor in the designer (preview parity)', () => {
|
|
129
|
+
const { container } = render(
|
|
130
|
+
<ThemeBlock
|
|
131
|
+
{...props({ block: blockOf({ htmlId: 'reviews' }), isDesigner: true })}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(container.querySelector('#reviews')).toBeInTheDocument();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('sanitizeHtmlId', () => {
|
|
140
|
+
it('passes a well-formed id through unchanged', () => {
|
|
141
|
+
expect(sanitizeHtmlId('size-guide_2')).toBe('size-guide_2');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('turns spaces into hyphens rather than dropping the words together', () => {
|
|
145
|
+
expect(sanitizeHtmlId(' Size guide ')).toBe('Size-guide');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('strips characters that would break a selector or a fragment', () => {
|
|
149
|
+
expect(sanitizeHtmlId('re#vie.ws:1/2?x"y')).toBe('reviews12xy');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('refuses a leading digit', () => {
|
|
153
|
+
expect(sanitizeHtmlId('3-reasons')).toBe('reasons');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('returns nothing for a value with nothing usable left', () => {
|
|
157
|
+
expect(sanitizeHtmlId('###')).toBe('');
|
|
158
|
+
expect(sanitizeHtmlId('42')).toBe('');
|
|
159
|
+
expect(sanitizeHtmlId('')).toBe('');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('ignores a non-string value', () => {
|
|
163
|
+
expect(sanitizeHtmlId(undefined)).toBe('');
|
|
164
|
+
expect(sanitizeHtmlId(7)).toBe('');
|
|
165
|
+
expect(sanitizeHtmlId({ desktop: 'x' })).toBe('');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`ProductGalleryBlock — golden default render renders arrows, dots and the counter unchanged 1`] = `"<div data-product-gallery="" data-product-gallery-thumb-position="bottom" style="display: flex; flex-direction: column; gap: 8px; width: 100%;"><div data-product-gallery-main="" data-part="main" style="position: relative; width: 100%; overflow: hidden;"><img alt="front" data-part="image" loading="eager" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/a.jpg"><button type="button" data-product-gallery-arrow="prev" data-part="arrow" aria-label="Previous image" style="position: absolute; top: 50%; left: 8px; transform: translateY(-50%); display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0px; border-radius: 50%; cursor: pointer;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"></path></svg></button><button type="button" data-product-gallery-arrow="next" data-part="arrow" aria-label="Next image" style="position: absolute; top: 50%; right: 8px; transform: translateY(-50%); display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0px; border-radius: 50%; cursor: pointer;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 5l7 7-7 7"></path></svg></button></div><div data-product-gallery-dots="" data-part="dot-list" style="display: flex; align-items: center; justify-content: center; gap: 6px;"><button type="button" data-product-gallery-dot="active" data-part="dot" data-state="selected" aria-label="Go to image 1" aria-current="true" style="width: 8px; height: 8px; padding: 0px; border-radius: 50%; cursor: pointer; background: currentColor;"></button><button type="button" data-product-gallery-dot="default" data-part="dot" aria-label="Go to image 2" aria-current="false" style="width: 8px; height: 8px; padding: 0px; border-radius: 50%; cursor: pointer;"></button><button type="button" data-product-gallery-dot="default" data-part="dot" aria-label="Go to image 3" aria-current="false" style="width: 8px; height: 8px; padding: 0px; border-radius: 50%; cursor: pointer;"></button></div><div data-product-gallery-counter="" data-part="counter" aria-live="polite" aria-atomic="true" style="text-align: center;">1 / 3</div><div data-product-gallery-thumbs="" data-part="thumb-list" style="display: flex; flex-direction: row; gap: 8px; overflow-x: auto; overflow-y: visible; flex-wrap: nowrap;"><button type="button" data-product-gallery-thumb="active" data-part="thumb" data-state="selected" aria-label="Wool Coat image 1" aria-current="true" style="flex: 0 0 auto; width: 64px; height: 64px; padding: 0px; cursor: pointer; overflow: hidden; background: transparent;"><img alt="" loading="lazy" data-part="thumb-image" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/a.jpg"></button><button type="button" data-product-gallery-thumb="default" data-part="thumb" aria-label="Wool Coat image 2" aria-current="false" style="flex: 0 0 auto; width: 64px; height: 64px; padding: 0px; cursor: pointer; overflow: hidden; background: transparent;"><img alt="" loading="lazy" data-part="thumb-image" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/b.jpg"></button><button type="button" data-product-gallery-thumb="default" data-part="thumb" aria-label="Wool Coat image 3" aria-current="false" style="flex: 0 0 auto; width: 64px; height: 64px; padding: 0px; cursor: pointer; overflow: hidden; background: transparent;"><img alt="" loading="lazy" data-part="thumb-image" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/c.jpg"></button></div></div>"`;
|
|
4
|
+
|
|
5
|
+
exports[`ProductGalleryBlock — golden default render renders the carousel exactly as it did before zoom existed 1`] = `"<div data-product-gallery="" data-product-gallery-thumb-position="bottom" style="display: flex; flex-direction: column; gap: 8px; width: 100%;"><div data-product-gallery-main="" data-part="main" style="width: 100%; overflow: hidden;"><img alt="front" data-part="image" loading="eager" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/a.jpg"></div><div data-product-gallery-thumbs="" data-part="thumb-list" style="display: flex; flex-direction: row; gap: 8px; overflow-x: auto; overflow-y: visible; flex-wrap: nowrap;"><button type="button" data-product-gallery-thumb="active" data-part="thumb" data-state="selected" aria-label="Wool Coat image 1" aria-current="true" style="flex: 0 0 auto; width: 64px; height: 64px; padding: 0px; cursor: pointer; overflow: hidden; background: transparent;"><img alt="" loading="lazy" data-part="thumb-image" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/a.jpg"></button><button type="button" data-product-gallery-thumb="default" data-part="thumb" aria-label="Wool Coat image 2" aria-current="false" style="flex: 0 0 auto; width: 64px; height: 64px; padding: 0px; cursor: pointer; overflow: hidden; background: transparent;"><img alt="" loading="lazy" data-part="thumb-image" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/b.jpg"></button><button type="button" data-product-gallery-thumb="default" data-part="thumb" aria-label="Wool Coat image 3" aria-current="false" style="flex: 0 0 auto; width: 64px; height: 64px; padding: 0px; cursor: pointer; overflow: hidden; background: transparent;"><img alt="" loading="lazy" data-part="thumb-image" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/c.jpg"></button></div></div>"`;
|
|
6
|
+
|
|
7
|
+
exports[`ProductGalleryBlock — golden default render renders the designer carousel unchanged 1`] = `"<div data-product-gallery="" data-product-gallery-thumb-position="bottom" style="display: flex; flex-direction: column; gap: 8px; width: 100%;"><div data-product-gallery-main="" data-part="main" style="width: 100%; overflow: hidden;"><img alt="front" data-part="image" loading="eager" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/a.jpg"></div><div data-product-gallery-thumbs="" data-part="thumb-list" style="display: flex; flex-direction: row; gap: 8px; overflow-x: auto; overflow-y: visible; flex-wrap: nowrap;"><button type="button" data-product-gallery-thumb="active" data-part="thumb" data-state="selected" aria-label="Wool Coat image 1" aria-current="true" style="flex: 0 0 auto; width: 64px; height: 64px; padding: 0px; cursor: pointer; overflow: hidden; background: transparent;"><img alt="" loading="lazy" data-part="thumb-image" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/a.jpg"></button><button type="button" data-product-gallery-thumb="default" data-part="thumb" aria-label="Wool Coat image 2" aria-current="false" style="flex: 0 0 auto; width: 64px; height: 64px; padding: 0px; cursor: pointer; overflow: hidden; background: transparent;"><img alt="" loading="lazy" data-part="thumb-image" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/b.jpg"></button><button type="button" data-product-gallery-thumb="default" data-part="thumb" aria-label="Wool Coat image 3" aria-current="false" style="flex: 0 0 auto; width: 64px; height: 64px; padding: 0px; cursor: pointer; overflow: hidden; background: transparent;"><img alt="" loading="lazy" data-part="thumb-image" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/c.jpg"></button></div></div>"`;
|
|
8
|
+
|
|
9
|
+
exports[`ProductGalleryBlock — golden default render renders the designer placeholder when the product has no photos 1`] = `"<span data-theme-block-placeholder="" data-product-gallery="" style="display: inline-block; padding: 6px 10px; border: 1px dashed #d1d5db; border-radius: 4px; color: rgb(107, 114, 128); font-size: 12px; line-height: 1.4;">Product gallery (no product images)</span>"`;
|
|
10
|
+
|
|
11
|
+
exports[`ProductGalleryBlock — golden default render renders the grid layout unchanged 1`] = `"<div data-product-gallery="" data-product-gallery-layout="grid" data-product-gallery-id="gallery" style="width: 100%;"><style>[data-product-gallery-id="gallery"] [data-part="grid"]{grid-template-columns:repeat(2,minmax(0,1fr));}@media (max-width:1023px){[data-product-gallery-id="gallery"] [data-part="grid"]{grid-template-columns:repeat(1,minmax(0,1fr));}}</style><div data-part="grid" style="display: grid; gap: 8px; width: 100%;"><div data-product-gallery-cell="active" data-part="main" data-state="selected" style="overflow: hidden;"><img alt="front" data-part="image" loading="eager" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/a.jpg"></div><div data-product-gallery-cell="default" data-part="main" style="overflow: hidden;"><img alt="back" data-part="image" loading="eager" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/b.jpg"></div><div data-product-gallery-cell="default" data-part="main" style="overflow: hidden;"><img alt="Wool Coat" data-part="image" loading="lazy" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/c.jpg"></div></div></div>"`;
|
|
12
|
+
|
|
13
|
+
exports[`ProductGalleryBlock — golden default render renders the side-thumbnail arrangement unchanged 1`] = `"<div data-product-gallery="" data-product-gallery-thumb-position="left" style="display: flex; flex-direction: row-reverse; align-items: flex-start; gap: 8px; width: 100%;"><div data-product-gallery-main="" data-part="main" style="flex: 1 1 auto; min-width: 0; overflow: hidden;"><img alt="front" data-part="image" loading="eager" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/a.jpg"></div><div data-product-gallery-thumbs="" data-part="thumb-list" style="display: flex; flex-direction: column; gap: 8px; overflow-x: visible; overflow-y: auto; flex-wrap: nowrap;"><button type="button" data-product-gallery-thumb="active" data-part="thumb" data-state="selected" aria-label="Wool Coat image 1" aria-current="true" style="flex: 0 0 auto; width: 80px; height: 80px; padding: 0px; cursor: pointer; overflow: hidden; background: transparent;"><img alt="" loading="lazy" data-part="thumb-image" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/a.jpg"></button><button type="button" data-product-gallery-thumb="default" data-part="thumb" aria-label="Wool Coat image 2" aria-current="false" style="flex: 0 0 auto; width: 80px; height: 80px; padding: 0px; cursor: pointer; overflow: hidden; background: transparent;"><img alt="" loading="lazy" data-part="thumb-image" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/b.jpg"></button></div></div>"`;
|
|
14
|
+
|
|
15
|
+
exports[`ProductGalleryBlock — golden default render renders the single-image product unchanged 1`] = `"<div data-product-gallery="" data-product-gallery-thumb-position="bottom" style="display: flex; flex-direction: column; gap: 8px; width: 100%;"><div data-product-gallery-main="" data-part="main" style="width: 100%; overflow: hidden;"><img alt="Product" data-part="image" loading="eager" style="display: block; width: 100%; height: 100%; object-fit: cover;" src="https://cdn.example.com/only.jpg"></div></div>"`;
|