@@ -38,6 +38,9 @@ global.numberToPitch = mockGlobals.numberToPitch;
3838global . pitchToNumber = mockGlobals . pitchToNumber ;
3939global . last = jest . fn ( array => array [ array . length - 1 ] ) ;
4040
41+ global . SEMITONES = 12 ;
42+ global . pitchToFrequency = jest . fn ( ) . mockReturnValue ( 440 ) ;
43+
4144const 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