// @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('+1234567890'); 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('Invalid email'); }); 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'); }); }); });