Files
chore/frontend/vue-app/e2e/mode_parent/user-profile/profile-editing.spec.ts
Ryan Kegel db6e0a7ce8
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m38s
feat: Implement user profile and parent profile button tests
- Added tests for user profile editing, including name changes, image uploads, and password changes.
- Implemented tests for changing the parent PIN with verification code handling.
- Created tests for account deletion with confirmation dialog and email validation.
- Introduced parent profile button tests for both temporary and permanent modes, verifying badge visibility and menu options.
- Updated Playwright configuration to include new test buckets for user profile and parent profile button scenarios.
- Added e2e plans documentation for user profile and parent profile button tests.
2026-03-18 17:20:31 -04:00

236 lines
8.1 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')
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()
})
})