Skip to content

Commit

Permalink
module9-task1
Browse files Browse the repository at this point in the history
  • Loading branch information
hanimohammad committed Dec 21, 2024
1 parent c92a8b2 commit 4a1eeea
Show file tree
Hide file tree
Showing 29 changed files with 674 additions and 3 deletions.
23 changes: 22 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"react-dom": "18.2.0",
"react-helmet-async": "1.3.0",
"react-redux": "8.1.3",
"react-router-dom": "6.16.0"
"react-router-dom": "6.16.0",
"react-toastify": "^11.0.2"
},
"devDependencies": {
"@jedmao/redux-mock-store": "3.0.5",
Expand Down
3 changes: 2 additions & 1 deletion src/action.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createAction } from '@reduxjs/toolkit';
import { OfferObject, OfferIdDetails } from './types/types';
import { OfferObject, OfferIdDetails, AppRoute } from './types/types';
export const changeCity = createAction<string>('ChangeCity');

export const AddOffer = createAction<OfferObject[]>('AddOffer');
Expand All @@ -9,3 +9,4 @@ export const loadOffers = createAction<OfferObject[]>('data/fetchOffers');
export const loadOfferDetails = createAction<OfferIdDetails>('data/loadOffer');

export const setOffer = createAction<OfferIdDetails>('offer/set');
export const redirectToRoute = createAction<AppRoute>('user/redirectToRoute');
159 changes: 159 additions & 0 deletions src/components/tests/app-router.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { createMemoryHistory, MemoryHistory } from 'history';
import { render, screen} from '@testing-library/react';
import { withHistory } from '../../shared/providers';
import { withStore } from '../../shared/providers/with-store';
import { makeFakeStore } from '../../shared/mocks';
import { App } from '../../App';
import { AppRoute, SortName } from '../../types/types';
import LoginPage from '../Login/LoginPage';
import { AuthorizationStatus } from '../../const';
import { Cities } from '../../shared/api';
import MainPage from '../MainPage/MainPage';
import Favorite from '../Favorites/Favorite';
import NotFoundPage from '../NotFoundPage/NotFoundPage';
import Offer from '../Offer/Offer';


