Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Has been cancelled
- Added a fetch generation counter to the fetchItems method to discard stale results from concurrent fetches. - Updated the loading state management to ensure it only updates if the fetch is the latest. - Enhanced the refresh method to prevent stale data from being rendered when multiple refreshes occur. - Added a test case to verify that stale fetches are discarded correctly when concurrent refreshes happen. - Fixed CSS for mobile banners to prevent overflow and adjusted font sizes for better visibility. - Resolved issues with task icons disappearing when extending time on overdue chores by ensuring only one fetch is active at a time.
175 lines
7.2 KiB
TypeScript
175 lines
7.2 KiB
TypeScript
import { test, expect, type APIRequestContext } from '@playwright/test'
|
|
|
|
const CHILD_NAME = 'AssignChoreChild'
|
|
const CHORE1_NAME = 'AssignTestChore1'
|
|
const CHORE2_NAME = 'AssignTestChore2'
|
|
const CHORE3_NAME = 'AssignTestChore3'
|
|
|
|
async function createTestChild(request: APIRequestContext, name: string): Promise<string> {
|
|
await request.put('/api/child/add', { data: { name, age: 8 } })
|
|
const res = await request.get('/api/child/list')
|
|
const data = (await res.json()) as { children?: { name: string; id: string }[] }
|
|
return data.children?.find((c) => c.name === name)?.id ?? ''
|
|
}
|
|
|
|
async function createTestTask(
|
|
request: APIRequestContext,
|
|
name: string,
|
|
type: 'chore' | 'kindness' | 'penalty',
|
|
): Promise<string> {
|
|
await request.put('/api/task/add', { data: { name, points: 5, type } })
|
|
const res = await request.get('/api/task/list')
|
|
const data = (await res.json()) as { tasks?: { name: string; id: string }[] }
|
|
return data.tasks?.find((t) => t.name === name)?.id ?? ''
|
|
}
|
|
|
|
test.describe('Chore assignment', () => {
|
|
test.describe.configure({ mode: 'serial' })
|
|
|
|
let childId = ''
|
|
let chore1Id = ''
|
|
let chore2Id = ''
|
|
let chore3Id = ''
|
|
|
|
test.beforeAll(async ({ request }) => {
|
|
// Clean up any stale child from a previous run
|
|
const listRes = await request.get('/api/child/list')
|
|
const listData = await listRes.json()
|
|
for (const c of listData.children ?? []) {
|
|
if (c.name === CHILD_NAME) await request.delete(`/api/child/${c.id}`)
|
|
}
|
|
|
|
// Clean up any stale test tasks from a previous run
|
|
const taskRes = await request.get('/api/task/list')
|
|
const taskData = await taskRes.json()
|
|
for (const t of taskData.tasks ?? []) {
|
|
if ([CHORE1_NAME, CHORE2_NAME, CHORE3_NAME].includes(t.name)) {
|
|
await request.delete(`/api/task/${t.id}`)
|
|
}
|
|
}
|
|
|
|
childId = await createTestChild(request, CHILD_NAME)
|
|
chore1Id = await createTestTask(request, CHORE1_NAME, 'chore')
|
|
chore2Id = await createTestTask(request, CHORE2_NAME, 'chore')
|
|
chore3Id = await createTestTask(request, CHORE3_NAME, 'chore')
|
|
})
|
|
|
|
test.afterAll(async ({ request }) => {
|
|
if (childId) await request.delete(`/api/child/${childId}`)
|
|
if (chore1Id) await request.delete(`/api/task/${chore1Id}`)
|
|
if (chore2Id) await request.delete(`/api/task/${chore2Id}`)
|
|
if (chore3Id) await request.delete(`/api/task/${chore3Id}`)
|
|
})
|
|
|
|
test('"Assign Chores" button navigates to the chore assign view', async ({ page }) => {
|
|
await page.goto(`/parent/${childId}`)
|
|
await expect(page.getByRole('button', { name: 'Assign Chores' })).toBeVisible()
|
|
|
|
await page.getByRole('button', { name: 'Assign Chores' }).click()
|
|
await page.waitForURL(new RegExp(`/parent/${childId}/assign-chores(\\?|$)`))
|
|
|
|
await expect(page.getByRole('heading', { name: 'Assign Chores' })).toBeVisible()
|
|
|
|
// Test-specific chores are listed
|
|
await expect(page.getByText(CHORE1_NAME, { exact: true })).toBeVisible()
|
|
await expect(page.getByText(CHORE2_NAME, { exact: true })).toBeVisible()
|
|
|
|
// Fresh child — nothing pre-checked
|
|
await expect(
|
|
page.locator('.list-row').filter({ hasText: CHORE1_NAME }).locator('.list-checkbox'),
|
|
).not.toBeChecked()
|
|
})
|
|
|
|
test('Select chores and submit — items appear in ParentView chores section', async ({ page }) => {
|
|
await page.goto(`/parent/${childId}`)
|
|
await page.getByRole('button', { name: 'Assign Chores' }).click()
|
|
await page.waitForURL(new RegExp(`/parent/${childId}/assign-chores(\\?|$)`))
|
|
await expect(page.getByRole('heading', { name: 'Assign Chores' })).toBeVisible()
|
|
|
|
// Select two chores
|
|
await page.getByText(CHORE1_NAME, { exact: true }).click()
|
|
await page.getByText(CHORE2_NAME, { exact: true }).click()
|
|
|
|
await expect(
|
|
page.locator('.list-row').filter({ hasText: CHORE1_NAME }).locator('.list-checkbox'),
|
|
).toBeChecked()
|
|
await expect(
|
|
page.locator('.list-row').filter({ hasText: CHORE2_NAME }).locator('.list-checkbox'),
|
|
).toBeChecked()
|
|
|
|
await page.getByRole('button', { name: 'Submit' }).click()
|
|
await page.waitForURL(new RegExp(`/parent/${childId}$`))
|
|
|
|
await expect(page.getByRole('button', { name: 'Assign Chores' })).toBeVisible()
|
|
await expect(page.getByText(CHORE1_NAME)).toBeVisible()
|
|
await expect(page.getByText(CHORE2_NAME)).toBeVisible()
|
|
})
|
|
|
|
test('Re-opening assign view shows assigned items as pre-checked', async ({ page }) => {
|
|
await page.goto(`/parent/${childId}/assign-chores`)
|
|
await expect(page.getByRole('heading', { name: 'Assign Chores' })).toBeVisible()
|
|
|
|
// Previously assigned items must be pre-checked
|
|
await expect(
|
|
page.locator('.list-row').filter({ hasText: CHORE1_NAME }).locator('.list-checkbox'),
|
|
).toBeChecked()
|
|
await expect(
|
|
page.locator('.list-row').filter({ hasText: CHORE2_NAME }).locator('.list-checkbox'),
|
|
).toBeChecked()
|
|
|
|
// An unassigned item must not be checked
|
|
await expect(
|
|
page.locator('.list-row').filter({ hasText: CHORE3_NAME }).locator('.list-checkbox'),
|
|
).not.toBeChecked()
|
|
})
|
|
|
|
test('Uncheck all chores and submit — items are removed from ParentView', async ({ page }) => {
|
|
await page.goto(`/parent/${childId}`)
|
|
await page.getByRole('button', { name: 'Assign Chores' }).click()
|
|
await page.waitForURL(new RegExp(`/parent/${childId}/assign-chores(\\?|$)`))
|
|
await expect(page.getByRole('heading', { name: 'Assign Chores' })).toBeVisible()
|
|
|
|
// Uncheck the two previously assigned chores
|
|
await page.getByText(CHORE1_NAME, { exact: true }).click()
|
|
await page.getByText(CHORE2_NAME, { exact: true }).click()
|
|
|
|
await expect(
|
|
page.locator('.list-row').filter({ hasText: CHORE1_NAME }).locator('.list-checkbox'),
|
|
).not.toBeChecked()
|
|
await expect(
|
|
page.locator('.list-row').filter({ hasText: CHORE2_NAME }).locator('.list-checkbox'),
|
|
).not.toBeChecked()
|
|
|
|
await page.getByRole('button', { name: 'Submit' }).click()
|
|
await page.waitForURL(new RegExp(`/parent/${childId}$`))
|
|
|
|
await expect(page.getByRole('button', { name: 'Assign Chores' })).toBeVisible()
|
|
await expect(page.getByText(CHORE1_NAME)).not.toBeVisible()
|
|
await expect(page.getByText(CHORE2_NAME)).not.toBeVisible()
|
|
})
|
|
|
|
test('Cancel does not persist selection changes', async ({ page }) => {
|
|
await page.goto(`/parent/${childId}`)
|
|
await page.getByRole('button', { name: 'Assign Chores' }).click()
|
|
await page.waitForURL(new RegExp(`/parent/${childId}/assign-chores(\\?|$)`))
|
|
await expect(page.getByRole('heading', { name: 'Assign Chores' })).toBeVisible()
|
|
|
|
// Check an item
|
|
await page.getByText(CHORE3_NAME, { exact: true }).click()
|
|
await expect(
|
|
page.locator('.list-row').filter({ hasText: CHORE3_NAME }).locator('.list-checkbox'),
|
|
).toBeChecked()
|
|
|
|
// Cancel instead of submitting
|
|
await page.getByRole('button', { name: 'Cancel' }).click()
|
|
await page.waitForURL(new RegExp(`/parent/${childId}$`))
|
|
|
|
// Navigate back to the assign view and verify the selection was not saved
|
|
await page.goto(`/parent/${childId}/assign-chores`)
|
|
await expect(page.getByRole('heading', { name: 'Assign Chores' })).toBeVisible()
|
|
await expect(
|
|
page.locator('.list-row').filter({ hasText: CHORE3_NAME }).locator('.list-checkbox'),
|
|
).not.toBeChecked()
|
|
})
|
|
})
|