claw-dashboard 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -1
- package/docs/API.md +1 -2
- package/index.js +31 -41
- package/package.json +22 -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/.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/removed-dependencies.test.js +0 -74
- 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,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
|
-
});
|