Add routine management features for child and parent views
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m8s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m8s
- Implemented routine child-mode flow tests to ensure proper functionality of routine assignment and task completion. - Created notification tests for parent view to verify routine completion notifications for children. - Developed ChildRoutineOverlay component for displaying routine tasks and handling user interactions. - Added RoutineApproveDialog component for approving or rejecting completed routines. - Created unit tests for ChildRoutineOverlay and RoutineEditView components to ensure correct behavior and rendering. - Enhanced RoutineEditView with proper handling of task addition and form submission. Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
213
frontend/e2e/mode_parent/routines/routine-child-mode.spec.ts
Normal file
213
frontend/e2e/mode_parent/routines/routine-child-mode.spec.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { test, expect, type APIRequestContext, type Page } from '@playwright/test'
|
||||
|
||||
const CHILD_NAME = 'RoutineChildModeChild'
|
||||
const ROUTINE_NAME = 'RoutineChildModeTest'
|
||||
const ROUTINE_POINTS = 10
|
||||
const TASK_NAME_1 = 'Brush Teeth'
|
||||
const TASK_NAME_2 = 'Wash Face'
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async function createChild(request: APIRequestContext, name: string): Promise<string> {
|
||||
const pre = await request.get('/api/child/list')
|
||||
for (const c of (await pre.json()).children ?? []) {
|
||||
if (c.name === name) await request.delete(`/api/child/${c.id}`)
|
||||
}
|
||||
await request.put('/api/child/add', { data: { name, age: 8 } })
|
||||
const list = await request.get('/api/child/list')
|
||||
return (await list.json()).children?.find((c: any) => c.name === name)?.id ?? ''
|
||||
}
|
||||
|
||||
async function createRoutine(
|
||||
request: APIRequestContext,
|
||||
name: string,
|
||||
points: number,
|
||||
): Promise<string> {
|
||||
const pre = await request.get('/api/routine/list')
|
||||
for (const r of (await pre.json()).routines ?? []) {
|
||||
if (r.name === name) await request.delete(`/api/routine/${r.id}`)
|
||||
}
|
||||
const res = await request.put('/api/routine/add', { data: { name, points } })
|
||||
return (await res.json()).routine?.id ?? ''
|
||||
}
|
||||
|
||||
function routineSection(page: Page) {
|
||||
return page.locator('.child-list-container').filter({
|
||||
has: page.locator('h3', { hasText: 'Routines' }),
|
||||
})
|
||||
}
|
||||
|
||||
// ── suite ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
test.describe('Routine child-mode flow', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let childId = ''
|
||||
let routineId = ''
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
childId = await createChild(request, CHILD_NAME)
|
||||
routineId = await createRoutine(request, ROUTINE_NAME, ROUTINE_POINTS)
|
||||
|
||||
await request.put(`/api/routine/${routineId}/item/add`, { data: { name: TASK_NAME_1 } })
|
||||
await request.put(`/api/routine/${routineId}/item/add`, { data: { name: TASK_NAME_2 } })
|
||||
await request.post(`/api/child/${childId}/assign-routine`, {
|
||||
data: { routine_id: routineId },
|
||||
})
|
||||
})
|
||||
|
||||
test.afterAll(async ({ request }) => {
|
||||
if (childId) await request.delete(`/api/child/${childId}`)
|
||||
if (routineId) await request.delete(`/api/routine/${routineId}`)
|
||||
})
|
||||
|
||||
// Intercept parentAuth so the router treats this session as child-mode (not parent-authenticated).
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
const orig = Storage.prototype.getItem
|
||||
Storage.prototype.getItem = function (key: string) {
|
||||
if (key === 'parentAuth') return null
|
||||
return orig.call(this, key)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ── 1. Routine section visible ────────────────────────────────────────────
|
||||
|
||||
test('Routine section appears in child view', async ({ page }) => {
|
||||
await page.goto(`/child/${childId}`)
|
||||
|
||||
const section = routineSection(page)
|
||||
await expect(section).toBeVisible({ timeout: 5000 })
|
||||
await expect(section.locator('.item-card').filter({ hasText: ROUTINE_NAME })).toBeVisible()
|
||||
})
|
||||
|
||||
// ── 2. Overlay opens on two-click ─────────────────────────────────────────
|
||||
|
||||
test('Clicking routine twice opens overlay with correct header', async ({ page }) => {
|
||||
await page.goto(`/child/${childId}`)
|
||||
|
||||
const card = routineSection(page).locator('.item-card').filter({ hasText: ROUTINE_NAME })
|
||||
await card.waitFor({ state: 'visible' })
|
||||
|
||||
// First click enters ready state
|
||||
await card.click()
|
||||
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
|
||||
|
||||
// Second click opens overlay
|
||||
await card.click()
|
||||
|
||||
const dialog = page.getByRole('dialog')
|
||||
await expect(dialog).toBeVisible({ timeout: 3000 })
|
||||
await expect(dialog).toContainText('Did you complete')
|
||||
await expect(dialog).toContainText(ROUTINE_NAME)
|
||||
})
|
||||
|
||||
// ── 3. Task cards inside overlay ──────────────────────────────────────────
|
||||
|
||||
test('Overlay shows task cards for each routine item', async ({ page }) => {
|
||||
await page.goto(`/child/${childId}`)
|
||||
|
||||
const card = routineSection(page).locator('.item-card').filter({ hasText: ROUTINE_NAME })
|
||||
await card.waitFor({ state: 'visible' })
|
||||
await card.click()
|
||||
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
|
||||
await card.click()
|
||||
|
||||
const dialog = page.getByRole('dialog')
|
||||
await expect(dialog).toBeVisible({ timeout: 3000 })
|
||||
await expect(dialog.getByText(TASK_NAME_1)).toBeVisible()
|
||||
await expect(dialog.getByText(TASK_NAME_2)).toBeVisible()
|
||||
})
|
||||
|
||||
// ── 4. Closing overlay with No ────────────────────────────────────────────
|
||||
|
||||
test('Clicking No closes the overlay without changing state', async ({ page }) => {
|
||||
await page.goto(`/child/${childId}`)
|
||||
|
||||
const card = routineSection(page).locator('.item-card').filter({ hasText: ROUTINE_NAME })
|
||||
await card.waitFor({ state: 'visible' })
|
||||
await card.click()
|
||||
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
|
||||
await card.click()
|
||||
|
||||
const dialog = page.getByRole('dialog')
|
||||
await expect(dialog).toBeVisible({ timeout: 3000 })
|
||||
await dialog.getByRole('button', { name: 'No' }).click()
|
||||
|
||||
await expect(dialog).not.toBeVisible({ timeout: 3000 })
|
||||
await expect(card.locator('.pending-stamp')).not.toBeVisible()
|
||||
})
|
||||
|
||||
// ── 5. Yes! confirms routine → PENDING stamp ──────────────────────────────
|
||||
|
||||
test('Clicking Yes! marks routine as PENDING', async ({ page }) => {
|
||||
await page.goto(`/child/${childId}`)
|
||||
|
||||
const card = routineSection(page).locator('.item-card').filter({ hasText: ROUTINE_NAME })
|
||||
await card.waitFor({ state: 'visible' })
|
||||
await card.click()
|
||||
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
|
||||
await card.click()
|
||||
|
||||
const dialog = page.getByRole('dialog')
|
||||
await expect(dialog).toBeVisible({ timeout: 3000 })
|
||||
await dialog.getByRole('button', { name: 'Yes!' }).click()
|
||||
|
||||
// Dialog closes
|
||||
await expect(dialog).not.toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Reload to pick up the refreshed state
|
||||
await page.reload()
|
||||
const reloadedCard = routineSection(page)
|
||||
.locator('.item-card')
|
||||
.filter({ hasText: ROUTINE_NAME })
|
||||
await expect(reloadedCard.locator('.pending-stamp')).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
// ── 6. Cancel dialog for pending routine ─────────────────────────────────
|
||||
|
||||
test('Clicking a pending routine shows the cancel dialog', async ({ page }) => {
|
||||
await page.goto(`/child/${childId}`)
|
||||
|
||||
// Routine should still be pending from the previous test
|
||||
const card = routineSection(page).locator('.item-card').filter({ hasText: ROUTINE_NAME })
|
||||
await card.waitFor({ state: 'visible' })
|
||||
await expect(card.locator('.pending-stamp')).toBeVisible({ timeout: 5000 })
|
||||
|
||||
await card.click()
|
||||
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
|
||||
await card.click()
|
||||
|
||||
const dialog = page.getByRole('dialog')
|
||||
await expect(dialog).toBeVisible({ timeout: 3000 })
|
||||
await expect(dialog).toContainText('This routine is pending')
|
||||
})
|
||||
|
||||
// ── 7. Canceling pending routine removes PENDING stamp ────────────────────
|
||||
|
||||
test('Confirming cancel removes the PENDING stamp', async ({ page }) => {
|
||||
await page.goto(`/child/${childId}`)
|
||||
|
||||
const card = routineSection(page).locator('.item-card').filter({ hasText: ROUTINE_NAME })
|
||||
await card.waitFor({ state: 'visible' })
|
||||
await card.click()
|
||||
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
|
||||
await card.click()
|
||||
|
||||
const dialog = page.getByRole('dialog')
|
||||
await expect(dialog).toBeVisible({ timeout: 3000 })
|
||||
await expect(dialog).toContainText('This routine is pending')
|
||||
|
||||
// "Yes" cancels the pending confirmation
|
||||
await dialog.getByRole('button', { name: 'Yes' }).click()
|
||||
|
||||
await expect(dialog).not.toBeVisible({ timeout: 5000 })
|
||||
|
||||
await page.reload()
|
||||
const reloadedCard = routineSection(page)
|
||||
.locator('.item-card')
|
||||
.filter({ hasText: ROUTINE_NAME })
|
||||
await expect(reloadedCard.locator('.pending-stamp')).not.toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,98 @@
|
||||
import { test, expect, type APIRequestContext } from '@playwright/test'
|
||||
|
||||
const CHILD_NAME = 'RoutineNotifChild'
|
||||
const ROUTINE_NAME = 'RoutineNotifTest'
|
||||
const ROUTINE_POINTS = 8
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async function createChild(request: APIRequestContext, name: string): Promise<string> {
|
||||
const pre = await request.get('/api/child/list')
|
||||
for (const c of (await pre.json()).children ?? []) {
|
||||
if (c.name === name) await request.delete(`/api/child/${c.id}`)
|
||||
}
|
||||
await request.put('/api/child/add', { data: { name, age: 8 } })
|
||||
const list = await request.get('/api/child/list')
|
||||
return (await list.json()).children?.find((c: any) => c.name === name)?.id ?? ''
|
||||
}
|
||||
|
||||
async function createRoutine(
|
||||
request: APIRequestContext,
|
||||
name: string,
|
||||
points: number,
|
||||
): Promise<string> {
|
||||
const pre = await request.get('/api/routine/list')
|
||||
for (const r of (await pre.json()).routines ?? []) {
|
||||
if (r.name === name) await request.delete(`/api/routine/${r.id}`)
|
||||
}
|
||||
const res = await request.put('/api/routine/add', { data: { name, points } })
|
||||
return (await res.json()).routine?.id ?? ''
|
||||
}
|
||||
|
||||
// ── suite ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
test.describe('Routine parent notification', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let childId = ''
|
||||
let routineId = ''
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
childId = await createChild(request, CHILD_NAME)
|
||||
routineId = await createRoutine(request, ROUTINE_NAME, ROUTINE_POINTS)
|
||||
|
||||
await request.post(`/api/child/${childId}/assign-routine`, {
|
||||
data: { routine_id: routineId },
|
||||
})
|
||||
|
||||
// Simulate the child confirming the routine
|
||||
await request.post(`/api/child/${childId}/confirm-routine`, {
|
||||
data: { routine_id: routineId },
|
||||
})
|
||||
})
|
||||
|
||||
test.afterAll(async ({ request }) => {
|
||||
if (childId) await request.delete(`/api/child/${childId}`)
|
||||
if (routineId) await request.delete(`/api/routine/${routineId}`)
|
||||
})
|
||||
|
||||
// ── 1. Notification appears ───────────────────────────────────────────────
|
||||
|
||||
test('Confirmed routine appears in notification view', async ({ page }) => {
|
||||
await page.goto('/parent/notifications')
|
||||
const item = page.locator('.list-item').filter({ hasText: ROUTINE_NAME })
|
||||
await expect(item).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
// ── 2. Notification shows "completed" ────────────────────────────────────
|
||||
|
||||
test('Notification shows "completed" for routine', async ({ page }) => {
|
||||
await page.goto('/parent/notifications')
|
||||
const item = page.locator('.list-item').filter({ hasText: ROUTINE_NAME })
|
||||
await expect(item).toBeVisible({ timeout: 5000 })
|
||||
await expect(item).toContainText('completed')
|
||||
})
|
||||
|
||||
// ── 3. Notification shows child name ─────────────────────────────────────
|
||||
|
||||
test('Notification shows the child name', async ({ page }) => {
|
||||
await page.goto('/parent/notifications')
|
||||
const item = page.locator('.list-item').filter({ hasText: ROUTINE_NAME })
|
||||
await expect(item).toBeVisible({ timeout: 5000 })
|
||||
await expect(item).toContainText(CHILD_NAME)
|
||||
})
|
||||
|
||||
// ── 4. Clicking notification navigates to parent view ────────────────────
|
||||
|
||||
test('Clicking notification navigates to parent view for the child', async ({ page }) => {
|
||||
await page.goto('/parent/notifications')
|
||||
const item = page.locator('.list-item').filter({ hasText: ROUTINE_NAME })
|
||||
await expect(item).toBeVisible({ timeout: 5000 })
|
||||
await item.click()
|
||||
|
||||
await page.waitForURL(`/parent/${childId}**`, { timeout: 5000 })
|
||||
const url = new URL(page.url())
|
||||
expect(url.searchParams.get('scrollTo')).toBe(routineId)
|
||||
expect(url.searchParams.get('entityType')).toBe('routine')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user