@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.
- package/CHANGELOG.md +15 -31
- package/package.json +4 -4
- package/src/__tests__/__snapshots__/facet-filters-block.test.tsx.snap +11 -0
- package/src/__tests__/custom-css.test.ts +181 -0
- package/src/__tests__/designer-chrome-clipping.test.tsx +497 -0
- package/src/__tests__/facet-filters-block.test.tsx +707 -0
- package/src/__tests__/item-template.test.tsx +184 -0
- package/src/__tests__/listing-controls.test.tsx +961 -0
- package/src/__tests__/part-style-scope.test.tsx +229 -0
- package/src/__tests__/part-styles.test.ts +311 -0
- package/src/__tests__/registry-parity.test.ts +42 -0
- package/src/__tests__/row-visibility.test.ts +672 -0
- package/src/__tests__/sort-select-page-cursor.test.tsx +336 -0
- package/src/__tests__/theme-block-html-id.test.tsx +167 -0
- package/src/blocks/__tests__/__snapshots__/product-gallery-block.test.tsx.snap +15 -0
- package/src/blocks/__tests__/accordion-block-state.test.tsx +201 -0
- package/src/blocks/__tests__/facet-filters-block.test.tsx +190 -0
- package/src/blocks/__tests__/listing-context-card-model.test.tsx +230 -0
- package/src/blocks/__tests__/listing-lookup-hardening.test.ts +80 -0
- package/src/blocks/__tests__/product-gallery-block.test.tsx +974 -0
- package/src/blocks/__tests__/product-grid-composition.test.tsx +570 -0
- package/src/blocks/__tests__/product-grid-lookups.test.tsx +230 -0
- package/src/blocks/__tests__/product-price-block.test.tsx +248 -0
- package/src/blocks/__tests__/product-recommendations-block.test.tsx +702 -0
- package/src/blocks/__tests__/share-block.test.tsx +392 -0
- package/src/blocks/__tests__/store-locator-block.test.tsx +691 -0
- package/src/blocks/__tests__/variant-selector-block.test.tsx +817 -0
- package/src/blocks/__tests__/variant-selector-golden.json +20 -0
- package/src/blocks/account-menu-block.tsx +264 -0
- package/src/blocks/add-to-cart-block.tsx +417 -0
- package/src/blocks/address-list-block.tsx +348 -0
- package/src/blocks/applied-filters-block.tsx +288 -0
- package/src/blocks/auth-form-block.tsx +295 -0
- package/src/blocks/basket-items-block.tsx +457 -0
- package/src/blocks/basket-summary-block.tsx +404 -0
- package/src/blocks/block-renderer-registry.tsx +94 -0
- package/src/blocks/change-password-form-block.tsx +240 -0
- package/src/blocks/checkout-steps-block.tsx +288 -0
- package/src/blocks/checkout-summary-block.tsx +449 -0
- package/src/blocks/countdown-block.tsx +182 -0
- package/src/blocks/coupon-list-block.tsx +292 -0
- package/src/blocks/facet-filters-block.tsx +1686 -0
- package/src/blocks/faq-accordion-block.tsx +358 -0
- package/src/blocks/favorite-button-block.tsx +252 -0
- package/src/blocks/favourites-grid-block.tsx +510 -0
- package/src/blocks/forgot-password-block.tsx +155 -0
- package/src/blocks/item-template-utils.ts +131 -0
- package/src/blocks/item-template.tsx +164 -0
- package/src/blocks/listing-breadcrumb-block.tsx +175 -0
- package/src/blocks/listing-context.ts +850 -0
- package/src/blocks/listing-pagination-block.tsx +923 -0
- package/src/blocks/listing-result-count-block.tsx +126 -0
- package/src/blocks/listing-title-block.tsx +115 -0
- package/src/blocks/oauth-buttons-block.tsx +205 -0
- package/src/blocks/order-detail-block.tsx +474 -0
- package/src/blocks/order-list-block.tsx +274 -0
- package/src/blocks/order-success-block.tsx +641 -0
- package/src/blocks/order-tracking-block.tsx +533 -0
- package/src/blocks/product-attributes-block.tsx +307 -0
- package/src/blocks/product-block-utils.tsx +639 -0
- package/src/blocks/product-breadcrumb-block.tsx +215 -0
- package/src/blocks/product-description-block.tsx +169 -0
- package/src/blocks/product-gallery-block.tsx +1523 -0
- package/src/blocks/product-grid-block.tsx +966 -0
- package/src/blocks/product-price-block.tsx +373 -0
- package/src/blocks/product-recommendations-block.tsx +1433 -0
- package/src/blocks/product-title-block.tsx +129 -0
- package/src/blocks/profile-form-block.tsx +297 -0
- package/src/blocks/recently-viewed-store.ts +201 -0
- package/src/blocks/register-form-block.tsx +362 -0
- package/src/blocks/share-block.tsx +667 -0
- package/src/blocks/size-guide-block.tsx +313 -0
- package/src/blocks/sort-select-block.tsx +636 -0
- package/src/blocks/stock-status-block.tsx +168 -0
- package/src/blocks/store-locator-block.tsx +1184 -0
- package/src/blocks/variant-selector-block.tsx +1110 -0
- package/src/sections/__tests__/tablet-raw-bucket-readers.test.tsx +186 -0
- package/src/utils/__tests__/part-ownership-guard.test.tsx +29 -4
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fireEvent, render, screen, within } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
const mockPush = jest.fn();
|
|
5
|
+
let mockQuery = '';
|
|
6
|
+
|
|
7
|
+
jest.mock('next/navigation', () => ({
|
|
8
|
+
useRouter: () => ({ push: mockPush }),
|
|
9
|
+
usePathname: () => '/list',
|
|
10
|
+
useSearchParams: () => new URLSearchParams(mockQuery)
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import FacetFiltersBlock from '../blocks/facet-filters-block';
|
|
14
|
+
import { ThemePageContextProvider } from '../theme-page-context';
|
|
15
|
+
|
|
16
|
+
type Choice = { label: string; value: string; quantity?: number; selected?: boolean };
|
|
17
|
+
|
|
18
|
+
const facet = (
|
|
19
|
+
key: string,
|
|
20
|
+
name: string,
|
|
21
|
+
choices: Choice[],
|
|
22
|
+
widgetType = 'multiselect'
|
|
23
|
+
) => ({
|
|
24
|
+
key,
|
|
25
|
+
name,
|
|
26
|
+
search_key: `attributes_${key}`,
|
|
27
|
+
widget_type: widgetType,
|
|
28
|
+
data: {
|
|
29
|
+
choices: choices.map((choice) => ({
|
|
30
|
+
label: choice.label,
|
|
31
|
+
value: choice.value,
|
|
32
|
+
quantity: choice.quantity ?? 5,
|
|
33
|
+
is_selected: choice.selected ?? false
|
|
34
|
+
}))
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const COLOR = facet('color', 'Renk', [
|
|
39
|
+
{ label: 'Siyah', value: 'black', quantity: 12 },
|
|
40
|
+
{ label: 'Beyaz', value: 'white', quantity: 7 },
|
|
41
|
+
{ label: 'Lacivert', value: 'navy', quantity: 3 }
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const SIZE = facet('size', 'Beden', [
|
|
45
|
+
{ label: 'S', value: 's', quantity: 9 },
|
|
46
|
+
{ label: 'M', value: 'm', quantity: 4 }
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const pageContext = (facets: unknown[] = [COLOR, SIZE]) => ({
|
|
50
|
+
listing: {
|
|
51
|
+
title: 'Shoes',
|
|
52
|
+
path: '/list',
|
|
53
|
+
query: '',
|
|
54
|
+
products: [],
|
|
55
|
+
facets,
|
|
56
|
+
sorters: [],
|
|
57
|
+
pagination: {},
|
|
58
|
+
breadcrumbs: []
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const renderBlock = (
|
|
63
|
+
properties: Record<string, unknown> = {},
|
|
64
|
+
options: {
|
|
65
|
+
isDesigner?: boolean;
|
|
66
|
+
facets?: unknown[];
|
|
67
|
+
currentBreakpoint?: string;
|
|
68
|
+
} = {}
|
|
69
|
+
) =>
|
|
70
|
+
render(
|
|
71
|
+
<ThemePageContextProvider value={pageContext(options.facets)}>
|
|
72
|
+
<FacetFiltersBlock
|
|
73
|
+
block={
|
|
74
|
+
{
|
|
75
|
+
id: 'blk-1',
|
|
76
|
+
type: 'facet-filters',
|
|
77
|
+
label: 'Filters',
|
|
78
|
+
order: 0,
|
|
79
|
+
hidden: false,
|
|
80
|
+
value: '',
|
|
81
|
+
properties,
|
|
82
|
+
styles: {},
|
|
83
|
+
blocks: []
|
|
84
|
+
} as never
|
|
85
|
+
}
|
|
86
|
+
placeholderId="p"
|
|
87
|
+
sectionId="s"
|
|
88
|
+
isDesigner={options.isDesigner ?? false}
|
|
89
|
+
currentBreakpoint={options.currentBreakpoint ?? 'desktop'}
|
|
90
|
+
/>
|
|
91
|
+
</ThemePageContextProvider>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
mockPush.mockClear();
|
|
96
|
+
mockQuery = '';
|
|
97
|
+
document.body.style.overflow = '';
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The lock. These snapshots were taken from the shipped renderer BEFORE any of
|
|
102
|
+
* the option-search / selected-count / facet-order / staged-apply / dropdown-
|
|
103
|
+
* close work landed. Every one of those arrives off by default, so a
|
|
104
|
+
* composition that sets no property must keep producing exactly this markup —
|
|
105
|
+
* if a snapshot moves, a published storefront moved with it.
|
|
106
|
+
*/
|
|
107
|
+
describe('default render is frozen', () => {
|
|
108
|
+
it('renders the sidebar layout unchanged', () => {
|
|
109
|
+
const { container } = renderBlock();
|
|
110
|
+
expect(container.innerHTML).toMatchSnapshot();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('renders the topbar layout unchanged', () => {
|
|
114
|
+
const { container } = renderBlock({ layout: 'topbar' });
|
|
115
|
+
expect(container.innerHTML).toMatchSnapshot();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('renders the pills layout unchanged', () => {
|
|
119
|
+
const { container } = renderBlock({ layout: 'pills' });
|
|
120
|
+
expect(container.innerHTML).toMatchSnapshot();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('renders an opened dropdown unchanged', () => {
|
|
124
|
+
const { container } = renderBlock({ layout: 'dropdown' });
|
|
125
|
+
fireEvent.click(screen.getByRole('button', { name: /Renk/ }));
|
|
126
|
+
expect(container.innerHTML).toMatchSnapshot();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('renders an opened panel unchanged', () => {
|
|
130
|
+
const { container } = renderBlock({
|
|
131
|
+
layout: 'panel',
|
|
132
|
+
showClear: true,
|
|
133
|
+
showApply: true
|
|
134
|
+
});
|
|
135
|
+
fireEvent.click(screen.getByRole('button', { name: /Filters/ }));
|
|
136
|
+
expect(container.innerHTML).toMatchSnapshot();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('draws no search box and no count badge anywhere by default', () => {
|
|
140
|
+
const { container } = renderBlock({
|
|
141
|
+
layout: 'panel',
|
|
142
|
+
showClear: true,
|
|
143
|
+
showApply: true
|
|
144
|
+
});
|
|
145
|
+
fireEvent.click(screen.getByRole('button', { name: /Filters/ }));
|
|
146
|
+
|
|
147
|
+
expect(container.querySelector('input')).toBeNull();
|
|
148
|
+
expect(container.querySelector('[data-part="option-search"]')).toBeNull();
|
|
149
|
+
expect(container.querySelector('[data-part="selected-count"]')).toBeNull();
|
|
150
|
+
expect(container.querySelector('[data-part="trigger-count"]')).toBeNull();
|
|
151
|
+
expect(container.querySelector('[data-part="no-match"]')).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('keeps apply-as-you-pick: a tick navigates immediately', () => {
|
|
155
|
+
renderBlock({ layout: 'panel', showApply: true });
|
|
156
|
+
fireEvent.click(screen.getByRole('button', { name: /Filters/ }));
|
|
157
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
158
|
+
|
|
159
|
+
expect(mockPush).toHaveBeenCalledWith('/list?attributes_color=black');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('keeps the default Apply button cosmetic — it only closes', () => {
|
|
163
|
+
renderBlock({ layout: 'panel', showApply: true });
|
|
164
|
+
fireEvent.click(screen.getByRole('button', { name: /Filters/ }));
|
|
165
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
166
|
+
|
|
167
|
+
expect(mockPush).not.toHaveBeenCalled();
|
|
168
|
+
expect(screen.queryByRole('dialog')).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('option search', () => {
|
|
173
|
+
it('is absent until a designer turns it on', () => {
|
|
174
|
+
const { container } = renderBlock({ showOptionSearch: false });
|
|
175
|
+
expect(container.querySelector('[data-part="option-search"]')).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('narrows a group to the options that match', () => {
|
|
179
|
+
renderBlock({ showOptionSearch: true });
|
|
180
|
+
|
|
181
|
+
const input = screen.getAllByRole('searchbox')[0];
|
|
182
|
+
fireEvent.change(input, { target: { value: 'la' } });
|
|
183
|
+
|
|
184
|
+
// "Lacivert" survives; the other two colours do not. The size group is
|
|
185
|
+
// searched on its own term, so both of its options stay.
|
|
186
|
+
expect(screen.getByRole('button', { name: /Lacivert/ })).toBeTruthy();
|
|
187
|
+
expect(screen.queryByRole('button', { name: /Siyah/ })).toBeNull();
|
|
188
|
+
expect(screen.getByRole('button', { name: /^S/ })).toBeTruthy();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('matches without regard to case', () => {
|
|
192
|
+
renderBlock({ showOptionSearch: true });
|
|
193
|
+
fireEvent.change(screen.getAllByRole('searchbox')[0], {
|
|
194
|
+
target: { value: 'SIYAH' }
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(screen.getByRole('button', { name: /Siyah/ })).toBeTruthy();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('says so when nothing matches, and marks the list empty', () => {
|
|
201
|
+
const { container } = renderBlock({ showOptionSearch: true });
|
|
202
|
+
fireEvent.change(screen.getAllByRole('searchbox')[0], {
|
|
203
|
+
target: { value: 'zzzz' }
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(container.querySelector('[data-part="no-match"]')?.textContent).toBe(
|
|
207
|
+
'No matches'
|
|
208
|
+
);
|
|
209
|
+
const lists = container.querySelectorAll('[data-part="option-list"]');
|
|
210
|
+
expect(lists[0].getAttribute('data-state')).toContain('empty');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('only appears on groups past the option threshold', () => {
|
|
214
|
+
const { container } = renderBlock({
|
|
215
|
+
showOptionSearch: true,
|
|
216
|
+
optionSearchMinOptions: 3
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Renk has three options, Beden two.
|
|
220
|
+
expect(container.querySelectorAll('[data-part="option-search"]')).toHaveLength(
|
|
221
|
+
1
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('stays out of the pill row, which is navigation', () => {
|
|
226
|
+
const { container } = renderBlock({
|
|
227
|
+
layout: 'pills',
|
|
228
|
+
showOptionSearch: true
|
|
229
|
+
});
|
|
230
|
+
expect(container.querySelector('[data-part="option-search"]')).toBeNull();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('selected counts', () => {
|
|
235
|
+
const withSelection = [
|
|
236
|
+
facet('color', 'Renk', [
|
|
237
|
+
{ label: 'Siyah', value: 'black', selected: true },
|
|
238
|
+
{ label: 'Beyaz', value: 'white', selected: true },
|
|
239
|
+
{ label: 'Lacivert', value: 'navy' }
|
|
240
|
+
]),
|
|
241
|
+
SIZE
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
it('puts the count on the group heading when asked', () => {
|
|
245
|
+
const { container } = renderBlock(
|
|
246
|
+
{ showSelectedCount: true },
|
|
247
|
+
{ facets: withSelection }
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const badge = container.querySelector('[data-part="selected-count"]');
|
|
251
|
+
expect(badge?.textContent).toBe('(2)');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('honours the designer template', () => {
|
|
255
|
+
const { container } = renderBlock(
|
|
256
|
+
{ showSelectedCount: true, selectedCountTemplate: '[{count}]' },
|
|
257
|
+
{ facets: withSelection }
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
expect(
|
|
261
|
+
container.querySelector('[data-part="selected-count"]')?.textContent
|
|
262
|
+
).toBe('[2]');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('shows nothing on a group with no selection', () => {
|
|
266
|
+
const { container } = renderBlock(
|
|
267
|
+
{ showSelectedCount: true },
|
|
268
|
+
{ facets: withSelection }
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const groups = container.querySelectorAll('[data-facet-group]');
|
|
272
|
+
expect(
|
|
273
|
+
(groups[1] as HTMLElement).querySelector('[data-part="selected-count"]')
|
|
274
|
+
).toBeNull();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('counts every applied filter on the panel trigger', () => {
|
|
278
|
+
const { container } = renderBlock(
|
|
279
|
+
{ layout: 'panel', showTriggerCount: true },
|
|
280
|
+
{ facets: withSelection }
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
expect(
|
|
284
|
+
container.querySelector('[data-part="trigger-count"]')?.textContent
|
|
285
|
+
).toBe('(2)');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('drops the trigger badge when nothing is applied', () => {
|
|
289
|
+
const { container } = renderBlock({
|
|
290
|
+
layout: 'panel',
|
|
291
|
+
showTriggerCount: true
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(container.querySelector('[data-part="trigger-count"]')).toBeNull();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('facet order', () => {
|
|
299
|
+
it('follows commerce when the designer names no order', () => {
|
|
300
|
+
const { container } = renderBlock();
|
|
301
|
+
const groups = Array.from(container.querySelectorAll('[data-facet-group]'));
|
|
302
|
+
expect(groups.map((g) => g.getAttribute('data-facet-group'))).toEqual([
|
|
303
|
+
'color',
|
|
304
|
+
'size'
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('puts the named facets first, in the order given', () => {
|
|
309
|
+
const { container } = renderBlock({ facetOrder: 'size' });
|
|
310
|
+
const groups = Array.from(container.querySelectorAll('[data-facet-group]'));
|
|
311
|
+
expect(groups.map((g) => g.getAttribute('data-facet-group'))).toEqual([
|
|
312
|
+
'size',
|
|
313
|
+
'color'
|
|
314
|
+
]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('matches the display name a designer reads off the page', () => {
|
|
318
|
+
const { container } = renderBlock({ facetOrder: 'Beden, Renk' });
|
|
319
|
+
const groups = Array.from(container.querySelectorAll('[data-facet-group]'));
|
|
320
|
+
expect(groups.map((g) => g.getAttribute('data-facet-group'))).toEqual([
|
|
321
|
+
'size',
|
|
322
|
+
'color'
|
|
323
|
+
]);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe('staged apply', () => {
|
|
328
|
+
const staged = {
|
|
329
|
+
layout: 'panel',
|
|
330
|
+
showApply: true,
|
|
331
|
+
showClear: true,
|
|
332
|
+
applyMode: 'staged'
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const openPanel = () =>
|
|
336
|
+
fireEvent.click(screen.getByRole('button', { name: /Filters/ }));
|
|
337
|
+
|
|
338
|
+
it('writes no URL while the shopper is picking', () => {
|
|
339
|
+
renderBlock(staged);
|
|
340
|
+
openPanel();
|
|
341
|
+
|
|
342
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
343
|
+
fireEvent.click(screen.getByRole('button', { name: /Beyaz/ }));
|
|
344
|
+
|
|
345
|
+
expect(mockPush).not.toHaveBeenCalled();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('writes one URL carrying every staged choice on Apply', () => {
|
|
349
|
+
renderBlock(staged);
|
|
350
|
+
openPanel();
|
|
351
|
+
|
|
352
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
353
|
+
fireEvent.click(screen.getByRole('button', { name: /^M/ }));
|
|
354
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
355
|
+
|
|
356
|
+
expect(mockPush).toHaveBeenCalledTimes(1);
|
|
357
|
+
expect(mockPush).toHaveBeenCalledWith(
|
|
358
|
+
'/list?attributes_color=black&attributes_size=m'
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('shows the shopper what is staged and what is applied', () => {
|
|
363
|
+
const { container } = renderBlock(staged);
|
|
364
|
+
openPanel();
|
|
365
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
366
|
+
|
|
367
|
+
const option = container.querySelector('[data-facet-choice="black"]');
|
|
368
|
+
expect(option?.getAttribute('data-state')).toContain('selected');
|
|
369
|
+
expect(option?.getAttribute('data-state')).toContain('staged');
|
|
370
|
+
|
|
371
|
+
const untouched = container.querySelector('[data-facet-choice="white"]');
|
|
372
|
+
expect(untouched?.getAttribute('data-state') || '').not.toContain('staged');
|
|
373
|
+
|
|
374
|
+
expect(
|
|
375
|
+
container
|
|
376
|
+
.querySelector('[data-part="apply"]')
|
|
377
|
+
?.getAttribute('data-state')
|
|
378
|
+
).toContain('staged');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('marks a staged un-tick as staged too', () => {
|
|
382
|
+
mockQuery = 'attributes_color=black';
|
|
383
|
+
const { container } = renderBlock(staged, {
|
|
384
|
+
facets: [
|
|
385
|
+
facet('color', 'Renk', [
|
|
386
|
+
{ label: 'Siyah', value: 'black', selected: true },
|
|
387
|
+
{ label: 'Beyaz', value: 'white' }
|
|
388
|
+
]),
|
|
389
|
+
SIZE
|
|
390
|
+
]
|
|
391
|
+
});
|
|
392
|
+
openPanel();
|
|
393
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
394
|
+
|
|
395
|
+
const option = container.querySelector('[data-facet-choice="black"]');
|
|
396
|
+
expect(option?.getAttribute('data-state') || '').not.toContain('selected');
|
|
397
|
+
expect(option?.getAttribute('data-state')).toContain('staged');
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('forgets a staged choice when the shopper abandons the panel', () => {
|
|
401
|
+
const { container } = renderBlock(staged);
|
|
402
|
+
openPanel();
|
|
403
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
404
|
+
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
|
405
|
+
|
|
406
|
+
expect(mockPush).not.toHaveBeenCalled();
|
|
407
|
+
|
|
408
|
+
openPanel();
|
|
409
|
+
expect(
|
|
410
|
+
container
|
|
411
|
+
.querySelector('[data-facet-choice="black"]')
|
|
412
|
+
?.getAttribute('data-state') || ''
|
|
413
|
+
).not.toContain('staged');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('drops the page cursor when the staged URL is written', () => {
|
|
417
|
+
mockQuery = 'page=4';
|
|
418
|
+
renderBlock(staged);
|
|
419
|
+
openPanel();
|
|
420
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
421
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
422
|
+
|
|
423
|
+
expect(mockPush).toHaveBeenCalledWith('/list?attributes_color=black');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The round trip. Staging's whole promise is one page load instead of one
|
|
428
|
+
* per tap, so a panel the shopper ended up not changing must cost ZERO —
|
|
429
|
+
* in App Router a push is a real navigation (RSC refetch, scroll to top, a
|
|
430
|
+
* history entry that makes Back look broken).
|
|
431
|
+
*/
|
|
432
|
+
it('writes no URL when the shopper undoes every staged choice', () => {
|
|
433
|
+
const { container } = renderBlock(staged);
|
|
434
|
+
openPanel();
|
|
435
|
+
|
|
436
|
+
fireEvent.click(screen.getByRole('button', { name: /^M/ }));
|
|
437
|
+
fireEvent.click(screen.getByRole('button', { name: /^M/ }));
|
|
438
|
+
|
|
439
|
+
expect(
|
|
440
|
+
container.querySelector('[data-part="apply"]')?.getAttribute('data-state') ||
|
|
441
|
+
''
|
|
442
|
+
).not.toContain('staged');
|
|
443
|
+
|
|
444
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
445
|
+
|
|
446
|
+
expect(mockPush).not.toHaveBeenCalled();
|
|
447
|
+
expect(screen.queryByRole('dialog')).toBeNull();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('writes no URL for Clear All on an already-clean panel', () => {
|
|
451
|
+
// `handleClear` stages unconditionally, so an untouched panel ends up with
|
|
452
|
+
// a staged query that is identical to the URL.
|
|
453
|
+
renderBlock(staged);
|
|
454
|
+
openPanel();
|
|
455
|
+
fireEvent.click(screen.getByRole('button', { name: 'Clear all' }));
|
|
456
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
457
|
+
|
|
458
|
+
expect(mockPush).not.toHaveBeenCalled();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* The page cursor is dropped while staging because a filter change
|
|
463
|
+
* invalidates it — but its absence is not itself a change. Comparing it
|
|
464
|
+
* lights Apply up on a panel nothing differs in, and committing then throws
|
|
465
|
+
* the shopper from page 4 back to page 1 for nothing.
|
|
466
|
+
*/
|
|
467
|
+
it('does not count the dropped page cursor as a staged change', () => {
|
|
468
|
+
mockQuery = 'attributes_size=m&page=4';
|
|
469
|
+
const { container } = renderBlock(staged, {
|
|
470
|
+
facets: [
|
|
471
|
+
facet('size', 'Beden', [
|
|
472
|
+
{ label: 'S', value: 's' },
|
|
473
|
+
{ label: 'M', value: 'm', selected: true }
|
|
474
|
+
]),
|
|
475
|
+
COLOR
|
|
476
|
+
]
|
|
477
|
+
});
|
|
478
|
+
openPanel();
|
|
479
|
+
|
|
480
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
481
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
482
|
+
|
|
483
|
+
expect(
|
|
484
|
+
container.querySelector('[data-part="apply"]')?.getAttribute('data-state') ||
|
|
485
|
+
''
|
|
486
|
+
).not.toContain('staged');
|
|
487
|
+
|
|
488
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
489
|
+
|
|
490
|
+
expect(mockPush).not.toHaveBeenCalled();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('still commits when a real change also dropped the page cursor', () => {
|
|
494
|
+
mockQuery = 'attributes_size=m&page=4';
|
|
495
|
+
renderBlock(staged, {
|
|
496
|
+
facets: [
|
|
497
|
+
facet('size', 'Beden', [
|
|
498
|
+
{ label: 'S', value: 's' },
|
|
499
|
+
{ label: 'M', value: 'm', selected: true }
|
|
500
|
+
]),
|
|
501
|
+
COLOR
|
|
502
|
+
]
|
|
503
|
+
});
|
|
504
|
+
openPanel();
|
|
505
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
506
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
507
|
+
|
|
508
|
+
expect(mockPush).toHaveBeenCalledWith(
|
|
509
|
+
'/list?attributes_size=m&attributes_color=black'
|
|
510
|
+
);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* The page cursor is free text, so a merchant can type a key a facet
|
|
515
|
+
* already owns into it. Honouring that would take the facet out of the
|
|
516
|
+
* staged-versus-applied comparison AND off every URL this block writes —
|
|
517
|
+
* the shopper's choice would be dropped on the floor by a control that
|
|
518
|
+
* looks like it worked.
|
|
519
|
+
*/
|
|
520
|
+
describe('a page cursor a facet already owns', () => {
|
|
521
|
+
it('commits a choice made under the facet\'s own key', () => {
|
|
522
|
+
renderBlock({ ...staged, pageQueryParam: 'attributes_color' });
|
|
523
|
+
openPanel();
|
|
524
|
+
|
|
525
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
526
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
527
|
+
|
|
528
|
+
expect(mockPush).toHaveBeenCalledWith('/list?attributes_color=black');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('leaves that facet on the URL when another filter changes', () => {
|
|
532
|
+
mockQuery = 'attributes_color=black';
|
|
533
|
+
renderBlock(
|
|
534
|
+
{ layout: 'sidebar', pageQueryParam: 'attributes_color' },
|
|
535
|
+
{
|
|
536
|
+
facets: [
|
|
537
|
+
facet('color', 'Renk', [
|
|
538
|
+
{ label: 'Siyah', value: 'black', selected: true },
|
|
539
|
+
{ label: 'Beyaz', value: 'white' }
|
|
540
|
+
]),
|
|
541
|
+
SIZE
|
|
542
|
+
]
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
fireEvent.click(screen.getByRole('button', { name: /^M/ }));
|
|
547
|
+
|
|
548
|
+
expect(mockPush).toHaveBeenCalledWith(
|
|
549
|
+
'/list?attributes_color=black&attributes_size=m'
|
|
550
|
+
);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('still drops the real cursor it fell back to', () => {
|
|
554
|
+
mockQuery = 'page=4';
|
|
555
|
+
renderBlock({ ...staged, pageQueryParam: 'attributes_color' });
|
|
556
|
+
openPanel();
|
|
557
|
+
|
|
558
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
559
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
560
|
+
|
|
561
|
+
expect(mockPush).toHaveBeenCalledWith('/list?attributes_color=black');
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('stages Clear All rather than navigating on the spot', () => {
|
|
566
|
+
mockQuery = 'attributes_color=black';
|
|
567
|
+
renderBlock(staged, {
|
|
568
|
+
facets: [
|
|
569
|
+
facet('color', 'Renk', [
|
|
570
|
+
{ label: 'Siyah', value: 'black', selected: true },
|
|
571
|
+
{ label: 'Beyaz', value: 'white' }
|
|
572
|
+
]),
|
|
573
|
+
SIZE
|
|
574
|
+
]
|
|
575
|
+
});
|
|
576
|
+
openPanel();
|
|
577
|
+
fireEvent.click(screen.getByRole('button', { name: 'Clear all' }));
|
|
578
|
+
|
|
579
|
+
expect(mockPush).not.toHaveBeenCalled();
|
|
580
|
+
expect(screen.getByRole('dialog')).toBeTruthy();
|
|
581
|
+
|
|
582
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
583
|
+
expect(mockPush).toHaveBeenCalledWith('/list');
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('reads the group count off the staged selection, the trigger off the applied one', () => {
|
|
587
|
+
// The URL carries what commerce reports as applied — that agreement is
|
|
588
|
+
// what every href in this block is already built on.
|
|
589
|
+
mockQuery = 'attributes_color=black';
|
|
590
|
+
const { container } = renderBlock(
|
|
591
|
+
{ ...staged, showSelectedCount: true, showTriggerCount: true },
|
|
592
|
+
{
|
|
593
|
+
facets: [
|
|
594
|
+
facet('color', 'Renk', [
|
|
595
|
+
{ label: 'Siyah', value: 'black', selected: true },
|
|
596
|
+
{ label: 'Beyaz', value: 'white' }
|
|
597
|
+
])
|
|
598
|
+
]
|
|
599
|
+
}
|
|
600
|
+
);
|
|
601
|
+
openPanel();
|
|
602
|
+
fireEvent.click(screen.getByRole('button', { name: /Beyaz/ }));
|
|
603
|
+
|
|
604
|
+
expect(
|
|
605
|
+
container.querySelector('[data-part="selected-count"]')?.textContent
|
|
606
|
+
).toBe('(2)');
|
|
607
|
+
expect(
|
|
608
|
+
container.querySelector('[data-part="trigger-count"]')?.textContent
|
|
609
|
+
).toBe('(1)');
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('marks only the group the shopper actually changed', () => {
|
|
613
|
+
const { container } = renderBlock({
|
|
614
|
+
...staged,
|
|
615
|
+
showSelectedCount: true
|
|
616
|
+
});
|
|
617
|
+
openPanel();
|
|
618
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
619
|
+
|
|
620
|
+
const badges = container.querySelectorAll('[data-part="selected-count"]');
|
|
621
|
+
// Only Renk has a staged tick; Beden has none, so it draws no badge at all.
|
|
622
|
+
expect(badges).toHaveLength(1);
|
|
623
|
+
expect(badges[0].getAttribute('data-state')).toContain('staged');
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('is inert in the designer', () => {
|
|
627
|
+
const { container } = renderBlock(staged, { isDesigner: true });
|
|
628
|
+
openPanel();
|
|
629
|
+
fireEvent.click(screen.getByRole('button', { name: /Siyah/ }));
|
|
630
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
631
|
+
|
|
632
|
+
expect(mockPush).not.toHaveBeenCalled();
|
|
633
|
+
expect(container).toBeTruthy();
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe('dropdown close control', () => {
|
|
638
|
+
// The floating list is addressed through its data hook, not its part: the
|
|
639
|
+
// dropdown spreads `panel` over it after `option-list`, so `data-part` reads
|
|
640
|
+
// "panel" in this layout.
|
|
641
|
+
const openList = (container: HTMLElement) =>
|
|
642
|
+
container.querySelector('[data-facet-choices]');
|
|
643
|
+
|
|
644
|
+
it('draws none by default', () => {
|
|
645
|
+
const { container } = renderBlock({ layout: 'dropdown' });
|
|
646
|
+
fireEvent.click(screen.getByRole('button', { name: /Renk/ }));
|
|
647
|
+
|
|
648
|
+
expect(openList(container)).toBeTruthy();
|
|
649
|
+
expect(container.querySelector('[data-part="close"]')).toBeNull();
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('closes the floating panel from inside it', () => {
|
|
653
|
+
const { container } = renderBlock({
|
|
654
|
+
layout: 'dropdown',
|
|
655
|
+
showDropdownClose: true
|
|
656
|
+
});
|
|
657
|
+
fireEvent.click(screen.getByRole('button', { name: /Renk/ }));
|
|
658
|
+
|
|
659
|
+
const close = container.querySelector('[data-part="close"]');
|
|
660
|
+
expect(close).toBeTruthy();
|
|
661
|
+
fireEvent.click(close as Element);
|
|
662
|
+
|
|
663
|
+
expect(openList(container)).toBeNull();
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('leaves an outside click alone unless the designer asked', () => {
|
|
667
|
+
const { container } = renderBlock({ layout: 'dropdown' });
|
|
668
|
+
fireEvent.click(screen.getByRole('button', { name: /Renk/ }));
|
|
669
|
+
fireEvent.mouseDown(document.body);
|
|
670
|
+
|
|
671
|
+
expect(openList(container)).toBeTruthy();
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('dismisses on an outside click when asked', () => {
|
|
675
|
+
const { container } = renderBlock({
|
|
676
|
+
layout: 'dropdown',
|
|
677
|
+
closeOnOutsideClick: true
|
|
678
|
+
});
|
|
679
|
+
fireEvent.click(screen.getByRole('button', { name: /Renk/ }));
|
|
680
|
+
expect(openList(container)).toBeTruthy();
|
|
681
|
+
|
|
682
|
+
fireEvent.mouseDown(document.body);
|
|
683
|
+
expect(openList(container)).toBeNull();
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('keeps a click inside the block from dismissing it', () => {
|
|
687
|
+
const { container } = renderBlock({
|
|
688
|
+
layout: 'dropdown',
|
|
689
|
+
closeOnOutsideClick: true
|
|
690
|
+
});
|
|
691
|
+
fireEvent.click(screen.getByRole('button', { name: /Renk/ }));
|
|
692
|
+
fireEvent.mouseDown(openList(container) as Element);
|
|
693
|
+
|
|
694
|
+
expect(openList(container)).toBeTruthy();
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('never listens for outside clicks in the designer', () => {
|
|
698
|
+
const { container } = renderBlock(
|
|
699
|
+
{ layout: 'dropdown', closeOnOutsideClick: true },
|
|
700
|
+
{ isDesigner: true }
|
|
701
|
+
);
|
|
702
|
+
fireEvent.click(screen.getByRole('button', { name: /Renk/ }));
|
|
703
|
+
fireEvent.mouseDown(document.body);
|
|
704
|
+
|
|
705
|
+
expect(openList(container)).toBeTruthy();
|
|
706
|
+
});
|
|
707
|
+
});
|