Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m56s
- Introduced a mechanism to hide the help button when modal dialogs are open by adding `helpButtonHidden` state in the tutorial controller. - Updated various components to set the help button visibility based on dialog states. - Added tests to verify help button visibility during reward and task confirmation dialogs. - Created new E2E tests for dialog help button functionality across different assignment views. - Refactored existing dialog components to utilize the new help button visibility logic. - Added unit tests for `RewardConfirmDialog` and `TaskConfirmDialog` to ensure correct titles are rendered based on task type. - Enhanced `HelpButton` component tests to validate visibility based on tutorial state.
289 lines
11 KiB
TypeScript
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 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('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 })
|
|
})
|
|
})
|