e9f4cb0178
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
A Playwright spec drives the whole offline flow in the mock build: force the installed-PWA display mode, enter offline via the Settings toggle (its readiness check fetches the dawgs), assert the blue chrome + online-games-hidden + Stats-disabled, then create and play a device-local English vs_ai game with a pinned bag seed (deterministic rack NEWYMAO) -- play the opening WAY across the centre, watch the robot reply with a real move, and reload to confirm the IndexedDB replay. Enabling infra (all e2e-only; nothing enters the production build): - mock/client.ts fetchDict serves the per-variant dawgs from the preview build's /e2edict/ (was: threw 'unsupported in mock'). - scripts/e2e-dict.mjs copies the real dawgs into dist-e2e from E2E_DICT_DIR (the ui CI job fetches the scrabble-dictionary release like the Go jobs; local default: the sibling scrabble-solver/dawg); playwright.config runs it between build and preview. - localgame/id.ts setForcedSeed + gateway.ts window.__mock.setLocalSeed: a mock-only seam to pin a local game's bag seed (tree-shaken from prod). - ci.yaml ui job: fetch the dawgs + pass E2E_DICT_DIR to the e2e step. - docs/TESTING.md: the offline e2e + the mock-dawg wiring. Verified: check 0 / unit 482 / e2e 198 (both engines) / app entry 113.8/114.
84 lines
4.5 KiB
TypeScript
84 lines
4.5 KiB
TypeScript
import { test, expect, type Page } from './fixtures';
|
|
|
|
// The offline mode is gated to an installed standalone PWA with a confirmed email. The mock account
|
|
// has an email; force the standalone display mode so the Settings offline toggle is offered. Only the
|
|
// `(display-mode: standalone)` query is overridden — theme/reduce-motion queries pass through.
|
|
async function forceStandalone(page: Page): Promise<void> {
|
|
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;
|
|
});
|
|
}
|
|
|
|
// After a load or reload, land in the lobby (dismiss the login screen if it is shown).
|
|
async function enterLobby(page: Page): Promise<void> {
|
|
const guest = page.getByRole('button', { name: /guest|гост/i });
|
|
if (await guest.count()) await guest.first().click();
|
|
// The lobby tab bar has three tabs in a fixed order: New (0), Stats (1), Settings (2). nth() is
|
|
// robust to locale, emoji variation selectors and the coachmark anchors (which the first-run
|
|
// onboarding strips after it completes).
|
|
await expect(page.locator('button.tab').nth(2)).toBeVisible();
|
|
}
|
|
|
|
// Pick a rack tile by its glyph and drop it on a board square (the rack of the pinned-seed game has
|
|
// all-distinct letters, so the glyph is unambiguous).
|
|
async function placeTile(page: Page, glyph: string, row: number, col: number): Promise<void> {
|
|
await page.locator('.rack .tile', { hasText: glyph }).first().click();
|
|
await page.locator(`[data-cell][data-row="${row}"][data-col="${col}"]`).click();
|
|
}
|
|
|
|
test.describe('offline mode', () => {
|
|
test('enter via the toggle, play a local vs_ai game, persist across a reload', async ({ page }) => {
|
|
await forceStandalone(page);
|
|
await page.goto('/');
|
|
await enterLobby(page);
|
|
|
|
// Online: a seeded online game (vs Ann) is listed.
|
|
await expect(page.getByText('Ann', { exact: false }).first()).toBeVisible();
|
|
|
|
// Enter offline through the real Settings toggle (its readiness check fetches every enabled
|
|
// variant's dawg, served from /e2edict/ by the mock) — the header turns blue with the chip.
|
|
await page.locator('button.tab').nth(2).click();
|
|
await page.getByRole('button', { name: /^(Offline|Оффлайн)$/ }).click();
|
|
await expect(page.locator('header.nav.offline')).toBeVisible({ timeout: 15000 });
|
|
|
|
// Back in the (now offline) lobby: the online games are hidden and the Stats tab is disabled.
|
|
await page.getByRole('button', { name: /Back|Назад/i }).click();
|
|
await expect(page.locator('button.tab').nth(2)).toBeVisible();
|
|
await expect(page.getByText('Ann', { exact: false })).toHaveCount(0);
|
|
await expect(page.locator('button.tab').nth(1)).toBeDisabled();
|
|
|
|
// Create a device-local English vs_ai game with a pinned bag seed (deals the rack NEWYMAO).
|
|
await page.evaluate(() => (window as unknown as { __mock: { setLocalSeed(s: string): void } }).__mock.setLocalSeed('1'));
|
|
await page.locator('button.tab').nth(0).click();
|
|
// The English variant's display name is "Scrabble" (Latin) in both locales — the Russian
|
|
// variants are "Скрэббл"/"Эрудит"/"Erudite", so this uniquely selects the English game.
|
|
await page.locator('.variant', { hasText: 'Scrabble' }).click();
|
|
await page.locator('button.invite').click();
|
|
await expect(page.locator('[data-cell]').first()).toBeVisible();
|
|
|
|
// The human plays WAY horizontally across the centre (7,5)-(7,7).
|
|
await placeTile(page, 'W', 7, 5);
|
|
await placeTile(page, 'A', 7, 6);
|
|
await placeTile(page, 'Y', 7, 7);
|
|
await expect(page.locator('[data-cell].pending')).toHaveCount(3);
|
|
await page.locator('.make').click();
|
|
|
|
// The play commits and the local robot replies with a real move, so the board carries more than
|
|
// WAY's three tiles.
|
|
await expect(async () => {
|
|
expect(await page.locator('[data-cell].filled').count()).toBeGreaterThan(3);
|
|
}).toPass({ timeout: 15000 });
|
|
const filled = await page.locator('[data-cell].filled').count();
|
|
|
|
// Reload: the hash router restores the /game/<id> route and the local game replays from
|
|
// IndexedDB with every committed tile intact.
|
|
await page.reload();
|
|
await expect(page.locator('[data-cell]').first()).toBeVisible();
|
|
await expect(page.locator('[data-cell].filled')).toHaveCount(filled);
|
|
});
|
|
});
|