Files
scrabble-game/ui/e2e/update.spec.ts
T
Ilia Denisov 872804e02e
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m13s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 20s
feat(offline): implicit net-state model, two-tier version gate, unified lobby
Land the offline-model redesign (ANDROID_PLAN.md O1-O7): replace the explicit offline toggle with a single detected net-state machine, unify the lobby, and add a two-tier client-version gate. Contour-safe: both version vars empty => dormant, the wire change is additive, so web/pwa/vk/tg behaviour is unchanged unless the gate is deliberately configured.

O1 net-state reducer (test-first). O2 store + wiring (connection/offline shims; +@capacitor/network). O3 remove the offline toggle + migrate the pref. O4 two-tier gate: hard update_required degrades to an offline Update/Play-offline notice (not terminal); soft GATEWAY_RECOMMENDED_CLIENT_VERSION -> X-Update-Recommended nudge. O5 unified lobby (device-local + greyed-from-cache server games; self-set identity; closes G-step-0). O6 create flows (with-friends online/offline segment + offline dict guard). O7 docs.

Wire the version gate through the deploy: GATEWAY_MIN_CLIENT_VERSION + GATEWAY_RECOMMENDED_CLIENT_VERSION are plain (unprefixed) Gitea vars, passed via compose + ci.yaml (test) + prod-deploy.yaml + write-prod-env.sh (prod) + .env.example, so the gate is live when set (a test-contour commit-hash version fails open; empty => dormant). Disable the manual android-build CI workflow for now (rename .disabled). Bump the app bundle budget 127->130 KB for the added always-loaded wiring.

Tests: gateway go green (incl. two-tier gate over a real HTTP handler), compose --no-interpolate confirms the gateway env, svelte-check 0/0, vitest 617, e2e 122/122 (chromium + webkit).
2026-07-13 01:58:20 +02:00

78 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> {
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);
});