@akinon/pz-theme 2.0.162-beta.0 → 2.0.162

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.162-beta.0
3
+ ## 2.0.162
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - Updated dependencies [cbbbfd757]
8
- - @akinon/next@2.0.162-beta.0
9
- - @akinon/theme-contract@2.0.162-beta.0
7
+ - 8758f3a3: ZERO-5391: Per-country placeholder content. When the request locale names a country (`en-ae` → `ae`) and the `theme-regions` registry lists a placeholder for it, the placeholder renders from that country's copy (`<slug>-region-<cc>`; in draft mode its `-preview-<id>` blob first) instead of the shared default, with the data-source records the copy carries (`theme_region_dataSources`) filling in any id theme-config lacks (theme-config's record wins when it has the id). Unlisted placeholders, language-only locales and a missing or unreadable registry keep today's reads; a server process that has read the registry keeps using it for up to 15 minutes while reads fail. `PLACEHOLDERS_AVAILABLE` now carries `contentCapabilities`, `localeUrlStrategy` and `defaultLocale`, and placeholders always report their canonical slug as `placeholderId`.
8
+ - @akinon/next@2.0.162
9
+ - @akinon/theme-contract@2.0.162
10
10
 
11
11
  ## 2.0.161
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.162-beta.0",
4
+ "version": "2.0.162",
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.162-beta.0",
11
+ "@akinon/next": "2.0.162",
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.162-beta.0",
16
+ "@akinon/theme-contract": "2.0.162",
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.162-beta.0",
24
+ "@akinon/next": "2.0.162",
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",
package/readme.md CHANGED
@@ -26,6 +26,28 @@ export default async function Page() {
26
26
 
27
27
  Developer-authored sections and blocks are written against `@akinon/pz-theme/src/custom` — see [`src/custom/README.md`](./src/custom/README.md) and [`src/custom/README-sections.md`](./src/custom/README-sections.md). Reference examples live in `examples/header/` at the root of the pz-next repository; they are not shipped in the npm package.
28
28
 
29
+ ## Per-country content (ZERO-5391)
30
+
31
+ The theme editor can give one country its own copy of a placeholder (a page body, the header or footer, a custom page, a modal) while every other visitor keeps the shared content. pz-theme serves it without any change in the brand's code:
32
+
33
+ - The country ("region") is the last part of the request locale when that part is a two-letter code: `en-ae` and `ar-ae` both read `ae`, `zh-hant-tw` reads `tw`. A language-only locale (`en`, `tr`) has no region, and its requests make no new reads.
34
+ - The editor lists each country's customised placeholders in the `theme-regions` widget. A listed placeholder renders from `<slug>-region-<cc>`, with the data-source records that copy carries (`theme_region_dataSources`) filling in any id theme-config lacks; theme-config's record wins when it has the id. An unlisted placeholder is never looked up under a country slug.
35
+ - Country content that cannot be read is replaced by the shared content, which is what the country's visitors saw before the area was customised. That covers a listed copy that does not answer or has no section order, and a registry that is missing or unreadable (a server process that has read the registry before keeps using that one for a while; see below).
36
+ - Placeholders report their canonical slug (`header`, never `header-region-ae`) as `placeholderId`.
37
+ - Every `PLACEHOLDERS_AVAILABLE` frame carries `contentCapabilities: ['region-content']`, `localeUrlStrategy` and `defaultLocale` (from `settings.localization`). The editor offers countries only to a storefront that announces the capability, does not use `LocaleUrlStrategy.Subdomain` (switching the preview's locale would change its origin) and has at least two audiences: its distinct countries, plus one if any locale is language-only.
38
+
39
+ There is no on-demand revalidation, so changes reach visitors as caches expire:
40
+
41
+ - Customising or reverting an area: up to about two minutes (the registry is cached for 60 s in Redis and kept for 60 s per server process), plus the page's ISR window. An area reverted and customised again inside the widget cache window can briefly show its previous country content.
42
+ - When a registry read fails, is unreadable or finds no registry, a server process that has read one before keeps serving that one for up to 15 minutes after its last successful read, trying again every 10 seconds, so an outage does not flip countries back to the shared content. A registry that is deleted on purpose stays in effect just as long.
43
+ - Publishing a country's edits: the same caching as any widget (Redis 900 s, then ISR).
44
+
45
+ Requirements and known limits:
46
+
47
+ - Live country content works on every core this package supports.
48
+ - Country previews (the editor's draft, "View on site" and signed draft links in a country locale) need `@akinon/next` 1.126.15 or later and the `[pz]` route segment, as named previews do. Without them a country's draft cannot be previewed on the storefront, and its pages show live content.
49
+ - A signed draft link opened in a country locale shows the shared DRAFT in the areas that country has not customised, while the editor's canvas shows the shared LIVE content there. Customised areas show the country's own draft in both.
50
+
29
51
  ## Using pz-theme on the v1 line
30
52
 
31
53
  `@akinon/pz-theme` and `@akinon/theme-contract` have a single release line. They are developed on `main` and published with the 2.x packages; the `v1` branch has no copy of them and publishes no 1.x version. A brand still on v1 (Next 14.2, React 18, `@akinon/next` 1.x) installs the same releases as a v2 brand.
@@ -460,4 +460,114 @@ describe('PLACEHOLDERS_AVAILABLE announce', () => {
460
460
  ]);
461
461
  expect(second.placeholders).toEqual(['header', 'footer']);
462
462
  });
463
+
464
+ // ZERO-5391 — per-country content.
465
+
466
+ it('announces per-country content and the locale settings before the editor proves itself', () => {
467
+ const { registry } = load();
468
+
469
+ registry.registerPlaceholder('header');
470
+ jest.advanceTimersByTime(100);
471
+
472
+ // On the gated frame, like the action list: for a build with no custom
473
+ // catalog it is the only frame the editor ever gets, and a frame without
474
+ // the capability keeps the editor's country picker hidden.
475
+ //
476
+ // Literals rather than the constants, for the same reason as the action
477
+ // list: these pin the wire values. The locale values are the ones
478
+ // jest/__mocks__/settings.js carries.
479
+ const [payload] = frames();
480
+ expect(payload.contentCapabilities).toEqual(['region-content']);
481
+ expect(payload.localeUrlStrategy).toBe('hide-default-locale');
482
+ expect(payload.defaultLocale).toBe('en-us');
483
+ });
484
+
485
+ it('keeps the per-country fields on both frames of the designer re-announce', () => {
486
+ const { registry, register } = load();
487
+
488
+ register.registerCustomBlocks('acme', { 'promo-strip': Promo });
489
+ registry.registerPlaceholder('header');
490
+ jest.advanceTimersByTime(100);
491
+
492
+ (window as { __themeEditorDesigner?: boolean }).__themeEditorDesigner =
493
+ true;
494
+ window.dispatchEvent(
495
+ new MessageEvent('message', {
496
+ origin: EDITOR_ORIGIN,
497
+ source: window.parent as Window,
498
+ data: { type: 'SET_THEME_EDITOR_COOKIE', data: true }
499
+ })
500
+ );
501
+ jest.advanceTimersByTime(100);
502
+
503
+ // Every frame, not just the first. The editor replaces what it holds with
504
+ // each announce, so a later frame without the capability would take the
505
+ // country picker away halfway through a session.
506
+ expect(postMessage).toHaveBeenCalledTimes(2);
507
+ expect(
508
+ frames().map(
509
+ ({ contentCapabilities, localeUrlStrategy, defaultLocale }) => ({
510
+ contentCapabilities,
511
+ localeUrlStrategy,
512
+ defaultLocale
513
+ })
514
+ )
515
+ ).toEqual([
516
+ {
517
+ contentCapabilities: ['region-content'],
518
+ localeUrlStrategy: 'hide-default-locale',
519
+ defaultLocale: 'en-us'
520
+ },
521
+ {
522
+ contentCapabilities: ['region-content'],
523
+ localeUrlStrategy: 'hide-default-locale',
524
+ defaultLocale: 'en-us'
525
+ }
526
+ ]);
527
+ });
528
+
529
+ it('announces the locale settings the build actually routes by', () => {
530
+ // The stub's strategy is also the fallback, so only settings of another
531
+ // shape prove the frame reads them rather than a constant.
532
+ jest.doMock('settings', () => ({
533
+ localization: { localeUrlStrategy: 'subdomain', defaultLocaleValue: 'tr' }
534
+ }));
535
+ try {
536
+ const { registry } = load();
537
+
538
+ registry.registerPlaceholder('header');
539
+ jest.advanceTimersByTime(100);
540
+
541
+ const [payload] = frames();
542
+ expect(payload.localeUrlStrategy).toBe('subdomain');
543
+ expect(payload.defaultLocale).toBe('tr');
544
+ expect(payload.contentCapabilities).toEqual(['region-content']);
545
+ } finally {
546
+ jest.dontMock('settings');
547
+ }
548
+ });
549
+
550
+ it('still announces when the settings cannot be read', () => {
551
+ jest.doMock('settings', () => ({
552
+ get localization() {
553
+ throw new Error('settings are not readable');
554
+ }
555
+ }));
556
+ try {
557
+ const { registry } = load();
558
+
559
+ registry.registerPlaceholder('header');
560
+ jest.advanceTimersByTime(100);
561
+
562
+ // The frame is what every editor load waits for. Losing it to a settings
563
+ // read would cost the whole canvas, not just the country picker.
564
+ expect(postMessage).toHaveBeenCalledTimes(1);
565
+ const [payload] = frames();
566
+ expect(payload.placeholders).toEqual(['header']);
567
+ expect(payload.localeUrlStrategy).toBe('hide-default-locale');
568
+ expect(payload.defaultLocale).toBe('');
569
+ } finally {
570
+ jest.dontMock('settings');
571
+ }
572
+ });
463
573
  });
