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,595 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach, mock } from "bun:test";
|
|
2
|
+
import { ErrorBoundary, withErrorBoundary } from "./ErrorBoundary";
|
|
3
|
+
import { createElement as h, Component, useState } from "react";
|
|
4
|
+
import { createRoot, Root } from "react-dom/client";
|
|
5
|
+
import { flushPromises, wait } from "../test-utils/helpers/asyncHelpers";
|
|
6
|
+
|
|
7
|
+
// Mock console.error to avoid noise in test output
|
|
8
|
+
const originalConsoleError = console.error;
|
|
9
|
+
let consoleErrorMock: ReturnType<typeof mock>;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
consoleErrorMock = mock(() => {});
|
|
13
|
+
console.error = consoleErrorMock;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
console.error = originalConsoleError;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Helper to create test container
|
|
21
|
+
function createTestContainer() {
|
|
22
|
+
const div = document.createElement("div");
|
|
23
|
+
document.body.appendChild(div);
|
|
24
|
+
return div;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function cleanupContainer(container: HTMLElement) {
|
|
28
|
+
try {
|
|
29
|
+
document.body.removeChild(container);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// Already removed
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Component that throws an error when rendered
|
|
36
|
+
function ThrowingComponent({ error }: { error: Error }) {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Component that throws conditionally
|
|
41
|
+
class ConditionallyThrowingComponent extends Component<{ shouldThrow: boolean; error: Error }> {
|
|
42
|
+
render() {
|
|
43
|
+
if (this.props.shouldThrow) {
|
|
44
|
+
throw this.props.error;
|
|
45
|
+
}
|
|
46
|
+
return h("div", null, "Safe content");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("ErrorBoundary - Error catching", () => {
|
|
51
|
+
describe("Component render errors", () => {
|
|
52
|
+
test("should catch errors thrown during render and display fallback UI", async () => {
|
|
53
|
+
const container = createTestContainer();
|
|
54
|
+
const root = createRoot(container);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const testError = new Error("Test render error");
|
|
58
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
59
|
+
level: "component",
|
|
60
|
+
componentName: "TestComponent",
|
|
61
|
+
children: h(ThrowingComponent, { error: testError })
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
root.render(errorBoundary);
|
|
65
|
+
await wait(10); // Wait for React to process error
|
|
66
|
+
|
|
67
|
+
// Verify fallback UI is displayed with error message
|
|
68
|
+
expect(container.textContent).toContain("Component Error");
|
|
69
|
+
expect(container.textContent).toContain("TestComponent");
|
|
70
|
+
expect(container.textContent).toContain("Test render error");
|
|
71
|
+
} finally {
|
|
72
|
+
root.unmount();
|
|
73
|
+
cleanupContainer(container);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("should render children when no error occurs", async () => {
|
|
78
|
+
const container = createTestContainer();
|
|
79
|
+
const root = createRoot(container);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
83
|
+
level: "component",
|
|
84
|
+
componentName: "TestComponent",
|
|
85
|
+
children: h("div", null, "Safe content")
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
root.render(errorBoundary);
|
|
89
|
+
await wait(10);
|
|
90
|
+
|
|
91
|
+
// Should render the safe content
|
|
92
|
+
expect(container.textContent).toContain("Safe content");
|
|
93
|
+
// Should NOT show error UI
|
|
94
|
+
expect(container.textContent).not.toContain("Component Error");
|
|
95
|
+
} finally {
|
|
96
|
+
root.unmount();
|
|
97
|
+
cleanupContainer(container);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("getDerivedStateFromError should return correct state shape", () => {
|
|
102
|
+
const error = new Error("Test error");
|
|
103
|
+
const state = ErrorBoundary.getDerivedStateFromError(error);
|
|
104
|
+
|
|
105
|
+
expect(state?.hasError).toBe(true);
|
|
106
|
+
expect(state?.error).toBe(error);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("should call onError callback when error is caught", async () => {
|
|
110
|
+
const container = createTestContainer();
|
|
111
|
+
const root = createRoot(container);
|
|
112
|
+
const onErrorMock = mock(() => {});
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const testError = new Error("Callback test error");
|
|
116
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
117
|
+
level: "component",
|
|
118
|
+
onError: onErrorMock,
|
|
119
|
+
children: h(ThrowingComponent, { error: testError })
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
root.render(errorBoundary);
|
|
123
|
+
await wait(10);
|
|
124
|
+
|
|
125
|
+
// Verify onError callback was called with the error
|
|
126
|
+
expect(onErrorMock).toHaveBeenCalled();
|
|
127
|
+
const callArgs = onErrorMock.mock.calls[0];
|
|
128
|
+
expect(callArgs[0]).toBe(testError);
|
|
129
|
+
} finally {
|
|
130
|
+
root.unmount();
|
|
131
|
+
cleanupContainer(container);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("Page level errors", () => {
|
|
137
|
+
test("should display page-level error UI when level is page", async () => {
|
|
138
|
+
const container = createTestContainer();
|
|
139
|
+
const root = createRoot(container);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const testError = new Error("Page rendering failed");
|
|
143
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
144
|
+
level: "page",
|
|
145
|
+
children: h(ThrowingComponent, { error: testError })
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
root.render(errorBoundary);
|
|
149
|
+
await wait(10);
|
|
150
|
+
|
|
151
|
+
// Should show page-level error UI
|
|
152
|
+
expect(container.textContent).toContain("Page Rendering Error");
|
|
153
|
+
expect(container.textContent).toContain("Page rendering failed");
|
|
154
|
+
} finally {
|
|
155
|
+
root.unmount();
|
|
156
|
+
cleanupContainer(container);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("should render page content when no error occurs", async () => {
|
|
161
|
+
const container = createTestContainer();
|
|
162
|
+
const root = createRoot(container);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
166
|
+
level: "page",
|
|
167
|
+
children: h("div", null, "Page content")
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
root.render(errorBoundary);
|
|
171
|
+
await wait(10);
|
|
172
|
+
|
|
173
|
+
expect(container.textContent).toContain("Page content");
|
|
174
|
+
expect(container.textContent).not.toContain("Page Rendering Error");
|
|
175
|
+
} finally {
|
|
176
|
+
root.unmount();
|
|
177
|
+
cleanupContainer(container);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("Custom fallback UI", () => {
|
|
183
|
+
test("should use custom fallback when provided", async () => {
|
|
184
|
+
const container = createTestContainer();
|
|
185
|
+
const root = createRoot(container);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const testError = new Error("Custom error");
|
|
189
|
+
const customFallback = (error: Error) => h("div", null, `Custom fallback: ${error.message}`);
|
|
190
|
+
|
|
191
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
192
|
+
level: "component",
|
|
193
|
+
fallback: customFallback,
|
|
194
|
+
children: h(ThrowingComponent, { error: testError })
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
root.render(errorBoundary);
|
|
198
|
+
await wait(10);
|
|
199
|
+
|
|
200
|
+
// Should show custom fallback
|
|
201
|
+
expect(container.textContent).toContain("Custom fallback: Custom error");
|
|
202
|
+
// Should NOT show default error UI
|
|
203
|
+
expect(container.textContent).not.toContain("Component Error");
|
|
204
|
+
} finally {
|
|
205
|
+
root.unmount();
|
|
206
|
+
cleanupContainer(container);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("Error state management", () => {
|
|
212
|
+
test("getDerivedStateFromError should set hasError to true", () => {
|
|
213
|
+
const error = new Error("Test");
|
|
214
|
+
const state = ErrorBoundary.getDerivedStateFromError(error);
|
|
215
|
+
|
|
216
|
+
expect(state?.hasError).toBe(true);
|
|
217
|
+
expect(state?.error).toBe(error);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("should handle different error types", () => {
|
|
221
|
+
const typeError = new TypeError("Type error");
|
|
222
|
+
const refError = new ReferenceError("Reference error");
|
|
223
|
+
const syntaxError = new SyntaxError("Syntax error");
|
|
224
|
+
|
|
225
|
+
expect(ErrorBoundary.getDerivedStateFromError(typeError)?.error).toBeInstanceOf(TypeError);
|
|
226
|
+
expect(ErrorBoundary.getDerivedStateFromError(refError)?.error).toBeInstanceOf(ReferenceError);
|
|
227
|
+
expect(ErrorBoundary.getDerivedStateFromError(syntaxError)?.error).toBeInstanceOf(SyntaxError);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("should handle null errors gracefully", () => {
|
|
231
|
+
const state = ErrorBoundary.getDerivedStateFromError(null as any);
|
|
232
|
+
|
|
233
|
+
expect(state?.hasError).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("ErrorBoundary - Error display", () => {
|
|
239
|
+
describe("Error message rendering", () => {
|
|
240
|
+
test("should display error message in fallback UI", async () => {
|
|
241
|
+
const container = createTestContainer();
|
|
242
|
+
const root = createRoot(container);
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
246
|
+
level: "component",
|
|
247
|
+
children: h(ThrowingComponent, { error: new Error("Specific error message") })
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
root.render(errorBoundary);
|
|
251
|
+
await wait(10);
|
|
252
|
+
|
|
253
|
+
expect(container.textContent).toContain("Specific error message");
|
|
254
|
+
} finally {
|
|
255
|
+
root.unmount();
|
|
256
|
+
cleanupContainer(container);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("should handle errors without messages", async () => {
|
|
261
|
+
const container = createTestContainer();
|
|
262
|
+
const root = createRoot(container);
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
266
|
+
level: "component",
|
|
267
|
+
children: h(ThrowingComponent, { error: new Error() })
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
root.render(errorBoundary);
|
|
271
|
+
await wait(10);
|
|
272
|
+
|
|
273
|
+
// Should still show error UI even without message
|
|
274
|
+
expect(container.textContent).toContain("Component Error");
|
|
275
|
+
} finally {
|
|
276
|
+
root.unmount();
|
|
277
|
+
cleanupContainer(container);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("Component name display", () => {
|
|
283
|
+
test("should display componentName in error UI", async () => {
|
|
284
|
+
const container = createTestContainer();
|
|
285
|
+
const root = createRoot(container);
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
289
|
+
level: "component",
|
|
290
|
+
componentName: "MyCustomComponent",
|
|
291
|
+
children: h(ThrowingComponent, { error: new Error("Error") })
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
root.render(errorBoundary);
|
|
295
|
+
await wait(10);
|
|
296
|
+
|
|
297
|
+
expect(container.textContent).toContain("MyCustomComponent");
|
|
298
|
+
} finally {
|
|
299
|
+
root.unmount();
|
|
300
|
+
cleanupContainer(container);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("should display generic error when componentName is not provided", async () => {
|
|
305
|
+
const container = createTestContainer();
|
|
306
|
+
const root = createRoot(container);
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
310
|
+
level: "component",
|
|
311
|
+
children: h(ThrowingComponent, { error: new Error("Error") })
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
root.render(errorBoundary);
|
|
315
|
+
await wait(10);
|
|
316
|
+
|
|
317
|
+
expect(container.textContent).toContain("Component Error");
|
|
318
|
+
} finally {
|
|
319
|
+
root.unmount();
|
|
320
|
+
cleanupContainer(container);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("Error level handling", () => {
|
|
326
|
+
test("should render component-level UI by default", async () => {
|
|
327
|
+
const container = createTestContainer();
|
|
328
|
+
const root = createRoot(container);
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
332
|
+
// No level prop - should default to 'component'
|
|
333
|
+
children: h(ThrowingComponent, { error: new Error("Test") })
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
root.render(errorBoundary);
|
|
337
|
+
await wait(10);
|
|
338
|
+
|
|
339
|
+
// Component-level shows "Component Error"
|
|
340
|
+
expect(container.textContent).toContain("Component Error");
|
|
341
|
+
// Should NOT show page-level error
|
|
342
|
+
expect(container.textContent).not.toContain("Page Rendering Error");
|
|
343
|
+
} finally {
|
|
344
|
+
root.unmount();
|
|
345
|
+
cleanupContainer(container);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("should log error to console with correct level", async () => {
|
|
350
|
+
const container = createTestContainer();
|
|
351
|
+
const root = createRoot(container);
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
355
|
+
level: "page",
|
|
356
|
+
componentName: "TestPage",
|
|
357
|
+
children: h(ThrowingComponent, { error: new Error("Log test") })
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
root.render(errorBoundary);
|
|
361
|
+
await wait(10);
|
|
362
|
+
|
|
363
|
+
// Verify console.error was called
|
|
364
|
+
expect(consoleErrorMock).toHaveBeenCalled();
|
|
365
|
+
} finally {
|
|
366
|
+
root.unmount();
|
|
367
|
+
cleanupContainer(container);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe("ErrorBoundary - withErrorBoundary utility", () => {
|
|
374
|
+
describe("Error catching", () => {
|
|
375
|
+
test("should catch synchronous errors in render function", () => {
|
|
376
|
+
const throwingFn = () => {
|
|
377
|
+
throw new Error("Render error");
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// Should not throw - returns an ErrorBoundary element instead
|
|
381
|
+
const result = withErrorBoundary(throwingFn, {
|
|
382
|
+
componentName: "TestComponent"
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Result should be a React element (ErrorBoundary)
|
|
386
|
+
expect(result).toBeDefined();
|
|
387
|
+
expect(typeof result).toBe("object");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("should return result when no error occurs", () => {
|
|
391
|
+
const successFn = () => "Success";
|
|
392
|
+
|
|
393
|
+
const result = withErrorBoundary(successFn);
|
|
394
|
+
|
|
395
|
+
expect(result).toBe("Success");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("should render wrapped component content when no error", async () => {
|
|
399
|
+
const container = createTestContainer();
|
|
400
|
+
const root = createRoot(container);
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const successFn = () => h("div", null, "Wrapped content");
|
|
404
|
+
const wrapped = withErrorBoundary(successFn, {
|
|
405
|
+
componentName: "WrappedComponent"
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
root.render(wrapped);
|
|
409
|
+
await wait(10);
|
|
410
|
+
|
|
411
|
+
expect(container.textContent).toContain("Wrapped content");
|
|
412
|
+
} finally {
|
|
413
|
+
root.unmount();
|
|
414
|
+
cleanupContainer(container);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("should render error boundary when function throws", async () => {
|
|
419
|
+
const container = createTestContainer();
|
|
420
|
+
const root = createRoot(container);
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const throwingFn = () => {
|
|
424
|
+
throw new Error("Sync error");
|
|
425
|
+
};
|
|
426
|
+
const wrapped = withErrorBoundary(throwingFn, {
|
|
427
|
+
componentName: "FailingComponent"
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
root.render(wrapped);
|
|
431
|
+
await wait(10);
|
|
432
|
+
|
|
433
|
+
// withErrorBoundary catches sync errors and returns an ErrorBoundary
|
|
434
|
+
// The ErrorBoundary renders with null children when an error was caught
|
|
435
|
+
expect(container.innerHTML).toBeDefined();
|
|
436
|
+
} finally {
|
|
437
|
+
root.unmount();
|
|
438
|
+
cleanupContainer(container);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe("Error handling options", () => {
|
|
444
|
+
test("should pass options to the error boundary", () => {
|
|
445
|
+
const onErrorMock = mock(() => {});
|
|
446
|
+
|
|
447
|
+
const throwingFn = () => {
|
|
448
|
+
throw new Error("Test error");
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const result = withErrorBoundary(throwingFn, {
|
|
452
|
+
componentName: "TestComponent",
|
|
453
|
+
level: "page",
|
|
454
|
+
onError: onErrorMock
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Result is an ErrorBoundary element with the provided props
|
|
458
|
+
expect(result).toBeDefined();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("should use default options when not provided", () => {
|
|
462
|
+
const successFn = () => h("div", null, "Content");
|
|
463
|
+
|
|
464
|
+
const result = withErrorBoundary(successFn);
|
|
465
|
+
|
|
466
|
+
// Should work without options
|
|
467
|
+
expect(result).toBeDefined();
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe("ErrorBoundary - Error types", () => {
|
|
473
|
+
test("should catch and display TypeError", async () => {
|
|
474
|
+
const container = createTestContainer();
|
|
475
|
+
const root = createRoot(container);
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
479
|
+
level: "component",
|
|
480
|
+
children: h(ThrowingComponent, { error: new TypeError("Type mismatch") })
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
root.render(errorBoundary);
|
|
484
|
+
await wait(10);
|
|
485
|
+
|
|
486
|
+
expect(container.textContent).toContain("Type mismatch");
|
|
487
|
+
expect(container.textContent).toContain("Component Error");
|
|
488
|
+
} finally {
|
|
489
|
+
root.unmount();
|
|
490
|
+
cleanupContainer(container);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("should catch and display ReferenceError", async () => {
|
|
495
|
+
const container = createTestContainer();
|
|
496
|
+
const root = createRoot(container);
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
500
|
+
level: "component",
|
|
501
|
+
children: h(ThrowingComponent, { error: new ReferenceError("undefined variable") })
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
root.render(errorBoundary);
|
|
505
|
+
await wait(10);
|
|
506
|
+
|
|
507
|
+
expect(container.textContent).toContain("undefined variable");
|
|
508
|
+
} finally {
|
|
509
|
+
root.unmount();
|
|
510
|
+
cleanupContainer(container);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("should display custom error messages clearly", async () => {
|
|
515
|
+
const container = createTestContainer();
|
|
516
|
+
const root = createRoot(container);
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
const errorBoundary = h(ErrorBoundary, {
|
|
520
|
+
level: "component",
|
|
521
|
+
children: h(ThrowingComponent, { error: new Error("Invalid component definition: Missing structure") })
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
root.render(errorBoundary);
|
|
525
|
+
await wait(10);
|
|
526
|
+
|
|
527
|
+
expect(container.textContent).toContain("Invalid component definition: Missing structure");
|
|
528
|
+
} finally {
|
|
529
|
+
root.unmount();
|
|
530
|
+
cleanupContainer(container);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
describe("ErrorBoundary - Multiple instances", () => {
|
|
536
|
+
test("should isolate errors to their own boundary", async () => {
|
|
537
|
+
const container = createTestContainer();
|
|
538
|
+
const root = createRoot(container);
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
// Create a parent with two error boundaries - one throws, one doesn't
|
|
542
|
+
const app = h("div", null,
|
|
543
|
+
h(ErrorBoundary, {
|
|
544
|
+
level: "component",
|
|
545
|
+
componentName: "ThrowingChild",
|
|
546
|
+
children: h(ThrowingComponent, { error: new Error("Child error") })
|
|
547
|
+
}),
|
|
548
|
+
h(ErrorBoundary, {
|
|
549
|
+
level: "component",
|
|
550
|
+
componentName: "SafeChild",
|
|
551
|
+
children: h("div", null, "Safe content")
|
|
552
|
+
})
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
root.render(app);
|
|
556
|
+
await wait(10);
|
|
557
|
+
|
|
558
|
+
// Both should be in the DOM
|
|
559
|
+
expect(container.textContent).toContain("Component Error");
|
|
560
|
+
expect(container.textContent).toContain("Safe content");
|
|
561
|
+
} finally {
|
|
562
|
+
root.unmount();
|
|
563
|
+
cleanupContainer(container);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("should handle nested error boundaries", async () => {
|
|
568
|
+
const container = createTestContainer();
|
|
569
|
+
const root = createRoot(container);
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
// Outer boundary catches errors that inner boundary doesn't handle
|
|
573
|
+
const app = h(ErrorBoundary, {
|
|
574
|
+
level: "page",
|
|
575
|
+
children: h(ErrorBoundary, {
|
|
576
|
+
level: "component",
|
|
577
|
+
componentName: "InnerComponent",
|
|
578
|
+
children: h(ThrowingComponent, { error: new Error("Inner error") })
|
|
579
|
+
})
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
root.render(app);
|
|
583
|
+
await wait(10);
|
|
584
|
+
|
|
585
|
+
// Inner boundary should catch the error
|
|
586
|
+
expect(container.textContent).toContain("Component Error");
|
|
587
|
+
expect(container.textContent).toContain("Inner error");
|
|
588
|
+
// Should NOT show page-level error since inner boundary caught it
|
|
589
|
+
expect(container.textContent).not.toContain("Page Rendering Error");
|
|
590
|
+
} finally {
|
|
591
|
+
root.unmount();
|
|
592
|
+
cleanupContainer(container);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
});
|