@akinon/pz-theme 2.0.132-beta.0 → 2.0.132
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
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.132
|
|
3
|
+
## 2.0.132
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- @akinon/next@2.0.132
|
|
9
|
-
- @akinon/theme-contract@2.0.132
|
|
7
|
+
- 6a296eb: ZERO-5161: resolve video clip and posters per storefront language
|
|
8
|
+
- @akinon/next@2.0.132
|
|
9
|
+
- @akinon/theme-contract@2.0.132
|
|
10
10
|
|
|
11
11
|
## 2.0.131
|
|
12
12
|
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.132
|
|
4
|
+
"version": "2.0.132",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.132
|
|
11
|
+
"@akinon/next": "2.0.132",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.132
|
|
16
|
+
"@akinon/theme-contract": "2.0.132",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"react-multi-carousel": "2.8.4",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"zod": "4.4.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@akinon/next": "2.0.132
|
|
24
|
+
"@akinon/next": "2.0.132",
|
|
25
25
|
"@testing-library/jest-dom": "5.16.4",
|
|
26
26
|
"@testing-library/react": "16.3.2",
|
|
27
27
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -375,7 +375,10 @@ describe('PLACEHOLDERS_AVAILABLE announce', () => {
|
|
|
375
375
|
// The literal string rather than the constant: the editor matches it by
|
|
376
376
|
// name and drops a name it does not know, so this pins the wire value a
|
|
377
377
|
// rename would silently break.
|
|
378
|
-
expect(frames()[0].actionCapabilities).toEqual([
|
|
378
|
+
expect(frames()[0].actionCapabilities).toEqual([
|
|
379
|
+
'add-to-cart-after-add',
|
|
380
|
+
'localized-video'
|
|
381
|
+
]);
|
|
379
382
|
});
|
|
380
383
|
|
|
381
384
|
it('keeps the action capability on the designer re-announce too', () => {
|
|
@@ -401,8 +404,8 @@ describe('PLACEHOLDERS_AVAILABLE announce', () => {
|
|
|
401
404
|
// panel back to "On Click" halfway through a session.
|
|
402
405
|
expect(postMessage).toHaveBeenCalledTimes(2);
|
|
403
406
|
expect(frames().map((frame) => frame.actionCapabilities)).toEqual([
|
|
404
|
-
['add-to-cart-after-add'],
|
|
405
|
-
['add-to-cart-after-add']
|
|
407
|
+
['add-to-cart-after-add', 'localized-video'],
|
|
408
|
+
['add-to-cart-after-add', 'localized-video']
|
|
406
409
|
]);
|
|
407
410
|
});
|
|
408
411
|
|
|
@@ -31,8 +31,9 @@
|
|
|
31
31
|
* the placeholder registry announces this, and it must not have to pull a
|
|
32
32
|
* client component in to say one list of strings.
|
|
33
33
|
*/
|
|
34
|
-
export type ActionCapability = 'add-to-cart-after-add';
|
|
34
|
+
export type ActionCapability = 'add-to-cart-after-add' | 'localized-video';
|
|
35
35
|
|
|
36
36
|
export const ACTION_CAPABILITIES: readonly ActionCapability[] = [
|
|
37
|
-
'add-to-cart-after-add'
|
|
37
|
+
'add-to-cart-after-add',
|
|
38
|
+
'localized-video'
|
|
38
39
|
];
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
let localizationValue: Record<string, unknown> = {};
|
|
5
|
+
|
|
6
|
+
jest.mock('@akinon/next/hooks', () => ({
|
|
7
|
+
__esModule: true,
|
|
8
|
+
useLocalization: () => localizationValue
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import VideoBlock from '../video-block';
|
|
12
|
+
import type { BlockRendererProps } from '../block-renderer-registry';
|
|
13
|
+
|
|
14
|
+
const setLocalization = (
|
|
15
|
+
locale: string,
|
|
16
|
+
locales: string[] = ['tr', 'en'],
|
|
17
|
+
defaultLocaleValue = 'tr'
|
|
18
|
+
) => {
|
|
19
|
+
localizationValue = {
|
|
20
|
+
locale,
|
|
21
|
+
locales: locales.map((value) => ({ value, label: value.toUpperCase() })),
|
|
22
|
+
defaultLocaleValue
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const TR = 'https://cdn.example.com/hero-tr.mp4';
|
|
27
|
+
const EN = 'https://cdn.example.com/hero-en.mp4';
|
|
28
|
+
const TR_MOBILE = 'https://cdn.example.com/hero-tr-mobile.mp4';
|
|
29
|
+
const EN_MOBILE = 'https://cdn.example.com/hero-en-mobile.mp4';
|
|
30
|
+
const TR_POSTER = 'https://cdn.example.com/poster-tr.jpg';
|
|
31
|
+
const EN_POSTER = 'https://cdn.example.com/poster-en.jpg';
|
|
32
|
+
|
|
33
|
+
const mount = (
|
|
34
|
+
value: unknown,
|
|
35
|
+
properties: Record<string, unknown> = {},
|
|
36
|
+
currentBreakpoint = 'desktop'
|
|
37
|
+
) =>
|
|
38
|
+
render(
|
|
39
|
+
<VideoBlock
|
|
40
|
+
{...({
|
|
41
|
+
block: { id: 'v', type: 'video', value, properties },
|
|
42
|
+
placeholderId: 'p1',
|
|
43
|
+
sectionId: 's1',
|
|
44
|
+
isDesigner: false,
|
|
45
|
+
currentBreakpoint
|
|
46
|
+
} as unknown as BlockRendererProps)}
|
|
47
|
+
/>
|
|
48
|
+
).container;
|
|
49
|
+
|
|
50
|
+
const srcOf = (
|
|
51
|
+
value: unknown,
|
|
52
|
+
properties: Record<string, unknown> = {},
|
|
53
|
+
currentBreakpoint = 'desktop'
|
|
54
|
+
) =>
|
|
55
|
+
mount(value, properties, currentBreakpoint)
|
|
56
|
+
.querySelector('video')
|
|
57
|
+
?.getAttribute('src') ?? null;
|
|
58
|
+
|
|
59
|
+
const posterOf = (
|
|
60
|
+
properties: Record<string, unknown>,
|
|
61
|
+
currentBreakpoint = 'desktop'
|
|
62
|
+
) =>
|
|
63
|
+
mount(TR, properties, currentBreakpoint)
|
|
64
|
+
.querySelector('video')
|
|
65
|
+
?.getAttribute('poster') ?? null;
|
|
66
|
+
|
|
67
|
+
const withViewport = (narrow: boolean, run: () => void) => {
|
|
68
|
+
const original = window.matchMedia;
|
|
69
|
+
window.matchMedia = ((query: string) => ({
|
|
70
|
+
matches: narrow && query.includes('max-width'),
|
|
71
|
+
media: query,
|
|
72
|
+
onchange: null,
|
|
73
|
+
addListener: () => {},
|
|
74
|
+
removeListener: () => {},
|
|
75
|
+
addEventListener: () => {},
|
|
76
|
+
removeEventListener: () => {},
|
|
77
|
+
dispatchEvent: () => false
|
|
78
|
+
})) as unknown as typeof window.matchMedia;
|
|
79
|
+
try {
|
|
80
|
+
run();
|
|
81
|
+
} finally {
|
|
82
|
+
window.matchMedia = original;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
beforeEach(() => setLocalization('tr'));
|
|
87
|
+
|
|
88
|
+
describe('the clip, per language', () => {
|
|
89
|
+
it('keeps playing a plain address exactly as before', () => {
|
|
90
|
+
expect(srcOf(TR)).toBe(TR);
|
|
91
|
+
|
|
92
|
+
setLocalization('en');
|
|
93
|
+
expect(srcOf(TR)).toBe(TR);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('picks the entry for the page’s language from a per-language map', () => {
|
|
97
|
+
expect(srcOf({ tr: TR, en: EN })).toBe(TR);
|
|
98
|
+
|
|
99
|
+
setLocalization('en');
|
|
100
|
+
expect(srcOf({ tr: TR, en: EN })).toBe(EN);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('falls back to the default language, then to the first clip there is', () => {
|
|
104
|
+
setLocalization('en');
|
|
105
|
+
expect(srcOf({ tr: TR, en: '' })).toBe(TR);
|
|
106
|
+
|
|
107
|
+
setLocalization('en', ['tr', 'en'], 'de');
|
|
108
|
+
expect(srcOf({ tr: TR, en: '' })).toBe(TR);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('reads the map the storefront receives serialised in a widget attribute', () => {
|
|
112
|
+
setLocalization('en');
|
|
113
|
+
expect(srcOf(JSON.stringify({ tr: TR, en: EN }))).toBe(EN);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('shows nothing rather than breaking when the map names languages the store lacks', () => {
|
|
117
|
+
setLocalization('de', ['de']);
|
|
118
|
+
const container = mount({ tr: TR, en: EN });
|
|
119
|
+
|
|
120
|
+
expect(container.querySelector('video')).toBeNull();
|
|
121
|
+
expect(container.querySelector('iframe')).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('resolves an embed per language too', () => {
|
|
125
|
+
const value = {
|
|
126
|
+
tr: 'https://www.youtube.com/watch?v=trclip01',
|
|
127
|
+
en: 'https://www.youtube.com/watch?v=enclip01'
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
expect(mount(value).querySelector('iframe')?.getAttribute('src')).toContain(
|
|
131
|
+
'trclip01'
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
setLocalization('en');
|
|
135
|
+
expect(mount(value).querySelector('iframe')?.getAttribute('src')).toContain(
|
|
136
|
+
'enclip01'
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('the mobile clip, per language', () => {
|
|
142
|
+
const mobile = (mobileUrl: unknown) => ({
|
|
143
|
+
useDifferentMobileVideo: true,
|
|
144
|
+
mobileUrl
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('keeps honouring a plain address and the per-breakpoint shape older JSON holds', () => {
|
|
148
|
+
withViewport(true, () => {
|
|
149
|
+
expect(srcOf(TR, mobile(TR_MOBILE), 'mobile')).toBe(TR_MOBILE);
|
|
150
|
+
expect(srcOf(TR, mobile({ desktop: TR_MOBILE }), 'mobile')).toBe(
|
|
151
|
+
TR_MOBILE
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('picks the mobile clip for the page’s language', () => {
|
|
157
|
+
withViewport(true, () => {
|
|
158
|
+
expect(
|
|
159
|
+
srcOf(TR, mobile({ tr: TR_MOBILE, en: EN_MOBILE }), 'mobile')
|
|
160
|
+
).toBe(TR_MOBILE);
|
|
161
|
+
|
|
162
|
+
setLocalization('en');
|
|
163
|
+
expect(
|
|
164
|
+
srcOf(EN, mobile({ tr: TR_MOBILE, en: EN_MOBILE }), 'mobile')
|
|
165
|
+
).toBe(EN_MOBILE);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('leaves the desktop clip alone on a wide screen', () => {
|
|
170
|
+
setLocalization('en');
|
|
171
|
+
withViewport(false, () => {
|
|
172
|
+
expect(
|
|
173
|
+
srcOf({ tr: TR, en: EN }, mobile({ tr: TR_MOBILE, en: EN_MOBILE }))
|
|
174
|
+
).toBe(EN);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('the poster, per language', () => {
|
|
180
|
+
it('keeps a plain poster as it is', () => {
|
|
181
|
+
expect(posterOf({ poster: TR_POSTER })).toBe(TR_POSTER);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('picks the poster for the page’s language, on desktop and on the phone', () => {
|
|
185
|
+
expect(posterOf({ poster: { tr: TR_POSTER, en: EN_POSTER } })).toBe(
|
|
186
|
+
TR_POSTER
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
setLocalization('en');
|
|
190
|
+
expect(posterOf({ poster: { tr: TR_POSTER, en: EN_POSTER } })).toBe(
|
|
191
|
+
EN_POSTER
|
|
192
|
+
);
|
|
193
|
+
expect(
|
|
194
|
+
posterOf(
|
|
195
|
+
{
|
|
196
|
+
useDifferentMobileVideo: true,
|
|
197
|
+
mobileUrl: EN_MOBILE,
|
|
198
|
+
mobilePoster: { tr: TR_POSTER, en: EN_POSTER }
|
|
199
|
+
},
|
|
200
|
+
'mobile'
|
|
201
|
+
)
|
|
202
|
+
).toBe(EN_POSTER);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
partAttrs
|
|
10
10
|
} from '../utils';
|
|
11
11
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
12
|
+
import {
|
|
13
|
+
resolveLocalizedValue,
|
|
14
|
+
useValueLocale,
|
|
15
|
+
type ValueLocale
|
|
16
|
+
} from '../utils/localized-value';
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* What an author's URL turned out to be.
|
|
@@ -311,6 +316,17 @@ const setAtAnyBreakpoint = (value: unknown): boolean => {
|
|
|
311
316
|
};
|
|
312
317
|
|
|
313
318
|
/** The first address a per-breakpoint value carries, asked the same way. */
|
|
319
|
+
const localizedMedia = (
|
|
320
|
+
value: unknown,
|
|
321
|
+
currentBreakpoint: string,
|
|
322
|
+
valueLocale: ValueLocale
|
|
323
|
+
): string => {
|
|
324
|
+
const localized = resolveLocalizedValue(value, valueLocale);
|
|
325
|
+
const atBreakpoint = getResponsiveValue(localized, currentBreakpoint, '');
|
|
326
|
+
const resolved = resolveLocalizedValue(atBreakpoint, valueLocale);
|
|
327
|
+
return typeof resolved === 'string' ? resolved : '';
|
|
328
|
+
};
|
|
329
|
+
|
|
314
330
|
const firstUrlAtAnyBreakpoint = (value: unknown): string => {
|
|
315
331
|
if (typeof value === 'string') return value;
|
|
316
332
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return '';
|
|
@@ -334,7 +350,11 @@ const VideoBlock = ({
|
|
|
334
350
|
isDesigner = false,
|
|
335
351
|
currentBreakpoint = 'desktop'
|
|
336
352
|
}: BlockRendererProps) => {
|
|
337
|
-
const
|
|
353
|
+
const valueLocale = useValueLocale();
|
|
354
|
+
const localizedValue = resolveLocalizedValue(block.value, valueLocale);
|
|
355
|
+
const desktopUrl = (
|
|
356
|
+
typeof localizedValue === 'string' ? localizedValue : block.value
|
|
357
|
+
) as string;
|
|
338
358
|
const properties = (block.properties || {}) as VideoProperties;
|
|
339
359
|
|
|
340
360
|
/**
|
|
@@ -361,11 +381,11 @@ const VideoBlock = ({
|
|
|
361
381
|
currentBreakpoint,
|
|
362
382
|
false
|
|
363
383
|
);
|
|
364
|
-
const mobileUrl =
|
|
384
|
+
const mobileUrl = localizedMedia(
|
|
365
385
|
properties.mobileUrl,
|
|
366
386
|
currentBreakpoint,
|
|
367
|
-
|
|
368
|
-
)
|
|
387
|
+
valueLocale
|
|
388
|
+
);
|
|
369
389
|
const videoUrl =
|
|
370
390
|
isMobile && useDifferentMobileVideo && mobileUrl ? mobileUrl : desktopUrl;
|
|
371
391
|
|
|
@@ -400,7 +420,9 @@ const VideoBlock = ({
|
|
|
400
420
|
* the element with no source at all and the effect below names the one file
|
|
401
421
|
* this viewport needs — see it for why the server cannot decide this.
|
|
402
422
|
*/
|
|
403
|
-
const mobileUrlAnywhere = firstUrlAtAnyBreakpoint(
|
|
423
|
+
const mobileUrlAnywhere = firstUrlAtAnyBreakpoint(
|
|
424
|
+
resolveLocalizedValue(properties.mobileUrl, valueLocale)
|
|
425
|
+
);
|
|
404
426
|
const hasDistinctMobileVideo =
|
|
405
427
|
setAtAnyBreakpoint(properties.useDifferentMobileVideo) &&
|
|
406
428
|
!!mobileUrlAnywhere &&
|
|
@@ -430,16 +452,16 @@ const VideoBlock = ({
|
|
|
430
452
|
const nativeControls =
|
|
431
453
|
getResponsiveValue(properties.controls, currentBreakpoint, true) !== false;
|
|
432
454
|
|
|
433
|
-
const desktopPoster =
|
|
455
|
+
const desktopPoster = localizedMedia(
|
|
434
456
|
properties.poster,
|
|
435
457
|
currentBreakpoint,
|
|
436
|
-
|
|
437
|
-
)
|
|
438
|
-
const mobilePoster =
|
|
458
|
+
valueLocale
|
|
459
|
+
);
|
|
460
|
+
const mobilePoster = localizedMedia(
|
|
439
461
|
properties.mobilePoster,
|
|
440
462
|
currentBreakpoint,
|
|
441
|
-
|
|
442
|
-
)
|
|
463
|
+
valueLocale
|
|
464
|
+
);
|
|
443
465
|
const posterUrl =
|
|
444
466
|
isMobile && useDifferentMobileVideo && mobilePoster
|
|
445
467
|
? mobilePoster
|