Files
TREK/client/tests/unit/utils/formatters.test.ts
T
Maurice 64087512df fix(planner): the plan's costs follow the expenses in Costs
The day pills, Total Cost at the foot of the plan and the same figures in
the PDF added up the price column of every planned place. No form sets
that price since expenses replaced it, and nothing in Costs changes it,
so deleting an expense left the plan's total where it was, and a place
planned on two days counted twice.

With the Costs addon on, the plan now adds up the expenses: each one once,
on the day its booking starts, else the first day its place is planned,
else its date, and in the total either way. A foreign expense counts at
the rate frozen when it was entered and a refund nets against its day,
so Total Cost matches Total trip spend in Costs. With Costs off, a
place's own price still counts, once per place.

Closes #2551
2026-09-29 21:16:25 +02:00

121 lines
4.5 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { formatDate, formatTime, formatClockTime, currencyDecimals, parseMeridiemTime } from '../../../src/utils/formatters';
describe('currencyDecimals', () => {
it('returns 0 for zero-decimal currencies', () => {
expect(currencyDecimals('JPY')).toBe(0);
expect(currencyDecimals('KRW')).toBe(0);
expect(currencyDecimals('jpy')).toBe(0); // case-insensitive
});
it('returns 2 for standard currencies', () => {
expect(currencyDecimals('EUR')).toBe(2);
expect(currencyDecimals('USD')).toBe(2);
expect(currencyDecimals('GBP')).toBe(2);
});
});
describe('formatDate', () => {
it('returns null for null/undefined input', () => {
expect(formatDate(null, 'en-US')).toBeNull();
expect(formatDate(undefined, 'en-US')).toBeNull();
});
it('formats a date string and returns a non-empty string', () => {
const result = formatDate('2025-06-01', 'en-US');
expect(result).not.toBeNull();
expect(typeof result).toBe('string');
expect(result!.length).toBeGreaterThan(0);
});
it('accepts an optional timeZone parameter without throwing', () => {
const result = formatDate('2025-06-01', 'en-US', 'America/New_York');
expect(result).not.toBeNull();
});
});
describe('formatTime', () => {
it('returns empty string for null/undefined', () => {
expect(formatTime(null, 'en-US', '24h')).toBe('');
expect(formatTime(undefined, 'en-US', '24h')).toBe('');
});
it('formats 24h time', () => {
expect(formatTime('14:30', 'en-US', '24h')).toBe('14:30');
expect(formatTime('09:05', 'en-US', '24h')).toBe('09:05');
});
it('appends Uhr suffix for German locale in 24h mode', () => {
expect(formatTime('14:30', 'de-DE', '24h')).toBe('14:30 Uhr');
});
it('formats 12h time', () => {
expect(formatTime('14:30', 'en-US', '12h')).toBe('2:30 PM');
expect(formatTime('00:00', 'en-US', '12h')).toBe('12:00 AM');
expect(formatTime('12:00', 'en-US', '12h')).toBe('12:00 PM');
expect(formatTime('01:00', 'en-US', '12h')).toBe('1:00 AM');
});
// #1725 — a value stored with a meridiem must still follow the configured format
it('converts a stored meridiem value to the configured format', () => {
expect(formatTime('3:00 PM', 'en-US', '24h')).toBe('15:00');
expect(formatTime('12:30 am', 'en-US', '24h')).toBe('00:30');
expect(formatTime('3:00 PM', 'en-US', '12h')).toBe('3:00 PM');
expect(formatTime('15:00', 'de-DE', '24h')).toBe('15:00 Uhr');
});
});
describe('parseMeridiemTime', () => {
it('returns null when there is no meridiem to convert', () => {
expect(parseMeridiemTime(null)).toBeNull();
expect(parseMeridiemTime(undefined)).toBeNull();
expect(parseMeridiemTime('')).toBeNull();
expect(parseMeridiemTime('14:30')).toBeNull();
expect(parseMeridiemTime('later')).toBeNull();
});
it('converts a meridiem time to HH:MM', () => {
expect(parseMeridiemTime('5:30 pm')).toBe('17:30');
expect(parseMeridiemTime('5:30 AM')).toBe('05:30');
expect(parseMeridiemTime('530pm')).toBe('17:30');
expect(parseMeridiemTime('5pm')).toBe('17:00');
});
it('maps 12 AM to midnight and 12 PM to noon', () => {
expect(parseMeridiemTime('12:30 AM')).toBe('00:30');
expect(parseMeridiemTime('12:30 PM')).toBe('12:30');
});
});
// The locale-free counterpart of formatTime, shared by the time picker and the dense
// day/booking rows so those two do not carry their own copy of the rules (#1725).
describe('formatClockTime', () => {
it('returns an empty string for a missing value', () => {
expect(formatClockTime(null, false)).toBe('');
expect(formatClockTime(undefined, true)).toBe('');
expect(formatClockTime('', true)).toBe('');
});
it('leaves a 24h value untouched in 24h and never adds a locale suffix', () => {
expect(formatClockTime('14:30', false)).toBe('14:30');
expect(formatClockTime('09:05', false)).toBe('09:05');
});
it('converts a 24h value for a 12h user', () => {
expect(formatClockTime('14:30', true)).toBe('2:30 PM');
expect(formatClockTime('00:15', true)).toBe('12:15 AM');
expect(formatClockTime('12:45', true)).toBe('12:45 PM');
});
it('converts a stored meridiem value down to 24h', () => {
expect(formatClockTime('3:00 PM', false)).toBe('15:00');
expect(formatClockTime('12:30 am', false)).toBe('00:30');
expect(formatClockTime('3:00 PM', true)).toBe('3:00 PM');
});
it('hands back anything it cannot read', () => {
expect(formatClockTime('later', false)).toBe('later');
expect(formatClockTime('later', true)).toBe('later');
});
});