From 02f924292f91b0caff78a1efca443652e0eede6f Mon Sep 17 00:00:00 2001 From: antoshkaxxr Date: Sun, 22 Dec 2024 02:54:59 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B3=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0?= =?UTF-8?q?=20=D0=BA=20=D0=B7=D0=B0=D1=89=D0=B8=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/card/card.tsx | 30 +++-- .../cities-list/cities-list.test.tsx | 34 ++++-- src/components/cities-list/cities-list.tsx | 8 +- src/components/comment-form/comment-form.tsx | 2 +- .../custom-toast/custom-toast.test.tsx | 3 +- src/components/custom-toast/custom-toast.tsx | 6 +- .../favorite-card/favorite-card.tsx | 6 +- .../favorites-list/favorites-list.test.tsx | 22 +++- .../favorites-list/favorites-list.tsx | 20 +++- src/components/login-form/login-form.test.tsx | 6 +- src/components/login-form/login-form.tsx | 2 +- .../offers-list/offers-list.test.tsx | 4 +- src/components/offers-list/offers-list.tsx | 10 +- src/hooks/useMap/useMap.ts | 2 +- src/pages/favorites-page/favorites-page.tsx | 6 +- src/pages/login-page/login-page.test.tsx | 4 +- src/pages/login-page/login-page.tsx | 31 ++++-- src/pages/offer-page/offer-page.tsx | 11 +- src/services/api.ts | 2 +- src/store/city-data/selectors.test.ts | 33 ++++++ src/store/comments-data/selectors.test.ts | 48 ++++++++ src/store/offer-data/selectors.test.ts | 75 +++++++++++++ src/store/offers-data/selectors.test.ts | 68 ++++++++++++ src/store/user-data/selectors.test.ts | 105 ++++++++++++++++++ src/utils/create-random-state.ts | 30 +++++ src/utils/show-custom-toast.tsx | 6 + 26 files changed, 509 insertions(+), 65 deletions(-) create mode 100644 src/store/city-data/selectors.test.ts create mode 100644 src/store/comments-data/selectors.test.ts create mode 100644 src/store/offer-data/selectors.test.ts create mode 100644 src/store/offers-data/selectors.test.ts create mode 100644 src/store/user-data/selectors.test.ts create mode 100644 src/utils/create-random-state.ts create mode 100644 src/utils/show-custom-toast.tsx diff --git a/src/components/card/card.tsx b/src/components/card/card.tsx index 05805ac..bcc96ac 100644 --- a/src/components/card/card.tsx +++ b/src/components/card/card.tsx @@ -1,33 +1,43 @@ import {Offer} from '../../types/offer.ts'; import {Link} from 'react-router-dom'; import {AppRoute, AuthorizationStatus} from '../../const.ts'; -import {memo, useCallback} from 'react'; +import {useCallback} from 'react'; import {useAppDispatch, useAppSelector} from '../../hooks'; -import {changeFavoriteAction} from '../../store/api-actions.ts'; +import {changeFavoriteAction, fetchNearbyOffersAction} from '../../store/api-actions.ts'; import {getAuthorizationStatus} from '../../store/user-data/selectors.ts'; import {redirectToRoute} from '../../store/action.ts'; +import {showCustomToast} from '../../utils/show-custom-toast.tsx'; type CardProps = { offer: Offer; onMouseEnter: () => void; onMouseLeave: () => void; - isNearby?: boolean; + parentOfferId?: string; } -function CardComponent({offer, onMouseEnter, onMouseLeave, isNearby = false}: CardProps) { - const stylePrefix = isNearby ? 'near-places' : 'cities'; +export function Card({offer, onMouseEnter, onMouseLeave, parentOfferId = undefined}: CardProps) { + const stylePrefix = parentOfferId ? 'near-places' : 'cities'; const dispatch = useAppDispatch(); const authorizationStatus = useAppSelector(getAuthorizationStatus); - const handleFavoriteClick = useCallback(() => { + const handleFavoriteClick = useCallback(async () => { if (authorizationStatus !== AuthorizationStatus.Auth) { dispatch(redirectToRoute(AppRoute.Login)); return; } const newStatus = offer.isFavorite ? 0 : 1; - dispatch(changeFavoriteAction({ offerId: offer.id, status: newStatus })); - }, [authorizationStatus, offer.isFavorite, offer.id, dispatch]); + await dispatch(changeFavoriteAction({offerId: offer.id, status: newStatus})); + if (parentOfferId) { + dispatch(fetchNearbyOffersAction(parentOfferId)); + } + }, [authorizationStatus, offer.isFavorite, offer.id, dispatch, parentOfferId]); + + const handleClickWrapper = () => { + handleFavoriteClick().catch((error) => { + showCustomToast(`${error}`); + }); + }; return (
@@ -77,5 +87,3 @@ function CardComponent({offer, onMouseEnter, onMouseLeave, isNearby = false}: Ca
); } - -export const Card = memo(CardComponent); diff --git a/src/components/cities-list/cities-list.test.tsx b/src/components/cities-list/cities-list.test.tsx index 31c733d..b796e95 100644 --- a/src/components/cities-list/cities-list.test.tsx +++ b/src/components/cities-list/cities-list.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { BrowserRouter } from 'react-router-dom'; import { CitiesList } from './cities-list'; describe('Component: CitiesList', () => { @@ -9,11 +10,13 @@ describe('Component: CitiesList', () => { it('should render correctly', () => { render( - + + + ); mockCities.forEach((city) => { @@ -22,15 +25,24 @@ describe('Component: CitiesList', () => { const activeCityLink = screen.getByRole('link', { name: mockActiveCity }); expect(activeCityLink).toHaveClass('tabs__item--active'); + + mockCities + .filter((city) => city !== mockActiveCity) + .forEach((city) => { + const cityLink = screen.getByRole('link', { name: city }); + expect(cityLink).not.toHaveClass('tabs__item--active'); + }); }); it('should call onCityChange with the correct city when a city is clicked', async () => { render( - + + + ); const cityToClick = 'Cologne'; @@ -39,5 +51,7 @@ describe('Component: CitiesList', () => { await userEvent.click(cityLink); expect(mockOnCityChange).toHaveBeenCalledWith(cityToClick); + + expect(mockOnCityChange).toHaveBeenCalledTimes(1); }); }); diff --git a/src/components/cities-list/cities-list.tsx b/src/components/cities-list/cities-list.tsx index bfacdf1..62ba6d3 100644 --- a/src/components/cities-list/cities-list.tsx +++ b/src/components/cities-list/cities-list.tsx @@ -1,4 +1,5 @@ import {memo} from 'react'; +import {Link} from 'react-router-dom'; type CitiesListProps = { cities: string[]; @@ -12,16 +13,17 @@ function CitiesListComponent({ cities, activeCity, onCityChange }: CitiesListPro diff --git a/src/components/comment-form/comment-form.tsx b/src/components/comment-form/comment-form.tsx index 490438f..1364b15 100644 --- a/src/components/comment-form/comment-form.tsx +++ b/src/components/comment-form/comment-form.tsx @@ -1,7 +1,7 @@ import React, {ChangeEvent, FormEvent, memo, useCallback, useState} from 'react'; import {useAppDispatch} from '../../hooks'; import {postCommentAction} from '../../store/api-actions.ts'; -import {showCustomToast} from '../custom-toast/custom-toast.tsx'; +import {showCustomToast} from '../../utils/show-custom-toast.tsx'; const getRatingTitle = (star: number) => { switch (star) { diff --git a/src/components/custom-toast/custom-toast.test.tsx b/src/components/custom-toast/custom-toast.test.tsx index 59fa367..7981482 100644 --- a/src/components/custom-toast/custom-toast.test.tsx +++ b/src/components/custom-toast/custom-toast.test.tsx @@ -1,6 +1,7 @@ import { render, screen } from '@testing-library/react'; import { toast } from 'react-toastify'; -import {showCustomToast, CustomToastContainer, CustomToast} from './custom-toast'; +import {CustomToastContainer, CustomToast} from './custom-toast'; +import {showCustomToast} from '../../utils/show-custom-toast.tsx'; vi.mock('react-toastify', () => ({ toast: vi.fn(), diff --git a/src/components/custom-toast/custom-toast.tsx b/src/components/custom-toast/custom-toast.tsx index 79654c5..de2bf00 100644 --- a/src/components/custom-toast/custom-toast.tsx +++ b/src/components/custom-toast/custom-toast.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { ToastContainer, toast } from 'react-toastify'; +import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; export const CustomToast: React.FC<{ message: string }> = ({ message }) => ( @@ -8,10 +8,6 @@ export const CustomToast: React.FC<{ message: string }> = ({ message }) => ( ); -export const showCustomToast = (message: string) => { - toast(); -}; - export function CustomToastContainer() { return ( { dispatch(changeFavoriteAction({ offerId: offer.id, status: 0 })); - }, [offer.isFavorite, offer.id, dispatch]); + }, [offer.id, dispatch]); return (
@@ -23,9 +23,9 @@ function FavoriteCardComponent({offer}: FavoriteCardProps) { Premium }
- + Place image - +
diff --git a/src/components/favorites-list/favorites-list.test.tsx b/src/components/favorites-list/favorites-list.test.tsx index 9a73e1e..8fc5ad5 100644 --- a/src/components/favorites-list/favorites-list.test.tsx +++ b/src/components/favorites-list/favorites-list.test.tsx @@ -1,11 +1,17 @@ import { render, screen } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { configureMockStore } from '@jedmao/redux-mock-store'; import { FavoritesList } from './favorites-list'; import { Offer } from '../../types/offer'; +import {BrowserRouter} from 'react-router-dom'; vi.mock('../favorite-card/favorite-card', () => ({ - FavoriteCard: vi.fn(({ offer }: {offer: Offer}) =>
{offer.title}
), + FavoriteCard: vi.fn(({ offer }: { offer: Offer }) =>
{offer.title}
), })); +const mockStore = configureMockStore(); +const store = mockStore({}); + describe('Component: FavoritesList', () => { const mockOffers: Offer[] = [ { @@ -47,7 +53,13 @@ describe('Component: FavoritesList', () => { ]; it('should render grouped offers by city', () => { - render(); + render( + + + + + + ); expect(screen.getByText('Paris')).toBeInTheDocument(); expect(screen.getByText('Amsterdam')).toBeInTheDocument(); @@ -64,7 +76,11 @@ describe('Component: FavoritesList', () => { }); it('should render no offers if offers array is empty', () => { - render(); + render( + + + + ); expect(screen.queryByText(/Paris|Amsterdam/)).not.toBeInTheDocument(); expect(screen.queryByTestId('favorite-card')).not.toBeInTheDocument(); diff --git a/src/components/favorites-list/favorites-list.tsx b/src/components/favorites-list/favorites-list.tsx index 3f29621..c869c9b 100644 --- a/src/components/favorites-list/favorites-list.tsx +++ b/src/components/favorites-list/favorites-list.tsx @@ -1,6 +1,10 @@ -import {useMemo} from 'react'; +import {useCallback, useMemo} from 'react'; import {Offer} from '../../types/offer.ts'; import {FavoriteCard} from '../favorite-card/favorite-card.tsx'; +import {Link} from 'react-router-dom'; +import {AppRoute} from '../../const.ts'; +import {setCity} from '../../store/slices/city-slice.ts'; +import {useAppDispatch} from '../../hooks'; type FavoritesListProps = { offers: Offer[]; @@ -19,15 +23,25 @@ export function FavoritesList({offers}: FavoritesListProps) { return acc; }, {} as Record), [offers]); + const dispatch = useAppDispatch(); + + const handleCityClick = useCallback((city: string) => { + dispatch(setCity(city)); + }, [dispatch]); + return (
    {Object.entries(groupedOffers).map(([city, cityOffers]) => (
  • diff --git a/src/components/login-form/login-form.test.tsx b/src/components/login-form/login-form.test.tsx index 3520b32..7028115 100644 --- a/src/components/login-form/login-form.test.tsx +++ b/src/components/login-form/login-form.test.tsx @@ -2,10 +2,14 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { LoginForm } from './login-form'; import {loginAction} from '../../store/api-actions'; -import { showCustomToast } from '../custom-toast/custom-toast'; import {withHistory, withStore} from '../../utils/mock-component.tsx'; +import {showCustomToast} from '../../utils/show-custom-toast.tsx'; vi.mock('../custom-toast/custom-toast', () => ({ + CustomToast: vi.fn(() => null), +})); + +vi.mock('../../utils/show-custom-toast', () => ({ showCustomToast: vi.fn(), })); diff --git a/src/components/login-form/login-form.tsx b/src/components/login-form/login-form.tsx index 4cd3580..0926b8d 100644 --- a/src/components/login-form/login-form.tsx +++ b/src/components/login-form/login-form.tsx @@ -1,7 +1,7 @@ import {FormEvent, useRef, useCallback, memo} from 'react'; import { useAppDispatch } from '../../hooks'; import { loginAction } from '../../store/api-actions.ts'; -import { showCustomToast } from '../custom-toast/custom-toast.tsx'; +import {showCustomToast} from '../../utils/show-custom-toast.tsx'; const validatePassword = (password: string): boolean => { const hasLetter = /[a-zA-Z]/.test(password); diff --git a/src/components/offers-list/offers-list.test.tsx b/src/components/offers-list/offers-list.test.tsx index a6a11c0..881c6dc 100644 --- a/src/components/offers-list/offers-list.test.tsx +++ b/src/components/offers-list/offers-list.test.tsx @@ -64,12 +64,12 @@ describe('Component: OffersList', () => { expect(mockSetActiveOfferId).toHaveBeenCalledWith(null); }); - it('should render with "near-places__list places__list" class when isNearby is true', () => { + it('should render with "near-places__list places__list" class when isNearby', () => { render( ); diff --git a/src/components/offers-list/offers-list.tsx b/src/components/offers-list/offers-list.tsx index 56d9d99..7a36561 100644 --- a/src/components/offers-list/offers-list.tsx +++ b/src/components/offers-list/offers-list.tsx @@ -1,14 +1,13 @@ -import { memo } from 'react'; import { Offer } from '../../types/offer'; import { Card } from '../card/card'; type OffersListProps = { offers: Offer[]; setActiveOfferId: (id: string | null) => void; - isNearby?: boolean; + parentOfferId?: string; } -function OffersListComponent({ offers, setActiveOfferId, isNearby = false }: OffersListProps) { +export function OffersList({ offers, setActiveOfferId, parentOfferId = undefined }: OffersListProps) { const handleMouseEnter = (id: string) => { setActiveOfferId(id); }; @@ -17,7 +16,7 @@ function OffersListComponent({ offers, setActiveOfferId, isNearby = false }: Off setActiveOfferId(null); }; - const containerName = isNearby ? 'near-places__list places__list' : 'cities__places-list places__list tabs__content'; + const containerName = parentOfferId ? 'near-places__list places__list' : 'cities__places-list places__list tabs__content'; return (
    @@ -27,10 +26,9 @@ function OffersListComponent({ offers, setActiveOfferId, isNearby = false }: Off key={offer.id} onMouseEnter={() => handleMouseEnter(offer.id)} onMouseLeave={handleMouseLeave} + parentOfferId={parentOfferId} /> ))}
    ); } - -export const OffersList = memo(OffersListComponent); diff --git a/src/hooks/useMap/useMap.ts b/src/hooks/useMap/useMap.ts index 9b8ac23..414a1a7 100644 --- a/src/hooks/useMap/useMap.ts +++ b/src/hooks/useMap/useMap.ts @@ -40,7 +40,7 @@ export function useMap(mapRef: React.MutableRefObject, city: City) { } } - }, [mapRef, city]); + }, [mapRef, city, map]); return map; } diff --git a/src/pages/favorites-page/favorites-page.tsx b/src/pages/favorites-page/favorites-page.tsx index 5511e87..56ecc3b 100644 --- a/src/pages/favorites-page/favorites-page.tsx +++ b/src/pages/favorites-page/favorites-page.tsx @@ -3,6 +3,8 @@ import {FavoritesList} from '../../components/favorites-list/favorites-list.tsx' import {useAppSelector} from '../../hooks'; import {Header} from '../../components/header/header.tsx'; import {getFavoriteOffers} from '../../store/user-data/selectors.ts'; +import {Link} from 'react-router-dom'; +import {AppRoute} from '../../const.ts'; export function FavoritesPage() { const favoriteOffers = useAppSelector(getFavoriteOffers); @@ -35,9 +37,9 @@ export function FavoritesPage() {
); diff --git a/src/pages/login-page/login-page.test.tsx b/src/pages/login-page/login-page.test.tsx index 278f550..eb3826a 100644 --- a/src/pages/login-page/login-page.test.tsx +++ b/src/pages/login-page/login-page.test.tsx @@ -54,7 +54,7 @@ describe('LoginPage Component', () => { render(withStoreComponent); - expect(screen.getByText('Amsterdam')).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'Amsterdam' })).toHaveAttribute('href', '#'); + const locationLink = screen.getByRole('link-to-main'); + expect(locationLink).not.toBeNull(); }); }); diff --git a/src/pages/login-page/login-page.tsx b/src/pages/login-page/login-page.tsx index 5bc00d3..e2c9ecd 100644 --- a/src/pages/login-page/login-page.tsx +++ b/src/pages/login-page/login-page.tsx @@ -1,9 +1,21 @@ -import {Helmet} from 'react-helmet-async'; -import {LoginForm} from '../../components/login-form/login-form.tsx'; -import {Header} from '../../components/header/header.tsx'; -import {memo} from 'react'; +import { Helmet } from 'react-helmet-async'; +import { LoginForm } from '../../components/login-form/login-form.tsx'; +import { Header } from '../../components/header/header.tsx'; +import {memo, useCallback, useMemo} from 'react'; +import { Link } from 'react-router-dom'; +import {AppRoute, CITIES} from '../../const'; +import {useAppDispatch} from '../../hooks'; +import {setCity} from '../../store/slices/city-slice.ts'; function LoginPageComponent() { + const dispatch = useAppDispatch(); + + const randomCity = useMemo(() => CITIES[Math.floor(Math.random() * CITIES.length)], []); + + const handleCityClick = useCallback(() => { + dispatch(setCity(randomCity)); + }, [dispatch, randomCity]); + return (
@@ -16,9 +28,14 @@ function LoginPageComponent() {
- - Amsterdam - + + {randomCity} +
diff --git a/src/pages/offer-page/offer-page.tsx b/src/pages/offer-page/offer-page.tsx index e00e751..30a06d0 100644 --- a/src/pages/offer-page/offer-page.tsx +++ b/src/pages/offer-page/offer-page.tsx @@ -14,6 +14,7 @@ import {getComments} from '../../store/comments-data/selectors.ts'; import {AppRoute, AuthorizationStatus} from '../../const.ts'; import {redirectToRoute} from '../../store/action.ts'; import {getAuthorizationStatus} from '../../store/user-data/selectors.ts'; +import {showCustomToast} from '../../utils/show-custom-toast.tsx'; export function OfferPage() { const {id} = useParams<{ id: string }>(); @@ -63,6 +64,12 @@ export function OfferPage() { dispatch(fetchDetailedOfferAction(offer.id)); }; + const handleClickWrapper = () => { + handleFavoriteClick().catch((error) => { + showCustomToast(`${error}`); + }); + }; + return (
@@ -95,7 +102,7 @@ export function OfferPage() {
diff --git a/src/services/api.ts b/src/services/api.ts index b42a85c..083215f 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,7 +1,7 @@ import axios, {AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig} from 'axios'; import {getToken} from './token.ts'; import {StatusCodes} from 'http-status-codes'; -import {showCustomToast} from '../components/custom-toast/custom-toast.tsx'; +import {showCustomToast} from '../utils/show-custom-toast.tsx'; type DetailMessageType = { type: string; diff --git a/src/store/city-data/selectors.test.ts b/src/store/city-data/selectors.test.ts new file mode 100644 index 0000000..77ccf71 --- /dev/null +++ b/src/store/city-data/selectors.test.ts @@ -0,0 +1,33 @@ +import { expect } from 'vitest'; +import { NameSpace } from '../../const.ts'; +import {getActiveCity} from './selectors.ts'; +import {createRandomState} from '../../utils/create-random-state.ts'; + +describe('Selector: getActiveCity', () => { + it('should return the active city from the state', () => { + const state = createRandomState(); + + const result = getActiveCity(state); + expect(result).toBe('Paris'); + }); + + it('should return an empty string if activeCity is not set', () => { + const state = createRandomState(); + state[NameSpace.City].activeCity = ''; + + const result = getActiveCity(state); + expect(result).toBe(''); + }); + + it('should return the active city after its change', () => { + const state = createRandomState(); + + const result1 = getActiveCity(state); + expect(result1).toBe('Paris'); + + state[NameSpace.City].activeCity = 'Amsterdam'; + + const result2 = getActiveCity(state); + expect(result2).toBe('Amsterdam'); + }); +}); diff --git a/src/store/comments-data/selectors.test.ts b/src/store/comments-data/selectors.test.ts new file mode 100644 index 0000000..dd1525f --- /dev/null +++ b/src/store/comments-data/selectors.test.ts @@ -0,0 +1,48 @@ +import { expect } from 'vitest'; +import { getComments } from './selectors.ts'; +import { createRandomState } from '../../utils/create-random-state.ts'; +import { NameSpace } from '../../const.ts'; +import { Review } from '../../types/review.ts'; + +describe('Selector: getComments', () => { + it('should return the comments from the state', () => { + const state = createRandomState(); + + const result = getComments(state); + expect(result).toBeInstanceOf(Array); + expect(result.length).toBeGreaterThan(0); + }); + + it('should return an empty array if comments are not set', () => { + const state = createRandomState(); + + state[NameSpace.Comments].comments = []; + + const result = getComments(state); + expect(result).toEqual([]); + }); + + it('should return the updated comments after their change', () => { + const state = createRandomState(); + + const initialComments = getComments(state); + expect(initialComments.length).toBeGreaterThan(0); + + const newComment: Review = { + id: '123', + comment: 'New comment', + rating: 5, + date: '2023-10-01T12:00:00.000Z', + user: { + name: 'John Doe', + avatarUrl: 'https://example.com/avatar.png', + isPro: true, + }, + }; + state[NameSpace.Comments].comments = [...initialComments, newComment]; + + const updatedComments = getComments(state); + expect(updatedComments.length).toBe(initialComments.length + 1); + expect(updatedComments).toContainEqual(newComment); + }); +}); diff --git a/src/store/offer-data/selectors.test.ts b/src/store/offer-data/selectors.test.ts new file mode 100644 index 0000000..2ade242 --- /dev/null +++ b/src/store/offer-data/selectors.test.ts @@ -0,0 +1,75 @@ +import {createRandomState} from '../../utils/create-random-state.ts'; +import {getNearbyOffers, getOffer} from './selectors.ts'; +import {NameSpace} from '../../const.ts'; +import {createRandomDetailedOffer} from '../../utils/create-random-detailed-offer.ts'; +import {createRandomOffer} from '../../utils/create-random-offer.ts'; + +describe('Selector: getOffer', () => { + it('should return the detailed offer from the state', () => { + const state = createRandomState(); + + const result = getOffer(state); + expect(result).toBeInstanceOf(Object); + expect(result).toHaveProperty('id'); + expect(result).toHaveProperty('title'); + expect(result).toHaveProperty('description'); + }); + + it('should return null if the detailed offer is not set', () => { + const state = createRandomState(); + + state[NameSpace.Offer].offer = null; + + const result = getOffer(state); + expect(result).toBeNull(); + }); + + it('should return the updated offer after its change', () => { + const state = createRandomState(); + + const initialOffer = getOffer(state); + expect(initialOffer).toBeInstanceOf(Object); + + const newOffer = createRandomDetailedOffer(); + state[NameSpace.Offer].offer = newOffer; + + const updatedOffer = getOffer(state); + expect(updatedOffer).toEqual(newOffer); + }); +}); + +describe('Selector: getNearbyOffers', () => { + it('should return the nearby offers from the state', () => { + const state = createRandomState(); + + const result = getNearbyOffers(state); + expect(result).toBeInstanceOf(Array); + expect(result.length).toBeGreaterThan(0); + expect(result[0]).toHaveProperty('id'); + expect(result[0]).toHaveProperty('title'); + expect(result[0]).toHaveProperty('type'); + }); + + it('should return an empty array if nearby offers are not set', () => { + const state = createRandomState(); + + state[NameSpace.Offer].nearbyOffers = []; + + const result = getNearbyOffers(state); + expect(result).toEqual([]); + }); + + it('should return the updated nearby offers after their change', () => { + const state = createRandomState(); + + const initialNearbyOffers = getNearbyOffers(state); + expect(initialNearbyOffers.length).toBeGreaterThan(0); + + const newNearbyOffer = createRandomOffer(); + state[NameSpace.Offer].nearbyOffers = [...initialNearbyOffers, newNearbyOffer]; + + const updatedNearbyOffers = getNearbyOffers(state); + expect(updatedNearbyOffers.length).toBe(initialNearbyOffers.length + 1); + expect(updatedNearbyOffers).toContainEqual(newNearbyOffer); + }); +}); diff --git a/src/store/offers-data/selectors.test.ts b/src/store/offers-data/selectors.test.ts new file mode 100644 index 0000000..e6aafe1 --- /dev/null +++ b/src/store/offers-data/selectors.test.ts @@ -0,0 +1,68 @@ +import {createRandomState} from '../../utils/create-random-state.ts'; +import {getDataLoadingStatus, getOffers} from './selectors.ts'; +import {NameSpace} from '../../const.ts'; +import {createRandomOffer} from '../../utils/create-random-offer.ts'; + +describe('Selector: getOffers', () => { + it('should return the offers from the state', () => { + const state = createRandomState(); + + const result = getOffers(state); + expect(result).toBeInstanceOf(Array); + expect(result.length).toBeGreaterThan(0); + expect(result[0]).toHaveProperty('id'); + expect(result[0]).toHaveProperty('title'); + expect(result[0]).toHaveProperty('isPremium'); + }); + + it('should return an empty array if offers are not set', () => { + const state = createRandomState(); + + state[NameSpace.Offers].offers = []; + + const result = getOffers(state); + expect(result).toEqual([]); + }); + + it('should return the updated offers after their change', () => { + const state = createRandomState(); + + const initialOffers = getOffers(state); + expect(initialOffers.length).toBeGreaterThan(0); + + const newOffer = createRandomOffer(); + state[NameSpace.Offers].offers = [...initialOffers, newOffer]; + + const updatedOffers = getOffers(state); + expect(updatedOffers.length).toBe(initialOffers.length + 1); + expect(updatedOffers).toContainEqual(newOffer); + }); +}); + +describe('Selector: getDataLoadingStatus', () => { + it('should return the data loading status from the state', () => { + const state = createRandomState(); + + const result = getDataLoadingStatus(state); + expect(typeof result).toBe('boolean'); + }); + + it('should return false if data loading status is not set', () => { + const state = createRandomState(); + + const result = getDataLoadingStatus(state); + expect(result).toBe(false); + }); + + it('should return the updated data loading status after its change', () => { + const state = createRandomState(); + + const initialStatus = getDataLoadingStatus(state); + expect(typeof initialStatus).toBe('boolean'); + + state[NameSpace.Offers].isDataLoading = !initialStatus; + + const updatedStatus = getDataLoadingStatus(state); + expect(updatedStatus).toBe(!initialStatus); + }); +}); diff --git a/src/store/user-data/selectors.test.ts b/src/store/user-data/selectors.test.ts new file mode 100644 index 0000000..2b3f2e0 --- /dev/null +++ b/src/store/user-data/selectors.test.ts @@ -0,0 +1,105 @@ +import {getAuthorizationStatus, getFavoriteOffers, getUserInfo} from './selectors.ts'; +import {createRandomState} from '../../utils/create-random-state.ts'; +import {AuthorizationStatus, NameSpace} from '../../const.ts'; +import {createRandomUserInfo} from '../../utils/create-random-user-info.ts'; +import {createRandomOffer} from '../../utils/create-random-offer.ts'; + +describe('Selector: getAuthorizationStatus', () => { + it('should return the authorization status from the state', () => { + const state = createRandomState(); + + const result = getAuthorizationStatus(state); + expect(Object.values(AuthorizationStatus)).toContain(result); + }); + + it('should return "NoAuth" if authorization status is not set', () => { + const state = createRandomState(); + + state[NameSpace.User].authorizationStatus = AuthorizationStatus.NoAuth; + + const result = getAuthorizationStatus(state); + expect(result).toBe(AuthorizationStatus.NoAuth); + }); + + it('should return the updated authorization status after its change', () => { + const state = createRandomState(); + + const initialStatus = getAuthorizationStatus(state); + expect(Object.values(AuthorizationStatus)).toContain(initialStatus); + + state[NameSpace.User].authorizationStatus = AuthorizationStatus.NoAuth; + + const updatedStatus = getAuthorizationStatus(state); + expect(updatedStatus).toBe(AuthorizationStatus.NoAuth); + }); +}); + +describe('Selector: getUserInfo', () => { + it('should return the user info from the state', () => { + const state = createRandomState(); + + const result = getUserInfo(state); + expect(result).toBeInstanceOf(Object); + expect(result).toHaveProperty('name'); + expect(result).toHaveProperty('avatarUrl'); + expect(result).toHaveProperty('isPro'); + }); + + it('should return null if user info is not set', () => { + const state = createRandomState(); + + state[NameSpace.User].userInfo = null; + + const result = getUserInfo(state); + expect(result).toBeNull(); + }); + + it('should return the updated user info after its change', () => { + const state = createRandomState(); + + const initialUserInfo = getUserInfo(state); + expect(initialUserInfo).toBeInstanceOf(Object); + + const newUserInfo = createRandomUserInfo(); + state[NameSpace.User].userInfo = newUserInfo; + + const updatedUserInfo = getUserInfo(state); + expect(updatedUserInfo).toEqual(newUserInfo); + }); +}); + +describe('Selector: getFavoriteOffers', () => { + it('should return the favorite offers from the state', () => { + const state = createRandomState(); + + const result = getFavoriteOffers(state); + expect(result).toBeInstanceOf(Array); + expect(result.length).toBeGreaterThan(0); + expect(result[0]).toHaveProperty('id'); + expect(result[0]).toHaveProperty('title'); + expect(result[0]).toHaveProperty('isFavorite'); + }); + + it('should return an empty array if favorite offers are not set', () => { + const state = createRandomState(); + + state[NameSpace.User].favoriteOffers = []; + + const result = getFavoriteOffers(state); + expect(result).toEqual([]); + }); + + it('should return the updated favorite offers after their change', () => { + const state = createRandomState(); + + const initialFavoriteOffers = getFavoriteOffers(state); + expect(initialFavoriteOffers.length).toBeGreaterThan(0); + + const newFavoriteOffer = createRandomOffer(); + state[NameSpace.User].favoriteOffers = [...initialFavoriteOffers, newFavoriteOffer]; + + const updatedFavoriteOffers = getFavoriteOffers(state); + expect(updatedFavoriteOffers.length).toBe(initialFavoriteOffers.length + 1); + expect(updatedFavoriteOffers).toContainEqual(newFavoriteOffer); + }); +}); diff --git a/src/utils/create-random-state.ts b/src/utils/create-random-state.ts new file mode 100644 index 0000000..6a1e795 --- /dev/null +++ b/src/utils/create-random-state.ts @@ -0,0 +1,30 @@ +import {State} from '../types/state.ts'; +import {AuthorizationStatus, NameSpace} from '../const.ts'; +import {createRandomReview} from './create-random-review.ts'; +import {createRandomDetailedOffer} from './create-random-detailed-offer.ts'; +import {createRandomOffer} from './create-random-offer.ts'; +import {createRandomUserInfo} from './create-random-user-info.ts'; + +export function createRandomState() : State { + return { + [NameSpace.City]: { + activeCity: 'Paris', + }, + [NameSpace.Comments]: { + comments: [createRandomReview()], + }, + [NameSpace.Offer]: { + offer: createRandomDetailedOffer(), + nearbyOffers: [createRandomOffer(), createRandomOffer()], + }, + [NameSpace.Offers]: { + offers: [createRandomOffer(), createRandomOffer(), createRandomOffer()], + isDataLoading: false, + }, + [NameSpace.User]: { + authorizationStatus: AuthorizationStatus.Auth, + userInfo: createRandomUserInfo(), + favoriteOffers: [createRandomOffer(), createRandomOffer()], + }, + }; +} diff --git a/src/utils/show-custom-toast.tsx b/src/utils/show-custom-toast.tsx new file mode 100644 index 0000000..3180c10 --- /dev/null +++ b/src/utils/show-custom-toast.tsx @@ -0,0 +1,6 @@ +import {toast} from 'react-toastify'; +import {CustomToast} from '../components/custom-toast/custom-toast.tsx'; + +export const showCustomToast = (message: string) => { + toast(); +};