Skip to content

Commit 4161f1b

Browse files
authored
added test to SensorsBlocks.js (#4565)
1 parent b9494a5 commit 4161f1b

File tree

2 files changed

+360
-0
lines changed

2 files changed

+360
-0
lines changed

js/blocks/SensorsBlocks.js

+5
Original file line numberDiff line numberDiff line change
@@ -1057,3 +1057,8 @@ function setupSensorsBlocks(activity) {
10571057
new MouseYBlock().setup(activity);
10581058
new MouseXBlock().setup(activity);
10591059
}
1060+
1061+
1062+
if (typeof module !== "undefined" && module.exports) {
1063+
module.exports = { setupSensorsBlocks };
1064+
}
+355
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
/**
2+
* MusicBlocks v3.6.2
3+
*
4+
* @author Alok Dangre
5+
*
6+
* @copyright 2025 Alok Dangre
7+
*
8+
* @license
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Affero General Public License as published by
11+
* the Free Software Foundation, either version 3 of the License, or
12+
* (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU Affero General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Affero General Public License
20+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
21+
*/
22+
23+
const { setupSensorsBlocks } = require("../SensorsBlocks");
24+
25+
// --- Dummy Base Classes ---
26+
27+
class DummyFlowBlock {
28+
constructor(name, displayName) {
29+
this.name = name;
30+
this.displayName = displayName;
31+
DummyFlowBlock.createdBlocks[name] = this;
32+
}
33+
setPalette(palette, activity) {
34+
this.palette = palette;
35+
return this;
36+
}
37+
beginnerBlock(flag) {
38+
this.isBeginner = flag;
39+
return this;
40+
}
41+
setHelpString(helpArray) {
42+
this.help = helpArray;
43+
return this;
44+
}
45+
formBlock(params) {
46+
this.blockParams = params;
47+
return this;
48+
}
49+
makeMacro(macroFunc) {
50+
this.macro = macroFunc;
51+
return this;
52+
}
53+
setup(activity) {
54+
// In production, this registers the block.
55+
return this;
56+
}
57+
}
58+
DummyFlowBlock.createdBlocks = {};
59+
60+
class DummyValueBlock extends DummyFlowBlock {}
61+
class DummyLeftBlock extends DummyFlowBlock {}
62+
class DummyBooleanSensorBlock extends DummyValueBlock {}
63+
64+
// Expose dummy classes globally.
65+
global.FlowBlock = DummyFlowBlock;
66+
global.ValueBlock = DummyValueBlock;
67+
global.LeftBlock = DummyLeftBlock;
68+
global.BooleanSensorBlock = DummyBooleanSensorBlock;
69+
70+
// --- Persistent Dummy DOM Elements ---
71+
72+
// Create a persistent store for dummy elements.
73+
const documentElements = {
74+
labelDiv: {
75+
innerHTML: "",
76+
classList: { add: jest.fn(), remove: jest.fn() },
77+
style: {},
78+
addEventListener: jest.fn()
79+
},
80+
textLabel: {
81+
value: "",
82+
placeholder: "",
83+
style: {},
84+
focus: jest.fn(),
85+
blur: jest.fn(),
86+
addEventListener: jest.fn()
87+
},
88+
overlayCanvas: {
89+
getContext: jest.fn(() => ({
90+
getImageData: jest.fn((x, y, w, h) => {
91+
return { data: [100, 150, 200, 255] };
92+
})
93+
}))
94+
}
95+
};
96+
97+
global.docById = function (id) {
98+
return documentElements[id] || null;
99+
};
100+
101+
// --- Dummy Utility Functions ---
102+
103+
global.toFixed2 = function (value) {
104+
return Number(value).toFixed(2);
105+
};
106+
107+
global.hex2rgb = function (hex) {
108+
// Dummy conversion: simply return a fixed rgb string.
109+
return "rgb(100,150,200)";
110+
};
111+
112+
global.searchColors = function (r, g, b) {
113+
return `rgb(${r},${g},${b})`;
114+
};
115+
116+
global.Tone = {
117+
Analyser: class {
118+
constructor(options) {
119+
this.type = options.type;
120+
this.size = options.size;
121+
this.smoothing = options.smoothing;
122+
this.sampleTime = 0.01;
123+
}
124+
getValue() {
125+
return Array(this.size).fill(-100).map((val, i) => val + i);
126+
}
127+
}
128+
};
129+
130+
global.platformColor = {
131+
background: "rgb(200,200,200)"
132+
};
133+
134+
global._THIS_IS_MUSIC_BLOCKS_ = true;
135+
136+
// --- Dummy Stage Functions ---
137+
138+
function dummyStageX() {
139+
return 123;
140+
}
141+
function dummyStageY() {
142+
return 456;
143+
}
144+
function dummyStageMouseDown() {
145+
return true;
146+
}
147+
function dummyCurrentKeyCode() {
148+
return 65;
149+
}
150+
function dummyClearCurrentKeyCode() {
151+
// For testing, do nothing.
152+
}
153+
154+
// --- Dummy Turtle ---
155+
156+
const dummyTurtle = {
157+
id: "t1",
158+
container: { x: 50, y: 100, visible: true },
159+
painter: { canvasColor: "rgb(255,0,0)" },
160+
doWait: jest.fn()
161+
};
162+
163+
const createDummyTurtle = () => {
164+
return { ...dummyTurtle };
165+
};
166+
167+
// --- Test Suite for SensorBlocks ---
168+
169+
describe("setupSensorsBlocks", () => {
170+
let activity, logo, turtleIndex;
171+
172+
beforeEach(() => {
173+
// Reset created blocks.
174+
DummyFlowBlock.createdBlocks = {};
175+
176+
// Define global constants.
177+
global.NOINPUTERRORMSG = "No input provided";
178+
global.NANERRORMSG = "Not a number";
179+
180+
// Dummy internationalization.
181+
global._ = jest.fn((str) => str);
182+
183+
// Dummy activity.
184+
activity = {
185+
errorMsg: jest.fn(),
186+
blocks: { blockList: {} },
187+
getStageX: dummyStageX,
188+
getStageY: dummyStageY,
189+
getStageMouseDown: dummyStageMouseDown,
190+
getCurrentKeyCode: dummyCurrentKeyCode,
191+
clearCurrentKeyCode: dummyClearCurrentKeyCode,
192+
refreshCanvas: jest.fn()
193+
};
194+
195+
// Dummy turtles.
196+
activity.turtles = {
197+
turtleObjs: {},
198+
getTurtle(turtle) {
199+
if (!this.turtleObjs[turtle]) {
200+
this.turtleObjs[turtle] = createDummyTurtle();
201+
}
202+
return this.turtleObjs[turtle];
203+
},
204+
ithTurtle(turtle) {
205+
return this.getTurtle(turtle);
206+
}
207+
};
208+
209+
// Dummy logo.
210+
logo = {
211+
inStatusMatrix: false,
212+
statusFields: [],
213+
inputValues: {},
214+
lastKeyCode: null,
215+
clearTurtleRun: jest.fn(),
216+
setDispatchBlock: jest.fn(),
217+
setTurtleListener: jest.fn()
218+
};
219+
220+
turtleIndex = 0;
221+
222+
// Initialize sensor blocks.
223+
setupSensorsBlocks(activity);
224+
});
225+
226+
describe("InputBlock", () => {
227+
it("should set up the input form and wait for input", () => {
228+
const inputBlock = DummyFlowBlock.createdBlocks["input"];
229+
// Stub doWait on the turtle.
230+
activity.turtles.ithTurtle(turtleIndex).doWait = jest.fn();
231+
// Set up dummy block connections.
232+
activity.blocks.blockList["blkInput"] = {
233+
connections: [null, "cblk1"]
234+
};
235+
activity.blocks.blockList["cblk1"] = { value: "Enter text" };
236+
inputBlock.flow([], logo, turtleIndex, "blkInput");
237+
const labelDiv = docById("labelDiv");
238+
expect(labelDiv.innerHTML).toContain("input id=\"textLabel\"");
239+
expect(activity.turtles.ithTurtle(turtleIndex).doWait).toHaveBeenCalledWith(120);
240+
// Note: we are not simulating the keypress event.
241+
});
242+
});
243+
244+
describe("InputValueBlock", () => {
245+
it("should return input value if present", () => {
246+
logo.inputValues[turtleIndex] = 42;
247+
const inputValueBlock = DummyFlowBlock.createdBlocks["inputvalue"];
248+
expect(inputValueBlock.updateParameter(logo, turtleIndex)).toEqual(42);
249+
expect(inputValueBlock.arg(logo, turtleIndex, "blkIV")).toEqual(42);
250+
});
251+
it("should call errorMsg and return 0 if no input value", () => {
252+
const inputValueBlock = DummyFlowBlock.createdBlocks["inputvalue"];
253+
const ret = inputValueBlock.arg(logo, turtleIndex, "blkIV");
254+
expect(activity.errorMsg).toHaveBeenCalledWith("No input provided", "blkIV");
255+
expect(ret).toEqual(0);
256+
});
257+
});
258+
259+
describe("PitchnessBlock", () => {
260+
it("should return a frequency based on pitch analyser", () => {
261+
logo.mic = { connect: jest.fn() };
262+
logo.pitchAnalyser = null;
263+
logo.limit = 16;
264+
const pitchBlock = DummyFlowBlock.createdBlocks["pitchness"];
265+
const freq = pitchBlock.arg(logo);
266+
expect(typeof freq).toEqual("number");
267+
});
268+
});
269+
270+
describe("LoudnessBlock", () => {
271+
it("should return computed loudness", () => {
272+
logo.mic = { connect: jest.fn() };
273+
logo.volumeAnalyser = null;
274+
logo.limit = 16;
275+
const loudnessBlock = DummyFlowBlock.createdBlocks["loudness"];
276+
const loudness = loudnessBlock.arg(logo);
277+
expect(typeof loudness).toEqual("number");
278+
});
279+
});
280+
281+
describe("MyClickBlock", () => {
282+
it("should return a click event string with turtle id", () => {
283+
activity.turtles.getTurtle = jest.fn(() => ({ id: "T123" }));
284+
const clickBlock = DummyFlowBlock.createdBlocks["myclick"];
285+
const ret = clickBlock.arg(logo, turtleIndex);
286+
expect(ret).toEqual("clickT123");
287+
});
288+
});
289+
290+
describe("MouseYBlock", () => {
291+
it("should return stage Y position", () => {
292+
const mouseYBlock = DummyFlowBlock.createdBlocks["mousey"];
293+
const ret = mouseYBlock.arg(logo, turtleIndex, "blkMY");
294+
expect(ret).toEqual(dummyStageY());
295+
});
296+
});
297+
298+
describe("ToASCIIBlock", () => {
299+
it("should convert a number to a character", () => {
300+
activity.blocks.blockList["blkASCII"] = { connections: [null, "cblkASCII"] };
301+
activity.blocks.blockList["cblkASCII"] = {};
302+
logo.parseArg = jest.fn(() => 65);
303+
const asciiBlock = DummyFlowBlock.createdBlocks["toascii"];
304+
const ret = asciiBlock.arg(logo, turtleIndex, "blkASCII", 65);
305+
expect(ret).toEqual("A");
306+
});
307+
it("should call errorMsg and return 0 for non-number input", () => {
308+
activity.blocks.blockList["blkASCII2"] = { connections: [null, "cblkASCII2"] };
309+
activity.blocks.blockList["cblkASCII2"] = {};
310+
logo.parseArg = jest.fn(() => "NaN");
311+
const asciiBlock = DummyFlowBlock.createdBlocks["toascii"];
312+
const ret = asciiBlock.arg(logo, turtleIndex, "blkASCII2", "NaN");
313+
expect(activity.errorMsg).toHaveBeenCalledWith("Not a number", "blkASCII2");
314+
expect(ret).toEqual(0);
315+
});
316+
});
317+
318+
describe("KeyboardBlock", () => {
319+
it("should return the last key code and clear it", () => {
320+
logo.lastKeyCode = 66;
321+
activity.getCurrentKeyCode = jest.fn(() => 66);
322+
activity.clearCurrentKeyCode = jest.fn();
323+
const keyboardBlock = DummyFlowBlock.createdBlocks["keyboard"];
324+
const ret = keyboardBlock.arg(logo);
325+
expect(ret).toEqual(66);
326+
expect(activity.clearCurrentKeyCode).toHaveBeenCalled();
327+
});
328+
});
329+
330+
describe("TimeBlock", () => {
331+
it("should return elapsed time in seconds", () => {
332+
logo.time = new Date().getTime() - 5000;
333+
const timeBlock = DummyFlowBlock.createdBlocks["time"];
334+
const ret = timeBlock.arg(logo);
335+
expect(ret).toBeGreaterThanOrEqual(4.9);
336+
expect(ret).toBeLessThanOrEqual(5.1);
337+
});
338+
});
339+
340+
describe("MouseXBlock", () => {
341+
it("should return stage X position", () => {
342+
const mouseXBlock = DummyFlowBlock.createdBlocks["mousex"];
343+
const ret = mouseXBlock.arg(logo, turtleIndex, "blkMX");
344+
expect(ret).toEqual(dummyStageX());
345+
});
346+
});
347+
348+
describe("MouseButtonBlock", () => {
349+
it("should return stage mouse down state", () => {
350+
const mouseButtonBlock = DummyFlowBlock.createdBlocks["mousebutton"];
351+
const ret = mouseButtonBlock.arg(logo);
352+
expect(ret).toEqual(dummyStageMouseDown());
353+
});
354+
});
355+
});

0 commit comments

Comments
 (0)