@akinon/pz-theme 2.0.100-rc.0 → 2.0.101-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.
@@ -257,3 +257,176 @@ describe('ImageBlock leaves its box to the sheet', () => {
257
257
  expect(screen.getByText('No image uploaded').style.minHeight).toBe('');
258
258
  });
259
259
  });
260
+
261
+ /**
262
+ * Per-breakpoint art direction.
263
+ *
264
+ * Two invariants carry this feature and both are easy to lose in a refactor:
265
+ *
266
+ * 1. A block with no override must emit BYTE-IDENTICAL markup to the old
267
+ * renderer — no `<picture>`, no extra element. 198 image blocks exist in
268
+ * this repo's fixtures alone and none of them has an override.
269
+ * 2. `<picture>` resolves by FIRST MATCH, not by cascade. The phone band is
270
+ * therefore exclusive when a tablet image exists and widened to 1023 when it
271
+ * does not, which is what reproduces the stylesheet's "mobile covers tablet
272
+ * unless tablet overrides" rule the merchant already knows.
273
+ */
274
+ describe('ImageBlock — per-breakpoint sources', () => {
275
+ const withSources = (
276
+ overrides: Record<string, unknown>,
277
+ extra: Record<string, unknown> = {}
278
+ ) => ({
279
+ id: 'b-art',
280
+ value: 'https://example.com/desktop.jpg',
281
+ properties: { alt: 'Kampanya', ...overrides },
282
+ ...extra
283
+ });
284
+
285
+ const sources = (container: HTMLElement) =>
286
+ Array.from(container.querySelectorAll('source')).map((el) => ({
287
+ media: el.getAttribute('media'),
288
+ srcSet: el.getAttribute('srcset')
289
+ }));
290
+
291
+ it('emits no <picture> when only the desktop image is authored', () => {
292
+ const { container } = renderBlock(withSources({}));
293
+
294
+ expect(container.querySelector('picture')).toBeNull();
295
+ expect(getImg(container)?.getAttribute('src')).toBe(
296
+ 'https://example.com/desktop.jpg'
297
+ );
298
+ });
299
+
300
+ it('widens the phone band to the tablet edge when no tablet image is authored', () => {
301
+ const { container } = renderBlock(
302
+ withSources({ imageMobile: 'https://example.com/phone.jpg' })
303
+ );
304
+
305
+ expect(container.querySelector('picture')).not.toBeNull();
306
+ expect(sources(container)).toEqual([
307
+ { media: '(max-width: 1023px)', srcSet: 'https://example.com/phone.jpg' }
308
+ ]);
309
+ });
310
+
311
+ it('narrows the phone band and orders phone before tablet when both exist', () => {
312
+ const { container } = renderBlock(
313
+ withSources({
314
+ imageMobile: 'https://example.com/phone.jpg',
315
+ imageTablet: 'https://example.com/tablet.jpg'
316
+ })
317
+ );
318
+
319
+ expect(sources(container)).toEqual([
320
+ { media: '(max-width: 767px)', srcSet: 'https://example.com/phone.jpg' },
321
+ {
322
+ media: '(min-width: 768px) and (max-width: 1023px)',
323
+ srcSet: 'https://example.com/tablet.jpg'
324
+ }
325
+ ]);
326
+ });
327
+
328
+ it('emits only the tablet band when the phone image is left empty', () => {
329
+ const { container } = renderBlock(
330
+ withSources({ imageTablet: 'https://example.com/tablet.jpg' })
331
+ );
332
+
333
+ expect(sources(container)).toEqual([
334
+ {
335
+ media: '(min-width: 768px) and (max-width: 1023px)',
336
+ srcSet: 'https://example.com/tablet.jpg'
337
+ }
338
+ ]);
339
+ });
340
+
341
+ it('drops an override that is the same file as the desktop image', () => {
342
+ const { container } = renderBlock(
343
+ withSources({ imageMobile: 'https://example.com/desktop.jpg' })
344
+ );
345
+
346
+ expect(container.querySelector('picture')).toBeNull();
347
+ });
348
+
349
+ it('keeps the <img> as the desktop candidate and the only carrier of alt', () => {
350
+ const { container } = renderBlock(
351
+ withSources({ imageMobile: 'https://example.com/phone.jpg' })
352
+ );
353
+
354
+ const img = getImg(container);
355
+ expect(img?.getAttribute('src')).toBe('https://example.com/desktop.jpg');
356
+ expect(img?.getAttribute('alt')).toBe('Kampanya');
357
+ // alt is illegal on <source>; the browser reads it from the <img> whichever
358
+ // candidate it picks.
359
+ expect(container.querySelector('source')?.hasAttribute('alt')).toBe(false);
360
+ });
361
+
362
+ /**
363
+ * Without `display: contents` the <picture> becomes the flex item the parent
364
+ * lays out and the percentage parent the <img> resolves against, and an
365
+ * authored `height: 100%` collapses against an auto-height box.
366
+ */
367
+ it('keeps the <picture> out of layout', () => {
368
+ const { container } = renderBlock(
369
+ withSources({ imageMobile: 'https://example.com/phone.jpg' })
370
+ );
371
+
372
+ expect(
373
+ (container.querySelector('picture') as HTMLElement).style.display
374
+ ).toBe('contents');
375
+ });
376
+
377
+ describe('the canvas resolves in JS instead, because <source media> answers the real viewport', () => {
378
+ const designer = (breakpoint: string, overrides: Record<string, unknown>) =>
379
+ render(
380
+ <ImageBlock
381
+ {...({
382
+ block: withSources(overrides),
383
+ isDesigner: true,
384
+ currentBreakpoint: breakpoint
385
+ } as unknown as BlockRendererProps)}
386
+ />
387
+ );
388
+
389
+ it('shows the phone image at the phone preview', () => {
390
+ const { container } = designer('mobile', {
391
+ imageMobile: 'https://example.com/phone.jpg'
392
+ });
393
+
394
+ expect(container.querySelector('picture')).toBeNull();
395
+ expect(getImg(container)?.getAttribute('src')).toBe(
396
+ 'https://example.com/phone.jpg'
397
+ );
398
+ });
399
+
400
+ it('falls through to the phone image at the tablet preview when no tablet image exists', () => {
401
+ const { container } = designer('tablet', {
402
+ imageMobile: 'https://example.com/phone.jpg'
403
+ });
404
+
405
+ expect(getImg(container)?.getAttribute('src')).toBe(
406
+ 'https://example.com/phone.jpg'
407
+ );
408
+ });
409
+
410
+ it('shows the desktop image at the desktop preview', () => {
411
+ const { container } = designer('desktop', {
412
+ imageMobile: 'https://example.com/phone.jpg',
413
+ imageTablet: 'https://example.com/tablet.jpg'
414
+ });
415
+
416
+ expect(getImg(container)?.getAttribute('src')).toBe(
417
+ 'https://example.com/desktop.jpg'
418
+ );
419
+ });
420
+ });
421
+
422
+ it('leaves the empty-state placeholder alone', () => {
423
+ const { container } = renderBlock({
424
+ id: 'b-empty',
425
+ value: '',
426
+ properties: { imageMobile: 'https://example.com/phone.jpg' }
427
+ });
428
+
429
+ expect(container.querySelector('picture')).toBeNull();
430
+ expect(screen.getByText('No image uploaded')).toBeInTheDocument();
431
+ });
432
+ });
@@ -91,6 +91,7 @@ import StoreLocatorBlock from './store-locator-block';
91
91
  import FaqAccordionBlock from './faq-accordion-block';
92
92
  import ShareBlock from './share-block';
93
93
  import ContainerBlock from './container-block';
94
+ import CollapsibleBlock from './collapsible-block';
94
95
  import CarouselItemBlock from './carousel-item-block';
95
96
  import HoverShowcaseItemBlock from './hover-showcase-item-block';
96
97
  import IconGroupBlock from './icon-group-block';
@@ -143,6 +144,7 @@ blockRendererRegistry.register('button', ButtonBlock);
143
144
  blockRendererRegistry.register('icon', IconBlock);
144
145
  blockRendererRegistry.register('video', VideoBlock);
145
146
  blockRendererRegistry.register('accordion', AccordionBlock);
147
+ blockRendererRegistry.register('collapsible', CollapsibleBlock);
146
148
  blockRendererRegistry.register('tab', TabBlock);
147
149
  blockRendererRegistry.register('counter', CounterBlock);
148
150
  blockRendererRegistry.register('image-gallery', ImageGalleryBlock);