import { act, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { __resetElapsedTimerRegistryForTests, useElapsedSeconds, useMeasuredDuration } from './activity-timer'
function Probe({ active, since, timerKey }: { active: boolean; since?: number; timerKey?: string }) {
const elapsed = useElapsedSeconds(active, timerKey, since)
return {elapsed}
}
function DurationProbe({ active, timerKey }: { active: boolean; timerKey: string }) {
const measured = useMeasuredDuration(active, timerKey)
return {measured === null ? 'unknown' : measured}
}
describe('useElapsedSeconds', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'))
vi.spyOn(document, 'hasFocus').mockReturnValue(true)
__resetElapsedTimerRegistryForTests()
})
afterEach(() => {
vi.restoreAllMocks()
vi.useRealTimers()
__resetElapsedTimerRegistryForTests()
})
it('keeps elapsed time stable across remounts for the same key', () => {
const first = render()
act(() => {
vi.advanceTimersByTime(5_000)
})
expect(screen.getByTestId('elapsed').textContent).toBe('5')
first.unmount()
act(() => {
vi.advanceTimersByTime(3_000)
})
render()
expect(screen.getByTestId('elapsed').textContent).toBe('8')
})
it('counts from an explicit epoch rather than mount time', () => {
const mountedAt = Date.now()
act(() => {
vi.advanceTimersByTime(30_000)
})
render()
expect(screen.getByTestId('elapsed').textContent).toBe('2')
})
it('re-anchors when the epoch moves', () => {
const { rerender } = render()
act(() => {
vi.advanceTimersByTime(10_000)
})
expect(screen.getByTestId('elapsed').textContent).toBe('10')
rerender()
expect(screen.getByTestId('elapsed').textContent).toBe('0')
})
it('pauses UI ticks without focus and catches up immediately on return', () => {
render()
vi.mocked(document.hasFocus).mockReturnValue(false)
window.dispatchEvent(new Event('blur'))
act(() => {
vi.advanceTimersByTime(5_000)
})
expect(screen.getByTestId('elapsed').textContent).toBe('0')
vi.mocked(document.hasFocus).mockReturnValue(true)
act(() => window.dispatchEvent(new Event('focus')))
expect(screen.getByTestId('elapsed').textContent).toBe('5')
})
})
describe('useMeasuredDuration', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'))
vi.spyOn(document, 'hasFocus').mockReturnValue(true)
__resetElapsedTimerRegistryForTests()
})
afterEach(() => {
vi.restoreAllMocks()
vi.useRealTimers()
__resetElapsedTimerRegistryForTests()
})
it('has nothing to report until it has watched something finish', () => {
render()
act(() => {
vi.advanceTimersByTime(4_000)
})
expect(screen.getByTestId('measured').textContent).toBe('unknown')
})
it('freezes the duration at the moment the thing finishes', () => {
const { rerender } = render()
act(() => {
vi.advanceTimersByTime(4_000)
})
rerender()
expect(screen.getByTestId('measured').textContent).toBe('4')
// Time keeps passing; the block is over and its duration must not creep up
// with it.
act(() => {
vi.advanceTimersByTime(9_000)
})
expect(screen.getByTestId('measured').textContent).toBe('4')
})
// The thread virtualizes, so the component that watched a block finish is
// usually gone by the time anyone scrolls back to read it.
it('remembers the duration for a component that mounts after the fact', () => {
const first = render()
act(() => {
vi.advanceTimersByTime(6_000)
})
first.rerender()
first.unmount()
render()
expect(screen.getByTestId('measured').textContent).toBe('6')
})
it('measures each key separately', () => {
const first = render()
act(() => {
vi.advanceTimersByTime(3_000)
})
first.rerender()
first.unmount()
const second = render()
act(() => {
vi.advanceTimersByTime(2_000)
})
second.rerender()
expect(screen.getByTestId('measured').textContent).toBe('2')
})
it('records the real finish time even if the UI clock was paused', () => {
const probe = render()
vi.mocked(document.hasFocus).mockReturnValue(false)
window.dispatchEvent(new Event('blur'))
act(() => {
vi.advanceTimersByTime(5_000)
})
probe.rerender()
expect(screen.getByTestId('measured').textContent).toBe('5')
})
})