@akinon/pz-theme 2.0.162-rc.0 → 2.0.163-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 -50
- package/package.json +4 -4
- package/readme.md +22 -0
- package/src/__tests__/custom-catalog-announce.test.ts +110 -0
- package/src/__tests__/editor-locale-bridge.test.tsx +1 -32
- package/src/blocks/__tests__/text-block.test.tsx +6 -34
- package/src/blocks/order-detail-block.tsx +95 -113
- package/src/blocks/text-block.tsx +2 -6
- package/src/chrome/composition-source.ts +7 -2
- package/src/chrome/preview-snapshot-source.ts +5 -1
- package/src/content-capabilities.ts +41 -0
- package/src/custom/README.md +219 -100
- package/src/custom/__tests__/use-theme-setting.test.tsx +0 -97
- package/src/custom/collect.ts +9 -4
- package/src/custom/property-types.ts +15 -15
- package/src/custom/use-theme-setting.ts +5 -21
- package/src/editor-locale-bridge.tsx +11 -39
- package/src/load-placeholder-sections.ts +13 -3
- package/src/placeholder-registry.ts +42 -1
- package/src/region/__tests__/editor-handshake.test.ts +144 -0
- package/src/region/__tests__/region-contract.test.ts +500 -0
- package/src/region/__tests__/region-placeholder-readers.test.ts +431 -0
- package/src/region/__tests__/region-source.test.ts +621 -0
- package/src/region/editor-handshake.ts +60 -0
- package/src/region/region-contract.ts +184 -0
- package/src/region/region-source.ts +360 -0
- package/src/theme-placeholder-wrapper.tsx +0 -3
- package/src/blocks/__tests__/order-line-actions.test.tsx +0 -54
- package/src/blocks/order-line-actions.ts +0 -45
- package/src/utils/__tests__/sanitize-rich-text.test.ts +0 -80
- package/src/utils/sanitize-rich-text.ts +0 -215
|
@@ -367,100 +367,3 @@ describe('useThemeSetting — invalid declarations throw in development', () =>
|
|
|
367
367
|
).toThrow(/not one of the editor's control types/);
|
|
368
368
|
});
|
|
369
369
|
});
|
|
370
|
-
|
|
371
|
-
describe('useThemeSetting — richtext', () => {
|
|
372
|
-
it('reads the breakpoint’s html and strips what the allowlist refuses', () => {
|
|
373
|
-
const { result } = renderHook(
|
|
374
|
-
() =>
|
|
375
|
-
useThemeSetting('body', {
|
|
376
|
-
type: 'richtext',
|
|
377
|
-
label: 'Body',
|
|
378
|
-
default: '<p>default</p>'
|
|
379
|
-
}),
|
|
380
|
-
{
|
|
381
|
-
wrapper: wrapperFor(
|
|
382
|
-
contextValue({
|
|
383
|
-
currentBreakpoint: 'mobile',
|
|
384
|
-
properties: {
|
|
385
|
-
body: {
|
|
386
|
-
desktop: '<p>desktop</p>',
|
|
387
|
-
mobile:
|
|
388
|
-
'<p onclick="x()">mobile <script>x</script><b>bold</b></p>'
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
})
|
|
392
|
-
)
|
|
393
|
-
}
|
|
394
|
-
);
|
|
395
|
-
|
|
396
|
-
expect(result.current).toBe('<p>mobile <b>bold</b></p>');
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
it('sanitizes the authored default too', () => {
|
|
400
|
-
const { result } = renderHook(
|
|
401
|
-
() =>
|
|
402
|
-
useThemeSetting('body', {
|
|
403
|
-
type: 'richtext',
|
|
404
|
-
label: 'Body',
|
|
405
|
-
default: '<p><img src=x onerror=alert(1)>safe</p>'
|
|
406
|
-
}),
|
|
407
|
-
{ wrapper: wrapperFor(contextValue()) }
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
expect(result.current).toBe('<p>safe</p>');
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
describe('written per language by the rich text control', () => {
|
|
414
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
415
|
-
const hooks = require('@akinon/next/hooks');
|
|
416
|
-
let localizationSpy: jest.SpyInstance;
|
|
417
|
-
|
|
418
|
-
const onPage = (locale: string) => {
|
|
419
|
-
const original = hooks.useLocalization;
|
|
420
|
-
localizationSpy = jest
|
|
421
|
-
.spyOn(hooks, 'useLocalization')
|
|
422
|
-
.mockImplementation(() => ({
|
|
423
|
-
...original(),
|
|
424
|
-
locale,
|
|
425
|
-
defaultLocaleValue: 'tr',
|
|
426
|
-
locales: [{ value: 'tr' }, { value: 'en' }]
|
|
427
|
-
}));
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
afterEach(() => localizationSpy?.mockRestore());
|
|
431
|
-
|
|
432
|
-
const read = (body: unknown) =>
|
|
433
|
-
renderHook(
|
|
434
|
-
() =>
|
|
435
|
-
useThemeSetting('body', {
|
|
436
|
-
type: 'richtext',
|
|
437
|
-
label: 'Body',
|
|
438
|
-
default: '<p>default</p>'
|
|
439
|
-
}),
|
|
440
|
-
{ wrapper: wrapperFor(contextValue({ properties: { body } })) }
|
|
441
|
-
).result.current;
|
|
442
|
-
|
|
443
|
-
it('reads the page’s language out of the locale map and sanitizes it', () => {
|
|
444
|
-
onPage('tr');
|
|
445
|
-
|
|
446
|
-
expect(
|
|
447
|
-
read({
|
|
448
|
-
en: '<p>old</p>',
|
|
449
|
-
tr: '<p>sss <b>bold</b><script>x()</script></p>'
|
|
450
|
-
})
|
|
451
|
-
).toBe('<p>sss <b>bold</b></p>');
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
it('falls back to the default language’s entry', () => {
|
|
455
|
-
onPage('en');
|
|
456
|
-
|
|
457
|
-
expect(read({ tr: '<p>türkçe</p>' })).toBe('<p>türkçe</p>');
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
it('still reads a breakpoint map written in the default language', () => {
|
|
461
|
-
onPage('tr');
|
|
462
|
-
|
|
463
|
-
expect(read({ desktop: '<p>desktop</p>' })).toBe('<p>desktop</p>');
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
});
|
package/src/custom/collect.ts
CHANGED
|
@@ -42,7 +42,9 @@ import type { CustomSettingDescriptor, CustomSlotDescriptor } from './types';
|
|
|
42
42
|
* through the one function below.
|
|
43
43
|
*/
|
|
44
44
|
export type ThemeSettingChoice =
|
|
45
|
-
|
|
45
|
+
| string
|
|
46
|
+
| number
|
|
47
|
+
| { label?: string; value: unknown };
|
|
46
48
|
|
|
47
49
|
/**
|
|
48
50
|
* Normalizes the shorthand into the `{label, value}` pairs the editor's select
|
|
@@ -78,8 +80,8 @@ export const normalizeSettingChoices = (
|
|
|
78
80
|
|
|
79
81
|
/**
|
|
80
82
|
* Why a control type is refused, named per type. "Unsupported" tells a
|
|
81
|
-
* developer nothing actionable; each of these fails for its own reason
|
|
82
|
-
* each has a different way out.
|
|
83
|
+
* developer nothing actionable; each of these three fails for its own reason
|
|
84
|
+
* and each has a different way out.
|
|
83
85
|
*/
|
|
84
86
|
const UNSUPPORTED_TYPE_REASON: Record<string, string> = {
|
|
85
87
|
multiselect:
|
|
@@ -88,7 +90,10 @@ const UNSUPPORTED_TYPE_REASON: Record<string, string> = {
|
|
|
88
90
|
'Use "select" and store one value until a per-type resolver exists',
|
|
89
91
|
localetext:
|
|
90
92
|
'the editor stores a localetext value as a locale map with no "desktop" key, so getResponsiveValue ' +
|
|
91
|
-
'falls back and the block would read its authored default forever. Use "text" and localize in the component'
|
|
93
|
+
'falls back and the block would read its authored default forever. Use "text" and localize in the component',
|
|
94
|
+
richtext:
|
|
95
|
+
'there is no safe renderer for it here — the sanitizer this repo owns parses by assigning to innerHTML ' +
|
|
96
|
+
'on a live element, and pz-theme renders on the server where there is no document at all. Use "text"'
|
|
92
97
|
};
|
|
93
98
|
|
|
94
99
|
/** The reason a setting key is refused, or null. Shared by the declared and the collected path. */
|
|
@@ -33,20 +33,19 @@ export type PropertyType = (typeof CUSTOM_PROPERTY_TYPES)[number];
|
|
|
33
33
|
* The subset whose value provably survives the full round trip — panel write,
|
|
34
34
|
* persistence, and resolution back into a prop.
|
|
35
35
|
*
|
|
36
|
-
* The
|
|
37
|
-
* guessed:
|
|
38
|
-
* (`theme-basic-properties.tsx:1547-1548` passes arrays and locale objects
|
|
39
|
-
* through without the per-breakpoint wrapper). `getResponsiveValue` then
|
|
40
|
-
* rejects an array with a devWarn and returns the fallback, and a locale
|
|
41
|
-
* object has no `desktop` key so it does too. They can never deliver a
|
|
42
|
-
* value until a per-type resolver exists.
|
|
36
|
+
* The three excluded members are excluded for reasons that were verified, not
|
|
37
|
+
* guessed:
|
|
43
38
|
*
|
|
44
|
-
* `
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
39
|
+
* - `multiselect` and `localetext` are stored UNBUCKETED by the editor
|
|
40
|
+
* (`theme-basic-properties.tsx:1547-1548` passes arrays and locale objects
|
|
41
|
+
* through without the per-breakpoint wrapper). `getResponsiveValue` then
|
|
42
|
+
* rejects an array with a devWarn and returns the fallback, and a locale
|
|
43
|
+
* object has no `desktop` key so it does too. They can never deliver a
|
|
44
|
+
* value until a per-type resolver exists.
|
|
45
|
+
* - `richtext` has no safe renderer here. The allowlist sanitizer this repo
|
|
46
|
+
* already owns parses by assigning to `innerHTML` on a live element, so the
|
|
47
|
+
* sink is its first act — and pz-theme renders server-side, where there is
|
|
48
|
+
* no `document` at all.
|
|
50
49
|
*
|
|
51
50
|
* Declaring one of these throws at define time rather than shipping a control
|
|
52
51
|
* that silently shows the default forever.
|
|
@@ -60,11 +59,12 @@ export const SUPPORTED_PROPERTY_TYPES: readonly PropertyType[] = [
|
|
|
60
59
|
'color',
|
|
61
60
|
'image',
|
|
62
61
|
'file',
|
|
63
|
-
'richtext',
|
|
64
62
|
'typography',
|
|
65
63
|
'icon'
|
|
66
64
|
];
|
|
67
65
|
|
|
68
|
-
export const isSupportedPropertyType = (
|
|
66
|
+
export const isSupportedPropertyType = (
|
|
67
|
+
type: unknown
|
|
68
|
+
): type is PropertyType =>
|
|
69
69
|
typeof type === 'string' &&
|
|
70
70
|
(SUPPORTED_PROPERTY_TYPES as readonly string[]).includes(type);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { getResponsiveValue } from '../utils';
|
|
4
|
-
import { sanitizeRichText } from '../utils/sanitize-rich-text';
|
|
5
4
|
import { devWarn } from '../utils/dev-warn';
|
|
6
5
|
import {
|
|
7
6
|
isLocalizedValue,
|
|
@@ -239,11 +238,6 @@ export function useThemeSetting<
|
|
|
239
238
|
|
|
240
239
|
const defaults = splitDefault(declaration, valueLocale);
|
|
241
240
|
|
|
242
|
-
const finish = (value: unknown): TValue =>
|
|
243
|
-
(declaration.type === 'richtext'
|
|
244
|
-
? sanitizeRichText(typeof value === 'string' ? value : '')
|
|
245
|
-
: value) as TValue;
|
|
246
|
-
|
|
247
241
|
if (!context) {
|
|
248
242
|
// A missing block context is never an error on its own: a developer
|
|
249
243
|
// previewing the component in a storybook or a unit test should see the
|
|
@@ -260,7 +254,7 @@ export function useThemeSetting<
|
|
|
260
254
|
`unreachable and the declared default is used instead. Render this ` +
|
|
261
255
|
`component through a block registered with registerCustomBlocks().`
|
|
262
256
|
);
|
|
263
|
-
return
|
|
257
|
+
return defaults.page as TValue;
|
|
264
258
|
}
|
|
265
259
|
|
|
266
260
|
// Reported on every render rather than once from an effect: the collection
|
|
@@ -277,17 +271,7 @@ export function useThemeSetting<
|
|
|
277
271
|
// attribute keys that were never declared. `node` rather than `block` so a
|
|
278
272
|
// setting declared inside a custom SECTION reads the section's own
|
|
279
273
|
// properties, which is where the editor writes it.
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
// The rich text control writes per language — `{ tr: '<p>…</p>', en: … }` —
|
|
283
|
-
// and the editor stores that map as it is, with no breakpoint key: the same
|
|
284
|
-
// shape that keeps `localetext` out (see `property-types.ts`). Read the
|
|
285
|
-
// page's language out of it first; what is left is a string or a breakpoint
|
|
286
|
-
// map, which the responsive read below already understands.
|
|
287
|
-
const stored =
|
|
288
|
-
declaration.type === 'richtext'
|
|
289
|
-
? resolveLocalizedValue(authored, valueLocale)
|
|
290
|
-
: authored;
|
|
274
|
+
const stored = context.node.properties?.[key];
|
|
291
275
|
|
|
292
276
|
// The sentinel again, for the same reason: handing `getResponsiveValue` an
|
|
293
277
|
// undefined fallback would resolve an undeclared default to `''`, so a
|
|
@@ -296,15 +280,15 @@ export function useThemeSetting<
|
|
|
296
280
|
const resolved = getResponsiveValue(stored, breakpoint, NOT_AUTHORED);
|
|
297
281
|
|
|
298
282
|
if (resolved === NOT_AUTHORED) {
|
|
299
|
-
return
|
|
283
|
+
return resolveAuthoredDefault(defaults.page, breakpoint) as TValue;
|
|
300
284
|
}
|
|
301
285
|
|
|
302
286
|
// The default-language text as the editor seeded it when the block was
|
|
303
287
|
// placed, never changed since: still the author's copy, so it follows the
|
|
304
288
|
// page's language. Anything the merchant typed is theirs and shows as typed.
|
|
305
289
|
if (defaults.perLocale && resolved === defaults.base) {
|
|
306
|
-
return
|
|
290
|
+
return defaults.page as TValue;
|
|
307
291
|
}
|
|
308
292
|
|
|
309
|
-
return
|
|
293
|
+
return resolved as TValue;
|
|
310
294
|
}
|
|
@@ -4,28 +4,14 @@ import { useEffect } from 'react';
|
|
|
4
4
|
import { useLocalization } from '@akinon/next/hooks';
|
|
5
5
|
|
|
6
6
|
import { isTrustedEditorEvent, postToEditor } from './editor-bridge';
|
|
7
|
-
import { useValueLocale } from './utils/localized-value';
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* The storefront half of the editor's Language dropdown.
|
|
11
10
|
*
|
|
12
11
|
* The editor starts that dropdown with English only and fills it from the
|
|
13
|
-
* `LOCALE_UPDATE` frame the preview posts (`{ locale, locales
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* The frame carries the storefront's default language as well. The editor's
|
|
18
|
-
* per-language text controls store a plain string in that language and a map
|
|
19
|
-
* in every other, so an editor that never heard it assumed English and gave
|
|
20
|
-
* every Turkish-default store a stray `en` key (ZERO-5296).
|
|
21
|
-
*
|
|
22
|
-
* It is posted BEFORE the document reports ready (`IFRAME_READY`): the bridge
|
|
23
|
-
* is a child of the placeholder wrapper, whose own effect posts that frame,
|
|
24
|
-
* and React runs a child's effects first. The editor relies on the order — it
|
|
25
|
-
* holds every per-language write until the preview is ready, and takes
|
|
26
|
-
* "ready without languages" to mean a storefront that will never send them.
|
|
27
|
-
*
|
|
28
|
-
* For a long time the only code answering either direction
|
|
12
|
+
* `LOCALE_UPDATE` frame the preview posts (`{ locale, locales }`); when the
|
|
13
|
+
* merchant picks another language it posts `LOCALE_UPDATE` back and expects the
|
|
14
|
+
* preview to follow. For a long time the only code answering either direction
|
|
29
15
|
* was the app-template's `LanguageSelect` — brand code. A brand whose header no
|
|
30
16
|
* longer renders that component (a header composed through ChromeSlot, a
|
|
31
17
|
* custom header section, a project older than the template) left the editor
|
|
@@ -68,7 +54,6 @@ export interface EditorLocaleOption {
|
|
|
68
54
|
interface LocaleState {
|
|
69
55
|
locale: string;
|
|
70
56
|
locales: EditorLocaleOption[];
|
|
71
|
-
defaultLocale?: string;
|
|
72
57
|
setLocale: (locale: string) => void;
|
|
73
58
|
}
|
|
74
59
|
|
|
@@ -101,22 +86,15 @@ const toEditorLocales = (locales: unknown): EditorLocaleOption[] =>
|
|
|
101
86
|
: entry.value
|
|
102
87
|
}));
|
|
103
88
|
|
|
104
|
-
const announcementKey = (
|
|
105
|
-
locale,
|
|
106
|
-
locales,
|
|
107
|
-
defaultLocale
|
|
108
|
-
}: Omit<LocaleState, 'setLocale'>) =>
|
|
109
|
-
JSON.stringify([locale, locales, defaultLocale ?? null]);
|
|
89
|
+
const announcementKey = (locale: string, locales: EditorLocaleOption[]) =>
|
|
90
|
+
JSON.stringify([locale, locales]);
|
|
110
91
|
|
|
111
|
-
const announce = ({ locale, locales
|
|
112
|
-
const key = announcementKey(
|
|
92
|
+
const announce = ({ locale, locales }: LocaleState) => {
|
|
93
|
+
const key = announcementKey(locale, locales);
|
|
113
94
|
if (key === lastAnnounced) return;
|
|
114
95
|
lastAnnounced = key;
|
|
115
96
|
|
|
116
|
-
postToEditor({
|
|
117
|
-
type: 'LOCALE_UPDATE',
|
|
118
|
-
data: { locale, locales, defaultLocale }
|
|
119
|
-
});
|
|
97
|
+
postToEditor({ type: 'LOCALE_UPDATE', data: { locale, locales } });
|
|
120
98
|
};
|
|
121
99
|
|
|
122
100
|
const handleEditorMessage = (event: MessageEvent) => {
|
|
@@ -137,7 +115,7 @@ const handleEditorMessage = (event: MessageEvent) => {
|
|
|
137
115
|
|
|
138
116
|
// The editor is already showing `requested`. Recording it as announced keeps
|
|
139
117
|
// the switch from being echoed back when the new locale reaches the page.
|
|
140
|
-
lastAnnounced = announcementKey(
|
|
118
|
+
lastAnnounced = announcementKey(requested, current.locales);
|
|
141
119
|
current.setLocale(requested);
|
|
142
120
|
};
|
|
143
121
|
|
|
@@ -148,7 +126,6 @@ const handleEditorMessage = (event: MessageEvent) => {
|
|
|
148
126
|
*/
|
|
149
127
|
export function EditorLocaleBridge(): null {
|
|
150
128
|
const { locale, locales, setLocale } = useLocalization();
|
|
151
|
-
const { defaultLocale } = useValueLocale();
|
|
152
129
|
|
|
153
130
|
useEffect(() => {
|
|
154
131
|
if (!isFramed()) return;
|
|
@@ -170,14 +147,9 @@ export function EditorLocaleBridge(): null {
|
|
|
170
147
|
useEffect(() => {
|
|
171
148
|
if (!isFramed() || !locale) return;
|
|
172
149
|
|
|
173
|
-
current = {
|
|
174
|
-
locale,
|
|
175
|
-
locales: toEditorLocales(locales),
|
|
176
|
-
defaultLocale,
|
|
177
|
-
setLocale
|
|
178
|
-
};
|
|
150
|
+
current = { locale, locales: toEditorLocales(locales), setLocale };
|
|
179
151
|
announce(current);
|
|
180
|
-
}, [locale, locales,
|
|
152
|
+
}, [locale, locales, setLocale]);
|
|
181
153
|
|
|
182
154
|
return null;
|
|
183
155
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
PreviewSnapshot
|
|
14
14
|
} from './chrome/preview-snapshot-source';
|
|
15
15
|
import { getThemeConfig, ThemeConfig } from './chrome/theme-config-source';
|
|
16
|
+
import { withRegionDataSources } from './region/region-source'; // ZERO-5391
|
|
16
17
|
import { readCustomBreakpoints } from './utils/breakpoints';
|
|
17
18
|
// One acceptance rule for this loader and the designer canvas's client fetch
|
|
18
19
|
// (utils/live-rows.ts), so the two cannot take different answers.
|
|
@@ -295,6 +296,11 @@ export async function loadPlaceholderSections(
|
|
|
295
296
|
);
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
// ZERO-5391: a region copy carries the data-source records its sections
|
|
300
|
+
// reference, so they resolve even while the shared theme-config is still
|
|
301
|
+
// cached without them; any other widget keeps theme-config's list.
|
|
302
|
+
const effectiveDataSources = withRegionDataSources(dataSources, data);
|
|
303
|
+
|
|
298
304
|
const sections: Section[] = [];
|
|
299
305
|
|
|
300
306
|
if (data?.attributes?.theme_editor_placeholder) {
|
|
@@ -460,7 +466,7 @@ export async function loadPlaceholderSections(
|
|
|
460
466
|
|
|
461
467
|
const dataSourceWithData = await resolveSectionDataSource(
|
|
462
468
|
dataSourceId,
|
|
463
|
-
|
|
469
|
+
effectiveDataSources, // ZERO-5391
|
|
464
470
|
sectionSlug,
|
|
465
471
|
sectionData
|
|
466
472
|
);
|
|
@@ -504,8 +510,12 @@ export async function loadPlaceholderSections(
|
|
|
504
510
|
|
|
505
511
|
return {
|
|
506
512
|
sections,
|
|
507
|
-
|
|
508
|
-
|
|
513
|
+
// ZERO-5391: always the canonical slug. A region copy
|
|
514
|
+
// (`<slug>-region-<cc>`) or a draft-mode per-widget preview copy answers
|
|
515
|
+
// under its own slug, while the editor and every frame address
|
|
516
|
+
// placeholders canonically.
|
|
517
|
+
placeholderId: slug,
|
|
518
|
+
dataSources: effectiveDataSources, // ZERO-5391
|
|
509
519
|
themeSettings,
|
|
510
520
|
customBreakpoints
|
|
511
521
|
};
|
|
@@ -13,6 +13,15 @@ import {
|
|
|
13
13
|
STATE_PIN_CAPABILITIES,
|
|
14
14
|
type StatePinCapability
|
|
15
15
|
} from './state-pin-capabilities';
|
|
16
|
+
// ZERO-5391: the per-country content handshake.
|
|
17
|
+
import {
|
|
18
|
+
CONTENT_CAPABILITIES,
|
|
19
|
+
type ContentCapability
|
|
20
|
+
} from './content-capabilities';
|
|
21
|
+
import {
|
|
22
|
+
defaultLocaleForEditor,
|
|
23
|
+
localeUrlStrategyForEditor
|
|
24
|
+
} from './region/editor-handshake';
|
|
16
25
|
import type { CustomSectionCatalog } from './custom/section-types';
|
|
17
26
|
import type { CustomPageCatalog } from './custom/page-types';
|
|
18
27
|
import type { CustomCatalog } from './custom/types';
|
|
@@ -119,6 +128,32 @@ interface PlaceholdersAvailablePayload {
|
|
|
119
128
|
* without the field would put it back to reporting nothing.
|
|
120
129
|
*/
|
|
121
130
|
signalCapabilities?: readonly SignalCapability[];
|
|
131
|
+
|
|
132
|
+
// ZERO-5391: the three fields below are the per-country content handshake.
|
|
133
|
+
/**
|
|
134
|
+
* Which content THIS build can serve differently by audience (see
|
|
135
|
+
* `content-capabilities.ts`). The editor offers its country picker only to
|
|
136
|
+
* a storefront that lists `region-content`.
|
|
137
|
+
*
|
|
138
|
+
* Same terms as `actionCapabilities` above and on every frame for the same
|
|
139
|
+
* reason: the editor replaces what it holds with each frame, so a frame
|
|
140
|
+
* without the field would hide the picker again halfway through a session.
|
|
141
|
+
*/
|
|
142
|
+
contentCapabilities?: readonly ContentCapability[];
|
|
143
|
+
/**
|
|
144
|
+
* `settings.localization.localeUrlStrategy`, 'hide-default-locale' when
|
|
145
|
+
* unset (see `region/editor-handshake.ts`). The editor moves its canvas
|
|
146
|
+
* between countries by switching the preview's locale, which under
|
|
147
|
+
* 'subdomain' would mean another origin. Every frame, on the same terms.
|
|
148
|
+
*/
|
|
149
|
+
localeUrlStrategy?: string;
|
|
150
|
+
/**
|
|
151
|
+
* `settings.localization.defaultLocaleValue`: the locale the hide-*
|
|
152
|
+
* strategies serve without a path prefix, which the editor needs to link a
|
|
153
|
+
* country's page and to choose the locale a country switch lands on. Every
|
|
154
|
+
* frame, on the same terms.
|
|
155
|
+
*/
|
|
156
|
+
defaultLocale?: string;
|
|
122
157
|
}
|
|
123
158
|
|
|
124
159
|
const isFramed = (): boolean =>
|
|
@@ -163,7 +198,13 @@ const flush = () => {
|
|
|
163
198
|
// Outside the designer gate for the same reason, and for one more: the
|
|
164
199
|
// panel it gates is the merchant's, so a slow handshake must not be the
|
|
165
200
|
// difference between a section being trackable and not.
|
|
166
|
-
signalCapabilities: SIGNAL_CAPABILITIES
|
|
201
|
+
signalCapabilities: SIGNAL_CAPABILITIES,
|
|
202
|
+
// ZERO-5391: outside the designer gate for the same reasons. A fact about
|
|
203
|
+
// this theme version and two public locale settings: what decides whether
|
|
204
|
+
// the editor offers per-country content, and how it switches countries.
|
|
205
|
+
contentCapabilities: CONTENT_CAPABILITIES,
|
|
206
|
+
localeUrlStrategy: localeUrlStrategyForEditor(),
|
|
207
|
+
defaultLocale: defaultLocaleForEditor()
|
|
167
208
|
};
|
|
168
209
|
|
|
169
210
|
// Omitted entirely when nothing is registered: a build with no brand pages
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO-5391 — the locale settings the storefront hands the editor on
|
|
3
|
+
* PLACEHOLDERS_AVAILABLE, read from the brand's `settings` module.
|
|
4
|
+
*
|
|
5
|
+
* `settings` resolves to `jest/__mocks__/settings.js` unless a test swaps it.
|
|
6
|
+
* Each swap loads a fresh copy of the module under test, and `afterEach`
|
|
7
|
+
* removes the swap so the next test sees the shared stub again.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Type-only, so the module under test is still loaded fresh per test below;
|
|
11
|
+
// it also keeps this file a module rather than a global script.
|
|
12
|
+
import type * as HandshakeModule from '../editor-handshake';
|
|
13
|
+
|
|
14
|
+
type Handshake = typeof HandshakeModule;
|
|
15
|
+
|
|
16
|
+
const load = (settingsModule?: unknown): Handshake => {
|
|
17
|
+
let loaded: Handshake | undefined;
|
|
18
|
+
jest.isolateModules(() => {
|
|
19
|
+
if (settingsModule !== undefined) {
|
|
20
|
+
jest.doMock('settings', () => settingsModule);
|
|
21
|
+
}
|
|
22
|
+
loaded = require('../editor-handshake');
|
|
23
|
+
});
|
|
24
|
+
return loaded!;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe('editor handshake settings', () => {
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
jest.dontMock('settings');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('reads the storefront settings the jest stub carries', () => {
|
|
33
|
+
const handshake = load();
|
|
34
|
+
|
|
35
|
+
expect(handshake.localeUrlStrategyForEditor()).toBe('hide-default-locale');
|
|
36
|
+
expect(handshake.defaultLocaleForEditor()).toBe('en-us');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('passes the configured strategy and default locale through', () => {
|
|
40
|
+
// The brand's settings.js is CommonJS, so its default import is the
|
|
41
|
+
// module object itself.
|
|
42
|
+
const handshake = load({
|
|
43
|
+
localization: { localeUrlStrategy: 'subdomain', defaultLocaleValue: 'tr' }
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(handshake.localeUrlStrategyForEditor()).toBe('subdomain');
|
|
47
|
+
expect(handshake.defaultLocaleForEditor()).toBe('tr');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('reads an ES-module settings default export the same way', () => {
|
|
51
|
+
const handshake = load({
|
|
52
|
+
__esModule: true,
|
|
53
|
+
default: {
|
|
54
|
+
localization: {
|
|
55
|
+
localeUrlStrategy: 'show-all-locales',
|
|
56
|
+
defaultLocaleValue: 'en-ae'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(handshake.localeUrlStrategyForEditor()).toBe('show-all-locales');
|
|
62
|
+
expect(handshake.defaultLocaleForEditor()).toBe('en-ae');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it.each([
|
|
66
|
+
['empty settings', {}],
|
|
67
|
+
['no localization values', { localization: {} }],
|
|
68
|
+
['a null localization', { localization: null }],
|
|
69
|
+
['an undefined default export', { __esModule: true, default: undefined }]
|
|
70
|
+
])('falls back to the defaults for %s', (_label, settingsModule) => {
|
|
71
|
+
const handshake = load(settingsModule);
|
|
72
|
+
|
|
73
|
+
// 'hide-default-locale' is what the core's locale middleware routes by
|
|
74
|
+
// when the strategy is unset, so the editor is told what really happens.
|
|
75
|
+
expect(handshake.localeUrlStrategyForEditor()).toBe('hide-default-locale');
|
|
76
|
+
expect(handshake.defaultLocaleForEditor()).toBe('');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('treats an empty or non-string strategy as unset', () => {
|
|
80
|
+
expect(
|
|
81
|
+
load({
|
|
82
|
+
localization: { localeUrlStrategy: '' }
|
|
83
|
+
}).localeUrlStrategyForEditor()
|
|
84
|
+
).toBe('hide-default-locale');
|
|
85
|
+
expect(
|
|
86
|
+
load({
|
|
87
|
+
localization: { localeUrlStrategy: 42 }
|
|
88
|
+
}).localeUrlStrategyForEditor()
|
|
89
|
+
).toBe('hide-default-locale');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('sends the default locale only when it is a string', () => {
|
|
93
|
+
expect(
|
|
94
|
+
load({
|
|
95
|
+
localization: { defaultLocaleValue: ['en'] }
|
|
96
|
+
}).defaultLocaleForEditor()
|
|
97
|
+
).toBe('');
|
|
98
|
+
expect(
|
|
99
|
+
load({
|
|
100
|
+
localization: { defaultLocaleValue: '' }
|
|
101
|
+
}).defaultLocaleForEditor()
|
|
102
|
+
).toBe('');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('answers the defaults instead of throwing when the settings read throws', () => {
|
|
106
|
+
// The announce these values ride is the frame every editor load waits
|
|
107
|
+
// for; a settings read must never be able to take it down.
|
|
108
|
+
const handshake = load({
|
|
109
|
+
get localization() {
|
|
110
|
+
throw new Error('settings are not readable');
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(handshake.localeUrlStrategyForEditor()).toBe('hide-default-locale');
|
|
115
|
+
expect(handshake.defaultLocaleForEditor()).toBe('');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('reads nothing at module scope and the current settings on every call', () => {
|
|
119
|
+
const settingsModule = {
|
|
120
|
+
localization: {
|
|
121
|
+
localeUrlStrategy: 'hide-all-locales',
|
|
122
|
+
defaultLocaleValue: 'en'
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const read = jest.fn(() => settingsModule.localization);
|
|
126
|
+
const handshake = load({
|
|
127
|
+
get localization() {
|
|
128
|
+
return read();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(read).not.toHaveBeenCalled();
|
|
133
|
+
expect(handshake.localeUrlStrategyForEditor()).toBe('hide-all-locales');
|
|
134
|
+
|
|
135
|
+
settingsModule.localization = {
|
|
136
|
+
localeUrlStrategy: 'show-all-locales',
|
|
137
|
+
defaultLocaleValue: 'tr'
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
expect(handshake.localeUrlStrategyForEditor()).toBe('show-all-locales');
|
|
141
|
+
expect(handshake.defaultLocaleForEditor()).toBe('tr');
|
|
142
|
+
expect(read).toHaveBeenCalledTimes(3);
|
|
143
|
+
});
|
|
144
|
+
});
|