Files
chore/frontend/e2e/mode_parent/tutorial/dialog-help-button.spec.ts
T
ryan 8e4f89f4ec
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m12s
feat(tutorial): update tutorial labels and modal titles to improve clarity
- Changed "Skip tour" button to "Cancel" in various tutorial overlays.
- Updated modal titles from "Confirm Task" to "Confirm Penalty" where applicable.
- Modified tutorial step call-to-action labels from "Got it" and "Next" to "Continue" for consistency.
- Added tests for new tutorial overlay behavior and button interactions.
- Refactored tutorial controller to allow ignoring tutorial enabled state in step visibility checks.
- Enhanced user profile tutorial restart functionality with appropriate modal messages.
- Updated user profile and task confirmation dialog components to reflect new titles and messages.
- Adjusted configuration files for local development environments.
2026-07-25 12:56:18 -04:00

289 lines
11 KiB
TypeScript

import { test, expect, type APIRequestContext, type Page } from '@playwright/test'
const BACKEND = 'http://localhost:5000'
const CHILD_NAME = 'DialogHelpChild'
const CHORE_NAME = 'DialogHelpChore'
const KINDNESS_NAME = 'DialogHelpKindness'
const PENALTY_NAME = 'DialogHelpPenalty'
const REWARD_NAME = 'DialogHelpReward'
const REWARD_COST = 10
const ROUTINE_NAME = 'DialogHelpRoutine'
async function setTutorialEnabled(request: APIRequestContext, enabled: boolean): Promise<void> {
const res = await request.patch(`${BACKEND}/user/tutorial-progress`, {
data: { enabled },
})
if (!res.ok()) {
throw new Error(
`Failed to set tutorial enabled=${enabled}: ${res.status()} ${await res.text()}`,
)
}
}
async function resetTutorialProgress(request: APIRequestContext): Promise<void> {
const res = await request.patch(`${BACKEND}/user/tutorial-progress`, {
data: { reset: true },
})
if (!res.ok()) {
throw new Error(`Failed to reset tutorial progress: ${res.status()} ${await res.text()}`)
}
}
async function createChild(request: APIRequestContext, name: string): Promise<string> {
const pre = await request.get(`${BACKEND}/child/list`)
for (const c of (await pre.json()).children ?? []) {
if (c.name === name) await request.delete(`${BACKEND}/child/${c.id}`)
}
await request.put(`${BACKEND}/child/add`, { data: { name, age: 8 } })
const list = await request.get(`${BACKEND}/child/list`)
return (
(await list.json()).children?.find(
(c: { name: string; id: string }) => c.name === name,
)?.id ?? ''
)
}
async function createTask(
request: APIRequestContext,
name: string,
type: 'chore' | 'kindness' | 'penalty',
): Promise<string> {
const pre = await request.get(`${BACKEND}/task/list`)
for (const t of (await pre.json()).tasks ?? []) {
if (t.name === name) await request.delete(`${BACKEND}/task/${t.id}`)
}
await request.put(`${BACKEND}/task/add`, { data: { name, points: 5, type } })
const list = await request.get(`${BACKEND}/task/list`)
return (
(await list.json()).tasks?.find((t: { name: string; id: string }) => t.name === name)?.id ?? ''
)
}
async function createReward(request: APIRequestContext, name: string): Promise<string> {
const pre = await request.get(`${BACKEND}/reward/list`)
for (const r of (await pre.json()).rewards ?? []) {
if (r.name === name) await request.delete(`${BACKEND}/reward/${r.id}`)
}
await request.put(`${BACKEND}/reward/add`, {
data: { name, description: 'E2E dialog reward', cost: REWARD_COST },
})
const list = await request.get(`${BACKEND}/reward/list`)
return (
(await list.json()).rewards?.find((r: { name: string; id: string }) => r.name === name)?.id ??
''
)
}
async function createRoutine(request: APIRequestContext, name: string): Promise<string> {
const pre = await request.get(`${BACKEND}/routine/list`)
for (const r of (await pre.json()).routines ?? []) {
if (r.name === name) await request.delete(`${BACKEND}/routine/${r.id}`)
}
const res = await request.put(`${BACKEND}/routine/add`, { data: { name, points: 5 } })
return (await res.json()).routine?.id ?? ''
}
async function assignTask(
request: APIRequestContext,
childId: string,
taskId: string,
type: 'chore' | 'kindness' | 'penalty',
): Promise<void> {
const res = await request.put(`${BACKEND}/child/${childId}/set-tasks`, {
data: { task_ids: [taskId], type },
})
if (!res.ok()) {
throw new Error(`Failed to assign ${type}: ${res.status()} ${await res.text()}`)
}
}
async function assignReward(
request: APIRequestContext,
childId: string,
rewardId: string,
): Promise<void> {
const res = await request.put(`${BACKEND}/child/${childId}/set-rewards`, {
data: { reward_ids: [rewardId] },
})
if (!res.ok()) {
throw new Error(`Failed to assign reward: ${res.status()} ${await res.text()}`)
}
}
async function assignRoutine(
request: APIRequestContext,
childId: string,
routineId: string,
): Promise<void> {
const res = await request.post(`${BACKEND}/child/${childId}/assign-routine`, {
data: { routine_id: routineId },
})
if (!res.ok()) {
throw new Error(`Failed to assign routine: ${res.status()} ${await res.text()}`)
}
}
function helpButton(page: Page) {
return page.getByRole('button', { name: 'Show help for this screen' })
}
function modalBackdrop(page: Page) {
return page.locator('.modal-backdrop')
}
async function dismissAutoTutorialChain(page: Page): Promise<void> {
const card = page.locator('.tutorial-root .card')
for (let i = 0; i < 10; i++) {
if (!(await card.isVisible().catch(() => false))) return
await page.locator('.tutorial-root .btn-primary').click()
await page.waitForTimeout(200)
}
}
function sectionByHeading(page: Page, heading: string) {
return page.locator('.child-list-container').filter({
has: page.locator('h3', { hasText: heading }),
})
}
test.describe('Dialog help button and titles', () => {
test.describe.configure({ mode: 'serial' })
let childId = ''
let choreId = ''
let kindnessId = ''
let penaltyId = ''
let rewardId = ''
let routineId = ''
test.beforeAll(async ({ request }) => {
await setTutorialEnabled(request, true)
await resetTutorialProgress(request)
childId = await createChild(request, CHILD_NAME)
choreId = await createTask(request, CHORE_NAME, 'chore')
kindnessId = await createTask(request, KINDNESS_NAME, 'kindness')
penaltyId = await createTask(request, PENALTY_NAME, 'penalty')
rewardId = await createReward(request, REWARD_NAME)
routineId = await createRoutine(request, ROUTINE_NAME)
await assignTask(request, childId, choreId, 'chore')
await assignTask(request, childId, kindnessId, 'kindness')
await assignTask(request, childId, penaltyId, 'penalty')
await assignReward(request, childId, rewardId)
await assignRoutine(request, childId, routineId)
// Give the child enough points so the reward is ready to redeem.
await request.put(`${BACKEND}/child/${childId}/edit`, { data: { points: REWARD_COST } })
})
test.afterAll(async ({ request }) => {
if (childId) await request.delete(`${BACKEND}/child/${childId}`)
if (choreId) await request.delete(`${BACKEND}/task/${choreId}`)
if (kindnessId) await request.delete(`${BACKEND}/task/${kindnessId}`)
if (penaltyId) await request.delete(`${BACKEND}/task/${penaltyId}`)
if (rewardId) await request.delete(`${BACKEND}/reward/${rewardId}`)
if (routineId) await request.delete(`${BACKEND}/routine/${routineId}`)
await setTutorialEnabled(request, false)
await resetTutorialProgress(request)
})
test.beforeEach(async ({ page }) => {
await page.goto(`/parent/${childId}`)
await expect(page.getByText(CHILD_NAME, { exact: true }).first()).toBeVisible({ timeout: 10000 })
await dismissAutoTutorialChain(page)
await expect(helpButton(page)).toBeVisible({ timeout: 5000 })
})
test('Task confirm dialog hides help button and shows "Confirm Task" for chores', async ({
page,
}) => {
const card = sectionByHeading(page, 'Chores').locator('.item-card').filter({ hasText: CHORE_NAME })
await card.waitFor({ state: 'visible' })
await card.click()
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
await card.click()
await expect(modalBackdrop(page)).toBeVisible({ timeout: 3000 })
await expect(page.locator('.modal-title')).toHaveText('Confirm Task')
await expect(helpButton(page)).not.toBeVisible()
await page.getByRole('button', { name: 'Cancel' }).click()
await expect(modalBackdrop(page)).not.toBeVisible({ timeout: 3000 })
await expect(helpButton(page)).toBeVisible({ timeout: 5000 })
})
test('Task confirm dialog hides help button and shows "Confirm Act" for kindness acts', async ({
page,
}) => {
const card = sectionByHeading(page, 'Kindness Acts')
.locator('.item-card')
.filter({ hasText: KINDNESS_NAME })
await card.waitFor({ state: 'visible' })
await card.click()
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
await card.click()
await expect(modalBackdrop(page)).toBeVisible({ timeout: 3000 })
await expect(page.locator('.modal-title')).toHaveText('Confirm Act')
await expect(helpButton(page)).not.toBeVisible()
await page.getByRole('button', { name: 'Cancel' }).click()
await expect(modalBackdrop(page)).not.toBeVisible({ timeout: 3000 })
await expect(helpButton(page)).toBeVisible({ timeout: 5000 })
})
test('Task confirm dialog hides help button for penalties', async ({ page }) => {
const card = sectionByHeading(page, 'Penalties')
.locator('.item-card')
.filter({ hasText: PENALTY_NAME })
await card.waitFor({ state: 'visible' })
await card.click()
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
await card.click()
await expect(modalBackdrop(page)).toBeVisible({ timeout: 3000 })
await expect(page.locator('.modal-title')).toHaveText('Confirm Penalty')
await expect(helpButton(page)).not.toBeVisible()
await page.getByRole('button', { name: 'Cancel' }).click()
await expect(modalBackdrop(page)).not.toBeVisible({ timeout: 3000 })
await expect(helpButton(page)).toBeVisible({ timeout: 5000 })
})
test('Reward confirm dialog hides help button and shows "Grant Reward"', async ({ page }) => {
const card = sectionByHeading(page, 'Rewards').locator('.item-card').filter({ hasText: REWARD_NAME })
await card.waitFor({ state: 'visible' })
await expect(card.getByText('REWARD READY')).toBeVisible()
await card.click()
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
await card.click()
await expect(modalBackdrop(page)).toBeVisible({ timeout: 3000 })
await expect(page.locator('.modal-title')).toHaveText('Grant Reward')
await expect(helpButton(page)).not.toBeVisible()
await page.getByRole('button', { name: 'No', exact: true }).click()
await expect(modalBackdrop(page)).not.toBeVisible({ timeout: 3000 })
await expect(helpButton(page)).toBeVisible({ timeout: 5000 })
})
test('Routine confirm dialog hides help button and shows "Confirm Routine"', async ({ page }) => {
const card = sectionByHeading(page, 'Routines').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()
await expect(modalBackdrop(page)).toBeVisible({ timeout: 3000 })
await expect(page.locator('.modal-title')).toHaveText('Confirm Routine')
await expect(helpButton(page)).not.toBeVisible()
await page.getByRole('button', { name: 'Cancel' }).click()
await expect(modalBackdrop(page)).not.toBeVisible({ timeout: 3000 })
await expect(helpButton(page)).toBeVisible({ timeout: 5000 })
})
})