Integration fix: Feature 12 feature-review findings F1-F8 (#95)
CI / ci (pull_request) Successful in 2m23s
CI / ci (push) Successful in 2m27s

Co-authored-by: orchestrator-agent <20+orchestrator-agent@code.cubecraftcreations.com>
This commit was merged in pull request #95.
This commit is contained in:
2026-10-06 19:54:47 -04:00
committed by approver_agent
parent 171c9c2d11
commit cc7875c76c
26 changed files with 2402 additions and 257 deletions
@@ -100,6 +100,7 @@ async function renderStep(enableNotifications?: EnableNotifications) {
element={<NotificationsStep enableNotifications={enableNotifications} />}
/>
<Route path="/" element={<h1>Home</h1>} />
<Route path="/onboarding/regulars" element={<h1>Regulars step</h1>} />
</Routes>
</AuthBootstrap>
</ApiProviders>
@@ -257,6 +258,41 @@ describe('onboarding step 3', () => {
})
})
describe('onboarding step 3 with too few Regulars', () => {
it('says why and offers a way back to add Regulars', async () => {
const server = await renderStep()
fireEvent.click(later())
await vi.waitFor(() => expect(server.held).toHaveLength(1))
server.held[0]!.answer(
jsonResponse(400, {
error: {
code: 'invalid_settings',
message: 'invalid household settings: onboarding_completed: add at least 3 Regulars first (have 2)',
retryable: false,
},
}),
)
expect(await screen.findByRole('alert')).toHaveTextContent(
'Could not finish setting up: invalid household settings: onboarding_completed: add at least 3 Regulars first (have 2)',
)
const addRegulars = screen.getByRole('button', { name: 'Add Regulars' })
expectMinHitArea(addRegulars)
fireEvent.click(addRegulars)
expect(await screen.findByRole('heading', { level: 1, name: 'Regulars step' })).toBeInTheDocument()
})
it('offers no way back for any other refusal', async () => {
const server = await renderStep()
fireEvent.click(later())
await vi.waitFor(() => expect(server.held).toHaveLength(1))
server.held[0]!.answer(errorResponse(500, 'internal'))
expect(await screen.findByRole('alert')).toHaveTextContent('Could not finish setting up: internal message')
expect(screen.queryByRole('button', { name: 'Add Regulars' })).not.toBeInTheDocument()
})
})
describe('notificationsUnavailable', () => {
it('answers "unavailable" without asking for permission', async () => {
await expect(notificationsUnavailable()).resolves.toBe('unavailable')
+18 -4
View File
@@ -2,7 +2,7 @@ import { useState } from 'react'
import { CalendarDays, ShoppingBasket, Sparkles, UtensilsCrossed, type LucideIcon } from 'lucide-react'
import { useNavigate } from 'react-router'
import { InstallHint } from '../pwa/InstallHint.tsx'
import { useCompleteOnboarding } from '../settings/api.ts'
import { needsMoreRegulars, useCompleteOnboarding } from '../settings/api.ts'
import { Button } from '../ui/Button.tsx'
import { Icon } from '../ui/Icon.tsx'
import { OnboardingStep } from './OnboardingStep.tsx'
@@ -50,12 +50,15 @@ export type NotificationsStepProps = {
// NotificationsStep is onboarding step 3: what MealQuest notifies about, the
// iOS install hint, and finishing onboarding. Either button records that
// onboarding is finished and goes Home.
// onboarding is finished and goes Home. When the server refuses because the
// household has fewer than three Regulars, the reason shows with a way back
// to step 2 to add them.
export function NotificationsStep({ enableNotifications = notificationsUnavailable }: NotificationsStepProps) {
const navigate = useNavigate()
const complete = useCompleteOnboarding()
const [enabling, setEnabling] = useState(false)
const [problem, setProblem] = useState<string | null>(null)
const [fewRegulars, setFewRegulars] = useState(false)
const busy = enabling || complete.isPending
async function finish() {
@@ -63,13 +66,19 @@ export function NotificationsStep({ enableNotifications = notificationsUnavailab
await complete.mutateAsync()
} catch (error) {
setProblem(`Could not finish setting up: ${(error as Error).message}`)
setFewRegulars(needsMoreRegulars(error as Error))
return
}
void navigate('/', { replace: true })
}
async function turnOn() {
function clearProblem() {
setProblem(null)
setFewRegulars(false)
}
async function turnOn() {
clearProblem()
setEnabling(true)
try {
await enableNotifications()
@@ -83,7 +92,7 @@ export function NotificationsStep({ enableNotifications = notificationsUnavailab
}
function later() {
setProblem(null)
clearProblem()
void finish()
}
@@ -124,6 +133,11 @@ export function NotificationsStep({ enableNotifications = notificationsUnavailab
{problem}
</p>
)}
{fewRegulars && (
<Button variant="outline" className={styles.recover} onClick={() => void navigate('/onboarding/regulars')}>
Add Regulars
</Button>
)}
</OnboardingStep>
)
}
+5
View File
@@ -229,6 +229,11 @@
color: var(--color-accent-700);
}
/* Step 3: back to step 2 when the server wants more Regulars. */
.recover {
margin-top: var(--space-3);
}
.status {
margin-top: 10px;
}
@@ -146,6 +146,14 @@ describe('onboarding step 2 (mock 1h)', () => {
expect(declaredStyle(pills[0]!).height).toBe('6px')
})
it('does not offer a common pick the server would refuse as a duplicate of a Regular', async () => {
await regularsStep([dinnerBody('d1', 'Homemade chili'), dinnerBody('d2', 'Easy Chicken Stir Fry')])
expect(screen.queryByRole('button', { name: 'Add Chili' })).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'Add Chicken stir-fry' })).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Add Beef tacos' })).toBeInTheDocument()
})
it('sizes the input pill, "+" button, picks and Next as the mock does', async () => {
await regularsStep()
expect(hitArea(input()).height).toBe(48)
+4 -6
View File
@@ -2,6 +2,7 @@ import { useState, type FormEvent } from 'react'
import { Plus } from 'lucide-react'
import { useNavigate } from 'react-router'
import { useAddRegular, useRegulars, useRemoveRegular, type Regular } from '../dinners/api.ts'
import { similarNames } from '../dinners/names.ts'
import { useRefusals } from '../dinners/useRefusals.ts'
import { Button } from '../ui/Button.tsx'
import { Chip } from '../ui/Chip.tsx'
@@ -16,7 +17,8 @@ import styles from './Onboarding.module.css'
// household moves on.
export const MIN_REGULARS = 3
// commonPicks are offered as one-tap Regulars until the household has them.
// commonPicks are offered as one-tap Regulars until the household has them,
// or a Regular the server would count as the same dinner.
export const commonPicks = [
'Beef tacos',
'Spaghetti bolognese',
@@ -28,10 +30,6 @@ export const commonPicks = [
'Fried rice',
]
function sameName(a: string, b: string): boolean {
return a.trim().toLowerCase() === b.trim().toLowerCase()
}
// RegularsStep is onboarding step 2, handoff mock 1h: the household adds
// the dinners it makes on repeat. Each add and remove shows at once and is
// undone if the server refuses it.
@@ -43,7 +41,7 @@ export function RegularsStep() {
const [draft, setDraft] = useState('')
const { refusals, track } = useRefusals()
const list = regulars.data ?? []
const picks = commonPicks.filter((pick) => !list.some((regular) => sameName(regular.name, pick)))
const picks = commonPicks.filter((pick) => !list.some((regular) => similarNames(regular.name, pick)))
// Next waits for every add to be answered, so a refused add's error shows
// here and an add the server takes back never counts towards the three.
const adding = list.some((regular) => regular.pending)