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') }) })