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.
This commit is contained in:
+1
-1
@@ -64,7 +64,7 @@ test('check-word sanitises input and shows a verdict', async ({ page }) => {
|
||||
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.getByRole('button', { name: 'Drop game' }).click(); // robust against menu growth
|
||||
await page.locator('button.danger').click(); // confirm in the modal
|
||||
await expect(page.locator('.status .over')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user