@akinon/pz-theme 2.0.82-beta.0 → 2.0.82
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 +8 -4
- package/package.json +4 -4
- package/src/blocks/__tests__/link-block.test.tsx +87 -0
- package/src/blocks/__tests__/text-block.test.tsx +88 -0
- package/src/blocks/__tests__/video-block.test.tsx +207 -2
- package/src/blocks/item-template-utils.ts +51 -5
- package/src/blocks/link-block.tsx +43 -0
- package/src/blocks/slider-block.tsx +92 -4
- package/src/blocks/text-block.tsx +42 -4
- package/src/blocks/video-block.tsx +208 -80
- package/src/sections/__tests__/posts-slider-arrows.test.tsx +470 -0
- package/src/sections/__tests__/posts-slider-edges.test.tsx +394 -0
- package/src/sections/__tests__/posts-slider-quick-view-no-fetch.test.tsx +205 -0
- package/src/sections/__tests__/posts-slider-quick-view.test.tsx +660 -0
- package/src/sections/posts-slider-section.tsx +482 -115
- package/src/sections/slider-quick-view.tsx +375 -0
- package/src/utils/__tests__/line-breaks.test.ts +215 -0
- package/src/utils/index.ts +12 -1
- package/src/utils/iterator-utils.ts +46 -0
- package/src/utils/line-breaks.ts +142 -0
- package/src/utils/slider-arrows.tsx +252 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { act, render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The strip's edges: flush right, a sliced next card, and a lead-in that only
|
|
6
|
+
* exists at the start.
|
|
7
|
+
*
|
|
8
|
+
* Two of the three are geometry, and geometry here belongs to the carousel
|
|
9
|
+
* library, not to this section — `itemWidth = round(containerWidth /
|
|
10
|
+
* slidesToShow)` and `transform = -itemWidth * currentSlide`. So the arithmetic
|
|
11
|
+
* is asserted against the library's OWN functions rather than against a DOM:
|
|
12
|
+
* the real carousel renders zero items under jsdom, so a render-and-measure
|
|
13
|
+
* test here would be measuring nothing.
|
|
14
|
+
*
|
|
15
|
+
* The third — the lead-in — is not geometry at all. No padding is
|
|
16
|
+
* position-dependent, so it is the library's `additionalTransfrom` (its
|
|
17
|
+
* spelling), handed back to 0 the moment the shopper leaves slide 0. What
|
|
18
|
+
* these lock is WHEN that prop is passed, and that a slider which has not
|
|
19
|
+
* opted in is handed the identical props it was handed before.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const carouselProps: Record<string, unknown>[] = [];
|
|
23
|
+
|
|
24
|
+
jest.mock('../../theme-block', () => ({
|
|
25
|
+
__esModule: true,
|
|
26
|
+
default: ({ block }: { block: { id: string } }) => (
|
|
27
|
+
<div data-testid="slide-content" data-id={block.id} />
|
|
28
|
+
)
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock('react-multi-carousel', () => ({
|
|
32
|
+
__esModule: true,
|
|
33
|
+
default: (props: Record<string, unknown>) => {
|
|
34
|
+
carouselProps.push(props);
|
|
35
|
+
return (
|
|
36
|
+
<div data-testid="carousel">{props.children as React.ReactNode}</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
import PostsSliderSection from '../posts-slider-section';
|
|
42
|
+
|
|
43
|
+
// The library's own maths, imported rather than reimplemented.
|
|
44
|
+
const { getItemClientSideWidth } =
|
|
45
|
+
require('react-multi-carousel/lib/utils') as {
|
|
46
|
+
getItemClientSideWidth: (
|
|
47
|
+
props: { slidesToShow?: number; centerMode?: boolean },
|
|
48
|
+
slidesToShow: number,
|
|
49
|
+
containerWidth: number
|
|
50
|
+
) => number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const slide = (id: string) => ({
|
|
54
|
+
id,
|
|
55
|
+
type: 'group',
|
|
56
|
+
label: id,
|
|
57
|
+
properties: {},
|
|
58
|
+
styles: {},
|
|
59
|
+
order: 0,
|
|
60
|
+
blocks: []
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const renderSection = (
|
|
64
|
+
properties: Record<string, unknown> = {},
|
|
65
|
+
slideCount = 6,
|
|
66
|
+
styles: Record<string, unknown> = {}
|
|
67
|
+
) => {
|
|
68
|
+
carouselProps.length = 0;
|
|
69
|
+
|
|
70
|
+
const result = render(
|
|
71
|
+
<PostsSliderSection
|
|
72
|
+
{...({
|
|
73
|
+
section: {
|
|
74
|
+
id: 'sec',
|
|
75
|
+
type: 'flash-deals-slider',
|
|
76
|
+
label: 'sec',
|
|
77
|
+
properties: { 'items-per-slide': 1, ...properties },
|
|
78
|
+
styles,
|
|
79
|
+
blocks: Array.from({ length: slideCount }, (_, i) => slide(`s${i}`))
|
|
80
|
+
},
|
|
81
|
+
currentBreakpoint: 'desktop',
|
|
82
|
+
placeholderId: 'ph',
|
|
83
|
+
isDesigner: false
|
|
84
|
+
} as any)}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return { ...result, props: carouselProps[carouselProps.length - 1] };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** The inline style of each slide wrapper, in order. */
|
|
92
|
+
const slidePadding = (container: HTMLElement) =>
|
|
93
|
+
Array.from(container.querySelectorAll('[data-testid="slide-content"]')).map(
|
|
94
|
+
(node) => {
|
|
95
|
+
const wrapper = node.parentElement as HTMLElement;
|
|
96
|
+
return {
|
|
97
|
+
left: wrapper.style.paddingLeft,
|
|
98
|
+
right: wrapper.style.paddingRight
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
describe('a slider that has not opted in', () => {
|
|
104
|
+
it('is handed no edge props at all', () => {
|
|
105
|
+
const { props } = renderSection();
|
|
106
|
+
|
|
107
|
+
expect('additionalTransfrom' in props).toBe(false);
|
|
108
|
+
expect('beforeChange' in props).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('keeps the half-gap on both sides of every slide', () => {
|
|
112
|
+
// gap defaults to 16, so slidePadding is 8 — unchanged from before.
|
|
113
|
+
const { container } = renderSection();
|
|
114
|
+
|
|
115
|
+
slidePadding(container).forEach((padding) => {
|
|
116
|
+
expect(padding).toEqual({ left: '8px', right: '8px' });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('flush edges', () => {
|
|
122
|
+
it('moves the whole gap to one side so the outer edges carry none', () => {
|
|
123
|
+
const { container } = renderSection({ 'flush-edges': true });
|
|
124
|
+
const padding = slidePadding(container);
|
|
125
|
+
|
|
126
|
+
// Card k ends at (k+1)·iw − gap and card k+1 starts at (k+1)·iw, so the
|
|
127
|
+
// visible gap between them is still exactly `gap`.
|
|
128
|
+
padding.slice(0, -1).forEach((entry) => {
|
|
129
|
+
expect(entry).toEqual({ left: '0px', right: '16px' });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('drops the last card’s own padding so its content reaches the edge', () => {
|
|
134
|
+
const { container } = renderSection({ 'flush-edges': true });
|
|
135
|
+
const padding = slidePadding(container);
|
|
136
|
+
|
|
137
|
+
expect(padding[padding.length - 1]).toEqual({ left: '0px', right: '0px' });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('is a no-op when there is no gap to move', () => {
|
|
141
|
+
// `gap` is a section STYLE, not a property.
|
|
142
|
+
const { container } = renderSection({ 'flush-edges': true }, 6, {
|
|
143
|
+
gap: 0
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
slidePadding(container).forEach((padding) => {
|
|
147
|
+
expect(padding).toEqual({ left: '0px', right: '0px' });
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('the lead-in', () => {
|
|
153
|
+
/**
|
|
154
|
+
* The shape the panel actually writes. Every authored property is stored per
|
|
155
|
+
* breakpoint (`{ desktop: 60 }`), and reading one without
|
|
156
|
+
* `getResponsiveValue` yields NaN — which fell back to 0 and disabled the
|
|
157
|
+
* feature silently while the editor showed the value as set. The flat number
|
|
158
|
+
* these tests used to pass hid exactly that.
|
|
159
|
+
*/
|
|
160
|
+
it('reads the per-breakpoint shape the panel stores', () => {
|
|
161
|
+
const { props } = renderSection({ 'lead-in': { desktop: 60 } });
|
|
162
|
+
|
|
163
|
+
expect(props.additionalTransfrom).toBe(60);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('resolves it at the breakpoint being rendered', () => {
|
|
167
|
+
const { props } = renderSection({
|
|
168
|
+
'lead-in': { desktop: 60, mobile: 12 }
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(props.additionalTransfrom).toBe(60);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('offsets the track while it sits at the start', () => {
|
|
175
|
+
const { props } = renderSection({ 'lead-in': 60 });
|
|
176
|
+
|
|
177
|
+
expect(props.additionalTransfrom).toBe(60);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('hands the offset back as soon as the slider moves', () => {
|
|
181
|
+
const { props } = renderSection({ 'lead-in': 60 });
|
|
182
|
+
|
|
183
|
+
act(() => {
|
|
184
|
+
(props.beforeChange as (next: number) => void)(1);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// The section re-renders; the newest props are the ones that matter.
|
|
188
|
+
expect(carouselProps[carouselProps.length - 1].additionalTransfrom).toBe(0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('takes it back when the slider returns to the start', () => {
|
|
192
|
+
const { props } = renderSection({ 'lead-in': 60 });
|
|
193
|
+
|
|
194
|
+
act(() => {
|
|
195
|
+
(props.beforeChange as (next: number) => void)(1);
|
|
196
|
+
});
|
|
197
|
+
act(() => {
|
|
198
|
+
(
|
|
199
|
+
carouselProps[carouselProps.length - 1].beforeChange as (
|
|
200
|
+
next: number
|
|
201
|
+
) => void
|
|
202
|
+
)(0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(carouselProps[carouselProps.length - 1].additionalTransfrom).toBe(
|
|
206
|
+
60
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('is refused in infinite mode, which never sits at slide 0', () => {
|
|
211
|
+
// The library starts on a clone at 2 × slidesToShow, so there is no start
|
|
212
|
+
// position for a lead-in to belong to.
|
|
213
|
+
const { props } = renderSection({
|
|
214
|
+
'lead-in': 60,
|
|
215
|
+
autoplay: true,
|
|
216
|
+
infinite: true
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect('additionalTransfrom' in props).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('is refused when there is nothing to scroll', () => {
|
|
223
|
+
const { props } = renderSection({ 'lead-in': 60, 'items-per-slide': 6 }, 6);
|
|
224
|
+
|
|
225
|
+
expect('additionalTransfrom' in props).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('is clamped rather than trusted', () => {
|
|
229
|
+
expect(renderSection({ 'lead-in': 9999 }).props.additionalTransfrom).toBe(
|
|
230
|
+
400
|
|
231
|
+
);
|
|
232
|
+
expect(
|
|
233
|
+
'additionalTransfrom' in renderSection({ 'lead-in': -20 }).props
|
|
234
|
+
).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Which edge the gap sits on. `left` is a lead-in at the start; `right` is its
|
|
240
|
+
* mirror, a trail-out at the far end — a right-hand gap at the START is not
|
|
241
|
+
* something the geometry can offer, because what sits against the right edge
|
|
242
|
+
* there is the sliced next card.
|
|
243
|
+
*/
|
|
244
|
+
describe('the gap edge', () => {
|
|
245
|
+
it('defaults to the left, so nothing authored before this moves', () => {
|
|
246
|
+
const { props } = renderSection({ 'lead-in': { desktop: 60 } });
|
|
247
|
+
|
|
248
|
+
expect(props.additionalTransfrom).toBe(60);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('opens on the right only once the track reaches the end', () => {
|
|
252
|
+
const authored = {
|
|
253
|
+
'lead-in': { desktop: 60 },
|
|
254
|
+
'lead-in-side': { desktop: 'right' },
|
|
255
|
+
'items-per-slide': 3.5
|
|
256
|
+
};
|
|
257
|
+
const { props } = renderSection(authored);
|
|
258
|
+
|
|
259
|
+
// At the start there is no trailing gap: the sliced card is there instead.
|
|
260
|
+
expect(props.additionalTransfrom).toBe(0);
|
|
261
|
+
|
|
262
|
+
// 6 slides at 3.5 per view ⇒ the library rests at 2.5.
|
|
263
|
+
act(() => {
|
|
264
|
+
(props.beforeChange as (next: number) => void)(2.5);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(carouselProps[carouselProps.length - 1].additionalTransfrom).toBe(
|
|
268
|
+
-60
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('closes again on the way back from the end', () => {
|
|
273
|
+
const { props } = renderSection({
|
|
274
|
+
'lead-in': { desktop: 60 },
|
|
275
|
+
'lead-in-side': { desktop: 'right' },
|
|
276
|
+
'items-per-slide': 3.5
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
act(() => {
|
|
280
|
+
(props.beforeChange as (next: number) => void)(2.5);
|
|
281
|
+
});
|
|
282
|
+
act(() => {
|
|
283
|
+
(
|
|
284
|
+
carouselProps[carouselProps.length - 1].beforeChange as (
|
|
285
|
+
next: number
|
|
286
|
+
) => void
|
|
287
|
+
)(1);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(carouselProps[carouselProps.length - 1].additionalTransfrom).toBe(0);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('refuses a side it does not know rather than guessing', () => {
|
|
294
|
+
const { props } = renderSection({
|
|
295
|
+
'lead-in': { desktop: 60 },
|
|
296
|
+
'lead-in-side': { desktop: 'middle' }
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Anything but `right` is the left edge, which is the documented default.
|
|
300
|
+
expect(props.additionalTransfrom).toBe(60);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('never opens both edges at once', () => {
|
|
304
|
+
const { props } = renderSection({
|
|
305
|
+
'lead-in': { desktop: 60 },
|
|
306
|
+
'lead-in-side': { desktop: 'left' },
|
|
307
|
+
'items-per-slide': 3.5
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(props.additionalTransfrom).toBe(60);
|
|
311
|
+
|
|
312
|
+
act(() => {
|
|
313
|
+
(props.beforeChange as (next: number) => void)(2.5);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// At the end, with the gap on the left, there is no offset at all.
|
|
317
|
+
expect(carouselProps[carouselProps.length - 1].additionalTransfrom).toBe(0);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('a fractional item count', () => {
|
|
322
|
+
it('reaches the library unrounded, which is what slices the next card', () => {
|
|
323
|
+
const { props } = renderSection({ 'items-per-slide': 3.5 });
|
|
324
|
+
const responsive = props.responsive as Record<string, { items: number }>;
|
|
325
|
+
|
|
326
|
+
expect(Object.values(responsive).some((entry) => entry.items === 3.5)).toBe(
|
|
327
|
+
true
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The geometry, from the library's own `getItemClientSideWidth`. What has to
|
|
334
|
+
* hold at every viewport: the next card is genuinely cut (not hidden, not
|
|
335
|
+
* fully visible), and the lead-in leaves enough of it to read as a peek.
|
|
336
|
+
*/
|
|
337
|
+
describe('the arithmetic of 3.5 items', () => {
|
|
338
|
+
const widths = [390, 768, 1280, 1440, 1630, 1920];
|
|
339
|
+
const items = 3.5;
|
|
340
|
+
|
|
341
|
+
it.each(widths)('slices the fourth card at %ipx', (width) => {
|
|
342
|
+
const itemWidth = getItemClientSideWidth({}, items, width);
|
|
343
|
+
const visibleWhole = Math.floor(items);
|
|
344
|
+
const consumed = visibleWhole * itemWidth;
|
|
345
|
+
|
|
346
|
+
// Strictly between: a fourth card exists on screen and is not complete.
|
|
347
|
+
expect(consumed).toBeLessThan(width);
|
|
348
|
+
expect(width - consumed).toBeLessThan(itemWidth);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it.each(widths)(
|
|
352
|
+
'keeps the last card flush within rounding at %ipx',
|
|
353
|
+
(width) => {
|
|
354
|
+
const itemWidth = getItemClientSideWidth({}, items, width);
|
|
355
|
+
|
|
356
|
+
// `itemWidth` is rounded per item, so the strip can miss the edge by at
|
|
357
|
+
// most half a pixel per visible item. Demanding an exact 0 would be a
|
|
358
|
+
// test that fails on arithmetic, not on layout.
|
|
359
|
+
expect(Math.abs(items * itemWidth - width)).toBeLessThanOrEqual(
|
|
360
|
+
items * 0.5
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The hard constraint, stated as a rule rather than as a blessed number: a
|
|
367
|
+
* lead-in wider than half a card eats the peek and the strip stops
|
|
368
|
+
* signalling that it scrolls. Measured: at 390px with 3.5 items a card is
|
|
369
|
+
* 111px, so a 60px lead-in is already over the line — mobile cannot take
|
|
370
|
+
* the desktop lead-in at this item count.
|
|
371
|
+
*/
|
|
372
|
+
it.each(widths)(
|
|
373
|
+
'admits a lead-in only under half a card at %ipx',
|
|
374
|
+
(width) => {
|
|
375
|
+
const itemWidth = getItemClientSideWidth({}, items, width);
|
|
376
|
+
const viable = itemWidth / 2;
|
|
377
|
+
const peek = (leadIn: number) =>
|
|
378
|
+
items - leadIn / itemWidth - Math.floor(items);
|
|
379
|
+
|
|
380
|
+
expect(peek(0)).toBeCloseTo(0.5, 1);
|
|
381
|
+
expect(peek(viable - 1)).toBeGreaterThan(0);
|
|
382
|
+
expect(peek(itemWidth)).toBeLessThanOrEqual(0);
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
it('rules out a 60px lead-in at phone width, and allows it from tablet up', () => {
|
|
387
|
+
const tooNarrow = getItemClientSideWidth({}, items, 390) / 2;
|
|
388
|
+
expect(60).toBeGreaterThan(tooNarrow);
|
|
389
|
+
|
|
390
|
+
[768, 1280, 1440, 1920].forEach((width) => {
|
|
391
|
+
expect(60).toBeLessThan(getItemClientSideWidth({}, items, width) / 2);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { act, render, screen } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The quick view asks commerce for nothing, and this is the guard on that.
|
|
6
|
+
*
|
|
7
|
+
* A by-pk fetch was added here to recover the "view product" link for rows
|
|
8
|
+
* that carry no URL, and then reverted: a collection data source is
|
|
9
|
+
* snapshotted from OMNITRON, so `row.pk` is an Omnitron product id, while the
|
|
10
|
+
* storefront's `/product/<pk>` is a different id space. Measured on a live
|
|
11
|
+
* store — row pk 48 (sku 8684464001616, a bed sheet) came back as pk 48, a
|
|
12
|
+
* glass serving plate — so the panel showed one product and linked another.
|
|
13
|
+
*
|
|
14
|
+
* These tests exist so that the next person to reach for that fetch sees a
|
|
15
|
+
* failing test with the reason in it, rather than rediscovering it in
|
|
16
|
+
* production. The right fix is upstream, where the widget is saved.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const asks: unknown[] = [];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Every `@akinon/next/data/client/*` subpath is folded onto one mock module by
|
|
23
|
+
* the jest moduleNameMapper, so one factory serves the product hooks and the
|
|
24
|
+
* wishlist hooks the favourite button needs. Each product hook RECORDS being
|
|
25
|
+
* called, which is what the assertions below read.
|
|
26
|
+
*/
|
|
27
|
+
function mockDataClient() {
|
|
28
|
+
return {
|
|
29
|
+
__esModule: true,
|
|
30
|
+
useGetProductByPkQuery: (pk: unknown, options?: { skip?: boolean }) => {
|
|
31
|
+
if (options?.skip !== true) asks.push({ hook: 'getProductByPk', pk });
|
|
32
|
+
return { data: undefined, isFetching: false, isError: false };
|
|
33
|
+
},
|
|
34
|
+
useGetProductByParamsQuery: (params: unknown) => {
|
|
35
|
+
asks.push({ hook: 'getProductByParams', params });
|
|
36
|
+
return { data: undefined, isFetching: false, isError: false };
|
|
37
|
+
},
|
|
38
|
+
useAddProductMutation: () => [
|
|
39
|
+
() => ({ unwrap: () => Promise.resolve({}) }),
|
|
40
|
+
{ isLoading: false }
|
|
41
|
+
],
|
|
42
|
+
useGetFavoritesQuery: () => ({ data: undefined, isFetching: false }),
|
|
43
|
+
useAddFavoriteMutation: () => [
|
|
44
|
+
() => ({ unwrap: () => Promise.resolve({}) }),
|
|
45
|
+
{ isLoading: false }
|
|
46
|
+
],
|
|
47
|
+
useRemoveFavoriteMutation: () => [
|
|
48
|
+
() => ({ unwrap: () => Promise.resolve({}) }),
|
|
49
|
+
{ isLoading: false }
|
|
50
|
+
]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
jest.mock('@akinon/next/data/client/product', mockDataClient);
|
|
55
|
+
jest.mock('@akinon/next/data/client/wishlist', mockDataClient);
|
|
56
|
+
|
|
57
|
+
jest.mock('../../theme-block', () => ({
|
|
58
|
+
__esModule: true,
|
|
59
|
+
default: ({ block }: { block: { id: string } }) => (
|
|
60
|
+
<a data-testid="card" data-id={block.id} href="">
|
|
61
|
+
card
|
|
62
|
+
</a>
|
|
63
|
+
)
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
jest.mock('react-multi-carousel', () => ({
|
|
67
|
+
__esModule: true,
|
|
68
|
+
default: ({ children }: { children?: React.ReactNode }) => (
|
|
69
|
+
<div data-testid="carousel">{children}</div>
|
|
70
|
+
)
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
import PostsSliderSection from '../posts-slider-section';
|
|
74
|
+
|
|
75
|
+
/** A row exactly as a collection data source snapshots it: no URL anywhere. */
|
|
76
|
+
const urllessRow = (pk: number, name: string) => ({
|
|
77
|
+
pk,
|
|
78
|
+
name,
|
|
79
|
+
sku: `sku-${pk}`,
|
|
80
|
+
active_price: { price: '2499.99', currency_type: 'try' },
|
|
81
|
+
productimage_set: [{ image: `/img/${pk}.jpg` }]
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const ITERATOR = {
|
|
85
|
+
id: 'iter',
|
|
86
|
+
label: 'Iterator',
|
|
87
|
+
isIterator: true,
|
|
88
|
+
order: 0,
|
|
89
|
+
properties: {},
|
|
90
|
+
styles: {},
|
|
91
|
+
blocks: [
|
|
92
|
+
{
|
|
93
|
+
id: 'card',
|
|
94
|
+
type: 'group',
|
|
95
|
+
label: 'Deal Card 1',
|
|
96
|
+
properties: { tag: 'a' },
|
|
97
|
+
styles: {},
|
|
98
|
+
blocks: []
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const renderSection = (products: Record<string, unknown>[]) =>
|
|
104
|
+
render(
|
|
105
|
+
<PostsSliderSection
|
|
106
|
+
{...({
|
|
107
|
+
section: {
|
|
108
|
+
id: 'sec',
|
|
109
|
+
type: 'flash-deals-slider',
|
|
110
|
+
label: 'sec',
|
|
111
|
+
properties: {
|
|
112
|
+
'items-per-slide': 1,
|
|
113
|
+
'show-quick-view': true,
|
|
114
|
+
'quick-view-link-label': 'ÜRÜNÜ GÖR'
|
|
115
|
+
},
|
|
116
|
+
styles: {},
|
|
117
|
+
blocks: [ITERATOR],
|
|
118
|
+
dataSource: { details: { collection: { products } } }
|
|
119
|
+
},
|
|
120
|
+
currentBreakpoint: 'desktop',
|
|
121
|
+
placeholderId: 'ph',
|
|
122
|
+
isDesigner: false
|
|
123
|
+
} as any)}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const openCard = (index = 0) => {
|
|
128
|
+
const card = screen.getAllByTestId('card')[index];
|
|
129
|
+
act(() => {
|
|
130
|
+
card.dispatchEvent(
|
|
131
|
+
new MouseEvent('click', { bubbles: true, cancelable: true, button: 0 })
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const link = () => document.querySelector('[data-part="quick-view-link"]');
|
|
137
|
+
const panel = () => document.querySelector('[data-part="quick-view-panel"]');
|
|
138
|
+
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
asks.length = 0;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('the quick view asks commerce for nothing', () => {
|
|
144
|
+
it('makes no product request when a card is opened', () => {
|
|
145
|
+
renderSection([urllessRow(48, 'Sarı Çarşaf')]);
|
|
146
|
+
|
|
147
|
+
openCard();
|
|
148
|
+
|
|
149
|
+
expect(panel()).not.toBeNull();
|
|
150
|
+
expect(asks).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('makes none even for a row whose pk looks perfectly askable', () => {
|
|
154
|
+
// The pk IS valid — in Omnitron. Asking the storefront with it is the bug.
|
|
155
|
+
renderSection([urllessRow(48, 'A'), urllessRow(49, 'B')]);
|
|
156
|
+
|
|
157
|
+
openCard(1);
|
|
158
|
+
|
|
159
|
+
expect(asks).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('what a row without an address gets', () => {
|
|
164
|
+
it('draws no link rather than one to the wrong product', () => {
|
|
165
|
+
renderSection([urllessRow(48, 'Sarı Çarşaf')]);
|
|
166
|
+
|
|
167
|
+
openCard();
|
|
168
|
+
|
|
169
|
+
expect(link()).toBeNull();
|
|
170
|
+
// Everything the row CAN answer is still shown.
|
|
171
|
+
expect(screen.getByText('Sarı Çarşaf')).toBeInTheDocument();
|
|
172
|
+
expect(
|
|
173
|
+
document.querySelector('[data-part="quick-view-price"]')?.textContent
|
|
174
|
+
).toContain('2.499,99');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('draws the link as soon as the row carries one', () => {
|
|
178
|
+
renderSection([{ ...urllessRow(48, 'Sarı'), absolute_url: '/carsaf/' }]);
|
|
179
|
+
|
|
180
|
+
openCard();
|
|
181
|
+
|
|
182
|
+
expect(link()).toHaveAttribute('href', '/carsaf/');
|
|
183
|
+
expect(link()).toHaveTextContent('ÜRÜNÜ GÖR');
|
|
184
|
+
expect(asks).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('accepts a row that names it `url` instead', () => {
|
|
188
|
+
renderSection([{ ...urllessRow(48, 'Sarı'), url: '/carsaf-alias/' }]);
|
|
189
|
+
|
|
190
|
+
openCard();
|
|
191
|
+
|
|
192
|
+
expect(link()).toHaveAttribute('href', '/carsaf-alias/');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('shows the row’s own name and price, never another product’s', () => {
|
|
196
|
+
renderSection([{ ...urllessRow(48, 'Sarı'), absolute_url: '/carsaf/' }]);
|
|
197
|
+
|
|
198
|
+
openCard();
|
|
199
|
+
|
|
200
|
+
expect(screen.getByText('Sarı')).toBeInTheDocument();
|
|
201
|
+
expect(
|
|
202
|
+
document.querySelector('[data-part="quick-view-media"]')
|
|
203
|
+
).toHaveAttribute('src', '/img/48.jpg');
|
|
204
|
+
});
|
|
205
|
+
});
|