diff --git a/src/__tests__/UpdateToast.test.tsx b/src/__tests__/UpdateToast.test.tsx new file mode 100644 index 0000000..4ebff28 --- /dev/null +++ b/src/__tests__/UpdateToast.test.tsx @@ -0,0 +1,63 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { render, cleanup } from '@testing-library/react' + +// Track the onRegistered callback so we can invoke it after render +let capturedOnRegistered: ((reg: unknown) => void) | undefined + +vi.mock('virtual:pwa-register/react', () => ({ + useRegisterSW: (opts: { onRegistered?: (reg: unknown) => void }) => { + capturedOnRegistered = opts.onRegistered + return { + needRefresh: [false, vi.fn()], + updateServiceWorker: vi.fn(), + } + }, +})) + +// Import after mock is set up +import UpdateToast from '../components/UpdateToast' + +describe('UpdateToast', () => { + beforeEach(() => { + vi.useFakeTimers() + capturedOnRegistered = undefined + }) + + afterEach(() => { + cleanup() + vi.useRealTimers() + }) + + it('clears the update-check interval on unmount', () => { + const fakeRegistration = { update: vi.fn() } + + const { unmount } = render() + + // Simulate the service worker registration callback + capturedOnRegistered?.(fakeRegistration) + + // Advance time to confirm the interval is active + vi.advanceTimersByTime(60 * 60 * 1000) + expect(fakeRegistration.update).toHaveBeenCalledTimes(1) + + // Unmount and advance again — no additional calls should happen + unmount() + vi.advanceTimersByTime(60 * 60 * 1000) + expect(fakeRegistration.update).toHaveBeenCalledTimes(1) + }) + + it('does not leak intervals across multiple mount/unmount cycles', () => { + const fakeRegistration = { update: vi.fn() } + + // Mount and unmount 3 times + for (let i = 0; i < 3; i++) { + const { unmount } = render() + capturedOnRegistered?.(fakeRegistration) + unmount() + } + + // After all unmounts, advancing time should trigger zero calls + vi.advanceTimersByTime(60 * 60 * 1000) + expect(fakeRegistration.update).toHaveBeenCalledTimes(0) + }) +}) diff --git a/src/components/UpdateToast.tsx b/src/components/UpdateToast.tsx index 1696b9c..d943d65 100644 --- a/src/components/UpdateToast.tsx +++ b/src/components/UpdateToast.tsx @@ -1,3 +1,4 @@ +import { useEffect, useRef } from 'react' import { useRegisterSW } from 'virtual:pwa-register/react' import { XIcon } from './Icons' @@ -32,16 +33,26 @@ export function UpdateToastView({ className = '', onReload, onDismiss }: UpdateT const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000 // 1 hour export default function UpdateToast() { + const intervalRef = useRef | null>(null) + const { needRefresh: [needRefresh, setNeedRefresh], updateServiceWorker, } = useRegisterSW({ onRegistered(registration) { if (!registration) return - setInterval(() => registration.update(), UPDATE_CHECK_INTERVAL_MS) + intervalRef.current = setInterval(() => registration.update(), UPDATE_CHECK_INTERVAL_MS) }, }) + useEffect(() => { + return () => { + if (intervalRef.current !== null) { + clearInterval(intervalRef.current) + } + } + }, []) + if (!needRefresh) return null return (