Skip to content

Commit 13ce9af

Browse files
committed
test: increase statistics.js coverage to 100%
1 parent ea7e29b commit 13ce9af

File tree

2 files changed

+123
-20
lines changed

2 files changed

+123
-20
lines changed

js/widgets/statistics.js

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

js/widgets/statistics.test.js

Lines changed: 122 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,14 @@ const StatsWindow = require("./statistics.js");
2525
describe("StatsWindow", () => {
2626
let mockActivity;
2727
let mockWidgetWindow;
28+
let mockWidgetBody;
29+
let capturedCallback;
2830

2931
beforeEach(() => {
32+
capturedCallback = null;
33+
34+
mockWidgetBody = document.createElement("div");
35+
3036
// 1. Mock the Activity object
3137
mockActivity = {
3238
blocks: { showBlocks: jest.fn(), hideBlocks: jest.fn(), activeBlock: null },
@@ -43,7 +49,7 @@ describe("StatsWindow", () => {
4349
sendToCenter: jest.fn(),
4450
onclose: null,
4551
onmaximize: null,
46-
getWidgetBody: jest.fn().mockReturnValue(document.createElement("div")),
52+
getWidgetBody: jest.fn().mockReturnValue(mockWidgetBody),
4753
getWidgetFrame: jest
4854
.fn()
4955
.mockReturnValue({ getBoundingClientRect: () => ({ height: 500 }) }),
@@ -55,29 +61,118 @@ describe("StatsWindow", () => {
5561
windowFor: jest.fn().mockReturnValue(mockWidgetWindow)
5662
};
5763

58-
// 4. CRITICAL FIX: Mock docById to return a fake Canvas with getContext
64+
// 4. Mock docById to return a fake Canvas with getContext
5965
global.docById = jest.fn().mockReturnValue({
60-
getContext: jest.fn().mockReturnValue({
61-
// Fake context methods if needed (not strictly needed for this test)
62-
})
66+
getContext: jest.fn().mockReturnValue({})
6367
});
6468

6569
// 5. Mock external analysis functions (global scope)
6670
global.analyzeProject = jest.fn().mockReturnValue({});
6771
global.runAnalytics = jest.fn();
6872
global.scoreToChartData = jest.fn().mockReturnValue({});
69-
global.getChartOptions = jest.fn().mockReturnValue({});
73+
global.getChartOptions = jest.fn().mockImplementation(cb => {
74+
capturedCallback = cb;
75+
return {};
76+
});
7077

71-
// Mock the Chart.js library
78+
// 6. Mock the Chart.js library — Radar returns an object with toBase64Image
7279
global.Chart = jest.fn().mockImplementation(() => ({
73-
Radar: jest.fn()
80+
Radar: jest.fn().mockReturnValue({
81+
toBase64Image: jest.fn().mockReturnValue("data:image/png;base64,fakedata")
82+
})
7483
}));
7584
});
7685

77-
test("displayInfo should correctly format note statistics and Hz calculations", () => {
78-
const statsWindow = new StatsWindow(mockActivity);
86+
test("constructor initialises the widget and calls doAnalytics", () => {
87+
const sw = new StatsWindow(mockActivity);
88+
89+
expect(window.widgetWindows.windowFor).toHaveBeenCalledWith(sw, "stats", "stats");
90+
expect(mockWidgetWindow.clear).toHaveBeenCalled();
91+
expect(mockWidgetWindow.show).toHaveBeenCalled();
92+
expect(mockWidgetWindow.sendToCenter).toHaveBeenCalled();
93+
expect(sw.isOpen).toBe(true);
94+
// doAnalytics was called during construction
95+
expect(global.analyzeProject).toHaveBeenCalledWith(mockActivity);
96+
expect(global.runAnalytics).toHaveBeenCalledWith(mockActivity);
97+
});
98+
99+
test("onclose sets isOpen to false and cleans up", () => {
100+
const sw = new StatsWindow(mockActivity);
101+
102+
// Trigger the onclose callback
103+
mockWidgetWindow.onclose();
104+
105+
expect(sw.isOpen).toBe(false);
106+
expect(mockActivity.blocks.showBlocks).toHaveBeenCalled();
107+
expect(mockWidgetWindow.destroy).toHaveBeenCalled();
108+
expect(mockActivity.logo.statsWindow).toBeNull();
109+
});
110+
111+
test("onmaximize when maximized sets flex layout and re-runs analytics", () => {
112+
const sw = new StatsWindow(mockActivity);
113+
114+
// Reset call counts from constructor
115+
global.analyzeProject.mockClear();
116+
global.runAnalytics.mockClear();
117+
118+
// Set isMaximized to return true
119+
mockWidgetWindow.isMaximized.mockReturnValue(true);
120+
121+
// Trigger onmaximize
122+
mockWidgetWindow.onmaximize();
123+
124+
expect(mockWidgetBody.style.display).toBe("flex");
125+
expect(mockWidgetBody.style.justifyContent).toBe("space-between");
126+
expect(mockWidgetBody.style.padding).toBe("0px 2vw");
127+
// doAnalytics is re-called
128+
expect(global.analyzeProject).toHaveBeenCalled();
129+
});
130+
131+
test("onmaximize when not maximized resets padding", () => {
132+
const sw = new StatsWindow(mockActivity);
133+
134+
mockWidgetWindow.isMaximized.mockReturnValue(false);
135+
mockWidgetWindow.onmaximize();
136+
137+
expect(mockWidgetBody.style.padding).toBe("0px 0px");
138+
});
139+
140+
test("__callback in doAnalytics sets image width to 200 when not maximized", () => {
141+
mockWidgetWindow.isMaximized.mockReturnValue(false);
142+
const sw = new StatsWindow(mockActivity);
143+
144+
// capturedCallback was captured by our getChartOptions mock
145+
expect(capturedCallback).not.toBeNull();
146+
capturedCallback();
147+
148+
// An <img> should have been appended to the widget body
149+
const imgs = mockWidgetBody.querySelectorAll("img");
150+
expect(imgs.length).toBeGreaterThanOrEqual(1);
151+
const img = imgs[imgs.length - 1];
152+
expect(img.width).toBe(200);
153+
154+
expect(mockActivity.blocks.hideBlocks).toHaveBeenCalled();
155+
expect(mockActivity.showBlocksAfterRun).toBe(false);
156+
expect(document.body.style.cursor).toBe("default");
157+
});
158+
159+
test("__callback in doAnalytics sets image width from frame height when maximized", () => {
160+
mockWidgetWindow.isMaximized.mockReturnValue(true);
161+
const sw = new StatsWindow(mockActivity);
162+
163+
expect(capturedCallback).not.toBeNull();
164+
capturedCallback();
165+
166+
const imgs = mockWidgetBody.querySelectorAll("img");
167+
expect(imgs.length).toBeGreaterThanOrEqual(1);
168+
const img = imgs[imgs.length - 1];
169+
// height (500) - 80 = 420
170+
expect(img.width).toBe(420);
171+
});
172+
173+
test("displayInfo correctly formats all note statistics and Hz calculations", () => {
174+
const sw = new StatsWindow(mockActivity);
79175

80-
// Prepare dummy data
81176
const mockStats = {
82177
duples: 5,
83178
triplets: 2,
@@ -90,16 +185,23 @@ describe("StatsWindow", () => {
90185
ornaments: 1
91186
};
92187

93-
// Run the function
94-
statsWindow.displayInfo(mockStats);
188+
sw.displayInfo(mockStats);
189+
190+
const html = sw.jsonObject.innerHTML;
95191

96-
// Check results
97-
const outputHtml = statsWindow.jsonObject.innerHTML;
192+
// Hz calculations: 440 + 0.5 = 440.5 → "441Hz"; 523.25 + 0.5 = 523.75 → "524Hz"
193+
expect(html).toContain("441Hz");
194+
expect(html).toContain("524Hz");
98195

99-
expect(outputHtml).toContain("441Hz"); // 440 + 0.5 rounded
100-
expect(outputHtml).toContain("524Hz"); // 523.25 + 0.5 rounded
101-
expect(outputHtml).toContain("duples: 5");
102-
expect(outputHtml).toContain("triplets: 2");
103-
expect(outputHtml).toContain("pitch names: A, C#, E");
196+
// All stat fields
197+
expect(html).toContain("duples: 5");
198+
expect(html).toContain("triplets: 2");
199+
expect(html).toContain("quintuplets: 0");
200+
expect(html).toContain("pitch names: A, C#, E");
201+
expect(html).toContain("number of notes: 20");
202+
expect(html).toContain("lowest note: A4");
203+
expect(html).toContain("highest note: C5");
204+
expect(html).toContain("rests used: 4");
205+
expect(html).toContain("ornaments used: 1");
104206
});
105207
});

0 commit comments

Comments
 (0)