@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,392 @@
1
+ import React from 'react';
2
+ import { act, fireEvent, render, waitFor } from '@testing-library/react';
3
+ import ShareBlock from '../share-block';
4
+ import { ThemePageContextProvider } from '../../theme-page-context';
5
+
6
+ const props = (
7
+ properties: Record<string, unknown> = {},
8
+ over: Record<string, unknown> = {}
9
+ ) =>
10
+ ({
11
+ block: {
12
+ id: 'b1',
13
+ type: 'share',
14
+ label: 'Share',
15
+ styles: {},
16
+ properties,
17
+ order: 0,
18
+ hidden: false
19
+ },
20
+ placeholderId: 'p',
21
+ sectionId: 's',
22
+ isDesigner: false,
23
+ ...over
24
+ } as any);
25
+
26
+ const PRODUCT = {
27
+ name: 'Striped Polo',
28
+ absolute_url: '/striped-polo',
29
+ image: '/media/polo.jpg'
30
+ };
31
+
32
+ const renderWithProduct = (
33
+ properties: Record<string, unknown> = {},
34
+ over: Record<string, unknown> = {}
35
+ ) =>
36
+ render(
37
+ <ThemePageContextProvider value={{ product: PRODUCT }}>
38
+ <ShareBlock {...props(properties, over)} />
39
+ </ThemePageContextProvider>
40
+ );
41
+
42
+ const originalClipboard = Object.getOwnPropertyDescriptor(
43
+ window.navigator,
44
+ 'clipboard'
45
+ );
46
+
47
+ const setClipboard = (value: unknown) => {
48
+ Object.defineProperty(window.navigator, 'clipboard', {
49
+ value,
50
+ configurable: true
51
+ });
52
+ };
53
+
54
+ afterEach(() => {
55
+ if (originalClipboard) {
56
+ Object.defineProperty(window.navigator, 'clipboard', originalClipboard);
57
+ } else {
58
+ delete (window.navigator as unknown as Record<string, unknown>).clipboard;
59
+ }
60
+ delete (window.navigator as unknown as Record<string, unknown>).share;
61
+ });
62
+
63
+ describe('ShareBlock', () => {
64
+ it('renders the inline icon row with the default networks', () => {
65
+ const { container } = renderWithProduct();
66
+
67
+ expect(container.querySelector('[data-share]')).not.toBeNull();
68
+ expect(container.querySelector('[data-share-trigger]')).toBeNull();
69
+ expect(
70
+ Array.from(container.querySelectorAll('[data-share-item]')).map((node) =>
71
+ node.getAttribute('data-share-item')
72
+ )
73
+ ).toEqual(['whatsapp', 'x', 'facebook', 'pinterest', 'email', 'copy']);
74
+ });
75
+
76
+ it('drops a network whose toggle is off', () => {
77
+ const { container } = renderWithProduct({ showFacebook: false });
78
+
79
+ expect(container.querySelector('[data-share-item="facebook"]')).toBeNull();
80
+ });
81
+
82
+ it('builds absolute, escaped share targets from the ambient product', async () => {
83
+ const { container } = renderWithProduct();
84
+
85
+ await waitFor(() => {
86
+ const facebook = container.querySelector(
87
+ '[data-share-item="facebook"]'
88
+ ) as HTMLAnchorElement;
89
+ expect(facebook.getAttribute('href')).toBe(
90
+ 'https://www.facebook.com/sharer/sharer.php?u=http%3A%2F%2Flocalhost%2Fstriped-polo'
91
+ );
92
+ });
93
+
94
+ const pinterest = container.querySelector(
95
+ '[data-share-item="pinterest"]'
96
+ ) as HTMLAnchorElement;
97
+ // Pinterest needs the product image, and every value is encoded.
98
+ expect(pinterest.getAttribute('href')).toContain(
99
+ 'media=http%3A%2F%2Flocalhost%2Fmedia%2Fpolo.jpg'
100
+ );
101
+ expect(pinterest.getAttribute('href')).toContain(
102
+ 'description=Striped%20Polo'
103
+ );
104
+
105
+ const email = container.querySelector(
106
+ '[data-share-item="email"]'
107
+ ) as HTMLAnchorElement;
108
+ expect(email.getAttribute('href')).toBe(
109
+ 'mailto:?subject=Striped%20Polo&body=Striped%20Polo%20http%3A%2F%2Flocalhost%2Fstriped-polo'
110
+ );
111
+ // A mailto must not be torn out into a second tab.
112
+ expect(email.getAttribute('target')).toBeNull();
113
+ });
114
+
115
+ it('escapes a title that would otherwise break out of the query string', async () => {
116
+ const { container } = render(
117
+ <ThemePageContextProvider
118
+ value={{ product: { ...PRODUCT, name: 'Polo &top=1 #2' } }}
119
+ >
120
+ <ShareBlock {...props()} />
121
+ </ThemePageContextProvider>
122
+ );
123
+
124
+ await waitFor(() => {
125
+ const x = container.querySelector(
126
+ '[data-share-item="x"]'
127
+ ) as HTMLAnchorElement;
128
+ expect(x.getAttribute('href')).toContain('text=Polo%20%26top%3D1%20%232');
129
+ });
130
+ });
131
+
132
+ it('falls back to the current page URL when there is no product', async () => {
133
+ const { container } = render(<ShareBlock {...props()} />);
134
+
135
+ await waitFor(() => {
136
+ const facebook = container.querySelector(
137
+ '[data-share-item="facebook"]'
138
+ ) as HTMLAnchorElement;
139
+ expect(facebook.getAttribute('href')).toBe(
140
+ `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
141
+ window.location.href
142
+ )}`
143
+ );
144
+ });
145
+ });
146
+
147
+ it('hides the list behind a trigger in the trigger layout', () => {
148
+ const { container } = renderWithProduct({ layout: 'trigger' });
149
+
150
+ expect(container.querySelector('[data-share-list]')).toBeNull();
151
+
152
+ fireEvent.click(
153
+ container.querySelector('[data-share-trigger]') as HTMLElement
154
+ );
155
+
156
+ expect(container.querySelector('[data-share-list]')).not.toBeNull();
157
+ expect(
158
+ container
159
+ .querySelector('[data-share-trigger]')
160
+ ?.getAttribute('aria-expanded')
161
+ ).toBe('true');
162
+ });
163
+
164
+ describe('nativeShare gate', () => {
165
+ it('renders the plain inline row and never touches navigator by default', () => {
166
+ const share = jest.fn();
167
+ (window.navigator as unknown as Record<string, unknown>).share = share;
168
+
169
+ const { container } = renderWithProduct();
170
+
171
+ expect(container.querySelector('[data-share-trigger]')).toBeNull();
172
+ expect(container.querySelector('[data-share-list]')).not.toBeNull();
173
+ expect(share).not.toHaveBeenCalled();
174
+ });
175
+
176
+ it('calls navigator.share from the trigger once switched on', async () => {
177
+ const share = jest.fn().mockResolvedValue(undefined);
178
+ (window.navigator as unknown as Record<string, unknown>).share = share;
179
+
180
+ const { container } = renderWithProduct({ nativeShare: true });
181
+ const trigger = container.querySelector(
182
+ '[data-share-trigger]'
183
+ ) as HTMLElement;
184
+
185
+ await act(async () => {
186
+ fireEvent.click(trigger);
187
+ });
188
+
189
+ expect(share).toHaveBeenCalledWith(
190
+ expect.objectContaining({ title: 'Striped Polo' })
191
+ );
192
+ // The sheet handled it, so the icon row stays shut.
193
+ expect(container.querySelector('[data-share-list]')).toBeNull();
194
+ });
195
+
196
+ it('reveals the icon row when the browser has no native share', async () => {
197
+ const { container } = renderWithProduct({ nativeShare: true });
198
+
199
+ await act(async () => {
200
+ fireEvent.click(
201
+ container.querySelector('[data-share-trigger]') as HTMLElement
202
+ );
203
+ });
204
+
205
+ expect(container.querySelector('[data-share-list]')).not.toBeNull();
206
+ });
207
+
208
+ it('keeps the panel shut when the shopper dismisses the sheet', async () => {
209
+ const abort = Object.assign(new Error('cancelled'), {
210
+ name: 'AbortError'
211
+ });
212
+ (window.navigator as unknown as Record<string, unknown>).share = jest
213
+ .fn()
214
+ .mockRejectedValue(abort);
215
+
216
+ const { container } = renderWithProduct({ nativeShare: true });
217
+
218
+ await act(async () => {
219
+ fireEvent.click(
220
+ container.querySelector('[data-share-trigger]') as HTMLElement
221
+ );
222
+ });
223
+
224
+ expect(container.querySelector('[data-share-list]')).toBeNull();
225
+ });
226
+ });
227
+
228
+ describe('copy link', () => {
229
+ it('confirms with the authored text and clears it after the delay', async () => {
230
+ jest.useFakeTimers();
231
+ const writeText = jest.fn().mockResolvedValue(undefined);
232
+ setClipboard({ writeText });
233
+
234
+ const { container } = renderWithProduct({
235
+ copyConfirmLabel: 'Kopyalandı',
236
+ confirmDuration: 3
237
+ });
238
+
239
+ await act(async () => {
240
+ fireEvent.click(
241
+ container.querySelector('[data-share-item="copy"]') as HTMLElement
242
+ );
243
+ });
244
+
245
+ expect(writeText).toHaveBeenCalledWith('http://localhost/striped-polo');
246
+ expect(
247
+ container.querySelector('[data-share-feedback="copied"]')?.textContent
248
+ ).toBe('Kopyalandı');
249
+
250
+ await act(async () => {
251
+ jest.advanceTimersByTime(3000);
252
+ });
253
+
254
+ expect(container.querySelector('[data-share-feedback]')).toBeNull();
255
+ jest.useRealTimers();
256
+ });
257
+
258
+ it('offers a selectable input instead of pretending when there is no clipboard', async () => {
259
+ setClipboard(undefined);
260
+
261
+ const { container } = renderWithProduct({
262
+ copyFailLabel: 'Copy it yourself'
263
+ });
264
+
265
+ await act(async () => {
266
+ fireEvent.click(
267
+ container.querySelector('[data-share-item="copy"]') as HTMLElement
268
+ );
269
+ });
270
+
271
+ expect(
272
+ container.querySelector('[data-share-feedback="failed"]')?.textContent
273
+ ).toBe('Copy it yourself');
274
+ const input = container.querySelector(
275
+ '[data-share-fallback-input]'
276
+ ) as HTMLInputElement;
277
+ expect(input.value).toBe('http://localhost/striped-polo');
278
+ });
279
+
280
+ it('does the same when writeText rejects on a non-secure origin', async () => {
281
+ setClipboard({ writeText: jest.fn().mockRejectedValue(new Error('no')) });
282
+
283
+ const { container } = renderWithProduct();
284
+
285
+ await act(async () => {
286
+ fireEvent.click(
287
+ container.querySelector('[data-share-item="copy"]') as HTMLElement
288
+ );
289
+ });
290
+
291
+ expect(
292
+ container.querySelector('[data-share-fallback-input]')
293
+ ).not.toBeNull();
294
+ });
295
+
296
+ it('removes the option entirely when the author chose to hide it', async () => {
297
+ setClipboard(undefined);
298
+
299
+ const { container } = renderWithProduct({ copyFallback: 'hide' });
300
+
301
+ await waitFor(() => {
302
+ expect(container.querySelector('[data-share-item="copy"]')).toBeNull();
303
+ });
304
+ });
305
+ });
306
+
307
+ describe('designer', () => {
308
+ it('makes the links inert', () => {
309
+ const { container } = renderWithProduct({}, { isDesigner: true });
310
+ const facebook = container.querySelector(
311
+ '[data-share-item="facebook"]'
312
+ ) as HTMLAnchorElement;
313
+
314
+ expect(facebook.getAttribute('href')).toBe('#');
315
+ expect(facebook.getAttribute('target')).toBeNull();
316
+ });
317
+
318
+ it('writes nothing to the clipboard and starts no timer', async () => {
319
+ const writeText = jest.fn();
320
+ setClipboard({ writeText });
321
+
322
+ const { container } = renderWithProduct({}, { isDesigner: true });
323
+
324
+ await act(async () => {
325
+ fireEvent.click(
326
+ container.querySelector('[data-share-item="copy"]') as HTMLElement
327
+ );
328
+ });
329
+
330
+ expect(writeText).not.toHaveBeenCalled();
331
+ expect(container.querySelector('[data-share-feedback]')).toBeNull();
332
+ });
333
+
334
+ it('never opens the native sheet', async () => {
335
+ const share = jest.fn();
336
+ (window.navigator as unknown as Record<string, unknown>).share = share;
337
+
338
+ const { container } = renderWithProduct(
339
+ { nativeShare: true },
340
+ { isDesigner: true }
341
+ );
342
+
343
+ await act(async () => {
344
+ fireEvent.click(
345
+ container.querySelector('[data-share-trigger]') as HTMLElement
346
+ );
347
+ });
348
+
349
+ expect(share).not.toHaveBeenCalled();
350
+ expect(container.querySelector('[data-share-list]')).not.toBeNull();
351
+ });
352
+
353
+ it('shows a placeholder when every network is switched off', () => {
354
+ const { container } = renderWithProduct(
355
+ {
356
+ showWhatsapp: false,
357
+ showX: false,
358
+ showFacebook: false,
359
+ showPinterest: false,
360
+ showEmail: false,
361
+ showCopy: false,
362
+ emptyLabel: 'Nothing enabled'
363
+ },
364
+ { isDesigner: true }
365
+ );
366
+
367
+ expect(container.textContent).toContain('Nothing enabled');
368
+ expect(container.querySelector('[data-share]')).not.toBeNull();
369
+ });
370
+ });
371
+
372
+ it('names every visually distinct element as a part', () => {
373
+ const { container } = renderWithProduct({
374
+ layout: 'trigger',
375
+ showLabels: true
376
+ });
377
+
378
+ fireEvent.click(
379
+ container.querySelector('[data-share-trigger]') as HTMLElement
380
+ );
381
+
382
+ const parts = new Set(
383
+ Array.from(container.querySelectorAll('[data-part]')).map((node) =>
384
+ node.getAttribute('data-part')
385
+ )
386
+ );
387
+
388
+ ['root', 'trigger', 'list', 'item', 'icon', 'label'].forEach((part) =>
389
+ expect(parts.has(part)).toBe(true)
390
+ );
391
+ });
392
+ });