@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,366 @@
|
|
|
1
|
+
import { AlephaContext } from "@alepha/react";
|
|
2
|
+
import { fireEvent, render } from "@testing-library/react";
|
|
3
|
+
import { Alepha, t } from "alepha";
|
|
4
|
+
import { AlephaLogger } from "alepha/logger";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import { describe, it } from "vitest";
|
|
7
|
+
import { useForm } from "../index.ts";
|
|
8
|
+
|
|
9
|
+
describe("useForm", () => {
|
|
10
|
+
const renderWithAlepha = (alepha: Alepha, element: ReactNode) => {
|
|
11
|
+
return render(
|
|
12
|
+
<AlephaContext.Provider value={alepha}>{element}</AlephaContext.Provider>,
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
it("should run handler on submit", async ({ expect }) => {
|
|
17
|
+
const alepha = Alepha.create().with(AlephaLogger);
|
|
18
|
+
const calls: Array<any> = [];
|
|
19
|
+
const Form = () => {
|
|
20
|
+
const form = useForm({
|
|
21
|
+
id: "test",
|
|
22
|
+
schema: t.object({
|
|
23
|
+
str: t.text(),
|
|
24
|
+
int: t.integer(),
|
|
25
|
+
nested: t.object({
|
|
26
|
+
str: t.text(),
|
|
27
|
+
another: t.object({
|
|
28
|
+
level: t.text(),
|
|
29
|
+
}),
|
|
30
|
+
}),
|
|
31
|
+
}),
|
|
32
|
+
handler: (values, args) => {
|
|
33
|
+
calls.push(values);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<form {...form.props} data-testid="test-form">
|
|
39
|
+
<input {...form.input.str.props} />
|
|
40
|
+
<input {...form.input.int.props} />
|
|
41
|
+
<input {...form.input.nested.items.str.props} />
|
|
42
|
+
<input {...form.input.nested.items.another.items.level.props} />
|
|
43
|
+
<button type="submit">Submit</button>
|
|
44
|
+
</form>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
await alepha.start();
|
|
49
|
+
|
|
50
|
+
const ui = renderWithAlepha(alepha, <Form />);
|
|
51
|
+
|
|
52
|
+
fireEvent.change(ui.getByTestId("test-str"), {
|
|
53
|
+
target: { value: "testuser" },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
fireEvent.change(ui.getByTestId("test-int"), {
|
|
57
|
+
target: { value: "123" },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
fireEvent.change(ui.getByTestId("test-nested.str"), {
|
|
61
|
+
target: { value: "nestedvalue" },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
fireEvent.change(ui.getByTestId("test-nested.another.level"), {
|
|
65
|
+
target: { value: "anothervalue" },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
fireEvent.submit(ui.getByText("Submit"));
|
|
69
|
+
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
71
|
+
|
|
72
|
+
expect(calls[0]).toEqual({
|
|
73
|
+
str: "testuser",
|
|
74
|
+
int: 123,
|
|
75
|
+
nested: {
|
|
76
|
+
str: "nestedvalue",
|
|
77
|
+
another: {
|
|
78
|
+
level: "anothervalue",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should provide correct InputField types for nested objects", async ({
|
|
85
|
+
expect,
|
|
86
|
+
}) => {
|
|
87
|
+
const alepha = Alepha.create().with(AlephaLogger);
|
|
88
|
+
|
|
89
|
+
const Form = () => {
|
|
90
|
+
const form = useForm({
|
|
91
|
+
id: "types-test",
|
|
92
|
+
schema: t.object({
|
|
93
|
+
name: t.text(),
|
|
94
|
+
address: t.object({
|
|
95
|
+
street: t.text(),
|
|
96
|
+
city: t.text(),
|
|
97
|
+
country: t.object({
|
|
98
|
+
code: t.text(),
|
|
99
|
+
name: t.text(),
|
|
100
|
+
}),
|
|
101
|
+
}),
|
|
102
|
+
}),
|
|
103
|
+
handler: () => {},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Verify nested object InputFields have items property
|
|
107
|
+
const addressInput = form.input.address;
|
|
108
|
+
const streetInput = addressInput.items.street;
|
|
109
|
+
const countryInput = addressInput.items.country;
|
|
110
|
+
const countryCodeInput = countryInput.items.code;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div data-testid="type-check">
|
|
114
|
+
<span data-testid="has-items">
|
|
115
|
+
{addressInput.items ? "true" : "false"}
|
|
116
|
+
</span>
|
|
117
|
+
<span data-testid="street-path">{streetInput.path}</span>
|
|
118
|
+
<span data-testid="country-code-path">{countryCodeInput.path}</span>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
await alepha.start();
|
|
124
|
+
const ui = renderWithAlepha(alepha, <Form />);
|
|
125
|
+
|
|
126
|
+
expect(ui.getByTestId("has-items").textContent).toBe("true");
|
|
127
|
+
expect(ui.getByTestId("street-path").textContent).toBe("/address/street");
|
|
128
|
+
expect(ui.getByTestId("country-code-path").textContent).toBe(
|
|
129
|
+
"/address/country/code",
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should provide ArrayInputField with items array for arrays", async ({
|
|
134
|
+
expect,
|
|
135
|
+
}) => {
|
|
136
|
+
const alepha = Alepha.create().with(AlephaLogger);
|
|
137
|
+
|
|
138
|
+
const Form = () => {
|
|
139
|
+
const form = useForm({
|
|
140
|
+
id: "array-test",
|
|
141
|
+
schema: t.object({
|
|
142
|
+
tags: t.array(t.text()),
|
|
143
|
+
contacts: t.array(
|
|
144
|
+
t.object({
|
|
145
|
+
name: t.text(),
|
|
146
|
+
email: t.text(),
|
|
147
|
+
}),
|
|
148
|
+
),
|
|
149
|
+
}),
|
|
150
|
+
handler: () => {},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Verify array InputFields have items property (initially empty)
|
|
154
|
+
const tagsInput = form.input.tags;
|
|
155
|
+
const contactsInput = form.input.contacts;
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div data-testid="array-check">
|
|
159
|
+
<span data-testid="tags-has-items">
|
|
160
|
+
{Array.isArray(tagsInput.items) ? "true" : "false"}
|
|
161
|
+
</span>
|
|
162
|
+
<span data-testid="tags-items-length">{tagsInput.items.length}</span>
|
|
163
|
+
<span data-testid="contacts-has-items">
|
|
164
|
+
{Array.isArray(contactsInput.items) ? "true" : "false"}
|
|
165
|
+
</span>
|
|
166
|
+
<span data-testid="contacts-items-length">
|
|
167
|
+
{contactsInput.items.length}
|
|
168
|
+
</span>
|
|
169
|
+
<span data-testid="tags-path">{tagsInput.path}</span>
|
|
170
|
+
<span data-testid="contacts-path">{contactsInput.path}</span>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
await alepha.start();
|
|
176
|
+
const ui = renderWithAlepha(alepha, <Form />);
|
|
177
|
+
|
|
178
|
+
// Arrays have items property as array (initially empty)
|
|
179
|
+
expect(ui.getByTestId("tags-has-items").textContent).toBe("true");
|
|
180
|
+
expect(ui.getByTestId("tags-items-length").textContent).toBe("0");
|
|
181
|
+
expect(ui.getByTestId("contacts-has-items").textContent).toBe("true");
|
|
182
|
+
expect(ui.getByTestId("contacts-items-length").textContent).toBe("0");
|
|
183
|
+
expect(ui.getByTestId("tags-path").textContent).toBe("/tags");
|
|
184
|
+
expect(ui.getByTestId("contacts-path").textContent).toBe("/contacts");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should update array values via set method", async ({ expect }) => {
|
|
188
|
+
const alepha = Alepha.create().with(AlephaLogger);
|
|
189
|
+
const calls: Array<any> = [];
|
|
190
|
+
|
|
191
|
+
const Form = () => {
|
|
192
|
+
const form = useForm({
|
|
193
|
+
id: "array-set-test",
|
|
194
|
+
schema: t.object({
|
|
195
|
+
tags: t.array(t.text()),
|
|
196
|
+
}),
|
|
197
|
+
handler: (values) => {
|
|
198
|
+
calls.push(values);
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<form {...form.props} data-testid="array-form">
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
data-testid="set-tags"
|
|
207
|
+
onClick={() => form.input.tags.set(["tag1", "tag2", "tag3"])}
|
|
208
|
+
>
|
|
209
|
+
Set Tags
|
|
210
|
+
</button>
|
|
211
|
+
<button type="submit">Submit</button>
|
|
212
|
+
</form>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
await alepha.start();
|
|
217
|
+
const ui = renderWithAlepha(alepha, <Form />);
|
|
218
|
+
|
|
219
|
+
fireEvent.click(ui.getByTestId("set-tags"));
|
|
220
|
+
fireEvent.submit(ui.getByText("Submit"));
|
|
221
|
+
|
|
222
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
223
|
+
|
|
224
|
+
expect(calls[0]).toEqual({
|
|
225
|
+
tags: ["tag1", "tag2", "tag3"],
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("should update array of objects via set method", async ({ expect }) => {
|
|
230
|
+
const alepha = Alepha.create().with(AlephaLogger);
|
|
231
|
+
const calls: Array<any> = [];
|
|
232
|
+
|
|
233
|
+
const Form = () => {
|
|
234
|
+
const form = useForm({
|
|
235
|
+
id: "array-objects-test",
|
|
236
|
+
schema: t.object({
|
|
237
|
+
contacts: t.array(
|
|
238
|
+
t.object({
|
|
239
|
+
name: t.text(),
|
|
240
|
+
email: t.text(),
|
|
241
|
+
}),
|
|
242
|
+
),
|
|
243
|
+
}),
|
|
244
|
+
handler: (values) => {
|
|
245
|
+
calls.push(values);
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<form {...form.props} data-testid="array-objects-form">
|
|
251
|
+
<button
|
|
252
|
+
type="button"
|
|
253
|
+
data-testid="set-contacts"
|
|
254
|
+
onClick={() =>
|
|
255
|
+
form.input.contacts.set([
|
|
256
|
+
{ name: "Alice", email: "alice@example.com" },
|
|
257
|
+
{ name: "Bob", email: "bob@example.com" },
|
|
258
|
+
])
|
|
259
|
+
}
|
|
260
|
+
>
|
|
261
|
+
Set Contacts
|
|
262
|
+
</button>
|
|
263
|
+
<button type="submit">Submit</button>
|
|
264
|
+
</form>
|
|
265
|
+
);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
await alepha.start();
|
|
269
|
+
const ui = renderWithAlepha(alepha, <Form />);
|
|
270
|
+
|
|
271
|
+
fireEvent.click(ui.getByTestId("set-contacts"));
|
|
272
|
+
fireEvent.submit(ui.getByText("Submit"));
|
|
273
|
+
|
|
274
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
275
|
+
|
|
276
|
+
expect(calls[0]).toEqual({
|
|
277
|
+
contacts: [
|
|
278
|
+
{ name: "Alice", email: "alice@example.com" },
|
|
279
|
+
{ name: "Bob", email: "bob@example.com" },
|
|
280
|
+
],
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should handle complex nested structures with objects and arrays", async ({
|
|
285
|
+
expect,
|
|
286
|
+
}) => {
|
|
287
|
+
const alepha = Alepha.create().with(AlephaLogger);
|
|
288
|
+
const calls: Array<any> = [];
|
|
289
|
+
|
|
290
|
+
const Form = () => {
|
|
291
|
+
const form = useForm({
|
|
292
|
+
id: "complex-test",
|
|
293
|
+
schema: t.object({
|
|
294
|
+
company: t.object({
|
|
295
|
+
name: t.text(),
|
|
296
|
+
address: t.object({
|
|
297
|
+
street: t.text(),
|
|
298
|
+
city: t.text(),
|
|
299
|
+
}),
|
|
300
|
+
}),
|
|
301
|
+
employees: t.array(
|
|
302
|
+
t.object({
|
|
303
|
+
name: t.text(),
|
|
304
|
+
role: t.text(),
|
|
305
|
+
}),
|
|
306
|
+
),
|
|
307
|
+
}),
|
|
308
|
+
handler: (values) => {
|
|
309
|
+
calls.push(values);
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<form {...form.props} data-testid="complex-form">
|
|
315
|
+
<input {...form.input.company.items.name.props} />
|
|
316
|
+
<input {...form.input.company.items.address.items.street.props} />
|
|
317
|
+
<input {...form.input.company.items.address.items.city.props} />
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
data-testid="set-employees"
|
|
321
|
+
onClick={() =>
|
|
322
|
+
form.input.employees.set([
|
|
323
|
+
{ name: "Alice", role: "Engineer" },
|
|
324
|
+
{ name: "Bob", role: "Designer" },
|
|
325
|
+
])
|
|
326
|
+
}
|
|
327
|
+
>
|
|
328
|
+
Set Employees
|
|
329
|
+
</button>
|
|
330
|
+
<button type="submit">Submit</button>
|
|
331
|
+
</form>
|
|
332
|
+
);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
await alepha.start();
|
|
336
|
+
const ui = renderWithAlepha(alepha, <Form />);
|
|
337
|
+
|
|
338
|
+
fireEvent.change(ui.getByTestId("complex-test-company.name"), {
|
|
339
|
+
target: { value: "Acme Corp" },
|
|
340
|
+
});
|
|
341
|
+
fireEvent.change(ui.getByTestId("complex-test-company.address.street"), {
|
|
342
|
+
target: { value: "123 Main St" },
|
|
343
|
+
});
|
|
344
|
+
fireEvent.change(ui.getByTestId("complex-test-company.address.city"), {
|
|
345
|
+
target: { value: "New York" },
|
|
346
|
+
});
|
|
347
|
+
fireEvent.click(ui.getByTestId("set-employees"));
|
|
348
|
+
fireEvent.submit(ui.getByText("Submit"));
|
|
349
|
+
|
|
350
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
351
|
+
|
|
352
|
+
expect(calls[0]).toEqual({
|
|
353
|
+
company: {
|
|
354
|
+
name: "Acme Corp",
|
|
355
|
+
address: {
|
|
356
|
+
street: "123 Main St",
|
|
357
|
+
city: "New York",
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
employees: [
|
|
361
|
+
{ name: "Alice", role: "Engineer" },
|
|
362
|
+
{ name: "Bob", role: "Designer" },
|
|
363
|
+
],
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
|
+
import { SeoExpander } from "../helpers/SeoExpander.ts";
|
|
4
|
+
|
|
5
|
+
describe("SeoExpander", () => {
|
|
6
|
+
it("should expand basic SEO configuration", ({ expect }) => {
|
|
7
|
+
const alepha = Alepha.create();
|
|
8
|
+
const seoExpander = alepha.inject(SeoExpander);
|
|
9
|
+
|
|
10
|
+
const result = seoExpander.expand({
|
|
11
|
+
title: "My App",
|
|
12
|
+
description: "Build amazing apps",
|
|
13
|
+
url: "https://example.com/",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(result.meta).toContainEqual({
|
|
17
|
+
name: "description",
|
|
18
|
+
content: "Build amazing apps",
|
|
19
|
+
});
|
|
20
|
+
expect(result.meta).toContainEqual({
|
|
21
|
+
property: "og:title",
|
|
22
|
+
content: "My App",
|
|
23
|
+
});
|
|
24
|
+
expect(result.meta).toContainEqual({
|
|
25
|
+
property: "og:description",
|
|
26
|
+
content: "Build amazing apps",
|
|
27
|
+
});
|
|
28
|
+
expect(result.meta).toContainEqual({
|
|
29
|
+
property: "og:url",
|
|
30
|
+
content: "https://example.com/",
|
|
31
|
+
});
|
|
32
|
+
expect(result.meta).toContainEqual({
|
|
33
|
+
property: "og:type",
|
|
34
|
+
content: "website",
|
|
35
|
+
});
|
|
36
|
+
expect(result.meta).toContainEqual({
|
|
37
|
+
name: "twitter:title",
|
|
38
|
+
content: "My App",
|
|
39
|
+
});
|
|
40
|
+
expect(result.meta).toContainEqual({
|
|
41
|
+
name: "twitter:description",
|
|
42
|
+
content: "Build amazing apps",
|
|
43
|
+
});
|
|
44
|
+
expect(result.link).toContainEqual({
|
|
45
|
+
rel: "canonical",
|
|
46
|
+
href: "https://example.com/",
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should expand full SEO configuration with image", ({ expect }) => {
|
|
51
|
+
const alepha = Alepha.create();
|
|
52
|
+
const seoExpander = alepha.inject(SeoExpander);
|
|
53
|
+
|
|
54
|
+
const result = seoExpander.expand({
|
|
55
|
+
title: "Alepha Framework",
|
|
56
|
+
description: "TypeScript framework made easy",
|
|
57
|
+
image: "https://alepha.dev/og-image.png",
|
|
58
|
+
url: "https://alepha.dev/",
|
|
59
|
+
siteName: "Alepha",
|
|
60
|
+
locale: "en_US",
|
|
61
|
+
type: "website",
|
|
62
|
+
imageWidth: 1200,
|
|
63
|
+
imageHeight: 630,
|
|
64
|
+
imageAlt: "Alepha Framework Logo",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// OpenGraph tags
|
|
68
|
+
expect(result.meta).toContainEqual({
|
|
69
|
+
property: "og:image",
|
|
70
|
+
content: "https://alepha.dev/og-image.png",
|
|
71
|
+
});
|
|
72
|
+
expect(result.meta).toContainEqual({
|
|
73
|
+
property: "og:image:width",
|
|
74
|
+
content: "1200",
|
|
75
|
+
});
|
|
76
|
+
expect(result.meta).toContainEqual({
|
|
77
|
+
property: "og:image:height",
|
|
78
|
+
content: "630",
|
|
79
|
+
});
|
|
80
|
+
expect(result.meta).toContainEqual({
|
|
81
|
+
property: "og:image:alt",
|
|
82
|
+
content: "Alepha Framework Logo",
|
|
83
|
+
});
|
|
84
|
+
expect(result.meta).toContainEqual({
|
|
85
|
+
property: "og:site_name",
|
|
86
|
+
content: "Alepha",
|
|
87
|
+
});
|
|
88
|
+
expect(result.meta).toContainEqual({
|
|
89
|
+
property: "og:locale",
|
|
90
|
+
content: "en_US",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Twitter tags
|
|
94
|
+
expect(result.meta).toContainEqual({
|
|
95
|
+
name: "twitter:image",
|
|
96
|
+
content: "https://alepha.dev/og-image.png",
|
|
97
|
+
});
|
|
98
|
+
expect(result.meta).toContainEqual({
|
|
99
|
+
name: "twitter:image:alt",
|
|
100
|
+
content: "Alepha Framework Logo",
|
|
101
|
+
});
|
|
102
|
+
// With image, default to summary_large_image
|
|
103
|
+
expect(result.meta).toContainEqual({
|
|
104
|
+
name: "twitter:card",
|
|
105
|
+
content: "summary_large_image",
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should support Twitter-specific overrides", ({ expect }) => {
|
|
110
|
+
const alepha = Alepha.create();
|
|
111
|
+
const seoExpander = alepha.inject(SeoExpander);
|
|
112
|
+
|
|
113
|
+
const result = seoExpander.expand({
|
|
114
|
+
title: "Base Title",
|
|
115
|
+
description: "Base description",
|
|
116
|
+
twitter: {
|
|
117
|
+
card: "summary",
|
|
118
|
+
site: "@alepha_dev",
|
|
119
|
+
creator: "@johndoe",
|
|
120
|
+
title: "Twitter-specific Title",
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result.meta).toContainEqual({
|
|
125
|
+
name: "twitter:card",
|
|
126
|
+
content: "summary",
|
|
127
|
+
});
|
|
128
|
+
expect(result.meta).toContainEqual({
|
|
129
|
+
name: "twitter:site",
|
|
130
|
+
content: "@alepha_dev",
|
|
131
|
+
});
|
|
132
|
+
expect(result.meta).toContainEqual({
|
|
133
|
+
name: "twitter:creator",
|
|
134
|
+
content: "@johndoe",
|
|
135
|
+
});
|
|
136
|
+
expect(result.meta).toContainEqual({
|
|
137
|
+
name: "twitter:title",
|
|
138
|
+
content: "Twitter-specific Title",
|
|
139
|
+
});
|
|
140
|
+
// OG should still use base title
|
|
141
|
+
expect(result.meta).toContainEqual({
|
|
142
|
+
property: "og:title",
|
|
143
|
+
content: "Base Title",
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should support OpenGraph-specific overrides", ({ expect }) => {
|
|
148
|
+
const alepha = Alepha.create();
|
|
149
|
+
const seoExpander = alepha.inject(SeoExpander);
|
|
150
|
+
|
|
151
|
+
const result = seoExpander.expand({
|
|
152
|
+
title: "Base Title",
|
|
153
|
+
description: "Base description",
|
|
154
|
+
og: {
|
|
155
|
+
title: "OG-specific Title",
|
|
156
|
+
description: "OG-specific description",
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(result.meta).toContainEqual({
|
|
161
|
+
property: "og:title",
|
|
162
|
+
content: "OG-specific Title",
|
|
163
|
+
});
|
|
164
|
+
expect(result.meta).toContainEqual({
|
|
165
|
+
property: "og:description",
|
|
166
|
+
content: "OG-specific description",
|
|
167
|
+
});
|
|
168
|
+
// Twitter should use base
|
|
169
|
+
expect(result.meta).toContainEqual({
|
|
170
|
+
name: "twitter:title",
|
|
171
|
+
content: "Base Title",
|
|
172
|
+
});
|
|
173
|
+
expect(result.meta).toContainEqual({
|
|
174
|
+
name: "twitter:description",
|
|
175
|
+
content: "Base description",
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should handle article type", ({ expect }) => {
|
|
180
|
+
const alepha = Alepha.create();
|
|
181
|
+
const seoExpander = alepha.inject(SeoExpander);
|
|
182
|
+
|
|
183
|
+
const result = seoExpander.expand({
|
|
184
|
+
title: "Blog Post",
|
|
185
|
+
type: "article",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(result.meta).toContainEqual({
|
|
189
|
+
property: "og:type",
|
|
190
|
+
content: "article",
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should return empty arrays for empty config", ({ expect }) => {
|
|
195
|
+
const alepha = Alepha.create();
|
|
196
|
+
const seoExpander = alepha.inject(SeoExpander);
|
|
197
|
+
|
|
198
|
+
const result = seoExpander.expand({});
|
|
199
|
+
|
|
200
|
+
expect(result.meta).toEqual([]);
|
|
201
|
+
expect(result.link).toEqual([]);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { $page } from "@alepha/react/router";
|
|
2
|
+
import { Alepha } from "alepha";
|
|
3
|
+
import { describe, it } from "vitest";
|
|
4
|
+
import { $head, AlephaReactHead } from "../index.ts";
|
|
5
|
+
|
|
6
|
+
class App {
|
|
7
|
+
head = $head({
|
|
8
|
+
htmlAttributes: { lang: "fr", "x-data-custom": "ok" },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
hello = $page({
|
|
12
|
+
head: {
|
|
13
|
+
title: "Hello World",
|
|
14
|
+
bodyAttributes: { class: "hello-world" },
|
|
15
|
+
meta: [
|
|
16
|
+
{ name: "description", content: "This is a test page." },
|
|
17
|
+
{ name: "keywords", content: "test, alepha, react" },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
component: () => "",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const alepha = Alepha.create().with(AlephaReactHead);
|
|
25
|
+
const a = alepha.inject(App);
|
|
26
|
+
|
|
27
|
+
describe("PageHead", () => {
|
|
28
|
+
it("should render page with custom head and body attributes", async ({
|
|
29
|
+
expect,
|
|
30
|
+
}) => {
|
|
31
|
+
const result = await a.hello.render({ html: true, hydration: false });
|
|
32
|
+
expect(result.html).toBe(
|
|
33
|
+
'<!DOCTYPE html><html lang="fr" x-data-custom="ok"><head><title>Hello World</title>\n' +
|
|
34
|
+
'<meta name="description" content="This is a test page.">\n' +
|
|
35
|
+
'<meta name="keywords" content="test, alepha, react">\n' +
|
|
36
|
+
'</head><body class="hello-world"></body></html>',
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|