6b6baf5710
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 31s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Lobby: group the my-games list into your-turn / opponent-turn / finished (empty sections hidden), ordered by last activity (your-turn oldest-first, the other two newest-first), as a compact line-separated list. gameDTO and FB GameView gain last_activity_unix (turn start while active, finish time once finished); a pure lib/lobbysort.ts holds the grouping/ordering. Friends: the in-game 'add to friends' item is now server-derived via a new GET /user/friends/outgoing (+ friends.outgoing op), returning addressees with a pending OR declined request (both read as 'request sent'), so it is correct across reloads; it shows a disabled '✓ in friends' once accepted. It live-updates when the opponent answers: RespondFriendRequest now publishes friend_added (accept) / friend_declined (new notify sub-kind, decline) to the original requester, whose open game re-derives its friend state. Tests: lobbysort unit test; gateway outgoing + last_activity transcode tests; backend integration ListOutgoingRequests + respond-publishes-to-requester; e2e updated for the new lobby section labels + a non-friend active opponent. Docs: ARCHITECTURE notify catalog, FUNCTIONAL(+ru) lobby/friends, PLAN.
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { resultBadge } from './result';
|
|
import type { GameView, Seat } from './model';
|
|
|
|
const seat = (s: number, accountId: string, score: number, isWinner = false): Seat => ({
|
|
seat: s,
|
|
accountId,
|
|
displayName: accountId,
|
|
score,
|
|
hintsUsed: 0,
|
|
isWinner,
|
|
});
|
|
|
|
function game(seats: Seat[], status = 'finished', toMove = 0): GameView {
|
|
return {
|
|
id: 'g',
|
|
variant: 'english',
|
|
dictVersion: 'v1',
|
|
status,
|
|
players: seats.length,
|
|
toMove,
|
|
turnTimeoutSecs: 0,
|
|
moveCount: 0,
|
|
endReason: '',
|
|
lastActivityUnix: 0,
|
|
seats,
|
|
};
|
|
}
|
|
|
|
describe('resultBadge', () => {
|
|
it('active: your move vs opponent', () => {
|
|
const g = game([seat(0, 'me', 5), seat(1, 'a', 3)], 'active', 0);
|
|
expect(resultBadge(g, 'me')).toEqual({ key: 'result.yourMove', emoji: '🟢' });
|
|
expect(resultBadge({ ...g, toMove: 1 }, 'me').key).toBe('result.oppMove');
|
|
});
|
|
|
|
it('finished two-player: victory / defeat / draw', () => {
|
|
expect(resultBadge(game([seat(0, 'me', 300, true), seat(1, 'a', 200)]), 'me')).toEqual({
|
|
key: 'result.victory',
|
|
emoji: '🏆',
|
|
});
|
|
expect(resultBadge(game([seat(0, 'me', 200), seat(1, 'a', 300, true)]), 'me')).toEqual({
|
|
key: 'result.defeat',
|
|
emoji: '🥈',
|
|
});
|
|
expect(resultBadge(game([seat(0, 'me', 200), seat(1, 'a', 200)]), 'me')).toEqual({
|
|
key: 'result.draw',
|
|
emoji: '🏅',
|
|
});
|
|
});
|
|
|
|
it('finished two-player: a 0-0 resignation is a defeat, not a score-tied win', () => {
|
|
// The opponent won by resignation (isWinner) although neither side scored — the lobby
|
|
// must read this as a loss, matching the game-detail screen (Stage 17 regression).
|
|
expect(resultBadge(game([seat(0, 'me', 0), seat(1, 'a', 0, true)]), 'me')).toEqual({
|
|
key: 'result.defeat',
|
|
emoji: '🥈',
|
|
});
|
|
});
|
|
|
|
it('finished four-player: places by score', () => {
|
|
const last = game([seat(0, 'me', 100), seat(1, 'a', 400, true), seat(2, 'b', 300), seat(3, 'c', 200)]);
|
|
expect(resultBadge(last, 'me')).toEqual({ key: 'result.place4', emoji: '🏅' });
|
|
const second = game([seat(0, 'me', 300), seat(1, 'a', 400, true), seat(2, 'b', 200), seat(3, 'c', 100)]);
|
|
expect(resultBadge(second, 'me')).toEqual({ key: 'result.place2', emoji: '🥈' });
|
|
});
|
|
});
|