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(); const outputs = new Map(); 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; }, get outputs() { return outputs as ReadonlyMap; }, 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 `` 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 ( {props.children} ); }