@alepha/react 0.14.1 → 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.browser.js +1488 -4
- package/dist/auth/index.browser.js.map +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +1827 -4
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +58 -937
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +139 -2014
- package/dist/core/index.js.map +1 -1
- package/dist/form/index.d.ts.map +1 -1
- package/dist/form/index.js +6 -1
- package/dist/form/index.js.map +1 -1
- package/dist/head/index.browser.js +3 -1
- package/dist/head/index.browser.js.map +1 -1
- package/dist/head/index.d.ts +552 -8
- package/dist/head/index.d.ts.map +1 -1
- package/dist/head/index.js +17 -2
- package/dist/head/index.js.map +1 -1
- package/dist/{core → router}/index.browser.js +126 -516
- package/dist/router/index.browser.js.map +1 -0
- package/dist/router/index.d.ts +1334 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +1939 -0
- package/dist/router/index.js.map +1 -0
- package/package.json +12 -6
- package/src/auth/__tests__/$auth.spec.ts +188 -0
- package/src/auth/index.ts +1 -1
- package/src/auth/services/ReactAuth.ts +1 -1
- package/src/core/__tests__/Router.spec.tsx +169 -0
- package/src/core/components/ClientOnly.tsx +14 -0
- package/src/core/components/ErrorBoundary.tsx +3 -2
- package/src/core/contexts/AlephaContext.ts +3 -0
- package/src/core/contexts/AlephaProvider.tsx +2 -1
- package/src/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/core/hooks/useAction.ts +11 -0
- package/src/core/index.ts +13 -102
- package/src/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/form/services/FormModel.ts +5 -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 +18 -8
- package/src/head/interfaces/Head.ts +3 -0
- package/src/head/providers/BrowserHeadProvider.browser.spec.ts +271 -0
- package/src/head/providers/HeadProvider.ts +6 -1
- package/src/head/providers/ServerHeadProvider.spec.ts +163 -0
- package/src/head/providers/ServerHeadProvider.ts +20 -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/{core → router}/components/ErrorViewer.tsx +2 -0
- package/src/router/components/Link.tsx +21 -0
- package/src/{core → router}/components/NestedView.tsx +3 -5
- package/src/router/components/NotFound.tsx +30 -0
- package/src/router/errors/Redirection.ts +28 -0
- package/src/{core → router}/hooks/useActive.ts +6 -2
- package/src/{core → router}/hooks/useQueryParams.ts +2 -2
- package/src/{core → router}/hooks/useRouter.ts +1 -1
- package/src/{core → router}/hooks/useRouterState.ts +1 -1
- package/src/{core → router}/index.browser.ts +14 -12
- package/src/{core/index.shared-router.ts → router/index.shared.ts} +6 -3
- package/src/router/index.ts +125 -0
- package/src/router/primitives/$page.browser.spec.tsx +702 -0
- package/src/router/primitives/$page.spec.tsx +702 -0
- package/src/{core → router}/primitives/$page.ts +1 -1
- package/src/{core → router}/providers/ReactBrowserProvider.ts +3 -13
- package/src/{core → router}/providers/ReactBrowserRendererProvider.ts +3 -0
- package/src/{core → router}/providers/ReactBrowserRouterProvider.ts +3 -0
- package/src/{core → router}/providers/ReactPageProvider.ts +5 -3
- package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
- package/src/{core → router}/providers/ReactServerProvider.ts +12 -30
- package/src/{core → router}/services/ReactPageServerService.ts +3 -0
- package/src/{core → router}/services/ReactPageService.ts +5 -5
- package/src/{core → router}/services/ReactRouter.ts +26 -5
- package/dist/core/index.browser.js.map +0 -1
- package/dist/core/index.native.js +0 -403
- package/dist/core/index.native.js.map +0 -1
- package/src/core/components/Link.tsx +0 -18
- package/src/core/components/NotFound.tsx +0 -27
- package/src/core/errors/Redirection.ts +0 -13
- package/src/core/hooks/useSchema.ts +0 -88
- package/src/core/index.native.ts +0 -21
- package/src/core/index.shared.ts +0 -9
- /package/src/{core → router}/contexts/RouterLayerContext.ts +0 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
title: "Alepha Framework",
|
|
9
|
+
description: "TypeScript framework made easy",
|
|
10
|
+
image: "https://alepha.dev/og-image.png",
|
|
11
|
+
url: "https://alepha.dev/",
|
|
12
|
+
siteName: "Alepha",
|
|
13
|
+
locale: "en_US",
|
|
14
|
+
type: "website",
|
|
15
|
+
imageWidth: 1200,
|
|
16
|
+
imageHeight: 630,
|
|
17
|
+
twitter: {
|
|
18
|
+
card: "summary_large_image",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
home = $page({
|
|
23
|
+
component: () => "Home",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const alepha = Alepha.create().with(AlephaReactHead);
|
|
28
|
+
const a = alepha.inject(App);
|
|
29
|
+
|
|
30
|
+
describe("SEO Head", () => {
|
|
31
|
+
it("should render page with SEO meta tags", async ({ expect }) => {
|
|
32
|
+
const result = await a.home.render({ html: true, hydration: false });
|
|
33
|
+
|
|
34
|
+
// Should include description
|
|
35
|
+
expect(result.html).toContain(
|
|
36
|
+
'<meta name="description" content="TypeScript framework made easy">',
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Should include OpenGraph tags with property attribute
|
|
40
|
+
expect(result.html).toContain(
|
|
41
|
+
'<meta property="og:title" content="Alepha Framework">',
|
|
42
|
+
);
|
|
43
|
+
expect(result.html).toContain(
|
|
44
|
+
'<meta property="og:description" content="TypeScript framework made easy">',
|
|
45
|
+
);
|
|
46
|
+
expect(result.html).toContain(
|
|
47
|
+
'<meta property="og:image" content="https://alepha.dev/og-image.png">',
|
|
48
|
+
);
|
|
49
|
+
expect(result.html).toContain(
|
|
50
|
+
'<meta property="og:url" content="https://alepha.dev/">',
|
|
51
|
+
);
|
|
52
|
+
expect(result.html).toContain('<meta property="og:type" content="website">');
|
|
53
|
+
expect(result.html).toContain(
|
|
54
|
+
'<meta property="og:site_name" content="Alepha">',
|
|
55
|
+
);
|
|
56
|
+
expect(result.html).toContain(
|
|
57
|
+
'<meta property="og:locale" content="en_US">',
|
|
58
|
+
);
|
|
59
|
+
expect(result.html).toContain(
|
|
60
|
+
'<meta property="og:image:width" content="1200">',
|
|
61
|
+
);
|
|
62
|
+
expect(result.html).toContain(
|
|
63
|
+
'<meta property="og:image:height" content="630">',
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Should include Twitter Card tags with name attribute
|
|
67
|
+
expect(result.html).toContain(
|
|
68
|
+
'<meta name="twitter:card" content="summary_large_image">',
|
|
69
|
+
);
|
|
70
|
+
expect(result.html).toContain(
|
|
71
|
+
'<meta name="twitter:title" content="Alepha Framework">',
|
|
72
|
+
);
|
|
73
|
+
expect(result.html).toContain(
|
|
74
|
+
'<meta name="twitter:description" content="TypeScript framework made easy">',
|
|
75
|
+
);
|
|
76
|
+
expect(result.html).toContain(
|
|
77
|
+
'<meta name="twitter:image" content="https://alepha.dev/og-image.png">',
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Should include canonical link
|
|
81
|
+
expect(result.html).toContain(
|
|
82
|
+
'<link rel="canonical" href="https://alepha.dev/">',
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
class AppWithPageSeo {
|
|
88
|
+
head = $head({
|
|
89
|
+
title: "My Site",
|
|
90
|
+
titleSeparator: " | ",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
blog = $page({
|
|
94
|
+
path: "/blog",
|
|
95
|
+
head: {
|
|
96
|
+
title: "Blog",
|
|
97
|
+
description: "Read our latest articles",
|
|
98
|
+
image: "https://example.com/blog-og.png",
|
|
99
|
+
type: "article",
|
|
100
|
+
},
|
|
101
|
+
component: () => "Blog",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const alepha2 = Alepha.create().with(AlephaReactHead);
|
|
106
|
+
const app2 = alepha2.inject(AppWithPageSeo);
|
|
107
|
+
|
|
108
|
+
describe("SEO Head on Page", () => {
|
|
109
|
+
it("should render page-level SEO", async ({ expect }) => {
|
|
110
|
+
const result = await app2.blog.render({ html: true, hydration: false });
|
|
111
|
+
|
|
112
|
+
expect(result.html).toContain("<title>Blog | My Site</title>");
|
|
113
|
+
expect(result.html).toContain(
|
|
114
|
+
'<meta name="description" content="Read our latest articles">',
|
|
115
|
+
);
|
|
116
|
+
expect(result.html).toContain('<meta property="og:type" content="article">');
|
|
117
|
+
expect(result.html).toContain(
|
|
118
|
+
'<meta property="og:image" content="https://example.com/blog-og.png">',
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { AlephaContext } from "@alepha/react";
|
|
2
|
+
import type { Head } from "../index.ts";
|
|
3
|
+
import { useHead } from "../index.ts";
|
|
4
|
+
import { render } from "@testing-library/react";
|
|
5
|
+
import { Alepha } from "alepha";
|
|
6
|
+
import { act, type ReactNode } from "react";
|
|
7
|
+
import { beforeEach, describe, it, vi } from "vitest";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @vitest-environment jsdom
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
describe("useHead", () => {
|
|
14
|
+
const renderWithAlepha = (alepha: Alepha, element: ReactNode) => {
|
|
15
|
+
return render(
|
|
16
|
+
<AlephaContext.Provider value={alepha}>{element}</AlephaContext.Provider>,
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
// Reset document state before each test
|
|
22
|
+
document.title = "";
|
|
23
|
+
document.head.innerHTML = "";
|
|
24
|
+
document.body.removeAttribute("class");
|
|
25
|
+
document.body.removeAttribute("style");
|
|
26
|
+
document.documentElement.removeAttribute("lang");
|
|
27
|
+
document.documentElement.removeAttribute("class");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should set initial head options on mount", ({ expect }) => {
|
|
31
|
+
const alepha = Alepha.create();
|
|
32
|
+
const TestComponent = () => {
|
|
33
|
+
useHead({
|
|
34
|
+
title: "Test Title",
|
|
35
|
+
});
|
|
36
|
+
return <div>Test</div>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
renderWithAlepha(alepha, <TestComponent />);
|
|
40
|
+
|
|
41
|
+
expect(document.title).toBe("Test Title");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should return current head state and setter function", ({ expect }) => {
|
|
45
|
+
const alepha = Alepha.create();
|
|
46
|
+
let headState: Head;
|
|
47
|
+
let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
|
|
48
|
+
|
|
49
|
+
const TestComponent = () => {
|
|
50
|
+
const [head, setHead] = useHead();
|
|
51
|
+
headState = head;
|
|
52
|
+
setHeadFn = setHead;
|
|
53
|
+
return <div>Test</div>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
renderWithAlepha(alepha, <TestComponent />);
|
|
57
|
+
|
|
58
|
+
expect(headState!).toBeDefined();
|
|
59
|
+
expect(setHeadFn!).toBeDefined();
|
|
60
|
+
expect(typeof setHeadFn!).toBe("function");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should update document title when setHead is called", ({ expect }) => {
|
|
64
|
+
const alepha = Alepha.create();
|
|
65
|
+
let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
|
|
66
|
+
|
|
67
|
+
const TestComponent = () => {
|
|
68
|
+
const [, setHead] = useHead();
|
|
69
|
+
setHeadFn = setHead;
|
|
70
|
+
return <div>Test</div>;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
renderWithAlepha(alepha, <TestComponent />);
|
|
74
|
+
|
|
75
|
+
act(() => {
|
|
76
|
+
setHeadFn({ title: "Updated Title" });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(document.title).toBe("Updated Title");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should update meta tags when setHead is called", ({ expect }) => {
|
|
83
|
+
const alepha = Alepha.create();
|
|
84
|
+
let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
|
|
85
|
+
|
|
86
|
+
const TestComponent = () => {
|
|
87
|
+
const [, setHead] = useHead();
|
|
88
|
+
setHeadFn = setHead;
|
|
89
|
+
return <div>Test</div>;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
renderWithAlepha(alepha, <TestComponent />);
|
|
93
|
+
|
|
94
|
+
act(() => {
|
|
95
|
+
setHeadFn({
|
|
96
|
+
meta: [
|
|
97
|
+
{ name: "description", content: "Test Description" },
|
|
98
|
+
{ name: "keywords", content: "test, keywords" },
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const descriptionMeta = document.querySelector('meta[name="description"]');
|
|
104
|
+
const keywordsMeta = document.querySelector('meta[name="keywords"]');
|
|
105
|
+
|
|
106
|
+
expect(descriptionMeta?.getAttribute("content")).toBe("Test Description");
|
|
107
|
+
expect(keywordsMeta?.getAttribute("content")).toBe("test, keywords");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should update body attributes when setHead is called", ({ expect }) => {
|
|
111
|
+
const alepha = Alepha.create();
|
|
112
|
+
let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
|
|
113
|
+
|
|
114
|
+
const TestComponent = () => {
|
|
115
|
+
const [, setHead] = useHead();
|
|
116
|
+
setHeadFn = setHead;
|
|
117
|
+
return <div>Test</div>;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
renderWithAlepha(alepha, <TestComponent />);
|
|
121
|
+
|
|
122
|
+
act(() => {
|
|
123
|
+
setHeadFn({
|
|
124
|
+
bodyAttributes: {
|
|
125
|
+
class: "dark-theme",
|
|
126
|
+
style: "background: black;",
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(document.body.getAttribute("class")).toBe("dark-theme");
|
|
132
|
+
expect(document.body.getAttribute("style")).toBe("background: black;");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should update html attributes when setHead is called", ({ expect }) => {
|
|
136
|
+
const alepha = Alepha.create();
|
|
137
|
+
let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
|
|
138
|
+
|
|
139
|
+
const TestComponent = () => {
|
|
140
|
+
const [, setHead] = useHead();
|
|
141
|
+
setHeadFn = setHead;
|
|
142
|
+
return <div>Test</div>;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
renderWithAlepha(alepha, <TestComponent />);
|
|
146
|
+
|
|
147
|
+
act(() => {
|
|
148
|
+
setHeadFn({
|
|
149
|
+
htmlAttributes: {
|
|
150
|
+
lang: "en",
|
|
151
|
+
class: "no-js",
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(document.documentElement.getAttribute("lang")).toBe("en");
|
|
157
|
+
expect(document.documentElement.getAttribute("class")).toBe("no-js");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should support functional updates", ({ expect }) => {
|
|
161
|
+
const alepha = Alepha.create();
|
|
162
|
+
let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
|
|
163
|
+
|
|
164
|
+
const TestComponent = () => {
|
|
165
|
+
const [, setHead] = useHead({ title: "Initial Title" });
|
|
166
|
+
setHeadFn = setHead;
|
|
167
|
+
return <div>Test</div>;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
renderWithAlepha(alepha, <TestComponent />);
|
|
171
|
+
|
|
172
|
+
expect(document.title).toBe("Initial Title");
|
|
173
|
+
|
|
174
|
+
act(() => {
|
|
175
|
+
setHeadFn((prev) => ({
|
|
176
|
+
...prev,
|
|
177
|
+
title: `${prev?.title} - Updated`,
|
|
178
|
+
}));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(document.title).toBe("Initial Title - Updated");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should handle multiple head updates", ({ expect }) => {
|
|
185
|
+
const alepha = Alepha.create();
|
|
186
|
+
let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
|
|
187
|
+
|
|
188
|
+
const TestComponent = () => {
|
|
189
|
+
const [, setHead] = useHead();
|
|
190
|
+
setHeadFn = setHead;
|
|
191
|
+
return <div>Test</div>;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
renderWithAlepha(alepha, <TestComponent />);
|
|
195
|
+
|
|
196
|
+
act(() => {
|
|
197
|
+
setHeadFn({
|
|
198
|
+
title: "First Title",
|
|
199
|
+
meta: [{ name: "description", content: "First Description" }],
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(document.title).toBe("First Title");
|
|
204
|
+
expect(
|
|
205
|
+
document
|
|
206
|
+
.querySelector('meta[name="description"]')
|
|
207
|
+
?.getAttribute("content"),
|
|
208
|
+
).toBe("First Description");
|
|
209
|
+
|
|
210
|
+
act(() => {
|
|
211
|
+
setHeadFn({
|
|
212
|
+
title: "Second Title",
|
|
213
|
+
meta: [{ name: "description", content: "Second Description" }],
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(document.title).toBe("Second Title");
|
|
218
|
+
expect(
|
|
219
|
+
document
|
|
220
|
+
.querySelector('meta[name="description"]')
|
|
221
|
+
?.getAttribute("content"),
|
|
222
|
+
).toBe("Second Description");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should not crash on server-side (non-browser environment)", ({
|
|
226
|
+
expect,
|
|
227
|
+
}) => {
|
|
228
|
+
const alepha = Alepha.create();
|
|
229
|
+
// Mock isBrowser to return false
|
|
230
|
+
vi.spyOn(alepha, "isBrowser").mockReturnValue(false);
|
|
231
|
+
|
|
232
|
+
let headState: Head;
|
|
233
|
+
let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
|
|
234
|
+
|
|
235
|
+
const TestComponent = () => {
|
|
236
|
+
const [head, setHead] = useHead({ title: "Server Title" });
|
|
237
|
+
headState = head;
|
|
238
|
+
setHeadFn = setHead;
|
|
239
|
+
return <div>Test</div>;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
expect(() => {
|
|
243
|
+
renderWithAlepha(alepha, <TestComponent />);
|
|
244
|
+
}).not.toThrow();
|
|
245
|
+
|
|
246
|
+
expect(headState!).toEqual({});
|
|
247
|
+
|
|
248
|
+
// setHead should not crash on server
|
|
249
|
+
expect(() => {
|
|
250
|
+
setHeadFn({ title: "New Title" });
|
|
251
|
+
}).not.toThrow();
|
|
252
|
+
|
|
253
|
+
// Document title should remain unchanged on server
|
|
254
|
+
expect(document.title).toBe("");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should get current head state from document", ({ expect }) => {
|
|
258
|
+
const alepha = Alepha.create();
|
|
259
|
+
|
|
260
|
+
// Pre-populate document with some head data
|
|
261
|
+
document.title = "Existing Title";
|
|
262
|
+
document.body.setAttribute("class", "existing-class");
|
|
263
|
+
document.documentElement.setAttribute("lang", "fr");
|
|
264
|
+
|
|
265
|
+
const meta = document.createElement("meta");
|
|
266
|
+
meta.setAttribute("name", "author");
|
|
267
|
+
meta.setAttribute("content", "John Doe");
|
|
268
|
+
document.head.appendChild(meta);
|
|
269
|
+
|
|
270
|
+
let headState: Head;
|
|
271
|
+
|
|
272
|
+
const TestComponent = () => {
|
|
273
|
+
const [head] = useHead();
|
|
274
|
+
headState = head;
|
|
275
|
+
return <div>Test</div>;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
renderWithAlepha(alepha, <TestComponent />);
|
|
279
|
+
|
|
280
|
+
expect(headState!.title).toBe("Existing Title");
|
|
281
|
+
expect(headState!.bodyAttributes?.class).toBe("existing-class");
|
|
282
|
+
expect(headState!.htmlAttributes?.lang).toBe("fr");
|
|
283
|
+
expect(headState!.meta).toContainEqual({
|
|
284
|
+
name: "author",
|
|
285
|
+
content: "John Doe",
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|