refactor(e2e): migrate all tests to Playwright Test

This commit is contained in:
Johan Lundberg 2026-02-18 11:37:23 +01:00
parent 174c6c001e
commit 7900f264ba
No known key found for this signature in database
GPG key ID: A6C152738D03C7D1
14 changed files with 398 additions and 401 deletions

View file

@ -0,0 +1,16 @@
// @ts-check
const { test, expect } = require('@playwright/test');
test.describe('Auth guard', () => {
test('unauthenticated /manage/credentials redirects to /login', async ({ page }) => {
await page.goto('/manage/credentials');
await page.waitForURL('**/login', { timeout: 5000 });
expect(page.url()).toContain('/login');
});
test('unauthenticated /manage/credentials?setup=1 redirects to /login', async ({ page }) => {
await page.goto('/manage/credentials?setup=1');
await page.waitForURL('**/login', { timeout: 5000 });
expect(page.url()).toContain('/login');
});
});

View file

@ -0,0 +1,74 @@
// @ts-check
const { test, expect } = require('@playwright/test');
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
test.describe('Credentials page', () => {
test.beforeEach(async ({ page }) => {
// Log in with dedicated credentials user
await page.goto('/login');
await page.fill('#username', fixtures.cred_username);
await page.fill('#password', fixtures.cred_password);
await page.click('form[hx-post="/login/password"] button[type="submit"]');
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
});
test.describe('Page structure', () => {
test('title contains Credentials and Porchlight', async ({ page }) => {
await expect(page).toHaveTitle(/Credentials/);
await expect(page).toHaveTitle(/Porchlight/);
});
test('H1 says "Credentials"', async ({ page }) => {
await expect(page.locator('h1')).toHaveText('Credentials');
});
test('security keys heading is visible', async ({ page }) => {
await expect(page.locator('h2:has-text("Security keys")')).toBeVisible();
});
test('add security key button is visible', async ({ page }) => {
await expect(page.locator('#webauthn-register-btn')).toBeVisible();
});
test('password heading is visible', async ({ page }) => {
await expect(page.locator('h2:has-text("Password")')).toBeVisible();
});
test('password section is visible', async ({ page }) => {
await expect(page.locator('#password-section')).toBeVisible();
});
});
test.describe('Password validation', () => {
test('shows mismatch error', async ({ page }) => {
await page.fill('#password', 'newpassword1');
await page.fill('#confirm', 'newpassword2');
await page.click('#password-section button[type="submit"]');
const alert = page.locator('#password-section [role="alert"]');
await expect(alert).toBeVisible({ timeout: 5000 });
await expect(alert).toContainText('do not match');
});
test('password input has minlength="8"', async ({ page }) => {
await expect(page.locator('#password')).toHaveAttribute('minlength', '8');
});
test('confirm input has minlength="8"', async ({ page }) => {
await expect(page.locator('#confirm')).toHaveAttribute('minlength', '8');
});
});
test.describe('Password change', () => {
test('succeeds with matching passwords', async ({ page }) => {
await page.fill('#password', 'newpassword123');
await page.fill('#confirm', 'newpassword123');
await page.click('#password-section button[type="submit"]');
const status = page.locator('#password-section [role="status"]');
await expect(status).toBeVisible({ timeout: 5000 });
await expect(status).toContainText('Password updated');
});
});
});

View file

