From 433ff388e0fed2a3e91d7e6c24783b805cfe82c5 Mon Sep 17 00:00:00 2001 From: dev_agent Date: Sat, 10 Oct 2026 21:47:09 -0400 Subject: [PATCH] Drive the offline-queue tests by event-loop turns, not real time The tests still waited on real time: the 20 ms and 50 ms pauses behind the "nothing more is sent" checks, Testing Library's waits (a real setInterval and a real end-of-wait setTimeout), and React Query's notices on a real setTimeout of 0. Under load, whether the ordered-replay check saw a PATCH sent too soon depended on when a real timer fired. The tests now wait with until, which lets the page take turns of the event loop (setImmediate inside act) until a check passes, and settle, which lets it take a fixed number of turns before a "nothing more happens" check. No time passes in either, real or fake. React Query's notices go out a turn later on setImmediate, and advance moves the fake clock synchronously. The Testing Library wait wrapper override is gone. The replay assertions are unchanged. Task 880e924e-c248-472f-8e92-1baec5b4cc9f. Co-Authored-By: Claude Opus 5.5 --- web/src/shopping/offlineQueue.test.tsx | 207 ++++++++++++------------- 1 file changed, 103 insertions(+), 104 deletions(-) diff --git a/web/src/shopping/offlineQueue.test.tsx b/web/src/shopping/offlineQueue.test.tsx index dc58a7c..8c8706f 100644 --- a/web/src/shopping/offlineQueue.test.tsx +++ b/web/src/shopping/offlineQueue.test.tsx @@ -1,7 +1,7 @@ import { defaultScheduler, notifyManager, QueryClientProvider } from '@tanstack/react-query' -import { act, configure, fireEvent, getConfig, render, screen, waitFor, within } from '@testing-library/react' +import { act, fireEvent, render, screen, within } from '@testing-library/react' import { MemoryRouter } from 'react-router' -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi, type Mock, type MockInstance } 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' @@ -213,17 +213,46 @@ function reload(page: { unmount: () => void }) { vi.setSystemTime(now) } -async function advance(ms: number) { - await act(() => vi.advanceTimersByTimeAsync(ms)) +// advance jumps the clock past a deadline or a wait, firing the timers due +// by then; what they set off runs as later turns (until, settle) let it. +function advance(ms: number) { + act(() => vi.advanceTimersByTime(ms)) } -// The real setTimeout, taken before any test fakes it. -const realSetTimeout = globalThis.setTimeout +// The host's setImmediate, which these tests do not fake: it runs its +// callback on the next turn of the event loop, once every promise already +// resolved has run, whatever the clock says. +const nextTurn = (globalThis as unknown as { setImmediate: (callback: () => void) => void }).setImmediate -// pause lets ms of real time pass, the fake clock standing still: time for -// work that must not happen, such as a PATCH sent too soon, to show itself. -function pause(ms: number) { - return new Promise((resolve) => realSetTimeout(resolve, ms)) +// turn lets the page take one turn of the event loop, inside act: the +// promises it has resolved run, and React renders and runs the effects they +// cause. No time passes, real or fake. +async function turn() { + await act(() => new Promise((resolve) => nextTurn(resolve))) +} + +// TURNS bounds how many turns until waits and settle takes: far more than +// any change here needs to reach the page. +const TURNS = 50 + +// until lets the page take turns until check passes, and returns what check +// returns; it fails with check's error if check still fails after TURNS. +async function until(check: () => T): Promise { + for (let turns = 0; ; turns++) { + try { + return check() + } catch (error) { + if (turns === TURNS) throw error + } + await turn() + } +} + +// settle lets the page take TURNS turns, the clock standing still: whatever +// it would do without a deadline or a wait passing, such as send a PATCH too +// soon, it has done. +async function settle() { + for (let turns = 0; turns < TURNS; turns++) await turn() } // The page's setTimeout, watched: retrySecondTry waits for a timer to be set. @@ -233,40 +262,10 @@ let timers: MockInstance // 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) + await until(() => expect(timers).toHaveBeenCalledWith(expect.any(Function), 2 * RETRY_FIRST)) + advance(2 * RETRY_FIRST) } -// React's act environment flag, which Testing Library turns off while it waits. -const actEnvironment = globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean } - -// Testing Library's own asyncWrapper, put back after these tests. -let testingLibraryAsyncWrapper: ReturnType['asyncWrapper'] - -beforeAll(() => { - testingLibraryAsyncWrapper = getConfig().asyncWrapper - // As Testing Library's own, which ends each wait with a setTimeout of 0 - // that the fake clock, standing still, would never fire: this one ends it - // with the real setTimeout. - configure({ - asyncWrapper: async (callback) => { - const previous = actEnvironment.IS_REACT_ACT_ENVIRONMENT - actEnvironment.IS_REACT_ACT_ENVIRONMENT = false - try { - const result: unknown = await callback() - await pause(0) - return result - } finally { - actEnvironment.IS_REACT_ACT_ENVIRONMENT = previous - } - }, - }) -}) - -afterAll(() => { - configure({ asyncWrapper: testingLibraryAsyncWrapper }) -}) - function checkbox(name: string) { return screen.getByRole('checkbox', { name: new RegExp(`^${name}`) }) } @@ -282,11 +281,11 @@ beforeEach(() => { // The clock and setTimeout move only when a test jumps past a deadline or // a wait (advance), never with real time: under load, real time would fire // a deadline or a wait the test never reached. React Query sends its - // batched notices on a real setTimeout of 0, as it does in the app, so the - // page renders its changes without the clock moving. + // batched notices a turn later, as it does in the app on a setTimeout of 0, + // so the page renders its changes without the clock moving. vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] }) vi.setSystemTime(new Date('2026-10-07T16:00:00Z')) - notifyManager.setScheduler((callback) => realSetTimeout(callback, 0)) + notifyManager.setScheduler(nextTurn) timers = vi.spyOn(globalThis, 'setTimeout') }) @@ -302,7 +301,7 @@ describe('offline toggle queue', () => { it('applies toggles made offline at once, saves them in order and sends them in that order once online', async () => { const { fetchMock } = stubServer() await openPage() - expect(await screen.findByText('2 / 3')).toBeInTheDocument() + expect(await until(() => screen.getByText('2 / 3'))).toBeInTheDocument() goOffline() fireEvent.click(checkbox('Taco seasoning')) @@ -320,7 +319,7 @@ describe('offline toggle queue', () => { expect(patches(fetchMock)).toEqual([]) goOnline() - await waitFor(() => expect(queue()).toEqual([])) + await until(() => expect(queue()).toEqual([])) expect(patches(fetchMock)).toEqual([ `${ITEM('i3')} {"needed":true}`, `${ITEM('i1')} {"needed":false}`, @@ -336,14 +335,14 @@ describe('offline toggle queue', () => { it('queues a toggle whose PATCH fails with a network error and sends it again once back online', async () => { const { fetchMock } = stubServer() await openPage() - fireEvent.click(await screen.findByRole('checkbox', { name: 'Taco seasoning' })) - await waitFor(() => expect(queue()).toEqual([])) + fireEvent.click(await until(() => screen.getByRole('checkbox', { name: 'Taco seasoning' }))) + await until(() => expect(queue()).toEqual([])) expect(screen.getByText('3 / 3')).toBeInTheDocument() // The browser still says online, but the server cannot be reached. reachable = false fireEvent.click(checkbox('Corn tortillas')) - expect(await within(row('Corn tortillas')).findByText('will sync')).toBeInTheDocument() + expect(await until(() => within(row('Corn tortillas')).getByText('will sync'))).toBeInTheDocument() expect(patches(fetchMock)).toEqual([`${ITEM('i3')} {"needed":true}`, `${ITEM('i2')} {"needed":false}`]) expect(checkbox('Corn tortillas')).not.toBeChecked() expect(screen.getByText('2 / 3')).toBeInTheDocument() @@ -351,7 +350,7 @@ describe('offline toggle queue', () => { expect(screen.queryByRole('alert')).not.toBeInTheDocument() goOnline() - await waitFor(() => expect(queue()).toEqual([])) + await until(() => expect(queue()).toEqual([])) expect(patches(fetchMock)).toEqual([`${ITEM('i3')} {"needed":true}`, `${ITEM('i2')} {"needed":false}`, `${ITEM('i2')} {"needed":false}`]) expect(screen.getByText('2 / 3')).toBeInTheDocument() expect(screen.queryByText('will sync')).not.toBeInTheDocument() @@ -360,7 +359,7 @@ describe('offline toggle queue', () => { it('opens the saved list after a reload offline and replays the saved toggles in order once online', async () => { const { fetchMock } = stubServer() const first = await openPage() - expect(await screen.findByText('2 / 3')).toBeInTheDocument() + expect(await until(() => screen.getByText('2 / 3'))).toBeInTheDocument() goOffline() fireEvent.click(checkbox('Taco seasoning')) fireEvent.click(checkbox('Ground beef')) @@ -369,7 +368,7 @@ describe('offline toggle queue', () => { // Reload while offline: the settings, /me and the list cannot be read. reload(first) await openPage() - expect(await screen.findByText('2 / 3')).toBeInTheDocument() + expect(await until(() => screen.getByText('2 / 3'))).toBeInTheDocument() expect(checkbox('Taco seasoning')).toBeChecked() expect(checkbox('Ground beef')).not.toBeChecked() expect(within(row('Taco seasoning')).getByText('will sync')).toBeInTheDocument() @@ -377,9 +376,9 @@ describe('offline toggle queue', () => { expect(patches(fetchMock)).toEqual([]) goOnline() - await waitFor(() => expect(queue()).toEqual([])) + await until(() => expect(queue()).toEqual([])) expect(patches(fetchMock)).toEqual([`${ITEM('i3')} {"needed":true}`, `${ITEM('i1')} {"needed":false}`]) - await waitFor(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) + await until(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) expect(screen.getByText('2 / 3')).toBeInTheDocument() expect(checkbox('Taco seasoning')).toBeChecked() expect(checkbox('Ground beef')).not.toBeChecked() @@ -388,7 +387,7 @@ describe('offline toggle queue', () => { it('discards a queued toggle the server refuses because the list was archived, puts the count back and says so', async () => { const server = stubServer(() => errorResponse(409, 'archived')) await openPage() - expect(await screen.findByText('2 / 3')).toBeInTheDocument() + expect(await until(() => screen.getByText('2 / 3'))).toBeInTheDocument() goOffline() fireEvent.click(checkbox('Taco seasoning')) @@ -396,25 +395,25 @@ describe('offline toggle queue', () => { server.archive() goOnline() - const alert = await screen.findByRole('alert') + const alert = await until(() => screen.getByRole('alert')) expect(alert).toHaveTextContent("Couldn't mark Taco seasoning to buy. archived message") - expect(await screen.findByText('Archived')).toBeInTheDocument() + expect(await until(() => screen.getByText('Archived'))).toBeInTheDocument() expect(screen.getByText('2 / 3')).toBeInTheDocument() expect(within(row('Taco seasoning')).getByText('have it')).toBeInTheDocument() expect(screen.queryByText('will sync')).not.toBeInTheDocument() expect(queue()).toEqual([]) // Sent once, never again. goOnline() - await waitFor(() => expect(patches(server.fetchMock)).toEqual([`${ITEM('i3')} {"needed":true}`])) + await until(() => expect(patches(server.fetchMock)).toEqual([`${ITEM('i3')} {"needed":true}`])) }) it('does not queue a toggle the server refuses: it is put back and says why', async () => { const server = stubServer(() => errorResponse(400, 'invalid')) await openPage() - expect(await screen.findByText('2 / 3')).toBeInTheDocument() + expect(await until(() => screen.getByText('2 / 3'))).toBeInTheDocument() fireEvent.click(checkbox('Ground beef')) - expect(await screen.findByRole('alert')).toHaveTextContent("Couldn't mark Ground beef “have it”. invalid message") + expect(await until(() => screen.getByRole('alert'))).toHaveTextContent("Couldn't mark Ground beef “have it”. invalid message") expect(checkbox('Ground beef')).toBeChecked() expect(screen.getByText('2 / 3')).toBeInTheDocument() expect(screen.queryByText('will sync')).not.toBeInTheDocument() @@ -422,7 +421,7 @@ describe('offline toggle queue', () => { goOffline() goOnline() - await waitFor(() => expect(patches(server.fetchMock)).toEqual([`${ITEM('i1')} {"needed":false}`])) + await until(() => expect(patches(server.fetchMock)).toEqual([`${ITEM('i1')} {"needed":false}`])) }) }) @@ -439,16 +438,16 @@ describe('offline toggle queue, replay', () => { localStorage.setItem(SAVED_KEY, JSON.stringify({ schedule: null, runs, queue: queued })) await openPage() - await waitFor(() => expect(answers.waiting()).toBe(1)) - fireEvent.click(await screen.findByRole('checkbox', { name: 'Corn tortillas' })) + await until(() => expect(answers.waiting()).toBe(1)) + fireEvent.click(await until(() => screen.getByRole('checkbox', { name: 'Corn tortillas' }))) for (let sent = 1; sent <= 4; sent++) { - await waitFor(() => expect(patches(server.fetchMock)).toHaveLength(sent)) + await until(() => expect(patches(server.fetchMock)).toHaveLength(sent)) // The next one is not sent before this one is answered. - await pause(20) + await settle() expect(patches(server.fetchMock)).toHaveLength(sent) act(() => answers.release()) } - await waitFor(() => expect(queue()).toEqual([])) + await until(() => expect(queue()).toEqual([])) expect(patches(server.fetchMock)).toEqual([ `${ITEM('i3')} {"needed":true}`, `${ITEM('n1')} {"needed":false}`, @@ -456,27 +455,27 @@ describe('offline toggle queue, replay', () => { `${ITEM('i2')} {"needed":false}`, ]) expect(server.mostInFlight()).toBe(1) - await waitFor(() => expect(screen.getByText('1 / 3')).toBeInTheDocument()) + await until(() => expect(screen.getByText('1 / 3')).toBeInTheDocument()) }) it('replays at least once: a toggle the server applied before a reload is sent again and leaves the item and count the same', async () => { const answers = held() const server = stubServer(undefined, answers.hold) const first = await openPage() - expect(await screen.findByText('2 / 3')).toBeInTheDocument() + expect(await until(() => screen.getByText('2 / 3'))).toBeInTheDocument() fireEvent.click(checkbox('Taco seasoning')) // The server applied it; the page reloads before its answer arrives. - await waitFor(() => expect(answers.waiting()).toBe(1)) + await until(() => expect(answers.waiting()).toBe(1)) expect(queue()).toEqual(['i3 true']) reload(first) act(() => answers.release()) await openPage() - await waitFor(() => expect(answers.waiting()).toBe(1)) + await until(() => expect(answers.waiting()).toBe(1)) act(() => answers.release()) - await waitFor(() => expect(queue()).toEqual([])) + await until(() => expect(queue()).toEqual([])) expect(patches(server.fetchMock)).toEqual([`${ITEM('i3')} {"needed":true}`, `${ITEM('i3')} {"needed":true}`]) - await waitFor(() => expect(screen.getByText('3 / 3')).toBeInTheDocument()) + await until(() => expect(screen.getByText('3 / 3')).toBeInTheDocument()) expect(checkbox('Taco seasoning')).toBeChecked() expect(server.run('2026-10-05')).toMatchObject({ needed_count: 3, total_count: 3 }) expect(screen.queryByRole('alert')).not.toBeInTheDocument() @@ -486,18 +485,18 @@ describe('offline toggle queue, replay', () => { const answers = held() stubServer(undefined, answers.hold) await openPage() - expect(await screen.findByText('2 / 3')).toBeInTheDocument() + expect(await until(() => screen.getByText('2 / 3'))).toBeInTheDocument() goOffline() fireEvent.click(checkbox('Ground beef')) expect(within(row('Ground beef')).getByText('will sync')).toBeInTheDocument() goOnline() - await waitFor(() => expect(answers.waiting()).toBe(1)) + await until(() => expect(answers.waiting()).toBe(1)) expect(within(row('Ground beef')).getByText('will sync')).toBeInTheDocument() expect(queue()).toEqual(['i1 false']) act(() => answers.release()) - await waitFor(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) + await until(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) expect(queue()).toEqual([]) expect(screen.getByText('1 / 3')).toBeInTheDocument() }) @@ -507,29 +506,29 @@ describe('offline toggle queue, replay', () => { // The first PATCH never answers; every later one answers at once. const server = stubServer(undefined, () => (++tries === 1 ? new Promise(() => {}) : Promise.resolve())) await openPage() - expect(await screen.findByText('2 / 3')).toBeInTheDocument() + expect(await until(() => screen.getByText('2 / 3'))).toBeInTheDocument() fireEvent.click(checkbox('Taco seasoning')) - await waitFor(() => expect(patches(server.fetchMock)).toHaveLength(1)) + await until(() => expect(patches(server.fetchMock)).toHaveLength(1)) fireEvent.click(checkbox('Ground beef')) expect(queue()).toEqual(['i3 true', 'i1 false']) // Past the deadline the hung PATCH is aborted; both still wait, in order. - await advance(TOGGLE_DEADLINE) - expect(await within(row('Taco seasoning')).findByText('will sync')).toBeInTheDocument() + advance(TOGGLE_DEADLINE) + expect(await until(() => within(row('Taco seasoning')).getByText('will sync'))).toBeInTheDocument() expect(within(row('Ground beef')).getByText('will sync')).toBeInTheDocument() expect(patches(server.fetchMock)).toHaveLength(1) expect(queue()).toEqual(['i3 true', 'i1 false']) await retrySecondTry() - await waitFor(() => expect(queue()).toEqual([])) + await until(() => expect(queue()).toEqual([])) expect(patches(server.fetchMock)).toEqual([ `${ITEM('i3')} {"needed":true}`, `${ITEM('i3')} {"needed":true}`, `${ITEM('i1')} {"needed":false}`, ]) expect(server.mostInFlight()).toBe(1) - await waitFor(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) + await until(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) expect(screen.getByText('2 / 3')).toBeInTheDocument() expect(screen.queryByRole('alert')).not.toBeInTheDocument() }) @@ -540,26 +539,26 @@ describe('offline toggle queue, replay', () => { // answers. Every later /me answers at once. const server = stubServer(undefined, undefined, () => (++tries === 2 ? new Promise(() => {}) : Promise.resolve())) await openPage() - expect(await screen.findByText('2 / 3')).toBeInTheDocument() + expect(await until(() => screen.getByText('2 / 3'))).toBeInTheDocument() fireEvent.click(checkbox('Taco seasoning')) - await waitFor(() => expect(tries).toBe(2)) + await until(() => expect(tries).toBe(2)) fireEvent.click(checkbox('Ground beef')) expect(queue()).toEqual(['i3 true', 'i1 false']) // Past the deadline the hung /me is abandoned; both still wait, in order. - await advance(TOGGLE_DEADLINE) - expect(await within(row('Taco seasoning')).findByText('will sync')).toBeInTheDocument() + advance(TOGGLE_DEADLINE) + expect(await until(() => within(row('Taco seasoning')).getByText('will sync'))).toBeInTheDocument() expect(within(row('Ground beef')).getByText('will sync')).toBeInTheDocument() expect(patches(server.fetchMock)).toEqual([]) // The next try sends a fresh /me, which the server answers. await retrySecondTry() - await waitFor(() => expect(queue()).toEqual([])) + await until(() => expect(queue()).toEqual([])) expect(mes(server.fetchMock)).toBe(3) expect(patches(server.fetchMock)).toEqual([`${ITEM('i3')} {"needed":true}`, `${ITEM('i1')} {"needed":false}`]) expect(server.mostInFlight()).toBe(1) - await waitFor(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) + await until(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) expect(screen.getByText('2 / 3')).toBeInTheDocument() expect(screen.queryByRole('alert')).not.toBeInTheDocument() }) @@ -580,29 +579,29 @@ describe('offline toggle queue, replay', () => { () => (refusedAt !== null && ++tries === 1 ? new Promise(() => {}) : Promise.resolve()), ) await openPage() - expect(await screen.findByText('2 / 3')).toBeInTheDocument() + expect(await until(() => screen.getByText('2 / 3'))).toBeInTheDocument() fireEvent.click(checkbox('Taco seasoning')) - await waitFor(() => expect(tries).toBe(1)) + await until(() => expect(tries).toBe(1)) fireEvent.click(checkbox('Ground beef')) expect(patches(server.fetchMock)).toHaveLength(1) // Past the deadline the hung refresh is abandoned; both still wait, in order. - await advance(TOGGLE_DEADLINE) - expect(await within(row('Taco seasoning')).findByText('will sync')).toBeInTheDocument() + advance(TOGGLE_DEADLINE) + expect(await until(() => within(row('Taco seasoning')).getByText('will sync'))).toBeInTheDocument() expect(within(row('Ground beef')).getByText('will sync')).toBeInTheDocument() expect(queue()).toEqual(['i3 true', 'i1 false']) // The next try is refused again and sends a fresh refresh, which answers. await retrySecondTry() - await waitFor(() => expect(queue()).toEqual([])) + await until(() => expect(queue()).toEqual([])) expect(patches(server.fetchMock)).toEqual([ `${ITEM('i3')} {"needed":true}`, `${ITEM('i3')} {"needed":true}`, `${ITEM('i3')} {"needed":true}`, `${ITEM('i1')} {"needed":false}`, ]) - await waitFor(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) + await until(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) expect(screen.getByText('2 / 3')).toBeInTheDocument() expect(screen.queryByRole('alert')).not.toBeInTheDocument() }) @@ -610,21 +609,21 @@ describe('offline toggle queue, replay', () => { it('tries a toggle again once the server is back, with no online event from the browser', async () => { const { fetchMock } = stubServer() await openPage() - expect(await screen.findByText('2 / 3')).toBeInTheDocument() + expect(await until(() => screen.getByText('2 / 3'))).toBeInTheDocument() // The browser says online throughout; the server cannot be reached, then can. reachable = false fireEvent.click(checkbox('Corn tortillas')) - expect(await within(row('Corn tortillas')).findByText('will sync')).toBeInTheDocument() + expect(await until(() => within(row('Corn tortillas')).getByText('will sync'))).toBeInTheDocument() reachable = true expect(patches(fetchMock)).toEqual([]) // Nothing says the server is back: only the wait's timer tries it again. await retrySecondTry() - await waitFor(() => expect(queue()).toEqual([])) + await until(() => expect(queue()).toEqual([])) // The first try failed at GET /api/v1/me, before its PATCH. expect(patches(fetchMock)).toEqual([`${ITEM('i2')} {"needed":false}`]) - await waitFor(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) + await until(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) expect(screen.getByText('1 / 3')).toBeInTheDocument() }) @@ -632,20 +631,20 @@ describe('offline toggle queue, replay', () => { const answers = held() stubServer(undefined, answers.hold) const page = await openPage() - expect(await screen.findByText('2 / 3')).toBeInTheDocument() + expect(await until(() => screen.getByText('2 / 3'))).toBeInTheDocument() goOffline() fireEvent.click(checkbox('Ground beef')) expect(within(row('Ground beef')).getByText('will sync')).toBeInTheDocument() page.leave() goOnline() - await waitFor(() => expect(answers.waiting()).toBe(1)) + await until(() => expect(answers.waiting()).toBe(1)) page.reopen() - expect(await within(row('Ground beef')).findByText('will sync')).toBeInTheDocument() + expect(await until(() => within(row('Ground beef')).getByText('will sync'))).toBeInTheDocument() expect(queue()).toEqual(['i1 false']) act(() => answers.release()) - await waitFor(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) + await until(() => expect(screen.queryByText('will sync')).not.toBeInTheDocument()) expect(queue()).toEqual([]) expect(screen.getByText('1 / 3')).toBeInTheDocument() }) @@ -657,10 +656,10 @@ describe('offline toggle queue, replay', () => { const getItem = vi.spyOn(Storage.prototype, 'getItem') await openPage() - expect(await screen.findByText(/Couldn't load the household settings/)).toBeInTheDocument() + expect(await until(() => screen.getByText(/Couldn't load the household settings/))).toBeInTheDocument() expect(screen.getByText(/can't be kept on this device while offline/)).toBeInTheDocument() const reads = getItem.mock.calls.filter(([key]) => key === SAVED_KEY).length - await pause(50) + await settle() expect(getItem.mock.calls.filter(([key]) => key === SAVED_KEY)).toHaveLength(reads) }) })