import { describe, expect, it } from 'vitest'; import { resultBadge, seatMedal } from './result'; import type { GameView, Seat } from './model'; const seat = (s: number, accountId: string, score: number, isWinner = false, resigned = false): Seat => ({ seat: s, accountId, displayName: accountId, score, hintsUsed: 0, isWinner, resigned, }); function game(seats: Seat[], status = 'finished', toMove = 0): GameView { return { id: 'g', variant: 'scrabble_en', dictVersion: 'v1', vsAi: false, unreadChat: false, unreadMessages: false, status, players: seats.length, toMove, turnTimeoutSecs: 0, multipleWordsPerTurn: true, 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('open (awaiting an opponent) reads as in-progress, not a finished result', () => { const g = game([seat(0, 'me', 0), seat(1, '', 0)], 'open', 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 (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: '🥈' }); }); it('tie for the lead (3-4p): a shared victory for the leaders, a placed finish for those below', () => { // The reported case, viewer-side: two seats tie for the lead (-8), one is last (-14) — no single // winner(), so this must NOT read as a full draw for everyone. expect(resultBadge(game([seat(0, 'me', -8), seat(1, 'a', -14), seat(2, 'b', -8)]), 'me')).toEqual({ key: 'result.victory', emoji: '🏆' }); expect(resultBadge(game([seat(0, 'x', -8), seat(1, 'me', -14), seat(2, 'b', -8)]), 'me')).toEqual({ key: 'result.place3', emoji: '🥉' }); }); it('an aborted game reads as a draw for everyone, whatever the scores', () => { const g = { ...game([seat(0, 'me', 50), seat(1, 'a', 30)]), endReason: 'aborted' }; expect(resultBadge(g, 'me')).toEqual({ key: 'result.draw', emoji: '🏅' }); }); }); describe('seatMedal', () => { it('is empty for a game still in progress', () => { expect(seatMedal(game([seat(0, 'a', 5), seat(1, 'b', 3)], 'active'), 0)).toBe(''); }); it('finished: places each seat by final score', () => { const g = game([seat(0, 'a', 100), seat(1, 'b', 400, true), seat(2, 'c', 300), seat(3, 'd', 200)]); expect(seatMedal(g, 1)).toBe('🏆'); // 400 → 1st expect(seatMedal(g, 2)).toBe('🥈'); // 300 → 2nd expect(seatMedal(g, 3)).toBe('🥉'); // 200 → 3rd expect(seatMedal(g, 0)).toBe('🏅'); // 100 → 4th }); it('a tie for the lead shares first place; the rest place below (competition ranking)', () => { // The owner's case: a scoreless end deducted racks to -8 / -14 / -8, tying seats 0 and 2 for the // lead — they must SHARE the trophy, not all show the last-place medal (the reported bug). const g = game([seat(0, 'a', -8), seat(1, 'b', -14), seat(2, 'c', -8)]); expect(seatMedal(g, 0)).toBe('🏆'); expect(seatMedal(g, 2)).toBe('🏆'); expect(seatMedal(g, 1)).toBe('🥉'); // two seats rank above it → 3rd }); it('an excluded / resigned seat gets no medal and does not push the others down', () => { const g = game([seat(0, 'a', 50), seat(1, 'b', 900, false, true), seat(2, 'c', 30)]); expect(seatMedal(g, 1)).toBe(''); // resigned: no medal, though its frozen score is the highest expect(seatMedal(g, 0)).toBe('🏆'); // ranked among the rest: highest → 1st expect(seatMedal(g, 2)).toBe('🥈'); }); });