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,624 +0,0 @@
1
- /**
2
- * Example Plugin Tests
3
- * Tests for loading, lifecycle hooks, and manifest validation of example plugins
4
- */
5
-
6
- import { jest } from '@jest/globals';
7
- import { join, dirname } from 'path';
8
- import { fileURLToPath } from 'url';
9
- import { mkdtemp, rm, writeFile, readFile, mkdir } from 'fs/promises';
10
- import { existsSync } from 'fs';
11
- import { tmpdir } from 'os';
12
-
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = dirname(__filename);
15
-
16
- // Import modules under test
17
- import { WidgetLoader } from '../src/widgets/widget-loader.js';
18
- import { validateManifest, BaseWidget, PluginAPI } from '../src/widgets/plugin-api.js';
19
-
20
- describe('Example Plugin Loading', () => {
21
- let loader;
22
- let tempDir;
23
-
24
- beforeEach(async () => {
25
- tempDir = await mkdtemp(join(tmpdir(), 'example-plugin-test-'));
26
- loader = new WidgetLoader({ pluginsDir: tempDir });
27
- });
28
-
29
- afterEach(async () => {
30
- if (loader) {
31
- await loader.clear();
32
- }
33
- if (tempDir) {
34
- await rm(tempDir, { recursive: true, force: true });
35
- }
36
- });
37
-
38
- describe('Plugin Manifest Validation', () => {
39
- test('should validate hello-world plugin manifest has required fields', async () => {
40
- const manifestPath = join(__dirname, '..', 'examples', 'plugins', 'hello-world', 'plugin.json');
41
- const manifest = JSON.parse(await readFile(manifestPath, 'utf-8'));
42
-
43
- // The validateManifest expects 'entryPoint' but example plugins use 'id'
44
- // Test that the manifest has the expected fields for the example plugin format
45
- expect(manifest.id).toBe('example-hello-world');
46
- expect(manifest.name).toBe('Hello World');
47
- expect(manifest.version).toBe('1.0.0');
48
- expect(manifest.type).toBe('widget');
49
-
50
- // Validate using the expected format (entryPoint required)
51
- const compatibleManifest = {
52
- name: manifest.name,
53
- version: manifest.version,
54
- entryPoint: 'index.js',
55
- };
56
- expect(() => validateManifest(compatibleManifest)).not.toThrow();
57
- });
58
-
59
- test('should validate api-status plugin manifest has required fields', async () => {
60
- const manifestPath = join(__dirname, '..', 'examples', 'plugins', 'api-status', 'plugin.json');
61
- const manifest = JSON.parse(await readFile(manifestPath, 'utf-8'));
62
-
63
- expect(manifest.id).toBe('example-api-status');
64
- expect(manifest.name).toBe('API Status');
65
- expect(manifest.version).toBe('1.1.0');
66
- expect(manifest.type).toBe('widget');
67
-
68
- const compatibleManifest = {
69
- name: manifest.name,
70
- version: manifest.version,
71
- entryPoint: 'index.js',
72
- };
73
- expect(() => validateManifest(compatibleManifest)).not.toThrow();
74
- });
75
-
76
- test('should validate system-metrics-chart plugin manifest has required fields', async () => {
77
- const manifestPath = join(__dirname, '..', 'examples', 'plugins', 'system-metrics-chart', 'plugin.json');
78
- const manifest = JSON.parse(await readFile(manifestPath, 'utf-8'));
79
-
80
- expect(manifest.id).toBe('example-system-metrics-chart');
81
- expect(manifest.name).toBe('System Metrics Chart');
82
- expect(manifest.version).toBe('1.0.0');
83
- expect(manifest.type).toBe('widget');
84
-
85
- const compatibleManifest = {
86
- name: manifest.name,
87
- version: manifest.version,
88
- entryPoint: 'index.js',
89
- };
90
- expect(() => validateManifest(compatibleManifest)).not.toThrow();
91
- });
92
-
93
- test('should validate weather-widget plugin manifest has required fields', async () => {
94
- const manifestPath = join(__dirname, '..', 'examples', 'plugins', 'weather-widget', 'plugin.json');
95
- const manifest = JSON.parse(await readFile(manifestPath, 'utf-8'));
96
-
97
- expect(manifest.id).toBe('example-weather');
98
- expect(manifest.name).toBe('Weather');
99
- expect(manifest.version).toBe('1.0.0');
100
- expect(manifest.type).toBe('widget');
101
-
102
- const compatibleManifest = {
103
- name: manifest.name,
104
- version: manifest.version,
105
- entryPoint: 'index.js',
106
- };
107
- expect(() => validateManifest(compatibleManifest)).not.toThrow();
108
- });
109
- });
110
-
111
- describe('Plugin Directory Structure', () => {
112
- test('hello-world has required files', () => {
113
- const pluginDir = join(__dirname, '..', 'examples', 'plugins', 'hello-world');
114
-
115
- expect(existsSync(join(pluginDir, 'plugin.json'))).toBe(true);
116
- expect(existsSync(join(pluginDir, 'index.js'))).toBe(true);
117
- });
118
-
119
- test('api-status has required files', () => {
120
- const pluginDir = join(__dirname, '..', 'examples', 'plugins', 'api-status');
121
-
122
- expect(existsSync(join(pluginDir, 'plugin.json'))).toBe(true);
123
- expect(existsSync(join(pluginDir, 'index.js'))).toBe(true);
124
- });
125
-
126
- test('system-metrics-chart has required files', () => {
127
- const pluginDir = join(__dirname, '..', 'examples', 'plugins', 'system-metrics-chart');
128
-
129
- expect(existsSync(join(pluginDir, 'plugin.json'))).toBe(true);
130
- expect(existsSync(join(pluginDir, 'index.js'))).toBe(true);
131
- });
132
-
133
- test('weather-widget has required files', () => {
134
- const pluginDir = join(__dirname, '..', 'examples', 'plugins', 'weather-widget');
135
-
136
- expect(existsSync(join(pluginDir, 'plugin.json'))).toBe(true);
137
- expect(existsSync(join(pluginDir, 'index.js'))).toBe(true);
138
- });
139
- });
140
-
141
- describe('Plugin Loading via WidgetLoader', () => {
142
- test('should discover hello-world plugin', async () => {
143
- // Copy hello-world to temp dir for testing
144
- const sourceDir = join(__dirname, '..', 'examples', 'plugins', 'hello-world');
145
- const targetDir = join(tempDir, 'example-hello-world');
146
-
147
- // Create target directory first
148
- await mkdir(targetDir, { recursive: true });
149
-
150
- await writeFile(
151
- join(targetDir, 'plugin.json'),
152
- await readFile(join(sourceDir, 'plugin.json'), 'utf-8')
153
- );
154
- await writeFile(
155
- join(targetDir, 'index.js'),
156
- await readFile(join(sourceDir, 'index.js'), 'utf-8')
157
- );
158
-
159
- const discovered = await loader.discoverPlugins();
160
-
161
- expect(discovered.some(p => p.id === 'example-hello-world')).toBe(true);
162
- });
163
-
164
- test('should register and load hello-world widget', async () => {
165
- const sourceDir = join(__dirname, '..', 'examples', 'plugins', 'hello-world');
166
-
167
- // Import the plugin module
168
- const pluginPath = join(sourceDir, 'index.js');
169
- const pluginModule = await import(pluginPath);
170
-
171
- // Create mock API
172
- const mockApi = new PluginAPI();
173
-
174
- // Register the widget
175
- const metadata = {
176
- id: 'example-hello-world',
177
- name: 'Hello World',
178
- version: '1.0.0',
179
- };
180
-
181
- loader.register('example-hello-world', metadata, async () => {
182
- const WidgetClass = pluginModule.default || pluginModule.HelloWidget;
183
- const widget = new WidgetClass({
184
- api: mockApi,
185
- config: { message: 'Hello, Test!' },
186
- });
187
- return widget;
188
- });
189
-
190
- // Load the widget
191
- const widget = await loader.load('example-hello-world');
192
-
193
- expect(widget).toBeDefined();
194
- expect(widget.name).toBe('Hello World');
195
- });
196
-
197
- test('should execute lifecycle hooks for hello-world', async () => {
198
- const sourceDir = join(__dirname, '..', 'examples', 'plugins', 'hello-world');
199
- const pluginPath = join(sourceDir, 'index.js');
200
- const pluginModule = await import(pluginPath);
201
-
202
- const mockApi = new PluginAPI();
203
-
204
- const WidgetClass = pluginModule.default || pluginModule.HelloWidget;
205
- const widget = new WidgetClass({
206
- api: mockApi,
207
- config: { message: 'Test', showTimestamp: false },
208
- });
209
-
210
- // Test init hook
211
- const initResult = await widget.init();
212
- expect(initResult).toBe(true);
213
- expect(widget.loaded).toBe(false); // Not loaded until create
214
-
215
- // Note: create() requires a blessed screen which we can't fully mock in tests
216
- // But we can test that the widget structure is correct
217
- expect(widget.getData).toBeDefined();
218
- expect(widget.render).toBeDefined();
219
- expect(widget.destroy).toBeDefined();
220
-
221
- // Test getData
222
- const data = await widget.getData();
223
- expect(data).toBeDefined();
224
- expect(data.message).toBe('Test');
225
- expect(data.timestamp).toBeNull(); // showTimestamp is false
226
-
227
- // Test destroy
228
- await widget.destroy();
229
- expect(widget.loaded).toBe(false);
230
- });
231
-
232
- test('should validate widget has required lifecycle methods', async () => {
233
- const sourceDir = join(__dirname, '..', 'examples', 'plugins', 'hello-world');
234
- const pluginPath = join(sourceDir, 'index.js');
235
- const pluginModule = await import(pluginPath);
236
-
237
- const WidgetClass = pluginModule.default || pluginModule.HelloWidget;
238
- const widget = new WidgetClass();
239
-
240
- // Check all required methods exist
241
- expect(typeof widget.init).toBe('function');
242
- expect(typeof widget.create).toBe('function');
243
- expect(typeof widget.getData).toBe('function');
244
- expect(typeof widget.render).toBe('function');
245
- expect(typeof widget.destroy).toBe('function');
246
- });
247
- });
248
-
249
- describe('API Status Widget Tests', () => {
250
- let originalFetch;
251
-
252
- beforeEach(() => {
253
- // Store original fetch
254
- originalFetch = globalThis.fetch;
255
- });
256
-
257
- afterEach(() => {
258
- // Restore original fetch
259
- globalThis.fetch = originalFetch;
260
- });
261
-
262
- test('should create api-status widget instance', async () => {
263
- const sourceDir = join(__dirname, '..', 'examples', 'plugins', 'api-status');
264
- const pluginPath = join(sourceDir, 'index.js');
265
- const pluginModule = await import(pluginPath);
266
-
267
- const WidgetClass = pluginModule.default || pluginModule.ApiStatusWidget;
268
- const widget = new WidgetClass({
269
- config: {
270
- apiUrl: 'https://api.github.com/zen',
271
- timeout: 5000,
272
- retries: 1,
273
- },
274
- });
275
-
276
- expect(widget.name).toBe('API Status');
277
- expect(widget.config.apiUrl).toBe('https://api.github.com/zen');
278
- });
279
-
280
- test('should handle getData with custom config', async () => {
281
- // Mock fetch to avoid real HTTP requests
282
- const mockResponse = {
283
- ok: true,
284
- status: 200,
285
- statusText: 'OK',
286
- headers: new Map([['content-type', 'text/plain']]),
287
- json: async () => ({}),
288
- text: async () => 'Mock API response',
289
- };
290
-
291
- globalThis.fetch = jest.fn().mockResolvedValue(mockResponse);
292
-
293
- const sourceDir = join(__dirname, '..', 'examples', 'plugins', 'api-status');
294
- const pluginPath = join(sourceDir, 'index.js');
295
- const pluginModule = await import(pluginPath);
296
-
297
- const WidgetClass = pluginModule.default || pluginModule.ApiStatusWidget;
298
- const widget = new WidgetClass({
299
- config: {
300
- apiUrl: 'https://api.github.com/zen',
301
- timeout: 100,
302
- retries: 0, // No retries for fast test
303
- },
304
- });
305
-
306
- // Mock updateStatus to avoid needing UI elements
307
- widget.updateStatus = jest.fn();
308
-
309
- const data = await widget.getData();
310
-
311
- // Should succeed with mocked response
312
- expect(data).toBeDefined();
313
- expect(data).toHaveProperty('success');
314
- expect(data).toHaveProperty('timestamp');
315
- expect(data).toHaveProperty('apiUrl');
316
- expect(globalThis.fetch).toHaveBeenCalled();
317
- });
318
-
319
- test('should handle fetch errors gracefully', async () => {
320
- // Mock fetch to return an error
321
- globalThis.fetch = jest.fn().mockRejectedValue(new Error('Network error'));
322
-
323
- const sourceDir = join(__dirname, '..', 'examples', 'plugins', 'api-status');
324
- const pluginPath = join(sourceDir, 'index.js');
325
- const pluginModule = await import(pluginPath);
326
-
327
- const WidgetClass = pluginModule.default || pluginModule.ApiStatusWidget;
328
- const widget = new WidgetClass({
329
- config: {
330
- apiUrl: 'https://api.github.com/zen',
331
- timeout: 100,
332
- retries: 1,
333
- },
334
- });
335
-
336
- // Mock updateStatus to avoid needing UI elements
337
- widget.updateStatus = jest.fn();
338
-
339
- const data = await widget.getData();
340
-
341
- // Should fail gracefully
342
- expect(data).toBeDefined();
343
- expect(data.success).toBe(false);
344
- expect(data.error).toBeDefined();
345
- });
346
-
347
- test('should handle HTTP error responses', async () => {
348
- // Mock fetch to return an HTTP error
349
- const mockResponse = {
350
- ok: false,
351
- status: 500,
352
- statusText: 'Internal Server Error',
353
- headers: new Map([['content-type', 'application/json']]),
354
- json: async () => ({ error: 'Server error' }),
355
- };
356
-
357
- globalThis.fetch = jest.fn().mockResolvedValue(mockResponse);
358
-
359
- const sourceDir = join(__dirname, '..', 'examples', 'plugins', 'api-status');
360
- const pluginPath = join(sourceDir, 'index.js');
361
- const pluginModule = await import(pluginPath);
362
-
363
- const WidgetClass = pluginModule.default || pluginModule.ApiStatusWidget;
364
- const widget = new WidgetClass({
365
- config: {
366
- apiUrl: 'https://api.github.com/zen',
367
- timeout: 100,
368
- retries: 0,
369
- },
370
- });
371
-
372
- // Mock updateStatus to avoid needing UI elements
373
- widget.updateStatus = jest.fn();
374
-
375
- const data = await widget.getData();
376
-
377
- // Should fail gracefully with HTTP error
378
- expect(data).toBeDefined();
379
- expect(data.success).toBe(false);
380
- expect(data.error).toContain('500');
381
- });
382
-
383
- test('should handle timeout/abort errors', async () => {
384
- // Mock fetch to throw AbortError
385
- const abortError = new Error('Aborted');
386
- abortError.name = 'AbortError';
387
-
388
- globalThis.fetch = jest.fn().mockRejectedValue(abortError);
389
-
390
- const sourceDir = join(__dirname, '..', 'examples', 'plugins', 'api-status');
391
- const pluginPath = join(sourceDir, 'index.js');
392
- const pluginModule = await import(pluginPath);
393
-
394
- const WidgetClass = pluginModule.default || pluginModule.ApiStatusWidget;
395
- const widget = new WidgetClass({
396
- config: {
397
- apiUrl: 'https://api.github.com/zen',
398
- timeout: 50,
399
- retries: 1,
400
- },
401
- });
402
-
403
- // Mock updateStatus to avoid needing UI elements
404
- widget.updateStatus = jest.fn();
405
-
406
- const data = await widget.getData();
407
-
408
- // Should fail gracefully with timeout
409
- expect(data).toBeDefined();
410
- expect(data.success).toBe(false);
411
- });
412
- });
413
-
414
- describe('System Metrics Chart Widget Tests', () => {
415
- test('should create system-metrics-chart widget instance', async () => {
416
- const sourceDir = join(__dirname, '..', 'examples', 'plugins', 'system-metrics-chart');
417
- const pluginPath = join(sourceDir, 'index.js');
418
- const pluginModule = await import(pluginPath);
419
-
420
- const WidgetClass = pluginModule.default || pluginModule.SystemMetricsChartWidget;
421
- const widget = new WidgetClass({
422
- config: {
423
- metricType: 'memory',
424
- maxDataPoints: 10,
425
- },
426
- });
427
-
428
- expect(widget.name).toBe('System Metrics Chart');
429
- expect(widget.metricType).toBe('memory');
430
- expect(widget.maxDataPoints).toBe(10);
431
- });
432
-
433
- test('should generate chart data correctly', async () => {
434
- const sourceDir = join(__dirname, '..', 'examples', 'plugins', 'system-metrics-chart');
435
- const pluginPath = join(sourceDir, 'index.js');
436
- const pluginModule = await import(pluginPath);
437
-
438
- const WidgetClass = pluginModule.default || pluginModule.SystemMetricsChartWidget;
439
- const widget = new WidgetClass({
440
- config: {
441
- metricType: 'cpu',
442
- maxDataPoints: 5,
443
- },
444
- });
445
-
446
- // First getData call
447
- const data1 = await widget.getData();
448
- expect(data1.labels).toHaveLength(1);
449
- expect(data1.values).toHaveLength(1);
450
-
451
- // Second getData call - should accumulate
452
- const data2 = await widget.getData();
453
- expect(data2.labels).toHaveLength(2);
454
- expect(data2.values).toHaveLength(2);
455
-
456
- // After maxDataPoints, should stop accumulating
457
- for (let i = 0; i < 10; i++) {
458
- await widget.getData();
459
- }
460
-
461
- const finalData = await widget.getData();
462
- expect(finalData.labels.length).toBeLessThanOrEqual(5);
463
- expect(finalData.values.length).toBeLessThanOrEqual(5);
464
- });
465
- });
466
-
467
- describe('Weather Widget Tests', () => {
468
- test('should create weather widget instance', async () => {
469
- const sourceDir = join(__dirname, '..', 'examples', 'plugins', 'weather-widget');
470
- const pluginPath = join(sourceDir, 'index.js');
471
- const pluginModule = await import(pluginPath);
472
-
473
- const WidgetClass = pluginModule.default || pluginModule.WeatherWidget;
474
- const widget = new WidgetClass({
475
- config: {
476
- location: 'New York',
477
- unit: 'fahrenheit',
478
- },
479
- });
480
-
481
- expect(widget.name).toBe('Weather');
482
- expect(widget.config.location).toBe('New York');
483
- expect(widget.config.unit).toBe('fahrenheit');
484
- });
485
- });
486
- });
487
-
488
- describe('Plugin Manifest Schema', () => {
489
- test('should enforce required fields', () => {
490
- const emptyManifest = {};
491
-
492
- expect(() => validateManifest(emptyManifest)).toThrow(/Missing required fields/);
493
- });
494
-
495
- test('should require name field', () => {
496
- const manifest = {
497
- entryPoint: 'index.js',
498
- };
499
-
500
- expect(() => validateManifest(manifest)).toThrow(/name/);
501
- });
502
-
503
- test('should require entryPoint field (legacy requirement)', () => {
504
- const manifest = {
505
- name: 'Test',
506
- version: '1.0.0',
507
- };
508
-
509
- expect(() => validateManifest(manifest)).toThrow(/entryPoint/);
510
- });
511
-
512
- test('should accept all required fields', () => {
513
- const manifest = {
514
- name: 'Test Widget',
515
- version: '1.0.0',
516
- entryPoint: 'index.js',
517
- };
518
-
519
- expect(() => validateManifest(manifest)).not.toThrow();
520
- });
521
-
522
- test('should validate semver format', () => {
523
- const validVersions = ['1.0.0', '0.0.1', '10.20.30'];
524
- const invalidVersions = ['v1.0.0', '1.0', 'latest'];
525
-
526
- for (const version of validVersions) {
527
- const manifest = { name: 'Test', version, entryPoint: 'index.js' };
528
- expect(() => validateManifest(manifest)).not.toThrow();
529
- }
530
-
531
- for (const version of invalidVersions) {
532
- const manifest = { name: 'Test', version, entryPoint: 'index.js' };
533
- expect(() => validateManifest(manifest)).toThrow(/semver/i);
534
- }
535
- });
536
-
537
- test('should accept valid categories', () => {
538
- const categories = ['system', 'monitoring', 'custom', 'example'];
539
-
540
- for (const category of categories) {
541
- const manifest = { name: 'Test', version: '1.0.0', entryPoint: 'index.js', category };
542
- expect(() => validateManifest(manifest)).not.toThrow();
543
- }
544
- });
545
- });
546
-
547
- describe('Plugin Lifecycle Integration', () => {
548
- test('should execute full lifecycle', async () => {
549
- const mockApi = new PluginAPI();
550
-
551
- // Create a simple test widget
552
- class TestWidget extends BaseWidget {
553
- constructor(options) {
554
- super(options);
555
- this.initCalled = false;
556
- this.createCalled = false;
557
- this.getDataCalled = false;
558
- this.renderCalled = false;
559
- this.destroyCalled = false;
560
- }
561
-
562
- async init() {
563
- this.initCalled = true;
564
- return true;
565
- }
566
-
567
- async create(screen, theme) {
568
- this.createCalled = true;
569
- this.loaded = true;
570
- return this;
571
- }
572
-
573
- async getData() {
574
- this.getDataCalled = true;
575
- return { test: 'data' };
576
- }
577
-
578
- render(data) {
579
- this.renderCalled = true;
580
- }
581
-
582
- async destroy() {
583
- this.destroyCalled = true;
584
- await super.destroy();
585
- }
586
- }
587
-
588
- const widget = new TestWidget({ api: mockApi });
589
-
590
- // Execute lifecycle
591
- await widget.init();
592
- expect(widget.initCalled).toBe(true);
593
-
594
- await widget.create({}, {});
595
- expect(widget.createCalled).toBe(true);
596
- expect(widget.loaded).toBe(true);
597
-
598
- const data = await widget.getData();
599
- expect(widget.getDataCalled).toBe(true);
600
- expect(data).toEqual({ test: 'data' });
601
-
602
- widget.render(data);
603
- expect(widget.renderCalled).toBe(true);
604
-
605
- await widget.destroy();
606
- expect(widget.destroyCalled).toBe(true);
607
- expect(widget.loaded).toBe(false);
608
- });
609
-
610
- test('should handle errors in lifecycle gracefully', async () => {
611
- const mockApi = new PluginAPI();
612
-
613
- class ErrorWidget extends BaseWidget {
614
- async getData() {
615
- throw new Error('Test error');
616
- }
617
- }
618
-
619
- const widget = new ErrorWidget({ api: mockApi });
620
-
621
- // Should not throw, should handle error gracefully
622
- await expect(widget.getData()).rejects.toThrow('Test error');
623
- });
624
- });