Skip to content

Commit b2e18e5

Browse files
committed
feat: improve test coverage from 4.7% to 23.2%
- Fixed Jest module mapping for @app and @engine paths - Added comprehensive mocks for Supabase and Auth modules - Implemented missing App.sync() method to emit Events.SYNC - Created new test suites: * Logger tests: comprehensive coverage for all log levels * EventBus tests: pub/sub functionality, once, off, clear * PerlinNoise tests: singleton, caching, coordinate handling * Router tests: navigation, history, event emission - Improved test infrastructure with proper setup/teardown Coverage improvements: - Statements: 4.69% → 23.22% - Branches: 2.4% → 18.49% - Functions: 4.37% → 28.05% - Lines: 4.81% → 23.81%
1 parent 9ddd30f commit b2e18e5

7 files changed

Lines changed: 603 additions & 3 deletions

File tree

jest.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ module.exports = {
1919
'\\.(css|less|sass|scss)$': '<rootDir>/test/mock/styleMock.js',
2020
'\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/test/mock/fileMock.js',
2121
'./webpack-hot-module': '<rootDir>/test/mock/fileMock.js',
22-
'^@app(.*)$': '<rootDir>/src/$1',
23-
'^engine(.*)$': '<rootDir>/src/engine/$1',
22+
'^@app/(.*)$': '<rootDir>/src/$1',
23+
'^@engine/(.*)$': '<rootDir>/src/engine/$1',
2424
},
2525
collectCoverage: true,
2626
coverageDirectory: 'coverage',

jest.setup.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,52 @@ if (typeof globalThis.structuredClone !== 'function') {
33
globalThis.structuredClone = (obj) => {
44
return JSON.parse(JSON.stringify(obj));
55
};
6-
}
6+
}
7+
8+
// Mock Supabase client
9+
jest.mock('./src/user-account/supabase-client', () => ({
10+
SUPABASE_URL: 'https://test.supabase.co',
11+
SUPABASE_ANON_KEY: 'test-key',
12+
isSupabaseApprovedDomain: true,
13+
getSupabaseClient: jest.fn(() => ({
14+
auth: {
15+
getSession: jest.fn().mockResolvedValue({ data: { session: null }, error: null }),
16+
onAuthStateChange: jest.fn(() => ({
17+
data: { subscription: { unsubscribe: jest.fn() } }
18+
})),
19+
},
20+
})),
21+
}));
22+
23+
// Mock Auth module
24+
jest.mock('./src/user-account/auth', () => ({
25+
Auth: {
26+
initializeAuth: jest.fn().mockResolvedValue(null),
27+
signUp: jest.fn().mockResolvedValue({ data: null, error: null }),
28+
signIn: jest.fn().mockResolvedValue({ data: null, error: null }),
29+
signInWithOAuthProvider: jest.fn(),
30+
updatePassword: jest.fn().mockResolvedValue({ error: null }),
31+
updateProfile: jest.fn().mockResolvedValue(null),
32+
signOut: jest.fn().mockResolvedValue(undefined),
33+
getCurrentUser: jest.fn().mockResolvedValue(null),
34+
getUserProfile: jest.fn().mockResolvedValue(null),
35+
isLoggedIn: jest.fn().mockResolvedValue(false),
36+
setSession: jest.fn().mockResolvedValue(undefined),
37+
onAuthStateChange: jest.fn(),
38+
getAccessToken: jest.fn().mockResolvedValue(null),
39+
getSession: jest.fn().mockResolvedValue(null),
40+
refreshSession: jest.fn().mockResolvedValue(null),
41+
isTokenExpired: jest.fn().mockResolvedValue(false),
42+
getValidAccessToken: jest.fn().mockResolvedValue(null),
43+
},
44+
}));
45+
46+
// Mock UserDataService
47+
jest.mock('./src/user-account/user-data-service', () => ({
48+
initUserDataService: jest.fn(),
49+
getUserDataService: jest.fn(() => ({
50+
getProgressData: jest.fn().mockResolvedValue(null),
51+
saveProgressData: jest.fn().mockResolvedValue(undefined),
52+
isInitialized: true,
53+
})),
54+
}));

src/app.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { Router } from './router';
99
import { SimulationManager } from './simulation/simulation-manager';
1010
import { Auth } from './user-account/auth';
1111
import { initUserDataService } from './user-account/user-data-service';
12+
import { EventBus } from './events/event-bus';
13+
import { Events } from './events/events';
1214

