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.
- package/CHANGELOG.md +29 -1
- package/docs/API.md +1 -2
- package/index.js +32 -42
- package/package.json +23 -12
- package/src/auto-save.js +11 -5
- package/src/cli/export-snapshot.js +3 -1
- package/src/cli/import-snapshot.js +3 -1
- package/src/config.js +9 -15
- package/src/errors.js +0 -9
- package/src/security.js +6 -2
- package/src/snapshot.js +73 -26
- package/src/web-server.js +7 -10
- package/src/widgets/builtin-widgets.js +0 -94
- package/src/widgets/index.js +0 -1
- package/.c8rc.json +0 -38
- package/.dockerignore +0 -68
- package/.github/dependabot.yml +0 -45
- package/.github/pull_request_template.md +0 -39
- package/.github/workflows/ci.yml +0 -147
- package/.github/workflows/release.yml +0 -40
- package/.github/workflows/security.yml +0 -84
- package/.husky/pre-commit +0 -1
- package/.planning/codebase/ARCHITECTURE.md +0 -206
- package/.planning/codebase/INTEGRATIONS.md +0 -150
- package/.planning/codebase/STACK.md +0 -122
- package/.planning/codebase/STRUCTURE.md +0 -201
- package/CONTRIBUTING.md +0 -378
- package/Dockerfile +0 -54
- package/FEATURES.md +0 -307
- package/TODO.md +0 -28
- package/ai.openclaw.dashboard.plist +0 -35
- package/build-cjs.js +0 -127
- package/cjs-shim.js +0 -13
- package/dist/clawdash +0 -1729
- package/dist/clawdash.meta.json +0 -2236
- package/dist/widgets.cjs +0 -6511
- package/docker-compose.yml +0 -67
- package/esbuild.config.js +0 -158
- package/eslint.config.js +0 -56
- package/examples/plugins/README.md +0 -122
- package/examples/plugins/api-status/index.js +0 -294
- package/examples/plugins/api-status/plugin.json +0 -19
- package/examples/plugins/hello-world/index.js +0 -104
- package/examples/plugins/hello-world/plugin.json +0 -15
- package/examples/plugins/system-metrics-chart/index.js +0 -339
- package/examples/plugins/system-metrics-chart/plugin.json +0 -18
- package/examples/plugins/weather-widget/index.js +0 -136
- package/examples/plugins/weather-widget/plugin.json +0 -19
- package/index.cjs +0 -23005
- package/jest.config.js +0 -11
- package/scripts/release.js +0 -595
- package/src/database.js +0 -734
- package/tests/alerts.test.js +0 -437
- package/tests/auto-save.test.js +0 -529
- package/tests/cache.test.js +0 -317
- package/tests/cli.test.js +0 -351
- package/tests/command-palette.test.js +0 -320
- package/tests/config-processor.test.js +0 -452
- package/tests/config-validator.test.js +0 -710
- package/tests/config-watcher.test.js +0 -594
- package/tests/config.test.js +0 -755
- package/tests/database.test.js +0 -438
- package/tests/errors.test.js +0 -189
- package/tests/example-plugins.test.js +0 -624
- package/tests/integration.test.js +0 -1321
- package/tests/loading-states.test.js +0 -300
- package/tests/manifest-validation-on-load.test.js +0 -671
- package/tests/performance-monitor.test.js +0 -190
- package/tests/plugin-api-rate-limit.test.js +0 -302
- package/tests/plugin-errors.test.js +0 -311
- package/tests/plugin-lifecycle-e2e.test.js +0 -1036
- package/tests/plugin-reload.test.js +0 -764
- package/tests/rate-limiter.test.js +0 -539
- package/tests/retry.test.js +0 -308
- package/tests/security.test.js +0 -411
- package/tests/settings-modal.test.js +0 -226
- package/tests/theme-selector.test.js +0 -57
- package/tests/utils.js +0 -242
- package/tests/utils.test.js +0 -317
- package/tests/validate-plugin-cli.test.js +0 -359
- package/tests/validation.test.js +0 -837
- package/tests/web-server.test.js +0 -646
- package/tests/widget-arrange-mode.test.js +0 -183
- package/tests/widget-config-hot-reload.test.js +0 -465
- package/tests/widget-dependency.test.js +0 -591
- package/tests/widget-error-boundary.test.js +0 -749
- package/tests/widget-error-isolation.test.js +0 -469
- package/tests/widget-integration.test.js +0 -1147
- package/tests/widget-refresh-intervals.test.js +0 -284
- package/tests/worker-pool.test.js +0 -522
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for loading-states.js
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import loadingStates, {
|
|
6
|
-
LoadingStateManager,
|
|
7
|
-
createWidgetSpinner,
|
|
8
|
-
createProgressBar,
|
|
9
|
-
loadSequentially,
|
|
10
|
-
loadStaggered,
|
|
11
|
-
getSpinnerFrame,
|
|
12
|
-
getSpinnerStyles,
|
|
13
|
-
SPINNER_FRAMES,
|
|
14
|
-
PROGRESS_STYLES
|
|
15
|
-
} from '../src/loading-states.js';
|
|
16
|
-
|
|
17
|
-
describe('Loading States', () => {
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
loadingStates.clearAll();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
loadingStates.clearAll();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe('LoadingStateManager', () => {
|
|
27
|
-
test('should create a loading state', () => {
|
|
28
|
-
const state = loadingStates.create('test', {
|
|
29
|
-
type: 'spinner',
|
|
30
|
-
message: 'Testing...'
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
expect(state).toBeDefined();
|
|
34
|
-
expect(state.id).toBe('test');
|
|
35
|
-
expect(typeof state.update).toBe('function');
|
|
36
|
-
expect(typeof state.complete).toBe('function');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('should track multiple loading states', () => {
|
|
40
|
-
loadingStates.create('test1', { type: 'spinner' });
|
|
41
|
-
loadingStates.create('test2', { type: 'progress', total: 100 });
|
|
42
|
-
|
|
43
|
-
const active = loadingStates.getActive();
|
|
44
|
-
expect(active).toHaveLength(2);
|
|
45
|
-
expect(active).toContain('test1');
|
|
46
|
-
expect(active).toContain('test2');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('should update loading message', () => {
|
|
50
|
-
const state = loadingStates.create('test', {
|
|
51
|
-
type: 'spinner',
|
|
52
|
-
message: 'Initial message'
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
state.update('New message');
|
|
56
|
-
const frame = state.getFrame();
|
|
57
|
-
expect(frame.message).toBe('New message');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test('should update progress', () => {
|
|
61
|
-
const state = loadingStates.create('test', {
|
|
62
|
-
type: 'progress',
|
|
63
|
-
message: 'Loading...',
|
|
64
|
-
total: 100
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
state.progress(50);
|
|
68
|
-
const frame = state.getFrame();
|
|
69
|
-
expect(frame.percentage).toBe('50.0');
|
|
70
|
-
expect(frame.current).toBe(50);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('should complete loading state', () => {
|
|
74
|
-
const state = loadingStates.create('test', { type: 'spinner' });
|
|
75
|
-
state.complete('Done!');
|
|
76
|
-
|
|
77
|
-
expect(state.getFrame().message).toBe('Done!');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('should remove loading state', () => {
|
|
81
|
-
loadingStates.create('test', { type: 'spinner' });
|
|
82
|
-
loadingStates.remove('test');
|
|
83
|
-
|
|
84
|
-
expect(loadingStates.getActive()).toHaveLength(0);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('should clear all loading states', () => {
|
|
88
|
-
loadingStates.create('test1', { type: 'spinner' });
|
|
89
|
-
loadingStates.create('test2', { type: 'spinner' });
|
|
90
|
-
loadingStates.create('test3', { type: 'spinner' });
|
|
91
|
-
|
|
92
|
-
loadingStates.clearAll();
|
|
93
|
-
expect(loadingStates.getActive()).toHaveLength(0);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('Spinner Styles', () => {
|
|
98
|
-
test('should have multiple spinner styles', () => {
|
|
99
|
-
const styles = getSpinnerStyles();
|
|
100
|
-
expect(styles).toContain('dots');
|
|
101
|
-
expect(styles).toContain('line');
|
|
102
|
-
expect(styles).toContain('pulse');
|
|
103
|
-
expect(styles).toContain('blocks');
|
|
104
|
-
expect(styles).toContain('arrows');
|
|
105
|
-
expect(styles).toContain('bouncing');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('should return frames for each style', () => {
|
|
109
|
-
Object.keys(SPINNER_FRAMES).forEach(style => {
|
|
110
|
-
const frame = getSpinnerFrame(style, 0);
|
|
111
|
-
expect(frame).toBeDefined();
|
|
112
|
-
expect(typeof frame).toBe('string');
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test('should default to dots style', () => {
|
|
117
|
-
const frame = getSpinnerFrame('invalid-style', 0);
|
|
118
|
-
expect(frame).toBe(SPINNER_FRAMES.dots.frames[0]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('should auto-increment frame', () => {
|
|
122
|
-
const frame1 = getSpinnerFrame('dots');
|
|
123
|
-
// Small delay to get different frame
|
|
124
|
-
const start = Date.now();
|
|
125
|
-
while (Date.now() - start < 10) {} // Tiny busy wait
|
|
126
|
-
const frame2 = getSpinnerFrame('dots');
|
|
127
|
-
// Frames could be same or different depending on timing
|
|
128
|
-
expect(typeof frame1).toBe('string');
|
|
129
|
-
expect(typeof frame2).toBe('string');
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('Progress Bar', () => {
|
|
134
|
-
test('should create progress bar', () => {
|
|
135
|
-
const controller = createProgressBar('test-operation', 50);
|
|
136
|
-
|
|
137
|
-
expect(controller).toBeDefined();
|
|
138
|
-
expect(typeof controller.progress).toBe('function');
|
|
139
|
-
expect(typeof controller.complete).toBe('function');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('should have progress bar styles', () => {
|
|
143
|
-
expect(PROGRESS_STYLES.blocks).toBeDefined();
|
|
144
|
-
expect(PROGRESS_STYLES.bars).toBeDefined();
|
|
145
|
-
expect(PROGRESS_STYLES.ascii).toBeDefined();
|
|
146
|
-
expect(PROGRESS_STYLES.dots).toBeDefined();
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe('Sequential Loading', () => {
|
|
151
|
-
test('should load items sequentially', async () => {
|
|
152
|
-
const items = ['a', 'b', 'c'];
|
|
153
|
-
let callCount = 0;
|
|
154
|
-
const loader = async (item) => {
|
|
155
|
-
callCount++;
|
|
156
|
-
return 'loaded';
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const results = await loadSequentially(items, loader);
|
|
160
|
-
|
|
161
|
-
expect(results).toHaveLength(3);
|
|
162
|
-
expect(callCount).toBe(3);
|
|
163
|
-
expect(results[0].success).toBe(true);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test('should handle loader failures gracefully', async () => {
|
|
167
|
-
const items = ['a', 'b'];
|
|
168
|
-
const loader = async (item) => {
|
|
169
|
-
if (item === 'b') throw new Error('failed');
|
|
170
|
-
return 'loaded';
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const results = await loadSequentially(items, loader);
|
|
174
|
-
|
|
175
|
-
expect(results).toHaveLength(2);
|
|
176
|
-
expect(results[0].success).toBe(true);
|
|
177
|
-
expect(results[1].success).toBe(false);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test('should call progress callback', async () => {
|
|
181
|
-
const items = ['a', 'b'];
|
|
182
|
-
const loader = async () => 'loaded';
|
|
183
|
-
const progressCalls = [];
|
|
184
|
-
const onProgress = (current, total, item) => {
|
|
185
|
-
progressCalls.push({ current, total, item });
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
await loadSequentially(items, loader, { onProgress });
|
|
189
|
-
|
|
190
|
-
expect(progressCalls).toHaveLength(2);
|
|
191
|
-
expect(progressCalls[0]).toEqual({ current: 1, total: 2, item: 'a' });
|
|
192
|
-
expect(progressCalls[1]).toEqual({ current: 2, total: 2, item: 'b' });
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test('should call item complete callback', async () => {
|
|
196
|
-
const items = [{ name: 'test' }];
|
|
197
|
-
const loader = async () => 'result';
|
|
198
|
-
const completedItems = [];
|
|
199
|
-
const onItemComplete = (item, result, index) => {
|
|
200
|
-
completedItems.push({ item, result, index });
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
await loadSequentially(items, loader, { onItemComplete });
|
|
204
|
-
|
|
205
|
-
expect(completedItems).toHaveLength(1);
|
|
206
|
-
expect(completedItems[0].item).toBe(items[0]);
|
|
207
|
-
expect(completedItems[0].result).toBe('result');
|
|
208
|
-
expect(completedItems[0].index).toBe(0);
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
describe('Staggered Loading', () => {
|
|
213
|
-
test('should load widgets with stagger delay', async () => {
|
|
214
|
-
const widgets = [{ id: 1 }, { id: 2 }];
|
|
215
|
-
let callCount = 0;
|
|
216
|
-
const factory = async () => {
|
|
217
|
-
callCount++;
|
|
218
|
-
return { created: true };
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
const results = await loadStaggered(widgets, factory, {
|
|
222
|
-
staggerDelay: 1 // Very small delay for fast test
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
expect(results).toHaveLength(2);
|
|
226
|
-
expect(callCount).toBe(2);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
test('should call widget loaded callback', async () => {
|
|
230
|
-
const widgets = [{ id: 1 }];
|
|
231
|
-
const factory = async () => ({ created: true });
|
|
232
|
-
const loadedWidgets = [];
|
|
233
|
-
const onWidgetLoaded = (widget, config, index) => {
|
|
234
|
-
loadedWidgets.push({ widget, config, index });
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
await loadStaggered(widgets, factory, { onWidgetLoaded });
|
|
238
|
-
|
|
239
|
-
expect(loadedWidgets).toHaveLength(1);
|
|
240
|
-
expect(loadedWidgets[0].widget).toEqual({ created: true });
|
|
241
|
-
expect(loadedWidgets[0].config).toBe(widgets[0]);
|
|
242
|
-
expect(loadedWidgets[0].index).toBe(0);
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
describe('Widget Spinner', () => {
|
|
247
|
-
test('should return spinner controller', () => {
|
|
248
|
-
const mockBlessed = {};
|
|
249
|
-
const mockParent = {
|
|
250
|
-
screen: { render: () => {} }
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const controller = createWidgetSpinner('test-widget', mockBlessed, mockParent);
|
|
254
|
-
|
|
255
|
-
expect(controller).toBeDefined();
|
|
256
|
-
expect(typeof controller.attach).toBe('function');
|
|
257
|
-
expect(typeof controller.update).toBe('function');
|
|
258
|
-
expect(typeof controller.complete).toBe('function');
|
|
259
|
-
expect(typeof controller.detach).toBe('function');
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
describe('Elapsed Time Formatting', () => {
|
|
264
|
-
test('should format milliseconds', () => {
|
|
265
|
-
const state = loadingStates.create('test', { type: 'spinner' });
|
|
266
|
-
// Wait a tiny bit
|
|
267
|
-
const elapsed = state.elapsed();
|
|
268
|
-
expect(elapsed).toBeGreaterThanOrEqual(0);
|
|
269
|
-
expect(typeof elapsed).toBe('number');
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
describe('Listener Management', () => {
|
|
274
|
-
test('should notify listeners on update', (done) => {
|
|
275
|
-
const state = loadingStates.create('test', { type: 'spinner' });
|
|
276
|
-
|
|
277
|
-
state.onUpdate((frame) => {
|
|
278
|
-
if (frame.message === 'Updated!') {
|
|
279
|
-
done();
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
state.update('Updated!');
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
test('should allow unsubscribing listeners', () => {
|
|
287
|
-
const state = loadingStates.create('test', { type: 'spinner' });
|
|
288
|
-
let callCount = 0;
|
|
289
|
-
const callback = () => {
|
|
290
|
-
callCount++;
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
const unsubscribe = state.onUpdate(callback);
|
|
294
|
-
unsubscribe(); // Remove listener
|
|
295
|
-
|
|
296
|
-
state.update('Test');
|
|
297
|
-
expect(callCount).toBe(0);
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
});
|