Files
scrabble-game/ui/src/lib/gamelimits.test.ts
T
Ilia Denisov e40adfb0c7
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m56s
fix(games): carry game kind on live events + fix the New Game lock funnel
Address review of the active-game limit lock:

- Live-event GameViews (opponent_moved, match_found, game_started, ...) did
  not carry game_kind, so a lobby patch from an event zeroed a game's kind
  and the client's per-kind count under-counted — a capped kind read as
  free (a disabled start instead of the lock, and a wrong per-kind result).
  Thread Kind through notify.GameSummary → the event GameView.

- New Game screen refreshes the lobby games on mount so the per-kind count
  reflects the current set, not a stale cached snapshot.

- The guest funnel's login button routes to the profile screen (the account
  controls), not settings.

- Copy: the guest prompt is "sign in to use all the game's features"; the
  durable notice is "finish your active games to start a new one".

Regression tests: the notify opponent-moved payload carries kind; the client
lock locks only the reached kind (vs_ai at cap leaves random open).
2026-07-10 10:55:48 +02:00

86 lines
3.3 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { activeByKind, capFor, isKindLocked } from './gamelimits';
import { GameKind, type GameView, type GameLimits } from './model';
// game builds a minimal GameView for the count: only kind, status and (optionally) hotseat matter.
function game(kind: number, status: string, extra: Partial<GameView> = {}): GameView {
return {
id: 'g',
variant: 'scrabble_en',
dictVersion: '',
status,
players: 2,
toMove: 0,
turnTimeoutSecs: 0,
multipleWordsPerTurn: true,
moveCount: 0,
endReason: '',
lastActivityUnix: 0,
vsAi: false,
unreadChat: false,
unreadMessages: false,
kind,
seats: [],
...extra,
};
}
describe('activeByKind', () => {
it('counts open+active games per kind, skipping finished, hotseat and untagged (kind 0)', () => {
const by = activeByKind([
game(GameKind.vsAi, 'active'),
game(GameKind.random, 'open'),
game(GameKind.random, 'active'),
game(GameKind.random, 'finished'), // finished → not counted
game(GameKind.vsAi, 'active', { hotseat: true }), // local pass-and-play → not counted
game(0, 'active'), // pre-existing / untagged → not counted
]);
expect(by[GameKind.vsAi]).toBe(1);
expect(by[GameKind.random]).toBe(2);
});
});
describe('capFor', () => {
const limits: GameLimits = { vsAi: 1, random: 10, friends: 0 };
it('maps each kind to its tier cap; unknown kind and absent limits are unlimited (-1)', () => {
expect(capFor(limits, GameKind.vsAi)).toBe(1);
expect(capFor(limits, GameKind.random)).toBe(10);
expect(capFor(limits, GameKind.friends)).toBe(0);
expect(capFor(limits, 0)).toBe(-1);
expect(capFor(undefined, GameKind.vsAi)).toBe(-1);
});
});
describe('isKindLocked', () => {
const guest: GameLimits = { vsAi: 1, random: 1, friends: 0 };
const durable: GameLimits = { vsAi: 10, random: 10, friends: 10 };
it('locks a guest at the per-kind cap, not another kind', () => {
const games = [game(GameKind.vsAi, 'active')];
expect(isKindLocked(games, guest, GameKind.vsAi)).toBe(true);
expect(isKindLocked(games, guest, GameKind.random)).toBe(false);
});
it('a cap of 0 always locks (a guest cannot start friend games)', () => {
expect(isKindLocked([], guest, GameKind.friends)).toBe(true);
});
it('an unlimited cap (-1) or absent limits never lock', () => {
const many = [game(GameKind.random, 'active'), game(GameKind.random, 'active')];
expect(isKindLocked(many, { vsAi: -1, random: -1, friends: -1 }, GameKind.random)).toBe(false);
expect(isKindLocked(many, undefined, GameKind.random)).toBe(false);
});
it('a durable account stays well under its higher cap', () => {
expect(isKindLocked([game(GameKind.random, 'active')], durable, GameKind.random)).toBe(false);
});
it('locks only the reached kind: at the vs_ai cap, random with no games stays open', () => {
const limits = { vsAi: 3, random: 2, friends: 1 };
const games = [game(GameKind.vsAi, 'active'), game(GameKind.vsAi, 'active'), game(GameKind.vsAi, 'active')];
expect(isKindLocked(games, limits, GameKind.vsAi)).toBe(true);
expect(isKindLocked(games, limits, GameKind.random)).toBe(false);
expect(isKindLocked(games, limits, GameKind.friends)).toBe(false);
});
});