Unit тесты проверяют отдельные функции и обёртки в изоляции. При работе с timeago.js это означает тестирование утилит форматирования, обработки крайних случаев и пользовательских локалей.
// src/utils/timeago.ts
import { format } from 'timeago.js';
export function formatDate(date: Date | string | number | null, locale = 'ru'): string {
if (!date) return '';
const d = new Date(date as any);
if (isNaN(d.getTime())) return '';
return format(d, locale);
}
// src/utils/timeago.test.ts
import { formatDate } from './timeago';
const FIXED = new Date('2025-06-01T12:00:00Z').getTime();
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(FIXED);
});
afterEach(() => jest.useRealTimers());
describe('formatDate', () => {
it('возвращает пустую строку для null', () => {
expect(formatDate(null)).toBe('');
});
it('возвращает пустую строку для некорректной даты', () => {
expect(formatDate('not-a-date')).toBe('');
});
it('принимает Date объект', () => {
expect(formatDate(new Date(FIXED - 60_000))).toMatch(/минут/);
});
it('принимает timestamp', () => {
expect(formatDate(FIXED - 3600_000)).toMatch(/час/);
});
it('принимает ISO строку', () => {
expect(formatDate('2025-06-01T11:00:00Z')).toMatch(/час/);
});
it('использует locale ru по умолчанию', () => {
const result = formatDate(new Date(FIXED - 60_000));
expect(result).toMatch(/назад|только что/);
});
});
// src/utils/tryFormat.ts
import { format } from 'timeago.js';
type Ok = { ok: true; value: string };
type Err = { ok: false; error: string };
export function tryFormat(date: unknown, locale = 'ru'): Ok | Err {
if (date == null || date === '') {
return { ok: false, error: 'Empty date' };
}
const d = new Date(date as any);
if (isNaN(d.getTime())) {
return { ok: false, error: `Invalid date: ${date}` };
}
return { ok: true, value: format(d, locale) };
}
// src/utils/tryFormat.test.ts
import { tryFormat } from './tryFormat';
describe('tryFormat', () => {
it('возвращает ok: false для null', () => {
const result = tryFormat(null);
expect(result.ok).toBe(false);
});
it('возвращает ok: false для undefined', () => {
expect(tryFormat(undefined).ok).toBe(false);
});
it('возвращает ok: false для пустой строки', () => {
expect(tryFormat('').ok).toBe(false);
});
it('возвращает ok: false для невалидной даты', () => {
const result = tryFormat('abc');
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain('abc');
}
});
it('возвращает ok: true для валидной даты', () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-06-01').getTime());
const result = tryFormat('2025-05-31');
expect(result.ok).toBe(true);
if (result.ok) {
expect(typeof result.value).toBe('string');
}
jest.useRealTimers();
});
});
// src/utils/enrich.ts
import { format } from 'timeago.js';
export function enrichWithTimeAgo<T extends { createdAt: string }>(
items: T[],
locale = 'ru'
): Array<T & { timeAgo: string }> {
return items.map(item => ({
...item,
timeAgo: format(item.createdAt, locale),
}));
}
// src/utils/enrich.test.ts
import { enrichWithTimeAgo } from './enrich';
describe('enrichWithTimeAgo', () => {
it('добавляет поле timeAgo к каждому элементу', () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-06-01T12:00:00Z').getTime());
const items = [
{ id: 1, createdAt: '2025-06-01T11:00:00Z' },
{ id: 2, createdAt: '2025-06-01T10:00:00Z' },
];
const result = enrichWithTimeAgo(items);
expect(result).toHaveLength(2);
expect(typeof result[0].timeAgo).toBe('string');
expect(typeof result[1].timeAgo).toBe('string');
expect(result[0].id).toBe(1); // Исходные поля сохранены
jest.useRealTimers();
});
it('возвращает пустой массив для пустого ввода', () => {
expect(enrichWithTimeAgo([])).toEqual([]);
});
});
// src/locales/custom.ts
import { register } from 'timeago.js';
const FORMS: [string, string][] = [
['только что', 'сейчас'],
['%s секунду назад', 'через %s секунду'],
['%s секунды назад', 'через %s секунды'],
['%s секунд назад', 'через %s секунд'],
['минуту назад', 'через минуту'],
['%s минуты назад', 'через %s минуты'],
['%s минут назад', 'через %s минут'],
['час назад', 'через час'],
['%s часа назад', 'через %s часа'],
['%s часов назад', 'через %s часов'],
['день назад', 'через день'],
['%s дня назад', 'через %s дня'],
['%s дней назад', 'через %s дней'],
['год назад', 'через год'],
['%s лет назад', 'через %s лет'],
];
export const customLocale = (n: number, i: number): [string, string] => FORMS[i];
register('custom', customLocale);
// src/locales/custom.test.ts
import { customLocale } from './custom';
describe('customLocale', () => {
it('возвращает кортеж для каждого индекса от 0 до 14', () => {
for (let i = 0; i <= 14; i++) {
const result = customLocale(1, i);
expect(Array.isArray(result)).toBe(true);
expect(result).toHaveLength(2);
expect(typeof result[0]).toBe('string');
expect(typeof result[1]).toBe('string');
}
});
it('индекс 0 — "только что"', () => {
expect(customLocale(1, 0)[0]).toBe('только что');
});
it('индекс 4 — "минуту назад"', () => {
expect(customLocale(1, 4)[0]).toBe('минуту назад');
});
});
// src/utils/date.ts
export function isValidDate(value: unknown): value is Date | string | number {
if (value == null) return false;
const d = new Date(value as any);
return !isNaN(d.getTime());
}
// src/utils/date.test.ts
import { isValidDate } from './date';
describe('isValidDate', () => {
const valid = [
new Date(),
Date.now(),
'2025-01-01',
'2025-06-01T12:00:00Z',
0,
-1000,
];
const invalid = [
null,
undefined,
'',
'not-a-date',
'Jan 32',
NaN,
{},
[],
];
it.each(valid)('принимает %s как валидную дату', (value) => {
expect(isValidDate(value)).toBe(true);
});
it.each(invalid)('отклоняет %s как невалидную дату', (value) => {
expect(isValidDate(value)).toBe(false);
});
});
// src/utils/normalizeDate.ts
export function normalizeDate(str: string): string {
if (!str.includes('T') && !str.includes('Z') && !str.includes('+')) {
return str + 'T00:00:00Z';
}
if (str.includes('T') && !str.endsWith('Z') && !str.includes('+')) {
return str + 'Z';
}
return str;
}
// src/utils/normalizeDate.test.ts
import { normalizeDate } from './normalizeDate';
describe('normalizeDate', () => {
it('добавляет T00:00:00Z к строке только с датой', () => {
expect(normalizeDate('2025-06-01')).toBe('2025-06-01T00:00:00Z');
});
it('добавляет Z к строке с датой и временем без часового пояса', () => {
expect(normalizeDate('2025-06-01T12:00:00')).toBe('2025-06-01T12:00:00Z');
});
it('не изменяет строку с Z', () => {
expect(normalizeDate('2025-06-01T12:00:00Z')).toBe('2025-06-01T12:00:00Z');
});
it('не изменяет строку с явным offset', () => {
expect(normalizeDate('2025-06-01T12:00:00+03:00')).toBe('2025-06-01T12:00:00+03:00');
});
});
| Функция | Что покрыть |
|---|---|
| Обёртка format | null, undefined, некорректная дата, корректная дата |
| Пользовательская локаль | Все индексы 0-14, корректный формат кортежа |
| Валидатор дат | Граничные значения, типы, NaN |
| Result обёртка | ok: true и ok: false ветки |
| enrich функции | Сохранение исходных полей, добавление нового поля |