From 6de84f31439e4f7deca9d3cb86ea2f02c94c6d1e Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 13:48:50 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20React=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=BE?= =?UTF-8?q?=D0=B2.=20=D0=A2=D0=B5=D1=81=D1=82=D1=8B=20=D0=B1=D0=B0=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=B9=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8?= =?UTF-8?q?,=20=D0=93=D1=80=D0=B0=D0=BD=D0=B8=D1=87=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2,=20=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Button/Button.test.tsx | 34 ++++++++ src/shared/ui/Card/Card.test.tsx | 18 ++++ .../ui/CircleTimeline/CircleTimeline.test.tsx | 56 +++++++++++++ .../ui/EventsCarousel/EventsCarousel.test.tsx | 52 ++++++++++++ .../TimeFrameSlider/TimeFrameSlider.test.tsx | 82 +++++++++++++++++++ 5 files changed, 242 insertions(+) create mode 100644 src/shared/ui/Button/Button.test.tsx create mode 100644 src/shared/ui/Card/Card.test.tsx create mode 100644 src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.test.tsx create mode 100644 src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.test.tsx create mode 100644 src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.test.tsx diff --git a/src/shared/ui/Button/Button.test.tsx b/src/shared/ui/Button/Button.test.tsx new file mode 100644 index 0000000..bb0b329 --- /dev/null +++ b/src/shared/ui/Button/Button.test.tsx @@ -0,0 +1,34 @@ +import { fireEvent, render, screen } from '@testing-library/react' + +import { Button } from './Button' + +describe('Button', () => { + // Тест на рендеринг кнопки + it('должна рендериться корректно', () => { + render() + expect(screen.getByText('Test Button')).toBeInTheDocument() + }) + + // Тест на применение класса варианта + it('должна применять класс варианта', () => { + render() + const button = screen.getByText('Regular Button') + // Проверяем наличие класса, который генерируется CSS модулями (частичное совпадение) + expect(button.className).toMatch(/regular/) + }) + + // Тест на обработку клика + it('должна вызывать обработчик onClick при клике', () => { + const handleClick = jest.fn() + render() + + fireEvent.click(screen.getByText('Click Me')) + expect(handleClick).toHaveBeenCalledTimes(1) + }) + + // Тест на отключенное состояние + it('должна быть отключена при передаче пропса disabled', () => { + render() + expect(screen.getByText('Disabled Button')).toBeDisabled() + }) +}) diff --git a/src/shared/ui/Card/Card.test.tsx b/src/shared/ui/Card/Card.test.tsx new file mode 100644 index 0000000..e823dab --- /dev/null +++ b/src/shared/ui/Card/Card.test.tsx @@ -0,0 +1,18 @@ +import { render, screen } from '@testing-library/react' + +import { Card } from './Card' + +describe('Card', () => { + // Тест на рендеринг заголовка и описания + it('должна рендерить заголовок и описание', () => { + render() + expect(screen.getByText('1992')).toBeInTheDocument() + expect(screen.getByText('Test Description')).toBeInTheDocument() + }) + + // Тест на рендеринг числового заголовка + it('должна корректно рендерить числовой заголовок', () => { + render() + expect(screen.getByText('2023')).toBeInTheDocument() + }) +}) diff --git a/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.test.tsx b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.test.tsx new file mode 100644 index 0000000..60da310 --- /dev/null +++ b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.test.tsx @@ -0,0 +1,56 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import gsap from 'gsap' + +import { HISTORICAL_PERIODS } from '@/entities/TimePeriod' + +import { CircleTimeline } from './CircleTimeline' + +describe('CircleTimeline', () => { + const mockOnPeriodChange = jest.fn() + const defaultProps = { + periods: HISTORICAL_PERIODS, + activeIndex: 0, + onPeriodChange: mockOnPeriodChange, + rotation: 0, + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + // Тест на рендеринг правильного количества точек + it('должна рендерить правильное количество точек', () => { + render() + const points = screen.getAllByRole('button') + expect(points).toHaveLength(HISTORICAL_PERIODS.length) + }) + + // Тест на активную точку + it('должна корректно отображать активную точку', () => { + render() + const points = screen.getAllByRole('button') + + // Проверяем aria-current для доступности + expect(points[1]).toHaveAttribute('aria-current', 'true') + expect(points[0]).toHaveAttribute('aria-current', 'false') + }) + + // Тест на клик по точке + it('должна вызывать onPeriodChange при клике по точке', () => { + render() + const points = screen.getAllByRole('button') + + fireEvent.click(points[2]) + expect(mockOnPeriodChange).toHaveBeenCalledWith(2) + }) + + // Тест на вызов GSAP анимации + it('должна вызывать GSAP анимацию при изменении rotation', () => { + const { rerender } = render() + + rerender() + + // Проверяем, что gsap.to был вызван + expect(gsap.to).toHaveBeenCalled() + }) +}) diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.test.tsx b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.test.tsx new file mode 100644 index 0000000..f32eebd --- /dev/null +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.test.tsx @@ -0,0 +1,52 @@ +import { render, screen } from '@testing-library/react' + +import { EventsCarousel } from './EventsCarousel' + +// Мокаем Swiper +jest.mock('swiper/react', () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Swiper: ({ children }: any) =>
{children}
, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + SwiperSlide: ({ children }: any) => ( +
{children}
+ ), +})) + +jest.mock('swiper/modules', () => ({ + Navigation: jest.fn(), + Pagination: jest.fn(), + FreeMode: jest.fn(), +})) + +// Мокаем стили Swiper +jest.mock('swiper/css', () => ({})) +jest.mock('swiper/css/navigation', () => ({})) +jest.mock('swiper/css/pagination', () => ({})) + +describe('EventsCarousel', () => { + const mockEvents = [ + { id: 1, year: 1990, description: 'Event 1' }, + { id: 2, year: 1991, description: 'Event 2' }, + ] + + // Тест на рендеринг событий + it('должен рендерить переданные события', () => { + render() + + // Проверяем, что слайды отрендерились + const slides = screen.getAllByTestId('swiper-slide') + expect(slides).toHaveLength(mockEvents.length) + + // Проверяем контент внутри слайдов (Card компонент) + expect(screen.getByText('1990')).toBeInTheDocument() + expect(screen.getByText('Event 1')).toBeInTheDocument() + }) + + // Тест на видимость + it('должен применять класс visible, когда visible=true', () => { + render() + // В реальном компоненте класс применяется к контейнеру Swiper или обертке + // Здесь мы проверяем наличие контента, так как opacity управляется через CSS/GSAP + expect(screen.getByTestId('swiper')).toBeInTheDocument() + }) +}) diff --git a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.test.tsx b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.test.tsx new file mode 100644 index 0000000..ac91efb --- /dev/null +++ b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.test.tsx @@ -0,0 +1,82 @@ +import { fireEvent, render, screen } from '@testing-library/react' + +import { HISTORICAL_PERIODS } from '@/entities/TimePeriod' + +import { TimeFrameSlider } from './TimeFrameSlider' + +// Мокаем дочерние компоненты, чтобы тестировать изолированно +jest.mock('../CircleTimeline/CircleTimeline', () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + CircleTimeline: ({ activeIndex, onPeriodChange }: any) => ( +
+ + Active: {activeIndex} +
+ ), +})) + +jest.mock('../EventsCarousel/EventsCarousel', () => ({ + EventsCarousel: () =>
Carousel
, +})) + +describe('TimeFrameSlider', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + // Тест на рендеринг заголовка + it('должен рендерить заголовок', () => { + render() + expect(screen.getByText('Исторические даты')).toBeInTheDocument() + }) + + // Тест на отображение начального периода + it('должен отображать начальный период (первый в списке)', () => { + render() + const firstPeriod = HISTORICAL_PERIODS[0] + expect(screen.getByText(firstPeriod.yearFrom)).toBeInTheDocument() + expect(screen.getByText(firstPeriod.yearTo)).toBeInTheDocument() + }) + + // Тест на переключение вперед + it('должен переключать период вперед при клике на кнопку "Следующий"', () => { + render() + + const nextButton = screen.getByLabelText('Следующий период') + fireEvent.click(nextButton) + + // Проверяем, что отображается второй период + const secondPeriod = HISTORICAL_PERIODS[1] + expect(screen.getByText(secondPeriod.yearFrom)).toBeInTheDocument() + }) + + // Тест на переключение назад + it('должен переключать период назад при клике на кнопку "Предыдущий"', () => { + render() + + // Сначала переключаем вперед, чтобы не быть на первом элементе (хотя логика циклична) + const nextButton = screen.getByLabelText('Следующий период') + fireEvent.click(nextButton) + + // Теперь назад + const prevButton = screen.getByLabelText('Предыдущий период') + fireEvent.click(prevButton) + + // Должны вернуться к первому периоду + const firstPeriod = HISTORICAL_PERIODS[0] + expect(screen.getByText(firstPeriod.yearFrom)).toBeInTheDocument() + }) + + // Тест на пагинацию (точки) + it('должен переключать период при клике на точку пагинации', () => { + render() + + const dots = screen.getAllByLabelText(/Перейти к периоду/) + fireEvent.click(dots[2]) // Клик по 3-й точке + + const thirdPeriod = HISTORICAL_PERIODS[2] + expect(screen.getByText(thirdPeriod.yearFrom)).toBeInTheDocument() + }) +})