@signalk/app-dock 1.2.0 → 1.3.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/README.md +20 -0
- package/package.json +6 -1
- package/public/app-icon-180.png +0 -0
- package/public/app-icon.svg +2 -4
- package/public/dock.css +62 -0
- package/public/dock.js +141 -4
- package/public/index.html +4 -0
- package/.gitattributes +0 -5
- package/.github/workflows/ci.yml +0 -11
- package/.github/workflows/release_on_tag.yml +0 -57
- package/.github/workflows/stale.yml +0 -42
- package/.prettierignore +0 -6
- package/.prettierrc +0 -6
- package/eslint.config.js +0 -59
- package/src/configpanel/PluginConfigurationPanel.js +0 -549
- package/src/configpanel/index.js +0 -1
- package/test/plugin.test.js +0 -696
- package/webpack.config.js +0 -39
package/test/plugin.test.js
DELETED
|
@@ -1,696 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { describe, it } = require('node:test')
|
|
4
|
-
const assert = require('node:assert/strict')
|
|
5
|
-
const path = require('path')
|
|
6
|
-
const fs = require('fs')
|
|
7
|
-
|
|
8
|
-
const pluginFactory = require('../plugin/index.js')
|
|
9
|
-
|
|
10
|
-
function createMockApp(webapps = []) {
|
|
11
|
-
return {
|
|
12
|
-
webapps,
|
|
13
|
-
debug: () => {},
|
|
14
|
-
error: () => {},
|
|
15
|
-
savePluginOptions: (opts, cb) => cb(null),
|
|
16
|
-
readPluginOptions: () => ({}),
|
|
17
|
-
registerPutHandler: () => {},
|
|
18
|
-
handleMessage: () => {},
|
|
19
|
-
getSelfPath: () => null
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const MOCK_WEBAPPS = [
|
|
24
|
-
{
|
|
25
|
-
name: '@signalk/freeboard-sk',
|
|
26
|
-
signalk: { displayName: 'Freeboard-SK', appIcon: './assets/icons/icon-72x72.png' }
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
name: '@mxtommy/kip',
|
|
30
|
-
signalk: { displayName: 'KIP Instrument MFD', appIcon: 'assets/icon-72x72.png' }
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
name: '@signalk/app-dock',
|
|
34
|
-
signalk: { displayName: 'App Dock' }
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
name: '@signalk/server-admin-ui',
|
|
38
|
-
signalk: { displayName: 'Admin UI' }
|
|
39
|
-
}
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
describe('plugin identity', () => {
|
|
43
|
-
it('returns correct id and name', () => {
|
|
44
|
-
const plugin = pluginFactory(createMockApp())
|
|
45
|
-
assert.equal(plugin.id, 'signalk-app-dock')
|
|
46
|
-
assert.equal(plugin.name, 'App Dock')
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('has required plugin interface methods', () => {
|
|
50
|
-
const plugin = pluginFactory(createMockApp())
|
|
51
|
-
assert.equal(typeof plugin.start, 'function')
|
|
52
|
-
assert.equal(typeof plugin.stop, 'function')
|
|
53
|
-
assert.equal(typeof plugin.registerWithRouter, 'function')
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
describe('schema', () => {
|
|
58
|
-
it('defines all expected properties', () => {
|
|
59
|
-
const plugin = pluginFactory(createMockApp())
|
|
60
|
-
const props = Object.keys(plugin.schema.properties)
|
|
61
|
-
assert.ok(props.includes('position'))
|
|
62
|
-
assert.ok(props.includes('iframeMode'))
|
|
63
|
-
assert.ok(props.includes('iconSize'))
|
|
64
|
-
assert.ok(props.includes('magnification'))
|
|
65
|
-
assert.ok(props.includes('magnificationScale'))
|
|
66
|
-
assert.ok(props.includes('apps'))
|
|
67
|
-
assert.ok(!props.includes('triggerCorner'), 'triggerCorner should be removed')
|
|
68
|
-
assert.ok(!props.includes('longPressActivation'), 'longPressActivation should be removed')
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('position enum contains all four edges', () => {
|
|
72
|
-
const plugin = pluginFactory(createMockApp())
|
|
73
|
-
const posEnum = plugin.schema.properties.position.enum
|
|
74
|
-
assert.deepEqual(posEnum, ['bottom', 'top', 'left', 'right'])
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('apps items require url', () => {
|
|
78
|
-
const plugin = pluginFactory(createMockApp())
|
|
79
|
-
const items = plugin.schema.properties.apps.items
|
|
80
|
-
assert.deepEqual(items.required, ['url'])
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('defines showNightModeButton with default true', () => {
|
|
84
|
-
const plugin = pluginFactory(createMockApp())
|
|
85
|
-
const prop = plugin.schema.properties.showNightModeButton
|
|
86
|
-
assert.equal(prop.type, 'boolean')
|
|
87
|
-
assert.equal(prop.default, true)
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('defines showExitButton with default false', () => {
|
|
91
|
-
const plugin = pluginFactory(createMockApp())
|
|
92
|
-
const prop = plugin.schema.properties.showExitButton
|
|
93
|
-
assert.equal(prop.type, 'boolean')
|
|
94
|
-
assert.equal(prop.default, false)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('defines tourDismissed with default false', () => {
|
|
98
|
-
const plugin = pluginFactory(createMockApp())
|
|
99
|
-
const prop = plugin.schema.properties.tourDismissed
|
|
100
|
-
assert.equal(prop.type, 'boolean')
|
|
101
|
-
assert.equal(prop.default, false)
|
|
102
|
-
})
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
describe('webapp discovery', () => {
|
|
106
|
-
it('filters out @signalk/app-dock and admin-ui from /webapps', (t, done) => {
|
|
107
|
-
const app = createMockApp(MOCK_WEBAPPS)
|
|
108
|
-
const plugin = pluginFactory(app)
|
|
109
|
-
const routes = {}
|
|
110
|
-
const router = {
|
|
111
|
-
get: (path, handler) => {
|
|
112
|
-
routes[path] = handler
|
|
113
|
-
},
|
|
114
|
-
post: () => {}
|
|
115
|
-
}
|
|
116
|
-
plugin.registerWithRouter(router)
|
|
117
|
-
|
|
118
|
-
const res = {
|
|
119
|
-
json: (data) => {
|
|
120
|
-
assert.equal(data.length, 2)
|
|
121
|
-
assert.ok(data.every((w) => w.name !== '@signalk/app-dock'))
|
|
122
|
-
assert.ok(data.every((w) => w.name !== '@signalk/server-admin-ui'))
|
|
123
|
-
done()
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
routes['/webapps']({}, res)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('maps webapp metadata correctly', (t, done) => {
|
|
130
|
-
const app = createMockApp(MOCK_WEBAPPS)
|
|
131
|
-
const plugin = pluginFactory(app)
|
|
132
|
-
const routes = {}
|
|
133
|
-
const router = {
|
|
134
|
-
get: (path, handler) => {
|
|
135
|
-
routes[path] = handler
|
|
136
|
-
},
|
|
137
|
-
post: () => {}
|
|
138
|
-
}
|
|
139
|
-
plugin.registerWithRouter(router)
|
|
140
|
-
|
|
141
|
-
const res = {
|
|
142
|
-
json: (data) => {
|
|
143
|
-
const fb = data.find((w) => w.name === '@signalk/freeboard-sk')
|
|
144
|
-
assert.equal(fb.label, 'Freeboard-SK')
|
|
145
|
-
assert.equal(fb.url, '/@signalk/freeboard-sk/')
|
|
146
|
-
assert.equal(fb.icon, '/@signalk/freeboard-sk/assets/icons/icon-72x72.png')
|
|
147
|
-
done()
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
routes['/webapps']({}, res)
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
it('handles missing signalk metadata gracefully', (t, done) => {
|
|
154
|
-
const app = createMockApp([{ name: 'bare-plugin' }])
|
|
155
|
-
const plugin = pluginFactory(app)
|
|
156
|
-
const routes = {}
|
|
157
|
-
const router = {
|
|
158
|
-
get: (path, handler) => {
|
|
159
|
-
routes[path] = handler
|
|
160
|
-
},
|
|
161
|
-
post: () => {}
|
|
162
|
-
}
|
|
163
|
-
plugin.registerWithRouter(router)
|
|
164
|
-
|
|
165
|
-
const res = {
|
|
166
|
-
json: (data) => {
|
|
167
|
-
assert.equal(data.length, 1)
|
|
168
|
-
assert.equal(data[0].label, 'bare-plugin')
|
|
169
|
-
assert.equal(data[0].icon, null)
|
|
170
|
-
done()
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
routes['/webapps']({}, res)
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
it('returns empty array when app.webapps is undefined', (t, done) => {
|
|
177
|
-
const app = createMockApp()
|
|
178
|
-
delete app.webapps
|
|
179
|
-
const plugin = pluginFactory(app)
|
|
180
|
-
const routes = {}
|
|
181
|
-
const router = {
|
|
182
|
-
get: (path, handler) => {
|
|
183
|
-
routes[path] = handler
|
|
184
|
-
},
|
|
185
|
-
post: () => {}
|
|
186
|
-
}
|
|
187
|
-
plugin.registerWithRouter(router)
|
|
188
|
-
|
|
189
|
-
const res = {
|
|
190
|
-
json: (data) => {
|
|
191
|
-
assert.deepEqual(data, [])
|
|
192
|
-
done()
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
routes['/webapps']({}, res)
|
|
196
|
-
})
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
describe('settings endpoint', () => {
|
|
200
|
-
it('returns plugin settings with resolved apps', (t, done) => {
|
|
201
|
-
const app = createMockApp()
|
|
202
|
-
const plugin = pluginFactory(app)
|
|
203
|
-
plugin.start({ position: 'left', iconSize: 64, apps: [] })
|
|
204
|
-
|
|
205
|
-
const routes = {}
|
|
206
|
-
const router = {
|
|
207
|
-
get: (path, handler) => {
|
|
208
|
-
routes[path] = handler
|
|
209
|
-
},
|
|
210
|
-
post: () => {}
|
|
211
|
-
}
|
|
212
|
-
plugin.registerWithRouter(router)
|
|
213
|
-
|
|
214
|
-
const res = {
|
|
215
|
-
json: (data) => {
|
|
216
|
-
assert.equal(data.position, 'left')
|
|
217
|
-
assert.equal(data.iconSize, 64)
|
|
218
|
-
assert.ok(Array.isArray(data.apps))
|
|
219
|
-
done()
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
routes['/settings']({}, res)
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it('serves the saved configuration before the plugin has started', (t, done) => {
|
|
226
|
-
const app = createMockApp()
|
|
227
|
-
app.readPluginOptions = () => ({
|
|
228
|
-
enabled: false,
|
|
229
|
-
configuration: {
|
|
230
|
-
position: 'right',
|
|
231
|
-
apps: [
|
|
232
|
-
{ url: '/a/', label: 'A', enabled: true },
|
|
233
|
-
{ url: '/b/', label: 'B', enabled: false }
|
|
234
|
-
]
|
|
235
|
-
}
|
|
236
|
-
})
|
|
237
|
-
const plugin = pluginFactory(app)
|
|
238
|
-
|
|
239
|
-
const routes = {}
|
|
240
|
-
plugin.registerWithRouter({
|
|
241
|
-
get: (path, handler) => {
|
|
242
|
-
routes[path] = handler
|
|
243
|
-
},
|
|
244
|
-
post: () => {}
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
const res = {
|
|
248
|
-
json: (data) => {
|
|
249
|
-
assert.equal(data.position, 'right')
|
|
250
|
-
assert.deepEqual(data.apps, [{ label: 'A', url: '/a/', icon: null, color: null, autostart: false }])
|
|
251
|
-
done()
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
routes['/settings']({}, res)
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
it('serves no apps when the saved apps value is not an array', (t, done) => {
|
|
258
|
-
const app = createMockApp()
|
|
259
|
-
app.readPluginOptions = () => ({ enabled: false, configuration: { apps: {} } })
|
|
260
|
-
const plugin = pluginFactory(app)
|
|
261
|
-
|
|
262
|
-
const routes = {}
|
|
263
|
-
plugin.registerWithRouter({
|
|
264
|
-
get: (path, handler) => {
|
|
265
|
-
routes[path] = handler
|
|
266
|
-
},
|
|
267
|
-
post: () => {}
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
const res = {
|
|
271
|
-
json: (data) => {
|
|
272
|
-
assert.deepEqual(data.apps, [])
|
|
273
|
-
done()
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
routes['/settings']({}, res)
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
it('serves the saved configuration after stop()', (t, done) => {
|
|
280
|
-
const app = createMockApp()
|
|
281
|
-
app.readPluginOptions = () => ({ enabled: false, configuration: { position: 'top', apps: [] } })
|
|
282
|
-
const plugin = pluginFactory(app)
|
|
283
|
-
plugin.start({ position: 'left', apps: [{ url: '/a/', label: 'A' }] })
|
|
284
|
-
plugin.stop()
|
|
285
|
-
|
|
286
|
-
const routes = {}
|
|
287
|
-
plugin.registerWithRouter({
|
|
288
|
-
get: (path, handler) => {
|
|
289
|
-
routes[path] = handler
|
|
290
|
-
},
|
|
291
|
-
post: () => {}
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
const res = {
|
|
295
|
-
json: (data) => {
|
|
296
|
-
assert.equal(data.position, 'top')
|
|
297
|
-
assert.deepEqual(data.apps, [])
|
|
298
|
-
done()
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
routes['/settings']({}, res)
|
|
302
|
-
})
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
describe('default apps seeding', () => {
|
|
306
|
-
it('seeds default apps when config has no apps key', () => {
|
|
307
|
-
const app = createMockApp()
|
|
308
|
-
let savedOptions = null
|
|
309
|
-
app.savePluginOptions = (opts, cb) => {
|
|
310
|
-
savedOptions = opts
|
|
311
|
-
cb(null)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const plugin = pluginFactory(app)
|
|
315
|
-
plugin.start({})
|
|
316
|
-
|
|
317
|
-
assert.ok(savedOptions)
|
|
318
|
-
assert.equal(savedOptions.apps[0].url, '/@signalk/freeboard-sk/')
|
|
319
|
-
assert.equal(savedOptions.apps[0].autostart, true)
|
|
320
|
-
assert.equal(savedOptions.apps[1].url, '/@halos-org/skip/')
|
|
321
|
-
assert.equal(savedOptions.apps[1].autostart, false)
|
|
322
|
-
assert.equal(savedOptions.apps[2].url, '/admin/')
|
|
323
|
-
assert.equal(savedOptions.apps[2].label, 'Settings')
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
it('does not seed when apps array already exists (even if empty)', () => {
|
|
327
|
-
const app = createMockApp()
|
|
328
|
-
let saveCalled = false
|
|
329
|
-
app.savePluginOptions = () => {
|
|
330
|
-
saveCalled = true
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const plugin = pluginFactory(app)
|
|
334
|
-
plugin.start({ apps: [] })
|
|
335
|
-
|
|
336
|
-
assert.equal(saveCalled, false)
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
it('preserves existing configured apps', () => {
|
|
340
|
-
const app = createMockApp()
|
|
341
|
-
let saveCalled = false
|
|
342
|
-
app.savePluginOptions = () => {
|
|
343
|
-
saveCalled = true
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const plugin = pluginFactory(app)
|
|
347
|
-
const existing = [{ enabled: true, url: '/custom-app/', label: 'Custom', icon: '', color: '' }]
|
|
348
|
-
plugin.start({ apps: existing })
|
|
349
|
-
|
|
350
|
-
assert.equal(saveCalled, false)
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
it('respects enabled:false when resolving apps', () => {
|
|
354
|
-
const app = createMockApp()
|
|
355
|
-
app.savePluginOptions = (_, cb) => cb(null)
|
|
356
|
-
const plugin = pluginFactory(app)
|
|
357
|
-
|
|
358
|
-
const configured = [
|
|
359
|
-
{ enabled: false, url: '/@signalk/freeboard-sk/', label: '', icon: '', color: '' },
|
|
360
|
-
{ enabled: true, url: '/@mxtommy/kip/', label: '', icon: '', color: '' }
|
|
361
|
-
]
|
|
362
|
-
plugin.start({ apps: configured })
|
|
363
|
-
|
|
364
|
-
const routes = {}
|
|
365
|
-
const router = {
|
|
366
|
-
get: (path, handler) => {
|
|
367
|
-
routes[path] = handler
|
|
368
|
-
},
|
|
369
|
-
post: () => {}
|
|
370
|
-
}
|
|
371
|
-
plugin.registerWithRouter(router)
|
|
372
|
-
|
|
373
|
-
let resolved
|
|
374
|
-
routes['/settings'](
|
|
375
|
-
{},
|
|
376
|
-
{
|
|
377
|
-
json: (d) => {
|
|
378
|
-
resolved = d
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
)
|
|
382
|
-
assert.ok(!resolved.apps.some((a) => a.url === '/@signalk/freeboard-sk/'))
|
|
383
|
-
assert.ok(resolved.apps.some((a) => a.url === '/@mxtommy/kip/'))
|
|
384
|
-
})
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
describe('package.json', () => {
|
|
388
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'))
|
|
389
|
-
|
|
390
|
-
it('has signalk-node-server-plugin keyword', () => {
|
|
391
|
-
assert.ok(pkg.keywords.includes('signalk-node-server-plugin'))
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
it('has signalk-webapp keyword', () => {
|
|
395
|
-
assert.ok(pkg.keywords.includes('signalk-webapp'))
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
it('has signalk-plugin-configurator keyword', () => {
|
|
399
|
-
assert.ok(pkg.keywords.includes('signalk-plugin-configurator'))
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
it('main points to plugin/index.js', () => {
|
|
403
|
-
assert.equal(pkg.main, 'plugin/index.js')
|
|
404
|
-
assert.ok(fs.existsSync(path.join(__dirname, '..', pkg.main)))
|
|
405
|
-
})
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
describe('night mode PUT handler', () => {
|
|
409
|
-
it('registers PUT handler when showNightModeButton is true', () => {
|
|
410
|
-
let registered = false
|
|
411
|
-
const app = createMockApp()
|
|
412
|
-
app.registerPutHandler = (context, path, callback, sourceId) => {
|
|
413
|
-
registered = true
|
|
414
|
-
assert.equal(context, 'vessels.self')
|
|
415
|
-
assert.equal(path, 'environment.mode')
|
|
416
|
-
assert.equal(sourceId, 'signalk-app-dock')
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const plugin = pluginFactory(app)
|
|
420
|
-
const originalSetTimeout = globalThis.setTimeout
|
|
421
|
-
globalThis.setTimeout = (fn) => fn()
|
|
422
|
-
try {
|
|
423
|
-
plugin.start({ showNightModeButton: true, apps: [] })
|
|
424
|
-
} finally {
|
|
425
|
-
globalThis.setTimeout = originalSetTimeout
|
|
426
|
-
}
|
|
427
|
-
assert.ok(registered)
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
it('does not register PUT handler when showNightModeButton is false', () => {
|
|
431
|
-
let registered = false
|
|
432
|
-
const app = createMockApp()
|
|
433
|
-
app.registerPutHandler = () => {
|
|
434
|
-
registered = true
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const plugin = pluginFactory(app)
|
|
438
|
-
const originalSetTimeout = globalThis.setTimeout
|
|
439
|
-
globalThis.setTimeout = (fn) => fn()
|
|
440
|
-
try {
|
|
441
|
-
plugin.start({ showNightModeButton: false, apps: [] })
|
|
442
|
-
} finally {
|
|
443
|
-
globalThis.setTimeout = originalSetTimeout
|
|
444
|
-
}
|
|
445
|
-
assert.equal(registered, false)
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
it('PUT handler accepts valid mode values and emits delta', () => {
|
|
449
|
-
const app = createMockApp()
|
|
450
|
-
let handlerCallback
|
|
451
|
-
let emittedDelta = null
|
|
452
|
-
app.registerPutHandler = (ctx, path, cb) => {
|
|
453
|
-
handlerCallback = cb
|
|
454
|
-
}
|
|
455
|
-
app.handleMessage = (id, delta) => {
|
|
456
|
-
emittedDelta = delta
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const plugin = pluginFactory(app)
|
|
460
|
-
const originalSetTimeout = globalThis.setTimeout
|
|
461
|
-
globalThis.setTimeout = (fn) => fn()
|
|
462
|
-
try {
|
|
463
|
-
plugin.start({ showNightModeButton: true, apps: [] })
|
|
464
|
-
} finally {
|
|
465
|
-
globalThis.setTimeout = originalSetTimeout
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const result = handlerCallback('vessels.self', 'environment.mode', 'night', () => {})
|
|
469
|
-
assert.equal(result.state, 'COMPLETED')
|
|
470
|
-
assert.equal(result.statusCode, 200)
|
|
471
|
-
assert.equal(emittedDelta.updates[0].values[0].path, 'environment.mode')
|
|
472
|
-
assert.equal(emittedDelta.updates[0].values[0].value, 'night')
|
|
473
|
-
})
|
|
474
|
-
|
|
475
|
-
it('PUT handler rejects invalid mode values', () => {
|
|
476
|
-
const app = createMockApp()
|
|
477
|
-
let handlerCallback
|
|
478
|
-
app.registerPutHandler = (ctx, path, cb) => {
|
|
479
|
-
handlerCallback = cb
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const plugin = pluginFactory(app)
|
|
483
|
-
const originalSetTimeout = globalThis.setTimeout
|
|
484
|
-
globalThis.setTimeout = (fn) => fn()
|
|
485
|
-
try {
|
|
486
|
-
plugin.start({ showNightModeButton: true, apps: [] })
|
|
487
|
-
} finally {
|
|
488
|
-
globalThis.setTimeout = originalSetTimeout
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const result = handlerCallback('vessels.self', 'environment.mode', 'invalid', () => {})
|
|
492
|
-
assert.equal(result.statusCode, 400)
|
|
493
|
-
})
|
|
494
|
-
})
|
|
495
|
-
|
|
496
|
-
describe('dismiss-tour endpoint', () => {
|
|
497
|
-
it('saves tourDismissed: true and responds', (t, done) => {
|
|
498
|
-
const app = createMockApp()
|
|
499
|
-
let saved = null
|
|
500
|
-
app.savePluginOptions = (opts, cb) => {
|
|
501
|
-
saved = opts
|
|
502
|
-
cb(null)
|
|
503
|
-
}
|
|
504
|
-
const plugin = pluginFactory(app)
|
|
505
|
-
plugin.start({ apps: [], position: 'left' })
|
|
506
|
-
|
|
507
|
-
const routes = { get: {}, post: {} }
|
|
508
|
-
const router = {
|
|
509
|
-
get: (path, handler) => {
|
|
510
|
-
routes.get[path] = handler
|
|
511
|
-
},
|
|
512
|
-
post: (path, handler) => {
|
|
513
|
-
routes.post[path] = handler
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
plugin.registerWithRouter(router)
|
|
517
|
-
|
|
518
|
-
const res = {
|
|
519
|
-
json: (data) => {
|
|
520
|
-
assert.equal(data.tourDismissed, true)
|
|
521
|
-
assert.equal(saved.tourDismissed, true)
|
|
522
|
-
assert.equal(saved.position, 'left')
|
|
523
|
-
done()
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
routes.post['/dismiss-tour']({}, res)
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
it('keeps the saved configuration when the plugin is not running', (t, done) => {
|
|
530
|
-
const app = createMockApp()
|
|
531
|
-
const apps = [{ url: '/a/', label: 'A', enabled: true }]
|
|
532
|
-
app.readPluginOptions = () => ({ enabled: false, configuration: { position: 'right', apps } })
|
|
533
|
-
let saved = null
|
|
534
|
-
app.savePluginOptions = (opts, cb) => {
|
|
535
|
-
saved = opts
|
|
536
|
-
cb(null)
|
|
537
|
-
}
|
|
538
|
-
const plugin = pluginFactory(app)
|
|
539
|
-
|
|
540
|
-
const routes = {}
|
|
541
|
-
plugin.registerWithRouter({
|
|
542
|
-
get: () => {},
|
|
543
|
-
post: (path, handler) => {
|
|
544
|
-
routes[path] = handler
|
|
545
|
-
}
|
|
546
|
-
})
|
|
547
|
-
|
|
548
|
-
const res = {
|
|
549
|
-
json: (data) => {
|
|
550
|
-
assert.equal(data.tourDismissed, true)
|
|
551
|
-
assert.deepEqual(saved, { position: 'right', apps, tourDismissed: true })
|
|
552
|
-
done()
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
routes['/dismiss-tour']({}, res)
|
|
556
|
-
})
|
|
557
|
-
})
|
|
558
|
-
|
|
559
|
-
describe('mode endpoint', () => {
|
|
560
|
-
it('returns day when no mode is set', (t, done) => {
|
|
561
|
-
const app = createMockApp()
|
|
562
|
-
const plugin = pluginFactory(app)
|
|
563
|
-
const routes = {}
|
|
564
|
-
const router = {
|
|
565
|
-
get: (path, handler) => {
|
|
566
|
-
routes[path] = handler
|
|
567
|
-
},
|
|
568
|
-
post: () => {}
|
|
569
|
-
}
|
|
570
|
-
plugin.registerWithRouter(router)
|
|
571
|
-
|
|
572
|
-
const res = {
|
|
573
|
-
json: (data) => {
|
|
574
|
-
assert.equal(data.value, 'day')
|
|
575
|
-
done()
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
routes['/mode']({}, res)
|
|
579
|
-
})
|
|
580
|
-
|
|
581
|
-
it('returns current mode when set', (t, done) => {
|
|
582
|
-
const app = createMockApp()
|
|
583
|
-
app.getSelfPath = (path) => {
|
|
584
|
-
if (path === 'environment.mode') return { value: 'night' }
|
|
585
|
-
return null
|
|
586
|
-
}
|
|
587
|
-
const plugin = pluginFactory(app)
|
|
588
|
-
const routes = {}
|
|
589
|
-
const router = {
|
|
590
|
-
get: (path, handler) => {
|
|
591
|
-
routes[path] = handler
|
|
592
|
-
},
|
|
593
|
-
post: () => {}
|
|
594
|
-
}
|
|
595
|
-
plugin.registerWithRouter(router)
|
|
596
|
-
|
|
597
|
-
const res = {
|
|
598
|
-
json: (data) => {
|
|
599
|
-
assert.equal(data.value, 'night')
|
|
600
|
-
done()
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
routes['/mode']({}, res)
|
|
604
|
-
})
|
|
605
|
-
})
|
|
606
|
-
|
|
607
|
-
describe('route access', () => {
|
|
608
|
-
// Records each route with the level it is registered at: 'admin' for a
|
|
609
|
-
// route registered on the router itself, the access() level otherwise.
|
|
610
|
-
function recordingRouter({ withAccess }) {
|
|
611
|
-
const levels = {}
|
|
612
|
-
const registrar = (level) => ({
|
|
613
|
-
get: (path) => {
|
|
614
|
-
levels[`GET ${path}`] = level
|
|
615
|
-
},
|
|
616
|
-
post: (path) => {
|
|
617
|
-
levels[`POST ${path}`] = level
|
|
618
|
-
}
|
|
619
|
-
})
|
|
620
|
-
const router = registrar('admin')
|
|
621
|
-
if (withAccess) router.access = registrar
|
|
622
|
-
return { router, levels }
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
it('opens /settings and /mode to readonly users and /dismiss-tour to readwrite users', () => {
|
|
626
|
-
const plugin = pluginFactory(createMockApp())
|
|
627
|
-
const { router, levels } = recordingRouter({ withAccess: true })
|
|
628
|
-
plugin.registerWithRouter(router)
|
|
629
|
-
|
|
630
|
-
assert.deepEqual(levels, {
|
|
631
|
-
'GET /settings': 'readonly',
|
|
632
|
-
'GET /webapps': 'admin',
|
|
633
|
-
'GET /mode': 'readonly',
|
|
634
|
-
'POST /dismiss-tour': 'readwrite'
|
|
635
|
-
})
|
|
636
|
-
})
|
|
637
|
-
|
|
638
|
-
it('registers every route on the router itself when it has no access()', () => {
|
|
639
|
-
const plugin = pluginFactory(createMockApp())
|
|
640
|
-
const { router, levels } = recordingRouter({ withAccess: false })
|
|
641
|
-
plugin.registerWithRouter(router)
|
|
642
|
-
|
|
643
|
-
assert.deepEqual(levels, {
|
|
644
|
-
'GET /settings': 'admin',
|
|
645
|
-
'GET /webapps': 'admin',
|
|
646
|
-
'GET /mode': 'admin',
|
|
647
|
-
'POST /dismiss-tour': 'admin'
|
|
648
|
-
})
|
|
649
|
-
})
|
|
650
|
-
})
|
|
651
|
-
|
|
652
|
-
describe('public files', () => {
|
|
653
|
-
const publicDir = path.join(__dirname, '..', 'public')
|
|
654
|
-
|
|
655
|
-
it('index.html exists', () => {
|
|
656
|
-
assert.ok(fs.existsSync(path.join(publicDir, 'index.html')))
|
|
657
|
-
})
|
|
658
|
-
|
|
659
|
-
it('dock.js exists', () => {
|
|
660
|
-
assert.ok(fs.existsSync(path.join(publicDir, 'dock.js')))
|
|
661
|
-
})
|
|
662
|
-
|
|
663
|
-
it('dock.css exists', () => {
|
|
664
|
-
assert.ok(fs.existsSync(path.join(publicDir, 'dock.css')))
|
|
665
|
-
})
|
|
666
|
-
|
|
667
|
-
it('remoteEntry.js exists (Module Federation)', () => {
|
|
668
|
-
assert.ok(fs.existsSync(path.join(publicDir, 'remoteEntry.js')))
|
|
669
|
-
})
|
|
670
|
-
|
|
671
|
-
it('app-icon.svg exists', () => {
|
|
672
|
-
assert.ok(fs.existsSync(path.join(publicDir, 'app-icon.svg')))
|
|
673
|
-
})
|
|
674
|
-
|
|
675
|
-
it('icon-settings.svg exists', () => {
|
|
676
|
-
assert.ok(fs.existsSync(path.join(publicDir, 'icon-settings.svg')))
|
|
677
|
-
})
|
|
678
|
-
|
|
679
|
-
it('manifest.webmanifest exists and is valid JSON with required PWA fields', () => {
|
|
680
|
-
const manifestPath = path.join(publicDir, 'manifest.webmanifest')
|
|
681
|
-
assert.ok(fs.existsSync(manifestPath))
|
|
682
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
683
|
-
assert.equal(typeof manifest.name, 'string')
|
|
684
|
-
assert.equal(typeof manifest.start_url, 'string')
|
|
685
|
-
assert.equal(manifest.display, 'standalone')
|
|
686
|
-
assert.ok(Array.isArray(manifest.icons) && manifest.icons.length > 0)
|
|
687
|
-
})
|
|
688
|
-
|
|
689
|
-
it('app-icon-180.png exists (apple-touch-icon)', () => {
|
|
690
|
-
assert.ok(fs.existsSync(path.join(publicDir, 'app-icon-180.png')))
|
|
691
|
-
})
|
|
692
|
-
|
|
693
|
-
it('app-icon-maskable.svg exists', () => {
|
|
694
|
-
assert.ok(fs.existsSync(path.join(publicDir, 'app-icon-maskable.svg')))
|
|
695
|
-
})
|
|
696
|
-
})
|