Skip to content

Commit

Permalink
Merge pull request #15 from strawberrycheeks/module9-task2
Browse files Browse the repository at this point in the history
  • Loading branch information
keksobot authored Jan 12, 2025
2 parents f41f039 + f2e903d commit c3cba1e
Show file tree
Hide file tree
Showing 17 changed files with 364 additions and 79 deletions.
20 changes: 10 additions & 10 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ jobs:
name: Check
runs-on: ubuntu-latest
steps:
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '20'

- uses: actions/checkout@master
name: Checkout
- uses: actions/checkout@master
name: Checkout

- name: Install dependencies
run: npm install
- name: Install dependencies
run: npm install

- name: Run checks
run: npm test && npm run lint
- name: Run checks
run: npm test && npm run lint
1 change: 1 addition & 0 deletions src/entities/offer-card/ui/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const OfferCard = (props: OfferCardProps) => {
)}
onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave}
data-offer-id={id}
>
{Boolean(isPremium) && (
<div className="place-card__mark">
Expand Down
1 change: 0 additions & 1 deletion src/features/city-places-list/index.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/features/city-places/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CityPlaces } from './ui/city-places';
File renamed without changes.
File renamed without changes.
177 changes: 177 additions & 0 deletions src/features/city-places/ui/city-places.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';

import { initAsyncActionsStore } from '@/app/lib/mocks';
import { cities } from '@/entities/city';
import { makeOffers } from '@/entities/offer-card/lib/mocks';
import { AuthorizationStatus, NameSpace } from '@/shared/model/enums';

import { SortVariant } from '../model/types';
import { CityPlaces } from './city-places';

describe('<CityPlaces />', () => {
const { mockStoreCreator } = initAsyncActionsStore();
let store: ReturnType<typeof mockStoreCreator>;

const offers = makeOffers();

beforeEach(() => {
store = mockStoreCreator({
[NameSpace.USER]: {
authorizationStatus: AuthorizationStatus.AUTH,
},
[NameSpace.OFFER]: {
activeOfferId: undefined,
},
});
});

it('should render correctly', () => {
const component = (
<Provider store={store}>
<BrowserRouter>
<CityPlaces city={cities.Paris} offers={offers} />
</BrowserRouter>
</Provider>
);

const { container } = render(component);

expect(container.querySelector('.cities')).toBeInTheDocument();
expect(container.querySelector('.cities__places')).toBeInTheDocument();
});

it('should render empty state if offers is empty', () => {
const component = (
<Provider store={store}>
<BrowserRouter>
<CityPlaces city={cities.Paris} offers={[]} />
</BrowserRouter>
</Provider>
);

render(component);

expect(screen.getByText('No places to stay available')).toBeInTheDocument();
});

it('should render first offer of array if default order', () => {
const component = (
<Provider store={store}>
<BrowserRouter>
<CityPlaces city={cities.Paris} offers={offers} />
</BrowserRouter>
</Provider>
);

const { container } = render(component);

const firstOfferId: string =
container.querySelector<HTMLElement>('.place-card[data-offer-id]')
?.dataset.offerId ?? '-1';
const targetOfferId = offers[0]?.id ?? -1;

expect(firstOfferId).toBe(targetOfferId);
});

it('should render most rated offer of array if top rated sorting', async () => {
const component = (
<Provider store={store}>
<BrowserRouter>
<CityPlaces city={cities.Paris} offers={offers} />
</BrowserRouter>
</Provider>
);

const { container } = render(component);

await userEvent.click(container.querySelector('.places__sorting-type')!);

const sortingOptions = container.querySelectorAll('.places__option');
let sortingOptionIndex = -1;
sortingOptions.forEach((option, index) => {
if (option.textContent === SortVariant.TOP_RATED) {
sortingOptionIndex = index;
}
});
await userEvent.click(
container.querySelectorAll('.places__option')[sortingOptionIndex]!,
);

const firstOfferId: string =
container.querySelector<HTMLElement>('.place-card[data-offer-id]')
?.dataset.offerId ?? '-1';
const targetOfferId =
offers.toSorted((a, b) => b.rating - a.rating)[0]?.id ?? -1;

expect(firstOfferId).toBe(targetOfferId);
});

it('should render most expensive offer of array if most expensive sorting', async () => {
const component = (
<Provider store={store}>
<BrowserRouter>
<CityPlaces city={cities.Paris} offers={offers} />
</BrowserRouter>
</Provider>
);

const { container } = render(component);

await userEvent.click(container.querySelector('.places__sorting-type')!);

const sortingOptions = container.querySelectorAll('.places__option');
let sortingOptionIndex = -1;
sortingOptions.forEach((option, index) => {
if (option.textContent === SortVariant.HIGH_TO_LOW) {
sortingOptionIndex = index;
}
});
await userEvent.click(
container.querySelectorAll('.places__option')[sortingOptionIndex]!,
);

const firstOfferId: string =
container.querySelector<HTMLElement>('.place-card[data-offer-id]')
?.dataset.offerId ?? '-1';
const targetOfferId =
offers.toSorted((a, b) => b.price - a.price)[0]?.id ?? -1;

expect(firstOfferId).toBe(targetOfferId);
});

it('should render most cheap offer of array if most cheap sorting', async () => {
const component = (
<Provider store={store}>
<BrowserRouter>
<CityPlaces city={cities.Paris} offers={offers} />
</BrowserRouter>
</Provider>
);

const { container } = render(component);

await userEvent.click(container.querySelector('.places__sorting-type')!);

const sortingOptions = container.querySelectorAll('.places__option');
let sortingOptionIndex = -1;
sortingOptions.forEach((option, index) => {
if (option.textContent === SortVariant.LOW_TO_HIGH) {
sortingOptionIndex = index;
}
});
await userEvent.click(
container.querySelectorAll('.places__option')[sortingOptionIndex]!,
);

const firstOfferId: string =
container.querySelector<HTMLElement>('.place-card[data-offer-id]')
?.dataset.offerId ?? '-1';
const targetOfferId =
offers.toSorted((a, b) => a.price - b.price)[0]?.id ?? -1;

expect(firstOfferId).toBe(targetOfferId);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { sortVariants } from '../model/consts';
import { SortVariant } from '../model/types';
import styles from './styles.module.css';

type CityPlacesListProps = { offers: OfferPreview[]; city: City };
type CityPlacesProps = { offers: OfferPreview[]; city: City };

export const CityPlacesList = (props: CityPlacesListProps) => {
export const CityPlaces = (props: CityPlacesProps) => {
const { offers, city } = props;

const [sortVariant, setSortVariant] = useState<SortVariant>(
Expand Down Expand Up @@ -57,6 +57,7 @@ export const CityPlacesList = (props: CityPlacesListProps) => {
options={sortVariants}
label="Sort by"
/>

<OffersList
offers={sortedOffers}
containerStyles="cities__places-list"
Expand Down
1 change: 1 addition & 0 deletions src/features/city-places/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CityPlaces } from './city-places';
64 changes: 1 addition & 63 deletions src/features/review-form/ui/index.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1 @@
import { useState } from 'react';

import { ReviewRating } from './ReviewRating';

type ReviewFormProps = {
onSubmit: ({ rating, comment }: { rating: number; comment: string }) => void;
};

export const ReviewForm = (props: ReviewFormProps) => {
const { onSubmit: submit } = props;

const [rating, setRating] = useState(0);
const [comment, setComment] = useState('');

const isSubmitDisabled =
rating === 0 || comment.length < 50 || comment.length > 300;

const handleTextChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setComment(event.target.value);
};

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
submit({ rating, comment });

setRating(0);
setComment('');
};

return (
<form className="reviews__form form" onSubmit={handleSubmit}>
<label className="reviews__label form__label" htmlFor="review">
Your review
</label>

<ReviewRating value={rating} onChange={setRating} />

<textarea
className="reviews__textarea form__textarea"
id="review"
name="review"
placeholder="Tell how was your stay, what you like and what can be improved"
value={comment}
onChange={handleTextChange}
/>

<div className="reviews__button-wrapper">
<p className="reviews__help">
To submit review please make sure to set{' '}
<span className="reviews__star">rating</span> and describe your stay
with at least <b className="reviews__text-amount">50 characters</b>.
</p>
<button
className="reviews__submit form__submit button"
type="submit"
disabled={isSubmitDisabled}
>
Submit
</button>
</div>
</form>
);
};
export { ReviewForm } from './review-form';
58 changes: 58 additions & 0 deletions src/features/review-form/ui/review-form.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { ReviewForm } from './review-form';

describe('<ReviewForm />', () => {
it('should render correctly', () => {
const onSubmit = vitest.fn();

const component = <ReviewForm onSubmit={onSubmit} />;

const { container } = render(component);

expect(screen.getByText('Your review')).toBeInTheDocument();
expect(container.querySelector('.reviews__form')).toBeInTheDocument();
});

it('button should be disabled by default', () => {
const onSubmit = vitest.fn();

const component = <ReviewForm onSubmit={onSubmit} />;

const { container } = render(component);

expect(
container.querySelector<HTMLButtonElement>('button[type="submit"]')
?.disabled,
).toBe(true);
});

it('should call onSubmit with correct data', async () => {
const onSubmit = vitest.fn();

const component = <ReviewForm onSubmit={onSubmit} />;

const { container } = render(component);

await userEvent.click(
container.querySelectorAll<HTMLInputElement>('.form__rating-input')[1]!,
);
await userEvent.click(container.querySelector('textarea')!);
const textToPaste = Array.from({ length: 51 })
.map(() => 'a')
.join('');
await userEvent.paste(textToPaste);

expect(
container.querySelector<HTMLButtonElement>('button[type="submit"]')
?.disabled,
).toBe(false);

await userEvent.click(
container.querySelector<HTMLButtonElement>('button[type="submit"]')!,
);

expect(onSubmit).toBeCalledWith({ rating: 4, comment: textToPaste });
});
});
Loading

0 comments on commit c3cba1e

Please sign in to comment.