Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ dist-ssr
*.sw?
.cursor
docs
coverage

*.pem
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "reactjs-template",
"name": "dreamteam-concierge-mini-app",
"private": true,
"version": "0.0.1",
"version": "1.0.1",
"type": "module",
"homepage": "https://caesai.github.io/dm_front/",
"scripts": {
Expand Down
33 changes: 33 additions & 0 deletions src/__mocks__/localStorage.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @fileoverview Мок для localStorage для использования в тестах.
*
* Предоставляет простую реализацию localStorage с возможностью очистки
* и перехвата вызовов для тестирования.
*/

export const localStorageMock = (() => {
let store: Record<string, string> = {};

return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => {
store[key] = value.toString();
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
},
};
})();

/**
* Устанавливает мок localStorage в window для тестов
*/
export const setupLocalStorageMock = () => {
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
writable: true,
});
};
2 changes: 1 addition & 1 deletion src/__tests__/booking/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ src/__tests__/
- Временные слоты
- Контактные данные
- Способ подтверждения
- Кнопка "Забронировать стол"
- Кнопка "Забронировать"
- Создание бронирования (с event_id)
- Редирект на онбординг
- Ошибки API
Expand Down
60 changes: 45 additions & 15 deletions src/__tests__/events/EventBookingPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ jest.mock('@/api/certificates.api.ts', () => ({
APIPostCertificateClaim: jest.fn(() => Promise.resolve({})),
}));

/**
* Мок useDataLoader. EventBookingPage вызывает loadEvents при перезагрузке
* (когда events пуст). В тестах данные задаются через атомы, мок предотвращает
* вызов APIGetEventsList.
*/
const mockLoadEvents = jest.fn();
jest.mock('@/hooks/useDataLoader.ts', () => ({
useDataLoader: () => ({ loadEvents: mockLoadEvents }),
}));

/**
* Мок Telegram WebApp объекта.
* Необходим для работы компонентов, использующих Telegram API.
Expand Down Expand Up @@ -225,6 +235,7 @@ describe('EventBookingPage', () => {
>
<Routes>
<Route path="/events/:eventId/booking" element={<EventBookingPage />} />
<Route path="/events" element={<div data-testid="events-list-page">Events List</div>} />
</Routes>
</MemoryRouter>
</TestProvider>
Expand Down Expand Up @@ -516,17 +527,17 @@ describe('EventBookingPage', () => {
// ============================================

/**
* Тесты кнопки "Забронировать стол".
* Тесты кнопки "Забронировать".
*/
describe('Кнопка бронирования', () => {
/**
* Проверяет наличие кнопки бронирования.
*/
test('должен отображать кнопку "Забронировать стол"', async () => {
test('должен отображать кнопку "Забронировать"', async () => {
renderComponent();

await waitFor(() => {
expect(screen.getByText('Забронировать стол')).toBeInTheDocument();
expect(screen.getByText('Забронировать')).toBeInTheDocument();
});
});

Expand All @@ -538,7 +549,7 @@ describe('EventBookingPage', () => {
renderComponent(mockUserData, mockEventsList, String(freeEvent.id), 0, 0);

await waitFor(() => {
const button = screen.getByText('Забронировать стол').closest('button');
const button = screen.getByText('Забронировать').closest('button');
expect(button).toBeDisabled();
});
});
Expand Down Expand Up @@ -570,7 +581,7 @@ describe('EventBookingPage', () => {
});

// Нажимаем кнопку бронирования
const bookButton = screen.getByText('Забронировать стол');
const bookButton = screen.getByText('Забронировать');

await act(async () => {
fireEvent.click(bookButton);
Expand Down Expand Up @@ -620,7 +631,7 @@ describe('EventBookingPage', () => {
await new Promise(resolve => setTimeout(resolve, 100));
});

const bookButton = screen.getByText('Забронировать стол');
const bookButton = screen.getByText('Забронировать');

await act(async () => {
fireEvent.click(bookButton);
Expand Down Expand Up @@ -662,7 +673,7 @@ describe('EventBookingPage', () => {
await new Promise(resolve => setTimeout(resolve, 100));
});

const bookButton = screen.getByText('Забронировать стол');
const bookButton = screen.getByText('Забронировать');

await act(async () => {
fireEvent.click(bookButton);
Expand All @@ -687,29 +698,48 @@ describe('EventBookingPage', () => {

/**
* Тесты поведения при отсутствии данных мероприятия.
* При перезагрузке страницы или неверном eventId данные восстанавливаются через API.
* Если мероприятие не найдено после загрузки — редирект на /events.
*/
describe('Отсутствие мероприятия', () => {
/**
* Проверяет что страница не падает при отсутствии мероприятия в списке.
* Проверяет редирект на /events при несуществующем eventId.
* Мероприятие не найдено в списке → редирект на список мероприятий.
*/
test('должен корректно обрабатывать отсутствие мероприятия', async () => {
test('должен перенаправлять на /events при отсутствии мероприятия в списке', async () => {
renderComponent(mockUserData, mockEventsList, '99999', 0, 0);

// Страница должна отрендериться без ошибок
await waitFor(() => {
expect(screen.getByText('Забронировать стол')).toBeInTheDocument();
expect(screen.getByTestId('events-list-page')).toBeInTheDocument();
expect(screen.queryByText('Забронировать')).not.toBeInTheDocument();
});
});

/**
* Проверяет что страница корректно обрабатывает пустой список мероприятий.
* Проверяет редирект на /events при пустом списке мероприятий.
* События загружены ([]), но мероприятие не найдено → редирект.
*/
test('должен корректно обрабатывать пустой список мероприятий', async () => {
test('должен перенаправлять на /events при пустом списке мероприятий', async () => {
renderComponent(mockUserData, [], String(freeEvent.id), 0, 0);

await waitFor(() => {
expect(screen.getByText('Забронировать стол')).toBeInTheDocument();
expect(screen.getByTestId('events-list-page')).toBeInTheDocument();
expect(screen.queryByText('Забронировать')).not.toBeInTheDocument();
});
});

/**
* Проверяет отображение скелетона при events === null (перезагрузка страницы).
* Вызывается loadEvents; пока данные не загружены — скелетон.
*/
test('должен показывать скелетон при загрузке данных (events === null)', async () => {
renderComponent(mockUserData, null, String(freeEvent.id), 0, 0);

await waitFor(() => {
expect(screen.getAllByTestId('placeholder-block').length).toBeGreaterThan(0);
expect(screen.queryByText('Забронировать')).not.toBeInTheDocument();
});
expect(mockLoadEvents).toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -753,7 +783,7 @@ describe('EventBookingPage', () => {
await new Promise(resolve => setTimeout(resolve, 100));
});

const bookButton = screen.getByText('Забронировать стол');
const bookButton = screen.getByText('Забронировать');

await act(async () => {
fireEvent.click(bookButton);
Expand Down
Loading