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,183 +0,0 @@
1
- /**
2
- * Tests for Widget Arrangement Mode
3
- * Verifies the fix for moveWidget() logic that was moving the wrong widget
4
- *
5
- * The bug was caused by updating arrangeWidgetIndex BEFORE getting the widgetId,
6
- * which resulted in getting the widget at the new position instead of the original.
7
- * This meant the wrong widget would be moved in the array.
8
- */
9
-
10
- import { jest } from '@jest/globals';
11
-
12
- describe('Widget Arrangement Mode', () => {
13
- describe('moveWidget() logic', () => {
14
- test('should move the originally selected widget, not the one at the new index', () => {
15
- // Simulate the dashboard state with widget order
16
- const state = {
17
- widgetOrder: ['cpu', 'mem', 'gpu', 'disk'],
18
- arrangeWidgetIndex: 2, // User has 'gpu' selected
19
- };
20
-
21
- // Simulate the FIXED moveWidget(-1) logic
22
- const moveWidgetFixed = (direction) => {
23
- const orderedWidgets = [...state.widgetOrder];
24
- if (orderedWidgets.length <= 1) return;
25
-
26
- // Get the widget currently selected (must be before any index changes)
27
- const widgetId = orderedWidgets[state.arrangeWidgetIndex];
28
- const oldIndex = state.arrangeWidgetIndex;
29
-
30
- // Calculate target index with wrapping
31
- let targetIndex = oldIndex + direction;
32
- if (targetIndex < 0) targetIndex = orderedWidgets.length - 1;
33
- if (targetIndex >= orderedWidgets.length) targetIndex = 0;
34
-
35
- // Reorder the array - remove from old position and insert at target
36
- const newOrder = [...orderedWidgets];
37
- newOrder.splice(oldIndex, 1);
38
- newOrder.splice(targetIndex, 0, widgetId);
39
-
40
- state.widgetOrder = newOrder;
41
- state.arrangeWidgetIndex = targetIndex;
42
- };
43
-
44
- // User presses 'left' to move 'gpu' to the left
45
- moveWidgetFixed(-1);
46
-
47
- // 'gpu' should have moved from index 2 to index 1
48
- expect(state.widgetOrder).toEqual(['cpu', 'gpu', 'mem', 'disk']);
49
- // The highlight should follow 'gpu' to its new position
50
- expect(state.arrangeWidgetIndex).toBe(1);
51
- });
52
-
53
- test('should move the correct widget when moving right', () => {
54
- const state = {
55
- widgetOrder: ['cpu', 'mem', 'gpu', 'disk'],
56
- arrangeWidgetIndex: 1, // User has 'mem' selected
57
- };
58
-
59
- const moveWidgetFixed = (direction) => {
60
- const orderedWidgets = [...state.widgetOrder];
61
- if (orderedWidgets.length <= 1) return;
62
-
63
- const widgetId = orderedWidgets[state.arrangeWidgetIndex];
64
- const oldIndex = state.arrangeWidgetIndex;
65
-
66
- let targetIndex = oldIndex + direction;
67
- if (targetIndex < 0) targetIndex = orderedWidgets.length - 1;
68
- if (targetIndex >= orderedWidgets.length) targetIndex = 0;
69
-
70
- const newOrder = [...orderedWidgets];
71
- newOrder.splice(oldIndex, 1);
72
- newOrder.splice(targetIndex, 0, widgetId);
73
-
74
- state.widgetOrder = newOrder;
75
- state.arrangeWidgetIndex = targetIndex;
76
- };
77
-
78
- // User presses 'right' to move 'mem' to the right
79
- moveWidgetFixed(1);
80
-
81
- // 'mem' should have moved from index 1 to index 2
82
- expect(state.widgetOrder).toEqual(['cpu', 'gpu', 'mem', 'disk']);
83
- expect(state.arrangeWidgetIndex).toBe(2);
84
- });
85
-
86
- test('should wrap correctly when moving left from first position', () => {
87
- const state = {
88
- widgetOrder: ['cpu', 'mem', 'gpu', 'disk'],
89
- arrangeWidgetIndex: 0, // User has 'cpu' selected (first)
90
- };
91
-
92
- const moveWidgetFixed = (direction) => {
93
- const orderedWidgets = [...state.widgetOrder];
94
- if (orderedWidgets.length <= 1) return;
95
-
96
- const widgetId = orderedWidgets[state.arrangeWidgetIndex];
97
- const oldIndex = state.arrangeWidgetIndex;
98
-
99
- let targetIndex = oldIndex + direction;
100
- if (targetIndex < 0) targetIndex = orderedWidgets.length - 1;
101
- if (targetIndex >= orderedWidgets.length) targetIndex = 0;
102
-
103
- const newOrder = [...orderedWidgets];
104
- newOrder.splice(oldIndex, 1);
105
- newOrder.splice(targetIndex, 0, widgetId);
106
-
107
- state.widgetOrder = newOrder;
108
- state.arrangeWidgetIndex = targetIndex;
109
- };
110
-
111
- // User presses 'left' to move 'cpu' - should wrap to end
112
- moveWidgetFixed(-1);
113
-
114
- // 'cpu' should have moved from index 0 to index 3 (last)
115
- expect(state.widgetOrder).toEqual(['mem', 'gpu', 'disk', 'cpu']);
116
- expect(state.arrangeWidgetIndex).toBe(3);
117
- });
118
-
119
- test('should wrap correctly when moving right from last position', () => {
120
- const state = {
121
- widgetOrder: ['cpu', 'mem', 'gpu', 'disk'],
122
- arrangeWidgetIndex: 3, // User has 'disk' selected (last)
123
- };
124
-
125
- const moveWidgetFixed = (direction) => {
126
- const orderedWidgets = [...state.widgetOrder];
127
- if (orderedWidgets.length <= 1) return;
128
-
129
- const widgetId = orderedWidgets[state.arrangeWidgetIndex];
130
- const oldIndex = state.arrangeWidgetIndex;
131
-
132
- let targetIndex = oldIndex + direction;
133
- if (targetIndex < 0) targetIndex = orderedWidgets.length - 1;
134
- if (targetIndex >= orderedWidgets.length) targetIndex = 0;
135
-
136
- const newOrder = [...orderedWidgets];
137
- newOrder.splice(oldIndex, 1);
138
- newOrder.splice(targetIndex, 0, widgetId);
139
-
140
- state.widgetOrder = newOrder;
141
- state.arrangeWidgetIndex = targetIndex;
142
- };
143
-
144
- // User presses 'right' to move 'disk' - should wrap to start
145
- moveWidgetFixed(1);
146
-
147
- // 'disk' should have moved from index 3 to index 0 (first)
148
- expect(state.widgetOrder).toEqual(['disk', 'cpu', 'mem', 'gpu']);
149
- expect(state.arrangeWidgetIndex).toBe(0);
150
- });
151
-
152
- test('should handle single widget array gracefully', () => {
153
- const state = {
154
- widgetOrder: ['cpu'],
155
- arrangeWidgetIndex: 0,
156
- };
157
-
158
- const moveWidgetFixed = (direction) => {
159
- const orderedWidgets = [...state.widgetOrder];
160
- if (orderedWidgets.length <= 1) return; // Should return early
161
-
162
- const widgetId = orderedWidgets[state.arrangeWidgetIndex];
163
- const oldIndex = state.arrangeWidgetIndex;
164
-
165
- let targetIndex = oldIndex + direction;
166
- if (targetIndex < 0) targetIndex = orderedWidgets.length - 1;
167
- if (targetIndex >= orderedWidgets.length) targetIndex = 0;
168
-
169
- const newOrder = [...orderedWidgets];
170
- newOrder.splice(oldIndex, 1);
171
- newOrder.splice(targetIndex, 0, widgetId);
172
-
173
- state.widgetOrder = newOrder;
174
- state.arrangeWidgetIndex = targetIndex;
175
- };
176
-
177
- // Should not throw and should not modify array
178
- moveWidgetFixed(-1);
179
- expect(state.widgetOrder).toEqual(['cpu']);
180
- expect(state.arrangeWidgetIndex).toBe(0);
181
- });
182
- });
183
- });
@@ -1,465 +0,0 @@
1
- /**
2
- * Widget Config Hot-Reload Tests
3
- * Tests for configuration hot-reload functionality in WidgetLoader
4
- */
5
-
6
- import { jest } from '@jest/globals';
7
- import { join } from 'path';
8
- import { mkdtemp, rm, mkdir, writeFile, readFile } from 'fs/promises';
9
- import { tmpdir } from 'os';
10
- import { setTimeout } from 'timers/promises';
11
-
12
- import { WidgetLoader } from '../src/widgets/widget-loader.js';
13
- import { processWidgetConfig, registerMigration } from '../src/widgets/config-processor.js';
14
-
15
- describe('Widget Config Hot-Reload', () => {
16
- let loader;
17
- let tempDir;
18
- let pluginDir;
19
-
20
- beforeEach(async () => {
21
- tempDir = await mkdtemp(join(tmpdir(), 'hot-reload-test-'));
22
- pluginDir = join(tempDir, 'test-widget');
23
- await mkdir(pluginDir, { recursive: true });
24
-
25
- loader = new WidgetLoader({ pluginsDir: tempDir });
26
- });
27
-
28
- afterEach(async () => {
29
- if (loader) {
30
- loader.disableConfigHotReload();
31
- await loader.clear();
32
- }
33
- if (tempDir) {
34
- await rm(tempDir, { recursive: true, force: true });
35
- }
36
- });
37
-
38
- describe('enableConfigHotReload()', () => {
39
- test('should enable hot-reload and return watcher', async () => {
40
- const watcher = loader.enableConfigHotReload();
41
-
42
- expect(watcher).toBeDefined();
43
- expect(loader.isConfigHotReloadEnabled()).toBe(true);
44
- });
45
-
46
- test('should return existing watcher if already enabled', async () => {
47
- const watcher1 = loader.enableConfigHotReload();
48
- const watcher2 = loader.enableConfigHotReload();
49
-
50
- expect(watcher1).toBe(watcher2);
51
- });
52
-
53
- test('should initialize reload stats', async () => {
54
- loader.enableConfigHotReload();
55
- const stats = loader.getHotReloadStats();
56
-
57
- expect(stats.enabled).toBe(true);
58
- expect(stats.reloads).toBe(0);
59
- expect(stats.errors).toBe(0);
60
- expect(stats.lastReload).toBeNull();
61
- });
62
-
63
- test('should accept custom options', async () => {
64
- const watcher = loader.enableConfigHotReload({
65
- debounceMs: 1000,
66
- usePolling: true,
67
- });
68
-
69
- expect(watcher).toBeDefined();
70
- expect(loader.isConfigHotReloadEnabled()).toBe(true);
71
- });
72
- });
73
-
74
- describe('disableConfigHotReload()', () => {
75
- test('should disable hot-reload', async () => {
76
- loader.enableConfigHotReload();
77
- expect(loader.isConfigHotReloadEnabled()).toBe(true);
78
-
79
- loader.disableConfigHotReload();
80
- expect(loader.isConfigHotReloadEnabled()).toBe(false);
81
- });
82
-
83
- test('should handle disabling when not enabled', async () => {
84
- expect(() => loader.disableConfigHotReload()).not.toThrow();
85
- });
86
- });
87
-
88
- describe('getHotReloadStats()', () => {
89
- test('should return stats when disabled', async () => {
90
- const stats = loader.getHotReloadStats();
91
-
92
- expect(stats.enabled).toBe(false);
93
- expect(stats.reloads).toBe(0);
94
- expect(stats.errors).toBe(0);
95
- });
96
-
97
- test('should track watched files count', async () => {
98
- // Create a plugin
99
- await writeFile(join(pluginDir, 'plugin.json'), JSON.stringify({
100
- id: 'test-widget',
101
- name: 'Test Widget',
102
- version: '1.0.0',
103
- type: 'widget',
104
- }));
105
- await writeFile(join(pluginDir, 'index.js'), 'export default { render: () => {}, getData: async () => ({}) };');
106
- await writeFile(join(pluginDir, 'package.json'), JSON.stringify({ type: 'module' }));
107
-
108
- await loader.loadPlugin(pluginDir);
109
-
110
- loader.enableConfigHotReload();
111
- const stats = loader.getHotReloadStats();
112
-
113
- expect(stats.watchedFiles).toBeGreaterThanOrEqual(0);
114
- });
115
- });
116
-
117
- describe('watchWidgetConfig()', () => {
118
- test('should warn if hot-reload not enabled', async () => {
119
- // Create a plugin
120
- await writeFile(join(pluginDir, 'plugin.json'), JSON.stringify({
121
- id: 'test-widget',
122
- name: 'Test Widget',
123
- version: '1.0.0',
124
- type: 'widget',
125
- }));
126
- await writeFile(join(pluginDir, 'index.js'), 'export default { render: () => {}, getData: async () => ({}) };');
127
- await writeFile(join(pluginDir, 'package.json'), JSON.stringify({ type: 'module' }));
128
-
129
- await loader.loadPlugin(pluginDir);
130
-
131
- const result = loader.watchWidgetConfig('test-widget');
132
- expect(result).toBe(false);
133
- });
134
-
135
- test('should warn if widget not found', async () => {
136
- loader.enableConfigHotReload();
137
-
138
- const result = loader.watchWidgetConfig('nonexistent');
139
- expect(result).toBe(false);
140
- });
141
- });
142
-
143
- describe('Plugin path tracking', () => {
144
- test('should store plugin path in metadata on load', async () => {
145
- await writeFile(join(pluginDir, 'plugin.json'), JSON.stringify({
146
- id: 'test-widget',
147
- name: 'Test Widget',
148
- version: '1.0.0',
149
- type: 'widget',
150
- }));
151
- await writeFile(join(pluginDir, 'index.js'), 'export default { render: () => {}, getData: async () => ({}) };');
152
- await writeFile(join(pluginDir, 'package.json'), JSON.stringify({ type: 'module' }));
153
-
154
- await loader.loadPlugin(pluginDir);
155
-
156
- const metadata = loader.getMetadata('test-widget');
157
- expect(metadata._pluginPath).toBe(pluginDir);
158
- expect(metadata._manifestPath).toBe(join(pluginDir, 'plugin.json'));
159
- expect(metadata._indexPath).toBe(join(pluginDir, 'index.js'));
160
- });
161
-
162
- test('should store plugin path on register', async () => {
163
- await writeFile(join(pluginDir, 'plugin.json'), JSON.stringify({
164
- id: 'test-widget',
165
- name: 'Test Widget',
166
- version: '1.0.0',
167
- type: 'widget',
168
- }));
169
- await writeFile(join(pluginDir, 'index.js'), 'export default { render: () => {}, getData: async () => ({}) };');
170
- await writeFile(join(pluginDir, 'package.json'), JSON.stringify({ type: 'module' }));
171
-
172
- await loader.registerPlugin(pluginDir);
173
-
174
- const metadata = loader.getMetadata('test-widget');
175
- expect(metadata._pluginPath).toBe(pluginDir);
176
- });
177
- });
178
-
179
- describe('Config file hot-reload', () => {
180
- test('should emit configReloaded event on successful reload', async () => {
181
- // Create initial plugin config
182
- await writeFile(join(pluginDir, 'plugin.json'), JSON.stringify({
183
- id: 'test-widget',
184
- name: 'Test Widget',
185
- version: '1.0.0',
186
- type: 'widget',
187
- config: {
188
- message: 'Hello World',
189
- count: 5,
190
- },
191
- }));
192
- await writeFile(join(pluginDir, 'index.js'), `
193
- export default class TestWidget {
194
- constructor(config) {
195
- this.config = config;
196
- }
197
- render() {}
198
- async getData() { return this.config; }
199
- }
200
- `);
201
- await writeFile(join(pluginDir, 'package.json'), JSON.stringify({ type: 'module' }));
202
-
203
- await loader.loadPlugin(pluginDir);
204
-
205
- // Set up event listener
206
- const reloadPromise = new Promise((resolve) => {
207
- loader.once('configReloaded', resolve);
208
- });
209
-
210
- // Enable hot-reload with low debounce for testing
211
- loader.enableConfigHotReload({ debounceMs: 100 });
212
-
213
- // Wait a bit then modify the config file
214
- await setTimeout(50);
215
- await writeFile(join(pluginDir, 'plugin.json'), JSON.stringify({
216
- id: 'test-widget',
217
- name: 'Test Widget',
218
- version: '1.0.0',
219
- type: 'widget',
220
- config: {
221
- message: 'Updated Message',
222
- count: 10,
223
- },
224
- }));
225
-
226
- // Wait for reload event
227
- const event = await reloadPromise;
228
-
229
- expect(event.widgetId).toBe('test-widget');
230
- expect(event.timestamp).toBeDefined();
231
- expect(event.config).toBeDefined();
232
- }, 10000);
233
-
234
- test('should process environment variable interpolation on reload', async () => {
235
- process.env.HOT_RELOAD_TEST_VAR = 'interpolated_value';
236
-
237
- await writeFile(join(pluginDir, 'plugin.json'), JSON.stringify({
238
- id: 'test-widget',
239
- name: 'Test Widget',
240
- version: '1.0.0',
241
- type: 'widget',
242
- config: {
243
- value: '${HOT_RELOAD_TEST_VAR}',
244
- },
245
- }));
246
- await writeFile(join(pluginDir, 'index.js'), `
247
- export default class TestWidget {
248
- constructor(config) {
249
- this.config = config;
250
- }
251
- render() {}
252
- async getData() { return this.config; }
253
- }
254
- `);
255
- await writeFile(join(pluginDir, 'package.json'), JSON.stringify({ type: 'module' }));
256
-
257
- await loader.loadPlugin(pluginDir);
258
-
259
- // Check initial config processing
260
- const instance = loader.get('test-widget');
261
- expect(instance.config.value).toBe('interpolated_value');
262
- });
263
-
264
- test('should handle config migration on reload', async () => {
265
- // Test that config version is properly handled during reload
266
- // The widget loader processes config through processWidgetConfig
267
- // which handles versioning
268
-
269
- await writeFile(join(pluginDir, 'plugin.json'), JSON.stringify({
270
- id: 'test-widget',
271
- name: 'Test Widget',
272
- version: '1.0.0',
273
- type: 'widget',
274
- config: {
275
- __version: '1.0.0',
276
- oldField: 'value',
277
- },
278
- }));
279
- await writeFile(join(pluginDir, 'index.js'), `
280
- export default class TestWidget {
281
- constructor(config) {
282
- this.config = config;
283
- }
284
- render() {}
285
- async getData() { return this.config; }
286
- }
287
- `);
288
- await writeFile(join(pluginDir, 'package.json'), JSON.stringify({ type: 'module' }));
289
-
290
- await loader.loadPlugin(pluginDir);
291
-
292
- const instance = loader.get('test-widget');
293
- // Config should have been processed and passed to the widget
294
- expect(instance.config.oldField).toBe('value');
295
- // The __version should be present
296
- expect(instance.config.__version).toBe('1.0.0');
297
- });
298
- });
299
-
300
- describe('Config reload errors', () => {
301
- test('should emit configReloadError on invalid JSON', async () => {
302
- await writeFile(join(pluginDir, 'plugin.json'), JSON.stringify({
303
- id: 'test-widget',
304
- name: 'Test Widget',
305
- version: '1.0.0',
306
- type: 'widget',
307
- config: {},
308
- }));
309
- await writeFile(join(pluginDir, 'index.js'), 'export default { render: () => {}, getData: async () => ({}) };');
310
- await writeFile(join(pluginDir, 'package.json'), JSON.stringify({ type: 'module' }));
311
-
312
- await loader.loadPlugin(pluginDir);
313
-
314
- // Set up event listener for error
315
- const errorPromise = new Promise((resolve) => {
316
- loader.once('configReloadError', resolve);
317
- });
318
-
319
- loader.enableConfigHotReload({ debounceMs: 100 });
320
-
321
- // Corrupt the JSON
322
- await setTimeout(50);
323
- await writeFile(join(pluginDir, 'plugin.json'), '{ invalid json }');
324
-
325
- const event = await errorPromise;
326
- expect(event.error).toBeDefined();
327
- expect(event.timestamp).toBeDefined();
328
-
329
- // Stats should track the error
330
- const stats = loader.getHotReloadStats();
331
- expect(stats.errors).toBeGreaterThan(0);
332
- }, 10000);
333
-
334
- test('should handle missing widget gracefully', async () => {
335
- loader.enableConfigHotReload({ debounceMs: 100 });
336
-
337
- // Manually trigger a reload for a non-existent widget path
338
- const result = await loader._reloadWidgetConfig('nonexistent', join(tempDir, 'plugin.json'));
339
-
340
- expect(result.success).toBe(false);
341
- expect(result.error).toContain('not found');
342
- });
343
- });
344
-
345
- describe('onConfigChange callback', () => {
346
- test('should call onConfigChange when widget supports it', async () => {
347
- const onConfigChange = jest.fn();
348
-
349
- await writeFile(join(pluginDir, 'plugin.json'), JSON.stringify({
350
- id: 'test-widget',
351
- name: 'Test Widget',
352
- version: '1.0.0',
353
- type: 'widget',
354
- config: { value: 1 },
355
- }));
356
- await writeFile(join(pluginDir, 'index.js'), `
357
- export default class TestWidget {
358
- constructor(config) {
359
- this.config = config;
360
- }
361
- render() {}
362
- async getData() { return this.config; }
363
- onConfigChange(newConfig, oldConfig) {
364
- this.config = newConfig;
365
- }
366
- }
367
- `);
368
- await writeFile(join(pluginDir, 'package.json'), JSON.stringify({ type: 'module' }));
369
-
370
- await loader.loadPlugin(pluginDir);
371
-
372
- // Manually trigger a reload
373
- const result = await loader._reloadWidgetConfig('test-widget', join(pluginDir, 'plugin.json'));
374
-
375
- expect(result.success).toBe(true);
376
- });
377
- });
378
-
379
- describe('_findWidgetIdByConfigPath', () => {
380
- test('should find widget by config path', async () => {
381
- await writeFile(join(pluginDir, 'plugin.json'), JSON.stringify({
382
- id: 'test-widget',
383
- name: 'Test Widget',
384
- version: '1.0.0',
385
- type: 'widget',
386
- }));
387
- await writeFile(join(pluginDir, 'index.js'), 'export default { render: () => {}, getData: async () => ({}) };');
388
- await writeFile(join(pluginDir, 'package.json'), JSON.stringify({ type: 'module' }));
389
-
390
- await loader.loadPlugin(pluginDir);
391
-
392
- const widgetId = loader._findWidgetIdByConfigPath(join(pluginDir, 'plugin.json'));
393
- expect(widgetId).toBe('test-widget');
394
- });
395
-
396
- test('should return null for unknown path', async () => {
397
- const widgetId = loader._findWidgetIdByConfigPath('/unknown/path/plugin.json');
398
- expect(widgetId).toBeNull();
399
- });
400
- });
401
- });
402
-
403
- describe('Widget Config Processing Integration', () => {
404
- test('should process widget config with env interpolation', () => {
405
- process.env.WIDGET_TEST_API_URL = 'https://api.example.com';
406
- process.env.WIDGET_TEST_TIMEOUT = '5000';
407
-
408
- const config = {
409
- apiUrl: '${WIDGET_TEST_API_URL}',
410
- timeout: '${WIDGET_TEST_TIMEOUT:-3000}',
411
- name: 'static_value',
412
- };
413
-
414
- const result = processWidgetConfig(config);
415
-
416
- expect(result.success).toBe(true);
417
- expect(result.config.apiUrl).toBe('https://api.example.com');
418
- expect(result.config.timeout).toBe('5000');
419
- expect(result.config.name).toBe('static_value');
420
- });
421
-
422
- test('should use default value when env var not set', () => {
423
- delete process.env.WIDGET_TEST_MISSING;
424
-
425
- const config = {
426
- value: '${WIDGET_TEST_MISSING:-default_value}',
427
- };
428
-
429
- const result = processWidgetConfig(config);
430
-
431
- expect(result.success).toBe(true);
432
- expect(result.config.value).toBe('default_value');
433
- });
434
-
435
- test('should handle nested config objects', () => {
436
- process.env.WIDGET_TEST_NESTED = 'nested_value';
437
-
438
- const config = {
439
- level1: {
440
- level2: {
441
- value: '${WIDGET_TEST_NESTED}',
442
- },
443
- },
444
- array: ['${WIDGET_TEST_NESTED}', 'static'],
445
- };
446
-
447
- const result = processWidgetConfig(config);
448
-
449
- expect(result.success).toBe(true);
450
- expect(result.config.level1.level2.value).toBe('nested_value');
451
- expect(result.config.array).toEqual(['nested_value', 'static']);
452
- });
453
-
454
- test('should handle config version validation', () => {
455
- const config = {
456
- __version: '99.99.99', // Future version
457
- value: 'test',
458
- };
459
-
460
- const result = processWidgetConfig(config, { throwOnError: false });
461
-
462
- expect(result.success).toBe(false);
463
- expect(result.error).toContain('newer than current');
464
- });
465
- });