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.
Files changed (89) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/docs/API.md +1 -2
  3. package/index.js +31 -41
  4. package/package.json +22 -12
  5. package/src/auto-save.js +11 -5
  6. package/src/cli/export-snapshot.js +3 -1
  7. package/src/cli/import-snapshot.js +3 -1
  8. package/src/config.js +9 -15
  9. package/src/errors.js +0 -9
  10. package/src/security.js +6 -2
  11. package/src/snapshot.js +73 -26
  12. package/src/web-server.js +7 -10
  13. package/.c8rc.json +0 -38
  14. package/.dockerignore +0 -68
  15. package/.github/dependabot.yml +0 -45
  16. package/.github/pull_request_template.md +0 -39
  17. package/.github/workflows/ci.yml +0 -147
  18. package/.github/workflows/release.yml +0 -40
  19. package/.github/workflows/security.yml +0 -84
  20. package/.husky/pre-commit +0 -1
  21. package/.planning/codebase/ARCHITECTURE.md +0 -206
  22. package/.planning/codebase/INTEGRATIONS.md +0 -150
  23. package/.planning/codebase/STACK.md +0 -122
  24. package/.planning/codebase/STRUCTURE.md +0 -201
  25. package/CONTRIBUTING.md +0 -378
  26. package/Dockerfile +0 -54
  27. package/FEATURES.md +0 -307
  28. package/TODO.md +0 -28
  29. package/ai.openclaw.dashboard.plist +0 -35
  30. package/build-cjs.js +0 -127
  31. package/cjs-shim.js +0 -13
  32. package/dist/clawdash +0 -1729
  33. package/dist/clawdash.meta.json +0 -2236
  34. package/dist/widgets.cjs +0 -6511
  35. package/docker-compose.yml +0 -67
  36. package/esbuild.config.js +0 -158
  37. package/eslint.config.js +0 -56
  38. package/examples/plugins/README.md +0 -122
  39. package/examples/plugins/api-status/index.js +0 -294
  40. package/examples/plugins/api-status/plugin.json +0 -19
  41. package/examples/plugins/hello-world/index.js +0 -104
  42. package/examples/plugins/hello-world/plugin.json +0 -15
  43. package/examples/plugins/system-metrics-chart/index.js +0 -339
  44. package/examples/plugins/system-metrics-chart/plugin.json +0 -18
  45. package/examples/plugins/weather-widget/index.js +0 -136
  46. package/examples/plugins/weather-widget/plugin.json +0 -19
  47. package/index.cjs +0 -23005
  48. package/jest.config.js +0 -11
  49. package/scripts/release.js +0 -595
  50. package/src/database.js +0 -734
  51. package/tests/alerts.test.js +0 -437
  52. package/tests/auto-save.test.js +0 -529
  53. package/tests/cache.test.js +0 -317
  54. package/tests/cli.test.js +0 -351
  55. package/tests/command-palette.test.js +0 -320
  56. package/tests/config-processor.test.js +0 -452
  57. package/tests/config-validator.test.js +0 -710
  58. package/tests/config-watcher.test.js +0 -594
  59. package/tests/config.test.js +0 -755
  60. package/tests/database.test.js +0 -438
  61. package/tests/errors.test.js +0 -189
  62. package/tests/example-plugins.test.js +0 -624
  63. package/tests/integration.test.js +0 -1321
  64. package/tests/loading-states.test.js +0 -300
  65. package/tests/manifest-validation-on-load.test.js +0 -671
  66. package/tests/performance-monitor.test.js +0 -190
  67. package/tests/plugin-api-rate-limit.test.js +0 -302
  68. package/tests/plugin-errors.test.js +0 -311
  69. package/tests/plugin-lifecycle-e2e.test.js +0 -1036
  70. package/tests/plugin-reload.test.js +0 -764
  71. package/tests/rate-limiter.test.js +0 -539
  72. package/tests/removed-dependencies.test.js +0 -74
  73. package/tests/retry.test.js +0 -308
  74. package/tests/security.test.js +0 -411
  75. package/tests/settings-modal.test.js +0 -226
  76. package/tests/theme-selector.test.js +0 -57
  77. package/tests/utils.js +0 -242
  78. package/tests/utils.test.js +0 -317
  79. package/tests/validate-plugin-cli.test.js +0 -359
  80. package/tests/validation.test.js +0 -837
  81. package/tests/web-server.test.js +0 -646
  82. package/tests/widget-arrange-mode.test.js +0 -183
  83. package/tests/widget-config-hot-reload.test.js +0 -465
  84. package/tests/widget-dependency.test.js +0 -591
  85. package/tests/widget-error-boundary.test.js +0 -749
  86. package/tests/widget-error-isolation.test.js +0 -469
  87. package/tests/widget-integration.test.js +0 -1147
  88. package/tests/widget-refresh-intervals.test.js +0 -284
  89. 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
- });