@alepha/react 0.14.2 → 0.14.3
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/auth/index.js +2 -2
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -4
- package/dist/core/index.js.map +1 -1
- package/dist/head/index.d.ts +17 -17
- package/dist/head/index.d.ts.map +1 -1
- package/dist/head/index.js +2 -1
- package/dist/head/index.js.map +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +2 -2
- package/dist/router/index.js.map +1 -1
- package/package.json +3 -3
- package/src/auth/__tests__/$auth.spec.ts +188 -0
- package/src/core/__tests__/Router.spec.tsx +169 -0
- package/src/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/core/hooks/useAction.ts +11 -0
- package/src/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/head/__tests__/expandSeo.spec.ts +203 -0
- package/src/head/__tests__/page-head.spec.ts +39 -0
- package/src/head/__tests__/seo-head.spec.ts +121 -0
- package/src/head/hooks/useHead.spec.tsx +288 -0
- package/src/head/index.ts +2 -1
- package/src/head/providers/BrowserHeadProvider.browser.spec.ts +271 -0
- package/src/head/providers/ServerHeadProvider.spec.ts +163 -0
- package/src/i18n/__tests__/integration.spec.tsx +239 -0
- package/src/i18n/components/Localize.spec.tsx +357 -0
- package/src/i18n/hooks/useI18n.browser.spec.tsx +438 -0
- package/src/i18n/providers/I18nProvider.spec.ts +389 -0
- package/src/router/primitives/$page.browser.spec.tsx +702 -0
- package/src/router/primitives/$page.spec.tsx +702 -0
- package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
- package/src/router/providers/ReactServerProvider.ts +4 -3
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
|
+
import { ServerHeadProvider } from "./ServerHeadProvider.ts";
|
|
4
|
+
|
|
5
|
+
const alepha = Alepha.create();
|
|
6
|
+
const serverHeadProvider = alepha.inject(ServerHeadProvider);
|
|
7
|
+
|
|
8
|
+
describe("ServerHeadProvider", () => {
|
|
9
|
+
it("should render head with custom attributes and meta tags", ({
|
|
10
|
+
expect,
|
|
11
|
+
}) => {
|
|
12
|
+
const template = `
|
|
13
|
+
<!DOCTYPE html>
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="UTF-8">
|
|
17
|
+
<title>Test</title>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const head = {
|
|
25
|
+
title: "Test Title",
|
|
26
|
+
htmlAttributes: { lang: "fr", style: "color: red;" },
|
|
27
|
+
bodyAttributes: { class: "test-class" },
|
|
28
|
+
meta: [
|
|
29
|
+
{ name: "description", content: "Test description" },
|
|
30
|
+
{ name: "keywords", content: "test, example" },
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const expectedOutput = `
|
|
35
|
+
<!DOCTYPE html>
|
|
36
|
+
<html lang="fr" style="color: red;">
|
|
37
|
+
<head>
|
|
38
|
+
<meta charset="UTF-8">
|
|
39
|
+
<title>Test Title</title>
|
|
40
|
+
<meta name="description" content="Test description">
|
|
41
|
+
<meta name="keywords" content="test, example">
|
|
42
|
+
</head>
|
|
43
|
+
<body class="test-class">
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
46
|
+
`
|
|
47
|
+
.trim()
|
|
48
|
+
.replace(/\s+/g, " ");
|
|
49
|
+
|
|
50
|
+
expect(
|
|
51
|
+
serverHeadProvider.renderHead(template, head).replace(/\s+/g, " "),
|
|
52
|
+
).toBe(expectedOutput);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should render script tags with src attribute", ({ expect }) => {
|
|
56
|
+
const template = `<!DOCTYPE html><html><head></head><body></body></html>`;
|
|
57
|
+
|
|
58
|
+
const head = {
|
|
59
|
+
script: [{ src: "https://example.com/script.js" }],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = serverHeadProvider.renderHead(template, head);
|
|
63
|
+
expect(result).toContain(
|
|
64
|
+
'<script src="https://example.com/script.js"></script>',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should render script tags with async and defer as boolean attributes", ({
|
|
69
|
+
expect,
|
|
70
|
+
}) => {
|
|
71
|
+
const template = `<!DOCTYPE html><html><head></head><body></body></html>`;
|
|
72
|
+
|
|
73
|
+
const head = {
|
|
74
|
+
script: [
|
|
75
|
+
{ src: "https://example.com/async.js", async: true } as Record<
|
|
76
|
+
string,
|
|
77
|
+
string | boolean
|
|
78
|
+
>,
|
|
79
|
+
{ src: "https://example.com/defer.js", defer: true } as Record<
|
|
80
|
+
string,
|
|
81
|
+
string | boolean
|
|
82
|
+
>,
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const result = serverHeadProvider.renderHead(template, head);
|
|
87
|
+
expect(result).toContain(
|
|
88
|
+
'<script src="https://example.com/async.js" async></script>',
|
|
89
|
+
);
|
|
90
|
+
expect(result).toContain(
|
|
91
|
+
'<script src="https://example.com/defer.js" defer></script>',
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should render script tags with type and crossorigin attributes", ({
|
|
96
|
+
expect,
|
|
97
|
+
}) => {
|
|
98
|
+
const template = `<!DOCTYPE html><html><head></head><body></body></html>`;
|
|
99
|
+
|
|
100
|
+
const head = {
|
|
101
|
+
script: [
|
|
102
|
+
{
|
|
103
|
+
src: "https://example.com/module.js",
|
|
104
|
+
type: "module",
|
|
105
|
+
crossorigin: "anonymous",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const result = serverHeadProvider.renderHead(template, head);
|
|
111
|
+
expect(result).toContain('type="module"');
|
|
112
|
+
expect(result).toContain('crossorigin="anonymous"');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should render multiple script tags", ({ expect }) => {
|
|
116
|
+
const template = `<!DOCTYPE html><html><head></head><body></body></html>`;
|
|
117
|
+
|
|
118
|
+
type ScriptAttrs = Record<string, string | boolean>;
|
|
119
|
+
const head = {
|
|
120
|
+
script: [
|
|
121
|
+
{ src: "https://example.com/first.js" } as ScriptAttrs,
|
|
122
|
+
{ src: "https://example.com/second.js", defer: true } as ScriptAttrs,
|
|
123
|
+
{ src: "https://example.com/third.js", type: "module" } as ScriptAttrs,
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const result = serverHeadProvider.renderHead(template, head);
|
|
128
|
+
expect(result).toContain(
|
|
129
|
+
'<script src="https://example.com/first.js"></script>',
|
|
130
|
+
);
|
|
131
|
+
expect(result).toContain(
|
|
132
|
+
'<script src="https://example.com/second.js" defer></script>',
|
|
133
|
+
);
|
|
134
|
+
expect(result).toContain('src="https://example.com/third.js"');
|
|
135
|
+
expect(result).toContain('type="module"');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should not render script attributes with false value", ({ expect }) => {
|
|
139
|
+
const template = `<!DOCTYPE html><html><head></head><body></body></html>`;
|
|
140
|
+
|
|
141
|
+
const head = {
|
|
142
|
+
script: [{ src: "https://example.com/script.js", defer: false }],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const result = serverHeadProvider.renderHead(template, head);
|
|
146
|
+
expect(result).toContain(
|
|
147
|
+
'<script src="https://example.com/script.js"></script>',
|
|
148
|
+
);
|
|
149
|
+
expect(result).not.toContain("defer");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should escape HTML in script attributes", ({ expect }) => {
|
|
153
|
+
const template = `<!DOCTYPE html><html><head></head><body></body></html>`;
|
|
154
|
+
|
|
155
|
+
const head = {
|
|
156
|
+
script: [{ src: 'https://example.com/script.js?a=1&b="2"' }],
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const result = serverHeadProvider.renderHead(template, head);
|
|
160
|
+
expect(result).toContain("&");
|
|
161
|
+
expect(result).toContain(""");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { describe, test } from "vitest";
|
|
3
|
+
import { $dictionary } from "../primitives/$dictionary.ts";
|
|
4
|
+
import { AlephaReactI18n } from "../index.ts";
|
|
5
|
+
import { I18nProvider } from "../providers/I18nProvider.ts";
|
|
6
|
+
|
|
7
|
+
describe("I18n Integration Tests", () => {
|
|
8
|
+
test("should lazy load dictionaries on server start", async ({ expect }) => {
|
|
9
|
+
let enLoaded = false;
|
|
10
|
+
let frLoaded = false;
|
|
11
|
+
|
|
12
|
+
class App {
|
|
13
|
+
en = $dictionary({
|
|
14
|
+
lazy: async () => {
|
|
15
|
+
enLoaded = true;
|
|
16
|
+
return { default: { hello: "Hello" } };
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
fr = $dictionary({
|
|
21
|
+
lazy: async () => {
|
|
22
|
+
frLoaded = true;
|
|
23
|
+
return { default: { hello: "Bonjour" } };
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
29
|
+
const app = alepha.inject(App);
|
|
30
|
+
|
|
31
|
+
expect(enLoaded).toBe(false);
|
|
32
|
+
expect(frLoaded).toBe(false);
|
|
33
|
+
|
|
34
|
+
await alepha.start();
|
|
35
|
+
|
|
36
|
+
expect(enLoaded).toBe(true);
|
|
37
|
+
expect(frLoaded).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("should share I18nProvider instance across application", async ({
|
|
41
|
+
expect,
|
|
42
|
+
}) => {
|
|
43
|
+
class App {
|
|
44
|
+
en = $dictionary({
|
|
45
|
+
lazy: async () => ({ default: { key: "value" } }),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
50
|
+
const app = alepha.inject(App);
|
|
51
|
+
|
|
52
|
+
const i18n1 = alepha.inject(I18nProvider);
|
|
53
|
+
const i18n2 = alepha.inject(I18nProvider);
|
|
54
|
+
|
|
55
|
+
expect(i18n1).toBe(i18n2);
|
|
56
|
+
expect(i18n1.registry).toBe(i18n2.registry);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("should support multiple dictionary registrations", async ({
|
|
60
|
+
expect,
|
|
61
|
+
}) => {
|
|
62
|
+
class App {
|
|
63
|
+
en = $dictionary({
|
|
64
|
+
lazy: async () => ({
|
|
65
|
+
default: {
|
|
66
|
+
"app.title": "My Application",
|
|
67
|
+
"app.subtitle": "Built with Alepha",
|
|
68
|
+
"auth.login": "Log In",
|
|
69
|
+
"auth.logout": "Log Out",
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
fr = $dictionary({
|
|
75
|
+
lazy: async () => ({
|
|
76
|
+
default: {
|
|
77
|
+
"app.title": "Mon Application",
|
|
78
|
+
"app.subtitle": "Construit avec Alepha",
|
|
79
|
+
"auth.login": "Se connecter",
|
|
80
|
+
"auth.logout": "Se déconnecter",
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
87
|
+
const app = alepha.inject(App);
|
|
88
|
+
const i18n = alepha.inject(I18nProvider);
|
|
89
|
+
|
|
90
|
+
await alepha.start();
|
|
91
|
+
|
|
92
|
+
// Test English
|
|
93
|
+
expect(i18n.tr("app.title")).toBe("My Application");
|
|
94
|
+
expect(i18n.tr("app.subtitle")).toBe("Built with Alepha");
|
|
95
|
+
expect(i18n.tr("auth.login")).toBe("Log In");
|
|
96
|
+
expect(i18n.tr("auth.logout")).toBe("Log Out");
|
|
97
|
+
|
|
98
|
+
// Switch to French
|
|
99
|
+
await i18n.setLang("fr");
|
|
100
|
+
|
|
101
|
+
expect(i18n.tr("app.title")).toBe("Mon Application");
|
|
102
|
+
expect(i18n.tr("app.subtitle")).toBe("Construit avec Alepha");
|
|
103
|
+
expect(i18n.tr("auth.login")).toBe("Se connecter");
|
|
104
|
+
expect(i18n.tr("auth.logout")).toBe("Se déconnecter");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("should handle translation with complex interpolation across languages", async ({
|
|
108
|
+
expect,
|
|
109
|
+
}) => {
|
|
110
|
+
class App {
|
|
111
|
+
en = $dictionary({
|
|
112
|
+
lazy: async () => ({
|
|
113
|
+
default: {
|
|
114
|
+
notification: "$1 sent you $2 message(s) about $3",
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
fr = $dictionary({
|
|
120
|
+
lazy: async () => ({
|
|
121
|
+
default: {
|
|
122
|
+
notification: "$1 vous a envoyé $2 message(s) à propos de $3",
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
es = $dictionary({
|
|
128
|
+
lazy: async () => ({
|
|
129
|
+
default: {
|
|
130
|
+
notification: "$1 te envió $2 mensaje(s) sobre $3",
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
137
|
+
const app = alepha.inject(App);
|
|
138
|
+
const i18n = alepha.inject(I18nProvider);
|
|
139
|
+
|
|
140
|
+
await alepha.start();
|
|
141
|
+
|
|
142
|
+
const args = ["John", "3", "your project"];
|
|
143
|
+
|
|
144
|
+
expect(i18n.tr("notification", { args })).toBe(
|
|
145
|
+
"John sent you 3 message(s) about your project",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await i18n.setLang("fr");
|
|
149
|
+
expect(i18n.tr("notification", { args })).toBe(
|
|
150
|
+
"John vous a envoyé 3 message(s) à propos de your project",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
await i18n.setLang("es");
|
|
154
|
+
expect(i18n.tr("notification", { args })).toBe(
|
|
155
|
+
"John te envió 3 mensaje(s) sobre your project",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("should properly cleanup on stop", async ({ expect }) => {
|
|
160
|
+
class App {
|
|
161
|
+
en = $dictionary({
|
|
162
|
+
lazy: async () => ({ default: { test: "Test" } }),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
167
|
+
const app = alepha.inject(App);
|
|
168
|
+
const i18n = alepha.inject(I18nProvider);
|
|
169
|
+
|
|
170
|
+
await alepha.start();
|
|
171
|
+
expect(i18n.registry[0].translations).toEqual({ test: "Test" });
|
|
172
|
+
|
|
173
|
+
await alepha.stop();
|
|
174
|
+
// Registry should still exist after stop
|
|
175
|
+
expect(i18n.registry).toBeDefined();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("should work with empty language fallback chain", async ({ expect }) => {
|
|
179
|
+
class App {
|
|
180
|
+
en = $dictionary({
|
|
181
|
+
lazy: async () => ({ default: { only_en: "Only in English" } }),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
fr = $dictionary({
|
|
185
|
+
lazy: async () => ({ default: { only_fr: "Seulement en français" } }),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
190
|
+
const app = alepha.inject(App);
|
|
191
|
+
const i18n = alepha.inject(I18nProvider);
|
|
192
|
+
|
|
193
|
+
await alepha.start();
|
|
194
|
+
|
|
195
|
+
// In English, can't find French-only key
|
|
196
|
+
expect(i18n.tr("only_fr")).toBe("only_fr");
|
|
197
|
+
expect(i18n.tr("only_en")).toBe("Only in English");
|
|
198
|
+
|
|
199
|
+
// In French, falls back to English for English-only key
|
|
200
|
+
await i18n.setLang("fr");
|
|
201
|
+
expect(i18n.tr("only_fr")).toBe("Seulement en français");
|
|
202
|
+
expect(i18n.tr("only_en")).toBe("Only in English");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("should handle rapid language switches", async ({ expect }) => {
|
|
206
|
+
class App {
|
|
207
|
+
en = $dictionary({
|
|
208
|
+
lazy: async () => ({ default: { status: "Active" } }),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
fr = $dictionary({
|
|
212
|
+
lazy: async () => ({ default: { status: "Actif" } }),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
de = $dictionary({
|
|
216
|
+
lazy: async () => ({ default: { status: "Aktiv" } }),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
221
|
+
const app = alepha.inject(App);
|
|
222
|
+
const i18n = alepha.inject(I18nProvider);
|
|
223
|
+
|
|
224
|
+
await alepha.start();
|
|
225
|
+
|
|
226
|
+
// Rapidly switch languages
|
|
227
|
+
await i18n.setLang("fr");
|
|
228
|
+
expect(i18n.tr("status")).toBe("Actif");
|
|
229
|
+
|
|
230
|
+
await i18n.setLang("de");
|
|
231
|
+
expect(i18n.tr("status")).toBe("Aktiv");
|
|
232
|
+
|
|
233
|
+
await i18n.setLang("en");
|
|
234
|
+
expect(i18n.tr("status")).toBe("Active");
|
|
235
|
+
|
|
236
|
+
await i18n.setLang("fr");
|
|
237
|
+
expect(i18n.tr("status")).toBe("Actif");
|
|
238
|
+
});
|
|
239
|
+
});
|