claw-dashboard 2.1.1 → 2.2.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/CHANGELOG.md +29 -1
- package/docs/API.md +1 -2
- package/index.js +31 -41
- package/package.json +22 -12
- package/src/auto-save.js +11 -5
- package/src/cli/export-snapshot.js +3 -1
- package/src/cli/import-snapshot.js +3 -1
- package/src/config.js +9 -15
- package/src/errors.js +0 -9
- package/src/security.js +6 -2
- package/src/snapshot.js +73 -26
- package/src/web-server.js +7 -10
- package/.c8rc.json +0 -38
- package/.dockerignore +0 -68
- package/.github/dependabot.yml +0 -45
- package/.github/pull_request_template.md +0 -39
- package/.github/workflows/ci.yml +0 -147
- package/.github/workflows/release.yml +0 -40
- package/.github/workflows/security.yml +0 -84
- package/.husky/pre-commit +0 -1
- package/.planning/codebase/ARCHITECTURE.md +0 -206
- package/.planning/codebase/INTEGRATIONS.md +0 -150
- package/.planning/codebase/STACK.md +0 -122
- package/.planning/codebase/STRUCTURE.md +0 -201
- package/CONTRIBUTING.md +0 -378
- package/Dockerfile +0 -54
- package/FEATURES.md +0 -307
- package/TODO.md +0 -28
- package/ai.openclaw.dashboard.plist +0 -35
- package/build-cjs.js +0 -127
- package/cjs-shim.js +0 -13
- package/dist/clawdash +0 -1729
- package/dist/clawdash.meta.json +0 -2236
- package/dist/widgets.cjs +0 -6511
- package/docker-compose.yml +0 -67
- package/esbuild.config.js +0 -158
- package/eslint.config.js +0 -56
- package/examples/plugins/README.md +0 -122
- package/examples/plugins/api-status/index.js +0 -294
- package/examples/plugins/api-status/plugin.json +0 -19
- package/examples/plugins/hello-world/index.js +0 -104
- package/examples/plugins/hello-world/plugin.json +0 -15
- package/examples/plugins/system-metrics-chart/index.js +0 -339
- package/examples/plugins/system-metrics-chart/plugin.json +0 -18
- package/examples/plugins/weather-widget/index.js +0 -136
- package/examples/plugins/weather-widget/plugin.json +0 -19
- package/index.cjs +0 -23005
- package/jest.config.js +0 -11
- package/scripts/release.js +0 -595
- package/src/database.js +0 -734
- package/tests/alerts.test.js +0 -437
- package/tests/auto-save.test.js +0 -529
- package/tests/cache.test.js +0 -317
- package/tests/cli.test.js +0 -351
- package/tests/command-palette.test.js +0 -320
- package/tests/config-processor.test.js +0 -452
- package/tests/config-validator.test.js +0 -710
- package/tests/config-watcher.test.js +0 -594
- package/tests/config.test.js +0 -755
- package/tests/database.test.js +0 -438
- package/tests/errors.test.js +0 -189
- package/tests/example-plugins.test.js +0 -624
- package/tests/integration.test.js +0 -1321
- package/tests/loading-states.test.js +0 -300
- package/tests/manifest-validation-on-load.test.js +0 -671
- package/tests/performance-monitor.test.js +0 -190
- package/tests/plugin-api-rate-limit.test.js +0 -302
- package/tests/plugin-errors.test.js +0 -311
- package/tests/plugin-lifecycle-e2e.test.js +0 -1036
- package/tests/plugin-reload.test.js +0 -764
- package/tests/rate-limiter.test.js +0 -539
- package/tests/removed-dependencies.test.js +0 -74
- package/tests/retry.test.js +0 -308
- package/tests/security.test.js +0 -411
- package/tests/settings-modal.test.js +0 -226
- package/tests/theme-selector.test.js +0 -57
- package/tests/utils.js +0 -242
- package/tests/utils.test.js +0 -317
- package/tests/validate-plugin-cli.test.js +0 -359
- package/tests/validation.test.js +0 -837
- package/tests/web-server.test.js +0 -646
- package/tests/widget-arrange-mode.test.js +0 -183
- package/tests/widget-config-hot-reload.test.js +0 -465
- package/tests/widget-dependency.test.js +0 -591
- package/tests/widget-error-boundary.test.js +0 -749
- package/tests/widget-error-isolation.test.js +0 -469
- package/tests/widget-integration.test.js +0 -1147
- package/tests/widget-refresh-intervals.test.js +0 -284
- package/tests/worker-pool.test.js +0 -522
|
@@ -1,749 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for widget error boundary functionality
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { jest } from '@jest/globals';
|
|
6
|
-
|
|
7
|
-
// Mock blessed before importing the module under test
|
|
8
|
-
const mockBlessedBox = jest.fn();
|
|
9
|
-
const mockBlessedText = jest.fn();
|
|
10
|
-
const mockBlessedButton = jest.fn();
|
|
11
|
-
|
|
12
|
-
jest.unstable_mockModule('blessed', () => ({
|
|
13
|
-
default: {
|
|
14
|
-
box: mockBlessedBox,
|
|
15
|
-
text: mockBlessedText,
|
|
16
|
-
button: mockBlessedButton,
|
|
17
|
-
},
|
|
18
|
-
box: mockBlessedBox,
|
|
19
|
-
text: mockBlessedText,
|
|
20
|
-
button: mockBlessedButton,
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
// Mock logger
|
|
24
|
-
jest.unstable_mockModule('../src/logger.js', () => ({
|
|
25
|
-
default: {
|
|
26
|
-
info: jest.fn(),
|
|
27
|
-
warn: jest.fn(),
|
|
28
|
-
error: jest.fn(),
|
|
29
|
-
debug: jest.fn(),
|
|
30
|
-
}
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
// Now import the modules under test
|
|
34
|
-
const { WidgetErrorBoundary, ErrorBoundaryManager, ErrorStyles, withErrorBoundary, getErrorBoundaryManager } = await import('../src/widgets/widget-error-boundary.js');
|
|
35
|
-
const { WidgetHealthStatus, WidgetErrorType } = await import('../src/widgets/widget-error-isolation.js');
|
|
36
|
-
|
|
37
|
-
// Mock blessed elements
|
|
38
|
-
const mockBlessedElements = [];
|
|
39
|
-
|
|
40
|
-
function createMockBox(options = {}) {
|
|
41
|
-
return {
|
|
42
|
-
destroyed: false,
|
|
43
|
-
hidden: false,
|
|
44
|
-
content: '',
|
|
45
|
-
...options,
|
|
46
|
-
children: [],
|
|
47
|
-
style: {},
|
|
48
|
-
focused: false,
|
|
49
|
-
_listeners: {},
|
|
50
|
-
show() { this.hidden = false; },
|
|
51
|
-
hide() { this.hidden = true; },
|
|
52
|
-
destroy() { this.destroyed = true; },
|
|
53
|
-
setContent(content) { this.content = content; },
|
|
54
|
-
focus() { this.focused = true; },
|
|
55
|
-
on(event, handler) {
|
|
56
|
-
if (!this._listeners[event]) this._listeners[event] = [];
|
|
57
|
-
this._listeners[event].push(handler);
|
|
58
|
-
},
|
|
59
|
-
removeListener(event, handler) {
|
|
60
|
-
if (this._listeners[event]) {
|
|
61
|
-
const idx = this._listeners[event].indexOf(handler);
|
|
62
|
-
if (idx > -1) this._listeners[event].splice(idx, 1);
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
emit(event, ...args) {
|
|
66
|
-
if (this._listeners[event]) {
|
|
67
|
-
this._listeners[event].forEach(h => h(...args));
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Mock widget for testing
|
|
74
|
-
function createMockWidget(options = {}) {
|
|
75
|
-
return {
|
|
76
|
-
id: options.id || 'test-widget',
|
|
77
|
-
name: options.name || 'Test Widget',
|
|
78
|
-
box: options.box || null,
|
|
79
|
-
create: options.create || jest.fn().mockResolvedValue(undefined),
|
|
80
|
-
init: options.init || jest.fn().mockResolvedValue(undefined),
|
|
81
|
-
getData: options.getData || jest.fn().mockResolvedValue({}),
|
|
82
|
-
render: options.render || jest.fn(),
|
|
83
|
-
update: options.update || jest.fn(),
|
|
84
|
-
destroy: options.destroy || jest.fn().mockResolvedValue(undefined),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Mock screen
|
|
89
|
-
function createMockScreen() {
|
|
90
|
-
const screen = createMockBox({
|
|
91
|
-
_keypressHandlers: [],
|
|
92
|
-
keypressCallbacks: [],
|
|
93
|
-
});
|
|
94
|
-
screen._keypressHandlers = [];
|
|
95
|
-
screen.on('keypress', (ch, key) => {
|
|
96
|
-
screen._keypressHandlers.forEach(h => h(ch, key));
|
|
97
|
-
});
|
|
98
|
-
return screen;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
describe('Widget Error Boundary', () => {
|
|
102
|
-
beforeEach(() => {
|
|
103
|
-
mockBlessedElements.length = 0;
|
|
104
|
-
mockBlessedBox.mockReset();
|
|
105
|
-
mockBlessedText.mockReset();
|
|
106
|
-
mockBlessedButton.mockReset();
|
|
107
|
-
|
|
108
|
-
// Set up blessed mocks to return mock boxes
|
|
109
|
-
mockBlessedBox.mockImplementation((opts) => {
|
|
110
|
-
const box = createMockBox(opts);
|
|
111
|
-
mockBlessedElements.push(box);
|
|
112
|
-
return box;
|
|
113
|
-
});
|
|
114
|
-
mockBlessedText.mockImplementation((opts) => {
|
|
115
|
-
const text = createMockBox(opts);
|
|
116
|
-
mockBlessedElements.push(text);
|
|
117
|
-
return text;
|
|
118
|
-
});
|
|
119
|
-
mockBlessedButton.mockImplementation((opts) => {
|
|
120
|
-
const button = createMockBox(opts);
|
|
121
|
-
mockBlessedElements.push(button);
|
|
122
|
-
return button;
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
jest.clearAllMocks();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
afterEach(() => {
|
|
129
|
-
// Clean up any remaining error boundaries
|
|
130
|
-
const manager = getErrorBoundaryManager();
|
|
131
|
-
manager.clearAll();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
describe('WidgetErrorBoundary', () => {
|
|
135
|
-
test('should create error boundary with default options', () => {
|
|
136
|
-
const widget = createMockWidget();
|
|
137
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
138
|
-
|
|
139
|
-
expect(boundary.widget).toBe(widget);
|
|
140
|
-
expect(boundary.options.maxRetries).toBe(3);
|
|
141
|
-
expect(boundary.options.retryDelay).toBe(5000);
|
|
142
|
-
expect(boundary.options.showErrorDetails).toBe(true);
|
|
143
|
-
expect(boundary.options.allowDismiss).toBe(true);
|
|
144
|
-
expect(boundary.options.errorTitle).toBe('Widget Error');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test('should create error boundary with custom options', () => {
|
|
148
|
-
const widget = createMockWidget();
|
|
149
|
-
const boundary = new WidgetErrorBoundary(widget, {
|
|
150
|
-
maxRetries: 5,
|
|
151
|
-
retryDelay: 10000,
|
|
152
|
-
showErrorDetails: false,
|
|
153
|
-
allowDismiss: false,
|
|
154
|
-
errorTitle: 'Custom Error',
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
expect(boundary.options.maxRetries).toBe(5);
|
|
158
|
-
expect(boundary.options.retryDelay).toBe(10000);
|
|
159
|
-
expect(boundary.options.showErrorDetails).toBe(false);
|
|
160
|
-
expect(boundary.options.allowDismiss).toBe(false);
|
|
161
|
-
expect(boundary.options.errorTitle).toBe('Custom Error');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test('should track initial error state', () => {
|
|
165
|
-
const widget = createMockWidget();
|
|
166
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
167
|
-
|
|
168
|
-
expect(boundary.errorState.hasError).toBe(false);
|
|
169
|
-
expect(boundary.errorState.error).toBe(null);
|
|
170
|
-
expect(boundary.errorState.retryCount).toBe(0);
|
|
171
|
-
expect(boundary.errorState.isRecovering).toBe(false);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test('should return error state info', () => {
|
|
175
|
-
const widget = createMockWidget({ id: 'test-widget' });
|
|
176
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
177
|
-
|
|
178
|
-
const state = boundary.getErrorState();
|
|
179
|
-
expect(state.hasError).toBe(false);
|
|
180
|
-
expect(state.error).toBe(null);
|
|
181
|
-
expect(state.health).toBeDefined();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('should report no error initially', () => {
|
|
185
|
-
const widget = createMockWidget();
|
|
186
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
187
|
-
|
|
188
|
-
expect(boundary.hasError()).toBe(false);
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
describe('Error boundary creation', () => {
|
|
193
|
-
test('should create widget successfully when no error', async () => {
|
|
194
|
-
const widget = createMockWidget();
|
|
195
|
-
const screen = createMockScreen();
|
|
196
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
197
|
-
|
|
198
|
-
const result = await boundary.create(screen, {});
|
|
199
|
-
|
|
200
|
-
expect(widget.create).toHaveBeenCalledWith(screen, {});
|
|
201
|
-
expect(boundary.hasError()).toBe(false);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test('should show error boundary when widget creation fails', async () => {
|
|
205
|
-
const widget = createMockWidget({
|
|
206
|
-
create: jest.fn().mockRejectedValue(new Error('Creation failed')),
|
|
207
|
-
});
|
|
208
|
-
const screen = createMockScreen();
|
|
209
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
210
|
-
|
|
211
|
-
await boundary.create(screen, {});
|
|
212
|
-
|
|
213
|
-
expect(boundary.hasError()).toBe(true);
|
|
214
|
-
expect(boundary.errorContainer).toBeDefined();
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test('should hide original box when showing error', async () => {
|
|
218
|
-
const originalBox = createMockBox();
|
|
219
|
-
const widget = createMockWidget({
|
|
220
|
-
create: jest.fn().mockRejectedValue(new Error('Creation failed')),
|
|
221
|
-
});
|
|
222
|
-
widget.box = originalBox;
|
|
223
|
-
|
|
224
|
-
const screen = createMockScreen();
|
|
225
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
226
|
-
boundary.originalBox = originalBox;
|
|
227
|
-
|
|
228
|
-
await boundary.create(screen, {});
|
|
229
|
-
|
|
230
|
-
expect(originalBox.hidden).toBe(true);
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
describe('Error boundary UI', () => {
|
|
235
|
-
test('should create error container with proper structure', async () => {
|
|
236
|
-
const widget = createMockWidget({
|
|
237
|
-
id: 'test-widget',
|
|
238
|
-
create: jest.fn().mockRejectedValue(new Error('Test error')),
|
|
239
|
-
});
|
|
240
|
-
const screen = createMockScreen();
|
|
241
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
242
|
-
|
|
243
|
-
await boundary.create(screen, {});
|
|
244
|
-
|
|
245
|
-
expect(boundary.errorContainer).toBeDefined();
|
|
246
|
-
expect(boundary.retryButton).toBeDefined();
|
|
247
|
-
expect(boundary.dismissButton).toBeDefined();
|
|
248
|
-
expect(boundary.errorText).toBeDefined();
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test('should display error message', async () => {
|
|
252
|
-
const errorMessage = 'Something went wrong';
|
|
253
|
-
const widget = createMockWidget({
|
|
254
|
-
create: jest.fn().mockRejectedValue(new Error(errorMessage)),
|
|
255
|
-
});
|
|
256
|
-
const screen = createMockScreen();
|
|
257
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
258
|
-
|
|
259
|
-
await boundary.create(screen, {});
|
|
260
|
-
|
|
261
|
-
expect(boundary.errorText.content).toContain(errorMessage);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test('should truncate long error messages', async () => {
|
|
265
|
-
const longMessage = 'A'.repeat(50);
|
|
266
|
-
const widget = createMockWidget({
|
|
267
|
-
create: jest.fn().mockRejectedValue(new Error(longMessage)),
|
|
268
|
-
});
|
|
269
|
-
const screen = createMockScreen();
|
|
270
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
271
|
-
|
|
272
|
-
await boundary.create(screen, {});
|
|
273
|
-
|
|
274
|
-
expect(boundary.errorText.content.length).toBeLessThan(longMessage.length);
|
|
275
|
-
expect(boundary.errorText.content).toContain('...');
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test('should show retry count after retries', async () => {
|
|
279
|
-
const widget = createMockWidget({
|
|
280
|
-
create: jest.fn().mockRejectedValue(new Error('Test')),
|
|
281
|
-
});
|
|
282
|
-
const screen = createMockScreen();
|
|
283
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
284
|
-
boundary.errorState.retryCount = 2;
|
|
285
|
-
|
|
286
|
-
await boundary.create(screen, {});
|
|
287
|
-
|
|
288
|
-
// Check that container has retry count text
|
|
289
|
-
const hasRetryCount = mockBlessedElements.some(
|
|
290
|
-
el => el.content && el.content.includes('Retry 2')
|
|
291
|
-
);
|
|
292
|
-
expect(hasRetryCount).toBe(true);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test('should not show dismiss button when disabled', async () => {
|
|
296
|
-
const widget = createMockWidget({
|
|
297
|
-
create: jest.fn().mockRejectedValue(new Error('Test')),
|
|
298
|
-
});
|
|
299
|
-
const screen = createMockScreen();
|
|
300
|
-
const boundary = new WidgetErrorBoundary(widget, { allowDismiss: false });
|
|
301
|
-
|
|
302
|
-
await boundary.create(screen, {});
|
|
303
|
-
|
|
304
|
-
expect(boundary.dismissButton).toBeNull();
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
describe('Retry functionality', () => {
|
|
309
|
-
test('should reset widget on successful retry', async () => {
|
|
310
|
-
let shouldFail = true;
|
|
311
|
-
const widget = createMockWidget({
|
|
312
|
-
create: jest.fn().mockImplementation(() => {
|
|
313
|
-
if (shouldFail) {
|
|
314
|
-
shouldFail = false;
|
|
315
|
-
return Promise.reject(new Error('Initial failure'));
|
|
316
|
-
}
|
|
317
|
-
return Promise.resolve();
|
|
318
|
-
}),
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
const screen = createMockScreen();
|
|
322
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
323
|
-
|
|
324
|
-
await boundary.create(screen, {});
|
|
325
|
-
expect(boundary.hasError()).toBe(true);
|
|
326
|
-
|
|
327
|
-
// Simulate retry
|
|
328
|
-
await boundary.handleRetry();
|
|
329
|
-
|
|
330
|
-
expect(boundary.hasError()).toBe(false);
|
|
331
|
-
expect(boundary.errorState.retryCount).toBe(1);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test('should increment retry count on failure', async () => {
|
|
335
|
-
const widget = createMockWidget({
|
|
336
|
-
create: jest.fn().mockRejectedValue(new Error('Always fails')),
|
|
337
|
-
});
|
|
338
|
-
const screen = createMockScreen();
|
|
339
|
-
const boundary = new WidgetErrorBoundary(widget, { maxRetries: 3 });
|
|
340
|
-
|
|
341
|
-
await boundary.create(screen, {});
|
|
342
|
-
expect(boundary.errorState.retryCount).toBe(0);
|
|
343
|
-
|
|
344
|
-
await boundary.handleRetry();
|
|
345
|
-
expect(boundary.errorState.retryCount).toBe(1);
|
|
346
|
-
|
|
347
|
-
await boundary.handleRetry();
|
|
348
|
-
expect(boundary.errorState.retryCount).toBe(2);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
test('should prevent concurrent retries', async () => {
|
|
352
|
-
const widget = createMockWidget({
|
|
353
|
-
create: jest.fn().mockImplementation(() =>
|
|
354
|
-
new Promise(resolve => setTimeout(resolve, 100))
|
|
355
|
-
),
|
|
356
|
-
});
|
|
357
|
-
const screen = createMockScreen();
|
|
358
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
359
|
-
|
|
360
|
-
boundary.errorState.isRecovering = true;
|
|
361
|
-
|
|
362
|
-
// Second retry should be ignored
|
|
363
|
-
await boundary.handleRetry();
|
|
364
|
-
expect(boundary.errorState.isRecovering).toBe(true);
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
test('should call onRetry callback on success', async () => {
|
|
368
|
-
const onRetry = jest.fn();
|
|
369
|
-
const widget = createMockWidget({
|
|
370
|
-
create: jest.fn().mockResolvedValue(undefined),
|
|
371
|
-
});
|
|
372
|
-
const screen = createMockScreen();
|
|
373
|
-
const boundary = new WidgetErrorBoundary(widget, { onRetry });
|
|
374
|
-
|
|
375
|
-
boundary.errorState.hasError = true;
|
|
376
|
-
await boundary.handleRetry();
|
|
377
|
-
|
|
378
|
-
expect(onRetry).toHaveBeenCalledWith(true, 1);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test('should call onRetry callback on failure', async () => {
|
|
382
|
-
const onRetry = jest.fn();
|
|
383
|
-
const widget = createMockWidget({
|
|
384
|
-
create: jest.fn().mockRejectedValue(new Error('Still fails')),
|
|
385
|
-
});
|
|
386
|
-
const screen = createMockScreen();
|
|
387
|
-
const boundary = new WidgetErrorBoundary(widget, { onRetry });
|
|
388
|
-
|
|
389
|
-
boundary.errorState.hasError = true;
|
|
390
|
-
await boundary.handleRetry();
|
|
391
|
-
|
|
392
|
-
expect(onRetry).toHaveBeenCalledWith(false, 1, expect.any(Error));
|
|
393
|
-
});
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
describe('Dismiss functionality', () => {
|
|
397
|
-
test('should clear error boundary on dismiss', async () => {
|
|
398
|
-
const onDismiss = jest.fn();
|
|
399
|
-
const widget = createMockWidget({
|
|
400
|
-
create: jest.fn().mockRejectedValue(new Error('Test')),
|
|
401
|
-
});
|
|
402
|
-
const screen = createMockScreen();
|
|
403
|
-
const boundary = new WidgetErrorBoundary(widget, { onDismiss });
|
|
404
|
-
|
|
405
|
-
await boundary.create(screen, {});
|
|
406
|
-
expect(boundary.errorContainer).toBeDefined();
|
|
407
|
-
|
|
408
|
-
boundary.handleDismiss();
|
|
409
|
-
|
|
410
|
-
expect(boundary.errorContainer).toBeNull();
|
|
411
|
-
expect(onDismiss).toHaveBeenCalled();
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
describe('Data and render operations', () => {
|
|
416
|
-
test('should return null data when in error state', async () => {
|
|
417
|
-
const widget = createMockWidget({
|
|
418
|
-
getData: jest.fn().mockResolvedValue({ test: true }),
|
|
419
|
-
});
|
|
420
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
421
|
-
boundary.errorState.hasError = true;
|
|
422
|
-
|
|
423
|
-
const data = await boundary.getData();
|
|
424
|
-
|
|
425
|
-
expect(data).toBeNull();
|
|
426
|
-
expect(widget.getData).not.toHaveBeenCalled();
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
test('should pass data through when not in error state', async () => {
|
|
430
|
-
const widget = createMockWidget({
|
|
431
|
-
getData: jest.fn().mockResolvedValue({ test: true }),
|
|
432
|
-
});
|
|
433
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
434
|
-
|
|
435
|
-
const data = await boundary.getData();
|
|
436
|
-
|
|
437
|
-
expect(data).toEqual({ test: true });
|
|
438
|
-
expect(widget.getData).toHaveBeenCalled();
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
test('should catch errors in getData and show boundary', async () => {
|
|
442
|
-
const widget = createMockWidget({
|
|
443
|
-
getData: jest.fn().mockRejectedValue(new Error('Data error')),
|
|
444
|
-
});
|
|
445
|
-
const screen = createMockScreen();
|
|
446
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
447
|
-
boundary.parentScreen = screen;
|
|
448
|
-
|
|
449
|
-
await boundary.getData();
|
|
450
|
-
|
|
451
|
-
expect(boundary.hasError()).toBe(true);
|
|
452
|
-
expect(boundary.errorState.error.message).toBe('Data error');
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
test('should not render when in error state', async () => {
|
|
456
|
-
const widget = createMockWidget();
|
|
457
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
458
|
-
boundary.errorState.hasError = true;
|
|
459
|
-
|
|
460
|
-
await boundary.render({ test: true });
|
|
461
|
-
|
|
462
|
-
expect(widget.render).not.toHaveBeenCalled();
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
test('should pass render through when not in error state', async () => {
|
|
466
|
-
const widget = createMockWidget();
|
|
467
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
468
|
-
|
|
469
|
-
await boundary.render({ test: true });
|
|
470
|
-
|
|
471
|
-
expect(widget.render).toHaveBeenCalledWith({ test: true });
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
test('should catch errors in render and show boundary', async () => {
|
|
475
|
-
const widget = createMockWidget({
|
|
476
|
-
render: jest.fn().mockImplementation(() => {
|
|
477
|
-
throw new Error('Render error');
|
|
478
|
-
}),
|
|
479
|
-
});
|
|
480
|
-
const screen = createMockScreen();
|
|
481
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
482
|
-
boundary.parentScreen = screen;
|
|
483
|
-
|
|
484
|
-
await boundary.render({});
|
|
485
|
-
|
|
486
|
-
expect(boundary.hasError()).toBe(true);
|
|
487
|
-
});
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
describe('Error state management', () => {
|
|
491
|
-
test('should reset error state', async () => {
|
|
492
|
-
const widget = createMockWidget({
|
|
493
|
-
create: jest.fn().mockResolvedValue(undefined),
|
|
494
|
-
});
|
|
495
|
-
const screen = createMockScreen();
|
|
496
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
497
|
-
|
|
498
|
-
await boundary.create(screen, {});
|
|
499
|
-
boundary.errorState.hasError = true;
|
|
500
|
-
boundary.errorState.retryCount = 2;
|
|
501
|
-
boundary.errorState.error = new Error('Test');
|
|
502
|
-
|
|
503
|
-
await boundary.reset();
|
|
504
|
-
|
|
505
|
-
expect(boundary.errorState.hasError).toBe(false);
|
|
506
|
-
expect(boundary.errorState.retryCount).toBe(0);
|
|
507
|
-
expect(boundary.errorState.error).toBe(null);
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
test('should force show error boundary', async () => {
|
|
511
|
-
const widget = createMockWidget();
|
|
512
|
-
const screen = createMockScreen();
|
|
513
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
514
|
-
|
|
515
|
-
// First create to initialize the boundary with a screen
|
|
516
|
-
await boundary.create(screen, {});
|
|
517
|
-
// Clear any error state from create
|
|
518
|
-
boundary.errorState.hasError = false;
|
|
519
|
-
|
|
520
|
-
boundary.showError('Custom error message');
|
|
521
|
-
|
|
522
|
-
expect(boundary.hasError()).toBe(true);
|
|
523
|
-
expect(boundary.errorContainer).toBeDefined();
|
|
524
|
-
expect(boundary.errorText.content).toContain('Custom error message');
|
|
525
|
-
});
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
describe('Keyboard navigation', () => {
|
|
529
|
-
test('should handle retry on keypress', async () => {
|
|
530
|
-
const widget = createMockWidget({
|
|
531
|
-
create: jest.fn().mockResolvedValue(undefined),
|
|
532
|
-
});
|
|
533
|
-
const screen = createMockScreen();
|
|
534
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
535
|
-
|
|
536
|
-
await boundary.create(screen, {});
|
|
537
|
-
boundary.errorState.hasError = true;
|
|
538
|
-
|
|
539
|
-
const handleRetrySpy = jest.spyOn(boundary, 'handleRetry').mockResolvedValue();
|
|
540
|
-
|
|
541
|
-
boundary.handleKeypress('r', { name: 'r' });
|
|
542
|
-
|
|
543
|
-
expect(handleRetrySpy).toHaveBeenCalled();
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
test('should handle dismiss on keypress', async () => {
|
|
547
|
-
const widget = createMockWidget();
|
|
548
|
-
const screen = createMockScreen();
|
|
549
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
550
|
-
|
|
551
|
-
await boundary.create(screen, {});
|
|
552
|
-
boundary.errorState.hasError = true;
|
|
553
|
-
|
|
554
|
-
const handleDismissSpy = jest.spyOn(boundary, 'handleDismiss').mockResolvedValue();
|
|
555
|
-
|
|
556
|
-
boundary.handleKeypress('d', { name: 'd' });
|
|
557
|
-
|
|
558
|
-
expect(handleDismissSpy).toHaveBeenCalled();
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
test('should ignore keypress when not in error state', () => {
|
|
562
|
-
const boundary = new WidgetErrorBoundary(createMockWidget());
|
|
563
|
-
boundary.errorState.hasError = false;
|
|
564
|
-
|
|
565
|
-
const handleRetrySpy = jest.spyOn(boundary, 'handleRetry');
|
|
566
|
-
|
|
567
|
-
boundary.handleKeypress('r', { name: 'r' });
|
|
568
|
-
|
|
569
|
-
expect(handleRetrySpy).not.toHaveBeenCalled();
|
|
570
|
-
});
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
describe('Error styles', () => {
|
|
574
|
-
test('should provide default error styles', () => {
|
|
575
|
-
expect(ErrorStyles.CONTAINER).toBeDefined();
|
|
576
|
-
expect(ErrorStyles.TITLE).toBeDefined();
|
|
577
|
-
expect(ErrorStyles.MESSAGE).toBeDefined();
|
|
578
|
-
expect(ErrorStyles.RETRY_BUTTON).toBeDefined();
|
|
579
|
-
expect(ErrorStyles.DISMISS_BUTTON).toBeDefined();
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
test('should merge theme colors into styles', () => {
|
|
583
|
-
const widget = createMockWidget();
|
|
584
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
585
|
-
|
|
586
|
-
const styles = boundary.getErrorStyles({ error: 'yellow', gray: 'blue' });
|
|
587
|
-
|
|
588
|
-
expect(styles.container.style.border.fg).toBe('yellow');
|
|
589
|
-
expect(styles.title.fg).toBe('yellow');
|
|
590
|
-
expect(styles.errorDetail.fg).toBe('blue');
|
|
591
|
-
});
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
describe('Destroy', () => {
|
|
595
|
-
test('should clear error boundary and destroy widget', async () => {
|
|
596
|
-
const widget = createMockWidget();
|
|
597
|
-
const screen = createMockScreen();
|
|
598
|
-
const boundary = new WidgetErrorBoundary(widget);
|
|
599
|
-
|
|
600
|
-
await boundary.create(screen, {});
|
|
601
|
-
boundary.errorState.hasError = true;
|
|
602
|
-
|
|
603
|
-
await boundary.destroy();
|
|
604
|
-
|
|
605
|
-
expect(widget.destroy).toHaveBeenCalled();
|
|
606
|
-
expect(boundary.errorContainer).toBeNull();
|
|
607
|
-
});
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
describe('withErrorBoundary helper', () => {
|
|
611
|
-
test('should create error boundary for widget', () => {
|
|
612
|
-
const widget = createMockWidget();
|
|
613
|
-
const boundary = withErrorBoundary(widget, { maxRetries: 5 });
|
|
614
|
-
|
|
615
|
-
expect(boundary).toBeInstanceOf(WidgetErrorBoundary);
|
|
616
|
-
expect(boundary.widget).toBe(widget);
|
|
617
|
-
expect(boundary.options.maxRetries).toBe(5);
|
|
618
|
-
});
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
describe('ErrorBoundaryManager', () => {
|
|
622
|
-
test('should create manager with empty boundaries', () => {
|
|
623
|
-
const manager = new ErrorBoundaryManager();
|
|
624
|
-
|
|
625
|
-
expect(manager.getStats().total).toBe(0);
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
test('should set global options', () => {
|
|
629
|
-
const manager = new ErrorBoundaryManager();
|
|
630
|
-
manager.setGlobalOptions({ maxRetries: 10, retryDelay: 1000 });
|
|
631
|
-
|
|
632
|
-
expect(manager.globalOptions.maxRetries).toBe(10);
|
|
633
|
-
expect(manager.globalOptions.retryDelay).toBe(1000);
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
test('should wrap widget with error boundary', () => {
|
|
637
|
-
const manager = new ErrorBoundaryManager();
|
|
638
|
-
const widget = createMockWidget({ id: 'widget-1' });
|
|
639
|
-
|
|
640
|
-
const boundary = manager.wrap(widget);
|
|
641
|
-
|
|
642
|
-
expect(boundary).toBeInstanceOf(WidgetErrorBoundary);
|
|
643
|
-
expect(manager.get('widget-1')).toBe(boundary);
|
|
644
|
-
expect(manager.getStats().total).toBe(1);
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
test('should merge global options with local options', () => {
|
|
648
|
-
const manager = new ErrorBoundaryManager();
|
|
649
|
-
manager.setGlobalOptions({ maxRetries: 10 });
|
|
650
|
-
|
|
651
|
-
const widget = createMockWidget({ id: 'widget-1' });
|
|
652
|
-
const boundary = manager.wrap(widget, { retryDelay: 2000 });
|
|
653
|
-
|
|
654
|
-
expect(boundary.options.maxRetries).toBe(10);
|
|
655
|
-
expect(boundary.options.retryDelay).toBe(2000);
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
test('should remove boundary', () => {
|
|
659
|
-
const manager = new ErrorBoundaryManager();
|
|
660
|
-
const widget = createMockWidget({ id: 'widget-1' });
|
|
661
|
-
|
|
662
|
-
manager.wrap(widget);
|
|
663
|
-
expect(manager.get('widget-1')).toBeDefined();
|
|
664
|
-
|
|
665
|
-
manager.remove('widget-1');
|
|
666
|
-
expect(manager.get('widget-1')).toBeNull();
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
test('should get all error states', () => {
|
|
670
|
-
const manager = new ErrorBoundaryManager();
|
|
671
|
-
const widget1 = createMockWidget({ id: 'widget-1' });
|
|
672
|
-
const widget2 = createMockWidget({ id: 'widget-2' });
|
|
673
|
-
|
|
674
|
-
manager.wrap(widget1);
|
|
675
|
-
manager.wrap(widget2);
|
|
676
|
-
|
|
677
|
-
const states = manager.getAllErrorStates();
|
|
678
|
-
|
|
679
|
-
expect(Object.keys(states)).toHaveLength(2);
|
|
680
|
-
expect(states['widget-1']).toBeDefined();
|
|
681
|
-
expect(states['widget-2']).toBeDefined();
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
test('should retry all failed widgets', async () => {
|
|
685
|
-
const manager = new ErrorBoundaryManager();
|
|
686
|
-
const widget1 = createMockWidget({
|
|
687
|
-
id: 'widget-1',
|
|
688
|
-
create: jest.fn().mockResolvedValue(undefined),
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
const boundary = manager.wrap(widget1);
|
|
692
|
-
boundary.errorState.hasError = true;
|
|
693
|
-
|
|
694
|
-
const results = await manager.retryAll();
|
|
695
|
-
|
|
696
|
-
expect(results['widget-1']).toBeDefined();
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
test('should clear all boundaries', () => {
|
|
700
|
-
const manager = new ErrorBoundaryManager();
|
|
701
|
-
const widget1 = createMockWidget({ id: 'widget-1' });
|
|
702
|
-
const widget2 = createMockWidget({ id: 'widget-2' });
|
|
703
|
-
|
|
704
|
-
manager.wrap(widget1);
|
|
705
|
-
manager.wrap(widget2);
|
|
706
|
-
|
|
707
|
-
manager.clearAll();
|
|
708
|
-
|
|
709
|
-
expect(manager.getStats().total).toBe(0);
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
test('should calculate stats correctly', () => {
|
|
713
|
-
const manager = new ErrorBoundaryManager();
|
|
714
|
-
const widget1 = createMockWidget({ id: 'widget-1' });
|
|
715
|
-
const widget2 = createMockWidget({ id: 'widget-2' });
|
|
716
|
-
const widget3 = createMockWidget({ id: 'widget-3' });
|
|
717
|
-
|
|
718
|
-
const b1 = manager.wrap(widget1);
|
|
719
|
-
const b2 = manager.wrap(widget2);
|
|
720
|
-
manager.wrap(widget3);
|
|
721
|
-
|
|
722
|
-
b1.errorState.hasError = true;
|
|
723
|
-
b2.errorState.hasError = true;
|
|
724
|
-
|
|
725
|
-
const stats = manager.getStats();
|
|
726
|
-
|
|
727
|
-
expect(stats.total).toBe(3);
|
|
728
|
-
expect(stats.inError).toBe(2);
|
|
729
|
-
expect(stats.healthy).toBe(1);
|
|
730
|
-
});
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
describe('getErrorBoundaryManager singleton', () => {
|
|
734
|
-
test('should return singleton instance', () => {
|
|
735
|
-
const manager1 = getErrorBoundaryManager();
|
|
736
|
-
const manager2 = getErrorBoundaryManager();
|
|
737
|
-
|
|
738
|
-
expect(manager1).toBe(manager2);
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
test('should return new instance after clear', () => {
|
|
742
|
-
const manager = getErrorBoundaryManager();
|
|
743
|
-
manager.wrap(createMockWidget({ id: 'test' }));
|
|
744
|
-
|
|
745
|
-
const stats = manager.getStats();
|
|
746
|
-
expect(stats.total).toBeGreaterThan(0);
|
|
747
|
-
});
|
|
748
|
-
});
|
|
749
|
-
});
|