All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m38s
- 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.
236 lines
8.1 KiB
TypeScript
236 lines
8.1 KiB
TypeScript
// 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()
|
||
})
|
||
})
|