133 lines
4.1 KiB
TypeScript
133 lines
4.1 KiB
TypeScript
/**
|
|
* Fake MIDI fixture — supplies the package's spec-mandated injectable seam.
|
|
* Lives outside `src/` so it does not appear in coverage reports yet remains
|
|
* shared between the unit, integration, and e2e suites.
|
|
*/
|
|
|
|
import type {
|
|
MidiAccessAdapter,
|
|
MidiAccessLike,
|
|
MidiInputLike,
|
|
MidiOutputLike,
|
|
} from "../src/base/midi-controller-plugin.types.js";
|
|
|
|
export interface FakeMidiInput extends MidiInputLike {
|
|
emit(data: ReadonlyArray<number>, timeStamp?: number): void;
|
|
}
|
|
|
|
export interface FakeMidiOutput extends MidiOutputLike {
|
|
readonly sent: number[][];
|
|
disconnect(): void;
|
|
}
|
|
|
|
export interface FakeMidiAccess extends MidiAccessLike {
|
|
readonly mutableInputs: Map<string, FakeMidiInput>;
|
|
readonly mutableOutputs: Map<string, FakeMidiOutput>;
|
|
disconnectPort(port: FakeMidiInput | FakeMidiOutput): void;
|
|
reconnectPort(port: FakeMidiInput | FakeMidiOutput): void;
|
|
}
|
|
|
|
function createInput(id: string, name: string): FakeMidiInput {
|
|
const port: FakeMidiInput = {
|
|
id,
|
|
name,
|
|
state: "connected",
|
|
type: "input",
|
|
onmidimessage: null,
|
|
emit(data: ReadonlyArray<number>, timeStamp = 0) {
|
|
port.onmidimessage?.({
|
|
data: new Uint8Array(data),
|
|
timeStamp,
|
|
});
|
|
},
|
|
} as FakeMidiInput;
|
|
return port;
|
|
}
|
|
|
|
function createOutput(id: string, name: string): FakeMidiOutput {
|
|
const sent: number[][] = [];
|
|
let connectedState: "connected" | "disconnected" = "connected";
|
|
const port = {
|
|
id,
|
|
name,
|
|
get state(): "connected" | "disconnected" {
|
|
return connectedState;
|
|
},
|
|
type: "output" as const,
|
|
sent,
|
|
send(data: ReadonlyArray<number> | Uint8Array) {
|
|
sent.push(Array.isArray(data) ? data.slice() : Array.from(data));
|
|
},
|
|
disconnect() {
|
|
connectedState = "disconnected";
|
|
},
|
|
} satisfies FakeMidiOutput;
|
|
return port;
|
|
}
|
|
|
|
export function createFakeAccess(
|
|
options: { readonly inputName?: string; readonly outputName?: string } = {}
|
|
): FakeMidiAccess {
|
|
const inputName = options.inputName ?? "Demo Input";
|
|
const outputName = options.outputName ?? "Demo Output";
|
|
const input = createInput("in-1", inputName);
|
|
const output = createOutput("out-1", outputName);
|
|
const mutableInputs = new Map<string, FakeMidiInput>([[input.id, input]]);
|
|
const mutableOutputs = new Map<string, FakeMidiOutput>([
|
|
[output.id, output],
|
|
]);
|
|
const access: FakeMidiAccess = {
|
|
get inputs(): ReadonlyMap<string, MidiInputLike> {
|
|
return mutableInputs;
|
|
},
|
|
get outputs(): ReadonlyMap<string, MidiOutputLike> {
|
|
return mutableOutputs;
|
|
},
|
|
mutableInputs,
|
|
mutableOutputs,
|
|
onstatechange: null,
|
|
disconnectPort(port) {
|
|
if (port.type === "input") mutableInputs.delete(port.id);
|
|
else {
|
|
port.disconnect();
|
|
mutableOutputs.delete(port.id);
|
|
}
|
|
access.onstatechange?.({
|
|
port: { ...port, state: "disconnected" },
|
|
});
|
|
},
|
|
reconnectPort(port) {
|
|
if (port.type === "input") mutableInputs.set(port.id, port);
|
|
else mutableOutputs.set(port.id, port);
|
|
access.onstatechange?.({
|
|
port: { ...port, state: "connected" },
|
|
});
|
|
},
|
|
};
|
|
return access;
|
|
}
|
|
|
|
export function createFakeAdapter(
|
|
access: FakeMidiAccess,
|
|
options: { readonly reject?: Error } = {}
|
|
): MidiAccessAdapter {
|
|
return {
|
|
async requestAccess() {
|
|
if (options.reject) throw options.reject;
|
|
return access;
|
|
},
|
|
};
|
|
}
|
|
|
|
export function getFirstInput(access: FakeMidiAccess): FakeMidiInput {
|
|
const first = access.mutableInputs.values().next().value;
|
|
if (!first) throw new Error("no input");
|
|
return first;
|
|
}
|
|
|
|
export function getFirstOutput(access: FakeMidiAccess): FakeMidiOutput {
|
|
const first = access.mutableOutputs.values().next().value;
|
|
if (!first) throw new Error("no output");
|
|
return first;
|
|
}
|