@bleedingdev/modern-js-i18n-runtime-extensions 3.8.3-ultramodern.6 → 3.9.0-ultramodern.10
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/dist/cjs/index.js +9 -2
- package/dist/cjs/localeRedirectPolicy.js +96 -0
- package/dist/cjs/localisedUrls/index.js +7 -0
- package/dist/cjs/localisedUrls/redirect.js +3 -6
- package/dist/cjs/localisedUrls/routeIdentity.js +118 -0
- package/dist/cjs/localisedUrls/routes.js +8 -0
- package/dist/cjs/paths.js +56 -0
- package/dist/cjs/router-navigation/adapter.js +235 -0
- package/dist/cjs/router-navigation/index.js +40 -0
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/localeRedirectPolicy.mjs +46 -0
- package/dist/esm/localisedUrls/index.mjs +1 -0
- package/dist/esm/localisedUrls/redirect.mjs +3 -6
- package/dist/esm/localisedUrls/routeIdentity.mjs +77 -0
- package/dist/esm/localisedUrls/routes.mjs +8 -0
- package/dist/esm/paths.mjs +12 -0
- package/dist/esm/router-navigation/adapter.mjs +197 -0
- package/dist/esm/router-navigation/index.mjs +1 -0
- package/dist/esm-node/index.mjs +1 -0
- package/dist/esm-node/localeRedirectPolicy.mjs +47 -0
- package/dist/esm-node/localisedUrls/index.mjs +1 -0
- package/dist/esm-node/localisedUrls/redirect.mjs +3 -6
- package/dist/esm-node/localisedUrls/routeIdentity.mjs +78 -0
- package/dist/esm-node/localisedUrls/routes.mjs +8 -0
- package/dist/esm-node/paths.mjs +13 -0
- package/dist/esm-node/router-navigation/adapter.mjs +198 -0
- package/dist/esm-node/router-navigation/index.mjs +2 -0
- package/dist/types/index.d.ts +4 -3
- package/dist/types/language-sync/react.d.ts +1 -1
- package/dist/types/localeRedirectPolicy.d.ts +22 -0
- package/dist/types/localisedUrls/config.d.ts +1 -1
- package/dist/types/localisedUrls/index.d.ts +10 -9
- package/dist/types/localisedUrls/pathname.d.ts +1 -1
- package/dist/types/localisedUrls/redirect.d.ts +8 -0
- package/dist/types/localisedUrls/resolve.d.ts +1 -1
- package/dist/types/localisedUrls/routeIdentity.d.ts +24 -0
- package/dist/types/localisedUrls/routes.d.ts +1 -1
- package/dist/types/paths.d.ts +9 -0
- package/dist/types/router-navigation/adapter.d.ts +71 -0
- package/dist/types/router-navigation/index.d.ts +1 -0
- package/package.json +30 -6
- package/src/index.ts +1 -0
- package/src/localeRedirectPolicy.ts +127 -0
- package/src/localisedUrls/index.ts +4 -0
- package/src/localisedUrls/redirect.ts +16 -2
- package/src/localisedUrls/routeIdentity.ts +107 -0
- package/src/localisedUrls/routes.ts +10 -0
- package/src/paths.ts +37 -0
- package/src/router-navigation/adapter.tsx +536 -0
- package/src/router-navigation/index.ts +6 -0
- package/tests/languageSyncController.test.ts +0 -77
- package/tests/localeRedirectPolicy.test.ts +173 -0
- package/tests/localisedUrls.test.ts +0 -141
- package/tests/paths.test.ts +24 -0
- package/tests/reactLanguageSync.test.tsx +20 -1
- package/tests/clientRedirectIntegration.test.tsx +0 -74
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, expect, test } from '@rstest/core';
|
|
2
|
+
import {
|
|
3
|
+
buildLocalizedUrl,
|
|
4
|
+
createLocaleRedirectResponse,
|
|
5
|
+
getLanguageFromPath,
|
|
6
|
+
isStaticResourceRequest,
|
|
7
|
+
shouldIgnoreRedirect,
|
|
8
|
+
} from '../src/localeRedirectPolicy';
|
|
9
|
+
|
|
10
|
+
const languages = ['en', 'cs'];
|
|
11
|
+
const request = (url: string) => ({
|
|
12
|
+
url,
|
|
13
|
+
header: () => ({ host: 'example.com' }),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('locale redirect resource policy', () => {
|
|
17
|
+
test('exempts default static and federation routes, including locale prefixes', () => {
|
|
18
|
+
const paths = [
|
|
19
|
+
'/backend-mf-manifest.json',
|
|
20
|
+
'/backendRemoteEntry.cjs',
|
|
21
|
+
'/mf-manifest.json',
|
|
22
|
+
'/mf-stats.json',
|
|
23
|
+
'/remoteEntry.js',
|
|
24
|
+
// A manifest may name a root entry; the federation layer serves these.
|
|
25
|
+
'/remoteEntry.catalog.js',
|
|
26
|
+
'/backendRemoteEntry.catalog-v2.cjs',
|
|
27
|
+
'/static/app.js',
|
|
28
|
+
'/upload/avatar.png',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
for (const pathname of paths) {
|
|
32
|
+
expect(isStaticResourceRequest(pathname, [], languages)).toBe(true);
|
|
33
|
+
expect(isStaticResourceRequest(`/cs${pathname}`, [], languages)).toBe(
|
|
34
|
+
true,
|
|
35
|
+
);
|
|
36
|
+
expect(shouldIgnoreRedirect(`/store${pathname}`, '/store/*')).toBe(true);
|
|
37
|
+
}
|
|
38
|
+
// Only the entry itself, not a page or asset that merely starts like one.
|
|
39
|
+
for (const pathname of [
|
|
40
|
+
'/remoteEntry.catalog.js.map',
|
|
41
|
+
'/remoteEntry.catalog/js',
|
|
42
|
+
'/products/remoteEntry.js',
|
|
43
|
+
]) {
|
|
44
|
+
expect(isStaticResourceRequest(pathname, [], languages)).toBe(false);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('configured static prefixes match complete path segments with or without a locale', () => {
|
|
49
|
+
for (const pathname of ['/assets', '/assets/app.js', '/en/assets/app.js']) {
|
|
50
|
+
expect(isStaticResourceRequest(pathname, ['/assets/'], languages)).toBe(
|
|
51
|
+
true,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
for (const pathname of [
|
|
55
|
+
'/assets-extra/app.js',
|
|
56
|
+
'/cs/assets-extra/app.js',
|
|
57
|
+
]) {
|
|
58
|
+
expect(isStaticResourceRequest(pathname, ['/assets'], languages)).toBe(
|
|
59
|
+
false,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
expect(
|
|
63
|
+
isStaticResourceRequest('/fr/assets/app.js', ['/assets'], languages),
|
|
64
|
+
).toBe(false);
|
|
65
|
+
expect(
|
|
66
|
+
isStaticResourceRequest('/remoteEntry.js/product', [], languages),
|
|
67
|
+
).toBe(false);
|
|
68
|
+
expect(isStaticResourceRequest('/static-product', [])).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('custom ignore lists operate after stripping the mount path', () => {
|
|
72
|
+
expect(
|
|
73
|
+
shouldIgnoreRedirect('/store/health/live', '/store/*', ['/health']),
|
|
74
|
+
).toBe(true);
|
|
75
|
+
expect(
|
|
76
|
+
shouldIgnoreRedirect('/store/healthy', '/store/*', ['/health']),
|
|
77
|
+
).toBe(false);
|
|
78
|
+
expect(shouldIgnoreRedirect('/store/products', '/store/*')).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('custom callbacks receive the remaining pathname and default exemptions take priority', () => {
|
|
82
|
+
const seen: string[] = [];
|
|
83
|
+
const ignore = (pathname: string) => {
|
|
84
|
+
seen.push(pathname);
|
|
85
|
+
return pathname === '/health';
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
expect(shouldIgnoreRedirect('/store/health', '/store/*', ignore)).toBe(
|
|
89
|
+
true,
|
|
90
|
+
);
|
|
91
|
+
expect(shouldIgnoreRedirect('/store/products', '/store/*', ignore)).toBe(
|
|
92
|
+
false,
|
|
93
|
+
);
|
|
94
|
+
expect(
|
|
95
|
+
shouldIgnoreRedirect('/store/static/app.js', '/store/*', ignore),
|
|
96
|
+
).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('locale redirect URL policy', () => {
|
|
101
|
+
test('detects a configured first language segment for relative or absolute URLs', () => {
|
|
102
|
+
expect(
|
|
103
|
+
getLanguageFromPath(
|
|
104
|
+
request('/store/cs/products?x=1'),
|
|
105
|
+
'/store/*',
|
|
106
|
+
languages,
|
|
107
|
+
),
|
|
108
|
+
).toBe('cs');
|
|
109
|
+
expect(
|
|
110
|
+
getLanguageFromPath(
|
|
111
|
+
request('https://example.com/en/products'),
|
|
112
|
+
'/',
|
|
113
|
+
languages,
|
|
114
|
+
),
|
|
115
|
+
).toBe('en');
|
|
116
|
+
expect(getLanguageFromPath(request('/cs/products'), '', languages)).toBe(
|
|
117
|
+
'cs',
|
|
118
|
+
);
|
|
119
|
+
expect(
|
|
120
|
+
getLanguageFromPath(request('/storecs/products'), '/store/*', languages),
|
|
121
|
+
).toBe('cs');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('switches localized dynamic paths while preserving mount, query and hash', () => {
|
|
125
|
+
expect(
|
|
126
|
+
buildLocalizedUrl(
|
|
127
|
+
request(
|
|
128
|
+
'https://example.com/store/en/products/red%20tractor?q=a%2Fb&sort=price#details',
|
|
129
|
+
),
|
|
130
|
+
'/store/*',
|
|
131
|
+
'cs',
|
|
132
|
+
languages,
|
|
133
|
+
{ '/products/:slug': { en: '/products/:slug', cs: '/produkty/:slug' } },
|
|
134
|
+
),
|
|
135
|
+
).toBe('/store/cs/produkty/red%20tractor?q=a%2Fb&sort=price#details');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('supports root and empty mounts and missing locale mapping', () => {
|
|
139
|
+
for (const mount of ['/', '', '/*']) {
|
|
140
|
+
expect(
|
|
141
|
+
buildLocalizedUrl(
|
|
142
|
+
request('https://example.com/products?x=1#top'),
|
|
143
|
+
mount,
|
|
144
|
+
'cs',
|
|
145
|
+
languages,
|
|
146
|
+
),
|
|
147
|
+
).toBe('/cs/products?x=1#top');
|
|
148
|
+
}
|
|
149
|
+
expect(
|
|
150
|
+
buildLocalizedUrl(
|
|
151
|
+
request('https://example.com/store'),
|
|
152
|
+
'/store/*',
|
|
153
|
+
'cs',
|
|
154
|
+
languages,
|
|
155
|
+
false,
|
|
156
|
+
),
|
|
157
|
+
).toBe('/store/cs');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('returns a non-cacheable temporary redirect with language negotiation headers', async () => {
|
|
161
|
+
const response = createLocaleRedirectResponse(
|
|
162
|
+
'/cs/produkty?sort=price#details',
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(response.status).toBe(302);
|
|
166
|
+
expect(response.headers.get('Location')).toBe(
|
|
167
|
+
'/cs/produkty?sort=price#details',
|
|
168
|
+
);
|
|
169
|
+
expect(response.headers.get('Cache-Control')).toBe('private, no-store');
|
|
170
|
+
expect(response.headers.get('Vary')).toBe('Accept-Language, Cookie');
|
|
171
|
+
expect(await response.text()).toBe('');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
matchPathPattern,
|
|
8
8
|
resolveCanonicalLocalisedPath,
|
|
9
9
|
resolveLocalisedPath,
|
|
10
|
-
resolveLocalisedUrlsConfig,
|
|
11
10
|
validateLocalisedUrls,
|
|
12
11
|
} from '../src/localisedUrls/index';
|
|
13
12
|
|
|
@@ -24,21 +23,6 @@ const createRoute = (
|
|
|
24
23
|
children,
|
|
25
24
|
});
|
|
26
25
|
|
|
27
|
-
describe('resolveLocalisedUrlsConfig', () => {
|
|
28
|
-
test('is opt-in: only a non-empty map enables the feature', () => {
|
|
29
|
-
const map = { '/about': { en: '/about', cs: '/o-nas' } };
|
|
30
|
-
expect(resolveLocalisedUrlsConfig(map)).toEqual({ enabled: true, map });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test('absent, boolean and empty-map options resolve to disabled', () => {
|
|
34
|
-
const disabled = { enabled: false, map: {} };
|
|
35
|
-
expect(resolveLocalisedUrlsConfig(undefined)).toEqual(disabled);
|
|
36
|
-
expect(resolveLocalisedUrlsConfig(false)).toEqual(disabled);
|
|
37
|
-
expect(resolveLocalisedUrlsConfig(true)).toEqual(disabled);
|
|
38
|
-
expect(resolveLocalisedUrlsConfig({})).toEqual(disabled);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
26
|
describe('localisedUrls', () => {
|
|
43
27
|
test('leaves framework-internal Module Federation routes unlocalised', () => {
|
|
44
28
|
const internalRoute = createRoute('_mf', [
|
|
@@ -207,9 +191,6 @@ describe('localisedUrls', () => {
|
|
|
207
191
|
localisedUrls,
|
|
208
192
|
);
|
|
209
193
|
|
|
210
|
-
expect(
|
|
211
|
-
applyLocalisedUrlsToRoutes(routes, ['en', 'cs', 'de'], localisedUrls),
|
|
212
|
-
).toEqual(localisedRoutes);
|
|
213
194
|
expect(localisedRoutes.map(route => route.path)).toEqual([
|
|
214
195
|
':lang/about',
|
|
215
196
|
':lang/o.nas',
|
|
@@ -304,62 +285,6 @@ describe('localisedUrls', () => {
|
|
|
304
285
|
]);
|
|
305
286
|
});
|
|
306
287
|
|
|
307
|
-
test('expands flat locale-prefixed route paths with canonical keys', () => {
|
|
308
|
-
const routes = [
|
|
309
|
-
createRoute('/:lang/about'),
|
|
310
|
-
createRoute('/:lang/products/:slug'),
|
|
311
|
-
];
|
|
312
|
-
|
|
313
|
-
const localisedRoutes = applyLocalisedUrlsToRoutes(routes, ['en', 'cs'], {
|
|
314
|
-
'/about': {
|
|
315
|
-
en: '/about',
|
|
316
|
-
cs: '/o-nas',
|
|
317
|
-
},
|
|
318
|
-
'/products/:slug': {
|
|
319
|
-
en: '/products/:slug',
|
|
320
|
-
cs: '/produkty/:slug',
|
|
321
|
-
},
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
expect(localisedRoutes.map(route => route.path)).toEqual([
|
|
325
|
-
':lang/about',
|
|
326
|
-
':lang/o-nas',
|
|
327
|
-
':lang/products/:slug',
|
|
328
|
-
':lang/produkty/:slug',
|
|
329
|
-
]);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test('resolves current localized path to the target language', () => {
|
|
333
|
-
const localisedUrls = {
|
|
334
|
-
'/terms-of-service': {
|
|
335
|
-
en: '/terms-of-service',
|
|
336
|
-
cs: '/podminky-pouzivani',
|
|
337
|
-
},
|
|
338
|
-
'/products/:slug': {
|
|
339
|
-
en: '/products/:slug',
|
|
340
|
-
cs: '/produkty/:slug',
|
|
341
|
-
},
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
expect(
|
|
345
|
-
resolveLocalisedPath(
|
|
346
|
-
'/terms-of-service',
|
|
347
|
-
'cs',
|
|
348
|
-
['en', 'cs'],
|
|
349
|
-
localisedUrls,
|
|
350
|
-
),
|
|
351
|
-
).toBe('/podminky-pouzivani');
|
|
352
|
-
expect(
|
|
353
|
-
resolveLocalisedPath('/produkty/cervena-bota', 'en', ['en', 'cs'], {
|
|
354
|
-
...localisedUrls,
|
|
355
|
-
'/products/:slug': {
|
|
356
|
-
en: '/products/:slug',
|
|
357
|
-
cs: '/produkty/:slug',
|
|
358
|
-
},
|
|
359
|
-
}),
|
|
360
|
-
).toBe('/products/cervena-bota');
|
|
361
|
-
});
|
|
362
|
-
|
|
363
288
|
test('resolves optional route params', () => {
|
|
364
289
|
const localisedUrls = {
|
|
365
290
|
'/products/:slug?': {
|
|
@@ -455,30 +380,6 @@ describe('localisedUrls', () => {
|
|
|
455
380
|
).toBe('/products/new');
|
|
456
381
|
});
|
|
457
382
|
|
|
458
|
-
test('reverse resolution is deterministic for equally specific matches', () => {
|
|
459
|
-
const firstOrder = {
|
|
460
|
-
'/z/:slug': {
|
|
461
|
-
en: '/x/:slug',
|
|
462
|
-
cs: '/z/:slug',
|
|
463
|
-
},
|
|
464
|
-
'/a/:section': {
|
|
465
|
-
en: '/:section/y',
|
|
466
|
-
cs: '/a/:section',
|
|
467
|
-
},
|
|
468
|
-
};
|
|
469
|
-
const reverseOrder = {
|
|
470
|
-
'/a/:section': firstOrder['/a/:section'],
|
|
471
|
-
'/z/:slug': firstOrder['/z/:slug'],
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
expect(
|
|
475
|
-
resolveCanonicalLocalisedPath('/x/y', ['en', 'cs'], firstOrder),
|
|
476
|
-
).toBe('/a/x');
|
|
477
|
-
expect(
|
|
478
|
-
resolveCanonicalLocalisedPath('/x/y', ['en', 'cs'], reverseOrder),
|
|
479
|
-
).toBe('/a/x');
|
|
480
|
-
});
|
|
481
|
-
|
|
482
383
|
test('reverse resolution prefers the most specific localised source pattern', () => {
|
|
483
384
|
const localisedUrls = {
|
|
484
385
|
'/catalog/:section/:item/:detail': {
|
|
@@ -500,23 +401,6 @@ describe('localisedUrls', () => {
|
|
|
500
401
|
).toBe('/shoes/red');
|
|
501
402
|
});
|
|
502
403
|
|
|
503
|
-
test('a specific localised source outranks a broad canonical source', () => {
|
|
504
|
-
const localisedUrls = {
|
|
505
|
-
'/:section/:slug': {
|
|
506
|
-
en: '/generic/:section/:slug',
|
|
507
|
-
cs: '/obecne/:section/:slug',
|
|
508
|
-
},
|
|
509
|
-
'/products/:id': {
|
|
510
|
-
en: '/fixed/:id',
|
|
511
|
-
cs: '/produkty/:id',
|
|
512
|
-
},
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
expect(
|
|
516
|
-
resolveCanonicalLocalisedPath('/fixed/red', ['en', 'cs'], localisedUrls),
|
|
517
|
-
).toBe('/products/red');
|
|
518
|
-
});
|
|
519
|
-
|
|
520
404
|
test('missing required target params never collapse into empty segments', () => {
|
|
521
405
|
expect(() =>
|
|
522
406
|
resolveLocalisedPath('/products/red-shoe', 'cs', ['en', 'cs'], {
|
|
@@ -537,31 +421,6 @@ describe('localisedUrls', () => {
|
|
|
537
421
|
).toThrow('Missing required path parameter "toString"');
|
|
538
422
|
});
|
|
539
423
|
|
|
540
|
-
test('localises and canonicalises full target pathnames through one helper', () => {
|
|
541
|
-
const localisedUrls = {
|
|
542
|
-
'/products/:slug': {
|
|
543
|
-
en: '/products/:slug',
|
|
544
|
-
cs: '/produkty/:slug',
|
|
545
|
-
},
|
|
546
|
-
};
|
|
547
|
-
|
|
548
|
-
expect(
|
|
549
|
-
localiseTargetPathname(
|
|
550
|
-
'/en/products/cervena-bota',
|
|
551
|
-
'cs',
|
|
552
|
-
['en', 'cs'],
|
|
553
|
-
localisedUrls,
|
|
554
|
-
),
|
|
555
|
-
).toBe('/cs/produkty/cervena-bota');
|
|
556
|
-
expect(
|
|
557
|
-
canonicalTargetPathname(
|
|
558
|
-
'/cs/produkty/cervena-bota',
|
|
559
|
-
['en', 'cs'],
|
|
560
|
-
localisedUrls,
|
|
561
|
-
),
|
|
562
|
-
).toBe('/products/cervena-bota');
|
|
563
|
-
});
|
|
564
|
-
|
|
565
424
|
test('strips case-insensitive locale prefixes before relocalising full pathnames', () => {
|
|
566
425
|
const localisedUrls = {
|
|
567
426
|
'/products/:slug': {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, test } from '@rstest/core';
|
|
2
|
+
import { canonicalPath, localizePath } from '../src/paths';
|
|
3
|
+
|
|
4
|
+
const config = {
|
|
5
|
+
languages: ['en', 'cs'],
|
|
6
|
+
localisedUrls: {
|
|
7
|
+
'/products/:slug': { en: '/products/:slug', cs: '/produkty/:slug' },
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe('@modern-js/i18n-runtime-extensions/paths', () => {
|
|
12
|
+
test('localises the pathname while leaving query and hash verbatim', () => {
|
|
13
|
+
expect(
|
|
14
|
+
localizePath(
|
|
15
|
+
'/products/red-shoe?tag=boots&tag=sale#details',
|
|
16
|
+
'cs',
|
|
17
|
+
config,
|
|
18
|
+
),
|
|
19
|
+
).toBe('/cs/produkty/red-shoe?tag=boots&tag=sale#details');
|
|
20
|
+
expect(
|
|
21
|
+
canonicalPath('/CS/produkty/red-shoe?tag=boots#details', config),
|
|
22
|
+
).toBe('/products/red-shoe?tag=boots#details');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -7,6 +7,14 @@ interface Target {
|
|
|
7
7
|
language: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
rstest.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
rstest.unstubAllGlobals();
|
|
16
|
+
});
|
|
17
|
+
|
|
10
18
|
test('an abandoned suspended render cannot publish an uncommitted target', async () => {
|
|
11
19
|
let resolveFirst!: () => void;
|
|
12
20
|
const firstChange = new Promise<void>(resolve => {
|
|
@@ -14,6 +22,7 @@ test('an abandoned suspended render cannot publish an uncommitted target', async
|
|
|
14
22
|
});
|
|
15
23
|
const changes: string[] = [];
|
|
16
24
|
const commits: string[] = [];
|
|
25
|
+
let suspended = false;
|
|
17
26
|
const neverSettles = new Promise<never>(() => undefined);
|
|
18
27
|
const first = { id: 'first', language: 'en' };
|
|
19
28
|
const speculative = { id: 'speculative', language: 'en' };
|
|
@@ -41,6 +50,7 @@ test('an abandoned suspended render cannot publish an uncommitted target', async
|
|
|
41
50
|
},
|
|
42
51
|
});
|
|
43
52
|
if (suspend) {
|
|
53
|
+
suspended = true;
|
|
44
54
|
throw neverSettles;
|
|
45
55
|
}
|
|
46
56
|
return <main>{target.id}</main>;
|
|
@@ -49,7 +59,11 @@ test('an abandoned suspended render cannot publish an uncommitted target', async
|
|
|
49
59
|
const container = document.createElement('div');
|
|
50
60
|
const root = createRoot(container);
|
|
51
61
|
await act(async () => {
|
|
52
|
-
root.render(
|
|
62
|
+
root.render(
|
|
63
|
+
<Suspense fallback={<p>loading</p>}>
|
|
64
|
+
<Harness target={first} />
|
|
65
|
+
</Suspense>,
|
|
66
|
+
);
|
|
53
67
|
});
|
|
54
68
|
expect(changes).toEqual(['first']);
|
|
55
69
|
|
|
@@ -64,6 +78,11 @@ test('an abandoned suspended render cannot publish an uncommitted target', async
|
|
|
64
78
|
await Promise.resolve();
|
|
65
79
|
});
|
|
66
80
|
|
|
81
|
+
expect(suspended).toBe(true);
|
|
82
|
+
expect(container.textContent).toBe('first');
|
|
83
|
+
expect(changes).toEqual(['first']);
|
|
84
|
+
expect(commits).toEqual([]);
|
|
85
|
+
|
|
67
86
|
await act(async () => {
|
|
68
87
|
resolveFirst();
|
|
69
88
|
await firstChange;
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import React, { act } from 'react';
|
|
2
|
-
import { createRoot } from 'react-dom/client';
|
|
3
|
-
import { useClientSideRedirect } from '../../plugin-i18n/src/runtime/hooks';
|
|
4
|
-
|
|
5
|
-
const navigate = rstest.fn(async () => undefined);
|
|
6
|
-
let location = {
|
|
7
|
-
pathname: '/cs/about',
|
|
8
|
-
search: '?from=test',
|
|
9
|
-
hash: '#section',
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
rstest.mock('../../plugin-i18n/src/runtime/routerAdapter', () => ({
|
|
13
|
-
useI18nRouterAdapter: () => ({
|
|
14
|
-
hasRouter: true,
|
|
15
|
-
location,
|
|
16
|
-
navigate,
|
|
17
|
-
}),
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
test('canonicalizes successive prefixed routes in one mounted provider', async () => {
|
|
21
|
-
const previousTarget = process.env.MODERN_TARGET;
|
|
22
|
-
process.env.MODERN_TARGET = 'browser';
|
|
23
|
-
const instance = {
|
|
24
|
-
language: 'cs',
|
|
25
|
-
isInitialized: true,
|
|
26
|
-
} as Parameters<typeof useClientSideRedirect>[0];
|
|
27
|
-
const localisedUrls = {
|
|
28
|
-
'/about': { en: '/about', cs: '/o-nas' },
|
|
29
|
-
'/terms-of-service': {
|
|
30
|
-
en: '/terms-of-service',
|
|
31
|
-
cs: '/podminky-pouzivani',
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
const Harness = () => {
|
|
35
|
-
useClientSideRedirect(
|
|
36
|
-
instance,
|
|
37
|
-
true,
|
|
38
|
-
['en', 'cs'],
|
|
39
|
-
'en',
|
|
40
|
-
undefined,
|
|
41
|
-
localisedUrls,
|
|
42
|
-
);
|
|
43
|
-
return null;
|
|
44
|
-
};
|
|
45
|
-
const container = document.createElement('div');
|
|
46
|
-
const root = createRoot(container);
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
window.history.replaceState(null, '', '/cs/about?from=test#section');
|
|
50
|
-
await act(async () => root.render(<Harness />));
|
|
51
|
-
expect(navigate).toHaveBeenLastCalledWith('/cs/o-nas?from=test#section', {
|
|
52
|
-
replace: true,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
location = {
|
|
56
|
-
pathname: '/cs/terms-of-service',
|
|
57
|
-
search: '',
|
|
58
|
-
hash: '',
|
|
59
|
-
};
|
|
60
|
-
window.history.replaceState(null, '', '/cs/terms-of-service');
|
|
61
|
-
await act(async () => root.render(<Harness />));
|
|
62
|
-
expect(navigate).toHaveBeenLastCalledWith('/cs/podminky-pouzivani', {
|
|
63
|
-
replace: true,
|
|
64
|
-
});
|
|
65
|
-
expect(navigate).toHaveBeenCalledTimes(2);
|
|
66
|
-
} finally {
|
|
67
|
-
await act(async () => root.unmount());
|
|
68
|
-
if (previousTarget === undefined) {
|
|
69
|
-
delete process.env.MODERN_TARGET;
|
|
70
|
-
} else {
|
|
71
|
-
process.env.MODERN_TARGET = previousTarget;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
});
|