@akinon/pz-theme 2.0.143-beta.0 → 2.0.143

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.
@@ -0,0 +1,332 @@
1
+ import React from 'react';
2
+ import { renderToString } from 'react-dom/server';
3
+ import { act, render } from '@testing-library/react';
4
+
5
+ // The registry first, as the app loads it (see card-media.test.tsx).
6
+ import '../../blocks/block-renderer-registry';
7
+ import GroupBlock from '../../blocks/group-block';
8
+ import {
9
+ __resetCardViewForTests,
10
+ buildCardViewCss,
11
+ CardListViews
12
+ } from '../../components/card-list-views';
13
+ import type { Block } from '../../theme-block';
14
+ import ThemeBlock from '../../theme-block';
15
+ import type { Section } from '../../theme-section';
16
+ import {
17
+ type CardViewSettings,
18
+ cardViewFromHash,
19
+ DEFAULT_CARD_VIEW_TOGGLES
20
+ } from '../card-media';
21
+ import { applyPropertySwitchesToSections } from '../property-switches';
22
+
23
+ /**
24
+ * ZERO-5173 — a card list that is not sliding is drawn in one of the GANT
25
+ * listing's grid views: catalogue (3 across, 1 below 1024px), classic (the
26
+ * list's own grid, the default) or archive (8 across, 4 below, pictures
27
+ * only). The designer picks it from a select; the listing's toggles, when
28
+ * offered, let the shopper switch, kept in the URL hash as on the listing,
29
+ * and are typed from the list's settings.
30
+ */
31
+ const node = (
32
+ id: string,
33
+ type: string,
34
+ properties: Record<string, unknown> = {},
35
+ blocks: Block[] = [],
36
+ extra: Partial<Block> = {}
37
+ ): Block => ({
38
+ id,
39
+ type,
40
+ label: id,
41
+ properties,
42
+ styles: {},
43
+ order: 0,
44
+ hidden: false,
45
+ blocks,
46
+ ...extra
47
+ });
48
+
49
+ const repeat = () =>
50
+ node(
51
+ 'repeat',
52
+ 'group',
53
+ { tag: 'div' },
54
+ [node('card', 'group', { tag: 'div', blockTemplate: 'product-card' })],
55
+ { isIterator: true, iteratorDataPath: 'products' }
56
+ );
57
+
58
+ const resolve = (properties: Record<string, unknown>) => {
59
+ const section: Section = {
60
+ id: 's1',
61
+ type: 'custom',
62
+ name: 's1',
63
+ label: 'S1',
64
+ order: 0,
65
+ hidden: false,
66
+ properties,
67
+ styles: {},
68
+ blocks: [repeat()]
69
+ };
70
+ return applyPropertySwitchesToSections([section])[0].blocks[0].cardsViews;
71
+ };
72
+
73
+ const setHash = (hash: string) => {
74
+ window.history.replaceState(null, '', `/erkek-polo/${hash}`);
75
+ };
76
+
77
+ const offered = (
78
+ view: CardViewSettings['view'] = 'classic'
79
+ ): CardViewSettings => ({
80
+ view,
81
+ toggles: { ...DEFAULT_CARD_VIEW_TOGGLES }
82
+ });
83
+
84
+ const pressed = (container: HTMLElement) =>
85
+ container
86
+ .querySelector('[aria-pressed="true"]')
87
+ ?.getAttribute('data-card-view-option');
88
+
89
+ describe('the listing’s grid views on a card list', () => {
90
+ beforeEach(() => {
91
+ setHash('');
92
+ __resetCardViewForTests();
93
+ });
94
+
95
+ it('draw the list in the view its designer picked, with the toggles only when offered', () => {
96
+ // Nothing set: the list as authored, and nothing written on it.
97
+ expect(resolve({})).toBeUndefined();
98
+ expect(resolve({ cardsView: 'classic' })).toBeUndefined();
99
+ // A view picked from the select, per breakpoint as the editor stores it.
100
+ expect(resolve({ cardsView: { desktop: 'archive' } })).toEqual({
101
+ view: 'archive'
102
+ });
103
+ // The toggles, with the toolbar's look unless the designer typed it.
104
+ expect(resolve({ cardsViewModes: { desktop: true } })).toEqual({
105
+ view: 'classic',
106
+ toggles: DEFAULT_CARD_VIEW_TOGGLES
107
+ });
108
+ expect(
109
+ resolve({
110
+ cardsViewModes: true,
111
+ cardsView: 'catalogue',
112
+ cardsViewTogglesAlign: 'left',
113
+ cardsViewTogglesColor: '#781526',
114
+ cardsViewTogglesSize: 20,
115
+ cardsViewTogglesGap: 12,
116
+ cardsViewTogglesPadding: '0 24'
117
+ })
118
+ ).toEqual({
119
+ view: 'catalogue',
120
+ toggles: {
121
+ align: 'left',
122
+ color: '#781526',
123
+ size: 20,
124
+ gap: 12,
125
+ padding: '0px 24px'
126
+ }
127
+ });
128
+ // Nothing that could leave the declaration it lands in.
129
+ expect(
130
+ resolve({
131
+ cardsViewModes: true,
132
+ cardsViewTogglesColor: 'red;}body{display:none',
133
+ cardsViewTogglesPadding: '1px;}x{'
134
+ })?.toggles
135
+ ).toEqual(
136
+ expect.objectContaining({
137
+ color: DEFAULT_CARD_VIEW_TOGGLES.color,
138
+ padding: DEFAULT_CARD_VIEW_TOGGLES.padding
139
+ })
140
+ );
141
+ // A slider lays its own row out: no grid views there.
142
+ expect(
143
+ resolve({ cardsView: 'archive', cardsViewModes: true, cardsSlider: true })
144
+ ).toBeUndefined();
145
+ });
146
+
147
+ it('read a shopper’s view off the hash as the listing does, none otherwise', () => {
148
+ expect(cardViewFromHash('#archive')).toBe('archive');
149
+ expect(cardViewFromHash('#CATALOGUE')).toBe('catalogue');
150
+ expect(cardViewFromHash('#reviews')).toBeNull();
151
+ expect(cardViewFromHash('')).toBeNull();
152
+ });
153
+
154
+ it('lay the list out per view, and leave the classic grid as authored', () => {
155
+ expect(buildCardViewCss('repeat', 'classic', false)).toBe('');
156
+
157
+ const catalogue = buildCardViewCss('repeat', 'catalogue', false);
158
+ expect(catalogue).toContain(
159
+ '[data-block-id="repeat"][data-block-id="repeat"]{grid-template-columns:repeat(3,minmax(0,1fr))}'
160
+ );
161
+ expect(catalogue).toContain(
162
+ '@media (max-width:1023px){[data-block-id="repeat"][data-block-id="repeat"]{grid-template-columns:repeat(1,minmax(0,1fr))}}'
163
+ );
164
+
165
+ const archive = buildCardViewCss('repeat', 'archive', false);
166
+ expect(archive).toContain('repeat(8,minmax(0,1fr))');
167
+ expect(archive).toContain('repeat(4,minmax(0,1fr))');
168
+ // Pictures only: what a card marks for the archive goes, box and all.
169
+ expect(archive).toContain(
170
+ '[data-block-id="repeat"][data-block-id="repeat"] [data-hide-in-card-view~="archive"]{display:none}'
171
+ );
172
+
173
+ // The phone canvas takes the phone layout outright.
174
+ const phone = buildCardViewCss('repeat', 'archive', true);
175
+ expect(phone).toContain('repeat(4,minmax(0,1fr))');
176
+ expect(phone).not.toContain('repeat(8,');
177
+
178
+ // The toggles: the glyph's size plus 8 from 1024px up, 44px squares below.
179
+ const toggles = buildCardViewCss('repeat', 'classic', false, {
180
+ ...DEFAULT_CARD_VIEW_TOGGLES,
181
+ size: 20,
182
+ gap: 12
183
+ });
184
+ expect(toggles).toContain(
185
+ '[data-card-view-switcher="repeat"]{gap:12px}[data-card-view-switcher="repeat"]>button{width:28px;height:28px}'
186
+ );
187
+ expect(toggles).toContain(
188
+ '[data-card-view-switcher="repeat"]>button{width:44px;height:44px}'
189
+ );
190
+
191
+ // An id that is not a plain token never reaches a selector.
192
+ expect(buildCardViewCss('x"]{display:none}', 'archive', false)).toBe('');
193
+ });
194
+
195
+ it('draw no toggles unless offered, and the picked view whatever the hash says', () => {
196
+ setHash('#classic');
197
+ const { container } = render(
198
+ <CardListViews scopeId="repeat" settings={{ view: 'archive' }} />
199
+ );
200
+ expect(container.querySelector('[data-card-view-switcher]')).toBeNull();
201
+ expect(container.querySelector('style')?.textContent).toContain(
202
+ 'repeat(8,minmax(0,1fr))'
203
+ );
204
+ });
205
+
206
+ it('switch with the toggles, typed as set, and keep the view in the hash', () => {
207
+ const { container } = render(
208
+ <CardListViews
209
+ scopeId="repeat"
210
+ settings={{
211
+ view: 'classic',
212
+ toggles: {
213
+ ...DEFAULT_CARD_VIEW_TOGGLES,
214
+ align: 'center',
215
+ color: '#781526',
216
+ padding: '0px 24px'
217
+ }
218
+ }}
219
+ />
220
+ );
221
+ const row = container.querySelector(
222
+ '[data-card-view-switcher="repeat"]'
223
+ ) as HTMLElement;
224
+ expect(row.style.justifyContent).toBe('center');
225
+ expect(row.style.color).toBe('rgb(120, 21, 38)');
226
+ expect(row.style.padding).toBe('0px 24px');
227
+ expect(
228
+ Array.from(container.querySelectorAll('[data-card-view-option]')).map(
229
+ (entry) => entry.getAttribute('data-card-view-option')
230
+ )
231
+ ).toEqual(['catalogue', 'classic', 'archive']);
232
+ expect(pressed(container)).toBe('classic');
233
+
234
+ act(() => {
235
+ (
236
+ container.querySelector(
237
+ '[data-card-view-option="archive"]'
238
+ ) as HTMLButtonElement
239
+ ).click();
240
+ });
241
+ expect(window.location.hash).toBe('#archive');
242
+ expect(pressed(container)).toBe('archive');
243
+ });
244
+
245
+ it('open on the hash they were linked with, and follow the hash after', () => {
246
+ setHash('#catalogue');
247
+ const { container } = render(
248
+ <CardListViews scopeId="repeat" settings={offered('archive')} />
249
+ );
250
+ expect(pressed(container)).toBe('catalogue');
251
+
252
+ act(() => {
253
+ setHash('#archive');
254
+ window.dispatchEvent(new HashChangeEvent('hashchange'));
255
+ });
256
+ expect(pressed(container)).toBe('archive');
257
+ });
258
+
259
+ it('leave the canvas URL alone and select the list that holds them', () => {
260
+ const select = jest.fn();
261
+ const { container } = render(
262
+ <CardListViews
263
+ scopeId="repeat"
264
+ settings={offered()}
265
+ isDesigner
266
+ onSelect={select}
267
+ />
268
+ );
269
+ act(() => {
270
+ (
271
+ container.querySelector(
272
+ '[data-card-view-option="catalogue"]'
273
+ ) as HTMLButtonElement
274
+ ).click();
275
+ });
276
+ expect(window.location.hash).toBe('');
277
+ expect(pressed(container)).toBe('catalogue');
278
+ expect(select).toHaveBeenCalledTimes(1);
279
+ });
280
+
281
+ it('render the picked view on the server, whatever the hash', () => {
282
+ setHash('#archive');
283
+ const markup = renderToString(
284
+ <CardListViews scopeId="repeat" settings={offered('catalogue')} />
285
+ );
286
+ expect(markup).toContain(
287
+ 'data-card-view-option="catalogue" aria-pressed="true"'
288
+ );
289
+ expect(markup).toContain('repeat(3,minmax(0,1fr))');
290
+ expect(markup).not.toContain('repeat(8,');
291
+ });
292
+
293
+ it('head the repeat’s grid with the toggles, and mark what the archive hides', () => {
294
+ const list: Block = {
295
+ ...node('repeat-1', 'group', { tag: 'div' }, [
296
+ node('c1', 'text', {}, [], { value: 'Kart 1' })
297
+ ]),
298
+ cardsViews: offered()
299
+ };
300
+ const { container } = render(
301
+ <GroupBlock
302
+ block={list}
303
+ placeholderId="p"
304
+ sectionId="s"
305
+ isDesigner={false}
306
+ />
307
+ );
308
+ const switcher = container.querySelector(
309
+ '[data-card-view-switcher="repeat-1"]'
310
+ ) as HTMLElement;
311
+ // A grid item across the repeat's columns, before its cards.
312
+ expect(switcher.style.gridColumn).toBe('1 / -1');
313
+
314
+ const caption = render(
315
+ <ThemeBlock
316
+ block={node(
317
+ 'caption',
318
+ 'group',
319
+ { tag: 'div', hideInCardView: 'archive' },
320
+ [node('t', 'text', {}, [], { value: 'Ad' })]
321
+ )}
322
+ placeholderId="p"
323
+ sectionId="s"
324
+ />
325
+ );
326
+ expect(
327
+ caption.container
328
+ .querySelector('[data-block-id="caption"]')
329
+ ?.getAttribute('data-hide-in-card-view')
330
+ ).toBe('archive');
331
+ });
332
+ });
@@ -0,0 +1,145 @@
1
+ import { normalizeListingProduct } from '../../blocks/listing-row';
2
+ import {
3
+ __resetSwatchColorsForTests,
4
+ registerSwatchColors,
5
+ resolveSwatchColor
6
+ } from '../swatch-colors';
7
+
8
+ /**
9
+ * ZERO-5173 — a product card's colour chips paint with the brand's own colour
10
+ * resolution when the list authored no mapping for them: GANT's listing turns
11
+ * a colour code into a tone and the tone into a hex, and a theme card over the
12
+ * same products has to show the same chips.
13
+ */
14
+ const product = (codes: string[], own = codes[0]) => ({
15
+ pk: 1,
16
+ name: 'GANT Erkek Yeşil Düz Şort',
17
+ price: '5999.00',
18
+ absolute_url: '/p-1/',
19
+ attributes: { integration_renk: own, integration_renk_tonu: 'YEŞİL' },
20
+ extra_data: {
21
+ variants: [
22
+ {
23
+ attribute_key: 'integration_renk',
24
+ // GANT's payload: the code is the option's label, no value at all.
25
+ options: codes.map((code, index) => ({
26
+ label: code,
27
+ is_selectable: true,
28
+ product: {
29
+ pk: index + 1,
30
+ absolute_url: `/p-${index + 1}/`,
31
+ attributes: { integration_renk: code }
32
+ }
33
+ }))
34
+ }
35
+ ]
36
+ }
37
+ });
38
+
39
+ const chips = (row: ReturnType<typeof normalizeListingProduct>) =>
40
+ (row?.colors ?? []).map((color) => color.color);
41
+
42
+ describe('swatch colours a brand registers', () => {
43
+ afterEach(() => __resetSwatchColorsForTests());
44
+
45
+ it('leaves a chip uncoloured when no brand registered and nothing is mapped', () => {
46
+ const row = normalizeListingProduct(product(['10', '433']), 0, {});
47
+
48
+ expect(chips(row)).toEqual([undefined, undefined]);
49
+ expect(
50
+ resolveSwatchColor({
51
+ key: '10',
52
+ label: '10',
53
+ product: {},
54
+ variant: {},
55
+ rows: []
56
+ })
57
+ ).toBeUndefined();
58
+ });
59
+
60
+ it('paints every chip the list did not map, from the code and both products', () => {
61
+ const seen: unknown[] = [];
62
+ registerSwatchColors(({ key, product: row, variant }) => {
63
+ seen.push([
64
+ key,
65
+ (row.attributes as Record<string, unknown>).integration_renk,
66
+ variant.pk
67
+ ]);
68
+ return ({ '10': '#2f5d3a', '433': '#1c2541' } as Record<string, string>)[
69
+ key
70
+ ];
71
+ });
72
+
73
+ const row = normalizeListingProduct(product(['10', '433', '999']), 0, {});
74
+
75
+ expect(chips(row)).toEqual(['#2f5d3a', '#1c2541', undefined]);
76
+ expect(seen[0]).toEqual(['10', '10', 1]);
77
+ });
78
+
79
+ it('hands the resolver the list the row is in, or the row alone', () => {
80
+ const lists: number[] = [];
81
+ registerSwatchColors(({ rows }) => {
82
+ lists.push(rows.length);
83
+ return '#000000';
84
+ });
85
+ const neighbour = product(['676'], '676');
86
+
87
+ normalizeListingProduct(product(['10']), 0, {});
88
+ normalizeListingProduct(product(['10']), 0, {
89
+ rows: [product(['10']), neighbour, null, 'not a row']
90
+ });
91
+
92
+ // Alone: itself. In a list: every row that is a product, itself included.
93
+ expect(lists).toEqual([1, 2]);
94
+ });
95
+
96
+ it('lets an authored mapping win over the brand', () => {
97
+ registerSwatchColors(() => '#2f5d3a');
98
+
99
+ const row = normalizeListingProduct(product(['10', '433']), 0, {
100
+ lookups: { swatch: { '433': '#ff0000' } }
101
+ });
102
+
103
+ expect(chips(row)).toEqual(['#2f5d3a', '#ff0000']);
104
+ });
105
+
106
+ it('never writes anything but one plain colour into a chip', () => {
107
+ const answers: Record<string, unknown> = {
108
+ a: 'red; background: url(//x)',
109
+ b: 'rgb(28, 37, 65)',
110
+ c: 42,
111
+ d: ' #ABC '
112
+ };
113
+ registerSwatchColors(({ key }) => answers[key] as string);
114
+ const ask = (key: string) =>
115
+ resolveSwatchColor({
116
+ key,
117
+ label: key,
118
+ product: {},
119
+ variant: {},
120
+ rows: []
121
+ });
122
+
123
+ expect(ask('a')).toBeUndefined();
124
+ expect(ask('b')).toBe('rgb(28, 37, 65)');
125
+ expect(ask('c')).toBeUndefined();
126
+ expect(ask('d')).toBe('#ABC');
127
+ });
128
+
129
+ it('survives a resolver that throws, and can be unregistered', () => {
130
+ const detach = registerSwatchColors(() => {
131
+ throw new Error('brand bug');
132
+ });
133
+ expect(chips(normalizeListingProduct(product(['10']), 0, {}))).toEqual([
134
+ undefined
135
+ ]);
136
+
137
+ detach();
138
+ registerSwatchColors(() => '#000000');
139
+ detach();
140
+ // An older unregister never removes a newer resolver.
141
+ expect(chips(normalizeListingProduct(product(['10']), 0, {}))).toEqual([
142
+ '#000000'
143
+ ]);
144
+ });
145
+ });