@akinon/pz-theme 2.0.37-rc.0 → 2.0.37
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 +7 -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,570 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import ProductGridBlock, {
|
|
4
|
+
buildInsertionRules,
|
|
5
|
+
interleaveCells,
|
|
6
|
+
placeInsertions
|
|
7
|
+
} from '../product-grid-block';
|
|
8
|
+
import { ThemePageContextProvider } from '../../theme-page-context';
|
|
9
|
+
import type { Block } from '../../theme-block';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The grid gained two authorable compositions: a merchandiser tile that takes
|
|
13
|
+
* a cell of the grid, and the no-results layout that replaces the grid.
|
|
14
|
+
*
|
|
15
|
+
* The first block of tests is the one that matters most — with nothing
|
|
16
|
+
* authored the DOM has to be what it was before any of this existed. The
|
|
17
|
+
* golden below was captured by rendering the previous renderer, so it is a
|
|
18
|
+
* record of the old output rather than a description of the new one.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const CARD_A =
|
|
22
|
+
'<a href="/a" data-product-card="" data-part="card" style="display: flex; flex-direction: column; gap: 8px; text-decoration: none;">' +
|
|
23
|
+
'<img alt="A" loading="lazy" data-product-card-image="" data-part="image" style="width: 100%; height: auto; display: block; object-fit: cover;" src="/a.jpg">' +
|
|
24
|
+
'<span data-product-card-name="" data-part="name">A</span>' +
|
|
25
|
+
'<span data-product-card-price="" data-part="price" style="display: flex; align-items: center; gap: 8px;">' +
|
|
26
|
+
'<s data-product-card-retail-price="" data-part="list-price" style="opacity: 0.6; font-size: 0.9em;">$200.00</s>' +
|
|
27
|
+
'<span data-part="sale-price">$100.00</span></span></a>';
|
|
28
|
+
|
|
29
|
+
const CARD_B =
|
|
30
|
+
'<a href="/b" data-product-card="" data-part="card" style="display: flex; flex-direction: column; gap: 8px; text-decoration: none;">' +
|
|
31
|
+
'<span data-product-card-image="" data-part="image" data-state="empty" style="display: block; width: 100%; background: rgb(241, 245, 249);"></span>' +
|
|
32
|
+
'<span data-product-card-name="" data-part="name">B</span>' +
|
|
33
|
+
'<span data-product-card-price="" data-part="price" style="display: flex; align-items: center; gap: 8px;">' +
|
|
34
|
+
'<span data-part="sale-price">$50.00</span></span></a>';
|
|
35
|
+
|
|
36
|
+
const CARD_C =
|
|
37
|
+
'<a href="/c" data-product-card="" data-part="card" style="display: flex; flex-direction: column; gap: 8px; text-decoration: none;">' +
|
|
38
|
+
'<img alt="C" loading="lazy" data-product-card-image="" data-part="image" style="width: 100%; height: auto; display: block; object-fit: cover;" src="/c.jpg">' +
|
|
39
|
+
'<span data-product-card-name="" data-part="name">C</span>' +
|
|
40
|
+
'<span data-product-card-price="" data-part="price" style="display: flex; align-items: center; gap: 8px;">' +
|
|
41
|
+
'<span data-part="sale-price">$75.00</span></span></a>';
|
|
42
|
+
|
|
43
|
+
const DEFAULT_HTML =
|
|
44
|
+
'<div data-product-grid="" data-product-grid-id="grid-1" data-part="list" style="display: grid; gap: var(--product-grid-gap, 16px);">' +
|
|
45
|
+
'<style>[data-product-grid-id="grid-1"]{grid-template-columns:repeat(4,minmax(0,1fr));}' +
|
|
46
|
+
'@media (max-width:1023px){[data-product-grid-id="grid-1"]{grid-template-columns:repeat(2,minmax(0,1fr));}}</style>' +
|
|
47
|
+
CARD_A +
|
|
48
|
+
CARD_B +
|
|
49
|
+
CARD_C +
|
|
50
|
+
'</div>';
|
|
51
|
+
|
|
52
|
+
const DESIGNER_PLACEHOLDER_HTML =
|
|
53
|
+
'<div data-product-grid="" style="display: inline-block; padding: 8px 12px; ' +
|
|
54
|
+
'border: 1px dashed #cbd5e1; border-radius: 4px; color: rgb(100, 116, 139); font-size: 12px;">' +
|
|
55
|
+
'Product grid (no listing data)</div>';
|
|
56
|
+
|
|
57
|
+
const product = (n: number, over: Record<string, unknown> = {}) => ({
|
|
58
|
+
pk: n,
|
|
59
|
+
name: `P${n}`,
|
|
60
|
+
url: `/p${n}`,
|
|
61
|
+
image: `/p${n}.jpg`,
|
|
62
|
+
price: 10 * n,
|
|
63
|
+
...over
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const listingContext = (
|
|
67
|
+
products: Record<string, unknown>[],
|
|
68
|
+
pagination: Record<string, unknown> = { current_page: 1, count: 99 }
|
|
69
|
+
) => ({
|
|
70
|
+
listing: {
|
|
71
|
+
title: 'Shoes',
|
|
72
|
+
path: '/shoes',
|
|
73
|
+
products,
|
|
74
|
+
facets: [{ key: 'colour' }],
|
|
75
|
+
sorters: [],
|
|
76
|
+
pagination
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const THREE_PRODUCTS = listingContext([
|
|
81
|
+
{ pk: 1, name: 'A', url: '/a', image: '/a.jpg', price: 100, retail_price: 200 },
|
|
82
|
+
{ pk: 2, name: 'B', url: '/b', image: '', price: 50 },
|
|
83
|
+
{ pk: 3, name: 'C', url: '/c', image: '/c.jpg', price: 75 }
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const child = (
|
|
87
|
+
role: string,
|
|
88
|
+
properties: Record<string, unknown> = {},
|
|
89
|
+
over: Partial<Block> = {}
|
|
90
|
+
): Block =>
|
|
91
|
+
({
|
|
92
|
+
id: 'tile-1',
|
|
93
|
+
type: 'group',
|
|
94
|
+
label: 'Campaign',
|
|
95
|
+
order: 0,
|
|
96
|
+
hidden: false,
|
|
97
|
+
value: '',
|
|
98
|
+
properties: { role: { desktop: role }, ...properties },
|
|
99
|
+
styles: {},
|
|
100
|
+
blocks: [
|
|
101
|
+
{
|
|
102
|
+
id: 'tile-copy',
|
|
103
|
+
type: 'text',
|
|
104
|
+
label: 'Copy',
|
|
105
|
+
order: 0,
|
|
106
|
+
hidden: false,
|
|
107
|
+
value: 'Winter campaign',
|
|
108
|
+
properties: {},
|
|
109
|
+
styles: {},
|
|
110
|
+
blocks: []
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
...over
|
|
114
|
+
} as Block);
|
|
115
|
+
|
|
116
|
+
const gridBlock = (
|
|
117
|
+
properties: Record<string, unknown> = {},
|
|
118
|
+
blocks: Block[] = []
|
|
119
|
+
): Block =>
|
|
120
|
+
({
|
|
121
|
+
id: 'grid-1',
|
|
122
|
+
type: 'product-grid',
|
|
123
|
+
label: 'Grid',
|
|
124
|
+
order: 0,
|
|
125
|
+
hidden: false,
|
|
126
|
+
value: '',
|
|
127
|
+
properties,
|
|
128
|
+
styles: {},
|
|
129
|
+
blocks
|
|
130
|
+
} as Block);
|
|
131
|
+
|
|
132
|
+
const draw = (
|
|
133
|
+
block: Block,
|
|
134
|
+
context: unknown,
|
|
135
|
+
isDesigner = false,
|
|
136
|
+
currentBreakpoint = 'desktop'
|
|
137
|
+
) =>
|
|
138
|
+
render(
|
|
139
|
+
<ThemePageContextProvider value={context as Record<string, unknown>}>
|
|
140
|
+
<ProductGridBlock
|
|
141
|
+
block={block}
|
|
142
|
+
placeholderId="p"
|
|
143
|
+
sectionId="s"
|
|
144
|
+
isDesigner={isDesigner}
|
|
145
|
+
currentBreakpoint={currentBreakpoint}
|
|
146
|
+
/>
|
|
147
|
+
</ThemePageContextProvider>
|
|
148
|
+
).container;
|
|
149
|
+
|
|
150
|
+
/** The grid's cells in DOM order, with the stylesheet dropped. */
|
|
151
|
+
const cellSequence = (container: HTMLElement): string[] =>
|
|
152
|
+
Array.from(
|
|
153
|
+
container.querySelector('[data-product-grid]')?.children ?? []
|
|
154
|
+
)
|
|
155
|
+
.filter((element) => element.tagName !== 'STYLE')
|
|
156
|
+
.map((element) =>
|
|
157
|
+
element.hasAttribute('data-product-grid-insert')
|
|
158
|
+
? `tile:${element.getAttribute('data-product-grid-insert')}`
|
|
159
|
+
: element.getAttribute('href') || element.tagName.toLowerCase()
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const sheet = (container: HTMLElement): string =>
|
|
163
|
+
container.querySelector('style')?.textContent ?? '';
|
|
164
|
+
|
|
165
|
+
/** Every cell-pinning rule the grid wrote, reduced to its bare selector. */
|
|
166
|
+
const pinSelectors = (css: string): string[] =>
|
|
167
|
+
Array.from(css.matchAll(/([^{}@]+)\{grid-column:[^{}]*\}/g)).map((match) =>
|
|
168
|
+
match[1].trim()
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
describe('nothing authored renders exactly what it rendered before', () => {
|
|
172
|
+
it('draws the grid byte for byte', () => {
|
|
173
|
+
expect(draw(gridBlock(), THREE_PRODUCTS).innerHTML).toBe(DEFAULT_HTML);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('still renders nothing on a storefront listing that came back empty', () => {
|
|
177
|
+
expect(draw(gridBlock(), listingContext([])).innerHTML).toBe('');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('still renders nothing on a page with no listing at all', () => {
|
|
181
|
+
expect(draw(gridBlock(), null).innerHTML).toBe('');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('still shows only the designer notice when there is no data', () => {
|
|
185
|
+
expect(draw(gridBlock(), null, true).innerHTML).toBe(
|
|
186
|
+
DESIGNER_PLACEHOLDER_HTML
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('emits no tile or empty-state markup', () => {
|
|
191
|
+
const container = draw(gridBlock(), THREE_PRODUCTS);
|
|
192
|
+
|
|
193
|
+
expect(container.querySelector('[data-product-grid-insert]')).toBeNull();
|
|
194
|
+
expect(container.querySelector('[data-product-grid-empty]')).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('leaves a child carrying no role unrendered, as it always was', () => {
|
|
198
|
+
const container = draw(
|
|
199
|
+
gridBlock({}, [child('')]),
|
|
200
|
+
THREE_PRODUCTS
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(container.innerHTML).toBe(DEFAULT_HTML);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('a merchandiser tile in the grid', () => {
|
|
208
|
+
const eightProducts = listingContext(
|
|
209
|
+
Array.from({ length: 8 }, (_, index) => product(index + 1))
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
it('takes the authored cell and pushes the products along', () => {
|
|
213
|
+
const container = draw(
|
|
214
|
+
gridBlock({}, [child('grid-insertion', { insertPosition: 5 })]),
|
|
215
|
+
eightProducts
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(cellSequence(container)).toEqual([
|
|
219
|
+
'/p1',
|
|
220
|
+
'/p2',
|
|
221
|
+
'/p3',
|
|
222
|
+
'/p4',
|
|
223
|
+
'tile:0',
|
|
224
|
+
'/p5',
|
|
225
|
+
'/p6',
|
|
226
|
+
'/p7',
|
|
227
|
+
'/p8'
|
|
228
|
+
]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('pins the cell for each column count the block emits', () => {
|
|
232
|
+
const css = sheet(
|
|
233
|
+
draw(
|
|
234
|
+
gridBlock({}, [child('grid-insertion', { insertPosition: 5 })]),
|
|
235
|
+
eightProducts
|
|
236
|
+
)
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Cell 5 is the start of row 2 across four columns…
|
|
240
|
+
expect(css).toContain(
|
|
241
|
+
'[data-product-grid-id="grid-1"] ' +
|
|
242
|
+
'[data-product-grid-page="1"][data-product-grid-insert="0"]' +
|
|
243
|
+
'{grid-column:1/span 1;grid-row:2/span 1;}'
|
|
244
|
+
);
|
|
245
|
+
// …and of row 3 across the two the same block drops to on a phone.
|
|
246
|
+
expect(css).toContain(
|
|
247
|
+
'[data-product-grid-id="grid-1"] ' +
|
|
248
|
+
'[data-product-grid-page="1"][data-product-grid-insert="0"]' +
|
|
249
|
+
'{grid-column:1/span 1;grid-row:3/span 1;}'
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('keeps the mobile rule inside the mobile query', () => {
|
|
254
|
+
const css = sheet(
|
|
255
|
+
draw(
|
|
256
|
+
gridBlock({}, [child('grid-insertion', { insertPosition: 5 })]),
|
|
257
|
+
eightProducts
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
const query = css.slice(css.indexOf('@media'));
|
|
261
|
+
|
|
262
|
+
expect(query).toContain('grid-row:3/span 1;');
|
|
263
|
+
expect(query.endsWith('}')).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('spans two columns and two rows when asked', () => {
|
|
267
|
+
const css = sheet(
|
|
268
|
+
draw(
|
|
269
|
+
gridBlock({}, [
|
|
270
|
+
child('grid-insertion', {
|
|
271
|
+
insertPosition: 5,
|
|
272
|
+
insertColumnSpan: 2,
|
|
273
|
+
insertRowSpan: 2
|
|
274
|
+
})
|
|
275
|
+
]),
|
|
276
|
+
eightProducts
|
|
277
|
+
)
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
expect(css).toContain('{grid-column:1/span 2;grid-row:2/span 2;}');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('narrows a wide tile to the column count it is actually in', () => {
|
|
284
|
+
// Two columns wide is the whole row on a one-column phone; asking for two
|
|
285
|
+
// tracks there would push the tile off the edge of the grid.
|
|
286
|
+
const css = sheet(
|
|
287
|
+
draw(
|
|
288
|
+
gridBlock({ columnsMobile: 1 }, [
|
|
289
|
+
child('grid-insertion', {
|
|
290
|
+
insertPosition: 5,
|
|
291
|
+
insertColumnSpan: 2
|
|
292
|
+
})
|
|
293
|
+
]),
|
|
294
|
+
eightProducts
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
const query = css.slice(css.indexOf('@media'));
|
|
298
|
+
|
|
299
|
+
expect(css.slice(0, css.indexOf('@media'))).toContain(
|
|
300
|
+
'{grid-column:1/span 2;grid-row:2/span 1;}'
|
|
301
|
+
);
|
|
302
|
+
expect(query).toContain('{grid-column:1/span 1;grid-row:5/span 1;}');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('pulls a tile back inside the last track rather than overhanging it', () => {
|
|
306
|
+
const css = sheet(
|
|
307
|
+
draw(
|
|
308
|
+
gridBlock({}, [
|
|
309
|
+
child('grid-insertion', {
|
|
310
|
+
insertPosition: 4,
|
|
311
|
+
insertColumnSpan: 2
|
|
312
|
+
})
|
|
313
|
+
]),
|
|
314
|
+
eightProducts
|
|
315
|
+
)
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// Cell 4 is the last column of four; a two-column tile starts one earlier.
|
|
319
|
+
expect(css.slice(0, css.indexOf('@media'))).toContain(
|
|
320
|
+
'{grid-column:3/span 2;grid-row:1/span 1;}'
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('lands at the end when the filters returned fewer products than that', () => {
|
|
325
|
+
// Three results and a tile authored for cell 5: pinning it to cell 5 would
|
|
326
|
+
// leave cell 4 unreachable and tear a hole through the row.
|
|
327
|
+
const container = draw(
|
|
328
|
+
gridBlock({}, [child('grid-insertion', { insertPosition: 5 })]),
|
|
329
|
+
THREE_PRODUCTS
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
expect(cellSequence(container)).toEqual(['/a', '/b', '/c', 'tile:0']);
|
|
333
|
+
expect(sheet(container).slice(0, sheet(container).indexOf('@media'))).toContain(
|
|
334
|
+
'{grid-column:4/span 1;grid-row:1/span 1;}'
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('is the only cell left when the listing came back empty', () => {
|
|
339
|
+
const container = draw(
|
|
340
|
+
gridBlock({}, [child('grid-insertion', { insertPosition: 5 })]),
|
|
341
|
+
listingContext([])
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
expect(cellSequence(container)).toEqual(['tile:0']);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('stays off a page it was not authored for', () => {
|
|
348
|
+
const secondPage = listingContext(
|
|
349
|
+
Array.from({ length: 8 }, (_, index) => product(index + 1)),
|
|
350
|
+
{ current_page: 2, count: 99 }
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
expect(
|
|
354
|
+
draw(
|
|
355
|
+
gridBlock({}, [child('grid-insertion', { insertPosition: 5 })]),
|
|
356
|
+
secondPage
|
|
357
|
+
).querySelector('[data-product-grid-insert]')
|
|
358
|
+
).toBeNull();
|
|
359
|
+
|
|
360
|
+
expect(
|
|
361
|
+
draw(
|
|
362
|
+
gridBlock({}, [
|
|
363
|
+
child('grid-insertion', { insertPosition: 5, insertPages: 'all' })
|
|
364
|
+
]),
|
|
365
|
+
secondPage
|
|
366
|
+
).querySelector('[data-product-grid-insert]')
|
|
367
|
+
).not.toBeNull();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('pins only the page it was rendered for', () => {
|
|
371
|
+
// "Cell 5" means the fifth cell of the page being read, so the rule that
|
|
372
|
+
// expresses it is only true of that page. Load More can put several pages
|
|
373
|
+
// in one grid; a rule that reaches past its own page pins two tiles into
|
|
374
|
+
// one cell and buries whatever the shopper was already looking at.
|
|
375
|
+
const everyPage = child('grid-insertion', {
|
|
376
|
+
insertPosition: 5,
|
|
377
|
+
insertPages: 'all'
|
|
378
|
+
});
|
|
379
|
+
const products = Array.from({ length: 8 }, (_, index) => product(index + 1));
|
|
380
|
+
|
|
381
|
+
const pageOne = draw(gridBlock({}, [everyPage]), listingContext(products));
|
|
382
|
+
const pageTwo = draw(
|
|
383
|
+
gridBlock({}, [everyPage]),
|
|
384
|
+
listingContext(products, { current_page: 2, count: 99 })
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const selectors = pinSelectors(sheet(pageOne));
|
|
388
|
+
expect(selectors.length).toBeGreaterThan(0);
|
|
389
|
+
selectors.forEach((selector) => {
|
|
390
|
+
expect(pageOne.querySelectorAll(selector)).toHaveLength(1);
|
|
391
|
+
expect(pageTwo.querySelectorAll(selector)).toHaveLength(0);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('is gone from the storefront when the designer hid it', () => {
|
|
396
|
+
const hidden = child('grid-insertion', { insertPosition: 2 }, {
|
|
397
|
+
hidden: true
|
|
398
|
+
} as Partial<Block>);
|
|
399
|
+
|
|
400
|
+
expect(
|
|
401
|
+
draw(gridBlock({}, [hidden]), THREE_PRODUCTS).querySelector(
|
|
402
|
+
'[data-product-grid-insert]'
|
|
403
|
+
)
|
|
404
|
+
).toBeNull();
|
|
405
|
+
// Still on the canvas, so it can be brought back — the rule the group
|
|
406
|
+
// block already applies to its own children.
|
|
407
|
+
expect(
|
|
408
|
+
draw(gridBlock({}, [hidden]), THREE_PRODUCTS, true).querySelector(
|
|
409
|
+
'[data-product-grid-insert]'
|
|
410
|
+
)
|
|
411
|
+
).not.toBeNull();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('declares the part it emits so the tile can be selected and styled', () => {
|
|
415
|
+
const container = draw(
|
|
416
|
+
gridBlock({}, [child('grid-insertion', { insertPosition: 2 })]),
|
|
417
|
+
THREE_PRODUCTS
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
expect(
|
|
421
|
+
container
|
|
422
|
+
.querySelector('[data-product-grid-insert]')
|
|
423
|
+
?.getAttribute('data-part')
|
|
424
|
+
).toBe('insert');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('writes nothing to storage while the designer arranges it', () => {
|
|
428
|
+
const setItem = jest.spyOn(window.sessionStorage.__proto__, 'setItem');
|
|
429
|
+
|
|
430
|
+
draw(
|
|
431
|
+
gridBlock({}, [child('grid-insertion', { insertPosition: 2 })]),
|
|
432
|
+
THREE_PRODUCTS,
|
|
433
|
+
true
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
expect(setItem).not.toHaveBeenCalled();
|
|
437
|
+
setItem.mockRestore();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe('the no-results page', () => {
|
|
442
|
+
const emptyState = () =>
|
|
443
|
+
child('listing-empty', {}, { id: 'empty-1' } as Partial<Block>);
|
|
444
|
+
|
|
445
|
+
it('replaces the grid when the filters return nothing', () => {
|
|
446
|
+
const container = draw(
|
|
447
|
+
gridBlock({}, [emptyState()]),
|
|
448
|
+
listingContext([])
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
expect(container.querySelector('[data-product-grid]')).toBeNull();
|
|
452
|
+
expect(container.querySelector('[data-product-grid-empty]')).not.toBeNull();
|
|
453
|
+
expect(container.textContent).toContain('Winter campaign');
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('declares the part it emits', () => {
|
|
457
|
+
const container = draw(gridBlock({}, [emptyState()]), listingContext([]));
|
|
458
|
+
|
|
459
|
+
expect(
|
|
460
|
+
container
|
|
461
|
+
.querySelector('[data-product-grid-empty]')
|
|
462
|
+
?.getAttribute('data-part')
|
|
463
|
+
).toBe('empty');
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('stays out of the way while the listing has results', () => {
|
|
467
|
+
const container = draw(gridBlock({}, [emptyState()]), THREE_PRODUCTS);
|
|
468
|
+
|
|
469
|
+
expect(container.querySelector('[data-product-grid-empty]')).toBeNull();
|
|
470
|
+
expect(container.querySelector('[data-product-grid]')).not.toBeNull();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('never speaks for a page that has no listing at all', () => {
|
|
474
|
+
// A grid dropped on a landing page has not "returned no results"; saying
|
|
475
|
+
// so would announce an empty search on a page that never ran one.
|
|
476
|
+
expect(draw(gridBlock({}, [emptyState()]), null).innerHTML).toBe('');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('can be previewed on the canvas without touching the storefront', () => {
|
|
480
|
+
const block = gridBlock({ previewEmptyState: true }, [emptyState()]);
|
|
481
|
+
|
|
482
|
+
expect(
|
|
483
|
+
draw(block, THREE_PRODUCTS, true).querySelector('[data-product-grid-empty]')
|
|
484
|
+
).not.toBeNull();
|
|
485
|
+
// The same composition on the live site, where the listing has products.
|
|
486
|
+
expect(draw(block, THREE_PRODUCTS, false).innerHTML).toBe(DEFAULT_HTML);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('is gone from the storefront when the designer hid it', () => {
|
|
490
|
+
const hidden = child('listing-empty', {}, {
|
|
491
|
+
id: 'empty-1',
|
|
492
|
+
hidden: true
|
|
493
|
+
} as Partial<Block>);
|
|
494
|
+
|
|
495
|
+
expect(draw(gridBlock({}, [hidden]), listingContext([])).innerHTML).toBe('');
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
describe('placing tiles', () => {
|
|
500
|
+
const tile = (position: number) => ({
|
|
501
|
+
block: { id: `t${position}` } as Block,
|
|
502
|
+
position,
|
|
503
|
+
columnSpan: 1,
|
|
504
|
+
rowSpan: 1,
|
|
505
|
+
everyPage: false
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('honours an authored cell that the results can reach', () => {
|
|
509
|
+
expect(placeInsertions([tile(5)], 20).map((t) => t.cell)).toEqual([5]);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('pulls a tile back to the first cell past the products', () => {
|
|
513
|
+
expect(placeInsertions([tile(9)], 3).map((t) => t.cell)).toEqual([4]);
|
|
514
|
+
expect(placeInsertions([tile(9)], 0).map((t) => t.cell)).toEqual([1]);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('keeps two tiles authored onto one cell from stacking', () => {
|
|
518
|
+
expect(placeInsertions([tile(5), tile(5)], 20).map((t) => t.cell)).toEqual([
|
|
519
|
+
5, 6
|
|
520
|
+
]);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('sorts by cell whatever order the children are in', () => {
|
|
524
|
+
expect(placeInsertions([tile(9), tile(2)], 20).map((t) => t.cell)).toEqual([
|
|
525
|
+
2, 9
|
|
526
|
+
]);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('gives every tile a cell even when none of them fit', () => {
|
|
530
|
+
expect(
|
|
531
|
+
placeInsertions([tile(40), tile(40), tile(40)], 0).map((t) => t.cell)
|
|
532
|
+
).toEqual([1, 2, 3]);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
describe('the interleave and the rules it agrees with', () => {
|
|
537
|
+
const placed = (...cells: number[]) =>
|
|
538
|
+
cells.map((cell) => ({
|
|
539
|
+
block: {} as Block,
|
|
540
|
+
position: cell,
|
|
541
|
+
columnSpan: 1,
|
|
542
|
+
rowSpan: 1,
|
|
543
|
+
everyPage: false,
|
|
544
|
+
cell
|
|
545
|
+
}));
|
|
546
|
+
|
|
547
|
+
it('returns the cards untouched when there is nothing to insert', () => {
|
|
548
|
+
const cards = ['a', 'b', 'c'];
|
|
549
|
+
|
|
550
|
+
expect(interleaveCells(cards, [], [])).toEqual(cards);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('puts each tile in the cell the stylesheet pins it to', () => {
|
|
554
|
+
expect(
|
|
555
|
+
interleaveCells(['a', 'b', 'c', 'd'], ['T1', 'T2'], placed(1, 4))
|
|
556
|
+
).toEqual(['T1', 'a', 'b', 'T2', 'c', 'd']);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('emits no rules for a grid with no tiles', () => {
|
|
560
|
+
expect(buildInsertionRules('grid-1', [], 4, 1)).toBe('');
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('writes a rule that only its own page can satisfy', () => {
|
|
564
|
+
expect(buildInsertionRules('grid-1', placed(3), 4, 2)).toBe(
|
|
565
|
+
'[data-product-grid-id="grid-1"] ' +
|
|
566
|
+
'[data-product-grid-page="2"][data-product-grid-insert="0"]' +
|
|
567
|
+
'{grid-column:3/span 1;grid-row:1/span 1;}'
|
|
568
|
+
);
|
|
569
|
+
});
|
|
570
|
+
});
|