@ -0,0 +1,62 @@
// @ts-check
const { test, expect } = require('@playwright/test');
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
test.describe('Full user journey', () => {
test('register via magic link, set password, logout, login', async ({ page, request }) => {
// Verify fixtures are loaded
expect(fixtures.register_token).toBeTruthy();
// ---- Step 1: Register via magic link ----
await page.goto(`/register/${fixtures.register_token}`);
// Should redirect to /manage/credentials?setup=1
await page.waitForURL('**/manage/credentials?setup=1', { timeout: 5000 });
expect(page.url()).toContain('/manage/credentials');
// Should show welcome message
const welcome = page.locator('[role="status"]');
await expect(welcome).toBeVisible();
const welcomeText = await welcome.textContent();
expect(welcomeText).toContain('Welcome');
// Page title should contain Porchlight
await expect(page).toHaveTitle(/Porchlight/);
// ---- Step 2: Set password ----
const passwordInput = page.locator('#password');
const confirmInput = page.locator('#confirm');
await expect(passwordInput).toBeVisible();
await expect(confirmInput).toBeVisible();
await passwordInput.fill('mypassword123');
await confirmInput.fill('mypassword123');
await page.click('#password-section button[type="submit"]');
// Wait for success message
const successMsg = page.locator('#password-section [role="status"]');
await expect(successMsg).toBeVisible({ timeout: 5000 });
await expect(successMsg).toContainText('Password updated');
// ---- Step 3: Logout ----
await request.post('/logout');
// Navigate to credentials — should redirect to login since we're logged out
await page.goto('/manage/credentials');
await page.waitForURL('**/login', { timeout: 5000 });
expect(page.url()).toContain('/login');
// ---- Step 4: Login with the password we just set ----
await page.fill('#username', fixtures.register_username);
await page.fill('#password', 'mypassword123');
await page.click('form[hx-post="/login/password"] button[type="submit"]');
// Wait for redirect to credentials page
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
expect(page.url()).toContain('/manage/credentials');
// Should NOT show setup message (no ?setup=1)
await expect(page.locator('[role="status"]:has-text("Welcome")')).toHaveCount(0);
});
});

11
tests/e2e/health.spec.js Normal file
View file

@ -0,0 +1,11 @@
// @ts-check
const { test, expect } = require('@playwright/test');
test.describe('Health endpoint', () => {
test('returns OK status', async ({ request }) => {
const resp = await request.get('/health');
expect(resp.ok()).toBe(true);
const body = await resp.json();
expect(body.status).toBe('ok');
});
});

155
tests/e2e/login.spec.js Normal file
View file

@ -0,0 +1,155 @@
// @ts-check
const { test, expect } = require('@playwright/test');
test.describe('Login page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test.describe('Branding', () => {
test('page title contains Porchlight', async ({ page }) => {
await expect(page).toHaveTitle(/Porchlight/);
});
test('page title does not contain FastAPI', async ({ page }) => {
const title = await page.title();
expect(title).not.toContain('FastAPI');
});
test('favicon link points to /static/favicon.png', async ({ page }) => {
await expect(page.locator('link[rel="icon"]')).toHaveAttribute('href', '/static/favicon.png');
});
test('favicon file is served', async ({ request }) => {
const resp = await request.get('/static/favicon.png');
expect(resp.ok()).toBe(true);
});
});
test.describe('Site header & logo', () => {
test('site header is visible', async ({ page }) => {
await expect(page.locator('.site-header')).toBeVisible();
});
test('logo image is visible', async ({ page }) => {
await expect(page.locator('.site-logo')).toBeVisible();
});
test('logo src is /static/logo.svg', async ({ page }) => {
await expect(page.locator('.site-logo')).toHaveAttribute('src', '/static/logo.svg');
});
test('logo SVG file is served', async ({ request }) => {
const resp = await request.get('/static/logo.svg');
expect(resp.ok()).toBe(true);
});
test('site title text is Porchlight', async ({ page }) => {
const siteTitle = page.locator('.site-title');
await expect(siteTitle).toBeVisible();
await expect(siteTitle).toHaveText('Porchlight');
});
});
test.describe('Accessibility', () => {
test('skip link is present', async ({ page }) => {
await expect(page.locator('.skip-link')).toHaveCount(1);
});
test('main landmark with id="main" exists', async ({ page }) => {
await expect(page.locator('main#main')).toHaveCount(1);
});
test('polite live region exists', async ({ page }) => {
await expect(page.locator('#live[aria-live="polite"]')).toHaveCount(1);
});
});
test.describe('Login form structure', () => {
test('H1 says "Sign in"', async ({ page }) => {
await expect(page.locator('h1')).toHaveText('Sign in');
});
test('password login form exists', async ({ page }) => {
await expect(page.locator('form[hx-post="/login/password"]')).toHaveCount(1);
});
test('username input is visible', async ({ page }) => {
await expect(page.locator('#username')).toBeVisible();
});
test('password input is visible', async ({ page }) => {
await expect(page.locator('#password')).toBeVisible();
});
test('submit button is visible', async ({ page }) => {
await expect(page.locator('form[hx-post="/login/password"] button[type="submit"]')).toBeVisible();
});
test('WebAuthn login form exists', async ({ page }) => {
await expect(page.locator('#webauthn-login-form')).toHaveCount(1);
});
});
test.describe('Theme / styling', () => {
test('body background is themed', async ({ page }) => {
const bgColor = await page.evaluate(() => getComputedStyle(document.body).backgroundColor);
expect(['rgb(250, 250, 249)', 'rgb(28, 25, 23)']).toContain(bgColor);
});
test('button uses amber accent color', async ({ page }) => {
const btnBg = await page.evaluate(() => {
const btn = document.querySelector('button[type="submit"]');
return getComputedStyle(btn).backgroundColor;
});
expect(['rgb(217, 119, 6)', 'rgb(245, 158, 11)']).toContain(btnBg);
});
test('sections have surface background', async ({ page }) => {
const sectionBg = await page.evaluate(() => {
const section = document.querySelector('section');
return getComputedStyle(section).backgroundColor;
});
expect(['rgb(245, 245, 244)', 'rgb(41, 37, 36)']).toContain(sectionBg);
});
test('sections have solid border', async ({ page }) => {
const sectionBorder = await page.evaluate(() => {
const section = document.querySelector('section');
return getComputedStyle(section).borderStyle;
});
expect(sectionBorder).toBe('solid');
});
});
test.describe('Static assets', () => {
test('style.css is served', async ({ request }) => {
const resp = await request.get('/static/style.css');
expect(resp.ok()).toBe(true);
});
test('CSS contains --accent custom property', async ({ request }) => {
const resp = await request.get('/static/style.css');
const body = await resp.text();
expect(body).toContain('--accent');
});
test('CSS contains amber accent color #d97706', async ({ request }) => {
const resp = await request.get('/static/style.css');
const body = await resp.text();
expect(body).toContain('#d97706');
});
test('CSS contains dark mode media query', async ({ request }) => {
const resp = await request.get('/static/style.css');
const body = await resp.text();
expect(body).toContain('prefers-color-scheme: dark');
});
test('CSS contains reduced motion media query', async ({ request }) => {
const resp = await request.get('/static/style.css');
const body = await resp.text();
expect(body).toContain('prefers-reduced-motion');
});
});
});

