Skip to content

Commit d7986cc

Browse files
test: Add unit tests for idle watcher
Ensures inactivity tracking and auto-saving work correctly.
1 parent 7329e3b commit d7986cc

1 file changed

Lines changed: 296 additions & 0 deletions

File tree

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
const { setupActivityIdleWatcher } = require("../idle-watcher");
2+
3+
describe("idle-watcher", () => {
4+
let activity;
5+
let mockCreatejs;
6+
let mockErrorHandler;
7+
let originalDateNow;
8+
9+
beforeEach(() => {
10+
jest.useFakeTimers();
11+
originalDateNow = Date.now;
12+
13+
// Setup globals
14+
global.debugLog = jest.fn();
15+
16+
mockCreatejs = {
17+
Ticker: {
18+
framerate: 60
19+
}
20+
};
21+
global.createjs = mockCreatejs;
22+
23+
mockErrorHandler = {
24+
recoverable: jest.fn()
25+
};
26+
global.ErrorHandler = mockErrorHandler;
27+
28+
// Mock window for event listeners
29+
global.window = {};
30+
31+
activity = {
32+
addEventListener: jest.fn(),
33+
removeEventListener: jest.fn(),
34+
turtles: {
35+
running: jest.fn().mockReturnValue(false)
36+
},
37+
saveLocally: jest.fn(),
38+
stageDirty: false,
39+
logo: {
40+
_alreadyRunning: false
41+
}
42+
};
43+
44+
setupActivityIdleWatcher(activity);
45+
});
46+
47+
afterEach(() => {
48+
jest.clearAllMocks();
49+
jest.useRealTimers();
50+
Date.now = originalDateNow;
51+
delete global.debugLog;
52+
delete global.createjs;
53+
delete global.ErrorHandler;
54+
delete global.window;
55+
});
56+
57+
describe("Initialization", () => {
58+
it("attaches _initIdleWatcher, _stopIdleWatcher, _initAutoSave, _stopAutoSave to activity", () => {
59+
expect(typeof activity._initIdleWatcher).toBe("function");
60+
expect(typeof activity._stopIdleWatcher).toBe("function");
61+
expect(typeof activity._initAutoSave).toBe("function");
62+
expect(typeof activity._stopAutoSave).toBe("function");
63+
});
64+
});
65+
66+
describe("Idle Watcher (_initIdleWatcher & _stopIdleWatcher)", () => {
67+
it("sets up interval and event listeners when _initIdleWatcher is called", () => {
68+
jest.spyOn(global, "setInterval");
69+
activity._initIdleWatcher();
70+
71+
expect(activity.addEventListener).toHaveBeenCalledWith(
72+
global.window,
73+
"mousemove",
74+
activity._resetIdleTimer
75+
);
76+
expect(activity.addEventListener).toHaveBeenCalledWith(
77+
global.window,
78+
"mousedown",
79+
activity._resetIdleTimer
80+
);
81+
expect(activity.addEventListener).toHaveBeenCalledWith(
82+
global.window,
83+
"keydown",
84+
activity._resetIdleTimer
85+
);
86+
expect(activity.addEventListener).toHaveBeenCalledWith(
87+
global.window,
88+
"touchstart",
89+
activity._resetIdleTimer
90+
);
91+
expect(activity.addEventListener).toHaveBeenCalledWith(
92+
global.window,
93+
"wheel",
94+
activity._resetIdleTimer,
95+
{ passive: true }
96+
);
97+
98+
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 1000);
99+
expect(activity._idleWatcherInterval).toBeDefined();
100+
});
101+
102+
it("clears interval and removes listeners when _stopIdleWatcher is called", () => {
103+
activity._initIdleWatcher();
104+
const resetTimerFunc = activity._resetIdleTimer;
105+
const interval = activity._idleWatcherInterval;
106+
107+
jest.spyOn(global, "clearInterval");
108+
109+
activity._stopIdleWatcher();
110+
111+
expect(clearInterval).toHaveBeenCalledWith(interval);
112+
113+
expect(activity.removeEventListener).toHaveBeenCalledWith(
114+
global.window,
115+
"mousemove",
116+
resetTimerFunc
117+
);
118+
expect(activity.removeEventListener).toHaveBeenCalledWith(
119+
global.window,
120+
"mousedown",
121+
resetTimerFunc
122+
);
123+
expect(activity.removeEventListener).toHaveBeenCalledWith(
124+
global.window,
125+
"keydown",
126+
resetTimerFunc
127+
);
128+
expect(activity.removeEventListener).toHaveBeenCalledWith(
129+
global.window,
130+
"touchstart",
131+
resetTimerFunc
132+
);
133+
expect(activity.removeEventListener).toHaveBeenCalledWith(
134+
global.window,
135+
"wheel",
136+
resetTimerFunc
137+
);
138+
139+
expect(activity._idleWatcherInterval).toBeUndefined();
140+
expect(activity._resetIdleTimer).toBeUndefined();
141+
});
142+
143+
it("does not leak listeners when _initIdleWatcher is called twice", () => {
144+
activity._initIdleWatcher();
145+
const firstInterval = activity._idleWatcherInterval;
146+
const resetTimerFunc = activity._resetIdleTimer;
147+
148+
jest.spyOn(global, "clearInterval");
149+
150+
activity._initIdleWatcher();
151+
152+
expect(clearInterval).toHaveBeenCalledWith(firstInterval);
153+
expect(activity.removeEventListener).toHaveBeenCalledWith(
154+
global.window,
155+
"mousemove",
156+
resetTimerFunc
157+
);
158+
// Verify new listeners and interval were created
159+
expect(activity.addEventListener).toHaveBeenCalledTimes(10); // 5 from first call, 5 from second
160+
});
161+
162+
it("becomes idle and drops framerate after IDLE_THRESHOLD", () => {
163+
let currentTime = 10000;
164+
Date.now = jest.fn(() => currentTime);
165+
166+
activity._initIdleWatcher();
167+
168+
expect(activity.isAppIdle).toBe(false);
169+
expect(mockCreatejs.Ticker.framerate).toBe(60);
170+
171+
// Advance time by 6 seconds (threshold is 5s)
172+
currentTime += 6000;
173+
174+
// Run interval callbacks
175+
jest.advanceTimersByTime(1000);
176+
177+
expect(activity.isAppIdle).toBe(true);
178+
expect(mockCreatejs.Ticker.framerate).toBe(1);
179+
expect(global.debugLog).toHaveBeenCalled();
180+
});
181+
182+
it("wakes up on user event resetting idle state and framerate", () => {
183+
let currentTime = 10000;
184+
Date.now = jest.fn(() => currentTime);
185+
186+
activity._initIdleWatcher();
187+
188+
currentTime += 6000;
189+
jest.advanceTimersByTime(1000);
190+
191+
expect(activity.isAppIdle).toBe(true);
192+
expect(mockCreatejs.Ticker.framerate).toBe(1);
193+
194+
// Trigger wake-up function
195+
activity._resetIdleTimer();
196+
197+
expect(activity.isAppIdle).toBe(false);
198+
expect(mockCreatejs.Ticker.framerate).toBe(60);
199+
expect(activity.stageDirty).toBe(true);
200+
});
201+
202+
it("wakes up if music starts playing while idle", () => {
203+
let currentTime = 10000;
204+
Date.now = jest.fn(() => currentTime);
205+
206+
activity._initIdleWatcher();
207+
208+
currentTime += 6000;
209+
jest.advanceTimersByTime(1000);
210+
211+
expect(activity.isAppIdle).toBe(true);
212+
213+
// Music starts playing
214+
activity.turtles.running.mockReturnValue(true);
215+
216+
jest.advanceTimersByTime(1000); // Check idle watcher again
217+
218+
expect(activity.isAppIdle).toBe(false);
219+
expect(mockCreatejs.Ticker.framerate).toBe(60);
220+
});
221+
222+
it("does not become idle if music is playing", () => {
223+
let currentTime = 10000;
224+
Date.now = jest.fn(() => currentTime);
225+
226+
activity._initIdleWatcher();
227+
228+
activity.turtles.running.mockReturnValue(true);
229+
230+
currentTime += 6000;
231+
jest.advanceTimersByTime(1000);
232+
233+
expect(activity.isAppIdle).toBe(false);
234+
expect(mockCreatejs.Ticker.framerate).toBe(60);
235+
});
236+
});
237+
238+
describe("Auto Save (_initAutoSave & _stopAutoSave)", () => {
239+
it("sets auto save interval and calls saveLocally", () => {
240+
activity._initAutoSave();
241+
242+
expect(activity._autoSaveInterval).not.toBeNull();
243+
244+
// Fast-forward time by 5 minutes
245+
jest.advanceTimersByTime(5 * 60 * 1000);
246+
247+
expect(activity.saveLocally).toHaveBeenCalledTimes(1);
248+
});
249+
250+
it("does not save locally if logo is running", () => {
251+
activity._initAutoSave();
252+
253+
activity.logo._alreadyRunning = true;
254+
255+
jest.advanceTimersByTime(5 * 60 * 1000);
256+
257+
expect(activity.saveLocally).not.toHaveBeenCalled();
258+
});
259+
260+
it("does not save locally if saveLocally is not defined", () => {
261+
activity.saveLocally = null;
262+
activity._initAutoSave();
263+
264+
jest.advanceTimersByTime(5 * 60 * 1000);
265+
266+
expect(activity.saveLocally).toBeNull();
267+
});
268+
269+
it("clears interval when _stopAutoSave is called", () => {
270+
activity._initAutoSave();
271+
const interval = activity._autoSaveInterval;
272+
273+
jest.spyOn(global, "clearInterval");
274+
275+
activity._stopAutoSave();
276+
277+
expect(clearInterval).toHaveBeenCalledWith(interval);
278+
expect(activity._autoSaveInterval).toBeNull();
279+
});
280+
281+
it("recovers from errors during saveLocally", () => {
282+
activity._initAutoSave();
283+
284+
const error = new Error("Save failed");
285+
activity.saveLocally.mockImplementation(() => {
286+
throw error;
287+
});
288+
289+
jest.advanceTimersByTime(5 * 60 * 1000);
290+
291+
expect(mockErrorHandler.recoverable).toHaveBeenCalledWith(error, {
292+
operation: "autoSave"
293+
});
294+
});
295+
});
296+
});

0 commit comments

Comments
 (0)