migrate from CRA to vite

This commit is contained in:
seavor 2026-04-12 18:35:13 -05:00
parent 98ce317ee1
commit 68e22d22bf
56 changed files with 5699 additions and 28288 deletions

View file

@ -1,26 +1,28 @@
jest.mock('./services/WebSocketService', () => ({
WebSocketService: jest.fn().mockImplementation(() => ({
message$: { subscribe: jest.fn() },
connect: jest.fn(),
testConnect: jest.fn(),
disconnect: jest.fn(),
vi.mock('./services/WebSocketService', () => ({
WebSocketService: vi.fn().mockImplementation(() => ({
message$: { subscribe: vi.fn() },
connect: vi.fn(),
testConnect: vi.fn(),
disconnect: vi.fn(),
})),
}));
jest.mock('./services/ProtobufService', () => ({
ProtobufService: jest.fn().mockImplementation(() => ({
handleMessageEvent: jest.fn(),
sendKeepAliveCommand: jest.fn(),
resetCommands: jest.fn(),
vi.mock('./services/ProtobufService', () => ({
ProtobufService: vi.fn().mockImplementation(() => ({
handleMessageEvent: vi.fn(),
sendKeepAliveCommand: vi.fn(),
resetCommands: vi.fn(),
})),
}));
jest.mock('./persistence', () => ({
RoomPersistence: { clearStore: jest.fn() },
SessionPersistence: { clearStore: jest.fn() },
vi.mock('./persistence', () => ({
RoomPersistence: { clearStore: vi.fn() },
SessionPersistence: { clearStore: vi.fn() },
}));
import { WebClient } from './WebClient';
import { WebSocketService } from './services/WebSocketService';
import { ProtobufService } from './services/ProtobufService';
import { RoomPersistence, SessionPersistence } from './persistence';
import { StatusEnum } from 'types';
import { Subject } from 'rxjs';
@ -30,28 +32,26 @@ describe('WebClient', () => {
let messageSubject: Subject<MessageEvent>;
beforeEach(() => {
jest.clearAllMocks();
const { ProtobufService } = require('./services/ProtobufService');
ProtobufService.mockImplementation(() => ({
handleMessageEvent: jest.fn(),
sendKeepAliveCommand: jest.fn(),
resetCommands: jest.fn(),
vi.clearAllMocks();
(ProtobufService as vi.Mock).mockImplementation(() => ({
handleMessageEvent: vi.fn(),
sendKeepAliveCommand: vi.fn(),
resetCommands: vi.fn(),
}));
messageSubject = new Subject<MessageEvent>();
const { WebSocketService } = require('./services/WebSocketService');
WebSocketService.mockImplementation(() => ({
(WebSocketService as vi.Mock).mockImplementation(() => ({
message$: messageSubject,
connect: jest.fn(),
testConnect: jest.fn(),
disconnect: jest.fn(),
connect: vi.fn(),
testConnect: vi.fn(),
disconnect: vi.fn(),
}));
// suppress console.log from constructor in non-test-env check
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(console, 'log').mockImplementation(() => {});
client = new WebClient();
});
afterEach(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});
describe('constructor', () => {
@ -94,7 +94,7 @@ describe('WebClient', () => {
describe('keepAlive', () => {
it('delegates to protobuf.sendKeepAliveCommand', () => {
const pingCb = jest.fn();
const pingCb = vi.fn();
client.keepAlive(pingCb);
expect(client.protobuf.sendKeepAliveCommand).toHaveBeenCalledWith(pingCb);
});

View file

@ -47,7 +47,7 @@ export class WebClient {
this.protobuf.handleMessageEvent(message);
});
if (process.env.NODE_ENV !== 'test') {
if (import.meta.env.MODE !== 'test') {
console.log(this);
}
}

View file

@ -1,13 +1,13 @@
/**
* Factory for invoking BackendService command callbacks in unit tests.
*
* @param mockFn - The jest.Mock for the BackendService send method
* (e.g. BackendService.sendSessionCommand as jest.Mock).
* @param mockFn - The vi.Mock for the BackendService send method
* (e.g. BackendService.sendSessionCommand as vi.Mock).
* @param optsArgIndex - Index of the options argument in the mock call.
* Defaults to 2 (commandName, params, options).
* Use 3 for sendRoomCommand (roomId, commandName, params, options).
*/
export function makeCallbackHelpers(mockFn: jest.Mock, optsArgIndex = 2) {
export function makeCallbackHelpers(mockFn: vi.Mock, optsArgIndex = 2) {
function getLastSendOpts() {
const calls = mockFn.mock.calls;
return calls[calls.length - 1]?.[optsArgIndex];

View file

@ -6,18 +6,18 @@
/** Builds a minimal mock of ProtoController.root */
export function makeMockProtoRoot() {
const encode = { finish: jest.fn().mockReturnValue(new Uint8Array()) };
const encode = { finish: vi.fn().mockReturnValue(new Uint8Array()) };
return {
CommandContainer: {
create: jest.fn(args => ({ ...args })),
encode: jest.fn().mockReturnValue(encode),
create: vi.fn(args => ({ ...args })),
encode: vi.fn().mockReturnValue(encode),
},
SessionCommand: { create: jest.fn(args => ({ ...args })) },
RoomCommand: { create: jest.fn(args => ({ ...args })) },
ModeratorCommand: { create: jest.fn(args => ({ ...args })) },
AdminCommand: { create: jest.fn(args => ({ ...args })) },
SessionCommand: { create: vi.fn(args => ({ ...args })) },
RoomCommand: { create: vi.fn(args => ({ ...args })) },
ModeratorCommand: { create: vi.fn(args => ({ ...args })) },
AdminCommand: { create: vi.fn(args => ({ ...args })) },
ServerMessage: {
decode: jest.fn(),
decode: vi.fn(),
MessageType: {
RESPONSE: 'RESPONSE',
ROOM_EVENT: 'ROOM_EVENT',
@ -52,8 +52,8 @@ export function makeMockProtoRoot() {
/** Builds a mock WebSocket instance */
export function makeMockWebSocketInstance() {
return {
send: jest.fn(),
close: jest.fn(),
send: vi.fn(),
close: vi.fn(),
readyState: WebSocket.OPEN,
binaryType: '' as BinaryType,
onopen: null as any,
@ -66,7 +66,7 @@ export function makeMockWebSocketInstance() {
/** Installs a mock WebSocket constructor on global. Returns the mock instance. */
export function installMockWebSocket() {
const mockInstance = makeMockWebSocketInstance();
const MockWS = jest.fn(() => mockInstance) as any;
const MockWS = vi.fn(() => mockInstance) as any;
MockWS.OPEN = 1;
MockWS.CLOSED = 3;
(global as any).WebSocket = MockWS;

View file

@ -1,10 +1,10 @@
/**
* Shared mock shape factories for session command specs.
*
* Usage inside jest.mock() factory callbacks (require is used because
* jest.mock() is hoisted above imports):
* Usage inside vi.mock() factory callbacks (require is used because
* vi.mock() is hoisted above imports):
*
* jest.mock('../../WebClient', () => {
* vi.mock('../../WebClient', () => {
* const { makeWebClientMock } = require('../../__mocks__/sessionCommandMocks');
* return { __esModule: true, default: makeWebClientMock() };
* });
@ -13,10 +13,10 @@
/** Superset WebClient mock — covers all properties used across both session spec files. */
export function makeWebClientMock() {
return {
connect: jest.fn(),
testConnect: jest.fn(),
disconnect: jest.fn(),
updateStatus: jest.fn(),
connect: vi.fn(),
testConnect: vi.fn(),
disconnect: vi.fn(),
updateStatus: vi.fn(),
clientConfig: { clientid: 'webatrice', clientver: '1.0', clientfeatures: [] },
options: {},
protocolVersion: 14,
@ -60,72 +60,72 @@ export function makeProtoControllerRootMock() {
/** Utils mock with unified return values. */
export function makeUtilsMock() {
return {
hashPassword: jest.fn().mockReturnValue('hashed_pw'),
generateSalt: jest.fn().mockReturnValue('randSalt'),
passwordSaltSupported: jest.fn().mockReturnValue(0),
hashPassword: vi.fn().mockReturnValue('hashed_pw'),
generateSalt: vi.fn().mockReturnValue('randSalt'),
passwordSaltSupported: vi.fn().mockReturnValue(0),
};
}
/** Superset SessionPersistence mock — covers all methods used across both session spec files. */
export function makeSessionPersistenceMock() {
return {
loginSuccessful: jest.fn(),
loginFailed: jest.fn(),
updateBuddyList: jest.fn(),
updateIgnoreList: jest.fn(),
updateUser: jest.fn(),
updateUsers: jest.fn(),
accountAwaitingActivation: jest.fn(),
accountActivationSuccess: jest.fn(),
accountActivationFailed: jest.fn(),
updateStatus: jest.fn(),
addToList: jest.fn(),
removeFromList: jest.fn(),
deleteServerDeck: jest.fn(),
deleteServerDeckDir: jest.fn(),
updateServerDecks: jest.fn(),
uploadServerDeck: jest.fn(),
createServerDeckDir: jest.fn(),
getGamesOfUser: jest.fn(),
getUserInfo: jest.fn(),
accountPasswordChange: jest.fn(),
accountEditChanged: jest.fn(),
accountImageChanged: jest.fn(),
replayList: jest.fn(),
replayAdded: jest.fn(),
replayModifyMatch: jest.fn(),
replayDeleteMatch: jest.fn(),
resetPasswordChallenge: jest.fn(),
resetPassword: jest.fn(),
resetPasswordFailed: jest.fn(),
resetPasswordSuccess: jest.fn(),
registrationFailed: jest.fn(),
registrationSuccess: jest.fn(),
registrationUserNameError: jest.fn(),
registrationPasswordError: jest.fn(),
registrationEmailError: jest.fn(),
registrationRequiresEmail: jest.fn(),
loginSuccessful: vi.fn(),
loginFailed: vi.fn(),
updateBuddyList: vi.fn(),
updateIgnoreList: vi.fn(),
updateUser: vi.fn(),
updateUsers: vi.fn(),
accountAwaitingActivation: vi.fn(),
accountActivationSuccess: vi.fn(),
accountActivationFailed: vi.fn(),
updateStatus: vi.fn(),
addToList: vi.fn(),
removeFromList: vi.fn(),
deleteServerDeck: vi.fn(),
deleteServerDeckDir: vi.fn(),
updateServerDecks: vi.fn(),
uploadServerDeck: vi.fn(),
createServerDeckDir: vi.fn(),
getGamesOfUser: vi.fn(),
getUserInfo: vi.fn(),
accountPasswordChange: vi.fn(),
accountEditChanged: vi.fn(),
accountImageChanged: vi.fn(),
replayList: vi.fn(),
replayAdded: vi.fn(),
replayModifyMatch: vi.fn(),
replayDeleteMatch: vi.fn(),
resetPasswordChallenge: vi.fn(),
resetPassword: vi.fn(),
resetPasswordFailed: vi.fn(),
resetPasswordSuccess: vi.fn(),
registrationFailed: vi.fn(),
registrationSuccess: vi.fn(),
registrationUserNameError: vi.fn(),
registrationPasswordError: vi.fn(),
registrationEmailError: vi.fn(),
registrationRequiresEmail: vi.fn(),
};
}
/**
* Session barrel mock pure jest.fn() map for all cross-command calls.
* Session barrel mock pure vi.fn() map for all cross-command calls.
* Used as-is by sessionCommands-complex.spec.ts, or spread over jest.requireActual
* by sessionCommands-simple.spec.ts to preserve real implementations for
* the commands under test.
*/
export function makeSessionBarrelMock() {
return {
login: jest.fn(),
register: jest.fn(),
activate: jest.fn(),
forgotPasswordReset: jest.fn(),
forgotPasswordRequest: jest.fn(),
forgotPasswordChallenge: jest.fn(),
requestPasswordSalt: jest.fn(),
listUsers: jest.fn(),
listRooms: jest.fn(),
updateStatus: jest.fn(),
disconnect: jest.fn(),
login: vi.fn(),
register: vi.fn(),
activate: vi.fn(),
forgotPasswordReset: vi.fn(),
forgotPasswordRequest: vi.fn(),
forgotPasswordChallenge: vi.fn(),
requestPasswordSalt: vi.fn(),
listUsers: vi.fn(),
listRooms: vi.fn(),
updateStatus: vi.fn(),
disconnect: vi.fn(),
};
}

View file

@ -1,33 +1,36 @@
jest.mock('../../services/BackendService', () => ({
vi.mock('../../services/BackendService', () => ({
BackendService: {
sendAdminCommand: jest.fn(),
sendAdminCommand: vi.fn(),
},
}));
jest.mock('../../persistence', () => ({
vi.mock('../../persistence', () => ({
AdminPersistence: {
adjustMod: jest.fn(),
reloadConfig: jest.fn(),
shutdownServer: jest.fn(),
updateServerMessage: jest.fn(),
adjustMod: vi.fn(),
reloadConfig: vi.fn(),
shutdownServer: vi.fn(),
updateServerMessage: vi.fn(),
},
}));
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
import { BackendService } from '../../services/BackendService';
import { AdminPersistence } from '../../persistence';
import { adjustMod } from './adjustMod';
import { reloadConfig } from './reloadConfig';
import { shutdownServer } from './shutdownServer';
import { updateServerMessage } from './updateServerMessage';
const { getLastSendOpts, invokeOnSuccess } = makeCallbackHelpers(
BackendService.sendAdminCommand as jest.Mock
BackendService.sendAdminCommand as vi.Mock
);
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
// ----------------------------------------------------------------
// adjustMod
// ----------------------------------------------------------------
describe('adjustMod', () => {
const { adjustMod } = jest.requireActual('./adjustMod');
it('calls sendAdminCommand with Command_AdjustMod', () => {
adjustMod('alice', true, false);
@ -49,7 +52,6 @@ describe('adjustMod', () => {
// reloadConfig
// ----------------------------------------------------------------
describe('reloadConfig', () => {
const { reloadConfig } = jest.requireActual('./reloadConfig');
it('calls sendAdminCommand with Command_ReloadConfig', () => {
reloadConfig();
@ -67,7 +69,6 @@ describe('reloadConfig', () => {
// shutdownServer
// ----------------------------------------------------------------
describe('shutdownServer', () => {
const { shutdownServer } = jest.requireActual('./shutdownServer');
it('calls sendAdminCommand with Command_ShutdownServer', () => {
shutdownServer('maintenance', 10);
@ -89,7 +90,6 @@ describe('shutdownServer', () => {
// updateServerMessage
// ----------------------------------------------------------------
describe('updateServerMessage', () => {
const { updateServerMessage } = jest.requireActual('./updateServerMessage');
it('calls sendAdminCommand with Command_UpdateServerMessage', () => {
updateServerMessage();

View file

@ -33,15 +33,15 @@ import { undoDraw } from './undoDraw';
import { unconcede } from './unconcede';
import { judge } from './judge';
jest.mock('../../services/BackendService', () => ({
BackendService: { sendGameCommand: jest.fn() },
vi.mock('../../services/BackendService', () => ({
BackendService: { sendGameCommand: vi.fn() },
}));
const gameId = 1;
const params = {} as any;
beforeEach(() => {
(BackendService.sendGameCommand as jest.Mock).mockClear();
(BackendService.sendGameCommand as vi.Mock).mockClear();
});
describe('Game commands — delegate to BackendService.sendGameCommand', () => {

View file

@ -1,39 +1,48 @@
jest.mock('../../services/BackendService', () => ({
vi.mock('../../services/BackendService', () => ({
BackendService: {
sendModeratorCommand: jest.fn(),
sendModeratorCommand: vi.fn(),
},
}));
jest.mock('../../persistence', () => ({
vi.mock('../../persistence', () => ({
ModeratorPersistence: {
banFromServer: jest.fn(),
forceActivateUser: jest.fn(),
getAdminNotes: jest.fn(),
banHistory: jest.fn(),
warnHistory: jest.fn(),
warnListOptions: jest.fn(),
grantReplayAccess: jest.fn(),
updateAdminNotes: jest.fn(),
viewLogs: jest.fn(),
warnUser: jest.fn(),
banFromServer: vi.fn(),
forceActivateUser: vi.fn(),
getAdminNotes: vi.fn(),
banHistory: vi.fn(),
warnHistory: vi.fn(),
warnListOptions: vi.fn(),
grantReplayAccess: vi.fn(),
updateAdminNotes: vi.fn(),
viewLogs: vi.fn(),
warnUser: vi.fn(),
},
}));
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
import { BackendService } from '../../services/BackendService';
import { ModeratorPersistence } from '../../persistence';
import { banFromServer } from './banFromServer';
import { forceActivateUser } from './forceActivateUser';
import { getAdminNotes } from './getAdminNotes';
import { getBanHistory } from './getBanHistory';
import { getWarnHistory } from './getWarnHistory';
import { getWarnList } from './getWarnList';
import { grantReplayAccess } from './grantReplayAccess';
import { updateAdminNotes } from './updateAdminNotes';
import { viewLogHistory } from './viewLogHistory';
import { warnUser } from './warnUser';
const { getLastSendOpts, invokeOnSuccess } = makeCallbackHelpers(
BackendService.sendModeratorCommand as jest.Mock
BackendService.sendModeratorCommand as vi.Mock
);
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
// ----------------------------------------------------------------
// banFromServer
// ----------------------------------------------------------------
describe('banFromServer', () => {
const { banFromServer } = jest.requireActual('./banFromServer');
it('calls sendModeratorCommand with Command_BanFromServer', () => {
banFromServer(30, 'alice', '1.2.3.4', 'reason', 'visible', 'cid', 1);
@ -55,7 +64,6 @@ describe('banFromServer', () => {
// forceActivateUser
// ----------------------------------------------------------------
describe('forceActivateUser', () => {
const { forceActivateUser } = jest.requireActual('./forceActivateUser');
it('calls sendModeratorCommand with Command_ForceActivateUser', () => {
forceActivateUser('alice', 'mod1');
@ -73,7 +81,6 @@ describe('forceActivateUser', () => {
// getAdminNotes
// ----------------------------------------------------------------
describe('getAdminNotes', () => {
const { getAdminNotes } = jest.requireActual('./getAdminNotes');
it('calls sendModeratorCommand with Command_GetAdminNotes', () => {
getAdminNotes('alice');
@ -96,7 +103,6 @@ describe('getAdminNotes', () => {
// getBanHistory
// ----------------------------------------------------------------
describe('getBanHistory', () => {
const { getBanHistory } = jest.requireActual('./getBanHistory');
it('calls sendModeratorCommand with Command_GetBanHistory', () => {
getBanHistory('alice');
@ -119,7 +125,6 @@ describe('getBanHistory', () => {
// getWarnHistory
// ----------------------------------------------------------------
describe('getWarnHistory', () => {
const { getWarnHistory } = jest.requireActual('./getWarnHistory');
it('calls sendModeratorCommand with Command_GetWarnHistory', () => {
getWarnHistory('alice');
@ -142,7 +147,6 @@ describe('getWarnHistory', () => {
// getWarnList
// ----------------------------------------------------------------
describe('getWarnList', () => {
const { getWarnList } = jest.requireActual('./getWarnList');
it('calls sendModeratorCommand with Command_GetWarnList', () => {
getWarnList('mod1', 'alice', 'US');
@ -165,7 +169,6 @@ describe('getWarnList', () => {
// grantReplayAccess
// ----------------------------------------------------------------
describe('grantReplayAccess', () => {
const { grantReplayAccess } = jest.requireActual('./grantReplayAccess');
it('calls sendModeratorCommand with Command_GrantReplayAccess', () => {
grantReplayAccess(10, 'mod1');
@ -183,7 +186,6 @@ describe('grantReplayAccess', () => {
// updateAdminNotes
// ----------------------------------------------------------------
describe('updateAdminNotes', () => {
const { updateAdminNotes } = jest.requireActual('./updateAdminNotes');
it('calls sendModeratorCommand with Command_UpdateAdminNotes', () => {
updateAdminNotes('alice', 'new notes');
@ -201,7 +203,6 @@ describe('updateAdminNotes', () => {
// viewLogHistory
// ----------------------------------------------------------------
describe('viewLogHistory', () => {
const { viewLogHistory } = jest.requireActual('./viewLogHistory');
it('calls sendModeratorCommand with Command_ViewLogHistory', () => {
viewLogHistory({ filters: 'all' } as any);
@ -224,7 +225,6 @@ describe('viewLogHistory', () => {
// warnUser
// ----------------------------------------------------------------
describe('warnUser', () => {
const { warnUser } = jest.requireActual('./warnUser');
it('calls sendModeratorCommand with Command_WarnUser', () => {
warnUser('alice', 'bad behavior', 'cid');

View file

@ -1,34 +1,37 @@
jest.mock('../../services/BackendService', () => ({
vi.mock('../../services/BackendService', () => ({
BackendService: {
sendRoomCommand: jest.fn(),
sendRoomCommand: vi.fn(),
},
}));
jest.mock('../../persistence', () => ({
vi.mock('../../persistence', () => ({
RoomPersistence: {
gameCreated: jest.fn(),
joinedGame: jest.fn(),
leaveRoom: jest.fn(),
gameCreated: vi.fn(),
joinedGame: vi.fn(),
leaveRoom: vi.fn(),
},
}));
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
import { BackendService } from '../../services/BackendService';
import { RoomPersistence } from '../../persistence';
import { createGame } from './createGame';
import { joinGame } from './joinGame';
import { leaveRoom } from './leaveRoom';
import { roomSay } from './roomSay';
const { getLastSendOpts, invokeOnSuccess } = makeCallbackHelpers(
BackendService.sendRoomCommand as jest.Mock,
BackendService.sendRoomCommand as vi.Mock,
// sendRoomCommand(roomId, commandName, params, options) — options at index 3
3
);
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
// ----------------------------------------------------------------
// createGame
// ----------------------------------------------------------------
describe('createGame', () => {
const { createGame } = jest.requireActual('./createGame');
it('calls sendRoomCommand with Command_CreateGame', () => {
createGame(5, { maxPlayers: 4 } as any);
@ -46,7 +49,6 @@ describe('createGame', () => {
// joinGame
// ----------------------------------------------------------------
describe('joinGame', () => {
const { joinGame } = jest.requireActual('./joinGame');
it('calls sendRoomCommand with Command_JoinGame', () => {
joinGame(7, { gameId: 42, password: '' } as any);
@ -64,7 +66,6 @@ describe('joinGame', () => {
// leaveRoom
// ----------------------------------------------------------------
describe('leaveRoom', () => {
const { leaveRoom } = jest.requireActual('./leaveRoom');
it('calls sendRoomCommand with Command_LeaveRoom', () => {
leaveRoom(3);
@ -82,7 +83,6 @@ describe('leaveRoom', () => {
// roomSay
// ----------------------------------------------------------------
describe('roomSay', () => {
const { roomSay } = jest.requireActual('./roomSay');
it('calls sendRoomCommand with trimmed message', () => {
roomSay(2, ' hello ');

View file

@ -1,38 +1,38 @@
// Tests for complex session commands that call webClient directly
// or have multiple branching callbacks.
jest.mock('../../services/BackendService', () => ({
vi.mock('../../services/BackendService', () => ({
BackendService: {
sendSessionCommand: jest.fn(),
sendSessionCommand: vi.fn(),
},
}));
jest.mock('../../persistence', () => {
const { makeSessionPersistenceMock } = require('../../__mocks__/sessionCommandMocks');
vi.mock('../../persistence', async () => {
const { makeSessionPersistenceMock } = await import('../../__mocks__/sessionCommandMocks');
return {
SessionPersistence: makeSessionPersistenceMock(),
RoomPersistence: {},
};
});
jest.mock('../../WebClient', () => {
const { makeWebClientMock } = require('../../__mocks__/sessionCommandMocks');
vi.mock('../../WebClient', async () => {
const { makeWebClientMock } = await import('../../__mocks__/sessionCommandMocks');
return { __esModule: true, default: makeWebClientMock() };
});
jest.mock('../../services/ProtoController', () => {
const { makeProtoControllerRootMock } = require('../../__mocks__/sessionCommandMocks');
vi.mock('../../services/ProtoController', async () => {
const { makeProtoControllerRootMock } = await import('../../__mocks__/sessionCommandMocks');
return { ProtoController: { root: makeProtoControllerRootMock() } };
});
jest.mock('../../utils', () => {
const { makeUtilsMock } = require('../../__mocks__/sessionCommandMocks');
vi.mock('../../utils', async () => {
const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks');
return makeUtilsMock();
});
// Intercept all re-exported commands to avoid recursive real invocations
jest.mock('./', () => {
const { makeSessionBarrelMock } = require('../../__mocks__/sessionCommandMocks');
vi.mock('./', async () => {
const { makeSessionBarrelMock } = await import('../../__mocks__/sessionCommandMocks');
return makeSessionBarrelMock();
});
@ -43,23 +43,31 @@ import webClient from '../../WebClient';
import * as SessionIndexMocks from './';
import { StatusEnum, WebSocketConnectReason } from 'types';
import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils';
import { connect } from './connect';
import { updateStatus } from './updateStatus';
import { login } from './login';
import { register } from './register';
import { activate } from './activate';
import { forgotPasswordChallenge } from './forgotPasswordChallenge';
import { forgotPasswordRequest } from './forgotPasswordRequest';
import { forgotPasswordReset } from './forgotPasswordReset';
import { requestPasswordSalt } from './requestPasswordSalt';
const { getLastSendOpts, invokeOnSuccess, invokeResponseCode, invokeOnError } = makeCallbackHelpers(
BackendService.sendSessionCommand as jest.Mock
BackendService.sendSessionCommand as vi.Mock
);
beforeEach(() => {
jest.clearAllMocks();
(hashPassword as jest.Mock).mockReturnValue('hashed_pw');
(generateSalt as jest.Mock).mockReturnValue('randSalt');
(passwordSaltSupported as jest.Mock).mockReturnValue(0);
vi.clearAllMocks();
(hashPassword as vi.Mock).mockReturnValue('hashed_pw');
(generateSalt as vi.Mock).mockReturnValue('randSalt');
(passwordSaltSupported as vi.Mock).mockReturnValue(0);
});
// ----------------------------------------------------------------
// connect.ts
// ----------------------------------------------------------------
describe('connect', () => {
const { connect } = jest.requireActual('./connect');
it('calls updateStatus CONNECTING for LOGIN reason', () => {
connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.LOGIN);
@ -108,7 +116,6 @@ describe('connect', () => {
// updateStatus.ts
// ----------------------------------------------------------------
describe('updateStatus', () => {
const { updateStatus } = jest.requireActual('./updateStatus');
it('calls SessionPersistence.updateStatus and webClient.updateStatus', () => {
updateStatus(StatusEnum.CONNECTED, 'OK');
@ -121,7 +128,6 @@ describe('updateStatus', () => {
// login.ts
// ----------------------------------------------------------------
describe('login', () => {
const { login } = jest.requireActual('./login');
it('sends Command_Login with plain password when no salt', () => {
login({ userName: 'alice' } as any, 'pw');
@ -167,7 +173,7 @@ describe('login', () => {
login({ userName: 'alice' } as any, 'secret');
const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } };
invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp });
const calledWith = (SessionPersistence.loginSuccessful as jest.Mock).mock.calls[0][0];
const calledWith = (SessionPersistence.loginSuccessful as vi.Mock).mock.calls[0][0];
expect(calledWith).not.toHaveProperty('password');
});
@ -175,7 +181,7 @@ describe('login', () => {
login({ userName: 'alice' } as any, 'pw', 'salt');
const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } };
invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp });
const calledWith = (SessionPersistence.loginSuccessful as jest.Mock).mock.calls[0][0];
const calledWith = (SessionPersistence.loginSuccessful as vi.Mock).mock.calls[0][0];
expect(calledWith).toHaveProperty('hashedPassword', 'hashed_pw');
});
@ -248,7 +254,6 @@ describe('login', () => {
// register.ts
// ----------------------------------------------------------------
describe('register', () => {
const { register } = jest.requireActual('./register');
it('sends Command_Register with plain password when no salt', () => {
register({ userName: 'alice', email: 'a@b.com', country: 'US', realName: 'Al' } as any, 'pw');
@ -350,7 +355,6 @@ describe('register', () => {
// activate.ts
// ----------------------------------------------------------------
describe('activate', () => {
const { activate } = jest.requireActual('./activate');
it('sends Command_Activate with userName and token, not password', () => {
activate({ userName: 'alice', token: 'tok' } as any, 'pw');
@ -385,7 +389,6 @@ describe('activate', () => {
// forgotPasswordChallenge.ts
// ----------------------------------------------------------------
describe('forgotPasswordChallenge', () => {
const { forgotPasswordChallenge } = jest.requireActual('./forgotPasswordChallenge');
it('sends Command_ForgotPasswordChallenge', () => {
forgotPasswordChallenge({ userName: 'alice', email: 'a@b.com' } as any);
@ -413,7 +416,6 @@ describe('forgotPasswordChallenge', () => {
// forgotPasswordRequest.ts
// ----------------------------------------------------------------
describe('forgotPasswordRequest', () => {
const { forgotPasswordRequest } = jest.requireActual('./forgotPasswordRequest');
it('sends Command_ForgotPasswordRequest', () => {
forgotPasswordRequest({ userName: 'alice' } as any);
@ -448,7 +450,6 @@ describe('forgotPasswordRequest', () => {
// forgotPasswordReset.ts
// ----------------------------------------------------------------
describe('forgotPasswordReset', () => {
const { forgotPasswordReset } = jest.requireActual('./forgotPasswordReset');
it('sends Command_ForgotPasswordReset with plain newPassword when no salt', () => {
forgotPasswordReset({ userName: 'alice', token: 'tok' } as any, 'newpw');
@ -487,7 +488,6 @@ describe('forgotPasswordReset', () => {
// requestPasswordSalt.ts
// ----------------------------------------------------------------
describe('requestPasswordSalt', () => {
const { requestPasswordSalt } = jest.requireActual('./requestPasswordSalt');
it('sends Command_RequestPasswordSalt', () => {
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw');

View file

@ -1,39 +1,39 @@
// Shared mock setup for session command tests
jest.mock('../../services/BackendService', () => ({
vi.mock('../../services/BackendService', () => ({
BackendService: {
sendSessionCommand: jest.fn(),
sendSessionCommand: vi.fn(),
},
}));
jest.mock('../../persistence', () => {
const { makeSessionPersistenceMock } = require('../../__mocks__/sessionCommandMocks');
vi.mock('../../persistence', async () => {
const { makeSessionPersistenceMock } = await import('../../__mocks__/sessionCommandMocks');
return {
SessionPersistence: makeSessionPersistenceMock(),
RoomPersistence: { joinRoom: jest.fn() },
RoomPersistence: { joinRoom: vi.fn() },
};
});
jest.mock('../../WebClient', () => {
const { makeWebClientMock } = require('../../__mocks__/sessionCommandMocks');
vi.mock('../../WebClient', async () => {
const { makeWebClientMock } = await import('../../__mocks__/sessionCommandMocks');
return { __esModule: true, default: makeWebClientMock() };
});
jest.mock('../../services/ProtoController', () => {
const { makeProtoControllerRootMock } = require('../../__mocks__/sessionCommandMocks');
vi.mock('../../services/ProtoController', async () => {
const { makeProtoControllerRootMock } = await import('../../__mocks__/sessionCommandMocks');
return { ProtoController: { root: makeProtoControllerRootMock() } };
});
jest.mock('../../utils', () => {
const { makeUtilsMock } = require('../../__mocks__/sessionCommandMocks');
vi.mock('../../utils', async () => {
const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks');
return makeUtilsMock();
});
// Mock session commands barrel to allow cross-command calls while keeping real implementations
jest.mock('./', () => {
const actual = jest.requireActual('./');
const { makeSessionBarrelMock } = require('../../__mocks__/sessionCommandMocks');
return { ...actual, ...makeSessionBarrelMock() };
vi.mock('./', async () => {
const actual = await vi.importActual('./');
const { makeSessionBarrelMock } = await import('../../__mocks__/sessionCommandMocks');
return { ...(actual as any), ...makeSessionBarrelMock() };
});
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
@ -43,23 +43,45 @@ import { RoomPersistence } from '../../persistence';
import webClient from '../../WebClient';
import * as SessionCommands from './';
import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils';
import { accountEdit } from './accountEdit';
import { accountImage } from './accountImage';
import { accountPassword } from './accountPassword';
import { deckDel } from './deckDel';
import { deckDelDir } from './deckDelDir';
import { deckList } from './deckList';
import { deckNewDir } from './deckNewDir';
import { deckUpload } from './deckUpload';
import { disconnect } from './disconnect';
import { getGamesOfUser } from './getGamesOfUser';
import { getUserInfo } from './getUserInfo';
import { joinRoom } from './joinRoom';
import { listRooms } from './listRooms';
import { listUsers } from './listUsers';
import { message } from './message';
import { ping } from './ping';
import { replayDeleteMatch } from './replayDeleteMatch';
import { replayList } from './replayList';
import { replayModifyMatch } from './replayModifyMatch';
import { addToList, addToBuddyList, addToIgnoreList } from './addToList';
import { removeFromList, removeFromBuddyList, removeFromIgnoreList } from './removeFromList';
import { replayGetCode } from './replayGetCode';
import { replaySubmitCode } from './replaySubmitCode';
const { invokeOnSuccess, invokeCallback } = makeCallbackHelpers(
BackendService.sendSessionCommand as jest.Mock
BackendService.sendSessionCommand as vi.Mock
);
beforeEach(() => {
jest.clearAllMocks();
(hashPassword as jest.Mock).mockReturnValue('hashed_pw');
(generateSalt as jest.Mock).mockReturnValue('randSalt');
(passwordSaltSupported as jest.Mock).mockReturnValue(0);
vi.clearAllMocks();
(hashPassword as vi.Mock).mockReturnValue('hashed_pw');
(generateSalt as vi.Mock).mockReturnValue('randSalt');
(passwordSaltSupported as vi.Mock).mockReturnValue(0);
});
// ----------------------------------------------------------------
describe('accountEdit', () => {
const { accountEdit } = jest.requireActual('./accountEdit');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_AccountEdit with correct params', () => {
accountEdit('pw', 'Alice', 'a@b.com', 'US');
@ -78,8 +100,7 @@ describe('accountEdit', () => {
});
describe('accountImage', () => {
const { accountImage } = jest.requireActual('./accountImage');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_AccountImage', () => {
const img = new Uint8Array([1, 2]);
@ -96,8 +117,7 @@ describe('accountImage', () => {
});
describe('accountPassword', () => {
const { accountPassword } = jest.requireActual('./accountPassword');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_AccountPassword', () => {
accountPassword('old', 'new', 'hashed');
@ -116,8 +136,7 @@ describe('accountPassword', () => {
});
describe('deckDel', () => {
const { deckDel } = jest.requireActual('./deckDel');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_DeckDel', () => {
deckDel(42);
@ -132,8 +151,7 @@ describe('deckDel', () => {
});
describe('deckDelDir', () => {
const { deckDelDir } = jest.requireActual('./deckDelDir');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_DeckDelDir', () => {
deckDelDir('/path');
@ -148,8 +166,7 @@ describe('deckDelDir', () => {
});
describe('deckList', () => {
const { deckList } = jest.requireActual('./deckList');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_DeckList', () => {
deckList();
@ -165,8 +182,7 @@ describe('deckList', () => {
});
describe('deckNewDir', () => {
const { deckNewDir } = jest.requireActual('./deckNewDir');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_DeckNewDir', () => {
deckNewDir('/path', 'dir');
@ -183,8 +199,7 @@ describe('deckNewDir', () => {
});
describe('deckUpload', () => {
const { deckUpload } = jest.requireActual('./deckUpload');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_DeckUpload', () => {
deckUpload('/path', 1, 'content');
@ -204,8 +219,7 @@ describe('deckUpload', () => {
});
describe('disconnect', () => {
const { disconnect } = jest.requireActual('./disconnect');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('calls webClient.disconnect', () => {
disconnect();
@ -214,8 +228,7 @@ describe('disconnect', () => {
});
describe('getGamesOfUser', () => {
const { getGamesOfUser } = jest.requireActual('./getGamesOfUser');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_GetGamesOfUser', () => {
getGamesOfUser('alice');
@ -231,8 +244,7 @@ describe('getGamesOfUser', () => {
});
describe('getUserInfo', () => {
const { getUserInfo } = jest.requireActual('./getUserInfo');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_GetUserInfo', () => {
getUserInfo('alice');
@ -248,8 +260,7 @@ describe('getUserInfo', () => {
});
describe('joinRoom', () => {
const { joinRoom } = jest.requireActual('./joinRoom');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_JoinRoom', () => {
joinRoom(5);
@ -265,8 +276,7 @@ describe('joinRoom', () => {
});
describe('listRooms (command)', () => {
const { listRooms } = jest.requireActual('./listRooms');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_ListRooms', () => {
listRooms();
@ -275,8 +285,7 @@ describe('listRooms (command)', () => {
});
describe('listUsers', () => {
const { listUsers } = jest.requireActual('./listUsers');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_ListUsers', () => {
listUsers();
@ -292,8 +301,7 @@ describe('listUsers', () => {
});
describe('message', () => {
const { message } = jest.requireActual('./message');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_Message', () => {
message('bob', 'hi');
@ -305,17 +313,16 @@ describe('message', () => {
});
describe('ping', () => {
const { ping } = jest.requireActual('./ping');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_Ping', () => {
const pingReceived = jest.fn();
const pingReceived = vi.fn();
ping(pingReceived);
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_Ping', {}, expect.any(Object));
});
it('calls pingReceived via onResponse', () => {
const pingReceived = jest.fn();
const pingReceived = vi.fn();
ping(pingReceived);
const raw = {};
invokeCallback('onResponse', raw);
@ -324,8 +331,7 @@ describe('ping', () => {
});
describe('replayDeleteMatch', () => {
const { replayDeleteMatch } = jest.requireActual('./replayDeleteMatch');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_ReplayDeleteMatch', () => {
replayDeleteMatch(7);
@ -340,8 +346,7 @@ describe('replayDeleteMatch', () => {
});
describe('replayList', () => {
const { replayList } = jest.requireActual('./replayList');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_ReplayList', () => {
replayList();
@ -357,8 +362,7 @@ describe('replayList', () => {
});
describe('replayModifyMatch', () => {
const { replayModifyMatch } = jest.requireActual('./replayModifyMatch');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_ReplayModifyMatch', () => {
replayModifyMatch(7, true);
@ -375,8 +379,7 @@ describe('replayModifyMatch', () => {
});
describe('addToList / addToBuddyList / addToIgnoreList', () => {
const { addToList, addToBuddyList, addToIgnoreList } = jest.requireActual('./addToList');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('addToBuddyList sends Command_AddToList with list=buddy', () => {
addToBuddyList('alice');
@ -400,8 +403,7 @@ describe('addToList / addToBuddyList / addToIgnoreList', () => {
});
describe('removeFromList / removeFromBuddyList / removeFromIgnoreList', () => {
const { removeFromList, removeFromBuddyList, removeFromIgnoreList } = jest.requireActual('./removeFromList');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('removeFromBuddyList sends Command_RemoveFromList with list=buddy', () => {
removeFromBuddyList('alice');
@ -425,11 +427,10 @@ describe('removeFromList / removeFromBuddyList / removeFromIgnoreList', () => {
});
describe('replayGetCode', () => {
const { replayGetCode } = jest.requireActual('./replayGetCode');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_ReplayGetCode with gameId and responseName', () => {
replayGetCode(42, jest.fn());
replayGetCode(42, vi.fn());
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
'Command_ReplayGetCode',
{ gameId: 42 },
@ -438,7 +439,7 @@ describe('replayGetCode', () => {
});
it('calls onCodeReceived with replayCode on success', () => {
const onCodeReceived = jest.fn();
const onCodeReceived = vi.fn();
replayGetCode(42, onCodeReceived);
invokeOnSuccess({ replayCode: 'abc123-xyz' });
expect(onCodeReceived).toHaveBeenCalledWith('abc123-xyz');
@ -446,8 +447,7 @@ describe('replayGetCode', () => {
});
describe('replaySubmitCode', () => {
const { replaySubmitCode } = jest.requireActual('./replaySubmitCode');
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it('sends Command_ReplaySubmitCode with replayCode', () => {
replaySubmitCode('42-abc123');
@ -459,14 +459,14 @@ describe('replaySubmitCode', () => {
});
it('forwards onSuccess callback', () => {
const onSuccess = jest.fn();
const onSuccess = vi.fn();
replaySubmitCode('42-abc123', onSuccess);
invokeOnSuccess();
expect(onSuccess).toHaveBeenCalled();
});
it('forwards onError callback', () => {
const onError = jest.fn();
const onError = vi.fn();
replaySubmitCode('42-abc123', undefined, onError);
invokeCallback('onError', 404);
expect(onError).toHaveBeenCalledWith(404);

View file

@ -1,34 +1,34 @@
jest.mock('../../persistence', () => ({
vi.mock('../../persistence', () => ({
GamePersistence: {
gameStateChanged: jest.fn(),
playerJoined: jest.fn(),
playerLeft: jest.fn(),
playerPropertiesChanged: jest.fn(),
gameClosed: jest.fn(),
gameHostChanged: jest.fn(),
kicked: jest.fn(),
gameSay: jest.fn(),
cardMoved: jest.fn(),
cardFlipped: jest.fn(),
cardDestroyed: jest.fn(),
cardAttached: jest.fn(),
tokenCreated: jest.fn(),
cardAttrChanged: jest.fn(),
cardCounterChanged: jest.fn(),
arrowCreated: jest.fn(),
arrowDeleted: jest.fn(),
counterCreated: jest.fn(),
counterSet: jest.fn(),
counterDeleted: jest.fn(),
cardsDrawn: jest.fn(),
cardsRevealed: jest.fn(),
zoneShuffled: jest.fn(),
dieRolled: jest.fn(),
activePlayerSet: jest.fn(),
activePhaseSet: jest.fn(),
turnReversed: jest.fn(),
zoneDumped: jest.fn(),
zonePropertiesChanged: jest.fn(),
gameStateChanged: vi.fn(),
playerJoined: vi.fn(),
playerLeft: vi.fn(),
playerPropertiesChanged: vi.fn(),
gameClosed: vi.fn(),
gameHostChanged: vi.fn(),
kicked: vi.fn(),
gameSay: vi.fn(),
cardMoved: vi.fn(),
cardFlipped: vi.fn(),
cardDestroyed: vi.fn(),
cardAttached: vi.fn(),
tokenCreated: vi.fn(),
cardAttrChanged: vi.fn(),
cardCounterChanged: vi.fn(),
arrowCreated: vi.fn(),
arrowDeleted: vi.fn(),
counterCreated: vi.fn(),
counterSet: vi.fn(),
counterDeleted: vi.fn(),
cardsDrawn: vi.fn(),
cardsRevealed: vi.fn(),
zoneShuffled: vi.fn(),
dieRolled: vi.fn(),
activePlayerSet: vi.fn(),
activePhaseSet: vi.fn(),
turnReversed: vi.fn(),
zoneDumped: vi.fn(),
zonePropertiesChanged: vi.fn(),
},
}));
@ -63,7 +63,7 @@ import { setCardCounter } from './setCardCounter';
import { setCounter } from './setCounter';
import { shuffle } from './shuffle';
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
const meta = { gameId: 5, playerId: 2, context: null, secondsElapsed: 0, forcedByJudge: 0 };

View file

@ -1,21 +1,25 @@
jest.mock('../../persistence', () => ({
vi.mock('../../persistence', () => ({
RoomPersistence: {
userJoined: jest.fn(),
userLeft: jest.fn(),
updateGames: jest.fn(),
removeMessages: jest.fn(),
addMessage: jest.fn(),
userJoined: vi.fn(),
userLeft: vi.fn(),
updateGames: vi.fn(),
removeMessages: vi.fn(),
addMessage: vi.fn(),
},
}));
import { RoomPersistence } from '../../persistence';
import { joinRoom } from './joinRoom';
import { leaveRoom } from './leaveRoom';
import { listGames } from './listGames';
import { removeMessages } from './removeMessages';
import { roomSay } from './roomSay';
const makeRoomEvent = (roomId: number) => ({ roomEvent: { roomId } });
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
describe('joinRoom room event', () => {
const { joinRoom } = jest.requireActual('./joinRoom');
it('calls RoomPersistence.userJoined with roomId and userInfo', () => {
const userInfo = { name: 'alice' } as any;
@ -25,7 +29,6 @@ describe('joinRoom room event', () => {
});
describe('leaveRoom room event', () => {
const { leaveRoom } = jest.requireActual('./leaveRoom');
it('calls RoomPersistence.userLeft with roomId and name', () => {
leaveRoom({ name: 'alice' }, makeRoomEvent(4));
@ -34,7 +37,6 @@ describe('leaveRoom room event', () => {
});
describe('listGames room event', () => {
const { listGames } = jest.requireActual('./listGames');
it('calls RoomPersistence.updateGames with roomId and gameList', () => {
const gameList = [{ gameId: 1 }] as any;
@ -44,7 +46,6 @@ describe('listGames room event', () => {
});
describe('removeMessages room event', () => {
const { removeMessages } = jest.requireActual('./removeMessages');
it('calls RoomPersistence.removeMessages with roomId, name, amount', () => {
removeMessages({ name: 'bob', amount: 10 }, makeRoomEvent(6));
@ -53,7 +54,6 @@ describe('removeMessages room event', () => {
});
describe('roomSay room event', () => {
const { roomSay } = jest.requireActual('./roomSay');
it('calls RoomPersistence.addMessage with roomId and message', () => {
const msg = { text: 'hello' } as any;

View file

@ -1,30 +1,30 @@
// Tests for simple session events that delegate 1:1 to SessionPersistence
// or RoomPersistence with minimal logic.
jest.mock('../../persistence', () => ({
vi.mock('../../persistence', () => ({
SessionPersistence: {
gameJoined: jest.fn(),
notifyUser: jest.fn(),
replayAdded: jest.fn(),
serverMessage: jest.fn(),
serverShutdown: jest.fn(),
updateUsers: jest.fn(),
updateInfo: jest.fn(),
userJoined: jest.fn(),
userLeft: jest.fn(),
userMessage: jest.fn(),
addToBuddyList: jest.fn(),
addToIgnoreList: jest.fn(),
removeFromBuddyList: jest.fn(),
removeFromIgnoreList: jest.fn(),
playerPropertiesChanged: jest.fn(),
gameJoined: vi.fn(),
notifyUser: vi.fn(),
replayAdded: vi.fn(),
serverMessage: vi.fn(),
serverShutdown: vi.fn(),
updateUsers: vi.fn(),
updateInfo: vi.fn(),
userJoined: vi.fn(),
userLeft: vi.fn(),
userMessage: vi.fn(),
addToBuddyList: vi.fn(),
addToIgnoreList: vi.fn(),
removeFromBuddyList: vi.fn(),
removeFromIgnoreList: vi.fn(),
playerPropertiesChanged: vi.fn(),
},
RoomPersistence: {
updateRooms: jest.fn(),
updateRooms: vi.fn(),
},
}));
jest.mock('../../WebClient', () => ({
vi.mock('../../WebClient', () => ({
__esModule: true,
default: {
clientOptions: { autojoinrooms: false },
@ -33,25 +33,25 @@ jest.mock('../../WebClient', () => ({
},
}));
jest.mock('../../commands/session', () => ({
joinRoom: jest.fn(),
updateStatus: jest.fn(),
disconnect: jest.fn(),
login: jest.fn(),
register: jest.fn(),
activate: jest.fn(),
requestPasswordSalt: jest.fn(),
forgotPasswordRequest: jest.fn(),
forgotPasswordChallenge: jest.fn(),
forgotPasswordReset: jest.fn(),
vi.mock('../../commands/session', () => ({
joinRoom: vi.fn(),
updateStatus: vi.fn(),
disconnect: vi.fn(),
login: vi.fn(),
register: vi.fn(),
activate: vi.fn(),
requestPasswordSalt: vi.fn(),
forgotPasswordRequest: vi.fn(),
forgotPasswordChallenge: vi.fn(),
forgotPasswordReset: vi.fn(),
}));
jest.mock('../../utils', () => ({
generateSalt: jest.fn().mockReturnValue('newSalt'),
passwordSaltSupported: jest.fn().mockReturnValue(0),
vi.mock('../../utils', () => ({
generateSalt: vi.fn().mockReturnValue('newSalt'),
passwordSaltSupported: vi.fn().mockReturnValue(0),
}));
jest.mock('../../services/ProtoController', () => ({
vi.mock('../../services/ProtoController', () => ({
ProtoController: {
root: {
Event_ConnectionClosed: {
@ -76,18 +76,31 @@ import { SessionPersistence, RoomPersistence } from '../../persistence';
import webClient from '../../WebClient';
import * as SessionCmds from '../../commands/session';
import * as Utils from '../../utils';
import { gameJoined } from './gameJoined';
import { notifyUser } from './notifyUser';
import { replayAdded } from './replayAdded';
import { serverCompleteList } from './serverCompleteList';
import { serverMessage } from './serverMessage';
import { serverShutdown } from './serverShutdown';
import { userJoined } from './userJoined';
import { userLeft } from './userLeft';
import { userMessage } from './userMessage';
import { addToList } from './addToList';
import { removeFromList } from './removeFromList';
import { listRooms } from './listRooms';
import { connectionClosed } from './connectionClosed';
import { serverIdentification } from './serverIdentification';
beforeEach(() => {
jest.clearAllMocks();
(Utils.generateSalt as jest.Mock).mockReturnValue('newSalt');
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
vi.clearAllMocks();
(Utils.generateSalt as vi.Mock).mockReturnValue('newSalt');
(Utils.passwordSaltSupported as vi.Mock).mockReturnValue(0);
});
// ----------------------------------------------------------------
// gameJoined
// ----------------------------------------------------------------
describe('gameJoined', () => {
const { gameJoined } = jest.requireActual('./gameJoined');
it('calls SessionPersistence.gameJoined', () => {
const data = { gameId: 1 } as any;
@ -100,7 +113,6 @@ describe('gameJoined', () => {
// notifyUser
// ----------------------------------------------------------------
describe('notifyUser', () => {
const { notifyUser } = jest.requireActual('./notifyUser');
it('calls SessionPersistence.notifyUser', () => {
const data = { message: 'yo' } as any;
@ -113,7 +125,6 @@ describe('notifyUser', () => {
// replayAdded
// ----------------------------------------------------------------
describe('replayAdded', () => {
const { replayAdded } = jest.requireActual('./replayAdded');
it('calls SessionPersistence.replayAdded with matchInfo', () => {
replayAdded({ matchInfo: { id: 42 } } as any);
@ -125,7 +136,6 @@ describe('replayAdded', () => {
// serverCompleteList
// ----------------------------------------------------------------
describe('serverCompleteList', () => {
const { serverCompleteList } = jest.requireActual('./serverCompleteList');
it('calls SessionPersistence.updateUsers and RoomPersistence.updateRooms', () => {
serverCompleteList({ userList: ['u'], roomList: ['r'] } as any);
@ -138,7 +148,6 @@ describe('serverCompleteList', () => {
// serverMessage
// ----------------------------------------------------------------
describe('serverMessage', () => {
const { serverMessage } = jest.requireActual('./serverMessage');
it('calls SessionPersistence.serverMessage with message', () => {
serverMessage({ message: 'hello server' });
@ -150,7 +159,6 @@ describe('serverMessage', () => {
// serverShutdown
// ----------------------------------------------------------------
describe('serverShutdown', () => {
const { serverShutdown } = jest.requireActual('./serverShutdown');
it('calls SessionPersistence.serverShutdown', () => {
const payload = { reason: 'maintenance' } as any;
@ -163,7 +171,6 @@ describe('serverShutdown', () => {
// userJoined
// ----------------------------------------------------------------
describe('userJoined', () => {
const { userJoined } = jest.requireActual('./userJoined');
it('calls SessionPersistence.userJoined with userInfo', () => {
userJoined({ userInfo: { name: 'alice' } } as any);
@ -175,7 +182,6 @@ describe('userJoined', () => {
// userLeft
// ----------------------------------------------------------------
describe('userLeft', () => {
const { userLeft } = jest.requireActual('./userLeft');
it('calls SessionPersistence.userLeft with name', () => {
userLeft({ name: 'bob' });
@ -187,7 +193,6 @@ describe('userLeft', () => {
// userMessage
// ----------------------------------------------------------------
describe('userMessage', () => {
const { userMessage } = jest.requireActual('./userMessage');
it('calls SessionPersistence.userMessage', () => {
const payload = { userName: 'alice', message: 'hi' } as any;
@ -200,8 +205,7 @@ describe('userMessage', () => {
// addToList
// ----------------------------------------------------------------
describe('addToList', () => {
const { addToList } = jest.requireActual('./addToList');
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
afterAll(() => logSpy.mockRestore());
it('buddy list → addToBuddyList', () => {
@ -224,7 +228,6 @@ describe('addToList', () => {
// removeFromList
// ----------------------------------------------------------------
describe('removeFromList', () => {
const { removeFromList } = jest.requireActual('./removeFromList');
it('buddy list → removeFromBuddyList', () => {
removeFromList({ listName: 'buddy', userName: 'alice' } as any);
@ -237,7 +240,7 @@ describe('removeFromList', () => {
});
it('unknown list → console.log', () => {
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
removeFromList({ listName: 'other', userName: 'x' } as any);
expect(logSpy).toHaveBeenCalled();
logSpy.mockRestore();
@ -248,7 +251,6 @@ describe('removeFromList', () => {
// listRooms
// ----------------------------------------------------------------
describe('listRooms', () => {
const { listRooms } = jest.requireActual('./listRooms');
it('calls RoomPersistence.updateRooms', () => {
listRooms({ roomList: [] });
@ -273,7 +275,6 @@ describe('listRooms', () => {
// connectionClosed
// ----------------------------------------------------------------
describe('connectionClosed', () => {
const { connectionClosed } = jest.requireActual('./connectionClosed');
it('uses reasonStr when provided', () => {
connectionClosed({ reason: 0, reasonStr: 'custom' } as any);
@ -361,7 +362,6 @@ describe('connectionClosed', () => {
// serverIdentification
// ----------------------------------------------------------------
describe('serverIdentification', () => {
const { serverIdentification } = jest.requireActual('./serverIdentification');
beforeEach(() => {
(webClient as any).protocolVersion = 14;
@ -376,7 +376,7 @@ describe('serverIdentification', () => {
it('LOGIN reason without salt → calls login with password as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.LOGIN, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
(Utils.passwordSaltSupported as vi.Mock).mockReturnValue(0);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.login).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
@ -386,7 +386,7 @@ describe('serverIdentification', () => {
it('LOGIN reason with salt → calls requestPasswordSalt with password as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.LOGIN, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
(Utils.passwordSaltSupported as vi.Mock).mockReturnValue(1);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
@ -396,7 +396,7 @@ describe('serverIdentification', () => {
it('REGISTER reason without salt → calls register with password and null salt', () => {
(webClient as any).options = { reason: WebSocketConnectReason.REGISTER, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
(Utils.passwordSaltSupported as vi.Mock).mockReturnValue(0);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.register).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
@ -407,7 +407,7 @@ describe('serverIdentification', () => {
it('REGISTER reason with salt → calls register with password and generated salt', () => {
(webClient as any).options = { reason: WebSocketConnectReason.REGISTER, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
(Utils.passwordSaltSupported as vi.Mock).mockReturnValue(1);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
expect(SessionCmds.register).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
@ -418,7 +418,7 @@ describe('serverIdentification', () => {
it('ACTIVATE_ACCOUNT reason without salt → calls activate with password as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
(Utils.passwordSaltSupported as vi.Mock).mockReturnValue(0);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.activate).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
@ -428,7 +428,7 @@ describe('serverIdentification', () => {
it('ACTIVATE_ACCOUNT reason with salt → calls requestPasswordSalt with password as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
(Utils.passwordSaltSupported as vi.Mock).mockReturnValue(1);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
@ -450,7 +450,7 @@ describe('serverIdentification', () => {
it('PASSWORD_RESET reason without salt → calls forgotPasswordReset with newPassword as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
(Utils.passwordSaltSupported as vi.Mock).mockReturnValue(0);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.forgotPasswordReset).toHaveBeenCalledWith(
expect.not.objectContaining({ newPassword: expect.anything() }),
@ -460,7 +460,7 @@ describe('serverIdentification', () => {
it('PASSWORD_RESET reason with salt → calls requestPasswordSalt with newPassword as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
(Utils.passwordSaltSupported as vi.Mock).mockReturnValue(1);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith(
expect.not.objectContaining({ newPassword: expect.anything() }),

View file

@ -1,9 +1,9 @@
jest.mock('store', () => ({
vi.mock('store', () => ({
ServerDispatch: {
adjustMod: jest.fn(),
reloadConfig: jest.fn(),
shutdownServer: jest.fn(),
updateServerMessage: jest.fn(),
adjustMod: vi.fn(),
reloadConfig: vi.fn(),
shutdownServer: vi.fn(),
updateServerMessage: vi.fn(),
},
}));
@ -11,7 +11,7 @@ import { AdminPersistence } from './AdminPersistence';
import { ServerDispatch } from 'store';
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
describe('AdminPersistence', () => {

View file

@ -1,42 +1,42 @@
import { GamePersistence } from './GamePersistence';
jest.mock('store', () => ({
vi.mock('store', () => ({
GameDispatch: {
gameStateChanged: jest.fn(),
playerJoined: jest.fn(),
playerLeft: jest.fn(),
playerPropertiesChanged: jest.fn(),
gameClosed: jest.fn(),
gameHostChanged: jest.fn(),
kicked: jest.fn(),
gameSay: jest.fn(),
cardMoved: jest.fn(),
cardFlipped: jest.fn(),
cardDestroyed: jest.fn(),
cardAttached: jest.fn(),
tokenCreated: jest.fn(),
cardAttrChanged: jest.fn(),
cardCounterChanged: jest.fn(),
arrowCreated: jest.fn(),
arrowDeleted: jest.fn(),
counterCreated: jest.fn(),
counterSet: jest.fn(),
counterDeleted: jest.fn(),
cardsDrawn: jest.fn(),
cardsRevealed: jest.fn(),
zoneShuffled: jest.fn(),
dieRolled: jest.fn(),
activePlayerSet: jest.fn(),
activePhaseSet: jest.fn(),
turnReversed: jest.fn(),
zoneDumped: jest.fn(),
zonePropertiesChanged: jest.fn(),
gameStateChanged: vi.fn(),
playerJoined: vi.fn(),
playerLeft: vi.fn(),
playerPropertiesChanged: vi.fn(),
gameClosed: vi.fn(),
gameHostChanged: vi.fn(),
kicked: vi.fn(),
gameSay: vi.fn(),
cardMoved: vi.fn(),
cardFlipped: vi.fn(),
cardDestroyed: vi.fn(),
cardAttached: vi.fn(),
tokenCreated: vi.fn(),
cardAttrChanged: vi.fn(),
cardCounterChanged: vi.fn(),
arrowCreated: vi.fn(),
arrowDeleted: vi.fn(),
counterCreated: vi.fn(),
counterSet: vi.fn(),
counterDeleted: vi.fn(),
cardsDrawn: vi.fn(),
cardsRevealed: vi.fn(),
zoneShuffled: vi.fn(),
dieRolled: vi.fn(),
activePlayerSet: vi.fn(),
activePhaseSet: vi.fn(),
turnReversed: vi.fn(),
zoneDumped: vi.fn(),
zonePropertiesChanged: vi.fn(),
},
}));
import { GameDispatch } from 'store';
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
describe('GamePersistence', () => {
it('gameStateChanged dispatches via GameDispatch', () => {

View file

@ -1,22 +1,22 @@
jest.mock('store', () => ({
vi.mock('store', () => ({
ServerDispatch: {
banFromServer: jest.fn(),
banHistory: jest.fn(),
viewLogs: jest.fn(),
warnHistory: jest.fn(),
warnListOptions: jest.fn(),
warnUser: jest.fn(),
grantReplayAccess: jest.fn(),
forceActivateUser: jest.fn(),
getAdminNotes: jest.fn(),
updateAdminNotes: jest.fn(),
banFromServer: vi.fn(),
banHistory: vi.fn(),
viewLogs: vi.fn(),
warnHistory: vi.fn(),
warnListOptions: vi.fn(),
warnUser: vi.fn(),
grantReplayAccess: vi.fn(),
forceActivateUser: vi.fn(),
getAdminNotes: vi.fn(),
updateAdminNotes: vi.fn(),
},
}));
jest.mock('../utils/NormalizeService', () => ({
vi.mock('../utils/NormalizeService', () => ({
__esModule: true,
default: {
normalizeLogs: jest.fn((logs: any) => ({ normalized: logs })),
normalizeLogs: vi.fn((logs: any) => ({ normalized: logs })),
},
}));
@ -25,8 +25,8 @@ import { ServerDispatch } from 'store';
import NormalizeService from '../utils/NormalizeService';
beforeEach(() => {
jest.clearAllMocks();
(NormalizeService.normalizeLogs as jest.Mock).mockImplementation((logs: any) => ({ normalized: logs }));
vi.clearAllMocks();
(NormalizeService.normalizeLogs as vi.Mock).mockImplementation((logs: any) => ({ normalized: logs }));
});
describe('ModeratorPersistence', () => {

View file

@ -1,29 +1,29 @@
jest.mock('store', () => ({
store: { getState: jest.fn().mockReturnValue({}) },
vi.mock('store', () => ({
store: { getState: vi.fn().mockReturnValue({}) },
RoomsDispatch: {
clearStore: jest.fn(),
joinRoom: jest.fn(),
leaveRoom: jest.fn(),
updateRooms: jest.fn(),
updateGames: jest.fn(),
addMessage: jest.fn(),
userJoined: jest.fn(),
userLeft: jest.fn(),
removeMessages: jest.fn(),
gameCreated: jest.fn(),
joinedGame: jest.fn(),
clearStore: vi.fn(),
joinRoom: vi.fn(),
leaveRoom: vi.fn(),
updateRooms: vi.fn(),
updateGames: vi.fn(),
addMessage: vi.fn(),
userJoined: vi.fn(),
userLeft: vi.fn(),
removeMessages: vi.fn(),
gameCreated: vi.fn(),
joinedGame: vi.fn(),
},
RoomsSelectors: {
getRoom: jest.fn(),
getRoom: vi.fn(),
},
}));
jest.mock('../utils/NormalizeService', () => ({
vi.mock('../utils/NormalizeService', () => ({
__esModule: true,
default: {
normalizeRoomInfo: jest.fn(),
normalizeGameObject: jest.fn(),
normalizeUserMessage: jest.fn(),
normalizeRoomInfo: vi.fn(),
normalizeGameObject: vi.fn(),
normalizeUserMessage: vi.fn(),
},
}));
@ -32,7 +32,7 @@ import { store, RoomsDispatch, RoomsSelectors } from 'store';
import NormalizeService from '../utils/NormalizeService';
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
describe('RoomPersistence', () => {
@ -62,7 +62,7 @@ describe('RoomPersistence', () => {
it('normalizes game when gameType is missing and room exists', () => {
const game = { gameType: null, gameTypes: [1] } as any;
const room = { gametypeMap: { 1: 'Standard' } } as any;
(RoomsSelectors.getRoom as jest.Mock).mockReturnValue(room);
(RoomsSelectors.getRoom as vi.Mock).mockReturnValue(room);
RoomPersistence.updateGames(1, [game]);
expect(NormalizeService.normalizeGameObject).toHaveBeenCalledWith(game, room.gametypeMap);
expect(RoomsDispatch.updateGames).toHaveBeenCalledWith(1, [game]);
@ -76,7 +76,7 @@ describe('RoomPersistence', () => {
it('does not normalize when room is not found', () => {
const game = { gameType: null } as any;
(RoomsSelectors.getRoom as jest.Mock).mockReturnValue(null);
(RoomsSelectors.getRoom as vi.Mock).mockReturnValue(null);
RoomPersistence.updateGames(1, [game]);
expect(NormalizeService.normalizeGameObject).not.toHaveBeenCalled();
});

View file

@ -1,74 +1,74 @@
jest.mock('store', () => ({
vi.mock('store', () => ({
ServerDispatch: {
initialized: jest.fn(),
clearStore: jest.fn(),
loginSuccessful: jest.fn(),
loginFailed: jest.fn(),
connectionClosed: jest.fn(),
connectionFailed: jest.fn(),
testConnectionSuccessful: jest.fn(),
testConnectionFailed: jest.fn(),
updateBuddyList: jest.fn(),
addToBuddyList: jest.fn(),
removeFromBuddyList: jest.fn(),
updateIgnoreList: jest.fn(),
addToIgnoreList: jest.fn(),
removeFromIgnoreList: jest.fn(),
updateInfo: jest.fn(),
updateStatus: jest.fn(),
updateUser: jest.fn(),
updateUsers: jest.fn(),
userJoined: jest.fn(),
userLeft: jest.fn(),
serverMessage: jest.fn(),
accountAwaitingActivation: jest.fn(),
accountActivationSuccess: jest.fn(),
accountActivationFailed: jest.fn(),
registrationRequiresEmail: jest.fn(),
registrationSuccess: jest.fn(),
registrationFailed: jest.fn(),
registrationEmailError: jest.fn(),
registrationPasswordError: jest.fn(),
registrationUserNameError: jest.fn(),
resetPasswordChallenge: jest.fn(),
resetPassword: jest.fn(),
resetPasswordSuccess: jest.fn(),
resetPasswordFailed: jest.fn(),
accountPasswordChange: jest.fn(),
accountEditChanged: jest.fn(),
accountImageChanged: jest.fn(),
getUserInfo: jest.fn(),
notifyUser: jest.fn(),
serverShutdown: jest.fn(),
userMessage: jest.fn(),
addToList: jest.fn(),
removeFromList: jest.fn(),
deckDelete: jest.fn(),
backendDecks: jest.fn(),
deckUpload: jest.fn(),
deckNewDir: jest.fn(),
deckDelDir: jest.fn(),
replayList: jest.fn(),
replayAdded: jest.fn(),
replayModifyMatch: jest.fn(),
replayDeleteMatch: jest.fn(),
gamesOfUser: jest.fn(),
initialized: vi.fn(),
clearStore: vi.fn(),
loginSuccessful: vi.fn(),
loginFailed: vi.fn(),
connectionClosed: vi.fn(),
connectionFailed: vi.fn(),
testConnectionSuccessful: vi.fn(),
testConnectionFailed: vi.fn(),
updateBuddyList: vi.fn(),
addToBuddyList: vi.fn(),
removeFromBuddyList: vi.fn(),
updateIgnoreList: vi.fn(),
addToIgnoreList: vi.fn(),
removeFromIgnoreList: vi.fn(),
updateInfo: vi.fn(),
updateStatus: vi.fn(),
updateUser: vi.fn(),
updateUsers: vi.fn(),
userJoined: vi.fn(),
userLeft: vi.fn(),
serverMessage: vi.fn(),
accountAwaitingActivation: vi.fn(),
accountActivationSuccess: vi.fn(),
accountActivationFailed: vi.fn(),
registrationRequiresEmail: vi.fn(),
registrationSuccess: vi.fn(),
registrationFailed: vi.fn(),
registrationEmailError: vi.fn(),
registrationPasswordError: vi.fn(),
registrationUserNameError: vi.fn(),
resetPasswordChallenge: vi.fn(),
resetPassword: vi.fn(),
resetPasswordSuccess: vi.fn(),
resetPasswordFailed: vi.fn(),
accountPasswordChange: vi.fn(),
accountEditChanged: vi.fn(),
accountImageChanged: vi.fn(),
getUserInfo: vi.fn(),
notifyUser: vi.fn(),
serverShutdown: vi.fn(),
userMessage: vi.fn(),
addToList: vi.fn(),
removeFromList: vi.fn(),
deckDelete: vi.fn(),
backendDecks: vi.fn(),
deckUpload: vi.fn(),
deckNewDir: vi.fn(),
deckDelDir: vi.fn(),
replayList: vi.fn(),
replayAdded: vi.fn(),
replayModifyMatch: vi.fn(),
replayDeleteMatch: vi.fn(),
gamesOfUser: vi.fn(),
},
GameDispatch: {
gameJoined: jest.fn(),
playerPropertiesChanged: jest.fn(),
gameJoined: vi.fn(),
playerPropertiesChanged: vi.fn(),
},
}));
jest.mock('websocket/utils', () => ({
sanitizeHtml: jest.fn((msg: string) => `sanitized:${msg}`),
vi.mock('websocket/utils', () => ({
sanitizeHtml: vi.fn((msg: string) => `sanitized:${msg}`),
}));
jest.mock('../utils/NormalizeService', () => ({
vi.mock('../utils/NormalizeService', () => ({
__esModule: true,
default: {
normalizeBannedUserError: jest.fn((r: string, t: number) => `banned:${r}:${t}`),
normalizeGameObject: jest.fn(),
normalizeBannedUserError: vi.fn((r: string, t: number) => `banned:${r}:${t}`),
normalizeGameObject: vi.fn(),
},
}));
@ -79,9 +79,9 @@ import NormalizeService from '../utils/NormalizeService';
import { StatusEnum } from 'types';
beforeEach(() => {
jest.clearAllMocks();
(sanitizeHtml as jest.Mock).mockImplementation((msg: string) => `sanitized:${msg}`);
(NormalizeService.normalizeBannedUserError as jest.Mock).mockImplementation(
vi.clearAllMocks();
(sanitizeHtml as vi.Mock).mockImplementation((msg: string) => `sanitized:${msg}`);
(NormalizeService.normalizeBannedUserError as vi.Mock).mockImplementation(
(r: string, t: number) => `banned:${r}:${t}`
);
});

View file

@ -1,16 +1,16 @@
import { makeMockProtoRoot } from '../__mocks__/helpers';
jest.mock('./ProtoController', () => ({
vi.mock('./ProtoController', () => ({
ProtoController: { root: null },
}));
jest.mock('../WebClient', () => {
vi.mock('../WebClient', () => {
const mockProtobuf = {
sendGameCommand: jest.fn(),
sendSessionCommand: jest.fn(),
sendRoomCommand: jest.fn(),
sendModeratorCommand: jest.fn(),
sendAdminCommand: jest.fn(),
sendGameCommand: vi.fn(),
sendSessionCommand: vi.fn(),
sendRoomCommand: vi.fn(),
sendModeratorCommand: vi.fn(),
sendAdminCommand: vi.fn(),
};
return { __esModule: true, default: { protobuf: mockProtobuf } };
});
@ -20,18 +20,18 @@ import { ProtoController } from './ProtoController';
import webClient from '../WebClient';
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
ProtoController.root = makeMockProtoRoot();
ProtoController.root.GameCommand = { create: jest.fn(args => ({ ...args })) };
ProtoController.root['Command_Game'] = { create: jest.fn(p => ({ ...p })) };
ProtoController.root['Command_Test'] = { create: jest.fn(p => ({ ...p })) };
ProtoController.root['Command_Room'] = { create: jest.fn(p => ({ ...p })) };
ProtoController.root['Command_Mod'] = { create: jest.fn(p => ({ ...p })) };
ProtoController.root['Command_Admin'] = { create: jest.fn(p => ({ ...p })) };
ProtoController.root.GameCommand = { create: vi.fn(args => ({ ...args })) };
ProtoController.root['Command_Game'] = { create: vi.fn(p => ({ ...p })) };
ProtoController.root['Command_Test'] = { create: vi.fn(p => ({ ...p })) };
ProtoController.root['Command_Room'] = { create: vi.fn(p => ({ ...p })) };
ProtoController.root['Command_Mod'] = { create: vi.fn(p => ({ ...p })) };
ProtoController.root['Command_Admin'] = { create: vi.fn(p => ({ ...p })) };
ProtoController.root['Response_Test'] = {};
});
function captureCallback(sendFn: jest.Mock) {
function captureCallback(sendFn: vi.Mock) {
return sendFn.mock.calls[0][sendFn === (webClient.protobuf as any).sendRoomCommand ? 2 : 1];
}
@ -51,7 +51,7 @@ describe('BackendService', () => {
describe('handleResponse via non-session command callbacks', () => {
it('sendGameCommand callback invokes handleResponse', () => {
const onSuccess = jest.fn();
const onSuccess = vi.fn();
BackendService.sendGameCommand(7, 'Command_Game', {}, { onSuccess });
const cb = (webClient.protobuf as any).sendGameCommand.mock.calls[0][2];
cb({ responseCode: 0 });
@ -59,21 +59,21 @@ describe('BackendService', () => {
});
it('sendRoomCommand callback invokes handleResponse', () => {
const onSuccess = jest.fn();
const onSuccess = vi.fn();
BackendService.sendRoomCommand(5, 'Command_Room', {}, { onSuccess });
captureCallback((webClient.protobuf as any).sendRoomCommand)({ responseCode: 0 });
expect(onSuccess).toHaveBeenCalled();
});
it('sendModeratorCommand callback invokes handleResponse', () => {
const onSuccess = jest.fn();
const onSuccess = vi.fn();
BackendService.sendModeratorCommand('Command_Mod', {}, { onSuccess });
captureCallback((webClient.protobuf as any).sendModeratorCommand)({ responseCode: 0 });
expect(onSuccess).toHaveBeenCalled();
});
it('sendAdminCommand callback invokes handleResponse', () => {
const onSuccess = jest.fn();
const onSuccess = vi.fn();
BackendService.sendAdminCommand('Command_Admin', {}, { onSuccess });
captureCallback((webClient.protobuf as any).sendAdminCommand)({ responseCode: 0 });
expect(onSuccess).toHaveBeenCalled();
@ -88,41 +88,41 @@ describe('BackendService', () => {
}
it('calls onResponse and returns early when provided', () => {
const onResponse = jest.fn();
const onSuccess = jest.fn();
const onResponse = vi.fn();
const onSuccess = vi.fn();
invokeCallback({ onResponse, onSuccess }, { responseCode: 99 });
expect(onResponse).toHaveBeenCalled();
expect(onSuccess).not.toHaveBeenCalled();
});
it('calls onSuccess with raw when responseCode is RespOk and no responseName', () => {
const onSuccess = jest.fn();
const onSuccess = vi.fn();
const raw = { responseCode: 0 };
invokeCallback({ onSuccess }, raw);
expect(onSuccess).toHaveBeenCalledWith(raw, raw);
});
it('calls onSuccess with nested response when responseName is set', () => {
const onSuccess = jest.fn();
const onSuccess = vi.fn();
const raw = { responseCode: 0, '.Response_Test.ext': { nested: true } };
invokeCallback({ onSuccess, responseName: 'Response_Test' }, raw);
expect(onSuccess).toHaveBeenCalledWith({ nested: true }, raw);
});
it('calls onResponseCode handler when code matches', () => {
const specificHandler = jest.fn();
const specificHandler = vi.fn();
invokeCallback({ onResponseCode: { 5: specificHandler } }, { responseCode: 5 });
expect(specificHandler).toHaveBeenCalled();
});
it('calls onError when responseCode is not RespOk and no specific handler', () => {
const onError = jest.fn();
const onError = vi.fn();
invokeCallback({ onError }, { responseCode: 99 });
expect(onError).toHaveBeenCalledWith(99, { responseCode: 99 });
});
it('logs error to console when no callbacks for non-RespOk response', () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
invokeCallback({}, { responseCode: 42 });
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();

View file

@ -6,7 +6,7 @@ describe('KeepAliveService', () => {
let service: KeepAliveService;
beforeEach(() => {
jest.useFakeTimers();
vi.useFakeTimers();
service = new KeepAliveService(webClient.socket);
});
@ -27,11 +27,11 @@ describe('KeepAliveService', () => {
promise = new Promise(resolve => resolvePing = resolve);
ping = (done) => promise.then(done);
checkReadyStateSpy = jest.spyOn(webClient.socket, 'checkReadyState');
checkReadyStateSpy = vi.spyOn(webClient.socket, 'checkReadyState');
checkReadyStateSpy.mockImplementation(() => true);
service.startPingLoop(interval, ping);
jest.advanceTimersByTime(interval);
vi.advanceTimersByTime(interval);
});
it('should start ping loop', () => {
@ -39,28 +39,27 @@ describe('KeepAliveService', () => {
expect((service as any).lastPingPending).toBeTruthy();
});
it('should call ping callback when done', (done: jest.DoneCallback) => {
it('should call ping callback when done', () => {
resolvePing();
promise.then(() => {
return promise.then(() => {
expect((service as any).lastPingPending).toBeFalsy();
done();
});
});
it('should fire disconnected$ if lastPingPending is still true', () => {
jest.spyOn(service.disconnected$, 'next').mockImplementation(() => {});
jest.advanceTimersByTime(interval);
vi.spyOn(service.disconnected$, 'next').mockImplementation(() => {});
vi.advanceTimersByTime(interval);
expect(service.disconnected$.next).toHaveBeenCalled();
});
it('should endPingLoop if socket is not open', () => {
jest.spyOn(service, 'endPingLoop').mockImplementation(() => {});
vi.spyOn(service, 'endPingLoop').mockImplementation(() => {});
checkReadyStateSpy.mockImplementation(() => false);
resolvePing();
jest.advanceTimersByTime(interval);
vi.advanceTimersByTime(interval);
expect(service.endPingLoop).toHaveBeenCalled();
});

View file

@ -1,17 +1,16 @@
jest.mock('../persistence', () => ({
SessionPersistence: { initialized: jest.fn() },
vi.mock('../persistence', () => ({
SessionPersistence: { initialized: vi.fn() },
}));
jest.mock('../../proto-files.json', () => ['test.proto'], { virtual: true });
vi.mock('../../proto-files.json', () => ({ default: ['test.proto'] }));
import { ProtoController } from './ProtoController';
import { SessionPersistence } from '../persistence';
import protobuf from 'protobufjs';
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
ProtoController.root = null;
(process.env as any).PUBLIC_URL = '';
});
describe('ProtoController', () => {
@ -22,7 +21,7 @@ describe('ProtoController', () => {
});
it('calls initialized when callback succeeds', () => {
const loadSpy = jest.spyOn(protobuf.Root.prototype, 'load').mockImplementation(
const loadSpy = vi.spyOn(protobuf.Root.prototype, 'load').mockImplementation(
((_files: any, _opts: any, cb: any) => cb(null)) as any
);
ProtoController.load();
@ -31,7 +30,7 @@ describe('ProtoController', () => {
});
it('throws when callback receives an error', () => {
const loadSpy = jest.spyOn(protobuf.Root.prototype, 'load').mockImplementation(
const loadSpy = vi.spyOn(protobuf.Root.prototype, 'load').mockImplementation(
((_files: any, _opts: any, cb: any) => cb(new Error('load failed'))) as any
);
expect(() => ProtoController.load()).toThrow('load failed');

View file

@ -3,7 +3,7 @@ import protobuf from 'protobufjs';
import { SessionPersistence } from '../persistence';
import ProtoFiles from '../../proto-files.json';
const PB_FILE_DIR = `${process.env.PUBLIC_URL}/pb`;
const PB_FILE_DIR = `${import.meta.env.BASE_URL}pb`;
// Leaf module — no imports from the websocket layer other than persistence.
// Both BackendService and ProtobufService import this; neither should import

View file

@ -1,21 +1,24 @@
import { makeMockProtoRoot } from '../__mocks__/helpers';
jest.mock('./ProtoController', () => ({
ProtoController: { root: null, load: jest.fn() },
vi.mock('./ProtoController', () => ({
ProtoController: { root: null, load: vi.fn() },
}));
jest.mock('../commands/session', () => ({
SessionCommands: { ping: jest.fn() },
ping: jest.fn(),
vi.mock('../commands/session', () => ({
SessionCommands: { ping: vi.fn() },
ping: vi.fn(),
}));
jest.mock('../events', () => ({
GameEvents: { '.Event_Game.ext': jest.fn() },
RoomEvents: { '.Event_Room.ext': jest.fn() },
SessionEvents: { '.Event_Session.ext': jest.fn() },
vi.mock('../events', () => ({
GameEvents: { '.Event_Game.ext': vi.fn() },
RoomEvents: { '.Event_Room.ext': vi.fn() },
SessionEvents: { '.Event_Session.ext': vi.fn() },
}));
jest.mock('../WebClient');
vi.mock('../WebClient', () => ({
__esModule: true,
default: {},
}));
import { ProtobufService } from './ProtobufService';
import { ProtoController } from './ProtoController';
@ -26,15 +29,15 @@ let mockSocket: any;
let mockWebClient: any;
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
ProtoController.root = makeMockProtoRoot();
const encodeResult = { finish: jest.fn().mockReturnValue(new Uint8Array([1, 2])) };
ProtoController.root.CommandContainer.encode = jest.fn().mockReturnValue(encodeResult);
const encodeResult = { finish: vi.fn().mockReturnValue(new Uint8Array([1, 2])) };
ProtoController.root.CommandContainer.encode = vi.fn().mockReturnValue(encodeResult);
mockSocket = {
checkReadyState: jest.fn().mockReturnValue(true),
send: jest.fn(),
checkReadyState: vi.fn().mockReturnValue(true),
send: vi.fn(),
};
mockWebClient = {
@ -52,7 +55,7 @@ describe('ProtobufService', () => {
it('resets cmdId and pendingCommands', () => {
const service = new ProtobufService(mockWebClient);
// add a pending command
service.sendSessionCommand({}, jest.fn());
service.sendSessionCommand({}, vi.fn());
expect((service as any).cmdId).toBe(1);
service.resetCommands();
expect((service as any).cmdId).toBe(0);
@ -63,7 +66,7 @@ describe('ProtobufService', () => {
describe('sendCommand', () => {
it('increments cmdId and stores callback', () => {
const service = new ProtobufService(mockWebClient);
const cb = jest.fn();
const cb = vi.fn();
service.sendCommand({}, cb);
expect((service as any).cmdId).toBe(1);
expect((service as any).pendingCommands[1]).toBe(cb);
@ -72,14 +75,14 @@ describe('ProtobufService', () => {
it('sends encoded data when socket is OPEN', () => {
const service = new ProtobufService(mockWebClient);
mockSocket.checkReadyState.mockReturnValue(true);
service.sendCommand({}, jest.fn());
service.sendCommand({}, vi.fn());
expect(mockSocket.send).toHaveBeenCalled();
});
it('does not send when socket is not OPEN', () => {
const service = new ProtobufService(mockWebClient);
mockSocket.checkReadyState.mockReturnValue(false);
service.sendCommand({}, jest.fn());
service.sendCommand({}, vi.fn());
expect(mockSocket.send).not.toHaveBeenCalled();
});
});
@ -87,7 +90,7 @@ describe('ProtobufService', () => {
describe('sendSessionCommand', () => {
it('creates a CommandContainer and calls sendCommand', () => {
const service = new ProtobufService(mockWebClient);
const cb = jest.fn();
const cb = vi.fn();
service.sendSessionCommand({ cmdType: 'test' }, cb);
expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith(
expect.objectContaining({ sessionCommand: expect.anything() })
@ -96,7 +99,7 @@ describe('ProtobufService', () => {
it('invokes callback with raw response when the pending command is triggered', () => {
const service = new ProtobufService(mockWebClient);
const cb = jest.fn();
const cb = vi.fn();
service.sendSessionCommand({ cmdType: 'test' }, cb);
const storedCb = (service as any).pendingCommands[1];
@ -117,7 +120,7 @@ describe('ProtobufService', () => {
describe('sendRoomCommand', () => {
it('creates a CommandContainer with roomId and calls sendCommand', () => {
const service = new ProtobufService(mockWebClient);
service.sendRoomCommand(42, { roomCmdType: 'test' }, jest.fn());
service.sendRoomCommand(42, { roomCmdType: 'test' }, vi.fn());
expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith(
expect.objectContaining({ roomId: 42 })
);
@ -125,7 +128,7 @@ describe('ProtobufService', () => {
it('invokes callback with raw response when the pending command is triggered', () => {
const service = new ProtobufService(mockWebClient);
const cb = jest.fn();
const cb = vi.fn();
service.sendRoomCommand(42, { roomCmdType: 'test' }, cb);
const storedCb = (service as any).pendingCommands[1];
@ -146,7 +149,7 @@ describe('ProtobufService', () => {
describe('sendGameCommand', () => {
it('creates a CommandContainer with gameId and gameCommand', () => {
const service = new ProtobufService(mockWebClient);
service.sendGameCommand(7, { gameCmdType: 'test' }, jest.fn());
service.sendGameCommand(7, { gameCmdType: 'test' }, vi.fn());
expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith(
expect.objectContaining({ gameId: 7, gameCommand: expect.anything() })
);
@ -154,7 +157,7 @@ describe('ProtobufService', () => {
it('invokes callback with raw response when the pending command is triggered', () => {
const service = new ProtobufService(mockWebClient);
const cb = jest.fn();
const cb = vi.fn();
service.sendGameCommand(7, { gameCmdType: 'test' }, cb);
const storedCb = (service as any).pendingCommands[1];
@ -175,7 +178,7 @@ describe('ProtobufService', () => {
describe('sendModeratorCommand', () => {
it('creates a CommandContainer with moderatorCommand', () => {
const service = new ProtobufService(mockWebClient);
service.sendModeratorCommand({ modCmdType: 'test' }, jest.fn());
service.sendModeratorCommand({ modCmdType: 'test' }, vi.fn());
expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith(
expect.objectContaining({ moderatorCommand: expect.anything() })
);
@ -183,7 +186,7 @@ describe('ProtobufService', () => {
it('invokes callback with raw response when the pending command is triggered', () => {
const service = new ProtobufService(mockWebClient);
const cb = jest.fn();
const cb = vi.fn();
service.sendModeratorCommand({ modCmdType: 'test' }, cb);
const storedCb = (service as any).pendingCommands[1];
@ -204,7 +207,7 @@ describe('ProtobufService', () => {
describe('sendAdminCommand', () => {
it('creates a CommandContainer with adminCommand', () => {
const service = new ProtobufService(mockWebClient);
service.sendAdminCommand({ adminCmdType: 'test' }, jest.fn());
service.sendAdminCommand({ adminCmdType: 'test' }, vi.fn());
expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith(
expect.objectContaining({ adminCommand: expect.anything() })
);
@ -212,7 +215,7 @@ describe('ProtobufService', () => {
it('invokes callback with raw response when the pending command is triggered', () => {
const service = new ProtobufService(mockWebClient);
const cb = jest.fn();
const cb = vi.fn();
service.sendAdminCommand({ adminCmdType: 'test' }, cb);
const storedCb = (service as any).pendingCommands[1];
@ -233,7 +236,7 @@ describe('ProtobufService', () => {
describe('sendKeepAliveCommand', () => {
it('delegates to SessionCommands.ping', () => {
const service = new ProtobufService(mockWebClient);
const pingReceived = jest.fn();
const pingReceived = vi.fn();
service.sendKeepAliveCommand(pingReceived);
expect(sessionPing).toHaveBeenCalledWith(pingReceived);
});
@ -242,13 +245,13 @@ describe('ProtobufService', () => {
describe('handleMessageEvent', () => {
it('routes RESPONSE message to processServerResponse', () => {
const service = new ProtobufService(mockWebClient);
const cb = jest.fn();
const cb = vi.fn();
// store a callback for cmdId 1
(service as any).cmdId = 1;
(service as any).pendingCommands[1] = cb;
const response = { cmdId: 1 };
ProtoController.root.ServerMessage.decode = jest.fn().mockReturnValue({
ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue({
messageType: ProtoController.root.ServerMessage.MessageType.RESPONSE,
response,
});
@ -260,14 +263,14 @@ describe('ProtobufService', () => {
it('resolves pending command when response cmdId is a protobufjs Long object', () => {
const service = new ProtobufService(mockWebClient);
const cb = jest.fn();
const cb = vi.fn();
(service as any).cmdId = 1;
(service as any).pendingCommands[1] = cb;
// Simulate protobufjs decoding cmdId as a Long object (low=1, high=0)
const longCmdId = { low: 1, high: 0, unsigned: false, toString: () => '1' };
const response = { cmdId: longCmdId };
ProtoController.root.ServerMessage.decode = jest.fn().mockReturnValue({
ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue({
messageType: ProtoController.root.ServerMessage.MessageType.RESPONSE,
response,
});
@ -279,8 +282,8 @@ describe('ProtobufService', () => {
it('routes ROOM_EVENT message', () => {
const service = new ProtobufService(mockWebClient);
const processRoomEvent = jest.spyOn(service as any, 'processRoomEvent');
ProtoController.root.ServerMessage.decode = jest.fn().mockReturnValue({
const processRoomEvent = vi.spyOn(service as any, 'processRoomEvent');
ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue({
messageType: ProtoController.root.ServerMessage.MessageType.ROOM_EVENT,
roomEvent: { '.Event_Room.ext': {} },
});
@ -290,8 +293,8 @@ describe('ProtobufService', () => {
it('routes SESSION_EVENT message', () => {
const service = new ProtobufService(mockWebClient);
const processSessionEvent = jest.spyOn(service as any, 'processSessionEvent');
ProtoController.root.ServerMessage.decode = jest.fn().mockReturnValue({
const processSessionEvent = vi.spyOn(service as any, 'processSessionEvent');
ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue({
messageType: ProtoController.root.ServerMessage.MessageType.SESSION_EVENT,
sessionEvent: { '.Event_Session.ext': {} },
});
@ -301,8 +304,8 @@ describe('ProtobufService', () => {
it('routes GAME_EVENT_CONTAINER message', () => {
const service = new ProtobufService(mockWebClient);
const processGameEvent = jest.spyOn(service as any, 'processGameEvent');
ProtoController.root.ServerMessage.decode = jest.fn().mockReturnValue({
const processGameEvent = vi.spyOn(service as any, 'processGameEvent');
ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue({
messageType: ProtoController.root.ServerMessage.MessageType.GAME_EVENT_CONTAINER,
gameEvent: { '.Event_Game.ext': {} },
});
@ -312,8 +315,8 @@ describe('ProtobufService', () => {
it('logs unknown message types (default case)', () => {
const service = new ProtobufService(mockWebClient);
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
ProtoController.root.ServerMessage.decode = jest.fn().mockReturnValue({
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue({
messageType: 'UNKNOWN_TYPE',
});
service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent);
@ -323,14 +326,14 @@ describe('ProtobufService', () => {
it('does nothing when decoded message is null', () => {
const service = new ProtobufService(mockWebClient);
ProtoController.root.ServerMessage.decode = jest.fn().mockReturnValue(null);
ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue(null);
expect(() => service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent)).not.toThrow();
});
it('catches and logs decode errors', () => {
const service = new ProtobufService(mockWebClient);
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
ProtoController.root.ServerMessage.decode = jest.fn().mockImplementation(() => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
ProtoController.root.ServerMessage.decode = vi.fn().mockImplementation(() => {
throw new Error('decode error');
});
expect(() => service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent)).not.toThrow();
@ -342,14 +345,14 @@ describe('ProtobufService', () => {
describe('processGameEvent', () => {
it('returns early when container has no eventList', () => {
const service = new ProtobufService(mockWebClient);
const gameEventHandler = (GameEvents as any)['.Event_Game.ext'] as jest.Mock;
const gameEventHandler = (GameEvents as any)['.Event_Game.ext'] as vi.Mock;
(service as any).processGameEvent(null, {});
expect(gameEventHandler).not.toHaveBeenCalled();
});
it('dispatches to a GameEvents handler when event key matches', () => {
const service = new ProtobufService(mockWebClient);
const gameEventHandler = (GameEvents as any)['.Event_Game.ext'] as jest.Mock;
const gameEventHandler = (GameEvents as any)['.Event_Game.ext'] as vi.Mock;
const payload = { someData: 1 };
(service as any).processGameEvent({
gameId: 42,
@ -366,7 +369,7 @@ describe('ProtobufService', () => {
describe('processEvent', () => {
it('calls matching event handler with payload and raw', () => {
const service = new ProtobufService(mockWebClient);
const handler = jest.fn();
const handler = vi.fn();
const events = { '.Event_Test.ext': handler };
const payload = { someData: 1 };
const response = { '.Event_Test.ext': payload };
@ -379,8 +382,8 @@ describe('ProtobufService', () => {
it('stops after first matching event', () => {
const service = new ProtobufService(mockWebClient);
const handler1 = jest.fn();
const handler2 = jest.fn();
const handler1 = vi.fn();
const handler2 = vi.fn();
const events = { '.Event_A.ext': handler1, '.Event_B.ext': handler2 };
const response = { '.Event_A.ext': { x: 1 } };

View file

@ -1,14 +1,14 @@
import { installMockWebSocket } from '../__mocks__/helpers';
jest.mock('../commands/session', () => ({
updateStatus: jest.fn(),
vi.mock('../commands/session', () => ({
updateStatus: vi.fn(),
}));
jest.mock('../persistence', () => ({
vi.mock('../persistence', () => ({
SessionPersistence: {
connectionFailed: jest.fn(),
testConnectionSuccessful: jest.fn(),
testConnectionFailed: jest.fn(),
connectionFailed: vi.fn(),
testConnectionSuccessful: vi.fn(),
testConnectionFailed: vi.fn(),
},
}));
@ -17,13 +17,13 @@ import { SessionPersistence } from '../persistence';
import { updateStatus } from '../commands/session';
import { StatusEnum } from 'types';
let MockWS: jest.Mock;
let MockWS: vi.Mock;
let mockInstance: ReturnType<typeof installMockWebSocket>['mockInstance'];
let mockWebClient: any;
beforeEach(() => {
jest.useFakeTimers();
jest.clearAllMocks();
vi.useFakeTimers();
vi.clearAllMocks();
const installed = installMockWebSocket();
MockWS = installed.MockWS;
@ -32,12 +32,12 @@ beforeEach(() => {
mockWebClient = {
status: StatusEnum.CONNECTED,
clientOptions: { keepalive: 1000 },
keepAlive: jest.fn(),
keepAlive: vi.fn(),
};
});
afterEach(() => {
jest.useRealTimers();
vi.useRealTimers();
});
describe('WebSocketService', () => {
@ -99,14 +99,14 @@ describe('WebSocketService', () => {
it('fires socket.close after keepalive timeout', () => {
createConnectedService();
jest.advanceTimersByTime(1000);
vi.advanceTimersByTime(1000);
expect(mockInstance.close).toHaveBeenCalled();
});
});
describe('socket event handlers (onopen)', () => {
it('clears the connection timeout when socket opens', () => {
const clearSpy = jest.spyOn(global, 'clearTimeout');
const clearSpy = vi.spyOn(global, 'clearTimeout');
createConnectedService();
mockInstance.onopen();
expect(clearSpy).toHaveBeenCalled();
@ -120,7 +120,7 @@ describe('WebSocketService', () => {
it('starts the ping loop with the keepalive interval', () => {
const service = new WebSocketService(mockWebClient);
const startSpy = jest.spyOn((service as any).keepAliveService, 'startPingLoop');
const startSpy = vi.spyOn((service as any).keepAliveService, 'startPingLoop');
service.connect({ host: 'h', port: 1 } as any, 'ws');
mockInstance.onopen();
expect(startSpy).toHaveBeenCalledWith(1000, expect.any(Function));
@ -128,11 +128,11 @@ describe('WebSocketService', () => {
it('ping loop callback calls webClient.keepAlive', () => {
const service = new WebSocketService(mockWebClient);
const startSpy = jest.spyOn((service as any).keepAliveService, 'startPingLoop');
const startSpy = vi.spyOn((service as any).keepAliveService, 'startPingLoop');
service.connect({ host: 'h', port: 1 } as any, 'ws');
mockInstance.onopen();
const pingCb = startSpy.mock.calls[0][1] as (done: Function) => void;
const done = jest.fn();
const done = vi.fn();
pingCb(done);
expect(mockWebClient.keepAlive).toHaveBeenCalledWith(done);
});
@ -154,7 +154,7 @@ describe('WebSocketService', () => {
it('ends the ping loop on close', () => {
const service = new WebSocketService(mockWebClient);
const endSpy = jest.spyOn((service as any).keepAliveService, 'endPingLoop');
const endSpy = vi.spyOn((service as any).keepAliveService, 'endPingLoop');
service.connect({ host: 'h', port: 1 } as any, 'ws');
mockInstance.onclose();
expect(endSpy).toHaveBeenCalled();
@ -178,7 +178,7 @@ describe('WebSocketService', () => {
describe('socket event handlers (onmessage)', () => {
it('emits on message$ subject', () => {
const service = createConnectedService();
const handler = jest.fn();
const handler = vi.fn();
service.message$.subscribe(handler);
const event = { data: new ArrayBuffer(4) } as MessageEvent;
mockInstance.onmessage(event);
@ -262,7 +262,7 @@ describe('WebSocketService', () => {
it('calls SessionPersistence.testConnectionSuccessful on open', () => {
createTestConnectedService();
const timer = jest.spyOn(global, 'clearTimeout');
const timer = vi.spyOn(global, 'clearTimeout');
mockInstance.onopen();
expect(SessionPersistence.testConnectionSuccessful).toHaveBeenCalled();
expect(mockInstance.close).toHaveBeenCalled();
@ -270,7 +270,7 @@ describe('WebSocketService', () => {
it('fires socket.close after keepalive timeout for testConnect', () => {
createTestConnectedService();
jest.advanceTimersByTime(1000);
vi.advanceTimersByTime(1000);
expect(mockInstance.close).toHaveBeenCalled();
});

View file

@ -11,7 +11,7 @@ describe('guid', () => {
});
it('returns deterministic value when Math.random is mocked', () => {
const spy = jest.spyOn(Math, 'random').mockReturnValue(0.5);
const spy = vi.spyOn(Math, 'random').mockReturnValue(0.5);
const result = guid();
expect(result).toBe(guid());
spy.mockRestore();

View file

@ -1,6 +1,6 @@
import { makeMockProtoRoot } from '../__mocks__/helpers';
jest.mock('../services/ProtoController', () => ({
vi.mock('../services/ProtoController', () => ({
ProtoController: { root: null },
}));

View file

@ -6,7 +6,7 @@ describe('sanitizeHtml', () => {
});
it('allows <br> tag', () => {
expect(sanitizeHtml('line1<br>line2')).toBe('line1<br />line2');
expect(sanitizeHtml('line1<br>line2')).toBe('line1<br>line2');
});
it('allows <b> tag', () => {
@ -14,7 +14,7 @@ describe('sanitizeHtml', () => {
});
it('allows <img> tag', () => {
expect(sanitizeHtml('<img>')).toBe('<img />');
expect(sanitizeHtml('<img>')).toBe('<img>');
});
it('allows <center> tag', () => {

View file

@ -1,18 +1,17 @@
import sanitize from 'sanitize-html';
import DOMPurify from 'dompurify';
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
if (node.tagName === 'A') {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noopener noreferrer');
}
});
export function sanitizeHtml(msg: string): string {
return sanitize(msg, {
allowedTags: ['br', 'a', 'img', 'center', 'b', 'font'],
allowedAttributes: {
'*': ['href', 'color', 'rel', 'target'],
'img': ['src', 'alt'],
},
allowedSchemes: ['http', 'https', 'ftp'],
transformTags: {
'a': sanitize.simpleTransform('a', {
target: '_blank',
rel: 'noopener noreferrer',
}),
}
return DOMPurify.sanitize(msg, {
ALLOWED_TAGS: ['br', 'a', 'img', 'center', 'b', 'font'],
ALLOWED_ATTR: ['href', 'color', 'rel', 'target', 'src', 'alt'],
ADD_URI_SAFE_ATTR: ['color'],
ALLOWED_URI_REGEXP: /^(?:(?:https?|ftp):)/i,
});
}