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,441 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach, mock } from "bun:test";
|
|
2
|
+
import { navigateTo, createNavigationHandler } from "./navigation";
|
|
3
|
+
|
|
4
|
+
// Setup browser-like environment
|
|
5
|
+
function setupWindowMock() {
|
|
6
|
+
if (typeof globalThis.window === 'undefined') {
|
|
7
|
+
globalThis.window = {
|
|
8
|
+
history: {
|
|
9
|
+
pushState: mock(() => {}),
|
|
10
|
+
},
|
|
11
|
+
dispatchEvent: mock(() => true),
|
|
12
|
+
} as any;
|
|
13
|
+
}
|
|
14
|
+
// Add PopStateEvent if not defined
|
|
15
|
+
if (typeof globalThis.PopStateEvent === 'undefined') {
|
|
16
|
+
globalThis.PopStateEvent = class PopStateEvent extends Event {
|
|
17
|
+
constructor(type: string, eventInitDict?: PopStateEventInit) {
|
|
18
|
+
super(type, eventInitDict);
|
|
19
|
+
}
|
|
20
|
+
} as any;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("Navigation - navigateTo", () => {
|
|
25
|
+
let originalPushState: any;
|
|
26
|
+
let originalDispatchEvent: any;
|
|
27
|
+
let pushStateCalls: Array<{ state: any; title: string; url: string }>;
|
|
28
|
+
let dispatchedEvents: Event[];
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
setupWindowMock();
|
|
32
|
+
|
|
33
|
+
// Save originals
|
|
34
|
+
originalPushState = (globalThis as any).window.history.pushState;
|
|
35
|
+
originalDispatchEvent = (globalThis as any).window.dispatchEvent;
|
|
36
|
+
|
|
37
|
+
// Track calls
|
|
38
|
+
pushStateCalls = [];
|
|
39
|
+
dispatchedEvents = [];
|
|
40
|
+
|
|
41
|
+
// Mock pushState
|
|
42
|
+
(globalThis as any).window.history.pushState = mock((state: any, title: string, url: string) => {
|
|
43
|
+
pushStateCalls.push({ state, title, url });
|
|
44
|
+
}) as any;
|
|
45
|
+
|
|
46
|
+
// Mock dispatchEvent
|
|
47
|
+
(globalThis as any).window.dispatchEvent = mock((event: Event) => {
|
|
48
|
+
dispatchedEvents.push(event);
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
// Restore originals
|
|
55
|
+
if ((globalThis as any).window) {
|
|
56
|
+
(globalThis as any).window.history.pushState = originalPushState;
|
|
57
|
+
(globalThis as any).window.dispatchEvent = originalDispatchEvent;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("should navigate to root path", () => {
|
|
62
|
+
navigateTo("/");
|
|
63
|
+
|
|
64
|
+
expect(pushStateCalls.length).toBe(1);
|
|
65
|
+
expect(pushStateCalls[0].url).toBe("/");
|
|
66
|
+
expect(dispatchedEvents.length).toBe(1);
|
|
67
|
+
expect(dispatchedEvents[0].type).toBe("popstate");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("should navigate to nested path", () => {
|
|
71
|
+
navigateTo("/about");
|
|
72
|
+
|
|
73
|
+
expect(pushStateCalls.length).toBe(1);
|
|
74
|
+
expect(pushStateCalls[0].url).toBe("/about");
|
|
75
|
+
expect(dispatchedEvents[0].type).toBe("popstate");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("should navigate to deeply nested path", () => {
|
|
79
|
+
navigateTo("/blog/post-1");
|
|
80
|
+
|
|
81
|
+
expect(pushStateCalls[0].url).toBe("/blog/post-1");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("should call pushState with empty state object", () => {
|
|
85
|
+
navigateTo("/test");
|
|
86
|
+
|
|
87
|
+
expect(pushStateCalls[0].state).toEqual({});
|
|
88
|
+
expect(pushStateCalls[0].title).toBe("");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("should dispatch PopStateEvent", () => {
|
|
92
|
+
navigateTo("/test");
|
|
93
|
+
|
|
94
|
+
expect(dispatchedEvents.length).toBe(1);
|
|
95
|
+
expect(dispatchedEvents[0]).toBeInstanceOf(PopStateEvent);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("should handle path with query string", () => {
|
|
99
|
+
navigateTo("/search?q=test");
|
|
100
|
+
|
|
101
|
+
expect(pushStateCalls[0].url).toBe("/search?q=test");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("should handle path with hash", () => {
|
|
105
|
+
navigateTo("/page#section");
|
|
106
|
+
|
|
107
|
+
expect(pushStateCalls[0].url).toBe("/page#section");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("Navigation - createNavigationHandler", () => {
|
|
112
|
+
let originalPushState: any;
|
|
113
|
+
let originalDispatchEvent: any;
|
|
114
|
+
let pushStateCalls: Array<{ url: string }>;
|
|
115
|
+
let preventDefaultCalled: boolean;
|
|
116
|
+
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
setupWindowMock();
|
|
119
|
+
originalPushState = (globalThis as any).window.history.pushState;
|
|
120
|
+
originalDispatchEvent = (globalThis as any).window.dispatchEvent;
|
|
121
|
+
|
|
122
|
+
pushStateCalls = [];
|
|
123
|
+
preventDefaultCalled = false;
|
|
124
|
+
|
|
125
|
+
(globalThis as any).window.history.pushState = mock((state: any, title: string, url: string) => {
|
|
126
|
+
pushStateCalls.push({ url });
|
|
127
|
+
}) as any;
|
|
128
|
+
|
|
129
|
+
(globalThis as any).window.dispatchEvent = mock(() => true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
afterEach(() => {
|
|
133
|
+
if ((globalThis as any).window) {
|
|
134
|
+
(globalThis as any).window.history.pushState = originalPushState;
|
|
135
|
+
(globalThis as any).window.dispatchEvent = originalDispatchEvent;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("should create handler that prevents default", () => {
|
|
140
|
+
const handler = createNavigationHandler("/test");
|
|
141
|
+
const mockEvent = {
|
|
142
|
+
preventDefault: mock(() => {
|
|
143
|
+
preventDefaultCalled = true;
|
|
144
|
+
})
|
|
145
|
+
} as any;
|
|
146
|
+
|
|
147
|
+
handler(mockEvent);
|
|
148
|
+
|
|
149
|
+
expect(preventDefaultCalled).toBe(true);
|
|
150
|
+
expect(mockEvent.preventDefault).toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("should create handler that navigates to path", () => {
|
|
154
|
+
const handler = createNavigationHandler("/about");
|
|
155
|
+
const mockEvent = {
|
|
156
|
+
preventDefault: mock(() => {})
|
|
157
|
+
} as any;
|
|
158
|
+
|
|
159
|
+
handler(mockEvent);
|
|
160
|
+
|
|
161
|
+
expect(pushStateCalls.length).toBe(1);
|
|
162
|
+
expect(pushStateCalls[0].url).toBe("/about");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("should prevent default before navigating", () => {
|
|
166
|
+
const handler = createNavigationHandler("/test");
|
|
167
|
+
let preventDefaultCalledFirst = false;
|
|
168
|
+
let navigationCalledFirst = false;
|
|
169
|
+
|
|
170
|
+
const mockEvent = {
|
|
171
|
+
preventDefault: mock(() => {
|
|
172
|
+
preventDefaultCalledFirst = !navigationCalledFirst;
|
|
173
|
+
})
|
|
174
|
+
} as any;
|
|
175
|
+
|
|
176
|
+
const originalPushState = (globalThis as any).window.history.pushState;
|
|
177
|
+
(globalThis as any).window.history.pushState = mock(() => {
|
|
178
|
+
navigationCalledFirst = !preventDefaultCalledFirst;
|
|
179
|
+
}) as any;
|
|
180
|
+
|
|
181
|
+
handler(mockEvent);
|
|
182
|
+
|
|
183
|
+
// preventDefault should be called before navigation
|
|
184
|
+
expect(preventDefaultCalledFirst).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("should handle root path in handler", () => {
|
|
188
|
+
const handler = createNavigationHandler("/");
|
|
189
|
+
const mockEvent = {
|
|
190
|
+
preventDefault: mock(() => {})
|
|
191
|
+
} as any;
|
|
192
|
+
|
|
193
|
+
handler(mockEvent);
|
|
194
|
+
|
|
195
|
+
expect(pushStateCalls[0].url).toBe("/");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("should handle nested paths in handler", () => {
|
|
199
|
+
const handler = createNavigationHandler("/blog/post-1");
|
|
200
|
+
const mockEvent = {
|
|
201
|
+
preventDefault: mock(() => {})
|
|
202
|
+
} as any;
|
|
203
|
+
|
|
204
|
+
handler(mockEvent);
|
|
205
|
+
|
|
206
|
+
expect(pushStateCalls[0].url).toBe("/blog/post-1");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("Navigation - Path handling", () => {
|
|
211
|
+
let originalPushState: any;
|
|
212
|
+
let originalDispatchEvent: any;
|
|
213
|
+
|
|
214
|
+
beforeEach(() => {
|
|
215
|
+
setupWindowMock();
|
|
216
|
+
originalPushState = (globalThis as any).window.history.pushState;
|
|
217
|
+
originalDispatchEvent = (globalThis as any).window.dispatchEvent;
|
|
218
|
+
|
|
219
|
+
(globalThis as any).window.history.pushState = mock(() => {}) as any;
|
|
220
|
+
(globalThis as any).window.dispatchEvent = mock(() => true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
afterEach(() => {
|
|
224
|
+
if ((globalThis as any).window) {
|
|
225
|
+
(globalThis as any).window.history.pushState = originalPushState;
|
|
226
|
+
(globalThis as any).window.dispatchEvent = originalDispatchEvent;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("Root path handling", () => {
|
|
231
|
+
test("should navigate to root path correctly", () => {
|
|
232
|
+
const pushStateCalls: Array<{ url: string }> = [];
|
|
233
|
+
(globalThis as any).window.history.pushState = mock((state: any, title: string, url: string) => {
|
|
234
|
+
pushStateCalls.push({ url });
|
|
235
|
+
}) as any;
|
|
236
|
+
|
|
237
|
+
navigateTo("/");
|
|
238
|
+
|
|
239
|
+
expect(pushStateCalls[0].url).toBe("/");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("should handle root path with handler", () => {
|
|
243
|
+
const handler = createNavigationHandler("/");
|
|
244
|
+
const pushStateCalls: Array<{ url: string }> = [];
|
|
245
|
+
(globalThis as any).window.history.pushState = mock((state: any, title: string, url: string) => {
|
|
246
|
+
pushStateCalls.push({ url });
|
|
247
|
+
}) as any;
|
|
248
|
+
|
|
249
|
+
const mockEvent = {
|
|
250
|
+
preventDefault: mock(() => {})
|
|
251
|
+
} as any;
|
|
252
|
+
|
|
253
|
+
handler(mockEvent);
|
|
254
|
+
|
|
255
|
+
expect(pushStateCalls[0].url).toBe("/");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("Nested paths handling", () => {
|
|
260
|
+
test("should handle single level nested path", () => {
|
|
261
|
+
const pushStateCalls: Array<{ url: string }> = [];
|
|
262
|
+
(globalThis as any).window.history.pushState = mock((state: any, title: string, url: string) => {
|
|
263
|
+
pushStateCalls.push({ url });
|
|
264
|
+
}) as any;
|
|
265
|
+
|
|
266
|
+
navigateTo("/about");
|
|
267
|
+
|
|
268
|
+
expect(pushStateCalls[0].url).toBe("/about");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("should handle multi-level nested paths", () => {
|
|
272
|
+
const pushStateCalls: Array<{ url: string }> = [];
|
|
273
|
+
(globalThis as any).window.history.pushState = mock((state: any, title: string, url: string) => {
|
|
274
|
+
pushStateCalls.push({ url });
|
|
275
|
+
}) as any;
|
|
276
|
+
|
|
277
|
+
navigateTo("/blog/category/post-1");
|
|
278
|
+
|
|
279
|
+
expect(pushStateCalls[0].url).toBe("/blog/category/post-1");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("should handle paths with trailing slash", () => {
|
|
283
|
+
const pushStateCalls: Array<{ url: string }> = [];
|
|
284
|
+
(globalThis as any).window.history.pushState = mock((state: any, title: string, url: string) => {
|
|
285
|
+
pushStateCalls.push({ url });
|
|
286
|
+
}) as any;
|
|
287
|
+
|
|
288
|
+
navigateTo("/about/");
|
|
289
|
+
|
|
290
|
+
expect(pushStateCalls[0].url).toBe("/about/");
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("Special path handling", () => {
|
|
295
|
+
test("should handle paths with query parameters", () => {
|
|
296
|
+
const pushStateCalls: Array<{ url: string }> = [];
|
|
297
|
+
(globalThis as any).window.history.pushState = mock((state: any, title: string, url: string) => {
|
|
298
|
+
pushStateCalls.push({ url });
|
|
299
|
+
}) as any;
|
|
300
|
+
|
|
301
|
+
navigateTo("/search?q=test&page=1");
|
|
302
|
+
|
|
303
|
+
expect(pushStateCalls[0].url).toBe("/search?q=test&page=1");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("should handle paths with hash fragments", () => {
|
|
307
|
+
const pushStateCalls: Array<{ url: string }> = [];
|
|
308
|
+
(globalThis as any).window.history.pushState = mock((state: any, title: string, url: string) => {
|
|
309
|
+
pushStateCalls.push({ url });
|
|
310
|
+
}) as any;
|
|
311
|
+
|
|
312
|
+
navigateTo("/page#section");
|
|
313
|
+
|
|
314
|
+
expect(pushStateCalls[0].url).toBe("/page#section");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("should handle paths with both query and hash", () => {
|
|
318
|
+
const pushStateCalls: Array<{ url: string }> = [];
|
|
319
|
+
(globalThis as any).window.history.pushState = mock((state: any, title: string, url: string) => {
|
|
320
|
+
pushStateCalls.push({ url });
|
|
321
|
+
}) as any;
|
|
322
|
+
|
|
323
|
+
navigateTo("/page?param=value#section");
|
|
324
|
+
|
|
325
|
+
expect(pushStateCalls[0].url).toBe("/page?param=value#section");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("Link component behavior simulation", () => {
|
|
330
|
+
test("should simulate Link component click behavior", () => {
|
|
331
|
+
const handler = createNavigationHandler("/link-target");
|
|
332
|
+
const mockEvent = {
|
|
333
|
+
preventDefault: mock(() => {}),
|
|
334
|
+
type: "click",
|
|
335
|
+
target: {}
|
|
336
|
+
} as any;
|
|
337
|
+
|
|
338
|
+
const pushStateCalls: Array<{ url: string }> = [];
|
|
339
|
+
(globalThis as any).window.history.pushState = mock((state: any, title: string, url: string) => {
|
|
340
|
+
pushStateCalls.push({ url });
|
|
341
|
+
}) as any;
|
|
342
|
+
|
|
343
|
+
handler(mockEvent);
|
|
344
|
+
|
|
345
|
+
// Should prevent default (no page reload)
|
|
346
|
+
expect(mockEvent.preventDefault).toHaveBeenCalled();
|
|
347
|
+
// Should navigate using history API
|
|
348
|
+
expect(pushStateCalls.length).toBe(1);
|
|
349
|
+
expect(pushStateCalls[0].url).toBe("/link-target");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("should handle multiple Link clicks", () => {
|
|
353
|
+
const handler1 = createNavigationHandler("/page1");
|
|
354
|
+
const handler2 = createNavigationHandler("/page2");
|
|
355
|
+
|
|
356
|
+
const pushStateCalls: Array<{ url: string }> = [];
|
|
357
|
+
(globalThis as any).window.history.pushState = mock((state: any, title: string, url: string) => {
|
|
358
|
+
pushStateCalls.push({ url });
|
|
359
|
+
}) as any;
|
|
360
|
+
|
|
361
|
+
const mockEvent1 = { preventDefault: mock(() => {}) } as any;
|
|
362
|
+
const mockEvent2 = { preventDefault: mock(() => {}) } as any;
|
|
363
|
+
|
|
364
|
+
handler1(mockEvent1);
|
|
365
|
+
handler2(mockEvent2);
|
|
366
|
+
|
|
367
|
+
expect(pushStateCalls.length).toBe(2);
|
|
368
|
+
expect(pushStateCalls[0].url).toBe("/page1");
|
|
369
|
+
expect(pushStateCalls[1].url).toBe("/page2");
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("Navigation - Integration scenarios", () => {
|
|
375
|
+
let originalPushState: any;
|
|
376
|
+
let originalDispatchEvent: any;
|
|
377
|
+
let pushStateCalls: Array<{ url: string }>;
|
|
378
|
+
let dispatchedEvents: Event[];
|
|
379
|
+
|
|
380
|
+
beforeEach(() => {
|
|
381
|
+
setupWindowMock();
|
|
382
|
+
originalPushState = (globalThis as any).window.history.pushState;
|
|
383
|
+
originalDispatchEvent = (globalThis as any).window.dispatchEvent;
|
|
384
|
+
|
|
385
|
+
pushStateCalls = [];
|
|
386
|
+
dispatchedEvents = [];
|
|
387
|
+
|
|
388
|
+
(globalThis as any).window.history.pushState = mock((state: any, title: string, url: string) => {
|
|
389
|
+
pushStateCalls.push({ url });
|
|
390
|
+
}) as any;
|
|
391
|
+
|
|
392
|
+
(globalThis as any).window.dispatchEvent = mock((event: Event) => {
|
|
393
|
+
dispatchedEvents.push(event);
|
|
394
|
+
return true;
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
afterEach(() => {
|
|
399
|
+
if ((globalThis as any).window) {
|
|
400
|
+
(globalThis as any).window.history.pushState = originalPushState;
|
|
401
|
+
(globalThis as any).window.dispatchEvent = originalDispatchEvent;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("should handle navigation sequence", () => {
|
|
406
|
+
navigateTo("/");
|
|
407
|
+
navigateTo("/about");
|
|
408
|
+
navigateTo("/contact");
|
|
409
|
+
|
|
410
|
+
expect(pushStateCalls.length).toBe(3);
|
|
411
|
+
expect(pushStateCalls[0].url).toBe("/");
|
|
412
|
+
expect(pushStateCalls[1].url).toBe("/about");
|
|
413
|
+
expect(pushStateCalls[2].url).toBe("/contact");
|
|
414
|
+
expect(dispatchedEvents.length).toBe(3);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("should work with navigation handler in sequence", () => {
|
|
418
|
+
const handler1 = createNavigationHandler("/page1");
|
|
419
|
+
const handler2 = createNavigationHandler("/page2");
|
|
420
|
+
|
|
421
|
+
const mockEvent = {
|
|
422
|
+
preventDefault: mock(() => {})
|
|
423
|
+
} as any;
|
|
424
|
+
|
|
425
|
+
handler1(mockEvent);
|
|
426
|
+
handler2(mockEvent);
|
|
427
|
+
|
|
428
|
+
expect(pushStateCalls.length).toBe(2);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("should handle back/forward navigation simulation", () => {
|
|
432
|
+
// Navigate forward
|
|
433
|
+
navigateTo("/page1");
|
|
434
|
+
navigateTo("/page2");
|
|
435
|
+
navigateTo("/page3");
|
|
436
|
+
|
|
437
|
+
// Simulate back button (this would normally be handled by popstate listener)
|
|
438
|
+
// The navigateTo function doesn't handle back/forward, but the router does
|
|
439
|
+
expect(pushStateCalls.length).toBe(3);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation utilities for client-side routing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Navigate to a path using the history API and dispatch a popstate event
|
|
7
|
+
* This allows the router to handle the navigation
|
|
8
|
+
*/
|
|
9
|
+
export function navigateTo(path: string): void {
|
|
10
|
+
window.history.pushState({}, '', path);
|
|
11
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a navigation click handler that prevents default and navigates
|
|
16
|
+
*/
|
|
17
|
+
export function createNavigationHandler(path: string): (e: Event) => void {
|
|
18
|
+
return (e: Event) => {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
navigateTo(path);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|