137 lines
4.0 KiB
TypeScript
137 lines
4.0 KiB
TypeScript
import { type ReactNode, useMemo } from "react";
|
|
import type {
|
|
WebMIDIAccess,
|
|
WebMIDIConnectionEvent,
|
|
WebMIDIInput,
|
|
WebMIDIMessageEvent,
|
|
WebMIDIOutput,
|
|
WebMIDIRequestFn,
|
|
} from "./port/web-midi.types.js";
|
|
import { MidiRequestContext } from "./provider/midi-context.js";
|
|
import { MidiProvider } from "./provider/midi-provider.js";
|
|
|
|
export interface FakeInput extends WebMIDIInput {
|
|
fire(bytes: number[]): void;
|
|
}
|
|
|
|
export interface FakeOutput extends WebMIDIOutput {
|
|
readonly sent: number[][];
|
|
}
|
|
|
|
export interface FakeAccess extends WebMIDIAccess {
|
|
addInput(input: FakeInput): void;
|
|
addOutput(output: FakeOutput): void;
|
|
removeInput(id: string): void;
|
|
}
|
|
|
|
export function makeFakeInput(id: string, name = `input-${id}`): FakeInput {
|
|
let handler: ((ev: WebMIDIMessageEvent) => void) | null = null;
|
|
return {
|
|
id,
|
|
name,
|
|
manufacturer: "Suaveplan",
|
|
state: "connected",
|
|
type: "input",
|
|
get onmidimessage() {
|
|
return handler;
|
|
},
|
|
set onmidimessage(fn) {
|
|
handler = fn;
|
|
},
|
|
fire(bytes) {
|
|
handler?.({ data: new Uint8Array(bytes), timeStamp: 0 });
|
|
},
|
|
};
|
|
}
|
|
|
|
export function makeFakeOutput(id: string, name = `output-${id}`): FakeOutput {
|
|
const sent: number[][] = [];
|
|
return {
|
|
id,
|
|
name,
|
|
manufacturer: "Suaveplan",
|
|
state: "connected",
|
|
type: "output",
|
|
send(bytes) {
|
|
sent.push(Array.from(bytes));
|
|
},
|
|
sent,
|
|
};
|
|
}
|
|
|
|
export function makeFakeAccess(opts?: {
|
|
inputs?: FakeInput[];
|
|
outputs?: FakeOutput[];
|
|
sysex?: boolean;
|
|
}): FakeAccess {
|
|
const inputs = new Map<string, FakeInput>();
|
|
const outputs = new Map<string, FakeOutput>();
|
|
let stateChange: ((ev: WebMIDIConnectionEvent) => void) | null = null;
|
|
for (const i of opts?.inputs ?? []) inputs.set(i.id, i);
|
|
for (const o of opts?.outputs ?? []) outputs.set(o.id, o);
|
|
const access: FakeAccess = {
|
|
get inputs() {
|
|
return inputs as ReadonlyMap<string, WebMIDIInput>;
|
|
},
|
|
get outputs() {
|
|
return outputs as ReadonlyMap<string, WebMIDIOutput>;
|
|
},
|
|
sysexEnabled: opts?.sysex ?? false,
|
|
get onstatechange() {
|
|
return stateChange;
|
|
},
|
|
set onstatechange(fn) {
|
|
stateChange = fn;
|
|
},
|
|
addInput(input) {
|
|
inputs.set(input.id, input);
|
|
stateChange?.({ port: input });
|
|
},
|
|
addOutput(output) {
|
|
outputs.set(output.id, output);
|
|
stateChange?.({ port: output });
|
|
},
|
|
removeInput(id) {
|
|
const port = inputs.get(id);
|
|
if (port !== undefined) {
|
|
inputs.delete(id);
|
|
stateChange?.({
|
|
port: { ...port, state: "disconnected" },
|
|
});
|
|
}
|
|
},
|
|
};
|
|
return access;
|
|
}
|
|
|
|
export interface FakeProviderProps {
|
|
readonly access: FakeAccess;
|
|
readonly requestImpl?: WebMIDIRequestFn;
|
|
readonly sysex?: boolean;
|
|
readonly children?: ReactNode;
|
|
readonly requestSpy?: (opts?: { sysex?: boolean }) => void;
|
|
}
|
|
|
|
/**
|
|
* Renders `<MidiProvider>` with a deterministic fake access injected
|
|
* through `MidiRequestContext`. The `requestSpy` callback is invoked
|
|
* once per request, allowing tests to assert call counts (e.g.
|
|
* exactly-once under StrictMode).
|
|
*/
|
|
export function FakeMidiProvider(props: FakeProviderProps): ReactNode {
|
|
const request: WebMIDIRequestFn = useMemo(() => {
|
|
if (props.requestImpl !== undefined) {
|
|
return props.requestImpl;
|
|
}
|
|
return (opts) => {
|
|
props.requestSpy?.(opts);
|
|
return Promise.resolve(props.access);
|
|
};
|
|
}, [props.access, props.requestImpl, props.requestSpy]);
|
|
return (
|
|
<MidiRequestContext.Provider value={request}>
|
|
<MidiProvider sysex={props.sysex}>{props.children}</MidiProvider>
|
|
</MidiRequestContext.Provider>
|
|
);
|
|
}
|