View file

@ -0,0 +1,61 @@
// @ts-check
const { test, expect } = require('@playwright/test');
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
test.describe('Password authentication', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test.describe('Error states', () => {
test('shows error for nonexistent user', async ({ page }) => {
await page.fill('#username', 'nobody');
await page.fill('#password', 'whatever');
await page.click('form[hx-post="/login/password"] button[type="submit"]');
const alert = page.locator('[role="alert"]');
await expect(alert).toBeVisible({ timeout: 5000 });
await expect(alert).toContainText('Invalid username or password');
});
test('shows error for wrong password', async ({ page }) => {
await page.fill('#username', fixtures.login_username);
await page.fill('#password', 'wrongpassword');
await page.click('form[hx-post="/login/password"] button[type="submit"]');
const alert = page.locator('[role="alert"]');
await expect(alert).toBeVisible({ timeout: 5000 });
await expect(alert).toContainText('Invalid username or password');
});
});
test.describe('Successful login', () => {
test('redirects to credentials page', async ({ page }) => {
await page.fill('#username', fixtures.login_username);
await page.fill('#password', fixtures.login_password);
await page.click('form[hx-post="/login/password"] button[type="submit"]');
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
expect(page.url()).toContain('/manage/credentials');
});
});
test.describe('Form validation attributes', () => {
test('username has required attribute', async ({ page }) => {
await expect(page.locator('#username')).toHaveAttribute('required', '');
});
test('password has required attribute', async ({ page }) => {
await expect(page.locator('#password')).toHaveAttribute('required', '');
});
test('username autocomplete is "username"', async ({ page }) => {
await expect(page.locator('#username')).toHaveAttribute('autocomplete', 'username');
});
test('password autocomplete is "current-password"', async ({ page }) => {
await expect(page.locator('#password')).toHaveAttribute('autocomplete', 'current-password');
});
});
});

View file

