Интеграционные тесты проверяют совместную работу нескольких частей приложения: компонентов с реальным DOM, хуков с библиотекой, взаимодействие рендеринга с таймерами. В отличие от unit тестов, они не изолируют timeago.js — библиотека работает по-настоящему.
Unit тест: formatDate(null) возвращает ''.
Интеграционный тест: компонент
<TimeAgo date={null} /> рендерит заглушку, а не
падает.
Интеграционный тест ближе к реальному использованию. Он проверяет, что все части работают вместе.
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterFramework: ['@testing-library/jest-dom'],
};
npm install --save-dev @testing-library/react @testing-library/jest-dom
// src/components/TimeAgo.tsx
import { format } from 'timeago.js';
interface Props {
date: Date | string | number;
locale?: string;
}
export function TimeAgo({ date, locale = 'ru' }: Props) {
return <time dateTime={new Date(date as any).toISOString()}>{format(date, locale)}</time>;
}
// src/components/TimeAgo.test.tsx
import { render, screen } from '@testing-library/react';
import { TimeAgo } from './TimeAgo';
const FIXED = new Date('2025-06-01T12:00:00Z').getTime();
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(FIXED);
});
afterEach(() => jest.useRealTimers());
describe('TimeAgo component (integration)', () => {
it('рендерит элемент time с корректным datetime', () => {
const date = new Date(FIXED - 3600_000);
render(<TimeAgo date={date} />);
const el = screen.getByRole('time');
expect(el).toBeInTheDocument();
expect(el).toHaveAttribute('dateTime');
});
it('рендерит относительное время для 1 часа назад', () => {
render(<TimeAgo date={new Date(FIXED - 3600_000)} />);
expect(screen.getByText(/час/)).toBeInTheDocument();
});
it('рендерит с другой локалью', () => {
render(<TimeAgo date={new Date(FIXED - 3600_000)} locale="en_US" />);
expect(screen.getByText(/ago|hour/i)).toBeInTheDocument();
});
});
// src/components/LiveTimeAgo.tsx
import { useEffect, useRef } from 'react';
import { render, cancel } from 'timeago.js';
export function LiveTimeAgo({ date, locale = 'ru' }: { date: Date; locale?: string }) {
const ref = useRef<HTMLTimeElement>(null);
useEffect(() => {
if (!ref.current) return;
render(ref.current, locale);
return () => { if (ref.current) cancel(ref.current); };
}, [locale]);
return <time ref={ref} dateTime={date.toISOString()} />;
}
// src/components/LiveTimeAgo.test.tsx
import { render as rtlRender, screen, waitFor } from '@testing-library/react';
import { LiveTimeAgo } from './LiveTimeAgo';
const FIXED = new Date('2025-06-01T12:00:00Z').getTime();
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(FIXED);
});
afterEach(() => jest.useRealTimers());
it('элемент time получает текст после монтирования', async () => {
const date = new Date(FIXED - 60_000);
rtlRender(<LiveTimeAgo date={date} />);
await waitFor(() => {
const el = document.querySelector('time');
expect(el?.textContent).toBeTruthy();
});
});
it('не выбрасывает при размонтировании', () => {
const date = new Date(FIXED - 60_000);
const { unmount } = rtlRender(<LiveTimeAgo date={date} />);
expect(() => unmount()).not.toThrow();
});
// src/components/PostList.test.tsx
import { render, screen } from '@testing-library/react';
import { PostList } from './PostList';
const FIXED = new Date('2025-06-01T12:00:00Z').getTime();
jest.useFakeTimers();
jest.setSystemTime(FIXED);
const posts = [
{ id: 1, title: 'Первый пост', createdAt: new Date(FIXED - 3600_000).toISOString() },
{ id: 2, title: 'Второй пост', createdAt: new Date(FIXED - 86400_000).toISOString() },
];
it('рендерит все посты с временем', () => {
render(<PostList posts={posts} />);
expect(screen.getByText('Первый пост')).toBeInTheDocument();
expect(screen.getByText('Второй пост')).toBeInTheDocument();
const timeElements = document.querySelectorAll('time');
expect(timeElements).toHaveLength(2);
});
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AppWithLocale } from './AppWithLocale';
const FIXED = new Date('2025-06-01T12:00:00Z').getTime();
it('обновляет время при смене локали', async () => {
jest.useFakeTimers();
jest.setSystemTime(FIXED);
render(<AppWithLocale />);
// По умолчанию — русская локаль
expect(screen.getByText(/назад|только что/)).toBeInTheDocument();
// Переключить на английский
await userEvent.click(screen.getByRole('button', { name: /English/i }));
expect(screen.getByText(/ago|just now/i)).toBeInTheDocument();
jest.useRealTimers();
});
import { format, register } from 'timeago.js';
it('кастомная локаль отображает правильный текст', () => {
register('test', (n, i) => {
const forms: [string, string][] = [
['прямо сейчас', 'сейчас'],
['%s с назад', 'через %s с'],
];
return forms[i] ?? ['давно', 'потом'];
});
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-06-01T12:00:00Z').getTime());
const result = format(new Date('2025-06-01T11:59:59Z'), 'test');
expect(result).toContain('с назад');
jest.useRealTimers();
});
it('MutationObserver отслеживает добавленные элементы', (done) => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-06-01T12:00:00Z').getTime());
const container = document.createElement('div');
document.body.appendChild(container);
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node instanceof HTMLElement) {
const times = node.querySelectorAll('[datetime]');
if (times.length > 0) {
expect(times.length).toBe(1);
done();
}
}
});
});
});
observer.observe(container, { childList: true, subtree: true });
const el = document.createElement('div');
const time = document.createElement('time');
time.setAttribute('datetime', '2025-06-01T11:00:00Z');
el.appendChild(time);
container.appendChild(el);
observer.disconnect();
jest.useRealTimers();
});
Фокус: компонент монтируется без ошибок, отображает время, корректно очищает ресурсы.