add basic e2e tests

This commit is contained in:
2026-04-04 19:33:04 +02:00
parent 94fb38e038
commit 5fce1d591f
14 changed files with 519 additions and 2 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules
playwright-report
test-results
.vscode
.git
+16
View File
@@ -0,0 +1,16 @@
FROM mcr.microsoft.com/playwright:v1.59.1-noble
WORKDIR /work
RUN apt-get update \
&& apt-get install -y --no-install-recommends libnss3-tools \
&& rm -rf /var/lib/apt/lists/*
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN chmod +x /work/docker-entrypoint.sh
ENTRYPOINT ["/work/docker-entrypoint.sh"]
CMD ["yarn", "test"]
+57
View File
@@ -0,0 +1,57 @@
# E2E tests (Playwright)
This folder contains browser-level end-to-end tests for public, backend-independent user flows.
## Current scope
- Guest redirect from `/` to `/2e/karakter`
- Guest header links (`Register`, `Login`)
- Public legal pages (`/privacy.html`, `/ogl.html`, `/aelf.html`)
## Run locally
From `frontend/`:
- Install Playwright browser binaries:
- `yarn test:e2e:install`
- Run tests:
- `yarn test:e2e`
- Run headed mode:
- `yarn test:e2e:headed`
- Run interactive UI mode:
- `yarn test:e2e:ui`
## Notes
- Tests use the config in `../e2e/playwright.config.ts`.
- The Playwright web server starts Vite on `http://127.0.0.1:4173`.
- Tests mock `GET /api/User/me` so they stay deterministic without the backend.
## Run with Docker Compose
This mode runs against a full containerized stack:
- tagged backend image (`BACKEND_IMAGE`)
- supporting services (`postgres`, `nginx` proxy)
- freshly built `frontend` image
- freshly built `e2e` image
Prerequisites:
- Docker with Compose
Run:
```shell
docker compose -f docker-compose.e2e.yaml up --build --abort-on-container-exit --exit-code-from e2e e2e
```
Run with a different backend tag:
```shell
BACKEND_IMAGE=ghcr.io/morbalint/kemkas-backend:<tag> \
docker compose -f docker-compose.e2e.yaml up --build --abort-on-container-exit --exit-code-from e2e e2e
```
The `certs` service generates a local CA and nginx server certificate in a shared volume.
The `e2e` image imports that CA (`/certs/rootCA.pem`) into both the system trust store and Chromium's NSS store before running tests.
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
trusted_cert_path="${E2E_TRUSTED_CERT_PATH:-}"
if [[ -n "$trusted_cert_path" ]]; then
if [[ ! -f "$trusted_cert_path" ]]; then
echo "Trusted certificate not found at: $trusted_cert_path" >&2
exit 1
fi
install -m 0644 -D "$trusted_cert_path" /usr/local/share/ca-certificates/kemkas-proxy.crt
update-ca-certificates
if command -v certutil >/dev/null 2>&1; then
for candidate_home in /root /home/pwuser; do
if [[ -d "$candidate_home" ]]; then
nss_db_dir="$candidate_home/.pki/nssdb"
mkdir -p "$nss_db_dir"
certutil -d "sql:$nss_db_dir" -N --empty-password >/dev/null 2>&1 || true
certutil -d "sql:$nss_db_dir" -D -n kemkas-proxy >/dev/null 2>&1 || true
certutil -d "sql:$nss_db_dir" -A -t "C,," -n kemkas-proxy -i "$trusted_cert_path"
fi
done
fi
fi
if [[ -n "${E2E_WAIT_FOR_URL:-}" ]]; then
echo "Waiting for target URL: ${E2E_WAIT_FOR_URL}"
for attempt in {1..60}; do
if curl --silent --show-error --fail "${E2E_WAIT_FOR_URL}" >/dev/null; then
break
fi
if [[ "$attempt" -eq 60 ]]; then
echo "Timed out waiting for ${E2E_WAIT_FOR_URL}" >&2
exit 1
fi
sleep 2
done
fi
exec "$@"
+14
View File
@@ -0,0 +1,14 @@
{
"name": "kemkas-e2e",
"version": "0.1.0",
"private": true,
"scripts": {
"test": "playwright test",
"test:headed": "playwright test --headed",
"test:ui": "playwright test --ui",
"install:browsers": "playwright install chromium"
},
"devDependencies": {
"@playwright/test": "1.59.1"
}
}
+43
View File
@@ -0,0 +1,43 @@
import { defineConfig, devices } from '@playwright/test';
import path from 'node:path';
const port = Number(process.env.E2E_PORT ?? 4173);
const baseURL = process.env.E2E_BASE_URL ?? `http://127.0.0.1:${port}`;
const useWebServer = process.env.E2E_USE_WEBSERVER !== 'false';
export default defineConfig({
testDir: path.join(__dirname, 'tests'),
outputDir: path.join(__dirname, 'test-results'),
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
timeout: 30_000,
reporter: [
['list'],
['html', { outputFolder: path.join(__dirname, 'playwright-report'), open: 'never' }],
],
use: {
baseURL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
...(useWebServer
? {
webServer: {
command: `node ./node_modules/vite/bin/vite.js --host 127.0.0.1 --port ${port}`,
cwd: path.resolve(__dirname, '../frontend'),
port,
timeout: 120_000,
reuseExistingServer: !process.env.CI,
},
}
: {}),
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
+49
View File
@@ -0,0 +1,49 @@
import { expect, test } from '@playwright/test';
async function mockGuestSession(page: import('@playwright/test').Page): Promise<void> {
await page.route('**/api/User/me', async (route) => {
await route.fulfill({
status: 200,
contentType: 'text/plain; charset=utf-8',
body: '',
});
});
}
test('guest users are redirected to 2e character creation', async ({ page }) => {
await mockGuestSession(page);
await page.goto('/');
await expect(page).toHaveURL(/\/2e\/karakter$/);
await expect(page.getByRole('heading', { name: 'Karakter létrehozása' })).toBeVisible();
await expect(page.getByRole('button', { name: /Mentés/ })).toBeVisible();
await expect(page.getByRole('button', { name: /PDF/ })).toBeVisible();
});
test('guest navigation shows Register and Login links', async ({ page }) => {
await mockGuestSession(page);
await page.goto('/2e/karakter');
const registerLink = page.getByRole('link', { name: 'Register' });
const loginLink = page.getByRole('link', { name: 'Login' });
await expect(registerLink).toBeVisible();
await expect(registerLink).toHaveAttribute('href', '/Identity/Account/Register');
await expect(loginLink).toBeVisible();
await expect(loginLink).toHaveAttribute('href', '/Identity/Account/Login');
});
test('public legal pages are served', async ({ page }) => {
await page.goto('/privacy.html');
await expect(page.getByRole('heading', { name: 'Privacy Policy', level: 1 })).toBeVisible();
await page.goto('/ogl.html');
await expect(page).toHaveTitle(/Open Game License v1\.0a/);
await expect(page.locator('body')).toContainText('OPEN GAME LICENSE Version 1.0a');
await page.goto('/aelf.html');
await expect(page).toHaveTitle(/AELF Open License version 1\.0a/);
await expect(page.locator('body')).toContainText('AELF OPEN LICENSE VERSION 1.0a');
});
+29
View File
@@ -0,0 +1,29 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@playwright/test@1.59.1":
version "1.59.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.59.1.tgz#5c4d38eac84a61527af466602ae20277685a02d6"
integrity sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==
dependencies:
playwright "1.59.1"
fsevents@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
playwright-core@1.59.1:
version "1.59.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.59.1.tgz#d8a2b28bcb8f2bd08ef3df93b02ae83c813244b2"
integrity sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==
playwright@1.59.1:
version "1.59.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.59.1.tgz#f7b0ca61637ae25264cec370df671bbe1f368a4a"
integrity sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==
dependencies:
playwright-core "1.59.1"
optionalDependencies:
fsevents "2.3.2"