midiwire 0.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/LICENSE +21 -0
- package/README.md +845 -0
- package/dist/midiwire.es.js +1987 -0
- package/dist/midiwire.umd.js +1 -0
- package/package.json +58 -0
- package/src/bindings/DataAttributeBinder.js +198 -0
- package/src/bindings/DataAttributeBinder.test.js +825 -0
- package/src/core/EventEmitter.js +93 -0
- package/src/core/EventEmitter.test.js +357 -0
- package/src/core/MIDIConnection.js +364 -0
- package/src/core/MIDIConnection.test.js +783 -0
- package/src/core/MIDIController.js +756 -0
- package/src/core/MIDIController.test.js +1958 -0
- package/src/core/MIDIDeviceManager.js +204 -0
- package/src/core/MIDIDeviceManager.test.js +638 -0
- package/src/core/errors.js +99 -0
- package/src/index.js +181 -0
- package/src/utils/dx7.js +1294 -0
- package/src/utils/dx7.test.js +1208 -0
- package/src/utils/midi.js +244 -0
- package/src/utils/midi.test.js +260 -0
- package/src/utils/sysex.js +98 -0
- package/src/utils/sysex.test.js +222 -0
- package/src/utils/validators.js +88 -0
- package/src/utils/validators.test.js +300 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
2
|
+
import { createMIDIController } from "../index.js"
|
|
3
|
+
import { EventEmitter } from "./EventEmitter.js"
|
|
4
|
+
import { CONNECTION_EVENTS } from "./MIDIConnection.js"
|
|
5
|
+
import { MIDIDeviceManager } from "./MIDIDeviceManager.js"
|
|
6
|
+
|
|
7
|
+
describe("MIDIDeviceManager", () => {
|
|
8
|
+
let deviceManager
|
|
9
|
+
let statusUpdates = []
|
|
10
|
+
let connectionUpdates = []
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
statusUpdates = []
|
|
14
|
+
connectionUpdates = []
|
|
15
|
+
deviceManager = new MIDIDeviceManager({
|
|
16
|
+
onStatusUpdate: (message, state) => {
|
|
17
|
+
statusUpdates.push({ message, state })
|
|
18
|
+
},
|
|
19
|
+
onConnectionUpdate: (device, midi) => {
|
|
20
|
+
connectionUpdates.push({ device, midi })
|
|
21
|
+
},
|
|
22
|
+
channel: 1,
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe("constructor", () => {
|
|
27
|
+
it("should initialize with default options", () => {
|
|
28
|
+
const dm = new MIDIDeviceManager()
|
|
29
|
+
expect(dm.midi).toBe(null)
|
|
30
|
+
expect(dm.currentDevice).toBe(null)
|
|
31
|
+
expect(dm.isConnecting).toBe(false)
|
|
32
|
+
expect(dm.channel).toBe(1)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("should initialize with custom options", () => {
|
|
36
|
+
const mockMidi = { connection: new EventEmitter() }
|
|
37
|
+
const dm = new MIDIDeviceManager({
|
|
38
|
+
midiController: mockMidi,
|
|
39
|
+
channel: 5,
|
|
40
|
+
onStatusUpdate: () => {},
|
|
41
|
+
onConnectionUpdate: () => {},
|
|
42
|
+
})
|
|
43
|
+
expect(dm.midi).toBe(mockMidi)
|
|
44
|
+
expect(dm.channel).toBe(5)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe("setMIDI", () => {
|
|
49
|
+
it("should set the MIDI controller", () => {
|
|
50
|
+
const mockMidi = {
|
|
51
|
+
connection: new EventEmitter(),
|
|
52
|
+
}
|
|
53
|
+
deviceManager.setMIDI(mockMidi)
|
|
54
|
+
expect(deviceManager.midi).toBe(mockMidi)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe("getOutputDevices", () => {
|
|
59
|
+
it("should return empty array when no MIDI connection", () => {
|
|
60
|
+
expect(deviceManager.getOutputDevices()).toEqual([])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("should return list of devices from MIDI connection", () => {
|
|
64
|
+
const mockDevices = [
|
|
65
|
+
{ id: "1", name: "Device 1", manufacturer: "Company A" },
|
|
66
|
+
{ id: "2", name: "Device 2", manufacturer: "Company B" },
|
|
67
|
+
]
|
|
68
|
+
const mockMidi = {
|
|
69
|
+
connection: {
|
|
70
|
+
getOutputs: () => mockDevices,
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
deviceManager.midi = mockMidi
|
|
74
|
+
|
|
75
|
+
expect(deviceManager.getOutputDevices()).toEqual(mockDevices)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe("isDeviceConnected", () => {
|
|
80
|
+
it("should return false when no MIDI connection", () => {
|
|
81
|
+
expect(deviceManager.isDeviceConnected("Device 1")).toBe(false)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("should return true if device is connected", () => {
|
|
85
|
+
const mockMidi = {
|
|
86
|
+
connection: {
|
|
87
|
+
getOutputs: () => [
|
|
88
|
+
{ id: "1", name: "Device 1", manufacturer: "Company A" },
|
|
89
|
+
{ id: "2", name: "Device 2", manufacturer: "Company B" },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
deviceManager.midi = mockMidi
|
|
94
|
+
|
|
95
|
+
expect(deviceManager.isDeviceConnected("Device 1")).toBe(true)
|
|
96
|
+
expect(deviceManager.isDeviceConnected("Device 3")).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe("connectDeviceSelection", () => {
|
|
101
|
+
it("should connect to selected device", async () => {
|
|
102
|
+
const mockMidi = {
|
|
103
|
+
setOutput: vi.fn().mockResolvedValue(undefined),
|
|
104
|
+
getCurrentOutput: vi.fn().mockReturnValue({ name: "Device 1" }),
|
|
105
|
+
connection: {
|
|
106
|
+
on: vi.fn(),
|
|
107
|
+
disconnect: vi.fn(),
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
deviceManager.setMIDI(mockMidi)
|
|
111
|
+
|
|
112
|
+
const select = document.createElement("select")
|
|
113
|
+
select.innerHTML =
|
|
114
|
+
'<option value="">Select a device</option><option value="0">Device 1</option><option value="1">Device 2</option>'
|
|
115
|
+
|
|
116
|
+
let connectedDevice = null
|
|
117
|
+
deviceManager.connectDeviceSelection(select, async (_midi, device) => {
|
|
118
|
+
connectedDevice = device
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Simulate selecting device
|
|
122
|
+
select.value = "0"
|
|
123
|
+
select.dispatchEvent(new Event("change"))
|
|
124
|
+
|
|
125
|
+
// Wait for async operation
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
127
|
+
|
|
128
|
+
expect(mockMidi.setOutput).toHaveBeenCalledWith(0)
|
|
129
|
+
expect(connectedDevice).toEqual({ name: "Device 1" })
|
|
130
|
+
expect(deviceManager.currentDevice).toEqual({ name: "Device 1" })
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it("should disconnect when selecting empty option", async () => {
|
|
134
|
+
const mockMidi = {
|
|
135
|
+
setOutput: vi.fn(),
|
|
136
|
+
getCurrentOutput: vi.fn(),
|
|
137
|
+
connection: {
|
|
138
|
+
on: vi.fn(),
|
|
139
|
+
disconnect: vi.fn(),
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
deviceManager.setMIDI(mockMidi)
|
|
143
|
+
deviceManager.currentDevice = { name: "Device 1" }
|
|
144
|
+
|
|
145
|
+
const select = document.createElement("select")
|
|
146
|
+
deviceManager.connectDeviceSelection(select)
|
|
147
|
+
|
|
148
|
+
// Select empty option
|
|
149
|
+
select.value = ""
|
|
150
|
+
select.dispatchEvent(new Event("change"))
|
|
151
|
+
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
153
|
+
|
|
154
|
+
expect(mockMidi.connection.disconnect).toHaveBeenCalled()
|
|
155
|
+
expect(deviceManager.currentDevice).toBe(null)
|
|
156
|
+
expect(statusUpdates).toContainEqual({
|
|
157
|
+
message: "Disconnected",
|
|
158
|
+
state: "",
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it("should prevent concurrent connections", async () => {
|
|
163
|
+
const mockMidi = {
|
|
164
|
+
setOutput: vi
|
|
165
|
+
.fn()
|
|
166
|
+
.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 50))),
|
|
167
|
+
getCurrentOutput: vi.fn().mockReturnValue({ name: "Device 1" }),
|
|
168
|
+
connection: {
|
|
169
|
+
on: vi.fn(),
|
|
170
|
+
disconnect: vi.fn(),
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
deviceManager.setMIDI(mockMidi)
|
|
174
|
+
|
|
175
|
+
const select = document.createElement("select")
|
|
176
|
+
select.innerHTML =
|
|
177
|
+
'<option value="">Select a device</option><option value="0">Device 1</option>'
|
|
178
|
+
|
|
179
|
+
let connectCount = 0
|
|
180
|
+
deviceManager.connectDeviceSelection(select, async () => {
|
|
181
|
+
connectCount++
|
|
182
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// Trigger change twice rapidly
|
|
186
|
+
select.value = "0"
|
|
187
|
+
select.dispatchEvent(new Event("change"))
|
|
188
|
+
select.dispatchEvent(new Event("change"))
|
|
189
|
+
|
|
190
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
191
|
+
|
|
192
|
+
expect(connectCount).toBe(1) // Should only connect once
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe("connectChannelSelection", () => {
|
|
197
|
+
it("should update channel when selection changes", () => {
|
|
198
|
+
const mockMidi = {
|
|
199
|
+
options: { channel: 1 },
|
|
200
|
+
}
|
|
201
|
+
deviceManager.setMIDI(mockMidi)
|
|
202
|
+
|
|
203
|
+
const select = document.createElement("select")
|
|
204
|
+
select.innerHTML = ""
|
|
205
|
+
for (let i = 1; i <= 16; i++) {
|
|
206
|
+
const option = document.createElement("option")
|
|
207
|
+
option.value = i
|
|
208
|
+
option.textContent = i
|
|
209
|
+
select.appendChild(option)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
deviceManager.connectChannelSelection(select)
|
|
213
|
+
|
|
214
|
+
select.value = "5"
|
|
215
|
+
select.dispatchEvent(new Event("change"))
|
|
216
|
+
|
|
217
|
+
expect(mockMidi.options.channel).toBe(5)
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe("integration with createMIDIController", () => {
|
|
222
|
+
it("should work with real MIDIController", async () => {
|
|
223
|
+
// Mock navigator.requestMIDIAccess
|
|
224
|
+
global.navigator.requestMIDIAccess = vi.fn().mockResolvedValue({
|
|
225
|
+
outputs: new Map(),
|
|
226
|
+
inputs: new Map(),
|
|
227
|
+
onstatechange: null,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const midi = await createMIDIController({ autoConnect: false, channel: 3 })
|
|
231
|
+
deviceManager.setMIDI(midi)
|
|
232
|
+
|
|
233
|
+
expect(midi.options.channel).toBe(3)
|
|
234
|
+
expect(deviceManager.midi).toBe(midi)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
describe("updateStatus", () => {
|
|
239
|
+
it("should call onStatusUpdate with message and state", () => {
|
|
240
|
+
deviceManager.updateStatus("Test message", "test-state")
|
|
241
|
+
|
|
242
|
+
expect(statusUpdates).toContainEqual({
|
|
243
|
+
message: "Test message",
|
|
244
|
+
state: "test-state",
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it("should call onStatusUpdate with message and empty state", () => {
|
|
249
|
+
deviceManager.updateStatus("Test message")
|
|
250
|
+
|
|
251
|
+
expect(statusUpdates).toContainEqual({
|
|
252
|
+
message: "Test message",
|
|
253
|
+
state: "",
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe("updateConnectionStatus", () => {
|
|
259
|
+
it("should call onConnectionUpdate with current device and midi", () => {
|
|
260
|
+
const mockMidi = { connection: {} }
|
|
261
|
+
const mockDevice = { name: "Test Device" }
|
|
262
|
+
|
|
263
|
+
deviceManager.setMIDI(mockMidi)
|
|
264
|
+
deviceManager.currentDevice = mockDevice
|
|
265
|
+
|
|
266
|
+
deviceManager.updateConnectionStatus()
|
|
267
|
+
|
|
268
|
+
expect(connectionUpdates).toContainEqual({
|
|
269
|
+
device: mockDevice,
|
|
270
|
+
midi: mockMidi,
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it("should call onConnectionUpdate with null device", () => {
|
|
275
|
+
const mockMidi = { connection: {} }
|
|
276
|
+
|
|
277
|
+
deviceManager.setMIDI(mockMidi)
|
|
278
|
+
deviceManager.currentDevice = null
|
|
279
|
+
|
|
280
|
+
deviceManager.updateConnectionStatus()
|
|
281
|
+
|
|
282
|
+
expect(connectionUpdates).toContainEqual({
|
|
283
|
+
device: null,
|
|
284
|
+
midi: mockMidi,
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
describe("setupDeviceListeners", () => {
|
|
290
|
+
it("should set up device connection event listeners", () => {
|
|
291
|
+
const mockConnection = new EventEmitter()
|
|
292
|
+
const mockMidi = { connection: mockConnection }
|
|
293
|
+
|
|
294
|
+
deviceManager.setMIDI(mockMidi)
|
|
295
|
+
|
|
296
|
+
const onDeviceListChange = vi.fn()
|
|
297
|
+
deviceManager.setupDeviceListeners(onDeviceListChange)
|
|
298
|
+
|
|
299
|
+
// Emit OUTPUT_DEVICE_CONNECTED event
|
|
300
|
+
const device = { name: "Device 1", id: "1" }
|
|
301
|
+
mockConnection.emit(CONNECTION_EVENTS.OUTPUT_DEVICE_CONNECTED, { device })
|
|
302
|
+
|
|
303
|
+
expect(statusUpdates).toContainEqual({
|
|
304
|
+
message: "Device connected: Device 1",
|
|
305
|
+
state: "connected",
|
|
306
|
+
})
|
|
307
|
+
expect(onDeviceListChange).toHaveBeenCalled()
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it("should set up device disconnection event listeners", () => {
|
|
311
|
+
const mockConnection = new EventEmitter()
|
|
312
|
+
const mockMidi = { connection: mockConnection }
|
|
313
|
+
|
|
314
|
+
deviceManager.setMIDI(mockMidi)
|
|
315
|
+
deviceManager.currentDevice = { name: "Device 1" }
|
|
316
|
+
|
|
317
|
+
const onDeviceListChange = vi.fn()
|
|
318
|
+
deviceManager.setupDeviceListeners(onDeviceListChange)
|
|
319
|
+
|
|
320
|
+
// Emit OUTPUT_DEVICE_DISCONNECTED event
|
|
321
|
+
const device = { name: "Device 1", id: "1" }
|
|
322
|
+
mockConnection.emit(CONNECTION_EVENTS.OUTPUT_DEVICE_DISCONNECTED, { device })
|
|
323
|
+
|
|
324
|
+
expect(statusUpdates).toContainEqual({
|
|
325
|
+
message: "Device disconnected: Device 1",
|
|
326
|
+
state: "error",
|
|
327
|
+
})
|
|
328
|
+
expect(deviceManager.currentDevice).toBe(null)
|
|
329
|
+
expect(onDeviceListChange).toHaveBeenCalled()
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it("should not clear currentDevice if different device disconnected", () => {
|
|
333
|
+
const mockConnection = new EventEmitter()
|
|
334
|
+
const mockMidi = { connection: mockConnection }
|
|
335
|
+
|
|
336
|
+
deviceManager.setMIDI(mockMidi)
|
|
337
|
+
deviceManager.currentDevice = { name: "Device 1" }
|
|
338
|
+
|
|
339
|
+
const onDeviceListChange = vi.fn()
|
|
340
|
+
deviceManager.setupDeviceListeners(onDeviceListChange)
|
|
341
|
+
|
|
342
|
+
// Emit OUTPUT_DEVICE_DISCONNECTED for different device
|
|
343
|
+
const device = { name: "Device 2", id: "2" }
|
|
344
|
+
mockConnection.emit(CONNECTION_EVENTS.OUTPUT_DEVICE_DISCONNECTED, { device })
|
|
345
|
+
|
|
346
|
+
expect(deviceManager.currentDevice).toEqual({ name: "Device 1" })
|
|
347
|
+
expect(onDeviceListChange).toHaveBeenCalled()
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it("should return early if no MIDI connection", () => {
|
|
351
|
+
deviceManager.midi = null
|
|
352
|
+
|
|
353
|
+
const onDeviceListChange = vi.fn()
|
|
354
|
+
deviceManager.setupDeviceListeners(onDeviceListChange)
|
|
355
|
+
|
|
356
|
+
// Should not throw error and onDeviceListChange should not be called
|
|
357
|
+
expect(onDeviceListChange).not.toHaveBeenCalled()
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
describe("populateDeviceList", () => {
|
|
362
|
+
it("should populate select element with devices", () => {
|
|
363
|
+
const mockMidi = {
|
|
364
|
+
connection: {
|
|
365
|
+
getOutputs: () => [
|
|
366
|
+
{ id: "1", name: "Device 1", manufacturer: "Company A" },
|
|
367
|
+
{ id: "2", name: "Device 2", manufacturer: "Company B" },
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
}
|
|
371
|
+
deviceManager.setMIDI(mockMidi)
|
|
372
|
+
|
|
373
|
+
const select = document.createElement("select")
|
|
374
|
+
deviceManager.populateDeviceList(select)
|
|
375
|
+
|
|
376
|
+
expect(select.innerHTML).toContain('value=""')
|
|
377
|
+
expect(select.innerHTML).toContain('value="0"')
|
|
378
|
+
expect(select.innerHTML).toContain("Device 1")
|
|
379
|
+
expect(select.innerHTML).toContain("Device 2")
|
|
380
|
+
expect(statusUpdates).toContainEqual({
|
|
381
|
+
message: "Select a MIDI device",
|
|
382
|
+
state: "",
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it("should show no devices message when no outputs", () => {
|
|
387
|
+
const mockMidi = {
|
|
388
|
+
connection: {
|
|
389
|
+
getOutputs: () => [],
|
|
390
|
+
},
|
|
391
|
+
}
|
|
392
|
+
deviceManager.setMIDI(mockMidi)
|
|
393
|
+
|
|
394
|
+
const select = document.createElement("select")
|
|
395
|
+
deviceManager.populateDeviceList(select)
|
|
396
|
+
|
|
397
|
+
expect(select.innerHTML).toContain("No MIDI devices found")
|
|
398
|
+
expect(statusUpdates).toContainEqual({
|
|
399
|
+
message: "No MIDI devices available",
|
|
400
|
+
state: "error",
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it("should call onChange callback if provided", () => {
|
|
405
|
+
const mockMidi = {
|
|
406
|
+
connection: {
|
|
407
|
+
getOutputs: () => [{ id: "1", name: "Device 1", manufacturer: "Company A" }],
|
|
408
|
+
},
|
|
409
|
+
}
|
|
410
|
+
deviceManager.setMIDI(mockMidi)
|
|
411
|
+
|
|
412
|
+
const onChange = vi.fn()
|
|
413
|
+
const select = document.createElement("select")
|
|
414
|
+
|
|
415
|
+
deviceManager.populateDeviceList(select, onChange)
|
|
416
|
+
|
|
417
|
+
expect(onChange).toHaveBeenCalled()
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it("should return early if no select element", () => {
|
|
421
|
+
const mockMidi = {
|
|
422
|
+
connection: {
|
|
423
|
+
getOutputs: () => [{ id: "1", name: "Device 1", manufacturer: "Company A" }],
|
|
424
|
+
},
|
|
425
|
+
}
|
|
426
|
+
deviceManager.setMIDI(mockMidi)
|
|
427
|
+
|
|
428
|
+
statusUpdates = []
|
|
429
|
+
deviceManager.populateDeviceList(null)
|
|
430
|
+
|
|
431
|
+
expect(statusUpdates).toEqual([]) // No status updates should occur
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it("should keep current device selected if still available", () => {
|
|
435
|
+
const mockMidi = {
|
|
436
|
+
connection: {
|
|
437
|
+
getOutputs: () => [
|
|
438
|
+
{ id: "1", name: "Device 1", manufacturer: "Company A" },
|
|
439
|
+
{ id: "2", name: "Device 2", manufacturer: "Company B" },
|
|
440
|
+
],
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
deviceManager.setMIDI(mockMidi)
|
|
444
|
+
deviceManager.currentDevice = { name: "Device 1" }
|
|
445
|
+
|
|
446
|
+
const select = document.createElement("select")
|
|
447
|
+
deviceManager.populateDeviceList(select)
|
|
448
|
+
|
|
449
|
+
expect(select.value).toBe("0") // Device 1 is at index 0
|
|
450
|
+
expect(deviceManager.currentDevice).toEqual({ name: "Device 1" })
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it("should clear selection and currentDevice if device disconnected", () => {
|
|
454
|
+
const mockMidi = {
|
|
455
|
+
connection: {
|
|
456
|
+
getOutputs: () => [{ id: "2", name: "Device 2", manufacturer: "Company B" }],
|
|
457
|
+
},
|
|
458
|
+
}
|
|
459
|
+
deviceManager.setMIDI(mockMidi)
|
|
460
|
+
deviceManager.currentDevice = { name: "Device 1" }
|
|
461
|
+
|
|
462
|
+
statusUpdates = []
|
|
463
|
+
connectionUpdates = []
|
|
464
|
+
const select = document.createElement("select")
|
|
465
|
+
deviceManager.populateDeviceList(select)
|
|
466
|
+
|
|
467
|
+
expect(select.value).toBe("") // Selection cleared
|
|
468
|
+
expect(deviceManager.currentDevice).toBe(null) // Current device cleared
|
|
469
|
+
expect(connectionUpdates).toContainEqual({
|
|
470
|
+
device: null,
|
|
471
|
+
midi: mockMidi,
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
})
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
describe("createMIDIDeviceManager", () => {
|
|
478
|
+
let originalNavigator
|
|
479
|
+
let _coverageCallback
|
|
480
|
+
let mockOutput
|
|
481
|
+
|
|
482
|
+
beforeEach(() => {
|
|
483
|
+
originalNavigator = global.navigator
|
|
484
|
+
_coverageCallback = vi.fn()
|
|
485
|
+
|
|
486
|
+
mockOutput = {
|
|
487
|
+
id: "test-output-1",
|
|
488
|
+
name: "Test Output Device",
|
|
489
|
+
manufacturer: "Test Manufacturer",
|
|
490
|
+
state: "connected",
|
|
491
|
+
send: vi.fn(),
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
global.navigator = {
|
|
495
|
+
requestMIDIAccess: vi.fn().mockResolvedValue({
|
|
496
|
+
outputs: new Map([
|
|
497
|
+
["test-output-1", mockOutput],
|
|
498
|
+
[
|
|
499
|
+
"test-output-2",
|
|
500
|
+
{
|
|
501
|
+
id: "test-output-2",
|
|
502
|
+
name: "Test Output 2",
|
|
503
|
+
manufacturer: "Test Manufacturer",
|
|
504
|
+
state: "connected",
|
|
505
|
+
send: vi.fn(),
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
]),
|
|
509
|
+
inputs: new Map(),
|
|
510
|
+
onstatechange: null,
|
|
511
|
+
addEventListener: vi.fn(),
|
|
512
|
+
}),
|
|
513
|
+
}
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
afterEach(() => {
|
|
517
|
+
vi.clearAllMocks()
|
|
518
|
+
global.navigator = originalNavigator
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
it("should create a MIDIDeviceManager with integrated MIDIController", async () => {
|
|
522
|
+
const { createMIDIDeviceManager } = await import("../../src/index.js")
|
|
523
|
+
|
|
524
|
+
const deviceManager = await createMIDIDeviceManager({
|
|
525
|
+
channel: 2,
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
expect(deviceManager).toBeInstanceOf(MIDIDeviceManager)
|
|
529
|
+
expect(deviceManager.midi).toBeDefined()
|
|
530
|
+
expect(deviceManager.midi.options.channel).toBe(2)
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it("should auto-connect to specified device if output option provided", async () => {
|
|
534
|
+
const { createMIDIDeviceManager } = await import("../../src/index.js")
|
|
535
|
+
|
|
536
|
+
const deviceManager = await createMIDIDeviceManager({
|
|
537
|
+
output: "Test Output Device",
|
|
538
|
+
channel: 1,
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
expect(deviceManager.currentDevice).toEqual({
|
|
542
|
+
id: "test-output-1",
|
|
543
|
+
name: "Test Output Device",
|
|
544
|
+
manufacturer: "Test Manufacturer",
|
|
545
|
+
})
|
|
546
|
+
// Device is connected if currentDevice is set
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
it("should call onReady callback when initialization completes", async () => {
|
|
550
|
+
const { createMIDIDeviceManager } = await import("../../src/index.js")
|
|
551
|
+
|
|
552
|
+
const onReady = vi.fn()
|
|
553
|
+
|
|
554
|
+
const deviceManager = await createMIDIDeviceManager({
|
|
555
|
+
onReady,
|
|
556
|
+
channel: 1,
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
expect(onReady).toHaveBeenCalledWith(deviceManager.midi, deviceManager)
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
it("should handle onStatusUpdate callback", async () => {
|
|
563
|
+
const { createMIDIDeviceManager } = await import("../../src/index.js")
|
|
564
|
+
|
|
565
|
+
const statusUpdates = []
|
|
566
|
+
const deviceManager = await createMIDIDeviceManager({
|
|
567
|
+
onStatusUpdate: (message, state) => {
|
|
568
|
+
statusUpdates.push({ message, state })
|
|
569
|
+
},
|
|
570
|
+
channel: 1,
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
deviceManager.updateStatus("Test status", "connected")
|
|
574
|
+
|
|
575
|
+
expect(statusUpdates).toContainEqual({ message: "Test status", state: "connected" })
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it("should handle onConnectionUpdate callback", async () => {
|
|
579
|
+
const { createMIDIDeviceManager } = await import("../../src/index.js")
|
|
580
|
+
|
|
581
|
+
const connectionUpdates = []
|
|
582
|
+
const deviceManager = await createMIDIDeviceManager({
|
|
583
|
+
onConnectionUpdate: (device, midi) => {
|
|
584
|
+
connectionUpdates.push({ device, midi })
|
|
585
|
+
},
|
|
586
|
+
channel: 1,
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
// Simulate a connection update
|
|
590
|
+
deviceManager.onConnectionUpdate({ name: "Test Device" }, deviceManager.midi)
|
|
591
|
+
|
|
592
|
+
expect(connectionUpdates).toContainEqual({
|
|
593
|
+
device: { name: "Test Device" },
|
|
594
|
+
midi: deviceManager.midi,
|
|
595
|
+
})
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
it("should pass through options to MIDIController", async () => {
|
|
599
|
+
const { createMIDIDeviceManager } = await import("../../src/index.js")
|
|
600
|
+
|
|
601
|
+
const deviceManager = await createMIDIDeviceManager({
|
|
602
|
+
channel: 5,
|
|
603
|
+
sysex: true,
|
|
604
|
+
selector: "[data-midi-cc]",
|
|
605
|
+
watchDOM: true,
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
expect(deviceManager.midi.options.channel).toBe(5)
|
|
609
|
+
// sysex is passed to MIDIConnection constructor
|
|
610
|
+
expect(deviceManager.midi.connection.options.sysex).toBe(true)
|
|
611
|
+
expect(deviceManager.midi._binder).toBeDefined() // Auto-binding was enabled
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it("should handle auto-connect errors gracefully", async () => {
|
|
615
|
+
const { createMIDIDeviceManager } = await import("../../src/index.js")
|
|
616
|
+
|
|
617
|
+
const onError = vi.fn()
|
|
618
|
+
|
|
619
|
+
const deviceManager = await createMIDIDeviceManager({
|
|
620
|
+
output: "Non-existent Device",
|
|
621
|
+
onError,
|
|
622
|
+
channel: 1,
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
expect(onError).toHaveBeenCalled()
|
|
626
|
+
expect(deviceManager.currentDevice).toBe(null)
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
it("should use default values when options not provided", async () => {
|
|
630
|
+
const { createMIDIDeviceManager } = await import("../../src/index.js")
|
|
631
|
+
|
|
632
|
+
const deviceManager = await createMIDIDeviceManager()
|
|
633
|
+
|
|
634
|
+
expect(deviceManager.midi.options.channel).toBe(1)
|
|
635
|
+
// sysex option is passed to MIDIConnection, not stored in options
|
|
636
|
+
expect(deviceManager.channel).toBe(1)
|
|
637
|
+
})
|
|
638
|
+
})
|