@carlonicora/nextjs-jsonapi 4.2.3 → 5.0.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/dist/{BlockNoteEditor-NTKPWYWW.mjs → BlockNoteEditor-7BKKGFH3.mjs} +2 -2
- package/dist/{BlockNoteEditor-J533UMWG.js → BlockNoteEditor-E6HJD4HB.js} +9 -9
- package/dist/{BlockNoteEditor-J533UMWG.js.map → BlockNoteEditor-E6HJD4HB.js.map} +1 -1
- package/dist/billing/index.js +371 -371
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-NCWCC56Q.mjs → chunk-MELHUPVN.mjs} +28 -13
- package/dist/chunk-MELHUPVN.mjs.map +1 -0
- package/dist/{chunk-4EGA5UP5.js → chunk-TPGXNPGY.js} +202 -187
- package/dist/chunk-TPGXNPGY.js.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/index.js +2 -2
- package/dist/components/index.mjs +1 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/features/analytics/index.js +88 -88
- package/dist/features/analytics/index.mjs +1 -1
- package/dist/features/help/index.js +31 -31
- package/dist/features/help/index.mjs +1 -1
- package/dist/features/tokenusage/index.js +75 -75
- package/dist/features/tokenusage/index.mjs +1 -1
- package/dist/next-config/index.d.mts +0 -1
- package/dist/next-config/index.d.ts +0 -1
- package/dist/next-config/index.js +0 -6
- package/dist/next-config/index.js.map +1 -1
- package/dist/next-config/index.mjs +0 -6
- package/dist/next-config/index.mjs.map +1 -1
- package/package.json +4 -2
- package/pwa/offline.template.html +138 -0
- package/pwa/sw.js +175 -0
- package/src/features/notification/components/notifications/PushNotificationProvider.tsx +5 -1
- package/src/features/notification/components/notifications/__tests__/PushNotificationProvider.spec.tsx +51 -0
- package/src/hooks/__tests__/usePushNotifications.spec.tsx +87 -34
- package/src/hooks/usePushNotifications.ts +42 -18
- package/src/next-config/__tests__/createNextConfig.spec.ts +7 -5
- package/src/next-config/index.ts +1 -8
- package/src/pwa/__tests__/offline.template.spec.ts +246 -0
- package/src/pwa/__tests__/sw.spec.ts +309 -0
- package/dist/chunk-4EGA5UP5.js.map +0 -1
- package/dist/chunk-NCWCC56Q.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-NTKPWYWW.mjs.map → BlockNoteEditor-7BKKGFH3.mjs.map} +0 -0
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect } from "react";
|
|
4
4
|
import { ENV } from "../core/env";
|
|
5
|
-
import { useCurrentUserContext } from "../contexts";
|
|
6
5
|
import { UserInterface } from "../features/user/data";
|
|
7
6
|
import { PushService } from "../features/push/data/push.service";
|
|
8
|
-
import { getRoleId } from "../roles";
|
|
9
|
-
import { getAppUrl } from "../client/config";
|
|
10
7
|
|
|
11
8
|
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|
12
9
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
|
@@ -19,23 +16,22 @@ function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|
|
19
16
|
return outputArray;
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
export default function usePushNotifications(
|
|
23
|
-
|
|
19
|
+
export default function usePushNotifications(params: {
|
|
20
|
+
currentUser: UserInterface | undefined;
|
|
21
|
+
canSubscribe: boolean;
|
|
22
|
+
}): void {
|
|
23
|
+
const { currentUser, canSubscribe } = params;
|
|
24
|
+
const userId = currentUser?.id;
|
|
24
25
|
|
|
25
26
|
useEffect(() => {
|
|
27
|
+
if (!userId || !canSubscribe) return;
|
|
28
|
+
|
|
26
29
|
const register = async () => {
|
|
27
30
|
if ("serviceWorker" in navigator && "PushManager" in window) {
|
|
28
31
|
try {
|
|
29
|
-
|
|
30
|
-
const sessionKey = `push_registered_${currentUser?.id}`;
|
|
31
|
-
const lastRegisteredSubscription = sessionStorage.getItem(sessionKey);
|
|
32
|
+
const sessionKey = `push_registered_${userId}`;
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
// so it can't cache stale Turbopack chunks the way the prod worker
|
|
35
|
-
// (sw.ts) would. Prod uses the full worker. Both live at scope "/", so
|
|
36
|
-
// the push subscription is identical either way.
|
|
37
|
-
const swUrl = ENV.IS_PRODUCTION ? `${getAppUrl()}/sw.js` : `${getAppUrl()}/sw-dev.js`;
|
|
38
|
-
const registration = await navigator.serviceWorker.register(swUrl);
|
|
34
|
+
const registration = await navigator.serviceWorker.register("/sw.js", { updateViaCache: "none" });
|
|
39
35
|
|
|
40
36
|
// Check current permission status first
|
|
41
37
|
let permission = Notification.permission;
|
|
@@ -75,8 +71,9 @@ export default function usePushNotifications(): void {
|
|
|
75
71
|
// Create a simple hash to detect subscription changes
|
|
76
72
|
const subscriptionHash = btoa(JSON.stringify(plainSubscription));
|
|
77
73
|
|
|
78
|
-
//
|
|
79
|
-
|
|
74
|
+
// Read the stored hash right before posting, not at the top: a rotation
|
|
75
|
+
// that lands while this registration awaits must not be posted twice.
|
|
76
|
+
if (sessionStorage.getItem(sessionKey) !== subscriptionHash) {
|
|
80
77
|
await PushService.register({ data: plainSubscription });
|
|
81
78
|
// Store the current subscription hash to avoid duplicate registrations
|
|
82
79
|
sessionStorage.setItem(sessionKey, subscriptionHash);
|
|
@@ -87,6 +84,33 @@ export default function usePushNotifications(): void {
|
|
|
87
84
|
}
|
|
88
85
|
};
|
|
89
86
|
|
|
90
|
-
|
|
91
|
-
}, [
|
|
87
|
+
register();
|
|
88
|
+
}, [userId, canSubscribe]);
|
|
89
|
+
|
|
90
|
+
// When the worker re-subscribes after the push service rotates the subscription
|
|
91
|
+
// (pushsubscriptionchange), it cannot call the authenticated API itself: it posts
|
|
92
|
+
// the new subscription to one window client, and this tab re-registers it under
|
|
93
|
+
// the user's auth, with the same hash guard as the registration above.
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!userId || !canSubscribe) return;
|
|
96
|
+
if (!("serviceWorker" in navigator)) return;
|
|
97
|
+
|
|
98
|
+
const sessionKey = `push_registered_${userId}`;
|
|
99
|
+
|
|
100
|
+
const onMessage = (event: MessageEvent) => {
|
|
101
|
+
if (event.data?.type !== "PUSH_RESUBSCRIBED" || !event.data.subscription) return;
|
|
102
|
+
|
|
103
|
+
const subscription = event.data.subscription;
|
|
104
|
+
const subscriptionHash = btoa(JSON.stringify(subscription));
|
|
105
|
+
if (sessionStorage.getItem(sessionKey) === subscriptionHash) return;
|
|
106
|
+
|
|
107
|
+
void PushService.register({ data: subscription })
|
|
108
|
+
.then(() => sessionStorage.setItem(sessionKey, subscriptionHash))
|
|
109
|
+
.catch((error) => console.error("Error re-registering rotated push subscription:", error));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const container = navigator.serviceWorker;
|
|
113
|
+
container.addEventListener("message", onMessage);
|
|
114
|
+
return () => container.removeEventListener("message", onMessage);
|
|
115
|
+
}, [userId, canSubscribe]);
|
|
92
116
|
}
|
|
@@ -147,12 +147,14 @@ describe("createNextConfig: behaviour", () => {
|
|
|
147
147
|
dangerouslyAllowLocalIP: true,
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
const full = createNextConfig({
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
const full = createNextConfig({
|
|
151
|
+
...base,
|
|
152
|
+
mdx: true,
|
|
153
|
+
serwist: true,
|
|
154
|
+
imageMinimumCacheTTL: 86400,
|
|
155
|
+
} as never) as Record<string, unknown>;
|
|
154
156
|
expect(full.pageExtensions).toEqual(["ts", "tsx", "mdx"]);
|
|
155
|
-
expect(full
|
|
157
|
+
expect(full).not.toHaveProperty("serverExternalPackages");
|
|
156
158
|
expect((full.images as { minimumCacheTTL: number }).minimumCacheTTL).toBe(86400);
|
|
157
159
|
});
|
|
158
160
|
|
package/src/next-config/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// next.config.js is loaded at RUNTIME by `next start`, and the production image
|
|
5
5
|
// installs with --prod. This entry may therefore import only Node built-ins at
|
|
6
|
-
// runtime: never next-intl, @next/mdx
|
|
6
|
+
// runtime: never next-intl, @next/mdx or any devDependency. `next` is
|
|
7
7
|
// imported for its types only.
|
|
8
8
|
import type { NextConfig } from "next";
|
|
9
9
|
import { buildCsp, listAppOrigins, type CspDirective, type CspService } from "./csp";
|
|
@@ -22,7 +22,6 @@ export interface NjaNextConfigOptions {
|
|
|
22
22
|
imageMinimumCacheTTL?: number;
|
|
23
23
|
permissionsPolicy?: { camera?: "none" | "self" };
|
|
24
24
|
mdx?: boolean;
|
|
25
|
-
serwist?: boolean;
|
|
26
25
|
securityHeaders?: boolean;
|
|
27
26
|
authInterrupts?: boolean;
|
|
28
27
|
reactCompiler?: boolean;
|
|
@@ -99,12 +98,6 @@ export function createNextConfig(options: NjaNextConfigOptions): NextConfig {
|
|
|
99
98
|
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY ?? "",
|
|
100
99
|
...options.env,
|
|
101
100
|
},
|
|
102
|
-
// Inlined replacement for @serwist/turbopack's withSerwist wrapper, which
|
|
103
|
-
// does nothing but append these two entries. Requiring the package here
|
|
104
|
-
// instead CRASHES production: it is a devDependency, next.config.js is loaded
|
|
105
|
-
// at RUNTIME by `next start`, and the web-production image installs with
|
|
106
|
-
// --prod. Do not re-add a require of any devDependency to this file.
|
|
107
|
-
...(options.serwist ? { serverExternalPackages: ["esbuild", "esbuild-wasm"] } : {}),
|
|
108
101
|
// Next 16 builds AND serves dev with Turbopack, so this is what pins yjs to a
|
|
109
102
|
// single instance in both modes.
|
|
110
103
|
turbopack: {
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { runInNewContext } from "node:vm";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
const TEMPLATE_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "pwa", "offline.template.html");
|
|
8
|
+
|
|
9
|
+
const PLACEHOLDERS = [
|
|
10
|
+
"{{APP_NAME}}",
|
|
11
|
+
"/*{{THEME_CSS}}*/",
|
|
12
|
+
"{{TITLE}}",
|
|
13
|
+
"{{DESCRIPTION}}",
|
|
14
|
+
"{{RETRY}}",
|
|
15
|
+
"{{OFFLINE_CONFIG}}",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
type OfflineStrings = { title: string; description: string; retry: string };
|
|
19
|
+
|
|
20
|
+
type OfflineConfig = {
|
|
21
|
+
defaultTheme: string;
|
|
22
|
+
enableSystem: boolean;
|
|
23
|
+
themes: string[];
|
|
24
|
+
defaultLocale: string;
|
|
25
|
+
locales: string[];
|
|
26
|
+
rtl: string[];
|
|
27
|
+
strings: Record<string, OfflineStrings>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const STRINGS: Record<string, OfflineStrings> = {
|
|
31
|
+
en: { title: "You're Offline", description: "Check your connection.", retry: "Try Again" },
|
|
32
|
+
it: { title: "Sei offline", description: "Controlla la connessione.", retry: "Riprova" },
|
|
33
|
+
ar: { title: "أنت غير متصل", description: "تحقق من اتصالك.", retry: "أعد المحاولة" },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function baseConfig(overrides: Partial<OfflineConfig> = {}): OfflineConfig {
|
|
37
|
+
return {
|
|
38
|
+
defaultTheme: "light",
|
|
39
|
+
enableSystem: true,
|
|
40
|
+
themes: ["dark", "cozy"],
|
|
41
|
+
defaultLocale: "en",
|
|
42
|
+
locales: ["en", "it", "ar"],
|
|
43
|
+
rtl: ["ar"],
|
|
44
|
+
strings: STRINGS,
|
|
45
|
+
...overrides,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readTemplate(): string {
|
|
50
|
+
return readFileSync(TEMPLATE_PATH, "utf8");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function countOccurrences(haystack: string, needle: string): number {
|
|
54
|
+
return haystack.split(needle).length - 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fill(config: OfflineConfig): string {
|
|
58
|
+
return readTemplate()
|
|
59
|
+
.split("{{APP_NAME}}")
|
|
60
|
+
.join("Test App")
|
|
61
|
+
.split("/*{{THEME_CSS}}*/")
|
|
62
|
+
.join(":root { --background: white; } .dark { --background: black; }")
|
|
63
|
+
.split("{{TITLE}}")
|
|
64
|
+
.join(STRINGS.en.title)
|
|
65
|
+
.split("{{DESCRIPTION}}")
|
|
66
|
+
.join(STRINGS.en.description)
|
|
67
|
+
.split("{{RETRY}}")
|
|
68
|
+
.join(STRINGS.en.retry)
|
|
69
|
+
.split("{{OFFLINE_CONFIG}}")
|
|
70
|
+
.join(JSON.stringify(config));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type Env = {
|
|
74
|
+
stored?: string | null;
|
|
75
|
+
storageThrows?: boolean;
|
|
76
|
+
languages?: string[];
|
|
77
|
+
pathname?: string;
|
|
78
|
+
cookie?: string;
|
|
79
|
+
prefersDark?: boolean;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type RunResult = {
|
|
83
|
+
lang: string;
|
|
84
|
+
dir: string;
|
|
85
|
+
classes: string[];
|
|
86
|
+
text: Record<"title" | "description" | "retry", string>;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function runScript(html: string, env: Env = {}): RunResult {
|
|
90
|
+
const scripts = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)];
|
|
91
|
+
expect(scripts.length).toBe(1);
|
|
92
|
+
const body = scripts[0][1];
|
|
93
|
+
|
|
94
|
+
// Host-realm containers: the sandbox only writes primitives into them, so
|
|
95
|
+
// assertions never compare arrays created inside the vm context.
|
|
96
|
+
const classes: string[] = [];
|
|
97
|
+
const elements: Record<"title" | "description" | "retry", { textContent: string }> = {
|
|
98
|
+
title: { textContent: STRINGS.en.title },
|
|
99
|
+
description: { textContent: STRINGS.en.description },
|
|
100
|
+
retry: { textContent: STRINGS.en.retry },
|
|
101
|
+
};
|
|
102
|
+
const documentElement = {
|
|
103
|
+
lang: "en",
|
|
104
|
+
dir: "",
|
|
105
|
+
className: "",
|
|
106
|
+
classList: {
|
|
107
|
+
add: (name: string) => {
|
|
108
|
+
classes.push(String(name));
|
|
109
|
+
documentElement.className = classes.join(" ");
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const sandbox: Record<string, unknown> = {
|
|
115
|
+
document: {
|
|
116
|
+
documentElement,
|
|
117
|
+
cookie: env.cookie ?? "",
|
|
118
|
+
getElementById: (id: string) => elements[id as keyof typeof elements] ?? null,
|
|
119
|
+
},
|
|
120
|
+
localStorage: {
|
|
121
|
+
getItem: (_key: string) => {
|
|
122
|
+
if (env.storageThrows) throw new Error("SecurityError");
|
|
123
|
+
return env.stored ?? null;
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
navigator: { languages: env.languages ?? [] },
|
|
127
|
+
location: { pathname: env.pathname ?? "/" },
|
|
128
|
+
matchMedia: () => ({ matches: env.prefersDark ?? false }),
|
|
129
|
+
};
|
|
130
|
+
sandbox.window = sandbox;
|
|
131
|
+
|
|
132
|
+
runInNewContext(body, sandbox);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
lang: String(documentElement.lang),
|
|
136
|
+
dir: String(documentElement.dir),
|
|
137
|
+
classes: classes.slice(),
|
|
138
|
+
text: {
|
|
139
|
+
title: String(elements.title.textContent),
|
|
140
|
+
description: String(elements.description.textContent),
|
|
141
|
+
retry: String(elements.retry.textContent),
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe("pwa/offline.template.html", () => {
|
|
147
|
+
it("contains no /_next/ reference, external script, stylesheet or font", () => {
|
|
148
|
+
const raw = readTemplate();
|
|
149
|
+
expect(raw).not.toContain("/_next/");
|
|
150
|
+
expect(raw).not.toMatch(/<script[^>]+src=/);
|
|
151
|
+
expect(raw).not.toMatch(/<link[^>]+href=/);
|
|
152
|
+
expect(raw).not.toContain("url(http");
|
|
153
|
+
expect(raw).not.toContain("@import");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("carries every placeholder exactly once", () => {
|
|
157
|
+
const raw = readTemplate();
|
|
158
|
+
for (const placeholder of PLACEHOLDERS) {
|
|
159
|
+
expect(countOccurrences(raw, placeholder), placeholder).toBe(1);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("applies a stored theme as a class", () => {
|
|
164
|
+
const result = runScript(fill(baseConfig()), { stored: "dark" });
|
|
165
|
+
expect(result.classes).toContain("dark");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("uses the default theme when nothing is stored", () => {
|
|
169
|
+
const light = runScript(fill(baseConfig()), { stored: null });
|
|
170
|
+
expect(light.classes.length).toBe(0);
|
|
171
|
+
|
|
172
|
+
const cozy = runScript(fill(baseConfig({ defaultTheme: "cozy" })), { stored: null });
|
|
173
|
+
expect(cozy.classes).toContain("cozy");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("follows the OS only for a stored system value with enableSystem", () => {
|
|
177
|
+
const followsOs = runScript(fill(baseConfig()), { stored: "system", prefersDark: true });
|
|
178
|
+
expect(followsOs.classes).toContain("dark");
|
|
179
|
+
|
|
180
|
+
const noSystem = runScript(fill(baseConfig({ enableSystem: false })), { stored: "system", prefersDark: true });
|
|
181
|
+
expect(noSystem.classes.length).toBe(0);
|
|
182
|
+
|
|
183
|
+
const noSystemCozy = runScript(fill(baseConfig({ enableSystem: false, defaultTheme: "cozy" })), {
|
|
184
|
+
stored: "system",
|
|
185
|
+
prefersDark: true,
|
|
186
|
+
});
|
|
187
|
+
expect(noSystemCozy.classes.join(" ")).toBe("cozy");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("falls back to the default theme when localStorage throws", () => {
|
|
191
|
+
const result = runScript(fill(baseConfig({ defaultTheme: "cozy" })), {
|
|
192
|
+
storageThrows: true,
|
|
193
|
+
pathname: "/it/sessions",
|
|
194
|
+
});
|
|
195
|
+
expect(result.classes.join(" ")).toBe("cozy");
|
|
196
|
+
expect(result.lang).toBe("it");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("picks the locale from the URL prefix first", () => {
|
|
200
|
+
const result = runScript(fill(baseConfig()), {
|
|
201
|
+
pathname: "/it/sessions",
|
|
202
|
+
cookie: "NEXT_LOCALE=en",
|
|
203
|
+
languages: ["en-US"],
|
|
204
|
+
});
|
|
205
|
+
expect(result.lang).toBe("it");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("then from the NEXT_LOCALE cookie", () => {
|
|
209
|
+
const result = runScript(fill(baseConfig()), {
|
|
210
|
+
pathname: "/sessions",
|
|
211
|
+
cookie: "NEXT_LOCALE=it",
|
|
212
|
+
languages: ["en-US"],
|
|
213
|
+
});
|
|
214
|
+
expect(result.lang).toBe("it");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("then from navigator.languages", () => {
|
|
218
|
+
const result = runScript(fill(baseConfig()), { pathname: "/sessions", languages: ["it-IT", "en"] });
|
|
219
|
+
expect(result.lang).toBe("it");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("skips locales the app does not ship", () => {
|
|
223
|
+
const result = runScript(fill(baseConfig()), {
|
|
224
|
+
pathname: "/fr/x",
|
|
225
|
+
cookie: "NEXT_LOCALE=de",
|
|
226
|
+
languages: ["pt-BR"],
|
|
227
|
+
});
|
|
228
|
+
expect(result.lang).toBe("en");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("sets dir=rtl for an RTL locale", () => {
|
|
232
|
+
const arabic = runScript(fill(baseConfig()), { pathname: "/ar/x" });
|
|
233
|
+
expect(arabic.lang).toBe("ar");
|
|
234
|
+
expect(arabic.dir).toBe("rtl");
|
|
235
|
+
|
|
236
|
+
const italian = runScript(fill(baseConfig()), { pathname: "/it/x" });
|
|
237
|
+
expect(italian.dir).toBe("ltr");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("replaces the three strings with the chosen locale's", () => {
|
|
241
|
+
const result = runScript(fill(baseConfig()), { pathname: "/it/x" });
|
|
242
|
+
expect(result.text.title).toBe(STRINGS.it.title);
|
|
243
|
+
expect(result.text.description).toBe(STRINGS.it.description);
|
|
244
|
+
expect(result.text.retry).toBe(STRINGS.it.retry);
|
|
245
|
+
});
|
|
246
|
+
});
|