28afcff551
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Failing after 12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Failing after 1s
CI / deploy (pull_request) Has been skipped
When an online game loses the connection the game screen now says so and freezes instead of silently greying out: a "connection lost" banner appears and the rack, the move controls, the add-friend/block controls and the chat/dictionary entry all disable (a started move stays a draft, committed by the player on reconnect). It is driven off the net-state machine (netState.offline), so it also covers the Telegram/VK mini-apps, where a lost connection was previously mute in-game. If the player is already in the dictionary when the drop happens, the word check falls back to the game's pinned on-device dictionary (exact when that dawg is cached; "unavailable offline" otherwise) and the network-only complaint + external look-up hide. Chat, when already open, keeps its existing read-only degrade (send/nudge disable). New pure helper localWordCheck is unit-tested; the in-game gating gets an e2e.
77 lines
4.5 KiB
TypeScript
77 lines
4.5 KiB
TypeScript
import { expect, test } from './fixtures';
|
|
|
|
// The in-game composition UX after the under-board status strip was removed:
|
|
// - whose turn (or the final result) shows in a thin strip above the score plaques;
|
|
// - the tiles left in the bag ride the exchange control as a badge and repeat at the foot of the
|
|
// move table;
|
|
// - staging a play tints its tiles on the board and shows the orange move-score badge, and the
|
|
// confirm control sits in the rack's fixed 7th slot.
|
|
// Mock transport only: the mock has no dictionary (Game.svelte skips the local evaluator under the
|
|
// mock mode) and its network evaluator accepts any placement, so a staged tile always reads as legal.
|
|
|
|
test('in-game UX: turn strip, bag badge + table footer, staged-play highlight and score badge', async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.getByRole('button', { name: /guest/i }).click();
|
|
await page.getByRole('button', { name: /🎲/ }).click();
|
|
await page.getByRole('button', { name: 'Random player' }).click();
|
|
await page.locator('.variant').first().click();
|
|
await page.getByRole('button', { name: /Start game/i }).click();
|
|
// Attach the opponent deterministically, then it is the player's turn.
|
|
await page.evaluate(() => (window as unknown as { __mock: { joinOpponent(): void } }).__mock.joinOpponent());
|
|
await expect(page.getByText('Robo')).toBeVisible();
|
|
|
|
// The old under-board status line is gone; a thin turn strip sits above the plaques instead.
|
|
await expect(page.locator('.turnstrip')).toBeVisible();
|
|
await expect(page.locator('.status')).toHaveCount(0);
|
|
|
|
// The bag count rides the exchange control (the first tab) as a badge, and repeats at the foot of
|
|
// the move table, out of the scrolling grid.
|
|
await expect(page.locator('.tab').first().locator('.badge')).toBeVisible();
|
|
await page.locator('.scoreboard').click(); // open the history drawer
|
|
await expect(page.locator('.hbagfoot')).toContainText(/bag/i);
|
|
await page.locator('.scoreboard').click(); // close it again
|
|
|
|
// Stage a play: the pending tile reads as legal (green), the orange move-score badge shows on the
|
|
// board, and the confirm control takes the rack's 7th slot.
|
|
await page.locator('.rack .tile').first().click();
|
|
await page.locator('[data-cell]:not(.filled)').nth(112).click(); // centre (row 7, col 7)
|
|
await expect(page.locator('[data-cell].pending.legal')).toHaveCount(1);
|
|
await expect(page.locator('.scorebadge')).toBeVisible();
|
|
await expect(page.locator('.make')).toBeVisible();
|
|
});
|
|
|
|
test('online game offline: banner shows, the tray and comms entry freeze, and it thaws on recovery', async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.getByRole('button', { name: /guest/i }).click();
|
|
await page.getByRole('button', { name: /🎲/ }).click();
|
|
await page.getByRole('button', { name: 'Random player' }).click();
|
|
await page.locator('.variant').first().click();
|
|
await page.getByRole('button', { name: /Start game/i }).click();
|
|
await page.evaluate(() => (window as unknown as { __mock: { joinOpponent(): void } }).__mock.joinOpponent());
|
|
await expect(page.getByText('Robo')).toBeVisible();
|
|
|
|
// Baseline online: no banner, the tray is interactive, the comms (chat/dictionary) entry is enabled.
|
|
await expect(page.locator('.offline-banner')).toHaveCount(0);
|
|
await expect(page.locator('.rack .tile').first()).toBeEnabled();
|
|
|
|
// The connection drops (the net hook rides past the hysteresis straight to offline).
|
|
await page.evaluate(() => (window as unknown as { __net: { offline(): void } }).__net.offline());
|
|
|
|
// The explicit "connection lost" banner appears and every rack tile freezes (cannot compose a move).
|
|
await expect(page.locator('.offline-banner')).toBeVisible({ timeout: 15000 });
|
|
const tiles = page.locator('.rack .tile');
|
|
const n = await tiles.count();
|
|
expect(n).toBeGreaterThan(0);
|
|
for (let i = 0; i < n; i++) await expect(tiles.nth(i)).toBeDisabled();
|
|
|
|
// The comms entry (💬 — chat + dictionary) is disabled; it lives in the history drawer, so open it.
|
|
await page.locator('.scoreboard').click();
|
|
await expect(page.locator('button:has(.chat-ico)')).toBeDisabled();
|
|
|
|
// Recovery: the network returns, the banner clears and the tray is interactive again.
|
|
await page.evaluate(() => (window as unknown as { __net: { online(): void } }).__net.online());
|
|
await expect(page.locator('.offline-banner')).toHaveCount(0, { timeout: 15000 });
|
|
await expect(page.locator('.rack .tile').first()).toBeEnabled();
|
|
await expect(page.locator('button:has(.chat-ico)')).toBeEnabled();
|
|
});
|