@akinon/pz-theme 2.0.37-beta.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +4 -4
  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,230 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import ProductGridBlock from '../product-grid-block';
4
+ import { ThemePageContextProvider } from '../../theme-page-context';
5
+
6
+ /**
7
+ * The lookup table the grid hands to the row normaliser.
8
+ *
9
+ * `item.badge.*` and `item.colors[0].color` are offered in the binding picker,
10
+ * but commerce sends a badge as a bare key ("new_season") and a colour as a
11
+ * bare value ("kirmizi") — neither carries a label, an image or a hex. The
12
+ * normaliser has always been able to resolve them through an author's table;
13
+ * nothing authored one, so every card that bound those four fields rendered
14
+ * nothing while the panel showed them as live targets.
15
+ *
16
+ * These tests bind them from a designer-authored row layout, which is the only
17
+ * place they can be read, and assert the value reaches the DOM.
18
+ */
19
+ const payload = {
20
+ listing: {
21
+ title: 'Ayakkabı',
22
+ path: '/kadin-ayakkabi/',
23
+ products: [
24
+ {
25
+ pk: 41,
26
+ name: 'Deri Sandalet',
27
+ absolute_url: '/deri-sandalet-41/',
28
+ image: 'https://cdn.example.com/41.jpg',
29
+ price: '100.00',
30
+ retail_price: '200.00',
31
+ currency_type: 'TRY',
32
+ attributes_kwargs: { badge: { value: 'new_season' } },
33
+ extra_data: {
34
+ variants: [
35
+ {
36
+ attribute_key: 'integration_color',
37
+ options: [
38
+ {
39
+ label: 'Kırmızı',
40
+ value: 'kirmizi',
41
+ is_selectable: true,
42
+ is_selected: true,
43
+ product: { image: 'https://cdn.example.com/41-red.jpg' }
44
+ }
45
+ ]
46
+ }
47
+ ]
48
+ }
49
+ }
50
+ ]
51
+ }
52
+ };
53
+
54
+ /** A row layout whose children bind the four lookup-fed fields. */
55
+ const rowTemplate = {
56
+ id: 'row',
57
+ type: 'group',
58
+ order: 0,
59
+ hidden: false,
60
+ styles: {},
61
+ properties: { role: 'item-template' },
62
+ blocks: [
63
+ {
64
+ id: 'badge-text',
65
+ type: 'text',
66
+ order: 0,
67
+ hidden: false,
68
+ styles: {},
69
+ properties: { dataBinding: 'item.badge.label' }
70
+ },
71
+ {
72
+ id: 'badge-image',
73
+ type: 'image',
74
+ order: 1,
75
+ hidden: false,
76
+ styles: {},
77
+ properties: { dataBinding: 'item.badge.image' }
78
+ },
79
+ {
80
+ id: 'badge-colour',
81
+ type: 'text',
82
+ order: 2,
83
+ hidden: false,
84
+ styles: {},
85
+ properties: { dataBinding: 'item.badge.color' }
86
+ },
87
+ {
88
+ id: 'swatch-colour',
89
+ type: 'text',
90
+ order: 3,
91
+ hidden: false,
92
+ styles: {},
93
+ properties: { dataBinding: 'item.colors[0].color' }
94
+ }
95
+ ]
96
+ };
97
+
98
+ const renderGrid = (
99
+ properties: Record<string, unknown> = {},
100
+ blocks: Record<string, unknown>[] = []
101
+ ) =>
102
+ render(
103
+ <ThemePageContextProvider value={payload}>
104
+ <ProductGridBlock
105
+ {...({
106
+ block: {
107
+ id: 'grid',
108
+ type: 'product-grid',
109
+ styles: {},
110
+ properties,
111
+ blocks,
112
+ order: 0,
113
+ hidden: false
114
+ },
115
+ placeholderId: 'p',
116
+ sectionId: 's'
117
+ } as never)}
118
+ />
119
+ </ThemePageContextProvider>
120
+ );
121
+
122
+ const authored = {
123
+ badgeAttribute: 'badge',
124
+ badgeMap: 'New_Season | Yeni Sezon | https://cdn.example.com/new.svg | #0f766e',
125
+ swatchMap: 'KIRMIZI | #c81e1e'
126
+ };
127
+
128
+ describe('the grid passes the authored lookup table to the row', () => {
129
+ it('resolves the badge key into text a bound row can draw', () => {
130
+ const { container } = renderGrid(authored, [rowTemplate]);
131
+
132
+ expect(container.innerHTML).toContain('Yeni Sezon');
133
+ });
134
+
135
+ it('resolves the badge image and colour', () => {
136
+ const { container } = renderGrid(authored, [rowTemplate]);
137
+
138
+ expect(container.querySelector('img[src="https://cdn.example.com/new.svg"]')).not.toBeNull();
139
+ expect(container.innerHTML).toContain('#0f766e');
140
+ });
141
+
142
+ it('resolves the colour value into the swatch hex', () => {
143
+ const { container } = renderGrid(authored, [rowTemplate]);
144
+
145
+ expect(container.innerHTML).toContain('#c81e1e');
146
+ });
147
+
148
+ it('matches the key in whatever case the integration wrote it', () => {
149
+ // The payload says `new_season` / `kirmizi`; the table above says
150
+ // `New_Season` / `KIRMIZI`.
151
+ const { container } = renderGrid(
152
+ { ...authored, badgeMap: 'NEW_SEASON | Yeni Sezon' },
153
+ [rowTemplate]
154
+ );
155
+
156
+ expect(container.innerHTML).toContain('Yeni Sezon');
157
+ });
158
+
159
+ it('publishes nothing for a key the table does not list', () => {
160
+ const { container } = renderGrid(
161
+ { ...authored, badgeMap: 'clearance | İndirim' },
162
+ [rowTemplate]
163
+ );
164
+
165
+ // Not a fallback to the raw key: "new_season" on a card is the bug the
166
+ // table exists to fix.
167
+ expect(container.innerHTML).not.toContain('new_season');
168
+ expect(container.innerHTML).not.toContain('İndirim');
169
+ });
170
+
171
+ it('reads the attribute the author named', () => {
172
+ const { container } = renderGrid(
173
+ { ...authored, badgeAttribute: 'not_the_badge_key' },
174
+ [rowTemplate]
175
+ );
176
+
177
+ expect(container.innerHTML).not.toContain('Yeni Sezon');
178
+ });
179
+ });
180
+
181
+ describe('an authored table is untrusted input', () => {
182
+ it('drops a script-scheme badge image and keeps the rest of the entry', () => {
183
+ const { container } = renderGrid(
184
+ {
185
+ ...authored,
186
+ // Browsers strip control characters while parsing a url, so the tab
187
+ // still resolves to a live handler.
188
+ badgeMap: 'new_season | Yeni Sezon | java\tscript:alert(1) | #0f766e'
189
+ },
190
+ [rowTemplate]
191
+ );
192
+
193
+ expect(container.innerHTML).not.toContain('script:alert(1)');
194
+ expect(container.innerHTML).toContain('Yeni Sezon');
195
+ });
196
+
197
+ it('drops a badge colour that could close the rule it lands in', () => {
198
+ const { container } = renderGrid(
199
+ {
200
+ ...authored,
201
+ badgeMap: 'new_season | Yeni Sezon | | red;}body{display:none'
202
+ },
203
+ [rowTemplate]
204
+ );
205
+
206
+ expect(container.innerHTML).not.toContain('display:none');
207
+ expect(container.innerHTML).toContain('Yeni Sezon');
208
+ });
209
+
210
+ it('drops a swatch colour that is not a colour', () => {
211
+ const { container } = renderGrid(
212
+ { ...authored, swatchMap: 'kirmizi | url(javascript:alert(1))' },
213
+ [rowTemplate]
214
+ );
215
+
216
+ expect(container.innerHTML).not.toContain('javascript:alert(1)');
217
+ });
218
+ });
219
+
220
+ describe('the built-in card is untouched by the table', () => {
221
+ it('renders identical markup with and without one authored', () => {
222
+ const withTable = renderGrid(authored).container.innerHTML;
223
+ const withoutTable = renderGrid({}).container.innerHTML;
224
+
225
+ // No row layout means the built-in card, and the built-in card draws none
226
+ // of the lookup-fed fields — a published grid cannot move because someone
227
+ // filled the table in.
228
+ expect(withTable).toBe(withoutTable);
229
+ });
230
+ });
@@ -0,0 +1,248 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import ProductPriceBlock from '../product-price-block';
4
+ import { ThemePageContextProvider } from '../../theme-page-context';
5
+
6
+ /**
7
+ * The three additions (price history caption, price range, percent+amount
8
+ * badge) are opt-in, so the first test here is the one that matters most: a
9
+ * block with none of the new properties set must still render exactly what it
10
+ * rendered before they existed.
11
+ */
12
+ const renderBlock = (
13
+ properties: Record<string, unknown>,
14
+ product: Record<string, unknown> | null,
15
+ extra: Record<string, unknown> = {}
16
+ ) =>
17
+ render(
18
+ <ThemePageContextProvider value={product ? { product } : null}>
19
+ <ProductPriceBlock
20
+ {...({
21
+ block: {
22
+ id: 'price',
23
+ type: 'product-price',
24
+ styles: {},
25
+ properties,
26
+ order: 0,
27
+ hidden: false
28
+ },
29
+ placeholderId: 'p',
30
+ sectionId: 's',
31
+ isDesigner: false,
32
+ currentBreakpoint: 'desktop',
33
+ ...extra
34
+ } as any)}
35
+ />
36
+ </ThemePageContextProvider>
37
+ );
38
+
39
+ const BASE_PRODUCT = {
40
+ pk: 1,
41
+ price: '1299.00',
42
+ retail_price: '1749.00',
43
+ currency: 'USD'
44
+ };
45
+
46
+ describe('ProductPriceBlock — untouched properties', () => {
47
+ it('renders the price and the struck-through retail price only', () => {
48
+ const { container } = renderBlock({}, BASE_PRODUCT);
49
+
50
+ const price = container.querySelector('[data-product-current-price]');
51
+ expect(price?.textContent).toBe('$1,299.00');
52
+ // Text child only: the range markup must not appear for a block that
53
+ // never asked for it.
54
+ expect(price?.querySelectorAll('[data-part]').length).toBe(0);
55
+ expect(
56
+ container.querySelector('[data-product-retail-price]')?.textContent
57
+ ).toBe('$1,749.00');
58
+ expect(container.querySelector('[data-product-previous-price]')).toBeNull();
59
+ expect(container.querySelector('[data-product-discount-badge]')).toBeNull();
60
+ });
61
+
62
+ it('keeps the single-string badge for the percentage mode', () => {
63
+ const { container } = renderBlock(
64
+ { showDiscountBadge: true },
65
+ BASE_PRODUCT
66
+ );
67
+
68
+ const badge = container.querySelector('[data-product-discount-badge]');
69
+ expect(badge?.textContent).toBe('-26%');
70
+ expect(badge?.querySelectorAll('[data-part]').length).toBe(0);
71
+ });
72
+
73
+ it('keeps the single-string badge for the amount mode', () => {
74
+ const { container } = renderBlock(
75
+ { showDiscountBadge: true, discountMode: 'amount' },
76
+ BASE_PRODUCT
77
+ );
78
+
79
+ expect(
80
+ container.querySelector('[data-product-discount-badge]')?.textContent
81
+ ).toBe('-$450.00');
82
+ });
83
+ });
84
+
85
+ describe('ProductPriceBlock — price history caption', () => {
86
+ const withHistory = {
87
+ ...BASE_PRODUCT,
88
+ price_extra_field: { previous_price: '1499.00' }
89
+ };
90
+
91
+ it('prints nothing until the caption is switched on', () => {
92
+ const { container } = renderBlock({}, withHistory);
93
+
94
+ expect(container.querySelector('[data-product-previous-price]')).toBeNull();
95
+ });
96
+
97
+ it('reads price_extra_field.previous_price and formats it with the locale', () => {
98
+ const { container } = renderBlock({ showPreviousPrice: true }, withHistory);
99
+
100
+ const line = container.querySelector('[data-product-previous-price]');
101
+ expect(line?.textContent).toBe('Lowest price in the last 30 days: $1,499.00');
102
+ expect(
103
+ line?.querySelector('[data-part="previous-price"]')?.textContent
104
+ ).toBe('$1,499.00');
105
+ expect(
106
+ line?.querySelector('[data-part="history-caption"]')?.textContent
107
+ ).toBe('Lowest price in the last 30 days: ');
108
+ });
109
+
110
+ it('reads the flattened previousPrice a listing row carries', () => {
111
+ const { container } = renderBlock(
112
+ { showPreviousPrice: true },
113
+ { ...BASE_PRODUCT, previousPrice: '1499.00' }
114
+ );
115
+
116
+ expect(
117
+ container.querySelector('[data-part="previous-price"]')?.textContent
118
+ ).toBe('$1,499.00');
119
+ });
120
+
121
+ it('prints no caption when commerce sent no price history', () => {
122
+ const { container } = renderBlock({ showPreviousPrice: true }, BASE_PRODUCT);
123
+
124
+ expect(container.querySelector('[data-product-previous-price]')).toBeNull();
125
+ });
126
+
127
+ it('shows the missing-history hint in the designer only', () => {
128
+ const { container } = renderBlock(
129
+ { showPreviousPrice: true },
130
+ BASE_PRODUCT,
131
+ { isDesigner: true }
132
+ );
133
+
134
+ expect(
135
+ container.querySelector('[data-theme-block-placeholder]')?.textContent
136
+ ).toBe('Lowest price (no price history)');
137
+ });
138
+
139
+ it('honours an authored caption and still prints the amount without {value}', () => {
140
+ const { container } = renderBlock(
141
+ { showPreviousPrice: true, previousPriceTemplate: 'Son 30 gün' },
142
+ withHistory
143
+ );
144
+
145
+ expect(
146
+ container.querySelector('[data-product-previous-price]')?.textContent
147
+ ).toBe('Son 30 gün $1,499.00');
148
+ });
149
+ });
150
+
151
+ describe('ProductPriceBlock — discount as both', () => {
152
+ it('gives the percentage and the amount their own parts', () => {
153
+ const { container } = renderBlock(
154
+ { showDiscountBadge: true, discountMode: 'both' },
155
+ BASE_PRODUCT
156
+ );
157
+
158
+ const badge = container.querySelector('[data-product-discount-badge]');
159
+ expect(badge?.textContent).toBe('-26% ($450.00)');
160
+ expect(
161
+ badge?.querySelector('[data-part="discount-percent"]')?.textContent
162
+ ).toBe('26%');
163
+ expect(
164
+ badge?.querySelector('[data-part="discount-amount"]')?.textContent
165
+ ).toBe('$450.00');
166
+ });
167
+
168
+ it('prints nothing when there is no discount to state', () => {
169
+ const { container } = renderBlock(
170
+ { showDiscountBadge: true, discountMode: 'both' },
171
+ { ...BASE_PRODUCT, retail_price: '1299.00' }
172
+ );
173
+
174
+ expect(container.querySelector('[data-product-discount-badge]')).toBeNull();
175
+ });
176
+ });
177
+
178
+ describe('ProductPriceBlock — price range', () => {
179
+ const grouped = {
180
+ ...BASE_PRODUCT,
181
+ variants: [
182
+ {
183
+ attribute_key: 'size',
184
+ options: [
185
+ { value: 's', product: { price: '1299.00' } },
186
+ { value: 'm', product: { price: '1899.00' } },
187
+ // Not sellable: its price is not a price a shopper can pay.
188
+ { value: 'l', is_selectable: false, product: { price: '99.00' } }
189
+ ]
190
+ }
191
+ ]
192
+ };
193
+
194
+ it('prints one price until the range is switched on', () => {
195
+ const { container } = renderBlock({}, grouped);
196
+
197
+ expect(
198
+ container.querySelector('[data-product-current-price]')?.textContent
199
+ ).toBe('$1,299.00');
200
+ });
201
+
202
+ it('prints the lowest and highest sellable variant price', () => {
203
+ const { container } = renderBlock({ showPriceRange: true }, grouped);
204
+
205
+ const price = container.querySelector('[data-product-current-price]');
206
+ expect(price?.querySelector('[data-part="price-min"]')?.textContent).toBe(
207
+ '$1,299.00'
208
+ );
209
+ expect(price?.querySelector('[data-part="price-max"]')?.textContent).toBe(
210
+ '$1,899.00'
211
+ );
212
+ expect(
213
+ price?.querySelector('[data-part="range-separator"]')?.textContent
214
+ ).toBe(' – ');
215
+ });
216
+
217
+ it('honours an authored separator', () => {
218
+ const { container } = renderBlock(
219
+ { showPriceRange: true, priceRangeSeparator: ' to ' },
220
+ grouped
221
+ );
222
+
223
+ expect(
224
+ container.querySelector('[data-part="range-separator"]')?.textContent
225
+ ).toBe(' to ');
226
+ });
227
+
228
+ it('falls back to the single price when the variants do not differ', () => {
229
+ const { container } = renderBlock(
230
+ { showPriceRange: true },
231
+ {
232
+ ...BASE_PRODUCT,
233
+ variants: [
234
+ {
235
+ options: [
236
+ { value: 's', product: { price: '1299.00' } },
237
+ { value: 'm', product: { price: '1299.00' } }
238
+ ]
239
+ }
240
+ ]
241
+ }
242
+ );
243
+
244
+ const price = container.querySelector('[data-product-current-price]');
245
+ expect(price?.textContent).toBe('$1,299.00');
246
+ expect(price?.querySelectorAll('[data-part]').length).toBe(0);
247
+ });
248
+ });