@ -0,0 +1,19 @@
// @ts-check
const { test, expect } = require('@playwright/test');
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
test.describe('Registration', () => {
test('invalid token returns 400', async ({ page }) => {
const resp = await page.goto('/register/invalid-token-12345');
expect(resp.status()).toBe(400);
await expect(page.locator('body')).toContainText('Invalid or expired');
});
test('used token returns 400', async ({ page }) => {
expect(fixtures.used_token).toBeTruthy();
const resp = await page.goto(`/register/${fixtures.used_token}`);
expect(resp.status()).toBe(400);
await expect(page.locator('body')).toContainText('Invalid or expired');
});
});

View file

@ -1,18 +0,0 @@
// tests/e2e/test_auth_guard.js
// Tests that protected routes redirect unauthenticated users to /login.
const { TARGET_URL, run } = require('./helpers');
run(async (page, assert) => {
// ---- Unauthenticated access to /manage/credentials ----
console.log('\n--- Auth guard: /manage/credentials ---');
await page.goto(`${TARGET_URL}/manage/credentials`);
await page.waitForURL('**/login', { timeout: 5000 });
assert(page.url().includes('/login'), 'GET /manage/credentials redirects to /login');
// ---- Unauthenticated access to /manage/credentials?setup=1 ----
console.log('\n--- Auth guard: /manage/credentials?setup=1 ---');
await page.goto(`${TARGET_URL}/manage/credentials?setup=1`);
await page.waitForURL('**/login', { timeout: 5000 });
assert(page.url().includes('/login'), 'GET /manage/credentials?setup=1 redirects to /login');
});

View file

@ -1,75 +0,0 @@
// tests/e2e/test_credentials.js
// Tests credential management page: structure, password set/change, validation errors.
const { TARGET_URL, run } = require('./helpers');
run(async (page, assert) => {
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
// ---- Setup: Log in with dedicated credentials user ----
console.log('\n--- Setup: login ---');
await page.goto(`${TARGET_URL}/login`);
await page.fill('#username', fixtures.cred_username);
await page.fill('#password', fixtures.cred_password);
await page.click('form[hx-post="/login/password"] button[type="submit"]');
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
// ---- Page structure ----
console.log('\n--- Credentials page structure ---');
const title = await page.title();
assert(title.includes('Credentials'), `Title contains "Credentials" (got: "${title}")`);
assert(title.includes('Porchlight'), `Title contains "Porchlight" (got: "${title}")`);
const h1 = await page.locator('h1').textContent();
assert(h1 === 'Credentials', `H1 says "Credentials" (got: "${h1}")`);
// Security keys section
const securityKeysH2 = page.locator('h2:has-text("Security keys")');
assert(await securityKeysH2.isVisible(), 'Security keys heading visible');
const registerBtn = page.locator('#webauthn-register-btn');
assert(await registerBtn.isVisible(), 'Add security key button visible');
// Password section
const passwordH2 = page.locator('h2:has-text("Password")');
assert(await passwordH2.isVisible(), 'Password heading visible');
const passwordSection = page.locator('#password-section');
assert(await passwordSection.isVisible(), 'Password section visible');
// ---- Password validation: mismatch ----
console.log('\n--- Password validation: mismatch ---');
await page.fill('#password', 'newpassword1');
await page.fill('#confirm', 'newpassword2');
await page.click('#password-section button[type="submit"]');
await page.waitForSelector('#password-section [role="alert"]', { timeout: 5000 });
const mismatchErr = await page.locator('#password-section [role="alert"]').textContent();
assert(
mismatchErr.includes('do not match'),
`Shows mismatch error (got: "${mismatchErr}")`
);
// ---- Password validation: minlength enforced client-side ----
console.log('\n--- Password validation: minlength attribute ---');
// Reload page to clear HTMX state (the form was replaced by the error div)
await page.goto(`${TARGET_URL}/manage/credentials`);
const pwMinlength = await page.locator('#password').getAttribute('minlength');
assert(pwMinlength === '8', `Password input has minlength="8" (got: "${pwMinlength}")`);
const confirmMinlength = await page.locator('#confirm').getAttribute('minlength');
assert(confirmMinlength === '8', `Confirm input has minlength="8" (got: "${confirmMinlength}")`);
// ---- Password change: success ----
console.log('\n--- Password change: success ---');
await page.goto(`${TARGET_URL}/manage/credentials`);
await page.fill('#password', 'newpassword123');
await page.fill('#confirm', 'newpassword123');
await page.click('#password-section button[type="submit"]');
await page.waitForSelector('#password-section [role="status"]', { timeout: 5000 });
const successMsg = await page.locator('#password-section [role="status"]').textContent();
assert(
successMsg.includes('Password updated'),
`Shows success message (got: "${successMsg}")`
);
});

