Stage 7: regression tests for the polished UI (logic + behaviour)
Tests · UI / test (push) Successful in 14s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 14s

Lock the polish branch's behaviour so a future UI edit surfaces as a failing
assertion to re-agree or fix.

Unit (vitest, node env):
- placement: recallIndex, cellOccupied/isBlankSlot, non-linear direction, the
  single-tile submit default, and placementFromHint blank-fallback / rack-exhausted.
- banner: the marquee scroll-cycle repeat-then-advance, stop(), root-relative and
  multiple links.
- client.GatewayError. Extract the check-word constraints out of Game.svelte into a
  pure lib/checkword.ts (sanitize + canCheck) and cover them.

E2E (playwright mock, Chromium + WebKit):
- commit via the 🏁 control, history slide-down + close, the exchange dialog,
  check-word input sanitising + verdict, resign-to-finished, and the Settings
  board-label mode changing the on-board labels.
This commit is contained in:
Ilia Denisov
2026-06-03 17:33:47 +02:00
parent 4c475f2b0e
commit f8f7d39364
7 changed files with 267 additions and 5 deletions
+86
View File
@@ -0,0 +1,86 @@
import { expect, test, type Page } from '@playwright/test';
// Behaviour/display coverage for the polished game screen, driven entirely by the mock
// transport (no backend). These lock the round-1..4 interactions so future UI edits
// surface as a failing assertion — to be re-agreed or fixed. The pure logic behind them
// (placement, check-word, board labels, result badges) is unit-tested separately.
async function openGame(page: Page): Promise<void> {
await page.goto('/');
await page.getByRole('button', { name: /guest/i }).click();
await page.getByRole('button', { name: /Ann/ }).click(); // the seeded active game vs Ann
await expect(page.locator('[data-cell]').first()).toBeVisible();
}
test('placing a tile and confirming via 🏁 commits the move', async ({ page }) => {
await openGame(page);
await page.locator('.rack .tile').first().click();
await page.locator('[data-cell]:not(.filled)').nth(30).click();
await expect(page.locator('[data-cell].pending')).toHaveCount(1);
await page.locator('.make').click(); // open the MakeMove popover (short tap)
await page.locator('.pop.go').click(); // "Make move ✅"
// After the commit the placement is cleared: no pending tile, no 🏁 control.
await expect(page.locator('[data-cell].pending')).toHaveCount(0);
await expect(page.locator('.make')).toBeHidden();
});
test('history slides the board down and closes on a board tap', async ({ page }) => {
await openGame(page);
await page.locator('.burger').click();
await page.locator('.dropdown button').nth(0).click(); // History
await expect(page.locator('.history')).toBeVisible();
await expect(page.locator('.boardwrap.slid')).toBeVisible();
await page.locator('.boardwrap').click(); // tapping the board closes it
await expect(page.locator('.history')).toBeHidden();
});
test('Draw opens the exchange dialog and confirms a selection', async ({ page }) => {
await openGame(page);
await page.locator('button:has-text("🔄")').click(); // Draw tab
await expect(page.locator('.exch')).toBeVisible();
await page.locator('.etile').first().click();
await expect(page.locator('.etile.sel')).toHaveCount(1);
await page.locator('button.confirm').click();
await expect(page.locator('.exch')).toBeHidden();
});
test('check-word sanitises input and shows a verdict', async ({ page }) => {
await openGame(page);
await page.locator('.burger').click();
await page.locator('.dropdown button').nth(2).click(); // Check word
const input = page.locator('.check input');
await input.fill('qz9!a'); // digits/punctuation dropped, letters upper-cased
await expect(input).toHaveValue('QZA');
await page.locator('.check button').click(); // Check (enabled: length 3)
await expect(page.locator('.ok, .bad')).toBeVisible();
});
test('dropping the game ends it and shows the result', async ({ page }) => {
await openGame(page);
await page.locator('.burger').click();
await page.locator('.dropdown button').nth(3).click(); // Drop game
await page.locator('button.danger').click(); // confirm in the modal
await expect(page.locator('.status .over')).toBeVisible();
});
test('the board-label mode in Settings changes the on-board labels', async ({ page }) => {
await openGame(page);
// beginner (default) renders split "3× / word" labels.
await expect(page.locator('.bsplit').first()).toBeVisible();
await expect(page.locator('.b1')).toHaveCount(0);
// Switch to "classic" in Settings (in-SPA hash nav keeps the guest session).
await page.evaluate(() => (location.hash = '/settings'));
await page.locator('.seg').nth(2).locator('.opt').nth(1).click(); // board labels -> classic
await page.evaluate(() => (location.hash = '/game/g1'));
// classic renders single "3W"/"2L" labels and no split labels.
await expect(page.locator('.b1').first()).toBeVisible();
await expect(page.locator('.bsplit')).toHaveCount(0);
});