@@ -3,6 +3,7 @@ import * as React from 'react';
3
3
  import { CompositionState, readCompositionState } from './composition-state';
4
4
  import { getPlaceholderPreviewSnapshot } from './preview-snapshot-source';
5
5
  import { getWidgetDataWithRetry } from './retrying-widget-data';
6
+ import { readMarketPlaceholderCopy } from '../region/region-source'; // ZERO-5391
6
7
 
7
8
  // `React.cache` exists on the RSC-capable React this package renders under;
8
9
  // fall back to the raw function on older cores, where correctness is identical
@@ -20,9 +21,13 @@ export type PlaceholderWidget = {
20
21
  const fetchPlaceholderWidget = async (
21
22
  slug: string
22
23
  ): Promise<PlaceholderWidget> =>
23
- (await getWidgetDataWithRetry<{
24
+ // ZERO-5391: a placeholder the request's region has its own copy of reads
25
+ // that copy; anything else, and a copy that does not answer, reads the
26
+ // shared default exactly as before.
27
+ ((await readMarketPlaceholderCopy(slug)) as PlaceholderWidget) ??
28
+ ((await getWidgetDataWithRetry<{
24
29
  attributes?: { theme_editor_placeholder?: unknown };
25
- }>(slug)) as PlaceholderWidget;
30
+ }>(slug)) as PlaceholderWidget);
26
31
 
27
32
  /**
28
33
  * The one fetch of a placeholder widget per request.
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import * as serverData from '@akinon/next/data/server';
3
+ import { resolveRegionSlug } from '../region/region-source'; // ZERO-5391
3
4
 
4
5
  /**
5
6
  * Whole-placeholder snapshot the theme editor writes for a named preview
@@ -31,7 +32,10 @@ const fetchPreviewSnapshot = async (
31
32
  if (!fn) return null;
32
33
 
33
34
  try {
34
- return (await fn({ slug })) ?? null;
35
+ // ZERO-5391: a placeholder the request's region customised previews from
36
+ // that region's own blob; without one the loader falls through to the
37
+ // region's LIVE copy (getPlaceholderWidget), never to the default's blob.
38
+ return (await fn({ slug: await resolveRegionSlug(slug) })) ?? null;
35
39
  } catch {
36
40
  // A failed preview read must fall back to the live render, never blank it.
37
41
  return null;
@@ -0,0 +1,41 @@
1
+ import { REGION_CONTENT_CAPABILITY } from './region/region-contract';
2
+
3
+ /**
4
+ * What content this build can serve differently by AUDIENCE, announced to the
5
+ * editor (ZERO-5391).
6
+ *
7
+ * The third sibling of `action-capabilities.ts` and `signal-capabilities.ts`.
8
+ * Those say what a stored action will do and which moments the renderer can
9
+ * report; this one says whether the storefront can tell its visitors apart
10
+ * when it picks the content a placeholder renders.
11
+ *
12
+ * - `region-content` a placeholder renders from its country copy
13
+ * (`<slug>-region-<cc>`; in draft mode that copy's
14
+ * `-preview-<id>` blob first) for a request whose locale
15
+ * names the country (`en-ae` → `ae`), whenever the
16
+ * `theme-regions` registry lists the placeholder for it
17
+ * (`region/region-source.ts`). The editor offers its
18
+ * country picker, and with it every write to a
19
+ * `-region-` slug, only to a storefront that says so: on
20
+ * an older theme those writes would save and render
21
+ * nothing.
22
+ *
23
+ * Names rather than a version number, for the reason the action capabilities
24
+ * are: the editor must not have to know which release added what. A build that
25
+ * does not send the list is read as serving one content to every visitor,
26
+ * which is exactly what every renderer shipped before this one did.
27
+ *
28
+ * Unlike its siblings this list does not spell its string out. The name lives
29
+ * in the region contract (`region/region-contract.ts`), whose copy in the
30
+ * editor is held byte-identical to it by a parity test on both sides, so the
31
+ * two can only be renamed together — there and never here.
32
+ *
33
+ * Its own module rather than a constant beside the reader that implements it:
34
+ * the placeholder registry announces this from the client, and it must not
35
+ * have to pull the server-only region reader in to say one list of strings.
36
+ */
37
+ export type ContentCapability = typeof REGION_CONTENT_CAPABILITY;
38
+
39
+ export const CONTENT_CAPABILITIES: readonly ContentCapability[] = [
40
+ REGION_CONTENT_CAPABILITY
41
+ ];
@@ -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
- dataSources,
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
- placeholderId: data?.slug?.toString() || slug,
508
- dataSources,
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
+ });