Gate onboarding Next on three saved Regulars
CI / ci (push) Successful in 2m22s
CI / ci (pull_request) Successful in 2m21s

Next: notifications counted optimistic chips, so a household with two
saved Regulars could tap a third and leave step 2 before the server
refused it. Next now waits until no add is pending and three Regulars
have server ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 17:02:44 -04:00
co-authored by Claude Opus 5.5
parent b568558875
commit 760ad9c421
2 changed files with 45 additions and 4 deletions
+38 -2
View File
@@ -234,7 +234,7 @@ describe('onboarding step 2 (mock 1h)', () => {
expect(regularNames()).toEqual(['Beef tacos', 'Pizza night'])
})
it('enables "Next: notifications" once there are 3 Regulars', async () => {
it('enables "Next: notifications" once the server has saved 3 Regulars', async () => {
const server = await regularsStep([
{ id: 'd1', name: 'Beef tacos' },
{ id: 'd2', name: 'Pizza night' },
@@ -242,10 +242,13 @@ describe('onboarding step 2 (mock 1h)', () => {
expect(next()).toBeDisabled()
fireEvent.click(screen.getByRole('button', { name: 'Add Burgers' }))
await vi.waitFor(() => expect(next()).toBeEnabled())
await screen.findByRole('heading', { level: 2, name: 'Your regulars · 3' })
// The third chip shows at once, but Next waits for the server.
expect(next()).toBeDisabled()
await vi.waitFor(() => expect(server.held).toHaveLength(1))
created(server, server.held[0]!, 'd3')
await screen.findByRole('button', { name: 'Remove Burgers' })
expect(next()).toBeEnabled()
fireEvent.click(screen.getByRole('button', { name: 'Remove Pizza night' }))
await vi.waitFor(() => expect(next()).toBeDisabled())
@@ -256,4 +259,37 @@ describe('onboarding step 2 (mock 1h)', () => {
fireEvent.click(next())
expect(await screen.findByText('Step 3 of 3')).toBeInTheDocument()
})
it('keeps "Next: notifications" disabled when the third add is refused', async () => {
const server = await regularsStep([
{ id: 'd1', name: 'Beef tacos' },
{ id: 'd2', name: 'Pizza night' },
])
fireEvent.click(screen.getByRole('button', { name: 'Add Burgers' }))
await screen.findByRole('heading', { level: 2, name: 'Your regulars · 3' })
expect(next()).toBeDisabled()
await vi.waitFor(() => expect(server.held).toHaveLength(1))
server.held[0]!.answer(errorResponse(409, 'duplicate_dinner'))
expect(await screen.findByRole('alert')).toHaveTextContent('Could not add Burgers: duplicate_dinner message')
expect(regularNames()).toEqual(['Beef tacos', 'Pizza night'])
expect(next()).toBeDisabled()
})
it('keeps "Next: notifications" disabled while a fourth add is unanswered', async () => {
const server = await regularsStep([
{ id: 'd1', name: 'Beef tacos' },
{ id: 'd2', name: 'Pizza night' },
{ id: 'd3', name: 'Chili' },
])
expect(next()).toBeEnabled()
fireEvent.click(screen.getByRole('button', { name: 'Add Burgers' }))
await screen.findByRole('heading', { level: 2, name: 'Your regulars · 4' })
expect(next()).toBeDisabled()
await vi.waitFor(() => expect(server.held).toHaveLength(1))
server.held[0]!.answer(errorResponse(409, 'duplicate_dinner'))
await screen.findByRole('alert')
expect(next()).toBeEnabled()
})
})
+7 -2
View File
@@ -11,7 +11,8 @@ import { TextInput } from '../ui/TextInput.tsx'
import { OnboardingStep } from './OnboardingStep.tsx'
import styles from './Onboarding.module.css'
// MIN_REGULARS is how many Regulars the household adds before moving on.
// MIN_REGULARS is how many Regulars the server has saved before the
// household moves on.
export const MIN_REGULARS = 3
// commonPicks are offered as one-tap Regulars until the household has them.
@@ -41,6 +42,10 @@ export function RegularsStep() {
const [draft, setDraft] = useState('')
const list = regulars.data ?? []
const picks = commonPicks.filter((pick) => !list.some((regular) => sameName(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)
const saved = list.filter((regular) => !regular.pending).length
function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
@@ -57,7 +62,7 @@ export function RegularsStep() {
action={
<Button
className={styles.action}
disabled={list.length < MIN_REGULARS}
disabled={adding || saved < MIN_REGULARS}
onClick={() => void navigate('/onboarding/notifications')}
>
Next: notifications