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.
Files changed (231) hide show
  1. package/bin/cli.ts +281 -0
  2. package/build-static.ts +298 -0
  3. package/bunfig.toml +39 -0
  4. package/entries/client-router.tsx +111 -0
  5. package/entries/server-router.tsx +71 -0
  6. package/lib/client/ClientInitializer.test.ts +9 -0
  7. package/lib/client/ClientInitializer.test.ts.skip +92 -0
  8. package/lib/client/ClientInitializer.ts +60 -0
  9. package/lib/client/ErrorBoundary.test.tsx +595 -0
  10. package/lib/client/ErrorBoundary.tsx +230 -0
  11. package/lib/client/componentRegistry.test.ts +165 -0
  12. package/lib/client/componentRegistry.ts +18 -0
  13. package/lib/client/contexts/ThemeContext.tsx +73 -0
  14. package/lib/client/core/ComponentBuilder.test.ts +677 -0
  15. package/lib/client/core/ComponentBuilder.ts +660 -0
  16. package/lib/client/core/ComponentRenderer.test.tsx +176 -0
  17. package/lib/client/core/ComponentRenderer.tsx +83 -0
  18. package/lib/client/core/cmsTemplateProcessor.ts +129 -0
  19. package/lib/client/elementRegistry.ts +81 -0
  20. package/lib/client/hmr/HMRManager.tsx +179 -0
  21. package/lib/client/hmr/index.ts +5 -0
  22. package/lib/client/hmrWebSocket.test.ts +9 -0
  23. package/lib/client/hmrWebSocket.ts +250 -0
  24. package/lib/client/hooks/useColorVariables.test.ts +166 -0
  25. package/lib/client/hooks/useColorVariables.ts +249 -0
  26. package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
  27. package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
  28. package/lib/client/hydration/HydrationUtils.test.ts +154 -0
  29. package/lib/client/hydration/HydrationUtils.ts +35 -0
  30. package/lib/client/i18nConfigService.test.ts +74 -0
  31. package/lib/client/i18nConfigService.ts +78 -0
  32. package/lib/client/index.ts +56 -0
  33. package/lib/client/navigation.test.ts +441 -0
  34. package/lib/client/navigation.ts +23 -0
  35. package/lib/client/responsiveStyleResolver.test.ts +491 -0
  36. package/lib/client/responsiveStyleResolver.ts +184 -0
  37. package/lib/client/routing/RouteLoader.test.ts +635 -0
  38. package/lib/client/routing/RouteLoader.ts +347 -0
  39. package/lib/client/routing/Router.tsx +382 -0
  40. package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
  41. package/lib/client/scripts/ScriptExecutor.ts +171 -0
  42. package/lib/client/scripts/formHandler.ts +103 -0
  43. package/lib/client/styleProcessor.test.ts +126 -0
  44. package/lib/client/styleProcessor.ts +92 -0
  45. package/lib/client/styles/StyleInjector.test.ts +354 -0
  46. package/lib/client/styles/StyleInjector.ts +154 -0
  47. package/lib/client/templateEngine.test.ts +660 -0
  48. package/lib/client/templateEngine.ts +667 -0
  49. package/lib/client/theme.test.ts +173 -0
  50. package/lib/client/theme.ts +159 -0
  51. package/lib/client/utils/toast.ts +46 -0
  52. package/lib/server/createServer.ts +170 -0
  53. package/lib/server/cssGenerator.test.ts +172 -0
  54. package/lib/server/cssGenerator.ts +58 -0
  55. package/lib/server/fileWatcher.ts +134 -0
  56. package/lib/server/index.ts +55 -0
  57. package/lib/server/jsonLoader.test.ts +103 -0
  58. package/lib/server/jsonLoader.ts +350 -0
  59. package/lib/server/middleware/cors.test.ts +177 -0
  60. package/lib/server/middleware/cors.ts +69 -0
  61. package/lib/server/middleware/errorHandler.test.ts +208 -0
  62. package/lib/server/middleware/errorHandler.ts +63 -0
  63. package/lib/server/middleware/index.ts +9 -0
  64. package/lib/server/middleware/logger.test.ts +233 -0
  65. package/lib/server/middleware/logger.ts +99 -0
  66. package/lib/server/pageCache.test.ts +167 -0
  67. package/lib/server/pageCache.ts +97 -0
  68. package/lib/server/projectContext.ts +51 -0
  69. package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
  70. package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
  71. package/lib/server/providers/fileSystemPageProvider.ts +83 -0
  72. package/lib/server/routes/api/cms.test.ts +177 -0
  73. package/lib/server/routes/api/cms.ts +82 -0
  74. package/lib/server/routes/api/colors.ts +59 -0
  75. package/lib/server/routes/api/components.ts +70 -0
  76. package/lib/server/routes/api/config.test.ts +9 -0
  77. package/lib/server/routes/api/config.ts +28 -0
  78. package/lib/server/routes/api/core-routes.ts +182 -0
  79. package/lib/server/routes/api/functions.ts +170 -0
  80. package/lib/server/routes/api/index.ts +69 -0
  81. package/lib/server/routes/api/pages.ts +95 -0
  82. package/lib/server/routes/api/shared.test.ts +81 -0
  83. package/lib/server/routes/api/shared.ts +31 -0
  84. package/lib/server/routes/editor.test.ts +9 -0
  85. package/lib/server/routes/index.ts +104 -0
  86. package/lib/server/routes/pages.ts +161 -0
  87. package/lib/server/routes/static.ts +107 -0
  88. package/lib/server/services/ColorService.ts +193 -0
  89. package/lib/server/services/cmsService.test.ts +388 -0
  90. package/lib/server/services/cmsService.ts +296 -0
  91. package/lib/server/services/componentService.test.ts +276 -0
  92. package/lib/server/services/componentService.ts +346 -0
  93. package/lib/server/services/configService.ts +156 -0
  94. package/lib/server/services/fileWatcherService.ts +67 -0
  95. package/lib/server/services/index.ts +10 -0
  96. package/lib/server/services/pageService.test.ts +258 -0
  97. package/lib/server/services/pageService.ts +240 -0
  98. package/lib/server/ssrRenderer.test.ts +1005 -0
  99. package/lib/server/ssrRenderer.ts +878 -0
  100. package/lib/server/utilityClassGenerator.ts +11 -0
  101. package/lib/server/utils/index.ts +5 -0
  102. package/lib/server/utils/jsonLineMapper.test.ts +100 -0
  103. package/lib/server/utils/jsonLineMapper.ts +166 -0
  104. package/lib/server/validateStyleCoverage.test.ts +9 -0
  105. package/lib/server/validateStyleCoverage.ts +167 -0
  106. package/lib/server/websocketManager.test.ts +9 -0
  107. package/lib/server/websocketManager.ts +95 -0
  108. package/lib/shared/attributeNodeUtils.test.ts +152 -0
  109. package/lib/shared/attributeNodeUtils.ts +50 -0
  110. package/lib/shared/breakpoints.test.ts +166 -0
  111. package/lib/shared/breakpoints.ts +65 -0
  112. package/lib/shared/colorProperties.test.ts +111 -0
  113. package/lib/shared/colorProperties.ts +40 -0
  114. package/lib/shared/colorVariableUtils.test.ts +319 -0
  115. package/lib/shared/colorVariableUtils.ts +97 -0
  116. package/lib/shared/constants.test.ts +175 -0
  117. package/lib/shared/constants.ts +116 -0
  118. package/lib/shared/cssGeneration.ts +481 -0
  119. package/lib/shared/cssProperties.test.ts +252 -0
  120. package/lib/shared/cssProperties.ts +338 -0
  121. package/lib/shared/elementUtils.test.ts +245 -0
  122. package/lib/shared/elementUtils.ts +90 -0
  123. package/lib/shared/fontLoader.ts +97 -0
  124. package/lib/shared/i18n.test.ts +313 -0
  125. package/lib/shared/i18n.ts +286 -0
  126. package/lib/shared/index.ts +50 -0
  127. package/lib/shared/interfaces/contentProvider.test.ts +9 -0
  128. package/lib/shared/interfaces/contentProvider.ts +121 -0
  129. package/lib/shared/nodeUtils.test.ts +320 -0
  130. package/lib/shared/nodeUtils.ts +220 -0
  131. package/lib/shared/pathArrayUtils.test.ts +315 -0
  132. package/lib/shared/pathArrayUtils.ts +17 -0
  133. package/lib/shared/pathUtils.test.ts +260 -0
  134. package/lib/shared/pathUtils.ts +244 -0
  135. package/lib/shared/paths/Path.test.ts +74 -0
  136. package/lib/shared/paths/Path.ts +23 -0
  137. package/lib/shared/paths/PathConverter.test.ts +232 -0
  138. package/lib/shared/paths/PathConverter.ts +141 -0
  139. package/lib/shared/paths/PathUtils.ts +290 -0
  140. package/lib/shared/paths/PathValidator.test.ts +193 -0
  141. package/lib/shared/paths/PathValidator.ts +53 -0
  142. package/lib/shared/paths/index.ts +48 -0
  143. package/lib/shared/propResolver.test.ts +639 -0
  144. package/lib/shared/propResolver.ts +124 -0
  145. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
  146. package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
  147. package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
  148. package/lib/shared/registry/ClientRegistry.test.ts +26 -0
  149. package/lib/shared/registry/ClientRegistry.ts +15 -0
  150. package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
  151. package/lib/shared/registry/ComponentRegistry.ts +100 -0
  152. package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
  153. package/lib/shared/registry/NodeTypeManager.ts +94 -0
  154. package/lib/shared/registry/RegistryManager.test.ts +58 -0
  155. package/lib/shared/registry/RegistryManager.ts +60 -0
  156. package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
  157. package/lib/shared/registry/SSRRegistry.test.ts +26 -0
  158. package/lib/shared/registry/SSRRegistry.ts +15 -0
  159. package/lib/shared/registry/createNodeType.ts +175 -0
  160. package/lib/shared/registry/defineNodeType.ts +73 -0
  161. package/lib/shared/registry/fieldPresets.ts +109 -0
  162. package/lib/shared/registry/index.ts +50 -0
  163. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
  164. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
  165. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
  166. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
  167. package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
  168. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
  169. package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
  170. package/lib/shared/registry/nodeTypes/index.ts +75 -0
  171. package/lib/shared/responsiveScaling.test.ts +268 -0
  172. package/lib/shared/responsiveScaling.ts +194 -0
  173. package/lib/shared/responsiveStyleUtils.test.ts +300 -0
  174. package/lib/shared/responsiveStyleUtils.ts +139 -0
  175. package/lib/shared/slugTranslator.test.ts +325 -0
  176. package/lib/shared/slugTranslator.ts +177 -0
  177. package/lib/shared/styleNodeUtils.test.ts +132 -0
  178. package/lib/shared/styleNodeUtils.ts +102 -0
  179. package/lib/shared/styleUtils.test.ts +238 -0
  180. package/lib/shared/styleUtils.ts +63 -0
  181. package/lib/shared/themeDefaults.test.ts +113 -0
  182. package/lib/shared/themeDefaults.ts +103 -0
  183. package/lib/shared/tree/PathBuilder.ts +383 -0
  184. package/lib/shared/treePathUtils.test.ts +539 -0
  185. package/lib/shared/treePathUtils.ts +339 -0
  186. package/lib/shared/types/api.ts +58 -0
  187. package/lib/shared/types/cms.ts +95 -0
  188. package/lib/shared/types/colors.ts +45 -0
  189. package/lib/shared/types/components.ts +121 -0
  190. package/lib/shared/types/errors.test.ts +103 -0
  191. package/lib/shared/types/errors.ts +69 -0
  192. package/lib/shared/types/index.ts +96 -0
  193. package/lib/shared/types/nodes.ts +20 -0
  194. package/lib/shared/types/rendering.ts +61 -0
  195. package/lib/shared/types/styles.ts +38 -0
  196. package/lib/shared/types.ts +11 -0
  197. package/lib/shared/utilityClassConfig.ts +287 -0
  198. package/lib/shared/utilityClassMapper.test.ts +140 -0
  199. package/lib/shared/utilityClassMapper.ts +229 -0
  200. package/lib/shared/utils/fileUtils.test.ts +99 -0
  201. package/lib/shared/utils/fileUtils.ts +56 -0
  202. package/lib/shared/utils.test.ts +261 -0
  203. package/lib/shared/utils.ts +84 -0
  204. package/lib/shared/validation/index.ts +7 -0
  205. package/lib/shared/validation/propValidator.test.ts +178 -0
  206. package/lib/shared/validation/propValidator.ts +238 -0
  207. package/lib/shared/validation/schemas.test.ts +177 -0
  208. package/lib/shared/validation/schemas.ts +401 -0
  209. package/lib/shared/validation/validators.test.ts +109 -0
  210. package/lib/shared/validation/validators.ts +304 -0
  211. package/lib/test-utils/dom-setup.ts +55 -0
  212. package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
  213. package/lib/test-utils/factories/DomMockFactory.ts +487 -0
  214. package/lib/test-utils/factories/EventMockFactory.ts +244 -0
  215. package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
  216. package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
  217. package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
  218. package/lib/test-utils/factories/index.ts +11 -0
  219. package/lib/test-utils/fixtures.ts +134 -0
  220. package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
  221. package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
  222. package/lib/test-utils/helpers/index.ts +6 -0
  223. package/lib/test-utils/helpers.test.ts +73 -0
  224. package/lib/test-utils/helpers.ts +90 -0
  225. package/lib/test-utils/index.ts +17 -0
  226. package/lib/test-utils/mockFactories.ts +92 -0
  227. package/lib/test-utils/mocks.ts +341 -0
  228. package/package.json +38 -0
  229. package/templates/index-router.html +34 -0
  230. package/tsconfig.json +14 -0
  231. package/vite.config.ts +43 -0
