temp changes
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m59s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m59s
This commit is contained in:
35
frontend/vue-app/e2e/.auth/user-no-pin.json
Normal file
35
frontend/vue-app/e2e/.auth/user-no-pin.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"cookies": [
|
||||
{
|
||||
"name": "refresh_token",
|
||||
"value": "exz9voXnacTUkQGnKkc2QHLZA1DB3-7neit29Gtan5w",
|
||||
"domain": "localhost",
|
||||
"path": "/api/auth",
|
||||
"expires": 1780801137.642288,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Strict"
|
||||
},
|
||||
{
|
||||
"name": "access_token",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI2NmQ5Yzk0NC05MzFmLTQyODktOWYxZS1kNzZhODQyZTM0MzIiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzMwMjYwMzd9.gjcizOIYTbdX6B-AobROaoJtMczY-7EnoyUco-b-xE8",
|
||||
"domain": "localhost",
|
||||
"path": "/",
|
||||
"expires": -1,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Strict"
|
||||
}
|
||||
],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "https://localhost:5173",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "authSyncEvent",
|
||||
"value": "{\"type\":\"logout\",\"at\":1773025137442}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
35
frontend/vue-app/e2e/.auth/user.json
Normal file
35
frontend/vue-app/e2e/.auth/user.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"cookies": [
|
||||
{
|
||||
"name": "refresh_token",
|
||||
"value": "aQ7Hdjmxefq4F6nLro-Sz0d2qO_3XN3v_tO4ioHOH6w",
|
||||
"domain": "localhost",
|
||||
"path": "/api/auth",
|
||||
"expires": 1780799347.476442,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Strict"
|
||||
},
|
||||
{
|
||||
"name": "access_token",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMzg1YWRjNC0yYmI4LTQyMTktOGFiNi05ZjkxNzAyNzA0MjEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzMwMjQyNDd9.g3audbmZ_S-Bc5ZfgwvpfoQuJEjCS2vd3dF8baExFEA",
|
||||
"domain": "localhost",
|
||||
"path": "/",
|
||||
"expires": -1,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Strict"
|
||||
}
|
||||
],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "https://localhost:5173",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "authSyncEvent",
|
||||
"value": "{\"type\":\"parent_logout\",\"at\":1773023350687}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
18
frontend/vue-app/e2e/auth-no-pin.setup.ts
Normal file
18
frontend/vue-app/e2e/auth-no-pin.setup.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { test as setup } from '@playwright/test'
|
||||
import { STORAGE_STATE_NO_PIN, E2E_EMAIL, E2E_PASSWORD } from './e2e-constants'
|
||||
|
||||
setup('authenticate without parent pin', async ({ page }) => {
|
||||
await page.goto('/auth/login')
|
||||
|
||||
await page.getByLabel('Email address').fill(E2E_EMAIL)
|
||||
await page.getByLabel('Password').fill(E2E_PASSWORD)
|
||||
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||
|
||||
// Wait for redirect to the authenticated area
|
||||
await page.waitForURL(/\/(parent|child)/)
|
||||
|
||||
// Remove parent auth from localStorage so the PIN prompt appears
|
||||
await page.evaluate(() => localStorage.removeItem('parentAuth'))
|
||||
|
||||
await page.context().storageState({ path: STORAGE_STATE_NO_PIN })
|
||||
})
|
||||
43
frontend/vue-app/e2e/auth.setup.ts
Normal file
43
frontend/vue-app/e2e/auth.setup.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { test as setup } from '@playwright/test'
|
||||
import { STORAGE_STATE, E2E_EMAIL, E2E_PASSWORD, E2E_PIN } from './e2e-constants'
|
||||
|
||||
setup('authenticate', async ({ page }) => {
|
||||
// Seed backend test data
|
||||
const backendUrl = 'http://localhost:5000'
|
||||
const seedRes = await page.request.post(`${backendUrl}/auth/e2e-seed`)
|
||||
if (!seedRes.ok()) {
|
||||
throw new Error(`e2e-seed failed: ${seedRes.status()} ${await seedRes.text()}`)
|
||||
}
|
||||
|
||||
await page.goto('/auth/login')
|
||||
await page.getByLabel('Email address').fill(E2E_EMAIL)
|
||||
await page.getByLabel('Password').fill(E2E_PASSWORD)
|
||||
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||
|
||||
// After login the router redirects to /child (not parent-authenticated yet)
|
||||
await page.waitForURL(/\/(parent|child)/)
|
||||
|
||||
// Click the LoginButton in the header to open the PIN modal
|
||||
await page.getByRole('button', { name: 'Parent login' }).click()
|
||||
|
||||
// Fill in the PIN and submit
|
||||
const pinInput = page.getByLabel('PIN').or(page.getByPlaceholder('Enter PIN'))
|
||||
await pinInput.waitFor({ timeout: 5000 })
|
||||
await page.screenshot({ path: 'auth-setup-before-pin.png' })
|
||||
await pinInput.fill(E2E_PIN)
|
||||
await page.screenshot({ path: 'auth-setup-after-pin.png' })
|
||||
await page.getByRole('button', { name: 'Verify' }).click()
|
||||
|
||||
// LoginButton does router.push('/parent') after PIN - wait for it
|
||||
await page.waitForURL(/\/parent(\/|$)/)
|
||||
|
||||
// Confirm parent mode is active by waiting for the Add Child FAB at /parent
|
||||
try {
|
||||
await page.getByRole('button', { name: 'Add Child' }).waitFor({ timeout: 5000 })
|
||||
} catch (e) {
|
||||
await page.screenshot({ path: 'auth-setup-parent-fail.png' })
|
||||
throw new Error('Parent mode not reached after PIN entry. See auth-setup-parent-fail.png for details.')
|
||||
}
|
||||
|
||||
await page.context().storageState({ path: STORAGE_STATE })
|
||||
})
|
||||
16
frontend/vue-app/e2e/create-child/authorization.spec.ts
Normal file
16
frontend/vue-app/e2e/create-child/authorization.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// spec: e2e/plans/create-child.plan.md
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { STORAGE_STATE_NO_PIN } from '../e2e-constants'
|
||||
|
||||
test.use({ storageState: STORAGE_STATE_NO_PIN })
|
||||
|
||||
test.describe('Create Child', () => {
|
||||
test('Add Child FAB is hidden when parent auth is expired', async ({ page }) => {
|
||||
// Navigate to app root - with no parent auth, router redirects to /child
|
||||
await page.goto('/')
|
||||
|
||||
// expect: the 'Add Child' FAB is NOT visible (not in parent mode)
|
||||
await expect(page.getByRole('button', { name: 'Add Child' })).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
104
frontend/vue-app/e2e/create-child/happy-path.spec.ts
Normal file
104
frontend/vue-app/e2e/create-child/happy-path.spec.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
// spec: e2e/plans/create-child.plan.md
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const TEST_IMAGE = path.join(__dirname, '../../../../resources/logo/star_only.png')
|
||||
|
||||
async function deleteNamedChildren(request: any, names: string[]) {
|
||||
const res = await request.get('/api/child/list')
|
||||
const data = await res.json()
|
||||
for (const child of data.children ?? []) {
|
||||
if (names.includes(child.name)) {
|
||||
await request.delete(`/api/child/${child.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAllChildren(request: any) {
|
||||
const res = await request.get('/api/child/list')
|
||||
const data = await res.json()
|
||||
for (const child of data.children ?? []) {
|
||||
await request.delete(`/api/child/${child.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Create Child', () => {
|
||||
test('Create a child with name and age only', async ({ page, request }) => {
|
||||
await deleteNamedChildren(request, ['Alice'])
|
||||
|
||||
// 1. Navigate to app root - router redirects to /parent (children list) when parent-authenticated
|
||||
await page.goto('/')
|
||||
await expect(page).toHaveURL('/parent')
|
||||
|
||||
// 2. Click the 'Add Child' FAB
|
||||
await page.getByRole('button', { name: 'Add Child' }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
|
||||
await expect(page.getByLabel('Name')).toBeVisible()
|
||||
await expect(page.getByLabel('Age')).toBeVisible()
|
||||
|
||||
// 3. Enter 'Alice' in the Name field
|
||||
await page.getByLabel('Name').fill('Alice')
|
||||
await expect(page.getByLabel('Name')).toHaveValue('Alice')
|
||||
|
||||
// 4. Enter '8' in the Age field
|
||||
await page.getByLabel('Age').fill('8')
|
||||
await expect(page.getByLabel('Age')).toHaveValue('8')
|
||||
|
||||
// 5. Leave Image as default and click Create
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.locator('.error')).not.toBeVisible()
|
||||
await expect(page).toHaveURL('/parent')
|
||||
await expect(page.getByText('Alice')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Create a child via the inline Create button in empty state', async ({ page, request }) => {
|
||||
await deleteAllChildren(request)
|
||||
|
||||
// 1. Navigate to app root - router redirects to /parent (children list)
|
||||
await page.goto('/')
|
||||
await expect(page.getByText('No children')).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Create' })).toBeVisible()
|
||||
|
||||
// 2. Click the inline 'Create' button
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
|
||||
|
||||
// 3. Enter 'Bob' and '10', then submit
|
||||
await page.getByLabel('Name').fill('Bob')
|
||||
await page.getByLabel('Age').fill('10')
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.locator('.error')).not.toBeVisible()
|
||||
await expect(page).toHaveURL('/parent')
|
||||
await expect(page.getByText('Bob')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Create a child with a custom uploaded image', async ({ page, request }) => {
|
||||
await deleteNamedChildren(request, ['Grace'])
|
||||
|
||||
// 1. Navigate to app root - router redirects to /parent (children list)
|
||||
await page.goto('/')
|
||||
await page.getByRole('button', { name: 'Add Child' }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
|
||||
|
||||
// 2. Enter 'Grace' and '6'
|
||||
await page.getByLabel('Name').fill('Grace')
|
||||
await page.getByLabel('Age').fill('6')
|
||||
await expect(page.getByLabel('Name')).toHaveValue('Grace')
|
||||
await expect(page.getByLabel('Age')).toHaveValue('6')
|
||||
|
||||
// 3. Upload a local image file via 'Add from device'
|
||||
const fileChooserPromise = page.waitForEvent('filechooser')
|
||||
await page.getByRole('button', { name: 'Add from device' }).click()
|
||||
const fileChooser = await fileChooserPromise
|
||||
await fileChooser.setFiles(TEST_IMAGE)
|
||||
|
||||
// 4. Submit the form
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.locator('.error')).not.toBeVisible()
|
||||
await expect(page).toHaveURL('/parent')
|
||||
await expect(page.getByText('Grace')).toBeVisible()
|
||||
})
|
||||
})
|
||||
21
frontend/vue-app/e2e/create-child/navigation.spec.ts
Normal file
21
frontend/vue-app/e2e/create-child/navigation.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// spec: e2e/plans/create-child.plan.md
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Create Child', () => {
|
||||
test('Cancel navigates back without saving', async ({ page }) => {
|
||||
// 1. Navigate to app root - router redirects to /parent (children list)
|
||||
await page.goto('/')
|
||||
await page.getByRole('button', { name: 'Add Child' }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
|
||||
|
||||
// 2. Fill in 'Frank' and '9', then click Cancel
|
||||
await page.getByLabel('Name').fill('Frank')
|
||||
await page.getByLabel('Age').fill('9')
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
|
||||
// expect: back on /parent and 'Frank' is NOT listed
|
||||
await expect(page).toHaveURL('/parent')
|
||||
await expect(page.getByText('Frank')).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
37
frontend/vue-app/e2e/create-child/sse.spec.ts
Normal file
37
frontend/vue-app/e2e/create-child/sse.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// spec: e2e/plans/create-child.plan.md
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Create Child', () => {
|
||||
test('New child appears in list without page reload', async ({ page, context, request }) => {
|
||||
// Clean up 'Hannah' before test
|
||||
const res = await request.get('/api/child/list')
|
||||
const data = await res.json()
|
||||
for (const child of data.children ?? []) {
|
||||
if (child.name === 'Hannah') {
|
||||
await request.delete(`/api/child/${child.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Open two browser tabs both on /parent (children list)
|
||||
await page.goto('/')
|
||||
await expect(page).toHaveURL('/parent')
|
||||
|
||||
const tab2 = await context.newPage()
|
||||
await tab2.goto('/')
|
||||
await expect(tab2).toHaveURL('/parent')
|
||||
|
||||
// 2. In Tab 1, create child 'Hannah' age '4'
|
||||
await page.getByRole('button', { name: 'Add Child' }).click()
|
||||
await page.getByLabel('Name').fill('Hannah')
|
||||
await page.getByLabel('Age').fill('4')
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page).toHaveURL('/parent')
|
||||
await expect(page.getByText('Hannah')).toBeVisible()
|
||||
|
||||
// 3. Tab 2 should show 'Hannah' via SSE without a manual refresh
|
||||
await expect(tab2.getByText('Hannah')).toBeVisible()
|
||||
|
||||
await tab2.close()
|
||||
})
|
||||
})
|
||||
74
frontend/vue-app/e2e/create-child/validation.spec.ts
Normal file
74
frontend/vue-app/e2e/create-child/validation.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// spec: e2e/plans/create-child.plan.md
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Create Child', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to app root - router redirects to /parent (children list) when parent-authenticated
|
||||
await page.goto('/')
|
||||
await page.getByRole('button', { name: 'Add Child' }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('Reject submission when Name is empty', async ({ page }) => {
|
||||
// 2. Leave Name empty, enter '7' in Age, click Create
|
||||
await page.getByLabel('Age').fill('7')
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
|
||||
// expect: error message and still on create form
|
||||
await expect(page.locator('.error')).toHaveText('Child name is required.')
|
||||
await expect(page).toHaveURL('/parent/children/create')
|
||||
})
|
||||
|
||||
test('Reject submission when Name is whitespace only', async ({ page }) => {
|
||||
// 2. Enter only spaces in Name, enter '7' in Age, click Create
|
||||
await page.getByLabel('Name').fill(' ')
|
||||
await page.getByLabel('Age').fill('7')
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
|
||||
// expect: error message and still on create form
|
||||
await expect(page.locator('.error')).toHaveText('Child name is required.')
|
||||
await expect(page).toHaveURL('/parent/children/create')
|
||||
})
|
||||
|
||||
test('Reject submission when Age is empty', async ({ page }) => {
|
||||
// 2. Enter 'Charlie', clear Age - Create button should be disabled
|
||||
await page.getByLabel('Name').fill('Charlie')
|
||||
await page.getByLabel('Age').clear()
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Create' })).toBeDisabled()
|
||||
await expect(page).toHaveURL('/parent/children/create')
|
||||
})
|
||||
|
||||
test('Reject negative age', async ({ page }) => {
|
||||
// 2. Enter 'Dave', enter '-1', click Create
|
||||
await page.getByLabel('Name').fill('Dave')
|
||||
await page.getByLabel('Age').fill('-1')
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
|
||||
// expect: error message and still on create form
|
||||
await expect(page.locator('.error')).toHaveText('Age must be a non-negative number.')
|
||||
await expect(page).toHaveURL('/parent/children/create')
|
||||
})
|
||||
|
||||
test('Enforce maximum Name length of 64 characters', async ({ page }) => {
|
||||
// 2. Type a 65-character name - HTML maxlength caps it at 64
|
||||
const longName = 'A'.repeat(65)
|
||||
await page.getByLabel('Name').fill(longName)
|
||||
await expect(page.getByLabel('Name')).toHaveValue('A'.repeat(64))
|
||||
|
||||
// 3. Enter '5' in Age and submit successfully
|
||||
await page.getByLabel('Age').fill('5')
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page).toHaveURL('/parent')
|
||||
})
|
||||
|
||||
test('Reject age greater than 120', async ({ page }) => {
|
||||
// 2. Enter 'Eve', enter '121' in Age - Create button should be disabled
|
||||
await page.getByLabel('Name').fill('Eve')
|
||||
await page.getByLabel('Age').fill('121')
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Create' })).toBeDisabled()
|
||||
await expect(page).toHaveURL('/parent/children/create')
|
||||
})
|
||||
})
|
||||
5
frontend/vue-app/e2e/e2e-constants.ts
Normal file
5
frontend/vue-app/e2e/e2e-constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const STORAGE_STATE = 'e2e/.auth/user.json'
|
||||
export const STORAGE_STATE_NO_PIN = 'e2e/.auth/user-no-pin.json'
|
||||
export const E2E_EMAIL = 'e2e@test.com'
|
||||
export const E2E_PASSWORD = 'E2eTestPass1!'
|
||||
export const E2E_PIN = '1234'
|
||||
18
frontend/vue-app/e2e/example.spec.ts
Normal file
18
frontend/vue-app/e2e/example.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('has title', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev/');
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Playwright/);
|
||||
});
|
||||
|
||||
test('get started link', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev/');
|
||||
|
||||
// Click the get started link.
|
||||
await page.getByRole('link', { name: 'Get started' }).click();
|
||||
|
||||
// Expects page to have a heading with the name of Installation.
|
||||
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
|
||||
});
|
||||
174
frontend/vue-app/e2e/plans/create-child.plan.md
Normal file
174
frontend/vue-app/e2e/plans/create-child.plan.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Create Child
|
||||
|
||||
## Application Overview
|
||||
|
||||
Tests for creating a new child from the Parent dashboard. The user is authenticated and in parent mode via stored auth state (tests/.auth/user.json). All tests start at /parent/children (Children List view).
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### 1. Happy Path
|
||||
|
||||
**Seed:** `frontend/vue-app/seed.ts`
|
||||
|
||||
#### 1.1. Create a child with name and age only
|
||||
|
||||
**File:** `tests/create-child/happy-path.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to /parent/children
|
||||
- expect: The Children List page is displayed
|
||||
2. Click the 'Add Child' floating action button (FAB) in the bottom-right corner
|
||||
- expect: The Create Child form is displayed with Name, Age, and Image fields
|
||||
3. Enter 'Alice' in the Name field
|
||||
- expect: The Name field shows 'Alice'
|
||||
4. Enter '8' in the Age field
|
||||
- expect: The Age field shows '8'
|
||||
5. Leave the Image field as the default pre-selected value and click the Save/Submit button
|
||||
- expect: No error messages are displayed
|
||||
- expect: Navigation returns to /parent/children
|
||||
- expect: The child 'Alice' appears in the children list
|
||||
|
||||
#### 1.2. Create a child via the inline 'Create' button in empty state
|
||||
|
||||
**File:** `tests/create-child/happy-path.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to /parent/children with no children in the account
|
||||
- expect: An empty state message is shown with an inline 'Create' button
|
||||
2. Click the inline 'Create' button
|
||||
- expect: The Create Child form is displayed
|
||||
3. Enter 'Bob' in the Name field and '10' in the Age field, then click Save/Submit
|
||||
- expect: No errors are displayed
|
||||
- expect: 'Bob' appears in the children list
|
||||
|
||||
#### 1.3. Create a child with a custom uploaded image
|
||||
|
||||
**File:** `tests/create-child/happy-path.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||
- expect: The Create Child form is displayed
|
||||
2. Enter 'Grace' in the Name field and '6' in the Age field
|
||||
- expect: Name and Age fields are populated
|
||||
3. In the Image field, select the option to upload a local file and choose a valid PNG or JPEG
|
||||
- expect: The image is selected and shown as a preview
|
||||
4. Click the Save/Submit button
|
||||
- expect: No error messages are displayed
|
||||
- expect: 'Grace' appears in the children list with the uploaded image
|
||||
|
||||
### 2. Validation - Required Fields
|
||||
|
||||
**Seed:** `frontend/vue-app/seed.ts`
|
||||
|
||||
#### 2.1. Reject submission when Name is empty
|
||||
|
||||
**File:** `tests/create-child/validation.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||
- expect: The Create Child form is displayed
|
||||
2. Leave the Name field empty, enter '7' in the Age field, and click Save/Submit
|
||||
- expect: Error message 'Child name is required.' is displayed
|
||||
- expect: The form does not navigate away
|
||||
|
||||
#### 2.2. Reject submission when Name is whitespace only
|
||||
|
||||
**File:** `tests/create-child/validation.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||
- expect: The Create Child form is displayed
|
||||
2. Enter only spaces in the Name field, enter '7' in the Age field, and click Save/Submit
|
||||
- expect: Error message 'Child name is required.' is displayed
|
||||
- expect: The form does not navigate away
|
||||
|
||||
#### 2.3. Reject submission when Age is empty
|
||||
|
||||
**File:** `tests/create-child/validation.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||
- expect: The Create Child form is displayed
|
||||
2. Enter 'Charlie' in the Name field, clear the Age field, and click Save/Submit
|
||||
- expect: An age validation error is displayed
|
||||
- expect: The form does not navigate away
|
||||
|
||||
### 3. Validation - Boundary Conditions
|
||||
|
||||
**Seed:** `frontend/vue-app/seed.ts`
|
||||
|
||||
#### 3.1. Reject negative age
|
||||
|
||||
**File:** `tests/create-child/validation.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||
- expect: The Create Child form is displayed
|
||||
2. Enter 'Dave' in the Name field, enter '-1' in the Age field, and click Save/Submit
|
||||
- expect: Error message 'Age must be a non-negative number.' is displayed
|
||||
- expect: The form does not navigate away
|
||||
|
||||
#### 3.2. Enforce maximum Name length of 64 characters
|
||||
|
||||
**File:** `tests/create-child/validation.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||
- expect: The Create Child form is displayed
|
||||
2. Attempt to type a name with 65 or more characters in the Name field
|
||||
- expect: The input is capped at 64 characters
|
||||
3. Enter '5' in the Age field and click Save/Submit
|
||||
- expect: The form submits successfully with the 64-character name
|
||||
|
||||
#### 3.3. Reject age greater than 120
|
||||
|
||||
**File:** `tests/create-child/validation.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||
- expect: The Create Child form is displayed
|
||||
2. Enter 'Eve' in the Name field, enter '121' in the Age field, and click Save/Submit
|
||||
- expect: A validation error is shown or the value is capped at 120
|
||||
|
||||
### 4. Navigation
|
||||
|
||||
**Seed:** `frontend/vue-app/seed.ts`
|
||||
|
||||
#### 4.1. Cancel navigates back without saving
|
||||
|
||||
**File:** `tests/create-child/navigation.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||
- expect: The Create Child form is displayed
|
||||
2. Enter 'Frank' in the Name field and '9' in the Age field, then click the Cancel button
|
||||
- expect: Navigation returns to /parent/children
|
||||
- expect: 'Frank' does NOT appear in the children list
|
||||
|
||||
### 5. Real-Time Updates via SSE
|
||||
|
||||
**Seed:** `frontend/vue-app/seed.ts`
|
||||
|
||||
#### 5.1. New child appears in list without page reload
|
||||
|
||||
**File:** `tests/create-child/sse.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Open two browser tabs both on /parent/children
|
||||
- expect: Both tabs show the Children List page
|
||||
2. In Tab 1, click 'Add Child' FAB, fill in name 'Hannah' and age '4', then submit
|
||||
- expect: Tab 1 navigates to /parent/children and 'Hannah' is visible
|
||||
3. Switch to Tab 2 which is still on /parent/children
|
||||
- expect: 'Hannah' appears in Tab 2 without a manual refresh (SSE real-time update)
|
||||
|
||||
### 6. Authorization
|
||||
|
||||
**Seed:** `frontend/vue-app/seed.ts`
|
||||
|
||||
#### 6.1. Add Child FAB is hidden when parent auth is expired
|
||||
|
||||
**File:** `tests/create-child/authorization.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Clear 'parentAuth' from localStorage to simulate expired parent authentication and navigate to /parent/children
|
||||
- expect: The 'Add Child' FAB is NOT visible on the page
|
||||
3
frontend/vue-app/e2e/seed.spec.ts
Normal file
3
frontend/vue-app/e2e/seed.spec.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default async function seed(page: any): Promise<void> {
|
||||
// no-op seed
|
||||
}
|
||||
Reference in New Issue
Block a user