View file

@ -1,84 +0,0 @@
// tests/e2e/test_full_flow.js
// Full user journey: magic link registration -> set password -> logout -> login.
const { TARGET_URL, run } = require('./helpers');
run(async (page, assert) => {
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
assert(fixtures.register_token, 'Test fixtures loaded (register_token present)');
// ---- Step 1: Register via magic link ----
console.log('\n--- Magic link registration ---');
await page.goto(`${TARGET_URL}/register/${fixtures.register_token}`);
// Should redirect to /manage/credentials?setup=1
await page.waitForURL('**/manage/credentials?setup=1', { timeout: 5000 });
assert(
page.url().includes('/manage/credentials'),
`Redirected to credentials page (url: ${page.url()})`
);
// Should show welcome message
const welcome = page.locator('[role="status"]');
assert(await welcome.isVisible(), 'Welcome/setup message is visible');
const welcomeText = await welcome.textContent();
assert(
welcomeText.includes('Welcome'),
`Welcome message shown (got: "${welcomeText}")`
);
// Page title should be Porchlight
const title = await page.title();
assert(title.includes('Porchlight'), 'Credentials page title contains Porchlight');
// ---- Step 2: Set password ----
console.log('\n--- Set password ---');
const passwordInput = page.locator('#password');
const confirmInput = page.locator('#confirm');
assert(await passwordInput.isVisible(), 'Password input is visible');
assert(await confirmInput.isVisible(), 'Confirm password input is visible');
await passwordInput.fill('mypassword123');
await confirmInput.fill('mypassword123');
await page.click('#password-section button[type="submit"]');
// Wait for HTMX response — password-section innerHTML gets replaced
// The success message uses role="status"
const successMsg = page.locator('#password-section [role="status"]');
await successMsg.waitFor({ timeout: 5000 });
const successText = await successMsg.textContent();
assert(
successText.includes('Password updated'),
`Password set successfully (got: "${successText}")`
);
// ---- Step 3: Logout ----
console.log('\n--- Logout ---');
// POST /logout returns an HX-Redirect header, not a standard redirect.
// Use page.request to call it, then navigate manually.
await page.request.post(`${TARGET_URL}/logout`);
// Navigate to credentials — should redirect to login since we're logged out
await page.goto(`${TARGET_URL}/manage/credentials`);
await page.waitForURL('**/login', { timeout: 5000 });
assert(page.url().includes('/login'), 'Redirected to login after logout');
// ---- Step 4: Login with the password we just set ----
console.log('\n--- Login with new password ---');
await page.fill('#username', fixtures.register_username);
await page.fill('#password', 'mypassword123');
// Submit via HTMX — on success, HX-Redirect header triggers redirect
await page.click('form[hx-post="/login/password"] button[type="submit"]');
// Wait for redirect to credentials page
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
assert(
page.url().includes('/manage/credentials'),
`Login succeeded, redirected to credentials (url: ${page.url()})`
);
// Should NOT show setup message (no ?setup=1)
const setupMsgCount = await page.locator('[role="status"]:has-text("Welcome")').count();
assert(setupMsgCount === 0, 'No welcome/setup message on normal login');
});

View file

@ -1,13 +0,0 @@
// tests/e2e/test_health.js
// Smoke test: health endpoint returns OK.
const { TARGET_URL, run } = require('./helpers');
run(async (page, assert) => {
console.log('\n--- Health endpoint ---');
const resp = await page.request.get(`${TARGET_URL}/health`);
assert(resp.ok(), `Health endpoint returns 200 (status: ${resp.status()})`);
const body = await resp.json();
assert(body.status === 'ok', `Health response is {"status":"ok"} (got: ${JSON.stringify(body)})`);
});

View file

