Skip to content

Commit 82fbcc2

Browse files
committed
feat(test): add integration test for initial state hydration
1 parent 6370778 commit 82fbcc2

1 file changed

Lines changed: 130 additions & 0 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
2+
import { createServer, Server as HTTPServer } from 'http';
3+
import { Server } from 'socket.io';
4+
import { io as Client, Socket as ClientSocket } from 'socket.io-client';
5+
import { setupSocket } from '../../src/sockets/index.js';
6+
import { CONFIG } from '../../src/config.js';
7+
import jwt from 'jsonwebtoken';
8+
import { LobbyService } from '../../src/services/lobby.service.js';
9+
import { CanvasService } from '../../src/services/canvas.service.js';
10+
import { canvasStore } from '../../src/store/canvas.store.js';
11+
12+
describe('Initial State Hydration Integration', () => {
13+
let io: Server;
14+
let httpServer: HTTPServer;
15+
let port: number;
16+
17+
const mockLobbyId = '507f1f77bcf86cd799439011';
18+
const userA = { id: 'user-a', username: 'Alice' };
19+
const userB = { id: 'user-b', username: 'Bob' };
20+
const tokenA = 'token-a';
21+
const tokenB = 'token-b';
22+
23+
const canvasWidth = 64;
24+
const canvasHeight = 64;
25+
const palette = ['#000000', '#FFFFFF'];
26+
27+
beforeAll(async () => {
28+
// Mock JWT verification
29+
vi.spyOn(jwt, 'verify').mockImplementation((token, secret, callback: any) => {
30+
if (token === tokenA) callback(null, userA);
31+
else if (token === tokenB) callback(null, userB);
32+
else callback(new Error('Invalid token'));
33+
});
34+
35+
// Mock LobbyService to allow joining
36+
vi.spyOn(LobbyService, 'getById').mockResolvedValue({
37+
_id: mockLobbyId,
38+
maxCollaborators: 10,
39+
bannedUsers: [],
40+
canvas: '507f191e810c19729de860ea'
41+
} as any);
42+
vi.spyOn(LobbyService, 'validateJoinAccess').mockImplementation(() => {});
43+
vi.spyOn(LobbyService, 'validateCapacity').mockImplementation(() => {});
44+
45+
// Mock CanvasService methods to avoid DB errors and unintended unloads (KISS)
46+
vi.spyOn(CanvasService, 'saveToDB').mockResolvedValue(undefined);
47+
vi.spyOn(CanvasService, 'unloadLobby').mockResolvedValue(undefined);
48+
49+
// Pre-load the lobby into memory
50+
const initialData = new Uint8Array(canvasWidth * canvasHeight).fill(0);
51+
canvasStore.loadLobbyToMemory(mockLobbyId, canvasWidth, canvasHeight, palette, initialData);
52+
53+
httpServer = createServer();
54+
io = new Server(httpServer);
55+
setupSocket(io);
56+
57+
return new Promise<void>((resolve) => {
58+
httpServer.listen(() => {
59+
const address = httpServer.address();
60+
port = typeof address === 'string' ? 0 : address?.port || 0;
61+
resolve();
62+
});
63+
});
64+
});
65+
66+
afterAll(() => {
67+
io.close();
68+
httpServer.close();
69+
canvasStore.removeLobby(mockLobbyId);
70+
vi.restoreAllMocks();
71+
});
72+
73+
const createClient = (token: string): Promise<ClientSocket> => {
74+
return new Promise((resolve, reject) => {
75+
const socket = Client(`http://localhost:${port}`, {
76+
auth: { token },
77+
transports: ['websocket'],
78+
});
79+
socket.on('connect', () => resolve(socket));
80+
socket.on('connect_error', (err) => reject(err));
81+
});
82+
};
83+
84+
const joinLobby = (socket: ClientSocket, lobbyId: string): Promise<any> => {
85+
return new Promise((resolve) => {
86+
socket.once(CONFIG.EVENTS.SERVER.INIT_STATE, (state) => resolve(state));
87+
socket.emit(CONFIG.EVENTS.CLIENT.JOIN_LOBBY, lobbyId);
88+
});
89+
};
90+
91+
it('should hydrate a new client with pixels drawn by a previous client', async () => {
92+
// 1. Connect Client A and join lobby
93+
const clientA = await createClient(tokenA);
94+
await joinLobby(clientA, mockLobbyId);
95+
96+
// 2. Client A draws a pixel at (10, 10) with color 1
97+
const x = 10;
98+
const y = 10;
99+
const color = 1;
100+
101+
await new Promise<void>((resolve) => {
102+
clientA.once(CONFIG.EVENTS.SERVER.PIXEL_UPDATE, (data) => {
103+
expect(data).toEqual({ x, y, color });
104+
resolve();
105+
});
106+
clientA.emit(CONFIG.EVENTS.CLIENT.DRAW, { lobbyId: mockLobbyId, x, y, color });
107+
});
108+
109+
// 3. Client A disconnects
110+
clientA.disconnect();
111+
112+
// Small delay to ensure server handles disconnection
113+
await new Promise(resolve => setTimeout(resolve, 100));
114+
115+
// 4. Connect Client B and join the same lobby
116+
const clientB = await createClient(tokenB);
117+
const state: any = await joinLobby(clientB, mockLobbyId);
118+
119+
// 5. Assertion: Client B receives the INIT_STATE with the pixel at (10, 10) as color 1
120+
expect(state.width).toBe(canvasWidth);
121+
expect(state.height).toBe(canvasHeight);
122+
123+
// Uint8Array might arrive as a Buffer/ArrayBuffer depending on the transport
124+
const pixelData = new Uint8Array(state.data);
125+
const index = y * canvasWidth + x;
126+
expect(pixelData[index]).toBe(color);
127+
128+
clientB.disconnect();
129+
});
130+
});

0 commit comments

Comments
 (0)