Add 7 new e2e tests verifying profile form validation in both manage and admin UIs: invalid phone number, phone normalization, E.164 hint attributes, and admin-side email/phone/picture URL validation errors. Fix 3 pre-existing test failures: - Replace invalid seeded phone number (+1234567890) with valid E.164 (+12025551234) that was causing profile update tests to fail - Update email validation error assertion to match actual pydantic message (value_error type uses raw message, not label-prefixed)
222 lines
9.4 KiB
JavaScript
222 lines
9.4 KiB
JavaScript
// @ts-check
|
|
const { test, expect } = require('@playwright/test');
|
|
|
|
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
|
|
|
|
test.describe('Profile page', () => {
|
|
test.describe('Auth guard', () => {
|
|
test('unauthenticated /manage/profile redirects to /login', async ({ page }) => {
|
|
await page.goto('/manage/profile');
|
|
await page.waitForURL('**/login', { timeout: 5000 });
|
|
expect(page.url()).toContain('/login');
|
|
});
|
|
});
|
|
|
|
test.describe('Page structure', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/login');
|
|
await page.fill('#username', fixtures.profile_username);
|
|
await page.fill('#password', fixtures.profile_password);
|
|
await page.click('form[hx-post="/login/password"] button[type="submit"]');
|
|
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
|
|
await page.goto('/manage/profile');
|
|
});
|
|
|
|
test('title contains Profile and Porchlight', async ({ page }) => {
|
|
await expect(page).toHaveTitle(/Profile/);
|
|
await expect(page).toHaveTitle(/Porchlight/);
|
|
});
|
|
|
|
test('H1 says "Profile"', async ({ page }) => {
|
|
await expect(page.locator('h1')).toHaveText('Profile');
|
|
});
|
|
|
|
test('shows username as read-only', async ({ page }) => {
|
|
await expect(page.locator('section:has(h2:has-text("Account"))')).toContainText('profileuser');
|
|
});
|
|
|
|
test('has all profile form fields', async ({ page }) => {
|
|
await expect(page.locator('#given_name')).toBeVisible();
|
|
await expect(page.locator('#family_name')).toBeVisible();
|
|
await expect(page.locator('#preferred_username')).toBeVisible();
|
|
await expect(page.locator('#email')).toBeVisible();
|
|
await expect(page.locator('#phone_number')).toBeVisible();
|
|
await expect(page.locator('#picture')).toBeVisible();
|
|
await expect(page.locator('#locale')).toBeVisible();
|
|
});
|
|
|
|
test('fields are pre-filled with existing data', async ({ page }) => {
|
|
await expect(page.locator('#given_name')).toHaveValue('Alice');
|
|
await expect(page.locator('#family_name')).toHaveValue('Smith');
|
|
await expect(page.locator('#preferred_username')).toHaveValue('asmith');
|
|
await expect(page.locator('#email')).toHaveValue('alice@example.com');
|
|
await expect(page.locator('#phone_number')).toHaveValue('+12025551234');
|
|
await expect(page.locator('#picture')).toHaveValue('https://example.com/alice.jpg');
|
|
await expect(page.locator('#locale')).toHaveValue('en');
|
|
});
|
|
|
|
test('has save button', async ({ page }) => {
|
|
await expect(page.locator('button[type="submit"]')).toHaveText('Save profile');
|
|
});
|
|
});
|
|
|
|
test.describe('Navigation', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/login');
|
|
await page.fill('#username', fixtures.profile_username);
|
|
await page.fill('#password', fixtures.profile_password);
|
|
await page.click('form[hx-post="/login/password"] button[type="submit"]');
|
|
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
|
|
});
|
|
|
|
test('manage nav is visible on credentials page', async ({ page }) => {
|
|
await expect(page.locator('nav.manage-nav')).toBeVisible();
|
|
});
|
|
|
|
test('credentials link is active on credentials page', async ({ page }) => {
|
|
const credLink = page.locator('nav.manage-nav a[href="/manage/credentials"]');
|
|
await expect(credLink).toHaveAttribute('aria-current', 'page');
|
|
});
|
|
|
|
test('can navigate from credentials to profile', async ({ page }) => {
|
|
await page.click('nav.manage-nav a[href="/manage/profile"]');
|
|
await page.waitForURL('**/manage/profile', { timeout: 5000 });
|
|
await expect(page.locator('h1')).toHaveText('Profile');
|
|
});
|
|
|
|
test('profile link is active on profile page', async ({ page }) => {
|
|
await page.goto('/manage/profile');
|
|
const profileLink = page.locator('nav.manage-nav a[href="/manage/profile"]');
|
|
await expect(profileLink).toHaveAttribute('aria-current', 'page');
|
|
});
|
|
|
|
test('can navigate from profile to credentials', async ({ page }) => {
|
|
await page.goto('/manage/profile');
|
|
await page.click('nav.manage-nav a[href="/manage/credentials"]');
|
|
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
|
|
await expect(page.locator('h1')).toHaveText('Credentials');
|
|
});
|
|
});
|
|
|
|
test.describe('Profile update', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/login');
|
|
await page.fill('#username', fixtures.profile_username);
|
|
await page.fill('#password', fixtures.profile_password);
|
|
await page.click('form[hx-post="/login/password"] button[type="submit"]');
|
|
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
|
|
await page.goto('/manage/profile');
|
|
});
|
|
|
|
test('successfully updates profile', async ({ page }) => {
|
|
await page.fill('#given_name', 'Bob');
|
|
await page.fill('#family_name', 'Jones');
|
|
await page.click('button[type="submit"]');
|
|
|
|
const status = page.locator('#profile-status [role="status"]');
|
|
await expect(status).toBeVisible({ timeout: 5000 });
|
|
await expect(status).toContainText('Profile updated');
|
|
});
|
|
|
|
test('updated values persist after reload', async ({ page }) => {
|
|
await page.fill('#given_name', 'Carol');
|
|
await page.fill('#family_name', 'Davis');
|
|
await page.fill('#preferred_username', 'cdavis');
|
|
await page.click('button[type="submit"]');
|
|
|
|
const status = page.locator('#profile-status [role="status"]');
|
|
await expect(status).toBeVisible({ timeout: 5000 });
|
|
|
|
await page.reload();
|
|
await expect(page.locator('#given_name')).toHaveValue('Carol');
|
|
await expect(page.locator('#family_name')).toHaveValue('Davis');
|
|
await expect(page.locator('#preferred_username')).toHaveValue('cdavis');
|
|
});
|
|
|
|
test('can clear optional fields', async ({ page }) => {
|
|
await page.fill('#phone_number', '');
|
|
await page.fill('#picture', '');
|
|
await page.fill('#locale', '');
|
|
await page.click('button[type="submit"]');
|
|
|
|
const status = page.locator('#profile-status [role="status"]');
|
|
await expect(status).toBeVisible({ timeout: 5000 });
|
|
await expect(status).toContainText('Profile updated');
|
|
|
|
await page.reload();
|
|
await expect(page.locator('#phone_number')).toHaveValue('');
|
|
await expect(page.locator('#picture')).toHaveValue('');
|
|
await expect(page.locator('#locale')).toHaveValue('');
|
|
});
|
|
});
|
|
|
|
test.describe('Validation', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/login');
|
|
await page.fill('#username', fixtures.profile_username);
|
|
await page.fill('#password', fixtures.profile_password);
|
|
await page.click('form[hx-post="/login/password"] button[type="submit"]');
|
|
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
|
|
await page.goto('/manage/profile');
|
|
});
|
|
|
|
test('shows error for invalid email', async ({ page }) => {
|
|
// Bypass HTML5 validation by removing type attribute
|
|
await page.locator('#email').evaluate(el => el.type = 'text');
|
|
await page.fill('#email', 'not-an-email');
|
|
await page.click('button[type="submit"]');
|
|
|
|
const alert = page.locator('#profile-status [role="alert"]');
|
|
await expect(alert).toBeVisible({ timeout: 5000 });
|
|
await expect(alert).toContainText('valid email address');
|
|
});
|
|
|
|
test('shows error for invalid picture URL', async ({ page }) => {
|
|
// Bypass HTML5 validation by removing type attribute
|
|
await page.locator('#picture').evaluate(el => el.type = 'text');
|
|
await page.fill('#picture', 'not-a-url');
|
|
await page.click('button[type="submit"]');
|
|
|
|
const alert = page.locator('#profile-status [role="alert"]');
|
|
await expect(alert).toBeVisible({ timeout: 5000 });
|
|
await expect(alert).toContainText('Picture URL');
|
|
});
|
|
|
|
test('email input has type="email"', async ({ page }) => {
|
|
await expect(page.locator('#email')).toHaveAttribute('type', 'email');
|
|
});
|
|
|
|
test('picture input has type="url"', async ({ page }) => {
|
|
await expect(page.locator('#picture')).toHaveAttribute('type', 'url');
|
|
});
|
|
|
|
test('shows error for invalid phone number', async ({ page }) => {
|
|
// Bypass HTML5 validation by removing pattern attribute
|
|
await page.locator('#phone_number').evaluate(el => el.removeAttribute('pattern'));
|
|
await page.fill('#phone_number', 'not-a-phone');
|
|
await page.click('button[type="submit"]');
|
|
|
|
const alert = page.locator('#profile-status [role="alert"]');
|
|
await expect(alert).toBeVisible({ timeout: 5000 });
|
|
await expect(alert).toContainText('valid phone number');
|
|
});
|
|
|
|
test('normalizes phone number with spaces on save', async ({ page }) => {
|
|
await page.fill('#phone_number', '+46 70 123 45 67');
|
|
await page.click('button[type="submit"]');
|
|
|
|
const status = page.locator('#profile-status [role="status"]');
|
|
await expect(status).toBeVisible({ timeout: 5000 });
|
|
await expect(status).toContainText('Profile updated');
|
|
|
|
await page.reload();
|
|
await expect(page.locator('#phone_number')).toHaveValue('+46701234567');
|
|
});
|
|
|
|
test('phone input has correct E.164 hint attributes', async ({ page }) => {
|
|
await expect(page.locator('#phone_number')).toHaveAttribute('type', 'tel');
|
|
await expect(page.locator('#phone_number')).toHaveAttribute('pattern', '\\+[0-9 ]+');
|
|
await expect(page.locator('#phone_number')).toHaveAttribute('title', /International format/);
|
|
});
|
|
});
|
|
});
|