@ -1,119 +0,0 @@
// Porchlight — Login page e2e test
//
// Tests branding, page structure, accessibility, theme, and responsive layout.
//
const { TARGET_URL, run } = require('./helpers');
run(async (page, assert) => {
// ---- Branding ----
console.log('\n--- Branding ---');
await page.goto(`${TARGET_URL}/login`);
const title = await page.title();
assert(title.includes('Porchlight'), `Page title contains "Porchlight" (got: "${title}")`);
assert(!title.includes('FastAPI'), `Page title does not contain "FastAPI" (got: "${title}")`);
// ---- Favicon ----
console.log('\n--- Favicon ---');
const favicon = await page.locator('link[rel="icon"]').getAttribute('href');
assert(favicon === '/static/favicon.png', `Favicon link points to /static/favicon.png (got: "${favicon}")`);
const faviconResp = await page.request.get(`${TARGET_URL}/static/favicon.png`);
assert(faviconResp.ok(), `Favicon file is served (status: ${faviconResp.status()})`);
// ---- Site header & logo ----
console.log('\n--- Site header & logo ---');
const header = page.locator('.site-header');
assert(await header.isVisible(), 'Site header is visible');
const logo = page.locator('.site-logo');
assert(await logo.isVisible(), 'Logo image is visible');
const logoSrc = await logo.getAttribute('src');
assert(logoSrc === '/static/logo.svg', `Logo src is /static/logo.svg (got: "${logoSrc}")`);
const logoResp = await page.request.get(`${TARGET_URL}/static/logo.svg`);
assert(logoResp.ok(), `Logo SVG file is served (status: ${logoResp.status()})`);
const siteTitle = page.locator('.site-title');
assert(await siteTitle.isVisible(), 'Site title text is visible');
const siteTitleText = await siteTitle.textContent();
assert(siteTitleText.trim() === 'Porchlight', `Site title text is "Porchlight" (got: "${siteTitleText.trim()}")`);
// ---- Accessibility ----
console.log('\n--- Accessibility ---');
const skipLink = page.locator('.skip-link');
assert(await skipLink.count() === 1, 'Skip link is present');
const main = page.locator('main#main');
assert(await main.count() === 1, 'Main landmark with id="main" exists');
const liveRegion = page.locator('#live[aria-live="polite"]');
assert(await liveRegion.count() === 1, 'Polite live region exists');
// ---- Login form structure ----
console.log('\n--- Login form structure ---');
const h1 = page.locator('h1');
assert(await h1.textContent() === 'Sign in', `H1 says "Sign in" (got: "${await h1.textContent()}")`);
const passwordForm = page.locator('form[hx-post="/login/password"]');
assert(await passwordForm.count() === 1, 'Password login form exists');
const usernameInput = page.locator('#username');
assert(await usernameInput.isVisible(), 'Username input is visible');
const passwordInput = page.locator('#password');
assert(await passwordInput.isVisible(), 'Password input is visible');
const submitBtn = passwordForm.locator('button[type="submit"]');
assert(await submitBtn.isVisible(), 'Submit button is visible');
const webauthnForm = page.locator('#webauthn-login-form');
assert(await webauthnForm.count() === 1, 'WebAuthn login form exists');
// ---- Theme / styling ----
console.log('\n--- Theme / styling ---');
const bgColor = await page.evaluate(() => {
return getComputedStyle(document.body).backgroundColor;
});
assert(
bgColor === 'rgb(250, 250, 249)' || bgColor === 'rgb(28, 25, 23)',
`Body background is themed (got: "${bgColor}")`
);
const btnBg = await page.evaluate(() => {
const btn = document.querySelector('button[type="submit"]');
return getComputedStyle(btn).backgroundColor;
});
assert(
btnBg === 'rgb(217, 119, 6)' || btnBg === 'rgb(245, 158, 11)',
`Button uses amber accent color (got: "${btnBg}")`
);
// ---- Section card styling ----
console.log('\n--- Section cards ---');
const sectionBg = await page.evaluate(() => {
const section = document.querySelector('section');
return getComputedStyle(section).backgroundColor;
});
assert(
sectionBg === 'rgb(245, 245, 244)' || sectionBg === 'rgb(41, 37, 36)',
`Sections have surface background (got: "${sectionBg}")`
);
const sectionBorder = await page.evaluate(() => {
const section = document.querySelector('section');
return getComputedStyle(section).borderStyle;
});
assert(sectionBorder === 'solid', `Sections have solid border (got: "${sectionBorder}")`);
// ---- Static assets ----
console.log('\n--- Static assets ---');
const cssResp = await page.request.get(`${TARGET_URL}/static/style.css`);
assert(cssResp.ok(), `style.css is served (status: ${cssResp.status()})`);
const cssBody = await cssResp.text();
assert(cssBody.includes('--accent'), 'CSS contains --accent custom property');
assert(cssBody.includes('#d97706'), 'CSS contains amber accent color #d97706');
assert(cssBody.includes('prefers-color-scheme: dark'), 'CSS contains dark mode media query');
assert(cssBody.includes('prefers-reduced-motion'), 'CSS contains reduced motion media query');
});

