diff --git a/tests/e2e/admin.spec.js b/tests/e2e/admin.spec.js index 24971a7..f00c4d8 100644 --- a/tests/e2e/admin.spec.js +++ b/tests/e2e/admin.spec.js @@ -129,7 +129,67 @@ test.describe('Admin pages', () => { }); // --------------------------------------------------------------- - // 8. Groups update + // 8. Profile validation + // --------------------------------------------------------------- + test.describe('Profile validation', () => { + test('invalid email shows error', async ({ page }) => { + await loginAsAdmin(page); + await page.goto(`/admin/users/${fixtures.admin_userid}`); + + // Bypass HTML5 email validation so the form can submit + await page.locator('#email').evaluate(el => el.type = 'text'); + await page.fill('#email', 'not-an-email'); + + await page.click('section:has(h2:has-text("Profile")) 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('invalid phone number shows error', async ({ page }) => { + await loginAsAdmin(page); + await page.goto(`/admin/users/${fixtures.admin_userid}`); + + // Bypass HTML5 pattern validation so the form can submit + await page.locator('#phone_number').evaluate(el => el.removeAttribute('pattern')); + await page.fill('#phone_number', 'not-a-phone'); + + await page.click('section:has(h2:has-text("Profile")) 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('invalid picture URL shows error', async ({ page }) => { + await loginAsAdmin(page); + await page.goto(`/admin/users/${fixtures.admin_userid}`); + + // Bypass HTML5 URL validation so the form can submit + await page.locator('#picture').evaluate(el => el.type = 'text'); + await page.fill('#picture', 'not-a-url'); + + await page.click('section:has(h2:has-text("Profile")) button[type="submit"]'); + + const alert = page.locator('#profile-status [role="alert"]'); + await expect(alert).toBeVisible({ timeout: 5000 }); + await expect(alert).toContainText('Picture URL'); + }); + + test('phone input has E.164 hint attributes', async ({ page }) => { + await loginAsAdmin(page); + await page.goto(`/admin/users/${fixtures.admin_userid}`); + + const phoneInput = page.locator('#phone_number'); + await expect(phoneInput).toHaveAttribute('type', 'tel'); + await expect(phoneInput).toHaveAttribute('pattern', '\\+[0-9 ]+'); + await expect(phoneInput).toHaveAttribute('title', /International format/); + }); + }); + + // --------------------------------------------------------------- + // 9. Groups update // --------------------------------------------------------------- test.describe('Groups update', () => { test('change groups and save', async ({ page }) => { @@ -146,7 +206,7 @@ test.describe('Admin pages', () => { }); // --------------------------------------------------------------- - // 9. Activate/deactivate toggle + // 10. Activate/deactivate toggle // --------------------------------------------------------------- test.describe('Activate/deactivate toggle', () => { test('deactivate then activate user', async ({ page }) => { @@ -175,7 +235,7 @@ test.describe('Admin pages', () => { }); // --------------------------------------------------------------- - // 10. Create invite from user list + // 11. Create invite from user list // --------------------------------------------------------------- test.describe('Create invite', () => { test('fill username and submit to get invite URL', async ({ page }) => { @@ -193,7 +253,7 @@ test.describe('Admin pages', () => { }); // --------------------------------------------------------------- - // 11. Re-invite from user detail + // 12. Re-invite from user detail // --------------------------------------------------------------- test.describe('Re-invite from user detail', () => { test('generate invite link from user detail page', async ({ page }) => { @@ -210,7 +270,7 @@ test.describe('Admin pages', () => { }); // --------------------------------------------------------------- - // 12. Delete user + // 13. Delete user // --------------------------------------------------------------- test.describe('Delete user', () => { test('delete a user and verify redirect to user list', async ({ page }) => { diff --git a/tests/e2e/profile.spec.js b/tests/e2e/profile.spec.js index 998d59d..6e0d49d 100644 --- a/tests/e2e/profile.spec.js +++ b/tests/e2e/profile.spec.js @@ -50,7 +50,7 @@ test.describe('Profile page', () => { 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('+1234567890'); + 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'); }); @@ -168,7 +168,7 @@ test.describe('Profile page', () => { const alert = page.locator('#profile-status [role="alert"]'); await expect(alert).toBeVisible({ timeout: 5000 }); - await expect(alert).toContainText('Invalid email'); + await expect(alert).toContainText('valid email address'); }); test('shows error for invalid picture URL', async ({ page }) => { @@ -189,5 +189,34 @@ test.describe('Profile page', () => { 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/); + }); }); }); diff --git a/tests/e2e/setup_db.py b/tests/e2e/setup_db.py index bce3821..bb43e8e 100644 --- a/tests/e2e/setup_db.py +++ b/tests/e2e/setup_db.py @@ -87,7 +87,7 @@ async def seed() -> None: family_name="Smith", preferred_username="asmith", email="alice@example.com", - phone_number="+1234567890", + phone_number="+12025551234", picture="https://example.com/alice.jpg", locale="en", groups=["users"],