Keep Push closed over a cached plan; bound kept week mutations
CI / ci (push) Successful in 4m21s
CI / ci (pull_request) Successful in 4m21s

A plan_changed reread answered from the service worker's cache left the
old plan shown with Push and Retry open. Push now needs a plan read from
the server (Plan.cached, set from cacheFallbackHeader), and the push
failure's Retry reads the plan again while the plan shown is cached.

Settled swaps and pushes were kept for every week ever touched. When a
week is shown, the settled ones of every other week with none pending are
now removed; pending ones still survive leaving the week and coming back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 01:45:33 -04:00
co-authored by Claude Opus 5.5
parent f77960ad05
commit 8a02e04110
4 changed files with 171 additions and 12 deletions
+68 -1
View File
@@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import { ApiProvider, createApiClient, createQueryClient, createVerifiedSession } from '../api/index.ts'
import { createNetworkStatus } from '../pwa/networkStatus.ts'
import { errorResponse, fakeLocation, jsonResponse, ME } from '../test/fetchStub.ts'
import { planKey, usePlan, useSwapDay, type Plan } from './api.ts'
import { planKey, usePlan, usePushToCalendar, useSwapDay, type Plan } from './api.ts'
const WEEK = '2026-10-05'
const PLAN = `/api/v1/plans/${WEEK}`
@@ -203,3 +203,70 @@ describe('useSwapDay', () => {
await act(async () => friday.send(jsonResponse(200, planBody())))
})
})
describe('week mutations', () => {
const NEXT = '2026-10-12'
const LATER = '2026-10-19'
// weekMutations lists the swaps and pushes the mutation cache holds for
// the week starting on weekStart, with their status.
function weekMutations(queryClient: QueryClient, weekStart: string) {
return queryClient
.getMutationCache()
.findAll({ mutationKey: ['plans', weekStart] })
.map((mutation) => `${String(mutation.options.mutationKey![2])} ${mutation.state.status}`)
}
it('keeps settled swaps and pushes while their week is shown, and leaves them behind once it is not, unless one is pending', async () => {
const nextPush = deferred()
stubServer({
[`GET ${PLAN}`]: () => jsonResponse(200, planBody()),
[`PUT ${FRIDAY}`]: () => jsonResponse(200, planBody({ 4: { id: 'q1', name: 'Lemon orzo', is_new: true } })),
[`POST ${PLAN}/push-calendar`]: () => errorResponse(500, 'internal'),
[`GET /api/v1/plans/${NEXT}`]: () => errorResponse(404, 'not_found'),
[`POST /api/v1/plans/${NEXT}/push-calendar`]: () => nextPush.reply,
[`GET /api/v1/plans/${LATER}`]: () => errorResponse(404, 'not_found'),
})
const api = createApiClient(fakeLocation('/plan'), createNetworkStatus(window), createVerifiedSession(localStorage))
await api.loadMe()
const queryClient = createQueryClient()
const wrapper = ({ children }: { children: ReactNode }) => (
<ApiProvider client={api}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</ApiProvider>
)
const hook = renderHook(
({ week }) => ({ plan: usePlan(week), swap: useSwapDay(week), push: usePushToCalendar(week) }),
{ wrapper, initialProps: { week: WEEK } },
)
await waitFor(() => expect(hook.result.current.plan.isSuccess).toBe(true))
// A swap and a push of the week settle: both stay while it is shown,
// the push's failure speaking for the week.
act(() => hook.result.current.swap.mutate(ORZO))
await waitFor(() => expect(hook.result.current.swap.pending).toBe(false))
act(() => hook.result.current.push.mutate())
await waitFor(() => expect(hook.result.current.push.error).not.toBeNull())
expect(weekMutations(queryClient, WEEK)).toEqual(['swap success', 'push error'])
// Another week shown leaves them behind; a push sent there stays
// pending across showing a third week.
hook.rerender({ week: NEXT })
await waitFor(() => expect(hook.result.current.plan.isSuccess).toBe(true))
expect(weekMutations(queryClient, WEEK)).toEqual([])
act(() => hook.result.current.push.mutate())
hook.rerender({ week: LATER })
await waitFor(() => expect(hook.result.current.plan.isSuccess).toBe(true))
expect(weekMutations(queryClient, NEXT)).toEqual(['push pending'])
hook.rerender({ week: NEXT })
expect(hook.result.current.push.pending).toBe(true)
// Once it settles, it stays while its week is shown and is left behind
// once another is.
await act(async () => nextPush.send(errorResponse(500, 'internal')))
await waitFor(() => expect(hook.result.current.push.error).not.toBeNull())
expect(weekMutations(queryClient, NEXT)).toEqual(['push error'])
hook.rerender({ week: LATER })
expect(weekMutations(queryClient, NEXT)).toEqual([])
})
})