View file

@ -1,63 +0,0 @@
// tests/e2e/test_password_auth.js
// Tests password login error states: wrong password, nonexistent user, form validation.
// Also tests successful login with seeded fixtures.
const { TARGET_URL, run } = require('./helpers');
run(async (page, assert) => {
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
// ---- Test: Nonexistent user ----
console.log('\n--- Login: nonexistent user ---');
await page.goto(`${TARGET_URL}/login`);
await page.fill('#username', 'nobody');
await page.fill('#password', 'whatever');
await page.click('form[hx-post="/login/password"] button[type="submit"]');
await page.waitForSelector('[role="alert"]', { timeout: 5000 });
const error1 = await page.locator('[role="alert"]').textContent();
assert(
error1.includes('Invalid username or password'),
`Error shown for nonexistent user (got: "${error1}")`
);
// ---- Test: Wrong password for existing user ----
console.log('\n--- Login: wrong password ---');
await page.goto(`${TARGET_URL}/login`);
await page.fill('#username', fixtures.login_username);
await page.fill('#password', 'wrongpassword');
await page.click('form[hx-post="/login/password"] button[type="submit"]');
await page.waitForSelector('[role="alert"]', { timeout: 5000 });
const error2 = await page.locator('[role="alert"]').textContent();
assert(
error2.includes('Invalid username or password'),
`Error shown for wrong password (got: "${error2}")`
);
// ---- Test: Successful login ----
console.log('\n--- Login: correct password ---');
await page.goto(`${TARGET_URL}/login`);
await page.fill('#username', fixtures.login_username);
await page.fill('#password', fixtures.login_password);
await page.click('form[hx-post="/login/password"] button[type="submit"]');
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
assert(
page.url().includes('/manage/credentials'),
`Successful login redirects to credentials (url: ${page.url()})`
);
// ---- Test: Form validation attributes ----
console.log('\n--- Form validation attributes ---');
await page.goto(`${TARGET_URL}/login`);
const usernameRequired = await page.locator('#username').getAttribute('required');
assert(usernameRequired !== null, 'Username has required attribute');
const passwordRequired = await page.locator('#password').getAttribute('required');
assert(passwordRequired !== null, 'Password has required attribute');
const usernameAutocomplete = await page.locator('#username').getAttribute('autocomplete');
assert(usernameAutocomplete === 'username', 'Username autocomplete is "username"');
const passwordAutocomplete = await page.locator('#password').getAttribute('autocomplete');
assert(passwordAutocomplete === 'current-password', 'Password autocomplete is "current-password"');
});

View file

@ -1,29 +0,0 @@
// tests/e2e/test_registration.js
// Tests magic link registration error states: invalid token, used token.
const { TARGET_URL, run } = require('./helpers');
run(async (page, assert) => {
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
// ---- Invalid token ----
console.log('\n--- Invalid registration token ---');
const resp1 = await page.goto(`${TARGET_URL}/register/invalid-token-12345`);
assert(resp1.status() === 400, `Invalid token returns 400 (got: ${resp1.status()})`);
const body1 = await page.locator('body').textContent();
assert(
body1.includes('Invalid or expired'),
`Shows invalid/expired message (got: "${body1.trim()}")`
);
// ---- Used token ----
console.log('\n--- Used registration token ---');
assert(fixtures.used_token, 'Used token fixture exists');
const resp2 = await page.goto(`${TARGET_URL}/register/${fixtures.used_token}`);
assert(resp2.status() === 400, `Used token returns 400 (got: ${resp2.status()})`);
const body2 = await page.locator('body').textContent();
assert(
body2.includes('Invalid or expired'),
`Shows invalid/expired for used token (got: "${body2.trim()}")`
);
});