meno-core 1.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/bin/cli.ts +281 -0
- package/build-static.ts +298 -0
- package/bunfig.toml +39 -0
- package/entries/client-router.tsx +111 -0
- package/entries/server-router.tsx +71 -0
- package/lib/client/ClientInitializer.test.ts +9 -0
- package/lib/client/ClientInitializer.test.ts.skip +92 -0
- package/lib/client/ClientInitializer.ts +60 -0
- package/lib/client/ErrorBoundary.test.tsx +595 -0
- package/lib/client/ErrorBoundary.tsx +230 -0
- package/lib/client/componentRegistry.test.ts +165 -0
- package/lib/client/componentRegistry.ts +18 -0
- package/lib/client/contexts/ThemeContext.tsx +73 -0
- package/lib/client/core/ComponentBuilder.test.ts +677 -0
- package/lib/client/core/ComponentBuilder.ts +660 -0
- package/lib/client/core/ComponentRenderer.test.tsx +176 -0
- package/lib/client/core/ComponentRenderer.tsx +83 -0
- package/lib/client/core/cmsTemplateProcessor.ts +129 -0
- package/lib/client/elementRegistry.ts +81 -0
- package/lib/client/hmr/HMRManager.tsx +179 -0
- package/lib/client/hmr/index.ts +5 -0
- package/lib/client/hmrWebSocket.test.ts +9 -0
- package/lib/client/hmrWebSocket.ts +250 -0
- package/lib/client/hooks/useColorVariables.test.ts +166 -0
- package/lib/client/hooks/useColorVariables.ts +249 -0
- package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
- package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
- package/lib/client/hydration/HydrationUtils.test.ts +154 -0
- package/lib/client/hydration/HydrationUtils.ts +35 -0
- package/lib/client/i18nConfigService.test.ts +74 -0
- package/lib/client/i18nConfigService.ts +78 -0
- package/lib/client/index.ts +56 -0
- package/lib/client/navigation.test.ts +441 -0
- package/lib/client/navigation.ts +23 -0
- package/lib/client/responsiveStyleResolver.test.ts +491 -0
- package/lib/client/responsiveStyleResolver.ts +184 -0
- package/lib/client/routing/RouteLoader.test.ts +635 -0
- package/lib/client/routing/RouteLoader.ts +347 -0
- package/lib/client/routing/Router.tsx +382 -0
- package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
- package/lib/client/scripts/ScriptExecutor.ts +171 -0
- package/lib/client/scripts/formHandler.ts +103 -0
- package/lib/client/styleProcessor.test.ts +126 -0
- package/lib/client/styleProcessor.ts +92 -0
- package/lib/client/styles/StyleInjector.test.ts +354 -0
- package/lib/client/styles/StyleInjector.ts +154 -0
- package/lib/client/templateEngine.test.ts +660 -0
- package/lib/client/templateEngine.ts +667 -0
- package/lib/client/theme.test.ts +173 -0
- package/lib/client/theme.ts +159 -0
- package/lib/client/utils/toast.ts +46 -0
- package/lib/server/createServer.ts +170 -0
- package/lib/server/cssGenerator.test.ts +172 -0
- package/lib/server/cssGenerator.ts +58 -0
- package/lib/server/fileWatcher.ts +134 -0
- package/lib/server/index.ts +55 -0
- package/lib/server/jsonLoader.test.ts +103 -0
- package/lib/server/jsonLoader.ts +350 -0
- package/lib/server/middleware/cors.test.ts +177 -0
- package/lib/server/middleware/cors.ts +69 -0
- package/lib/server/middleware/errorHandler.test.ts +208 -0
- package/lib/server/middleware/errorHandler.ts +63 -0
- package/lib/server/middleware/index.ts +9 -0
- package/lib/server/middleware/logger.test.ts +233 -0
- package/lib/server/middleware/logger.ts +99 -0
- package/lib/server/pageCache.test.ts +167 -0
- package/lib/server/pageCache.ts +97 -0
- package/lib/server/projectContext.ts +51 -0
- package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
- package/lib/server/providers/fileSystemPageProvider.ts +83 -0
- package/lib/server/routes/api/cms.test.ts +177 -0
- package/lib/server/routes/api/cms.ts +82 -0
- package/lib/server/routes/api/colors.ts +59 -0
- package/lib/server/routes/api/components.ts +70 -0
- package/lib/server/routes/api/config.test.ts +9 -0
- package/lib/server/routes/api/config.ts +28 -0
- package/lib/server/routes/api/core-routes.ts +182 -0
- package/lib/server/routes/api/functions.ts +170 -0
- package/lib/server/routes/api/index.ts +69 -0
- package/lib/server/routes/api/pages.ts +95 -0
- package/lib/server/routes/api/shared.test.ts +81 -0
- package/lib/server/routes/api/shared.ts +31 -0
- package/lib/server/routes/editor.test.ts +9 -0
- package/lib/server/routes/index.ts +104 -0
- package/lib/server/routes/pages.ts +161 -0
- package/lib/server/routes/static.ts +107 -0
- package/lib/server/services/ColorService.ts +193 -0
- package/lib/server/services/cmsService.test.ts +388 -0
- package/lib/server/services/cmsService.ts +296 -0
- package/lib/server/services/componentService.test.ts +276 -0
- package/lib/server/services/componentService.ts +346 -0
- package/lib/server/services/configService.ts +156 -0
- package/lib/server/services/fileWatcherService.ts +67 -0
- package/lib/server/services/index.ts +10 -0
- package/lib/server/services/pageService.test.ts +258 -0
- package/lib/server/services/pageService.ts +240 -0
- package/lib/server/ssrRenderer.test.ts +1005 -0
- package/lib/server/ssrRenderer.ts +878 -0
- package/lib/server/utilityClassGenerator.ts +11 -0
- package/lib/server/utils/index.ts +5 -0
- package/lib/server/utils/jsonLineMapper.test.ts +100 -0
- package/lib/server/utils/jsonLineMapper.ts +166 -0
- package/lib/server/validateStyleCoverage.test.ts +9 -0
- package/lib/server/validateStyleCoverage.ts +167 -0
- package/lib/server/websocketManager.test.ts +9 -0
- package/lib/server/websocketManager.ts +95 -0
- package/lib/shared/attributeNodeUtils.test.ts +152 -0
- package/lib/shared/attributeNodeUtils.ts +50 -0
- package/lib/shared/breakpoints.test.ts +166 -0
- package/lib/shared/breakpoints.ts +65 -0
- package/lib/shared/colorProperties.test.ts +111 -0
- package/lib/shared/colorProperties.ts +40 -0
- package/lib/shared/colorVariableUtils.test.ts +319 -0
- package/lib/shared/colorVariableUtils.ts +97 -0
- package/lib/shared/constants.test.ts +175 -0
- package/lib/shared/constants.ts +116 -0
- package/lib/shared/cssGeneration.ts +481 -0
- package/lib/shared/cssProperties.test.ts +252 -0
- package/lib/shared/cssProperties.ts +338 -0
- package/lib/shared/elementUtils.test.ts +245 -0
- package/lib/shared/elementUtils.ts +90 -0
- package/lib/shared/fontLoader.ts +97 -0
- package/lib/shared/i18n.test.ts +313 -0
- package/lib/shared/i18n.ts +286 -0
- package/lib/shared/index.ts +50 -0
- package/lib/shared/interfaces/contentProvider.test.ts +9 -0
- package/lib/shared/interfaces/contentProvider.ts +121 -0
- package/lib/shared/nodeUtils.test.ts +320 -0
- package/lib/shared/nodeUtils.ts +220 -0
- package/lib/shared/pathArrayUtils.test.ts +315 -0
- package/lib/shared/pathArrayUtils.ts +17 -0
- package/lib/shared/pathUtils.test.ts +260 -0
- package/lib/shared/pathUtils.ts +244 -0
- package/lib/shared/paths/Path.test.ts +74 -0
- package/lib/shared/paths/Path.ts +23 -0
- package/lib/shared/paths/PathConverter.test.ts +232 -0
- package/lib/shared/paths/PathConverter.ts +141 -0
- package/lib/shared/paths/PathUtils.ts +290 -0
- package/lib/shared/paths/PathValidator.test.ts +193 -0
- package/lib/shared/paths/PathValidator.ts +53 -0
- package/lib/shared/paths/index.ts +48 -0
- package/lib/shared/propResolver.test.ts +639 -0
- package/lib/shared/propResolver.ts +124 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
- package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
- package/lib/shared/registry/ClientRegistry.test.ts +26 -0
- package/lib/shared/registry/ClientRegistry.ts +15 -0
- package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
- package/lib/shared/registry/ComponentRegistry.ts +100 -0
- package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
- package/lib/shared/registry/NodeTypeManager.ts +94 -0
- package/lib/shared/registry/RegistryManager.test.ts +58 -0
- package/lib/shared/registry/RegistryManager.ts +60 -0
- package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
- package/lib/shared/registry/SSRRegistry.test.ts +26 -0
- package/lib/shared/registry/SSRRegistry.ts +15 -0
- package/lib/shared/registry/createNodeType.ts +175 -0
- package/lib/shared/registry/defineNodeType.ts +73 -0
- package/lib/shared/registry/fieldPresets.ts +109 -0
- package/lib/shared/registry/index.ts +50 -0
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
- package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
- package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
- package/lib/shared/registry/nodeTypes/index.ts +75 -0
- package/lib/shared/responsiveScaling.test.ts +268 -0
- package/lib/shared/responsiveScaling.ts +194 -0
- package/lib/shared/responsiveStyleUtils.test.ts +300 -0
- package/lib/shared/responsiveStyleUtils.ts +139 -0
- package/lib/shared/slugTranslator.test.ts +325 -0
- package/lib/shared/slugTranslator.ts +177 -0
- package/lib/shared/styleNodeUtils.test.ts +132 -0
- package/lib/shared/styleNodeUtils.ts +102 -0
- package/lib/shared/styleUtils.test.ts +238 -0
- package/lib/shared/styleUtils.ts +63 -0
- package/lib/shared/themeDefaults.test.ts +113 -0
- package/lib/shared/themeDefaults.ts +103 -0
- package/lib/shared/tree/PathBuilder.ts +383 -0
- package/lib/shared/treePathUtils.test.ts +539 -0
- package/lib/shared/treePathUtils.ts +339 -0
- package/lib/shared/types/api.ts +58 -0
- package/lib/shared/types/cms.ts +95 -0
- package/lib/shared/types/colors.ts +45 -0
- package/lib/shared/types/components.ts +121 -0
- package/lib/shared/types/errors.test.ts +103 -0
- package/lib/shared/types/errors.ts +69 -0
- package/lib/shared/types/index.ts +96 -0
- package/lib/shared/types/nodes.ts +20 -0
- package/lib/shared/types/rendering.ts +61 -0
- package/lib/shared/types/styles.ts +38 -0
- package/lib/shared/types.ts +11 -0
- package/lib/shared/utilityClassConfig.ts +287 -0
- package/lib/shared/utilityClassMapper.test.ts +140 -0
- package/lib/shared/utilityClassMapper.ts +229 -0
- package/lib/shared/utils/fileUtils.test.ts +99 -0
- package/lib/shared/utils/fileUtils.ts +56 -0
- package/lib/shared/utils.test.ts +261 -0
- package/lib/shared/utils.ts +84 -0
- package/lib/shared/validation/index.ts +7 -0
- package/lib/shared/validation/propValidator.test.ts +178 -0
- package/lib/shared/validation/propValidator.ts +238 -0
- package/lib/shared/validation/schemas.test.ts +177 -0
- package/lib/shared/validation/schemas.ts +401 -0
- package/lib/shared/validation/validators.test.ts +109 -0
- package/lib/shared/validation/validators.ts +304 -0
- package/lib/test-utils/dom-setup.ts +55 -0
- package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
- package/lib/test-utils/factories/DomMockFactory.ts +487 -0
- package/lib/test-utils/factories/EventMockFactory.ts +244 -0
- package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
- package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
- package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
- package/lib/test-utils/factories/index.ts +11 -0
- package/lib/test-utils/fixtures.ts +134 -0
- package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
- package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
- package/lib/test-utils/helpers/index.ts +6 -0
- package/lib/test-utils/helpers.test.ts +73 -0
- package/lib/test-utils/helpers.ts +90 -0
- package/lib/test-utils/index.ts +17 -0
- package/lib/test-utils/mockFactories.ts +92 -0
- package/lib/test-utils/mocks.ts +341 -0
- package/package.json +38 -0
- package/templates/index-router.html +34 -0
- package/tsconfig.json +14 -0
- package/vite.config.ts +43 -0
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
generateSSRHTML,
|
|
4
|
+
renderPageSSR,
|
|
5
|
+
extractPageMeta,
|
|
6
|
+
generateMetaTags
|
|
7
|
+
} from "./ssrRenderer";
|
|
8
|
+
import type { ComponentNode, ComponentDefinition, JSONPage } from "../shared/types";
|
|
9
|
+
|
|
10
|
+
// Mock jsonLoader
|
|
11
|
+
const mockLoadBreakpointConfig = async () => ({
|
|
12
|
+
tablet: 1024,
|
|
13
|
+
mobile: 540
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Mock the jsonLoader module
|
|
17
|
+
const originalJsonLoader = await import("./jsonLoader");
|
|
18
|
+
if (originalJsonLoader) {
|
|
19
|
+
// We'll need to test with actual imports, but can provide mocks where needed
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("SSR Renderer - extractPageMeta", () => {
|
|
23
|
+
test("should extract basic meta information", () => {
|
|
24
|
+
const pageData: JSONPage = {
|
|
25
|
+
meta: {
|
|
26
|
+
title: "Test Page",
|
|
27
|
+
description: "Test description",
|
|
28
|
+
keywords: "test, page"
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const meta = extractPageMeta(pageData);
|
|
33
|
+
|
|
34
|
+
expect(meta.title).toBe("Test Page");
|
|
35
|
+
expect(meta.description).toBe("Test description");
|
|
36
|
+
expect(meta.keywords).toBe("test, page");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("should extract Open Graph meta", () => {
|
|
40
|
+
const pageData: JSONPage = {
|
|
41
|
+
meta: {
|
|
42
|
+
title: "Test Page",
|
|
43
|
+
ogTitle: "OG Title",
|
|
44
|
+
ogDescription: "OG Description",
|
|
45
|
+
ogImage: "https://example.com/image.jpg",
|
|
46
|
+
ogType: "article"
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const meta = extractPageMeta(pageData);
|
|
51
|
+
|
|
52
|
+
expect(meta.ogTitle).toBe("OG Title");
|
|
53
|
+
expect(meta.ogDescription).toBe("OG Description");
|
|
54
|
+
expect(meta.ogImage).toBe("https://example.com/image.jpg");
|
|
55
|
+
expect(meta.ogType).toBe("article");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("should use title as fallback for ogTitle", () => {
|
|
59
|
+
const pageData: JSONPage = {
|
|
60
|
+
meta: {
|
|
61
|
+
title: "Test Page"
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const meta = extractPageMeta(pageData);
|
|
66
|
+
|
|
67
|
+
expect(meta.ogTitle).toBe("Test Page");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("should use description as fallback for ogDescription", () => {
|
|
71
|
+
const pageData: JSONPage = {
|
|
72
|
+
meta: {
|
|
73
|
+
description: "Test description"
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const meta = extractPageMeta(pageData);
|
|
78
|
+
|
|
79
|
+
expect(meta.ogDescription).toBe("Test description");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("should default ogType to 'website'", () => {
|
|
83
|
+
const pageData: JSONPage = {
|
|
84
|
+
meta: {}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const meta = extractPageMeta(pageData);
|
|
88
|
+
|
|
89
|
+
expect(meta.ogType).toBe("website");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("should handle missing meta object", () => {
|
|
93
|
+
const pageData: JSONPage = {};
|
|
94
|
+
|
|
95
|
+
const meta = extractPageMeta(pageData);
|
|
96
|
+
|
|
97
|
+
expect(Object.keys(meta).length).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("SSR Renderer - generateMetaTags", () => {
|
|
102
|
+
test("should generate title tag", () => {
|
|
103
|
+
const meta = {
|
|
104
|
+
title: "Test Page"
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const tags = generateMetaTags(meta);
|
|
108
|
+
|
|
109
|
+
expect(tags).toContain("<title>Test Page</title>");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("should generate description meta tag", () => {
|
|
113
|
+
const meta = {
|
|
114
|
+
description: "Test description"
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const tags = generateMetaTags(meta);
|
|
118
|
+
|
|
119
|
+
expect(tags).toContain(`<meta name="description" content="Test description" />`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("should generate keywords meta tag", () => {
|
|
123
|
+
const meta = {
|
|
124
|
+
keywords: "test, keywords"
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const tags = generateMetaTags(meta);
|
|
128
|
+
|
|
129
|
+
expect(tags).toContain(`<meta name="keywords" content="test, keywords" />`);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("should generate Open Graph tags", () => {
|
|
133
|
+
const meta = {
|
|
134
|
+
ogTitle: "OG Title",
|
|
135
|
+
ogDescription: "OG Description",
|
|
136
|
+
ogImage: "https://example.com/image.jpg",
|
|
137
|
+
ogType: "article"
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const tags = generateMetaTags(meta);
|
|
141
|
+
|
|
142
|
+
expect(tags).toContain(`<meta property="og:title" content="OG Title" />`);
|
|
143
|
+
expect(tags).toContain(`<meta property="og:description" content="OG Description" />`);
|
|
144
|
+
expect(tags).toContain(`<meta property="og:image" content="https://example.com/image.jpg" />`);
|
|
145
|
+
expect(tags).toContain(`<meta property="og:type" content="article" />`);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("should generate canonical URL", () => {
|
|
149
|
+
const meta = {};
|
|
150
|
+
const url = "https://example.com/page";
|
|
151
|
+
|
|
152
|
+
const tags = generateMetaTags(meta, url);
|
|
153
|
+
|
|
154
|
+
expect(tags).toContain(`<link rel="canonical" href="https://example.com/page" />`);
|
|
155
|
+
expect(tags).toContain(`<meta property="og:url" content="https://example.com/page" />`);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("should escape HTML in meta content", () => {
|
|
159
|
+
const meta = {
|
|
160
|
+
title: "Test & Page",
|
|
161
|
+
description: "Test <description>"
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const tags = generateMetaTags(meta);
|
|
165
|
+
|
|
166
|
+
expect(tags).toContain("<title>Test & Page</title>");
|
|
167
|
+
expect(tags).toContain(`content="Test <description>"`);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("should handle empty meta", () => {
|
|
171
|
+
const meta = {};
|
|
172
|
+
|
|
173
|
+
const tags = generateMetaTags(meta);
|
|
174
|
+
|
|
175
|
+
expect(tags).toBe("");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("SSR Renderer - renderPageSSR", () => {
|
|
180
|
+
describe("HTML generation", () => {
|
|
181
|
+
test("should render simple div element", async () => {
|
|
182
|
+
const pageData: JSONPage = {
|
|
183
|
+
root: {
|
|
184
|
+
type: "node",
|
|
185
|
+
tag: "div",
|
|
186
|
+
children: ["Hello World"]
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const result = await renderPageSSR(pageData);
|
|
191
|
+
|
|
192
|
+
expect(result.html).toContain("<div");
|
|
193
|
+
expect(result.html).toContain("Hello World");
|
|
194
|
+
expect(result.html).toContain("</div>");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("should render nested component structures", async () => {
|
|
198
|
+
const pageData: JSONPage = {
|
|
199
|
+
root: {
|
|
200
|
+
type: "node",
|
|
201
|
+
tag: "div",
|
|
202
|
+
children: [
|
|
203
|
+
{
|
|
204
|
+
type: "node",
|
|
205
|
+
tag: "div",
|
|
206
|
+
children: [
|
|
207
|
+
{
|
|
208
|
+
type: "node",
|
|
209
|
+
tag: "span",
|
|
210
|
+
children: ["Nested"]
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const result = await renderPageSSR(pageData);
|
|
219
|
+
|
|
220
|
+
expect(result.html).toContain("<div");
|
|
221
|
+
expect(result.html).toContain("<span");
|
|
222
|
+
expect(result.html).toContain("Nested");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("should render children arrays", async () => {
|
|
226
|
+
const pageData: JSONPage = {
|
|
227
|
+
root: {
|
|
228
|
+
type: "node",
|
|
229
|
+
tag: "div",
|
|
230
|
+
children: [
|
|
231
|
+
{ type: "node", tag: "p", children: ["First"] },
|
|
232
|
+
{ type: "node", tag: "p", children: ["Second"] }
|
|
233
|
+
]
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const result = await renderPageSSR(pageData);
|
|
238
|
+
|
|
239
|
+
expect(result.html).toContain("First");
|
|
240
|
+
expect(result.html).toContain("Second");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("should render string children", async () => {
|
|
244
|
+
const pageData: JSONPage = {
|
|
245
|
+
root: {
|
|
246
|
+
type: "node",
|
|
247
|
+
tag: "p",
|
|
248
|
+
children: ["Simple text"]
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const result = await renderPageSSR(pageData);
|
|
253
|
+
|
|
254
|
+
expect(result.html).toContain("Simple text");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("should render number children", async () => {
|
|
258
|
+
const pageData: JSONPage = {
|
|
259
|
+
root: {
|
|
260
|
+
type: "node",
|
|
261
|
+
tag: "span",
|
|
262
|
+
children: [42]
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const result = await renderPageSSR(pageData);
|
|
267
|
+
|
|
268
|
+
expect(result.html).toContain("42");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("should escape HTML in text content", async () => {
|
|
272
|
+
const pageData: JSONPage = {
|
|
273
|
+
root: {
|
|
274
|
+
type: "node",
|
|
275
|
+
tag: "div",
|
|
276
|
+
children: ["<script>alert('xss')</script>"]
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const result = await renderPageSSR(pageData);
|
|
281
|
+
|
|
282
|
+
expect(result.html).toContain("<script>");
|
|
283
|
+
expect(result.html).not.toContain("<script>");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("should render anchor tag", async () => {
|
|
287
|
+
const pageData: JSONPage = {
|
|
288
|
+
root: {
|
|
289
|
+
type: "node",
|
|
290
|
+
tag: "a",
|
|
291
|
+
children: ["Go to About"]
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const result = await renderPageSSR(pageData);
|
|
296
|
+
|
|
297
|
+
expect(result.html).toContain(`<a`);
|
|
298
|
+
expect(result.html).toContain("Go to About");
|
|
299
|
+
expect(result.html).toContain("</a>");
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("CSS generation", () => {
|
|
304
|
+
test("should generate CSS for responsive styles", async () => {
|
|
305
|
+
const pageData: JSONPage = {
|
|
306
|
+
root: {
|
|
307
|
+
type: "node",
|
|
308
|
+
tag: "div",
|
|
309
|
+
style: {
|
|
310
|
+
base: {
|
|
311
|
+
fontSize: "16px",
|
|
312
|
+
color: "red"
|
|
313
|
+
},
|
|
314
|
+
tablet: {
|
|
315
|
+
fontSize: "14px"
|
|
316
|
+
},
|
|
317
|
+
mobile: {
|
|
318
|
+
fontSize: "12px"
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const result = await renderPageSSR(pageData);
|
|
325
|
+
|
|
326
|
+
// SSR renders HTML with styles (may be inline or via classes)
|
|
327
|
+
expect(result.html).toBeDefined();
|
|
328
|
+
expect(result.html).toContain("<div");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("should generate CSS class names for responsive styles", async () => {
|
|
332
|
+
const pageData: JSONPage = {
|
|
333
|
+
root: {
|
|
334
|
+
type: "node",
|
|
335
|
+
tag: "div",
|
|
336
|
+
style: {
|
|
337
|
+
base: {
|
|
338
|
+
color: "red"
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const result = await renderPageSSR(pageData);
|
|
345
|
+
|
|
346
|
+
// HTML should be generated with styles
|
|
347
|
+
expect(result.html).toBeDefined();
|
|
348
|
+
expect(result.html).toContain("<div");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("should handle empty responsive styles", async () => {
|
|
352
|
+
const pageData: JSONPage = {
|
|
353
|
+
root: {
|
|
354
|
+
type: "node",
|
|
355
|
+
tag: "div",
|
|
356
|
+
style: {
|
|
357
|
+
base: {},
|
|
358
|
+
tablet: {},
|
|
359
|
+
mobile: {}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const result = await renderPageSSR(pageData);
|
|
365
|
+
|
|
366
|
+
// Should still render HTML
|
|
367
|
+
expect(result.html).toContain("<div");
|
|
368
|
+
// componentCSS might be empty or minimal
|
|
369
|
+
expect(result.componentCSS).toBeDefined();
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe("Component rendering", () => {
|
|
374
|
+
test("should render Button component (from Button.json)", async () => {
|
|
375
|
+
const buttonComponent: ComponentDefinition = {
|
|
376
|
+
type: "Button",
|
|
377
|
+
component: {
|
|
378
|
+
interface: {
|
|
379
|
+
variant: {
|
|
380
|
+
type: "select",
|
|
381
|
+
default: "primary"
|
|
382
|
+
},
|
|
383
|
+
children: {
|
|
384
|
+
type: "slot",
|
|
385
|
+
default: "Click me"
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
structure: {
|
|
389
|
+
type: "node",
|
|
390
|
+
tag: "button",
|
|
391
|
+
style: {
|
|
392
|
+
base: {
|
|
393
|
+
background: {
|
|
394
|
+
_mapping: true,
|
|
395
|
+
prop: "variant",
|
|
396
|
+
values: {
|
|
397
|
+
primary: "blue",
|
|
398
|
+
secondary: "gray"
|
|
399
|
+
}
|
|
400
|
+
} as any,
|
|
401
|
+
color: "white"
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
children: "{{children}}"
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const pageData: JSONPage = {
|
|
410
|
+
root: {
|
|
411
|
+
type: "component",
|
|
412
|
+
component: "Button",
|
|
413
|
+
props: {
|
|
414
|
+
variant: "primary"
|
|
415
|
+
},
|
|
416
|
+
children: ["Custom Button"]
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const globalComponents = {
|
|
421
|
+
Button: buttonComponent
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const result = await renderPageSSR(pageData, globalComponents);
|
|
425
|
+
|
|
426
|
+
// Should render button element
|
|
427
|
+
expect(result.html).toContain("<button");
|
|
428
|
+
// Should render children
|
|
429
|
+
expect(result.html).toContain("Custom Button");
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("should render BasicCard component with template evaluation", async () => {
|
|
433
|
+
const cardComponent: ComponentDefinition = {
|
|
434
|
+
type: "BasicCard",
|
|
435
|
+
component: {
|
|
436
|
+
interface: {
|
|
437
|
+
title: {
|
|
438
|
+
type: "string",
|
|
439
|
+
default: "Card Title"
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
structure: {
|
|
443
|
+
type: "node",
|
|
444
|
+
tag: "div",
|
|
445
|
+
children: [
|
|
446
|
+
{
|
|
447
|
+
type: "node",
|
|
448
|
+
tag: "h3",
|
|
449
|
+
children: ["{{title}}"]
|
|
450
|
+
}
|
|
451
|
+
]
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const pageData: JSONPage = {
|
|
457
|
+
root: {
|
|
458
|
+
type: "component",
|
|
459
|
+
component: "BasicCard",
|
|
460
|
+
props: {
|
|
461
|
+
title: "My Card"
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const globalComponents = {
|
|
467
|
+
BasicCard: cardComponent
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const result = await renderPageSSR(pageData, globalComponents);
|
|
471
|
+
|
|
472
|
+
expect(result.html).toContain("<div");
|
|
473
|
+
expect(result.html).toContain("<h3");
|
|
474
|
+
expect(result.html).toContain("My Card");
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
describe("Template evaluation in SSR", () => {
|
|
479
|
+
test("should evaluate template expressions in structure", async () => {
|
|
480
|
+
const pageData: JSONPage = {
|
|
481
|
+
root: {
|
|
482
|
+
type: "node",
|
|
483
|
+
tag: "div",
|
|
484
|
+
children: ["Hello {{name}}"]
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// Note: This test may need adjustment based on actual template evaluation in SSR
|
|
489
|
+
const result = await renderPageSSR(pageData);
|
|
490
|
+
|
|
491
|
+
// Template should be evaluated or passed through
|
|
492
|
+
expect(result.html).toBeDefined();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("should handle style mappings in component structures", async () => {
|
|
496
|
+
const buttonComponent: ComponentDefinition = {
|
|
497
|
+
type: "Button",
|
|
498
|
+
component: {
|
|
499
|
+
interface: {
|
|
500
|
+
variant: {
|
|
501
|
+
type: "select",
|
|
502
|
+
default: "primary"
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
structure: {
|
|
506
|
+
type: "node",
|
|
507
|
+
tag: "button",
|
|
508
|
+
style: {
|
|
509
|
+
base: {
|
|
510
|
+
background: {
|
|
511
|
+
_mapping: true,
|
|
512
|
+
prop: "variant",
|
|
513
|
+
values: {
|
|
514
|
+
primary: "blue",
|
|
515
|
+
secondary: "gray"
|
|
516
|
+
}
|
|
517
|
+
} as any
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const pageData: JSONPage = {
|
|
525
|
+
root: {
|
|
526
|
+
type: "component",
|
|
527
|
+
component: "Button",
|
|
528
|
+
props: {
|
|
529
|
+
variant: "primary"
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const globalComponents = {
|
|
535
|
+
Button: buttonComponent
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const result = await renderPageSSR(pageData, globalComponents);
|
|
539
|
+
|
|
540
|
+
// Should resolve style mapping based on variant prop
|
|
541
|
+
expect(result.html).toBeDefined();
|
|
542
|
+
// CSS should include resolved background color or inline style
|
|
543
|
+
expect(result.html.length).toBeGreaterThan(0);
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe("Error handling", () => {
|
|
548
|
+
test("should handle missing root node", async () => {
|
|
549
|
+
const pageData: JSONPage = {};
|
|
550
|
+
|
|
551
|
+
// Implementation throws error for missing root
|
|
552
|
+
await expect(renderPageSSR(pageData)).rejects.toThrow('Page data must have a root node');
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test("should handle null root node", async () => {
|
|
556
|
+
const pageData: JSONPage = {
|
|
557
|
+
root: null as any
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// Implementation throws error for null root
|
|
561
|
+
await expect(renderPageSSR(pageData)).rejects.toThrow('Page data must have a root node');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("should handle component with missing structure", async () => {
|
|
565
|
+
const invalidComponent: ComponentDefinition = {
|
|
566
|
+
type: "Invalid",
|
|
567
|
+
component: {} as any
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const pageData: JSONPage = {
|
|
571
|
+
root: {
|
|
572
|
+
type: "Invalid"
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const globalComponents = {
|
|
577
|
+
Invalid: invalidComponent
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// Should not throw, but return empty HTML for invalid component
|
|
581
|
+
const result = await renderPageSSR(pageData, globalComponents);
|
|
582
|
+
|
|
583
|
+
expect(result.html).toBe("");
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("should handle malformed JSON structures gracefully", async () => {
|
|
587
|
+
const pageData: JSONPage = {
|
|
588
|
+
root: {
|
|
589
|
+
type: "node",
|
|
590
|
+
tag: "div",
|
|
591
|
+
children: [
|
|
592
|
+
null as any,
|
|
593
|
+
undefined as any
|
|
594
|
+
]
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
const result = await renderPageSSR(pageData);
|
|
599
|
+
|
|
600
|
+
// Should skip null/undefined children
|
|
601
|
+
expect(result.html).toBeDefined();
|
|
602
|
+
expect(result.html).toContain("<div");
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
describe("Meta information in SSR", () => {
|
|
607
|
+
test("should extract and return meta information", async () => {
|
|
608
|
+
const pageData: JSONPage = {
|
|
609
|
+
meta: {
|
|
610
|
+
title: "Test Page",
|
|
611
|
+
description: "Test description"
|
|
612
|
+
},
|
|
613
|
+
root: {
|
|
614
|
+
type: "node",
|
|
615
|
+
tag: "div",
|
|
616
|
+
children: ["Content"]
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const result = await renderPageSSR(pageData);
|
|
621
|
+
|
|
622
|
+
expect(result.title).toBe("Test Page");
|
|
623
|
+
expect(result.meta).toContain("Test Page");
|
|
624
|
+
expect(result.meta).toContain("Test description");
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("should use default title when meta not provided", async () => {
|
|
628
|
+
const pageData: JSONPage = {
|
|
629
|
+
root: {
|
|
630
|
+
type: "node",
|
|
631
|
+
tag: "div"
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const result = await renderPageSSR(pageData);
|
|
636
|
+
|
|
637
|
+
expect(result.title).toBe("UPLO");
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
describe("SSR Renderer - Security (XSS Prevention)", () => {
|
|
643
|
+
describe("Text content sanitization", () => {
|
|
644
|
+
test("should escape script tags in text content", async () => {
|
|
645
|
+
const pageData: JSONPage = {
|
|
646
|
+
root: {
|
|
647
|
+
type: "node",
|
|
648
|
+
tag: "div",
|
|
649
|
+
children: ["<script>alert('xss')</script>"]
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const result = await renderPageSSR(pageData);
|
|
654
|
+
|
|
655
|
+
expect(result.html).toContain("<script>");
|
|
656
|
+
expect(result.html).not.toContain("<script>alert");
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("should escape event handlers in text content", async () => {
|
|
660
|
+
const pageData: JSONPage = {
|
|
661
|
+
root: {
|
|
662
|
+
type: "node",
|
|
663
|
+
tag: "div",
|
|
664
|
+
children: ['<img onerror="alert(1)" src="x">']
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
const result = await renderPageSSR(pageData);
|
|
669
|
+
|
|
670
|
+
expect(result.html).not.toContain('onerror="alert');
|
|
671
|
+
expect(result.html).toContain("<img");
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
test("should escape javascript: URLs in text content", async () => {
|
|
675
|
+
const pageData: JSONPage = {
|
|
676
|
+
root: {
|
|
677
|
+
type: "node",
|
|
678
|
+
tag: "div",
|
|
679
|
+
children: ['<a href="javascript:alert(1)">click</a>']
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const result = await renderPageSSR(pageData);
|
|
684
|
+
|
|
685
|
+
expect(result.html).not.toContain('href="javascript:');
|
|
686
|
+
expect(result.html).toContain("<a");
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test("should escape HTML entities in text content", async () => {
|
|
690
|
+
const pageData: JSONPage = {
|
|
691
|
+
root: {
|
|
692
|
+
type: "node",
|
|
693
|
+
tag: "p",
|
|
694
|
+
children: ['Test & <special> "quotes" \'apostrophe\'']
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const result = await renderPageSSR(pageData);
|
|
699
|
+
|
|
700
|
+
expect(result.html).toContain("&");
|
|
701
|
+
expect(result.html).toContain("<special>");
|
|
702
|
+
expect(result.html).toContain(""quotes"");
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("should escape nested malicious content", async () => {
|
|
706
|
+
const pageData: JSONPage = {
|
|
707
|
+
root: {
|
|
708
|
+
type: "node",
|
|
709
|
+
tag: "div",
|
|
710
|
+
children: [
|
|
711
|
+
{
|
|
712
|
+
type: "node",
|
|
713
|
+
tag: "p",
|
|
714
|
+
children: ["<script>document.cookie</script>"]
|
|
715
|
+
}
|
|
716
|
+
]
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const result = await renderPageSSR(pageData);
|
|
721
|
+
|
|
722
|
+
expect(result.html).not.toContain("<script>document");
|
|
723
|
+
expect(result.html).toContain("<script>");
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
describe("Meta tag sanitization", () => {
|
|
728
|
+
test("should escape HTML in page title", () => {
|
|
729
|
+
const meta = {
|
|
730
|
+
title: "Test <script>alert('xss')</script> Page"
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const tags = generateMetaTags(meta);
|
|
734
|
+
|
|
735
|
+
expect(tags).toContain("<title>Test <script>");
|
|
736
|
+
expect(tags).not.toContain("<script>alert");
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test("should escape HTML in meta description", () => {
|
|
740
|
+
const meta = {
|
|
741
|
+
description: 'Description with "quotes" and <tags>'
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
const tags = generateMetaTags(meta);
|
|
745
|
+
|
|
746
|
+
expect(tags).toContain(""quotes"");
|
|
747
|
+
expect(tags).toContain("<tags>");
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
test("should escape HTML in Open Graph tags", () => {
|
|
751
|
+
const meta = {
|
|
752
|
+
ogTitle: "<script>alert('og')</script>",
|
|
753
|
+
ogDescription: "Description <img src=x onerror=alert(1)>"
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
const tags = generateMetaTags(meta);
|
|
757
|
+
|
|
758
|
+
// Script tags should be escaped
|
|
759
|
+
expect(tags).not.toContain("<script>alert");
|
|
760
|
+
expect(tags).toContain("<script>");
|
|
761
|
+
// IMG tag should be escaped, preventing onerror from executing
|
|
762
|
+
expect(tags).not.toContain("<img src=x");
|
|
763
|
+
expect(tags).toContain("<img");
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
describe("Attribute sanitization", () => {
|
|
768
|
+
test("should not allow malicious attributes through style", async () => {
|
|
769
|
+
const pageData: JSONPage = {
|
|
770
|
+
root: {
|
|
771
|
+
type: "node",
|
|
772
|
+
tag: "div",
|
|
773
|
+
style: {
|
|
774
|
+
base: {
|
|
775
|
+
// Attempting to inject via style properties
|
|
776
|
+
color: 'red; background-image: url("javascript:alert(1)")'
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
const result = await renderPageSSR(pageData);
|
|
783
|
+
|
|
784
|
+
// Style should be processed safely via utility classes
|
|
785
|
+
expect(result.html).toBeDefined();
|
|
786
|
+
expect(result.html).toContain("<div");
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
test("should handle unicode escape sequences safely", async () => {
|
|
790
|
+
const pageData: JSONPage = {
|
|
791
|
+
root: {
|
|
792
|
+
type: "node",
|
|
793
|
+
tag: "div",
|
|
794
|
+
children: ["\u003cscript\u003ealert('unicode')\u003c/script\u003e"]
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const result = await renderPageSSR(pageData);
|
|
799
|
+
|
|
800
|
+
// Unicode-encoded script tags should still be escaped
|
|
801
|
+
expect(result.html).not.toContain("<script>alert");
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
describe("Component prop sanitization", () => {
|
|
806
|
+
test("should sanitize component props with HTML content", async () => {
|
|
807
|
+
const cardComponent: ComponentDefinition = {
|
|
808
|
+
type: "Card",
|
|
809
|
+
component: {
|
|
810
|
+
interface: {
|
|
811
|
+
title: { type: "string", default: "" }
|
|
812
|
+
},
|
|
813
|
+
structure: {
|
|
814
|
+
type: "node",
|
|
815
|
+
tag: "div",
|
|
816
|
+
children: [
|
|
817
|
+
{
|
|
818
|
+
type: "node",
|
|
819
|
+
tag: "h3",
|
|
820
|
+
children: ["{{title}}"]
|
|
821
|
+
}
|
|
822
|
+
]
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
const pageData: JSONPage = {
|
|
828
|
+
root: {
|
|
829
|
+
type: "component",
|
|
830
|
+
component: "Card",
|
|
831
|
+
props: {
|
|
832
|
+
title: "<script>alert('props')</script>"
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const globalComponents = {
|
|
838
|
+
Card: cardComponent
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
const result = await renderPageSSR(pageData, globalComponents);
|
|
842
|
+
|
|
843
|
+
// Script tags should be escaped when rendering
|
|
844
|
+
expect(result.html).not.toContain("<script>alert");
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
describe("Image sanitization", () => {
|
|
849
|
+
test("should escape src attribute in image nodes", async () => {
|
|
850
|
+
const pageData: JSONPage = {
|
|
851
|
+
root: {
|
|
852
|
+
type: "image",
|
|
853
|
+
tag: "img",
|
|
854
|
+
src: 'javascript:alert("img")',
|
|
855
|
+
alt: "test"
|
|
856
|
+
} as any
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
const result = await renderPageSSR(pageData);
|
|
860
|
+
|
|
861
|
+
// Implementation should handle this safely
|
|
862
|
+
expect(result.html).toBeDefined();
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
test("should escape alt attribute in image nodes", async () => {
|
|
866
|
+
const pageData: JSONPage = {
|
|
867
|
+
root: {
|
|
868
|
+
type: "image",
|
|
869
|
+
tag: "img",
|
|
870
|
+
src: "/test.jpg",
|
|
871
|
+
alt: '"><script>alert("alt")</script><img x="'
|
|
872
|
+
} as any
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
const result = await renderPageSSR(pageData);
|
|
876
|
+
|
|
877
|
+
// Alt should be escaped
|
|
878
|
+
expect(result.html).not.toContain('<script>alert("alt")');
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
describe("Embed content sanitization", () => {
|
|
883
|
+
test("should sanitize embed HTML with DOMPurify", async () => {
|
|
884
|
+
const pageData: JSONPage = {
|
|
885
|
+
root: {
|
|
886
|
+
type: "embed",
|
|
887
|
+
tag: "div",
|
|
888
|
+
html: '<div onclick="alert(1)"><script>alert(2)</script><p>safe</p></div>'
|
|
889
|
+
} as any
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
const result = await renderPageSSR(pageData);
|
|
893
|
+
|
|
894
|
+
// onclick should be stripped
|
|
895
|
+
expect(result.html).not.toContain('onclick="alert');
|
|
896
|
+
// script tags should be stripped
|
|
897
|
+
expect(result.html).not.toContain('<script>alert');
|
|
898
|
+
// Safe content should remain
|
|
899
|
+
expect(result.html).toContain("safe");
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
test("should allow safe SVG in embeds", async () => {
|
|
903
|
+
const pageData: JSONPage = {
|
|
904
|
+
root: {
|
|
905
|
+
type: "embed",
|
|
906
|
+
tag: "div",
|
|
907
|
+
html: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="red"/></svg>'
|
|
908
|
+
} as any
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
const result = await renderPageSSR(pageData);
|
|
912
|
+
|
|
913
|
+
// Safe SVG should be preserved
|
|
914
|
+
expect(result.html).toContain("<svg");
|
|
915
|
+
expect(result.html).toContain("<circle");
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
describe("SSR Renderer - generateSSRHTML", () => {
|
|
921
|
+
test("should generate complete HTML document", async () => {
|
|
922
|
+
const pageData: JSONPage = {
|
|
923
|
+
meta: {
|
|
924
|
+
title: "Test Page"
|
|
925
|
+
},
|
|
926
|
+
root: {
|
|
927
|
+
type: "node",
|
|
928
|
+
tag: "div",
|
|
929
|
+
children: ["Hello World"]
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
const html = await generateSSRHTML(pageData, {}, "/", "");
|
|
934
|
+
|
|
935
|
+
expect(html).toContain("<!DOCTYPE html>");
|
|
936
|
+
expect(html).toContain("<html");
|
|
937
|
+
expect(html).toContain("<head");
|
|
938
|
+
expect(html).toContain("<body");
|
|
939
|
+
expect(html).toContain('<div id="root">');
|
|
940
|
+
expect(html).toContain("Hello World");
|
|
941
|
+
expect(html).toContain("Test Page");
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
test("should include CSS in style tag", async () => {
|
|
945
|
+
const pageData: JSONPage = {
|
|
946
|
+
root: {
|
|
947
|
+
type: "node",
|
|
948
|
+
tag: "div",
|
|
949
|
+
style: {
|
|
950
|
+
base: {
|
|
951
|
+
color: "red"
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
const html = await generateSSRHTML(pageData);
|
|
958
|
+
|
|
959
|
+
// HTML should be generated with styles (either inline or in style tag)
|
|
960
|
+
expect(html).toContain("<div");
|
|
961
|
+
expect(html).toBeDefined();
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
test("should include client script", async () => {
|
|
965
|
+
const pageData: JSONPage = {
|
|
966
|
+
root: {
|
|
967
|
+
type: "node",
|
|
968
|
+
tag: "div"
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
const html = await generateSSRHTML(pageData, {}, "/", "", false);
|
|
973
|
+
|
|
974
|
+
expect(html).toContain('src="/client-router.tsx"');
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
test("should use built bundle when specified", async () => {
|
|
978
|
+
const pageData: JSONPage = {
|
|
979
|
+
root: {
|
|
980
|
+
type: "node",
|
|
981
|
+
tag: "div"
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
const html = await generateSSRHTML(pageData, {}, "/", "", true);
|
|
986
|
+
|
|
987
|
+
// When useBuiltBundle is true, implementation generates static HTML without client script
|
|
988
|
+
expect(html).toBeDefined();
|
|
989
|
+
expect(html).toContain("<div");
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
test("should include viewport meta tag", async () => {
|
|
993
|
+
const pageData: JSONPage = {
|
|
994
|
+
root: {
|
|
995
|
+
type: "node",
|
|
996
|
+
tag: "div"
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
const html = await generateSSRHTML(pageData);
|
|
1001
|
+
|
|
1002
|
+
expect(html).toContain('name="viewport"');
|
|
1003
|
+
expect(html).toContain('content="width=device-width, initial-scale=1.0"');
|
|
1004
|
+
});
|
|
1005
|
+
});
|