Skip to content

Commit 12e8e01

Browse files
authored
Add unit tests for RhythmActions (#4909)
* Added unit tests for RhythmActions and export setupRhythmActions * changed the author name * chore: format RhythmActions files with Prettier
1 parent 7ebfc7a commit 12e8e01

File tree

2 files changed

+300
-0
lines changed

2 files changed

+300
-0
lines changed

js/turtleactions/RhythmActions.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,3 +458,7 @@ function setupRhythmActions(activity) {
458458
}
459459
};
460460
}
461+
462+
if (typeof module !== "undefined" && module.exports) {
463+
module.exports = setupRhythmActions;
464+
}
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
/**
2+
* @license
3+
* MusicBlocks v3.4.1
4+
* Copyright (C) 2025 Shreya Saxena
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
const setupRhythmActions = require("../RhythmActions");
21+
22+
describe("setupRhythmActions", () => {
23+
let activity;
24+
let targetTurtle;
25+
26+
beforeAll(() => {
27+
global._ = msg => msg;
28+
global.last = arr => arr[arr.length - 1];
29+
global.TONEBPM = 120;
30+
31+
global.Singer = {
32+
processNote: jest.fn()
33+
};
34+
35+
global.MusicBlocks = { isRun: false };
36+
global.Mouse = {
37+
getMouseFromTurtle: jest.fn(() => ({ MB: { listeners: [] } }))
38+
};
39+
});
40+
41+
beforeEach(() => {
42+
targetTurtle = {
43+
id: 0,
44+
singer: {
45+
inNoteBlock: [],
46+
notesPlayed: [0, 1],
47+
pickup: 0.5,
48+
noteValuePerBeat: 1,
49+
beatsPerMeasure: 4,
50+
beatList: [],
51+
factorList: [],
52+
beatFactor: 1,
53+
currentBeat: null,
54+
currentMeasure: null,
55+
56+
// below fields are touched later in function
57+
noteValue: {},
58+
multipleVoices: false,
59+
inNeighbor: [],
60+
neighborArgBeat: [],
61+
neighborArgCurrentBeat: [],
62+
oscList: {},
63+
noteBeat: {},
64+
noteBeatValues: {},
65+
notePitches: {},
66+
noteOctaves: {},
67+
noteCents: {},
68+
noteHertz: {},
69+
noteDrums: {},
70+
embeddedGraphics: {}
71+
}
72+
};
73+
74+
activity = {
75+
turtles: {
76+
ithTurtle: jest.fn(() => targetTurtle)
77+
},
78+
blocks: {
79+
blockList: {}
80+
},
81+
stage: {
82+
dispatchEvent: jest.fn()
83+
},
84+
logo: {
85+
clearNoteParams: jest.fn(),
86+
setDispatchBlock: jest.fn(),
87+
setTurtleListener: jest.fn(),
88+
notation: {
89+
notationVoices: jest.fn()
90+
},
91+
pitchBlocks: [],
92+
drumBlocks: []
93+
}
94+
};
95+
96+
setupRhythmActions(activity);
97+
});
98+
99+
it("sets beat and measure to 0 when pickup not crossed", () => {
100+
const enqueue = jest.fn();
101+
102+
Singer.RhythmActions.playNote(1, "note", 0, 1, enqueue);
103+
104+
expect(targetTurtle.singer.currentBeat).toBe(0);
105+
expect(targetTurtle.singer.currentMeasure).toBe(0);
106+
});
107+
108+
it("sets correct beat and measure when pickup is crossed", () => {
109+
const enqueue = jest.fn();
110+
111+
targetTurtle.singer.notesPlayed = [2, 1]; // pickup crossed
112+
targetTurtle.singer.pickup = 0;
113+
targetTurtle.singer.noteValuePerBeat = 1;
114+
targetTurtle.singer.beatsPerMeasure = 4;
115+
targetTurtle.singer.beatList = [];
116+
targetTurtle.singer.factorList = [];
117+
118+
Singer.RhythmActions.playNote(1, "note", 0, 1, enqueue);
119+
120+
expect(targetTurtle.singer.currentBeat).toBe(3);
121+
expect(targetTurtle.singer.currentMeasure).toBe(1);
122+
});
123+
124+
it("triggers everybeat event when beatList contains 'everybeat'", () => {
125+
const enqueue = jest.fn();
126+
127+
targetTurtle.singer.notesPlayed = [1, 1]; // pickup crossed
128+
targetTurtle.singer.pickup = 0;
129+
targetTurtle.singer.beatList = ["everybeat"];
130+
targetTurtle.singer.factorList = [];
131+
132+
Singer.RhythmActions.playNote(1, "note", 0, 1, enqueue);
133+
134+
expect(enqueue).toHaveBeenCalled();
135+
expect(activity.stage.dispatchEvent).toHaveBeenCalledWith("__everybeat_0__");
136+
});
137+
138+
it("triggers specific beat event when beatList contains current beat", () => {
139+
const enqueue = jest.fn();
140+
141+
// setup so beat = 2
142+
targetTurtle.singer.notesPlayed = [1, 1]; // 1 beat played
143+
targetTurtle.singer.pickup = 0;
144+
targetTurtle.singer.noteValuePerBeat = 1;
145+
targetTurtle.singer.beatsPerMeasure = 4;
146+
147+
targetTurtle.singer.beatList = [2];
148+
targetTurtle.singer.factorList = [];
149+
150+
Singer.RhythmActions.playNote(1, "note", 0, 1, enqueue);
151+
152+
expect(targetTurtle.singer.currentBeat).toBe(2);
153+
expect(enqueue).toHaveBeenCalled();
154+
expect(activity.stage.dispatchEvent).toHaveBeenCalledWith("__beat_2_0__");
155+
});
156+
157+
it("triggers offbeat event when beatList contains 'offbeat' and beat > 1", () => {
158+
const enqueue = jest.fn();
159+
160+
// make beat = 2
161+
targetTurtle.singer.notesPlayed = [1, 1]; // beat = 1 → beatValue = 2
162+
targetTurtle.singer.pickup = 0;
163+
targetTurtle.singer.noteValuePerBeat = 1;
164+
targetTurtle.singer.beatsPerMeasure = 4;
165+
166+
targetTurtle.singer.beatList = ["offbeat"];
167+
targetTurtle.singer.factorList = [];
168+
169+
Singer.RhythmActions.playNote(1, "note", 0, 1, enqueue);
170+
171+
expect(targetTurtle.singer.currentBeat).toBe(2);
172+
expect(enqueue).toHaveBeenCalled();
173+
expect(activity.stage.dispatchEvent).toHaveBeenCalledWith("__offbeat_0__");
174+
});
175+
it("triggers factorList beat event when beat matches factor", () => {
176+
const enqueue = jest.fn();
177+
178+
// beat = 2
179+
targetTurtle.singer.notesPlayed = [1, 1]; // beatValue = 2
180+
targetTurtle.singer.pickup = 0;
181+
targetTurtle.singer.noteValuePerBeat = 1;
182+
targetTurtle.singer.beatsPerMeasure = 4;
183+
184+
targetTurtle.singer.beatList = [];
185+
targetTurtle.singer.factorList = [2];
186+
187+
Singer.RhythmActions.playNote(1, "note", 0, 1, enqueue);
188+
189+
expect(enqueue).toHaveBeenCalled();
190+
expect(activity.stage.dispatchEvent).toHaveBeenCalledWith("__beat_2_0__");
191+
});
192+
it("adds rest note when inside a note block", () => {
193+
// setup: one active note block
194+
targetTurtle.singer.inNoteBlock = [1];
195+
targetTurtle.singer.beatFactor = 1;
196+
197+
targetTurtle.singer.notePitches[1] = [];
198+
targetTurtle.singer.noteOctaves[1] = [];
199+
targetTurtle.singer.noteCents[1] = [];
200+
targetTurtle.singer.noteHertz[1] = [];
201+
targetTurtle.singer.noteBeatValues[1] = [];
202+
203+
Singer.RhythmActions.playRest(0);
204+
205+
expect(targetTurtle.singer.notePitches[1]).toContain("rest");
206+
expect(targetTurtle.singer.noteOctaves[1]).toContain(4);
207+
expect(targetTurtle.singer.noteCents[1]).toContain(0);
208+
expect(targetTurtle.singer.noteHertz[1]).toContain(0);
209+
expect(targetTurtle.singer.noteBeatValues[1]).toContain(1);
210+
expect(targetTurtle.singer.pushedNote).toBe(true);
211+
});
212+
it("updates dotCount and beatFactor for valid dot value", () => {
213+
targetTurtle.singer.dotCount = 0;
214+
targetTurtle.singer.beatFactor = 1;
215+
216+
Singer.RhythmActions.doRhythmicDot(1, 0, 1);
217+
218+
expect(targetTurtle.singer.dotCount).toBe(1);
219+
expect(targetTurtle.singer.beatFactor).not.toBe(1);
220+
});
221+
222+
it("shows error when dot value is -1", () => {
223+
targetTurtle.singer.dotCount = 0;
224+
targetTurtle.singer.beatFactor = 1;
225+
226+
activity.errorMsg = jest.fn();
227+
228+
Singer.RhythmActions.doRhythmicDot(-1, 0, 1);
229+
230+
expect(activity.errorMsg).toHaveBeenCalledWith(
231+
"An argument of -1 results in a note value of 0.",
232+
1
233+
);
234+
expect(targetTurtle.singer.dotCount).toBe(0);
235+
});
236+
237+
it("multiplies beatFactor correctly", () => {
238+
targetTurtle.singer.beatFactor = 2;
239+
240+
Singer.RhythmActions.multiplyNoteValue(2, 0, 1);
241+
242+
expect(targetTurtle.singer.beatFactor).toBe(1);
243+
});
244+
245+
it("adds swing when not suppressed", () => {
246+
targetTurtle.singer.suppressOutput = false;
247+
targetTurtle.singer.swing = [];
248+
targetTurtle.singer.swingTarget = [];
249+
250+
Singer.RhythmActions.addSwing(2, 4, 0, 1);
251+
252+
expect(targetTurtle.singer.swing).toContain(0.5);
253+
expect(targetTurtle.singer.swingTarget).toContain(0.25);
254+
});
255+
it("removes swing on listener execution", () => {
256+
targetTurtle.singer.suppressOutput = false;
257+
targetTurtle.singer.swing = [];
258+
targetTurtle.singer.swingTarget = [];
259+
260+
let listener;
261+
activity.logo.setTurtleListener = jest.fn((_, __, cb) => {
262+
listener = cb;
263+
});
264+
265+
Singer.RhythmActions.addSwing(2, 4, 0, 1);
266+
listener();
267+
268+
expect(targetTurtle.singer.swing.length).toBe(0);
269+
expect(targetTurtle.singer.swingTarget.length).toBe(0);
270+
});
271+
272+
it("returns note value from active note", () => {
273+
targetTurtle.singer.inNoteBlock = [1];
274+
targetTurtle.singer.noteValue = { 1: 0.25 };
275+
276+
const value = Singer.RhythmActions.getNoteValue(0);
277+
278+
expect(value).toBe(0.25);
279+
});
280+
it("falls back to lastNotePlayed when no active note", () => {
281+
targetTurtle.singer.inNoteBlock = [];
282+
targetTurtle.singer.lastNotePlayed = [null, 8];
283+
284+
const value = Singer.RhythmActions.getNoteValue(0);
285+
286+
expect(value).toBe(0.125);
287+
});
288+
it("returns 0 when no note info exists", () => {
289+
targetTurtle.singer.inNoteBlock = [];
290+
targetTurtle.singer.lastNotePlayed = null;
291+
292+
const value = Singer.RhythmActions.getNoteValue(0);
293+
294+
expect(value).toBe(0);
295+
});
296+
});

0 commit comments

Comments
 (0)