Тестирование кода с timeago.js требует контроля над временем, изоляции от реального DOM и проверки поведения таймеров. Существует несколько подходов в зависимости от уровня тестирования.
Тестируется не сама библиотека, а код, который её использует: правильность вызовов, корректная обработка крайних случаев, поведение компонентов при обновлении времени, корректная очистка при размонтировании.
| Уровень | Что тестируется | Инструмент |
|---|---|---|
| Unit | Обёртки, утилиты, хуки | Jest, Vitest |
| Интеграционный | Компоненты с реальным DOM | Testing Library |
| E2E | Полный рендер в браузере | Playwright, Cypress |
| Производительность | Количество таймеров, скорость update | performance.now(), DevTools |
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
moduleNameMapper: {
'^timeago.js$': '<rootDir>/__mocks__/timeago.js',
},
};
import { format } from '../src/utils/time';
describe('time utils', () => {
describe('format', () => {
it('форматирует дату в прошлом', () => { /* ... */ });
it('форматирует дату в будущем', () => { /* ... */ });
it('обрабатывает null', () => { /* ... */ });
});
describe('render lifecycle', () => {
it('вызывает cancel при размонтировании', () => { /* ... */ });
it('не утекает таймер при быстром размонтировании', () => { /* ... */ });
});
});
Для стабильных снэпшотов время замораживается перед тестами и восстанавливается после:
describe('timeago format', () => {
const NOW = new Date('2025-06-01T12:00:00Z').getTime();
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(NOW);
});
afterEach(() => {
jest.useRealTimers();
});
it('показывает "только что" для текущего времени', () => {
const result = format(new Date(), 'ru');
expect(result).toBe('только что');
});
it('показывает "5 минут назад" для времени 5 минут назад', () => {
const fiveMinutesAgo = new Date(NOW - 5 * 60 * 1000);
const result = format(fiveMinutesAgo, 'ru');
expect(result).toContain('минут');
});
});
import { maybeFormat } from '../src/utils/timeago';
describe('maybeFormat', () => {
it('возвращает null для null', () => {
expect(maybeFormat(null)).toBeNull();
});
it('возвращает null для undefined', () => {
expect(maybeFormat(undefined)).toBeNull();
});
it('возвращает null для некорректной строки даты', () => {
expect(maybeFormat('not-a-date')).toBeNull();
});
it('возвращает строку для валидной даты', () => {
expect(maybeFormat('2020-01-01')).toEqual(expect.any(String));
});
});
import { render, screen } from '@testing-library/react';
import { TimeAgo } from '../src/components/TimeAgo';
const FIXED_NOW = new Date('2025-06-01T12:00:00Z').getTime();
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(FIXED_NOW);
});
afterEach(() => {
jest.useRealTimers();
});
describe('TimeAgo component', () => {
it('рендерит относительное время', () => {
const date = new Date(FIXED_NOW - 2 * 60 * 1000); // 2 минуты назад
render(<TimeAgo date={date} />);
expect(screen.getByText(/минут/)).toBeInTheDocument();
});
it('обновляет текст при смене пропса date', () => {
const date1 = new Date(FIXED_NOW - 60_000);
const { rerender } = render(<TimeAgo date={date1} />);
const date2 = new Date(FIXED_NOW - 120_000);
rerender(<TimeAgo date={date2} />);
// Компонент должен отображать новое значение
});
});
import { render, unmountComponentAtNode } from 'react-dom';
import { act } from 'react-dom/test-utils';
it('отменяет render при размонтировании', () => {
const cancel = jest.fn();
jest.mock('timeago.js', () => ({
render: jest.fn(),
cancel,
format: jest.fn(() => 'mock'),
register: jest.fn(),
}));
const container = document.createElement('div');
document.body.appendChild(container);
act(() => { render(<LiveTimeAgo date={new Date()} />, container); });
act(() => { unmountComponentAtNode(container); });
expect(cancel).toHaveBeenCalled();
});
import { renderHook, act } from '@testing-library/react-hooks';
import { useTimeAgo } from '../src/hooks/useTimeAgo';
const FIXED = new Date('2025-06-01T12:00:00Z').getTime();
it('обновляет значение с интервалом', () => {
jest.useFakeTimers();
jest.setSystemTime(FIXED);
const { result } = renderHook(() => useTimeAgo(new Date(FIXED - 30_000)));
const firstValue = result.current;
// Перемотать время вперёд на 60 секунд
act(() => { jest.advanceTimersByTime(60_000); });
// Значение должно обновиться (или оставаться тем же — проверяем тип)
expect(typeof result.current).toBe('string');
jest.useRealTimers();
});
describe.each([
[new Date('2025-01-01'), 'ru', expect.any(String)],
[1_700_000_000_000, 'ru', expect.any(String)],
['2024-12-25T00:00:00Z', 'de', expect.any(String)],
])('format(%s, %s)', (date, locale, expected) => {
it(`возвращает строку для ${locale}`, () => {
expect(format(date, locale)).toEqual(expected);
});
});
it('соответствует снэпшоту для конкретного момента', () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-06-01T12:00:00Z').getTime());
const result = format(new Date('2025-05-31T12:00:00Z'), 'ru');
expect(result).toMatchSnapshot();
jest.useRealTimers();
});
Снэпшот фиксирует значение "1 день назад". При изменении
поведения библиотеки тест упадёт.
const LOCALES: Array<[string, RegExp]> = [
['ru', /назад|только что/],
['en_US', /ago|just now/],
['de', /vor|gerade/],
];
describe.each(LOCALES)('locale %s', (locale, pattern) => {
it(`возвращает строку, соответствующую паттерну`, () => {
jest.setSystemTime(new Date('2025-06-01').getTime());
const result = format(new Date('2025-05-30'), locale);
expect(result).toMatch(pattern);
});
});