porchlight/tests/e2e/credentials.spec.js
Johan Lundberg bcfe3a2a15
fix: keep password form visible on validation error
The password setup/change form used hx-target="#password-section" with
hx-swap="innerHTML", but that div wraps the form itself. On a validation
error the route returns only an alert div, so the swap replaced the entire
form — the password inputs disappeared. Most visible during registration's
"set password" step.

Retarget the form to a dedicated #password-error div outside the form
(mirrors the working login form's #login-error pattern), so the form and
its inputs survive errors while messages still render inside #password-section.

Also fix pre-existing broken e2e tests: they omitted the required
current_password fill and used passwords below the zxcvbn strength
threshold (score 1 < MIN_PASSWORD_STRENGTH=2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:22:01 +02:00

93 lines
3.6 KiB
JavaScript

// @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('#current_password', fixtures.cred_password);
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('keeps the password form visible after a validation error', async ({ page }) => {
await page.fill('#current_password', fixtures.cred_password);
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 });
// Regression: the form and its inputs must NOT disappear on error.
await expect(page.locator('#password')).toBeVisible();
await expect(page.locator('#confirm')).toBeVisible();
await expect(
page.locator('#password-section button[type="submit"]'),
).toBeVisible();
});
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('#current_password', fixtures.cred_password);
await page.fill('#password', 'purple-tiger-mountain-42');
await page.fill('#confirm', 'purple-tiger-mountain-42');
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');
});
});
});