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