@akinon/pz-theme 2.0.152-rc.3 → 2.0.153-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.
- package/CHANGELOG.md +8 -52
- package/package.json +4 -4
- package/src/blocks/__tests__/add-to-cart-action.test.tsx +1 -0
- package/src/blocks/__tests__/add-to-cart-stock-alert.test.tsx +673 -0
- package/src/blocks/add-to-cart-block.tsx +400 -10
- package/src/blocks/item-template.tsx +19 -25
- package/src/blocks/order-list-block.tsx +2 -25
- package/src/blocks/product-block-utils.tsx +40 -0
- package/src/blocks/stock-alert-pick.ts +106 -0
- package/src/blocks/variant-selector-block.tsx +167 -9
- package/src/chrome/__tests__/page-context-builders.test.ts +1 -14
- package/src/chrome/page-context.ts +0 -4
- package/src/custom/README.md +0 -20
- package/src/overlays/overlay-surface.tsx +21 -12
- package/src/__tests__/row-scope.test.tsx +0 -118
- package/src/blocks/__tests__/order-list-rows.test.tsx +0 -112
- package/src/blocks/row-scope.tsx +0 -35
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import AddToCartBlock from '../add-to-cart-block';
|
|
5
|
+
import VariantSelectorBlock from '../variant-selector-block';
|
|
6
|
+
import { ThemePageContextProvider } from '../../theme-page-context';
|
|
7
|
+
import {
|
|
8
|
+
__resetStockAlertPickForTests,
|
|
9
|
+
setStockAlertPick
|
|
10
|
+
} from '../stock-alert-pick';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ZERO-5233: the PDP button says "notify me" for a sold-out product or a
|
|
14
|
+
* sold-out size the shopper picked, and "sold out" only for a product with
|
|
15
|
+
* nothing left to come back.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
let currentPath = '/p/s';
|
|
19
|
+
|
|
20
|
+
jest.mock('next/navigation', () => ({
|
|
21
|
+
useRouter: () => ({ push: jest.fn() }),
|
|
22
|
+
usePathname: () => currentPath,
|
|
23
|
+
useSearchParams: () => new URLSearchParams('')
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
let sessionStatus: 'authenticated' | 'unauthenticated' = 'authenticated';
|
|
27
|
+
let sessionEmail = 'shopper@example.com';
|
|
28
|
+
|
|
29
|
+
jest.mock('next-auth/react', () => ({
|
|
30
|
+
useSession: () => ({
|
|
31
|
+
status: sessionStatus,
|
|
32
|
+
data:
|
|
33
|
+
sessionStatus === 'authenticated'
|
|
34
|
+
? { user: { email: sessionEmail } }
|
|
35
|
+
: null
|
|
36
|
+
})
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const mockAddProduct = jest.fn();
|
|
40
|
+
const mockAddStockAlert = jest.fn();
|
|
41
|
+
|
|
42
|
+
jest.mock('@akinon/next/data/client/product', () => ({
|
|
43
|
+
__esModule: true,
|
|
44
|
+
useAddProductMutation: () => [
|
|
45
|
+
(payload: unknown) => mockAddProduct(payload),
|
|
46
|
+
{ isLoading: false }
|
|
47
|
+
],
|
|
48
|
+
useAddStockAlertMutation: () => [
|
|
49
|
+
(payload: unknown) => mockAddStockAlert(payload),
|
|
50
|
+
{ isLoading: false }
|
|
51
|
+
],
|
|
52
|
+
basketApi: {
|
|
53
|
+
util: { invalidateTags: () => ({ type: 'basket/invalidateTags' }) }
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
jest.mock('@akinon/next/redux/hooks', () => ({
|
|
58
|
+
__esModule: true,
|
|
59
|
+
useAppSelector: () => undefined,
|
|
60
|
+
useAppDispatch: () => () => undefined
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
const SIZES = {
|
|
64
|
+
attribute_key: 'integration_beden',
|
|
65
|
+
attribute_name: 'Beden',
|
|
66
|
+
options: [
|
|
67
|
+
{
|
|
68
|
+
label: 'S',
|
|
69
|
+
value: 's',
|
|
70
|
+
in_stock: true,
|
|
71
|
+
is_selectable: true,
|
|
72
|
+
product: { pk: 101, absolute_url: '/p/s', stock: 9 }
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: 'M',
|
|
76
|
+
value: 'm',
|
|
77
|
+
in_stock: false,
|
|
78
|
+
is_selectable: false,
|
|
79
|
+
product: { pk: 102, absolute_url: '/p/m', stock: 0 }
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const SOLD_OUT_SIZES = {
|
|
85
|
+
...SIZES,
|
|
86
|
+
options: SIZES.options.map((option) => ({
|
|
87
|
+
...option,
|
|
88
|
+
in_stock: false,
|
|
89
|
+
is_selectable: false,
|
|
90
|
+
product: { ...option.product, stock: 0 }
|
|
91
|
+
}))
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const IN_STOCK = { pk: 101, name: 'Polo', in_stock: true, variants: [SIZES] };
|
|
95
|
+
// The page's own size is gone, another size of the colour is not.
|
|
96
|
+
const SOLD_OUT = { pk: 101, name: 'Polo', in_stock: false, variants: [SIZES] };
|
|
97
|
+
// Every size of the colour is gone.
|
|
98
|
+
const COLOUR_GONE = {
|
|
99
|
+
pk: 101,
|
|
100
|
+
name: 'Polo',
|
|
101
|
+
in_stock: false,
|
|
102
|
+
variants: [SOLD_OUT_SIZES]
|
|
103
|
+
};
|
|
104
|
+
const DELISTED = { pk: 101, name: 'Polo', in_stock: false, variants: [] };
|
|
105
|
+
|
|
106
|
+
const LABELS = {
|
|
107
|
+
label: 'Sepete Ekle',
|
|
108
|
+
outOfStockLabel: 'Tükendi',
|
|
109
|
+
stockAlertLabel: 'Stok Gelince Haber Ver',
|
|
110
|
+
stockAlertTitle: 'Stok alarmı',
|
|
111
|
+
stockAlertMessage: 'Ürün stoğa girdiğinde {email} adresine haber vereceğiz.',
|
|
112
|
+
stockAlertMessageNoEmail: 'Ürün stoğa girdiğinde haber vereceğiz.',
|
|
113
|
+
stockAlertErrorMessage: 'Bir hata oluştu.',
|
|
114
|
+
stockAlertCloseLabel: 'Kapat'
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const block = (type: string, properties: Record<string, unknown>) =>
|
|
118
|
+
({
|
|
119
|
+
id: type,
|
|
120
|
+
type,
|
|
121
|
+
label: type,
|
|
122
|
+
value: '',
|
|
123
|
+
order: 0,
|
|
124
|
+
hidden: false,
|
|
125
|
+
properties,
|
|
126
|
+
styles: {}
|
|
127
|
+
} as never);
|
|
128
|
+
|
|
129
|
+
const renderPdp = (
|
|
130
|
+
product: Record<string, unknown>,
|
|
131
|
+
{
|
|
132
|
+
alert = true,
|
|
133
|
+
selector = false,
|
|
134
|
+
extra = {}
|
|
135
|
+
}: {
|
|
136
|
+
alert?: boolean;
|
|
137
|
+
selector?: boolean;
|
|
138
|
+
extra?: Record<string, unknown>;
|
|
139
|
+
} = {}
|
|
140
|
+
) =>
|
|
141
|
+
render(
|
|
142
|
+
<ThemePageContextProvider value={{ product } as never}>
|
|
143
|
+
{selector && (
|
|
144
|
+
<VariantSelectorBlock
|
|
145
|
+
{...({
|
|
146
|
+
block: block('variant-selector', {
|
|
147
|
+
stockAlert: true,
|
|
148
|
+
stockAlertPresentation: 'add-to-cart'
|
|
149
|
+
}),
|
|
150
|
+
currentBreakpoint: 'desktop'
|
|
151
|
+
} as never)}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
<AddToCartBlock
|
|
155
|
+
{...({
|
|
156
|
+
block: block('add-to-cart', {
|
|
157
|
+
...LABELS,
|
|
158
|
+
stockAlert: alert,
|
|
159
|
+
...extra
|
|
160
|
+
}),
|
|
161
|
+
currentBreakpoint: 'desktop'
|
|
162
|
+
} as never)}
|
|
163
|
+
/>
|
|
164
|
+
</ThemePageContextProvider>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const button = (container: HTMLElement) =>
|
|
168
|
+
container.querySelector('[data-add-to-cart]') as HTMLButtonElement;
|
|
169
|
+
|
|
170
|
+
const dialog = () => document.querySelector('[role="dialog"]') as HTMLElement;
|
|
171
|
+
|
|
172
|
+
const press = async (container: HTMLElement) => {
|
|
173
|
+
await act(async () => {
|
|
174
|
+
fireEvent.click(button(container));
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const withLocation = async (run: (assign: jest.Mock) => Promise<void>) => {
|
|
179
|
+
const assign = jest.fn();
|
|
180
|
+
const original = window.location;
|
|
181
|
+
Object.defineProperty(window, 'location', {
|
|
182
|
+
configurable: true,
|
|
183
|
+
value: { ...original, pathname: '/polo-101/', search: '?a=1', assign }
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
await run(assign);
|
|
188
|
+
} finally {
|
|
189
|
+
Object.defineProperty(window, 'location', {
|
|
190
|
+
configurable: true,
|
|
191
|
+
value: original
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
beforeEach(() => {
|
|
197
|
+
sessionStatus = 'authenticated';
|
|
198
|
+
sessionEmail = 'shopper@example.com';
|
|
199
|
+
currentPath = '/p/s';
|
|
200
|
+
mockAddProduct.mockReset();
|
|
201
|
+
mockAddStockAlert.mockReset();
|
|
202
|
+
mockAddStockAlert.mockReturnValue({ unwrap: () => Promise.resolve({}) });
|
|
203
|
+
__resetStockAlertPickForTests();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('add-to-cart stock alert', () => {
|
|
207
|
+
it('keeps the disabled out-of-stock button while the switch is off', () => {
|
|
208
|
+
const { container } = renderPdp(SOLD_OUT, { alert: false });
|
|
209
|
+
|
|
210
|
+
expect(button(container).textContent).toBe('Tükendi');
|
|
211
|
+
expect(button(container).disabled).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('adds to cart as before while the product is in stock', () => {
|
|
215
|
+
const { container } = renderPdp(IN_STOCK);
|
|
216
|
+
|
|
217
|
+
expect(button(container).textContent).toBe('Sepete Ekle');
|
|
218
|
+
expect(button(container).getAttribute('data-add-to-cart-state')).toBe(
|
|
219
|
+
'idle'
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('offers the alert while another size of the colour is in stock', () => {
|
|
224
|
+
const { container } = renderPdp(SOLD_OUT);
|
|
225
|
+
|
|
226
|
+
expect(button(container).textContent).toBe('Stok Gelince Haber Ver');
|
|
227
|
+
expect(button(container).disabled).toBe(false);
|
|
228
|
+
expect(button(container).getAttribute('data-state')).toBe('unavailable');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('says sold out when every size of the colour is gone', () => {
|
|
232
|
+
const { container } = renderPdp(COLOUR_GONE);
|
|
233
|
+
|
|
234
|
+
expect(button(container).textContent).toBe('Tükendi');
|
|
235
|
+
expect(button(container).disabled).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('says sold out for a product with no variants left', () => {
|
|
239
|
+
const { container } = renderPdp(DELISTED);
|
|
240
|
+
|
|
241
|
+
expect(button(container).textContent).toBe('Tükendi');
|
|
242
|
+
expect(button(container).disabled).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('registers a signed-in shopper and names their email in a dialog', async () => {
|
|
246
|
+
const { container } = renderPdp(SOLD_OUT);
|
|
247
|
+
|
|
248
|
+
expect(dialog()).toBeNull();
|
|
249
|
+
|
|
250
|
+
await press(container);
|
|
251
|
+
|
|
252
|
+
expect(mockAddStockAlert).toHaveBeenCalledWith({
|
|
253
|
+
productPk: 101,
|
|
254
|
+
email: 'shopper@example.com'
|
|
255
|
+
});
|
|
256
|
+
expect(mockAddProduct).not.toHaveBeenCalled();
|
|
257
|
+
expect(dialog().textContent).toContain('Stok alarmı');
|
|
258
|
+
expect(dialog().textContent).toContain(
|
|
259
|
+
'Ürün stoğa girdiğinde shopper@example.com adresine haber vereceğiz.'
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Only the × closes it: no second close button under the message.
|
|
263
|
+
expect(dialog().querySelectorAll('button')).toHaveLength(1);
|
|
264
|
+
expect(dialog().querySelector('[data-part="title"]')?.textContent).toBe(
|
|
265
|
+
'Stok alarmı'
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
act(() => {
|
|
269
|
+
fireEvent.click(screen.getByRole('button', { name: 'Kapat' }));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(dialog()).toBeNull();
|
|
273
|
+
|
|
274
|
+
// Already on the list: the dialog again, no second request.
|
|
275
|
+
await press(container);
|
|
276
|
+
|
|
277
|
+
expect(mockAddStockAlert).toHaveBeenCalledTimes(1);
|
|
278
|
+
expect(dialog()).not.toBeNull();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('opens as a drawer on the side the author picked', async () => {
|
|
282
|
+
const { container } = renderPdp(SOLD_OUT, {
|
|
283
|
+
extra: { stockAlertPresentation: 'drawer', stockAlertWidth: 420 }
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await press(container);
|
|
287
|
+
|
|
288
|
+
// jsdom drops `min()`, so the side and the full height stand in for it.
|
|
289
|
+
expect(dialog().style.position).toBe('absolute');
|
|
290
|
+
expect(dialog().style.right).toBe('0px');
|
|
291
|
+
expect(dialog().style.top).toBe('0px');
|
|
292
|
+
expect(dialog().style.bottom).toBe('0px');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('opens as a bottom sheet with the backdrop the author picked', async () => {
|
|
296
|
+
const { container } = renderPdp(SOLD_OUT, {
|
|
297
|
+
extra: {
|
|
298
|
+
stockAlertPresentation: 'sheet',
|
|
299
|
+
stockAlertSheetHeight: 87,
|
|
300
|
+
stockAlertBackdropColor: '#00321B',
|
|
301
|
+
stockAlertBackdropOpacity: 85
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await press(container);
|
|
306
|
+
|
|
307
|
+
expect(dialog().style.bottom).toBe('0px');
|
|
308
|
+
expect(dialog().style.maxHeight).toBe('87vh');
|
|
309
|
+
expect(
|
|
310
|
+
(document.querySelector('[data-part="overlay"]') as HTMLElement).style
|
|
311
|
+
.background
|
|
312
|
+
).toBe('rgba(0, 50, 27, 0.85)');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('closes with the brand icon glyph when one is set', async () => {
|
|
316
|
+
const { container } = renderPdp(SOLD_OUT, {
|
|
317
|
+
extra: {
|
|
318
|
+
stockAlertCloseIcon: 'pz-icon-close"><script>',
|
|
319
|
+
stockAlertCloseIconSize: 12
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await press(container);
|
|
324
|
+
|
|
325
|
+
const close = screen.getByRole('button', { name: 'Kapat' });
|
|
326
|
+
const glyph = close.querySelector('i') as HTMLElement;
|
|
327
|
+
|
|
328
|
+
expect(close.querySelector('svg')).toBeNull();
|
|
329
|
+
// Only class-name characters reach `className`.
|
|
330
|
+
expect(glyph.className).toBe('flex pz-icon-closescript');
|
|
331
|
+
expect(glyph.style.fontSize).toBe('12px');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('tells the shopper when commerce refuses the alert', async () => {
|
|
335
|
+
mockAddStockAlert.mockReturnValue({
|
|
336
|
+
unwrap: () => Promise.reject({ status: 400 })
|
|
337
|
+
});
|
|
338
|
+
const { container } = renderPdp(SOLD_OUT);
|
|
339
|
+
|
|
340
|
+
await press(container);
|
|
341
|
+
|
|
342
|
+
expect(dialog().textContent).toContain('Bir hata oluştu.');
|
|
343
|
+
expect(dialog().textContent).not.toContain('shopper@example.com');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('sends a guest to sign in with the page as the way back', async () => {
|
|
347
|
+
sessionStatus = 'unauthenticated';
|
|
348
|
+
|
|
349
|
+
await withLocation(async (assign) => {
|
|
350
|
+
const { container } = renderPdp(SOLD_OUT);
|
|
351
|
+
|
|
352
|
+
await press(container);
|
|
353
|
+
|
|
354
|
+
expect(mockAddStockAlert).not.toHaveBeenCalled();
|
|
355
|
+
expect(assign).toHaveBeenCalledWith(
|
|
356
|
+
`/auth?callbackUrl=${encodeURIComponent('/polo-101/?a=1')}`
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('sends a shopper whose session lapsed to sign in too', async () => {
|
|
362
|
+
mockAddStockAlert.mockReturnValue({
|
|
363
|
+
unwrap: () => Promise.reject({ status: 401 })
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
await withLocation(async (assign) => {
|
|
367
|
+
const { container } = renderPdp(SOLD_OUT, {
|
|
368
|
+
extra: { stockAlertReturnParam: 'next' }
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
await press(container);
|
|
372
|
+
|
|
373
|
+
expect(assign).toHaveBeenCalledWith(
|
|
374
|
+
`/auth?next=${encodeURIComponent('/polo-101/?a=1')}`
|
|
375
|
+
);
|
|
376
|
+
expect(dialog()).toBeNull();
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('leaves the email out of the message for an account without one', async () => {
|
|
381
|
+
sessionEmail = '';
|
|
382
|
+
const { container } = renderPdp(SOLD_OUT);
|
|
383
|
+
|
|
384
|
+
await press(container);
|
|
385
|
+
|
|
386
|
+
expect(mockAddStockAlert).toHaveBeenCalledWith({
|
|
387
|
+
productPk: 101,
|
|
388
|
+
email: ''
|
|
389
|
+
});
|
|
390
|
+
expect(dialog().textContent).toContain(
|
|
391
|
+
'Ürün stoğa girdiğinde haber vereceğiz.'
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('carries the picked size on the way back from sign-in', async () => {
|
|
396
|
+
sessionStatus = 'unauthenticated';
|
|
397
|
+
act(() => {
|
|
398
|
+
setStockAlertPick({
|
|
399
|
+
pagePk: 101,
|
|
400
|
+
productPk: 102,
|
|
401
|
+
optionLabel: 'M',
|
|
402
|
+
attributeKey: 'integration_beden'
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
await withLocation(async (assign) => {
|
|
407
|
+
const { container } = renderPdp(IN_STOCK);
|
|
408
|
+
|
|
409
|
+
await press(container);
|
|
410
|
+
|
|
411
|
+
expect(assign).toHaveBeenCalledWith(
|
|
412
|
+
`/auth?callbackUrl=${encodeURIComponent(
|
|
413
|
+
'/polo-101/?a=1&stock_alert=102'
|
|
414
|
+
)}`
|
|
415
|
+
);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('ignores a pick made on another page', () => {
|
|
420
|
+
act(() => {
|
|
421
|
+
setStockAlertPick({
|
|
422
|
+
pagePk: 999,
|
|
423
|
+
productPk: 102,
|
|
424
|
+
optionLabel: 'M',
|
|
425
|
+
attributeKey: 'integration_beden'
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const { container } = renderPdp(IN_STOCK);
|
|
430
|
+
|
|
431
|
+
expect(button(container).textContent).toBe('Sepete Ekle');
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe('variant selector handing a sold-out size to the button', () => {
|
|
436
|
+
const sizeOption = (container: HTMLElement, label: string) =>
|
|
437
|
+
Array.from(
|
|
438
|
+
container.querySelectorAll('[data-variant-option]')
|
|
439
|
+
).find((node) => node.textContent === label) as HTMLElement;
|
|
440
|
+
|
|
441
|
+
it('turns the button into the alert for the picked size, and back', async () => {
|
|
442
|
+
const { container } = renderPdp(IN_STOCK, { selector: true });
|
|
443
|
+
const m = sizeOption(container, 'M');
|
|
444
|
+
|
|
445
|
+
expect(m.tagName).toBe('BUTTON');
|
|
446
|
+
expect(m.getAttribute('aria-pressed')).toBe('false');
|
|
447
|
+
|
|
448
|
+
act(() => {
|
|
449
|
+
fireEvent.click(m);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
expect(m.getAttribute('aria-pressed')).toBe('true');
|
|
453
|
+
expect(m.getAttribute('data-state')).toBe('disabled selected');
|
|
454
|
+
expect(button(container).textContent).toBe('Stok Gelince Haber Ver');
|
|
455
|
+
|
|
456
|
+
await act(async () => {
|
|
457
|
+
fireEvent.click(button(container));
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
expect(mockAddStockAlert).toHaveBeenCalledWith({
|
|
461
|
+
productPk: 102,
|
|
462
|
+
email: 'shopper@example.com'
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
act(() => {
|
|
466
|
+
fireEvent.click(sizeOption(container, 'M'));
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
expect(button(container).textContent).toBe('Sepete Ekle');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('drops the pick when an in-stock size is picked, even the one on the page', () => {
|
|
473
|
+
const { container } = renderPdp(IN_STOCK, { selector: true });
|
|
474
|
+
|
|
475
|
+
act(() => {
|
|
476
|
+
fireEvent.click(sizeOption(container, 'M'));
|
|
477
|
+
});
|
|
478
|
+
expect(button(container).textContent).toBe('Stok Gelince Haber Ver');
|
|
479
|
+
|
|
480
|
+
act(() => {
|
|
481
|
+
fireEvent.click(sizeOption(container, 'S'));
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
expect(button(container).textContent).toBe('Sepete Ekle');
|
|
485
|
+
expect(sizeOption(container, 'M').getAttribute('aria-pressed')).toBe(
|
|
486
|
+
'false'
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('drops the pick on navigation, so coming back does not restore it', () => {
|
|
491
|
+
const view = renderPdp(IN_STOCK, { selector: true });
|
|
492
|
+
|
|
493
|
+
act(() => {
|
|
494
|
+
fireEvent.click(sizeOption(view.container, 'M'));
|
|
495
|
+
});
|
|
496
|
+
expect(button(view.container).textContent).toBe('Stok Gelince Haber Ver');
|
|
497
|
+
|
|
498
|
+
// Another colour (the blocks remount on the new page), then back.
|
|
499
|
+
view.unmount();
|
|
500
|
+
currentPath = '/p/other';
|
|
501
|
+
renderPdp({ ...IN_STOCK, pk: 301 }, { selector: true }).unmount();
|
|
502
|
+
currentPath = '/p/s';
|
|
503
|
+
|
|
504
|
+
const back = renderPdp(IN_STOCK, { selector: true });
|
|
505
|
+
|
|
506
|
+
expect(button(back.container).textContent).toBe('Sepete Ekle');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('picks the size again when sign-in hands the shopper back', () => {
|
|
510
|
+
window.history.replaceState(null, '', '/p/s?a=1&stock_alert=102');
|
|
511
|
+
|
|
512
|
+
const { container } = renderPdp(IN_STOCK, { selector: true });
|
|
513
|
+
|
|
514
|
+
expect(button(container).textContent).toBe('Stok Gelince Haber Ver');
|
|
515
|
+
expect(sizeOption(container, 'M').getAttribute('aria-pressed')).toBe(
|
|
516
|
+
'true'
|
|
517
|
+
);
|
|
518
|
+
expect(window.location.search).toBe('?a=1');
|
|
519
|
+
|
|
520
|
+
window.history.replaceState(null, '', '/');
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('only lets the size row hand a pick over, never an outer one', () => {
|
|
524
|
+
const product = {
|
|
525
|
+
...IN_STOCK,
|
|
526
|
+
variants: [
|
|
527
|
+
{
|
|
528
|
+
attribute_key: 'integration_renk',
|
|
529
|
+
attribute_name: 'Renk',
|
|
530
|
+
options: [
|
|
531
|
+
{
|
|
532
|
+
label: 'Mavi',
|
|
533
|
+
value: 'mavi',
|
|
534
|
+
in_stock: true,
|
|
535
|
+
is_selected: true,
|
|
536
|
+
is_selectable: true,
|
|
537
|
+
product: { pk: 101, absolute_url: '/p/s', stock: 9 }
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
label: 'Kırmızı',
|
|
541
|
+
value: 'kirmizi',
|
|
542
|
+
in_stock: false,
|
|
543
|
+
is_selectable: false,
|
|
544
|
+
product: { pk: 201, absolute_url: '/p/red', stock: 0 }
|
|
545
|
+
}
|
|
546
|
+
]
|
|
547
|
+
},
|
|
548
|
+
SIZES
|
|
549
|
+
]
|
|
550
|
+
};
|
|
551
|
+
const { container } = renderPdp(product, { selector: true });
|
|
552
|
+
|
|
553
|
+
expect(sizeOption(container, 'Kırmızı').tagName).toBe('SPAN');
|
|
554
|
+
expect(sizeOption(container, 'M').tagName).toBe('BUTTON');
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('leaves a sold-out size inert when the colour has no size left', () => {
|
|
558
|
+
const { container } = renderPdp(COLOUR_GONE, { selector: true });
|
|
559
|
+
|
|
560
|
+
expect(sizeOption(container, 'M').tagName).toBe('SPAN');
|
|
561
|
+
expect(container.querySelector('form')).toBeNull();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('leaves a sold-out size inert while the switch is off', () => {
|
|
565
|
+
const { container } = render(
|
|
566
|
+
<ThemePageContextProvider value={{ product: IN_STOCK } as never}>
|
|
567
|
+
<VariantSelectorBlock
|
|
568
|
+
{...({
|
|
569
|
+
block: block('variant-selector', { stockAlert: false }),
|
|
570
|
+
currentBreakpoint: 'desktop'
|
|
571
|
+
} as never)}
|
|
572
|
+
/>
|
|
573
|
+
</ThemePageContextProvider>
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
expect(sizeOption(container, 'M').tagName).toBe('SPAN');
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Commerce's real shape: a size the colour does not have is filled with that
|
|
582
|
+
* size of ANOTHER colour, and a colour that is sold out altogether has every
|
|
583
|
+
* size filled that way — in stock.
|
|
584
|
+
*/
|
|
585
|
+
describe('only the colour on the page counts', () => {
|
|
586
|
+
const size = (
|
|
587
|
+
label: string,
|
|
588
|
+
pk: number,
|
|
589
|
+
renk: string,
|
|
590
|
+
inStock: boolean
|
|
591
|
+
) => ({
|
|
592
|
+
label,
|
|
593
|
+
value: label.toLowerCase(),
|
|
594
|
+
in_stock: inStock,
|
|
595
|
+
is_selectable: inStock,
|
|
596
|
+
product: {
|
|
597
|
+
pk,
|
|
598
|
+
absolute_url: `/p/${pk}`,
|
|
599
|
+
stock: inStock ? 9 : 0,
|
|
600
|
+
attributes: { integration_renk: renk }
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const page = (renk: string, sizes: ReturnType<typeof size>[]) => ({
|
|
605
|
+
pk: 500,
|
|
606
|
+
name: 'Polo',
|
|
607
|
+
in_stock: false,
|
|
608
|
+
attributesKeyed: { integration_renk: renk },
|
|
609
|
+
variants: [
|
|
610
|
+
{
|
|
611
|
+
attribute_key: 'integration_renk',
|
|
612
|
+
options: [{ label: renk, value: renk, in_stock: false }]
|
|
613
|
+
},
|
|
614
|
+
{ attribute_key: 'integration_beden', options: sizes }
|
|
615
|
+
]
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('says sold out on a colour whose every size is gone, whatever the row lists', () => {
|
|
619
|
+
// The yellow page: S and M are the green and the brown ones.
|
|
620
|
+
const yellow = page('714', [
|
|
621
|
+
size('XS', 501, '714', false),
|
|
622
|
+
size('S', 502, '308', true),
|
|
623
|
+
size('M', 503, '284', true)
|
|
624
|
+
]);
|
|
625
|
+
const { container } = renderPdp(yellow, { selector: true });
|
|
626
|
+
|
|
627
|
+
expect(button(container).textContent).toBe('Tükendi');
|
|
628
|
+
expect(
|
|
629
|
+
Array.from(container.querySelectorAll('[data-variant-option]')).find(
|
|
630
|
+
(node) => node.textContent === 'XS'
|
|
631
|
+
)?.tagName
|
|
632
|
+
).toBe('SPAN');
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('offers the alert while one of the colour own sizes is in stock', () => {
|
|
636
|
+
const { container } = renderPdp(
|
|
637
|
+
page('714', [size('XS', 501, '714', false), size('S', 502, '714', true)])
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
expect(button(container).textContent).toBe('Stok Gelince Haber Ver');
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('lets another colour stand-in size be picked, as gant_zero did', async () => {
|
|
644
|
+
const white = {
|
|
645
|
+
...page('110', [
|
|
646
|
+
// XS of grey stands in for the white XS that does not exist.
|
|
647
|
+
size('XS', 601, '95', false),
|
|
648
|
+
size('M', 602, '110', false),
|
|
649
|
+
size('L', 603, '110', true)
|
|
650
|
+
]),
|
|
651
|
+
in_stock: true
|
|
652
|
+
};
|
|
653
|
+
const { container } = renderPdp(white, { selector: true });
|
|
654
|
+
const option = (label: string) =>
|
|
655
|
+
Array.from(container.querySelectorAll('[data-variant-option]')).find(
|
|
656
|
+
(node) => node.textContent === label
|
|
657
|
+
) as HTMLElement;
|
|
658
|
+
|
|
659
|
+
expect(option('XS').tagName).toBe('BUTTON');
|
|
660
|
+
expect(option('M').tagName).toBe('BUTTON');
|
|
661
|
+
|
|
662
|
+
act(() => {
|
|
663
|
+
fireEvent.click(option('XS'));
|
|
664
|
+
});
|
|
665
|
+
await press(container);
|
|
666
|
+
|
|
667
|
+
// The alert goes to the product commerce sent for the size.
|
|
668
|
+
expect(mockAddStockAlert).toHaveBeenCalledWith({
|
|
669
|
+
productPk: 601,
|
|
670
|
+
email: 'shopper@example.com'
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
});
|