describe('Application Routing', () => {
let mockHistory: MemoryHistory;

beforeEach(() => {
mockHistory = createMemoryHistory();
});

it('should render MainPage when user navigate to "/"', () => {
// eslint-disable-next-line react/jsx-no-undef
const withHistoryComponent = withHistory(<App />, mockHistory);
const { withStoreComponent } = withStore(withHistoryComponent, makeFakeStore());
mockHistory.push(AppRoute.Main);

render(withStoreComponent);

expect(screen.getByText(/Cities/i)).toBeInTheDocument();
expect(screen.getAllByTestId('location_item')).toHaveLength(Object.values(Cities).length);
});

it('should render LoginPage when user navigate to "/login"', () => {
const withHistoryComponent = withHistory(<LoginPage />, mockHistory);
const { withStoreComponent } = withStore(withHistoryComponent, makeFakeStore({ user: {
authorizationStatus:AuthorizationStatus.NoAuth,
user: null
},}));
mockHistory.push(AppRoute.Login);

render(withStoreComponent);

expect(screen.getAllByText(/Sign in/i)).toHaveLength(2);
expect(screen.getByTestId('location_item-link')).toBeInTheDocument();
});

it('should render MainPage when authenticated user navigate to "/login"', () => {
const withHistoryComponent = withHistory(<MainPage currentCity={{
title: '',
lat: 0,
lng: 0
}} cities={[]} offers={[]}
// eslint-disable-next-line react/jsx-closing-bracket-location
/>, mockHistory);
const { withStoreComponent } = withStore(withHistoryComponent, makeFakeStore());
mockHistory.push(AppRoute.Login);

render(withStoreComponent);

expect(screen.getByText(/Cities/i)).toBeInTheDocument();
expect(screen.getAllByTestId('location_item')).toHaveLength(Object.values(Cities).length);
});

it('should render FavoritesPage when user navigate to "/favorites"', () => {
const withHistoryComponent = withHistory(<Favorite offers={null} />, mockHistory);
const { withStoreComponent } = withStore(withHistoryComponent, makeFakeStore());
mockHistory.push(AppRoute.Favorites);

render(withStoreComponent);

expect(screen.getByText(/Saved listing/i)).toBeInTheDocument();
});

it('should render FavoritesPage when user navigate to "/favorites" with empty list', () => {
const withHistoryComponent = withHistory(<Favorite offers={null} />, mockHistory);
const { withStoreComponent } = withStore(withHistoryComponent, makeFakeStore({offer:{favorites:[],city: Cities.Paris,
offers: [],
nearOffers: [],
sort: SortName.popular,
isLoading: false,
offerOnPage:null}}));
mockHistory.push(AppRoute.Favorites);

render(withStoreComponent);

expect(screen.getByText(/Save properties to narrow down search or plan your future trips./i)).toBeInTheDocument();
});

it('should render NotFoundPage when user navigate to "/notFound"', () => {
const withHistoryComponent = withHistory(<NotFoundPage />, mockHistory);
const { withStoreComponent } = withStore(withHistoryComponent, makeFakeStore());
mockHistory.push('NotFoundPage');

render(withStoreComponent);

expect(screen.getByText(/404 Not Found/i)).toBeInTheDocument();
});

it('should render NotFoundPage when user navigate to non-existent path', () => {
const withHistoryComponent = withHistory(<NotFoundPage />, mockHistory);
const { withStoreComponent } = withStore(withHistoryComponent, makeFakeStore());
mockHistory.push('123321');

render(withStoreComponent);

expect(screen.getByText(/404 Not Found/i)).toBeInTheDocument();
});

it('should render OfferPage when user navigate to "/offer/a20a52b2-efc2-4b0f-9396-4bdfbe5e9543"', () => {
const withHistoryComponent = withHistory(<Offer offerdetails={{
id: '',
title: '',
type: '',
price: 0,
city: {
name: '',
location: {
latitude: 0,
longitude: 0,
zoom: 0
}
},
location: {
latitude: 0,
longitude: 0,
zoom: 0
},
isFavorite: false,
isPremium: false,
rating: 0,
description: '',
bedrooms: 0,
goods: [''],
host: {
name: '',
avatarUrl: '',
isPro: false
},
images: [''],
maxAdults: 0
}} offers={null} currentCity={{
title: '',
lat: 0,
lng: 0
// eslint-disable-next-line react/jsx-closing-bracket-location
}} />, mockHistory);
const { withStoreComponent } = withStore(withHistoryComponent, makeFakeStore());
mockHistory.push('/offer/a20a52b2-efc2-4b0f-9396-4bdfbe5e9543');

render(withStoreComponent);

expect(screen.getByText(/Wood and stone place/i)).toBeInTheDocument();
expect(screen.getByText(/A quiet cozy and picturesque that hides behind a a river by the unique lightness of Amsterdam./i)).toBeInTheDocument();
expect(screen.getByText(/Other places in the neighbourhood/i)).toBeInTheDocument();
});
});
5 changes: 5 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {createBrowserHistory} from 'history';

const browserHistory = createBrowserHistory();

export default browserHistory;
46 changes: 46 additions & 0 deletions src/middlewares.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { MockStore, configureMockStore } from '@jedmao/redux-mock-store';
import { AnyAction } from '@reduxjs/toolkit';

import { redirect } from './middlewares';
import { RootState } from './shared/lib/types';
import browserHistory from './config';
import { routesEnum } from './shared/config';
import { AppRoute } from './types/types';
import { redirectToRoute } from './action';

vi.mock('../../browser-history', () => ({
default: {
location: { pathname: '' },
push(path: string) {
this.location.pathname = path;
},
},
}));

