feat(playwright-cli): add comprehensive test generation, tracing, and video recording documentation
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m29s
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m29s
- Introduced detailed documentation for test generation workflow in Playwright CLI, covering planning, generating, and healing tests. - Added tracing capabilities documentation, including usage, output files, and best practices for debugging and performance analysis. - Included video recording instructions, emphasizing best practices for capturing browser automation sessions with chapter markers and overlays. - Implemented user tutorial authentication setup and tutorial tests for parent mode in the E2E testing framework. - Created JSON files for user tutorial state management, ensuring isolated test environments.
This commit is contained in:
@@ -6,6 +6,17 @@ import { E2E_PIN } from '../../e2e-constants'
|
||||
const BACKEND = 'http://localhost:5000'
|
||||
const NEW_PIN = '5678'
|
||||
|
||||
async function openAccountSection(page: import('@playwright/test').Page): Promise<void> {
|
||||
const accountHeader = page
|
||||
.locator('.profile-section')
|
||||
.filter({ has: page.locator('.section-title', { hasText: 'Account' }) })
|
||||
.locator('.section-header')
|
||||
if ((await accountHeader.getAttribute('aria-expanded')) === 'false') {
|
||||
await accountHeader.click()
|
||||
}
|
||||
await page.locator('#section-account').waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async function setPinDirectly(request: APIRequestContext, pin: string): Promise<void> {
|
||||
// Request a new code, retrieve it via test endpoint, verify it, then set the pin
|
||||
await request.post(`${BACKEND}/user/request-pin-setup`)
|
||||
@@ -25,6 +36,7 @@ test.describe('User Profile – Change Parent PIN', () => {
|
||||
|
||||
test('Change Parent PIN link navigates to PIN setup page', async ({ page }) => {
|
||||
await page.goto('/parent/profile')
|
||||
await openAccountSection(page)
|
||||
|
||||
await page.getByRole('button', { name: 'Change Parent PIN' }).click()
|
||||
|
||||
@@ -35,6 +47,7 @@ test.describe('User Profile – Change Parent PIN', () => {
|
||||
test('Back from PIN setup page returns to profile', async ({ page }) => {
|
||||
// Navigate from the profile page so browser history exists
|
||||
await page.goto('/parent/profile')
|
||||
await openAccountSection(page)
|
||||
await page.getByRole('button', { name: 'Change Parent PIN' }).click()
|
||||
await expect(page).toHaveURL(/\/parent\/pin-setup/)
|
||||
|
||||
|
||||
@@ -3,11 +3,23 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { E2E_DELETE_EMAIL, E2E_DELETE_PASSWORD } from '../../e2e-constants'
|
||||
|
||||
async function openAccountSection(page: import('@playwright/test').Page): Promise<void> {
|
||||
const accountHeader = page
|
||||
.locator('.profile-section')
|
||||
.filter({ has: page.locator('.section-title', { hasText: 'Account' }) })
|
||||
.locator('.section-header')
|
||||
if ((await accountHeader.getAttribute('aria-expanded')) === 'false') {
|
||||
await accountHeader.click()
|
||||
}
|
||||
await page.locator('#section-account').waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
test.describe('User Profile – Delete Account', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test('Delete My Account opens confirmation dialog', async ({ page }) => {
|
||||
await page.goto('/parent/profile')
|
||||
await openAccountSection(page)
|
||||
|
||||
await page.getByRole('button', { name: 'Delete My Account' }).click()
|
||||
|
||||
@@ -24,6 +36,7 @@ test.describe('User Profile – Delete Account', () => {
|
||||
|
||||
test('Delete button stays disabled for incomplete email', async ({ page }) => {
|
||||
await page.goto('/parent/profile')
|
||||
await openAccountSection(page)
|
||||
await page.getByRole('button', { name: 'Delete My Account' }).click()
|
||||
await expect(page.locator('.modal-title', { hasText: 'Delete Your Account?' })).toBeVisible()
|
||||
|
||||
@@ -36,6 +49,7 @@ test.describe('User Profile – Delete Account', () => {
|
||||
|
||||
test('Delete button enables when matching email is entered', async ({ page }) => {
|
||||
await page.goto('/parent/profile')
|
||||
await openAccountSection(page)
|
||||
await page.getByRole('button', { name: 'Delete My Account' }).click()
|
||||
await expect(page.locator('.modal-title', { hasText: 'Delete Your Account?' })).toBeVisible()
|
||||
|
||||
@@ -48,6 +62,7 @@ test.describe('User Profile – Delete Account', () => {
|
||||
|
||||
test('Cancel closes the dialog without deleting the account', async ({ page }) => {
|
||||
await page.goto('/parent/profile')
|
||||
await openAccountSection(page)
|
||||
await page.getByRole('button', { name: 'Delete My Account' }).click()
|
||||
await expect(page.locator('.modal-title', { hasText: 'Delete Your Account?' })).toBeVisible()
|
||||
|
||||
@@ -57,12 +72,13 @@ test.describe('User Profile – Delete Account', () => {
|
||||
await expect(
|
||||
page.locator('.modal-title', { hasText: 'Delete Your Account?' }),
|
||||
).not.toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: 'User Profile' })).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible()
|
||||
await expect(page).toHaveURL(/\/parent\/profile/)
|
||||
})
|
||||
|
||||
test('Backdrop click does NOT close the delete dialog', async ({ page }) => {
|
||||
await page.goto('/parent/profile')
|
||||
await openAccountSection(page)
|
||||
await page.getByRole('button', { name: 'Delete My Account' }).click()
|
||||
await expect(page.locator('.modal-title', { hasText: 'Delete Your Account?' })).toBeVisible()
|
||||
|
||||
@@ -77,6 +93,7 @@ test.describe('User Profile – Delete Account', () => {
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/parent/profile')
|
||||
await openAccountSection(page)
|
||||
await page.getByRole('button', { name: 'Delete My Account' }).click()
|
||||
await expect(page.locator('.modal-title', { hasText: 'Delete Your Account?' })).toBeVisible()
|
||||
|
||||
|
||||
@@ -32,11 +32,27 @@ async function restoreProfile(request: APIRequestContext, profile: ProfileData):
|
||||
})
|
||||
}
|
||||
|
||||
/** Expand a collapsible profile section by its header title and wait for its content. */
|
||||
async function expandSection(page: import('@playwright/test').Page, title: string): Promise<void> {
|
||||
const header = page
|
||||
.locator('.profile-section')
|
||||
.filter({ has: page.locator('.section-title', { hasText: title }) })
|
||||
.locator('.section-header')
|
||||
const expanded = await header.getAttribute('aria-expanded').catch(() => 'false')
|
||||
if (expanded === 'false') {
|
||||
await header.click()
|
||||
}
|
||||
await page.locator(`#section-${title.toLowerCase()}`).waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
/** Navigate to /parent/profile and wait for the form to finish loading. */
|
||||
async function gotoProfile(page: import('@playwright/test').Page): Promise<void> {
|
||||
await page.goto('/parent/profile')
|
||||
// EntityEditForm hides the form behind v-if while loading=true; wait for it to render.
|
||||
await expect(page.getByLabel('First Name')).toBeVisible({ timeout: 10000 })
|
||||
// Expand sections that are collapsed by default so their fields/buttons are reachable.
|
||||
await expandSection(page, 'Account')
|
||||
await expandSection(page, 'Notifications')
|
||||
}
|
||||
|
||||
test.describe('User Profile – editing', () => {
|
||||
@@ -55,7 +71,7 @@ test.describe('User Profile – editing', () => {
|
||||
test('Profile page loads with correct data', async ({ page }) => {
|
||||
await gotoProfile(page)
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'User Profile' })).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: '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)
|
||||
@@ -69,82 +85,33 @@ test.describe('User Profile – editing', () => {
|
||||
await page.getByRole('menuitem', { name: 'Profile' }).click()
|
||||
await expect(page).toHaveURL('/parent/profile')
|
||||
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
// The profile view shows a header Back button (auto-save form has no Cancel).
|
||||
await page.getByRole('button', { name: 'Back' }).click()
|
||||
|
||||
await expect(page).toHaveURL('/parent')
|
||||
})
|
||||
|
||||
test('Save is disabled when form is clean (not dirty)', async ({ page }) => {
|
||||
await gotoProfile(page)
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
|
||||
})
|
||||
|
||||
test('Save is disabled when First Name is empty', async ({ page }) => {
|
||||
await gotoProfile(page)
|
||||
|
||||
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 gotoProfile(page)
|
||||
|
||||
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 gotoProfile(page)
|
||||
|
||||
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 gotoProfile(page)
|
||||
|
||||
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 }) => {
|
||||
test('Name changes auto-save on blur', async ({ page, request }) => {
|
||||
await gotoProfile(page)
|
||||
|
||||
await page.getByLabel('First Name').fill('UpdatedE2E')
|
||||
await page.getByLabel('Last Name').fill('UpdatedTester')
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await page.getByLabel('Last Name').blur()
|
||||
|
||||
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()
|
||||
// Wait for the auto-save PUT to complete and verify persistence via API.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const profile = await getProfile(request)
|
||||
return profile.first_name === 'UpdatedE2E' && profile.last_name === 'UpdatedTester'
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
// OK navigates back; go back to profile to verify persistence
|
||||
// Reloading the profile page shows the persisted values.
|
||||
await gotoProfile(page)
|
||||
await expect(page.getByLabel('First Name')).toHaveValue('UpdatedE2E')
|
||||
await expect(page.getByLabel('Last Name')).toHaveValue('UpdatedTester')
|
||||
})
|
||||
|
||||
test('Cancel discards unsaved changes', async ({ page }) => {
|
||||
await gotoProfile(page)
|
||||
|
||||
await page.getByLabel('First Name').fill('Discarded')
|
||||
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
|
||||
// Navigate back to verify no changes were saved
|
||||
await gotoProfile(page)
|
||||
await expect(page.getByLabel('First Name')).toHaveValue(E2E_FIRST_NAME)
|
||||
})
|
||||
|
||||
test('Email field is read-only', async ({ page }) => {
|
||||
await gotoProfile(page)
|
||||
|
||||
@@ -153,7 +120,7 @@ test.describe('User Profile – editing', () => {
|
||||
await expect(emailInput).toHaveValue(E2E_EMAIL)
|
||||
})
|
||||
|
||||
test('Change profile image (built-in)', async ({ page }) => {
|
||||
test('Change profile image (built-in)', async ({ page, request }) => {
|
||||
await gotoProfile(page)
|
||||
|
||||
// Wait for images to load
|
||||
@@ -180,24 +147,22 @@ test.describe('User Profile – editing', () => {
|
||||
// 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()
|
||||
// Images auto-save on selection; verify via API.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const profile = await getProfile(request)
|
||||
return Boolean(profile.image_id)
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
// Re-visit and confirm selection persists
|
||||
await gotoProfile(page)
|
||||
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 }) => {
|
||||
test('Upload a custom profile image', async ({ page, request }) => {
|
||||
await gotoProfile(page)
|
||||
|
||||
await page.waitForSelector('.selectable-image')
|
||||
@@ -210,14 +175,13 @@ test.describe('User Profile – editing', () => {
|
||||
// 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()
|
||||
// Images auto-save on upload; verify via API.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const profile = await getProfile(request)
|
||||
return Boolean(profile.image_id)
|
||||
})
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
test('Change Password shows email-sent modal', async ({ page }) => {
|
||||
@@ -243,6 +207,6 @@ test.describe('User Profile – editing', () => {
|
||||
await dialog.getByRole('button', { name: 'OK' }).click()
|
||||
|
||||
// Modal dismissed, back on the profile page
|
||||
await expect(page.getByRole('heading', { name: 'User Profile' })).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user