import { test, expect, type Page } from './fixtures'; // Offline pass-and-play (hotseat) is offered only in offline mode, which is gated to an installed // standalone PWA with a confirmed email. The mock account has an email; force standalone so the // Settings offline toggle shows. async function forceStandalone(page: Page): Promise { await page.addInitScript(() => { const orig = window.matchMedia.bind(window); window.matchMedia = (q: string) => (q.includes('display-mode: standalone') ? { matches: true, media: q, onchange: null, addEventListener() {}, removeEventListener() {}, addListener() {}, removeListener() {}, dispatchEvent: () => false } : orig(q)) as MediaQueryList; }); } async function enterLobby(page: Page): Promise { await page.getByRole('button', { name: /guest|гост/i }).first().click(); await expect(page.locator('button.tab').nth(2)).toBeVisible(); } async function goOffline(page: Page): Promise { await page.locator('button.tab').nth(2).click(); // Settings await page.getByRole('button', { name: /^(Offline|Оффлайн)$/ }).click(); await expect(page.locator('header.nav.offline')).toBeVisible({ timeout: 15000 }); await page.getByRole('button', { name: /Back|Назад/i }).click(); await expect(page.locator('button.tab').nth(2)).toBeVisible(); } // typePin clicks the on-screen keypad digits (the pad has no OK button — the 4th digit is the action). async function typePin(page: Page, digits: string): Promise { for (const d of digits) await page.locator('.pad .key', { hasText: d }).click(); } test.describe('offline hotseat (pass-and-play)', () => { test('create with a locked seat, unlock, host-skip, terminate from the lobby', async ({ page }) => { await forceStandalone(page); await page.goto('/'); await enterLobby(page); await goOffline(page); // A known bag seed so the locked seat deals a full rack deterministically. await page.evaluate(() => (window as unknown as { __mock: { setLocalSeed(s: string): void } }).__mock.setLocalSeed('1')); // New game -> the offline mode selector now offers "with friends" (hotseat). await page.locator('button.tab').nth(0).click(); await page.getByRole('button', { name: /With friends|друзьями/i }).click(); // The player rows are disabled until the mandatory host (master) PIN is set. await expect(page.locator('.pname').first()).toBeDisabled(); // Set the host PIN (enter + confirm), then decline taking a seat. await page.locator('.hostpin .plink').click(); await typePin(page, '9999'); await typePin(page, '9999'); await page.getByRole('button', { name: /^(No|Нет)$/ }).click(); // Pick the English variant (the plaques mirror Quick Match; "Scrabble" is the Latin English name). await page.locator('.variant', { hasText: 'Scrabble' }).click(); // Two players: Ann (PIN-locked) and Bob (open). await page.locator('.pname').nth(0).fill('Ann'); await page.locator('.pname').nth(1).fill('Bob'); // Row-delete: a 3rd player's kebab asks the host PIN, which ARMS a ❌ (not a silent delete); // tapping the ❌ removes the row. await page.getByRole('button', { name: /Add player|Добавить/i }).click(); await expect(page.locator('.prow')).toHaveCount(3); await page.locator('.prow').nth(2).locator('.pkebab').click(); await typePin(page, '9999'); const rowDel = page.locator('.prow').nth(2).locator('.prow-del'); await expect(rowDel).toBeVisible(); await rowDel.click(); await expect(page.locator('.prow')).toHaveCount(2); // Lock Ann's seat with a PIN. await page.locator('.prow').nth(0).locator('.plink').click(); await typePin(page, '1234'); await typePin(page, '1234'); await page.locator('button.invite').click(); await expect(page.locator('[data-cell]').first()).toBeVisible(); // Ann is to move and PIN-locked: the board is visible but her rack is withheld behind Unlock. await expect(page.locator('.unlock')).toBeVisible(); await expect(page.locator('.rack .tile')).toHaveCount(0); // A wrong PIN keeps it locked; the right PIN reveals the full rack. await page.locator('.unlock').click(); await typePin(page, '0000'); await expect(page.locator('.pad')).toBeVisible(); // still open after a wrong PIN await typePin(page, '1234'); await expect(page.locator('.rack .tile')).toHaveCount(7); // Host override: 🔐 -> master PIN -> skip the current turn -> confirm. The turn passes to Bob, // whose seat is open, so his rack shows without a lock. await page.locator('button.tab', { hasText: /Host|Ведущий/ }).click(); await typePin(page, '9999'); await page.getByRole('button', { name: /Skip|Пропустить/ }).click(); await page.getByRole('button', { name: /^(OK|ОК)$/ }).click(); await expect(page.locator('.unlock')).toHaveCount(0); await expect(page.locator('.rack .tile')).toHaveCount(7); // Back in the lobby the active hotseat game has a kebab; terminating it needs the master PIN. await page.getByRole('button', { name: /Back|Назад/i }).click(); await expect(page.getByText('Ann', { exact: false }).first()).toBeVisible(); await expect(page.locator('.rowwrap')).toHaveCount(1); await page.locator('.kebab').first().click(); const del = page.locator('.rowwrap.revealed .del'); await expect(del).toBeVisible(); await del.click(); await typePin(page, '9999'); await expect(page.locator('.rowwrap')).toHaveCount(0); }); });