describe('Redirect middleware', () => {
let store: MockStore;

beforeAll(() => {
const middleware = [redirect];
const mockStoreCreator = configureMockStore<RootState, AnyAction>(
middleware
);
store = mockStoreCreator();
});

beforeEach(() => {
browserHistory.push('');
});

it('should redirect to "/login" with redirectToRoute action', () => {
const redirectAction = redirectToRoute(AppRoute.Login);
store.dispatch(redirectAction);
expect(browserHistory.location.pathname).toBe(AppRoute.Login);
});

it('should not redirect to "/" with empty action', () => {
const emptyAction = { type: '', payload: routesEnum.MAIN };
store.dispatch(emptyAction);
expect(browserHistory.location.pathname).not.toBe(routesEnum.MAIN);
});
});
14 changes: 14 additions & 0 deletions src/middlewares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PayloadAction } from '@reduxjs/toolkit';
import { Middleware } from 'redux';

import browserHistory from './config';
import { ReducerType } from './types/types';

export const redirect: Middleware<unknown, ReducerType> =
() => (next) => (action: PayloadAction<string>) => {
if (action.type === 'user/redirectToRoute') {
browserHistory.push(action.payload);
}

return next(action);
};
1 change: 1 addition & 0 deletions src/shared/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Cities, $api } from './typicode';
47 changes: 47 additions & 0 deletions src/shared/api/typicode/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { API_URL, REQUEST_TIMEOUT } from '../../config';
import { StatusCodes } from 'http-status-codes';
import { toast } from 'react-toastify';
import { getToken } from './token';
import { ErrorMessageType } from '../../types';

const BadStatusCodesArray: (StatusCodes | string)[] = [
StatusCodes.BAD_REQUEST,
StatusCodes.UNAUTHORIZED,
StatusCodes.BAD_GATEWAY,
StatusCodes.INTERNAL_SERVER_ERROR,
'ERR_NETWORK',
'ERR_BAD_REQUEST',
'ECONNABORTED',
];

export const $api = axios.create({
timeout: REQUEST_TIMEOUT,
baseURL: API_URL,
});

$api.interceptors.request.use(
(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
const token = getToken();

if (token && config.headers) {
config.headers['x-token'] = token;
}

return config;
}
);

$api.interceptors.response.use(
(response) => response,
(error: AxiosError<ErrorMessageType>) => {
if (error.code && BadStatusCodesArray.includes(error.code)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
toast.warn('ERRORS WITH SERVER', {
position: 'top-left',
autoClose: 3000,
});
}
throw error;
}
);
8 changes: 8 additions & 0 deletions src/shared/api/typicode/cities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export enum Cities {
Paris = 'Paris',
Cologne ='Cologne',
Brussels ='Brussels',
Amsterdam ='Amsterdam',
Hamburg = 'Hamburg',
Dusseldorf = 'Dusseldorf'
}
2 changes: 2 additions & 0 deletions src/shared/api/typicode/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {Cities} from './cities';
export {$api} from './base';
16 changes: 16 additions & 0 deletions src/shared/api/typicode/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Token } from '../../types';

const AUTH_TOKEN_KEY_NAME = 'guess-six-cities-token';

export const getToken = (): Token => {
const token = localStorage.getItem(AUTH_TOKEN_KEY_NAME);
return token ?? '';
};

export const saveToken = (token: Token | undefined): void => {
localStorage.setItem(AUTH_TOKEN_KEY_NAME, token ?? '');
};

export const dropToken = (): void => {
localStorage.removeItem(AUTH_TOKEN_KEY_NAME);
};
10 changes: 10 additions & 0 deletions src/shared/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const API_URL = 'https://14.design.htmlacademy.pro/six-cities';
export const REQUEST_TIMEOUT = 3000;

export enum routesEnum {
LOGIN = '/login',
MAIN = '/',
FAVORITES = '/favorites',
OFFER = '/offer/:id',
NOT_FOUND = '/notFound'
}
Loading

0 comments on commit 4a1eeea

Please sign in to comment.