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

@ -30,7 +30,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
node_version: node_version:
- 16 - 20
- lts/* - lts/*
steps: steps:

View file

@ -1 +1 @@
ESLINT_NO_DEV_ERRORS=true

View file

@ -1 +1 @@
DISABLE_ESLINT_PLUGIN=true

View file

@ -1 +1 @@
CI=true

View file

@ -1,7 +1,7 @@
module.exports = { module.exports = {
"root": true, "root": true,
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": {"project": ["./tsconfig.json"]}, "parserOptions": {"ecmaVersion": 2020, "sourceType": "module", "ecmaFeatures": {"jsx": true}},
"plugins": [ "plugins": [
"@typescript-eslint" "@typescript-eslint"
], ],

22
webclient/index.html Normal file
View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" type="text/css" href="/reset.css">
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Webatrice: A Cockatrice Web Client"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>Webatrice</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

31974
webclient/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,13 +5,13 @@
"scripts": { "scripts": {
"prebuild": "node prebuild.js", "prebuild": "node prebuild.js",
"prestart": "node prebuild.js", "prestart": "node prebuild.js",
"build": "react-scripts build", "build": "vite build",
"start": "react-scripts start", "start": "vite",
"test": "react-scripts test", "preview": "vite preview",
"test:watch": "react-scripts test", "test": "vitest run",
"eject": "react-scripts eject", "test:watch": "vitest",
"lint": "eslint \"./**/*.{ts,tsx}\"", "lint": "eslint src/**/*.{ts,tsx}",
"lint:fix": "eslint \"./**/*.{ts,tsx}\" --fix", "lint:fix": "eslint src/**/*.{ts,tsx} --fix",
"golden": "npm run lint && npm run test", "golden": "npm run lint && npm run test",
"prepare": "cd .. && husky install", "prepare": "cd .. && husky install",
"translate": "node prebuild.js -i18nOnly" "translate": "node prebuild.js -i18nOnly"
@ -23,6 +23,7 @@
"@mui/material": "^5.5.1", "@mui/material": "^5.5.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dexie": "^3.2.2", "dexie": "^3.2.2",
"dompurify": "^3.3.3",
"final-form": "^4.20.6", "final-form": "^4.20.6",
"final-form-set-field-touched": "^1.0.1", "final-form-set-field-touched": "^1.0.1",
"i18next": "^22.0.4", "i18next": "^22.0.4",
@ -39,21 +40,18 @@
"react-i18next": "^12.0.0", "react-i18next": "^12.0.0",
"react-redux": "^8.0.4", "react-redux": "^8.0.4",
"react-router-dom": "^6.2.2", "react-router-dom": "^6.2.2",
"react-scripts": "5.0.1",
"react-virtualized-auto-sizer": "^1.0.6", "react-virtualized-auto-sizer": "^1.0.6",
"react-window": "^1.8.6", "react-window": "^1.8.6",
"redux": "^4.1.2", "redux": "^4.1.2",
"redux-form": "^8.3.8", "redux-form": "^8.3.8",
"redux-thunk": "^2.4.1", "redux-thunk": "^2.4.1",
"rxjs": "^7.5.4", "rxjs": "^7.5.4"
"sanitize-html": "^2.7.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.17.5",
"@mui/types": "^7.1.3", "@mui/types": "^7.1.3",
"@testing-library/jest-dom": "^5.16.2", "@testing-library/jest-dom": "^6.4.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@types/jest": "29.2.0", "@types/dompurify": "^3.0.5",
"@types/jquery": "^3.5.14", "@types/jquery": "^3.5.14",
"@types/lodash": "^4.14.179", "@types/lodash": "^4.14.179",
"@types/node": "18.11.7", "@types/node": "18.11.7",
@ -67,12 +65,16 @@
"@types/redux-form": "^8.3.3", "@types/redux-form": "^8.3.3",
"@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/eslint-plugin": "^5.14.0",
"@typescript-eslint/parser": "^5.14.0", "@typescript-eslint/parser": "^5.14.0",
"@vitejs/plugin-react": "^4.2.0",
"@vitest/coverage-v8": "^1.3.0",
"eslint": "^8.0.0",
"fs-extra": "^10.0.1", "fs-extra": "^10.0.1",
"husky": "^8.0.1", "husky": "^8.0.1",
"typescript": "^4.6.2" "jsdom": "^24.0.0",
}, "typescript": "^4.6.2",
"eslintConfig": { "vite": "^5.1.0",
"extends": "react-app" "vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.3.0"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@ -85,10 +87,5 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"jest": {
"moduleNameMapper": {
"\\.(css|less)$": "identity-obj-proxy"
}
} }
} }

View file

@ -1,44 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%/reset.css">
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Webatrice: A Cockatrice Web Client"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Webatrice</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View file

