RolandJansen/intermix.js

View on GitHub
src/plugins/Sequencer/__tests__/Sequencer.test.ts

Summary

Maintainability
D
1 day
Test Coverage
/// <reference path="../../../../typings/web-audio-test-api.d.ts" />
import "web-audio-test-api";
import Sequencer from "../Sequencer";
import SeqPart from "../../../seqpart/SeqPart";
import { IntermixNote } from "../../../interfaces/interfaces";
import { IOscActionDef, OscArgSequence } from "../../../interfaces/IActions";
import { IPlugin } from "../../../interfaces/IRegistryItems";
import { createInlineWorker } from "../../../fileLoader";
import { IClockMessage } from "../clock.worker";

// mock dependencies of the module under test
jest.mock("../clock.worker");
jest.mock("../../../fileLoader");

const workerMock = {
    postMessage: jest.fn(),
    onMessage: jest.fn(),
};

(createInlineWorker as jest.Mock).mockReturnValue(workerMock);

// WebAudioTestAPI Config
WebAudioTestAPI.setState({
    "AudioContext#suspend": "enabled",
    "AudioContext#resume": "enabled",
});

const createTestSequencer = (ac: AudioContext): IPlugin => {
    const newSequencer = new Sequencer("abcd", ac);
    newSequencer.actionCreators = {
        position: jest.fn(),
    };
    return newSequencer;
};

