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
matrix:
node_version:
- 16
- 20
- lts/*
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 = {
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {"project": ["./tsconfig.json"]},
"parserOptions": {"ecmaVersion": 2020, "sourceType": "module", "ecmaFeatures": {"jsx": true}},
"plugins": [
"@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": {
"prebuild": "node prebuild.js",
"prestart": "node prebuild.js",
"build": "react-scripts build",
"start": "react-scripts start",
"test": "react-scripts test",
"test:watch": "react-scripts test",
"eject": "react-scripts eject",
"lint": "eslint \"./**/*.{ts,tsx}\"",
"lint:fix": "eslint \"./**/*.{ts,tsx}\" --fix",
"build": "vite build",
"start": "vite",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint src/**/*.{ts,tsx}",
"lint:fix": "eslint src/**/*.{ts,tsx} --fix",
"golden": "npm run lint && npm run test",
"prepare": "cd .. && husky install",
"translate": "node prebuild.js -i18nOnly"
@ -23,6 +23,7 @@
"@mui/material": "^5.5.1",
"crypto-js": "^4.2.0",
"dexie": "^3.2.2",
"dompurify": "^3.3.3",
"final-form": "^4.20.6",
"final-form-set-field-touched": "^1.0.1",
"i18next": "^22.0.4",
@ -39,21 +40,18 @@
"react-i18next": "^12.0.0",
"react-redux": "^8.0.4",
"react-router-dom": "^6.2.2",
"react-scripts": "5.0.1",
"react-virtualized-auto-sizer": "^1.0.6",
"react-window": "^1.8.6",
"redux": "^4.1.2",
"redux-form": "^8.3.8",
"redux-thunk": "^2.4.1",
"rxjs": "^7.5.4",
"sanitize-html": "^2.7.3"
"rxjs": "^7.5.4"
},
"devDependencies": {
"@babel/core": "^7.17.5",
"@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",
"@types/jest": "29.2.0",
"@types/dompurify": "^3.0.5",
"@types/jquery": "^3.5.14",
"@types/lodash": "^4.14.179",
"@types/node": "18.11.7",
@ -67,12 +65,16 @@
"@types/redux-form": "^8.3.3",
"@typescript-eslint/eslint-plugin": "^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",
"husky": "^8.0.1",
"typescript": "^4.6.2"
},
"eslintConfig": {
"extends": "react-app"
"jsdom": "^24.0.0",
"typescript": "^4.6.2",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.3.0"
},
"browserslist": {
"production": [
@ -85,10 +87,5 @@
"last 1 firefox 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",
"name": "Create React App Sample",
"short_name": "Webatrice",
"name": "Webatrice",
"icons": [
{
"src": "favicon.ico",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import { Language } from 'types';
class I18nBackend {
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) {
if (!Language[language]) {

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import protobuf from 'protobufjs';
// 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 {
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 { Actions } from './game.actions';
@ -11,7 +11,7 @@ import {
makePlayerProperties,
} from './__mocks__/fixtures';
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
describe('Dispatch', () => {
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', () => {
const state = makeState();
jest.spyOn(Date, 'now').mockReturnValue(123456789);
vi.spyOn(Date, 'now').mockReturnValue(123456789);
const result = gamesReducer(state, {
type: Types.GAME_SAY,
gameId: 1,
playerId: 2,
message: 'gg',
});
jest.restoreAllMocks();
vi.restoreAllMocks();
expect(result.games[1].messages).toHaveLength(1);
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() } }));
jest.mock('redux-form', () => ({
reset: jest.fn((form) => ({ type: '@@redux-form/RESET', meta: { form } })),
vi.mock('store/store', () => ({ store: { dispatch: vi.fn() } }));
vi.mock('redux-form', () => ({
reset: vi.fn((form) => ({ type: '@@redux-form/RESET', meta: { form } })),
}));
import { store } from 'store/store';
@ -10,7 +10,7 @@ import { Dispatch } from './rooms.dispatch';
import { makeGame, makeMessage, makeRoom, makeUser } from './__mocks__/rooms-fixtures';
import { GameSortField, SortDirection } from 'types';
beforeEach(() => jest.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
describe('Dispatch', () => {
it('clearStore dispatches Actions.clearStore()', () => {
@ -45,7 +45,7 @@ describe('Dispatch', () => {
it('addMessage with message.name truthy → dispatches reset("sayMessage") then Actions.addMessage()', () => {
const message = { ...makeMessage(), name: 'Alice' };
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));
});

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,7 +17,7 @@
"strict": false,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": 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,
},
});