@akinon/pz-theme 2.0.37-rc.0 → 2.0.38-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.
Files changed (78) hide show
  1. package/CHANGELOG.md +15 -31
  2. package/package.json +4 -4
  3. package/src/__tests__/__snapshots__/facet-filters-block.test.tsx.snap +11 -0
  4. package/src/__tests__/custom-css.test.ts +181 -0
  5. package/src/__tests__/designer-chrome-clipping.test.tsx +497 -0
  6. package/src/__tests__/facet-filters-block.test.tsx +707 -0
  7. package/src/__tests__/item-template.test.tsx +184 -0
  8. package/src/__tests__/listing-controls.test.tsx +961 -0
  9. package/src/__tests__/part-style-scope.test.tsx +229 -0
  10. package/src/__tests__/part-styles.test.ts +311 -0
  11. package/src/__tests__/registry-parity.test.ts +42 -0
  12. package/src/__tests__/row-visibility.test.ts +672 -0
  13. package/src/__tests__/sort-select-page-cursor.test.tsx +336 -0
  14. package/src/__tests__/theme-block-html-id.test.tsx +167 -0
  15. package/src/blocks/__tests__/__snapshots__/product-gallery-block.test.tsx.snap +15 -0
  16. package/src/blocks/__tests__/accordion-block-state.test.tsx +201 -0
  17. package/src/blocks/__tests__/facet-filters-block.test.tsx +190 -0
  18. package/src/blocks/__tests__/listing-context-card-model.test.tsx +230 -0
  19. package/src/blocks/__tests__/listing-lookup-hardening.test.ts +80 -0
  20. package/src/blocks/__tests__/product-gallery-block.test.tsx +974 -0
  21. package/src/blocks/__tests__/product-grid-composition.test.tsx +570 -0
  22. package/src/blocks/__tests__/product-grid-lookups.test.tsx +230 -0
  23. package/src/blocks/__tests__/product-price-block.test.tsx +248 -0
  24. package/src/blocks/__tests__/product-recommendations-block.test.tsx +702 -0
  25. package/src/blocks/__tests__/share-block.test.tsx +392 -0
  26. package/src/blocks/__tests__/store-locator-block.test.tsx +691 -0
  27. package/src/blocks/__tests__/variant-selector-block.test.tsx +817 -0
  28. package/src/blocks/__tests__/variant-selector-golden.json +20 -0
  29. package/src/blocks/account-menu-block.tsx +264 -0
  30. package/src/blocks/add-to-cart-block.tsx +417 -0
  31. package/src/blocks/address-list-block.tsx +348 -0
  32. package/src/blocks/applied-filters-block.tsx +288 -0
  33. package/src/blocks/auth-form-block.tsx +295 -0
  34. package/src/blocks/basket-items-block.tsx +457 -0
  35. package/src/blocks/basket-summary-block.tsx +404 -0
  36. package/src/blocks/block-renderer-registry.tsx +94 -0
  37. package/src/blocks/change-password-form-block.tsx +240 -0
  38. package/src/blocks/checkout-steps-block.tsx +288 -0
  39. package/src/blocks/checkout-summary-block.tsx +449 -0
  40. package/src/blocks/countdown-block.tsx +182 -0
  41. package/src/blocks/coupon-list-block.tsx +292 -0
  42. package/src/blocks/facet-filters-block.tsx +1686 -0
  43. package/src/blocks/faq-accordion-block.tsx +358 -0
  44. package/src/blocks/favorite-button-block.tsx +252 -0
  45. package/src/blocks/favourites-grid-block.tsx +510 -0
  46. package/src/blocks/forgot-password-block.tsx +155 -0
  47. package/src/blocks/item-template-utils.ts +131 -0
  48. package/src/blocks/item-template.tsx +164 -0
  49. package/src/blocks/listing-breadcrumb-block.tsx +175 -0
  50. package/src/blocks/listing-context.ts +850 -0
  51. package/src/blocks/listing-pagination-block.tsx +923 -0
  52. package/src/blocks/listing-result-count-block.tsx +126 -0
  53. package/src/blocks/listing-title-block.tsx +115 -0
  54. package/src/blocks/oauth-buttons-block.tsx +205 -0
  55. package/src/blocks/order-detail-block.tsx +474 -0
  56. package/src/blocks/order-list-block.tsx +274 -0
  57. package/src/blocks/order-success-block.tsx +641 -0
  58. package/src/blocks/order-tracking-block.tsx +533 -0
  59. package/src/blocks/product-attributes-block.tsx +307 -0
  60. package/src/blocks/product-block-utils.tsx +639 -0
  61. package/src/blocks/product-breadcrumb-block.tsx +215 -0
  62. package/src/blocks/product-description-block.tsx +169 -0
  63. package/src/blocks/product-gallery-block.tsx +1523 -0
  64. package/src/blocks/product-grid-block.tsx +966 -0
  65. package/src/blocks/product-price-block.tsx +373 -0
  66. package/src/blocks/product-recommendations-block.tsx +1433 -0
  67. package/src/blocks/product-title-block.tsx +129 -0
  68. package/src/blocks/profile-form-block.tsx +297 -0
  69. package/src/blocks/recently-viewed-store.ts +201 -0
  70. package/src/blocks/register-form-block.tsx +362 -0
  71. package/src/blocks/share-block.tsx +667 -0
  72. package/src/blocks/size-guide-block.tsx +313 -0
  73. package/src/blocks/sort-select-block.tsx +636 -0
  74. package/src/blocks/stock-status-block.tsx +168 -0
  75. package/src/blocks/store-locator-block.tsx +1184 -0
  76. package/src/blocks/variant-selector-block.tsx +1110 -0
  77. package/src/sections/__tests__/tablet-raw-bucket-readers.test.tsx +186 -0
  78. 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>"`;