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>
214 lines
8.6 KiB
TypeScript
214 lines
8.6 KiB
TypeScript
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 })
|
|
})
|
|
})
|