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 (๐Ÿ’ฌ) and the resign control are HIDDEN offline (like the frozen social controls); // both live in the history drawer, so open it and assert no action icons remain in its header. await page.locator('.scoreboard').click(); await expect(page.locator('.hhead button.hicon')).toHaveCount(0); // Recovery: the network returns, the banner clears, the tray is interactive and the comms entry returns. 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)')).toBeVisible(); }); test('online game offline: the dictionary still accepts input and checks (variant seeded from cache)', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: /guest/i }).click(); // A Russian (erudit_ru โ€” first in the mock's variant preferences) game, so the offline dictionary is // exercised with a non-Latin alphabet that differs from the panel's default 'scrabble_en'. The panel // seeds its variant + pinned version from the cached game; the mock resolves gameState even offline // (no kill switch), so on the real backend โ€” where that fetch fails offline โ€” the seed is what keeps // the input and the dawg fallback working. This guards the offline dictionary flow end-to-end. 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(); // Go offline, then open the dictionary directly (its in-game entry is hidden offline) so CheckScreen // mounts with no network and must seed the variant/version from the cached game, not a failed fetch. await page.evaluate(() => (window as unknown as { __net: { offline(): void } }).__net.offline()); await expect(page.locator('.offline-banner')).toBeVisible({ timeout: 15000 }); await page.evaluate(() => { const route = location.hash.slice(1); // /game/ (window as unknown as { __router: { navigate(p: string): void } }).__router.navigate(route + '/check'); }); // The check input keeps the Cyrillic word (the variant is known) and the Check button is usable โ€” // the outcome the cache seed guarantees on the real backend where the offline gameState fetch fails. const input = page.locator('.check input'); await expect(input).toBeVisible(); await input.fill('ะกะ›ะžะ’ะž'); await expect(input).toHaveValue('ะกะ›ะžะ’ะž'); await expect(page.locator('.check button')).toBeEnabled(); // Running the check resolves to a verdict (or the neutral offline note), proving the fallback path // executes rather than hanging. await page.locator('.check button').click(); await expect(page.locator('.verdict')).toBeVisible(); });