Files
scrabble-game/ui/e2e/social.spec.ts
T
Ilia Denisov d733ce3119
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 13s
Tests · UI / test (push) Successful in 16s
Stage 8: UI social/account/history surfaces
Wire the deferred Stage 7 surfaces end-to-end (UI -> gateway transcode ->
backend REST -> existing domain services): friends (incl. one-time friend
codes), per-user blocks, friend-game invitations, profile editing + email
binding, the statistics screen, and the in-game history + GCG export.

Friends gain two add paths (interview decision, a deliberate plan change):
one-time 6-digit codes (friend_codes table, 12h TTL, single-use, rate-limited
redeem); and play-gated requests (shared game required) where an explicit
decline is permanent, an ignored request lapses after 30 days, and a code
bypasses a decline. Migration 00006 widens friendships_status_chk and adds
friend_codes.

Lobby notification badge is poll + push: a new generic `notify` event drives
it live; the client polls on open/focus. Language stays a single Settings
control that writes through to the durable account's preferred_language. GCG
export is finished-only (game.ErrGameActive) and shares/downloads the .gcg file.

Tests: backend unit + inttest (friend gate/decline/code, ListInvitations,
GetStats, GCG gate), gateway transcode round-trips + notify constructor, UI
vitest (codecs, win-rate, share choice) + Playwright social specs. Docs: PLAN
(Stage 8 done + refinements + TODO-5), ARCHITECTURE, FUNCTIONAL(+ru), UI_DESIGN,
TESTING, module READMEs.
2026-06-03 19:47:40 +02:00

76 lines
3.2 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test';
// Stage 8 social / account / history surfaces against the mock transport (no backend).
// The mock profile is a durable account, so friends, invitations, stats and the GCG
// export are reachable from the seeded fixture.
async function loginLobby(page: Page): Promise<void> {
await page.goto('/');
await page.getByRole('button', { name: /guest/i }).click();
await expect(page.getByText('Active games')).toBeVisible();
}
async function openFriends(page: Page): Promise<void> {
await page.locator('.burger').first().click();
await page.getByRole('button', { name: /Friends/ }).click();
}
test('friends: issue a code, accept an incoming request, redeem a code', async ({ page }) => {
await loginLobby(page);
await openFriends(page);
// Issue a one-time code — it is shown to share.
await page.getByRole('button', { name: /Show my code/i }).click();
await expect(page.getByTestId('friend-code')).toContainText('246813');
// The seeded incoming request (Rick) can be accepted; the requests section clears.
await expect(page.getByText('Friend requests')).toBeVisible();
await page.getByRole('button', { name: /^Accept$/ }).click();
await expect(page.getByText('Friend requests')).toBeHidden();
// Redeeming a code adds a new friend to the list.
await page.locator('.codein').fill('111111');
await page.getByRole('button', { name: /^Add$/ }).click();
await expect(page.locator('.who', { hasText: 'Friend 111111' })).toBeVisible();
});
test('invitations: the lobby shows an invitation and accepting clears it', async ({ page }) => {
await loginLobby(page);
await expect(page.getByText('Invitations')).toBeVisible();
await expect(page.getByText(/From Kaya/)).toBeVisible();
await page.getByRole('button', { name: /^Accept$/ }).click();
await expect(page.getByText(/From Kaya/)).toBeHidden();
});
test('stats screen shows the metrics', async ({ page }) => {
await loginLobby(page);
await page.getByRole('button', { name: /Stats/ }).click();
await expect(page.getByText('Win rate')).toBeVisible();
await expect(page.getByText('Best move')).toBeVisible();
});
test('profile edit saves a new display name', async ({ page }) => {
await loginLobby(page);
await page.locator('.burger').first().click();
await page.getByRole('button', { name: /Profile/ }).click();
await page.getByRole('button', { name: /Edit profile/ }).click();
await page.locator('.edit input').first().fill('Kaya Test');
await page.getByRole('button', { name: /^Save$/ }).click();
await expect(page.locator('.name')).toHaveText('Kaya Test');
});
test('GCG export appears only for a finished game', async ({ page }) => {
await loginLobby(page);
// The finished game vs Kaya exposes the export; the menu carries the item.
await page.getByRole('button', { name: /Kaya/ }).click();
await page.locator('.burger').first().click();
await expect(page.getByRole('button', { name: /Export GCG/ })).toBeVisible();
});
test('GCG export is hidden for an active game', async ({ page }) => {
await loginLobby(page);
await page.getByRole('button', { name: /Ann/ }).click();
await page.locator('.burger').first().click();
await expect(page.getByRole('button', { name: /Export GCG/ })).toHaveCount(0);
});