describe("Sequencer", () => {
    let ac: AudioContext;
    let sequencer: IPlugin;

    beforeEach(() => {
        ac = new AudioContext();
        sequencer = createTestSequencer(ac);
        workerMock.onMessage.mockClear();
        workerMock.postMessage.mockClear();
        // sequencer.clock.mockClear();
    });

    test("ensure that we're testing against the WebAudioTestAPI", () => {
        expect(ac.$name).toEqual("AudioContext");
    });

    test("has a static metadata section", () => {
        expect(Sequencer.METADATA.type).toEqual("controller");
        expect(Sequencer.METADATA.name).toEqual("Intermix Sequencer");
    });

    test("has action definitions", () => {
        const testActionDef: IOscActionDef = {
            address: "/intermix/plugin/<UID>/position",
            typeTag: ",i",
            description: "jump to a specific step in the score",
        };
        expect(sequencer.actionDefs).toContainEqual(testActionDef);
    });

    test("has an empty list of inputs", () => {
        expect(sequencer.inputs).toHaveLength(0);
    });

    test("has an empty list of outputs", () => {
        expect(sequencer.outputs).toHaveLength(0);
    });

    describe("onChange", () => {
        beforeEach(() => {
            // sequencer.actionCreators["QUEUE"] = (): boolean => true;
        });

        test("returns true when called with a recognized value", () => {
            const expected = sequencer.onChange(["BPM", 0]);
            expect(expected).toBeTruthy();
        });

        test("returns false when called with an unrecognized value", () => {
            const expected = sequencer.onChange(["something-different", true]);
            expect(expected).toBeFalsy();
        });

        test("sets bpm and timePerStep", () => {
            sequencer.onChange(["BPM", 160]);
            expect(sequencer["bpm"]).toEqual(160);
            expect(sequencer["timePerStepInSec"]).toEqual(0.0234375);
        });

        // test("adds a SeqPart object", () => {
        //     const testPart = new SeqPart("abcd");
        //     sequencer.onChange(["ADD_PART", testPart]);
        //     const partList = sequencer["score"]["parts"].getUidList();
        //     expect(partList).toContain("abcd");
        // });

        // test("removes a SeqPart object", () => {
        //     const testPart = new SeqPart("abcd");
        //     const parts = sequencer["score"]["parts"];
        //     parts.add(testPart);

        //     sequencer.onChange(["REMOVE_PART", "abcd"]);
        //     expect(parts.getUidList()).toHaveLength(0);
        // });

        test("puts a seq part on the score", () => {
            const testPart = new SeqPart("abcd");
            const fictivePluginUid = "efgh";
            const testTuple = [testPart.uid, fictivePluginUid];
            // const partID = sequencer["score"]["parts"].add(testPart);
            sequencer.onChange(["addToScore", testTuple]);
            expect(sequencer["score"]["queue"][0]).toContain(testTuple);
        });

        test("removes a seq part from score", () => {
            const testPart = new SeqPart("abcd");
            const fictivePluginUid = "efgh";
            sequencer["score"]["queue"][23] = [[testPart.uid, fictivePluginUid]];
            sequencer["score"].activeStep = 23;
            sequencer.onChange(["removeFromScore", [testPart.uid, fictivePluginUid]]);
            expect(sequencer["score"]["queue"][23]).toHaveLength(0);
        });

        test("sets loopStart", () => {
            sequencer.onChange(["loopStart", 23]);
            expect(sequencer["score"]["loop"].start).toEqual(23);
        });

        test("sets loopEnd", () => {
            sequencer.onChange(["loopEnd", 42]);
            expect(sequencer["score"]["loop"].end).toEqual(42);
        });

        // test("sets the loop interval", () => {
        //     const loop: ILoop = {
        //         start: 23,
        //         end: 42,
        //     };
        //     sequencer.onChange(["LOOP", loop]);
        //     expect(sequencer["score"]["loopStart"]).toEqual(23);
        //     expect(sequencer["score"]["loopEnd"]).toEqual(42);
        // });

        test("activates loop mode", () => {
            sequencer["score"]["loopActive"] = false;
            sequencer.onChange(["loopActive", 1]);
            expect(sequencer["score"]["loopActive"]).toBeTruthy();
        });

        test("deactivates loop mode", () => {
            sequencer["score"]["loopActive"] = true;
            sequencer.onChange(["loopActive", 0]);
            expect(sequencer["score"]["loopActive"]).toBeFalsy();
        });

        test("moves the score pointer to a certain position", () => {
            sequencer.onChange(["position", 23]);
            expect(sequencer["score"]["nextStep"]).toEqual(23);
        });
    });

    describe("playback", () => {
        beforeEach(() => {
            const score = sequencer["score"];
            score.resetScorePointer = jest.fn();
            window.requestAnimationFrame = jest.fn();
        });

        test("starts", () => {
            const msg: IClockMessage = { command: "start" };
            sequencer.onChange(["running", 1]);
            expect(sequencer["clock"].postMessage).toBeCalledWith(msg);
            expect(sequencer["isRunning"]).toBeTruthy();
            expect(window.requestAnimationFrame).toBeCalled();
        });

        test("stops", () => {
            const msg: IClockMessage = { command: "stop" };
            sequencer.onChange(["running", 1]);
            expect(sequencer["isRunning"]).toBeTruthy();

            sequencer.onChange(["running", 0]);
            expect(sequencer["clock"].postMessage).toBeCalledWith(msg);
            expect(sequencer["ac"].state).toMatch("suspended");
            expect(sequencer["isRunning"]).toBeFalsy();
            expect(sequencer.actionCreators.position).toHaveBeenCalled();
        });

        test("doesn't stop if sequencer is not running", () => {
            sequencer.onChange(["running", 0]);
            expect(sequencer["ac"].state).toMatch("running");
        });

        test("reactivates audio context when restarted", () => {
            sequencer.onChange(["running", 1]);
            sequencer.onChange(["running", 0]);
            expect(sequencer["ac"].state).toMatch("suspended");
            sequencer.onChange(["running", 1]);
            expect(sequencer["ac"].state).toMatch("running");
        });

        test("resets (stop and reset queue pointer)", () => {
            const msg: IClockMessage = { command: "stop" };
            sequencer.onChange(["reset", 1]);
            expect(sequencer["clock"].postMessage).toBeCalledWith(msg);
            expect(sequencer["isRunning"]).toBeFalsy();
            // expect(sequencer["nextStep"]).toEqual(0);
            expect(sequencer["score"].resetScorePointer).toHaveBeenCalled();
        });
    });

    describe("scheduler", () => {
        // const action1: IAction = {
        //     listener: "abcd",
        //     type: "SOME_TYPE",
        //     payload: 23,
        // };
        // const action2: IAction = {
        //     listener: "efgh",
        //     type: "SOME_TYPE",
        //     payload: 42,
        // };
        // let part: SeqPart;

        const partId = "abcd";
        const pluginId = "efgh";
        const note1: IntermixNote = ["note", 23, 1, 1, 0];
        const note2: IntermixNote = ["note", 42, 1, 1, 0];

        beforeEach(() => {
            const noteVal1 = Array.from(note1);
            const noteVal2 = Array.from(note2);
            const pattern = Array(64).fill([]);
            pattern[0].push(noteVal1);
            pattern[1].push(noteVal2);
            sequencer["getGlobalState"] = jest.fn();
            (sequencer["getGlobalState"] as jest.Mock).mockReturnValue({
                abcd: {
                    stepsPerBar: 0,
                    stepMultiplier: 0,
                    pattern,
                },
            });

            // part = new SeqPart("abcd");
            // part.addAction(action1, 0);
            // part.addAction(action2, 1);

            // const partId = sequencer["score"].parts.add(part);
            sequencer["score"].addPartToScore([partId, pluginId]);
            sequencer["score"].increaseScorePointer = jest.fn();
            sequencer["score"]["addPatternToRunqueue"] = jest.fn();
            sequencer["sendAllActionsInNextStep"] = jest.fn();
            sequencer["ac"].$processTo("00:01.000");
            sequencer["scheduler"]();
        });

        afterEach(() => {
            sequencer["ac"].$processTo("00:00.000");
        });

        test("runs until all steps in lookahead are processed", () => {
            // runs 10 times with lookahead=0.3 and bpm=120
            expect(sequencer["score"].increaseScorePointer).toHaveBeenCalledTimes(10);
        });

        test("adds parts to runqueue", () => {
            expect(sequencer["score"]["addPatternToRunqueue"]).toHaveBeenCalled();
        });

        test("increases nextStepTime on every step", () => {
            const expected = 1.3125; // 10 x timePerStep + timestamp
            expect(sequencer["nextStepTimeInSec"]).toEqual(expected);
        });

        test("fires all actions", () => {
            expect(sequencer["sendAllActionsInNextStep"]).toHaveBeenCalled();
        });
    });

    describe("process Actions from queue", () => {
        // probably more mocks needed. we're testing against
        // real score object (which effects other tests, too)

        // const action1: IAction = {
        //     listener: "abcd",
        //     type: "SOME_TYPE",
        //     payload: 23,
        // };
        // const action2: IAction = {
        //     listener: "efgh",
        //     type: "SOME_TYPE",
        //     payload: 42,
        // };
        // const brokenPayload = {
        //     value: 23,
        //     velocity: 1,
        //     steps: 4,
        // };
        // const sanePayload = Object.assign({}, brokenPayload, { duration: 0 });
        // const brokenNoteAction: IAction = {
        //     listener: "ijkl",
        //     type: "NOTE",
        //     payload: brokenPayload,
        // };
        // const saneNoteAction = {
        //     listener: "ijkl",
        //     type: "NOTE",
        //     payload: sanePayload,
        // };
        // let part: SeqPart;

        // same as "scheduler" tests
        const partId = "abcd";
        const pluginId = "efgh";
        const note1: IntermixNote = ["note", 23, 1, 1, 0];
        const note2: IntermixNote = ["note", 42, 1, 1, 0];

        beforeEach(() => {
            // const subArray: OscArgSequence[] = [];
            const pattern = [];

            for (let i = 0; i < 64; i++) {
                const subArray: OscArgSequence[] = [];
                pattern[i] = subArray;
            }

            const noteVal1 = Array.from(note1);
            const noteVal2 = Array.from(note2);
            const saneNote = Array.from(note1);
            const brokenNote = ["note", 5, 1, 1, "hello"];
            pattern[0].push(noteVal1);
            pattern[1].push(noteVal2);
            pattern[2].push(saneNote);
            pattern[2].push(brokenNote);

            sequencer["getGlobalState"] = jest.fn();
            (sequencer["getGlobalState"] as jest.Mock).mockReturnValue({
                abcd: {
                    pattern,
                },
            });

            // part = new SeqPart("abcd");
            // part.addAction(action1, 0);
            // part.addAction(action2, 1);
            // part.addAction(saneNoteAction, 2);
            // part.addAction(brokenNoteAction, 2);

            // const partId = sequencer["score"].parts.add(part);
            sequencer["score"].addPartToScore([partId, pluginId]);
            sequencer.sendAction = jest.fn();

            sequencer["ac"].$processTo("00:01.000");
            sequencer["scheduler"]();
        });

        afterEach(() => {
            sequencer["ac"].$processTo("00:00.000");
            (sequencer.sendAction as jest.Mock).mockClear(); // reset call history
        });

        test("sends all actions from the score to dispatch", () => {
            expect(sequencer.sendAction).toHaveBeenCalledTimes(4);
        });

        test("adds delay to the actions payload", () => {
            const sendAction = sequencer.sendAction as jest.Mock;
            const delayedAction1 = sendAction.mock.calls[0][0];
            const delayedAction2 = sendAction.mock.calls[1][0];

            expect(delayedAction1.payload[delayedAction1.payload.length - 1]).toEqual(1);
            expect(delayedAction2.payload[delayedAction1.payload.length - 1]).toEqual(1.03125);
        });

        test("adds delay to payload of NOTE actions", () => {
            const sendAction = sequencer.sendAction as jest.Mock;
            const delayedNote = sendAction.mock.calls[2][0];
            expect(delayedNote.payload[delayedNote.payload.length - 1]).toEqual(1.0625);
        });
        test("changes duration for NOTE actions", () => {
            const sendAction = sequencer.sendAction as jest.Mock;
            const delayedNoteAction = sendAction.mock.calls[2][0];

            expect(delayedNoteAction.payload.length - 2).toEqual(3);
        });

        test("adds duration on NOTE actions if its not defined", () => {
            const sendAction = sequencer.sendAction as jest.Mock;
            const delayedNoteAction = sendAction.mock.calls[3][0];

            expect(delayedNoteAction.payload.length - 2).toEqual(3);
        });
    });

    // describe(".draw", function () {

    //     beforeEach(function () {
    //         spyOn(sequencer, "updateFrame");
    //     });

    //     describe("if sequencer is running", function () {

    //         beforeEach(function () {
    //             var step = sequencer.getMasterQueuePosition(0, sequencer.ac.currentTime + 0.1);
    //             sequencer.isRunning = true;
    //             sequencer.stepList.push(step);
    //             // sequencer.ac.$processTo('00:00.050');
    //         });

    //         describe("but is within start delay", function () {

    //             beforeEach(function () {
    //                 sequencer.draw();
    //             });

    //             it("calls window.requestAnimationFrame", function () {
    //                 expect(window.requestAnimationFrame).toHaveBeenCalled();
    //             });

    //             it("doesn't call .updateFrame", function () {
    //                 expect(sequencer.updateFrame).not.toHaveBeenCalled();
    //             });

    //         });

    //         describe("and has passed start delay", function () {

    //             beforeEach(function () {
    //                 sequencer.ac.$processTo("00:00.200");
    //                 sequencer.draw();
    //             });

    //             it("calls window.requestAnimationFrame", function () {
    //                 expect(window.requestAnimationFrame).toHaveBeenCalledTimes(1);
    //             });

    //             it("calls .updateFrame", function () {
    //                 expect(sequencer.updateFrame).toHaveBeenCalledWith(0);
    //             });

    //             it("removes first element in stepList", function () {
    //                 expect(sequencer.stepList.length).toEqual(0);
    //             });

    //         });

    //     });

    //     describe("if sequencer is not running", function () {

    //         beforeEach(function () {
    //             sequencer.isRunning = false;
    //         });

    //         it("doesn't call .updateFrame", function () {
    //             expect(sequencer.updateFrame).not.toHaveBeenCalled();
    //         });

    //         it("doesn't call window.requestAnimationFrame", function () {
    //             expect(window.requestAnimationFrame).not.toHaveBeenCalled();
    //         });

    //     });

    // });

    // describe(".updateFrame", function () {

    //     it("is a function", function () {
    //         expect(typeof sequencer.updateFrame).toBe("function");
    //     });

    // });
});