14918cc94f
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s
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. Deploy/CI: wire the version gate through the deploy (GATEWAY_MIN_CLIENT_VERSION + GATEWAY_RECOMMENDED_CLIENT_VERSION as plain unprefixed vars via compose + ci.yaml + prod-deploy.yaml + write-prod-env.sh + .env.example) so the gate is live when set (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. Fix the UI Docker stage (gateway/Dockerfile): install --ignore-scripts so the Alpine/musl SPA build no longer tries to native-build sharp (a local android:assets tool, unused in the image); esbuild's binary is an optional dep so Vite still builds. Tests: gateway go green (two-tier gate over a real HTTP handler); docker build of the landing + gateway targets green locally; compose --no-interpolate confirms the gateway env; svelte-check 0/0, vitest 617, e2e 122/122 (chromium + webkit).
78 lines
4.0 KiB
TypeScript
78 lines
4.0 KiB
TypeScript
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);
|
||
});
|