feat(ui): per-kind active-game limit lock on the New Game screen
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 28s
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m46s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 28s
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m46s
Carry the caller's per-tier active-game caps and each game's kind on the wire (Profile.game_limits + GameView.kind, additive FBS + gateway transcode + client codec, committed regen). The New Game screen counts the player's active games per kind from the lobby and locks a capped start: an outline button with a lock that opens a funnel modal instead of a game -- a sign-in prompt for a guest, a "finish a current game first" notice for a signed-in account (native Telegram popup, in-app modal elsewhere). The lock lifts via the existing profile refetch after a guest->durable upgrade. Remove the lobby's old at_game_limit New-Game tab disable + notice: the flag (now the random-kind cap) conflicted with the per-kind lock -- it hid the screen where the lock lives and wrongly blocked an unfulfilled kind. The New Game tab is always enabled; the per-kind start lock is the only gate. The at_game_limit wire field stays (unused by the client) for a later cleanup. Tests: client lock logic + codec kind/game_limits roundtrip + gateway transcode encode + native popup builders (unit); a mock e2e for the lock badge and the modal.
This commit is contained in:
+31
-22
@@ -1,31 +1,40 @@
|
||||
import { expect, test } from './fixtures';
|
||||
|
||||
// The simultaneous quick-game cap. When the backend reports the player is at the limit
|
||||
// (games.list at_game_limit), the lobby disables the "New Game" tab and shows a plain,
|
||||
// low-emphasis notice under the lists; when it clears, the button re-enables and the
|
||||
// notice goes. Driven by the mock transport's __mock.setGameLimit seam (no backend), which
|
||||
// also nudges the lobby to re-fetch (the lobby refreshes on any live event).
|
||||
// The active-game limit lock on the New Game screen. When a start's kind is at the caller's per-kind
|
||||
// cap (Profile.game_limits), the start button shows a 🔒 and opens a funnel modal instead of a game;
|
||||
// when the cap lifts (the profile refetch a guest→durable upgrade would trigger), the lock clears and
|
||||
// the button starts a game again. Driven by the mock's __mock.setGameLimits seam (no backend). The
|
||||
// native Telegram popup path is verified on the contour; here the cross-platform in-app modal shows
|
||||
// (the mock profile is a durable account, so it is the "finish a current game first" notice).
|
||||
|
||||
test('lobby: New Game disables and a notice shows at the simultaneous-game limit', async ({ page }) => {
|
||||
type Limits = { vsAi: number; random: number; friends: number };
|
||||
function setLimits(l: Limits) {
|
||||
(window as unknown as { __mock: { setGameLimits(l: Limits): void } }).__mock.setGameLimits(l);
|
||||
}
|
||||
|
||||
test('new game: a start locks at its per-kind cap and the funnel modal opens', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: /guest/i }).click();
|
||||
|
||||
// Below the limit: the New Game tab is enabled and no notice is shown.
|
||||
const newGame = page.getByRole('button', { name: /🎲/ });
|
||||
await expect(newGame).toBeEnabled();
|
||||
await expect(page.getByText(/reached the simultaneous games limit/i)).toHaveCount(0);
|
||||
// The New Game button always opens the screen now — the per-kind lock lives on the start button.
|
||||
await page.getByRole('button', { name: /🎲/ }).click();
|
||||
|
||||
// Reach the limit: the button greys out (disabled) and the notice appears under the lists.
|
||||
await page.evaluate(() =>
|
||||
(window as unknown as { __mock: { setGameLimit(v: boolean): void } }).__mock.setGameLimit(true),
|
||||
);
|
||||
await expect(newGame).toBeDisabled();
|
||||
await expect(page.getByText(/reached the simultaneous games limit/i)).toBeVisible();
|
||||
const start = page.getByRole('button', { name: /start game/i });
|
||||
await expect(start).not.toContainText('🔒');
|
||||
|
||||
// Drop back below the limit (a game finished): the button re-enables and the notice clears.
|
||||
await page.evaluate(() =>
|
||||
(window as unknown as { __mock: { setGameLimit(v: boolean): void } }).__mock.setGameLimit(false),
|
||||
);
|
||||
await expect(newGame).toBeEnabled();
|
||||
await expect(page.getByText(/reached the simultaneous games limit/i)).toHaveCount(0);
|
||||
// Cap every kind: the AI start (the default opponent) locks.
|
||||
await page.evaluate(setLimits, { vsAi: 0, random: 0, friends: 0 });
|
||||
await expect(start).toContainText('🔒');
|
||||
|
||||
// Tapping the locked start opens the modal instead of a game, and does not navigate away.
|
||||
await start.click();
|
||||
const notice = page.getByText(/reached the limit of simultaneous games/i);
|
||||
await expect(notice).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/new$/);
|
||||
await page.getByRole('button', { name: /^ok$/i }).click();
|
||||
await expect(notice).toHaveCount(0);
|
||||
|
||||
// Lift the cap (the same profile-refetch path a registration takes): the lock clears.
|
||||
await page.evaluate(setLimits, { vsAi: 10, random: 10, friends: 10 });
|
||||
await expect(start).not.toContainText('🔒');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user