@jeremyckahn/farmhand
Version:
A farming game
51 lines (40 loc) • 1.93 kB
text/typescript
import { screen, waitFor } from '@testing-library/react'
import { farmhandStub } from '../test-utils/stubs/farmhandStub.js'
// Regression coverage for the isMounted guard added to the post-increment-day
// effect in useFarmhand.ts. Before that guard existed, unmounting while an
// incrementDay's persistence promise was still in flight could run the
// effect's continuation (further setState calls and notifications) against a
// torn-down instance. This forces that race by holding the persistence
// promise open across an unmount, then resolving it afterward.
describe('unmount safety', () => {
test('does not error or leave a pending rejection when unmounted mid-incrementDay', async () => {
let resolveSetItem: (value?: unknown) => void = () => {}
const setItemPromise = new Promise(resolve => {
resolveSetItem = resolve
})
const { unmount } = await farmhandStub({
localforage: {
getItem: () => Promise.resolve(null),
setItem: (_key: string, data: unknown) =>
setItemPromise.then(() => data),
},
})
// The fresh-game boot path calls incrementDay(true), which schedules the
// post-increment-day effect. By the time "Day 1" renders, that effect's
// async continuation is already awaiting localforage.setItem (the mock
// above), i.e. it's suspended exactly where the isMounted guard matters.
await waitFor(() => {
expect(screen.getByText('Day 1', { exact: false })).toBeInTheDocument()
})
const consoleError = vitest
.spyOn(console, 'error')
.mockImplementation(() => {})
unmount()
resolveSetItem()
// Flush the now-resolved promise's continuation (the effect's `try`/
// `finally` body) so any errors it throws surface within this test.
await new Promise(resolve => setTimeout(resolve, 0))
expect(consoleError).not.toHaveBeenCalled()
consoleError.mockRestore()
})
})