// spec: e2e/plans/user-profile.plan.md import { test, expect, type APIRequestContext } from '@playwright/test' import path from 'path' import { fileURLToPath } from 'url' import { E2E_EMAIL, E2E_FIRST_NAME } from '../../e2e-constants' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const TEST_IMAGE = path.join(__dirname, '../../.resources/crown.png') const BACKEND = 'http://localhost:5000' interface ProfileData { first_name: string last_name: string image_id: string | null } async function getProfile(request: APIRequestContext): Promise { const res = await request.get(`${BACKEND}/user/profile`) const data = await res.json() return { first_name: data.first_name, last_name: data.last_name, image_id: data.image_id ?? null } } async function restoreProfile(request: APIRequestContext, profile: ProfileData): Promise { await request.put(`${BACKEND}/user/profile`, { data: { first_name: profile.first_name, last_name: profile.last_name, image_id: profile.image_id, }, }) } test.describe('User Profile – editing', () => { test.describe.configure({ mode: 'serial' }) let originalProfile: ProfileData test.beforeEach(async ({ request }) => { originalProfile = await getProfile(request) }) test.afterEach(async ({ request }) => { await restoreProfile(request, originalProfile) }) test('Profile page loads with correct data', async ({ page }) => { await page.goto('/parent/profile') await expect(page.getByRole('heading', { name: 'User Profile' })).toBeVisible() await expect(page.getByLabel('First Name')).toHaveValue(E2E_FIRST_NAME) await expect(page.getByLabel('Last Name')).toHaveValue('Tester') await expect(page.getByLabel('Email Address')).toHaveValue(E2E_EMAIL) await expect(page.getByLabel('Email Address')).toBeDisabled() }) test('Back button navigates back', async ({ page }) => { await page.goto('/parent') await page.goto('/parent/profile') await page.getByRole('button', { name: 'Cancel' }).click() await expect(page).toHaveURL('/parent') }) test('Save is disabled when form is clean (not dirty)', async ({ page }) => { await page.goto('/parent/profile') await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled() }) test('Save is disabled when First Name is empty', async ({ page }) => { await page.goto('/parent/profile') await page.getByLabel('First Name').fill('') await page.getByLabel('First Name').blur() await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled() }) test('Save is disabled when Last Name is empty', async ({ page }) => { await page.goto('/parent/profile') await page.getByLabel('Last Name').fill('') await page.getByLabel('Last Name').blur() await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled() }) test('Save is disabled when both name fields are empty', async ({ page }) => { await page.goto('/parent/profile') await page.getByLabel('First Name').fill('') await page.getByLabel('Last Name').fill('') await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled() }) test('Save enables when a name is changed', async ({ page }) => { await page.goto('/parent/profile') await page.getByLabel('First Name').fill('UpdatedE2E') await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled() }) test('Save persists name changes and shows confirmation modal', async ({ page }) => { await page.goto('/parent/profile') await page.getByLabel('First Name').fill('UpdatedE2E') await page.getByLabel('Last Name').fill('UpdatedTester') await page.getByRole('button', { name: 'Save' }).click() const dialog = page.locator('.modal-dialog') await expect(dialog.locator('.modal-title', { hasText: 'Profile Updated' })).toBeVisible() await expect(dialog.getByText('Your profile was updated successfully.')).toBeVisible() await dialog.getByRole('button', { name: 'OK' }).click() // OK navigates back; go back to profile to verify persistence await page.goto('/parent/profile') await expect(page.getByLabel('First Name')).toHaveValue('UpdatedE2E') await expect(page.getByLabel('Last Name')).toHaveValue('UpdatedTester') }) test('Cancel discards unsaved changes', async ({ page }) => { await page.goto('/parent/profile') await page.getByLabel('First Name').fill('Discarded') await page.getByRole('button', { name: 'Cancel' }).click() // Navigate back to verify no changes were saved await page.goto('/parent/profile') await expect(page.getByLabel('First Name')).toHaveValue(E2E_FIRST_NAME) }) test('Email field is read-only', async ({ page }) => { await page.goto('/parent/profile') const emailInput = page.locator('#email') await expect(emailInput).toBeDisabled() await expect(emailInput).toHaveValue(E2E_EMAIL) }) test('Change profile image (built-in)', async ({ page }) => { await page.goto('/parent/profile') // Wait for images to load await page.waitForSelector('.selectable-image') // Pick the second image in the list (index 1), regardless of which is selected const images = page.locator('.selectable-image') const count = await images.count() expect(count).toBeGreaterThan(1) // Find first image that is NOT currently selected let targetIndex = -1 for (let i = 0; i < count; i++) { const cls = await images.nth(i).getAttribute('class') if (!cls?.includes('selected')) { targetIndex = i break } } expect(targetIndex).toBeGreaterThanOrEqual(0) await images.nth(targetIndex).click() // Confirm it is now selected await expect(images.nth(targetIndex)).toHaveClass(/selected/) // Save await page.getByRole('button', { name: 'Save' }).click() await expect( page.locator('.modal-dialog .modal-title', { hasText: 'Profile Updated' }), ).toBeVisible() await page.locator('.modal-dialog').getByRole('button', { name: 'OK' }).click() // Re-visit and confirm selection persists await page.goto('/parent/profile') await page.waitForSelector('.selectable-image') const selectedSrc = await page.locator('.selectable-image.selected').getAttribute('src') expect(selectedSrc).toBeTruthy() // The URL will differ after a new load (Object URL vs cached), so verify via API const profile = await getProfile(page.request) expect(profile.image_id).toBeTruthy() }) test('Upload a custom profile image', async ({ page }) => { await page.goto('/parent/profile') await page.waitForSelector('.selectable-image') const fileChooserPromise = page.waitForEvent('filechooser') await page.getByRole('button', { name: 'Add from device' }).click() const fileChooser = await fileChooserPromise await fileChooser.setFiles(TEST_IMAGE) // The uploaded image appears first in the list and is selected await expect(page.locator('.selectable-image').first()).toHaveClass(/selected/) // Save is now enabled await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled() await page.getByRole('button', { name: 'Save' }).click() await expect( page.locator('.modal-dialog .modal-title', { hasText: 'Profile Updated' }), ).toBeVisible() await page.locator('.modal-dialog').getByRole('button', { name: 'OK' }).click() }) test('Change Password shows email-sent modal', async ({ page }) => { await page.goto('/parent/profile') await page.getByRole('button', { name: 'Change Password' }).click() // Modal appears const dialog = page.locator('.modal-dialog') await expect(dialog.locator('.modal-title', { hasText: 'Change Password' })).toBeVisible() // Eventually becomes the confirmed state await expect( dialog.locator('.modal-title', { hasText: 'Password Change Email Sent' }), ).toBeVisible({ timeout: 10000, }) await expect(dialog.locator('.modal-message')).toContainText(/password change link/i) await dialog.getByRole('button', { name: 'OK' }).click() // Modal dismissed, back on the profile page await expect(page.getByRole('heading', { name: 'User Profile' })).toBeVisible() }) })