Show no_ideas_found as the ErrorBanner with Retry (Feature 18, mock 1i)
A failed ideas job always shows the ErrorBanner with Retry and no cards, no_ideas_found included. The failure messages and the 404-means-search- again rule move to ideas/api.ts (ideasJobFailure, ideasJobLost) for the Quick dinners screen to share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -471,18 +471,24 @@ describe('CookFromPantryPage', () => {
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(message)
|
||||
})
|
||||
|
||||
it('says no ideas were found, with no cards', async () => {
|
||||
it('shows no ideas found with the ErrorBanner and no cards, and Retry searches again', async () => {
|
||||
let n = 0
|
||||
const { fetchMock, jobs } = stubServer({ [FIND]: () => jsonResponse(202, { jobId: `j${++n}` }) })
|
||||
jobs.j1 = state('failed', 'grounding', [], false, 'no_ideas_found')
|
||||
jobs.j2 = state('failed', 'grounding', [], false, 'no_ideas_found')
|
||||
jobs.j2 = state('succeeded', 'done', [CURRY])
|
||||
await renderPage()
|
||||
await searched(fetchMock)
|
||||
|
||||
expect(await screen.findByRole('heading', { name: 'No ideas found' })).toBeInTheDocument()
|
||||
const alert = await screen.findByRole('alert')
|
||||
expect(alert).toHaveTextContent("Couldn't find ideas. No ideas came back with a recipe to match.")
|
||||
expect(screen.queryAllByRole('article')).toHaveLength(0)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Retry' }))
|
||||
await waitFor(() => expect(calls(fetchMock, FIND)).toHaveLength(2))
|
||||
expect(screen.queryAllByTestId('skeleton-card')).toHaveLength(0)
|
||||
expect(screen.queryByRole('heading', { name: 'No ideas found' })).toBeNull()
|
||||
|
||||
fireEvent.click(within(alert).getByRole('button', { name: 'Retry' }))
|
||||
await screen.findByRole('article', { name: CURRY.name })
|
||||
expect(calls(fetchMock, FIND)).toHaveLength(2)
|
||||
expect(screen.queryByRole('alert')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows an unknown job with the ErrorBanner, and Retry searches again', async () => {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { duration } from '../plan/PlanPage.tsx'
|
||||
import { Button } from '../ui/Button.tsx'
|
||||
import { Card } from '../ui/Card.tsx'
|
||||
import { Chip } from '../ui/Chip.tsx'
|
||||
import { EmptyState } from '../ui/EmptyState.tsx'
|
||||
import { ErrorBanner } from '../ui/ErrorBanner.tsx'
|
||||
import { Icon } from '../ui/Icon.tsx'
|
||||
import { IconButton } from '../ui/IconButton.tsx'
|
||||
@@ -16,6 +15,8 @@ import { TextInput } from '../ui/TextInput.tsx'
|
||||
import {
|
||||
FROM_INGREDIENTS_JOB_KIND,
|
||||
IDEAS_KEPT,
|
||||
ideasJobFailure,
|
||||
ideasJobLost,
|
||||
INGREDIENT_LIMIT,
|
||||
MAX_INGREDIENTS,
|
||||
parseIdeasResult,
|
||||
@@ -42,16 +43,6 @@ const findingLines: Record<Exclude<IdeaStage, 'done'>, string> = {
|
||||
grounding: 'Finding recipes for them…',
|
||||
}
|
||||
|
||||
// searchFailures says why a search failed, by its failure code. Any other
|
||||
// code is shown as it is; no_ideas_found is not a failure to fix, and has
|
||||
// its own empty state.
|
||||
const searchFailures: Record<string, string> = {
|
||||
ai_failed: "The AI assistant couldn't come up with ideas.",
|
||||
search_not_configured: 'Recipe search is not set up. An admin can set it up in Settings.',
|
||||
search_failed: "The recipe search couldn't be reached.",
|
||||
internal_error: 'Something went wrong on the server.',
|
||||
}
|
||||
|
||||
// fold is an ingredient as the chips compare it, so "Rice" repeats " rice".
|
||||
function fold(ingredient: string): string {
|
||||
return ingredient.trim().replace(/\s+/g, ' ').toLowerCase()
|
||||
@@ -235,15 +226,7 @@ export function CookFromPantryPage() {
|
||||
} else if (findIdeas.isError) {
|
||||
results = <ErrorBanner onRetry={retry}>Couldn't start looking for ideas. {findIdeas.error.message}</ErrorBanner>
|
||||
} else if (jobId !== null && job.status === 'failed') {
|
||||
const code = job.error!.message
|
||||
results =
|
||||
code === 'no_ideas_found' ? (
|
||||
<EmptyState title="No ideas found" action={{ label: 'Retry', onClick: retry }}>
|
||||
No recipe matched {asked!.quickOnly ? 'a quick dinner from ' : ''}those ingredients. Add a few more and try again.
|
||||
</EmptyState>
|
||||
) : (
|
||||
<ErrorBanner onRetry={retry}>Couldn't find ideas. {searchFailures[code] ?? code}</ErrorBanner>
|
||||
)
|
||||
results = <ErrorBanner onRetry={retry}>Couldn't find ideas. {ideasJobFailure(job.error!.message)}</ErrorBanner>
|
||||
} else if (found.error !== null) {
|
||||
results = <ErrorBanner onRetry={retry}>Couldn't read the ideas. {found.error.message}</ErrorBanner>
|
||||
} else if (jobId !== null) {
|
||||
@@ -251,7 +234,7 @@ export function CookFromPantryPage() {
|
||||
const done = job.status === 'succeeded'
|
||||
// A job not found will not be found by asking again: Retry searches
|
||||
// again. Any other failed poll is asked again.
|
||||
const lost = job.error instanceof ApiError && job.error.status === 404
|
||||
const lost = ideasJobLost(job.error)
|
||||
results = (
|
||||
<>
|
||||
{job.error !== null && (
|
||||
|
||||
+24
-1
@@ -1,5 +1,5 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useApi } from '../api/index.ts'
|
||||
import { ApiError, useApi } from '../api/index.ts'
|
||||
import { isRecord } from '../api/errors.ts'
|
||||
import { parseDinner } from '../dinners/api.ts'
|
||||
import { QUEST_LOG_KEY } from '../discover/api.ts'
|
||||
@@ -49,6 +49,29 @@ export type IdeasResult = {
|
||||
dropped: number
|
||||
}
|
||||
|
||||
// IDEAS_JOB_FAILURES says why an ideas job failed, by its failure code.
|
||||
// no_ideas_found is no fault to fix, but the search still came back empty.
|
||||
const IDEAS_JOB_FAILURES: Record<string, string> = {
|
||||
no_ideas_found: 'No ideas came back with a recipe to match.',
|
||||
ai_failed: "The AI assistant couldn't come up with ideas.",
|
||||
search_not_configured: 'Recipe search is not set up. An admin can set it up in Settings.',
|
||||
search_failed: "The recipe search couldn't be reached.",
|
||||
internal_error: 'Something went wrong on the server.',
|
||||
}
|
||||
|
||||
// ideasJobFailure says why an ideas job failed; an unknown code is shown
|
||||
// as it is.
|
||||
export function ideasJobFailure(code: string): string {
|
||||
return IDEAS_JOB_FAILURES[code] ?? code
|
||||
}
|
||||
|
||||
// ideasJobLost says a poll of an ideas job failed because the server no
|
||||
// longer has the job (404), so asking for it again will not find it: the
|
||||
// search must be started again.
|
||||
export function ideasJobLost(error: Error | null): boolean {
|
||||
return error instanceof ApiError && error.status === 404
|
||||
}
|
||||
|
||||
// Search is what Find ideas asks for.
|
||||
export type Search = { ingredients: string[]; quickOnly: boolean }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user