Skip to content

Commit dc9fead

Browse files
authored
Add regression tests for core lifecycle and pitch execution in turtle-singer
2 parents 220b3c2 + be7caaa commit dc9fead

File tree

1 file changed

+155
-0
lines changed

1 file changed

+155
-0
lines changed

js/__tests__/turtle-singer.test.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ global.numberToPitch = mockGlobals.numberToPitch;
3838
global.pitchToNumber = mockGlobals.pitchToNumber;
3939
global.last = jest.fn(array => array[array.length - 1]);
4040

41+
global.SEMITONES = 12;
42+
global.pitchToFrequency = jest.fn().mockReturnValue(440);
43+
4144
const createTurtleMock = () => ({
4245
turtles: [],
4346
singer: null,
@@ -684,3 +687,155 @@ describe("Musical state mutability", () => {
684687
expect(singer.duplicateFactor).toBe(3);
685688
});
686689
});
690+
691+
describe("killAllVoices lifecycle safety", () => {
692+
let singer;
693+
694+
beforeEach(() => {
695+
const turtleMock = createTurtleMock();
696+
singer = new Singer(turtleMock);
697+
});
698+
699+
test("should stop, release, or disconnect all active voices and clear set", () => {
700+
const stopMock = jest.fn();
701+
const releaseMock = jest.fn();
702+
const disconnectMock = jest.fn();
703+
704+
const voice1 = { stop: stopMock };
705+
const voice2 = { releaseAll: releaseMock };
706+
const voice3 = { disconnect: disconnectMock };
707+
708+
singer.activeVoices.add(voice1);
709+
singer.activeVoices.add(voice2);
710+
singer.activeVoices.add(voice3);
711+
712+
singer.killAllVoices();
713+
714+
expect(stopMock).toHaveBeenCalled();
715+
expect(releaseMock).toHaveBeenCalled();
716+
expect(disconnectMock).toHaveBeenCalled();
717+
expect(singer.activeVoices.size).toBe(0);
718+
});
719+
720+
test("should ignore errors from already stopped nodes", () => {
721+
const voice = {
722+
stop: jest.fn(() => {
723+
throw new Error("already stopped");
724+
})
725+
};
726+
727+
singer.activeVoices.add(voice);
728+
729+
expect(() => singer.killAllVoices()).not.toThrow();
730+
expect(singer.activeVoices.size).toBe(0);
731+
});
732+
});
733+
734+
describe("numberOfNotes — state restoration and tally logic", () => {
735+
let turtleMock;
736+
let activityMock;
737+
let logoMock;
738+
739+
beforeEach(() => {
740+
turtleMock = createTurtleMock();
741+
turtleMock.singer = new Singer(turtleMock);
742+
743+
// Extend minimal state
744+
turtleMock.x = 10;
745+
turtleMock.y = 20;
746+
turtleMock.orientation = 90;
747+
turtleMock.endOfClampSignals = {};
748+
turtleMock.butNotThese = {};
749+
turtleMock.running = false;
750+
turtleMock.painter = {
751+
color: "red",
752+
value: 1,
753+
chroma: 2,
754+
stroke: 3,
755+
canvasAlpha: 1,
756+
penState: true,
757+
doPenUp: jest.fn(),
758+
doSetXY: jest.fn(),
759+
doSetHeading: jest.fn()
760+
};
761+
762+
activityMock = {
763+
turtles: {
764+
ithTurtle: jest.fn().mockReturnValue(turtleMock),
765+
getTurtle: jest.fn().mockReturnValue({ queue: [] }),
766+
turtleList: [turtleMock]
767+
},
768+
logo: {
769+
runFromBlockNow: jest.fn((logo, turtle) => {
770+
const tur = turtleMock;
771+
tur.singer.tallyNotes += 5;
772+
}),
773+
boxes: {},
774+
turtleHeaps: { 0: {} },
775+
turtleDicts: { 0: {} }
776+
}
777+
};
778+
779+
logoMock = {
780+
activity: activityMock,
781+
boxes: {},
782+
turtleHeaps: { 0: {} },
783+
turtleDicts: { 0: {} }
784+
};
785+
});
786+
787+
test("should return tally difference and restore state", () => {
788+
turtleMock.singer.tallyNotes = 2;
789+
790+
const result = Singer.numberOfNotes(logoMock, 0, 123);
791+
792+
expect(result).toBe(5);
793+
expect(turtleMock.singer.tallyNotes).toBe(2);
794+
expect(turtleMock.painter.doPenUp).toHaveBeenCalled();
795+
});
796+
});
797+
798+
describe("processPitch — note block execution path", () => {
799+
let turtleMock;
800+
let activityMock;
801+
802+
beforeEach(() => {
803+
turtleMock = createTurtleMock();
804+
turtleMock.singer = new Singer(turtleMock);
805+
806+
turtleMock.singer.inNoteBlock = [0];
807+
turtleMock.singer.notePitches = { 0: [] };
808+
turtleMock.singer.noteOctaves = { 0: [] };
809+
turtleMock.singer.noteCents = { 0: [] };
810+
turtleMock.singer.noteHertz = { 0: [] };
811+
turtleMock.singer.noteBeatValues = { 0: [] };
812+
813+
turtleMock.singer.beatFactor = 1;
814+
turtleMock.singer.transposition = 0;
815+
turtleMock.singer.register = 0;
816+
turtleMock.singer.invertList = [];
817+
turtleMock.singer.intervals = [];
818+
turtleMock.singer.semitoneIntervals = [];
819+
turtleMock.singer.chordIntervals = [];
820+
turtleMock.singer.ratioIntervals = [];
821+
822+
activityMock = {
823+
turtles: {
824+
ithTurtle: jest.fn().mockReturnValue(turtleMock)
825+
},
826+
logo: {
827+
synth: { inTemperament: false },
828+
clearNoteParams: jest.fn()
829+
}
830+
};
831+
});
832+
833+
test("should push pitch data and mark note as pushed", () => {
834+
Singer.processPitch(activityMock, "C", 4, 0, 0, 123);
835+
836+
expect(turtleMock.singer.notePitches[0].length).toBe(1);
837+
expect(turtleMock.singer.noteOctaves[0].length).toBe(1);
838+
expect(turtleMock.singer.noteBeatValues[0].length).toBe(1);
839+
expect(turtleMock.singer.pushedNote).toBe(true);
840+
});
841+
});

0 commit comments

Comments
 (0)