import { expect, test, type Page } from './fixtures'; // The two-tier client-version gate (docs/ARCHITECTURE.md §2). The mock transport never produces a real // version signal, so — like the maintenance overlay — the e2e drives both tiers through the // window.__update hook (gateway.ts, mock-only): __update.on() raises the HARD "update required" notice // (the net-state machine's offlineVersionLocked), __update.recommend() raises the SOFT "update // available" nudge. async function enterLobby(page: Page): Promise { await page.getByRole('button', { name: /guest|гост/i }).first().click(); await expect(page.locator('button.tab').nth(2)).toBeVisible(); } test('the update-required notice covers the app and offers Update or Play offline', async ({ page }) => { await page.goto('/'); await expect(page.getByRole('alertdialog')).toHaveCount(0); await page.evaluate(() => (window as unknown as { __update: { on(): void } }).__update.on()); const notice = page.getByRole('alertdialog'); await expect(notice).toBeVisible(); await expect(notice.getByRole('button', { name: /Update|Обновить/i })).toBeVisible(); await expect(notice.getByRole('button', { name: /Play offline|Играть офлайн/i })).toBeVisible(); }); test('the Update action reloads the web client', async ({ page }) => { await page.goto('/'); await page.evaluate(() => (window as unknown as { __update: { on(): void } }).__update.on()); const notice = page.getByRole('alertdialog'); await expect(notice).toBeVisible(); // On the web the action reloads the page to fetch the current client (a native build opens the store // instead). The reload re-bootstraps to INITIAL online, so the notice is gone and the login is back. await Promise.all([ page.waitForEvent('load'), notice.getByRole('button', { name: /Update|Обновить/i }).click(), ]); await expect(page.getByRole('alertdialog')).toHaveCount(0); await expect(page.getByRole('button', { name: /guest/i })).toBeVisible(); }); test('Play offline dismisses the notice into the offline lobby', async ({ page }) => { await page.goto('/'); await enterLobby(page); // Online first: the seeded online game (vs Ann) is listed. await expect(page.getByText('Ann', { exact: false }).first()).toBeVisible(); // A too-old foreground call locks the version: the notice covers the (now offline) app. await page.evaluate(() => (window as unknown as { __update: { on(): void } }).__update.on()); const notice = page.getByRole('alertdialog'); await expect(notice).toBeVisible(); // "Play offline" dismisses the notice and leaves the app in the offline lobby (the version lock is an // offline state): the header is blue and the online game is gone. await notice.getByRole('button', { name: /Play offline|Играть офлайн/i }).click(); await expect(page.getByRole('alertdialog')).toHaveCount(0); await expect(page.locator('header.nav.offline')).toBeVisible(); // The server game (vs Ann) rides along greyed from the cache (offline, un-openable). await expect(page.locator('.rowwrap.greyed').filter({ hasText: 'Ann' })).toBeVisible(); }); test('the soft update-available nudge shows in the lobby and dismisses', async ({ page }) => { await page.goto('/'); await enterLobby(page); await expect(page.locator('.nudge')).toHaveCount(0); // A served response below the recommended version carries X-Update-Recommended: the non-blocking // nudge appears in the lobby (above the tab bar); play is unaffected — no overlay. await page.evaluate(() => (window as unknown as { __update: { recommend(): void } }).__update.recommend()); const nudge = page.locator('.nudge'); await expect(nudge).toBeVisible(); await expect(nudge).toContainText(/Update available|Доступно обновление/i); await expect(page.getByRole('alertdialog')).toHaveCount(0); // Dismissing it (the ×) hides it for the session. await nudge.getByRole('button', { name: /Close|Закрыть/i }).click(); await expect(page.locator('.nudge')).toHaveCount(0); });