Round-6 follow-up: UX polish + client-IP fix
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 32s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s

- Client IP: the compose caddy trusts X-Forwarded-For from private-range
  upstreams (trusted_proxies private_ranges), so the real client IP survives
  the host-caddy hop (it was logging the docker caddy hop 172.18.0.x for chat
  moderation and bucketing the gateway per-IP rate limiter on it). Correct and
  spoof-safe in both contours (prod has no host caddy); peerIP unit-tested.
- Ad banner gated off behind a compile-time SHOW_AD_BANNER=false (the if-branch,
  the AdBanner import and banner.ts are tree-shaken out of the prod bundle).
- Landing: the Telegram entry is just the 64px logo (clickable, no button/text).
- TG-fullscreen header: title + menu centred as a pair (hamburger right of the
  title), pinned to the bottom of the TG nav band.
- Edge-swipe back (Screen): a left-edge rightward drag navigates to back
  (touch/pen only, armed from <=24px; skipped inside Telegram).
- Chat soft-keyboard: a bottom-sheet Modal lifted above the keyboard by a
  visualViewport-driven transform (compositor-only, no page/sheet relayout).
  iOS-specific, needs on-device tuning; native resize=none awaits Capacitor.
- Tests: e2e for the in-game '✓ in friends' item and a board→board tile
  relocation; codec units for last_activity_unix + OutgoingRequestList.

Deferred to the next PR (agreed): #4 enrich the your-turn/game-end push; #5 hide
finished games from the lobby.
This commit is contained in:
Ilia Denisov
2026-06-08 21:31:44 +02:00
parent f95a6cb9c8
commit 645df52c0b
12 changed files with 229 additions and 29 deletions
+23
View File
@@ -139,6 +139,29 @@ test('dropping the game ends it and shows the result', async ({ page }) => {
await expect(page.locator('.status .over')).toBeVisible();
});
test('a placed tile drags from one board cell to another (Stage 17 relocation)', async ({ page }) => {
await openGame(page);
await page.locator('.rack .tile').first().click();
await page.locator('[data-cell]:not(.filled)').nth(30).click();
const pending = page.locator('[data-cell].pending');
await expect(pending).toHaveCount(1);
const from = `${await pending.first().getAttribute('data-row')},${await pending.first().getAttribute('data-col')}`;
const target = page.locator('[data-cell]:not(.filled):not(.pending)').nth(45);
const fb = await pending.first().boundingBox();
const tb = await target.boundingBox();
// Pointer-drag the placed tile to a new cell (mouse events synthesise pointer events).
await page.mouse.move(fb!.x + fb!.width / 2, fb!.y + fb!.height / 2);
await page.mouse.down();
await page.mouse.move(tb!.x + tb!.width / 2, tb!.y + tb!.height / 2, { steps: 10 });
await page.mouse.up();
// Still exactly one pending tile (relocated, not duplicated), now at a different cell.
await expect(pending).toHaveCount(1);
const to = `${await pending.first().getAttribute('data-row')},${await pending.first().getAttribute('data-col')}`;
expect(to).not.toBe(from);
});
test('the board-label mode in Settings changes the on-board labels', async ({ page }) => {
await openGame(page);
// beginner (default) renders split "3× / word" labels.
+12
View File
@@ -122,6 +122,18 @@ test('game: add-to-friends flips to a disabled "request sent"', async ({ page })
await expect(sent).toBeDisabled();
});
test('game: an opponent who is already a friend shows a disabled "in friends"', async ({ page }) => {
await loginLobby(page);
await page.getByRole('button', { name: /Kaya/ }).click(); // the finished game vs Kaya, a seeded friend
await page.locator('.burger').first().click();
// The in-game friend item is derived from the server's friend list (Stage 17): a friend reads
// a disabled "✓ in friends", not the addable "Add to friends".
const inFriends = page.getByRole('button', { name: /in friends/i });
await expect(inFriends).toBeVisible();
await expect(inFriends).toBeDisabled();
await expect(page.getByRole('button', { name: /Add to friends: Kaya/ })).toHaveCount(0);
});
test('profile edit disables Save and flags an invalid display name', async ({ page }) => {
await loginLobby(page);
await page.locator('.burger').first().click();