Skip to content

Commit ab8f8df

Browse files
authored
test: add unit tests for statistics.js with 100% coverage (#4972)
1 parent 75bb85d commit ab8f8df

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-0
lines changed

js/widgets/statistics.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,7 @@ class StatsWindow {
116116
<li>ornaments used: ${stats["ornaments"]}</li>`;
117117
}
118118
}
119+
/* istanbul ignore next */
120+
if (typeof module !== "undefined") {
121+
module.exports = StatsWindow;
122+
}

js/widgets/statistics.test.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* MusicBlocks v3.6.2
3+
*
4+
* @author Divyam Agarwal
5+
*
6+
* @copyright 2026 Divyam Agarwal
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 StatsWindow = require("./statistics.js");
24+
25+
describe("StatsWindow", () => {
26+
let activity, widgetWin, body, capturedCb;
27+
28+
beforeEach(() => {
29+
capturedCb = null;
30+
body = document.createElement("div");
31+
32+
activity = {
33+
blocks: {
34+
showBlocks: jest.fn(),
35+
hideBlocks: jest.fn(),
36+
activeBlock: null
37+
},
38+
logo: { statsWindow: null },
39+
loading: false,
40+
showBlocksAfterRun: true
41+
};
42+
43+
widgetWin = {
44+
clear: jest.fn(),
45+
show: jest.fn(),
46+
destroy: jest.fn(),
47+
sendToCenter: jest.fn(),
48+
onclose: null,
49+
onmaximize: null,
50+
getWidgetBody: jest.fn().mockReturnValue(body),
51+
getWidgetFrame: jest
52+
.fn()
53+
.mockReturnValue({ getBoundingClientRect: () => ({ height: 500 }) }),
54+
isMaximized: jest.fn().mockReturnValue(false)
55+
};
56+
57+
window.widgetWindows = {
58+
windowFor: jest.fn().mockReturnValue(widgetWin)
59+
};
60+
61+
global.docById = jest.fn().mockReturnValue({ getContext: jest.fn().mockReturnValue({}) });
62+
63+
global.analyzeProject = jest.fn().mockReturnValue({});
64+
global.runAnalytics = jest.fn();
65+
global.scoreToChartData = jest.fn().mockReturnValue({});
66+
global.getChartOptions = jest.fn().mockImplementation(cb => {
67+
capturedCb = cb;
68+
return {};
69+
});
70+
71+
global.Chart = jest.fn().mockImplementation(() => ({
72+
Radar: jest.fn().mockReturnValue({
73+
toBase64Image: jest.fn().mockReturnValue("data:image/png;base64,fakedata")
74+
})
75+
}));
76+
});
77+
78+
test("constructor initialises widget and calls doAnalytics", () => {
79+
const sw = new StatsWindow(activity);
80+
81+
expect(window.widgetWindows.windowFor).toHaveBeenCalledWith(sw, "stats", "stats");
82+
expect(widgetWin.clear).toHaveBeenCalled();
83+
expect(widgetWin.show).toHaveBeenCalled();
84+
expect(widgetWin.sendToCenter).toHaveBeenCalled();
85+
expect(sw.isOpen).toBe(true);
86+
expect(global.analyzeProject).toHaveBeenCalledWith(activity);
87+
expect(global.runAnalytics).toHaveBeenCalledWith(activity);
88+
});
89+
90+
test("onclose cleans up and marks window closed", () => {
91+
const sw = new StatsWindow(activity);
92+
widgetWin.onclose();
93+
94+
expect(sw.isOpen).toBe(false);
95+
expect(activity.blocks.showBlocks).toHaveBeenCalled();
96+
expect(widgetWin.destroy).toHaveBeenCalled();
97+
expect(activity.logo.statsWindow).toBeNull();
98+
});
99+
100+
test("onmaximize sets flex layout when maximized", () => {
101+
const sw = new StatsWindow(activity);
102+
global.analyzeProject.mockClear();
103+
104+
widgetWin.isMaximized.mockReturnValue(true);
105+
widgetWin.onmaximize();
106+
107+
expect(body.style.display).toBe("flex");
108+
expect(body.style.justifyContent).toBe("space-between");
109+
expect(body.style.padding).toBe("0px 2vw");
110+
expect(global.analyzeProject).toHaveBeenCalled();
111+
});
112+
113+
test("onmaximize resets padding when not maximized", () => {
114+
const sw = new StatsWindow(activity);
115+
widgetWin.isMaximized.mockReturnValue(false);
116+
widgetWin.onmaximize();
117+
118+
expect(body.style.padding).toBe("0px 0px");
119+
});
120+
121+
test("chart callback sets img width to 200 when not maximized", () => {
122+
widgetWin.isMaximized.mockReturnValue(false);
123+
const sw = new StatsWindow(activity);
124+
125+
expect(capturedCb).not.toBeNull();
126+
capturedCb();
127+
128+
const imgs = body.querySelectorAll("img");
129+
expect(imgs.length).toBeGreaterThanOrEqual(1);
130+
expect(imgs[imgs.length - 1].width).toBe(200);
131+
expect(activity.blocks.hideBlocks).toHaveBeenCalled();
132+
expect(activity.showBlocksAfterRun).toBe(false);
133+
expect(document.body.style.cursor).toBe("default");
134+
});
135+
136+
test("chart callback uses frame height for img width when maximized", () => {
137+
widgetWin.isMaximized.mockReturnValue(true);
138+
const sw = new StatsWindow(activity);
139+
140+
capturedCb();
141+
142+
const imgs = body.querySelectorAll("img");
143+
expect(imgs[imgs.length - 1].width).toBe(420); // 500 - 80
144+
});
145+
146+
test("displayInfo renders all stats fields with correct Hz", () => {
147+
const sw = new StatsWindow(activity);
148+
149+
sw.displayInfo({
150+
duples: 5,
151+
triplets: 2,
152+
quintuplets: 0,
153+
pitchNames: new Set(["A", "C#", "E"]),
154+
numberOfNotes: 20,
155+
lowestNote: ["A4", 60, 440],
156+
highestNote: ["C5", 72, 523.25],
157+
rests: 4,
158+
ornaments: 1
159+
});
160+
161+
const html = sw.jsonObject.innerHTML;
162+
expect(html).toContain("duples: 5");
163+
expect(html).toContain("triplets: 2");
164+
expect(html).toContain("quintuplets: 0");
165+
expect(html).toContain("pitch names: A, C#, E");
166+
expect(html).toContain("number of notes: 20");
167+
expect(html).toContain("lowest note: A4");
168+
expect(html).toContain("441Hz"); // 440 + 0.5 rounded
169+
expect(html).toContain("highest note: C5");
170+
expect(html).toContain("524Hz"); // 523.25 + 0.5 rounded
171+
expect(html).toContain("rests used: 4");
172+
expect(html).toContain("ornaments used: 1");
173+
});
174+
});

0 commit comments

Comments
 (0)