Refactored frontend directory
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m46s

This commit is contained in:
2026-04-25 00:40:15 -04:00
parent db846f4e31
commit 127378797c
263 changed files with 88 additions and 79 deletions

View File

@@ -0,0 +1,233 @@
// shared helpers for chore-scheduler spec files
import { expect, type APIRequestContext, type Locator, type Page } from '@playwright/test'
// ─── Date Helpers ─────────────────────────────────────────────────────────────
export function todayISO(): string {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
export function yesterdayISO(): string {
const d = new Date()
d.setDate(d.getDate() - 1)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
export function futureDateISO(daysAhead = 7): string {
const d = new Date()
d.setDate(d.getDate() + daysAhead)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
/** Today's weekday (0 = Sunday, 6 = Saturday) — matches DayConfig.day */
export function todayDayIndex(): number {
return new Date().getDay()
}
/** A weekday guaranteed not to be today */
export function nonTodayDayIndex(): number {
return (new Date().getDay() + 1) % 7
}
// ─── API Helpers ──────────────────────────────────────────────────────────────
export 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: { name: string; id: string }) => c.name === name)?.id ??
''
)
}
export async function createChoreTask(request: APIRequestContext, name: string): Promise<string> {
const pre = await request.get('/api/task/list')
for (const t of (await pre.json()).tasks ?? []) {
if (t.name === name) await request.delete(`/api/task/${t.id}`)
}
await request.put('/api/task/add', { data: { name, points: 5, type: 'chore' } })
const list = await request.get('/api/task/list')
return (
(await list.json()).tasks?.find((t: { name: string; id: string }) => t.name === name)?.id ?? ''
)
}
export async function assignChore(
request: APIRequestContext,
childId: string,
choreIds: string[],
): Promise<void> {
await request.put(`/api/child/${childId}/set-tasks`, {
data: { task_ids: choreIds, type: 'chore' },
})
}
export interface DaysScheduleOpts {
hour?: number
minute?: number
hasDeadline?: boolean
enabled?: boolean
}
export async function setDaysSchedule(
request: APIRequestContext,
childId: string,
taskId: string,
days: number[],
opts: DaysScheduleOpts = {},
): Promise<void> {
const { hour = 8, minute = 0, hasDeadline = true, enabled = true } = opts
const day_configs = days.map((day) => ({ day, hour, minute }))
await request.put(`/api/child/${childId}/task/${taskId}/schedule`, {
data: {
mode: 'days',
day_configs,
default_hour: hour,
default_minute: minute,
default_has_deadline: hasDeadline,
enabled,
},
})
}
export interface IntervalScheduleOpts {
hour?: number
minute?: number
hasDeadline?: boolean
enabled?: boolean
}
export async function setIntervalSchedule(
request: APIRequestContext,
childId: string,
taskId: string,
intervalDays: number,
anchorDate: string,
opts: IntervalScheduleOpts = {},
): Promise<void> {
const { hour = 8, minute = 0, hasDeadline = true, enabled = true } = opts
await request.put(`/api/child/${childId}/task/${taskId}/schedule`, {
data: {
mode: 'interval',
interval_days: intervalDays,
anchor_date: anchorDate,
interval_has_deadline: hasDeadline,
interval_hour: hour,
interval_minute: minute,
enabled,
},
})
}
export async function deleteSchedule(
request: APIRequestContext,
childId: string,
taskId: string,
): Promise<void> {
// 404 is acceptable (no schedule existed)
await request.delete(`/api/child/${childId}/task/${taskId}/schedule`)
}
// ─── UI Helpers ───────────────────────────────────────────────────────────────
/** Locator for the Chores section */
export function choreSection(page: Page): Locator {
return page.locator('.child-list-container').filter({
has: page.locator('h3', { hasText: 'Chores' }),
})
}
/** Locator for a named chore card in the Chores section */
export function choreCard(page: Page, name: string): Locator {
return choreSection(page)
.locator('.item-card')
.filter({
has: page.locator('.item-name').getByText(name, { exact: true }),
})
}
/** Click the chore card (select it), wait for ready state, then click Options */
export async function openChoreOptions(page: Page, card: Locator): Promise<void> {
await card.click()
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
await card.getByRole('button', { name: 'Options' }).click()
}
/** Open the schedule modal for a named chore card */
export async function openScheduleModal(page: Page, card: Locator): Promise<void> {
await openChoreOptions(page, card)
await page.getByRole('button', { name: 'Schedule' }).click()
await expect(page.getByText('Schedule Chore')).toBeVisible()
}
/**
* Set a TimePickerPopover to a specific time.
* @param timePicker - the `.time-picker-popover` locator wrapping the popover
*/
export async function setTimePicker(
page: Page,
timePicker: Locator,
hour12: number,
minute: number,
period: 'AM' | 'PM',
): Promise<void> {
await timePicker.locator('.time-display').click()
// Scope panel to this specific time picker to avoid strict-mode violations
const panel = timePicker.locator('.popover-panel')
await panel.waitFor()
// Use exact regex patterns — getByRole name uses substring matching by default,
// so "3" would also match "30" and "1" would match "10", "11", "12" etc.
await panel.getByRole('button', { name: new RegExp(`^${period}$`) }).click()
await panel.getByRole('button', { name: new RegExp(`^${hour12}$`) }).click()
await panel
.getByRole('button', { name: new RegExp(`^${String(minute).padStart(2, '0')}$`) })
.click()
// Close popover by clicking the modal title (outside popover bounds)
await page.locator('.modal-title').click()
await panel.waitFor({ state: 'hidden' })
}
/** Set the hidden date input and fire a change event */
export async function setDateInput(page: Page, isoDate: string): Promise<void> {
await page.locator('input.date-input-hidden').evaluate((el: HTMLInputElement, val: string) => {
el.value = val
el.dispatchEvent(new Event('change', { bubbles: true }))
}, isoDate)
}
/**
* Navigate to the child view for a given child.
* Parent-authenticated users are blocked from /child by the auth guard, so we
* must clear the parentAuth localStorage key before navigating there.
*/
export async function goToChildView(page: Page, childId: string): Promise<void> {
// Go to the app domain first so we can access its localStorage
await page.goto(`/parent/${childId}`)
// Clear the parent auth token — router will now allow /child/:id
await page.evaluate(() => localStorage.removeItem('parentAuth'))
// Navigate to child view and wait for the chore section to appear
await page.goto(`/child/${childId}`)
await page.locator('.child-list-container').first().waitFor({ state: 'visible', timeout: 10000 })
}
/**
* Navigate (back) to the parent view from any page.
* Restores the parentAuth localStorage key so the router allows /parent/:id.
*/
export async function goToParentView(page: Page, childId: string): Promise<void> {
// Restore parent auth so the router guard allows /parent/:id
await page.evaluate(() => {
localStorage.setItem(
'parentAuth',
JSON.stringify({ expiresAt: Date.now() + 2 * 24 * 60 * 60 * 1000 }),
)
})
await page.goto(`/parent/${childId}`)
await page.locator('.child-list-container').first().waitFor({ state: 'visible', timeout: 10000 })
}