Files
chore/frontend/e2e/mode_parent/user-profile/profile-editing.spec.ts
Ryan Kegel 127378797c
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m46s
Refactored frontend directory
2026-04-25 00:40:15 -04:00

242 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<ProfileData> {
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<void> {
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')
// Navigate in-app via the profile dropdown so Vue Router has a real history entry.
await page.getByRole('button', { name: 'Parent menu' }).click()
await page.getByRole('menuitem', { name: 'Profile' }).click()
await expect(page).toHaveURL('/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 (title can transition quickly from loading to success)
const dialog = page.locator('.modal-dialog')
await expect(dialog).toBeVisible()
await expect(dialog.locator('.modal-title')).toContainText(
/Change Password|Password Change Email Sent|Password Change Failed/,
)
// 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()
})
})