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,677 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, mock } from "bun:test";
|
|
2
|
+
import { ComponentBuilder } from "./ComponentBuilder";
|
|
3
|
+
import { ComponentRegistry } from "../componentRegistry";
|
|
4
|
+
import { ElementRegistry } from "../elementRegistry";
|
|
5
|
+
import type { ComponentNode } from "../../shared/types";
|
|
6
|
+
import type { HighlightManager } from "../highlightManager";
|
|
7
|
+
import { NODE_TYPE } from "../../shared/constants";
|
|
8
|
+
import { createMockHighlightManager, createMockElementRegistry } from "../../test-utils/mocks";
|
|
9
|
+
|
|
10
|
+
// Note: Using typed mocks from test-utils/mocks instead of inline 'as any' casts
|
|
11
|
+
|
|
12
|
+
describe("ComponentBuilder", () => {
|
|
13
|
+
let componentRegistry: ComponentRegistry;
|
|
14
|
+
let hoverHighlightManager: HighlightManager;
|
|
15
|
+
let elementRegistry: ElementRegistry;
|
|
16
|
+
let builder: ComponentBuilder;
|
|
17
|
+
let registeredPaths: Array<{ path: string; tag?: string }>;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
componentRegistry = new ComponentRegistry();
|
|
21
|
+
hoverHighlightManager = createMockHighlightManager();
|
|
22
|
+
registeredPaths = [];
|
|
23
|
+
|
|
24
|
+
// Create mock element registry with path tracking
|
|
25
|
+
elementRegistry = createMockElementRegistry();
|
|
26
|
+
elementRegistry.register = mock((path: any) => {
|
|
27
|
+
// Capture the path being registered (convert array to string)
|
|
28
|
+
const pathStr = Array.isArray(path) ? path.join(',') : String(path);
|
|
29
|
+
registeredPaths.push({ path: pathStr });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
builder = new ComponentBuilder({
|
|
33
|
+
componentRegistry,
|
|
34
|
+
hoverHighlightManager,
|
|
35
|
+
elementRegistry,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("buildComponent - Basic Cases", () => {
|
|
40
|
+
test("should return null for null node", () => {
|
|
41
|
+
const result = builder.buildComponent({ node: null });
|
|
42
|
+
expect(result).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("should return null for undefined node", () => {
|
|
46
|
+
const result = builder.buildComponent({ node: undefined });
|
|
47
|
+
expect(result).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("should return string for string node", () => {
|
|
51
|
+
const result = builder.buildComponent({ node: "Hello" });
|
|
52
|
+
expect(result).toBe("Hello");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("should return number for number node", () => {
|
|
56
|
+
const result = builder.buildComponent({ node: 42 });
|
|
57
|
+
expect(result).toBe(42);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("should return array for array of nodes", () => {
|
|
61
|
+
const nodes: ComponentNode[] = [
|
|
62
|
+
{ type: "node", tag: "div", children: ["Hello"] },
|
|
63
|
+
{ type: "node", tag: "span", children: ["World"] },
|
|
64
|
+
];
|
|
65
|
+
const result = builder.buildComponent({ node: nodes });
|
|
66
|
+
expect(Array.isArray(result)).toBe(true);
|
|
67
|
+
expect((result as any[]).length).toBe(2);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("should return ReactElement for HTML node", () => {
|
|
71
|
+
const node: ComponentNode = {
|
|
72
|
+
type: "node",
|
|
73
|
+
tag: "div",
|
|
74
|
+
children: ["Test"],
|
|
75
|
+
};
|
|
76
|
+
const result = builder.buildComponent({ node });
|
|
77
|
+
expect(result).not.toBeNull();
|
|
78
|
+
expect(typeof result).toBe("object");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("should handle HTML node with empty tag gracefully", () => {
|
|
82
|
+
const node: ComponentNode = {
|
|
83
|
+
type: "node",
|
|
84
|
+
tag: "",
|
|
85
|
+
children: [],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const result = builder.buildComponent({ node });
|
|
89
|
+
|
|
90
|
+
// Implementation returns a div element for empty tag (graceful fallback)
|
|
91
|
+
expect(result).not.toBeNull();
|
|
92
|
+
expect(typeof result).toBe("object");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("buildComponent - Component Instances", () => {
|
|
97
|
+
test("should handle non-existent component gracefully", () => {
|
|
98
|
+
const node: ComponentNode = {
|
|
99
|
+
type: "component",
|
|
100
|
+
component: "NonExistent",
|
|
101
|
+
};
|
|
102
|
+
const result = builder.buildComponent({ node });
|
|
103
|
+
// Returns error boundary or placeholder element for missing component
|
|
104
|
+
expect(result).not.toBeNull();
|
|
105
|
+
expect(typeof result).toBe("object");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("should build component instance with structure", () => {
|
|
109
|
+
const componentDef = {
|
|
110
|
+
component: {
|
|
111
|
+
interface: {},
|
|
112
|
+
structure: {
|
|
113
|
+
type: "node",
|
|
114
|
+
tag: "div",
|
|
115
|
+
children: ["Component Content"],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
componentRegistry.register("TestComponent", componentDef as any);
|
|
120
|
+
|
|
121
|
+
const node: ComponentNode = {
|
|
122
|
+
type: "component",
|
|
123
|
+
component: "TestComponent",
|
|
124
|
+
};
|
|
125
|
+
const result = builder.buildComponent({ node });
|
|
126
|
+
expect(result).not.toBeNull();
|
|
127
|
+
expect(typeof result).toBe("object");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("should handle component missing structure gracefully", () => {
|
|
131
|
+
// Test with component that has no component property at all
|
|
132
|
+
const componentDef = {
|
|
133
|
+
// Missing component property entirely
|
|
134
|
+
};
|
|
135
|
+
componentRegistry.register("BrokenComponent", componentDef as any);
|
|
136
|
+
|
|
137
|
+
const node: ComponentNode = {
|
|
138
|
+
type: "component",
|
|
139
|
+
component: "BrokenComponent",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const result = builder.buildComponent({ node });
|
|
143
|
+
|
|
144
|
+
// When componentDef.component is missing, it should return ErrorBoundary
|
|
145
|
+
expect(result).not.toBeNull();
|
|
146
|
+
expect(typeof result).toBe("object");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("should resolve props with defaults from interface", () => {
|
|
150
|
+
const componentDef = {
|
|
151
|
+
component: {
|
|
152
|
+
interface: {
|
|
153
|
+
title: {
|
|
154
|
+
type: "string",
|
|
155
|
+
default: "Default Title",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
structure: {
|
|
159
|
+
type: "node",
|
|
160
|
+
tag: "div",
|
|
161
|
+
children: [],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
componentRegistry.register("ComponentWithDefaults", componentDef as any);
|
|
166
|
+
|
|
167
|
+
const node: ComponentNode = {
|
|
168
|
+
type: "component",
|
|
169
|
+
component: "ComponentWithDefaults",
|
|
170
|
+
};
|
|
171
|
+
const result = builder.buildComponent({ node });
|
|
172
|
+
expect(result).not.toBeNull();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("should handle component building errors gracefully", () => {
|
|
176
|
+
// This test verifies error handling is in place
|
|
177
|
+
// We can't easily mock processStructure in Bun, so we test with a valid component
|
|
178
|
+
// Error handling is verified by the try-catch in buildComponent
|
|
179
|
+
const componentDef = {
|
|
180
|
+
component: {
|
|
181
|
+
interface: {},
|
|
182
|
+
structure: {
|
|
183
|
+
type: "node",
|
|
184
|
+
tag: "div",
|
|
185
|
+
children: [],
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
componentRegistry.register("ValidComponent", componentDef as any);
|
|
190
|
+
|
|
191
|
+
const node: ComponentNode = {
|
|
192
|
+
type: "component",
|
|
193
|
+
component: "ValidComponent",
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const result = builder.buildComponent({ node });
|
|
197
|
+
|
|
198
|
+
// Component should build successfully
|
|
199
|
+
expect(result).not.toBeNull();
|
|
200
|
+
expect(typeof result).toBe("object");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("buildComponent - Link Component", () => {
|
|
205
|
+
test("should render Link component as anchor tag", () => {
|
|
206
|
+
const node: ComponentNode = {
|
|
207
|
+
type: "node",
|
|
208
|
+
tag: "Link",
|
|
209
|
+
props: {
|
|
210
|
+
to: "/about",
|
|
211
|
+
},
|
|
212
|
+
children: ["Click me"],
|
|
213
|
+
};
|
|
214
|
+
const result = builder.buildComponent({ node });
|
|
215
|
+
expect(result).not.toBeNull();
|
|
216
|
+
expect(typeof result).toBe("object");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("should handle Link navigation when selecting mode is disabled", () => {
|
|
220
|
+
const node: ComponentNode = {
|
|
221
|
+
type: "node",
|
|
222
|
+
tag: "Link",
|
|
223
|
+
props: {
|
|
224
|
+
to: "/test",
|
|
225
|
+
},
|
|
226
|
+
children: [],
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Link component should render regardless of selecting mode
|
|
230
|
+
// The actual behavior is tested through integration tests
|
|
231
|
+
const result = builder.buildComponent({ node });
|
|
232
|
+
expect(result).not.toBeNull();
|
|
233
|
+
expect(typeof result).toBe("object");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("buildComponent - HTML Elements", () => {
|
|
238
|
+
test("should register element via ref callback", () => {
|
|
239
|
+
const node: ComponentNode = {
|
|
240
|
+
type: "node",
|
|
241
|
+
tag: "div",
|
|
242
|
+
children: [],
|
|
243
|
+
};
|
|
244
|
+
const result = builder.buildComponent({ node });
|
|
245
|
+
expect(result).not.toBeNull();
|
|
246
|
+
// Element registration happens via ref callback, which is called during React rendering
|
|
247
|
+
// We can't easily test this without rendering, but we verify the ref is set
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("should extract and merge styles", () => {
|
|
251
|
+
const node: ComponentNode = {
|
|
252
|
+
type: "node",
|
|
253
|
+
tag: "div",
|
|
254
|
+
style: {
|
|
255
|
+
base: {
|
|
256
|
+
color: "red",
|
|
257
|
+
fontSize: "16px",
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
children: [],
|
|
261
|
+
};
|
|
262
|
+
const result = builder.buildComponent({ node, viewportWidth: 1920 });
|
|
263
|
+
expect(result).not.toBeNull();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("should extract and merge attributes", () => {
|
|
267
|
+
const node: ComponentNode = {
|
|
268
|
+
type: "node",
|
|
269
|
+
tag: "div",
|
|
270
|
+
attributes: {
|
|
271
|
+
id: "test-id",
|
|
272
|
+
className: "test-class",
|
|
273
|
+
},
|
|
274
|
+
children: [],
|
|
275
|
+
};
|
|
276
|
+
const result = builder.buildComponent({ node });
|
|
277
|
+
expect(result).not.toBeNull();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("should filter out internal props", () => {
|
|
281
|
+
const node: ComponentNode = {
|
|
282
|
+
type: "node",
|
|
283
|
+
tag: "div",
|
|
284
|
+
props: {
|
|
285
|
+
type: "should-be-removed",
|
|
286
|
+
tag: "should-be-removed",
|
|
287
|
+
component: "should-be-removed",
|
|
288
|
+
children: "should-be-removed",
|
|
289
|
+
validProp: "should-remain",
|
|
290
|
+
},
|
|
291
|
+
children: [],
|
|
292
|
+
};
|
|
293
|
+
const result = builder.buildComponent({ node });
|
|
294
|
+
expect(result).not.toBeNull();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("should detect component root via __componentProps", () => {
|
|
298
|
+
const node: ComponentNode = {
|
|
299
|
+
type: "node",
|
|
300
|
+
tag: "div",
|
|
301
|
+
children: [],
|
|
302
|
+
};
|
|
303
|
+
const result = builder.buildComponent({
|
|
304
|
+
node,
|
|
305
|
+
customProps: {
|
|
306
|
+
__componentProps: { title: "Test" },
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
expect(result).not.toBeNull();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe("buildChildren", () => {
|
|
314
|
+
test("should return array for array of children", () => {
|
|
315
|
+
const children: ComponentNode[] = [
|
|
316
|
+
{ type: "node", tag: "div", children: ["Child 1"] },
|
|
317
|
+
{ type: "node", tag: "span", children: ["Child 2"] },
|
|
318
|
+
];
|
|
319
|
+
const result = builder.buildChildren(children, [0], null, 1920, null);
|
|
320
|
+
expect(Array.isArray(result)).toBe(true);
|
|
321
|
+
expect((result as any[]).length).toBe(2);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("should return single element for single child", () => {
|
|
325
|
+
const child: ComponentNode = {
|
|
326
|
+
type: "node",
|
|
327
|
+
tag: "div",
|
|
328
|
+
children: ["Single child"],
|
|
329
|
+
};
|
|
330
|
+
const result = builder.buildChildren(child, [0], null, 1920, null);
|
|
331
|
+
expect(result).not.toBeNull();
|
|
332
|
+
expect(Array.isArray(result)).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("should return null for undefined children", () => {
|
|
336
|
+
const result = builder.buildChildren(undefined, [0], null, 1920, null);
|
|
337
|
+
expect(result).toBeNull();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("should return empty array for empty array", () => {
|
|
341
|
+
const result = builder.buildChildren([], [0], null, 1920, null);
|
|
342
|
+
expect(Array.isArray(result)).toBe(true);
|
|
343
|
+
expect((result as any[]).length).toBe(0);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("should generate correct paths for children", () => {
|
|
347
|
+
const children: ComponentNode[] = [
|
|
348
|
+
{ type: "node", tag: "div", children: [] },
|
|
349
|
+
{ type: "node", tag: "span", children: [] },
|
|
350
|
+
];
|
|
351
|
+
const result = builder.buildChildren(children, [0, 1], null, 1920, null);
|
|
352
|
+
expect(Array.isArray(result)).toBe(true);
|
|
353
|
+
expect((result as any[]).length).toBe(2);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe("Helper Functions", () => {
|
|
358
|
+
test("getParentComponentNameForNestedComponent should prioritize componentContext", () => {
|
|
359
|
+
// This is tested indirectly through component building
|
|
360
|
+
// We can't directly test private methods, but we can verify behavior
|
|
361
|
+
const componentDef = {
|
|
362
|
+
component: {
|
|
363
|
+
interface: {},
|
|
364
|
+
structure: {
|
|
365
|
+
type: "node",
|
|
366
|
+
tag: "div",
|
|
367
|
+
children: [],
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
componentRegistry.register("ParentComponent", componentDef as any);
|
|
372
|
+
|
|
373
|
+
const node: ComponentNode = {
|
|
374
|
+
type: "component",
|
|
375
|
+
component: "ParentComponent",
|
|
376
|
+
};
|
|
377
|
+
const result = builder.buildComponent({
|
|
378
|
+
node,
|
|
379
|
+
componentContext: "ContextComponent",
|
|
380
|
+
parentComponentName: "ParentName",
|
|
381
|
+
});
|
|
382
|
+
expect(result).not.toBeNull();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("getEffectiveParentComponentName should use componentContext when available", () => {
|
|
386
|
+
// Tested indirectly through component building
|
|
387
|
+
const node: ComponentNode = {
|
|
388
|
+
type: "node",
|
|
389
|
+
tag: "div",
|
|
390
|
+
children: [],
|
|
391
|
+
};
|
|
392
|
+
const result = builder.buildComponent({
|
|
393
|
+
node,
|
|
394
|
+
componentContext: "ContextComponent",
|
|
395
|
+
parentComponentName: "ParentName",
|
|
396
|
+
});
|
|
397
|
+
expect(result).not.toBeNull();
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe("Path Assignment for Slot Markers", () => {
|
|
402
|
+
/**
|
|
403
|
+
* KNOWN ISSUE: Slot markers consume path indices incorrectly
|
|
404
|
+
*
|
|
405
|
+
* When buildChildren iterates over children, slot markers get assigned path indices
|
|
406
|
+
* like regular children. This causes sibling components after a slot to have incorrect
|
|
407
|
+
* paths relative to the instance children that replace the slot.
|
|
408
|
+
*
|
|
409
|
+
* Example: Structure div > div > [slot, Card]
|
|
410
|
+
* - Slot marker gets path [0,0,0]
|
|
411
|
+
* - Card gets path [0,0,1]
|
|
412
|
+
* - Instance child replacing slot also gets [0,0,0]
|
|
413
|
+
* - Result: Instance child and Card appear as siblings at different indices,
|
|
414
|
+
* but semantically they should be handled differently.
|
|
415
|
+
*
|
|
416
|
+
* TODO: Fix path assignment to skip slot markers when assigning indices,
|
|
417
|
+
* or adjust indices after slot expansion.
|
|
418
|
+
*/
|
|
419
|
+
test.skip("slot markers should not consume path indices", () => {
|
|
420
|
+
const sectionDef = {
|
|
421
|
+
component: {
|
|
422
|
+
interface: {},
|
|
423
|
+
structure: {
|
|
424
|
+
type: "node",
|
|
425
|
+
tag: "div",
|
|
426
|
+
children: [
|
|
427
|
+
{
|
|
428
|
+
type: "node",
|
|
429
|
+
tag: "div",
|
|
430
|
+
children: [
|
|
431
|
+
{ type: "slot" },
|
|
432
|
+
{ type: "component", component: "Card" },
|
|
433
|
+
],
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const cardDef = {
|
|
441
|
+
component: {
|
|
442
|
+
interface: {},
|
|
443
|
+
structure: {
|
|
444
|
+
type: "node",
|
|
445
|
+
tag: "div",
|
|
446
|
+
children: ["Card Content"],
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
componentRegistry.register("Section", sectionDef as any);
|
|
452
|
+
componentRegistry.register("Card", cardDef as any);
|
|
453
|
+
|
|
454
|
+
const pageNode: ComponentNode = {
|
|
455
|
+
type: "component",
|
|
456
|
+
component: "Section",
|
|
457
|
+
children: [{ type: "node", tag: "p", children: ["Instance Child"] }],
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const result = builder.buildComponent({ node: pageNode });
|
|
461
|
+
expect(result).not.toBeNull();
|
|
462
|
+
|
|
463
|
+
// When fixed, this test should verify that:
|
|
464
|
+
// 1. Instance child (replacing slot) gets path [0,0,0]
|
|
465
|
+
// 2. Card gets path [0,0,0] (same as slot was, since slot doesn't consume an index)
|
|
466
|
+
// OR Card gets a path that properly reflects its position relative to expanded slot content
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("should build component with slot marker correctly", () => {
|
|
470
|
+
// Button.json: button > children
|
|
471
|
+
const buttonDef = {
|
|
472
|
+
component: {
|
|
473
|
+
interface: {
|
|
474
|
+
children: { type: "string", default: "Click me" },
|
|
475
|
+
},
|
|
476
|
+
structure: {
|
|
477
|
+
type: "node",
|
|
478
|
+
tag: "button",
|
|
479
|
+
children: [{ type: "slot" }],
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
componentRegistry.register("Button", buttonDef as any);
|
|
485
|
+
|
|
486
|
+
const pageNode: ComponentNode = {
|
|
487
|
+
type: "component",
|
|
488
|
+
component: "Button",
|
|
489
|
+
children: [{ type: "node", tag: "span", children: ["Submit"] }],
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const result = builder.buildComponent({ node: pageNode });
|
|
493
|
+
expect(result).not.toBeNull();
|
|
494
|
+
expect(typeof result).toBe("object");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test("should build nested children with multiple siblings after slot", () => {
|
|
498
|
+
// Structure: div > div > [slot, Card, Button]
|
|
499
|
+
|
|
500
|
+
const sectionDef = {
|
|
501
|
+
component: {
|
|
502
|
+
interface: {},
|
|
503
|
+
structure: {
|
|
504
|
+
type: "node",
|
|
505
|
+
tag: "div",
|
|
506
|
+
children: [
|
|
507
|
+
{
|
|
508
|
+
type: "node",
|
|
509
|
+
tag: "div",
|
|
510
|
+
children: [
|
|
511
|
+
{ type: "slot" },
|
|
512
|
+
{ type: "component", component: "Card" },
|
|
513
|
+
{ type: "component", component: "Button" },
|
|
514
|
+
],
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const cardDef = {
|
|
522
|
+
component: {
|
|
523
|
+
interface: {},
|
|
524
|
+
structure: {
|
|
525
|
+
type: "node",
|
|
526
|
+
tag: "div",
|
|
527
|
+
children: ["Card"],
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const buttonDef = {
|
|
533
|
+
component: {
|
|
534
|
+
interface: {},
|
|
535
|
+
structure: {
|
|
536
|
+
type: "node",
|
|
537
|
+
tag: "button",
|
|
538
|
+
children: ["Button"],
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
componentRegistry.register("Section", sectionDef as any);
|
|
544
|
+
componentRegistry.register("Card", cardDef as any);
|
|
545
|
+
componentRegistry.register("Button", buttonDef as any);
|
|
546
|
+
|
|
547
|
+
const pageNode: ComponentNode = {
|
|
548
|
+
type: "component",
|
|
549
|
+
component: "Section",
|
|
550
|
+
children: [{ type: "node", tag: "p", children: ["Content"] }],
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const result = builder.buildComponent({ node: pageNode });
|
|
554
|
+
expect(result).not.toBeNull();
|
|
555
|
+
expect(typeof result).toBe("object");
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
describe("Integration Tests", () => {
|
|
560
|
+
test("should build complex component tree", () => {
|
|
561
|
+
const cardDef = {
|
|
562
|
+
component: {
|
|
563
|
+
interface: {
|
|
564
|
+
title: { type: "string", default: "Card Title" },
|
|
565
|
+
},
|
|
566
|
+
structure: {
|
|
567
|
+
type: "node",
|
|
568
|
+
tag: "div",
|
|
569
|
+
children: [
|
|
570
|
+
{ type: "node", tag: "h2", children: ["{{title}}"] },
|
|
571
|
+
{ type: "slot" },
|
|
572
|
+
],
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
componentRegistry.register("Card", cardDef as any);
|
|
577
|
+
|
|
578
|
+
const pageNode: ComponentNode = {
|
|
579
|
+
type: "node",
|
|
580
|
+
tag: "div",
|
|
581
|
+
children: [
|
|
582
|
+
{
|
|
583
|
+
type: "component",
|
|
584
|
+
component: "Card",
|
|
585
|
+
props: { title: "My Card" },
|
|
586
|
+
children: [{ type: "node", tag: "p", children: ["Card content"] }],
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
};
|
|
590
|
+
const result = builder.buildComponent({ node: pageNode });
|
|
591
|
+
expect(result).not.toBeNull();
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("should handle nested component instances", () => {
|
|
595
|
+
const innerDef = {
|
|
596
|
+
component: {
|
|
597
|
+
interface: {},
|
|
598
|
+
structure: {
|
|
599
|
+
type: "node",
|
|
600
|
+
tag: "span",
|
|
601
|
+
children: ["Inner"],
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
componentRegistry.register("Inner", innerDef as any);
|
|
606
|
+
|
|
607
|
+
const outerDef = {
|
|
608
|
+
component: {
|
|
609
|
+
interface: {},
|
|
610
|
+
structure: {
|
|
611
|
+
type: "node",
|
|
612
|
+
tag: "div",
|
|
613
|
+
children: [
|
|
614
|
+
{ type: "component", component: "Inner" },
|
|
615
|
+
],
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
componentRegistry.register("Outer", outerDef as any);
|
|
620
|
+
|
|
621
|
+
const node: ComponentNode = {
|
|
622
|
+
type: "component",
|
|
623
|
+
component: "Outer",
|
|
624
|
+
};
|
|
625
|
+
const result = builder.buildComponent({ node });
|
|
626
|
+
expect(result).not.toBeNull();
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test("should propagate component context correctly", () => {
|
|
630
|
+
const componentDef = {
|
|
631
|
+
component: {
|
|
632
|
+
interface: {},
|
|
633
|
+
structure: {
|
|
634
|
+
type: "node",
|
|
635
|
+
tag: "div",
|
|
636
|
+
children: [
|
|
637
|
+
{ type: "node", tag: "span", children: ["Child"] },
|
|
638
|
+
],
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
componentRegistry.register("TestComponent", componentDef as any);
|
|
643
|
+
|
|
644
|
+
const node: ComponentNode = {
|
|
645
|
+
type: "component",
|
|
646
|
+
component: "TestComponent",
|
|
647
|
+
};
|
|
648
|
+
const result = builder.buildComponent({
|
|
649
|
+
node,
|
|
650
|
+
componentContext: "TestComponent",
|
|
651
|
+
});
|
|
652
|
+
expect(result).not.toBeNull();
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test("should respect viewportWidth for responsive styles", () => {
|
|
656
|
+
const node: ComponentNode = {
|
|
657
|
+
type: "node",
|
|
658
|
+
tag: "div",
|
|
659
|
+
style: {
|
|
660
|
+
base: { fontSize: "24px" },
|
|
661
|
+
tablet: { fontSize: "18px" },
|
|
662
|
+
mobile: { fontSize: "16px" },
|
|
663
|
+
},
|
|
664
|
+
children: [],
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
const resultDesktop = builder.buildComponent({ node, viewportWidth: 1920 });
|
|
668
|
+
const resultTablet = builder.buildComponent({ node, viewportWidth: 768 });
|
|
669
|
+
const resultMobile = builder.buildComponent({ node, viewportWidth: 375 });
|
|
670
|
+
|
|
671
|
+
expect(resultDesktop).not.toBeNull();
|
|
672
|
+
expect(resultTablet).not.toBeNull();
|
|
673
|
+
expect(resultMobile).not.toBeNull();
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|