speedandcurrent 2.0.1 → 2.1.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/index.js +7 -0
- package/package.json +5 -5
- package/public/index.html +8 -2
- package/test/plugin.test.js +0 -414
package/index.js
CHANGED
|
@@ -518,6 +518,9 @@ module.exports = function (app) {
|
|
|
518
518
|
if (table && !isTableEmpty(table)) {
|
|
519
519
|
saveTableSync(table, path.join(app.getDataDirPath(), table.id + '.json'));
|
|
520
520
|
}
|
|
521
|
+
// Clear all active output paths from the SK bus before teardown.
|
|
522
|
+
if (smoothedCurrent) PolarSmoother.clear(app, plugin.id, [smoothedCurrent]);
|
|
523
|
+
if (options.estimateBoatSpeed && correctedBoatSpeed) Polar.clear(app, plugin.id, [correctedBoatSpeed]);
|
|
521
524
|
smoothedHeading = smoothedHeading?.terminate();
|
|
522
525
|
smoothedAttitude = smoothedAttitude?.terminate();
|
|
523
526
|
rawCurrent = rawCurrent?.terminate();
|
|
@@ -750,6 +753,10 @@ module.exports = function (app) {
|
|
|
750
753
|
// All other keys (sogFallback, estimateBoatSpeed, assumeCurrent,
|
|
751
754
|
// stability, smootherClass, etc.) are read
|
|
752
755
|
// directly from options.* so no extra action needed.
|
|
756
|
+
if (key === 'estimateBoatSpeed' && !value && correctedBoatSpeed) {
|
|
757
|
+
Polar.clear(app, plugin.id, [correctedBoatSpeed]);
|
|
758
|
+
PolarSmoother.clear(app, plugin.id, [smoothedCurrent]);
|
|
759
|
+
}
|
|
753
760
|
|
|
754
761
|
delete changedOptions[key];
|
|
755
762
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speedandcurrent",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A plugin that automatically corrects boat speed and calculates current and leeway using sensor fusion.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"homepage": "https://github.com/Asw1n/speedandcurrent#readme",
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=22.0.0"
|
|
20
|
+
"node": ">=22.0.0",
|
|
21
|
+
"signalk-server": ">=2.28.0"
|
|
21
22
|
},
|
|
22
23
|
"keywords": [
|
|
23
24
|
"signalk-webapp",
|
|
@@ -37,13 +38,12 @@
|
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
40
|
"kalman-filter": "^2.3.0",
|
|
40
|
-
"signalkutilities": "^2.
|
|
41
|
+
"signalkutilities": "^2.1.0"
|
|
41
42
|
},
|
|
42
43
|
"files": [
|
|
43
44
|
"index.js",
|
|
44
45
|
"correctionTable.js",
|
|
45
46
|
"public/",
|
|
46
|
-
"docs/"
|
|
47
|
-
"test"
|
|
47
|
+
"docs/"
|
|
48
48
|
]
|
|
49
49
|
}
|
package/public/index.html
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
<img src="icon.png" alt="" style="height:48px;width:auto">
|
|
55
55
|
<span class="app-title">Speed & Current</span>
|
|
56
56
|
</a>
|
|
57
|
-
<button class="navbar-toggler sidebar-toggler
|
|
57
|
+
<button class="navbar-toggler sidebar-toggler ms-2" type="button" id="sidebarToggler">
|
|
58
58
|
<span class="navbar-toggler-icon"></span>
|
|
59
59
|
</button>
|
|
60
60
|
</div>
|
|
@@ -214,8 +214,13 @@
|
|
|
214
214
|
document.body.classList.toggle('sidebar-minimized');
|
|
215
215
|
document.body.classList.toggle('brand-minimized');
|
|
216
216
|
});
|
|
217
|
+
const isMobile = () => window.matchMedia('(max-width: 767.98px)').matches;
|
|
217
218
|
document.getElementById('sidebarToggler').addEventListener('click', () => {
|
|
218
|
-
|
|
219
|
+
if (isMobile()) {
|
|
220
|
+
document.body.classList.toggle('sidebar-mobile-show');
|
|
221
|
+
} else {
|
|
222
|
+
document.body.classList.toggle('sidebar-hidden');
|
|
223
|
+
}
|
|
219
224
|
});
|
|
220
225
|
document.querySelectorAll('#main-nav .nav-link').forEach(link => {
|
|
221
226
|
link.addEventListener('click', e => {
|
|
@@ -225,6 +230,7 @@
|
|
|
225
230
|
const target = link.dataset.section;
|
|
226
231
|
document.querySelectorAll('.app-section').forEach(s => s.style.display = 'none');
|
|
227
232
|
document.getElementById('section-' + target).style.display = '';
|
|
233
|
+
if (isMobile()) document.body.classList.remove('sidebar-mobile-show');
|
|
228
234
|
});
|
|
229
235
|
});
|
|
230
236
|
</script>
|
package/test/plugin.test.js
DELETED
|
@@ -1,414 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { describe, it, before, after } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// App shim helpers
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Minimal BaconJS-style reactive-stream bus mock.
|
|
15
|
-
* Every chaining method returns `this`; onValue returns an unsubscribe no-op.
|
|
16
|
-
* This satisfies the internal stream API used by signalkutilities.
|
|
17
|
-
*/
|
|
18
|
-
function createMockBus() {
|
|
19
|
-
const bus = {};
|
|
20
|
-
const chainMethods = [
|
|
21
|
-
'onError', 'onEnd', 'skipDuplicates', 'map', 'filter', 'take', 'first',
|
|
22
|
-
'toPromise', 'flatMap', 'flatMapLatest', 'merge', 'debounce',
|
|
23
|
-
'debounceImmediate', 'throttle', 'delay', 'bufferWithTime', 'bufferWithCount',
|
|
24
|
-
'combine', 'sampledBy', 'scan', 'fold', 'zip', 'awaiting', 'not', 'log',
|
|
25
|
-
'doAction', 'doLog', 'doError', 'doEnd', 'withHandler', 'name',
|
|
26
|
-
'withDescription', 'skip', 'slidingWindow', 'startWith', 'mapEnd',
|
|
27
|
-
'skipWhile', 'takeWhile', 'takeUntil', 'errors', 'mapError', 'subscribe',
|
|
28
|
-
];
|
|
29
|
-
for (const m of chainMethods) bus[m] = () => bus;
|
|
30
|
-
bus.onValue = (_cb) => () => {};
|
|
31
|
-
bus.push = () => {};
|
|
32
|
-
bus.plug = () => () => {};
|
|
33
|
-
bus.end = () => {};
|
|
34
|
-
return bus;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Create a minimal SignalK app shim that satisfies the speedandcurrent plugin.
|
|
39
|
-
* A Proxy is used so that any method not explicitly stubbed returns a no-op
|
|
40
|
-
* instead of throwing a TypeError.
|
|
41
|
-
*
|
|
42
|
-
* @returns {{ app: object, cleanup: () => void }}
|
|
43
|
-
*/
|
|
44
|
-
function createAppShim() {
|
|
45
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'speedandcurrent-test-'));
|
|
46
|
-
const dataDir = path.join(tmpDir, 'data');
|
|
47
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
48
|
-
const configFile = path.join(tmpDir, 'SpeedAndCurrent.json');
|
|
49
|
-
|
|
50
|
-
const base = {
|
|
51
|
-
// Logging
|
|
52
|
-
debug: () => {},
|
|
53
|
-
error: () => {},
|
|
54
|
-
|
|
55
|
-
// Plugin status
|
|
56
|
-
setPluginStatus: () => {},
|
|
57
|
-
setPluginError: () => {},
|
|
58
|
-
|
|
59
|
-
// Delta output
|
|
60
|
-
handleMessage: () => {},
|
|
61
|
-
|
|
62
|
-
// Data access
|
|
63
|
-
getSelfPath: () => undefined,
|
|
64
|
-
getPath: () => undefined,
|
|
65
|
-
getMetadata: () => undefined,
|
|
66
|
-
putSelfPath: (_p, _v, cb) => { if (cb) cb({ state: 'COMPLETED' }); },
|
|
67
|
-
putPath: (_p, _v, cb) => { if (cb) cb({ state: 'COMPLETED' }); },
|
|
68
|
-
|
|
69
|
-
// Plugin config persistence
|
|
70
|
-
readPluginOptions: () => {
|
|
71
|
-
try { return JSON.parse(fs.readFileSync(configFile, 'utf-8')); } catch { return {}; }
|
|
72
|
-
},
|
|
73
|
-
savePluginOptions: (config, cb) => {
|
|
74
|
-
fs.writeFileSync(configFile, JSON.stringify(config));
|
|
75
|
-
if (cb) cb();
|
|
76
|
-
},
|
|
77
|
-
getPluginOptions: () => ({}),
|
|
78
|
-
getDataDirPath: () => dataDir,
|
|
79
|
-
|
|
80
|
-
// Subscription infrastructure
|
|
81
|
-
registerDeltaInputHandler: () => () => {},
|
|
82
|
-
registerPutHandler: () => () => {},
|
|
83
|
-
|
|
84
|
-
streambundle: {
|
|
85
|
-
getSelfBus: () => createMockBus(),
|
|
86
|
-
getBus: () => createMockBus(),
|
|
87
|
-
getSelfStream: () => createMockBus(),
|
|
88
|
-
getAvailablePaths: () => [],
|
|
89
|
-
},
|
|
90
|
-
|
|
91
|
-
subscriptionmanager: {
|
|
92
|
-
subscribe: (_msg, unsubscribes, _errorCb, _deltaCb) => {
|
|
93
|
-
const unsub = () => {};
|
|
94
|
-
if (Array.isArray(unsubscribes)) unsubscribes.push(unsub);
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
// Event emitter API
|
|
99
|
-
on: () => {},
|
|
100
|
-
once: () => {},
|
|
101
|
-
emit: () => {},
|
|
102
|
-
removeListener: () => {},
|
|
103
|
-
removeAllListeners: () => {},
|
|
104
|
-
|
|
105
|
-
// Server identity
|
|
106
|
-
selfId: 'urn:mrn:signalk:uuid:00000000-0000-0000-0000-000000000000',
|
|
107
|
-
selfType: 'vessels',
|
|
108
|
-
selfContext: 'vessels.urn:mrn:signalk:uuid:00000000-0000-0000-0000-000000000000',
|
|
109
|
-
|
|
110
|
-
config: {
|
|
111
|
-
configPath: tmpDir,
|
|
112
|
-
appPath: tmpDir,
|
|
113
|
-
version: '2.24.0',
|
|
114
|
-
name: 'signalk-server',
|
|
115
|
-
basePath: '/signalk/v1',
|
|
116
|
-
defaults: {},
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
reportOutputMessages: () => {},
|
|
120
|
-
|
|
121
|
-
wrappedEmitter: {
|
|
122
|
-
bindMethodsById: () => ({ on: () => {}, removeListener: () => {} }),
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// Proxy: any property not found in base returns a no-op function so unstubbed
|
|
127
|
-
// accesses from signalkutilities don't throw.
|
|
128
|
-
const app = new Proxy(base, {
|
|
129
|
-
get(target, prop) {
|
|
130
|
-
if (prop in target) return target[prop];
|
|
131
|
-
if (typeof prop === 'symbol') return undefined;
|
|
132
|
-
return () => {};
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const cleanup = () => {
|
|
137
|
-
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
return { app, cleanup };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
// Tests
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
|
|
147
|
-
describe('module export', () => {
|
|
148
|
-
it('exports a factory function', () => {
|
|
149
|
-
const factory = require('../index.js');
|
|
150
|
-
assert.strictEqual(typeof factory, 'function', 'module.exports must be a function');
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
describe('plugin object shape', () => {
|
|
155
|
-
let plugin;
|
|
156
|
-
let cleanup;
|
|
157
|
-
|
|
158
|
-
before(() => {
|
|
159
|
-
const shim = createAppShim();
|
|
160
|
-
cleanup = shim.cleanup;
|
|
161
|
-
plugin = require('../index.js')(shim.app);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
after(() => cleanup());
|
|
165
|
-
|
|
166
|
-
it('has a non-empty string id', () => {
|
|
167
|
-
assert.strictEqual(typeof plugin.id, 'string');
|
|
168
|
-
assert.ok(plugin.id.length > 0, 'plugin.id must not be empty');
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('has a non-empty string name', () => {
|
|
172
|
-
assert.strictEqual(typeof plugin.name, 'string');
|
|
173
|
-
assert.ok(plugin.name.length > 0, 'plugin.name must not be empty');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('has a string description', () => {
|
|
177
|
-
assert.strictEqual(typeof plugin.description, 'string');
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('exposes a valid JSON Schema object', () => {
|
|
181
|
-
assert.strictEqual(typeof plugin.schema, 'object', 'plugin.schema must be an object');
|
|
182
|
-
assert.ok(plugin.schema !== null);
|
|
183
|
-
assert.strictEqual(plugin.schema.type, 'object', 'schema.type must be "object"');
|
|
184
|
-
assert.strictEqual(typeof plugin.schema.properties, 'object', 'schema.properties must be an object');
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('has start and stop functions', () => {
|
|
188
|
-
assert.strictEqual(typeof plugin.start, 'function', 'plugin.start must be a function');
|
|
189
|
-
assert.strictEqual(typeof plugin.stop, 'function', 'plugin.stop must be a function');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('has a registerWithRouter function', () => {
|
|
193
|
-
assert.strictEqual(typeof plugin.registerWithRouter, 'function', 'plugin.registerWithRouter must be a function');
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe('registerWithRouter', () => {
|
|
198
|
-
it('registers the expected routes', () => {
|
|
199
|
-
const { app, cleanup } = createAppShim();
|
|
200
|
-
try {
|
|
201
|
-
const plugin = require('../index.js')(app);
|
|
202
|
-
const registered = [];
|
|
203
|
-
const mockRouter = {
|
|
204
|
-
get: (p) => registered.push(`GET ${p}`),
|
|
205
|
-
put: (p) => registered.push(`PUT ${p}`),
|
|
206
|
-
post: (p) => registered.push(`POST ${p}`),
|
|
207
|
-
};
|
|
208
|
-
plugin.registerWithRouter(mockRouter);
|
|
209
|
-
|
|
210
|
-
assert.ok(registered.includes('GET /api/report'), 'missing GET /api/report');
|
|
211
|
-
assert.ok(registered.includes('GET /api/meta'), 'missing GET /api/meta');
|
|
212
|
-
assert.ok(registered.includes('GET /api/status'), 'missing GET /api/status');
|
|
213
|
-
assert.ok(registered.includes('GET /api/settings'), 'missing GET /api/settings');
|
|
214
|
-
assert.ok(registered.includes('PUT /api/settings'), 'missing PUT /api/settings');
|
|
215
|
-
assert.ok(registered.includes('GET /api/tables'), 'missing GET /api/tables');
|
|
216
|
-
assert.ok(registered.includes('POST /api/tables/create'), 'missing POST /api/tables/create');
|
|
217
|
-
assert.ok(registered.includes('POST /api/tables/load'), 'missing POST /api/tables/load');
|
|
218
|
-
assert.ok(registered.includes('POST /api/tables/copy'), 'missing POST /api/tables/copy');
|
|
219
|
-
assert.ok(registered.includes('POST /api/tables/resize'), 'missing POST /api/tables/resize');
|
|
220
|
-
} finally {
|
|
221
|
-
cleanup();
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('GET /api/settings returns options with expected default keys', () => {
|
|
226
|
-
const { app, cleanup } = createAppShim();
|
|
227
|
-
try {
|
|
228
|
-
const plugin = require('../index.js')(app);
|
|
229
|
-
const routes = {};
|
|
230
|
-
const mockRouter = {
|
|
231
|
-
get: (p, h) => { routes[`GET ${p}`] = h; },
|
|
232
|
-
put: (p, h) => { routes[`PUT ${p}`] = h; },
|
|
233
|
-
post: (p, h) => { routes[`POST ${p}`] = h; },
|
|
234
|
-
};
|
|
235
|
-
plugin.registerWithRouter(mockRouter);
|
|
236
|
-
|
|
237
|
-
let response = null;
|
|
238
|
-
routes['GET /api/settings']({}, { json: (d) => { response = d; } });
|
|
239
|
-
|
|
240
|
-
assert.ok(response !== null, 'GET /api/settings returned no response');
|
|
241
|
-
assert.ok('estimateBoatSpeed' in response, 'missing estimateBoatSpeed');
|
|
242
|
-
assert.ok('updateCorrectionTable' in response, 'missing updateCorrectionTable');
|
|
243
|
-
assert.ok('smootherClass' in response, 'missing smootherClass');
|
|
244
|
-
assert.ok('stability' in response, 'missing stability');
|
|
245
|
-
} finally {
|
|
246
|
-
cleanup();
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it('PUT /api/settings rejects blocked key "tableName" with 400', () => {
|
|
251
|
-
const { app, cleanup } = createAppShim();
|
|
252
|
-
try {
|
|
253
|
-
const plugin = require('../index.js')(app);
|
|
254
|
-
const routes = {};
|
|
255
|
-
const mockRouter = {
|
|
256
|
-
get: (p, h) => { routes[`GET ${p}`] = h; },
|
|
257
|
-
put: (p, h) => { routes[`PUT ${p}`] = h; },
|
|
258
|
-
post: (p, h) => { routes[`POST ${p}`] = h; },
|
|
259
|
-
};
|
|
260
|
-
plugin.registerWithRouter(mockRouter);
|
|
261
|
-
|
|
262
|
-
let statusCode = null;
|
|
263
|
-
let response = null;
|
|
264
|
-
const res = {
|
|
265
|
-
status: (code) => { statusCode = code; return res; },
|
|
266
|
-
json: (d) => { response = d; },
|
|
267
|
-
};
|
|
268
|
-
routes['PUT /api/settings']({ body: { tableName: 'hack' } }, res);
|
|
269
|
-
|
|
270
|
-
assert.strictEqual(statusCode, 400, 'should respond 400 for blocked key "tableName"');
|
|
271
|
-
assert.ok(response && typeof response.error === 'string', 'should return an error message');
|
|
272
|
-
} finally {
|
|
273
|
-
cleanup();
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('PUT /api/settings rejects blocked key "correctionTable" with 400', () => {
|
|
278
|
-
const { app, cleanup } = createAppShim();
|
|
279
|
-
try {
|
|
280
|
-
const plugin = require('../index.js')(app);
|
|
281
|
-
const routes = {};
|
|
282
|
-
const mockRouter = {
|
|
283
|
-
get: (p, h) => { routes[`GET ${p}`] = h; },
|
|
284
|
-
put: (p, h) => { routes[`PUT ${p}`] = h; },
|
|
285
|
-
post: (p, h) => { routes[`POST ${p}`] = h; },
|
|
286
|
-
};
|
|
287
|
-
plugin.registerWithRouter(mockRouter);
|
|
288
|
-
|
|
289
|
-
let statusCode = null;
|
|
290
|
-
let response = null;
|
|
291
|
-
const res = {
|
|
292
|
-
status: (code) => { statusCode = code; return res; },
|
|
293
|
-
json: (d) => { response = d; },
|
|
294
|
-
};
|
|
295
|
-
routes['PUT /api/settings']({ body: { correctionTable: {} } }, res);
|
|
296
|
-
|
|
297
|
-
assert.strictEqual(statusCode, 400, 'should respond 400 for blocked key "correctionTable"');
|
|
298
|
-
assert.ok(response && typeof response.error === 'string', 'should return an error message');
|
|
299
|
-
} finally {
|
|
300
|
-
cleanup();
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('PUT /api/settings accepts valid settings and reflects them back', () => {
|
|
305
|
-
const { app, cleanup } = createAppShim();
|
|
306
|
-
try {
|
|
307
|
-
const plugin = require('../index.js')(app);
|
|
308
|
-
const routes = {};
|
|
309
|
-
const mockRouter = {
|
|
310
|
-
get: (p, h) => { routes[`GET ${p}`] = h; },
|
|
311
|
-
put: (p, h) => { routes[`PUT ${p}`] = h; },
|
|
312
|
-
post: (p, h) => { routes[`POST ${p}`] = h; },
|
|
313
|
-
};
|
|
314
|
-
plugin.registerWithRouter(mockRouter);
|
|
315
|
-
|
|
316
|
-
let response = null;
|
|
317
|
-
const res = { json: (d) => { response = d; } };
|
|
318
|
-
routes['PUT /api/settings']({ body: { estimateBoatSpeed: true } }, res);
|
|
319
|
-
|
|
320
|
-
assert.ok(response !== null, 'PUT /api/settings returned no response');
|
|
321
|
-
assert.strictEqual(response.estimateBoatSpeed, true, 'response should reflect the updated value');
|
|
322
|
-
} finally {
|
|
323
|
-
cleanup();
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('GET /api/status returns isRunning and status fields', () => {
|
|
328
|
-
const { app, cleanup } = createAppShim();
|
|
329
|
-
try {
|
|
330
|
-
const plugin = require('../index.js')(app);
|
|
331
|
-
const routes = {};
|
|
332
|
-
const mockRouter = {
|
|
333
|
-
get: (p, h) => { routes[`GET ${p}`] = h; },
|
|
334
|
-
put: (p, h) => { routes[`PUT ${p}`] = h; },
|
|
335
|
-
post: (p, h) => { routes[`POST ${p}`] = h; },
|
|
336
|
-
};
|
|
337
|
-
plugin.registerWithRouter(mockRouter);
|
|
338
|
-
|
|
339
|
-
let response = null;
|
|
340
|
-
routes['GET /api/status']({}, { json: (d) => { response = d; } });
|
|
341
|
-
|
|
342
|
-
assert.ok(response !== null, 'GET /api/status returned no response');
|
|
343
|
-
assert.ok('isRunning' in response, 'missing isRunning field');
|
|
344
|
-
assert.ok('status' in response, 'missing status field');
|
|
345
|
-
assert.strictEqual(response.isRunning, false, 'plugin should not be running before start()');
|
|
346
|
-
} finally {
|
|
347
|
-
cleanup();
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
describe('plugin lifecycle', () => {
|
|
353
|
-
it('start() completes without throwing', () => {
|
|
354
|
-
const { app, cleanup } = createAppShim();
|
|
355
|
-
let plugin;
|
|
356
|
-
try {
|
|
357
|
-
plugin = require('../index.js')(app);
|
|
358
|
-
assert.doesNotThrow(() => plugin.start(), 'plugin.start() must not throw');
|
|
359
|
-
} finally {
|
|
360
|
-
if (plugin) plugin.stop();
|
|
361
|
-
cleanup();
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it('stop() resolves cleanly after start()', async () => {
|
|
366
|
-
const { app, cleanup } = createAppShim();
|
|
367
|
-
try {
|
|
368
|
-
const plugin = require('../index.js')(app);
|
|
369
|
-
plugin.start();
|
|
370
|
-
await assert.doesNotReject(
|
|
371
|
-
() => plugin.stop(),
|
|
372
|
-
'plugin.stop() must resolve without rejection'
|
|
373
|
-
);
|
|
374
|
-
} finally {
|
|
375
|
-
cleanup();
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it('can be restarted (start → stop → start → stop)', async () => {
|
|
380
|
-
const { app, cleanup } = createAppShim();
|
|
381
|
-
try {
|
|
382
|
-
const plugin = require('../index.js')(app);
|
|
383
|
-
plugin.start();
|
|
384
|
-
await plugin.stop();
|
|
385
|
-
assert.doesNotThrow(() => plugin.start(), 'second start() must not throw');
|
|
386
|
-
await assert.doesNotReject(() => plugin.stop(), 'second stop() must resolve');
|
|
387
|
-
} finally {
|
|
388
|
-
cleanup();
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it('GET /api/status reports isRunning=true after start()', async () => {
|
|
393
|
-
const { app, cleanup } = createAppShim();
|
|
394
|
-
try {
|
|
395
|
-
const plugin = require('../index.js')(app);
|
|
396
|
-
const routes = {};
|
|
397
|
-
const mockRouter = {
|
|
398
|
-
get: (p, h) => { routes[`GET ${p}`] = h; },
|
|
399
|
-
put: (p, h) => { routes[`PUT ${p}`] = h; },
|
|
400
|
-
post: (p, h) => { routes[`POST ${p}`] = h; },
|
|
401
|
-
};
|
|
402
|
-
plugin.registerWithRouter(mockRouter);
|
|
403
|
-
plugin.start();
|
|
404
|
-
|
|
405
|
-
let response = null;
|
|
406
|
-
routes['GET /api/status']({}, { json: (d) => { response = d; } });
|
|
407
|
-
assert.strictEqual(response.isRunning, true, 'plugin should be running after start()');
|
|
408
|
-
|
|
409
|
-
await plugin.stop();
|
|
410
|
-
} finally {
|
|
411
|
-
cleanup();
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
});
|