An item's pickup window starts when it goes into its box. Once it runs out, the item is discarded without a refund and the box is freed; a box that's open at that moment is left alone. With no scheduler yet, reads, box actions and the kitchen simulation check for expired items first. The order card shows a live countdown under "Ready for pickup" and a "Not picked up in time" banner once discarded. Also bounces the lock icon and animates the pickup button's label changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
2.3 KiB
TypeScript
84 lines
2.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
formatCountdown,
|
|
formatDuration,
|
|
formatEtaClock,
|
|
formatEtaParts,
|
|
} from '#/lib/format.ts'
|
|
|
|
describe('formatDuration', () => {
|
|
it('rounds up to at least 1 minute', () => {
|
|
expect(formatDuration(0)).toBe('1 min')
|
|
expect(formatDuration(10)).toBe('1 min')
|
|
})
|
|
|
|
it('formats minutes under an hour', () => {
|
|
expect(formatDuration(90)).toBe('2 min')
|
|
expect(formatDuration(59 * 60)).toBe('59 min')
|
|
})
|
|
|
|
it('formats whole hours without minutes', () => {
|
|
expect(formatDuration(60 * 60)).toBe('1 hr')
|
|
expect(formatDuration(3 * 60 * 60)).toBe('3 hr')
|
|
})
|
|
|
|
it('formats hours with remaining minutes', () => {
|
|
expect(formatDuration(230 * 60)).toBe('3 hr 50 min')
|
|
})
|
|
})
|
|
|
|
describe('formatEtaClock', () => {
|
|
const noon = new Date(2026, 0, 15, 12, 0, 0)
|
|
|
|
it('labels a time later the same day as today', () => {
|
|
expect(formatEtaClock(30 * 60, noon)).toBe('today, 12:30 PM')
|
|
})
|
|
|
|
it('labels a time past midnight as tomorrow', () => {
|
|
expect(formatEtaClock(13 * 60 * 60, noon)).toBe('tomorrow, 01:00 AM')
|
|
})
|
|
|
|
it('labels a time two or more days out by weekday', () => {
|
|
expect(formatEtaClock(2 * 24 * 60 * 60, noon)).toBe('Saturday, 12:00 PM')
|
|
})
|
|
})
|
|
|
|
describe('formatEtaParts', () => {
|
|
const noon = new Date(2026, 0, 15, 12, 0, 0)
|
|
|
|
it('shows the relative time at or under the 3 hour threshold', () => {
|
|
expect(formatEtaParts(34 * 60, noon)).toEqual({
|
|
prefix: 'Ready in about',
|
|
value: '34 min',
|
|
})
|
|
expect(formatEtaParts(3 * 60 * 60, noon)).toEqual({
|
|
prefix: 'Ready in about',
|
|
value: '3 hr',
|
|
})
|
|
})
|
|
|
|
it('shows the absolute time once the eta passes 3 hours', () => {
|
|
expect(formatEtaParts(3 * 60 * 60 + 60, noon)).toEqual({
|
|
prefix: 'Ready',
|
|
value: 'today, 03:01 PM',
|
|
})
|
|
expect(formatEtaParts(230 * 60, noon)).toEqual({
|
|
prefix: 'Ready',
|
|
value: 'today, 03:50 PM',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('formatCountdown', () => {
|
|
it('shows minutes and zero-padded seconds, rounding up', () => {
|
|
expect(formatCountdown(15 * 60 * 1000)).toBe('15:00')
|
|
expect(formatCountdown(65_500)).toBe('1:06')
|
|
expect(formatCountdown(9_000)).toBe('0:09')
|
|
})
|
|
|
|
it('stops at zero once the time is up', () => {
|
|
expect(formatCountdown(-5_000)).toBe('0:00')
|
|
})
|
|
})
|