From 248870897cc24ea24d79b48d4efb35680f6f4626 Mon Sep 17 00:00:00 2001 From: dev_agent Date: Thu, 8 Oct 2026 12:47:35 -0400 Subject: [PATCH] Wait for a toggle's retry timer before jumping past it in the offline queue tests The four tests that retry a toggle after a failed first try advanced the fake clock by RETRY_FIRST, but the wait before a second try is RETRY_FIRST doubled once (2000 ms). The retry only fired because shouldAdvanceTime let real time push the clock the rest of the way inside waitFor's 1000 ms timeout, a race that CI lost once (run 3860). retrySecondTry now waits until that 2 * RETRY_FIRST timer is set, then advances past it, so the jump can no longer land before the timer. The no-online-event test also asserts nothing was sent before the timer fires. Co-Authored-By: Claude Opus 5.5 --- web/src/shopping/offlineQueue.test.tsx | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/web/src/shopping/offlineQueue.test.tsx b/web/src/shopping/offlineQueue.test.tsx index 2f64ec5..1ab93fc 100644 --- a/web/src/shopping/offlineQueue.test.tsx +++ b/web/src/shopping/offlineQueue.test.tsx @@ -1,7 +1,7 @@ import { QueryClientProvider } from '@tanstack/react-query' import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import { MemoryRouter } from 'react-router' -import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi, type Mock, type MockInstance } from 'vitest' import { ApiProvider, createApiClient, createQueryClient, createVerifiedSession } from '../api/index.ts' import { createNetworkStatus, type NetworkWindow } from '../pwa/networkStatus.ts' import { HOUSEHOLD_SETTINGS } from '../test/ApiProviders.tsx' @@ -211,6 +211,17 @@ async function advance(ms: number) { await act(() => vi.advanceTimersByTimeAsync(ms)) } +// The page's setTimeout, watched: retrySecondTry waits for a timer to be set. +let timers: MockInstance + +// retrySecondTry jumps past the wait before a toggle's second try — RETRY_FIRST +// doubled by its failed first try — once that wait's timer is set, so the jump +// never lands before the timer it is meant to fire. +async function retrySecondTry() { + await waitFor(() => expect(timers).toHaveBeenCalledWith(expect.any(Function), 2 * RETRY_FIRST)) + await advance(2 * RETRY_FIRST) +} + function checkbox(name: string) { return screen.getByRole('checkbox', { name: new RegExp(`^${name}`) }) } @@ -227,6 +238,7 @@ beforeEach(() => { // a wait (advance). vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'], shouldAdvanceTime: true }) vi.setSystemTime(new Date('2026-10-07T16:00:00Z')) + timers = vi.spyOn(globalThis, 'setTimeout') }) afterEach(() => { @@ -459,7 +471,7 @@ describe('offline toggle queue, replay', () => { expect(patches(server.fetchMock)).toHaveLength(1) expect(queue()).toEqual(['i3 true', 'i1 false']) - await advance(RETRY_FIRST) + await retrySecondTry() await waitFor(() => expect(queue()).toEqual([])) expect(patches(server.fetchMock)).toEqual([ `${ITEM('i3')} {"needed":true}`, @@ -492,7 +504,7 @@ describe('offline toggle queue, replay', () => { expect(patches(server.fetchMock)).toEqual([]) // The next try sends a fresh /me, which the server answers. - await advance(RETRY_FIRST) + await retrySecondTry() await waitFor(() => expect(queue()).toEqual([])) expect(mes(server.fetchMock)).toBe(3) expect(patches(server.fetchMock)).toEqual([`${ITEM('i3')} {"needed":true}`, `${ITEM('i1')} {"needed":false}`]) @@ -532,7 +544,7 @@ describe('offline toggle queue, replay', () => { expect(queue()).toEqual(['i3 true', 'i1 false']) // The next try is refused again and sends a fresh refresh, which answers. - await advance(RETRY_FIRST) + await retrySecondTry() await waitFor(() => expect(queue()).toEqual([])) expect(patches(server.fetchMock)).toEqual([ `${ITEM('i3')} {"needed":true}`, @@ -555,8 +567,10 @@ describe('offline toggle queue, replay', () => { fireEvent.click(checkbox('Corn tortillas')) expect(await within(row('Corn tortillas')).findByText('will sync')).toBeInTheDocument() reachable = true + expect(patches(fetchMock)).toEqual([]) - await advance(RETRY_FIRST) + // Nothing says the server is back: only the wait's timer tries it again. + await retrySecondTry() await waitFor(() => expect(queue()).toEqual([])) // The first try failed at GET /api/v1/me, before its PATCH. expect(patches(fetchMock)).toEqual([`${ITEM('i2')} {"needed":false}`])