feat: add comprehensive Playwright UI test suite

25 tests across auth (A1-A5), posting (P1-P5), validation (V1-V4),
tracker (T1-T5), and nav (N1-N5). Uses storageState for single login
per run. Replaces post-with-photo.spec.js with post.spec.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 22:34:11 +02:00
parent 545e3f5ba0
commit a50e7f5386
12 changed files with 567 additions and 91 deletions
+62
View File
@@ -0,0 +1,62 @@
// @ts-check
// Tests: A1A5 — authentication and access control
// A1/A2/A3 run WITHOUT storageState to test the login page itself.
// A4 uses a fresh context without storageState to verify access control.
// A5 uses storageState (already logged in).
const { test, expect } = require('@playwright/test');
const USER = process.env.GRAV_TEST_USER;
const PASS = process.env.GRAV_TEST_PASS;
// ── A1A3: require a clean session (no prior auth) ────────────────────────────
test.describe('login page (unauthenticated)', () => {
test.use({ storageState: { cookies: [], origins: [] } });
// A1: Login page loads
test('A1: login page renders the login form', async ({ page }) => {
await page.goto('/login');
await expect(page.locator('input[name="username"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toBeVisible();
await expect(page.locator('button[type="submit"], input[type="submit"]')).toBeVisible();
});
// A2: Invalid credentials
test('A2: invalid credentials show an error message', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="username"]', 'wronguser');
await page.fill('input[name="password"]', 'wrongpass');
await page.click('button[type="submit"], input[type="submit"]');
// Grav stays on the login page and shows a flash error
await expect(page.locator('body')).toContainText(/invalid|incorrect|failed/i, { timeout: 8_000 });
await expect(page).toHaveURL(/login/);
});
// A3: Valid login
test('A3: valid credentials show success message', async ({ page }) => {
if (!USER || !PASS) test.skip(true, 'GRAV_TEST_USER / GRAV_TEST_PASS not set');
await page.goto('/login');
await page.fill('input[name="username"]', USER);
await page.fill('input[name="password"]', PASS);
await page.click('button[type="submit"], input[type="submit"]');
await expect(page.locator('body')).toContainText('successfully logged in', { timeout: 10_000 });
});
});
// ── A4: /post without auth shows a login form ─────────────────────────────────
// Uses a fresh context with no storageState.
test('A4: /post without auth renders an inline login form', async ({ browser }) => {
const ctx = await browser.newContext({ storageState: { cookies: [], origins: [] } });
const page = await ctx.newPage();
const baseURL = process.env.GRAV_BASE_URL || 'http://localhost:8081';
await page.goto(`${baseURL}/post`);
// Grav renders a login form inline at the /post URL (section#grav-login)
await expect(page.locator('#grav-login')).toBeVisible({ timeout: 8_000 });
await ctx.close();
});
// ── A5: /post with auth shows the post form ───────────────────────────────────
test('A5: /post with auth renders the post form', async ({ page }) => {
await page.goto('/post');
await expect(page.locator('.post-form-wrap')).toBeVisible();
await expect(page.locator('input[name="data[title]"]')).toBeVisible();
});