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,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
|
+
|