date-groups.test.ts3.4 KBView on GitHub
import { act, renderHook } from '@testing-library/react';
import {
  DATE_GROUP_ORDER,
  getDateGroup,
  startOfLocalDay,
  useLocalDayStart,
} from '@/modules/threads/threadList/utils/date-groups';

// Local-midnight boundary for a fixed "now". All fixtures below are expressed
// in local time so the assertions hold in any TZ the suite runs under.
const local = (y: number, m: number, d: number, h = 0, min = 0) =>
  new Date(y, m - 1, d, h, min).toISOString();

const todayStart = startOfLocalDay(new Date(2026, 6, 21, 10, 30)); // Jul 21 2026, local

describe('getDateGroup', () => {
  it('buckets by calendar day, not a rolling 24 hours', () => {
    // 18:55 yesterday is under 24h before 10:30 today, but it is still a
    // different calendar day — the case that put a Jul 20 thread under "Today".
    expect(getDateGroup(local(2026, 7, 20, 18, 55), todayStart)).toBe('Yesterday');
    // …and 00:05 today is only ~10h earlier yet belongs to "Today".
    expect(getDateGroup(local(2026, 7, 21, 0, 5), todayStart)).toBe('Today');
  });

  it('treats local midnight as the boundary', () => {
    expect(getDateGroup(local(2026, 7, 20, 23, 59), todayStart)).toBe('Yesterday');
    expect(getDateGroup(local(2026, 7, 21, 0, 0), todayStart)).toBe('Today');
  });

  it('covers the remaining buckets', () => {
    expect(getDateGroup(local(2026, 7, 21, 23, 59), todayStart)).toBe('Today');
    expect(getDateGroup(local(2026, 7, 19, 12, 0), todayStart)).toBe('Last 7 days');
    expect(getDateGroup(local(2026, 7, 15, 12, 0), todayStart)).toBe('Last 7 days');
    expect(getDateGroup(local(2026, 7, 14, 23, 59), todayStart)).toBe('Older');
  });

  it('falls back to Older for missing or unparseable dates', () => {
    expect(getDateGroup(undefined, todayStart)).toBe('Older');
    expect(getDateGroup('not-a-date', todayStart)).toBe('Older');
  });

  it('exposes every bucket in render order', () => {
    expect(DATE_GROUP_ORDER).toEqual(['Today', 'Yesterday', 'Last 7 days', 'Older']);
  });
});

// The headline fix: a tab left open across midnight must re-emit the day boundary, or a
// thread received yesterday evening keeps rendering under "Today" the next morning.
describe('useLocalDayStart', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });
  afterEach(() => {
    jest.useRealTimers();
  });

  it('re-emits the new local midnight when the clock rolls over', () => {
    // 30s before local midnight on Jul 21.
    jest.setSystemTime(new Date(2026, 6, 21, 23, 59, 30));

    const { result } = renderHook(() => useLocalDayStart());
    expect(result.current.getTime()).toBe(startOfLocalDay(new Date(2026, 6, 21)).getTime());

    // Cross local midnight: the armed timeout (fires ~1s past the boundary) runs.
    act(() => {
      jest.advanceTimersByTime(31_000 + 1_000);
    });

    expect(result.current.getTime()).toBe(startOfLocalDay(new Date(2026, 6, 22)).getTime());
  });

  it('does not churn the boundary when the tab wakes on the same day', () => {
    jest.setSystemTime(new Date(2026, 6, 21, 9, 0, 0));
    const { result } = renderHook(() => useLocalDayStart());
    const first = result.current;

    // A visibility re-check mid-morning recomputes the same day — the functional
    // setState must return the prior reference so it doesn't re-bucket the list.
    act(() => {
      document.dispatchEvent(new Event('visibilitychange'));
    });

    expect(result.current).toBe(first);
  });
});