mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-13 12:14:59 +00:00
feat(online)!: seat two to four players in a room
A room holds up to four people and needs two to start. Both numbers are server constants sent to the client in RoomState, so the lobby draws whatever the server allows and widening a room is a server change alone. Failing a turn eliminates that player rather than ending the game. The syllable and the used words survive them, the turn passes to whoever is next, and the last player standing wins. Two seats is that same rule seen from close up, which is why there is one implementation of it and not two. A dead end still costs the first player to face it their own clock, as before: they get their turn, and lose it. Everyone behind them has already seen that board, so they go out together rather than each sitting out a turn limit they cannot use — which leaves the player who closed the position standing, the same outcome two players get. A player who is knocked out keeps their seat. They watch the rest of the game, chat included, with only the word input gone, and everybody lands back in the same lobby when it ends. The result screen is the whole table, ranked by who outlasted whom, with each score reported beside the place rather than deciding it. The turn clock is deliberately not paused for a seat that has dropped, so a player who loses their connection on their own turn loses it the way anybody else would. Their reconnect window decides only whether they are still in the game afterwards. Any number of windows can be open at once, settled by one timer armed for the nearest of them. Starting waits for every guest, not merely the first: a room of four that began on one yes would have dealt three people a turn they never agreed to. Kicking names a seat and is still refused on a player who is ready, and on the owner's own — leaving is what an owner who wants out does, and it hands the room on. Joining stays a lobby thing: a room with a game running turns a latecomer away even with seats going spare, because there is no way to hand somebody a game already in progress. BREAKING CHANGE: RoomState, TurnUpdate and GameOver lose the fields that could only ever describe a second player, OpponentLeft is retired in favour of presence on RoomState, and suggestions move to the new PlayerEliminated — they describe the position that beat a player, which by the end of a longer game is nobody else's position. ProtocolVersion goes to 2, so a client built against 1 is refused with a readable error rather than decoding a frame that now means something else.
This commit is contained in:
1 parent
0b507c3537
commit
4e2e9e1433
54 files changed
+3692
-1129
No files matched your search
+198
-65
@@ -18,6 +18,14 @@ function msg(kind, value) {
|
||||
return create(ServerMessageSchema, { payload: { case: kind, value } });
|
||||
}
|
||||
|
||||
/** The two players every in-game fixture below is scored for. */
|
||||
function table(mine = 0, theirs = 0) {
|
||||
return [
|
||||
{ playerId: 'p1', name: 'Minh', isMe: true, score: mine, connected: true },
|
||||
{ playerId: 'p2', name: 'Lan', isMe: false, score: theirs, connected: true }
|
||||
];
|
||||
}
|
||||
|
||||
function started(overrides = {}) {
|
||||
return msg('gameStarted', {
|
||||
openingWord: 'học sinh',
|
||||
@@ -26,6 +34,8 @@ function started(overrides = {}) {
|
||||
deadlineUnixMs: 1_700_000_020_000n,
|
||||
turnSeq: 1,
|
||||
turnLimitMs: 20_000,
|
||||
players: table(),
|
||||
turnPlayerId: 'p1',
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
@@ -82,14 +92,21 @@ describe('turnUpdate', () => {
|
||||
store.apply(started());
|
||||
store.apply(
|
||||
msg('turnUpdate', {
|
||||
played: { word: 'sinh viên', byMe: true, points: 12, syllables: 2, typed: 'sinh vien' },
|
||||
played: {
|
||||
word: 'sinh viên',
|
||||
byMe: true,
|
||||
points: 12,
|
||||
syllables: 2,
|
||||
typed: 'sinh vien',
|
||||
playerId: 'p1'
|
||||
},
|
||||
currentSyllable: 'viên',
|
||||
myTurn: false,
|
||||
deadlineUnixMs: 1_700_000_040_000n,
|
||||
turnSeq: 2,
|
||||
myScore: 12,
|
||||
opponentScore: 0,
|
||||
chainLength: 2
|
||||
chainLength: 2,
|
||||
players: table(12, 0),
|
||||
turnPlayerId: 'p2'
|
||||
})
|
||||
);
|
||||
|
||||
@@ -103,11 +120,39 @@ describe('turnUpdate', () => {
|
||||
});
|
||||
expect(store.state.currentSyllable).toBe('viên');
|
||||
expect(store.state.myTurn).toBe(false);
|
||||
expect(store.state.myScore).toBe(12);
|
||||
expect(store.myScore).toBe(12);
|
||||
expect(store.state.turnPlayerId).toBe('p2');
|
||||
expect(store.state.chainLength).toBe(2);
|
||||
expect(store.state.turnSeq).toBe(2);
|
||||
});
|
||||
|
||||
it('moves the turn on without a word when an elimination did it', () => {
|
||||
// The syllable and the chain survive the player who could not answer
|
||||
// them, so there is nothing to append — but the deadline and the player
|
||||
// to act are both new.
|
||||
const store = createGameStore();
|
||||
store.apply(started());
|
||||
store.apply(
|
||||
msg('turnUpdate', {
|
||||
currentSyllable: 'sinh',
|
||||
myTurn: true,
|
||||
deadlineUnixMs: 1_700_000_050_000n,
|
||||
turnSeq: 3,
|
||||
chainLength: 1,
|
||||
players: [
|
||||
{ playerId: 'p1', name: 'Minh', isMe: true, score: 0, connected: true },
|
||||
{ playerId: 'p2', name: 'Lan', score: 0, eliminated: true, connected: true }
|
||||
],
|
||||
turnPlayerId: 'p1'
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.state.chain).toHaveLength(1);
|
||||
expect(store.state.turnSeq).toBe(3);
|
||||
expect(store.state.deadlineMs).toBe(1_700_000_050_000);
|
||||
expect(store.state.gamePlayers[1].eliminated).toBe(true);
|
||||
});
|
||||
|
||||
it('clears the standing rejection, because an accepted move answers it', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(started());
|
||||
@@ -156,8 +201,11 @@ describe('gameOver', () => {
|
||||
msg('gameOver', {
|
||||
iWon: true,
|
||||
reason: GameEndReason.NO_LEGAL_MOVE,
|
||||
myScore: 42,
|
||||
chainLength: 7
|
||||
chainLength: 7,
|
||||
standings: [
|
||||
{ playerId: 'p1', name: 'Minh', isMe: true, score: 42, connected: true, rank: 1 },
|
||||
{ playerId: 'p2', name: 'Lan', score: 8, eliminated: true, connected: true, rank: 2 }
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
@@ -167,35 +215,81 @@ describe('gameOver', () => {
|
||||
iWon: true,
|
||||
reason: GameEndReason.NO_LEGAL_MOVE,
|
||||
myScore: 42,
|
||||
chainLength: 7,
|
||||
suggestions: []
|
||||
chainLength: 7
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the words the losing side could have played', () => {
|
||||
it('keeps the final table in the order the server ranked it', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(started());
|
||||
store.apply(
|
||||
msg('gameOver', {
|
||||
iWon: false,
|
||||
reason: GameEndReason.TIMEOUT,
|
||||
myScore: 12,
|
||||
chainLength: 3,
|
||||
chainLength: 9,
|
||||
standings: [
|
||||
{ playerId: 'p3', name: 'Hà', score: 60, connected: true, rank: 1 },
|
||||
{ playerId: 'p1', name: 'Minh', isMe: true, score: 40, eliminated: true, rank: 2 },
|
||||
{ playerId: 'p2', name: 'Lan', score: 55, eliminated: true, rank: 3 }
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.state.standings.map((p) => p.playerId)).toEqual(['p3', 'p1', 'p2']);
|
||||
// Rank is finishing order, not score order: Lan outscored Minh and
|
||||
// still placed below him, because he outlasted her.
|
||||
expect(store.state.standings[2].score).toBeGreaterThan(store.state.standings[1].score);
|
||||
expect(store.state.result?.myScore).toBe(40);
|
||||
});
|
||||
});
|
||||
|
||||
describe('playerEliminated', () => {
|
||||
it('keeps the words this player could have played', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(started());
|
||||
store.apply(
|
||||
msg('playerEliminated', {
|
||||
playerId: 'p1',
|
||||
name: 'Minh',
|
||||
isMe: true,
|
||||
reason: GameEndReason.TIMEOUT,
|
||||
suggestions: ['sinh viên', 'sinh sôi']
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.state.result?.suggestions).toEqual(['sinh viên', 'sinh sôi']);
|
||||
expect(store.state.elimination?.suggestions).toEqual(['sinh viên', 'sinh sôi']);
|
||||
expect(store.iAmOut).toBe(true);
|
||||
expect(store.state.myTurn).toBe(false);
|
||||
});
|
||||
|
||||
it('reads an absent list as a position that had nothing left', () => {
|
||||
// A dead end and an older server that never sends the field arrive the
|
||||
// same way, and both mean "no words to offer" rather than undefined.
|
||||
// A dead end arrives as an empty list, which means "no words to offer"
|
||||
// rather than undefined.
|
||||
const store = createGameStore();
|
||||
store.apply(started());
|
||||
store.apply(msg('gameOver', { iWon: false, reason: GameEndReason.NO_LEGAL_MOVE }));
|
||||
store.apply(
|
||||
msg('playerEliminated', { playerId: 'p1', isMe: true, reason: GameEndReason.NO_LEGAL_MOVE })
|
||||
);
|
||||
|
||||
expect(store.state.result?.suggestions).toEqual([]);
|
||||
expect(store.state.elimination?.suggestions).toEqual([]);
|
||||
});
|
||||
|
||||
it('records somebody else going out without claiming this player did', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(started());
|
||||
store.apply(
|
||||
msg('playerEliminated', {
|
||||
playerId: 'p2',
|
||||
name: 'Lan',
|
||||
isMe: false,
|
||||
reason: GameEndReason.RESIGNED,
|
||||
suggestions: []
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.state.lastOut?.name).toBe('Lan');
|
||||
expect(store.state.elimination).toBeNull();
|
||||
expect(store.iAmOut).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -214,50 +308,70 @@ describe('error', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('opponentLeft', () => {
|
||||
it('exposes the grace window so the UI can say how long to wait', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(msg('opponentLeft', { canReconnect: true, graceMs: 30_000 }));
|
||||
expect(store.state.opponentLeft).toEqual({ canReconnect: true, graceMs: 30_000 });
|
||||
});
|
||||
});
|
||||
/** One seat, with the fields a test does not care about defaulted. */
|
||||
function seat(fields = {}) {
|
||||
return {
|
||||
playerId: 'p1',
|
||||
name: 'Minh',
|
||||
isMe: false,
|
||||
isOwner: false,
|
||||
ready: false,
|
||||
connected: true,
|
||||
...fields
|
||||
};
|
||||
}
|
||||
|
||||
/** One lobby snapshot, with the fields a test does not care about defaulted. */
|
||||
/** One lobby snapshot. `players` is the whole room, the recipient included. */
|
||||
function lobby(fields = {}) {
|
||||
return msg('roomState', {
|
||||
roomCode: 'ABCD',
|
||||
iAmOwner: false,
|
||||
canStart: false,
|
||||
iAmReady: false,
|
||||
opponentPresent: false,
|
||||
opponentName: '',
|
||||
opponentReady: false,
|
||||
opponentConnected: false,
|
||||
maxPlayers: 4,
|
||||
minPlayers: 2,
|
||||
graceMs: 30_000,
|
||||
players: [seat({ isMe: true })],
|
||||
...fields
|
||||
});
|
||||
}
|
||||
|
||||
/** A two-player room, seen by the guest. */
|
||||
function pair(fields = {}) {
|
||||
return lobby({
|
||||
players: [
|
||||
seat({ playerId: 'p1', name: 'Chủ', isOwner: true }),
|
||||
seat({ playerId: 'p2', name: 'Lan', isMe: true })
|
||||
],
|
||||
...fields
|
||||
});
|
||||
}
|
||||
|
||||
describe('room messages', () => {
|
||||
it('keeps the room code and the sanitized opponent name', () => {
|
||||
it('keeps the room code and the sanitized names', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(lobby({ opponentPresent: true, opponentName: 'Lan', opponentConnected: true }));
|
||||
store.apply(pair());
|
||||
|
||||
expect(store.state.roomCode).toBe('ABCD');
|
||||
expect(store.state.opponentName).toBe('Lan');
|
||||
expect(store.state.roomPlayers.map((p) => p.name)).toEqual(['Chủ', 'Lan']);
|
||||
expect(store.me?.playerId).toBe('p2');
|
||||
});
|
||||
|
||||
it('survives a reset, because identity outlives one game', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(msg('welcome', { acceptedNickname: 'Minh', sessionId: 's', resumeToken: 't' }));
|
||||
store.apply(lobby({ opponentPresent: true, opponentName: 'Lan', opponentConnected: true }));
|
||||
store.apply(pair());
|
||||
store.reset();
|
||||
|
||||
expect(store.state.nickname).toBe('Minh');
|
||||
expect(store.state.roomCode).toBe('ABCD');
|
||||
expect(store.state.opponentName).toBe('Lan');
|
||||
expect(store.state.opponentPresent).toBe(true);
|
||||
expect(store.state.roomPlayers).toHaveLength(2);
|
||||
expect(store.state.chain).toEqual([]);
|
||||
});
|
||||
|
||||
it('counts the seats nobody is in yet', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(pair());
|
||||
expect(store.freeSeats).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pong', () => {
|
||||
@@ -379,36 +493,48 @@ describe('chat', () => {
|
||||
describe('the lobby', () => {
|
||||
it('opens the lobby when the room appears', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(lobby({ roomCode: 'K7M2QP', iAmOwner: true }));
|
||||
store.apply(lobby({ roomCode: 'K7M2QP', players: [seat({ isMe: true, isOwner: true })] }));
|
||||
|
||||
expect(store.state.phase).toBe('lobby');
|
||||
expect(store.state.roomCode).toBe('K7M2QP');
|
||||
expect(store.state.isOwner).toBe(true);
|
||||
expect(store.isOwner).toBe(true);
|
||||
});
|
||||
|
||||
it('takes every field from the server rather than deriving any', () => {
|
||||
it("reads this player’s role and readiness off their own row", () => {
|
||||
// There is one encoding of each, so a client cannot end up believing a
|
||||
// role the list it is rendering disagrees with.
|
||||
const store = createGameStore();
|
||||
store.apply(
|
||||
lobby({
|
||||
iAmOwner: false,
|
||||
iAmReady: true,
|
||||
pair({
|
||||
canStart: true,
|
||||
opponentPresent: true,
|
||||
opponentName: 'Lan',
|
||||
opponentReady: false,
|
||||
opponentConnected: true
|
||||
players: [
|
||||
seat({ playerId: 'p1', name: 'Chủ', isOwner: true }),
|
||||
seat({ playerId: 'p2', name: 'Lan', isMe: true, ready: true })
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.state).toMatchObject({
|
||||
isOwner: false,
|
||||
isReady: true,
|
||||
canStart: true,
|
||||
opponentPresent: true,
|
||||
opponentName: 'Lan',
|
||||
opponentReady: false,
|
||||
opponentConnected: true
|
||||
});
|
||||
expect(store.isOwner).toBe(false);
|
||||
expect(store.isReady).toBe(true);
|
||||
expect(store.state.canStart).toBe(true);
|
||||
expect(store.state.maxPlayers).toBe(4);
|
||||
expect(store.state.minPlayers).toBe(2);
|
||||
});
|
||||
|
||||
it('lists everybody whose reconnect window is running, and nobody else', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(
|
||||
lobby({
|
||||
players: [
|
||||
seat({ playerId: 'p1', name: 'Chủ', isOwner: true }),
|
||||
seat({ playerId: 'p2', name: 'Lan', isMe: true }),
|
||||
seat({ playerId: 'p3', name: 'Hà', connected: false })
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.awayPlayers.map((p) => p.name)).toEqual(['Hà']);
|
||||
expect(store.state.graceMs).toBe(30_000);
|
||||
});
|
||||
|
||||
it('does not drop a live game back into the lobby', () => {
|
||||
@@ -416,7 +542,7 @@ describe('the lobby', () => {
|
||||
// Acting on it would replace the board with the lobby mid-turn.
|
||||
const store = createGameStore();
|
||||
store.apply(started());
|
||||
store.apply(lobby({ opponentPresent: true, opponentConnected: true }));
|
||||
store.apply(pair());
|
||||
|
||||
expect(store.state.phase).toBe('playing');
|
||||
});
|
||||
@@ -425,25 +551,32 @@ describe('the lobby', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(started());
|
||||
store.apply(msg('gameOver', { iWon: false, reason: GameEndReason.RESIGNED }));
|
||||
store.apply(lobby({ opponentPresent: true, opponentConnected: true }));
|
||||
store.apply(pair());
|
||||
|
||||
expect(store.state.phase).toBe('over');
|
||||
expect(store.state.result).not.toBeNull();
|
||||
});
|
||||
|
||||
it('stops waiting for an opponent who is connected again', () => {
|
||||
it('stops waiting for a player who is connected again', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(started());
|
||||
store.apply(msg('opponentLeft', { canReconnect: true, graceMs: 30_000 }));
|
||||
store.apply(lobby({ opponentPresent: true, opponentConnected: true }));
|
||||
store.apply(
|
||||
pair({
|
||||
players: [
|
||||
seat({ playerId: 'p1', name: 'Chủ', isOwner: true, connected: false }),
|
||||
seat({ playerId: 'p2', name: 'Lan', isMe: true })
|
||||
]
|
||||
})
|
||||
);
|
||||
expect(store.awayPlayers).toHaveLength(1);
|
||||
|
||||
expect(store.state.opponentLeft).toBeNull();
|
||||
expect(store.state.opponentConnected).toBe(true);
|
||||
store.apply(pair());
|
||||
expect(store.awayPlayers).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('forgets the room when this player is kicked out of it', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(lobby({ opponentPresent: true, opponentConnected: true }));
|
||||
store.apply(pair());
|
||||
store.apply(msg('error', { code: 'kicked', message: '' }));
|
||||
|
||||
expect(store.state.phase).toBe('idle');
|
||||
@@ -453,7 +586,7 @@ describe('the lobby', () => {
|
||||
|
||||
it('forgets a room that closed for sitting idle', () => {
|
||||
const store = createGameStore();
|
||||
store.apply(lobby({ iAmOwner: true }));
|
||||
store.apply(lobby());
|
||||
store.apply(msg('error', { code: 'room_idle_closed', message: '' }));
|
||||
|
||||
expect(store.state.phase).toBe('idle');
|
||||
|
||||
@@ -11,6 +11,7 @@ import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { PROTOCOL_VERSION } from '../src/lib/ws/messages.js';
|
||||
import { fromBinary } from '@bufbuild/protobuf';
|
||||
import {
|
||||
ClientMessageSchema,
|
||||
@@ -48,7 +49,7 @@ describe('generated wire types', () => {
|
||||
const msg = decode('server_welcome');
|
||||
expect(msg.payload.case).toBe('welcome');
|
||||
expect(msg.payload.value.acceptedNickname).toBe('Người chơi ẩn danh');
|
||||
expect(msg.payload.value.protocolVersion).toBe(1);
|
||||
expect(msg.payload.value.protocolVersion).toBe(PROTOCOL_VERSION);
|
||||
});
|
||||
|
||||
// int64 is a bigint in this runtime. Reading it as a Number would silently
|
||||
@@ -71,9 +72,13 @@ describe('generated wire types', () => {
|
||||
expect(over.payload.case).toBe('gameOver');
|
||||
expect(over.payload.value.reason).toBe(GameEndReason.NO_LEGAL_MOVE);
|
||||
expect(over.payload.value.iWon).toBe(false);
|
||||
// The only repeated field in the contract, and the one the losing
|
||||
// The final table, in the order the server ranked it.
|
||||
expect(over.payload.value.standings.map((/** @type {any} */ p) => p.rank)).toEqual([1, 2, 3]);
|
||||
|
||||
const out = decode('server_player_eliminated');
|
||||
// The only repeated string in the contract, and the one the losing
|
||||
// player's screen is built from.
|
||||
expect(over.payload.value.suggestions).toEqual(['sinh viên', 'sinh sôi']);
|
||||
expect(out.payload.value.suggestions).toEqual(['sinh viên', 'sinh sôi']);
|
||||
|
||||
const chat = decode('server_chat_message');
|
||||
expect(chat.payload.case).toBe('chatMessage');
|
||||
|
||||
@@ -9,15 +9,27 @@ import { chainToText, historyFilename } from '../src/lib/history-export.js';
|
||||
* @param {Partial<import('../src/lib/stores/game.svelte.js').ChainEntry>} entry
|
||||
*/
|
||||
function link(entry) {
|
||||
return { word: '', typed: '', byMe: false, points: 0, syllables: 2, opening: false, ...entry };
|
||||
return {
|
||||
word: '',
|
||||
typed: '',
|
||||
byMe: false,
|
||||
playerId: '',
|
||||
points: 0,
|
||||
syllables: 2,
|
||||
opening: false,
|
||||
...entry
|
||||
};
|
||||
}
|
||||
|
||||
const chain = [
|
||||
link({ word: 'học sinh', opening: true, syllables: 0 }),
|
||||
link({ word: 'sinh viên', byMe: true, points: 2 }),
|
||||
link({ word: 'viên chức', points: 2 })
|
||||
link({ word: 'sinh viên', byMe: true, playerId: 'p1', points: 2 }),
|
||||
link({ word: 'viên chức', playerId: 'p2', points: 2 })
|
||||
];
|
||||
|
||||
/** Resolves a seat to the name that seat was playing under. */
|
||||
const nameOf = (/** @type {string} */ id) => (id === 'p2' ? 'Minh' : '');
|
||||
|
||||
const at = new Date(2026, 8, 7, 15, 25);
|
||||
|
||||
describe('chainToText', () => {
|
||||
@@ -39,12 +51,21 @@ describe('chainToText', () => {
|
||||
});
|
||||
|
||||
it('names who played each word and what it scored', () => {
|
||||
const text = chainToText({ chain, at, opponentLabel: 'Minh' });
|
||||
const text = chainToText({ chain, at, nameOf });
|
||||
|
||||
expect(text).toContain('2. sinh viên — Bạn +2');
|
||||
expect(text).toContain('3. viên chức — Minh +2');
|
||||
});
|
||||
|
||||
it('falls back to a generic label for a seat whose name is gone', () => {
|
||||
// A player who left takes their name with them, and the transcript is
|
||||
// written long after. The line still has to say it was not this
|
||||
// player's word.
|
||||
const text = chainToText({ chain, at });
|
||||
|
||||
expect(text).toContain('3. viên chức — Đối thủ +2');
|
||||
});
|
||||
|
||||
it('heads the file with the result when there is one', () => {
|
||||
const text = chainToText({
|
||||
chain,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { create, toBinary } from '@bufbuild/protobuf';
|
||||
import { PROTOCOL_VERSION } from '../src/lib/ws/messages.js';
|
||||
import {
|
||||
ClientMessageSchema,
|
||||
ServerMessageSchema
|
||||
@@ -171,7 +172,7 @@ describe('handshake', () => {
|
||||
|
||||
const [first] = await sentMessages(h.last());
|
||||
expect(first.payload.case).toBe('hello');
|
||||
expect(first.payload.value.protocolVersion).toBe(1);
|
||||
expect(first.payload.value.protocolVersion).toBe(PROTOCOL_VERSION);
|
||||
expect(first.payload.value.nickname).toBe('Minh');
|
||||
expect(first.payload.value.resumeToken).toBe('');
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user