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,261 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import { deepClone, wait, isEditableElement, getParentPathFromTreePath, normalizeChildrenArray } from './utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* utils Tests
|
|
6
|
+
* Tests shared utility functions
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
describe('utils', () => {
|
|
10
|
+
describe('deepClone', () => {
|
|
11
|
+
test('should clone simple object', () => {
|
|
12
|
+
const obj = { a: 1, b: 'test' };
|
|
13
|
+
const cloned = deepClone(obj);
|
|
14
|
+
expect(cloned).toEqual(obj);
|
|
15
|
+
expect(cloned).not.toBe(obj);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should clone nested object', () => {
|
|
19
|
+
const obj = {
|
|
20
|
+
a: 1,
|
|
21
|
+
b: {
|
|
22
|
+
c: 2,
|
|
23
|
+
d: {
|
|
24
|
+
e: 3,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const cloned = deepClone(obj);
|
|
29
|
+
expect(cloned).toEqual(obj);
|
|
30
|
+
expect(cloned).not.toBe(obj);
|
|
31
|
+
expect(cloned.b).not.toBe(obj.b);
|
|
32
|
+
expect(cloned.b.d).not.toBe(obj.b.d);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should clone arrays', () => {
|
|
36
|
+
const arr = [1, 2, 3, 4];
|
|
37
|
+
const cloned = deepClone(arr);
|
|
38
|
+
expect(cloned).toEqual(arr);
|
|
39
|
+
expect(cloned).not.toBe(arr);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should clone nested arrays', () => {
|
|
43
|
+
const arr = [1, [2, 3], [4, [5, 6]]];
|
|
44
|
+
const cloned = deepClone(arr);
|
|
45
|
+
expect(cloned).toEqual(arr);
|
|
46
|
+
expect(cloned).not.toBe(arr);
|
|
47
|
+
expect(cloned[1]).not.toBe(arr[1]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('should clone mixed object and array', () => {
|
|
51
|
+
const obj = {
|
|
52
|
+
arr: [1, 2, 3],
|
|
53
|
+
nested: {
|
|
54
|
+
arr2: [4, 5],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const cloned = deepClone(obj);
|
|
58
|
+
expect(cloned).toEqual(obj);
|
|
59
|
+
expect(cloned.arr).not.toBe(obj.arr);
|
|
60
|
+
expect(cloned.nested.arr2).not.toBe(obj.nested.arr2);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should handle null values', () => {
|
|
64
|
+
const obj = { a: null, b: 'test' };
|
|
65
|
+
const cloned = deepClone(obj);
|
|
66
|
+
expect(cloned).toEqual(obj);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should handle boolean values', () => {
|
|
70
|
+
const obj = { a: true, b: false };
|
|
71
|
+
const cloned = deepClone(obj);
|
|
72
|
+
expect(cloned).toEqual(obj);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('should handle number values', () => {
|
|
76
|
+
const obj = { a: 0, b: -1, c: 3.14 };
|
|
77
|
+
const cloned = deepClone(obj);
|
|
78
|
+
expect(cloned).toEqual(obj);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('should handle empty object', () => {
|
|
82
|
+
const obj = {};
|
|
83
|
+
const cloned = deepClone(obj);
|
|
84
|
+
expect(cloned).toEqual(obj);
|
|
85
|
+
expect(cloned).not.toBe(obj);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('should handle empty array', () => {
|
|
89
|
+
const arr: any[] = [];
|
|
90
|
+
const cloned = deepClone(arr);
|
|
91
|
+
expect(cloned).toEqual(arr);
|
|
92
|
+
expect(cloned).not.toBe(arr);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('wait', () => {
|
|
97
|
+
test('should wait for specified milliseconds', async () => {
|
|
98
|
+
const start = Date.now();
|
|
99
|
+
await wait(50);
|
|
100
|
+
const elapsed = Date.now() - start;
|
|
101
|
+
expect(elapsed).toBeGreaterThanOrEqual(45); // Allow some tolerance
|
|
102
|
+
expect(elapsed).toBeLessThan(100);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('should wait for zero milliseconds', async () => {
|
|
106
|
+
const start = Date.now();
|
|
107
|
+
await wait(0);
|
|
108
|
+
const elapsed = Date.now() - start;
|
|
109
|
+
expect(elapsed).toBeLessThan(10);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('should work with multiple waits', async () => {
|
|
113
|
+
const start = Date.now();
|
|
114
|
+
await wait(20);
|
|
115
|
+
await wait(20);
|
|
116
|
+
const elapsed = Date.now() - start;
|
|
117
|
+
expect(elapsed).toBeGreaterThanOrEqual(35);
|
|
118
|
+
expect(elapsed).toBeLessThan(60);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('isEditableElement', () => {
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
// Clear any focused element before each test
|
|
125
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
126
|
+
document.activeElement.blur();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('should return false when no element is focused', () => {
|
|
131
|
+
expect(isEditableElement()).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('should return true for input element', () => {
|
|
135
|
+
const input = document.createElement('input');
|
|
136
|
+
document.body.appendChild(input);
|
|
137
|
+
input.focus();
|
|
138
|
+
expect(isEditableElement()).toBe(true);
|
|
139
|
+
document.body.removeChild(input);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('should return true for textarea element', () => {
|
|
143
|
+
const textarea = document.createElement('textarea');
|
|
144
|
+
document.body.appendChild(textarea);
|
|
145
|
+
textarea.focus();
|
|
146
|
+
expect(isEditableElement()).toBe(true);
|
|
147
|
+
document.body.removeChild(textarea);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('should return true for contenteditable element', () => {
|
|
151
|
+
const div = document.createElement('div');
|
|
152
|
+
div.setAttribute('contenteditable', 'true');
|
|
153
|
+
document.body.appendChild(div);
|
|
154
|
+
div.focus();
|
|
155
|
+
expect(isEditableElement()).toBe(true);
|
|
156
|
+
document.body.removeChild(div);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('should return false for non-editable element', () => {
|
|
160
|
+
const div = document.createElement('div');
|
|
161
|
+
document.body.appendChild(div);
|
|
162
|
+
div.focus();
|
|
163
|
+
expect(isEditableElement()).toBe(false);
|
|
164
|
+
document.body.removeChild(div);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('getParentPathFromTreePath', () => {
|
|
169
|
+
test('should return parent path from comma-separated string path', () => {
|
|
170
|
+
expect(getParentPathFromTreePath('0,0')).toBe('root_0');
|
|
171
|
+
expect(getParentPathFromTreePath('0,0,1')).toBe('root_0_children_0');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('should return parent path from array path', () => {
|
|
175
|
+
expect(getParentPathFromTreePath([0, 0])).toBe('root_0');
|
|
176
|
+
expect(getParentPathFromTreePath([0, 0, 1])).toBe('root_0_children_0');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('should return root for root path', () => {
|
|
180
|
+
expect(getParentPathFromTreePath([0])).toBe('root_0');
|
|
181
|
+
expect(getParentPathFromTreePath('0')).toBe('root_0');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('should handle custom root path', () => {
|
|
185
|
+
expect(getParentPathFromTreePath([0], 'custom_root')).toBe('custom_root');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('should handle deep nesting', () => {
|
|
189
|
+
expect(getParentPathFromTreePath([0, 0, 1, 2])).toBe('root_0_children_0_children_1');
|
|
190
|
+
expect(getParentPathFromTreePath([0, 1, 2, 3, 4])).toBe('root_0_children_1_children_2_children_3');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('should handle string format deep nesting', () => {
|
|
194
|
+
expect(getParentPathFromTreePath('0,0,1,2')).toBe('root_0_children_0_children_1');
|
|
195
|
+
expect(getParentPathFromTreePath('0,1,2,3,4')).toBe('root_0_children_1_children_2_children_3');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('normalizeChildrenArray', () => {
|
|
200
|
+
test('should convert undefined children to empty array', () => {
|
|
201
|
+
const node: any = {};
|
|
202
|
+
normalizeChildrenArray(node);
|
|
203
|
+
expect(node.children).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('should keep existing array unchanged', () => {
|
|
207
|
+
const node = { children: [1, 2, 3] };
|
|
208
|
+
normalizeChildrenArray(node);
|
|
209
|
+
expect(node.children).toEqual([1, 2, 3]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('should wrap single child in array', () => {
|
|
213
|
+
const node: any = { children: 'single' };
|
|
214
|
+
normalizeChildrenArray(node);
|
|
215
|
+
expect(node.children).toEqual(['single']);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('should wrap single object in array', () => {
|
|
219
|
+
const child = { element: 'div' };
|
|
220
|
+
const node: any = { children: child };
|
|
221
|
+
normalizeChildrenArray(node);
|
|
222
|
+
expect(node.children).toEqual([child]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('should handle empty array', () => {
|
|
226
|
+
const node = { children: [] };
|
|
227
|
+
normalizeChildrenArray(node);
|
|
228
|
+
expect(node.children).toEqual([]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('should mutate node directly', () => {
|
|
232
|
+
const node: any = { children: 'single' };
|
|
233
|
+
const original = node;
|
|
234
|
+
normalizeChildrenArray(node);
|
|
235
|
+
expect(node).toBe(original);
|
|
236
|
+
expect(node.children).toEqual(['single']);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('should handle node with no children property', () => {
|
|
240
|
+
const node: any = { element: 'div' };
|
|
241
|
+
normalizeChildrenArray(node);
|
|
242
|
+
expect(node.children).toEqual([]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('should handle complex children array', () => {
|
|
246
|
+
const node = {
|
|
247
|
+
children: [
|
|
248
|
+
{ element: 'div' },
|
|
249
|
+
{ element: 'span' },
|
|
250
|
+
'text',
|
|
251
|
+
],
|
|
252
|
+
};
|
|
253
|
+
normalizeChildrenArray(node);
|
|
254
|
+
expect(node.children).toEqual([
|
|
255
|
+
{ element: 'div' },
|
|
256
|
+
{ element: 'span' },
|
|
257
|
+
'text',
|
|
258
|
+
]);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility functions used across the codebase
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Path } from './pathArrayUtils';
|
|
6
|
+
import { stringToPath, pathToLegacyString, getParentPath, isRootPath as isRootPathArray } from './pathArrayUtils';
|
|
7
|
+
import { ROOT_0_STRING } from './pathArrayUtils';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Deep clone an object using JSON serialization
|
|
11
|
+
* @param obj - The object to clone
|
|
12
|
+
* @returns A deep clone of the object
|
|
13
|
+
*/
|
|
14
|
+
export function deepClone<T>(obj: T): T {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(JSON.stringify(obj)) as T;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Wait for a specified amount of time
|
|
24
|
+
* @param ms - Milliseconds to wait
|
|
25
|
+
*/
|
|
26
|
+
export function wait(ms: number): Promise<void> {
|
|
27
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if the currently focused element is editable (input, textarea, or contenteditable)
|
|
32
|
+
* @returns true if the active element is editable
|
|
33
|
+
*/
|
|
34
|
+
export function isEditableElement(): boolean {
|
|
35
|
+
const activeElement = document.activeElement;
|
|
36
|
+
if (!activeElement) return false;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
activeElement.tagName === 'INPUT' ||
|
|
40
|
+
activeElement.tagName === 'TEXTAREA' ||
|
|
41
|
+
activeElement.hasAttribute('contenteditable') ||
|
|
42
|
+
(activeElement as HTMLElement).isContentEditable
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract the parent path from a tree path
|
|
48
|
+
* Example: root_0_children_0 -> root_0, root_0_children_0_children_1 -> root_0_children_0
|
|
49
|
+
* Also accepts array paths: [0,0] -> "root_0", [0,0,1] -> "root_0_children_0"
|
|
50
|
+
*
|
|
51
|
+
* @param treePath - The tree path (string or Path array)
|
|
52
|
+
* @param rootPath - The root path constant (default: 'root_0')
|
|
53
|
+
* @returns The parent path, or rootPath if no parent found
|
|
54
|
+
*/
|
|
55
|
+
export function getParentPathFromTreePath(treePath: string | Path, rootPath: string = ROOT_0_STRING): string {
|
|
56
|
+
// Convert to array if needed
|
|
57
|
+
const pathArray = typeof treePath === 'string' ? stringToPath(treePath) : treePath;
|
|
58
|
+
|
|
59
|
+
// Handle root paths
|
|
60
|
+
if (isRootPathArray(pathArray)) {
|
|
61
|
+
return rootPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Use array operations to get parent path
|
|
65
|
+
const parentArray = getParentPath(pathArray);
|
|
66
|
+
return pathToLegacyString(parentArray);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Normalize children property to always be an array
|
|
71
|
+
* Mutates the node object directly (for performance in large trees)
|
|
72
|
+
* Handles cases where children might be undefined, a single item, or already an array
|
|
73
|
+
* @param node - The node object with a children property (will be mutated)
|
|
74
|
+
*/
|
|
75
|
+
export function normalizeChildrenArray(node: { children?: any[] | any }): void {
|
|
76
|
+
if (!node.children) {
|
|
77
|
+
node.children = [];
|
|
78
|
+
} else if (!Array.isArray(node.children)) {
|
|
79
|
+
// Single child - wrap in array
|
|
80
|
+
node.children = [node.children];
|
|
81
|
+
}
|
|
82
|
+
// If already an array, no change needed
|
|
83
|
+
}
|
|
84
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prop Validator Tests
|
|
3
|
+
* Critical path tests for prop validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect } from 'bun:test';
|
|
7
|
+
import { validateComponentProps } from './propValidator';
|
|
8
|
+
import type { PropDefinition } from '../types';
|
|
9
|
+
|
|
10
|
+
describe('Prop Validator', () => {
|
|
11
|
+
describe('Valid props pass validation', () => {
|
|
12
|
+
test('string prop with valid value', () => {
|
|
13
|
+
const propDefs: Record<string, PropDefinition> = {
|
|
14
|
+
title: { type: 'string', default: 'Hello' },
|
|
15
|
+
};
|
|
16
|
+
const passedProps = { title: 'World' };
|
|
17
|
+
|
|
18
|
+
const result = validateComponentProps(propDefs, passedProps);
|
|
19
|
+
|
|
20
|
+
expect(result.valid).toBe(true);
|
|
21
|
+
expect(result.props.title).toBe('World');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('number prop with valid value', () => {
|
|
25
|
+
const propDefs: Record<string, PropDefinition> = {
|
|
26
|
+
count: { type: 'number', default: 0 },
|
|
27
|
+
};
|
|
28
|
+
const passedProps = { count: 42 };
|
|
29
|
+
|
|
30
|
+
const result = validateComponentProps(propDefs, passedProps);
|
|
31
|
+
|
|
32
|
+
expect(result.valid).toBe(true);
|
|
33
|
+
expect(result.props.count).toBe(42);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('boolean prop with valid value', () => {
|
|
37
|
+
const propDefs: Record<string, PropDefinition> = {
|
|
38
|
+
enabled: { type: 'boolean', default: false },
|
|
39
|
+
};
|
|
40
|
+
const passedProps = { enabled: true };
|
|
41
|
+
|
|
42
|
+
const result = validateComponentProps(propDefs, passedProps);
|
|
43
|
+
|
|
44
|
+
expect(result.valid).toBe(true);
|
|
45
|
+
expect(result.props.enabled).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('select prop with valid option', () => {
|
|
49
|
+
const propDefs: Record<string, PropDefinition> = {
|
|
50
|
+
variant: {
|
|
51
|
+
type: 'select',
|
|
52
|
+
default: 'primary',
|
|
53
|
+
options: ['primary', 'secondary', 'tertiary'],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
const passedProps = { variant: 'secondary' };
|
|
57
|
+
|
|
58
|
+
const result = validateComponentProps(propDefs, passedProps);
|
|
59
|
+
|
|
60
|
+
expect(result.valid).toBe(true);
|
|
61
|
+
expect(result.props.variant).toBe('secondary');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('Invalid types are caught', () => {
|
|
66
|
+
test('string prop with number value (coerced to string)', () => {
|
|
67
|
+
const propDefs: Record<string, PropDefinition> = {
|
|
68
|
+
title: { type: 'string', default: 'Hello' },
|
|
69
|
+
};
|
|
70
|
+
const passedProps = { title: 123 };
|
|
71
|
+
|
|
72
|
+
const result = validateComponentProps(propDefs, passedProps);
|
|
73
|
+
|
|
74
|
+
// Coercion makes this valid (123 -> "123")
|
|
75
|
+
expect(result.valid).toBe(true);
|
|
76
|
+
expect(result.props.title).toBe('123');
|
|
77
|
+
expect(typeof result.props.title).toBe('string');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('number prop with string value (non-numeric)', () => {
|
|
81
|
+
const propDefs: Record<string, PropDefinition> = {
|
|
82
|
+
count: { type: 'number', default: 0 },
|
|
83
|
+
};
|
|
84
|
+
const passedProps = { count: 'not-a-number' };
|
|
85
|
+
|
|
86
|
+
const result = validateComponentProps(propDefs, passedProps);
|
|
87
|
+
|
|
88
|
+
expect(result.valid).toBe(false);
|
|
89
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
90
|
+
// Should fall back to default
|
|
91
|
+
expect(result.props.count).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('select prop with invalid option', () => {
|
|
95
|
+
const propDefs: Record<string, PropDefinition> = {
|
|
96
|
+
variant: {
|
|
97
|
+
type: 'select',
|
|
98
|
+
default: 'primary',
|
|
99
|
+
options: ['primary', 'secondary'],
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const passedProps = { variant: 'invalid' };
|
|
103
|
+
|
|
104
|
+
const result = validateComponentProps(propDefs, passedProps);
|
|
105
|
+
|
|
106
|
+
expect(result.valid).toBe(false);
|
|
107
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
108
|
+
expect(result.errors[0].message).toContain('not in allowed options');
|
|
109
|
+
// Should fall back to default
|
|
110
|
+
expect(result.props.variant).toBe('primary');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('Type coercion works correctly', () => {
|
|
115
|
+
test('coerces string number to number', () => {
|
|
116
|
+
const propDefs: Record<string, PropDefinition> = {
|
|
117
|
+
count: { type: 'number', default: 0 },
|
|
118
|
+
};
|
|
119
|
+
const passedProps = { count: '123' };
|
|
120
|
+
|
|
121
|
+
const result = validateComponentProps(propDefs, passedProps);
|
|
122
|
+
|
|
123
|
+
expect(result.valid).toBe(true);
|
|
124
|
+
expect(result.props.count).toBe(123);
|
|
125
|
+
expect(typeof result.props.count).toBe('number');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('coerces string boolean to boolean', () => {
|
|
129
|
+
const propDefs: Record<string, PropDefinition> = {
|
|
130
|
+
enabled: { type: 'boolean', default: false },
|
|
131
|
+
};
|
|
132
|
+
const passedProps = { enabled: 'true' };
|
|
133
|
+
|
|
134
|
+
const result = validateComponentProps(propDefs, passedProps);
|
|
135
|
+
|
|
136
|
+
expect(result.valid).toBe(true);
|
|
137
|
+
expect(result.props.enabled).toBe(true);
|
|
138
|
+
expect(typeof result.props.enabled).toBe('boolean');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('Backward compatibility maintained', () => {
|
|
143
|
+
test('unknown props allowed but logged', () => {
|
|
144
|
+
const propDefs: Record<string, PropDefinition> = {
|
|
145
|
+
title: { type: 'string', default: 'Hello' },
|
|
146
|
+
};
|
|
147
|
+
const passedProps = {
|
|
148
|
+
title: 'World',
|
|
149
|
+
unknownProp: 'allowed',
|
|
150
|
+
anotherUnknown: 123,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const result = validateComponentProps(propDefs, passedProps);
|
|
154
|
+
|
|
155
|
+
// Should be valid (unknown props don't fail validation)
|
|
156
|
+
expect(result.valid).toBe(true);
|
|
157
|
+
// Unknown props should be preserved
|
|
158
|
+
expect(result.props.unknownProp).toBe('allowed');
|
|
159
|
+
expect(result.props.anotherUnknown).toBe(123);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('missing props use defaults', () => {
|
|
163
|
+
const propDefs: Record<string, PropDefinition> = {
|
|
164
|
+
title: { type: 'string', default: 'Hello' },
|
|
165
|
+
count: { type: 'number', default: 42 },
|
|
166
|
+
};
|
|
167
|
+
const passedProps = {};
|
|
168
|
+
|
|
169
|
+
const result = validateComponentProps(propDefs, passedProps);
|
|
170
|
+
|
|
171
|
+
expect(result.valid).toBe(true);
|
|
172
|
+
expect(result.props.title).toBe('Hello');
|
|
173
|
+
expect(result.props.count).toBe(42);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
|