@@ -0,0 +1,635 @@
1
+ import { test, expect, describe, beforeEach, afterEach, mock } from 'bun:test';
2
+ import { RouteLoader, RouteLoaderConfig } from './RouteLoader';
3
+ import { ComponentRegistry } from '../componentRegistry';
4
+ import { API_ROUTES, NOT_FOUND_TIMEOUT_MS } from '../../shared/constants';
5
+ import type { ComponentNode } from '../../shared/types';
6
+
7
+ describe('RouteLoader', () => {
8
+ let componentRegistry: ComponentRegistry;
9
+ let config: RouteLoaderConfig;
10
+ let routeLoader: RouteLoader;
11
+ let mockFetch: ReturnType<typeof mock>;
12
+
13
+ beforeEach(() => {
14
+ componentRegistry = new ComponentRegistry();
15
+ config = {
16
+ componentRegistry,
17
+ };
18
+ routeLoader = new RouteLoader(config);
19
+
20
+ // Mock fetch globally
21
+ mockFetch = mock(() => Promise.resolve({
22
+ ok: true,
23
+ json: () => Promise.resolve({}),
24
+ text: () => Promise.resolve('{}'),
25
+ }));
26
+ global.fetch = mockFetch as any;
27
+ });
28
+
29
+ afterEach(() => {
30
+ routeLoader.cancel();
31
+ });
32
+
33
+ describe('loadGlobalComponents', () => {
34
+ test('should successfully load and merge global components', async () => {
35
+ const globalComps = {
36
+ Button: {
37
+ type: 'component',
38
+ component: {
39
+ interface: {},
40
+ structure: { type: 'node', tag: 'button' },
41
+ },
42
+ },
43
+ };
44
+
45
+ mockFetch.mockResolvedValueOnce({
46
+ ok: true,
47
+ json: () => Promise.resolve(globalComps),
48
+ } as any);
49
+
50
+ await routeLoader.loadGlobalComponents();
51
+
52
+ expect(componentRegistry.get('Button')).toBeDefined();
53
+ expect(mockFetch).toHaveBeenCalledWith(API_ROUTES.COMPONENTS, {
54
+ cache: 'no-store',
55
+ });
56
+ });
57
+
58
+ test('should handle network errors gracefully', async () => {
59
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
60
+
61
+ // Should not throw - errors are caught internally
62
+ await routeLoader.loadGlobalComponents();
63
+ // Test passes if no exception is thrown
64
+ });
65
+
66
+ test('should handle invalid JSON gracefully', async () => {
67
+ mockFetch.mockResolvedValueOnce({
68
+ ok: true,
69
+ json: () => Promise.reject(new Error('Invalid JSON')),
70
+ } as any);
71
+
72
+ // Should not throw - errors are caught internally
73
+ await routeLoader.loadGlobalComponents();
74
+ // Test passes if no exception is thrown
75
+ });
76
+
77
+ test('should use no-cache header', async () => {
78
+ mockFetch.mockResolvedValueOnce({
79
+ ok: true,
80
+ json: () => Promise.resolve({}),
81
+ } as any);
82
+
83
+ await routeLoader.loadGlobalComponents();
84
+
85
+ expect(mockFetch).toHaveBeenCalledWith(API_ROUTES.COMPONENTS, {
86
+ cache: 'no-store',
87
+ });
88
+ });
89
+
90
+ test('should respect abort signal when loading global components', async () => {
91
+ const abortController = new AbortController();
92
+
93
+ // Start loading
94
+ const loadPromise = routeLoader.loadGlobalComponents(abortController.signal);
95
+
96
+ // Abort immediately
97
+ abortController.abort();
98
+
99
+ await loadPromise;
100
+
101
+ // Should complete without error (abort is handled gracefully)
102
+ // Test passes if no exception is thrown
103
+ });
104
+ });
105
+
106
+ describe('loadPages', () => {
107
+ test('should successfully load pages list', async () => {
108
+ const pagesData = { pages: ['/', '/about', '/contact'] };
109
+
110
+ mockFetch.mockResolvedValueOnce({
111
+ ok: true,
112
+ json: () => Promise.resolve(pagesData),
113
+ } as any);
114
+
115
+ const pages = await routeLoader.loadPages();
116
+
117
+ expect(pages).toEqual(['/', '/about', '/contact']);
118
+ expect(mockFetch).toHaveBeenCalledWith(API_ROUTES.PAGES);
119
+ });
120
+
121
+ test('should call onPagesLoaded callback', async () => {
122
+ const pagesData = { pages: ['/', '/about'] };
123
+ const onPagesLoaded = mock(() => {});
124
+
125
+ config.onPagesLoaded = onPagesLoaded;
126
+ routeLoader = new RouteLoader(config);
127
+
128
+ mockFetch.mockResolvedValueOnce({
129
+ ok: true,
130
+ json: () => Promise.resolve(pagesData),
131
+ } as any);
132
+
133
+ await routeLoader.loadPages();
134
+
135
+ expect(onPagesLoaded).toHaveBeenCalledWith(['/', '/about']);
136
+ });
137
+
138
+ test('should return empty array on error', async () => {
139
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
140
+
141
+ const pages = await routeLoader.loadPages();
142
+
143
+ expect(pages).toEqual([]);
144
+ });
145
+
146
+ test('should handle invalid response format', async () => {
147
+ mockFetch.mockResolvedValueOnce({
148
+ ok: true,
149
+ json: () => Promise.resolve({}),
150
+ } as any);
151
+
152
+ const pages = await routeLoader.loadPages();
153
+
154
+ expect(pages).toEqual([]);
155
+ });
156
+
157
+ test('should handle missing pages property', async () => {
158
+ mockFetch.mockResolvedValueOnce({
159
+ ok: true,
160
+ json: () => Promise.resolve({ other: 'data' }),
161
+ } as any);
162
+
163
+ const pages = await routeLoader.loadPages();
164
+
165
+ expect(pages).toEqual([]);
166
+ });
167
+ });
168
+
169
+ describe('loadComponents', () => {
170
+ test('should successfully load page with root only', async () => {
171
+ const pageData = {
172
+ root: {
173
+ type: 'node',
174
+ tag: 'div',
175
+ children: ['Hello'],
176
+ },
177
+ };
178
+
179
+ mockFetch
180
+ .mockResolvedValueOnce({
181
+ ok: true,
182
+ json: () => Promise.resolve({}),
183
+ } as any) // loadGlobalComponents
184
+ .mockResolvedValueOnce({
185
+ ok: true,
186
+ text: () => Promise.resolve(JSON.stringify(pageData)),
187
+ } as any); // loadComponents
188
+
189
+ const onLoadStart = mock(() => {});
190
+ const onLoadComplete = mock(() => {});
191
+
192
+ config.onLoadStart = onLoadStart;
193
+ config.onLoadComplete = onLoadComplete;
194
+ routeLoader = new RouteLoader(config);
195
+
196
+ const tree = await routeLoader.loadComponents('/');
197
+
198
+ expect(tree).toEqual(pageData.root);
199
+ expect(onLoadStart).toHaveBeenCalled();
200
+ expect(onLoadComplete).toHaveBeenCalledWith(pageData.root);
201
+ });
202
+
203
+ test('should successfully load page with components section', async () => {
204
+ const pageData = {
205
+ components: {
206
+ Card: {
207
+ type: 'component',
208
+ component: {
209
+ interface: {},
210
+ structure: { type: 'node', tag: 'div' },
211
+ },
212
+ },
213
+ },
214
+ root: {
215
+ type: 'node',
216
+ tag: 'div',
217
+ children: [],
218
+ },
219
+ };
220
+
221
+ mockFetch
222
+ .mockResolvedValueOnce({
223
+ ok: true,
224
+ json: () => Promise.resolve({}),
225
+ } as any) // loadGlobalComponents
226
+ .mockResolvedValueOnce({
227
+ ok: true,
228
+ text: () => Promise.resolve(JSON.stringify(pageData)),
229
+ } as any); // loadComponents
230
+
231
+ const tree = await routeLoader.loadComponents('/');
232
+
233
+ expect(tree).toEqual(pageData.root);
234
+ expect(componentRegistry.get('Card')).toBeDefined();
235
+ });
236
+
237
+ test('should handle legacy format (direct tree)', async () => {
238
+ const pageData = {
239
+ type: 'node',
240
+ tag: 'div',
241
+ children: ['Legacy'],
242
+ };
243
+
244
+ mockFetch
245
+ .mockResolvedValueOnce({
246
+ ok: true,
247
+ json: () => Promise.resolve({}),
248
+ } as any) // loadGlobalComponents
249
+ .mockResolvedValueOnce({
250
+ ok: true,
251
+ text: () => Promise.resolve(JSON.stringify(pageData)),
252
+ } as any); // loadComponents
253
+
254
+ const tree = await routeLoader.loadComponents('/');
255
+
256
+ expect(tree).toEqual(pageData);
257
+ });
258
+
259
+ test('should cancel previous request on new load', async () => {
260
+ const abortSpy = mock(() => {});
261
+
262
+ // First request - will be aborted
263
+ const firstAbortController = new AbortController();
264
+ firstAbortController.signal.addEventListener('abort', abortSpy);
265
+
266
+ mockFetch
267
+ .mockResolvedValueOnce({
268
+ ok: true,
269
+ json: () => Promise.resolve({}),
270
+ } as any)
271
+ .mockImplementationOnce(() => {
272
+ // Simulate slow request
273
+ return new Promise((resolve) => {
274
+ setTimeout(() => {
275
+ resolve({
276
+ ok: true,
277
+ text: () => Promise.resolve('{}'),
278
+ } as any);
279
+ }, 100);
280
+ });
281
+ });
282
+
283
+ const promise1 = routeLoader.loadComponents('/page1');
284
+
285
+ // Second request - should abort first
286
+ mockFetch
287
+ .mockResolvedValueOnce({
288
+ ok: true,
289
+ json: () => Promise.resolve({}),
290
+ } as any)
291
+ .mockResolvedValueOnce({
292
+ ok: true,
293
+ text: () => Promise.resolve('{}'),
294
+ } as any);
295
+
296
+ await routeLoader.loadComponents('/page2');
297
+
298
+ // First request should be aborted
299
+ await promise1;
300
+ // Note: We can't directly test abortController.abort() was called,
301
+ // but we can verify the second request completed
302
+ });
303
+
304
+ test('should handle 404 errors and call onNotFound after delay', async () => {
305
+ const onLoadComplete = mock(() => {});
306
+ const onNotFound = mock(() => {});
307
+
308
+ config.onLoadComplete = onLoadComplete;
309
+ config.onNotFound = onNotFound;
310
+ routeLoader = new RouteLoader(config);
311
+
312
+ mockFetch
313
+ .mockResolvedValueOnce({
314
+ ok: true,
315
+ json: () => Promise.resolve({}),
316
+ } as any) // loadGlobalComponents
317
+ .mockResolvedValueOnce({
318
+ ok: false,
319
+ status: 404,
320
+ } as any); // loadComponents
321
+
322
+ await routeLoader.loadComponents('/nonexistent');
323
+
324
+ expect(onLoadComplete).toHaveBeenCalledWith(null);
325
+
326
+ // Wait for timeout
327
+ await new Promise((resolve) => setTimeout(resolve, NOT_FOUND_TIMEOUT_MS + 50));
328
+
329
+ expect(onNotFound).toHaveBeenCalled();
330
+ });
331
+
332
+ test('should handle JSON parse errors and create error tree', async () => {
333
+ const onLoadComplete = mock(() => {});
334
+ const onLoadError = mock(() => {});
335
+
336
+ config.onLoadComplete = onLoadComplete;
337
+ config.onLoadError = onLoadError;
338
+ routeLoader = new RouteLoader(config);
339
+
340
+ mockFetch
341
+ .mockResolvedValueOnce({
342
+ ok: true,
343
+ json: () => Promise.resolve({}),
344
+ } as any) // loadGlobalComponents
345
+ .mockResolvedValueOnce({
346
+ ok: true,
347
+ text: () => Promise.resolve('invalid json {'),
348
+ } as any); // loadComponents
349
+
350
+ const tree = await routeLoader.loadComponents('/');
351
+
352
+ expect(tree).toBeDefined();
353
+ expect((tree as ComponentNode).type).toBe('node');
354
+ expect((tree as ComponentNode).tag).toBe('div');
355
+ expect(onLoadComplete).toHaveBeenCalled();
356
+ expect(onLoadError).toHaveBeenCalled();
357
+ });
358
+
359
+ test('should handle network errors and create error tree', async () => {
360
+ const onLoadComplete = mock(() => {});
361
+ const onLoadError = mock(() => {});
362
+
363
+ config.onLoadComplete = onLoadComplete;
364
+ config.onLoadError = onLoadError;
365
+ routeLoader = new RouteLoader(config);
366
+
367
+ mockFetch
368
+ .mockResolvedValueOnce({
369
+ ok: true,
370
+ json: () => Promise.resolve({}),
371
+ } as any) // loadGlobalComponents
372
+ .mockRejectedValueOnce(new Error('Network error')); // loadComponents
373
+
374
+ const tree = await routeLoader.loadComponents('/');
375
+
376
+ expect(tree).toBeDefined();
377
+ expect((tree as ComponentNode).type).toBe('node');
378
+ expect((tree as ComponentNode).tag).toBe('div');
379
+ expect(onLoadComplete).toHaveBeenCalled();
380
+ expect(onLoadError).toHaveBeenCalled();
381
+ });
382
+
383
+ test('should ignore abort errors', async () => {
384
+ const onLoadComplete = mock(() => {});
385
+ const onLoadError = mock(() => {});
386
+
387
+ config.onLoadComplete = onLoadComplete;
388
+ config.onLoadError = onLoadError;
389
+ routeLoader = new RouteLoader(config);
390
+
391
+ mockFetch
392
+ .mockResolvedValueOnce({
393
+ ok: true,
394
+ json: () => Promise.resolve({}),
395
+ } as any)
396
+ .mockImplementationOnce(() => {
397
+ const abortController = new AbortController();
398
+ abortController.abort();
399
+ return Promise.reject(new DOMException('Aborted', 'AbortError'));
400
+ });
401
+
402
+ const tree = await routeLoader.loadComponents('/');
403
+
404
+ expect(tree).toBeNull();
405
+ expect(onLoadComplete).not.toHaveBeenCalled();
406
+ expect(onLoadError).not.toHaveBeenCalled();
407
+ });
408
+
409
+ test('should check abort signal at multiple points', async () => {
410
+ const onLoadComplete = mock(() => {});
411
+
412
+ config.onLoadComplete = onLoadComplete;
413
+ routeLoader = new RouteLoader(config);
414
+
415
+ // Start loading
416
+ const loadPromise = routeLoader.loadComponents('/');
417
+
418
+ // Cancel immediately
419
+ routeLoader.cancel();
420
+
421
+ await loadPromise;
422
+
423
+ // Should not call onLoadComplete after cancel
424
+ expect(onLoadComplete).not.toHaveBeenCalled();
425
+ });
426
+
427
+ test('should merge page-specific components correctly', async () => {
428
+ const globalComps = {
429
+ Button: {
430
+ type: 'component',
431
+ component: {
432
+ interface: {},
433
+ structure: { type: 'node', tag: 'button' },
434
+ },
435
+ },
436
+ };
437
+
438
+ const pageData = {
439
+ components: {
440
+ Card: {
441
+ type: 'component',
442
+ component: {
443
+ interface: {},
444
+ structure: { type: 'node', tag: 'div' },
445
+ },
446
+ },
447
+ },
448
+ root: { type: 'node', tag: 'div' },
449
+ };
450
+
451
+ mockFetch
452
+ .mockResolvedValueOnce({
453
+ ok: true,
454
+ json: () => Promise.resolve(globalComps),
455
+ } as any) // loadGlobalComponents
456
+ .mockResolvedValueOnce({
457
+ ok: true,
458
+ text: () => Promise.resolve(JSON.stringify(pageData)),
459
+ } as any); // loadComponents
460
+
461
+ await routeLoader.loadComponents('/');
462
+
463
+ // Both global and page-specific components should be available
464
+ expect(componentRegistry.get('Button')).toBeDefined();
465
+ expect(componentRegistry.get('Card')).toBeDefined();
466
+ });
467
+
468
+ test('should not call callbacks after cancel', async () => {
469
+ const onLoadStart = mock(() => {});
470
+ const onLoadComplete = mock(() => {});
471
+
472
+ config.onLoadStart = onLoadStart;
473
+ config.onLoadComplete = onLoadComplete;
474
+ routeLoader = new RouteLoader(config);
475
+
476
+ mockFetch
477
+ .mockResolvedValueOnce({
478
+ ok: true,
479
+ json: () => Promise.resolve({}),
480
+ } as any)
481
+ .mockImplementationOnce(() => {
482
+ return new Promise((resolve) => {
483
+ setTimeout(() => {
484
+ resolve({
485
+ ok: true,
486
+ text: () => Promise.resolve(JSON.stringify({ root: { type: 'node', tag: 'div' } })),
487
+ } as any);
488
+ }, 100);
489
+ });
490
+ });
491
+
492
+ const loadPromise = routeLoader.loadComponents('/');
493
+
494
+ // Cancel immediately
495
+ routeLoader.cancel();
496
+
497
+ await loadPromise;
498
+
499
+ // onLoadStart might have been called before cancel, but onLoadComplete should not be called
500
+ // Actually, onLoadStart is called synchronously, so it will be called
501
+ // But onLoadComplete should not be called after cancel
502
+ expect(onLoadComplete).not.toHaveBeenCalled();
503
+ });
504
+ });
505
+
506
+ describe('cancel', () => {
507
+ test('should abort current request', async () => {
508
+ mockFetch
509
+ .mockResolvedValueOnce({
510
+ ok: true,
511
+ json: () => Promise.resolve({}),
512
+ } as any)
513
+ .mockImplementationOnce(() => {
514
+ return new Promise((resolve) => {
515
+ setTimeout(() => {
516
+ resolve({
517
+ ok: true,
518
+ text: () => Promise.resolve('{}'),
519
+ } as any);
520
+ }, 100);
521
+ });
522
+ });
523
+
524
+ const loadPromise = routeLoader.loadComponents('/');
525
+ routeLoader.cancel();
526
+
527
+ const result = await loadPromise;
528
+ expect(result).toBeNull();
529
+ });
530
+
531
+ test('should clear not found timeout', async () => {
532
+ const onNotFound = mock(() => {});
533
+
534
+ config.onNotFound = onNotFound;
535
+ routeLoader = new RouteLoader(config);
536
+
537
+ mockFetch
538
+ .mockResolvedValueOnce({
539
+ ok: true,
540
+ json: () => Promise.resolve({}),
541
+ } as any)
542
+ .mockResolvedValueOnce({
543
+ ok: false,
544
+ status: 404,
545
+ } as any);
546
+
547
+ await routeLoader.loadComponents('/nonexistent');
548
+ routeLoader.cancel();
549
+
550
+ // Wait for timeout - should not fire because we cancelled
551
+ await new Promise((resolve) => setTimeout(resolve, NOT_FOUND_TIMEOUT_MS + 50));
552
+
553
+ expect(onNotFound).not.toHaveBeenCalled();
554
+ });
555
+
556
+ test('should reset state', () => {
557
+ routeLoader.cancel();
558
+ // Should not throw
559
+ expect(() => routeLoader.cancel()).not.toThrow();
560
+ });
561
+ });
562
+
563
+ describe('createErrorTree', () => {
564
+ test('should create properly structured error trees', async () => {
565
+ const onLoadComplete = mock(() => {});
566
+
567
+ config.onLoadComplete = onLoadComplete;
568
+ routeLoader = new RouteLoader(config);
569
+
570
+ mockFetch
571
+ .mockResolvedValueOnce({
572
+ ok: true,
573
+ json: () => Promise.resolve({}),
574
+ } as any)
575
+ .mockRejectedValueOnce(new Error('Test error'));
576
+
577
+ const tree = await routeLoader.loadComponents('/');
578
+
579
+ expect(tree).toBeDefined();
580
+ const errorTree = tree as ComponentNode;
581
+ expect(errorTree.type).toBe('node');
582
+ expect(errorTree.tag).toBe('div');
583
+ expect(errorTree.children).toBeDefined();
584
+ expect(Array.isArray(errorTree.children)).toBe(true);
585
+ });
586
+
587
+ test('should include error details in parse errors', async () => {
588
+ const onLoadComplete = mock(() => {});
589
+
590
+ config.onLoadComplete = onLoadComplete;
591
+ routeLoader = new RouteLoader(config);
592
+
593
+ mockFetch
594
+ .mockResolvedValueOnce({
595
+ ok: true,
596
+ json: () => Promise.resolve({}),
597
+ } as any)
598
+ .mockResolvedValueOnce({
599
+ ok: true,
600
+ text: () => Promise.resolve('invalid json'),
601
+ } as any);
602
+
603
+ const tree = await routeLoader.loadComponents('/');
604
+
605
+ expect(tree).toBeDefined();
606
+ const errorTree = tree as ComponentNode;
607
+ expect(errorTree.children).toBeDefined();
608
+ // Should have 3 children: h2, p, pre
609
+ expect(Array.isArray(errorTree.children) && errorTree.children.length).toBeGreaterThanOrEqual(2);
610
+ });
611
+
612
+ test('should use consistent styling', async () => {
613
+ const onLoadComplete = mock(() => {});
614
+
615
+ config.onLoadComplete = onLoadComplete;
616
+ routeLoader = new RouteLoader(config);
617
+
618
+ mockFetch
619
+ .mockResolvedValueOnce({
620
+ ok: true,
621
+ json: () => Promise.resolve({}),
622
+ } as any)
623
+ .mockRejectedValueOnce(new Error('Test error'));
624
+
625
+ const tree = await routeLoader.loadComponents('/');
626
+
627
+ expect(tree).toBeDefined();
628
+ const errorTree = tree as ComponentNode;
629
+ expect(errorTree.style).toBeDefined();
630
+ expect((errorTree.style as any).padding).toBe('40px');
631
+ expect((errorTree.style as any).textAlign).toBe('center');
632
+ });
633
+ });
634
+ });
635
+