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,487 @@
1
+ /**
2
+ * DOM Mock Factory
3
+ * Properly typed mock factories for Window, Document, and HTMLElement
4
+ */
5
+
6
+ import { mock } from 'bun:test';
7
+
8
+ // ========== Types ==========
9
+
10
+ export interface MockDOMRect {
11
+ top: number;
12
+ left: number;
13
+ right: number;
14
+ bottom: number;
15
+ width: number;
16
+ height: number;
17
+ x: number;
18
+ y: number;
19
+ toJSON: () => object;
20
+ }
21
+
22
+ export interface MockHTMLElementOptions {
23
+ tagName?: string;
24
+ id?: string;
25
+ className?: string;
26
+ textContent?: string;
27
+ innerHTML?: string;
28
+ style?: Partial<CSSStyleDeclaration>;
29
+ dataset?: Record<string, string>;
30
+ attributes?: Record<string, string>;
31
+ }
32
+
33
+ export interface MockWindowOptions {
34
+ location?: Partial<Location>;
35
+ innerWidth?: number;
36
+ innerHeight?: number;
37
+ }
38
+
39
+ export interface MockDocumentOptions {
40
+ elements?: Map<string, HTMLElement>;
41
+ }
42
+
43
+ // ========== DOMRect ==========
44
+
45
+ export function createMockDOMRect(overrides?: Partial<MockDOMRect>): DOMRect {
46
+ return {
47
+ top: 0,
48
+ left: 0,
49
+ right: 100,
50
+ bottom: 100,
51
+ width: 100,
52
+ height: 100,
53
+ x: 0,
54
+ y: 0,
55
+ toJSON: () => ({}),
56
+ ...overrides,
57
+ } as DOMRect;
58
+ }
59
+
60
+ // ========== CSSStyleDeclaration ==========
61
+
62
+ export function createMockCSSStyleDeclaration(
63
+ initial?: Partial<CSSStyleDeclaration>
64
+ ): CSSStyleDeclaration {
65
+ const styles: Record<string, string> = {};
66
+
67
+ // Copy initial values
68
+ if (initial) {
69
+ for (const [key, value] of Object.entries(initial)) {
70
+ if (typeof value === 'string') {
71
+ styles[key] = value;
72
+ }
73
+ }
74
+ }
75
+
76
+ return new Proxy(styles as unknown as CSSStyleDeclaration, {
77
+ get(target, prop: string) {
78
+ if (prop === 'setProperty') {
79
+ return (name: string, value: string) => {
80
+ styles[name] = value;
81
+ };
82
+ }
83
+ if (prop === 'getPropertyValue') {
84
+ return (name: string) => styles[name] || '';
85
+ }
86
+ if (prop === 'removeProperty') {
87
+ return (name: string) => {
88
+ const old = styles[name];
89
+ delete styles[name];
90
+ return old || '';
91
+ };
92
+ }
93
+ return styles[prop] ?? '';
94
+ },
95
+ set(_, prop: string, value: string) {
96
+ styles[prop] = value;
97
+ return true;
98
+ },
99
+ });
100
+ }
101
+
102
+ // ========== HTMLElement ==========
103
+
104
+ export interface TypedMockHTMLElement {
105
+ tagName: string;
106
+ id: string;
107
+ className: string;
108
+ textContent: string;
109
+ innerHTML: string;
110
+ style: CSSStyleDeclaration;
111
+ dataset: DOMStringMap;
112
+ childNodes: Node[];
113
+ children: Element[];
114
+ parentNode: Node | null;
115
+ parentElement: HTMLElement | null;
116
+ getAttribute: ReturnType<typeof mock>;
117
+ setAttribute: ReturnType<typeof mock>;
118
+ removeAttribute: ReturnType<typeof mock>;
119
+ hasAttribute: ReturnType<typeof mock>;
120
+ addEventListener: ReturnType<typeof mock>;
121
+ removeEventListener: ReturnType<typeof mock>;
122
+ dispatchEvent: ReturnType<typeof mock>;
123
+ appendChild: ReturnType<typeof mock>;
124
+ removeChild: ReturnType<typeof mock>;
125
+ insertBefore: ReturnType<typeof mock>;
126
+ contains: ReturnType<typeof mock>;
127
+ matches: ReturnType<typeof mock>;
128
+ querySelector: ReturnType<typeof mock>;
129
+ querySelectorAll: ReturnType<typeof mock>;
130
+ closest: ReturnType<typeof mock>;
131
+ getBoundingClientRect: ReturnType<typeof mock>;
132
+ remove: ReturnType<typeof mock>;
133
+ cloneNode: ReturnType<typeof mock>;
134
+ focus: ReturnType<typeof mock>;
135
+ blur: ReturnType<typeof mock>;
136
+ scrollIntoView: ReturnType<typeof mock>;
137
+ // Expose for assertions
138
+ _attributes: Map<string, string>;
139
+ _eventListeners: Map<string, EventListener[]>;
140
+ }
141
+
142
+ export function createTypedMockHTMLElement(
143
+ options: MockHTMLElementOptions = {}
144
+ ): TypedMockHTMLElement {
145
+ const attributes = new Map<string, string>(
146
+ Object.entries(options.attributes || {})
147
+ );
148
+ const eventListeners = new Map<string, EventListener[]>();
149
+ const childNodes: Node[] = [];
150
+
151
+ const element: TypedMockHTMLElement = {
152
+ tagName: (options.tagName || 'DIV').toUpperCase(),
153
+ id: options.id || '',
154
+ className: options.className || '',
155
+ textContent: options.textContent || '',
156
+ innerHTML: options.innerHTML || '',
157
+ style: createMockCSSStyleDeclaration(options.style),
158
+ dataset: (options.dataset || {}) as DOMStringMap,
159
+ childNodes,
160
+ children: childNodes as unknown as Element[],
161
+ parentNode: null,
162
+ parentElement: null,
163
+
164
+ getAttribute: mock((name: string) => attributes.get(name) ?? null),
165
+ setAttribute: mock((name: string, value: string) => {
166
+ attributes.set(name, value);
167
+ }),
168
+ removeAttribute: mock((name: string) => {
169
+ attributes.delete(name);
170
+ }),
171
+ hasAttribute: mock((name: string) => attributes.has(name)),
172
+
173
+ addEventListener: mock((type: string, listener: EventListener) => {
174
+ if (!eventListeners.has(type)) {
175
+ eventListeners.set(type, []);
176
+ }
177
+ eventListeners.get(type)!.push(listener);
178
+ }),
179
+ removeEventListener: mock((type: string, listener: EventListener) => {
180
+ const listeners = eventListeners.get(type);
181
+ if (listeners) {
182
+ const index = listeners.indexOf(listener);
183
+ if (index > -1) listeners.splice(index, 1);
184
+ }
185
+ }),
186
+ dispatchEvent: mock((event: Event) => {
187
+ const listeners = eventListeners.get(event.type);
188
+ listeners?.forEach((l) => l(event));
189
+ return true;
190
+ }),
191
+
192
+ appendChild: mock((child: Node) => {
193
+ childNodes.push(child);
194
+ return child;
195
+ }),
196
+ removeChild: mock((child: Node) => {
197
+ const index = childNodes.indexOf(child);
198
+ if (index > -1) childNodes.splice(index, 1);
199
+ return child;
200
+ }),
201
+ insertBefore: mock((newNode: Node, refNode: Node | null) => {
202
+ if (refNode === null) {
203
+ childNodes.push(newNode);
204
+ } else {
205
+ const index = childNodes.indexOf(refNode);
206
+ if (index > -1) {
207
+ childNodes.splice(index, 0, newNode);
208
+ }
209
+ }
210
+ return newNode;
211
+ }),
212
+
213
+ contains: mock(() => false),
214
+ matches: mock(() => false),
215
+ querySelector: mock(() => null),
216
+ querySelectorAll: mock(() => [] as unknown as NodeListOf<Element>),
217
+ closest: mock(() => null),
218
+ getBoundingClientRect: mock(() => createMockDOMRect()),
219
+ remove: mock(() => {}),
220
+ cloneNode: mock(() => createTypedMockHTMLElement(options) as unknown as Node),
221
+ focus: mock(() => {}),
222
+ blur: mock(() => {}),
223
+ scrollIntoView: mock(() => {}),
224
+
225
+ // Internal state for assertions
226
+ _attributes: attributes,
227
+ _eventListeners: eventListeners,
228
+ };
229
+
230
+ return element;
231
+ }
232
+
233
+ // ========== Location ==========
234
+
235
+ export interface TypedMockLocation {
236
+ href: string;
237
+ pathname: string;
238
+ search: string;
239
+ hash: string;
240
+ host: string;
241
+ hostname: string;
242
+ port: string;
243
+ protocol: string;
244
+ origin: string;
245
+ assign: ReturnType<typeof mock>;
246
+ replace: ReturnType<typeof mock>;
247
+ reload: ReturnType<typeof mock>;
248
+ toString: () => string;
249
+ }
250
+
251
+ export function createTypedMockLocation(
252
+ overrides?: Partial<Location>
253
+ ): TypedMockLocation {
254
+ const base = {
255
+ href: 'http://localhost/',
256
+ pathname: '/',
257
+ search: '',
258
+ hash: '',
259
+ host: 'localhost',
260
+ hostname: 'localhost',
261
+ port: '',
262
+ protocol: 'http:',
263
+ origin: 'http://localhost',
264
+ ...overrides,
265
+ };
266
+
267
+ return {
268
+ ...base,
269
+ assign: mock(() => {}),
270
+ replace: mock(() => {}),
271
+ reload: mock(() => {}),
272
+ toString: () => base.href,
273
+ };
274
+ }
275
+
276
+ // ========== History ==========
277
+
278
+ export interface TypedMockHistory {
279
+ length: number;
280
+ state: unknown;
281
+ scrollRestoration: ScrollRestoration;
282
+ pushState: ReturnType<typeof mock>;
283
+ replaceState: ReturnType<typeof mock>;
284
+ go: ReturnType<typeof mock>;
285
+ back: ReturnType<typeof mock>;
286
+ forward: ReturnType<typeof mock>;
287
+ // Internal state for assertions
288
+ _stateStack: Array<{ state: unknown; title: string; url: string }>;
289
+ }
290
+
291
+ export function createTypedMockHistory(): TypedMockHistory {
292
+ const stateStack: Array<{ state: unknown; title: string; url: string }> = [];
293
+
294
+ return {
295
+ length: 1,
296
+ state: null,
297
+ scrollRestoration: 'auto',
298
+ pushState: mock((state: unknown, title: string, url?: string | null) => {
299
+ stateStack.push({ state, title, url: url || '' });
300
+ }),
301
+ replaceState: mock((state: unknown, title: string, url?: string | null) => {
302
+ if (stateStack.length > 0) {
303
+ stateStack[stateStack.length - 1] = { state, title, url: url || '' };
304
+ } else {
305
+ stateStack.push({ state, title, url: url || '' });
306
+ }
307
+ }),
308
+ go: mock(() => {}),
309
+ back: mock(() => {}),
310
+ forward: mock(() => {}),
311
+ _stateStack: stateStack,
312
+ };
313
+ }
314
+
315
+ // ========== Window ==========
316
+
317
+ export interface TypedMockWindow {
318
+ location: TypedMockLocation;
319
+ history: TypedMockHistory;
320
+ innerWidth: number;
321
+ innerHeight: number;
322
+ addEventListener: ReturnType<typeof mock>;
323
+ removeEventListener: ReturnType<typeof mock>;
324
+ dispatchEvent: ReturnType<typeof mock>;
325
+ getComputedStyle: ReturnType<typeof mock>;
326
+ requestAnimationFrame: ReturnType<typeof mock>;
327
+ cancelAnimationFrame: ReturnType<typeof mock>;
328
+ setTimeout: typeof globalThis.setTimeout;
329
+ clearTimeout: typeof globalThis.clearTimeout;
330
+ setInterval: typeof globalThis.setInterval;
331
+ clearInterval: typeof globalThis.clearInterval;
332
+ // Internal state
333
+ _eventListeners: Map<string, EventListener[]>;
334
+ }
335
+
336
+ export function createTypedMockWindow(
337
+ options: MockWindowOptions = {}
338
+ ): TypedMockWindow {
339
+ const eventListeners = new Map<string, EventListener[]>();
340
+ const location = createTypedMockLocation(options.location);
341
+ const history = createTypedMockHistory();
342
+
343
+ return {
344
+ location,
345
+ history,
346
+ innerWidth: options.innerWidth ?? 1920,
347
+ innerHeight: options.innerHeight ?? 1080,
348
+
349
+ addEventListener: mock((type: string, listener: EventListener) => {
350
+ if (!eventListeners.has(type)) {
351
+ eventListeners.set(type, []);
352
+ }
353
+ eventListeners.get(type)!.push(listener);
354
+ }),
355
+ removeEventListener: mock((type: string, listener: EventListener) => {
356
+ const listeners = eventListeners.get(type);
357
+ if (listeners) {
358
+ const index = listeners.indexOf(listener);
359
+ if (index > -1) listeners.splice(index, 1);
360
+ }
361
+ }),
362
+ dispatchEvent: mock((event: Event) => {
363
+ const listeners = eventListeners.get(event.type);
364
+ listeners?.forEach((l) => l(event));
365
+ return true;
366
+ }),
367
+
368
+ getComputedStyle: mock(() => createMockCSSStyleDeclaration()),
369
+ requestAnimationFrame: mock((cb: FrameRequestCallback) => {
370
+ setTimeout(() => cb(performance.now()), 0);
371
+ return 1;
372
+ }),
373
+ cancelAnimationFrame: mock(() => {}),
374
+
375
+ setTimeout: globalThis.setTimeout,
376
+ clearTimeout: globalThis.clearTimeout,
377
+ setInterval: globalThis.setInterval,
378
+ clearInterval: globalThis.clearInterval,
379
+
380
+ _eventListeners: eventListeners,
381
+ };
382
+ }
383
+
384
+ // ========== Document ==========
385
+
386
+ export interface TypedMockDocument {
387
+ createElement: ReturnType<typeof mock>;
388
+ getElementById: ReturnType<typeof mock>;
389
+ querySelector: ReturnType<typeof mock>;
390
+ querySelectorAll: ReturnType<typeof mock>;
391
+ addEventListener: ReturnType<typeof mock>;
392
+ removeEventListener: ReturnType<typeof mock>;
393
+ dispatchEvent: ReturnType<typeof mock>;
394
+ head: TypedMockHTMLElement;
395
+ body: TypedMockHTMLElement;
396
+ documentElement: TypedMockHTMLElement;
397
+ // Internal state
398
+ _createdElements: TypedMockHTMLElement[];
399
+ _registeredElements: Map<string, TypedMockHTMLElement>;
400
+ _eventListeners: Map<string, EventListener[]>;
401
+ }
402
+
403
+ export function createTypedMockDocument(
404
+ options: MockDocumentOptions = {}
405
+ ): TypedMockDocument {
406
+ const registeredElements = new Map<string, TypedMockHTMLElement>();
407
+ const createdElements: TypedMockHTMLElement[] = [];
408
+ const eventListeners = new Map<string, EventListener[]>();
409
+
410
+ // Convert any provided elements
411
+ if (options.elements) {
412
+ for (const [id, el] of options.elements) {
413
+ registeredElements.set(id, el as unknown as TypedMockHTMLElement);
414
+ }
415
+ }
416
+
417
+ return {
418
+ createElement: mock((tag: string) => {
419
+ const el = createTypedMockHTMLElement({ tagName: tag });
420
+ createdElements.push(el);
421
+ return el;
422
+ }),
423
+ getElementById: mock((id: string) => registeredElements.get(id) ?? null),
424
+ querySelector: mock(() => null),
425
+ querySelectorAll: mock(() => [] as unknown as NodeListOf<Element>),
426
+
427
+ addEventListener: mock((type: string, listener: EventListener) => {
428
+ if (!eventListeners.has(type)) {
429
+ eventListeners.set(type, []);
430
+ }
431
+ eventListeners.get(type)!.push(listener);
432
+ }),
433
+ removeEventListener: mock((type: string, listener: EventListener) => {
434
+ const listeners = eventListeners.get(type);
435
+ if (listeners) {
436
+ const index = listeners.indexOf(listener);
437
+ if (index > -1) listeners.splice(index, 1);
438
+ }
439
+ }),
440
+ dispatchEvent: mock((event: Event) => {
441
+ const listeners = eventListeners.get(event.type);
442
+ listeners?.forEach((l) => l(event));
443
+ return true;
444
+ }),
445
+
446
+ head: createTypedMockHTMLElement({ tagName: 'HEAD' }),
447
+ body: createTypedMockHTMLElement({ tagName: 'BODY' }),
448
+ documentElement: createTypedMockHTMLElement({ tagName: 'HTML' }),
449
+
450
+ _createdElements: createdElements,
451
+ _registeredElements: registeredElements,
452
+ _eventListeners: eventListeners,
453
+ };
454
+ }
455
+
456
+ // ========== Global Installation Helpers ==========
457
+
458
+ export interface DomMockInstallation {
459
+ window: TypedMockWindow;
460
+ document: TypedMockDocument;
461
+ cleanup: () => void;
462
+ }
463
+
464
+ /**
465
+ * Install mock DOM globally and return cleanup function
466
+ */
467
+ export function installMockDom(
468
+ options: { window?: MockWindowOptions; document?: MockDocumentOptions } = {}
469
+ ): DomMockInstallation {
470
+ const originalWindow = (globalThis as any).window;
471
+ const originalDocument = (globalThis as any).document;
472
+
473
+ const mockWindow = createTypedMockWindow(options.window);
474
+ const mockDocument = createTypedMockDocument(options.document);
475
+
476
+ (globalThis as any).window = mockWindow;
477
+ (globalThis as any).document = mockDocument;
478
+
479
+ return {
480
+ window: mockWindow,
481
+ document: mockDocument,
482
+ cleanup: () => {
483
+ (globalThis as any).window = originalWindow;
484
+ (globalThis as any).document = originalDocument;
485
+ },
486
+ };
487
+ }