v0.15.0 password recovery, phone layout, CLI pager, embed policy fixes
This commit is contained in:
1 parent
a999177f0c
commit
e1fa5242e3
48 files changed
+2101
-135
No files matched your search
+14
-8
@@ -38,11 +38,12 @@ The server runs with `-dev`, debug logging, HTTP request logging, and relaxed ra
|
||||
It drops the test tables on exit, so it's preferred for clean testing.
|
||||
Can be used for all the tests.
|
||||
|
||||
Logging can be adjusted without editing the script, and `WEB_PORT` also
|
||||
serves the embedded web client (needed by the end-to-end suite):
|
||||
Logging can be adjusted without editing the script; `WEB_PORT` also serves
|
||||
the embedded web client and `MAIL_DIR` turns on password recovery with the
|
||||
email written to that directory (both used by the end-to-end suite):
|
||||
```bash
|
||||
LOG_LEVEL=info LOG_HTTP=false test/run-test-server.sh bin/chess-server
|
||||
WEB_PORT=9090 test/run-test-server.sh bin/chess-server
|
||||
WEB_PORT=9090 MAIL_DIR=/tmp/chess-mail test/run-test-server.sh bin/chess-server
|
||||
```
|
||||
|
||||
### Pre-configured Users
|
||||
@@ -226,20 +227,22 @@ Playwright drives the embedded web client in Chromium against a running
|
||||
server with PostgreSQL and Stockfish: it plays a short game, lists it under
|
||||
My games, replays it with the buttons and the keyboard, autoplays to the end,
|
||||
compares the FEN and PGN it exports (copy and download) with the API, follows
|
||||
a deep link to a checkmate, steps back through the browser history, and
|
||||
replays an anonymous game from the device list.
|
||||
a deep link to a checkmate, steps back through the browser history, replays
|
||||
an anonymous game from the device list, checks that a phone-sized viewport
|
||||
holds the board and the controls without scrolling, and resets a forgotten
|
||||
password through the link in the emailed message (needs `MAIL_DIR`).
|
||||
|
||||
Requirements: Node.js 20+ and a Chromium that Playwright can run.
|
||||
|
||||
```bash
|
||||
# Terminal 1: the test server with the web client
|
||||
WEB_PORT=9090 test/run-test-server.sh bin/chess-server
|
||||
# Terminal 1: the test server with the web client and mail to a directory
|
||||
WEB_PORT=9090 MAIL_DIR=/tmp/chess-mail test/run-test-server.sh bin/chess-server
|
||||
|
||||
# Terminal 2, once
|
||||
cd test/e2e && npm install && npx playwright install chromium
|
||||
|
||||
# Terminal 2: run
|
||||
npm test # or, from the repository root: make test-e2e
|
||||
CHESS_E2E_MAIL_DIR=/tmp/chess-mail npm test # or, from the repository root: make test-e2e
|
||||
```
|
||||
|
||||
Environment:
|
||||
@@ -249,6 +252,9 @@ Environment:
|
||||
to register users, create games and fetch what the client should export
|
||||
- `CHESS_E2E_CHROMIUM`: path to a Chromium binary when
|
||||
`npx playwright install` is not possible (an already installed browser)
|
||||
- `CHESS_E2E_MAIL_DIR`: the server's `-mail-dir` (`MAIL_DIR` of the test
|
||||
server); the password-reset test reads the link from the newest file and
|
||||
is skipped when unset
|
||||
|
||||
Each test registers its own user or plays anonymously, so the suite can be
|
||||
repeated against the same server. The engine plays at level 0 with a 100 ms
|
||||
|
||||
+91
-1
@@ -6,6 +6,11 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const API = process.env.CHESS_E2E_API || 'http://localhost:8080';
|
||||
// Where the server writes password-reset email (-mail-dir); the reset test
|
||||
// is skipped without it.
|
||||
const MAIL_DIR = process.env.CHESS_E2E_MAIL_DIR || '';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
|
||||
|
||||
// The engine level and search time are set through the sliders' inputs.
|
||||
@@ -174,6 +179,90 @@ test.describe('web replay', () => {
|
||||
await expect(page).toHaveURL(/\/$/);
|
||||
});
|
||||
|
||||
test('a phone fits the board and the controls without scrolling', async ({ browser }) => {
|
||||
const context = await browser.newContext({ viewport: { width: 412, height: 780 }, isMobile: true, hasTouch: true });
|
||||
const page = await context.newPage();
|
||||
await page.goto('/');
|
||||
const fits = () => page.evaluate(() => ({
|
||||
w: document.documentElement.scrollWidth <= innerWidth,
|
||||
h: document.documentElement.scrollHeight <= innerHeight,
|
||||
}));
|
||||
expect(await fits()).toEqual({ w: true, h: true });
|
||||
// The controls sit in one row of short labels.
|
||||
await expect(page.locator('#new-game-btn')).toHaveText('New');
|
||||
const [newBox, resignBox] = await Promise.all([
|
||||
page.locator('#new-game-btn').boundingBox(), page.locator('#resign-btn').boundingBox()]);
|
||||
expect(Math.abs(newBox.y - resignBox.y)).toBeLessThan(2);
|
||||
expect(newBox.height).toBeLessThan(50);
|
||||
await expect(page.locator('.pane-live .toolbar')).toBeInViewport();
|
||||
// A replay keeps its controls on screen too.
|
||||
const created = await (await context.request.post(`${API}/api/games`, { data: { white: { type: 1 }, black: { type: 1 } } })).json();
|
||||
await context.request.post(`${API}/api/games/${created.gameId}/moves`, { data: { move: 'e2e4' } });
|
||||
await page.goto(`/?replay=${created.gameId}&ply=1`);
|
||||
await expect(page.locator('body')).toHaveAttribute('data-mode', 'replay');
|
||||
expect(await fits()).toEqual({ w: true, h: true });
|
||||
await expect(page.locator('#replay-exit')).toBeInViewport();
|
||||
await expect(page.locator('#replay-link')).toBeInViewport();
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('a forgotten password is reset through the emailed link', async ({ page, request }) => {
|
||||
test.skip(!MAIL_DIR, 'CHESS_E2E_MAIL_DIR is not set (start the server with MAIL_DIR)');
|
||||
const username = `reset${Date.now().toString(36)}${Math.floor(Math.random() * 1000)}`;
|
||||
const email = `${username}@example.test`;
|
||||
const created = await request.post(`${API}/api/auth/register`, {
|
||||
data: { username, email, password: 'OldPass123' },
|
||||
});
|
||||
expect(created.status(), await created.text()).toBe(201);
|
||||
|
||||
await page.goto('/');
|
||||
await expect(page.locator('#storage-dot')).toHaveAttribute('data-status', 'ok');
|
||||
await page.click('#auth-btn');
|
||||
// A wrong password keeps the focus in the password field.
|
||||
await page.fill('#login-identifier', username);
|
||||
await page.fill('#login-password', 'wrong');
|
||||
await page.click('#login-submit-btn');
|
||||
await expect(page.locator('#auth-modal-message')).toContainText('Wrong username or password');
|
||||
await expect(page.locator('#login-password')).toBeFocused();
|
||||
|
||||
await expect(page.locator('#forgot-btn')).toBeVisible();
|
||||
await page.click('#forgot-btn');
|
||||
await page.fill('#recover-identifier', email);
|
||||
const mails = () => (fs.existsSync(MAIL_DIR) ? fs.readdirSync(MAIL_DIR) : []);
|
||||
const before = mails().length;
|
||||
await page.click('#recover-submit-btn');
|
||||
await expect(page.locator('#auth-modal-message')).toContainText('on its way');
|
||||
await expect.poll(() => mails().length, { timeout: 10_000 }).toBeGreaterThan(before);
|
||||
const files = fs.readdirSync(MAIL_DIR).map(f => path.join(MAIL_DIR, f))
|
||||
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
||||
const mail = fs.readFileSync(files[0], 'utf8');
|
||||
expect(mail).toContain(`To: ${email}`);
|
||||
const link = /(https?:\/\/\S+\?reset=[A-Za-z0-9_-]+)/.exec(mail)[1];
|
||||
const origin = new URL(page.url()).origin;
|
||||
await page.goto(link.replace(/^https?:\/\/[^/]+/, origin));
|
||||
await expect(page.locator('#reset-overlay')).toHaveClass(/show/);
|
||||
// The token is removed from the address at once.
|
||||
expect(page.url()).not.toContain('reset=');
|
||||
await page.fill('#reset-password', 'NewPass123');
|
||||
await page.fill('#reset-password-2', 'NewPass124');
|
||||
await page.click('#reset-submit-btn');
|
||||
await expect(page.locator('#reset-modal-message')).toContainText('differ');
|
||||
await page.fill('#reset-password-2', 'NewPass123');
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.locator('#auth-label')).toHaveText(username);
|
||||
await expect(page.locator('#reset-overlay')).not.toHaveClass(/show/, { timeout: 5_000 });
|
||||
// The old password is gone, the new one and the link are not reusable.
|
||||
const old = await request.post(`${API}/api/auth/login`, { data: { identifier: username, password: 'OldPass123' } });
|
||||
expect(old.status()).toBe(401);
|
||||
const fresh = await request.post(`${API}/api/auth/login`, { data: { identifier: username, password: 'NewPass123' } });
|
||||
expect(fresh.status()).toBe(200);
|
||||
await page.goto(link.replace(/^https?:\/\/[^/]+/, origin));
|
||||
await page.fill('#reset-password', 'NewPass125');
|
||||
await page.fill('#reset-password-2', 'NewPass125');
|
||||
await page.click('#reset-submit-btn');
|
||||
await expect(page.locator('#reset-modal-message')).toContainText('invalid or has expired');
|
||||
});
|
||||
|
||||
test('an anonymous visitor replays from the device list', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('#auth-label')).toHaveText('Sign in');
|
||||
@@ -193,6 +282,7 @@ test.describe('web replay', () => {
|
||||
await expect(page.locator('.coordinates.ranks span').first()).toHaveText('1');
|
||||
expect(await page.evaluate(() => replay.history.initialFen)).toBe(START_FEN);
|
||||
await page.click('#replay-exit');
|
||||
await expect(page.locator('#turn-text')).toContainText('Your move (Black)');
|
||||
await expect(page.locator('#turn-text')).toHaveText('Your move');
|
||||
await expect(page.locator('#turn-dot')).toHaveAttribute('data-status', 'black');
|
||||
});
|
||||
});
|
||||
+10
-1
@@ -10,8 +10,11 @@ CHESS_SERVER_EXEC=${1:-"bin/chess-server"}
|
||||
export CHESS_DSN="$CHESS_TEST_DSN"
|
||||
PID_FILE="/tmp/chess-server_test.pid"
|
||||
API_PORT=${API_PORT:-8080}
|
||||
# Set WEB_PORT to also serve the embedded web client (for test/e2e).
|
||||
# Set WEB_PORT to also serve the embedded web client (for test/e2e), and
|
||||
# MAIL_DIR to enable password recovery with the email written to that
|
||||
# directory instead of sent.
|
||||
WEB_PORT=${WEB_PORT:-}
|
||||
MAIL_DIR=${MAIL_DIR:-}
|
||||
LOG_LEVEL=${LOG_LEVEL:-debug}
|
||||
LOG_HTTP=${LOG_HTTP:-true}
|
||||
|
||||
@@ -98,6 +101,7 @@ echo "Configuration:"
|
||||
echo " Executable: $CHESS_SERVER_EXEC"
|
||||
echo " Database: \$CHESS_TEST_DSN (tables dropped on exit)"
|
||||
echo " Port: $API_PORT${WEB_PORT:+ (web client on $WEB_PORT)}"
|
||||
echo " Mail: ${MAIL_DIR:-off (set MAIL_DIR for password recovery)}"
|
||||
echo " Mode: Development (relaxed rate limits; PostgreSQL storage)"
|
||||
echo " Log level: $LOG_LEVEL (HTTP requests: $LOG_HTTP)"
|
||||
echo " Purpose: Backend for chess-server tests"
|
||||
@@ -115,6 +119,11 @@ echo ""
|
||||
# Start chess-server in foreground with dev mode and storage
|
||||
WEB_FLAGS=()
|
||||
[ -n "$WEB_PORT" ] && WEB_FLAGS=(-serve -web-port "$WEB_PORT")
|
||||
if [ -n "$MAIL_DIR" ]; then
|
||||
mkdir -p "$MAIL_DIR"
|
||||
WEB_FLAGS+=(-mail-dir "$MAIL_DIR" -mail-from "Chess <chess@localhost>" \
|
||||
-public-url "http://localhost:${WEB_PORT:-9090}/")
|
||||
fi
|
||||
"$CHESS_SERVER_EXEC" \
|
||||
-dev \
|
||||
-log-level "$LOG_LEVEL" \
|
||||
|
||||
Reference in new issue
Block a user