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,660 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, mock } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
evaluateTemplate,
|
|
4
|
+
resolveStyleMapping,
|
|
5
|
+
processStructure,
|
|
6
|
+
normalizeStyle
|
|
7
|
+
} from "./templateEngine";
|
|
8
|
+
import type { ComponentNode, ComponentDefinition, StructuredComponentDefinition, StyleMapping, TemplateContext } from "../shared/types";
|
|
9
|
+
|
|
10
|
+
describe("Template Engine - evaluateTemplate", () => {
|
|
11
|
+
describe("Complex template expressions", () => {
|
|
12
|
+
test("should evaluate nested property access", () => {
|
|
13
|
+
const template = "{{variants[variant].background}}";
|
|
14
|
+
const context = {
|
|
15
|
+
variants: {
|
|
16
|
+
primary: { background: "#0070f3" },
|
|
17
|
+
secondary: { background: "#f3f4f6" }
|
|
18
|
+
},
|
|
19
|
+
variant: "primary"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const result = evaluateTemplate(template, context);
|
|
23
|
+
expect(result).toBe("#0070f3");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("should evaluate ternary operators", () => {
|
|
27
|
+
const template = "{{disabled ? '0.5' : '1'}}";
|
|
28
|
+
const context = { disabled: true };
|
|
29
|
+
|
|
30
|
+
const result = evaluateTemplate(template, context);
|
|
31
|
+
expect(result).toBe("0.5");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("should evaluate ternary operators with false condition", () => {
|
|
35
|
+
const template = "{{disabled ? '0.5' : '1'}}";
|
|
36
|
+
const context = { disabled: false };
|
|
37
|
+
|
|
38
|
+
const result = evaluateTemplate(template, context);
|
|
39
|
+
expect(result).toBe("1");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("should evaluate mathematical operations - addition", () => {
|
|
43
|
+
const template = "{{count + 1}}";
|
|
44
|
+
const context = { count: 5 };
|
|
45
|
+
|
|
46
|
+
const result = evaluateTemplate(template, context);
|
|
47
|
+
expect(result).toBe(6);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("should evaluate mathematical operations - multiplication", () => {
|
|
51
|
+
const template = "{{price * 1.1}}";
|
|
52
|
+
const context = { price: 100 };
|
|
53
|
+
|
|
54
|
+
const result = evaluateTemplate(template, context);
|
|
55
|
+
expect(result).toBeCloseTo(110, 5);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("should evaluate string concatenation", () => {
|
|
59
|
+
const template = "{{'Hello ' + name}}";
|
|
60
|
+
const context = { name: "World" };
|
|
61
|
+
|
|
62
|
+
const result = evaluateTemplate(template, context);
|
|
63
|
+
expect(result).toBe("Hello World");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("should evaluate logical AND operator", () => {
|
|
67
|
+
const template = "{{isActive && 'active'}}";
|
|
68
|
+
const context = { isActive: true };
|
|
69
|
+
|
|
70
|
+
const result = evaluateTemplate(template, context);
|
|
71
|
+
expect(result).toBe("active");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("should evaluate logical AND operator with false", () => {
|
|
75
|
+
const template = "{{isActive && 'active'}}";
|
|
76
|
+
const context = { isActive: false };
|
|
77
|
+
|
|
78
|
+
const result = evaluateTemplate(template, context);
|
|
79
|
+
expect(result).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("should evaluate complex nested expressions", () => {
|
|
83
|
+
const template = "{{user.profile.name || 'Anonymous'}}";
|
|
84
|
+
const context = { user: { profile: { name: "John" } } };
|
|
85
|
+
|
|
86
|
+
const result = evaluateTemplate(template, context);
|
|
87
|
+
expect(result).toBe("John");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("should evaluate expressions with fallback", () => {
|
|
91
|
+
const template = "{{user.profile.name || 'Anonymous'}}";
|
|
92
|
+
const context = { user: { profile: {} } };
|
|
93
|
+
|
|
94
|
+
const result = evaluateTemplate(template, context);
|
|
95
|
+
expect(result).toBe("Anonymous");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("Edge cases", () => {
|
|
100
|
+
test("should return original string if not a template", () => {
|
|
101
|
+
const template = "plain text";
|
|
102
|
+
const context = { name: "John" };
|
|
103
|
+
|
|
104
|
+
const result = evaluateTemplate(template, context);
|
|
105
|
+
expect(result).toBe("plain text");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("should return template string on invalid expression", () => {
|
|
109
|
+
const template = "{{invalid.expression}}";
|
|
110
|
+
const context = {};
|
|
111
|
+
|
|
112
|
+
const result = evaluateTemplate(template, context);
|
|
113
|
+
expect(result).toBe(template); // Should return original template on error
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("should return template string on syntax error", () => {
|
|
117
|
+
const template = "{{count +}}"; // Missing operand
|
|
118
|
+
const context = { count: 5 };
|
|
119
|
+
|
|
120
|
+
const result = evaluateTemplate(template, context);
|
|
121
|
+
expect(result).toBe(template); // Should return original template on error
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("should handle missing context variables", () => {
|
|
125
|
+
const template = "{{undefinedVar}}";
|
|
126
|
+
const context = {};
|
|
127
|
+
|
|
128
|
+
const result = evaluateTemplate(template, context);
|
|
129
|
+
expect(result).toBe(template); // Should return original template when var is undefined
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("should handle non-string templates", () => {
|
|
133
|
+
const template = 123 as unknown as string;
|
|
134
|
+
const context = {};
|
|
135
|
+
|
|
136
|
+
const result = evaluateTemplate(template, context);
|
|
137
|
+
expect(result).toBe(123); // Should pass through non-strings
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("should handle empty template expression", () => {
|
|
141
|
+
const template = "{{}}";
|
|
142
|
+
const context = {};
|
|
143
|
+
|
|
144
|
+
const result = evaluateTemplate(template, context);
|
|
145
|
+
expect(result).toBe(template); // Invalid expression
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("should handle template with whitespace", () => {
|
|
149
|
+
const template = "{{ name }}";
|
|
150
|
+
const context = { name: "John" };
|
|
151
|
+
|
|
152
|
+
const result = evaluateTemplate(template, context);
|
|
153
|
+
expect(result).toBe("John"); // Should trim whitespace
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("Template Engine - resolveStyleMapping", () => {
|
|
159
|
+
describe("Valid style mappings", () => {
|
|
160
|
+
test("should resolve prop-based style mapping", () => {
|
|
161
|
+
const mapping: StyleMapping = {
|
|
162
|
+
_mapping: true,
|
|
163
|
+
prop: "variant",
|
|
164
|
+
values: {
|
|
165
|
+
primary: "#0070f3",
|
|
166
|
+
secondary: "#f3f4f6"
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const props = { variant: "primary" };
|
|
170
|
+
|
|
171
|
+
const result = resolveStyleMapping(mapping, props);
|
|
172
|
+
expect(result).toBe("#0070f3");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("should resolve style mapping for secondary variant", () => {
|
|
176
|
+
const mapping: StyleMapping = {
|
|
177
|
+
_mapping: true,
|
|
178
|
+
prop: "variant",
|
|
179
|
+
values: {
|
|
180
|
+
primary: "#0070f3",
|
|
181
|
+
secondary: "#f3f4f6"
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const props = { variant: "secondary" };
|
|
185
|
+
|
|
186
|
+
const result = resolveStyleMapping(mapping, props);
|
|
187
|
+
expect(result).toBe("#f3f4f6");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("should resolve numeric style mapping values", () => {
|
|
191
|
+
const mapping: StyleMapping = {
|
|
192
|
+
_mapping: true,
|
|
193
|
+
prop: "size",
|
|
194
|
+
values: {
|
|
195
|
+
small: 12,
|
|
196
|
+
large: 24
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const props = { size: "small" };
|
|
200
|
+
|
|
201
|
+
const result = resolveStyleMapping(mapping, props);
|
|
202
|
+
expect(result).toBe(12);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("Missing or invalid mappings", () => {
|
|
207
|
+
test("should return undefined for missing prop value", () => {
|
|
208
|
+
const mapping: StyleMapping = {
|
|
209
|
+
_mapping: true,
|
|
210
|
+
prop: "variant",
|
|
211
|
+
values: {
|
|
212
|
+
primary: "#0070f3",
|
|
213
|
+
secondary: "#f3f4f6"
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const props = {}; // variant not provided
|
|
217
|
+
|
|
218
|
+
const result = resolveStyleMapping(mapping, props);
|
|
219
|
+
expect(result).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("should return undefined for null prop value", () => {
|
|
223
|
+
const mapping: StyleMapping = {
|
|
224
|
+
_mapping: true,
|
|
225
|
+
prop: "variant",
|
|
226
|
+
values: {
|
|
227
|
+
primary: "#0070f3"
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const props = { variant: null };
|
|
231
|
+
|
|
232
|
+
const result = resolveStyleMapping(mapping, props);
|
|
233
|
+
expect(result).toBeUndefined();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("should return undefined for prop value not in mapping", () => {
|
|
237
|
+
const mapping: StyleMapping = {
|
|
238
|
+
_mapping: true,
|
|
239
|
+
prop: "variant",
|
|
240
|
+
values: {
|
|
241
|
+
primary: "#0070f3"
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const props = { variant: "invalid" };
|
|
245
|
+
|
|
246
|
+
const result = resolveStyleMapping(mapping, props);
|
|
247
|
+
expect(result).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("should return undefined for invalid mapping object (no _mapping)", () => {
|
|
251
|
+
const mapping = {
|
|
252
|
+
prop: "variant",
|
|
253
|
+
values: { primary: "#0070f3" }
|
|
254
|
+
};
|
|
255
|
+
const props = { variant: "primary" };
|
|
256
|
+
|
|
257
|
+
const result = resolveStyleMapping(mapping, props);
|
|
258
|
+
expect(result).toBeUndefined();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("should return undefined for non-object mapping", () => {
|
|
262
|
+
const mapping = "not an object";
|
|
263
|
+
const props = { variant: "primary" };
|
|
264
|
+
|
|
265
|
+
const result = resolveStyleMapping(mapping, props);
|
|
266
|
+
expect(result).toBeUndefined();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("should return undefined for null mapping", () => {
|
|
270
|
+
const mapping = null;
|
|
271
|
+
const props = { variant: "primary" };
|
|
272
|
+
|
|
273
|
+
const result = resolveStyleMapping(mapping, props);
|
|
274
|
+
expect(result).toBeUndefined();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("should return undefined for mapping with missing prop field", () => {
|
|
278
|
+
const mapping = {
|
|
279
|
+
_mapping: true,
|
|
280
|
+
values: { primary: "#0070f3" }
|
|
281
|
+
};
|
|
282
|
+
const props = { variant: "primary" };
|
|
283
|
+
|
|
284
|
+
const result = resolveStyleMapping(mapping, props);
|
|
285
|
+
expect(result).toBeUndefined();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("should return undefined for mapping with missing values field", () => {
|
|
289
|
+
const mapping = {
|
|
290
|
+
_mapping: true,
|
|
291
|
+
prop: "variant"
|
|
292
|
+
};
|
|
293
|
+
const props = { variant: "primary" };
|
|
294
|
+
|
|
295
|
+
const result = resolveStyleMapping(mapping, props);
|
|
296
|
+
expect(result).toBeUndefined();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("Template Engine - processStructure", () => {
|
|
302
|
+
const mockComponentDef: StructuredComponentDefinition = {
|
|
303
|
+
interface: {},
|
|
304
|
+
structure: {}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Helper to create TemplateContext
|
|
308
|
+
const createContext = (props: Record<string, unknown>, componentDef = mockComponentDef): TemplateContext => ({
|
|
309
|
+
props,
|
|
310
|
+
componentDef
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe("String processing", () => {
|
|
314
|
+
test("should process template strings in structure", () => {
|
|
315
|
+
const structure = "{{name}}";
|
|
316
|
+
const props = { name: "John" };
|
|
317
|
+
|
|
318
|
+
const result = processStructure(structure, createContext(props));
|
|
319
|
+
expect(result).toBe("John");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("should process non-template strings", () => {
|
|
323
|
+
const structure = "plain text";
|
|
324
|
+
const props = {};
|
|
325
|
+
|
|
326
|
+
const result = processStructure(structure, createContext(props));
|
|
327
|
+
expect(result).toBe("plain text");
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe("Array processing", () => {
|
|
332
|
+
test("should process array of strings", () => {
|
|
333
|
+
const structure = ["Hello", "{{name}}", "World"] as any;
|
|
334
|
+
const props = { name: "John" };
|
|
335
|
+
|
|
336
|
+
const result = processStructure(structure, createContext(props));
|
|
337
|
+
expect(Array.isArray(result)).toBe(true);
|
|
338
|
+
const resultArray = result as (string | ComponentNode)[];
|
|
339
|
+
expect(resultArray).toEqual(["Hello", "John", "World"]);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("should process array of component nodes", () => {
|
|
343
|
+
const structure: ComponentNode[] = [
|
|
344
|
+
{ type: "node", tag: "div", children: ["{{text}}"] },
|
|
345
|
+
{ type: "node", tag: "span", children: ["Static"] }
|
|
346
|
+
];
|
|
347
|
+
const props = { text: "Dynamic" };
|
|
348
|
+
|
|
349
|
+
const result = processStructure(structure, createContext(props));
|
|
350
|
+
expect(Array.isArray(result)).toBe(true);
|
|
351
|
+
const resultArray = result as ComponentNode[];
|
|
352
|
+
expect(resultArray[0].children).toEqual(["Dynamic"]);
|
|
353
|
+
expect(resultArray[1].children).toEqual(["Static"]);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("should filter null values from arrays", () => {
|
|
357
|
+
const structure = ["Hello", null, "World"] as any;
|
|
358
|
+
const props = {};
|
|
359
|
+
|
|
360
|
+
const result = processStructure(structure, createContext(props));
|
|
361
|
+
expect(Array.isArray(result)).toBe(true);
|
|
362
|
+
const resultArray = result as (string | ComponentNode)[];
|
|
363
|
+
expect(resultArray.length).toBe(2);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe("Component node processing", () => {
|
|
368
|
+
test("should process simple component node", () => {
|
|
369
|
+
const structure: ComponentNode = {
|
|
370
|
+
type: "node",
|
|
371
|
+
tag: "div",
|
|
372
|
+
children: ["Hello World"]
|
|
373
|
+
};
|
|
374
|
+
const props = {};
|
|
375
|
+
|
|
376
|
+
const result = processStructure(structure, createContext(props));
|
|
377
|
+
expect(typeof result === 'object' && !Array.isArray(result)).toBe(true);
|
|
378
|
+
const node = result as ComponentNode;
|
|
379
|
+
expect(node.tag).toBe("div");
|
|
380
|
+
expect(node.children).toEqual(["Hello World"]);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("should process component node with template in children", () => {
|
|
384
|
+
const structure: ComponentNode = {
|
|
385
|
+
type: "node",
|
|
386
|
+
tag: "div",
|
|
387
|
+
children: ["{{greeting}}"]
|
|
388
|
+
};
|
|
389
|
+
const props = { greeting: "Hello" };
|
|
390
|
+
|
|
391
|
+
const result = processStructure(structure, createContext(props));
|
|
392
|
+
const node = result as ComponentNode;
|
|
393
|
+
expect(node.children).toEqual(["Hello"]);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("should process template in tag property", () => {
|
|
397
|
+
const structure: ComponentNode = {
|
|
398
|
+
type: "node",
|
|
399
|
+
tag: "h{{size}}",
|
|
400
|
+
children: ["Heading"]
|
|
401
|
+
};
|
|
402
|
+
const props = { size: 1 };
|
|
403
|
+
|
|
404
|
+
const result = processStructure(structure, createContext(props));
|
|
405
|
+
const node = result as ComponentNode;
|
|
406
|
+
expect(node.tag).toBe("h1");
|
|
407
|
+
expect(node.children).toEqual(["Heading"]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("should process template in tag with string prop", () => {
|
|
411
|
+
const structure: ComponentNode = {
|
|
412
|
+
type: "node",
|
|
413
|
+
tag: "h{{level}}",
|
|
414
|
+
children: ["Title"]
|
|
415
|
+
};
|
|
416
|
+
const props = { level: "2" };
|
|
417
|
+
|
|
418
|
+
const result = processStructure(structure, createContext(props));
|
|
419
|
+
const node = result as ComponentNode;
|
|
420
|
+
expect(node.tag).toBe("h2");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("should process nested component structures", () => {
|
|
424
|
+
const structure: ComponentNode = {
|
|
425
|
+
type: "node",
|
|
426
|
+
tag: "div",
|
|
427
|
+
children: [
|
|
428
|
+
{
|
|
429
|
+
type: "node",
|
|
430
|
+
tag: "span",
|
|
431
|
+
children: ["{{text}}"]
|
|
432
|
+
}
|
|
433
|
+
]
|
|
434
|
+
};
|
|
435
|
+
const props = { text: "Nested" };
|
|
436
|
+
|
|
437
|
+
const result = processStructure(structure, createContext(props));
|
|
438
|
+
const node = result as ComponentNode;
|
|
439
|
+
expect(node.children?.[0]).toBeDefined();
|
|
440
|
+
const child = node.children![0] as ComponentNode;
|
|
441
|
+
expect(child.children).toEqual(["Nested"]);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe("Style mapping resolution in structure", () => {
|
|
446
|
+
test("should resolve style mappings in flat style objects", () => {
|
|
447
|
+
const structure: ComponentNode = {
|
|
448
|
+
type: "node",
|
|
449
|
+
tag: "button",
|
|
450
|
+
style: {
|
|
451
|
+
base: {
|
|
452
|
+
background: {
|
|
453
|
+
_mapping: true,
|
|
454
|
+
prop: "variant",
|
|
455
|
+
values: {
|
|
456
|
+
primary: "#0070f3",
|
|
457
|
+
secondary: "#f3f4f6"
|
|
458
|
+
}
|
|
459
|
+
} as StyleMapping,
|
|
460
|
+
color: "#000"
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
const props = { variant: "primary" };
|
|
465
|
+
|
|
466
|
+
const result = processStructure(structure, createContext(props));
|
|
467
|
+
const node = result as ComponentNode;
|
|
468
|
+
const baseStyle = (node.style as any)?.base;
|
|
469
|
+
expect(baseStyle?.background).toBe("#0070f3");
|
|
470
|
+
expect(baseStyle?.color).toBe("#000");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("should resolve multiple style mappings", () => {
|
|
474
|
+
const structure: ComponentNode = {
|
|
475
|
+
type: "node",
|
|
476
|
+
tag: "button",
|
|
477
|
+
style: {
|
|
478
|
+
base: {
|
|
479
|
+
background: {
|
|
480
|
+
_mapping: true,
|
|
481
|
+
prop: "variant",
|
|
482
|
+
values: { primary: "blue", secondary: "gray" }
|
|
483
|
+
} as StyleMapping,
|
|
484
|
+
color: {
|
|
485
|
+
_mapping: true,
|
|
486
|
+
prop: "variant",
|
|
487
|
+
values: { primary: "white", secondary: "black" }
|
|
488
|
+
} as StyleMapping
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
const props = { variant: "primary" };
|
|
493
|
+
|
|
494
|
+
const result = processStructure(structure, createContext(props));
|
|
495
|
+
const node = result as ComponentNode;
|
|
496
|
+
const baseStyle = (node.style as any)?.base;
|
|
497
|
+
expect(baseStyle?.background).toBe("blue");
|
|
498
|
+
expect(baseStyle?.color).toBe("white");
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
describe("Template evaluation in style objects", () => {
|
|
503
|
+
test("should evaluate templates in style values", () => {
|
|
504
|
+
const structure: ComponentNode = {
|
|
505
|
+
type: "node",
|
|
506
|
+
tag: "div",
|
|
507
|
+
style: {
|
|
508
|
+
base: {
|
|
509
|
+
opacity: "{{disabled ? '0.5' : '1'}}",
|
|
510
|
+
fontSize: "{{size}}px"
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
const props = { disabled: true, size: 16 };
|
|
515
|
+
|
|
516
|
+
const result = processStructure(structure, createContext(props));
|
|
517
|
+
const node = result as ComponentNode;
|
|
518
|
+
const baseStyle = (node.style as any)?.base;
|
|
519
|
+
expect(baseStyle).toBeDefined();
|
|
520
|
+
expect(baseStyle?.opacity).toBe("0.5");
|
|
521
|
+
expect(baseStyle?.fontSize).toBe("16px");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe("Edge cases and error handling", () => {
|
|
526
|
+
test("should return null for null structure", () => {
|
|
527
|
+
const result = processStructure(null, createContext({}));
|
|
528
|
+
expect(result).toBeNull();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("should return null for undefined structure", () => {
|
|
532
|
+
const result = processStructure(undefined, createContext({}));
|
|
533
|
+
expect(result).toBeNull();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("should handle numbers in structure", () => {
|
|
537
|
+
const result = processStructure(42, createContext({}));
|
|
538
|
+
expect(result).toBe(42);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test("should handle empty objects", () => {
|
|
542
|
+
const structure: ComponentNode = {
|
|
543
|
+
type: "node",
|
|
544
|
+
tag: "div",
|
|
545
|
+
children: []
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const result = processStructure(structure, createContext({}));
|
|
549
|
+
const node = result as ComponentNode;
|
|
550
|
+
expect(node.tag).toBe("div");
|
|
551
|
+
expect(node.type).toBe("node");
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("should handle component definition context in templates", () => {
|
|
555
|
+
const componentDef = {
|
|
556
|
+
interface: {},
|
|
557
|
+
structure: {},
|
|
558
|
+
variants: {
|
|
559
|
+
primary: { background: "blue" }
|
|
560
|
+
}
|
|
561
|
+
} as StructuredComponentDefinition & { variants?: Record<string, any> };
|
|
562
|
+
const structure = "{{variants.primary.background}}";
|
|
563
|
+
const props = {};
|
|
564
|
+
|
|
565
|
+
const result = processStructure(structure, createContext(props, componentDef));
|
|
566
|
+
expect(result).toBe("blue");
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
describe("Integration with Button.json example", () => {
|
|
571
|
+
test("should process Button component structure with style mappings", () => {
|
|
572
|
+
// Based on components/Button.json
|
|
573
|
+
const structure: ComponentNode = {
|
|
574
|
+
type: "node",
|
|
575
|
+
tag: "button",
|
|
576
|
+
style: {
|
|
577
|
+
base: {
|
|
578
|
+
padding: {
|
|
579
|
+
_mapping: true,
|
|
580
|
+
prop: "variant",
|
|
581
|
+
values: {
|
|
582
|
+
primary: "16px 34px",
|
|
583
|
+
secondary: "16px 34px"
|
|
584
|
+
}
|
|
585
|
+
} as StyleMapping,
|
|
586
|
+
background: {
|
|
587
|
+
_mapping: true,
|
|
588
|
+
prop: "variant",
|
|
589
|
+
values: {
|
|
590
|
+
primary: "blue",
|
|
591
|
+
secondary: "#f3f4f6"
|
|
592
|
+
}
|
|
593
|
+
} as StyleMapping,
|
|
594
|
+
color: {
|
|
595
|
+
_mapping: true,
|
|
596
|
+
prop: "variant",
|
|
597
|
+
values: {
|
|
598
|
+
primary: "white",
|
|
599
|
+
secondary: "#374151"
|
|
600
|
+
}
|
|
601
|
+
} as StyleMapping,
|
|
602
|
+
borderRadius: "0px",
|
|
603
|
+
fontSize: "15px"
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
children: "{{children}}"
|
|
607
|
+
} as unknown as ComponentNode;
|
|
608
|
+
const props = { variant: "primary", children: "Click me" };
|
|
609
|
+
|
|
610
|
+
const result = processStructure(structure, createContext(props));
|
|
611
|
+
const node = result as ComponentNode;
|
|
612
|
+
|
|
613
|
+
const baseStyle = (node.style as any)?.base;
|
|
614
|
+
expect(baseStyle?.background).toBe("blue");
|
|
615
|
+
expect(baseStyle?.color).toBe("white");
|
|
616
|
+
expect(baseStyle?.padding).toBe("16px 34px");
|
|
617
|
+
// processStructure converts string children template to evaluated result (array)
|
|
618
|
+
if (Array.isArray(node.children)) {
|
|
619
|
+
expect(node.children).toEqual(["Click me"]);
|
|
620
|
+
} else {
|
|
621
|
+
// If not array, it might still be the template string or evaluated string
|
|
622
|
+
expect(node.children).toBeDefined();
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
describe("Template Engine - normalizeStyle", () => {
|
|
629
|
+
test("should normalize flat style objects", () => {
|
|
630
|
+
const style = {
|
|
631
|
+
color: "red",
|
|
632
|
+
fontSize: "16px"
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const result = normalizeStyle(style);
|
|
636
|
+
expect(result).toEqual(style);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("should normalize responsive style objects using 'all' strategy", () => {
|
|
640
|
+
const style = {
|
|
641
|
+
base: { color: "red" },
|
|
642
|
+
tablet: { color: "blue" },
|
|
643
|
+
mobile: { color: "green" }
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const result = normalizeStyle(style);
|
|
647
|
+
expect(result).toBeDefined();
|
|
648
|
+
expect(result?.color).toBe("green"); // 'all' strategy merges all, mobile wins
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test("should return null for null style", () => {
|
|
652
|
+
const result = normalizeStyle(null);
|
|
653
|
+
expect(result).toBeNull();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("should return null for undefined style", () => {
|
|
657
|
+
const result = normalizeStyle(undefined);
|
|
658
|
+
expect(result).toBeNull();
|
|
659
|
+
});
|
|
660
|
+
});
|