1315
/**
1416
* Main Application Class
@@ -93,6 +95,13 @@ export class App extends BaseElement {
9395
document.addEventListener('contextmenu', (event) => event.preventDefault());
9496
}
9597

98+
/**
99+
* Trigger a sync event to notify components to save their state
100+
*/
101+
sync(): void {
102+
EventBus.getInstance().emit(Events.SYNC);
103+
}
104+
96105
static __resetAll__(): void {
97106
Header['instance_'] = null;
98107
Body['instance_'] = null;

test/events/event-bus.test.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { EventBus } from '../../src/events/event-bus';
2+
import { Events } from '../../src/events/events';
3+
4+
describe('EventBus', () => {
5+
let eventBus: EventBus;
6+
7+
beforeEach(() => {
8+
// Reset the singleton instance before each test
9+
EventBus.destroy();
10+
eventBus = EventBus.getInstance();
11+
});
12+
13+
afterEach(() => {
14+
EventBus.destroy();
15+
});
16+
17+
describe('getInstance', () => {
18+
it('should return a singleton instance', () => {
19+
const instance1 = EventBus.getInstance();
20+
const instance2 = EventBus.getInstance();
21+
22+
expect(instance1).toBe(instance2);
23+
});
24+
});
25+
26+
describe('on and emit', () => {
27+
it('should allow subscribing to events and emit them', () => {
28+
const callback = jest.fn();
29+
30+
eventBus.on(Events.UPDATE, callback);
31+
eventBus.emit(Events.UPDATE, 123);
32+
33+
expect(callback).toHaveBeenCalledTimes(1);
34+
expect(callback).toHaveBeenCalledWith(123);
35+
});
36+
37+
it('should call multiple callbacks for the same event', () => {
38+
const callback1 = jest.fn();
39+
const callback2 = jest.fn();
40+
41+
eventBus.on(Events.DRAW, callback1);
42+
eventBus.on(Events.DRAW, callback2);
43+
eventBus.emit(Events.DRAW, 456);
44+
45+
expect(callback1).toHaveBeenCalledWith(456);
46+
expect(callback2).toHaveBeenCalledWith(456);
47+
});
48+
49+
it('should pass multiple arguments to callbacks', () => {
50+
const callback = jest.fn();
51+
52+
eventBus.on(Events.SYNC, callback);
53+
eventBus.emit(Events.SYNC);
54+
55+
expect(callback).toHaveBeenCalledTimes(1);
56+
});
57+
58+
it('should not throw error when emitting event with no subscribers', () => {
59+
expect(() => {
60+
eventBus.emit(Events.UPDATE, 789);
61+
}).not.toThrow();
62+
});
63+
});
64+
65+
describe('off', () => {
66+
it('should unsubscribe a callback from an event', () => {
67+
const callback = jest.fn();
68+
69+
eventBus.on(Events.UPDATE, callback);
70+
eventBus.off(Events.UPDATE, callback);
71+
eventBus.emit(Events.UPDATE, 100);
72+
73+
expect(callback).not.toHaveBeenCalled();
74+
});
75+
76+
it('should only remove the specified callback', () => {
77+
const callback1 = jest.fn();
78+
const callback2 = jest.fn();
79+
80+
eventBus.on(Events.DRAW, callback1);
81+
eventBus.on(Events.DRAW, callback2);
82+
eventBus.off(Events.DRAW, callback1);
83+
eventBus.emit(Events.DRAW, 200);
84+
85+
expect(callback1).not.toHaveBeenCalled();
86+
expect(callback2).toHaveBeenCalledWith(200);
87+
});
88+
89+
it('should not throw error when removing non-existent callback', () => {
90+
const callback = jest.fn();
91+
92+
expect(() => {
93+
eventBus.off(Events.UPDATE, callback);
94+
}).not.toThrow();
95+
});
96+
});
97+
98+
describe('once', () => {
99+
it('should only call the callback once', () => {
100+
const callback = jest.fn();
101+
102+
eventBus.once(Events.UPDATE, callback);
103+
eventBus.emit(Events.UPDATE, 1);
104+
eventBus.emit(Events.UPDATE, 2);
105+
eventBus.emit(Events.UPDATE, 3);
106+
107+
expect(callback).toHaveBeenCalledTimes(1);
108+
expect(callback).toHaveBeenCalledWith(1);
109+
});
110+
111+
it('should automatically unsubscribe after first call', () => {
112+
const callback = jest.fn();
113+
114+
eventBus.once(Events.DRAW, callback);
115+
eventBus.emit(Events.DRAW, 100);
116+
117+
// Manually check if callback is removed by trying to remove it again
118+
expect(() => {
119+
eventBus.off(Events.DRAW, callback);
120+
}).not.toThrow();
121+
122+
eventBus.emit(Events.DRAW, 200);
123+
expect(callback).toHaveBeenCalledTimes(1);
124+
});
125+
});
126+
127+
describe('clear', () => {
128+
it('should remove all callbacks for an event', () => {
129+
const callback1 = jest.fn();
130+
const callback2 = jest.fn();
131+
132+
eventBus.on(Events.UPDATE, callback1);
133+
eventBus.on(Events.UPDATE, callback2);
134+
eventBus.clear(Events.UPDATE);
135+
eventBus.emit(Events.UPDATE, 300);
136+
137+
expect(callback1).not.toHaveBeenCalled();
138+
expect(callback2).not.toHaveBeenCalled();
139+
});
140+
141+
it('should only clear callbacks for the specified event', () => {
142+
const updateCallback = jest.fn();
143+
const drawCallback = jest.fn();
144+
145+
eventBus.on(Events.UPDATE, updateCallback);
146+
eventBus.on(Events.DRAW, drawCallback);
147+
eventBus.clear(Events.UPDATE);
148+
149+
eventBus.emit(Events.UPDATE, 400);
150+
eventBus.emit(Events.DRAW, 500);
151+
152+
expect(updateCallback).not.toHaveBeenCalled();
153+
expect(drawCallback).toHaveBeenCalledWith(500);
154+
});
155+
});
156+
157+
describe('destroy', () => {
158+
it('should reset the singleton instance', () => {
159+
const instance1 = EventBus.getInstance();
160+
EventBus.destroy();
161+
const instance2 = EventBus.getInstance();
162+
163+
expect(instance1).not.toBe(instance2);
164+
});
165+
});
166+
});

test/logging/logger.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Logger } from '../../src/logging/logger';
2+
3+
describe('Logger', () => {
4+
let consoleLogSpy: jest.SpyInstance;
5+
let consoleInfoSpy: jest.SpyInstance;
6+
let consoleWarnSpy: jest.SpyInstance;
7+
let consoleErrorSpy: jest.SpyInstance;
8+
9+
beforeEach(() => {
10+
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
11+
consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
12+
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
13+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
14+
});
15+
16+
afterEach(() => {
17+
consoleLogSpy.mockRestore();
18+
consoleInfoSpy.mockRestore();
19+
consoleWarnSpy.mockRestore();
20+
consoleErrorSpy.mockRestore();
21+
});
22+
23+
describe('log', () => {
24+
it('should log messages with LOG prefix and blue color', () => {
25+
Logger.log('Test message');
26+
27+
expect(consoleLogSpy).toHaveBeenCalledWith(
28+
'%c[LOG] Test message',
29+
'color: #2196F3'
30+
);
31+
});
32+
33+
it('should log messages with optional parameters', () => {
34+
const obj = { key: 'value' };
35+
Logger.log('Test message', obj, 123);
36+
37+
expect(consoleLogSpy).toHaveBeenCalledWith(
38+
'%c[LOG] Test message',
39+
'color: #2196F3',
40+
obj,
41+
123
42+
);
43+
});
44+
45+
it('should ignore logs containing "app:" namespace', () => {
46+
Logger.log('app: some message');
47+
48+
expect(consoleLogSpy).not.toHaveBeenCalled();
49+
});
50+
});
51+
52+
describe('info', () => {
53+
it('should log info messages with INFO prefix and green color', () => {
54+
Logger.info('Info message');
55+
56+
expect(consoleInfoSpy).toHaveBeenCalledWith(
57+
'%c[INFO] Info message',
58+
'color: #4CAF50'
59+
);
60+
});
61+
62+
it('should log info messages with optional parameters', () => {
63+
Logger.info('Info message', 'param1', 'param2');
64+
65+
expect(consoleInfoSpy).toHaveBeenCalledWith(
66+
'%c[INFO] Info message',
67+
'color: #4CAF50',
68+
'param1',
69+
'param2'
70+
);
71+
});
72+
});
73+
74+
describe('warn', () => {
75+
it('should log warning messages with WARN prefix and amber color', () => {
76+
Logger.warn('Warning message');
77+
78+
expect(consoleWarnSpy).toHaveBeenCalledWith(
79+
'%c[WARN] Warning message',
80+
'color: #FFC107'
81+
);
82+
});
83+
84+
it('should log warning messages with optional parameters', () => {
85+
const warning = new Error('Test warning');
86+
Logger.warn('Warning message', warning);
87+
88+
expect(consoleWarnSpy).toHaveBeenCalledWith(
89+
'%c[WARN] Warning message',
90+
'color: #FFC107',
91+
warning
92+
);
93+
});
94+
});
95+
96+
describe('error', () => {
97+
it('should log error messages with ERROR prefix and red color', () => {
98+
Logger.error('Error message');
99+
100+
expect(consoleErrorSpy).toHaveBeenCalledWith(
101+
'%c[ERROR] Error message',
102+
'color: #F44336'
103+
);
104+
});
105+
106+
it('should log error messages with optional parameters', () => {
107+
const error = new Error('Test error');
108+
Logger.error('Error message', error);
109+
110+
expect(consoleErrorSpy).toHaveBeenCalledWith(
111+
'%c[ERROR] Error message',
112+
'color: #F44336',
113+
error
114+
);
115+
});
116+
});
117+
});

0 commit comments

Comments
 (0)