Remediate feature-review HIGH: report the Supply run outcome after Push to Calendar #141
@@ -1,6 +1,6 @@
|
|||||||
import { QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||||
import { MemoryRouter, Route, Routes, useLocation } from 'react-router'
|
import { Link, MemoryRouter, Route, Routes, useLocation } from 'react-router'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest'
|
||||||
import { ApiProvider, createApiClient, createQueryClient, createVerifiedSession } from '../api/index.ts'
|
import { ApiProvider, createApiClient, createQueryClient, createVerifiedSession } from '../api/index.ts'
|
||||||
import { createNetworkStatus } from '../pwa/networkStatus.ts'
|
import { createNetworkStatus } from '../pwa/networkStatus.ts'
|
||||||
@@ -177,10 +177,16 @@ async function renderPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opened stands in for the screen a link opened, saying where it is.
|
// Opened stands in for the screen a link opened, saying where it is, with
|
||||||
|
// a link back to the Plan.
|
||||||
function Opened() {
|
function Opened() {
|
||||||
const { pathname, search } = useLocation()
|
const { pathname, search } = useLocation()
|
||||||
return <p>Opened {pathname + search}</p>
|
return (
|
||||||
|
<>
|
||||||
|
<p>Opened {pathname + search}</p>
|
||||||
|
<Link to="/plan">Back to the Plan</Link>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// supplyRun is the Supply run of the week of Oct 5 with needed of its 32
|
// supplyRun is the Supply run of the week of Oct 5 with needed of its 32
|
||||||
@@ -542,6 +548,35 @@ describe('PlanPage', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('no longer says the Supply run could not be made once the shopping list is made, after leaving the Plan and coming back', async () => {
|
||||||
|
stubServer({
|
||||||
|
[`GET ${THIS_WEEK}`]: () => jsonResponse(200, planBody('draft')),
|
||||||
|
[`POST ${PUSH}`]: () => jsonResponse(200, syncedNow(SUPPLY_RUN_FAILED)),
|
||||||
|
'POST /api/v1/shopping/2026-10-05/generate': () => jsonResponse(200, { ...supplyRun(28), skipped: [] }),
|
||||||
|
})
|
||||||
|
await renderPage()
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole('button', { name: 'Looks good — push to Calendar' }))
|
||||||
|
const alert = await screen.findByRole('alert')
|
||||||
|
expect(alert).toHaveTextContent("Pushed to Calendar, but this week's Supply run couldn't be made.")
|
||||||
|
|
||||||
|
// A Supply run never made is still said after leaving the Plan and
|
||||||
|
// coming back.
|
||||||
|
fireEvent.click(screen.getByRole('link', { name: 'Shopping list' }))
|
||||||
|
fireEvent.click(await screen.findByRole('link', { name: 'Back to the Plan' }))
|
||||||
|
expect(await screen.findByRole('alert')).toHaveTextContent("Pushed to Calendar, but this week's Supply run couldn't be made.")
|
||||||
|
|
||||||
|
fireEvent.click(within(screen.getByRole('alert')).getByRole('button', { name: 'Make the shopping list' }))
|
||||||
|
expect(await screen.findByRole('status')).toHaveTextContent("Made this week's Supply run from the plan.")
|
||||||
|
|
||||||
|
// Once made, it is not said again to be unmade.
|
||||||
|
fireEvent.click(screen.getByRole('link', { name: 'Shopping list · 28' }))
|
||||||
|
fireEvent.click(await screen.findByRole('link', { name: 'Back to the Plan' }))
|
||||||
|
expect(await screen.findByRole('link', { name: 'Shopping list · 28' })).toBeInTheDocument()
|
||||||
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByRole('button', { name: 'Make the shopping list' })).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('pushes nothing again until the changed plan is read and shown', async () => {
|
it('pushes nothing again until the changed plan is read and shown', async () => {
|
||||||
let read: () => Response | Promise<Response> = () => jsonResponse(200, planBody('draft'))
|
let read: () => Response | Promise<Response> = () => jsonResponse(200, planBody('draft'))
|
||||||
let push = () => errorResponse(409, 'plan_changed')
|
let push = () => errorResponse(409, 'plan_changed')
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ function WeekBoard({
|
|||||||
setJobId(null)
|
setJobId(null)
|
||||||
swap.dismiss()
|
swap.dismiss()
|
||||||
push.reset()
|
push.reset()
|
||||||
|
generate.reset()
|
||||||
mutate(
|
mutate(
|
||||||
{ weekStart, confirm },
|
{ weekStart, confirm },
|
||||||
{
|
{
|
||||||
@@ -371,8 +372,17 @@ function WeekBoard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const retryPush = cached ? () => void plan.refetch() : pushToCalendar
|
const retryPush = cached ? () => void plan.refetch() : pushToCalendar
|
||||||
// A push's Supply run made again on the Plan stands for the push's.
|
// makeShoppingList makes the week's Supply run again after a push could
|
||||||
const supplyRun: PushedSupplyRun | null = push.supplyRun && (generate.isSuccess ? { status: 'generated', error: null } : push.supplyRun)
|
// not make it. Once made, the push is settled, even if the Plan was left
|
||||||
|
// meanwhile: coming back does not say again that it couldn't be made. A
|
||||||
|
// failure is the mutation's state, which generate.error reports.
|
||||||
|
function makeShoppingList() {
|
||||||
|
const { settle } = push
|
||||||
|
generate.mutateAsync().then(settle, () => undefined)
|
||||||
|
}
|
||||||
|
// A push's Supply run made again on the Plan stands for the push's, which
|
||||||
|
// is settled.
|
||||||
|
const supplyRun: PushedSupplyRun | null = generate.isSuccess ? { status: 'generated', error: null } : push.supplyRun
|
||||||
const failure = push.error !== null ? (
|
const failure = push.error !== null ? (
|
||||||
<PushFailure error={push.error} cached={cached} onRetry={retryPush} />
|
<PushFailure error={push.error} cached={cached} onRetry={retryPush} />
|
||||||
) : supplyRun?.status === 'failed' ? (
|
) : supplyRun?.status === 'failed' ? (
|
||||||
@@ -382,7 +392,7 @@ function WeekBoard({
|
|||||||
it again.
|
it again.
|
||||||
</ErrorBanner>
|
</ErrorBanner>
|
||||||
) : (
|
) : (
|
||||||
<ErrorBanner onRetry={generate.isPending ? undefined : () => generate.mutate()} retryLabel="Make the shopping list">
|
<ErrorBanner onRetry={generate.isPending ? undefined : makeShoppingList} retryLabel="Make the shopping list">
|
||||||
Pushed to Calendar, but this week's Supply run couldn't be made.
|
Pushed to Calendar, but this week's Supply run couldn't be made.
|
||||||
{generate.isPending
|
{generate.isPending
|
||||||
? ' Making the shopping list…'
|
? ' Making the shopping list…'
|
||||||
@@ -426,6 +436,7 @@ function WeekBoard({
|
|||||||
function swapIn(chosen: Swap) {
|
function swapIn(chosen: Swap) {
|
||||||
setSwappingDate(null)
|
setSwappingDate(null)
|
||||||
push.reset()
|
push.reset()
|
||||||
|
generate.reset()
|
||||||
swap.mutate(chosen)
|
swap.mutate(chosen)
|
||||||
}
|
}
|
||||||
// dropIn swaps in a dinner dragged onto a day. A drag is not picked up,
|
// dropIn swaps in a dinner dragged onto a day. A drag is not picked up,
|
||||||
|
|||||||
+17
-3
@@ -430,6 +430,7 @@ export type PushToCalendar = {
|
|||||||
supplyRun: PushedSupplyRun | null
|
supplyRun: PushedSupplyRun | null
|
||||||
mutate: () => void
|
mutate: () => void
|
||||||
reset: () => void
|
reset: () => void
|
||||||
|
settle: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// usePushToCalendar pushes the plan of the week starting on weekStart to the
|
// usePushToCalendar pushes the plan of the week starting on weekStart to the
|
||||||
@@ -450,8 +451,10 @@ export type PushToCalendar = {
|
|||||||
//
|
//
|
||||||
// The week's pushes live in the mutation cache, not in the component, as its
|
// The week's pushes live in the mutation cache, not in the component, as its
|
||||||
// swaps do, so leaving the week and coming back keeps one pending. reset
|
// swaps do, so leaving the week and coming back keeps one pending. reset
|
||||||
// leaves settled pushes behind; it is refused while one is pending. Settled
|
// leaves settled pushes behind; it is refused while one is pending. settle
|
||||||
// pushes of a week no longer shown are left behind too
|
// leaves behind the push shown, once its failed Supply run has been made
|
||||||
|
// another way, so it is not said again to be unmade; a push sent since is
|
||||||
|
// kept. Settled pushes of a week no longer shown are left behind too
|
||||||
// (keepWeekMutations).
|
// (keepWeekMutations).
|
||||||
export function usePushToCalendar(weekStart: string): PushToCalendar {
|
export function usePushToCalendar(weekStart: string): PushToCalendar {
|
||||||
const { apiFetch } = useApi()
|
const { apiFetch } = useApi()
|
||||||
@@ -463,7 +466,12 @@ export function usePushToCalendar(weekStart: string): PushToCalendar {
|
|||||||
// The week's pushes, in the order sent.
|
// The week's pushes, in the order sent.
|
||||||
const pushes = useMutationState({
|
const pushes = useMutationState({
|
||||||
filters: { mutationKey },
|
filters: { mutationKey },
|
||||||
select: (mutation) => ({ status: mutation.state.status, error: mutation.state.error, data: mutation.state.data as PushedPlan | undefined }),
|
select: (mutation) => ({
|
||||||
|
mutationId: mutation.mutationId,
|
||||||
|
status: mutation.state.status,
|
||||||
|
error: mutation.state.error,
|
||||||
|
data: mutation.state.data as PushedPlan | undefined,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
const latest = pushes.at(-1)
|
const latest = pushes.at(-1)
|
||||||
|
|
||||||
@@ -504,11 +512,17 @@ export function usePushToCalendar(weekStart: string): PushToCalendar {
|
|||||||
for (const mutation of week) mutations.remove(mutation)
|
for (const mutation of week) mutations.remove(mutation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function settle() {
|
||||||
|
const shown = mutations.findAll({ mutationKey }).find((mutation) => mutation.mutationId === latest?.mutationId)
|
||||||
|
if (shown !== undefined) mutations.remove(shown)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pending: pushes.some((push) => push.status === 'pending'),
|
pending: pushes.some((push) => push.status === 'pending'),
|
||||||
error: latest?.status === 'error' ? latest.error! : null,
|
error: latest?.status === 'error' ? latest.error! : null,
|
||||||
supplyRun: latest?.status === 'success' ? latest.data!.supply_run : null,
|
supplyRun: latest?.status === 'success' ? latest.data!.supply_run : null,
|
||||||
mutate,
|
mutate,
|
||||||
reset,
|
reset,
|
||||||
|
settle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user