import { describe, expect, it } from 'vitest'; import type { ResolvedDay, ShopStatus, ShopStatusKind } from '@/lib/schedule/types'; import { CHANGE_MARGIN_SEC, MAX_REFRESH_SEC, MIN_REFRESH_SEC, computeRefreshRate, } from './refresh'; const ZURICH = 'Europe/Zurich'; const OPEN_SEC = 600; const CLOSED_SEC = 7200; const DAY: ResolvedDay = { date: '2026-09-22', dayOfWeek: 2, isOpen: true, slots: [], isException: false, exceptionKind: null, noteFr: null, noteEn: null, }; function status( kind: ShopStatusKind, nextChangeAt: ShopStatus['nextChangeAt'] = null, ): ShopStatus { return { status: kind, today: DAY, nextChangeAt, nextOpening: nextChangeAt }; } function rate(now: string, value: ShopStatus): number { return computeRefreshRate({ now: new Date(now), status: value, timezone: ZURICH, openSec: OPEN_SEC, closedSec: CLOSED_SEC, }); } describe('computeRefreshRate', () => { it('uses the short interval while the shop is trading', () => { // 12:00 local, closing at 18:30: far from any change, so the base wins. expect(rate('2026-09-22T10:00:00Z', status('OPEN', { date: '2026-09-22', time: '18:30' }))).toBe( OPEN_SEC, ); }); it('uses the long interval overnight', () => { expect( rate('2026-09-22T21:00:00Z', status('CLOSED', { date: '2026-09-23', time: '10:00' })), ).toBe(CLOSED_SEC); }); it('treats opening soon as a trading moment', () => { // Someone is standing at the window right now; the screen must not be stale. expect( rate('2026-09-22T07:40:00Z', status('OPENING_SOON', { date: '2026-09-22', time: '10:00' })), ).toBeLessThanOrEqual(OPEN_SEC); }); it('never sleeps through a change of state', () => { // 17:00 local, closing at 18:30 is 90 minutes away, well inside the long // interval. The device must still wake just after the change. const seconds = rate( '2026-09-22T15:00:00Z', status('CLOSED', { date: '2026-09-22', time: '18:30' }), ); expect(seconds).toBe(Math.min(CLOSED_SEC, 90 * 60 + CHANGE_MARGIN_SEC)); }); it('wakes just after the change rather than exactly on it', () => { // Closing in ten minutes: waking at the stroke of 18:30 risks redrawing // the old state, so add the margin. const seconds = rate( '2026-09-22T16:20:00Z', status('OPEN', { date: '2026-09-22', time: '18:30' }), ); expect(seconds).toBe(130 * 60 > OPEN_SEC ? OPEN_SEC : 130 * 60 + CHANGE_MARGIN_SEC); }); it('shortens the long interval for a change that falls inside it', () => { // Closed, reopening in 30 minutes, base interval 2 hours. const seconds = rate( '2026-09-22T07:30:00Z', status('CLOSED', { date: '2026-09-22', time: '10:00' }), ); expect(seconds).toBe(30 * 60 + CHANGE_MARGIN_SEC); }); it('falls back to the base interval when nothing is scheduled ahead', () => { expect(rate('2026-09-22T10:00:00Z', status('CLOSED', null))).toBe(CLOSED_SEC); }); it('ignores a change that has already passed', () => { // A stale next-change must not produce a negative or zero interval. expect( rate('2026-09-22T16:00:00Z', status('CLOSED', { date: '2026-09-22', time: '10:00' })), ).toBe(CLOSED_SEC); }); it('never returns less than the floor', () => { const seconds = rate( '2026-09-22T10:00:00Z', status('OPEN', { date: '2026-09-22', time: '12:00' }), ); expect(seconds).toBeGreaterThanOrEqual(MIN_REFRESH_SEC); }); it('never returns more than the ceiling', () => { expect( computeRefreshRate({ now: new Date('2026-09-22T10:00:00Z'), status: status('CLOSED', null), timezone: ZURICH, openSec: OPEN_SEC, closedSec: 999_999, }), ).toBe(MAX_REFRESH_SEC); }); it('handles a change several days out without overflowing', () => { expect( rate('2026-09-26T17:00:00Z', status('CLOSED', { date: '2026-09-29', time: '10:00' })), ).toBe(CLOSED_SEC); }); });