signalk-vessels-to-ais 1.6.0 → 2.0.0-beta.1
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/.github/dependabot.yml +0 -0
- package/.mocharc.json +5 -0
- package/README.md +34 -32
- package/index.js +244 -463
- package/lib/helpers.js +357 -0
- package/package.json +27 -26
- package/test/helpers.test.js +609 -0
- package/test/plugin.test.js +479 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
const assert = require('assert')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration tests for signalk-vessels-to-ais-ws plugin
|
|
5
|
+
* Tests the plugin with mock SignalK app object
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Mock ggencoder module
|
|
9
|
+
const mockAisEncode = {
|
|
10
|
+
nmea: null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Create mock for ggencoder
|
|
14
|
+
const mockGgencoder = {
|
|
15
|
+
AisEncode: function (msg) {
|
|
16
|
+
this.msg = msg
|
|
17
|
+
// Generate a simple mock NMEA sentence
|
|
18
|
+
this.nmea = `!AIVDM,1,1,,A,${msg.aistype}${msg.mmsi},0*00`
|
|
19
|
+
return this
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// We need to mock the require before loading the plugin
|
|
24
|
+
// For now, we'll test the helpers directly and create mock-based integration tests
|
|
25
|
+
|
|
26
|
+
describe('signalk-vessels-to-ais-ws plugin integration', function () {
|
|
27
|
+
/**
|
|
28
|
+
* Create a mock SignalK app object
|
|
29
|
+
*/
|
|
30
|
+
function createMockApp(options = {}) {
|
|
31
|
+
const debugMessages = []
|
|
32
|
+
const emittedMessages = []
|
|
33
|
+
const statusMessages = []
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
// Data access methods
|
|
37
|
+
getSelfPath: function (path) {
|
|
38
|
+
if (options.selfPath && options.selfPath[path]) {
|
|
39
|
+
return options.selfPath[path]
|
|
40
|
+
}
|
|
41
|
+
if (path === 'navigation.position.value') {
|
|
42
|
+
return options.ownPosition || null
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
getPath: function (path) {
|
|
48
|
+
if (path === 'vessels') {
|
|
49
|
+
return options.vessels || null
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Logging and status
|
|
55
|
+
debug: function (...args) {
|
|
56
|
+
debugMessages.push(args.join(' '))
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
setPluginStatus: function (msg) {
|
|
60
|
+
statusMessages.push(msg)
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
setProviderStatus: function (msg) {
|
|
64
|
+
statusMessages.push(msg)
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// Event emission
|
|
68
|
+
emit: function (eventName, data) {
|
|
69
|
+
emittedMessages.push({ eventName, data })
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
reportOutputMessages: function (count) {
|
|
73
|
+
// Track output message count
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// Test helpers
|
|
77
|
+
_getDebugMessages: () => debugMessages,
|
|
78
|
+
_getEmittedMessages: () => emittedMessages,
|
|
79
|
+
_getStatusMessages: () => statusMessages,
|
|
80
|
+
_clearMessages: () => {
|
|
81
|
+
debugMessages.length = 0
|
|
82
|
+
emittedMessages.length = 0
|
|
83
|
+
statusMessages.length = 0
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create a mock vessel object using real SignalK structure
|
|
90
|
+
*/
|
|
91
|
+
function createMockVessel(options = {}) {
|
|
92
|
+
const now = new Date().toISOString()
|
|
93
|
+
return {
|
|
94
|
+
mmsi: options.mmsi || '123456789',
|
|
95
|
+
name: options.name || 'Test Vessel',
|
|
96
|
+
navigation: {
|
|
97
|
+
position: {
|
|
98
|
+
// Real SignalK structure: value contains {latitude, longitude}
|
|
99
|
+
value: {
|
|
100
|
+
latitude: options.lat || 60.1,
|
|
101
|
+
longitude: options.lon || 24.9
|
|
102
|
+
},
|
|
103
|
+
timestamp: options.timestamp || now
|
|
104
|
+
},
|
|
105
|
+
speedOverGround: { value: options.sog || 5.0 },
|
|
106
|
+
courseOverGroundTrue: { value: options.cog || Math.PI / 4 },
|
|
107
|
+
headingTrue: { value: options.hdg || Math.PI / 4 },
|
|
108
|
+
rateOfTurn: { value: options.rot || 0 },
|
|
109
|
+
state: { value: options.state || 'motoring' }
|
|
110
|
+
},
|
|
111
|
+
sensors: {
|
|
112
|
+
ais: {
|
|
113
|
+
class: { value: options.aisClass || 'A' }
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
design: {
|
|
117
|
+
length: { overall: { value: options.length || 15 } },
|
|
118
|
+
beam: { value: options.beam || 5 },
|
|
119
|
+
draft: { current: { value: options.draft || 2 } },
|
|
120
|
+
aisShipType: { id: { value: options.shipType || 36 } }
|
|
121
|
+
},
|
|
122
|
+
communication: {
|
|
123
|
+
callsignVhf: { value: options.callsign || 'TEST1' }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
describe('plugin initialization', function () {
|
|
129
|
+
it('exports a function', function () {
|
|
130
|
+
// We can't easily test the full plugin due to ggencoder dependency
|
|
131
|
+
// but we can verify the export pattern
|
|
132
|
+
const helpers = require('../lib/helpers')
|
|
133
|
+
assert(typeof helpers.getValue === 'function')
|
|
134
|
+
assert(typeof helpers.extractVesselData === 'function')
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('mock app integration', function () {
|
|
139
|
+
it('getSelfPath returns own position', function () {
|
|
140
|
+
const app = createMockApp({
|
|
141
|
+
ownPosition: { latitude: 60.0, longitude: 24.0 }
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const pos = app.getSelfPath('navigation.position.value')
|
|
145
|
+
assert.strictEqual(pos.latitude, 60.0)
|
|
146
|
+
assert.strictEqual(pos.longitude, 24.0)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('getPath returns vessels object', function () {
|
|
150
|
+
const vessels = {
|
|
151
|
+
'urn:mrn:imo:mmsi:123456789': createMockVessel()
|
|
152
|
+
}
|
|
153
|
+
const app = createMockApp({ vessels })
|
|
154
|
+
|
|
155
|
+
const result = app.getPath('vessels')
|
|
156
|
+
assert.deepStrictEqual(result, vessels)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('emit captures messages', function () {
|
|
160
|
+
const app = createMockApp()
|
|
161
|
+
app.emit('nmea0183out', '!AIVDM,1,1,,A,test,0*00')
|
|
162
|
+
|
|
163
|
+
const messages = app._getEmittedMessages()
|
|
164
|
+
assert.strictEqual(messages.length, 1)
|
|
165
|
+
assert.strictEqual(messages[0].eventName, 'nmea0183out')
|
|
166
|
+
assert.strictEqual(messages[0].data, '!AIVDM,1,1,,A,test,0*00')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('debug captures log messages', function () {
|
|
170
|
+
const app = createMockApp()
|
|
171
|
+
app.debug('Test message', 123)
|
|
172
|
+
|
|
173
|
+
const messages = app._getDebugMessages()
|
|
174
|
+
assert.strictEqual(messages.length, 1)
|
|
175
|
+
assert.strictEqual(messages[0], 'Test message 123')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('setPluginStatus captures status', function () {
|
|
179
|
+
const app = createMockApp()
|
|
180
|
+
app.setPluginStatus('Running')
|
|
181
|
+
|
|
182
|
+
const messages = app._getStatusMessages()
|
|
183
|
+
assert.strictEqual(messages.length, 1)
|
|
184
|
+
assert.strictEqual(messages[0], 'Running')
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('vessel data extraction with helpers', function () {
|
|
189
|
+
const { extractVesselData, isDataFresh } = require('../lib/helpers')
|
|
190
|
+
|
|
191
|
+
it('extracts complete vessel data', function () {
|
|
192
|
+
const vessel = createMockVessel({
|
|
193
|
+
mmsi: '230123456',
|
|
194
|
+
name: 'Nordic Spirit',
|
|
195
|
+
lat: 60.15,
|
|
196
|
+
lon: 24.95,
|
|
197
|
+
sog: 8.0,
|
|
198
|
+
aisClass: 'A'
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const data = extractVesselData(vessel)
|
|
202
|
+
|
|
203
|
+
assert.strictEqual(data.mmsi, '230123456')
|
|
204
|
+
assert.strictEqual(data.shipName, 'Nordic Spirit')
|
|
205
|
+
assert.strictEqual(data.lat, 60.15)
|
|
206
|
+
assert.strictEqual(data.lon, 24.95)
|
|
207
|
+
assert.strictEqual(data.aisClass, 'A')
|
|
208
|
+
// SOG converted from m/s to knots
|
|
209
|
+
assert(data.sog > 15 && data.sog < 16) // 8 m/s ≈ 15.55 knots
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('handles Class B vessel', function () {
|
|
213
|
+
const vessel = createMockVessel({
|
|
214
|
+
aisClass: 'B',
|
|
215
|
+
mmsi: '230654321'
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const data = extractVesselData(vessel)
|
|
219
|
+
assert.strictEqual(data.aisClass, 'B')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('handles BASE station', function () {
|
|
223
|
+
const vessel = createMockVessel({
|
|
224
|
+
aisClass: 'BASE',
|
|
225
|
+
mmsi: '002766140'
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const data = extractVesselData(vessel)
|
|
229
|
+
assert.strictEqual(data.aisClass, 'BASE')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('validates data freshness', function () {
|
|
233
|
+
const fresh = new Date().toISOString()
|
|
234
|
+
const stale = new Date(Date.now() - 120000).toISOString() // 2 minutes ago
|
|
235
|
+
|
|
236
|
+
assert.strictEqual(isDataFresh(fresh, 60), true)
|
|
237
|
+
assert.strictEqual(isDataFresh(stale, 60), false)
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
describe('AIS message building', function () {
|
|
242
|
+
const {
|
|
243
|
+
buildAisMessage3,
|
|
244
|
+
buildAisMessage5,
|
|
245
|
+
buildAisMessage18,
|
|
246
|
+
buildAisMessage24A,
|
|
247
|
+
buildAisMessage24B,
|
|
248
|
+
extractVesselData
|
|
249
|
+
} = require('../lib/helpers')
|
|
250
|
+
|
|
251
|
+
it('builds complete Class A message set', function () {
|
|
252
|
+
const vessel = createMockVessel({
|
|
253
|
+
mmsi: '230111222',
|
|
254
|
+
name: 'Class A Ship',
|
|
255
|
+
aisClass: 'A'
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
const data = extractVesselData(vessel)
|
|
259
|
+
const msg3 = buildAisMessage3(data, false)
|
|
260
|
+
const msg5 = buildAisMessage5(data, false)
|
|
261
|
+
|
|
262
|
+
// Message 3 - Position report
|
|
263
|
+
assert.strictEqual(msg3.aistype, 3)
|
|
264
|
+
assert.strictEqual(msg3.mmsi, '230111222')
|
|
265
|
+
assert.strictEqual(msg3.own, false)
|
|
266
|
+
|
|
267
|
+
// Message 5 - Static data
|
|
268
|
+
assert.strictEqual(msg5.aistype, 5)
|
|
269
|
+
assert.strictEqual(msg5.shipname, 'Class A Ship')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('builds complete Class B message set', function () {
|
|
273
|
+
const vessel = createMockVessel({
|
|
274
|
+
mmsi: '230333444',
|
|
275
|
+
name: 'Class B Boat',
|
|
276
|
+
aisClass: 'B'
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const data = extractVesselData(vessel)
|
|
280
|
+
const msg18 = buildAisMessage18(data, false)
|
|
281
|
+
const msg24a = buildAisMessage24A(data, false)
|
|
282
|
+
const msg24b = buildAisMessage24B(data, false)
|
|
283
|
+
|
|
284
|
+
// Message 18 - Position report
|
|
285
|
+
assert.strictEqual(msg18.aistype, 18)
|
|
286
|
+
assert.strictEqual(msg18.mmsi, '230333444')
|
|
287
|
+
|
|
288
|
+
// Message 24A - Name
|
|
289
|
+
assert.strictEqual(msg24a.aistype, 24)
|
|
290
|
+
assert.strictEqual(msg24a.part, 0)
|
|
291
|
+
assert.strictEqual(msg24a.shipname, 'Class B Boat')
|
|
292
|
+
|
|
293
|
+
// Message 24B - Call sign & dimensions
|
|
294
|
+
assert.strictEqual(msg24b.aistype, 24)
|
|
295
|
+
assert.strictEqual(msg24b.part, 1)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('sets own flag for own vessel', function () {
|
|
299
|
+
const vessel = createMockVessel()
|
|
300
|
+
const data = extractVesselData(vessel)
|
|
301
|
+
|
|
302
|
+
const msgOwn = buildAisMessage3(data, true)
|
|
303
|
+
const msgOther = buildAisMessage3(data, false)
|
|
304
|
+
|
|
305
|
+
assert.strictEqual(msgOwn.own, true)
|
|
306
|
+
assert.strictEqual(msgOther.own, false)
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
describe('distance calculation scenario', function () {
|
|
311
|
+
const haversine = require('haversine-distance')
|
|
312
|
+
|
|
313
|
+
it('calculates distance between two positions', function () {
|
|
314
|
+
const ownPos = { lat: 60.0, lon: 24.0 }
|
|
315
|
+
const otherPos = { lat: 60.1, lon: 24.0 }
|
|
316
|
+
|
|
317
|
+
const distMeters = haversine(ownPos, otherPos)
|
|
318
|
+
const distKm = distMeters / 1000
|
|
319
|
+
|
|
320
|
+
// Should be approximately 11.1 km (1 degree latitude ≈ 111 km)
|
|
321
|
+
assert(distKm > 10 && distKm < 12)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('identifies vessels within range', function () {
|
|
325
|
+
const ownPos = { lat: 60.0, lon: 24.0 }
|
|
326
|
+
const nearVessel = { lat: 60.05, lon: 24.0 } // ~5.5 km
|
|
327
|
+
const farVessel = { lat: 61.0, lon: 24.0 } // ~111 km
|
|
328
|
+
|
|
329
|
+
const nearDist = haversine(ownPos, nearVessel) / 1000
|
|
330
|
+
const farDist = haversine(ownPos, farVessel) / 1000
|
|
331
|
+
|
|
332
|
+
const maxDistance = 50 // km
|
|
333
|
+
|
|
334
|
+
assert(nearDist < maxDistance, 'Near vessel should be within range')
|
|
335
|
+
assert(farDist > maxDistance, 'Far vessel should be out of range')
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
describe('navigation state mapping', function () {
|
|
340
|
+
const { getNavStatus } = require('../lib/helpers')
|
|
341
|
+
|
|
342
|
+
it('maps all motoring variations', function () {
|
|
343
|
+
assert.strictEqual(getNavStatus('motoring'), 0)
|
|
344
|
+
assert.strictEqual(getNavStatus('UnderWayUsingEngine'), 0)
|
|
345
|
+
assert.strictEqual(getNavStatus('under way using engine'), 0)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('maps all sailing variations', function () {
|
|
349
|
+
assert.strictEqual(getNavStatus('sailing'), 8)
|
|
350
|
+
assert.strictEqual(getNavStatus('UnderWaySailing'), 8)
|
|
351
|
+
assert.strictEqual(getNavStatus('under way sailing'), 8)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('maps anchored state', function () {
|
|
355
|
+
assert.strictEqual(getNavStatus('anchored'), 1)
|
|
356
|
+
assert.strictEqual(getNavStatus('AtAnchor'), 1)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('maps moored state', function () {
|
|
360
|
+
assert.strictEqual(getNavStatus('moored'), 5)
|
|
361
|
+
assert.strictEqual(getNavStatus('Moored'), 5)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('returns empty string for unknown state', function () {
|
|
365
|
+
assert.strictEqual(getNavStatus('flying'), '')
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
describe('tag block generation', function () {
|
|
370
|
+
const { createTagBlock, toHexString } = require('../lib/helpers')
|
|
371
|
+
|
|
372
|
+
it('generates valid NMEA tag block', function () {
|
|
373
|
+
const tag = createTagBlock(1609459200000) // 2021-01-01 00:00:00 UTC
|
|
374
|
+
|
|
375
|
+
// Tag block format: \s:source,c:timestamp*checksum\
|
|
376
|
+
assert(tag.startsWith('\\'), 'Should start with backslash')
|
|
377
|
+
assert(tag.endsWith('\\'), 'Should end with backslash')
|
|
378
|
+
assert(tag.includes('s:SK0001'), 'Should include source')
|
|
379
|
+
assert(tag.includes('c:1609459200000'), 'Should include timestamp')
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('generates correct hex for checksum', function () {
|
|
383
|
+
assert.strictEqual(toHexString(0), '00')
|
|
384
|
+
assert.strictEqual(toHexString(255), 'FF')
|
|
385
|
+
assert.strictEqual(toHexString(16), '10')
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
describe('edge cases', function () {
|
|
390
|
+
const { getValue, extractVesselData } = require('../lib/helpers')
|
|
391
|
+
|
|
392
|
+
it('handles empty vessel object', function () {
|
|
393
|
+
const data = extractVesselData({})
|
|
394
|
+
|
|
395
|
+
assert.strictEqual(data.mmsi, null)
|
|
396
|
+
assert.strictEqual(data.lat, null)
|
|
397
|
+
assert.strictEqual(data.lon, null)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('handles vessel with only MMSI', function () {
|
|
401
|
+
const data = extractVesselData({ mmsi: '123456789' })
|
|
402
|
+
|
|
403
|
+
assert.strictEqual(data.mmsi, '123456789')
|
|
404
|
+
assert.strictEqual(data.lat, null)
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
it('handles deeply nested null values', function () {
|
|
408
|
+
const vessel = {
|
|
409
|
+
navigation: {
|
|
410
|
+
position: null
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const data = extractVesselData(vessel)
|
|
415
|
+
assert.strictEqual(data.lat, null)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('handles numeric name (converts to empty string)', function () {
|
|
419
|
+
const vessel = {
|
|
420
|
+
mmsi: '123456789',
|
|
421
|
+
name: 12345
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const data = extractVesselData(vessel)
|
|
425
|
+
assert.strictEqual(data.shipName, '')
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it('handles IMO prefix removal', function () {
|
|
429
|
+
const vessel = {
|
|
430
|
+
mmsi: '123456789',
|
|
431
|
+
registrations: {
|
|
432
|
+
imo: { value: 'IMO 9876543' }
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const data = extractVesselData(vessel)
|
|
437
|
+
assert.strictEqual(data.imo, '9876543')
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
it('handles IMO without prefix', function () {
|
|
441
|
+
const vessel = {
|
|
442
|
+
mmsi: '123456789',
|
|
443
|
+
registrations: {
|
|
444
|
+
imo: { value: '9876543' }
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const data = extractVesselData(vessel)
|
|
449
|
+
assert.strictEqual(data.imo, '9876543')
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
describe('unit conversions', function () {
|
|
454
|
+
const { radToDegrees, msToKnots } = require('../lib/helpers')
|
|
455
|
+
|
|
456
|
+
it('converts common navigation angles', function () {
|
|
457
|
+
// North
|
|
458
|
+
assert.strictEqual(radToDegrees(0), 0)
|
|
459
|
+
// East
|
|
460
|
+
assert.strictEqual(radToDegrees(Math.PI / 2), 90)
|
|
461
|
+
// South
|
|
462
|
+
assert.strictEqual(radToDegrees(Math.PI), 180)
|
|
463
|
+
// West
|
|
464
|
+
assert.strictEqual(radToDegrees(3 * Math.PI / 2), 270)
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
it('converts common speeds', function () {
|
|
468
|
+
// 1 knot in m/s
|
|
469
|
+
const oneKnotInMs = 0.514444
|
|
470
|
+
const result = msToKnots(oneKnotInMs)
|
|
471
|
+
assert(Math.abs(result - 1.0) < 0.01, `Expected ~1 knot, got ${result}`)
|
|
472
|
+
|
|
473
|
+
// 10 knots
|
|
474
|
+
const tenKnotsInMs = 5.14444
|
|
475
|
+
const result10 = msToKnots(tenKnotsInMs)
|
|
476
|
+
assert(Math.abs(result10 - 10.0) < 0.1, `Expected ~10 knots, got ${result10}`)
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
})
|