@ -1,6 +1,6 @@
{ {
"short_name": "React App", "short_name": "Webatrice",
"name": "Create React App Sample", "name": "Webatrice",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",

View file

@ -1,16 +1,16 @@
jest.mock('websocket', () => ({ vi.mock('websocket', () => ({
AdminCommands: { AdminCommands: {
adjustMod: jest.fn(), adjustMod: vi.fn(),
reloadConfig: jest.fn(), reloadConfig: vi.fn(),
shutdownServer: jest.fn(), shutdownServer: vi.fn(),
updateServerMessage: jest.fn(), updateServerMessage: vi.fn(),
}, },
})); }));
import { AdminService } from './AdminService'; import { AdminService } from './AdminService';
import { AdminCommands } from 'websocket'; import { AdminCommands } from 'websocket';
beforeEach(() => jest.clearAllMocks()); beforeEach(() => vi.clearAllMocks());
describe('AdminService', () => { describe('AdminService', () => {
describe('adjustMod', () => { describe('adjustMod', () => {

View file

@ -1,14 +1,14 @@
jest.mock('websocket', () => ({ vi.mock('websocket', () => ({
SessionCommands: { SessionCommands: {
connect: jest.fn(), connect: vi.fn(),
disconnect: jest.fn(), disconnect: vi.fn(),
}, },
webClient: { webClient: {
connectionAttemptMade: false, connectionAttemptMade: false,
}, },
})); }));
jest.mock('websocket/services/ProtoController', () => ({ vi.mock('websocket/services/ProtoController', () => ({
ProtoController: { ProtoController: {
root: { root: {
ServerInfo_User: { ServerInfo_User: {
@ -26,7 +26,7 @@ import { StatusEnum, WebSocketConnectOptions, WebSocketConnectReason } from 'typ
const testOptions: WebSocketConnectOptions = { host: 'localhost', port: '4748', userName: 'user', password: 'pw' }; const testOptions: WebSocketConnectOptions = { host: 'localhost', port: '4748', userName: 'user', password: 'pw' };
beforeEach(() => jest.clearAllMocks()); beforeEach(() => vi.clearAllMocks());
describe('AuthenticationService', () => { describe('AuthenticationService', () => {
describe('login', () => { describe('login', () => {

View file

@ -1,11 +1,11 @@
jest.mock('websocket', () => ({ vi.mock('websocket', () => ({
ModeratorCommands: { ModeratorCommands: {
banFromServer: jest.fn(), banFromServer: vi.fn(),
getBanHistory: jest.fn(), getBanHistory: vi.fn(),
getWarnHistory: jest.fn(), getWarnHistory: vi.fn(),
getWarnList: jest.fn(), getWarnList: vi.fn(),
viewLogHistory: jest.fn(), viewLogHistory: vi.fn(),
warnUser: jest.fn(), warnUser: vi.fn(),
}, },
})); }));
@ -13,7 +13,7 @@ import { ModeratorService } from './ModeratorService';
import { ModeratorCommands } from 'websocket'; import { ModeratorCommands } from 'websocket';
import { LogFilters } from 'types'; import { LogFilters } from 'types';
beforeEach(() => jest.clearAllMocks()); beforeEach(() => vi.clearAllMocks());
describe('ModeratorService', () => { describe('ModeratorService', () => {
describe('banFromServer', () => { describe('banFromServer', () => {

View file

@ -1,17 +1,17 @@
jest.mock('websocket', () => ({ vi.mock('websocket', () => ({
SessionCommands: { SessionCommands: {
joinRoom: jest.fn(), joinRoom: vi.fn(),
}, },
RoomCommands: { RoomCommands: {
leaveRoom: jest.fn(), leaveRoom: vi.fn(),
roomSay: jest.fn(), roomSay: vi.fn(),
}, },
})); }));
import { RoomsService } from './RoomsService'; import { RoomsService } from './RoomsService';
import { RoomCommands, SessionCommands } from 'websocket'; import { RoomCommands, SessionCommands } from 'websocket';
beforeEach(() => jest.clearAllMocks()); beforeEach(() => vi.clearAllMocks());
describe('RoomsService', () => { describe('RoomsService', () => {
describe('joinRoom', () => { describe('joinRoom', () => {

View file

@ -1,22 +1,22 @@
jest.mock('websocket', () => ({ vi.mock('websocket', () => ({
SessionCommands: { SessionCommands: {
addToBuddyList: jest.fn(), addToBuddyList: vi.fn(),
removeFromBuddyList: jest.fn(), removeFromBuddyList: vi.fn(),
addToIgnoreList: jest.fn(), addToIgnoreList: vi.fn(),
removeFromIgnoreList: jest.fn(), removeFromIgnoreList: vi.fn(),
accountPassword: jest.fn(), accountPassword: vi.fn(),
accountEdit: jest.fn(), accountEdit: vi.fn(),
accountImage: jest.fn(), accountImage: vi.fn(),
message: jest.fn(), message: vi.fn(),
getUserInfo: jest.fn(), getUserInfo: vi.fn(),
getGamesOfUser: jest.fn(), getGamesOfUser: vi.fn(),
}, },
})); }));
import { SessionService } from './SessionService'; import { SessionService } from './SessionService';
import { SessionCommands } from 'websocket'; import { SessionCommands } from 'websocket';
beforeEach(() => jest.clearAllMocks()); beforeEach(() => vi.clearAllMocks());
describe('SessionService', () => { describe('SessionService', () => {
describe('addToBuddyList', () => { describe('addToBuddyList', () => {

View file

@ -10,7 +10,7 @@ import { useFireOnce } from './useFireOnce';
describe('useFireOnce hook', () => { describe('useFireOnce hook', () => {
test('it only fires once when button is clicked twice', async () => { test('it only fires once when button is clicked twice', async () => {
// Mock a promise with a delay // Mock a promise with a delay
const onClickWithPromise = jest.fn((e) => { const onClickWithPromise = vi.fn((e) => {
e.preventDefault() e.preventDefault()
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
@ -54,7 +54,7 @@ describe('useFireOnce hook', () => {
test('it only fires once when form is submitted twice', async () => { test('it only fires once when form is submitted twice', async () => {
// Mock a promise with a delay // Mock a promise with a delay
const onClickWithPromise = jest.fn((e) => { const onClickWithPromise = vi.fn((e) => {
e.preventDefault() e.preventDefault()
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {

View file

@ -4,7 +4,7 @@ import { Language } from 'types';
class I18nBackend { class I18nBackend {
static type: ModuleType = 'backend'; static type: ModuleType = 'backend';
static BASE_URL = `${process.env.PUBLIC_URL}/locales`; static BASE_URL = `${import.meta.env.BASE_URL}locales`;
read(language, namespace, callback) { read(language, namespace, callback) {
if (!Language[language]) { if (!Language[language]) {

View file

@ -1,256 +1,254 @@
// Remove !file-loader! once the following is no longer an issue import ad from './ad.svg';
// https://github.com/facebook/create-react-app/issues/11770 import ae from './ae.svg';
import ad from '!file-loader!./ad.svg'; import af from './af.svg';
import ae from '!file-loader!./ae.svg'; import ag from './ag.svg';
import af from '!file-loader!./af.svg'; import ai from './ai.svg';
import ag from '!file-loader!./ag.svg'; import al from './al.svg';
import ai from '!file-loader!./ai.svg'; import am from './am.svg';
import al from '!file-loader!./al.svg'; import ao from './ao.svg';
import am from '!file-loader!./am.svg'; import aq from './aq.svg';
import ao from '!file-loader!./ao.svg'; import ar from './ar.svg';
import aq from '!file-loader!./aq.svg'; import as from './as.svg';
import ar from '!file-loader!./ar.svg'; import at from './at.svg';
import as from '!file-loader!./as.svg'; import au from './au.svg';
import at from '!file-loader!./at.svg'; import aw from './aw.svg';
import au from '!file-loader!./au.svg'; import ax from './ax.svg';
import aw from '!file-loader!./aw.svg'; import az from './az.svg';
import ax from '!file-loader!./ax.svg'; import ba from './ba.svg';
import az from '!file-loader!./az.svg'; import bb from './bb.svg';
import ba from '!file-loader!./ba.svg'; import bd from './bd.svg';
import bb from '!file-loader!./bb.svg'; import be from './be.svg';
import bd from '!file-loader!./bd.svg'; import bf from './bf.svg';
import be from '!file-loader!./be.svg'; import bg from './bg.svg';
import bf from '!file-loader!./bf.svg'; import bh from './bh.svg';
import bg from '!file-loader!./bg.svg'; import bi from './bi.svg';
import bh from '!file-loader!./bh.svg'; import bj from './bj.svg';
import bi from '!file-loader!./bi.svg'; import bl from './bl.svg';
import bj from '!file-loader!./bj.svg'; import bm from './bm.svg';
import bl from '!file-loader!./bl.svg'; import bn from './bn.svg';
import bm from '!file-loader!./bm.svg'; import bo from './bo.svg';
import bn from '!file-loader!./bn.svg'; import bq from './bq.svg';
import bo from '!file-loader!./bo.svg'; import br from './br.svg';
import bq from '!file-loader!./bq.svg'; import bs from './bs.svg';
import br from '!file-loader!./br.svg'; import bt from './bt.svg';
import bs from '!file-loader!./bs.svg'; import bv from './bv.svg';
import bt from '!file-loader!./bt.svg'; import bw from './bw.svg';
import bv from '!file-loader!./bv.svg'; import by from './by.svg';
import bw from '!file-loader!./bw.svg'; import bz from './bz.svg';
import by from '!file-loader!./by.svg'; import ca from './ca.svg';
import bz from '!file-loader!./bz.svg'; import cc from './cc.svg';
import ca from '!file-loader!./ca.svg'; import cd from './cd.svg';
import cc from '!file-loader!./cc.svg'; import cf from './cf.svg';
import cd from '!file-loader!./cd.svg'; import cg from './cg.svg';
import cf from '!file-loader!./cf.svg'; import ch from './ch.svg';
import cg from '!file-loader!./cg.svg'; import ci from './ci.svg';
import ch from '!file-loader!./ch.svg'; import ck from './ck.svg';
import ci from '!file-loader!./ci.svg'; import cl from './cl.svg';
import ck from '!file-loader!./ck.svg'; import cm from './cm.svg';
import cl from '!file-loader!./cl.svg'; import cn from './cn.svg';
import cm from '!file-loader!./cm.svg'; import co from './co.svg';
import cn from '!file-loader!./cn.svg'; import cr from './cr.svg';
import co from '!file-loader!./co.svg'; import cu from './cu.svg';
import cr from '!file-loader!./cr.svg'; import cv from './cv.svg';
import cu from '!file-loader!./cu.svg'; import cw from './cw.svg';
import cv from '!file-loader!./cv.svg'; import cx from './cx.svg';
import cw from '!file-loader!./cw.svg'; import cy from './cy.svg';
import cx from '!file-loader!./cx.svg'; import cz from './cz.svg';
import cy from '!file-loader!./cy.svg'; import de from './de.svg';
import cz from '!file-loader!./cz.svg'; import dj from './dj.svg';
import de from '!file-loader!./de.svg'; import dk from './dk.svg';
import dj from '!file-loader!./dj.svg'; import dm from './dm.svg';
import dk from '!file-loader!./dk.svg'; import _do from './do.svg';
import dm from '!file-loader!./dm.svg'; import dz from './dz.svg';
import _do from '!file-loader!./do.svg'; import ec from './ec.svg';
import dz from '!file-loader!./dz.svg'; import ee from './ee.svg';
import ec from '!file-loader!./ec.svg'; import eg from './eg.svg';
import ee from '!file-loader!./ee.svg'; import eh from './eh.svg';
import eg from '!file-loader!./eg.svg'; import er from './er.svg';
import eh from '!file-loader!./eh.svg'; import es from './es.svg';
import er from '!file-loader!./er.svg'; import et from './et.svg';
import es from '!file-loader!./es.svg'; import eu from './eu.svg';
import et from '!file-loader!./et.svg'; import fi from './fi.svg';
import eu from '!file-loader!./eu.svg'; import fj from './fj.svg';
import fi from '!file-loader!./fi.svg'; import fk from './fk.svg';
import fj from '!file-loader!./fj.svg'; import fm from './fm.svg';
import fk from '!file-loader!./fk.svg'; import fo from './fo.svg';
import fm from '!file-loader!./fm.svg'; import fr from './fr.svg';
import fo from '!file-loader!./fo.svg'; import ga from './ga.svg';
import fr from '!file-loader!./fr.svg'; import gb from './gb.svg';
import ga from '!file-loader!./ga.svg'; import gd from './gd.svg';
import gb from '!file-loader!./gb.svg'; import ge from './ge.svg';
import gd from '!file-loader!./gd.svg'; import gf from './gf.svg';
import ge from '!file-loader!./ge.svg'; import gg from './gg.svg';
import gf from '!file-loader!./gf.svg'; import gh from './gh.svg';
import gg from '!file-loader!./gg.svg'; import gi from './gi.svg';
import gh from '!file-loader!./gh.svg'; import gl from './gl.svg';
import gi from '!file-loader!./gi.svg'; import gm from './gm.svg';
import gl from '!file-loader!./gl.svg'; import gn from './gn.svg';
import gm from '!file-loader!./gm.svg'; import gp from './gp.svg';
import gn from '!file-loader!./gn.svg'; import gq from './gq.svg';
import gp from '!file-loader!./gp.svg'; import gr from './gr.svg';
import gq from '!file-loader!./gq.svg'; import gs from './gs.svg';
import gr from '!file-loader!./gr.svg'; import gt from './gt.svg';
import gs from '!file-loader!./gs.svg'; import gu from './gu.svg';
import gt from '!file-loader!./gt.svg'; import gw from './gw.svg';
import gu from '!file-loader!./gu.svg'; import gy from './gy.svg';
import gw from '!file-loader!./gw.svg'; import hk from './hk.svg';
import gy from '!file-loader!./gy.svg'; import hm from './hm.svg';
import hk from '!file-loader!./hk.svg'; import hn from './hn.svg';
import hm from '!file-loader!./hm.svg'; import hr from './hr.svg';
import hn from '!file-loader!./hn.svg'; import ht from './ht.svg';
import hr from '!file-loader!./hr.svg'; import hu from './hu.svg';
import ht from '!file-loader!./ht.svg'; import id from './id.svg';
import hu from '!file-loader!./hu.svg'; import ie from './ie.svg';
import id from '!file-loader!./id.svg'; import il from './il.svg';
import ie from '!file-loader!./ie.svg'; import im from './im.svg';
import il from '!file-loader!./il.svg'; import _in from './in.svg';
import im from '!file-loader!./im.svg'; import io from './io.svg';
import _in from '!file-loader!./in.svg'; import iq from './iq.svg';
import io from '!file-loader!./io.svg'; import ir from './ir.svg';
import iq from '!file-loader!./iq.svg'; import is from './is.svg';
import ir from '!file-loader!./ir.svg'; import it from './it.svg';
import is from '!file-loader!./is.svg'; import je from './je.svg';
import it from '!file-loader!./it.svg'; import jm from './jm.svg';
import je from '!file-loader!./je.svg'; import jo from './jo.svg';
import jm from '!file-loader!./jm.svg'; import jp from './jp.svg';
import jo from '!file-loader!./jo.svg'; import ke from './ke.svg';
import jp from '!file-loader!./jp.svg'; import kg from './kg.svg';
import ke from '!file-loader!./ke.svg'; import kh from './kh.svg';
import kg from '!file-loader!./kg.svg'; import ki from './ki.svg';
import kh from '!file-loader!./kh.svg'; import km from './km.svg';
import ki from '!file-loader!./ki.svg'; import kn from './kn.svg';
import km from '!file-loader!./km.svg'; import kp from './kp.svg';
import kn from '!file-loader!./kn.svg'; import kr from './kr.svg';
import kp from '!file-loader!./kp.svg'; import kw from './kw.svg';
import kr from '!file-loader!./kr.svg'; import ky from './ky.svg';
import kw from '!file-loader!./kw.svg'; import kz from './kz.svg';
import ky from '!file-loader!./ky.svg'; import la from './la.svg';
import kz from '!file-loader!./kz.svg'; import lb from './lb.svg';
import la from '!file-loader!./la.svg'; import lc from './lc.svg';
import lb from '!file-loader!./lb.svg'; import li from './li.svg';
import lc from '!file-loader!./lc.svg'; import lk from './lk.svg';
import li from '!file-loader!./li.svg'; import lr from './lr.svg';
import lk from '!file-loader!./lk.svg'; import ls from './ls.svg';
import lr from '!file-loader!./lr.svg'; import lt from './lt.svg';
import ls from '!file-loader!./ls.svg'; import lu from './lu.svg';
import lt from '!file-loader!./lt.svg'; import lv from './lv.svg';
import lu from '!file-loader!./lu.svg'; import ly from './ly.svg';
import lv from '!file-loader!./lv.svg'; import ma from './ma.svg';
import ly from '!file-loader!./ly.svg'; import mc from './mc.svg';
import ma from '!file-loader!./ma.svg'; import md from './md.svg';
import mc from '!file-loader!./mc.svg'; import me from './me.svg';
import md from '!file-loader!./md.svg'; import mf from './mf.svg';
import me from '!file-loader!./me.svg'; import mg from './mg.svg';
import mf from '!file-loader!./mf.svg'; import mh from './mh.svg';
import mg from '!file-loader!./mg.svg'; import mk from './mk.svg';
import mh from '!file-loader!./mh.svg'; import ml from './ml.svg';
import mk from '!file-loader!./mk.svg'; import mm from './mm.svg';
import ml from '!file-loader!./ml.svg'; import mn from './mn.svg';
import mm from '!file-loader!./mm.svg'; import mo from './mo.svg';
import mn from '!file-loader!./mn.svg'; import mp from './mp.svg';
import mo from '!file-loader!./mo.svg'; import mq from './mq.svg';
import mp from '!file-loader!./mp.svg'; import mr from './mr.svg';
import mq from '!file-loader!./mq.svg'; import ms from './ms.svg';
import mr from '!file-loader!./mr.svg'; import mt from './mt.svg';
import ms from '!file-loader!./ms.svg'; import mu from './mu.svg';
import mt from '!file-loader!./mt.svg'; import mv from './mv.svg';
import mu from '!file-loader!./mu.svg'; import mw from './mw.svg';
import mv from '!file-loader!./mv.svg'; import mx from './mx.svg';
import mw from '!file-loader!./mw.svg'; import my from './my.svg';
import mx from '!file-loader!./mx.svg'; import mz from './mz.svg';
import my from '!file-loader!./my.svg'; import na from './na.svg';
import mz from '!file-loader!./mz.svg'; import nc from './nc.svg';
import na from '!file-loader!./na.svg'; import ne from './ne.svg';
import nc from '!file-loader!./nc.svg'; import nf from './nf.svg';
import ne from '!file-loader!./ne.svg'; import ng from './ng.svg';
import nf from '!file-loader!./nf.svg'; import ni from './ni.svg';
import ng from '!file-loader!./ng.svg'; import nl from './nl.svg';
import ni from '!file-loader!./ni.svg'; import no from './no.svg';
import nl from '!file-loader!./nl.svg'; import np from './np.svg';
import no from '!file-loader!./no.svg'; import nr from './nr.svg';
import np from '!file-loader!./np.svg'; import nu from './nu.svg';
import nr from '!file-loader!./nr.svg'; import nz from './nz.svg';
import nu from '!file-loader!./nu.svg'; import om from './om.svg';
import nz from '!file-loader!./nz.svg'; import pa from './pa.svg';
import om from '!file-loader!./om.svg'; import pe from './pe.svg';
import pa from '!file-loader!./pa.svg'; import pf from './pf.svg';
import pe from '!file-loader!./pe.svg'; import pg from './pg.svg';
import pf from '!file-loader!./pf.svg'; import ph from './ph.svg';
import pg from '!file-loader!./pg.svg'; import pk from './pk.svg';
import ph from '!file-loader!./ph.svg'; import pl from './pl.svg';
import pk from '!file-loader!./pk.svg'; import pm from './pm.svg';
import pl from '!file-loader!./pl.svg'; import pn from './pn.svg';
import pm from '!file-loader!./pm.svg'; import pr from './pr.svg';
import pn from '!file-loader!./pn.svg'; import ps from './ps.svg';
import pr from '!file-loader!./pr.svg'; import pt from './pt.svg';
import ps from '!file-loader!./ps.svg'; import pw from './pw.svg';
import pt from '!file-loader!./pt.svg'; import py from './py.svg';
import pw from '!file-loader!./pw.svg'; import qa from './qa.svg';
import py from '!file-loader!./py.svg'; import re from './re.svg';
import qa from '!file-loader!./qa.svg'; import ro from './ro.svg';
import re from '!file-loader!./re.svg'; import rs from './rs.svg';
import ro from '!file-loader!./ro.svg'; import ru from './ru.svg';
import rs from '!file-loader!./rs.svg'; import rw from './rw.svg';
import ru from '!file-loader!./ru.svg'; import sa from './sa.svg';
import rw from '!file-loader!./rw.svg'; import sb from './sb.svg';
import sa from '!file-loader!./sa.svg'; import sc from './sc.svg';
import sb from '!file-loader!./sb.svg'; import sd from './sd.svg';
import sc from '!file-loader!./sc.svg'; import se from './se.svg';
import sd from '!file-loader!./sd.svg'; import sg from './sg.svg';
import se from '!file-loader!./se.svg'; import sh from './sh.svg';
import sg from '!file-loader!./sg.svg'; import si from './si.svg';
import sh from '!file-loader!./sh.svg'; import sj from './sj.svg';
import si from '!file-loader!./si.svg'; import sk from './sk.svg';
import sj from '!file-loader!./sj.svg'; import sl from './sl.svg';
import sk from '!file-loader!./sk.svg'; import sm from './sm.svg';
import sl from '!file-loader!./sl.svg'; import sn from './sn.svg';
import sm from '!file-loader!./sm.svg'; import so from './so.svg';
import sn from '!file-loader!./sn.svg'; import sr from './sr.svg';
import so from '!file-loader!./so.svg'; import ss from './ss.svg';
import sr from '!file-loader!./sr.svg'; import st from './st.svg';
import ss from '!file-loader!./ss.svg'; import sv from './sv.svg';
import st from '!file-loader!./st.svg'; import sx from './sx.svg';
import sv from '!file-loader!./sv.svg'; import sy from './sy.svg';
import sx from '!file-loader!./sx.svg'; import sz from './sz.svg';
import sy from '!file-loader!./sy.svg'; import tc from './tc.svg';
import sz from '!file-loader!./sz.svg'; import td from './td.svg';
import tc from '!file-loader!./tc.svg'; import tf from './tf.svg';
import td from '!file-loader!./td.svg'; import tg from './tg.svg';
import tf from '!file-loader!./tf.svg'; import th from './th.svg';
import tg from '!file-loader!./tg.svg'; import tj from './tj.svg';
import th from '!file-loader!./th.svg'; import tk from './tk.svg';
import tj from '!file-loader!./tj.svg'; import tl from './tl.svg';
import tk from '!file-loader!./tk.svg'; import tm from './tm.svg';
import tl from '!file-loader!./tl.svg'; import tn from './tn.svg';
import tm from '!file-loader!./tm.svg'; import to from './to.svg';
import tn from '!file-loader!./tn.svg'; import tr from './tr.svg';
import to from '!file-loader!./to.svg'; import tt from './tt.svg';
import tr from '!file-loader!./tr.svg'; import tv from './tv.svg';
import tt from '!file-loader!./tt.svg'; import tw from './tw.svg';
import tv from '!file-loader!./tv.svg'; import tz from './tz.svg';
import tw from '!file-loader!./tw.svg'; import ua from './ua.svg';
import tz from '!file-loader!./tz.svg'; import ug from './ug.svg';
import ua from '!file-loader!./ua.svg'; import um from './um.svg';
import ug from '!file-loader!./ug.svg'; import us from './us.svg';
import um from '!file-loader!./um.svg'; import uy from './uy.svg';
import us from '!file-loader!./us.svg'; import uz from './uz.svg';
import uy from '!file-loader!./uy.svg'; import va from './va.svg';
import uz from '!file-loader!./uz.svg'; import vc from './vc.svg';
import va from '!file-loader!./va.svg'; import ve from './ve.svg';
import vc from '!file-loader!./vc.svg'; import vg from './vg.svg';
import ve from '!file-loader!./ve.svg'; import vi from './vi.svg';
import vg from '!file-loader!./vg.svg'; import vn from './vn.svg';
import vi from '!file-loader!./vi.svg'; import vu from './vu.svg';
import vn from '!file-loader!./vn.svg'; import wf from './wf.svg';
import vu from '!file-loader!./vu.svg'; import ws from './ws.svg';
import wf from '!file-loader!./wf.svg'; import xk from './xk.svg';
import ws from '!file-loader!./ws.svg'; import ye from './ye.svg';
import xk from '!file-loader!./xk.svg'; import yt from './yt.svg';
import ye from '!file-loader!./ye.svg'; import za from './za.svg';
import yt from '!file-loader!./yt.svg'; import zm from './zm.svg';
import za from '!file-loader!./za.svg'; import zw from './zw.svg';
import zm from '!file-loader!./zm.svg';
import zw from '!file-loader!./zw.svg';
export const Countries = { export const Countries = {
ad, ad,

View file

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View file

@ -1,7 +1,7 @@
import protobuf from 'protobufjs'; import protobuf from 'protobufjs';
// ensure jest-dom is always available during testing to cut down on boilerplate // ensure jest-dom is always available during testing to cut down on boilerplate
import '@testing-library/jest-dom'; import '@testing-library/jest-dom/vitest';
class MockProtobufRoot { class MockProtobufRoot {
load() {} load() {}

View file

@ -1,4 +1,4 @@
jest.mock('store/store', () => ({ store: { dispatch: jest.fn() } })); vi.mock('store/store', () => ({ store: { dispatch: vi.fn() } }));
import { store } from 'store/store'; import { store } from 'store/store';
import { Actions } from './game.actions'; import { Actions } from './game.actions';
@ -11,7 +11,7 @@ import {
makePlayerProperties, makePlayerProperties,
} from './__mocks__/fixtures'; } from './__mocks__/fixtures';
beforeEach(() => jest.clearAllMocks()); beforeEach(() => vi.clearAllMocks());
describe('Dispatch', () => { describe('Dispatch', () => {
it('clearStore dispatches Actions.clearStore()', () => { it('clearStore dispatches Actions.clearStore()', () => {

View file

@ -890,14 +890,14 @@ describe('2J: Turn, phase, and chat', () => {
it('GAME_SAY → appends message with mocked Date.now() as timeReceived', () => { it('GAME_SAY → appends message with mocked Date.now() as timeReceived', () => {
const state = makeState(); const state = makeState();
jest.spyOn(Date, 'now').mockReturnValue(123456789); vi.spyOn(Date, 'now').mockReturnValue(123456789);
const result = gamesReducer(state, { const result = gamesReducer(state, {
type: Types.GAME_SAY, type: Types.GAME_SAY,
gameId: 1, gameId: 1,
playerId: 2, playerId: 2,
message: 'gg', message: 'gg',
}); });
jest.restoreAllMocks(); vi.restoreAllMocks();
expect(result.games[1].messages).toHaveLength(1); expect(result.games[1].messages).toHaveLength(1);
expect(result.games[1].messages[0]).toEqual({ playerId: 2, message: 'gg', timeReceived: 123456789 }); expect(result.games[1].messages[0]).toEqual({ playerId: 2, message: 'gg', timeReceived: 123456789 });

View file

@ -1,6 +1,6 @@
jest.mock('store/store', () => ({ store: { dispatch: jest.fn() } })); vi.mock('store/store', () => ({ store: { dispatch: vi.fn() } }));
jest.mock('redux-form', () => ({ vi.mock('redux-form', () => ({
reset: jest.fn((form) => ({ type: '@@redux-form/RESET', meta: { form } })), reset: vi.fn((form) => ({ type: '@@redux-form/RESET', meta: { form } })),
})); }));
import { store } from 'store/store'; import { store } from 'store/store';
@ -10,7 +10,7 @@ import { Dispatch } from './rooms.dispatch';
import { makeGame, makeMessage, makeRoom, makeUser } from './__mocks__/rooms-fixtures'; import { makeGame, makeMessage, makeRoom, makeUser } from './__mocks__/rooms-fixtures';
import { GameSortField, SortDirection } from 'types'; import { GameSortField, SortDirection } from 'types';
beforeEach(() => jest.clearAllMocks()); beforeEach(() => vi.clearAllMocks());
describe('Dispatch', () => { describe('Dispatch', () => {
it('clearStore dispatches Actions.clearStore()', () => { it('clearStore dispatches Actions.clearStore()', () => {
@ -45,7 +45,7 @@ describe('Dispatch', () => {
it('addMessage with message.name truthy → dispatches reset("sayMessage") then Actions.addMessage()', () => { it('addMessage with message.name truthy → dispatches reset("sayMessage") then Actions.addMessage()', () => {
const message = { ...makeMessage(), name: 'Alice' }; const message = { ...makeMessage(), name: 'Alice' };
Dispatch.addMessage(1, message); Dispatch.addMessage(1, message);
expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as jest.Mock)('sayMessage')); expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as vi.Mock)('sayMessage'));
expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addMessage(1, message)); expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addMessage(1, message));
}); });

View file

@ -1,6 +1,6 @@
jest.mock('store/store', () => ({ store: { dispatch: jest.fn() } })); vi.mock('store/store', () => ({ store: { dispatch: vi.fn() } }));
jest.mock('redux-form', () => ({ vi.mock('redux-form', () => ({
reset: jest.fn((form) => ({ type: '@@redux-form/RESET', meta: { form } })), reset: vi.fn((form) => ({ type: '@@redux-form/RESET', meta: { form } })),
})); }));
import { store } from 'store/store'; import { store } from 'store/store';
@ -18,7 +18,7 @@ import {
makeWarnListItem, makeWarnListItem,
} from './__mocks__/server-fixtures'; } from './__mocks__/server-fixtures';
beforeEach(() => jest.clearAllMocks()); beforeEach(() => vi.clearAllMocks());
describe('Dispatch', () => { describe('Dispatch', () => {
it('initialized dispatches Actions.initialized()', () => { it('initialized dispatches Actions.initialized()', () => {
@ -71,7 +71,7 @@ describe('Dispatch', () => {
it('addToBuddyList dispatches reset("addToBuddies") then Actions.addToBuddyList()', () => { it('addToBuddyList dispatches reset("addToBuddies") then Actions.addToBuddyList()', () => {
const user = makeUser(); const user = makeUser();
Dispatch.addToBuddyList(user); Dispatch.addToBuddyList(user);
expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as jest.Mock)('addToBuddies')); expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as vi.Mock)('addToBuddies'));
expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addToBuddyList(user)); expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addToBuddyList(user));
}); });
@ -89,7 +89,7 @@ describe('Dispatch', () => {
it('addToIgnoreList dispatches reset("addToIgnore") then Actions.addToIgnoreList()', () => { it('addToIgnoreList dispatches reset("addToIgnore") then Actions.addToIgnoreList()', () => {
const user = makeUser(); const user = makeUser();
Dispatch.addToIgnoreList(user); Dispatch.addToIgnoreList(user);
expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as jest.Mock)('addToIgnore')); expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as vi.Mock)('addToIgnore'));
expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addToIgnoreList(user)); expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addToIgnoreList(user));
}); });

2
webclient/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vitest/globals" />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,34 +1,34 @@
jest.mock('../../persistence', () => ({ vi.mock('../../persistence', () => ({
GamePersistence: { GamePersistence: {
gameStateChanged: jest.fn(), gameStateChanged: vi.fn(),
playerJoined: jest.fn(), playerJoined: vi.fn(),
playerLeft: jest.fn(), playerLeft: vi.fn(),
playerPropertiesChanged: jest.fn(), playerPropertiesChanged: vi.fn(),
gameClosed: jest.fn(), gameClosed: vi.fn(),
gameHostChanged: jest.fn(), gameHostChanged: vi.fn(),
kicked: jest.fn(), kicked: vi.fn(),
gameSay: jest.fn(), gameSay: vi.fn(),
cardMoved: jest.fn(), cardMoved: vi.fn(),
cardFlipped: jest.fn(), cardFlipped: vi.fn(),
cardDestroyed: jest.fn(), cardDestroyed: vi.fn(),
cardAttached: jest.fn(), cardAttached: vi.fn(),
tokenCreated: jest.fn(), tokenCreated: vi.fn(),
cardAttrChanged: jest.fn(), cardAttrChanged: vi.fn(),
cardCounterChanged: jest.fn(), cardCounterChanged: vi.fn(),
arrowCreated: jest.fn(), arrowCreated: vi.fn(),
arrowDeleted: jest.fn(), arrowDeleted: vi.fn(),
counterCreated: jest.fn(), counterCreated: vi.fn(),
counterSet: jest.fn(), counterSet: vi.fn(),
counterDeleted: jest.fn(), counterDeleted: vi.fn(),
cardsDrawn: jest.fn(), cardsDrawn: vi.fn(),
cardsRevealed: jest.fn(), cardsRevealed: vi.fn(),
zoneShuffled: jest.fn(), zoneShuffled: vi.fn(),
dieRolled: jest.fn(), dieRolled: vi.fn(),
activePlayerSet: jest.fn(), activePlayerSet: vi.fn(),
activePhaseSet: jest.fn(), activePhaseSet: vi.fn(),
turnReversed: jest.fn(), turnReversed: vi.fn(),
zoneDumped: jest.fn(), zoneDumped: vi.fn(),
zonePropertiesChanged: jest.fn(), zonePropertiesChanged: vi.fn(),
}, },
})); }));
@ -63,7 +63,7 @@ import { setCardCounter } from './setCardCounter';
import { setCounter } from './setCounter'; import { setCounter } from './setCounter';
import { shuffle } from './shuffle'; import { shuffle } from './shuffle';
beforeEach(() => jest.clearAllMocks()); beforeEach(() => vi.clearAllMocks());
const meta = { gameId: 5, playerId: 2, context: null, secondsElapsed: 0, forcedByJudge: 0 }; 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: { RoomPersistence: {
userJoined: jest.fn(), userJoined: vi.fn(),
userLeft: jest.fn(), userLeft: vi.fn(),
updateGames: jest.fn(), updateGames: vi.fn(),
removeMessages: jest.fn(), removeMessages: vi.fn(),
addMessage: jest.fn(), addMessage: vi.fn(),
}, },
})); }));
import { RoomPersistence } from '../../persistence'; 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 } }); const makeRoomEvent = (roomId: number) => ({ roomEvent: { roomId } });
beforeEach(() => jest.clearAllMocks()); beforeEach(() => vi.clearAllMocks());
describe('joinRoom room event', () => { describe('joinRoom room event', () => {
const { joinRoom } = jest.requireActual('./joinRoom');
it('calls RoomPersistence.userJoined with roomId and userInfo', () => { it('calls RoomPersistence.userJoined with roomId and userInfo', () => {
const userInfo = { name: 'alice' } as any; const userInfo = { name: 'alice' } as any;
@ -25,7 +29,6 @@ describe('joinRoom room event', () => {
}); });
describe('leaveRoom room event', () => { describe('leaveRoom room event', () => {
const { leaveRoom } = jest.requireActual('./leaveRoom');
it('calls RoomPersistence.userLeft with roomId and name', () => { it('calls RoomPersistence.userLeft with roomId and name', () => {
leaveRoom({ name: 'alice' }, makeRoomEvent(4)); leaveRoom({ name: 'alice' }, makeRoomEvent(4));
@ -34,7 +37,6 @@ describe('leaveRoom room event', () => {
}); });
describe('listGames room event', () => { describe('listGames room event', () => {
const { listGames } = jest.requireActual('./listGames');
it('calls RoomPersistence.updateGames with roomId and gameList', () => { it('calls RoomPersistence.updateGames with roomId and gameList', () => {
const gameList = [{ gameId: 1 }] as any; const gameList = [{ gameId: 1 }] as any;
@ -44,7 +46,6 @@ describe('listGames room event', () => {
}); });
describe('removeMessages room event', () => { describe('removeMessages room event', () => {
const { removeMessages } = jest.requireActual('./removeMessages');
it('calls RoomPersistence.removeMessages with roomId, name, amount', () => { it('calls RoomPersistence.removeMessages with roomId, name, amount', () => {
removeMessages({ name: 'bob', amount: 10 }, makeRoomEvent(6)); removeMessages({ name: 'bob', amount: 10 }, makeRoomEvent(6));
@ -53,7 +54,6 @@ describe('removeMessages room event', () => {
}); });
describe('roomSay room event', () => { describe('roomSay room event', () => {
const { roomSay } = jest.requireActual('./roomSay');
it('calls RoomPersistence.addMessage with roomId and message', () => { it('calls RoomPersistence.addMessage with roomId and message', () => {
const msg = { text: 'hello' } as any; const msg = { text: 'hello' } as any;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +1,16 @@
jest.mock('../persistence', () => ({ vi.mock('../persistence', () => ({
SessionPersistence: { initialized: jest.fn() }, 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 { ProtoController } from './ProtoController';
import { SessionPersistence } from '../persistence'; import { SessionPersistence } from '../persistence';
import protobuf from 'protobufjs'; import protobuf from 'protobufjs';
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); vi.clearAllMocks();
ProtoController.root = null; ProtoController.root = null;
(process.env as any).PUBLIC_URL = '';
}); });
describe('ProtoController', () => { describe('ProtoController', () => {
@ -22,7 +21,7 @@ describe('ProtoController', () => {
}); });
it('calls initialized when callback succeeds', () => { 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 ((_files: any, _opts: any, cb: any) => cb(null)) as any
); );
ProtoController.load(); ProtoController.load();
@ -31,7 +30,7 @@ describe('ProtoController', () => {
}); });
it('throws when callback receives an error', () => { 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 ((_files: any, _opts: any, cb: any) => cb(new Error('load failed'))) as any
); );
expect(() => ProtoController.load()).toThrow('load failed'); expect(() => ProtoController.load()).toThrow('load failed');

View file

@ -3,7 +3,7 @@ import protobuf from 'protobufjs';
import { SessionPersistence } from '../persistence'; import { SessionPersistence } from '../persistence';
import ProtoFiles from '../../proto-files.json'; 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. // Leaf module — no imports from the websocket layer other than persistence.
// Both BackendService and ProtobufService import this; neither should import // Both BackendService and ProtobufService import this; neither should import

View file

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

View file

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

View file

@ -11,7 +11,7 @@ describe('guid', () => {
}); });
it('returns deterministic value when Math.random is mocked', () => { 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(); const result = guid();
expect(result).toBe(guid()); expect(result).toBe(guid());
spy.mockRestore(); spy.mockRestore();

View file

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

View file

@ -6,7 +6,7 @@ describe('sanitizeHtml', () => {
}); });
it('allows <br> tag', () => { 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', () => { it('allows <b> tag', () => {
@ -14,7 +14,7 @@ describe('sanitizeHtml', () => {
}); });
it('allows <img> tag', () => { it('allows <img> tag', () => {
expect(sanitizeHtml('<img>')).toBe('<img />'); expect(sanitizeHtml('<img>')).toBe('<img>');
}); });
it('allows <center> tag', () => { 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 { export function sanitizeHtml(msg: string): string {
return sanitize(msg, { return DOMPurify.sanitize(msg, {
allowedTags: ['br', 'a', 'img', 'center', 'b', 'font'], ALLOWED_TAGS: ['br', 'a', 'img', 'center', 'b', 'font'],
allowedAttributes: { ALLOWED_ATTR: ['href', 'color', 'rel', 'target', 'src', 'alt'],
'*': ['href', 'color', 'rel', 'target'], ADD_URI_SAFE_ATTR: ['color'],
'img': ['src', 'alt'], ALLOWED_URI_REGEXP: /^(?:(?:https?|ftp):)/i,
},
allowedSchemes: ['http', 'https', 'ftp'],
transformTags: {
'a': sanitize.simpleTransform('a', {
target: '_blank',
rel: 'noopener noreferrer',
}),
}
}); });
} }

View file

@ -17,7 +17,7 @@
"strict": false, "strict": false,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,

21
webclient/vite.config.ts Normal file
View file

@ -0,0 +1,21 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
publicDir: 'public',
build: {
outDir: 'build',
},
server: {
open: true,
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/setupTests.ts'],
include: ['src/**/*.spec.{ts,tsx}'],
css: true,
},
});